@effect-app/infra 4.0.0-beta.81 → 4.0.0-beta.83

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.
@@ -9,6 +9,7 @@ import { InfraLogger } from "../logger.js"
9
9
  import type { FieldValues } from "../Model/filter/types.js"
10
10
  import { type RawQuery } from "../Model/query.js"
11
11
  import { buildWhereCosmosQuery3, logQuery } from "./Cosmos/query.js"
12
+ import { storeId } from "./Memory.js"
12
13
  import { type FilterArgs, type PersistenceModelType, type StorageConfig, type Store, type StoreConfig, StoreMaker } from "./service.js"
13
14
 
14
15
  const makeMapId =
@@ -50,7 +51,21 @@ function makeCosmosStore({ prefix }: StorageConfig) {
50
51
  }))
51
52
  )
52
53
 
53
- const mainPartitionKey = config?.partitionValue() ?? "primary"
54
+ const basePartitionKey = config?.partitionValue() ?? "primary"
55
+ const nsPrefix = (ns: string) => ns === "primary" ? "" : `${ns}::`
56
+ const nsPartitionValue = (ns: string, e?: Encoded) => {
57
+ const base = config?.partitionValue(e) ?? "primary"
58
+ return `${nsPrefix(ns)}${base}`
59
+ }
60
+ const resolveNamespace = !config?.allowNamespace
61
+ ? Effect.succeed("primary")
62
+ : storeId.asEffect().pipe(Effect.map((namespace) => {
63
+ if (namespace !== "primary" && !config.allowNamespace!(namespace)) {
64
+ throw new Error(`Namespace ${namespace} not allowed!`)
65
+ }
66
+ return namespace
67
+ }))
68
+ const resolvePartitionKey = Effect.map(resolveNamespace, (ns) => `${nsPrefix(ns)}${basePartitionKey}`)
54
69
 
55
70
  const defaultValues = config?.defaultValues ?? {}
56
71
  const container = db.container(containerId)
