@effect-app/infra 4.0.0-beta.9 → 4.0.0-beta.91

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 (176) hide show
  1. package/CHANGELOG.md +596 -0
  2. package/dist/CUPS.d.ts +3 -3
  3. package/dist/CUPS.d.ts.map +1 -1
  4. package/dist/CUPS.js +3 -3
  5. package/dist/Emailer/Sendgrid.js +1 -1
  6. package/dist/Emailer/service.d.ts +3 -3
  7. package/dist/Emailer/service.d.ts.map +1 -1
  8. package/dist/Emailer/service.js +3 -3
  9. package/dist/MainFiberSet.d.ts +2 -2
  10. package/dist/MainFiberSet.d.ts.map +1 -1
  11. package/dist/MainFiberSet.js +3 -3
  12. package/dist/Model/Repository/internal/internal.d.ts +3 -3
  13. package/dist/Model/Repository/internal/internal.d.ts.map +1 -1
  14. package/dist/Model/Repository/internal/internal.js +11 -7
  15. package/dist/Model/Repository/makeRepo.d.ts +2 -2
  16. package/dist/Model/Repository/makeRepo.d.ts.map +1 -1
  17. package/dist/Model/Repository/makeRepo.js +1 -1
  18. package/dist/Model/Repository/validation.d.ts +7 -6
  19. package/dist/Model/Repository/validation.d.ts.map +1 -1
  20. package/dist/Model/query/dsl.d.ts +9 -9
  21. package/dist/Operations.d.ts +2 -2
  22. package/dist/Operations.d.ts.map +1 -1
  23. package/dist/Operations.js +3 -3
  24. package/dist/OperationsRepo.d.ts +2 -2
  25. package/dist/OperationsRepo.d.ts.map +1 -1
  26. package/dist/OperationsRepo.js +3 -3
  27. package/dist/QueueMaker/SQLQueue.d.ts +3 -5
  28. package/dist/QueueMaker/SQLQueue.d.ts.map +1 -1
  29. package/dist/QueueMaker/SQLQueue.js +9 -7
  30. package/dist/QueueMaker/errors.d.ts +1 -1
  31. package/dist/QueueMaker/errors.d.ts.map +1 -1
  32. package/dist/QueueMaker/memQueue.d.ts.map +1 -1
  33. package/dist/QueueMaker/memQueue.js +10 -9
  34. package/dist/QueueMaker/sbqueue.d.ts.map +1 -1
  35. package/dist/QueueMaker/sbqueue.js +11 -9
  36. package/dist/RequestContext.d.ts +41 -21
  37. package/dist/RequestContext.d.ts.map +1 -1
  38. package/dist/RequestContext.js +5 -5
  39. package/dist/RequestFiberSet.d.ts +2 -2
  40. package/dist/RequestFiberSet.d.ts.map +1 -1
  41. package/dist/RequestFiberSet.js +5 -5
  42. package/dist/Store/ContextMapContainer.d.ts +14 -3
  43. package/dist/Store/ContextMapContainer.d.ts.map +1 -1
  44. package/dist/Store/ContextMapContainer.js +64 -3
  45. package/dist/Store/Cosmos.d.ts.map +1 -1
  46. package/dist/Store/Cosmos.js +91 -56
  47. package/dist/Store/Disk.d.ts.map +1 -1
  48. package/dist/Store/Disk.js +3 -4
  49. package/dist/Store/Memory.d.ts +2 -2
  50. package/dist/Store/Memory.d.ts.map +1 -1
  51. package/dist/Store/Memory.js +4 -4
  52. package/dist/Store/SQL/Pg.d.ts +4 -0
  53. package/dist/Store/SQL/Pg.d.ts.map +1 -0
  54. package/dist/Store/SQL/Pg.js +186 -0
  55. package/dist/Store/SQL/query.d.ts +36 -0
  56. package/dist/Store/SQL/query.d.ts.map +1 -0
  57. package/dist/Store/SQL/query.js +385 -0
  58. package/dist/Store/SQL.d.ts +11 -0
  59. package/dist/Store/SQL.d.ts.map +1 -0
  60. package/dist/Store/SQL.js +212 -0
  61. package/dist/Store/index.d.ts +1 -1
  62. package/dist/Store/index.d.ts.map +1 -1
  63. package/dist/Store/index.js +11 -1
  64. package/dist/Store/service.d.ts +8 -5
  65. package/dist/Store/service.d.ts.map +1 -1
  66. package/dist/Store/service.js +14 -6
  67. package/dist/adapters/SQL/Model.d.ts +2 -5
  68. package/dist/adapters/SQL/Model.d.ts.map +1 -1
  69. package/dist/adapters/SQL/Model.js +21 -13
  70. package/dist/adapters/ServiceBus.d.ts +6 -6
  71. package/dist/adapters/ServiceBus.d.ts.map +1 -1
  72. package/dist/adapters/ServiceBus.js +9 -9
  73. package/dist/adapters/cosmos-client.d.ts +2 -2
  74. package/dist/adapters/cosmos-client.d.ts.map +1 -1
  75. package/dist/adapters/cosmos-client.js +3 -3
  76. package/dist/adapters/logger.d.ts.map +1 -1
  77. package/dist/adapters/memQueue.d.ts +2 -2
  78. package/dist/adapters/memQueue.d.ts.map +1 -1
  79. package/dist/adapters/memQueue.js +3 -3
  80. package/dist/adapters/mongo-client.d.ts +2 -2
  81. package/dist/adapters/mongo-client.d.ts.map +1 -1
  82. package/dist/adapters/mongo-client.js +3 -3
  83. package/dist/adapters/redis-client.d.ts +3 -3
  84. package/dist/adapters/redis-client.d.ts.map +1 -1
  85. package/dist/adapters/redis-client.js +3 -3
  86. package/dist/api/ContextProvider.d.ts +6 -6
  87. package/dist/api/ContextProvider.d.ts.map +1 -1
  88. package/dist/api/ContextProvider.js +6 -6
  89. package/dist/api/internal/auth.d.ts +1 -1
  90. package/dist/api/internal/events.d.ts +2 -2
  91. package/dist/api/internal/events.d.ts.map +1 -1
  92. package/dist/api/internal/events.js +7 -5
  93. package/dist/api/layerUtils.d.ts +5 -5
  94. package/dist/api/layerUtils.d.ts.map +1 -1
  95. package/dist/api/layerUtils.js +5 -5
  96. package/dist/api/routing/middleware/RouterMiddleware.d.ts +3 -3
  97. package/dist/api/routing/middleware/RouterMiddleware.d.ts.map +1 -1
  98. package/dist/api/routing/middleware/middleware.d.ts +35 -1
  99. package/dist/api/routing/middleware/middleware.d.ts.map +1 -1
  100. package/dist/api/routing/middleware/middleware.js +39 -1
  101. package/dist/api/routing/schema/jwt.d.ts +1 -1
  102. package/dist/api/routing/schema/jwt.d.ts.map +1 -1
  103. package/dist/api/routing/schema/jwt.js +1 -1
  104. package/dist/api/routing.d.ts +1 -5
  105. package/dist/api/routing.d.ts.map +1 -1
  106. package/dist/api/routing.js +3 -2
  107. package/dist/api/setupRequest.d.ts +6 -3
  108. package/dist/api/setupRequest.d.ts.map +1 -1
  109. package/dist/api/setupRequest.js +11 -6
  110. package/dist/errorReporter.d.ts +1 -1
  111. package/dist/errorReporter.d.ts.map +1 -1
  112. package/dist/errorReporter.js +1 -1
  113. package/dist/fileUtil.js +1 -1
  114. package/dist/logger.d.ts.map +1 -1
  115. package/dist/rateLimit.js +1 -1
  116. package/examples/query.ts +29 -25
  117. package/package.json +32 -18
  118. package/src/CUPS.ts +2 -2
  119. package/src/Emailer/Sendgrid.ts +1 -1
  120. package/src/Emailer/service.ts +2 -2
  121. package/src/MainFiberSet.ts +2 -2
  122. package/src/Model/Repository/internal/internal.ts +11 -8
  123. package/src/Model/Repository/makeRepo.ts +2 -2
  124. package/src/Operations.ts +2 -2
  125. package/src/OperationsRepo.ts +2 -2
  126. package/src/QueueMaker/SQLQueue.ts +10 -10
  127. package/src/QueueMaker/memQueue.ts +41 -42
  128. package/src/QueueMaker/sbqueue.ts +65 -62
  129. package/src/RequestContext.ts +4 -4
  130. package/src/RequestFiberSet.ts +4 -4
  131. package/src/Store/ContextMapContainer.ts +98 -2
  132. package/src/Store/Cosmos.ts +273 -207
  133. package/src/Store/Disk.ts +2 -3
  134. package/src/Store/Memory.ts +4 -6
  135. package/src/Store/SQL/Pg.ts +328 -0
  136. package/src/Store/SQL/query.ts +430 -0
  137. package/src/Store/SQL.ts +357 -0
  138. package/src/Store/index.ts +10 -0
  139. package/src/Store/service.ts +16 -7
  140. package/src/adapters/SQL/Model.ts +76 -71
  141. package/src/adapters/ServiceBus.ts +8 -8
  142. package/src/adapters/cosmos-client.ts +2 -2
  143. package/src/adapters/memQueue.ts +2 -2
  144. package/src/adapters/mongo-client.ts +2 -2
  145. package/src/adapters/redis-client.ts +2 -2
  146. package/src/api/ContextProvider.ts +11 -11
  147. package/src/api/internal/events.ts +7 -6
  148. package/src/api/layerUtils.ts +8 -8
  149. package/src/api/routing/middleware/RouterMiddleware.ts +4 -4
  150. package/src/api/routing/middleware/middleware.ts +43 -0
  151. package/src/api/routing/schema/jwt.ts +2 -3
  152. package/src/api/routing.ts +7 -6
  153. package/src/api/setupRequest.ts +27 -7
  154. package/src/errorReporter.ts +1 -1
  155. package/src/fileUtil.ts +1 -1
  156. package/src/rateLimit.ts +2 -2
  157. package/test/contextProvider.test.ts +5 -5
  158. package/test/controller.test.ts +12 -9
  159. package/test/dist/contextProvider.test.d.ts.map +1 -1
  160. package/test/dist/controller.test.d.ts.map +1 -1
  161. package/test/dist/fixtures.d.ts +19 -9
  162. package/test/dist/fixtures.d.ts.map +1 -1
  163. package/test/dist/fixtures.js +11 -9
  164. package/test/dist/query.test.d.ts.map +1 -1
  165. package/test/dist/rawQuery.test.d.ts.map +1 -1
  166. package/test/dist/requires.test.d.ts.map +1 -1
  167. package/test/dist/rpc-multi-middleware.test.d.ts.map +1 -1
  168. package/test/dist/sql-store.test.d.ts.map +1 -0
  169. package/test/fixtures.ts +10 -8
  170. package/test/query.test.ts +160 -14
  171. package/test/rawQuery.test.ts +19 -17
  172. package/test/requires.test.ts +6 -5
  173. package/test/rpc-multi-middleware.test.ts +73 -4
  174. package/test/sql-store.test.ts +776 -0
  175. package/test/validateSample.test.ts +1 -1
  176. package/tsconfig.json +0 -1
