@effect-app/infra 2.10.0 → 2.12.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 (70) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/_cjs/Model/Repository/ext.cjs +1 -1
  3. package/_cjs/Model/Repository/ext.cjs.map +1 -1
  4. package/_cjs/Model/Repository/internal/internal.cjs +245 -0
  5. package/_cjs/Model/Repository/internal/internal.cjs.map +1 -0
  6. package/_cjs/Model/Repository/legacy.cjs +0 -139
  7. package/_cjs/Model/Repository/legacy.cjs.map +1 -1
  8. package/_cjs/Model/Repository/makeRepo.cjs +2 -241
  9. package/_cjs/Model/Repository/makeRepo.cjs.map +1 -1
  10. package/_cjs/Store/Cosmos/query.cjs +9 -2
  11. package/_cjs/Store/Cosmos/query.cjs.map +1 -1
  12. package/_cjs/Store/Cosmos.cjs +49 -23
  13. package/_cjs/Store/Cosmos.cjs.map +1 -1
  14. package/_cjs/Store/Disk.cjs +5 -5
  15. package/_cjs/Store/Disk.cjs.map +1 -1
  16. package/_cjs/Store/Memory.cjs +9 -9
  17. package/_cjs/Store/Memory.cjs.map +1 -1
  18. package/_cjs/Store/codeFilter.cjs.map +1 -1
  19. package/_cjs/Store/service.cjs.map +1 -1
  20. package/_cjs/Store/utils.cjs +3 -3
  21. package/_cjs/Store/utils.cjs.map +1 -1
  22. package/dist/Model/Repository/ext.d.ts +6 -9
  23. package/dist/Model/Repository/ext.d.ts.map +1 -1
  24. package/dist/Model/Repository/ext.js +2 -2
  25. package/dist/Model/Repository/internal/internal.d.ts +61 -0
  26. package/dist/Model/Repository/internal/internal.d.ts.map +1 -0
  27. package/dist/Model/Repository/internal/internal.js +245 -0
  28. package/dist/Model/Repository/legacy.d.ts +4 -192
  29. package/dist/Model/Repository/legacy.d.ts.map +1 -1
  30. package/dist/Model/Repository/legacy.js +2 -123
  31. package/dist/Model/Repository/makeRepo.d.ts +7 -77
  32. package/dist/Model/Repository/makeRepo.d.ts.map +1 -1
  33. package/dist/Model/Repository/makeRepo.js +3 -247
  34. package/dist/Model/Repository/service.d.ts +1 -3
  35. package/dist/Model/Repository/service.d.ts.map +1 -1
  36. package/dist/Operations.d.ts +3 -3
  37. package/dist/QueueMaker/sbqueue.d.ts +2 -2
  38. package/dist/Store/Cosmos/query.d.ts +1 -1
  39. package/dist/Store/Cosmos/query.d.ts.map +1 -1
  40. package/dist/Store/Cosmos/query.js +7 -3
  41. package/dist/Store/Cosmos.d.ts.map +1 -1
  42. package/dist/Store/Cosmos.js +43 -23
  43. package/dist/Store/Disk.d.ts +2 -3
  44. package/dist/Store/Disk.d.ts.map +1 -1
  45. package/dist/Store/Disk.js +6 -6
  46. package/dist/Store/Memory.d.ts +4 -9
  47. package/dist/Store/Memory.d.ts.map +1 -1
  48. package/dist/Store/Memory.js +12 -12
  49. package/dist/Store/codeFilter.d.ts +2 -3
  50. package/dist/Store/codeFilter.d.ts.map +1 -1
  51. package/dist/Store/codeFilter.js +1 -1
  52. package/dist/Store/service.d.ts +9 -23
  53. package/dist/Store/service.d.ts.map +1 -1
  54. package/dist/Store/service.js +1 -1
  55. package/dist/Store/utils.d.ts +1 -3
  56. package/dist/Store/utils.d.ts.map +1 -1
  57. package/dist/Store/utils.js +4 -4
  58. package/package.json +1 -1
  59. package/src/Model/Repository/ext.ts +6 -5
  60. package/src/Model/Repository/internal/internal.ts +482 -0
  61. package/src/Model/Repository/legacy.ts +4 -538
  62. package/src/Model/Repository/makeRepo.ts +11 -489
  63. package/src/Model/Repository/service.ts +1 -1
  64. package/src/Store/Cosmos/query.ts +6 -1
  65. package/src/Store/Cosmos.ts +85 -48
  66. package/src/Store/Disk.ts +20 -8
  67. package/src/Store/Memory.ts +25 -16
  68. package/src/Store/codeFilter.ts +2 -1
  69. package/src/Store/service.ts +8 -7
  70. package/src/Store/utils.ts +4 -3
