@effect-app/infra 4.0.0-beta.8 → 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 +540 -0
- package/dist/CUPS.d.ts +3 -3
- package/dist/CUPS.d.ts.map +1 -1
- package/dist/CUPS.js +3 -3
- package/dist/Emailer/Sendgrid.js +1 -1
- package/dist/Emailer/service.d.ts +3 -3
- package/dist/Emailer/service.d.ts.map +1 -1
- package/dist/Emailer/service.js +3 -3
- package/dist/MainFiberSet.d.ts +2 -2
- package/dist/MainFiberSet.d.ts.map +1 -1
- package/dist/MainFiberSet.js +3 -3
- package/dist/Model/Repository/internal/internal.d.ts +3 -3
- package/dist/Model/Repository/internal/internal.d.ts.map +1 -1
- package/dist/Model/Repository/internal/internal.js +11 -7
- package/dist/Model/Repository/makeRepo.d.ts +2 -2
- package/dist/Model/Repository/makeRepo.d.ts.map +1 -1
- package/dist/Model/Repository/makeRepo.js +1 -1
- package/dist/Model/Repository/validation.d.ts +5 -4
- package/dist/Model/Repository/validation.d.ts.map +1 -1
- package/dist/Model/query/dsl.d.ts +9 -9
- package/dist/Operations.d.ts +2 -2
- package/dist/Operations.d.ts.map +1 -1
- package/dist/Operations.js +3 -3
- package/dist/OperationsRepo.d.ts +2 -2
- package/dist/OperationsRepo.d.ts.map +1 -1
- package/dist/OperationsRepo.js +3 -3
- package/dist/QueueMaker/SQLQueue.d.ts +3 -5
- package/dist/QueueMaker/SQLQueue.d.ts.map +1 -1
- package/dist/QueueMaker/SQLQueue.js +9 -7
- package/dist/QueueMaker/errors.d.ts +1 -1
- package/dist/QueueMaker/errors.d.ts.map +1 -1
- package/dist/QueueMaker/memQueue.d.ts.map +1 -1
- package/dist/QueueMaker/memQueue.js +10 -9
- package/dist/QueueMaker/sbqueue.d.ts.map +1 -1
- package/dist/QueueMaker/sbqueue.js +11 -9
- package/dist/RequestContext.d.ts +19 -14
- package/dist/RequestContext.d.ts.map +1 -1
- package/dist/RequestContext.js +5 -5
- package/dist/RequestFiberSet.d.ts +2 -2
- package/dist/RequestFiberSet.d.ts.map +1 -1
- package/dist/RequestFiberSet.js +5 -5
- package/dist/Store/ContextMapContainer.d.ts +14 -3
- package/dist/Store/ContextMapContainer.d.ts.map +1 -1
- package/dist/Store/ContextMapContainer.js +64 -3
- package/dist/Store/Cosmos.d.ts.map +1 -1
- package/dist/Store/Cosmos.js +55 -32
- package/dist/Store/Disk.d.ts.map +1 -1
- package/dist/Store/Disk.js +3 -4
- package/dist/Store/Memory.d.ts +2 -2
- package/dist/Store/Memory.d.ts.map +1 -1
- package/dist/Store/Memory.js +4 -4
- 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 +8 -5
- package/dist/Store/service.d.ts.map +1 -1
- package/dist/Store/service.js +14 -6
- package/dist/adapters/SQL/Model.d.ts +2 -5
- package/dist/adapters/SQL/Model.d.ts.map +1 -1
- package/dist/adapters/SQL/Model.js +21 -13
- package/dist/adapters/ServiceBus.d.ts +6 -6
- package/dist/adapters/ServiceBus.d.ts.map +1 -1
- package/dist/adapters/ServiceBus.js +9 -9
- package/dist/adapters/cosmos-client.d.ts +2 -2
- package/dist/adapters/cosmos-client.d.ts.map +1 -1
- package/dist/adapters/cosmos-client.js +3 -3
- package/dist/adapters/logger.d.ts.map +1 -1
- package/dist/adapters/memQueue.d.ts +2 -2
- package/dist/adapters/memQueue.d.ts.map +1 -1
- package/dist/adapters/memQueue.js +3 -3
- package/dist/adapters/mongo-client.d.ts +2 -2
- package/dist/adapters/mongo-client.d.ts.map +1 -1
- package/dist/adapters/mongo-client.js +3 -3
- package/dist/adapters/redis-client.d.ts +3 -3
- package/dist/adapters/redis-client.d.ts.map +1 -1
- package/dist/adapters/redis-client.js +3 -3
- package/dist/api/ContextProvider.d.ts +6 -6
- package/dist/api/ContextProvider.d.ts.map +1 -1
- package/dist/api/ContextProvider.js +6 -6
- package/dist/api/internal/RequestContextMiddleware.d.ts +1 -1
- package/dist/api/internal/auth.d.ts +1 -1
- package/dist/api/internal/events.d.ts +2 -2
- package/dist/api/internal/events.d.ts.map +1 -1
- package/dist/api/internal/events.js +7 -5
- package/dist/api/layerUtils.d.ts +5 -5
- package/dist/api/layerUtils.d.ts.map +1 -1
- package/dist/api/layerUtils.js +5 -5
- package/dist/api/routing/middleware/RouterMiddleware.d.ts +3 -3
- package/dist/api/routing/middleware/RouterMiddleware.d.ts.map +1 -1
- 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/routing/schema/jwt.d.ts +1 -1
- package/dist/api/routing/schema/jwt.d.ts.map +1 -1
- package/dist/api/routing/schema/jwt.js +1 -1
- package/dist/api/routing.d.ts +1 -5
- package/dist/api/routing.d.ts.map +1 -1
- package/dist/api/routing.js +3 -2
- 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/dist/errorReporter.d.ts +1 -1
- package/dist/errorReporter.d.ts.map +1 -1
- package/dist/errorReporter.js +1 -1
- package/dist/fileUtil.js +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/rateLimit.js +1 -1
- package/examples/query.ts +29 -25
- package/package.json +32 -18
- package/src/CUPS.ts +2 -2
- package/src/Emailer/Sendgrid.ts +1 -1
- package/src/Emailer/service.ts +2 -2
- package/src/MainFiberSet.ts +2 -2
- package/src/Model/Repository/internal/internal.ts +11 -8
- package/src/Model/Repository/makeRepo.ts +2 -2
- package/src/Operations.ts +2 -2
- package/src/OperationsRepo.ts +2 -2
- package/src/QueueMaker/SQLQueue.ts +10 -10
- package/src/QueueMaker/memQueue.ts +41 -42
- package/src/QueueMaker/sbqueue.ts +65 -62
- package/src/RequestContext.ts +4 -4
- package/src/RequestFiberSet.ts +4 -4
- package/src/Store/ContextMapContainer.ts +98 -2
- package/src/Store/Cosmos.ts +207 -172
- package/src/Store/Disk.ts +2 -3
- package/src/Store/Memory.ts +4 -6
- 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 +16 -7
- package/src/adapters/SQL/Model.ts +76 -71
- package/src/adapters/ServiceBus.ts +8 -8
- package/src/adapters/cosmos-client.ts +2 -2
- package/src/adapters/memQueue.ts +2 -2
- package/src/adapters/mongo-client.ts +2 -2
- package/src/adapters/redis-client.ts +2 -2
- package/src/api/ContextProvider.ts +11 -11
- package/src/api/internal/events.ts +7 -6
- package/src/api/layerUtils.ts +8 -8
- package/src/api/routing/middleware/RouterMiddleware.ts +4 -4
- package/src/api/routing/middleware/middleware.ts +43 -0
- package/src/api/routing/schema/jwt.ts +2 -3
- package/src/api/routing.ts +7 -6
- package/src/api/setupRequest.ts +27 -7
- package/src/errorReporter.ts +1 -1
- package/src/fileUtil.ts +1 -1
- package/src/rateLimit.ts +2 -2
- package/test/contextProvider.test.ts +5 -5
- package/test/controller.test.ts +12 -9
- package/test/dist/contextProvider.test.d.ts.map +1 -1
- package/test/dist/controller.test.d.ts.map +1 -1
- package/test/dist/fixtures.d.ts +18 -8
- package/test/dist/fixtures.d.ts.map +1 -1
- package/test/dist/fixtures.js +11 -9
- package/test/dist/query.test.d.ts.map +1 -1
- package/test/dist/rawQuery.test.d.ts.map +1 -1
- package/test/dist/requires.test.d.ts.map +1 -1
- package/test/dist/rpc-multi-middleware.test.d.ts.map +1 -1
- package/test/dist/sql-store.test.d.ts.map +1 -0
- package/test/fixtures.ts +10 -8
- package/test/query.test.ts +160 -14
- package/test/rawQuery.test.ts +19 -17
- package/test/requires.test.ts +6 -5
- package/test/rpc-multi-middleware.test.ts +73 -4
- package/test/sql-store.test.ts +444 -0
- package/test/validateSample.test.ts +1 -1
- package/tsconfig.json +0 -1
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
|
@@ -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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
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
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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"]
|