@effect-app/infra 4.0.0-beta.20 → 4.0.0-beta.201

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