@effect-dynamodb/schema 1.21.0 → 1.23.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/DynamoModel.d.ts.map +1 -1
- package/dist/DynamoModel.js +4 -16
- package/dist/DynamoModel.js.map +1 -1
- package/dist/DynamoSchema.d.ts +26 -18
- package/dist/DynamoSchema.d.ts.map +1 -1
- package/dist/DynamoSchema.js +13 -16
- package/dist/DynamoSchema.js.map +1 -1
- package/dist/Errors.d.ts +58 -0
- package/dist/Errors.d.ts.map +1 -1
- package/dist/Errors.js +30 -0
- package/dist/Errors.js.map +1 -1
- package/dist/KeyComposer.d.ts +29 -0
- package/dist/KeyComposer.d.ts.map +1 -1
- package/dist/KeyComposer.js +31 -0
- package/dist/KeyComposer.js.map +1 -1
- package/dist/Projection.d.ts.map +1 -1
- package/dist/Projection.js +4 -2
- package/dist/Projection.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/EntitySchemas.d.ts +177 -3
- package/dist/internal/EntitySchemas.d.ts.map +1 -1
- package/dist/internal/EntitySchemas.js +1132 -61
- package/dist/internal/EntitySchemas.js.map +1 -1
- package/dist/internal/ZonedIso.d.ts +21 -0
- package/dist/internal/ZonedIso.d.ts.map +1 -0
- package/dist/internal/ZonedIso.js +35 -0
- package/dist/internal/ZonedIso.js.map +1 -0
- package/package.json +1 -1
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { DateTime, Effect, Redacted, Schema, SchemaAST, SchemaGetter, SchemaIssue } from "effect";
|
|
7
7
|
import { getEncoding, isConfiguredModel, isHidden, } from "../DynamoModel.js";
|
|
8
|
-
import { firstTypeParameterAst, getSchemaFields, isSchemaLike, transformWireKind as transformWireKindImpl, } from "./SchemaAccessors.js";
|
|
8
|
+
import { firstTypeParameterAst, getSchemaFields, isSchemaLike, numericTypeWithStringEncoding, transformWireKind as transformWireKindImpl, } from "./SchemaAccessors.js";
|
|
9
|
+
import { parseZonedIso } from "./ZonedIso.js";
|
|
9
10
|
/**
|
|
10
11
|
* Resolve the effective DynamoEncoding for a model-declared field, applying
|
|
11
12
|
* ConfiguredModel storage overrides on top of schema-level annotations.
|
|
@@ -282,6 +283,54 @@ export const isSelfDateSchema = (schema) => {
|
|
|
282
283
|
// type constructor matches one of the supported date domains.
|
|
283
284
|
return inferDefaultEncoding(schema) !== undefined;
|
|
284
285
|
};
|
|
286
|
+
/**
|
|
287
|
+
* Whether a value of this schema can hold something, AT ANY DEPTH, whose stored
|
|
288
|
+
* form differs from its domain form (#133):
|
|
289
|
+
*
|
|
290
|
+
* - any node carrying an encoding transformation — a leaf transform
|
|
291
|
+
* (`DateTimeUtcFromString`, `BigIntFromString`, …), a `Schema.Class`, a
|
|
292
|
+
* `withDecodingDefault` chain;
|
|
293
|
+
* - a self date (`Schema.DateTimeUtc` / `DateTimeZoned` / `Date`), whose domain
|
|
294
|
+
* value marshals to a map rather than its wire primitive;
|
|
295
|
+
* - a `Redacted` declaration.
|
|
296
|
+
*
|
|
297
|
+
* Walks through containers (`Arrays`, `Objects`, `Union` — which covers
|
|
298
|
+
* `Schema.optional` and `NullOr` — and `Suspend`); an `optionalKey` wrapper is
|
|
299
|
+
* the inner AST with an optional context, so it needs no case of its own. The
|
|
300
|
+
* tolerant `substituteSchemaDeep` walks the same containers (see
|
|
301
|
+
* `walkedContainer`), except `Suspend`: a recursive schema keeps its own
|
|
302
|
+
* codec, so the aggregate encodes it through its own `encode`.
|
|
303
|
+
*
|
|
304
|
+
* `hasEncodingTransformation` asks the same question of the TOP-LEVEL node
|
|
305
|
+
* only, and an `Arrays` / `Objects` node never carries an encoding itself — so a
|
|
306
|
+
* `Schema.Array(DateTimeUtcFromString)` attribute was treated as identity and
|
|
307
|
+
* its `DateTime`s were marshalled as maps.
|
|
308
|
+
*/
|
|
309
|
+
export const containsWireTransform = (schema) => astContainsWireTransform(schema.ast, new Set());
|
|
310
|
+
const astContainsWireTransform = (ast, seen) => {
|
|
311
|
+
if (seen.has(ast))
|
|
312
|
+
return false;
|
|
313
|
+
seen.add(ast);
|
|
314
|
+
if (ast.encoding !== undefined)
|
|
315
|
+
return true;
|
|
316
|
+
if (SchemaAST.isDeclaration(ast)) {
|
|
317
|
+
const resolved = SchemaAST.resolve(ast);
|
|
318
|
+
const rep = resolved?.representation;
|
|
319
|
+
return matchDateRepresentation(resolved) !== undefined || rep?.id === "effect/schema/Redacted";
|
|
320
|
+
}
|
|
321
|
+
const walk = (child) => astContainsWireTransform(child, seen);
|
|
322
|
+
if (SchemaAST.isArrays(ast))
|
|
323
|
+
return ast.elements.some(walk) || ast.rest.some(walk);
|
|
324
|
+
if (SchemaAST.isObjects(ast)) {
|
|
325
|
+
return (ast.propertySignatures.some((ps) => walk(ps.type)) ||
|
|
326
|
+
ast.indexSignatures.some((is) => walk(is.type)));
|
|
327
|
+
}
|
|
328
|
+
if (SchemaAST.isUnion(ast))
|
|
329
|
+
return ast.types.some(walk);
|
|
330
|
+
if (SchemaAST.isSuspend(ast))
|
|
331
|
+
return walk(ast.thunk());
|
|
332
|
+
return false;
|
|
333
|
+
};
|
|
285
334
|
/**
|
|
286
335
|
* Returns true when the schema is a transform schema with a date typeConstructor
|
|
287
336
|
* (e.g. `Schema.DateTimeUtcFromString`, `DynamoModel.DateEpochSeconds`).
|
|
@@ -295,6 +344,69 @@ export const isDateTransform = (schema) => {
|
|
|
295
344
|
// ---------------------------------------------------------------------------
|
|
296
345
|
// Self-date schema substitution
|
|
297
346
|
// ---------------------------------------------------------------------------
|
|
347
|
+
/** A plain record — what unmarshalling produces, as opposed to a class instance. */
|
|
348
|
+
const isPlainObject = (value) => {
|
|
349
|
+
if (value === null || typeof value !== "object")
|
|
350
|
+
return false;
|
|
351
|
+
const proto = Object.getPrototypeOf(value);
|
|
352
|
+
return proto === Object.prototype || proto === null;
|
|
353
|
+
};
|
|
354
|
+
/**
|
|
355
|
+
* A real `DateTime` instance. `DateTime.isDateTime` checks for the type-id
|
|
356
|
+
* property only, so a plain object read back from a marshalled instance passes
|
|
357
|
+
* it too (#133) — and is then neither `Equal` to the instant nor usable with the
|
|
358
|
+
* `DateTime` API that relies on the prototype.
|
|
359
|
+
*/
|
|
360
|
+
const isGenuineDateTime = (value) => DateTime.isDateTime(value) && !isPlainObject(value);
|
|
361
|
+
const reviveTimeZone = (zone) => {
|
|
362
|
+
if (!isPlainObject(zone))
|
|
363
|
+
return undefined;
|
|
364
|
+
try {
|
|
365
|
+
if (zone._tag === "Named" && typeof zone.id === "string") {
|
|
366
|
+
return DateTime.zoneMakeNamedUnsafe(zone.id);
|
|
367
|
+
}
|
|
368
|
+
if (zone._tag === "Offset" && typeof zone.offset === "number") {
|
|
369
|
+
return DateTime.zoneMakeOffset(zone.offset);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
return undefined;
|
|
376
|
+
};
|
|
377
|
+
/**
|
|
378
|
+
* Rebuild a `DateTime` from a MARSHALLED instance (#133).
|
|
379
|
+
*
|
|
380
|
+
* Before #133 an aggregate stored some `DateTime`s by marshalling the instance
|
|
381
|
+
* itself, which keeps its own enumerable properties:
|
|
382
|
+
*
|
|
383
|
+
* - `Utc`: `{ epochMilliseconds, <type-id>, _tag: "Utc" }`
|
|
384
|
+
* - `Zoned`: the same plus `zone`, itself `{ id, _tag: "Named" }` or
|
|
385
|
+
* `{ offset, _tag: "Offset" }` (a named zone's `Intl` formatter marshals to
|
|
386
|
+
* an empty map and is not needed).
|
|
387
|
+
*
|
|
388
|
+
* The type-id key is `~effect/time/DateTime` on effect 4.0.0-rc and
|
|
389
|
+
* `~effect/DateTime` on 4.0.0, so it is deliberately NOT inspected: the instant
|
|
390
|
+
* and the tag are what identify the value, and they are all a `DateTime` holds.
|
|
391
|
+
* A zoned value is rebuilt only when its zone is recoverable.
|
|
392
|
+
*
|
|
393
|
+
* Returns `undefined` for anything else — including a genuine `DateTime`, which
|
|
394
|
+
* needs no rebuilding.
|
|
395
|
+
*/
|
|
396
|
+
export const reviveMarshalledDateTime = (value) => {
|
|
397
|
+
if (!isPlainObject(value))
|
|
398
|
+
return undefined;
|
|
399
|
+
const epochMs = value.epochMilliseconds;
|
|
400
|
+
if (typeof epochMs !== "number" || !Number.isFinite(epochMs))
|
|
401
|
+
return undefined;
|
|
402
|
+
if (value._tag === "Utc")
|
|
403
|
+
return DateTime.makeUnsafe(epochMs);
|
|
404
|
+
if (value._tag === "Zoned") {
|
|
405
|
+
const timeZone = reviveTimeZone(value.zone);
|
|
406
|
+
return timeZone === undefined ? undefined : DateTime.makeZonedUnsafe(epochMs, { timeZone });
|
|
407
|
+
}
|
|
408
|
+
return undefined;
|
|
409
|
+
};
|
|
298
410
|
/**
|
|
299
411
|
* Convert any input value (string / number / DateTime / Date) to the wire
|
|
300
412
|
* primitive for a given encoding. Used by date substitutes' `encode` path.
|
|
@@ -305,7 +417,8 @@ export const isDateTransform = (schema) => {
|
|
|
305
417
|
* - `epochMs` storage → integer milliseconds since the Unix epoch
|
|
306
418
|
* - `epochSeconds` storage → integer seconds since the Unix epoch (TTL format)
|
|
307
419
|
*/
|
|
308
|
-
const toWirePrimitive = (
|
|
420
|
+
const toWirePrimitive = (input, encoding) => {
|
|
421
|
+
const value = reviveMarshalledDateTime(input) ?? input;
|
|
309
422
|
let epochMs;
|
|
310
423
|
let zoned;
|
|
311
424
|
if (DateTime.isDateTime(value)) {
|
|
@@ -356,7 +469,9 @@ const toWirePrimitive = (value, encoding) => {
|
|
|
356
469
|
*
|
|
357
470
|
* @internal
|
|
358
471
|
*/
|
|
359
|
-
export const buildDateTransform = (encoding) => {
|
|
472
|
+
export const buildDateTransform = (encoding, options) => {
|
|
473
|
+
const strict = options?.strictWireKind === true;
|
|
474
|
+
const canonicalOnly = options?.canonicalOnly === true;
|
|
360
475
|
const targetSchema = (() => {
|
|
361
476
|
switch (encoding.domain) {
|
|
362
477
|
case "DateTime.Utc":
|
|
@@ -367,9 +482,59 @@ export const buildDateTransform = (encoding) => {
|
|
|
367
482
|
return Schema.Date;
|
|
368
483
|
}
|
|
369
484
|
})();
|
|
370
|
-
const
|
|
371
|
-
|
|
485
|
+
const fromDateTime = (value) => {
|
|
486
|
+
switch (encoding.domain) {
|
|
487
|
+
case "DateTime.Utc":
|
|
488
|
+
return DateTime.toUtc(value);
|
|
489
|
+
case "DateTime.Zoned":
|
|
490
|
+
return DateTime.isZoned(value)
|
|
491
|
+
? value
|
|
492
|
+
: DateTime.makeZonedUnsafe(DateTime.toEpochMillis(value), { timeZone: "UTC" });
|
|
493
|
+
case "Date":
|
|
494
|
+
return DateTime.toDateUtc(value);
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
/** Under `strictWireKind`: whether a DateTime is of this encoding's own domain. */
|
|
498
|
+
const ownDomain = (value) => encoding.domain === "DateTime.Utc"
|
|
499
|
+
? DateTime.isUtc(value)
|
|
500
|
+
: encoding.domain === "DateTime.Zoned" && DateTime.isZoned(value);
|
|
501
|
+
const liftStrict = (value) => {
|
|
502
|
+
const revived = reviveMarshalledDateTime(value);
|
|
503
|
+
if (revived !== undefined) {
|
|
504
|
+
if (ownDomain(revived))
|
|
505
|
+
return revived;
|
|
506
|
+
throw new Error("[effect-dynamodb] marshalled DateTime of another domain");
|
|
507
|
+
}
|
|
508
|
+
if (isGenuineDateTime(value) && ownDomain(value))
|
|
372
509
|
return value;
|
|
510
|
+
if (value instanceof Date && encoding.domain === "Date" && !Number.isNaN(value.getTime())) {
|
|
511
|
+
return value;
|
|
512
|
+
}
|
|
513
|
+
if (typeof value === (encoding.storage === "string" ? "string" : "number")) {
|
|
514
|
+
const lifted = liftToDomain(value);
|
|
515
|
+
if (lifted instanceof Date && Number.isNaN(lifted.getTime())) {
|
|
516
|
+
throw new Error("[effect-dynamodb] invalid date");
|
|
517
|
+
}
|
|
518
|
+
if (canonicalOnly && toWirePrimitive(lifted, encoding) !== value) {
|
|
519
|
+
throw new Error("[effect-dynamodb] not this date member's canonical wire form");
|
|
520
|
+
}
|
|
521
|
+
return lifted;
|
|
522
|
+
}
|
|
523
|
+
throw new Error("[effect-dynamodb] not this date member's wire or domain form");
|
|
524
|
+
};
|
|
525
|
+
const liftToDomain = (value) => {
|
|
526
|
+
// A marshalled `DateTime` instance (#133) — rebuilt, whatever type-id key
|
|
527
|
+
// the Effect version that wrote it used.
|
|
528
|
+
const revived = reviveMarshalledDateTime(value);
|
|
529
|
+
if (revived !== undefined)
|
|
530
|
+
return fromDateTime(revived);
|
|
531
|
+
if (DateTime.isDateTime(value)) {
|
|
532
|
+
if (isGenuineDateTime(value))
|
|
533
|
+
return value;
|
|
534
|
+
// Duck-types as a DateTime but carries no recoverable instant: refuse it
|
|
535
|
+
// rather than hand a plain object to the domain.
|
|
536
|
+
throw new Error("[effect-dynamodb] unrecoverable marshalled DateTime");
|
|
537
|
+
}
|
|
373
538
|
if (value instanceof Date) {
|
|
374
539
|
switch (encoding.domain) {
|
|
375
540
|
case "DateTime.Utc":
|
|
@@ -384,15 +549,9 @@ export const buildDateTransform = (encoding) => {
|
|
|
384
549
|
switch (encoding.domain) {
|
|
385
550
|
case "DateTime.Utc":
|
|
386
551
|
return DateTime.makeUnsafe(value);
|
|
387
|
-
case "DateTime.Zoned":
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const utc = DateTime.makeUnsafe(match[1]);
|
|
391
|
-
return DateTime.makeZonedUnsafe(utc, { timeZone: match[2] });
|
|
392
|
-
}
|
|
393
|
-
const utc = DateTime.makeUnsafe(value);
|
|
394
|
-
return DateTime.makeZonedUnsafe(utc, { timeZone: "UTC" });
|
|
395
|
-
}
|
|
552
|
+
case "DateTime.Zoned":
|
|
553
|
+
// Named and offset zones are both rebuilt as written (#133).
|
|
554
|
+
return parseZonedIso(value);
|
|
396
555
|
case "Date":
|
|
397
556
|
return new Date(value);
|
|
398
557
|
}
|
|
@@ -413,7 +572,7 @@ export const buildDateTransform = (encoding) => {
|
|
|
413
572
|
return Schema.Any.pipe(Schema.decodeTo(targetSchema, {
|
|
414
573
|
decode: SchemaGetter.transformEffect((value) => {
|
|
415
574
|
try {
|
|
416
|
-
return Effect.succeed(liftToDomain(value));
|
|
575
|
+
return Effect.succeed(strict ? liftStrict(value) : liftToDomain(value));
|
|
417
576
|
}
|
|
418
577
|
catch {
|
|
419
578
|
return Effect.fail(new SchemaIssue.InvalidType(Schema.Any.ast, value));
|
|
@@ -510,14 +669,31 @@ const buildTolerantTransform = (schema) => {
|
|
|
510
669
|
const decodeWire = Schema.decodeUnknownOption(codec);
|
|
511
670
|
const validateDomain = Schema.decodeUnknownOption(typeSide);
|
|
512
671
|
const encodeDomain = Schema.encodeUnknownOption(codec);
|
|
672
|
+
const bigintDomain = SchemaAST.isBigInt(SchemaAST.toType(schema.ast));
|
|
513
673
|
return Schema.Any.pipe(Schema.decodeTo(typeSide, {
|
|
514
674
|
decode: SchemaGetter.transformEffect((value) => {
|
|
515
675
|
const fromWire = decodeWire(value);
|
|
516
676
|
if (fromWire._tag === "Some")
|
|
517
677
|
return Effect.succeed(fromWire.value);
|
|
518
|
-
|
|
678
|
+
// A marshalled `DateTime` (#133) is rebuilt before it is validated: a
|
|
679
|
+
// 4.0.0-era map would otherwise pass a `DateTime` check by duck typing
|
|
680
|
+
// and come back as a plain object.
|
|
681
|
+
const revived = reviveMarshalledDateTime(value);
|
|
682
|
+
const alreadyDomain = validateDomain(revived ?? value);
|
|
519
683
|
if (alreadyDomain._tag === "Some")
|
|
520
684
|
return Effect.succeed(alreadyDomain.value);
|
|
685
|
+
// A value stored in its DOMAIN form, as a transform nested in a container
|
|
686
|
+
// (or under `optional`) was before #133, comes back changed by the
|
|
687
|
+
// unmarshaller: a `bigint` is written as `N` and read back as a JS
|
|
688
|
+
// `number` (or a `bigint` only beyond the safe-integer range). Lift an
|
|
689
|
+
// integer back to the `bigint` it was. Every other primitive domain form
|
|
690
|
+
// (`number`, `string`, `boolean`, `Uint8Array`) reads back as itself and
|
|
691
|
+
// is already accepted above.
|
|
692
|
+
if (bigintDomain && typeof value === "number" && Number.isSafeInteger(value)) {
|
|
693
|
+
const lifted = validateDomain(BigInt(value));
|
|
694
|
+
if (lifted._tag === "Some")
|
|
695
|
+
return Effect.succeed(lifted.value);
|
|
696
|
+
}
|
|
521
697
|
return Effect.fail(new SchemaIssue.InvalidType(schema.ast, value));
|
|
522
698
|
}),
|
|
523
699
|
encode: SchemaGetter.transformEffect((value) => {
|
|
@@ -535,6 +711,155 @@ const buildTolerantTransform = (schema) => {
|
|
|
535
711
|
}),
|
|
536
712
|
}));
|
|
537
713
|
};
|
|
714
|
+
/** A plain `Schema.BigInt` (no transformation): stored as `N`, read back as a JS number. */
|
|
715
|
+
const isSelfBigInt = (schema) => schema.ast.encoding === undefined && SchemaAST.isBigInt(schema.ast);
|
|
716
|
+
/** A safe integer, or a bigint the unmarshaller returned for an out-of-range `N`. */
|
|
717
|
+
const legacyBigInt = (value) => typeof value === "bigint"
|
|
718
|
+
? value
|
|
719
|
+
: typeof value === "number" && Number.isSafeInteger(value)
|
|
720
|
+
? BigInt(value)
|
|
721
|
+
: undefined;
|
|
722
|
+
/**
|
|
723
|
+
* A plain `Schema.BigInt` that also reads the form DynamoDB hands back (#133).
|
|
724
|
+
* A `bigint` is marshalled as `N` and unmarshalled as a JS `number` (or as a
|
|
725
|
+
* `bigint` beyond the safe-integer range), which `Schema.BigInt` rejects — so a
|
|
726
|
+
* plain bigint field could be written but never read. Decode lifts a safe
|
|
727
|
+
* integer back to the `bigint` it was; encode is the schema's own (identity).
|
|
728
|
+
*/
|
|
729
|
+
const buildBigIntLift = (schema) => {
|
|
730
|
+
const validate = Schema.decodeUnknownOption(schema);
|
|
731
|
+
const encode = Schema.encodeUnknownOption(schema);
|
|
732
|
+
return Schema.Any.pipe(Schema.decodeTo(schema, {
|
|
733
|
+
decode: SchemaGetter.transformEffect((value) => {
|
|
734
|
+
const lifted = legacyBigInt(value);
|
|
735
|
+
const valid = lifted === undefined ? validate(value) : validate(lifted);
|
|
736
|
+
return valid._tag === "Some"
|
|
737
|
+
? Effect.succeed(valid.value)
|
|
738
|
+
: Effect.fail(new SchemaIssue.InvalidType(schema.ast, value));
|
|
739
|
+
}),
|
|
740
|
+
encode: SchemaGetter.transformEffect((value) => {
|
|
741
|
+
const wire = encode(value);
|
|
742
|
+
return wire._tag === "Some"
|
|
743
|
+
? Effect.succeed(wire.value)
|
|
744
|
+
: Effect.fail(new SchemaIssue.InvalidType(schema.ast, value));
|
|
745
|
+
}),
|
|
746
|
+
}));
|
|
747
|
+
};
|
|
748
|
+
/**
|
|
749
|
+
* An ENTITY transform field that also reads the domain-form values earlier
|
|
750
|
+
* releases left behind (#133) — but only the forms no other wire value can be
|
|
751
|
+
* mistaken for:
|
|
752
|
+
*
|
|
753
|
+
* - a `number` for a number-from-string transform (`NumberFromString`);
|
|
754
|
+
* - a safe-integer `number` (or unmarshalled `bigint`) for a bigint-from-string;
|
|
755
|
+
* - a marshalled `DateTime` map for a date transform (`DateTimeUtcFromString`).
|
|
756
|
+
*
|
|
757
|
+
* Path-based updates marshalled their value as given, so a `5` set on a
|
|
758
|
+
* `NumberFromString` was stored as `N` and every later read failed with
|
|
759
|
+
* "Expected string". A string is never accepted here other than through the
|
|
760
|
+
* transform's own decode, and the encode is the transform's own with no
|
|
761
|
+
* fallback, so what the transform writes is unchanged. Read schemas only.
|
|
762
|
+
*/
|
|
763
|
+
const buildLegacyReadTransform = (schema) => {
|
|
764
|
+
const typeAst = SchemaAST.toType(schema.ast);
|
|
765
|
+
const typeSide = Schema.make(typeAst);
|
|
766
|
+
const codec = schema;
|
|
767
|
+
const decodeWire = Schema.decodeUnknownOption(codec);
|
|
768
|
+
const validateDomain = Schema.decodeUnknownOption(typeSide);
|
|
769
|
+
const encodeWire = Schema.encodeUnknownOption(codec);
|
|
770
|
+
const isDate = isDateTransform(schema);
|
|
771
|
+
const numberDomain = SchemaAST.isNumber(typeAst);
|
|
772
|
+
const bigintDomain = SchemaAST.isBigInt(typeAst);
|
|
773
|
+
const legacy = (value) => {
|
|
774
|
+
if (isDate)
|
|
775
|
+
return reviveMarshalledDateTime(value);
|
|
776
|
+
if (numberDomain && typeof value === "number")
|
|
777
|
+
return value;
|
|
778
|
+
if (bigintDomain)
|
|
779
|
+
return legacyBigInt(value);
|
|
780
|
+
return undefined;
|
|
781
|
+
};
|
|
782
|
+
return Schema.Any.pipe(Schema.decodeTo(typeSide, {
|
|
783
|
+
decode: SchemaGetter.transformEffect((value) => {
|
|
784
|
+
const fromWire = decodeWire(value);
|
|
785
|
+
if (fromWire._tag === "Some")
|
|
786
|
+
return Effect.succeed(fromWire.value);
|
|
787
|
+
const domain = legacy(value);
|
|
788
|
+
if (domain !== undefined) {
|
|
789
|
+
const valid = validateDomain(domain);
|
|
790
|
+
if (valid._tag === "Some")
|
|
791
|
+
return Effect.succeed(valid.value);
|
|
792
|
+
}
|
|
793
|
+
return Effect.fail(new SchemaIssue.InvalidType(schema.ast, value));
|
|
794
|
+
}),
|
|
795
|
+
encode: SchemaGetter.transformEffect((value) => {
|
|
796
|
+
const wire = encodeWire(value);
|
|
797
|
+
return wire._tag === "Some"
|
|
798
|
+
? Effect.succeed(wire.value)
|
|
799
|
+
: Effect.fail(new SchemaIssue.InvalidType(schema.ast, value));
|
|
800
|
+
}),
|
|
801
|
+
}));
|
|
802
|
+
};
|
|
803
|
+
/**
|
|
804
|
+
* The primitive kinds a union member can be STORED as (#133). A self date is
|
|
805
|
+
* stored as its configured storage kind; a plain `bigint` as `N`; anything
|
|
806
|
+
* else as its encoded side's string / number forms. Objects, arrays, booleans
|
|
807
|
+
* and null never collide with a date's primitive and contribute nothing.
|
|
808
|
+
*/
|
|
809
|
+
const memberWireKinds = (member, options) => {
|
|
810
|
+
const kinds = new Set();
|
|
811
|
+
const inner = optionalField(member)?.inner ?? member;
|
|
812
|
+
if (isSelfDateSchema(inner)) {
|
|
813
|
+
const encoding = getEncoding(inner) ?? inferDefaultEncoding(inner);
|
|
814
|
+
kinds.add(encoding?.storage === "string" || encoding === undefined ? "string" : "number");
|
|
815
|
+
return kinds;
|
|
816
|
+
}
|
|
817
|
+
const visit = (ast, seen) => {
|
|
818
|
+
if (seen.has(ast))
|
|
819
|
+
return;
|
|
820
|
+
seen.add(ast);
|
|
821
|
+
if (SchemaAST.isDeclaration(ast)) {
|
|
822
|
+
// A self date nested in this member — `NullOr(DateTimeUtc)` inside an
|
|
823
|
+
// outer union — is stored as its storage kind too.
|
|
824
|
+
const nested = Schema.make(ast);
|
|
825
|
+
if (isSelfDateSchema(nested)) {
|
|
826
|
+
const encoding = getEncoding(nested) ?? inferDefaultEncoding(nested);
|
|
827
|
+
kinds.add(encoding === undefined || encoding.storage === "string" ? "string" : "number");
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
else if (SchemaAST.isAny(ast) || SchemaAST.isUnknown(ast)) {
|
|
831
|
+
kinds.add("string");
|
|
832
|
+
kinds.add("number");
|
|
833
|
+
}
|
|
834
|
+
else if (SchemaAST.isString(ast) || SchemaAST.isTemplateLiteral(ast)) {
|
|
835
|
+
kinds.add("string");
|
|
836
|
+
}
|
|
837
|
+
else if (SchemaAST.isNumber(ast) || SchemaAST.isBigInt(ast)) {
|
|
838
|
+
kinds.add("number");
|
|
839
|
+
}
|
|
840
|
+
else if (SchemaAST.isLiteral(ast)) {
|
|
841
|
+
if (typeof ast.literal === "string")
|
|
842
|
+
kinds.add("string");
|
|
843
|
+
if (typeof ast.literal === "number" || typeof ast.literal === "bigint")
|
|
844
|
+
kinds.add("number");
|
|
845
|
+
}
|
|
846
|
+
else if (SchemaAST.isEnum(ast)) {
|
|
847
|
+
for (const [, value] of ast.enums)
|
|
848
|
+
kinds.add(typeof value === "number" ? "number" : "string");
|
|
849
|
+
}
|
|
850
|
+
else if (SchemaAST.isUnion(ast)) {
|
|
851
|
+
for (const type of ast.types)
|
|
852
|
+
visit(type, seen);
|
|
853
|
+
}
|
|
854
|
+
else if (SchemaAST.isSuspend(ast)) {
|
|
855
|
+
visit(ast.thunk(), seen);
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
visit(SchemaAST.toEncoded(inner.ast), new Set());
|
|
859
|
+
if (options?.domainSide)
|
|
860
|
+
visit(SchemaAST.toType(inner.ast), new Set());
|
|
861
|
+
return kinds;
|
|
862
|
+
};
|
|
538
863
|
/**
|
|
539
864
|
* True when the schema is a Schema.Class (a constructable function carrying
|
|
540
865
|
* fields whose AST is a `Declaration`). The AST check matters: since Effect
|
|
@@ -543,6 +868,12 @@ const buildTolerantTransform = (schema) => {
|
|
|
543
868
|
const isClassSchema = (schema) => typeof schema === "function" &&
|
|
544
869
|
schemaFieldsOf(schema) !== undefined &&
|
|
545
870
|
schema.ast._tag === "Declaration";
|
|
871
|
+
/** The Struct AST a class decodes from — the last link of its encoding chain. */
|
|
872
|
+
const classStructAst = (schema) => {
|
|
873
|
+
const encoding = schema.ast.encoding;
|
|
874
|
+
const last = encoding === undefined ? undefined : encoding[encoding.length - 1];
|
|
875
|
+
return last !== undefined && SchemaAST.isObjects(last.to) ? last.to : undefined;
|
|
876
|
+
};
|
|
546
877
|
/** True when the schema is `Schema.Array(...)` (AST tag `Arrays`). */
|
|
547
878
|
const isArraySchema = (schema) => schema.ast._tag === "Arrays";
|
|
548
879
|
/**
|
|
@@ -568,8 +899,10 @@ const arrayElementOf = (schema) => {
|
|
|
568
899
|
* accessors (which return real schemas with `.fields` / `.value` / constructor
|
|
569
900
|
* intact), NOT AST reconstruction (which loses those). Returns `undefined` for
|
|
570
901
|
* non-optional fields.
|
|
902
|
+
*
|
|
903
|
+
* @internal Also used by the aggregate runtime to see through a field's optionality.
|
|
571
904
|
*/
|
|
572
|
-
const optionalField = (field) => {
|
|
905
|
+
export const optionalField = (field) => {
|
|
573
906
|
const ast = field.ast;
|
|
574
907
|
if (ast.context?.isOptional !== true)
|
|
575
908
|
return undefined;
|
|
@@ -594,6 +927,281 @@ const optionalField = (field) => {
|
|
|
594
927
|
rewrap: (s) => Schema.optionalKey(s),
|
|
595
928
|
};
|
|
596
929
|
};
|
|
930
|
+
/** The options a nested schema inherits: everything but the per-position ones. */
|
|
931
|
+
const descend = (opts) => opts?.tolerantTransforms ||
|
|
932
|
+
opts?.resolveRef ||
|
|
933
|
+
opts?.strictWireKind ||
|
|
934
|
+
opts?.legacyReads ||
|
|
935
|
+
opts?.path ||
|
|
936
|
+
opts?.enforceChecks
|
|
937
|
+
? {
|
|
938
|
+
...(opts?.tolerantTransforms ? { tolerantTransforms: true } : {}),
|
|
939
|
+
...(opts?.resolveRef ? { resolveRef: opts.resolveRef } : {}),
|
|
940
|
+
...(opts?.strictWireKind ? { strictWireKind: true } : {}),
|
|
941
|
+
...(opts?.legacyReads ? { legacyReads: true } : {}),
|
|
942
|
+
...(opts?.path ? { path: opts.path } : {}),
|
|
943
|
+
...(opts?.enforceChecks ? { enforceChecks: true } : {}),
|
|
944
|
+
}
|
|
945
|
+
: undefined;
|
|
946
|
+
/** Options for a struct / class field: the inherited ones, with the field on the path. */
|
|
947
|
+
const atField = (deeper, opts, name) => ({ ...deeper, path: [...(opts?.path ?? []), name] });
|
|
948
|
+
/**
|
|
949
|
+
* The containers besides Struct / Class / Array that a value can nest a
|
|
950
|
+
* transformed leaf in: `Union` (incl. `NullOr` and a non-field `UndefinedOr`),
|
|
951
|
+
* `Record`, `Tuple`, `TupleWithRest` and `StructWithRest`. Each is rebuilt
|
|
952
|
+
* through its own runtime accessors so its kind, union options and checks are
|
|
953
|
+
* kept. `containsWireTransform` walks the same shapes.
|
|
954
|
+
*
|
|
955
|
+
* Walked in every mode. Without `tolerantTransforms` (entity derivation) the
|
|
956
|
+
* walk substitutes only self-date and `Redacted` leaves, so an entity's
|
|
957
|
+
* `NullOr(Schema.DateTimeUtc)` stores its wire form instead of a marshalled
|
|
958
|
+
* `DateTime` (#133), while a Pattern B transform inside keeps owning its wire.
|
|
959
|
+
*/
|
|
960
|
+
const walkedContainer = (schema) => {
|
|
961
|
+
const shape = containerShape(schema);
|
|
962
|
+
if (shape === undefined)
|
|
963
|
+
return undefined;
|
|
964
|
+
return { ...shape, rebuild: (children) => withMetadataOf(schema, shape.rebuild(children)) };
|
|
965
|
+
};
|
|
966
|
+
/**
|
|
967
|
+
* Give a rebuilt container the original's node-level metadata — its
|
|
968
|
+
* annotations, `.check()` refinements and context. Rebuilding through a
|
|
969
|
+
* constructor (`Schema.Record(k, v)`, `Schema.TupleWithRest(…)`) produces a
|
|
970
|
+
* fresh node without them, so a `.check(Schema.isMaxProperties(1))` on the
|
|
971
|
+
* original was silently dropped and an invalid value accepted (#133).
|
|
972
|
+
*/
|
|
973
|
+
const withMetadataOf = (original, rebuilt) => withAstMetadataOf(original.ast, rebuilt);
|
|
974
|
+
/** `withMetadataOf`, taking the metadata from an AST node. */
|
|
975
|
+
const withAstMetadataOf = (source, rebuilt) => {
|
|
976
|
+
const from = source;
|
|
977
|
+
const to = rebuilt.ast;
|
|
978
|
+
if (to.annotations === from.annotations &&
|
|
979
|
+
to.checks === from.checks &&
|
|
980
|
+
to.context === from.context &&
|
|
981
|
+
to.encodingChecks === from.encodingChecks) {
|
|
982
|
+
return rebuilt;
|
|
983
|
+
}
|
|
984
|
+
// AST nodes are immutable value objects: a shallow copy with the metadata
|
|
985
|
+
// fields replaced is the same node shape the constructors produce.
|
|
986
|
+
const ast = Object.assign(Object.create(Object.getPrototypeOf(to)), to, {
|
|
987
|
+
annotations: from.annotations,
|
|
988
|
+
checks: from.checks,
|
|
989
|
+
context: from.context,
|
|
990
|
+
encodingChecks: from.encodingChecks,
|
|
991
|
+
});
|
|
992
|
+
return Schema.make(ast);
|
|
993
|
+
};
|
|
994
|
+
const containerShape = (schema) => {
|
|
995
|
+
const s = schema;
|
|
996
|
+
const ast = schema.ast;
|
|
997
|
+
// A container piped through its own transformation (`Record(…).pipe(decodeTo(…))`)
|
|
998
|
+
// has an encoding chain tied to its ORIGINAL children; rebuilding it would
|
|
999
|
+
// detach that chain, so it is left exactly as declared.
|
|
1000
|
+
if (ast.encoding !== undefined)
|
|
1001
|
+
return undefined;
|
|
1002
|
+
const schemas = (value) => Array.isArray(value) && value.every(isSchemaLike)
|
|
1003
|
+
? value
|
|
1004
|
+
: undefined;
|
|
1005
|
+
if (SchemaAST.isUnion(ast)) {
|
|
1006
|
+
const members = schemas(s.members);
|
|
1007
|
+
if (members === undefined || typeof s.mapMembers !== "function")
|
|
1008
|
+
return undefined;
|
|
1009
|
+
const union = schema;
|
|
1010
|
+
return {
|
|
1011
|
+
children: members,
|
|
1012
|
+
rebuild: (subs) => union.mapMembers(() => subs, { unsafePreserveChecks: true }),
|
|
1013
|
+
isUnion: true,
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
if (SchemaAST.isArrays(ast)) {
|
|
1017
|
+
const elements = schemas(s.elements);
|
|
1018
|
+
if (elements !== undefined && typeof s.mapElements === "function") {
|
|
1019
|
+
const tuple = schema;
|
|
1020
|
+
return {
|
|
1021
|
+
children: elements,
|
|
1022
|
+
rebuild: (subs) => tuple.mapElements(() => subs, { unsafePreserveChecks: true }),
|
|
1023
|
+
isUnion: false,
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
const rest = schemas(s.rest);
|
|
1027
|
+
if (isSchemaLike(s.schema) && rest !== undefined) {
|
|
1028
|
+
return {
|
|
1029
|
+
children: [s.schema, ...rest],
|
|
1030
|
+
rebuild: ([head, ...tail]) => Schema.TupleWithRest(head, tail),
|
|
1031
|
+
isUnion: false,
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
return undefined;
|
|
1035
|
+
}
|
|
1036
|
+
if (SchemaAST.isObjects(ast)) {
|
|
1037
|
+
if (isSchemaLike(s.key) && isSchemaLike(s.value)) {
|
|
1038
|
+
const key = s.key;
|
|
1039
|
+
return {
|
|
1040
|
+
children: [s.value],
|
|
1041
|
+
rebuild: ([value]) => Schema.Record(key, value),
|
|
1042
|
+
isUnion: false,
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
const records = schemas(s.records);
|
|
1046
|
+
if (isSchemaLike(s.schema) && records !== undefined) {
|
|
1047
|
+
return {
|
|
1048
|
+
children: [s.schema, ...records],
|
|
1049
|
+
rebuild: ([head, ...tail]) => Schema.StructWithRest(head, tail),
|
|
1050
|
+
isUnion: false,
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
return undefined;
|
|
1055
|
+
};
|
|
1056
|
+
/** Options for a tolerant container's children: a Union's members decode strictly by kind. */
|
|
1057
|
+
const childOptions = (container, deeper, index, opts) => {
|
|
1058
|
+
if (!container.isUnion)
|
|
1059
|
+
return deeper;
|
|
1060
|
+
// What the other members are stored as — plus, for a union nested inside
|
|
1061
|
+
// another (`Union([NullOr(DateTimeUtc), String])`), what the OUTER union's
|
|
1062
|
+
// other members are stored as: they compete for the same stored value.
|
|
1063
|
+
const collidingKinds = new Set(opts?.collidingKinds ?? []);
|
|
1064
|
+
container.children.forEach((member, i) => {
|
|
1065
|
+
if (i !== index) {
|
|
1066
|
+
const kinds = memberWireKinds(member, { domainSide: opts?.tolerantTransforms === true });
|
|
1067
|
+
for (const kind of kinds)
|
|
1068
|
+
collidingKinds.add(kind);
|
|
1069
|
+
}
|
|
1070
|
+
});
|
|
1071
|
+
return {
|
|
1072
|
+
...deeper,
|
|
1073
|
+
strictWireKind: true,
|
|
1074
|
+
collidingKinds,
|
|
1075
|
+
...(opts?.unionDateOverride ? { dateEncodingOverride: opts.unionDateOverride } : {}),
|
|
1076
|
+
};
|
|
1077
|
+
};
|
|
1078
|
+
/** The options an optional wrapper hands its inner schema: it is transparent. */
|
|
1079
|
+
/**
|
|
1080
|
+
* A field whose LAST encoding step makes the encoded key optional while the
|
|
1081
|
+
* type side stays required — `Schema.withDecodingDefault(…)` /
|
|
1082
|
+
* `withDecodingDefaultKey(…)`, or any `optional(…).pipe(decodeTo(…))`. Its
|
|
1083
|
+
* inner schema (the field without that step) is what substitution works on;
|
|
1084
|
+
* `rewrap` re-applies the same step on top of the substitute, so the default
|
|
1085
|
+
* survives (#133). Without it a self-date / transform leaf was replaced
|
|
1086
|
+
* wholesale and the read schema lost the default — a stored item without the
|
|
1087
|
+
* key failed to decode.
|
|
1088
|
+
*/
|
|
1089
|
+
const decodingDefaultField = (field) => {
|
|
1090
|
+
const ast = field.ast;
|
|
1091
|
+
const encoding = ast.encoding;
|
|
1092
|
+
if (encoding === undefined || encoding.length === 0)
|
|
1093
|
+
return undefined;
|
|
1094
|
+
if (ast.context?.isOptional === true) {
|
|
1095
|
+
return undefined;
|
|
1096
|
+
}
|
|
1097
|
+
const last = encoding[encoding.length - 1];
|
|
1098
|
+
const lastTo = last.to;
|
|
1099
|
+
if (lastTo.context?.isOptional !== true)
|
|
1100
|
+
return undefined;
|
|
1101
|
+
const transformation = last.transformation;
|
|
1102
|
+
if (transformation._tag !== "Transformation")
|
|
1103
|
+
return undefined;
|
|
1104
|
+
// The same node without its last encoding step (what Effect's internal
|
|
1105
|
+
// `replaceEncoding` does: a shallow copy carrying the shorter chain).
|
|
1106
|
+
const innerAst = Object.assign(Object.create(Object.getPrototypeOf(ast)), ast, {
|
|
1107
|
+
encoding: encoding.length === 1 ? undefined : encoding.slice(0, -1),
|
|
1108
|
+
});
|
|
1109
|
+
const inner = Schema.make(innerAst);
|
|
1110
|
+
const withUndefined = SchemaAST.isUnion(lastTo) && lastTo.types.some((t) => SchemaAST.isUndefined(t));
|
|
1111
|
+
return {
|
|
1112
|
+
inner,
|
|
1113
|
+
rewrap: (substitute) => {
|
|
1114
|
+
const encodedSide = Schema.toEncoded(substitute);
|
|
1115
|
+
const optionalEncoded = withUndefined
|
|
1116
|
+
? Schema.optional(encodedSide)
|
|
1117
|
+
: Schema.optionalKey(encodedSide);
|
|
1118
|
+
return optionalEncoded.pipe(Schema.decodeTo(substitute, transformation));
|
|
1119
|
+
},
|
|
1120
|
+
};
|
|
1121
|
+
};
|
|
1122
|
+
const throughOptional = (opts) => {
|
|
1123
|
+
const deeper = descend(opts);
|
|
1124
|
+
return opts?.unionDateOverride || opts?.collidingKinds || opts?.dateEncodingOverride
|
|
1125
|
+
? {
|
|
1126
|
+
...deeper,
|
|
1127
|
+
...(opts.unionDateOverride ? { unionDateOverride: opts.unionDateOverride } : {}),
|
|
1128
|
+
...(opts.collidingKinds ? { collidingKinds: opts.collidingKinds } : {}),
|
|
1129
|
+
...(opts.dateEncodingOverride ? { dateEncodingOverride: opts.dateEncodingOverride } : {}),
|
|
1130
|
+
}
|
|
1131
|
+
: deeper;
|
|
1132
|
+
};
|
|
1133
|
+
/**
|
|
1134
|
+
* The substitute for a self-date leaf: its own (or the configured) storage,
|
|
1135
|
+
* adjusted for a union whose other members collide with it (#133).
|
|
1136
|
+
*/
|
|
1137
|
+
const selfDatePlan = (schema, opts) => {
|
|
1138
|
+
const own = getEncoding(schema) ?? inferDefaultEncoding(schema);
|
|
1139
|
+
if (!own)
|
|
1140
|
+
return undefined;
|
|
1141
|
+
const encoding = opts?.dateEncodingOverride
|
|
1142
|
+
? { storage: opts.dateEncodingOverride.storage, domain: own.domain }
|
|
1143
|
+
: own;
|
|
1144
|
+
const colliding = opts?.collidingKinds;
|
|
1145
|
+
let canonicalOnly = false;
|
|
1146
|
+
if (colliding !== undefined) {
|
|
1147
|
+
// An epoch number is indistinguishable from a number member's value — any
|
|
1148
|
+
// integer is a canonical epoch — so no stored number could be read back
|
|
1149
|
+
// reliably. Refused at make() time (EDD-9058) rather than guessed.
|
|
1150
|
+
if (encoding.storage !== "string" && colliding.has("number")) {
|
|
1151
|
+
const field = opts?.path?.length ? opts.path.join(".") : "(unnamed)";
|
|
1152
|
+
const reason = opts?.tolerantTransforms
|
|
1153
|
+
? `next to a member that is stored as a number or decodes to one (aggregates ` +
|
|
1154
|
+
`re-decode domain values on update, so a NumberFromString / BigIntFromString ` +
|
|
1155
|
+
`member collides too). A number could belong to either member`
|
|
1156
|
+
: `next to a member that is also stored as a number. A stored number could ` +
|
|
1157
|
+
`belong to either member`;
|
|
1158
|
+
throw new Error(`[EDD-9058] Field "${field}" is a union whose date member is stored as an epoch ` +
|
|
1159
|
+
`number (${encoding.storage}), ${reason}, so it cannot be read back reliably. ` +
|
|
1160
|
+
`Store the date as a string (DynamoModel.DateString — the default for a self ` +
|
|
1161
|
+
`date), or remove the numeric member from the union.`);
|
|
1162
|
+
}
|
|
1163
|
+
canonicalOnly = colliding.has(encoding.storage === "string" ? "string" : "number");
|
|
1164
|
+
}
|
|
1165
|
+
return { encoding, canonicalOnly };
|
|
1166
|
+
};
|
|
1167
|
+
const selfDateSubstitute = (schema, opts) => {
|
|
1168
|
+
const plan = selfDatePlan(schema, opts);
|
|
1169
|
+
if (plan === undefined)
|
|
1170
|
+
return schema;
|
|
1171
|
+
return buildDateTransform(plan.encoding, {
|
|
1172
|
+
strictWireKind: opts?.strictWireKind,
|
|
1173
|
+
canonicalOnly: plan.canonicalOnly,
|
|
1174
|
+
});
|
|
1175
|
+
};
|
|
1176
|
+
/**
|
|
1177
|
+
* Whether a union member is a self date that claims only its canonical form
|
|
1178
|
+
* (see `selfDatePlan`). Such a member is decoded FIRST: its canonical spelling
|
|
1179
|
+
* is the one value it and a colliding member could both take, and that value
|
|
1180
|
+
* is what the library writes for a date — so it is read as the date whatever
|
|
1181
|
+
* the declared member order. Encoding is unaffected: a date and a string /
|
|
1182
|
+
* number are told apart by their Type side, which no reordering changes.
|
|
1183
|
+
*/
|
|
1184
|
+
const isCanonicalDateMember = (member, opts) => {
|
|
1185
|
+
const inner = optionalField(member)?.inner ?? member;
|
|
1186
|
+
if (isSelfDateSchema(inner))
|
|
1187
|
+
return selfDatePlan(inner, opts)?.canonicalOnly === true;
|
|
1188
|
+
// A nested union holding such a member (`NullOr(DateTimeUtc)` next to a
|
|
1189
|
+
// `String`) is decoded first for the same reason.
|
|
1190
|
+
const container = walkedContainer(inner);
|
|
1191
|
+
if (container === undefined || !container.isUnion)
|
|
1192
|
+
return false;
|
|
1193
|
+
return container.children.some((child, index) => isCanonicalDateMember(child, childOptions(container, descend(opts), index, opts)));
|
|
1194
|
+
};
|
|
1195
|
+
/** Whether a leaf gets a read-leniency substitute (see `legacyReads`). */
|
|
1196
|
+
const needsLegacyRead = (schema, opts) => opts?.legacyReads === true &&
|
|
1197
|
+
opts.strictWireKind !== true &&
|
|
1198
|
+
schemaFieldsOf(schema) === undefined &&
|
|
1199
|
+
!isArraySchema(schema) &&
|
|
1200
|
+
(isDateTransform(schema) || numericTypeWithStringEncoding(schema));
|
|
1201
|
+
/** Whether a plain `Schema.BigInt` leaf gets the read lift (aggregate decode, entity reads). */
|
|
1202
|
+
const needsBigIntLift = (schema, opts) => (opts?.tolerantTransforms === true || opts?.legacyReads === true) &&
|
|
1203
|
+
opts.strictWireKind !== true &&
|
|
1204
|
+
isSelfBigInt(schema);
|
|
597
1205
|
/**
|
|
598
1206
|
* Recursively determine whether a schema contains — at any depth reachable
|
|
599
1207
|
* through nested `Schema.Struct` / `Schema.Class` / `Schema.Array` — a leaf that
|
|
@@ -601,10 +1209,9 @@ const optionalField = (field) => {
|
|
|
601
1209
|
* `RedactedFromValue`). Returns false for schemas that don't, so
|
|
602
1210
|
* {@link substituteSchemaDeep} can return them unchanged (zero structural churn).
|
|
603
1211
|
*
|
|
604
|
-
* Optional
|
|
605
|
-
* `
|
|
606
|
-
*
|
|
607
|
-
* as the pre-existing behavior).
|
|
1212
|
+
* Optional wrappers are unwrapped to their real inner schema, and the walk
|
|
1213
|
+
* enters `Union` members, `Record` values and `Tuple` elements too (see
|
|
1214
|
+
* `walkedContainer`).
|
|
608
1215
|
*/
|
|
609
1216
|
const needsDeepSubstitution = (schema, opts) => {
|
|
610
1217
|
if (schema == null || schema.ast == null)
|
|
@@ -617,12 +1224,7 @@ const needsDeepSubstitution = (schema, opts) => {
|
|
|
617
1224
|
// so the walker cannot see through it and the target's own transformed fields
|
|
618
1225
|
// keep their strict schemas. Only callers that pass `resolveRef` are affected
|
|
619
1226
|
// — the entity derivation passes no options at all (#116).
|
|
620
|
-
const deeper = opts
|
|
621
|
-
? {
|
|
622
|
-
...(opts?.tolerantTransforms ? { tolerantTransforms: true } : {}),
|
|
623
|
-
...(opts?.resolveRef ? { resolveRef: opts.resolveRef } : {}),
|
|
624
|
-
}
|
|
625
|
-
: undefined;
|
|
1227
|
+
const deeper = descend(opts);
|
|
626
1228
|
// Optional wrapper FIRST — unwrap to the REAL inner before the leaf / array /
|
|
627
1229
|
// struct checks. `optionalKey(X)` is a bare AST (Declaration/Arrays) with an
|
|
628
1230
|
// `isOptional` context, so the leaf detectors (which resolve through to the
|
|
@@ -631,7 +1233,10 @@ const needsDeepSubstitution = (schema, opts) => {
|
|
|
631
1233
|
// has the `Arrays` AST but no runtime `.value`.
|
|
632
1234
|
const opt = optionalField(schema);
|
|
633
1235
|
if (opt !== undefined)
|
|
634
|
-
return needsDeepSubstitution(opt.inner,
|
|
1236
|
+
return needsDeepSubstitution(opt.inner, throughOptional(opts));
|
|
1237
|
+
const defaulted = decodingDefaultField(schema);
|
|
1238
|
+
if (defaulted !== undefined)
|
|
1239
|
+
return needsDeepSubstitution(defaulted.inner, opts);
|
|
635
1240
|
if (isSelfDateSchema(schema))
|
|
636
1241
|
return true;
|
|
637
1242
|
if (opts?.tolerantTransforms && isDateTransform(schema))
|
|
@@ -642,6 +1247,12 @@ const needsDeepSubstitution = (schema, opts) => {
|
|
|
642
1247
|
// case: after a mutation the field may hold either form (#116).
|
|
643
1248
|
if (opts?.tolerantTransforms && isLeafEncodingTransform(schema))
|
|
644
1249
|
return true;
|
|
1250
|
+
if (needsLegacyRead(schema, opts) || needsBigIntLift(schema, opts))
|
|
1251
|
+
return true;
|
|
1252
|
+
const container = walkedContainer(schema);
|
|
1253
|
+
if (container !== undefined) {
|
|
1254
|
+
return container.children.some((child, index) => needsDeepSubstitution(child, childOptions(container, deeper, index, opts)));
|
|
1255
|
+
}
|
|
645
1256
|
if (isArraySchema(schema)) {
|
|
646
1257
|
const element = arrayElementOf(schema);
|
|
647
1258
|
return element !== undefined && needsDeepSubstitution(element, deeper);
|
|
@@ -677,20 +1288,16 @@ const needsDeepSubstitution = (schema, opts) => {
|
|
|
677
1288
|
export const substituteSchemaDeep = (schema, opts) => {
|
|
678
1289
|
if (!needsDeepSubstitution(schema, opts))
|
|
679
1290
|
return schema;
|
|
680
|
-
// `tolerantTransforms
|
|
681
|
-
//
|
|
1291
|
+
// `tolerantTransforms`, `resolveRef`, `strictWireKind` and `legacyReads`
|
|
1292
|
+
// propagate through the recursion (`descend`); `skipTopLevel` and the
|
|
1293
|
+
// per-position union options do not.
|
|
682
1294
|
//
|
|
683
1295
|
// `resolveRef` has to descend: a `DynamoModel.ref` field nested INSIDE an edge
|
|
684
1296
|
// model is annotated, and `Schema.annotate` drops a `Schema.Class`'s `.fields`,
|
|
685
1297
|
// so the walker cannot see through it and the target's own transformed fields
|
|
686
1298
|
// keep their strict schemas. Only callers that pass `resolveRef` are affected
|
|
687
1299
|
// — the entity derivation passes no options at all (#116).
|
|
688
|
-
const deeper = opts
|
|
689
|
-
? {
|
|
690
|
-
...(opts?.tolerantTransforms ? { tolerantTransforms: true } : {}),
|
|
691
|
-
...(opts?.resolveRef ? { resolveRef: opts.resolveRef } : {}),
|
|
692
|
-
}
|
|
693
|
-
: undefined;
|
|
1300
|
+
const deeper = descend(opts);
|
|
694
1301
|
// Optional wrapper FIRST — unwrap to the REAL inner, substitute it, then
|
|
695
1302
|
// re-apply the same optionality. Handled BEFORE the leaf / array / struct
|
|
696
1303
|
// checks: the leaf detectors resolve through an `optionalKey` wrapper and would
|
|
@@ -698,16 +1305,26 @@ export const substituteSchemaDeep = (schema, opts) => {
|
|
|
698
1305
|
// the optional key), and an optional array's `.value` lives only on the inner.
|
|
699
1306
|
const opt = optionalField(schema);
|
|
700
1307
|
if (opt !== undefined) {
|
|
701
|
-
return opt.rewrap(substituteSchemaDeep(opt.inner,
|
|
1308
|
+
return opt.rewrap(substituteSchemaDeep(opt.inner, throughOptional(opts)));
|
|
702
1309
|
}
|
|
703
|
-
//
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
1310
|
+
// A decoding default (or any optional-encoded step) on top: substitute the
|
|
1311
|
+
// field beneath it and keep the step, so a missing key still defaults (#133).
|
|
1312
|
+
const defaulted = decodingDefaultField(schema);
|
|
1313
|
+
if (defaulted !== undefined) {
|
|
1314
|
+
return defaulted.rewrap(substituteSchemaDeep(defaulted.inner, opts));
|
|
707
1315
|
}
|
|
1316
|
+
// Leaf: self-date schema → tolerant bidirectional date transform.
|
|
1317
|
+
if (isSelfDateSchema(schema))
|
|
1318
|
+
return selfDateSubstitute(schema, opts);
|
|
708
1319
|
// Leaf: Pattern B transform date schema (only under `tolerantTransforms`) →
|
|
709
1320
|
// tolerant date transform whose decode also accepts the already-domain value.
|
|
1321
|
+
// Inside a union it keeps the transform's OWN decode instead: the union picks
|
|
1322
|
+
// a member by what each one accepts, and the generic date transform accepts
|
|
1323
|
+
// more (any epoch number, any parseable string) than the user's transform —
|
|
1324
|
+
// enough to claim a value that belongs to a later member (#133).
|
|
710
1325
|
if (opts?.tolerantTransforms && isDateTransform(schema)) {
|
|
1326
|
+
if (opts.strictWireKind)
|
|
1327
|
+
return buildTolerantTransform(schema);
|
|
711
1328
|
const encoding = getEncoding(schema) ?? inferDefaultEncoding(schema);
|
|
712
1329
|
if (encoding)
|
|
713
1330
|
return buildDateTransform(encoding);
|
|
@@ -724,12 +1341,38 @@ export const substituteSchemaDeep = (schema, opts) => {
|
|
|
724
1341
|
if (opts?.tolerantTransforms && isLeafEncodingTransform(schema)) {
|
|
725
1342
|
return buildTolerantTransform(schema);
|
|
726
1343
|
}
|
|
1344
|
+
// Leaf: read leniency for legacy domain-form values (entity read schemas) and
|
|
1345
|
+
// the plain-bigint lift (#133).
|
|
1346
|
+
if (needsLegacyRead(schema, opts))
|
|
1347
|
+
return buildLegacyReadTransform(schema);
|
|
1348
|
+
if (needsBigIntLift(schema, opts))
|
|
1349
|
+
return buildBigIntLift(schema);
|
|
1350
|
+
// Union / Record / Tuple / *WithRest: substitute every child and rebuild the
|
|
1351
|
+
// same container kind, metadata included (#133). Without `tolerantTransforms`
|
|
1352
|
+
// (entity derivation) only self-date / Redacted leaves inside are
|
|
1353
|
+
// substituted, exactly as for a Struct or an Array. A union's members each
|
|
1354
|
+
// learn which primitive kinds the others are stored as (`childOptions`).
|
|
1355
|
+
const container = walkedContainer(schema);
|
|
1356
|
+
if (container !== undefined) {
|
|
1357
|
+
const children = container.children.map((child, index) => {
|
|
1358
|
+
const childOpts = childOptions(container, deeper, index, opts);
|
|
1359
|
+
return {
|
|
1360
|
+
substituted: substituteSchemaDeep(child, childOpts),
|
|
1361
|
+
first: container.isUnion && isCanonicalDateMember(child, childOpts),
|
|
1362
|
+
};
|
|
1363
|
+
});
|
|
1364
|
+
return container.rebuild([
|
|
1365
|
+
...children.filter((c) => c.first).map((c) => c.substituted),
|
|
1366
|
+
...children.filter((c) => !c.first).map((c) => c.substituted),
|
|
1367
|
+
]);
|
|
1368
|
+
}
|
|
727
1369
|
// Array: substitute the element schema.
|
|
728
1370
|
if (isArraySchema(schema)) {
|
|
729
1371
|
const element = arrayElementOf(schema);
|
|
730
1372
|
if (element === undefined)
|
|
731
1373
|
return schema;
|
|
732
|
-
|
|
1374
|
+
const rebuilt = Schema.Array(substituteSchemaDeep(element, deeper));
|
|
1375
|
+
return opts?.enforceChecks ? withMetadataOf(schema, rebuilt) : rebuilt;
|
|
733
1376
|
}
|
|
734
1377
|
// Struct / Class: substitute each field, recursing into nested structures.
|
|
735
1378
|
// Ref edge fields are re-pointed via `resolveRef` (re-applying any optionality);
|
|
@@ -747,16 +1390,23 @@ export const substituteSchemaDeep = (schema, opts) => {
|
|
|
747
1390
|
const refTarget = opts?.resolveRef?.(name, field);
|
|
748
1391
|
if (refTarget !== undefined) {
|
|
749
1392
|
const fieldOpt = optionalField(field);
|
|
750
|
-
const sub = substituteSchemaDeep(refTarget, deeper);
|
|
1393
|
+
const sub = substituteSchemaDeep(refTarget, atField(deeper, opts, name));
|
|
751
1394
|
subFields[name] = fieldOpt ? fieldOpt.rewrap(sub) : sub;
|
|
752
1395
|
}
|
|
753
1396
|
else {
|
|
754
|
-
subFields[name] = substituteSchemaDeep(field, deeper);
|
|
1397
|
+
subFields[name] = substituteSchemaDeep(field, atField(deeper, opts, name));
|
|
755
1398
|
}
|
|
756
1399
|
}
|
|
757
|
-
const
|
|
758
|
-
if (!isClassSchema(schema))
|
|
759
|
-
return
|
|
1400
|
+
const plainStruct = Schema.Struct(subFields);
|
|
1401
|
+
if (!isClassSchema(schema)) {
|
|
1402
|
+
return opts?.enforceChecks ? withMetadataOf(schema, plainStruct) : plainStruct;
|
|
1403
|
+
}
|
|
1404
|
+
// A class built over a checked Struct (`Schema.Class("C")(Struct(…).check(f))`)
|
|
1405
|
+
// keeps that check on the Struct its encoding leads to.
|
|
1406
|
+
const classStruct = classStructAst(schema);
|
|
1407
|
+
const subStruct = (opts?.enforceChecks && classStruct !== undefined
|
|
1408
|
+
? withAstMetadataOf(classStruct, plainStruct)
|
|
1409
|
+
: plainStruct);
|
|
760
1410
|
// Preserve the class instance: decode the substituted struct to the original
|
|
761
1411
|
// class via prototype attach (no constructor re-validation, no field re-decode).
|
|
762
1412
|
const ctor = schema;
|
|
@@ -792,8 +1442,12 @@ export const substituteSchemaDeep = (schema, opts) => {
|
|
|
792
1442
|
*
|
|
793
1443
|
* @internal
|
|
794
1444
|
*/
|
|
795
|
-
export const substituteSchemas = (modelFields, fieldEncodings) => {
|
|
1445
|
+
export const substituteSchemas = (modelFields, fieldEncodings, options) => {
|
|
796
1446
|
const out = {};
|
|
1447
|
+
const deep = {
|
|
1448
|
+
...(options?.legacyReads ? { legacyReads: true } : {}),
|
|
1449
|
+
...(options?.enforceChecks ? { enforceChecks: true } : {}),
|
|
1450
|
+
};
|
|
797
1451
|
for (const [name, schema] of Object.entries(modelFields)) {
|
|
798
1452
|
// 1. Self-date substitution. Pattern A: user declared `Schema.DateTimeUtc`
|
|
799
1453
|
// and chose a wire format via annotation. We substitute with the
|
|
@@ -806,6 +1460,15 @@ export const substituteSchemas = (modelFields, fieldEncodings) => {
|
|
|
806
1460
|
continue;
|
|
807
1461
|
}
|
|
808
1462
|
}
|
|
1463
|
+
// …also beneath a decoding default, which is kept on top (#133).
|
|
1464
|
+
const defaulted = decodingDefaultField(schema);
|
|
1465
|
+
if (defaulted !== undefined && isSelfDateSchema(defaulted.inner)) {
|
|
1466
|
+
const enc = fieldEncodings[name];
|
|
1467
|
+
if (enc) {
|
|
1468
|
+
out[name] = defaulted.rewrap(buildDateTransform(enc));
|
|
1469
|
+
continue;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
809
1472
|
// 2. RedactedFromValue substitution. Effect v4's `RedactedFromValue`
|
|
810
1473
|
// forbids encoding by design; substitute with a tolerant Redacted
|
|
811
1474
|
// transform so the round-trip works for storage.
|
|
@@ -821,10 +1484,44 @@ export const substituteSchemas = (modelFields, fieldEncodings) => {
|
|
|
821
1484
|
// `DynamoModel.ref` target) may itself carry a self-date or Redacted leaf
|
|
822
1485
|
// — `substituteSchemaDeep` substitutes those while preserving class
|
|
823
1486
|
// identity, and returns the schema unchanged when nothing nested needs it.
|
|
824
|
-
|
|
1487
|
+
// A configured storage override on a UNION field (`NullOr(DateTimeUtc)`)
|
|
1488
|
+
// is applied to the union's self-date member (#133).
|
|
1489
|
+
const unionOverride = unionDateOverride(name, schema, fieldEncodings[name]);
|
|
1490
|
+
out[name] = substituteSchemaDeep(schema, {
|
|
1491
|
+
...deep,
|
|
1492
|
+
path: [name],
|
|
1493
|
+
...(unionOverride === undefined ? {} : { unionDateOverride: unionOverride }),
|
|
1494
|
+
});
|
|
825
1495
|
}
|
|
826
1496
|
return out;
|
|
827
1497
|
};
|
|
1498
|
+
/**
|
|
1499
|
+
* The storage a `DynamoModel.configure(..., { field: { storedAs } })` override
|
|
1500
|
+
* gives a top-level UNION field's date member, or `undefined` when the field is
|
|
1501
|
+
* not a union with a self-date member (#133).
|
|
1502
|
+
*
|
|
1503
|
+
* The override names one storage for the field; a union holding several self
|
|
1504
|
+
* dates gives it no single member to apply to, so that is refused at
|
|
1505
|
+
* `Entity.make()` rather than guessed (EDD-9057).
|
|
1506
|
+
*/
|
|
1507
|
+
const unionDateOverride = (name, schema, encoding) => {
|
|
1508
|
+
if (encoding === undefined)
|
|
1509
|
+
return undefined;
|
|
1510
|
+
const inner = optionalField(schema)?.inner ?? schema;
|
|
1511
|
+
if (!SchemaAST.isUnion(inner.ast))
|
|
1512
|
+
return undefined;
|
|
1513
|
+
const members = inner.members;
|
|
1514
|
+
const dateMembers = (members ?? []).filter((member) => isSelfDateSchema(optionalField(member)?.inner ?? member));
|
|
1515
|
+
if (dateMembers.length === 0)
|
|
1516
|
+
return undefined;
|
|
1517
|
+
if (dateMembers.length > 1) {
|
|
1518
|
+
throw new Error(`[EDD-9057] Field "${name}" is a union with ${dateMembers.length} date members, and its ` +
|
|
1519
|
+
`configured storage override (DynamoModel.configure … storedAs) cannot say which one it ` +
|
|
1520
|
+
`applies to. Annotate the intended member directly with ` +
|
|
1521
|
+
`\`.pipe(DynamoModel.storedAs(...))\` and drop the field-level override.`);
|
|
1522
|
+
}
|
|
1523
|
+
return encoding;
|
|
1524
|
+
};
|
|
828
1525
|
/**
|
|
829
1526
|
* Validate that no model field combines a transform schema with a storage
|
|
830
1527
|
* override. The two configuration paths are mutually exclusive: either declare
|
|
@@ -896,7 +1593,14 @@ export const buildDerivedSchemas = (modelFields, indexes, systemFields, resolved
|
|
|
896
1593
|
// fields are replaced with a custom bidirectional transform so encoding
|
|
897
1594
|
// round-trips. All other transform schemas are passed through — the user's
|
|
898
1595
|
// transform IS the wire format and we never override it.
|
|
899
|
-
|
|
1596
|
+
// WRITE schemas (input / create / update / key) enforce the container checks
|
|
1597
|
+
// the substitution rebuilds; the READ schemas below do not (#133).
|
|
1598
|
+
const fields = substituteSchemas(modelFields, fieldEncodings, { enforceChecks: true });
|
|
1599
|
+
// READ schemas (model / record / item / deleted / history decode) also take
|
|
1600
|
+
// the domain-form values earlier releases left on transform fields (#133).
|
|
1601
|
+
// Their encode is the strict one, and keys are composed from `inputSchema`,
|
|
1602
|
+
// so neither writes nor keys see a difference.
|
|
1603
|
+
const readFields = substituteSchemas(modelFields, fieldEncodings, { legacyReads: true });
|
|
900
1604
|
// Ref fields carry the `DynamoModel.ref` annotation (an opaque Declaration that
|
|
901
1605
|
// hides the target's fields), so `substituteSchemaDeep` above can't see into
|
|
902
1606
|
// them. Re-point each denormalized ref field at its target model, recursively
|
|
@@ -910,10 +1614,11 @@ export const buildDerivedSchemas = (modelFields, indexes, systemFields, resolved
|
|
|
910
1614
|
const resolved = isConfiguredModel(targetModel)
|
|
911
1615
|
? targetModel.model
|
|
912
1616
|
: targetModel;
|
|
913
|
-
fields[ref.fieldName] = substituteSchemaDeep(resolved);
|
|
1617
|
+
fields[ref.fieldName] = substituteSchemaDeep(resolved, { enforceChecks: true });
|
|
1618
|
+
readFields[ref.fieldName] = substituteSchemaDeep(resolved, { legacyReads: true });
|
|
914
1619
|
}
|
|
915
|
-
// --- Model Schema: pure model fields (
|
|
916
|
-
const modelSchema = Schema.Struct(
|
|
1620
|
+
// --- Model Schema: pure model fields (decode of stored items in "model" mode) ---
|
|
1621
|
+
const modelSchema = Schema.Struct(readFields);
|
|
917
1622
|
// --- Record Schema: model + system fields ---
|
|
918
1623
|
// System timestamp fields use Schema.DateTimeUtcFromString as the canonical
|
|
919
1624
|
// wire ↔ domain transform when they're library-managed. When the field
|
|
@@ -931,10 +1636,12 @@ export const buildDerivedSchemas = (modelFields, indexes, systemFields, resolved
|
|
|
931
1636
|
: Schema.DateTimeUtcFromString;
|
|
932
1637
|
}
|
|
933
1638
|
if (systemFields.version && !systemFields.versionCollision) {
|
|
934
|
-
|
|
1639
|
+
// An item written before the entity was `versioned` has no version: it
|
|
1640
|
+
// reads as version 0 — real versions start at 1 (#133).
|
|
1641
|
+
systemSchemaFields[systemFields.version] = Schema.Number.pipe(Schema.withDecodingDefaultKey(Effect.succeed(0)));
|
|
935
1642
|
}
|
|
936
1643
|
const recordSchema = Schema.Struct({
|
|
937
|
-
...
|
|
1644
|
+
...readFields,
|
|
938
1645
|
...systemSchemaFields,
|
|
939
1646
|
});
|
|
940
1647
|
// --- System-collision handling for input/update schemas ---
|
|
@@ -1067,14 +1774,14 @@ export const buildDerivedSchemas = (modelFields, indexes, systemFields, resolved
|
|
|
1067
1774
|
: Schema.optional(Schema.String);
|
|
1068
1775
|
}
|
|
1069
1776
|
const itemSchema = Schema.Struct({
|
|
1070
|
-
...
|
|
1777
|
+
...readFields,
|
|
1071
1778
|
...systemSchemaFields,
|
|
1072
1779
|
...keyAttrFields,
|
|
1073
1780
|
__edd_e__: Schema.String,
|
|
1074
1781
|
});
|
|
1075
1782
|
// --- Deleted Record Schema: record + deletedAt ---
|
|
1076
1783
|
const deletedRecordSchema = Schema.Struct({
|
|
1077
|
-
...
|
|
1784
|
+
...readFields,
|
|
1078
1785
|
...systemSchemaFields,
|
|
1079
1786
|
deletedAt: Schema.String,
|
|
1080
1787
|
});
|
|
@@ -1088,11 +1795,11 @@ export const buildDerivedSchemas = (modelFields, indexes, systemFields, resolved
|
|
|
1088
1795
|
}
|
|
1089
1796
|
const hasHiddenFields = hiddenFieldNames.size > 0;
|
|
1090
1797
|
const modelVisibleSchema = hasHiddenFields
|
|
1091
|
-
? Schema.Struct(Object.fromEntries(Object.entries(
|
|
1798
|
+
? Schema.Struct(Object.fromEntries(Object.entries(readFields).filter(([k]) => !hiddenFieldNames.has(k))))
|
|
1092
1799
|
: modelSchema;
|
|
1093
1800
|
const recordVisibleSchema = hasHiddenFields
|
|
1094
1801
|
? Schema.Struct(Object.fromEntries(Object.entries({
|
|
1095
|
-
...
|
|
1802
|
+
...readFields,
|
|
1096
1803
|
...systemSchemaFields,
|
|
1097
1804
|
}).filter(([k]) => !hiddenFieldNames.has(k))))
|
|
1098
1805
|
: recordSchema;
|
|
@@ -1113,7 +1820,7 @@ export const buildDerivedSchemas = (modelFields, indexes, systemFields, resolved
|
|
|
1113
1820
|
// Compute encodings for the appendInput fields (annotation +
|
|
1114
1821
|
// inferred from typeConstructor for self schemas).
|
|
1115
1822
|
const appendEncodings = buildFieldEncodings(userFields, {});
|
|
1116
|
-
const subbedFields = substituteSchemas(userFields, appendEncodings);
|
|
1823
|
+
const subbedFields = substituteSchemas(userFields, appendEncodings, { enforceChecks: true });
|
|
1117
1824
|
return Schema.Struct(subbedFields);
|
|
1118
1825
|
})()
|
|
1119
1826
|
: null;
|
|
@@ -1122,11 +1829,12 @@ export const buildDerivedSchemas = (modelFields, indexes, systemFields, resolved
|
|
|
1122
1829
|
const hiddenFilter = (record) => hasHiddenFields
|
|
1123
1830
|
? Object.fromEntries(Object.entries(record).filter(([k]) => !hiddenFieldNames.has(k)))
|
|
1124
1831
|
: record;
|
|
1125
|
-
return Schema.Struct(hiddenFilter({ ...
|
|
1832
|
+
return Schema.Struct(hiddenFilter({ ...readFields }));
|
|
1126
1833
|
})()
|
|
1127
1834
|
: null;
|
|
1128
1835
|
return {
|
|
1129
1836
|
modelSchema: modelVisibleSchema,
|
|
1837
|
+
writeModelSchema: Schema.Struct(fields),
|
|
1130
1838
|
recordSchema: recordVisibleSchema,
|
|
1131
1839
|
inputSchema: inputSchema,
|
|
1132
1840
|
createSchema: createSchema,
|
|
@@ -1142,4 +1850,367 @@ export const buildDerivedSchemas = (modelFields, indexes, systemFields, resolved
|
|
|
1142
1850
|
// Unique constraint field resolver
|
|
1143
1851
|
// ---------------------------------------------------------------------------
|
|
1144
1852
|
export const resolveUniqueFields = (def) => Array.isArray(def) ? def : def.fields;
|
|
1853
|
+
// ---------------------------------------------------------------------------
|
|
1854
|
+
// Path-addressed values (#133)
|
|
1855
|
+
// ---------------------------------------------------------------------------
|
|
1856
|
+
/** The schema one path segment addresses inside `schema`, or `undefined`. */
|
|
1857
|
+
const childAtSegment = (schema, segment) => {
|
|
1858
|
+
const inner = optionalField(schema)?.inner ?? schema;
|
|
1859
|
+
const s = inner;
|
|
1860
|
+
const fields = schemaFieldsOf(inner);
|
|
1861
|
+
if (fields !== undefined)
|
|
1862
|
+
return typeof segment === "string" ? fields[segment] : undefined;
|
|
1863
|
+
// An OPAQUE class — `X.check(…)`, `X.annotate(…)`, a `DynamoModel.ref` nested
|
|
1864
|
+
// in a ref target — is deliberately NOT followed. Its `.fields` are gone, so
|
|
1865
|
+
// the derived read schema keeps it unsubstituted and decodes its leaves
|
|
1866
|
+
// exactly as `put` stores them; encoding a value under it into wire form
|
|
1867
|
+
// would leave the item unreadable (#133). A path into it is passed through
|
|
1868
|
+
// as given. A top-level `DynamoModel.ref` field is followed through its
|
|
1869
|
+
// target model instead (`makePathValueEncoder`'s `refTargets`), whose read
|
|
1870
|
+
// schema IS substituted.
|
|
1871
|
+
if (SchemaAST.isUnion(inner.ast)) {
|
|
1872
|
+
// `NullOr(Stamp)` and friends: the first member the segment resolves in.
|
|
1873
|
+
const members = Array.isArray(s.members) ? s.members : [];
|
|
1874
|
+
for (const member of members) {
|
|
1875
|
+
const child = childAtSegment(member, segment);
|
|
1876
|
+
if (child !== undefined)
|
|
1877
|
+
return child;
|
|
1878
|
+
}
|
|
1879
|
+
return undefined;
|
|
1880
|
+
}
|
|
1881
|
+
if (SchemaAST.isArrays(inner.ast)) {
|
|
1882
|
+
if (typeof segment !== "number")
|
|
1883
|
+
return undefined;
|
|
1884
|
+
if (isSchemaLike(s.value))
|
|
1885
|
+
return s.value;
|
|
1886
|
+
if (Array.isArray(s.elements))
|
|
1887
|
+
return s.elements[segment];
|
|
1888
|
+
if (isSchemaLike(s.schema) && Array.isArray(s.rest)) {
|
|
1889
|
+
const head = s.schema
|
|
1890
|
+
.elements;
|
|
1891
|
+
if (head !== undefined && segment < head.length)
|
|
1892
|
+
return head[segment];
|
|
1893
|
+
return s.rest[0];
|
|
1894
|
+
}
|
|
1895
|
+
return undefined;
|
|
1896
|
+
}
|
|
1897
|
+
if (SchemaAST.isObjects(inner.ast) && typeof segment === "string") {
|
|
1898
|
+
if (isSchemaLike(s.value) && isSchemaLike(s.key))
|
|
1899
|
+
return s.value;
|
|
1900
|
+
if (isSchemaLike(s.schema) && Array.isArray(s.records)) {
|
|
1901
|
+
const declared = schemaFieldsOf(s.schema)?.[segment];
|
|
1902
|
+
if (declared !== undefined)
|
|
1903
|
+
return declared;
|
|
1904
|
+
const record = s.records[0];
|
|
1905
|
+
return record?.value;
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
return undefined;
|
|
1909
|
+
};
|
|
1910
|
+
/** The element schema of the list a path addresses (for append / prepend). */
|
|
1911
|
+
const listElementOf = (schema) => {
|
|
1912
|
+
const inner = optionalField(schema)?.inner ?? schema;
|
|
1913
|
+
if (!SchemaAST.isArrays(inner.ast))
|
|
1914
|
+
return undefined;
|
|
1915
|
+
const value = inner.value;
|
|
1916
|
+
if (isSchemaLike(value))
|
|
1917
|
+
return value;
|
|
1918
|
+
// A rebuilt array with its checks restored has no runtime `.value`; a plain
|
|
1919
|
+
// list (no fixed elements) still has its element in the AST's rest.
|
|
1920
|
+
return inner.ast.elements.length === 0 ? arrayElementOf(inner) : undefined;
|
|
1921
|
+
};
|
|
1922
|
+
/** Structural equality over plain values (primitives, arrays, plain objects). */
|
|
1923
|
+
const sameValue = (a, b) => {
|
|
1924
|
+
if (Object.is(a, b))
|
|
1925
|
+
return true;
|
|
1926
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
1927
|
+
return a.length === b.length && a.every((entry, i) => sameValue(entry, b[i]));
|
|
1928
|
+
}
|
|
1929
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
1930
|
+
const keys = Object.keys(a);
|
|
1931
|
+
return keys.length === Object.keys(b).length && keys.every((k) => sameValue(a[k], b[k]));
|
|
1932
|
+
}
|
|
1933
|
+
return false;
|
|
1934
|
+
};
|
|
1935
|
+
/**
|
|
1936
|
+
* Whether a path value is ALREADY in the wire form of `original`, in a way
|
|
1937
|
+
* encoding cannot tell apart from a domain value: it decodes as wire, it
|
|
1938
|
+
* validates as the Type side, and encoding it would change it. `StringFromBase64`
|
|
1939
|
+
* given `"aGk="`, or `fromJsonString(Unknown)` given `'{"a":2}'`, are both
|
|
1940
|
+
* valid domain strings and valid wire strings; encoding would double-encode.
|
|
1941
|
+
* Before path values were encoded, a wire value was the only way to round-trip
|
|
1942
|
+
* such a field, so it is passed through as given (#133).
|
|
1943
|
+
*
|
|
1944
|
+
* Only a LEAF transform with a primitive wire form qualifies. A container — a
|
|
1945
|
+
* class, struct, array, record, tuple or union — is always encoded: encoding a
|
|
1946
|
+
* `Schema.Class` yields a plain object, so it would always look "changed", and
|
|
1947
|
+
* passing it through whole would marshal its `Redacted` / `Date` / `DateTime`
|
|
1948
|
+
* leaves as maps and lose them. A self date never qualifies either: its domain
|
|
1949
|
+
* `DateTime` is "its own encoded form" to Effect, but the library stores it as
|
|
1950
|
+
* a primitive.
|
|
1951
|
+
*/
|
|
1952
|
+
const makeAmbiguityCheck = (original) => {
|
|
1953
|
+
if (!isLeafEncodingTransform(original))
|
|
1954
|
+
return () => false;
|
|
1955
|
+
// "Already wire" means the value DECODES as wire — a plain string that is not
|
|
1956
|
+
// valid base64 is a domain value for `StringFromBase64`, and is encoded.
|
|
1957
|
+
const decodesAsWire = Schema.decodeUnknownOption(original);
|
|
1958
|
+
const isType = Schema.is(Schema.make(SchemaAST.toType(original.ast)));
|
|
1959
|
+
const encode = Schema.encodeUnknownOption(original);
|
|
1960
|
+
return (value) => {
|
|
1961
|
+
if (decodesAsWire(value)._tag === "None" || !isType(value))
|
|
1962
|
+
return false;
|
|
1963
|
+
const encoded = encode(value);
|
|
1964
|
+
return encoded._tag === "Some" && !sameValue(encoded.value, value);
|
|
1965
|
+
};
|
|
1966
|
+
};
|
|
1967
|
+
/**
|
|
1968
|
+
* A path value as DynamoDB will store it: the marshaller drops `undefined`
|
|
1969
|
+
* object entries (`removeUndefinedValues`), so `{ opt: undefined }` is stored as
|
|
1970
|
+
* `{}` and must be validated as such — an `optionalKey` rejects a present
|
|
1971
|
+
* `undefined` that never reaches the table.
|
|
1972
|
+
*/
|
|
1973
|
+
const asStored = (value) => {
|
|
1974
|
+
if (Array.isArray(value))
|
|
1975
|
+
return value.map(asStored);
|
|
1976
|
+
if (!isPlainObject(value))
|
|
1977
|
+
return value;
|
|
1978
|
+
const out = {};
|
|
1979
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1980
|
+
if (entry !== undefined)
|
|
1981
|
+
out[key] = asStored(entry);
|
|
1982
|
+
}
|
|
1983
|
+
return out;
|
|
1984
|
+
};
|
|
1985
|
+
/** A plain object or class instance whose fields a path value can be walked by. */
|
|
1986
|
+
const isWalkableObject = (value) => value !== null &&
|
|
1987
|
+
typeof value === "object" &&
|
|
1988
|
+
!Array.isArray(value) &&
|
|
1989
|
+
!(value instanceof Date) &&
|
|
1990
|
+
!DateTime.isDateTime(value) &&
|
|
1991
|
+
!Redacted.isRedacted(value);
|
|
1992
|
+
/**
|
|
1993
|
+
* Encoders for the VALUES of path-based updates (`pathSet`, `pathAppend`,
|
|
1994
|
+
* `pathPrepend`, `pathIfNotExists`) and record-based `append` (#133).
|
|
1995
|
+
*
|
|
1996
|
+
* Those values were marshalled as given, never encoded, so a `DateTime` set at
|
|
1997
|
+
* `["days", 0]` — or appended to a list of dates — was stored as a marshalled
|
|
1998
|
+
* `{ epochMilliseconds, … }` map, and a `NumberFromString` as a number. The
|
|
1999
|
+
* schema the path addresses is resolved from the model and substituted exactly
|
|
2000
|
+
* as the record schema substitutes it (a top-level field uses the record
|
|
2001
|
+
* schema's own, so a configured `storedAs` applies), and the value is encoded
|
|
2002
|
+
* through it.
|
|
2003
|
+
*
|
|
2004
|
+
* Passed through unchanged instead:
|
|
2005
|
+
* - a value already in an ambiguous wire form (see `makeAmbiguityCheck`);
|
|
2006
|
+
* - a value that does not encode — already wire-shaped, or a path the schema
|
|
2007
|
+
* cannot follow (an opaque `DynamoModel.ref`, a dynamic key).
|
|
2008
|
+
*
|
|
2009
|
+
* @internal
|
|
2010
|
+
*/
|
|
2011
|
+
export const makePathValueEncoder = (modelFields, writeFields,
|
|
2012
|
+
/**
|
|
2013
|
+
* The model each `DynamoModel.ref` field is denormalised from, keyed by
|
|
2014
|
+
* field name. A ref field's own schema is opaque (the annotation drops its
|
|
2015
|
+
* `.fields`), so a path under it is followed through the target model.
|
|
2016
|
+
*/
|
|
2017
|
+
refTargets = {}) => {
|
|
2018
|
+
const recordFields = writeFields;
|
|
2019
|
+
const cache = new Map();
|
|
2020
|
+
const encoderFor = (key, resolve) => {
|
|
2021
|
+
const cached = cache.get(key);
|
|
2022
|
+
if (cached !== undefined)
|
|
2023
|
+
return cached;
|
|
2024
|
+
const resolved = resolve();
|
|
2025
|
+
const fn = resolved === undefined ? null : leafAwareEncoder(resolved.original, resolved.stored);
|
|
2026
|
+
cache.set(key, fn);
|
|
2027
|
+
return fn;
|
|
2028
|
+
};
|
|
2029
|
+
/**
|
|
2030
|
+
* Encode through `stored`, passing an ambiguous wire value of a LEAF transform
|
|
2031
|
+
* through. An array of such leaves (`Array(StringFromBase64)`) is the one
|
|
2032
|
+
* container handled element by element, so a list of wire strings is not
|
|
2033
|
+
* double-encoded either; every other container is encoded whole.
|
|
2034
|
+
*/
|
|
2035
|
+
const leafAwareEncoder = (originalSchema, stored) => {
|
|
2036
|
+
const original = optionalField(originalSchema)?.inner ?? originalSchema;
|
|
2037
|
+
const originalElement = listElementOf(original);
|
|
2038
|
+
const storedElement = listElementOf(stored);
|
|
2039
|
+
if (originalElement !== undefined &&
|
|
2040
|
+
storedElement !== undefined &&
|
|
2041
|
+
isLeafEncodingTransform(optionalField(originalElement)?.inner ?? originalElement)) {
|
|
2042
|
+
const element = leafAwareEncoder(originalElement, storedElement);
|
|
2043
|
+
return (value) => (Array.isArray(value) ? value.map((entry) => element(entry)) : value);
|
|
2044
|
+
}
|
|
2045
|
+
const encode = Schema.encodeUnknownOption(stored);
|
|
2046
|
+
const decode = Schema.decodeUnknownOption(stored);
|
|
2047
|
+
const ambiguous = makeAmbiguityCheck(original);
|
|
2048
|
+
return (value) => {
|
|
2049
|
+
if (ambiguous(value))
|
|
2050
|
+
return value;
|
|
2051
|
+
// A container holding an ambiguous wire leaf (`{ b64: "aGk=", at }`) is
|
|
2052
|
+
// encoded part by part, so that leaf follows the leaf rule instead of
|
|
2053
|
+
// being encoded a second time by the whole-value encode.
|
|
2054
|
+
if (holdsAmbiguousLeaf(original, value))
|
|
2055
|
+
return encodeByParts(original, value);
|
|
2056
|
+
const encoded = encode(value);
|
|
2057
|
+
if (encoded._tag === "Some")
|
|
2058
|
+
return encoded.value;
|
|
2059
|
+
// `decode -> encode`, as `.set()` / `put` do: a plain object given for a
|
|
2060
|
+
// class-typed field, or a value already in wire form, round-trips to its
|
|
2061
|
+
// wire form instead of being marshalled as given.
|
|
2062
|
+
const decoded = decode(value);
|
|
2063
|
+
if (decoded._tag === "Some") {
|
|
2064
|
+
const reencoded = encode(decoded.value);
|
|
2065
|
+
if (reencoded._tag === "Some")
|
|
2066
|
+
return reencoded.value;
|
|
2067
|
+
}
|
|
2068
|
+
return encodeByParts(original, value);
|
|
2069
|
+
};
|
|
2070
|
+
};
|
|
2071
|
+
const ambiguityChecks = new WeakMap();
|
|
2072
|
+
const isAmbiguousAt = (schema, value) => {
|
|
2073
|
+
let check = ambiguityChecks.get(schema);
|
|
2074
|
+
if (check === undefined) {
|
|
2075
|
+
check = makeAmbiguityCheck(optionalField(schema)?.inner ?? schema);
|
|
2076
|
+
ambiguityChecks.set(schema, check);
|
|
2077
|
+
}
|
|
2078
|
+
return check(value);
|
|
2079
|
+
};
|
|
2080
|
+
/**
|
|
2081
|
+
* Whether a container value holds, at any depth, a leaf in ambiguous wire
|
|
2082
|
+
* form. Only arrays and PLAIN objects are looked into: a class instance was
|
|
2083
|
+
* built from domain values by its constructor, so its leaves are domain and
|
|
2084
|
+
* the whole instance is encoded.
|
|
2085
|
+
*/
|
|
2086
|
+
const holdsAmbiguousLeaf = (schema, value) => {
|
|
2087
|
+
const parts = Array.isArray(value)
|
|
2088
|
+
? value.map((entry, index) => [index, entry])
|
|
2089
|
+
: isPlainObject(value)
|
|
2090
|
+
? Object.entries(value)
|
|
2091
|
+
: [];
|
|
2092
|
+
return parts.some(([segment, entry]) => {
|
|
2093
|
+
const child = childAtSegment(schema, segment);
|
|
2094
|
+
return (child !== undefined && (isAmbiguousAt(child, entry) || holdsAmbiguousLeaf(child, entry)));
|
|
2095
|
+
});
|
|
2096
|
+
};
|
|
2097
|
+
/** `leafAwareEncoder` for a schema nested in a path value, memoised per schema. */
|
|
2098
|
+
const partEncoders = new WeakMap();
|
|
2099
|
+
const partEncoder = (schema) => {
|
|
2100
|
+
const cached = partEncoders.get(schema);
|
|
2101
|
+
if (cached !== undefined)
|
|
2102
|
+
return cached;
|
|
2103
|
+
const fn = leafAwareEncoder(schema, substituteSchemaDeep(schema, { enforceChecks: true }));
|
|
2104
|
+
partEncoders.set(schema, fn);
|
|
2105
|
+
return fn;
|
|
2106
|
+
};
|
|
2107
|
+
/**
|
|
2108
|
+
* The last resort for a container value that neither encodes nor
|
|
2109
|
+
* decode→encodes as a whole — one that MIXES wire and domain leaves, such as
|
|
2110
|
+
* `{ b64: "hi", at: DateTime, n: "5" }` (`n` is already wire, `b64` is not
|
|
2111
|
+
* valid base64). Each part is put into its stored form on its own, by the
|
|
2112
|
+
* schema at its position, with the same leaf rules; storing the value raw
|
|
2113
|
+
* would marshal its `DateTime` / `Date` / `Redacted` leaves as maps (#133).
|
|
2114
|
+
*
|
|
2115
|
+
* Arrays and tuples go element by element, structs, classes and records key
|
|
2116
|
+
* by key. A part the schema has no position for, and a value that is neither
|
|
2117
|
+
* an array nor a plain object or class instance, are kept as given.
|
|
2118
|
+
*/
|
|
2119
|
+
const encodeByParts = (schema, value) => {
|
|
2120
|
+
if (Array.isArray(value)) {
|
|
2121
|
+
return value.map((entry, index) => {
|
|
2122
|
+
const child = childAtSegment(schema, index);
|
|
2123
|
+
return child === undefined ? entry : partEncoder(child)(entry);
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
if (!isWalkableObject(value))
|
|
2127
|
+
return value;
|
|
2128
|
+
const out = {};
|
|
2129
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
2130
|
+
const child = childAtSegment(schema, key);
|
|
2131
|
+
out[key] = child === undefined ? entry : partEncoder(child)(entry);
|
|
2132
|
+
}
|
|
2133
|
+
return out;
|
|
2134
|
+
};
|
|
2135
|
+
/** The model's own schema at a path, before any substitution. */
|
|
2136
|
+
const originalAt = (segments) => {
|
|
2137
|
+
const [head, ...rest] = segments;
|
|
2138
|
+
if (typeof head !== "string")
|
|
2139
|
+
return undefined;
|
|
2140
|
+
let current = refTargets[head] ?? modelFields[head];
|
|
2141
|
+
for (const segment of rest) {
|
|
2142
|
+
if (current === undefined)
|
|
2143
|
+
return undefined;
|
|
2144
|
+
current = childAtSegment(current, segment);
|
|
2145
|
+
}
|
|
2146
|
+
return current;
|
|
2147
|
+
};
|
|
2148
|
+
const schemaAt = (segments) => {
|
|
2149
|
+
const original = originalAt(segments);
|
|
2150
|
+
if (original === undefined)
|
|
2151
|
+
return undefined;
|
|
2152
|
+
const head = segments[0];
|
|
2153
|
+
const stored = segments.length === 1
|
|
2154
|
+
? recordFields[head]
|
|
2155
|
+
: substituteSchemaDeep(original, { enforceChecks: true });
|
|
2156
|
+
return stored === undefined ? undefined : { original, stored };
|
|
2157
|
+
};
|
|
2158
|
+
const validators = new Map();
|
|
2159
|
+
const validatorFor = (key, resolve) => {
|
|
2160
|
+
const cached = validators.get(key);
|
|
2161
|
+
if (cached !== undefined)
|
|
2162
|
+
return cached;
|
|
2163
|
+
const schema = resolve();
|
|
2164
|
+
const fn = schema === undefined
|
|
2165
|
+
? null
|
|
2166
|
+
: (() => {
|
|
2167
|
+
const decode = Schema.decodeUnknownExit(schema);
|
|
2168
|
+
return (encoded) => {
|
|
2169
|
+
const exit = decode(asStored(encoded));
|
|
2170
|
+
return exit._tag === "Success" ? undefined : exit.cause;
|
|
2171
|
+
};
|
|
2172
|
+
})();
|
|
2173
|
+
validators.set(key, fn);
|
|
2174
|
+
return fn;
|
|
2175
|
+
};
|
|
2176
|
+
return {
|
|
2177
|
+
validate: (segments, encoded) => {
|
|
2178
|
+
const fn = validatorFor(`v:${JSON.stringify(segments)}`, () => schemaAt(segments)?.stored);
|
|
2179
|
+
return fn === null ? undefined : fn(encoded);
|
|
2180
|
+
},
|
|
2181
|
+
validateElements: (segments, encoded) => {
|
|
2182
|
+
if (!Array.isArray(encoded))
|
|
2183
|
+
return undefined;
|
|
2184
|
+
const fn = validatorFor(`e:${JSON.stringify(segments)}`, () => {
|
|
2185
|
+
const list = schemaAt(segments);
|
|
2186
|
+
return list === undefined ? undefined : listElementOf(list.stored);
|
|
2187
|
+
});
|
|
2188
|
+
if (fn === null)
|
|
2189
|
+
return undefined;
|
|
2190
|
+
for (const entry of encoded) {
|
|
2191
|
+
const issue = fn(entry);
|
|
2192
|
+
if (issue !== undefined)
|
|
2193
|
+
return issue;
|
|
2194
|
+
}
|
|
2195
|
+
return undefined;
|
|
2196
|
+
},
|
|
2197
|
+
value: (segments, value) => {
|
|
2198
|
+
const fn = encoderFor(`v:${JSON.stringify(segments)}`, () => schemaAt(segments));
|
|
2199
|
+
return fn === null ? value : fn(value);
|
|
2200
|
+
},
|
|
2201
|
+
elements: (segments, value) => {
|
|
2202
|
+
if (!Array.isArray(value))
|
|
2203
|
+
return value;
|
|
2204
|
+
const fn = encoderFor(`e:${JSON.stringify(segments)}`, () => {
|
|
2205
|
+
const list = schemaAt(segments);
|
|
2206
|
+
if (list === undefined)
|
|
2207
|
+
return undefined;
|
|
2208
|
+
const original = listElementOf(list.original);
|
|
2209
|
+
const stored = listElementOf(list.stored);
|
|
2210
|
+
return original === undefined || stored === undefined ? undefined : { original, stored };
|
|
2211
|
+
});
|
|
2212
|
+
return fn === null ? value : value.map((entry) => fn(entry));
|
|
2213
|
+
},
|
|
2214
|
+
};
|
|
2215
|
+
};
|
|
1145
2216
|
//# sourceMappingURL=EntitySchemas.js.map
|