@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
package/src/Store/SQL.ts CHANGED
@@ -51,323 +51,15 @@ const parseSelectRow = (
51
51
  }
52
52
 
53
53
  function makeSQLStoreInt(dialect: SQLDialect, jsonColumnType: string) {
54
- return ({ prefix }: StorageConfig) =>
55
- Effect.gen(function*() {
56
- const sql = yield* SqlClient.SqlClient
57
- return {
58
- make: <IdKey extends keyof Encoded, Encoded extends FieldValues, R = never, E = never>(
59
- name: string,
60
- idKey: IdKey,
61
- seed?: Effect.Effect<Iterable<Encoded>, E, R>,
62
- config?: StoreConfig<Encoded>
63
- ) =>
64
- Effect.gen(function*() {
65
- type PM = PersistenceModelType<Encoded>
66
- const tableName = `${prefix}${name}`
67
- const defaultValues = config?.defaultValues ?? {}
68
-
69
- const resolveNamespace = !config?.allowNamespace
70
- ? Effect.succeed("primary")
71
- : storeId.asEffect().pipe(Effect.map((namespace) => {
72
- if (namespace !== "primary" && !config.allowNamespace!(namespace)) {
73
- throw new Error(`Namespace ${namespace} not allowed!`)
74
- }
75
- return namespace
76
- }))
77
-
78
- const ensureTable = sql
79
- .unsafe(
80
- `CREATE TABLE IF NOT EXISTS "${tableName}" (id TEXT NOT NULL, _namespace TEXT NOT NULL DEFAULT 'primary', _etag TEXT, data ${jsonColumnType} NOT NULL, PRIMARY KEY (id, _namespace))`
81
- )
82
- .pipe(
83
- Effect.andThen(
84
- sql.unsafe(
85
- `CREATE TABLE IF NOT EXISTS "_migrations" (id TEXT NOT NULL, version TEXT NOT NULL, PRIMARY KEY (id, version))`
86
- )
87
- ),
88
- Effect.orDie,
89
- Effect.asVoid
90
- )
91
-
92
- const toRow = (e: PM) => {
93
- const newE = makeETag(e)
94
- const id = newE[idKey] as string
95
- const { _etag, [idKey]: _id, ...rest } = newE as any
96
- const data = JSON.stringify(rest)
97
- return { id, _etag: newE._etag!, data, item: newE }
98
- }
99
-
100
- const exec = (query: string, params?: readonly unknown[]) =>
101
- sql.unsafe(query, params as any).pipe(Effect.orDie)
102
-
103
- const setInternal = (e: PM, ns: string) =>
104
- Effect.gen(function*() {
105
- const row = toRow(e)
106
- if (e._etag) {
107
- yield* exec(
108
- `UPDATE "${tableName}" SET _etag = ?, data = ? WHERE id = ? AND _etag = ? AND _namespace = ?`,
109
- [row._etag, row.data, row.id, e._etag, ns]
110
- )
111
- const existing = yield* exec(
112
- `SELECT _etag FROM "${tableName}" WHERE id = ? AND _namespace = ?`,
113
- [row.id, ns]
114
- )
115
- const current = (existing as any[])[0]
116
- if (!current || current._etag !== row._etag) {
117
- if (current) {
118
- return yield* new OptimisticConcurrencyException({
119
- type: name,
120
- id: row.id,
121
- current: current._etag,
122
- found: e._etag,
123
- code: 412
124
- })
125
- }
126
- return yield* new OptimisticConcurrencyException({
127
- type: name,
128
- id: row.id,
129
- current: "",
130
- found: e._etag,
131
- code: 404
132
- })
133
- }
134
- } else {
135
- yield* exec(
136
- `INSERT INTO "${tableName}" (id, _namespace, _etag, data) VALUES (?, ?, ?, ?)`,
137
- [row.id, ns, row._etag, row.data]
138
- )
139
- }
140
- return row.item
141
- })
142
-
143
- const bulkSetInternal = (items: NonEmptyReadonlyArray<PM>, ns: string) =>
144
- sql
145
- .withTransaction(Effect.forEach(items, (e) => setInternal(e, ns)))
146
- .pipe(
147
- Effect.orDie,
148
- Effect.map((_) => _ as unknown as NonEmptyReadonlyArray<PM>)
149
- )
150
-
151
- const ctx = yield* Effect.context<R>()
152
- const seedCache = new Map<string, Effect.Effect<void>>()
153
- const makeSeedEffect = Effect.fnUntraced(function*(ns: string) {
154
- yield* ensureTable
155
- if (!seed) return
156
- const existing = yield* exec(
157
- `SELECT id FROM "_migrations" WHERE id = ? AND version = ?`,
158
- [`${tableName}::${ns}`, tableName]
159
- )
160
- if ((existing as any[]).length > 0) return
161
- yield* InfraLogger.logInfo(`Seeding data for ${name} (namespace: ${ns})`)
162
- const items = yield* seed.pipe(Effect.provide(ctx), Effect.orDie)
163
- const ne = toNonEmptyArray([...items])
164
- if (Option.isSome(ne)) yield* bulkSetInternal(ne.value, ns)
165
- yield* exec(
166
- `INSERT INTO "_migrations" (id, version) VALUES (?, ?)`,
167
- [`${tableName}::${ns}`, tableName]
168
- )
169
- })
170
- const seedNamespace = (ns: string) => {
171
- let cached = seedCache.get(ns)
172
- if (!cached) {
173
- cached = Effect.cached(Effect.uninterruptible(makeSeedEffect(ns))).pipe(Effect.runSync)
174
- seedCache.set(ns, cached)
175
- }
176
- return cached
177
- }
178
- const s: Store<IdKey, Encoded> = {
179
- seedNamespace: (ns) => seedNamespace(ns),
180
-
181
- all: resolveNamespace.pipe(Effect.flatMap((ns) =>
182
- exec(`SELECT id, _etag, data FROM "${tableName}" WHERE _namespace = ?`, [ns])
183
- .pipe(
184
- Effect.map((rows) => (rows as any[]).map((r) => parseRow<Encoded>(r, idKey, defaultValues))),
185
- Effect.withSpan("SQL.all [effect-app/infra/Store]", {
186
- attributes: {
187
- "repository.table_name": tableName,
188
- "repository.model_name": name,
189
- "repository.namespace": ns
190
- }
191
- }, { captureStackTrace: false })
192
- )
193
- )),
194
-
195
- find: (id) =>
196
- resolveNamespace.pipe(Effect.flatMap((ns) =>
197
- exec(`SELECT id, _etag, data FROM "${tableName}" WHERE id = ? AND _namespace = ?`, [id, ns])
198
- .pipe(
199
- Effect.map((rows) => {
200
- const row = (rows as any[])[0]
201
- return row
202
- ? Option.some(parseRow<Encoded>(row, idKey, defaultValues))
203
- : Option.none()
204
- }),
205
- Effect.withSpan("SQL.find [effect-app/infra/Store]", {
206
- attributes: { "repository.table_name": tableName, "repository.model_name": name, id }
207
- }, { captureStackTrace: false })
208
- )
209
- )),
210
-
211
- filter: <U extends keyof Encoded = never>(f: FilterArgs<Encoded, U>) => {
212
- const filter = f
213
- .filter
214
- type M = U extends undefined ? Encoded
215
- : Pick<Encoded, U>
216
- return resolveNamespace
217
- .pipe(Effect
218
- .flatMap((ns) =>
219
- Effect
220
- .sync(() => {
221
- const q = buildWhereSQLQuery(
222
- dialect,
223
- idKey,
224
- filter ? [{ t: "where-scope", result: filter, relation: "some" }] : [],
225
- tableName,
226
- defaultValues,
227
- f
228
- .select as
229
- | NonEmptyReadonlyArray<string | { key: string; subKeys: readonly string[] }>
230
- | undefined,
231
- f
232
- .order as NonEmptyReadonlyArray<{ key: string; direction: "ASC" | "DESC" }> | undefined,
233
- f
234
- .skip,
235
- f
236
- .limit
237
- )
238
- const hasWhere = q
239
- .sql
240
- .includes("WHERE")
241
- const nsSql = hasWhere
242
- ? q
243
- .sql
244
- .replace("WHERE", `WHERE _namespace = ? AND`)
245
- : q
246
- .sql
247
- .replace(
248
- `FROM "${tableName}"`,
249
- `FROM "${tableName}" WHERE _namespace = ?`
250
- )
251
- return {
252
- sql: nsSql,
253
- params: [
254
- ns,
255
- ...q
256
- .params
257
- ]
258
- }
259
- })
260
- .pipe(
261
- Effect
262
- .tap((q) =>
263
- logQuery(q)
264
- ),
265
- Effect.flatMap((q) =>
266
- exec(q.sql, q.params).pipe(
267
- Effect.map((rows) => {
268
- if (f.select) {
269
- return (rows as any[]).map((r) => {
270
- const selected = parseSelectRow(r, idKey)
271
- return {
272
- ...Struct.pick(
273
- defaultValues as any,
274
- f.select!.filter((_) => typeof _ === "string") as never[]
275
- ),
276
- ...selected
277
- } as M
278
- })
279
- }
280
- return (rows as any[]).map((r) =>
281
- parseRow<Encoded>(r, idKey, defaultValues) as any as M
282
- )
283
- })
284
- )
285
- ),
286
- Effect.withSpan("SQL.filter [effect-app/infra/Store]", {
287
- attributes: { "repository.table_name": tableName, "repository.model_name": name }
288
- }, { captureStackTrace: false })
289
- )
290
- ))
291
- },
292
-
293
- set: (e) =>
294
- resolveNamespace.pipe(Effect.flatMap((ns) =>
295
- setInternal(e, ns).pipe(
296
- Effect.withSpan("SQL.set [effect-app/infra/Store]", {
297
- attributes: { "repository.table_name": tableName, "repository.model_name": name, id: e[idKey] }
298
- }, { captureStackTrace: false })
299
- )
300
- )),
301
-
302
- batchSet: (items) =>
303
- resolveNamespace.pipe(Effect.flatMap((ns) =>
304
- bulkSetInternal(items, ns).pipe(
305
- Effect.withSpan("SQL.batchSet [effect-app/infra/Store]", {
306
- attributes: { "repository.table_name": tableName, "repository.model_name": name }
307
- }, { captureStackTrace: false })
308
- )
309
- )),
310
-
311
- bulkSet: (items) =>
312
- resolveNamespace.pipe(Effect.flatMap((ns) =>
313
- bulkSetInternal(items, ns).pipe(
314
- Effect.withSpan("SQL.bulkSet [effect-app/infra/Store]", {
315
- attributes: { "repository.table_name": tableName, "repository.model_name": name }
316
- }, { captureStackTrace: false })
317
- )
318
- )),
319
-
320
- batchRemove: (ids) => {
321
- const placeholders = ids.map(() => "?").join(", ")
322
- return resolveNamespace.pipe(Effect.flatMap((ns) =>
323
- exec(
324
- `DELETE FROM "${tableName}" WHERE id IN (${placeholders}) AND _namespace = ?`,
325
- [...ids, ns]
326
- )
327
- .pipe(
328
- Effect.asVoid,
329
- Effect.withSpan("SQL.batchRemove [effect-app/infra/Store]", {
330
- attributes: { "repository.table_name": tableName, "repository.model_name": name }
331
- }, { captureStackTrace: false })
332
- )
333
- ))
334
- },
335
-
336
- queryRaw: (query) =>
337
- s.all.pipe(
338
- Effect.map(query.memory),
339
- Effect.withSpan("SQL.queryRaw [effect-app/infra/Store]", {
340
- attributes: { "repository.table_name": tableName, "repository.model_name": name }
341
- }, { captureStackTrace: false })
342
- )
343
- }
344
-
345
- // Eagerly seed primary namespace on initialization
346
- yield* seedNamespace("primary")
347
-
348
- return s
349
- })
350
- }
351
- })
352
- }
353
-
354
- type WithNsSqlFn = <A, E2, R2>(
355
- ns: string,
356
- f: (sql: SqlClient.SqlClient) => Effect.Effect<A, E2, R2>
357
- ) => Effect.Effect<A, E2, R2>
358
-
359
- function makeSQLiteStorePerNs(
360
- withNsSql: WithNsSqlFn,
361
- { prefix }: StorageConfig
362
- ) {
363
- return {
364
- make: <IdKey extends keyof Encoded, Encoded extends FieldValues, R = never, E = never>(
365
- name: string,
366
- idKey: IdKey,
367
- seed?: Effect.Effect<Iterable<Encoded>, E, R>,
368
- config?: StoreConfig<Encoded>
369
- ) =>
370
- Effect.gen(function*() {
54
+ return Effect.fnUntraced(function*({ prefix }: StorageConfig) {
55
+ const sql = yield* SqlClient.SqlClient
56
+ return {
57
+ make: Effect.fnUntraced(function*<IdKey extends keyof Encoded, Encoded extends FieldValues, R = never, E = never>(
58
+ name: string,
59
+ idKey: IdKey,
60
+ seed?: Effect.Effect<Iterable<Encoded>, E, R>,
61
+ config?: StoreConfig<Encoded>
62
+ ) {
371
63
  type PM = PersistenceModelType<Encoded>
372
64
  const tableName = `${prefix}${name}`
373
65
  const defaultValues = config?.defaultValues ?? {}
@@ -381,6 +73,20 @@ function makeSQLiteStorePerNs(
381
73
  return namespace
382
74
  }))
383
75
 
76
+ const ensureTable = sql
77
+ .unsafe(
78
+ `CREATE TABLE IF NOT EXISTS "${tableName}" (id TEXT NOT NULL, _namespace TEXT NOT NULL DEFAULT 'primary', _etag TEXT, data ${jsonColumnType} NOT NULL, PRIMARY KEY (id, _namespace))`
79
+ )
80
+ .pipe(
81
+ Effect.andThen(
82
+ sql.unsafe(
83
+ `CREATE TABLE IF NOT EXISTS "_migrations" (id TEXT NOT NULL, version TEXT NOT NULL, PRIMARY KEY (id, version))`
84
+ )
85
+ ),
86
+ Effect.orDie,
87
+ Effect.asVoid
88
+ )
89
+
384
90
  const toRow = (e: PM) => {
385
91
  const newE = makeETag(e)
386
92
  const id = newE[idKey] as string
@@ -389,86 +95,63 @@ function makeSQLiteStorePerNs(
389
95
  return { id, _etag: newE._etag!, data, item: newE }
390
96
  }
391
97
 
392
- const exec = (ns: string, query: string, params?: readonly unknown[]) =>
393
- withNsSql(ns, (sql) => sql.unsafe(query, params as any).pipe(Effect.orDie))
98
+ const exec = (query: string, params?: readonly unknown[]) => sql.unsafe(query, params as any).pipe(Effect.orDie)
394
99
 
395
- const ensureTable = (ns: string) =>
396
- withNsSql(ns, (sql) =>
397
- sql
398
- .unsafe(
399
- `CREATE TABLE IF NOT EXISTS "${tableName}" (id TEXT NOT NULL PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`
400
- )
401
- .pipe(
402
- Effect.andThen(
403
- sql.unsafe(
404
- `CREATE TABLE IF NOT EXISTS "_migrations" (id TEXT NOT NULL, version TEXT NOT NULL, PRIMARY KEY (id, version))`
405
- )
406
- ),
407
- Effect.orDie,
408
- Effect.asVoid
409
- ))
410
-
411
- const setInternal = (e: PM, ns: string) =>
412
- Effect.gen(function*() {
413
- const row = toRow(e)
414
- if (e._etag) {
415
- yield* exec(
416
- ns,
417
- `UPDATE "${tableName}" SET _etag = ?, data = ? WHERE id = ? AND _etag = ?`,
418
- [row._etag, row.data, row.id, e._etag]
419
- )
420
- const existing = yield* exec(
421
- ns,
422
- `SELECT _etag FROM "${tableName}" WHERE id = ?`,
423
- [row.id]
424
- )
425
- const current = (existing as any[])[0]
426
- if (!current || current._etag !== row._etag) {
427
- if (current) {
428
- return yield* new OptimisticConcurrencyException({
429
- type: name,
430
- id: row.id,
431
- current: current._etag,
432
- found: e._etag,
433
- code: 412
434
- })
435
- }
100
+ const setInternal = Effect.fnUntraced(function*(e: PM, ns: string) {
101
+ const row = toRow(e)
102
+ if (e._etag) {
103
+ yield* exec(
104
+ `UPDATE "${tableName}" SET _etag = ?, data = ? WHERE id = ? AND _etag = ? AND _namespace = ?`,
105
+ [row._etag, row.data, row.id, e._etag, ns]
106
+ )
107
+ const existing = yield* exec(
108
+ `SELECT _etag FROM "${tableName}" WHERE id = ? AND _namespace = ?`,
109
+ [row.id, ns]
110
+ )
111
+ const current = (existing as any[])[0]
112
+ if (!current || current._etag !== row._etag) {
113
+ if (current) {
436
114
  return yield* new OptimisticConcurrencyException({
437
115
  type: name,
438
116
  id: row.id,
439
- current: "",
117
+ current: current._etag,
440
118
  found: e._etag,
441
- code: 404
119
+ code: 412
442
120
  })
443
121
  }
444
- } else {
445
- yield* exec(
446
- ns,
447
- `INSERT INTO "${tableName}" (id, _etag, data) VALUES (?, ?, ?)`,
448
- [row.id, row._etag, row.data]
449
- )
122
+ return yield* new OptimisticConcurrencyException({
123
+ type: name,
124
+ id: row.id,
125
+ current: "",
126
+ found: e._etag,
127
+ code: 404
128
+ })
450
129
  }
451
- return row.item
452
- })
130
+ } else {
131
+ yield* exec(
132
+ `INSERT INTO "${tableName}" (id, _namespace, _etag, data) VALUES (?, ?, ?, ?)`,
133
+ [row.id, ns, row._etag, row.data]
134
+ )
135
+ }
136
+ return row.item
137
+ })
453
138
 
454
139
  const bulkSetInternal = (items: NonEmptyReadonlyArray<PM>, ns: string) =>
455
- withNsSql(ns, (sql) =>
456
- sql
457
- .withTransaction(Effect.forEach(items, (e) => setInternal(e, ns)))
458
- .pipe(
459
- Effect.orDie,
460
- Effect.map((_) => _ as unknown as NonEmptyReadonlyArray<PM>)
461
- ))
140
+ sql
141
+ .withTransaction(Effect.forEach(items, (e) => setInternal(e, ns)))
142
+ .pipe(
143
+ Effect.orDie,
144
+ Effect.map((_) => _ as unknown as NonEmptyReadonlyArray<PM>)
145
+ )
462
146
 
463
147
  const ctx = yield* Effect.context<R>()
464
148
  const seedCache = new Map<string, Effect.Effect<void>>()
465
149
  const makeSeedEffect = Effect.fnUntraced(function*(ns: string) {
466
- yield* ensureTable(ns)
150
+ yield* ensureTable
467
151
  if (!seed) return
468
152
  const existing = yield* exec(
469
- ns,
470
153
  `SELECT id FROM "_migrations" WHERE id = ? AND version = ?`,
471
- [tableName, tableName]
154
+ [`${tableName}::${ns}`, tableName]
472
155
  )
473
156
  if ((existing as any[]).length > 0) return
474
157
  yield* InfraLogger.logInfo(`Seeding data for ${name} (namespace: ${ns})`)
@@ -476,9 +159,8 @@ function makeSQLiteStorePerNs(
476
159
  const ne = toNonEmptyArray([...items])
477
160
  if (Option.isSome(ne)) yield* bulkSetInternal(ne.value, ns)
478
161
  yield* exec(
479
- ns,
480
162
  `INSERT INTO "_migrations" (id, version) VALUES (?, ?)`,
481
- [tableName, tableName]
163
+ [`${tableName}::${ns}`, tableName]
482
164
  )
483
165
  })
484
166
  const seedNamespace = (ns: string) => {
@@ -489,39 +171,42 @@ function makeSQLiteStorePerNs(
489
171
  }
490
172
  return cached
491
173
  }
492
-
493
174
  const s: Store<IdKey, Encoded> = {
494
175
  seedNamespace: (ns) => seedNamespace(ns),
495
176
 
496
- all: resolveNamespace.pipe(Effect.flatMap((ns) =>
497
- exec(ns, `SELECT id, _etag, data FROM "${tableName}"`)
498
- .pipe(
499
- Effect.map((rows) => (rows as any[]).map((r) => parseRow<Encoded>(r, idKey, defaultValues))),
500
- Effect.withSpan("SQLite.all [effect-app/infra/Store]", {
501
- attributes: {
502
- "repository.table_name": tableName,
503
- "repository.model_name": name,
504
- "repository.namespace": ns
505
- }
506
- }, { captureStackTrace: false })
507
- )
508
- )),
509
-
510
- find: (id) =>
511
- resolveNamespace.pipe(Effect.flatMap((ns) =>
512
- exec(ns, `SELECT id, _etag, data FROM "${tableName}" WHERE id = ?`, [id])
177
+ all: resolveNamespace.pipe(
178
+ Effect.flatMap((ns) =>
179
+ exec(`SELECT id, _etag, data FROM "${tableName}" WHERE _namespace = ?`, [ns])
513
180
  .pipe(
514
- Effect.map((rows) => {
515
- const row = (rows as any[])[0]
516
- return row
517
- ? Option.some(parseRow<Encoded>(row, idKey, defaultValues))
518
- : Option.none()
519
- }),
520
- Effect.withSpan("SQLite.find [effect-app/infra/Store]", {
521
- attributes: { "repository.table_name": tableName, "repository.model_name": name, id }
181
+ Effect.map((rows) => (rows as any[]).map((r) => parseRow<Encoded>(r, idKey, defaultValues))),
182
+ Effect.withSpan("SQL.all [effect-app/infra/Store]", {
183
+ attributes: {
184
+ "repository.table_name": tableName,
185
+ "repository.model_name": name,
186
+ "repository.namespace": ns
187
+ }
522
188
  }, { captureStackTrace: false })
523
189
  )
524
- )),
190
+ )
191
+ ),
192
+
193
+ find: (id) =>
194
+ resolveNamespace.pipe(
195
+ Effect.flatMap((ns) =>
196
+ exec(`SELECT id, _etag, data FROM "${tableName}" WHERE id = ? AND _namespace = ?`, [id, ns])
197
+ .pipe(
198
+ Effect.map((rows) => {
199
+ const row = (rows as any[])[0]
200
+ return row
201
+ ? Option.some(parseRow<Encoded>(row, idKey, defaultValues))
202
+ : Option.none()
203
+ }),
204
+ Effect.withSpan("SQL.find [effect-app/infra/Store]", {
205
+ attributes: { "repository.table_name": tableName, "repository.model_name": name, id }
206
+ }, { captureStackTrace: false })
207
+ )
208
+ )
209
+ ),
525
210
 
526
211
  filter: <U extends keyof Encoded = never>(f: FilterArgs<Encoded, U>) => {
527
212
  const filter = f
@@ -532,9 +217,9 @@ function makeSQLiteStorePerNs(
532
217
  .pipe(Effect
533
218
  .flatMap((ns) =>
534
219
  Effect
535
- .sync(() =>
536
- buildWhereSQLQuery(
537
- sqliteDialect,
220
+ .sync(() => {
221
+ const q = buildWhereSQLQuery(
222
+ dialect,
538
223
  idKey,
539
224
  filter ? [{ t: "where-scope", result: filter, relation: "some" }] : [],
540
225
  tableName,
@@ -550,14 +235,33 @@ function makeSQLiteStorePerNs(
550
235
  f
551
236
  .limit
552
237
  )
553
- )
238
+ const hasWhere = q
239
+ .sql
240
+ .includes("WHERE")
241
+ const nsSql = hasWhere
242
+ ? q
243
+ .sql
244
+ .replace("WHERE", `WHERE _namespace = ? AND`)
245
+ : q
246
+ .sql
247
+ .replace(
248
+ `FROM "${tableName}"`,
249
+ `FROM "${tableName}" WHERE _namespace = ?`
250
+ )
251
+ return {
252
+ sql: nsSql,
253
+ params: [
254
+ ns,
255
+ ...q
256
+ .params
257
+ ]
258
+ }
259
+ })
554
260
  .pipe(
555
261
  Effect
556
- .tap((q) =>
557
- logQuery(q)
558
- ),
262
+ .tap((q) => logQuery(q)),
559
263
  Effect.flatMap((q) =>
560
- exec(ns, q.sql, q.params).pipe(
264
+ exec(q.sql, q.params).pipe(
561
265
  Effect.map((rows) => {
562
266
  if (f.select) {
563
267
  return (rows as any[]).map((r) => {
@@ -575,7 +279,7 @@ function makeSQLiteStorePerNs(
575
279
  })
576
280
  )
577
281
  ),
578
- Effect.withSpan("SQLite.filter [effect-app/infra/Store]", {
282
+ Effect.withSpan("SQL.filter [effect-app/infra/Store]", {
579
283
  attributes: { "repository.table_name": tableName, "repository.model_name": name }
580
284
  }, { captureStackTrace: false })
581
285
  )
@@ -585,7 +289,7 @@ function makeSQLiteStorePerNs(
585
289
  set: (e) =>
586
290
  resolveNamespace.pipe(Effect.flatMap((ns) =>
587
291
  setInternal(e, ns).pipe(
588
- Effect.withSpan("SQLite.set [effect-app/infra/Store]", {
292
+ Effect.withSpan("SQL.set [effect-app/infra/Store]", {
589
293
  attributes: { "repository.table_name": tableName, "repository.model_name": name, id: e[idKey] }
590
294
  }, { captureStackTrace: false })
591
295
  )
@@ -594,7 +298,7 @@ function makeSQLiteStorePerNs(
594
298
  batchSet: (items) =>
595
299
  resolveNamespace.pipe(Effect.flatMap((ns) =>
596
300
  bulkSetInternal(items, ns).pipe(
597
- Effect.withSpan("SQLite.batchSet [effect-app/infra/Store]", {
301
+ Effect.withSpan("SQL.batchSet [effect-app/infra/Store]", {
598
302
  attributes: { "repository.table_name": tableName, "repository.model_name": name }
599
303
  }, { captureStackTrace: false })
600
304
  )
@@ -603,7 +307,7 @@ function makeSQLiteStorePerNs(
603
307
  bulkSet: (items) =>
604
308
  resolveNamespace.pipe(Effect.flatMap((ns) =>
605
309
  bulkSetInternal(items, ns).pipe(
606
- Effect.withSpan("SQLite.bulkSet [effect-app/infra/Store]", {
310
+ Effect.withSpan("SQL.bulkSet [effect-app/infra/Store]", {
607
311
  attributes: { "repository.table_name": tableName, "repository.model_name": name }
608
312
  }, { captureStackTrace: false })
609
313
  )
@@ -613,13 +317,12 @@ function makeSQLiteStorePerNs(
613
317
  const placeholders = ids.map(() => "?").join(", ")
614
318
  return resolveNamespace.pipe(Effect.flatMap((ns) =>
615
319
  exec(
616
- ns,
617
- `DELETE FROM "${tableName}" WHERE id IN (${placeholders})`,
618
- [...ids]
320
+ `DELETE FROM "${tableName}" WHERE id IN (${placeholders}) AND _namespace = ?`,
321
+ [...ids, ns]
619
322
  )
620
323
  .pipe(
621
324
  Effect.asVoid,
622
- Effect.withSpan("SQLite.batchRemove [effect-app/infra/Store]", {
325
+ Effect.withSpan("SQL.batchRemove [effect-app/infra/Store]", {
623
326
  attributes: { "repository.table_name": tableName, "repository.model_name": name }
624
327
  }, { captureStackTrace: false })
625
328
  )
@@ -629,16 +332,307 @@ function makeSQLiteStorePerNs(
629
332
  queryRaw: (query) =>
630
333
  s.all.pipe(
631
334
  Effect.map(query.memory),
632
- Effect.withSpan("SQLite.queryRaw [effect-app/infra/Store]", {
335
+ Effect.withSpan("SQL.queryRaw [effect-app/infra/Store]", {
633
336
  attributes: { "repository.table_name": tableName, "repository.model_name": name }
634
337
  }, { captureStackTrace: false })
635
338
  )
636
339
  }
637
340
 
341
+ // Eagerly seed primary namespace on initialization
638
342
  yield* seedNamespace("primary")
639
343
 
640
344
  return s
641
345
  })
346
+ }
347
+ })
348
+ }
349
+
350
+ type WithNsSqlFn = <A, E2, R2>(
351
+ ns: string,
352
+ f: (sql: SqlClient.SqlClient) => Effect.Effect<A, E2, R2>
353
+ ) => Effect.Effect<A, E2, R2>
354
+
355
+ function makeSQLiteStorePerNs(
356
+ withNsSql: WithNsSqlFn,
357
+ { prefix }: StorageConfig
358
+ ) {
359
+ return {
360
+ make: Effect.fnUntraced(function*<IdKey extends keyof Encoded, Encoded extends FieldValues, R = never, E = never>(
361
+ name: string,
362
+ idKey: IdKey,
363
+ seed?: Effect.Effect<Iterable<Encoded>, E, R>,
364
+ config?: StoreConfig<Encoded>
365
+ ) {
366
+ type PM = PersistenceModelType<Encoded>
367
+ const tableName = `${prefix}${name}`
368
+ const defaultValues = config?.defaultValues ?? {}
369
+
370
+ const resolveNamespace = !config?.allowNamespace
371
+ ? Effect.succeed("primary")
372
+ : storeId.asEffect().pipe(Effect.map((namespace) => {
373
+ if (namespace !== "primary" && !config.allowNamespace!(namespace)) {
374
+ throw new Error(`Namespace ${namespace} not allowed!`)
375
+ }
376
+ return namespace
377
+ }))
378
+
379
+ const toRow = (e: PM) => {
380
+ const newE = makeETag(e)
381
+ const id = newE[idKey] as string
382
+ const { _etag, [idKey]: _id, ...rest } = newE as any
383
+ const data = JSON.stringify(rest)
384
+ return { id, _etag: newE._etag!, data, item: newE }
385
+ }
386
+
387
+ const exec = (ns: string, query: string, params?: readonly unknown[]) =>
388
+ withNsSql(ns, (sql) => sql.unsafe(query, params as any).pipe(Effect.orDie))
389
+
390
+ const ensureTable = (ns: string) =>
391
+ withNsSql(ns, (sql) =>
392
+ sql
393
+ .unsafe(
394
+ `CREATE TABLE IF NOT EXISTS "${tableName}" (id TEXT NOT NULL PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`
395
+ )
396
+ .pipe(
397
+ Effect.andThen(
398
+ sql.unsafe(
399
+ `CREATE TABLE IF NOT EXISTS "_migrations" (id TEXT NOT NULL, version TEXT NOT NULL, PRIMARY KEY (id, version))`
400
+ )
401
+ ),
402
+ Effect.orDie,
403
+ Effect.asVoid
404
+ ))
405
+
406
+ const setInternal = Effect.fnUntraced(function*(e: PM, ns: string) {
407
+ const row = toRow(e)
408
+ if (e._etag) {
409
+ yield* exec(
410
+ ns,
411
+ `UPDATE "${tableName}" SET _etag = ?, data = ? WHERE id = ? AND _etag = ?`,
412
+ [row._etag, row.data, row.id, e._etag]
413
+ )
414
+ const existing = yield* exec(
415
+ ns,
416
+ `SELECT _etag FROM "${tableName}" WHERE id = ?`,
417
+ [row.id]
418
+ )
419
+ const current = (existing as any[])[0]
420
+ if (!current || current._etag !== row._etag) {
421
+ if (current) {
422
+ return yield* new OptimisticConcurrencyException({
423
+ type: name,
424
+ id: row.id,
425
+ current: current._etag,
426
+ found: e._etag,
427
+ code: 412
428
+ })
429
+ }
430
+ return yield* new OptimisticConcurrencyException({
431
+ type: name,
432
+ id: row.id,
433
+ current: "",
434
+ found: e._etag,
435
+ code: 404
436
+ })
437
+ }
438
+ } else {
439
+ yield* exec(
440
+ ns,
441
+ `INSERT INTO "${tableName}" (id, _etag, data) VALUES (?, ?, ?)`,
442
+ [row.id, row._etag, row.data]
443
+ )
444
+ }
445
+ return row.item
446
+ })
447
+
448
+ const bulkSetInternal = (items: NonEmptyReadonlyArray<PM>, ns: string) =>
449
+ withNsSql(ns, (sql) =>
450
+ sql
451
+ .withTransaction(Effect.forEach(items, (e) => setInternal(e, ns)))
452
+ .pipe(
453
+ Effect.orDie,
454
+ Effect.map((_) => _ as unknown as NonEmptyReadonlyArray<PM>)
455
+ ))
456
+
457
+ const ctx = yield* Effect.context<R>()
458
+ const seedCache = new Map<string, Effect.Effect<void>>()
459
+ const makeSeedEffect = Effect.fnUntraced(function*(ns: string) {
460
+ yield* ensureTable(ns)
461
+ if (!seed) return
462
+ const existing = yield* exec(
463
+ ns,
464
+ `SELECT id FROM "_migrations" WHERE id = ? AND version = ?`,
465
+ [tableName, tableName]
466
+ )
467
+ if ((existing as any[]).length > 0) return
468
+ yield* InfraLogger.logInfo(`Seeding data for ${name} (namespace: ${ns})`)
469
+ const items = yield* seed.pipe(Effect.provide(ctx), Effect.orDie)
470
+ const ne = toNonEmptyArray([...items])
471
+ if (Option.isSome(ne)) yield* bulkSetInternal(ne.value, ns)
472
+ yield* exec(
473
+ ns,
474
+ `INSERT INTO "_migrations" (id, version) VALUES (?, ?)`,
475
+ [tableName, tableName]
476
+ )
477
+ })
478
+ const seedNamespace = (ns: string) => {
479
+ let cached = seedCache.get(ns)
480
+ if (!cached) {
481
+ cached = Effect.cached(Effect.uninterruptible(makeSeedEffect(ns))).pipe(Effect.runSync)
482
+ seedCache.set(ns, cached)
483
+ }
484
+ return cached
485
+ }
486
+
487
+ const s: Store<IdKey, Encoded> = {
488
+ seedNamespace: (ns) => seedNamespace(ns),
489
+
490
+ all: resolveNamespace.pipe(Effect.flatMap((ns) =>
491
+ exec(ns, `SELECT id, _etag, data FROM "${tableName}"`)
492
+ .pipe(
493
+ Effect.map((rows) => (rows as any[]).map((r) => parseRow<Encoded>(r, idKey, defaultValues))),
494
+ Effect.withSpan("SQLite.all [effect-app/infra/Store]", {
495
+ attributes: {
496
+ "repository.table_name": tableName,
497
+ "repository.model_name": name,
498
+ "repository.namespace": ns
499
+ }
500
+ }, { captureStackTrace: false })
501
+ )
502
+ )),
503
+
504
+ find: (id) =>
505
+ resolveNamespace.pipe(
506
+ Effect.flatMap((ns) =>
507
+ exec(ns, `SELECT id, _etag, data FROM "${tableName}" WHERE id = ?`, [id])
508
+ .pipe(
509
+ Effect.map((rows) => {
510
+ const row = (rows as any[])[0]
511
+ return row
512
+ ? Option.some(parseRow<Encoded>(row, idKey, defaultValues))
513
+ : Option.none()
514
+ }),
515
+ Effect.withSpan("SQLite.find [effect-app/infra/Store]", {
516
+ attributes: { "repository.table_name": tableName, "repository.model_name": name, id }
517
+ }, { captureStackTrace: false })
518
+ )
519
+ )
520
+ ),
521
+
522
+ filter: <U extends keyof Encoded = never>(f: FilterArgs<Encoded, U>) => {
523
+ const filter = f
524
+ .filter
525
+ type M = U extends undefined ? Encoded
526
+ : Pick<Encoded, U>
527
+ return resolveNamespace
528
+ .pipe(Effect
529
+ .flatMap((ns) =>
530
+ Effect
531
+ .sync(() =>
532
+ buildWhereSQLQuery(
533
+ sqliteDialect,
534
+ idKey,
535
+ filter ? [{ t: "where-scope", result: filter, relation: "some" }] : [],
536
+ tableName,
537
+ defaultValues,
538
+ f
539
+ .select as
540
+ | NonEmptyReadonlyArray<string | { key: string; subKeys: readonly string[] }>
541
+ | undefined,
542
+ f
543
+ .order as NonEmptyReadonlyArray<{ key: string; direction: "ASC" | "DESC" }> | undefined,
544
+ f
545
+ .skip,
546
+ f
547
+ .limit
548
+ )
549
+ )
550
+ .pipe(
551
+ Effect
552
+ .tap((q) => logQuery(q)),
553
+ Effect.flatMap((q) =>
554
+ exec(ns, q.sql, q.params).pipe(
555
+ Effect.map((rows) => {
556
+ if (f.select) {
557
+ return (rows as any[]).map((r) => {
558
+ const selected = parseSelectRow(r, idKey)
559
+ return {
560
+ ...Struct.pick(
561
+ defaultValues as any,
562
+ f.select!.filter((_) => typeof _ === "string") as never[]
563
+ ),
564
+ ...selected
565
+ } as M
566
+ })
567
+ }
568
+ return (rows as any[]).map((r) => parseRow<Encoded>(r, idKey, defaultValues) as any as M)
569
+ })
570
+ )
571
+ ),
572
+ Effect.withSpan("SQLite.filter [effect-app/infra/Store]", {
573
+ attributes: { "repository.table_name": tableName, "repository.model_name": name }
574
+ }, { captureStackTrace: false })
575
+ )
576
+ ))
577
+ },
578
+
579
+ set: (e) =>
580
+ resolveNamespace.pipe(Effect.flatMap((ns) =>
581
+ setInternal(e, ns).pipe(
582
+ Effect.withSpan("SQLite.set [effect-app/infra/Store]", {
583
+ attributes: { "repository.table_name": tableName, "repository.model_name": name, id: e[idKey] }
584
+ }, { captureStackTrace: false })
585
+ )
586
+ )),
587
+
588
+ batchSet: (items) =>
589
+ resolveNamespace.pipe(Effect.flatMap((ns) =>
590
+ bulkSetInternal(items, ns).pipe(
591
+ Effect.withSpan("SQLite.batchSet [effect-app/infra/Store]", {
592
+ attributes: { "repository.table_name": tableName, "repository.model_name": name }
593
+ }, { captureStackTrace: false })
594
+ )
595
+ )),
596
+
597
+ bulkSet: (items) =>
598
+ resolveNamespace.pipe(Effect.flatMap((ns) =>
599
+ bulkSetInternal(items, ns).pipe(
600
+ Effect.withSpan("SQLite.bulkSet [effect-app/infra/Store]", {
601
+ attributes: { "repository.table_name": tableName, "repository.model_name": name }
602
+ }, { captureStackTrace: false })
603
+ )
604
+ )),
605
+
606
+ batchRemove: (ids) => {
607
+ const placeholders = ids.map(() => "?").join(", ")
608
+ return resolveNamespace.pipe(Effect.flatMap((ns) =>
609
+ exec(
610
+ ns,
611
+ `DELETE FROM "${tableName}" WHERE id IN (${placeholders})`,
612
+ [...ids]
613
+ )
614
+ .pipe(
615
+ Effect.asVoid,
616
+ Effect.withSpan("SQLite.batchRemove [effect-app/infra/Store]", {
617
+ attributes: { "repository.table_name": tableName, "repository.model_name": name }
618
+ }, { captureStackTrace: false })
619
+ )
620
+ ))
621
+ },
622
+
623
+ queryRaw: (query) =>
624
+ s.all.pipe(
625
+ Effect.map(query.memory),
626
+ Effect.withSpan("SQLite.queryRaw [effect-app/infra/Store]", {
627
+ attributes: { "repository.table_name": tableName, "repository.model_name": name }
628
+ }, { captureStackTrace: false })
629
+ )
630
+ }
631
+
632
+ yield* seedNamespace("primary")
633
+
634
+ return s
635
+ })
642
636
  }
643
637
  }
644
638