* integrate websocket redis adapter
* use APP_SECRET for jwt signing
* auto migrate database on startup in production
* add updatedAt to update db operations
* create enterprise ee package directory
* fix comment editor focus
* other fixes
This commit is contained in:
Philipinho
2024-06-07 17:29:34 +01:00
parent eef9081aaf
commit 38ef610e5e
36 changed files with 541 additions and 166 deletions

View File

@ -21,6 +21,7 @@ import { PageHistoryRepo } from './repos/page/page-history.repo';
import { AttachmentRepo } from './repos/attachment/attachment.repo';
import { KyselyDB } from '@docmost/db/types/kysely.types';
import * as process from 'node:process';
import { MigrationService } from '@docmost/db/services/migration.service';
// https://github.com/brianc/node-postgres/issues/811
types.setTypeParser(types.builtins.INT8, (val) => Number(val));
@ -39,7 +40,7 @@ types.setTypeParser(types.builtins.INT8, (val) => Number(val));
}),
plugins: [new CamelCasePlugin()],
log: (event: LogEvent) => {
if (environmentService.getEnv() !== 'development') return;
if (environmentService.getNodeEnv() !== 'development') return;
if (event.level === 'query') {
// console.log(event.query.sql);
//if (event.query.parameters.length > 0) {
@ -52,6 +53,7 @@ types.setTypeParser(types.builtins.INT8, (val) => Number(val));
}),
],
providers: [
MigrationService,
WorkspaceRepo,
UserRepo,
GroupRepo,
@ -79,10 +81,18 @@ types.setTypeParser(types.builtins.INT8, (val) => Number(val));
export class DatabaseModule implements OnModuleDestroy, OnApplicationBootstrap {
private readonly logger = new Logger(DatabaseModule.name);
constructor(@InjectKysely() private readonly db: KyselyDB) {}
constructor(
@InjectKysely() private readonly db: KyselyDB,
private readonly migrationService: MigrationService,
private readonly environmentService: EnvironmentService,
) {}
async onApplicationBootstrap() {
await this.establishConnection();
if (this.environmentService.getNodeEnv() === 'production') {
await this.migrationService.migrateToLatest();
}
}
async onModuleDestroy(): Promise<void> {
@ -110,7 +120,7 @@ export class DatabaseModule implements OnModuleDestroy, OnApplicationBootstrap {
if (i < retryAttempts - 1) {
this.logger.log(
`Retrying [${i + 1}/${retryAttempts}] in ${retryDelay} ms`,
`Retrying [${i + 1}/${retryAttempts}] in ${retryDelay / 1000} seconds`,
);
await new Promise((resolve) => setTimeout(resolve, retryDelay));
} else {

View File

@ -16,9 +16,7 @@ export async function up(db: Kysely<any>): Promise<void> {
.addColumn('default_role', 'varchar', (col) =>
col.defaultTo(UserRole.MEMBER).notNull(),
)
.addColumn('allowed_email_domains', sql`varchar[]`, (col) =>
col.defaultTo('{}'),
)
.addColumn('email_domains', sql`varchar[]`, (col) => col.defaultTo('{}'))
.addColumn('default_space_id', 'uuid', (col) => col)
.addColumn('created_at', 'timestamptz', (col) =>
col.notNull().defaultTo(sql`now()`),

View File

@ -11,7 +11,7 @@ export async function up(db: Kysely<any>): Promise<void> {
.addColumn('email_verified_at', 'timestamptz', (col) => col)
.addColumn('password', 'varchar', (col) => col)
.addColumn('avatar_url', 'varchar', (col) => col)
.addColumn('role', 'varchar', (col) => col.notNull())
.addColumn('role', 'varchar', (col) => col)
.addColumn('invited_by_id', 'uuid', (col) =>
col.references('users.id').onDelete('set null'),
)

View File

@ -12,7 +12,7 @@ export async function up(db: Kysely<any>): Promise<void> {
.addColumn('slug', 'varchar', (col) => col.notNull())
.addColumn('logo', 'varchar', (col) => col)
.addColumn('visibility', 'varchar', (col) =>
col.defaultTo(SpaceVisibility.OPEN).notNull(),
col.defaultTo(SpaceVisibility.PRIVATE).notNull(),
)
.addColumn('default_role', 'varchar', (col) =>
col.defaultTo(SpaceRole.WRITER).notNull(),

View File

@ -19,6 +19,7 @@ export async function up(db: Kysely<any>): Promise<void> {
.addColumn('workspace_id', 'uuid', (col) =>
col.references('workspaces.id').notNull(),
)
.addColumn('resolved_at', 'timestamptz', (col) => col)
.addColumn('created_at', 'timestamptz', (col) =>
col.notNull().defaultTo(sql`now()`),
)

View File

@ -54,7 +54,7 @@ export class GroupRepo {
): Promise<void> {
await this.db
.updateTable('groups')
.set(updatableGroup)
.set({ ...updatableGroup, updatedAt: new Date() })
.where('id', '=', groupId)
.where('workspaceId', '=', workspaceId)
.execute();

View File

@ -17,8 +17,13 @@ import { DB } from '@docmost/db/types/db';
export class PageHistoryRepo {
constructor(@InjectKysely() private readonly db: KyselyDB) {}
async findById(pageHistoryId: string): Promise<PageHistory> {
return await this.db
async findById(
pageHistoryId: string,
trx?: KyselyTransaction,
): Promise<PageHistory> {
const db = dbOrTx(this.db, trx);
return await db
.selectFrom('pageHistory')
.selectAll()
.select((eb) => this.withLastUpdatedBy(eb))
@ -38,18 +43,21 @@ export class PageHistoryRepo {
.executeTakeFirst();
}
async saveHistory(page: Page): Promise<void> {
await this.insertPageHistory({
pageId: page.id,
slugId: page.slugId,
title: page.title,
content: page.content,
icon: page.icon,
coverPhoto: page.coverPhoto,
lastUpdatedById: page.lastUpdatedById ?? page.creatorId,
spaceId: page.spaceId,
workspaceId: page.workspaceId,
});
async saveHistory(page: Page, trx?: KyselyTransaction): Promise<void> {
await this.insertPageHistory(
{
pageId: page.id,
slugId: page.slugId,
title: page.title,
content: page.content,
icon: page.icon,
coverPhoto: page.coverPhoto,
lastUpdatedById: page.lastUpdatedById ?? page.creatorId,
spaceId: page.spaceId,
workspaceId: page.workspaceId,
},
trx,
);
}
async findPageHistoryByPageId(pageId: string, pagination: PaginationOptions) {
@ -68,6 +76,18 @@ export class PageHistoryRepo {
return result;
}
async findPageLastHistory(pageId: string, trx?: KyselyTransaction) {
const db = dbOrTx(this.db, trx);
return await db
.selectFrom('pageHistory')
.selectAll()
.where('pageId', '=', pageId)
.limit(1)
.orderBy('createdAt', 'desc')
.executeTakeFirst();
}
withLastUpdatedBy(eb: ExpressionBuilder<DB, 'pageHistory'>) {
return jsonObjectFrom(
eb

View File

@ -46,9 +46,13 @@ export class PageRepo {
includeContent?: boolean;
includeYdoc?: boolean;
includeSpace?: boolean;
withLock?: boolean;
trx?: KyselyTransaction;
},
): Promise<Page> {
let query = this.db
const db = dbOrTx(this.db, opts?.trx);
let query = db
.selectFrom('pages')
.select(this.baseFields)
.$if(opts?.includeContent, (qb) => qb.select('content'))
@ -58,6 +62,10 @@ export class PageRepo {
query = query.select((eb) => this.withSpace(eb));
}
if (opts?.withLock && opts?.trx) {
query = query.forUpdate();
}
if (isValidUUID(pageId)) {
query = query.where('id', '=', pageId);
} else {
@ -73,7 +81,9 @@ export class PageRepo {
trx?: KyselyTransaction,
) {
const db = dbOrTx(this.db, trx);
let query = db.updateTable('pages').set(updatablePage);
let query = db
.updateTable('pages')
.set({ ...updatablePage, updatedAt: new Date() });
if (isValidUUID(pageId)) {
query = query.where('id', '=', pageId);

View File

@ -77,7 +77,7 @@ export class SpaceRepo {
const db = dbOrTx(this.db, trx);
return db
.updateTable('spaces')
.set(updatableSpace)
.set({ ...updatableSpace, updatedAt: new Date() })
.where('id', '=', spaceId)
.where('workspaceId', '=', workspaceId)
.returningAll()

View File

@ -80,7 +80,7 @@ export class UserRepo {
return await db
.updateTable('users')
.set(updatableUser)
.set({ ...updatableUser, updatedAt: new Date() })
.where('id', '=', userId)
.where('workspaceId', '=', workspaceId)
.execute();

View File

@ -53,7 +53,7 @@ export class WorkspaceRepo {
const db = dbOrTx(this.db, trx);
return db
.updateTable('workspaces')
.set(updatableWorkspace)
.set({ ...updatableWorkspace, updatedAt: new Date() })
.where('id', '=', workspaceId)
.execute();
}

View File

@ -0,0 +1,47 @@
import { Injectable, Logger } from '@nestjs/common';
import * as path from 'path';
import { promises as fs } from 'fs';
import { Migrator, FileMigrationProvider } from 'kysely';
import { InjectKysely } from 'nestjs-kysely';
import { KyselyDB } from '@docmost/db/types/kysely.types';
@Injectable()
export class MigrationService {
private readonly logger = new Logger(`Database${MigrationService.name}`);
constructor(@InjectKysely() private readonly db: KyselyDB) {}
async migrateToLatest(): Promise<void> {
const migrator = new Migrator({
db: this.db,
provider: new FileMigrationProvider({
fs,
path,
migrationFolder: path.join(__dirname, '..', 'migrations'),
}),
});
const { error, results } = await migrator.migrateToLatest();
if (results && results.length === 0) {
this.logger.log('No pending database migrations');
return;
}
results?.forEach((it) => {
if (it.status === 'Success') {
this.logger.log(
`Migration "${it.migrationName}" executed successfully`,
);
} else if (it.status === 'Error') {
this.logger.error(`Failed to execute migration "${it.migrationName}"`);
}
});
if (error) {
this.logger.error('Failed to run database migration. Exiting program.');
this.logger.error(error);
process.exit(1);
}
}
}