@effect-app/infra 4.0.0-beta.1 → 4.0.0-beta.100

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