effect-dynamodb 1.8.2 → 1.9.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.
Files changed (107) hide show
  1. package/dist/Aggregate.d.ts +9 -9
  2. package/dist/Aggregate.d.ts.map +1 -1
  3. package/dist/Aggregate.js +9 -9
  4. package/dist/Aggregate.js.map +1 -1
  5. package/dist/Batch.d.ts +1 -1
  6. package/dist/Batch.d.ts.map +1 -1
  7. package/dist/Batch.js +1 -1
  8. package/dist/Batch.js.map +1 -1
  9. package/dist/Collection.d.ts +3 -3
  10. package/dist/Collection.d.ts.map +1 -1
  11. package/dist/Collection.js +3 -3
  12. package/dist/Collection.js.map +1 -1
  13. package/dist/Collections.d.ts +1 -1
  14. package/dist/Collections.d.ts.map +1 -1
  15. package/dist/Collections.js +1 -1
  16. package/dist/Collections.js.map +1 -1
  17. package/dist/DynamoClient.d.ts +12 -8
  18. package/dist/DynamoClient.d.ts.map +1 -1
  19. package/dist/DynamoClient.js +23 -7
  20. package/dist/DynamoClient.js.map +1 -1
  21. package/dist/Entity.d.ts +41 -19
  22. package/dist/Entity.d.ts.map +1 -1
  23. package/dist/Entity.js +185 -58
  24. package/dist/Entity.js.map +1 -1
  25. package/dist/EventStore.d.ts +1 -1
  26. package/dist/EventStore.d.ts.map +1 -1
  27. package/dist/EventStore.js +6 -5
  28. package/dist/EventStore.js.map +1 -1
  29. package/dist/Marshaller.d.ts +1 -1
  30. package/dist/Marshaller.d.ts.map +1 -1
  31. package/dist/Query.d.ts +1 -1
  32. package/dist/Query.d.ts.map +1 -1
  33. package/dist/Query.js +1 -1
  34. package/dist/Query.js.map +1 -1
  35. package/dist/Table.d.ts +2 -2
  36. package/dist/Table.d.ts.map +1 -1
  37. package/dist/Table.js.map +1 -1
  38. package/dist/Transaction.d.ts +1 -1
  39. package/dist/Transaction.d.ts.map +1 -1
  40. package/dist/Transaction.js +1 -1
  41. package/dist/Transaction.js.map +1 -1
  42. package/dist/index.d.ts +9 -9
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +5 -5
  45. package/dist/index.js.map +1 -1
  46. package/dist/internal/BoundQuery.d.ts +1 -1
  47. package/dist/internal/BoundQuery.d.ts.map +1 -1
  48. package/dist/internal/BoundQuery.js.map +1 -1
  49. package/dist/internal/EntityCombinators.d.ts +1 -1
  50. package/dist/internal/EntityCombinators.d.ts.map +1 -1
  51. package/dist/internal/EntityCombinators.js.map +1 -1
  52. package/dist/internal/EntityOps.d.ts +2 -2
  53. package/dist/internal/EntityOps.d.ts.map +1 -1
  54. package/dist/internal/EntityOps.js.map +1 -1
  55. package/dist/internal/TransactableOps.d.ts +16 -4
  56. package/dist/internal/TransactableOps.d.ts.map +1 -1
  57. package/dist/internal/TransactableOps.js +24 -13
  58. package/dist/internal/TransactableOps.js.map +1 -1
  59. package/package.json +7 -6
  60. package/dist/DynamoModel.d.ts +0 -370
  61. package/dist/DynamoModel.d.ts.map +0 -1
  62. package/dist/DynamoModel.js +0 -562
  63. package/dist/DynamoModel.js.map +0 -1
  64. package/dist/DynamoSchema.d.ts +0 -137
  65. package/dist/DynamoSchema.d.ts.map +0 -1
  66. package/dist/DynamoSchema.js +0 -188
  67. package/dist/DynamoSchema.js.map +0 -1
  68. package/dist/Errors.d.ts +0 -387
  69. package/dist/Errors.d.ts.map +0 -1
  70. package/dist/Errors.js +0 -218
  71. package/dist/Errors.js.map +0 -1
  72. package/dist/KeyComposer.d.ts +0 -264
  73. package/dist/KeyComposer.d.ts.map +0 -1
  74. package/dist/KeyComposer.js +0 -516
  75. package/dist/KeyComposer.js.map +0 -1
  76. package/dist/Projection.d.ts +0 -28
  77. package/dist/Projection.d.ts.map +0 -1
  78. package/dist/Projection.js +0 -39
  79. package/dist/Projection.js.map +0 -1
  80. package/dist/internal/AggregateCursor.d.ts +0 -44
  81. package/dist/internal/AggregateCursor.d.ts.map +0 -1
  82. package/dist/internal/AggregateCursor.js +0 -18
  83. package/dist/internal/AggregateCursor.js.map +0 -1
  84. package/dist/internal/AggregateEdges.d.ts +0 -77
  85. package/dist/internal/AggregateEdges.d.ts.map +0 -1
  86. package/dist/internal/AggregateEdges.js +0 -41
  87. package/dist/internal/AggregateEdges.js.map +0 -1
  88. package/dist/internal/AggregateSchemas.d.ts +0 -60
  89. package/dist/internal/AggregateSchemas.d.ts.map +0 -1
  90. package/dist/internal/AggregateSchemas.js +0 -201
  91. package/dist/internal/AggregateSchemas.js.map +0 -1
  92. package/dist/internal/AggregateTypes.d.ts +0 -94
  93. package/dist/internal/AggregateTypes.d.ts.map +0 -1
  94. package/dist/internal/AggregateTypes.js +0 -7
  95. package/dist/internal/AggregateTypes.js.map +0 -1
  96. package/dist/internal/EntityConfig.d.ts +0 -134
  97. package/dist/internal/EntityConfig.d.ts.map +0 -1
  98. package/dist/internal/EntityConfig.js +0 -7
  99. package/dist/internal/EntityConfig.js.map +0 -1
  100. package/dist/internal/EntitySchemas.d.ts +0 -190
  101. package/dist/internal/EntitySchemas.d.ts.map +0 -1
  102. package/dist/internal/EntitySchemas.js +0 -794
  103. package/dist/internal/EntitySchemas.js.map +0 -1
  104. package/dist/internal/EntityTypes.d.ts +0 -208
  105. package/dist/internal/EntityTypes.d.ts.map +0 -1
  106. package/dist/internal/EntityTypes.js +0 -7
  107. package/dist/internal/EntityTypes.js.map +0 -1
