From 38ef610e5ea2eb505849b2545c1df66ff369492a Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Fri, 7 Jun 2024 17:29:34 +0100 Subject: [PATCH] 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 --- .dockerignore | 4 + .env.example | 18 +- apps/client/.dockerignore | 4 + .../comment/components/comment-editor.tsx | 107 +++++--- .../src/features/user/user-provider.tsx | 2 +- apps/server/.dockerignore | 5 + apps/server/.gitignore | 2 +- apps/server/package.json | 12 +- .../src/core/auth/services/token.service.ts | 2 +- .../src/core/auth/strategies/jwt.strategy.ts | 2 +- apps/server/src/database/database.module.ts | 16 +- .../migrations/20240324T085500-workspaces.ts | 4 +- .../migrations/20240324T085600-users.ts | 2 +- .../migrations/20240324T085900-spaces.ts | 2 +- .../migrations/20240324T086600-comments.ts | 1 + .../src/database/repos/group/group.repo.ts | 2 +- .../database/repos/page/page-history.repo.ts | 48 +++- .../src/database/repos/page/page.repo.ts | 14 +- .../src/database/repos/space/space.repo.ts | 2 +- .../src/database/repos/user/user.repo.ts | 2 +- .../repos/workspace/workspace.repo.ts | 2 +- .../database/services/migration.service.ts | 47 ++++ apps/server/src/helpers/constants.ts | 2 + apps/server/src/helpers/index.ts | 1 + apps/server/src/helpers/utils.ts | 17 +- .../environment/environment.service.ts | 52 ++-- .../environment/environment.validation.ts | 8 +- .../logger/internal-log-filter.ts | 19 ++ .../src/integrations/queue/queue.module.ts | 6 +- .../src/integrations/static/static.module.ts | 2 +- .../storage/providers/storage.provider.ts | 5 +- apps/server/src/main.ts | 12 +- .../server/src/ws/adapter/ws-redis.adapter.ts | 26 ++ package.json | 5 + packages/ee/LICENSE | 0 pnpm-lock.yaml | 252 ++++++++++++++++-- 36 files changed, 541 insertions(+), 166 deletions(-) create mode 100644 .dockerignore create mode 100644 apps/client/.dockerignore create mode 100644 apps/server/.dockerignore create mode 100644 apps/server/src/database/services/migration.service.ts create mode 100644 apps/server/src/helpers/constants.ts create mode 100644 apps/server/src/integrations/logger/internal-log-filter.ts create mode 100644 apps/server/src/ws/adapter/ws-redis.adapter.ts create mode 100644 packages/ee/LICENSE diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..325b561 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +node_modules +.git +.gitignore +dist diff --git a/.env.example b/.env.example index a4de9d6..1dd0dd3 100644 --- a/.env.example +++ b/.env.example @@ -1,22 +1,15 @@ -APP_URL=http://localhost -APP_SECRET= - +APP_URL=http://localhost:3000 +APP_SECRET=REPLACE_WITH_LONG_SECRET PORT=3000 -DEBUG_MODE=true -NODE_ENV=production -JWT_SECRET_KEY=ba8642edbed7f6c450e46875e8c835c7e417031abe1f7b03f3e56bb7481706d8 JWT_TOKEN_EXPIRES_IN=30d -DATABASE_URL="postgresql://postgres:password@localhost:5432/dc?schema=public" -REDIS_URL=redis://@127.0.0.1:6379 +DATABASE_URL="postgresql://postgres:password@localhost:5432/docmost?schema=public" +REDIS_URL=redis://127.0.0.1:6379 # local | s3 STORAGE_DRIVER=local -# local config -LOCAL_STORAGE_PATH=/storage - # S3 Config AWS_S3_ACCESS_KEY_ID= AWS_S3_SECRET_ACCESS_KEY= @@ -24,7 +17,6 @@ AWS_S3_REGION= AWS_S3_BUCKET= AWS_S3_ENDPOINT= AWS_S3_URL= -AWS_S3_USE_PATH_STYLE_ENDPOINT=false # EMAIL drivers: smtp / postmark / log MAIL_DRIVER=smtp @@ -33,7 +25,7 @@ MAIL_PORT=2525 MAIL_USERNAME= MAIL_PASSWORD= MAIL_FROM_ADDRESS=hello@example.com -MAIL_FROM_NAME= +MAIL_FROM_NAME=Docmost # for postmark driver POSTMARK_TOKEN= diff --git a/apps/client/.dockerignore b/apps/client/.dockerignore new file mode 100644 index 0000000..325b561 --- /dev/null +++ b/apps/client/.dockerignore @@ -0,0 +1,4 @@ +node_modules +.git +.gitignore +dist diff --git a/apps/client/src/features/comment/components/comment-editor.tsx b/apps/client/src/features/comment/components/comment-editor.tsx index 22fb947..62e1b13 100644 --- a/apps/client/src/features/comment/components/comment-editor.tsx +++ b/apps/client/src/features/comment/components/comment-editor.tsx @@ -1,12 +1,12 @@ -import { EditorContent, useEditor } from '@tiptap/react'; -import { Placeholder } from '@tiptap/extension-placeholder'; -import { Underline } from '@tiptap/extension-underline'; -import { Link } from '@tiptap/extension-link'; -import { StarterKit } from '@tiptap/starter-kit'; -import classes from './comment.module.css'; -import { useFocusWithin } from '@mantine/hooks'; -import clsx from 'clsx'; -import { forwardRef, useImperativeHandle } from 'react'; +import { EditorContent, useEditor } from "@tiptap/react"; +import { Placeholder } from "@tiptap/extension-placeholder"; +import { Underline } from "@tiptap/extension-underline"; +import { Link } from "@tiptap/extension-link"; +import { StarterKit } from "@tiptap/starter-kit"; +import classes from "./comment.module.css"; +import { useFocusWithin } from "@mantine/hooks"; +import clsx from "clsx"; +import { forwardRef, useEffect, useImperativeHandle } from "react"; interface CommentEditorProps { defaultContent?: any; @@ -16,43 +16,62 @@ interface CommentEditorProps { autofocus?: boolean; } -const CommentEditor = forwardRef(({ defaultContent, onUpdate, editable, placeholder, autofocus }: CommentEditorProps, - ref) => { - const { ref: focusRef, focused } = useFocusWithin(); +const CommentEditor = forwardRef( + ( + { + defaultContent, + onUpdate, + editable, + placeholder, + autofocus, + }: CommentEditorProps, + ref, + ) => { + const { ref: focusRef, focused } = useFocusWithin(); - const commentEditor = useEditor({ - extensions: [ - StarterKit.configure({ - gapcursor: false, - dropcursor: false, - }), - Placeholder.configure({ - placeholder: placeholder || 'Reply...', - }), - Underline, - Link, - ], - onUpdate({ editor }) { - if (onUpdate) onUpdate(editor.getJSON()); - }, - content: defaultContent, - editable, - autofocus: (autofocus && 'end') || false, - }); + const commentEditor = useEditor({ + extensions: [ + StarterKit.configure({ + gapcursor: false, + dropcursor: false, + }), + Placeholder.configure({ + placeholder: placeholder || "Reply...", + }), + Underline, + Link, + ], + onUpdate({ editor }) { + if (onUpdate) onUpdate(editor.getJSON()); + }, + content: defaultContent, + editable, + autofocus: (autofocus && "end") || false, + }); - useImperativeHandle(ref, () => ({ - clearContent: () => { - commentEditor.commands.clearContent(); - }, - })); + useEffect(() => { + setTimeout(() => { + if (autofocus) { + commentEditor?.commands.focus("end"); + } + }, 10); + }, [commentEditor, autofocus]); - return ( -
- -
- ); -}); + useImperativeHandle(ref, () => ({ + clearContent: () => { + commentEditor.commands.clearContent(); + }, + })); + + return ( +
+ +
+ ); + }, +); export default CommentEditor; diff --git a/apps/client/src/features/user/user-provider.tsx b/apps/client/src/features/user/user-provider.tsx index 5e77162..25548ed 100644 --- a/apps/client/src/features/user/user-provider.tsx +++ b/apps/client/src/features/user/user-provider.tsx @@ -15,7 +15,7 @@ export function UserProvider({ children }: React.PropsWithChildren) { if (isLoading) return <>; - if (!data.user && !data.workspace) return <>; + if (!data?.user && !data?.workspace) return <>; if (error) { return <>an error occurred; diff --git a/apps/server/.dockerignore b/apps/server/.dockerignore new file mode 100644 index 0000000..a5acced --- /dev/null +++ b/apps/server/.dockerignore @@ -0,0 +1,5 @@ +node_modules +.git +.gitignore +dist +data diff --git a/apps/server/.gitignore b/apps/server/.gitignore index 95d6c29..e9b116a 100644 --- a/apps/server/.gitignore +++ b/apps/server/.gitignore @@ -1,9 +1,9 @@ -/storage .env package-lock.json # compiled output /dist /node_modules +/data # Logs logs diff --git a/apps/server/package.json b/apps/server/package.json index 7da4d63..e7f70f8 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -8,10 +8,10 @@ "scripts": { "build": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", - "start": "nest start", - "start:dev": "nest start --watch", - "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", + "start": "cross-env NODE_ENV=development nest start", + "start:dev": "cross-env NODE_ENV=development nest start --watch", + "start:debug": "cross-env NODE_ENV=development nest start --debug --watch", + "start:prod": "cross-env NODE_ENV=production node dist/main", "email:dev": "email dev -p 5019 -d ./src/integrations/transactional/emails", "migration:create": "tsx src/database/migrate.ts create", "migration:up": "tsx src/database/migrate.ts up", @@ -37,6 +37,7 @@ "@nestjs/common": "^10.3.9", "@nestjs/config": "^3.2.2", "@nestjs/core": "^10.3.9", + "@nestjs/event-emitter": "^2.0.4", "@nestjs/jwt": "^10.2.0", "@nestjs/mapped-types": "^2.0.5", "@nestjs/passport": "^10.0.3", @@ -45,6 +46,7 @@ "@nestjs/websockets": "^10.3.9", "@react-email/components": "0.0.19", "@react-email/render": "^0.0.15", + "@socket.io/redis-adapter": "^8.3.0", "@types/pg": "^8.11.6", "bcrypt": "^5.1.1", "bullmq": "^5.7.14", @@ -64,6 +66,8 @@ "pg": "^8.11.5", "pg-tsquery": "^8.4.2", "postmark": "^4.0.2", + "react": "^18.3.1", + "redis": "^4.6.14", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "sanitize-filename-ts": "^1.0.2", diff --git a/apps/server/src/core/auth/services/token.service.ts b/apps/server/src/core/auth/services/token.service.ts index dc10a72..a037866 100644 --- a/apps/server/src/core/auth/services/token.service.ts +++ b/apps/server/src/core/auth/services/token.service.ts @@ -44,7 +44,7 @@ export class TokenService { async verifyJwt(token: string) { return this.jwtService.verifyAsync(token, { - secret: this.environmentService.getJwtSecret(), + secret: this.environmentService.getAppSecret(), }); } } diff --git a/apps/server/src/core/auth/strategies/jwt.strategy.ts b/apps/server/src/core/auth/strategies/jwt.strategy.ts index e521e03..f58be82 100644 --- a/apps/server/src/core/auth/strategies/jwt.strategy.ts +++ b/apps/server/src/core/auth/strategies/jwt.strategy.ts @@ -20,7 +20,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, - secretOrKey: environmentService.getJwtSecret(), + secretOrKey: environmentService.getAppSecret(), passReqToCallback: true, }); } diff --git a/apps/server/src/database/database.module.ts b/apps/server/src/database/database.module.ts index 5d5e366..0b0ba87 100644 --- a/apps/server/src/database/database.module.ts +++ b/apps/server/src/database/database.module.ts @@ -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 { @@ -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 { diff --git a/apps/server/src/database/migrations/20240324T085500-workspaces.ts b/apps/server/src/database/migrations/20240324T085500-workspaces.ts index c642213..981908b 100644 --- a/apps/server/src/database/migrations/20240324T085500-workspaces.ts +++ b/apps/server/src/database/migrations/20240324T085500-workspaces.ts @@ -16,9 +16,7 @@ export async function up(db: Kysely): Promise { .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()`), diff --git a/apps/server/src/database/migrations/20240324T085600-users.ts b/apps/server/src/database/migrations/20240324T085600-users.ts index 9ccfe6f..fbf0043 100644 --- a/apps/server/src/database/migrations/20240324T085600-users.ts +++ b/apps/server/src/database/migrations/20240324T085600-users.ts @@ -11,7 +11,7 @@ export async function up(db: Kysely): Promise { .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'), ) diff --git a/apps/server/src/database/migrations/20240324T085900-spaces.ts b/apps/server/src/database/migrations/20240324T085900-spaces.ts index cb396c2..2822a59 100644 --- a/apps/server/src/database/migrations/20240324T085900-spaces.ts +++ b/apps/server/src/database/migrations/20240324T085900-spaces.ts @@ -12,7 +12,7 @@ export async function up(db: Kysely): Promise { .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(), diff --git a/apps/server/src/database/migrations/20240324T086600-comments.ts b/apps/server/src/database/migrations/20240324T086600-comments.ts index 35848c7..59d0f53 100644 --- a/apps/server/src/database/migrations/20240324T086600-comments.ts +++ b/apps/server/src/database/migrations/20240324T086600-comments.ts @@ -19,6 +19,7 @@ export async function up(db: Kysely): Promise { .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()`), ) diff --git a/apps/server/src/database/repos/group/group.repo.ts b/apps/server/src/database/repos/group/group.repo.ts index 32f6936..67aaa94 100644 --- a/apps/server/src/database/repos/group/group.repo.ts +++ b/apps/server/src/database/repos/group/group.repo.ts @@ -54,7 +54,7 @@ export class GroupRepo { ): Promise { await this.db .updateTable('groups') - .set(updatableGroup) + .set({ ...updatableGroup, updatedAt: new Date() }) .where('id', '=', groupId) .where('workspaceId', '=', workspaceId) .execute(); diff --git a/apps/server/src/database/repos/page/page-history.repo.ts b/apps/server/src/database/repos/page/page-history.repo.ts index 028662f..cfefd48 100644 --- a/apps/server/src/database/repos/page/page-history.repo.ts +++ b/apps/server/src/database/repos/page/page-history.repo.ts @@ -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 { - return await this.db + async findById( + pageHistoryId: string, + trx?: KyselyTransaction, + ): Promise { + 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 { - 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 { + 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) { return jsonObjectFrom( eb diff --git a/apps/server/src/database/repos/page/page.repo.ts b/apps/server/src/database/repos/page/page.repo.ts index 1ad6404..d14a91d 100644 --- a/apps/server/src/database/repos/page/page.repo.ts +++ b/apps/server/src/database/repos/page/page.repo.ts @@ -46,9 +46,13 @@ export class PageRepo { includeContent?: boolean; includeYdoc?: boolean; includeSpace?: boolean; + withLock?: boolean; + trx?: KyselyTransaction; }, ): Promise { - 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); diff --git a/apps/server/src/database/repos/space/space.repo.ts b/apps/server/src/database/repos/space/space.repo.ts index 6700529..fc24bcb 100644 --- a/apps/server/src/database/repos/space/space.repo.ts +++ b/apps/server/src/database/repos/space/space.repo.ts @@ -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() diff --git a/apps/server/src/database/repos/user/user.repo.ts b/apps/server/src/database/repos/user/user.repo.ts index 2da4d92..9c450da 100644 --- a/apps/server/src/database/repos/user/user.repo.ts +++ b/apps/server/src/database/repos/user/user.repo.ts @@ -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(); diff --git a/apps/server/src/database/repos/workspace/workspace.repo.ts b/apps/server/src/database/repos/workspace/workspace.repo.ts index 5d1c8ab..7f8a0fd 100644 --- a/apps/server/src/database/repos/workspace/workspace.repo.ts +++ b/apps/server/src/database/repos/workspace/workspace.repo.ts @@ -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(); } diff --git a/apps/server/src/database/services/migration.service.ts b/apps/server/src/database/services/migration.service.ts new file mode 100644 index 0000000..3247e5f --- /dev/null +++ b/apps/server/src/database/services/migration.service.ts @@ -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 { + 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); + } + } +} diff --git a/apps/server/src/helpers/constants.ts b/apps/server/src/helpers/constants.ts new file mode 100644 index 0000000..6839962 --- /dev/null +++ b/apps/server/src/helpers/constants.ts @@ -0,0 +1,2 @@ +export const APP_DATA_PATH = 'data'; +export const LOCAL_STORAGE_PATH = `${APP_DATA_PATH}/storage`; diff --git a/apps/server/src/helpers/index.ts b/apps/server/src/helpers/index.ts index dedfea3..13e44d5 100644 --- a/apps/server/src/helpers/index.ts +++ b/apps/server/src/helpers/index.ts @@ -1,3 +1,4 @@ export * from './utils'; export * from './nanoid.utils'; export * from './file.helper'; +export * from './constants'; diff --git a/apps/server/src/helpers/utils.ts b/apps/server/src/helpers/utils.ts index 24c6bc0..33e45cc 100644 --- a/apps/server/src/helpers/utils.ts +++ b/apps/server/src/helpers/utils.ts @@ -3,12 +3,6 @@ import * as bcrypt from 'bcrypt'; export const envPath = path.resolve(process.cwd(), '..', '..', '.env'); -export function generateHostname(name: string): string { - let hostname = name.replace(/[^a-z0-9]/gi, '').toLowerCase(); - hostname = hostname.substring(0, 30); - return hostname; -} - export async function hashPassword(password: string) { const saltRounds = 12; return bcrypt.hash(password, saltRounds); @@ -21,15 +15,12 @@ export async function comparePasswordHash( return bcrypt.compare(plainPassword, passwordHash); } -export function getRandomInt(min = 4, max = 5) { - return Math.floor(Math.random() * (max - min + 1)) + min; -} - export type RedisConfig = { host: string; port: number; password?: string; }; + export function parseRedisUrl(redisUrl: string): RedisConfig { // format - redis[s]://[[username][:password]@][host][:port][/db-number] const { hostname, port, password } = new URL(redisUrl); @@ -37,3 +28,9 @@ export function parseRedisUrl(redisUrl: string): RedisConfig { return { host: hostname, port: portInt, password }; } + +export function createRetryStrategy() { + return function (times: number): number { + return Math.max(Math.min(Math.exp(times), 20000), 3000); + }; +} diff --git a/apps/server/src/integrations/environment/environment.service.ts b/apps/server/src/integrations/environment/environment.service.ts index 4222239..a61cdd4 100644 --- a/apps/server/src/integrations/environment/environment.service.ts +++ b/apps/server/src/integrations/environment/environment.service.ts @@ -5,8 +5,8 @@ import { ConfigService } from '@nestjs/config'; export class EnvironmentService { constructor(private configService: ConfigService) {} - getEnv(): string { - return this.configService.get('NODE_ENV'); + getNodeEnv(): string { + return this.configService.get('NODE_ENV', 'development'); } getAppUrl(): string { @@ -17,7 +17,7 @@ export class EnvironmentService { } getPort(): number { - return parseInt(this.configService.get('PORT')); + return parseInt(this.configService.get('PORT', '3000')); } getAppSecret(): string { @@ -28,20 +28,19 @@ export class EnvironmentService { return this.configService.get('DATABASE_URL'); } - getJwtSecret(): string { - return this.configService.get('JWT_SECRET_KEY'); + getRedisUrl(): string { + return this.configService.get( + 'REDIS_URL', + 'redis://localhost:6379', + ); } getJwtTokenExpiresIn(): string { - return this.configService.get('JWT_TOKEN_EXPIRES_IN'); + return this.configService.get('JWT_TOKEN_EXPIRES_IN', '30d'); } getStorageDriver(): string { - return this.configService.get('STORAGE_DRIVER'); - } - - getLocalStoragePath(): string { - return this.configService.get('LOCAL_STORAGE_PATH'); + return this.configService.get('STORAGE_DRIVER', 'local'); } getAwsS3AccessKeyId(): string { @@ -68,27 +67,12 @@ export class EnvironmentService { return this.configService.get('AWS_S3_URL'); } - getAwsS3UsePathStyleEndpoint(): boolean { - return this.configService.get('AWS_S3_USE_PATH_STYLE_ENDPOINT'); - } - - isCloud(): boolean { - const cloudConfig = this.configService - .get('CLOUD', 'false') - .toLowerCase(); - return cloudConfig === 'true'; - } - - isSelfHosted(): boolean { - return !this.isCloud(); - } - getMailDriver(): string { return this.configService.get('MAIL_DRIVER', 'log'); } getMailHost(): string { - return this.configService.get('MAIL_HOST', '127.0.0.1'); + return this.configService.get('MAIL_HOST'); } getMailPort(): number { @@ -115,10 +99,14 @@ export class EnvironmentService { return this.configService.get('POSTMARK_TOKEN'); } - getRedisUrl(): string { - return this.configService.get( - 'REDIS_URL', - 'redis://@127.0.0.1:6379', - ); + isCloud(): boolean { + const cloudConfig = this.configService + .get('CLOUD', 'false') + .toLowerCase(); + return cloudConfig === 'true'; + } + + isSelfHosted(): boolean { + return !this.isCloud(); } } diff --git a/apps/server/src/integrations/environment/environment.validation.ts b/apps/server/src/integrations/environment/environment.validation.ts index 457d7cb..949e1d4 100644 --- a/apps/server/src/integrations/environment/environment.validation.ts +++ b/apps/server/src/integrations/environment/environment.validation.ts @@ -1,14 +1,12 @@ -import { IsString, IsUrl, validateSync } from 'class-validator'; +import { IsNotEmpty, IsUrl, validateSync } from 'class-validator'; import { plainToInstance } from 'class-transformer'; export class EnvironmentVariables { - @IsString() - NODE_ENV: string; - + @IsNotEmpty() @IsUrl({ protocols: ['postgres', 'postgresql'], require_tld: false }) DATABASE_URL: string; - @IsString() + @IsNotEmpty() APP_SECRET: string; } diff --git a/apps/server/src/integrations/logger/internal-log-filter.ts b/apps/server/src/integrations/logger/internal-log-filter.ts new file mode 100644 index 0000000..1f6df4e --- /dev/null +++ b/apps/server/src/integrations/logger/internal-log-filter.ts @@ -0,0 +1,19 @@ +import { ConsoleLogger } from '@nestjs/common'; + +export class InternalLogFilter extends ConsoleLogger { + static contextsToIgnore = [ + 'InstanceLoader', + 'RoutesResolver', + 'RouterExplorer', + 'WebSocketsController', + ]; + + log(_: any, context?: string): void { + if ( + process.env.NODE_ENV !== 'production' || + !InternalLogFilter.contextsToIgnore.includes(context) + ) { + super.log.apply(this, arguments); + } + } +} diff --git a/apps/server/src/integrations/queue/queue.module.ts b/apps/server/src/integrations/queue/queue.module.ts index 4ba9257..b4ed917 100644 --- a/apps/server/src/integrations/queue/queue.module.ts +++ b/apps/server/src/integrations/queue/queue.module.ts @@ -1,7 +1,7 @@ import { Global, Module } from '@nestjs/common'; import { BullModule } from '@nestjs/bullmq'; import { EnvironmentService } from '../environment/environment.service'; -import { parseRedisUrl } from '../../helpers'; +import { createRetryStrategy, parseRedisUrl } from '../../helpers'; import { QueueName } from './constants'; @Global() @@ -15,9 +15,7 @@ import { QueueName } from './constants'; host: redisConfig.host, port: redisConfig.port, password: redisConfig.password, - retryStrategy: function (times: number) { - return Math.max(Math.min(Math.exp(times), 20000), 1000); - }, + retryStrategy: createRetryStrategy(), }, defaultJobOptions: { attempts: 3, diff --git a/apps/server/src/integrations/static/static.module.ts b/apps/server/src/integrations/static/static.module.ts index 583fbea..9ab43eb 100644 --- a/apps/server/src/integrations/static/static.module.ts +++ b/apps/server/src/integrations/static/static.module.ts @@ -30,7 +30,7 @@ export class StaticModule implements OnModuleInit { const windowVar = ''; const configString = { - ENV: this.environmentService.getEnv(), + ENV: this.environmentService.getNodeEnv(), APP_URL: this.environmentService.getAppUrl(), IS_CLOUD: this.environmentService.isCloud(), }; diff --git a/apps/server/src/integrations/storage/providers/storage.provider.ts b/apps/server/src/integrations/storage/providers/storage.provider.ts index 0004078..dfaab7f 100644 --- a/apps/server/src/integrations/storage/providers/storage.provider.ts +++ b/apps/server/src/integrations/storage/providers/storage.provider.ts @@ -11,6 +11,8 @@ import { StorageOption, } from '../interfaces'; import { LocalDriver, S3Driver } from '../drivers'; +import * as process from 'node:process'; +import { LOCAL_STORAGE_PATH } from '../../../helpers'; function createStorageDriver(disk: StorageConfig): StorageDriver { switch (disk.driver) { @@ -33,8 +35,7 @@ export const storageDriverConfigProvider = { return { driver, config: { - storagePath: - process.cwd() + '/' + environmentService.getLocalStoragePath(), + storagePath: process.cwd() + '/' + LOCAL_STORAGE_PATH, }, }; diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index f81ddf5..3e9fc24 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -7,6 +7,8 @@ import { import { NotFoundException, ValidationPipe } from '@nestjs/common'; import { TransformHttpResponseInterceptor } from './interceptors/http-response.interceptor'; import fastifyMultipart from '@fastify/multipart'; +import { WsRedisIoAdapter } from './ws/adapter/ws-redis.adapter'; +import { InternalLogFilter } from './integrations/logger/internal-log-filter'; async function bootstrap() { const app = await NestFactory.create( @@ -15,10 +17,18 @@ async function bootstrap() { ignoreTrailingSlash: true, ignoreDuplicateSlashes: true, }), + { + logger: new InternalLogFilter(), + }, ); app.setGlobalPrefix('api'); + const redisIoAdapter = new WsRedisIoAdapter(app); + await redisIoAdapter.connectToRedis(); + + app.useWebSocketAdapter(redisIoAdapter); + await app.register(fastifyMultipart); app @@ -50,7 +60,7 @@ async function bootstrap() { app.useGlobalInterceptors(new TransformHttpResponseInterceptor()); app.enableShutdownHooks(); - await app.listen(process.env.PORT || 3000); + await app.listen(process.env.PORT || 3000, '0.0.0.0'); } bootstrap(); diff --git a/apps/server/src/ws/adapter/ws-redis.adapter.ts b/apps/server/src/ws/adapter/ws-redis.adapter.ts new file mode 100644 index 0000000..09f00af --- /dev/null +++ b/apps/server/src/ws/adapter/ws-redis.adapter.ts @@ -0,0 +1,26 @@ +import { IoAdapter } from '@nestjs/platform-socket.io'; +import { ServerOptions } from 'socket.io'; +import { createAdapter } from '@socket.io/redis-adapter'; +import Redis, { RedisOptions } from 'ioredis'; +import { createRetryStrategy } from '../../helpers'; + +export class WsRedisIoAdapter extends IoAdapter { + private adapterConstructor: ReturnType; + + async connectToRedis(): Promise { + const options: RedisOptions = { + retryStrategy: createRetryStrategy(), + }; + + const pubClient = new Redis(process.env.REDIS_URL, options); + const subClient = new Redis(process.env.REDIS_URL, options); + + this.adapterConstructor = createAdapter(pubClient, subClient); + } + + createIOServer(port: number, options?: ServerOptions): any { + const server = super.createIOServer(port, options); + server.adapter(this.adapterConstructor); + return server; + } +} diff --git a/package.json b/package.json index 6b3d259..a166612 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "private": true, "scripts": { "build": "nx run-many -t build", + "start": "pnpm --filter ./apps/server run start:prod", "server:build": "nx run server:build", "client:build": "nx run client:build", "editor-ext:editor-ext": "nx run editor-ext:build", @@ -14,10 +15,12 @@ }, "dependencies": { "@docmost/editor-ext": "workspace:*", + "@hocuspocus/extension-redis": "^2.13.0", "@hocuspocus/provider": "^2.13.0", "@hocuspocus/server": "^2.13.0", "@hocuspocus/transformer": "^2.13.0", "@sindresorhus/slugify": "^2.2.1", + "@tiptap/core": "^2.4.0", "@tiptap/extension-code-block": "^2.4.0", "@tiptap/extension-collaboration": "^2.4.0", "@tiptap/extension-collaboration-cursor": "^2.4.0", @@ -45,7 +48,9 @@ "@tiptap/react": "^2.4.0", "@tiptap/starter-kit": "^2.4.0", "@tiptap/suggestion": "^2.4.0", + "cross-env": "^7.0.3", "fractional-indexing-jittered": "^0.9.1", + "ioredis": "^5.4.1", "tiptap-extension-global-drag-handle": "^0.1.8", "y-indexeddb": "^9.0.12", "yjs": "^13.6.15" diff --git a/packages/ee/LICENSE b/packages/ee/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 245c729..242f91f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@docmost/editor-ext': specifier: workspace:* version: link:packages/editor-ext + '@hocuspocus/extension-redis': + specifier: ^2.13.0 + version: 2.13.0(y-protocols@1.0.6(yjs@13.6.15))(yjs@13.6.15) '@hocuspocus/provider': specifier: ^2.13.0 version: 2.13.0(y-protocols@1.0.6(yjs@13.6.15))(yjs@13.6.15) @@ -23,6 +26,9 @@ importers: '@sindresorhus/slugify': specifier: ^2.2.1 version: 2.2.1 + '@tiptap/core': + specifier: ^2.4.0 + version: 2.4.0(@tiptap/pm@2.4.0) '@tiptap/extension-code-block': specifier: ^2.4.0 version: 2.4.0(@tiptap/core@2.4.0(@tiptap/pm@2.4.0))(@tiptap/pm@2.4.0) @@ -104,9 +110,15 @@ importers: '@tiptap/suggestion': specifier: ^2.4.0 version: 2.4.0(@tiptap/core@2.4.0(@tiptap/pm@2.4.0))(@tiptap/pm@2.4.0) + cross-env: + specifier: ^7.0.3 + version: 7.0.3 fractional-indexing-jittered: specifier: ^0.9.1 version: 0.9.1 + ioredis: + specifier: ^5.4.1 + version: 5.4.1 tiptap-extension-global-drag-handle: specifier: ^0.1.8 version: 0.1.8 @@ -119,7 +131,7 @@ importers: devDependencies: '@nx/js': specifier: 19.1.2 - version: 19.1.2(@babel/traverse@7.24.1)(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@20.14.0)(nx@19.1.2(@swc/core@1.3.101(@swc/helpers@0.5.2)))(typescript@5.4.5) + version: 19.1.2(@babel/traverse@7.24.6)(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@20.14.0)(nx@19.1.2(@swc/core@1.3.101(@swc/helpers@0.5.2)))(typescript@5.4.5) nx: specifier: 19.1.2 version: 19.1.2(@swc/core@1.3.101(@swc/helpers@0.5.2)) @@ -307,6 +319,9 @@ importers: '@nestjs/core': specifier: ^10.3.9 version: 10.3.9(@nestjs/common@10.3.9(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.3.9)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/event-emitter': + specifier: ^2.0.4 + version: 2.0.4(@nestjs/common@10.3.9(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.9(@nestjs/common@10.3.9(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.3.9)(reflect-metadata@0.2.2)(rxjs@7.8.1)) '@nestjs/jwt': specifier: ^10.2.0 version: 10.2.0(@nestjs/common@10.3.9(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)) @@ -331,6 +346,9 @@ importers: '@react-email/render': specifier: ^0.0.15 version: 0.0.15 + '@socket.io/redis-adapter': + specifier: ^8.3.0 + version: 8.3.0(socket.io-adapter@2.5.4) '@types/pg': specifier: ^8.11.6 version: 8.11.6 @@ -388,6 +406,12 @@ importers: postmark: specifier: ^4.0.2 version: 4.0.2 + react: + specifier: ^18.3.1 + version: 18.3.1 + redis: + specifier: ^4.6.14 + version: 4.6.14 reflect-metadata: specifier: ^0.2.2 version: 0.2.2 @@ -1875,6 +1899,12 @@ packages: '@hocuspocus/common@2.13.0': resolution: {integrity: sha512-gyuUZ/7Jlht/cokGSTYWmG2+ZR1amBkV7gZgY6Bw8ko0OkUbEFsRdzWaB6RVcYNv+ZvYcsbkM8JU6rPsNykMrg==} + '@hocuspocus/extension-redis@2.13.0': + resolution: {integrity: sha512-II4aPdET+jYKVO1RsCTIVOC2nTs4WnaVE7Jlh6QMyndQ23fW2+WbYqikjsZo+/n5LFtr0sev77o8CDMLL7ladQ==} + peerDependencies: + y-protocols: ^1.0.6 + yjs: ^13.6.8 + '@hocuspocus/provider@2.13.0': resolution: {integrity: sha512-xWRcyjW3koQajBCTEK3X2PrwaadUpaABsbYGKMWlr6I86pfknLJR8QIjEw1xgump9EbXjwtoXP6V6wuBkN4wkQ==} peerDependencies: @@ -2168,6 +2198,12 @@ packages: '@nestjs/websockets': optional: true + '@nestjs/event-emitter@2.0.4': + resolution: {integrity: sha512-quMiw8yOwoSul0pp3mOonGz8EyXWHSBTqBy8B0TbYYgpnG1Ix2wGUnuTksLWaaBiiOTDhciaZ41Y5fJZsSJE1Q==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/jwt@10.2.0': resolution: {integrity: sha512-x8cG90SURkEiLOehNaN2aRlotxT0KZESUliOPKKnjWiyJOcWurkF3w345WOX0P4MgFzUjGoZ1Sy0aZnxeihT0g==} peerDependencies: @@ -2862,6 +2898,35 @@ packages: peerDependencies: react: ^18.2.0 + '@redis/bloom@1.2.0': + resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/client@1.5.16': + resolution: {integrity: sha512-X1a3xQ5kEMvTib5fBrHKh6Y+pXbeKXqziYuxOUo1ojQNECg4M5Etd1qqyhMap+lFUOAh8S7UYevgJHOm4A+NOg==} + engines: {node: '>=14'} + + '@redis/graph@1.1.1': + resolution: {integrity: sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/json@1.0.6': + resolution: {integrity: sha512-rcZO3bfQbm2zPRpqo82XbW8zg4G/w4W3tI7X8Mqleq9goQjAGLL7q/1n1ZX4dXEAmORVZ4s1+uKLaUOg7LrUhw==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/search@1.1.6': + resolution: {integrity: sha512-mZXCxbTYKBQ3M2lZnEddwEAks0Kc7nauire8q20oA0oA/LoA+E/b5Y5KZn232ztPb1FkIGqo12vh3Lf+Vw5iTw==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/time-series@1.0.5': + resolution: {integrity: sha512-IFjIgTusQym2B5IZJG3XKr5llka7ey84fw/NOYqESP5WUfQs9zz1ww/9+qoz4ka/S6KcGBodzlCeZ5UImKbscg==} + peerDependencies: + '@redis/client': ^1.0.0 + '@remirror/core-constants@2.0.2': resolution: {integrity: sha512-dyHY+sMF0ihPus3O27ODd4+agdHMEmuRdyiZJ2CCWjPV5UFmn17ZbElvk6WOGVE4rdCJKZQCrPV2BcikOMLUGQ==} @@ -3164,6 +3229,12 @@ packages: '@socket.io/component-emitter@3.1.0': resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==} + '@socket.io/redis-adapter@8.3.0': + resolution: {integrity: sha512-ly0cra+48hDmChxmIpnESKrc94LjRL80TEmZVscuQ/WWkRP81nNj8W8cCGMqbI4L6NCuAaPRSzZF1a9GlAxxnA==} + engines: {node: '>=10.0.0'} + peerDependencies: + socket.io-adapter: ^2.5.4 + '@swc/core-darwin-arm64@1.3.101': resolution: {integrity: sha512-mNFK+uHNPRXSnfTOG34zJOeMl2waM4hF4a2NY7dkMXrPqw9CoJn4MwTXJcyMiSz1/BnNjjTCHF3Yhj0jPxmkzQ==} engines: {node: '>=10'} @@ -3263,11 +3334,6 @@ packages: peerDependencies: react: ^18.0.0 - '@tiptap/core@2.3.1': - resolution: {integrity: sha512-ycpQlmczAOc05TgB5sc3RUTEEBXAVmS8MR9PqQzg96qidaRfVkgE+2w4k7t83PMHl2duC0MGqOCy96pLYwSpeg==} - peerDependencies: - '@tiptap/pm': ^2.0.0 - '@tiptap/core@2.4.0': resolution: {integrity: sha512-YJSahk8pkxpCs8SflCZfTnJpE7IPyUWIylfgXM2DefjRQa5DZ+c6sNY0s/zbxKYFQ6AuHVX40r9pCfcqHChGxQ==} peerDependencies: @@ -4098,6 +4164,9 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + bowser@2.11.0: resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} @@ -4397,6 +4466,11 @@ packages: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} engines: {node: '>=12.0.0'} + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -4465,6 +4539,10 @@ packages: delegates@1.0.0: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + denque@1.5.1: + resolution: {integrity: sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==} + engines: {node: '>=0.10'} + denque@2.1.0: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} @@ -4765,6 +4843,9 @@ packages: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} + eventemitter2@6.4.9: + resolution: {integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==} + events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -4972,6 +5053,10 @@ packages: resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} engines: {node: '>=10'} + generic-pool@3.9.0: + resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} + engines: {node: '>= 4'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -5167,6 +5252,10 @@ packages: invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + ioredis@4.28.5: + resolution: {integrity: sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==} + engines: {node: '>=6'} + ioredis@5.4.1: resolution: {integrity: sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==} engines: {node: '>=12.22.0'} @@ -5639,6 +5728,9 @@ packages: lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + lodash.flatten@4.4.0: + resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} + lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} @@ -5951,6 +6043,9 @@ packages: resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} engines: {node: '>=0.10.0'} + notepack.io@3.0.1: + resolution: {integrity: sha512-TKC/8zH5pXIAMVQio2TvVDTtPRX+DJPHDqjRbxogtFiByHyzKmy96RA0JtCQJ+WouyyL4A10xomQzgbUT+1jCg==} + npm-package-arg@11.0.1: resolution: {integrity: sha512-M7s1BD4NxdAvBKUPqqRW957Xwcl/4Zvo8Aj+ANrzvIPzGJZElrH7Z//rSaec2ORcND6FHHLnZeY8qgTpXDMFQQ==} engines: {node: ^16.14.0 || >=18.0.0} @@ -6052,6 +6147,10 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -6597,6 +6696,9 @@ packages: resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} engines: {node: '>= 0.10'} + redis-commands@1.7.0: + resolution: {integrity: sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==} + redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -6605,6 +6707,13 @@ packages: resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} engines: {node: '>=4'} + redis@4.6.14: + resolution: {integrity: sha512-GrNg/e33HtsQwNXL7kJT+iNFPSwE1IPmd7wzV3j4f2z0EYxZfZE7FVTmUysgAtqQQtg5NXF5SNLR9OdO/UHOfw==} + + redlock@4.2.0: + resolution: {integrity: sha512-j+oQlG+dOwcetUt2WJWttu4CZVeRzUrcVcISFmEmfyuwCVSJ93rDT7YSgg7H7rnxwoRyk/jU46kycVka5tW7jA==} + engines: {node: '>=8.0.0'} + redux@4.2.1: resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} @@ -7238,6 +7347,10 @@ packages: uc.micro@2.0.0: resolution: {integrity: sha512-DffL94LsNOccVn4hyfRe5rdKa273swqeA5DJpMOeFmEn1wCDc7nAbbB0gXlgBCL7TNzeTv6G7XVWzan7iJtfig==} + uid2@1.0.0: + resolution: {integrity: sha512-+I6aJUv63YAcY9n4mQreLUt0d4lvwkkopDNmpomkAUz0fAkEMV9pRWxN0EjhW1YfRhcuyHg2v3mwddCDW1+LFQ==} + engines: {node: '>= 4.0.0'} + uid@2.0.2: resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} engines: {node: '>=8'} @@ -9383,6 +9496,21 @@ snapshots: dependencies: lib0: 0.2.93 + '@hocuspocus/extension-redis@2.13.0(y-protocols@1.0.6(yjs@13.6.15))(yjs@13.6.15)': + dependencies: + '@hocuspocus/server': 2.13.0(y-protocols@1.0.6(yjs@13.6.15))(yjs@13.6.15) + ioredis: 4.28.5 + kleur: 4.1.5 + lodash.debounce: 4.0.8 + redlock: 4.2.0 + uuid: 9.0.1 + y-protocols: 1.0.6(yjs@13.6.15) + yjs: 13.6.15 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + '@hocuspocus/provider@2.13.0(y-protocols@1.0.6(yjs@13.6.15))(yjs@13.6.15)': dependencies: '@hocuspocus/common': 2.13.0 @@ -9411,7 +9539,7 @@ snapshots: '@hocuspocus/transformer@2.13.0(@tiptap/pm@2.4.0)(y-prosemirror@1.2.3(prosemirror-model@1.19.4)(prosemirror-state@1.4.3)(prosemirror-view@1.32.7)(y-protocols@1.0.6(yjs@13.6.15))(yjs@13.6.15))(yjs@13.6.15)': dependencies: - '@tiptap/core': 2.3.1(@tiptap/pm@2.4.0) + '@tiptap/core': 2.4.0(@tiptap/pm@2.4.0) '@tiptap/pm': 2.4.0 '@tiptap/starter-kit': 2.4.0(@tiptap/pm@2.4.0) y-prosemirror: 1.2.3(prosemirror-model@1.19.4)(prosemirror-state@1.4.3)(prosemirror-view@1.32.7)(y-protocols@1.0.6(yjs@13.6.15))(yjs@13.6.15) @@ -9809,6 +9937,12 @@ snapshots: transitivePeerDependencies: - encoding + '@nestjs/event-emitter@2.0.4(@nestjs/common@10.3.9(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.9(@nestjs/common@10.3.9(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.3.9)(reflect-metadata@0.2.2)(rxjs@7.8.1))': + dependencies: + '@nestjs/common': 10.3.9(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.3.9(@nestjs/common@10.3.9(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.3.9)(reflect-metadata@0.2.2)(rxjs@7.8.1) + eventemitter2: 6.4.9 + '@nestjs/jwt@10.2.0(@nestjs/common@10.3.9(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))': dependencies: '@nestjs/common': 10.3.9(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -9943,9 +10077,9 @@ snapshots: transitivePeerDependencies: - nx - '@nrwl/js@19.1.2(@babel/traverse@7.24.1)(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@20.14.0)(nx@19.1.2(@swc/core@1.3.101(@swc/helpers@0.5.2)))(typescript@5.4.5)': + '@nrwl/js@19.1.2(@babel/traverse@7.24.6)(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@20.14.0)(nx@19.1.2(@swc/core@1.3.101(@swc/helpers@0.5.2)))(typescript@5.4.5)': dependencies: - '@nx/js': 19.1.2(@babel/traverse@7.24.1)(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@20.14.0)(nx@19.1.2(@swc/core@1.3.101(@swc/helpers@0.5.2)))(typescript@5.4.5) + '@nx/js': 19.1.2(@babel/traverse@7.24.6)(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@20.14.0)(nx@19.1.2(@swc/core@1.3.101(@swc/helpers@0.5.2)))(typescript@5.4.5) transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -9996,7 +10130,7 @@ snapshots: tslib: 2.6.2 yargs-parser: 21.1.1 - '@nx/js@19.1.2(@babel/traverse@7.24.1)(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@20.14.0)(nx@19.1.2(@swc/core@1.3.101(@swc/helpers@0.5.2)))(typescript@5.4.5)': + '@nx/js@19.1.2(@babel/traverse@7.24.6)(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@20.14.0)(nx@19.1.2(@swc/core@1.3.101(@swc/helpers@0.5.2)))(typescript@5.4.5)': dependencies: '@babel/core': 7.24.3 '@babel/plugin-proposal-decorators': 7.23.7(@babel/core@7.24.3) @@ -10005,12 +10139,12 @@ snapshots: '@babel/preset-env': 7.23.8(@babel/core@7.24.3) '@babel/preset-typescript': 7.23.3(@babel/core@7.24.3) '@babel/runtime': 7.23.7 - '@nrwl/js': 19.1.2(@babel/traverse@7.24.1)(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@20.14.0)(nx@19.1.2(@swc/core@1.3.101(@swc/helpers@0.5.2)))(typescript@5.4.5) + '@nrwl/js': 19.1.2(@babel/traverse@7.24.6)(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@20.14.0)(nx@19.1.2(@swc/core@1.3.101(@swc/helpers@0.5.2)))(typescript@5.4.5) '@nx/devkit': 19.1.2(nx@19.1.2(@swc/core@1.3.101(@swc/helpers@0.5.2))) '@nx/workspace': 19.1.2(@swc/core@1.3.101(@swc/helpers@0.5.2)) babel-plugin-const-enum: 1.2.0(@babel/core@7.24.3) babel-plugin-macros: 2.8.0 - babel-plugin-transform-typescript-metadata: 0.3.2(@babel/core@7.24.3)(@babel/traverse@7.24.1) + babel-plugin-transform-typescript-metadata: 0.3.2(@babel/core@7.24.3)(@babel/traverse@7.24.6) chalk: 4.1.2 columnify: 1.6.0 detect-port: 1.5.1 @@ -10529,6 +10663,32 @@ snapshots: dependencies: react: 18.3.1 + '@redis/bloom@1.2.0(@redis/client@1.5.16)': + dependencies: + '@redis/client': 1.5.16 + + '@redis/client@1.5.16': + dependencies: + cluster-key-slot: 1.1.2 + generic-pool: 3.9.0 + yallist: 4.0.0 + + '@redis/graph@1.1.1(@redis/client@1.5.16)': + dependencies: + '@redis/client': 1.5.16 + + '@redis/json@1.0.6(@redis/client@1.5.16)': + dependencies: + '@redis/client': 1.5.16 + + '@redis/search@1.1.6(@redis/client@1.5.16)': + dependencies: + '@redis/client': 1.5.16 + + '@redis/time-series@1.0.5(@redis/client@1.5.16)': + dependencies: + '@redis/client': 1.5.16 + '@remirror/core-constants@2.0.2': {} '@remirror/core-helpers@3.0.0': @@ -10938,6 +11098,15 @@ snapshots: '@socket.io/component-emitter@3.1.0': {} + '@socket.io/redis-adapter@8.3.0(socket.io-adapter@2.5.4)': + dependencies: + debug: 4.3.4 + notepack.io: 3.0.1 + socket.io-adapter: 2.5.4 + uid2: 1.0.0 + transitivePeerDependencies: + - supports-color + '@swc/core-darwin-arm64@1.3.101': optional: true @@ -11017,10 +11186,6 @@ snapshots: '@tanstack/query-core': 5.40.0 react: 18.3.1 - '@tiptap/core@2.3.1(@tiptap/pm@2.4.0)': - dependencies: - '@tiptap/pm': 2.4.0 - '@tiptap/core@2.4.0(@tiptap/pm@2.4.0)': dependencies: '@tiptap/pm': 2.4.0 @@ -11946,12 +12111,12 @@ snapshots: transitivePeerDependencies: - supports-color - babel-plugin-transform-typescript-metadata@0.3.2(@babel/core@7.24.3)(@babel/traverse@7.24.1): + babel-plugin-transform-typescript-metadata@0.3.2(@babel/core@7.24.3)(@babel/traverse@7.24.6): dependencies: '@babel/core': 7.24.3 '@babel/helper-plugin-utils': 7.24.0 optionalDependencies: - '@babel/traverse': 7.24.1 + '@babel/traverse': 7.24.6 babel-preset-current-node-syntax@1.0.1(@babel/core@7.24.3): dependencies: @@ -11997,6 +12162,8 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + bluebird@3.7.2: {} + bowser@2.11.0: {} brace-expansion@1.1.11: @@ -12295,6 +12462,10 @@ snapshots: dependencies: luxon: 3.4.4 + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.3 + cross-spawn@7.0.3: dependencies: path-key: 3.1.1 @@ -12341,6 +12512,8 @@ snapshots: delegates@1.0.0: {} + denque@1.5.1: {} + denque@2.1.0: {} depd@2.0.0: {} @@ -12684,6 +12857,8 @@ snapshots: event-target-shim@5.0.1: {} + eventemitter2@6.4.9: {} + events@3.3.0: {} execa@5.1.1: @@ -12942,6 +13117,8 @@ snapshots: strip-ansi: 6.0.1 wide-align: 1.1.5 + generic-pool@3.9.0: {} + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -13174,6 +13351,22 @@ snapshots: dependencies: loose-envify: 1.4.0 + ioredis@4.28.5: + dependencies: + cluster-key-slot: 1.1.2 + debug: 4.3.4 + denque: 1.5.1 + lodash.defaults: 4.2.0 + lodash.flatten: 4.4.0 + lodash.isarguments: 3.1.0 + p-map: 2.1.0 + redis-commands: 1.7.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + ioredis@5.4.1: dependencies: '@ioredis/commands': 1.2.0 @@ -13784,6 +13977,8 @@ snapshots: lodash.defaults@4.2.0: {} + lodash.flatten@4.4.0: {} + lodash.includes@4.3.0: {} lodash.isarguments@3.1.0: {} @@ -14048,6 +14243,8 @@ snapshots: normalize-range@0.1.2: {} + notepack.io@3.0.1: {} + npm-package-arg@11.0.1: dependencies: hosted-git-info: 7.0.1 @@ -14204,6 +14401,8 @@ snapshots: dependencies: p-limit: 3.1.0 + p-map@2.1.0: {} + p-try@2.2.0: {} parent-module@1.0.1: @@ -14833,12 +15032,27 @@ snapshots: dependencies: resolve: 1.22.8 + redis-commands@1.7.0: {} + redis-errors@1.2.0: {} redis-parser@3.0.0: dependencies: redis-errors: 1.2.0 + redis@4.6.14: + dependencies: + '@redis/bloom': 1.2.0(@redis/client@1.5.16) + '@redis/client': 1.5.16 + '@redis/graph': 1.1.1(@redis/client@1.5.16) + '@redis/json': 1.0.6(@redis/client@1.5.16) + '@redis/search': 1.1.6(@redis/client@1.5.16) + '@redis/time-series': 1.0.5(@redis/client@1.5.16) + + redlock@4.2.0: + dependencies: + bluebird: 3.7.2 + redux@4.2.1: dependencies: '@babel/runtime': 7.23.7 @@ -15522,6 +15736,8 @@ snapshots: uc.micro@2.0.0: {} + uid2@1.0.0: {} + uid@2.0.2: dependencies: '@lukeed/csprng': 1.1.0