@@ -63,6 +78,7 @@ function makeCosmosStore({ prefix }: StorageConfig) {
63
78
  const bulkSet = (items: NonEmptyReadonlyArray<PM>) =>
64
79
  Effect
65
80
  .gen(function*() {
81
+ const ns = yield* resolveNamespace
66
82
  // TODO: disable batching if need atomicity
67
83
  // we delay and batch to keep low amount of RUs
68
84
  const b = [...items]
@@ -77,7 +93,7 @@ function makeCosmosStore({ prefix }: StorageConfig) {
77
93
  resourceBody: {
78
94
  ...Struct.omit(x, ["_etag", idKey]),
79
95
  id: x[idKey],
80
- _partitionKey: config?.partitionValue(x)
96
+ _partitionKey: nsPartitionValue(ns, x)
81
97
  }
82
98
  // don't use this or we get an error that the request and some item partition key dont match - makese no sense
83
99
  // partitionKey: config?.partitionValue(x)
@@ -89,7 +105,7 @@ function makeCosmosStore({ prefix }: StorageConfig) {
89
105
  resourceBody: {
90
106
  ...Struct.omit(x, ["_etag", idKey]),
91
107
  id: x[idKey],
92
- _partitionKey: config?.partitionValue(x)
108
+ _partitionKey: nsPartitionValue(ns, x)
93
109
  },
94
110
  ifMatch: eTag
95
111
  // don't use this or we get an error that the request and some item partition key dont match - makese no sense
@@ -166,65 +182,68 @@ function makeCosmosStore({ prefix }: StorageConfig) {
166
182
  }, { captureStackTrace: false }))
167
183
 
168
184
  const batchSet = (items: NonEmptyReadonlyArray<PM>) => {
169
- return Effect
170
- .suspend(() => {
171
- const batch = [...items].map(
172
- (x) =>
173
- [
174
- x,
175
- Option.match(Option.fromNullishOr(x._etag), {
176
- onNone: () => ({
177
- operationType: "Create" as const,
178
- resourceBody: {
179
- ...Struct.omit(x, ["_etag", idKey]),
180
- id: x[idKey],
181
- _partitionKey: config?.partitionValue(x)
182
- }
183
- // don't use this or we get an error that the request and some item partition key dont match - makese no sense
184
- // partitionKey: config?.partitionValue(x)
185
- }),
186
- onSome: (eTag) => ({
187
- operationType: "Replace" as const,
188
- id: x[idKey],
189
- resourceBody: {
190
- ...Struct.omit(x, ["_etag", idKey]),
191
- id: x[idKey],
192
- _partitionKey: config?.partitionValue(x)
193
- },
194
- // don't use this or we get an error that the request and some item partition key dont match - makese no sense
195
- // partitionKey: config?.partitionValue(x)
196
- ifMatch: eTag
197
- })
198
- })
199
- ] as const
200
- )
185
+ return resolveNamespace
186
+ .pipe(Effect.flatMap((ns) =>
187
+ Effect
188
+ .suspend(() => {
189
+ const batch = [...items].map(
190
+ (x) =>
191
+ [
192
+ x,
193
+ Option.match(Option.fromNullishOr(x._etag), {
194
+ onNone: () => ({
195
+ operationType: "Create" as const,
196
+ resourceBody: {
197
+ ...Struct.omit(x, ["_etag", idKey]),
198
+ id: x[idKey],
199
+ _partitionKey: nsPartitionValue(ns, x)
200
+ }
201
+ // don't use this or we get an error that the request and some item partition key dont match - makese no sense
202
+ // partitionKey: config?.partitionValue(x)
203
+ }),
204
+ onSome: (eTag) => ({
205
+ operationType: "Replace" as const,
206
+ id: x[idKey],
207
+ resourceBody: {
208
+ ...Struct.omit(x, ["_etag", idKey]),
209
+ id: x[idKey],
210
+ _partitionKey: nsPartitionValue(ns, x)
211
+ },
212
+ // don't use this or we get an error that the request and some item partition key dont match - makese no sense
213
+ // partitionKey: config?.partitionValue(x)
214
+ ifMatch: eTag
215
+ })
216
+ })
217
+ ] as const
218
+ )
201
219
 
202
- const ex = batch.map(([, c]) => c)
220
+ const ex = batch.map(([, c]) => c)
203
221
 
204
- return Effect
205
- .promise(() => execBatch(ex, ex[0]?.resourceBody._partitionKey))
206
- .pipe(Effect.flatMap(Effect.fnUntraced(function*(x) {
207
- const result = x.result ?? []
208
- const firstFailed = result.find(
209
- (x: any) => x.statusCode > 299 || x.statusCode < 200
210
- )
211
- if (firstFailed) {
212
- const code = firstFailed.statusCode ?? 0
213
- if (code === 412 || code === 404 || code === 409) {
214
- return yield* new OptimisticConcurrencyException({ type: name, id: "batch", code })
215
- }
222
+ return Effect
223
+ .promise(() => execBatch(ex, ex[0]?.resourceBody._partitionKey))
224
+ .pipe(Effect.flatMap(Effect.fnUntraced(function*(x) {
225
+ const result = x.result ?? []
226
+ const firstFailed = result.find(
227
+ (x: any) => x.statusCode > 299 || x.statusCode < 200
228
+ )
229
+ if (firstFailed) {
230
+ const code = firstFailed.statusCode ?? 0
231
+ if (code === 412 || code === 404 || code === 409) {
232
+ return yield* new OptimisticConcurrencyException({ type: name, id: "batch", code })
233
+ }
216
234
 
217
- return yield* Effect.die(
218
- new CosmosDbOperationError("not able to update record: " + code)
219
- )
220
- }
235
+ return yield* Effect.die(
236
+ new CosmosDbOperationError("not able to update record: " + code)
237
+ )
238
+ }
221
239
 
222
- return batch.map(([e], i) => ({
223
- ...e,
224
- _etag: result[i]?.eTag
225
- })) as unknown as NonEmptyReadonlyArray<Encoded>
226
- })))
227
- })
240
+ return batch.map(([e], i) => ({
241
+ ...e,
242
+ _etag: result[i]?.eTag
243
+ })) as unknown as NonEmptyReadonlyArray<Encoded>
244
+ })))
245
+ })
246
+ ))
228
247
  .pipe(Effect
229
248
  .withSpan("Cosmos.batchSet [effect-app/infra/Store]", {
230
249
  attributes: { "repository.container_id": containerId, "repository.model_name": name }
@@ -234,14 +253,14 @@ function makeCosmosStore({ prefix }: StorageConfig) {
234
253
  const s: Store<IdKey, Encoded> = {
235
254
  queryRaw: <Out>(query: RawQuery<Encoded, Out>) =>
236
255
  Effect
237
- .sync(() => query.cosmos({ name }))
256
+ .all({ q: Effect.sync(() => query.cosmos({ name })), pk: resolvePartitionKey })
238
257
  .pipe(
239
- Effect.tap((q) => logQuery(q)),
240
- Effect.flatMap((q) =>
258
+ Effect.tap(({ q }) => logQuery(q)),
259
+ Effect.flatMap(({ pk, q }) =>
241
260
  Effect.promise(() =>
242
261
  container
243
262
  .items
244
- .query<Out>(q, { partitionKey: mainPartitionKey })
263
+ .query<Out>(q, { partitionKey: pk })
245
264
  .fetchAll()
246
265
  .then(({ resources }) =>
247
266
  resources.map(
@@ -256,31 +275,36 @@ function makeCosmosStore({ prefix }: StorageConfig) {
256
275
  }, { captureStackTrace: false })
257
276
  ),
258
277
  batchRemove: (ids, partitionKey?: string) =>
259
- Effect.promise(() =>
260
- execBatch(
261
- mutable(ids.map((id) =>
262
- dropUndefinedT({
263
- operationType: "Delete" as const,
264
- id
265
- // don't use this or we get an error that the request and some item partition key dont match - makese no sense
266
- // partitionKey: config?.partitionValue({ [idKey]: id } as Encoded)
267
- })
268
- )),
269
- partitionKey ?? mainPartitionKey
278
+ resolvePartitionKey.pipe(Effect.flatMap((pk) =>
279
+ Effect.promise(() =>
280
+ execBatch(
281
+ mutable(ids.map((id) =>
282
+ dropUndefinedT({
283
+ operationType: "Delete" as const,
284
+ id
285
+ // don't use this or we get an error that the request and some item partition key dont match - makese no sense
286
+ // partitionKey: config?.partitionValue({ [idKey]: id } as Encoded)
287
+ })
288
+ )),
289
+ partitionKey ?? pk
290
+ )
270
291
  )
271
- ),
292
+ )),
272
293
  all: Effect
273
- .sync(() => ({
274
- query: `SELECT * FROM ${name}`,
275
- parameters: []
276
- }))
294
+ .all({
295
+ q: Effect.sync(() => ({
296
+ query: `SELECT * FROM ${name}`,
297
+ parameters: []
298
+ })),
299
+ pk: resolvePartitionKey
300
+ })
277
301
  .pipe(
278
- Effect.tap((q) => logQuery(q)),
279
- Effect.flatMap((q) =>
302
+ Effect.tap(({ q }) => logQuery(q)),
303
+ Effect.flatMap(({ pk, q }) =>
280
304
  Effect.promise(() =>
281
305
  container
282
306
  .items
283
- .query<PMCosmos>(q, { partitionKey: mainPartitionKey })
307
+ .query<PMCosmos>(q, { partitionKey: pk })
284
308
  .fetchAll()
285
309
  .then(({ resources }) =>
286
310
  resources.map(
@@ -305,27 +329,32 @@ function makeCosmosStore({ prefix }: StorageConfig) {
305
329
  const filter = f.filter
306
330
  type M = U extends undefined ? Encoded : Pick<Encoded, U>
307
331
  return Effect
308
- .sync(() =>
309
- buildWhereCosmosQuery3(
310
- idKey,
311
- filter ? [{ t: "where-scope", result: filter, relation: "some" }] : [],
312
- name,
313
- defaultValues,
314
- f.select as NonEmptyReadonlyArray<string | { key: string; subKeys: readonly string[] }> | undefined,
315
- f.order as NonEmptyReadonlyArray<{ key: string; direction: "ASC" | "DESC" }> | undefined,
316
- skip,
317
- limit
318
- )
319
- )
332
+ .all({
333
+ q: Effect.sync(() =>
334
+ buildWhereCosmosQuery3(
335
+ idKey,
336
+ filter ? [{ t: "where-scope", result: filter, relation: "some" }] : [],
337
+ name,
338
+ defaultValues,
339
+ f.select as
340
+ | NonEmptyReadonlyArray<string | { key: string; subKeys: readonly string[] }>
341
+ | undefined,
342
+ f.order as NonEmptyReadonlyArray<{ key: string; direction: "ASC" | "DESC" }> | undefined,
343
+ skip,
344
+ limit
345
+ )
346
+ ),
347
+ pk: resolvePartitionKey
348
+ })
320
349
  .pipe(
321
- Effect.tap((q) => logQuery(q)),
350
+ Effect.tap(({ q }) => logQuery(q)),
322
351
  Effect
323
- .flatMap((q) =>
352
+ .flatMap(({ pk, q }) =>
324
353
  Effect.promise(() =>
325
354
  f.select
326
355
  ? container
327
356
  .items
328
- .query<M>(q, { partitionKey: mainPartitionKey })
357
+ .query<M>(q, { partitionKey: pk })
329
358
  .fetchAll()
330
359
  .then(({ resources }) =>
331
360
  resources.map((_) => ({
@@ -338,7 +367,7 @@ function makeCosmosStore({ prefix }: StorageConfig) {
338
367
  )
339
368
  : container
340
369
  .items
341
- .query<{ f: M }>(q, { partitionKey: mainPartitionKey })
370
+ .query<{ f: M }>(q, { partitionKey: pk })
342
371
  .fetchAll()
343
372
  .then(({ resources }) =>
344
373
  resources.map(({ f }) => ({ ...defaultValues, ...mapReverseId(f as any) }) as any)
@@ -353,80 +382,86 @@ function makeCosmosStore({ prefix }: StorageConfig) {
353
382
  )
354
383
  },
355
384
  find: (id) =>
356
- Effect
357
- .promise(() =>
358
- container
359
- .item(id, config?.partitionValue({ [idKey]: id } as Encoded))
360
- .read<Encoded>()
361
- .then(({ resource }) =>
362
- Option.fromNullishOr(resource).pipe(Option.map((_) => ({ ...defaultValues, ...mapReverseId(_) })))
363
- )
364
- )
365
- .pipe(Effect
366
- .withSpan("Cosmos.find [effect-app/infra/Store]", {
367
- attributes: {
368
- "repository.container_id": containerId,
369
- "repository.model_name": name,
370
- partitionValue: config?.partitionValue({ [idKey]: id } as Encoded),
371
- id
372
- }
373
- }, { captureStackTrace: false })),
374
- set: (e) =>
375
- Option
376
- .match(
377
- Option
378
- .fromNullishOr(e._etag),
379
- {
380
- onNone: () =>
381
- Effect.promise(() =>
382
- container.items.create({
383
- ...mapId(e),
384
- _partitionKey: config?.partitionValue(e)
385
- })
386
- ),
387
- onSome: (eTag) =>
388
- Effect.promise(() =>
389
- container.item(e[idKey], config?.partitionValue(e)).replace(
390
- { ...mapId(e), _partitionKey: config?.partitionValue(e) },
391
- {
392
- accessCondition: {
393
- type: "IfMatch",
394
- condition: eTag
395
- }
396
- }
385
+ resolveNamespace.pipe(Effect.flatMap((ns) =>
386
+ Effect
387
+ .promise(() =>
388
+ container
389
+ .item(id, nsPartitionValue(ns, { [idKey]: id } as Encoded))
390
+ .read<Encoded>()
391
+ .then(({ resource }) =>
392
+ Option.fromNullishOr(resource).pipe(
393
+ Option.map((_) => ({ ...defaultValues, ...mapReverseId(_) }))
397
394
  )
398
395
  )
399
- }
400
- )
401
- .pipe(
402
- Effect
403
- .flatMap((x) => {
404
- if (x.statusCode === 412 || x.statusCode === 404 || x.statusCode === 409) {
405
- return Effect.fail(
406
- new OptimisticConcurrencyException({ type: name, id: e[idKey], code: x.statusCode })
407
- )
408
- }
409
- if (x.statusCode > 299 || x.statusCode < 200) {
410
- return Effect.die(
411
- new CosmosDbOperationError(
412
- "not able to update record: " + x.statusCode
413
- )
414
- )
415
- }
416
- return Effect.sync(() => ({
417
- ...e,
418
- _etag: x.etag
419
- }))
420
- }),
421
- Effect
422
- .withSpan("Cosmos.set [effect-app/infra/Store]", {
396
+ )
397
+ .pipe(Effect
398
+ .withSpan("Cosmos.find [effect-app/infra/Store]", {
423
399
  attributes: {
424
400
  "repository.container_id": containerId,
425
401
  "repository.model_name": name,
426
- id: e[idKey]
402
+ partitionValue: nsPartitionValue(ns, { [idKey]: id } as Encoded),
403
+ id
427
404
  }
428
- }, { captureStackTrace: false })
429
- ),
405
+ }, { captureStackTrace: false }))
406
+ )),
407
+ set: (e) =>
408
+ resolveNamespace.pipe(Effect.flatMap((ns) =>
409
+ Option
410
+ .match(
411
+ Option
412
+ .fromNullishOr(e._etag),
413
+ {
414
+ onNone: () =>
415
+ Effect.promise(() =>
416
+ container.items.create({
417
+ ...mapId(e),
418
+ _partitionKey: nsPartitionValue(ns, e)
419
+ })
420
+ ),
421
+ onSome: (eTag) =>
422
+ Effect.promise(() =>
423
+ container.item(e[idKey], nsPartitionValue(ns, e)).replace(
424
+ { ...mapId(e), _partitionKey: nsPartitionValue(ns, e) },
425
+ {
426
+ accessCondition: {
427
+ type: "IfMatch",
428
+ condition: eTag
429
+ }
430
+ }
431
+ )
432
+ )
433
+ }
434
+ )
435
+ .pipe(
436
+ Effect
437
+ .flatMap((x) => {
438
+ if (x.statusCode === 412 || x.statusCode === 404 || x.statusCode === 409) {
439
+ return Effect.fail(
440
+ new OptimisticConcurrencyException({ type: name, id: e[idKey], code: x.statusCode })
441
+ )
442
+ }
443
+ if (x.statusCode > 299 || x.statusCode < 200) {
444
+ return Effect.die(
445
+ new CosmosDbOperationError(
446
+ "not able to update record: " + x.statusCode
447
+ )
448
+ )
449
+ }
450
+ return Effect.sync(() => ({
451
+ ...e,
452
+ _etag: x.etag
453
+ }))
454
+ }),
455
+ Effect
456
+ .withSpan("Cosmos.set [effect-app/infra/Store]", {
457
+ attributes: {
458
+ "repository.container_id": containerId,
459
+ "repository.model_name": name,
460
+ id: e[idKey]
461
+ }
462
+ }, { captureStackTrace: false })
463
+ )
464
+ )),
430
465
  batchSet,
431
466
  bulkSet
432
467
  }