1import type { Contract } from "@prisma/orm-postgres/contract/types";
2import type { SqlStorage } from "@prisma/orm-postgres/family-contract/types";
3import type {
4 AggregateBuilder,
5 AggregateResult,
6 AggregateSpec,
7 Collection,
8 CollectionTypeState,
9 DefaultCollectionTypeState,
10 DefaultModelRow,
11 GroupedCollection,
12 RelatedModelName,
13 RelationNames,
14 RelationsOf,
15} from "@prisma/orm-postgres/orm-client";
16import type { PostgresClient } from "@prisma/orm-postgres/runtime";
17import * as Effect from "effect/Effect";
18import * as Stream from "effect/Stream";
19import { type ClientError, wrapPrismaError } from "./Errors.ts";
20
21type AnyContract = Contract<SqlStorage>;
22type Simplify<T> = { [K in keyof T]: T[K] } & {};
23type InitialState<Ns extends string> = Omit<
24 DefaultCollectionTypeState,
25 "nsId"
26> & {
27 readonly nsId: Ns;
28};
29type WithState<S extends CollectionTypeState, Patch> = Omit<S, keyof Patch> &
30 Patch;
31type Args<F> = F extends {
32 (...args: infer A): unknown;
33 (...args: infer B): unknown;
34 (...args: infer C): unknown;
35 (...args: infer D): unknown;
36}
37 ? A | B | C | D
38 : never;
39type Native<
40 C extends AnyContract,
41 M extends string,
42 Row,
43 S extends CollectionTypeState,
44> = Collection<C, M, Row, S>;
45type NativeRow<T> = T extends { readonly _row?: infer Row } ? Row : never;
46type Root<
47 C extends AnyContract,
48 Ns extends string,
49 M extends string,
50> = Ns extends keyof PostgresClient<C>["orm"]
51 ? M extends keyof PostgresClient<C>["orm"][Ns]
52 ? PostgresClient<C>["orm"][Ns][M]
53 : never
54 : never;
55
56type RootRow<C extends AnyContract, Ns extends string, M extends string> = [
57 Root<C, Ns, M>,
58] extends [never]
59 ? DefaultModelRow<C, M, Ns>
60 : NativeRow<Root<C, Ns, M>>;
61
62export type WhereFilter<
63 C extends AnyContract,
64 M extends string,
65 Ns extends string,
66> = Args<Native<C, M, DefaultModelRow<C, M, Ns>, InitialState<Ns>>["where"]>[0];
67
68/** Buffer a query with `yield*`, or consume its rows incrementally through `stream`. */
69export interface QueryResult<Row, E, R> extends Effect.Effect<Row[], E, R> {
70 readonly stream: Stream.Stream<Row, E, R>;
71}
72
73type Relation<
74 C extends AnyContract,
75 Ns extends string,
76 M extends string,
77 Rel extends string,
78> = Rel extends keyof RelationsOf<C, M, Ns>
79 ? RelationsOf<C, M, Ns>[Rel]
80 : never;
81type TargetNamespace<
82 C extends AnyContract,
83 Rel,
84 Ns extends string,
85> = Rel extends {
86 readonly to: { readonly namespace: infer Target extends string };
87}
88 ? {
89 [K in keyof C["domain"]["namespaces"] & string]: Target extends K
90 ? K
91 : never;
92 }[keyof C["domain"]["namespaces"] & string]
93 : Ns;
94type RelatedRow<
95 C extends AnyContract,
96 Ns extends string,
97 M extends string,
98 Rel extends string,
99> = RootRow<
100 C,
101 TargetNamespace<C, Relation<C, Ns, M, Rel>, Ns>,
102 RelatedModelName<C, M, Rel, Ns> & string
103>;
104type IncludeOwner<
105 C extends AnyContract,
106 Ns extends string,
107 M extends string,
108 Rel extends string,
109 S extends CollectionTypeState,
110> = Rel extends RelationNames<C, M, Ns> ? M : S["variantName"] & string;
111type KeysOfUnion<T> = T extends unknown ? keyof T : never;
112type Cardinality<Rel> = Rel extends { readonly cardinality: infer Card }
113 ? Card
114 : "1:N";
115type RelationValue<Rel, Row, Refined extends boolean = false> =
116 Cardinality<Rel> extends "1:1" | "N:1"
117 ? Refined extends true
118 ? Row | null
119 : Rel extends { readonly nullable: false }
120 ? Row
121 : Row | null
122 : Row[];
123type Terminal =
124 | "all"
125 | "first"
126 | "aggregate"
127 | "groupBy"
128 | "create"
129 | "createAll"
130 | "createAndCount"
131 | "upsert"
132 | "update"
133 | "updateAll"
134 | "updateAndCount"
135 | "delete"
136 | "deleteAll"
137 | "deleteAndCount";
138type Refinement<
139 C extends AnyContract,
140 Ns extends string,
141 M extends string,
142 Rel extends string,
143> = Omit<
144 Native<
145 C,
146 RelatedModelName<C, M, Rel, Ns> & string,
147 RelatedRow<C, Ns, M, Rel>,
148 InitialState<TargetNamespace<C, Relation<C, Ns, M, Rel>, Ns>>
149 >,
150 | Terminal
151 | (Cardinality<Relation<C, Ns, M, Rel>> extends "1:1" | "N:1"
152 ?
153 | "combine"
154 | (string extends keyof AggregateBuilder<C, M, Ns>
155 ? never
156 : keyof AggregateBuilder<C, M, Ns>)
157 : never)
158>;
159type RefinedValue<Rel, T> = T extends {
160 readonly kind: "includeScalar" | "includeCombine";
161}
162 ? T[Extract<keyof T, symbol>]
163 : RelationValue<Rel, NativeRow<T>, true>;
164type RefinementResult =
165 | { readonly _row?: unknown }
166 | { readonly kind: "includeScalar" | "includeCombine" };
167type Model<
168 C extends AnyContract,
169 Ns extends string,
170 M extends string,
171> = Ns extends keyof C["domain"]["namespaces"]
172 ? M extends keyof C["domain"]["namespaces"][Ns]["models"]
173 ? C["domain"]["namespaces"][Ns]["models"][M]
174 : never
175 : never;
176type VariantRow<
177 C extends AnyContract,
178 Ns extends string,
179 M extends string,
180 V extends string,
181> =
182 Model<C, Ns, M> extends {
183 readonly discriminator: { readonly field: infer Field extends string };
184 readonly variants: infer Variants;
185 }
186 ? V extends keyof Variants
187 ? Variants[V] extends { readonly value: infer Value }
188 ? Simplify<
189 Omit<DefaultModelRow<C, M, Ns>, Field> &
190 DefaultModelRow<C, V, Ns> & { [K in Field]: Value }
191 >
192 : never
193 : never
194 : never;
195
196/** Prisma's fluent collection, with native parameter types and Effect terminals. */
197export interface EffectCollection<
198 C extends AnyContract,
199 Ns extends string,
200 M extends string,
201 Row,
202 S extends CollectionTypeState = InitialState<Ns>,
203 E = never,
204 R = never,
205> {
206 where(
207 ...args: Args<Native<C, M, Row, S>["where"]>
208 ): EffectCollection<
209 C,
210 Ns,
211 M,
212 Row,
213 WithState<S, { readonly hasWhere: true }>,
214 E,
215 R
216 >;
217 variant<V extends Parameters<Native<C, M, Row, S>["variant"]>[0]>(
218 name: V,
219 ): EffectCollection<
220 C,
221 Ns,
222 M,
223 VariantRow<C, Ns, M, V>,
224 WithState<S, { readonly hasWhere: true; readonly variantName: V }>,
225 E,
226 R
227 >;
228 include<Rel extends Parameters<Native<C, M, Row, S>["include"]>[0]>(
229 relation: Rel,
230 ): EffectCollection<
231 C,
232 Ns,
233 M,
234 Simplify<
235 Row & {
236 [K in Rel]: RelationValue<
237 Relation<C, Ns, IncludeOwner<C, Ns, M, K, S>, K>,
238 RelatedRow<C, Ns, IncludeOwner<C, Ns, M, K, S>, K>
239 >;
240 }
241 >,
242 S,
243 E,
244 R
245 >;
246 include<
247 Rel extends Parameters<Native<C, M, Row, S>["include"]>[0],
248 Refined extends (Cardinality<
249 Relation<C, Ns, IncludeOwner<C, Ns, M, Rel, S>, Rel>
250 > extends "1:1" | "N:1"
251 ? { readonly _row?: unknown }
252 : RefinementResult),
253 >(
254 relation: Rel,
255 refine: (
256 collection: Refinement<C, Ns, IncludeOwner<C, Ns, M, Rel, S>, Rel>,
257 ) => Refined,
258 ): EffectCollection<
259 C,
260 Ns,
261 M,
262 Simplify<
263 Row & {
264 [K in Rel]: RefinedValue<
265 Relation<C, Ns, IncludeOwner<C, Ns, M, K, S>, K>,
266 Refined
267 >;
268 }
269 >,
270 S,
271 E,
272 R
273 >;
274 select<
275 const Fields extends readonly [
276 keyof DefaultModelRow<C, M, Ns> & string,
277 ...(keyof DefaultModelRow<C, M, Ns> & string)[],
278 ],
279 >(
280 ...fields: Fields
281 ): EffectCollection<
282 C,
283 Ns,
284 M,
285 Simplify<
286 Pick<DefaultModelRow<C, M, Ns>, Fields[number]> &
287 Omit<Row, KeysOfUnion<RootRow<C, Ns, M>>>
288 >,
289 S,
290 E,
291 R
292 >;
293 orderBy(
294 ...args: Parameters<Native<C, M, Row, S>["orderBy"]>
295 ): EffectCollection<
296 C,
297 Ns,
298 M,
299 Row,
300 WithState<S, { readonly hasOrderBy: true }>,
301 E,
302 R
303 >;
304 cursor(
305 ...args: Parameters<Native<C, M, Row, S>["cursor"]>
306 ): EffectCollection<C, Ns, M, Row, S, E, R>;
307 distinct(
308 ...fields: Parameters<Native<C, M, Row, S>["distinct"]>
309 ): EffectCollection<C, Ns, M, Row, S, E, R>;
310 distinctOn(
311 ...fields: Parameters<Native<C, M, Row, S>["distinctOn"]>
312 ): EffectCollection<C, Ns, M, Row, S, E, R>;
313 limit(n: number): EffectCollection<C, Ns, M, Row, S, E, R>;
314 offset(n: number): EffectCollection<C, Ns, M, Row, S, E, R>;
315 groupBy<
316 const Fields extends readonly [
317 keyof DefaultModelRow<C, M, Ns> & string,
318 ...(keyof DefaultModelRow<C, M, Ns> & string)[],
319 ],
320 >(
321 ...fields: Fields
322 ): EffectGroupedCollection<C, Ns, M, Fields, false, E, R>;
323 all(
324 ...args: Parameters<Native<C, M, Row, S>["all"]>
325 ): QueryResult<Row, ClientError | E, R>;
326 first(
327 ...args: Args<Native<C, M, Row, S>["first"]>
328 ): Effect.Effect<Row | null, ClientError | E, R>;
329 aggregate<Spec extends AggregateSpec>(
330 fn: (aggregate: AggregateBuilder<C, M, Ns>) => Spec,
331 ...configure: Parameters<Native<C, M, Row, S>["aggregate"]> extends [
332 unknown,
333 ...infer Rest,
334 ]
335 ? Rest
336 : never
337 ): Effect.Effect<AggregateResult<Spec>, ClientError | E, R>;
338 create(
339 ...args: Args<Native<C, M, Row, S>["create"]>
340 ): Effect.Effect<Row, ClientError | E, R>;
341 createAll(
342 ...args: Parameters<Native<C, M, Row, S>["createAll"]>
343 ): QueryResult<Row, ClientError | E, R>;
344 createAndCount(
345 ...args: Parameters<Native<C, M, Row, S>["createAndCount"]>
346 ): Effect.Effect<number, ClientError | E, R>;
347 upsert(
348 ...args: Parameters<Native<C, M, Row, S>["upsert"]>
349 ): Effect.Effect<Row, ClientError | E, R>;
350 update(
351 ...args: Parameters<Native<C, M, Row, S>["update"]>
352 ): Effect.Effect<Row | null, ClientError | E, R>;
353 updateAll(
354 ...args: Parameters<Native<C, M, Row, S>["updateAll"]>
355 ): QueryResult<Row, ClientError | E, R>;
356 updateAndCount(
357 ...args: Parameters<Native<C, M, Row, S>["updateAndCount"]>
358 ): Effect.Effect<number, ClientError | E, R>;
359 delete(
360 this: S["hasWhere"] extends true
361 ? EffectCollection<C, Ns, M, Row, S, E, R>
362 : never,
363 ...args: Parameters<Native<C, M, Row, S>["delete"]>
364 ): Effect.Effect<Row | null, ClientError | E, R>;
365 deleteAll(
366 this: S["hasWhere"] extends true
367 ? EffectCollection<C, Ns, M, Row, S, E, R>
368 : never,
369 ...args: Parameters<Native<C, M, Row, S>["deleteAll"]>
370 ): QueryResult<Row, ClientError | E, R>;
371 deleteAndCount(
372 this: S["hasWhere"] extends true
373 ? EffectCollection<C, Ns, M, Row, S, E, R>
374 : never,
375 ...args: Parameters<Native<C, M, Row, S>["deleteAndCount"]>
376 ): Effect.Effect<number, ClientError | E, R>;
377}
378
379/** Grouped aggregates retain their selected keys and ordering requirements. */
380export interface EffectGroupedCollection<
381 C extends AnyContract,
382 Ns extends string,
383 M extends string,
384 Fields extends readonly (keyof DefaultModelRow<C, M, Ns> & string)[],
385 Ordered extends boolean = false,
386 E = never,
387 R = never,
388> {
389 having(
390 ...args: Parameters<GroupedCollection<C, M, Fields, Ns, Ordered>["having"]>
391 ): EffectGroupedCollection<C, Ns, M, Fields, Ordered, E, R>;
392 orderBy(
393 ...args: Parameters<GroupedCollection<C, M, Fields, Ns, Ordered>["orderBy"]>
394 ): EffectGroupedCollection<C, Ns, M, Fields, true, E, R>;
395 limit(
396 n: Ordered extends true ? number : never,
397 ): EffectGroupedCollection<C, Ns, M, Fields, Ordered, E, R>;
398 offset(
399 n: Ordered extends true ? number : never,
400 ): EffectGroupedCollection<C, Ns, M, Fields, Ordered, E, R>;
401 aggregate<Spec extends AggregateSpec>(
402 fn: (aggregate: AggregateBuilder<C, M, Ns>) => Spec,
403 ...configure: Parameters<
404 GroupedCollection<C, M, Fields, Ns, Ordered>["aggregate"]
405 > extends [unknown, ...infer Rest]
406 ? Rest
407 : never
408 ): Effect.Effect<
409 Array<
410 Simplify<
411 Pick<DefaultModelRow<C, M, Ns>, Fields[number]> & AggregateResult<Spec>
412 >
413 >,
414 ClientError | E,
415 R
416 >;
417}
418
419export type EffectOrm<C extends AnyContract, E = never, R = never> = {
420 readonly [Ns in keyof C["domain"]["namespaces"] & string]: {
421 readonly [
422 M in keyof C["domain"]["namespaces"][Ns]["models"] & string
423 ]: EffectCollection<
424 C,
425 Ns,
426 M,
427 NativeRow<Root<C, Ns, M>>,
428 InitialState<Ns>,
429 E,
430 R
431 >;
432 };
433};
434
435const TERMINALS = new Set([
436 "all",
437 "first",
438 "aggregate",
439 "create",
440 "createAll",
441 "createAndCount",
442 "upsert",
443 "update",
444 "updateAll",
445 "updateAndCount",
446 "delete",
447 "deleteAll",
448 "deleteAndCount",
449]);
450const STREAMING_TERMINALS = new Set([
451 "all",
452 "createAll",
453 "updateAll",
454 "deleteAll",
455]);
456interface PathStep {
457 readonly prop: string;
458 readonly args?: readonly unknown[];
459}
460const replayPath = (base: unknown, path: readonly PathStep[]): unknown =>
461 path.reduce<any>(
462 (current, step) =>
463 step.args === undefined
464 ? current[step.prop]
465 : current[step.prop](...step.args),
466 base,
467 );
468
469const node = (
470 root: Effect.Effect<unknown, any, any>,
471 path: readonly PathStep[],
472): any =>
473 new Proxy(function () {}, {
474 get: (_target, prop) => {
475 if (typeof prop !== "string" || prop === "then") return undefined;
476 return node(root, [...path, { prop }]);
477 },
478 apply: (_target, _this, args: unknown[]) => {
479 const last = path[path.length - 1]!;
480 const called = [...path.slice(0, -1), { prop: last.prop, args }];
481 if (!TERMINALS.has(last.prop)) return node(root, called);
482 const effect = Effect.flatMap(root, (base) =>
483 Effect.tryPromise({
484 try: () => Promise.resolve(replayPath(base, called)),
485 catch: wrapPrismaError,
486 }),
487 );
488 if (!STREAMING_TERMINALS.has(last.prop)) return effect;
489 return Object.assign(effect, {
490 stream: Stream.unwrap(
491 Effect.flatMap(root, (base) =>
492 Effect.try({
493 try: () =>
494 Stream.fromAsyncIterable(
495 replayPath(base, called) as AsyncIterable<unknown>,
496 wrapPrismaError,
497 ),
498 catch: wrapPrismaError,
499 }),
500 ),
501 ),
502 });
503 },
504 });
505
506/** Replays native builders per execution; refinement callbacks stay native and pure. */
507export const makeOrmProxy = <C extends AnyContract, E, R>(
508 root: Effect.Effect<unknown, E, R>,
509): EffectOrm<C, E, R> => node(root, []);