@@ -0,0 +1,357 @@
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 { buildWhereSQLQuery, logQuery, type SQLDialect, sqliteDialect } from "./SQL/query.js"
12
+ import { makeETag } from "./utils.js"
13
+
14
+ /** @internal */
15
+ export const parseRow = <Encoded extends FieldValues>(
16
+ row: { id: string; _etag: string | null; data: string },
17
+ idKey: PropertyKey,
18
+ defaultValues: Partial<Encoded>
19
+ ): PersistenceModelType<Encoded> => {
20
+ const data = (typeof row.data === "string" ? JSON.parse(row.data) : row.data) as object
21
+ return { ...defaultValues, ...data, [idKey]: row.id, _etag: row._etag ?? undefined } as PersistenceModelType<Encoded>
22
+ }
23
+
24
+ const parseSelectRow = (
25
+ row: Record<string, unknown>,
26
+ idKey: PropertyKey
27
+ ): any => {
28
+ const result: Record<string, unknown> = {}
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 if (typeof value === "string") {
34
+ try {
35
+ result[key] = JSON.parse(value)
36
+ } catch {
37
+ result[key] = value
38
+ }
39
+ } else {
40
+ result[key] = value
41
+ }
42
+ }
43
+ return result
44
+ }
45
+
46
+ function makeSQLStoreInt(dialect: SQLDialect, jsonColumnType: string) {
47
+ return ({ prefix }: StorageConfig) =>
48
+ Effect.gen(function*() {
49
+ const sql = yield* SqlClient.SqlClient
50
+ return {
51
+ make: <IdKey extends keyof Encoded, Encoded extends FieldValues, R = never, E = never>(
52
+ name: string,
53
+ idKey: IdKey,
54
+ seed?: Effect.Effect<Iterable<Encoded>, E, R>,
55
+ config?: StoreConfig<Encoded>
56
+ ) =>
57
+ Effect.gen(function*() {
58
+ type PM = PersistenceModelType<Encoded>
59
+ const tableName = `${prefix}${name}`
60
+ const defaultValues = config?.defaultValues ?? {}
61
+
62
+ const resolveNamespace = !config?.allowNamespace
63
+ ? Effect.succeed("primary")
64
+ : storeId.asEffect().pipe(Effect.map((namespace) => {
65
+ if (namespace !== "primary" && !config.allowNamespace!(namespace)) {
66
+ throw new Error(`Namespace ${namespace} not allowed!`)
67
+ }
68
+ return namespace
69
+ }))
70
+
71
+ yield* sql
72
+ .unsafe(
73
+ `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))`
74
+ )
75
+ .pipe(Effect.orDie)
76
+
77
+ const toRow = (e: PM) => {
78
+ const newE = makeETag(e)
79
+ const id = newE[idKey] as string
80
+ const { _etag, [idKey]: _id, ...rest } = newE as any
81
+ const data = JSON.stringify(rest)
82
+ return { id, _etag: newE._etag!, data, item: newE }
83
+ }
84
+
85
+ const exec = (query: string, params?: readonly unknown[]) =>
86
+ sql.unsafe(query, params as any).pipe(Effect.orDie)
87
+
88
+ const seedMarkerId = `__seed_marker__`
89
+
90
+ const setInternal = (e: PM, ns: string) =>
91
+ Effect.gen(function*() {
92
+ const row = toRow(e)
93
+ if (e._etag) {
94
+ yield* exec(
95
+ `UPDATE "${tableName}" SET _etag = ?, data = ? WHERE id = ? AND _etag = ? AND _namespace = ?`,
96
+ [row._etag, row.data, row.id, e._etag, ns]
97
+ )
98
+ const existing = yield* exec(
99
+ `SELECT _etag FROM "${tableName}" WHERE id = ? AND _namespace = ?`,
100
+ [row.id, ns]
101
+ )
102
+ const current = (existing as any[])[0]
103
+ if (!current || current._etag !== row._etag) {
104
+ if (current) {
105
+ return yield* new OptimisticConcurrencyException({
106
+ type: name,
107
+ id: row.id,
108
+ current: current._etag,
109
+ found: e._etag,
110
+ code: 412
111
+ })
112
+ }
113
+ return yield* new OptimisticConcurrencyException({
114
+ type: name,
115
+ id: row.id,
116
+ current: "",
117
+ found: e._etag,
118
+ code: 404
119
+ })
120
+ }
121
+ } else {
122
+ yield* exec(
123
+ `INSERT INTO "${tableName}" (id, _namespace, _etag, data) VALUES (?, ?, ?, ?)`,
124
+ [row.id, ns, row._etag, row.data]
125
+ )
126
+ }
127
+ return row.item
128
+ })
129
+
130
+ const bulkSetInternal = (items: NonEmptyReadonlyArray<PM>, ns: string) =>
131
+ sql
132
+ .withTransaction(Effect.forEach(items, (e) => setInternal(e, ns)))
133
+ .pipe(
134
+ Effect.orDie,
135
+ Effect.map((_) => _ as unknown as NonEmptyReadonlyArray<PM>)
136
+ )
137
+
138
+ const ctx = yield* Effect.context<R>()
139
+ const seedCache = new Map<string, Effect.Effect<void>>()
140
+ const makeSeedEffect = (ns: string) =>
141
+ exec(
142
+ `SELECT id FROM "${tableName}" WHERE id = ? AND _namespace = ?`,
143
+ [seedMarkerId, `__seed__::${ns}`]
144
+ )
145
+ .pipe(
146
+ Effect.flatMap((existing) => {
147
+ if ((existing as any[]).length > 0) return Effect.void
148
+ return InfraLogger.logInfo(`Seeding data for ${name} (namespace: ${ns})`).pipe(
149
+ Effect.andThen(seed!),
150
+ Effect.flatMap((items) =>
151
+ Effect.flatMapOption(
152
+ Effect.succeed(toNonEmptyArray([...items])),
153
+ (a) => bulkSetInternal(a, ns)
154
+ )
155
+ ),
156
+ Effect.andThen(
157
+ exec(
158
+ `INSERT INTO "${tableName}" (id, _namespace, _etag, data) VALUES (?, ?, ?, ?)`,
159
+ [seedMarkerId, `__seed__::${ns}`, null, JSON.stringify({ _marker: true })]
160
+ )
161
+ ),
162
+ Effect.provide(ctx),
163
+ Effect.orDie
164
+ )
165
+ })
166
+ )
167
+ const seedNamespace = Effect.fn("seedNamespace")(function*(ns: string) {
168
+ if (!seed) return
169
+ let cached = seedCache.get(ns)
170
+ if (!cached) {
171
+ cached = yield* Effect.cached(makeSeedEffect(ns))
172
+ seedCache.set(ns, cached)
173
+ }
174
+ yield* cached
175
+ })
176
+ const resolveAndSeed = resolveNamespace.pipe(
177
+ Effect.tap((ns) => seedNamespace(ns))
178
+ )
179
+
180
+ const s: Store<IdKey, Encoded> = {
181
+ all: resolveAndSeed.pipe(Effect.flatMap((ns) =>
182
+ exec(`SELECT id, _etag, data FROM "${tableName}" WHERE _namespace = ?`, [ns])
183
+ .pipe(
184
+ Effect.map((rows) => (rows as any[]).map((r) => parseRow<Encoded>(r, idKey, defaultValues))),
185
+ Effect.withSpan("SQL.all [effect-app/infra/Store]", {
186
+ attributes: {
187
+ "repository.table_name": tableName,
188
+ "repository.model_name": name,
189
+ "repository.namespace": ns
190
+ }
191
+ }, { captureStackTrace: false })
192
+ )
193
+ )),
194
+
195
+ find: (id) =>
196
+ resolveAndSeed.pipe(Effect.flatMap((ns) =>
197
+ exec(`SELECT id, _etag, data FROM "${tableName}" WHERE id = ? AND _namespace = ?`, [id, ns])
198
+ .pipe(
199
+ Effect.map((rows) => {
200
+ const row = (rows as any[])[0]
201
+ return row
202
+ ? Option.some(parseRow<Encoded>(row, idKey, defaultValues))
203
+ : Option.none()
204
+ }),
205
+ Effect.withSpan("SQL.find [effect-app/infra/Store]", {
206
+ attributes: { "repository.table_name": tableName, "repository.model_name": name, id }
207
+ }, { captureStackTrace: false })
208
+ )
209
+ )),
210
+
211
+ filter: <U extends keyof Encoded = never>(f: FilterArgs<Encoded, U>) => {
212
+ const filter = f
213
+ .filter
214
+ type M = U extends undefined ? Encoded
215
+ : Pick<Encoded, U>
216
+ return resolveAndSeed
217
+ .pipe(Effect
218
+ .flatMap((ns) =>
219
+ Effect
220
+ .sync(() => {
221
+ const q = buildWhereSQLQuery(
222
+ dialect,
223
+ idKey,
224
+ filter ? [{ t: "where-scope", result: filter, relation: "some" }] : [],
225
+ tableName,
226
+ defaultValues,
227
+ f
228
+ .select as
229
+ | NonEmptyReadonlyArray<string | { key: string; subKeys: readonly string[] }>
230
+ | undefined,
231
+ f
232
+ .order as NonEmptyReadonlyArray<{ key: string; direction: "ASC" | "DESC" }> | undefined,
233
+ f
234
+ .skip,
235
+ f
236
+ .limit
237
+ )
238
+ const hasWhere = q
239
+ .sql
240
+ .includes("WHERE")
241
+ const nsSql = hasWhere
242
+ ? q
243
+ .sql
244
+ .replace("WHERE", `WHERE _namespace = ? AND`)
245
+ : q
246
+ .sql
247
+ .replace(
248
+ `FROM "${tableName}"`,
249
+ `FROM "${tableName}" WHERE _namespace = ?`
250
+ )
251
+ return {
252
+ sql: nsSql,
253
+ params: [
254
+ ns,
255
+ ...q
256
+ .params
257
+ ]
258
+ }
259
+ })
260
+ .pipe(
261
+ Effect
262
+ .tap((q) =>
263
+ logQuery(q)
264
+ ),
265
+ Effect.flatMap((q) =>
266
+ exec(q.sql, q.params).pipe(
267
+ Effect.map((rows) => {
268
+ if (f.select) {
269
+ return (rows as any[]).map((r) => {
270
+ const selected = parseSelectRow(r, idKey)
271
+ return {
272
+ ...Struct.pick(
273
+ defaultValues as any,
274
+ f.select!.filter((_) => typeof _ === "string") as never[]
275
+ ),
276
+ ...selected
277
+ } as M
278
+ })
279
+ }
280
+ return (rows as any[]).map((r) =>
281
+ parseRow<Encoded>(r, idKey, defaultValues) as any as M
282
+ )
283
+ })
284
+ )
285
+ ),
286
+ Effect.withSpan("SQL.filter [effect-app/infra/Store]", {
287
+ attributes: { "repository.table_name": tableName, "repository.model_name": name }
288
+ }, { captureStackTrace: false })
289
+ )
290
+ ))
291
+ },
292
+
293
+ set: (e) =>
294
+ resolveAndSeed.pipe(Effect.flatMap((ns) =>
295
+ setInternal(e, ns).pipe(
296
+ Effect.withSpan("SQL.set [effect-app/infra/Store]", {
297
+ attributes: { "repository.table_name": tableName, "repository.model_name": name, id: e[idKey] }
298
+ }, { captureStackTrace: false })
299
+ )
300
+ )),
301
+
302
+ batchSet: (items) =>
303
+ resolveAndSeed.pipe(Effect.flatMap((ns) =>
304
+ bulkSetInternal(items, ns).pipe(
305
+ Effect.withSpan("SQL.batchSet [effect-app/infra/Store]", {
306
+ attributes: { "repository.table_name": tableName, "repository.model_name": name }
307
+ }, { captureStackTrace: false })
308
+ )
309
+ )),
310
+
311
+ bulkSet: (items) =>
312
+ resolveAndSeed.pipe(Effect.flatMap((ns) =>
313
+ bulkSetInternal(items, ns).pipe(
314
+ Effect.withSpan("SQL.bulkSet [effect-app/infra/Store]", {
315
+ attributes: { "repository.table_name": tableName, "repository.model_name": name }
316
+ }, { captureStackTrace: false })
317
+ )
318
+ )),
319
+
320
+ batchRemove: (ids) => {
321
+ const placeholders = ids.map(() => "?").join(", ")
322
+ return resolveAndSeed.pipe(Effect.flatMap((ns) =>
323
+ exec(
324
+ `DELETE FROM "${tableName}" WHERE id IN (${placeholders}) AND _namespace = ?`,
325
+ [...ids, ns]
326
+ )
327
+ .pipe(
328
+ Effect.asVoid,
329
+ Effect.withSpan("SQL.batchRemove [effect-app/infra/Store]", {
330
+ attributes: { "repository.table_name": tableName, "repository.model_name": name }
331
+ }, { captureStackTrace: false })
332
+ )
333
+ ))
334
+ },
335
+
336
+ queryRaw: (query) =>
337
+ s.all.pipe(
338
+ Effect.map(query.memory),
339
+ Effect.withSpan("SQL.queryRaw [effect-app/infra/Store]", {
340
+ attributes: { "repository.table_name": tableName, "repository.model_name": name }
341
+ }, { captureStackTrace: false })
342
+ )
343
+ }
344
+
345
+ // Eagerly seed primary namespace on initialization
346
+ yield* seedNamespace("primary")
347
+
348
+ return s
349
+ })
350
+ }
351
+ })
352
+ }
353
+
354
+ export function SQLiteStoreLayer(cfg: StorageConfig) {
355
+ return StoreMaker
356
+ .toLayer(makeSQLStoreInt(sqliteDialect, "JSON")(cfg))
357
+ }
@@ -5,6 +5,8 @@ import { DiskStoreLayer } from "./Disk.js"
5
5
  import { MemoryStoreLive } from "./Memory.js"
