Skip to content

Commit 03b3a10

Browse files
authored
feat(runner): createDataCellURI with circular reference detection and relative link rewriting (#1917)
Moves createDataCellURI helper from test file into link-utils.ts and adds two key features: Circular data detection: Throws an error when encountering circular references, preventing JSON.stringify failures and providing clear error messages. Relative link rewriting: When a base cell/link is provided, rewrites all internal relative links to include the base's id, ensuring links remain valid when embedded in data URIs. Includes comprehensive test coverage for both features, including edge cases with nested structures and arrays.
1 parent 41506c1 commit 03b3a10

File tree

3 files changed

+226
-32
lines changed

3 files changed

+226
-32
lines changed

packages/runner/src/link-utils.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,52 @@ export function findAndInlineDataURILinks(value: any): any {
507507
}
508508
}
509509

510+
// Helper to create data URIs for testing
511+
export function createDataCellURI(
512+
data: any,
513+
base?: Cell | NormalizedLink,
514+
): string {
515+
const baseId = isCell(base) ? base.getAsNormalizedFullLink().id : base?.id;
516+
517+
function traverseAndAddBaseIdToRelativeLinks(
518+
value: any,
519+
seen: Set<any>,
520+
): any {
521+
if (!isRecord(value)) return value;
522+
if (seen.has(value)) {
523+
throw new Error(`Cycle detected when creating data URI`);
524+
}
525+
seen.add(value);
526+
try {
527+
if (isAnyCellLink(value)) {
528+
const link = parseLink(value);
529+
if (!link.id) {
530+
return createSigilLinkFromParsedLink({ ...link, id: baseId });
531+
} else {
532+
return value;
533+
}
534+
} else if (Array.isArray(value)) {
535+
return value.map((item) =>
536+
traverseAndAddBaseIdToRelativeLinks(item, seen)
537+
);
538+
} else { // isObject
539+
return Object.fromEntries(
540+
Object.entries(value).map((
541+
[key, value],
542+
) => [key, traverseAndAddBaseIdToRelativeLinks(value, seen)]),
543+
);
544+
}
545+
} finally {
546+
seen.delete(value);
547+
}
548+
}
549+
const json = JSON.stringify({
550+
value: traverseAndAddBaseIdToRelativeLinks(data, new Set()),
551+
});
552+
const base64 = btoa(json);
553+
return `data:application/json;charset=utf-8;base64,${base64}`;
554+
}
555+
510556
/**
511557
* Traverse schema and remove all asCell and asStream flags.
512558
*/

packages/runner/test/data-uri-inlining.test.ts

Lines changed: 28 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,15 @@ import { Identity } from "@commontools/identity";
44
import { StorageManager } from "@commontools/runner/storage/cache.deno";
55
import { Runtime } from "../src/runtime.ts";
66
import type { IExtendedStorageTransaction } from "../src/storage/interface.ts";
7-
import { findAndInlineDataURILinks } from "../src/link-utils.ts";
7+
import {
8+
createDataCellURI,
9+
findAndInlineDataURILinks,
10+
} from "../src/link-utils.ts";
811
import { LINK_V1_TAG } from "../src/sigil-types.ts";
912

1013
const signer = await Identity.fromPassphrase("test operator");
1114
const space = signer.did();
1215

