@effect-app/infra 2.10.0 → 2.11.0

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.
@@ -7,489 +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 type { NonEmptyReadonlyArray } from "effect-app"
11
- import { Array, Chunk, Context, Effect, Equivalence, flow, Option, pipe, Pipeable, PubSub, S, Unify } from "effect-app"
12
- import { toNonEmptyArray } from "effect-app/Array"
13
- import { NotFoundError } from "effect-app/client"
14
- import { flatMapOption } from "effect-app/Effect"
15
- import type { Schema } from "effect-app/Schema"
16
- import { NonNegativeInt } from "effect-app/Schema"
17
- import { setupRequestContextFromCurrent } from "../../api/setupRequest.js"
18
- import type { FilterArgs, PersistenceModelType, StoreConfig } from "../../Store.js"
19
- import { StoreMaker } from "../../Store.js"
20
- import { getContextMap } from "../../Store/ContextMapContainer.js"
21
- import type { FieldValues } from "../filter/types.js"
22
- import * as Q from "../query.js"
10
+ import type { Context, NonEmptyReadonlyArray, S } from "effect-app"
11
+ import { Effect } from "effect-app"
12
+ import type { StoreConfig, StoreMaker } from "../../Store.js"
23
13
  import type { ExtendedRepository } from "./ext.js"
24
14
  import { extendRepo } from "./ext.js"
