@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.
Files changed (138) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/dist/bin/normalize-contract.js +1 -0
  3. package/dist/bin/normalize-contract.js.map +1 -1
  4. package/dist/database.d.ts +4 -4
  5. package/dist/database.d.ts.map +1 -1
  6. package/dist/database.js +12 -12
  7. package/dist/database.js.map +1 -1
  8. package/dist/error.d.ts +5 -5
  9. package/dist/error.d.ts.map +1 -1
  10. package/dist/error.js +5 -6
  11. package/dist/error.js.map +1 -1
  12. package/dist/index.d.ts +3 -3
  13. package/dist/internal/adapters/transaction-settlement.d.ts +10 -0
  14. package/dist/internal/adapters/transaction-settlement.d.ts.map +1 -0
  15. package/dist/internal/adapters/transaction-settlement.js +56 -0
  16. package/dist/internal/adapters/transaction-settlement.js.map +1 -0
  17. package/dist/internal/client-lifecycle.d.ts +2 -2
  18. package/dist/internal/client-lifecycle.d.ts.map +1 -1
  19. package/dist/internal/client-lifecycle.js.map +1 -1
  20. package/dist/internal/contract-normalization.d.ts +0 -4
  21. package/dist/internal/contract-normalization.d.ts.map +1 -1
  22. package/dist/internal/contract-normalization.js +5 -10
  23. package/dist/internal/contract-normalization.js.map +1 -1
  24. package/dist/internal/database-facade.d.ts +19 -0
  25. package/dist/internal/database-facade.d.ts.map +1 -0
  26. package/dist/internal/database-facade.js +79 -0
  27. package/dist/internal/database-facade.js.map +1 -0
  28. package/dist/internal/database-factory.d.ts +4 -48
  29. package/dist/internal/database-factory.d.ts.map +1 -1
  30. package/dist/internal/database-factory.js +11 -90
  31. package/dist/internal/database-factory.js.map +1 -1
  32. package/dist/internal/database-types.d.ts +33 -0
  33. package/dist/internal/database-types.d.ts.map +1 -0
  34. package/dist/internal/database-types.js +2 -0
  35. package/dist/internal/database-types.js.map +1 -0
  36. package/dist/internal/dynamic.d.ts +5 -0
  37. package/dist/internal/dynamic.d.ts.map +1 -0
  38. package/dist/internal/dynamic.js +12 -0
  39. package/dist/internal/dynamic.js.map +1 -0
  40. package/dist/internal/executor.d.ts +3 -7
  41. package/dist/internal/executor.d.ts.map +1 -1
  42. package/dist/internal/promise.d.ts +1 -1
  43. package/dist/internal/promise.d.ts.map +1 -1
  44. package/dist/internal/promise.js +2 -4
  45. package/dist/internal/promise.js.map +1 -1
  46. package/dist/internal/query-execution.d.ts +2 -2
  47. package/dist/internal/query-execution.d.ts.map +1 -1
  48. package/dist/internal/query-execution.js +2 -6
  49. package/dist/internal/query-execution.js.map +1 -1
  50. package/dist/internal/recipe.d.ts +3 -3
  51. package/dist/internal/recipe.d.ts.map +1 -1
  52. package/dist/internal/recipe.js +9 -20
  53. package/dist/internal/recipe.js.map +1 -1
  54. package/dist/internal/relation-plan.d.ts +1 -1
  55. package/dist/internal/relation-result.d.ts +4 -0
  56. package/dist/internal/relation-result.d.ts.map +1 -0
  57. package/dist/internal/relation-result.js +19 -0
  58. package/dist/internal/relation-result.js.map +1 -0
  59. package/dist/internal/relation-runtime.d.ts +3 -3
  60. package/dist/internal/relation-runtime.d.ts.map +1 -1
  61. package/dist/internal/relation-runtime.js +12 -37
  62. package/dist/internal/relation-runtime.js.map +1 -1
  63. package/dist/internal/relation-stream.d.ts +3 -3
  64. package/dist/internal/relation-stream.d.ts.map +1 -1
  65. package/dist/internal/relation-stream.js +20 -24
  66. package/dist/internal/relation-stream.js.map +1 -1
  67. package/dist/internal/sqlite-datetime.d.ts +1 -27
  68. package/dist/internal/sqlite-datetime.d.ts.map +1 -1
  69. package/dist/internal/sqlite-datetime.js +16 -35
  70. package/dist/internal/sqlite-datetime.js.map +1 -1
  71. package/dist/internal/sqlite-pragmas.d.ts +1 -13
  72. package/dist/internal/sqlite-pragmas.d.ts.map +1 -1
  73. package/dist/internal/sqlite-pragmas.js +2 -12
  74. package/dist/internal/sqlite-pragmas.js.map +1 -1
  75. package/dist/internal/testing.d.ts +1 -1
  76. package/dist/internal/testing.d.ts.map +1 -1
  77. package/dist/internal/testing.js +5 -2
  78. package/dist/internal/testing.js.map +1 -1
  79. package/dist/internal/transaction.d.ts +5 -5
  80. package/dist/internal/transaction.d.ts.map +1 -1
  81. package/dist/internal/transaction.js +5 -53
  82. package/dist/internal/transaction.js.map +1 -1
  83. package/dist/relation/include-metadata.d.ts +1 -1
  84. package/dist/relation/include-metadata.d.ts.map +1 -1
  85. package/dist/relation/include.d.ts +8 -5
  86. package/dist/relation/include.d.ts.map +1 -1
  87. package/dist/relation/prisma-methods.d.ts +17 -17
  88. package/dist/relation/prisma-methods.d.ts.map +1 -1
  89. package/dist/relation.d.ts +4 -4
  90. package/dist/relation.d.ts.map +1 -1
  91. package/dist/sqlite.d.ts +5 -10
  92. package/dist/sqlite.d.ts.map +1 -1
  93. package/dist/sqlite.js +14 -12
  94. package/dist/sqlite.js.map +1 -1
  95. package/dist/testing/transaction.d.ts +6 -7
  96. package/dist/testing/transaction.d.ts.map +1 -1
  97. package/dist/testing/transaction.js +2 -2
  98. package/dist/testing/transaction.js.map +1 -1
  99. package/dist/testing/types.d.ts +7 -7
  100. package/dist/testing/types.d.ts.map +1 -1
  101. package/dist/testing/vitest.d.ts +3 -3
  102. package/dist/testing/vitest.d.ts.map +1 -1
  103. package/dist/testing/vitest.js +2 -2
  104. package/dist/testing/vitest.js.map +1 -1
  105. package/dist/testing.d.ts +3 -3
  106. package/package.json +35 -37
  107. package/src/bin/normalize-contract.ts +2 -1
  108. package/src/database.ts +49 -67
  109. package/src/error.ts +13 -23
  110. package/src/index.ts +3 -3
  111. package/src/internal/adapters/transaction-settlement.ts +70 -0
  112. package/src/internal/client-lifecycle.ts +4 -11
  113. package/src/internal/contract-normalization.ts +6 -14
  114. package/src/internal/database-facade.ts +138 -0
  115. package/src/internal/database-factory.ts +34 -306
  116. package/src/internal/database-types.ts +65 -0
  117. package/src/internal/dynamic.ts +16 -0
  118. package/src/internal/executor.ts +4 -11
  119. package/src/internal/promise.ts +4 -12
  120. package/src/internal/query-execution.ts +4 -8
  121. package/src/internal/recipe.ts +24 -89
  122. package/src/internal/relation-plan.ts +1 -1
  123. package/src/internal/relation-result.ts +27 -0
  124. package/src/internal/relation-runtime.ts +45 -135
  125. package/src/internal/relation-stream.ts +24 -51
  126. package/src/internal/sqlite-datetime.ts +20 -54
  127. package/src/internal/sqlite-pragmas.ts +3 -16
  128. package/src/internal/testing.ts +13 -15
  129. package/src/internal/transaction.ts +16 -109
  130. package/src/relation/include-metadata.ts +11 -43
  131. package/src/relation/include.ts +46 -140
  132. package/src/relation/prisma-methods.ts +36 -122
  133. package/src/relation.ts +20 -48
  134. package/src/sqlite.ts +59 -85
  135. package/src/testing/transaction.ts +20 -33
  136. package/src/testing/types.ts +15 -34
  137. package/src/testing/vitest.ts +14 -28
  138. package/src/testing.ts +3 -3
