Skip to content

Commit 56183b9

Browse files
committed
fix: produce NotFoundError's when reading paths that don't exist
1 parent 859fc39 commit 56183b9

File tree

9 files changed

+168
-69
lines changed

9 files changed

+168
-69
lines changed

packages/runner/src/storage/interface.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -656,10 +656,6 @@ export type CommitError =
656656
| InactiveTransactionError
657657
| StorageTransactionRejected;
658658

659-
export interface INotFoundError extends Error {
660-
name: "NotFoundError";
661-
path?: MemoryAddressPathComponent[];
662-
}
663659

664660
/**
665661
* Error returned when the media type is not supported by the storage transaction.
@@ -688,6 +684,7 @@ export type ReadError =
688684

689685
export type WriteError =
690686
| INotFoundError
687+
| IStorageTransactionInconsistent
691688
| IUnsupportedMediaTypeError
692689
| InactiveTransactionError
693690
| IReadOnlyAddressError;
@@ -714,6 +711,13 @@ export interface INotFoundError extends Error {
714711
* Address that we could not resolve.
715712
*/
716713
address: IMemoryAddress;
714+
715+
/**
716+
* @deprecated Use `address.path` instead. This property exists for backward compatibility.
717+
*/
718+
path?: MemoryAddressPathComponent[];
719+
720+
from(space: MemorySpace): INotFoundError;
717721
}
718722

