Skip to content

Commit aea68c8

Browse files
authored
Emulate value and source path behaviors in StorageTransaction shim (#1344)
- Enhanced StorageTransaction shim to support correct emulation of 'value' and 'source' path semantics for read/write operations. - Added/updated tests to verify: - Writing and reading to nested paths under 'value' - Special handling and validation for the 'source' path (including error cases for invalid source references, non-existent docs, and invalid path usage) - Data URI read-only behaviors and error handling for invalid/unsupported cases - Improved error reporting for invalid path, type, and data URI scenarios.
1 parent 2a3bf94 commit aea68c8

File tree

2 files changed

+258
-63
lines changed

2 files changed

+258
-63
lines changed

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

Lines changed: 124 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,13 @@ function validateParentPath(
8080

8181
// Set pathError.path to last valid parent path component
8282
pathError.path = [];
83-
while (parentPath.length > 0) {
84-
const segment = parentPath.shift()!;
85-
value = value[segment];
86-
if (!isRecord(value)) break;
87-
pathError.path.push(segment);
83+
if (isRecord(value)) {
84+
while (parentPath.length > 0) {
85+
const segment = parentPath.shift()!;
86+
value = value[segment];
87+
if (!isRecord(value)) break;
88+
pathError.path.push(segment);
89+
}
8890
}
8991

9092
return pathError;
@@ -191,23 +193,60 @@ class TransactionReader implements ITransactionReader {
191193
return { ok: undefined, error: notFoundError };
192194
}
193195

194-
const validationError = validateParentPath(doc.get(), address.path);
195-
if (validationError) {
196-
return { ok: undefined, error: validationError };
196+
// Path-based logic
197+
if (!address.path.length) {
198+
const notFoundError: INotFoundError = new Error(
199+
`Path must not be empty`,
200+
) as INotFoundError;
201+
notFoundError.name = "NotFoundError";
202+
return { ok: undefined, error: notFoundError };
203+
}
204+
const [first, ...rest] = address.path;
205+
if (first === "value") {
206+
// Validate parent path exists and is a record for nested writes/reads
207+
const validationError = validateParentPath(doc.get(), rest);
208+
if (validationError) {
209+
return { ok: undefined, error: validationError };
210+
}
211+
// Read from doc itself
212+
const value = doc.getAtPath(rest);
213+
const read: Read = {
214+
address,
215+
value,
216+
cause: refer("shim does not care"),
217+
};
218+
this.log.addRead(read);
219+
return { ok: read };
220+
} else if (first === "source") {
221+
// Only allow path length 1
222+
if (rest.length > 0) {
223+
const notFoundError: INotFoundError = new Error(
224+
`Path beyond 'source' is not allowed`,
225+
) as INotFoundError;
226+
notFoundError.name = "NotFoundError";
227+
return { ok: undefined, error: notFoundError };
228+
}
229+
// Return the URI of the sourceCell if it exists
230+
const sourceCell = doc.sourceCell;
231+
let value: string | undefined = undefined;
232+
if (sourceCell) {
233+
// Convert EntityId to URI string
234+
value = `of:${JSON.parse(JSON.stringify(sourceCell.entityId))["/"]}`;
235+
}
236+
const read: Read = {
237+
address,
238+
value,
239+
cause: refer("shim does not care"),
240+
};
241+
this.log.addRead(read);
242+
return { ok: read };
243+
} else {
244+
const notFoundError: INotFoundError = new Error(
245+
`Invalid first path element: ${String(first)}`,
246+
) as INotFoundError;
247+
notFoundError.name = "NotFoundError";
248+
return { ok: undefined, error: notFoundError };
197249
}
198-
199-
// Read the value at the specified path
200-
const value = getValueAtPath(doc.get(), address.path);
201-
202-
// Create the read invariant
203-
const read: Read = {
204-
address,
205-
value,
206-
cause: refer("shim does not care"),
207-
};
208-
this.log.addRead(read);
209-
210-
return { ok: read };
211250
}
212251
}
213252

@@ -264,35 +303,76 @@ class TransactionWriter extends TransactionReader
264303
throw new Error(`Failed to get or create document: ${address.id}`);
265304
}
266305

267-
// For non-empty paths, check if document exists and is a record
268-
if (address.path.length > 0) {
269-
if (doc.get() === undefined || !isRecord(doc.get())) {
306+
// Path-based logic
307+
if (!address.path.length) {
308+
const notFoundError: INotFoundError = new Error(
309+
`Path must not be empty`,
310+
) as INotFoundError;
311+
notFoundError.name = "NotFoundError";
312+
return { ok: undefined, error: notFoundError };
313+
}
314+
const [first, ...rest] = address.path;
315+
if (first === "value") {
316+
// Validate parent path exists and is a record for nested writes
317+
const validationError = validateParentPath(doc.get(), rest);
318+
if (validationError) {
319+
return { ok: undefined, error: validationError };
320+
}
321+
// Write to doc itself
322+
doc.setAtPath(rest, value);
323+
const write: Write = {
324+
address,
325+
value,
326+
cause: refer(address.id),
327+
};
328+
this.log.addWrite(write);
329+
return { ok: write };
330+
} else if (first === "source") {
331+
// Only allow path length 1
332+
if (rest.length > 0) {
270333
const notFoundError: INotFoundError = new Error(
271-
`Document not found or not a record: ${address.id}`,
334+
`Path beyond 'source' is not allowed`,
272335
) as INotFoundError;
273336
notFoundError.name = "NotFoundError";
274337
return { ok: undefined, error: notFoundError };
275338
}
339+
// Value must be a URI string (of:...)
340+
if (typeof value !== "string" || !value.startsWith("of:")) {
341+
const notFoundError: INotFoundError = new Error(
342+
`Value for 'source' must be a URI string (of:...)`,
343+
) as INotFoundError;
344+
notFoundError.name = "NotFoundError";
345+
return { ok: undefined, error: notFoundError };
346+
}
347+
// Get the source doc in the same space
348+
const sourceEntityId = uriToEntityId(value);
349+
const sourceDoc = this.runtime.documentMap.getDocByEntityId(
350+
address.space,
351+
sourceEntityId,
352+
false,
353+
);
354+
if (!sourceDoc) {
355+
const notFoundError: INotFoundError = new Error(
356+
`Source document not found: ${value}`,
357+
) as INotFoundError;
358+
notFoundError.name = "NotFoundError";
359+
return { ok: undefined, error: notFoundError };
360+
}
361+
doc.sourceCell = sourceDoc;
362+
const write: Write = {
363+
address,
364+
value,
365+
cause: refer(address.id),
366+
};
367+
this.log.addWrite(write);
368+
return { ok: write };
369+
} else {
370+
const notFoundError: INotFoundError = new Error(
371+
`Invalid first path element: ${String(first)}`,
372+
) as INotFoundError;
373+
notFoundError.name = "NotFoundError";
374+
return { ok: undefined, error: notFoundError };
276375
}
277-
278-
// Validate parent path exists and is a record for nested writes
279-
const validationError = validateParentPath(doc.get(), address.path);
280-
if (validationError) {
281-
return { ok: undefined, error: validationError };
282-
}
283-
284-
// Write the value at the specified path
285-
doc.setAtPath(address.path, value);
286-
287-
// Create the write invariant
288-
const write: Write = {
289-
address,
290-
value,
291-
cause: refer(address.id),
292-
};
293-
this.log.addWrite(write);
294-
295-
return { ok: write };
296376
}
297377
}
298378

0 commit comments

Comments
 (0)