@effect-app/infra 4.0.0-beta.12 → 4.0.0-beta.120
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 +794 -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/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/Registry.d.ts +20 -0
- package/dist/Model/Repository/Registry.d.ts.map +1 -0
- package/dist/Model/Repository/Registry.js +17 -0
- 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 +22 -16
- package/dist/Model/Repository/makeRepo.d.ts +5 -4
- package/dist/Model/Repository/makeRepo.d.ts.map +1 -1
- package/dist/Model/Repository/makeRepo.js +4 -1
- package/dist/Model/Repository/service.d.ts +5 -0
- package/dist/Model/Repository/service.d.ts.map +1 -1
- package/dist/Model/Repository/validation.d.ts +7 -6
- package/dist/Model/Repository/validation.d.ts.map +1 -1
- package/dist/Model/Repository.d.ts +1 -0
- package/dist/Model/Repository.d.ts.map +1 -1
- package/dist/Model/Repository.js +2 -1
- package/dist/Model/query/dsl.d.ts +9 -9
- package/dist/Model.d.ts +1 -0
- package/dist/Model.d.ts.map +1 -1
- package/dist/Model.js +2 -1
- 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 +3 -3
- package/dist/OperationsRepo.d.ts.map +1 -1
- package/dist/OperationsRepo.js +3 -3
- package/dist/QueueMaker/SQLQueue.d.ts +2 -4
- package/dist/QueueMaker/SQLQueue.d.ts.map +1 -1
- package/dist/QueueMaker/SQLQueue.js +8 -6
- package/dist/QueueMaker/errors.d.ts +1 -1
- package/dist/QueueMaker/errors.d.ts.map +1 -1
- package/dist/QueueMaker/memQueue.js +3 -3
- package/dist/QueueMaker/sbqueue.js +3 -3
- package/dist/RequestContext.d.ts +22 -17
- 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 +19 -3
- package/dist/Store/ContextMapContainer.d.ts.map +1 -1
- package/dist/Store/ContextMapContainer.js +13 -3
- package/dist/Store/Cosmos.d.ts.map +1 -1
- package/dist/Store/Cosmos.js +136 -68
- package/dist/Store/Disk.d.ts.map +1 -1
- package/dist/Store/Disk.js +24 -21
- package/dist/Store/Memory.d.ts +2 -2
- package/dist/Store/Memory.d.ts.map +1 -1
- package/dist/Store/Memory.js +26 -21
- 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 +191 -0
- package/dist/Store/SQL/query.d.ts +38 -0
- package/dist/Store/SQL/query.d.ts.map +1 -0
- package/dist/Store/SQL/query.js +367 -0
- package/dist/Store/SQL.d.ts +20 -0
- package/dist/Store/SQL.d.ts.map +1 -0
- package/dist/Store/SQL.js +381 -0
- package/dist/Store/index.d.ts +4 -1
- package/dist/Store/index.d.ts.map +1 -1
- package/dist/Store/index.js +15 -3
- package/dist/Store/service.d.ts +16 -5
- package/dist/Store/service.d.ts.map +1 -1
- package/dist/Store/service.js +24 -6
- 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/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 +11 -7
- 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.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/logger.d.ts.map +1 -1
- package/examples/query.ts +30 -26
- package/package.json +36 -18
- package/src/CUPS.ts +2 -2
- package/src/Emailer/service.ts +2 -2
- package/src/MainFiberSet.ts +2 -2
- package/src/Model/Repository/Registry.ts +33 -0
- package/src/Model/Repository/internal/internal.ts +76 -59
- package/src/Model/Repository/makeRepo.ts +7 -4
- package/src/Model/Repository/service.ts +6 -0
- package/src/Model/Repository.ts +1 -0
- package/src/Model.ts +1 -0
- package/src/Operations.ts +2 -2
- package/src/OperationsRepo.ts +2 -2
- package/src/QueueMaker/SQLQueue.ts +8 -7
- package/src/QueueMaker/memQueue.ts +2 -2
- package/src/QueueMaker/sbqueue.ts +2 -2
- package/src/RequestContext.ts +4 -4
- package/src/RequestFiberSet.ts +4 -4
- package/src/Store/ContextMapContainer.ts +41 -2
- package/src/Store/Cosmos.ts +350 -255
- package/src/Store/Disk.ts +37 -33
- package/src/Store/Memory.ts +29 -22
- package/src/Store/SQL/Pg.ts +321 -0
- package/src/Store/SQL/query.ts +409 -0
- package/src/Store/SQL.ts +674 -0
- package/src/Store/index.ts +17 -2
- package/src/Store/service.ts +31 -7
- 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 +14 -9
- 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.ts +3 -3
- package/src/api/setupRequest.ts +27 -7
- package/test/contextProvider.test.ts +11 -11
- 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/date-query.test.d.ts.map +1 -0
- package/test/dist/fixtures.d.ts +19 -9
- 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 +182 -33
- package/test/rawQuery.test.ts +22 -18
- package/test/requires.test.ts +6 -5
- package/test/rpc-multi-middleware.test.ts +72 -3
- package/test/sql-store.test.ts +1064 -0
- package/test/validateSample.test.ts +12 -9
- 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
|
-
|
|
72
|
+
file,
|
|
74
73
|
Effect.gen(function*() {
|
|
75
74
|
const shouldSeed = !(fs.existsSync(file))
|
|
76
75
|
|
|
@@ -140,43 +139,48 @@ export function makeDiskStore({ prefix }: StorageConfig, dir: string) {
|
|
|
140
139
|
config?: StoreConfig<Encoded>
|
|
141
140
|
) =>
|
|
142
141
|
Effect.gen(function*() {
|
|
143
|
-
const storesSem = Semaphore.makeUnsafe(1)
|
|
144
142
|
const primary = yield* makeDiskStoreInt(prefix, idKey, "primary", dir, name, seed, config?.defaultValues)
|
|
145
143
|
const stores = new Map<string, Store<IdKey, Encoded>>([["primary", primary]])
|
|
146
|
-
const ctx = yield* Effect.
|
|
144
|
+
const ctx = yield* Effect.context<R>()
|
|
145
|
+
const semaphores = new Map<string, Semaphore.Semaphore>()
|
|
146
|
+
const getSem = (ns: string) => {
|
|
147
|
+
let sem = semaphores.get(ns)
|
|
148
|
+
if (!sem) {
|
|
149
|
+
sem = Semaphore.makeUnsafe(1)
|
|
150
|
+
semaphores.set(ns, sem)
|
|
151
|
+
}
|
|
152
|
+
return sem
|
|
153
|
+
}
|
|
154
|
+
const ensureStore = (namespace: string) =>
|
|
155
|
+
getSem(namespace).withPermits(1)(
|
|
156
|
+
Effect.suspend(() => {
|
|
157
|
+
const existing = stores.get(namespace)
|
|
158
|
+
if (existing) return Effect.succeed(existing)
|
|
159
|
+
if (config?.allowNamespace && !config.allowNamespace(namespace)) {
|
|
160
|
+
throw new Error(`Namespace ${namespace} not allowed!`)
|
|
161
|
+
}
|
|
162
|
+
return makeDiskStoreInt<IdKey, Encoded, R, E>(
|
|
163
|
+
prefix,
|
|
164
|
+
idKey,
|
|
165
|
+
namespace,
|
|
166
|
+
dir,
|
|
167
|
+
name,
|
|
168
|
+
seed,
|
|
169
|
+
config?.defaultValues
|
|
170
|
+
)
|
|
171
|
+
.pipe(
|
|
172
|
+
Effect.orDie,
|
|
173
|
+
Effect.provide(ctx),
|
|
174
|
+
Effect.tap((store) => Effect.sync(() => stores.set(namespace, store)))
|
|
175
|
+
)
|
|
176
|
+
})
|
|
177
|
+
)
|
|
147
178
|
const getStore = !config?.allowNamespace
|
|
148
179
|
? Effect.succeed(primary)
|
|
149
|
-
: storeId.asEffect().pipe(Effect.flatMap((namespace) =>
|
|
150
|
-
const store = stores.get(namespace)
|
|
151
|
-
if (store) {
|
|
152
|
-
return Effect.succeed(store)
|
|
153
|
-
}
|
|
154
|
-
if (!config.allowNamespace!(namespace)) {
|
|
155
|
-
throw new Error(`Namespace ${namespace} not allowed!`)
|
|
156
|
-
}
|
|
157
|
-
return storesSem.withPermits(1)(
|
|
158
|
-
Effect.suspend(() => {
|
|
159
|
-
const existing = stores.get(namespace)
|
|
160
|
-
if (existing) return Effect.sync(() => existing)
|
|
161
|
-
return makeDiskStoreInt<IdKey, Encoded, R, E>(
|
|
162
|
-
prefix,
|
|
163
|
-
idKey,
|
|
164
|
-
namespace,
|
|
165
|
-
dir,
|
|
166
|
-
name,
|
|
167
|
-
seed,
|
|
168
|
-
config?.defaultValues
|
|
169
|
-
)
|
|
170
|
-
.pipe(
|
|
171
|
-
Effect.orDie,
|
|
172
|
-
Effect.provide(ctx),
|
|
173
|
-
Effect.tap((store) => Effect.sync(() => stores.set(namespace, store)))
|
|
174
|
-
)
|
|
175
|
-
})
|
|
176
|
-
)
|
|
177
|
-
}))
|
|
180
|
+
: storeId.asEffect().pipe(Effect.flatMap((namespace) => ensureStore(namespace)))
|
|
178
181
|
|
|
179
182
|
const s: Store<IdKey, Encoded> = {
|
|
183
|
+
seedNamespace: (namespace) => ensureStore(namespace).pipe(Effect.asVoid),
|
|
180
184
|
all: Effect.flatMap(getStore, (_) => _.all),
|
|
181
185
|
find: (...args) => Effect.flatMap(getStore, (_) => _.find(...args)),
|
|
182
186
|
filter: (...args) => Effect.flatMap(getStore, (_) => _.filter(...args)),
|
package/src/Store/Memory.ts
CHANGED
|
@@ -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,
|
|
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,7 +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 extends
|
|
75
|
+
export class storeId extends Context.Reference("StoreId", { defaultValue: (): NonEmptyString255 => defaultNs }) {}
|
|
76
76
|
|
|
77
77
|
function logQuery(f: FilterArgs<any, any>, defaultValues?: any) {
|
|
78
78
|
return InfraLogger
|
|
@@ -151,6 +151,8 @@ export function makeMemoryStoreInt<IdKey extends keyof Encoded, Encoded extends
|
|
|
151
151
|
withPermit
|
|
152
152
|
)
|
|
153
153
|
const s: Store<IdKey, Encoded> = {
|
|
154
|
+
seedNamespace: () => Effect.void,
|
|
155
|
+
|
|
154
156
|
queryRaw: (query) =>
|
|
155
157
|
all
|
|
156
158
|
.pipe(
|
|
@@ -257,7 +259,6 @@ export const makeMemoryStore = () => ({
|
|
|
257
259
|
config?: StoreConfig<Encoded>
|
|
258
260
|
) =>
|
|
259
261
|
Effect.gen(function*() {
|
|
260
|
-
const storesSem = Semaphore.makeUnsafe(1)
|
|
261
262
|
const primary = yield* makeMemoryStoreInt<IdKey, Encoded, R, E>(
|
|
262
263
|
modelName,
|
|
263
264
|
idKey,
|
|
@@ -265,30 +266,36 @@ export const makeMemoryStore = () => ({
|
|
|
265
266
|
seed,
|
|
266
267
|
config?.defaultValues
|
|
267
268
|
)
|
|
268
|
-
const ctx = yield* Effect.
|
|
269
|
+
const ctx = yield* Effect.context<R>()
|
|
269
270
|
const stores = new Map([["primary", primary]])
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
271
|
+
const semaphores = new Map<string, Semaphore.Semaphore>()
|
|
272
|
+
const getSem = (ns: string) => {
|
|
273
|
+
let sem = semaphores.get(ns)
|
|
274
|
+
if (!sem) {
|
|
275
|
+
sem = Semaphore.makeUnsafe(1)
|
|
276
|
+
semaphores.set(ns, sem)
|
|
277
|
+
}
|
|
278
|
+
return sem
|
|
279
|
+
}
|
|
280
|
+
const ensureStore = (namespace: string) =>
|
|
281
|
+
getSem(namespace).withPermits(1)(Effect.suspend(() => {
|
|
273
282
|
const store = stores.get(namespace)
|
|
274
|
-
if (store)
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
if (!config.allowNamespace!(namespace)) {
|
|
283
|
+
if (store) return Effect.succeed(store)
|
|
284
|
+
if (config?.allowNamespace && !config.allowNamespace(namespace)) {
|
|
278
285
|
throw new Error(`Namespace ${namespace} not allowed!`)
|
|
279
286
|
}
|
|
280
|
-
return
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
.
|
|
285
|
-
|
|
286
|
-
Effect.provide(ctx),
|
|
287
|
-
Effect.tap((store) => Effect.sync(() => stores.set(namespace, store)))
|
|
288
|
-
)
|
|
289
|
-
}))
|
|
287
|
+
return makeMemoryStoreInt(modelName, idKey, namespace, seed, config?.defaultValues)
|
|
288
|
+
.pipe(
|
|
289
|
+
Effect.orDie,
|
|
290
|
+
Effect.provide(ctx),
|
|
291
|
+
Effect.tap((store) => Effect.sync(() => stores.set(namespace, store)))
|
|
292
|
+
)
|
|
290
293
|
}))
|
|
294
|
+
const getStore = !config?.allowNamespace
|
|
295
|
+
? Effect.succeed(primary)
|
|
296
|
+
: storeId.asEffect().pipe(Effect.flatMap((namespace) => ensureStore(namespace)))
|
|
291
297
|
const s: Store<IdKey, Encoded> = {
|
|
298
|
+
seedNamespace: (namespace) => ensureStore(namespace).pipe(Effect.asVoid),
|
|
292
299
|
all: Effect.flatMap(getStore, (_) => _.all),
|
|
293
300
|
queryRaw: (...args) => Effect.flatMap(getStore, (_) => _.queryRaw(...args)),
|
|
294
301
|
find: (...args) => Effect.flatMap(getStore, (_) => _.find(...args)),
|
|
@@ -0,0 +1,321 @@
|
|
|
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
|
+
const ensureTable = 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(
|
|
69
|
+
Effect.andThen(
|
|
70
|
+
sql.unsafe(
|
|
71
|
+
`CREATE TABLE IF NOT EXISTS "_migrations" (id TEXT NOT NULL, version TEXT NOT NULL, PRIMARY KEY (id, version))`
|
|
72
|
+
)
|
|
73
|
+
),
|
|
74
|
+
Effect.orDie,
|
|
75
|
+
Effect.asVoid
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
const toRow = (e: PM) => {
|
|
79
|
+
const newE = makeETag(e)
|
|
80
|
+
const id = newE[idKey] as string
|
|
81
|
+
const { _etag, [idKey]: _id, ...rest } = newE as any
|
|
82
|
+
const data = JSON.stringify(rest)
|
|
83
|
+
return { id, _etag: newE._etag!, data, item: newE }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const exec = (query: string, params?: readonly unknown[]) =>
|
|
87
|
+
sql.unsafe(query, params as any).pipe(Effect.orDie)
|
|
88
|
+
|
|
89
|
+
const setInternal = (e: PM, ns: string) =>
|
|
90
|
+
Effect.gen(function*() {
|
|
91
|
+
const row = toRow(e)
|
|
92
|
+
if (e._etag) {
|
|
93
|
+
yield* exec(
|
|
94
|
+
`UPDATE "${tableName}" SET _etag = $1, data = $2 WHERE id = $3 AND _etag = $4 AND _namespace = $5`,
|
|
95
|
+
[row._etag, row.data, row.id, e._etag, ns]
|
|
96
|
+
)
|
|
97
|
+
const existing = yield* exec(
|
|
98
|
+
`SELECT _etag FROM "${tableName}" WHERE id = $1 AND _namespace = $2`,
|
|
99
|
+
[row.id, ns]
|
|
100
|
+
)
|
|
101
|
+
const current = (existing as any[])[0]
|
|
102
|
+
if (!current || current._etag !== row._etag) {
|
|
103
|
+
if (current) {
|
|
104
|
+
return yield* new OptimisticConcurrencyException({
|
|
105
|
+
type: name,
|
|
106
|
+
id: row.id,
|
|
107
|
+
current: current._etag,
|
|
108
|
+
found: e._etag,
|
|
109
|
+
code: 412
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
return yield* new OptimisticConcurrencyException({
|
|
113
|
+
type: name,
|
|
114
|
+
id: row.id,
|
|
115
|
+
current: "",
|
|
116
|
+
found: e._etag,
|
|
117
|
+
code: 404
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
yield* exec(
|
|
122
|
+
`INSERT INTO "${tableName}" (id, _namespace, _etag, data) VALUES ($1, $2, $3, $4)`,
|
|
123
|
+
[row.id, ns, row._etag, row.data]
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
return row.item
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
const bulkSetInternal = (items: NonEmptyReadonlyArray<PM>, ns: string) =>
|
|
130
|
+
sql
|
|
131
|
+
.withTransaction(Effect.forEach(items, (e) => setInternal(e, ns)))
|
|
132
|
+
.pipe(
|
|
133
|
+
Effect.orDie,
|
|
134
|
+
Effect.map((_) => _ as unknown as NonEmptyReadonlyArray<PM>)
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
const ctx = yield* Effect.context<R>()
|
|
138
|
+
const seedCache = new Map<string, Effect.Effect<void>>()
|
|
139
|
+
const makeSeedEffect = Effect.fnUntraced(function*(ns: string) {
|
|
140
|
+
yield* ensureTable
|
|
141
|
+
if (!seed) return
|
|
142
|
+
const existing = yield* exec(
|
|
143
|
+
`SELECT id FROM "_migrations" WHERE id = $1 AND version = $2`,
|
|
144
|
+
[`${tableName}::${ns}`, tableName]
|
|
145
|
+
)
|
|
146
|
+
if ((existing as any[]).length > 0) return
|
|
147
|
+
yield* InfraLogger.logInfo(`Seeding data for ${name} (namespace: ${ns})`)
|
|
148
|
+
const items = yield* seed.pipe(Effect.provide(ctx), Effect.orDie)
|
|
149
|
+
const ne = toNonEmptyArray([...items])
|
|
150
|
+
if (Option.isSome(ne)) yield* bulkSetInternal(ne.value, ns)
|
|
151
|
+
yield* exec(
|
|
152
|
+
`INSERT INTO "_migrations" (id, version) VALUES ($1, $2)`,
|
|
153
|
+
[`${tableName}::${ns}`, tableName]
|
|
154
|
+
)
|
|
155
|
+
})
|
|
156
|
+
const seedNamespace = (ns: string) => {
|
|
157
|
+
let cached = seedCache.get(ns)
|
|
158
|
+
if (!cached) {
|
|
159
|
+
cached = Effect.cached(Effect.uninterruptible(makeSeedEffect(ns))).pipe(Effect.runSync)
|
|
160
|
+
seedCache.set(ns, cached)
|
|
161
|
+
}
|
|
162
|
+
return cached
|
|
163
|
+
}
|
|
164
|
+
const s: Store<IdKey, Encoded> = {
|
|
165
|
+
seedNamespace: (ns) => seedNamespace(ns),
|
|
166
|
+
|
|
167
|
+
all: resolveNamespace.pipe(Effect.flatMap((ns) =>
|
|
168
|
+
exec(`SELECT id, _etag, data FROM "${tableName}" WHERE _namespace = $1`, [ns])
|
|
169
|
+
.pipe(
|
|
170
|
+
Effect.map((rows) => (rows as any[]).map((r) => parseRow<Encoded>(r, idKey, defaultValues))),
|
|
171
|
+
Effect.withSpan("PgSQL.all [effect-app/infra/Store]", {
|
|
172
|
+
attributes: {
|
|
173
|
+
"repository.table_name": tableName,
|
|
174
|
+
"repository.model_name": name,
|
|
175
|
+
"repository.namespace": ns
|
|
176
|
+
}
|
|
177
|
+
}, { captureStackTrace: false })
|
|
178
|
+
)
|
|
179
|
+
)),
|
|
180
|
+
|
|
181
|
+
find: (id) =>
|
|
182
|
+
resolveNamespace.pipe(Effect
|
|
183
|
+
.flatMap((ns) =>
|
|
184
|
+
exec(`SELECT id, _etag, data FROM "${tableName}" WHERE id = $1 AND _namespace = $2`, [id, ns])
|
|
185
|
+
.pipe(
|
|
186
|
+
Effect.map((rows) => {
|
|
187
|
+
const row = (rows as any[])[0]
|
|
188
|
+
return row
|
|
189
|
+
? Option.some(parseRow<Encoded>(row, idKey, defaultValues))
|
|
190
|
+
: Option.none()
|
|
191
|
+
}),
|
|
192
|
+
Effect.withSpan("PgSQL.find [effect-app/infra/Store]", {
|
|
193
|
+
attributes: { "repository.table_name": tableName, "repository.model_name": name, id }
|
|
194
|
+
}, { captureStackTrace: false })
|
|
195
|
+
)
|
|
196
|
+
)),
|
|
197
|
+
|
|
198
|
+
filter: <U extends keyof Encoded = never>(f: FilterArgs<Encoded, U>) => {
|
|
199
|
+
const filter = f
|
|
200
|
+
.filter
|
|
201
|
+
type M = U extends undefined ? Encoded : Pick<Encoded, U>
|
|
202
|
+
return resolveNamespace.pipe(Effect.flatMap((ns) =>
|
|
203
|
+
Effect
|
|
204
|
+
.sync(() => {
|
|
205
|
+
const q = buildWhereSQLQuery(
|
|
206
|
+
pgDialect,
|
|
207
|
+
idKey,
|
|
208
|
+
filter ? [{ t: "where-scope", result: filter, relation: "some" }] : [],
|
|
209
|
+
tableName,
|
|
210
|
+
defaultValues,
|
|
211
|
+
f.select as
|
|
212
|
+
| NonEmptyReadonlyArray<string | { key: string; subKeys: readonly string[] }>
|
|
213
|
+
| undefined,
|
|
214
|
+
f.order as NonEmptyReadonlyArray<{ key: string; direction: "ASC" | "DESC" }> | undefined,
|
|
215
|
+
f.skip,
|
|
216
|
+
f.limit
|
|
217
|
+
)
|
|
218
|
+
const nsPlaceholder = pgDialect.placeholder(q.params.length + 1)
|
|
219
|
+
const hasWhere = q.sql.includes("WHERE")
|
|
220
|
+
const nsSql = hasWhere
|
|
221
|
+
? q.sql.replace("WHERE", `WHERE _namespace = ${nsPlaceholder} AND`)
|
|
222
|
+
: q.sql.replace(
|
|
223
|
+
`FROM "${tableName}"`,
|
|
224
|
+
`FROM "${tableName}" WHERE _namespace = ${nsPlaceholder}`
|
|
225
|
+
)
|
|
226
|
+
return { sql: nsSql, params: [...q.params, ns] }
|
|
227
|
+
})
|
|
228
|
+
.pipe(
|
|
229
|
+
Effect.tap((q) => logQuery(q)),
|
|
230
|
+
Effect.flatMap((q) =>
|
|
231
|
+
exec(q.sql, q.params).pipe(
|
|
232
|
+
Effect.map((rows) => {
|
|
233
|
+
if (f.select) {
|
|
234
|
+
return (rows as any[]).map((r) => {
|
|
235
|
+
const selected = parseSelectRow(r, idKey, {})
|
|
236
|
+
return {
|
|
237
|
+
...Struct.pick(
|
|
238
|
+
defaultValues as any,
|
|
239
|
+
f.select!.filter((_) => typeof _ === "string") as never[]
|
|
240
|
+
),
|
|
241
|
+
...selected
|
|
242
|
+
} as M
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
return (rows as any[]).map((r) => parseRow<Encoded>(r, idKey, defaultValues) as any as M)
|
|
246
|
+
})
|
|
247
|
+
)
|
|
248
|
+
),
|
|
249
|
+
Effect.withSpan("PgSQL.filter [effect-app/infra/Store]", {
|
|
250
|
+
attributes: { "repository.table_name": tableName, "repository.model_name": name }
|
|
251
|
+
}, { captureStackTrace: false })
|
|
252
|
+
)
|
|
253
|
+
))
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
set: (e) =>
|
|
257
|
+
resolveNamespace.pipe(Effect.flatMap((ns) =>
|
|
258
|
+
setInternal(e, ns).pipe(
|
|
259
|
+
Effect.withSpan("PgSQL.set [effect-app/infra/Store]", {
|
|
260
|
+
attributes: { "repository.table_name": tableName, "repository.model_name": name, id: e[idKey] }
|
|
261
|
+
}, { captureStackTrace: false })
|
|
262
|
+
)
|
|
263
|
+
)),
|
|
264
|
+
|
|
265
|
+
batchSet: (items) =>
|
|
266
|
+
resolveNamespace.pipe(Effect.flatMap((ns) =>
|
|
267
|
+
bulkSetInternal(items, ns).pipe(
|
|
268
|
+
Effect.withSpan("PgSQL.batchSet [effect-app/infra/Store]", {
|
|
269
|
+
attributes: { "repository.table_name": tableName, "repository.model_name": name }
|
|
270
|
+
}, { captureStackTrace: false })
|
|
271
|
+
)
|
|
272
|
+
)),
|
|
273
|
+
|
|
274
|
+
bulkSet: (items) =>
|
|
275
|
+
resolveNamespace.pipe(Effect.flatMap((ns) =>
|
|
276
|
+
bulkSetInternal(items, ns).pipe(
|
|
277
|
+
Effect.withSpan("PgSQL.bulkSet [effect-app/infra/Store]", {
|
|
278
|
+
attributes: { "repository.table_name": tableName, "repository.model_name": name }
|
|
279
|
+
}, { captureStackTrace: false })
|
|
280
|
+
)
|
|
281
|
+
)),
|
|
282
|
+
|
|
283
|
+
batchRemove: (ids) => {
|
|
284
|
+
const placeholders = ids.map((_, i) => `$${i + 1}`).join(", ")
|
|
285
|
+
const nsPlaceholder = `$${ids.length + 1}`
|
|
286
|
+
return resolveNamespace.pipe(Effect.flatMap((ns) =>
|
|
287
|
+
exec(
|
|
288
|
+
`DELETE FROM "${tableName}" WHERE id IN (${placeholders}) AND _namespace = ${nsPlaceholder}`,
|
|
289
|
+
[...ids, ns]
|
|
290
|
+
)
|
|
291
|
+
.pipe(
|
|
292
|
+
Effect.asVoid,
|
|
293
|
+
Effect.withSpan("PgSQL.batchRemove [effect-app/infra/Store]", {
|
|
294
|
+
attributes: { "repository.table_name": tableName, "repository.model_name": name }
|
|
295
|
+
}, { captureStackTrace: false })
|
|
296
|
+
)
|
|
297
|
+
))
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
queryRaw: (query) =>
|
|
301
|
+
s.all.pipe(
|
|
302
|
+
Effect.map(query.memory),
|
|
303
|
+
Effect.withSpan("PgSQL.queryRaw [effect-app/infra/Store]", {
|
|
304
|
+
attributes: { "repository.table_name": tableName, "repository.model_name": name }
|
|
305
|
+
}, { captureStackTrace: false })
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Eagerly seed primary namespace on initialization
|
|
310
|
+
yield* seedNamespace("primary")
|
|
311
|
+
|
|
312
|
+
return s
|
|
313
|
+
})
|
|
314
|
+
}
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function PgStoreLayer(cfg: StorageConfig) {
|
|
319
|
+
return StoreMaker
|
|
320
|
+
.toLayer(makePgStore(cfg))
|
|
321
|
+
}
|