@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/Emailer/service.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { MailContent, MailData } from "@sendgrid/helpers/classes/mail.js"
|
|
2
2
|
import type { ResponseError } from "@sendgrid/mail"
|
|
3
|
-
import { Data, type Effect, type NonEmptyReadonlyArray, type Redacted
|
|
3
|
+
import { Context, Data, type Effect, type NonEmptyReadonlyArray, type Redacted } from "effect-app"
|
|
4
4
|
import type { Email } from "effect-app/Schema"
|
|
5
5
|
|
|
6
6
|
export class SendMailError extends Data.TaggedError("SendMailError")<{
|
|
7
7
|
readonly raw: Error | ResponseError
|
|
8
8
|
}> {}
|
|
9
9
|
|
|
10
|
-
export class Emailer extends
|
|
10
|
+
export class Emailer extends Context.Opaque<Emailer, {
|
|
11
11
|
sendMail: (msg: EmailMsgOptionalFrom) => Effect.Effect<void, SendMailError>
|
|
12
12
|
}>()("effect-app/Emailer") {}
|
|
13
13
|
|
package/src/MainFiberSet.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect, Fiber, FiberSet, Layer
|
|
1
|
+
import { Context, Effect, Fiber, FiberSet, Layer } from "effect-app"
|
|
2
2
|
import type {} from "effect/Scope"
|
|
3
3
|
import { InfraLogger } from "./logger.js"
|
|
4
4
|
import { reportNonInterruptedFailureCause } from "./QueueMaker/errors.js"
|
|
@@ -62,7 +62,7 @@ const make = Effect.gen(function*() {
|
|
|
62
62
|
* you should register these long running fibers in a FiberSet, and join them at the end of your main program.
|
|
63
63
|
* This way any errors will blow up the main program instead of fibers dying unknowingly.
|
|
64
64
|
*/
|
|
65
|
-
export class MainFiberSet extends
|
|
65
|
+
export class MainFiberSet extends Context.Service<MainFiberSet>()("MainFiberSet", { make }) {
|
|
66
66
|
static readonly Live = Layer.effect(this, this.make)
|
|
67
67
|
static readonly JoinLive = this.asEffect().pipe(
|
|
68
68
|
Effect.andThen((_) => _.join),
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Context, Effect } from "effect-app"
|
|
2
|
+
|
|
3
|
+
export interface RegisteredRepository {
|
|
4
|
+
readonly seedNamespace: (namespace: string) => Effect.Effect<void>
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const make = Effect.sync(() => {
|
|
8
|
+
const repos = new Map<string, RegisteredRepository>()
|
|
9
|
+
return {
|
|
10
|
+
register(modelName: string, repo: RegisteredRepository) {
|
|
11
|
+
repos.set(modelName, repo)
|
|
12
|
+
},
|
|
13
|
+
seedNamespace: (namespace: string) =>
|
|
14
|
+
Effect.suspend(() =>
|
|
15
|
+
Effect.forEach(
|
|
16
|
+
repos.values(),
|
|
17
|
+
(r) => r.seedNamespace(namespace),
|
|
18
|
+
{ concurrency: "unbounded", discard: true }
|
|
19
|
+
)
|
|
20
|
+
),
|
|
21
|
+
get entries(): ReadonlyMap<string, RegisteredRepository> {
|
|
22
|
+
return repos
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export class RepositoryRegistry extends Context.Opaque<RepositoryRegistry, {
|
|
28
|
+
readonly register: (modelName: string, repo: RegisteredRepository) => void
|
|
29
|
+
readonly seedNamespace: (namespace: string) => Effect.Effect<void>
|
|
30
|
+
readonly entries: ReadonlyMap<string, RegisteredRepository>
|
|
31
|
+
}>()("effect-app/RepositoryRegistry", { make }) {}
|
|
32
|
+
|
|
33
|
+
export const RepositoryRegistryLive = RepositoryRegistry.toLayer(RepositoryRegistry.make)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import type {} from "effect/Equal"
|
|
3
3
|
import type {} from "effect/Hash"
|
|
4
|
-
import { Array, Chunk, Effect, Equivalence, flow, type NonEmptyReadonlyArray, Option, pipe, Pipeable, PubSub, Result, S, SchemaAST,
|
|
4
|
+
import { Array, Chunk, Context, Effect, Equivalence, flow, type NonEmptyReadonlyArray, Option, pipe, Pipeable, PubSub, Result, S, SchemaAST, Unify } from "effect-app"
|
|
5
5
|
import { toNonEmptyArray } from "effect-app/Array"
|
|
6
6
|
import { NotFoundError } from "effect-app/client/errors"
|
|
7
7
|
import { flatMapOption } from "effect-app/Effect"
|
|
@@ -55,14 +55,14 @@ export function makeRepoInternal<
|
|
|
55
55
|
|
|
56
56
|
function make<RInitial = never, E = never, RPublish = never, RCtx = never>(
|
|
57
57
|
args: [Evt] extends [never] ? {
|
|
58
|
-
schemaContext?:
|
|
58
|
+
schemaContext?: Context.Context<RCtx>
|
|
59
59
|
makeInitial?: Effect.Effect<readonly T[], E, RInitial> | undefined
|
|
60
60
|
config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
|
|
61
61
|
partitionValue?: (e?: Encoded) => string
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
: {
|
|
65
|
-
schemaContext?:
|
|
65
|
+
schemaContext?: Context.Context<RCtx>
|
|
66
66
|
publishEvents: (evt: NonEmptyReadonlyArray<Evt>) => Effect.Effect<void, never, RPublish>
|
|
67
67
|
makeInitial?: Effect.Effect<readonly T[], E, RInitial> | undefined
|
|
68
68
|
config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
|
|
@@ -72,12 +72,12 @@ export function makeRepoInternal<
|
|
|
72
72
|
) {
|
|
73
73
|
return Effect
|
|
74
74
|
.gen(function*() {
|
|
75
|
-
const rctx:
|
|
75
|
+
const rctx: Context.Context<RCtx> = args.schemaContext ?? Context.empty() as any
|
|
76
76
|
const provideRctx = Effect.provide(rctx)
|
|
77
77
|
const encodeMany = flow(
|
|
78
78
|
S.encodeEffect(S.Array(schema)),
|
|
79
79
|
provideRctx,
|
|
80
|
-
Effect.withSpan("encodeMany", {}, { captureStackTrace: false })
|
|
80
|
+
Effect.withSpan("encodeMany", { attributes: { itemType: name } }, { captureStackTrace: false })
|
|
81
81
|
)
|
|
82
82
|
const decode = flow(S.decodeEffect(schema), provideRctx)
|
|
83
83
|
const decodeMany = flow(
|
|
@@ -113,11 +113,14 @@ export function makeRepoInternal<
|
|
|
113
113
|
let ast = _.ast
|
|
114
114
|
if (ast._tag === "Declaration") ast = ast.typeParameters[0]!
|
|
115
115
|
|
|
116
|
-
// In v4, to get the encoded (from) side of a schema, use SchemaAST.toEncoded
|
|
117
116
|
const pickIdFromAst = (a: SchemaAST.AST) => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
117
|
+
// Unwrap Declaration (e.g. TaggedClass) to get the underlying Objects AST
|
|
118
|
+
let inner = a
|
|
119
|
+
if (inner._tag === "Declaration") inner = inner.typeParameters[0]!
|
|
120
|
+
// Pick from the original AST to preserve the full encoding chain (e.g. decodeTo transformations).
|
|
121
|
+
// Using toEncoded would lose transformation info needed to encode Type -> Encoded.
|
|
122
|
+
if (SchemaAST.isObjects(inner)) {
|
|
123
|
+
const field = inner.propertySignatures.find((_) => _.name === idKey)
|
|
121
124
|
if (field) {
|
|
122
125
|
return S.Struct({ [idKey]: S.make(field.type) }) as unknown as Codec<T, Encoded>
|
|
123
126
|
}
|
|
@@ -161,7 +164,7 @@ export function makeRepoInternal<
|
|
|
161
164
|
)
|
|
162
165
|
})
|
|
163
166
|
|
|
164
|
-
const find = Effect.fn("find")(function*(id: T[IdKey]) {
|
|
167
|
+
const find = Effect.fn("find", { attributes: { itemType: name } })(function*(id: T[IdKey]) {
|
|
165
168
|
yield* Effect.annotateCurrentSpan({ itemId: id })
|
|
166
169
|
|
|
167
170
|
return yield* flatMapOption(findE(id), (_) => Effect.orDie(decode(_)))
|
|
@@ -188,62 +191,71 @@ export function makeRepoInternal<
|
|
|
188
191
|
Effect.andThen(saveAllE)
|
|
189
192
|
)
|
|
190
193
|
|
|
191
|
-
const saveAndPublish = Effect.fn("saveAndPublish"
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
194
|
+
const saveAndPublish = Effect.fn("saveAndPublish", { attributes: { itemType: name } })(
|
|
195
|
+
function*(items: Iterable<T>, events: Iterable<Evt> = []) {
|
|
196
|
+
const it = Chunk.fromIterable(items)
|
|
197
|
+
const evts = [...events]
|
|
198
|
+
yield* Effect.annotateCurrentSpan({ itemIds: [...Chunk.map(it, (_) => _[idKey])], events: evts.length })
|
|
199
|
+
return yield* saveAll(it)
|
|
200
|
+
.pipe(
|
|
201
|
+
Effect.andThen(Effect.sync(() => toNonEmptyArray(evts))),
|
|
202
|
+
// TODO: for full consistency the events should be stored within the same database transaction, and then picked up.
|
|
203
|
+
(_) => flatMapOption(_, pub),
|
|
204
|
+
Effect.andThen(PubSub.publish(changeFeed, [Chunk.toArray(it), "save"] as [T[], "save" | "remove"])),
|
|
205
|
+
Effect.asVoid
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
)
|
|
204
209
|
|
|
205
|
-
const removeAndPublish = Effect.fn("removeAndPublish"
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
210
|
+
const removeAndPublish = Effect.fn("removeAndPublish", { attributes: { itemType: name } })(
|
|
211
|
+
function*(a: Iterable<T>, events: Iterable<Evt> = []) {
|
|
212
|
+
const { set } = yield* cms
|
|
213
|
+
const it = [...a]
|
|
214
|
+
const evts = [...events]
|
|
215
|
+
yield* Effect.annotateCurrentSpan({ itemIds: it.map((_) => _[idKey]), eventCount: evts.length })
|
|
216
|
+
const items = yield* encodeMany(it).pipe(Effect.orDie)
|
|
217
|
+
if (Array.isReadonlyArrayNonEmpty(items)) {
|
|
218
|
+
yield* store.batchRemove(
|
|
219
|
+
items.map((_) => (_[idKey])),
|
|
220
|
+
args.config?.partitionValue?.(items[0])
|
|
221
|
+
)
|
|
222
|
+
for (const e of items) {
|
|
223
|
+
set(e[idKey], undefined)
|
|
224
|
+
}
|
|
225
|
+
yield* Effect
|
|
226
|
+
.sync(() => toNonEmptyArray(evts))
|
|
227
|
+
// TODO: for full consistency the events should be stored within the same database transaction, and then picked up.
|
|
228
|
+
.pipe((_) => flatMapOption(_, pub))
|
|
223
229
|
|
|
224
|
-
|
|
230
|
+
yield* PubSub.publish(changeFeed, [it, "remove"] as [T[], "save" | "remove"])
|
|
231
|
+
}
|
|
225
232
|
}
|
|
226
|
-
|
|
233
|
+
)
|
|
227
234
|
|
|
228
|
-
const removeById = Effect.fn("removeById"
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
235
|
+
const removeById = Effect.fn("removeById", { attributes: { itemType: name } })(
|
|
236
|
+
function*(...ids: readonly T[IdKey][]) {
|
|
237
|
+
if (!Array.isReadonlyArrayNonEmpty(ids)) {
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
const { set } = yield* cms
|
|
241
|
+
const eids = yield* Effect.forEach(ids, (_) => encodeIdOnly(_ as any)).pipe(Effect.orDie)
|
|
242
|
+
yield* Effect.annotateCurrentSpan({ itemIds: eids })
|
|
243
|
+
yield* store.batchRemove(eids)
|
|
244
|
+
for (const id of eids) {
|
|
245
|
+
set(id, undefined)
|
|
246
|
+
}
|
|
247
|
+
yield* PubSub.publish(changeFeed, [[], "remove"] as [T[], "save" | "remove"])
|
|
238
248
|
}
|
|
239
|
-
|
|
240
|
-
})
|
|
249
|
+
)
|
|
241
250
|
|
|
242
251
|
const parseMany = (items: readonly PM[]) =>
|
|
243
252
|
Effect
|
|
244
253
|
.flatMap(cms, (cm) =>
|
|
245
254
|
decodeMany(items.map((_) => mapReverse(_, cm.set)))
|
|
246
|
-
.pipe(
|
|
255
|
+
.pipe(
|
|
256
|
+
Effect.orDie,
|
|
257
|
+
Effect.withSpan("parseMany", { attributes: { itemType: name } }, { captureStackTrace: false })
|
|
258
|
+
))
|
|
247
259
|
const parseMany2 = <A, R>(
|
|
248
260
|
items: readonly PM[],
|
|
249
261
|
schema: S.Codec<A, Encoded, R>
|
|
@@ -254,7 +266,10 @@ export function makeRepoInternal<
|
|
|
254
266
|
.decodeEffect(S.Array(schema))(
|
|
255
267
|
items.map((_) => mapReverse(_, cm.set))
|
|
256
268
|
)
|
|
257
|
-
.pipe(
|
|
269
|
+
.pipe(
|
|
270
|
+
Effect.orDie,
|
|
271
|
+
Effect.withSpan("parseMany2", { attributes: { itemType: name } }, { captureStackTrace: false })
|
|
272
|
+
))
|
|
258
273
|
const filter = <U extends keyof Encoded = keyof Encoded>(args: FilterArgs<Encoded, U>) =>
|
|
259
274
|
store
|
|
260
275
|
.filter(
|
|
@@ -327,6 +342,7 @@ export function makeRepoInternal<
|
|
|
327
342
|
: eff,
|
|
328
343
|
Effect.withSpan("Repository.query [effect-app/infra]", {
|
|
329
344
|
attributes: {
|
|
345
|
+
itemType: name,
|
|
330
346
|
"repository.model_name": name,
|
|
331
347
|
query: { ...a, schema: a.schema ? "__SCHEMA__" : a.schema, filter: a.filter }
|
|
332
348
|
}
|
|
@@ -334,7 +350,7 @@ export function makeRepoInternal<
|
|
|
334
350
|
)
|
|
335
351
|
}) as any
|
|
336
352
|
|
|
337
|
-
const validateSample = Effect.fn("validateSample")(function*(options?: {
|
|
353
|
+
const validateSample = Effect.fn("validateSample", { attributes: { itemType: name } })(function*(options?: {
|
|
338
354
|
percentage?: number
|
|
339
355
|
maxItems?: number
|
|
340
356
|
}) {
|
|
@@ -401,6 +417,7 @@ export function makeRepoInternal<
|
|
|
401
417
|
saveAndPublish,
|
|
402
418
|
removeAndPublish,
|
|
403
419
|
removeById,
|
|
420
|
+
seedNamespace: (namespace: string) => store.seedNamespace(namespace),
|
|
404
421
|
validateSample,
|
|
405
422
|
queryRaw<A, Out, QR>(schema: S.Codec<A, Out, QR>, q: Q.RawQuery<Encoded, Out>) {
|
|
406
423
|
const dec = S.decodeEffect(S.Array(schema))
|
|
@@ -441,7 +458,7 @@ export function makeRepoInternal<
|
|
|
441
458
|
// },
|
|
442
459
|
save: (...xes: any[]) =>
|
|
443
460
|
Effect.flatMap(encMany(xes), (_) => saveAllE(_)).pipe(
|
|
444
|
-
Effect.withSpan("mapped.save", {}, { captureStackTrace: false })
|
|
461
|
+
Effect.withSpan("mapped.save", { attributes: { itemType: name } }, { captureStackTrace: false })
|
|
445
462
|
)
|
|
446
463
|
}
|
|
447
464
|
}
|
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
// import type { ParserEnv } from "effect-app/Schema/custom/Parser"
|
|
8
8
|
import type {} from "effect/Equal"
|
|
9
9
|
import type {} from "effect/Hash"
|
|
10
|
-
import { Effect, type NonEmptyReadonlyArray, type S
|
|
10
|
+
import { type Context, Effect, type NonEmptyReadonlyArray, type S } from "effect-app"
|
|
11
11
|
import type { StoreConfig, StoreMaker } from "../../Store.js"
|
|
12
12
|
import type { FieldValues } from "../filter/types.js"
|
|
13
13
|
import { type ExtendedRepository, extendRepo } from "./ext.js"
|
|
14
14
|
import { makeRepoInternal } from "./internal/internal.js"
|
|
15
|
+
import { RepositoryRegistry } from "./Registry.js"
|
|
15
16
|
import type { Repository } from "./service.js"
|
|
16
17
|
|
|
17
18
|
export interface RepositoryOptions<
|
|
@@ -52,7 +53,7 @@ export interface RepositoryOptions<
|
|
|
52
53
|
* Optional context to be provided to Schema decode/encode.
|
|
53
54
|
* Useful for effectful transformations like XWithItems, where items is a transformation retrieving elements from another database table or other source.
|
|
54
55
|
*/
|
|
55
|
-
schemaContext?:
|
|
56
|
+
schemaContext?: Context.Context<RCtx>
|
|
56
57
|
|
|
57
58
|
overrides?: (
|
|
58
59
|
repo: Repository<T, Encoded, Evt, ItemType, IdKey, Exclude<RSchema, RCtx>, RPublish>
|
|
@@ -85,7 +86,7 @@ export const makeRepo: {
|
|
|
85
86
|
): Effect.Effect<
|
|
86
87
|
ExtendedRepository<T, Encoded, Evt, ItemType, IdKey, Exclude<RSchema, RCtx>, RPublish>,
|
|
87
88
|
E,
|
|
88
|
-
RInitial | StoreMaker
|
|
89
|
+
RInitial | StoreMaker | RepositoryRegistry
|
|
89
90
|
>
|
|
90
91
|
<
|
|
91
92
|
ItemType extends string,
|
|
@@ -104,7 +105,7 @@ export const makeRepo: {
|
|
|
104
105
|
): Effect.Effect<
|
|
105
106
|
ExtendedRepository<T, Encoded, Evt, ItemType, "id", Exclude<RSchema, RCtx>, RPublish>,
|
|
106
107
|
E,
|
|
107
|
-
RInitial | StoreMaker
|
|
108
|
+
RInitial | StoreMaker | RepositoryRegistry
|
|
108
109
|
>
|
|
109
110
|
} = <
|
|
110
111
|
ItemType extends string,
|
|
@@ -135,5 +136,7 @@ export const makeRepo: {
|
|
|
135
136
|
let r = yield* mkRepo.make<RInitial, E, RPublish, RCtx>(options as any)
|
|
136
137
|
if (options.overrides) r = options.overrides(r)
|
|
137
138
|
const repo = extendRepo(r)
|
|
139
|
+
const registry = yield* RepositoryRegistry
|
|
140
|
+
registry.register(itemType, repo)
|
|
138
141
|
return repo
|
|
139
142
|
})
|
|
@@ -37,6 +37,12 @@ export interface Repository<
|
|
|
37
37
|
raw: RawQuery<Encoded, Out>
|
|
38
38
|
) => Effect.Effect<readonly T[], S.SchemaError, R>
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Explicitly seed a namespace. Primary is seeded eagerly on initialization.
|
|
42
|
+
* Non-primary namespaces must be seeded explicitly before use.
|
|
43
|
+
*/
|
|
44
|
+
readonly seedNamespace: (namespace: string) => Effect.Effect<void>
|
|
45
|
+
|
|
40
46
|
readonly query: {
|
|
41
47
|
// ending with projection
|
|
42
48
|
<
|
package/src/Model/Repository.ts
CHANGED
package/src/Model.ts
CHANGED
package/src/Operations.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { reportError } from "@effect-app/infra/errorReporter"
|
|
2
2
|
import { subHours } from "date-fns"
|
|
3
|
-
import { Cause, copy, Duration, Effect, Exit, type Fiber, Layer, Option, S, Schedule
|
|
3
|
+
import { Cause, Context, copy, Duration, Effect, Exit, type Fiber, Layer, Option, S, Schedule } from "effect-app"
|
|
4
4
|
import { annotateLogscoped } from "effect-app/Effect"
|
|
5
5
|
import { dual, pipe } from "effect-app/Function"
|
|
6
6
|
import { Operation, OperationFailure, OperationId, type OperationProgress, OperationSuccess } from "effect-app/Operations"
|
|
@@ -189,7 +189,7 @@ const make = Effect.gen(function*() {
|
|
|
189
189
|
}
|
|
190
190
|
})
|
|
191
191
|
|
|
192
|
-
export class Operations extends
|
|
192
|
+
export class Operations extends Context.Opaque<Operations>()("effect-app/Operations", { make }) {
|
|
193
193
|
private static readonly CleanupLive = this
|
|
194
194
|
.use((_) =>
|
|
195
195
|
_.cleanup.pipe(
|
package/src/OperationsRepo.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Context, Effect } from "effect-app"
|
|
2
2
|
import { Operation } from "effect-app/Operations"
|
|
3
3
|
import { makeRepo } from "./Model.js"
|
|
4
4
|
|
|
5
|
-
export class OperationsRepo extends
|
|
5
|
+
export class OperationsRepo extends Context.Service<OperationsRepo>()(
|
|
6
6
|
"OperationRepo",
|
|
7
7
|
{
|
|
8
8
|
make: Effect.gen(function*() {
|
|
@@ -3,13 +3,13 @@ import { reportNonInterruptedFailure } from "@effect-app/infra/QueueMaker/errors
|
|
|
3
3
|
import { type QueueBase, QueueMeta } from "@effect-app/infra/QueueMaker/service"
|
|
4
4
|
import { subMinutes } from "date-fns"
|
|
5
5
|
import { Effect, Fiber, type NonEmptyReadonlyArray, Option, S, Tracer } from "effect-app"
|
|
6
|
-
import type
|
|
6
|
+
import { type NonEmptyString255 } from "effect-app/Schema"
|
|
7
7
|
import { pretty } from "effect-app/utils"
|
|
8
8
|
import { SqlClient } from "effect/unstable/sql"
|
|
9
9
|
import { SQLModel } from "../adapters/SQL.js"
|
|
10
10
|
import { InfraLogger } from "../logger.js"
|
|
11
11
|
|
|
12
|
-
export const QueueId = S.
|
|
12
|
+
export const QueueId = S.Finite.pipe(S.brand("QueueId"))
|
|
13
13
|
export type QueueId = typeof QueueId.Type
|
|
14
14
|
|
|
15
15
|
// TODO: let the model track and Auto Generate versionColumn on every update instead
|
|
@@ -78,14 +78,14 @@ export function makeSQLQueue<
|
|
|
78
78
|
|
|
79
79
|
const q = {
|
|
80
80
|
offer: Effect.fnUntraced(function*(body: Evt, meta: typeof QueueMeta.Type) {
|
|
81
|
-
yield* queueRepo.insertVoid({
|
|
81
|
+
yield* queueRepo.insertVoid(Queue.insert.make({
|
|
82
82
|
body,
|
|
83
83
|
meta,
|
|
84
84
|
name: queueName,
|
|
85
85
|
processingAt: Option.none(),
|
|
86
86
|
finishedAt: Option.none(),
|
|
87
87
|
etag: crypto.randomUUID()
|
|
88
|
-
})
|
|
88
|
+
}))
|
|
89
89
|
}),
|
|
90
90
|
take: Effect.gen(function*() {
|
|
91
91
|
while (true) {
|
|
@@ -94,15 +94,16 @@ export function makeSQLQueue<
|
|
|
94
94
|
const dec = yield* decodeDrain(first)
|
|
95
95
|
const { createdAt, updatedAt, ...rest } = dec
|
|
96
96
|
return yield* drainRepo.update(
|
|
97
|
-
{ ...rest, processingAt: Option.some(new Date()) } // auto in lib , etag: crypto.randomUUID()
|
|
97
|
+
Drain.update.make({ ...rest, processingAt: Option.some(new Date()) }) // auto in lib , etag: crypto.randomUUID()
|
|
98
98
|
)
|
|
99
99
|
}
|
|
100
100
|
if (first) return first
|
|
101
101
|
yield* Effect.sleep(250)
|
|
102
102
|
}
|
|
103
103
|
}),
|
|
104
|
-
finish: ({ createdAt, updatedAt, ...q }: Drain)
|
|
105
|
-
drainRepo.updateVoid({ ...q, finishedAt: Option.some(new Date()) }) // auto in lib , etag: crypto.randomUUID()
|
|
104
|
+
finish: Effect.fn(function*({ createdAt, updatedAt, ...q }: Drain) {
|
|
105
|
+
return yield* drainRepo.updateVoid(Drain.update.make({ ...q, finishedAt: Option.some(new Date()) })) // auto in lib , etag: crypto.randomUUID()
|
|
106
|
+
})
|
|
106
107
|
}
|
|
107
108
|
const queue = {
|
|
108
109
|
publish: (...messages: NonEmptyReadonlyArray<Evt>) =>
|
|
@@ -25,10 +25,10 @@ export function makeMemQueue<
|
|
|
25
25
|
const qDrain = yield* mem.getOrCreateQueue(queueDrainName)
|
|
26
26
|
|
|
27
27
|
const wireSchema = S.Struct({ body: schema, meta: QueueMeta })
|
|
28
|
-
const wireSchemaJson = S.fromJsonString(wireSchema)
|
|
28
|
+
const wireSchemaJson = S.fromJsonString(S.toCodecJson(wireSchema))
|
|
29
29
|
const encodePublish = S.encodeEffect(wireSchemaJson)
|
|
30
30
|
const drainW = S.Struct({ body: drainSchema, meta: QueueMeta })
|
|
31
|
-
const drainWJson = S.fromJsonString(drainW)
|
|
31
|
+
const drainWJson = S.fromJsonString(S.toCodecJson(drainW))
|
|
32
32
|
|
|
33
33
|
const parseDrain = flow(S.decodeUnknownEffect(drainWJson), Effect.orDie)
|
|
34
34
|
|
|
@@ -21,10 +21,10 @@ export function makeServiceBusQueue<
|
|
|
21
21
|
body: schema,
|
|
22
22
|
meta: QueueMeta
|
|
23
23
|
})
|
|
24
|
-
const wireSchemaJson = S.fromJsonString(wireSchema)
|
|
24
|
+
const wireSchemaJson = S.fromJsonString(S.toCodecJson(wireSchema))
|
|
25
25
|
const encodePublish = S.encodeEffect(wireSchemaJson)
|
|
26
26
|
const drainW = S.Struct({ body: drainSchema, meta: QueueMeta })
|
|
27
|
-
const drainWJson = S.fromJsonString(drainW)
|
|
27
|
+
const drainWJson = S.fromJsonString(S.toCodecJson(drainW))
|
|
28
28
|
const parseDrain = flow(S.decodeUnknownEffect(drainWJson), Effect.orDie)
|
|
29
29
|
|
|
30
30
|
return Effect.gen(function*() {
|
package/src/RequestContext.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Context, S } from "effect-app"
|
|
2
2
|
import { UserProfileId } from "effect-app/ids"
|
|
3
3
|
import { NonEmptyString255 } from "effect-app/Schema"
|
|
4
4
|
|
|
5
|
-
export const Locale = S.
|
|
5
|
+
export const Locale = S.Literals(["en", "de"])
|
|
6
6
|
export type Locale = typeof Locale.Type
|
|
7
7
|
|
|
8
|
-
export class LocaleRef extends
|
|
8
|
+
export class LocaleRef extends Context.Reference("Locale", { defaultValue: (): Locale => "en" }) {}
|
|
9
9
|
|
|
10
10
|
export class RequestContext extends S.ExtendedClass<
|
|
11
11
|
RequestContext,
|
|
@@ -23,7 +23,7 @@ export class RequestContext extends S.ExtendedClass<
|
|
|
23
23
|
/** @deprecated */
|
|
24
24
|
userProfile: S.optional(S.Struct({ sub: UserProfileId })) //
|
|
25
25
|
}) {
|
|
26
|
-
// static Tag =
|
|
26
|
+
// static Tag = Context.Tag<RequestContext>()
|
|
27
27
|
|
|
28
28
|
static toMonitoring(this: void, self: RequestContext) {
|
|
29
29
|
return {
|
package/src/RequestFiberSet.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { Effect, Fiber, FiberSet, Layer,
|
|
2
|
+
import { Context, Effect, Fiber, FiberSet, Layer, Option, type Tracer } from "effect-app"
|
|
3
3
|
import { reportRequestError, reportUnknownRequestError } from "./api/reportError.js"
|
|
4
4
|
import { InfraLogger } from "./logger.js"
|
|
5
5
|
|
|
@@ -8,8 +8,8 @@ const getRootParentSpan = Effect.gen(function*() {
|
|
|
8
8
|
Effect.catchTag("NoSuchElementError", () => Effect.succeed(null))
|
|
9
9
|
)
|
|
10
10
|
if (!span) return span
|
|
11
|
-
while (span._tag === "Span" && span.parent
|
|
12
|
-
span = span.parent
|
|
11
|
+
while (span._tag === "Span" && Option.isSome(span.parent)) {
|
|
12
|
+
span = span.parent.value
|
|
13
13
|
}
|
|
14
14
|
return span
|
|
15
15
|
})
|
|
@@ -92,7 +92,7 @@ const make = Effect.gen(function*() {
|
|
|
92
92
|
* Whenever you fork a fiber for a Request, and you want to prevent dependent services to close prematurely on interruption,
|
|
93
93
|
* like the ServiceBus Sender, you should register these fibers in this FiberSet.
|
|
94
94
|
*/
|
|
95
|
-
export class RequestFiberSet extends
|
|
95
|
+
export class RequestFiberSet extends Context.Service<RequestFiberSet>()("RequestFiberSet", { make }) {
|
|
96
96
|
static readonly Live = Layer.effect(this, this.make)
|
|
97
97
|
static readonly register = <A, E, R>(self: Effect.Effect<A, E, R>) =>
|
|
98
98
|
this.asEffect().pipe(Effect.andThen((_) => _.register(self)))
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { Data, Effect, Layer,
|
|
1
|
+
import { Context, Data, Effect, Layer, RequestResolver } from "effect-app"
|
|
2
|
+
import { dual } from "effect/Function"
|
|
3
|
+
import type * as Request from "effect/Request"
|
|
2
4
|
import { ContextMap } from "./service.js"
|
|
3
5
|
|
|
4
6
|
// TODO: we have to create a new contextmap on every request.
|
|
@@ -7,7 +9,7 @@ import { ContextMap } from "./service.js"
|
|
|
7
9
|
// we can call another start after startup. but it would be even better if we could Die on accessing rootmap
|
|
8
10
|
// we could also make the ContextMap optional, and when missing, issue a warning instead?
|
|
9
11
|
|
|
10
|
-
export class ContextMapContainer extends
|
|
12
|
+
export class ContextMapContainer extends Context.Reference("ContextMapContainer", {
|
|
11
13
|
defaultValue: (): ContextMap | "root" => "root"
|
|
12
14
|
}) {
|
|
13
15
|
static readonly layer = Layer.effect(this, ContextMap.make.pipe(Effect.map(ContextMap.of)))
|
|
@@ -18,3 +20,40 @@ export class ContextMapNotStartedError extends Data.TaggedError("ContextMapNotSt
|
|
|
18
20
|
export const getContextMap = ContextMapContainer.asEffect().pipe(
|
|
19
21
|
Effect.filterOrFail((_) => _ !== "root", () => new ContextMapNotStartedError())
|
|
20
22
|
)
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Uses the official `RequestResolver.withCache` internally,
|
|
26
|
+
* creating one cached resolver per ContextMap (i.e. per request).
|
|
27
|
+
* Uses a shared semaphore in the ContextMap to ensure safe single initialization.
|
|
28
|
+
*/
|
|
29
|
+
export const withRequestResolverCache: {
|
|
30
|
+
<A extends Request.Request<any, any>>(options: {
|
|
31
|
+
readonly capacity: number
|
|
32
|
+
readonly strategy?: "lru" | "fifo" | undefined
|
|
33
|
+
}): (
|
|
34
|
+
self: RequestResolver.RequestResolver<A>
|
|
35
|
+
) => Effect.Effect<RequestResolver.RequestResolver<A>, ContextMapNotStartedError>
|
|
36
|
+
<A extends Request.Request<any, any>>(
|
|
37
|
+
self: RequestResolver.RequestResolver<A>,
|
|
38
|
+
options: {
|
|
39
|
+
readonly capacity: number
|
|
40
|
+
readonly strategy?: "lru" | "fifo" | undefined
|
|
41
|
+
}
|
|
42
|
+
): Effect.Effect<RequestResolver.RequestResolver<A>, ContextMapNotStartedError>
|
|
43
|
+
} = dual(2, <A extends Request.Request<any, any>>(
|
|
44
|
+
self: RequestResolver.RequestResolver<A>,
|
|
45
|
+
options: {
|
|
46
|
+
readonly capacity: number
|
|
47
|
+
readonly strategy?: "lru" | "fifo" | undefined
|
|
48
|
+
}
|
|
49
|
+
): Effect.Effect<RequestResolver.RequestResolver<A>, ContextMapNotStartedError> => {
|
|
50
|
+
const cacheKey = Symbol()
|
|
51
|
+
return getContextMap.pipe(
|
|
52
|
+
Effect.flatMap((ctxMap) =>
|
|
53
|
+
ctxMap.getOrCreateStoreEffect(
|
|
54
|
+
cacheKey,
|
|
55
|
+
RequestResolver.withCache(self, options)
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
})
|