@@ -7,10 +7,21 @@ import { dropUndefinedT } from "effect-app/utils"
7
7
  import { CosmosClient, CosmosClientLayer } from "../adapters/cosmos-client.js"
8
8
  import { OptimisticConcurrencyException } from "../errors.js"
9
9
  import { InfraLogger } from "../logger.js"
10
+ import type { FieldValues } from "../Model/filter/types.js"
10
11
  import { buildWhereCosmosQuery3, logQuery } from "./Cosmos/query.js"
11
12
  import { StoreMaker } from "./service.js"
12
13
  import type { FilterArgs, PersistenceModelType, StorageConfig, Store, StoreConfig } from "./service.js"
13
14
 
15
+ const makeMapId =
16
+ <IdKey extends keyof Encoded, Encoded extends FieldValues>(idKey: IdKey) => ({ [idKey]: id, ...e }: Encoded) => ({
17
+ ...e,
18
+ id
19
+ })
20
+ const makeReverseMapId =
21
+ <IdKey extends keyof Encoded, Encoded extends FieldValues>(idKey: IdKey) =>
22
+ ({ id, ...t }: PersistenceModelType<Omit<Encoded, IdKey> & { id: string }>) =>
23
+ ({ ...t, [idKey]: id }) as any as PersistenceModelType<Encoded>
24
+
14
25
  class CosmosDbOperationError {
15
26
  constructor(readonly message: string) {}
16
27
  } // TODO: Retry operation when running into RU limit.
