@effect-dynamodb/schema 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.
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/dist/Aggregate.d.ts +128 -0
- package/dist/Aggregate.d.ts.map +1 -0
- package/dist/Aggregate.js +69 -0
- package/dist/Aggregate.js.map +1 -0
- package/dist/DynamoModel.d.ts +370 -0
- package/dist/DynamoModel.d.ts.map +1 -0
- package/dist/DynamoModel.js +541 -0
- package/dist/DynamoModel.js.map +1 -0
- package/dist/DynamoSchema.d.ts +137 -0
- package/dist/DynamoSchema.d.ts.map +1 -0
- package/dist/DynamoSchema.js +188 -0
- package/dist/DynamoSchema.js.map +1 -0
- package/dist/Entity.d.ts +239 -0
- package/dist/Entity.d.ts.map +1 -0
- package/dist/Entity.js +558 -0
- package/dist/Entity.js.map +1 -0
- package/dist/Errors.d.ts +387 -0
- package/dist/Errors.d.ts.map +1 -0
- package/dist/Errors.js +218 -0
- package/dist/Errors.js.map +1 -0
- package/dist/KeyComposer.d.ts +264 -0
- package/dist/KeyComposer.d.ts.map +1 -0
- package/dist/KeyComposer.js +516 -0
- package/dist/KeyComposer.js.map +1 -0
- package/dist/Projection.d.ts +28 -0
- package/dist/Projection.d.ts.map +1 -0
- package/dist/Projection.js +39 -0
- package/dist/Projection.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/AggregateCursor.d.ts +44 -0
- package/dist/internal/AggregateCursor.d.ts.map +1 -0
- package/dist/internal/AggregateCursor.js +18 -0
- package/dist/internal/AggregateCursor.js.map +1 -0
- package/dist/internal/AggregateEdges.d.ts +117 -0
- package/dist/internal/AggregateEdges.d.ts.map +1 -0
- package/dist/internal/AggregateEdges.js +51 -0
- package/dist/internal/AggregateEdges.js.map +1 -0
- package/dist/internal/AggregateSchemas.d.ts +76 -0
- package/dist/internal/AggregateSchemas.d.ts.map +1 -0
- package/dist/internal/AggregateSchemas.js +164 -0
- package/dist/internal/AggregateSchemas.js.map +1 -0
- package/dist/internal/AggregateTypes.d.ts +132 -0
- package/dist/internal/AggregateTypes.d.ts.map +1 -0
- package/dist/internal/AggregateTypes.js +7 -0
- package/dist/internal/AggregateTypes.js.map +1 -0
- package/dist/internal/DefaultCrypto.d.ts +26 -0
- package/dist/internal/DefaultCrypto.d.ts.map +1 -0
- package/dist/internal/DefaultCrypto.js +40 -0
- package/dist/internal/DefaultCrypto.js.map +1 -0
- package/dist/internal/EntityConfig.d.ts +152 -0
- package/dist/internal/EntityConfig.d.ts.map +1 -0
- package/dist/internal/EntityConfig.js +7 -0
- package/dist/internal/EntityConfig.js.map +1 -0
- package/dist/internal/EntitySchemas.d.ts +198 -0
- package/dist/internal/EntitySchemas.d.ts.map +1 -0
- package/dist/internal/EntitySchemas.js +798 -0
- package/dist/internal/EntitySchemas.js.map +1 -0
- package/dist/internal/EntityTypes.d.ts +233 -0
- package/dist/internal/EntityTypes.d.ts.map +1 -0
- package/dist/internal/EntityTypes.js +7 -0
- package/dist/internal/EntityTypes.js.map +1 -0
- package/dist/internal/SchemaAccessors.d.ts +90 -0
- package/dist/internal/SchemaAccessors.d.ts.map +1 -0
- package/dist/internal/SchemaAccessors.js +171 -0
- package/dist/internal/SchemaAccessors.js.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,798 @@
|
|
|
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
|
+
import { firstTypeParameterAst, getSchemaFields, transformWireKind as transformWireKindImpl, } from "./SchemaAccessors.js";
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the effective DynamoEncoding for a model-declared field, applying
|
|
11
|
+
* ConfiguredModel storage overrides on top of schema-level annotations.
|
|
12
|
+
*
|
|
13
|
+
* Precedence: schema annotation (getEncoding) > inferred default (Effect date
|
|
14
|
+
* schemas) > none. ConfiguredModel `storedAs` then overrides storage only,
|
|
15
|
+
* preserving the domain from the schema side when present.
|
|
16
|
+
*
|
|
17
|
+
* Returns `null` if the field is not a date-compatible schema.
|
|
18
|
+
*/
|
|
19
|
+
export const resolveFieldEncoding = (fieldSchema, override) => {
|
|
20
|
+
let encoding = getEncoding(fieldSchema) ?? inferDefaultEncoding(fieldSchema) ?? null;
|
|
21
|
+
if (override) {
|
|
22
|
+
encoding = {
|
|
23
|
+
storage: override.storage,
|
|
24
|
+
domain: encoding?.domain ?? override.domain,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return encoding;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Build the field → DynamoEncoding map for a set of model fields, merging
|
|
31
|
+
* ConfiguredModel storage overrides. Used at `Entity.make()` time.
|
|
32
|
+
*/
|
|
33
|
+
export const buildFieldEncodings = (modelFields, configuredAttributes) => {
|
|
34
|
+
const encodings = {};
|
|
35
|
+
for (const [fieldName, fieldSchema] of Object.entries(modelFields)) {
|
|
36
|
+
const encoding = resolveFieldEncoding(fieldSchema, configuredAttributes[fieldName]?.encoding);
|
|
37
|
+
if (encoding)
|
|
38
|
+
encodings[fieldName] = encoding;
|
|
39
|
+
}
|
|
40
|
+
return encodings;
|
|
41
|
+
};
|
|
42
|
+
/** Check if a value is a Schema (has .ast property) vs a plain config object */
|
|
43
|
+
const isSchema = (value) => typeof value === "object" && value !== null && "ast" in value;
|
|
44
|
+
/**
|
|
45
|
+
* Resolve a single TimestampFieldConfig into field name + encoding.
|
|
46
|
+
* Returns [fieldName, encoding | null].
|
|
47
|
+
*/
|
|
48
|
+
const resolveTimestampField = (config, defaultName) => {
|
|
49
|
+
if (config === undefined)
|
|
50
|
+
return [defaultName, null];
|
|
51
|
+
if (typeof config === "string")
|
|
52
|
+
return [config, null];
|
|
53
|
+
if (isSchema(config)) {
|
|
54
|
+
const encoding = getEncoding(config);
|
|
55
|
+
return [defaultName, encoding ?? null];
|
|
56
|
+
}
|
|
57
|
+
// Object config: { field?, schema? }
|
|
58
|
+
const fieldName = config.field ?? defaultName;
|
|
59
|
+
const encoding = config.schema ? (getEncoding(config.schema) ?? null) : null;
|
|
60
|
+
return [fieldName, encoding];
|
|
61
|
+
};
|
|
62
|
+
export const resolveSystemFields = (timestamps, versioned, timeSeries, modelFields = {}, configuredAttributes = {}) => {
|
|
63
|
+
let createdAt = null;
|
|
64
|
+
let createdAtEncoding = null;
|
|
65
|
+
let updatedAt = null;
|
|
66
|
+
let updatedAtEncoding = null;
|
|
67
|
+
let version = null;
|
|
68
|
+
if (timestamps === true) {
|
|
69
|
+
createdAt = "createdAt";
|
|
70
|
+
updatedAt = "updatedAt";
|
|
71
|
+
}
|
|
72
|
+
else if (typeof timestamps === "object" && timestamps !== null) {
|
|
73
|
+
;
|
|
74
|
+
[createdAt, createdAtEncoding] = resolveTimestampField(timestamps.created, "createdAt");
|
|
75
|
+
[updatedAt, updatedAtEncoding] = resolveTimestampField(timestamps.updated, "updatedAt");
|
|
76
|
+
}
|
|
77
|
+
if (versioned === true) {
|
|
78
|
+
version = "version";
|
|
79
|
+
}
|
|
80
|
+
else if (typeof versioned === "object" && versioned !== null) {
|
|
81
|
+
version = versioned.field ?? "version";
|
|
82
|
+
}
|
|
83
|
+
// Time-series entities auto-suppress `updatedAt` — the `orderBy` attribute
|
|
84
|
+
// serves as the update clock. `createdAt` is preserved.
|
|
85
|
+
if (timeSeries !== undefined && timeSeries !== null) {
|
|
86
|
+
if (updatedAt !== null) {
|
|
87
|
+
if (typeof timestamps === "object" &&
|
|
88
|
+
timestamps !== null &&
|
|
89
|
+
timestamps.updated !== undefined) {
|
|
90
|
+
// eslint-disable-next-line no-console
|
|
91
|
+
console.warn("[effect-dynamodb] timeSeries entity: `timestamps.updated` is ignored — " +
|
|
92
|
+
"the `orderBy` attribute is the event-time clock.");
|
|
93
|
+
}
|
|
94
|
+
updatedAt = null;
|
|
95
|
+
updatedAtEncoding = null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Collision detection: when the model declares a field whose name matches
|
|
99
|
+
// an active system field, there are two cases:
|
|
100
|
+
// (a) Date-compatible schema → library-managed with user override
|
|
101
|
+
// support. Encoding reads from the model field (precedence over
|
|
102
|
+
// `timestamps` config default).
|
|
103
|
+
// (b) Non-date schema → user owns the field entirely. Library skips
|
|
104
|
+
// timestamp management for this field (effectively as if the
|
|
105
|
+
// corresponding `timestamps.created`/`updated` was never set).
|
|
106
|
+
// This preserves existing patterns where users declare `createdAt`
|
|
107
|
+
// as a plain string composite.
|
|
108
|
+
let createdAtCollision = false;
|
|
109
|
+
if (createdAt && createdAt in modelFields) {
|
|
110
|
+
const modelEncoding = resolveFieldEncoding(modelFields[createdAt], configuredAttributes[createdAt]?.encoding);
|
|
111
|
+
if (modelEncoding) {
|
|
112
|
+
createdAtEncoding = modelEncoding;
|
|
113
|
+
createdAtCollision = true;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
// Non-date collision: user owns the field.
|
|
117
|
+
createdAt = null;
|
|
118
|
+
createdAtEncoding = null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
let updatedAtCollision = false;
|
|
122
|
+
if (updatedAt && updatedAt in modelFields) {
|
|
123
|
+
const modelEncoding = resolveFieldEncoding(modelFields[updatedAt], configuredAttributes[updatedAt]?.encoding);
|
|
124
|
+
if (modelEncoding) {
|
|
125
|
+
updatedAtEncoding = modelEncoding;
|
|
126
|
+
updatedAtCollision = true;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
updatedAt = null;
|
|
130
|
+
updatedAtEncoding = null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const versionCollision = !!(version && version in modelFields);
|
|
134
|
+
return {
|
|
135
|
+
createdAt,
|
|
136
|
+
createdAtEncoding,
|
|
137
|
+
createdAtCollision,
|
|
138
|
+
updatedAt,
|
|
139
|
+
updatedAtEncoding,
|
|
140
|
+
updatedAtCollision,
|
|
141
|
+
version,
|
|
142
|
+
versionCollision,
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Helper: collect primary key composite attribute names
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
export const primaryKeyComposites = (indexes) => {
|
|
149
|
+
const primary = indexes.primary;
|
|
150
|
+
if (!primary)
|
|
151
|
+
return [];
|
|
152
|
+
const composites = new Set();
|
|
153
|
+
for (const attr of primary.pk.composite)
|
|
154
|
+
composites.add(attr);
|
|
155
|
+
for (const attr of primary.sk.composite)
|
|
156
|
+
composites.add(attr);
|
|
157
|
+
return Array.from(composites);
|
|
158
|
+
};
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Helper: collect all key field names across all indexes
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
export const allKeyFieldNames = (indexes) => {
|
|
163
|
+
const fields = new Set();
|
|
164
|
+
for (const key of Object.keys(indexes)) {
|
|
165
|
+
const index = indexes[key];
|
|
166
|
+
if (index) {
|
|
167
|
+
fields.add(index.pk.field);
|
|
168
|
+
fields.add(index.sk.field);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return Array.from(fields);
|
|
172
|
+
};
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Helper: collect all composite attribute names across all indexes
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
export const allCompositeAttributes = (indexes) => {
|
|
177
|
+
const attrs = new Set();
|
|
178
|
+
for (const key of Object.keys(indexes)) {
|
|
179
|
+
const index = indexes[key];
|
|
180
|
+
if (index) {
|
|
181
|
+
for (const attr of index.pk.composite)
|
|
182
|
+
attrs.add(attr);
|
|
183
|
+
for (const attr of index.sk.composite)
|
|
184
|
+
attrs.add(attr);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return Array.from(attrs);
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Re-export of the shared typed `.fields` accessor (returns `undefined` for
|
|
191
|
+
* non-Struct/Class schemas). Use this when the call site has its own
|
|
192
|
+
* fallback/error handling; use {@link getFields} when a missing `.fields` is a
|
|
193
|
+
* hard error.
|
|
194
|
+
*/
|
|
195
|
+
export { getSchemaFields };
|
|
196
|
+
export const getFields = (model) => {
|
|
197
|
+
const fields = getSchemaFields(model);
|
|
198
|
+
if (fields)
|
|
199
|
+
return fields;
|
|
200
|
+
throw new Error("Entity model must be a Schema.Class or Schema.Struct with .fields");
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* Infer a default DynamoEncoding for standard Effect date schemas that lack
|
|
204
|
+
* an explicit DynamoEncoding annotation. This enables Pattern B (pure model
|
|
205
|
+
* with Schema.DateTimeUtcFromString etc.) to work without explicit storedAs.
|
|
206
|
+
*
|
|
207
|
+
* Detection uses the typeConstructor annotation from Effect's schema AST.
|
|
208
|
+
*/
|
|
209
|
+
export const inferDefaultEncoding = (schema) => {
|
|
210
|
+
const resolved = SchemaAST.resolve(schema.ast);
|
|
211
|
+
if (!resolved)
|
|
212
|
+
return undefined;
|
|
213
|
+
const tc = resolved.typeConstructor;
|
|
214
|
+
if (tc?._tag === "effect/DateTime.Utc")
|
|
215
|
+
return { storage: "string", domain: "DateTime.Utc" };
|
|
216
|
+
if (tc?._tag === "effect/DateTime.Zoned")
|
|
217
|
+
return { storage: "string", domain: "DateTime.Zoned" };
|
|
218
|
+
// Check for native Date via meta annotation
|
|
219
|
+
const meta = resolved.meta;
|
|
220
|
+
if (meta?._tag === "isDateValid")
|
|
221
|
+
return { storage: "string", domain: "Date" };
|
|
222
|
+
return undefined;
|
|
223
|
+
};
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// Self vs. transform schema detection
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
/**
|
|
228
|
+
* Returns true when the schema is a "self" schema — i.e. it carries no
|
|
229
|
+
* encoding transformation. Self schemas have `ast.encoding === undefined`.
|
|
230
|
+
*
|
|
231
|
+
* - `Schema.DateTimeUtc` — self
|
|
232
|
+
* - `Schema.DateTimeUtcFromString` — transform (encodes to string)
|
|
233
|
+
* - `Schema.RedactedFromValue(...)` — transform
|
|
234
|
+
* - any `.pipe(Schema.decodeTo(...))` chain — transform
|
|
235
|
+
*/
|
|
236
|
+
export const isSelfSchema = (schema) => schema.ast.encoding === undefined;
|
|
237
|
+
/**
|
|
238
|
+
* True if the schema is a self DateTime/Date schema that can have its storage
|
|
239
|
+
* configured via a DynamoEncoding annotation. Distinguished from a transform
|
|
240
|
+
* by `ast.encoding === undefined`.
|
|
241
|
+
*/
|
|
242
|
+
export const isSelfDateSchema = (schema) => {
|
|
243
|
+
if (!isSelfSchema(schema))
|
|
244
|
+
return false;
|
|
245
|
+
// The self-schema check is necessary but not sufficient — also confirm the
|
|
246
|
+
// type constructor matches one of the supported date domains.
|
|
247
|
+
return inferDefaultEncoding(schema) !== undefined;
|
|
248
|
+
};
|
|
249
|
+
/**
|
|
250
|
+
* Returns true when the schema is a transform schema with a date typeConstructor
|
|
251
|
+
* (e.g. `Schema.DateTimeUtcFromString`, `DynamoModel.DateEpochSeconds`).
|
|
252
|
+
* Used to enforce the "transform owns the wire format" policy at Entity.make().
|
|
253
|
+
*/
|
|
254
|
+
export const isDateTransform = (schema) => {
|
|
255
|
+
if (isSelfSchema(schema))
|
|
256
|
+
return false;
|
|
257
|
+
return inferDefaultEncoding(schema) !== undefined;
|
|
258
|
+
};
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
// Self-date schema substitution
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
/**
|
|
263
|
+
* Convert any input value (string / number / DateTime / Date) to the wire
|
|
264
|
+
* primitive for a given encoding. Used by date substitutes' `encode` path.
|
|
265
|
+
*
|
|
266
|
+
* Wire format precisely matches the legacy `serializeDateForDynamo` output:
|
|
267
|
+
* - `string` storage → ISO 8601 string (UTC for DateTime.Utc / Date; extended
|
|
268
|
+
* format for DateTime.Zoned)
|
|
269
|
+
* - `epochMs` storage → integer milliseconds since the Unix epoch
|
|
270
|
+
* - `epochSeconds` storage → integer seconds since the Unix epoch (TTL format)
|
|
271
|
+
*/
|
|
272
|
+
const toWirePrimitive = (value, encoding) => {
|
|
273
|
+
let epochMs;
|
|
274
|
+
let zoned;
|
|
275
|
+
if (DateTime.isDateTime(value)) {
|
|
276
|
+
if (DateTime.isZoned(value))
|
|
277
|
+
zoned = value;
|
|
278
|
+
epochMs = DateTime.toEpochMillis(value);
|
|
279
|
+
}
|
|
280
|
+
else if (value instanceof Date) {
|
|
281
|
+
epochMs = value.getTime();
|
|
282
|
+
}
|
|
283
|
+
else if (typeof value === "string") {
|
|
284
|
+
epochMs = new Date(value).getTime();
|
|
285
|
+
}
|
|
286
|
+
else if (typeof value === "number") {
|
|
287
|
+
// Numeric inputs are interpreted in the encoding's storage scale already.
|
|
288
|
+
if (encoding.storage === "epochSeconds")
|
|
289
|
+
epochMs = value * 1000;
|
|
290
|
+
else
|
|
291
|
+
epochMs = value;
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
throw new Error(`[effect-dynamodb] toWirePrimitive: unsupported input ${typeof value} for ${encoding.domain}/${encoding.storage}`);
|
|
295
|
+
}
|
|
296
|
+
switch (encoding.storage) {
|
|
297
|
+
case "string":
|
|
298
|
+
if (encoding.domain === "DateTime.Zoned" && zoned)
|
|
299
|
+
return DateTime.formatIsoZoned(zoned);
|
|
300
|
+
return new Date(epochMs).toISOString();
|
|
301
|
+
case "epochMs":
|
|
302
|
+
return epochMs;
|
|
303
|
+
case "epochSeconds":
|
|
304
|
+
return Math.floor(epochMs / 1000);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
/**
|
|
308
|
+
* Build a bidirectional Schema for a given DynamoEncoding.
|
|
309
|
+
*
|
|
310
|
+
* The encoded side is `Schema.Any` so `decode` tolerates either:
|
|
311
|
+
* - the wire primitive (string / number) — the storage-shape input, OR
|
|
312
|
+
* - a domain value (`DateTime.Utc`, `Date`, etc.) — what users typically pass
|
|
313
|
+
* into `entity.put({...})` and `.set({...})`.
|
|
314
|
+
*
|
|
315
|
+
* The `decode` direction always lifts to the domain type (DateTime.Utc /
|
|
316
|
+
* DateTime.Zoned / Date). The `encode` direction always produces the wire
|
|
317
|
+
* primitive. This makes both `Schema.encode(substituted)` and
|
|
318
|
+
* `Schema.decode(substituted)` work end-to-end regardless of whether the
|
|
319
|
+
* caller passed wire-form or domain-form.
|
|
320
|
+
*
|
|
321
|
+
* @internal
|
|
322
|
+
*/
|
|
323
|
+
export const buildDateTransform = (encoding) => {
|
|
324
|
+
const targetSchema = (() => {
|
|
325
|
+
switch (encoding.domain) {
|
|
326
|
+
case "DateTime.Utc":
|
|
327
|
+
return Schema.DateTimeUtc;
|
|
328
|
+
case "DateTime.Zoned":
|
|
329
|
+
return Schema.DateTimeZoned;
|
|
330
|
+
case "Date":
|
|
331
|
+
return Schema.DateValid;
|
|
332
|
+
}
|
|
333
|
+
})();
|
|
334
|
+
const liftToDomain = (value) => {
|
|
335
|
+
if (DateTime.isDateTime(value))
|
|
336
|
+
return value;
|
|
337
|
+
if (value instanceof Date) {
|
|
338
|
+
switch (encoding.domain) {
|
|
339
|
+
case "DateTime.Utc":
|
|
340
|
+
return DateTime.makeUnsafe(value);
|
|
341
|
+
case "DateTime.Zoned":
|
|
342
|
+
return DateTime.makeZonedUnsafe(value, { timeZone: "UTC" });
|
|
343
|
+
case "Date":
|
|
344
|
+
return value;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (typeof value === "string") {
|
|
348
|
+
switch (encoding.domain) {
|
|
349
|
+
case "DateTime.Utc":
|
|
350
|
+
return DateTime.makeUnsafe(value);
|
|
351
|
+
case "DateTime.Zoned": {
|
|
352
|
+
const match = value.match(/^(.+)\[(.+)\]$/);
|
|
353
|
+
if (match) {
|
|
354
|
+
const utc = DateTime.makeUnsafe(match[1]);
|
|
355
|
+
return DateTime.makeZonedUnsafe(utc, { timeZone: match[2] });
|
|
356
|
+
}
|
|
357
|
+
const utc = DateTime.makeUnsafe(value);
|
|
358
|
+
return DateTime.makeZonedUnsafe(utc, { timeZone: "UTC" });
|
|
359
|
+
}
|
|
360
|
+
case "Date":
|
|
361
|
+
return new Date(value);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (typeof value === "number") {
|
|
365
|
+
const ms = encoding.storage === "epochSeconds" ? value * 1000 : value;
|
|
366
|
+
switch (encoding.domain) {
|
|
367
|
+
case "DateTime.Utc":
|
|
368
|
+
return DateTime.makeUnsafe(ms);
|
|
369
|
+
case "DateTime.Zoned":
|
|
370
|
+
return DateTime.makeZonedUnsafe(new Date(ms), { timeZone: "UTC" });
|
|
371
|
+
case "Date":
|
|
372
|
+
return new Date(ms);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return value;
|
|
376
|
+
};
|
|
377
|
+
return Schema.Any.pipe(Schema.decodeTo(targetSchema, {
|
|
378
|
+
decode: SchemaGetter.transformOrFail((value) => {
|
|
379
|
+
try {
|
|
380
|
+
return Effect.succeed(liftToDomain(value));
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
return Effect.fail(new SchemaIssue.InvalidType(Schema.Any.ast, Option.some(value)));
|
|
384
|
+
}
|
|
385
|
+
}),
|
|
386
|
+
encode: SchemaGetter.transformOrFail((value) => {
|
|
387
|
+
try {
|
|
388
|
+
return Effect.succeed(toWirePrimitive(value, encoding));
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
return Effect.fail(new SchemaIssue.InvalidType(Schema.Any.ast, Option.some(value)));
|
|
392
|
+
}
|
|
393
|
+
}),
|
|
394
|
+
}));
|
|
395
|
+
};
|
|
396
|
+
/**
|
|
397
|
+
* Detects `Schema.RedactedFromValue(inner)` fields (and `Schema.Redacted`).
|
|
398
|
+
* The encoded form of these schemas is "Forbidden" by Effect v4 design —
|
|
399
|
+
* encoding bails out to prevent accidental leaks. To make the round-trip work
|
|
400
|
+
* for our use case (write to DynamoDB, read back with Redacted on the domain
|
|
401
|
+
* side), we substitute with a custom transform that explicitly extracts the
|
|
402
|
+
* inner value via `Redacted.value` on encode and rewraps via `Redacted.make`
|
|
403
|
+
* on decode.
|
|
404
|
+
*
|
|
405
|
+
* Returns the inner schema (the wire-format schema) when the field is a
|
|
406
|
+
* Redacted-typed schema, otherwise undefined.
|
|
407
|
+
*/
|
|
408
|
+
const tryGetRedactedInner = (schema) => {
|
|
409
|
+
const resolved = SchemaAST.resolve(schema.ast);
|
|
410
|
+
if (resolved?.typeConstructor?._tag !== "effect/Redacted")
|
|
411
|
+
return undefined;
|
|
412
|
+
// The decoded type is `Redacted<T>` where the inner schema is the first type
|
|
413
|
+
// parameter. `firstTypeParameterAst` isolates that single untyped AST read
|
|
414
|
+
// (no public accessor exists); we then rebuild a fully-functional Schema via
|
|
415
|
+
// `Schema.make` so the result has `.pipe`.
|
|
416
|
+
const innerAst = firstTypeParameterAst(schema.ast);
|
|
417
|
+
if (!innerAst)
|
|
418
|
+
return undefined;
|
|
419
|
+
return Schema.make(innerAst);
|
|
420
|
+
};
|
|
421
|
+
/**
|
|
422
|
+
* Build a substitute Schema for a `Schema.RedactedFromValue(inner)` field.
|
|
423
|
+
*
|
|
424
|
+
* The substitute round-trips: produces `Redacted<T>` on the domain side and
|
|
425
|
+
* the inner wire primitive on the encoded side. Both directions tolerate
|
|
426
|
+
* either form on input — a user passing a plain inner value is wrapped via
|
|
427
|
+
* `Redacted.make`; a user passing a `Redacted<T>` instance is unwrapped on
|
|
428
|
+
* encode via `Redacted.value`. This permissive shape lets the put/update
|
|
429
|
+
* paths handle the mixed wire/domain payloads users typically construct
|
|
430
|
+
* (e.g. `.put({ secret: Redacted.make(...) })`).
|
|
431
|
+
*
|
|
432
|
+
* The encoded side is `Schema.Any` so decode tolerates either form; the
|
|
433
|
+
* branch logic in `decode`/`encode` then steers the value to the right
|
|
434
|
+
* representation.
|
|
435
|
+
*/
|
|
436
|
+
const buildRedactedSubstitute = (inner) => {
|
|
437
|
+
return Schema.Any.pipe(Schema.decodeTo(Schema.Redacted(inner), {
|
|
438
|
+
decode: SchemaGetter.transform((value) => Redacted.isRedacted(value) ? value : Redacted.make(value)),
|
|
439
|
+
encode: SchemaGetter.transform((r) => Redacted.isRedacted(r) ? Redacted.value(r) : r),
|
|
440
|
+
}));
|
|
441
|
+
};
|
|
442
|
+
/**
|
|
443
|
+
* Substitute model fields with bidirectional transforms where needed:
|
|
444
|
+
*
|
|
445
|
+
* - Self date schemas with an effective encoding (annotation or override)
|
|
446
|
+
* → tolerant date transform that accepts either wire form (string/number)
|
|
447
|
+
* or domain form (DateTime / Date) on decode, and always produces the
|
|
448
|
+
* configured wire primitive on encode.
|
|
449
|
+
* - `Schema.RedactedFromValue(inner)` fields → tolerant Redacted transform
|
|
450
|
+
* that accepts either form on decode and unwraps to wire primitive on
|
|
451
|
+
* encode (Effect v4's `RedactedFromValue` forbids encoding by default;
|
|
452
|
+
* our substitution makes the round-trip work for storage).
|
|
453
|
+
*
|
|
454
|
+
* Existing transform schemas (other than `RedactedFromValue`) are passed
|
|
455
|
+
* through unchanged — the user-declared transform IS the wire format.
|
|
456
|
+
*
|
|
457
|
+
* Schema.Class fields (and Schema.Class fields nested inside Schema.Array,
|
|
458
|
+
* Schema.optional, etc.) are NOT substituted here. Instead, callers run
|
|
459
|
+
* `decode → encode` against the substituted schema: `Schema.decode` is
|
|
460
|
+
* forgiving for Schema.Class (lifts plain objects to instances), and the
|
|
461
|
+
* tolerant date / Redacted substitutes make the decode pass accept both
|
|
462
|
+
* wire and domain values. The subsequent `Schema.encode` produces the
|
|
463
|
+
* canonical wire shape.
|
|
464
|
+
*
|
|
465
|
+
* @internal
|
|
466
|
+
*/
|
|
467
|
+
export const substituteSchemas = (modelFields, fieldEncodings) => {
|
|
468
|
+
const out = {};
|
|
469
|
+
for (const [name, schema] of Object.entries(modelFields)) {
|
|
470
|
+
// 1. Self-date substitution. Pattern A: user declared `Schema.DateTimeUtc`
|
|
471
|
+
// and chose a wire format via annotation. We substitute with the
|
|
472
|
+
// tolerant date transform so the encode pipeline produces the right
|
|
473
|
+
// wire primitive.
|
|
474
|
+
if (isSelfDateSchema(schema)) {
|
|
475
|
+
const enc = fieldEncodings[name];
|
|
476
|
+
if (enc) {
|
|
477
|
+
out[name] = buildDateTransform(enc);
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
// 2. RedactedFromValue substitution. Effect v4's `RedactedFromValue`
|
|
482
|
+
// forbids encoding by design; substitute with a tolerant Redacted
|
|
483
|
+
// transform so the round-trip works for storage.
|
|
484
|
+
const redactedInner = tryGetRedactedInner(schema);
|
|
485
|
+
if (redactedInner !== undefined) {
|
|
486
|
+
out[name] = buildRedactedSubstitute(redactedInner);
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
// Default: pass through unchanged. Pattern B transforms (DynamoModel.*,
|
|
490
|
+
// Schema.DateTimeUtcFromString, Schema.NumberFromString, …) own their
|
|
491
|
+
// wire format directly — `Schema.encode` handles the conversion.
|
|
492
|
+
out[name] = schema;
|
|
493
|
+
}
|
|
494
|
+
return out;
|
|
495
|
+
};
|
|
496
|
+
/**
|
|
497
|
+
* Validate that no model field combines a transform schema with a storage
|
|
498
|
+
* override. The two configuration paths are mutually exclusive: either declare
|
|
499
|
+
* a self schema (`Schema.DateTimeUtc`) and let the annotation drive storage,
|
|
500
|
+
* OR declare a transform and own the wire format yourself — not both.
|
|
501
|
+
*
|
|
502
|
+
* Two override sources are checked:
|
|
503
|
+
*
|
|
504
|
+
* 1. `ConfiguredModel.storedAs` (configured attributes) — surfaces the
|
|
505
|
+
* override via `configuredAttributes[name].encoding`.
|
|
506
|
+
*
|
|
507
|
+
* 2. `DynamoModel.storedAs(...)` modifier piped onto the field schema —
|
|
508
|
+
* adds a `DynamoEncoding` annotation directly to the AST. We detect this
|
|
509
|
+
* by checking whether the field-level annotation disagrees with the
|
|
510
|
+
* transform's actual wire format (the `inferDefaultEncoding` of the
|
|
511
|
+
* transform). For an unmodified transform (e.g. `DynamoModel.DateString`
|
|
512
|
+
* with a matching ISO-string annotation), the two agree and the field is
|
|
513
|
+
* accepted.
|
|
514
|
+
*
|
|
515
|
+
* Throws at `Entity.make()` time if the conflict is detected.
|
|
516
|
+
*
|
|
517
|
+
* @internal
|
|
518
|
+
*/
|
|
519
|
+
export const validateNoTransformOverride = (modelFields, configuredAttributes) => {
|
|
520
|
+
const conflictMessage = (name) => `[effect-dynamodb] Field "${name}": cannot apply DynamoEncoding storage override to a transform schema. ` +
|
|
521
|
+
`Either declare a self schema (Schema.DateTimeUtc) and let the annotation drive storage, OR declare a transform and own the wire format — not both.`;
|
|
522
|
+
for (const [name, schema] of Object.entries(modelFields)) {
|
|
523
|
+
if (!isDateTransform(schema))
|
|
524
|
+
continue;
|
|
525
|
+
// 1. ConfiguredModel.storedAs override?
|
|
526
|
+
const configOverride = configuredAttributes[name]?.encoding;
|
|
527
|
+
if (configOverride !== undefined) {
|
|
528
|
+
throw new Error(conflictMessage(name));
|
|
529
|
+
}
|
|
530
|
+
// 2. DynamoModel.storedAs() modifier on the schema itself? The modifier
|
|
531
|
+
// applies a `DynamoEncoding` annotation to the AST. Detect a conflict
|
|
532
|
+
// by comparing the annotation's storage with the transform's actual
|
|
533
|
+
// wire format (resolved via `transformWireKind`, which walks the typed
|
|
534
|
+
// `Base.encoding` chain to its leaf). Granularity: we can distinguish
|
|
535
|
+
// "string" vs "number" wire forms; we cannot distinguish epochMs vs
|
|
536
|
+
// epochSeconds at this layer. The string-vs-number mismatch covers the
|
|
537
|
+
// most common breaking case.
|
|
538
|
+
const annotated = getEncoding(schema);
|
|
539
|
+
if (annotated === undefined)
|
|
540
|
+
continue;
|
|
541
|
+
const transformWire = transformWireKind(schema);
|
|
542
|
+
if (transformWire === undefined)
|
|
543
|
+
continue;
|
|
544
|
+
const annotatedKind = annotated.storage === "string" ? "string" : "number";
|
|
545
|
+
if (transformWire !== annotatedKind) {
|
|
546
|
+
throw new Error(conflictMessage(name));
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
/**
|
|
551
|
+
* Read the wire form ("string" or "number") that a transform schema actually
|
|
552
|
+
* encodes to, by walking the encoding chain. Returns undefined when the chain
|
|
553
|
+
* is missing or the leaf isn't a String / Number node.
|
|
554
|
+
*
|
|
555
|
+
* Delegates to {@link SchemaAccessors.transformWireKind}, which uses the typed
|
|
556
|
+
* `Base.encoding` property and `SchemaAST.isString`/`isNumber` guards on the
|
|
557
|
+
* leaf `Link.to` AST.
|
|
558
|
+
*/
|
|
559
|
+
const transformWireKind = (schema) => transformWireKindImpl(schema);
|
|
560
|
+
export const buildDerivedSchemas = (modelFields, indexes, systemFields, resolvedRefs = [], immutableFields = new Set(), identifierField = undefined, timeSeries = undefined, fieldEncodings = {}, generatedIdField = undefined) => {
|
|
561
|
+
// --- Substitute model fields ---
|
|
562
|
+
// Self date schemas (with effective encoding) are replaced with bidirectional
|
|
563
|
+
// transforms that produce the configured wire primitive. RedactedFromValue
|
|
564
|
+
// fields are replaced with a custom bidirectional transform so encoding
|
|
565
|
+
// round-trips. All other transform schemas are passed through — the user's
|
|
566
|
+
// transform IS the wire format and we never override it.
|
|
567
|
+
const fields = substituteSchemas(modelFields, fieldEncodings);
|
|
568
|
+
// --- Model Schema: pure model fields (for input decode/encode) ---
|
|
569
|
+
const modelSchema = Schema.Struct(fields);
|
|
570
|
+
// --- Record Schema: model + system fields ---
|
|
571
|
+
// System timestamp fields use Schema.DateTimeUtcFromString as the canonical
|
|
572
|
+
// wire ↔ domain transform when they're library-managed. When the field
|
|
573
|
+
// collides with a model-declared field, the model's substituted schema is
|
|
574
|
+
// already in `fields` and we don't add a system-schema entry.
|
|
575
|
+
const systemSchemaFields = {};
|
|
576
|
+
if (systemFields.createdAt && !systemFields.createdAtCollision) {
|
|
577
|
+
systemSchemaFields[systemFields.createdAt] = systemFields.createdAtEncoding
|
|
578
|
+
? buildDateTransform(systemFields.createdAtEncoding)
|
|
579
|
+
: Schema.DateTimeUtcFromString;
|
|
580
|
+
}
|
|
581
|
+
if (systemFields.updatedAt && !systemFields.updatedAtCollision) {
|
|
582
|
+
systemSchemaFields[systemFields.updatedAt] = systemFields.updatedAtEncoding
|
|
583
|
+
? buildDateTransform(systemFields.updatedAtEncoding)
|
|
584
|
+
: Schema.DateTimeUtcFromString;
|
|
585
|
+
}
|
|
586
|
+
if (systemFields.version && !systemFields.versionCollision) {
|
|
587
|
+
systemSchemaFields[systemFields.version] = Schema.Number;
|
|
588
|
+
}
|
|
589
|
+
const recordSchema = Schema.Struct({
|
|
590
|
+
...fields,
|
|
591
|
+
...systemSchemaFields,
|
|
592
|
+
});
|
|
593
|
+
// --- System-collision handling for input/update schemas ---
|
|
594
|
+
// When the model declares a field whose name matches an active system field,
|
|
595
|
+
// the input shape changes:
|
|
596
|
+
// - createdAt / updatedAt → marked optional (user may supply a value or
|
|
597
|
+
// leave absent and let the library generate).
|
|
598
|
+
// - version → stripped entirely (optimistic locking requires library-
|
|
599
|
+
// managed increment; user-supplied values would break the mechanic).
|
|
600
|
+
// `createdAt` is also treated as immutable in the update schema.
|
|
601
|
+
const optionalOnCollide = new Set();
|
|
602
|
+
if (systemFields.createdAtCollision && systemFields.createdAt)
|
|
603
|
+
optionalOnCollide.add(systemFields.createdAt);
|
|
604
|
+
if (systemFields.updatedAtCollision && systemFields.updatedAt)
|
|
605
|
+
optionalOnCollide.add(systemFields.updatedAt);
|
|
606
|
+
// Auto-generated id: optional in input (caller may omit and let the library
|
|
607
|
+
// fill it from the Crypto service) but required in recordSchema (a decoded
|
|
608
|
+
// record always carries it). Reuses the system-collision optionality path.
|
|
609
|
+
if (generatedIdField)
|
|
610
|
+
optionalOnCollide.add(generatedIdField);
|
|
611
|
+
const strippedFromInput = new Set();
|
|
612
|
+
if (systemFields.versionCollision && systemFields.version)
|
|
613
|
+
strippedFromInput.add(systemFields.version);
|
|
614
|
+
const strippedFromUpdate = new Set(strippedFromInput);
|
|
615
|
+
if (systemFields.createdAtCollision && systemFields.createdAt)
|
|
616
|
+
strippedFromUpdate.add(systemFields.createdAt);
|
|
617
|
+
const applySystemCollisionAdjustments = (fieldName, fieldSchema, kind) => {
|
|
618
|
+
const strippedSet = kind === "update" ? strippedFromUpdate : strippedFromInput;
|
|
619
|
+
if (strippedSet.has(fieldName))
|
|
620
|
+
return null;
|
|
621
|
+
if (optionalOnCollide.has(fieldName))
|
|
622
|
+
return Schema.optional(fieldSchema);
|
|
623
|
+
return fieldSchema;
|
|
624
|
+
};
|
|
625
|
+
// --- Input Schema: ref-aware (swap ref fields for ID fields when refs present) ---
|
|
626
|
+
const refFieldSet = new Set(resolvedRefs.map((r) => r.fieldName));
|
|
627
|
+
const buildInputFieldsAcc = () => {
|
|
628
|
+
const acc = {};
|
|
629
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
630
|
+
if (refFieldSet.has(key))
|
|
631
|
+
continue;
|
|
632
|
+
const adjusted = applySystemCollisionAdjustments(key, field, "input");
|
|
633
|
+
if (adjusted !== null)
|
|
634
|
+
acc[key] = adjusted;
|
|
635
|
+
}
|
|
636
|
+
for (const ref of resolvedRefs) {
|
|
637
|
+
acc[ref.idFieldName] = ref.identifierSchema;
|
|
638
|
+
}
|
|
639
|
+
return acc;
|
|
640
|
+
};
|
|
641
|
+
const inputSchema = Schema.Struct(buildInputFieldsAcc());
|
|
642
|
+
// --- Create Schema: input fields minus identifier (or PK composites as fallback) ---
|
|
643
|
+
const pkComposites = new Set(primaryKeyComposites(indexes));
|
|
644
|
+
const createOmitFields = identifierField ? new Set([identifierField]) : pkComposites;
|
|
645
|
+
const createFieldsAcc = {};
|
|
646
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
647
|
+
if (createOmitFields.has(key))
|
|
648
|
+
continue; // skip identifier/PK composites (auto-generated)
|
|
649
|
+
if (refFieldSet.has(key))
|
|
650
|
+
continue; // skip ref fields (replaced by ID fields)
|
|
651
|
+
const adjusted = applySystemCollisionAdjustments(key, field, "input");
|
|
652
|
+
if (adjusted !== null)
|
|
653
|
+
createFieldsAcc[key] = adjusted;
|
|
654
|
+
}
|
|
655
|
+
// Add ID fields for each ref (skip if the ref field is an omitted field)
|
|
656
|
+
for (const ref of resolvedRefs) {
|
|
657
|
+
if (createOmitFields.has(ref.fieldName))
|
|
658
|
+
continue;
|
|
659
|
+
createFieldsAcc[ref.idFieldName] = ref.identifierSchema;
|
|
660
|
+
}
|
|
661
|
+
const createSchema = Schema.Struct(createFieldsAcc);
|
|
662
|
+
// --- Update Schema: optional model fields minus key composites & immutable ---
|
|
663
|
+
// Each field is wrapped in `Schema.optional` so the key may be omitted
|
|
664
|
+
// entirely (the structural composer treats absent keys as absent for GSI
|
|
665
|
+
// composition under v3's two-way classification). The field's value type
|
|
666
|
+
// matches the model declaration exactly — there is no `null` widening,
|
|
667
|
+
// because EDD-9025 prevents composites from declaring `null` and the
|
|
668
|
+
// standard update payload follows the same shape. If the model declares a
|
|
669
|
+
// non-composite attribute as `Schema.NullOr(X)`, `set({ attr: null })`
|
|
670
|
+
// still typechecks; the v1.6 `NullishOr` wrap that allowed null on
|
|
671
|
+
// arbitrary fields (regardless of model declaration) is removed.
|
|
672
|
+
const buildUpdateFieldsAcc = () => {
|
|
673
|
+
const acc = {};
|
|
674
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
675
|
+
if (pkComposites.has(key))
|
|
676
|
+
continue;
|
|
677
|
+
if (immutableFields.has(key))
|
|
678
|
+
continue;
|
|
679
|
+
if (refFieldSet.has(key))
|
|
680
|
+
continue;
|
|
681
|
+
if (strippedFromUpdate.has(key))
|
|
682
|
+
continue;
|
|
683
|
+
acc[key] = Schema.optional(field);
|
|
684
|
+
}
|
|
685
|
+
for (const ref of resolvedRefs) {
|
|
686
|
+
if (pkComposites.has(ref.fieldName))
|
|
687
|
+
continue;
|
|
688
|
+
if (immutableFields.has(ref.fieldName))
|
|
689
|
+
continue;
|
|
690
|
+
acc[ref.idFieldName] = Schema.optional(ref.identifierSchema);
|
|
691
|
+
}
|
|
692
|
+
return acc;
|
|
693
|
+
};
|
|
694
|
+
const updateSchema = Schema.Struct(buildUpdateFieldsAcc());
|
|
695
|
+
// --- Key Schema: primary key composite attributes only ---
|
|
696
|
+
const keyCompositeNames = primaryKeyComposites(indexes);
|
|
697
|
+
const keyFields = {};
|
|
698
|
+
for (const name of keyCompositeNames) {
|
|
699
|
+
const field = fields[name];
|
|
700
|
+
if (field)
|
|
701
|
+
keyFields[name] = field;
|
|
702
|
+
}
|
|
703
|
+
const keySchema = Schema.Struct(keyFields);
|
|
704
|
+
// --- Item Schema: record + key attrs (pk, sk, gsi*) + __edd_e__ ---
|
|
705
|
+
// Primary pk/sk are guaranteed present on every live item. GSI key fields
|
|
706
|
+
// are sparse: they're written only when every source composite resolves, and
|
|
707
|
+
// dropped when any composite is missing or indexPolicy evicts the index.
|
|
708
|
+
// Stream NewImage / raw query results therefore routinely lack GSI keys —
|
|
709
|
+
// mark them optional so decodeMarshalledItem decodes sparse items cleanly.
|
|
710
|
+
const primaryKeyFieldNames = new Set();
|
|
711
|
+
const primary = indexes.primary;
|
|
712
|
+
if (primary) {
|
|
713
|
+
primaryKeyFieldNames.add(primary.pk.field);
|
|
714
|
+
primaryKeyFieldNames.add(primary.sk.field);
|
|
715
|
+
}
|
|
716
|
+
const keyAttrFields = {};
|
|
717
|
+
for (const fieldName of allKeyFieldNames(indexes)) {
|
|
718
|
+
keyAttrFields[fieldName] = primaryKeyFieldNames.has(fieldName)
|
|
719
|
+
? Schema.String
|
|
720
|
+
: Schema.optional(Schema.String);
|
|
721
|
+
}
|
|
722
|
+
const itemSchema = Schema.Struct({
|
|
723
|
+
...fields,
|
|
724
|
+
...systemSchemaFields,
|
|
725
|
+
...keyAttrFields,
|
|
726
|
+
__edd_e__: Schema.String,
|
|
727
|
+
});
|
|
728
|
+
// --- Deleted Record Schema: record + deletedAt ---
|
|
729
|
+
const deletedRecordSchema = Schema.Struct({
|
|
730
|
+
...fields,
|
|
731
|
+
...systemSchemaFields,
|
|
732
|
+
deletedAt: Schema.String,
|
|
733
|
+
});
|
|
734
|
+
// --- Hidden-aware decode schemas: strip DynamoModel.Hidden fields from model/record ---
|
|
735
|
+
// These are used for "model" and "record" decode modes. Hidden fields are
|
|
736
|
+
// still present in "item" and "native" modes.
|
|
737
|
+
const hiddenFieldNames = new Set();
|
|
738
|
+
for (const [fieldName, fieldSchema] of Object.entries(modelFields)) {
|
|
739
|
+
if (isHidden(fieldSchema))
|
|
740
|
+
hiddenFieldNames.add(fieldName);
|
|
741
|
+
}
|
|
742
|
+
const hasHiddenFields = hiddenFieldNames.size > 0;
|
|
743
|
+
const modelVisibleSchema = hasHiddenFields
|
|
744
|
+
? Schema.Struct(Object.fromEntries(Object.entries(fields).filter(([k]) => !hiddenFieldNames.has(k))))
|
|
745
|
+
: modelSchema;
|
|
746
|
+
const recordVisibleSchema = hasHiddenFields
|
|
747
|
+
? Schema.Struct(Object.fromEntries(Object.entries({
|
|
748
|
+
...fields,
|
|
749
|
+
...systemSchemaFields,
|
|
750
|
+
}).filter(([k]) => !hiddenFieldNames.has(k))))
|
|
751
|
+
: recordSchema;
|
|
752
|
+
// --- Time-series derived schemas ---
|
|
753
|
+
// `appendInputSchema`: the user-supplied schema, with the same substitution
|
|
754
|
+
// rules applied as the model fields (self date schemas → tolerant
|
|
755
|
+
// transforms, RedactedFromValue → tolerant Redacted). `Entity.make()`
|
|
756
|
+
// enforces that every PK/SK composite + `orderBy` appears in its fields.
|
|
757
|
+
// `historyRecordSchema`: event-item shape. Substituted model fields + no
|
|
758
|
+
// system fields (events are point-in-time facts keyed by `orderBy`).
|
|
759
|
+
const timeSeriesEnabled = timeSeries !== undefined && timeSeries !== null;
|
|
760
|
+
const appendInputSchema = timeSeriesEnabled
|
|
761
|
+
? (() => {
|
|
762
|
+
const userSchema = timeSeries.appendInput;
|
|
763
|
+
const userFields = getSchemaFields(userSchema);
|
|
764
|
+
if (!userFields)
|
|
765
|
+
return userSchema;
|
|
766
|
+
// Compute encodings for the appendInput fields (annotation +
|
|
767
|
+
// inferred from typeConstructor for self schemas).
|
|
768
|
+
const appendEncodings = buildFieldEncodings(userFields, {});
|
|
769
|
+
const subbedFields = substituteSchemas(userFields, appendEncodings);
|
|
770
|
+
return Schema.Struct(subbedFields);
|
|
771
|
+
})()
|
|
772
|
+
: null;
|
|
773
|
+
const historyRecordSchema = timeSeriesEnabled
|
|
774
|
+
? (() => {
|
|
775
|
+
const hiddenFilter = (record) => hasHiddenFields
|
|
776
|
+
? Object.fromEntries(Object.entries(record).filter(([k]) => !hiddenFieldNames.has(k)))
|
|
777
|
+
: record;
|
|
778
|
+
return Schema.Struct(hiddenFilter({ ...fields }));
|
|
779
|
+
})()
|
|
780
|
+
: null;
|
|
781
|
+
return {
|
|
782
|
+
modelSchema: modelVisibleSchema,
|
|
783
|
+
recordSchema: recordVisibleSchema,
|
|
784
|
+
inputSchema: inputSchema,
|
|
785
|
+
createSchema: createSchema,
|
|
786
|
+
updateSchema: updateSchema,
|
|
787
|
+
keySchema: keySchema,
|
|
788
|
+
itemSchema: itemSchema,
|
|
789
|
+
deletedRecordSchema: deletedRecordSchema,
|
|
790
|
+
appendInputSchema: appendInputSchema,
|
|
791
|
+
historyRecordSchema: historyRecordSchema,
|
|
792
|
+
};
|
|
793
|
+
};
|
|
794
|
+
// ---------------------------------------------------------------------------
|
|
795
|
+
// Unique constraint field resolver
|
|
796
|
+
// ---------------------------------------------------------------------------
|
|
797
|
+
export const resolveUniqueFields = (def) => Array.isArray(def) ? def : def.fields;
|
|
798
|
+
//# sourceMappingURL=EntitySchemas.js.map
|