effect-dynamodb 1.3.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Aggregate.d.ts.map +1 -1
- package/dist/Aggregate.js +51 -23
- package/dist/Aggregate.js.map +1 -1
- package/dist/Entity.d.ts +1 -3
- package/dist/Entity.d.ts.map +1 -1
- package/dist/Entity.js +282 -282
- package/dist/Entity.js.map +1 -1
- package/dist/Marshaller.d.ts +6 -17
- package/dist/Marshaller.d.ts.map +1 -1
- package/dist/Marshaller.js +6 -102
- package/dist/Marshaller.js.map +1 -1
- package/dist/internal/EntitySchemas.d.ts +93 -16
- package/dist/internal/EntitySchemas.d.ts.map +1 -1
- package/dist/internal/EntitySchemas.js +396 -79
- package/dist/internal/EntitySchemas.js.map +1 -1
- package/dist/internal/TransactableOps.d.ts +5 -0
- package/dist/internal/TransactableOps.d.ts.map +1 -1
- package/dist/internal/TransactableOps.js +33 -27
- package/dist/internal/TransactableOps.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Extracted from Entity.ts for decomposition. Not part of the public API.
|
|
5
5
|
*/
|
|
6
|
-
import { Schema, SchemaAST } from "effect";
|
|
6
|
+
import { DateTime, Effect, Option, Redacted, Schema, SchemaAST, SchemaGetter, SchemaIssue, } from "effect";
|
|
7
7
|
import { getEncoding, isHidden } from "../DynamoModel.js";
|
|
8
8
|
/**
|
|
9
9
|
* Resolve the effective DynamoEncoding for a model-declared field, applying
|
|
@@ -213,62 +213,382 @@ export const inferDefaultEncoding = (schema) => {
|
|
|
213
213
|
return { storage: "string", domain: "Date" };
|
|
214
214
|
return undefined;
|
|
215
215
|
};
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// Self vs. transform schema detection
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
/**
|
|
220
|
+
* Returns true when the schema is a "self" schema — i.e. it carries no
|
|
221
|
+
* encoding transformation. Self schemas have `ast.encoding === undefined`.
|
|
222
|
+
*
|
|
223
|
+
* - `Schema.DateTimeUtc` — self
|
|
224
|
+
* - `Schema.DateTimeUtcFromString` — transform (encodes to string)
|
|
225
|
+
* - `Schema.RedactedFromValue(...)` — transform
|
|
226
|
+
* - any `.pipe(Schema.decodeTo(...))` chain — transform
|
|
227
|
+
*/
|
|
228
|
+
export const isSelfSchema = (schema) => schema.ast.encoding === undefined;
|
|
229
|
+
/**
|
|
230
|
+
* True if the schema is a self DateTime/Date schema that can have its storage
|
|
231
|
+
* configured via a DynamoEncoding annotation. Distinguished from a transform
|
|
232
|
+
* by `ast.encoding === undefined`.
|
|
233
|
+
*/
|
|
234
|
+
export const isSelfDateSchema = (schema) => {
|
|
235
|
+
if (!isSelfSchema(schema))
|
|
236
|
+
return false;
|
|
237
|
+
// The self-schema check is necessary but not sufficient — also confirm the
|
|
238
|
+
// type constructor matches one of the supported date domains.
|
|
239
|
+
return inferDefaultEncoding(schema) !== undefined;
|
|
240
|
+
};
|
|
241
|
+
/**
|
|
242
|
+
* Returns true when the schema is a transform schema with a date typeConstructor
|
|
243
|
+
* (e.g. `Schema.DateTimeUtcFromString`, `DynamoModel.DateEpochSeconds`).
|
|
244
|
+
* Used to enforce the "transform owns the wire format" policy at Entity.make().
|
|
245
|
+
*/
|
|
246
|
+
export const isDateTransform = (schema) => {
|
|
247
|
+
if (isSelfSchema(schema))
|
|
248
|
+
return false;
|
|
249
|
+
return inferDefaultEncoding(schema) !== undefined;
|
|
250
|
+
};
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
// Self-date schema substitution
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
216
254
|
/**
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
255
|
+
* Convert any input value (string / number / DateTime / Date) to the wire
|
|
256
|
+
* primitive for a given encoding. Used by date substitutes' `encode` path.
|
|
257
|
+
*
|
|
258
|
+
* Wire format precisely matches the legacy `serializeDateForDynamo` output:
|
|
259
|
+
* - `string` storage → ISO 8601 string (UTC for DateTime.Utc / Date; extended
|
|
260
|
+
* format for DateTime.Zoned)
|
|
261
|
+
* - `epochMs` storage → integer milliseconds since the Unix epoch
|
|
262
|
+
* - `epochSeconds` storage → integer seconds since the Unix epoch (TTL format)
|
|
220
263
|
*/
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
264
|
+
const toWirePrimitive = (value, encoding) => {
|
|
265
|
+
let epochMs;
|
|
266
|
+
let zoned;
|
|
267
|
+
if (DateTime.isDateTime(value)) {
|
|
268
|
+
if (DateTime.isZoned(value))
|
|
269
|
+
zoned = value;
|
|
270
|
+
epochMs = DateTime.toEpochMillis(value);
|
|
271
|
+
}
|
|
272
|
+
else if (value instanceof Date) {
|
|
273
|
+
epochMs = value.getTime();
|
|
274
|
+
}
|
|
275
|
+
else if (typeof value === "string") {
|
|
276
|
+
epochMs = new Date(value).getTime();
|
|
277
|
+
}
|
|
278
|
+
else if (typeof value === "number") {
|
|
279
|
+
// Numeric inputs are interpreted in the encoding's storage scale already.
|
|
280
|
+
if (encoding.storage === "epochSeconds")
|
|
281
|
+
epochMs = value * 1000;
|
|
282
|
+
else
|
|
283
|
+
epochMs = value;
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
throw new Error(`[effect-dynamodb] toWirePrimitive: unsupported input ${typeof value} for ${encoding.domain}/${encoding.storage}`);
|
|
287
|
+
}
|
|
288
|
+
switch (encoding.storage) {
|
|
289
|
+
case "string":
|
|
290
|
+
if (encoding.domain === "DateTime.Zoned" && zoned)
|
|
291
|
+
return DateTime.formatIsoZoned(zoned);
|
|
292
|
+
return new Date(epochMs).toISOString();
|
|
293
|
+
case "epochMs":
|
|
294
|
+
return epochMs;
|
|
295
|
+
case "epochSeconds":
|
|
296
|
+
return Math.floor(epochMs / 1000);
|
|
229
297
|
}
|
|
230
298
|
};
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
299
|
+
/**
|
|
300
|
+
* Build a bidirectional Schema for a given DynamoEncoding.
|
|
301
|
+
*
|
|
302
|
+
* The encoded side is `Schema.Any` so `decode` tolerates either:
|
|
303
|
+
* - the wire primitive (string / number) — the storage-shape input, OR
|
|
304
|
+
* - a domain value (`DateTime.Utc`, `Date`, etc.) — what users typically pass
|
|
305
|
+
* into `entity.put({...})` and `.set({...})`.
|
|
306
|
+
*
|
|
307
|
+
* The `decode` direction always lifts to the domain type (DateTime.Utc /
|
|
308
|
+
* DateTime.Zoned / Date). The `encode` direction always produces the wire
|
|
309
|
+
* primitive. This makes both `Schema.encode(substituted)` and
|
|
310
|
+
* `Schema.decode(substituted)` work end-to-end regardless of whether the
|
|
311
|
+
* caller passed wire-form or domain-form.
|
|
312
|
+
*
|
|
313
|
+
* @internal
|
|
314
|
+
*/
|
|
315
|
+
export const buildDateTransform = (encoding) => {
|
|
316
|
+
const targetSchema = (() => {
|
|
317
|
+
switch (encoding.domain) {
|
|
318
|
+
case "DateTime.Utc":
|
|
319
|
+
return Schema.DateTimeUtc;
|
|
320
|
+
case "DateTime.Zoned":
|
|
321
|
+
return Schema.DateTimeZoned;
|
|
322
|
+
case "Date":
|
|
323
|
+
return Schema.DateValid;
|
|
324
|
+
}
|
|
325
|
+
})();
|
|
326
|
+
const liftToDomain = (value) => {
|
|
327
|
+
if (DateTime.isDateTime(value))
|
|
328
|
+
return value;
|
|
329
|
+
if (value instanceof Date) {
|
|
330
|
+
switch (encoding.domain) {
|
|
331
|
+
case "DateTime.Utc":
|
|
332
|
+
return DateTime.makeUnsafe(value);
|
|
333
|
+
case "DateTime.Zoned":
|
|
334
|
+
return DateTime.makeZonedUnsafe(value, { timeZone: "UTC" });
|
|
335
|
+
case "Date":
|
|
336
|
+
return value;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (typeof value === "string") {
|
|
340
|
+
switch (encoding.domain) {
|
|
341
|
+
case "DateTime.Utc":
|
|
342
|
+
return DateTime.makeUnsafe(value);
|
|
343
|
+
case "DateTime.Zoned": {
|
|
344
|
+
const match = value.match(/^(.+)\[(.+)\]$/);
|
|
345
|
+
if (match) {
|
|
346
|
+
const utc = DateTime.makeUnsafe(match[1]);
|
|
347
|
+
return DateTime.makeZonedUnsafe(utc, { timeZone: match[2] });
|
|
348
|
+
}
|
|
349
|
+
const utc = DateTime.makeUnsafe(value);
|
|
350
|
+
return DateTime.makeZonedUnsafe(utc, { timeZone: "UTC" });
|
|
351
|
+
}
|
|
352
|
+
case "Date":
|
|
353
|
+
return new Date(value);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (typeof value === "number") {
|
|
357
|
+
const ms = encoding.storage === "epochSeconds" ? value * 1000 : value;
|
|
358
|
+
switch (encoding.domain) {
|
|
359
|
+
case "DateTime.Utc":
|
|
360
|
+
return DateTime.makeUnsafe(ms);
|
|
361
|
+
case "DateTime.Zoned":
|
|
362
|
+
return DateTime.makeZonedUnsafe(new Date(ms), { timeZone: "UTC" });
|
|
363
|
+
case "Date":
|
|
364
|
+
return new Date(ms);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return value;
|
|
368
|
+
};
|
|
369
|
+
return Schema.Any.pipe(Schema.decodeTo(targetSchema, {
|
|
370
|
+
decode: SchemaGetter.transformOrFail((value) => {
|
|
371
|
+
try {
|
|
372
|
+
return Effect.succeed(liftToDomain(value));
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
return Effect.fail(new SchemaIssue.InvalidType(Schema.Any.ast, Option.some(value)));
|
|
376
|
+
}
|
|
377
|
+
}),
|
|
378
|
+
encode: SchemaGetter.transformOrFail((value) => {
|
|
379
|
+
try {
|
|
380
|
+
return Effect.succeed(toWirePrimitive(value, encoding));
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
return Effect.fail(new SchemaIssue.InvalidType(Schema.Any.ast, Option.some(value)));
|
|
384
|
+
}
|
|
385
|
+
}),
|
|
386
|
+
}));
|
|
387
|
+
};
|
|
388
|
+
/**
|
|
389
|
+
* Detects `Schema.RedactedFromValue(inner)` fields (and `Schema.Redacted`).
|
|
390
|
+
* The encoded form of these schemas is "Forbidden" by Effect v4 design —
|
|
391
|
+
* encoding bails out to prevent accidental leaks. To make the round-trip work
|
|
392
|
+
* for our use case (write to DynamoDB, read back with Redacted on the domain
|
|
393
|
+
* side), we substitute with a custom transform that explicitly extracts the
|
|
394
|
+
* inner value via `Redacted.value` on encode and rewraps via `Redacted.make`
|
|
395
|
+
* on decode.
|
|
396
|
+
*
|
|
397
|
+
* Returns the inner schema (the wire-format schema) when the field is a
|
|
398
|
+
* Redacted-typed schema, otherwise undefined.
|
|
399
|
+
*/
|
|
400
|
+
const tryGetRedactedInner = (schema) => {
|
|
401
|
+
const resolved = SchemaAST.resolve(schema.ast);
|
|
402
|
+
if (resolved?.typeConstructor?._tag !== "effect/Redacted")
|
|
403
|
+
return undefined;
|
|
404
|
+
// The decoded type is `Redacted<T>` where the inner schema is at
|
|
405
|
+
// `ast.typeParameters[0]`. Walk the AST and rebuild the Schema via
|
|
406
|
+
// `Schema.make` so we get a fully-functional Schema (with `.pipe`).
|
|
407
|
+
const ast = schema.ast;
|
|
408
|
+
const params = ast.typeParameters;
|
|
409
|
+
if (!params || params.length === 0)
|
|
410
|
+
return undefined;
|
|
411
|
+
const innerAst = params[0];
|
|
412
|
+
return Schema.make(innerAst);
|
|
413
|
+
};
|
|
414
|
+
/**
|
|
415
|
+
* Build a substitute Schema for a `Schema.RedactedFromValue(inner)` field.
|
|
416
|
+
*
|
|
417
|
+
* The substitute round-trips: produces `Redacted<T>` on the domain side and
|
|
418
|
+
* the inner wire primitive on the encoded side. Both directions tolerate
|
|
419
|
+
* either form on input — a user passing a plain inner value is wrapped via
|
|
420
|
+
* `Redacted.make`; a user passing a `Redacted<T>` instance is unwrapped on
|
|
421
|
+
* encode via `Redacted.value`. This permissive shape lets the put/update
|
|
422
|
+
* paths handle the mixed wire/domain payloads users typically construct
|
|
423
|
+
* (e.g. `.put({ secret: Redacted.make(...) })`).
|
|
424
|
+
*
|
|
425
|
+
* The encoded side is `Schema.Any` so decode tolerates either form; the
|
|
426
|
+
* branch logic in `decode`/`encode` then steers the value to the right
|
|
427
|
+
* representation.
|
|
428
|
+
*/
|
|
429
|
+
const buildRedactedSubstitute = (inner) => {
|
|
430
|
+
return Schema.Any.pipe(Schema.decodeTo(Schema.Redacted(inner), {
|
|
431
|
+
decode: SchemaGetter.transform((value) => Redacted.isRedacted(value) ? value : Redacted.make(value)),
|
|
432
|
+
encode: SchemaGetter.transform((r) => Redacted.isRedacted(r) ? Redacted.value(r) : r),
|
|
433
|
+
}));
|
|
434
|
+
};
|
|
435
|
+
/**
|
|
436
|
+
* Substitute model fields with bidirectional transforms where needed:
|
|
437
|
+
*
|
|
438
|
+
* - Self date schemas with an effective encoding (annotation or override)
|
|
439
|
+
* → tolerant date transform that accepts either wire form (string/number)
|
|
440
|
+
* or domain form (DateTime / Date) on decode, and always produces the
|
|
441
|
+
* configured wire primitive on encode.
|
|
442
|
+
* - `Schema.RedactedFromValue(inner)` fields → tolerant Redacted transform
|
|
443
|
+
* that accepts either form on decode and unwraps to wire primitive on
|
|
444
|
+
* encode (Effect v4's `RedactedFromValue` forbids encoding by default;
|
|
445
|
+
* our substitution makes the round-trip work for storage).
|
|
446
|
+
*
|
|
447
|
+
* Existing transform schemas (other than `RedactedFromValue`) are passed
|
|
448
|
+
* through unchanged — the user-declared transform IS the wire format.
|
|
449
|
+
*
|
|
450
|
+
* Schema.Class fields (and Schema.Class fields nested inside Schema.Array,
|
|
451
|
+
* Schema.optional, etc.) are NOT substituted here. Instead, callers run
|
|
452
|
+
* `decode → encode` against the substituted schema: `Schema.decode` is
|
|
453
|
+
* forgiving for Schema.Class (lifts plain objects to instances), and the
|
|
454
|
+
* tolerant date / Redacted substitutes make the decode pass accept both
|
|
455
|
+
* wire and domain values. The subsequent `Schema.encode` produces the
|
|
456
|
+
* canonical wire shape.
|
|
457
|
+
*
|
|
458
|
+
* @internal
|
|
459
|
+
*/
|
|
460
|
+
export const substituteSchemas = (modelFields, fieldEncodings) => {
|
|
461
|
+
const out = {};
|
|
462
|
+
for (const [name, schema] of Object.entries(modelFields)) {
|
|
463
|
+
// 1. Self-date substitution. Pattern A: user declared `Schema.DateTimeUtc`
|
|
464
|
+
// and chose a wire format via annotation. We substitute with the
|
|
465
|
+
// tolerant date transform so the encode pipeline produces the right
|
|
466
|
+
// wire primitive.
|
|
467
|
+
if (isSelfDateSchema(schema)) {
|
|
468
|
+
const enc = fieldEncodings[name];
|
|
469
|
+
if (enc) {
|
|
470
|
+
out[name] = buildDateTransform(enc);
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
// 2. RedactedFromValue substitution. Effect v4's `RedactedFromValue`
|
|
475
|
+
// forbids encoding by design; substitute with a tolerant Redacted
|
|
476
|
+
// transform so the round-trip works for storage.
|
|
477
|
+
const redactedInner = tryGetRedactedInner(schema);
|
|
478
|
+
if (redactedInner !== undefined) {
|
|
479
|
+
out[name] = buildRedactedSubstitute(redactedInner);
|
|
480
|
+
continue;
|
|
242
481
|
}
|
|
482
|
+
// Default: pass through unchanged. Pattern B transforms (DynamoModel.*,
|
|
483
|
+
// Schema.DateTimeUtcFromString, Schema.NumberFromString, …) own their
|
|
484
|
+
// wire format directly — `Schema.encode` handles the conversion.
|
|
485
|
+
out[name] = schema;
|
|
243
486
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
487
|
+
return out;
|
|
488
|
+
};
|
|
489
|
+
/**
|
|
490
|
+
* Validate that no model field combines a transform schema with a storage
|
|
491
|
+
* override. The two configuration paths are mutually exclusive: either declare
|
|
492
|
+
* a self schema (`Schema.DateTimeUtc`) and let the annotation drive storage,
|
|
493
|
+
* OR declare a transform and own the wire format yourself — not both.
|
|
494
|
+
*
|
|
495
|
+
* Two override sources are checked:
|
|
496
|
+
*
|
|
497
|
+
* 1. `ConfiguredModel.storedAs` (configured attributes) — surfaces the
|
|
498
|
+
* override via `configuredAttributes[name].encoding`.
|
|
499
|
+
*
|
|
500
|
+
* 2. `DynamoModel.storedAs(...)` modifier piped onto the field schema —
|
|
501
|
+
* adds a `DynamoEncoding` annotation directly to the AST. We detect this
|
|
502
|
+
* by checking whether the field-level annotation disagrees with the
|
|
503
|
+
* transform's actual wire format (the `inferDefaultEncoding` of the
|
|
504
|
+
* transform). For an unmodified transform (e.g. `DynamoModel.DateString`
|
|
505
|
+
* with a matching ISO-string annotation), the two agree and the field is
|
|
506
|
+
* accepted.
|
|
507
|
+
*
|
|
508
|
+
* Throws at `Entity.make()` time if the conflict is detected.
|
|
509
|
+
*
|
|
510
|
+
* @internal
|
|
511
|
+
*/
|
|
512
|
+
export const validateNoTransformOverride = (modelFields, configuredAttributes) => {
|
|
513
|
+
const conflictMessage = (name) => `[effect-dynamodb] Field "${name}": cannot apply DynamoEncoding storage override to a transform schema. ` +
|
|
514
|
+
`Either declare a self schema (Schema.DateTimeUtc) and let the annotation drive storage, OR declare a transform and own the wire format — not both.`;
|
|
515
|
+
for (const [name, schema] of Object.entries(modelFields)) {
|
|
516
|
+
if (!isDateTransform(schema))
|
|
517
|
+
continue;
|
|
518
|
+
// 1. ConfiguredModel.storedAs override?
|
|
519
|
+
const configOverride = configuredAttributes[name]?.encoding;
|
|
520
|
+
if (configOverride !== undefined) {
|
|
521
|
+
throw new Error(conflictMessage(name));
|
|
522
|
+
}
|
|
523
|
+
// 2. DynamoModel.storedAs() modifier on the schema itself? The modifier
|
|
524
|
+
// applies a `DynamoEncoding` annotation to the AST. Detect a conflict
|
|
525
|
+
// by comparing the annotation's storage with the transform's actual
|
|
526
|
+
// wire format (read from `ast.encoding[last].to._tag`). Granularity:
|
|
527
|
+
// we can distinguish "string" vs "number" wire forms; we cannot
|
|
528
|
+
// distinguish epochMs vs epochSeconds at this layer. The string-vs-
|
|
529
|
+
// number mismatch covers the most common breaking case.
|
|
530
|
+
const annotated = getEncoding(schema);
|
|
531
|
+
if (annotated === undefined)
|
|
532
|
+
continue;
|
|
533
|
+
const transformWire = transformWireKind(schema);
|
|
534
|
+
if (transformWire === undefined)
|
|
535
|
+
continue;
|
|
536
|
+
const annotatedKind = annotated.storage === "string" ? "string" : "number";
|
|
537
|
+
if (transformWire !== annotatedKind) {
|
|
538
|
+
throw new Error(conflictMessage(name));
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
/**
|
|
543
|
+
* Read the wire form ("string" or "number") that a transform schema actually
|
|
544
|
+
* encodes to, by walking the encoding chain. Returns undefined when the chain
|
|
545
|
+
* is missing or the leaf isn't a String / Number node.
|
|
546
|
+
*/
|
|
547
|
+
const transformWireKind = (schema) => {
|
|
548
|
+
const enc = schema.ast.encoding;
|
|
549
|
+
if (!enc || enc.length === 0)
|
|
550
|
+
return undefined;
|
|
551
|
+
const leaf = enc[enc.length - 1];
|
|
552
|
+
if (!leaf)
|
|
553
|
+
return undefined;
|
|
554
|
+
const tag = leaf.to._tag;
|
|
555
|
+
if (tag === "String")
|
|
556
|
+
return "string";
|
|
557
|
+
if (tag === "Number")
|
|
558
|
+
return "number";
|
|
559
|
+
return undefined;
|
|
560
|
+
};
|
|
561
|
+
export const buildDerivedSchemas = (modelFields, indexes, systemFields, resolvedRefs = [], immutableFields = new Set(), identifierField = undefined, timeSeries = undefined, fieldEncodings = {}) => {
|
|
562
|
+
// --- Substitute model fields ---
|
|
563
|
+
// Self date schemas (with effective encoding) are replaced with bidirectional
|
|
564
|
+
// transforms that produce the configured wire primitive. RedactedFromValue
|
|
565
|
+
// fields are replaced with a custom bidirectional transform so encoding
|
|
566
|
+
// round-trips. All other transform schemas are passed through — the user's
|
|
567
|
+
// transform IS the wire format and we never override it.
|
|
568
|
+
const fields = substituteSchemas(modelFields, fieldEncodings);
|
|
569
|
+
// --- Model Schema: pure model fields (for input decode/encode) ---
|
|
570
|
+
const modelSchema = Schema.Struct(fields);
|
|
251
571
|
// --- Record Schema: model + system fields ---
|
|
252
|
-
//
|
|
572
|
+
// System timestamp fields use Schema.DateTimeUtcFromString as the canonical
|
|
573
|
+
// wire ↔ domain transform when they're library-managed. When the field
|
|
574
|
+
// collides with a model-declared field, the model's substituted schema is
|
|
575
|
+
// already in `fields` and we don't add a system-schema entry.
|
|
253
576
|
const systemSchemaFields = {};
|
|
254
|
-
if (systemFields.createdAt) {
|
|
255
|
-
// Custom encoding → use fromSelf (deserializeDateFields handles storage→domain)
|
|
256
|
-
// Default → use DateTimeUtcFromString (ISO string → DateTime.Utc)
|
|
577
|
+
if (systemFields.createdAt && !systemFields.createdAtCollision) {
|
|
257
578
|
systemSchemaFields[systemFields.createdAt] = systemFields.createdAtEncoding
|
|
258
|
-
?
|
|
579
|
+
? buildDateTransform(systemFields.createdAtEncoding)
|
|
259
580
|
: Schema.DateTimeUtcFromString;
|
|
260
581
|
}
|
|
261
|
-
if (systemFields.updatedAt) {
|
|
582
|
+
if (systemFields.updatedAt && !systemFields.updatedAtCollision) {
|
|
262
583
|
systemSchemaFields[systemFields.updatedAt] = systemFields.updatedAtEncoding
|
|
263
|
-
?
|
|
584
|
+
? buildDateTransform(systemFields.updatedAtEncoding)
|
|
264
585
|
: Schema.DateTimeUtcFromString;
|
|
265
586
|
}
|
|
266
|
-
if (systemFields.version) {
|
|
587
|
+
if (systemFields.version && !systemFields.versionCollision) {
|
|
267
588
|
systemSchemaFields[systemFields.version] = Schema.Number;
|
|
268
589
|
}
|
|
269
590
|
const recordSchema = Schema.Struct({
|
|
270
|
-
...
|
|
271
|
-
...fromSelfFields,
|
|
591
|
+
...fields,
|
|
272
592
|
...systemSchemaFields,
|
|
273
593
|
});
|
|
274
594
|
// --- System-collision handling for input/update schemas ---
|
|
@@ -300,13 +620,12 @@ export const buildDerivedSchemas = (modelFields, indexes, systemFields, resolved
|
|
|
300
620
|
};
|
|
301
621
|
// --- Input Schema: ref-aware (swap ref fields for ID fields when refs present) ---
|
|
302
622
|
const refFieldSet = new Set(resolvedRefs.map((r) => r.fieldName));
|
|
303
|
-
const buildInputFieldsAcc = (
|
|
623
|
+
const buildInputFieldsAcc = () => {
|
|
304
624
|
const acc = {};
|
|
305
|
-
for (const [key, field] of Object.entries(
|
|
625
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
306
626
|
if (refFieldSet.has(key))
|
|
307
627
|
continue;
|
|
308
|
-
const
|
|
309
|
-
const adjusted = applySystemCollisionAdjustments(key, base, "input");
|
|
628
|
+
const adjusted = applySystemCollisionAdjustments(key, field, "input");
|
|
310
629
|
if (adjusted !== null)
|
|
311
630
|
acc[key] = adjusted;
|
|
312
631
|
}
|
|
@@ -315,13 +634,12 @@ export const buildDerivedSchemas = (modelFields, indexes, systemFields, resolved
|
|
|
315
634
|
}
|
|
316
635
|
return acc;
|
|
317
636
|
};
|
|
318
|
-
const inputSchema = Schema.Struct(buildInputFieldsAcc(
|
|
319
|
-
const inputDecodeSchema = Schema.Struct(buildInputFieldsAcc("fromSelf"));
|
|
637
|
+
const inputSchema = Schema.Struct(buildInputFieldsAcc());
|
|
320
638
|
// --- Create Schema: input fields minus identifier (or PK composites as fallback) ---
|
|
321
639
|
const pkComposites = new Set(primaryKeyComposites(indexes));
|
|
322
640
|
const createOmitFields = identifierField ? new Set([identifierField]) : pkComposites;
|
|
323
641
|
const createFieldsAcc = {};
|
|
324
|
-
for (const [key, field] of Object.entries(
|
|
642
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
325
643
|
if (createOmitFields.has(key))
|
|
326
644
|
continue; // skip identifier/PK composites (auto-generated)
|
|
327
645
|
if (refFieldSet.has(key))
|
|
@@ -338,9 +656,9 @@ export const buildDerivedSchemas = (modelFields, indexes, systemFields, resolved
|
|
|
338
656
|
}
|
|
339
657
|
const createSchema = Schema.Struct(createFieldsAcc);
|
|
340
658
|
// --- Update Schema: optional model fields minus key composites & immutable ---
|
|
341
|
-
const buildUpdateFieldsAcc = (
|
|
659
|
+
const buildUpdateFieldsAcc = () => {
|
|
342
660
|
const acc = {};
|
|
343
|
-
for (const [key, field] of Object.entries(
|
|
661
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
344
662
|
if (pkComposites.has(key))
|
|
345
663
|
continue;
|
|
346
664
|
if (immutableFields.has(key))
|
|
@@ -349,8 +667,7 @@ export const buildDerivedSchemas = (modelFields, indexes, systemFields, resolved
|
|
|
349
667
|
continue;
|
|
350
668
|
if (strippedFromUpdate.has(key))
|
|
351
669
|
continue;
|
|
352
|
-
|
|
353
|
-
acc[key] = Schema.optional(base);
|
|
670
|
+
acc[key] = Schema.optional(field);
|
|
354
671
|
}
|
|
355
672
|
for (const ref of resolvedRefs) {
|
|
356
673
|
if (pkComposites.has(ref.fieldName))
|
|
@@ -361,13 +678,12 @@ export const buildDerivedSchemas = (modelFields, indexes, systemFields, resolved
|
|
|
361
678
|
}
|
|
362
679
|
return acc;
|
|
363
680
|
};
|
|
364
|
-
const updateSchema = Schema.Struct(buildUpdateFieldsAcc(
|
|
365
|
-
const updateDecodeSchema = Schema.Struct(buildUpdateFieldsAcc("fromSelf"));
|
|
681
|
+
const updateSchema = Schema.Struct(buildUpdateFieldsAcc());
|
|
366
682
|
// --- Key Schema: primary key composite attributes only ---
|
|
367
683
|
const keyCompositeNames = primaryKeyComposites(indexes);
|
|
368
684
|
const keyFields = {};
|
|
369
685
|
for (const name of keyCompositeNames) {
|
|
370
|
-
const field =
|
|
686
|
+
const field = fields[name];
|
|
371
687
|
if (field)
|
|
372
688
|
keyFields[name] = field;
|
|
373
689
|
}
|
|
@@ -391,16 +707,14 @@ export const buildDerivedSchemas = (modelFields, indexes, systemFields, resolved
|
|
|
391
707
|
: Schema.optional(Schema.String);
|
|
392
708
|
}
|
|
393
709
|
const itemSchema = Schema.Struct({
|
|
394
|
-
...
|
|
395
|
-
...fromSelfFields,
|
|
710
|
+
...fields,
|
|
396
711
|
...systemSchemaFields,
|
|
397
712
|
...keyAttrFields,
|
|
398
713
|
__edd_e__: Schema.String,
|
|
399
714
|
});
|
|
400
715
|
// --- Deleted Record Schema: record + deletedAt ---
|
|
401
716
|
const deletedRecordSchema = Schema.Struct({
|
|
402
|
-
...
|
|
403
|
-
...fromSelfFields,
|
|
717
|
+
...fields,
|
|
404
718
|
...systemSchemaFields,
|
|
405
719
|
deletedAt: Schema.String,
|
|
406
720
|
});
|
|
@@ -413,47 +727,50 @@ export const buildDerivedSchemas = (modelFields, indexes, systemFields, resolved
|
|
|
413
727
|
hiddenFieldNames.add(fieldName);
|
|
414
728
|
}
|
|
415
729
|
const hasHiddenFields = hiddenFieldNames.size > 0;
|
|
416
|
-
const
|
|
417
|
-
? Schema.Struct(Object.fromEntries(Object.entries(
|
|
418
|
-
:
|
|
730
|
+
const modelVisibleSchema = hasHiddenFields
|
|
731
|
+
? Schema.Struct(Object.fromEntries(Object.entries(fields).filter(([k]) => !hiddenFieldNames.has(k))))
|
|
732
|
+
: modelSchema;
|
|
419
733
|
const recordVisibleSchema = hasHiddenFields
|
|
420
734
|
? Schema.Struct(Object.fromEntries(Object.entries({
|
|
421
|
-
...
|
|
422
|
-
...fromSelfFields,
|
|
735
|
+
...fields,
|
|
423
736
|
...systemSchemaFields,
|
|
424
737
|
}).filter(([k]) => !hiddenFieldNames.has(k))))
|
|
425
738
|
: recordSchema;
|
|
426
739
|
// --- Time-series derived schemas ---
|
|
427
|
-
// `appendInputSchema`: the user-supplied schema
|
|
740
|
+
// `appendInputSchema`: the user-supplied schema, with the same substitution
|
|
741
|
+
// rules applied as the model fields (self date schemas → tolerant
|
|
742
|
+
// transforms, RedactedFromValue → tolerant Redacted). `Entity.make()`
|
|
428
743
|
// enforces that every PK/SK composite + `orderBy` appears in its fields.
|
|
429
|
-
// `historyRecordSchema`: event-item shape.
|
|
430
|
-
//
|
|
431
|
-
// timeSeries auto-suppresses). No GSI key attrs — events strip GSI keys.
|
|
744
|
+
// `historyRecordSchema`: event-item shape. Substituted model fields + no
|
|
745
|
+
// system fields (events are point-in-time facts keyed by `orderBy`).
|
|
432
746
|
const timeSeriesEnabled = timeSeries !== undefined && timeSeries !== null;
|
|
433
|
-
const appendInputSchema = timeSeriesEnabled
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
747
|
+
const appendInputSchema = timeSeriesEnabled
|
|
748
|
+
? (() => {
|
|
749
|
+
const userSchema = timeSeries.appendInput;
|
|
750
|
+
const userFields = userSchema.fields;
|
|
751
|
+
if (!userFields)
|
|
752
|
+
return userSchema;
|
|
753
|
+
// Compute encodings for the appendInput fields (annotation +
|
|
754
|
+
// inferred from typeConstructor for self schemas).
|
|
755
|
+
const appendEncodings = buildFieldEncodings(userFields, {});
|
|
756
|
+
const subbedFields = substituteSchemas(userFields, appendEncodings);
|
|
757
|
+
return Schema.Struct(subbedFields);
|
|
758
|
+
})()
|
|
759
|
+
: null;
|
|
437
760
|
const historyRecordSchema = timeSeriesEnabled
|
|
438
761
|
? (() => {
|
|
439
762
|
const hiddenFilter = (record) => hasHiddenFields
|
|
440
763
|
? Object.fromEntries(Object.entries(record).filter(([k]) => !hiddenFieldNames.has(k)))
|
|
441
764
|
: record;
|
|
442
|
-
return Schema.Struct(hiddenFilter({
|
|
443
|
-
...modelFields,
|
|
444
|
-
...fromSelfFields,
|
|
445
|
-
}));
|
|
765
|
+
return Schema.Struct(hiddenFilter({ ...fields }));
|
|
446
766
|
})()
|
|
447
767
|
: null;
|
|
448
768
|
return {
|
|
449
|
-
modelSchema:
|
|
450
|
-
modelDecodeSchema: modelDecodeVisibleSchema,
|
|
769
|
+
modelSchema: modelVisibleSchema,
|
|
451
770
|
recordSchema: recordVisibleSchema,
|
|
452
771
|
inputSchema: inputSchema,
|
|
453
|
-
inputDecodeSchema: inputDecodeSchema,
|
|
454
772
|
createSchema: createSchema,
|
|
455
773
|
updateSchema: updateSchema,
|
|
456
|
-
updateDecodeSchema: updateDecodeSchema,
|
|
457
774
|
keySchema: keySchema,
|
|
458
775
|
itemSchema: itemSchema,
|
|
459
776
|
deletedRecordSchema: deletedRecordSchema,
|