effect-dynamodb 1.1.0 → 1.3.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/DynamoClient.d.ts +1 -1
- package/dist/DynamoClient.d.ts.map +1 -1
- package/dist/Entity.d.ts +54 -37
- package/dist/Entity.d.ts.map +1 -1
- package/dist/Entity.js +195 -121
- package/dist/Entity.js.map +1 -1
- package/dist/KeyComposer.d.ts +67 -17
- package/dist/KeyComposer.d.ts.map +1 -1
- package/dist/KeyComposer.js +67 -39
- package/dist/KeyComposer.js.map +1 -1
- package/dist/Marshaller.d.ts.map +1 -1
- package/dist/Marshaller.js +1 -1
- package/dist/Marshaller.js.map +1 -1
- package/dist/internal/BoundCrud.d.ts +181 -0
- package/dist/internal/BoundCrud.d.ts.map +1 -0
- package/dist/internal/BoundCrud.js +160 -0
- package/dist/internal/BoundCrud.js.map +1 -0
- package/dist/internal/EntitySchemas.d.ts +47 -2
- package/dist/internal/EntitySchemas.d.ts.map +1 -1
- package/dist/internal/EntitySchemas.js +154 -37
- package/dist/internal/EntitySchemas.js.map +1 -1
- package/dist/internal/EntityTypes.d.ts +56 -15
- package/dist/internal/EntityTypes.d.ts.map +1 -1
- package/dist/internal/TransactableOps.d.ts.map +1 -1
- package/dist/internal/TransactableOps.js +38 -11
- package/dist/internal/TransactableOps.js.map +1 -1
- package/package.json +1 -1
package/dist/Entity.js
CHANGED
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
* carries derived schemas for 7 type extractors (Model, Record, Input, Update,
|
|
7
7
|
* Key, Item, Marshalled).
|
|
8
8
|
*/
|
|
9
|
-
import { Duration, Effect, Schema, Stream } from "effect";
|
|
9
|
+
import { DateTime, Duration, Effect, Schema, Stream } from "effect";
|
|
10
10
|
import { DynamoClient } from "./DynamoClient.js";
|
|
11
11
|
import { getEncoding, getIdentifierField, isConfiguredModel, isHidden, isRef, isRefField, } from "./DynamoModel.js";
|
|
12
12
|
import * as DynamoSchema from "./DynamoSchema.js";
|
|
13
13
|
import { CascadePartialFailure, ConditionalCheckFailed, ItemNotFound, isAwsConditionalCheckFailed, isAwsTransactionCancelled, OptimisticLockError, RefNotFound, TransactionOverflow, UniqueConstraintViolation, ValidationError, } from "./Errors.js";
|
|
14
|
+
import { makeBoundDelete, makeBoundPut, makeBoundUpdate } from "./internal/BoundCrud.js";
|
|
14
15
|
import { BoundQueryImpl } from "./internal/BoundQuery.js";
|
|
15
16
|
import { compileExpr, createConditionOps, isExpr, parseShorthand, parseSimpleShorthand, } from "./internal/Expr.js";
|
|
16
17
|
import { compilePath, createPathBuilder } from "./internal/PathBuilder.js";
|
|
@@ -100,7 +101,20 @@ const makeImpl = (config) => {
|
|
|
100
101
|
const isSchemaClass = typeof rawModel === "function";
|
|
101
102
|
const modelFields = getFields(rawModel);
|
|
102
103
|
const hasHiddenFields = Object.values(modelFields).some(isHidden);
|
|
103
|
-
const systemFields = resolveSystemFields(config.timestamps, config.versioned, config.timeSeries);
|
|
104
|
+
const systemFields = resolveSystemFields(config.timestamps, config.versioned, config.timeSeries, modelFields, configuredAttributes);
|
|
105
|
+
// System-field collision policy:
|
|
106
|
+
// - Timestamp collisions with a non-date model field silently yield the
|
|
107
|
+
// field to the user (see `resolveSystemFields`).
|
|
108
|
+
// - `version` collision is allowed. The field is stripped from
|
|
109
|
+
// `inputSchema` / `createSchema` / `updateSchema` (see
|
|
110
|
+
// `buildDerivedSchemas`), so user code cannot supply it via `put` /
|
|
111
|
+
// `create` / `.set()`. Library continues to own the write path
|
|
112
|
+
// (auto-increment + `.expectedVersion()` optimistic locking). The model
|
|
113
|
+
// declaration is purely for type-level visibility — callers can
|
|
114
|
+
// construct Schema.Class instances and read `version` off the record.
|
|
115
|
+
// - Manual version fixups remain available by dropping to the raw
|
|
116
|
+
// `DynamoClient` service; they deliberately do not appear on the
|
|
117
|
+
// `Entity` surface to keep the locking invariant visibly inviolable.
|
|
104
118
|
// ---------------------------------------------------------------------------
|
|
105
119
|
// Validate indexes
|
|
106
120
|
// ---------------------------------------------------------------------------
|
|
@@ -415,6 +429,10 @@ const makeImpl = (config) => {
|
|
|
415
429
|
/**
|
|
416
430
|
* Generate a timestamp value appropriate for the field's encoding.
|
|
417
431
|
* Default (no encoding): ISO string. Custom encoding: serialized primitive.
|
|
432
|
+
*
|
|
433
|
+
* Used for non-colliding system fields (field not declared in model) — the
|
|
434
|
+
* value is written straight into the DynamoDB item and is NOT routed through
|
|
435
|
+
* `serializeDateFields` (because the field is absent from `fieldEncodings`).
|
|
418
436
|
*/
|
|
419
437
|
const generateTimestamp = (encoding) => {
|
|
420
438
|
if (!encoding)
|
|
@@ -422,6 +440,22 @@ const makeImpl = (config) => {
|
|
|
422
440
|
const now = new Date();
|
|
423
441
|
return serializeDateForDynamo(now, encoding);
|
|
424
442
|
};
|
|
443
|
+
/**
|
|
444
|
+
* Generate a timestamp as a domain value (DateTime.Utc / DateTime.Zoned / Date),
|
|
445
|
+
* matching the field's declared domain. Used for system fields that COLLIDE
|
|
446
|
+
* with a model-declared field — the value flows through `serializeDateFields`
|
|
447
|
+
* (which requires a domain value) alongside any user-supplied override.
|
|
448
|
+
*/
|
|
449
|
+
const generateDomainTimestamp = (encoding) => {
|
|
450
|
+
switch (encoding.domain) {
|
|
451
|
+
case "DateTime.Utc":
|
|
452
|
+
return DateTime.makeUnsafe(new Date());
|
|
453
|
+
case "DateTime.Zoned":
|
|
454
|
+
return DateTime.makeZonedUnsafe(new Date(), { timeZone: "UTC" });
|
|
455
|
+
case "Date":
|
|
456
|
+
return new Date();
|
|
457
|
+
}
|
|
458
|
+
};
|
|
425
459
|
// ---------------------------------------------------------------------------
|
|
426
460
|
// Lifecycle config helpers
|
|
427
461
|
// ---------------------------------------------------------------------------
|
|
@@ -710,7 +744,7 @@ const makeImpl = (config) => {
|
|
|
710
744
|
const client = yield* DynamoClient;
|
|
711
745
|
const { name: tableName } = yield* tableTag;
|
|
712
746
|
// Decode input
|
|
713
|
-
const decodedInput = yield* Schema.decodeUnknownEffect(schemas.
|
|
747
|
+
const decodedInput = yield* Schema.decodeUnknownEffect(schemas.inputDecodeSchema)(input).pipe(Effect.mapError((cause) => new ValidationError({
|
|
714
748
|
entityType,
|
|
715
749
|
operation: "put.decode",
|
|
716
750
|
cause,
|
|
@@ -723,11 +757,29 @@ const makeImpl = (config) => {
|
|
|
723
757
|
: decodedInput;
|
|
724
758
|
// Build the DynamoDB item
|
|
725
759
|
const item = { ...decoded };
|
|
726
|
-
// Add system fields
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
if
|
|
730
|
-
|
|
760
|
+
// Add system fields. When a timestamp field collides with a
|
|
761
|
+
// model-declared field, the user may have supplied their own value
|
|
762
|
+
// (see `inputDecodeSchema` — optional on collision). Respect their
|
|
763
|
+
// value if present; otherwise generate. In the collision path the
|
|
764
|
+
// value is a DOMAIN value (DateTime.Utc / Date / ...) so it flows
|
|
765
|
+
// through `serializeDateFields` downstream. Non-colliding fields
|
|
766
|
+
// get a storage primitive directly (no `serializeDateFields` pass).
|
|
767
|
+
if (systemFields.createdAt) {
|
|
768
|
+
if (item[systemFields.createdAt] === undefined) {
|
|
769
|
+
item[systemFields.createdAt] =
|
|
770
|
+
systemFields.createdAtCollision && systemFields.createdAtEncoding
|
|
771
|
+
? generateDomainTimestamp(systemFields.createdAtEncoding)
|
|
772
|
+
: generateTimestamp(systemFields.createdAtEncoding);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (systemFields.updatedAt) {
|
|
776
|
+
if (item[systemFields.updatedAt] === undefined) {
|
|
777
|
+
item[systemFields.updatedAt] =
|
|
778
|
+
systemFields.updatedAtCollision && systemFields.updatedAtEncoding
|
|
779
|
+
? generateDomainTimestamp(systemFields.updatedAtEncoding)
|
|
780
|
+
: generateTimestamp(systemFields.updatedAtEncoding);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
731
783
|
if (systemFields.version)
|
|
732
784
|
item[systemFields.version] = 1;
|
|
733
785
|
// Add entity type discriminator
|
|
@@ -935,7 +987,7 @@ const makeImpl = (config) => {
|
|
|
935
987
|
cause,
|
|
936
988
|
})));
|
|
937
989
|
// Decode updates
|
|
938
|
-
const decodedUpdates = yield* Schema.decodeUnknownEffect(schemas.
|
|
990
|
+
const decodedUpdates = yield* Schema.decodeUnknownEffect(schemas.updateDecodeSchema)(updates ?? {}).pipe(Effect.mapError((cause) => new ValidationError({
|
|
939
991
|
entityType,
|
|
940
992
|
operation: "update.decodeUpdates",
|
|
941
993
|
cause,
|
|
@@ -1031,12 +1083,19 @@ const makeImpl = (config) => {
|
|
|
1031
1083
|
}
|
|
1032
1084
|
}
|
|
1033
1085
|
}
|
|
1034
|
-
// Increment version and update timestamp
|
|
1086
|
+
// Increment version and update timestamp. If the caller supplied a
|
|
1087
|
+
// value for `updatedAt` (allowed when the field collides with a
|
|
1088
|
+
// model-declared field), respect it; else generate.
|
|
1035
1089
|
const newVersion = currentVersion + 1;
|
|
1036
1090
|
if (systemFields.version)
|
|
1037
1091
|
newItem[systemFields.version] = newVersion;
|
|
1038
|
-
if (systemFields.updatedAt)
|
|
1039
|
-
|
|
1092
|
+
if (systemFields.updatedAt) {
|
|
1093
|
+
const userSupplied = hydratedUpdates[systemFields.updatedAt];
|
|
1094
|
+
newItem[systemFields.updatedAt] =
|
|
1095
|
+
userSupplied !== undefined
|
|
1096
|
+
? userSupplied
|
|
1097
|
+
: generateTimestamp(systemFields.updatedAtEncoding);
|
|
1098
|
+
}
|
|
1040
1099
|
// Convert DynamoDB attribute names to domain names for key composition
|
|
1041
1100
|
// and sentinel value comparison (newItem was built from currentRaw which
|
|
1042
1101
|
// uses DynamoDB names; composites and unique fields use domain names)
|
|
@@ -1202,10 +1261,22 @@ const makeImpl = (config) => {
|
|
|
1202
1261
|
const names = {};
|
|
1203
1262
|
const values = {};
|
|
1204
1263
|
let counter = 0;
|
|
1264
|
+
// Serialize any domain-value date fields in updates to storage primitives.
|
|
1265
|
+
// Without this, a `.set({ occurredAt: DateTime.Utc })` would marshal as a
|
|
1266
|
+
// class-instance map.
|
|
1267
|
+
const serializedUpdates = { ...hydratedUpdates };
|
|
1268
|
+
serializeDateFields(serializedUpdates);
|
|
1269
|
+
// System-colliding updatedAt is handled by the system-field block below.
|
|
1270
|
+
// createdAt and version are already excluded by `updateDecodeSchema`.
|
|
1271
|
+
const updateSystemColliders = new Set();
|
|
1272
|
+
if (systemFields.updatedAtCollision && systemFields.updatedAt)
|
|
1273
|
+
updateSystemColliders.add(systemFields.updatedAt);
|
|
1205
1274
|
// Add user-provided updates
|
|
1206
|
-
for (const [attr, val] of Object.entries(
|
|
1275
|
+
for (const [attr, val] of Object.entries(serializedUpdates)) {
|
|
1207
1276
|
if (val === undefined)
|
|
1208
1277
|
continue;
|
|
1278
|
+
if (updateSystemColliders.has(attr))
|
|
1279
|
+
continue;
|
|
1209
1280
|
const nameKey = `#u${counter}`;
|
|
1210
1281
|
const valKey = `:u${counter}`;
|
|
1211
1282
|
names[nameKey] = resolveDbName(attr);
|
|
@@ -1213,52 +1284,16 @@ const makeImpl = (config) => {
|
|
|
1213
1284
|
setClauses.push(`${nameKey} = ${valKey}`);
|
|
1214
1285
|
counter++;
|
|
1215
1286
|
}
|
|
1216
|
-
//
|
|
1217
|
-
|
|
1218
|
-
const pkCompositeSet = new Set(primaryKeyComposites(config.indexes));
|
|
1219
|
-
// Compose GSI keys for indexes whose composites are in the update,
|
|
1220
|
-
// filtering out any GSI whose composites are targeted by REMOVE
|
|
1221
|
-
const indexesForGsiUpdate = removedSet
|
|
1222
|
-
? (() => {
|
|
1223
|
-
const filtered = {};
|
|
1224
|
-
for (const [indexName, indexDef] of Object.entries(allIndexes)) {
|
|
1225
|
-
if (!("index" in indexDef)) {
|
|
1226
|
-
filtered[indexName] = indexDef;
|
|
1227
|
-
continue;
|
|
1228
|
-
}
|
|
1229
|
-
const gsiComposites = [...indexDef.pk.composite, ...indexDef.sk.composite].filter((attr) => !pkCompositeSet.has(attr));
|
|
1230
|
-
if (!gsiComposites.some((attr) => removedSet.has(attr))) {
|
|
1231
|
-
filtered[indexName] = indexDef;
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
return filtered;
|
|
1235
|
-
})()
|
|
1236
|
-
: allIndexes;
|
|
1237
|
-
let gsiKeys;
|
|
1238
|
-
try {
|
|
1239
|
-
gsiKeys = KeyComposer.composeGsiKeysForUpdate(schema, entityType, entityVersion, indexesForGsiUpdate, hydratedUpdates, decodedKey);
|
|
1240
|
-
}
|
|
1241
|
-
catch (e) {
|
|
1242
|
-
return yield* new ValidationError({
|
|
1243
|
-
entityType,
|
|
1244
|
-
operation: "update.gsiComposites",
|
|
1245
|
-
cause: e instanceof Error ? e.message : e,
|
|
1246
|
-
});
|
|
1247
|
-
}
|
|
1248
|
-
for (const [field, value] of Object.entries(gsiKeys)) {
|
|
1249
|
-
const nameKey = `#u${counter}`;
|
|
1250
|
-
const valKey = `:u${counter}`;
|
|
1251
|
-
names[nameKey] = field;
|
|
1252
|
-
values[valKey] = toAttributeValue(value);
|
|
1253
|
-
setClauses.push(`${nameKey} = ${valKey}`);
|
|
1254
|
-
counter++;
|
|
1255
|
-
}
|
|
1256
|
-
// Add updatedAt timestamp (encoding-aware)
|
|
1287
|
+
// Add updatedAt timestamp. User-supplied value wins (pre-serialized
|
|
1288
|
+
// above); else fall back to a freshly generated storage primitive.
|
|
1257
1289
|
if (systemFields.updatedAt) {
|
|
1290
|
+
const userSupplied = serializedUpdates[systemFields.updatedAt];
|
|
1258
1291
|
const nameKey = `#u${counter}`;
|
|
1259
1292
|
const valKey = `:u${counter}`;
|
|
1260
1293
|
names[nameKey] = systemFields.updatedAt;
|
|
1261
|
-
values[valKey] = toAttributeValue(
|
|
1294
|
+
values[valKey] = toAttributeValue(userSupplied !== undefined
|
|
1295
|
+
? userSupplied
|
|
1296
|
+
: generateTimestamp(systemFields.updatedAtEncoding));
|
|
1262
1297
|
setClauses.push(`${nameKey} = ${valKey}`);
|
|
1263
1298
|
counter++;
|
|
1264
1299
|
}
|
|
@@ -1297,6 +1332,7 @@ const makeImpl = (config) => {
|
|
|
1297
1332
|
}
|
|
1298
1333
|
}
|
|
1299
1334
|
// REMOVE
|
|
1335
|
+
const removedSet = uState.remove ? new Set(uState.remove) : undefined;
|
|
1300
1336
|
if (uState.remove) {
|
|
1301
1337
|
for (const attr of uState.remove) {
|
|
1302
1338
|
const nameKey = `#r${removeClauses.length}`;
|
|
@@ -1304,22 +1340,23 @@ const makeImpl = (config) => {
|
|
|
1304
1340
|
removeClauses.push(nameKey);
|
|
1305
1341
|
}
|
|
1306
1342
|
}
|
|
1307
|
-
//
|
|
1308
|
-
//
|
|
1309
|
-
//
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1343
|
+
// Policy-aware GSI key composition. One call covers SETs (full
|
|
1344
|
+
// recompose or half-wise), sparse dropout REMOVEs, and cascade
|
|
1345
|
+
// REMOVEs when an Entity.remove() targets a GSI composite.
|
|
1346
|
+
// See DESIGN.md §7 Policy-Aware GSI Composition.
|
|
1347
|
+
const gsiUpdate = KeyComposer.composeGsiKeysForUpdatePolicyAware(schema, entityType, entityVersion, allIndexes, hydratedUpdates, decodedKey, { removedSet });
|
|
1348
|
+
for (const [field, value] of Object.entries(gsiUpdate.sets)) {
|
|
1349
|
+
const nameKey = `#u${counter}`;
|
|
1350
|
+
const valKey = `:u${counter}`;
|
|
1351
|
+
names[nameKey] = field;
|
|
1352
|
+
values[valKey] = toAttributeValue(value);
|
|
1353
|
+
setClauses.push(`${nameKey} = ${valKey}`);
|
|
1354
|
+
counter++;
|
|
1355
|
+
}
|
|
1356
|
+
for (const keyField of gsiUpdate.removes) {
|
|
1357
|
+
const nameKey = `#r${removeClauses.length}`;
|
|
1358
|
+
names[nameKey] = keyField;
|
|
1359
|
+
removeClauses.push(nameKey);
|
|
1323
1360
|
}
|
|
1324
1361
|
// ADD (atomic numeric increment / set addition)
|
|
1325
1362
|
if (uState.add) {
|
|
@@ -1677,7 +1714,7 @@ const makeImpl = (config) => {
|
|
|
1677
1714
|
const client = yield* DynamoClient;
|
|
1678
1715
|
const { name: tableName } = yield* tableTag;
|
|
1679
1716
|
// Decode input
|
|
1680
|
-
const decodedInput = yield* Schema.decodeUnknownEffect(schemas.
|
|
1717
|
+
const decodedInput = yield* Schema.decodeUnknownEffect(schemas.inputDecodeSchema)(input).pipe(Effect.mapError((cause) => new ValidationError({
|
|
1681
1718
|
entityType,
|
|
1682
1719
|
operation: "upsert.decode",
|
|
1683
1720
|
cause,
|
|
@@ -1697,6 +1734,15 @@ const makeImpl = (config) => {
|
|
|
1697
1734
|
let counter = 0;
|
|
1698
1735
|
// Determine primary key composite attribute names
|
|
1699
1736
|
const pkComposites = new Set(primaryKeyComposites(config.indexes));
|
|
1737
|
+
// System-colliding fields are written below in the system-field block
|
|
1738
|
+
// (so their semantics — if_not_exists / always-set — stay consistent).
|
|
1739
|
+
const systemColliders = new Set();
|
|
1740
|
+
if (systemFields.createdAtCollision && systemFields.createdAt)
|
|
1741
|
+
systemColliders.add(systemFields.createdAt);
|
|
1742
|
+
if (systemFields.updatedAtCollision && systemFields.updatedAt)
|
|
1743
|
+
systemColliders.add(systemFields.updatedAt);
|
|
1744
|
+
if (systemFields.versionCollision && systemFields.version)
|
|
1745
|
+
systemColliders.add(systemFields.version);
|
|
1700
1746
|
// Serialize date fields on the decoded item
|
|
1701
1747
|
if (hasDateFields) {
|
|
1702
1748
|
serializeDateFields(item);
|
|
@@ -1705,6 +1751,8 @@ const makeImpl = (config) => {
|
|
|
1705
1751
|
for (const [attr, val] of Object.entries(item)) {
|
|
1706
1752
|
if (pkComposites.has(attr))
|
|
1707
1753
|
continue;
|
|
1754
|
+
if (systemColliders.has(attr))
|
|
1755
|
+
continue;
|
|
1708
1756
|
if (val === undefined)
|
|
1709
1757
|
continue;
|
|
1710
1758
|
const nameKey = `#u${counter}`;
|
|
@@ -1743,21 +1791,29 @@ const makeImpl = (config) => {
|
|
|
1743
1791
|
setClauses.push(`${nameKey} = ${valKey}`);
|
|
1744
1792
|
counter++;
|
|
1745
1793
|
}
|
|
1746
|
-
// Add createdAt with if_not_exists — only set on first create
|
|
1794
|
+
// Add createdAt with if_not_exists — only set on first create.
|
|
1795
|
+
// User-supplied value wins (domain input → already serialized above);
|
|
1796
|
+
// else fall back to a freshly generated storage primitive.
|
|
1747
1797
|
if (systemFields.createdAt) {
|
|
1798
|
+
const userSupplied = item[systemFields.createdAt];
|
|
1748
1799
|
const nameKey = `#u${counter}`;
|
|
1749
1800
|
const valKey = `:u${counter}`;
|
|
1750
1801
|
names[nameKey] = systemFields.createdAt;
|
|
1751
|
-
values[valKey] = toAttributeValue(
|
|
1802
|
+
values[valKey] = toAttributeValue(userSupplied !== undefined
|
|
1803
|
+
? userSupplied
|
|
1804
|
+
: generateTimestamp(systemFields.createdAtEncoding));
|
|
1752
1805
|
setClauses.push(`${nameKey} = if_not_exists(${nameKey}, ${valKey})`);
|
|
1753
1806
|
counter++;
|
|
1754
1807
|
}
|
|
1755
|
-
// Add updatedAt — always set to current time
|
|
1808
|
+
// Add updatedAt — user-supplied wins, else always set to current time.
|
|
1756
1809
|
if (systemFields.updatedAt) {
|
|
1810
|
+
const userSupplied = item[systemFields.updatedAt];
|
|
1757
1811
|
const nameKey = `#u${counter}`;
|
|
1758
1812
|
const valKey = `:u${counter}`;
|
|
1759
1813
|
names[nameKey] = systemFields.updatedAt;
|
|
1760
|
-
values[valKey] = toAttributeValue(
|
|
1814
|
+
values[valKey] = toAttributeValue(userSupplied !== undefined
|
|
1815
|
+
? userSupplied
|
|
1816
|
+
: generateTimestamp(systemFields.updatedAtEncoding));
|
|
1761
1817
|
setClauses.push(`${nameKey} = ${valKey}`);
|
|
1762
1818
|
counter++;
|
|
1763
1819
|
}
|
|
@@ -1874,29 +1930,48 @@ const makeImpl = (config) => {
|
|
|
1874
1930
|
counter++;
|
|
1875
1931
|
}
|
|
1876
1932
|
// GSI key recomposition: any GSI whose composites are touched by input.
|
|
1877
|
-
// Reuses the
|
|
1878
|
-
// from the update payload
|
|
1879
|
-
// during an append. Non-PK composites
|
|
1880
|
-
//
|
|
1881
|
-
// set (partial presence raises `PartialGsiCompositeError`).
|
|
1933
|
+
// Reuses the policy-aware helper from `.update()`. PK composites are
|
|
1934
|
+
// excluded from the update payload — they're already in the key and
|
|
1935
|
+
// never change during an append. Non-PK composites in appendInput that
|
|
1936
|
+
// overlap a GSI's composites trigger per-GSI resolution via indexPolicy.
|
|
1882
1937
|
const nonPkAppendFields = {};
|
|
1883
1938
|
for (const [attr, val] of Object.entries(decoded)) {
|
|
1884
1939
|
if (!pkCompositeSet.has(attr)) {
|
|
1885
1940
|
nonPkAppendFields[attr] = val;
|
|
1886
1941
|
}
|
|
1887
1942
|
}
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1943
|
+
// At append-time, indexPolicy is restricted to composites that are
|
|
1944
|
+
// members of appendInput. Non-appendInput composites are owned by
|
|
1945
|
+
// other writers and are by contract never touched by .append(); their
|
|
1946
|
+
// policy cannot fire at append-time. Each GSI's indexPolicy return is
|
|
1947
|
+
// filtered to appendInput attrs only — non-appendInput keys fall through
|
|
1948
|
+
// to the implicit `"preserve"` default, leaving the half containing
|
|
1949
|
+
// them untouched.
|
|
1950
|
+
const appendInputFieldSet = new Set(Object.keys(appendInputSchema
|
|
1951
|
+
.fields ?? {}));
|
|
1952
|
+
const policyAwareIndexes = {};
|
|
1953
|
+
for (const [indexName, indexDef] of Object.entries(allIndexes)) {
|
|
1954
|
+
if (!indexDef.indexPolicy) {
|
|
1955
|
+
policyAwareIndexes[indexName] = indexDef;
|
|
1956
|
+
continue;
|
|
1957
|
+
}
|
|
1958
|
+
const originalPolicy = indexDef.indexPolicy;
|
|
1959
|
+
policyAwareIndexes[indexName] = {
|
|
1960
|
+
...indexDef,
|
|
1961
|
+
indexPolicy: (item) => {
|
|
1962
|
+
const raw = originalPolicy(item);
|
|
1963
|
+
const filtered = {};
|
|
1964
|
+
for (const [attr, policy] of Object.entries(raw)) {
|
|
1965
|
+
if (appendInputFieldSet.has(attr) && policy !== undefined) {
|
|
1966
|
+
filtered[attr] = policy;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
return filtered;
|
|
1970
|
+
},
|
|
1971
|
+
};
|
|
1898
1972
|
}
|
|
1899
|
-
|
|
1973
|
+
const gsiUpdate = KeyComposer.composeGsiKeysForUpdatePolicyAware(schema, entityType, entityVersion, policyAwareIndexes, nonPkAppendFields, decoded);
|
|
1974
|
+
for (const [field, value] of Object.entries(gsiUpdate.sets)) {
|
|
1900
1975
|
const nameKey = `#a${counter}`;
|
|
1901
1976
|
const valKey = `:a${counter}`;
|
|
1902
1977
|
names[nameKey] = field;
|
|
@@ -1904,6 +1979,13 @@ const makeImpl = (config) => {
|
|
|
1904
1979
|
setClauses.push(`${nameKey} = ${valKey}`);
|
|
1905
1980
|
counter++;
|
|
1906
1981
|
}
|
|
1982
|
+
const appendRemoveClauses = [];
|
|
1983
|
+
for (const keyField of gsiUpdate.removes) {
|
|
1984
|
+
const nameKey = `#a${counter}`;
|
|
1985
|
+
names[nameKey] = keyField;
|
|
1986
|
+
appendRemoveClauses.push(nameKey);
|
|
1987
|
+
counter++;
|
|
1988
|
+
}
|
|
1907
1989
|
// Entity type discriminator — idempotent; ensures existing items
|
|
1908
1990
|
// without __edd_e__ still get tagged on first append.
|
|
1909
1991
|
{
|
|
@@ -1940,7 +2022,12 @@ const makeImpl = (config) => {
|
|
|
1940
2022
|
Object.assign(names, uc.names);
|
|
1941
2023
|
Object.assign(values, uc.values);
|
|
1942
2024
|
}
|
|
1943
|
-
const
|
|
2025
|
+
const exprParts = [];
|
|
2026
|
+
if (setClauses.length > 0)
|
|
2027
|
+
exprParts.push(`SET ${setClauses.join(", ")}`);
|
|
2028
|
+
if (appendRemoveClauses.length > 0)
|
|
2029
|
+
exprParts.push(`REMOVE ${appendRemoveClauses.join(", ")}`);
|
|
2030
|
+
const updateExpression = exprParts.join(" ");
|
|
1944
2031
|
// ---------- Build Put of event item ----------
|
|
1945
2032
|
const eventSk = KeyComposer.composeEventSk(currentSk, newObValue, schema.casing);
|
|
1946
2033
|
const eventItem = { ...decoded };
|
|
@@ -2505,6 +2592,7 @@ const makeImpl = (config) => {
|
|
|
2505
2592
|
_resolvedRefs: resolvedRefs,
|
|
2506
2593
|
/** @internal Full decode pipeline: rename + date deser + schema decode. Used by Batch/Aggregate. */
|
|
2507
2594
|
_decodeRecord: decodeRecord,
|
|
2595
|
+
_serializeDateFields: serializeDateFields,
|
|
2508
2596
|
_attachPrototype: attachPrototype,
|
|
2509
2597
|
_configure: (injectedSchema, injectedTableTag) => {
|
|
2510
2598
|
schema = injectedSchema;
|
|
@@ -2572,26 +2660,12 @@ const makeImpl = (config) => {
|
|
|
2572
2660
|
export const bind = (entity) => Effect.gen(function* () {
|
|
2573
2661
|
const ctx = yield* Effect.context();
|
|
2574
2662
|
const provide = (effect) => Effect.provide(effect, ctx);
|
|
2575
|
-
//
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
};
|
|
2582
|
-
// Helper: apply combinators to an EntityPut, then run in record mode + provide
|
|
2583
|
-
const applyPut = (op, combinators) => {
|
|
2584
|
-
let result = op;
|
|
2585
|
-
for (const fn of combinators)
|
|
2586
|
-
result = fn(result);
|
|
2587
|
-
return provide(result._run("record"));
|
|
2588
|
-
};
|
|
2589
|
-
// Helper: apply combinators to an EntityDelete, then asEffect + provide
|
|
2590
|
-
const applyDelete = (op, combinators) => {
|
|
2591
|
-
let result = op;
|
|
2592
|
-
for (const fn of combinators)
|
|
2593
|
-
result = fn(result);
|
|
2594
|
-
return provide(result.asEffect());
|
|
2663
|
+
// Shared config for the bound-CRUD builders — pre-resolved services plus
|
|
2664
|
+
// a typed PathBuilder/ConditionOps so `.condition((t, ops) => ...)` works.
|
|
2665
|
+
const boundCrudConfig = {
|
|
2666
|
+
pathBuilder: createPathBuilder(),
|
|
2667
|
+
conditionOps: createConditionOps(),
|
|
2668
|
+
provide,
|
|
2595
2669
|
};
|
|
2596
2670
|
// Helper: apply query combinators then execute
|
|
2597
2671
|
const applyQuery = (q, combinators) => {
|
|
@@ -2613,15 +2687,15 @@ export const bind = (entity) => Effect.gen(function* () {
|
|
|
2613
2687
|
return new BoundQueryImpl(q, bqConfig);
|
|
2614
2688
|
};
|
|
2615
2689
|
return {
|
|
2616
|
-
// CRUD —
|
|
2690
|
+
// CRUD — fluent bound builders (yieldable, no .run() terminal)
|
|
2617
2691
|
get: (key) => provide(entity.get(key)._run("record")),
|
|
2618
|
-
put: (input
|
|
2619
|
-
create: (input
|
|
2620
|
-
update: (key
|
|
2621
|
-
delete: (key
|
|
2622
|
-
upsert: (input
|
|
2623
|
-
patch: (key
|
|
2624
|
-
deleteIfExists: (key
|
|
2692
|
+
put: (input) => makeBoundPut(entity.put(input), boundCrudConfig),
|
|
2693
|
+
create: (input) => makeBoundPut(entity.create(input), boundCrudConfig),
|
|
2694
|
+
update: (key) => makeBoundUpdate(entity.update(key), boundCrudConfig),
|
|
2695
|
+
delete: (key) => makeBoundDelete(entity.delete(key), boundCrudConfig),
|
|
2696
|
+
upsert: (input) => makeBoundPut(entity.upsert(input), boundCrudConfig),
|
|
2697
|
+
patch: (key) => makeBoundUpdate(entity.patch(key), boundCrudConfig),
|
|
2698
|
+
deleteIfExists: (key) => makeBoundDelete(entity.deleteIfExists(key), boundCrudConfig),
|
|
2625
2699
|
// Lifecycle
|
|
2626
2700
|
getVersion: (key, version) => provide(entity.getVersion(key, version)._run("record")),
|
|
2627
2701
|
versions: (key) => wrapAsBoundQuery(entity.versions(key)),
|