mirror of
https://github.com/docmost/docmost.git
synced 2026-05-21 01:04:39 +08:00
fix: scope synced blocks to workspace, gate unsync on edit permission
This commit is contained in:
@@ -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
@@ -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 {
|
||||||
|
|||||||
+1
-1
Submodule apps/server/src/ee updated: 35c0f3c4f8...6479522986
Reference in New Issue
Block a user