@shivaedev/effect-prisma 0.0.0 → 0.2.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.
Files changed (104) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +150 -2
  4. package/dist/database.d.ts +35 -0
  5. package/dist/database.d.ts.map +1 -0
  6. package/dist/database.js +83 -0
  7. package/dist/database.js.map +1 -0
  8. package/dist/error.d.ts +52 -0
  9. package/dist/error.d.ts.map +1 -0
  10. package/dist/error.js +48 -0
  11. package/dist/error.js.map +1 -0
  12. package/dist/index.d.ts +4 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +3 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/internal/client-lifecycle.d.ts +7 -0
  17. package/dist/internal/client-lifecycle.d.ts.map +1 -0
  18. package/dist/internal/client-lifecycle.js +19 -0
  19. package/dist/internal/client-lifecycle.js.map +1 -0
  20. package/dist/internal/executor.d.ts +16 -0
  21. package/dist/internal/executor.d.ts.map +1 -0
  22. package/dist/internal/executor.js +2 -0
  23. package/dist/internal/executor.js.map +1 -0
  24. package/dist/internal/promise.d.ts +4 -0
  25. package/dist/internal/promise.d.ts.map +1 -0
  26. package/dist/internal/promise.js +9 -0
  27. package/dist/internal/promise.js.map +1 -0
  28. package/dist/internal/recipe.d.ts +13 -0
  29. package/dist/internal/recipe.d.ts.map +1 -0
  30. package/dist/internal/recipe.js +92 -0
  31. package/dist/internal/recipe.js.map +1 -0
  32. package/dist/internal/relation-plan.d.ts +8 -0
  33. package/dist/internal/relation-plan.d.ts.map +1 -0
  34. package/dist/internal/relation-plan.js +10 -0
  35. package/dist/internal/relation-plan.js.map +1 -0
  36. package/dist/internal/relation-runtime.d.ts +5 -0
  37. package/dist/internal/relation-runtime.d.ts.map +1 -0
  38. package/dist/internal/relation-runtime.js +106 -0
  39. package/dist/internal/relation-runtime.js.map +1 -0
  40. package/dist/internal/testing.d.ts +11 -0
  41. package/dist/internal/testing.d.ts.map +1 -0
  42. package/dist/internal/testing.js +9 -0
  43. package/dist/internal/testing.js.map +1 -0
  44. package/dist/internal/transaction.d.ts +14 -0
  45. package/dist/internal/transaction.d.ts.map +1 -0
  46. package/dist/internal/transaction.js +79 -0
  47. package/dist/internal/transaction.js.map +1 -0
  48. package/dist/internal/vitest-database.d.ts +18 -0
  49. package/dist/internal/vitest-database.d.ts.map +1 -0
  50. package/dist/internal/vitest-database.js +73 -0
  51. package/dist/internal/vitest-database.js.map +1 -0
  52. package/dist/relation/include-metadata.d.ts +34 -0
  53. package/dist/relation/include-metadata.d.ts.map +1 -0
  54. package/dist/relation/include-metadata.js +2 -0
  55. package/dist/relation/include-metadata.js.map +1 -0
  56. package/dist/relation/include.d.ts +25 -0
  57. package/dist/relation/include.d.ts.map +1 -0
  58. package/dist/relation/include.js +2 -0
  59. package/dist/relation/include.js.map +1 -0
  60. package/dist/relation/prisma-methods.d.ts +56 -0
  61. package/dist/relation/prisma-methods.d.ts.map +1 -0
  62. package/dist/relation/prisma-methods.js +2 -0
  63. package/dist/relation/prisma-methods.js.map +1 -0
  64. package/dist/relation.d.ts +34 -0
  65. package/dist/relation.d.ts.map +1 -0
  66. package/dist/relation.js +2 -0
  67. package/dist/relation.js.map +1 -0
  68. package/dist/testing/transaction.d.ts +10 -0
  69. package/dist/testing/transaction.d.ts.map +1 -0
  70. package/dist/testing/transaction.js +6 -0
  71. package/dist/testing/transaction.js.map +1 -0
  72. package/dist/testing/types.d.ts +29 -0
  73. package/dist/testing/types.d.ts.map +1 -0
  74. package/dist/testing/types.js +2 -0
  75. package/dist/testing/types.js.map +1 -0
  76. package/dist/testing/vitest.d.ts +6 -0
  77. package/dist/testing/vitest.d.ts.map +1 -0
  78. package/dist/testing/vitest.js +28 -0
  79. package/dist/testing/vitest.js.map +1 -0
  80. package/dist/testing.d.ts +4 -0
  81. package/dist/testing.d.ts.map +1 -0
  82. package/dist/testing.js +3 -0
  83. package/dist/testing.js.map +1 -0
  84. package/package.json +69 -4
  85. package/src/database.ts +232 -0
  86. package/src/error.ts +89 -0
  87. package/src/index.ts +14 -0
  88. package/src/internal/client-lifecycle.ts +31 -0
  89. package/src/internal/executor.ts +28 -0
  90. package/src/internal/promise.ts +16 -0
  91. package/src/internal/recipe.ts +148 -0
  92. package/src/internal/relation-plan.ts +22 -0
  93. package/src/internal/relation-runtime.ts +249 -0
  94. package/src/internal/testing.ts +32 -0
  95. package/src/internal/transaction.ts +149 -0
  96. package/src/internal/vitest-database.ts +210 -0
  97. package/src/relation/include-metadata.ts +102 -0
  98. package/src/relation/include.ts +169 -0
  99. package/src/relation/prisma-methods.ts +196 -0
  100. package/src/relation.ts +118 -0
  101. package/src/testing/transaction.ts +28 -0
  102. package/src/testing/types.ts +67 -0
  103. package/src/testing/vitest.ts +67 -0
  104. package/src/testing.ts +8 -0
