effect-dynamodb 0.1.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 (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +69 -0
  3. package/dist/Aggregate.d.ts +210 -0
  4. package/dist/Aggregate.d.ts.map +1 -0
  5. package/dist/Aggregate.js +1169 -0
  6. package/dist/Aggregate.js.map +1 -0
  7. package/dist/Batch.d.ts +62 -0
  8. package/dist/Batch.d.ts.map +1 -0
  9. package/dist/Batch.js +218 -0
  10. package/dist/Batch.js.map +1 -0
  11. package/dist/Collection.d.ts +58 -0
  12. package/dist/Collection.d.ts.map +1 -0
  13. package/dist/Collection.js +195 -0
  14. package/dist/Collection.js.map +1 -0
  15. package/dist/Collections.d.ts +121 -0
  16. package/dist/Collections.d.ts.map +1 -0
  17. package/dist/Collections.js +121 -0
  18. package/dist/Collections.js.map +1 -0
  19. package/dist/DynamoClient.d.ts +317 -0
  20. package/dist/DynamoClient.d.ts.map +1 -0
  21. package/dist/DynamoClient.js +653 -0
  22. package/dist/DynamoClient.js.map +1 -0
  23. package/dist/DynamoModel.d.ts +278 -0
  24. package/dist/DynamoModel.d.ts.map +1 -0
  25. package/dist/DynamoModel.js +453 -0
  26. package/dist/DynamoModel.js.map +1 -0
  27. package/dist/DynamoSchema.d.ts +137 -0
  28. package/dist/DynamoSchema.d.ts.map +1 -0
  29. package/dist/DynamoSchema.js +188 -0
  30. package/dist/DynamoSchema.js.map +1 -0
  31. package/dist/Entity.d.ts +628 -0
  32. package/dist/Entity.d.ts.map +1 -0
  33. package/dist/Entity.js +2371 -0
  34. package/dist/Entity.js.map +1 -0
  35. package/dist/Errors.d.ts +227 -0
  36. package/dist/Errors.d.ts.map +1 -0
  37. package/dist/Errors.js +93 -0
  38. package/dist/Errors.js.map +1 -0
  39. package/dist/EventStore.d.ts +208 -0
  40. package/dist/EventStore.d.ts.map +1 -0
  41. package/dist/EventStore.js +333 -0
  42. package/dist/EventStore.js.map +1 -0
  43. package/dist/Expression.d.ts +50 -0
  44. package/dist/Expression.d.ts.map +1 -0
  45. package/dist/Expression.js +170 -0
  46. package/dist/Expression.js.map +1 -0
  47. package/dist/KeyComposer.d.ts +125 -0
  48. package/dist/KeyComposer.d.ts.map +1 -0
  49. package/dist/KeyComposer.js +257 -0
  50. package/dist/KeyComposer.js.map +1 -0
  51. package/dist/Marshaller.d.ts +30 -0
  52. package/dist/Marshaller.d.ts.map +1 -0
  53. package/dist/Marshaller.js +115 -0
  54. package/dist/Marshaller.js.map +1 -0
  55. package/dist/Projection.d.ts +28 -0
  56. package/dist/Projection.d.ts.map +1 -0
  57. package/dist/Projection.js +39 -0
  58. package/dist/Projection.js.map +1 -0
  59. package/dist/Query.d.ts +214 -0
  60. package/dist/Query.d.ts.map +1 -0
  61. package/dist/Query.js +397 -0
  62. package/dist/Query.js.map +1 -0
  63. package/dist/Table.d.ts +178 -0
  64. package/dist/Table.d.ts.map +1 -0
  65. package/dist/Table.js +211 -0
  66. package/dist/Table.js.map +1 -0
  67. package/dist/Transaction.d.ts +79 -0
  68. package/dist/Transaction.d.ts.map +1 -0
  69. package/dist/Transaction.js +215 -0
  70. package/dist/Transaction.js.map +1 -0
  71. package/dist/_AggregateCursor.d.ts +44 -0
  72. package/dist/_AggregateCursor.d.ts.map +1 -0
  73. package/dist/_AggregateCursor.js +18 -0
  74. package/dist/_AggregateCursor.js.map +1 -0
  75. package/dist/_AggregateEdges.d.ts +77 -0
  76. package/dist/_AggregateEdges.d.ts.map +1 -0
  77. package/dist/_AggregateEdges.js +41 -0
  78. package/dist/_AggregateEdges.js.map +1 -0
  79. package/dist/_AggregateSchemas.d.ts +69 -0
  80. package/dist/_AggregateSchemas.d.ts.map +1 -0
  81. package/dist/_AggregateSchemas.js +194 -0
  82. package/dist/_AggregateSchemas.js.map +1 -0
  83. package/dist/_AggregateTypes.d.ts +94 -0
  84. package/dist/_AggregateTypes.d.ts.map +1 -0
  85. package/dist/_AggregateTypes.js +7 -0
  86. package/dist/_AggregateTypes.js.map +1 -0
  87. package/dist/_EntityCombinators.d.ts +155 -0
  88. package/dist/_EntityCombinators.d.ts.map +1 -0
  89. package/dist/_EntityCombinators.js +158 -0
  90. package/dist/_EntityCombinators.js.map +1 -0
  91. package/dist/_EntityConfig.d.ts +100 -0
  92. package/dist/_EntityConfig.d.ts.map +1 -0
  93. package/dist/_EntityConfig.js +7 -0
  94. package/dist/_EntityConfig.js.map +1 -0
  95. package/dist/_EntityOps.d.ts +205 -0
  96. package/dist/_EntityOps.d.ts.map +1 -0
  97. package/dist/_EntityOps.js +137 -0
  98. package/dist/_EntityOps.js.map +1 -0
  99. package/dist/_EntitySchemas.d.ts +57 -0
  100. package/dist/_EntitySchemas.d.ts.map +1 -0
  101. package/dist/_EntitySchemas.js +298 -0
  102. package/dist/_EntitySchemas.js.map +1 -0
  103. package/dist/_EntityTypes.d.ts +81 -0
  104. package/dist/_EntityTypes.d.ts.map +1 -0
  105. package/dist/_EntityTypes.js +7 -0
  106. package/dist/_EntityTypes.js.map +1 -0
  107. package/dist/_sparse_check.d.ts +2 -0
  108. package/dist/_sparse_check.d.ts.map +1 -0
  109. package/dist/_sparse_check.js +193 -0
  110. package/dist/_sparse_check.js.map +1 -0
  111. package/dist/index.d.ts +35 -0
  112. package/dist/index.d.ts.map +1 -0
  113. package/dist/index.js +20 -0
  114. package/dist/index.js.map +1 -0
  115. package/dist/internal/AggregateCursor.d.ts +44 -0
  116. package/dist/internal/AggregateCursor.d.ts.map +1 -0
  117. package/dist/internal/AggregateCursor.js +18 -0
  118. package/dist/internal/AggregateCursor.js.map +1 -0
  119. package/dist/internal/AggregateEdges.d.ts +77 -0
  120. package/dist/internal/AggregateEdges.d.ts.map +1 -0
  121. package/dist/internal/AggregateEdges.js +41 -0
  122. package/dist/internal/AggregateEdges.js.map +1 -0
  123. package/dist/internal/AggregateSchemas.d.ts +69 -0
  124. package/dist/internal/AggregateSchemas.d.ts.map +1 -0
  125. package/dist/internal/AggregateSchemas.js +193 -0
  126. package/dist/internal/AggregateSchemas.js.map +1 -0
  127. package/dist/internal/AggregateTypes.d.ts +94 -0
  128. package/dist/internal/AggregateTypes.d.ts.map +1 -0
  129. package/dist/internal/AggregateTypes.js +7 -0
  130. package/dist/internal/AggregateTypes.js.map +1 -0
  131. package/dist/internal/BoundQuery.d.ts +107 -0
  132. package/dist/internal/BoundQuery.d.ts.map +1 -0
  133. package/dist/internal/BoundQuery.js +111 -0
  134. package/dist/internal/BoundQuery.js.map +1 -0
  135. package/dist/internal/EntityCombinators.d.ts +173 -0
  136. package/dist/internal/EntityCombinators.d.ts.map +1 -0
  137. package/dist/internal/EntityCombinators.js +183 -0
  138. package/dist/internal/EntityCombinators.js.map +1 -0
  139. package/dist/internal/EntityConfig.d.ts +100 -0
  140. package/dist/internal/EntityConfig.d.ts.map +1 -0
  141. package/dist/internal/EntityConfig.js +7 -0
  142. package/dist/internal/EntityConfig.js.map +1 -0
  143. package/dist/internal/EntityOps.d.ts +268 -0
  144. package/dist/internal/EntityOps.d.ts.map +1 -0
  145. package/dist/internal/EntityOps.js +153 -0
  146. package/dist/internal/EntityOps.js.map +1 -0
  147. package/dist/internal/EntitySchemas.d.ts +57 -0
  148. package/dist/internal/EntitySchemas.d.ts.map +1 -0
  149. package/dist/internal/EntitySchemas.js +299 -0
  150. package/dist/internal/EntitySchemas.js.map +1 -0
  151. package/dist/internal/EntityTypes.d.ts +128 -0
  152. package/dist/internal/EntityTypes.d.ts.map +1 -0
  153. package/dist/internal/EntityTypes.js +7 -0
  154. package/dist/internal/EntityTypes.js.map +1 -0
  155. package/dist/internal/Expr.d.ts +141 -0
  156. package/dist/internal/Expr.d.ts.map +1 -0
  157. package/dist/internal/Expr.js +332 -0
  158. package/dist/internal/Expr.js.map +1 -0
  159. package/dist/internal/PathBuilder.d.ts +88 -0
  160. package/dist/internal/PathBuilder.d.ts.map +1 -0
  161. package/dist/internal/PathBuilder.js +100 -0
  162. package/dist/internal/PathBuilder.js.map +1 -0
  163. package/dist/internal/TransactableOps.d.ts +25 -0
  164. package/dist/internal/TransactableOps.d.ts.map +1 -0
  165. package/dist/internal/TransactableOps.js +58 -0
  166. package/dist/internal/TransactableOps.js.map +1 -0
  167. package/dist/internal/sparse_check.d.ts +2 -0
  168. package/dist/internal/sparse_check.d.ts.map +1 -0
  169. package/dist/internal/sparse_check.js +196 -0
  170. package/dist/internal/sparse_check.js.map +1 -0
  171. package/package.json +71 -0
@@ -0,0 +1,653 @@
1
+ /**
2
+ * DynamoClient — Effect service wrapping the AWS SDK DynamoDBClient.
3
+ *
4
+ * All DynamoDB operations are exposed as Effect-returning methods that fail
5
+ * with {@link DynamoError}. The underlying SDK client lifecycle is managed
6
+ * via `Layer.scoped` (acquire on layer build, destroy on scope close).
7
+ *
8
+ * @module
9
+ */
10
+ import { BatchGetItemCommand, BatchWriteItemCommand, CreateBackupCommand, CreateTableCommand, DeleteBackupCommand, DeleteItemCommand, DeleteTableCommand, DescribeContinuousBackupsCommand, DescribeExportCommand, DescribeTableCommand, DescribeTimeToLiveCommand, DynamoDBClient, ExportTableToPointInTimeCommand, GetItemCommand, ListBackupsCommand, ListTablesCommand, ListTagsOfResourceCommand, PutItemCommand, QueryCommand, RestoreTableFromBackupCommand, RestoreTableToPointInTimeCommand, ScanCommand, TagResourceCommand, TransactGetItemsCommand, TransactWriteItemsCommand, UntagResourceCommand, UpdateContinuousBackupsCommand, UpdateItemCommand, UpdateTableCommand, UpdateTimeToLiveCommand, } from "@aws-sdk/client-dynamodb";
11
+ import { Config, Context, Effect, Layer } from "effect";
12
+ import { bind as aggregateBind } from "./Aggregate.js";
13
+ import * as DynamoSchema from "./DynamoSchema.js";
14
+ import { bind as entityBind } from "./Entity.js";
15
+ import { DynamoError, DynamoValidationError, InternalServerError, isAwsInternalServerError, isAwsResourceNotFound, isAwsThrottling, isAwsValidationError, ResourceNotFoundError, ThrottlingError, ValidationError, } from "./Errors.js";
16
+ import { BoundQueryImpl } from "./internal/BoundQuery.js";
17
+ import { createConditionOps } from "./internal/Expr.js";
18
+ import { createPathBuilder } from "./internal/PathBuilder.js";
19
+ import * as KeyComposer from "./KeyComposer.js";
20
+ import * as Query from "./Query.js";
21
+ import { definition as tableDefinition } from "./Table.js";
22
+ /**
23
+ * Effect Context.Service for the DynamoDB client service.
24
+ *
25
+ * Use `DynamoClient.layer(config)` to construct a live Layer that manages
26
+ * the underlying AWS SDK client lifecycle with `Effect.acquireRelease`.
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * const live = DynamoClient.layer({ region: "us-east-1" })
31
+ * pipe(program, Effect.provide(live), Effect.runPromise)
32
+ * ```
33
+ */
34
+ export class DynamoClient extends Context.Service()("@effect-dynamodb/DynamoClient") {
35
+ /**
36
+ * Create a live Layer that manages an AWS DynamoDBClient.
37
+ *
38
+ * @param config.region - AWS region
39
+ * @param config.endpoint - Optional endpoint override (e.g., for DynamoDB Local)
40
+ * @param config.credentials - Optional static credentials
41
+ */
42
+ static layer = (config) => Layer.effect(DynamoClient, buildService(config.region, config.endpoint, config.credentials));
43
+ /**
44
+ * Create a live Layer that reads configuration from Effect Config providers
45
+ * (e.g., environment variables, config files).
46
+ *
47
+ * @param config.region - Config for AWS region
48
+ * @param config.endpoint - Optional Config for endpoint override
49
+ * @param config.credentials - Optional Config for static credentials
50
+ */
51
+ static layerConfig = (config) => Layer.effect(DynamoClient, Effect.gen(function* () {
52
+ const region = yield* config.region;
53
+ const endpoint = config.endpoint ? yield* Config.option(config.endpoint) : undefined;
54
+ const credentials = config.credentials
55
+ ? yield* Config.option(config.credentials)
56
+ : undefined;
57
+ return yield* buildService(region, endpoint && endpoint._tag === "Some" ? endpoint.value : undefined, credentials && credentials._tag === "Some" ? credentials.value : undefined);
58
+ }));
59
+ /**
60
+ * Create a typed client gateway from entities, aggregates, and (optionally) tables.
61
+ *
62
+ * Returns a namespaced client with `entities`, `aggregates`, `collections`, and
63
+ * `tables` properties. Collections are auto-discovered from entity index
64
+ * `collection` properties — no separate registration needed.
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * const db = yield* DynamoClient.make({
69
+ * entities: { Users, Tasks },
70
+ * aggregates: { OrderAggregate },
71
+ * tables: { MainTable },
72
+ * })
73
+ *
74
+ * yield* db.entities.Users.get({ userId: "123" })
75
+ * yield* db.aggregates.OrderAggregate.get({ orderId: "o-1" })
76
+ * yield* db.collections.assignments({ userId: "123" }).collect()
77
+ * yield* db.tables.MainTable.create()
78
+ * ```
79
+ */
80
+ static make = (config) => makeFromConfig(config);
81
+ }
82
+ const makeFromConfig = (config) => Effect.gen(function* () {
83
+ // 1. Resolve the provide function from context
84
+ const ctx = yield* Effect.context();
85
+ const provide = (effect) => Effect.provide(effect, ctx);
86
+ // Helper: validate query composites at runtime
87
+ const validateQueryComposites = (indexName, indexDef, composites) => {
88
+ const pkAttrs = indexDef.pk.composite;
89
+ const skAttrs = indexDef.sk.composite;
90
+ // 1. All PK composites must be present
91
+ for (const attr of pkAttrs) {
92
+ if (composites[attr] === undefined) {
93
+ throw new Error(`[EDD-9002] Missing required partition key attribute "${attr}" for index "${indexName}"`);
94
+ }
95
+ }
96
+ // 2. SK composites must follow prefix ordering
97
+ let lastProvided = -1;
98
+ for (let i = 0; i < skAttrs.length; i++) {
99
+ if (composites[skAttrs[i]] !== undefined) {
100
+ if (i !== lastProvided + 1) {
101
+ const missing = skAttrs.slice(lastProvided + 1, i).join(", ");
102
+ throw new Error(`[EDD-9004] Sort key composite "${skAttrs[i]}" for index "${indexName}" requires prior composites: ${missing}. Sort key composites must follow prefix ordering.`);
103
+ }
104
+ lastProvided = i;
105
+ }
106
+ }
107
+ // 3. No excess properties
108
+ const validKeys = new Set([...pkAttrs, ...skAttrs]);
109
+ for (const key of Object.keys(composites)) {
110
+ if (!validKeys.has(key)) {
111
+ throw new Error(`[EDD-9006] Unknown composite attribute "${key}" for index "${indexName}". Valid attributes: ${[...validKeys].join(", ")}`);
112
+ }
113
+ }
114
+ };
115
+ // Helper: build a BoundQuery for a single-entity index query
116
+ const buildEntityQueryAccessor = (entityLike, _indexName, indexDef) => {
117
+ return (composites) => {
118
+ validateQueryComposites(_indexName, indexDef, composites);
119
+ const pkValue = KeyComposer.composePk(entityLike._schema, entityLike.entityType, indexDef, composites);
120
+ const query = Query.make({
121
+ tableName: "",
122
+ indexName: indexDef.index,
123
+ pkField: indexDef.pk.field,
124
+ pkValue,
125
+ skField: indexDef.sk.field,
126
+ entityTypes: [entityLike.entityType],
127
+ decoder: (raw) => entityLike._decodeRecord(raw),
128
+ resolveTableName: entityLike._tableTag.useSync((tc) => tc.name),
129
+ });
130
+ // Apply SK prefix from provided composites
131
+ const hasSkComposites = indexDef.sk.composite.some((attr) => composites[attr] !== undefined);
132
+ const finalQuery = hasSkComposites
133
+ ? Query.where(query, {
134
+ beginsWith: KeyComposer.composeSortKeyPrefix(entityLike._schema, entityLike.entityType, 1, indexDef, composites),
135
+ })
136
+ : query;
137
+ const pathBuilder = createPathBuilder();
138
+ const conditionOps = createConditionOps();
139
+ // composeSkCondition: prepend the entity SK prefix to user's .where() condition
140
+ const composeSkCondition = (condition) => {
141
+ const skPrefix = KeyComposer.composeSortKeyPrefix(entityLike._schema, entityLike.entityType, 1, indexDef, composites);
142
+ const prepend = (value) => `${skPrefix}#${value}`;
143
+ if ("eq" in condition)
144
+ return { eq: prepend(condition.eq) };
145
+ if ("lt" in condition)
146
+ return { lt: prepend(condition.lt) };
147
+ if ("lte" in condition)
148
+ return { lte: prepend(condition.lte) };
149
+ if ("gt" in condition)
150
+ return { gt: prepend(condition.gt) };
151
+ if ("gte" in condition)
152
+ return { gte: prepend(condition.gte) };
153
+ if ("between" in condition)
154
+ return { between: [prepend(condition.between[0]), prepend(condition.between[1])] };
155
+ if ("beginsWith" in condition)
156
+ return { beginsWith: prepend(condition.beginsWith) };
157
+ return condition;
158
+ };
159
+ const bqConfig = {
160
+ pathBuilder,
161
+ conditionOps,
162
+ provide,
163
+ composeSkCondition,
164
+ skFields: indexDef.sk.composite,
165
+ };
166
+ return new BoundQueryImpl(finalQuery, bqConfig);
167
+ };
168
+ };
169
+ // 2. Bind entities + build entity query accessors
170
+ const boundEntities = {};
171
+ // Track collection memberships for auto-discovery
172
+ // collectionName → { entityKey, entityLike, indexName, indexDef }[]
173
+ const collectionMembers = new Map();
174
+ for (const [key, entity] of Object.entries(config.entities)) {
175
+ const bound = yield* entityBind(entity);
176
+ const entityLike = entity;
177
+ const accessors = {};
178
+ // Add query accessor for each non-primary index
179
+ for (const [indexName, indexDef] of Object.entries(entityLike.indexes)) {
180
+ if (indexName === "primary")
181
+ continue;
182
+ if (!indexDef)
183
+ continue;
184
+ accessors[indexName] = buildEntityQueryAccessor(entityLike, indexName, indexDef);
185
+ // Track collection membership for auto-discovery
186
+ if (indexDef.collection) {
187
+ const collNames = Array.isArray(indexDef.collection)
188
+ ? indexDef.collection
189
+ : [indexDef.collection];
190
+ for (const collName of collNames) {
191
+ if (!collectionMembers.has(collName)) {
192
+ collectionMembers.set(collName, []);
193
+ }
194
+ collectionMembers.get(collName).push({ entityKey: key, entityLike, indexDef });
195
+ }
196
+ }
197
+ }
198
+ // Add scan accessor
199
+ accessors.scan = () => {
200
+ const scanQuery = Query.makeScan({
201
+ tableName: "",
202
+ indexName: undefined,
203
+ entityTypes: [entityLike.entityType],
204
+ decoder: (raw) => entityLike._decodeRecord(raw),
205
+ resolveTableName: entityLike._tableTag.useSync((tc) => tc.name),
206
+ });
207
+ const pathBuilder = createPathBuilder();
208
+ const conditionOps = createConditionOps();
209
+ const bqConfig = { pathBuilder, conditionOps, provide };
210
+ return new BoundQueryImpl(scanQuery, bqConfig);
211
+ };
212
+ boundEntities[key] = { ...bound, ...accessors };
213
+ }
214
+ // 3. Build auto-discovered collection accessors
215
+ const boundCollections = {};
216
+ for (const [collName, members] of collectionMembers) {
217
+ boundCollections[collName] = (composites) => {
218
+ // Use the first member for PK composition
219
+ const firstMember = members[0];
220
+ const indexDef = firstMember.indexDef;
221
+ // Build entity type → entity key lookup
222
+ const memberByType = new Map();
223
+ const entityTypes = [];
224
+ for (const member of members) {
225
+ memberByType.set(member.entityLike.entityType, member.entityKey);
226
+ entityTypes.push(member.entityLike.entityType);
227
+ }
228
+ // Collection decoder: decode and group by entity key
229
+ const collDecoder = (raw) => {
230
+ const entityType = raw.__edd_e__;
231
+ if (!entityType) {
232
+ return Effect.fail(new ValidationError({
233
+ entityType: "unknown",
234
+ operation: "collection.decode",
235
+ cause: "Item missing __edd_e__",
236
+ }));
237
+ }
238
+ const entityKey = memberByType.get(entityType);
239
+ if (!entityKey) {
240
+ return Effect.succeed({ _memberKey: "__unknown__", _decoded: raw });
241
+ }
242
+ const member = members.find((m) => m.entityLike.entityType === entityType);
243
+ if (!member) {
244
+ return Effect.succeed({ _memberKey: "__unknown__", _decoded: raw });
245
+ }
246
+ return member.entityLike
247
+ ._decodeRecord(raw)
248
+ .pipe(Effect.map((decoded) => ({ _memberKey: entityKey, _decoded: decoded })));
249
+ };
250
+ const pkValue = KeyComposer.composePk(firstMember.entityLike._schema, firstMember.entityLike.entityType, indexDef, composites);
251
+ // Always isolated — begins_with on collection SK prefix
252
+ let query = Query.make({
253
+ tableName: "",
254
+ indexName: indexDef.index,
255
+ pkField: indexDef.pk.field,
256
+ pkValue,
257
+ skField: indexDef.sk.field,
258
+ entityTypes,
259
+ decoder: collDecoder,
260
+ resolveTableName: firstMember.entityLike._tableTag.useSync((tc) => tc.name),
261
+ });
262
+ // Add begins_with on collection SK prefix for clustered collections.
263
+ // For isolated collections (default), each entity has its own entity-type
264
+ // SK prefix, so a collection-name begins_with would filter them all out.
265
+ //
266
+ // For sub-collections (collection: ["parent", "child"]), the SK prefix
267
+ // includes the full hierarchy from root up to and including the queried
268
+ // collection name. A query at the parent level matches every descendant.
269
+ if (indexDef.sk.field && indexDef.type === "clustered") {
270
+ const coll = indexDef.collection;
271
+ const hierarchy = Array.isArray(coll)
272
+ ? coll.slice(0, coll.indexOf(collName) + 1)
273
+ : [collName];
274
+ const casing = indexDef.casing ?? firstMember.entityLike._schema.casing;
275
+ const pre = DynamoSchema.prefix(firstMember.entityLike._schema);
276
+ const casedNames = hierarchy.map((n) => DynamoSchema.applyCasing(n, casing));
277
+ const skPrefix = `${pre}#${casedNames.join("#")}`;
278
+ query = Query.where(query, { beginsWith: skPrefix });
279
+ }
280
+ // Wrap in BoundQuery that groups results by entity key
281
+ const pathBuilder = createPathBuilder();
282
+ const conditionOps = createConditionOps();
283
+ const collectionProvide = (eff) => Effect.provide(eff, ctx);
284
+ const bqConfig = {
285
+ pathBuilder,
286
+ conditionOps,
287
+ provide: collectionProvide,
288
+ };
289
+ const bq = new BoundQueryImpl(query, bqConfig);
290
+ // Override collect to group results
291
+ const originalCollect = bq.collect.bind(bq);
292
+ bq.collect = () => Effect.map(originalCollect(), (items) => {
293
+ const result = {};
294
+ for (const member of members) {
295
+ result[member.entityKey] = [];
296
+ }
297
+ for (const item of items) {
298
+ const memberKey = item._memberKey;
299
+ if (memberKey && result[memberKey]) {
300
+ result[memberKey].push(item._decoded);
301
+ }
302
+ }
303
+ return result;
304
+ });
305
+ return bq;
306
+ };
307
+ }
308
+ // 4. Bind aggregates
309
+ const boundAggregates = {};
310
+ if (config.aggregates) {
311
+ for (const [key, aggregate] of Object.entries(config.aggregates)) {
312
+ boundAggregates[key] = yield* aggregateBind(aggregate);
313
+ }
314
+ }
315
+ // 5. Build table operations
316
+ const client = yield* DynamoClient;
317
+ const tables = {};
318
+ if (config.tables) {
319
+ for (const [tableKey, table] of Object.entries(config.tables)) {
320
+ const tableConfig = yield* table.Tag;
321
+ // Merge any runtime-registered aggregates from `config.aggregates` whose
322
+ // `_tableTag` matches this table's tag. Users may intentionally omit
323
+ // aggregates from `Table.make({ aggregates })` to sidestep the multi-file
324
+ // circular-import problem, registering them on `DynamoClient.make` instead.
325
+ // Without this merge, `tableDefinition()` would never see those aggregates
326
+ // and `db.tables.*.create()` would silently drop their (L|G)SIs.
327
+ let mergedTable = table;
328
+ if (config.aggregates) {
329
+ const tableTagKey = table.Tag.key;
330
+ const existingAggs = table.aggregates;
331
+ const existingAggSet = new Set(Object.values(existingAggs));
332
+ const extraAggregateEntries = [];
333
+ for (const [aggKey, agg] of Object.entries(config.aggregates)) {
334
+ const aggTag = agg._tableTag;
335
+ if (aggTag === undefined)
336
+ continue;
337
+ if (aggTag.key !== tableTagKey)
338
+ continue;
339
+ // Skip if this aggregate is already registered on the table object
340
+ // (reference equality) so we don't emit duplicate LSI/GSI entries.
341
+ if (existingAggSet.has(agg))
342
+ continue;
343
+ extraAggregateEntries.push([aggKey, agg]);
344
+ }
345
+ if (extraAggregateEntries.length > 0) {
346
+ mergedTable = {
347
+ ...table,
348
+ aggregates: {
349
+ ...existingAggs,
350
+ ...Object.fromEntries(extraAggregateEntries),
351
+ },
352
+ };
353
+ }
354
+ }
355
+ tables[tableKey] = buildTableOperationsFromTable(tableConfig.name, mergedTable, client);
356
+ }
357
+ }
358
+ else {
359
+ // Group entities by table tag so we can derive full table schema for create().
360
+ // Aggregates are also assigned to a table tag via their underlying entities;
361
+ // we collect them here so create() includes their GSIs.
362
+ const entitiesByTag = new Map();
363
+ for (const [, entity] of Object.entries(config.entities)) {
364
+ const entityLike = entity;
365
+ const tagId = entityLike._tableTag.key;
366
+ if (!entitiesByTag.has(tagId)) {
367
+ entitiesByTag.set(tagId, { tag: entityLike._tableTag, entities: [], aggregates: [] });
368
+ }
369
+ entitiesByTag.get(tagId).entities.push(entityLike);
370
+ }
371
+ // Attach aggregates to whichever table tag matches their root entity tag.
372
+ // (Aggregates expose `_tableTag` from their root entity registration.)
373
+ if (config.aggregates) {
374
+ for (const [, agg] of Object.entries(config.aggregates)) {
375
+ const aggTag = agg._tableTag;
376
+ if (aggTag && entitiesByTag.has(aggTag.key)) {
377
+ entitiesByTag.get(aggTag.key).aggregates.push(agg);
378
+ }
379
+ }
380
+ }
381
+ for (const [, { tag, entities: tableEntities, aggregates: tableAggregates },] of entitiesByTag) {
382
+ const tableConfig = yield* tag;
383
+ // Build a minimal Table-like object so tableDefinition() can derive GSIs
384
+ // (entities + aggregates).
385
+ const syntheticTable = {
386
+ entities: Object.fromEntries(tableEntities.map((e) => [e.entityType, e])),
387
+ aggregates: Object.fromEntries(tableAggregates.map((a, i) => [
388
+ a.name ?? `__aggregate_${i}__`,
389
+ a,
390
+ ])),
391
+ };
392
+ tables[tableConfig.name] = buildTableOperationsFromTable(tableConfig.name, syntheticTable, client);
393
+ }
394
+ }
395
+ return {
396
+ entities: boundEntities,
397
+ aggregates: boundAggregates,
398
+ collections: boundCollections,
399
+ tables,
400
+ };
401
+ });
402
+ // ---------------------------------------------------------------------------
403
+ // buildTableOperations — pre-bound table management
404
+ // ---------------------------------------------------------------------------
405
+ /** Check if a DynamoError wraps a ResourceInUseException (table already exists). */
406
+ const isResourceInUse = (err) => err._tag === "DynamoError" &&
407
+ err.cause != null &&
408
+ typeof err.cause === "object" &&
409
+ "name" in err.cause &&
410
+ err.cause.name === "ResourceInUseException";
411
+ /** Make createTable idempotent — ignore if table already exists. */
412
+ const idempotentCreate = (effect) => Effect.catchIf(effect, isResourceInUse, () => Effect.void);
413
+ const buildTableOperationsFromTable = (tableName, table, client) => {
414
+ const def = tableDefinition(table);
415
+ return {
416
+ ...buildTableOperations(tableName, client),
417
+ create: (options) => idempotentCreate(client
418
+ .createTable({
419
+ TableName: tableName,
420
+ BillingMode: options?.billingMode ?? "PAY_PER_REQUEST",
421
+ ...def,
422
+ })
423
+ .pipe(Effect.asVoid)),
424
+ };
425
+ };
426
+ const buildTableOperations = (tableName, client) => ({
427
+ create: (options) => idempotentCreate(client
428
+ .createTable({
429
+ TableName: tableName,
430
+ BillingMode: options?.billingMode ?? "PAY_PER_REQUEST",
431
+ KeySchema: [
432
+ { AttributeName: "pk", KeyType: "HASH" },
433
+ { AttributeName: "sk", KeyType: "RANGE" },
434
+ ],
435
+ AttributeDefinitions: [
436
+ { AttributeName: "pk", AttributeType: "S" },
437
+ { AttributeName: "sk", AttributeType: "S" },
438
+ ],
439
+ })
440
+ .pipe(Effect.asVoid)),
441
+ delete: () => client.deleteTable({ TableName: tableName }).pipe(Effect.asVoid),
442
+ describe: () => client.describeTable({ TableName: tableName }),
443
+ update: (input) => client
444
+ .updateTable({ ...input, TableName: tableName })
445
+ .pipe(Effect.asVoid),
446
+ backup: (name) => client.createBackup({ TableName: tableName, BackupName: name }),
447
+ listBackups: () => client.listBackups({ TableName: tableName }),
448
+ restoreFromBackup: (backupArn) => client.restoreTableFromBackup({
449
+ TargetTableName: `${tableName}-restore`,
450
+ BackupArn: backupArn,
451
+ }),
452
+ enablePointInTimeRecovery: () => client
453
+ .updateContinuousBackups({
454
+ TableName: tableName,
455
+ PointInTimeRecoverySpecification: { PointInTimeRecoveryEnabled: true },
456
+ })
457
+ .pipe(Effect.asVoid),
458
+ disablePointInTimeRecovery: () => client
459
+ .updateContinuousBackups({
460
+ TableName: tableName,
461
+ PointInTimeRecoverySpecification: { PointInTimeRecoveryEnabled: false },
462
+ })
463
+ .pipe(Effect.asVoid),
464
+ restoreToPointInTime: (timestamp) => client.restoreTableToPointInTime({
465
+ SourceTableName: tableName,
466
+ TargetTableName: `${tableName}-pitr-restore`,
467
+ RestoreDateTime: timestamp,
468
+ }),
469
+ exportToS3: (s3Bucket, options) => client.exportTableToPointInTime({
470
+ TableArn: tableName, // Will need actual ARN — use describe first
471
+ S3Bucket: s3Bucket,
472
+ S3Prefix: options?.s3Prefix,
473
+ ExportFormat: options?.exportFormat,
474
+ }),
475
+ enableTTL: (attributeName) => client
476
+ .updateTimeToLive({
477
+ TableName: tableName,
478
+ TimeToLiveSpecification: {
479
+ Enabled: true,
480
+ AttributeName: attributeName,
481
+ },
482
+ })
483
+ .pipe(Effect.asVoid),
484
+ disableTTL: (attributeName) => client
485
+ .updateTimeToLive({
486
+ TableName: tableName,
487
+ TimeToLiveSpecification: {
488
+ Enabled: false,
489
+ AttributeName: attributeName,
490
+ },
491
+ })
492
+ .pipe(Effect.asVoid),
493
+ describeTTL: () => client.describeTimeToLive({ TableName: tableName }),
494
+ tag: (tags) => client
495
+ .tagResource({
496
+ ResourceArn: tableName, // Will need actual ARN
497
+ Tags: Object.entries(tags).map(([Key, Value]) => ({ Key, Value })),
498
+ })
499
+ .pipe(Effect.asVoid),
500
+ untag: (tagKeys) => client
501
+ .untagResource({
502
+ ResourceArn: tableName, // Will need actual ARN
503
+ TagKeys: [...tagKeys],
504
+ })
505
+ .pipe(Effect.asVoid),
506
+ tags: () => client.listTagsOfResource({ ResourceArn: tableName }),
507
+ });
508
+ /** @internal Classify an AWS SDK error into a specific tagged error type. */
509
+ const classifyError = (operation) => (cause) => {
510
+ if (isAwsThrottling(cause))
511
+ return new ThrottlingError({ operation, cause });
512
+ if (isAwsValidationError(cause))
513
+ return new DynamoValidationError({ operation, cause });
514
+ if (isAwsInternalServerError(cause))
515
+ return new InternalServerError({ operation, cause });
516
+ if (isAwsResourceNotFound(cause))
517
+ return new ResourceNotFoundError({ operation, cause });
518
+ return new DynamoError({ operation, cause });
519
+ };
520
+ /** @internal Build the DynamoClient service implementation from resolved config values. */
521
+ const buildService = (region, endpoint, credentials) => Effect.gen(function* () {
522
+ const clientConfig = { region };
523
+ if (endpoint !== undefined)
524
+ clientConfig.endpoint = endpoint;
525
+ if (credentials !== undefined)
526
+ clientConfig.credentials = credentials;
527
+ const client = yield* Effect.acquireRelease(Effect.sync(() => new DynamoDBClient(clientConfig)), (c) => Effect.sync(() => c.destroy()));
528
+ return {
529
+ createTable: (input) => Effect.tryPromise({
530
+ try: () => client.send(new CreateTableCommand(input)),
531
+ catch: classifyError("CreateTable"),
532
+ }),
533
+ deleteTable: (input) => Effect.tryPromise({
534
+ try: () => client.send(new DeleteTableCommand(input)),
535
+ catch: classifyError("DeleteTable"),
536
+ }),
537
+ describeTable: (input) => Effect.tryPromise({
538
+ try: () => client.send(new DescribeTableCommand(input)),
539
+ catch: classifyError("DescribeTable"),
540
+ }),
541
+ putItem: (input) => Effect.tryPromise({
542
+ try: () => client.send(new PutItemCommand(input)),
543
+ catch: classifyError("PutItem"),
544
+ }),
545
+ getItem: (input) => Effect.tryPromise({
546
+ try: () => client.send(new GetItemCommand(input)),
547
+ catch: classifyError("GetItem"),
548
+ }),
549
+ deleteItem: (input) => Effect.tryPromise({
550
+ try: () => client.send(new DeleteItemCommand(input)),
551
+ catch: classifyError("DeleteItem"),
552
+ }),
553
+ updateItem: (input) => Effect.tryPromise({
554
+ try: () => client.send(new UpdateItemCommand(input)),
555
+ catch: classifyError("UpdateItem"),
556
+ }),
557
+ query: (input) => Effect.tryPromise({
558
+ try: () => client.send(new QueryCommand(input)),
559
+ catch: classifyError("Query"),
560
+ }),
561
+ scan: (input) => Effect.tryPromise({
562
+ try: () => client.send(new ScanCommand(input)),
563
+ catch: classifyError("Scan"),
564
+ }),
565
+ batchGetItem: (input) => Effect.tryPromise({
566
+ try: () => client.send(new BatchGetItemCommand(input)),
567
+ catch: classifyError("BatchGetItem"),
568
+ }),
569
+ batchWriteItem: (input) => Effect.tryPromise({
570
+ try: () => client.send(new BatchWriteItemCommand(input)),
571
+ catch: classifyError("BatchWriteItem"),
572
+ }),
573
+ transactGetItems: (input) => Effect.tryPromise({
574
+ try: () => client.send(new TransactGetItemsCommand(input)),
575
+ catch: classifyError("TransactGetItems"),
576
+ }),
577
+ transactWriteItems: (input) => Effect.tryPromise({
578
+ try: () => client.send(new TransactWriteItemsCommand(input)),
579
+ catch: classifyError("TransactWriteItems"),
580
+ }),
581
+ // --- Table management ---
582
+ updateTable: (input) => Effect.tryPromise({
583
+ try: () => client.send(new UpdateTableCommand(input)),
584
+ catch: classifyError("UpdateTable"),
585
+ }),
586
+ listTables: (input) => Effect.tryPromise({
587
+ try: () => client.send(new ListTablesCommand(input)),
588
+ catch: classifyError("ListTables"),
589
+ }),
590
+ // --- Backup ---
591
+ createBackup: (input) => Effect.tryPromise({
592
+ try: () => client.send(new CreateBackupCommand(input)),
593
+ catch: classifyError("CreateBackup"),
594
+ }),
595
+ deleteBackup: (input) => Effect.tryPromise({
596
+ try: () => client.send(new DeleteBackupCommand(input)),
597
+ catch: classifyError("DeleteBackup"),
598
+ }),
599
+ listBackups: (input) => Effect.tryPromise({
600
+ try: () => client.send(new ListBackupsCommand(input)),
601
+ catch: classifyError("ListBackups"),
602
+ }),
603
+ restoreTableFromBackup: (input) => Effect.tryPromise({
604
+ try: () => client.send(new RestoreTableFromBackupCommand(input)),
605
+ catch: classifyError("RestoreTableFromBackup"),
606
+ }),
607
+ // --- PITR ---
608
+ describeContinuousBackups: (input) => Effect.tryPromise({
609
+ try: () => client.send(new DescribeContinuousBackupsCommand(input)),
610
+ catch: classifyError("DescribeContinuousBackups"),
611
+ }),
612
+ updateContinuousBackups: (input) => Effect.tryPromise({
613
+ try: () => client.send(new UpdateContinuousBackupsCommand(input)),
614
+ catch: classifyError("UpdateContinuousBackups"),
615
+ }),
616
+ restoreTableToPointInTime: (input) => Effect.tryPromise({
617
+ try: () => client.send(new RestoreTableToPointInTimeCommand(input)),
618
+ catch: classifyError("RestoreTableToPointInTime"),
619
+ }),
620
+ // --- Export ---
621
+ exportTableToPointInTime: (input) => Effect.tryPromise({
622
+ try: () => client.send(new ExportTableToPointInTimeCommand(input)),
623
+ catch: classifyError("ExportTableToPointInTime"),
624
+ }),
625
+ describeExport: (input) => Effect.tryPromise({
626
+ try: () => client.send(new DescribeExportCommand(input)),
627
+ catch: classifyError("DescribeExport"),
628
+ }),
629
+ // --- TTL ---
630
+ updateTimeToLive: (input) => Effect.tryPromise({
631
+ try: () => client.send(new UpdateTimeToLiveCommand(input)),
632
+ catch: classifyError("UpdateTimeToLive"),
633
+ }),
634
+ describeTimeToLive: (input) => Effect.tryPromise({
635
+ try: () => client.send(new DescribeTimeToLiveCommand(input)),
636
+ catch: classifyError("DescribeTimeToLive"),
637
+ }),
638
+ // --- Tags ---
639
+ tagResource: (input) => Effect.tryPromise({
640
+ try: () => client.send(new TagResourceCommand(input)),
641
+ catch: classifyError("TagResource"),
642
+ }),
643
+ untagResource: (input) => Effect.tryPromise({
644
+ try: () => client.send(new UntagResourceCommand(input)),
645
+ catch: classifyError("UntagResource"),
646
+ }),
647
+ listTagsOfResource: (input) => Effect.tryPromise({
648
+ try: () => client.send(new ListTagsOfResourceCommand(input)),
649
+ catch: classifyError("ListTagsOfResource"),
650
+ }),
651
+ };
652
+ });
653
+ //# sourceMappingURL=DynamoClient.js.map