@effect-app/infra 4.0.0-beta.22 → 4.0.0-beta.220

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 (310) hide show
  1. package/CHANGELOG.md +1640 -0
  2. package/_check.sh +1 -1
  3. package/dist/CUPS.d.ts +7 -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 +103 -51
  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 +46 -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 +139 -16
  53. package/dist/Model/query/dsl.d.ts.map +1 -1
  54. package/dist/Model/query/dsl.js +187 -1
  55. package/dist/Model/query/new-kid-interpreter.d.ts +76 -7
  56. package/dist/Model/query/new-kid-interpreter.d.ts.map +1 -1
  57. package/dist/Model/query/new-kid-interpreter.js +122 -6
  58. package/dist/Model/query.d.ts +1 -1
  59. package/dist/Model.d.ts +2 -1
  60. package/dist/Model.d.ts.map +1 -1
  61. package/dist/Model.js +2 -1
  62. package/dist/QueueMaker/SQLQueue.d.ts +5 -7
  63. package/dist/QueueMaker/SQLQueue.d.ts.map +1 -1
  64. package/dist/QueueMaker/SQLQueue.js +130 -116
  65. package/dist/QueueMaker/errors.d.ts +2 -2
  66. package/dist/QueueMaker/errors.d.ts.map +1 -1
  67. package/dist/QueueMaker/memQueue.d.ts +7 -4
  68. package/dist/QueueMaker/memQueue.d.ts.map +1 -1
  69. package/dist/QueueMaker/memQueue.js +75 -63
  70. package/dist/QueueMaker/sbqueue.d.ts +6 -3
  71. package/dist/QueueMaker/sbqueue.d.ts.map +1 -1
  72. package/dist/QueueMaker/sbqueue.js +52 -53
  73. package/dist/QueueMaker/service.d.ts +1 -1
  74. package/dist/RequestContext.d.ts +74 -35
  75. package/dist/RequestContext.d.ts.map +1 -1
  76. package/dist/RequestContext.js +13 -14
  77. package/dist/RequestFiberSet.d.ts +7 -7
  78. package/dist/RequestFiberSet.d.ts.map +1 -1
  79. package/dist/RequestFiberSet.js +3 -3
  80. package/dist/Store/ContextMapContainer.d.ts +19 -3
  81. package/dist/Store/ContextMapContainer.d.ts.map +1 -1
  82. package/dist/Store/ContextMapContainer.js +13 -3
  83. package/dist/Store/Cosmos/query.d.ts +5 -1
  84. package/dist/Store/Cosmos/query.d.ts.map +1 -1
  85. package/dist/Store/Cosmos/query.js +113 -34
  86. package/dist/Store/Cosmos.d.ts +1 -1
  87. package/dist/Store/Cosmos.d.ts.map +1 -1
  88. package/dist/Store/Cosmos.js +335 -243
  89. package/dist/Store/Disk.d.ts +2 -2
  90. package/dist/Store/Disk.d.ts.map +1 -1
  91. package/dist/Store/Disk.js +72 -35
  92. package/dist/Store/Memory.d.ts +6 -4
  93. package/dist/Store/Memory.d.ts.map +1 -1
  94. package/dist/Store/Memory.js +242 -58
  95. package/dist/Store/SQL/Pg.d.ts +4 -0
  96. package/dist/Store/SQL/Pg.d.ts.map +1 -0
  97. package/dist/Store/SQL/Pg.js +231 -0
  98. package/dist/Store/SQL/query.d.ts +42 -0
  99. package/dist/Store/SQL/query.d.ts.map +1 -0
  100. package/dist/Store/SQL/query.js +479 -0
  101. package/dist/Store/SQL.d.ts +20 -0
  102. package/dist/Store/SQL.d.ts.map +1 -0
  103. package/dist/Store/SQL.js +446 -0
  104. package/dist/Store/codeFilter.d.ts +1 -1
  105. package/dist/Store/codeFilter.d.ts.map +1 -1
  106. package/dist/Store/codeFilter.js +4 -2
  107. package/dist/Store/index.d.ts +5 -2
  108. package/dist/Store/index.d.ts.map +1 -1
  109. package/dist/Store/index.js +15 -3
  110. package/dist/Store/service.d.ts +22 -8
  111. package/dist/Store/service.d.ts.map +1 -1
  112. package/dist/Store/service.js +24 -6
  113. package/dist/Store/utils.d.ts +1 -1
  114. package/dist/Store/utils.d.ts.map +1 -1
  115. package/dist/Store/utils.js +3 -4
  116. package/dist/Store.d.ts +1 -1
  117. package/dist/adapters/SQL/Model.d.ts +31 -42
  118. package/dist/adapters/SQL/Model.d.ts.map +1 -1
  119. package/dist/adapters/SQL/Model.js +29 -38
  120. package/dist/adapters/SQL.d.ts +1 -1
  121. package/dist/adapters/ServiceBus.d.ts +11 -11
  122. package/dist/adapters/ServiceBus.d.ts.map +1 -1
  123. package/dist/adapters/ServiceBus.js +25 -21
  124. package/dist/adapters/cosmos-client.d.ts +3 -3
  125. package/dist/adapters/cosmos-client.d.ts.map +1 -1
  126. package/dist/adapters/cosmos-client.js +3 -3
  127. package/dist/adapters/index.d.ts +8 -2
  128. package/dist/adapters/index.d.ts.map +1 -1
  129. package/dist/adapters/index.js +8 -2
  130. package/dist/adapters/logger.d.ts +1 -1
  131. package/dist/adapters/logger.d.ts.map +1 -1
  132. package/dist/adapters/memQueue.d.ts +3 -3
  133. package/dist/adapters/memQueue.d.ts.map +1 -1
  134. package/dist/adapters/memQueue.js +3 -3
  135. package/dist/adapters/mongo-client.d.ts +3 -3
  136. package/dist/adapters/mongo-client.d.ts.map +1 -1
  137. package/dist/adapters/mongo-client.js +3 -3
  138. package/dist/adapters/redis-client.d.ts +3 -3
  139. package/dist/adapters/redis-client.d.ts.map +1 -1
  140. package/dist/adapters/redis-client.js +3 -3
  141. package/dist/api/ContextProvider.d.ts +8 -8
  142. package/dist/api/ContextProvider.d.ts.map +1 -1
  143. package/dist/api/ContextProvider.js +6 -6
  144. package/dist/api/codec.d.ts +1 -1
  145. package/dist/api/internal/RequestContextMiddleware.d.ts +2 -2
  146. package/dist/api/internal/RequestContextMiddleware.d.ts.map +1 -1
  147. package/dist/api/internal/RequestContextMiddleware.js +9 -6
  148. package/dist/api/internal/auth.d.ts +44 -6
  149. package/dist/api/internal/auth.d.ts.map +1 -1
  150. package/dist/api/internal/auth.js +160 -29
  151. package/dist/api/internal/events.d.ts +3 -3
  152. package/dist/api/internal/events.d.ts.map +1 -1
  153. package/dist/api/internal/events.js +10 -8
  154. package/dist/api/internal/health.d.ts +1 -1
  155. package/dist/api/layerUtils.d.ts +6 -6
  156. package/dist/api/layerUtils.d.ts.map +1 -1
  157. package/dist/api/layerUtils.js +5 -5
  158. package/dist/api/middlewares.d.ts +1 -1
  159. package/dist/api/reportError.d.ts +1 -1
  160. package/dist/api/routing/middleware/RouterMiddleware.d.ts +4 -4
  161. package/dist/api/routing/middleware/RouterMiddleware.d.ts.map +1 -1
  162. package/dist/api/routing/middleware/middleware.d.ts +39 -3
  163. package/dist/api/routing/middleware/middleware.d.ts.map +1 -1
  164. package/dist/api/routing/middleware/middleware.js +48 -16
  165. package/dist/api/routing/middleware.d.ts +1 -2
  166. package/dist/api/routing/middleware.d.ts.map +1 -1
  167. package/dist/api/routing/middleware.js +1 -2
  168. package/dist/api/routing/schema/jwt.d.ts +1 -1
  169. package/dist/api/routing/schema/jwt.d.ts.map +1 -1
  170. package/dist/api/routing/tsort.d.ts +1 -1
  171. package/dist/api/routing/tsort.d.ts.map +1 -1
  172. package/dist/api/routing/utils.d.ts +3 -3
  173. package/dist/api/routing/utils.d.ts.map +1 -1
  174. package/dist/api/routing.d.ts +80 -37
  175. package/dist/api/routing.d.ts.map +1 -1
  176. package/dist/api/routing.js +109 -41
  177. package/dist/api/setupRequest.d.ts +8 -5
  178. package/dist/api/setupRequest.d.ts.map +1 -1
  179. package/dist/api/setupRequest.js +12 -7
  180. package/dist/api/util.d.ts +1 -1
  181. package/dist/arbs.d.ts +1 -1
  182. package/dist/arbs.d.ts.map +1 -1
  183. package/dist/arbs.js +5 -3
  184. package/dist/errorReporter.d.ts +4 -4
  185. package/dist/errorReporter.d.ts.map +1 -1
  186. package/dist/errorReporter.js +20 -25
  187. package/dist/errors.d.ts +1 -1
  188. package/dist/fileUtil.d.ts +1 -1
  189. package/dist/fileUtil.d.ts.map +1 -1
  190. package/dist/index.d.ts +1 -1
  191. package/dist/logger/jsonLogger.d.ts +1 -1
  192. package/dist/logger/logFmtLogger.d.ts +1 -1
  193. package/dist/logger/shared.d.ts +1 -1
  194. package/dist/logger/shared.js +2 -2
  195. package/dist/logger.d.ts +1 -1
  196. package/dist/logger.d.ts.map +1 -1
  197. package/dist/otel.d.ts +75 -0
  198. package/dist/otel.d.ts.map +1 -0
  199. package/dist/otel.js +65 -0
  200. package/dist/rateLimit.d.ts +9 -3
  201. package/dist/rateLimit.d.ts.map +1 -1
  202. package/dist/rateLimit.js +5 -11
  203. package/dist/test.d.ts +2 -2
  204. package/dist/test.d.ts.map +1 -1
  205. package/dist/test.js +1 -1
  206. package/dist/vitest.d.ts +1 -1
  207. package/examples/query.ts +42 -38
  208. package/package.json +46 -37
  209. package/src/CUPS.ts +9 -11
  210. package/src/Emailer/Sendgrid.ts +17 -14
  211. package/src/Emailer/service.ts +9 -3
  212. package/src/MainFiberSet.ts +5 -6
  213. package/src/Model/Repository/Registry.ts +33 -0
  214. package/src/Model/Repository/ext.ts +96 -10
  215. package/src/Model/Repository/internal/internal.ts +218 -149
  216. package/src/Model/Repository/makeRepo.ts +12 -10
  217. package/src/Model/Repository/service.ts +31 -22
  218. package/src/Model/Repository/validation.ts +4 -4
  219. package/src/Model/Repository.ts +1 -0
  220. package/src/Model/dsl.ts +3 -3
  221. package/src/Model/filter/types/path/eager.ts +1 -2
  222. package/src/Model/query/dsl.ts +348 -18
  223. package/src/Model/query/new-kid-interpreter.ts +206 -6
  224. package/src/Model.ts +1 -0
  225. package/src/QueueMaker/SQLQueue.ts +144 -152
  226. package/src/QueueMaker/memQueue.ts +104 -103
  227. package/src/QueueMaker/sbqueue.ts +70 -86
  228. package/src/RequestContext.ts +14 -16
  229. package/src/RequestFiberSet.ts +2 -2
  230. package/src/Store/ContextMapContainer.ts +41 -2
  231. package/src/Store/Cosmos/query.ts +140 -43
  232. package/src/Store/Cosmos.ts +482 -349
  233. package/src/Store/Disk.ts +102 -65
  234. package/src/Store/Memory.ts +275 -87
  235. package/src/Store/SQL/Pg.ts +361 -0
  236. package/src/Store/SQL/query.ts +539 -0
  237. package/src/Store/SQL.ts +731 -0
  238. package/src/Store/codeFilter.ts +3 -1
  239. package/src/Store/index.ts +17 -2
  240. package/src/Store/service.ts +41 -10
  241. package/src/Store/utils.ts +23 -22
  242. package/src/adapters/SQL/Model.ts +41 -40
  243. package/src/adapters/ServiceBus.ts +125 -121
  244. package/src/adapters/cosmos-client.ts +2 -2
  245. package/src/adapters/index.ts +7 -0
  246. package/src/adapters/memQueue.ts +2 -2
  247. package/src/adapters/mongo-client.ts +2 -2
  248. package/src/adapters/redis-client.ts +2 -2
  249. package/src/api/ContextProvider.ts +12 -13
  250. package/src/api/internal/RequestContextMiddleware.ts +15 -5
  251. package/src/api/internal/auth.ts +246 -44
  252. package/src/api/internal/events.ts +13 -9
  253. package/src/api/layerUtils.ts +8 -8
  254. package/src/api/routing/middleware/RouterMiddleware.ts +4 -4
  255. package/src/api/routing/middleware/middleware.ts +55 -14
  256. package/src/api/routing/middleware.ts +0 -2
  257. package/src/api/routing.ts +296 -131
  258. package/src/api/setupRequest.ts +28 -8
  259. package/src/arbs.ts +4 -2
  260. package/src/errorReporter.ts +62 -74
  261. package/src/logger/shared.ts +1 -1
  262. package/src/otel.ts +152 -0
  263. package/src/rateLimit.ts +30 -22
  264. package/src/test.ts +1 -1
  265. package/test/auth.test.ts +101 -0
  266. package/test/contextProvider.test.ts +11 -11
  267. package/test/controller.test.ts +21 -30
  268. package/test/dist/auth.test.d.ts.map +1 -0
  269. package/test/dist/contextProvider.test.d.ts.map +1 -1
  270. package/test/dist/controller.test.d.ts.map +1 -1
  271. package/test/dist/date-query.test.d.ts.map +1 -0
  272. package/test/dist/fixtures.d.ts +26 -12
  273. package/test/dist/fixtures.d.ts.map +1 -1
  274. package/test/dist/fixtures.js +12 -10
  275. package/test/dist/query.test.d.ts.map +1 -1
  276. package/test/dist/rawQuery.test.d.ts.map +1 -1
  277. package/test/dist/repository-ext.test.d.ts.map +1 -0
  278. package/test/dist/requires.test.d.ts.map +1 -1
  279. package/test/dist/router-generator.test.d.ts.map +1 -0
  280. package/test/dist/routing-interruptibility.test.d.ts.map +1 -0
  281. package/test/dist/rpc-e2e-invalidation.test.d.ts.map +1 -0
  282. package/test/dist/rpc-multi-middleware.test.d.ts.map +1 -1
  283. package/test/dist/rpc-stream-fullstack.test.d.ts.map +1 -0
  284. package/test/dist/sql-store.test.d.ts.map +1 -0
  285. package/test/fixtures.ts +11 -9
  286. package/test/query.test.ts +813 -38
  287. package/test/rawQuery.test.ts +301 -20
  288. package/test/repository-ext.test.ts +60 -0
  289. package/test/requires.test.ts +6 -6
  290. package/test/router-generator.test.ts +183 -0
  291. package/test/routing-interruptibility.test.ts +63 -0
  292. package/test/rpc-e2e-invalidation.test.ts +251 -0
  293. package/test/rpc-multi-middleware.test.ts +78 -9
  294. package/test/rpc-stream-fullstack.test.ts +300 -0
  295. package/test/sql-store.test.ts +1592 -0
  296. package/test/validateSample.test.ts +15 -12
  297. package/tsconfig.examples.json +1 -1
  298. package/tsconfig.json +0 -1
  299. package/tsconfig.json.bak +2 -2
  300. package/tsconfig.src.json +35 -35
  301. package/tsconfig.test.json +2 -2
  302. package/dist/Operations.d.ts +0 -55
  303. package/dist/Operations.d.ts.map +0 -1
  304. package/dist/Operations.js +0 -102
  305. package/dist/OperationsRepo.d.ts +0 -41
  306. package/dist/OperationsRepo.d.ts.map +0 -1
  307. package/dist/OperationsRepo.js +0 -14
  308. package/eslint.config.mjs +0 -24
  309. package/src/Operations.ts +0 -235
  310. package/src/OperationsRepo.ts +0 -16
