/* eslint-disable @typescript-eslint/no-explicit-any */ import type { UniqueKey } from "@azure/cosmos" import type { Parser, ParserEnv } from "@effect-ts-app/schema/custom/Parser" import type { These } from "@effect-ts-app/boilerplate-prelude/schema" import type { OptimisticConcurrencyException } from "../../errors.js" export type StoreConfig = { uniqueKeys?: UniqueKey[] maxBulkSize?: number partitionValue: (e: E) => string | undefined } export type SupportedValues = string | boolean | number | null // default is eq export type Where = { key: string; t?: "eq" | "not-eq"; value: SupportedValues } | { key: string t: "in" | "not-in" value: readonly (SupportedValues)[] } // default is where export type StoreWhereFilter = { type?: "where" mode?: "and" | "or" // default is and where: readonly [ Where, ...(Where[]) ] } export type LegacyFilter = | { by: keyof E; type: "startsWith"; value: any } | { by: keyof E; type: "endsWith"; value: any } | { by: keyof E; type: "contains"; value: any } export type JoinFindFilter = { type: "join_find" keys: readonly string[] /* value paths of E */ valueKey: string /* value paths of E[keys][valueKey] */ value: any /* value path[valueKey] of E */ } export type Filter = | JoinFindFilter | StoreWhereFilter | LegacyFilter export type FilterJoinSelect = { type: "filter_join_select" keys: readonly string[] /* value paths of E */ valueKey: string /* value paths of E[keys][valueKey] */ value: any /* value path[valueKey] of E */ } export interface Store, Id extends string> { all: Effect> filter: ( filter: Filter, cursor?: { limit?: number; skip?: number } ) => Effect> filterJoinSelect: ( filter: FilterJoinSelect ) => Effect> find: (id: Id) => Effect> set: (e: PM) => Effect batchSet: ( items: NonEmptyReadonlyArray ) => Effect> bulkSet: ( items: NonEmptyReadonlyArray ) => Effect> /** * Requires the PM type, not Id, because various stores may need to calculate e.g partition keys. */ remove: (e: PM) => Effect } export interface StoreMaker { make: , Id extends string, Id2 extends Id>( name: string, existing?: Effect>, config?: StoreConfig ) => Effect> } /** * @tsplus type StoreMaker.Ops */ export interface StoreMakerOps extends Tag {} export const StoreMaker: StoreMakerOps = Tag() /** * @tsplus static ContextMap.Ops Make */ export const makeMap = Effect(() => { const etags = ROMap.make([])["|>"](ROMap.toMutable) const getEtag = (id: string) => etags.get(id) const setEtag = (id: string, eTag: string | undefined) => { eTag === undefined ? etags.delete(id) : etags.set(id, eTag) } const parsedCache = ROMap.make< Parser, Map> >([])["|>"](ROMap.toMutable) const parserCache = ROMap.make< Parser, (i: any) => These.These >([])["|>"](ROMap.toMutable) const setAndReturn = ( p: Parser, np: (i: I) => These.These ) => { parserCache.set(p, np) return np } const parserEnv: ParserEnv = { // TODO: lax: true would turn off refinement checks, may help on large payloads // but of course removes confirming of validation rules (which may be okay for a database owned by the app, as we write safely) lax: false, cache: { getOrSetParser: p => parserCache.get(p) ?? setAndReturn(p, i => parserEnv.cache!.getOrSet(i, p)), getOrSetParsers: parsers => { return Object.entries(parsers).reduce((prev, [k, v]) => { prev[k] = parserEnv.cache!.getOrSetParser(v) return prev }, {} as any) }, getOrSet: (i, parse): any => { const c = parsedCache.get(parse) if (c) { const f = c.get(i) if (f) { // console.log("$$$ cache hit", i) return f } else { const nf = parse(i, parserEnv) c.set(i, nf) return nf } } else { const nf = parse(i, parserEnv) parsedCache.set(parse, ROMap.make([[i, nf]])["|>"](ROMap.toMutable)) return nf } } } } return { get: getEtag, set: setEtag, parserEnv } }) export interface ContextMap extends Effect.Success {} /** * @tsplus type ContextMap.Ops */ export interface ContextMapOps extends Tag {} export const ContextMap: ContextMapOps = Tag() export interface PersistenceModelType { id: Id _etag: string | undefined } export interface StorageConfig { url: ConfigSecret prefix: string dbName: string }