@effect-app/infra 4.0.0-beta.120 → 4.0.0-beta.122

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 (74) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/CUPS.d.ts.map +1 -1
  3. package/dist/CUPS.js +8 -10
  4. package/dist/Model/Repository/ext.d.ts +17 -5
  5. package/dist/Model/Repository/ext.d.ts.map +1 -1
  6. package/dist/Model/Repository/ext.js +25 -2
  7. package/dist/Model/Repository/internal/internal.d.ts +1 -1
  8. package/dist/Model/Repository/internal/internal.d.ts.map +1 -1
  9. package/dist/Model/Repository/internal/internal.js +9 -8
  10. package/dist/Model/Repository/makeRepo.d.ts +3 -3
  11. package/dist/Model/Repository/makeRepo.d.ts.map +1 -1
  12. package/dist/Model/Repository/service.d.ts +21 -21
  13. package/dist/Model/Repository/service.d.ts.map +1 -1
  14. package/dist/Model/query/new-kid-interpreter.d.ts.map +1 -1
  15. package/dist/Model/query/new-kid-interpreter.js +3 -3
  16. package/dist/Operations.d.ts +3 -3
  17. package/dist/Operations.d.ts.map +1 -1
  18. package/dist/Operations.js +54 -57
  19. package/dist/OperationsRepo.d.ts +2 -2
  20. package/dist/QueueMaker/SQLQueue.d.ts +2 -3
  21. package/dist/QueueMaker/SQLQueue.d.ts.map +1 -1
  22. package/dist/QueueMaker/SQLQueue.js +104 -115
  23. package/dist/QueueMaker/memQueue.d.ts +2 -2
  24. package/dist/QueueMaker/memQueue.d.ts.map +1 -1
  25. package/dist/QueueMaker/memQueue.js +51 -62
  26. package/dist/QueueMaker/sbqueue.d.ts.map +1 -1
  27. package/dist/QueueMaker/sbqueue.js +34 -50
  28. package/dist/Store/Cosmos.d.ts.map +1 -1
  29. package/dist/Store/Cosmos.js +304 -306
  30. package/dist/Store/Disk.d.ts +1 -1
  31. package/dist/Store/Disk.d.ts.map +1 -1
  32. package/dist/Store/Disk.js +2 -2
  33. package/dist/Store/Memory.d.ts +1 -1
  34. package/dist/Store/Memory.d.ts.map +1 -1
  35. package/dist/Store/Memory.js +2 -2
  36. package/dist/Store/SQL/Pg.d.ts.map +1 -1
  37. package/dist/Store/SQL/Pg.js +147 -149
  38. package/dist/Store/SQL.d.ts.map +1 -1
  39. package/dist/Store/SQL.js +6 -6
  40. package/dist/Store/utils.d.ts.map +1 -1
  41. package/dist/Store/utils.js +3 -4
  42. package/dist/adapters/ServiceBus.d.ts.map +1 -1
  43. package/dist/adapters/ServiceBus.js +7 -9
  44. package/dist/api/internal/auth.d.ts.map +1 -1
  45. package/dist/api/internal/auth.js +1 -1
  46. package/dist/api/routing/middleware/middleware.d.ts.map +1 -1
  47. package/dist/api/routing/middleware/middleware.js +2 -2
  48. package/dist/errorReporter.d.ts +3 -3
  49. package/dist/errorReporter.d.ts.map +1 -1
  50. package/dist/errorReporter.js +16 -23
  51. package/package.json +14 -14
  52. package/src/CUPS.ts +7 -9
  53. package/src/Model/Repository/ext.ts +71 -6
  54. package/src/Model/Repository/internal/internal.ts +13 -25
  55. package/src/Model/Repository/makeRepo.ts +4 -4
  56. package/src/Model/Repository/service.ts +22 -21
  57. package/src/Model/query/new-kid-interpreter.ts +2 -2
  58. package/src/Operations.ts +76 -111
  59. package/src/QueueMaker/SQLQueue.ts +119 -150
  60. package/src/QueueMaker/memQueue.ts +81 -102
  61. package/src/QueueMaker/sbqueue.ts +51 -81
  62. package/src/Store/Cosmos.ts +481 -484
  63. package/src/Store/Disk.ts +52 -53
  64. package/src/Store/Memory.ts +49 -50
  65. package/src/Store/SQL/Pg.ts +247 -250
  66. package/src/Store/SQL.ts +420 -426
  67. package/src/Store/utils.ts +23 -22
  68. package/src/adapters/ServiceBus.ts +106 -110
  69. package/src/api/internal/auth.ts +8 -6
  70. package/src/api/routing/middleware/middleware.ts +10 -11
  71. package/src/errorReporter.ts +58 -72
  72. package/test/dist/repository-ext.test.d.ts.map +1 -0
  73. package/test/query.test.ts +27 -0
  74. package/test/repository-ext.test.ts +58 -0
@@ -26,340 +26,311 @@ class CosmosDbOperationError {
26
26
  constructor(readonly message: string, readonly raw?: unknown) {}
27
27
  } // TODO: Retry operation when running into RU limit.
28
28
 