25
- import type { Repository } from "./service.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 { id: string },
39
- T,
40
- IdKey extends keyof T
41
- >(
42
- name: ItemType,
43
- schema: S.Schema<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.id))
54
- }
55
-
56
- function mapReverse(
57
- { _etag, ...e }: PM,
58
- setEtag: (id: string, eTag: string | undefined) => void
59
- ): Encoded {
60
- setEtag(e.id, _etag)
61
- return mapFrom(e as unknown as Encoded)
62
- }
63
-
64
- const mkStore = makeStore<Encoded>()(name, schema, mapTo)
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<readonly T[], E, RInitial>
70
- config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
71
- partitionValue?: (a: Encoded) => string
72
- }
73
- }
74
- : {
75
- schemaContext?: Context.Context<RCtx>
76
- publishEvents: (evt: NonEmptyReadonlyArray<Evt>) => Effect<void, never, RPublish>
77
- makeInitial?: Effect<readonly T[], E, RInitial>
78
- config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
79
- partitionValue?: (a: Encoded) => string
80
- }
81
- }
82
- ) {
83
- return Effect
84
- .gen(function*() {
85
- const rctx: Context<RCtx> = args.schemaContext ?? Context.empty() as any
86
- const provideRctx = Effect.provide(rctx)
87
- const encodeMany = flow(
88
- S.encode(S.Array(schema)),
89
- provideRctx,
90
- Effect.withSpan("encodeMany", { captureStackTrace: false })
91
- )
92
- const decode = flow(S.decode(schema), provideRctx)
93
- const decodeMany = flow(
94
- S.decode(S.Array(schema)),
95
- provideRctx,
96
- Effect.withSpan("decodeMany", { captureStackTrace: false })
97
- )
98
-
99
- const store = yield* mkStore(args.makeInitial, args.config)
100
- const cms = Effect.andThen(getContextMap.pipe(Effect.orDie), (_) => ({
101
- get: (id: string) => _.get(`${name}.${id}`),
102
- set: (id: string, etag: string | undefined) => _.set(`${name}.${id}`, etag)
103
- }))
104
-
105
- const pub = "publishEvents" in args
106
- ? args.publishEvents
107
- : () => Effect.void
108
- const changeFeed = yield* PubSub.unbounded<[T[], "save" | "remove"]>()
109
-
110
- const allE = cms
111
- .pipe(Effect.flatMap((cm) => Effect.map(store.all, (_) => _.map((_) => mapReverse(_, cm.set)))))
112
-
113
- const all = Effect
114
- .flatMap(
115
- allE,
116
- (_) => decodeMany(_).pipe(Effect.orDie)
117
- )
118
- .pipe(Effect.map((_) => _ as T[]))
119
-
120
- const fieldsSchema = schema as unknown as { fields: any }
121
- // assumes the id field never needs a service...
122
- const i = ("fields" in fieldsSchema ? S.Struct(fieldsSchema["fields"]) as unknown as typeof schema : schema)
123
- .pipe((_) => {
124
- let ast = _.ast
125
- if (ast._tag === "Declaration") ast = ast.typeParameters[0]!
126
-
127
- const s = S.make(ast) as unknown as Schema<T, Encoded, R>
128
-
129
- return ast._tag === "Union"
130
- // we need to get the TypeLiteral, incase of class it's behind a transform...
131
- ? S.Union(
132
- ...ast.types.map((_) =>
133
- (S.make(_._tag === "Transformation" ? _.from : _) as unknown as Schema<T, Encoded>)
134
- .pipe(S.pick(idKey as any))
135
- )
136
- )
137
- : s.pipe(S.pick(idKey as any))
138
- })
139
- const encodeId = flow(S.encode(i), provideRctx)
140
- function findEId(id: Encoded["id"]) {
141
- return Effect.flatMap(
142
- store.find(id),
143
- (item) =>
144
- Effect.gen(function*() {
145
- const { set } = yield* cms
146
- return item.pipe(Option.map((_) => mapReverse(_, set)))
147
- })
148
- )
149
- }
150
- function findE(id: T[IdKey]) {
151
- return pipe(
152
- encodeId({ [idKey]: id } as any),
153
- Effect.orDie,
154
- // we will have idKey because the transform is undone again by the encode schema mumbo jumbo above
155
- // TODO: make reliable. (Security: isin: PrimaryKey(ISIN), idKey: "isin", does end up with "id")
156
- Effect.map((_) => (_ as any)[idKey] ?? (_ as any).id),
157
- Effect.flatMap(findEId)
158
- )
159
- }
160
-
161
- function find(id: T[IdKey]) {
162
- return Effect.flatMapOption(findE(id), (_) => Effect.orDie(decode(_)))
163
- }
164
-
165
- const saveAllE = (a: Iterable<Encoded>) =>
166
- Effect
167
- .flatMapOption(
168
- Effect
169
- .sync(() => toNonEmptyArray([...a])),
170
- (a) =>
171
- Effect.gen(function*() {
172
- const { get, set } = yield* cms
173
- const items = a.map((_) => mapToPersistenceModel(_, get))
174
- const ret = yield* store.batchSet(items)
175
- ret.forEach((_) => set(_.id, _._etag))
176
- })
177
- )
178
- .pipe(Effect.asVoid)
179
-
180
- const saveAll = (a: Iterable<T>) =>
181
- encodeMany(Array.fromIterable(a))
182
- .pipe(
183
- Effect.orDie,
184
- Effect.andThen(saveAllE)
185
- )
186
-
187
- const saveAndPublish = (items: Iterable<T>, events: Iterable<Evt> = []) => {
188
- return Effect
189
- .suspend(() => {
190
- const it = Chunk.fromIterable(items)
191
- return saveAll(it)
192
- .pipe(
193
- Effect.andThen(Effect.sync(() => toNonEmptyArray([...events]))),
194
- // TODO: for full consistency the events should be stored within the same database transaction, and then picked up.
195
- (_) => Effect.flatMapOption(_, pub),
196
- Effect.andThen(changeFeed.publish([Chunk.toArray(it), "save"])),
197
- Effect.asVoid
198
- )
199
- })
200
- .pipe(Effect.withSpan("saveAndPublish", { captureStackTrace: false }))
201
- }
202
-
203
- function removeAndPublish(a: Iterable<T>, events: Iterable<Evt> = []) {
204
- return Effect.gen(function*() {
205
- const { get, set } = yield* cms
206
- const it = [...a]
207
- const items = yield* encodeMany(it).pipe(Effect.orDie)
208
- // TODO: we should have a batchRemove on store so the adapter can actually batch...
209
- for (const e of items) {
210
- yield* store.remove(mapToPersistenceModel(e, get))
211
- set(e.id, undefined)
212
- }
213
- yield* Effect
214
- .sync(() => toNonEmptyArray([...events]))
215
- // TODO: for full consistency the events should be stored within the same database transaction, and then picked up.
216
- .pipe((_) => Effect.flatMapOption(_, pub))
217
-
218
- yield* changeFeed.publish([it, "remove"])
219
- })
220
- }
221
-
222
- const parseMany = (items: readonly PM[]) =>
223
- Effect
224
- .flatMap(cms, (cm) =>
225
- decodeMany(items.map((_) => mapReverse(_, cm.set)))
226
- .pipe(Effect.orDie, Effect.withSpan("parseMany", { captureStackTrace: false })))
227
- const parseMany2 = <A, R>(
228
- items: readonly PM[],
229
- schema: S.Schema<A, Encoded, R>
230
- ) =>
231
- Effect
232
- .flatMap(cms, (cm) =>
233
- S
234
- .decode(S.Array(schema))(
235
- items.map((_) => mapReverse(_, cm.set))
236
- )
237
- .pipe(Effect.orDie, Effect.withSpan("parseMany2", { captureStackTrace: false })))
238
- const filter = <U extends keyof Encoded = keyof Encoded>(args: FilterArgs<Encoded, U>) =>
239
- store
240
- .filter(
241
- // always enforce id and _etag because they are system fields, required for etag tracking etc
242
- {
243
- ...args,
244
- select: args.select
245
- ? dedupe([...args.select, "id", "_etag" as any])
246
- : undefined
247
- } as typeof args
248
- )
249
- .pipe(
250
- Effect.tap((items) =>
251
- Effect.map(cms, ({ set }) => items.forEach((_) => set((_ as Encoded).id, (_ as PM)._etag)))
252
- )
253
- )
254
-
255
- // TODO: For raw we should use S.from, and drop the R...
256
- const query: {
257
- <A, R, From extends FieldValues>(
258
- q: Q.QueryProjection<Encoded extends From ? From : never, A, R>
259
- ): Effect.Effect<readonly A[], S.ParseResult.ParseError, R>
260
- <A, R, EncodedRefined extends Encoded = Encoded>(
261
- q: Q.QAll<NoInfer<Encoded>, NoInfer<EncodedRefined>, A, R>
262
- ): Effect.Effect<readonly A[], never, R>
263
- } = (<A, R, EncodedRefined extends Encoded = Encoded>(q: Q.QAll<Encoded, EncodedRefined, A, R>) => {
264
- const a = Q.toFilter(q)
265
- const eff = a.mode === "project"
266
- ? filter(a)
267
- // TODO: mapFrom but need to support per field and dependencies
268
- .pipe(
269
- Effect.andThen(flow(S.decode(S.Array(a.schema ?? schema)), provideRctx))
270
- )
271
- : a.mode === "collect"
272
- ? filter(a)
273
- // TODO: mapFrom but need to support per field and dependencies
274
- .pipe(
275
- Effect.flatMap(flow(
276
- S.decode(S.Array(a.schema)),
277
- Effect.map(Array.getSomes),
278
- provideRctx
279
- ))
280
- )
281
- : Effect.flatMap(
282
- filter(a),
283
- (_) =>
284
- Unify.unify(
285
- a.schema
286
- // TODO: partial may not match?
287
- ? parseMany2(_ as any, a.schema as any)
288
- : parseMany(_ as any)
289
- )
290
- )
291
- return pipe(
292
- a.ttype === "one"
293
- ? Effect.andThen(
294
- eff,
295
- flow(
296
- Array.head,
297
- Effect.mapError(() => new NotFoundError({ id: "query", /* TODO */ type: name }))
298
- )
299
- )
300
- : a.ttype === "count"
301
- ? Effect
302
- .andThen(eff, (_) => NonNegativeInt(_.length))
303
- .pipe(Effect.catchTag("ParseError", (e) => Effect.die(e)))
304
- : eff,
305
- Effect.withSpan("Repository.query [effect-app/infra]", {
306
- captureStackTrace: false,
307
- attributes: {
308
- "repository.model_name": name,
309
- query: { ...a, schema: a.schema ? "__SCHEMA__" : a.schema, filter: a.filter }
310
- }
311
- })
312
- )
313
- }) as any
314
-
315
- const r: Repository<T, Encoded, Evt, ItemType, IdKey, Exclude<R, RCtx>, RPublish> = {
316
- changeFeed,
317
- itemType: name,
318
- idKey,
319
- find,
320
- all,
321
- saveAndPublish,
322
- removeAndPublish,
323
- query(q: any) {
324
- // eslint-disable-next-line prefer-rest-params
325
- return query(typeof q === "function" ? Pipeable.pipeArguments(Q.make(), arguments) : q) as any
326
- },
327
- /**
328
- * @internal
329
- */
330
- mapped: <A, R>(schema: S.Schema<A, any, R>) => {
331
- const dec = S.decode(schema)
332
- const encMany = S.encode(S.Array(schema))
333
- const decMany = S.decode(S.Array(schema))
334
- return {
335
- all: allE.pipe(
336
- Effect.flatMap(decMany),
337
- Effect.map((_) => _ as any[])
338
- ),
339
- find: (id: T[IdKey]) => flatMapOption(findE(id), dec),
340
- // query: (q: any) => {
341
- // const a = Q.toFilter(q)
342
-
343
- // return filter(a)
344
- // .pipe(
345
- // Effect.flatMap(decMany),
346
- // Effect.map((_) => _ as any[]),
347
- // Effect.withSpan("Repository.mapped.query [effect-app/infra]", {
348
- // captureStackTrace: false,
349
- // attributes: {
350
- // "repository.model_name": name,
351
- // query: { ...a, schema: a.schema ? "__SCHEMA__" : a.schema, filter: a.filter.build() }
352
- // }
353
- // })
354
- // )
355
- // },
356
- save: (...xes: any[]) =>
357
- Effect.flatMap(encMany(xes), (_) => saveAllE(_)).pipe(
358
- Effect.withSpan("mapped.save", { captureStackTrace: false })
359
- )
360
- }
361
- }
362
- }
363
- return r
364
- })
365
- .pipe(Effect
366
- // .withSpan("Repository.make [effect-app/infra]", { attributes: { "repository.model_name": name } })
367
- .withLogSpan("Repository.make: " + name))
368
- }
369
-
370
- return {
371
- make,
372
- Q: Q.make<Encoded>()
373
- }
374
- }
375
- }
376
-
377
- const pluralize = (s: string) =>
378
- s.endsWith("s")
379
- ? s + "es"
380
- : s.endsWith("y")
381
- ? s.substring(0, s.length - 1) + "ies"
382
- : s + "s"
383
-
384
- export function makeStore<
385
- Encoded extends { id: string }
386
- >() {
387
- return <
388
- ItemType extends string,
389
- R,
390
- E extends { id: string },
391
- T
392
- >(
393
- name: ItemType,
394
- schema: S.Schema<T, E, R>,
395
- mapTo: (e: E, etag: string | undefined) => Encoded
396
- ) => {
397
- function encodeToEncoded() {
398
- const getEtag = () => undefined
399
- return (t: T) =>
400
- S.encode(schema)(t).pipe(
401
- Effect.orDie,
402
- Effect.map((_) => mapToPersistenceModel(_, getEtag))
403
- )
404
- }
405
-
406
- function mapToPersistenceModel(
407
- e: E,
408
- getEtag: (id: string) => string | undefined
409
- ): Encoded {
410
- return mapTo(e, getEtag(e.id))
411
- }
412
-
413
- function makeStore<RInitial = never, EInitial = never>(
414
- makeInitial?: Effect<readonly T[], EInitial, RInitial>,
415
- config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
416
- partitionValue?: (a: Encoded) => string
417
- }
418
- ) {
419
- return Effect.gen(function*() {
420
- const { make } = yield* StoreMaker
421
-
422
- const store = yield* make<Encoded, string, RInitial | R, EInitial>(
423
- pluralize(name),
424
- makeInitial
425
- ? makeInitial
426
- .pipe(
427
- Effect.flatMap(Effect.forEach(encodeToEncoded())),
428
- setupRequestContextFromCurrent("Repository.makeInitial [effect-app/infra]", {
429
- attributes: { "repository.model_name": name }
430
- })
431
- )
432
- : undefined,
433
- {
434
- ...config,
435
- partitionValue: config?.partitionValue
436
- ?? ((_) => "primary") /*(isIntegrationEvent(r) ? r.companyId : r.id*/
437
- }
438
- )
439
-
440
- return store
441
- })
442
- }
443
-
444
- return makeStore
445
- }
446
- }
447
-
448
- export interface Repos<
449
- T,
450
- Encoded extends { id: string },
451
- RSchema,
452
- Evt,
453
- ItemType extends string,
454
- IdKey extends keyof T,
455
- RPublish
456
- > {
457
- make<RInitial = never, E = never, R2 = never>(
458
- args: [Evt] extends [never] ? {
459
- makeInitial?: Effect<readonly T[], E, RInitial>
460
- config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
461
- partitionValue?: (a: Encoded) => string
462
- }
463
- }
464
- : {
465
- publishEvents: (evt: NonEmptyReadonlyArray<Evt>) => Effect<void, never, R2>
466
- makeInitial?: Effect<readonly T[], E, RInitial>
467
- config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
468
- partitionValue?: (a: Encoded) => string
469
- }
470
- }
471
- ): Effect<Repository<T, Encoded, Evt, ItemType, IdKey, RSchema, RPublish>, E, StoreMaker | RInitial | R2>
472
- makeWith<Out, RInitial = never, E = never, R2 = never>(
473
- args: [Evt] extends [never] ? {
474
- makeInitial?: Effect<readonly T[], E, RInitial>
475
- config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
476
- partitionValue?: (a: Encoded) => string
477
- }
478
- }
479
- : {
480
- publishEvents: (evt: NonEmptyReadonlyArray<Evt>) => Effect<void, never, R2>
481
- makeInitial?: Effect<readonly T[], E, RInitial>
482
- config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
483
- partitionValue?: (a: Encoded) => string
484
- }
485
- },
486
- f: (r: Repository<T, Encoded, Evt, ItemType, IdKey, RSchema, RPublish>) => Out
487
- ): Effect<Out, E, StoreMaker | RInitial | R2>
488
- readonly Q: ReturnType<typeof Q.make<Encoded>>
489
- readonly type: Repository<T, Encoded, Evt, ItemType, IdKey, RSchema, RPublish>
490
- }
491
-
492
- export type GetRepoType<T> = T extends { type: infer R } ? R : never
15
+ import { makeRepoInternal } from "./internal/internal.js"
493
16
 
494
17
  export interface RepositoryOptions<
495
18
  IdKey extends keyof T,