mirror of
https://github.com/Shadowfita/docmost.git
synced 2025-11-16 17:51:06 +10:00
fixes
* 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:
@ -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 {
|
||||
|
||||
@ -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()`),
|
||||
|
||||
@ -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'),
|
||||
)
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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()`),
|
||||
)
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
47
apps/server/src/database/services/migration.service.ts
Normal file
47
apps/server/src/database/services/migration.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user