refactor(database): run knex migrations on startup

Co-authored-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
Erik Michelson 2025-05-17 23:27:47 +02:00
parent d67e44f540
commit 21a1f35281
No known key found for this signature in database
GPG key ID: DB99ADDDC5C0AF82
85 changed files with 830 additions and 418 deletions

View file

@ -4,6 +4,14 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { AliasDto } from '@hedgedoc/commons'; import { AliasDto } from '@hedgedoc/commons';
import {
Alias,
FieldNameAlias,
FieldNameNote,
Note,
TableAlias,
TypeInsertAlias,
} from '@hedgedoc/database';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import base32Encode from 'base32-encode'; import base32Encode from 'base32-encode';
import { randomBytes } from 'crypto'; import { randomBytes } from 'crypto';
@ -11,14 +19,6 @@ import { Knex } from 'knex';
import { InjectConnection } from 'nest-knexjs'; import { InjectConnection } from 'nest-knexjs';
import noteConfiguration, { NoteConfig } from '../config/note.config'; import noteConfiguration, { NoteConfig } from '../config/note.config';
import {
Alias,
FieldNameAlias,
FieldNameNote,
Note,
TableAlias,
} from '../database/types';
import { TypeInsertAlias } from '../database/types/alias';
import { import {
AlreadyInDBError, AlreadyInDBError,
ForbiddenIdError, ForbiddenIdError,

View file

@ -4,14 +4,18 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { ApiTokenDto, ApiTokenWithSecretDto } from '@hedgedoc/commons'; import { ApiTokenDto, ApiTokenWithSecretDto } from '@hedgedoc/commons';
import {
ApiToken,
FieldNameApiToken,
TableApiToken,
TypeInsertApiToken,
} from '@hedgedoc/database';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Cron, Timeout } from '@nestjs/schedule'; import { Cron, Timeout } from '@nestjs/schedule';
import { randomBytes } from 'crypto'; import { randomBytes } from 'crypto';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { InjectConnection } from 'nest-knexjs'; import { InjectConnection } from 'nest-knexjs';
import { ApiToken, FieldNameApiToken, TableApiToken } from '../database/types';
import { TypeInsertApiToken } from '../database/types/api-token';
import { import {
NotInDBError, NotInDBError,
TokenNotValidError, TokenNotValidError,
@ -71,7 +75,7 @@ export class ApiTokenService {
} }
const tokenHash = token[FieldNameApiToken.secretHash]; const tokenHash = token[FieldNameApiToken.secretHash];
const validUntil = token[FieldNameApiToken.validUntil]; const validUntil = new Date(token[FieldNameApiToken.validUntil]);
this.ensureTokenIsValid(secret, tokenHash, validUntil); this.ensureTokenIsValid(secret, tokenHash, validUntil);
await transaction(TableApiToken) await transaction(TableApiToken)
@ -133,6 +137,10 @@ export class ApiTokenService {
return this.toAuthTokenWithSecretDto( return this.toAuthTokenWithSecretDto(
{ {
...token, ...token,
[FieldNameApiToken.validUntil]:
token[FieldNameApiToken.validUntil].toISOString(),
[FieldNameApiToken.createdAt]:
token[FieldNameApiToken.createdAt].toISOString(),
[FieldNameApiToken.lastUsedAt]: null, [FieldNameApiToken.lastUsedAt]: null,
}, },
secret, secret,
@ -206,9 +214,13 @@ export class ApiTokenService {
return { return {
label: apiToken[FieldNameApiToken.label], label: apiToken[FieldNameApiToken.label],
keyId: apiToken[FieldNameApiToken.id], keyId: apiToken[FieldNameApiToken.id],
createdAt: apiToken[FieldNameApiToken.createdAt].toISOString(), createdAt: new Date(apiToken[FieldNameApiToken.createdAt]).toISOString(),
validUntil: apiToken[FieldNameApiToken.validUntil].toISOString(), validUntil: new Date(
lastUsedAt: apiToken[FieldNameApiToken.lastUsedAt]?.toISOString() ?? null, apiToken[FieldNameApiToken.validUntil],
).toISOString(),
lastUsedAt: apiToken[FieldNameApiToken.lastUsedAt]
? new Date(apiToken[FieldNameApiToken.lastUsedAt]).toISOString()
: null,
}; };
} }

View file

@ -8,6 +8,7 @@ import {
ApiTokenDto, ApiTokenDto,
ApiTokenWithSecretDto, ApiTokenWithSecretDto,
} from '@hedgedoc/commons'; } from '@hedgedoc/commons';
import { FieldNameUser, User } from '@hedgedoc/database';
import { import {
Body, Body,
Controller, Controller,
@ -21,7 +22,6 @@ import { ApiTags } from '@nestjs/swagger';
import { ApiTokenService } from '../../../api-token/api-token.service'; import { ApiTokenService } from '../../../api-token/api-token.service';
import { SessionGuard } from '../../../auth/session.guard'; import { SessionGuard } from '../../../auth/session.guard';
import { FieldNameUser, User } from '../../../database/types';
import { ConsoleLoggerService } from '../../../logger/console-logger.service'; import { ConsoleLoggerService } from '../../../logger/console-logger.service';
import { OpenApi } from '../../utils/decorators/openapi.decorator'; import { OpenApi } from '../../utils/decorators/openapi.decorator';
import { RequestUserId } from '../../utils/decorators/request-user-id.decorator'; import { RequestUserId } from '../../utils/decorators/request-user-id.decorator';

View file

@ -8,6 +8,7 @@ import {
LdapLoginDto, LdapLoginDto,
LdapLoginResponseDto, LdapLoginResponseDto,
} from '@hedgedoc/commons'; } from '@hedgedoc/commons';
import { FieldNameIdentity } from '@hedgedoc/database';
import { import {
Body, Body,
Controller, Controller,
@ -20,7 +21,6 @@ import { ApiTags } from '@nestjs/swagger';
import { IdentityService } from '../../../../auth/identity.service'; import { IdentityService } from '../../../../auth/identity.service';
import { LdapService } from '../../../../auth/ldap/ldap.service'; import { LdapService } from '../../../../auth/ldap/ldap.service';
import { FieldNameIdentity } from '../../../../database/types';
import { NotInDBError } from '../../../../errors/errors'; import { NotInDBError } from '../../../../errors/errors';
import { ConsoleLoggerService } from '../../../../logger/console-logger.service'; import { ConsoleLoggerService } from '../../../../logger/console-logger.service';
import { UsersService } from '../../../../users/users.service'; import { UsersService } from '../../../../users/users.service';

View file

@ -9,6 +9,7 @@ import {
RegisterDto, RegisterDto,
UpdatePasswordDto, UpdatePasswordDto,
} from '@hedgedoc/commons'; } from '@hedgedoc/commons';
import { FieldNameIdentity, FieldNameUser } from '@hedgedoc/database';
import { import {
Body, Body,
Controller, Controller,
@ -22,7 +23,6 @@ import { ApiTags } from '@nestjs/swagger';
import { LocalService } from '../../../../auth/local/local.service'; import { LocalService } from '../../../../auth/local/local.service';
import { SessionGuard } from '../../../../auth/session.guard'; import { SessionGuard } from '../../../../auth/session.guard';
import { FieldNameIdentity, FieldNameUser } from '../../../../database/types';
import { NoLocalIdentityError } from '../../../../errors/errors'; import { NoLocalIdentityError } from '../../../../errors/errors';
import { ConsoleLoggerService } from '../../../../logger/console-logger.service'; import { ConsoleLoggerService } from '../../../../logger/console-logger.service';
import { UsersService } from '../../../../users/users.service'; import { UsersService } from '../../../../users/users.service';

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { AuthProviderType } from '@hedgedoc/commons'; import { AuthProviderType } from '@hedgedoc/commons';
import { FieldNameIdentity } from '@hedgedoc/database';
import { import {
Controller, Controller,
Get, Get,
@ -18,7 +19,6 @@ import { ApiTags } from '@nestjs/swagger';
import { IdentityService } from '../../../../auth/identity.service'; import { IdentityService } from '../../../../auth/identity.service';
import { OidcService } from '../../../../auth/oidc/oidc.service'; import { OidcService } from '../../../../auth/oidc/oidc.service';
import { FieldNameIdentity } from '../../../../database/types';
import { ConsoleLoggerService } from '../../../../logger/console-logger.service'; import { ConsoleLoggerService } from '../../../../logger/console-logger.service';
import { UsersService } from '../../../../users/users.service'; import { UsersService } from '../../../../users/users.service';
import { OpenApi } from '../../../utils/decorators/openapi.decorator'; import { OpenApi } from '../../../utils/decorators/openapi.decorator';

View file

@ -23,7 +23,6 @@ import {
import { ApiSecurity, ApiTags } from '@nestjs/swagger'; import { ApiSecurity, ApiTags } from '@nestjs/swagger';
import { AliasService } from '../../../alias/alias.service'; import { AliasService } from '../../../alias/alias.service';
import { User } from '../../../database/types';
import { ConsoleLoggerService } from '../../../logger/console-logger.service'; import { ConsoleLoggerService } from '../../../logger/console-logger.service';
import { NoteService } from '../../../notes/note.service'; import { NoteService } from '../../../notes/note.service';
import { PermissionService } from '../../../permissions/permission.service'; import { PermissionService } from '../../../permissions/permission.service';

View file

@ -12,9 +12,9 @@ import {
NoteMetadataDto, NoteMetadataDto,
NoteMetadataSchema, NoteMetadataSchema,
} from '@hedgedoc/commons'; } from '@hedgedoc/commons';
import { User } from '@hedgedoc/database';
import { Controller, Get, UseGuards } from '@nestjs/common'; import { Controller, Get, UseGuards } from '@nestjs/common';
import { ApiSecurity, ApiTags } from '@nestjs/swagger'; import { ApiSecurity, ApiTags } from '@nestjs/swagger';
import { User } from 'src/database/types';
import { ConsoleLoggerService } from '../../../logger/console-logger.service'; import { ConsoleLoggerService } from '../../../logger/console-logger.service';
import { MediaService } from '../../../media/media.service'; import { MediaService } from '../../../media/media.service';

View file

@ -4,6 +4,13 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { MediaUploadDto, MediaUploadSchema } from '@hedgedoc/commons'; import { MediaUploadDto, MediaUploadSchema } from '@hedgedoc/commons';
import {
FieldNameMediaUpload,
FieldNameNote,
FieldNameUser,
Note,
User,
} from '@hedgedoc/database';
import { import {
BadRequestException, BadRequestException,
Controller, Controller,
@ -24,13 +31,6 @@ import {
ApiTags, ApiTags,
} from '@nestjs/swagger'; } from '@nestjs/swagger';
import {
FieldNameMediaUpload,
FieldNameNote,
FieldNameUser,
Note,
User,
} from '../../../database/types';
import { PermissionError } from '../../../errors/errors'; import { PermissionError } from '../../../errors/errors';
import { ConsoleLoggerService } from '../../../logger/console-logger.service'; import { ConsoleLoggerService } from '../../../logger/console-logger.service';
import { MediaService } from '../../../media/media.service'; import { MediaService } from '../../../media/media.service';

View file

@ -3,9 +3,9 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Note } from '@hedgedoc/database';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { Note } from '../../database/types';
import { NoteService } from '../../notes/note.service'; import { NoteService } from '../../notes/note.service';
import { extractNoteIdFromRequest } from './extract-note-id-from-request'; import { extractNoteIdFromRequest } from './extract-note-id-from-request';
import { CompleteRequest } from './request.type'; import { CompleteRequest } from './request.type';

View file

@ -3,9 +3,9 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { FieldNameNote, Note } from '@hedgedoc/database';
import { isArray } from 'class-validator'; import { isArray } from 'class-validator';
import { FieldNameNote, Note } from '../../database/types';
import { NoteService } from '../../notes/note.service'; import { NoteService } from '../../notes/note.service';
import { CompleteRequest } from './request.type'; import { CompleteRequest } from './request.type';

View file

@ -3,15 +3,15 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Note } from '@hedgedoc/database';
import { CallHandler, ExecutionContext } from '@nestjs/common'; import { CallHandler, ExecutionContext } from '@nestjs/common';
import { HttpArgumentsHost } from '@nestjs/common/interfaces/features/arguments-host.interface'; import { HttpArgumentsHost } from '@nestjs/common/interfaces/features/arguments-host.interface';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Mock } from 'ts-mockery'; 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 { GetNoteIdInterceptor } from './get-note-id.interceptor';
import { CompleteRequest } from './request.type';
describe('get note interceptor', () => { describe('get note interceptor', () => {
const mockNote = Mock.of<Note>({}); const mockNote = Mock.of<Note>({});

View file

@ -4,11 +4,10 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { AuthProviderType } from '@hedgedoc/commons'; import { AuthProviderType } from '@hedgedoc/commons';
import { FieldNameNote, FieldNameUser, Note, User } from '@hedgedoc/database';
import { Request } from 'express'; import { Request } from 'express';
import { SessionState } from 'src/sessions/session-state.type'; import { SessionState } from 'src/sessions/session-state.type';
import { FieldNameNote, FieldNameUser, Note, User } from '../../database/types';
export type CompleteRequest = Request & { export type CompleteRequest = Request & {
userId?: User[FieldNameUser.id]; userId?: User[FieldNameUser.id];
authProviderType?: AuthProviderType; authProviderType?: AuthProviderType;

View file

@ -3,16 +3,18 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { MediaBackendType } from '@hedgedoc/commons';
import { HttpAdapterHost } from '@nestjs/core'; import { HttpAdapterHost } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express'; import { NestExpressApplication } from '@nestjs/platform-express';
import { WsAdapter } from '@nestjs/platform-ws'; import { WsAdapter } from '@nestjs/platform-ws';
import { Knex } from 'knex';
import { getConnectionToken } from 'nest-knexjs';
import { AppConfig } from './config/app.config'; import { AppConfig } from './config/app.config';
import { AuthConfig } from './config/auth.config'; import { AuthConfig } from './config/auth.config';
import { MediaConfig } from './config/media.config'; import { MediaConfig } from './config/media.config';
import { ErrorExceptionMapping } from './errors/error-mapping'; import { ErrorExceptionMapping } from './errors/error-mapping';
import { ConsoleLoggerService } from './logger/console-logger.service'; import { ConsoleLoggerService } from './logger/console-logger.service';
import { BackendType } from './media/backends/backend-type.enum';
import { SessionService } from './sessions/session.service'; import { SessionService } from './sessions/session.service';
import { setupSessionMiddleware } from './utils/session'; import { setupSessionMiddleware } from './utils/session';
import { setupValidationPipe } from './utils/setup-pipes'; 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<Knex>(knexConnectionToken);
await knex.migrate.latest();
logger.log('Finished database migrations... ', 'AppBootstrap');
// Setup session handling // Setup session handling
setupSessionMiddleware( setupSessionMiddleware(
app, app,
@ -65,7 +73,7 @@ export async function setupApp(
app.useGlobalPipes(setupValidationPipe(logger)); app.useGlobalPipes(setupValidationPipe(logger));
// Map URL paths to directories // Map URL paths to directories
if (mediaConfig.backend.use === BackendType.FILESYSTEM) { if (mediaConfig.backend.use === MediaBackendType.FILESYSTEM) {
logger.log( logger.log(
`Serving the local folder '${mediaConfig.backend.filesystem.uploadPath}' under '/uploads'`, `Serving the local folder '${mediaConfig.backend.filesystem.uploadPath}' under '/uploads'`,
'AppBootstrap', 'AppBootstrap',

View file

@ -8,18 +8,19 @@ import {
PendingUserConfirmationDto, PendingUserConfirmationDto,
PendingUserInfoDto, PendingUserInfoDto,
} from '@hedgedoc/commons'; } 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 { import {
FieldNameIdentity, FieldNameIdentity,
FieldNameUser, FieldNameUser,
Identity, Identity,
TableIdentity, TableIdentity,
TypeInsertIdentity,
User, 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 { NotInDBError } from '../errors/errors';
import { ConsoleLoggerService } from '../logger/console-logger.service'; import { ConsoleLoggerService } from '../logger/console-logger.service';
import { UsersService } from '../users/users.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. * 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 authProviderType - the providerType of the wanted identity
* @param authProviderIdentifier - optional name of the provider if multiple exist * @param authProviderIdentifier - optional name of the provider if multiple exist
* @return * @return
@ -96,15 +97,12 @@ export class IdentityService {
transaction?: Knex, transaction?: Knex,
): Promise<void> { ): Promise<void> {
const dbActor = transaction ?? this.knex; const dbActor = transaction ?? this.knex;
const date = new Date(); const identity: TypeInsertIdentity = {
const identity: Identity = {
[FieldNameIdentity.userId]: userId, [FieldNameIdentity.userId]: userId,
[FieldNameIdentity.providerType]: authProviderType, [FieldNameIdentity.providerType]: authProviderType,
[FieldNameIdentity.providerIdentifier]: authProviderIdentifier, [FieldNameIdentity.providerIdentifier]: authProviderIdentifier,
[FieldNameIdentity.providerUserId]: authProviderUserId, [FieldNameIdentity.providerUserId]: authProviderUserId,
[FieldNameIdentity.passwordHash]: passwordHash ?? null, [FieldNameIdentity.passwordHash]: passwordHash ?? null,
[FieldNameIdentity.createdAt]: date,
[FieldNameIdentity.updatedAt]: date,
}; };
await dbActor(TableIdentity).insert(identity); await dbActor(TableIdentity).insert(identity);
} }

View file

@ -4,6 +4,13 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { AuthProviderType } from '@hedgedoc/commons'; import { AuthProviderType } from '@hedgedoc/commons';
import {
FieldNameIdentity,
FieldNameUser,
Identity,
TableIdentity,
User,
} from '@hedgedoc/database';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { import {
OptionsGraph, OptionsGraph,
@ -23,13 +30,6 @@ import { Knex } from 'knex';
import { InjectConnection } from 'nest-knexjs'; import { InjectConnection } from 'nest-knexjs';
import authConfiguration, { AuthConfig } from '../../config/auth.config'; import authConfiguration, { AuthConfig } from '../../config/auth.config';
import {
FieldNameIdentity,
FieldNameUser,
Identity,
TableIdentity,
User,
} from '../../database/types';
import { import {
InvalidCredentialsError, InvalidCredentialsError,
PasswordTooWeakError, PasswordTooWeakError,

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { AuthProviderType, PendingUserInfoDto } from '@hedgedoc/commons'; import { AuthProviderType, PendingUserInfoDto } from '@hedgedoc/commons';
import { Identity } from '@hedgedoc/database';
import { import {
ForbiddenException, ForbiddenException,
Inject, Inject,
@ -20,7 +21,6 @@ import authConfiguration, {
AuthConfig, AuthConfig,
OidcConfig, OidcConfig,
} from '../../config/auth.config'; } from '../../config/auth.config';
import { Identity } from '../../database/types';
import { NotInDBError } from '../../errors/errors'; import { NotInDBError } from '../../errors/errors';
import { ConsoleLoggerService } from '../../logger/console-logger.service'; import { ConsoleLoggerService } from '../../logger/console-logger.service';
import { IdentityService } from '../identity.service'; import { IdentityService } from '../identity.service';

View file

@ -3,9 +3,9 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { MediaBackendType } from '@hedgedoc/commons';
import mockedEnv from 'mocked-env'; import mockedEnv from 'mocked-env';
import { BackendType } from '../media/backends/backend-type.enum';
import mediaConfig, { import mediaConfig, {
AzureMediaConfig, AzureMediaConfig,
FilesystemMediaConfig, FilesystemMediaConfig,
@ -39,7 +39,7 @@ describe('mediaConfig', () => {
const restore = mockedEnv( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
HD_MEDIA_BACKEND: BackendType.FILESYSTEM, HD_MEDIA_BACKEND: MediaBackendType.FILESYSTEM,
HD_MEDIA_BACKEND_FILESYSTEM_UPLOAD_PATH: uploadPath, HD_MEDIA_BACKEND_FILESYSTEM_UPLOAD_PATH: uploadPath,
/* eslint-enable @typescript-eslint/naming-convention */ /* eslint-enable @typescript-eslint/naming-convention */
}, },
@ -48,7 +48,7 @@ describe('mediaConfig', () => {
}, },
); );
const config = mediaConfig() as { backend: FilesystemMediaConfig }; 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); expect(config.backend.filesystem.uploadPath).toEqual(uploadPath);
restore(); restore();
}); });
@ -57,7 +57,7 @@ describe('mediaConfig', () => {
const restore = mockedEnv( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* 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_ACCESS_KEY: accessKeyId,
HD_MEDIA_BACKEND_S3_SECRET_KEY: secretAccessKey, HD_MEDIA_BACKEND_S3_SECRET_KEY: secretAccessKey,
HD_MEDIA_BACKEND_S3_BUCKET: bucket, HD_MEDIA_BACKEND_S3_BUCKET: bucket,
@ -71,7 +71,7 @@ describe('mediaConfig', () => {
}, },
); );
const config = mediaConfig() as { backend: S3MediaConfig }; 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.accessKeyId).toEqual(accessKeyId);
expect(config.backend.s3.secretAccessKey).toEqual(secretAccessKey); expect(config.backend.s3.secretAccessKey).toEqual(secretAccessKey);
expect(config.backend.s3.bucket).toEqual(bucket); expect(config.backend.s3.bucket).toEqual(bucket);
@ -85,7 +85,7 @@ describe('mediaConfig', () => {
const restore = mockedEnv( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* 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_CONNECTION_STRING: azureConnectionString,
HD_MEDIA_BACKEND_AZURE_CONTAINER: container, HD_MEDIA_BACKEND_AZURE_CONTAINER: container,
/* eslint-enable @typescript-eslint/naming-convention */ /* eslint-enable @typescript-eslint/naming-convention */
@ -95,7 +95,7 @@ describe('mediaConfig', () => {
}, },
); );
const config = mediaConfig() as { backend: AzureMediaConfig }; 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( expect(config.backend.azure.connectionString).toEqual(
azureConnectionString, azureConnectionString,
); );
@ -107,7 +107,7 @@ describe('mediaConfig', () => {
const restore = mockedEnv( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
HD_MEDIA_BACKEND: BackendType.IMGUR, HD_MEDIA_BACKEND: MediaBackendType.IMGUR,
HD_MEDIA_BACKEND_IMGUR_CLIENT_ID: clientID, HD_MEDIA_BACKEND_IMGUR_CLIENT_ID: clientID,
/* eslint-enable @typescript-eslint/naming-convention */ /* eslint-enable @typescript-eslint/naming-convention */
}, },
@ -116,7 +116,7 @@ describe('mediaConfig', () => {
}, },
); );
const config = mediaConfig() as { backend: ImgurMediaConfig }; 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); expect(config.backend.imgur.clientId).toEqual(clientID);
restore(); restore();
}); });
@ -125,7 +125,7 @@ describe('mediaConfig', () => {
const restore = mockedEnv( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* 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_CONNECTION_STRING: webdavConnectionString,
HD_MEDIA_BACKEND_WEBDAV_UPLOAD_DIR: uploadDir, HD_MEDIA_BACKEND_WEBDAV_UPLOAD_DIR: uploadDir,
HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL: publicUrl, HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL: publicUrl,
@ -136,7 +136,7 @@ describe('mediaConfig', () => {
}, },
); );
const config = mediaConfig() as { backend: WebdavMediaConfig }; 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( expect(config.backend.webdav.connectionString).toEqual(
webdavConnectionString, webdavConnectionString,
); );
@ -152,7 +152,7 @@ describe('mediaConfig', () => {
const restore = mockedEnv( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
HD_MEDIA_BACKEND: BackendType.FILESYSTEM, HD_MEDIA_BACKEND: MediaBackendType.FILESYSTEM,
/* eslint-enable @typescript-eslint/naming-convention */ /* eslint-enable @typescript-eslint/naming-convention */
}, },
{ {
@ -171,7 +171,7 @@ describe('mediaConfig', () => {
const restore = mockedEnv( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* 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_SECRET_KEY: secretAccessKey,
HD_MEDIA_BACKEND_S3_BUCKET: bucket, HD_MEDIA_BACKEND_S3_BUCKET: bucket,
HD_MEDIA_BACKEND_S3_ENDPOINT: endPoint, HD_MEDIA_BACKEND_S3_ENDPOINT: endPoint,
@ -190,7 +190,7 @@ describe('mediaConfig', () => {
const restore = mockedEnv( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* 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_ACCESS_KEY: accessKeyId,
HD_MEDIA_BACKEND_S3_BUCKET: bucket, HD_MEDIA_BACKEND_S3_BUCKET: bucket,
HD_MEDIA_BACKEND_S3_ENDPOINT: endPoint, HD_MEDIA_BACKEND_S3_ENDPOINT: endPoint,
@ -209,7 +209,7 @@ describe('mediaConfig', () => {
const restore = mockedEnv( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* 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_ACCESS_KEY: accessKeyId,
HD_MEDIA_BACKEND_S3_SECRET_KEY: secretAccessKey, HD_MEDIA_BACKEND_S3_SECRET_KEY: secretAccessKey,
HD_MEDIA_BACKEND_S3_ENDPOINT: endPoint, HD_MEDIA_BACKEND_S3_ENDPOINT: endPoint,
@ -228,7 +228,7 @@ describe('mediaConfig', () => {
const restore = mockedEnv( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* 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_ACCESS_KEY: accessKeyId,
HD_MEDIA_BACKEND_S3_SECRET_KEY: secretAccessKey, HD_MEDIA_BACKEND_S3_SECRET_KEY: secretAccessKey,
HD_MEDIA_BACKEND_S3_BUCKET: bucket, HD_MEDIA_BACKEND_S3_BUCKET: bucket,
@ -247,7 +247,7 @@ describe('mediaConfig', () => {
const restore = mockedEnv( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* 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_ACCESS_KEY: accessKeyId,
HD_MEDIA_BACKEND_S3_SECRET_KEY: secretAccessKey, HD_MEDIA_BACKEND_S3_SECRET_KEY: secretAccessKey,
HD_MEDIA_BACKEND_S3_BUCKET: bucket, HD_MEDIA_BACKEND_S3_BUCKET: bucket,
@ -270,7 +270,7 @@ describe('mediaConfig', () => {
const restore = mockedEnv( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
HD_MEDIA_BACKEND: BackendType.AZURE, HD_MEDIA_BACKEND: MediaBackendType.AZURE,
HD_MEDIA_BACKEND_AZURE_CONTAINER: container, HD_MEDIA_BACKEND_AZURE_CONTAINER: container,
/* eslint-enable @typescript-eslint/naming-convention */ /* eslint-enable @typescript-eslint/naming-convention */
}, },
@ -287,7 +287,7 @@ describe('mediaConfig', () => {
const restore = mockedEnv( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* 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_CONNECTION_STRING: azureConnectionString,
/* eslint-enable @typescript-eslint/naming-convention */ /* eslint-enable @typescript-eslint/naming-convention */
}, },
@ -307,7 +307,7 @@ describe('mediaConfig', () => {
const restore = mockedEnv( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
HD_MEDIA_BACKEND: BackendType.IMGUR, HD_MEDIA_BACKEND: MediaBackendType.IMGUR,
/* eslint-enable @typescript-eslint/naming-convention */ /* eslint-enable @typescript-eslint/naming-convention */
}, },
{ {
@ -326,7 +326,7 @@ describe('mediaConfig', () => {
const restore = mockedEnv( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* 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_UPLOAD_DIR: uploadDir,
HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL: publicUrl, HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL: publicUrl,
/* eslint-enable @typescript-eslint/naming-convention */ /* eslint-enable @typescript-eslint/naming-convention */
@ -344,7 +344,7 @@ describe('mediaConfig', () => {
const restore = mockedEnv( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* 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_CONNECTION_STRING: 'not-an-url',
HD_MEDIA_BACKEND_WEBDAV_UPLOAD_DIR: uploadDir, HD_MEDIA_BACKEND_WEBDAV_UPLOAD_DIR: uploadDir,
HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL: publicUrl, HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL: publicUrl,
@ -363,7 +363,7 @@ describe('mediaConfig', () => {
const restore = mockedEnv( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* 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_CONNECTION_STRING: webdavConnectionString,
HD_MEDIA_BACKEND_WEBDAV_UPLOAD_DIR: uploadDir, HD_MEDIA_BACKEND_WEBDAV_UPLOAD_DIR: uploadDir,
/* eslint-enable @typescript-eslint/naming-convention */ /* eslint-enable @typescript-eslint/naming-convention */
@ -381,7 +381,7 @@ describe('mediaConfig', () => {
const restore = mockedEnv( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* 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_CONNECTION_STRING: webdavConnectionString,
HD_MEDIA_BACKEND_WEBDAV_UPLOAD_DIR: uploadDir, HD_MEDIA_BACKEND_WEBDAV_UPLOAD_DIR: uploadDir,
HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL: 'not-an-url', HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL: 'not-an-url',

View file

@ -3,10 +3,10 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { MediaBackendType } from '@hedgedoc/commons';
import { registerAs } from '@nestjs/config'; import { registerAs } from '@nestjs/config';
import z from 'zod'; import z from 'zod';
import { BackendType } from '../media/backends/backend-type.enum';
import { parseOptionalBoolean } from './utils'; import { parseOptionalBoolean } from './utils';
import { import {
buildErrorMessage, buildErrorMessage,
@ -14,7 +14,7 @@ import {
} from './zod-error-message'; } from './zod-error-message';
const azureSchema = z.object({ const azureSchema = z.object({
use: z.literal(BackendType.AZURE), use: z.literal(MediaBackendType.AZURE),
azure: z.object({ azure: z.object({
connectionString: z connectionString: z
.string() .string()
@ -24,21 +24,21 @@ const azureSchema = z.object({
}); });
const filesystemSchema = z.object({ const filesystemSchema = z.object({
use: z.literal(BackendType.FILESYSTEM), use: z.literal(MediaBackendType.FILESYSTEM),
filesystem: z.object({ filesystem: z.object({
uploadPath: z.string().describe('HD_MEDIA_BACKEND_FILESYSTEM_UPLOAD_PATH'), uploadPath: z.string().describe('HD_MEDIA_BACKEND_FILESYSTEM_UPLOAD_PATH'),
}), }),
}); });
const imgurSchema = z.object({ const imgurSchema = z.object({
use: z.literal(BackendType.IMGUR), use: z.literal(MediaBackendType.IMGUR),
imgur: z.object({ imgur: z.object({
clientId: z.string().describe('HD_MEDIA_BACKEND_IMGUR_CLIENT_ID'), clientId: z.string().describe('HD_MEDIA_BACKEND_IMGUR_CLIENT_ID'),
}), }),
}); });
const s3Schema = z.object({ const s3Schema = z.object({
use: z.literal(BackendType.S3), use: z.literal(MediaBackendType.S3),
s3: z.object({ s3: z.object({
accessKeyId: z.string().describe('HD_MEDIA_BACKEND_S3_ACCESS_KEY'), accessKeyId: z.string().describe('HD_MEDIA_BACKEND_S3_ACCESS_KEY'),
secretAccessKey: z.string().describe('HD_MEDIA_BACKEND_S3_SECRET_KEY'), secretAccessKey: z.string().describe('HD_MEDIA_BACKEND_S3_SECRET_KEY'),
@ -53,7 +53,7 @@ const s3Schema = z.object({
}); });
const webdavSchema = z.object({ const webdavSchema = z.object({
use: z.literal(BackendType.WEBDAV), use: z.literal(MediaBackendType.WEBDAV),
webdav: z.object({ webdav: z.object({
connectionString: z connectionString: z
.string() .string()

View file

@ -3,16 +3,16 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { MediaBackendType } from '@hedgedoc/commons';
import { ConfigFactoryKeyHost, registerAs } from '@nestjs/config'; import { ConfigFactoryKeyHost, registerAs } from '@nestjs/config';
import { ConfigFactory } from '@nestjs/config/dist/interfaces'; import { ConfigFactory } from '@nestjs/config/dist/interfaces';
import { BackendType } from '../../media/backends/backend-type.enum';
import { MediaConfig } from '../media.config'; import { MediaConfig } from '../media.config';
export function createDefaultMockMediaConfig(): MediaConfig { export function createDefaultMockMediaConfig(): MediaConfig {
return { return {
backend: { backend: {
use: BackendType.FILESYSTEM, use: MediaBackendType.FILESYSTEM,
filesystem: { filesystem: {
uploadPath: uploadPath:
'test_uploads' + Math.floor(Math.random() * 100000).toString(), 'test_uploads' + Math.floor(Math.random() * 100000).toString(),

View file

@ -1,13 +1,6 @@
/* /* eslint-disable */
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file) const {
* AuthProviderType,
* 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 {
FieldNameAlias, FieldNameAlias,
FieldNameApiToken, FieldNameApiToken,
FieldNameAuthorshipInfo, FieldNameAuthorshipInfo,
@ -22,6 +15,9 @@ import {
FieldNameRevisionTag, FieldNameRevisionTag,
FieldNameUser, FieldNameUser,
FieldNameUserPinnedNote, FieldNameUserPinnedNote,
MediaBackendType,
NoteType,
SpecialGroup,
TableAlias, TableAlias,
TableApiToken, TableApiToken,
TableAuthorshipInfo, TableAuthorshipInfo,
@ -36,9 +32,9 @@ import {
TableRevisionTag, TableRevisionTag,
TableUser, TableUser,
TableUserPinnedNote, TableUserPinnedNote,
} from '../types'; } = require('@hedgedoc/database');
export async function up(knex: Knex): Promise<void> { const up = async function (knex) {
// Create the user table first as it's referenced by other tables // Create the user table first as it's referenced by other tables
await knex.schema.createTable(TableUser, (table) => { await knex.schema.createTable(TableUser, (table) => {
table.increments(FieldNameUser.id).primary(); table.increments(FieldNameUser.id).primary();
@ -212,8 +208,8 @@ export async function up(knex: Knex): Promise<void> {
.unsigned() .unsigned()
.notNullable() .notNullable()
.references(FieldNameRevision.uuid) .references(FieldNameRevision.uuid)
.onDelete('CASCADE') .inTable(TableRevision)
.inTable(TableRevision); .onDelete('CASCADE');
table.string(FieldNameRevisionTag.tag).notNullable(); table.string(FieldNameRevisionTag.tag).notNullable();
table.primary([ table.primary([
FieldNameRevisionTag.revisionUuid, FieldNameRevisionTag.revisionUuid,
@ -228,15 +224,15 @@ export async function up(knex: Knex): Promise<void> {
.unsigned() .unsigned()
.notNullable() .notNullable()
.references(FieldNameRevision.uuid) .references(FieldNameRevision.uuid)
.onDelete('CASCADE') .inTable(TableRevision)
.inTable(TableRevision); .onDelete('CASCADE');
table table
.integer(FieldNameAuthorshipInfo.authorId) .integer(FieldNameAuthorshipInfo.authorId)
.unsigned() .unsigned()
.notNullable() .notNullable()
.references(FieldNameUser.id) .references(FieldNameUser.id)
.onDelete('CASCADE') .inTable(TableUser)
.inTable(TableUser); .onDelete('CASCADE');
table table
.integer(FieldNameAuthorshipInfo.startPosition) .integer(FieldNameAuthorshipInfo.startPosition)
.unsigned() .unsigned()
@ -254,15 +250,15 @@ export async function up(knex: Knex): Promise<void> {
.unsigned() .unsigned()
.notNullable() .notNullable()
.references(FieldNameNote.id) .references(FieldNameNote.id)
.onDelete('CASCADE') .inTable(TableNote)
.inTable(TableNote); .onDelete('CASCADE');
table table
.integer(FieldNameNoteUserPermission.userId) .integer(FieldNameNoteUserPermission.userId)
.unsigned() .unsigned()
.notNullable() .notNullable()
.references(FieldNameUser.id) .references(FieldNameUser.id)
.onDelete('CASCADE') .inTable(TableUser)
.inTable(TableUser); .onDelete('CASCADE');
table table
.boolean(FieldNameNoteUserPermission.canEdit) .boolean(FieldNameNoteUserPermission.canEdit)
.notNullable() .notNullable()
@ -280,15 +276,15 @@ export async function up(knex: Knex): Promise<void> {
.unsigned() .unsigned()
.notNullable() .notNullable()
.references(FieldNameNote.id) .references(FieldNameNote.id)
.onDelete('CASCADE') .inTable(TableNote)
.inTable(TableNote); .onDelete('CASCADE');
table table
.integer(FieldNameNoteGroupPermission.groupId) .integer(FieldNameNoteGroupPermission.groupId)
.unsigned() .unsigned()
.notNullable() .notNullable()
.references(FieldNameGroup.id) .references(FieldNameGroup.id)
.onDelete('CASCADE') .inTable(TableGroup)
.inTable(TableGroup); .onDelete('CASCADE');
table table
.boolean(FieldNameNoteGroupPermission.canEdit) .boolean(FieldNameNoteGroupPermission.canEdit)
.notNullable() .notNullable()
@ -319,11 +315,11 @@ export async function up(knex: Knex): Promise<void> {
.enu( .enu(
FieldNameMediaUpload.backendType, FieldNameMediaUpload.backendType,
[ [
BackendType.AZURE, MediaBackendType.AZURE,
BackendType.FILESYSTEM, MediaBackendType.FILESYSTEM,
BackendType.IMGUR, MediaBackendType.IMGUR,
BackendType.S3, MediaBackendType.S3,
BackendType.WEBDAV, MediaBackendType.WEBDAV,
], ],
{ {
useNative: true, useNative: true,
@ -356,9 +352,9 @@ export async function up(knex: Knex): Promise<void> {
FieldNameUserPinnedNote.noteId, FieldNameUserPinnedNote.noteId,
]); ]);
}); });
} };
export async function down(knex: Knex): Promise<void> { const down = async function (knex) {
// Drop tables in reverse order of creation to avoid integer key constraints // Drop tables in reverse order of creation to avoid integer key constraints
await knex.schema.dropTableIfExists(TableUserPinnedNote); await knex.schema.dropTableIfExists(TableUserPinnedNote);
await knex.schema.dropTableIfExists(TableMediaUpload); await knex.schema.dropTableIfExists(TableMediaUpload);
@ -374,4 +370,9 @@ export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableNote); await knex.schema.dropTableIfExists(TableNote);
await knex.schema.dropTableIfExists(TableGroup); await knex.schema.dropTableIfExists(TableGroup);
await knex.schema.dropTableIfExists(TableUser); await knex.schema.dropTableIfExists(TableUser);
} };
module.exports = {
up,
down,
};

View file

@ -4,15 +4,15 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { AuthProviderType } from '@hedgedoc/commons'; import { AuthProviderType } from '@hedgedoc/commons';
import { Knex } from 'knex';
import { hashPassword } from '../../utils/password';
import { import {
FieldNameIdentity, FieldNameIdentity,
FieldNameUser, FieldNameUser,
TableIdentity, TableIdentity,
TableUser, TableUser,
} from '../types'; } from '@hedgedoc/database';
import { Knex } from 'knex';
import { hashPassword } from '../../utils/password';
export async function seed(knex: Knex): Promise<void> { export async function seed(knex: Knex): Promise<void> {
// Clear tables beforehand // Clear tables beforehand

View file

@ -3,11 +3,10 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { FieldNameApiToken, TableApiToken } from '@hedgedoc/database';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { FieldNameApiToken, TableApiToken } from '../types';
export async function seed(knex: Knex): Promise<void> { export async function seed(knex: Knex): Promise<void> {
// Clear table beforehand // Clear table beforehand
await knex(TableApiToken).del(); await knex(TableApiToken).del();

View file

@ -4,11 +4,6 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { NoteType } from '@hedgedoc/commons'; 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 { import {
FieldNameAlias, FieldNameAlias,
FieldNameAuthorshipInfo, FieldNameAuthorshipInfo,
@ -24,7 +19,12 @@ import {
TableNoteUserPermission, TableNoteUserPermission,
TableRevision, TableRevision,
TableRevisionTag, 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<void> { export async function seed(knex: Knex): Promise<void> {
// Clear tables beforehand // Clear tables beforehand
@ -160,18 +160,21 @@ export async function seed(knex: Knex): Promise<void> {
[FieldNameAuthorshipInfo.authorId]: 1, [FieldNameAuthorshipInfo.authorId]: 1,
[FieldNameAuthorshipInfo.startPosition]: 0, [FieldNameAuthorshipInfo.startPosition]: 0,
[FieldNameAuthorshipInfo.endPosition]: guestNoteContent.length, [FieldNameAuthorshipInfo.endPosition]: guestNoteContent.length,
[FieldNameAuthorshipInfo.createdAt]: new Date(),
}, },
{ {
[FieldNameAuthorshipInfo.revisionUuid]: userNoteRevisionUuid, [FieldNameAuthorshipInfo.revisionUuid]: userNoteRevisionUuid,
[FieldNameAuthorshipInfo.authorId]: 2, [FieldNameAuthorshipInfo.authorId]: 2,
[FieldNameAuthorshipInfo.startPosition]: 0, [FieldNameAuthorshipInfo.startPosition]: 0,
[FieldNameAuthorshipInfo.endPosition]: userNoteContent.length, [FieldNameAuthorshipInfo.endPosition]: userNoteContent.length,
[FieldNameAuthorshipInfo.createdAt]: new Date(),
}, },
{ {
[FieldNameAuthorshipInfo.revisionUuid]: userSlideRevisionUuid, [FieldNameAuthorshipInfo.revisionUuid]: userSlideRevisionUuid,
[FieldNameAuthorshipInfo.authorId]: 2, [FieldNameAuthorshipInfo.authorId]: 2,
[FieldNameAuthorshipInfo.startPosition]: 0, [FieldNameAuthorshipInfo.startPosition]: 0,
[FieldNameAuthorshipInfo.endPosition]: userSlideContent.length, [FieldNameAuthorshipInfo.endPosition]: userSlideContent.length,
[FieldNameAuthorshipInfo.createdAt]: new Date(),
}, },
]); ]);
await knex(TableNoteGroupPermission).insert([ await knex(TableNoteGroupPermission).insert([

View file

@ -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';

View file

@ -3,15 +3,18 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * 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 { import {
Alias,
ApiToken,
AuthorshipInfo, AuthorshipInfo,
Group,
GroupUser, GroupUser,
Identity,
MediaUpload,
Note,
NoteGroupPermission,
NoteUserPermission,
Revision,
RevisionTag, RevisionTag,
TableAlias, TableAlias,
TableApiToken, TableApiToken,
@ -27,24 +30,28 @@ import {
TableRevisionTag, TableRevisionTag,
TableUser, TableUser,
TableUserPinnedNote, TableUserPinnedNote,
UserPinnedNote, TypeInsertAlias,
} from './index'; TypeInsertApiToken,
import { TypeInsertAuthorshipInfo,
MediaUpload, TypeInsertGroup,
TypeInsertIdentity,
TypeInsertMediaUpload, TypeInsertMediaUpload,
TypeInsertNote,
TypeInsertRevision,
TypeInsertUser,
TypeUpdateAlias,
TypeUpdateApiToken,
TypeUpdateGroup,
TypeUpdateIdentity,
TypeUpdateMediaUpload, TypeUpdateMediaUpload,
} from './media-upload'; TypeUpdateNote,
import { Note, TypeInsertNote, TypeUpdateNote } from './note';
import {
NoteGroupPermission,
TypeUpdateNoteGroupPermission, TypeUpdateNoteGroupPermission,
} from './note-group-permission';
import {
NoteUserPermission,
TypeUpdateNoteUserPermission, TypeUpdateNoteUserPermission,
} from './note-user-permission'; TypeUpdateUser,
import { Revision, TypeInsertRevision } from './revision'; User,
import { TypeInsertUser, TypeUpdateUser, User } from './user'; UserPinnedNote,
} from '@hedgedoc/database';
import { Knex } from 'knex';
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
declare module 'knex/types/tables.js' { declare module 'knex/types/tables.js' {
@ -59,7 +66,10 @@ declare module 'knex/types/tables.js' {
TypeInsertApiToken, TypeInsertApiToken,
TypeUpdateApiToken TypeUpdateApiToken
>; >;
[TableAuthorshipInfo]: AuthorshipInfo; [TableAuthorshipInfo]: Knex.CompositeTableType<
AuthorshipInfo,
TypeInsertAuthorshipInfo
>;
[TableGroup]: Knex.CompositeTableType< [TableGroup]: Knex.CompositeTableType<
Group, Group,
TypeInsertGroup, TypeInsertGroup,

View file

@ -4,12 +4,12 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { import {
AuthProviderDto,
AuthProviderType, AuthProviderType,
BrandingDto, BrandingDto,
FrontendConfigDto, FrontendConfigDto,
SpecialUrlDto, SpecialUrlDto,
} from '@hedgedoc/commons'; } from '@hedgedoc/commons';
import { AuthProviderDto } from '@hedgedoc/commons';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { URL } from 'url'; import { URL } from 'url';

View file

@ -4,12 +4,15 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { GroupInfoDto } from '@hedgedoc/commons'; import { GroupInfoDto } from '@hedgedoc/commons';
import {
FieldNameGroup,
TableGroup,
TypeInsertGroup,
} from '@hedgedoc/database';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { InjectConnection } from 'nest-knexjs'; import { InjectConnection } from 'nest-knexjs';
import { FieldNameGroup, TableGroup } from '../database/types';
import { TypeInsertGroup } from '../database/types/group';
import { AlreadyInDBError, NotInDBError } from '../errors/errors'; import { AlreadyInDBError, NotInDBError } from '../errors/errors';
import { ConsoleLoggerService } from '../logger/console-logger.service'; import { ConsoleLoggerService } from '../logger/console-logger.service';
@ -72,11 +75,13 @@ export class GroupsService {
* Fetches a groupId by its identifier name * Fetches a groupId by its identifier name
* *
* @param name Name of the group to query * @param name Name of the group to query
* @param transaction The optional database transaction to use
* @return The groupId * @return The groupId
* @throws {NotInDBError} if there is no group with this name * @throws {NotInDBError} if there is no group with this name
*/ */
async getGroupIdByName(name: string): Promise<number> { async getGroupIdByName(name: string, transaction?: Knex): Promise<number> {
const group = await this.knex(TableGroup) const dbActor = transaction ?? this.knex;
const group = await dbActor(TableGroup)
.select(FieldNameGroup.id) .select(FieldNameGroup.id)
.where(FieldNameGroup.name, name) .where(FieldNameGroup.name, name)
.first(); .first();

View file

@ -11,6 +11,7 @@ import {
generateBlobSASQueryParameters, generateBlobSASQueryParameters,
StorageSharedKeyCredential, StorageSharedKeyCredential,
} from '@azure/storage-blob'; } from '@azure/storage-blob';
import { MediaBackendType } from '@hedgedoc/commons';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { FileTypeResult } from 'file-type'; import { FileTypeResult } from 'file-type';
@ -21,7 +22,6 @@ import mediaConfiguration, {
import { MediaBackendError } from '../../errors/errors'; import { MediaBackendError } from '../../errors/errors';
import { ConsoleLoggerService } from '../../logger/console-logger.service'; import { ConsoleLoggerService } from '../../logger/console-logger.service';
import { MediaBackend } from '../media-backend.interface'; import { MediaBackend } from '../media-backend.interface';
import { BackendType } from './backend-type.enum';
@Injectable() @Injectable()
export class AzureBackend implements MediaBackend { export class AzureBackend implements MediaBackend {
@ -36,7 +36,7 @@ export class AzureBackend implements MediaBackend {
) { ) {
this.logger.setContext(AzureBackend.name); this.logger.setContext(AzureBackend.name);
this.config = (this.mediaConfig.backend as AzureMediaConfig).azure; 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 // only create the client if the backend is configured to azure
const blobServiceClient = BlobServiceClient.fromConnectionString( const blobServiceClient = BlobServiceClient.fromConnectionString(
this.config.connectionString, this.config.connectionString,

View file

@ -3,11 +3,3 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
export enum BackendType {
FILESYSTEM = 'filesystem',
S3 = 's3',
IMGUR = 'imgur',
AZURE = 'azure',
WEBDAV = 'webdav',
}

View file

@ -3,13 +3,13 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { MediaBackendType } from '@hedgedoc/commons';
import * as MinioModule from 'minio'; import * as MinioModule from 'minio';
import { Client, ClientOptions } from 'minio'; import { Client, ClientOptions } from 'minio';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { MediaConfig } from '../../config/media.config'; import { MediaConfig } from '../../config/media.config';
import { ConsoleLoggerService } from '../../logger/console-logger.service'; import { ConsoleLoggerService } from '../../logger/console-logger.service';
import { BackendType } from './backend-type.enum';
import { S3Backend } from './s3-backend'; import { S3Backend } from './s3-backend';
jest.mock('minio'); jest.mock('minio');
@ -43,7 +43,7 @@ describe('s3 backend', () => {
function mockMediaConfig(endPoint: string): MediaConfig { function mockMediaConfig(endPoint: string): MediaConfig {
return Mock.of<MediaConfig>({ return Mock.of<MediaConfig>({
backend: { backend: {
use: BackendType.S3, use: MediaBackendType.S3,
s3: { s3: {
accessKeyId: mockedS3AccessKeyId, accessKeyId: mockedS3AccessKeyId,
secretAccessKey: mockedS3SecretAccessKey, secretAccessKey: mockedS3SecretAccessKey,

View file

@ -3,6 +3,7 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { MediaBackendType } from '@hedgedoc/commons';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { FileTypeResult } from 'file-type'; import { FileTypeResult } from 'file-type';
import { Client } from 'minio'; import { Client } from 'minio';
@ -15,7 +16,6 @@ import mediaConfiguration, {
import { MediaBackendError } from '../../errors/errors'; import { MediaBackendError } from '../../errors/errors';
import { ConsoleLoggerService } from '../../logger/console-logger.service'; import { ConsoleLoggerService } from '../../logger/console-logger.service';
import { MediaBackend } from '../media-backend.interface'; import { MediaBackend } from '../media-backend.interface';
import { BackendType } from './backend-type.enum';
@Injectable() @Injectable()
export class S3Backend implements MediaBackend { export class S3Backend implements MediaBackend {
@ -33,7 +33,7 @@ export class S3Backend implements MediaBackend {
private mediaConfig: MediaConfig, private mediaConfig: MediaConfig,
) { ) {
this.logger.setContext(S3Backend.name); this.logger.setContext(S3Backend.name);
if (this.mediaConfig.backend.use !== BackendType.S3) { if (this.mediaConfig.backend.use !== MediaBackendType.S3) {
return; return;
} }
this.config = this.mediaConfig.backend.s3; this.config = this.mediaConfig.backend.s3;

View file

@ -3,6 +3,7 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { MediaBackendType } from '@hedgedoc/commons';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { FileTypeResult } from 'file-type'; import { FileTypeResult } from 'file-type';
import fetch, { Response } from 'node-fetch'; import fetch, { Response } from 'node-fetch';
@ -15,7 +16,6 @@ import mediaConfiguration, {
import { MediaBackendError } from '../../errors/errors'; import { MediaBackendError } from '../../errors/errors';
import { ConsoleLoggerService } from '../../logger/console-logger.service'; import { ConsoleLoggerService } from '../../logger/console-logger.service';
import { MediaBackend } from '../media-backend.interface'; import { MediaBackend } from '../media-backend.interface';
import { BackendType } from './backend-type.enum';
@Injectable() @Injectable()
export class WebdavBackend implements MediaBackend { export class WebdavBackend implements MediaBackend {
@ -29,7 +29,7 @@ export class WebdavBackend implements MediaBackend {
private mediaConfig: MediaConfig, private mediaConfig: MediaConfig,
) { ) {
this.logger.setContext(WebdavBackend.name); 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; this.config = this.mediaConfig.backend.webdav;
const url = new URL(this.config.connectionString); const url = new URL(this.config.connectionString);
this.baseUrl = url.toString(); this.baseUrl = url.toString();

View file

@ -3,15 +3,7 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { MediaUploadDto } from '@hedgedoc/commons'; import { MediaBackendType, 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 { import {
Alias, Alias,
FieldNameAlias, FieldNameAlias,
@ -24,12 +16,18 @@ import {
TableMediaUpload, TableMediaUpload,
TableUser, TableUser,
User, 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 { ClientError, NotInDBError } from '../errors/errors';
import { ConsoleLoggerService } from '../logger/console-logger.service'; import { ConsoleLoggerService } from '../logger/console-logger.service';
import { NoteService } from '../notes/note.service';
import { AzureBackend } from './backends/azure-backend'; import { AzureBackend } from './backends/azure-backend';
import { BackendType } from './backends/backend-type.enum';
import { FilesystemBackend } from './backends/filesystem-backend'; import { FilesystemBackend } from './backends/filesystem-backend';
import { ImgurBackend } from './backends/imgur-backend'; import { ImgurBackend } from './backends/imgur-backend';
import { S3Backend } from './backends/s3-backend'; import { S3Backend } from './backends/s3-backend';
@ -39,7 +37,7 @@ import { MediaBackend } from './media-backend.interface';
@Injectable() @Injectable()
export class MediaService { export class MediaService {
mediaBackend: MediaBackend; mediaBackend: MediaBackend;
mediaBackendType: BackendType; mediaBackendType: MediaBackendType;
constructor( constructor(
private readonly logger: ConsoleLoggerService, private readonly logger: ConsoleLoggerService,
@ -246,18 +244,18 @@ export class MediaService {
.where(FieldNameMediaUpload.uuid, uuid); .where(FieldNameMediaUpload.uuid, uuid);
} }
private chooseBackendType(): BackendType { private chooseBackendType(): MediaBackendType {
switch (this.mediaConfig.backend.use as string) { switch (this.mediaConfig.backend.use as string) {
case 'filesystem': case 'filesystem':
return BackendType.FILESYSTEM; return MediaBackendType.FILESYSTEM;
case 'azure': case 'azure':
return BackendType.AZURE; return MediaBackendType.AZURE;
case 'imgur': case 'imgur':
return BackendType.IMGUR; return MediaBackendType.IMGUR;
case 's3': case 's3':
return BackendType.S3; return MediaBackendType.S3;
case 'webdav': case 'webdav':
return BackendType.WEBDAV; return MediaBackendType.WEBDAV;
default: default:
throw new Error( throw new Error(
`Unexpected media backend ${this.mediaConfig.backend.use}`, `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) { switch (type) {
case BackendType.FILESYSTEM: case MediaBackendType.FILESYSTEM:
return this.moduleRef.get(FilesystemBackend); return this.moduleRef.get(FilesystemBackend);
case BackendType.S3: case MediaBackendType.S3:
return this.moduleRef.get(S3Backend); return this.moduleRef.get(S3Backend);
case BackendType.AZURE: case MediaBackendType.AZURE:
return this.moduleRef.get(AzureBackend); return this.moduleRef.get(AzureBackend);
case BackendType.IMGUR: case MediaBackendType.IMGUR:
return this.moduleRef.get(ImgurBackend); return this.moduleRef.get(ImgurBackend);
case BackendType.WEBDAV: case MediaBackendType.WEBDAV:
return this.moduleRef.get(WebdavBackend); return this.moduleRef.get(WebdavBackend);
} }
} }
@ -309,7 +307,9 @@ export class MediaService {
uuid: mediaUpload[FieldNameMediaUpload.uuid], uuid: mediaUpload[FieldNameMediaUpload.uuid],
fileName: mediaUpload[FieldNameMediaUpload.fileName], fileName: mediaUpload[FieldNameMediaUpload.fileName],
noteId: mediaUpload[FieldNameAlias.alias], noteId: mediaUpload[FieldNameAlias.alias],
createdAt: mediaUpload[FieldNameMediaUpload.createdAt].toISOString(), createdAt: new Date(
mediaUpload[FieldNameMediaUpload.createdAt],
).toISOString(),
username: mediaUpload[FieldNameUser.username], username: mediaUpload[FieldNameUser.username],
})); }));
} }

View file

@ -9,14 +9,6 @@ import {
NotePermissionsDto, NotePermissionsDto,
SpecialGroup, SpecialGroup,
} from '@hedgedoc/commons'; } 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 { import {
FieldNameAlias, FieldNameAlias,
FieldNameGroup, FieldNameGroup,
@ -36,7 +28,15 @@ import {
TableNoteUserPermission, TableNoteUserPermission,
TableUser, TableUser,
User, 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 { import {
ForbiddenIdError, ForbiddenIdError,
GenericDBError, GenericDBError,
@ -141,6 +141,7 @@ export class NoteService {
await this.revisionsService.createRevision( await this.revisionsService.createRevision(
noteId, noteId,
noteContent, noteContent,
true,
transaction, transaction,
); );
@ -162,6 +163,7 @@ export class NoteService {
if (everyoneAccessLevel !== DefaultAccessLevel.NONE) { if (everyoneAccessLevel !== DefaultAccessLevel.NONE) {
const everyoneAccessGroupId = await this.groupsService.getGroupIdByName( const everyoneAccessGroupId = await this.groupsService.getGroupIdByName(
SpecialGroup.EVERYONE, SpecialGroup.EVERYONE,
transaction,
); );
await this.permissionService.setGroupPermission( await this.permissionService.setGroupPermission(
noteId, noteId,
@ -173,7 +175,10 @@ export class NoteService {
if (loggedInUsersAccessLevel !== DefaultAccessLevel.NONE) { if (loggedInUsersAccessLevel !== DefaultAccessLevel.NONE) {
const loggedInUsersAccessGroupId = const loggedInUsersAccessGroupId =
await this.groupsService.getGroupIdByName(SpecialGroup.LOGGED_IN); await this.groupsService.getGroupIdByName(
SpecialGroup.LOGGED_IN,
transaction,
);
await this.permissionService.setGroupPermission( await this.permissionService.setGroupPermission(
noteId, noteId,
loggedInUsersAccessGroupId, loggedInUsersAccessGroupId,
@ -415,6 +420,12 @@ export class NoteService {
'toNoteMetadataDto', '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( const latestRevision = await this.revisionsService.getLatestRevision(
noteId, noteId,
transaction, transaction,
@ -423,28 +434,40 @@ export class NoteService {
latestRevision[FieldNameRevision.uuid], latestRevision[FieldNameRevision.uuid],
transaction, transaction,
); );
const permissions = await this.toNotePermissionsDto(noteId, transaction);
const updateUsers = await this.revisionsService.getRevisionUserInfo( const updateUsers = await this.revisionsService.getRevisionUserInfo(
latestRevision[FieldNameRevision.uuid], latestRevision[FieldNameRevision.uuid],
transaction,
); );
updateUsers.users.sort( updateUsers.users.sort();
(userA, userB) => userB.createdAt.getTime() - userA.createdAt.getTime(),
); let lastUpdatedBy;
const lastEdit = updateUsers.users[0]; let editedBy;
const editedBy = updateUsers.users.map((user) => user.username); let updatedAt;
const permissions = await this.toNotePermissionsDto(noteId, transaction); 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 { return {
aliases: aliases.map((alias) => alias[FieldNameAlias.alias]), aliases: aliases.map((alias) => alias[FieldNameAlias.alias]),
primaryAlias: primaryAlias[FieldNameAlias.alias], primaryAlias: primaryAlias[FieldNameAlias.alias],
title: latestRevision.title, title: latestRevision.title,
description: latestRevision.description, description: latestRevision.description,
tags: tags, tags,
createdAt: note[FieldNameNote.createdAt].toISOString(), createdAt,
editedBy: editedBy, editedBy,
permissions: permissions, permissions,
version: note[FieldNameNote.version], version,
updatedAt: lastEdit.createdAt.toISOString(), updatedAt,
lastUpdatedBy: lastEdit.username, lastUpdatedBy,
}; };
} }

View file

@ -8,12 +8,6 @@ import {
PermissionLevel, PermissionLevel,
SpecialGroup, SpecialGroup,
} from '@hedgedoc/commons'; } 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 { import {
FieldNameGroup, FieldNameGroup,
FieldNameGroupUser, FieldNameGroupUser,
@ -29,7 +23,13 @@ import {
TableNoteGroupPermission, TableNoteGroupPermission,
TableNoteUserPermission, TableNoteUserPermission,
TableUser, 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 { GenericDBError, NotInDBError } from '../errors/errors';
import { NoteEvent, NoteEventMap } from '../events'; import { NoteEvent, NoteEventMap } from '../events';
import { ConsoleLoggerService } from '../logger/console-logger.service'; import { ConsoleLoggerService } from '../logger/console-logger.service';
@ -334,6 +334,7 @@ export class PermissionService {
* @param noteId - the if of the note * @param noteId - the if of the note
* @param groupId - the name of the group for which the permission should be set * @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 canEdit - specifies if the group can edit the note
* @param transaction The optional transaction for the database
*/ */
async setGroupPermission( async setGroupPermission(
noteId: number, noteId: number,

View file

@ -3,6 +3,7 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { FieldNameUser } from '@hedgedoc/database';
import { import {
CanActivate, CanActivate,
ExecutionContext, ExecutionContext,
@ -14,7 +15,6 @@ import { Reflector } from '@nestjs/core';
import { extractNoteIdFromRequest } from '../api/utils/extract-note-id-from-request'; import { extractNoteIdFromRequest } from '../api/utils/extract-note-id-from-request';
import { CompleteRequest } from '../api/utils/request.type'; import { CompleteRequest } from '../api/utils/request.type';
import { FieldNameUser } from '../database/types';
import { ConsoleLoggerService } from '../logger/console-logger.service'; import { ConsoleLoggerService } from '../logger/console-logger.service';
import { NoteService } from '../notes/note.service'; import { NoteService } from '../notes/note.service';
import { UsersService } from '../users/users.service'; import { UsersService } from '../users/users.service';

View file

@ -9,9 +9,9 @@ import {
YDocSyncServerAdapter, YDocSyncServerAdapter,
} from '@hedgedoc/commons'; } from '@hedgedoc/commons';
import * as HedgeDocCommonsModule from '@hedgedoc/commons'; import * as HedgeDocCommonsModule from '@hedgedoc/commons';
import { FieldNameUser, User } from '@hedgedoc/database';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { FieldNameUser, User } from '../../database/types';
import * as NameRandomizerModule from './random-word-lists/name-randomizer'; import * as NameRandomizerModule from './random-word-lists/name-randomizer';
import { RealtimeConnection } from './realtime-connection'; import { RealtimeConnection } from './realtime-connection';
import { RealtimeNote } from './realtime-note'; import { RealtimeNote } from './realtime-note';

View file

@ -3,13 +3,13 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { FieldNameRevision } from '@hedgedoc/database';
import { Optional } from '@mrdrogdrog/optional'; import { Optional } from '@mrdrogdrog/optional';
import { BeforeApplicationShutdown, Inject, Injectable } from '@nestjs/common'; import { BeforeApplicationShutdown, Inject, Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter'; import { OnEvent } from '@nestjs/event-emitter';
import { SchedulerRegistry } from '@nestjs/schedule'; import { SchedulerRegistry } from '@nestjs/schedule';
import appConfiguration, { AppConfig } from '../../config/app.config'; import appConfiguration, { AppConfig } from '../../config/app.config';
import { FieldNameRevision } from '../../database/types';
import { NoteEvent } from '../../events'; import { NoteEvent } from '../../events';
import { ConsoleLoggerService } from '../../logger/console-logger.service'; import { ConsoleLoggerService } from '../../logger/console-logger.service';
import { NotePermissionLevel } from '../../permissions/note-permission.enum'; import { NotePermissionLevel } from '../../permissions/note-permission.enum';
@ -47,6 +47,7 @@ export class RealtimeNoteService implements BeforeApplicationShutdown {
.createRevision( .createRevision(
realtimeNote.getNoteId(), realtimeNote.getNoteId(),
realtimeNote.getRealtimeDoc().getCurrentContent(), realtimeNote.getRealtimeDoc().getCurrentContent(),
false,
undefined, undefined,
realtimeNote.getRealtimeDoc().encodeStateAsUpdate(), realtimeNote.getRealtimeDoc().encodeStateAsUpdate(),
) )

View file

@ -7,9 +7,9 @@ import {
MockedBackendTransportAdapter, MockedBackendTransportAdapter,
YDocSyncServerAdapter, YDocSyncServerAdapter,
} from '@hedgedoc/commons'; } from '@hedgedoc/commons';
import { FieldNameUser, User } from '@hedgedoc/database';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { FieldNameUser, User } from '../../../database/types';
import { RealtimeConnection } from '../realtime-connection'; import { RealtimeConnection } from '../realtime-connection';
import { RealtimeNote } from '../realtime-note'; import { RealtimeNote } from '../realtime-note';
import { RealtimeUserStatusAdapter } from '../realtime-user-status-adapter'; import { RealtimeUserStatusAdapter } from '../realtime-user-status-adapter';

View file

@ -4,11 +4,11 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { DisconnectReason, MessageTransporter } from '@hedgedoc/commons'; import { DisconnectReason, MessageTransporter } from '@hedgedoc/commons';
import { FieldNameUser } from '@hedgedoc/database';
import { OnGatewayConnection, WebSocketGateway } from '@nestjs/websockets'; import { OnGatewayConnection, WebSocketGateway } from '@nestjs/websockets';
import { IncomingMessage } from 'http'; import { IncomingMessage } from 'http';
import WebSocket from 'ws'; import WebSocket from 'ws';
import { FieldNameUser } from '../../database/types';
import { ConsoleLoggerService } from '../../logger/console-logger.service'; import { ConsoleLoggerService } from '../../logger/console-logger.service';
import { NoteService } from '../../notes/note.service'; import { NoteService } from '../../notes/note.service';
import { NotePermissionLevel } from '../../permissions/note-permission.enum'; import { NotePermissionLevel } from '../../permissions/note-permission.enum';

View file

@ -4,15 +4,6 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { RevisionDto, RevisionMetadataDto } from '@hedgedoc/commons'; 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 { import {
AuthorshipInfo, AuthorshipInfo,
FieldNameAlias, FieldNameAlias,
@ -30,7 +21,16 @@ import {
TableRevisionTag, TableRevisionTag,
TableUser, TableUser,
User, 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 { GenericDBError, NotInDBError } from '../errors/errors';
import { ConsoleLoggerService } from '../logger/console-logger.service'; import { ConsoleLoggerService } from '../logger/console-logger.service';
import { extractRevisionMetadataFromContent } from './utils/extract-revision-metadata-from-content'; import { extractRevisionMetadataFromContent } from './utils/extract-revision-metadata-from-content';
@ -129,7 +129,7 @@ export class RevisionsService {
recordMap.set(revision[FieldNameRevision.uuid], { recordMap.set(revision[FieldNameRevision.uuid], {
uuid: revision[FieldNameRevision.uuid], uuid: revision[FieldNameRevision.uuid],
length: (revision[FieldNameRevision.content] ?? '').length, length: (revision[FieldNameRevision.content] ?? '').length,
createdAt: revision[FieldNameRevision.createdAt].toISOString(), createdAt: revision[FieldNameRevision.createdAt],
authorUsernames: authorUsernames:
revision[FieldNameUser.username] !== null revision[FieldNameUser.username] !== null
? [revision[FieldNameUser.username]] ? [revision[FieldNameUser.username]]
@ -217,7 +217,7 @@ export class RevisionsService {
uuid: revision[FieldNameRevision.uuid], uuid: revision[FieldNameRevision.uuid],
content: revision[FieldNameRevision.content], content: revision[FieldNameRevision.content],
length: (revision[FieldNameRevision.content] ?? '').length, length: (revision[FieldNameRevision.content] ?? '').length,
createdAt: revision[FieldNameRevision.createdAt].toISOString(), createdAt: revision[FieldNameRevision.createdAt],
title: revision[FieldNameRevision.title], title: revision[FieldNameRevision.title],
description: revision[FieldNameRevision.description], description: revision[FieldNameRevision.description],
patch: revision.patch, patch: revision.patch,
@ -241,7 +241,7 @@ export class RevisionsService {
.first(); .first();
if (revision === undefined) { if (revision === undefined) {
throw new NotInDBError( throw new NotInDBError(
`No revisions for note ${noteId} found`, 'No revisions for note found',
this.logger.getContext(), this.logger.getContext(),
'getLatestRevision', 'getLatestRevision',
); );
@ -249,8 +249,12 @@ export class RevisionsService {
return revision; return revision;
} }
async getRevisionUserInfo(revisionUuid: string): Promise<RevisionUserInfo> { async getRevisionUserInfo(
const authorUsernamesAndGuestUuids = (await this.knex(TableAuthorshipInfo) revisionUuid: string,
transaction?: Knex,
): Promise<RevisionUserInfo> {
const dbActor = transaction ?? this.knex;
const authorUsernamesAndGuestUuids = (await dbActor(TableAuthorshipInfo)
.join( .join(
TableUser, TableUser,
`${TableAuthorshipInfo}.${FieldNameAuthorshipInfo.authorId}`, `${TableAuthorshipInfo}.${FieldNameAuthorshipInfo.authorId}`,
@ -293,6 +297,7 @@ export class RevisionsService {
* @async * @async
* @param noteId The note for which the revision should be created * @param noteId The note for which the revision should be created
* @param newContent The new note content * @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 transaction The optional pre-existing database transaction to use
* @param yjsStateVector The yjs state vector that describes the new content * @param yjsStateVector The yjs state vector that describes the new content
* @return {Revision} the created revision * @return {Revision} the created revision
@ -301,6 +306,7 @@ export class RevisionsService {
async createRevision( async createRevision(
noteId: number, noteId: number,
newContent: string, newContent: string,
firstRevision: boolean = false,
transaction?: Knex, transaction?: Knex,
yjsStateVector?: ArrayBuffer, yjsStateVector?: ArrayBuffer,
): Promise<void> { ): Promise<void> {
@ -309,6 +315,7 @@ export class RevisionsService {
await this.innerCreateRevision( await this.innerCreateRevision(
noteId, noteId,
newContent, newContent,
firstRevision,
newTransaction, newTransaction,
yjsStateVector, yjsStateVector,
); );
@ -318,6 +325,7 @@ export class RevisionsService {
await this.innerCreateRevision( await this.innerCreateRevision(
noteId, noteId,
newContent, newContent,
firstRevision,
transaction, transaction,
yjsStateVector, yjsStateVector,
); );
@ -326,13 +334,13 @@ export class RevisionsService {
private async innerCreateRevision( private async innerCreateRevision(
noteId: number, noteId: number,
newContent: string, newContent: string,
firstRevision: boolean,
transaction: Knex, transaction: Knex,
yjsStateVector?: ArrayBuffer, yjsStateVector?: ArrayBuffer,
): Promise<void> { ): Promise<void> {
const latestRevision = const latestRevision = firstRevision
noteId === undefined ? null
? null : await this.getLatestRevision(noteId, transaction);
: await this.getLatestRevision(noteId, transaction);
const oldContent = latestRevision?.content; const oldContent = latestRevision?.content;
if (oldContent === newContent) { if (oldContent === newContent) {
return undefined; return undefined;
@ -346,6 +354,7 @@ export class RevisionsService {
latestRevision?.content ?? '', latestRevision?.content ?? '',
newContent, newContent,
); );
const { title, description, tags, noteType } = const { title, description, tags, noteType } =
extractRevisionMetadataFromContent(newContent); extractRevisionMetadataFromContent(newContent);
const revisionIds = await transaction(TableRevision).insert( const revisionIds = await transaction(TableRevision).insert(
@ -369,12 +378,14 @@ export class RevisionsService {
); );
} }
const revisionId = revisionIds[0][FieldNameRevision.uuid]; const revisionId = revisionIds[0][FieldNameRevision.uuid];
await transaction(TableRevisionTag).insert( if (tags.length > 0) {
tags.map((tag) => ({ await transaction(TableRevisionTag).insert(
[FieldNameRevisionTag.tag]: tag, tags.map((tag) => ({
[FieldNameRevisionTag.revisionUuid]: revisionId, [FieldNameRevisionTag.tag]: tag,
})), [FieldNameRevisionTag.revisionUuid]: revisionId,
); })),
);
}
} }
async getTagsByRevisionUuid( async getTagsByRevisionUuid(

View file

@ -4,10 +4,9 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { AuthProviderType, PendingUserInfoDto } from '@hedgedoc/commons'; import { AuthProviderType, PendingUserInfoDto } from '@hedgedoc/commons';
import { FieldNameUser, User } from '@hedgedoc/database';
import { Cookie } from 'express-session'; import { Cookie } from 'express-session';
import { FieldNameUser, User } from '../database/types';
export interface SessionState { export interface SessionState {
/** Details about the currently used session cookie */ /** Details about the currently used session cookie */
cookie: Cookie; cookie: Cookie;

View file

@ -3,6 +3,7 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { FieldNameUser, User } from '@hedgedoc/database';
import { Optional } from '@mrdrogdrog/optional'; import { Optional } from '@mrdrogdrog/optional';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { parse as parseCookie } from 'cookie'; import { parse as parseCookie } from 'cookie';
@ -10,7 +11,6 @@ import { unsign } from 'cookie-signature';
import { IncomingMessage } from 'http'; import { IncomingMessage } from 'http';
import authConfiguration, { AuthConfig } from '../config/auth.config'; import authConfiguration, { AuthConfig } from '../config/auth.config';
import { FieldNameUser, User } from '../database/types';
import { ConsoleLoggerService } from '../logger/console-logger.service'; import { ConsoleLoggerService } from '../logger/console-logger.service';
import { HEDGEDOC_SESSION } from '../utils/session'; import { HEDGEDOC_SESSION } from '../utils/session';
import { KeyvSessionStore } from './keyv-session-store'; import { KeyvSessionStore } from './keyv-session-store';

View file

@ -5,17 +5,21 @@
*/ */
import { import {
AuthProviderType, AuthProviderType,
LoginUserInfoDto,
REGEX_USERNAME, REGEX_USERNAME,
UserInfoDto, UserInfoDto,
} from '@hedgedoc/commons'; } from '@hedgedoc/commons';
import { LoginUserInfoDto } from '@hedgedoc/commons'; import {
FieldNameUser,
TableUser,
TypeUpdateUser,
User,
} from '@hedgedoc/database';
import { BadRequestException, Injectable } from '@nestjs/common'; import { BadRequestException, Injectable } from '@nestjs/common';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { InjectConnection } from 'nest-knexjs'; import { InjectConnection } from 'nest-knexjs';
import { v4 as uuidv4 } from 'uuid'; 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 { GenericDBError, NotInDBError } from '../errors/errors';
import { ConsoleLoggerService } from '../logger/console-logger.service'; import { ConsoleLoggerService } from '../logger/console-logger.service';
import { generateRandomName } from '../realtime/realtime-note/random-word-lists/name-randomizer'; import { generateRandomName } from '../realtime/realtime-note/random-word-lists/name-randomizer';
@ -209,11 +213,12 @@ export class UsersService {
transaction?: Knex, transaction?: Knex,
): Promise<boolean> { ): Promise<boolean> {
const dbActor = transaction ? transaction : this.knex; const dbActor = transaction ? transaction : this.knex;
const username = await dbActor(TableUser) const usernameResponse = await dbActor(TableUser)
.select(FieldNameUser.username) .select(FieldNameUser.username)
.where(FieldNameUser.id, userId) .where(FieldNameUser.id, userId)
.first(); .first();
return username !== null && username !== undefined; const username = usernameResponse?.[FieldNameUser.username] ?? null;
return username !== null;
} }
/** /**

View file

@ -4,3 +4,4 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
export * from './media-upload.dto.js' export * from './media-upload.dto.js'
export * from './media-backend-type.enum.js'

View file

@ -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',
}

40
database/.eslintrc.cjs Normal file
View file

@ -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"
}
}

31
database/.gitignore vendored Normal file
View file

@ -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*

9
database/.npmignore Normal file
View file

@ -0,0 +1,9 @@
.idea
.babelrc
.eslintrc
.travis.yml
karma.conf.js
tests.webpack.js
webpack.config.*.js
coverage/
test/

View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: CC0-1.0

1
database/.prettierignore Normal file
View file

@ -0,0 +1 @@
node_modules/

View file

@ -0,0 +1,4 @@
SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: CC0-1.0

11
database/.prettierrc.json Normal file
View file

@ -0,0 +1,11 @@
{
"parser": "typescript",
"singleQuote": true,
"jsxSingleQuote": true,
"semi": false,
"tabWidth": 2,
"trailingComma": "all",
"bracketSpacing": true,
"bracketSameLine": true,
"arrowParens": "always"
}

View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: CC0-1.0

23
database/build.sh Executable file
View file

@ -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 <<!EOF
{
"type": "commonjs"
}
!EOF
echo "🦔 > Done!"

27
database/jest.config.json Normal file
View file

@ -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": ["<rootDir>/node_modules/"],
"transform" : {
"^.+\\.tsx?$" : [
"ts-jest",
{
"tsconfig" : "tsconfig.test.json",
"useESM" : true
}
]
}
}

View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: CC0-1.0

47
database/package.json Normal file
View file

@ -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"
}

View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: CC0-1.0

6
database/src/index.ts Normal file
View file

@ -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'

View file

@ -12,13 +12,13 @@
*/ */
export interface Alias { export interface Alias {
/** The alias as defined by the user. Is unique. */ /** The alias as defined by the user. Is unique. */
[FieldNameAlias.alias]: string; [FieldNameAlias.alias]: string
/** The id of the associated {@link Note}. */ /** The id of the associated {@link Note}. */
[FieldNameAlias.noteId]: number; [FieldNameAlias.noteId]: number
/** Whether the alias is the primary one for the note. */ /** Whether the alias is the primary one for the note. */
[FieldNameAlias.isPrimary]: boolean; [FieldNameAlias.isPrimary]: boolean
} }
export enum FieldNameAlias { export enum FieldNameAlias {
@ -27,7 +27,7 @@ export enum FieldNameAlias {
isPrimary = 'is_primary', isPrimary = 'is_primary',
} }
export const TableAlias = 'alias'; export const TableAlias = 'alias'
export type TypeInsertAlias = Alias; export type TypeInsertAlias = Alias
export type TypeUpdateAlias = Pick<Alias, FieldNameAlias.isPrimary>; export type TypeUpdateAlias = Pick<Alias, FieldNameAlias.isPrimary>

View file

@ -11,25 +11,25 @@
*/ */
export interface ApiToken { export interface ApiToken {
/** The id of the token, a short random ASCII string. Is unique */ /** 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 */ /** The {@link User} whose permissions the token has */
[FieldNameApiToken.userId]: number; [FieldNameApiToken.userId]: number
/** The user-defined label for the token, such as "CLI" */ /** The user-defined label for the token, such as "CLI" */
[FieldNameApiToken.label]: string; [FieldNameApiToken.label]: string
/** Hashed version of the token's secret */ /** Hashed version of the token's secret */
[FieldNameApiToken.secretHash]: string; [FieldNameApiToken.secretHash]: string
/** Expiry date of the token */ /** Expiry date of the token */
[FieldNameApiToken.validUntil]: Date; [FieldNameApiToken.validUntil]: string
/** Date when the API token was created */ /** 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 */ /** 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 { export enum FieldNameApiToken {
@ -42,7 +42,24 @@ export enum FieldNameApiToken {
lastUsedAt = 'last_used_at', lastUsedAt = 'last_used_at',
} }
export const TableApiToken = 'api_token'; export const TableApiToken = 'api_token'
export type TypeInsertApiToken = Omit<ApiToken, FieldNameApiToken.lastUsedAt>; type TypeApiTokenDate = Omit<
export type TypeUpdateApiToken = Pick<ApiToken, FieldNameApiToken.lastUsedAt>; 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
>

View file

@ -3,6 +3,8 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * 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} * The AuthorshipInfo holds the information from where to where one {@link User} has changed a {@link Note}
* *
@ -11,19 +13,19 @@
*/ */
export interface AuthorshipInfo { export interface AuthorshipInfo {
/** The id of the {@link Revision} this belongs to. */ /** The id of the {@link Revision} this belongs to. */
[FieldNameAuthorshipInfo.revisionUuid]: string; [FieldNameAuthorshipInfo.revisionUuid]: string
/** The id of the author of the edit. */ /** 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. */ /** 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. */ /** 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. */ /** The timestamp when the authorship entry was created. */
[FieldNameAuthorshipInfo.createdAt]: Date; [FieldNameAuthorshipInfo.createdAt]: string
} }
export enum FieldNameAuthorshipInfo { export enum FieldNameAuthorshipInfo {
@ -34,4 +36,12 @@ export enum FieldNameAuthorshipInfo {
createdAt = 'created_at', 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'

View file

@ -9,10 +9,10 @@
*/ */
export interface GroupUser { export interface GroupUser {
/** The id of the {@link Group} a {@link User} is part of */ /** The id of the {@link Group} a {@link User} is part of */
[FieldNameGroupUser.groupId]: number; [FieldNameGroupUser.groupId]: number
/** The id of the {@link User} */ /** The id of the {@link User} */
[FieldNameGroupUser.userId]: number; [FieldNameGroupUser.userId]: number
} }
export enum FieldNameGroupUser { export enum FieldNameGroupUser {
@ -20,4 +20,4 @@ export enum FieldNameGroupUser {
userId = 'user_id', userId = 'user_id',
} }
export const TableGroupUser = 'group_user'; export const TableGroupUser = 'group_user'

View file

@ -4,6 +4,11 @@
* SPDX-License-Identifier: AGPL-3.0-only * 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. * 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 * 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 { export interface Group {
/** The unique id for internal referencing */ /** The unique id for internal referencing */
[FieldNameGroup.id]: number; [FieldNameGroup.id]: number
/** The public identifier of the group (username for the group) */ /** The public identifier of the group (username for the group) */
[FieldNameGroup.name]: string; [FieldNameGroup.name]: string
/** The display name of the group */ /** The display name of the group */
[FieldNameGroup.displayName]: string; [FieldNameGroup.displayName]: string
/** Whether the group is one of the special groups */ /** Whether the group is one of the special groups */
[FieldNameGroup.isSpecial]: boolean; [FieldNameGroup.isSpecial]: boolean
} }
export enum FieldNameGroup { export enum FieldNameGroup {
@ -30,9 +35,9 @@ export enum FieldNameGroup {
isSpecial = 'is_special', isSpecial = 'is_special',
} }
export const TableGroup = 'group'; export const TableGroup = 'group'
export type TypeInsertGroup = Omit<Group, FieldNameGroup.id>; export type TypeInsertGroup = Omit<Group, FieldNameGroup.id>
export type TypeUpdateGroup = Pick< export type TypeUpdateGroup = Pick<
Group, Group,
FieldNameGroup.name | FieldNameGroup.displayName FieldNameGroup.name | FieldNameGroup.displayName
>; >

View file

@ -3,32 +3,39 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * 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 * An auth identity holds the information how a {@link User} can authenticate themselves using a certain auth provider
*/ */
export interface Identity { export interface Identity {
/** The id of the user */ /** The id of the user */
[FieldNameIdentity.userId]: number; [FieldNameIdentity.userId]: number
/** The type of the auth provider */ /** The type of the auth provider */
[FieldNameIdentity.providerType]: AuthProviderType; [FieldNameIdentity.providerType]: AuthProviderType
/** The identifier of the auth provider, e.g. gitlab */ /** The identifier of the auth provider, e.g. gitlab */
[FieldNameIdentity.providerIdentifier]: string | null; [FieldNameIdentity.providerIdentifier]: string | null
/** Timestamp when this identity was created */ /** Timestamp when this identity was created */
[FieldNameIdentity.createdAt]: Date; [FieldNameIdentity.createdAt]: string
/** Timestamp when this identity was last updated */ /** 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 */ /** 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 */ /** The hashed password for local identities or null for other auth providers */
[FieldNameIdentity.passwordHash]: string | null; [FieldNameIdentity.passwordHash]: string | null
} }
export enum FieldNameIdentity { export enum FieldNameIdentity {
@ -41,13 +48,21 @@ export enum FieldNameIdentity {
passwordHash = 'password_hash', 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< export type TypeInsertIdentity = Omit<
Identity, Identity,
FieldNameIdentity.createdAt | FieldNameIdentity.updatedAt FieldNameIdentity.createdAt | FieldNameIdentity.updatedAt
>; >
export type TypeUpdateIdentity = Pick< export type TypeUpdateIdentity = Pick<
Identity, TypeIdentityDate,
FieldNameIdentity.passwordHash | FieldNameIdentity.updatedAt FieldNameIdentity.passwordHash | FieldNameIdentity.updatedAt
>; >

View file

@ -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'

View file

@ -3,7 +3,14 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * 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, * 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 { export interface MediaUpload {
/** UUID (v7) identifying the media upload. Is public and unique */ /** 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 */ /** 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 */ /** The id of the {@link User} who uploaded the media file */
[FieldNameMediaUpload.userId]: number; [FieldNameMediaUpload.userId]: number
/** The name of the uploaded file */ /** The name of the uploaded file */
[FieldNameMediaUpload.fileName]: string; [FieldNameMediaUpload.fileName]: string
/** The backend where this upload is stored */ /** 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 */ /** 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 */ /** Timestamp when the file was uploaded */
[FieldNameMediaUpload.createdAt]: Date; [FieldNameMediaUpload.createdAt]: string
} }
export enum FieldNameMediaUpload { export enum FieldNameMediaUpload {
@ -43,13 +50,17 @@ export enum FieldNameMediaUpload {
createdAt = 'created_at', createdAt = 'created_at',
} }
export const TableMediaUpload = 'media_upload'; export const TableMediaUpload = 'media_upload'
type TypeMediaUploadDate = Omit<MediaUpload, FieldNameMediaUpload.createdAt> & {
[FieldNameMediaUpload.createdAt]: Date
}
export type TypeInsertMediaUpload = Omit< export type TypeInsertMediaUpload = Omit<
MediaUpload, TypeMediaUploadDate,
FieldNameMediaUpload.createdAt | FieldNameMediaUpload.uuid FieldNameMediaUpload.createdAt | FieldNameMediaUpload.uuid
>; >
export type TypeUpdateMediaUpload = Pick< export type TypeUpdateMediaUpload = Pick<
MediaUpload, TypeMediaUploadDate,
FieldNameMediaUpload.noteId FieldNameMediaUpload.noteId
>; >

View file

@ -8,13 +8,13 @@
*/ */
export interface NoteGroupPermission { export interface NoteGroupPermission {
/** The id of the {@link Group} to give the {@link Note} permission to. */ /** 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. */ /** 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. */ /** Whether the {@link Group} can edit the {@link Note} or not. */
[FieldNameNoteGroupPermission.canEdit]: boolean; [FieldNameNoteGroupPermission.canEdit]: boolean
} }
export enum FieldNameNoteGroupPermission { export enum FieldNameNoteGroupPermission {
@ -23,9 +23,9 @@ export enum FieldNameNoteGroupPermission {
canEdit = 'can_edit', canEdit = 'can_edit',
} }
export const TableNoteGroupPermission = 'note_group_permission'; export const TableNoteGroupPermission = 'note_group_permission'
export type TypeUpdateNoteGroupPermission = Pick< export type TypeUpdateNoteGroupPermission = Pick<
NoteGroupPermission, NoteGroupPermission,
FieldNameNoteGroupPermission.canEdit FieldNameNoteGroupPermission.canEdit
>; >

View file

@ -8,13 +8,13 @@
*/ */
export interface NoteUserPermission { export interface NoteUserPermission {
/** The id of the {@link User} to give the {@link Note} permission to. */ /** 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. */ /** 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. */ /** Whether the {@link User} can edit the {@link Note} or not. */
[FieldNameNoteUserPermission.canEdit]: boolean; [FieldNameNoteUserPermission.canEdit]: boolean
} }
export enum FieldNameNoteUserPermission { export enum FieldNameNoteUserPermission {
@ -23,9 +23,9 @@ export enum FieldNameNoteUserPermission {
canEdit = 'can_edit', canEdit = 'can_edit',
} }
export const TableNoteUserPermission = 'note_user_permission'; export const TableNoteUserPermission = 'note_user_permission'
export type TypeUpdateNoteUserPermission = Pick< export type TypeUpdateNoteUserPermission = Pick<
NoteUserPermission, NoteUserPermission,
FieldNameNoteUserPermission.canEdit FieldNameNoteUserPermission.canEdit
>; >

View file

@ -11,16 +11,16 @@
*/ */
export interface Note { export interface Note {
/** The unique id of the note for internal referencing */ /** The unique id of the note for internal referencing */
[FieldNameNote.id]: number; [FieldNameNote.id]: number
/** The {@link User} id of the note owner */ /** 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 */ /** 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 */ /** Timestamp when the note was created */
[FieldNameNote.createdAt]: Date; [FieldNameNote.createdAt]: string
} }
export enum FieldNameNote { export enum FieldNameNote {
@ -30,10 +30,14 @@ export enum FieldNameNote {
createdAt = 'created_at', createdAt = 'created_at',
} }
export const TableNote = 'note'; export const TableNote = 'note'
type TypeNoteDate = Omit<Note, FieldNameNote.createdAt> & {
[FieldNameNote.createdAt]: Date
}
export type TypeInsertNote = Omit< export type TypeInsertNote = Omit<
Note, TypeNoteDate,
FieldNameNote.createdAt | FieldNameNote.id FieldNameNote.createdAt | FieldNameNote.id
>; >
export type TypeUpdateNote = Pick<Note, FieldNameNote.ownerId>; export type TypeUpdateNote = Pick<TypeNoteDate, FieldNameNote.ownerId>

View file

@ -8,10 +8,10 @@
*/ */
export interface RevisionTag { export interface RevisionTag {
/** The id of {@link Revision} the {@link RevisionTag Tags} are asspcoated with. */ /** The id of {@link Revision} the {@link RevisionTag Tags} are asspcoated with. */
[FieldNameRevisionTag.revisionUuid]: string; [FieldNameRevisionTag.revisionUuid]: string
/** The {@link RevisionTag Tag} text. */ /** The {@link RevisionTag Tag} text. */
[FieldNameRevisionTag.tag]: string; [FieldNameRevisionTag.tag]: string
} }
export enum FieldNameRevisionTag { export enum FieldNameRevisionTag {
@ -19,4 +19,4 @@ export enum FieldNameRevisionTag {
tag = 'tag', tag = 'tag',
} }
export const TableRevisionTag = 'revision_tag'; export const TableRevisionTag = 'revision_tag'

View file

@ -3,38 +3,42 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * 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. * A revision represents the content of a {@link Note} at a specific point in time.
*/ */
export interface Revision { export interface Revision {
/** The unique id of the revision for internal referencing */ /** 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 */ /** 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 */ /** 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 */ /** The content of the note at this revision */
[FieldNameRevision.content]: string; [FieldNameRevision.content]: string
/** The stored Y.js state for realtime editing */ /** 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 */ /** Whether the note is a document or presentation at this revision */
[FieldNameRevision.noteType]: NoteType; [FieldNameRevision.noteType]: NoteType
/** The extracted note title from this revision */ /** The extracted note title from this revision */
[FieldNameRevision.title]: string; [FieldNameRevision.title]: string
/** The extracted description from this revision */ /** The extracted description from this revision */
[FieldNameRevision.description]: string; [FieldNameRevision.description]: string
/** Timestamp when this revision was created */ /** Timestamp when this revision was created */
[FieldNameRevision.createdAt]: Date; [FieldNameRevision.createdAt]: string
} }
export enum FieldNameRevision { export enum FieldNameRevision {
@ -49,6 +53,13 @@ export enum FieldNameRevision {
createdAt = 'created_at', createdAt = 'created_at',
} }
export const TableRevision = 'revision'; export const TableRevision = 'revision'
export type TypeInsertRevision = Omit<Revision, FieldNameRevision.createdAt>; type TypeRevisionDate = Omit<Revision, FieldNameRevision.createdAt> & {
[FieldNameRevision.createdAt]: Date
}
export type TypeInsertRevision = Omit<
TypeRevisionDate,
FieldNameRevision.createdAt
>

View file

@ -10,10 +10,10 @@
*/ */
export interface UserPinnedNote { export interface UserPinnedNote {
/** The id of the {@link User} */ /** The id of the {@link User} */
user_id: number; user_id: number
/** The id of the {@link Note} */ /** The id of the {@link Note} */
note_id: number; note_id: number
} }
export enum FieldNameUserPinnedNote { export enum FieldNameUserPinnedNote {
@ -21,4 +21,4 @@ export enum FieldNameUserPinnedNote {
noteId = 'note_id', noteId = 'note_id',
} }
export const TableUserPinnedNote = 'user_pinned_note'; export const TableUserPinnedNote = 'user_pinned_note'

View file

@ -17,28 +17,28 @@
*/ */
export interface User { export interface User {
/** The unique id of the user for internal referencing */ /** 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 */ /** 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 */ /** 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 */ /** The user's chosen display name */
[FieldNameUser.displayName]: string; [FieldNameUser.displayName]: string
/** Timestamp when the user was created */ /** Timestamp when the user was created */
[FieldNameUser.createdAt]: Date; [FieldNameUser.createdAt]: string
/** URL to the user's profile picture if present */ /** URL to the user's profile picture if present */
[FieldNameUser.photoUrl]: string | null; [FieldNameUser.photoUrl]: string | null
/** The user's email address if present */ /** 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 */ /** The index which author style (e.g. color) should be used for this user */
[FieldNameUser.authorStyle]: number; [FieldNameUser.authorStyle]: number
} }
export const enum FieldNameUser { export const enum FieldNameUser {
@ -52,16 +52,20 @@ export const enum FieldNameUser {
authorStyle = 'author_style', authorStyle = 'author_style',
} }
export const TableUser = 'user'; export const TableUser = 'user'
type TypeUserDate = Omit<User, FieldNameUser.createdAt> & {
[FieldNameUser.createdAt]: Date
}
export type TypeInsertUser = Omit< export type TypeInsertUser = Omit<
User, TypeUserDate,
FieldNameUser.id | FieldNameUser.createdAt FieldNameUser.id | FieldNameUser.createdAt
>; >
export type TypeUpdateUser = Pick< export type TypeUpdateUser = Pick<
User, User,
| FieldNameUser.displayName | FieldNameUser.displayName
| FieldNameUser.photoUrl | FieldNameUser.photoUrl
| FieldNameUser.email | FieldNameUser.email
| FieldNameUser.authorStyle | FieldNameUser.authorStyle
>; >

View file

@ -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"]
}

View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2023 Tilman Vatteroth
SPDX-License-Identifier: CC0-1.0

View file

@ -0,0 +1,10 @@
{
"extends" : "./tsconfig.base.json",
"compilerOptions": {
"module": "CommonJS",
"target": "ES2015",
"outDir": "dist/cjs",
"declarationDir": "dist/cjs",
"moduleResolution": "node"
}
}

View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2023 Tilman Vatteroth
SPDX-License-Identifier: CC0-1.0

View file

@ -0,0 +1,4 @@
{
"extends" : "./tsconfig.esm.json",
"exclude": ["./dist"]
}

View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2023 Tilman Vatteroth
SPDX-License-Identifier: CC0-1.0

View file

@ -6,6 +6,7 @@
"backend", "backend",
"frontend", "frontend",
"commons", "commons",
"database",
"dev-reverse-proxy", "dev-reverse-proxy",
"docs", "docs",
"html-to-react", "html-to-react",

View file

@ -22,6 +22,14 @@
"dist/**" "dist/**"
] ]
}, },
"@hedgedoc/database#build": {
"dependsOn": [
"^build"
],
"outputs": [
"dist/**"
]
},
"@hedgedoc/markdown-it-plugins#build": { "@hedgedoc/markdown-it-plugins#build": {
"dependsOn": [ "dependsOn": [
"^build" "^build"

View file

@ -2824,6 +2824,21 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft 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": "@hedgedoc/dev-reverse-proxy@workspace:dev-reverse-proxy":
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@hedgedoc/dev-reverse-proxy@workspace:dev-reverse-proxy" resolution: "@hedgedoc/dev-reverse-proxy@workspace:dev-reverse-proxy"