@@ -1,35 +1,30 @@
1
- import { getRelationPlan, type RelationPlan } from "./relation-plan.js";
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
- recipe: RelationRecipe,
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: 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
- ) {
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) {
@@ -1,4 +1,4 @@
1
- import type { RelationRecipe } from "./recipe.js";
1
+ import type { RelationRecipe } from "./recipe.ts";
2
2
 
3
3
  export interface RelationPlan {
4
4
  readonly liveness: { readonly open: boolean };
@@ -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, Option, type Stream } from "effect";
2
- import type { PrismaError } from "../error.js";
3
- import type { Relation } from "../relation.js";
4
- import type { AnyPostgresContract, DatabaseExecutor } from "./executor.js";
5
- import { fromPrismaPromise } from "./promise.js";
6
- import { executeQuery } from "./query-execution.js";
7
- import {
8
- appendOperation,
9
- type RelationRecipe,
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
- type AnyFunction = (...arguments_: ReadonlyArray<never>) => unknown;
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
- const runtimes = new WeakMap<object, unknown>();
22
+ type AnyRelationRuntime = RelationRuntime<object, AnyPostgresContract>;
23
+
24
+ const runtimes = new WeakMap<object, AnyRelationRuntime>();
94
25
 
95
- const runtimeOf = <Models extends object, Contract extends AnyPostgresContract>(
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 as RelationRuntime<Models, Contract>;
31
+ return runtime;
103
32
  };
104
33
 
105
- const relationEffect = <
106
- Models extends object,
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
- `prisma.${runtime.recipe.model}.${String(runtime.terminal ?? "all")}`,
130
- {
131
- kind: "client",
132
- attributes: {
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 relation = this as RelationValue;
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
- Models extends object,
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_: ReadonlyArray<unknown>) =>
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
- makeRelationProxy({
223
- executor,
224
- recipe: rootRecipe(model),
225
- resolveExecutor,
226
- }) as Relation<Collection, undefined, Model>;
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.js";
3
- import type { AnyPostgresContract, DatabaseExecutor } from "./executor.js";
4
- import { fromPrismaPromise } from "./promise.js";
5
- import { executeQuery } from "./query-execution.js";
6
- import { type RelationRecipe, replayRecipe } from "./recipe.js";
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
- type AnyFunction = (...arguments_: ReadonlyArray<never>) => unknown;
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 extends object,
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
- return Stream.unwrap(
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
- executor.models,
27
- recipe,
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.js";
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
- * Stamp `Z` onto a SQLite datetime string that carries no zone designator.
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
- id: sqliteDatetimeCodecId,
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
- decodeJson: (json: CodecJson) =>
38
- codec.decodeJson(
39
- typeof json === "string" ? normalizeSqliteDatetime(json) : json,
40
- ),
22
+ id: sqliteDatetimeCodecId,
41
23
  });
42
24
 
43
- /**
44
- * Replace the client's codec registry with one that decodes zone-less
45
- * `sqlite/datetime@1` values as UTC.
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 as { contractCodecs: CodecRegistry }).contractCodecs = utcRegistry;
58
+ if (!Reflect.set(context, "contractCodecs", utcRegistry)) {
59
+ throw new TypeError("The SQLite execution context does not accept a codec registry");
60
+ }
95
61
  };