mirror of
https://github.com/docmost/docmost.git
synced 2026-08-28 09:17:06 +08:00
feat: notifications (#1947)
* feat: notifications * feat: watchers * improvements * handle page move for watchers * make watchers non-blocking * more
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import { type Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.createTable('notifications')
|
||||
.addColumn('id', 'uuid', (col) =>
|
||||
col.primaryKey().defaultTo(sql`gen_uuid_v7()`),
|
||||
)
|
||||
.addColumn('user_id', 'uuid', (col) =>
|
||||
col.references('users.id').onDelete('cascade').notNull(),
|
||||
)
|
||||
.addColumn('workspace_id', 'uuid', (col) =>
|
||||
col.references('workspaces.id').onDelete('cascade').notNull(),
|
||||
)
|
||||
.addColumn('type', 'text', (col) => col.notNull())
|
||||
.addColumn('actor_id', 'uuid', (col) =>
|
||||
col.references('users.id').onDelete('set null'),
|
||||
)
|
||||
.addColumn('page_id', 'uuid', (col) =>
|
||||
col.references('pages.id').onDelete('cascade'),
|
||||
)
|
||||
.addColumn('space_id', 'uuid', (col) =>
|
||||
col.references('spaces.id').onDelete('cascade'),
|
||||
)
|
||||
.addColumn('comment_id', 'uuid', (col) =>
|
||||
col.references('comments.id').onDelete('cascade'),
|
||||
)
|
||||
.addColumn('data', 'jsonb')
|
||||
.addColumn('read_at', 'timestamptz')
|
||||
.addColumn('emailed_at', 'timestamptz')
|
||||
.addColumn('archived_at', 'timestamptz')
|
||||
.addColumn('created_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_notifications_user_id')
|
||||
.on('notifications')
|
||||
.columns(['user_id', 'id desc'])
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_notifications_user_unread')
|
||||
.on('notifications')
|
||||
.column('user_id')
|
||||
.where(sql.ref('read_at'), 'is', null)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await db.schema.dropTable('notifications').execute();
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { type Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.createTable('watchers')
|
||||
.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').notNull(),
|
||||
)
|
||||
.addColumn('workspace_id', 'uuid', (col) =>
|
||||
col.references('workspaces.id').onDelete('cascade').notNull(),
|
||||
)
|
||||
.addColumn('type', 'text', (col) => col.notNull())
|
||||
.addColumn('added_by_id', 'uuid', (col) =>
|
||||
col.references('users.id').onDelete('set null'),
|
||||
)
|
||||
.addColumn('muted_at', 'timestamptz')
|
||||
.addColumn('created_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_watchers_user_page')
|
||||
.on('watchers')
|
||||
.columns(['user_id', 'page_id'])
|
||||
.unique()
|
||||
.where('page_id', 'is not', null)
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('idx_watchers_user_space')
|
||||
.on('watchers')
|
||||
.columns(['user_id', 'space_id'])
|
||||
.unique()
|
||||
.where(sql.ref('page_id'), 'is', null)
|
||||
.execute();
|
||||
|
||||
// Query index for fetching watchers by page
|
||||
await db.schema
|
||||
.createIndex('idx_watchers_page_id')
|
||||
.on('watchers')
|
||||
.column('page_id')
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await db.schema.dropTable('watchers').execute();
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { type Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
// Backfill watchers from pages.contributorIds and pages.creatorId
|
||||
// This inserts unique user-page combinations from both sources
|
||||
await sql`
|
||||
INSERT INTO watchers (user_id, page_id, space_id, workspace_id, type, added_by_id)
|
||||
SELECT DISTINCT
|
||||
u.user_id,
|
||||
p.id as page_id,
|
||||
p.space_id,
|
||||
p.workspace_id,
|
||||
'page' as type,
|
||||
u.user_id as added_by_id
|
||||
FROM pages p
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT unnest(p.contributor_ids) as user_id
|
||||
UNION
|
||||
SELECT p.creator_id as user_id WHERE p.creator_id IS NOT NULL
|
||||
) u
|
||||
WHERE p.deleted_at IS NULL
|
||||
AND u.user_id IS NOT NULL
|
||||
ON CONFLICT DO NOTHING
|
||||
`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`DELETE FROM watchers WHERE type = 'page'`.execute(db);
|
||||
}
|
||||
Reference in New Issue
Block a user