mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-19 09:45:37 -04:00
feat(knex): initial knexjs migration
Co-authored-by: Philip Molares <philip.molares@udo.edu> Signed-off-by: Erik Michelson <github@erik.michelson.eu> Signed-off-by: Philip Molares <philip.molares@udo.edu>
This commit is contained in:
parent
a9183e82bf
commit
b696c1e661
24 changed files with 1066 additions and 185 deletions
|
@ -38,14 +38,25 @@ module.exports = {
|
|||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['src/database/**'],
|
||||
rules: {
|
||||
'@typescript-eslint/naming-convention': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
'@typescript-eslint',
|
||||
'jest',
|
||||
'eslint-plugin-local-rules',
|
||||
'@darraghor/nestjs-typed',
|
||||
],
|
||||
plugins: ['@typescript-eslint', 'jest', 'eslint-plugin-local-rules','@darraghor/nestjs-typed'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:@typescript-eslint/recommended-requiring-type-checking',
|
||||
'plugin:prettier/recommended',
|
||||
'plugin:@darraghor/nestjs-typed/recommended'
|
||||
'plugin:@darraghor/nestjs-typed/recommended',
|
||||
],
|
||||
root: true,
|
||||
env: {
|
||||
|
|
21
backend/knexfile.ts
Normal file
21
backend/knexfile.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { Knex } from 'knex';
|
||||
|
||||
/** This is used for the Knex CLI to create migrations during development */
|
||||
const config: { [key: string]: Knex.Config } = {
|
||||
development: {
|
||||
client: 'better-sqlite3',
|
||||
connection: {
|
||||
filename: './hedgedoc.sqlite',
|
||||
},
|
||||
migrations: {
|
||||
directory: './src/database/migrations',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = config;
|
|
@ -21,8 +21,7 @@
|
|||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config jest-e2e.json && rimraf test_uploads*",
|
||||
"test:e2e:ci": "jest --config jest-e2e.json --coverage && rimraf test_uploads*",
|
||||
"seed": "ts-node src/seed.ts",
|
||||
"typeorm": "typeorm-ts-node-commonjs -d src/ormconfig.ts"
|
||||
"knex": "tsx ../node_modules/.bin/knex --migrations-directory src/database/migrations --knexfile ./knexfile.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/storage-blob": "12.25.0",
|
||||
|
@ -108,8 +107,8 @@
|
|||
"supertest": "6.3.4",
|
||||
"ts-jest": "29.2.5",
|
||||
"ts-mockery": "1.2.0",
|
||||
"ts-node": "11.0.0-beta.1",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "5.6.3"
|
||||
},
|
||||
"jest": {
|
||||
|
|
|
@ -76,6 +76,9 @@ const routes: Routes = [
|
|||
deprecate: knexLoggerService.deprecate.bind(knexLoggerService),
|
||||
debug: knexLoggerService.debug.bind(knexLoggerService),
|
||||
},
|
||||
migrations: {
|
||||
directory: 'src/database/migrations/',
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { ProviderType } from '../auth/provider-type.enum';
|
||||
|
||||
/**
|
||||
* An auth identity holds the information how a {@link User} can authenticate themself using a certain auth provider
|
||||
*/
|
||||
export interface Identity {
|
||||
/** The id of the user */
|
||||
userId: number;
|
||||
|
||||
/** The type of the auth provider */
|
||||
providerType: ProviderType;
|
||||
|
||||
/** The identifier of the auth provider, e.g. gitlab */
|
||||
providerIdentifier: string | null;
|
||||
|
||||
/** Timestamp when this identity was created */
|
||||
createdAt: Date;
|
||||
|
||||
/** Timestamp when this identity was last updated */
|
||||
updatedAt: Date;
|
||||
|
||||
/** The remote id of the user at the auth provider or null for local identities */
|
||||
providerUserId: string | null;
|
||||
|
||||
/** The hashed password for local identities or null for other auth providers */
|
||||
passwordHash: string | null;
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { Knex } from 'knex';
|
||||
|
||||
import { Alias } from './alias';
|
||||
import { ApiToken } from './api-token';
|
||||
import { AuthorshipInfo } from './authorship-info';
|
||||
import { Group } from './group';
|
||||
import { GroupUser } from './group-user';
|
||||
import { Identity } from './identity';
|
||||
import { MediaUpload } from './media-upload';
|
||||
import { Note } from './note';
|
||||
import { NoteGroupPermission } from './note-group-permission';
|
||||
import { NoteUserPermission } from './note-user-permission';
|
||||
import { Revision } from './revision';
|
||||
import { RevisionTag } from './revision-tag';
|
||||
import { User } from './user';
|
||||
import { UserPinnedNote } from './user-pinned-note';
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
declare module 'knex/types/tables' {
|
||||
interface Tables {
|
||||
alias_composite: Knex.CompositeTableType<
|
||||
Alias,
|
||||
Alias,
|
||||
Pick<Alias, 'isPrimary'>
|
||||
>;
|
||||
api_token_composite: Knex.CompositeTableType<
|
||||
ApiToken,
|
||||
Omit<ApiToken, 'createdAt' | 'lastUsedAt'>,
|
||||
Pick<ApiToken, 'lastUsedAt'>
|
||||
>;
|
||||
authorship_info_composite: Knex.CompositeTableType<
|
||||
AuthorshipInfo,
|
||||
Omit<AuthorshipInfo, 'createdAt'>
|
||||
>;
|
||||
group_composite: Knex.CompositeTableType<
|
||||
Group,
|
||||
Omit<Group, 'id'>,
|
||||
Pick<Group, 'name' | 'displayName'>
|
||||
>;
|
||||
group_user_composite: Knex.CompositeTableType<GroupUser>;
|
||||
identity_composite: Knex.CompositeTableType<
|
||||
Identity,
|
||||
Omit<Identity, 'createdAt'>,
|
||||
Pick<Identity, 'passwordHash' | 'updatedAt'>
|
||||
>;
|
||||
media_upload_composite: Knex.CompositeTableType<
|
||||
MediaUpload,
|
||||
Omit<MediaUpload, 'createdAt' | 'uuid'>,
|
||||
Pick<MediaUpload, 'noteId'>
|
||||
>;
|
||||
note_composite: Knex.CompositeTableType<
|
||||
Note,
|
||||
Omit<Note, 'createdAt' | 'id'>,
|
||||
Pick<Note, 'ownerId'>
|
||||
>;
|
||||
note_group_permission_composite: Knex.CompositeTableType<
|
||||
NoteGroupPermission,
|
||||
NoteGroupPermission,
|
||||
Pick<NoteGroupPermission, 'canEdit'>
|
||||
>;
|
||||
note_user_permission_composite: Knex.CompositeTableType<
|
||||
NoteUserPermission,
|
||||
NoteUserPermission,
|
||||
Pick<NoteUserPermission, 'canEdit'>
|
||||
>;
|
||||
revision_composite: Knex.CompositeTableType<
|
||||
Revision,
|
||||
Omit<Revision, 'createdAt' | 'id'>
|
||||
>;
|
||||
revision_tag_composite: Knex.CompositeTableType<RevisionTag>;
|
||||
user_composite: Knex.CompositeTableType<
|
||||
User,
|
||||
Omit<User, 'id' | 'createdAt'>,
|
||||
Pick<User, 'displayName' | 'photoUrl' | 'email' | 'authorStyle'>
|
||||
>;
|
||||
user_pinned_note_composite: Knex.CompositeTableType<UserPinnedNote>;
|
||||
}
|
||||
}
|
351
backend/src/database/migrations/20250312211152_initial.ts
Normal file
351
backend/src/database/migrations/20250312211152_initial.ts
Normal file
|
@ -0,0 +1,351 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { NoteType } from '@hedgedoc/commons';
|
||||
import type { Knex } from 'knex';
|
||||
|
||||
import { ProviderType } from '../../auth/provider-type.enum';
|
||||
import { SpecialGroup } from '../../groups/groups.special';
|
||||
import { BackendType } from '../../media/backends/backend-type.enum';
|
||||
import {
|
||||
FieldNameAlias,
|
||||
FieldNameApiToken,
|
||||
FieldNameAuthorshipInfo,
|
||||
FieldNameGroup,
|
||||
FieldNameGroupUser,
|
||||
FieldNameIdentity,
|
||||
FieldNameMediaUpload,
|
||||
FieldNameNote,
|
||||
FieldNameNoteGroupPermission,
|
||||
FieldNameNoteUserPermission,
|
||||
FieldNameRevision,
|
||||
FieldNameRevisionTag,
|
||||
FieldNameUser,
|
||||
FieldNameUserPinnedNote,
|
||||
TableAlias,
|
||||
TableApiToken,
|
||||
TableAuthorshipInfo,
|
||||
TableGroup,
|
||||
TableGroupUser,
|
||||
TableIdentity,
|
||||
TableMediaUpload,
|
||||
TableNote,
|
||||
TableNoteGroupPermission,
|
||||
TableNoteUserPermission,
|
||||
TableRevision,
|
||||
TableRevisionTag,
|
||||
TableUser,
|
||||
TableUserPinnedNote,
|
||||
} from '../types';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
// Create user table first as it's referenced by other tables
|
||||
await knex.schema.createTable(TableUser, (table) => {
|
||||
table.increments(FieldNameUser.id).primary();
|
||||
table.string(FieldNameUser.username).nullable().unique();
|
||||
table.string(FieldNameUser.displayName).nullable();
|
||||
table.string(FieldNameUser.photoUrl).nullable();
|
||||
table.string(FieldNameUser.email).nullable();
|
||||
table.integer(FieldNameUser.authorStyle).notNullable();
|
||||
table.uuid(FieldNameUser.guestUuid).nullable().unique();
|
||||
table.timestamp(FieldNameUser.createdAt).defaultTo(knex.fn.now());
|
||||
});
|
||||
|
||||
// Create group table
|
||||
await knex.schema.createTable(TableGroup, (table) => {
|
||||
table.increments(FieldNameGroup.id).primary();
|
||||
table.string(FieldNameGroup.name).notNullable();
|
||||
table.string(FieldNameGroup.displayName).notNullable();
|
||||
table.boolean(FieldNameGroup.isSpecial).notNullable().defaultTo(false);
|
||||
});
|
||||
|
||||
// Create special groups _EVERYONE and _LOGGED_IN
|
||||
await knex(TableGroup).insert([
|
||||
{
|
||||
name: SpecialGroup.EVERYONE,
|
||||
display_name: SpecialGroup.EVERYONE,
|
||||
is_special: true,
|
||||
},
|
||||
{
|
||||
name: SpecialGroup.LOGGED_IN,
|
||||
display_name: SpecialGroup.EVERYONE,
|
||||
is_special: true,
|
||||
},
|
||||
]);
|
||||
|
||||
// Create note table
|
||||
await knex.schema.createTable(TableNote, (table) => {
|
||||
table.increments(FieldNameNote.id).primary();
|
||||
table.integer(FieldNameNote.version).notNullable().defaultTo(2);
|
||||
table.timestamp(FieldNameNote.createdAt).defaultTo(knex.fn.now());
|
||||
table
|
||||
.integer(FieldNameNote.ownerId)
|
||||
.unsigned()
|
||||
.notNullable()
|
||||
.references(FieldNameUser.id)
|
||||
.inTable(TableUser);
|
||||
});
|
||||
|
||||
// Create alias table
|
||||
await knex.schema.createTable(TableAlias, (table) => {
|
||||
table.comment(
|
||||
'Stores aliases of notes, only on alias per note can be is_primary == true, all other need to have is_primary == null ',
|
||||
);
|
||||
table.string(FieldNameAlias.alias).primary();
|
||||
table
|
||||
.integer(FieldNameAlias.noteId)
|
||||
.unsigned()
|
||||
.notNullable()
|
||||
.references(FieldNameNote.id)
|
||||
.inTable(TableNote);
|
||||
table.boolean(FieldNameAlias.isPrimary).nullable();
|
||||
table.unique([FieldNameAlias.noteId, FieldNameAlias.isPrimary], {
|
||||
indexName: 'only_one_note_can_be_primary',
|
||||
useConstraint: true,
|
||||
});
|
||||
});
|
||||
|
||||
// Create api_token table
|
||||
await knex.schema.createTable(TableApiToken, (table) => {
|
||||
table.string(FieldNameApiToken.id).primary();
|
||||
table
|
||||
.integer(FieldNameApiToken.userId)
|
||||
.unsigned()
|
||||
.notNullable()
|
||||
.references(FieldNameUser.id)
|
||||
.inTable(TableUser);
|
||||
table.string(FieldNameApiToken.label).notNullable();
|
||||
table.string(FieldNameApiToken.secretHash).notNullable();
|
||||
table.timestamp(FieldNameApiToken.validUntil).notNullable();
|
||||
table.timestamp(FieldNameApiToken.lastUsedAt).nullable();
|
||||
});
|
||||
|
||||
// Create identity table
|
||||
await knex.schema.createTable(TableIdentity, (table) => {
|
||||
table
|
||||
.integer(FieldNameIdentity.userId)
|
||||
.unsigned()
|
||||
.notNullable()
|
||||
.references(FieldNameUser.id)
|
||||
.inTable(TableUser);
|
||||
table.enu(
|
||||
FieldNameIdentity.providerType,
|
||||
[ProviderType.LDAP, ProviderType.LOCAL, ProviderType.OIDC], // ProviderType.GUEST is not relevant for the DB
|
||||
{
|
||||
useNative: true,
|
||||
enumName: FieldNameIdentity.providerType,
|
||||
},
|
||||
);
|
||||
table.string(FieldNameIdentity.providerIdentifier).nullable();
|
||||
table.string(FieldNameIdentity.providerUserId).nullable();
|
||||
table.string(FieldNameIdentity.passwordHash).nullable();
|
||||
table.timestamp(FieldNameIdentity.createdAt).defaultTo(knex.fn.now());
|
||||
table.timestamp(FieldNameIdentity.updatedAt).defaultTo(knex.fn.now());
|
||||
table.unique(
|
||||
[
|
||||
FieldNameIdentity.userId,
|
||||
FieldNameIdentity.providerType,
|
||||
FieldNameIdentity.providerIdentifier,
|
||||
],
|
||||
{
|
||||
indexName: 'each_user_has_only_one_account_per_provider',
|
||||
useConstraint: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// Create group_user join table
|
||||
await knex.schema.createTable(TableGroupUser, (table) => {
|
||||
table
|
||||
.integer(FieldNameGroupUser.userId)
|
||||
.unsigned()
|
||||
.notNullable()
|
||||
.references(FieldNameUser.id)
|
||||
.inTable(TableUser);
|
||||
table
|
||||
.integer(FieldNameGroupUser.groupId)
|
||||
.unsigned()
|
||||
.notNullable()
|
||||
.references(FieldNameGroup.id)
|
||||
.inTable(TableGroup);
|
||||
table.primary([FieldNameGroupUser.userId, FieldNameGroupUser.groupId]);
|
||||
});
|
||||
|
||||
// Create revision table
|
||||
await knex.schema.createTable(TableRevision, (table) => {
|
||||
table.increments(FieldNameRevision.id).primary();
|
||||
table
|
||||
.integer(FieldNameRevision.noteId)
|
||||
.unsigned()
|
||||
.notNullable()
|
||||
.references(FieldNameNote.id)
|
||||
.inTable(TableNote);
|
||||
table.text(FieldNameRevision.patch).notNullable();
|
||||
table.text(FieldNameRevision.content).notNullable();
|
||||
table.string(FieldNameRevision.title).notNullable();
|
||||
table.text(FieldNameRevision.description).notNullable();
|
||||
table.binary(FieldNameRevision.yjsStateVector).nullable();
|
||||
table.enu(FieldNameRevision.noteType, [NoteType.DOCUMENT, NoteType.SLIDE], {
|
||||
useNative: true,
|
||||
enumName: FieldNameRevision.noteType,
|
||||
});
|
||||
table.timestamp(FieldNameRevision.createdAt).defaultTo(knex.fn.now());
|
||||
});
|
||||
|
||||
// Create revision_tag table
|
||||
await knex.schema.createTable(TableRevisionTag, (table) => {
|
||||
table
|
||||
.integer(FieldNameRevisionTag.revisionId)
|
||||
.unsigned()
|
||||
.notNullable()
|
||||
.references(FieldNameRevision.id)
|
||||
.inTable(TableRevision);
|
||||
table.string(FieldNameRevisionTag.tag).notNullable();
|
||||
table.primary([FieldNameRevisionTag.revisionId, FieldNameRevisionTag.tag]);
|
||||
});
|
||||
|
||||
// Create authorship_info table
|
||||
await knex.schema.createTable(TableAuthorshipInfo, (table) => {
|
||||
table
|
||||
.integer(FieldNameAuthorshipInfo.revisionId)
|
||||
.unsigned()
|
||||
.notNullable()
|
||||
.references(FieldNameRevision.id)
|
||||
.inTable(TableRevision);
|
||||
table
|
||||
.integer(FieldNameAuthorshipInfo.authorId)
|
||||
.unsigned()
|
||||
.notNullable()
|
||||
.references(FieldNameUser.id)
|
||||
.inTable(TableUser);
|
||||
table
|
||||
.integer(FieldNameAuthorshipInfo.startPosition)
|
||||
.unsigned()
|
||||
.notNullable();
|
||||
table.integer(FieldNameAuthorshipInfo.endPosition).unsigned().notNullable();
|
||||
});
|
||||
|
||||
// Create note_user_permission table
|
||||
await knex.schema.createTable(TableNoteUserPermission, (table) => {
|
||||
table
|
||||
.integer(FieldNameNoteUserPermission.noteId)
|
||||
.unsigned()
|
||||
.notNullable()
|
||||
.references(FieldNameNote.id)
|
||||
.inTable(TableNote);
|
||||
table
|
||||
.integer(FieldNameNoteUserPermission.userId)
|
||||
.unsigned()
|
||||
.notNullable()
|
||||
.references(FieldNameUser.id)
|
||||
.inTable(TableUser);
|
||||
table
|
||||
.boolean(FieldNameNoteUserPermission.canEdit)
|
||||
.notNullable()
|
||||
.defaultTo(false);
|
||||
table.primary([
|
||||
FieldNameNoteUserPermission.noteId,
|
||||
FieldNameNoteUserPermission.userId,
|
||||
]);
|
||||
});
|
||||
|
||||
// Create note_group_permission table
|
||||
await knex.schema.createTable(TableNoteGroupPermission, (table) => {
|
||||
table
|
||||
.integer(FieldNameNoteGroupPermission.noteId)
|
||||
.unsigned()
|
||||
.notNullable()
|
||||
.references(FieldNameNote.id)
|
||||
.inTable(TableNote);
|
||||
table
|
||||
.integer(FieldNameNoteGroupPermission.groupId)
|
||||
.unsigned()
|
||||
.notNullable()
|
||||
.references(FieldNameGroup.id)
|
||||
.inTable(TableGroup);
|
||||
table
|
||||
.boolean(FieldNameNoteGroupPermission.canEdit)
|
||||
.notNullable()
|
||||
.defaultTo(false);
|
||||
table.primary([
|
||||
FieldNameNoteGroupPermission.noteId,
|
||||
FieldNameNoteGroupPermission.groupId,
|
||||
]);
|
||||
});
|
||||
|
||||
// Create media_upload table
|
||||
await knex.schema.createTable(TableMediaUpload, (table) => {
|
||||
table.uuid(FieldNameMediaUpload.uuid).primary();
|
||||
table
|
||||
.integer(FieldNameMediaUpload.noteId)
|
||||
.unsigned()
|
||||
.nullable()
|
||||
.references(FieldNameNote.id)
|
||||
.inTable(TableNote);
|
||||
table
|
||||
.integer(FieldNameMediaUpload.userId)
|
||||
.unsigned()
|
||||
.notNullable()
|
||||
.references(FieldNameUser.id)
|
||||
.inTable(TableUser);
|
||||
table.string(FieldNameMediaUpload.fileName).notNullable();
|
||||
table
|
||||
.enu(
|
||||
FieldNameMediaUpload.backendType,
|
||||
[
|
||||
BackendType.AZURE,
|
||||
BackendType.FILESYSTEM,
|
||||
BackendType.IMGUR,
|
||||
BackendType.S3,
|
||||
BackendType.WEBDAV,
|
||||
],
|
||||
{
|
||||
useNative: true,
|
||||
enumName: FieldNameMediaUpload.backendType,
|
||||
},
|
||||
)
|
||||
.notNullable();
|
||||
table.text(FieldNameMediaUpload.backendData).nullable();
|
||||
table.timestamp(FieldNameMediaUpload.createdAt).defaultTo(knex.fn.now());
|
||||
});
|
||||
|
||||
// Create user_pinned_note table
|
||||
await knex.schema.createTable(TableUserPinnedNote, (table) => {
|
||||
table
|
||||
.integer(FieldNameUserPinnedNote.userId)
|
||||
.unsigned()
|
||||
.notNullable()
|
||||
.references(FieldNameUser.id)
|
||||
.inTable(TableUser);
|
||||
table
|
||||
.integer(FieldNameUserPinnedNote.noteId)
|
||||
.unsigned()
|
||||
.notNullable()
|
||||
.references(FieldNameNote.id)
|
||||
.inTable(TableNote);
|
||||
table.primary([
|
||||
FieldNameUserPinnedNote.userId,
|
||||
FieldNameUserPinnedNote.noteId,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
// Drop tables in reverse order of creation to avoid integer key constraints
|
||||
await knex.schema.dropTableIfExists(TableUserPinnedNote);
|
||||
await knex.schema.dropTableIfExists(TableMediaUpload);
|
||||
await knex.schema.dropTableIfExists(TableNoteGroupPermission);
|
||||
await knex.schema.dropTableIfExists(TableNoteUserPermission);
|
||||
await knex.schema.dropTableIfExists(TableAuthorshipInfo);
|
||||
await knex.schema.dropTableIfExists(TableRevisionTag);
|
||||
await knex.schema.dropTableIfExists(TableRevision);
|
||||
await knex.schema.dropTableIfExists(TableGroupUser);
|
||||
await knex.schema.dropTableIfExists(TableIdentity);
|
||||
await knex.schema.dropTableIfExists(TableApiToken);
|
||||
await knex.schema.dropTableIfExists(TableApiToken);
|
||||
await knex.schema.dropTableIfExists(TableNote);
|
||||
await knex.schema.dropTableIfExists(TableGroup);
|
||||
await knex.schema.dropTableIfExists(TableUser);
|
||||
}
|
|
@ -12,11 +12,19 @@
|
|||
*/
|
||||
export interface Alias {
|
||||
/** The alias as defined by the user. Is unique. */
|
||||
alias: string;
|
||||
[FieldNameAlias.alias]: string;
|
||||
|
||||
/** The id of the associated {@link Note}. */
|
||||
noteId: number;
|
||||
[FieldNameAlias.noteId]: number;
|
||||
|
||||
/** Whether the alias is the primary one for the note. */
|
||||
isPrimary: boolean;
|
||||
[FieldNameAlias.isPrimary]: boolean;
|
||||
}
|
||||
|
||||
export enum FieldNameAlias {
|
||||
alias = 'alias',
|
||||
noteId = 'note_id',
|
||||
isPrimary = 'is_primary',
|
||||
}
|
||||
|
||||
export const TableAlias = 'alias';
|
|
@ -11,20 +11,31 @@
|
|||
*/
|
||||
export interface ApiToken {
|
||||
/** The id of the token, a short random ASCII string. Is unique */
|
||||
id: string;
|
||||
[FieldNameApiToken.id]: string;
|
||||
|
||||
/** The {@link User} whose permissions the token has */
|
||||
userId: number;
|
||||
[FieldNameApiToken.userId]: number;
|
||||
|
||||
/** The user-defined label for the token, such as "CLI" */
|
||||
label: string;
|
||||
[FieldNameApiToken.label]: string;
|
||||
|
||||
/** Hashed version of the token's secret */
|
||||
secretHash: string;
|
||||
[FieldNameApiToken.secretHash]: string;
|
||||
|
||||
/** Expiry date of the token */
|
||||
validUntil: Date;
|
||||
[FieldNameApiToken.validUntil]: Date;
|
||||
|
||||
/** When the token was last used. When it was never used yet, this field is null */
|
||||
lastUsedAt: Date | null;
|
||||
[FieldNameApiToken.lastUsedAt]: Date | null;
|
||||
}
|
||||
|
||||
export enum FieldNameApiToken {
|
||||
id = 'id',
|
||||
userId = 'user_id',
|
||||
label = 'label',
|
||||
secretHash = 'secret_hash',
|
||||
validUntil = 'valid_until',
|
||||
lastUsedAt = 'last_used_at',
|
||||
}
|
||||
|
||||
export const TableApiToken = 'api_token';
|
|
@ -11,17 +11,23 @@
|
|||
*/
|
||||
export interface AuthorshipInfo {
|
||||
/** The id of the {@link Revision} this belongs to. */
|
||||
revisionId: number;
|
||||
[FieldNameAuthorshipInfo.revisionId]: number;
|
||||
|
||||
/** The id of the author of the edit. */
|
||||
authorId: number;
|
||||
[FieldNameAuthorshipInfo.authorId]: number;
|
||||
|
||||
/** The start position of the change in the note as a positive index. */
|
||||
startPos: number;
|
||||
[FieldNameAuthorshipInfo.startPosition]: number;
|
||||
|
||||
/** The end position of the change in the note as a positive index. */
|
||||
endPos: number;
|
||||
|
||||
/** The creation datetime of the edit. */
|
||||
createdAt: Date;
|
||||
[FieldNameAuthorshipInfo.endPosition]: number;
|
||||
}
|
||||
|
||||
export enum FieldNameAuthorshipInfo {
|
||||
revisionId = 'revision_id',
|
||||
authorId = 'author_id',
|
||||
startPosition = 'start_position',
|
||||
endPosition = 'end_position',
|
||||
}
|
||||
|
||||
export const TableAuthorshipInfo = 'authorship_info';
|
|
@ -9,8 +9,15 @@
|
|||
*/
|
||||
export interface GroupUser {
|
||||
/** The id of the {@link Group} a {@link User} is part of */
|
||||
groupId: number;
|
||||
[FieldNameGroupUser.groupId]: number;
|
||||
|
||||
/** The id of the {@link User} */
|
||||
userId: number;
|
||||
[FieldNameGroupUser.userId]: number;
|
||||
}
|
||||
|
||||
export enum FieldNameGroupUser {
|
||||
groupId = 'group_id',
|
||||
userId = 'user_id',
|
||||
}
|
||||
|
||||
export const TableGroupUser = 'group_user';
|
|
@ -11,14 +11,23 @@
|
|||
*/
|
||||
export interface Group {
|
||||
/** The unique id for internal referencing */
|
||||
id: number;
|
||||
[FieldNameGroup.id]: number;
|
||||
|
||||
/** The public identifier of the group (username for the group) */
|
||||
name: string;
|
||||
[FieldNameGroup.name]: string;
|
||||
|
||||
/** The display name of the group */
|
||||
displayName: string;
|
||||
[FieldNameGroup.displayName]: string;
|
||||
|
||||
/** Whether the group is one of the special groups */
|
||||
isSpecial: boolean;
|
||||
[FieldNameGroup.isSpecial]: boolean;
|
||||
}
|
||||
|
||||
export enum FieldNameGroup {
|
||||
id = 'id',
|
||||
name = 'name',
|
||||
displayName = 'display_name',
|
||||
isSpecial = 'is_special',
|
||||
}
|
||||
|
||||
export const TableGroup = 'group';
|
44
backend/src/database/types/identity.ts
Normal file
44
backend/src/database/types/identity.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { ProviderType } from '../../auth/provider-type.enum';
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/** The type of the auth provider */
|
||||
[FieldNameIdentity.providerType]: ProviderType;
|
||||
|
||||
/** The identifier of the auth provider, e.g. gitlab */
|
||||
[FieldNameIdentity.providerIdentifier]: string | null;
|
||||
|
||||
/** Timestamp when this identity was created */
|
||||
[FieldNameIdentity.createdAt]: Date;
|
||||
|
||||
/** Timestamp when this identity was last updated */
|
||||
[FieldNameIdentity.updatedAt]: Date;
|
||||
|
||||
/** The remote id of the user at the auth provider or null for local identities */
|
||||
[FieldNameIdentity.providerUserId]: string | null;
|
||||
|
||||
/** The hashed password for local identities or null for other auth providers */
|
||||
[FieldNameIdentity.passwordHash]: string | null;
|
||||
}
|
||||
|
||||
export enum FieldNameIdentity {
|
||||
userId = 'user_id',
|
||||
providerType = 'provider_type',
|
||||
providerIdentifier = 'provider_identifier',
|
||||
createdAt = 'created_at',
|
||||
updatedAt = 'updated_at',
|
||||
providerUserId = 'provider_user_id',
|
||||
passwordHash = 'password_hash',
|
||||
}
|
||||
|
||||
export const TableIdentity = 'identity';
|
44
backend/src/database/types/index.ts
Normal file
44
backend/src/database/types/index.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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';
|
118
backend/src/database/types/knex.types.ts
Normal file
118
backend/src/database/types/knex.types.ts
Normal file
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { Knex } from 'knex';
|
||||
|
||||
import {
|
||||
Alias,
|
||||
ApiToken,
|
||||
AuthorshipInfo,
|
||||
FieldNameAlias,
|
||||
FieldNameApiToken,
|
||||
FieldNameGroup,
|
||||
FieldNameIdentity,
|
||||
FieldNameMediaUpload,
|
||||
FieldNameNote,
|
||||
FieldNameNoteGroupPermission,
|
||||
FieldNameNoteUserPermission,
|
||||
FieldNameRevision,
|
||||
FieldNameUser,
|
||||
Group,
|
||||
GroupUser,
|
||||
Identity,
|
||||
MediaUpload,
|
||||
Note,
|
||||
NoteGroupPermission,
|
||||
NoteUserPermission,
|
||||
Revision,
|
||||
RevisionTag,
|
||||
TableAlias,
|
||||
TableApiToken,
|
||||
TableAuthorshipInfo,
|
||||
TableGroup,
|
||||
TableGroupUser,
|
||||
TableIdentity,
|
||||
TableMediaUpload,
|
||||
TableNote,
|
||||
TableNoteGroupPermission,
|
||||
TableNoteUserPermission,
|
||||
TableRevision,
|
||||
TableRevisionTag,
|
||||
TableUser,
|
||||
TableUserPinnedNote,
|
||||
User,
|
||||
UserPinnedNote,
|
||||
} from './index';
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
declare module 'knex/types/tables.js' {
|
||||
interface Tables {
|
||||
[TableAlias]: Knex.CompositeTableType<
|
||||
Alias,
|
||||
Alias,
|
||||
Pick<Alias, FieldNameAlias.isPrimary>
|
||||
>;
|
||||
[TableApiToken]: Knex.CompositeTableType<
|
||||
ApiToken,
|
||||
Omit<ApiToken, FieldNameApiToken.lastUsedAt>,
|
||||
Pick<ApiToken, FieldNameApiToken.lastUsedAt>
|
||||
>;
|
||||
[TableAuthorshipInfo]: AuthorshipInfo;
|
||||
[TableGroup]: Knex.CompositeTableType<
|
||||
Group,
|
||||
Omit<Group, FieldNameGroup.id>,
|
||||
Pick<Group, FieldNameGroup.name | FieldNameGroup.displayName>
|
||||
>;
|
||||
[TableGroupUser]: GroupUser;
|
||||
[TableIdentity]: Knex.CompositeTableType<
|
||||
Identity,
|
||||
Omit<Identity, FieldNameIdentity.createdAt | FieldNameIdentity.updatedAt>,
|
||||
Pick<
|
||||
Identity,
|
||||
FieldNameIdentity.passwordHash | FieldNameIdentity.updatedAt
|
||||
>
|
||||
>;
|
||||
[TableMediaUpload]: Knex.CompositeTableType<
|
||||
MediaUpload,
|
||||
Omit<
|
||||
MediaUpload,
|
||||
FieldNameMediaUpload.createdAt | FieldNameMediaUpload.uuid
|
||||
>,
|
||||
Pick<MediaUpload, FieldNameMediaUpload.noteId>
|
||||
>;
|
||||
[TableNote]: Knex.CompositeTableType<
|
||||
Note,
|
||||
Omit<Note, FieldNameNote.createdAt | FieldNameNote.id>,
|
||||
Pick<Note, FieldNameNote.ownerId>
|
||||
>;
|
||||
[TableNoteGroupPermission]: Knex.CompositeTableType<
|
||||
NoteGroupPermission,
|
||||
NoteGroupPermission,
|
||||
Pick<NoteGroupPermission, FieldNameNoteGroupPermission.canEdit>
|
||||
>;
|
||||
[TableNoteUserPermission]: Knex.CompositeTableType<
|
||||
NoteUserPermission,
|
||||
NoteUserPermission,
|
||||
Pick<NoteUserPermission, FieldNameNoteUserPermission.canEdit>
|
||||
>;
|
||||
[TableRevision]: Knex.CompositeTableType<
|
||||
Revision,
|
||||
Omit<Revision, FieldNameRevision.createdAt | FieldNameRevision.id>
|
||||
>;
|
||||
[TableRevisionTag]: RevisionTag;
|
||||
[TableUser]: Knex.CompositeTableType<
|
||||
User,
|
||||
Omit<User, FieldNameUser.id | FieldNameUser.createdAt>,
|
||||
Pick<
|
||||
User,
|
||||
| FieldNameUser.displayName
|
||||
| FieldNameUser.photoUrl
|
||||
| FieldNameUser.email
|
||||
| FieldNameUser.authorStyle
|
||||
>
|
||||
>;
|
||||
[TableUserPinnedNote]: UserPinnedNote;
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { BackendType } from '../media/backends/backend-type.enum';
|
||||
import { BackendType } from '../../media/backends/backend-type.enum';
|
||||
|
||||
/**
|
||||
* A media upload object represents an uploaded file. While the file itself is stored in the configured storage backend,
|
||||
|
@ -12,23 +12,35 @@ import { BackendType } from '../media/backends/backend-type.enum';
|
|||
*/
|
||||
export interface MediaUpload {
|
||||
/** UUID (v7) identifying the media upload. Is public and unique */
|
||||
uuid: string;
|
||||
[FieldNameMediaUpload.uuid]: string;
|
||||
|
||||
/** The id of the attached {@link Note} or null if the media upload was detached from a note */
|
||||
noteId: number | null;
|
||||
[FieldNameMediaUpload.noteId]: number | null;
|
||||
|
||||
/** The id of the {@link User} who uploaded the media file */
|
||||
userId: number;
|
||||
[FieldNameMediaUpload.userId]: number;
|
||||
|
||||
/** The name of the uploaded file */
|
||||
fileName: string;
|
||||
[FieldNameMediaUpload.fileName]: string;
|
||||
|
||||
/** The backend where this upload is stored */
|
||||
backendType: BackendType;
|
||||
[FieldNameMediaUpload.backendType]: BackendType;
|
||||
|
||||
/** Additional data required by the backend storage to identify the uploaded file */
|
||||
backendData: string | null;
|
||||
[FieldNameMediaUpload.backendData]: string | null;
|
||||
|
||||
/** Timestamp when the file was uploaded */
|
||||
createdAt: Date;
|
||||
[FieldNameMediaUpload.createdAt]: Date;
|
||||
}
|
||||
|
||||
export enum FieldNameMediaUpload {
|
||||
uuid = 'uuid',
|
||||
noteId = 'note_id',
|
||||
userId = 'user_id',
|
||||
fileName = 'file_name',
|
||||
backendType = 'backend_type',
|
||||
backendData = 'backend_data',
|
||||
createdAt = 'created_at',
|
||||
}
|
||||
|
||||
export const TableMediaUpload = 'media_upload';
|
|
@ -8,11 +8,19 @@
|
|||
*/
|
||||
export interface NoteGroupPermission {
|
||||
/** The id of the {@link Group} to give the {@link Note} permission to. */
|
||||
groupId: number;
|
||||
[FieldNameNoteGroupPermission.groupId]: number;
|
||||
|
||||
/** The id of the {@link Note} to give the {@link Group} permission to. */
|
||||
noteId: number;
|
||||
[FieldNameNoteGroupPermission.noteId]: number;
|
||||
|
||||
/** Whether the {@link Group} can edit the {@link Note} or not. */
|
||||
canEdit: boolean;
|
||||
[FieldNameNoteGroupPermission.canEdit]: boolean;
|
||||
}
|
||||
|
||||
export enum FieldNameNoteGroupPermission {
|
||||
groupId = 'group_id',
|
||||
noteId = 'note_id',
|
||||
canEdit = 'can_edit',
|
||||
}
|
||||
|
||||
export const TableNoteGroupPermission = 'note_group_permission';
|
|
@ -8,11 +8,19 @@
|
|||
*/
|
||||
export interface NoteUserPermission {
|
||||
/** The id of the {@link User} to give the {@link Note} permission to. */
|
||||
noteId: number;
|
||||
[FieldNameNoteUserPermission.userId]: number;
|
||||
|
||||
/** The id of the {@link Note} to give the {@link User} permission to. */
|
||||
userId: number;
|
||||
[FieldNameNoteUserPermission.noteId]: number;
|
||||
|
||||
/** Whether the {@link User} can edit the {@link Note} or not. */
|
||||
canEdit: boolean;
|
||||
[FieldNameNoteUserPermission.canEdit]: boolean;
|
||||
}
|
||||
|
||||
export enum FieldNameNoteUserPermission {
|
||||
userId = 'user_id',
|
||||
noteId = 'note_id',
|
||||
canEdit = 'can_edit',
|
||||
}
|
||||
|
||||
export const TableNoteUserPermission = 'note_user_permission';
|
|
@ -11,14 +11,23 @@
|
|||
*/
|
||||
export interface Note {
|
||||
/** The unique id of the note for internal referencing */
|
||||
id: number;
|
||||
[FieldNameNote.id]: number;
|
||||
|
||||
/** The {@link User} id of the note owner */
|
||||
ownerId: string;
|
||||
[FieldNameNote.ownerId]: number;
|
||||
|
||||
/** The HedgeDoc major version this note was created in. This is used to migrate certain features from HD1 to HD2 */
|
||||
version: number;
|
||||
[FieldNameNote.version]: number;
|
||||
|
||||
/** Timestamp when the note was created */
|
||||
createdAt: Date;
|
||||
[FieldNameNote.createdAt]: Date;
|
||||
}
|
||||
|
||||
export enum FieldNameNote {
|
||||
id = 'id',
|
||||
ownerId = 'owner_id',
|
||||
version = 'version',
|
||||
createdAt = 'created_at',
|
||||
}
|
||||
|
||||
export const TableNote = 'note';
|
|
@ -8,8 +8,15 @@
|
|||
*/
|
||||
export interface RevisionTag {
|
||||
/** The id of {@link Revision} the {@link RevisionTag Tags} are asspcoated with. */
|
||||
revisionId: number;
|
||||
[FieldNameRevisionTag.revisionId]: number;
|
||||
|
||||
/** The {@link RevisionTag Tag} text. */
|
||||
tag: string;
|
||||
[FieldNameRevisionTag.tag]: string;
|
||||
}
|
||||
|
||||
export enum FieldNameRevisionTag {
|
||||
revisionId = 'revision_id',
|
||||
tag = 'tag',
|
||||
}
|
||||
|
||||
export const TableRevisionTag = 'revision_tag';
|
|
@ -10,29 +10,43 @@ import { NoteType } from '@hedgedoc/commons';
|
|||
*/
|
||||
export interface Revision {
|
||||
/** The unique id of the revision for internal referencing */
|
||||
id: number;
|
||||
[FieldNameRevision.id]: number;
|
||||
|
||||
/** The id of the note that this revision belongs to */
|
||||
noteId: number;
|
||||
[FieldNameRevision.noteId]: number;
|
||||
|
||||
/** The changes between this revision and the previous one in patch file format */
|
||||
patch: string;
|
||||
[FieldNameRevision.patch]: string;
|
||||
|
||||
/** The content of the note at this revision */
|
||||
content: string;
|
||||
[FieldNameRevision.content]: string;
|
||||
|
||||
/** The stored Y.js state for realtime editing */
|
||||
yjsStateVector: null | ArrayBuffer;
|
||||
[FieldNameRevision.yjsStateVector]: null | ArrayBuffer;
|
||||
|
||||
/** Whether the note is a document or presentation at this revision */
|
||||
noteType: NoteType;
|
||||
[FieldNameRevision.noteType]: NoteType;
|
||||
|
||||
/** The extracted note title from this revision */
|
||||
title: string;
|
||||
[FieldNameRevision.title]: string;
|
||||
|
||||
/** The extracted description from this revision */
|
||||
description: string;
|
||||
[FieldNameRevision.description]: string;
|
||||
|
||||
/** Timestamp when this revision was created */
|
||||
createdAt: Date;
|
||||
[FieldNameRevision.createdAt]: Date;
|
||||
}
|
||||
|
||||
export enum FieldNameRevision {
|
||||
id = 'id',
|
||||
noteId = 'note_id',
|
||||
patch = 'patch',
|
||||
content = 'content',
|
||||
yjsStateVector = 'yjs_state_vector',
|
||||
noteType = 'note_type',
|
||||
title = 'title',
|
||||
description = 'description',
|
||||
createdAt = 'created_at',
|
||||
}
|
||||
|
||||
export const TableRevision = 'revision';
|
|
@ -10,8 +10,15 @@
|
|||
*/
|
||||
export interface UserPinnedNote {
|
||||
/** The id of the {@link User} */
|
||||
userId: number;
|
||||
user_id: number;
|
||||
|
||||
/** The id of the {@link Note} */
|
||||
noteId: number;
|
||||
note_id: number;
|
||||
}
|
||||
|
||||
export enum FieldNameUserPinnedNote {
|
||||
userId = 'user_id',
|
||||
noteId = 'note_id',
|
||||
}
|
||||
|
||||
export const TableUserPinnedNote = 'user_pinned_note';
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { Username } from '../utils/username';
|
||||
import type { Username } from '../../utils/username';
|
||||
|
||||
/**
|
||||
* The user object represents either a registered user in the instance or a guest user.
|
||||
|
@ -18,26 +18,39 @@ import type { Username } from '../utils/username';
|
|||
*/
|
||||
export interface User {
|
||||
/** The unique id of the user for internal referencing */
|
||||
id: number;
|
||||
[FieldNameUser.id]: number;
|
||||
|
||||
/** The user's chosen username or null if it is a guest user */
|
||||
username: Username | null;
|
||||
[FieldNameUser.username]: Username | null;
|
||||
|
||||
/** The guest user's UUID or null if it is a registered user */
|
||||
guestUuid: string | null;
|
||||
[FieldNameUser.guestUuid]: string | null;
|
||||
|
||||
/** The user's chosen display name */
|
||||
displayName: string;
|
||||
[FieldNameUser.displayName]: string | null;
|
||||
|
||||
/** Timestamp when the user was created */
|
||||
createdAt: Date;
|
||||
[FieldNameUser.createdAt]: Date;
|
||||
|
||||
/** URL to the user's profile picture if present */
|
||||
photoUrl: string | null;
|
||||
[FieldNameUser.photoUrl]: string | null;
|
||||
|
||||
/** The user's email address if present */
|
||||
email: string | null;
|
||||
[FieldNameUser.email]: string | null;
|
||||
|
||||
/** The index which author style (e.g. color) should be used for this user */
|
||||
authorStyle: number;
|
||||
[FieldNameUser.authorStyle]: number;
|
||||
}
|
||||
|
||||
export enum FieldNameUser {
|
||||
id = 'id',
|
||||
username = 'username',
|
||||
guestUuid = 'guest_uuid',
|
||||
displayName = 'display_name',
|
||||
createdAt = 'created_at',
|
||||
photoUrl = 'photo_url',
|
||||
email = 'email',
|
||||
authorStyle = 'author_style',
|
||||
}
|
||||
|
||||
export const TableUser = 'user';
|
Loading…
Add table
Add a link
Reference in a new issue