mirror of
https://github.com/docmost/docmost.git
synced 2026-08-22 10:22:12 +10:00
merge main
This commit is contained in:
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
Global,
|
||||
Logger,
|
||||
Module,
|
||||
OnApplicationBootstrap,
|
||||
BeforeApplicationShutdown,
|
||||
} from '@nestjs/common';
|
||||
import { Global, Logger, Module, OnApplicationBootstrap } from '@nestjs/common';
|
||||
import { InjectKysely, KyselyModule } from 'nestjs-kysely';
|
||||
import { EnvironmentService } from '../integrations/environment/environment.service';
|
||||
import { CamelCasePlugin, LogEvent, sql } from 'kysely';
|
||||
@@ -15,17 +9,24 @@ import { GroupUserRepo } from '@docmost/db/repos/group/group-user.repo';
|
||||
import { SpaceRepo } from '@docmost/db/repos/space/space.repo';
|
||||
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
||||
import { PageRepo } from './repos/page/page.repo';
|
||||
import { PagePermissionRepo } from './repos/page/page-permission.repo';
|
||||
import { CommentRepo } from './repos/comment/comment.repo';
|
||||
import { PageTransclusionsRepo } from './repos/page-transclusions/page-transclusions.repo';
|
||||
import { PageTransclusionReferencesRepo } from './repos/page-transclusions/page-transclusion-references.repo';
|
||||
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';
|
||||
import { UserTokenRepo } from './repos/user-token/user-token.repo';
|
||||
import { UserSessionRepo } from '@docmost/db/repos/session/user-session.repo';
|
||||
import { BacklinkRepo } from '@docmost/db/repos/backlink/backlink.repo';
|
||||
import { ShareRepo } from '@docmost/db/repos/share/share.repo';
|
||||
import { NotificationRepo } from '@docmost/db/repos/notification/notification.repo';
|
||||
import { WatcherRepo } from '@docmost/db/repos/watcher/watcher.repo';
|
||||
import { LabelRepo } from '@docmost/db/repos/label/label.repo';
|
||||
import { FavoriteRepo } from '@docmost/db/repos/favorite/favorite.repo';
|
||||
import { TemplateRepo } from '@docmost/db/repos/template/template.repo';
|
||||
import { PageListener } from '@docmost/db/listeners/page.listener';
|
||||
import { PostgresJSDialect } from 'kysely-postgres-js';
|
||||
import * as postgres from 'postgres';
|
||||
@@ -76,14 +77,21 @@ import { normalizePostgresUrl } from '../common/helpers';
|
||||
SpaceRepo,
|
||||
SpaceMemberRepo,
|
||||
PageRepo,
|
||||
PagePermissionRepo,
|
||||
PageTransclusionsRepo,
|
||||
PageTransclusionReferencesRepo,
|
||||
PageHistoryRepo,
|
||||
CommentRepo,
|
||||
FavoriteRepo,
|
||||
AttachmentRepo,
|
||||
UserTokenRepo,
|
||||
UserSessionRepo,
|
||||
BacklinkRepo,
|
||||
ShareRepo,
|
||||
NotificationRepo,
|
||||
WatcherRepo,
|
||||
LabelRepo,
|
||||
TemplateRepo,
|
||||
PageListener,
|
||||
],
|
||||
exports: [
|
||||
@@ -94,19 +102,24 @@ import { normalizePostgresUrl } from '../common/helpers';
|
||||
SpaceRepo,
|
||||
SpaceMemberRepo,
|
||||
PageRepo,
|
||||
PagePermissionRepo,
|
||||
PageTransclusionsRepo,
|
||||
PageTransclusionReferencesRepo,
|
||||
PageHistoryRepo,
|
||||
CommentRepo,
|
||||
FavoriteRepo,
|
||||
AttachmentRepo,
|
||||
UserTokenRepo,
|
||||
UserSessionRepo,
|
||||
BacklinkRepo,
|
||||
ShareRepo,
|
||||
NotificationRepo,
|
||||
WatcherRepo,
|
||||
LabelRepo,
|
||||
TemplateRepo,
|
||||
],
|
||||
})
|
||||
export class DatabaseModule
|
||||
implements OnApplicationBootstrap, BeforeApplicationShutdown
|
||||
{
|
||||
export class DatabaseModule implements OnApplicationBootstrap {
|
||||
private readonly logger = new Logger(DatabaseModule.name);
|
||||
|
||||
constructor(
|
||||
@@ -123,12 +136,6 @@ export class DatabaseModule
|
||||
}
|
||||
}
|
||||
|
||||
async beforeApplicationShutdown(): Promise<void> {
|
||||
if (this.db) {
|
||||
await this.db.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
async establishConnection() {
|
||||
const retryAttempts = 15;
|
||||
const retryDelay = 3000;
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.createTable('page_access')
|
||||
.addColumn('id', 'uuid', (col) =>
|
||||
col.primaryKey().defaultTo(sql`gen_uuid_v7()`),
|
||||
)
|
||||
.addColumn('page_id', 'uuid', (col) =>
|
||||
col.notNull().unique().references('pages.id').onDelete('cascade'),
|
||||
)
|
||||
.addColumn('workspace_id', 'uuid', (col) =>
|
||||
col.notNull().references('workspaces.id').onDelete('cascade'),
|
||||
)
|
||||
.addColumn('space_id', 'uuid', (col) =>
|
||||
col.notNull().references('spaces.id').onDelete('cascade'),
|
||||
)
|
||||
.addColumn('access_level', 'varchar', (col) => col.notNull())
|
||||
.addColumn('creator_id', 'uuid', (col) =>
|
||||
col.references('users.id').onDelete('set null'),
|
||||
)
|
||||
.addColumn('created_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addColumn('updated_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createTable('page_permissions')
|
||||
.addColumn('id', 'uuid', (col) =>
|
||||
col.primaryKey().defaultTo(sql`gen_uuid_v7()`),
|
||||
)
|
||||
.addColumn('page_access_id', 'uuid', (col) =>
|
||||
col.notNull().references('page_access.id').onDelete('cascade'),
|
||||
)
|
||||
.addColumn('user_id', 'uuid', (col) =>
|
||||
col.references('users.id').onDelete('cascade'),
|
||||
)
|
||||
.addColumn('group_id', 'uuid', (col) =>
|
||||
col.references('groups.id').onDelete('cascade'),
|
||||
)
|
||||
.addColumn('role', 'varchar', (col) => col.notNull())
|
||||
.addColumn('added_by_id', 'uuid', (col) =>
|
||||
col.references('users.id').onDelete('set null'),
|
||||
)
|
||||
.addColumn('created_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addColumn('updated_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addUniqueConstraint('page_access_user_unique', [
|
||||
'page_access_id',
|
||||
'user_id',
|
||||
])
|
||||
.addUniqueConstraint('page_access_group_unique', [
|
||||
'page_access_id',
|
||||
'group_id',
|
||||
])
|
||||
.addCheckConstraint(
|
||||
'allow_either_user_id_or_group_id_check',
|
||||
sql`((user_id IS NOT NULL AND group_id IS NULL) OR (user_id IS NULL AND group_id IS NOT NULL))`,
|
||||
)
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_page_access_space')
|
||||
.on('page_access')
|
||||
.column('space_id')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_page_permissions_user')
|
||||
.on('page_permissions')
|
||||
.column('user_id')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_page_permissions_group')
|
||||
.on('page_permissions')
|
||||
.column('group_id')
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await db.schema.dropTable('page_permissions').ifExists().execute();
|
||||
await db.schema.dropTable('page_access').ifExists().execute();
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.createTable('audit')
|
||||
.ifNotExists()
|
||||
.addColumn('id', 'uuid', (col) =>
|
||||
col.primaryKey().defaultTo(sql`gen_uuid_v7()`),
|
||||
)
|
||||
.addColumn('workspace_id', 'uuid', (col) =>
|
||||
col.notNull().references('workspaces.id').onDelete('cascade'),
|
||||
)
|
||||
.addColumn('actor_id', 'uuid')
|
||||
.addColumn('actor_type', 'varchar', (col) =>
|
||||
col.notNull().defaultTo('user'),
|
||||
)
|
||||
.addColumn('event', 'varchar', (col) => col.notNull())
|
||||
.addColumn('resource_type', 'varchar', (col) => col.notNull())
|
||||
.addColumn('resource_id', 'uuid')
|
||||
.addColumn('space_id', 'uuid')
|
||||
.addColumn('changes', 'jsonb')
|
||||
.addColumn('metadata', 'jsonb')
|
||||
.addColumn('ip_address', sql`inet`)
|
||||
.addColumn('created_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_audit_workspace_id')
|
||||
.ifNotExists()
|
||||
.on('audit')
|
||||
.columns(['workspace_id', 'id desc'])
|
||||
.execute();
|
||||
|
||||
// add new workspace columns
|
||||
await db.schema
|
||||
.alterTable('workspaces')
|
||||
.addColumn('audit_retention_days', 'int8', (col) => col)
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.alterTable('workspaces')
|
||||
.addColumn('trash_retention_days', 'int8', (col) => col)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.alterTable('workspaces')
|
||||
.dropColumn('audit_retention_days')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.alterTable('workspaces')
|
||||
.dropColumn('trash_retention_days')
|
||||
.execute();
|
||||
|
||||
await db.schema.dropTable('audit').execute();
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.createTable('user_sessions')
|
||||
.addColumn('id', 'uuid', (col) =>
|
||||
col.primaryKey().defaultTo(sql`gen_uuid_v7()`),
|
||||
)
|
||||
.addColumn('user_id', 'uuid', (col) =>
|
||||
col.notNull().references('users.id').onDelete('cascade'),
|
||||
)
|
||||
.addColumn('workspace_id', 'uuid', (col) =>
|
||||
col.notNull().references('workspaces.id').onDelete('cascade'),
|
||||
)
|
||||
.addColumn('device_name', 'varchar')
|
||||
.addColumn('user_agent', 'text')
|
||||
.addColumn('ip_address', sql`inet`)
|
||||
.addColumn('geo_location', 'varchar')
|
||||
.addColumn('last_active_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addColumn('expires_at', 'timestamptz', (col) => col.notNull())
|
||||
.addColumn('metadata', 'jsonb')
|
||||
.addColumn('revoked_at', 'timestamptz')
|
||||
.addColumn('created_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.execute();
|
||||
|
||||
await sql`
|
||||
CREATE INDEX idx_user_sessions_active
|
||||
ON user_sessions (user_id, workspace_id, last_active_at DESC)
|
||||
WHERE revoked_at IS NULL
|
||||
`.execute(db);
|
||||
|
||||
await sql`
|
||||
CREATE INDEX idx_user_sessions_revoked
|
||||
ON user_sessions (expires_at)
|
||||
WHERE revoked_at IS NOT NULL
|
||||
`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await db.schema.dropTable('user_sessions').execute();
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
import { type Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.createIndex('idx_group_users_user_id')
|
||||
.ifNotExists()
|
||||
.on('group_users')
|
||||
.column('user_id')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_space_members_user_id')
|
||||
.ifNotExists()
|
||||
.on('space_members')
|
||||
.column('user_id')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_space_members_group_id')
|
||||
.ifNotExists()
|
||||
.on('space_members')
|
||||
.column('group_id')
|
||||
.execute();
|
||||
|
||||
// Page tree
|
||||
await sql`
|
||||
CREATE INDEX IF NOT EXISTS idx_pages_space_parent_position
|
||||
ON pages (space_id, parent_page_id, position COLLATE "C")
|
||||
WHERE deleted_at IS NULL
|
||||
`.execute(db);
|
||||
|
||||
await sql`
|
||||
CREATE INDEX IF NOT EXISTS idx_pages_parent_page_id
|
||||
ON pages (parent_page_id)
|
||||
WHERE deleted_at IS NULL
|
||||
`.execute(db);
|
||||
|
||||
// Recent pages query
|
||||
await sql`
|
||||
CREATE INDEX IF NOT EXISTS idx_pages_space_updated
|
||||
ON pages (space_id, updated_at DESC)
|
||||
WHERE deleted_at IS NULL
|
||||
`.execute(db);
|
||||
|
||||
// Trash view
|
||||
await sql`
|
||||
CREATE INDEX IF NOT EXISTS idx_pages_space_deleted
|
||||
ON pages (space_id, deleted_at DESC)
|
||||
WHERE deleted_at IS NOT NULL
|
||||
`.execute(db);
|
||||
|
||||
await sql`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_workspaces_hostname_lower
|
||||
ON workspaces (LOWER(hostname))
|
||||
`.execute(db);
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_workspaces_created_at')
|
||||
.ifNotExists()
|
||||
.on('workspaces')
|
||||
.column('created_at')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_users_workspace_deleted')
|
||||
.ifNotExists()
|
||||
.on('users')
|
||||
.columns(['workspace_id', 'deleted_at'])
|
||||
.execute();
|
||||
|
||||
await sql`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_spaces_slug_lower_workspace
|
||||
ON spaces (LOWER(slug), workspace_id)
|
||||
`.execute(db);
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_spaces_workspace_id')
|
||||
.ifNotExists()
|
||||
.on('spaces')
|
||||
.column('workspace_id')
|
||||
.execute();
|
||||
|
||||
await sql`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_groups_name_lower_workspace
|
||||
ON groups (LOWER(name), workspace_id)
|
||||
`.execute(db);
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_groups_workspace_id')
|
||||
.ifNotExists()
|
||||
.on('groups')
|
||||
.column('workspace_id')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_shares_page_id')
|
||||
.ifNotExists()
|
||||
.on('shares')
|
||||
.column('page_id')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_attachments_page_id')
|
||||
.ifNotExists()
|
||||
.on('attachments')
|
||||
.column('page_id')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_attachments_space_id')
|
||||
.ifNotExists()
|
||||
.on('attachments')
|
||||
.column('space_id')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_comments_page_id')
|
||||
.ifNotExists()
|
||||
.on('comments')
|
||||
.column('page_id')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_comments_parent_comment_id')
|
||||
.ifNotExists()
|
||||
.on('comments')
|
||||
.column('parent_comment_id')
|
||||
.execute();
|
||||
|
||||
await sql`
|
||||
CREATE INDEX IF NOT EXISTS idx_page_history_page_created
|
||||
ON page_history (page_id, created_at DESC)
|
||||
`.execute(db);
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_attachments_workspace_id')
|
||||
.ifNotExists()
|
||||
.on('attachments')
|
||||
.column('workspace_id')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_backlinks_target_page_id')
|
||||
.ifNotExists()
|
||||
.on('backlinks')
|
||||
.column('target_page_id')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_pages_workspace_id')
|
||||
.ifNotExists()
|
||||
.on('pages')
|
||||
.column('workspace_id')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_pages_creator_id')
|
||||
.ifNotExists()
|
||||
.on('pages')
|
||||
.column('creator_id')
|
||||
.execute();
|
||||
|
||||
// Notifications: FK cascade from pages, spaces, comments
|
||||
await db.schema
|
||||
.createIndex('idx_notifications_page_id')
|
||||
.ifNotExists()
|
||||
.on('notifications')
|
||||
.column('page_id')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_notifications_space_id')
|
||||
.ifNotExists()
|
||||
.on('notifications')
|
||||
.column('space_id')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_notifications_comment_id')
|
||||
.ifNotExists()
|
||||
.on('notifications')
|
||||
.column('comment_id')
|
||||
.execute();
|
||||
|
||||
// Watchers: cleanup queries and FK cascade
|
||||
await db.schema
|
||||
.createIndex('idx_watchers_user_workspace')
|
||||
.ifNotExists()
|
||||
.on('watchers')
|
||||
.columns(['user_id', 'workspace_id'])
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_watchers_space_id')
|
||||
.ifNotExists()
|
||||
.on('watchers')
|
||||
.column('space_id')
|
||||
.execute();
|
||||
|
||||
// Auth providers: all queries filter by workspaceId
|
||||
await db.schema
|
||||
.createIndex('idx_auth_providers_workspace_id')
|
||||
.ifNotExists()
|
||||
.on('auth_providers')
|
||||
.column('workspace_id')
|
||||
.execute();
|
||||
|
||||
// Auth accounts: SSO login lookup by provider user
|
||||
await db.schema
|
||||
.createIndex('idx_auth_accounts_provider_user_id')
|
||||
.ifNotExists()
|
||||
.on('auth_accounts')
|
||||
.columns(['provider_user_id', 'auth_provider_id'])
|
||||
.execute();
|
||||
|
||||
// Workspace invitations: listing and SSO lookup
|
||||
await db.schema
|
||||
.createIndex('idx_workspace_invitations_workspace_id')
|
||||
.ifNotExists()
|
||||
.on('workspace_invitations')
|
||||
.column('workspace_id')
|
||||
.execute();
|
||||
|
||||
// API keys: query and FK cascade
|
||||
await db.schema
|
||||
.createIndex('idx_api_keys_workspace_id')
|
||||
.ifNotExists()
|
||||
.on('api_keys')
|
||||
.column('workspace_id')
|
||||
.execute();
|
||||
|
||||
// User sessions: delete queries and FK cascade on all session states
|
||||
await db.schema
|
||||
.createIndex('idx_user_sessions_user_workspace')
|
||||
.ifNotExists()
|
||||
.on('user_sessions')
|
||||
.columns(['user_id', 'workspace_id'])
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await db.schema.dropIndex('idx_group_users_user_id').ifExists().execute();
|
||||
await db.schema.dropIndex('idx_space_members_user_id').ifExists().execute();
|
||||
await db.schema.dropIndex('idx_space_members_group_id').ifExists().execute();
|
||||
await db.schema
|
||||
.dropIndex('idx_pages_space_parent_position')
|
||||
.ifExists()
|
||||
.execute();
|
||||
await db.schema.dropIndex('idx_pages_parent_page_id').ifExists().execute();
|
||||
await db.schema.dropIndex('idx_pages_space_updated').ifExists().execute();
|
||||
await db.schema.dropIndex('idx_pages_space_deleted').ifExists().execute();
|
||||
await db.schema
|
||||
.dropIndex('idx_workspaces_hostname_lower')
|
||||
.ifExists()
|
||||
.execute();
|
||||
await db.schema.dropIndex('idx_workspaces_created_at').ifExists().execute();
|
||||
await db.schema
|
||||
.dropIndex('idx_users_workspace_deleted')
|
||||
.ifExists()
|
||||
.execute();
|
||||
await db.schema
|
||||
.dropIndex('idx_spaces_slug_lower_workspace')
|
||||
.ifExists()
|
||||
.execute();
|
||||
await db.schema
|
||||
.dropIndex('idx_spaces_workspace_id')
|
||||
.ifExists()
|
||||
.execute();
|
||||
await db.schema
|
||||
.dropIndex('idx_groups_name_lower_workspace')
|
||||
.ifExists()
|
||||
.execute();
|
||||
await db.schema.dropIndex('idx_groups_workspace_id').ifExists().execute();
|
||||
await db.schema.dropIndex('idx_shares_page_id').ifExists().execute();
|
||||
await db.schema.dropIndex('idx_attachments_page_id').ifExists().execute();
|
||||
await db.schema.dropIndex('idx_attachments_space_id').ifExists().execute();
|
||||
await db.schema.dropIndex('idx_comments_page_id').ifExists().execute();
|
||||
await db.schema
|
||||
.dropIndex('idx_comments_parent_comment_id')
|
||||
.ifExists()
|
||||
.execute();
|
||||
await db.schema
|
||||
.dropIndex('idx_page_history_page_created')
|
||||
.ifExists()
|
||||
.execute();
|
||||
await db.schema
|
||||
.dropIndex('idx_attachments_workspace_id')
|
||||
.ifExists()
|
||||
.execute();
|
||||
await db.schema
|
||||
.dropIndex('idx_backlinks_target_page_id')
|
||||
.ifExists()
|
||||
.execute();
|
||||
await db.schema.dropIndex('idx_pages_workspace_id').ifExists().execute();
|
||||
await db.schema.dropIndex('idx_pages_creator_id').ifExists().execute();
|
||||
await db.schema
|
||||
.dropIndex('idx_notifications_page_id')
|
||||
.ifExists()
|
||||
.execute();
|
||||
await db.schema
|
||||
.dropIndex('idx_notifications_space_id')
|
||||
.ifExists()
|
||||
.execute();
|
||||
await db.schema
|
||||
.dropIndex('idx_notifications_comment_id')
|
||||
.ifExists()
|
||||
.execute();
|
||||
await db.schema
|
||||
.dropIndex('idx_watchers_user_workspace')
|
||||
.ifExists()
|
||||
.execute();
|
||||
await db.schema.dropIndex('idx_watchers_space_id').ifExists().execute();
|
||||
await db.schema
|
||||
.dropIndex('idx_auth_providers_workspace_id')
|
||||
.ifExists()
|
||||
.execute();
|
||||
await db.schema
|
||||
.dropIndex('idx_auth_accounts_provider_user_id')
|
||||
.ifExists()
|
||||
.execute();
|
||||
await db.schema
|
||||
.dropIndex('idx_workspace_invitations_workspace_id')
|
||||
.ifExists()
|
||||
.execute();
|
||||
await db.schema
|
||||
.dropIndex('idx_api_keys_workspace_id')
|
||||
.ifExists()
|
||||
.execute();
|
||||
await db.schema
|
||||
.dropIndex('idx_user_sessions_user_workspace')
|
||||
.ifExists()
|
||||
.execute();
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { type Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.createTable('ai_chats')
|
||||
.ifNotExists()
|
||||
.addColumn('id', 'uuid', (col) =>
|
||||
col.primaryKey().defaultTo(sql`gen_uuid_v7()`),
|
||||
)
|
||||
.addColumn('workspace_id', 'uuid', (col) =>
|
||||
col.references('workspaces.id').onDelete('cascade').notNull(),
|
||||
)
|
||||
.addColumn('creator_id', 'uuid', (col) =>
|
||||
col.references('users.id').notNull(),
|
||||
)
|
||||
.addColumn('title', 'varchar', (col) => col)
|
||||
.addColumn('created_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addColumn('updated_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addColumn('deleted_at', 'timestamptz', (col) => col)
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_ai_chats_workspace_creator')
|
||||
.ifNotExists()
|
||||
.on('ai_chats')
|
||||
.columns(['workspace_id', 'creator_id', 'id'])
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createTable('ai_chat_messages')
|
||||
.ifNotExists()
|
||||
.addColumn('id', 'uuid', (col) =>
|
||||
col.primaryKey().defaultTo(sql`gen_uuid_v7()`),
|
||||
)
|
||||
.addColumn('chat_id', 'uuid', (col) =>
|
||||
col.references('ai_chats.id').onDelete('cascade').notNull(),
|
||||
)
|
||||
.addColumn('workspace_id', 'uuid', (col) =>
|
||||
col.references('workspaces.id').onDelete('cascade').notNull(),
|
||||
)
|
||||
.addColumn('user_id', 'uuid', (col) =>
|
||||
col.references('users.id').onDelete('set null'),
|
||||
)
|
||||
.addColumn('role', 'varchar', (col) => col.notNull())
|
||||
.addColumn('content', 'text', (col) => col)
|
||||
.addColumn('tool_calls', 'jsonb', (col) => col)
|
||||
.addColumn('metadata', 'jsonb', (col) => col)
|
||||
.addColumn('tsv', sql`tsvector`, (col) => col)
|
||||
.addColumn('created_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addColumn('updated_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addColumn('deleted_at', 'timestamptz', (col) => col)
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_ai_chat_messages_chat_id')
|
||||
.ifNotExists()
|
||||
.on('ai_chat_messages')
|
||||
.columns(['chat_id', 'id'])
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_ai_chat_messages_tsv')
|
||||
.ifNotExists()
|
||||
.on('ai_chat_messages')
|
||||
.using('GIN')
|
||||
.column('tsv')
|
||||
.execute();
|
||||
|
||||
//ts-vector
|
||||
await sql`
|
||||
CREATE OR REPLACE FUNCTION ai_chat_messages_tsvector_trigger() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
NEW.tsv := to_tsvector('english', f_unaccent(substring(coalesce(NEW.content, ''), 1, 100000)));
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
`.execute(db);
|
||||
|
||||
await sql`
|
||||
CREATE OR REPLACE TRIGGER ai_chat_messages_tsvector_update
|
||||
BEFORE INSERT OR UPDATE ON ai_chat_messages
|
||||
FOR EACH ROW EXECUTE FUNCTION ai_chat_messages_tsvector_trigger();
|
||||
`.execute(db);
|
||||
|
||||
await db.schema
|
||||
.alterTable('attachments')
|
||||
.addColumn('ai_chat_id', 'uuid', (col) => col)
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_attachments_ai_chat_id')
|
||||
.ifNotExists()
|
||||
.on('attachments')
|
||||
.column('ai_chat_id')
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await db.schema.dropIndex('idx_attachments_ai_chat_id').execute();
|
||||
await db.schema.alterTable('attachments').dropColumn('ai_chat_id').execute();
|
||||
|
||||
await sql`DROP TRIGGER IF EXISTS ai_chat_messages_tsvector_update ON ai_chat_messages`.execute(
|
||||
db,
|
||||
);
|
||||
await sql`DROP FUNCTION IF EXISTS ai_chat_messages_tsvector_trigger`.execute(
|
||||
db,
|
||||
);
|
||||
await db.schema.dropTable('ai_chat_messages').execute();
|
||||
await db.schema.dropTable('ai_chats').execute();
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.createTable('templates')
|
||||
.addColumn('id', 'uuid', (col) =>
|
||||
col.primaryKey().defaultTo(sql`gen_uuid_v7()`),
|
||||
)
|
||||
.addColumn('title', 'varchar')
|
||||
.addColumn('description', 'text')
|
||||
.addColumn('content', 'jsonb')
|
||||
.addColumn('ydoc', 'bytea')
|
||||
.addColumn('icon', 'varchar')
|
||||
.addColumn('space_id', 'uuid', (col) =>
|
||||
col.references('spaces.id').onDelete('cascade'),
|
||||
)
|
||||
.addColumn('workspace_id', 'uuid', (col) =>
|
||||
col.notNull().references('workspaces.id').onDelete('cascade'),
|
||||
)
|
||||
.addColumn('creator_id', 'uuid', (col) =>
|
||||
col.references('users.id').onDelete('set null'),
|
||||
)
|
||||
.addColumn('last_updated_by_id', 'uuid', (col) =>
|
||||
col.references('users.id').onDelete('set null'),
|
||||
)
|
||||
.addColumn('collaborator_ids', sql`uuid[]`)
|
||||
.addColumn('text_content', 'text', (col) => col)
|
||||
.addColumn('tsv', sql`tsvector`, (col) => col)
|
||||
.addColumn('created_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addColumn('updated_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addColumn('deleted_at', 'timestamptz')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_templates_workspace_id')
|
||||
.on('templates')
|
||||
.columns(['workspace_id'])
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_templates_space_id')
|
||||
.on('templates')
|
||||
.columns(['space_id'])
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('templates_tsv_idx')
|
||||
.on('templates')
|
||||
.using('GIN')
|
||||
.column('tsv')
|
||||
.execute();
|
||||
|
||||
await sql`
|
||||
CREATE OR REPLACE FUNCTION templates_tsvector_trigger() RETURNS trigger AS $$
|
||||
begin
|
||||
new.tsv :=
|
||||
setweight(to_tsvector('english', f_unaccent(coalesce(new.title, ''))), 'A') ||
|
||||
setweight(to_tsvector('english', f_unaccent(substring(coalesce(new.text_content, ''), 1, 1000000))), 'B');
|
||||
return new;
|
||||
end;
|
||||
$$ LANGUAGE plpgsql;
|
||||
`.execute(db);
|
||||
|
||||
await sql`CREATE OR REPLACE TRIGGER templates_tsvector_update BEFORE INSERT OR UPDATE
|
||||
ON templates FOR EACH ROW EXECUTE FUNCTION templates_tsvector_trigger();`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`DROP TRIGGER IF EXISTS templates_tsvector_update ON templates`.execute(db);
|
||||
await sql`DROP FUNCTION IF EXISTS templates_tsvector_trigger`.execute(db);
|
||||
await db.schema.dropTable('templates').execute();
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { type Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.createTable('favorites')
|
||||
.addColumn('id', 'uuid', (col) =>
|
||||
col.primaryKey().defaultTo(sql`gen_uuid_v7()`),
|
||||
)
|
||||
.addColumn('user_id', 'uuid', (col) =>
|
||||
col.references('users.id').onDelete('cascade').notNull(),
|
||||
)
|
||||
.addColumn('page_id', 'uuid', (col) =>
|
||||
col.references('pages.id').onDelete('cascade'),
|
||||
)
|
||||
.addColumn('space_id', 'uuid', (col) =>
|
||||
col.references('spaces.id').onDelete('cascade'),
|
||||
)
|
||||
.addColumn('template_id', 'uuid', (col) =>
|
||||
col.references('templates.id').onDelete('cascade'),
|
||||
)
|
||||
.addColumn('type', 'varchar', (col) => col.notNull())
|
||||
.addColumn('workspace_id', 'uuid', (col) =>
|
||||
col.references('workspaces.id').onDelete('cascade').notNull(),
|
||||
)
|
||||
.addColumn('created_at', 'timestamptz', (col) =>
|
||||
col.defaultTo(sql`now()`).notNull(),
|
||||
)
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_favorites_user_page')
|
||||
.on('favorites')
|
||||
.columns(['user_id', 'page_id'])
|
||||
.unique()
|
||||
.where('page_id', 'is not', null)
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_favorites_user_space')
|
||||
.on('favorites')
|
||||
.columns(['user_id', 'space_id'])
|
||||
.unique()
|
||||
.where('space_id', 'is not', null)
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_favorites_user_template')
|
||||
.on('favorites')
|
||||
.columns(['user_id', 'template_id'])
|
||||
.unique()
|
||||
.where('template_id', 'is not', null)
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_favorites_user_workspace_type')
|
||||
.on('favorites')
|
||||
.columns(['user_id', 'workspace_id', 'type'])
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await db.schema.dropTable('favorites').execute();
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.createTable('page_verifications')
|
||||
.addColumn('id', 'uuid', (col) =>
|
||||
col.primaryKey().defaultTo(sql`gen_uuid_v7()`),
|
||||
)
|
||||
.addColumn('page_id', 'uuid', (col) =>
|
||||
col.notNull().unique().references('pages.id').onDelete('cascade'),
|
||||
)
|
||||
.addColumn('workspace_id', 'uuid', (col) =>
|
||||
col.notNull().references('workspaces.id').onDelete('cascade'),
|
||||
)
|
||||
.addColumn('space_id', 'uuid', (col) =>
|
||||
col.notNull().references('spaces.id').onDelete('cascade'),
|
||||
)
|
||||
.addColumn('type', 'varchar', (col) => col.notNull().defaultTo('expiring'))
|
||||
.addColumn('status', 'varchar')
|
||||
.addColumn('mode', 'varchar')
|
||||
.addColumn('period_amount', 'integer')
|
||||
.addColumn('period_unit', 'varchar')
|
||||
.addColumn('verified_at', 'timestamptz')
|
||||
.addColumn('verified_by_id', 'uuid', (col) =>
|
||||
col.references('users.id').onDelete('set null'),
|
||||
)
|
||||
.addColumn('expires_at', 'timestamptz')
|
||||
.addColumn('requested_at', 'timestamptz')
|
||||
.addColumn('requested_by_id', 'uuid', (col) =>
|
||||
col.references('users.id').onDelete('set null'),
|
||||
)
|
||||
.addColumn('rejected_at', 'timestamptz')
|
||||
.addColumn('rejected_by_id', 'uuid', (col) =>
|
||||
col.references('users.id').onDelete('set null'),
|
||||
)
|
||||
.addColumn('rejection_comment', 'text')
|
||||
.addColumn('data', 'jsonb')
|
||||
.addColumn('creator_id', 'uuid', (col) =>
|
||||
col.references('users.id').onDelete('set null'),
|
||||
)
|
||||
.addColumn('created_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addColumn('updated_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createTable('page_verifiers')
|
||||
.addColumn('id', 'uuid', (col) =>
|
||||
col.primaryKey().defaultTo(sql`gen_uuid_v7()`),
|
||||
)
|
||||
.addColumn('page_verification_id', 'uuid', (col) =>
|
||||
col.notNull().references('page_verifications.id').onDelete('cascade'),
|
||||
)
|
||||
.addColumn('user_id', 'uuid', (col) =>
|
||||
col.notNull().references('users.id').onDelete('cascade'),
|
||||
)
|
||||
.addColumn('is_primary', 'boolean', (col) => col.notNull().defaultTo(false))
|
||||
.addColumn('added_by_id', 'uuid', (col) =>
|
||||
col.references('users.id').onDelete('set null'),
|
||||
)
|
||||
.addColumn('created_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addUniqueConstraint('page_verifiers_verification_user_unique', [
|
||||
'page_verification_id',
|
||||
'user_id',
|
||||
])
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_page_verifications_expires_at')
|
||||
.ifNotExists()
|
||||
.on('page_verifications')
|
||||
.column('expires_at')
|
||||
.where('expires_at', 'is not', null)
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_page_verifications_workspace_id_id')
|
||||
.ifNotExists()
|
||||
.on('page_verifications')
|
||||
.columns(['workspace_id', 'id desc'])
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_page_verifications_space_id')
|
||||
.ifNotExists()
|
||||
.on('page_verifications')
|
||||
.column('space_id')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_page_verifiers_user_id')
|
||||
.ifNotExists()
|
||||
.on('page_verifiers')
|
||||
.column('user_id')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.alterTable('notifications')
|
||||
.addColumn('page_verification_id', 'uuid', (col) =>
|
||||
col.references('page_verifications.id').onDelete('cascade'),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.alterTable('notifications')
|
||||
.dropColumn('page_verification_id')
|
||||
.execute();
|
||||
await db.schema.dropTable('page_verifiers').ifExists().execute();
|
||||
await db.schema.dropTable('page_verifications').ifExists().execute();
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.alterTable('file_tasks')
|
||||
.addColumn('page_id', 'uuid', (col) =>
|
||||
col.references('pages.id').onDelete('set null').ifNotExists(),
|
||||
)
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.alterTable('file_tasks')
|
||||
.addColumn('metadata', 'jsonb', (col) => col.ifNotExists())
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_file_tasks_page_export')
|
||||
.ifNotExists()
|
||||
.on('file_tasks')
|
||||
.columns(['page_id', 'workspace_id'])
|
||||
.where(sql.ref('type'), '=', 'export')
|
||||
.where(sql.ref('deleted_at'), 'is', null)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await db.schema.dropIndex('idx_file_tasks_page_export').execute();
|
||||
|
||||
await db.schema.alterTable('file_tasks').dropColumn('page_id').execute();
|
||||
|
||||
await db.schema.alterTable('file_tasks').dropColumn('metadata').execute();
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.createTable('scim_tokens')
|
||||
.addColumn('id', 'uuid', (col) =>
|
||||
col.primaryKey().defaultTo(sql`gen_uuid_v7()`),
|
||||
)
|
||||
.addColumn('name', 'varchar', (col) => col.notNull())
|
||||
.addColumn('token_hash', 'varchar', (col) => col.notNull())
|
||||
.addColumn('token_last_four', 'varchar(4)', (col) => col.notNull())
|
||||
.addColumn('last_used_at', 'timestamptz')
|
||||
.addColumn('is_enabled', 'boolean', (col) => col.notNull().defaultTo(true))
|
||||
.addColumn('creator_id', 'uuid', (col) =>
|
||||
col.references('users.id').onDelete('set null'),
|
||||
)
|
||||
.addColumn('workspace_id', 'uuid', (col) =>
|
||||
col.references('workspaces.id').onDelete('cascade').notNull(),
|
||||
)
|
||||
.addColumn('created_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addColumn('updated_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addColumn('deleted_at', 'timestamptz')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_scim_tokens_token_hash')
|
||||
.ifNotExists()
|
||||
.on('scim_tokens')
|
||||
.column('token_hash')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_scim_tokens_workspace_id')
|
||||
.ifNotExists()
|
||||
.on('scim_tokens')
|
||||
.column('workspace_id')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.alterTable('users')
|
||||
.addColumn('scim_external_id', 'text')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_users_workspace_scim_external_id')
|
||||
.ifNotExists()
|
||||
.on('users')
|
||||
.columns(['workspace_id', 'scim_external_id'])
|
||||
.where('scim_external_id', 'is not', null)
|
||||
.unique()
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.alterTable('groups')
|
||||
.addColumn('scim_external_id', 'text')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_groups_workspace_scim_external_id')
|
||||
.ifNotExists()
|
||||
.on('groups')
|
||||
.columns(['workspace_id', 'scim_external_id'])
|
||||
.where('scim_external_id', 'is not', null)
|
||||
.unique()
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.alterTable('groups')
|
||||
.addColumn('is_external', 'boolean', (col) =>
|
||||
col.notNull().defaultTo(false),
|
||||
)
|
||||
.execute();
|
||||
|
||||
// Backfill: mark all non-default groups as external in workspaces with SSO group sync enabled
|
||||
await sql`
|
||||
UPDATE groups SET is_external = true
|
||||
WHERE is_default = false
|
||||
AND workspace_id IN (
|
||||
SELECT workspace_id FROM auth_providers WHERE group_sync = true
|
||||
)
|
||||
`.execute(db);
|
||||
|
||||
await db.schema
|
||||
.alterTable('workspaces')
|
||||
.addColumn('is_scim_enabled', 'boolean', (col) =>
|
||||
col.notNull().defaultTo(false),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await db.schema.dropTable('scim_tokens').execute();
|
||||
|
||||
await db.schema.dropIndex('idx_users_workspace_scim_external_id').execute();
|
||||
await db.schema.alterTable('users').dropColumn('scim_external_id').execute();
|
||||
|
||||
await db.schema.dropIndex('idx_groups_workspace_scim_external_id').execute();
|
||||
await db.schema.alterTable('groups').dropColumn('scim_external_id').execute();
|
||||
|
||||
await db.schema.alterTable('groups').dropColumn('is_external').execute();
|
||||
|
||||
await db.schema
|
||||
.alterTable('workspaces')
|
||||
.dropColumn('is_scim_enabled')
|
||||
.execute();
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { type Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.createTable('page_transclusions')
|
||||
.addColumn('id', 'uuid', (col) =>
|
||||
col.primaryKey().defaultTo(sql`gen_uuid_v7()`),
|
||||
)
|
||||
.addColumn('workspace_id', 'uuid', (col) =>
|
||||
col.notNull().references('workspaces.id').onDelete('cascade'),
|
||||
)
|
||||
.addColumn('page_id', 'uuid', (col) =>
|
||||
col.notNull().references('pages.id').onDelete('cascade'),
|
||||
)
|
||||
.addColumn('transclusion_id', 'varchar', (col) => col.notNull())
|
||||
.addColumn('content', 'jsonb', (col) => col.notNull())
|
||||
.addColumn('created_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addColumn('updated_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addUniqueConstraint('page_transclusions_page_transclusion_unique', [
|
||||
'page_id',
|
||||
'transclusion_id',
|
||||
])
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_page_transclusions_workspace')
|
||||
.on('page_transclusions')
|
||||
.column('workspace_id')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createTable('page_transclusion_references')
|
||||
.addColumn('id', 'uuid', (col) =>
|
||||
col.primaryKey().defaultTo(sql`gen_uuid_v7()`),
|
||||
)
|
||||
.addColumn('workspace_id', 'uuid', (col) =>
|
||||
col.notNull().references('workspaces.id').onDelete('cascade'),
|
||||
)
|
||||
.addColumn('reference_page_id', 'uuid', (col) =>
|
||||
col.notNull().references('pages.id').onDelete('cascade'),
|
||||
)
|
||||
.addColumn('source_page_id', 'uuid', (col) =>
|
||||
col.notNull().references('pages.id').onDelete('cascade'),
|
||||
)
|
||||
.addColumn('transclusion_id', 'varchar', (col) => col.notNull())
|
||||
.addColumn('created_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addUniqueConstraint('page_transclusion_references_unique', [
|
||||
'reference_page_id',
|
||||
'source_page_id',
|
||||
'transclusion_id',
|
||||
])
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_page_transclusion_references_source')
|
||||
.on('page_transclusion_references')
|
||||
.columns(['source_page_id', 'transclusion_id'])
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_page_transclusion_references_workspace')
|
||||
.on('page_transclusion_references')
|
||||
.column('workspace_id')
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await db.schema.dropTable('page_transclusion_references').execute();
|
||||
await db.schema.dropTable('page_transclusions').execute();
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { type Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.createTable('labels')
|
||||
.addColumn('id', 'uuid', (col) =>
|
||||
col.primaryKey().defaultTo(sql`gen_uuid_v7()`),
|
||||
)
|
||||
.addColumn('name', 'varchar', (col) => col.notNull())
|
||||
.addColumn('type', 'varchar', (col) => col.notNull().defaultTo('page'))
|
||||
.addColumn('workspace_id', 'uuid', (col) =>
|
||||
col.references('workspaces.id').onDelete('cascade').notNull(),
|
||||
)
|
||||
.addColumn('created_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addColumn('updated_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('labels_workspace_id_type_name_unique')
|
||||
.on('labels')
|
||||
.columns(['workspace_id', 'type', 'name'])
|
||||
.unique()
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createTable('page_labels')
|
||||
.addColumn('id', 'uuid', (col) =>
|
||||
col.primaryKey().defaultTo(sql`gen_uuid_v7()`),
|
||||
)
|
||||
.addColumn('page_id', 'uuid', (col) =>
|
||||
col.references('pages.id').onDelete('cascade').notNull(),
|
||||
)
|
||||
.addColumn('label_id', 'uuid', (col) =>
|
||||
col.references('labels.id').onDelete('cascade').notNull(),
|
||||
)
|
||||
.addColumn('created_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addUniqueConstraint('page_labels_page_id_label_id_unique', [
|
||||
'page_id',
|
||||
'label_id',
|
||||
])
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('page_labels_label_id_idx')
|
||||
.on('page_labels')
|
||||
.column('label_id')
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await db.schema.dropTable('page_labels').execute();
|
||||
await db.schema.dropTable('labels').execute();
|
||||
}
|
||||
@@ -306,6 +306,21 @@ export function defaultEncodeCursor<
|
||||
return Buffer.from(cursor.toString(), 'utf8').toString('base64url');
|
||||
}
|
||||
|
||||
export function emptyCursorPaginationResult<T>(
|
||||
limit: number,
|
||||
): CursorPaginationResult<T> {
|
||||
return {
|
||||
items: [],
|
||||
meta: {
|
||||
limit,
|
||||
hasNextPage: false,
|
||||
hasPrevPage: false,
|
||||
nextCursor: null,
|
||||
prevCursor: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function defaultDecodeCursor<
|
||||
DB,
|
||||
TB extends keyof DB,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
InsertableAttachment,
|
||||
UpdatableAttachment,
|
||||
} from '@docmost/db/types/entity.types';
|
||||
import { AttachmentType } from '../../../core/attachment/attachment.constants';
|
||||
|
||||
@Injectable()
|
||||
export class AttachmentRepo {
|
||||
@@ -23,6 +24,7 @@ export class AttachmentRepo {
|
||||
'creatorId',
|
||||
'pageId',
|
||||
'spaceId',
|
||||
'aiChatId',
|
||||
'workspaceId',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
@@ -44,6 +46,21 @@ export class AttachmentRepo {
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async findByIdWithContent(
|
||||
attachmentId: string,
|
||||
opts?: {
|
||||
trx?: KyselyTransaction;
|
||||
},
|
||||
): Promise<Attachment> {
|
||||
const db = dbOrTx(this.db, opts?.trx);
|
||||
|
||||
return db
|
||||
.selectFrom('attachments')
|
||||
.select([...this.baseFields, 'textContent'])
|
||||
.where('id', '=', attachmentId)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async insertAttachment(
|
||||
insertableAttachment: InsertableAttachment,
|
||||
trx?: KyselyTransaction,
|
||||
@@ -72,6 +89,37 @@ export class AttachmentRepo {
|
||||
.execute();
|
||||
}
|
||||
|
||||
async findByIds(
|
||||
ids: string[],
|
||||
opts?: {
|
||||
trx?: KyselyTransaction;
|
||||
},
|
||||
): Promise<Attachment[]> {
|
||||
if (ids.length === 0) return [];
|
||||
const db = dbOrTx(this.db, opts?.trx);
|
||||
|
||||
return db
|
||||
.selectFrom('attachments')
|
||||
.select(this.baseFields)
|
||||
.where('id', 'in', ids)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async findByAiChatId(
|
||||
aiChatId: string,
|
||||
opts?: {
|
||||
trx?: KyselyTransaction;
|
||||
},
|
||||
): Promise<Attachment[]> {
|
||||
const db = dbOrTx(this.db, opts?.trx);
|
||||
|
||||
return db
|
||||
.selectFrom('attachments')
|
||||
.select(this.baseFields)
|
||||
.where('aiChatId', '=', aiChatId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
updateAttachmentsByPageId(
|
||||
updatableAttachment: UpdatableAttachment,
|
||||
pageIds: string[],
|
||||
@@ -97,6 +145,25 @@ export class AttachmentRepo {
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async claimAttachmentsForChat(
|
||||
attachmentIds: string[],
|
||||
aiChatId: string,
|
||||
creatorId: string,
|
||||
workspaceId: string,
|
||||
): Promise<void> {
|
||||
if (attachmentIds.length === 0) return;
|
||||
|
||||
await this.db
|
||||
.updateTable('attachments')
|
||||
.set({ aiChatId })
|
||||
.where('id', 'in', attachmentIds)
|
||||
.where('creatorId', '=', creatorId)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.where('type', '=', AttachmentType.Chat)
|
||||
.where('aiChatId', 'is', null)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async deleteAttachmentById(attachmentId: string): Promise<void> {
|
||||
await this.db
|
||||
.deleteFrom('attachments')
|
||||
|
||||
@@ -7,10 +7,20 @@ import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
|
||||
import { dbOrTx } from '@docmost/db/utils';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
||||
import {
|
||||
executeWithCursorPagination,
|
||||
emptyCursorPaginationResult,
|
||||
} from '@docmost/db/pagination/cursor-pagination';
|
||||
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
|
||||
@Injectable()
|
||||
export class BacklinkRepo {
|
||||
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
||||
constructor(
|
||||
@InjectKysely() private readonly db: KyselyDB,
|
||||
private readonly spaceMemberRepo: SpaceMemberRepo,
|
||||
) {}
|
||||
|
||||
async findById(
|
||||
backlinkId: string,
|
||||
@@ -69,4 +79,84 @@ export class BacklinkRepo {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
await db.deleteFrom('backlinks').where('id', '=', backlinkId).execute();
|
||||
}
|
||||
|
||||
async findRelatedPageIds(
|
||||
pageId: string,
|
||||
direction: 'incoming' | 'outgoing',
|
||||
userId: string,
|
||||
): Promise<string[]> {
|
||||
const userSpaceIds = this.spaceMemberRepo.getUserSpaceIdsQuery(userId);
|
||||
|
||||
if (direction === 'incoming') {
|
||||
const rows = await this.db
|
||||
.selectFrom('backlinks')
|
||||
.innerJoin('pages', 'pages.id', 'backlinks.sourcePageId')
|
||||
.select('backlinks.sourcePageId as relatedId')
|
||||
.where('backlinks.targetPageId', '=', pageId)
|
||||
.where('pages.deletedAt', 'is', null)
|
||||
.where('pages.spaceId', 'in', userSpaceIds)
|
||||
.execute();
|
||||
return rows.map((r) => r.relatedId);
|
||||
}
|
||||
|
||||
const rows = await this.db
|
||||
.selectFrom('backlinks')
|
||||
.innerJoin('pages', 'pages.id', 'backlinks.targetPageId')
|
||||
.select('backlinks.targetPageId as relatedId')
|
||||
.where('backlinks.sourcePageId', '=', pageId)
|
||||
.where('pages.deletedAt', 'is', null)
|
||||
.where('pages.spaceId', 'in', userSpaceIds)
|
||||
.execute();
|
||||
return rows.map((r) => r.relatedId);
|
||||
}
|
||||
|
||||
async findPagesByIdsPaginated(
|
||||
pageIds: string[],
|
||||
pagination: PaginationOptions,
|
||||
) {
|
||||
if (pageIds.length === 0) {
|
||||
return emptyCursorPaginationResult<{
|
||||
id: string;
|
||||
slugId: string;
|
||||
title: string | null;
|
||||
icon: string | null;
|
||||
spaceId: string;
|
||||
updatedAt: Date;
|
||||
space: { id: string; slug: string; name: string } | null;
|
||||
}>(pagination.limit);
|
||||
}
|
||||
|
||||
const query = this.db
|
||||
.selectFrom('pages')
|
||||
.select((eb) => [
|
||||
'pages.id',
|
||||
'pages.slugId',
|
||||
'pages.title',
|
||||
'pages.icon',
|
||||
'pages.spaceId',
|
||||
'pages.updatedAt',
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom('spaces')
|
||||
.select(['spaces.id', 'spaces.slug', 'spaces.name'])
|
||||
.whereRef('spaces.id', '=', 'pages.spaceId'),
|
||||
).as('space'),
|
||||
])
|
||||
.where('pages.deletedAt', 'is', null)
|
||||
.where('pages.id', 'in', pageIds);
|
||||
|
||||
return executeWithCursorPagination(query, {
|
||||
perPage: pagination.limit,
|
||||
cursor: pagination.cursor,
|
||||
beforeCursor: pagination.beforeCursor,
|
||||
fields: [
|
||||
{ expression: 'pages.updatedAt', direction: 'desc', key: 'updatedAt' },
|
||||
{ expression: 'pages.id', direction: 'desc', key: 'id' },
|
||||
],
|
||||
parseCursor: (cursor) => ({
|
||||
updatedAt: new Date(cursor.updatedAt),
|
||||
id: cursor.id,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
|
||||
import { InsertableFavorite, Favorite } from '@docmost/db/types/entity.types';
|
||||
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
||||
import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagination';
|
||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { ExpressionBuilder, SelectQueryBuilder, sql } from 'kysely';
|
||||
import { DB } from '@docmost/db/types/db';
|
||||
import { dbOrTx } from '@docmost/db/utils';
|
||||
|
||||
export const FavoriteType = {
|
||||
PAGE: 'page',
|
||||
SPACE: 'space',
|
||||
TEMPLATE: 'template',
|
||||
} as const;
|
||||
|
||||
export type FavoriteType = (typeof FavoriteType)[keyof typeof FavoriteType];
|
||||
|
||||
@Injectable()
|
||||
export class FavoriteRepo {
|
||||
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
||||
|
||||
async insert(favorite: InsertableFavorite): Promise<Favorite | undefined> {
|
||||
try {
|
||||
return await this.db
|
||||
.insertInto('favorites')
|
||||
.values(favorite)
|
||||
.returningAll()
|
||||
.executeTakeFirst();
|
||||
} catch (err: any) {
|
||||
if (err?.code === '23505') return undefined;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteByUserAndPage(userId: string, pageId: string): Promise<void> {
|
||||
await this.db
|
||||
.deleteFrom('favorites')
|
||||
.where('userId', '=', userId)
|
||||
.where('pageId', '=', pageId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async deleteByUserAndSpace(userId: string, spaceId: string): Promise<void> {
|
||||
await this.db
|
||||
.deleteFrom('favorites')
|
||||
.where('userId', '=', userId)
|
||||
.where('spaceId', '=', spaceId)
|
||||
.where('type', '=', FavoriteType.SPACE)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async deleteByUserAndTemplate(
|
||||
userId: string,
|
||||
templateId: string,
|
||||
): Promise<void> {
|
||||
await this.db
|
||||
.deleteFrom('favorites')
|
||||
.where('userId', '=', userId)
|
||||
.where('templateId', '=', templateId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async getFavoriteIds(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
type: FavoriteType,
|
||||
spaceId?: string,
|
||||
): Promise<{ items: string[]; meta: any }> {
|
||||
const idColumn =
|
||||
type === FavoriteType.PAGE
|
||||
? 'pageId'
|
||||
: type === FavoriteType.SPACE
|
||||
? 'spaceId'
|
||||
: 'templateId';
|
||||
|
||||
let query = this.db
|
||||
.selectFrom('favorites')
|
||||
.select(['favorites.id', `favorites.${idColumn} as entityId`])
|
||||
.where('favorites.userId', '=', userId)
|
||||
.where('favorites.workspaceId', '=', workspaceId)
|
||||
.where('favorites.type', '=', type);
|
||||
|
||||
if (spaceId) {
|
||||
query = this.applySpaceFilter(query, type, spaceId);
|
||||
}
|
||||
|
||||
const result = await executeWithCursorPagination(query, {
|
||||
perPage: 250,
|
||||
fields: [{ expression: 'favorites.id', direction: 'desc' }],
|
||||
parseCursor: (cursor) => ({ id: cursor.id }),
|
||||
});
|
||||
|
||||
return {
|
||||
items: result.items
|
||||
.map((r) => (r as any).entityId as string)
|
||||
.filter(Boolean),
|
||||
meta: result.meta,
|
||||
};
|
||||
}
|
||||
|
||||
async findUserFavorites(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
pagination: PaginationOptions,
|
||||
type?: FavoriteType,
|
||||
spaceId?: string,
|
||||
) {
|
||||
let query = this.db
|
||||
.selectFrom('favorites')
|
||||
.selectAll('favorites')
|
||||
.where('favorites.userId', '=', userId)
|
||||
.where('favorites.workspaceId', '=', workspaceId);
|
||||
|
||||
if (type) {
|
||||
query = query.where('favorites.type', '=', type);
|
||||
}
|
||||
|
||||
if (spaceId) {
|
||||
query = this.applySpaceFilter(query, type, spaceId);
|
||||
}
|
||||
|
||||
if (type === FavoriteType.PAGE || !type) {
|
||||
query = query.select((eb) => this.withPage(eb));
|
||||
}
|
||||
|
||||
if (type === FavoriteType.PAGE) {
|
||||
query = query.select((eb) => this.withPageSpace(eb));
|
||||
} else if (type === FavoriteType.SPACE) {
|
||||
query = query.select((eb) => this.withSpace(eb));
|
||||
} else {
|
||||
query = query.select((eb) => this.withSpaceResolved(eb));
|
||||
}
|
||||
|
||||
if (type === FavoriteType.TEMPLATE || !type) {
|
||||
query = query.select((eb) => this.withTemplate(eb));
|
||||
}
|
||||
|
||||
return executeWithCursorPagination(query, {
|
||||
perPage: pagination.limit,
|
||||
cursor: pagination.cursor,
|
||||
beforeCursor: pagination.beforeCursor,
|
||||
fields: [{ expression: 'favorites.id', direction: 'desc' }],
|
||||
parseCursor: (cursor) => ({
|
||||
id: cursor.id,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
async deleteByUsersWithoutSpaceAccess(
|
||||
userIds: string[],
|
||||
spaceId: string,
|
||||
opts?: { trx?: KyselyTransaction },
|
||||
): Promise<void> {
|
||||
if (userIds.length === 0) return;
|
||||
|
||||
const { trx } = opts;
|
||||
const db = dbOrTx(this.db, trx);
|
||||
|
||||
const usersWithAccess = db
|
||||
.selectFrom('spaceMembers')
|
||||
.select('userId')
|
||||
.where('spaceId', '=', spaceId)
|
||||
.where('userId', 'is not', null)
|
||||
.union(
|
||||
db
|
||||
.selectFrom('spaceMembers')
|
||||
.innerJoin('groupUsers', 'groupUsers.groupId', 'spaceMembers.groupId')
|
||||
.select('groupUsers.userId')
|
||||
.where('spaceMembers.spaceId', '=', spaceId),
|
||||
);
|
||||
|
||||
await db
|
||||
.deleteFrom('favorites')
|
||||
.where('userId', 'in', userIds)
|
||||
.where('spaceId', '=', spaceId)
|
||||
.where('userId', 'not in', usersWithAccess)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async deleteByUserAndWorkspace(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
opts?: { trx?: KyselyTransaction },
|
||||
): Promise<void> {
|
||||
const { trx } = opts;
|
||||
const db = dbOrTx(this.db, trx);
|
||||
|
||||
await db
|
||||
.deleteFrom('favorites')
|
||||
.where('userId', '=', userId)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
private applySpaceFilter<Q extends SelectQueryBuilder<any, any, any>>(
|
||||
query: Q,
|
||||
type: FavoriteType | undefined,
|
||||
spaceId: string,
|
||||
): Q {
|
||||
if (type === FavoriteType.PAGE) {
|
||||
return query.where((eb: any) =>
|
||||
eb.exists(
|
||||
eb
|
||||
.selectFrom('pages')
|
||||
.select(sql`1`.as('one'))
|
||||
.whereRef('pages.id', '=', 'favorites.pageId')
|
||||
.where('pages.spaceId', '=', spaceId),
|
||||
),
|
||||
) as Q;
|
||||
}
|
||||
if (type === FavoriteType.SPACE) {
|
||||
return query.where('favorites.spaceId' as any, '=', spaceId) as Q;
|
||||
}
|
||||
if (type === FavoriteType.TEMPLATE) {
|
||||
return query.where((eb: any) =>
|
||||
eb.exists(
|
||||
eb
|
||||
.selectFrom('templates')
|
||||
.select(sql`1`.as('one'))
|
||||
.whereRef('templates.id', '=', 'favorites.templateId')
|
||||
.where('templates.spaceId', '=', spaceId),
|
||||
),
|
||||
) as Q;
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
private withPage(eb: ExpressionBuilder<DB, 'favorites'>) {
|
||||
return jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom('pages')
|
||||
.select([
|
||||
'pages.id',
|
||||
'pages.slugId',
|
||||
'pages.title',
|
||||
'pages.icon',
|
||||
'pages.spaceId',
|
||||
])
|
||||
.whereRef('pages.id', '=', 'favorites.pageId'),
|
||||
).as('page');
|
||||
}
|
||||
|
||||
private withSpace(eb: ExpressionBuilder<DB, 'favorites'>) {
|
||||
return jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom('spaces')
|
||||
.select(['spaces.id', 'spaces.name', 'spaces.slug', 'spaces.logo'])
|
||||
.whereRef('spaces.id', '=', 'favorites.spaceId'),
|
||||
).as('space');
|
||||
}
|
||||
|
||||
private withPageSpace(eb: ExpressionBuilder<DB, 'favorites'>) {
|
||||
return jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom('spaces')
|
||||
.innerJoin('pages', 'pages.spaceId', 'spaces.id')
|
||||
.select(['spaces.id', 'spaces.name', 'spaces.slug', 'spaces.logo'])
|
||||
.whereRef('pages.id', '=', 'favorites.pageId'),
|
||||
).as('space');
|
||||
}
|
||||
|
||||
private withSpaceResolved(eb: ExpressionBuilder<DB, 'favorites'>) {
|
||||
return jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom('spaces')
|
||||
.select(['spaces.id', 'spaces.name', 'spaces.slug', 'spaces.logo'])
|
||||
.where(({ or, ref }) =>
|
||||
or([
|
||||
sql<boolean>`${ref('spaces.id')} = ${ref('favorites.spaceId')}`,
|
||||
sql<boolean>`${ref('spaces.id')} = (SELECT pages.space_id FROM pages WHERE pages.id = ${ref('favorites.pageId')})`,
|
||||
]),
|
||||
),
|
||||
).as('space');
|
||||
}
|
||||
|
||||
private withTemplate(eb: ExpressionBuilder<DB, 'favorites'>) {
|
||||
return jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom('templates')
|
||||
.select([
|
||||
'templates.id',
|
||||
'templates.title',
|
||||
'templates.description',
|
||||
'templates.icon',
|
||||
'templates.spaceId',
|
||||
])
|
||||
.whereRef('templates.id', '=', 'favorites.templateId'),
|
||||
).as('template');
|
||||
}
|
||||
}
|
||||
@@ -175,4 +175,14 @@ export class GroupUserRepo {
|
||||
.where('groupId', '=', groupId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async getUserGroupIds(userId: string): Promise<string[]> {
|
||||
const results = await this.db
|
||||
.selectFrom('groupUsers')
|
||||
.select('groupId')
|
||||
.where('userId', '=', userId)
|
||||
.execute();
|
||||
|
||||
return results.map((r) => r.groupId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from '@docmost/db/types/entity.types';
|
||||
import { ExpressionBuilder, sql } from 'kysely';
|
||||
import { PaginationOptions } from '../../pagination/pagination-options';
|
||||
import { DB } from '@docmost/db/types/db';
|
||||
import { DB, Groups } from '@docmost/db/types/db';
|
||||
import { DefaultGroup } from '../../../core/group/dto/create-group.dto';
|
||||
import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagination';
|
||||
|
||||
@@ -17,16 +17,34 @@ import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagin
|
||||
export class GroupRepo {
|
||||
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
||||
|
||||
private baseFields: Array<keyof Groups> = [
|
||||
'id',
|
||||
'name',
|
||||
'description',
|
||||
'isDefault',
|
||||
'isExternal',
|
||||
'creatorId',
|
||||
'workspaceId',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'deletedAt',
|
||||
];
|
||||
|
||||
async findById(
|
||||
groupId: string,
|
||||
workspaceId: string,
|
||||
opts?: { includeMemberCount?: boolean; trx?: KyselyTransaction },
|
||||
opts?: {
|
||||
includeMemberCount?: boolean;
|
||||
includeScimExternalId?: boolean;
|
||||
trx?: KyselyTransaction;
|
||||
},
|
||||
): Promise<Group> {
|
||||
const db = dbOrTx(this.db, opts?.trx);
|
||||
return db
|
||||
.selectFrom('groups')
|
||||
.selectAll('groups')
|
||||
.select(this.baseFields)
|
||||
.$if(opts?.includeMemberCount, (qb) => qb.select(this.withMemberCount))
|
||||
.$if(opts?.includeScimExternalId, (qb) => qb.select('scimExternalId'))
|
||||
.where('id', '=', groupId)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.executeTakeFirst();
|
||||
@@ -35,13 +53,18 @@ export class GroupRepo {
|
||||
async findByName(
|
||||
groupName: string,
|
||||
workspaceId: string,
|
||||
opts?: { includeMemberCount?: boolean; trx?: KyselyTransaction },
|
||||
opts?: {
|
||||
includeMemberCount?: boolean;
|
||||
includeScimExternalId?: boolean;
|
||||
trx?: KyselyTransaction;
|
||||
},
|
||||
): Promise<Group> {
|
||||
const db = dbOrTx(this.db, opts?.trx);
|
||||
return db
|
||||
.selectFrom('groups')
|
||||
.selectAll('groups')
|
||||
.select(this.baseFields)
|
||||
.$if(opts?.includeMemberCount, (qb) => qb.select(this.withMemberCount))
|
||||
.$if(opts?.includeScimExternalId, (qb) => qb.select('scimExternalId'))
|
||||
.where(sql`LOWER(name)`, '=', sql`LOWER(${groupName})`)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.executeTakeFirst();
|
||||
@@ -51,8 +74,11 @@ export class GroupRepo {
|
||||
updatableGroup: UpdatableGroup,
|
||||
groupId: string,
|
||||
workspaceId: string,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<void> {
|
||||
await this.db
|
||||
const db = dbOrTx(this.db, trx);
|
||||
|
||||
await db
|
||||
.updateTable('groups')
|
||||
.set({ ...updatableGroup, updatedAt: new Date() })
|
||||
.where('id', '=', groupId)
|
||||
@@ -68,7 +94,7 @@ export class GroupRepo {
|
||||
return db
|
||||
.insertInto('groups')
|
||||
.values(insertableGroup)
|
||||
.returningAll()
|
||||
.returning(this.baseFields)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@@ -80,7 +106,7 @@ export class GroupRepo {
|
||||
return (
|
||||
db
|
||||
.selectFrom('groups')
|
||||
.selectAll()
|
||||
.select(this.baseFields)
|
||||
// .select((eb) => this.withMemberCount(eb))
|
||||
.where('isDefault', '=', true)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
@@ -106,7 +132,7 @@ export class GroupRepo {
|
||||
async getGroupsPaginated(workspaceId: string, pagination: PaginationOptions) {
|
||||
let baseQuery = this.db
|
||||
.selectFrom('groups')
|
||||
.selectAll('groups')
|
||||
.select(this.baseFields)
|
||||
.select((eb) => this.withMemberCount(eb))
|
||||
.where('workspaceId', '=', workspaceId);
|
||||
|
||||
@@ -135,10 +161,12 @@ export class GroupRepo {
|
||||
direction: 'desc',
|
||||
key: 'memberCount',
|
||||
},
|
||||
{ expression: 'sub.name', direction: 'asc', key: 'name' },
|
||||
{ expression: 'sub.id', direction: 'asc', key: 'id' },
|
||||
],
|
||||
parseCursor: (cursor) => ({
|
||||
memberCount: parseInt(cursor.memberCount, 10),
|
||||
name: cursor.name,
|
||||
id: cursor.id,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { KyselyDB, KyselyTransaction } from '../../types/kysely.types';
|
||||
import { Label } from '@docmost/db/types/entity.types';
|
||||
import { dbOrTx } from '@docmost/db/utils';
|
||||
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
||||
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
||||
import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagination';
|
||||
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { normalizeLabelName } from '../../../core/label/utils';
|
||||
|
||||
export const LabelType = {
|
||||
PAGE: 'page',
|
||||
SPACE: 'space',
|
||||
} as const;
|
||||
|
||||
export type LabelType = (typeof LabelType)[keyof typeof LabelType];
|
||||
|
||||
@Injectable()
|
||||
export class LabelRepo {
|
||||
constructor(
|
||||
@InjectKysely() private readonly db: KyselyDB,
|
||||
private readonly spaceMemberRepo: SpaceMemberRepo,
|
||||
) {}
|
||||
|
||||
async findById(
|
||||
labelId: string,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<Label | undefined> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
return db
|
||||
.selectFrom('labels')
|
||||
.selectAll()
|
||||
.where('id', '=', labelId)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async findByNameAndWorkspace(
|
||||
name: string,
|
||||
workspaceId: string,
|
||||
type: LabelType,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<Label | undefined> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
return db
|
||||
.selectFrom('labels')
|
||||
.selectAll()
|
||||
.where('name', '=', normalizeLabelName(name))
|
||||
.where('type', '=', type)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async findOrCreate(
|
||||
name: string,
|
||||
workspaceId: string,
|
||||
type: LabelType,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<Label> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
const normalizedName = normalizeLabelName(name);
|
||||
|
||||
// DO UPDATE (rather than DO NOTHING) so RETURNING always emits a row,
|
||||
// even on conflict. Avoids a race where a follow-up SELECT could miss a
|
||||
// row inserted by a concurrent transaction. The set is a no-op write.
|
||||
return db
|
||||
.insertInto('labels')
|
||||
.values({ name: normalizedName, type, workspaceId })
|
||||
.onConflict((oc) =>
|
||||
oc
|
||||
.columns(['name', 'type', 'workspaceId'])
|
||||
.doUpdateSet({ name: normalizedName }),
|
||||
)
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
async findLabelsByPageId(pageId: string, pagination: PaginationOptions) {
|
||||
const query = this.db
|
||||
.selectFrom('labels')
|
||||
.innerJoin('pageLabels', 'pageLabels.labelId', 'labels.id')
|
||||
.select([
|
||||
'labels.id',
|
||||
'labels.name',
|
||||
'labels.type',
|
||||
'labels.createdAt',
|
||||
'labels.updatedAt',
|
||||
'labels.workspaceId',
|
||||
'pageLabels.id as joinId',
|
||||
])
|
||||
.where('pageLabels.pageId', '=', pageId)
|
||||
.where('labels.type', '=', LabelType.PAGE);
|
||||
|
||||
const result = await executeWithCursorPagination(query, {
|
||||
perPage: pagination.limit,
|
||||
cursor: pagination.cursor,
|
||||
beforeCursor: pagination.beforeCursor,
|
||||
fields: [
|
||||
{ expression: 'pageLabels.id', direction: 'asc', key: 'joinId' },
|
||||
],
|
||||
parseCursor: (cursor) => ({
|
||||
joinId: cursor.joinId,
|
||||
}),
|
||||
});
|
||||
|
||||
// joinId is an internal pagination cursor; don't leak it to callers.
|
||||
return {
|
||||
...result,
|
||||
items: result.items.map(({ joinId: _joinId, ...rest }) => rest),
|
||||
};
|
||||
}
|
||||
|
||||
async findLabels(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
type: LabelType,
|
||||
pagination: PaginationOptions,
|
||||
) {
|
||||
// Label visibility is scoped to space membership: a label surfaces if it
|
||||
// is attached to any non-deleted page in a space the user belongs to.
|
||||
// Per-page permission restrictions intentionally do not narrow this
|
||||
// further — labels are a space-level concept, not a page-level one.
|
||||
let query = this.db
|
||||
.selectFrom('labels')
|
||||
.select(['id', 'name', 'type', 'createdAt', 'updatedAt', 'workspaceId'])
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.where('type', '=', type)
|
||||
.where(
|
||||
'id',
|
||||
'in',
|
||||
this.db
|
||||
.selectFrom('pageLabels')
|
||||
.innerJoin('pages', 'pages.id', 'pageLabels.pageId')
|
||||
.select('pageLabels.labelId')
|
||||
.where('pages.deletedAt', 'is', null)
|
||||
.where(
|
||||
'pages.spaceId',
|
||||
'in',
|
||||
this.spaceMemberRepo.getUserSpaceIdsQuery(userId),
|
||||
),
|
||||
);
|
||||
|
||||
if (pagination.query) {
|
||||
query = query.where(
|
||||
'name',
|
||||
'like',
|
||||
`%${pagination.query.toLowerCase()}%`,
|
||||
);
|
||||
}
|
||||
|
||||
return executeWithCursorPagination(query, {
|
||||
perPage: pagination.limit,
|
||||
cursor: pagination.cursor,
|
||||
beforeCursor: pagination.beforeCursor,
|
||||
fields: [
|
||||
{ expression: 'name', direction: 'asc' },
|
||||
{ expression: 'id', direction: 'asc' },
|
||||
],
|
||||
parseCursor: (cursor) => ({
|
||||
name: cursor.name,
|
||||
id: cursor.id,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
async addLabelToPage(
|
||||
pageId: string,
|
||||
labelId: string,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<void> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
await db
|
||||
.insertInto('pageLabels')
|
||||
.values({ pageId, labelId })
|
||||
.onConflict((oc) => oc.doNothing())
|
||||
.execute();
|
||||
}
|
||||
|
||||
async removeLabelFromPage(
|
||||
pageId: string,
|
||||
labelId: string,
|
||||
workspaceId: string,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<void> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
await db
|
||||
.deleteFrom('pageLabels')
|
||||
.where('pageId', '=', pageId)
|
||||
.where('labelId', '=', labelId)
|
||||
.where((eb) =>
|
||||
eb.exists(
|
||||
eb
|
||||
.selectFrom('labels')
|
||||
.select('id')
|
||||
.whereRef('labels.id', '=', 'pageLabels.labelId')
|
||||
.where('labels.workspaceId', '=', workspaceId),
|
||||
),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async getPageLabelCount(
|
||||
pageId: string,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<number> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
const result = await db
|
||||
.selectFrom('pageLabels')
|
||||
.select((eb) => eb.fn.count('id').as('count'))
|
||||
.where('pageId', '=', pageId)
|
||||
.executeTakeFirst();
|
||||
|
||||
return Number(result?.count ?? 0);
|
||||
}
|
||||
|
||||
async getLabelPageCount(
|
||||
labelId: string,
|
||||
workspaceId: string,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<number> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
const result = await db
|
||||
.selectFrom('pageLabels')
|
||||
.innerJoin('labels', 'labels.id', 'pageLabels.labelId')
|
||||
.select((eb) => eb.fn.count('pageLabels.id').as('count'))
|
||||
.where('pageLabels.labelId', '=', labelId)
|
||||
.where('labels.workspaceId', '=', workspaceId)
|
||||
.executeTakeFirst();
|
||||
|
||||
return Number(result?.count ?? 0);
|
||||
}
|
||||
|
||||
async deleteLabel(
|
||||
labelId: string,
|
||||
workspaceId: string,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<void> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
await db
|
||||
.deleteFrom('labels')
|
||||
.where('id', '=', labelId)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async findPagesByLabelId(
|
||||
labelId: string,
|
||||
userId: string,
|
||||
opts: {
|
||||
spaceId?: string;
|
||||
query?: string;
|
||||
pagination: PaginationOptions;
|
||||
},
|
||||
) {
|
||||
let query = this.db
|
||||
.selectFrom('pages')
|
||||
.innerJoin('pageLabels', 'pageLabels.pageId', 'pages.id')
|
||||
.select((eb) => [
|
||||
'pages.id',
|
||||
'pages.slugId',
|
||||
'pages.title',
|
||||
'pages.icon',
|
||||
'pages.spaceId',
|
||||
'pages.createdAt',
|
||||
'pages.updatedAt',
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom('spaces')
|
||||
.select(['spaces.id', 'spaces.name', 'spaces.slug', 'spaces.logo'])
|
||||
.whereRef('spaces.id', '=', 'pages.spaceId'),
|
||||
).as('space'),
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom('users')
|
||||
.select(['users.id', 'users.name', 'users.avatarUrl'])
|
||||
.whereRef('users.id', '=', 'pages.creatorId'),
|
||||
).as('creator'),
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom('labels')
|
||||
.innerJoin('pageLabels as pl', 'pl.labelId', 'labels.id')
|
||||
.select(['labels.id', 'labels.name'])
|
||||
.whereRef('pl.pageId', '=', 'pages.id')
|
||||
.where('labels.type', '=', LabelType.PAGE)
|
||||
.orderBy('pl.id', 'asc'),
|
||||
).as('labels'),
|
||||
])
|
||||
.where('pageLabels.labelId', '=', labelId)
|
||||
.where('pages.deletedAt', 'is', null);
|
||||
|
||||
if (opts.spaceId) {
|
||||
query = query.where('pages.spaceId', '=', opts.spaceId);
|
||||
} else {
|
||||
query = query.where(
|
||||
'pages.spaceId',
|
||||
'in',
|
||||
this.spaceMemberRepo.getUserSpaceIdsQuery(userId),
|
||||
);
|
||||
}
|
||||
|
||||
if (opts.query) {
|
||||
query = query.where('pages.title', 'ilike', `%${opts.query}%`);
|
||||
}
|
||||
|
||||
return executeWithCursorPagination(query, {
|
||||
perPage: opts.pagination.limit,
|
||||
cursor: opts.pagination.cursor,
|
||||
beforeCursor: opts.pagination.beforeCursor,
|
||||
fields: [
|
||||
{ expression: 'pages.updatedAt', direction: 'desc', key: 'updatedAt' },
|
||||
{ expression: 'pages.id', direction: 'desc', key: 'id' },
|
||||
],
|
||||
parseCursor: (cursor) => ({
|
||||
updatedAt: new Date(cursor.updatedAt),
|
||||
id: cursor.id,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
async getLabelPageCountForUser(
|
||||
labelId: string,
|
||||
userId: string,
|
||||
spaceId?: string,
|
||||
): Promise<number> {
|
||||
let query = this.db
|
||||
.selectFrom('pageLabels')
|
||||
.innerJoin('pages', 'pages.id', 'pageLabels.pageId')
|
||||
.select((eb) => eb.fn.count('pageLabels.id').as('count'))
|
||||
.where('pageLabels.labelId', '=', labelId)
|
||||
.where('pages.deletedAt', 'is', null);
|
||||
|
||||
if (spaceId) {
|
||||
query = query.where('pages.spaceId', '=', spaceId);
|
||||
} else {
|
||||
query = query.where(
|
||||
'pages.spaceId',
|
||||
'in',
|
||||
this.spaceMemberRepo.getUserSpaceIdsQuery(userId),
|
||||
);
|
||||
}
|
||||
|
||||
const result = await query.executeTakeFirst();
|
||||
return Number(result?.count ?? 0);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,10 @@ import { ExpressionBuilder } from 'kysely';
|
||||
import { DB } from '@docmost/db/types/db';
|
||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
||||
import {
|
||||
NotificationTab,
|
||||
NotificationType,
|
||||
} from '../../../core/notification/notification.constants';
|
||||
|
||||
@Injectable()
|
||||
export class NotificationRepo {
|
||||
@@ -27,8 +31,12 @@ export class NotificationRepo {
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async findByUserId(userId: string, pagination: PaginationOptions) {
|
||||
const query = this.db
|
||||
async findByUserId(
|
||||
userId: string,
|
||||
pagination: PaginationOptions,
|
||||
type: NotificationTab = 'all',
|
||||
) {
|
||||
let query = this.db
|
||||
.selectFrom('notifications')
|
||||
.selectAll('notifications')
|
||||
.select((eb) => this.withActor(eb))
|
||||
@@ -38,10 +46,20 @@ export class NotificationRepo {
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb('spaceId', 'is', null),
|
||||
eb('spaceId', 'in', this.spaceMemberRepo.getUserSpaceIdsQuery(userId)),
|
||||
eb(
|
||||
'spaceId',
|
||||
'in',
|
||||
this.spaceMemberRepo.getUserSpaceIdsQuery(userId),
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
||||
if (type === 'direct') {
|
||||
query = query.where('type', '!=', NotificationType.PAGE_UPDATED);
|
||||
} else if (type === 'updates') {
|
||||
query = query.where('type', '=', NotificationType.PAGE_UPDATED);
|
||||
}
|
||||
|
||||
return executeWithCursorPagination(query, {
|
||||
perPage: pagination.limit,
|
||||
cursor: pagination.cursor,
|
||||
@@ -51,6 +69,14 @@ export class NotificationRepo {
|
||||
});
|
||||
}
|
||||
|
||||
async insert(notification: InsertableNotification): Promise<Notification> {
|
||||
return this.db
|
||||
.insertInto('notifications')
|
||||
.values(notification)
|
||||
.returningAll()
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async getUnreadCount(userId: string): Promise<number> {
|
||||
const result = await this.db
|
||||
.selectFrom('notifications')
|
||||
@@ -60,7 +86,11 @@ export class NotificationRepo {
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb('spaceId', 'is', null),
|
||||
eb('spaceId', 'in', this.spaceMemberRepo.getUserSpaceIdsQuery(userId)),
|
||||
eb(
|
||||
'spaceId',
|
||||
'in',
|
||||
this.spaceMemberRepo.getUserSpaceIdsQuery(userId),
|
||||
),
|
||||
]),
|
||||
)
|
||||
.executeTakeFirst();
|
||||
@@ -68,14 +98,6 @@ export class NotificationRepo {
|
||||
return Number(result?.count ?? 0);
|
||||
}
|
||||
|
||||
async insert(notification: InsertableNotification): Promise<Notification> {
|
||||
return this.db
|
||||
.insertInto('notifications')
|
||||
.values(notification)
|
||||
.returningAll()
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async markAsRead(notificationId: string, userId: string): Promise<void> {
|
||||
await this.db
|
||||
.updateTable('notifications')
|
||||
@@ -83,12 +105,6 @@ export class NotificationRepo {
|
||||
.where('id', '=', notificationId)
|
||||
.where('userId', '=', userId)
|
||||
.where('readAt', 'is', null)
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb('spaceId', 'is', null),
|
||||
eb('spaceId', 'in', this.spaceMemberRepo.getUserSpaceIdsQuery(userId)),
|
||||
]),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
@@ -105,12 +121,15 @@ export class NotificationRepo {
|
||||
.where('id', 'in', notificationIds)
|
||||
.where('userId', '=', userId)
|
||||
.where('readAt', 'is', null)
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb('spaceId', 'is', null),
|
||||
eb('spaceId', 'in', this.spaceMemberRepo.getUserSpaceIdsQuery(userId)),
|
||||
]),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async markAllAsRead(userId: string): Promise<void> {
|
||||
await this.db
|
||||
.updateTable('notifications')
|
||||
.set({ readAt: new Date() })
|
||||
.where('userId', '=', userId)
|
||||
.where('readAt', 'is', null)
|
||||
.execute();
|
||||
}
|
||||
|
||||
@@ -123,19 +142,27 @@ export class NotificationRepo {
|
||||
.execute();
|
||||
}
|
||||
|
||||
async markAllAsRead(userId: string): Promise<void> {
|
||||
await this.db
|
||||
.updateTable('notifications')
|
||||
.set({ readAt: new Date() })
|
||||
.where('userId', '=', userId)
|
||||
.where('readAt', 'is', null)
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb('spaceId', 'is', null),
|
||||
eb('spaceId', 'in', this.spaceMemberRepo.getUserSpaceIdsQuery(userId)),
|
||||
]),
|
||||
)
|
||||
async getRecentlyNotifiedUserIds(
|
||||
userIds: string[],
|
||||
pageId: string,
|
||||
type: string,
|
||||
withinHours: number,
|
||||
): Promise<Set<string>> {
|
||||
if (userIds.length === 0) return new Set();
|
||||
|
||||
const cutoff = new Date(Date.now() - withinHours * 60 * 60 * 1000);
|
||||
|
||||
const rows = await this.db
|
||||
.selectFrom('notifications')
|
||||
.select('userId')
|
||||
.where('userId', 'in', userIds)
|
||||
.where('pageId', '=', pageId)
|
||||
.where('type', '=', type)
|
||||
.where('createdAt', '>', cutoff)
|
||||
.groupBy('userId')
|
||||
.execute();
|
||||
|
||||
return new Set(rows.map((r) => r.userId));
|
||||
}
|
||||
|
||||
withActor(eb: ExpressionBuilder<DB, 'notifications'>) {
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
|
||||
import { dbOrTx } from '@docmost/db/utils';
|
||||
import {
|
||||
InsertablePageTransclusionReference,
|
||||
PageTransclusionReference,
|
||||
} from '@docmost/db/types/entity.types';
|
||||
|
||||
export type TransclusionReferenceKey = {
|
||||
sourcePageId: string;
|
||||
transclusionId: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class PageTransclusionReferencesRepo {
|
||||
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
||||
|
||||
async findByReferencePageId(
|
||||
referencePageId: string,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<PageTransclusionReference[]> {
|
||||
return dbOrTx(this.db, trx)
|
||||
.selectFrom('pageTransclusionReferences')
|
||||
.selectAll()
|
||||
.where('referencePageId', '=', referencePageId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async findReferencePageIdsByTransclusion(
|
||||
sourcePageId: string,
|
||||
transclusionId: string,
|
||||
workspaceId: string,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<string[]> {
|
||||
const rows = await dbOrTx(this.db, trx)
|
||||
.selectFrom('pageTransclusionReferences')
|
||||
.select('referencePageId')
|
||||
.distinct()
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.where('sourcePageId', '=', sourcePageId)
|
||||
.where('transclusionId', '=', transclusionId)
|
||||
.execute();
|
||||
return rows.map((r) => r.referencePageId);
|
||||
}
|
||||
|
||||
async insertMany(
|
||||
rows: InsertablePageTransclusionReference[],
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<void> {
|
||||
if (rows.length === 0) return;
|
||||
await dbOrTx(this.db, trx)
|
||||
.insertInto('pageTransclusionReferences')
|
||||
.values(rows)
|
||||
.onConflict((oc) =>
|
||||
oc
|
||||
.columns(['referencePageId', 'sourcePageId', 'transclusionId'])
|
||||
.doNothing(),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async deleteByReferenceAndKeys(
|
||||
referencePageId: string,
|
||||
keys: TransclusionReferenceKey[],
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<void> {
|
||||
if (keys.length === 0) return;
|
||||
await dbOrTx(this.db, trx)
|
||||
.deleteFrom('pageTransclusionReferences')
|
||||
.where('referencePageId', '=', referencePageId)
|
||||
.where((eb) =>
|
||||
eb.or(
|
||||
keys.map((k) =>
|
||||
eb.and([
|
||||
eb('sourcePageId', '=', k.sourcePageId),
|
||||
eb('transclusionId', '=', k.transclusionId),
|
||||
]),
|
||||
),
|
||||
),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async deleteOne(
|
||||
referencePageId: string,
|
||||
sourcePageId: string,
|
||||
transclusionId: string,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<void> {
|
||||
await dbOrTx(this.db, trx)
|
||||
.deleteFrom('pageTransclusionReferences')
|
||||
.where('referencePageId', '=', referencePageId)
|
||||
.where('sourcePageId', '=', sourcePageId)
|
||||
.where('transclusionId', '=', transclusionId)
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
|
||||
import { dbOrTx } from '@docmost/db/utils';
|
||||
import {
|
||||
InsertablePageTransclusion,
|
||||
PageTransclusion,
|
||||
UpdatablePageTransclusion,
|
||||
} from '@docmost/db/types/entity.types';
|
||||
|
||||
@Injectable()
|
||||
export class PageTransclusionsRepo {
|
||||
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
||||
|
||||
async findByPageId(
|
||||
pageId: string,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<PageTransclusion[]> {
|
||||
return dbOrTx(this.db, trx)
|
||||
.selectFrom('pageTransclusions')
|
||||
.selectAll()
|
||||
.where('pageId', '=', pageId)
|
||||
.orderBy('createdAt', 'asc')
|
||||
.execute();
|
||||
}
|
||||
|
||||
async findByPageAndTransclusion(
|
||||
pageId: string,
|
||||
transclusionId: string,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<PageTransclusion | undefined> {
|
||||
return dbOrTx(this.db, trx)
|
||||
.selectFrom('pageTransclusions')
|
||||
.selectAll()
|
||||
.where('pageId', '=', pageId)
|
||||
.where('transclusionId', '=', transclusionId)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async findManyByPageAndTransclusion(
|
||||
keys: Array<{ pageId: string; transclusionId: string }>,
|
||||
workspaceId: string,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<PageTransclusion[]> {
|
||||
if (keys.length === 0) return [];
|
||||
return dbOrTx(this.db, trx)
|
||||
.selectFrom('pageTransclusions')
|
||||
.selectAll()
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.where((eb) =>
|
||||
eb.or(
|
||||
keys.map((k) =>
|
||||
eb.and([
|
||||
eb('pageId', '=', k.pageId),
|
||||
eb('transclusionId', '=', k.transclusionId),
|
||||
]),
|
||||
),
|
||||
),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async insert(
|
||||
data: InsertablePageTransclusion,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<PageTransclusion> {
|
||||
return dbOrTx(this.db, trx)
|
||||
.insertInto('pageTransclusions')
|
||||
.values(data)
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
async insertMany(
|
||||
data: InsertablePageTransclusion[],
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<void> {
|
||||
if (data.length === 0) return;
|
||||
await dbOrTx(this.db, trx)
|
||||
.insertInto('pageTransclusions')
|
||||
.values(data)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async update(
|
||||
pageId: string,
|
||||
transclusionId: string,
|
||||
data: UpdatablePageTransclusion,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<void> {
|
||||
await dbOrTx(this.db, trx)
|
||||
.updateTable('pageTransclusions')
|
||||
.set({ ...data, updatedAt: new Date() })
|
||||
.where('pageId', '=', pageId)
|
||||
.where('transclusionId', '=', transclusionId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async deleteByPageAndTransclusionIds(
|
||||
pageId: string,
|
||||
transclusionIds: string[],
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<void> {
|
||||
if (transclusionIds.length === 0) return;
|
||||
await dbOrTx(this.db, trx)
|
||||
.deleteFrom('pageTransclusions')
|
||||
.where('pageId', '=', pageId)
|
||||
.where('transclusionId', 'in', transclusionIds)
|
||||
.execute();
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -54,6 +54,7 @@ export class PageRepo {
|
||||
includeCreator?: boolean;
|
||||
includeLastUpdatedBy?: boolean;
|
||||
includeContributors?: boolean;
|
||||
includeDeletedBy?: boolean;
|
||||
includeHasChildren?: boolean;
|
||||
withLock?: boolean;
|
||||
trx?: KyselyTransaction;
|
||||
@@ -83,6 +84,10 @@ export class PageRepo {
|
||||
query = query.select((eb) => this.withContributors(eb));
|
||||
}
|
||||
|
||||
if (opts?.includeDeletedBy) {
|
||||
query = query.select((eb) => this.withDeletedBy(eb));
|
||||
}
|
||||
|
||||
if (opts?.includeSpace) {
|
||||
query = query.select((eb) => this.withSpace(eb));
|
||||
}
|
||||
@@ -100,6 +105,30 @@ export class PageRepo {
|
||||
return query.executeTakeFirst();
|
||||
}
|
||||
|
||||
async findManyByIds(
|
||||
pageIds: string[],
|
||||
opts?: {
|
||||
trx?: KyselyTransaction;
|
||||
workspaceId?: string;
|
||||
},
|
||||
): Promise<Page[]> {
|
||||
if (pageIds.length === 0) return [];
|
||||
const db = dbOrTx(this.db, opts?.trx);
|
||||
|
||||
let query = db
|
||||
.selectFrom('pages')
|
||||
.select(this.baseFields)
|
||||
.where('id', 'in', pageIds);
|
||||
|
||||
if (opts?.workspaceId) {
|
||||
query = query
|
||||
.where('workspaceId', '=', opts.workspaceId)
|
||||
.where('deletedAt', 'is', null);
|
||||
}
|
||||
|
||||
return query.execute();
|
||||
}
|
||||
|
||||
async updatePage(
|
||||
updatablePage: UpdatablePage,
|
||||
pageId: string,
|
||||
@@ -175,11 +204,13 @@ export class PageRepo {
|
||||
.selectFrom('pages')
|
||||
.select(['id'])
|
||||
.where('id', '=', pageId)
|
||||
.where('deletedAt', 'is', null)
|
||||
.unionAll((exp) =>
|
||||
exp
|
||||
.selectFrom('pages as p')
|
||||
.select(['p.id'])
|
||||
.innerJoin('page_descendants as pd', 'pd.id', 'p.parentPageId'),
|
||||
.innerJoin('page_descendants as pd', 'pd.id', 'p.parentPageId')
|
||||
.where('p.deletedAt', 'is', null),
|
||||
),
|
||||
)
|
||||
.selectFrom('page_descendants')
|
||||
@@ -197,6 +228,7 @@ export class PageRepo {
|
||||
deletedAt: currentDate,
|
||||
})
|
||||
.where('id', 'in', pageIds)
|
||||
.where('deletedAt', 'is', null)
|
||||
.execute();
|
||||
|
||||
await trx.deleteFrom('shares').where('pageId', 'in', pageIds).execute();
|
||||
@@ -321,6 +353,35 @@ export class PageRepo {
|
||||
});
|
||||
}
|
||||
|
||||
async getCreatedByPages(creatorId: string, requestingUserId: string, pagination: PaginationOptions, spaceId?: string) {
|
||||
let query = this.db
|
||||
.selectFrom('pages')
|
||||
.select(this.baseFields)
|
||||
.select((eb) => this.withSpace(eb))
|
||||
.where('creatorId', '=', creatorId)
|
||||
.where('deletedAt', 'is', null);
|
||||
|
||||
if (spaceId) {
|
||||
query = query.where('spaceId', '=', spaceId);
|
||||
} else {
|
||||
query = query.where('spaceId', 'in', this.spaceMemberRepo.getUserSpaceIdsQuery(requestingUserId));
|
||||
}
|
||||
|
||||
return executeWithCursorPagination(query, {
|
||||
perPage: pagination.limit,
|
||||
cursor: pagination.cursor,
|
||||
beforeCursor: pagination.beforeCursor,
|
||||
fields: [
|
||||
{ expression: 'updatedAt', direction: 'desc' },
|
||||
{ expression: 'id', direction: 'desc' },
|
||||
],
|
||||
parseCursor: (cursor) => ({
|
||||
updatedAt: new Date(cursor.updatedAt),
|
||||
id: cursor.id,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
async getDeletedPagesInSpace(spaceId: string, pagination: PaginationOptions) {
|
||||
const query = this.db
|
||||
.selectFrom('pages')
|
||||
@@ -472,4 +533,75 @@ export class PageRepo {
|
||||
.selectAll()
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page and all descendants, excluding restricted pages and their subtrees.
|
||||
* More efficient than getPageAndDescendants + filtering because:
|
||||
* 1. Single DB query (no separate restricted IDs query)
|
||||
* 2. Stops traversing at restricted pages (doesn't fetch data to discard)
|
||||
* 3. No in-memory filtering needed
|
||||
*/
|
||||
async getPageAndDescendantsExcludingRestricted(
|
||||
parentPageId: string,
|
||||
opts: { includeContent: boolean },
|
||||
) {
|
||||
return (
|
||||
this.db
|
||||
.withRecursive('page_hierarchy', (db) =>
|
||||
db
|
||||
.selectFrom('pages')
|
||||
.leftJoin('pageAccess', 'pageAccess.pageId', 'pages.id')
|
||||
.select([
|
||||
'pages.id',
|
||||
'pages.slugId',
|
||||
'pages.title',
|
||||
'pages.icon',
|
||||
'pages.position',
|
||||
'pages.parentPageId',
|
||||
'pages.spaceId',
|
||||
'pages.workspaceId',
|
||||
sql<boolean>`page_access.id IS NOT NULL`.as('isRestricted'),
|
||||
])
|
||||
.$if(opts?.includeContent, (qb) => qb.select('pages.content'))
|
||||
.where('pages.id', '=', parentPageId)
|
||||
.where('pages.deletedAt', 'is', null)
|
||||
.unionAll((exp) =>
|
||||
exp
|
||||
.selectFrom('pages as p')
|
||||
.innerJoin('page_hierarchy as ph', 'p.parentPageId', 'ph.id')
|
||||
.leftJoin('pageAccess', 'pageAccess.pageId', 'p.id')
|
||||
.select([
|
||||
'p.id',
|
||||
'p.slugId',
|
||||
'p.title',
|
||||
'p.icon',
|
||||
'p.position',
|
||||
'p.parentPageId',
|
||||
'p.spaceId',
|
||||
'p.workspaceId',
|
||||
sql<boolean>`page_access.id IS NOT NULL`.as('isRestricted'),
|
||||
])
|
||||
.$if(opts?.includeContent, (qb) => qb.select('p.content'))
|
||||
.where('p.deletedAt', 'is', null)
|
||||
// Only recurse into children of non-restricted pages
|
||||
.where('ph.isRestricted', '=', false),
|
||||
),
|
||||
)
|
||||
.selectFrom('page_hierarchy')
|
||||
.select([
|
||||
'id',
|
||||
'slugId',
|
||||
'title',
|
||||
'icon',
|
||||
'position',
|
||||
'parentPageId',
|
||||
'spaceId',
|
||||
'workspaceId',
|
||||
])
|
||||
.$if(opts?.includeContent, (qb) => qb.select('content'))
|
||||
// Filter out restricted pages from the result
|
||||
.where('isRestricted', '=', false)
|
||||
.execute()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
type PagePermissionUserMember = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
avatarUrl: string | null;
|
||||
type: 'user';
|
||||
role: string;
|
||||
createdAt: Date;
|
||||
};
|
||||
|
||||
type PagePermissionGroupMember = {
|
||||
id: string;
|
||||
name: string;
|
||||
memberCount: number;
|
||||
isDefault: boolean;
|
||||
type: 'group';
|
||||
role: string;
|
||||
createdAt: Date;
|
||||
};
|
||||
|
||||
export type PagePermissionMember =
|
||||
| PagePermissionUserMember
|
||||
| PagePermissionGroupMember;
|
||||
@@ -0,0 +1,162 @@
|
||||
import {
|
||||
InsertableUserSession,
|
||||
UserSession,
|
||||
} from '@docmost/db/types/entity.types';
|
||||
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
|
||||
import { dbOrTx } from '@docmost/db/utils';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { sql } from 'kysely';
|
||||
|
||||
@Injectable()
|
||||
export class UserSessionRepo {
|
||||
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
||||
|
||||
async insertSession(
|
||||
session: InsertableUserSession,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<UserSession> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
return db
|
||||
.insertInto('userSessions')
|
||||
.values(session)
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
async findActiveById(id: string): Promise<UserSession | undefined> {
|
||||
return this.db
|
||||
.selectFrom('userSessions')
|
||||
.selectAll()
|
||||
.where('id', '=', id)
|
||||
.where('expiresAt', '>', new Date())
|
||||
.where('revokedAt', 'is', null)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async findActiveByUser(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
): Promise<UserSession[]> {
|
||||
return this.db
|
||||
.selectFrom('userSessions')
|
||||
.selectAll()
|
||||
.where('userId', '=', userId)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.where('expiresAt', '>', new Date())
|
||||
.where('revokedAt', 'is', null)
|
||||
.orderBy('lastActiveAt', 'desc')
|
||||
.execute();
|
||||
}
|
||||
|
||||
async updateLastActiveAt(id: string): Promise<void> {
|
||||
await this.db
|
||||
.updateTable('userSessions')
|
||||
.set({ lastActiveAt: new Date() })
|
||||
.where('id', '=', id)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async revokeById(
|
||||
id: string,
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
): Promise<void> {
|
||||
await this.db
|
||||
.updateTable('userSessions')
|
||||
.set({ revokedAt: new Date() })
|
||||
.where('id', '=', id)
|
||||
.where('userId', '=', userId)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.where('revokedAt', 'is', null)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async revokeAllExceptCurrent(
|
||||
currentSessionId: string,
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
): Promise<void> {
|
||||
await this.db
|
||||
.updateTable('userSessions')
|
||||
.set({ revokedAt: new Date() })
|
||||
.where('userId', '=', userId)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.where('id', '!=', currentSessionId)
|
||||
.where('revokedAt', 'is', null)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async revokeByUserId(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<void> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
await db
|
||||
.updateTable('userSessions')
|
||||
.set({ revokedAt: new Date() })
|
||||
.where('userId', '=', userId)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.where('revokedAt', 'is', null)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async deleteByUserId(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
): Promise<void> {
|
||||
await this.db
|
||||
.deleteFrom('userSessions')
|
||||
.where('userId', '=', userId)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async deleteAllExceptCurrent(
|
||||
currentSessionId: string,
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
): Promise<void> {
|
||||
await this.db
|
||||
.deleteFrom('userSessions')
|
||||
.where('userId', '=', userId)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.where('id', '!=', currentSessionId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async deleteStale(retentionDays: number): Promise<void> {
|
||||
const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000);
|
||||
await this.db
|
||||
.deleteFrom('userSessions')
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb('revokedAt', '<', cutoff),
|
||||
eb('expiresAt', '<', cutoff),
|
||||
]),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async trimExcessSessions(maxPerUser: number): Promise<void> {
|
||||
const overflowed = await this.db
|
||||
.selectFrom('userSessions')
|
||||
.select(['userId', 'workspaceId'])
|
||||
.groupBy(['userId', 'workspaceId'])
|
||||
.having(sql`COUNT(*)`, '>', maxPerUser)
|
||||
.execute();
|
||||
|
||||
for (const { userId, workspaceId } of overflowed) {
|
||||
await sql`
|
||||
DELETE FROM user_sessions
|
||||
WHERE id IN (
|
||||
SELECT id FROM user_sessions
|
||||
WHERE user_id = ${userId} AND workspace_id = ${workspaceId}
|
||||
ORDER BY last_active_at DESC
|
||||
OFFSET ${maxPerUser}
|
||||
)
|
||||
`.execute(this.db);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,15 +136,23 @@ export class ShareRepo {
|
||||
await query.execute();
|
||||
}
|
||||
|
||||
async deleteBySpaceId(spaceId: string): Promise<void> {
|
||||
await this.db
|
||||
async deleteBySpaceId(
|
||||
spaceId: string,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<void> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
await db
|
||||
.deleteFrom('shares')
|
||||
.where('spaceId', '=', spaceId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async deleteByWorkspaceId(workspaceId: string): Promise<void> {
|
||||
await this.db
|
||||
async deleteByWorkspaceId(
|
||||
workspaceId: string,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<void> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
await db
|
||||
.deleteFrom('shares')
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.execute();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { Cache } from 'cache-manager';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
|
||||
import { dbOrTx } from '@docmost/db/utils';
|
||||
@@ -13,6 +15,11 @@ import { MemberInfo, UserSpaceRole } from './types';
|
||||
import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagination';
|
||||
import { GroupRepo } from '@docmost/db/repos/group/group.repo';
|
||||
import { SpaceRepo } from '@docmost/db/repos/space/space.repo';
|
||||
import { withCache } from '../../../common/helpers/with-cache';
|
||||
import {
|
||||
CacheKey,
|
||||
PERMISSION_CACHE_TTL_MS,
|
||||
} from '../../../common/helpers/cache-keys';
|
||||
|
||||
@Injectable()
|
||||
export class SpaceMemberRepo {
|
||||
@@ -20,6 +27,7 @@ export class SpaceMemberRepo {
|
||||
@InjectKysely() private readonly db: KyselyDB,
|
||||
private readonly groupRepo: GroupRepo,
|
||||
private readonly spaceRepo: SpaceRepo,
|
||||
@Inject(CACHE_MANAGER) private readonly cacheManager: Cache,
|
||||
) {}
|
||||
|
||||
async insertSpaceMember(
|
||||
@@ -104,6 +112,7 @@ export class SpaceMemberRepo {
|
||||
.leftJoin('users', 'users.id', 'spaceMembers.userId')
|
||||
.leftJoin('groups', 'groups.id', 'spaceMembers.groupId')
|
||||
.select([
|
||||
'spaceMembers.id as id',
|
||||
'users.id as userId',
|
||||
'users.name as userName',
|
||||
'users.avatarUrl as userAvatarUrl',
|
||||
@@ -120,6 +129,12 @@ export class SpaceMemberRepo {
|
||||
'isGroup',
|
||||
),
|
||||
)
|
||||
.select(
|
||||
sql<number>`case "space_members"."role" when 'admin' then 1 when 'writer' then 2 when 'reader' then 3 else 4 end`.as(
|
||||
'roleOrder',
|
||||
),
|
||||
)
|
||||
.select(sql<string>`coalesce(users.name, groups.name)`.as('memberName'))
|
||||
.where('spaceId', '=', spaceId);
|
||||
|
||||
if (pagination.query) {
|
||||
@@ -149,12 +164,16 @@ export class SpaceMemberRepo {
|
||||
cursor: pagination.cursor,
|
||||
beforeCursor: pagination.beforeCursor,
|
||||
fields: [
|
||||
{ expression: 'sub.roleOrder', direction: 'asc', key: 'roleOrder' },
|
||||
{ expression: 'sub.isGroup', direction: 'desc', key: 'isGroup' },
|
||||
{ expression: 'sub.createdAt', direction: 'asc', key: 'createdAt' },
|
||||
{ expression: 'sub.memberName', direction: 'asc', key: 'memberName' },
|
||||
{ expression: 'sub.id', direction: 'asc', key: 'id' },
|
||||
],
|
||||
parseCursor: (cursor) => ({
|
||||
roleOrder: parseInt(cursor.roleOrder, 10),
|
||||
isGroup: parseInt(cursor.isGroup, 10),
|
||||
createdAt: new Date(cursor.createdAt),
|
||||
memberName: cursor.memberName,
|
||||
id: cursor.id,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -203,25 +222,36 @@ export class SpaceMemberRepo {
|
||||
userId: string,
|
||||
spaceId: string,
|
||||
): Promise<UserSpaceRole[]> {
|
||||
const roles = await this.db
|
||||
.selectFrom('spaceMembers')
|
||||
.select(['userId', 'role'])
|
||||
.where('userId', '=', userId)
|
||||
.where('spaceId', '=', spaceId)
|
||||
.unionAll(
|
||||
this.db
|
||||
return withCache(
|
||||
this.cacheManager,
|
||||
CacheKey.SPACE_ROLES(userId, spaceId),
|
||||
PERMISSION_CACHE_TTL_MS,
|
||||
async () => {
|
||||
const roles = await this.db
|
||||
.selectFrom('spaceMembers')
|
||||
.innerJoin('groupUsers', 'groupUsers.groupId', 'spaceMembers.groupId')
|
||||
.select(['groupUsers.userId', 'spaceMembers.role'])
|
||||
.where('groupUsers.userId', '=', userId)
|
||||
.where('spaceMembers.spaceId', '=', spaceId),
|
||||
)
|
||||
.execute();
|
||||
.select(['userId', 'role'])
|
||||
.where('userId', '=', userId)
|
||||
.where('spaceId', '=', spaceId)
|
||||
.unionAll(
|
||||
this.db
|
||||
.selectFrom('spaceMembers')
|
||||
.innerJoin(
|
||||
'groupUsers',
|
||||
'groupUsers.groupId',
|
||||
'spaceMembers.groupId',
|
||||
)
|
||||
.select(['groupUsers.userId', 'spaceMembers.role'])
|
||||
.where('groupUsers.userId', '=', userId)
|
||||
.where('spaceMembers.spaceId', '=', spaceId),
|
||||
)
|
||||
.execute();
|
||||
|
||||
if (!roles || roles.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return roles;
|
||||
if (!roles || roles.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return roles;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async getUserIdsWithSpaceAccess(
|
||||
@@ -279,6 +309,32 @@ export class SpaceMemberRepo {
|
||||
return membership.map((space) => space.id);
|
||||
}
|
||||
|
||||
async getUserRolesForSpaces(
|
||||
userId: string,
|
||||
spaceIds: string[],
|
||||
): Promise<{ spaceId: string; role: string }[]> {
|
||||
if (spaceIds.length === 0) return [];
|
||||
|
||||
return this.db
|
||||
.selectFrom('spaceMembers')
|
||||
.select(['spaceId', 'role'])
|
||||
.where('userId', '=', userId)
|
||||
.where('spaceId', 'in', spaceIds)
|
||||
.unionAll(
|
||||
this.db
|
||||
.selectFrom('spaceMembers')
|
||||
.innerJoin(
|
||||
'groupUsers',
|
||||
'groupUsers.groupId',
|
||||
'spaceMembers.groupId',
|
||||
)
|
||||
.select(['spaceMembers.spaceId', 'spaceMembers.role'])
|
||||
.where('groupUsers.userId', '=', userId)
|
||||
.where('spaceMembers.spaceId', 'in', spaceIds),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async getUserSpaces(userId: string, pagination: PaginationOptions) {
|
||||
let query = this.db
|
||||
.selectFrom('spaces')
|
||||
@@ -304,8 +360,11 @@ export class SpaceMemberRepo {
|
||||
perPage: pagination.limit,
|
||||
cursor: pagination.cursor,
|
||||
beforeCursor: pagination.beforeCursor,
|
||||
fields: [{ expression: 'id', direction: 'asc' }],
|
||||
parseCursor: (cursor) => ({ id: cursor.id }),
|
||||
fields: [
|
||||
{ expression: 'name', direction: 'asc' },
|
||||
{ expression: 'id', direction: 'asc' },
|
||||
],
|
||||
parseCursor: (cursor) => ({ name: cursor.name, id: cursor.id }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,8 +94,10 @@ export class SpaceRepo {
|
||||
workspaceId: string,
|
||||
prefKey: string,
|
||||
prefValue: string | boolean,
|
||||
trx?: KyselyTransaction,
|
||||
) {
|
||||
return this.db
|
||||
const db = dbOrTx(this.db, trx);
|
||||
return db
|
||||
.updateTable('spaces')
|
||||
.set({
|
||||
settings: sql`COALESCE(settings, '{}'::jsonb)
|
||||
@@ -109,6 +111,28 @@ export class SpaceRepo {
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async updateCommentSettings(
|
||||
spaceId: string,
|
||||
workspaceId: string,
|
||||
prefKey: string,
|
||||
prefValue: string | boolean,
|
||||
trx?: KyselyTransaction,
|
||||
) {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
return db
|
||||
.updateTable('spaces')
|
||||
.set({
|
||||
settings: sql`COALESCE(settings, '{}'::jsonb)
|
||||
|| jsonb_build_object('comments', COALESCE(settings->'comments', '{}'::jsonb)
|
||||
|| jsonb_build_object('${sql.raw(prefKey)}', ${sql.lit(prefValue)}))`,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where('id', '=', spaceId)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.returningAll()
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async insertSpace(
|
||||
insertableSpace: InsertableSpace,
|
||||
trx?: KyselyTransaction,
|
||||
@@ -150,8 +174,11 @@ export class SpaceRepo {
|
||||
perPage: pagination.limit,
|
||||
cursor: pagination.cursor,
|
||||
beforeCursor: pagination.beforeCursor,
|
||||
fields: [{ expression: 'id', direction: 'asc' }],
|
||||
parseCursor: (cursor) => ({ id: cursor.id }),
|
||||
fields: [
|
||||
{ expression: 'name', direction: 'asc' },
|
||||
{ expression: 'id', direction: 'asc' },
|
||||
],
|
||||
parseCursor: (cursor) => ({ name: cursor.name, id: cursor.id }),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
|
||||
import { dbOrTx } from '@docmost/db/utils';
|
||||
import {
|
||||
InsertableTemplate,
|
||||
Page,
|
||||
Template,
|
||||
UpdatableTemplate,
|
||||
} from '@docmost/db/types/entity.types';
|
||||
import { PaginationOptions } from '../../pagination/pagination-options';
|
||||
import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagination';
|
||||
import { ExpressionBuilder, sql } from 'kysely';
|
||||
import { DB } from '@docmost/db/types/db';
|
||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
|
||||
@Injectable()
|
||||
export class TemplateRepo {
|
||||
private baseFields: Array<keyof Template> = [
|
||||
'id',
|
||||
'title',
|
||||
'description',
|
||||
'icon',
|
||||
'spaceId',
|
||||
'workspaceId',
|
||||
'creatorId',
|
||||
'lastUpdatedById',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
];
|
||||
|
||||
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
||||
|
||||
async findById(
|
||||
templateId: string,
|
||||
workspaceId: string,
|
||||
opts?: { includeContent?: boolean; trx?: KyselyTransaction },
|
||||
): Promise<Template> {
|
||||
const db = dbOrTx(this.db, opts?.trx);
|
||||
|
||||
const query = db
|
||||
.selectFrom('templates')
|
||||
.select(this.baseFields)
|
||||
.$if(opts?.includeContent ?? false, (qb) => qb.select('content'))
|
||||
.select((eb) => [this.withCreator(eb)])
|
||||
.where('id', '=', templateId)
|
||||
.where('workspaceId', '=', workspaceId);
|
||||
|
||||
return query.executeTakeFirst() as Promise<Template>;
|
||||
}
|
||||
|
||||
async findTemplates(
|
||||
workspaceId: string,
|
||||
accessibleSpaceIds: string[],
|
||||
pagination: PaginationOptions,
|
||||
opts?: { spaceId?: string },
|
||||
) {
|
||||
let query = this.db
|
||||
.selectFrom('templates')
|
||||
.select(this.baseFields)
|
||||
.select((eb) => [this.withCreator(eb)])
|
||||
.where('workspaceId', '=', workspaceId);
|
||||
|
||||
if (opts?.spaceId) {
|
||||
if (!accessibleSpaceIds.includes(opts.spaceId)) {
|
||||
query = query.where(sql<boolean>`false`);
|
||||
} else {
|
||||
query = query.where('spaceId', '=', opts.spaceId);
|
||||
}
|
||||
} else {
|
||||
query = query.where((eb) =>
|
||||
eb.or([
|
||||
eb('spaceId', 'is', null),
|
||||
...(accessibleSpaceIds.length > 0
|
||||
? [eb('spaceId', 'in', accessibleSpaceIds)]
|
||||
: []),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
if (pagination.query) {
|
||||
const searchTerm = `%${pagination.query}%`;
|
||||
query = query.where((eb) =>
|
||||
eb.or([
|
||||
eb(sql`f_unaccent(title)`, 'ilike', sql`f_unaccent(${searchTerm})`),
|
||||
eb(
|
||||
sql`f_unaccent(description)`,
|
||||
'ilike',
|
||||
sql`f_unaccent(${searchTerm})`,
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
return executeWithCursorPagination(query, {
|
||||
perPage: pagination.limit,
|
||||
cursor: pagination.cursor,
|
||||
beforeCursor: pagination.beforeCursor,
|
||||
fields: [
|
||||
{ expression: 'title', direction: 'asc' },
|
||||
{ expression: 'id', direction: 'asc' },
|
||||
],
|
||||
parseCursor: (cursor) => ({
|
||||
title: cursor.title,
|
||||
id: cursor.id,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
async insertTemplate(
|
||||
insertableTemplate: InsertableTemplate,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<{ id: string }> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
return db
|
||||
.insertInto('templates')
|
||||
.values(insertableTemplate)
|
||||
.returning('id')
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async updateTemplate(
|
||||
updatableTemplate: UpdatableTemplate,
|
||||
templateId: string,
|
||||
workspaceId: string,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<void> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
await db
|
||||
.updateTable('templates')
|
||||
.set({ ...updatableTemplate, updatedAt: new Date() })
|
||||
.where('id', '=', templateId)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async deleteTemplate(
|
||||
templateId: string,
|
||||
workspaceId: string,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<void> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
await db
|
||||
.deleteFrom('templates')
|
||||
.where('id', '=', templateId)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
withCreator(eb: ExpressionBuilder<DB, 'templates'>) {
|
||||
return jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom('users')
|
||||
.select(['users.id', 'users.name', 'users.avatarUrl'])
|
||||
.whereRef('users.id', '=', 'templates.creatorId'),
|
||||
).as('creator');
|
||||
}
|
||||
}
|
||||
@@ -38,9 +38,9 @@ export class UserTokenRepo {
|
||||
|
||||
async insertUserToken(
|
||||
insertableUserToken: InsertableUserToken,
|
||||
trx?: KyselyTransaction,
|
||||
opts?: { trx?: KyselyTransaction },
|
||||
) {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
const db = dbOrTx(this.db, opts?.trx);
|
||||
return db
|
||||
.insertInto('userTokens')
|
||||
.values(insertableUserToken)
|
||||
|
||||
@@ -13,6 +13,7 @@ import { PaginationOptions } from '../../pagination/pagination-options';
|
||||
import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagination';
|
||||
import { ExpressionBuilder, sql } from 'kysely';
|
||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { NotificationSettingKey } from '../../../core/notification/notification.constants';
|
||||
|
||||
@Injectable()
|
||||
export class UserRepo {
|
||||
@@ -43,6 +44,7 @@ export class UserRepo {
|
||||
opts?: {
|
||||
includePassword?: boolean;
|
||||
includeUserMfa?: boolean;
|
||||
includeScimExternalId?: boolean;
|
||||
trx?: KyselyTransaction;
|
||||
},
|
||||
): Promise<User> {
|
||||
@@ -52,6 +54,7 @@ export class UserRepo {
|
||||
.select(this.baseFields)
|
||||
.$if(opts?.includePassword, (qb) => qb.select('password'))
|
||||
.$if(opts?.includeUserMfa, (qb) => qb.select(this.withUserMfa))
|
||||
.$if(opts?.includeScimExternalId, (qb) => qb.select('scimExternalId'))
|
||||
.where('id', '=', userId)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.executeTakeFirst();
|
||||
@@ -63,6 +66,7 @@ export class UserRepo {
|
||||
opts?: {
|
||||
includePassword?: boolean;
|
||||
includeUserMfa?: boolean;
|
||||
includeScimExternalId?: boolean;
|
||||
trx?: KyselyTransaction;
|
||||
},
|
||||
): Promise<User> {
|
||||
@@ -72,6 +76,7 @@ export class UserRepo {
|
||||
.select(this.baseFields)
|
||||
.$if(opts?.includePassword, (qb) => qb.select('password'))
|
||||
.$if(opts?.includeUserMfa, (qb) => qb.select(this.withUserMfa))
|
||||
.$if(opts?.includeScimExternalId, (qb) => qb.select('scimExternalId'))
|
||||
.where(sql`LOWER(email)`, '=', sql`LOWER(${email})`)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.executeTakeFirst();
|
||||
@@ -165,8 +170,11 @@ export class UserRepo {
|
||||
perPage: pagination.limit,
|
||||
cursor: pagination.cursor,
|
||||
beforeCursor: pagination.beforeCursor,
|
||||
fields: [{ expression: 'id', direction: 'asc' }],
|
||||
parseCursor: (cursor) => ({ id: cursor.id }),
|
||||
fields: [
|
||||
{ expression: 'name', direction: 'asc' },
|
||||
{ expression: 'id', direction: 'asc' },
|
||||
],
|
||||
parseCursor: (cursor) => ({ name: cursor.name, id: cursor.id }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -188,6 +196,24 @@ export class UserRepo {
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async updateNotificationSetting(
|
||||
userId: string,
|
||||
settingKey: NotificationSettingKey,
|
||||
settingValue: boolean,
|
||||
) {
|
||||
return await this.db
|
||||
.updateTable('users')
|
||||
.set({
|
||||
settings: sql`COALESCE(settings, '{}'::jsonb)
|
||||
|| jsonb_build_object('notifications', COALESCE(settings->'notifications', '{}'::jsonb)
|
||||
|| jsonb_build_object(${sql.lit(settingKey)}, ${sql.lit(settingValue)}))`,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where('id', '=', userId)
|
||||
.returning(this.baseFields)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
withUserMfa(eb: ExpressionBuilder<DB, 'users'>) {
|
||||
return jsonObjectFrom(
|
||||
eb
|
||||
|
||||
@@ -20,18 +20,6 @@ export type WatcherType = (typeof WatcherType)[keyof typeof WatcherType];
|
||||
export class WatcherRepo {
|
||||
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
||||
|
||||
async findByUserAndPage(
|
||||
userId: string,
|
||||
pageId: string,
|
||||
): Promise<Watcher | undefined> {
|
||||
return this.db
|
||||
.selectFrom('watchers')
|
||||
.selectAll()
|
||||
.where('userId', '=', userId)
|
||||
.where('pageId', '=', pageId)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async findPageWatchers(pageId: string, pagination: PaginationOptions) {
|
||||
const query = this.db
|
||||
.selectFrom('watchers')
|
||||
@@ -66,6 +54,53 @@ export class WatcherRepo {
|
||||
return watchers.map((w) => w.userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recipients for a `page.updated` notification, combining:
|
||||
* - Active page watchers on this page, AND
|
||||
* - Active space watchers on this space, EXCLUDING any user who has a
|
||||
* muted page watcher row for this page (per-page mute always wins).
|
||||
*
|
||||
* Deduplicated at the SQL level — a user watching both the page and the
|
||||
* containing space appears once.
|
||||
*/
|
||||
async getPageUpdateRecipientIds(
|
||||
pageId: string,
|
||||
spaceId: string,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<string[]> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
|
||||
const pageWatchers = db
|
||||
.selectFrom('watchers')
|
||||
.select('userId')
|
||||
.where('pageId', '=', pageId)
|
||||
.where('type', '=', WatcherType.PAGE)
|
||||
.where('mutedAt', 'is', null);
|
||||
|
||||
const spaceWatchers = db
|
||||
.selectFrom('watchers as sw')
|
||||
.select('sw.userId')
|
||||
.where('sw.spaceId', '=', spaceId)
|
||||
.where('sw.pageId', 'is', null)
|
||||
.where('sw.type', '=', WatcherType.SPACE)
|
||||
.where((eb) =>
|
||||
eb.not(
|
||||
eb.exists(
|
||||
eb
|
||||
.selectFrom('watchers as pw')
|
||||
.select('pw.id')
|
||||
.whereRef('pw.userId', '=', 'sw.userId')
|
||||
.where('pw.pageId', '=', pageId)
|
||||
.where('pw.type', '=', WatcherType.PAGE)
|
||||
.where('pw.mutedAt', 'is not', null),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const rows = await pageWatchers.union(spaceWatchers).execute();
|
||||
return [...new Set(rows.map((r) => r.userId))];
|
||||
}
|
||||
|
||||
async insert(
|
||||
watcher: InsertableWatcher,
|
||||
trx?: KyselyTransaction,
|
||||
@@ -110,20 +145,97 @@ export class WatcherRepo {
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async upsertSpace(
|
||||
watcher: InsertableWatcher,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<Watcher | undefined> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
return db
|
||||
.insertInto('watchers')
|
||||
.values(watcher)
|
||||
.onConflict((oc) =>
|
||||
oc
|
||||
.columns(['userId', 'spaceId'])
|
||||
.where('pageId', 'is', null)
|
||||
.doNothing(),
|
||||
)
|
||||
.returningAll()
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async mute(
|
||||
userId: string,
|
||||
pageId: string,
|
||||
spaceId: string,
|
||||
workspaceId: string,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<void> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
const mutedAt = new Date();
|
||||
await db
|
||||
.insertInto('watchers')
|
||||
.values({
|
||||
userId,
|
||||
pageId,
|
||||
spaceId,
|
||||
workspaceId,
|
||||
type: WatcherType.PAGE,
|
||||
addedById: userId,
|
||||
mutedAt,
|
||||
})
|
||||
.onConflict((oc) =>
|
||||
oc
|
||||
.columns(['userId', 'pageId'])
|
||||
.where('pageId', 'is not', null)
|
||||
.doUpdateSet({ mutedAt }),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async deleteSpaceWatch(
|
||||
userId: string,
|
||||
spaceId: string,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<void> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
await db
|
||||
.updateTable('watchers')
|
||||
.set({ mutedAt: new Date() })
|
||||
.deleteFrom('watchers')
|
||||
.where('userId', '=', userId)
|
||||
.where('pageId', '=', pageId)
|
||||
.where('spaceId', '=', spaceId)
|
||||
.where('pageId', 'is', null)
|
||||
.where('type', '=', WatcherType.SPACE)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async getWatchedSpaceIds(userId: string, workspaceId: string) {
|
||||
const query = this.db
|
||||
.selectFrom('watchers')
|
||||
.select(['watchers.id', 'watchers.spaceId'])
|
||||
.where('userId', '=', userId)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.where('pageId', 'is', null)
|
||||
.where('type', '=', WatcherType.SPACE);
|
||||
|
||||
return executeWithCursorPagination(query, {
|
||||
perPage: 250,
|
||||
fields: [{ expression: 'watchers.id', direction: 'asc' }],
|
||||
parseCursor: (cursor) => ({ id: cursor.id }),
|
||||
});
|
||||
}
|
||||
|
||||
async isWatchingSpace(userId: string, spaceId: string): Promise<boolean> {
|
||||
const watcher = await this.db
|
||||
.selectFrom('watchers')
|
||||
.select('id')
|
||||
.where('userId', '=', userId)
|
||||
.where('spaceId', '=', spaceId)
|
||||
.where('pageId', 'is', null)
|
||||
.where('type', '=', WatcherType.SPACE)
|
||||
.executeTakeFirst();
|
||||
|
||||
return !!watcher;
|
||||
}
|
||||
|
||||
async isWatching(userId: string, pageId: string): Promise<boolean> {
|
||||
const watcher = await this.db
|
||||
.selectFrom('watchers')
|
||||
@@ -164,14 +276,14 @@ export class WatcherRepo {
|
||||
.where('spaceId', '=', spaceId)
|
||||
.where('userId', 'is not', null)
|
||||
.union(
|
||||
this.db
|
||||
db
|
||||
.selectFrom('spaceMembers')
|
||||
.innerJoin('groupUsers', 'groupUsers.groupId', 'spaceMembers.groupId')
|
||||
.select('groupUsers.userId')
|
||||
.where('spaceMembers.spaceId', '=', spaceId),
|
||||
);
|
||||
|
||||
await this.db
|
||||
await db
|
||||
.deleteFrom('watchers')
|
||||
.where('userId', 'in', userIds)
|
||||
.where('spaceId', '=', spaceId)
|
||||
|
||||
@@ -33,6 +33,8 @@ export class WorkspaceRepo {
|
||||
'enforceSso',
|
||||
'plan',
|
||||
'enforceMfa',
|
||||
'trashRetentionDays',
|
||||
'isScimEnabled',
|
||||
];
|
||||
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
||||
|
||||
@@ -67,6 +69,17 @@ export class WorkspaceRepo {
|
||||
return query.executeTakeFirst();
|
||||
}
|
||||
|
||||
async findLicenseKeyById(
|
||||
workspaceId: string,
|
||||
): Promise<string | undefined> {
|
||||
const row = await this.db
|
||||
.selectFrom('workspaces')
|
||||
.select('licenseKey')
|
||||
.where('id', '=', workspaceId)
|
||||
.executeTakeFirst();
|
||||
return row?.licenseKey;
|
||||
}
|
||||
|
||||
async findFirst(): Promise<Workspace> {
|
||||
return await this.db
|
||||
.selectFrom('workspaces')
|
||||
@@ -162,8 +175,10 @@ export class WorkspaceRepo {
|
||||
workspaceId: string,
|
||||
prefKey: string,
|
||||
prefValue: string | boolean,
|
||||
trx?: KyselyTransaction,
|
||||
) {
|
||||
return this.db
|
||||
const db = dbOrTx(this.db, trx);
|
||||
return db
|
||||
.updateTable('workspaces')
|
||||
.set({
|
||||
settings: sql`COALESCE(settings, '{}'::jsonb)
|
||||
@@ -180,8 +195,10 @@ export class WorkspaceRepo {
|
||||
workspaceId: string,
|
||||
prefKey: string,
|
||||
prefValue: string | boolean,
|
||||
trx?: KyselyTransaction,
|
||||
) {
|
||||
return this.db
|
||||
const db = dbOrTx(this.db, trx);
|
||||
return db
|
||||
.updateTable('workspaces')
|
||||
.set({
|
||||
settings: sql`COALESCE(settings, '{}'::jsonb)
|
||||
@@ -198,8 +215,10 @@ export class WorkspaceRepo {
|
||||
workspaceId: string,
|
||||
prefKey: string,
|
||||
prefValue: string | boolean,
|
||||
trx?: KyselyTransaction,
|
||||
) {
|
||||
return this.db
|
||||
const db = dbOrTx(this.db, trx);
|
||||
return db
|
||||
.updateTable('workspaces')
|
||||
.set({
|
||||
settings: sql`COALESCE(settings, '{}'::jsonb)
|
||||
@@ -211,4 +230,25 @@ export class WorkspaceRepo {
|
||||
.returning(this.baseFields)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async updateTemplateSettings(
|
||||
workspaceId: string,
|
||||
prefKey: string,
|
||||
prefValue: string | boolean,
|
||||
trx?: KyselyTransaction,
|
||||
) {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
return db
|
||||
.updateTable('workspaces')
|
||||
.set({
|
||||
settings: sql`COALESCE(settings, '{}'::jsonb)
|
||||
|| jsonb_build_object('templates', COALESCE(settings->'templates', '{}'::jsonb)
|
||||
|| jsonb_build_object('${sql.raw(prefKey)}', ${sql.lit(prefValue)}))`,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where('id', '=', workspaceId)
|
||||
.returning(this.baseFields)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+214
@@ -43,6 +43,7 @@ export interface ApiKeys {
|
||||
}
|
||||
|
||||
export interface Attachments {
|
||||
aiChatId: string | null;
|
||||
createdAt: Generated<Timestamp>;
|
||||
creatorId: string;
|
||||
deletedAt: Timestamp | null;
|
||||
@@ -61,6 +62,21 @@ export interface Attachments {
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
export interface Audit {
|
||||
actorId: string | null;
|
||||
actorType: Generated<string>;
|
||||
changes: Json | null;
|
||||
createdAt: Generated<Timestamp>;
|
||||
event: string;
|
||||
id: Generated<string>;
|
||||
ipAddress: string | null;
|
||||
metadata: Json | null;
|
||||
resourceId: string | null;
|
||||
resourceType: string;
|
||||
spaceId: string | null;
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
export interface AuthAccounts {
|
||||
authProviderId: string | null;
|
||||
createdAt: Generated<Timestamp>;
|
||||
@@ -159,6 +175,17 @@ export interface Comments {
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
export interface Favorites {
|
||||
id: Generated<string>;
|
||||
userId: string;
|
||||
pageId: string | null;
|
||||
spaceId: string | null;
|
||||
templateId: string | null;
|
||||
type: string;
|
||||
workspaceId: string;
|
||||
createdAt: Generated<Timestamp>;
|
||||
}
|
||||
|
||||
export interface FileTasks {
|
||||
createdAt: Generated<Timestamp>;
|
||||
creatorId: string | null;
|
||||
@@ -169,6 +196,8 @@ export interface FileTasks {
|
||||
filePath: string;
|
||||
fileSize: Int8 | null;
|
||||
id: Generated<string>;
|
||||
metadata: Json | null;
|
||||
pageId: string | null;
|
||||
source: string | null;
|
||||
spaceId: string | null;
|
||||
status: string | null;
|
||||
@@ -184,7 +213,9 @@ export interface Groups {
|
||||
description: string | null;
|
||||
id: Generated<string>;
|
||||
isDefault: boolean;
|
||||
isExternal: Generated<boolean>;
|
||||
name: string;
|
||||
scimExternalId: string | null;
|
||||
updatedAt: Generated<Timestamp>;
|
||||
workspaceId: string;
|
||||
}
|
||||
@@ -197,6 +228,25 @@ export interface GroupUsers {
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface PageTransclusionReferences {
|
||||
createdAt: Generated<Timestamp>;
|
||||
transclusionId: string;
|
||||
referencePageId: string;
|
||||
id: Generated<string>;
|
||||
sourcePageId: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
export interface PageTransclusions {
|
||||
content: Json;
|
||||
createdAt: Generated<Timestamp>;
|
||||
transclusionId: string;
|
||||
id: Generated<string>;
|
||||
pageId: string;
|
||||
updatedAt: Generated<Timestamp>;
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
export interface PageHistory {
|
||||
content: Json | null;
|
||||
contributorIds: Generated<string[] | null>;
|
||||
@@ -309,6 +359,7 @@ export interface Users {
|
||||
name: string | null;
|
||||
password: string | null;
|
||||
role: string | null;
|
||||
scimExternalId: string | null;
|
||||
settings: Json | null;
|
||||
timezone: string | null;
|
||||
updatedAt: Generated<Timestamp>;
|
||||
@@ -339,6 +390,8 @@ export interface WorkspaceInvitations {
|
||||
}
|
||||
|
||||
export interface Workspaces {
|
||||
auditRetentionDays: Generated<number>;
|
||||
trashRetentionDays: Generated<number>;
|
||||
billingEmail: string | null;
|
||||
createdAt: Generated<Timestamp>;
|
||||
customDomain: string | null;
|
||||
@@ -350,6 +403,7 @@ export interface Workspaces {
|
||||
enforceMfa: Generated<boolean | null>;
|
||||
enforceSso: Generated<boolean>;
|
||||
hostname: string | null;
|
||||
isScimEnabled: Generated<boolean>;
|
||||
id: Generated<string>;
|
||||
licenseKey: string | null;
|
||||
logo: string | null;
|
||||
@@ -371,6 +425,7 @@ export interface Notifications {
|
||||
pageId: string | null;
|
||||
spaceId: string | null;
|
||||
commentId: string | null;
|
||||
pageVerificationId: string | null;
|
||||
data: Json | null;
|
||||
readAt: Timestamp | null;
|
||||
emailedAt: Timestamp | null;
|
||||
@@ -378,6 +433,20 @@ export interface Notifications {
|
||||
createdAt: Generated<Timestamp>;
|
||||
}
|
||||
|
||||
export interface ScimTokens {
|
||||
createdAt: Generated<Timestamp>;
|
||||
deletedAt: Timestamp | null;
|
||||
id: Generated<string>;
|
||||
isEnabled: Generated<boolean>;
|
||||
lastUsedAt: Timestamp | null;
|
||||
name: string;
|
||||
tokenHash: string;
|
||||
tokenLastFour: string;
|
||||
creatorId: string | null;
|
||||
updatedAt: Generated<Timestamp>;
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
export interface Watchers {
|
||||
id: Generated<string>;
|
||||
userId: string;
|
||||
@@ -429,28 +498,173 @@ export interface IntegrationWebhooks {
|
||||
updatedAt: Generated<Timestamp>;
|
||||
}
|
||||
|
||||
export interface Labels {
|
||||
id: Generated<string>;
|
||||
name: string;
|
||||
type: Generated<string>;
|
||||
workspaceId: string;
|
||||
createdAt: Generated<Timestamp>;
|
||||
updatedAt: Generated<Timestamp>;
|
||||
}
|
||||
|
||||
export interface PageAccess {
|
||||
id: Generated<string>;
|
||||
pageId: string;
|
||||
workspaceId: string;
|
||||
spaceId: string;
|
||||
accessLevel: string;
|
||||
creatorId: string | null;
|
||||
createdAt: Generated<Timestamp>;
|
||||
updatedAt: Generated<Timestamp>;
|
||||
}
|
||||
|
||||
export interface PageLabels {
|
||||
id: Generated<string>;
|
||||
pageId: string;
|
||||
labelId: string;
|
||||
createdAt: Generated<Timestamp>;
|
||||
}
|
||||
|
||||
export interface PagePermissions {
|
||||
id: Generated<string>;
|
||||
pageAccessId: string;
|
||||
userId: string | null;
|
||||
groupId: string | null;
|
||||
role: string;
|
||||
addedById: string | null;
|
||||
createdAt: Generated<Timestamp>;
|
||||
updatedAt: Generated<Timestamp>;
|
||||
}
|
||||
|
||||
export interface PageVerifications {
|
||||
id: Generated<string>;
|
||||
pageId: string;
|
||||
workspaceId: string;
|
||||
spaceId: string;
|
||||
type: Generated<string>;
|
||||
status: string | null;
|
||||
mode: string | null;
|
||||
periodAmount: number | null;
|
||||
periodUnit: string | null;
|
||||
verifiedAt: Timestamp | null;
|
||||
verifiedById: string | null;
|
||||
expiresAt: Timestamp | null;
|
||||
requestedAt: Timestamp | null;
|
||||
requestedById: string | null;
|
||||
rejectedAt: Timestamp | null;
|
||||
rejectedById: string | null;
|
||||
rejectionComment: string | null;
|
||||
data: Json | null;
|
||||
creatorId: string | null;
|
||||
createdAt: Generated<Timestamp>;
|
||||
updatedAt: Generated<Timestamp>;
|
||||
}
|
||||
|
||||
export interface PageVerifiers {
|
||||
id: Generated<string>;
|
||||
pageVerificationId: string;
|
||||
userId: string;
|
||||
isPrimary: Generated<boolean>;
|
||||
addedById: string | null;
|
||||
createdAt: Generated<Timestamp>;
|
||||
}
|
||||
|
||||
export interface Templates {
|
||||
id: Generated<string>;
|
||||
title: string | null;
|
||||
description: string | null;
|
||||
content: Json | null;
|
||||
ydoc: Buffer | null;
|
||||
icon: string | null;
|
||||
spaceId: string | null;
|
||||
workspaceId: string;
|
||||
creatorId: string | null;
|
||||
lastUpdatedById: string | null;
|
||||
collaboratorIds: string[] | null;
|
||||
textContent: string | null;
|
||||
tsv: string | null;
|
||||
createdAt: Generated<Timestamp>;
|
||||
updatedAt: Generated<Timestamp>;
|
||||
deletedAt: Timestamp | null;
|
||||
}
|
||||
|
||||
export interface AiChats {
|
||||
id: Generated<string>;
|
||||
workspaceId: string;
|
||||
creatorId: string;
|
||||
title: string | null;
|
||||
createdAt: Generated<Timestamp>;
|
||||
updatedAt: Generated<Timestamp>;
|
||||
deletedAt: Timestamp | null;
|
||||
}
|
||||
|
||||
export interface AiChatMessages {
|
||||
id: Generated<string>;
|
||||
chatId: string;
|
||||
workspaceId: string;
|
||||
userId: string | null;
|
||||
role: string;
|
||||
content: string | null;
|
||||
toolCalls: Json | null;
|
||||
metadata: Json | null;
|
||||
tsv: string | null;
|
||||
createdAt: Generated<Timestamp>;
|
||||
updatedAt: Generated<Timestamp>;
|
||||
deletedAt: Timestamp | null;
|
||||
}
|
||||
|
||||
export interface UserSessions {
|
||||
id: Generated<string>;
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
deviceName: string | null;
|
||||
userAgent: string | null;
|
||||
ipAddress: string | null;
|
||||
geoLocation: string | null;
|
||||
metadata: Json | null;
|
||||
lastActiveAt: Generated<Timestamp>;
|
||||
expiresAt: Timestamp;
|
||||
revokedAt: Timestamp | null;
|
||||
createdAt: Generated<Timestamp>;
|
||||
}
|
||||
|
||||
export interface DB {
|
||||
aiChats: AiChats;
|
||||
aiChatMessages: AiChatMessages;
|
||||
apiKeys: ApiKeys;
|
||||
attachments: Attachments;
|
||||
audit: Audit;
|
||||
authAccounts: AuthAccounts;
|
||||
authProviders: AuthProviders;
|
||||
backlinks: Backlinks;
|
||||
billing: Billing;
|
||||
comments: Comments;
|
||||
favorites: Favorites;
|
||||
fileTasks: FileTasks;
|
||||
groups: Groups;
|
||||
groupUsers: GroupUsers;
|
||||
integrationConnections: IntegrationConnections;
|
||||
integrationWebhooks: IntegrationWebhooks;
|
||||
integrations: Integrations;
|
||||
labels: Labels;
|
||||
notifications: Notifications;
|
||||
pageAccess: PageAccess;
|
||||
pageTransclusionReferences: PageTransclusionReferences;
|
||||
pageTransclusions: PageTransclusions;
|
||||
pagePermissions: PagePermissions;
|
||||
pageHistory: PageHistory;
|
||||
pageLabels: PageLabels;
|
||||
pageVerifications: PageVerifications;
|
||||
pageVerifiers: PageVerifiers;
|
||||
pages: Pages;
|
||||
scimTokens: ScimTokens;
|
||||
shares: Shares;
|
||||
spaceMembers: SpaceMembers;
|
||||
spaces: Spaces;
|
||||
templates: Templates;
|
||||
userMfa: UserMfa;
|
||||
users: Users;
|
||||
userSessions: UserSessions;
|
||||
userTokens: UserTokens;
|
||||
watchers: Watchers;
|
||||
workspaceInvitations: WorkspaceInvitations;
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
import { Insertable, Selectable, Updateable } from 'kysely';
|
||||
import {
|
||||
AiChats,
|
||||
AiChatMessages,
|
||||
Attachments,
|
||||
Comments,
|
||||
Groups,
|
||||
Integrations as _Integrations,
|
||||
IntegrationConnections as _IntegrationConnections,
|
||||
IntegrationWebhooks as _IntegrationWebhooks,
|
||||
Labels,
|
||||
Notifications,
|
||||
PageLabels,
|
||||
PageAccess as _PageAccess,
|
||||
PageTransclusions,
|
||||
PageTransclusionReferences,
|
||||
PagePermissions as _PagePermissions,
|
||||
PageVerifications as _PageVerifications,
|
||||
PageVerifiers as _PageVerifiers,
|
||||
Pages,
|
||||
Spaces,
|
||||
Users,
|
||||
@@ -21,13 +31,33 @@ import {
|
||||
AuthProviders,
|
||||
AuthAccounts,
|
||||
Shares,
|
||||
Favorites,
|
||||
FileTasks,
|
||||
UserMfa as _UserMFA,
|
||||
UserSessions,
|
||||
ApiKeys,
|
||||
ScimTokens,
|
||||
Watchers,
|
||||
Audit as _Audit,
|
||||
Templates,
|
||||
} from './db';
|
||||
import { PageEmbeddings } from '@docmost/db/types/embeddings.types';
|
||||
|
||||
// AI Chat
|
||||
export type AiChat = Selectable<AiChats>;
|
||||
export type InsertableAiChat = Insertable<AiChats>;
|
||||
export type UpdatableAiChat = Updateable<Omit<AiChats, 'id'>>;
|
||||
|
||||
// AI Chat Message
|
||||
// `tsv` is an internal tsvector column maintained by a trigger for
|
||||
// full-text search. It is omitted from the public type so it never leaks
|
||||
// into HTTP responses or the chat history fed to the language model.
|
||||
export type AiChatMessage = Omit<Selectable<AiChatMessages>, 'tsv'>;
|
||||
export type InsertableAiChatMessage = Omit<
|
||||
Insertable<AiChatMessages>,
|
||||
'tsv'
|
||||
>;
|
||||
|
||||
// Workspace
|
||||
export type Workspace = Selectable<Workspaces>;
|
||||
export type InsertableWorkspace = Insertable<Workspaces>;
|
||||
@@ -117,6 +147,23 @@ export type Share = Selectable<Shares>;
|
||||
export type InsertableShare = Insertable<Shares>;
|
||||
export type UpdatableShare = Updateable<Omit<Shares, 'id'>>;
|
||||
|
||||
// Favorite
|
||||
export type Favorite = Selectable<Favorites>;
|
||||
export type InsertableFavorite = Insertable<Favorites>;
|
||||
export type UpdatableFavorite = Updateable<Omit<Favorites, 'id'>>;
|
||||
|
||||
// Page Transclusion
|
||||
export type PageTransclusion = Selectable<PageTransclusions>;
|
||||
export type InsertablePageTransclusion = Insertable<PageTransclusions>;
|
||||
export type UpdatablePageTransclusion = Updateable<Omit<PageTransclusions, 'id'>>;
|
||||
|
||||
// Page Transclusion Reference
|
||||
export type PageTransclusionReference = Selectable<PageTransclusionReferences>;
|
||||
export type InsertablePageTransclusionReference = Insertable<PageTransclusionReferences>;
|
||||
export type UpdatablePageTransclusionReference = Updateable<
|
||||
Omit<PageTransclusionReferences, 'id'>
|
||||
>;
|
||||
|
||||
// File Task
|
||||
export type FileTask = Selectable<FileTasks>;
|
||||
export type InsertableFileTask = Insertable<FileTasks>;
|
||||
@@ -132,6 +179,11 @@ export type ApiKey = Selectable<ApiKeys>;
|
||||
export type InsertableApiKey = Insertable<ApiKeys>;
|
||||
export type UpdatableApiKey = Updateable<Omit<ApiKeys, 'id'>>;
|
||||
|
||||
// Scim Tokens
|
||||
export type ScimToken = Selectable<ScimTokens>;
|
||||
export type InsertableScimToken = Insertable<ScimTokens>;
|
||||
export type UpdatableScimToken = Updateable<Omit<ScimTokens, 'id'>>;
|
||||
|
||||
// Page Embedding
|
||||
export type PageEmbedding = Selectable<PageEmbeddings>;
|
||||
export type InsertablePageEmbedding = Insertable<PageEmbeddings>;
|
||||
@@ -166,3 +218,46 @@ export type InsertableIntegrationWebhook = Insertable<_IntegrationWebhooks>;
|
||||
export type UpdatableIntegrationWebhook = Updateable<
|
||||
Omit<_IntegrationWebhooks, 'id'>
|
||||
>;
|
||||
|
||||
// Label
|
||||
export type Label = Selectable<Labels>;
|
||||
export type InsertableLabel = Insertable<Labels>;
|
||||
export type UpdatableLabel = Updateable<Omit<Labels, 'id'>>;
|
||||
|
||||
// PageLabel
|
||||
export type PageLabel = Selectable<PageLabels>;
|
||||
export type InsertablePageLabel = Insertable<PageLabels>;
|
||||
|
||||
// Page Access
|
||||
export type PageAccess = Selectable<_PageAccess>;
|
||||
export type InsertablePageAccess = Insertable<_PageAccess>;
|
||||
export type UpdatablePageAccess = Updateable<Omit<_PageAccess, 'id'>>;
|
||||
|
||||
// Page Permission
|
||||
export type PagePermission = Selectable<_PagePermissions>;
|
||||
export type InsertablePagePermission = Insertable<_PagePermissions>;
|
||||
export type UpdatablePagePermission = Updateable<Omit<_PagePermissions, 'id'>>;
|
||||
|
||||
// Page Verification
|
||||
export type PageVerification = Selectable<_PageVerifications>;
|
||||
export type InsertablePageVerification = Insertable<_PageVerifications>;
|
||||
export type UpdatablePageVerification = Updateable<Omit<_PageVerifications, 'id'>>;
|
||||
|
||||
// Page Verifier
|
||||
export type PageVerifier = Selectable<_PageVerifiers>;
|
||||
export type InsertablePageVerifier = Insertable<_PageVerifiers>;
|
||||
|
||||
// User Session
|
||||
export type UserSession = Selectable<UserSessions>;
|
||||
export type InsertableUserSession = Insertable<UserSessions>;
|
||||
export type UpdatableUserSession = Updateable<Omit<UserSessions, 'id'>>;
|
||||
|
||||
// Audit
|
||||
export type Audit = Selectable<_Audit>;
|
||||
export type InsertableAudit = Insertable<_Audit>;
|
||||
export type UpdatableAudit = Updateable<Omit<_Audit, 'id'>>;
|
||||
|
||||
// Template
|
||||
export type Template = Selectable<Templates>;
|
||||
export type InsertableTemplate = Insertable<Templates>;
|
||||
export type UpdatableTemplate = Updateable<Omit<Templates, 'id'>>;
|
||||
|
||||
Reference in New Issue
Block a user