From 21a1f35281232bcb4a683b30a0575176df2afdd0 Mon Sep 17 00:00:00 2001 From: Erik Michelson Date: Sat, 17 May 2025 23:27:47 +0200 Subject: [PATCH] refactor(database): run knex migrations on startup Co-authored-by: Philip Molares Signed-off-by: Philip Molares Signed-off-by: Erik Michelson --- backend/src/alias/alias.service.ts | 16 ++--- backend/src/api-token/api-token.service.ts | 24 +++++-- .../api-tokens/api-tokens.controller.ts | 2 +- .../api/private/auth/ldap/ldap.controller.ts | 2 +- .../private/auth/local/local.controller.ts | 2 +- .../api/private/auth/oidc/oidc.controller.ts | 2 +- .../src/api/public/alias/alias.controller.ts | 1 - backend/src/api/public/me/me.controller.ts | 2 +- .../src/api/public/media/media.controller.ts | 14 ++-- .../utils/extract-note-from-request.spec.ts | 2 +- .../api/utils/extract-note-id-from-request.ts | 2 +- .../get-note-id.interceptor.spec.ts | 6 +- backend/src/api/utils/request.type.ts | 3 +- backend/src/app-init.ts | 12 +++- backend/src/auth/identity.service.ts | 20 +++--- backend/src/auth/local/local.service.ts | 14 ++-- backend/src/auth/oidc/oidc.service.ts | 2 +- backend/src/config/media.config.spec.ts | 48 ++++++------- backend/src/config/media.config.ts | 12 ++-- backend/src/config/mock/media.config.mock.ts | 4 +- ...2_initial.ts => 20250312211152_initial.js} | 69 ++++++++++--------- backend/src/database/seeds/01_user.ts | 8 +-- backend/src/database/seeds/02_api_token.ts | 3 +- backend/src/database/seeds/03_note.ts | 15 ++-- backend/src/database/types/index.ts | 44 ------------ backend/src/database/types/knex.types.ts | 52 ++++++++------ .../frontend-config.service.ts | 2 +- backend/src/groups/groups.service.ts | 13 ++-- backend/src/media/backends/azure-backend.ts | 4 +- .../src/media/backends/backend-type.enum.ts | 8 --- backend/src/media/backends/s3-backend.spec.ts | 4 +- backend/src/media/backends/s3-backend.ts | 4 +- backend/src/media/backends/webdav-backend.ts | 4 +- backend/src/media/media.service.ts | 52 +++++++------- backend/src/notes/note.service.ts | 69 ++++++++++++------- backend/src/permissions/permission.service.ts | 15 ++-- backend/src/permissions/permissions.guard.ts | 2 +- .../realtime-note/realtime-connection.spec.ts | 2 +- .../realtime-note/realtime-note.service.ts | 3 +- .../test-utils/mock-connection.ts | 2 +- .../realtime/websocket/websocket.gateway.ts | 2 +- backend/src/revisions/revisions.service.ts | 61 +++++++++------- backend/src/sessions/session-state.type.ts | 3 +- backend/src/sessions/session.service.ts | 2 +- backend/src/users/users.service.ts | 15 ++-- commons/src/dtos/media/index.ts | 1 + .../src/dtos/media/media-backend-type.enum.ts | 13 ++++ database/.eslintrc.cjs | 40 +++++++++++ database/.gitignore | 31 +++++++++ database/.npmignore | 9 +++ database/.npmignore.license | 3 + database/.prettierignore | 1 + database/.prettierignore.license | 4 ++ database/.prettierrc.json | 11 +++ database/.prettierrc.json.license | 3 + database/build.sh | 23 +++++++ database/jest.config.json | 27 ++++++++ database/jest.config.json.license | 3 + database/package.json | 47 +++++++++++++ database/package.json.license | 3 + database/src/index.ts | 6 ++ .../database => database/src}/types/alias.ts | 12 ++-- .../src}/types/api-token.ts | 37 +++++++--- .../src}/types/authorship-info.ts | 22 ++++-- .../src}/types/group-user.ts | 6 +- .../database => database/src}/types/group.ts | 19 +++-- .../src}/types/identity.ts | 39 +++++++---- database/src/types/index.ts | 20 ++++++ .../src}/types/media-upload.ts | 37 ++++++---- .../src}/types/note-group-permission.ts | 10 +-- .../src}/types/note-user-permission.ts | 10 +-- .../database => database/src}/types/note.ts | 20 +++--- .../src}/types/revision-tag.ts | 6 +- .../src}/types/revision.ts | 35 ++++++---- .../src}/types/user-pinned-note.ts | 6 +- .../database => database/src}/types/user.ts | 28 ++++---- database/tsconfig.base.json | 21 ++++++ database/tsconfig.base.json.license | 3 + database/tsconfig.cjs.json | 10 +++ database/tsconfig.cjs.json.license | 3 + database/tsconfig.test.json | 4 ++ database/tsconfig.test.json.license | 3 + package.json | 1 + turbo.json | 8 +++ yarn.lock | 15 ++++ 85 files changed, 830 insertions(+), 418 deletions(-) rename backend/src/database/migrations/{20250312211152_initial.ts => 20250312211152_initial.js} (91%) delete mode 100644 backend/src/database/types/index.ts create mode 100644 commons/src/dtos/media/media-backend-type.enum.ts create mode 100644 database/.eslintrc.cjs create mode 100644 database/.gitignore create mode 100644 database/.npmignore create mode 100644 database/.npmignore.license create mode 100644 database/.prettierignore create mode 100644 database/.prettierignore.license create mode 100644 database/.prettierrc.json create mode 100644 database/.prettierrc.json.license create mode 100755 database/build.sh create mode 100644 database/jest.config.json create mode 100644 database/jest.config.json.license create mode 100644 database/package.json create mode 100644 database/package.json.license create mode 100644 database/src/index.ts rename {backend/src/database => database/src}/types/alias.ts (80%) rename {backend/src/database => database/src}/types/api-token.ts (56%) rename {backend/src/database => database/src}/types/authorship-info.ts (62%) rename {backend/src/database => database/src}/types/group-user.ts (76%) rename {backend/src/database => database/src}/types/group.ts (72%) rename {backend/src/database => database/src}/types/identity.ts (63%) create mode 100644 database/src/types/index.ts rename {backend/src/database => database/src}/types/media-upload.ts (66%) rename {backend/src/database => database/src}/types/note-group-permission.ts (76%) rename {backend/src/database => database/src}/types/note-user-permission.ts (76%) rename {backend/src/database => database/src}/types/note.ts (72%) rename {backend/src/database => database/src}/types/revision-tag.ts (77%) rename {backend/src/database => database/src}/types/revision.ts (61%) rename {backend/src/database => database/src}/types/user-pinned-note.ts (83%) rename {backend/src/database => database/src}/types/user.ts (80%) create mode 100644 database/tsconfig.base.json create mode 100644 database/tsconfig.base.json.license create mode 100644 database/tsconfig.cjs.json create mode 100644 database/tsconfig.cjs.json.license create mode 100644 database/tsconfig.test.json create mode 100644 database/tsconfig.test.json.license diff --git a/backend/src/alias/alias.service.ts b/backend/src/alias/alias.service.ts index 23e7e2858..1bdd67a67 100644 --- a/backend/src/alias/alias.service.ts +++ b/backend/src/alias/alias.service.ts @@ -4,6 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { AliasDto } from '@hedgedoc/commons'; +import { + Alias, + FieldNameAlias, + FieldNameNote, + Note, + TableAlias, + TypeInsertAlias, +} from '@hedgedoc/database'; import { Inject, Injectable } from '@nestjs/common'; import base32Encode from 'base32-encode'; import { randomBytes } from 'crypto'; @@ -11,14 +19,6 @@ import { Knex } from 'knex'; import { InjectConnection } from 'nest-knexjs'; import noteConfiguration, { NoteConfig } from '../config/note.config'; -import { - Alias, - FieldNameAlias, - FieldNameNote, - Note, - TableAlias, -} from '../database/types'; -import { TypeInsertAlias } from '../database/types/alias'; import { AlreadyInDBError, ForbiddenIdError, diff --git a/backend/src/api-token/api-token.service.ts b/backend/src/api-token/api-token.service.ts index 752d669ec..87b2c99ca 100644 --- a/backend/src/api-token/api-token.service.ts +++ b/backend/src/api-token/api-token.service.ts @@ -4,14 +4,18 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { ApiTokenDto, ApiTokenWithSecretDto } from '@hedgedoc/commons'; +import { + ApiToken, + FieldNameApiToken, + TableApiToken, + TypeInsertApiToken, +} from '@hedgedoc/database'; import { Injectable } from '@nestjs/common'; import { Cron, Timeout } from '@nestjs/schedule'; import { randomBytes } from 'crypto'; import { Knex } from 'knex'; import { InjectConnection } from 'nest-knexjs'; -import { ApiToken, FieldNameApiToken, TableApiToken } from '../database/types'; -import { TypeInsertApiToken } from '../database/types/api-token'; import { NotInDBError, TokenNotValidError, @@ -71,7 +75,7 @@ export class ApiTokenService { } const tokenHash = token[FieldNameApiToken.secretHash]; - const validUntil = token[FieldNameApiToken.validUntil]; + const validUntil = new Date(token[FieldNameApiToken.validUntil]); this.ensureTokenIsValid(secret, tokenHash, validUntil); await transaction(TableApiToken) @@ -133,6 +137,10 @@ export class ApiTokenService { return this.toAuthTokenWithSecretDto( { ...token, + [FieldNameApiToken.validUntil]: + token[FieldNameApiToken.validUntil].toISOString(), + [FieldNameApiToken.createdAt]: + token[FieldNameApiToken.createdAt].toISOString(), [FieldNameApiToken.lastUsedAt]: null, }, secret, @@ -206,9 +214,13 @@ export class ApiTokenService { return { label: apiToken[FieldNameApiToken.label], keyId: apiToken[FieldNameApiToken.id], - createdAt: apiToken[FieldNameApiToken.createdAt].toISOString(), - validUntil: apiToken[FieldNameApiToken.validUntil].toISOString(), - lastUsedAt: apiToken[FieldNameApiToken.lastUsedAt]?.toISOString() ?? null, + createdAt: new Date(apiToken[FieldNameApiToken.createdAt]).toISOString(), + validUntil: new Date( + apiToken[FieldNameApiToken.validUntil], + ).toISOString(), + lastUsedAt: apiToken[FieldNameApiToken.lastUsedAt] + ? new Date(apiToken[FieldNameApiToken.lastUsedAt]).toISOString() + : null, }; } diff --git a/backend/src/api/private/api-tokens/api-tokens.controller.ts b/backend/src/api/private/api-tokens/api-tokens.controller.ts index d583afdd4..d858cf072 100644 --- a/backend/src/api/private/api-tokens/api-tokens.controller.ts +++ b/backend/src/api/private/api-tokens/api-tokens.controller.ts @@ -8,6 +8,7 @@ import { ApiTokenDto, ApiTokenWithSecretDto, } from '@hedgedoc/commons'; +import { FieldNameUser, User } from '@hedgedoc/database'; import { Body, Controller, @@ -21,7 +22,6 @@ import { ApiTags } from '@nestjs/swagger'; import { ApiTokenService } from '../../../api-token/api-token.service'; import { SessionGuard } from '../../../auth/session.guard'; -import { FieldNameUser, User } from '../../../database/types'; import { ConsoleLoggerService } from '../../../logger/console-logger.service'; import { OpenApi } from '../../utils/decorators/openapi.decorator'; import { RequestUserId } from '../../utils/decorators/request-user-id.decorator'; diff --git a/backend/src/api/private/auth/ldap/ldap.controller.ts b/backend/src/api/private/auth/ldap/ldap.controller.ts index 60eb6020a..0f1c86825 100644 --- a/backend/src/api/private/auth/ldap/ldap.controller.ts +++ b/backend/src/api/private/auth/ldap/ldap.controller.ts @@ -8,6 +8,7 @@ import { LdapLoginDto, LdapLoginResponseDto, } from '@hedgedoc/commons'; +import { FieldNameIdentity } from '@hedgedoc/database'; import { Body, Controller, @@ -20,7 +21,6 @@ import { ApiTags } from '@nestjs/swagger'; import { IdentityService } from '../../../../auth/identity.service'; import { LdapService } from '../../../../auth/ldap/ldap.service'; -import { FieldNameIdentity } from '../../../../database/types'; import { NotInDBError } from '../../../../errors/errors'; import { ConsoleLoggerService } from '../../../../logger/console-logger.service'; import { UsersService } from '../../../../users/users.service'; diff --git a/backend/src/api/private/auth/local/local.controller.ts b/backend/src/api/private/auth/local/local.controller.ts index c6f434d1e..d29b048a6 100644 --- a/backend/src/api/private/auth/local/local.controller.ts +++ b/backend/src/api/private/auth/local/local.controller.ts @@ -9,6 +9,7 @@ import { RegisterDto, UpdatePasswordDto, } from '@hedgedoc/commons'; +import { FieldNameIdentity, FieldNameUser } from '@hedgedoc/database'; import { Body, Controller, @@ -22,7 +23,6 @@ import { ApiTags } from '@nestjs/swagger'; import { LocalService } from '../../../../auth/local/local.service'; import { SessionGuard } from '../../../../auth/session.guard'; -import { FieldNameIdentity, FieldNameUser } from '../../../../database/types'; import { NoLocalIdentityError } from '../../../../errors/errors'; import { ConsoleLoggerService } from '../../../../logger/console-logger.service'; import { UsersService } from '../../../../users/users.service'; diff --git a/backend/src/api/private/auth/oidc/oidc.controller.ts b/backend/src/api/private/auth/oidc/oidc.controller.ts index 4f072dcd5..d8eb44874 100644 --- a/backend/src/api/private/auth/oidc/oidc.controller.ts +++ b/backend/src/api/private/auth/oidc/oidc.controller.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { AuthProviderType } from '@hedgedoc/commons'; +import { FieldNameIdentity } from '@hedgedoc/database'; import { Controller, Get, @@ -18,7 +19,6 @@ import { ApiTags } from '@nestjs/swagger'; import { IdentityService } from '../../../../auth/identity.service'; import { OidcService } from '../../../../auth/oidc/oidc.service'; -import { FieldNameIdentity } from '../../../../database/types'; import { ConsoleLoggerService } from '../../../../logger/console-logger.service'; import { UsersService } from '../../../../users/users.service'; import { OpenApi } from '../../../utils/decorators/openapi.decorator'; diff --git a/backend/src/api/public/alias/alias.controller.ts b/backend/src/api/public/alias/alias.controller.ts index 0571d0d34..7ddf8bf41 100644 --- a/backend/src/api/public/alias/alias.controller.ts +++ b/backend/src/api/public/alias/alias.controller.ts @@ -23,7 +23,6 @@ import { import { ApiSecurity, ApiTags } from '@nestjs/swagger'; import { AliasService } from '../../../alias/alias.service'; -import { User } from '../../../database/types'; import { ConsoleLoggerService } from '../../../logger/console-logger.service'; import { NoteService } from '../../../notes/note.service'; import { PermissionService } from '../../../permissions/permission.service'; diff --git a/backend/src/api/public/me/me.controller.ts b/backend/src/api/public/me/me.controller.ts index dc17298d5..729ebd35a 100644 --- a/backend/src/api/public/me/me.controller.ts +++ b/backend/src/api/public/me/me.controller.ts @@ -12,9 +12,9 @@ import { NoteMetadataDto, NoteMetadataSchema, } from '@hedgedoc/commons'; +import { User } from '@hedgedoc/database'; import { Controller, Get, UseGuards } from '@nestjs/common'; import { ApiSecurity, ApiTags } from '@nestjs/swagger'; -import { User } from 'src/database/types'; import { ConsoleLoggerService } from '../../../logger/console-logger.service'; import { MediaService } from '../../../media/media.service'; diff --git a/backend/src/api/public/media/media.controller.ts b/backend/src/api/public/media/media.controller.ts index 99631f02c..09f6df8bc 100644 --- a/backend/src/api/public/media/media.controller.ts +++ b/backend/src/api/public/media/media.controller.ts @@ -4,6 +4,13 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { MediaUploadDto, MediaUploadSchema } from '@hedgedoc/commons'; +import { + FieldNameMediaUpload, + FieldNameNote, + FieldNameUser, + Note, + User, +} from '@hedgedoc/database'; import { BadRequestException, Controller, @@ -24,13 +31,6 @@ import { ApiTags, } from '@nestjs/swagger'; -import { - FieldNameMediaUpload, - FieldNameNote, - FieldNameUser, - Note, - User, -} from '../../../database/types'; import { PermissionError } from '../../../errors/errors'; import { ConsoleLoggerService } from '../../../logger/console-logger.service'; import { MediaService } from '../../../media/media.service'; diff --git a/backend/src/api/utils/extract-note-from-request.spec.ts b/backend/src/api/utils/extract-note-from-request.spec.ts index 615ef1586..d9cb68037 100644 --- a/backend/src/api/utils/extract-note-from-request.spec.ts +++ b/backend/src/api/utils/extract-note-from-request.spec.ts @@ -3,9 +3,9 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { Note } from '@hedgedoc/database'; import { Mock } from 'ts-mockery'; -import { Note } from '../../database/types'; import { NoteService } from '../../notes/note.service'; import { extractNoteIdFromRequest } from './extract-note-id-from-request'; import { CompleteRequest } from './request.type'; diff --git a/backend/src/api/utils/extract-note-id-from-request.ts b/backend/src/api/utils/extract-note-id-from-request.ts index 24daf5733..aa22d3627 100644 --- a/backend/src/api/utils/extract-note-id-from-request.ts +++ b/backend/src/api/utils/extract-note-id-from-request.ts @@ -3,9 +3,9 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { FieldNameNote, Note } from '@hedgedoc/database'; import { isArray } from 'class-validator'; -import { FieldNameNote, Note } from '../../database/types'; import { NoteService } from '../../notes/note.service'; import { CompleteRequest } from './request.type'; diff --git a/backend/src/api/utils/interceptors/get-note-id.interceptor.spec.ts b/backend/src/api/utils/interceptors/get-note-id.interceptor.spec.ts index 16e3b194f..41627ac9f 100644 --- a/backend/src/api/utils/interceptors/get-note-id.interceptor.spec.ts +++ b/backend/src/api/utils/interceptors/get-note-id.interceptor.spec.ts @@ -3,15 +3,15 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { Note } from '@hedgedoc/database'; import { CallHandler, ExecutionContext } from '@nestjs/common'; import { HttpArgumentsHost } from '@nestjs/common/interfaces/features/arguments-host.interface'; import { Observable } from 'rxjs'; import { Mock } from 'ts-mockery'; -import { Note } from '../../database/types'; -import { NoteService } from '../../notes/note.service'; +import { NoteService } from '../../../notes/note.service'; +import { CompleteRequest } from '../request.type'; import { GetNoteIdInterceptor } from './get-note-id.interceptor'; -import { CompleteRequest } from './request.type'; describe('get note interceptor', () => { const mockNote = Mock.of({}); diff --git a/backend/src/api/utils/request.type.ts b/backend/src/api/utils/request.type.ts index b9374799b..bdfff704b 100644 --- a/backend/src/api/utils/request.type.ts +++ b/backend/src/api/utils/request.type.ts @@ -4,11 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { AuthProviderType } from '@hedgedoc/commons'; +import { FieldNameNote, FieldNameUser, Note, User } from '@hedgedoc/database'; import { Request } from 'express'; import { SessionState } from 'src/sessions/session-state.type'; -import { FieldNameNote, FieldNameUser, Note, User } from '../../database/types'; - export type CompleteRequest = Request & { userId?: User[FieldNameUser.id]; authProviderType?: AuthProviderType; diff --git a/backend/src/app-init.ts b/backend/src/app-init.ts index 282e41ac7..3f4263857 100644 --- a/backend/src/app-init.ts +++ b/backend/src/app-init.ts @@ -3,16 +3,18 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { MediaBackendType } from '@hedgedoc/commons'; import { HttpAdapterHost } from '@nestjs/core'; import { NestExpressApplication } from '@nestjs/platform-express'; import { WsAdapter } from '@nestjs/platform-ws'; +import { Knex } from 'knex'; +import { getConnectionToken } from 'nest-knexjs'; import { AppConfig } from './config/app.config'; import { AuthConfig } from './config/auth.config'; import { MediaConfig } from './config/media.config'; import { ErrorExceptionMapping } from './errors/error-mapping'; import { ConsoleLoggerService } from './logger/console-logger.service'; -import { BackendType } from './media/backends/backend-type.enum'; import { SessionService } from './sessions/session.service'; import { setupSessionMiddleware } from './utils/session'; import { setupValidationPipe } from './utils/setup-pipes'; @@ -42,6 +44,12 @@ export async function setupApp( ); } + logger.log('Starting database migrations... ', 'AppBootstrap'); + const knexConnectionToken = getConnectionToken(); + const knex: Knex = app.get(knexConnectionToken); + await knex.migrate.latest(); + logger.log('Finished database migrations... ', 'AppBootstrap'); + // Setup session handling setupSessionMiddleware( app, @@ -65,7 +73,7 @@ export async function setupApp( app.useGlobalPipes(setupValidationPipe(logger)); // Map URL paths to directories - if (mediaConfig.backend.use === BackendType.FILESYSTEM) { + if (mediaConfig.backend.use === MediaBackendType.FILESYSTEM) { logger.log( `Serving the local folder '${mediaConfig.backend.filesystem.uploadPath}' under '/uploads'`, 'AppBootstrap', diff --git a/backend/src/auth/identity.service.ts b/backend/src/auth/identity.service.ts index ef5c113d6..2d058dc55 100644 --- a/backend/src/auth/identity.service.ts +++ b/backend/src/auth/identity.service.ts @@ -8,18 +8,19 @@ import { PendingUserConfirmationDto, PendingUserInfoDto, } from '@hedgedoc/commons'; -import { Inject, Injectable } from '@nestjs/common'; -import { Knex } from 'knex'; -import { InjectConnection } from 'nest-knexjs'; - -import AuthConfiguration, { AuthConfig } from '../config/auth.config'; import { FieldNameIdentity, FieldNameUser, Identity, TableIdentity, + TypeInsertIdentity, User, -} from '../database/types'; +} from '@hedgedoc/database'; +import { Inject, Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { InjectConnection } from 'nest-knexjs'; + +import AuthConfiguration, { AuthConfig } from '../config/auth.config'; import { NotInDBError } from '../errors/errors'; import { ConsoleLoggerService } from '../logger/console-logger.service'; import { UsersService } from '../users/users.service'; @@ -52,7 +53,7 @@ export class IdentityService { /** * Retrieve an identity from the information received from an auth provider. * - * @param userId - the userId of the wanted identity + * @param authProviderUserId - the userId of the wanted identity * @param authProviderType - the providerType of the wanted identity * @param authProviderIdentifier - optional name of the provider if multiple exist * @return @@ -96,15 +97,12 @@ export class IdentityService { transaction?: Knex, ): Promise { const dbActor = transaction ?? this.knex; - const date = new Date(); - const identity: Identity = { + const identity: TypeInsertIdentity = { [FieldNameIdentity.userId]: userId, [FieldNameIdentity.providerType]: authProviderType, [FieldNameIdentity.providerIdentifier]: authProviderIdentifier, [FieldNameIdentity.providerUserId]: authProviderUserId, [FieldNameIdentity.passwordHash]: passwordHash ?? null, - [FieldNameIdentity.createdAt]: date, - [FieldNameIdentity.updatedAt]: date, }; await dbActor(TableIdentity).insert(identity); } diff --git a/backend/src/auth/local/local.service.ts b/backend/src/auth/local/local.service.ts index 0e69aeade..79dfe3063 100644 --- a/backend/src/auth/local/local.service.ts +++ b/backend/src/auth/local/local.service.ts @@ -4,6 +4,13 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { AuthProviderType } from '@hedgedoc/commons'; +import { + FieldNameIdentity, + FieldNameUser, + Identity, + TableIdentity, + User, +} from '@hedgedoc/database'; import { Inject, Injectable } from '@nestjs/common'; import { OptionsGraph, @@ -23,13 +30,6 @@ import { Knex } from 'knex'; import { InjectConnection } from 'nest-knexjs'; import authConfiguration, { AuthConfig } from '../../config/auth.config'; -import { - FieldNameIdentity, - FieldNameUser, - Identity, - TableIdentity, - User, -} from '../../database/types'; import { InvalidCredentialsError, PasswordTooWeakError, diff --git a/backend/src/auth/oidc/oidc.service.ts b/backend/src/auth/oidc/oidc.service.ts index c8fce28e6..fd97e741e 100644 --- a/backend/src/auth/oidc/oidc.service.ts +++ b/backend/src/auth/oidc/oidc.service.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { AuthProviderType, PendingUserInfoDto } from '@hedgedoc/commons'; +import { Identity } from '@hedgedoc/database'; import { ForbiddenException, Inject, @@ -20,7 +21,6 @@ import authConfiguration, { AuthConfig, OidcConfig, } from '../../config/auth.config'; -import { Identity } from '../../database/types'; import { NotInDBError } from '../../errors/errors'; import { ConsoleLoggerService } from '../../logger/console-logger.service'; import { IdentityService } from '../identity.service'; diff --git a/backend/src/config/media.config.spec.ts b/backend/src/config/media.config.spec.ts index 84740b502..a8c0e2d7e 100644 --- a/backend/src/config/media.config.spec.ts +++ b/backend/src/config/media.config.spec.ts @@ -3,9 +3,9 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { MediaBackendType } from '@hedgedoc/commons'; import mockedEnv from 'mocked-env'; -import { BackendType } from '../media/backends/backend-type.enum'; import mediaConfig, { AzureMediaConfig, FilesystemMediaConfig, @@ -39,7 +39,7 @@ describe('mediaConfig', () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_MEDIA_BACKEND: BackendType.FILESYSTEM, + HD_MEDIA_BACKEND: MediaBackendType.FILESYSTEM, HD_MEDIA_BACKEND_FILESYSTEM_UPLOAD_PATH: uploadPath, /* eslint-enable @typescript-eslint/naming-convention */ }, @@ -48,7 +48,7 @@ describe('mediaConfig', () => { }, ); const config = mediaConfig() as { backend: FilesystemMediaConfig }; - expect(config.backend.use).toEqual(BackendType.FILESYSTEM); + expect(config.backend.use).toEqual(MediaBackendType.FILESYSTEM); expect(config.backend.filesystem.uploadPath).toEqual(uploadPath); restore(); }); @@ -57,7 +57,7 @@ describe('mediaConfig', () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_MEDIA_BACKEND: BackendType.S3, + HD_MEDIA_BACKEND: MediaBackendType.S3, HD_MEDIA_BACKEND_S3_ACCESS_KEY: accessKeyId, HD_MEDIA_BACKEND_S3_SECRET_KEY: secretAccessKey, HD_MEDIA_BACKEND_S3_BUCKET: bucket, @@ -71,7 +71,7 @@ describe('mediaConfig', () => { }, ); const config = mediaConfig() as { backend: S3MediaConfig }; - expect(config.backend.use).toEqual(BackendType.S3); + expect(config.backend.use).toEqual(MediaBackendType.S3); expect(config.backend.s3.accessKeyId).toEqual(accessKeyId); expect(config.backend.s3.secretAccessKey).toEqual(secretAccessKey); expect(config.backend.s3.bucket).toEqual(bucket); @@ -85,7 +85,7 @@ describe('mediaConfig', () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_MEDIA_BACKEND: BackendType.AZURE, + HD_MEDIA_BACKEND: MediaBackendType.AZURE, HD_MEDIA_BACKEND_AZURE_CONNECTION_STRING: azureConnectionString, HD_MEDIA_BACKEND_AZURE_CONTAINER: container, /* eslint-enable @typescript-eslint/naming-convention */ @@ -95,7 +95,7 @@ describe('mediaConfig', () => { }, ); const config = mediaConfig() as { backend: AzureMediaConfig }; - expect(config.backend.use).toEqual(BackendType.AZURE); + expect(config.backend.use).toEqual(MediaBackendType.AZURE); expect(config.backend.azure.connectionString).toEqual( azureConnectionString, ); @@ -107,7 +107,7 @@ describe('mediaConfig', () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_MEDIA_BACKEND: BackendType.IMGUR, + HD_MEDIA_BACKEND: MediaBackendType.IMGUR, HD_MEDIA_BACKEND_IMGUR_CLIENT_ID: clientID, /* eslint-enable @typescript-eslint/naming-convention */ }, @@ -116,7 +116,7 @@ describe('mediaConfig', () => { }, ); const config = mediaConfig() as { backend: ImgurMediaConfig }; - expect(config.backend.use).toEqual(BackendType.IMGUR); + expect(config.backend.use).toEqual(MediaBackendType.IMGUR); expect(config.backend.imgur.clientId).toEqual(clientID); restore(); }); @@ -125,7 +125,7 @@ describe('mediaConfig', () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_MEDIA_BACKEND: BackendType.WEBDAV, + HD_MEDIA_BACKEND: MediaBackendType.WEBDAV, HD_MEDIA_BACKEND_WEBDAV_CONNECTION_STRING: webdavConnectionString, HD_MEDIA_BACKEND_WEBDAV_UPLOAD_DIR: uploadDir, HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL: publicUrl, @@ -136,7 +136,7 @@ describe('mediaConfig', () => { }, ); const config = mediaConfig() as { backend: WebdavMediaConfig }; - expect(config.backend.use).toEqual(BackendType.WEBDAV); + expect(config.backend.use).toEqual(MediaBackendType.WEBDAV); expect(config.backend.webdav.connectionString).toEqual( webdavConnectionString, ); @@ -152,7 +152,7 @@ describe('mediaConfig', () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_MEDIA_BACKEND: BackendType.FILESYSTEM, + HD_MEDIA_BACKEND: MediaBackendType.FILESYSTEM, /* eslint-enable @typescript-eslint/naming-convention */ }, { @@ -171,7 +171,7 @@ describe('mediaConfig', () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_MEDIA_BACKEND: BackendType.S3, + HD_MEDIA_BACKEND: MediaBackendType.S3, HD_MEDIA_BACKEND_S3_SECRET_KEY: secretAccessKey, HD_MEDIA_BACKEND_S3_BUCKET: bucket, HD_MEDIA_BACKEND_S3_ENDPOINT: endPoint, @@ -190,7 +190,7 @@ describe('mediaConfig', () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_MEDIA_BACKEND: BackendType.S3, + HD_MEDIA_BACKEND: MediaBackendType.S3, HD_MEDIA_BACKEND_S3_ACCESS_KEY: accessKeyId, HD_MEDIA_BACKEND_S3_BUCKET: bucket, HD_MEDIA_BACKEND_S3_ENDPOINT: endPoint, @@ -209,7 +209,7 @@ describe('mediaConfig', () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_MEDIA_BACKEND: BackendType.S3, + HD_MEDIA_BACKEND: MediaBackendType.S3, HD_MEDIA_BACKEND_S3_ACCESS_KEY: accessKeyId, HD_MEDIA_BACKEND_S3_SECRET_KEY: secretAccessKey, HD_MEDIA_BACKEND_S3_ENDPOINT: endPoint, @@ -228,7 +228,7 @@ describe('mediaConfig', () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_MEDIA_BACKEND: BackendType.S3, + HD_MEDIA_BACKEND: MediaBackendType.S3, HD_MEDIA_BACKEND_S3_ACCESS_KEY: accessKeyId, HD_MEDIA_BACKEND_S3_SECRET_KEY: secretAccessKey, HD_MEDIA_BACKEND_S3_BUCKET: bucket, @@ -247,7 +247,7 @@ describe('mediaConfig', () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_MEDIA_BACKEND: BackendType.S3, + HD_MEDIA_BACKEND: MediaBackendType.S3, HD_MEDIA_BACKEND_S3_ACCESS_KEY: accessKeyId, HD_MEDIA_BACKEND_S3_SECRET_KEY: secretAccessKey, HD_MEDIA_BACKEND_S3_BUCKET: bucket, @@ -270,7 +270,7 @@ describe('mediaConfig', () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_MEDIA_BACKEND: BackendType.AZURE, + HD_MEDIA_BACKEND: MediaBackendType.AZURE, HD_MEDIA_BACKEND_AZURE_CONTAINER: container, /* eslint-enable @typescript-eslint/naming-convention */ }, @@ -287,7 +287,7 @@ describe('mediaConfig', () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_MEDIA_BACKEND: BackendType.AZURE, + HD_MEDIA_BACKEND: MediaBackendType.AZURE, HD_MEDIA_BACKEND_AZURE_CONNECTION_STRING: azureConnectionString, /* eslint-enable @typescript-eslint/naming-convention */ }, @@ -307,7 +307,7 @@ describe('mediaConfig', () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_MEDIA_BACKEND: BackendType.IMGUR, + HD_MEDIA_BACKEND: MediaBackendType.IMGUR, /* eslint-enable @typescript-eslint/naming-convention */ }, { @@ -326,7 +326,7 @@ describe('mediaConfig', () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_MEDIA_BACKEND: BackendType.WEBDAV, + HD_MEDIA_BACKEND: MediaBackendType.WEBDAV, HD_MEDIA_BACKEND_WEBDAV_UPLOAD_DIR: uploadDir, HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL: publicUrl, /* eslint-enable @typescript-eslint/naming-convention */ @@ -344,7 +344,7 @@ describe('mediaConfig', () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_MEDIA_BACKEND: BackendType.WEBDAV, + HD_MEDIA_BACKEND: MediaBackendType.WEBDAV, HD_MEDIA_BACKEND_WEBDAV_CONNECTION_STRING: 'not-an-url', HD_MEDIA_BACKEND_WEBDAV_UPLOAD_DIR: uploadDir, HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL: publicUrl, @@ -363,7 +363,7 @@ describe('mediaConfig', () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_MEDIA_BACKEND: BackendType.WEBDAV, + HD_MEDIA_BACKEND: MediaBackendType.WEBDAV, HD_MEDIA_BACKEND_WEBDAV_CONNECTION_STRING: webdavConnectionString, HD_MEDIA_BACKEND_WEBDAV_UPLOAD_DIR: uploadDir, /* eslint-enable @typescript-eslint/naming-convention */ @@ -381,7 +381,7 @@ describe('mediaConfig', () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_MEDIA_BACKEND: BackendType.WEBDAV, + HD_MEDIA_BACKEND: MediaBackendType.WEBDAV, HD_MEDIA_BACKEND_WEBDAV_CONNECTION_STRING: webdavConnectionString, HD_MEDIA_BACKEND_WEBDAV_UPLOAD_DIR: uploadDir, HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL: 'not-an-url', diff --git a/backend/src/config/media.config.ts b/backend/src/config/media.config.ts index 16c862345..5dc7a3ec4 100644 --- a/backend/src/config/media.config.ts +++ b/backend/src/config/media.config.ts @@ -3,10 +3,10 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { MediaBackendType } from '@hedgedoc/commons'; import { registerAs } from '@nestjs/config'; import z from 'zod'; -import { BackendType } from '../media/backends/backend-type.enum'; import { parseOptionalBoolean } from './utils'; import { buildErrorMessage, @@ -14,7 +14,7 @@ import { } from './zod-error-message'; const azureSchema = z.object({ - use: z.literal(BackendType.AZURE), + use: z.literal(MediaBackendType.AZURE), azure: z.object({ connectionString: z .string() @@ -24,21 +24,21 @@ const azureSchema = z.object({ }); const filesystemSchema = z.object({ - use: z.literal(BackendType.FILESYSTEM), + use: z.literal(MediaBackendType.FILESYSTEM), filesystem: z.object({ uploadPath: z.string().describe('HD_MEDIA_BACKEND_FILESYSTEM_UPLOAD_PATH'), }), }); const imgurSchema = z.object({ - use: z.literal(BackendType.IMGUR), + use: z.literal(MediaBackendType.IMGUR), imgur: z.object({ clientId: z.string().describe('HD_MEDIA_BACKEND_IMGUR_CLIENT_ID'), }), }); const s3Schema = z.object({ - use: z.literal(BackendType.S3), + use: z.literal(MediaBackendType.S3), s3: z.object({ accessKeyId: z.string().describe('HD_MEDIA_BACKEND_S3_ACCESS_KEY'), secretAccessKey: z.string().describe('HD_MEDIA_BACKEND_S3_SECRET_KEY'), @@ -53,7 +53,7 @@ const s3Schema = z.object({ }); const webdavSchema = z.object({ - use: z.literal(BackendType.WEBDAV), + use: z.literal(MediaBackendType.WEBDAV), webdav: z.object({ connectionString: z .string() diff --git a/backend/src/config/mock/media.config.mock.ts b/backend/src/config/mock/media.config.mock.ts index 3a36bf800..636ccb1ee 100644 --- a/backend/src/config/mock/media.config.mock.ts +++ b/backend/src/config/mock/media.config.mock.ts @@ -3,16 +3,16 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { MediaBackendType } from '@hedgedoc/commons'; import { ConfigFactoryKeyHost, registerAs } from '@nestjs/config'; import { ConfigFactory } from '@nestjs/config/dist/interfaces'; -import { BackendType } from '../../media/backends/backend-type.enum'; import { MediaConfig } from '../media.config'; export function createDefaultMockMediaConfig(): MediaConfig { return { backend: { - use: BackendType.FILESYSTEM, + use: MediaBackendType.FILESYSTEM, filesystem: { uploadPath: 'test_uploads' + Math.floor(Math.random() * 100000).toString(), diff --git a/backend/src/database/migrations/20250312211152_initial.ts b/backend/src/database/migrations/20250312211152_initial.js similarity index 91% rename from backend/src/database/migrations/20250312211152_initial.ts rename to backend/src/database/migrations/20250312211152_initial.js index 873a01fb6..431954277 100644 --- a/backend/src/database/migrations/20250312211152_initial.ts +++ b/backend/src/database/migrations/20250312211152_initial.js @@ -1,13 +1,6 @@ -/* - * SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { AuthProviderType, NoteType, SpecialGroup } from '@hedgedoc/commons'; -import type { Knex } from 'knex'; - -import { BackendType } from '../../media/backends/backend-type.enum'; -import { +/* eslint-disable */ +const { + AuthProviderType, FieldNameAlias, FieldNameApiToken, FieldNameAuthorshipInfo, @@ -22,6 +15,9 @@ import { FieldNameRevisionTag, FieldNameUser, FieldNameUserPinnedNote, + MediaBackendType, + NoteType, + SpecialGroup, TableAlias, TableApiToken, TableAuthorshipInfo, @@ -36,9 +32,9 @@ import { TableRevisionTag, TableUser, TableUserPinnedNote, -} from '../types'; +} = require('@hedgedoc/database'); -export async function up(knex: Knex): Promise { +const up = async function (knex) { // Create the user table first as it's referenced by other tables await knex.schema.createTable(TableUser, (table) => { table.increments(FieldNameUser.id).primary(); @@ -212,8 +208,8 @@ export async function up(knex: Knex): Promise { .unsigned() .notNullable() .references(FieldNameRevision.uuid) - .onDelete('CASCADE') - .inTable(TableRevision); + .inTable(TableRevision) + .onDelete('CASCADE'); table.string(FieldNameRevisionTag.tag).notNullable(); table.primary([ FieldNameRevisionTag.revisionUuid, @@ -228,15 +224,15 @@ export async function up(knex: Knex): Promise { .unsigned() .notNullable() .references(FieldNameRevision.uuid) - .onDelete('CASCADE') - .inTable(TableRevision); + .inTable(TableRevision) + .onDelete('CASCADE'); table .integer(FieldNameAuthorshipInfo.authorId) .unsigned() .notNullable() .references(FieldNameUser.id) - .onDelete('CASCADE') - .inTable(TableUser); + .inTable(TableUser) + .onDelete('CASCADE'); table .integer(FieldNameAuthorshipInfo.startPosition) .unsigned() @@ -254,15 +250,15 @@ export async function up(knex: Knex): Promise { .unsigned() .notNullable() .references(FieldNameNote.id) - .onDelete('CASCADE') - .inTable(TableNote); + .inTable(TableNote) + .onDelete('CASCADE'); table .integer(FieldNameNoteUserPermission.userId) .unsigned() .notNullable() .references(FieldNameUser.id) - .onDelete('CASCADE') - .inTable(TableUser); + .inTable(TableUser) + .onDelete('CASCADE'); table .boolean(FieldNameNoteUserPermission.canEdit) .notNullable() @@ -280,15 +276,15 @@ export async function up(knex: Knex): Promise { .unsigned() .notNullable() .references(FieldNameNote.id) - .onDelete('CASCADE') - .inTable(TableNote); + .inTable(TableNote) + .onDelete('CASCADE'); table .integer(FieldNameNoteGroupPermission.groupId) .unsigned() .notNullable() .references(FieldNameGroup.id) - .onDelete('CASCADE') - .inTable(TableGroup); + .inTable(TableGroup) + .onDelete('CASCADE'); table .boolean(FieldNameNoteGroupPermission.canEdit) .notNullable() @@ -319,11 +315,11 @@ export async function up(knex: Knex): Promise { .enu( FieldNameMediaUpload.backendType, [ - BackendType.AZURE, - BackendType.FILESYSTEM, - BackendType.IMGUR, - BackendType.S3, - BackendType.WEBDAV, + MediaBackendType.AZURE, + MediaBackendType.FILESYSTEM, + MediaBackendType.IMGUR, + MediaBackendType.S3, + MediaBackendType.WEBDAV, ], { useNative: true, @@ -356,9 +352,9 @@ export async function up(knex: Knex): Promise { FieldNameUserPinnedNote.noteId, ]); }); -} +}; -export async function down(knex: Knex): Promise { +const down = async function (knex) { // Drop tables in reverse order of creation to avoid integer key constraints await knex.schema.dropTableIfExists(TableUserPinnedNote); await knex.schema.dropTableIfExists(TableMediaUpload); @@ -374,4 +370,9 @@ export async function down(knex: Knex): Promise { await knex.schema.dropTableIfExists(TableNote); await knex.schema.dropTableIfExists(TableGroup); await knex.schema.dropTableIfExists(TableUser); -} +}; + +module.exports = { + up, + down, +}; diff --git a/backend/src/database/seeds/01_user.ts b/backend/src/database/seeds/01_user.ts index bdbb81b8c..8e8b0b6ac 100644 --- a/backend/src/database/seeds/01_user.ts +++ b/backend/src/database/seeds/01_user.ts @@ -4,15 +4,15 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { AuthProviderType } from '@hedgedoc/commons'; -import { Knex } from 'knex'; - -import { hashPassword } from '../../utils/password'; import { FieldNameIdentity, FieldNameUser, TableIdentity, TableUser, -} from '../types'; +} from '@hedgedoc/database'; +import { Knex } from 'knex'; + +import { hashPassword } from '../../utils/password'; export async function seed(knex: Knex): Promise { // Clear tables beforehand diff --git a/backend/src/database/seeds/02_api_token.ts b/backend/src/database/seeds/02_api_token.ts index 535b8492b..6a6bdea83 100644 --- a/backend/src/database/seeds/02_api_token.ts +++ b/backend/src/database/seeds/02_api_token.ts @@ -3,11 +3,10 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { FieldNameApiToken, TableApiToken } from '@hedgedoc/database'; import { createHash } from 'crypto'; import { Knex } from 'knex'; -import { FieldNameApiToken, TableApiToken } from '../types'; - export async function seed(knex: Knex): Promise { // Clear table beforehand await knex(TableApiToken).del(); diff --git a/backend/src/database/seeds/03_note.ts b/backend/src/database/seeds/03_note.ts index 8e4a4e6b7..cb8b16c42 100644 --- a/backend/src/database/seeds/03_note.ts +++ b/backend/src/database/seeds/03_note.ts @@ -4,11 +4,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { NoteType } from '@hedgedoc/commons'; -import { createPatch } from 'diff'; -import { readFileSync } from 'fs'; -import { Knex } from 'knex'; - -import { extractRevisionMetadataFromContent } from '../../revisions/utils/extract-revision-metadata-from-content'; import { FieldNameAlias, FieldNameAuthorshipInfo, @@ -24,7 +19,12 @@ import { TableNoteUserPermission, TableRevision, TableRevisionTag, -} from '../types'; +} from '@hedgedoc/database'; +import { createPatch } from 'diff'; +import { readFileSync } from 'fs'; +import { Knex } from 'knex'; + +import { extractRevisionMetadataFromContent } from '../../revisions/utils/extract-revision-metadata-from-content'; export async function seed(knex: Knex): Promise { // Clear tables beforehand @@ -160,18 +160,21 @@ export async function seed(knex: Knex): Promise { [FieldNameAuthorshipInfo.authorId]: 1, [FieldNameAuthorshipInfo.startPosition]: 0, [FieldNameAuthorshipInfo.endPosition]: guestNoteContent.length, + [FieldNameAuthorshipInfo.createdAt]: new Date(), }, { [FieldNameAuthorshipInfo.revisionUuid]: userNoteRevisionUuid, [FieldNameAuthorshipInfo.authorId]: 2, [FieldNameAuthorshipInfo.startPosition]: 0, [FieldNameAuthorshipInfo.endPosition]: userNoteContent.length, + [FieldNameAuthorshipInfo.createdAt]: new Date(), }, { [FieldNameAuthorshipInfo.revisionUuid]: userSlideRevisionUuid, [FieldNameAuthorshipInfo.authorId]: 2, [FieldNameAuthorshipInfo.startPosition]: 0, [FieldNameAuthorshipInfo.endPosition]: userSlideContent.length, + [FieldNameAuthorshipInfo.createdAt]: new Date(), }, ]); await knex(TableNoteGroupPermission).insert([ diff --git a/backend/src/database/types/index.ts b/backend/src/database/types/index.ts deleted file mode 100644 index 17f46106e..000000000 --- a/backend/src/database/types/index.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export { Alias, FieldNameAlias, TableAlias } from './alias'; -export { ApiToken, FieldNameApiToken, TableApiToken } from './api-token'; -export { - AuthorshipInfo, - FieldNameAuthorshipInfo, - TableAuthorshipInfo, -} from './authorship-info'; -export { Group, FieldNameGroup, TableGroup } from './group'; -export { GroupUser, FieldNameGroupUser, TableGroupUser } from './group-user'; -export { Identity, FieldNameIdentity, TableIdentity } from './identity'; -export { - MediaUpload, - FieldNameMediaUpload, - TableMediaUpload, -} from './media-upload'; -export { Note, FieldNameNote, TableNote } from './note'; -export { - NoteGroupPermission, - FieldNameNoteGroupPermission, - TableNoteGroupPermission, -} from './note-group-permission'; -export { - NoteUserPermission, - FieldNameNoteUserPermission, - TableNoteUserPermission, -} from './note-user-permission'; -export { Revision, FieldNameRevision, TableRevision } from './revision'; -export { - RevisionTag, - FieldNameRevisionTag, - TableRevisionTag, -} from './revision-tag'; -export { User, FieldNameUser, TableUser } from './user'; -export { - UserPinnedNote, - FieldNameUserPinnedNote, - TableUserPinnedNote, -} from './user-pinned-note'; diff --git a/backend/src/database/types/knex.types.ts b/backend/src/database/types/knex.types.ts index 6a73e9ea4..317635a10 100644 --- a/backend/src/database/types/knex.types.ts +++ b/backend/src/database/types/knex.types.ts @@ -3,15 +3,18 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { Knex } from 'knex'; - -import { Alias, TypeInsertAlias, TypeUpdateAlias } from './alias'; -import { ApiToken, TypeInsertApiToken, TypeUpdateApiToken } from './api-token'; -import { Group, TypeInsertGroup, TypeUpdateGroup } from './group'; -import { Identity, TypeInsertIdentity, TypeUpdateIdentity } from './identity'; import { + Alias, + ApiToken, AuthorshipInfo, + Group, GroupUser, + Identity, + MediaUpload, + Note, + NoteGroupPermission, + NoteUserPermission, + Revision, RevisionTag, TableAlias, TableApiToken, @@ -27,24 +30,28 @@ import { TableRevisionTag, TableUser, TableUserPinnedNote, - UserPinnedNote, -} from './index'; -import { - MediaUpload, + TypeInsertAlias, + TypeInsertApiToken, + TypeInsertAuthorshipInfo, + TypeInsertGroup, + TypeInsertIdentity, TypeInsertMediaUpload, + TypeInsertNote, + TypeInsertRevision, + TypeInsertUser, + TypeUpdateAlias, + TypeUpdateApiToken, + TypeUpdateGroup, + TypeUpdateIdentity, TypeUpdateMediaUpload, -} from './media-upload'; -import { Note, TypeInsertNote, TypeUpdateNote } from './note'; -import { - NoteGroupPermission, + TypeUpdateNote, TypeUpdateNoteGroupPermission, -} from './note-group-permission'; -import { - NoteUserPermission, TypeUpdateNoteUserPermission, -} from './note-user-permission'; -import { Revision, TypeInsertRevision } from './revision'; -import { TypeInsertUser, TypeUpdateUser, User } from './user'; + TypeUpdateUser, + User, + UserPinnedNote, +} from '@hedgedoc/database'; +import { Knex } from 'knex'; /* eslint-disable @typescript-eslint/naming-convention */ declare module 'knex/types/tables.js' { @@ -59,7 +66,10 @@ declare module 'knex/types/tables.js' { TypeInsertApiToken, TypeUpdateApiToken >; - [TableAuthorshipInfo]: AuthorshipInfo; + [TableAuthorshipInfo]: Knex.CompositeTableType< + AuthorshipInfo, + TypeInsertAuthorshipInfo + >; [TableGroup]: Knex.CompositeTableType< Group, TypeInsertGroup, diff --git a/backend/src/frontend-config/frontend-config.service.ts b/backend/src/frontend-config/frontend-config.service.ts index 8517fc403..44dbfc296 100644 --- a/backend/src/frontend-config/frontend-config.service.ts +++ b/backend/src/frontend-config/frontend-config.service.ts @@ -4,12 +4,12 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { + AuthProviderDto, AuthProviderType, BrandingDto, FrontendConfigDto, SpecialUrlDto, } from '@hedgedoc/commons'; -import { AuthProviderDto } from '@hedgedoc/commons'; import { Inject, Injectable } from '@nestjs/common'; import { URL } from 'url'; diff --git a/backend/src/groups/groups.service.ts b/backend/src/groups/groups.service.ts index 96bb27286..826b894dd 100644 --- a/backend/src/groups/groups.service.ts +++ b/backend/src/groups/groups.service.ts @@ -4,12 +4,15 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { GroupInfoDto } from '@hedgedoc/commons'; +import { + FieldNameGroup, + TableGroup, + TypeInsertGroup, +} from '@hedgedoc/database'; import { Injectable } from '@nestjs/common'; import { Knex } from 'knex'; import { InjectConnection } from 'nest-knexjs'; -import { FieldNameGroup, TableGroup } from '../database/types'; -import { TypeInsertGroup } from '../database/types/group'; import { AlreadyInDBError, NotInDBError } from '../errors/errors'; import { ConsoleLoggerService } from '../logger/console-logger.service'; @@ -72,11 +75,13 @@ export class GroupsService { * Fetches a groupId by its identifier name * * @param name Name of the group to query + * @param transaction The optional database transaction to use * @return The groupId * @throws {NotInDBError} if there is no group with this name */ - async getGroupIdByName(name: string): Promise { - const group = await this.knex(TableGroup) + async getGroupIdByName(name: string, transaction?: Knex): Promise { + const dbActor = transaction ?? this.knex; + const group = await dbActor(TableGroup) .select(FieldNameGroup.id) .where(FieldNameGroup.name, name) .first(); diff --git a/backend/src/media/backends/azure-backend.ts b/backend/src/media/backends/azure-backend.ts index b007f1a47..6400f1d33 100644 --- a/backend/src/media/backends/azure-backend.ts +++ b/backend/src/media/backends/azure-backend.ts @@ -11,6 +11,7 @@ import { generateBlobSASQueryParameters, StorageSharedKeyCredential, } from '@azure/storage-blob'; +import { MediaBackendType } from '@hedgedoc/commons'; import { Inject, Injectable } from '@nestjs/common'; import { FileTypeResult } from 'file-type'; @@ -21,7 +22,6 @@ import mediaConfiguration, { import { MediaBackendError } from '../../errors/errors'; import { ConsoleLoggerService } from '../../logger/console-logger.service'; import { MediaBackend } from '../media-backend.interface'; -import { BackendType } from './backend-type.enum'; @Injectable() export class AzureBackend implements MediaBackend { @@ -36,7 +36,7 @@ export class AzureBackend implements MediaBackend { ) { this.logger.setContext(AzureBackend.name); this.config = (this.mediaConfig.backend as AzureMediaConfig).azure; - if (this.mediaConfig.backend.use === BackendType.AZURE) { + if (this.mediaConfig.backend.use === MediaBackendType.AZURE) { // only create the client if the backend is configured to azure const blobServiceClient = BlobServiceClient.fromConnectionString( this.config.connectionString, diff --git a/backend/src/media/backends/backend-type.enum.ts b/backend/src/media/backends/backend-type.enum.ts index c35d19596..c07d50130 100644 --- a/backend/src/media/backends/backend-type.enum.ts +++ b/backend/src/media/backends/backend-type.enum.ts @@ -3,11 +3,3 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ - -export enum BackendType { - FILESYSTEM = 'filesystem', - S3 = 's3', - IMGUR = 'imgur', - AZURE = 'azure', - WEBDAV = 'webdav', -} diff --git a/backend/src/media/backends/s3-backend.spec.ts b/backend/src/media/backends/s3-backend.spec.ts index 948cb0db5..9be57bfa0 100644 --- a/backend/src/media/backends/s3-backend.spec.ts +++ b/backend/src/media/backends/s3-backend.spec.ts @@ -3,13 +3,13 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { MediaBackendType } from '@hedgedoc/commons'; import * as MinioModule from 'minio'; import { Client, ClientOptions } from 'minio'; import { Mock } from 'ts-mockery'; import { MediaConfig } from '../../config/media.config'; import { ConsoleLoggerService } from '../../logger/console-logger.service'; -import { BackendType } from './backend-type.enum'; import { S3Backend } from './s3-backend'; jest.mock('minio'); @@ -43,7 +43,7 @@ describe('s3 backend', () => { function mockMediaConfig(endPoint: string): MediaConfig { return Mock.of({ backend: { - use: BackendType.S3, + use: MediaBackendType.S3, s3: { accessKeyId: mockedS3AccessKeyId, secretAccessKey: mockedS3SecretAccessKey, diff --git a/backend/src/media/backends/s3-backend.ts b/backend/src/media/backends/s3-backend.ts index 7d28b1e26..8cd43130c 100644 --- a/backend/src/media/backends/s3-backend.ts +++ b/backend/src/media/backends/s3-backend.ts @@ -3,6 +3,7 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { MediaBackendType } from '@hedgedoc/commons'; import { Inject, Injectable } from '@nestjs/common'; import { FileTypeResult } from 'file-type'; import { Client } from 'minio'; @@ -15,7 +16,6 @@ import mediaConfiguration, { import { MediaBackendError } from '../../errors/errors'; import { ConsoleLoggerService } from '../../logger/console-logger.service'; import { MediaBackend } from '../media-backend.interface'; -import { BackendType } from './backend-type.enum'; @Injectable() export class S3Backend implements MediaBackend { @@ -33,7 +33,7 @@ export class S3Backend implements MediaBackend { private mediaConfig: MediaConfig, ) { this.logger.setContext(S3Backend.name); - if (this.mediaConfig.backend.use !== BackendType.S3) { + if (this.mediaConfig.backend.use !== MediaBackendType.S3) { return; } this.config = this.mediaConfig.backend.s3; diff --git a/backend/src/media/backends/webdav-backend.ts b/backend/src/media/backends/webdav-backend.ts index a98d5261f..92c497c2f 100644 --- a/backend/src/media/backends/webdav-backend.ts +++ b/backend/src/media/backends/webdav-backend.ts @@ -3,6 +3,7 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { MediaBackendType } from '@hedgedoc/commons'; import { Inject, Injectable } from '@nestjs/common'; import { FileTypeResult } from 'file-type'; import fetch, { Response } from 'node-fetch'; @@ -15,7 +16,6 @@ import mediaConfiguration, { import { MediaBackendError } from '../../errors/errors'; import { ConsoleLoggerService } from '../../logger/console-logger.service'; import { MediaBackend } from '../media-backend.interface'; -import { BackendType } from './backend-type.enum'; @Injectable() export class WebdavBackend implements MediaBackend { @@ -29,7 +29,7 @@ export class WebdavBackend implements MediaBackend { private mediaConfig: MediaConfig, ) { this.logger.setContext(WebdavBackend.name); - if (this.mediaConfig.backend.use === BackendType.WEBDAV) { + if (this.mediaConfig.backend.use === MediaBackendType.WEBDAV) { this.config = this.mediaConfig.backend.webdav; const url = new URL(this.config.connectionString); this.baseUrl = url.toString(); diff --git a/backend/src/media/media.service.ts b/backend/src/media/media.service.ts index a96457ea3..b442cd2ef 100644 --- a/backend/src/media/media.service.ts +++ b/backend/src/media/media.service.ts @@ -3,15 +3,7 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { MediaUploadDto } from '@hedgedoc/commons'; -import { Inject, Injectable } from '@nestjs/common'; -import { ModuleRef } from '@nestjs/core'; -import * as FileType from 'file-type'; -import { Knex } from 'knex'; -import { InjectConnection } from 'nest-knexjs'; -import { v7 as uuidV7 } from 'uuid'; - -import mediaConfiguration, { MediaConfig } from '../config/media.config'; +import { MediaBackendType, MediaUploadDto } from '@hedgedoc/commons'; import { Alias, FieldNameAlias, @@ -24,12 +16,18 @@ import { TableMediaUpload, TableUser, User, -} from '../database/types'; +} from '@hedgedoc/database'; +import { Inject, Injectable } from '@nestjs/common'; +import { ModuleRef } from '@nestjs/core'; +import * as FileType from 'file-type'; +import { Knex } from 'knex'; +import { InjectConnection } from 'nest-knexjs'; +import { v7 as uuidV7 } from 'uuid'; + +import mediaConfiguration, { MediaConfig } from '../config/media.config'; import { ClientError, NotInDBError } from '../errors/errors'; import { ConsoleLoggerService } from '../logger/console-logger.service'; -import { NoteService } from '../notes/note.service'; import { AzureBackend } from './backends/azure-backend'; -import { BackendType } from './backends/backend-type.enum'; import { FilesystemBackend } from './backends/filesystem-backend'; import { ImgurBackend } from './backends/imgur-backend'; import { S3Backend } from './backends/s3-backend'; @@ -39,7 +37,7 @@ import { MediaBackend } from './media-backend.interface'; @Injectable() export class MediaService { mediaBackend: MediaBackend; - mediaBackendType: BackendType; + mediaBackendType: MediaBackendType; constructor( private readonly logger: ConsoleLoggerService, @@ -246,18 +244,18 @@ export class MediaService { .where(FieldNameMediaUpload.uuid, uuid); } - private chooseBackendType(): BackendType { + private chooseBackendType(): MediaBackendType { switch (this.mediaConfig.backend.use as string) { case 'filesystem': - return BackendType.FILESYSTEM; + return MediaBackendType.FILESYSTEM; case 'azure': - return BackendType.AZURE; + return MediaBackendType.AZURE; case 'imgur': - return BackendType.IMGUR; + return MediaBackendType.IMGUR; case 's3': - return BackendType.S3; + return MediaBackendType.S3; case 'webdav': - return BackendType.WEBDAV; + return MediaBackendType.WEBDAV; default: throw new Error( `Unexpected media backend ${this.mediaConfig.backend.use}`, @@ -265,17 +263,17 @@ export class MediaService { } } - private getBackendFromType(type: BackendType): MediaBackend { + private getBackendFromType(type: MediaBackendType): MediaBackend { switch (type) { - case BackendType.FILESYSTEM: + case MediaBackendType.FILESYSTEM: return this.moduleRef.get(FilesystemBackend); - case BackendType.S3: + case MediaBackendType.S3: return this.moduleRef.get(S3Backend); - case BackendType.AZURE: + case MediaBackendType.AZURE: return this.moduleRef.get(AzureBackend); - case BackendType.IMGUR: + case MediaBackendType.IMGUR: return this.moduleRef.get(ImgurBackend); - case BackendType.WEBDAV: + case MediaBackendType.WEBDAV: return this.moduleRef.get(WebdavBackend); } } @@ -309,7 +307,9 @@ export class MediaService { uuid: mediaUpload[FieldNameMediaUpload.uuid], fileName: mediaUpload[FieldNameMediaUpload.fileName], noteId: mediaUpload[FieldNameAlias.alias], - createdAt: mediaUpload[FieldNameMediaUpload.createdAt].toISOString(), + createdAt: new Date( + mediaUpload[FieldNameMediaUpload.createdAt], + ).toISOString(), username: mediaUpload[FieldNameUser.username], })); } diff --git a/backend/src/notes/note.service.ts b/backend/src/notes/note.service.ts index 77e3af1e6..0a8f7bf5e 100644 --- a/backend/src/notes/note.service.ts +++ b/backend/src/notes/note.service.ts @@ -9,14 +9,6 @@ import { NotePermissionsDto, SpecialGroup, } from '@hedgedoc/commons'; -import { forwardRef, Inject, Injectable } from '@nestjs/common'; -import { EventEmitter2 } from '@nestjs/event-emitter'; -import { Knex } from 'knex'; -import { InjectConnection } from 'nest-knexjs'; - -import { AliasService } from '../alias/alias.service'; -import { DefaultAccessLevel } from '../config/default-access-level.enum'; -import noteConfiguration, { NoteConfig } from '../config/note.config'; import { FieldNameAlias, FieldNameGroup, @@ -36,7 +28,15 @@ import { TableNoteUserPermission, TableUser, User, -} from '../database/types'; +} from '@hedgedoc/database'; +import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { Knex } from 'knex'; +import { InjectConnection } from 'nest-knexjs'; + +import { AliasService } from '../alias/alias.service'; +import { DefaultAccessLevel } from '../config/default-access-level.enum'; +import noteConfiguration, { NoteConfig } from '../config/note.config'; import { ForbiddenIdError, GenericDBError, @@ -141,6 +141,7 @@ export class NoteService { await this.revisionsService.createRevision( noteId, noteContent, + true, transaction, ); @@ -162,6 +163,7 @@ export class NoteService { if (everyoneAccessLevel !== DefaultAccessLevel.NONE) { const everyoneAccessGroupId = await this.groupsService.getGroupIdByName( SpecialGroup.EVERYONE, + transaction, ); await this.permissionService.setGroupPermission( noteId, @@ -173,7 +175,10 @@ export class NoteService { if (loggedInUsersAccessLevel !== DefaultAccessLevel.NONE) { const loggedInUsersAccessGroupId = - await this.groupsService.getGroupIdByName(SpecialGroup.LOGGED_IN); + await this.groupsService.getGroupIdByName( + SpecialGroup.LOGGED_IN, + transaction, + ); await this.permissionService.setGroupPermission( noteId, loggedInUsersAccessGroupId, @@ -415,6 +420,12 @@ export class NoteService { 'toNoteMetadataDto', ); } + const createdAtString = note[FieldNameNote.createdAt]; + const version = note[FieldNameNote.version]; + this.logger.debug(`createdAt: ${createdAtString}`); + this.logger.debug(`createversion: ${version}`); + const createdAt = new Date(createdAtString).toISOString(); + const latestRevision = await this.revisionsService.getLatestRevision( noteId, transaction, @@ -423,28 +434,40 @@ export class NoteService { latestRevision[FieldNameRevision.uuid], transaction, ); + const permissions = await this.toNotePermissionsDto(noteId, transaction); + const updateUsers = await this.revisionsService.getRevisionUserInfo( latestRevision[FieldNameRevision.uuid], + transaction, ); - updateUsers.users.sort( - (userA, userB) => userB.createdAt.getTime() - userA.createdAt.getTime(), - ); - const lastEdit = updateUsers.users[0]; - const editedBy = updateUsers.users.map((user) => user.username); - const permissions = await this.toNotePermissionsDto(noteId, transaction); + updateUsers.users.sort(); + + let lastUpdatedBy; + let editedBy; + let updatedAt; + if (updateUsers.users.length > 0) { + const lastEdit = updateUsers.users[0]; + lastUpdatedBy = lastEdit.username; + editedBy = updateUsers.users.map((user) => user.username); + updatedAt = new Date(lastEdit.createdAt).toISOString(); + } else { + lastUpdatedBy = permissions.owner; + editedBy = permissions.owner ? [permissions.owner] : []; + updatedAt = createdAt; + } return { aliases: aliases.map((alias) => alias[FieldNameAlias.alias]), primaryAlias: primaryAlias[FieldNameAlias.alias], title: latestRevision.title, description: latestRevision.description, - tags: tags, - createdAt: note[FieldNameNote.createdAt].toISOString(), - editedBy: editedBy, - permissions: permissions, - version: note[FieldNameNote.version], - updatedAt: lastEdit.createdAt.toISOString(), - lastUpdatedBy: lastEdit.username, + tags, + createdAt, + editedBy, + permissions, + version, + updatedAt, + lastUpdatedBy, }; } diff --git a/backend/src/permissions/permission.service.ts b/backend/src/permissions/permission.service.ts index 6e90615c0..202bd368e 100644 --- a/backend/src/permissions/permission.service.ts +++ b/backend/src/permissions/permission.service.ts @@ -8,12 +8,6 @@ import { PermissionLevel, SpecialGroup, } from '@hedgedoc/commons'; -import { Inject, Injectable } from '@nestjs/common'; -import { EventEmitter2 } from '@nestjs/event-emitter'; -import { Knex } from 'knex'; -import { InjectConnection } from 'nest-knexjs'; - -import noteConfiguration, { NoteConfig } from '../config/note.config'; import { FieldNameGroup, FieldNameGroupUser, @@ -29,7 +23,13 @@ import { TableNoteGroupPermission, TableNoteUserPermission, TableUser, -} from '../database/types'; +} from '@hedgedoc/database'; +import { Inject, Injectable } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { Knex } from 'knex'; +import { InjectConnection } from 'nest-knexjs'; + +import noteConfiguration, { NoteConfig } from '../config/note.config'; import { GenericDBError, NotInDBError } from '../errors/errors'; import { NoteEvent, NoteEventMap } from '../events'; import { ConsoleLoggerService } from '../logger/console-logger.service'; @@ -334,6 +334,7 @@ export class PermissionService { * @param noteId - the if of the note * @param groupId - the name of the group for which the permission should be set * @param canEdit - specifies if the group can edit the note + * @param transaction The optional transaction for the database */ async setGroupPermission( noteId: number, diff --git a/backend/src/permissions/permissions.guard.ts b/backend/src/permissions/permissions.guard.ts index 021e9fc33..7676b2897 100644 --- a/backend/src/permissions/permissions.guard.ts +++ b/backend/src/permissions/permissions.guard.ts @@ -3,6 +3,7 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { FieldNameUser } from '@hedgedoc/database'; import { CanActivate, ExecutionContext, @@ -14,7 +15,6 @@ import { Reflector } from '@nestjs/core'; import { extractNoteIdFromRequest } from '../api/utils/extract-note-id-from-request'; import { CompleteRequest } from '../api/utils/request.type'; -import { FieldNameUser } from '../database/types'; import { ConsoleLoggerService } from '../logger/console-logger.service'; import { NoteService } from '../notes/note.service'; import { UsersService } from '../users/users.service'; diff --git a/backend/src/realtime/realtime-note/realtime-connection.spec.ts b/backend/src/realtime/realtime-note/realtime-connection.spec.ts index b666707fa..7f8b16146 100644 --- a/backend/src/realtime/realtime-note/realtime-connection.spec.ts +++ b/backend/src/realtime/realtime-note/realtime-connection.spec.ts @@ -9,9 +9,9 @@ import { YDocSyncServerAdapter, } from '@hedgedoc/commons'; import * as HedgeDocCommonsModule from '@hedgedoc/commons'; +import { FieldNameUser, User } from '@hedgedoc/database'; import { Mock } from 'ts-mockery'; -import { FieldNameUser, User } from '../../database/types'; import * as NameRandomizerModule from './random-word-lists/name-randomizer'; import { RealtimeConnection } from './realtime-connection'; import { RealtimeNote } from './realtime-note'; diff --git a/backend/src/realtime/realtime-note/realtime-note.service.ts b/backend/src/realtime/realtime-note/realtime-note.service.ts index 521a42374..e3c064e31 100644 --- a/backend/src/realtime/realtime-note/realtime-note.service.ts +++ b/backend/src/realtime/realtime-note/realtime-note.service.ts @@ -3,13 +3,13 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { FieldNameRevision } from '@hedgedoc/database'; import { Optional } from '@mrdrogdrog/optional'; import { BeforeApplicationShutdown, Inject, Injectable } from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; import { SchedulerRegistry } from '@nestjs/schedule'; import appConfiguration, { AppConfig } from '../../config/app.config'; -import { FieldNameRevision } from '../../database/types'; import { NoteEvent } from '../../events'; import { ConsoleLoggerService } from '../../logger/console-logger.service'; import { NotePermissionLevel } from '../../permissions/note-permission.enum'; @@ -47,6 +47,7 @@ export class RealtimeNoteService implements BeforeApplicationShutdown { .createRevision( realtimeNote.getNoteId(), realtimeNote.getRealtimeDoc().getCurrentContent(), + false, undefined, realtimeNote.getRealtimeDoc().encodeStateAsUpdate(), ) diff --git a/backend/src/realtime/realtime-note/test-utils/mock-connection.ts b/backend/src/realtime/realtime-note/test-utils/mock-connection.ts index b18011542..5a6ac797e 100644 --- a/backend/src/realtime/realtime-note/test-utils/mock-connection.ts +++ b/backend/src/realtime/realtime-note/test-utils/mock-connection.ts @@ -7,9 +7,9 @@ import { MockedBackendTransportAdapter, YDocSyncServerAdapter, } from '@hedgedoc/commons'; +import { FieldNameUser, User } from '@hedgedoc/database'; import { Mock } from 'ts-mockery'; -import { FieldNameUser, User } from '../../../database/types'; import { RealtimeConnection } from '../realtime-connection'; import { RealtimeNote } from '../realtime-note'; import { RealtimeUserStatusAdapter } from '../realtime-user-status-adapter'; diff --git a/backend/src/realtime/websocket/websocket.gateway.ts b/backend/src/realtime/websocket/websocket.gateway.ts index cfe6ea9ff..97a8cdff3 100644 --- a/backend/src/realtime/websocket/websocket.gateway.ts +++ b/backend/src/realtime/websocket/websocket.gateway.ts @@ -4,11 +4,11 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { DisconnectReason, MessageTransporter } from '@hedgedoc/commons'; +import { FieldNameUser } from '@hedgedoc/database'; import { OnGatewayConnection, WebSocketGateway } from '@nestjs/websockets'; import { IncomingMessage } from 'http'; import WebSocket from 'ws'; -import { FieldNameUser } from '../../database/types'; import { ConsoleLoggerService } from '../../logger/console-logger.service'; import { NoteService } from '../../notes/note.service'; import { NotePermissionLevel } from '../../permissions/note-permission.enum'; diff --git a/backend/src/revisions/revisions.service.ts b/backend/src/revisions/revisions.service.ts index 83e43eaa2..a853ce466 100644 --- a/backend/src/revisions/revisions.service.ts +++ b/backend/src/revisions/revisions.service.ts @@ -4,15 +4,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { RevisionDto, RevisionMetadataDto } from '@hedgedoc/commons'; -import { Inject, Injectable } from '@nestjs/common'; -import { Cron, Timeout } from '@nestjs/schedule'; -import { createPatch } from 'diff'; -import { Knex } from 'knex'; -import { InjectConnection } from 'nest-knexjs'; -import { v7 as uuidv7 } from 'uuid'; - -import { AliasService } from '../alias/alias.service'; -import noteConfiguration, { NoteConfig } from '../config/note.config'; import { AuthorshipInfo, FieldNameAlias, @@ -30,7 +21,16 @@ import { TableRevisionTag, TableUser, User, -} from '../database/types'; +} from '@hedgedoc/database'; +import { Inject, Injectable } from '@nestjs/common'; +import { Cron, Timeout } from '@nestjs/schedule'; +import { createPatch } from 'diff'; +import { Knex } from 'knex'; +import { InjectConnection } from 'nest-knexjs'; +import { v7 as uuidv7 } from 'uuid'; + +import { AliasService } from '../alias/alias.service'; +import noteConfiguration, { NoteConfig } from '../config/note.config'; import { GenericDBError, NotInDBError } from '../errors/errors'; import { ConsoleLoggerService } from '../logger/console-logger.service'; import { extractRevisionMetadataFromContent } from './utils/extract-revision-metadata-from-content'; @@ -129,7 +129,7 @@ export class RevisionsService { recordMap.set(revision[FieldNameRevision.uuid], { uuid: revision[FieldNameRevision.uuid], length: (revision[FieldNameRevision.content] ?? '').length, - createdAt: revision[FieldNameRevision.createdAt].toISOString(), + createdAt: revision[FieldNameRevision.createdAt], authorUsernames: revision[FieldNameUser.username] !== null ? [revision[FieldNameUser.username]] @@ -217,7 +217,7 @@ export class RevisionsService { uuid: revision[FieldNameRevision.uuid], content: revision[FieldNameRevision.content], length: (revision[FieldNameRevision.content] ?? '').length, - createdAt: revision[FieldNameRevision.createdAt].toISOString(), + createdAt: revision[FieldNameRevision.createdAt], title: revision[FieldNameRevision.title], description: revision[FieldNameRevision.description], patch: revision.patch, @@ -241,7 +241,7 @@ export class RevisionsService { .first(); if (revision === undefined) { throw new NotInDBError( - `No revisions for note ${noteId} found`, + 'No revisions for note found', this.logger.getContext(), 'getLatestRevision', ); @@ -249,8 +249,12 @@ export class RevisionsService { return revision; } - async getRevisionUserInfo(revisionUuid: string): Promise { - const authorUsernamesAndGuestUuids = (await this.knex(TableAuthorshipInfo) + async getRevisionUserInfo( + revisionUuid: string, + transaction?: Knex, + ): Promise { + const dbActor = transaction ?? this.knex; + const authorUsernamesAndGuestUuids = (await dbActor(TableAuthorshipInfo) .join( TableUser, `${TableAuthorshipInfo}.${FieldNameAuthorshipInfo.authorId}`, @@ -293,6 +297,7 @@ export class RevisionsService { * @async * @param noteId The note for which the revision should be created * @param newContent The new note content + * @param firstRevision Whether this is called for the first revision of a note * @param transaction The optional pre-existing database transaction to use * @param yjsStateVector The yjs state vector that describes the new content * @return {Revision} the created revision @@ -301,6 +306,7 @@ export class RevisionsService { async createRevision( noteId: number, newContent: string, + firstRevision: boolean = false, transaction?: Knex, yjsStateVector?: ArrayBuffer, ): Promise { @@ -309,6 +315,7 @@ export class RevisionsService { await this.innerCreateRevision( noteId, newContent, + firstRevision, newTransaction, yjsStateVector, ); @@ -318,6 +325,7 @@ export class RevisionsService { await this.innerCreateRevision( noteId, newContent, + firstRevision, transaction, yjsStateVector, ); @@ -326,13 +334,13 @@ export class RevisionsService { private async innerCreateRevision( noteId: number, newContent: string, + firstRevision: boolean, transaction: Knex, yjsStateVector?: ArrayBuffer, ): Promise { - const latestRevision = - noteId === undefined - ? null - : await this.getLatestRevision(noteId, transaction); + const latestRevision = firstRevision + ? null + : await this.getLatestRevision(noteId, transaction); const oldContent = latestRevision?.content; if (oldContent === newContent) { return undefined; @@ -346,6 +354,7 @@ export class RevisionsService { latestRevision?.content ?? '', newContent, ); + const { title, description, tags, noteType } = extractRevisionMetadataFromContent(newContent); const revisionIds = await transaction(TableRevision).insert( @@ -369,12 +378,14 @@ export class RevisionsService { ); } const revisionId = revisionIds[0][FieldNameRevision.uuid]; - await transaction(TableRevisionTag).insert( - tags.map((tag) => ({ - [FieldNameRevisionTag.tag]: tag, - [FieldNameRevisionTag.revisionUuid]: revisionId, - })), - ); + if (tags.length > 0) { + await transaction(TableRevisionTag).insert( + tags.map((tag) => ({ + [FieldNameRevisionTag.tag]: tag, + [FieldNameRevisionTag.revisionUuid]: revisionId, + })), + ); + } } async getTagsByRevisionUuid( diff --git a/backend/src/sessions/session-state.type.ts b/backend/src/sessions/session-state.type.ts index 46769237f..6ce6381f7 100644 --- a/backend/src/sessions/session-state.type.ts +++ b/backend/src/sessions/session-state.type.ts @@ -4,10 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { AuthProviderType, PendingUserInfoDto } from '@hedgedoc/commons'; +import { FieldNameUser, User } from '@hedgedoc/database'; import { Cookie } from 'express-session'; -import { FieldNameUser, User } from '../database/types'; - export interface SessionState { /** Details about the currently used session cookie */ cookie: Cookie; diff --git a/backend/src/sessions/session.service.ts b/backend/src/sessions/session.service.ts index d4f26a03e..7639ae202 100644 --- a/backend/src/sessions/session.service.ts +++ b/backend/src/sessions/session.service.ts @@ -3,6 +3,7 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { FieldNameUser, User } from '@hedgedoc/database'; import { Optional } from '@mrdrogdrog/optional'; import { Inject, Injectable } from '@nestjs/common'; import { parse as parseCookie } from 'cookie'; @@ -10,7 +11,6 @@ import { unsign } from 'cookie-signature'; import { IncomingMessage } from 'http'; import authConfiguration, { AuthConfig } from '../config/auth.config'; -import { FieldNameUser, User } from '../database/types'; import { ConsoleLoggerService } from '../logger/console-logger.service'; import { HEDGEDOC_SESSION } from '../utils/session'; import { KeyvSessionStore } from './keyv-session-store'; diff --git a/backend/src/users/users.service.ts b/backend/src/users/users.service.ts index 0e6d76b6c..2497cb6e2 100644 --- a/backend/src/users/users.service.ts +++ b/backend/src/users/users.service.ts @@ -5,17 +5,21 @@ */ import { AuthProviderType, + LoginUserInfoDto, REGEX_USERNAME, UserInfoDto, } from '@hedgedoc/commons'; -import { LoginUserInfoDto } from '@hedgedoc/commons'; +import { + FieldNameUser, + TableUser, + TypeUpdateUser, + User, +} from '@hedgedoc/database'; import { BadRequestException, Injectable } from '@nestjs/common'; import { Knex } from 'knex'; import { InjectConnection } from 'nest-knexjs'; import { v4 as uuidv4 } from 'uuid'; -import { FieldNameUser, TableUser, User } from '../database/types'; -import { TypeUpdateUser } from '../database/types/user'; import { GenericDBError, NotInDBError } from '../errors/errors'; import { ConsoleLoggerService } from '../logger/console-logger.service'; import { generateRandomName } from '../realtime/realtime-note/random-word-lists/name-randomizer'; @@ -209,11 +213,12 @@ export class UsersService { transaction?: Knex, ): Promise { const dbActor = transaction ? transaction : this.knex; - const username = await dbActor(TableUser) + const usernameResponse = await dbActor(TableUser) .select(FieldNameUser.username) .where(FieldNameUser.id, userId) .first(); - return username !== null && username !== undefined; + const username = usernameResponse?.[FieldNameUser.username] ?? null; + return username !== null; } /** diff --git a/commons/src/dtos/media/index.ts b/commons/src/dtos/media/index.ts index 5a3c0bc50..77c50bade 100644 --- a/commons/src/dtos/media/index.ts +++ b/commons/src/dtos/media/index.ts @@ -4,3 +4,4 @@ * SPDX-License-Identifier: AGPL-3.0-only */ export * from './media-upload.dto.js' +export * from './media-backend-type.enum.js' diff --git a/commons/src/dtos/media/media-backend-type.enum.ts b/commons/src/dtos/media/media-backend-type.enum.ts new file mode 100644 index 000000000..5903cf209 --- /dev/null +++ b/commons/src/dtos/media/media-backend-type.enum.ts @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export enum MediaBackendType { + FILESYSTEM = 'filesystem', + S3 = 's3', + IMGUR = 'imgur', + AZURE = 'azure', + WEBDAV = 'webdav', +} diff --git a/database/.eslintrc.cjs b/database/.eslintrc.cjs new file mode 100644 index 000000000..90db6a39d --- /dev/null +++ b/database/.eslintrc.cjs @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +module.exports = { + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": [ + "./tsconfig.test.json" + ] + }, + "plugins": [ + "@typescript-eslint", + "jest", + "prettier" + ], + "env": { + "jest": true, + "jest/globals": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", + "plugin:@typescript-eslint/recommended", + "prettier" + ], + "rules": { + "prettier/prettier": ["error", + require('./.prettierrc.json') + ], + "jest/no-disabled-tests": "warn", + "jest/no-focused-tests": "error", + "jest/no-identical-title": "error", + "jest/prefer-to-have-length": "warn", + "jest/valid-expect": "error" + } +} diff --git a/database/.gitignore b/database/.gitignore new file mode 100644 index 000000000..015c626f9 --- /dev/null +++ b/database/.gitignore @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) +# +# SPDX-License-Identifier: CC0-1.0 + +# dependencies +/node_modules +/.pnp +.pnp.* + +# package manager +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# testing +/coverage + +# production +/dist + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/database/.npmignore b/database/.npmignore new file mode 100644 index 000000000..b47b7d1c5 --- /dev/null +++ b/database/.npmignore @@ -0,0 +1,9 @@ +.idea +.babelrc +.eslintrc +.travis.yml +karma.conf.js +tests.webpack.js +webpack.config.*.js +coverage/ +test/ \ No newline at end of file diff --git a/database/.npmignore.license b/database/.npmignore.license new file mode 100644 index 000000000..c223474fb --- /dev/null +++ b/database/.npmignore.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + +SPDX-License-Identifier: CC0-1.0 diff --git a/database/.prettierignore b/database/.prettierignore new file mode 100644 index 000000000..c2658d7d1 --- /dev/null +++ b/database/.prettierignore @@ -0,0 +1 @@ +node_modules/ diff --git a/database/.prettierignore.license b/database/.prettierignore.license new file mode 100644 index 000000000..b29776d49 --- /dev/null +++ b/database/.prettierignore.license @@ -0,0 +1,4 @@ +SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + +SPDX-License-Identifier: CC0-1.0 + diff --git a/database/.prettierrc.json b/database/.prettierrc.json new file mode 100644 index 000000000..c4b3736a8 --- /dev/null +++ b/database/.prettierrc.json @@ -0,0 +1,11 @@ +{ + "parser": "typescript", + "singleQuote": true, + "jsxSingleQuote": true, + "semi": false, + "tabWidth": 2, + "trailingComma": "all", + "bracketSpacing": true, + "bracketSameLine": true, + "arrowParens": "always" +} diff --git a/database/.prettierrc.json.license b/database/.prettierrc.json.license new file mode 100644 index 000000000..c223474fb --- /dev/null +++ b/database/.prettierrc.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + +SPDX-License-Identifier: CC0-1.0 diff --git a/database/build.sh b/database/build.sh new file mode 100755 index 000000000..ad68a4e82 --- /dev/null +++ b/database/build.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# +# SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) +# +# SPDX-License-Identifier: AGPL-3.0-only +# + +set -e + +echo "🦔 > Clear dist directory.." +rm -rf dist + +echo "🦔 > Compile to CJS.." +tsc --project tsconfig.cjs.json + +echo "🦔 > Fix CJS package.json.." +cat > dist/cjs/package.json < Done!" diff --git a/database/jest.config.json b/database/jest.config.json new file mode 100644 index 000000000..af756215c --- /dev/null +++ b/database/jest.config.json @@ -0,0 +1,27 @@ +{ + "testRegex" : "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$", + "testPathIgnorePatterns" : [ + "/dist/" + ], + "moduleFileExtensions" : [ + "ts", + "tsx", + "js" + ], + "extensionsToTreatAsEsm" : [ + ".ts" + ], + "moduleNameMapper" : { + "^(\\.{1,2}/.*)\\.js$" : "$1" + }, + "transformIgnorePatterns": ["/node_modules/"], + "transform" : { + "^.+\\.tsx?$" : [ + "ts-jest", + { + "tsconfig" : "tsconfig.test.json", + "useESM" : true + } + ] + } +} diff --git a/database/jest.config.json.license b/database/jest.config.json.license new file mode 100644 index 000000000..c223474fb --- /dev/null +++ b/database/jest.config.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + +SPDX-License-Identifier: CC0-1.0 diff --git a/database/package.json b/database/package.json new file mode 100644 index 000000000..1c6154de7 --- /dev/null +++ b/database/package.json @@ -0,0 +1,47 @@ +{ + "name": "@hedgedoc/database", + "private": true, + "version": "0.1.0", + "description": "CJS code required for the database migrations", + "author": "The HedgeDoc Authors", + "license": "AGPL-3.0", + "scripts": { + "build": "./build.sh", + "lint": "eslint src --ext .ts", + "lint:fix": "eslint --fix --ext .ts src" + }, + "type": "module", + "source": "src/index.ts", + "main": "dist/cjs/index.js", + "types": "dist/cjs/index.d.ts", + "exports": { + "require": { + "types": "./dist/cjs/index.d.ts", + "default": "./dist/cjs/index.js" + } + }, + "files": [ + "LICENSES/*", + "package.json", + "README.md", + "dist/**" + ], + "browserslist": [ + "node> 12" + ], + "repository": { + "type": "git", + "url": "https://github.com/hedgedoc/hedgedoc.git" + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "8.14.0", + "@typescript-eslint/parser": "8.14.0", + "eslint": "8.57.1", + "eslint-config-prettier": "9.1.0", + "eslint-plugin-jest": "28.9.0", + "eslint-plugin-prettier": "5.2.3", + "prettier": "3.3.3", + "typescript": "5.6.3" + }, + "packageManager": "yarn@4.5.3" +} diff --git a/database/package.json.license b/database/package.json.license new file mode 100644 index 000000000..c223474fb --- /dev/null +++ b/database/package.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + +SPDX-License-Identifier: CC0-1.0 diff --git a/database/src/index.ts b/database/src/index.ts new file mode 100644 index 000000000..83f51452e --- /dev/null +++ b/database/src/index.ts @@ -0,0 +1,6 @@ +/* + * SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +export * from './types/index.js' diff --git a/backend/src/database/types/alias.ts b/database/src/types/alias.ts similarity index 80% rename from backend/src/database/types/alias.ts rename to database/src/types/alias.ts index b364f5c62..0a9b6ff10 100644 --- a/backend/src/database/types/alias.ts +++ b/database/src/types/alias.ts @@ -12,13 +12,13 @@ */ export interface Alias { /** The alias as defined by the user. Is unique. */ - [FieldNameAlias.alias]: string; + [FieldNameAlias.alias]: string /** The id of the associated {@link Note}. */ - [FieldNameAlias.noteId]: number; + [FieldNameAlias.noteId]: number /** Whether the alias is the primary one for the note. */ - [FieldNameAlias.isPrimary]: boolean; + [FieldNameAlias.isPrimary]: boolean } export enum FieldNameAlias { @@ -27,7 +27,7 @@ export enum FieldNameAlias { isPrimary = 'is_primary', } -export const TableAlias = 'alias'; +export const TableAlias = 'alias' -export type TypeInsertAlias = Alias; -export type TypeUpdateAlias = Pick; +export type TypeInsertAlias = Alias +export type TypeUpdateAlias = Pick diff --git a/backend/src/database/types/api-token.ts b/database/src/types/api-token.ts similarity index 56% rename from backend/src/database/types/api-token.ts rename to database/src/types/api-token.ts index a84f69e9e..31cadf29b 100644 --- a/backend/src/database/types/api-token.ts +++ b/database/src/types/api-token.ts @@ -11,25 +11,25 @@ */ export interface ApiToken { /** The id of the token, a short random ASCII string. Is unique */ - [FieldNameApiToken.id]: string; + [FieldNameApiToken.id]: string /** The {@link User} whose permissions the token has */ - [FieldNameApiToken.userId]: number; + [FieldNameApiToken.userId]: number /** The user-defined label for the token, such as "CLI" */ - [FieldNameApiToken.label]: string; + [FieldNameApiToken.label]: string /** Hashed version of the token's secret */ - [FieldNameApiToken.secretHash]: string; + [FieldNameApiToken.secretHash]: string /** Expiry date of the token */ - [FieldNameApiToken.validUntil]: Date; + [FieldNameApiToken.validUntil]: string /** Date when the API token was created */ - [FieldNameApiToken.createdAt]: Date; + [FieldNameApiToken.createdAt]: string /** When the token was last used. When it was never used yet, this field is null */ - [FieldNameApiToken.lastUsedAt]: Date | null; + [FieldNameApiToken.lastUsedAt]: string | null } export enum FieldNameApiToken { @@ -42,7 +42,24 @@ export enum FieldNameApiToken { lastUsedAt = 'last_used_at', } -export const TableApiToken = 'api_token'; +export const TableApiToken = 'api_token' -export type TypeInsertApiToken = Omit; -export type TypeUpdateApiToken = Pick; +type TypeApiTokenDate = Omit< + ApiToken, + | FieldNameApiToken.validUntil + | FieldNameApiToken.createdAt + | FieldNameApiToken.lastUsedAt +> & { + [FieldNameApiToken.validUntil]: Date + [FieldNameApiToken.createdAt]: Date + [FieldNameApiToken.lastUsedAt]: Date | null +} + +export type TypeInsertApiToken = Omit< + TypeApiTokenDate, + FieldNameApiToken.lastUsedAt +> +export type TypeUpdateApiToken = Pick< + TypeApiTokenDate, + FieldNameApiToken.lastUsedAt +> diff --git a/backend/src/database/types/authorship-info.ts b/database/src/types/authorship-info.ts similarity index 62% rename from backend/src/database/types/authorship-info.ts rename to database/src/types/authorship-info.ts index 3efc4f07b..0c8996222 100644 --- a/backend/src/database/types/authorship-info.ts +++ b/database/src/types/authorship-info.ts @@ -3,6 +3,8 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { FieldNameApiToken } from './api-token' + /** * The AuthorshipInfo holds the information from where to where one {@link User} has changed a {@link Note} * @@ -11,19 +13,19 @@ */ export interface AuthorshipInfo { /** The id of the {@link Revision} this belongs to. */ - [FieldNameAuthorshipInfo.revisionUuid]: string; + [FieldNameAuthorshipInfo.revisionUuid]: string /** The id of the author of the edit. */ - [FieldNameAuthorshipInfo.authorId]: number; + [FieldNameAuthorshipInfo.authorId]: number /** The start position of the change in the note as a positive index. */ - [FieldNameAuthorshipInfo.startPosition]: number; + [FieldNameAuthorshipInfo.startPosition]: number /** The end position of the change in the note as a positive index. */ - [FieldNameAuthorshipInfo.endPosition]: number; + [FieldNameAuthorshipInfo.endPosition]: number /** The timestamp when the authorship entry was created. */ - [FieldNameAuthorshipInfo.createdAt]: Date; + [FieldNameAuthorshipInfo.createdAt]: string } export enum FieldNameAuthorshipInfo { @@ -34,4 +36,12 @@ export enum FieldNameAuthorshipInfo { createdAt = 'created_at', } -export const TableAuthorshipInfo = 'authorship_info'; +type TypeAuthorshipInfoDate = Omit< + AuthorshipInfo, + FieldNameAuthorshipInfo.createdAt +> & { + [FieldNameAuthorshipInfo.createdAt]: Date +} + +export type TypeInsertAuthorshipInfo = TypeAuthorshipInfoDate +export const TableAuthorshipInfo = 'authorship_info' diff --git a/backend/src/database/types/group-user.ts b/database/src/types/group-user.ts similarity index 76% rename from backend/src/database/types/group-user.ts rename to database/src/types/group-user.ts index 32f599a07..ad37d4bd9 100644 --- a/backend/src/database/types/group-user.ts +++ b/database/src/types/group-user.ts @@ -9,10 +9,10 @@ */ export interface GroupUser { /** The id of the {@link Group} a {@link User} is part of */ - [FieldNameGroupUser.groupId]: number; + [FieldNameGroupUser.groupId]: number /** The id of the {@link User} */ - [FieldNameGroupUser.userId]: number; + [FieldNameGroupUser.userId]: number } export enum FieldNameGroupUser { @@ -20,4 +20,4 @@ export enum FieldNameGroupUser { userId = 'user_id', } -export const TableGroupUser = 'group_user'; +export const TableGroupUser = 'group_user' diff --git a/backend/src/database/types/group.ts b/database/src/types/group.ts similarity index 72% rename from backend/src/database/types/group.ts rename to database/src/types/group.ts index 585881d02..1c8253a61 100644 --- a/backend/src/database/types/group.ts +++ b/database/src/types/group.ts @@ -4,6 +4,11 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +export enum SpecialGroup { + EVERYONE = '_EVERYONE', + LOGGED_IN = '_LOGGED_IN', +} + /** * A group represents one or multiple {@link User}s and can be used for permission management. * There are special groups that are created by the system and cannot be deleted, these include the set of all @@ -11,16 +16,16 @@ */ export interface Group { /** The unique id for internal referencing */ - [FieldNameGroup.id]: number; + [FieldNameGroup.id]: number /** The public identifier of the group (username for the group) */ - [FieldNameGroup.name]: string; + [FieldNameGroup.name]: string /** The display name of the group */ - [FieldNameGroup.displayName]: string; + [FieldNameGroup.displayName]: string /** Whether the group is one of the special groups */ - [FieldNameGroup.isSpecial]: boolean; + [FieldNameGroup.isSpecial]: boolean } export enum FieldNameGroup { @@ -30,9 +35,9 @@ export enum FieldNameGroup { isSpecial = 'is_special', } -export const TableGroup = 'group'; -export type TypeInsertGroup = Omit; +export const TableGroup = 'group' +export type TypeInsertGroup = Omit export type TypeUpdateGroup = Pick< Group, FieldNameGroup.name | FieldNameGroup.displayName ->; +> diff --git a/backend/src/database/types/identity.ts b/database/src/types/identity.ts similarity index 63% rename from backend/src/database/types/identity.ts rename to database/src/types/identity.ts index 926c31cbb..d8fe64e29 100644 --- a/backend/src/database/types/identity.ts +++ b/database/src/types/identity.ts @@ -3,32 +3,39 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { AuthProviderType } from '@hedgedoc/commons'; + +export enum AuthProviderType { + GUEST = 'guest', + TOKEN = 'token', + LOCAL = 'local', + LDAP = 'ldap', + OIDC = 'oidc', +} /** * An auth identity holds the information how a {@link User} can authenticate themselves using a certain auth provider */ export interface Identity { /** The id of the user */ - [FieldNameIdentity.userId]: number; + [FieldNameIdentity.userId]: number /** The type of the auth provider */ - [FieldNameIdentity.providerType]: AuthProviderType; + [FieldNameIdentity.providerType]: AuthProviderType /** The identifier of the auth provider, e.g. gitlab */ - [FieldNameIdentity.providerIdentifier]: string | null; + [FieldNameIdentity.providerIdentifier]: string | null /** Timestamp when this identity was created */ - [FieldNameIdentity.createdAt]: Date; + [FieldNameIdentity.createdAt]: string /** Timestamp when this identity was last updated */ - [FieldNameIdentity.updatedAt]: Date; + [FieldNameIdentity.updatedAt]: string /** The remote id of the user at the auth provider or null for local identities */ - [FieldNameIdentity.providerUserId]: string | null; + [FieldNameIdentity.providerUserId]: string | null /** The hashed password for local identities or null for other auth providers */ - [FieldNameIdentity.passwordHash]: string | null; + [FieldNameIdentity.passwordHash]: string | null } export enum FieldNameIdentity { @@ -41,13 +48,21 @@ export enum FieldNameIdentity { passwordHash = 'password_hash', } -export const TableIdentity = 'identity'; +export const TableIdentity = 'identity' + +type TypeIdentityDate = Omit< + Identity, + FieldNameIdentity.createdAt | FieldNameIdentity.updatedAt +> & { + [FieldNameIdentity.createdAt]: Date + [FieldNameIdentity.updatedAt]: Date +} export type TypeInsertIdentity = Omit< Identity, FieldNameIdentity.createdAt | FieldNameIdentity.updatedAt ->; +> export type TypeUpdateIdentity = Pick< - Identity, + TypeIdentityDate, FieldNameIdentity.passwordHash | FieldNameIdentity.updatedAt ->; +> diff --git a/database/src/types/index.ts b/database/src/types/index.ts new file mode 100644 index 000000000..6c982e1f3 --- /dev/null +++ b/database/src/types/index.ts @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export * from './alias.js' +export * from './api-token.js' +export * from './authorship-info.js' +export * from './group.js' +export * from './group-user.js' +export * from './identity.js' +export * from './media-upload.js' +export * from './note.js' +export * from './note-group-permission.js' +export * from './note-user-permission.js' +export * from './revision.js' +export * from './revision-tag.js' +export * from './user.js' +export * from './user-pinned-note.js' diff --git a/backend/src/database/types/media-upload.ts b/database/src/types/media-upload.ts similarity index 66% rename from backend/src/database/types/media-upload.ts rename to database/src/types/media-upload.ts index d0e37f2e0..d1843808d 100644 --- a/backend/src/database/types/media-upload.ts +++ b/database/src/types/media-upload.ts @@ -3,7 +3,14 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { BackendType } from '../../media/backends/backend-type.enum'; + +export enum MediaBackendType { + FILESYSTEM = 'filesystem', + S3 = 's3', + IMGUR = 'imgur', + AZURE = 'azure', + WEBDAV = 'webdav', +} /** * A media upload object represents an uploaded file. While the file itself is stored in the configured storage backend, @@ -12,25 +19,25 @@ import { BackendType } from '../../media/backends/backend-type.enum'; */ export interface MediaUpload { /** UUID (v7) identifying the media upload. Is public and unique */ - [FieldNameMediaUpload.uuid]: string; + [FieldNameMediaUpload.uuid]: string /** The id of the attached {@link Note} or null if the media upload was detached from a note */ - [FieldNameMediaUpload.noteId]: number | null; + [FieldNameMediaUpload.noteId]: number | null /** The id of the {@link User} who uploaded the media file */ - [FieldNameMediaUpload.userId]: number; + [FieldNameMediaUpload.userId]: number /** The name of the uploaded file */ - [FieldNameMediaUpload.fileName]: string; + [FieldNameMediaUpload.fileName]: string /** The backend where this upload is stored */ - [FieldNameMediaUpload.backendType]: BackendType; + [FieldNameMediaUpload.backendType]: MediaBackendType /** Additional data required by the backend storage to identify the uploaded file */ - [FieldNameMediaUpload.backendData]: string | null; + [FieldNameMediaUpload.backendData]: string | null /** Timestamp when the file was uploaded */ - [FieldNameMediaUpload.createdAt]: Date; + [FieldNameMediaUpload.createdAt]: string } export enum FieldNameMediaUpload { @@ -43,13 +50,17 @@ export enum FieldNameMediaUpload { createdAt = 'created_at', } -export const TableMediaUpload = 'media_upload'; +export const TableMediaUpload = 'media_upload' + +type TypeMediaUploadDate = Omit & { + [FieldNameMediaUpload.createdAt]: Date +} export type TypeInsertMediaUpload = Omit< - MediaUpload, + TypeMediaUploadDate, FieldNameMediaUpload.createdAt | FieldNameMediaUpload.uuid ->; +> export type TypeUpdateMediaUpload = Pick< - MediaUpload, + TypeMediaUploadDate, FieldNameMediaUpload.noteId ->; +> diff --git a/backend/src/database/types/note-group-permission.ts b/database/src/types/note-group-permission.ts similarity index 76% rename from backend/src/database/types/note-group-permission.ts rename to database/src/types/note-group-permission.ts index 7e817e7fd..6f1c95d7d 100644 --- a/backend/src/database/types/note-group-permission.ts +++ b/database/src/types/note-group-permission.ts @@ -8,13 +8,13 @@ */ export interface NoteGroupPermission { /** The id of the {@link Group} to give the {@link Note} permission to. */ - [FieldNameNoteGroupPermission.groupId]: number; + [FieldNameNoteGroupPermission.groupId]: number /** The id of the {@link Note} to give the {@link Group} permission to. */ - [FieldNameNoteGroupPermission.noteId]: number; + [FieldNameNoteGroupPermission.noteId]: number /** Whether the {@link Group} can edit the {@link Note} or not. */ - [FieldNameNoteGroupPermission.canEdit]: boolean; + [FieldNameNoteGroupPermission.canEdit]: boolean } export enum FieldNameNoteGroupPermission { @@ -23,9 +23,9 @@ export enum FieldNameNoteGroupPermission { canEdit = 'can_edit', } -export const TableNoteGroupPermission = 'note_group_permission'; +export const TableNoteGroupPermission = 'note_group_permission' export type TypeUpdateNoteGroupPermission = Pick< NoteGroupPermission, FieldNameNoteGroupPermission.canEdit ->; +> diff --git a/backend/src/database/types/note-user-permission.ts b/database/src/types/note-user-permission.ts similarity index 76% rename from backend/src/database/types/note-user-permission.ts rename to database/src/types/note-user-permission.ts index 61ec8cd76..6d1c6ba3f 100644 --- a/backend/src/database/types/note-user-permission.ts +++ b/database/src/types/note-user-permission.ts @@ -8,13 +8,13 @@ */ export interface NoteUserPermission { /** The id of the {@link User} to give the {@link Note} permission to. */ - [FieldNameNoteUserPermission.userId]: number; + [FieldNameNoteUserPermission.userId]: number /** The id of the {@link Note} to give the {@link User} permission to. */ - [FieldNameNoteUserPermission.noteId]: number; + [FieldNameNoteUserPermission.noteId]: number /** Whether the {@link User} can edit the {@link Note} or not. */ - [FieldNameNoteUserPermission.canEdit]: boolean; + [FieldNameNoteUserPermission.canEdit]: boolean } export enum FieldNameNoteUserPermission { @@ -23,9 +23,9 @@ export enum FieldNameNoteUserPermission { canEdit = 'can_edit', } -export const TableNoteUserPermission = 'note_user_permission'; +export const TableNoteUserPermission = 'note_user_permission' export type TypeUpdateNoteUserPermission = Pick< NoteUserPermission, FieldNameNoteUserPermission.canEdit ->; +> diff --git a/backend/src/database/types/note.ts b/database/src/types/note.ts similarity index 72% rename from backend/src/database/types/note.ts rename to database/src/types/note.ts index dc9fdef98..d7127636e 100644 --- a/backend/src/database/types/note.ts +++ b/database/src/types/note.ts @@ -11,16 +11,16 @@ */ export interface Note { /** The unique id of the note for internal referencing */ - [FieldNameNote.id]: number; + [FieldNameNote.id]: number /** The {@link User} id of the note owner */ - [FieldNameNote.ownerId]: number; + [FieldNameNote.ownerId]: number /** The HedgeDoc major version this note was created in. This is used to migrate certain features from HD1 to HD2 */ - [FieldNameNote.version]: number; + [FieldNameNote.version]: number /** Timestamp when the note was created */ - [FieldNameNote.createdAt]: Date; + [FieldNameNote.createdAt]: string } export enum FieldNameNote { @@ -30,10 +30,14 @@ export enum FieldNameNote { createdAt = 'created_at', } -export const TableNote = 'note'; +export const TableNote = 'note' + +type TypeNoteDate = Omit & { + [FieldNameNote.createdAt]: Date +} export type TypeInsertNote = Omit< - Note, + TypeNoteDate, FieldNameNote.createdAt | FieldNameNote.id ->; -export type TypeUpdateNote = Pick; +> +export type TypeUpdateNote = Pick diff --git a/backend/src/database/types/revision-tag.ts b/database/src/types/revision-tag.ts similarity index 77% rename from backend/src/database/types/revision-tag.ts rename to database/src/types/revision-tag.ts index f7da2afcd..98c88c852 100644 --- a/backend/src/database/types/revision-tag.ts +++ b/database/src/types/revision-tag.ts @@ -8,10 +8,10 @@ */ export interface RevisionTag { /** The id of {@link Revision} the {@link RevisionTag Tags} are asspcoated with. */ - [FieldNameRevisionTag.revisionUuid]: string; + [FieldNameRevisionTag.revisionUuid]: string /** The {@link RevisionTag Tag} text. */ - [FieldNameRevisionTag.tag]: string; + [FieldNameRevisionTag.tag]: string } export enum FieldNameRevisionTag { @@ -19,4 +19,4 @@ export enum FieldNameRevisionTag { tag = 'tag', } -export const TableRevisionTag = 'revision_tag'; +export const TableRevisionTag = 'revision_tag' diff --git a/backend/src/database/types/revision.ts b/database/src/types/revision.ts similarity index 61% rename from backend/src/database/types/revision.ts rename to database/src/types/revision.ts index 85c4c016c..f129b690c 100644 --- a/backend/src/database/types/revision.ts +++ b/database/src/types/revision.ts @@ -3,38 +3,42 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { NoteType } from '@hedgedoc/commons'; + +export enum NoteType { + DOCUMENT = 'document', + SLIDE = 'slide', +} /** * A revision represents the content of a {@link Note} at a specific point in time. */ export interface Revision { /** The unique id of the revision for internal referencing */ - [FieldNameRevision.uuid]: string; + [FieldNameRevision.uuid]: string /** The id of the note that this revision belongs to */ - [FieldNameRevision.noteId]: number; + [FieldNameRevision.noteId]: number /** The changes between this revision and the previous one in patch file format */ - [FieldNameRevision.patch]: string; + [FieldNameRevision.patch]: string /** The content of the note at this revision */ - [FieldNameRevision.content]: string; + [FieldNameRevision.content]: string /** The stored Y.js state for realtime editing */ - [FieldNameRevision.yjsStateVector]: null | ArrayBuffer; + [FieldNameRevision.yjsStateVector]: null | ArrayBuffer /** Whether the note is a document or presentation at this revision */ - [FieldNameRevision.noteType]: NoteType; + [FieldNameRevision.noteType]: NoteType /** The extracted note title from this revision */ - [FieldNameRevision.title]: string; + [FieldNameRevision.title]: string /** The extracted description from this revision */ - [FieldNameRevision.description]: string; + [FieldNameRevision.description]: string /** Timestamp when this revision was created */ - [FieldNameRevision.createdAt]: Date; + [FieldNameRevision.createdAt]: string } export enum FieldNameRevision { @@ -49,6 +53,13 @@ export enum FieldNameRevision { createdAt = 'created_at', } -export const TableRevision = 'revision'; +export const TableRevision = 'revision' -export type TypeInsertRevision = Omit; +type TypeRevisionDate = Omit & { + [FieldNameRevision.createdAt]: Date +} + +export type TypeInsertRevision = Omit< + TypeRevisionDate, + FieldNameRevision.createdAt +> diff --git a/backend/src/database/types/user-pinned-note.ts b/database/src/types/user-pinned-note.ts similarity index 83% rename from backend/src/database/types/user-pinned-note.ts rename to database/src/types/user-pinned-note.ts index fd9206ec4..1ba65d4d5 100644 --- a/backend/src/database/types/user-pinned-note.ts +++ b/database/src/types/user-pinned-note.ts @@ -10,10 +10,10 @@ */ export interface UserPinnedNote { /** The id of the {@link User} */ - user_id: number; + user_id: number /** The id of the {@link Note} */ - note_id: number; + note_id: number } export enum FieldNameUserPinnedNote { @@ -21,4 +21,4 @@ export enum FieldNameUserPinnedNote { noteId = 'note_id', } -export const TableUserPinnedNote = 'user_pinned_note'; +export const TableUserPinnedNote = 'user_pinned_note' diff --git a/backend/src/database/types/user.ts b/database/src/types/user.ts similarity index 80% rename from backend/src/database/types/user.ts rename to database/src/types/user.ts index eccc2792f..cff21e313 100644 --- a/backend/src/database/types/user.ts +++ b/database/src/types/user.ts @@ -17,28 +17,28 @@ */ export interface User { /** The unique id of the user for internal referencing */ - [FieldNameUser.id]: number; + [FieldNameUser.id]: number /** The user's chosen username or null if it is a guest user */ - [FieldNameUser.username]: string | null; + [FieldNameUser.username]: string | null /** The guest user's UUID or null if it is a registered user */ - [FieldNameUser.guestUuid]: string | null; + [FieldNameUser.guestUuid]: string | null /** The user's chosen display name */ - [FieldNameUser.displayName]: string; + [FieldNameUser.displayName]: string /** Timestamp when the user was created */ - [FieldNameUser.createdAt]: Date; + [FieldNameUser.createdAt]: string /** URL to the user's profile picture if present */ - [FieldNameUser.photoUrl]: string | null; + [FieldNameUser.photoUrl]: string | null /** The user's email address if present */ - [FieldNameUser.email]: string | null; + [FieldNameUser.email]: string | null /** The index which author style (e.g. color) should be used for this user */ - [FieldNameUser.authorStyle]: number; + [FieldNameUser.authorStyle]: number } export const enum FieldNameUser { @@ -52,16 +52,20 @@ export const enum FieldNameUser { authorStyle = 'author_style', } -export const TableUser = 'user'; +export const TableUser = 'user' + +type TypeUserDate = Omit & { + [FieldNameUser.createdAt]: Date +} export type TypeInsertUser = Omit< - User, + TypeUserDate, FieldNameUser.id | FieldNameUser.createdAt ->; +> export type TypeUpdateUser = Pick< User, | FieldNameUser.displayName | FieldNameUser.photoUrl | FieldNameUser.email | FieldNameUser.authorStyle ->; +> diff --git a/database/tsconfig.base.json b/database/tsconfig.base.json new file mode 100644 index 000000000..4eeed81b9 --- /dev/null +++ b/database/tsconfig.base.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "removeComments": true, + "preserveConstEnums": true, + "lib": [ + "es2022", + "dom" + ], + "declaration": true, + "strict": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "esModuleInterop": true, + "allowJs": true, + "declarationMap":true, + "sourceMap": true, + "typeRoots": ["./types"] + }, + "include": ["./src", "./types"], + "exclude": ["./dist", "**/*.test.ts"] +} diff --git a/database/tsconfig.base.json.license b/database/tsconfig.base.json.license new file mode 100644 index 000000000..52af02f4b --- /dev/null +++ b/database/tsconfig.base.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2023 Tilman Vatteroth + +SPDX-License-Identifier: CC0-1.0 diff --git a/database/tsconfig.cjs.json b/database/tsconfig.cjs.json new file mode 100644 index 000000000..a88d7f768 --- /dev/null +++ b/database/tsconfig.cjs.json @@ -0,0 +1,10 @@ +{ + "extends" : "./tsconfig.base.json", + "compilerOptions": { + "module": "CommonJS", + "target": "ES2015", + "outDir": "dist/cjs", + "declarationDir": "dist/cjs", + "moduleResolution": "node" + } +} diff --git a/database/tsconfig.cjs.json.license b/database/tsconfig.cjs.json.license new file mode 100644 index 000000000..52af02f4b --- /dev/null +++ b/database/tsconfig.cjs.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2023 Tilman Vatteroth + +SPDX-License-Identifier: CC0-1.0 diff --git a/database/tsconfig.test.json b/database/tsconfig.test.json new file mode 100644 index 000000000..67bc6edb2 --- /dev/null +++ b/database/tsconfig.test.json @@ -0,0 +1,4 @@ +{ + "extends" : "./tsconfig.esm.json", + "exclude": ["./dist"] +} diff --git a/database/tsconfig.test.json.license b/database/tsconfig.test.json.license new file mode 100644 index 000000000..52af02f4b --- /dev/null +++ b/database/tsconfig.test.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2023 Tilman Vatteroth + +SPDX-License-Identifier: CC0-1.0 diff --git a/package.json b/package.json index f58551175..054dc9d03 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "backend", "frontend", "commons", + "database", "dev-reverse-proxy", "docs", "html-to-react", diff --git a/turbo.json b/turbo.json index b11922ef2..a4580e444 100644 --- a/turbo.json +++ b/turbo.json @@ -22,6 +22,14 @@ "dist/**" ] }, + "@hedgedoc/database#build": { + "dependsOn": [ + "^build" + ], + "outputs": [ + "dist/**" + ] + }, "@hedgedoc/markdown-it-plugins#build": { "dependsOn": [ "^build" diff --git a/yarn.lock b/yarn.lock index 79374512e..0130ac049 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2824,6 +2824,21 @@ __metadata: languageName: unknown linkType: soft +"@hedgedoc/database@workspace:database": + version: 0.0.0-use.local + resolution: "@hedgedoc/database@workspace:database" + dependencies: + "@typescript-eslint/eslint-plugin": "npm:8.14.0" + "@typescript-eslint/parser": "npm:8.14.0" + eslint: "npm:8.57.1" + eslint-config-prettier: "npm:9.1.0" + eslint-plugin-jest: "npm:28.9.0" + eslint-plugin-prettier: "npm:5.2.3" + prettier: "npm:3.3.3" + typescript: "npm:5.6.3" + languageName: unknown + linkType: soft + "@hedgedoc/dev-reverse-proxy@workspace:dev-reverse-proxy": version: 0.0.0-use.local resolution: "@hedgedoc/dev-reverse-proxy@workspace:dev-reverse-proxy"