package/src/error.ts ADDED
@@ -0,0 +1,89 @@
1
+ import { Data, Redacted } from "effect";
2
+
3
+ export class PrismaRuntimeFailure extends Data.TaggedClass(
4
+ "PrismaRuntimeFailure",
5
+ )<{
6
+ readonly code: string;
7
+ readonly original: Redacted.Redacted<unknown>;
8
+ }> {}
9
+
10
+ export class PrismaQueryFailure extends Data.TaggedClass("PrismaQueryFailure")<{
11
+ readonly sqlState?: string;
12
+ readonly constraint?: string;
13
+ readonly table?: string;
14
+ readonly column?: string;
15
+ readonly original: Redacted.Redacted<unknown>;
16
+ }> {}
17
+
18
+ export class PrismaConnectionFailure extends Data.TaggedClass(
19
+ "PrismaConnectionFailure",
20
+ )<{
21
+ readonly transient?: boolean;
22
+ readonly original: Redacted.Redacted<unknown>;
23
+ }> {}
24
+
25
+ export type PrismaErrorReason =
26
+ | PrismaRuntimeFailure
27
+ | PrismaQueryFailure
28
+ | PrismaConnectionFailure;
29
+
30
+ export class PrismaError extends Data.TaggedError("PrismaError")<{
31
+ readonly reason: PrismaErrorReason;
32
+ }> {}
33
+
34
+ type PrismaFailure =
35
+ | { readonly code: string }
36
+ | {
37
+ readonly kind: "sql_query";
38
+ readonly sqlState?: string;
39
+ readonly constraint?: string;
40
+ readonly table?: string;
41
+ readonly column?: string;
42
+ }
43
+ | {
44
+ readonly kind: "sql_connection";
45
+ readonly transient?: boolean;
46
+ };
47
+
48
+ export const isPrismaFailure = (error: unknown): error is PrismaFailure => {
49
+ if (typeof error !== "object" || error === null) {
50
+ return false;
51
+ }
52
+ if ("code" in error && typeof error.code === "string") {
53
+ return true;
54
+ }
55
+ return (
56
+ "kind" in error &&
57
+ (error.kind === "sql_query" || error.kind === "sql_connection")
58
+ );
59
+ };
60
+
61
+ export const toPrismaError = (error: PrismaFailure): PrismaError => {
62
+ const original = Redacted.make(error);
63
+
64
+ if ("kind" in error && error.kind === "sql_query") {
65
+ return new PrismaError({
66
+ reason: new PrismaQueryFailure({
67
+ sqlState: error.sqlState,
68
+ constraint: error.constraint,
69
+ table: error.table,
70
+ column: error.column,
71
+ original,
72
+ }),
73
+ });
74
+ }
75
+ if ("kind" in error && error.kind === "sql_connection") {
76
+ return new PrismaError({
77
+ reason: new PrismaConnectionFailure({
78
+ transient: error.transient,
79
+ original,
80
+ }),
81
+ });
82
+ }
83
+ return new PrismaError({
84
+ reason: new PrismaRuntimeFailure({
85
+ code: error.code,
86
+ original,
87
+ }),
88
+ });
89
+ };
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ export {
2
+ type DatabaseDefinition,
3
+ type DatabaseLayerOptions,
4
+ type DatabaseService,
5
+ makeDatabase,
6
+ } from "./database.js";
7
+ export {
8
+ PrismaConnectionFailure,
9
+ PrismaError,
10
+ type PrismaErrorReason,
11
+ PrismaQueryFailure,
12
+ PrismaRuntimeFailure,
13
+ } from "./error.js";
14
+ export type { Relation } from "./relation.js";
@@ -0,0 +1,31 @@
1
+ export interface ClientLifecycle {
2
+ readonly connect: () => PromiseLike<unknown>;
3
+ readonly close: () => PromiseLike<void>;
4
+ }
5
+
6
+ export const acquireConnectedClient = async <A>(
7
+ client: ClientLifecycle,
8
+ initialize: () => A,
9
+ ): Promise<A> => {
10
+ try {
11
+ await client.connect();
12
+ return initialize();
13
+ } catch (error) {
14
+ await Promise.resolve(client.close()).catch(() => undefined);
15
+ throw error;
16
+ }
17
+ };
18
+
19
+ const reservedModelNames = new Set(["transaction"]);
20
+
21
+ export const assertAvailableModelNames = (
22
+ modelNames: ReadonlyArray<string>,
23
+ ): void => {
24
+ for (const modelName of modelNames) {
25
+ if (reservedModelNames.has(modelName)) {
26
+ throw new TypeError(
27
+ `Prisma model name conflicts with the database facade: ${modelName}`,
28
+ );
29
+ }
30
+ }
31
+ };
@@ -0,0 +1,28 @@
1
+ import type { Contract as PrismaContract } from "@prisma-next/contract/types";
2
+ import type { PostgresClient } from "@prisma-next/postgres/runtime";
3
+ import type { SqlStorage } from "@prisma-next/sql-contract/types";
4
+ import type { Context } from "effect";
5
+
6
+ export type AnyPostgresContract = PrismaContract<SqlStorage>;
7
+
8
+ export interface DatabaseExecutor<
9
+ Models extends object,
10
+ Contract extends AnyPostgresContract = AnyPostgresContract,
11
+ > {
12
+ readonly client: PostgresClient<Contract>;
13
+ readonly models: Models;
14
+ readonly transactional: boolean;
15
+ }
16
+
17
+ export type ExecutorService<
18
+ Models extends object,
19
+ Contract extends AnyPostgresContract = AnyPostgresContract,
20
+ > = Context.Service<
21
+ ExecutorIdentifier<Models>,
22
+ DatabaseExecutor<Models, Contract>
23
+ >;
24
+
25
+ export interface ExecutorIdentifier<Models extends object> {
26
+ readonly _models: Models;
27
+ readonly _executorIdentifier: unique symbol;
28
+ }
@@ -0,0 +1,16 @@
1
+ import { Effect } from "effect";
2
+ import { isPrismaFailure, type PrismaError, toPrismaError } from "../error.js";
3
+
4
+ export const fromPrismaPromise = <A>(
5
+ evaluate: (signal: AbortSignal) => PromiseLike<A>,
6
+ ): Effect.Effect<A, PrismaError> =>
7
+ Effect.tryPromise({
8
+ try: (signal) => Promise.resolve(evaluate(signal)),
9
+ catch: (error) => error,
10
+ }).pipe(
11
+ Effect.catch((error) =>
12
+ isPrismaFailure(error)
13
+ ? Effect.fail(toPrismaError(error))
14
+ : Effect.die(error),
15
+ ),
16
+ );
@@ -0,0 +1,148 @@
1
+ import { getRelationPlan, type RelationPlan } from "./relation-plan.js";
2
+
3
+ export interface RelationOperation {
4
+ readonly name: PropertyKey;
5
+ readonly arguments: ReadonlyArray<unknown>;
6
+ }
7
+
8
+ export interface RelationRecipe {
9
+ readonly model: string;
10
+ readonly parent?: RelationRecipe;
11
+ readonly operation?: RelationOperation;
12
+ }
13
+
14
+ export const rootRecipe = (model: string): RelationRecipe => ({ model });
15
+
16
+ export const appendOperation = (
17
+ parent: RelationRecipe,
18
+ name: PropertyKey,
19
+ arguments_: ReadonlyArray<unknown>,
20
+ ): RelationRecipe => ({
21
+ model: parent.model,
22
+ parent,
23
+ operation: {
24
+ name,
25
+ arguments: arguments_,
26
+ },
27
+ });
28
+
29
+ const operations = (
30
+ recipe: RelationRecipe,
31
+ ): ReadonlyArray<RelationOperation> => {
32
+ const reversed: Array<RelationOperation> = [];
33
+ let current: RelationRecipe | undefined = recipe;
34
+
35
+ while (current !== undefined) {
36
+ if (current.operation !== undefined) {
37
+ reversed.push(current.operation);
38
+ }
39
+ current = current.parent;
40
+ }
41
+
42
+ return reversed.reverse();
43
+ };
44
+
45
+ const applyMethod = (
46
+ current: unknown,
47
+ name: PropertyKey,
48
+ arguments_: ReadonlyArray<unknown>,
49
+ model: string,
50
+ ): unknown => {
51
+ if (
52
+ typeof current !== "object" ||
53
+ current === null ||
54
+ typeof Reflect.get(current, name) !== "function"
55
+ ) {
56
+ throw new TypeError(`Cannot call ${String(name)} while replaying ${model}`);
57
+ }
58
+
59
+ const method = Reflect.get(current, name) as (
60
+ ...arguments_: ReadonlyArray<unknown>
61
+ ) => unknown;
62
+ return Reflect.apply(method, current, arguments_);
63
+ };
64
+
65
+ const replayPlan = (collection: unknown, plan: RelationPlan): unknown => {
66
+ const relatedModel =
67
+ typeof collection === "object" && collection !== null
68
+ ? Reflect.get(collection, "modelName")
69
+ : undefined;
70
+ if (typeof relatedModel === "string" && relatedModel !== plan.recipe.model) {
71
+ throw new TypeError(
72
+ `Included relation expects ${relatedModel}, received ${plan.recipe.model}`,
73
+ );
74
+ }
75
+
76
+ const refined = replayRecipeFrom(collection, plan.recipe);
77
+ if (plan.terminal !== "count") {
78
+ return refined;
79
+ }
80
+ return applyMethod(refined, "count", [], plan.recipe.model);
81
+ };
82
+
83
+ const includeRefinement = (
84
+ value: unknown,
85
+ ): ((collection: unknown) => unknown) => {
86
+ const plan = getRelationPlan(value);
87
+ if (plan !== undefined) {
88
+ return (collection) => replayPlan(collection, plan);
89
+ }
90
+
91
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
92
+ throw new TypeError(
93
+ "An included relation must be a Relation or query record",
94
+ );
95
+ }
96
+
97
+ const entries = Object.entries(value);
98
+ if (entries.length === 0) {
99
+ throw new TypeError("An included query record cannot be empty");
100
+ }
101
+
102
+ const plans = entries.map(([name, query]) => {
103
+ const queryPlan = getRelationPlan(query);
104
+ if (queryPlan === undefined) {
105
+ throw new TypeError(`Included query "${name}" is not a Relation`);
106
+ }
107
+ return [name, queryPlan] as const;
108
+ });
109
+ const model = plans[0]?.[1].recipe.model;
110
+ if (plans.some(([, queryPlan]) => queryPlan.recipe.model !== model)) {
111
+ throw new TypeError("Included queries must use the same related model");
112
+ }
113
+
114
+ return (collection) => {
115
+ const branches = Object.fromEntries(
116
+ plans.map(([name, queryPlan]) => [
117
+ name,
118
+ replayPlan(collection, queryPlan),
119
+ ]),
120
+ );
121
+ return applyMethod(collection, "combine", [branches], model ?? "relation");
122
+ };
123
+ };
124
+
125
+ const replayRecipeFrom = (root: unknown, recipe: RelationRecipe): unknown => {
126
+ let current = root;
127
+ for (const operation of operations(recipe)) {
128
+ const arguments_ =
129
+ operation.name === "include" && operation.arguments.length === 2
130
+ ? [operation.arguments[0], includeRefinement(operation.arguments[1])]
131
+ : operation.arguments;
132
+ current = applyMethod(current, operation.name, arguments_, recipe.model);
133
+ }
134
+ return current;
135
+ };
136
+
137
+ export const replayRecipe = (
138
+ models: object,
139
+ recipe: RelationRecipe,
140
+ ): unknown => {
141
+ const current: unknown = Reflect.get(models, recipe.model);
142
+
143
+ if (current === undefined) {
144
+ throw new TypeError(`Unknown Prisma model: ${recipe.model}`);
145
+ }
146
+
147
+ return replayRecipeFrom(current, recipe);
148
+ };
@@ -0,0 +1,22 @@
1
+ import type { RelationRecipe } from "./recipe.js";
2
+
3
+ export const RelationPlanTypeId = Symbol.for(
4
+ "@shivaedev/effect-prisma/RelationPlan",
5
+ );
6
+
7
+ export interface RelationPlan {
8
+ readonly recipe: RelationRecipe;
9
+ readonly terminal?: PropertyKey;
10
+ }
11
+
12
+ export const getRelationPlan = (value: unknown): RelationPlan | undefined => {
13
+ if (
14
+ typeof value !== "object" ||
15
+ value === null ||
16
+ !Reflect.has(value, RelationPlanTypeId)
17
+ ) {
18
+ return undefined;
19
+ }
20
+
21
+ return Reflect.get(value, RelationPlanTypeId) as RelationPlan;
22
+ };
@@ -0,0 +1,249 @@
1
+ import type { Context } from "effect";
2
+ import { Effect, Effectable, Option, Stream } from "effect";
3
+ import { isPrismaFailure, type PrismaError, toPrismaError } from "../error.js";
4
+ import type { Relation } from "../relation.js";
5
+ import type {
6
+ AnyPostgresContract,
7
+ DatabaseExecutor,
8
+ ExecutorIdentifier,
9
+ } from "./executor.js";
10
+ import { fromPrismaPromise } from "./promise.js";
11
+ import {
12
+ appendOperation,
13
+ type RelationRecipe,
14
+ replayRecipe,
15
+ rootRecipe,
16
+ } from "./recipe.js";
17
+ import { RelationPlanTypeId } from "./relation-plan.js";
18
+
19
+ type AnyFunction = (...arguments_: ReadonlyArray<never>) => unknown;
20
+
21
+ interface RelationRuntime<
22
+ Models extends object,
23
+ Contract extends AnyPostgresContract,
24
+ > {
25
+ readonly executor: Context.Service<
26
+ ExecutorIdentifier<Models>,
27
+ DatabaseExecutor<Models, Contract>
28
+ >;
29
+ readonly recipe: RelationRecipe;
30
+ readonly terminal?: PropertyKey;
31
+ }
32
+
33
+ const evaluateResult = (
34
+ value: unknown,
35
+ terminal: PropertyKey | undefined,
36
+ ): Effect.Effect<unknown, PrismaError> => {
37
+ if (
38
+ terminal === "count" &&
39
+ typeof value === "object" &&
40
+ value !== null &&
41
+ typeof Reflect.get(value, "aggregate") === "function"
42
+ ) {
43
+ const aggregate = Reflect.apply(
44
+ Reflect.get(value, "aggregate") as AnyFunction,
45
+ value,
46
+ [(summary: { count(): unknown }) => ({ count: summary.count() })],
47
+ );
48
+ return fromPrismaPromise(
49
+ () => aggregate as PromiseLike<{ count: number }>,
50
+ ).pipe(Effect.map((result) => result.count));
51
+ }
52
+
53
+ if (
54
+ terminal === "exists" &&
55
+ typeof value === "object" &&
56
+ value !== null &&
57
+ typeof Reflect.get(value, "first") === "function"
58
+ ) {
59
+ const first = Reflect.apply(
60
+ Reflect.get(value, "first") as AnyFunction,
61
+ value,
62
+ [],
63
+ );
64
+ return fromPrismaPromise(() => first as PromiseLike<unknown>).pipe(
65
+ Effect.map((result) => result !== null),
66
+ );
67
+ }
68
+
69
+ const executable =
70
+ typeof value === "object" &&
71
+ value !== null &&
72
+ typeof Reflect.get(value, "all") === "function"
73
+ ? Reflect.apply(Reflect.get(value, "all") as AnyFunction, value, [])
74
+ : value;
75
+
76
+ if (
77
+ typeof executable === "object" &&
78
+ executable !== null &&
79
+ "then" in executable
80
+ ) {
81
+ return fromPrismaPromise(() => executable as PromiseLike<unknown>).pipe(
82
+ Effect.map((result) =>
83
+ terminal === "first" ? Option.fromNullishOr(result) : result,
84
+ ),
85
+ );
86
+ }
87
+
88
+ return Effect.succeed(executable);
89
+ };
90
+
91
+ interface RelationValue<
92
+ Models extends object,
93
+ Contract extends AnyPostgresContract,
94
+ > extends Effect.Effect<unknown, PrismaError, ExecutorIdentifier<Models>> {
95
+ readonly [RelationPlanTypeId]: {
96
+ readonly recipe: RelationRecipe;
97
+ readonly terminal?: PropertyKey;
98
+ };
99
+ readonly runtime: RelationRuntime<Models, Contract>;
100
+ readonly stream: Stream.Stream<
101
+ unknown,
102
+ PrismaError,
103
+ ExecutorIdentifier<Models>
104
+ >;
105
+ }
106
+
107
+ const relationEffect = <
108
+ Models extends object,
109
+ Contract extends AnyPostgresContract,
110
+ >(
111
+ self: RelationValue<Models, Contract>,
112
+ ): Effect.Effect<unknown, PrismaError, ExecutorIdentifier<Models>> =>
113
+ Effect.flatMap(self.runtime.executor, (executor) =>
114
+ Effect.suspend(() =>
115
+ evaluateResult(
116
+ replayRecipe(executor.models, self.runtime.recipe),
117
+ self.runtime.terminal,
118
+ ),
119
+ ),
120
+ ).pipe(
121
+ Effect.withSpan(
122
+ `prisma.${self.runtime.recipe.model}.${String(self.runtime.terminal ?? "all")}`,
123
+ {
124
+ kind: "client",
125
+ attributes: {
126
+ "db.system": "postgresql",
127
+ "db.model": self.runtime.recipe.model,
128
+ "db.operation": String(self.runtime.terminal ?? "all"),
129
+ },
130
+ },
131
+ ),
132
+ );
133
+
134
+ const relationStream = <
135
+ Models extends object,
136
+ Contract extends AnyPostgresContract,
137
+ >(
138
+ self: RelationValue<Models, Contract>,
139
+ ): Stream.Stream<unknown, PrismaError, ExecutorIdentifier<Models>> => {
140
+ const result = Effect.map(self.runtime.executor, (executor) => {
141
+ const collection = replayRecipe(executor.models, self.runtime.recipe);
142
+ if (
143
+ typeof collection !== "object" ||
144
+ collection === null ||
145
+ typeof Reflect.get(collection, "all") !== "function"
146
+ ) {
147
+ throw new TypeError("Only collection Relations can be streamed");
148
+ }
149
+ return Reflect.apply(
150
+ Reflect.get(collection, "all") as AnyFunction,
151
+ collection,
152
+ [],
153
+ ) as AsyncIterable<unknown>;
154
+ });
155
+
156
+ return Stream.unwrap(
157
+ Effect.map(result, (iterable) =>
158
+ Stream.fromAsyncIterable(iterable, (error) => error).pipe(
159
+ Stream.catch((error) =>
160
+ isPrismaFailure(error)
161
+ ? Stream.fail(toPrismaError(error))
162
+ : Stream.die(error),
163
+ ),
164
+ ),
165
+ ),
166
+ );
167
+ };
168
+
169
+ const RelationPrototype = {
170
+ ...Effectable.Prototype<
171
+ RelationValue<Record<string, unknown>, AnyPostgresContract>
172
+ >({
173
+ label: "EffectPrismaRelation",
174
+ evaluate() {
175
+ return relationEffect(this);
176
+ },
177
+ }),
178
+ get stream(): Stream.Stream<
179
+ unknown,
180
+ PrismaError,
181
+ ExecutorIdentifier<Record<string, unknown>>
182
+ > {
183
+ return relationStream(
184
+ this as RelationValue<Record<string, unknown>, AnyPostgresContract>,
185
+ );
186
+ },
187
+ };
188
+
189
+ const makeRelationProxy = <
190
+ Models extends object,
191
+ Contract extends AnyPostgresContract,
192
+ >(
193
+ runtime: RelationRuntime<Models, Contract>,
194
+ ): unknown => {
195
+ const target = Object.assign(Object.create(RelationPrototype), {
196
+ [RelationPlanTypeId]: {
197
+ recipe: runtime.recipe,
198
+ ...(runtime.terminal === undefined ? {} : { terminal: runtime.terminal }),
199
+ },
200
+ runtime,
201
+ }) as RelationValue<Models, Contract>;
202
+
203
+ return new Proxy(target, {
204
+ get(self, property, receiver) {
205
+ if (property === "then") {
206
+ return undefined;
207
+ }
208
+ if (Reflect.has(self, property)) {
209
+ return Reflect.get(self, property, receiver);
210
+ }
211
+ if (property === "exists") {
212
+ return () =>
213
+ makeRelationProxy({
214
+ ...runtime,
215
+ terminal: "exists",
216
+ });
217
+ }
218
+ if (property === "count") {
219
+ return () =>
220
+ makeRelationProxy({
221
+ ...runtime,
222
+ terminal: "count",
223
+ });
224
+ }
225
+ return (...arguments_: ReadonlyArray<unknown>) =>
226
+ makeRelationProxy({
227
+ ...runtime,
228
+ recipe: appendOperation(runtime.recipe, property, arguments_),
229
+ terminal: property,
230
+ });
231
+ },
232
+ });
233
+ };
234
+
235
+ export const makeModelRelation = <
236
+ Collection,
237
+ Models extends object,
238
+ Contract extends AnyPostgresContract = AnyPostgresContract,
239
+ >(
240
+ executor: Context.Service<
241
+ ExecutorIdentifier<Models>,
242
+ DatabaseExecutor<Models, Contract>
243
+ >,
244
+ model: string,
245
+ ): Relation<Collection, ExecutorIdentifier<Models>> =>
246
+ makeRelationProxy({
247
+ executor,
248
+ recipe: rootRecipe(model),
249
+ }) as Relation<Collection, ExecutorIdentifier<Models>>;
@@ -0,0 +1,32 @@
1
+ import type { Effect } from "effect";
2
+ import type { PrismaError } from "../error.js";
3
+
4
+ export const DatabaseTestingTypeId: unique symbol = Symbol.for(
5
+ "@shivaedev/effect-prisma/DatabaseTesting",
6
+ );
7
+
8
+ export interface DatabaseTesting<Requirement> {
9
+ readonly withTestTransaction: <A, E, R>(
10
+ program: Effect.Effect<A, E, R>,
11
+ ) => Effect.Effect<A, E | PrismaError, R | Requirement>;
12
+ }
13
+
14
+ export interface DatabaseWithTesting<Requirement> {
15
+ readonly [DatabaseTestingTypeId]: DatabaseTesting<Requirement>;
16
+ }
17
+
18
+ export const getDatabaseTesting = <Requirement>(
19
+ database: object,
20
+ ): DatabaseTesting<Requirement> => {
21
+ const testing = Reflect.get(database, DatabaseTestingTypeId) as
22
+ | DatabaseTesting<Requirement>
23
+ | undefined;
24
+
25
+ if (testing === undefined) {
26
+ throw new TypeError(
27
+ "The database was not created by this copy of @shivaedev/effect-prisma",
28
+ );
29
+ }
30
+
31
+ return testing;
32
+ };