mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-13 06:34:39 -04:00
do some backend stuff that needs to be touched later again!!
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
parent
90170a1107
commit
c4a66ba671
37 changed files with 189 additions and 1063 deletions
|
@ -6,7 +6,7 @@
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { IsDate, IsNumber, IsOptional, IsString } from 'class-validator';
|
import { IsDate, IsNumber, IsOptional, IsString } from 'class-validator';
|
||||||
|
|
||||||
import { BaseDto } from '../utils/base.dto.';
|
import { BaseDto } from '../utils/base.dto';
|
||||||
import { TimestampMillis } from '../utils/timestamp';
|
import { TimestampMillis } from '../utils/timestamp';
|
||||||
|
|
||||||
export class ApiTokenDto extends BaseDto {
|
export class ApiTokenDto extends BaseDto {
|
||||||
|
|
64
backend/src/api/private/explore/explore.controller.ts
Normal file
64
backend/src/api/private/explore/explore.controller.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
|
||||||
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
|
import { Note } from 'src/notes/note.entity';
|
||||||
|
|
||||||
|
import { SessionGuard } from '../../../auth/session.guard';
|
||||||
|
import { ConsoleLoggerService } from '../../../logger/console-logger.service';
|
||||||
|
import { NoteExploreEntryDto } from '../../../notes/note-explore-entry.dto';
|
||||||
|
import { NotesService } from '../../../notes/notes.service';
|
||||||
|
import { User } from '../../../users/user.entity';
|
||||||
|
import { OpenApi } from '../../utils/openapi.decorator';
|
||||||
|
import { RequestUser } from '../../utils/request-user.decorator';
|
||||||
|
|
||||||
|
@UseGuards(SessionGuard)
|
||||||
|
@OpenApi(401, 403)
|
||||||
|
@ApiTags('explore')
|
||||||
|
@Controller('explore')
|
||||||
|
export class ExploreController {
|
||||||
|
constructor(
|
||||||
|
private readonly logger: ConsoleLoggerService,
|
||||||
|
private notesService: NotesService,
|
||||||
|
) {
|
||||||
|
this.logger.setContext(ExploreController.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('my')
|
||||||
|
@OpenApi(200)
|
||||||
|
async getMyNotes(
|
||||||
|
@RequestUser() user: User,
|
||||||
|
@Query('sort') sort: string,
|
||||||
|
@Query('search') search: string,
|
||||||
|
@Query('type') type: string,
|
||||||
|
): Promise<NoteExploreEntryDto[]> {
|
||||||
|
const entries = (await user.ownedNotes).map((aNote: Note) =>
|
||||||
|
this.notesService.toNoteExploreEntryDto(aNote, user),
|
||||||
|
);
|
||||||
|
return await Promise.all(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('shared')
|
||||||
|
@OpenApi(200)
|
||||||
|
async getSharedNotes(
|
||||||
|
@RequestUser() user: User,
|
||||||
|
@Query('sort') sort: string,
|
||||||
|
@Query('search') search: string,
|
||||||
|
@Query('type') type: string,
|
||||||
|
): Promise<NoteExploreEntryDto[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('public')
|
||||||
|
@OpenApi(200)
|
||||||
|
async getPublicNotes(
|
||||||
|
@Query('sort') sort: string,
|
||||||
|
@Query('search') search: string,
|
||||||
|
@Query('type') type: string,
|
||||||
|
): Promise<NoteExploreEntryDto[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,92 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import {
|
|
||||||
Body,
|
|
||||||
Controller,
|
|
||||||
Delete,
|
|
||||||
Get,
|
|
||||||
Post,
|
|
||||||
Put,
|
|
||||||
UseGuards,
|
|
||||||
UseInterceptors,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
|
||||||
|
|
||||||
import { SessionGuard } from '../../../../auth/session.guard';
|
|
||||||
import { HistoryEntryImportListDto } from '../../../../history/history-entry-import.dto';
|
|
||||||
import { HistoryEntryUpdateDto } from '../../../../history/history-entry-update.dto';
|
|
||||||
import { HistoryEntryDto } from '../../../../history/history-entry.dto';
|
|
||||||
import { HistoryService } from '../../../../history/history.service';
|
|
||||||
import { ConsoleLoggerService } from '../../../../logger/console-logger.service';
|
|
||||||
import { Note } from '../../../../notes/note.entity';
|
|
||||||
import { User } from '../../../../users/user.entity';
|
|
||||||
import { GetNoteInterceptor } from '../../../utils/get-note.interceptor';
|
|
||||||
import { OpenApi } from '../../../utils/openapi.decorator';
|
|
||||||
import { RequestNote } from '../../../utils/request-note.decorator';
|
|
||||||
import { RequestUser } from '../../../utils/request-user.decorator';
|
|
||||||
|
|
||||||
@UseGuards(SessionGuard)
|
|
||||||
@OpenApi(401)
|
|
||||||
@ApiTags('history')
|
|
||||||
@Controller('/me/history')
|
|
||||||
export class HistoryController {
|
|
||||||
constructor(
|
|
||||||
private readonly logger: ConsoleLoggerService,
|
|
||||||
private historyService: HistoryService,
|
|
||||||
) {
|
|
||||||
this.logger.setContext(HistoryController.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
@OpenApi(200, 404)
|
|
||||||
async getHistory(@RequestUser() user: User): Promise<HistoryEntryDto[]> {
|
|
||||||
const foundEntries = await this.historyService.getEntriesByUser(user);
|
|
||||||
return await Promise.all(
|
|
||||||
foundEntries.map((entry) => this.historyService.toHistoryEntryDto(entry)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post()
|
|
||||||
@OpenApi(201, 404)
|
|
||||||
async setHistory(
|
|
||||||
@RequestUser() user: User,
|
|
||||||
@Body() historyImport: HistoryEntryImportListDto,
|
|
||||||
): Promise<void> {
|
|
||||||
await this.historyService.setHistory(user, historyImport.history);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete()
|
|
||||||
@OpenApi(204, 404)
|
|
||||||
async deleteHistory(@RequestUser() user: User): Promise<void> {
|
|
||||||
await this.historyService.deleteHistory(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Put(':noteIdOrAlias')
|
|
||||||
@OpenApi(200, 404)
|
|
||||||
@UseInterceptors(GetNoteInterceptor)
|
|
||||||
async updateHistoryEntry(
|
|
||||||
@RequestNote() note: Note,
|
|
||||||
@RequestUser() user: User,
|
|
||||||
@Body() entryUpdateDto: HistoryEntryUpdateDto,
|
|
||||||
): Promise<HistoryEntryDto> {
|
|
||||||
const newEntry = await this.historyService.updateHistoryEntry(
|
|
||||||
note,
|
|
||||||
user,
|
|
||||||
entryUpdateDto,
|
|
||||||
);
|
|
||||||
return await this.historyService.toHistoryEntryDto(newEntry);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete(':noteIdOrAlias')
|
|
||||||
@OpenApi(204, 404)
|
|
||||||
@UseInterceptors(GetNoteInterceptor)
|
|
||||||
async deleteHistoryEntry(
|
|
||||||
@RequestNote() note: Note,
|
|
||||||
@RequestUser() user: User,
|
|
||||||
): Promise<void> {
|
|
||||||
await this.historyService.deleteHistoryEntry(note, user);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -9,7 +9,6 @@ import { ApiTokenModule } from '../../api-token/api-token.module';
|
||||||
import { AuthModule } from '../../auth/auth.module';
|
import { AuthModule } from '../../auth/auth.module';
|
||||||
import { FrontendConfigModule } from '../../frontend-config/frontend-config.module';
|
import { FrontendConfigModule } from '../../frontend-config/frontend-config.module';
|
||||||
import { GroupsModule } from '../../groups/groups.module';
|
import { GroupsModule } from '../../groups/groups.module';
|
||||||
import { HistoryModule } from '../../history/history.module';
|
|
||||||
import { LoggerModule } from '../../logger/logger.module';
|
import { LoggerModule } from '../../logger/logger.module';
|
||||||
import { MediaModule } from '../../media/media.module';
|
import { MediaModule } from '../../media/media.module';
|
||||||
import { NotesModule } from '../../notes/notes.module';
|
import { NotesModule } from '../../notes/notes.module';
|
||||||
|
@ -23,7 +22,6 @@ import { LocalController } from './auth/local/local.controller';
|
||||||
import { OidcController } from './auth/oidc/oidc.controller';
|
import { OidcController } from './auth/oidc/oidc.controller';
|
||||||
import { ConfigController } from './config/config.controller';
|
import { ConfigController } from './config/config.controller';
|
||||||
import { GroupsController } from './groups/groups.controller';
|
import { GroupsController } from './groups/groups.controller';
|
||||||
import { HistoryController } from './me/history/history.controller';
|
|
||||||
import { MeController } from './me/me.controller';
|
import { MeController } from './me/me.controller';
|
||||||
import { MediaController } from './media/media.controller';
|
import { MediaController } from './media/media.controller';
|
||||||
import { NotesController } from './notes/notes.controller';
|
import { NotesController } from './notes/notes.controller';
|
||||||
|
@ -36,7 +34,6 @@ import { UsersController } from './users/users.controller';
|
||||||
UsersModule,
|
UsersModule,
|
||||||
ApiTokenModule,
|
ApiTokenModule,
|
||||||
FrontendConfigModule,
|
FrontendConfigModule,
|
||||||
HistoryModule,
|
|
||||||
PermissionsModule,
|
PermissionsModule,
|
||||||
NotesModule,
|
NotesModule,
|
||||||
MediaModule,
|
MediaModule,
|
||||||
|
@ -48,7 +45,6 @@ import { UsersController } from './users/users.controller';
|
||||||
ApiTokensController,
|
ApiTokensController,
|
||||||
ConfigController,
|
ConfigController,
|
||||||
MediaController,
|
MediaController,
|
||||||
HistoryController,
|
|
||||||
MeController,
|
MeController,
|
||||||
NotesController,
|
NotesController,
|
||||||
AliasController,
|
AliasController,
|
||||||
|
|
|
@ -17,7 +17,7 @@ import {
|
||||||
ApiUnauthorizedResponse,
|
ApiUnauthorizedResponse,
|
||||||
} from '@nestjs/swagger';
|
} from '@nestjs/swagger';
|
||||||
|
|
||||||
import { BaseDto } from '../../utils/base.dto.';
|
import { BaseDto } from '../../utils/base.dto';
|
||||||
import {
|
import {
|
||||||
badRequestDescription,
|
badRequestDescription,
|
||||||
conflictDescription,
|
conflictDescription,
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
import { IsOptional, IsString } from 'class-validator';
|
import { IsOptional, IsString } from 'class-validator';
|
||||||
|
|
||||||
import { BaseDto } from '../utils/base.dto.';
|
import { BaseDto } from '../utils/base.dto';
|
||||||
|
|
||||||
export class PendingUserConfirmationDto extends BaseDto {
|
export class PendingUserConfirmationDto extends BaseDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|
5
backend/src/explore/explore.service.ts
Normal file
5
backend/src/explore/explore.service.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
|
@ -18,7 +18,7 @@ import { URL } from 'url';
|
||||||
import { ProviderType } from '../auth/provider-type.enum';
|
import { ProviderType } from '../auth/provider-type.enum';
|
||||||
import { GuestAccess } from '../config/guest_access.enum';
|
import { GuestAccess } from '../config/guest_access.enum';
|
||||||
import { ServerVersion } from '../monitoring/server-status.dto';
|
import { ServerVersion } from '../monitoring/server-status.dto';
|
||||||
import { BaseDto } from '../utils/base.dto.';
|
import { BaseDto } from '../utils/base.dto';
|
||||||
|
|
||||||
export type AuthProviderTypeWithCustomName =
|
export type AuthProviderTypeWithCustomName =
|
||||||
| ProviderType.LDAP
|
| ProviderType.LDAP
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsBoolean, IsString } from 'class-validator';
|
import { IsBoolean, IsString } from 'class-validator';
|
||||||
|
|
||||||
import { BaseDto } from '../utils/base.dto.';
|
import { BaseDto } from '../utils/base.dto';
|
||||||
|
|
||||||
export class GroupInfoDto extends BaseDto {
|
export class GroupInfoDto extends BaseDto {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import { Type } from 'class-transformer';
|
|
||||||
import {
|
|
||||||
IsArray,
|
|
||||||
IsBoolean,
|
|
||||||
IsDate,
|
|
||||||
IsString,
|
|
||||||
ValidateNested,
|
|
||||||
} from 'class-validator';
|
|
||||||
// This needs to be here because of weird import-behaviour during tests
|
|
||||||
import 'reflect-metadata';
|
|
||||||
|
|
||||||
import { BaseDto } from '../utils/base.dto.';
|
|
||||||
|
|
||||||
export class HistoryEntryImportDto extends BaseDto {
|
|
||||||
/**
|
|
||||||
* ID or Alias of the note
|
|
||||||
*/
|
|
||||||
@IsString()
|
|
||||||
note: string;
|
|
||||||
/**
|
|
||||||
* True if the note should be pinned
|
|
||||||
* @example true
|
|
||||||
*/
|
|
||||||
@IsBoolean()
|
|
||||||
pinStatus: boolean;
|
|
||||||
/**
|
|
||||||
* Datestring of the last time this note was updated
|
|
||||||
* @example "2020-12-01 12:23:34"
|
|
||||||
*/
|
|
||||||
@IsDate()
|
|
||||||
@Type(() => Date)
|
|
||||||
lastVisitedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class HistoryEntryImportListDto extends BaseDto {
|
|
||||||
@ValidateNested({ each: true })
|
|
||||||
@IsArray()
|
|
||||||
@Type(() => HistoryEntryImportDto)
|
|
||||||
history: HistoryEntryImportDto[];
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
|
||||||
import { IsBoolean } from 'class-validator';
|
|
||||||
|
|
||||||
import { BaseDto } from '../utils/base.dto.';
|
|
||||||
|
|
||||||
export class HistoryEntryUpdateDto extends BaseDto {
|
|
||||||
/**
|
|
||||||
* True if the note should be pinned
|
|
||||||
*/
|
|
||||||
@IsBoolean()
|
|
||||||
@ApiProperty()
|
|
||||||
pinStatus: boolean;
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
|
||||||
import { Type } from 'class-transformer';
|
|
||||||
import {
|
|
||||||
IsArray,
|
|
||||||
IsBoolean,
|
|
||||||
IsDate,
|
|
||||||
IsOptional,
|
|
||||||
IsString,
|
|
||||||
} from 'class-validator';
|
|
||||||
|
|
||||||
import { BaseDto } from '../utils/base.dto.';
|
|
||||||
|
|
||||||
export class HistoryEntryDto extends BaseDto {
|
|
||||||
/**
|
|
||||||
* ID or Alias of the note
|
|
||||||
*/
|
|
||||||
@IsString()
|
|
||||||
@ApiProperty()
|
|
||||||
identifier: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Title of the note
|
|
||||||
* Does not contain any markup but might be empty
|
|
||||||
* @example "Shopping List"
|
|
||||||
*/
|
|
||||||
@IsString()
|
|
||||||
@ApiProperty()
|
|
||||||
title: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The username of the owner of the note
|
|
||||||
* Might be null for anonymous notes
|
|
||||||
* @example "alice"
|
|
||||||
*/
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@ApiProperty()
|
|
||||||
owner: string | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Datestring of the last time this note was updated
|
|
||||||
* @example "2020-12-01 12:23:34"
|
|
||||||
*/
|
|
||||||
@IsDate()
|
|
||||||
@Type(() => Date)
|
|
||||||
@ApiProperty()
|
|
||||||
lastVisitedAt: Date;
|
|
||||||
|
|
||||||
@IsArray()
|
|
||||||
@IsString({ each: true })
|
|
||||||
@ApiProperty({ isArray: true, type: String })
|
|
||||||
tags: string[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* True if this note is pinned
|
|
||||||
* @example false
|
|
||||||
*/
|
|
||||||
@IsBoolean()
|
|
||||||
@ApiProperty()
|
|
||||||
pinStatus: boolean;
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import {
|
|
||||||
Column,
|
|
||||||
Entity,
|
|
||||||
Index,
|
|
||||||
ManyToOne,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
UpdateDateColumn,
|
|
||||||
} from 'typeorm';
|
|
||||||
|
|
||||||
import { Note } from '../notes/note.entity';
|
|
||||||
import { User } from '../users/user.entity';
|
|
||||||
|
|
||||||
@Entity()
|
|
||||||
@Index(['note', 'user'], { unique: true })
|
|
||||||
export class HistoryEntry {
|
|
||||||
@PrimaryGeneratedColumn()
|
|
||||||
id: number;
|
|
||||||
|
|
||||||
@ManyToOne((_) => User, (user) => user.historyEntries, {
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
orphanedRowAction: 'delete', // This ensures the row of the history entry is deleted when no user references it anymore
|
|
||||||
})
|
|
||||||
user: Promise<User>;
|
|
||||||
|
|
||||||
@ManyToOne((_) => Note, (note) => note.historyEntries, {
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
orphanedRowAction: 'delete', // This ensures the row of the history entry is deleted when no note references it anymore
|
|
||||||
})
|
|
||||||
note: Promise<Note>;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
pinStatus: boolean;
|
|
||||||
|
|
||||||
@UpdateDateColumn()
|
|
||||||
updatedAt: Date;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a history entry
|
|
||||||
* @param user the user the history entry is associated with
|
|
||||||
* @param note the note the history entry is associated with
|
|
||||||
* @param [pinStatus=false] if the history entry should be pinned
|
|
||||||
*/
|
|
||||||
public static create(
|
|
||||||
user: User,
|
|
||||||
note: Note,
|
|
||||||
pinStatus = false,
|
|
||||||
): Omit<HistoryEntry, 'updatedAt'> {
|
|
||||||
const newHistoryEntry = new HistoryEntry();
|
|
||||||
newHistoryEntry.user = Promise.resolve(user);
|
|
||||||
newHistoryEntry.note = Promise.resolve(note);
|
|
||||||
newHistoryEntry.pinStatus = pinStatus;
|
|
||||||
return newHistoryEntry;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { ConfigModule } from '@nestjs/config';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { LoggerModule } from '../logger/logger.module';
|
|
||||||
import { NotesModule } from '../notes/notes.module';
|
|
||||||
import { RevisionsModule } from '../revisions/revisions.module';
|
|
||||||
import { UsersModule } from '../users/users.module';
|
|
||||||
import { HistoryEntry } from './history-entry.entity';
|
|
||||||
import { HistoryService } from './history.service';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
providers: [HistoryService],
|
|
||||||
exports: [HistoryService],
|
|
||||||
imports: [
|
|
||||||
LoggerModule,
|
|
||||||
TypeOrmModule.forFeature([HistoryEntry]),
|
|
||||||
UsersModule,
|
|
||||||
NotesModule,
|
|
||||||
ConfigModule,
|
|
||||||
RevisionsModule,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class HistoryModule {}
|
|
|
@ -1,470 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import { ConfigModule } from '@nestjs/config';
|
|
||||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { getDataSourceToken, getRepositoryToken } from '@nestjs/typeorm';
|
|
||||||
import assert from 'assert';
|
|
||||||
import { Mock } from 'ts-mockery';
|
|
||||||
import { DataSource, EntityManager, Repository } from 'typeorm';
|
|
||||||
|
|
||||||
import { ApiToken } from '../api-token/api-token.entity';
|
|
||||||
import { Identity } from '../auth/identity.entity';
|
|
||||||
import { Author } from '../authors/author.entity';
|
|
||||||
import appConfigMock from '../config/mock/app.config.mock';
|
|
||||||
import authConfigMock from '../config/mock/auth.config.mock';
|
|
||||||
import databaseConfigMock from '../config/mock/database.config.mock';
|
|
||||||
import noteConfigMock from '../config/mock/note.config.mock';
|
|
||||||
import { NotInDBError } from '../errors/errors';
|
|
||||||
import { eventModuleConfig } from '../events';
|
|
||||||
import { Group } from '../groups/group.entity';
|
|
||||||
import { LoggerModule } from '../logger/logger.module';
|
|
||||||
import { Alias } from '../notes/alias.entity';
|
|
||||||
import { Note } from '../notes/note.entity';
|
|
||||||
import { NotesModule } from '../notes/notes.module';
|
|
||||||
import { Tag } from '../notes/tag.entity';
|
|
||||||
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
|
|
||||||
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
|
||||||
import { Edit } from '../revisions/edit.entity';
|
|
||||||
import { Revision } from '../revisions/revision.entity';
|
|
||||||
import { RevisionsModule } from '../revisions/revisions.module';
|
|
||||||
import { RevisionsService } from '../revisions/revisions.service';
|
|
||||||
import { Session } from '../sessions/session.entity';
|
|
||||||
import { User } from '../users/user.entity';
|
|
||||||
import { UsersModule } from '../users/users.module';
|
|
||||||
import { mockSelectQueryBuilderInRepo } from '../utils/test-utils/mockSelectQueryBuilder';
|
|
||||||
import { HistoryEntryImportDto } from './history-entry-import.dto';
|
|
||||||
import { HistoryEntry } from './history-entry.entity';
|
|
||||||
import { HistoryService } from './history.service';
|
|
||||||
|
|
||||||
describe('HistoryService', () => {
|
|
||||||
let service: HistoryService;
|
|
||||||
let revisionsService: RevisionsService;
|
|
||||||
let historyRepo: Repository<HistoryEntry>;
|
|
||||||
let noteRepo: Repository<Note>;
|
|
||||||
let mockedTransaction: jest.Mock<
|
|
||||||
Promise<void>,
|
|
||||||
[(entityManager: EntityManager) => Promise<void>]
|
|
||||||
>;
|
|
||||||
|
|
||||||
class CreateQueryBuilderClass {
|
|
||||||
leftJoinAndSelect: () => CreateQueryBuilderClass;
|
|
||||||
where: () => CreateQueryBuilderClass;
|
|
||||||
orWhere: () => CreateQueryBuilderClass;
|
|
||||||
setParameter: () => CreateQueryBuilderClass;
|
|
||||||
getOne: () => HistoryEntry;
|
|
||||||
getMany: () => HistoryEntry[];
|
|
||||||
}
|
|
||||||
|
|
||||||
let createQueryBuilderFunc: CreateQueryBuilderClass;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
noteRepo = new Repository<Note>(
|
|
||||||
'',
|
|
||||||
new EntityManager(
|
|
||||||
new DataSource({
|
|
||||||
type: 'sqlite',
|
|
||||||
database: ':memory:',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
providers: [
|
|
||||||
HistoryService,
|
|
||||||
{
|
|
||||||
provide: getDataSourceToken(),
|
|
||||||
useFactory: () => {
|
|
||||||
mockedTransaction = jest.fn();
|
|
||||||
return Mock.of<DataSource>({
|
|
||||||
transaction: mockedTransaction,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: getRepositoryToken(HistoryEntry),
|
|
||||||
useClass: Repository,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: getRepositoryToken(Note),
|
|
||||||
useValue: noteRepo,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
imports: [
|
|
||||||
LoggerModule,
|
|
||||||
UsersModule,
|
|
||||||
NotesModule,
|
|
||||||
RevisionsModule,
|
|
||||||
ConfigModule.forRoot({
|
|
||||||
isGlobal: true,
|
|
||||||
load: [
|
|
||||||
appConfigMock,
|
|
||||||
databaseConfigMock,
|
|
||||||
authConfigMock,
|
|
||||||
noteConfigMock,
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
EventEmitterModule.forRoot(eventModuleConfig),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
.overrideProvider(getRepositoryToken(User))
|
|
||||||
.useValue({})
|
|
||||||
.overrideProvider(getRepositoryToken(ApiToken))
|
|
||||||
.useValue({})
|
|
||||||
.overrideProvider(getRepositoryToken(Identity))
|
|
||||||
.useValue({})
|
|
||||||
.overrideProvider(getRepositoryToken(Edit))
|
|
||||||
.useValue({})
|
|
||||||
.overrideProvider(getRepositoryToken(Revision))
|
|
||||||
.useValue({})
|
|
||||||
.overrideProvider(getRepositoryToken(Note))
|
|
||||||
.useValue(noteRepo)
|
|
||||||
.overrideProvider(getRepositoryToken(Tag))
|
|
||||||
.useValue({})
|
|
||||||
.overrideProvider(getRepositoryToken(NoteGroupPermission))
|
|
||||||
.useValue({})
|
|
||||||
.overrideProvider(getRepositoryToken(NoteUserPermission))
|
|
||||||
.useValue({})
|
|
||||||
.overrideProvider(getRepositoryToken(Group))
|
|
||||||
.useValue({})
|
|
||||||
.overrideProvider(getRepositoryToken(Session))
|
|
||||||
.useValue({})
|
|
||||||
.overrideProvider(getRepositoryToken(Author))
|
|
||||||
.useValue({})
|
|
||||||
.overrideProvider(getRepositoryToken(Alias))
|
|
||||||
.useClass(Repository)
|
|
||||||
.compile();
|
|
||||||
|
|
||||||
service = module.get<HistoryService>(HistoryService);
|
|
||||||
revisionsService = module.get<RevisionsService>(RevisionsService);
|
|
||||||
historyRepo = module.get<Repository<HistoryEntry>>(
|
|
||||||
getRepositoryToken(HistoryEntry),
|
|
||||||
);
|
|
||||||
noteRepo = module.get<Repository<Note>>(getRepositoryToken(Note));
|
|
||||||
const historyEntry = new HistoryEntry();
|
|
||||||
const createQueryBuilder = {
|
|
||||||
leftJoinAndSelect: () => createQueryBuilder,
|
|
||||||
where: () => createQueryBuilder,
|
|
||||||
orWhere: () => createQueryBuilder,
|
|
||||||
setParameter: () => createQueryBuilder,
|
|
||||||
getOne: () => historyEntry,
|
|
||||||
getMany: () => [historyEntry],
|
|
||||||
};
|
|
||||||
createQueryBuilderFunc = createQueryBuilder as CreateQueryBuilderClass;
|
|
||||||
jest
|
|
||||||
.spyOn(historyRepo, 'createQueryBuilder')
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
.mockImplementation(() => createQueryBuilder);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(service).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getEntriesByUser', () => {
|
|
||||||
describe('works', () => {
|
|
||||||
it('with an empty list', async () => {
|
|
||||||
createQueryBuilderFunc.getMany = () => [];
|
|
||||||
expect(await service.getEntriesByUser({} as User)).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('with an one element list', async () => {
|
|
||||||
const historyEntry = new HistoryEntry();
|
|
||||||
createQueryBuilderFunc.getMany = () => [historyEntry];
|
|
||||||
expect(await service.getEntriesByUser({} as User)).toEqual([
|
|
||||||
historyEntry,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('with an multiple element list', async () => {
|
|
||||||
const historyEntry = new HistoryEntry();
|
|
||||||
const historyEntry2 = new HistoryEntry();
|
|
||||||
createQueryBuilderFunc.getMany = () => [historyEntry, historyEntry2];
|
|
||||||
expect(await service.getEntriesByUser({} as User)).toEqual([
|
|
||||||
historyEntry,
|
|
||||||
historyEntry2,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('updateHistoryEntryTimestamp', () => {
|
|
||||||
describe('works', () => {
|
|
||||||
const user = {} as User;
|
|
||||||
const alias = 'alias';
|
|
||||||
const historyEntry = HistoryEntry.create(
|
|
||||||
user,
|
|
||||||
Note.create(user, alias) as Note,
|
|
||||||
) as HistoryEntry;
|
|
||||||
it('without an preexisting entry', async () => {
|
|
||||||
mockSelectQueryBuilderInRepo(historyRepo, null);
|
|
||||||
jest
|
|
||||||
.spyOn(historyRepo, 'save')
|
|
||||||
.mockImplementation(
|
|
||||||
async (entry): Promise<HistoryEntry> => entry as HistoryEntry,
|
|
||||||
);
|
|
||||||
const createHistoryEntry = await service.updateHistoryEntryTimestamp(
|
|
||||||
Note.create(user, alias) as Note,
|
|
||||||
user,
|
|
||||||
);
|
|
||||||
assert(createHistoryEntry != null);
|
|
||||||
expect(await (await createHistoryEntry.note).aliases).toHaveLength(1);
|
|
||||||
expect((await (await createHistoryEntry.note).aliases)[0].name).toEqual(
|
|
||||||
alias,
|
|
||||||
);
|
|
||||||
expect(await (await createHistoryEntry.note).owner).toEqual(user);
|
|
||||||
expect(await createHistoryEntry.user).toEqual(user);
|
|
||||||
expect(createHistoryEntry.pinStatus).toEqual(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('with an preexisting entry', async () => {
|
|
||||||
mockSelectQueryBuilderInRepo(historyRepo, historyEntry);
|
|
||||||
jest
|
|
||||||
.spyOn(historyRepo, 'save')
|
|
||||||
.mockImplementation(
|
|
||||||
async (entry): Promise<HistoryEntry> => entry as HistoryEntry,
|
|
||||||
);
|
|
||||||
const createHistoryEntry = await service.updateHistoryEntryTimestamp(
|
|
||||||
Note.create(user, alias) as Note,
|
|
||||||
user,
|
|
||||||
);
|
|
||||||
assert(createHistoryEntry != null);
|
|
||||||
expect(await (await createHistoryEntry.note).aliases).toHaveLength(1);
|
|
||||||
expect((await (await createHistoryEntry.note).aliases)[0].name).toEqual(
|
|
||||||
alias,
|
|
||||||
);
|
|
||||||
expect(await (await createHistoryEntry.note).owner).toEqual(user);
|
|
||||||
expect(await createHistoryEntry.user).toEqual(user);
|
|
||||||
expect(createHistoryEntry.pinStatus).toEqual(false);
|
|
||||||
expect(createHistoryEntry.updatedAt.getTime()).toBeGreaterThanOrEqual(
|
|
||||||
historyEntry.updatedAt.getTime(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('returns null if user is null', async () => {
|
|
||||||
const entry = await service.updateHistoryEntryTimestamp({} as Note, null);
|
|
||||||
expect(entry).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('updateHistoryEntry', () => {
|
|
||||||
const user = {} as User;
|
|
||||||
const alias = 'alias';
|
|
||||||
const note = Note.create(user, alias) as Note;
|
|
||||||
beforeEach(() => {
|
|
||||||
mockSelectQueryBuilderInRepo(noteRepo, note);
|
|
||||||
});
|
|
||||||
describe('works', () => {
|
|
||||||
it('with an entry', async () => {
|
|
||||||
const historyEntry = HistoryEntry.create(user, note) as HistoryEntry;
|
|
||||||
mockSelectQueryBuilderInRepo(historyRepo, historyEntry);
|
|
||||||
jest
|
|
||||||
.spyOn(historyRepo, 'save')
|
|
||||||
.mockImplementation(
|
|
||||||
async (entry): Promise<HistoryEntry> => entry as HistoryEntry,
|
|
||||||
);
|
|
||||||
const updatedHistoryEntry = await service.updateHistoryEntry(
|
|
||||||
note,
|
|
||||||
user,
|
|
||||||
{
|
|
||||||
pinStatus: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(await (await updatedHistoryEntry.note).aliases).toHaveLength(1);
|
|
||||||
expect(
|
|
||||||
(await (await updatedHistoryEntry.note).aliases)[0].name,
|
|
||||||
).toEqual(alias);
|
|
||||||
expect(await (await updatedHistoryEntry.note).owner).toEqual(user);
|
|
||||||
expect(await updatedHistoryEntry.user).toEqual(user);
|
|
||||||
expect(updatedHistoryEntry.pinStatus).toEqual(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('without an entry', async () => {
|
|
||||||
mockSelectQueryBuilderInRepo(historyRepo, null);
|
|
||||||
await expect(
|
|
||||||
service.updateHistoryEntry(note, user, {
|
|
||||||
pinStatus: true,
|
|
||||||
}),
|
|
||||||
).rejects.toThrow(NotInDBError);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('deleteHistoryEntry', () => {
|
|
||||||
describe('works', () => {
|
|
||||||
const user = {} as User;
|
|
||||||
const alias = 'alias';
|
|
||||||
const note = Note.create(user, alias) as Note;
|
|
||||||
const historyEntry = HistoryEntry.create(user, note) as HistoryEntry;
|
|
||||||
it('with an entry', async () => {
|
|
||||||
createQueryBuilderFunc.getMany = () => [historyEntry];
|
|
||||||
jest
|
|
||||||
.spyOn(historyRepo, 'remove')
|
|
||||||
.mockImplementationOnce(
|
|
||||||
async (entry: HistoryEntry): Promise<HistoryEntry> => {
|
|
||||||
expect(entry).toEqual(historyEntry);
|
|
||||||
return entry;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await service.deleteHistory(user);
|
|
||||||
});
|
|
||||||
it('with multiple entries', async () => {
|
|
||||||
const alias2 = 'alias2';
|
|
||||||
const note2 = Note.create(user, alias2) as Note;
|
|
||||||
const historyEntry2 = HistoryEntry.create(user, note2) as HistoryEntry;
|
|
||||||
createQueryBuilderFunc.getMany = () => [historyEntry, historyEntry2];
|
|
||||||
jest
|
|
||||||
.spyOn(historyRepo, 'remove')
|
|
||||||
.mockImplementationOnce(
|
|
||||||
async (entry: HistoryEntry): Promise<HistoryEntry> => {
|
|
||||||
expect(entry).toEqual(historyEntry);
|
|
||||||
return entry;
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.mockImplementationOnce(
|
|
||||||
async (entry: HistoryEntry): Promise<HistoryEntry> => {
|
|
||||||
expect(entry).toEqual(historyEntry2);
|
|
||||||
return entry;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await service.deleteHistory(user);
|
|
||||||
});
|
|
||||||
it('without an entry', async () => {
|
|
||||||
createQueryBuilderFunc.getMany = () => [];
|
|
||||||
await service.deleteHistory(user);
|
|
||||||
expect(true).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('deleteHistory', () => {
|
|
||||||
describe('works', () => {
|
|
||||||
it('with an entry', async () => {
|
|
||||||
const user = {} as User;
|
|
||||||
const alias = 'alias';
|
|
||||||
const note = Note.create(user, alias) as Note;
|
|
||||||
const historyEntry = HistoryEntry.create(user, note) as HistoryEntry;
|
|
||||||
mockSelectQueryBuilderInRepo(historyRepo, historyEntry);
|
|
||||||
mockSelectQueryBuilderInRepo(noteRepo, note);
|
|
||||||
jest
|
|
||||||
.spyOn(historyRepo, 'remove')
|
|
||||||
.mockImplementation(
|
|
||||||
async (entry: HistoryEntry): Promise<HistoryEntry> => {
|
|
||||||
expect(entry).toEqual(historyEntry);
|
|
||||||
return entry;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await service.deleteHistoryEntry(note, user);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('fails', () => {
|
|
||||||
const user = {} as User;
|
|
||||||
const alias = 'alias';
|
|
||||||
it('without an entry', async () => {
|
|
||||||
const note = Note.create(user, alias) as Note;
|
|
||||||
|
|
||||||
mockSelectQueryBuilderInRepo(historyRepo, null);
|
|
||||||
mockSelectQueryBuilderInRepo(noteRepo, note);
|
|
||||||
await expect(service.deleteHistoryEntry(note, user)).rejects.toThrow(
|
|
||||||
NotInDBError,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('setHistory', () => {
|
|
||||||
it('works', async () => {
|
|
||||||
const user = {} as User;
|
|
||||||
const alias = 'alias';
|
|
||||||
const note = Note.create(user, alias) as Note;
|
|
||||||
const historyEntry = HistoryEntry.create(user, note);
|
|
||||||
const historyEntryImport: HistoryEntryImportDto = {
|
|
||||||
lastVisitedAt: new Date('2020-12-01 12:23:34'),
|
|
||||||
note: alias,
|
|
||||||
pinStatus: true,
|
|
||||||
};
|
|
||||||
const newlyCreatedHistoryEntry: HistoryEntry = {
|
|
||||||
...historyEntry,
|
|
||||||
pinStatus: historyEntryImport.pinStatus,
|
|
||||||
updatedAt: historyEntryImport.lastVisitedAt,
|
|
||||||
};
|
|
||||||
|
|
||||||
mockSelectQueryBuilderInRepo(noteRepo, note);
|
|
||||||
const createQueryBuilderForEntityManager = {
|
|
||||||
where: () => createQueryBuilderForEntityManager,
|
|
||||||
getMany: () => [historyEntry],
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockedManager = Mock.of<EntityManager>({
|
|
||||||
createQueryBuilder: jest
|
|
||||||
.fn()
|
|
||||||
.mockImplementation(() => createQueryBuilderForEntityManager),
|
|
||||||
remove: jest
|
|
||||||
.fn()
|
|
||||||
.mockImplementationOnce(async (entry: HistoryEntry) => {
|
|
||||||
expect(await (await entry.note).aliases).toHaveLength(1);
|
|
||||||
expect((await (await entry.note).aliases)[0].name).toEqual(alias);
|
|
||||||
expect(entry.pinStatus).toEqual(false);
|
|
||||||
}),
|
|
||||||
save: jest.fn().mockImplementationOnce(async (entry: HistoryEntry) => {
|
|
||||||
expect((await entry.note).aliases).toEqual(
|
|
||||||
(await newlyCreatedHistoryEntry.note).aliases,
|
|
||||||
);
|
|
||||||
expect(entry.pinStatus).toEqual(newlyCreatedHistoryEntry.pinStatus);
|
|
||||||
expect(entry.updatedAt).toEqual(newlyCreatedHistoryEntry.updatedAt);
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
mockedTransaction.mockImplementation((cb) => cb(mockedManager));
|
|
||||||
await service.setHistory(user, [historyEntryImport]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('toHistoryEntryDto', () => {
|
|
||||||
describe('works', () => {
|
|
||||||
it('with aliased note', async () => {
|
|
||||||
const user = {} as User;
|
|
||||||
const alias = 'alias';
|
|
||||||
const title = 'title';
|
|
||||||
const tags = ['tag1', 'tag2'];
|
|
||||||
const note = Note.create(user, alias) as Note;
|
|
||||||
const revision = Revision.create(
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
note,
|
|
||||||
null,
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
[],
|
|
||||||
) as Revision;
|
|
||||||
revision.title = title;
|
|
||||||
revision.tags = Promise.resolve(
|
|
||||||
tags.map((tag) => {
|
|
||||||
const newTag = new Tag();
|
|
||||||
newTag.name = tag;
|
|
||||||
return newTag;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const historyEntry = HistoryEntry.create(user, note) as HistoryEntry;
|
|
||||||
historyEntry.pinStatus = true;
|
|
||||||
|
|
||||||
mockSelectQueryBuilderInRepo(noteRepo, note);
|
|
||||||
jest
|
|
||||||
.spyOn(revisionsService, 'getLatestRevision')
|
|
||||||
.mockImplementation((requestedNote) => {
|
|
||||||
expect(note).toBe(requestedNote);
|
|
||||||
return Promise.resolve(revision);
|
|
||||||
});
|
|
||||||
|
|
||||||
const historyEntryDto = await service.toHistoryEntryDto(historyEntry);
|
|
||||||
expect(historyEntryDto.pinStatus).toEqual(true);
|
|
||||||
expect(historyEntryDto.identifier).toEqual(alias);
|
|
||||||
expect(historyEntryDto.tags).toEqual(tags);
|
|
||||||
expect(historyEntryDto.title).toEqual(title);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,194 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { InjectConnection, InjectRepository } from '@nestjs/typeorm';
|
|
||||||
import { Connection, Repository } from 'typeorm';
|
|
||||||
|
|
||||||
import { NotInDBError } from '../errors/errors';
|
|
||||||
import { ConsoleLoggerService } from '../logger/console-logger.service';
|
|
||||||
import { Note } from '../notes/note.entity';
|
|
||||||
import { NotesService } from '../notes/notes.service';
|
|
||||||
import { RevisionsService } from '../revisions/revisions.service';
|
|
||||||
import { User } from '../users/user.entity';
|
|
||||||
import { UsersService } from '../users/users.service';
|
|
||||||
import { HistoryEntryImportDto } from './history-entry-import.dto';
|
|
||||||
import { HistoryEntryUpdateDto } from './history-entry-update.dto';
|
|
||||||
import { HistoryEntryDto } from './history-entry.dto';
|
|
||||||
import { HistoryEntry } from './history-entry.entity';
|
|
||||||
import { getIdentifier } from './utils';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class HistoryService {
|
|
||||||
constructor(
|
|
||||||
private readonly logger: ConsoleLoggerService,
|
|
||||||
@InjectConnection()
|
|
||||||
private connection: Connection,
|
|
||||||
@InjectRepository(HistoryEntry)
|
|
||||||
private historyEntryRepository: Repository<HistoryEntry>,
|
|
||||||
private usersService: UsersService,
|
|
||||||
private notesService: NotesService,
|
|
||||||
private revisionsService: RevisionsService,
|
|
||||||
) {
|
|
||||||
this.logger.setContext(HistoryService.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @async
|
|
||||||
* Get all entries of a user
|
|
||||||
* @param {User} user - the user the entries should be from
|
|
||||||
* @return {HistoryEntry[]} an array of history entries of the specified user
|
|
||||||
*/
|
|
||||||
async getEntriesByUser(user: User): Promise<HistoryEntry[]> {
|
|
||||||
return await this.historyEntryRepository
|
|
||||||
.createQueryBuilder('entry')
|
|
||||||
.where('entry.userId = :userId', { userId: user.id })
|
|
||||||
.getMany();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @async
|
|
||||||
* Get a history entry by the user and note
|
|
||||||
* @param {Note} note - the note that the history entry belongs to
|
|
||||||
* @param {User} user - the user that the history entry belongs to
|
|
||||||
* @return {HistoryEntry} the requested history entry
|
|
||||||
*/
|
|
||||||
async getEntryByNote(note: Note, user: User): Promise<HistoryEntry> {
|
|
||||||
const entry = await this.historyEntryRepository
|
|
||||||
.createQueryBuilder('entry')
|
|
||||||
.where('entry.note = :note', { note: note.id })
|
|
||||||
.andWhere('entry.user = :user', { user: user.id })
|
|
||||||
.leftJoinAndSelect('entry.note', 'note')
|
|
||||||
.leftJoinAndSelect('entry.user', 'user')
|
|
||||||
.getOne();
|
|
||||||
if (!entry) {
|
|
||||||
throw new NotInDBError(
|
|
||||||
`User '${user.username}' has no HistoryEntry for Note with id '${note.id}'`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @async
|
|
||||||
* Updates the updatedAt timestamp of a HistoryEntry.
|
|
||||||
* If no history entry exists, it will be created.
|
|
||||||
* @param {Note} note - the note that the history entry belongs to
|
|
||||||
* @param {User | null} user - the user that the history entry belongs to
|
|
||||||
* @return {HistoryEntry} the requested history entry
|
|
||||||
*/
|
|
||||||
async updateHistoryEntryTimestamp(
|
|
||||||
note: Note,
|
|
||||||
user: User | null,
|
|
||||||
): Promise<HistoryEntry | null> {
|
|
||||||
if (user == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const entry = await this.getEntryByNote(note, user);
|
|
||||||
entry.updatedAt = new Date();
|
|
||||||
return await this.historyEntryRepository.save(entry);
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof NotInDBError) {
|
|
||||||
const entry = HistoryEntry.create(user, note);
|
|
||||||
return await this.historyEntryRepository.save(entry);
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @async
|
|
||||||
* Update a history entry identified by the user and a note id or alias
|
|
||||||
* @param {Note} note - the note that the history entry belongs to
|
|
||||||
* @param {User} user - the user that the history entry belongs to
|
|
||||||
* @param {HistoryEntryUpdateDto} updateDto - the change that should be applied to the history entry
|
|
||||||
* @return {HistoryEntry} the requested history entry
|
|
||||||
*/
|
|
||||||
async updateHistoryEntry(
|
|
||||||
note: Note,
|
|
||||||
user: User,
|
|
||||||
updateDto: HistoryEntryUpdateDto,
|
|
||||||
): Promise<HistoryEntry> {
|
|
||||||
const entry = await this.getEntryByNote(note, user);
|
|
||||||
entry.pinStatus = updateDto.pinStatus;
|
|
||||||
return await this.historyEntryRepository.save(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @async
|
|
||||||
* Delete the history entry identified by the user and a note id or alias
|
|
||||||
* @param {Note} note - the note that the history entry belongs to
|
|
||||||
* @param {User} user - the user that the history entry belongs to
|
|
||||||
* @throws {NotInDBError} the specified history entry does not exist
|
|
||||||
*/
|
|
||||||
async deleteHistoryEntry(note: Note, user: User): Promise<void> {
|
|
||||||
const entry = await this.getEntryByNote(note, user);
|
|
||||||
await this.historyEntryRepository.remove(entry);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @async
|
|
||||||
* Delete all history entries of a specific user
|
|
||||||
* @param {User} user - the user that the entry belongs to
|
|
||||||
*/
|
|
||||||
async deleteHistory(user: User): Promise<void> {
|
|
||||||
const entries: HistoryEntry[] = await this.getEntriesByUser(user);
|
|
||||||
for (const entry of entries) {
|
|
||||||
await this.historyEntryRepository.remove(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @async
|
|
||||||
* Replace the user history with the provided history
|
|
||||||
* @param {User} user - the user that get's their history replaces
|
|
||||||
* @param {HistoryEntryImportDto[]} history
|
|
||||||
* @throws {ForbiddenIdError} one of the note ids or alias in the new history are forbidden
|
|
||||||
*/
|
|
||||||
async setHistory(
|
|
||||||
user: User,
|
|
||||||
history: HistoryEntryImportDto[],
|
|
||||||
): Promise<void> {
|
|
||||||
await this.connection.transaction(async (manager) => {
|
|
||||||
const currentHistory = await manager
|
|
||||||
.createQueryBuilder(HistoryEntry, 'entry')
|
|
||||||
.where('entry.userId = :userId', { userId: user.id })
|
|
||||||
.getMany();
|
|
||||||
for (const entry of currentHistory) {
|
|
||||||
await manager.remove<HistoryEntry>(entry);
|
|
||||||
}
|
|
||||||
for (const historyEntry of history) {
|
|
||||||
const note = await this.notesService.getNoteByIdOrAlias(
|
|
||||||
historyEntry.note,
|
|
||||||
);
|
|
||||||
const entry = HistoryEntry.create(user, note) as HistoryEntry;
|
|
||||||
entry.pinStatus = historyEntry.pinStatus;
|
|
||||||
entry.updatedAt = historyEntry.lastVisitedAt;
|
|
||||||
await manager.save<HistoryEntry>(entry);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build HistoryEntryDto from a history entry.
|
|
||||||
* @param {HistoryEntry} entry - the history entry to use
|
|
||||||
* @return {HistoryEntryDto} the built HistoryEntryDto
|
|
||||||
*/
|
|
||||||
async toHistoryEntryDto(entry: HistoryEntry): Promise<HistoryEntryDto> {
|
|
||||||
const note = await entry.note;
|
|
||||||
const owner = await note.owner;
|
|
||||||
const revision = await this.revisionsService.getLatestRevision(note);
|
|
||||||
return {
|
|
||||||
identifier: await getIdentifier(entry),
|
|
||||||
lastVisitedAt: entry.updatedAt,
|
|
||||||
tags: (await revision.tags).map((tag) => tag.name),
|
|
||||||
title: revision.title ?? '',
|
|
||||||
pinStatus: entry.pinStatus,
|
|
||||||
owner: owner ? owner.username : null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import { Alias } from '../notes/alias.entity';
|
|
||||||
import { Note } from '../notes/note.entity';
|
|
||||||
import { User } from '../users/user.entity';
|
|
||||||
import { HistoryEntry } from './history-entry.entity';
|
|
||||||
import { getIdentifier } from './utils';
|
|
||||||
|
|
||||||
describe('getIdentifier', () => {
|
|
||||||
const alias = 'alias';
|
|
||||||
let note: Note;
|
|
||||||
let entry: HistoryEntry;
|
|
||||||
beforeEach(() => {
|
|
||||||
const user = User.create('hardcoded', 'Testy') as User;
|
|
||||||
note = Note.create(user, alias) as Note;
|
|
||||||
entry = HistoryEntry.create(user, note) as HistoryEntry;
|
|
||||||
});
|
|
||||||
it('returns the publicId if there are no aliases', async () => {
|
|
||||||
note.aliases = Promise.resolve(undefined as unknown as Alias[]);
|
|
||||||
expect(await getIdentifier(entry)).toEqual(note.publicId);
|
|
||||||
});
|
|
||||||
it('returns the publicId, if the alias array is empty', async () => {
|
|
||||||
note.aliases = Promise.resolve([]);
|
|
||||||
expect(await getIdentifier(entry)).toEqual(note.publicId);
|
|
||||||
});
|
|
||||||
it('returns the publicId, if the only alias is not primary', async () => {
|
|
||||||
(await note.aliases)[0].primary = false;
|
|
||||||
expect(await getIdentifier(entry)).toEqual(note.publicId);
|
|
||||||
});
|
|
||||||
it('returns the primary alias, if one exists', async () => {
|
|
||||||
expect(await getIdentifier(entry)).toEqual((await note.aliases)[0].name);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,19 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import { getPrimaryAlias } from '../notes/utils';
|
|
||||||
import { HistoryEntry } from './history-entry.entity';
|
|
||||||
|
|
||||||
export async function getIdentifier(entry: HistoryEntry): Promise<string> {
|
|
||||||
const aliases = await (await entry.note).aliases;
|
|
||||||
if (!aliases || aliases.length === 0) {
|
|
||||||
return (await entry.note).publicId;
|
|
||||||
}
|
|
||||||
const primaryAlias = await getPrimaryAlias(await entry.note);
|
|
||||||
if (primaryAlias === undefined) {
|
|
||||||
return (await entry.note).publicId;
|
|
||||||
}
|
|
||||||
return primaryAlias;
|
|
||||||
}
|
|
|
@ -7,7 +7,7 @@ import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { IsDate, IsLowercase, IsOptional, IsString } from 'class-validator';
|
import { IsDate, IsLowercase, IsOptional, IsString } from 'class-validator';
|
||||||
|
|
||||||
import { BaseDto } from '../utils/base.dto.';
|
import { BaseDto } from '../utils/base.dto';
|
||||||
import { Username } from '../utils/username';
|
import { Username } from '../utils/username';
|
||||||
|
|
||||||
export class MediaUploadDto extends BaseDto {
|
export class MediaUploadDto extends BaseDto {
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
import { BaseDto } from '../utils/base.dto.';
|
import { BaseDto } from '../utils/base.dto';
|
||||||
|
|
||||||
export class ServerVersion {
|
export class ServerVersion {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsString } from 'class-validator';
|
import { IsString } from 'class-validator';
|
||||||
|
|
||||||
import { BaseDto } from '../utils/base.dto.';
|
import { BaseDto } from '../utils/base.dto';
|
||||||
|
|
||||||
export class AliasCreateDto extends BaseDto {
|
export class AliasCreateDto extends BaseDto {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsBoolean } from 'class-validator';
|
import { IsBoolean } from 'class-validator';
|
||||||
|
|
||||||
import { BaseDto } from '../utils/base.dto.';
|
import { BaseDto } from '../utils/base.dto';
|
||||||
|
|
||||||
export class AliasUpdateDto extends BaseDto {
|
export class AliasUpdateDto extends BaseDto {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsBoolean, IsString } from 'class-validator';
|
import { IsBoolean, IsString } from 'class-validator';
|
||||||
|
|
||||||
import { BaseDto } from '../utils/base.dto.';
|
import { BaseDto } from '../utils/base.dto';
|
||||||
|
|
||||||
export class AliasDto extends BaseDto {
|
export class AliasDto extends BaseDto {
|
||||||
/**
|
/**
|
||||||
|
|
69
backend/src/notes/note-explore-entry.dto.ts
Normal file
69
backend/src/notes/note-explore-entry.dto.ts
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import type { NoteType } from '@hedgedoc/commons';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { IsArray, IsBoolean, IsDate, IsString } from 'class-validator';
|
||||||
|
import { BaseDto } from 'src/utils/base.dto';
|
||||||
|
|
||||||
|
export class NoteExploreEntryDto extends BaseDto {
|
||||||
|
/**
|
||||||
|
* Primary address of the note
|
||||||
|
* @example my-note
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty()
|
||||||
|
primaryAddress: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The title of the note
|
||||||
|
* @example My Note
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty()
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the note (document or slide)
|
||||||
|
* @example document
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty()
|
||||||
|
type: NoteType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of tags assigned to this note
|
||||||
|
* @example "['shopping', 'personal']
|
||||||
|
*/
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
@ApiProperty({ isArray: true, type: String })
|
||||||
|
tags: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The owner of the note (or null)
|
||||||
|
* @example John Doe
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty()
|
||||||
|
owner: string | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the note is pinned or not
|
||||||
|
*/
|
||||||
|
@IsBoolean()
|
||||||
|
@ApiProperty()
|
||||||
|
isPinned: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Datestring of the time this note was last changed
|
||||||
|
* @example "2020-12-01 12:23:34"
|
||||||
|
*/
|
||||||
|
@IsDate()
|
||||||
|
@Type(() => Date)
|
||||||
|
@ApiProperty()
|
||||||
|
lastChangedAt: Date;
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ import {
|
||||||
ValidateNested,
|
ValidateNested,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
|
||||||
import { BaseDto } from '../utils/base.dto.';
|
import { BaseDto } from '../utils/base.dto';
|
||||||
import { AliasDto } from './alias.dto';
|
import { AliasDto } from './alias.dto';
|
||||||
import { NotePermissionsDto } from './note-permissions.dto';
|
import { NotePermissionsDto } from './note-permissions.dto';
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ import {
|
||||||
ValidateNested,
|
ValidateNested,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
|
||||||
import { BaseDto } from '../utils/base.dto.';
|
import { BaseDto } from '../utils/base.dto';
|
||||||
import { Username } from '../utils/username';
|
import { Username } from '../utils/username';
|
||||||
|
|
||||||
export class NoteUserPermissionEntryDto extends BaseDto {
|
export class NoteUserPermissionEntryDto extends BaseDto {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { Type } from 'class-transformer';
|
||||||
import { IsArray, IsString, ValidateNested } from 'class-validator';
|
import { IsArray, IsString, ValidateNested } from 'class-validator';
|
||||||
|
|
||||||
import { EditDto } from '../revisions/edit.dto';
|
import { EditDto } from '../revisions/edit.dto';
|
||||||
import { BaseDto } from '../utils/base.dto.';
|
import { BaseDto } from '../utils/base.dto';
|
||||||
import { NoteMetadataDto } from './note-metadata.dto';
|
import { NoteMetadataDto } from './note-metadata.dto';
|
||||||
|
|
||||||
export class NoteDto extends BaseDto {
|
export class NoteDto extends BaseDto {
|
||||||
|
|
|
@ -12,7 +12,6 @@ import {
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
import { HistoryEntry } from '../history/history-entry.entity';
|
|
||||||
import { MediaUpload } from '../media/media-upload.entity';
|
import { MediaUpload } from '../media/media-upload.entity';
|
||||||
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
|
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
|
||||||
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
||||||
|
@ -65,9 +64,6 @@ export class Note {
|
||||||
@OneToMany((_) => Revision, (revision) => revision.note, { cascade: true })
|
@OneToMany((_) => Revision, (revision) => revision.note, { cascade: true })
|
||||||
revisions: Promise<Revision[]>;
|
revisions: Promise<Revision[]>;
|
||||||
|
|
||||||
@OneToMany((_) => HistoryEntry, (historyEntry) => historyEntry.user)
|
|
||||||
historyEntries: Promise<HistoryEntry[]>;
|
|
||||||
|
|
||||||
@OneToMany((_) => MediaUpload, (mediaUpload) => mediaUpload.note)
|
@OneToMany((_) => MediaUpload, (mediaUpload) => mediaUpload.note)
|
||||||
mediaUploads: Promise<MediaUpload[]>;
|
mediaUploads: Promise<MediaUpload[]>;
|
||||||
|
|
||||||
|
@ -101,7 +97,6 @@ export class Note {
|
||||||
newNote.viewCount = 0;
|
newNote.viewCount = 0;
|
||||||
newNote.owner = Promise.resolve(owner);
|
newNote.owner = Promise.resolve(owner);
|
||||||
newNote.revisions = Promise.resolve([]);
|
newNote.revisions = Promise.resolve([]);
|
||||||
newNote.historyEntries = Promise.resolve([]);
|
|
||||||
newNote.mediaUploads = Promise.resolve([]);
|
newNote.mediaUploads = Promise.resolve([]);
|
||||||
newNote.version = 2;
|
newNote.version = 2;
|
||||||
return newNote;
|
return newNote;
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsBoolean } from 'class-validator';
|
import { IsBoolean } from 'class-validator';
|
||||||
|
|
||||||
import { BaseDto } from '../utils/base.dto.';
|
import { BaseDto } from '../utils/base.dto';
|
||||||
|
|
||||||
export class NoteMediaDeletionDto extends BaseDto {
|
export class NoteMediaDeletionDto extends BaseDto {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
import { NoteType } from '@hedgedoc/commons';
|
||||||
import { Optional } from '@mrdrogdrog/optional';
|
import { Optional } from '@mrdrogdrog/optional';
|
||||||
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
@ -20,7 +21,6 @@ import {
|
||||||
import { NoteEvent, NoteEventMap } from '../events';
|
import { NoteEvent, NoteEventMap } from '../events';
|
||||||
import { Group } from '../groups/group.entity';
|
import { Group } from '../groups/group.entity';
|
||||||
import { GroupsService } from '../groups/groups.service';
|
import { GroupsService } from '../groups/groups.service';
|
||||||
import { HistoryEntry } from '../history/history-entry.entity';
|
|
||||||
import { ConsoleLoggerService } from '../logger/console-logger.service';
|
import { ConsoleLoggerService } from '../logger/console-logger.service';
|
||||||
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
|
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
|
||||||
import { RealtimeNoteStore } from '../realtime/realtime-note/realtime-note-store';
|
import { RealtimeNoteStore } from '../realtime/realtime-note/realtime-note-store';
|
||||||
|
@ -30,6 +30,7 @@ import { User } from '../users/user.entity';
|
||||||
import { UsersService } from '../users/users.service';
|
import { UsersService } from '../users/users.service';
|
||||||
import { Alias } from './alias.entity';
|
import { Alias } from './alias.entity';
|
||||||
import { AliasService } from './alias.service';
|
import { AliasService } from './alias.service';
|
||||||
|
import { NoteExploreEntryDto } from './note-explore-entry.dto';
|
||||||
import { NoteMetadataDto } from './note-metadata.dto';
|
import { NoteMetadataDto } from './note-metadata.dto';
|
||||||
import { NotePermissionsDto } from './note-permissions.dto';
|
import { NotePermissionsDto } from './note-permissions.dto';
|
||||||
import { NoteDto } from './note.dto';
|
import { NoteDto } from './note.dto';
|
||||||
|
@ -112,10 +113,6 @@ export class NotesService {
|
||||||
let everyoneAccessLevel;
|
let everyoneAccessLevel;
|
||||||
|
|
||||||
if (owner) {
|
if (owner) {
|
||||||
// When we know an owner, an initial history entry is created
|
|
||||||
newNote.historyEntries = Promise.resolve([
|
|
||||||
HistoryEntry.create(owner, newNote) as HistoryEntry,
|
|
||||||
]);
|
|
||||||
// Use the default access level from the config
|
// Use the default access level from the config
|
||||||
everyoneAccessLevel = this.noteConfig.permissions.default.everyone;
|
everyoneAccessLevel = this.noteConfig.permissions.default.everyone;
|
||||||
} else {
|
} else {
|
||||||
|
@ -450,4 +447,28 @@ export class NotesService {
|
||||||
editedByAtPosition: [],
|
editedByAtPosition: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @async
|
||||||
|
* Build NoteDto from a note.
|
||||||
|
* @param {Note} note - the note to use
|
||||||
|
* @return {NoteDto} the built NoteDto
|
||||||
|
*/
|
||||||
|
async toNoteExploreEntryDto(
|
||||||
|
note: Note,
|
||||||
|
user: User,
|
||||||
|
): Promise<NoteExploreEntryDto> {
|
||||||
|
const latestRevision = await this.revisionsService.getLatestRevision(note);
|
||||||
|
return {
|
||||||
|
primaryAddress: (await getPrimaryAlias(note)) ?? note.publicId,
|
||||||
|
title: latestRevision.title,
|
||||||
|
type: NoteType.DOCUMENT,
|
||||||
|
tags: (await latestRevision.tags).map((tag) => tag.name),
|
||||||
|
owner: (await note.owner)?.username ?? null,
|
||||||
|
isPinned: ((await user.pinnedNotes) ?? []).some(
|
||||||
|
(aPinnedNote) => aPinnedNote.id === note.id,
|
||||||
|
),
|
||||||
|
lastChangedAt: latestRevision.createdAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { Type } from 'class-transformer';
|
||||||
import { IsDate, IsNumber, IsOptional, IsString, Min } from 'class-validator';
|
import { IsDate, IsNumber, IsOptional, IsString, Min } from 'class-validator';
|
||||||
|
|
||||||
import { UserInfoDto } from '../users/user-info.dto';
|
import { UserInfoDto } from '../users/user-info.dto';
|
||||||
import { BaseDto } from '../utils/base.dto.';
|
import { BaseDto } from '../utils/base.dto';
|
||||||
|
|
||||||
export class EditDto extends BaseDto {
|
export class EditDto extends BaseDto {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { IsArray, IsDate, IsNumber, IsString } from 'class-validator';
|
import { IsArray, IsDate, IsNumber, IsString } from 'class-validator';
|
||||||
|
|
||||||
import { BaseDto } from '../utils/base.dto.';
|
import { BaseDto } from '../utils/base.dto';
|
||||||
import { Revision } from './revision.entity';
|
import { Revision } from './revision.entity';
|
||||||
|
|
||||||
export class RevisionMetadataDto extends BaseDto {
|
export class RevisionMetadataDto extends BaseDto {
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
import { NoteType } from '@hedgedoc/commons';
|
||||||
import {
|
import {
|
||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
|
@ -60,6 +61,9 @@ export class Revision {
|
||||||
})
|
})
|
||||||
content: string;
|
content: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
type: NoteType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The length of the note content.
|
* The length of the note content.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { IsLowercase, IsOptional, IsString } from 'class-validator';
|
import { IsLowercase, IsOptional, IsString } from 'class-validator';
|
||||||
|
|
||||||
import { BaseDto } from '../utils/base.dto.';
|
import { BaseDto } from '../utils/base.dto';
|
||||||
import { Username } from '../utils/username';
|
import { Username } from '../utils/username';
|
||||||
|
|
||||||
export class UserInfoDto extends BaseDto {
|
export class UserInfoDto extends BaseDto {
|
||||||
|
|
|
@ -65,8 +65,8 @@ export class User {
|
||||||
@ManyToMany((_) => Group, (group) => group.members)
|
@ManyToMany((_) => Group, (group) => group.members)
|
||||||
groups: Promise<Group[]>;
|
groups: Promise<Group[]>;
|
||||||
|
|
||||||
@OneToMany((_) => HistoryEntry, (historyEntry) => historyEntry.user)
|
@OneToMany((_) => Note, (note) => note.owner)
|
||||||
historyEntries: Promise<HistoryEntry[]>;
|
pinnedNotes: Promise<Note[]>;
|
||||||
|
|
||||||
@OneToMany((_) => MediaUpload, (mediaUpload) => mediaUpload.user)
|
@OneToMany((_) => MediaUpload, (mediaUpload) => mediaUpload.user)
|
||||||
mediaUploads: Promise<MediaUpload[]>;
|
mediaUploads: Promise<MediaUpload[]>;
|
||||||
|
@ -92,7 +92,7 @@ export class User {
|
||||||
newUser.apiTokens = Promise.resolve([]);
|
newUser.apiTokens = Promise.resolve([]);
|
||||||
newUser.identities = Promise.resolve([]);
|
newUser.identities = Promise.resolve([]);
|
||||||
newUser.groups = Promise.resolve([]);
|
newUser.groups = Promise.resolve([]);
|
||||||
newUser.historyEntries = Promise.resolve([]);
|
newUser.pinnedNotes = Promise.resolve([]);
|
||||||
newUser.mediaUploads = Promise.resolve([]);
|
newUser.mediaUploads = Promise.resolve([]);
|
||||||
newUser.authors = Promise.resolve([]);
|
newUser.authors = Promise.resolve([]);
|
||||||
return newUser;
|
return newUser;
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
import { IsBoolean, IsLowercase, IsString } from 'class-validator';
|
import { IsBoolean, IsLowercase, IsString } from 'class-validator';
|
||||||
|
|
||||||
import { BaseDto } from '../utils/base.dto.';
|
import { BaseDto } from '../utils/base.dto';
|
||||||
import { Username } from '../utils/username';
|
import { Username } from '../utils/username';
|
||||||
|
|
||||||
export class UsernameCheckDto extends BaseDto {
|
export class UsernameCheckDto extends BaseDto {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue