mirror of
https://github.com/docmost/docmost.git
synced 2025-11-14 05:01:15 +10:00
feat: keep track of page contributors (#959)
* WIP * feat: store and retrieve page contributors
This commit is contained in:
@ -1,5 +1,7 @@
|
||||
import {
|
||||
afterUnloadDocumentPayload,
|
||||
Extension,
|
||||
onChangePayload,
|
||||
onLoadDocumentPayload,
|
||||
onStoreDocumentPayload,
|
||||
} from '@hocuspocus/server';
|
||||
@ -26,6 +28,7 @@ import { Page } from '@docmost/db/types/entity.types';
|
||||
@Injectable()
|
||||
export class PersistenceExtension implements Extension {
|
||||
private readonly logger = new Logger(PersistenceExtension.name);
|
||||
private contributors: Map<string, Set<string>> = new Map();
|
||||
|
||||
constructor(
|
||||
private readonly pageRepo: PageRepo,
|
||||
@ -116,12 +119,27 @@ export class PersistenceExtension implements Extension {
|
||||
return;
|
||||
}
|
||||
|
||||
let contributorIds = undefined;
|
||||
try {
|
||||
const existingContributors = page.contributorIds || [];
|
||||
const contributorSet = this.contributors.get(documentName);
|
||||
contributorSet.add(page.creatorId);
|
||||
const newContributors = [...contributorSet];
|
||||
contributorIds = Array.from(
|
||||
new Set([...existingContributors, ...newContributors]),
|
||||
);
|
||||
this.contributors.delete(documentName);
|
||||
} catch (err) {
|
||||
this.logger.log('Contributors error:' + err?.['message']);
|
||||
}
|
||||
|
||||
await this.pageRepo.updatePage(
|
||||
{
|
||||
content: tiptapJson,
|
||||
textContent: textContent,
|
||||
ydoc: ydocState,
|
||||
lastUpdatedById: context.user.id,
|
||||
contributorIds: contributorIds,
|
||||
},
|
||||
pageId,
|
||||
trx,
|
||||
@ -152,4 +170,21 @@ export class PersistenceExtension implements Extension {
|
||||
} as IPageBacklinkJob);
|
||||
}
|
||||
}
|
||||
|
||||
async onChange(data: onChangePayload) {
|
||||
const documentName = data.documentName;
|
||||
const userId = data.context?.user.id;
|
||||
if (!userId) return;
|
||||
|
||||
if (!this.contributors.has(documentName)) {
|
||||
this.contributors.set(documentName, new Set());
|
||||
}
|
||||
|
||||
this.contributors.get(documentName).add(userId);
|
||||
}
|
||||
|
||||
async afterUnloadDocument(data: afterUnloadDocumentPayload) {
|
||||
const documentName = data.documentName;
|
||||
this.contributors.delete(documentName);
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,6 +46,7 @@ export class PageController {
|
||||
includeContent: true,
|
||||
includeCreator: true,
|
||||
includeLastUpdatedBy: true,
|
||||
includeContributors: true,
|
||||
});
|
||||
|
||||
if (!page) {
|
||||
@ -93,7 +94,7 @@ export class PageController {
|
||||
}
|
||||
|
||||
return this.pageService.update(
|
||||
updatePageDto.pageId,
|
||||
page,
|
||||
updatePageDto,
|
||||
user.id,
|
||||
);
|
||||
|
||||
@ -112,21 +112,32 @@ export class PageService {
|
||||
}
|
||||
|
||||
async update(
|
||||
pageId: string,
|
||||
page: Page,
|
||||
updatePageDto: UpdatePageDto,
|
||||
userId: string,
|
||||
): Promise<Page> {
|
||||
const contributors = new Set<string>(page.contributorIds);
|
||||
contributors.add(userId);
|
||||
const contributorIds = Array.from(contributors);
|
||||
|
||||
await this.pageRepo.updatePage(
|
||||
{
|
||||
title: updatePageDto.title,
|
||||
icon: updatePageDto.icon,
|
||||
lastUpdatedById: userId,
|
||||
updatedAt: new Date(),
|
||||
contributorIds: contributorIds,
|
||||
},
|
||||
pageId,
|
||||
page.id,
|
||||
);
|
||||
|
||||
return await this.pageRepo.findById(pageId);
|
||||
return await this.pageRepo.findById(page.id, {
|
||||
includeSpace: true,
|
||||
includeContent: true,
|
||||
includeCreator: true,
|
||||
includeLastUpdatedBy: true,
|
||||
includeContributors: true,
|
||||
});
|
||||
}
|
||||
|
||||
withHasChildren(eb: ExpressionBuilder<DB, 'pages'>) {
|
||||
|
||||
@ -47,7 +47,7 @@ types.setTypeParser(types.builtins.INT8, (val) => Number(val));
|
||||
log: (event: LogEvent) => {
|
||||
if (environmentService.getNodeEnv() !== 'development') return;
|
||||
const logger = new Logger(DatabaseModule.name);
|
||||
if (event.level === 'query') {
|
||||
if (event.level) {
|
||||
if (process.env.DEBUG_DB?.toLowerCase() === 'true') {
|
||||
logger.debug(event.query.sql);
|
||||
logger.debug('query time: ' + event.queryDurationMillis + ' ms');
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
import { type Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.alterTable('pages')
|
||||
.addColumn('contributor_ids', sql`uuid[]`, (col) => col.defaultTo("{}"))
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await db.schema.alterTable('pages').dropColumn('contributor_ids').execute();
|
||||
}
|
||||
@ -10,9 +10,9 @@ import {
|
||||
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
||||
import { executeWithPagination } from '@docmost/db/pagination/pagination';
|
||||
import { validate as isValidUUID } from 'uuid';
|
||||
import { ExpressionBuilder } from 'kysely';
|
||||
import { ExpressionBuilder, sql } from 'kysely';
|
||||
import { DB } from '@docmost/db/types/db';
|
||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
||||
|
||||
@Injectable()
|
||||
@ -38,6 +38,7 @@ export class PageRepo {
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'deletedAt',
|
||||
'contributorIds',
|
||||
];
|
||||
|
||||
async findById(
|
||||
@ -48,6 +49,7 @@ export class PageRepo {
|
||||
includeSpace?: boolean;
|
||||
includeCreator?: boolean;
|
||||
includeLastUpdatedBy?: boolean;
|
||||
includeContributors?: boolean;
|
||||
withLock?: boolean;
|
||||
trx?: KyselyTransaction;
|
||||
},
|
||||
@ -68,6 +70,10 @@ export class PageRepo {
|
||||
query = query.select((eb) => this.withLastUpdatedBy(eb));
|
||||
}
|
||||
|
||||
if (opts?.includeContributors) {
|
||||
query = query.select((eb) => this.withContributors(eb));
|
||||
}
|
||||
|
||||
if (opts?.includeSpace) {
|
||||
query = query.select((eb) => this.withSpace(eb));
|
||||
}
|
||||
@ -189,6 +195,15 @@ export class PageRepo {
|
||||
).as('lastUpdatedBy');
|
||||
}
|
||||
|
||||
withContributors(eb: ExpressionBuilder<DB, 'pages'>) {
|
||||
return jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom('users')
|
||||
.select(['users.id', 'users.name', 'users.avatarUrl'])
|
||||
.whereRef('users.id', '=', sql`ANY(${eb.ref('pages.contributorIds')})`),
|
||||
).as('contributors');
|
||||
}
|
||||
|
||||
async getPageAndDescendants(parentPageId: string) {
|
||||
return this.db
|
||||
.withRecursive('page_hierarchy', (db) =>
|
||||
|
||||
1
apps/server/src/database/types/db.d.ts
vendored
1
apps/server/src/database/types/db.d.ts
vendored
@ -161,6 +161,7 @@ export interface PageHistory {
|
||||
|
||||
export interface Pages {
|
||||
content: Json | null;
|
||||
contributorIds: Generated<string[] | null>;
|
||||
coverPhoto: string | null;
|
||||
createdAt: Generated<Timestamp>;
|
||||
creatorId: string | null;
|
||||
|
||||
Reference in New Issue
Block a user