29
- function makeCosmosStore({ prefix }: StorageConfig) {
30
- return Effect.gen(function*() {
31
- const { db } = yield* CosmosClient
32
- return {
33
- make: <IdKey extends keyof Encoded, Encoded extends FieldValues, R = never, E = never>(
34
- name: string,
35
- idKey: IdKey,
36
- seed?: Effect.Effect<Iterable<Encoded>, E, R>,
37
- config?: StoreConfig<Encoded>
38
- ) =>
39
- Effect.gen(function*() {
40
- const mapId = makeMapId<IdKey, Encoded>(idKey)
41
- const mapReverseId = makeReverseMapId<IdKey, Encoded>(idKey)
42
- type PM = PersistenceModelType<Encoded>
43
- type PMCosmos = PersistenceModelType<Omit<Encoded, IdKey> & { id: string }>
44
- const containerId = `${prefix}${name}`
45
- yield* Effect.promise(() =>
46
- db.containers.createIfNotExists(dropUndefinedT({
47
- id: containerId,
48
- uniqueKeyPolicy: config?.uniqueKeys
49
- ? { uniqueKeys: config.uniqueKeys }
50
- : undefined,
51
- partitionKey: {
52
- paths: ["/_partitionKey"],
53
- version: 2 // support large partitionkeys so that the hash is not based on just the first 100 bytes!
54
- }
55
- }))
56
- )
29
+ const makeCosmosStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
30
+ const { db } = yield* CosmosClient
31
+ return {
32
+ make: Effect.fnUntraced(function*<IdKey extends keyof Encoded, Encoded extends FieldValues, R = never, E = never>(
33
+ name: string,
34
+ idKey: IdKey,
35
+ seed?: Effect.Effect<Iterable<Encoded>, E, R>,
36
+ config?: StoreConfig<Encoded>
37
+ ) {
38
+ const mapId = makeMapId<IdKey, Encoded>(idKey)
39
+ const mapReverseId = makeReverseMapId<IdKey, Encoded>(idKey)
40
+ type PM = PersistenceModelType<Encoded>
41
+ type PMCosmos = PersistenceModelType<Omit<Encoded, IdKey> & { id: string }>
42
+ const containerId = `${prefix}${name}`
43
+ yield* Effect.promise(() =>
44
+ db.containers.createIfNotExists(dropUndefinedT({
45
+ id: containerId,
46
+ uniqueKeyPolicy: config?.uniqueKeys
47
+ ? { uniqueKeys: config.uniqueKeys }
48
+ : undefined,
49
+ partitionKey: {
50
+ paths: ["/_partitionKey"],
51
+ version: 2 // support large partitionkeys so that the hash is not based on just the first 100 bytes!
52
+ }
53
+ }))
54
+ )
57
55
 
58
- const basePartitionKey = config?.partitionValue() ?? "primary"
59
- const nsPrefix = (ns: string) => ns === "primary" ? "" : `${ns}::`
60
- const nsPartitionValue = (ns: string, e?: Encoded) => {
61
- const base = config?.partitionValue(e) ?? "primary"
62
- return `${nsPrefix(ns)}${base}`
56
+ const basePartitionKey = config?.partitionValue() ?? "primary"
57
+ const nsPrefix = (ns: string) => ns === "primary" ? "" : `${ns}::`
58
+ const nsPartitionValue = (ns: string, e?: Encoded) => {
59
+ const base = config?.partitionValue(e) ?? "primary"
60
+ return `${nsPrefix(ns)}${base}`
61
+ }
62
+ const nsBasePartitionKey = (ns: string) => `${nsPrefix(ns)}${basePartitionKey}`
63
+ const resolveNamespace = !config?.allowNamespace
64
+ ? Effect.succeed("primary")
65
+ : storeId.asEffect().pipe(Effect.map((namespace) => {
66
+ if (namespace !== "primary" && !config.allowNamespace!(namespace)) {
67
+ throw new Error(`Namespace ${namespace} not allowed!`)
63
68
  }
64
- const nsBasePartitionKey = (ns: string) => `${nsPrefix(ns)}${basePartitionKey}`
65
- const resolveNamespace = !config?.allowNamespace
66
- ? Effect.succeed("primary")
67
- : storeId.asEffect().pipe(Effect.map((namespace) => {
68
- if (namespace !== "primary" && !config.allowNamespace!(namespace)) {
69
- throw new Error(`Namespace ${namespace} not allowed!`)
70
- }
71
- return namespace
72
- }))
69
+ return namespace
70
+ }))
73
71
 
74
- const defaultValues = config?.defaultValues ?? {}
75
- const container = db.container(containerId)
76
- const bulk = container.items.bulk.bind(container.items)
77
- const execBatch = container.items.batch.bind(container.items)
78
- // TODO: move the marker to a separate container and get rid of the checks on every query
79
- // then need to clean up the actual data.. perhaps first do with a config toggle to prescribe to it.
80
- const importedMarkerId = containerId
72
+ const defaultValues = config?.defaultValues ?? {}
73
+ const container = db.container(containerId)
74
+ const bulk = container.items.bulk.bind(container.items)
75
+ const execBatch = container.items.batch.bind(container.items)
76
+ // TODO: move the marker to a separate container and get rid of the checks on every query
77
+ // then need to clean up the actual data.. perhaps first do with a config toggle to prescribe to it.
78
+ const importedMarkerId = containerId
81
79
 
82
- const ctx = yield* Effect.context<R>()
83
- const seedCache = new Map<string, Effect.Effect<void>>()
84
- const makeSeedEffect = (ns: string) => {
85
- const markerId = ns === "primary" ? importedMarkerId : `${importedMarkerId}::${ns}`
86
- return Effect
87
- .promise(() =>
88
- container
89
- .item(markerId, markerId)
90
- .read<{ id: string }>()
91
- .then(({ resource }) => Option.fromNullishOr(resource))
92
- )
93
- .pipe(
94
- Effect.flatMap((marker) => {
95
- if (Option.isSome(marker)) return Effect.void
96
- return InfraLogger.logInfo(`Creating mock data for ${name} (namespace: ${ns})`).pipe(
97
- Effect.andThen(seed!),
98
- Effect.flatMap((m) =>
99
- Effect.flatMapOption(
100
- Effect.succeed(toNonEmptyArray([...m])),
101
- (a) => bulkSetInternal(a, ns).pipe(Effect.orDie)
102
- )
103
- ),
104
- Effect.andThen(
105
- Effect.promise(() =>
106
- container.items.create({
107
- _partitionKey: markerId,
108
- id: markerId,
109
- ttl: -1
110
- })
111
- )
112
- ),
113
- Effect.provide(ctx),
114
- Effect.orDie
80
+ const ctx = yield* Effect.context<R>()
81
+ const seedCache = new Map<string, Effect.Effect<void>>()
82
+ const makeSeedEffect = (ns: string) => {
83
+ const markerId = ns === "primary" ? importedMarkerId : `${importedMarkerId}::${ns}`
84
+ return Effect
85
+ .promise(() =>
86
+ container
87
+ .item(markerId, markerId)
88
+ .read<{ id: string }>()
89
+ .then(({ resource }) => Option.fromNullishOr(resource))
90
+ )
91
+ .pipe(
92
+ Effect.flatMap((marker) => {
93
+ if (Option.isSome(marker)) return Effect.void
94
+ return InfraLogger.logInfo(`Creating mock data for ${name} (namespace: ${ns})`).pipe(
95
+ Effect.andThen(seed!),
96
+ Effect.flatMap((m) =>
97
+ Effect.flatMapOption(
98
+ Effect.succeed(toNonEmptyArray([...m])),
99
+ (a) => bulkSetInternal(a, ns).pipe(Effect.orDie)
100
+ )
101
+ ),
102
+ Effect.andThen(
103
+ Effect.promise(() =>
104
+ container.items.create({
105
+ _partitionKey: markerId,
106
+ id: markerId,
107
+ ttl: -1
108
+ })
115
109
  )
116
- }),
117
- Effect.withLogSpan(`Cosmos.seedCheck ${name} in ${ns} [effect-app/infra/Store]`),
118
- Effect.withSpan("Cosmos.seed [effect-app/infra/Store]", { attributes: { name, namespace: ns } })
110
+ ),
111
+ Effect.provide(ctx),
112
+ Effect.orDie
119
113
  )
120
- }
121
- const seedNamespace = Effect.fn("seedNamespace")(function*(ns: string) {
122
- if (!seed) return
123
- let cached = seedCache.get(ns)
124
- if (!cached) {
125
- cached = yield* Effect.cached(Effect.uninterruptible(makeSeedEffect(ns)))
126
- seedCache.set(ns, cached)
127
- }
128
- yield* cached
129
- })
130
- const bulkSetInternal = (items: NonEmptyReadonlyArray<PM>, ns: string) =>
131
- Effect
132
- .gen(function*() {
133
- // TODO: disable batching if need atomicity
134
- // we delay and batch to keep low amount of RUs
135
- const b = [...items]
136
- .map(
137
- (x) =>
138
- [
139
- x,
140
- Option.match(Option.fromNullishOr(x._etag), {
141
- onNone: () =>
142
- dropUndefinedT({
143
- operationType: "Create" as const,
144
- resourceBody: {
145
- ...Struct.omit(x, ["_etag", idKey]),
146
- id: x[idKey],
147
- _partitionKey: nsPartitionValue(ns, x)
148
- }
149
- // don't use this or we get an error that the request and some item partition key dont match - makese no sense
150
- // partitionKey: config?.partitionValue(x)
151
- }),
152
- onSome: (eTag) =>
153
- dropUndefinedT({
154
- operationType: "Replace" as const,
155
- id: x[idKey],
156
- resourceBody: {
157
- ...Struct.omit(x, ["_etag", idKey]),
158
- id: x[idKey],
159
- _partitionKey: nsPartitionValue(ns, x)
160
- },
161
- ifMatch: eTag
162
- // don't use this or we get an error that the request and some item partition key dont match - makese no sense
163
- // partitionKey: config?.partitionValue(x)
164
- })
114
+ }),
115
+ Effect.withLogSpan(`Cosmos.seedCheck ${name} in ${ns} [effect-app/infra/Store]`),
116
+ Effect.withSpan("Cosmos.seed [effect-app/infra/Store]", { attributes: { name, namespace: ns } })
117
+ )
118
+ }
119
+ const seedNamespace = Effect.fn("seedNamespace")(function*(ns: string) {
120
+ if (!seed) return
121
+ let cached = seedCache.get(ns)
122
+ if (!cached) {
123
+ cached = yield* Effect.cached(Effect.uninterruptible(makeSeedEffect(ns)))
124
+ seedCache.set(ns, cached)
125
+ }
126
+ yield* cached
127
+ })
128
+ const bulkSetInternal = (items: NonEmptyReadonlyArray<PM>, ns: string) =>
129
+ Effect
130
+ .gen(function*() {
131
+ // TODO: disable batching if need atomicity
132
+ // we delay and batch to keep low amount of RUs
133
+ const b = [...items]
134
+ .map(
135
+ (x) =>
136
+ [
137
+ x,
138
+ Option.match(Option.fromNullishOr(x._etag), {
139
+ onNone: () =>
140
+ dropUndefinedT({
141
+ operationType: "Create" as const,
142
+ resourceBody: {
143
+ ...Struct.omit(x, ["_etag", idKey]),
144
+ id: x[idKey],
145
+ _partitionKey: nsPartitionValue(ns, x)
146
+ }
147
+ // don't use this or we get an error that the request and some item partition key dont match - makese no sense
148
+ // partitionKey: config?.partitionValue(x)
149
+ }),
150
+ onSome: (eTag) =>
151
+ dropUndefinedT({
152
+ operationType: "Replace" as const,
153
+ id: x[idKey],
154
+ resourceBody: {
155
+ ...Struct.omit(x, ["_etag", idKey]),
156
+ id: x[idKey],
157
+ _partitionKey: nsPartitionValue(ns, x)
158
+ },
159
+ ifMatch: eTag
160
+ // don't use this or we get an error that the request and some item partition key dont match - makese no sense
161
+ // partitionKey: config?.partitionValue(x)
165
162
  })
166
- ] as const
167
- )
168
- const batches = Array.chunksOf(b, config?.maxBulkSize ?? 10)
163
+ })
164
+ ] as const
165
+ )
166
+ const batches = Array.chunksOf(b, config?.maxBulkSize ?? 10)
169
167
 
170
- const batchResult = yield* Effect.forEach(
171
- batches
172
- .map((x, i) => [i, x] as const),
173
- ([i, batch]) =>
168
+ const batchResult = yield* Effect.forEach(
169
+ batches
170
+ .map((x, i) => [i, x] as const),
171
+ ([i, batch]) =>
172
+ Effect
173
+ .promise(() => bulk(batch.map(([, op]) => op)))
174
+ .pipe(
174
175
  Effect
175
- .promise(() => bulk(batch.map(([, op]) => op)))
176
- .pipe(
177
- Effect
178
- .delay(Duration.millis(i === 0 ? 0 : 150)),
179
- Effect
180
- .flatMap((responses) =>
181
- Effect.gen(function*() {
182
- const r = responses.find((x) =>
183
- x.statusCode === 412 || x.statusCode === 404 || x.statusCode === 409
184
- )
185
- if (r) {
186
- return yield* Effect.fail(
187
- new OptimisticConcurrencyException(
188
- {
189
- type: name,
190
- id: JSON.stringify(r.resourceBody?.["id"]),
191
- code: r.statusCode,
192
- raw: responses
193
- }
194
- )
195
- )
196
- }
197
- const r2 = responses.find(
198
- (x) => x.statusCode !== 424 && (x.statusCode > 299 || x.statusCode < 200)
176
+ .delay(Duration.millis(i === 0 ? 0 : 150)),
177
+ Effect
178
+ .flatMap((responses) =>
179
+ Effect.gen(function*() {
180
+ const r = responses.find((x) =>
181
+ x.statusCode === 412 || x.statusCode === 404 || x.statusCode === 409
182
+ )
183
+ if (r) {
184
+ return yield* Effect.fail(
185
+ new OptimisticConcurrencyException(
186
+ {
187
+ type: name,
188
+ id: JSON.stringify(r.resourceBody?.["id"]),
189
+ code: r.statusCode,
190
+ raw: responses
191
+ }
199
192
  )
200
- if (r2) {
201
- return yield* Effect.die(
202
- new CosmosDbOperationError(
203
- "not able to update records: " + r2.statusCode,
204
- responses
205
- )
206
- )
207
- }
208
- const r3 = responses.find(
209
- (x) => x.statusCode > 299 || x.statusCode < 200
193
+ )
194
+ }
195
+ const r2 = responses.find(
196
+ (x) => x.statusCode !== 424 && (x.statusCode > 299 || x.statusCode < 200)
197
+ )
198
+ if (r2) {
199
+ return yield* Effect.die(
200
+ new CosmosDbOperationError(
201
+ "not able to update records: " + r2.statusCode,
202
+ responses
210
203
  )
211
- if (r3) {
212
- return yield* Effect.die(
213
- new CosmosDbOperationError(
214
- "not able to update records: " + r3.statusCode,
215
- responses
216
- )
217
- )
218
- }
219
- return batch.map(([e], i) => ({
220
- ...e,
221
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
222
- _etag: responses[i]!.eTag
223
- }))
224
- })
204
+ )
205
+ }
206
+ const r3 = responses.find(
207
+ (x) => x.statusCode > 299 || x.statusCode < 200
225
208
  )
209
+ if (r3) {
210
+ return yield* Effect.die(
211
+ new CosmosDbOperationError(
212
+ "not able to update records: " + r3.statusCode,
213
+ responses
214
+ )
215
+ )
216
+ }
217
+ return batch.map(([e], i) => ({
218
+ ...e,
219
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
220
+ _etag: responses[i]!.eTag
221
+ }))
222
+ })
226
223
  )
227
- )
228
-
229
- return batchResult.flat() as unknown as NonEmptyReadonlyArray<Encoded>
230
- })
231
- .pipe(
232
- Effect.withSpan("Cosmos.bulkSet [effect-app/infra/Store]", {
233
- attributes: { "repository.container_id": containerId, "repository.model_name": name, namespace: ns }
234
- }, { captureStackTrace: false })
235
- )
236
-
237
- const bulkSet = (items: NonEmptyReadonlyArray<PM>) =>
238
- resolveNamespace.pipe(Effect.flatMap((ns) => bulkSetInternal(items, ns)))
224
+ )
225
+ )
239
226
 
240
- const batchSet = (items: NonEmptyReadonlyArray<PM>) => {
241
- return resolveNamespace
242
- .pipe(Effect.flatMap((ns) =>
243
- Effect
244
- .suspend(() => {
245
- const batch = [...items].map(
246
- (x) =>
247
- [
248
- x,
249
- Option.match(Option.fromNullishOr(x._etag), {
250
- onNone: () => ({
251
- operationType: "Create" as const,
252
- resourceBody: {
253
- ...Struct.omit(x, ["_etag", idKey]),
254
- id: x[idKey],
255
- _partitionKey: nsPartitionValue(ns, x)
256
- }
257
- // don't use this or we get an error that the request and some item partition key dont match - makese no sense
258
- // partitionKey: config?.partitionValue(x)
259
- }),
260
- onSome: (eTag) => ({
261
- operationType: "Replace" as const,
262
- id: x[idKey],
263
- resourceBody: {
264
- ...Struct.omit(x, ["_etag", idKey]),
265
- id: x[idKey],
266
- _partitionKey: nsPartitionValue(ns, x)
267
- },
268
- // don't use this or we get an error that the request and some item partition key dont match - makese no sense
269
- // partitionKey: config?.partitionValue(x)
270
- ifMatch: eTag
271
- })
272
- })
273
- ] as const
274
- )
227
+ return batchResult.flat() as unknown as NonEmptyReadonlyArray<Encoded>
228
+ })
229
+ .pipe(
230
+ Effect.withSpan("Cosmos.bulkSet [effect-app/infra/Store]", {
231
+ attributes: { "repository.container_id": containerId, "repository.model_name": name, namespace: ns }
232
+ }, { captureStackTrace: false })
233
+ )
275
234
 
276
- const ex = batch.map(([, c]) => c)
235
+ const bulkSet = (items: NonEmptyReadonlyArray<PM>) =>
236
+ resolveNamespace.pipe(Effect.flatMap((ns) => bulkSetInternal(items, ns)))
277
237
 
278
- return Effect
279
- .promise(() => execBatch(ex, ex[0]?.resourceBody._partitionKey))
280
- .pipe(Effect.flatMap(Effect.fnUntraced(function*(x) {
281
- const result = x.result ?? []
282
- const firstFailed = result.find(
283
- (x: any) => x.statusCode > 299 || x.statusCode < 200
284
- )
285
- if (firstFailed) {
286
- const code = firstFailed.statusCode ?? 0
287
- if (code === 412 || code === 404 || code === 409) {
288
- return yield* new OptimisticConcurrencyException({ type: name, id: "batch", code })
238
+ const batchSet = (items: NonEmptyReadonlyArray<PM>) => {
239
+ return resolveNamespace
240
+ .pipe(Effect.flatMap((ns) =>
241
+ Effect
242
+ .suspend(() => {
243
+ const batch = [...items].map(
244
+ (x) =>
245
+ [
246
+ x,
247
+ Option.match(Option.fromNullishOr(x._etag), {
248
+ onNone: () => ({
249
+ operationType: "Create" as const,
250
+ resourceBody: {
251
+ ...Struct.omit(x, ["_etag", idKey]),
252
+ id: x[idKey],
253
+ _partitionKey: nsPartitionValue(ns, x)
289
254
  }
255
+ // don't use this or we get an error that the request and some item partition key dont match - makese no sense
256
+ // partitionKey: config?.partitionValue(x)
257
+ }),
258
+ onSome: (eTag) => ({
259
+ operationType: "Replace" as const,
260
+ id: x[idKey],
261
+ resourceBody: {
262
+ ...Struct.omit(x, ["_etag", idKey]),
263
+ id: x[idKey],
264
+ _partitionKey: nsPartitionValue(ns, x)
265
+ },
266
+ // don't use this or we get an error that the request and some item partition key dont match - makese no sense
267
+ // partitionKey: config?.partitionValue(x)
268
+ ifMatch: eTag
269
+ })
270
+ })
271
+ ] as const
272
+ )
290
273
 
291
- return yield* Effect.die(
292
- new CosmosDbOperationError("not able to update record: " + code)
293
- )
294
- }
274
+ const ex = batch.map(([, c]) => c)
295
275
 
296
- return batch.map(([e], i) => ({
297
- ...e,
298
- _etag: result[i]?.eTag
299
- })) as unknown as NonEmptyReadonlyArray<Encoded>
300
- })))
301
- })
302
- .pipe(Effect
303
- .withSpan("Cosmos.batchSet [effect-app/infra/Store]", {
304
- attributes: {
305
- "repository.container_id": containerId,
306
- "repository.model_name": name,
307
- namespace: ns
276
+ return Effect
277
+ .promise(() => execBatch(ex, ex[0]?.resourceBody._partitionKey))
278
+ .pipe(Effect.flatMap(Effect.fnUntraced(function*(x) {
279
+ const result = x.result ?? []
280
+ const firstFailed = result.find(
281
+ (x: any) => x.statusCode > 299 || x.statusCode < 200
282
+ )
283
+ if (firstFailed) {
284
+ const code = firstFailed.statusCode ?? 0
285
+ if (code === 412 || code === 404 || code === 409) {
286
+ return yield* new OptimisticConcurrencyException({ type: name, id: "batch", code })
308
287
  }
309
- }, { captureStackTrace: false }))
310
- ))
311
- }
312
-
313
- const s: Store<IdKey, Encoded> = {
314
- seedNamespace: (ns) => seedNamespace(ns),
315
288
 
316
- queryRaw: <Out>(query: RawQuery<Encoded, Out>) =>
317
- Effect
318
- .all({ q: Effect.sync(() => query.cosmos({ name })), ns: resolveNamespace })
319
- .pipe(
320
- Effect.tap(({ q }) => logQuery(q)),
321
- Effect.flatMap(({ ns, q }) =>
322
- Effect
323
- .promise(() =>
324
- container
325
- .items
326
- .query<Out>(q, { partitionKey: nsBasePartitionKey(ns) })
327
- .fetchAll()
328
- .then(({ resources }) =>
329
- resources.map(
330
- (_) => ({ ...defaultValues, ...mapReverseId(_ as any) }) as Out
331
- )
332
- )
289
+ return yield* Effect.die(
290
+ new CosmosDbOperationError("not able to update record: " + code)
333
291
  )
334
- .pipe(
335
- Effect.withSpan("Cosmos.queryRaw [effect-app/infra/Store]", {
336
- attributes: {
337
- "repository.container_id": containerId,
338
- "repository.model_name": name,
339
- namespace: ns
340
- }
341
- }, { captureStackTrace: false })
342
- )
343
- )
344
- ),
345
- batchRemove: (ids, partitionKey?: string) =>
346
- resolveNamespace.pipe(Effect.flatMap((ns) =>
292
+ }
293
+
294
+ return batch.map(([e], i) => ({
295
+ ...e,
296
+ _etag: result[i]?.eTag
297
+ })) as unknown as NonEmptyReadonlyArray<Encoded>
298
+ })))
299
+ })
300
+ .pipe(Effect
301
+ .withSpan("Cosmos.batchSet [effect-app/infra/Store]", {
302
+ attributes: {
303
+ "repository.container_id": containerId,
304
+ "repository.model_name": name,
305
+ namespace: ns
306
+ }
307
+ }, { captureStackTrace: false }))
308
+ ))
309
+ }
310
+
311
+ const s: Store<IdKey, Encoded> = {
312
+ seedNamespace: (ns) => seedNamespace(ns),
313
+
314
+ queryRaw: <Out>(query: RawQuery<Encoded, Out>) =>
315
+ Effect
316
+ .all({ q: Effect.sync(() => query.cosmos({ name })), ns: resolveNamespace })
317
+ .pipe(
318
+ Effect.tap(({ q }) => logQuery(q)),
319
+ Effect.flatMap(({ ns, q }) =>
347
320
  Effect
348
321
  .promise(() =>
349
- execBatch(
350
- mutable(ids.map((id) =>
351
- dropUndefinedT({
352
- operationType: "Delete" as const,
353
- id
354
- // don't use this or we get an error that the request and some item partition key dont match - makese no sense
355
- // partitionKey: config?.partitionValue({ [idKey]: id } as Encoded)
356
- })
357
- )),
358
- partitionKey ?? nsBasePartitionKey(ns)
359
- )
322
+ container
323
+ .items
324
+ .query<Out>(q, { partitionKey: nsBasePartitionKey(ns) })
325
+ .fetchAll()
326
+ .then(({ resources }) =>
327
+ resources.map(
328
+ (_) => ({ ...defaultValues, ...mapReverseId(_ as any) }) as Out
329
+ )
330
+ )
360
331
  )
361
332
  .pipe(
362
- Effect.withSpan("Cosmos.batchRemove [effect-app/infra/Store]", {
333
+ Effect.withSpan("Cosmos.queryRaw [effect-app/infra/Store]", {
363
334
  attributes: {
364
335
  "repository.container_id": containerId,
365
336
  "repository.model_name": name,
@@ -367,32 +338,126 @@ function makeCosmosStore({ prefix }: StorageConfig) {
367
338
  }
368
339
  }, { captureStackTrace: false })
369
340
  )
370
- )),
371
- all: Effect
372
- .all({
373
- q: Effect.sync(() => ({
374
- query: `SELECT * FROM ${name}`,
375
- parameters: []
376
- })),
377
- ns: resolveNamespace
378
- })
341
+ )
342
+ ),
343
+ batchRemove: (ids, partitionKey?: string) =>
344
+ resolveNamespace.pipe(Effect.flatMap((ns) =>
345
+ Effect
346
+ .promise(() =>
347
+ execBatch(
348
+ mutable(ids.map((id) =>
349
+ dropUndefinedT({
350
+ operationType: "Delete" as const,
351
+ id
352
+ // don't use this or we get an error that the request and some item partition key dont match - makese no sense
353
+ // partitionKey: config?.partitionValue({ [idKey]: id } as Encoded)
354
+ })
355
+ )),
356
+ partitionKey ?? nsBasePartitionKey(ns)
357
+ )
358
+ )
379
359
  .pipe(
380
- Effect.tap(({ q }) => logQuery(q)),
381
- Effect.flatMap(({ ns, q }) =>
360
+ Effect.withSpan("Cosmos.batchRemove [effect-app/infra/Store]", {
361
+ attributes: {
362
+ "repository.container_id": containerId,
363
+ "repository.model_name": name,
364
+ namespace: ns
365
+ }
366
+ }, { captureStackTrace: false })
367
+ )
368
+ )),
369
+ all: Effect
370
+ .all({
371
+ q: Effect.sync(() => ({
372
+ query: `SELECT * FROM ${name}`,
373
+ parameters: []
374
+ })),
375
+ ns: resolveNamespace
376
+ })
377
+ .pipe(
378
+ Effect.tap(({ q }) => logQuery(q)),
379
+ Effect.flatMap(({ ns, q }) =>
380
+ Effect
381
+ .promise(() =>
382
+ container
383
+ .items
384
+ .query<PMCosmos>(q, { partitionKey: nsBasePartitionKey(ns) })
385
+ .fetchAll()
386
+ .then(({ resources }) =>
387
+ resources.map(
388
+ (_) => ({ ...defaultValues, ...mapReverseId(_) })
389
+ )
390
+ )
391
+ )
392
+ .pipe(
393
+ Effect.withSpan("Cosmos.all [effect-app/infra/Store]", {
394
+ attributes: {
395
+ "repository.container_id": containerId,
396
+ "repository.model_name": name,
397
+ namespace: ns
398
+ }
399
+ }, { captureStackTrace: false })
400
+ )
401
+ )
402
+ ),
403
+ /**
404
+ * May return duplicate results for "join_find", when matching more than once.
405
+ */
406
+ filter: <U extends keyof Encoded = never>(
407
+ f: FilterArgs<Encoded, U>
408
+ ) => {
409
+ const skip = f?.skip
410
+ const limit = f?.limit
411
+ const filter = f.filter
412
+ type M = U extends undefined ? Encoded : Pick<Encoded, U>
413
+ return Effect
414
+ .all({
415
+ q: Effect.sync(() =>
416
+ buildWhereCosmosQuery3(
417
+ idKey,
418
+ filter ? [{ t: "where-scope", result: filter, relation: "some" }] : [],
419
+ name,
420
+ defaultValues,
421
+ f.select as
422
+ | NonEmptyReadonlyArray<string | { key: string; subKeys: readonly string[] }>
423
+ | undefined,
424
+ f.order as NonEmptyReadonlyArray<{ key: string; direction: "ASC" | "DESC" }> | undefined,
425
+ skip,
426
+ limit
427
+ )
428
+ ),
429
+ ns: resolveNamespace
430
+ })
431
+ .pipe(
432
+ Effect.tap(({ q }) => logQuery(q)),
433
+ Effect
434
+ .flatMap(({ ns, q }) =>
382
435
  Effect
383
436
  .promise(() =>
384
- container
385
- .items
386
- .query<PMCosmos>(q, { partitionKey: nsBasePartitionKey(ns) })
387
- .fetchAll()
388
- .then(({ resources }) =>
389
- resources.map(
390
- (_) => ({ ...defaultValues, ...mapReverseId(_) })
437
+ f.select
438
+ ? container
439
+ .items
440
+ .query<M>(q, { partitionKey: nsBasePartitionKey(ns) })
441
+ .fetchAll()
442
+ .then(({ resources }) =>
443
+ resources.map((_) => ({
444
+ ...pipe(
445
+ defaultValues,
446
+ Struct.pick(f.select!.filter((_) => typeof _ === "string") as never[])
447
+ ),
448
+ ...mapReverseId(_ as any)
449
+ }))
450
+ )
451
+ : container
452
+ .items
453
+ .query<{ f: M }>(q, { partitionKey: nsBasePartitionKey(ns) })
454
+ .fetchAll()
455
+ .then(({ resources }) =>
456
+ resources.map(({ f }) => ({ ...defaultValues, ...mapReverseId(f as any) }) as any)
391
457
  )
392
- )
393
458
  )
394
459
  .pipe(
395
- Effect.withSpan("Cosmos.all [effect-app/infra/Store]", {
460
+ Effect.withSpan("Cosmos.filter [effect-app/infra/Store]", {
396
461
  attributes: {
397
462
  "repository.container_id": containerId,
398
463
  "repository.model_name": name,
@@ -401,170 +466,102 @@ function makeCosmosStore({ prefix }: StorageConfig) {
401
466
  }, { captureStackTrace: false })
402
467
  )
403
468
  )
404
- ),
405
- /**
406
- * May return duplicate results for "join_find", when matching more than once.
407
- */
408
- filter: <U extends keyof Encoded = never>(
409
- f: FilterArgs<Encoded, U>
410
- ) => {
411
- const skip = f?.skip
412
- const limit = f?.limit
413
- const filter = f.filter
414
- type M = U extends undefined ? Encoded : Pick<Encoded, U>
415
- return Effect
416
- .all({
417
- q: Effect.sync(() =>
418
- buildWhereCosmosQuery3(
419
- idKey,
420
- filter ? [{ t: "where-scope", result: filter, relation: "some" }] : [],
421
- name,
422
- defaultValues,
423
- f.select as
424
- | NonEmptyReadonlyArray<string | { key: string; subKeys: readonly string[] }>
425
- | undefined,
426
- f.order as NonEmptyReadonlyArray<{ key: string; direction: "ASC" | "DESC" }> | undefined,
427
- skip,
428
- limit
469
+ )
470
+ },
471
+ find: (id) =>
472
+ resolveNamespace.pipe(Effect.flatMap((ns) =>
473
+ Effect
474
+ .promise(() =>
475
+ container
476
+ .item(id, nsPartitionValue(ns, { [idKey]: id } as Encoded))
477
+ .read<Encoded>()
478
+ .then(({ resource }) =>
479
+ Option.fromNullishOr(resource).pipe(
480
+ Option.map((_) => ({ ...defaultValues, ...mapReverseId(_) }))
429
481
  )
430
- ),
431
- ns: resolveNamespace
432
- })
433
- .pipe(
434
- Effect.tap(({ q }) => logQuery(q)),
435
- Effect
436
- .flatMap(({ ns, q }) =>
437
- Effect
438
- .promise(() =>
439
- f.select
440
- ? container
441
- .items
442
- .query<M>(q, { partitionKey: nsBasePartitionKey(ns) })
443
- .fetchAll()
444
- .then(({ resources }) =>
445
- resources.map((_) => ({
446
- ...pipe(
447
- defaultValues,
448
- Struct.pick(f.select!.filter((_) => typeof _ === "string") as never[])
449
- ),
450
- ...mapReverseId(_ as any)
451
- }))
452
- )
453
- : container
454
- .items
455
- .query<{ f: M }>(q, { partitionKey: nsBasePartitionKey(ns) })
456
- .fetchAll()
457
- .then(({ resources }) =>
458
- resources.map(({ f }) => ({ ...defaultValues, ...mapReverseId(f as any) }) as any)
459
- )
460
- )
461
- .pipe(
462
- Effect.withSpan("Cosmos.filter [effect-app/infra/Store]", {
463
- attributes: {
464
- "repository.container_id": containerId,
465
- "repository.model_name": name,
466
- namespace: ns
467
- }
468
- }, { captureStackTrace: false })
469
- )
482
+ )
483
+ )
484
+ .pipe(Effect
485
+ .withSpan("Cosmos.find [effect-app/infra/Store]", {
486
+ attributes: {
487
+ "repository.container_id": containerId,
488
+ "repository.model_name": name,
489
+ partitionValue: nsPartitionValue(ns, { [idKey]: id } as Encoded),
490
+ namespace: ns,
491
+ id
492
+ }
493
+ }, { captureStackTrace: false }))
494
+ )),
495
+ set: (e) =>
496
+ resolveNamespace.pipe(Effect.flatMap((ns) =>
497
+ Option
498
+ .match(
499
+ Option
500
+ .fromNullishOr(e._etag),
501
+ {
502
+ onNone: () =>
503
+ Effect.promise(() =>
504
+ container.items.create({
505
+ ...mapId(e),
506
+ _partitionKey: nsPartitionValue(ns, e)
507
+ })
508
+ ),
509
+ onSome: (eTag) =>
510
+ Effect.promise(() =>
511
+ container.item(e[idKey], nsPartitionValue(ns, e)).replace(
512
+ { ...mapId(e), _partitionKey: nsPartitionValue(ns, e) },
513
+ {
514
+ accessCondition: {
515
+ type: "IfMatch",
516
+ condition: eTag
517
+ }
518
+ }
519
+ )
470
520
  )
471
- )
472
- },
473
- find: (id) =>
474
- resolveNamespace.pipe(Effect.flatMap((ns) =>
521
+ }
522
+ )
523
+ .pipe(
475
524
  Effect
476
- .promise(() =>
477
- container
478
- .item(id, nsPartitionValue(ns, { [idKey]: id } as Encoded))
479
- .read<Encoded>()
480
- .then(({ resource }) =>
481
- Option.fromNullishOr(resource).pipe(
482
- Option.map((_) => ({ ...defaultValues, ...mapReverseId(_) }))
483
- )
525
+ .flatMap((x) => {
526
+ if (x.statusCode === 412 || x.statusCode === 404 || x.statusCode === 409) {
527
+ return Effect.fail(
528
+ new OptimisticConcurrencyException({ type: name, id: e[idKey], code: x.statusCode })
484
529
  )
485
- )
486
- .pipe(Effect
487
- .withSpan("Cosmos.find [effect-app/infra/Store]", {
488
- attributes: {
489
- "repository.container_id": containerId,
490
- "repository.model_name": name,
491
- partitionValue: nsPartitionValue(ns, { [idKey]: id } as Encoded),
492
- namespace: ns,
493
- id
494
- }
495
- }, { captureStackTrace: false }))
496
- )),
497
- set: (e) =>
498
- resolveNamespace.pipe(Effect.flatMap((ns) =>
499
- Option
500
- .match(
501
- Option
502
- .fromNullishOr(e._etag),
503
- {
504
- onNone: () =>
505
- Effect.promise(() =>
506
- container.items.create({
507
- ...mapId(e),
508
- _partitionKey: nsPartitionValue(ns, e)
509
- })
510
- ),
511
- onSome: (eTag) =>
512
- Effect.promise(() =>
513
- container.item(e[idKey], nsPartitionValue(ns, e)).replace(
514
- { ...mapId(e), _partitionKey: nsPartitionValue(ns, e) },
515
- {
516
- accessCondition: {
517
- type: "IfMatch",
518
- condition: eTag
519
- }
520
- }
521
- )
530
+ }
531
+ if (x.statusCode > 299 || x.statusCode < 200) {
532
+ return Effect.die(
533
+ new CosmosDbOperationError(
534
+ "not able to update record: " + x.statusCode
522
535
  )
536
+ )
523
537
  }
524
- )
525
- .pipe(
526
- Effect
527
- .flatMap((x) => {
528
- if (x.statusCode === 412 || x.statusCode === 404 || x.statusCode === 409) {
529
- return Effect.fail(
530
- new OptimisticConcurrencyException({ type: name, id: e[idKey], code: x.statusCode })
531
- )
532
- }
533
- if (x.statusCode > 299 || x.statusCode < 200) {
534
- return Effect.die(
535
- new CosmosDbOperationError(
536
- "not able to update record: " + x.statusCode
537
- )
538
- )
539
- }
540
- return Effect.sync(() => ({
541
- ...e,
542
- _etag: x.etag
543
- }))
544
- }),
545
- Effect
546
- .withSpan("Cosmos.set [effect-app/infra/Store]", {
547
- attributes: {
548
- "repository.container_id": containerId,
549
- "repository.model_name": name,
550
- namespace: ns,
551
- id: e[idKey]
552
- }
553
- }, { captureStackTrace: false })
554
- )
555
- )),
556
- batchSet,
557
- bulkSet
558
- }
538
+ return Effect.sync(() => ({
539
+ ...e,
540
+ _etag: x.etag
541
+ }))
542
+ }),
543
+ Effect
544
+ .withSpan("Cosmos.set [effect-app/infra/Store]", {
545
+ attributes: {
546
+ "repository.container_id": containerId,
547
+ "repository.model_name": name,
548
+ namespace: ns,
549
+ id: e[idKey]
550
+ }
551
+ }, { captureStackTrace: false })
552
+ )
553
+ )),
554
+ batchSet,
555
+ bulkSet
556
+ }
559
557
 
560
- // Eagerly seed primary namespace on initialization
561
- yield* seedNamespace("primary")
558
+ // Eagerly seed primary namespace on initialization
559
+ yield* seedNamespace("primary")
562
560
 
563
- return s
564
- })
565
- }
566
- })
567
- }
561
+ return s
562
+ })
563
+ }
564
+ })
568
565
 
569
566
  export function CosmosStoreLayer(cfg: StorageConfig) {
570
567
  return StoreMaker