6
6
  // import { RedisStoreLayer } from "./Redis.js"
7
7
  import type { StorageConfig } from "./service.js"
8
+ import { SQLiteStoreLayer } from "./SQL.js"
9
+ import { PgStoreLayer } from "./SQL/Pg.js"
8
10
 
9
11
  export function StoreMakerLayer(cfg: StorageConfig) {
10
12
  return Effect
@@ -19,6 +21,14 @@ export function StoreMakerLayer(cfg: StorageConfig) {
19
21
  console.log("Using disk store at " + dir)
20
22
  return DiskStoreLayer(cfg, dir)
21
23
  }
24
+ if (storageUrl.startsWith("sqlite://")) {
25
+ console.log("Using SQLite store")
26
+ return SQLiteStoreLayer(cfg)
27
+ }
28
+ if (storageUrl.startsWith("pg://")) {
29
+ console.log("Using PostgreSQL store")
30
+ return PgStoreLayer(cfg)
31
+ }
22
32
  // if (storageUrl.startsWith("redis://")) {
23
33
  // console.log("Using Redis store")
24
34
  // return RedisStoreLayer(cfg)
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
  import type { UniqueKey } from "@azure/cosmos"
3
- import { Effect, type NonEmptyReadonlyArray, type Option, type Redacted, ServiceMap } from "effect-app"
3
+ import { Context, Effect, type NonEmptyReadonlyArray, type Option, type Redacted } from "effect-app"
4
4
  import type { OptimisticConcurrencyException } from "../errors.js"
5
5
  import type { FilterResult } from "../Model/filter/filterApi.js"
6
6
  import type { FieldValues } from "../Model/filter/types.js"
@@ -10,8 +10,8 @@ import { type RawQuery } from "../Model/query.js"
10
10
  export interface StoreConfig<E> {
11
11
  partitionValue: (e?: E) => string
12
12
  /**
13
- * Primarily used for testing, creating namespaces in the database to separate data e.g to run multiple tests in isolation within the same database
14
- * currently only supported in disk/memory. CosmosDB is TODO.
13
+ * Primarily used for testing, creating namespaces in the database to separate data e.g to run multiple tests in isolation within the same database.
14
+ * Memory/Disk use separate store instances per namespace. CosmosDB uses namespace-prefixed partition keys. SQL uses a `_namespace` column.
15
15
  */
16
16
  allowNamespace?: (namespace: string) => boolean
17
17
  /**
@@ -89,7 +89,7 @@ export interface Store<
89
89
  queryRaw: <Out>(query: RawQuery<Encoded, Out>) => Effect.Effect<readonly Out[]>
90
90
  }
91
91
 
92
- export class StoreMaker extends ServiceMap.Opaque<StoreMaker, {
92
+ export class StoreMaker extends Context.Opaque<StoreMaker, {
93
93
  make: <IdKey extends keyof Encoded, Encoded extends FieldValues, R = never, E = never>(
94
94
  name: string,
95
95
  idKey: IdKey,
@@ -161,16 +161,25 @@ export const makeContextMap = () => {
161
161
  // }
162
162
  // }
163
163
 
164
+ const store = new Map<symbol, unknown>()
165
+
164
166
  return {
165
167
  get: getEtag,
166
- set: setEtag
167
- // parserEnv
168
+ set: setEtag,
169
+ getOrCreateStore: <T>(key: symbol, make: () => T): T => {
170
+ let value = store.get(key) as T | undefined
171
+ if (value === undefined) {
172
+ value = make()
173
+ store.set(key, value)
174
+ }
175
+ return value
176
+ }
168
177
  }
169
178
  }
170
179
 
171
180
  const makeMap = Effect.sync(() => makeContextMap())
172
181
 
173
- export class ContextMap extends ServiceMap.Opaque<ContextMap>()("effect-app/ContextMap", { make: makeMap }) {
182
+ export class ContextMap extends Context.Opaque<ContextMap>()("effect-app/ContextMap", { make: makeMap }) {
174
183
  }
175
184
 
176
185
  export type PersistenceModelType<Encoded extends object> = Encoded & {
@@ -7,10 +7,6 @@
7
7
  /**
8
8
  * @since 1.0.0
9
9
  */
10
- import * as VariantSchema from "effect/unstable/schema/VariantSchema"
11
- import { SqlClient } from "effect/unstable/sql/SqlClient"
12
- import * as SqlResolver from "effect/unstable/sql/SqlResolver"
13
- import * as SqlSchema from "effect/unstable/sql/SqlSchema"
14
10
  import crypto from "crypto" // TODO
15
11
  import type { Brand } from "effect/Brand"
16
12
  import * as DateTime from "effect/DateTime"
@@ -24,6 +20,10 @@ import * as Schema from "effect/Schema"
24
20
  import * as Getter from "effect/SchemaGetter"
25
21
  import * as Transformation from "effect/SchemaTransformation"
26
22
  import type { Scope } from "effect/Scope"
23
+ import * as VariantSchema from "effect/unstable/schema/VariantSchema"
24
+ import { SqlClient } from "effect/unstable/sql/SqlClient"
25
+ import * as SqlResolver from "effect/unstable/sql/SqlResolver"
26
+ import * as SqlSchema from "effect/unstable/sql/SqlSchema"
27
27
 
28
28
  const {
29
29
  Class,
@@ -190,14 +190,13 @@ export const Generated = <S extends Schema.Top>(
190
190
  * @since 1.0.0
191
191
  * @category generated
192
192
  */
193
- export interface GeneratedByApp<S extends Schema.Top>
194
- extends
195
- VariantSchema.Field<{
196
- readonly select: S
197
- readonly insert: S
198
- readonly update: S
199
- readonly json: S
200
- }>
193
+ export interface GeneratedByApp<S extends Schema.Top> extends
194
+ VariantSchema.Field<{
195
+ readonly select: S
196
+ readonly insert: S
197
+ readonly update: S
198
+ readonly json: S
199
+ }>
201
200
  {}
202
201
 
203
202
  /**
@@ -303,8 +302,7 @@ export const FieldOption: <Field extends VariantSchema.Field<any> | Schema.Top>(
303
302
  ) => Field extends Schema.Top ? FieldOption<Field>
304
303
  : Field extends VariantSchema.Field<infer S> ? VariantSchema.Field<
305
304
  {
306
- readonly [K in keyof S]: S[K] extends Schema.Top
307
- ? K extends VariantsDatabase ? Schema.OptionFromNullOr<S[K]>
305
+ readonly [K in keyof S]: S[K] extends Schema.Top ? K extends VariantsDatabase ? Schema.OptionFromNullOr<S[K]>
308
306
  : optionalOption<S[K]>
309
307
  : never
310
308
  }
@@ -545,16 +543,15 @@ export const DateTimeUpdateFromNumber: DateTimeUpdateFromNumber = Field({
545
543
  * @since 1.0.0
546
544
  * @category json
547
545
  */
548
- export interface JsonFromString<S extends Schema.Top>
549
- extends
550
- VariantSchema.Field<{
551
- readonly select: Schema.fromJsonString<S>
552
- readonly insert: Schema.fromJsonString<S>
553
- readonly update: Schema.fromJsonString<S>
554
- readonly json: S
555
- readonly jsonCreate: S
556
- readonly jsonUpdate: S
557
- }>
546
+ export interface JsonFromString<S extends Schema.Top> extends
547
+ VariantSchema.Field<{
548
+ readonly select: Schema.fromJsonString<S>
549
+ readonly insert: Schema.fromJsonString<S>
550
+ readonly update: Schema.fromJsonString<S>
551
+ readonly json: S
552
+ readonly jsonCreate: S
553
+ readonly jsonUpdate: S
554
+ }>
558
555
  {}
559
556
 
560
557
  /**
@@ -825,26 +822,28 @@ export const makeDataLoaders = <
825
822
  const idColumn = options.idColumn as string
826
823
  const setMaxBatchSize = options.maxBatchSize ? RequestResolver.batchN(options.maxBatchSize) : identity
827
824
 
828
- const insertResolver = SqlResolver.ordered({
829
- Request: Model.insert,
830
- Result: Model,
831
- execute: (request: any) =>
832
- sql.onDialectOrElse({
833
- mysql: () =>
834
- Effect.forEach(request, (request: any) =>
835
- sql`insert into ${sql(options.tableName)} ${sql.insert(request)};
825
+ const insertResolver = SqlResolver
826
+ .ordered({
827
+ Request: Model.insert,
828
+ Result: Model,
829
+ execute: (request: any) =>
830
+ sql.onDialectOrElse({
831
+ mysql: () =>
832
+ Effect.forEach(request, (request: any) =>
833
+ sql`insert into ${sql(options.tableName)} ${sql.insert(request)};
836
834
  select * from ${sql(options.tableName)} where ${sql(idColumn)} = LAST_INSERT_ID();`
837
- .unprepared
838
- .pipe(
839
- Effect.map(([, results]) => results![0] as any)
840
- ), { concurrency: 10 }),
841
- orElse: () => sql`insert into ${sql(options.tableName)} ${sql.insert(request).returning("*")}`
842
- })
843
- }).pipe(
844
- RequestResolver.setDelay(options.window),
845
- setMaxBatchSize,
846
- RequestResolver.withSpan(`${options.spanPrefix}.insertResolver`)
847
- )
835
+ .unprepared
836
+ .pipe(
837
+ Effect.map(([, results]) => results![0] as any)
838
+ ), { concurrency: 10 }),
839
+ orElse: () => sql`insert into ${sql(options.tableName)} ${sql.insert(request).returning("*")}`
840
+ })
841
+ })
842
+ .pipe(
843
+ RequestResolver.setDelay(options.window),
844
+ setMaxBatchSize,
845
+ RequestResolver.withSpan(`${options.spanPrefix}.insertResolver`)
846
+ )
848
847
  const insertExecute = SqlResolver.request(insertResolver)
849
848
  const insert = (
850
849
  insert: S["insert"]["Type"]
@@ -860,14 +859,16 @@ select * from ${sql(options.tableName)} where ${sql(idColumn)} = LAST_INSERT_ID(
860
859
  })
861
860
  ) as any
862
861
 
863
- const insertVoidResolver = SqlResolver.void({
864
- Request: Model.insert,
865
- execute: (request: any) => sql`insert into ${sql(options.tableName)} ${sql.insert(request)}`
866
- }).pipe(
867
- RequestResolver.setDelay(options.window),
868
- setMaxBatchSize,
869
- RequestResolver.withSpan(`${options.spanPrefix}.insertVoidResolver`)
870
- )
862
+ const insertVoidResolver = SqlResolver
863
+ .void({
864
+ Request: Model.insert,
865
+ execute: (request: any) => sql`insert into ${sql(options.tableName)} ${sql.insert(request)}`
866
+ })
867
+ .pipe(
868
+ RequestResolver.setDelay(options.window),
869
+ setMaxBatchSize,
870
+ RequestResolver.withSpan(`${options.spanPrefix}.insertVoidResolver`)
871
+ )
871
872
  const insertVoidExecute = SqlResolver.request(insertVoidResolver)
872
873
  const insertVoid = (
873
874
  insert: S["insert"]["Type"]
@@ -878,18 +879,20 @@ select * from ${sql(options.tableName)} where ${sql(idColumn)} = LAST_INSERT_ID(
878
879
  })
879
880
  ) as any
880
881
 
881
- const findByIdResolver = SqlResolver.findById({
882
- Id: idSchema,
883
- Result: Model,
884
- ResultId(request: any) {
885
- return request[idColumn]
886
- },
887
- execute: (ids: any) => sql`select * from ${sql(options.tableName)} where ${sql.in(idColumn, ids)}`
888
- }).pipe(
889
- RequestResolver.setDelay(options.window),
890
- setMaxBatchSize,
891
- RequestResolver.withSpan(`${options.spanPrefix}.findByIdResolver`)
892
- )
882
+ const findByIdResolver = SqlResolver
883
+ .findById({
884
+ Id: idSchema,
885
+ Result: Model,
886
+ ResultId(request: any) {
887
+ return request[idColumn]
888
+ },
889
+ execute: (ids: any) => sql`select * from ${sql(options.tableName)} where ${sql.in(idColumn, ids)}`
890
+ })
891
+ .pipe(
892
+ RequestResolver.setDelay(options.window),
893
+ setMaxBatchSize,
894
+ RequestResolver.withSpan(`${options.spanPrefix}.findByIdResolver`)
895
+ )
893
896
  const findByIdExecute = SqlResolver.request(findByIdResolver)
894
897
  const findById = (
895
898
  id: S["fields"][Id]["Type"]
@@ -904,14 +907,16 @@ select * from ${sql(options.tableName)} where ${sql(idColumn)} = LAST_INSERT_ID(
904
907
  })
905
908
  ) as any
906
909
 
907
- const deleteResolver = SqlResolver.void({
908
- Request: idSchema,
909
- execute: (ids: any) => sql`delete from ${sql(options.tableName)} where ${sql.in(idColumn, ids)}`
910
- }).pipe(
911
- RequestResolver.setDelay(options.window),
912
- setMaxBatchSize,
913
- RequestResolver.withSpan(`${options.spanPrefix}.deleteResolver`)
914
- )
910
+ const deleteResolver = SqlResolver
911
+ .void({
912
+ Request: idSchema,
913
+ execute: (ids: any) => sql`delete from ${sql(options.tableName)} where ${sql.in(idColumn, ids)}`
914
+ })
915
+ .pipe(
916
+ RequestResolver.setDelay(options.window),
917
+ setMaxBatchSize,
918
+ RequestResolver.withSpan(`${options.spanPrefix}.deleteResolver`)
919
+ )
915
920
  const deleteExecute = SqlResolver.request(deleteResolver)
916
921
  const delete_ = (
917
922
  id: S["fields"][Id]["Type"]