// Update = Must return updated items // Modify = Must `set` updated items, and can return anything. import type { InvalidStateError, OptimisticConcurrencyException } from "../errors.js" import { NotFoundError } from "../errors.js" import type { RequestContext } from "../lib/RequestContext.js" import { ContextMap} from "../services/Store.js"; import type { Filter } from "../services/Store.js" import type { FixEnv, PureLogT} from "@effect-ts-app/boilerplate-prelude/_ext/Pure"; import { Pure } from "@effect-ts-app/boilerplate-prelude/_ext/Pure" import type { ParserEnv } from "@effect-ts-app/schema/custom/Parser" /** * @tsplus type Repository */ export interface Repository { itemType: ItemType find: (id: Id) => Effect> all: Effect> save: ( items: Iterable, events?: Iterable ) => Effect utils: { mapReverse: ( pm: PM, setEtag: (id: string, eTag: string | undefined) => void ) => unknown, // TODO parse: (a: unknown, env?: ParserEnv | undefined) => T, all: Effect>, filter: (filter: Filter, cursor?: { limit?: number, skip?: number}) => Effect> } } export interface PureDSL { get: ReturnType>, set: typeof Pure.set, log: (...w: W[]) => PureLogT } export const AnyPureDSL: PureDSL = { get: Pure.get(), set: Pure.set, log: (...evt: any[]) => Pure.logMany(evt) } /** * @tsplus fluent Repository get */ export function get< Id extends string, T extends { id: Id }, PM extends { id: string }, Evt, ItemType extends string >( self: Repository, id: Id ) { return self.find(id).flatMap(_ => _.encaseInEffect(() => new NotFoundError(self.itemType, id))) } /** * @tsplus fluent Repository filter */ export function filter< Id extends string, T extends { id: Id }, PM extends { id: string }, Evt, ItemType extends string, >(self: Repository, filter: Predicate) { return self.all.map(_ => _.filter(filter)) } /** * @tsplus fluent Repository filterAll */ export function filterAll< Id extends string, T extends { id: Id }, PM extends { id: string }, Evt, ItemType extends string, S extends T, >(self: Repository, map: (items: Chunk) => Chunk) { return self.all.map(map) } /** * @tsplus fluent Repository collect */ export function collect< Id extends string, T extends { id: Id }, PM extends { id: string }, Evt, ItemType extends string, S extends T, >(self: Repository, collect: (item: T) => Opt) { return self.all.map(_ => _.filterMap(collect)) } /** * @tsplus getter Repository log */ export function log< Id extends string, T extends { id: Id }, PM extends { id: string }, Evt, ItemType extends string >(_: Repository) { return (evt: Evt) => AnyPureDSL.log(evt) } /** * TODO: project inside the db. * @tsplus fluent Repository projectEffect */ export function projectEffect< Id extends string, T extends { id: Id }, PM extends { id: string }, Evt, ItemType extends string, S, R, E >( self: Repository, map: Effect; collect: (t: PM) => Opt; limit?: number; skip?: number }> ) { // TODO: a projection that gets sent to the db instead. return map.flatMap(f => (f.filter ? self.utils.filter(f.filter, { limit: f.limit, skip: f.skip}) : self.utils.all) .map(_ => _.filterMap(f.collect)) ) } /** * TODO: project inside the db. * @tsplus fluent Repository project */ export function project< Id extends string, T extends { id: Id }, PM extends { id: string }, Evt, ItemType extends string, S, >( self: Repository, map: { filter?: Filter; collect: (t: PM) => Opt; limit?: number; skip?: number } ) { return self.projectEffect(Effect(map)) } /** * @tsplus fluent Repository queryEffect */ export function queryEffect< Id extends string, T extends { id: Id }, PM extends { id: string }, Evt, ItemType extends string, R, E, S, >( self: Repository, // TODO: think about collectPM, collectE, and collect(Parsed) map: Effect; collect: (t: T) => Opt; limit?: number; skip?: number }> ) { return map.flatMap(f => (f.filter ? self.utils.filter(f.filter, { limit: f.limit, skip: f.skip}) : self.utils.all) .flatMap(items => Do($ => { const { set } = $(Effect.service(ContextMap)) return items.map(_ => self.utils.mapReverse(_, set)) }) ) .map(_ => _.map(_ => self.utils.parse(_))) .map(_ => _.filterMap(f.collect)) ) } /** * @tsplus fluent Repository queryOneEffect */ export function queryOneEffect< Id extends string, T extends { id: Id }, PM extends { id: string }, Evt, ItemType extends string, R, E, S, >( self: Repository, // TODO: think about collectPM, collectE, and collect(Parsed) map: Effect; collect: (t: T) => Opt }> ) { return map.flatMap(f => (f.filter ? self.utils.filter(f.filter, { limit: 1 }) : self.utils.all) .flatMap(items => Do($ => { const { set } = $(Effect.service(ContextMap)) return items.map(_ => self.utils.mapReverse(_, set)) }) ) .map(_ => _.map(_ => self.utils.parse(_))) .flatMap(_ => _.filterMap(f.collect).toNonEmptyArray.encaseInEffect(() => new NotFoundError(self.itemType, JSON.stringify(f.filter))).map(_ => _[0])) ) } /** * @tsplus fluent Repository query */ export function query< Id extends string, T extends { id: Id }, PM extends { id: string }, Evt, ItemType extends string, S, >( self: Repository, // TODO: think about collectPM, collectE, and collect(Parsed) map: { filter?: Filter; collect: (t: T) => Opt; limit?: number; skip?: number } ) { return self.queryEffect(Effect(map)) } /** * @tsplus fluent Repository queryOne */ export function queryOne< Id extends string, T extends { id: Id }, PM extends { id: string }, Evt, ItemType extends string, S, >( self: Repository, // TODO: think about collectPM, collectE, and collect(Parsed) map: { filter?: Filter; collect: (t: T) => Opt } ) { return self.queryOneEffect(Effect(map)) } /** * @tsplus fluent Repository queryAndSavePureEffect */ export function queryAndSavePureEffect< Id extends string, T extends { id: Id }, PM extends { id: string }, Evt, ItemType extends string, R, E, S extends T = T, >( self: Repository, // TODO: think about collectPM, collectE, and collect(Parsed) map: Effect; collect: (t: T) => Opt; limit?: number; skip?: number }> ) { return (pure: Effect, Iterable>, E2, A>) => queryEffect(self, map) .flatMap(_ => self.saveManyWithPure_(_, pure)) } /** * @tsplus fluent Repository queryAndSavePure */ export function queryAndSavePure< Id extends string, T extends { id: Id }, PM extends { id: string }, Evt, ItemType extends string, S extends T = T, >( self: Repository, // TODO: think about collectPM, collectE, and collect(Parsed) map: { filter: Filter; collect: (t: T) => Opt; limit?: number; skip?: number } ) { return self.queryAndSavePureEffect(Effect(map)) } /** * @tsplus getter Repository saveManyWithPure */ export function saveManyWithPure< Id extends string, T extends { id: Id }, PM extends { id: string }, Evt, ItemType extends string >(self: Repository) { return (pure: Effect, Iterable>, E, A>) => (items: Iterable) => saveManyWithPure_(self, items, pure) } /** * @tsplus fluent Repository byIdAndSaveWithPure */ export function byIdAndSaveWithPure< Id extends string, T extends { id: Id }, PM extends { id: string }, Evt, ItemType extends string >(self: Repository, id: Id) { return (pure: Effect, E, A>) => get(self, id).flatMap(item => saveWithPure_(self, item, pure)) } /** * NOTE: it's not as composable, only useful when the request is simple, and only this part needs request args. * @tsplus getter Repository handleByIdAndSaveWithPure */ export function handleByIdAndSaveWithPure< Id extends string, T extends { id: Id }, PM extends { id: string }, Evt, ItemType extends string, >(self: Repository) { return (pure: (req: Req, ctx: Context) => Effect, E, A>) => (req: Req, ctx: Context) => byIdAndSaveWithPure(self, req.id)(pure(req, ctx)) } /** * @tsplus fluent Repository saveManyWithPure_ */ export function saveManyWithPure_< Id extends string, R, T extends { id: Id }, PM extends { id: string }, A, E, Evt, S1 extends T, S2 extends T, ItemType extends string >( self: Repository, items: Iterable, pure: Effect, Iterable>, E, A> ) { return saveAllWithEffectInt( self, pure.runTerm(items.toChunk) ) } /** * @tsplus fluent Repository saveWithPure_ */ export function saveWithPure_< Id extends string, R, T extends { id: Id }, PM extends { id: string }, A, E, Evt, S1 extends T, S2 extends T, ItemType extends string >( self: Repository, item: S1, pure: Effect, E, A> ) { return saveAllWithEffectInt( self, pure.runTerm(item) .map(([item, events, a]) => [[item], events, a]) ) } function saveAllWithEffectInt< Id extends string, T extends { id: Id }, PM extends { id: string }, P extends T, Evt, ItemType extends string, R, E, A >( self: Repository, gen: Effect, Iterable, A]> ) { return gen .flatMap( ([items, events, a]) => self.save(items, events) .map(() => a) ) } const anyDSL = makeDSL() export type AllDSL = ((pure: (dsl: PureDSL, Iterable, Evt>) => Effect) => Effect, Iterable>, E, A>) & AllDSLExt /** * @tsplus type DSLExt */ export interface AllDSLExt { modify: (pure: (items: Chunk, dsl: PureDSL, Iterable, Evt>) => Effect) => Effect, Iterable>, E, A>; update: (pure: (items: Chunk, log: (...evt: Evt[]) => PureLogT) => Effect>) => Effect, Iterable>, E, Iterable>; } export function makeAllDSL() { const dsl: AllDSL = anyDSL return dsl } export type OneDSL = ((pure: (dsl: PureDSL) => Effect, E, A>) => Effect, E, A>) & OneDSLExt /** * @tsplus type DSLExt */ export interface OneDSLExt { modify: (pure: (items: S1, dsl: PureDSL) => Effect, E, A>) => Effect | PureEnvEnv, E, A>; update: (pure: (items: S1, log: (...evt: Evt[]) => PureLogT) => Effect, E, S2>) => Effect, E, S2>; } /** * @tsplus fluent DSLExt updateWith */ export function updateWithOne(self: OneDSL, upd: (item: S1) => S2) { return self.update((_: S1) => Effect(upd(_))) } /** * @tsplus fluent DSLExt updateWith */ export function updateWith(self: AllDSL, upd: (item: Chunk) => Iterable) { return self.update((_: Chunk) => Effect(upd(_))) } export function makeOneDSL(): OneDSL { return anyDSL } export function makeDSL() { const dsl: PureDSL = AnyPureDSL function modify< R, E, A >( pure: ( items: S1, dsl: PureDSL ) => Effect ): Effect, E, A> { return dsl.get.flatMap(items => pure(items, dsl)) as any } function update< R, E >( pure: ( items: S1, log: (...evt: Evt[]) => PureLogT ) => Effect ): Effect, E, S2> { return dsl.get.flatMap(items => pure(items, dsl.log).tap(dsl.set)) as any } function withDSL< R, A, E >( pure: (dsl: PureDSL) => Effect ): Effect, E, A> { return pure(AnyPureDSL) as any } return Object.assign( withDSL, { modify, update, } ) } export interface DSLExt extends ReturnType> { } export function ifAny(fn: (items: NonEmptyReadonlyArray) => Effect) { return (items: Iterable) => Effect(items.toNonEmptyArray).flatMapOpt(fn) } /** * @tsplus fluent Iterable ifAny */ export function ifAny_(items: Iterable, fn: (items: NonEmptyReadonlyArray) => Effect) { return Effect(items.toNonEmptyArray).flatMapOpt(fn) }