@prisma-next/cli 0.3.0-pr.87.2 → 0.3.0-pr.88.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.
Files changed (43) hide show
  1. package/dist/{chunk-74IELXRA.js → chunk-4GX6MW6J.js} +265 -19
  2. package/dist/chunk-4GX6MW6J.js.map +1 -0
  3. package/dist/chunk-BO73VO4I.js +45 -0
  4. package/dist/chunk-BO73VO4I.js.map +1 -0
  5. package/dist/{chunk-U6QI3AZ3.js → chunk-HLEJGWAP.js} +44 -5
  6. package/dist/chunk-HLEJGWAP.js.map +1 -0
  7. package/dist/{chunk-VI2YETW7.js → chunk-MPSJAVF6.js} +4 -2
  8. package/dist/cli.js +694 -522
  9. package/dist/cli.js.map +1 -1
  10. package/dist/commands/contract-emit.js +2 -3
  11. package/dist/commands/db-init.js +5 -46
  12. package/dist/commands/db-init.js.map +1 -1
  13. package/dist/commands/db-introspect.d.ts.map +1 -1
  14. package/dist/commands/db-introspect.js +128 -133
  15. package/dist/commands/db-introspect.js.map +1 -1
  16. package/dist/commands/db-schema-verify.d.ts.map +1 -1
  17. package/dist/commands/db-schema-verify.js +119 -107
  18. package/dist/commands/db-schema-verify.js.map +1 -1
  19. package/dist/commands/db-sign.d.ts.map +1 -1
  20. package/dist/commands/db-sign.js +128 -125
  21. package/dist/commands/db-sign.js.map +1 -1
  22. package/dist/commands/db-verify.d.ts.map +1 -1
  23. package/dist/commands/db-verify.js +141 -116
  24. package/dist/commands/db-verify.js.map +1 -1
  25. package/dist/control-api/types.d.ts +24 -0
  26. package/dist/control-api/types.d.ts.map +1 -1
  27. package/dist/exports/control-api.js +2 -3
  28. package/dist/exports/index.js +2 -3
  29. package/dist/exports/index.js.map +1 -1
  30. package/package.json +10 -10
  31. package/src/commands/db-introspect.ts +181 -177
  32. package/src/commands/db-schema-verify.ts +150 -143
  33. package/src/commands/db-sign.ts +172 -164
  34. package/src/commands/db-verify.ts +179 -149
  35. package/src/control-api/client.ts +246 -22
  36. package/src/control-api/types.ts +24 -0
  37. package/dist/chunk-5MPKZYVI.js +0 -47
  38. package/dist/chunk-5MPKZYVI.js.map +0 -1
  39. package/dist/chunk-6EPKRATC.js +0 -91
  40. package/dist/chunk-6EPKRATC.js.map +0 -1
  41. package/dist/chunk-74IELXRA.js.map +0 -1
  42. package/dist/chunk-U6QI3AZ3.js.map +0 -1
  43. /package/dist/{chunk-VI2YETW7.js.map → chunk-MPSJAVF6.js.map} +0 -0
@@ -1,28 +1,28 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { relative, resolve } from 'node:path';
3
- import {
4
- errorDatabaseConnectionRequired,
5
- errorDriverRequired,
6
- errorRuntime,
7
- errorUnexpected,
8
- } from '@prisma-next/core-control-plane/errors';
9
3
  import type { CoreSchemaView } from '@prisma-next/core-control-plane/schema-view';
10
4
  import type { IntrospectSchemaResult } from '@prisma-next/core-control-plane/types';
11
- import { createControlPlaneStack } from '@prisma-next/core-control-plane/types';
5
+ import { notOk, ok, type Result } from '@prisma-next/utils/result';
12
6
  import { Command } from 'commander';
13
7
  import { loadConfig } from '../config-loader';
14
- import { performAction } from '../utils/action';
8
+ import { createControlClient } from '../control-api/client';
9
+ import {
10
+ CliStructuredError,
11
+ errorDatabaseConnectionRequired,
12
+ errorDriverRequired,
13
+ errorJsonFormatNotSupported,
14
+ errorUnexpected,
15
+ } from '../utils/cli-errors';
15
16
  import { setCommandDescriptions } from '../utils/command-helpers';
16
- import { assertContractRequirementsSatisfied } from '../utils/framework-components';
17
- import { parseGlobalFlags } from '../utils/global-flags';
17
+ import { type GlobalFlags, parseGlobalFlags } from '../utils/global-flags';
18
18
  import {
19
19
  formatCommandHelp,
20
20
  formatIntrospectJson,
21
21
  formatIntrospectOutput,
22
22
  formatStyledHeader,
23
23
  } from '../utils/output';