@@ -1,794 +0,0 @@
1
- /**
2
- * @internal Entity schema building — derived schemas and system field resolution.
3
- *
4
- * Extracted from Entity.ts for decomposition. Not part of the public API.
5
- */
6
- import { DateTime, Effect, Option, Redacted, Schema, SchemaAST, SchemaGetter, SchemaIssue, } from "effect";
7
- import { getEncoding, isHidden } from "../DynamoModel.js";
8
- /**
9
- * Resolve the effective DynamoEncoding for a model-declared field, applying
10
- * ConfiguredModel storage overrides on top of schema-level annotations.
11
- *
12
- * Precedence: schema annotation (getEncoding) > inferred default (Effect date
13
- * schemas) > none. ConfiguredModel `storedAs` then overrides storage only,
14
- * preserving the domain from the schema side when present.
15
- *
16
- * Returns `null` if the field is not a date-compatible schema.
17
- */
18
- export const resolveFieldEncoding = (fieldSchema, override) => {
19
- let encoding = getEncoding(fieldSchema) ?? inferDefaultEncoding(fieldSchema) ?? null;
20
- if (override) {
21
- encoding = {
22
- storage: override.storage,
23
- domain: encoding?.domain ?? override.domain,
24
- };
25
- }
26
- return encoding;
27
- };
28
- /**
29
- * Build the field → DynamoEncoding map for a set of model fields, merging
30
- * ConfiguredModel storage overrides. Used at `Entity.make()` time.
31
- */
32
- export const buildFieldEncodings = (modelFields, configuredAttributes) => {
33
- const encodings = {};
34
- for (const [fieldName, fieldSchema] of Object.entries(modelFields)) {
35
- const encoding = resolveFieldEncoding(fieldSchema, configuredAttributes[fieldName]?.encoding);
36
- if (encoding)
37
- encodings[fieldName] = encoding;
38
- }
39
- return encodings;
40
- };
41
- /** Check if a value is a Schema (has .ast property) vs a plain config object */
42
- const isSchema = (value) => typeof value === "object" && value !== null && "ast" in value;
43
- /**
44
- * Resolve a single TimestampFieldConfig into field name + encoding.
45
- * Returns [fieldName, encoding | null].
46
- */
47
- const resolveTimestampField = (config, defaultName) => {
48
- if (config === undefined)
49
- return [defaultName, null];
50
- if (typeof config === "string")
51
- return [config, null];
52
- if (isSchema(config)) {
53
- const encoding = getEncoding(config);
54
- return [defaultName, encoding ?? null];
55
- }
56
- // Object config: { field?, schema? }
57
- const fieldName = config.field ?? defaultName;
58
- const encoding = config.schema ? (getEncoding(config.schema) ?? null) : null;
59
- return [fieldName, encoding];
60
- };
61
- export const resolveSystemFields = (timestamps, versioned, timeSeries, modelFields = {}, configuredAttributes = {}) => {
62
- let createdAt = null;
63
- let createdAtEncoding = null;
64
- let updatedAt = null;
65
- let updatedAtEncoding = null;
66
- let version = null;
67
- if (timestamps === true) {
68
- createdAt = "createdAt";
69
- updatedAt = "updatedAt";
70
- }
71
- else if (typeof timestamps === "object" && timestamps !== null) {
72
- ;
73
- [createdAt, createdAtEncoding] = resolveTimestampField(timestamps.created, "createdAt");
74
- [updatedAt, updatedAtEncoding] = resolveTimestampField(timestamps.updated, "updatedAt");
75
- }
76
- if (versioned === true) {
77
- version = "version";
78
- }
79
- else if (typeof versioned === "object" && versioned !== null) {
80
- version = versioned.field ?? "version";
81
- }
82
- // Time-series entities auto-suppress `updatedAt` — the `orderBy` attribute
83
- // serves as the update clock. `createdAt` is preserved.
84
- if (timeSeries !== undefined && timeSeries !== null) {
85
- if (updatedAt !== null) {
86
- if (typeof timestamps === "object" &&
87
- timestamps !== null &&
88
- timestamps.updated !== undefined) {
89
- // eslint-disable-next-line no-console
90
- console.warn("[effect-dynamodb] timeSeries entity: `timestamps.updated` is ignored — " +
91
- "the `orderBy` attribute is the event-time clock.");
92
- }
93
- updatedAt = null;
94
- updatedAtEncoding = null;
95
- }
96
- }
97
- // Collision detection: when the model declares a field whose name matches
98
- // an active system field, there are two cases:
99
- // (a) Date-compatible schema → library-managed with user override
100
- // support. Encoding reads from the model field (precedence over
101
- // `timestamps` config default).
102
- // (b) Non-date schema → user owns the field entirely. Library skips
103
- // timestamp management for this field (effectively as if the
104
- // corresponding `timestamps.created`/`updated` was never set).
105
- // This preserves existing patterns where users declare `createdAt`
106
- // as a plain string composite.
107
- let createdAtCollision = false;
108
- if (createdAt && createdAt in modelFields) {
109
- const modelEncoding = resolveFieldEncoding(modelFields[createdAt], configuredAttributes[createdAt]?.encoding);
110
- if (modelEncoding) {
111
- createdAtEncoding = modelEncoding;
112
- createdAtCollision = true;
113
- }
114
- else {
115
- // Non-date collision: user owns the field.
116
- createdAt = null;
117
- createdAtEncoding = null;
118
- }
119
- }
120
- let updatedAtCollision = false;
121
- if (updatedAt && updatedAt in modelFields) {
122
- const modelEncoding = resolveFieldEncoding(modelFields[updatedAt], configuredAttributes[updatedAt]?.encoding);
123
- if (modelEncoding) {
124
- updatedAtEncoding = modelEncoding;
125
- updatedAtCollision = true;
126
- }
127
- else {
128
- updatedAt = null;
129
- updatedAtEncoding = null;
130
- }
131
- }
132
- const versionCollision = !!(version && version in modelFields);
133
- return {
134
- createdAt,
135
- createdAtEncoding,
136
- createdAtCollision,
137
- updatedAt,
138
- updatedAtEncoding,
139
- updatedAtCollision,
140
- version,
141
- versionCollision,
142
- };
143
- };
144
- // ---------------------------------------------------------------------------
145
- // Helper: collect primary key composite attribute names
146
- // ---------------------------------------------------------------------------
147
- export const primaryKeyComposites = (indexes) => {
148
- const primary = indexes.primary;
149
- if (!primary)
150
- return [];
151
- const composites = new Set();
152
- for (const attr of primary.pk.composite)
153
- composites.add(attr);
154
- for (const attr of primary.sk.composite)
155
- composites.add(attr);
156
- return Array.from(composites);
157
- };
158
- // ---------------------------------------------------------------------------
159
- // Helper: collect all key field names across all indexes
160
- // ---------------------------------------------------------------------------
161
- export const allKeyFieldNames = (indexes) => {
162
- const fields = new Set();
163
- for (const key of Object.keys(indexes)) {
164
- const index = indexes[key];
165
- if (index) {
166
- fields.add(index.pk.field);
167
- fields.add(index.sk.field);
168
- }
169
- }
170
- return Array.from(fields);
171
- };
172
- // ---------------------------------------------------------------------------
173
- // Helper: collect all composite attribute names across all indexes
174
- // ---------------------------------------------------------------------------
175
- export const allCompositeAttributes = (indexes) => {
176
- const attrs = new Set();
177
- for (const key of Object.keys(indexes)) {
178
- const index = indexes[key];
179
- if (index) {
180
- for (const attr of index.pk.composite)
181
- attrs.add(attr);
182
- for (const attr of index.sk.composite)
183
- attrs.add(attr);
184
- }
185
- }
186
- return Array.from(attrs);
187
- };
188
- const hasFields = (schema) => "fields" in schema && typeof schema.fields === "object";
189
- export const getFields = (model) => {
190
- if (hasFields(model))
191
- return model.fields;
192
- throw new Error("Entity model must be a Schema.Class or Schema.Struct with .fields");
193
- };
194
- /**
195
- * Infer a default DynamoEncoding for standard Effect date schemas that lack
196
- * an explicit DynamoEncoding annotation. This enables Pattern B (pure model
197
- * with Schema.DateTimeUtcFromString etc.) to work without explicit storedAs.
198
- *
199
- * Detection uses the typeConstructor annotation from Effect's schema AST.
200
- */
201
- export const inferDefaultEncoding = (schema) => {
202
- const resolved = SchemaAST.resolve(schema.ast);
203
- if (!resolved)
204
- return undefined;
205
- const tc = resolved.typeConstructor;
206
- if (tc?._tag === "effect/DateTime.Utc")
207
- return { storage: "string", domain: "DateTime.Utc" };
208
- if (tc?._tag === "effect/DateTime.Zoned")
209
- return { storage: "string", domain: "DateTime.Zoned" };
210
- // Check for native Date via meta annotation
211
- const meta = resolved.meta;
212
- if (meta?._tag === "isDateValid")
213
- return { storage: "string", domain: "Date" };
214
- return undefined;
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
- // ---------------------------------------------------------------------------
254
- /**
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)
263
- */
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);
297
- }
298
- };
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;
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;
486
- }
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);
571
- // --- Record Schema: model + system fields ---
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.
576
- const systemSchemaFields = {};
577
- if (systemFields.createdAt && !systemFields.createdAtCollision) {
578
- systemSchemaFields[systemFields.createdAt] = systemFields.createdAtEncoding
579
- ? buildDateTransform(systemFields.createdAtEncoding)
580
- : Schema.DateTimeUtcFromString;
581
- }
582
- if (systemFields.updatedAt && !systemFields.updatedAtCollision) {
583
- systemSchemaFields[systemFields.updatedAt] = systemFields.updatedAtEncoding
584
- ? buildDateTransform(systemFields.updatedAtEncoding)
585
- : Schema.DateTimeUtcFromString;
586
- }
587
- if (systemFields.version && !systemFields.versionCollision) {
588
- systemSchemaFields[systemFields.version] = Schema.Number;
589
- }
590
- const recordSchema = Schema.Struct({
591
- ...fields,
592
- ...systemSchemaFields,
593
- });
594
- // --- System-collision handling for input/update schemas ---
595
- // When the model declares a field whose name matches an active system field,
596
- // the input shape changes:
597
- // - createdAt / updatedAt → marked optional (user may supply a value or
598
- // leave absent and let the library generate).
599
- // - version → stripped entirely (optimistic locking requires library-
600
- // managed increment; user-supplied values would break the mechanic).
601
- // `createdAt` is also treated as immutable in the update schema.
602
- const optionalOnCollide = new Set();
603
- if (systemFields.createdAtCollision && systemFields.createdAt)
604
- optionalOnCollide.add(systemFields.createdAt);
605
- if (systemFields.updatedAtCollision && systemFields.updatedAt)
606
- optionalOnCollide.add(systemFields.updatedAt);
607
- const strippedFromInput = new Set();
608
- if (systemFields.versionCollision && systemFields.version)
609
- strippedFromInput.add(systemFields.version);
610
- const strippedFromUpdate = new Set(strippedFromInput);
611
- if (systemFields.createdAtCollision && systemFields.createdAt)
612
- strippedFromUpdate.add(systemFields.createdAt);
613
- const applySystemCollisionAdjustments = (fieldName, fieldSchema, kind) => {
614
- const strippedSet = kind === "update" ? strippedFromUpdate : strippedFromInput;
615
- if (strippedSet.has(fieldName))
616
- return null;
617
- if (optionalOnCollide.has(fieldName))
618
- return Schema.optional(fieldSchema);
619
- return fieldSchema;
620
- };
621
- // --- Input Schema: ref-aware (swap ref fields for ID fields when refs present) ---
622
- const refFieldSet = new Set(resolvedRefs.map((r) => r.fieldName));
623
- const buildInputFieldsAcc = () => {
624
- const acc = {};
625
- for (const [key, field] of Object.entries(fields)) {
626
- if (refFieldSet.has(key))
627
- continue;
628
- const adjusted = applySystemCollisionAdjustments(key, field, "input");
629
- if (adjusted !== null)
630
- acc[key] = adjusted;
631
- }
632
- for (const ref of resolvedRefs) {
633
- acc[ref.idFieldName] = ref.identifierSchema;
634
- }
635
- return acc;
636
- };
637
- const inputSchema = Schema.Struct(buildInputFieldsAcc());
638
- // --- Create Schema: input fields minus identifier (or PK composites as fallback) ---
639
- const pkComposites = new Set(primaryKeyComposites(indexes));
640
- const createOmitFields = identifierField ? new Set([identifierField]) : pkComposites;
641
- const createFieldsAcc = {};
642
- for (const [key, field] of Object.entries(fields)) {
643
- if (createOmitFields.has(key))
644
- continue; // skip identifier/PK composites (auto-generated)
645
- if (refFieldSet.has(key))
646
- continue; // skip ref fields (replaced by ID fields)
647
- const adjusted = applySystemCollisionAdjustments(key, field, "input");
648
- if (adjusted !== null)
649
- createFieldsAcc[key] = adjusted;
650
- }
651
- // Add ID fields for each ref (skip if the ref field is an omitted field)
652
- for (const ref of resolvedRefs) {
653
- if (createOmitFields.has(ref.fieldName))
654
- continue;
655
- createFieldsAcc[ref.idFieldName] = ref.identifierSchema;
656
- }
657
- const createSchema = Schema.Struct(createFieldsAcc);
658
- // --- Update Schema: optional model fields minus key composites & immutable ---
659
- // Each field is wrapped in `Schema.optional` so the key may be omitted
660
- // entirely (the structural composer treats absent keys as absent for GSI
661
- // composition under v3's two-way classification). The field's value type
662
- // matches the model declaration exactly — there is no `null` widening,
663
- // because EDD-9025 prevents composites from declaring `null` and the
664
- // standard update payload follows the same shape. If the model declares a
665
- // non-composite attribute as `Schema.NullOr(X)`, `set({ attr: null })`
666
- // still typechecks; the v1.6 `NullishOr` wrap that allowed null on
667
- // arbitrary fields (regardless of model declaration) is removed.
668
- const buildUpdateFieldsAcc = () => {
669
- const acc = {};
670
- for (const [key, field] of Object.entries(fields)) {
671
- if (pkComposites.has(key))
672
- continue;
673
- if (immutableFields.has(key))
674
- continue;
675
- if (refFieldSet.has(key))
676
- continue;
677
- if (strippedFromUpdate.has(key))
678
- continue;
679
- acc[key] = Schema.optional(field);
680
- }
681
- for (const ref of resolvedRefs) {
682
- if (pkComposites.has(ref.fieldName))
683
- continue;
684
- if (immutableFields.has(ref.fieldName))
685
- continue;
686
- acc[ref.idFieldName] = Schema.optional(ref.identifierSchema);
687
- }
688
- return acc;
689
- };
690
- const updateSchema = Schema.Struct(buildUpdateFieldsAcc());
691
- // --- Key Schema: primary key composite attributes only ---
692
- const keyCompositeNames = primaryKeyComposites(indexes);
693
- const keyFields = {};
694
- for (const name of keyCompositeNames) {
695
- const field = fields[name];
696
- if (field)
697
- keyFields[name] = field;
698
- }
699
- const keySchema = Schema.Struct(keyFields);
700
- // --- Item Schema: record + key attrs (pk, sk, gsi*) + __edd_e__ ---
701
- // Primary pk/sk are guaranteed present on every live item. GSI key fields
702
- // are sparse: they're written only when every source composite resolves, and
703
- // dropped when any composite is missing or indexPolicy evicts the index.
704
- // Stream NewImage / raw query results therefore routinely lack GSI keys —
705
- // mark them optional so decodeMarshalledItem decodes sparse items cleanly.
706
- const primaryKeyFieldNames = new Set();
707
- const primary = indexes.primary;
708
- if (primary) {
709
- primaryKeyFieldNames.add(primary.pk.field);
710
- primaryKeyFieldNames.add(primary.sk.field);
711
- }
712
- const keyAttrFields = {};
713
- for (const fieldName of allKeyFieldNames(indexes)) {
714
- keyAttrFields[fieldName] = primaryKeyFieldNames.has(fieldName)
715
- ? Schema.String
716
- : Schema.optional(Schema.String);
717
- }
718
- const itemSchema = Schema.Struct({
719
- ...fields,
720
- ...systemSchemaFields,
721
- ...keyAttrFields,
722
- __edd_e__: Schema.String,
723
- });
724
- // --- Deleted Record Schema: record + deletedAt ---
725
- const deletedRecordSchema = Schema.Struct({
726
- ...fields,
727
- ...systemSchemaFields,
728
- deletedAt: Schema.String,
729
- });
730
- // --- Hidden-aware decode schemas: strip DynamoModel.Hidden fields from model/record ---
731
- // These are used for "model" and "record" decode modes. Hidden fields are
732
- // still present in "item" and "native" modes.
733
- const hiddenFieldNames = new Set();
734
- for (const [fieldName, fieldSchema] of Object.entries(modelFields)) {
735
- if (isHidden(fieldSchema))
736
- hiddenFieldNames.add(fieldName);
737
- }
738
- const hasHiddenFields = hiddenFieldNames.size > 0;
739
- const modelVisibleSchema = hasHiddenFields
740
- ? Schema.Struct(Object.fromEntries(Object.entries(fields).filter(([k]) => !hiddenFieldNames.has(k))))
741
- : modelSchema;
742
- const recordVisibleSchema = hasHiddenFields
743
- ? Schema.Struct(Object.fromEntries(Object.entries({
744
- ...fields,
745
- ...systemSchemaFields,
746
- }).filter(([k]) => !hiddenFieldNames.has(k))))
747
- : recordSchema;
748
- // --- Time-series derived schemas ---
749
- // `appendInputSchema`: the user-supplied schema, with the same substitution
750
- // rules applied as the model fields (self date schemas → tolerant
751
- // transforms, RedactedFromValue → tolerant Redacted). `Entity.make()`
752
- // enforces that every PK/SK composite + `orderBy` appears in its fields.
753
- // `historyRecordSchema`: event-item shape. Substituted model fields + no
754
- // system fields (events are point-in-time facts keyed by `orderBy`).
755
- const timeSeriesEnabled = timeSeries !== undefined && timeSeries !== null;
756
- const appendInputSchema = timeSeriesEnabled
757
- ? (() => {
758
- const userSchema = timeSeries.appendInput;
759
- const userFields = userSchema.fields;
760
- if (!userFields)
761
- return userSchema;
762
- // Compute encodings for the appendInput fields (annotation +
763
- // inferred from typeConstructor for self schemas).
764
- const appendEncodings = buildFieldEncodings(userFields, {});
765
- const subbedFields = substituteSchemas(userFields, appendEncodings);
766
- return Schema.Struct(subbedFields);
767
- })()
768
- : null;
769
- const historyRecordSchema = timeSeriesEnabled
770
- ? (() => {
771
- const hiddenFilter = (record) => hasHiddenFields
772
- ? Object.fromEntries(Object.entries(record).filter(([k]) => !hiddenFieldNames.has(k)))
773
- : record;
774
- return Schema.Struct(hiddenFilter({ ...fields }));
775
- })()
776
- : null;
777
- return {
778
- modelSchema: modelVisibleSchema,
779
- recordSchema: recordVisibleSchema,
780
- inputSchema: inputSchema,
781
- createSchema: createSchema,
782
- updateSchema: updateSchema,
783
- keySchema: keySchema,
784
- itemSchema: itemSchema,
785
- deletedRecordSchema: deletedRecordSchema,
786
- appendInputSchema: appendInputSchema,
787
- historyRecordSchema: historyRecordSchema,
788
- };
789
- };
790
- // ---------------------------------------------------------------------------
791
- // Unique constraint field resolver
792
- // ---------------------------------------------------------------------------
793
- export const resolveUniqueFields = (def) => Array.isArray(def) ? def : def.fields;
794
- //# sourceMappingURL=EntitySchemas.js.map