mirror of
https://github.com/docmost/docmost.git
synced 2026-09-11 07:56:54 +08:00
fix: add page hierarchy cycle contract
This commit is contained in:
@@ -0,0 +1,48 @@
|
|||||||
|
import {
|
||||||
|
assertAcyclicPageTraversal,
|
||||||
|
PageHierarchyCycleError,
|
||||||
|
stripPageTraversalMetadata,
|
||||||
|
} from './page-hierarchy-cycle';
|
||||||
|
|
||||||
|
describe('page hierarchy cycle contract', () => {
|
||||||
|
it('does nothing when no row is marked as a cycle', () => {
|
||||||
|
expect(() =>
|
||||||
|
assertAcyclicPageTraversal(
|
||||||
|
[
|
||||||
|
{ id: 'page-1', title: 'Root', isCycle: false },
|
||||||
|
{ id: 'page-2', title: 'Child', isCycle: false },
|
||||||
|
],
|
||||||
|
'page-1',
|
||||||
|
),
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws PageHierarchyCycleError when a row is marked as a cycle', () => {
|
||||||
|
expect(() =>
|
||||||
|
assertAcyclicPageTraversal(
|
||||||
|
[{ id: 'page-1', title: 'Root', isCycle: true }],
|
||||||
|
'page-1',
|
||||||
|
),
|
||||||
|
).toThrow(PageHierarchyCycleError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the root page id on the error for safe logging', () => {
|
||||||
|
try {
|
||||||
|
assertAcyclicPageTraversal(
|
||||||
|
[{ id: 'page-1', title: 'Root', isCycle: true }],
|
||||||
|
'root-page',
|
||||||
|
);
|
||||||
|
throw new Error('expected a cycle error');
|
||||||
|
} catch (error) {
|
||||||
|
expect(error).toBeInstanceOf(PageHierarchyCycleError);
|
||||||
|
expect((error as PageHierarchyCycleError).rootPageId).toBe('root-page');
|
||||||
|
expect((error as PageHierarchyCycleError).code).toBe('PAGE_HIERARCHY_CYCLE');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes traversal metadata without changing the public row fields', () => {
|
||||||
|
const row = { id: 'page-1', title: 'Root', isCycle: false };
|
||||||
|
|
||||||
|
expect(stripPageTraversalMetadata(row)).toEqual({ id: 'page-1', title: 'Root' });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
export type CycleTrackedRow = {
|
||||||
|
isCycle: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class PageHierarchyCycleError extends Error {
|
||||||
|
readonly code = 'PAGE_HIERARCHY_CYCLE';
|
||||||
|
|
||||||
|
constructor(readonly rootPageId: string) {
|
||||||
|
super('Cyclic page hierarchy detected');
|
||||||
|
this.name = 'PageHierarchyCycleError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assertAcyclicPageTraversal<T extends CycleTrackedRow>(
|
||||||
|
rows: readonly T[],
|
||||||
|
rootPageId: string,
|
||||||
|
): void {
|
||||||
|
if (rows.some((row) => row.isCycle)) {
|
||||||
|
throw new PageHierarchyCycleError(rootPageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stripPageTraversalMetadata<T extends CycleTrackedRow>(
|
||||||
|
row: T,
|
||||||
|
): Omit<T, 'isCycle'> {
|
||||||
|
const { isCycle: _isCycle, ...publicRow } = row;
|
||||||
|
return publicRow;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user