effect-app 4.0.0-beta.248 → 4.0.0-beta.249
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 +9 -1
- package/dist/Emailer.d.ts +51 -0
- package/dist/Emailer.d.ts.map +1 -0
- package/dist/Emailer.js +7 -0
- package/dist/Model/Repository/Registry.d.ts +21 -0
- package/dist/Model/Repository/Registry.d.ts.map +1 -0
- package/dist/Model/Repository/Registry.js +18 -0
- package/dist/Model/Repository/ext.d.ts +60 -0
- package/dist/Model/Repository/ext.d.ts.map +1 -0
- package/dist/Model/Repository/ext.js +122 -0
- package/dist/Model/Repository/internal/internal.d.ts +62 -0
- package/dist/Model/Repository/internal/internal.d.ts.map +1 -0
- package/dist/Model/Repository/internal/internal.js +413 -0
- package/dist/Model/Repository/legacy.d.ts +21 -0
- package/dist/Model/Repository/legacy.d.ts.map +1 -0
- package/dist/Model/Repository/legacy.js +2 -0
- package/dist/Model/Repository/makeRepo.d.ts +53 -0
- package/dist/Model/Repository/makeRepo.d.ts.map +1 -0
- package/dist/Model/Repository/makeRepo.js +27 -0
- package/dist/Model/Repository/service.d.ts +97 -0
- package/dist/Model/Repository/service.d.ts.map +1 -0
- package/dist/Model/Repository/service.js +2 -0
- package/dist/Model/Repository/validation.d.ts +71 -0
- package/dist/Model/Repository/validation.d.ts.map +1 -0
- package/dist/Model/Repository/validation.js +32 -0
- package/dist/Model/Repository.d.ts +7 -0
- package/dist/Model/Repository.d.ts.map +1 -0
- package/dist/Model/Repository.js +7 -0
- package/dist/Model/dsl.d.ts +33 -0
- package/dist/Model/dsl.d.ts.map +1 -0
- package/dist/Model/dsl.js +43 -0
- package/dist/Model/filter/filterApi.d.ts +30 -0
- package/dist/Model/filter/filterApi.d.ts.map +1 -0
- package/dist/Model/filter/filterApi.js +2 -0
- package/dist/Model/filter/types/errors.d.ts +29 -0
- package/dist/Model/filter/types/errors.d.ts.map +1 -0
- package/dist/Model/filter/types/errors.js +2 -0
- package/dist/Model/filter/types/fields.d.ts +15 -0
- package/dist/Model/filter/types/fields.d.ts.map +1 -0
- package/dist/Model/filter/types/fields.js +2 -0
- package/dist/Model/filter/types/path/common.d.ts +316 -0
- package/dist/Model/filter/types/path/common.d.ts.map +1 -0
- package/dist/Model/filter/types/path/common.js +2 -0
- package/dist/Model/filter/types/path/eager.d.ts +95 -0
- package/dist/Model/filter/types/path/eager.d.ts.map +1 -0
- package/dist/Model/filter/types/path/eager.js +31 -0
- package/dist/Model/filter/types/path/index.d.ts +4 -0
- package/dist/Model/filter/types/path/index.d.ts.map +1 -0
- package/dist/Model/filter/types/path/index.js +3 -0
- package/dist/Model/filter/types/utils.d.ts +79 -0
- package/dist/Model/filter/types/utils.d.ts.map +1 -0
- package/dist/Model/filter/types/utils.js +2 -0
- package/dist/Model/filter/types/validator.d.ts +30 -0
- package/dist/Model/filter/types/validator.d.ts.map +1 -0
- package/dist/Model/filter/types/validator.js +2 -0
- package/dist/Model/filter/types.d.ts +5 -0
- package/dist/Model/filter/types.d.ts.map +1 -0
- package/dist/Model/filter/types.js +7 -0
- package/dist/Model/query/dsl.d.ts +446 -0
- package/dist/Model/query/dsl.d.ts.map +1 -0
- package/dist/Model/query/dsl.js +342 -0
- package/dist/Model/query/new-kid-interpreter.d.ts +136 -0
- package/dist/Model/query/new-kid-interpreter.d.ts.map +1 -0
- package/dist/Model/query/new-kid-interpreter.js +336 -0
- package/dist/Model/query.d.ts +15 -0
- package/dist/Model/query.d.ts.map +1 -0
- package/dist/Model/query.js +3 -0
- package/dist/Model.d.ts +5 -0
- package/dist/Model.d.ts.map +1 -0
- package/dist/Model.js +5 -0
- package/dist/QueueMaker.d.ts +13 -0
- package/dist/QueueMaker.d.ts.map +1 -0
- package/dist/QueueMaker.js +4 -0
- package/dist/RequestContext.d.ts +103 -0
- package/dist/RequestContext.d.ts.map +1 -0
- package/dist/RequestContext.js +49 -0
- package/dist/Store.d.ts +147 -0
- package/dist/Store.d.ts.map +1 -0
- package/dist/Store.js +95 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/runtime.d.ts +19 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +40 -0
- package/dist/toast.d.ts +51 -0
- package/dist/toast.d.ts.map +1 -0
- package/dist/toast.js +34 -0
- package/dist/withToast.d.ts +30 -0
- package/dist/withToast.d.ts.map +1 -0
- package/dist/withToast.js +64 -0
- package/package.json +113 -1
- package/src/Emailer.ts +51 -0
- package/src/Model/Repository/Registry.ts +34 -0
- package/src/Model/Repository/ext.ts +375 -0
- package/src/Model/Repository/internal/internal.ts +708 -0
- package/src/Model/Repository/legacy.ts +29 -0
- package/src/Model/Repository/makeRepo.ts +144 -0
- package/src/Model/Repository/service.ts +639 -0
- package/src/Model/Repository/validation.ts +31 -0
- package/src/Model/Repository.ts +6 -0
- package/src/Model/dsl.ts +129 -0
- package/src/Model/filter/filterApi.ts +60 -0
- package/src/Model/filter/types/errors.ts +47 -0
- package/src/Model/filter/types/fields.ts +50 -0
- package/src/Model/filter/types/path/common.ts +404 -0
- package/src/Model/filter/types/path/eager.ts +297 -0
- package/src/Model/filter/types/path/index.ts +4 -0
- package/src/Model/filter/types/utils.ts +128 -0
- package/src/Model/filter/types/validator.ts +46 -0
- package/src/Model/filter/types.ts +6 -0
- package/src/Model/query/dsl.ts +2546 -0
- package/src/Model/query/new-kid-interpreter.ts +484 -0
- package/src/Model/query.ts +13 -0
- package/src/Model.ts +4 -0
- package/src/QueueMaker.ts +19 -0
- package/src/RequestContext.ts +62 -0
- package/src/Store.ts +243 -0
- package/src/index.ts +2 -0
- package/src/runtime.ts +56 -0
- package/src/toast.ts +54 -0
- package/src/withToast.ts +133 -0
- package/test/dist/rpc-dynamic-middleware.test.d.ts.map +1 -0
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
|
|
3
|
+
import * as Equivalence from "effect/Equivalence"
|
|
4
|
+
import { flow, pipe } from "effect/Function"
|
|
5
|
+
import * as Pipeable from "effect/Pipeable"
|
|
6
|
+
import * as PubSub from "effect/PubSub"
|
|
7
|
+
import * as Result from "effect/Result"
|
|
8
|
+
import * as SchemaAST from "effect/SchemaAST"
|
|
9
|
+
import * as Unify from "effect/Unify"
|
|
10
|
+
import * as Array from "../../../Array.js"
|
|
11
|
+
import type { NonEmptyReadonlyArray } from "../../../Array.js"
|
|
12
|
+
import { toNonEmptyArray } from "../../../Array.js"
|
|
13
|
+
import * as Chunk from "../../../Chunk.js"
|
|
14
|
+
import { NotFoundError } from "../../../client/errors.js"
|
|
15
|
+
import * as Context from "../../../Context.js"
|
|
16
|
+
import * as Effect from "../../../Effect.js"
|
|
17
|
+
import { flatMapOption } from "../../../Effect.js"
|
|
18
|
+
import * as Option from "../../../Option.js"
|
|
19
|
+
import * as S from "../../../Schema.js"
|
|
20
|
+
import { type Codec, NonNegativeInt } from "../../../Schema.js"
|
|
21
|
+
import { ContextMap, type FilterArgs, type PersistenceModelType, type StoreConfig, StoreMaker } from "../../../Store.js"
|
|
22
|
+
import type { FieldValues } from "../../filter/types.js"
|
|
23
|
+
import * as Q from "../../query.js"
|
|
24
|
+
import type { Repository } from "../service.js"
|
|
25
|
+
import { ValidationError, ValidationResult } from "../validation.js"
|
|
26
|
+
|
|
27
|
+
const dedupe = Array.dedupeWith(Equivalence.String)
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A base implementation to create a repository.
|
|
31
|
+
*/
|
|
32
|
+
export function makeRepoInternal<
|
|
33
|
+
Evt = never
|
|
34
|
+
>() {
|
|
35
|
+
return <
|
|
36
|
+
ItemType extends string,
|
|
37
|
+
R,
|
|
38
|
+
Encoded extends FieldValues,
|
|
39
|
+
T,
|
|
40
|
+
IdKey extends keyof T & keyof Encoded
|
|
41
|
+
>(
|
|
42
|
+
name: ItemType,
|
|
43
|
+
schema: S.Codec<T, Encoded, R>,
|
|
44
|
+
mapFrom: (pm: Encoded) => Encoded,
|
|
45
|
+
mapTo: (e: Encoded, etag: string | undefined) => PersistenceModelType<Encoded>,
|
|
46
|
+
idKey: IdKey
|
|
47
|
+
) => {
|
|
48
|
+
type PM = PersistenceModelType<Encoded>
|
|
49
|
+
function mapToPersistenceModel(
|
|
50
|
+
e: Encoded,
|
|
51
|
+
getEtag: (id: string) => string | undefined
|
|
52
|
+
): PM {
|
|
53
|
+
return mapTo(e, getEtag(e[idKey]))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function mapReverse(
|
|
57
|
+
{ _etag, ...e }: PM,
|
|
58
|
+
setEtag: (id: string, eTag: string | undefined) => void
|
|
59
|
+
): Encoded {
|
|
60
|
+
setEtag((e as any)[idKey], _etag)
|
|
61
|
+
return mapFrom(e as unknown as Encoded)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const mkStore = makeStore<Encoded>()(name, schema, mapTo, idKey)
|
|
65
|
+
|
|
66
|
+
function make<RInitial = never, E = never, RPublish = never, RCtx = never>(
|
|
67
|
+
args: [Evt] extends [never] ? {
|
|
68
|
+
schemaContext?: Context.Context<RCtx>
|
|
69
|
+
makeInitial?: Effect.Effect<readonly T[], E, RInitial> | undefined
|
|
70
|
+
config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
|
|
71
|
+
partitionValue?: (e?: Encoded) => string
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
: {
|
|
75
|
+
schemaContext?: Context.Context<RCtx>
|
|
76
|
+
publishEvents: (evt: NonEmptyReadonlyArray<Evt>) => Effect.Effect<void, never, RPublish>
|
|
77
|
+
makeInitial?: Effect.Effect<readonly T[], E, RInitial> | undefined
|
|
78
|
+
config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
|
|
79
|
+
partitionValue?: (e?: Encoded) => string
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
) {
|
|
83
|
+
return Effect
|
|
84
|
+
.gen(function*() {
|
|
85
|
+
const rctx: Context.Context<RCtx> = args.schemaContext ?? Context.empty() as any
|
|
86
|
+
const provideRctx = Effect.provide(rctx)
|
|
87
|
+
const encodeMany = flow(
|
|
88
|
+
S.encodeEffect(S.Array(schema)),
|
|
89
|
+
provideRctx,
|
|
90
|
+
Effect.withSpan("encodeMany", { attributes: { "app.entity": name } }, { captureStackTrace: false })
|
|
91
|
+
)
|
|
92
|
+
const decode = flow(S.decodeEffectConcurrently(schema), provideRctx)
|
|
93
|
+
const decodeMany = flow(
|
|
94
|
+
S.decodeEffectConcurrently(S.Array(schema)),
|
|
95
|
+
provideRctx
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
const store = yield* mkStore(args.makeInitial, args.config)
|
|
99
|
+
const etags = new Map<string, string | undefined>()
|
|
100
|
+
const cms = Effect.serviceOption(ContextMap).pipe(
|
|
101
|
+
Effect.map(Option.match({
|
|
102
|
+
onNone: () => ({
|
|
103
|
+
get: (id: string) => etags.get(`${name}.${id}`),
|
|
104
|
+
set: (id: string, etag: string | undefined) => {
|
|
105
|
+
const key = `${name}.${id}`
|
|
106
|
+
if (etag === undefined) {
|
|
107
|
+
etags.delete(key)
|
|
108
|
+
} else {
|
|
109
|
+
etags.set(key, etag)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}),
|
|
113
|
+
onSome: (contextMap) => ({
|
|
114
|
+
get: (id: string) => contextMap.get(`${name}.${id}`),
|
|
115
|
+
set: (id: string, etag: string | undefined) => contextMap.set(`${name}.${id}`, etag)
|
|
116
|
+
})
|
|
117
|
+
}))
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
const pub = "publishEvents" in args
|
|
121
|
+
? args.publishEvents
|
|
122
|
+
: () => Effect.void
|
|
123
|
+
const changeFeed = yield* PubSub.unbounded<[T[], "save" | "remove"]>()
|
|
124
|
+
|
|
125
|
+
const allE = cms
|
|
126
|
+
.pipe(Effect.flatMap((cm) => Effect.map(store.all, (_) => _.map((_) => mapReverse(_, cm.set)))))
|
|
127
|
+
|
|
128
|
+
const all = Effect
|
|
129
|
+
.flatMap(
|
|
130
|
+
allE,
|
|
131
|
+
(_) => decodeMany(_).pipe(Effect.orDie)
|
|
132
|
+
)
|
|
133
|
+
.pipe(
|
|
134
|
+
Effect.map((_) => _ as T[]),
|
|
135
|
+
Effect.withSpan("Repository.all", {
|
|
136
|
+
kind: "client",
|
|
137
|
+
attributes: { "app.entity": name }
|
|
138
|
+
}, { captureStackTrace: false })
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
const fieldsSchema = schema as unknown as { fields: any }
|
|
142
|
+
// assumes the id field never needs a service...
|
|
143
|
+
const i = ("fields" in fieldsSchema ? S.Struct(fieldsSchema["fields"]) as unknown as typeof schema : schema)
|
|
144
|
+
.pipe((_) => {
|
|
145
|
+
let ast = _.ast
|
|
146
|
+
if (ast._tag === "Declaration") ast = ast.typeParameters[0]!
|
|
147
|
+
|
|
148
|
+
const pickIdFromAst = (a: SchemaAST.AST) => {
|
|
149
|
+
// Unwrap Declaration (e.g. TaggedClass) to get the underlying Objects AST
|
|
150
|
+
let inner = a
|
|
151
|
+
if (inner._tag === "Declaration") inner = inner.typeParameters[0]!
|
|
152
|
+
// Pick from the original AST to preserve the full encoding chain (e.g. decodeTo transformations).
|
|
153
|
+
// Using toEncoded would lose transformation info needed to encode Type -> Encoded.
|
|
154
|
+
if (SchemaAST.isObjects(inner)) {
|
|
155
|
+
const field = inner.propertySignatures.find((_) => _.name === idKey)
|
|
156
|
+
if (field) {
|
|
157
|
+
return S.Struct({ [idKey]: S.make(field.type) }) as unknown as Codec<T, Encoded>
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return S.make(a) as unknown as Codec<T, Encoded>
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return ast._tag === "Union"
|
|
164
|
+
// we need to get the Objects (TypeLiteral), in case of class it has encoding chain...
|
|
165
|
+
? S.Union(
|
|
166
|
+
ast.types.map((_) => pickIdFromAst(_))
|
|
167
|
+
)
|
|
168
|
+
: pickIdFromAst(ast)
|
|
169
|
+
})
|
|
170
|
+
const encodeId = flow(S.encodeEffect(i), provideRctx)
|
|
171
|
+
const encodeIdOnly = (id: string) =>
|
|
172
|
+
encodeId({ [idKey]: id } as any).pipe(
|
|
173
|
+
Effect.map((_: Record<string, unknown>) => _[idKey as string] as Encoded[IdKey])
|
|
174
|
+
)
|
|
175
|
+
const findEId = Effect.fnUntraced(function*(id: Encoded[IdKey]) {
|
|
176
|
+
yield* Effect.annotateCurrentSpan({ "app.entity.id": id })
|
|
177
|
+
|
|
178
|
+
return yield* Effect.flatMap(
|
|
179
|
+
store.find(id),
|
|
180
|
+
(item) =>
|
|
181
|
+
Effect.gen(function*() {
|
|
182
|
+
const { set } = yield* cms
|
|
183
|
+
return item.pipe(Option.map((_) => mapReverse(_, set)))
|
|
184
|
+
})
|
|
185
|
+
)
|
|
186
|
+
})
|
|
187
|
+
// TODO: select the particular field, instead of as struct
|
|
188
|
+
const findE = Effect.fnUntraced(function*(id: T[IdKey]) {
|
|
189
|
+
yield* Effect.annotateCurrentSpan({ "app.entity.id": id })
|
|
190
|
+
|
|
191
|
+
return yield* pipe(
|
|
192
|
+
encodeId({ [idKey]: id } as any),
|
|
193
|
+
Effect.orDie,
|
|
194
|
+
Effect.map((_) => (_ as any)[idKey]),
|
|
195
|
+
Effect.flatMap(findEId)
|
|
196
|
+
)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
const find = Effect.fn("Repository.find", {
|
|
200
|
+
kind: "client",
|
|
201
|
+
attributes: { "app.entity": name }
|
|
202
|
+
})(function*(id: T[IdKey]) {
|
|
203
|
+
yield* Effect.annotateCurrentSpan({ "app.entity.id": id })
|
|
204
|
+
return yield* flatMapOption(findE(id), (_) => Effect.orDie(decode(_)))
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
const saveAllE = (a: Iterable<Encoded>) =>
|
|
208
|
+
flatMapOption(
|
|
209
|
+
Effect
|
|
210
|
+
.sync(() => toNonEmptyArray([...a])),
|
|
211
|
+
(a) =>
|
|
212
|
+
Effect.gen(function*() {
|
|
213
|
+
const { get, set } = yield* cms
|
|
214
|
+
const items = a.map((_) => mapToPersistenceModel(_, get))
|
|
215
|
+
const ret = yield* store.batchSet(items)
|
|
216
|
+
ret.forEach((_) => set(_[idKey], _._etag))
|
|
217
|
+
})
|
|
218
|
+
)
|
|
219
|
+
.pipe(Effect.asVoid)
|
|
220
|
+
|
|
221
|
+
const saveAll = (a: Iterable<T>) =>
|
|
222
|
+
encodeMany(Array.fromIterable(a))
|
|
223
|
+
.pipe(
|
|
224
|
+
Effect.orDie,
|
|
225
|
+
Effect.andThen(saveAllE)
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
const saveAndPublish = Effect.fn("Repository.saveAndPublish", { attributes: { "app.entity": name } })(
|
|
229
|
+
function*(items: Iterable<T>, events: Iterable<Evt> = []) {
|
|
230
|
+
const it = Chunk.fromIterable(items)
|
|
231
|
+
const evts = [...events]
|
|
232
|
+
yield* Effect.annotateCurrentSpan({
|
|
233
|
+
"app.entity.ids": Chunk.map(it, (_) => _[idKey]),
|
|
234
|
+
"app.event.count": evts.length
|
|
235
|
+
})
|
|
236
|
+
return yield* saveAll(it)
|
|
237
|
+
.pipe(
|
|
238
|
+
Effect.andThen(Effect.sync(() => toNonEmptyArray(evts))),
|
|
239
|
+
// TODO: for full consistency the events should be stored within the same database transaction, and then picked up.
|
|
240
|
+
(_) => flatMapOption(_, pub),
|
|
241
|
+
Effect.andThen(PubSub.publish(changeFeed, [Chunk.toArray(it), "save"] as [T[], "save" | "remove"])),
|
|
242
|
+
Effect.asVoid
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
const removeAndPublish = Effect.fn("Repository.removeAndPublish", { attributes: { "app.entity": name } })(
|
|
248
|
+
function*(a: Iterable<T>, events: Iterable<Evt> = []) {
|
|
249
|
+
const { set } = yield* cms
|
|
250
|
+
const it = [...a]
|
|
251
|
+
const evts = [...events]
|
|
252
|
+
yield* Effect.annotateCurrentSpan({
|
|
253
|
+
"app.entity.ids": it.map((_) => _[idKey]),
|
|
254
|
+
"app.event.count": evts.length
|
|
255
|
+
})
|
|
256
|
+
const items = yield* encodeMany(it).pipe(Effect.orDie)
|
|
257
|
+
if (Array.isReadonlyArrayNonEmpty(items)) {
|
|
258
|
+
yield* store.batchRemove(
|
|
259
|
+
items.map((_) => (_[idKey])),
|
|
260
|
+
args.config?.partitionValue?.(items[0])
|
|
261
|
+
)
|
|
262
|
+
for (const e of items) {
|
|
263
|
+
set(e[idKey], undefined)
|
|
264
|
+
}
|
|
265
|
+
yield* Effect
|
|
266
|
+
.sync(() => toNonEmptyArray(evts))
|
|
267
|
+
// TODO: for full consistency the events should be stored within the same database transaction, and then picked up.
|
|
268
|
+
.pipe((_) => flatMapOption(_, pub))
|
|
269
|
+
|
|
270
|
+
yield* PubSub.publish(changeFeed, [it, "remove"] as [T[], "save" | "remove"])
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
const removeById = Effect.fn("Repository.removeById", { attributes: { "app.entity": name } })(
|
|
276
|
+
function*(idOrIds: T[IdKey] | ReadonlyArray<T[IdKey]>) {
|
|
277
|
+
const ids = globalThis.Array.isArray(idOrIds)
|
|
278
|
+
? idOrIds as readonly T[IdKey][]
|
|
279
|
+
: [idOrIds as T[IdKey]]
|
|
280
|
+
if (!Array.isReadonlyArrayNonEmpty(ids)) {
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
const { set } = yield* cms
|
|
284
|
+
const eids = yield* Effect.forEach(ids, (_) => encodeIdOnly(_ as any)).pipe(Effect.orDie)
|
|
285
|
+
yield* Effect.annotateCurrentSpan({ "app.entity.ids": eids })
|
|
286
|
+
yield* store.batchRemove(eids)
|
|
287
|
+
for (const id of eids) {
|
|
288
|
+
set(id, undefined)
|
|
289
|
+
}
|
|
290
|
+
yield* PubSub.publish(changeFeed, [[], "remove"] as [T[], "save" | "remove"])
|
|
291
|
+
}
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
const parseMany = Effect.fn("parseMany", {
|
|
295
|
+
attributes: { "app.entity": name, "app.query.mode": "transform" }
|
|
296
|
+
})(
|
|
297
|
+
function*(items: readonly PM[]) {
|
|
298
|
+
const cm = yield* cms
|
|
299
|
+
return yield* decodeMany(items.map((_) => mapReverse(_, cm.set))).pipe(Effect.orDie)
|
|
300
|
+
}
|
|
301
|
+
)
|
|
302
|
+
const decodeManyCache = new WeakMap<
|
|
303
|
+
S.Codec<any, any, any>,
|
|
304
|
+
(i: readonly any[]) => Effect.Effect<any, any, any>
|
|
305
|
+
>()
|
|
306
|
+
const getDecodeMany = (s: S.Codec<any, Encoded, any>) => {
|
|
307
|
+
let dec = decodeManyCache.get(s)
|
|
308
|
+
if (!dec) {
|
|
309
|
+
dec = S.decodeEffectConcurrently(S.Array(s))
|
|
310
|
+
decodeManyCache.set(s, dec)
|
|
311
|
+
}
|
|
312
|
+
return dec
|
|
313
|
+
}
|
|
314
|
+
const parseMany2 = Effect.fn("parseMany", {
|
|
315
|
+
attributes: { "app.entity": name, "app.query.mode": "transform" }
|
|
316
|
+
})(
|
|
317
|
+
function*<A, R>(items: readonly PM[], schema: S.Codec<A, Encoded, R>) {
|
|
318
|
+
const cm = yield* cms
|
|
319
|
+
return yield* getDecodeMany(schema)(items.map((_) => mapReverse(_, cm.set))).pipe(Effect.orDie)
|
|
320
|
+
}
|
|
321
|
+
)
|
|
322
|
+
const filter = <U extends keyof Encoded = keyof Encoded>(args: FilterArgs<Encoded, U>) =>
|
|
323
|
+
store
|
|
324
|
+
.filter(
|
|
325
|
+
// always enforce id and _etag because they are system fields, required for etag tracking etc
|
|
326
|
+
{
|
|
327
|
+
...args,
|
|
328
|
+
select: args.select
|
|
329
|
+
? dedupe([...args.select, idKey, "_etag" as any])
|
|
330
|
+
: undefined
|
|
331
|
+
} as typeof args
|
|
332
|
+
)
|
|
333
|
+
.pipe(
|
|
334
|
+
Effect.tap((items) =>
|
|
335
|
+
Effect.map(cms, ({ set }) => items.forEach((_) => set((_ as Encoded)[idKey], (_ as PM)._etag)))
|
|
336
|
+
)
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
// TODO: For raw we should use S.from, and drop the R...
|
|
340
|
+
const query: {
|
|
341
|
+
<A, R, From extends FieldValues>(
|
|
342
|
+
q: Q.QueryProjection<Encoded extends From ? From : never, A, R>
|
|
343
|
+
): Effect.Effect<readonly A[], S.SchemaError, Exclude<R, RCtx>>
|
|
344
|
+
<A, R, EncodedRefined extends Encoded = Encoded>(
|
|
345
|
+
q: Q.QAll<NoInfer<Encoded>, NoInfer<EncodedRefined>, A, R>
|
|
346
|
+
): Effect.Effect<readonly A[], never, Exclude<R, RCtx>>
|
|
347
|
+
} = (<A, R, EncodedRefined extends Encoded = Encoded>(q: Q.QAll<Encoded, EncodedRefined, A, R>) => {
|
|
348
|
+
const a = Q.toFilter(q, schema)
|
|
349
|
+
// Mode dispatch — see `Q.project` JSDoc for the contract:
|
|
350
|
+
// aggregate: GROUP BY + aggregate functions at DB level; decode raw rows with schema; SchemaError surfaces.
|
|
351
|
+
// project : decode raw encoded rows with schema; no PM reverse-mapping; SchemaError surfaces.
|
|
352
|
+
// collect : same as project, but schema yields Option and None rows are dropped.
|
|
353
|
+
// transform: PM reverse-map (re-inject _etag/PM state from cms cache) then decode; orDie.
|
|
354
|
+
const eff = a.mode === "aggregate"
|
|
355
|
+
? store
|
|
356
|
+
// `a.select` contains `{ key, aggregate }` items not expressible in FilterFunc<Encoded, U>'s
|
|
357
|
+
// `U extends keyof Encoded` generic. Cast is unavoidable until FilterFunc supports aggregate mode.
|
|
358
|
+
.filter(a as any)
|
|
359
|
+
// Decode raw aggregate rows directly — no PM reverse-mapping, no id/_etag needed.
|
|
360
|
+
.pipe(
|
|
361
|
+
Effect.andThen(
|
|
362
|
+
flow(
|
|
363
|
+
S.decodeEffectConcurrently(S.Array(a.schema ?? schema)),
|
|
364
|
+
provideRctx,
|
|
365
|
+
Effect.withSpan("parseMany", {
|
|
366
|
+
attributes: { "app.entity": name, "app.query.mode": "aggregate" }
|
|
367
|
+
})
|
|
368
|
+
)
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
: a.mode === "project"
|
|
372
|
+
? filter(a)
|
|
373
|
+
// TODO: mapFrom but need to support per field and dependencies
|
|
374
|
+
.pipe(
|
|
375
|
+
Effect.andThen(
|
|
376
|
+
flow(
|
|
377
|
+
S.decodeEffectConcurrently(S.Array(a.schema ?? schema)),
|
|
378
|
+
provideRctx,
|
|
379
|
+
Effect.withSpan("parseMany", {
|
|
380
|
+
attributes: { "app.entity": name, "app.query.mode": "project" }
|
|
381
|
+
})
|
|
382
|
+
)
|
|
383
|
+
)
|
|
384
|
+
)
|
|
385
|
+
: a.mode === "collect"
|
|
386
|
+
? filter(a)
|
|
387
|
+
// TODO: mapFrom but need to support per field and dependencies
|
|
388
|
+
.pipe(
|
|
389
|
+
Effect.flatMap(flow(
|
|
390
|
+
S.decodeEffectConcurrently(S.Array(a.schema)),
|
|
391
|
+
Effect.map(Array.getSomes),
|
|
392
|
+
provideRctx,
|
|
393
|
+
Effect.withSpan("parseMany", {
|
|
394
|
+
attributes: { "app.entity": name, "app.query.mode": "collect" }
|
|
395
|
+
})
|
|
396
|
+
))
|
|
397
|
+
)
|
|
398
|
+
: Effect.flatMap(
|
|
399
|
+
filter(a),
|
|
400
|
+
(_) =>
|
|
401
|
+
Unify.unify(
|
|
402
|
+
a.schema
|
|
403
|
+
// TODO: partial may not match?
|
|
404
|
+
? parseMany2(_ as any, a.schema as any)
|
|
405
|
+
: parseMany(_ as any)
|
|
406
|
+
)
|
|
407
|
+
)
|
|
408
|
+
return pipe(
|
|
409
|
+
a.ttype === "one"
|
|
410
|
+
? Effect.flatMap(
|
|
411
|
+
eff,
|
|
412
|
+
flow(
|
|
413
|
+
Array.head,
|
|
414
|
+
Option.match({
|
|
415
|
+
onNone: () => Effect.fail(new NotFoundError({ id: "query", /* TODO */ type: name })),
|
|
416
|
+
onSome: Effect.succeed
|
|
417
|
+
})
|
|
418
|
+
)
|
|
419
|
+
)
|
|
420
|
+
: a.ttype === "count"
|
|
421
|
+
? Effect
|
|
422
|
+
.map(eff, (_) => NonNegativeInt(_.length))
|
|
423
|
+
.pipe(Effect.orDie)
|
|
424
|
+
: eff,
|
|
425
|
+
Effect.tap((r) =>
|
|
426
|
+
Effect.annotateCurrentSpan({
|
|
427
|
+
"app.query.ttype": a.ttype,
|
|
428
|
+
"app.query.mode": a.mode,
|
|
429
|
+
"db.response.returned_rows": Array.isArray(r) ? r.length : 1
|
|
430
|
+
})
|
|
431
|
+
),
|
|
432
|
+
Effect.withSpan("Repository.query", {
|
|
433
|
+
kind: "client",
|
|
434
|
+
attributes: { "app.entity": name }
|
|
435
|
+
}, { captureStackTrace: false })
|
|
436
|
+
)
|
|
437
|
+
}) as any
|
|
438
|
+
|
|
439
|
+
const validateSample = Effect.fn("Repository.validateSample", { attributes: { "app.entity": name } })(
|
|
440
|
+
function*(options?: {
|
|
441
|
+
percentage?: number
|
|
442
|
+
maxItems?: number
|
|
443
|
+
}) {
|
|
444
|
+
const percentage = options?.percentage ?? 0.1 // default 10%
|
|
445
|
+
const maxItems = options?.maxItems
|
|
446
|
+
|
|
447
|
+
// 1. get all IDs with projection (bypasses main schema decode)
|
|
448
|
+
const allIds = yield* store
|
|
449
|
+
.filter({
|
|
450
|
+
t: null as unknown as Encoded,
|
|
451
|
+
select: [idKey as keyof Encoded]
|
|
452
|
+
})
|
|
453
|
+
.pipe(Effect.withSpan("Repository.filter", {
|
|
454
|
+
kind: "client",
|
|
455
|
+
attributes: { "app.entity": name }
|
|
456
|
+
}, { captureStackTrace: false }))
|
|
457
|
+
|
|
458
|
+
// 2. random subset
|
|
459
|
+
const shuffled = [...allIds].sort(() => Math.random() - 0.5)
|
|
460
|
+
const sampleSize = Math.min(
|
|
461
|
+
maxItems ?? Infinity,
|
|
462
|
+
Math.ceil(allIds.length * percentage)
|
|
463
|
+
)
|
|
464
|
+
const sample = shuffled.slice(0, sampleSize)
|
|
465
|
+
|
|
466
|
+
// 3. validate each item
|
|
467
|
+
const errors: ValidationError[] = []
|
|
468
|
+
|
|
469
|
+
for (const item of sample) {
|
|
470
|
+
const id = item[idKey]
|
|
471
|
+
const rawResult = yield* store.find(id).pipe(
|
|
472
|
+
Effect.withSpan("Repository.find", {
|
|
473
|
+
kind: "client",
|
|
474
|
+
attributes: { "app.entity": name, "app.entity.id": id }
|
|
475
|
+
}, { captureStackTrace: false })
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
if (Option.isNone(rawResult)) continue
|
|
479
|
+
|
|
480
|
+
const rawData = rawResult.value as Encoded
|
|
481
|
+
const jitMResult = mapFrom(rawData) // apply jitM
|
|
482
|
+
|
|
483
|
+
const decodeResult = yield* S.decodeEffectConcurrently(schema)(jitMResult).pipe(
|
|
484
|
+
Effect.result,
|
|
485
|
+
provideRctx
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
if (Result.isFailure(decodeResult)) {
|
|
489
|
+
errors.push(
|
|
490
|
+
ValidationError.make({
|
|
491
|
+
id,
|
|
492
|
+
rawData,
|
|
493
|
+
jitMResult,
|
|
494
|
+
error: decodeResult.failure
|
|
495
|
+
})
|
|
496
|
+
)
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return ValidationResult.make({
|
|
501
|
+
total: NonNegativeInt(allIds.length),
|
|
502
|
+
sampled: NonNegativeInt(sample.length),
|
|
503
|
+
valid: NonNegativeInt(sample.length - errors.length),
|
|
504
|
+
errors
|
|
505
|
+
})
|
|
506
|
+
}
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
const r = {
|
|
510
|
+
changeFeed,
|
|
511
|
+
itemType: name,
|
|
512
|
+
idKey,
|
|
513
|
+
find,
|
|
514
|
+
all,
|
|
515
|
+
saveAndPublish,
|
|
516
|
+
removeAndPublish,
|
|
517
|
+
removeById,
|
|
518
|
+
seedNamespace: (namespace: string) => store.seedNamespace(namespace),
|
|
519
|
+
validateSample,
|
|
520
|
+
queryRaw<A, Out, QR>(schema: S.Codec<A, Out, QR>, q: Q.RawQuery<Encoded, Out>) {
|
|
521
|
+
const dec = S.decodeEffectConcurrently(S.Array(schema))
|
|
522
|
+
return store.queryRaw(q).pipe(
|
|
523
|
+
Effect.flatMap(dec),
|
|
524
|
+
Effect.withSpan("Repository.queryRaw", {
|
|
525
|
+
kind: "client",
|
|
526
|
+
attributes: { "app.entity": name }
|
|
527
|
+
}, { captureStackTrace: false })
|
|
528
|
+
)
|
|
529
|
+
},
|
|
530
|
+
query(q: any) {
|
|
531
|
+
// eslint-disable-next-line prefer-rest-params
|
|
532
|
+
return query(typeof q === "function" ? Pipeable.pipeArguments(Q.make(), arguments) : q) as any
|
|
533
|
+
},
|
|
534
|
+
/**
|
|
535
|
+
* @internal
|
|
536
|
+
*/
|
|
537
|
+
mapped: <A, R>(schema: S.Codec<A, any, R>) => {
|
|
538
|
+
const dec = S.decodeEffectConcurrently(schema)
|
|
539
|
+
const encMany = S.encodeEffect(S.Array(schema))
|
|
540
|
+
const decMany = S.decodeEffectConcurrently(S.Array(schema))
|
|
541
|
+
const spanAttrs = { kind: "client" as const, attributes: { "app.entity": name } }
|
|
542
|
+
return {
|
|
543
|
+
all: allE.pipe(
|
|
544
|
+
Effect.flatMap(decMany),
|
|
545
|
+
Effect.map((_) => _ as any[]),
|
|
546
|
+
Effect.withSpan("Repository.mapped.all", spanAttrs, { captureStackTrace: false })
|
|
547
|
+
),
|
|
548
|
+
find: (id: T[IdKey]) =>
|
|
549
|
+
flatMapOption(findE(id), dec).pipe(
|
|
550
|
+
Effect.withSpan("Repository.mapped.find", {
|
|
551
|
+
...spanAttrs,
|
|
552
|
+
attributes: { ...spanAttrs.attributes, "app.entity.id": id }
|
|
553
|
+
}, { captureStackTrace: false })
|
|
554
|
+
),
|
|
555
|
+
// query: (q: any) => {
|
|
556
|
+
// const a = Q.toFilter(q)
|
|
557
|
+
|
|
558
|
+
// return filter(a)
|
|
559
|
+
// .pipe(
|
|
560
|
+
// Effect.flatMap(decMany),
|
|
561
|
+
// Effect.map((_) => _ as any[]),
|
|
562
|
+
// Effect.withSpan("Repository.mapped.query [effect-app/infra]", {
|
|
563
|
+
// captureStackTrace: false,
|
|
564
|
+
// attributes: {
|
|
565
|
+
// "repository.model_name": name,
|
|
566
|
+
// query: { ...a, schema: a.schema ? "__SCHEMA__" : a.schema, filter: a.filter.build() }
|
|
567
|
+
// }
|
|
568
|
+
// })
|
|
569
|
+
// )
|
|
570
|
+
// },
|
|
571
|
+
save: (...xes: any[]) =>
|
|
572
|
+
Effect.flatMap(encMany(xes), (_) => saveAllE(_)).pipe(
|
|
573
|
+
Effect.withSpan("Repository.mapped.save", spanAttrs, { captureStackTrace: false })
|
|
574
|
+
)
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return r as Repository<T, Encoded, Evt, ItemType, IdKey, Exclude<R, RCtx>, RPublish, RCtx>
|
|
579
|
+
})
|
|
580
|
+
.pipe(Effect
|
|
581
|
+
// .withSpan("Repository.make [effect-app/infra]", { attributes: { "repository.model_name": name } })
|
|
582
|
+
.withLogSpan("Repository.make: " + name))
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return {
|
|
586
|
+
make,
|
|
587
|
+
Q: Q.make<Encoded>()
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const pluralize = (s: string) =>
|
|
593
|
+
s.endsWith("s")
|
|
594
|
+
? s + "es"
|
|
595
|
+
: s.endsWith("y")
|
|
596
|
+
? s.substring(0, s.length - 1) + "ies"
|
|
597
|
+
: s + "s"
|
|
598
|
+
|
|
599
|
+
export function makeStore<Encoded extends FieldValues>() {
|
|
600
|
+
return <
|
|
601
|
+
ItemType extends string,
|
|
602
|
+
R,
|
|
603
|
+
E,
|
|
604
|
+
T,
|
|
605
|
+
IdKey extends keyof Encoded
|
|
606
|
+
>(
|
|
607
|
+
name: ItemType,
|
|
608
|
+
schema: S.Codec<T, E, R>,
|
|
609
|
+
mapTo: (e: E, etag: string | undefined) => Encoded,
|
|
610
|
+
idKey: IdKey
|
|
611
|
+
) => {
|
|
612
|
+
function makeStore<RInitial = never, EInitial = never>(
|
|
613
|
+
makeInitial?: Effect.Effect<readonly T[], EInitial, RInitial>,
|
|
614
|
+
config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
|
|
615
|
+
partitionValue?: (e?: Encoded) => string
|
|
616
|
+
}
|
|
617
|
+
) {
|
|
618
|
+
function encodeToEncoded() {
|
|
619
|
+
const getEtag = () => undefined
|
|
620
|
+
return (t: T) =>
|
|
621
|
+
S.encodeEffect(schema)(t).pipe(
|
|
622
|
+
Effect.orDie,
|
|
623
|
+
Effect.map((_) => mapToPersistenceModel(_, getEtag))
|
|
624
|
+
)
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function mapToPersistenceModel(
|
|
628
|
+
e: E,
|
|
629
|
+
getEtag: (id: string) => string | undefined
|
|
630
|
+
): Encoded {
|
|
631
|
+
return mapTo(e, getEtag((e as any)[idKey] as string))
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return Effect.gen(function*() {
|
|
635
|
+
const { make } = yield* StoreMaker
|
|
636
|
+
|
|
637
|
+
const store = yield* make<IdKey, Encoded, RInitial | R, EInitial>(
|
|
638
|
+
pluralize(name),
|
|
639
|
+
idKey,
|
|
640
|
+
makeInitial
|
|
641
|
+
? makeInitial
|
|
642
|
+
.pipe(
|
|
643
|
+
Effect.flatMap(Effect.forEach(encodeToEncoded())),
|
|
644
|
+
Effect.withSpan("Repository.makeInitial", {
|
|
645
|
+
attributes: { "app.entity": name }
|
|
646
|
+
}, {
|
|
647
|
+
captureStackTrace: false
|
|
648
|
+
})
|
|
649
|
+
)
|
|
650
|
+
: undefined,
|
|
651
|
+
{
|
|
652
|
+
...config,
|
|
653
|
+
partitionValue: config?.partitionValue
|
|
654
|
+
?? ((_) => "primary") /*(isIntegrationEvent(r) ? r.companyId : r.id*/
|
|
655
|
+
}
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
return store
|
|
659
|
+
})
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return makeStore
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
export interface Repos<
|
|
667
|
+
T,
|
|
668
|
+
Encoded extends { id: string },
|
|
669
|
+
RSchema,
|
|
670
|
+
Evt,
|
|
671
|
+
ItemType extends string,
|
|
672
|
+
IdKey extends keyof T,
|
|
673
|
+
RPublish
|
|
674
|
+
> {
|
|
675
|
+
make<RInitial = never, E = never, R2 = never>(
|
|
676
|
+
args: [Evt] extends [never] ? {
|
|
677
|
+
makeInitial?: Effect.Effect<readonly T[], E, RInitial> | undefined
|
|
678
|
+
config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
|
|
679
|
+
partitionValue?: (e?: Encoded) => string
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
: {
|
|
683
|
+
publishEvents: (evt: NonEmptyReadonlyArray<Evt>) => Effect.Effect<void, never, R2>
|
|
684
|
+
makeInitial?: Effect.Effect<readonly T[], E, RInitial> | undefined
|
|
685
|
+
config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
|
|
686
|
+
partitionValue?: (e?: Encoded) => string
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
): Effect.Effect<Repository<T, Encoded, Evt, ItemType, IdKey, RSchema, RPublish>, E, StoreMaker | RInitial | R2>
|
|
690
|
+
makeWith<Out, RInitial = never, E = never, R2 = never>(
|
|
691
|
+
args: [Evt] extends [never] ? {
|
|
692
|
+
makeInitial?: Effect.Effect<readonly T[], E, RInitial> | undefined
|
|
693
|
+
config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
|
|
694
|
+
partitionValue?: (e?: Encoded) => string
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
: {
|
|
698
|
+
publishEvents: (evt: NonEmptyReadonlyArray<Evt>) => Effect.Effect<void, never, R2>
|
|
699
|
+
makeInitial?: Effect.Effect<readonly T[], E, RInitial> | undefined
|
|
700
|
+
config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
|
|
701
|
+
partitionValue?: (e?: Encoded) => string
|
|
702
|
+
}
|
|
703
|
+
},
|
|
704
|
+
f: (r: Repository<T, Encoded, Evt, ItemType, IdKey, RSchema, RPublish>) => Out
|
|
705
|
+
): Effect.Effect<Out, E, StoreMaker | RInitial | R2>
|
|
706
|
+
readonly Q: ReturnType<typeof Q.make<Encoded>>
|
|
707
|
+
readonly type: Repository<T, Encoded, Evt, ItemType, IdKey, RSchema, RPublish>
|
|
708
|
+
}
|