@effect-app/infra 4.0.0-beta.81 → 4.0.0-beta.82
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.
- package/CHANGELOG.md +13 -0
- package/dist/Store/Cosmos.d.ts.map +1 -1
- package/dist/Store/Cosmos.js +55 -32
- package/dist/Store/SQL/Pg.d.ts +4 -0
- package/dist/Store/SQL/Pg.d.ts.map +1 -0
- package/dist/Store/SQL/Pg.js +174 -0
- package/dist/Store/SQL/query.d.ts +34 -0
- package/dist/Store/SQL/query.d.ts.map +1 -0
- package/dist/Store/SQL/query.js +326 -0
- package/dist/Store/SQL.d.ts +4 -0
- package/dist/Store/SQL.d.ts.map +1 -0
- package/dist/Store/SQL.js +203 -0
- package/dist/Store/index.d.ts +1 -1
- package/dist/Store/index.d.ts.map +1 -1
- package/dist/Store/index.js +11 -1
- package/dist/Store/service.d.ts +2 -2
- package/dist/api/routing/middleware/middleware.d.ts +35 -1
- package/dist/api/routing/middleware/middleware.d.ts.map +1 -1
- package/dist/api/routing/middleware/middleware.js +39 -1
- package/dist/api/setupRequest.d.ts +6 -3
- package/dist/api/setupRequest.d.ts.map +1 -1
- package/dist/api/setupRequest.js +11 -6
- package/package.json +19 -5
- package/src/Store/Cosmos.ts +200 -165
- package/src/Store/SQL/Pg.ts +294 -0
- package/src/Store/SQL/query.ts +372 -0
- package/src/Store/SQL.ts +327 -0
- package/src/Store/index.ts +10 -0
- package/src/Store/service.ts +2 -2
- package/src/api/routing/middleware/middleware.ts +43 -0
- package/src/api/setupRequest.ts +24 -4
- package/test/dist/sql-store.test.d.ts.map +1 -0
- package/test/sql-store.test.ts +444 -0
package/src/Store/SQL.ts
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
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
|
+
const parseRow = <Encoded extends FieldValues>(
|
|
15
|
+
row: { id: string; _etag: string | null; data: string },
|
|
16
|
+
defaultValues: Partial<Encoded>
|
|
17
|
+
): PersistenceModelType<Encoded> => {
|
|
18
|
+
const data = (typeof row.data === "string" ? JSON.parse(row.data) : row.data) as object
|
|
19
|
+
return { ...defaultValues, ...data, _etag: row._etag ?? undefined } as PersistenceModelType<Encoded>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const parseSelectRow = <Encoded extends FieldValues>(
|
|
23
|
+
row: Record<string, unknown>,
|
|
24
|
+
idKey: PropertyKey,
|
|
25
|
+
defaultValues: Partial<Encoded>
|
|
26
|
+
): any => {
|
|
27
|
+
const result: Record<string, unknown> = { ...defaultValues }
|
|
28
|
+
for (const [key, value] of Object.entries(row)) {
|
|
29
|
+
if (key === "id") {
|
|
30
|
+
result[idKey as string] = value
|
|
31
|
+
result["id"] = value
|
|
32
|
+
} else if (typeof value === "string") {
|
|
33
|
+
try {
|
|
34
|
+
result[key] = JSON.parse(value)
|
|
35
|
+
} catch {
|
|
36
|
+
result[key] = value
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
result[key] = value
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return result
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function makeSQLStoreInt(dialect: SQLDialect, jsonColumnType: string) {
|
|
46
|
+
return ({ prefix }: StorageConfig) =>
|
|
47
|
+
Effect.gen(function*() {
|
|
48
|
+
const sql = yield* SqlClient.SqlClient
|
|
49
|
+
return {
|
|
50
|
+
make: <IdKey extends keyof Encoded, Encoded extends FieldValues, R = never, E = never>(
|
|
51
|
+
name: string,
|
|
52
|
+
idKey: IdKey,
|
|
53
|
+
seed?: Effect.Effect<Iterable<Encoded>, E, R>,
|
|
54
|
+
config?: StoreConfig<Encoded>
|
|
55
|
+
) =>
|
|
56
|
+
Effect.gen(function*() {
|
|
57
|
+
type PM = PersistenceModelType<Encoded>
|
|
58
|
+
const tableName = `${prefix}${name}`
|
|
59
|
+
const defaultValues = config?.defaultValues ?? {}
|
|
60
|
+
|
|
61
|
+
const resolveNamespace = !config?.allowNamespace
|
|
62
|
+
? Effect.succeed("primary")
|
|
63
|
+
: storeId.asEffect().pipe(Effect.map((namespace) => {
|
|
64
|
+
if (namespace !== "primary" && !config.allowNamespace!(namespace)) {
|
|
65
|
+
throw new Error(`Namespace ${namespace} not allowed!`)
|
|
66
|
+
}
|
|
67
|
+
return namespace
|
|
68
|
+
}))
|
|
69
|
+
|
|
70
|
+
yield* sql
|
|
71
|
+
.unsafe(
|
|
72
|
+
`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))`
|
|
73
|
+
)
|
|
74
|
+
.pipe(Effect.orDie)
|
|
75
|
+
|
|
76
|
+
const toRow = (e: PM) => {
|
|
77
|
+
const newE = makeETag(e)
|
|
78
|
+
const id = newE[idKey] as string
|
|
79
|
+
const data = JSON.stringify(newE)
|
|
80
|
+
return { id, _etag: newE._etag!, data, item: newE }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const exec = (query: string, params?: readonly unknown[]) =>
|
|
84
|
+
sql.unsafe(query, params as any).pipe(Effect.orDie)
|
|
85
|
+
|
|
86
|
+
const s: Store<IdKey, Encoded> = {
|
|
87
|
+
all: resolveNamespace.pipe(Effect.flatMap((ns) =>
|
|
88
|
+
exec(`SELECT id, _etag, data FROM "${tableName}" WHERE _namespace = ?`, [ns])
|
|
89
|
+
.pipe(
|
|
90
|
+
Effect.map((rows) => (rows as any[]).map((r) => parseRow<Encoded>(r, defaultValues))),
|
|
91
|
+
Effect.withSpan("SQL.all [effect-app/infra/Store]", {
|
|
92
|
+
attributes: {
|
|
93
|
+
"repository.table_name": tableName,
|
|
94
|
+
"repository.model_name": name,
|
|
95
|
+
"repository.namespace": ns
|
|
96
|
+
}
|
|
97
|
+
}, { captureStackTrace: false })
|
|
98
|
+
)
|
|
99
|
+
)),
|
|
100
|
+
|
|
101
|
+
find: (id) =>
|
|
102
|
+
resolveNamespace.pipe(Effect.flatMap((ns) =>
|
|
103
|
+
exec(`SELECT id, _etag, data FROM "${tableName}" WHERE id = ? AND _namespace = ?`, [id, ns])
|
|
104
|
+
.pipe(
|
|
105
|
+
Effect.map((rows) => {
|
|
106
|
+
const row = (rows as any[])[0]
|
|
107
|
+
return row
|
|
108
|
+
? Option.some(parseRow<Encoded>(row, defaultValues))
|
|
109
|
+
: Option.none()
|
|
110
|
+
}),
|
|
111
|
+
Effect.withSpan("SQL.find [effect-app/infra/Store]", {
|
|
112
|
+
attributes: { "repository.table_name": tableName, "repository.model_name": name, id }
|
|
113
|
+
}, { captureStackTrace: false })
|
|
114
|
+
)
|
|
115
|
+
)),
|
|
116
|
+
|
|
117
|
+
filter: <U extends keyof Encoded = never>(f: FilterArgs<Encoded, U>) => {
|
|
118
|
+
const filter = f
|
|
119
|
+
.filter
|
|
120
|
+
type M = U extends undefined ? Encoded
|
|
121
|
+
: Pick<Encoded, U>
|
|
122
|
+
return resolveNamespace
|
|
123
|
+
.pipe(Effect
|
|
124
|
+
.flatMap((ns) =>
|
|
125
|
+
Effect
|
|
126
|
+
.sync(() => {
|
|
127
|
+
const q = buildWhereSQLQuery(
|
|
128
|
+
dialect,
|
|
129
|
+
idKey,
|
|
130
|
+
filter ? [{ t: "where-scope", result: filter, relation: "some" }] : [],
|
|
131
|
+
tableName,
|
|
132
|
+
defaultValues,
|
|
133
|
+
f
|
|
134
|
+
.select as
|
|
135
|
+
| NonEmptyReadonlyArray<string | { key: string; subKeys: readonly string[] }>
|
|
136
|
+
| undefined,
|
|
137
|
+
f
|
|
138
|
+
.order as NonEmptyReadonlyArray<{ key: string; direction: "ASC" | "DESC" }> | undefined,
|
|
139
|
+
f
|
|
140
|
+
.skip,
|
|
141
|
+
f
|
|
142
|
+
.limit
|
|
143
|
+
)
|
|
144
|
+
const nsPlaceholder = dialect
|
|
145
|
+
.placeholder(
|
|
146
|
+
q
|
|
147
|
+
.params
|
|
148
|
+
.length + 1
|
|
149
|
+
)
|
|
150
|
+
const hasWhere = q
|
|
151
|
+
.sql
|
|
152
|
+
.includes("WHERE")
|
|
153
|
+
const nsSql = hasWhere
|
|
154
|
+
? q
|
|
155
|
+
.sql
|
|
156
|
+
.replace("WHERE", `WHERE _namespace = ${nsPlaceholder} AND`)
|
|
157
|
+
: q
|
|
158
|
+
.sql
|
|
159
|
+
.replace(
|
|
160
|
+
`FROM "${tableName}"`,
|
|
161
|
+
`FROM "${tableName}" WHERE _namespace = ${nsPlaceholder}`
|
|
162
|
+
)
|
|
163
|
+
return {
|
|
164
|
+
sql: nsSql,
|
|
165
|
+
params: [
|
|
166
|
+
...q
|
|
167
|
+
.params,
|
|
168
|
+
ns
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
.pipe(
|
|
173
|
+
Effect
|
|
174
|
+
.tap((q) =>
|
|
175
|
+
logQuery(q)
|
|
176
|
+
),
|
|
177
|
+
Effect.flatMap((q) =>
|
|
178
|
+
exec(q.sql, q.params).pipe(
|
|
179
|
+
Effect.map((rows) => {
|
|
180
|
+
if (f.select) {
|
|
181
|
+
return (rows as any[]).map((r) => {
|
|
182
|
+
const selected = parseSelectRow(r, idKey, {})
|
|
183
|
+
return {
|
|
184
|
+
...Struct.pick(
|
|
185
|
+
defaultValues as any,
|
|
186
|
+
f.select!.filter((_) => typeof _ === "string") as never[]
|
|
187
|
+
),
|
|
188
|
+
...selected
|
|
189
|
+
} as M
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
return (rows as any[]).map((r) => parseRow<Encoded>(r, defaultValues) as any as M)
|
|
193
|
+
})
|
|
194
|
+
)
|
|
195
|
+
),
|
|
196
|
+
Effect.withSpan("SQL.filter [effect-app/infra/Store]", {
|
|
197
|
+
attributes: { "repository.table_name": tableName, "repository.model_name": name }
|
|
198
|
+
}, { captureStackTrace: false })
|
|
199
|
+
)
|
|
200
|
+
))
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
set: (e) =>
|
|
204
|
+
resolveNamespace.pipe(Effect.flatMap((ns) =>
|
|
205
|
+
Effect
|
|
206
|
+
.gen(function*() {
|
|
207
|
+
const row = toRow(e)
|
|
208
|
+
if (e._etag) {
|
|
209
|
+
yield* exec(
|
|
210
|
+
`UPDATE "${tableName}" SET _etag = ?, data = ? WHERE id = ? AND _etag = ? AND _namespace = ?`,
|
|
211
|
+
[row._etag, row.data, row.id, e._etag, ns]
|
|
212
|
+
)
|
|
213
|
+
const existing = yield* exec(
|
|
214
|
+
`SELECT _etag FROM "${tableName}" WHERE id = ? AND _namespace = ?`,
|
|
215
|
+
[row.id, ns]
|
|
216
|
+
)
|
|
217
|
+
const current = (existing as any[])[0]
|
|
218
|
+
if (!current || current._etag !== row._etag) {
|
|
219
|
+
if (current) {
|
|
220
|
+
return yield* new OptimisticConcurrencyException({
|
|
221
|
+
type: name,
|
|
222
|
+
id: row.id,
|
|
223
|
+
current: current._etag,
|
|
224
|
+
found: e._etag,
|
|
225
|
+
code: 412
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
return yield* new OptimisticConcurrencyException({
|
|
229
|
+
type: name,
|
|
230
|
+
id: row.id,
|
|
231
|
+
current: "",
|
|
232
|
+
found: e._etag,
|
|
233
|
+
code: 404
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
yield* exec(
|
|
238
|
+
`INSERT INTO "${tableName}" (id, _namespace, _etag, data) VALUES (?, ?, ?, ?)`,
|
|
239
|
+
[row.id, ns, row._etag, row.data]
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
return row.item
|
|
243
|
+
})
|
|
244
|
+
.pipe(
|
|
245
|
+
Effect.withSpan("SQL.set [effect-app/infra/Store]", {
|
|
246
|
+
attributes: { "repository.table_name": tableName, "repository.model_name": name, id: e[idKey] }
|
|
247
|
+
}, { captureStackTrace: false })
|
|
248
|
+
)
|
|
249
|
+
)),
|
|
250
|
+
|
|
251
|
+
batchSet: (items) =>
|
|
252
|
+
sql
|
|
253
|
+
.withTransaction(
|
|
254
|
+
Effect.forEach(items, (e) => s.set(e))
|
|
255
|
+
)
|
|
256
|
+
.pipe(
|
|
257
|
+
Effect.orDie,
|
|
258
|
+
Effect.map((_) => _ as unknown as NonEmptyReadonlyArray<PM>),
|
|
259
|
+
Effect.withSpan("SQL.batchSet [effect-app/infra/Store]", {
|
|
260
|
+
attributes: { "repository.table_name": tableName, "repository.model_name": name }
|
|
261
|
+
}, { captureStackTrace: false })
|
|
262
|
+
),
|
|
263
|
+
|
|
264
|
+
bulkSet: (items) =>
|
|
265
|
+
sql
|
|
266
|
+
.withTransaction(
|
|
267
|
+
Effect.forEach(items, (e) => s.set(e))
|
|
268
|
+
)
|
|
269
|
+
.pipe(
|
|
270
|
+
Effect.orDie,
|
|
271
|
+
Effect.map((_) => _ as unknown as NonEmptyReadonlyArray<PM>),
|
|
272
|
+
Effect.withSpan("SQL.bulkSet [effect-app/infra/Store]", {
|
|
273
|
+
attributes: { "repository.table_name": tableName, "repository.model_name": name }
|
|
274
|
+
}, { captureStackTrace: false })
|
|
275
|
+
),
|
|
276
|
+
|
|
277
|
+
batchRemove: (ids) => {
|
|
278
|
+
const placeholders = ids.map(() => "?").join(", ")
|
|
279
|
+
return resolveNamespace.pipe(Effect.flatMap((ns) =>
|
|
280
|
+
exec(
|
|
281
|
+
`DELETE FROM "${tableName}" WHERE id IN (${placeholders}) AND _namespace = ?`,
|
|
282
|
+
[...ids, ns]
|
|
283
|
+
)
|
|
284
|
+
.pipe(
|
|
285
|
+
Effect.asVoid,
|
|
286
|
+
Effect.withSpan("SQL.batchRemove [effect-app/infra/Store]", {
|
|
287
|
+
attributes: { "repository.table_name": tableName, "repository.model_name": name }
|
|
288
|
+
}, { captureStackTrace: false })
|
|
289
|
+
)
|
|
290
|
+
))
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
queryRaw: (query) =>
|
|
294
|
+
s.all.pipe(
|
|
295
|
+
Effect.map(query.memory),
|
|
296
|
+
Effect.withSpan("SQL.queryRaw [effect-app/infra/Store]", {
|
|
297
|
+
attributes: { "repository.table_name": tableName, "repository.model_name": name }
|
|
298
|
+
}, { captureStackTrace: false })
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (seed) {
|
|
303
|
+
const existing = yield* exec(
|
|
304
|
+
`SELECT COUNT(*) as cnt FROM "${tableName}" WHERE _namespace = ?`,
|
|
305
|
+
["primary"]
|
|
306
|
+
)
|
|
307
|
+
const count = (existing as any[])[0]?.cnt ?? 0
|
|
308
|
+
if (count === 0) {
|
|
309
|
+
yield* InfraLogger.logInfo("Seeding data for " + name)
|
|
310
|
+
const items = yield* seed
|
|
311
|
+
yield* Effect.flatMapOption(
|
|
312
|
+
Effect.succeed(toNonEmptyArray([...items])),
|
|
313
|
+
(a) => s.bulkSet(a).pipe(Effect.orDie)
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return s
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
})
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function SQLiteStoreLayer(cfg: StorageConfig) {
|
|
325
|
+
return StoreMaker
|
|
326
|
+
.toLayer(makeSQLStoreInt(sqliteDialect, "JSON")(cfg))
|
|
327
|
+
}
|
package/src/Store/index.ts
CHANGED
|
@@ -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)
|
package/src/Store/service.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
/**
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import { Cause, Config, Effect, Layer, Schema } from "effect"
|
|
3
3
|
import { ConfigureInterruptibilityMiddleware, DevMode, DevModeMiddleware, LoggerMiddleware, RequestCacheMiddleware } from "effect-app/middleware"
|
|
4
|
+
import { RpcContextMap, type RpcMiddleware } from "effect-app/rpc"
|
|
4
5
|
import { pretty } from "effect-app/utils"
|
|
6
|
+
import { type Rpc } from "effect/unstable/rpc"
|
|
7
|
+
import { SqlClient } from "effect/unstable/sql"
|
|
5
8
|
import { logError, reportError } from "../../../errorReporter.js"
|
|
6
9
|
import { InfraLogger } from "../../../logger.js"
|
|
7
10
|
import { determineMethod, isCommand } from "../utils.js"
|
|
@@ -126,3 +129,43 @@ export const DefaultGenericMiddlewaresLive = Layer.mergeAll(
|
|
|
126
129
|
LoggerMiddlewareLive,
|
|
127
130
|
DevModeMiddlewareLive
|
|
128
131
|
)
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Config entry for `RequestContextMap` that controls per-RPC transaction wrapping.
|
|
135
|
+
* Defaults to `false` (no transaction). Set `requiresTransaction: true` on a route to enable.
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```ts
|
|
139
|
+
* class RequestContextMap extends RpcContextMap.makeMap({
|
|
140
|
+
* requiresTransaction: requiresTransactionConfig,
|
|
141
|
+
* // ...
|
|
142
|
+
* }) {}
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
export const requiresTransactionConfig = RpcContextMap.makeCustom()(Schema.Never, false as boolean)
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Creates the middleware Effect for SQL transaction wrapping.
|
|
149
|
+
* Requires `SqlClient` directly (not via serviceOption).
|
|
150
|
+
* Reads `requiresTransaction` from the RPC config; defaults to `false`.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```ts
|
|
154
|
+
* const SqlTransactionMiddlewareLive = Layer.effect(
|
|
155
|
+
* SqlTransactionMiddleware,
|
|
156
|
+
* makeSqlTransactionMiddleware(RequestContextMap)
|
|
157
|
+
* )
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
export const makeSqlTransactionMiddleware = (
|
|
161
|
+
rcm: { getConfig: (rpc: Rpc.AnyWithProps) => { readonly requiresTransaction?: boolean } }
|
|
162
|
+
) =>
|
|
163
|
+
Effect.gen(function*() {
|
|
164
|
+
const sql = yield* SqlClient.SqlClient
|
|
165
|
+
const mw: RpcMiddleware.RpcMiddlewareV4<never, never, never> = (effect, { rpc }) => {
|
|
166
|
+
const { requiresTransaction } = rcm.getConfig(rpc)
|
|
167
|
+
if (requiresTransaction !== true) return effect
|
|
168
|
+
return sql.withTransaction(effect).pipe(Effect.orDie)
|
|
169
|
+
}
|
|
170
|
+
return mw
|
|
171
|
+
})
|
package/src/api/setupRequest.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import { Effect, Layer, Tracer } from "effect-app"
|
|
1
|
+
import { Effect, Layer, Option, Tracer } from "effect-app"
|
|
2
2
|
import { NonEmptyString255 } from "effect-app/Schema"
|
|
3
|
+
import { SqlClient } from "effect/unstable/sql"
|
|
3
4
|
import { LocaleRef, RequestContext, spanAttributes } from "../RequestContext.js"
|
|
4
5
|
import { ContextMapContainer } from "../Store/ContextMapContainer.js"
|
|
5
6
|
import { storeId } from "../Store/Memory.js"
|
|
6
7
|
|
|
8
|
+
const withSqlTransaction = <R, E, A>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
|
|
9
|
+
Effect.serviceOption(SqlClient.SqlClient).pipe(
|
|
10
|
+
Effect.flatMap(Option.match({
|
|
11
|
+
onNone: () => self,
|
|
12
|
+
onSome: (sql) => sql.withTransaction(self).pipe(Effect.orDie)
|
|
13
|
+
}))
|
|
14
|
+
)
|
|
15
|
+
|
|
7
16
|
export const getRequestContext = Effect
|
|
8
17
|
.all({
|
|
9
18
|
span: Effect.currentSpan.pipe(Effect.orDie),
|
|
@@ -43,16 +52,25 @@ const withRequestSpan = (name = "request", options?: Tracer.SpanOptions) => <R,
|
|
|
43
52
|
)
|
|
44
53
|
)
|
|
45
54
|
|
|
55
|
+
export interface SetupRequestOptions {
|
|
56
|
+
readonly withTransaction?: boolean
|
|
57
|
+
}
|
|
58
|
+
|
|
46
59
|
export const setupRequestContextFromCurrent =
|
|
47
|
-
(name = "request", options?: Tracer.SpanOptions) => <R, E, A>(self: Effect.Effect<A, E, R>) =>
|
|
60
|
+
(name = "request", options?: Tracer.SpanOptions & SetupRequestOptions) => <R, E, A>(self: Effect.Effect<A, E, R>) =>
|
|
48
61
|
self
|
|
49
62
|
.pipe(
|
|
63
|
+
options?.withTransaction === true ? withSqlTransaction : (_) => _,
|
|
50
64
|
withRequestSpan(name, options),
|
|
51
65
|
Effect.provide(ContextMapContainer.layer, { local: true })
|
|
52
66
|
)
|
|
53
67
|
|
|
54
68
|
// TODO: consider integrating Effect.withParentSpan
|
|
55
|
-
export function setupRequestContext<R, E, A>(
|
|
69
|
+
export function setupRequestContext<R, E, A>(
|
|
70
|
+
self: Effect.Effect<A, E, R>,
|
|
71
|
+
requestContext: RequestContext,
|
|
72
|
+
options?: SetupRequestOptions
|
|
73
|
+
) {
|
|
56
74
|
const layer = Layer.mergeAll(
|
|
57
75
|
ContextMapContainer.layer,
|
|
58
76
|
Layer.succeed(LocaleRef, requestContext.locale),
|
|
@@ -60,6 +78,7 @@ export function setupRequestContext<R, E, A>(self: Effect.Effect<A, E, R>, reque
|
|
|
60
78
|
)
|
|
61
79
|
return self
|
|
62
80
|
.pipe(
|
|
81
|
+
options?.withTransaction === true ? withSqlTransaction : (_) => _,
|
|
63
82
|
withRequestSpan(requestContext.name),
|
|
64
83
|
Effect.provide(layer, { local: true })
|
|
65
84
|
)
|
|
@@ -69,7 +88,7 @@ export function setupRequestContextWithCustomSpan<R, E, A>(
|
|
|
69
88
|
self: Effect.Effect<A, E, R>,
|
|
70
89
|
requestContext: RequestContext,
|
|
71
90
|
name: string,
|
|
72
|
-
options?: Tracer.SpanOptions
|
|
91
|
+
options?: Tracer.SpanOptions & SetupRequestOptions
|
|
73
92
|
) {
|
|
74
93
|
const layer = Layer.mergeAll(
|
|
75
94
|
ContextMapContainer.layer,
|
|
@@ -78,6 +97,7 @@ export function setupRequestContextWithCustomSpan<R, E, A>(
|
|
|
78
97
|
)
|
|
79
98
|
return self
|
|
80
99
|
.pipe(
|
|
100
|
+
options?.withTransaction === true ? withSqlTransaction : (_) => _,
|
|
81
101
|
withRequestSpan(name, options),
|
|
82
102
|
Effect.provide(layer, { local: true })
|
|
83
103
|
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql-store.test.d.ts","sourceRoot":"","sources":["../sql-store.test.ts"],"names":[],"mappings":""}
|