@@ -0,0 +1,731 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
3
+ import { Context, Layer, LayerMap } from "effect"
4
+ import { Effect, type NonEmptyReadonlyArray, Option, Struct } from "effect-app"
5
+ import { toNonEmptyArray } from "effect-app/Array"
6
+ import { SqlClient } from "effect/unstable/sql"
7
+ import { OptimisticConcurrencyException } from "../errors.js"
8
+ import { InfraLogger } from "../logger.js"
9
+ import type { FieldValues } from "../Model/filter/types.js"
10
+ import type { ComputedProjectionIrExpression } from "../Model/query.js"
11
+ import { annotateDb, type DbSystem } from "../otel.js"
12
+ import { storeId } from "./Memory.js"
13
+ import { type FilterArgs, type PersistenceModelType, type StorageConfig, type Store, type StoreConfig, StoreMaker } from "./service.js"
14
+ import { buildWhereSQLQuery, logQuery, type SQLDialect, sqliteDialect } from "./SQL/query.js"
15
+ import { makeETag } from "./utils.js"
16
+
17
+ export type WithNsTransactionFn = <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>
18
+
19
+ export class WithNsTransaction
20
+ extends Context.Service<WithNsTransaction, WithNsTransactionFn>()("effect-app/WithNsTransaction")
21
+ {}
22
+
23
+ /** @internal */
24
+ export const parseRow = <Encoded extends FieldValues>(
25
+ row: { id: string; _etag: string | null; data: string },
26
+ idKey: PropertyKey,
27
+ defaultValues: Partial<Encoded>
28
+ ): PersistenceModelType<Encoded> => {
29
+ const data = (typeof row.data === "string" ? JSON.parse(row.data) : row.data) as object
30
+ return { ...defaultValues, ...data, [idKey]: row.id, _etag: row._etag ?? undefined } as PersistenceModelType<Encoded>
31
+ }
32
+
33
+ const parseSelectRow = (
34
+ row: Record<string, unknown>,
35
+ idKey: PropertyKey
36
+ ): any => {
37
+ const result: Record<string, unknown> = {}
38
+ for (const [key, value] of Object.entries(row)) {
39
+ if (key === "id") {
40
+ result[idKey as string] = value
41
+ result["id"] = value
42
+ } else if (typeof value === "string") {
43
+ try {
44
+ result[key] = JSON.parse(value)
45
+ } catch {
46
+ result[key] = value
47
+ }
48
+ } else {
49
+ result[key] = value
50
+ }
51
+ }
52
+ return result
53
+ }
54
+
55
+ function makeSQLStoreInt(system: DbSystem, dialect: SQLDialect, jsonColumnType: string) {
56
+ return Effect.fnUntraced(function*({ prefix }: StorageConfig) {
57
+ const sql = yield* SqlClient.SqlClient
58
+ return {
59
+ make: Effect.fnUntraced(function*<IdKey extends keyof Encoded, Encoded extends FieldValues, R = never, E = never>(
60
+ name: string,
61
+ idKey: IdKey,
62
+ seed?: Effect.Effect<Iterable<Encoded>, E, R>,
63
+ config?: StoreConfig<Encoded>
64
+ ) {
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[]) => sql.unsafe(query, params as any).pipe(Effect.orDie)
101
+
102
+ const setInternal = Effect.fnUntraced(function*(e: PM, ns: string) {
103
+ const row = toRow(e)
104
+ if (e._etag) {
105
+ yield* exec(
106
+ `UPDATE "${tableName}" SET _etag = ?, data = ? WHERE id = ? AND _etag = ? AND _namespace = ?`,
107
+ [row._etag, row.data, row.id, e._etag, ns]
108
+ )
109
+ const existing = yield* exec(
110
+ `SELECT _etag FROM "${tableName}" WHERE id = ? AND _namespace = ?`,
111
+ [row.id, ns]
112
+ )
113
+ const current = (existing as any[])[0]
114
+ if (!current || current._etag !== row._etag) {
115
+ if (current) {
116
+ return yield* new OptimisticConcurrencyException({
117
+ type: name,
118
+ id: row.id,
119
+ current: current._etag,
120
+ found: e._etag,
121
+ code: 412
122
+ })
123
+ }
124
+ return yield* new OptimisticConcurrencyException({
125
+ type: name,
126
+ id: row.id,
127
+ current: "",
128
+ found: e._etag,
129
+ code: 404
130
+ })
131
+ }
132
+ } else {
133
+ yield* exec(
134
+ `INSERT INTO "${tableName}" (id, _namespace, _etag, data) VALUES (?, ?, ?, ?)`,
135
+ [row.id, ns, row._etag, row.data]
136
+ )
137
+ }
138
+ return row.item
139
+ })
140
+
141
+ const bulkSetInternal = (items: NonEmptyReadonlyArray<PM>, ns: string) =>
142
+ sql
143
+ .withTransaction(Effect.forEach(items, (e) => setInternal(e, ns)))
144
+ .pipe(
145
+ Effect.orDie,
146
+ Effect.map((_) => _ as unknown as NonEmptyReadonlyArray<PM>)
147
+ )
148
+
149
+ const ctx = yield* Effect.context<R>()
150
+ const seedCache = new Map<string, Effect.Effect<void>>()
151
+ const makeSeedEffect = Effect.fnUntraced(function*(ns: string) {
152
+ yield* ensureTable
153
+ if (!seed) return
154
+ const existing = yield* exec(
155
+ `SELECT id FROM "_migrations" WHERE id = ? AND version = ?`,
156
+ [`${tableName}::${ns}`, tableName]
157
+ )
158
+ if ((existing as any[]).length > 0) return
159
+ yield* InfraLogger.logInfo(`Seeding data for ${name} (namespace: ${ns})`)
160
+ const items = yield* seed.pipe(Effect.provide(ctx), Effect.orDie)
161
+ const ne = toNonEmptyArray([...items])
162
+ if (Option.isSome(ne)) yield* bulkSetInternal(ne.value, ns)
163
+ yield* exec(
164
+ `INSERT INTO "_migrations" (id, version) VALUES (?, ?)`,
165
+ [`${tableName}::${ns}`, tableName]
166
+ )
167
+ })
168
+ const seedNamespace = (ns: string) => {
169
+ let cached = seedCache.get(ns)
170
+ if (!cached) {
171
+ cached = Effect.cached(Effect.uninterruptible(makeSeedEffect(ns))).pipe(Effect.runSync)
172
+ seedCache.set(ns, cached)
173
+ }
174
+ return cached
175
+ }
176
+ const s: Store<IdKey, Encoded> = {
177
+ seedNamespace: (ns) => seedNamespace(ns),
178
+
179
+ all: resolveNamespace.pipe(
180
+ Effect.flatMap((ns) => {
181
+ const sqlText = `SELECT id, _etag, data FROM "${tableName}" WHERE _namespace = ?`
182
+ return exec(sqlText, [ns])
183
+ .pipe(
184
+ Effect.map((rows) => (rows as any[]).map((r) => parseRow<Encoded>(r, idKey, defaultValues))),
185
+ annotateDb({
186
+ operation: "all",
187
+ system,
188
+ collection: tableName,
189
+ namespace: ns,
190
+ entity: name,
191
+ query: sqlText
192
+ })
193
+ )
194
+ })
195
+ ),
196
+
197
+ find: (id) =>
198
+ resolveNamespace.pipe(
199
+ Effect.flatMap((ns) => {
200
+ const sqlText = `SELECT id, _etag, data FROM "${tableName}" WHERE id = ? AND _namespace = ?`
201
+ return exec(sqlText, [id, ns])
202
+ .pipe(
203
+ Effect.map((rows) => {
204
+ const row = (rows as any[])[0]
205
+ return row
206
+ ? Option.some(parseRow<Encoded>(row, idKey, defaultValues))
207
+ : Option.none()
208
+ }),
209
+ annotateDb({
210
+ operation: "find",
211
+ system,
212
+ collection: tableName,
213
+ namespace: ns,
214
+ entity: name,
215
+ query: sqlText,
216
+ extra: { "app.entity.id": id }
217
+ })
218
+ )
219
+ })
220
+ ),
221
+
222
+ filter: <U extends keyof Encoded = never>(f: FilterArgs<Encoded, U>) => {
223
+ const filter = f
224
+ .filter
225
+ type M = U extends undefined ? Encoded
226
+ : Pick<Encoded, U>
227
+ return resolveNamespace
228
+ .pipe(Effect
229
+ .flatMap((ns) =>
230
+ Effect
231
+ .sync(() => {
232
+ return buildWhereSQLQuery(
233
+ dialect,
234
+ idKey,
235
+ filter ? [{ t: "where-scope", result: filter, relation: "some" }] : [],
236
+ tableName,
237
+ defaultValues,
238
+ f
239
+ .select as
240
+ | NonEmptyReadonlyArray<
241
+ string | {
242
+ key: string
243
+ subKeys: readonly string[]
244
+ } | {
245
+ key: string
246
+ computed: ComputedProjectionIrExpression
247
+ }
248
+ >
249
+ | undefined,
250
+ f
251
+ .order,
252
+ f
253
+ .skip,
254
+ f
255
+ .limit,
256
+ ns
257
+ )
258
+ })
259
+ .pipe(
260
+ Effect
261
+ .tap((q) => logQuery(q)),
262
+ Effect.tap((q) => Effect.annotateCurrentSpan({ "db.query.text": q.sql })),
263
+ Effect.flatMap((q) =>
264
+ exec(q.sql, q.params).pipe(
265
+ Effect.map((rows) => {
266
+ if (f.select) {
267
+ return (rows as any[]).map((r) => {
268
+ const selected = parseSelectRow(r, idKey)
269
+ return {
270
+ ...Struct.pick(
271
+ defaultValues as any,
272
+ f.select!.filter((_) => typeof _ === "string") as never[]
273
+ ),
274
+ ...selected
275
+ } as M
276
+ })
277
+ }
278
+ return (rows as any[]).map((r) => parseRow<Encoded>(r, idKey, defaultValues) as any as M)
279
+ })
280
+ )
281
+ ),
282
+ annotateDb({
283
+ operation: "filter",
284
+ system,
285
+ collection: tableName,
286
+ namespace: ns,
287
+ entity: name
288
+ })
289
+ )
290
+ ))
291
+ },
292
+
293
+ set: (e) =>
294
+ resolveNamespace.pipe(Effect.flatMap((ns) =>
295
+ setInternal(e, ns).pipe(
296
+ annotateDb({
297
+ operation: "set",
298
+ system,
299
+ collection: tableName,
300
+ namespace: ns,
301
+ entity: name,
302
+ extra: { "app.entity.id": e[idKey] }
303
+ })
304
+ )
305
+ )),
306
+
307
+ batchSet: (items) =>
308
+ resolveNamespace.pipe(Effect.flatMap((ns) =>
309
+ bulkSetInternal(items, ns).pipe(
310
+ annotateDb({
311
+ operation: "batchSet",
312
+ system,
313
+ collection: tableName,
314
+ namespace: ns,
315
+ entity: name
316
+ })
317
+ )
318
+ )),
319
+
320
+ bulkSet: (items) =>
321
+ resolveNamespace.pipe(Effect.flatMap((ns) =>
322
+ bulkSetInternal(items, ns).pipe(
323
+ annotateDb({
324
+ operation: "bulkSet",
325
+ system,
326
+ collection: tableName,
327
+ namespace: ns,
328
+ entity: name
329
+ })
330
+ )
331
+ )),
332
+
333
+ batchRemove: (ids) => {
334
+ const placeholders = ids.map(() => "?").join(", ")
335
+ return resolveNamespace.pipe(Effect.flatMap((ns) => {
336
+ const sqlText = `DELETE FROM "${tableName}" WHERE id IN (${placeholders}) AND _namespace = ?`
337
+ return exec(sqlText, [...ids, ns])
338
+ .pipe(
339
+ Effect.asVoid,
340
+ annotateDb({
341
+ operation: "batchRemove",
342
+ system,
343
+ collection: tableName,
344
+ namespace: ns,
345
+ entity: name,
346
+ query: sqlText
347
+ })
348
+ )
349
+ }))
350
+ },
351
+
352
+ queryRaw: (query) =>
353
+ s.all.pipe(
354
+ Effect.map(query.memory),
355
+ annotateDb({
356
+ operation: "queryRaw",
357
+ system,
358
+ collection: tableName,
359
+ entity: name
360
+ })
361
+ )
362
+ }
363
+
364
+ // Eagerly seed primary namespace on initialization
365
+ yield* seedNamespace("primary")
366
+
367
+ return s
368
+ })
369
+ }
370
+ })
371
+ }
372
+
373
+ type WithNsSqlFn = <A, E2, R2>(
374
+ ns: string,
375
+ f: (sql: SqlClient.SqlClient) => Effect.Effect<A, E2, R2>
376
+ ) => Effect.Effect<A, E2, R2>
377
+
378
+ function makeSQLiteStorePerNs(
379
+ withNsSql: WithNsSqlFn,
380
+ { prefix }: StorageConfig
381
+ ) {
382
+ return {
383
+ make: Effect.fnUntraced(function*<IdKey extends keyof Encoded, Encoded extends FieldValues, R = never, E = never>(
384
+ name: string,
385
+ idKey: IdKey,
386
+ seed?: Effect.Effect<Iterable<Encoded>, E, R>,
387
+ config?: StoreConfig<Encoded>
388
+ ) {
389
+ type PM = PersistenceModelType<Encoded>
390
+ const tableName = `${prefix}${name}`
391
+ const defaultValues = config?.defaultValues ?? {}
392
+
393
+ const resolveNamespace = !config?.allowNamespace
394
+ ? Effect.succeed("primary")
395
+ : storeId.asEffect().pipe(Effect.map((namespace) => {
396
+ if (namespace !== "primary" && !config.allowNamespace!(namespace)) {
397
+ throw new Error(`Namespace ${namespace} not allowed!`)
398
+ }
399
+ return namespace
400
+ }))
401
+
402
+ const toRow = (e: PM) => {
403
+ const newE = makeETag(e)
404
+ const id = newE[idKey] as string
405
+ const { _etag, [idKey]: _id, ...rest } = newE as any
406
+ const data = JSON.stringify(rest)
407
+ return { id, _etag: newE._etag!, data, item: newE }
408
+ }
409
+
410
+ const exec = (ns: string, query: string, params?: readonly unknown[]) =>
411
+ withNsSql(ns, (sql) => sql.unsafe(query, params as any).pipe(Effect.orDie))
412
+
413
+ const ensureTable = (ns: string) =>
414
+ withNsSql(ns, (sql) =>
415
+ sql
416
+ .unsafe(
417
+ `CREATE TABLE IF NOT EXISTS "${tableName}" (id TEXT NOT NULL PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`
418
+ )
419
+ .pipe(
420
+ Effect.andThen(
421
+ sql.unsafe(
422
+ `CREATE TABLE IF NOT EXISTS "_migrations" (id TEXT NOT NULL, version TEXT NOT NULL, PRIMARY KEY (id, version))`
423
+ )
424
+ ),
425
+ Effect.orDie,
426
+ Effect.asVoid
427
+ ))
428
+
429
+ const setInternal = Effect.fnUntraced(function*(e: PM, ns: string) {
430
+ const row = toRow(e)
431
+ if (e._etag) {
432
+ yield* exec(
433
+ ns,
434
+ `UPDATE "${tableName}" SET _etag = ?, data = ? WHERE id = ? AND _etag = ?`,
435
+ [row._etag, row.data, row.id, e._etag]
436
+ )
437
+ const existing = yield* exec(
438
+ ns,
439
+ `SELECT _etag FROM "${tableName}" WHERE id = ?`,
440
+ [row.id]
441
+ )
442
+ const current = (existing as any[])[0]
443
+ if (!current || current._etag !== row._etag) {
444
+ if (current) {
445
+ return yield* new OptimisticConcurrencyException({
446
+ type: name,
447
+ id: row.id,
448
+ current: current._etag,
449
+ found: e._etag,
450
+ code: 412
451
+ })
452
+ }
453
+ return yield* new OptimisticConcurrencyException({
454
+ type: name,
455
+ id: row.id,
456
+ current: "",
457
+ found: e._etag,
458
+ code: 404
459
+ })
460
+ }
461
+ } else {
462
+ yield* exec(
463
+ ns,
464
+ `INSERT INTO "${tableName}" (id, _etag, data) VALUES (?, ?, ?)`,
465
+ [row.id, row._etag, row.data]
466
+ )
467
+ }
468
+ return row.item
469
+ })
470
+
471
+ const bulkSetInternal = (items: NonEmptyReadonlyArray<PM>, ns: string) =>
472
+ withNsSql(ns, (sql) =>
473
+ sql
474
+ .withTransaction(Effect.forEach(items, (e) => setInternal(e, ns)))
475
+ .pipe(
476
+ Effect.orDie,
477
+ Effect.map((_) => _ as unknown as NonEmptyReadonlyArray<PM>)
478
+ ))
479
+
480
+ const ctx = yield* Effect.context<R>()
481
+ const seedCache = new Map<string, Effect.Effect<void>>()
482
+ const makeSeedEffect = Effect.fnUntraced(function*(ns: string) {
483
+ yield* ensureTable(ns)
484
+ if (!seed) return
485
+ const existing = yield* exec(
486
+ ns,
487
+ `SELECT id FROM "_migrations" WHERE id = ? AND version = ?`,
488
+ [tableName, tableName]
489
+ )
490
+ if ((existing as any[]).length > 0) return
491
+ yield* InfraLogger.logInfo(`Seeding data for ${name} (namespace: ${ns})`)
492
+ const items = yield* seed.pipe(Effect.provide(ctx), Effect.orDie)
493
+ const ne = toNonEmptyArray([...items])
494
+ if (Option.isSome(ne)) yield* bulkSetInternal(ne.value, ns)
495
+ yield* exec(
496
+ ns,
497
+ `INSERT INTO "_migrations" (id, version) VALUES (?, ?)`,
498
+ [tableName, tableName]
499
+ )
500
+ })
501
+ const seedNamespace = (ns: string) => {
502
+ let cached = seedCache.get(ns)
503
+ if (!cached) {
504
+ cached = Effect.cached(Effect.uninterruptible(makeSeedEffect(ns))).pipe(Effect.runSync)
505
+ seedCache.set(ns, cached)
506
+ }
507
+ return cached
508
+ }
509
+
510
+ const s: Store<IdKey, Encoded> = {
511
+ seedNamespace: (ns) => seedNamespace(ns),
512
+
513
+ all: resolveNamespace.pipe(Effect.flatMap((ns) => {
514
+ const sqlText = `SELECT id, _etag, data FROM "${tableName}"`
515
+ return exec(ns, sqlText)
516
+ .pipe(
517
+ Effect.map((rows) => (rows as any[]).map((r) => parseRow<Encoded>(r, idKey, defaultValues))),
518
+ annotateDb({
519
+ operation: "all",
520
+ system: "sqlite",
521
+ collection: tableName,
522
+ namespace: ns,
523
+ entity: name,
524
+ query: sqlText
525
+ })
526
+ )
527
+ })),
528
+
529
+ find: (id) =>
530
+ resolveNamespace.pipe(
531
+ Effect.flatMap((ns) => {
532
+ const sqlText = `SELECT id, _etag, data FROM "${tableName}" WHERE id = ?`
533
+ return exec(ns, sqlText, [id])
534
+ .pipe(
535
+ Effect.map((rows) => {
536
+ const row = (rows as any[])[0]
537
+ return row
538
+ ? Option.some(parseRow<Encoded>(row, idKey, defaultValues))
539
+ : Option.none()
540
+ }),
541
+ annotateDb({
542
+ operation: "find",
543
+ system: "sqlite",
544
+ collection: tableName,
545
+ namespace: ns,
546
+ entity: name,
547
+ query: sqlText,
548
+ extra: { "app.entity.id": id }
549
+ })
550
+ )
551
+ })
552
+ ),
553
+
554
+ filter: <U extends keyof Encoded = never>(f: FilterArgs<Encoded, U>) => {
555
+ const filter = f
556
+ .filter
557
+ type M = U extends undefined ? Encoded
558
+ : Pick<Encoded, U>
559
+ return resolveNamespace
560
+ .pipe(Effect
561
+ .flatMap((ns) =>
562
+ Effect
563
+ .sync(() =>
564
+ buildWhereSQLQuery(
565
+ sqliteDialect,
566
+ idKey,
567
+ filter ? [{ t: "where-scope", result: filter, relation: "some" }] : [],
568
+ tableName,
569
+ defaultValues,
570
+ f
571
+ .select as
572
+ | NonEmptyReadonlyArray<
573
+ string | {
574
+ key: string
575
+ subKeys: readonly string[]
576
+ } | {
577
+ key: string
578
+ computed: ComputedProjectionIrExpression
579
+ }
580
+ >
581
+ | undefined,
582
+ f
583
+ .order as NonEmptyReadonlyArray<{ key: string; direction: "ASC" | "DESC" }> | undefined,
584
+ f
585
+ .skip,
586
+ f
587
+ .limit
588
+ )
589
+ )
590
+ .pipe(
591
+ Effect
592
+ .tap((q) => logQuery(q)),
593
+ Effect.tap((q) => Effect.annotateCurrentSpan({ "db.query.text": q.sql })),
594
+ Effect.flatMap((q) =>
595
+ exec(ns, q.sql, q.params).pipe(
596
+ Effect.map((rows) => {
597
+ if (f.select) {
598
+ return (rows as any[]).map((r) => {
599
+ const selected = parseSelectRow(r, idKey)
600
+ return {
601
+ ...Struct.pick(
602
+ defaultValues as any,
603
+ f.select!.filter((_) => typeof _ === "string") as never[]
604
+ ),
605
+ ...selected
606
+ } as M
607
+ })
608
+ }
609
+ return (rows as any[]).map((r) => parseRow<Encoded>(r, idKey, defaultValues) as any as M)
610
+ })
611
+ )
612
+ ),
613
+ annotateDb({
614
+ operation: "filter",
615
+ system: "sqlite",
616
+ collection: tableName,
617
+ namespace: ns,
618
+ entity: name
619
+ })
620
+ )
621
+ ))
622
+ },
623
+
624
+ set: (e) =>
625
+ resolveNamespace.pipe(Effect.flatMap((ns) =>
626
+ setInternal(e, ns).pipe(
627
+ annotateDb({
628
+ operation: "set",
629
+ system: "sqlite",
630
+ collection: tableName,
631
+ namespace: ns,
632
+ entity: name,
633
+ extra: { "app.entity.id": e[idKey] }
634
+ })
635
+ )
636
+ )),
637
+
638
+ batchSet: (items) =>
639
+ resolveNamespace.pipe(Effect.flatMap((ns) =>
640
+ bulkSetInternal(items, ns).pipe(
641
+ annotateDb({
642
+ operation: "batchSet",
643
+ system: "sqlite",
644
+ collection: tableName,
645
+ namespace: ns,
646
+ entity: name
647
+ })
648
+ )
649
+ )),
650
+
651
+ bulkSet: (items) =>
652
+ resolveNamespace.pipe(Effect.flatMap((ns) =>
653
+ bulkSetInternal(items, ns).pipe(
654
+ annotateDb({
655
+ operation: "bulkSet",
656
+ system: "sqlite",
657
+ collection: tableName,
658
+ namespace: ns,
659
+ entity: name
660
+ })
661
+ )
662
+ )),
663
+
664
+ batchRemove: (ids) => {
665
+ const placeholders = ids.map(() => "?").join(", ")
666
+ return resolveNamespace.pipe(Effect.flatMap((ns) => {
667
+ const sqlText = `DELETE FROM "${tableName}" WHERE id IN (${placeholders})`
668
+ return exec(ns, sqlText, [...ids])
669
+ .pipe(
670
+ Effect.asVoid,
671
+ annotateDb({
672
+ operation: "batchRemove",
673
+ system: "sqlite",
674
+ collection: tableName,
675
+ namespace: ns,
676
+ entity: name,
677
+ query: sqlText
678
+ })
679
+ )
680
+ }))
681
+ },
682
+
683
+ queryRaw: (query) =>
684
+ s.all.pipe(
685
+ Effect.map(query.memory),
686
+ annotateDb({
687
+ operation: "queryRaw",
688
+ system: "sqlite",
689
+ collection: tableName,
690
+ entity: name
691
+ })
692
+ )
693
+ }
694
+
695
+ yield* seedNamespace("primary")
696
+
697
+ return s
698
+ })
699
+ }
700
+ }
701
+
702
+ export function SQLiteStoreLayer(
703
+ cfg: StorageConfig,
704
+ options?: { makeSqlClientLayer?: (namespace: string) => Layer.Layer<SqlClient.SqlClient> }
705
+ ) {
706
+ if (options?.makeSqlClientLayer) {
707
+ return Layer.effectContext(
708
+ Effect.gen(function*() {
709
+ const layerMap = yield* LayerMap.make(
710
+ (namespace: string) => options.makeSqlClientLayer!(namespace),
711
+ { idleTimeToLive: "10 minutes" }
712
+ )
713
+
714
+ const withNsSql: WithNsSqlFn = (ns, f) => SqlClient.SqlClient.use(f).pipe(Effect.provide(layerMap.get(ns)))
715
+
716
+ const storeMaker = makeSQLiteStorePerNs(withNsSql, cfg)
717
+
718
+ const withTransaction: WithNsTransactionFn = (effect) =>
719
+ storeId.asEffect().pipe(
720
+ Effect.flatMap((ns) => withNsSql(ns, (sql) => sql.withTransaction(effect).pipe(Effect.orDie)))
721
+ )
722
+
723
+ return StoreMaker.context(storeMaker).pipe(
724
+ Context.add(WithNsTransaction, withTransaction)
725
+ )
726
+ })
727
+ )
728
+ }
729
+ return StoreMaker
730
+ .toLayer(makeSQLStoreInt("sqlite", sqliteDialect, "JSON")(cfg))
731
+ }