mirror of
https://github.com/docmost/docmost.git
synced 2025-11-14 03:31:11 +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 {
|
import {
|
||||||
|
afterUnloadDocumentPayload,
|
||||||
Extension,
|
Extension,
|
||||||
|
onChangePayload,
|
||||||
onLoadDocumentPayload,
|
onLoadDocumentPayload,
|
||||||
onStoreDocumentPayload,
|
onStoreDocumentPayload,
|
||||||
} from '@hocuspocus/server';
|
} from '@hocuspocus/server';
|
||||||
@ -26,6 +28,7 @@ import { Page } from '@docmost/db/types/entity.types';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class PersistenceExtension implements Extension {
|
export class PersistenceExtension implements Extension {
|
||||||
private readonly logger = new Logger(PersistenceExtension.name);
|
private readonly logger = new Logger(PersistenceExtension.name);
|
||||||
|
private contributors: Map<string, Set<string>> = new Map();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly pageRepo: PageRepo,
|
private readonly pageRepo: PageRepo,
|
||||||
@ -116,12 +119,27 @@ export class PersistenceExtension implements Extension {
|
|||||||
return;
|
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(
|
await this.pageRepo.updatePage(
|
||||||
{
|
{
|
||||||
content: tiptapJson,
|
content: tiptapJson,
|
||||||
textContent: textContent,
|
textContent: textContent,
|
||||||
ydoc: ydocState,
|
ydoc: ydocState,
|
||||||
lastUpdatedById: context.user.id,
|
lastUpdatedById: context.user.id,
|
||||||
|
contributorIds: contributorIds,
|
||||||
},
|
},
|
||||||
pageId,
|
pageId,
|
||||||
trx,
|
trx,
|
||||||
@ -152,4 +170,21 @@ export class PersistenceExtension implements Extension {
|
|||||||
} as IPageBacklinkJob);
|
} 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,
|
includeContent: true,
|
||||||
includeCreator: true,
|
includeCreator: true,
|
||||||
includeLastUpdatedBy: true,
|
includeLastUpdatedBy: true,
|
||||||
|
includeContributors: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!page) {
|
if (!page) {
|
||||||
@ -93,7 +94,7 @@ export class PageController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this.pageService.update(
|
return this.pageService.update(
|
||||||
updatePageDto.pageId,
|
page,
|
||||||
updatePageDto,
|
updatePageDto,
|
||||||
user.id,
|
user.id,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -112,21 +112,32 @@ export class PageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async update(
|
async update(
|
||||||
pageId: string,
|
page: Page,
|
||||||
updatePageDto: UpdatePageDto,
|
updatePageDto: UpdatePageDto,
|
||||||
userId: string,
|
userId: string,
|
||||||
): Promise<Page> {
|
): Promise<Page> {
|
||||||
|
const contributors = new Set<string>(page.contributorIds);
|
||||||
|
contributors.add(userId);
|
||||||
|
const contributorIds = Array.from(contributors);
|
||||||
|
|
||||||
await this.pageRepo.updatePage(
|
await this.pageRepo.updatePage(
|
||||||
{
|
{
|
||||||
title: updatePageDto.title,
|
title: updatePageDto.title,
|
||||||
icon: updatePageDto.icon,
|
icon: updatePageDto.icon,
|
||||||
lastUpdatedById: userId,
|
lastUpdatedById: userId,
|
||||||
updatedAt: new Date(),
|
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'>) {
|
withHasChildren(eb: ExpressionBuilder<DB, 'pages'>) {
|
||||||
|
|||||||
@ -47,7 +47,7 @@ types.setTypeParser(types.builtins.INT8, (val) => Number(val));
|
|||||||
log: (event: LogEvent) => {
|
log: (event: LogEvent) => {
|
||||||
if (environmentService.getNodeEnv() !== 'development') return;
|
if (environmentService.getNodeEnv() !== 'development') return;
|
||||||
const logger = new Logger(DatabaseModule.name);
|
const logger = new Logger(DatabaseModule.name);
|
||||||
if (event.level === 'query') {
|
if (event.level) {
|
||||||
if (process.env.DEBUG_DB?.toLowerCase() === 'true') {
|
if (process.env.DEBUG_DB?.toLowerCase() === 'true') {
|
||||||
logger.debug(event.query.sql);
|
logger.debug(event.query.sql);
|
||||||
logger.debug('query time: ' + event.queryDurationMillis + ' ms');
|
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 { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
||||||
import { executeWithPagination } from '@docmost/db/pagination/pagination';
|
import { executeWithPagination } from '@docmost/db/pagination/pagination';
|
||||||
import { validate as isValidUUID } from 'uuid';
|
import { validate as isValidUUID } from 'uuid';
|
||||||
import { ExpressionBuilder } from 'kysely';
|
import { ExpressionBuilder, sql } from 'kysely';
|
||||||
import { DB } from '@docmost/db/types/db';
|
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';
|
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -38,6 +38,7 @@ export class PageRepo {
|
|||||||
'createdAt',
|
'createdAt',
|
||||||
'updatedAt',
|
'updatedAt',
|
||||||
'deletedAt',
|
'deletedAt',
|
||||||
|
'contributorIds',
|
||||||
];
|
];
|
||||||
|
|
||||||
async findById(
|
async findById(
|
||||||
@ -48,6 +49,7 @@ export class PageRepo {
|
|||||||
includeSpace?: boolean;
|
includeSpace?: boolean;
|
||||||
includeCreator?: boolean;
|
includeCreator?: boolean;
|
||||||
includeLastUpdatedBy?: boolean;
|
includeLastUpdatedBy?: boolean;
|
||||||
|
includeContributors?: boolean;
|
||||||
withLock?: boolean;
|
withLock?: boolean;
|
||||||
trx?: KyselyTransaction;
|
trx?: KyselyTransaction;
|
||||||
},
|
},
|
||||||
@ -68,6 +70,10 @@ export class PageRepo {
|
|||||||
query = query.select((eb) => this.withLastUpdatedBy(eb));
|
query = query.select((eb) => this.withLastUpdatedBy(eb));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (opts?.includeContributors) {
|
||||||
|
query = query.select((eb) => this.withContributors(eb));
|
||||||
|
}
|
||||||
|
|
||||||
if (opts?.includeSpace) {
|
if (opts?.includeSpace) {
|
||||||
query = query.select((eb) => this.withSpace(eb));
|
query = query.select((eb) => this.withSpace(eb));
|
||||||
}
|
}
|
||||||
@ -189,6 +195,15 @@ export class PageRepo {
|
|||||||
).as('lastUpdatedBy');
|
).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) {
|
async getPageAndDescendants(parentPageId: string) {
|
||||||
return this.db
|
return this.db
|
||||||
.withRecursive('page_hierarchy', (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 {
|
export interface Pages {
|
||||||
content: Json | null;
|
content: Json | null;
|
||||||
|
contributorIds: Generated<string[] | null>;
|
||||||
coverPhoto: string | null;
|
coverPhoto: string | null;
|
||||||
createdAt: Generated<Timestamp>;
|
createdAt: Generated<Timestamp>;
|
||||||
creatorId: string | null;
|
creatorId: string | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user