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

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 (172) hide show
  1. package/CHANGELOG.md +794 -0
  2. package/dist/CUPS.d.ts +3 -3
  3. package/dist/CUPS.d.ts.map +1 -1
  4. package/dist/CUPS.js +3 -3
  5. package/dist/Emailer/service.d.ts +3 -3
  6. package/dist/Emailer/service.d.ts.map +1 -1
  7. package/dist/Emailer/service.js +3 -3
  8. package/dist/MainFiberSet.d.ts +2 -2
  9. package/dist/MainFiberSet.d.ts.map +1 -1
  10. package/dist/MainFiberSet.js +3 -3
  11. package/dist/Model/Repository/Registry.d.ts +20 -0
  12. package/dist/Model/Repository/Registry.d.ts.map +1 -0
  13. package/dist/Model/Repository/Registry.js +17 -0
  14. package/dist/Model/Repository/internal/internal.d.ts +3 -3
  15. package/dist/Model/Repository/internal/internal.d.ts.map +1 -1
  16. package/dist/Model/Repository/internal/internal.js +22 -16
  17. package/dist/Model/Repository/makeRepo.d.ts +5 -4
  18. package/dist/Model/Repository/makeRepo.d.ts.map +1 -1
  19. package/dist/Model/Repository/makeRepo.js +4 -1
  20. package/dist/Model/Repository/service.d.ts +5 -0
  21. package/dist/Model/Repository/service.d.ts.map +1 -1
  22. package/dist/Model/Repository/validation.d.ts +7 -6
  23. package/dist/Model/Repository/validation.d.ts.map +1 -1
  24. package/dist/Model/Repository.d.ts +1 -0
  25. package/dist/Model/Repository.d.ts.map +1 -1
  26. package/dist/Model/Repository.js +2 -1
  27. package/dist/Model/query/dsl.d.ts +9 -9
  28. package/dist/Model.d.ts +1 -0
  29. package/dist/Model.d.ts.map +1 -1
  30. package/dist/Model.js +2 -1
  31. package/dist/Operations.d.ts +2 -2
  32. package/dist/Operations.d.ts.map +1 -1
  33. package/dist/Operations.js +3 -3
  34. package/dist/OperationsRepo.d.ts +3 -3
  35. package/dist/OperationsRepo.d.ts.map +1 -1
  36. package/dist/OperationsRepo.js +3 -3
  37. package/dist/QueueMaker/SQLQueue.d.ts +2 -4
  38. package/dist/QueueMaker/SQLQueue.d.ts.map +1 -1
  39. package/dist/QueueMaker/SQLQueue.js +8 -6
  40. package/dist/QueueMaker/errors.d.ts +1 -1
  41. package/dist/QueueMaker/errors.d.ts.map +1 -1
  42. package/dist/QueueMaker/memQueue.js +3 -3
  43. package/dist/QueueMaker/sbqueue.js +3 -3
  44. package/dist/RequestContext.d.ts +22 -17
  45. package/dist/RequestContext.d.ts.map +1 -1
  46. package/dist/RequestContext.js +5 -5
  47. package/dist/RequestFiberSet.d.ts +2 -2
  48. package/dist/RequestFiberSet.d.ts.map +1 -1
  49. package/dist/RequestFiberSet.js +5 -5
  50. package/dist/Store/ContextMapContainer.d.ts +19 -3
  51. package/dist/Store/ContextMapContainer.d.ts.map +1 -1
  52. package/dist/Store/ContextMapContainer.js +13 -3
  53. package/dist/Store/Cosmos.d.ts.map +1 -1
  54. package/dist/Store/Cosmos.js +136 -68
  55. package/dist/Store/Disk.d.ts.map +1 -1
  56. package/dist/Store/Disk.js +24 -21
  57. package/dist/Store/Memory.d.ts +2 -2
  58. package/dist/Store/Memory.d.ts.map +1 -1
  59. package/dist/Store/Memory.js +26 -21
  60. package/dist/Store/SQL/Pg.d.ts +4 -0
  61. package/dist/Store/SQL/Pg.d.ts.map +1 -0
  62. package/dist/Store/SQL/Pg.js +191 -0
  63. package/dist/Store/SQL/query.d.ts +38 -0
  64. package/dist/Store/SQL/query.d.ts.map +1 -0
  65. package/dist/Store/SQL/query.js +367 -0
  66. package/dist/Store/SQL.d.ts +20 -0
  67. package/dist/Store/SQL.d.ts.map +1 -0
  68. package/dist/Store/SQL.js +381 -0
  69. package/dist/Store/index.d.ts +4 -1
  70. package/dist/Store/index.d.ts.map +1 -1
  71. package/dist/Store/index.js +15 -3
  72. package/dist/Store/service.d.ts +16 -5
  73. package/dist/Store/service.d.ts.map +1 -1
  74. package/dist/Store/service.js +24 -6
  75. package/dist/adapters/ServiceBus.d.ts +6 -6
  76. package/dist/adapters/ServiceBus.d.ts.map +1 -1
  77. package/dist/adapters/ServiceBus.js +9 -9
  78. package/dist/adapters/cosmos-client.d.ts +2 -2
  79. package/dist/adapters/cosmos-client.d.ts.map +1 -1
  80. package/dist/adapters/cosmos-client.js +3 -3
  81. package/dist/adapters/logger.d.ts.map +1 -1
  82. package/dist/adapters/memQueue.d.ts +2 -2
  83. package/dist/adapters/memQueue.d.ts.map +1 -1
  84. package/dist/adapters/memQueue.js +3 -3
  85. package/dist/adapters/mongo-client.d.ts +2 -2
  86. package/dist/adapters/mongo-client.d.ts.map +1 -1
  87. package/dist/adapters/mongo-client.js +3 -3
  88. package/dist/adapters/redis-client.d.ts +3 -3
  89. package/dist/adapters/redis-client.d.ts.map +1 -1
  90. package/dist/adapters/redis-client.js +3 -3
  91. package/dist/api/ContextProvider.d.ts +6 -6
  92. package/dist/api/ContextProvider.d.ts.map +1 -1
  93. package/dist/api/ContextProvider.js +6 -6
  94. package/dist/api/internal/auth.d.ts +1 -1
  95. package/dist/api/internal/events.d.ts +2 -2
  96. package/dist/api/internal/events.d.ts.map +1 -1
  97. package/dist/api/internal/events.js +11 -7
  98. package/dist/api/layerUtils.d.ts +5 -5
  99. package/dist/api/layerUtils.d.ts.map +1 -1
  100. package/dist/api/layerUtils.js +5 -5
  101. package/dist/api/routing/middleware/RouterMiddleware.d.ts +3 -3
  102. package/dist/api/routing/middleware/RouterMiddleware.d.ts.map +1 -1
  103. package/dist/api/routing/middleware/middleware.d.ts +35 -1
  104. package/dist/api/routing/middleware/middleware.d.ts.map +1 -1
  105. package/dist/api/routing/middleware/middleware.js +39 -1
  106. package/dist/api/routing.d.ts +1 -5
  107. package/dist/api/routing.d.ts.map +1 -1
  108. package/dist/api/routing.js +3 -2
  109. package/dist/api/setupRequest.d.ts +6 -3
  110. package/dist/api/setupRequest.d.ts.map +1 -1
  111. package/dist/api/setupRequest.js +11 -6
  112. package/dist/logger.d.ts.map +1 -1
  113. package/examples/query.ts +30 -26
  114. package/package.json +36 -18
  115. package/src/CUPS.ts +2 -2
  116. package/src/Emailer/service.ts +2 -2
  117. package/src/MainFiberSet.ts +2 -2
  118. package/src/Model/Repository/Registry.ts +33 -0
  119. package/src/Model/Repository/internal/internal.ts +76 -59
  120. package/src/Model/Repository/makeRepo.ts +7 -4
  121. package/src/Model/Repository/service.ts +6 -0
  122. package/src/Model/Repository.ts +1 -0
  123. package/src/Model.ts +1 -0
  124. package/src/Operations.ts +2 -2
  125. package/src/OperationsRepo.ts +2 -2
  126. package/src/QueueMaker/SQLQueue.ts +8 -7
  127. package/src/QueueMaker/memQueue.ts +2 -2
  128. package/src/QueueMaker/sbqueue.ts +2 -2
  129. package/src/RequestContext.ts +4 -4
  130. package/src/RequestFiberSet.ts +4 -4
  131. package/src/Store/ContextMapContainer.ts +41 -2
  132. package/src/Store/Cosmos.ts +350 -255
  133. package/src/Store/Disk.ts +37 -33
  134. package/src/Store/Memory.ts +29 -22
  135. package/src/Store/SQL/Pg.ts +321 -0
  136. package/src/Store/SQL/query.ts +409 -0
  137. package/src/Store/SQL.ts +674 -0
  138. package/src/Store/index.ts +17 -2
  139. package/src/Store/service.ts +31 -7
  140. package/src/adapters/ServiceBus.ts +8 -8
  141. package/src/adapters/cosmos-client.ts +2 -2
  142. package/src/adapters/memQueue.ts +2 -2
  143. package/src/adapters/mongo-client.ts +2 -2
  144. package/src/adapters/redis-client.ts +2 -2
  145. package/src/api/ContextProvider.ts +11 -11
  146. package/src/api/internal/events.ts +14 -9
  147. package/src/api/layerUtils.ts +8 -8
  148. package/src/api/routing/middleware/RouterMiddleware.ts +4 -4
  149. package/src/api/routing/middleware/middleware.ts +43 -0
  150. package/src/api/routing.ts +3 -3
  151. package/src/api/setupRequest.ts +27 -7
  152. package/test/contextProvider.test.ts +11 -11
  153. package/test/controller.test.ts +12 -9
  154. package/test/dist/contextProvider.test.d.ts.map +1 -1
  155. package/test/dist/controller.test.d.ts.map +1 -1
  156. package/test/dist/date-query.test.d.ts.map +1 -0
  157. package/test/dist/fixtures.d.ts +19 -9
  158. package/test/dist/fixtures.d.ts.map +1 -1
  159. package/test/dist/fixtures.js +11 -9
  160. package/test/dist/query.test.d.ts.map +1 -1
  161. package/test/dist/rawQuery.test.d.ts.map +1 -1
  162. package/test/dist/requires.test.d.ts.map +1 -1
  163. package/test/dist/rpc-multi-middleware.test.d.ts.map +1 -1
  164. package/test/dist/sql-store.test.d.ts.map +1 -0
  165. package/test/fixtures.ts +10 -8
  166. package/test/query.test.ts +182 -33
  167. package/test/rawQuery.test.ts +22 -18
  168. package/test/requires.test.ts +6 -5
  169. package/test/rpc-multi-middleware.test.ts +72 -3
  170. package/test/sql-store.test.ts +1064 -0
  171. package/test/validateSample.test.ts +12 -9
  172. package/tsconfig.json +0 -1
