@effect-app/infra 4.0.0-beta.21 → 4.0.0-beta.211

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