24
+ import { createProgressAdapter } from '../utils/progress-adapter';
24
25
  import { handleResult } from '../utils/result-handler';
25
- import { withSpinner } from '../utils/spinner';
26
26
 
27
27
  interface DbIntrospectOptions {
28
28
  readonly db?: string;
@@ -39,6 +39,162 @@ interface DbIntrospectOptions {
39
39
  readonly 'no-color'?: boolean;
40
40
  }
41
41
 
42
+ interface DbIntrospectCommandResult {
43
+ readonly introspectResult: IntrospectSchemaResult<unknown>;
44
+ readonly schemaView: CoreSchemaView | undefined;
45
+ }
46
+
47
+ /**
48
+ * Executes the db introspect command and returns a structured Result.
49
+ */
50
+ async function executeDbIntrospectCommand(
51
+ options: DbIntrospectOptions,
52
+ flags: GlobalFlags,
53
+ startTime: number,
54
+ ): Promise<Result<DbIntrospectCommandResult, CliStructuredError>> {
55
+ // Load config
56
+ const config = await loadConfig(options.config);
57
+ const configPath = options.config
58
+ ? relative(process.cwd(), resolve(options.config))
59
+ : 'prisma-next.config.ts';
60
+
61
+ // Optionally load contract if contract config exists
62
+ let contractIR: unknown | undefined;
63
+ if (config.contract?.output) {
64
+ const contractPath = resolve(config.contract.output);
65
+ try {
66
+ const contractJsonContent = await readFile(contractPath, 'utf-8');
67
+ contractIR = JSON.parse(contractJsonContent);
68
+ } catch (error) {
69
+ // Contract file is optional for introspection - don't fail if it doesn't exist
70
+ if (error instanceof Error && (error as { code?: string }).code !== 'ENOENT') {
71
+ return notOk(
72
+ errorUnexpected(error.message, {
73
+ why: `Failed to read contract file: ${error.message}`,
74
+ }),
75
+ );
76
+ }
77
+ }
78
+ }
79
+
80
+ // Output header
81
+ if (flags.json !== 'object' && !flags.quiet) {
82
+ const details: Array<{ label: string; value: string }> = [
83
+ { label: 'config', value: configPath },
84
+ ];
85
+ if (options.db) {
86
+ // Mask password in URL for security
87
+ const maskedUrl = options.db.replace(/:([^:@]+)@/, ':****@');
88
+ details.push({ label: 'database', value: maskedUrl });
89
+ } else if (config.db?.connection && typeof config.db.connection === 'string') {
90
+ // Mask password in URL for security
91
+ const maskedUrl = config.db.connection.replace(/:([^:@]+)@/, ':****@');
92
+ details.push({ label: 'database', value: maskedUrl });
93
+ }
94
+ const header = formatStyledHeader({
95
+ command: 'db introspect',
96
+ description: 'Inspect the database schema',
97
+ url: 'https://pris.ly/db-introspect',
98
+ details,
99
+ flags,
100
+ });
101
+ console.log(header);
102
+ }
103
+
104
+ // Resolve database connection (--db flag or config.db.connection)
105
+ const dbConnection = options.db ?? config.db?.connection;
106
+ if (!dbConnection) {
107
+ return notOk(
108
+ errorDatabaseConnectionRequired({
109
+ why: `Database connection is required for db introspect (set db.connection in ${configPath}, or pass --db <url>)`,
110
+ }),
111
+ );
112
+ }
113
+
114
+ // Check for driver
115
+ if (!config.driver) {
116
+ return notOk(errorDriverRequired({ why: 'Config.driver is required for db introspect' }));
117
+ }
118
+
119
+ // Create control client
120
+ const client = createControlClient({
121
+ family: config.family,
122
+ target: config.target,
123
+ adapter: config.adapter,
124
+ driver: config.driver,
125
+ extensionPacks: config.extensionPacks ?? [],
126
+ });
127
+
128
+ // Create progress adapter
129
+ const onProgress = createProgressAdapter({ flags });
130
+
131
+ try {
132
+ // Introspect with connection and progress
133
+ const schemaIR = await client.introspect({
134
+ connection: dbConnection,
135
+ onProgress,
136
+ });
137
+
138
+ // Add blank line after all async operations if spinners were shown
139
+ if (!flags.quiet && flags.json !== 'object' && process.stdout.isTTY) {
140
+ console.log('');
141
+ }
142
+
143
+ // Optionally call toSchemaView if available
144
+ // We need to access the family instance for toSchemaView
145
+ // Since ControlClient doesn't expose this, we access it through init
146
+ client.init();
147
+
148
+ let schemaView: CoreSchemaView | undefined;
149
+ // The ControlClient doesn't expose toSchemaView, so we skip it for now
150
+ // In the future, the introspect method could return the schema view
151
+ void contractIR; // Mark as used
152
+
153
+ const totalTime = Date.now() - startTime;
154
+
155
+ // Get masked connection URL for meta (only for string connections)
156
+ const connectionForMeta =
157
+ typeof dbConnection === 'string' ? dbConnection.replace(/:([^:@]+)@/, ':****@') : undefined;
158
+
159
+ const introspectResult: IntrospectSchemaResult<unknown> = {
160
+ ok: true,
161
+ summary: 'Schema introspected successfully',
162
+ target: {
163
+ familyId: config.family.familyId,
164
+ id: config.target.targetId,
165
+ },
166
+ schema: schemaIR,
167
+ ...(configPath || connectionForMeta
168
+ ? {
169
+ meta: {
170
+ ...(configPath ? { configPath } : {}),
171
+ ...(connectionForMeta ? { dbUrl: connectionForMeta } : {}),
172
+ },
173
+ }
174
+ : {}),
175
+ timings: {
176
+ total: totalTime,
177
+ },
178
+ };
179
+
180
+ return ok({ introspectResult, schemaView });
181
+ } catch (error) {
182
+ // Driver already throws CliStructuredError for connection failures
183
+ if (error instanceof CliStructuredError) {
184
+ return notOk(error);
185
+ }
186
+
187
+ // Wrap unexpected errors
188
+ return notOk(
189
+ errorUnexpected(error instanceof Error ? error.message : String(error), {
190
+ why: `Unexpected error during db introspect: ${error instanceof Error ? error.message : String(error)}`,
191
+ }),
192
+ );
193
+ } finally {
194
+ await client.close();
195
+ }
196
+ }
197
+
42
198
  export function createDbIntrospectCommand(): Command {
43
199
  const command = new Command('introspect');
44
200
  setCommandDescriptions(
@@ -57,7 +213,7 @@ export function createDbIntrospectCommand(): Command {
57
213
  })
58
214
  .option('--db <url>', 'Database connection string')
59
215
  .option('--config <path>', 'Path to prisma-next.config.ts')
60
- .option('--json [format]', 'Output as JSON (object or ndjson)', false)
216
+ .option('--json [format]', 'Output as JSON (object)', false)
61
217
  .option('-q, --quiet', 'Quiet mode: errors only')
62
218
  .option('-v, --verbose', 'Verbose output: debug info, timings')
63
219
  .option('-vv, --trace', 'Trace output: deep internals, stack traces')
@@ -66,181 +222,29 @@ export function createDbIntrospectCommand(): Command {
66
222
  .option('--no-color', 'Disable color output')
67
223
  .action(async (options: DbIntrospectOptions) => {
68
224
  const flags = parseGlobalFlags(options);
225
+ const startTime = Date.now();
69
226
 
70
- const result = await performAction(async () => {
71
- const startTime = Date.now();
72
-
73
- // Load config (file I/O)
74
- const config = await loadConfig(options.config);
75
- // Normalize config path for display (match contract path format - no ./ prefix)
76
- const configPath = options.config
77
- ? relative(process.cwd(), resolve(options.config))
78
- : 'prisma-next.config.ts';
79
-
80
- // Optionally load contract if contract config exists
81
- let contractIR: unknown | undefined;
82
- if (config.contract?.output) {
83
- const contractPath = resolve(config.contract.output);
84
- try {
85
- const contractJsonContent = await readFile(contractPath, 'utf-8');
86
- contractIR = JSON.parse(contractJsonContent);
87
- } catch (error) {
88
- // Contract file is optional for introspection - don't fail if it doesn't exist
89
- if (error instanceof Error && (error as { code?: string }).code !== 'ENOENT') {
90
- throw errorUnexpected(error.message, {
91
- why: `Failed to read contract file: ${error.message}`,
92
- });
93
- }
94
- }
95
- }
96
-
97
- // Output header (only for human-readable output)
98
- if (flags.json !== 'object' && !flags.quiet) {
99
- const details: Array<{ label: string; value: string }> = [
100
- { label: 'config', value: configPath },
101
- ];
102
- if (options.db) {
103
- // Mask password in URL for security
104
- const maskedUrl = options.db.replace(/:([^:@]+)@/, ':****@');
105
- details.push({ label: 'database', value: maskedUrl });
106
- } else if (config.db?.connection && typeof config.db.connection === 'string') {
107
- // Mask password in URL for security
108
- const maskedUrl = config.db.connection.replace(/:([^:@]+)@/, ':****@');
109
- details.push({ label: 'database', value: maskedUrl });
110
- }
111
- const header = formatStyledHeader({
227
+ // Validate JSON format option
228
+ if (flags.json === 'ndjson') {
229
+ const result = notOk(
230
+ errorJsonFormatNotSupported({
112
231
  command: 'db introspect',
113
- description: 'Inspect the database schema',
114
- url: 'https://pris.ly/db-introspect',
115
- details,
116
- flags,
117
- });
118
- console.log(header);
119
- }
120
-
121
- // Resolve database connection (--db flag or config.db.connection)
122
- const dbConnection = options.db ?? config.db?.connection;
123
- if (!dbConnection) {
124
- throw errorDatabaseConnectionRequired();
125
- }
126
-
127
- // Check for driver
128
- if (!config.driver) {
129
- throw errorDriverRequired();
130
- }
232
+ format: 'ndjson',
233
+ supportedFormats: ['object'],
234
+ }),
235
+ );
236
+ const exitCode = handleResult(result, flags);
237
+ process.exit(exitCode);
238
+ }
131
239
 
132
- // Store driver descriptor after null check
133
- const driverDescriptor = config.driver;
134
-
135
- const driver = await withSpinner(() => driverDescriptor.create(dbConnection), {
136
- message: 'Connecting to database...',
137
- flags,
138
- });
139
-
140
- try {
141
- // Create family instance
142
- const stack = createControlPlaneStack({
143
- target: config.target,
144
- adapter: config.adapter,
145
- driver: driverDescriptor,
146
- extensionPacks: config.extensionPacks,
147
- });
148
- const familyInstance = config.family.create(stack);
149
-
150
- // Validate contract IR if we loaded it
151
- if (contractIR) {
152
- const validatedContract = familyInstance.validateContractIR(contractIR);
153
- assertContractRequirementsSatisfied({ contract: validatedContract, stack });
154
- contractIR = validatedContract;
155
- }
156
-
157
- // Call family instance introspect method
158
- let schemaIR: unknown;
159
- try {
160
- schemaIR = await withSpinner(
161
- () =>
162
- familyInstance.introspect({
163
- driver,
164
- contractIR,
165
- }),
166
- {
167
- message: 'Introspecting database schema...',
168
- flags,
169
- },
170
- );
171
- } catch (error) {
172
- // Wrap errors from introspect() in structured error
173
- throw errorRuntime(error instanceof Error ? error.message : String(error), {
174
- why: `Failed to introspect database: ${error instanceof Error ? error.message : String(error)}`,
175
- });
176
- }
177
-
178
- // Optionally call toSchemaView if available
179
- let schemaView: CoreSchemaView | undefined;
180
- if (familyInstance.toSchemaView) {
181
- try {
182
- schemaView = familyInstance.toSchemaView(schemaIR);
183
- } catch (error) {
184
- // Schema view projection is optional - log but don't fail
185
- if (flags.verbose) {
186
- console.error(
187
- `Warning: Failed to project schema to view: ${error instanceof Error ? error.message : String(error)}`,
188
- );
189
- }
190
- }
191
- }
192
-
193
- const totalTime = Date.now() - startTime;
194
-
195
- // Add blank line after all async operations if spinners were shown
196
- if (!flags.quiet && flags.json !== 'object' && process.stdout.isTTY) {
197
- console.log('');
198
- }
199
-
200
- // Build result envelope
201
- // Get masked connection URL for meta (only for string connections)
202
- const connectionForMeta =
203
- typeof dbConnection === 'string'
204
- ? dbConnection.replace(/:([^:@]+)@/, ':****@')
205
- : undefined;
206
-
207
- const introspectResult: IntrospectSchemaResult<unknown> = {
208
- ok: true,
209
- summary: 'Schema introspected successfully',
210
- target: {
211
- familyId: config.family.familyId,
212
- id: config.target.targetId,
213
- },
214
- schema: schemaIR,
215
- ...(configPath || connectionForMeta
216
- ? {
217
- meta: {
218
- ...(configPath ? { configPath } : {}),
219
- ...(connectionForMeta ? { dbUrl: connectionForMeta } : {}),
220
- },
221
- }
222
- : {}),
223
- timings: {
224
- total: totalTime,
225
- },
226
- };
227
-
228
- return { introspectResult, schemaView };
229
- } finally {
230
- // Ensure driver connection is closed
231
- await driver.close();
232
- }
233
- });
240
+ const result = await executeDbIntrospectCommand(options, flags, startTime);
234
241
 
235
242
  // Handle result - formats output and returns exit code
236
243
  const exitCode = handleResult(result, flags, (value) => {
237
244
  const { introspectResult, schemaView } = value;
238
- // Output based on flags
239
245
  if (flags.json === 'object') {
240
- // JSON output to stdout
241
246
  console.log(formatIntrospectJson(introspectResult));
242
247
  } else {
243
- // Human-readable output to stdout
244
248
  const output = formatIntrospectOutput(introspectResult, schemaView, flags);
245
249
  if (output) {
246
250
  console.log(output);