@shivaedev/effect-prisma 0.6.3 → 0.7.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.
- package/CHANGELOG.md +26 -1
- package/dist/bin/normalize-contract.js +1 -0
- package/dist/bin/normalize-contract.js.map +1 -1
- package/dist/database.d.ts +4 -4
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +12 -12
- package/dist/database.js.map +1 -1
- package/dist/error.d.ts +5 -5
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +5 -6
- package/dist/error.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/internal/adapters/transaction-settlement.d.ts +10 -0
- package/dist/internal/adapters/transaction-settlement.d.ts.map +1 -0
- package/dist/internal/adapters/transaction-settlement.js +56 -0
- package/dist/internal/adapters/transaction-settlement.js.map +1 -0
- package/dist/internal/client-lifecycle.d.ts +2 -2
- package/dist/internal/client-lifecycle.d.ts.map +1 -1
- package/dist/internal/client-lifecycle.js.map +1 -1
- package/dist/internal/contract-normalization.d.ts +0 -4
- package/dist/internal/contract-normalization.d.ts.map +1 -1
- package/dist/internal/contract-normalization.js +5 -10
- package/dist/internal/contract-normalization.js.map +1 -1
- package/dist/internal/database-facade.d.ts +19 -0
- package/dist/internal/database-facade.d.ts.map +1 -0
- package/dist/internal/database-facade.js +79 -0
- package/dist/internal/database-facade.js.map +1 -0
- package/dist/internal/database-factory.d.ts +4 -48
- package/dist/internal/database-factory.d.ts.map +1 -1
- package/dist/internal/database-factory.js +11 -90
- package/dist/internal/database-factory.js.map +1 -1
- package/dist/internal/database-types.d.ts +33 -0
- package/dist/internal/database-types.d.ts.map +1 -0
- package/dist/internal/database-types.js +2 -0
- package/dist/internal/database-types.js.map +1 -0
- package/dist/internal/dynamic.d.ts +5 -0
- package/dist/internal/dynamic.d.ts.map +1 -0
- package/dist/internal/dynamic.js +12 -0
- package/dist/internal/dynamic.js.map +1 -0
- package/dist/internal/executor.d.ts +3 -7
- package/dist/internal/executor.d.ts.map +1 -1
- package/dist/internal/promise.d.ts +1 -1
- package/dist/internal/promise.d.ts.map +1 -1
- package/dist/internal/promise.js +2 -4
- package/dist/internal/promise.js.map +1 -1
- package/dist/internal/query-execution.d.ts +2 -2
- package/dist/internal/query-execution.d.ts.map +1 -1
- package/dist/internal/query-execution.js +2 -6
- package/dist/internal/query-execution.js.map +1 -1
- package/dist/internal/recipe.d.ts +3 -3
- package/dist/internal/recipe.d.ts.map +1 -1
- package/dist/internal/recipe.js +9 -20
- package/dist/internal/recipe.js.map +1 -1
- package/dist/internal/relation-plan.d.ts +1 -1
- package/dist/internal/relation-result.d.ts +4 -0
- package/dist/internal/relation-result.d.ts.map +1 -0
- package/dist/internal/relation-result.js +19 -0
- package/dist/internal/relation-result.js.map +1 -0
- package/dist/internal/relation-runtime.d.ts +3 -3
- package/dist/internal/relation-runtime.d.ts.map +1 -1
- package/dist/internal/relation-runtime.js +12 -37
- package/dist/internal/relation-runtime.js.map +1 -1
- package/dist/internal/relation-stream.d.ts +3 -3
- package/dist/internal/relation-stream.d.ts.map +1 -1
- package/dist/internal/relation-stream.js +20 -24
- package/dist/internal/relation-stream.js.map +1 -1
- package/dist/internal/sqlite-datetime.d.ts +1 -27
- package/dist/internal/sqlite-datetime.d.ts.map +1 -1
- package/dist/internal/sqlite-datetime.js +16 -35
- package/dist/internal/sqlite-datetime.js.map +1 -1
- package/dist/internal/sqlite-pragmas.d.ts +1 -13
- package/dist/internal/sqlite-pragmas.d.ts.map +1 -1
- package/dist/internal/sqlite-pragmas.js +2 -12
- package/dist/internal/sqlite-pragmas.js.map +1 -1
- package/dist/internal/testing.d.ts +1 -1
- package/dist/internal/testing.d.ts.map +1 -1
- package/dist/internal/testing.js +5 -2
- package/dist/internal/testing.js.map +1 -1
- package/dist/internal/transaction.d.ts +5 -5
- package/dist/internal/transaction.d.ts.map +1 -1
- package/dist/internal/transaction.js +5 -53
- package/dist/internal/transaction.js.map +1 -1
- package/dist/relation/include-metadata.d.ts +1 -1
- package/dist/relation/include-metadata.d.ts.map +1 -1
- package/dist/relation/include.d.ts +8 -5
- package/dist/relation/include.d.ts.map +1 -1
- package/dist/relation/prisma-methods.d.ts +17 -17
- package/dist/relation/prisma-methods.d.ts.map +1 -1
- package/dist/relation.d.ts +4 -4
- package/dist/relation.d.ts.map +1 -1
- package/dist/sqlite.d.ts +5 -10
- package/dist/sqlite.d.ts.map +1 -1
- package/dist/sqlite.js +14 -12
- package/dist/sqlite.js.map +1 -1
- package/dist/testing/transaction.d.ts +6 -7
- package/dist/testing/transaction.d.ts.map +1 -1
- package/dist/testing/transaction.js +2 -2
- package/dist/testing/transaction.js.map +1 -1
- package/dist/testing/types.d.ts +7 -7
- package/dist/testing/types.d.ts.map +1 -1
- package/dist/testing/vitest.d.ts +3 -3
- package/dist/testing/vitest.d.ts.map +1 -1
- package/dist/testing/vitest.js +2 -2
- package/dist/testing/vitest.js.map +1 -1
- package/dist/testing.d.ts +3 -3
- package/package.json +35 -37
- package/src/bin/normalize-contract.ts +2 -1
- package/src/database.ts +49 -67
- package/src/error.ts +13 -23
- package/src/index.ts +3 -3
- package/src/internal/adapters/transaction-settlement.ts +70 -0
- package/src/internal/client-lifecycle.ts +4 -11
- package/src/internal/contract-normalization.ts +6 -14
- package/src/internal/database-facade.ts +138 -0
- package/src/internal/database-factory.ts +34 -306
- package/src/internal/database-types.ts +65 -0
- package/src/internal/dynamic.ts +16 -0
- package/src/internal/executor.ts +4 -11
- package/src/internal/promise.ts +4 -12
- package/src/internal/query-execution.ts +4 -8
- package/src/internal/recipe.ts +24 -89
- package/src/internal/relation-plan.ts +1 -1
- package/src/internal/relation-result.ts +27 -0
- package/src/internal/relation-runtime.ts +45 -135
- package/src/internal/relation-stream.ts +24 -51
- package/src/internal/sqlite-datetime.ts +20 -54
- package/src/internal/sqlite-pragmas.ts +3 -16
- package/src/internal/testing.ts +13 -15
- package/src/internal/transaction.ts +16 -109
- package/src/relation/include-metadata.ts +11 -43
- package/src/relation/include.ts +46 -140
- package/src/relation/prisma-methods.ts +36 -122
- package/src/relation.ts +20 -48
- package/src/sqlite.ts +59 -85
- package/src/testing/transaction.ts +20 -33
- package/src/testing/types.ts +15 -34
- package/src/testing/vitest.ts +14 -28
- package/src/testing.ts +3 -3
package/src/internal/recipe.ts
CHANGED
|
@@ -1,35 +1,30 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { hasMethod, invokeMethod } from "./dynamic.ts";
|
|
2
|
+
import { getRelationPlan, type RelationPlan } from "./relation-plan.ts";
|
|
2
3
|
|
|
3
4
|
export interface RelationOperation {
|
|
5
|
+
readonly arguments: readonly unknown[];
|
|
4
6
|
readonly name: PropertyKey;
|
|
5
|
-
readonly arguments: ReadonlyArray<unknown>;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
export interface RelationRecipe {
|
|
9
10
|
readonly model: string;
|
|
10
|
-
readonly parent?: RelationRecipe;
|
|
11
11
|
readonly operation?: RelationOperation;
|
|
12
|
+
readonly parent?: RelationRecipe;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export const rootRecipe = (model: string): RelationRecipe => ({ model });
|
|
15
16
|
|
|
16
|
-
export const appendOperation = (
|
|
17
|
-
parent: RelationRecipe,
|
|
18
|
-
name: PropertyKey,
|
|
19
|
-
arguments_: ReadonlyArray<unknown>,
|
|
20
|
-
): RelationRecipe => ({
|
|
17
|
+
export const appendOperation = (parent: RelationRecipe, name: PropertyKey, arguments_: readonly unknown[]): RelationRecipe => ({
|
|
21
18
|
model: parent.model,
|
|
22
|
-
parent,
|
|
23
19
|
operation: {
|
|
24
|
-
name,
|
|
25
20
|
arguments: arguments_,
|
|
21
|
+
name,
|
|
26
22
|
},
|
|
23
|
+
parent,
|
|
27
24
|
});
|
|
28
25
|
|
|
29
|
-
const operations = (
|
|
30
|
-
|
|
31
|
-
): ReadonlyArray<RelationOperation> => {
|
|
32
|
-
const reversed: Array<RelationOperation> = [];
|
|
26
|
+
const operations = (recipe: RelationRecipe): readonly RelationOperation[] => {
|
|
27
|
+
const reversed: RelationOperation[] = [];
|
|
33
28
|
let current: RelationRecipe | undefined = recipe;
|
|
34
29
|
|
|
35
30
|
while (current !== undefined) {
|
|
@@ -42,81 +37,43 @@ const operations = (
|
|
|
42
37
|
return reversed.reverse();
|
|
43
38
|
};
|
|
44
39
|
|
|
45
|
-
const applyMethod = (
|
|
46
|
-
current
|
|
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
|
-
) {
|
|
40
|
+
const applyMethod = (current: unknown, name: PropertyKey, arguments_: readonly unknown[], model: string): unknown => {
|
|
41
|
+
if (!hasMethod(current, name)) {
|
|
56
42
|
throw new TypeError(`Cannot call ${String(name)} while replaying ${model}`);
|
|
57
43
|
}
|
|
58
|
-
|
|
59
|
-
const method = Reflect.get(current, name) as (
|
|
60
|
-
...arguments_: ReadonlyArray<unknown>
|
|
61
|
-
) => unknown;
|
|
62
|
-
return Reflect.apply(method, current, arguments_);
|
|
44
|
+
return invokeMethod(current, name, arguments_);
|
|
63
45
|
};
|
|
64
46
|
|
|
65
|
-
const replayPlan = (
|
|
66
|
-
collection: unknown,
|
|
67
|
-
plan: RelationPlan,
|
|
68
|
-
owner: object,
|
|
69
|
-
transactionIdentity: object | undefined,
|
|
70
|
-
): unknown => {
|
|
47
|
+
const replayPlan = (collection: unknown, plan: RelationPlan, owner: object, transactionIdentity: object | undefined): unknown => {
|
|
71
48
|
if (!plan.liveness.open) {
|
|
72
49
|
throw new TypeError("Included Relation is closed");
|
|
73
50
|
}
|
|
74
51
|
if (plan.owner !== owner) {
|
|
75
52
|
throw new TypeError("Included Relations must use the same Database");
|
|
76
53
|
}
|
|
77
|
-
if (
|
|
78
|
-
plan.transactionIdentity !== undefined &&
|
|
79
|
-
plan.transactionIdentity !== transactionIdentity
|
|
80
|
-
) {
|
|
54
|
+
if (plan.transactionIdentity !== undefined && plan.transactionIdentity !== transactionIdentity) {
|
|
81
55
|
throw new TypeError("Included Relation belongs to another transaction");
|
|
82
56
|
}
|
|
83
|
-
const relatedModel =
|
|
84
|
-
typeof collection === "object" && collection !== null
|
|
85
|
-
? Reflect.get(collection, "modelName")
|
|
86
|
-
: undefined;
|
|
57
|
+
const relatedModel = typeof collection === "object" && collection !== null ? Reflect.get(collection, "modelName") : undefined;
|
|
87
58
|
if (typeof relatedModel === "string" && relatedModel !== plan.recipe.model) {
|
|
88
|
-
throw new TypeError(
|
|
89
|
-
`Included relation expects ${relatedModel}, received ${plan.recipe.model}`,
|
|
90
|
-
);
|
|
59
|
+
throw new TypeError(`Included relation expects ${relatedModel}, received ${plan.recipe.model}`);
|
|
91
60
|
}
|
|
92
61
|
|
|
93
|
-
const refined = replayRecipeFrom(
|
|
94
|
-
collection,
|
|
95
|
-
plan.recipe,
|
|
96
|
-
owner,
|
|
97
|
-
transactionIdentity,
|
|
98
|
-
);
|
|
62
|
+
const refined = replayRecipeFrom(collection, plan.recipe, owner, transactionIdentity);
|
|
99
63
|
if (plan.terminal !== "count") {
|
|
100
64
|
return refined;
|
|
101
65
|
}
|
|
102
66
|
return applyMethod(refined, "count", [], plan.recipe.model);
|
|
103
67
|
};
|
|
104
68
|
|
|
105
|
-
const includeRefinement = (
|
|
106
|
-
value: unknown,
|
|
107
|
-
owner: object,
|
|
108
|
-
transactionIdentity: object | undefined,
|
|
109
|
-
): ((collection: unknown) => unknown) => {
|
|
69
|
+
const includeRefinement = (value: unknown, owner: object, transactionIdentity: object | undefined): ((collection: unknown) => unknown) => {
|
|
110
70
|
const plan = getRelationPlan(value);
|
|
111
71
|
if (plan !== undefined) {
|
|
112
|
-
return (collection) =>
|
|
113
|
-
replayPlan(collection, plan, owner, transactionIdentity);
|
|
72
|
+
return (collection) => replayPlan(collection, plan, owner, transactionIdentity);
|
|
114
73
|
}
|
|
115
74
|
|
|
116
75
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
117
|
-
throw new TypeError(
|
|
118
|
-
"An included relation must be a Relation or query record",
|
|
119
|
-
);
|
|
76
|
+
throw new TypeError("An included relation must be a Relation or query record");
|
|
120
77
|
}
|
|
121
78
|
|
|
122
79
|
const entries = Object.entries(value);
|
|
@@ -137,46 +94,24 @@ const includeRefinement = (
|
|
|
137
94
|
}
|
|
138
95
|
|
|
139
96
|
return (collection) => {
|
|
140
|
-
const branches = Object.fromEntries(
|
|
141
|
-
plans.map(([name, queryPlan]) => [
|
|
142
|
-
name,
|
|
143
|
-
replayPlan(collection, queryPlan, owner, transactionIdentity),
|
|
144
|
-
]),
|
|
145
|
-
);
|
|
97
|
+
const branches = Object.fromEntries(plans.map(([name, queryPlan]) => [name, replayPlan(collection, queryPlan, owner, transactionIdentity)]));
|
|
146
98
|
return applyMethod(collection, "combine", [branches], model ?? "relation");
|
|
147
99
|
};
|
|
148
100
|
};
|
|
149
101
|
|
|
150
|
-
const replayRecipeFrom = (
|
|
151
|
-
root: unknown,
|
|
152
|
-
recipe: RelationRecipe,
|
|
153
|
-
owner: object,
|
|
154
|
-
transactionIdentity: object | undefined,
|
|
155
|
-
): unknown => {
|
|
102
|
+
const replayRecipeFrom = (root: unknown, recipe: RelationRecipe, owner: object, transactionIdentity: object | undefined): unknown => {
|
|
156
103
|
let current = root;
|
|
157
104
|
for (const operation of operations(recipe)) {
|
|
158
105
|
const arguments_ =
|
|
159
106
|
operation.name === "include" && operation.arguments.length === 2
|
|
160
|
-
? [
|
|
161
|
-
operation.arguments[0],
|
|
162
|
-
includeRefinement(
|
|
163
|
-
operation.arguments[1],
|
|
164
|
-
owner,
|
|
165
|
-
transactionIdentity,
|
|
166
|
-
),
|
|
167
|
-
]
|
|
107
|
+
? [operation.arguments[0], includeRefinement(operation.arguments[1], owner, transactionIdentity)]
|
|
168
108
|
: operation.arguments;
|
|
169
109
|
current = applyMethod(current, operation.name, arguments_, recipe.model);
|
|
170
110
|
}
|
|
171
111
|
return current;
|
|
172
112
|
};
|
|
173
113
|
|
|
174
|
-
export const replayRecipe = (
|
|
175
|
-
models: object,
|
|
176
|
-
recipe: RelationRecipe,
|
|
177
|
-
owner: object,
|
|
178
|
-
transactionIdentity: object | undefined,
|
|
179
|
-
): unknown => {
|
|
114
|
+
export const replayRecipe = (models: object, recipe: RelationRecipe, owner: object, transactionIdentity: object | undefined): unknown => {
|
|
180
115
|
const current: unknown = Reflect.get(models, recipe.model);
|
|
181
116
|
|
|
182
117
|
if (current === undefined) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Effect, Option } from "effect";
|
|
2
|
+
import type { PrismaError } from "../error.ts";
|
|
3
|
+
import { hasMethod, invokeMethod, isPromiseLike } from "./dynamic.ts";
|
|
4
|
+
import { fromPrismaPromise } from "./promise.ts";
|
|
5
|
+
|
|
6
|
+
const settle = (value: unknown, map: (result: unknown) => unknown): Effect.Effect<unknown, PrismaError> =>
|
|
7
|
+
isPromiseLike(value) ? fromPrismaPromise(() => value).pipe(Effect.map(map)) : Effect.succeed(map(value));
|
|
8
|
+
|
|
9
|
+
const countOf = (result: unknown): unknown => (typeof result === "object" && result !== null ? Reflect.get(result, "count") : undefined);
|
|
10
|
+
|
|
11
|
+
export const evaluateResult = (value: unknown, terminal: PropertyKey | undefined): Effect.Effect<unknown, PrismaError> => {
|
|
12
|
+
if (terminal === "count" && hasMethod(value, "aggregate")) {
|
|
13
|
+
return settle(invokeMethod(value, "aggregate", [(summary: { count: () => unknown }) => ({ count: summary.count() })]), countOf);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (terminal === "exists" && hasMethod(value, "first")) {
|
|
17
|
+
return settle(invokeMethod(value, "first", []), (result) => result !== null);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const executable = hasMethod(value, "all") ? invokeMethod(value, "all", []) : value;
|
|
21
|
+
|
|
22
|
+
if (isPromiseLike(executable)) {
|
|
23
|
+
return settle(executable, (result) => (terminal === "first" ? Option.fromNullishOr(result) : result));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return Effect.succeed(executable);
|
|
27
|
+
};
|
|
@@ -1,167 +1,72 @@
|
|
|
1
|
-
import { Effect, Effectable,
|
|
2
|
-
import type { PrismaError } from "../error.
|
|
3
|
-
import type { Relation } from "../relation.
|
|
4
|
-
import type { AnyPostgresContract, DatabaseExecutor } from "./executor.
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
replayRecipe,
|
|
11
|
-
rootRecipe,
|
|
12
|
-
} from "./recipe.js";
|
|
13
|
-
import { setRelationPlan } from "./relation-plan.js";
|
|
14
|
-
import { makeRelationStream } from "./relation-stream.js";
|
|
1
|
+
import { Effect, Effectable, type Stream } from "effect";
|
|
2
|
+
import type { PrismaError } from "../error.ts";
|
|
3
|
+
import type { Relation } from "../relation.ts";
|
|
4
|
+
import type { AnyPostgresContract, DatabaseExecutor } from "./executor.ts";
|
|
5
|
+
import { executeQuery } from "./query-execution.ts";
|
|
6
|
+
import { appendOperation, type RelationRecipe, replayRecipe, rootRecipe } from "./recipe.ts";
|
|
7
|
+
import { setRelationPlan } from "./relation-plan.ts";
|
|
8
|
+
import { evaluateResult } from "./relation-result.ts";
|
|
9
|
+
import { makeRelationStream } from "./relation-stream.ts";
|
|
15
10
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
interface RelationRuntime<
|
|
19
|
-
Models extends object,
|
|
20
|
-
Contract extends AnyPostgresContract,
|
|
21
|
-
> {
|
|
11
|
+
interface RelationRuntime<Models extends object, Contract extends AnyPostgresContract> {
|
|
22
12
|
readonly executor: DatabaseExecutor<Models, Contract>;
|
|
23
13
|
readonly recipe: RelationRecipe;
|
|
24
|
-
readonly resolveExecutor: Effect.Effect<
|
|
25
|
-
DatabaseExecutor<Models, Contract>,
|
|
26
|
-
PrismaError
|
|
27
|
-
>;
|
|
14
|
+
readonly resolveExecutor: Effect.Effect<DatabaseExecutor<Models, Contract>, PrismaError>;
|
|
28
15
|
readonly terminal?: PropertyKey;
|
|
29
16
|
}
|
|
30
17
|
|
|
31
|
-
const evaluateResult = (
|
|
32
|
-
value: unknown,
|
|
33
|
-
terminal: PropertyKey | undefined,
|
|
34
|
-
): Effect.Effect<unknown, PrismaError> => {
|
|
35
|
-
if (
|
|
36
|
-
terminal === "count" &&
|
|
37
|
-
typeof value === "object" &&
|
|
38
|
-
value !== null &&
|
|
39
|
-
typeof Reflect.get(value, "aggregate") === "function"
|
|
40
|
-
) {
|
|
41
|
-
const aggregate = Reflect.apply(
|
|
42
|
-
Reflect.get(value, "aggregate") as AnyFunction,
|
|
43
|
-
value,
|
|
44
|
-
[(summary: { count(): unknown }) => ({ count: summary.count() })],
|
|
45
|
-
);
|
|
46
|
-
return fromPrismaPromise(
|
|
47
|
-
() => aggregate as PromiseLike<{ count: number }>,
|
|
48
|
-
).pipe(Effect.map((result) => result.count));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
terminal === "exists" &&
|
|
53
|
-
typeof value === "object" &&
|
|
54
|
-
value !== null &&
|
|
55
|
-
typeof Reflect.get(value, "first") === "function"
|
|
56
|
-
) {
|
|
57
|
-
const first = Reflect.apply(
|
|
58
|
-
Reflect.get(value, "first") as AnyFunction,
|
|
59
|
-
value,
|
|
60
|
-
[],
|
|
61
|
-
);
|
|
62
|
-
return fromPrismaPromise(() => first as PromiseLike<unknown>).pipe(
|
|
63
|
-
Effect.map((result) => result !== null),
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const executable =
|
|
68
|
-
typeof value === "object" &&
|
|
69
|
-
value !== null &&
|
|
70
|
-
typeof Reflect.get(value, "all") === "function"
|
|
71
|
-
? Reflect.apply(Reflect.get(value, "all") as AnyFunction, value, [])
|
|
72
|
-
: value;
|
|
73
|
-
|
|
74
|
-
if (
|
|
75
|
-
typeof executable === "object" &&
|
|
76
|
-
executable !== null &&
|
|
77
|
-
"then" in executable
|
|
78
|
-
) {
|
|
79
|
-
return fromPrismaPromise(() => executable as PromiseLike<unknown>).pipe(
|
|
80
|
-
Effect.map((result) =>
|
|
81
|
-
terminal === "first" ? Option.fromNullishOr(result) : result,
|
|
82
|
-
),
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return Effect.succeed(executable);
|
|
87
|
-
};
|
|
88
|
-
|
|
89
18
|
interface RelationValue extends Effect.Effect<unknown, PrismaError> {
|
|
90
19
|
readonly stream: Stream.Stream<unknown, PrismaError>;
|
|
91
20
|
}
|
|
92
21
|
|
|
93
|
-
|
|
22
|
+
type AnyRelationRuntime = RelationRuntime<object, AnyPostgresContract>;
|
|
23
|
+
|
|
24
|
+
const runtimes = new WeakMap<object, AnyRelationRuntime>();
|
|
94
25
|
|
|
95
|
-
const runtimeOf =
|
|
96
|
-
self: object,
|
|
97
|
-
): RelationRuntime<Models, Contract> => {
|
|
26
|
+
const runtimeOf = (self: object): AnyRelationRuntime => {
|
|
98
27
|
const runtime = runtimes.get(self);
|
|
99
28
|
if (runtime === undefined) {
|
|
100
29
|
throw new TypeError("Relation runtime is unavailable");
|
|
101
30
|
}
|
|
102
|
-
return runtime
|
|
31
|
+
return runtime;
|
|
103
32
|
};
|
|
104
33
|
|
|
105
|
-
const relationEffect = <
|
|
106
|
-
|
|
107
|
-
Contract extends AnyPostgresContract,
|
|
108
|
-
>(
|
|
109
|
-
self: RelationValue,
|
|
110
|
-
): Effect.Effect<unknown, PrismaError> => {
|
|
111
|
-
const runtime = runtimeOf<Models, Contract>(self);
|
|
34
|
+
const relationEffect = (self: object): Effect.Effect<unknown, PrismaError> => {
|
|
35
|
+
const runtime = runtimeOf(self);
|
|
112
36
|
return Effect.flatMap(runtime.resolveExecutor, (executor) =>
|
|
113
37
|
executeQuery(
|
|
114
38
|
executor,
|
|
115
39
|
Effect.suspend(() =>
|
|
116
|
-
evaluateResult(
|
|
117
|
-
replayRecipe(
|
|
118
|
-
executor.models,
|
|
119
|
-
runtime.recipe,
|
|
120
|
-
executor.identity,
|
|
121
|
-
executor.transactionIdentity,
|
|
122
|
-
),
|
|
123
|
-
runtime.terminal,
|
|
124
|
-
),
|
|
40
|
+
evaluateResult(replayRecipe(executor.models, runtime.recipe, executor.identity, executor.transactionIdentity), runtime.terminal),
|
|
125
41
|
),
|
|
126
42
|
),
|
|
127
43
|
).pipe(
|
|
128
|
-
Effect.withSpan(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
"db.system": "postgresql",
|
|
134
|
-
"db.model": runtime.recipe.model,
|
|
135
|
-
"db.operation": String(runtime.terminal ?? "all"),
|
|
136
|
-
},
|
|
44
|
+
Effect.withSpan(`prisma.${runtime.recipe.model}.${String(runtime.terminal ?? "all")}`, {
|
|
45
|
+
attributes: {
|
|
46
|
+
"db.model": runtime.recipe.model,
|
|
47
|
+
"db.operation": String(runtime.terminal ?? "all"),
|
|
48
|
+
"db.system": "postgresql",
|
|
137
49
|
},
|
|
138
|
-
|
|
50
|
+
kind: "client",
|
|
51
|
+
}),
|
|
139
52
|
);
|
|
140
53
|
};
|
|
141
54
|
|
|
142
55
|
const RelationPrototype = {
|
|
143
56
|
...Effectable.Prototype<RelationValue>({
|
|
144
|
-
label: "EffectPrismaRelation",
|
|
145
57
|
evaluate() {
|
|
146
58
|
return relationEffect(this);
|
|
147
59
|
},
|
|
60
|
+
label: "EffectPrismaRelation",
|
|
148
61
|
}),
|
|
149
62
|
get stream(): Stream.Stream<unknown, PrismaError> {
|
|
150
|
-
const
|
|
151
|
-
const runtime = runtimeOf<Record<string, unknown>, AnyPostgresContract>(
|
|
152
|
-
relation,
|
|
153
|
-
);
|
|
63
|
+
const runtime = runtimeOf(this);
|
|
154
64
|
return makeRelationStream(runtime.resolveExecutor, runtime.recipe);
|
|
155
65
|
},
|
|
156
66
|
};
|
|
157
67
|
|
|
158
|
-
const makeRelationProxy =
|
|
159
|
-
|
|
160
|
-
Contract extends AnyPostgresContract,
|
|
161
|
-
>(
|
|
162
|
-
runtime: RelationRuntime<Models, Contract>,
|
|
163
|
-
): unknown => {
|
|
164
|
-
const target = Object.create(RelationPrototype) as RelationValue;
|
|
68
|
+
const makeRelationProxy = (runtime: AnyRelationRuntime): object => {
|
|
69
|
+
const target: object = Object.create(RelationPrototype);
|
|
165
70
|
const proxy = new Proxy(target, {
|
|
166
71
|
get(self, property, receiver) {
|
|
167
72
|
if (property === "then") {
|
|
@@ -184,7 +89,7 @@ const makeRelationProxy = <
|
|
|
184
89
|
terminal: "count",
|
|
185
90
|
});
|
|
186
91
|
}
|
|
187
|
-
return (...arguments_:
|
|
92
|
+
return (...arguments_: readonly unknown[]) =>
|
|
188
93
|
makeRelationProxy({
|
|
189
94
|
...runtime,
|
|
190
95
|
recipe: appendOperation(runtime.recipe, property, arguments_),
|
|
@@ -206,6 +111,12 @@ const makeRelationProxy = <
|
|
|
206
111
|
return proxy;
|
|
207
112
|
};
|
|
208
113
|
|
|
114
|
+
// The proxy replays every Relation call onto the Prisma collection for `model`, so it has that collection's Relation surface.
|
|
115
|
+
function asRelation<Collection, Model extends string>(proxy: object): Relation<Collection, undefined, Model>;
|
|
116
|
+
function asRelation(proxy: object): unknown {
|
|
117
|
+
return proxy;
|
|
118
|
+
}
|
|
119
|
+
|
|
209
120
|
export const makeModelRelation = <
|
|
210
121
|
Collection,
|
|
211
122
|
Models extends object,
|
|
@@ -214,13 +125,12 @@ export const makeModelRelation = <
|
|
|
214
125
|
>(
|
|
215
126
|
executor: DatabaseExecutor<Models, ExecutorContract>,
|
|
216
127
|
model: Model,
|
|
217
|
-
resolveExecutor: Effect.Effect<
|
|
218
|
-
DatabaseExecutor<Models, ExecutorContract>,
|
|
219
|
-
PrismaError
|
|
220
|
-
> = Effect.succeed(executor),
|
|
128
|
+
resolveExecutor: Effect.Effect<DatabaseExecutor<Models, ExecutorContract>, PrismaError> = Effect.succeed(executor),
|
|
221
129
|
): Relation<Collection, undefined, Model> =>
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
130
|
+
asRelation<Collection, Model>(
|
|
131
|
+
makeRelationProxy({
|
|
132
|
+
executor,
|
|
133
|
+
recipe: rootRecipe(model),
|
|
134
|
+
resolveExecutor,
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
@@ -1,72 +1,45 @@
|
|
|
1
1
|
import { Effect, Stream } from "effect";
|
|
2
|
-
import { isPrismaFailure, type PrismaError, toPrismaError } from "../error.
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
2
|
+
import { isPrismaFailure, type PrismaError, toPrismaError } from "../error.ts";
|
|
3
|
+
import { hasMethod, invokeMethod, isAsyncIterable } from "./dynamic.ts";
|
|
4
|
+
import type { AnyPostgresContract, DatabaseExecutor } from "./executor.ts";
|
|
5
|
+
import { fromPrismaPromise } from "./promise.ts";
|
|
6
|
+
import { executeQuery } from "./query-execution.ts";
|
|
7
|
+
import { type RelationRecipe, replayRecipe } from "./recipe.ts";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
const collectRows = (iterable: AsyncIterable<unknown>): Effect.Effect<unknown[], PrismaError> =>
|
|
10
|
+
fromPrismaPromise(async () => {
|
|
11
|
+
const rows: unknown[] = [];
|
|
12
|
+
for await (const row of iterable) {
|
|
13
|
+
rows.push(row);
|
|
14
|
+
}
|
|
15
|
+
return rows;
|
|
16
|
+
});
|
|
9
17
|
|
|
10
|
-
export const makeRelationStream = <
|
|
11
|
-
Models
|
|
12
|
-
Contract extends AnyPostgresContract,
|
|
13
|
-
>(
|
|
14
|
-
resolveExecutor: Effect.Effect<
|
|
15
|
-
DatabaseExecutor<Models, Contract>,
|
|
16
|
-
PrismaError
|
|
17
|
-
>,
|
|
18
|
+
export const makeRelationStream = <Models extends object, Contract extends AnyPostgresContract>(
|
|
19
|
+
resolveExecutor: Effect.Effect<DatabaseExecutor<Models, Contract>, PrismaError>,
|
|
18
20
|
recipe: RelationRecipe,
|
|
19
|
-
): Stream.Stream<unknown, PrismaError> =>
|
|
20
|
-
|
|
21
|
+
): Stream.Stream<unknown, PrismaError> =>
|
|
22
|
+
Stream.unwrap(
|
|
21
23
|
Effect.flatMap(resolveExecutor, (executor) =>
|
|
22
24
|
executeQuery(
|
|
23
25
|
executor,
|
|
24
26
|
Effect.sync(() => {
|
|
25
|
-
const collection = replayRecipe(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
executor.identity,
|
|
29
|
-
executor.transactionIdentity,
|
|
30
|
-
);
|
|
31
|
-
if (
|
|
32
|
-
typeof collection !== "object" ||
|
|
33
|
-
collection === null ||
|
|
34
|
-
typeof Reflect.get(collection, "all") !== "function"
|
|
35
|
-
) {
|
|
27
|
+
const collection = replayRecipe(executor.models, recipe, executor.identity, executor.transactionIdentity);
|
|
28
|
+
const iterable = hasMethod(collection, "all") ? invokeMethod(collection, "all", []) : undefined;
|
|
29
|
+
if (!isAsyncIterable(iterable)) {
|
|
36
30
|
throw new TypeError("Only collection Relations can be streamed");
|
|
37
31
|
}
|
|
38
|
-
const iterable = Reflect.apply(
|
|
39
|
-
Reflect.get(collection, "all") as AnyFunction,
|
|
40
|
-
collection,
|
|
41
|
-
[],
|
|
42
|
-
) as AsyncIterable<unknown>;
|
|
43
32
|
|
|
44
33
|
if (executor.mode === "root") {
|
|
45
34
|
return Stream.fromAsyncIterable(iterable, (error) => error).pipe(
|
|
46
|
-
Stream.catch((error) =>
|
|
47
|
-
isPrismaFailure(error)
|
|
48
|
-
? Stream.fail(toPrismaError(error))
|
|
49
|
-
: Stream.die(error),
|
|
50
|
-
),
|
|
35
|
+
Stream.catch((error) => (isPrismaFailure(error) ? Stream.fail(toPrismaError(error)) : Stream.die(error))),
|
|
51
36
|
);
|
|
52
37
|
}
|
|
53
38
|
|
|
54
|
-
const buffered = executeQuery(
|
|
55
|
-
executor,
|
|
56
|
-
fromPrismaPromise(async () => {
|
|
57
|
-
const rows: Array<unknown> = [];
|
|
58
|
-
for await (const row of iterable) {
|
|
59
|
-
rows.push(row);
|
|
60
|
-
}
|
|
61
|
-
return rows;
|
|
62
|
-
}),
|
|
63
|
-
);
|
|
39
|
+
const buffered = executeQuery(executor, collectRows(iterable));
|
|
64
40
|
|
|
65
|
-
return Stream.unwrap(
|
|
66
|
-
Effect.map(buffered, (rows) => Stream.fromIterable(rows)),
|
|
67
|
-
);
|
|
41
|
+
return Stream.unwrap(Effect.map(buffered, (rows) => Stream.fromIterable(rows)));
|
|
68
42
|
}),
|
|
69
43
|
),
|
|
70
44
|
),
|
|
71
45
|
);
|
|
72
|
-
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExecutionContext } from "@prisma-next/sql-runtime";
|
|
2
|
-
import type { AnySqlContract } from "./executor.
|
|
2
|
+
import type { AnySqlContract } from "./executor.ts";
|
|
3
3
|
|
|
4
4
|
type CodecRegistry = ExecutionContext<AnySqlContract>["contractCodecs"];
|
|
5
5
|
type ContractCodec = ReturnType<CodecRegistry["forCodecRef"]>;
|
|
@@ -8,56 +8,24 @@ type CodecJson = Parameters<ContractCodec["decodeJson"]>[0];
|
|
|
8
8
|
|
|
9
9
|
const sqliteDatetimeCodecId = "sqlite/datetime@1";
|
|
10
10
|
|
|
11
|
-
const zonelessDatetime =
|
|
12
|
-
/^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?)$/;
|
|
11
|
+
const zonelessDatetime = /^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?)$/u;
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* SQLite stores `DateTime` as text and computes `datetime('now')` — the default
|
|
18
|
-
* `prisma-next db init` generates — in UTC, but writes it as
|
|
19
|
-
* `YYYY-MM-DD HH:MM:SS` with no zone. `new Date` reads that form as local time,
|
|
20
|
-
* so decoding it unchanged shifts the instant by the process' UTC offset.
|
|
21
|
-
* Values that already carry `Z` or a numeric offset, date-only values (which
|
|
22
|
-
* `new Date` already reads as UTC), and anything that is not a datetime string
|
|
23
|
-
* are returned unchanged.
|
|
24
|
-
*/
|
|
25
|
-
export const normalizeSqliteDatetime = (value: string): string =>
|
|
26
|
-
value.replace(zonelessDatetime, "$1T$2Z");
|
|
13
|
+
// SQLite computes `datetime('now')`, the default `prisma-next db init` generates, in UTC but writes it as `YYYY-MM-DD HH:MM:SS`
|
|
14
|
+
// with no zone, and `new Date` reads that form as local time. Values with a zone and date-only values already read as UTC.
|
|
15
|
+
export const normalizeSqliteDatetime = (value: string): string => value.replace(zonelessDatetime, "$1T$2Z");
|
|
27
16
|
|
|
28
17
|
const decodeAsUtc = (codec: ContractCodec): ContractCodec => ({
|
|
29
|
-
|
|
18
|
+
decode: (wire: CodecWire, context) => codec.decode(typeof wire === "string" ? normalizeSqliteDatetime(wire) : wire, context),
|
|
19
|
+
decodeJson: (json: CodecJson) => codec.decodeJson(typeof json === "string" ? normalizeSqliteDatetime(json) : json),
|
|
30
20
|
encode: (value, context) => codec.encode(value, context),
|
|
31
|
-
decode: (wire: CodecWire, context) =>
|
|
32
|
-
codec.decode(
|
|
33
|
-
typeof wire === "string" ? normalizeSqliteDatetime(wire) : wire,
|
|
34
|
-
context,
|
|
35
|
-
),
|
|
36
21
|
encodeJson: (value) => codec.encodeJson(value),
|
|
37
|
-
|
|
38
|
-
codec.decodeJson(
|
|
39
|
-
typeof json === "string" ? normalizeSqliteDatetime(json) : json,
|
|
40
|
-
),
|
|
22
|
+
id: sqliteDatetimeCodecId,
|
|
41
23
|
});
|
|
42
24
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
* The registry is the single resolution point for both row decoding and the
|
|
48
|
-
* JSON decoding of included relations, so wrapping it covers every read path
|
|
49
|
-
* without touching encoding. Prisma Next rejects a second descriptor for an
|
|
50
|
-
* already-registered codec id, so an extension pack cannot replace the codec
|
|
51
|
-
* itself. Must run before the first query, because the runtime reads the
|
|
52
|
-
* registry off the context when it is created.
|
|
53
|
-
*
|
|
54
|
-
* Codecs are identified by the resolved reference rather than by `codec.id`:
|
|
55
|
-
* Prisma Next materializes a codec through an unbound descriptor factory, so
|
|
56
|
-
* the instance's `id` accessor throws.
|
|
57
|
-
*/
|
|
58
|
-
export const decodeSqliteDatetimesAsUtc = (
|
|
59
|
-
context: ExecutionContext<AnySqlContract>,
|
|
60
|
-
): void => {
|
|
25
|
+
// The registry resolves codecs for rows and included relations alike, and Prisma Next rejects a second descriptor for a registered
|
|
26
|
+
// codec id, so wrapping the registry is the one way to cover every read. It must run before the first query, which reads the
|
|
27
|
+
// registry off the context. Codecs are matched by reference because a materialized codec's `id` accessor throws.
|
|
28
|
+
export const decodeSqliteDatetimesAsUtc = (context: ExecutionContext<AnySqlContract>): void => {
|
|
61
29
|
const registry = context.contractCodecs;
|
|
62
30
|
const descriptors = context.codecDescriptors;
|
|
63
31
|
const utcCodecs = new WeakMap<ContractCodec, ContractCodec>();
|
|
@@ -73,23 +41,21 @@ export const decodeSqliteDatetimesAsUtc = (
|
|
|
73
41
|
};
|
|
74
42
|
|
|
75
43
|
const utcRegistry: CodecRegistry = {
|
|
44
|
+
forCodecRef: (reference) => {
|
|
45
|
+
const codec = registry.forCodecRef(reference);
|
|
46
|
+
return reference.codecId === sqliteDatetimeCodecId ? wrap(codec) : codec;
|
|
47
|
+
},
|
|
76
48
|
forColumn: (namespaceId, table, column) => {
|
|
77
49
|
const codec = registry.forColumn(namespaceId, table, column);
|
|
78
50
|
if (codec === undefined) {
|
|
79
51
|
return undefined;
|
|
80
52
|
}
|
|
81
|
-
const reference = descriptors.codecRefForColumn(
|
|
82
|
-
namespaceId,
|
|
83
|
-
table,
|
|
84
|
-
column,
|
|
85
|
-
);
|
|
53
|
+
const reference = descriptors.codecRefForColumn(namespaceId, table, column);
|
|
86
54
|
return reference?.codecId === sqliteDatetimeCodecId ? wrap(codec) : codec;
|
|
87
55
|
},
|
|
88
|
-
forCodecRef: (reference) => {
|
|
89
|
-
const codec = registry.forCodecRef(reference);
|
|
90
|
-
return reference.codecId === sqliteDatetimeCodecId ? wrap(codec) : codec;
|
|
91
|
-
},
|
|
92
56
|
};
|
|
93
57
|
|
|
94
|
-
(context
|
|
58
|
+
if (!Reflect.set(context, "contractCodecs", utcRegistry)) {
|
|
59
|
+
throw new TypeError("The SQLite execution context does not accept a codec registry");
|
|
60
|
+
}
|
|
95
61
|
};
|