fix: scope synced blocks to workspace, gate unsync on edit permission

This commit is contained in:
Philipinho
2026-05-08 01:15:13 +01:00
parent 09f2b84988
commit 159920bc84
13 changed files with 122 additions and 49 deletions
@@ -165,7 +165,7 @@ export class PersistenceExtension implements Extension {
} }
if (page) { if (page) {
await this.syncTransclusion(pageId, tiptapJson); await this.syncTransclusion(pageId, page.workspaceId, tiptapJson);
} }
if (page) { if (page) {
@@ -250,10 +250,15 @@ export class PersistenceExtension implements Extension {
*/ */
private async syncTransclusion( private async syncTransclusion(
pageId: string, pageId: string,
workspaceId: string,
tiptapJson: unknown, tiptapJson: unknown,
): Promise<void> { ): Promise<void> {
try { try {
await this.transclusionService.syncPageTransclusions(pageId, tiptapJson); await this.transclusionService.syncPageTransclusions(
pageId,
workspaceId,
tiptapJson,
);
} catch (err) { } catch (err) {
this.logger.error( this.logger.error(
{ err, pageId }, { err, pageId },
@@ -261,7 +266,11 @@ export class PersistenceExtension implements Extension {
); );
} }
try { try {
await this.transclusionService.syncPageReferences(pageId, tiptapJson); await this.transclusionService.syncPageReferences(
pageId,
workspaceId,
tiptapJson,
);
} catch (err) { } catch (err) {
this.logger.error( this.logger.error(
{ err, pageId }, { err, pageId },
@@ -677,7 +677,11 @@ export class PageService {
// pages never have prior rows so we can skip the diff and just bulk-insert. // pages never have prior rows so we can skip the diff and just bulk-insert.
try { try {
await this.transclusionService.insertTransclusionsForPages( await this.transclusionService.insertTransclusionsForPages(
insertablePages.map((p) => ({ id: p.id, content: p.content })), insertablePages.map((p) => ({
id: p.id,
workspaceId: p.workspaceId,
content: p.content,
})),
); );
} catch (err) { } catch (err) {
this.logger.error( this.logger.error(
@@ -688,7 +692,11 @@ export class PageService {
try { try {
await this.transclusionService.insertReferencesForPages( await this.transclusionService.insertReferencesForPages(
insertablePages.map((p) => ({ id: p.id, content: p.content })), insertablePages.map((p) => ({
id: p.id,
workspaceId: p.workspaceId,
content: p.content,
})),
); );
} catch (err) { } catch (err) {
this.logger.error( this.logger.error(
@@ -25,10 +25,10 @@ describe('TransclusionController.lookup', () => {
controller = module.get(TransclusionController); controller = module.get(TransclusionController);
}); });
const user = { id: 'u1' } as any; const user = { id: 'u1', workspaceId: 'w1' } as any;
const ref = { sourcePageId: 'p1', transclusionId: 'e1' }; const ref = { sourcePageId: 'p1', transclusionId: 'e1' };
it('passes the references and viewer id through to the service and returns its result', async () => { it('passes the references, viewer id and workspace id through to the service and returns its result', async () => {
service.lookup.mockResolvedValue({ service.lookup.mockResolvedValue({
items: [ items: [
{ {
@@ -43,6 +43,6 @@ describe('TransclusionController.lookup', () => {
const out = await controller.lookup({ references: [ref] } as any, user); const out = await controller.lookup({ references: [ref] } as any, user);
expect(out.items[0]).not.toHaveProperty('status'); expect(out.items[0]).not.toHaveProperty('status');
expect((out.items[0] as any).content).toEqual({ type: 'doc' }); expect((out.items[0] as any).content).toEqual({ type: 'doc' });
expect(service.lookup).toHaveBeenCalledWith([ref], 'u1'); expect(service.lookup).toHaveBeenCalledWith([ref], 'u1', 'w1');
}); });
}); });
@@ -6,6 +6,7 @@ import { PageRepo } from '@docmost/db/repos/page/page.repo';
import { PagePermissionRepo } from '@docmost/db/repos/page/page-permission.repo'; import { PagePermissionRepo } from '@docmost/db/repos/page/page-permission.repo';
import { AttachmentRepo } from '@docmost/db/repos/attachment/attachment.repo'; import { AttachmentRepo } from '@docmost/db/repos/attachment/attachment.repo';
import { StorageService } from '../../../../integrations/storage/storage.service'; import { StorageService } from '../../../../integrations/storage/storage.service';
import { PageAccessService } from '../../page-access/page-access.service';
describe('TransclusionService.syncPageTransclusions', () => { describe('TransclusionService.syncPageTransclusions', () => {
let service: TransclusionService; let service: TransclusionService;
@@ -27,6 +28,7 @@ describe('TransclusionService.syncPageTransclusions', () => {
{ provide: PagePermissionRepo, useValue: {} }, { provide: PagePermissionRepo, useValue: {} },
{ provide: AttachmentRepo, useValue: {} }, { provide: AttachmentRepo, useValue: {} },
{ provide: StorageService, useValue: {} }, { provide: StorageService, useValue: {} },
{ provide: PageAccessService, useValue: {} },
], ],
}).compile(); }).compile();
service = module.get(TransclusionService); service = module.get(TransclusionService);
@@ -34,6 +36,7 @@ describe('TransclusionService.syncPageTransclusions', () => {
}); });
const pageId = '00000000-0000-0000-0000-000000000001'; const pageId = '00000000-0000-0000-0000-000000000001';
const workspaceId = '00000000-0000-0000-0000-000000000099';
it('inserts new transclusions that did not exist before', async () => { it('inserts new transclusions that did not exist before', async () => {
repo.findByPageId.mockResolvedValue([]); repo.findByPageId.mockResolvedValue([]);
@@ -48,7 +51,7 @@ describe('TransclusionService.syncPageTransclusions', () => {
], ],
}; };
const result = await service.syncPageTransclusions(pageId, pm); const result = await service.syncPageTransclusions(pageId, workspaceId, pm);
expect(result).toEqual({ inserted: 1, updated: 0, deleted: 0 }); expect(result).toEqual({ inserted: 1, updated: 0, deleted: 0 });
expect(repo.insert).toHaveBeenCalledTimes(1); expect(repo.insert).toHaveBeenCalledTimes(1);
@@ -91,7 +94,7 @@ describe('TransclusionService.syncPageTransclusions', () => {
], ],
}; };
const result = await service.syncPageTransclusions(pageId, pm); const result = await service.syncPageTransclusions(pageId, workspaceId, pm);
expect(result).toEqual({ inserted: 0, updated: 1, deleted: 0 }); expect(result).toEqual({ inserted: 0, updated: 1, deleted: 0 });
expect(repo.update).toHaveBeenCalledWith( expect(repo.update).toHaveBeenCalledWith(
@@ -128,7 +131,7 @@ describe('TransclusionService.syncPageTransclusions', () => {
], ],
}; };
const result = await service.syncPageTransclusions(pageId, pm); const result = await service.syncPageTransclusions(pageId, workspaceId, pm);
expect(result).toEqual({ inserted: 0, updated: 0, deleted: 0 }); expect(result).toEqual({ inserted: 0, updated: 0, deleted: 0 });
expect(repo.update).not.toHaveBeenCalled(); expect(repo.update).not.toHaveBeenCalled();
@@ -147,7 +150,7 @@ describe('TransclusionService.syncPageTransclusions', () => {
]); ]);
const pm = { type: 'doc', content: [{ type: 'paragraph' }] }; const pm = { type: 'doc', content: [{ type: 'paragraph' }] };
const result = await service.syncPageTransclusions(pageId, pm); const result = await service.syncPageTransclusions(pageId, workspaceId, pm);
expect(result).toEqual({ inserted: 0, updated: 0, deleted: 1 }); expect(result).toEqual({ inserted: 0, updated: 0, deleted: 1 });
expect(repo.deleteByPageAndTransclusionIds).toHaveBeenCalledWith( expect(repo.deleteByPageAndTransclusionIds).toHaveBeenCalledWith(
@@ -159,7 +162,7 @@ describe('TransclusionService.syncPageTransclusions', () => {
it('handles empty doc → noop', async () => { it('handles empty doc → noop', async () => {
repo.findByPageId.mockResolvedValue([]); repo.findByPageId.mockResolvedValue([]);
const result = await service.syncPageTransclusions(pageId, null); const result = await service.syncPageTransclusions(pageId, workspaceId, null);
expect(result).toEqual({ inserted: 0, updated: 0, deleted: 0 }); expect(result).toEqual({ inserted: 0, updated: 0, deleted: 0 });
expect(repo.insert).not.toHaveBeenCalled(); expect(repo.insert).not.toHaveBeenCalled();
expect(repo.update).not.toHaveBeenCalled(); expect(repo.update).not.toHaveBeenCalled();
@@ -187,6 +190,7 @@ describe('TransclusionService.syncPageReferences', () => {
{ provide: PagePermissionRepo, useValue: {} }, { provide: PagePermissionRepo, useValue: {} },
{ provide: AttachmentRepo, useValue: {} }, { provide: AttachmentRepo, useValue: {} },
{ provide: StorageService, useValue: {} }, { provide: StorageService, useValue: {} },
{ provide: PageAccessService, useValue: {} },
], ],
}).compile(); }).compile();
service = module.get(TransclusionService); service = module.get(TransclusionService);
@@ -194,6 +198,7 @@ describe('TransclusionService.syncPageReferences', () => {
}); });
const referencePageId = '00000000-0000-0000-0000-000000000001'; const referencePageId = '00000000-0000-0000-0000-000000000001';
const workspaceId = '00000000-0000-0000-0000-000000000099';
it('inserts new loose references, no deletes when none existed', async () => { it('inserts new loose references, no deletes when none existed', async () => {
refRepo.findByReferencePageId.mockResolvedValue([]); refRepo.findByReferencePageId.mockResolvedValue([]);
@@ -211,17 +216,19 @@ describe('TransclusionService.syncPageReferences', () => {
], ],
}; };
const result = await service.syncPageReferences(referencePageId, pm); const result = await service.syncPageReferences(referencePageId, workspaceId, pm);
expect(result).toEqual({ inserted: 2, deleted: 0 }); expect(result).toEqual({ inserted: 2, deleted: 0 });
expect(refRepo.insertMany).toHaveBeenCalledWith( expect(refRepo.insertMany).toHaveBeenCalledWith(
[ [
{ {
workspaceId,
referencePageId, referencePageId,
sourcePageId: 'p1', sourcePageId: 'p1',
transclusionId: 'e1', transclusionId: 'e1',
}, },
{ {
workspaceId,
referencePageId, referencePageId,
sourcePageId: 'p2', sourcePageId: 'p2',
transclusionId: 'e2', transclusionId: 'e2',
@@ -250,7 +257,7 @@ describe('TransclusionService.syncPageReferences', () => {
], ],
}; };
const result = await service.syncPageReferences(referencePageId, pm); const result = await service.syncPageReferences(referencePageId, workspaceId, pm);
expect(result).toEqual({ inserted: 0, deleted: 0 }); expect(result).toEqual({ inserted: 0, deleted: 0 });
expect(refRepo.insertMany).not.toHaveBeenCalled(); expect(refRepo.insertMany).not.toHaveBeenCalled();
@@ -268,7 +275,7 @@ describe('TransclusionService.syncPageReferences', () => {
]); ]);
const pm = { type: 'doc', content: [{ type: 'paragraph' }] }; const pm = { type: 'doc', content: [{ type: 'paragraph' }] };
const result = await service.syncPageReferences(referencePageId, pm); const result = await service.syncPageReferences(referencePageId, workspaceId, pm);
expect(result).toEqual({ inserted: 0, deleted: 1 }); expect(result).toEqual({ inserted: 0, deleted: 1 });
expect(refRepo.deleteByReferenceAndKeys).toHaveBeenCalledWith( expect(refRepo.deleteByReferenceAndKeys).toHaveBeenCalledWith(
@@ -304,7 +311,7 @@ describe('TransclusionService.syncPageReferences', () => {
], ],
}; };
const result = await service.syncPageReferences(referencePageId, pm); const result = await service.syncPageReferences(referencePageId, workspaceId, pm);
expect(result).toEqual({ inserted: 0, deleted: 0 }); expect(result).toEqual({ inserted: 0, deleted: 0 });
expect(refRepo.insertMany).not.toHaveBeenCalled(); expect(refRepo.insertMany).not.toHaveBeenCalled();
@@ -24,7 +24,8 @@ export class TransclusionController {
async lookup(@Body() dto: LookupDto, @AuthUser() user: User) { async lookup(@Body() dto: LookupDto, @AuthUser() user: User) {
return this.transclusionService.lookup( return this.transclusionService.lookup(
dto.references, dto.references,
user?.id ?? null, user.id,
user.workspaceId,
); );
} }
@@ -38,6 +39,7 @@ export class TransclusionController {
sourcePageId: dto.sourcePageId, sourcePageId: dto.sourcePageId,
transclusionId: dto.transclusionId, transclusionId: dto.transclusionId,
viewerUserId: user.id, viewerUserId: user.id,
workspaceId: user.workspaceId,
}); });
} }
@@ -51,7 +53,7 @@ export class TransclusionController {
dto.referencePageId, dto.referencePageId,
dto.sourcePageId, dto.sourcePageId,
dto.transclusionId, dto.transclusionId,
user.id, user,
); );
} }
} }
@@ -19,7 +19,8 @@ import {
} from './utils/transclusion-prosemirror.util'; } from './utils/transclusion-prosemirror.util';
import { rewriteAttachmentsForUnsync } from './utils/transclusion-unsync.util'; import { rewriteAttachmentsForUnsync } from './utils/transclusion-unsync.util';
import { TransclusionLookup } from './transclusion.types'; import { TransclusionLookup } from './transclusion.types';
import { Page } from '@docmost/db/types/entity.types'; import { Page, User } from '@docmost/db/types/entity.types';
import { PageAccessService } from '../page-access/page-access.service';
type ReferencingPageInfo = { type ReferencingPageInfo = {
id: string; id: string;
@@ -41,10 +42,12 @@ export class TransclusionService {
private readonly pagePermissionRepo: PagePermissionRepo, private readonly pagePermissionRepo: PagePermissionRepo,
private readonly attachmentRepo: AttachmentRepo, private readonly attachmentRepo: AttachmentRepo,
private readonly storageService: StorageService, private readonly storageService: StorageService,
private readonly pageAccessService: PageAccessService,
) {} ) {}
async syncPageTransclusions( async syncPageTransclusions(
pageId: string, pageId: string,
workspaceId: string,
pmJson: unknown, pmJson: unknown,
trx?: KyselyTransaction, trx?: KyselyTransaction,
): Promise<{ inserted: number; updated: number; deleted: number }> { ): Promise<{ inserted: number; updated: number; deleted: number }> {
@@ -63,6 +66,7 @@ export class TransclusionService {
if (!prev) { if (!prev) {
await this.pageTransclusionsRepo.insert( await this.pageTransclusionsRepo.insert(
{ {
workspaceId,
pageId, pageId,
transclusionId: d.transclusionId, transclusionId: d.transclusionId,
content: d.content as any, content: d.content as any,
@@ -102,6 +106,7 @@ export class TransclusionService {
async syncPageReferences( async syncPageReferences(
referencePageId: string, referencePageId: string,
workspaceId: string,
pmJson: unknown, pmJson: unknown,
trx?: KyselyTransaction, trx?: KyselyTransaction,
): Promise<{ inserted: number; deleted: number }> { ): Promise<{ inserted: number; deleted: number }> {
@@ -121,6 +126,7 @@ export class TransclusionService {
const toInsert = desired const toInsert = desired
.filter((d) => !existingKeys.has(keyOf(d))) .filter((d) => !existingKeys.has(keyOf(d)))
.map((d) => ({ .map((d) => ({
workspaceId,
referencePageId, referencePageId,
sourcePageId: d.sourcePageId, sourcePageId: d.sourcePageId,
transclusionId: d.transclusionId, transclusionId: d.transclusionId,
@@ -156,7 +162,7 @@ export class TransclusionService {
* (e.g. duplication, import) where there is nothing to diff against. * (e.g. duplication, import) where there is nothing to diff against.
*/ */
async insertTransclusionsForPages( async insertTransclusionsForPages(
pages: Array<{ id: string; content: unknown }>, pages: Array<{ id: string; workspaceId: string; content: unknown }>,
trx?: KyselyTransaction, trx?: KyselyTransaction,
): Promise<{ inserted: number }> { ): Promise<{ inserted: number }> {
const rows: Parameters<PageTransclusionsRepo['insertMany']>[0] = []; const rows: Parameters<PageTransclusionsRepo['insertMany']>[0] = [];
@@ -164,6 +170,7 @@ export class TransclusionService {
const snapshots = collectTransclusionsFromPmJson(page.content); const snapshots = collectTransclusionsFromPmJson(page.content);
for (const s of snapshots) { for (const s of snapshots) {
rows.push({ rows.push({
workspaceId: page.workspaceId,
pageId: page.id, pageId: page.id,
transclusionId: s.transclusionId, transclusionId: s.transclusionId,
content: s.content as any, content: s.content as any,
@@ -181,10 +188,11 @@ export class TransclusionService {
* (duplication, import) where there is nothing to diff against. * (duplication, import) where there is nothing to diff against.
*/ */
async insertReferencesForPages( async insertReferencesForPages(
pages: Array<{ id: string; content: unknown }>, pages: Array<{ id: string; workspaceId: string; content: unknown }>,
trx?: KyselyTransaction, trx?: KyselyTransaction,
): Promise<{ inserted: number }> { ): Promise<{ inserted: number }> {
const rows: Array<{ const rows: Array<{
workspaceId: string;
referencePageId: string; referencePageId: string;
sourcePageId: string; sourcePageId: string;
transclusionId: string; transclusionId: string;
@@ -193,6 +201,7 @@ export class TransclusionService {
const refs = collectReferencesFromPmJson(page.content); const refs = collectReferencesFromPmJson(page.content);
for (const r of refs) { for (const r of refs) {
rows.push({ rows.push({
workspaceId: page.workspaceId,
referencePageId: page.id, referencePageId: page.id,
sourcePageId: r.sourcePageId, sourcePageId: r.sourcePageId,
transclusionId: r.transclusionId, transclusionId: r.transclusionId,
@@ -206,23 +215,22 @@ export class TransclusionService {
async lookup( async lookup(
references: Array<{ sourcePageId: string; transclusionId: string }>, references: Array<{ sourcePageId: string; transclusionId: string }>,
viewerUserId: string | null, viewerUserId: string,
workspaceId: string,
): Promise<{ items: TransclusionLookup[] }> { ): Promise<{ items: TransclusionLookup[] }> {
if (references.length === 0) return { items: [] }; if (references.length === 0) return { items: [] };
const candidatePageIds = Array.from( const candidatePageIds = Array.from(
new Set(references.map((r) => r.sourcePageId)), new Set(references.map((r) => r.sourcePageId)),
); );
const accessibleSet = viewerUserId const accessibleSet = new Set(
? new Set( await this.pagePermissionRepo.filterAccessiblePageIds({
await this.pagePermissionRepo.filterAccessiblePageIds({ pageIds: candidatePageIds,
pageIds: candidatePageIds, userId: viewerUserId,
userId: viewerUserId, }),
}), );
)
: new Set<string>();
return this.lookupWithAccessSet(references, accessibleSet); return this.lookupWithAccessSet(references, accessibleSet, workspaceId);
} }
/** /**
@@ -234,6 +242,7 @@ export class TransclusionService {
async lookupWithAccessSet( async lookupWithAccessSet(
references: Array<{ sourcePageId: string; transclusionId: string }>, references: Array<{ sourcePageId: string; transclusionId: string }>,
accessibleSet: Set<string>, accessibleSet: Set<string>,
workspaceId: string,
): Promise<{ items: TransclusionLookup[] }> { ): Promise<{ items: TransclusionLookup[] }> {
if (references.length === 0) return { items: [] }; if (references.length === 0) return { items: [] };
@@ -248,6 +257,7 @@ export class TransclusionService {
pageId: references[i].sourcePageId, pageId: references[i].sourcePageId,
transclusionId: references[i].transclusionId, transclusionId: references[i].transclusionId,
})), })),
workspaceId,
); );
const rowKey = (r: { pageId: string; transclusionId: string }) => const rowKey = (r: { pageId: string; transclusionId: string }) =>
`${r.pageId}::${r.transclusionId}`; `${r.pageId}::${r.transclusionId}`;
@@ -256,10 +266,12 @@ export class TransclusionService {
const accessiblePageIds = Array.from( const accessiblePageIds = Array.from(
new Set(accessiblePending.map((i) => references[i].sourcePageId)), new Set(accessiblePending.map((i) => references[i].sourcePageId)),
); );
const pages = await this.pageRepo.findManyByIds(accessiblePageIds); const pages = await this.pageRepo.findManyByIds(accessiblePageIds, {
workspaceId,
});
const pageMeta = new Map<string, Date>(); const pageMeta = new Map<string, Date>();
for (const p of pages) { for (const p of pages) {
if (!p.deletedAt) pageMeta.set(p.id, p.updatedAt); pageMeta.set(p.id, p.updatedAt);
} }
for (const i of pendingIdx) { for (const i of pendingIdx) {
@@ -306,16 +318,18 @@ export class TransclusionService {
sourcePageId: string; sourcePageId: string;
transclusionId: string; transclusionId: string;
viewerUserId: string; viewerUserId: string;
workspaceId: string;
}): Promise<{ }): Promise<{
source: ReferencingPageInfo | null; source: ReferencingPageInfo | null;
references: ReferencingPageInfo[]; references: ReferencingPageInfo[];
}> { }> {
const { sourcePageId, transclusionId, viewerUserId } = opts; const { sourcePageId, transclusionId, viewerUserId, workspaceId } = opts;
const referencePageIds = const referencePageIds =
await this.pageTransclusionReferencesRepo.findReferencePageIdsByTransclusion( await this.pageTransclusionReferencesRepo.findReferencePageIdsByTransclusion(
sourcePageId, sourcePageId,
transclusionId, transclusionId,
workspaceId,
); );
const candidatePageIds = Array.from( const candidatePageIds = Array.from(
@@ -342,7 +356,7 @@ export class TransclusionService {
); );
const byId = new Map<string, ReferencingPageInfo>(); const byId = new Map<string, ReferencingPageInfo>();
for (const p of rows) { for (const p of rows) {
if (!p || p.deletedAt) continue; if (!p || p.deletedAt || p.workspaceId !== workspaceId) continue;
const space = (p as Page & { space?: { slug?: string } }).space; const space = (p as Page & { space?: { slug?: string } }).space;
byId.set(p.id, { byId.set(p.id, {
id: p.id, id: p.id,
@@ -376,7 +390,7 @@ export class TransclusionService {
referencePageId: string, referencePageId: string,
sourcePageId: string, sourcePageId: string,
transclusionId: string, transclusionId: string,
viewerUserId: string, user: User,
): Promise<{ content: unknown }> { ): Promise<{ content: unknown }> {
const referencePage = await this.pageRepo.findById(referencePageId); const referencePage = await this.pageRepo.findById(referencePageId);
if (!referencePage || referencePage.deletedAt) { if (!referencePage || referencePage.deletedAt) {
@@ -388,16 +402,16 @@ export class TransclusionService {
throw new NotFoundException('Source page not found'); throw new NotFoundException('Source page not found');
} }
const accessible = new Set( if (
await this.pagePermissionRepo.filterAccessiblePageIds({ referencePage.workspaceId !== user.workspaceId ||
pageIds: [referencePageId, sourcePageId], sourcePage.workspaceId !== user.workspaceId
userId: viewerUserId, ) {
}),
);
if (!accessible.has(referencePageId) || !accessible.has(sourcePageId)) {
throw new ForbiddenException(); throw new ForbiddenException();
} }
await this.pageAccessService.validateCanEdit(referencePage, user);
await this.pageAccessService.validateCanView(sourcePage, user);
const transclusion = const transclusion =
await this.pageTransclusionsRepo.findByPageAndTransclusion( await this.pageTransclusionsRepo.findByPageAndTransclusion(
sourcePageId, sourcePageId,
@@ -445,7 +459,7 @@ export class TransclusionService {
fileSize: old.fileSize, fileSize: old.fileSize,
mimeType: old.mimeType, mimeType: old.mimeType,
fileExt: old.fileExt, fileExt: old.fileExt,
creatorId: viewerUserId, creatorId: user.id,
workspaceId: referencePage.workspaceId, workspaceId: referencePage.workspaceId,
pageId: referencePageId, pageId: referencePageId,
spaceId: referencePage.spaceId, spaceId: referencePage.spaceId,
@@ -357,6 +357,7 @@ export class ShareService {
const { items } = await this.transclusionService.lookupWithAccessSet( const { items } = await this.transclusionService.lookupWithAccessSet(
references, references,
accessibleSet, accessibleSet,
workspaceId,
); );
// Sanitize each item's content for public delivery // Sanitize each item's content for public delivery
@@ -6,6 +6,9 @@ export async function up(db: Kysely<any>): Promise<void> {
.addColumn('id', 'uuid', (col) => .addColumn('id', 'uuid', (col) =>
col.primaryKey().defaultTo(sql`gen_uuid_v7()`), col.primaryKey().defaultTo(sql`gen_uuid_v7()`),
) )
.addColumn('workspace_id', 'uuid', (col) =>
col.notNull().references('workspaces.id').onDelete('cascade'),
)
.addColumn('page_id', 'uuid', (col) => .addColumn('page_id', 'uuid', (col) =>
col.notNull().references('pages.id').onDelete('cascade'), col.notNull().references('pages.id').onDelete('cascade'),
) )
@@ -23,11 +26,20 @@ export async function up(db: Kysely<any>): Promise<void> {
]) ])
.execute(); .execute();
await db.schema
.createIndex('idx_page_transclusions_workspace')
.on('page_transclusions')
.column('workspace_id')
.execute();
await db.schema await db.schema
.createTable('page_transclusion_references') .createTable('page_transclusion_references')
.addColumn('id', 'uuid', (col) => .addColumn('id', 'uuid', (col) =>
col.primaryKey().defaultTo(sql`gen_uuid_v7()`), 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) => .addColumn('reference_page_id', 'uuid', (col) =>
col.notNull().references('pages.id').onDelete('cascade'), col.notNull().references('pages.id').onDelete('cascade'),
) )
@@ -50,6 +62,12 @@ export async function up(db: Kysely<any>): Promise<void> {
.on('page_transclusion_references') .on('page_transclusion_references')
.columns(['source_page_id', 'transclusion_id']) .columns(['source_page_id', 'transclusion_id'])
.execute(); .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> { export async function down(db: Kysely<any>): Promise<void> {
@@ -30,12 +30,14 @@ export class PageTransclusionReferencesRepo {
async findReferencePageIdsByTransclusion( async findReferencePageIdsByTransclusion(
sourcePageId: string, sourcePageId: string,
transclusionId: string, transclusionId: string,
workspaceId: string,
trx?: KyselyTransaction, trx?: KyselyTransaction,
): Promise<string[]> { ): Promise<string[]> {
const rows = await dbOrTx(this.db, trx) const rows = await dbOrTx(this.db, trx)
.selectFrom('pageTransclusionReferences') .selectFrom('pageTransclusionReferences')
.select('referencePageId') .select('referencePageId')
.distinct() .distinct()
.where('workspaceId', '=', workspaceId)
.where('sourcePageId', '=', sourcePageId) .where('sourcePageId', '=', sourcePageId)
.where('transclusionId', '=', transclusionId) .where('transclusionId', '=', transclusionId)
.execute(); .execute();
@@ -39,12 +39,14 @@ export class PageTransclusionsRepo {
async findManyByPageAndTransclusion( async findManyByPageAndTransclusion(
keys: Array<{ pageId: string; transclusionId: string }>, keys: Array<{ pageId: string; transclusionId: string }>,
workspaceId: string,
trx?: KyselyTransaction, trx?: KyselyTransaction,
): Promise<PageTransclusion[]> { ): Promise<PageTransclusion[]> {
if (keys.length === 0) return []; if (keys.length === 0) return [];
return dbOrTx(this.db, trx) return dbOrTx(this.db, trx)
.selectFrom('pageTransclusions') .selectFrom('pageTransclusions')
.selectAll() .selectAll()
.where('workspaceId', '=', workspaceId)
.where((eb) => .where((eb) =>
eb.or( eb.or(
keys.map((k) => keys.map((k) =>
@@ -104,16 +104,24 @@ export class PageRepo {
pageIds: string[], pageIds: string[],
opts?: { opts?: {
trx?: KyselyTransaction; trx?: KyselyTransaction;
workspaceId?: string;
}, },
): Promise<Page[]> { ): Promise<Page[]> {
if (pageIds.length === 0) return []; if (pageIds.length === 0) return [];
const db = dbOrTx(this.db, opts?.trx); const db = dbOrTx(this.db, opts?.trx);
return db let query = db
.selectFrom('pages') .selectFrom('pages')
.select(this.baseFields) .select(this.baseFields)
.where('id', 'in', pageIds) .where('id', 'in', pageIds);
.execute();
if (opts?.workspaceId) {
query = query
.where('workspaceId', '=', opts.workspaceId)
.where('deletedAt', 'is', null);
}
return query.execute();
} }
async updatePage( async updatePage(
+2
View File
@@ -234,6 +234,7 @@ export interface PageTransclusionReferences {
referencePageId: string; referencePageId: string;
id: Generated<string>; id: Generated<string>;
sourcePageId: string; sourcePageId: string;
workspaceId: string;
} }
export interface PageTransclusions { export interface PageTransclusions {
@@ -243,6 +244,7 @@ export interface PageTransclusions {
id: Generated<string>; id: Generated<string>;
pageId: string; pageId: string;
updatedAt: Generated<Timestamp>; updatedAt: Generated<Timestamp>;
workspaceId: string;
} }
export interface PageHistory { export interface PageHistory {