@prisma-next/sql-runtime 0.5.0-dev.1 → 0.5.0-dev.2
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/dist/{exports-TJ70Qw3r.mjs → exports-BQZSVXXt.mjs} +32 -4
- package/dist/exports-BQZSVXXt.mjs.map +1 -0
- package/dist/{index-DyDQ4fyK.d.mts → index-yb51L_1h.d.mts} +45 -18
- package/dist/index-yb51L_1h.d.mts.map +1 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/test/utils.d.mts +1 -1
- package/dist/test/utils.mjs +1 -1
- package/package.json +11 -11
- package/src/middleware/before-compile-chain.ts +28 -0
- package/src/middleware/budgets.ts +16 -27
- package/src/middleware/lints.ts +3 -3
- package/src/middleware/sql-middleware.ts +31 -2
- package/src/sql-runtime.ts +24 -8
- package/test/before-compile-chain.test.ts +223 -0
- package/test/budgets.test.ts +6 -6
- package/test/lints.test.ts +5 -4
- package/test/sql-runtime.test.ts +161 -3
- package/dist/exports-TJ70Qw3r.mjs.map +0 -1
- package/dist/index-DyDQ4fyK.d.mts.map +0 -1
package/src/middleware/lints.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ExecutionPlan } from '@prisma-next/contract/types';
|
|
2
2
|
import { runtimeError } from '@prisma-next/framework-components/runtime';
|
|
3
|
-
import type { Middleware, MiddlewareContext } from '@prisma-next/runtime-executor';
|
|
4
3
|
import { evaluateRawGuardrails } from '@prisma-next/runtime-executor';
|
|
5
4
|
import {
|
|
6
5
|
type AnyFromSource,
|
|
@@ -8,6 +7,7 @@ import {
|
|
|
8
7
|
isQueryAst,
|
|
9
8
|
} from '@prisma-next/sql-relational-core/ast';
|
|
10
9
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
10
|
+
import type { SqlMiddleware, SqlMiddlewareContext } from './sql-middleware';
|
|
11
11
|
|
|
12
12
|
export interface LintsOptions {
|
|
13
13
|
readonly severities?: {
|
|
@@ -138,14 +138,14 @@ function getConfiguredSeverity(code: string, options?: LintsOptions): 'warn' | '
|
|
|
138
138
|
* Fallback: When ast is missing, `fallbackWhenAstMissing: 'raw'` uses heuristic
|
|
139
139
|
* SQL parsing; `'skip'` skips all lints. Default is `'raw'`.
|
|
140
140
|
*/
|
|
141
|
-
export function lints
|
|
141
|
+
export function lints(options?: LintsOptions): SqlMiddleware {
|
|
142
142
|
const fallback = options?.fallbackWhenAstMissing ?? 'raw';
|
|
143
143
|
|
|
144
144
|
return Object.freeze({
|
|
145
145
|
name: 'lints',
|
|
146
146
|
familyId: 'sql' as const,
|
|
147
147
|
|
|
148
|
-
async beforeExecute(plan: ExecutionPlan, ctx:
|
|
148
|
+
async beforeExecute(plan: ExecutionPlan, ctx: SqlMiddlewareContext) {
|
|
149
149
|
if (isQueryAst(plan.ast)) {
|
|
150
150
|
const findings = evaluateAstLints(plan.ast);
|
|
151
151
|
|
|
@@ -1,17 +1,46 @@
|
|
|
1
|
-
import type { Contract, ExecutionPlan } from '@prisma-next/contract/types';
|
|
1
|
+
import type { Contract, ExecutionPlan, PlanMeta } from '@prisma-next/contract/types';
|
|
2
2
|
import type {
|
|
3
3
|
AfterExecuteResult,
|
|
4
4
|
RuntimeMiddleware,
|
|
5
5
|
RuntimeMiddlewareContext,
|
|
6
6
|
} from '@prisma-next/framework-components/runtime';
|
|
7
7
|
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
8
|
+
import type { AnyQueryAst } from '@prisma-next/sql-relational-core/ast';
|
|
8
9
|
|
|
9
10
|
export interface SqlMiddlewareContext extends RuntimeMiddlewareContext {
|
|
10
11
|
readonly contract: Contract<SqlStorage>;
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Pre-lowering query view passed to `beforeCompile`. Carries the typed SQL
|
|
16
|
+
* AST and plan metadata; `sql`/`params` are produced later by the adapter.
|
|
17
|
+
*/
|
|
18
|
+
export interface DraftPlan {
|
|
19
|
+
readonly ast: AnyQueryAst;
|
|
20
|
+
readonly meta: PlanMeta;
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
export interface SqlMiddleware extends RuntimeMiddleware {
|
|
14
|
-
readonly familyId
|
|
24
|
+
readonly familyId?: 'sql';
|
|
25
|
+
/**
|
|
26
|
+
* Rewrite the query AST before it is lowered to SQL. Middlewares run in
|
|
27
|
+
* registration order; each sees the predecessor's output, so rewrites
|
|
28
|
+
* compose (e.g. soft-delete + tenant isolation).
|
|
29
|
+
*
|
|
30
|
+
* Return `undefined` (or a draft whose `ast` reference equals the input's)
|
|
31
|
+
* to pass through. Return a draft with a new `ast` reference to replace it;
|
|
32
|
+
* the runtime emits a `middleware.rewrite` debug log event and continues
|
|
33
|
+
* with the new draft. `adapter.lower()` runs once after the chain.
|
|
34
|
+
*
|
|
35
|
+
* Use `AstRewriter` / `SelectAst.withWhere` / `AndExpr.of` etc. to build
|
|
36
|
+
* the rewritten AST. Predicates and literals go through parameterized
|
|
37
|
+
* constructors by default — no SQL-injection surface is added. **Warning:**
|
|
38
|
+
* constructing `LiteralExpr.of(userInput)` from untrusted input bypasses
|
|
39
|
+
* that guarantee; use `ParamRef.of(userInput, ...)` instead.
|
|
40
|
+
*
|
|
41
|
+
* See `docs/architecture docs/subsystems/4. Runtime & Middleware Framework.md`.
|
|
42
|
+
*/
|
|
43
|
+
beforeCompile?(draft: DraftPlan, ctx: SqlMiddlewareContext): Promise<DraftPlan | undefined>;
|
|
15
44
|
beforeExecute?(plan: ExecutionPlan, ctx: SqlMiddlewareContext): Promise<void>;
|
|
16
45
|
onRow?(
|
|
17
46
|
row: Record<string, unknown>,
|
package/src/sql-runtime.ts
CHANGED
|
@@ -6,7 +6,6 @@ import type {
|
|
|
6
6
|
import { checkMiddlewareCompatibility } from '@prisma-next/framework-components/runtime';
|
|
7
7
|
import type {
|
|
8
8
|
Log,
|
|
9
|
-
Middleware,
|
|
10
9
|
RuntimeCore,
|
|
11
10
|
RuntimeCoreOptions,
|
|
12
11
|
RuntimeTelemetryEvent,
|
|
@@ -33,6 +32,8 @@ import { decodeRow } from './codecs/decoding';
|
|
|
33
32
|
import { encodeParams } from './codecs/encoding';
|
|
34
33
|
import { validateCodecRegistryCompleteness } from './codecs/validation';
|
|
35
34
|
import { lowerSqlPlan } from './lower-sql-plan';
|
|
35
|
+
import { runBeforeCompileChain } from './middleware/before-compile-chain';
|
|
36
|
+
import type { SqlMiddleware } from './middleware/sql-middleware';
|
|
36
37
|
import type {
|
|
37
38
|
ExecutionContext,
|
|
38
39
|
SqlRuntimeAdapterInstance,
|
|
@@ -45,7 +46,7 @@ export interface RuntimeOptions<TContract extends Contract<SqlStorage> = Contrac
|
|
|
45
46
|
readonly adapter: Adapter<AnyQueryAst, Contract<SqlStorage>, LoweredStatement>;
|
|
46
47
|
readonly driver: SqlDriver<unknown>;
|
|
47
48
|
readonly verify: RuntimeVerifyOptions;
|
|
48
|
-
readonly middleware?: readonly
|
|
49
|
+
readonly middleware?: readonly SqlMiddleware[];
|
|
49
50
|
readonly mode?: 'strict' | 'permissive';
|
|
50
51
|
readonly log?: Log;
|
|
51
52
|
}
|
|
@@ -64,7 +65,7 @@ export interface CreateRuntimeOptions<
|
|
|
64
65
|
readonly context: ExecutionContext<TContract>;
|
|
65
66
|
readonly driver: SqlDriver<unknown>;
|
|
66
67
|
readonly verify: RuntimeVerifyOptions;
|
|
67
|
-
readonly middleware?: readonly
|
|
68
|
+
readonly middleware?: readonly SqlMiddleware[];
|
|
68
69
|
readonly mode?: 'strict' | 'permissive';
|
|
69
70
|
readonly log?: Log;
|
|
70
71
|
}
|
|
@@ -125,7 +126,7 @@ export type { RuntimeTelemetryEvent, RuntimeVerifyOptions, TelemetryOutcome };
|
|
|
125
126
|
class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorage>>
|
|
126
127
|
implements Runtime
|
|
127
128
|
{
|
|
128
|
-
private readonly core: RuntimeCore<TContract, SqlDriver<unknown
|
|
129
|
+
private readonly core: RuntimeCore<TContract, SqlDriver<unknown>, SqlMiddleware>;
|
|
129
130
|
private readonly contract: TContract;
|
|
130
131
|
private readonly adapter: Adapter<AnyQueryAst, Contract<SqlStorage>, LoweredStatement>;
|
|
131
132
|
private readonly codecRegistry: CodecRegistry;
|
|
@@ -148,7 +149,7 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
|
|
|
148
149
|
|
|
149
150
|
const familyAdapter = new SqlFamilyAdapter(context.contract, adapter.profile);
|
|
150
151
|
|
|
151
|
-
const coreOptions: RuntimeCoreOptions<TContract, SqlDriver<unknown
|
|
152
|
+
const coreOptions: RuntimeCoreOptions<TContract, SqlDriver<unknown>, SqlMiddleware> = {
|
|
152
153
|
familyAdapter,
|
|
153
154
|
driver,
|
|
154
155
|
verify,
|
|
@@ -172,12 +173,27 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
|
|
|
172
173
|
}
|
|
173
174
|
}
|
|
174
175
|
|
|
175
|
-
private toExecutionPlan<Row>(
|
|
176
|
+
private async toExecutionPlan<Row>(
|
|
177
|
+
plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
|
|
178
|
+
): Promise<ExecutionPlan<Row>> {
|
|
176
179
|
const isSqlQueryPlan = (p: ExecutionPlan<Row> | SqlQueryPlan<Row>): p is SqlQueryPlan<Row> => {
|
|
177
180
|
return 'ast' in p && !('sql' in p);
|
|
178
181
|
};
|
|
179
182
|
|
|
180
|
-
|
|
183
|
+
if (!isSqlQueryPlan(plan)) {
|
|
184
|
+
return plan;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const rewrittenDraft = await runBeforeCompileChain(
|
|
188
|
+
this.core.middleware,
|
|
189
|
+
{ ast: plan.ast, meta: plan.meta },
|
|
190
|
+
this.core.middlewareContext,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const planToLower: SqlQueryPlan<Row> =
|
|
194
|
+
rewrittenDraft.ast === plan.ast ? plan : { ...plan, ast: rewrittenDraft.ast };
|
|
195
|
+
|
|
196
|
+
return lowerSqlPlan(this.adapter, this.contract, planToLower);
|
|
181
197
|
}
|
|
182
198
|
|
|
183
199
|
private executeAgainstQueryable<Row = Record<string, unknown>>(
|
|
@@ -185,11 +201,11 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
|
|
|
185
201
|
queryable: CoreQueryable,
|
|
186
202
|
): AsyncIterableResult<Row> {
|
|
187
203
|
this.ensureCodecRegistryValidated(this.contract);
|
|
188
|
-
const executablePlan = this.toExecutionPlan(plan);
|
|
189
204
|
|
|
190
205
|
const iterator = async function* (
|
|
191
206
|
self: SqlRuntimeImpl<TContract>,
|
|
192
207
|
): AsyncGenerator<Row, void, unknown> {
|
|
208
|
+
const executablePlan = await self.toExecutionPlan(plan);
|
|
193
209
|
const encodedParams = encodeParams(executablePlan, self.codecRegistry);
|
|
194
210
|
const planWithEncodedParams: ExecutionPlan<Row> = {
|
|
195
211
|
...executablePlan,
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import type { Contract, PlanMeta } from '@prisma-next/contract/types';
|
|
2
|
+
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
3
|
+
import {
|
|
4
|
+
AndExpr,
|
|
5
|
+
BinaryExpr,
|
|
6
|
+
ColumnRef,
|
|
7
|
+
LiteralExpr,
|
|
8
|
+
SelectAst,
|
|
9
|
+
TableSource,
|
|
10
|
+
} from '@prisma-next/sql-relational-core/ast';
|
|
11
|
+
import { timeouts } from '@prisma-next/test-utils';
|
|
12
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
13
|
+
import { runBeforeCompileChain } from '../src/middleware/before-compile-chain';
|
|
14
|
+
import type {
|
|
15
|
+
DraftPlan,
|
|
16
|
+
SqlMiddleware,
|
|
17
|
+
SqlMiddlewareContext,
|
|
18
|
+
} from '../src/middleware/sql-middleware';
|
|
19
|
+
|
|
20
|
+
function createContext(): SqlMiddlewareContext & {
|
|
21
|
+
log: { debug: ReturnType<typeof vi.fn> };
|
|
22
|
+
} {
|
|
23
|
+
const debug = vi.fn();
|
|
24
|
+
return {
|
|
25
|
+
contract: {} as Contract<SqlStorage>,
|
|
26
|
+
mode: 'strict' as const,
|
|
27
|
+
now: () => 0,
|
|
28
|
+
log: {
|
|
29
|
+
info: vi.fn(),
|
|
30
|
+
warn: vi.fn(),
|
|
31
|
+
error: vi.fn(),
|
|
32
|
+
debug,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const meta: PlanMeta = {
|
|
38
|
+
target: 'postgres',
|
|
39
|
+
storageHash: 'sha256:test',
|
|
40
|
+
lane: 'dsl',
|
|
41
|
+
paramDescriptors: [],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function createDraft(): DraftPlan {
|
|
45
|
+
const users = TableSource.named('users');
|
|
46
|
+
return {
|
|
47
|
+
ast: SelectAst.from(users).withProjection([]),
|
|
48
|
+
meta,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('runBeforeCompileChain', () => {
|
|
53
|
+
it(
|
|
54
|
+
'returns the initial draft unchanged when no middleware rewrites',
|
|
55
|
+
async () => {
|
|
56
|
+
const draft = createDraft();
|
|
57
|
+
const ctx = createContext();
|
|
58
|
+
const mw: SqlMiddleware = {
|
|
59
|
+
name: 'noop',
|
|
60
|
+
familyId: 'sql',
|
|
61
|
+
async beforeCompile() {
|
|
62
|
+
return undefined;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const result = await runBeforeCompileChain([mw], draft, ctx);
|
|
67
|
+
|
|
68
|
+
expect(result).toBe(draft);
|
|
69
|
+
expect(ctx.log.debug).not.toHaveBeenCalled();
|
|
70
|
+
},
|
|
71
|
+
timeouts.default,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
it(
|
|
75
|
+
'treats a returned draft with same ast reference as passthrough',
|
|
76
|
+
async () => {
|
|
77
|
+
const draft = createDraft();
|
|
78
|
+
const ctx = createContext();
|
|
79
|
+
const mw: SqlMiddleware = {
|
|
80
|
+
name: 'sameRef',
|
|
81
|
+
familyId: 'sql',
|
|
82
|
+
async beforeCompile(d) {
|
|
83
|
+
return { ...d };
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const result = await runBeforeCompileChain([mw], draft, ctx);
|
|
88
|
+
|
|
89
|
+
expect(result.ast).toBe(draft.ast);
|
|
90
|
+
expect(ctx.log.debug).not.toHaveBeenCalled();
|
|
91
|
+
},
|
|
92
|
+
timeouts.default,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
it(
|
|
96
|
+
'replaces the current draft when a middleware returns a new ast ref',
|
|
97
|
+
async () => {
|
|
98
|
+
const draft = createDraft();
|
|
99
|
+
const ctx = createContext();
|
|
100
|
+
const addWhere = BinaryExpr.eq(ColumnRef.of('users', 'deleted_at'), LiteralExpr.of(null));
|
|
101
|
+
const mw: SqlMiddleware = {
|
|
102
|
+
name: 'softDelete',
|
|
103
|
+
familyId: 'sql',
|
|
104
|
+
async beforeCompile(d) {
|
|
105
|
+
if (d.ast.kind !== 'select') return;
|
|
106
|
+
return { ...d, ast: d.ast.withWhere(addWhere) };
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const result = await runBeforeCompileChain([mw], draft, ctx);
|
|
111
|
+
|
|
112
|
+
expect(result.ast).not.toBe(draft.ast);
|
|
113
|
+
expect(result.ast.kind).toBe('select');
|
|
114
|
+
expect((result.ast as SelectAst).where).toBe(addWhere);
|
|
115
|
+
},
|
|
116
|
+
timeouts.default,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
it(
|
|
120
|
+
'chains rewrites in registration order',
|
|
121
|
+
async () => {
|
|
122
|
+
const draft = createDraft();
|
|
123
|
+
const ctx = createContext();
|
|
124
|
+
const order: string[] = [];
|
|
125
|
+
|
|
126
|
+
const predA = BinaryExpr.eq(ColumnRef.of('users', 'a'), LiteralExpr.of(1));
|
|
127
|
+
const predB = BinaryExpr.eq(ColumnRef.of('users', 'b'), LiteralExpr.of(2));
|
|
128
|
+
|
|
129
|
+
const mwA: SqlMiddleware = {
|
|
130
|
+
name: 'addA',
|
|
131
|
+
familyId: 'sql',
|
|
132
|
+
async beforeCompile(d) {
|
|
133
|
+
order.push('A');
|
|
134
|
+
if (d.ast.kind !== 'select') return;
|
|
135
|
+
return { ...d, ast: d.ast.withWhere(predA) };
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
const mwB: SqlMiddleware = {
|
|
139
|
+
name: 'addB',
|
|
140
|
+
familyId: 'sql',
|
|
141
|
+
async beforeCompile(d) {
|
|
142
|
+
order.push('B');
|
|
143
|
+
if (d.ast.kind !== 'select') return;
|
|
144
|
+
const current = d.ast.where;
|
|
145
|
+
const combined = current ? AndExpr.of([current, predB]) : predB;
|
|
146
|
+
return { ...d, ast: d.ast.withWhere(combined) };
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const result = await runBeforeCompileChain([mwA, mwB], draft, ctx);
|
|
151
|
+
|
|
152
|
+
expect(order).toEqual(['A', 'B']);
|
|
153
|
+
expect(result.ast.kind).toBe('select');
|
|
154
|
+
const where = (result.ast as SelectAst).where;
|
|
155
|
+
expect(where?.kind).toBe('and');
|
|
156
|
+
},
|
|
157
|
+
timeouts.default,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
it(
|
|
161
|
+
'emits a debug log event per rewrite with middleware name and lane',
|
|
162
|
+
async () => {
|
|
163
|
+
const draft = createDraft();
|
|
164
|
+
const ctx = createContext();
|
|
165
|
+
const pred = BinaryExpr.eq(ColumnRef.of('users', 'a'), LiteralExpr.of(1));
|
|
166
|
+
const mw: SqlMiddleware = {
|
|
167
|
+
name: 'rewriteOne',
|
|
168
|
+
familyId: 'sql',
|
|
169
|
+
async beforeCompile(d) {
|
|
170
|
+
if (d.ast.kind !== 'select') return;
|
|
171
|
+
return { ...d, ast: d.ast.withWhere(pred) };
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
await runBeforeCompileChain([mw, mw], draft, ctx);
|
|
176
|
+
|
|
177
|
+
expect(ctx.log.debug).toHaveBeenCalledTimes(2);
|
|
178
|
+
expect(ctx.log.debug).toHaveBeenCalledWith({
|
|
179
|
+
event: 'middleware.rewrite',
|
|
180
|
+
middleware: 'rewriteOne',
|
|
181
|
+
lane: 'dsl',
|
|
182
|
+
});
|
|
183
|
+
},
|
|
184
|
+
timeouts.default,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
it(
|
|
188
|
+
'skips middleware without beforeCompile',
|
|
189
|
+
async () => {
|
|
190
|
+
const draft = createDraft();
|
|
191
|
+
const ctx = createContext();
|
|
192
|
+
const observerOnly: SqlMiddleware = {
|
|
193
|
+
name: 'observer',
|
|
194
|
+
familyId: 'sql',
|
|
195
|
+
async beforeExecute() {},
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const result = await runBeforeCompileChain([observerOnly], draft, ctx);
|
|
199
|
+
|
|
200
|
+
expect(result).toBe(draft);
|
|
201
|
+
expect(ctx.log.debug).not.toHaveBeenCalled();
|
|
202
|
+
},
|
|
203
|
+
timeouts.default,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
it(
|
|
207
|
+
'propagates errors thrown inside beforeCompile',
|
|
208
|
+
async () => {
|
|
209
|
+
const draft = createDraft();
|
|
210
|
+
const ctx = createContext();
|
|
211
|
+
const mw: SqlMiddleware = {
|
|
212
|
+
name: 'thrower',
|
|
213
|
+
familyId: 'sql',
|
|
214
|
+
async beforeCompile() {
|
|
215
|
+
throw new Error('boom');
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
await expect(runBeforeCompileChain([mw], draft, ctx)).rejects.toThrow('boom');
|
|
220
|
+
},
|
|
221
|
+
timeouts.default,
|
|
222
|
+
);
|
|
223
|
+
});
|
package/test/budgets.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { ExecutionPlan, PlanMeta } from '@prisma-next/contract/types';
|
|
2
|
-
import type { AfterExecuteResult
|
|
1
|
+
import type { Contract, ExecutionPlan, PlanMeta } from '@prisma-next/contract/types';
|
|
2
|
+
import type { AfterExecuteResult } from '@prisma-next/runtime-executor';
|
|
3
|
+
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
3
4
|
import {
|
|
4
5
|
AggregateExpr,
|
|
5
6
|
ColumnRef,
|
|
@@ -11,15 +12,14 @@ import {
|
|
|
11
12
|
import { timeouts } from '@prisma-next/test-utils';
|
|
12
13
|
import { describe, expect, it, vi } from 'vitest';
|
|
13
14
|
import { budgets } from '../src/middleware/budgets';
|
|
15
|
+
import type { SqlMiddlewareContext } from '../src/middleware/sql-middleware';
|
|
14
16
|
|
|
15
17
|
const userTable = TableSource.named('user');
|
|
16
18
|
const idCol = ColumnRef.of('user', 'id');
|
|
17
19
|
|
|
18
|
-
function createMiddlewareContext(
|
|
19
|
-
overrides?: Partial<MiddlewareContext<unknown>>,
|
|
20
|
-
): MiddlewareContext<unknown> {
|
|
20
|
+
function createMiddlewareContext(overrides?: Partial<SqlMiddlewareContext>): SqlMiddlewareContext {
|
|
21
21
|
return {
|
|
22
|
-
contract: {}
|
|
22
|
+
contract: {} as Contract<SqlStorage>,
|
|
23
23
|
mode: 'strict' as const,
|
|
24
24
|
now: () => Date.now(),
|
|
25
25
|
log: {
|
package/test/lints.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ExecutionPlan, PlanMeta } from '@prisma-next/contract/types';
|
|
2
|
-
import type {
|
|
1
|
+
import type { Contract, ExecutionPlan, PlanMeta } from '@prisma-next/contract/types';
|
|
2
|
+
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
3
3
|
import {
|
|
4
4
|
BinaryExpr,
|
|
5
5
|
ColumnRef,
|
|
@@ -14,10 +14,11 @@ import {
|
|
|
14
14
|
import { timeouts } from '@prisma-next/test-utils';
|
|
15
15
|
import { describe, expect, it, vi } from 'vitest';
|
|
16
16
|
import { lints } from '../src/middleware/lints';
|
|
17
|
+
import type { SqlMiddlewareContext } from '../src/middleware/sql-middleware';
|
|
17
18
|
|
|
18
|
-
function createMiddlewareContext():
|
|
19
|
+
function createMiddlewareContext(): SqlMiddlewareContext {
|
|
19
20
|
return {
|
|
20
|
-
contract: {}
|
|
21
|
+
contract: {} as Contract<SqlStorage>,
|
|
21
22
|
mode: 'strict' as const,
|
|
22
23
|
now: () => Date.now(),
|
|
23
24
|
log: {
|
package/test/sql-runtime.test.ts
CHANGED
|
@@ -9,12 +9,21 @@ import {
|
|
|
9
9
|
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
10
10
|
import type {
|
|
11
11
|
CodecRegistry,
|
|
12
|
-
SelectAst,
|
|
13
12
|
SqlDriver,
|
|
14
13
|
SqlExecuteRequest,
|
|
15
14
|
} from '@prisma-next/sql-relational-core/ast';
|
|
16
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
BinaryExpr,
|
|
17
|
+
ColumnRef,
|
|
18
|
+
codec,
|
|
19
|
+
createCodecRegistry,
|
|
20
|
+
LiteralExpr,
|
|
21
|
+
SelectAst,
|
|
22
|
+
TableSource,
|
|
23
|
+
} from '@prisma-next/sql-relational-core/ast';
|
|
24
|
+
import type { SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
|
|
17
25
|
import { describe, expect, it, vi } from 'vitest';
|
|
26
|
+
import type { SqlMiddleware } from '../src/middleware/sql-middleware';
|
|
18
27
|
import type {
|
|
19
28
|
SqlRuntimeAdapterDescriptor,
|
|
20
29
|
SqlRuntimeAdapterInstance,
|
|
@@ -380,18 +389,167 @@ describe('createRuntime', () => {
|
|
|
380
389
|
|
|
381
390
|
it('rejects a Mongo middleware with a clear error', () => {
|
|
382
391
|
const { stackInstance, context, driver } = createTestSetup();
|
|
392
|
+
// Simulate a caller bypassing the SqlMiddleware type constraint (e.g. dynamically-loaded
|
|
393
|
+
// middleware). Static typing already rejects familyId: 'mongo'; this tests the runtime guard.
|
|
394
|
+
const mongoMiddleware = { name: 'mongo-mw', familyId: 'mongo' } as unknown as SqlMiddleware;
|
|
383
395
|
expect(() =>
|
|
384
396
|
createRuntime({
|
|
385
397
|
stackInstance,
|
|
386
398
|
context,
|
|
387
399
|
driver,
|
|
388
400
|
verify: { mode: 'onFirstUse', requireMarker: false },
|
|
389
|
-
middleware: [
|
|
401
|
+
middleware: [mongoMiddleware],
|
|
390
402
|
}),
|
|
391
403
|
).toThrow(
|
|
392
404
|
"Middleware 'mongo-mw' requires family 'mongo' but the runtime is configured for family 'sql'",
|
|
393
405
|
);
|
|
394
406
|
});
|
|
407
|
+
|
|
408
|
+
it('invokes beforeCompile and lowers the rewritten AST', async () => {
|
|
409
|
+
const { stackInstance, context, driver } = createTestSetup();
|
|
410
|
+
const debug = vi.fn();
|
|
411
|
+
const softDeletePredicate = BinaryExpr.eq(
|
|
412
|
+
ColumnRef.of('users', 'deleted_at'),
|
|
413
|
+
LiteralExpr.of(null),
|
|
414
|
+
);
|
|
415
|
+
const softDelete: SqlMiddleware = {
|
|
416
|
+
name: 'softDelete',
|
|
417
|
+
familyId: 'sql',
|
|
418
|
+
async beforeCompile(draft) {
|
|
419
|
+
if (draft.ast.kind !== 'select') return;
|
|
420
|
+
return { ...draft, ast: draft.ast.withWhere(softDeletePredicate) };
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const runtime = createRuntime({
|
|
425
|
+
stackInstance,
|
|
426
|
+
context,
|
|
427
|
+
driver,
|
|
428
|
+
verify: { mode: 'onFirstUse', requireMarker: false },
|
|
429
|
+
middleware: [softDelete],
|
|
430
|
+
log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug },
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
const queryPlan: SqlQueryPlan = {
|
|
434
|
+
ast: SelectAst.from(TableSource.named('users')).withProjection([]),
|
|
435
|
+
params: [],
|
|
436
|
+
meta: {
|
|
437
|
+
target: 'postgres',
|
|
438
|
+
storageHash: testContract.storage.storageHash,
|
|
439
|
+
lane: 'dsl',
|
|
440
|
+
paramDescriptors: [],
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
await runtime.execute(queryPlan).toArray();
|
|
445
|
+
|
|
446
|
+
expect(driver.__spies.rootExecute).toHaveBeenCalledTimes(1);
|
|
447
|
+
const request = driver.__spies.rootExecute.mock.calls[0]?.[0] as SqlExecuteRequest;
|
|
448
|
+
expect(request.sql).toContain('deleted_at');
|
|
449
|
+
expect(debug).toHaveBeenCalledWith({
|
|
450
|
+
event: 'middleware.rewrite',
|
|
451
|
+
middleware: 'softDelete',
|
|
452
|
+
lane: 'dsl',
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('invokes adapter.lower exactly once per execute regardless of chain length', async () => {
|
|
457
|
+
const adapter = createStubAdapter();
|
|
458
|
+
const lowerSpy = vi.spyOn(adapter, 'lower');
|
|
459
|
+
const driver = createMockDriver();
|
|
460
|
+
|
|
461
|
+
const targetDescriptor = createTestTargetDescriptor();
|
|
462
|
+
const adapterDescriptor = createTestAdapterDescriptor(adapter);
|
|
463
|
+
const stack = createSqlExecutionStack({
|
|
464
|
+
target: targetDescriptor,
|
|
465
|
+
adapter: adapterDescriptor,
|
|
466
|
+
extensionPacks: [],
|
|
467
|
+
});
|
|
468
|
+
const stackInstance = instantiateExecutionStack(stack) as ExecutionStackInstance<
|
|
469
|
+
'sql',
|
|
470
|
+
'postgres',
|
|
471
|
+
SqlRuntimeAdapterInstance<'postgres'>,
|
|
472
|
+
RuntimeDriverInstance<'sql', 'postgres'>,
|
|
473
|
+
RuntimeExtensionInstance<'sql', 'postgres'>
|
|
474
|
+
>;
|
|
475
|
+
const context = createExecutionContext({
|
|
476
|
+
contract: testContract,
|
|
477
|
+
stack: { target: targetDescriptor, adapter: adapterDescriptor, extensionPacks: [] },
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
const rewriteA: SqlMiddleware = {
|
|
481
|
+
name: 'rewriteA',
|
|
482
|
+
familyId: 'sql',
|
|
483
|
+
async beforeCompile(draft) {
|
|
484
|
+
if (draft.ast.kind !== 'select') return undefined;
|
|
485
|
+
return {
|
|
486
|
+
...draft,
|
|
487
|
+
ast: draft.ast.withWhere(BinaryExpr.eq(ColumnRef.of('users', 'a'), LiteralExpr.of(1))),
|
|
488
|
+
};
|
|
489
|
+
},
|
|
490
|
+
};
|
|
491
|
+
const rewriteB: SqlMiddleware = {
|
|
492
|
+
name: 'rewriteB',
|
|
493
|
+
familyId: 'sql',
|
|
494
|
+
async beforeCompile(draft) {
|
|
495
|
+
if (draft.ast.kind !== 'select') return undefined;
|
|
496
|
+
return {
|
|
497
|
+
...draft,
|
|
498
|
+
ast: draft.ast.withWhere(BinaryExpr.eq(ColumnRef.of('users', 'b'), LiteralExpr.of(2))),
|
|
499
|
+
};
|
|
500
|
+
},
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const runtime = createRuntime({
|
|
504
|
+
stackInstance,
|
|
505
|
+
context,
|
|
506
|
+
driver,
|
|
507
|
+
verify: { mode: 'onFirstUse', requireMarker: false },
|
|
508
|
+
middleware: [rewriteA, rewriteB],
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
const queryPlan: SqlQueryPlan = {
|
|
512
|
+
ast: SelectAst.from(TableSource.named('users')).withProjection([]),
|
|
513
|
+
params: [],
|
|
514
|
+
meta: {
|
|
515
|
+
target: 'postgres',
|
|
516
|
+
storageHash: testContract.storage.storageHash,
|
|
517
|
+
lane: 'dsl',
|
|
518
|
+
paramDescriptors: [],
|
|
519
|
+
},
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
await runtime.execute(queryPlan).toArray();
|
|
523
|
+
|
|
524
|
+
expect(lowerSpy).toHaveBeenCalledTimes(1);
|
|
525
|
+
const loweredAst = lowerSpy.mock.calls[0]?.[0] as SelectAst;
|
|
526
|
+
expect(loweredAst.where?.kind).toBe('binary');
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it('skips beforeCompile for raw execution plans with no AST', async () => {
|
|
530
|
+
const { stackInstance, context, driver } = createTestSetup();
|
|
531
|
+
const debug = vi.fn();
|
|
532
|
+
const beforeCompile = vi.fn();
|
|
533
|
+
const observer: SqlMiddleware = {
|
|
534
|
+
name: 'observer',
|
|
535
|
+
familyId: 'sql',
|
|
536
|
+
beforeCompile,
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
const runtime = createRuntime({
|
|
540
|
+
stackInstance,
|
|
541
|
+
context,
|
|
542
|
+
driver,
|
|
543
|
+
verify: { mode: 'onFirstUse', requireMarker: false },
|
|
544
|
+
middleware: [observer],
|
|
545
|
+
log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug },
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
await runtime.execute(createRawExecutionPlan()).toArray();
|
|
549
|
+
|
|
550
|
+
expect(beforeCompile).not.toHaveBeenCalled();
|
|
551
|
+
expect(debug).not.toHaveBeenCalled();
|
|
552
|
+
});
|
|
395
553
|
});
|
|
396
554
|
|
|
397
555
|
describe('withTransaction', () => {
|