719723
/**

packages/runner/src/storage/transaction-shim.ts

Lines changed: 124 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,60 @@ import { getValueAtPath } from "../path-utils.ts";
4141
import { getJSONFromDataURI } from "../uri-utils.ts";
4242
import { ignoreReadForScheduling } from "../scheduler.ts";
4343

44+
/**
45+
* NotFoundError implementation for transaction-shim
46+
*/
47+
class NotFoundError extends RangeError implements INotFoundError {
48+
override readonly name = "NotFoundError" as const;
49+
public readonly source: IAttestation;
50+
public readonly address: IMemorySpaceAddress;
51+
private readonly space?: MemorySpace;
52+
53+
constructor(
54+
message: string,
55+
id: string,
56+
path: readonly MemoryAddressPathComponent[] = [],
57+
value?: JSONValue,
58+
space?: MemorySpace,
59+
) {
60+
super(message);
61+
this.space = space;
62+
// Ensure id has a valid URI format for type compatibility
63+
const uri = id.includes(":") ? id as `${string}:${string}` : `of:${id}` as `${string}:${string}`;
64+
this.address = {
65+
id: uri,
66+
type: "application/json",
67+
path: [...path], // Convert readonly to mutable array
68+
space: space || "" as MemorySpace,
69+
};
70+
this.source = {
71+
address: {
72+
id: uri,
73+
type: "application/json",
74+
path: [],
75+
},
76+
value,
77+
};
78+
}
79+
80+
/**
81+
* @deprecated Use `address.path` instead. This property exists for backward compatibility.
82+
*/
83+
get path(): MemoryAddressPathComponent[] {
84+
return [...this.address.path];
85+
}
86+
87+
from(space: MemorySpace): INotFoundError {
88+
return new NotFoundError(
89+
this.message,
90+
this.address.id,
91+
[...this.address.path], // Convert to mutable array
92+
this.source.value,
93+
space,
94+
);
95+
}
96+
}
97+
4498
/**
4599
* Convert a URI string to an EntityId object
46100
*/
@@ -57,6 +111,7 @@ export function uriToEntityId(uri: string): EntityId {
57111
function validateParentPath(
58112
value: any,
59113
path: readonly MemoryAddressPathComponent[],
114+
id: string,
60115
): INotFoundError | null {
61116
const pathLength = path.length;
62117

@@ -67,11 +122,12 @@ function validateParentPath(
67122
// Check if the document itself exists and is an object for first-level writes
68123
if (pathLength === 1) {
69124
if (value === undefined || !isRecord(value)) {
70-
const pathError: INotFoundError = new Error(
125+
return new NotFoundError(
71126
`Cannot access path [${String(path[0])}] - document is not a record`,
72-
) as INotFoundError;
73-
pathError.name = "NotFoundError";
74-
return pathError;
127+
id,
128+
[...path],
129+
value,
130+
);
75131
}
76132
return null;
77133
}
@@ -92,17 +148,15 @@ function validateParentPath(
92148
if (
93149
value === undefined || parentValue === undefined || !isRecord(parentValue)
94150
) {
95-
const pathError: INotFoundError = new Error(
151+
const errorPath = (parentIndex > 0) ? path.slice(0, parentIndex - 1) : [];
152+
return new NotFoundError(
96153
`Cannot access path [${path.join(", ")}] - parent path [${
97154
path.slice(0, lastIndex).join(", ")
98155
}] does not exist or is not a record`,
99-
) as INotFoundError;
100-
pathError.name = "NotFoundError";
101-
102-
// Set pathError.path to last valid parent path component
103-
if (parentIndex > 0) pathError.path = path.slice(0, parentIndex - 1);
104-
105-
return pathError;
156+
id,
157+
errorPath,
158+
value,
159+
);
106160
}
107161

108162
return null;
@@ -201,7 +255,7 @@ class TransactionReader implements ITransactionReader {
201255
try {
202256
const json = getJSONFromDataURI(address.id);
203257

204-
const validationError = validateParentPath(json, address.path);
258+
const validationError = validateParentPath(json, address.path, address.id);
205259
if (validationError) {
206260
return { ok: undefined, error: validationError };
207261
}
@@ -239,25 +293,31 @@ class TransactionReader implements ITransactionReader {
239293
);
240294

241295
if (!doc) {
242-
const notFoundError: INotFoundError = new Error(
296+
const notFoundError = new NotFoundError(
243297
`Document not found: ${address.id}`,
244-
) as INotFoundError;
245-
notFoundError.name = "NotFoundError";
298+
address.id,
299+
[],
300+
undefined,
301+
address.space,
302+
);
246303
return { ok: undefined, error: notFoundError };
247304
}
248305

249306
// Path-based logic
250307
if (!address.path.length) {
251-
const notFoundError: INotFoundError = new Error(
308+
const notFoundError = new NotFoundError(
252309
`Path must not be empty`,
253-
) as INotFoundError;
254-
notFoundError.name = "NotFoundError";
310+
address.id,
311+
[],
312+
undefined,
313+
address.space,
314+
);
255315
return { ok: undefined, error: notFoundError };
256316
}
257317
const [first, ...rest] = address.path;
258318
if (first === "value") {
259319
// Validate parent path exists and is a record for nested writes/reads
260-
const validationError = validateParentPath(doc.get(), rest);
320+
const validationError = validateParentPath(doc.get(), rest, address.id);
261321
if (validationError) {
262322
return { ok: undefined, error: validationError };
263323
}
@@ -272,10 +332,13 @@ class TransactionReader implements ITransactionReader {
272332
} else if (first === "source") {
273333
// Only allow path length 1
274334
if (rest.length > 0) {
275-
const notFoundError: INotFoundError = new Error(
335+
const notFoundError = new NotFoundError(
276336
`Path beyond 'source' is not allowed`,
277-
) as INotFoundError;
278-
notFoundError.name = "NotFoundError";
337+
address.id,
338+
[...address.path],
339+
undefined,
340+
address.space,
341+
);
279342
return { ok: undefined, error: notFoundError };
280343
}
281344
// Return the URI of the sourceCell if it exists
@@ -292,10 +355,13 @@ class TransactionReader implements ITransactionReader {
292355
this.journal.addRead(address, options);
293356
return { ok: read };
294357
} else {
295-
const notFoundError: INotFoundError = new Error(
358+
const notFoundError = new NotFoundError(
296359
`Invalid first path element: ${String(first)}`,
297-
) as INotFoundError;
298-
notFoundError.name = "NotFoundError";
360+
address.id,
361+
[...address.path],
362+
undefined,
363+
address.space,
364+
);
299365
return { ok: undefined, error: notFoundError };
300366
}
301367
}
@@ -369,16 +435,19 @@ class TransactionWriter extends TransactionReader
369435

370436
// Path-based logic
371437
if (!address.path.length) {
372-
const notFoundError: INotFoundError = new Error(
438+
const notFoundError = new NotFoundError(
373439
`Path must not be empty`,
374-
) as INotFoundError;
375-
notFoundError.name = "NotFoundError";
440+
address.id,
441+
[],
442+
undefined,
443+
address.space,
444+
);
376445
return { ok: undefined, error: notFoundError };
377446
}
378447
const [first, ...rest] = address.path;
379448
if (first === "value") {
380449
// Validate parent path exists and is a record for nested writes
381-
const validationError = validateParentPath(doc.get(), rest);
450+
const validationError = validateParentPath(doc.get(), rest, address.id);
382451
if (validationError) {
383452
return { ok: undefined, error: validationError };
384453
}
@@ -398,18 +467,24 @@ class TransactionWriter extends TransactionReader
398467
} else if (first === "source") {
399468
// Only allow path length 1
400469
if (rest.length > 0) {
401-
const notFoundError: INotFoundError = new Error(
470+
const notFoundError = new NotFoundError(
402471
`Path beyond 'source' is not allowed`,
403-
) as INotFoundError;
404-
notFoundError.name = "NotFoundError";
472+
address.id,
473+
[...address.path],
474+
undefined,
475+
address.space,
476+
);
405477
return { ok: undefined, error: notFoundError };
406478
}
407479
// Value must be a URI string (of:...)
408480
if (typeof value !== "string" || !value.startsWith("of:")) {
409-
const notFoundError: INotFoundError = new Error(
481+
const notFoundError = new NotFoundError(
410482
`Value for 'source' must be a URI string (of:...)`,
411-
) as INotFoundError;
412-
notFoundError.name = "NotFoundError";
483+
address.id,
484+
[...address.path],
485+
value,
486+
address.space,
487+
);
413488
return { ok: undefined, error: notFoundError };
414489
}
415490
// Get the source doc in the same space
@@ -420,10 +495,13 @@ class TransactionWriter extends TransactionReader
420495
false,
421496
);
422497
if (!sourceDoc) {
423-
const notFoundError: INotFoundError = new Error(
498+
const notFoundError = new NotFoundError(
424499
`Source document not found: ${value}`,
425-
) as INotFoundError;
426-
notFoundError.name = "NotFoundError";
500+
address.id,
501+
[...address.path],
502+
value,
503+
address.space,
504+
);
427505
return { ok: undefined, error: notFoundError };
428506
}
429507
doc.sourceCell = sourceDoc;
@@ -434,10 +512,13 @@ class TransactionWriter extends TransactionReader
434512
this.journal.addWrite(address);
435513
return { ok: write };
436514
} else {
437-
const notFoundError: INotFoundError = new Error(
515+
const notFoundError = new NotFoundError(
438516
`Invalid first path element: ${String(first)}`,
439-
) as INotFoundError;
440-
notFoundError.name = "NotFoundError";
517+
address.id,
518+
[...address.path],
519+
undefined,
520+
address.space,
521+
);
441522
return { ok: undefined, error: notFoundError };
442523
}
443524
}
@@ -646,7 +727,7 @@ export class ExtendedStorageTransaction implements IExtendedStorageTransaction {
646727
const writeResult = this.tx.write(address, value);
647728
if (writeResult.error && writeResult.error.name === "NotFoundError") {
648729
// Create parent entries if needed
649-
const lastValidPath = writeResult.error.path;
730+
const lastValidPath = (writeResult.error as INotFoundError).path;
650731
const valueObj = lastValidPath
651732
? this.readValueOrThrow({ ...address, path: lastValidPath }, {
652733
meta: ignoreReadForScheduling,

0 commit comments

Comments
 (0)