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
*/
import { AliasDto } from '@hedgedoc/commons';
import {
Alias,
FieldNameAlias,
FieldNameNote,
Note,
TableAlias,
TypeInsertAlias,
} from '@hedgedoc/database';
import { Inject, Injectable } from '@nestjs/common';
import base32Encode from 'base32-encode';
import { randomBytes } from 'crypto';
@ -11,14 +19,6 @@ import { Knex } from 'knex';
import { InjectConnection } from 'nest-knexjs';
import noteConfiguration, { NoteConfig } from '../config/note.config';
import {
Alias,
FieldNameAlias,
FieldNameNote,
Note,
TableAlias,
} from '../database/types';
import { TypeInsertAlias } from '../database/types/alias';
import {
AlreadyInDBError,
ForbiddenIdError,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,18 +8,19 @@ import {
PendingUserConfirmationDto,
PendingUserInfoDto,
} from '@hedgedoc/commons';
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import { InjectConnection } from 'nest-knexjs';
import AuthConfiguration, { AuthConfig } from '../config/auth.config';
import {
FieldNameIdentity,
FieldNameUser,
Identity,
TableIdentity,
TypeInsertIdentity,
User,
} from '../database/types';
} from '@hedgedoc/database';
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import { InjectConnection } from 'nest-knexjs';
import AuthConfiguration, { AuthConfig } from '../config/auth.config';
import { NotInDBError } from '../errors/errors';
import { ConsoleLoggerService } from '../logger/console-logger.service';
import { UsersService } from '../users/users.service';
@ -52,7 +53,7 @@ export class IdentityService {
/**
* Retrieve an identity from the information received from an auth provider.
*
* @param userId - the userId of the wanted identity
* @param authProviderUserId - the userId of the wanted identity
* @param authProviderType - the providerType of the wanted identity
* @param authProviderIdentifier - optional name of the provider if multiple exist
* @return
@ -96,15 +97,12 @@ export class IdentityService {
transaction?: Knex,
): Promise<void> {
const dbActor = transaction ?? this.knex;
const date = new Date();
const identity: Identity = {
const identity: TypeInsertIdentity = {
[FieldNameIdentity.userId]: userId,
[FieldNameIdentity.providerType]: authProviderType,
[FieldNameIdentity.providerIdentifier]: authProviderIdentifier,
[FieldNameIdentity.providerUserId]: authProviderUserId,
[FieldNameIdentity.passwordHash]: passwordHash ?? null,
[FieldNameIdentity.createdAt]: date,
[FieldNameIdentity.updatedAt]: date,
};
await dbActor(TableIdentity).insert(identity);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,14 +9,6 @@ import {
NotePermissionsDto,
SpecialGroup,
} from '@hedgedoc/commons';
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Knex } from 'knex';
import { InjectConnection } from 'nest-knexjs';
import { AliasService } from '../alias/alias.service';
import { DefaultAccessLevel } from '../config/default-access-level.enum';
import noteConfiguration, { NoteConfig } from '../config/note.config';
import {
FieldNameAlias,
FieldNameGroup,
@ -36,7 +28,15 @@ import {
TableNoteUserPermission,
TableUser,
User,
} from '../database/types';
} from '@hedgedoc/database';
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Knex } from 'knex';
import { InjectConnection } from 'nest-knexjs';
import { AliasService } from '../alias/alias.service';
import { DefaultAccessLevel } from '../config/default-access-level.enum';
import noteConfiguration, { NoteConfig } from '../config/note.config';
import {
ForbiddenIdError,
GenericDBError,
@ -141,6 +141,7 @@ export class NoteService {
await this.revisionsService.createRevision(
noteId,
noteContent,
true,
transaction,
);
@ -162,6 +163,7 @@ export class NoteService {
if (everyoneAccessLevel !== DefaultAccessLevel.NONE) {
const everyoneAccessGroupId = await this.groupsService.getGroupIdByName(
SpecialGroup.EVERYONE,
transaction,
);
await this.permissionService.setGroupPermission(
noteId,
@ -173,7 +175,10 @@ export class NoteService {
if (loggedInUsersAccessLevel !== DefaultAccessLevel.NONE) {
const loggedInUsersAccessGroupId =
await this.groupsService.getGroupIdByName(SpecialGroup.LOGGED_IN);
await this.groupsService.getGroupIdByName(
SpecialGroup.LOGGED_IN,
transaction,
);
await this.permissionService.setGroupPermission(
noteId,
loggedInUsersAccessGroupId,
@ -415,6 +420,12 @@ export class NoteService {
'toNoteMetadataDto',
);
}
const createdAtString = note[FieldNameNote.createdAt];
const version = note[FieldNameNote.version];
this.logger.debug(`createdAt: ${createdAtString}`);
this.logger.debug(`createversion: ${version}`);
const createdAt = new Date(createdAtString).toISOString();
const latestRevision = await this.revisionsService.getLatestRevision(
noteId,
transaction,
@ -423,28 +434,40 @@ export class NoteService {
latestRevision[FieldNameRevision.uuid],
transaction,
);
const permissions = await this.toNotePermissionsDto(noteId, transaction);
const updateUsers = await this.revisionsService.getRevisionUserInfo(
latestRevision[FieldNameRevision.uuid],
transaction,
);
updateUsers.users.sort(
(userA, userB) => userB.createdAt.getTime() - userA.createdAt.getTime(),
);
const lastEdit = updateUsers.users[0];
const editedBy = updateUsers.users.map((user) => user.username);
const permissions = await this.toNotePermissionsDto(noteId, transaction);
updateUsers.users.sort();
let lastUpdatedBy;
let editedBy;
let updatedAt;
if (updateUsers.users.length > 0) {
const lastEdit = updateUsers.users[0];
lastUpdatedBy = lastEdit.username;
editedBy = updateUsers.users.map((user) => user.username);
updatedAt = new Date(lastEdit.createdAt).toISOString();
} else {
lastUpdatedBy = permissions.owner;
editedBy = permissions.owner ? [permissions.owner] : [];
updatedAt = createdAt;
}
return {
aliases: aliases.map((alias) => alias[FieldNameAlias.alias]),
primaryAlias: primaryAlias[FieldNameAlias.alias],
title: latestRevision.title,
description: latestRevision.description,
tags: tags,
createdAt: note[FieldNameNote.createdAt].toISOString(),
editedBy: editedBy,
permissions: permissions,
version: note[FieldNameNote.version],
updatedAt: lastEdit.createdAt.toISOString(),
lastUpdatedBy: lastEdit.username,
tags,
createdAt,
editedBy,
permissions,
version,
updatedAt,
lastUpdatedBy,
};
}

View file

@ -8,12 +8,6 @@ import {
PermissionLevel,
SpecialGroup,
} from '@hedgedoc/commons';
import { Inject, Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Knex } from 'knex';
import { InjectConnection } from 'nest-knexjs';
import noteConfiguration, { NoteConfig } from '../config/note.config';
import {
FieldNameGroup,
FieldNameGroupUser,
@ -29,7 +23,13 @@ import {
TableNoteGroupPermission,
TableNoteUserPermission,
TableUser,
} from '../database/types';
} from '@hedgedoc/database';
import { Inject, Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Knex } from 'knex';
import { InjectConnection } from 'nest-knexjs';
import noteConfiguration, { NoteConfig } from '../config/note.config';
import { GenericDBError, NotInDBError } from '../errors/errors';
import { NoteEvent, NoteEventMap } from '../events';
import { ConsoleLoggerService } from '../logger/console-logger.service';
@ -334,6 +334,7 @@ export class PermissionService {
* @param noteId - the if of the note
* @param groupId - the name of the group for which the permission should be set
* @param canEdit - specifies if the group can edit the note
* @param transaction The optional transaction for the database
*/
async setGroupPermission(
noteId: number,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,3 +4,4 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
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 {
/** The alias as defined by the user. Is unique. */
[FieldNameAlias.alias]: string;
[FieldNameAlias.alias]: string
/** The id of the associated {@link Note}. */
[FieldNameAlias.noteId]: number;
[FieldNameAlias.noteId]: number
/** Whether the alias is the primary one for the note. */
[FieldNameAlias.isPrimary]: boolean;
[FieldNameAlias.isPrimary]: boolean
}
export enum FieldNameAlias {
@ -27,7 +27,7 @@ export enum FieldNameAlias {
isPrimary = 'is_primary',
}
export const TableAlias = 'alias';
export const TableAlias = 'alias'
export type TypeInsertAlias = Alias;
export type TypeUpdateAlias = Pick<Alias, FieldNameAlias.isPrimary>;
export type TypeInsertAlias = Alias
export type TypeUpdateAlias = Pick<Alias, FieldNameAlias.isPrimary>

View file

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

View file

@ -3,6 +3,8 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { FieldNameApiToken } from './api-token'
/**
* The AuthorshipInfo holds the information from where to where one {@link User} has changed a {@link Note}
*
@ -11,19 +13,19 @@
*/
export interface AuthorshipInfo {
/** The id of the {@link Revision} this belongs to. */
[FieldNameAuthorshipInfo.revisionUuid]: string;
[FieldNameAuthorshipInfo.revisionUuid]: string
/** The id of the author of the edit. */
[FieldNameAuthorshipInfo.authorId]: number;
[FieldNameAuthorshipInfo.authorId]: number
/** The start position of the change in the note as a positive index. */
[FieldNameAuthorshipInfo.startPosition]: number;
[FieldNameAuthorshipInfo.startPosition]: number
/** The end position of the change in the note as a positive index. */
[FieldNameAuthorshipInfo.endPosition]: number;
[FieldNameAuthorshipInfo.endPosition]: number
/** The timestamp when the authorship entry was created. */
[FieldNameAuthorshipInfo.createdAt]: Date;
[FieldNameAuthorshipInfo.createdAt]: string
}
export enum FieldNameAuthorshipInfo {
@ -34,4 +36,12 @@ export enum FieldNameAuthorshipInfo {
createdAt = 'created_at',
}
export const TableAuthorshipInfo = 'authorship_info';
type TypeAuthorshipInfoDate = Omit<
AuthorshipInfo,
FieldNameAuthorshipInfo.createdAt
> & {
[FieldNameAuthorshipInfo.createdAt]: Date
}
export type TypeInsertAuthorshipInfo = TypeAuthorshipInfoDate
export const TableAuthorshipInfo = 'authorship_info'

View file

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

View file

@ -4,6 +4,11 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
export enum SpecialGroup {
EVERYONE = '_EVERYONE',
LOGGED_IN = '_LOGGED_IN',
}
/**
* A group represents one or multiple {@link User}s and can be used for permission management.
* There are special groups that are created by the system and cannot be deleted, these include the set of all
@ -11,16 +16,16 @@
*/
export interface Group {
/** The unique id for internal referencing */
[FieldNameGroup.id]: number;
[FieldNameGroup.id]: number
/** The public identifier of the group (username for the group) */
[FieldNameGroup.name]: string;
[FieldNameGroup.name]: string
/** The display name of the group */
[FieldNameGroup.displayName]: string;
[FieldNameGroup.displayName]: string
/** Whether the group is one of the special groups */
[FieldNameGroup.isSpecial]: boolean;
[FieldNameGroup.isSpecial]: boolean
}
export enum FieldNameGroup {
@ -30,9 +35,9 @@ export enum FieldNameGroup {
isSpecial = 'is_special',
}
export const TableGroup = 'group';
export type TypeInsertGroup = Omit<Group, FieldNameGroup.id>;
export const TableGroup = 'group'
export type TypeInsertGroup = Omit<Group, FieldNameGroup.id>
export type TypeUpdateGroup = Pick<
Group,
FieldNameGroup.name | FieldNameGroup.displayName
>;
>

View file

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

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
*/
import { BackendType } from '../../media/backends/backend-type.enum';
export enum MediaBackendType {
FILESYSTEM = 'filesystem',
S3 = 's3',
IMGUR = 'imgur',
AZURE = 'azure',
WEBDAV = 'webdav',
}
/**
* A media upload object represents an uploaded file. While the file itself is stored in the configured storage backend,
@ -12,25 +19,25 @@ import { BackendType } from '../../media/backends/backend-type.enum';
*/
export interface MediaUpload {
/** UUID (v7) identifying the media upload. Is public and unique */
[FieldNameMediaUpload.uuid]: string;
[FieldNameMediaUpload.uuid]: string
/** The id of the attached {@link Note} or null if the media upload was detached from a note */
[FieldNameMediaUpload.noteId]: number | null;
[FieldNameMediaUpload.noteId]: number | null
/** The id of the {@link User} who uploaded the media file */
[FieldNameMediaUpload.userId]: number;
[FieldNameMediaUpload.userId]: number
/** The name of the uploaded file */
[FieldNameMediaUpload.fileName]: string;
[FieldNameMediaUpload.fileName]: string
/** The backend where this upload is stored */
[FieldNameMediaUpload.backendType]: BackendType;
[FieldNameMediaUpload.backendType]: MediaBackendType
/** Additional data required by the backend storage to identify the uploaded file */
[FieldNameMediaUpload.backendData]: string | null;
[FieldNameMediaUpload.backendData]: string | null
/** Timestamp when the file was uploaded */
[FieldNameMediaUpload.createdAt]: Date;
[FieldNameMediaUpload.createdAt]: string
}
export enum FieldNameMediaUpload {
@ -43,13 +50,17 @@ export enum FieldNameMediaUpload {
createdAt = 'created_at',
}
export const TableMediaUpload = 'media_upload';
export const TableMediaUpload = 'media_upload'
type TypeMediaUploadDate = Omit<MediaUpload, FieldNameMediaUpload.createdAt> & {
[FieldNameMediaUpload.createdAt]: Date
}
export type TypeInsertMediaUpload = Omit<
MediaUpload,
TypeMediaUploadDate,
FieldNameMediaUpload.createdAt | FieldNameMediaUpload.uuid
>;
>
export type TypeUpdateMediaUpload = Pick<
MediaUpload,
TypeMediaUploadDate,
FieldNameMediaUpload.noteId
>;
>

View file

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

View file

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

View file

@ -11,16 +11,16 @@
*/
export interface Note {
/** The unique id of the note for internal referencing */
[FieldNameNote.id]: number;
[FieldNameNote.id]: number
/** The {@link User} id of the note owner */
[FieldNameNote.ownerId]: number;
[FieldNameNote.ownerId]: number
/** The HedgeDoc major version this note was created in. This is used to migrate certain features from HD1 to HD2 */
[FieldNameNote.version]: number;
[FieldNameNote.version]: number
/** Timestamp when the note was created */
[FieldNameNote.createdAt]: Date;
[FieldNameNote.createdAt]: string
}
export enum FieldNameNote {
@ -30,10 +30,14 @@ export enum FieldNameNote {
createdAt = 'created_at',
}
export const TableNote = 'note';
export const TableNote = 'note'
type TypeNoteDate = Omit<Note, FieldNameNote.createdAt> & {
[FieldNameNote.createdAt]: Date
}
export type TypeInsertNote = Omit<
Note,
TypeNoteDate,
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 {
/** The id of {@link Revision} the {@link RevisionTag Tags} are asspcoated with. */
[FieldNameRevisionTag.revisionUuid]: string;
[FieldNameRevisionTag.revisionUuid]: string
/** The {@link RevisionTag Tag} text. */
[FieldNameRevisionTag.tag]: string;
[FieldNameRevisionTag.tag]: string
}
export enum FieldNameRevisionTag {
@ -19,4 +19,4 @@ export enum FieldNameRevisionTag {
tag = 'tag',
}
export const TableRevisionTag = 'revision_tag';
export const TableRevisionTag = 'revision_tag'

View file

@ -3,38 +3,42 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { NoteType } from '@hedgedoc/commons';
export enum NoteType {
DOCUMENT = 'document',
SLIDE = 'slide',
}
/**
* A revision represents the content of a {@link Note} at a specific point in time.
*/
export interface Revision {
/** The unique id of the revision for internal referencing */
[FieldNameRevision.uuid]: string;
[FieldNameRevision.uuid]: string
/** The id of the note that this revision belongs to */
[FieldNameRevision.noteId]: number;
[FieldNameRevision.noteId]: number
/** The changes between this revision and the previous one in patch file format */
[FieldNameRevision.patch]: string;
[FieldNameRevision.patch]: string
/** The content of the note at this revision */
[FieldNameRevision.content]: string;
[FieldNameRevision.content]: string
/** The stored Y.js state for realtime editing */
[FieldNameRevision.yjsStateVector]: null | ArrayBuffer;
[FieldNameRevision.yjsStateVector]: null | ArrayBuffer
/** Whether the note is a document or presentation at this revision */
[FieldNameRevision.noteType]: NoteType;
[FieldNameRevision.noteType]: NoteType
/** The extracted note title from this revision */
[FieldNameRevision.title]: string;
[FieldNameRevision.title]: string
/** The extracted description from this revision */
[FieldNameRevision.description]: string;
[FieldNameRevision.description]: string
/** Timestamp when this revision was created */
[FieldNameRevision.createdAt]: Date;
[FieldNameRevision.createdAt]: string
}
export enum FieldNameRevision {
@ -49,6 +53,13 @@ export enum FieldNameRevision {
createdAt = 'created_at',
}
export const TableRevision = 'revision';
export const TableRevision = 'revision'
export type TypeInsertRevision = Omit<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 {
/** The id of the {@link User} */
user_id: number;
user_id: number
/** The id of the {@link Note} */
note_id: number;
note_id: number
}
export enum FieldNameUserPinnedNote {
@ -21,4 +21,4 @@ export enum FieldNameUserPinnedNote {
noteId = 'note_id',
}
export const TableUserPinnedNote = 'user_pinned_note';
export const TableUserPinnedNote = 'user_pinned_note'

View file

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

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",
"frontend",
"commons",
"database",
"dev-reverse-proxy",
"docs",
"html-to-react",

View file

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

View file

@ -2824,6 +2824,21 @@ __metadata:
languageName: unknown
linkType: soft
"@hedgedoc/database@workspace:database":
version: 0.0.0-use.local
resolution: "@hedgedoc/database@workspace:database"
dependencies:
"@typescript-eslint/eslint-plugin": "npm:8.14.0"
"@typescript-eslint/parser": "npm:8.14.0"
eslint: "npm:8.57.1"
eslint-config-prettier: "npm:9.1.0"
eslint-plugin-jest: "npm:28.9.0"
eslint-plugin-prettier: "npm:5.2.3"
prettier: "npm:3.3.3"
typescript: "npm:5.6.3"
languageName: unknown
linkType: soft
"@hedgedoc/dev-reverse-proxy@workspace:dev-reverse-proxy":
version: 0.0.0-use.local
resolution: "@hedgedoc/dev-reverse-proxy@workspace:dev-reverse-proxy"