effect-app 4.0.0-beta.247 → 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.
Files changed (140) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/Emailer.d.ts +51 -0
  3. package/dist/Emailer.d.ts.map +1 -0
  4. package/dist/Emailer.js +7 -0
  5. package/dist/Model/Repository/Registry.d.ts +21 -0
  6. package/dist/Model/Repository/Registry.d.ts.map +1 -0
  7. package/dist/Model/Repository/Registry.js +18 -0
  8. package/dist/Model/Repository/ext.d.ts +60 -0
  9. package/dist/Model/Repository/ext.d.ts.map +1 -0
  10. package/dist/Model/Repository/ext.js +122 -0
  11. package/dist/Model/Repository/internal/internal.d.ts +62 -0
  12. package/dist/Model/Repository/internal/internal.d.ts.map +1 -0
  13. package/dist/Model/Repository/internal/internal.js +413 -0
  14. package/dist/Model/Repository/legacy.d.ts +21 -0
  15. package/dist/Model/Repository/legacy.d.ts.map +1 -0
  16. package/dist/Model/Repository/legacy.js +2 -0
  17. package/dist/Model/Repository/makeRepo.d.ts +53 -0
  18. package/dist/Model/Repository/makeRepo.d.ts.map +1 -0
  19. package/dist/Model/Repository/makeRepo.js +27 -0
  20. package/dist/Model/Repository/service.d.ts +97 -0
  21. package/dist/Model/Repository/service.d.ts.map +1 -0
  22. package/dist/Model/Repository/service.js +2 -0
  23. package/dist/Model/Repository/validation.d.ts +71 -0
  24. package/dist/Model/Repository/validation.d.ts.map +1 -0
  25. package/dist/Model/Repository/validation.js +32 -0
  26. package/dist/Model/Repository.d.ts +7 -0
  27. package/dist/Model/Repository.d.ts.map +1 -0
  28. package/dist/Model/Repository.js +7 -0
  29. package/dist/Model/dsl.d.ts +33 -0
  30. package/dist/Model/dsl.d.ts.map +1 -0
  31. package/dist/Model/dsl.js +43 -0
  32. package/dist/Model/filter/filterApi.d.ts +30 -0
  33. package/dist/Model/filter/filterApi.d.ts.map +1 -0
  34. package/dist/Model/filter/filterApi.js +2 -0
  35. package/dist/Model/filter/types/errors.d.ts +29 -0
  36. package/dist/Model/filter/types/errors.d.ts.map +1 -0
  37. package/dist/Model/filter/types/errors.js +2 -0
  38. package/dist/Model/filter/types/fields.d.ts +15 -0
  39. package/dist/Model/filter/types/fields.d.ts.map +1 -0
  40. package/dist/Model/filter/types/fields.js +2 -0
  41. package/dist/Model/filter/types/path/common.d.ts +316 -0
  42. package/dist/Model/filter/types/path/common.d.ts.map +1 -0
  43. package/dist/Model/filter/types/path/common.js +2 -0
  44. package/dist/Model/filter/types/path/eager.d.ts +95 -0
  45. package/dist/Model/filter/types/path/eager.d.ts.map +1 -0
  46. package/dist/Model/filter/types/path/eager.js +31 -0
  47. package/dist/Model/filter/types/path/index.d.ts +4 -0
  48. package/dist/Model/filter/types/path/index.d.ts.map +1 -0
  49. package/dist/Model/filter/types/path/index.js +3 -0
  50. package/dist/Model/filter/types/utils.d.ts +79 -0
  51. package/dist/Model/filter/types/utils.d.ts.map +1 -0
  52. package/dist/Model/filter/types/utils.js +2 -0
  53. package/dist/Model/filter/types/validator.d.ts +30 -0
  54. package/dist/Model/filter/types/validator.d.ts.map +1 -0
  55. package/dist/Model/filter/types/validator.js +2 -0
  56. package/dist/Model/filter/types.d.ts +5 -0
  57. package/dist/Model/filter/types.d.ts.map +1 -0
  58. package/dist/Model/filter/types.js +7 -0
  59. package/dist/Model/query/dsl.d.ts +446 -0
  60. package/dist/Model/query/dsl.d.ts.map +1 -0
  61. package/dist/Model/query/dsl.js +342 -0
  62. package/dist/Model/query/new-kid-interpreter.d.ts +136 -0
  63. package/dist/Model/query/new-kid-interpreter.d.ts.map +1 -0
  64. package/dist/Model/query/new-kid-interpreter.js +336 -0
  65. package/dist/Model/query.d.ts +15 -0
  66. package/dist/Model/query.d.ts.map +1 -0
  67. package/dist/Model/query.js +3 -0
  68. package/dist/Model.d.ts +5 -0
  69. package/dist/Model.d.ts.map +1 -0
  70. package/dist/Model.js +5 -0
  71. package/dist/QueueMaker.d.ts +13 -0
  72. package/dist/QueueMaker.d.ts.map +1 -0
  73. package/dist/QueueMaker.js +4 -0
  74. package/dist/RequestContext.d.ts +103 -0
  75. package/dist/RequestContext.d.ts.map +1 -0
  76. package/dist/RequestContext.js +49 -0
  77. package/dist/Schema/ext.d.ts +9 -9
  78. package/dist/Schema/ext.d.ts.map +1 -1
  79. package/dist/Schema/ext.js +1 -1
  80. package/dist/Store.d.ts +147 -0
  81. package/dist/Store.d.ts.map +1 -0
  82. package/dist/Store.js +95 -0
  83. package/dist/client/apiClientFactory.d.ts +1 -1
  84. package/dist/client/clientFor.d.ts +5 -5
  85. package/dist/client/clientFor.d.ts.map +1 -1
  86. package/dist/client/makeClient.d.ts +36 -36
  87. package/dist/client/makeClient.d.ts.map +1 -1
  88. package/dist/index.d.ts +3 -1
  89. package/dist/index.d.ts.map +1 -1
  90. package/dist/index.js +3 -1
  91. package/dist/rpc/MiddlewareMaker.d.ts +2 -2
  92. package/dist/rpc/MiddlewareMaker.d.ts.map +1 -1
  93. package/dist/rpc/RpcMiddleware.d.ts +2 -2
  94. package/dist/rpc/RpcMiddleware.d.ts.map +1 -1
  95. package/dist/runtime.d.ts +19 -0
  96. package/dist/runtime.d.ts.map +1 -0
  97. package/dist/runtime.js +40 -0
  98. package/dist/toast.d.ts +51 -0
  99. package/dist/toast.d.ts.map +1 -0
  100. package/dist/toast.js +34 -0
  101. package/dist/withToast.d.ts +30 -0
  102. package/dist/withToast.d.ts.map +1 -0
  103. package/dist/withToast.js +64 -0
  104. package/package.json +113 -1
  105. package/src/Emailer.ts +51 -0
  106. package/src/Model/Repository/Registry.ts +34 -0
  107. package/src/Model/Repository/ext.ts +375 -0
  108. package/src/Model/Repository/internal/internal.ts +708 -0
  109. package/src/Model/Repository/legacy.ts +29 -0
  110. package/src/Model/Repository/makeRepo.ts +144 -0
  111. package/src/Model/Repository/service.ts +639 -0
  112. package/src/Model/Repository/validation.ts +31 -0
  113. package/src/Model/Repository.ts +6 -0
  114. package/src/Model/dsl.ts +129 -0
  115. package/src/Model/filter/filterApi.ts +60 -0
  116. package/src/Model/filter/types/errors.ts +47 -0
  117. package/src/Model/filter/types/fields.ts +50 -0
  118. package/src/Model/filter/types/path/common.ts +404 -0
  119. package/src/Model/filter/types/path/eager.ts +297 -0
  120. package/src/Model/filter/types/path/index.ts +4 -0
  121. package/src/Model/filter/types/utils.ts +128 -0
  122. package/src/Model/filter/types/validator.ts +46 -0
  123. package/src/Model/filter/types.ts +6 -0
  124. package/src/Model/query/dsl.ts +2546 -0
  125. package/src/Model/query/new-kid-interpreter.ts +484 -0
  126. package/src/Model/query.ts +13 -0
  127. package/src/Model.ts +4 -0
  128. package/src/QueueMaker.ts +19 -0
  129. package/src/RequestContext.ts +62 -0
  130. package/src/Schema/ext.ts +6 -6
  131. package/src/Store.ts +243 -0
  132. package/src/client/clientFor.ts +8 -8
  133. package/src/client/makeClient.ts +11 -11
  134. package/src/index.ts +2 -0
  135. package/src/rpc/MiddlewareMaker.ts +1 -1
  136. package/src/rpc/RpcMiddleware.ts +1 -1
  137. package/src/runtime.ts +56 -0
  138. package/src/toast.ts +54 -0
  139. package/src/withToast.ts +133 -0
  140. 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
+ }