diff --git a/backend/src/api-token/api-token.dto.ts b/backend/src/api-token/api-token.dto.ts index 64189fad8..4e9036c86 100644 --- a/backend/src/api-token/api-token.dto.ts +++ b/backend/src/api-token/api-token.dto.ts @@ -6,7 +6,7 @@ import { Type } from 'class-transformer'; 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'; export class ApiTokenDto extends BaseDto { diff --git a/backend/src/api/private/explore/explore.controller.ts b/backend/src/api/private/explore/explore.controller.ts new file mode 100644 index 000000000..c9e53ddfc --- /dev/null +++ b/backend/src/api/private/explore/explore.controller.ts @@ -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 { + 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 { + return []; + } + + @Get('public') + @OpenApi(200) + async getPublicNotes( + @Query('sort') sort: string, + @Query('search') search: string, + @Query('type') type: string, + ): Promise { + return []; + } +} diff --git a/backend/src/api/private/me/history/history.controller.ts b/backend/src/api/private/me/history/history.controller.ts deleted file mode 100644 index c35306a23..000000000 --- a/backend/src/api/private/me/history/history.controller.ts +++ /dev/null @@ -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 { - 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 { - await this.historyService.setHistory(user, historyImport.history); - } - - @Delete() - @OpenApi(204, 404) - async deleteHistory(@RequestUser() user: User): Promise { - await this.historyService.deleteHistory(user); - } - - @Put(':noteIdOrAlias') - @OpenApi(200, 404) - @UseInterceptors(GetNoteInterceptor) - async updateHistoryEntry( - @RequestNote() note: Note, - @RequestUser() user: User, - @Body() entryUpdateDto: HistoryEntryUpdateDto, - ): Promise { - 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 { - await this.historyService.deleteHistoryEntry(note, user); - } -} diff --git a/backend/src/api/private/private-api.module.ts b/backend/src/api/private/private-api.module.ts index 169ec3ec6..3c0b1f06d 100644 --- a/backend/src/api/private/private-api.module.ts +++ b/backend/src/api/private/private-api.module.ts @@ -9,7 +9,6 @@ import { ApiTokenModule } from '../../api-token/api-token.module'; import { AuthModule } from '../../auth/auth.module'; import { FrontendConfigModule } from '../../frontend-config/frontend-config.module'; import { GroupsModule } from '../../groups/groups.module'; -import { HistoryModule } from '../../history/history.module'; import { LoggerModule } from '../../logger/logger.module'; import { MediaModule } from '../../media/media.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 { ConfigController } from './config/config.controller'; import { GroupsController } from './groups/groups.controller'; -import { HistoryController } from './me/history/history.controller'; import { MeController } from './me/me.controller'; import { MediaController } from './media/media.controller'; import { NotesController } from './notes/notes.controller'; @@ -36,7 +34,6 @@ import { UsersController } from './users/users.controller'; UsersModule, ApiTokenModule, FrontendConfigModule, - HistoryModule, PermissionsModule, NotesModule, MediaModule, @@ -48,7 +45,6 @@ import { UsersController } from './users/users.controller'; ApiTokensController, ConfigController, MediaController, - HistoryController, MeController, NotesController, AliasController, diff --git a/backend/src/api/utils/openapi.decorator.ts b/backend/src/api/utils/openapi.decorator.ts index ceec342b8..e52af0503 100644 --- a/backend/src/api/utils/openapi.decorator.ts +++ b/backend/src/api/utils/openapi.decorator.ts @@ -17,7 +17,7 @@ import { ApiUnauthorizedResponse, } from '@nestjs/swagger'; -import { BaseDto } from '../../utils/base.dto.'; +import { BaseDto } from '../../utils/base.dto'; import { badRequestDescription, conflictDescription, diff --git a/backend/src/auth/pending-user-confirmation.dto.ts b/backend/src/auth/pending-user-confirmation.dto.ts index d4a3f82da..97cb9dbe2 100644 --- a/backend/src/auth/pending-user-confirmation.dto.ts +++ b/backend/src/auth/pending-user-confirmation.dto.ts @@ -5,7 +5,7 @@ */ import { IsOptional, IsString } from 'class-validator'; -import { BaseDto } from '../utils/base.dto.'; +import { BaseDto } from '../utils/base.dto'; export class PendingUserConfirmationDto extends BaseDto { @IsString() diff --git a/backend/src/explore/explore.service.ts b/backend/src/explore/explore.service.ts new file mode 100644 index 000000000..696758a67 --- /dev/null +++ b/backend/src/explore/explore.service.ts @@ -0,0 +1,5 @@ +/* + * SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ diff --git a/backend/src/frontend-config/frontend-config.dto.ts b/backend/src/frontend-config/frontend-config.dto.ts index 4805084d9..d04f5f30d 100644 --- a/backend/src/frontend-config/frontend-config.dto.ts +++ b/backend/src/frontend-config/frontend-config.dto.ts @@ -18,7 +18,7 @@ import { URL } from 'url'; import { ProviderType } from '../auth/provider-type.enum'; import { GuestAccess } from '../config/guest_access.enum'; import { ServerVersion } from '../monitoring/server-status.dto'; -import { BaseDto } from '../utils/base.dto.'; +import { BaseDto } from '../utils/base.dto'; export type AuthProviderTypeWithCustomName = | ProviderType.LDAP diff --git a/backend/src/groups/group-info.dto.ts b/backend/src/groups/group-info.dto.ts index f2ea39672..af09045da 100644 --- a/backend/src/groups/group-info.dto.ts +++ b/backend/src/groups/group-info.dto.ts @@ -6,7 +6,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsBoolean, IsString } from 'class-validator'; -import { BaseDto } from '../utils/base.dto.'; +import { BaseDto } from '../utils/base.dto'; export class GroupInfoDto extends BaseDto { /** diff --git a/backend/src/history/history-entry-import.dto.ts b/backend/src/history/history-entry-import.dto.ts deleted file mode 100644 index 92c79d394..000000000 --- a/backend/src/history/history-entry-import.dto.ts +++ /dev/null @@ -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[]; -} diff --git a/backend/src/history/history-entry-update.dto.ts b/backend/src/history/history-entry-update.dto.ts deleted file mode 100644 index fe0457dd5..000000000 --- a/backend/src/history/history-entry-update.dto.ts +++ /dev/null @@ -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; -} diff --git a/backend/src/history/history-entry.dto.ts b/backend/src/history/history-entry.dto.ts deleted file mode 100644 index c5ae719fa..000000000 --- a/backend/src/history/history-entry.dto.ts +++ /dev/null @@ -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; -} diff --git a/backend/src/history/history-entry.entity.ts b/backend/src/history/history-entry.entity.ts deleted file mode 100644 index 1bd1386b7..000000000 --- a/backend/src/history/history-entry.entity.ts +++ /dev/null @@ -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; - - @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; - - @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 { - const newHistoryEntry = new HistoryEntry(); - newHistoryEntry.user = Promise.resolve(user); - newHistoryEntry.note = Promise.resolve(note); - newHistoryEntry.pinStatus = pinStatus; - return newHistoryEntry; - } -} diff --git a/backend/src/history/history.module.ts b/backend/src/history/history.module.ts deleted file mode 100644 index f1693b49d..000000000 --- a/backend/src/history/history.module.ts +++ /dev/null @@ -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 {} diff --git a/backend/src/history/history.service.spec.ts b/backend/src/history/history.service.spec.ts deleted file mode 100644 index 5ca219d94..000000000 --- a/backend/src/history/history.service.spec.ts +++ /dev/null @@ -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; - let noteRepo: Repository; - let mockedTransaction: jest.Mock< - Promise, - [(entityManager: EntityManager) => Promise] - >; - - class CreateQueryBuilderClass { - leftJoinAndSelect: () => CreateQueryBuilderClass; - where: () => CreateQueryBuilderClass; - orWhere: () => CreateQueryBuilderClass; - setParameter: () => CreateQueryBuilderClass; - getOne: () => HistoryEntry; - getMany: () => HistoryEntry[]; - } - - let createQueryBuilderFunc: CreateQueryBuilderClass; - - beforeEach(async () => { - noteRepo = new Repository( - '', - 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({ - 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); - revisionsService = module.get(RevisionsService); - historyRepo = module.get>( - getRepositoryToken(HistoryEntry), - ); - noteRepo = module.get>(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 => 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 => 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 => 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 => { - 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 => { - expect(entry).toEqual(historyEntry); - return entry; - }, - ) - .mockImplementationOnce( - async (entry: HistoryEntry): Promise => { - 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 => { - 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({ - 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); - }); - }); - }); -}); diff --git a/backend/src/history/history.service.ts b/backend/src/history/history.service.ts deleted file mode 100644 index 93356e807..000000000 --- a/backend/src/history/history.service.ts +++ /dev/null @@ -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, - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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(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(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 { - 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, - }; - } -} diff --git a/backend/src/history/utils.spec.ts b/backend/src/history/utils.spec.ts deleted file mode 100644 index d65fc617b..000000000 --- a/backend/src/history/utils.spec.ts +++ /dev/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); - }); -}); diff --git a/backend/src/history/utils.ts b/backend/src/history/utils.ts deleted file mode 100644 index c964412eb..000000000 --- a/backend/src/history/utils.ts +++ /dev/null @@ -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 { - 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; -} diff --git a/backend/src/media/media-upload.dto.ts b/backend/src/media/media-upload.dto.ts index 37387ec4d..3949cb073 100644 --- a/backend/src/media/media-upload.dto.ts +++ b/backend/src/media/media-upload.dto.ts @@ -7,7 +7,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; 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'; export class MediaUploadDto extends BaseDto { diff --git a/backend/src/monitoring/server-status.dto.ts b/backend/src/monitoring/server-status.dto.ts index af20ec836..1231f9d1b 100644 --- a/backend/src/monitoring/server-status.dto.ts +++ b/backend/src/monitoring/server-status.dto.ts @@ -5,7 +5,7 @@ */ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { BaseDto } from '../utils/base.dto.'; +import { BaseDto } from '../utils/base.dto'; export class ServerVersion { @ApiProperty() diff --git a/backend/src/notes/alias-create.dto.ts b/backend/src/notes/alias-create.dto.ts index 6c04f4604..4159a38ad 100644 --- a/backend/src/notes/alias-create.dto.ts +++ b/backend/src/notes/alias-create.dto.ts @@ -6,7 +6,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsString } from 'class-validator'; -import { BaseDto } from '../utils/base.dto.'; +import { BaseDto } from '../utils/base.dto'; export class AliasCreateDto extends BaseDto { /** diff --git a/backend/src/notes/alias-update.dto.ts b/backend/src/notes/alias-update.dto.ts index 6ce7bf330..a6d581d84 100644 --- a/backend/src/notes/alias-update.dto.ts +++ b/backend/src/notes/alias-update.dto.ts @@ -6,7 +6,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsBoolean } from 'class-validator'; -import { BaseDto } from '../utils/base.dto.'; +import { BaseDto } from '../utils/base.dto'; export class AliasUpdateDto extends BaseDto { /** diff --git a/backend/src/notes/alias.dto.ts b/backend/src/notes/alias.dto.ts index 1f5c2af29..45bd5bc35 100644 --- a/backend/src/notes/alias.dto.ts +++ b/backend/src/notes/alias.dto.ts @@ -6,7 +6,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsBoolean, IsString } from 'class-validator'; -import { BaseDto } from '../utils/base.dto.'; +import { BaseDto } from '../utils/base.dto'; export class AliasDto extends BaseDto { /** diff --git a/backend/src/notes/note-explore-entry.dto.ts b/backend/src/notes/note-explore-entry.dto.ts new file mode 100644 index 000000000..5e0b8e229 --- /dev/null +++ b/backend/src/notes/note-explore-entry.dto.ts @@ -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; +} diff --git a/backend/src/notes/note-metadata.dto.ts b/backend/src/notes/note-metadata.dto.ts index cbb6a5e78..52e06f439 100644 --- a/backend/src/notes/note-metadata.dto.ts +++ b/backend/src/notes/note-metadata.dto.ts @@ -14,7 +14,7 @@ import { ValidateNested, } from 'class-validator'; -import { BaseDto } from '../utils/base.dto.'; +import { BaseDto } from '../utils/base.dto'; import { AliasDto } from './alias.dto'; import { NotePermissionsDto } from './note-permissions.dto'; diff --git a/backend/src/notes/note-permissions.dto.ts b/backend/src/notes/note-permissions.dto.ts index b462d79df..5fedcd262 100644 --- a/backend/src/notes/note-permissions.dto.ts +++ b/backend/src/notes/note-permissions.dto.ts @@ -14,7 +14,7 @@ import { ValidateNested, } from 'class-validator'; -import { BaseDto } from '../utils/base.dto.'; +import { BaseDto } from '../utils/base.dto'; import { Username } from '../utils/username'; export class NoteUserPermissionEntryDto extends BaseDto { diff --git a/backend/src/notes/note.dto.ts b/backend/src/notes/note.dto.ts index deff740f4..a64b50324 100644 --- a/backend/src/notes/note.dto.ts +++ b/backend/src/notes/note.dto.ts @@ -8,7 +8,7 @@ import { Type } from 'class-transformer'; import { IsArray, IsString, ValidateNested } from 'class-validator'; import { EditDto } from '../revisions/edit.dto'; -import { BaseDto } from '../utils/base.dto.'; +import { BaseDto } from '../utils/base.dto'; import { NoteMetadataDto } from './note-metadata.dto'; export class NoteDto extends BaseDto { diff --git a/backend/src/notes/note.entity.ts b/backend/src/notes/note.entity.ts index 5f69bb3a4..11cb390f9 100644 --- a/backend/src/notes/note.entity.ts +++ b/backend/src/notes/note.entity.ts @@ -12,7 +12,6 @@ import { PrimaryGeneratedColumn, } from 'typeorm'; -import { HistoryEntry } from '../history/history-entry.entity'; import { MediaUpload } from '../media/media-upload.entity'; import { NoteGroupPermission } from '../permissions/note-group-permission.entity'; import { NoteUserPermission } from '../permissions/note-user-permission.entity'; @@ -65,9 +64,6 @@ export class Note { @OneToMany((_) => Revision, (revision) => revision.note, { cascade: true }) revisions: Promise; - @OneToMany((_) => HistoryEntry, (historyEntry) => historyEntry.user) - historyEntries: Promise; - @OneToMany((_) => MediaUpload, (mediaUpload) => mediaUpload.note) mediaUploads: Promise; @@ -101,7 +97,6 @@ export class Note { newNote.viewCount = 0; newNote.owner = Promise.resolve(owner); newNote.revisions = Promise.resolve([]); - newNote.historyEntries = Promise.resolve([]); newNote.mediaUploads = Promise.resolve([]); newNote.version = 2; return newNote; diff --git a/backend/src/notes/note.media-deletion.dto.ts b/backend/src/notes/note.media-deletion.dto.ts index 1d8eaa184..09b153e39 100644 --- a/backend/src/notes/note.media-deletion.dto.ts +++ b/backend/src/notes/note.media-deletion.dto.ts @@ -6,7 +6,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsBoolean } from 'class-validator'; -import { BaseDto } from '../utils/base.dto.'; +import { BaseDto } from '../utils/base.dto'; export class NoteMediaDeletionDto extends BaseDto { /** diff --git a/backend/src/notes/notes.service.ts b/backend/src/notes/notes.service.ts index 0bc8e3691..8f54d9a2d 100644 --- a/backend/src/notes/notes.service.ts +++ b/backend/src/notes/notes.service.ts @@ -3,6 +3,7 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { NoteType } from '@hedgedoc/commons'; import { Optional } from '@mrdrogdrog/optional'; import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; @@ -20,7 +21,6 @@ import { import { NoteEvent, NoteEventMap } from '../events'; import { Group } from '../groups/group.entity'; import { GroupsService } from '../groups/groups.service'; -import { HistoryEntry } from '../history/history-entry.entity'; import { ConsoleLoggerService } from '../logger/console-logger.service'; import { NoteGroupPermission } from '../permissions/note-group-permission.entity'; 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 { Alias } from './alias.entity'; import { AliasService } from './alias.service'; +import { NoteExploreEntryDto } from './note-explore-entry.dto'; import { NoteMetadataDto } from './note-metadata.dto'; import { NotePermissionsDto } from './note-permissions.dto'; import { NoteDto } from './note.dto'; @@ -112,10 +113,6 @@ export class NotesService { let everyoneAccessLevel; 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 everyoneAccessLevel = this.noteConfig.permissions.default.everyone; } else { @@ -450,4 +447,28 @@ export class NotesService { 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 { + 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, + }; + } } diff --git a/backend/src/revisions/edit.dto.ts b/backend/src/revisions/edit.dto.ts index fcc6facdf..ccc5c1e3a 100644 --- a/backend/src/revisions/edit.dto.ts +++ b/backend/src/revisions/edit.dto.ts @@ -8,7 +8,7 @@ import { Type } from 'class-transformer'; import { IsDate, IsNumber, IsOptional, IsString, Min } from 'class-validator'; import { UserInfoDto } from '../users/user-info.dto'; -import { BaseDto } from '../utils/base.dto.'; +import { BaseDto } from '../utils/base.dto'; export class EditDto extends BaseDto { /** diff --git a/backend/src/revisions/revision-metadata.dto.ts b/backend/src/revisions/revision-metadata.dto.ts index 347edf470..b029a342c 100644 --- a/backend/src/revisions/revision-metadata.dto.ts +++ b/backend/src/revisions/revision-metadata.dto.ts @@ -7,7 +7,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; 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'; export class RevisionMetadataDto extends BaseDto { diff --git a/backend/src/revisions/revision.entity.ts b/backend/src/revisions/revision.entity.ts index cdd770220..0b0128360 100644 --- a/backend/src/revisions/revision.entity.ts +++ b/backend/src/revisions/revision.entity.ts @@ -3,6 +3,7 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { NoteType } from '@hedgedoc/commons'; import { Column, CreateDateColumn, @@ -60,6 +61,9 @@ export class Revision { }) content: string; + @Column() + type: NoteType; + /** * The length of the note content. */ diff --git a/backend/src/users/user-info.dto.ts b/backend/src/users/user-info.dto.ts index 7246c69ce..43088749d 100644 --- a/backend/src/users/user-info.dto.ts +++ b/backend/src/users/user-info.dto.ts @@ -7,7 +7,7 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsLowercase, IsOptional, IsString } from 'class-validator'; -import { BaseDto } from '../utils/base.dto.'; +import { BaseDto } from '../utils/base.dto'; import { Username } from '../utils/username'; export class UserInfoDto extends BaseDto { diff --git a/backend/src/users/user.entity.ts b/backend/src/users/user.entity.ts index 1003a995e..ece56114d 100644 --- a/backend/src/users/user.entity.ts +++ b/backend/src/users/user.entity.ts @@ -65,8 +65,8 @@ export class User { @ManyToMany((_) => Group, (group) => group.members) groups: Promise; - @OneToMany((_) => HistoryEntry, (historyEntry) => historyEntry.user) - historyEntries: Promise; + @OneToMany((_) => Note, (note) => note.owner) + pinnedNotes: Promise; @OneToMany((_) => MediaUpload, (mediaUpload) => mediaUpload.user) mediaUploads: Promise; @@ -92,7 +92,7 @@ export class User { newUser.apiTokens = Promise.resolve([]); newUser.identities = Promise.resolve([]); newUser.groups = Promise.resolve([]); - newUser.historyEntries = Promise.resolve([]); + newUser.pinnedNotes = Promise.resolve([]); newUser.mediaUploads = Promise.resolve([]); newUser.authors = Promise.resolve([]); return newUser; diff --git a/backend/src/users/username-check.dto.ts b/backend/src/users/username-check.dto.ts index 8709aea0b..821cd1ae5 100644 --- a/backend/src/users/username-check.dto.ts +++ b/backend/src/users/username-check.dto.ts @@ -5,7 +5,7 @@ */ import { IsBoolean, IsLowercase, IsString } from 'class-validator'; -import { BaseDto } from '../utils/base.dto.'; +import { BaseDto } from '../utils/base.dto'; import { Username } from '../utils/username'; export class UsernameCheckDto extends BaseDto { diff --git a/backend/src/utils/base.dto..ts b/backend/src/utils/base.dto.ts similarity index 100% rename from backend/src/utils/base.dto..ts rename to backend/src/utils/base.dto.ts