@@ -19,13 +30,17 @@ function makeCosmosStore({ prefix }: StorageConfig) {
19
30
  return Effect.gen(function*() {
20
31
  const { db } = yield* CosmosClient
21
32
  return {
22
- make: <Id extends string, Encoded extends Record<string, any> & { id: Id }, R = never, E = never>(
33
+ make: <IdKey extends keyof Encoded, Encoded extends FieldValues, R = never, E = never>(
23
34
  name: string,
35
+ idKey: IdKey,
24
36
  seed?: Effect<Iterable<Encoded>, E, R>,
25
37
  config?: StoreConfig<Encoded>
26
38
  ) =>
27
39
  Effect.gen(function*() {
40
+ const mapId = makeMapId<IdKey, Encoded>(idKey)
41
+ const mapReverseId = makeReverseMapId<IdKey, Encoded>(idKey)
28
42
  type PM = PersistenceModelType<Encoded>
43
+ type PMCosmos = PersistenceModelType<Omit<Encoded, IdKey> & { id: string }>
29
44
  const containerId = `${prefix}${name}`
30
45
  yield* Effect.promise(() =>
31
46
  db.containers.createIfNotExists(dropUndefinedT({
@@ -47,34 +62,37 @@ function makeCosmosStore({ prefix }: StorageConfig) {
47
62
  .gen(function*() {
48
63
  // TODO: disable batching if need atomicity
49
64
  // we delay and batch to keep low amount of RUs
50
- const b = [...items].map(
51
- (x) =>
52
- [
53
- x,
54
- Option.match(Option.fromNullable(x._etag), {
55
- onNone: () =>
56
- dropUndefinedT({
57
- operationType: "Create" as const,
58
- resourceBody: {
59
- ...Struct.omit(x, "_etag"),
60
- _partitionKey: config?.partitionValue(x)
61
- },
62
- partitionKey: config?.partitionValue(x)
63
- }),
64
- onSome: (eTag) =>
65
- dropUndefinedT({
66
- operationType: "Replace" as const,
67
- id: x.id,
68
- resourceBody: {
69
- ...Struct.omit(x, "_etag"),
70
- _partitionKey: config?.partitionValue(x)
71
- },
72
- ifMatch: eTag,
73
- partitionKey: config?.partitionValue(x)
74
- })
75
- })
76
- ] as const
77
- )
65
+ const b = [...items]
66
+ .map(
67
+ (x) =>
68
+ [
69
+ x,
70
+ Option.match(Option.fromNullable(x._etag), {
71
+ onNone: () =>
72
+ dropUndefinedT({
73
+ operationType: "Create" as const,
74
+ resourceBody: {
75
+ ...Struct.omit(x, "_etag", idKey),
76
+ id: x[idKey],
77
+ _partitionKey: config?.partitionValue(x)
78
+ },
79
+ partitionKey: config?.partitionValue(x)
80
+ }),
81
+ onSome: (eTag) =>
82
+ dropUndefinedT({
83
+ operationType: "Replace" as const,
84
+ id: x[idKey],
85
+ resourceBody: {
86
+ ...Struct.omit(x, "_etag", idKey),
87
+ id: x[idKey],
88
+ _partitionKey: config?.partitionValue(x)
89
+ },
90
+ ifMatch: eTag,
91
+ partitionKey: config?.partitionValue(x)
92
+ })
93
+ })
94
+ ] as const
95
+ )
78
96
  const batches = Chunk.toReadonlyArray(Array.chunk_(b, config?.maxBulkSize ?? 10))
79
97
 
80
98
  const batchResult = yield* Effect.forEach(
@@ -135,15 +153,17 @@ function makeCosmosStore({ prefix }: StorageConfig) {
135
153
  onNone: () => ({
136
154
  operationType: "Create" as const,
137
155
  resourceBody: {
138
- ...Struct.omit(x, "_etag"),
156
+ ...Struct.omit(x, "_etag", idKey),
157
+ id: x[idKey],
139
158
  _partitionKey: config?.partitionValue(x)
140
159
  }
141
160
  }),
142
161
  onSome: (eTag) => ({
143
162
  operationType: "Replace" as const,
144
- id: x.id,
163
+ id: x[idKey],
145
164
  resourceBody: {
146
- ...Struct.omit(x, "_etag"),
165
+ ...Struct.omit(x, "_etag", idKey),
166
+ id: x[idKey],
147
167
  _partitionKey: config?.partitionValue(x)
148
168
  },
149
169
  ifMatch: eTag
@@ -173,8 +193,9 @@ function makeCosmosStore({ prefix }: StorageConfig) {
173
193
  )
174
194
  }
175
195
 
176
- return batch.map(([e], i) => ({
196
+ return batch.map(([{ id, ...e }], i) => ({
177
197
  ...e,
198
+ [idKey]: id,
178
199
  _etag: result[i]?.eTag
179
200
  })) as unknown as NonEmptyReadonlyArray<Encoded>
180
201
  })
@@ -187,7 +208,7 @@ function makeCosmosStore({ prefix }: StorageConfig) {
187
208
  }))
188
209
  }
189
210
 
190
- const s: Store<Encoded, Id> = {
211
+ const s: Store<IdKey, Encoded> = {
191
212
  all: Effect
192
213
  .sync(() => ({
193
214
  query: `SELECT * FROM ${name} f WHERE f.id != @id`,
@@ -199,9 +220,13 @@ function makeCosmosStore({ prefix }: StorageConfig) {
199
220
  Effect.promise(() =>
200
221
  container
201
222
  .items
202
- .query<PM>(q)
223
+ .query<PMCosmos>(q)
203
224
  .fetchAll()
204
- .then(({ resources }) => resources.map((_) => ({ ...defaultValues, ..._ })))
225
+ .then(({ resources }) =>
226
+ resources.map(
227
+ (_) => ({ ...defaultValues, ...mapReverseId(_) })
228
+ )
229
+ )
205
230
  )
206
231
  ),
207
232
  Effect
@@ -223,6 +248,7 @@ function makeCosmosStore({ prefix }: StorageConfig) {
223
248
  return Effect
224
249
  .sync(() =>
225
250
  buildWhereCosmosQuery3(
251
+ idKey,
226
252
  filter ?? [],
227
253
  name,
228
254
  importedMarkerId,
@@ -244,13 +270,20 @@ function makeCosmosStore({ prefix }: StorageConfig) {
244
270
  .query<M>(q)
245
271
  .fetchAll()
246
272
  .then(({ resources }) =>
247
- resources.map((_) => ({ ...pipe(defaultValues, Struct.pick(...f.select!)), ..._ }))
273
+ resources.map((_) =>
274
+ ({
275
+ ...pipe(defaultValues, Struct.pick(...f.select!)),
276
+ ...mapReverseId(_ as any)
277
+ }) as any
278
+ )
248
279
  )
249
280
  : container
250
281
  .items
251
282
  .query<{ f: M }>(q)
252
283
  .fetchAll()
253
- .then(({ resources }) => resources.map((_) => ({ ...defaultValues, ..._.f })))
284
+ .then(({ resources }) =>
285
+ resources.map(({ f }) => ({ ...defaultValues, ...mapReverseId(f as any) }) as any)
286
+ )
254
287
  )
255
288
  )
256
289
  )
@@ -263,10 +296,10 @@ function makeCosmosStore({ prefix }: StorageConfig) {
263
296
  Effect
264
297
  .promise(() =>
265
298
  container
266
- .item(id, config?.partitionValue({ id } as Encoded))
299
+ .item(id, config?.partitionValue({ [idKey]: id } as Encoded))
267
300
  .read<Encoded>()
268
301
  .then(({ resource }) =>
269
- Option.fromNullable(resource).pipe(Option.map((_) => ({ ...defaultValues, ..._ })))
302
+ Option.fromNullable(resource).pipe(Option.map((_) => ({ ...defaultValues, ...mapReverseId(_) })))
270
303
  )
271
304
  )
272
305
  .pipe(Effect
@@ -275,7 +308,7 @@ function makeCosmosStore({ prefix }: StorageConfig) {
275
308
  attributes: {
276
309
  "repository.container_id": containerId,
277
310
  "repository.model_name": name,
278
- partitionValue: config?.partitionValue({ id } as Encoded),
311
+ partitionValue: config?.partitionValue({ [idKey]: id } as Encoded),
279
312
  id
280
313
  }
281
314
  })),
@@ -288,14 +321,14 @@ function makeCosmosStore({ prefix }: StorageConfig) {
288
321
  onNone: () =>
289
322
  Effect.promise(() =>
290
323
  container.items.create({
291
- ...e,
324
+ ...mapId(e),
292
325
  _partitionKey: config?.partitionValue(e)
293
326
  })
294
327
  ),
295
328
  onSome: (eTag) =>
296
329
  Effect.promise(() =>
297
- container.item(e.id, config?.partitionValue(e)).replace(
298
- { ...e, _partitionKey: config?.partitionValue(e) },
330
+ container.item(e[idKey], config?.partitionValue(e)).replace(
331
+ { ...mapId(e), _partitionKey: config?.partitionValue(e) },
299
332
  {
300
333
  accessCondition: {
301
334
  type: "IfMatch",
@@ -310,7 +343,7 @@ function makeCosmosStore({ prefix }: StorageConfig) {
310
343
  Effect
311
344
  .flatMap((x) => {
312
345
  if (x.statusCode === 412 || x.statusCode === 404) {
313
- return new OptimisticConcurrencyException({ type: name, id: e.id })
346
+ return new OptimisticConcurrencyException({ type: name, id: e[idKey] })
314
347
  }
315
348
  if (x.statusCode > 299 || x.statusCode < 200) {
316
349
  return Effect.die(
@@ -327,18 +360,22 @@ function makeCosmosStore({ prefix }: StorageConfig) {
327
360
  Effect
328
361
  .withSpan("Cosmos.set [effect-app/infra/Store]", {
329
362
  captureStackTrace: false,
330
- attributes: { "repository.container_id": containerId, "repository.model_name": name, id: e.id }
363
+ attributes: {
364
+ "repository.container_id": containerId,
365
+ "repository.model_name": name,
366
+ id: e[idKey]
367
+ }
331
368
  })
332
369
  ),
333
370
  batchSet,
334
371
  bulkSet,
335
372
  remove: (e: Encoded) =>
336
373
  Effect
337
- .promise(() => container.item(e.id, config?.partitionValue(e)).delete())
374
+ .promise(() => container.item(e[idKey], config?.partitionValue(e)).delete())
338
375
  .pipe(Effect
339
376
  .withSpan("Cosmos.remove [effect-app/infra/Store]", {
340
377
  captureStackTrace: false,
341
- attributes: { "repository.container_id": containerId, "repository.model_name": name, id: e.id }
378
+ attributes: { "repository.container_id": containerId, "repository.model_name": name, id: e[idKey] }
342
379
  }))
343
380
  }
344
381
 
package/src/Store/Disk.ts CHANGED
@@ -4,12 +4,14 @@ import * as fu from "../fileUtil.js"
4
4
  import fs from "fs"
5
5
 
6
6
  import { Console, Effect, FiberRef, flow } from "effect-app"
7
+ import type { FieldValues } from "../Model/filter/types.js"
7
8
  import { makeMemoryStoreInt, storeId } from "./Memory.js"
8
9
  import type { PersistenceModelType, StorageConfig, Store, StoreConfig } from "./service.js"
9
10
  import { StoreMaker } from "./service.js"
10
11
 
11
- function makeDiskStoreInt<Id extends string, Encoded extends { id: Id }, R, E>(
12
+ function makeDiskStoreInt<IdKey extends keyof Encoded, Encoded extends FieldValues, R, E>(
12
13
  prefix: string,
14
+ idKey: IdKey,
13
15
  namespace: string,
14
16
  dir: string,
15
17
  name: string,
@@ -68,8 +70,9 @@ function makeDiskStoreInt<Id extends string, Encoded extends { id: Id }, R, E>(
68
70
  )
69
71
  }
70
72
 
71
- const store = yield* makeMemoryStoreInt<Id, Encoded, R, E>(
73
+ const store = yield* makeMemoryStoreInt<IdKey, Encoded, R, E>(
72
74
  name,
75
+ idKey,
73
76
  namespace,
74
77
  !fs.existsSync(file)
75
78
  ? seed
@@ -107,7 +110,7 @@ function makeDiskStoreInt<Id extends string, Encoded extends { id: Id }, R, E>(
107
110
  store.remove,
108
111
  Effect.tap(flushToDiskInBackground)
109
112
  )
110
- } satisfies Store<Encoded, Id>
113
+ } satisfies Store<IdKey, Encoded>
111
114
  })
112
115
  }
113
116
 
@@ -121,15 +124,16 @@ export function makeDiskStore({ prefix }: StorageConfig, dir: string) {
121
124
  fs.mkdirSync(dir)
122
125
  }
123
126
  return {
124
- make: <Id extends string, Encoded extends { id: Id }, R, E>(
127
+ make: <IdKey extends keyof Encoded, Encoded extends FieldValues, R, E>(
125
128
  name: string,
129
+ idKey: IdKey,
126
130
  seed?: Effect<Iterable<Encoded>, E, R>,
127
131
  config?: StoreConfig<Encoded>
128
132
  ) =>
129
133
  Effect.gen(function*() {
130
134
  const storesSem = Effect.unsafeMakeSemaphore(1)
131
- const primary = yield* makeDiskStoreInt(prefix, "primary", dir, name, seed, config?.defaultValues)
132
- const stores = new Map<string, Store<Encoded, Id>>([["primary", primary]])
135
+ const primary = yield* makeDiskStoreInt(prefix, idKey, "primary", dir, name, seed, config?.defaultValues)
136
+ const stores = new Map<string, Store<IdKey, Encoded>>([["primary", primary]])
133
137
  const ctx = yield* Effect.context<R>()
134
138
  const getStore = !config?.allowNamespace
135
139
  ? Effect.succeed(primary)
@@ -145,7 +149,15 @@ export function makeDiskStore({ prefix }: StorageConfig, dir: string) {
145
149
  Effect.suspend(() => {
146
150
  const existing = stores.get(namespace)
147
151
  if (existing) return Effect.sync(() => existing)
148
- return makeDiskStoreInt<Id, Encoded, R, E>(prefix, namespace, dir, name, seed, config?.defaultValues)
152
+ return makeDiskStoreInt<IdKey, Encoded, R, E>(
153
+ prefix,
154
+ idKey,
155
+ namespace,
156
+ dir,
157
+ name,
158
+ seed,
159
+ config?.defaultValues
160
+ )
149
161
  .pipe(
150
162
  Effect.orDie,
151
163
  Effect.provide(ctx),
@@ -155,7 +167,7 @@ export function makeDiskStore({ prefix }: StorageConfig, dir: string) {
155
167
  )
156
168
  }))
157
169
 
158
- const s: Store<Encoded, Id> = {
170
+ const s: Store<IdKey, Encoded> = {
159
171
  all: Effect.flatMap(getStore, (_) => _.all),
160
172
  find: (...args) => Effect.flatMap(getStore, (_) => _.find(...args)),
161
173
  filter: (...args) => Effect.flatMap(getStore, (_) => _.filter(...args)),
@@ -5,12 +5,13 @@ import type { NonEmptyReadonlyArray } from "effect-app"
5
5
  import { NonEmptyString255 } from "effect-app/Schema"
6
6
  import { get } from "effect-app/utils"
7
7
  import { InfraLogger } from "../logger.js"
8
+ import type { FieldValues } from "../Model/filter/types.js"
8
9
  import { codeFilter } from "./codeFilter.js"
9
10
  import type { FilterArgs, PersistenceModelType, Store, StoreConfig } from "./service.js"
10
11
  import { StoreMaker } from "./service.js"
11
12
  import { makeUpdateETag } from "./utils.js"
12
13
 
13
- export function memFilter<T extends { id: string }, U extends keyof T = never>(f: FilterArgs<T, U>) {
14
+ export function memFilter<T extends FieldValues, U extends keyof T = never>(f: FilterArgs<T, U>) {
14
15
  type M = U extends undefined ? T : Pick<T, U>
15
16
  return ((c: T[]): M[] => {
16
17
  const select = (r: T[]): M[] => (f.select ? r.map(Struct.pick(...f.select)) : r) as any
@@ -76,8 +77,9 @@ function logQuery(f: FilterArgs<any, any>, defaultValues?: any) {
76
77
  }))
77
78
  }
78
79
 
79
- export function makeMemoryStoreInt<Id extends string, Encoded extends { id: Id }, R = never, E = never>(
80
+ export function makeMemoryStoreInt<IdKey extends keyof Encoded, Encoded extends FieldValues, R = never, E = never>(
80
81
  modelName: string,
82
+ idKey: IdKey,
81
83
  namespace: string,
82
84
  seed?: Effect<Iterable<Encoded>, E, R>,
83
85
  _defaultValues?: Partial<Encoded>
@@ -88,8 +90,8 @@ export function makeMemoryStoreInt<Id extends string, Encoded extends { id: Id }
88
90
  const items_ = yield* seed ?? Effect.sync(() => [])
89
91
  const defaultValues = _defaultValues ?? {}
90
92
 
91
- const items = new Map([...items_].map((_) => [_.id, { _etag: undefined, ...defaultValues, ..._ }] as const))
92
- const store = Ref.unsafeMake<ReadonlyMap<Id, PM>>(items)
93
+ const items = new Map([...items_].map((_) => [_[idKey], { _etag: undefined, ...defaultValues, ..._ }] as const))
94
+ const store = Ref.unsafeMake<ReadonlyMap<string, PM>>(items)
93
95
  const sem = Effect.unsafeMakeSemaphore(1)
94
96
  const withPermit = sem.withPermits(1)
95
97
  const values = Effect.map(Ref.get(store), (s) => s.values())
@@ -98,7 +100,7 @@ export function makeMemoryStoreInt<Id extends string, Encoded extends { id: Id }
98
100
 
99
101
  const batchSet = (items: NonEmptyReadonlyArray<PM>) =>
100
102
  Effect
101
- .forEach(items, (i) => Effect.flatMap(s.find(i.id), (current) => updateETag(i, current)))
103
+ .forEach(items, (i) => Effect.flatMap(s.find(i[idKey]), (current) => updateETag(i, idKey, current)))
102
104
  .pipe(
103
105
  Effect
104
106
  .tap((items) =>
@@ -107,8 +109,8 @@ export function makeMemoryStoreInt<Id extends string, Encoded extends { id: Id }
107
109
  .pipe(
108
110
  Effect
109
111
  .map((m) => {
110
- const mut = m as Map<Id, PM>
111
- items.forEach((e) => mut.set(e.id, e))
112
+ const mut = m as Map<string, PM>
113
+ items.forEach((e) => mut.set(e[idKey], e))
112
114
  return mut
113
115
  }),
114
116
  Effect
@@ -119,7 +121,7 @@ export function makeMemoryStoreInt<Id extends string, Encoded extends { id: Id }
119
121
  .map((_) => _),
120
122
  withPermit
121
123
  )
122
- const s: Store<Encoded, Id> = {
124
+ const s: Store<IdKey, Encoded> = {
123
125
  all: all.pipe(Effect.withSpan("Memory.all [effect-app/infra/Store]", {
124
126
  captureStackTrace: false,
125
127
  attributes: {
@@ -153,13 +155,13 @@ export function makeMemoryStoreInt<Id extends string, Encoded extends { id: Id }
153
155
  ),
154
156
  set: (e) =>
155
157
  s
156
- .find(e.id)
158
+ .find(e[idKey])
157
159
  .pipe(
158
- Effect.flatMap((current) => updateETag(e, current)),
160
+ Effect.flatMap((current) => updateETag(e, idKey, current)),
159
161
  Effect
160
162
  .tap((e) =>
161
163
  Ref.get(store).pipe(
162
- Effect.map((_) => new Map([..._, [e.id, e]])),
164
+ Effect.map((_) => new Map([..._, [e[idKey], e]])),
163
165
  Effect.flatMap((_) => Ref.set(store, _))
164
166
  )
165
167
  ),
@@ -197,7 +199,7 @@ export function makeMemoryStoreInt<Id extends string, Encoded extends { id: Id }
197
199
  Ref
198
200
  .get(store)
199
201
  .pipe(
200
- Effect.map((_) => new Map([..._].filter(([_]) => _ !== e.id))),
202
+ Effect.map((_) => new Map([..._].filter(([_]) => _ !== e[idKey]))),
201
203
  Effect.flatMap((_) => Ref.set(store, _)),
202
204
  withPermit,
203
205
  Effect.withSpan("Memory.remove [effect-app/infra/Store]", {
@@ -211,14 +213,21 @@ export function makeMemoryStoreInt<Id extends string, Encoded extends { id: Id }
211
213
  }
212
214
 
213
215
  export const makeMemoryStore = () => ({
214
- make: <Id extends string, Encoded extends { id: Id }, R = never, E = never>(
216
+ make: <IdKey extends keyof Encoded, Encoded extends FieldValues, R, E>(
215
217
  modelName: string,
218
+ idKey: IdKey,
216
219
  seed?: Effect<Iterable<Encoded>, E, R>,
217
220
  config?: StoreConfig<Encoded>
218
221
  ) =>
219
222
  Effect.gen(function*() {
220
223
  const storesSem = Effect.unsafeMakeSemaphore(1)
221
- const primary = yield* makeMemoryStoreInt<Id, Encoded, R, E>(modelName, "primary", seed, config?.defaultValues)
224
+ const primary = yield* makeMemoryStoreInt<IdKey, Encoded, R, E>(
225
+ modelName,
226
+ idKey,
227
+ "primary",
228
+ seed,
229
+ config?.defaultValues
230
+ )
222
231
  const ctx = yield* Effect.context<R>()
223
232
  const stores = new Map([["primary", primary]])
224
233
  const getStore = !config?.allowNamespace
@@ -234,7 +243,7 @@ export const makeMemoryStore = () => ({
234
243
  return storesSem.withPermits(1)(Effect.suspend(() => {
235
244
  const store = stores.get(namespace)
236
245
  if (store) return Effect.sync(() => store)
237
- return makeMemoryStoreInt(modelName, namespace, seed, config?.defaultValues)
246
+ return makeMemoryStoreInt(modelName, idKey, namespace, seed, config?.defaultValues)
238
247
  .pipe(
239
248
  Effect.orDie,
240
249
  Effect.provide(ctx),
@@ -242,7 +251,7 @@ export const makeMemoryStore = () => ({
242
251
  )
243
252
  }))
244
253
  }))
245
- const s: Store<Encoded, Id> = {
254
+ const s: Store<IdKey, Encoded> = {
246
255
  all: Effect.flatMap(getStore, (_) => _.all),
247
256
  find: (...args) => Effect.flatMap(getStore, (_) => _.find(...args)),
248
257
  filter: (...args) => Effect.flatMap(getStore, (_) => _.filter(...args)),
@@ -3,6 +3,7 @@
3
3
  import { Array, Option } from "effect-app"
4
4
  import { assertUnreachable, get } from "effect-app/utils"
5
5
  import type { FilterR, FilterResult } from "../Model/filter/filterApi.js"
6
+ import type { FieldValues } from "../Model/filter/types.js"
6
7
  import type { Filter } from "./service.js"
7
8
  import { compare, greaterThan, greaterThanExclusive, lowerThan, lowerThanExclusive } from "./utils.js"
8
9
 
@@ -120,6 +121,6 @@ export const codeFilter3_ = <E>(state: readonly FilterResult[], sut: E): boolean
120
121
  return eval(s)
121
122
  }
122
123
 
123
- export function codeFilter<E extends { id: string }, NE extends E>(filter: Filter) {
124
+ export function codeFilter<E extends FieldValues, NE extends E>(filter: Filter) {
124
125
  return (x: E) => codeFilter3_(filter, x) ? Option.some(x as unknown as NE) : Option.none()
125
126
  }
@@ -57,7 +57,7 @@ export interface O<TFieldValues extends FieldValues> {
57
57
  direction: "ASC" | "DESC"
58
58
  }
59
59
 
60
- export interface FilterArgs<Encoded extends { id: string }, U extends keyof Encoded = never> {
60
+ export interface FilterArgs<Encoded extends FieldValues, U extends keyof Encoded = never> {
61
61
  t: Encoded
62
62
  filter?: Filter | undefined
63
63
  select?: NonEmptyReadonlyArray<U> | undefined
@@ -66,18 +66,18 @@ export interface FilterArgs<Encoded extends { id: string }, U extends keyof Enco
66
66
  skip?: number | undefined
67
67
  }
68
68
 
69
- export type FilterFunc<Encoded extends { id: string }> = <U extends keyof Encoded = never>(
69
+ export type FilterFunc<Encoded extends FieldValues> = <U extends keyof Encoded = never>(
70
70
  args: FilterArgs<Encoded, U>
71
71
  ) => Effect<(U extends undefined ? Encoded : Pick<Encoded, U>)[]>
72
72
 
73
73
  export interface Store<
74
- Encoded extends { id: Id },
75
- Id extends string,
74
+ IdKey extends keyof Encoded,
75
+ Encoded extends FieldValues,
76
76
  PM extends PersistenceModelType<Encoded> = PersistenceModelType<Encoded>
77
77
  > {
78
78
  all: Effect<PM[]>
79
79
  filter: FilterFunc<Encoded>
80
- find: (id: Id) => Effect<Option<PM>>
80
+ find: (id: Encoded[IdKey]) => Effect<Option<PM>>
81
81
  set: (e: PM) => Effect<PM, OptimisticConcurrencyException>
82
82
  batchSet: (
83
83
  items: NonEmptyReadonlyArray<PM>
@@ -96,11 +96,12 @@ export interface Store<
96
96
  * @tsplus companion StoreMaker.Ops
97
97
  */
98
98
  export class StoreMaker extends Context.TagId("effect-app/StoreMaker")<StoreMaker, {
99
- make: <Encoded extends { id: Id }, Id extends string, R = never, E = never>(
99
+ make: <IdKey extends keyof Encoded, Encoded extends FieldValues, R = never, E = never>(
100
100
  name: string,
101
+ idKey: IdKey,
101
102
  seed?: Effect<Iterable<Encoded>, E, R>,
102
103
  config?: StoreConfig<Encoded>
103
- ) => Effect<Store<Encoded, Id>, E, R>
104
+ ) => Effect<Store<IdKey, Encoded>, E, R>
104
105
  }>() {
105
106
  }
106
107
 
@@ -13,18 +13,19 @@ export const makeETag = <E extends PersistenceModelType<{}>>(
13
13
  }) as any
14
14
 
15
15
  export const makeUpdateETag =
16
- (type: string) => <E extends PersistenceModelType<{ id: string }>>(e: E, current: Option<E>) =>
16
+ (type: string) =>
17
+ <IdKey extends keyof E, E extends PersistenceModelType<{}>>(e: E, idKey: IdKey, current: Option<E>) =>
17
18
  Effect.gen(function*() {
18
19
  if (e._etag) {
19
20
  yield* Effect.mapError(
20
21
  current,
21
- () => new OptimisticConcurrencyException({ type, id: e.id, current: "", found: e._etag })
22
+ () => new OptimisticConcurrencyException({ type, id: e[idKey] as string, current: "", found: e._etag })
22
23
  )
23
24
  }
24
25
  if (Option.isSome(current) && current.value._etag !== e._etag) {
25
26
  return yield* new OptimisticConcurrencyException({
26
27
  type,
27
- id: current.value.id,
28
+ id: current.value[idKey] as string,
28
29
  current: current.value._etag,
29
30
  found: e._etag
30
31
  })