13-
// Helper to create data URIs for testing
14-
function createDataURI(data: any): string {
15-
const json = JSON.stringify({ value: data });
16-
const base64 = btoa(json);
17-
return `data:application/json;charset=utf-8;base64,${base64}`;
18-
}
19-
2016
describe("data URI inlining", () => {
2117
let storageManager: ReturnType<typeof StorageManager.emulate>;
2218
let runtime: Runtime;
@@ -39,7 +35,7 @@ describe("data URI inlining", () => {
3935

4036
describe("findAndInlineDataURILinks", () => {
4137
it("should inline simple data URI links", () => {
42-
const dataURI = createDataURI("test data");
38+
const dataURI = createDataCellURI("test data");
4339
const link = {
4440
"/": {
4541
[LINK_V1_TAG]: {
@@ -54,7 +50,7 @@ describe("data URI inlining", () => {
5450
});
5551

5652
it("should inline data URI links with paths", () => {
57-
const dataURI = createDataURI({ nested: { value: 42 } });
53+
const dataURI = createDataCellURI({ nested: { value: 42 } });
5854
const link = {
5955
"/": {
6056
[LINK_V1_TAG]: {
@@ -69,8 +65,8 @@ describe("data URI inlining", () => {
6965
});
7066

7167
it("should inline data URI links in arrays", () => {
72-
const dataURI1 = createDataURI("first");
73-
const dataURI2 = createDataURI("second");
68+
const dataURI1 = createDataCellURI("first");
69+
const dataURI2 = createDataCellURI("second");
7470

7571
const array = [
7672
{
@@ -96,7 +92,7 @@ describe("data URI inlining", () => {
9692
});
9793

9894
it("should inline data URI links in objects", () => {
99-
const dataURI = createDataURI("nested value");
95+
const dataURI = createDataCellURI("nested value");
10096
const obj = {
10197
key1: "regular value",
10298
key2: {
@@ -120,7 +116,7 @@ describe("data URI inlining", () => {
120116
const innerCell = runtime.getCell(space, "inner", undefined, tx);
121117
innerCell.set({ value: "inner data" });
122118

123-
const dataURI = createDataURI(innerCell.getAsLink());
119+
const dataURI = createDataCellURI(innerCell.getAsLink());
124120
const link = {
125121
"/": {
126122
[LINK_V1_TAG]: {
@@ -145,7 +141,7 @@ describe("data URI inlining", () => {
145141
const innerCell = runtime.getCell(space, "inner", undefined, tx);
146142
innerCell.set({ nested: { value: "inner data" } });
147143

148-
const dataURI = createDataURI(innerCell.getAsLink());
144+
const dataURI = createDataCellURI(innerCell.getAsLink());
149145
const link = {
150146
"/": {
151147
[LINK_V1_TAG]: {
@@ -166,7 +162,7 @@ describe("data URI inlining", () => {
166162
});
167163

168164
it("should return undefined for data URIs with invalid paths", () => {
169-
const dataURI = createDataURI({ a: 1 });
165+
const dataURI = createDataCellURI({ a: 1 });
170166
const link = {
171167
"/": {
172168
[LINK_V1_TAG]: {
@@ -196,7 +192,7 @@ describe("data URI inlining", () => {
196192
});
197193

198194
it("should deeply traverse nested structures", () => {
199-
const dataURI = createDataURI("deep value");
195+
const dataURI = createDataCellURI("deep value");
200196
const complex = {
201197
level1: {
202198
level2: [
@@ -219,7 +215,7 @@ describe("data URI inlining", () => {
219215
});
220216

221217
it("should handle data URIs with schema", () => {
222-
const dataURI = createDataURI(42);
218+
const dataURI = createDataCellURI(42);
223219
const link = {
224220
"/": {
225221
[LINK_V1_TAG]: {
@@ -239,7 +235,7 @@ describe("data URI inlining", () => {
239235
const innerCell = runtime.getCell(space, "inner", undefined, tx);
240236
innerCell.set({ nested: { value: "data" } });
241237

242-
const dataURI = createDataURI(innerCell.getAsLink({
238+
const dataURI = createDataCellURI(innerCell.getAsLink({
243239
includeSchema: true,
244240
}));
245241
const link = {
@@ -266,7 +262,7 @@ describe("data URI inlining", () => {
266262

267263
describe("setRaw with data URI inlining", () => {
268264
it("should inline data URIs when using setRaw", () => {
269-
const dataURI = createDataURI("inlined value");
265+
const dataURI = createDataCellURI("inlined value");
270266
const targetCell = runtime.getCell(space, "target", undefined, tx);
271267

272268
const link = {
@@ -283,8 +279,8 @@ describe("data URI inlining", () => {
283279
});
284280

285281
it("should inline nested data URIs in objects", () => {
286-
const dataURI1 = createDataURI("value1");
287-
const dataURI2 = createDataURI("value2");
282+
const dataURI1 = createDataCellURI("value1");
283+
const dataURI2 = createDataCellURI("value2");
288284
const targetCell = runtime.getCell(space, "target", undefined, tx);
289285

290286
targetCell.setRaw({
@@ -313,7 +309,7 @@ describe("data URI inlining", () => {
313309
});
314310

315311
it("should inline data URIs in arrays", () => {
316-
const dataURI = createDataURI("array item");
312+
const dataURI = createDataCellURI("array item");
317313
const targetCell = runtime.getCell(space, "target", undefined, tx);
318314

319315
targetCell.setRaw([
@@ -334,7 +330,7 @@ describe("data URI inlining", () => {
334330

335331
describe("diffAndUpdate with data URI inlining", () => {
336332
it("should inline data URIs during diffAndUpdate", () => {
337-
const dataURI = createDataURI("updated value");
333+
const dataURI = createDataCellURI("updated value");
338334
const targetCell = runtime.getCell(space, "target", undefined, tx);
339335
targetCell.set({ initial: "value" });
340336

@@ -352,7 +348,7 @@ describe("data URI inlining", () => {
352348
});
353349

354350
it("should handle data URIs with complex nested structures", () => {
355-
const dataURI = createDataURI({
351+
const dataURI = createDataCellURI({
356352
nested: {
357353
array: [1, 2, 3],
358354
obj: { key: "value" },
@@ -379,7 +375,7 @@ describe("data URI inlining", () => {
379375
});
380376

381377
it("should not write data URIs to storage", () => {
382-
const dataURI = createDataURI("test");
378+
const dataURI = createDataCellURI("test");
383379
const targetCell = runtime.getCell(space, "target", undefined, tx);
384380

385381
const link = {
@@ -412,7 +408,7 @@ describe("data URI inlining", () => {
412408
},
413409
},
414410
};
415-
const dataURI = createDataURI({
411+
const dataURI = createDataCellURI({
416412
link: relativeLink,
417413
other: { path: "success" },
418414
});
@@ -488,7 +484,7 @@ describe("data URI inlining", () => {
488484
};
489485

490486
// Embed the link in a data URI at some intermediate level
491-
const dataURI = createDataURI({ intermediate: linkToOtherDoc });
487+
const dataURI = createDataCellURI({ intermediate: linkToOtherDoc });
492488

493489
// Now create a link that goes through data URI, then through intermediate,
494490
// and then further into the linked document beyond what data URI describes
@@ -540,7 +536,7 @@ describe("data URI inlining", () => {
540536
},
541537
},
542538
};
543-
const dataURI = createDataURI(relativeLink);
539+
const dataURI = createDataCellURI(relativeLink);
544540

545541
// Link with additional path that goes beyond the relative link
546542
const link = {
@@ -580,7 +576,7 @@ describe("data URI inlining", () => {
580576
const linkToTarget = targetCell.getAsLink();
581577

582578
// Wrap it in a data URI
583-
const dataURI1 = createDataURI({ wrapped: linkToTarget });
579+
const dataURI1 = createDataCellURI({ wrapped: linkToTarget });
584580

585581
// Create a link to first data URI
586582
const linkToDataURI1 = {
@@ -593,7 +589,7 @@ describe("data URI inlining", () => {
593589
};
594590

595591
// Wrap that in another data URI
596-
const dataURI2 = createDataURI({ doubleWrapped: linkToDataURI1 });
592+
const dataURI2 = createDataCellURI({ doubleWrapped: linkToDataURI1 });
597593

598594
// Create final link
599595
const finalLink = {
@@ -646,7 +642,7 @@ describe("data URI inlining", () => {
646642
},
647643
};
648644

649-
const dataURI = createDataURI(linkWithSchema);
645+
const dataURI = createDataCellURI(linkWithSchema);
650646

651647
// Path extends into the linked document
652648
const link = {

0 commit comments

Comments
 (0)