@prisma-next/cli 0.3.0-dev.4 → 0.3.0-dev.41
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/README.md +125 -40
- package/dist/cli.d.mts +1 -0
- package/dist/cli.js +1 -2376
- package/dist/cli.mjs +168 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/client-Lm9Q6aQM.mjs +694 -0
- package/dist/client-Lm9Q6aQM.mjs.map +1 -0
- package/dist/commands/contract-emit.d.mts +7 -0
- package/dist/commands/contract-emit.d.mts.map +1 -0
- package/dist/commands/contract-emit.mjs +140 -0
- package/dist/commands/contract-emit.mjs.map +1 -0
- package/dist/commands/db-init.d.mts +7 -0
- package/dist/commands/db-init.d.mts.map +1 -0
- package/dist/commands/db-init.mjs +179 -0
- package/dist/commands/db-init.mjs.map +1 -0
- package/dist/commands/db-introspect.d.mts +7 -0
- package/dist/commands/db-introspect.d.mts.map +1 -0
- package/dist/commands/db-introspect.mjs +120 -0
- package/dist/commands/db-introspect.mjs.map +1 -0
- package/dist/commands/db-schema-verify.d.mts +7 -0
- package/dist/commands/db-schema-verify.d.mts.map +1 -0
- package/dist/commands/db-schema-verify.mjs +116 -0
- package/dist/commands/db-schema-verify.mjs.map +1 -0
- package/dist/commands/db-sign.d.mts +7 -0
- package/dist/commands/db-sign.d.mts.map +1 -0
- package/dist/commands/db-sign.mjs +138 -0
- package/dist/commands/db-sign.mjs.map +1 -0
- package/dist/commands/db-verify.d.mts +7 -0
- package/dist/commands/db-verify.d.mts.map +1 -0
- package/dist/commands/db-verify.mjs +129 -0
- package/dist/commands/db-verify.mjs.map +1 -0
- package/dist/config-loader-CnnWuluc.mjs +42 -0
- package/dist/config-loader-CnnWuluc.mjs.map +1 -0
- package/dist/{config-loader.d.ts → config-loader.d.mts} +5 -2
- package/dist/config-loader.d.mts.map +1 -0
- package/dist/config-loader.mjs +3 -0
- package/dist/exports/config-types.d.mts +2 -0
- package/dist/exports/config-types.mjs +3 -0
- package/dist/exports/control-api.d.mts +451 -0
- package/dist/exports/control-api.d.mts.map +1 -0
- package/dist/exports/control-api.mjs +59 -0
- package/dist/exports/control-api.mjs.map +1 -0
- package/dist/{index.d.ts → exports/index.d.mts} +7 -6
- package/dist/exports/index.d.mts.map +1 -0
- package/dist/exports/index.mjs +130 -0
- package/dist/exports/index.mjs.map +1 -0
- package/dist/result-handler-BZPY7HX4.mjs +1029 -0
- package/dist/result-handler-BZPY7HX4.mjs.map +1 -0
- package/package.json +50 -38
- package/src/cli.ts +260 -0
- package/src/commands/contract-emit.ts +267 -0
- package/src/commands/db-init.ts +355 -0
- package/src/commands/db-introspect.ts +227 -0
- package/src/commands/db-schema-verify.ts +238 -0
- package/src/commands/db-sign.ts +279 -0
- package/src/commands/db-verify.ts +259 -0
- package/src/config-loader.ts +76 -0
- package/src/control-api/client.ts +591 -0
- package/src/control-api/operations/contract-emit.ts +103 -0
- package/src/control-api/operations/db-init.ts +281 -0
- package/src/control-api/types.ts +493 -0
- package/src/exports/config-types.ts +6 -0
- package/src/exports/control-api.ts +51 -0
- package/src/exports/index.ts +4 -0
- package/src/load-ts-contract.ts +222 -0
- package/src/utils/cli-errors.ts +26 -0
- package/src/utils/command-helpers.ts +26 -0
- package/src/utils/framework-components.ts +177 -0
- package/src/utils/global-flags.ts +75 -0
- package/src/utils/output.ts +1477 -0
- package/src/utils/progress-adapter.ts +86 -0
- package/src/utils/result-handler.ts +44 -0
- package/dist/chunk-464LNZCE.js +0 -134
- package/dist/chunk-464LNZCE.js.map +0 -1
- package/dist/chunk-BZMBKEEQ.js +0 -997
- package/dist/chunk-BZMBKEEQ.js.map +0 -1
- package/dist/chunk-HWYQOCAJ.js +0 -47
- package/dist/chunk-HWYQOCAJ.js.map +0 -1
- package/dist/chunk-ZKYEJROM.js +0 -94
- package/dist/chunk-ZKYEJROM.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/commands/contract-emit.d.ts +0 -5
- package/dist/commands/contract-emit.js +0 -9
- package/dist/commands/contract-emit.js.map +0 -1
- package/dist/commands/db-init.d.ts +0 -5
- package/dist/commands/db-init.js +0 -341
- package/dist/commands/db-init.js.map +0 -1
- package/dist/commands/db-introspect.d.ts +0 -5
- package/dist/commands/db-introspect.js +0 -190
- package/dist/commands/db-introspect.js.map +0 -1
- package/dist/commands/db-schema-verify.d.ts +0 -5
- package/dist/commands/db-schema-verify.js +0 -164
- package/dist/commands/db-schema-verify.js.map +0 -1
- package/dist/commands/db-sign.d.ts +0 -5
- package/dist/commands/db-sign.js +0 -199
- package/dist/commands/db-sign.js.map +0 -1
- package/dist/commands/db-verify.d.ts +0 -5
- package/dist/commands/db-verify.js +0 -173
- package/dist/commands/db-verify.js.map +0 -1
- package/dist/config-loader.js +0 -7
- package/dist/config-loader.js.map +0 -1
- package/dist/config-types.d.ts +0 -1
- package/dist/config-types.js +0 -6
- package/dist/config-types.js.map +0 -1
- package/dist/index.js +0 -175
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
import type { TargetBoundComponentDescriptor } from '@prisma-next/contract/framework-components';
|
|
2
|
+
import type { CoreSchemaView } from '@prisma-next/core-control-plane/schema-view';
|
|
3
|
+
import { createControlPlaneStack } from '@prisma-next/core-control-plane/stack';
|
|
4
|
+
import type {
|
|
5
|
+
ControlDriverInstance,
|
|
6
|
+
ControlFamilyInstance,
|
|
7
|
+
ControlPlaneStack,
|
|
8
|
+
SignDatabaseResult,
|
|
9
|
+
VerifyDatabaseResult,
|
|
10
|
+
VerifyDatabaseSchemaResult,
|
|
11
|
+
} from '@prisma-next/core-control-plane/types';
|
|
12
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
13
|
+
import { notOk, ok } from '@prisma-next/utils/result';
|
|
14
|
+
import { assertFrameworkComponentsCompatible } from '../utils/framework-components';
|
|
15
|
+
import { executeDbInit } from './operations/db-init';
|
|
16
|
+
import type {
|
|
17
|
+
ControlClient,
|
|
18
|
+
ControlClientOptions,
|
|
19
|
+
DbInitOptions,
|
|
20
|
+
DbInitResult,
|
|
21
|
+
EmitOptions,
|
|
22
|
+
EmitResult,
|
|
23
|
+
IntrospectOptions,
|
|
24
|
+
SchemaVerifyOptions,
|
|
25
|
+
SignOptions,
|
|
26
|
+
VerifyOptions,
|
|
27
|
+
} from './types';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Creates a programmatic control client for Prisma Next operations.
|
|
31
|
+
*
|
|
32
|
+
* The client accepts framework component descriptors at creation time,
|
|
33
|
+
* manages driver lifecycle via connect()/close(), and exposes domain
|
|
34
|
+
* operations that delegate to the existing family instance methods.
|
|
35
|
+
*
|
|
36
|
+
* @see {@link ControlClient} for the client interface
|
|
37
|
+
* @see README.md "Programmatic Control API" section for usage examples
|
|
38
|
+
*/
|
|
39
|
+
export function createControlClient(options: ControlClientOptions): ControlClient {
|
|
40
|
+
return new ControlClientImpl(options);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Implementation of ControlClient.
|
|
45
|
+
* Manages initialization and connection state, delegates operations to family instance.
|
|
46
|
+
*/
|
|
47
|
+
class ControlClientImpl implements ControlClient {
|
|
48
|
+
private readonly options: ControlClientOptions;
|
|
49
|
+
private stack: ControlPlaneStack<string, string> | null = null;
|
|
50
|
+
private driver: ControlDriverInstance<string, string> | null = null;
|
|
51
|
+
private familyInstance: ControlFamilyInstance<string> | null = null;
|
|
52
|
+
private frameworkComponents: ReadonlyArray<
|
|
53
|
+
TargetBoundComponentDescriptor<string, string>
|
|
54
|
+
> | null = null;
|
|
55
|
+
private initialized = false;
|
|
56
|
+
private readonly defaultConnection: unknown;
|
|
57
|
+
|
|
58
|
+
constructor(options: ControlClientOptions) {
|
|
59
|
+
this.options = options;
|
|
60
|
+
this.defaultConnection = options.connection;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
init(): void {
|
|
64
|
+
if (this.initialized) {
|
|
65
|
+
return; // Idempotent
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Create the control plane stack
|
|
69
|
+
this.stack = createControlPlaneStack({
|
|
70
|
+
target: this.options.target,
|
|
71
|
+
adapter: this.options.adapter,
|
|
72
|
+
driver: this.options.driver,
|
|
73
|
+
extensionPacks: this.options.extensionPacks,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Create family instance using the stack
|
|
77
|
+
this.familyInstance = this.options.family.create(this.stack);
|
|
78
|
+
|
|
79
|
+
// Validate and type-narrow framework components
|
|
80
|
+
const rawComponents = [
|
|
81
|
+
this.options.target,
|
|
82
|
+
this.options.adapter,
|
|
83
|
+
...(this.options.extensionPacks ?? []),
|
|
84
|
+
];
|
|
85
|
+
this.frameworkComponents = assertFrameworkComponentsCompatible(
|
|
86
|
+
this.options.family.familyId,
|
|
87
|
+
this.options.target.targetId,
|
|
88
|
+
rawComponents,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
this.initialized = true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async connect(connection?: unknown): Promise<void> {
|
|
95
|
+
// Auto-init if needed
|
|
96
|
+
this.init();
|
|
97
|
+
|
|
98
|
+
if (this.driver) {
|
|
99
|
+
throw new Error('Already connected. Call close() before reconnecting.');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Resolve connection: argument > default from options
|
|
103
|
+
const resolvedConnection = connection ?? this.defaultConnection;
|
|
104
|
+
if (resolvedConnection === undefined) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
'No connection provided. Pass a connection to connect() or provide a default connection when creating the client.',
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Check for driver descriptor
|
|
111
|
+
if (!this.stack?.driver) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
'Driver is not configured. Pass a driver descriptor when creating the control client to enable database operations.',
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Create driver instance
|
|
118
|
+
// Cast through any since connection type is driver-specific at runtime.
|
|
119
|
+
// The driver descriptor is typed with any for TConnection in ControlClientOptions,
|
|
120
|
+
// but createControlPlaneStack defaults it to string. We bridge this at runtime.
|
|
121
|
+
// biome-ignore lint/suspicious/noExplicitAny: required for runtime connection type flexibility
|
|
122
|
+
this.driver = await this.stack?.driver.create(resolvedConnection as any);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async close(): Promise<void> {
|
|
126
|
+
if (this.driver) {
|
|
127
|
+
await this.driver.close();
|
|
128
|
+
this.driver = null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private async ensureConnected(): Promise<{
|
|
133
|
+
driver: ControlDriverInstance<string, string>;
|
|
134
|
+
familyInstance: ControlFamilyInstance<string>;
|
|
135
|
+
frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<string, string>>;
|
|
136
|
+
}> {
|
|
137
|
+
// Auto-init if needed
|
|
138
|
+
this.init();
|
|
139
|
+
|
|
140
|
+
// Auto-connect if not connected and default connection is available
|
|
141
|
+
if (!this.driver && this.defaultConnection !== undefined) {
|
|
142
|
+
await this.connect(this.defaultConnection);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!this.driver || !this.familyInstance || !this.frameworkComponents) {
|
|
146
|
+
throw new Error('Not connected. Call connect(connection) first.');
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
driver: this.driver,
|
|
150
|
+
familyInstance: this.familyInstance,
|
|
151
|
+
frameworkComponents: this.frameworkComponents,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async verify(options: VerifyOptions): Promise<VerifyDatabaseResult> {
|
|
156
|
+
const { onProgress } = options;
|
|
157
|
+
|
|
158
|
+
// Connect with progress span if connection provided
|
|
159
|
+
if (options.connection !== undefined) {
|
|
160
|
+
onProgress?.({
|
|
161
|
+
action: 'verify',
|
|
162
|
+
kind: 'spanStart',
|
|
163
|
+
spanId: 'connect',
|
|
164
|
+
label: 'Connecting to database...',
|
|
165
|
+
});
|
|
166
|
+
try {
|
|
167
|
+
await this.connect(options.connection);
|
|
168
|
+
onProgress?.({
|
|
169
|
+
action: 'verify',
|
|
170
|
+
kind: 'spanEnd',
|
|
171
|
+
spanId: 'connect',
|
|
172
|
+
outcome: 'ok',
|
|
173
|
+
});
|
|
174
|
+
} catch (error) {
|
|
175
|
+
onProgress?.({
|
|
176
|
+
action: 'verify',
|
|
177
|
+
kind: 'spanEnd',
|
|
178
|
+
spanId: 'connect',
|
|
179
|
+
outcome: 'error',
|
|
180
|
+
});
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const { driver, familyInstance } = await this.ensureConnected();
|
|
186
|
+
|
|
187
|
+
// Validate contract using family instance
|
|
188
|
+
const contractIR = familyInstance.validateContractIR(options.contractIR);
|
|
189
|
+
|
|
190
|
+
// Emit verify span
|
|
191
|
+
onProgress?.({
|
|
192
|
+
action: 'verify',
|
|
193
|
+
kind: 'spanStart',
|
|
194
|
+
spanId: 'verify',
|
|
195
|
+
label: 'Verifying contract marker...',
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
// Delegate to family instance verify method
|
|
200
|
+
// Note: We pass empty strings for contractPath/configPath since the programmatic
|
|
201
|
+
// API doesn't deal with file paths. The family instance accepts these as optional
|
|
202
|
+
// metadata for error reporting.
|
|
203
|
+
const result = await familyInstance.verify({
|
|
204
|
+
driver,
|
|
205
|
+
contractIR,
|
|
206
|
+
expectedTargetId: this.options.target.targetId,
|
|
207
|
+
contractPath: '',
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
onProgress?.({
|
|
211
|
+
action: 'verify',
|
|
212
|
+
kind: 'spanEnd',
|
|
213
|
+
spanId: 'verify',
|
|
214
|
+
outcome: result.ok ? 'ok' : 'error',
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
return result;
|
|
218
|
+
} catch (error) {
|
|
219
|
+
onProgress?.({
|
|
220
|
+
action: 'verify',
|
|
221
|
+
kind: 'spanEnd',
|
|
222
|
+
spanId: 'verify',
|
|
223
|
+
outcome: 'error',
|
|
224
|
+
});
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async schemaVerify(options: SchemaVerifyOptions): Promise<VerifyDatabaseSchemaResult> {
|
|
230
|
+
const { onProgress } = options;
|
|
231
|
+
|
|
232
|
+
// Connect with progress span if connection provided
|
|
233
|
+
if (options.connection !== undefined) {
|
|
234
|
+
onProgress?.({
|
|
235
|
+
action: 'schemaVerify',
|
|
236
|
+
kind: 'spanStart',
|
|
237
|
+
spanId: 'connect',
|
|
238
|
+
label: 'Connecting to database...',
|
|
239
|
+
});
|
|
240
|
+
try {
|
|
241
|
+
await this.connect(options.connection);
|
|
242
|
+
onProgress?.({
|
|
243
|
+
action: 'schemaVerify',
|
|
244
|
+
kind: 'spanEnd',
|
|
245
|
+
spanId: 'connect',
|
|
246
|
+
outcome: 'ok',
|
|
247
|
+
});
|
|
248
|
+
} catch (error) {
|
|
249
|
+
onProgress?.({
|
|
250
|
+
action: 'schemaVerify',
|
|
251
|
+
kind: 'spanEnd',
|
|
252
|
+
spanId: 'connect',
|
|
253
|
+
outcome: 'error',
|
|
254
|
+
});
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const { driver, familyInstance, frameworkComponents } = await this.ensureConnected();
|
|
260
|
+
|
|
261
|
+
// Validate contract using family instance
|
|
262
|
+
const contractIR = familyInstance.validateContractIR(options.contractIR);
|
|
263
|
+
|
|
264
|
+
// Emit schemaVerify span
|
|
265
|
+
onProgress?.({
|
|
266
|
+
action: 'schemaVerify',
|
|
267
|
+
kind: 'spanStart',
|
|
268
|
+
spanId: 'schemaVerify',
|
|
269
|
+
label: 'Verifying database schema...',
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
// Delegate to family instance schemaVerify method
|
|
274
|
+
const result = await familyInstance.schemaVerify({
|
|
275
|
+
driver,
|
|
276
|
+
contractIR,
|
|
277
|
+
strict: options.strict ?? false,
|
|
278
|
+
contractPath: '',
|
|
279
|
+
frameworkComponents,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
onProgress?.({
|
|
283
|
+
action: 'schemaVerify',
|
|
284
|
+
kind: 'spanEnd',
|
|
285
|
+
spanId: 'schemaVerify',
|
|
286
|
+
outcome: result.ok ? 'ok' : 'error',
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
return result;
|
|
290
|
+
} catch (error) {
|
|
291
|
+
onProgress?.({
|
|
292
|
+
action: 'schemaVerify',
|
|
293
|
+
kind: 'spanEnd',
|
|
294
|
+
spanId: 'schemaVerify',
|
|
295
|
+
outcome: 'error',
|
|
296
|
+
});
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async sign(options: SignOptions): Promise<SignDatabaseResult> {
|
|
302
|
+
const { onProgress } = options;
|
|
303
|
+
|
|
304
|
+
// Connect with progress span if connection provided
|
|
305
|
+
if (options.connection !== undefined) {
|
|
306
|
+
onProgress?.({
|
|
307
|
+
action: 'sign',
|
|
308
|
+
kind: 'spanStart',
|
|
309
|
+
spanId: 'connect',
|
|
310
|
+
label: 'Connecting to database...',
|
|
311
|
+
});
|
|
312
|
+
try {
|
|
313
|
+
await this.connect(options.connection);
|
|
314
|
+
onProgress?.({
|
|
315
|
+
action: 'sign',
|
|
316
|
+
kind: 'spanEnd',
|
|
317
|
+
spanId: 'connect',
|
|
318
|
+
outcome: 'ok',
|
|
319
|
+
});
|
|
320
|
+
} catch (error) {
|
|
321
|
+
onProgress?.({
|
|
322
|
+
action: 'sign',
|
|
323
|
+
kind: 'spanEnd',
|
|
324
|
+
spanId: 'connect',
|
|
325
|
+
outcome: 'error',
|
|
326
|
+
});
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const { driver, familyInstance } = await this.ensureConnected();
|
|
332
|
+
|
|
333
|
+
// Validate contract using family instance
|
|
334
|
+
const contractIR = familyInstance.validateContractIR(options.contractIR);
|
|
335
|
+
|
|
336
|
+
// Emit sign span
|
|
337
|
+
onProgress?.({
|
|
338
|
+
action: 'sign',
|
|
339
|
+
kind: 'spanStart',
|
|
340
|
+
spanId: 'sign',
|
|
341
|
+
label: 'Signing database...',
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
// Delegate to family instance sign method
|
|
346
|
+
const result = await familyInstance.sign({
|
|
347
|
+
driver,
|
|
348
|
+
contractIR,
|
|
349
|
+
contractPath: options.contractPath ?? '',
|
|
350
|
+
...ifDefined('configPath', options.configPath),
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
onProgress?.({
|
|
354
|
+
action: 'sign',
|
|
355
|
+
kind: 'spanEnd',
|
|
356
|
+
spanId: 'sign',
|
|
357
|
+
outcome: 'ok',
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
return result;
|
|
361
|
+
} catch (error) {
|
|
362
|
+
onProgress?.({
|
|
363
|
+
action: 'sign',
|
|
364
|
+
kind: 'spanEnd',
|
|
365
|
+
spanId: 'sign',
|
|
366
|
+
outcome: 'error',
|
|
367
|
+
});
|
|
368
|
+
throw error;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async dbInit(options: DbInitOptions): Promise<DbInitResult> {
|
|
373
|
+
const { onProgress } = options;
|
|
374
|
+
|
|
375
|
+
// Connect with progress span if connection provided
|
|
376
|
+
if (options.connection !== undefined) {
|
|
377
|
+
onProgress?.({
|
|
378
|
+
action: 'dbInit',
|
|
379
|
+
kind: 'spanStart',
|
|
380
|
+
spanId: 'connect',
|
|
381
|
+
label: 'Connecting to database...',
|
|
382
|
+
});
|
|
383
|
+
try {
|
|
384
|
+
await this.connect(options.connection);
|
|
385
|
+
onProgress?.({
|
|
386
|
+
action: 'dbInit',
|
|
387
|
+
kind: 'spanEnd',
|
|
388
|
+
spanId: 'connect',
|
|
389
|
+
outcome: 'ok',
|
|
390
|
+
});
|
|
391
|
+
} catch (error) {
|
|
392
|
+
onProgress?.({
|
|
393
|
+
action: 'dbInit',
|
|
394
|
+
kind: 'spanEnd',
|
|
395
|
+
spanId: 'connect',
|
|
396
|
+
outcome: 'error',
|
|
397
|
+
});
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const { driver, familyInstance, frameworkComponents } = await this.ensureConnected();
|
|
403
|
+
|
|
404
|
+
// Check target supports migrations
|
|
405
|
+
if (!this.options.target.migrations) {
|
|
406
|
+
throw new Error(`Target "${this.options.target.targetId}" does not support migrations`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Validate contract using family instance
|
|
410
|
+
const contractIR = familyInstance.validateContractIR(options.contractIR);
|
|
411
|
+
|
|
412
|
+
// Delegate to extracted dbInit operation
|
|
413
|
+
return executeDbInit({
|
|
414
|
+
driver,
|
|
415
|
+
familyInstance,
|
|
416
|
+
contractIR,
|
|
417
|
+
mode: options.mode,
|
|
418
|
+
migrations: this.options.target.migrations,
|
|
419
|
+
frameworkComponents,
|
|
420
|
+
...(onProgress ? { onProgress } : {}),
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async introspect(options?: IntrospectOptions): Promise<unknown> {
|
|
425
|
+
const onProgress = options?.onProgress;
|
|
426
|
+
|
|
427
|
+
// Connect with progress span if connection provided
|
|
428
|
+
if (options?.connection !== undefined) {
|
|
429
|
+
onProgress?.({
|
|
430
|
+
action: 'introspect',
|
|
431
|
+
kind: 'spanStart',
|
|
432
|
+
spanId: 'connect',
|
|
433
|
+
label: 'Connecting to database...',
|
|
434
|
+
});
|
|
435
|
+
try {
|
|
436
|
+
await this.connect(options.connection);
|
|
437
|
+
onProgress?.({
|
|
438
|
+
action: 'introspect',
|
|
439
|
+
kind: 'spanEnd',
|
|
440
|
+
spanId: 'connect',
|
|
441
|
+
outcome: 'ok',
|
|
442
|
+
});
|
|
443
|
+
} catch (error) {
|
|
444
|
+
onProgress?.({
|
|
445
|
+
action: 'introspect',
|
|
446
|
+
kind: 'spanEnd',
|
|
447
|
+
spanId: 'connect',
|
|
448
|
+
outcome: 'error',
|
|
449
|
+
});
|
|
450
|
+
throw error;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const { driver, familyInstance } = await this.ensureConnected();
|
|
455
|
+
|
|
456
|
+
// TODO: Pass schema option to familyInstance.introspect when schema filtering is implemented
|
|
457
|
+
const _schema = options?.schema;
|
|
458
|
+
void _schema;
|
|
459
|
+
|
|
460
|
+
// Emit introspect span
|
|
461
|
+
onProgress?.({
|
|
462
|
+
action: 'introspect',
|
|
463
|
+
kind: 'spanStart',
|
|
464
|
+
spanId: 'introspect',
|
|
465
|
+
label: 'Introspecting database schema...',
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
try {
|
|
469
|
+
const result = await familyInstance.introspect({ driver });
|
|
470
|
+
|
|
471
|
+
onProgress?.({
|
|
472
|
+
action: 'introspect',
|
|
473
|
+
kind: 'spanEnd',
|
|
474
|
+
spanId: 'introspect',
|
|
475
|
+
outcome: 'ok',
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
return result;
|
|
479
|
+
} catch (error) {
|
|
480
|
+
onProgress?.({
|
|
481
|
+
action: 'introspect',
|
|
482
|
+
kind: 'spanEnd',
|
|
483
|
+
spanId: 'introspect',
|
|
484
|
+
outcome: 'error',
|
|
485
|
+
});
|
|
486
|
+
throw error;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
toSchemaView(schemaIR: unknown): CoreSchemaView | undefined {
|
|
491
|
+
this.init();
|
|
492
|
+
if (this.familyInstance?.toSchemaView) {
|
|
493
|
+
return this.familyInstance.toSchemaView(schemaIR);
|
|
494
|
+
}
|
|
495
|
+
return undefined;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async emit(options: EmitOptions): Promise<EmitResult> {
|
|
499
|
+
const { onProgress, contractConfig } = options;
|
|
500
|
+
|
|
501
|
+
// Ensure initialized (creates stack and family instance)
|
|
502
|
+
// emit() does NOT require a database connection
|
|
503
|
+
this.init();
|
|
504
|
+
|
|
505
|
+
if (!this.familyInstance) {
|
|
506
|
+
throw new Error('Family instance was not initialized. This is a bug.');
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Resolve contract source
|
|
510
|
+
let contractRaw: unknown;
|
|
511
|
+
onProgress?.({
|
|
512
|
+
action: 'emit',
|
|
513
|
+
kind: 'spanStart',
|
|
514
|
+
spanId: 'resolveSource',
|
|
515
|
+
label: 'Resolving contract source...',
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
try {
|
|
519
|
+
switch (contractConfig.source.kind) {
|
|
520
|
+
case 'loader':
|
|
521
|
+
contractRaw = await contractConfig.source.load();
|
|
522
|
+
break;
|
|
523
|
+
case 'value':
|
|
524
|
+
contractRaw = contractConfig.source.value;
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
onProgress?.({
|
|
529
|
+
action: 'emit',
|
|
530
|
+
kind: 'spanEnd',
|
|
531
|
+
spanId: 'resolveSource',
|
|
532
|
+
outcome: 'ok',
|
|
533
|
+
});
|
|
534
|
+
} catch (error) {
|
|
535
|
+
onProgress?.({
|
|
536
|
+
action: 'emit',
|
|
537
|
+
kind: 'spanEnd',
|
|
538
|
+
spanId: 'resolveSource',
|
|
539
|
+
outcome: 'error',
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
return notOk({
|
|
543
|
+
code: 'CONTRACT_SOURCE_INVALID',
|
|
544
|
+
summary: 'Failed to resolve contract source',
|
|
545
|
+
why: error instanceof Error ? error.message : String(error),
|
|
546
|
+
meta: undefined,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Emit contract
|
|
551
|
+
onProgress?.({
|
|
552
|
+
action: 'emit',
|
|
553
|
+
kind: 'spanStart',
|
|
554
|
+
spanId: 'emit',
|
|
555
|
+
label: 'Emitting contract...',
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
try {
|
|
559
|
+
const emitResult = await this.familyInstance.emitContract({ contractIR: contractRaw });
|
|
560
|
+
|
|
561
|
+
onProgress?.({
|
|
562
|
+
action: 'emit',
|
|
563
|
+
kind: 'spanEnd',
|
|
564
|
+
spanId: 'emit',
|
|
565
|
+
outcome: 'ok',
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
return ok({
|
|
569
|
+
storageHash: emitResult.storageHash,
|
|
570
|
+
...ifDefined('executionHash', emitResult.executionHash),
|
|
571
|
+
profileHash: emitResult.profileHash,
|
|
572
|
+
contractJson: emitResult.contractJson,
|
|
573
|
+
contractDts: emitResult.contractDts,
|
|
574
|
+
});
|
|
575
|
+
} catch (error) {
|
|
576
|
+
onProgress?.({
|
|
577
|
+
action: 'emit',
|
|
578
|
+
kind: 'spanEnd',
|
|
579
|
+
spanId: 'emit',
|
|
580
|
+
outcome: 'error',
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
return notOk({
|
|
584
|
+
code: 'EMIT_FAILED',
|
|
585
|
+
summary: 'Failed to emit contract',
|
|
586
|
+
why: error instanceof Error ? error.message : String(error),
|
|
587
|
+
meta: undefined,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { createControlPlaneStack } from '@prisma-next/core-control-plane/stack';
|
|
3
|
+
import { abortable } from '@prisma-next/utils/abortable';
|
|
4
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
5
|
+
import { dirname, isAbsolute, join, resolve } from 'pathe';
|
|
6
|
+
import { loadConfig } from '../../config-loader';
|
|
7
|
+
import { errorContractConfigMissing } from '../../utils/cli-errors';
|
|
8
|
+
import type { ContractEmitOptions, ContractEmitResult } from '../types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Executes the contract emit operation.
|
|
12
|
+
*
|
|
13
|
+
* This is an offline operation that:
|
|
14
|
+
* 1. Loads the Prisma Next config from the specified path
|
|
15
|
+
* 2. Resolves the contract source from config
|
|
16
|
+
* 3. Creates a control plane stack and family instance
|
|
17
|
+
* 4. Emits contract artifacts (JSON and DTS)
|
|
18
|
+
* 5. Writes files to the paths specified in config
|
|
19
|
+
*
|
|
20
|
+
* Supports AbortSignal for cancellation, enabling "last change wins" behavior.
|
|
21
|
+
*
|
|
22
|
+
* @param options - Options including configPath and optional signal
|
|
23
|
+
* @returns File paths and hashes of emitted artifacts
|
|
24
|
+
* @throws If config loading fails, contract is invalid, or file I/O fails
|
|
25
|
+
* @throws signal.reason if cancelled via AbortSignal (typically DOMException with name 'AbortError')
|
|
26
|
+
*/
|
|
27
|
+
export async function executeContractEmit(
|
|
28
|
+
options: ContractEmitOptions,
|
|
29
|
+
): Promise<ContractEmitResult> {
|
|
30
|
+
const { configPath, signal = new AbortController().signal } = options;
|
|
31
|
+
const unlessAborted = abortable(signal);
|
|
32
|
+
|
|
33
|
+
// Load config using the existing config loader
|
|
34
|
+
const config = await unlessAborted(loadConfig(configPath));
|
|
35
|
+
|
|
36
|
+
// Validate contract config is present
|
|
37
|
+
if (!config.contract) {
|
|
38
|
+
throw errorContractConfigMissing({
|
|
39
|
+
why: 'Config.contract is required for emit. Define it in your config: contract: { source: ..., output: ... }',
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const contractConfig = config.contract;
|
|
44
|
+
|
|
45
|
+
// Validate output path is present and ends with .json
|
|
46
|
+
if (!contractConfig.output) {
|
|
47
|
+
throw errorContractConfigMissing({
|
|
48
|
+
why: 'Contract config must have output path. This should not happen if defineConfig() was used.',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (!contractConfig.output.endsWith('.json')) {
|
|
52
|
+
throw errorContractConfigMissing({
|
|
53
|
+
why: 'Contract config output path must end with .json (e.g., "src/prisma/contract.json")',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Validate source is defined and is either a function or a non-null object
|
|
58
|
+
if (
|
|
59
|
+
contractConfig.source === null ||
|
|
60
|
+
(typeof contractConfig.source !== 'function' && typeof contractConfig.source !== 'object')
|
|
61
|
+
) {
|
|
62
|
+
throw errorContractConfigMissing({
|
|
63
|
+
why: 'Contract config must include a valid source (function or non-null object)',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Normalize configPath and resolve artifact paths relative to config file directory
|
|
68
|
+
const normalizedConfigPath = resolve(configPath);
|
|
69
|
+
const configDir = dirname(normalizedConfigPath);
|
|
70
|
+
const outputJsonPath = isAbsolute(contractConfig.output)
|
|
71
|
+
? contractConfig.output
|
|
72
|
+
: join(configDir, contractConfig.output);
|
|
73
|
+
// Colocate .d.ts with .json (contract.json → contract.d.ts)
|
|
74
|
+
const outputDtsPath = `${outputJsonPath.slice(0, -5)}.d.ts`;
|
|
75
|
+
|
|
76
|
+
// Create control plane stack from config
|
|
77
|
+
const stack = createControlPlaneStack(config);
|
|
78
|
+
const familyInstance = config.family.create(stack);
|
|
79
|
+
|
|
80
|
+
// Resolve contract source from config
|
|
81
|
+
const contractRaw =
|
|
82
|
+
typeof contractConfig.source === 'function'
|
|
83
|
+
? await unlessAborted(contractConfig.source())
|
|
84
|
+
: contractConfig.source;
|
|
85
|
+
|
|
86
|
+
// Emit contract via family instance
|
|
87
|
+
const emitResult = await unlessAborted(familyInstance.emitContract({ contractIR: contractRaw }));
|
|
88
|
+
|
|
89
|
+
// Create directory if needed and write files (both colocated)
|
|
90
|
+
await unlessAborted(mkdir(dirname(outputJsonPath), { recursive: true }));
|
|
91
|
+
await unlessAborted(writeFile(outputJsonPath, emitResult.contractJson, 'utf-8'));
|
|
92
|
+
await unlessAborted(writeFile(outputDtsPath, emitResult.contractDts, 'utf-8'));
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
storageHash: emitResult.storageHash,
|
|
96
|
+
...ifDefined('executionHash', emitResult.executionHash),
|
|
97
|
+
profileHash: emitResult.profileHash,
|
|
98
|
+
files: {
|
|
99
|
+
json: outputJsonPath,
|
|
100
|
+
dts: outputDtsPath,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|