package/src/Store/Disk.ts CHANGED
@@ -66,11 +66,10 @@ function makeDiskStoreInt<IdKey extends keyof Encoded, Encoded extends FieldValu
66
66
  }
67
67
 
68
68
  // lock file for cross-process coordination during initialization
69
- const lockFile = file + ".lock"
70
69
 
71
70
  // wrap initialization in file lock to prevent race conditions in multi-worker setups
72
71
  const store = yield* fu.withFileLock(
73
- lockFile,
72
+ file,
74
73
  Effect.gen(function*() {
75
74
  const shouldSeed = !(fs.existsSync(file))
76
75
 
@@ -140,43 +139,48 @@ export function makeDiskStore({ prefix }: StorageConfig, dir: string) {
140
139
  config?: StoreConfig<Encoded>
141
140
  ) =>
142
141
  Effect.gen(function*() {
143
- const storesSem = Semaphore.makeUnsafe(1)
144
142
  const primary = yield* makeDiskStoreInt(prefix, idKey, "primary", dir, name, seed, config?.defaultValues)
145
143
  const stores = new Map<string, Store<IdKey, Encoded>>([["primary", primary]])
146
- const ctx = yield* Effect.services<R>()
144
+ const ctx = yield* Effect.context<R>()
145
+ const semaphores = new Map<string, Semaphore.Semaphore>()
146
+ const getSem = (ns: string) => {
147
+ let sem = semaphores.get(ns)
148
+ if (!sem) {
149
+ sem = Semaphore.makeUnsafe(1)
150
+ semaphores.set(ns, sem)
151
+ }
152
+ return sem
153
+ }
154
+ const ensureStore = (namespace: string) =>
155
+ getSem(namespace).withPermits(1)(
156
+ Effect.suspend(() => {
157
+ const existing = stores.get(namespace)
158
+ if (existing) return Effect.succeed(existing)
159
+ if (config?.allowNamespace && !config.allowNamespace(namespace)) {
160
+ throw new Error(`Namespace ${namespace} not allowed!`)
161
+ }
162
+ return makeDiskStoreInt<IdKey, Encoded, R, E>(
163
+ prefix,
164
+ idKey,
165
+ namespace,
166
+ dir,
167
+ name,
168
+ seed,
169
+ config?.defaultValues
170
+ )
171
+ .pipe(
172
+ Effect.orDie,
173
+ Effect.provide(ctx),
174
+ Effect.tap((store) => Effect.sync(() => stores.set(namespace, store)))
175
+ )
176
+ })
177
+ )
147
178
  const getStore = !config?.allowNamespace
148
179
  ? Effect.succeed(primary)
149
- : storeId.asEffect().pipe(Effect.flatMap((namespace) => {
150
- const store = stores.get(namespace)
151
- if (store) {
152
- return Effect.succeed(store)
153
- }
154
- if (!config.allowNamespace!(namespace)) {
155
- throw new Error(`Namespace ${namespace} not allowed!`)
156
- }
157
- return storesSem.withPermits(1)(
158
- Effect.suspend(() => {
159
- const existing = stores.get(namespace)
160
- if (existing) return Effect.sync(() => existing)
161
- return makeDiskStoreInt<IdKey, Encoded, R, E>(
162
- prefix,
163
- idKey,
164
- namespace,
165
- dir,
166
- name,
167
- seed,
168
- config?.defaultValues
169
- )
170
- .pipe(
171
- Effect.orDie,
172
- Effect.provide(ctx),
173
- Effect.tap((store) => Effect.sync(() => stores.set(namespace, store)))
174
- )
175
- })
176
- )
177
- }))
180
+ : storeId.asEffect().pipe(Effect.flatMap((namespace) => ensureStore(namespace)))
178
181
 
179
182
  const s: Store<IdKey, Encoded> = {
183
+ seedNamespace: (namespace) => ensureStore(namespace).pipe(Effect.asVoid),
180
184
  all: Effect.flatMap(getStore, (_) => _.all),
181
185
  find: (...args) => Effect.flatMap(getStore, (_) => _.find(...args)),
182
186
  filter: (...args) => Effect.flatMap(getStore, (_) => _.filter(...args)),
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
 
3
- import { Array, Effect, flow, type NonEmptyReadonlyArray, Option, Order, pipe, Ref, Result, Semaphore, ServiceMap, Struct } from "effect-app"
3
+ import { Array, Context, Effect, flow, type NonEmptyReadonlyArray, Option, Order, pipe, Ref, Result, Semaphore, Struct } from "effect-app"
4
4
  import { NonEmptyString255 } from "effect-app/Schema"
5
5
  import { get } from "effect-app/utils"
6
6
  import { InfraLogger } from "../logger.js"
@@ -24,7 +24,7 @@ export function memFilter<T extends FieldValues, U extends keyof T = never>(f: F
24
24
  )
25
25
  const n = Struct.pick(i, keys)
26
26
  subKeys.forEach((subKey) => {
27
- n[subKey.key] = i[subKey.key]!.map(Struct.pick(subKey.subKeys))
27
+ n[subKey.key] = i[subKey.key]!.map(Struct.pick(subKey.subKeys as never[]))
28
28
  })
29
29
  return n as M
30
30
  }) as any
@@ -72,7 +72,7 @@ export function memFilter<T extends FieldValues, U extends keyof T = never>(f: F
72
72
  }
73
73
 
74
74
  const defaultNs: NonEmptyString255 = NonEmptyString255("primary")
75
- export class storeId extends ServiceMap.Reference("StoreId", { defaultValue: (): NonEmptyString255 => defaultNs }) {}
75
+ export class storeId extends Context.Reference("StoreId", { defaultValue: (): NonEmptyString255 => defaultNs }) {}
76
76
 
77
77
  function logQuery(f: FilterArgs<any, any>, defaultValues?: any) {
78
78
  return InfraLogger
@@ -151,6 +151,8 @@ export function makeMemoryStoreInt<IdKey extends keyof Encoded, Encoded extends
151
151
  withPermit
152
152
  )
153
153
  const s: Store<IdKey, Encoded> = {
154
+ seedNamespace: () => Effect.void,
155
+
154
156
  queryRaw: (query) =>
155
157
  all
156
158
  .pipe(
@@ -257,7 +259,6 @@ export const makeMemoryStore = () => ({
257
259
  config?: StoreConfig<Encoded>
258
260
  ) =>
259
261
  Effect.gen(function*() {
260
- const storesSem = Semaphore.makeUnsafe(1)
261
262
  const primary = yield* makeMemoryStoreInt<IdKey, Encoded, R, E>(
262
263
  modelName,
263
264
  idKey,
@@ -265,30 +266,36 @@ export const makeMemoryStore = () => ({
265
266
  seed,
266
267
  config?.defaultValues
267
268
  )
268
- const ctx = yield* Effect.services<R>()
269
+ const ctx = yield* Effect.context<R>()
269
270
  const stores = new Map([["primary", primary]])
270
- const getStore = !config?.allowNamespace
271
- ? Effect.succeed(primary)
272
- : storeId.asEffect().pipe(Effect.flatMap((namespace) => {
271
+ const semaphores = new Map<string, Semaphore.Semaphore>()
272
+ const getSem = (ns: string) => {
273
+ let sem = semaphores.get(ns)
274
+ if (!sem) {
275
+ sem = Semaphore.makeUnsafe(1)
276
+ semaphores.set(ns, sem)
277
+ }
278
+ return sem
279
+ }
280
+ const ensureStore = (namespace: string) =>
281
+ getSem(namespace).withPermits(1)(Effect.suspend(() => {
273
282
  const store = stores.get(namespace)
274
- if (store) {
275
- return Effect.succeed(store)
276
- }
277
- if (!config.allowNamespace!(namespace)) {
283
+ if (store) return Effect.succeed(store)
284
+ if (config?.allowNamespace && !config.allowNamespace(namespace)) {
278
285
  throw new Error(`Namespace ${namespace} not allowed!`)
279
286
  }
280
- return storesSem.withPermits(1)(Effect.suspend(() => {
281
- const store = stores.get(namespace)
282
- if (store) return Effect.sync(() => store)
283
- return makeMemoryStoreInt(modelName, idKey, namespace, seed, config?.defaultValues)
284
- .pipe(
285
- Effect.orDie,
286
- Effect.provide(ctx),
287
- Effect.tap((store) => Effect.sync(() => stores.set(namespace, store)))
288
- )
289
- }))
287
+ return makeMemoryStoreInt(modelName, idKey, namespace, seed, config?.defaultValues)
288
+ .pipe(
289
+ Effect.orDie,
290
+ Effect.provide(ctx),
291
+ Effect.tap((store) => Effect.sync(() => stores.set(namespace, store)))
292
+ )
290
293
  }))
294
+ const getStore = !config?.allowNamespace
295
+ ? Effect.succeed(primary)
296
+ : storeId.asEffect().pipe(Effect.flatMap((namespace) => ensureStore(namespace)))
291
297
  const s: Store<IdKey, Encoded> = {
298
+ seedNamespace: (namespace) => ensureStore(namespace).pipe(Effect.asVoid),
292
299
  all: Effect.flatMap(getStore, (_) => _.all),
293
300
  queryRaw: (...args) => Effect.flatMap(getStore, (_) => _.queryRaw(...args)),
294
301
  find: (...args) => Effect.flatMap(getStore, (_) => _.find(...args)),
@@ -0,0 +1,321 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
3
+ import { Effect, type NonEmptyReadonlyArray, Option, Struct } from "effect-app"
4
+ import { toNonEmptyArray } from "effect-app/Array"
5
+ import { SqlClient } from "effect/unstable/sql"
6
+ import { OptimisticConcurrencyException } from "../../errors.js"
7
+ import { InfraLogger } from "../../logger.js"
8
+ import type { FieldValues } from "../../Model/filter/types.js"
9
+ import { storeId } from "../Memory.js"
10
+ import { type FilterArgs, type PersistenceModelType, type StorageConfig, type Store, type StoreConfig, StoreMaker } from "../service.js"
11
+ import { makeETag } from "../utils.js"
12
+ import { buildWhereSQLQuery, logQuery, pgDialect } from "./query.js"
13
+
14
+ const parseRow = <Encoded extends FieldValues>(
15
+ row: { id: string; _etag: string | null; data: unknown },
16
+ idKey: PropertyKey,
17
+ defaultValues: Partial<Encoded>
18
+ ): PersistenceModelType<Encoded> => {
19
+ const data = (typeof row.data === "string" ? JSON.parse(row.data) : row.data) as object
20
+ return { ...defaultValues, ...data, [idKey]: row.id, _etag: row._etag ?? undefined } as PersistenceModelType<Encoded>
21
+ }
22
+
23
+ const parseSelectRow = (
24
+ row: Record<string, unknown>,
25
+ idKey: PropertyKey,
26
+ defaultValues: Record<string, unknown>
27
+ ): any => {
28
+ const result: Record<string, unknown> = { ...defaultValues }
29
+ for (const [key, value] of Object.entries(row)) {
30
+ if (key === "id") {
31
+ result[idKey as string] = value
32
+ result["id"] = value
33
+ } else {
34
+ result[key] = value
35
+ }
36
+ }
37
+ return result
38
+ }
39
+
40
+ function makePgStore({ prefix }: StorageConfig) {
41
+ return Effect.gen(function*() {
42
+ const sql = yield* SqlClient.SqlClient
43
+ return {
44
+ make: <IdKey extends keyof Encoded, Encoded extends FieldValues, R = never, E = never>(
45
+ name: string,
46
+ idKey: IdKey,
47
+ seed?: Effect.Effect<Iterable<Encoded>, E, R>,
48
+ config?: StoreConfig<Encoded>
49
+ ) =>
50
+ Effect.gen(function*() {
51
+ type PM = PersistenceModelType<Encoded>
52
+ const tableName = `${prefix}${name}`
53
+ const defaultValues = config?.defaultValues ?? {}
54
+
55
+ const resolveNamespace = !config?.allowNamespace
56
+ ? Effect.succeed("primary")
57
+ : storeId.asEffect().pipe(Effect.map((namespace) => {
58
+ if (namespace !== "primary" && !config.allowNamespace!(namespace)) {
59
+ throw new Error(`Namespace ${namespace} not allowed!`)
60
+ }
61
+ return namespace
62
+ }))
63
+
64
+ const ensureTable = sql
65
+ .unsafe(
66
+ `CREATE TABLE IF NOT EXISTS "${tableName}" (id TEXT NOT NULL, _namespace TEXT NOT NULL DEFAULT 'primary', _etag TEXT, data JSONB NOT NULL, PRIMARY KEY (id, _namespace))`
67
+ )
68
+ .pipe(
69
+ Effect.andThen(
70
+ sql.unsafe(
71
+ `CREATE TABLE IF NOT EXISTS "_migrations" (id TEXT NOT NULL, version TEXT NOT NULL, PRIMARY KEY (id, version))`
72
+ )
73
+ ),
74
+ Effect.orDie,
75
+ Effect.asVoid
76
+ )
77
+
78
+ const toRow = (e: PM) => {
79
+ const newE = makeETag(e)
80
+ const id = newE[idKey] as string
81
+ const { _etag, [idKey]: _id, ...rest } = newE as any
82
+ const data = JSON.stringify(rest)
83
+ return { id, _etag: newE._etag!, data, item: newE }
84
+ }
85
+
86
+ const exec = (query: string, params?: readonly unknown[]) =>
87
+ sql.unsafe(query, params as any).pipe(Effect.orDie)
88
+
89
+ const setInternal = (e: PM, ns: string) =>
90
+ Effect.gen(function*() {
91
+ const row = toRow(e)
92
+ if (e._etag) {
93
+ yield* exec(
94
+ `UPDATE "${tableName}" SET _etag = $1, data = $2 WHERE id = $3 AND _etag = $4 AND _namespace = $5`,
95
+ [row._etag, row.data, row.id, e._etag, ns]
96
+ )
97
+ const existing = yield* exec(
98
+ `SELECT _etag FROM "${tableName}" WHERE id = $1 AND _namespace = $2`,
99
+ [row.id, ns]
100
+ )
101
+ const current = (existing as any[])[0]
102
+ if (!current || current._etag !== row._etag) {
103
+ if (current) {
104
+ return yield* new OptimisticConcurrencyException({
105
+ type: name,
106
+ id: row.id,
107
+ current: current._etag,
108
+ found: e._etag,
109
+ code: 412
110
+ })
111
+ }
112
+ return yield* new OptimisticConcurrencyException({
113
+ type: name,
114
+ id: row.id,
115
+ current: "",
116
+ found: e._etag,
117
+ code: 404
118
+ })
119
+ }
120
+ } else {
121
+ yield* exec(
122
+ `INSERT INTO "${tableName}" (id, _namespace, _etag, data) VALUES ($1, $2, $3, $4)`,
123
+ [row.id, ns, row._etag, row.data]
124
+ )
125
+ }
126
+ return row.item
127
+ })
128
+
129
+ const bulkSetInternal = (items: NonEmptyReadonlyArray<PM>, ns: string) =>
130
+ sql
131
+ .withTransaction(Effect.forEach(items, (e) => setInternal(e, ns)))
132
+ .pipe(
133
+ Effect.orDie,
134
+ Effect.map((_) => _ as unknown as NonEmptyReadonlyArray<PM>)
135
+ )
136
+
137
+ const ctx = yield* Effect.context<R>()
138
+ const seedCache = new Map<string, Effect.Effect<void>>()
139
+ const makeSeedEffect = Effect.fnUntraced(function*(ns: string) {
140
+ yield* ensureTable
141
+ if (!seed) return
142
+ const existing = yield* exec(
143
+ `SELECT id FROM "_migrations" WHERE id = $1 AND version = $2`,
144
+ [`${tableName}::${ns}`, tableName]
145
+ )
146
+ if ((existing as any[]).length > 0) return
147
+ yield* InfraLogger.logInfo(`Seeding data for ${name} (namespace: ${ns})`)
148
+ const items = yield* seed.pipe(Effect.provide(ctx), Effect.orDie)
149
+ const ne = toNonEmptyArray([...items])
150
+ if (Option.isSome(ne)) yield* bulkSetInternal(ne.value, ns)
151
+ yield* exec(
152
+ `INSERT INTO "_migrations" (id, version) VALUES ($1, $2)`,
153
+ [`${tableName}::${ns}`, tableName]
154
+ )
155
+ })
156
+ const seedNamespace = (ns: string) => {
157
+ let cached = seedCache.get(ns)
158
+ if (!cached) {
159
+ cached = Effect.cached(Effect.uninterruptible(makeSeedEffect(ns))).pipe(Effect.runSync)
160
+ seedCache.set(ns, cached)
161
+ }
162
+ return cached
163
+ }
164
+ const s: Store<IdKey, Encoded> = {
165
+ seedNamespace: (ns) => seedNamespace(ns),
166
+
167
+ all: resolveNamespace.pipe(Effect.flatMap((ns) =>
168
+ exec(`SELECT id, _etag, data FROM "${tableName}" WHERE _namespace = $1`, [ns])
169
+ .pipe(
170
+ Effect.map((rows) => (rows as any[]).map((r) => parseRow<Encoded>(r, idKey, defaultValues))),
171
+ Effect.withSpan("PgSQL.all [effect-app/infra/Store]", {
172
+ attributes: {
173
+ "repository.table_name": tableName,
174
+ "repository.model_name": name,
175
+ "repository.namespace": ns
176
+ }
177
+ }, { captureStackTrace: false })
178
+ )
179
+ )),
180
+
181
+ find: (id) =>
182
+ resolveNamespace.pipe(Effect
183
+ .flatMap((ns) =>
184
+ exec(`SELECT id, _etag, data FROM "${tableName}" WHERE id = $1 AND _namespace = $2`, [id, ns])
185
+ .pipe(
186
+ Effect.map((rows) => {
187
+ const row = (rows as any[])[0]
188
+ return row
189
+ ? Option.some(parseRow<Encoded>(row, idKey, defaultValues))
190
+ : Option.none()
191
+ }),
192
+ Effect.withSpan("PgSQL.find [effect-app/infra/Store]", {
193
+ attributes: { "repository.table_name": tableName, "repository.model_name": name, id }
194
+ }, { captureStackTrace: false })
195
+ )
196
+ )),
197
+
198
+ filter: <U extends keyof Encoded = never>(f: FilterArgs<Encoded, U>) => {
199
+ const filter = f
200
+ .filter
201
+ type M = U extends undefined ? Encoded : Pick<Encoded, U>
202
+ return resolveNamespace.pipe(Effect.flatMap((ns) =>
203
+ Effect
204
+ .sync(() => {
205
+ const q = buildWhereSQLQuery(
206
+ pgDialect,
207
+ idKey,
208
+ filter ? [{ t: "where-scope", result: filter, relation: "some" }] : [],
209
+ tableName,
210
+ defaultValues,
211
+ f.select as
212
+ | NonEmptyReadonlyArray<string | { key: string; subKeys: readonly string[] }>
213
+ | undefined,
214
+ f.order as NonEmptyReadonlyArray<{ key: string; direction: "ASC" | "DESC" }> | undefined,
215
+ f.skip,
216
+ f.limit
217
+ )
218
+ const nsPlaceholder = pgDialect.placeholder(q.params.length + 1)
219
+ const hasWhere = q.sql.includes("WHERE")
220
+ const nsSql = hasWhere
221
+ ? q.sql.replace("WHERE", `WHERE _namespace = ${nsPlaceholder} AND`)
222
+ : q.sql.replace(
223
+ `FROM "${tableName}"`,
224
+ `FROM "${tableName}" WHERE _namespace = ${nsPlaceholder}`
225
+ )
226
+ return { sql: nsSql, params: [...q.params, ns] }
227
+ })
228
+ .pipe(
229
+ Effect.tap((q) => logQuery(q)),
230
+ Effect.flatMap((q) =>
231
+ exec(q.sql, q.params).pipe(
232
+ Effect.map((rows) => {
233
+ if (f.select) {
234
+ return (rows as any[]).map((r) => {
235
+ const selected = parseSelectRow(r, idKey, {})
236
+ return {
237
+ ...Struct.pick(
238
+ defaultValues as any,
239
+ f.select!.filter((_) => typeof _ === "string") as never[]
240
+ ),
241
+ ...selected
242
+ } as M
243
+ })
244
+ }
245
+ return (rows as any[]).map((r) => parseRow<Encoded>(r, idKey, defaultValues) as any as M)
246
+ })
247
+ )
248
+ ),
249
+ Effect.withSpan("PgSQL.filter [effect-app/infra/Store]", {
250
+ attributes: { "repository.table_name": tableName, "repository.model_name": name }
251
+ }, { captureStackTrace: false })
252
+ )
253
+ ))
254
+ },
255
+
256
+ set: (e) =>
257
+ resolveNamespace.pipe(Effect.flatMap((ns) =>
258
+ setInternal(e, ns).pipe(
259
+ Effect.withSpan("PgSQL.set [effect-app/infra/Store]", {
260
+ attributes: { "repository.table_name": tableName, "repository.model_name": name, id: e[idKey] }
261
+ }, { captureStackTrace: false })
262
+ )
263
+ )),
264
+
265
+ batchSet: (items) =>
266
+ resolveNamespace.pipe(Effect.flatMap((ns) =>
267
+ bulkSetInternal(items, ns).pipe(
268
+ Effect.withSpan("PgSQL.batchSet [effect-app/infra/Store]", {
269
+ attributes: { "repository.table_name": tableName, "repository.model_name": name }
270
+ }, { captureStackTrace: false })
271
+ )
272
+ )),
273
+
274
+ bulkSet: (items) =>
275
+ resolveNamespace.pipe(Effect.flatMap((ns) =>
276
+ bulkSetInternal(items, ns).pipe(
277
+ Effect.withSpan("PgSQL.bulkSet [effect-app/infra/Store]", {
278
+ attributes: { "repository.table_name": tableName, "repository.model_name": name }
279
+ }, { captureStackTrace: false })
280
+ )
281
+ )),
282
+
283
+ batchRemove: (ids) => {
284
+ const placeholders = ids.map((_, i) => `$${i + 1}`).join(", ")
285
+ const nsPlaceholder = `$${ids.length + 1}`
286
+ return resolveNamespace.pipe(Effect.flatMap((ns) =>
287
+ exec(
288
+ `DELETE FROM "${tableName}" WHERE id IN (${placeholders}) AND _namespace = ${nsPlaceholder}`,
289
+ [...ids, ns]
290
+ )
291
+ .pipe(
292
+ Effect.asVoid,
293
+ Effect.withSpan("PgSQL.batchRemove [effect-app/infra/Store]", {
294
+ attributes: { "repository.table_name": tableName, "repository.model_name": name }
295
+ }, { captureStackTrace: false })
296
+ )
297
+ ))
298
+ },
299
+
300
+ queryRaw: (query) =>
301
+ s.all.pipe(
302
+ Effect.map(query.memory),
303
+ Effect.withSpan("PgSQL.queryRaw [effect-app/infra/Store]", {
304
+ attributes: { "repository.table_name": tableName, "repository.model_name": name }
305
+ }, { captureStackTrace: false })
306
+ )
307
+ }
308
+
309
+ // Eagerly seed primary namespace on initialization
310
+ yield* seedNamespace("primary")
311
+
312
+ return s
313
+ })
314
+ }
315
+ })
316
+ }
317
+
318
+ export function PgStoreLayer(cfg: StorageConfig) {
319
+ return StoreMaker
320
+ .toLayer(makePgStore(cfg))
321
+ }