@prisma-next/cli 0.14.0 → 0.15.0-dev.10
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 +2 -2
- package/dist/cli.mjs +33 -14
- package/dist/cli.mjs.map +1 -1
- package/dist/{client-CJzuo5wX.mjs → client-DwOyZz0a.mjs} +141 -123
- package/dist/client-DwOyZz0a.mjs.map +1 -0
- package/dist/{command-helpers-DGMvGBeX.mjs → command-helpers-CNwURRf9.mjs} +30 -11
- package/dist/command-helpers-CNwURRf9.mjs.map +1 -0
- package/dist/commands/contract-emit.mjs +1 -1
- package/dist/commands/contract-infer.mjs +1 -1
- package/dist/commands/db-init.mjs +3 -3
- package/dist/commands/db-schema.mjs +3 -3
- package/dist/commands/db-sign.mjs +5 -5
- package/dist/commands/db-sign.mjs.map +1 -1
- package/dist/commands/db-update.mjs +4 -4
- package/dist/commands/db-verify.d.mts.map +1 -1
- package/dist/commands/db-verify.mjs +1 -1
- package/dist/commands/migrate.d.mts +1 -1
- package/dist/commands/migrate.d.mts.map +1 -1
- package/dist/commands/migrate.mjs +37 -38
- package/dist/commands/migrate.mjs.map +1 -1
- package/dist/commands/migration-check.d.mts +1 -1
- package/dist/commands/migration-check.mjs +1 -1
- package/dist/commands/migration-graph.d.mts +1 -1
- package/dist/commands/migration-graph.mjs +9 -9
- package/dist/commands/migration-graph.mjs.map +1 -1
- package/dist/commands/migration-list.d.mts +6 -6
- package/dist/commands/migration-list.d.mts.map +1 -1
- package/dist/commands/migration-list.mjs +1 -1
- package/dist/commands/migration-log.d.mts +1 -1
- package/dist/commands/migration-log.mjs +1 -1
- package/dist/commands/migration-new.mjs +4 -4
- package/dist/commands/migration-new.mjs.map +1 -1
- package/dist/commands/migration-plan.d.mts.map +1 -1
- package/dist/commands/migration-plan.mjs +1 -1
- package/dist/commands/migration-show.mjs +4 -4
- package/dist/commands/migration-show.mjs.map +1 -1
- package/dist/commands/migration-status.d.mts +1 -1
- package/dist/commands/migration-status.mjs +1 -1
- package/dist/commands/ref.d.mts +1 -1
- package/dist/commands/ref.mjs +3 -3
- package/dist/commands/ref.mjs.map +1 -1
- package/dist/commands/telemetry/index.mjs +1 -1
- package/dist/{contract-at-errors-CFXsstzm.mjs → contract-at-errors-BHbY04pl.mjs} +2 -2
- package/dist/{contract-at-errors-CFXsstzm.mjs.map → contract-at-errors-BHbY04pl.mjs.map} +1 -1
- package/dist/{contract-emit-C8HmtboH.mjs → contract-emit-BeidkyUC.mjs} +9 -11
- package/dist/contract-emit-BeidkyUC.mjs.map +1 -0
- package/dist/{contract-emit-B_qriF8B.mjs → contract-emit-IQBYv351.mjs} +6 -6
- package/dist/contract-emit-IQBYv351.mjs.map +1 -0
- package/dist/{contract-infer-BYT_ra_U.mjs → contract-infer-B3Wq2cu1.mjs} +3 -3
- package/dist/{contract-infer-BYT_ra_U.mjs.map → contract-infer-B3Wq2cu1.mjs.map} +1 -1
- package/dist/{contract-space-aggregate-loader-ClI1KN6d.mjs → contract-space-aggregate-loader-BirzMmW1.mjs} +63 -5
- package/dist/contract-space-aggregate-loader-BirzMmW1.mjs.map +1 -0
- package/dist/{db-verify-C24FKhb7.mjs → db-verify-DpRrecs7.mjs} +57 -61
- package/dist/db-verify-DpRrecs7.mjs.map +1 -0
- package/dist/exports/control-api.d.mts +3 -3
- package/dist/exports/control-api.d.mts.map +1 -1
- package/dist/exports/control-api.mjs +2 -2
- package/dist/exports/index.d.mts +5 -1
- package/dist/exports/index.d.mts.map +1 -1
- package/dist/exports/index.mjs +4 -3
- package/dist/exports/index.mjs.map +1 -1
- package/dist/format-CaCF63ld.mjs +102 -0
- package/dist/format-CaCF63ld.mjs.map +1 -0
- package/dist/{framework-components-YVQHhPH7.mjs → framework-components-CnBcJDmF.mjs} +2 -2
- package/dist/{framework-components-YVQHhPH7.mjs.map → framework-components-CnBcJDmF.mjs.map} +1 -1
- package/dist/{init-0HwB-Vh8.mjs → init-Boa0LzeJ.mjs} +27 -19
- package/dist/init-Boa0LzeJ.mjs.map +1 -0
- package/dist/{inspect-live-schema-DF6IwcDl.mjs → inspect-live-schema-CldSQjVI.mjs} +4 -4
- package/dist/inspect-live-schema-CldSQjVI.mjs.map +1 -0
- package/dist/{migration-check-VwM8xCZV.mjs → migration-check-CGtYnR9k.mjs} +10 -11
- package/dist/migration-check-CGtYnR9k.mjs.map +1 -0
- package/dist/migration-cli.mjs +1 -1
- package/dist/migration-cli.mjs.map +1 -1
- package/dist/{migration-command-scaffold-DA-Lhx6o.mjs → migration-command-scaffold-B3m-MMAx.mjs} +4 -4
- package/dist/migration-command-scaffold-B3m-MMAx.mjs.map +1 -0
- package/dist/{migration-graph-command-render-CEez7YUK.mjs → migration-graph-command-render-B83xrnNy.mjs} +24 -24
- package/dist/{migration-graph-command-render-CEez7YUK.mjs.map → migration-graph-command-render-B83xrnNy.mjs.map} +1 -1
- package/dist/{migration-list-DlJJ_38Z.mjs → migration-list-DBU6Pneg.mjs} +16 -16
- package/dist/migration-list-DBU6Pneg.mjs.map +1 -0
- package/dist/{migration-log-CG0qQAFm.mjs → migration-log-BYngyvXa.mjs} +5 -5
- package/dist/migration-log-BYngyvXa.mjs.map +1 -0
- package/dist/{migration-path-target-Ce6OZImp.mjs → migration-path-target-DQa4Caoc.mjs} +2 -2
- package/dist/{migration-path-target-Ce6OZImp.mjs.map → migration-path-target-DQa4Caoc.mjs.map} +1 -1
- package/dist/{migration-plan-W_E8FQOk.mjs → migration-plan-D6bHpCzE.mjs} +30 -30
- package/dist/migration-plan-D6bHpCzE.mjs.map +1 -0
- package/dist/{migration-status-CD-LC2Ip.mjs → migration-status-CpQ5UhcU.mjs} +14 -14
- package/dist/migration-status-CpQ5UhcU.mjs.map +1 -0
- package/dist/{telemetry-BIM4beEO.mjs → telemetry-BQJ_XHa9.mjs} +2 -2
- package/dist/{telemetry-BIM4beEO.mjs.map → telemetry-BQJ_XHa9.mjs.map} +1 -1
- package/dist/{types-C_tYiJYx.d.mts → types-DFoqNIcQ.d.mts} +27 -21
- package/dist/types-DFoqNIcQ.d.mts.map +1 -0
- package/dist/{verify-DcOYZ1tH.mjs → verify-CInGyt4U.mjs} +58 -159
- package/dist/verify-CInGyt4U.mjs.map +1 -0
- package/package.json +28 -30
- package/src/cli.ts +7 -0
- package/src/commands/contract-emit.ts +1 -1
- package/src/commands/db-sign.ts +1 -1
- package/src/commands/db-verify.ts +34 -20
- package/src/commands/format.ts +75 -0
- package/src/commands/init/detect-package-manager.ts +8 -4
- package/src/commands/init/hygiene-package-scripts.ts +16 -3
- package/src/commands/inspect-live-schema.ts +1 -1
- package/src/commands/lsp.ts +32 -0
- package/src/commands/migrate.ts +47 -44
- package/src/commands/migration-check.ts +6 -6
- package/src/commands/migration-graph.ts +5 -5
- package/src/commands/migration-list.ts +13 -13
- package/src/commands/migration-log.ts +1 -1
- package/src/commands/migration-new.ts +1 -1
- package/src/commands/migration-plan.ts +15 -4
- package/src/commands/migration-show.ts +1 -1
- package/src/commands/migration-status.ts +10 -10
- package/src/commands/ref.ts +1 -1
- package/src/control-api/client.ts +8 -23
- package/src/control-api/operations/contract-emit.ts +9 -27
- package/src/control-api/operations/db-init.ts +2 -2
- package/src/control-api/operations/db-run.ts +18 -17
- package/src/control-api/operations/db-verify.ts +52 -44
- package/src/control-api/operations/format.ts +98 -0
- package/src/control-api/operations/migrate.ts +139 -95
- package/src/control-api/operations/run-migration.ts +6 -5
- package/src/control-api/types.ts +8 -8
- package/src/exports/index.ts +1 -0
- package/src/migration-cli.ts +1 -1
- package/src/utils/cli-errors.ts +14 -1
- package/src/utils/combine-verify-results.ts +94 -0
- package/src/utils/extension-pack-inputs.ts +1 -1
- package/src/utils/formatters/errors.ts +34 -5
- package/src/utils/formatters/verify.ts +89 -227
- package/src/utils/migration-command-scaffold.ts +1 -1
- package/src/utils/plan-resolution.ts +18 -18
- package/dist/client-CJzuo5wX.mjs.map +0 -1
- package/dist/command-helpers-DGMvGBeX.mjs.map +0 -1
- package/dist/config-loader-p9JMrekQ.mjs +0 -88
- package/dist/config-loader-p9JMrekQ.mjs.map +0 -1
- package/dist/config-loader.d.mts +0 -16
- package/dist/config-loader.d.mts.map +0 -1
- package/dist/config-loader.mjs +0 -2
- package/dist/contract-emit-B_qriF8B.mjs.map +0 -1
- package/dist/contract-emit-C8HmtboH.mjs.map +0 -1
- package/dist/contract-space-aggregate-loader-ClI1KN6d.mjs.map +0 -1
- package/dist/db-verify-C24FKhb7.mjs.map +0 -1
- package/dist/extension-pack-inputs-1ySHqxKG.mjs +0 -62
- package/dist/extension-pack-inputs-1ySHqxKG.mjs.map +0 -1
- package/dist/init-0HwB-Vh8.mjs.map +0 -1
- package/dist/inspect-live-schema-DF6IwcDl.mjs.map +0 -1
- package/dist/migration-check-VwM8xCZV.mjs.map +0 -1
- package/dist/migration-command-scaffold-DA-Lhx6o.mjs.map +0 -1
- package/dist/migration-list-DlJJ_38Z.mjs.map +0 -1
- package/dist/migration-log-CG0qQAFm.mjs.map +0 -1
- package/dist/migration-plan-W_E8FQOk.mjs.map +0 -1
- package/dist/migration-status-CD-LC2Ip.mjs.map +0 -1
- package/dist/types-C_tYiJYx.d.mts.map +0 -1
- package/dist/verify-DcOYZ1tH.mjs.map +0 -1
- package/src/config-loader.ts +0 -92
- package/src/config-path-validation.ts +0 -74
- package/src/utils/combine-schema-results.ts +0 -84
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { VerifyDatabaseSchemaResult } from '@prisma-next/framework-components/control';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The combined per-space contract-satisfaction result plus the standalone
|
|
5
|
+
* unclaimed-elements list, reported once. The CLI renders
|
|
6
|
+
* both; `unclaimed` is never folded into the combined tree or its issues.
|
|
7
|
+
*/
|
|
8
|
+
export interface CombinedVerifyResult {
|
|
9
|
+
readonly result: VerifyDatabaseSchemaResult;
|
|
10
|
+
readonly unclaimed: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Collapse the aggregate verifier's per-space contract-satisfaction results
|
|
15
|
+
* into a single {@link VerifyDatabaseSchemaResult} for the CLI display
|
|
16
|
+
* surface, and carry the deduplicated unclaimed-elements list alongside it.
|
|
17
|
+
* Concatenates the issue lists across spaces; uses the app space's result as
|
|
18
|
+
* the structural envelope (storage hash, target). Extras are already
|
|
19
|
+
* stripped from each per-space result, so nothing here duplicates an
|
|
20
|
+
* unclaimed element per space.
|
|
21
|
+
*
|
|
22
|
+
* **Unclaimed disposition.** In strict mode a non-empty `unclaimed` list fails
|
|
23
|
+
* the combined verdict (`ok: false`); in lenient mode it is carried for
|
|
24
|
+
* informational rendering only. The list itself is returned unchanged for the
|
|
25
|
+
* renderer.
|
|
26
|
+
*
|
|
27
|
+
* **Summary policy.** Preserve the per-family phrasing whenever the
|
|
28
|
+
* combined `ok` flag agrees with the app space's `ok` flag — this is
|
|
29
|
+
* the common case (single-family deployments, single-app deployments)
|
|
30
|
+
* and the family's "satisfies / does not satisfy contract" phrasing
|
|
31
|
+
* stays user-visible. When the app passes but an extension fails (or
|
|
32
|
+
* vice versa) the app's summary contradicts the envelope, so fall back
|
|
33
|
+
* to the first failing space's summary. This keeps family phrasing
|
|
34
|
+
* intact and the envelope internally consistent (`ok: false` ↔ failure
|
|
35
|
+
* summary).
|
|
36
|
+
*/
|
|
37
|
+
export function combineVerifyResults(
|
|
38
|
+
perSpace: ReadonlyMap<string, VerifyDatabaseSchemaResult>,
|
|
39
|
+
appSpaceId: string,
|
|
40
|
+
strict: boolean,
|
|
41
|
+
unclaimed: readonly string[],
|
|
42
|
+
): CombinedVerifyResult {
|
|
43
|
+
const appResult = perSpace.get(appSpaceId) ?? perSpace.values().next().value;
|
|
44
|
+
if (appResult === undefined) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
'Aggregate verifier returned no per-space verify results — this is a wiring bug.',
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let okAll = true;
|
|
51
|
+
let firstFailure: VerifyDatabaseSchemaResult | undefined;
|
|
52
|
+
let issues: VerifyDatabaseSchemaResult['schema']['issues'] = [];
|
|
53
|
+
let warningIssues: VerifyDatabaseSchemaResult['schema']['issues'] = [];
|
|
54
|
+
for (const [, result] of perSpace) {
|
|
55
|
+
if (!result.ok) {
|
|
56
|
+
okAll = false;
|
|
57
|
+
if (firstFailure === undefined) firstFailure = result;
|
|
58
|
+
}
|
|
59
|
+
issues = [...issues, ...result.schema.issues];
|
|
60
|
+
warningIssues = [...warningIssues, ...(result.schema.warnings?.issues ?? [])];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const unclaimedFails = strict && unclaimed.length > 0;
|
|
64
|
+
const ok = okAll && !unclaimedFails;
|
|
65
|
+
|
|
66
|
+
// Prefer a failing space's family phrasing; else, when only the unclaimed list
|
|
67
|
+
// fails the verdict, say so; else keep the app space's phrasing. When `okAll`
|
|
68
|
+
// is false the loop assigned `firstFailure`, so the `?? appResult.summary`
|
|
69
|
+
// fallback is unreachable — it exists only to keep the read cast-free.
|
|
70
|
+
const summary = okAll
|
|
71
|
+
? unclaimedFails
|
|
72
|
+
? `Database schema has ${unclaimed.length} unclaimed element${unclaimed.length === 1 ? '' : 's'} (not in any contract)`
|
|
73
|
+
: appResult.summary
|
|
74
|
+
: appResult.ok
|
|
75
|
+
? (firstFailure?.summary ?? appResult.summary)
|
|
76
|
+
: appResult.summary;
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
result: {
|
|
80
|
+
ok,
|
|
81
|
+
...(ok ? {} : { code: appResult.code ?? 'PN-RUN-3010' }),
|
|
82
|
+
summary,
|
|
83
|
+
contract: appResult.contract,
|
|
84
|
+
target: appResult.target,
|
|
85
|
+
schema: {
|
|
86
|
+
issues,
|
|
87
|
+
warnings: { issues: warningIssues },
|
|
88
|
+
},
|
|
89
|
+
meta: { strict },
|
|
90
|
+
timings: { total: 0 },
|
|
91
|
+
},
|
|
92
|
+
unclaimed,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -111,7 +111,7 @@ export function toExtensionInputs(
|
|
|
111
111
|
* {@link import('./contract-space-aggregate-loader').buildContractSpaceAggregate}.
|
|
112
112
|
*
|
|
113
113
|
* Codec-only extensions (no `contractSpace` declaration) are filtered
|
|
114
|
-
* out: they
|
|
114
|
+
* out: they contribute no contract space, so the aggregate loader
|
|
115
115
|
* has nothing to do with them. Filtering happens at this descriptor-
|
|
116
116
|
* import boundary so the loader stays oblivious to that distinction —
|
|
117
117
|
* every entry it sees expects an on-disk `migrations/<id>/` directory.
|
|
@@ -7,6 +7,25 @@ import type { GlobalFlags } from '../global-flags';
|
|
|
7
7
|
import { createColorFormatter, formatDim, isVerbose } from './helpers';
|
|
8
8
|
import { formatPlannerWarningsBlock } from './migrations';
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* The display label for a schema-diff issue in the shared error envelope,
|
|
12
|
+
* derived from which sides are present: expected-only is a missing object,
|
|
13
|
+
* actual-only an extra one, both a mismatch. `undefined` when the entry is not
|
|
14
|
+
* a schema-diff issue (it carries neither side) so the caller can fall through
|
|
15
|
+
* to its generic label.
|
|
16
|
+
*/
|
|
17
|
+
function schemaDiffIssueLabel(issue: {
|
|
18
|
+
readonly expected?: unknown;
|
|
19
|
+
readonly actual?: unknown;
|
|
20
|
+
}): 'missing' | 'extra' | 'mismatch' | undefined {
|
|
21
|
+
const hasExpected = issue.expected !== undefined;
|
|
22
|
+
const hasActual = issue.actual !== undefined;
|
|
23
|
+
if (hasExpected && hasActual) return 'mismatch';
|
|
24
|
+
if (hasExpected) return 'missing';
|
|
25
|
+
if (hasActual) return 'extra';
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
10
29
|
/**
|
|
11
30
|
* Formats error output for human-readable display.
|
|
12
31
|
*/
|
|
@@ -48,9 +67,19 @@ export function formatErrorOutput(error: CliErrorEnvelope, flags: GlobalFlags):
|
|
|
48
67
|
}
|
|
49
68
|
}
|
|
50
69
|
}
|
|
51
|
-
// Show issues list if present (always show a short list; show full list when verbose)
|
|
70
|
+
// Show issues list if present (always show a short list; show full list when verbose).
|
|
71
|
+
// `issues` is a shared error-envelope field: PSL interpretation diagnostics stamp
|
|
72
|
+
// `kind` and `message` (their diagnostic code and prose); schema-diff issues
|
|
73
|
+
// (`SchemaDiffIssue`) carry no `message` and stamp `path` plus the
|
|
74
|
+
// `expected`/`actual` presence a label is derived from.
|
|
52
75
|
if (error.meta?.['issues']) {
|
|
53
|
-
const issues = error.meta['issues'] as readonly {
|
|
76
|
+
const issues = error.meta['issues'] as readonly {
|
|
77
|
+
kind?: string;
|
|
78
|
+
message?: string;
|
|
79
|
+
path?: readonly string[];
|
|
80
|
+
expected?: unknown;
|
|
81
|
+
actual?: unknown;
|
|
82
|
+
}[];
|
|
54
83
|
if (issues.length > 0) {
|
|
55
84
|
const maxToShow = isVerbose(flags, 1) ? issues.length : Math.min(3, issues.length);
|
|
56
85
|
const header = isVerbose(flags, 1)
|
|
@@ -58,9 +87,9 @@ export function formatErrorOutput(error: CliErrorEnvelope, flags: GlobalFlags):
|
|
|
58
87
|
: ` Issues (showing ${maxToShow} of ${issues.length}):`;
|
|
59
88
|
lines.push(`${formatDimText(header)}`);
|
|
60
89
|
for (const issue of issues.slice(0, maxToShow)) {
|
|
61
|
-
const
|
|
62
|
-
const message = issue.message ?? '';
|
|
63
|
-
lines.push(`${formatDimText(` - [${
|
|
90
|
+
const label = issue.kind ?? schemaDiffIssueLabel(issue) ?? 'issue';
|
|
91
|
+
const message = issue.message ?? issue.path?.join('/') ?? '';
|
|
92
|
+
lines.push(`${formatDimText(` - [${label}] ${message}`)}`);
|
|
64
93
|
}
|
|
65
94
|
if (!isVerbose(flags, 1) && issues.length > maxToShow) {
|
|
66
95
|
lines.push(`${formatDimText(' Re-run with -v/--verbose to see all issues')}`);
|
|
@@ -1,17 +1,36 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
CoreSchemaView,
|
|
3
|
+
ExpectationFailureReason,
|
|
3
4
|
IntrospectSchemaResult,
|
|
5
|
+
SchemaDiffIssue,
|
|
4
6
|
SchemaTreeNode,
|
|
5
|
-
SchemaVerificationNode,
|
|
6
7
|
SignDatabaseResult,
|
|
7
8
|
VerifyDatabaseResult,
|
|
8
9
|
VerifyDatabaseSchemaResult,
|
|
9
10
|
} from '@prisma-next/framework-components/control';
|
|
11
|
+
import { issueOutcome } from '@prisma-next/framework-components/control';
|
|
10
12
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
11
13
|
import { bold, cyan, dim, green, magenta, red, yellow } from 'colorette';
|
|
12
14
|
import type { GlobalFlags } from '../global-flags';
|
|
13
15
|
import { createColorFormatter, formatDim, isVerbose } from './helpers';
|
|
14
16
|
|
|
17
|
+
/** Human-readable label for each outcome, prefixed onto an issue's message for display. */
|
|
18
|
+
const OUTCOME_LABEL: Record<ExpectationFailureReason, string> = {
|
|
19
|
+
'not-found': 'missing',
|
|
20
|
+
'not-expected': 'extra',
|
|
21
|
+
'not-equal': 'mismatch',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The issue's display text: its own path, prefixed with a human label for
|
|
26
|
+
* why it's flagged. Turning the presence-derived outcome (and the path) into
|
|
27
|
+
* prose is this formatter's job, not the differ's — the differ's issue is
|
|
28
|
+
* data (`path` + nodes), not prose.
|
|
29
|
+
*/
|
|
30
|
+
function formatIssueMessage(issue: SchemaDiffIssue): string {
|
|
31
|
+
return `${OUTCOME_LABEL[issueOutcome(issue)]}: ${issue.path.join('/')}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
15
34
|
// ============================================================================
|
|
16
35
|
// Verify Output Formatters
|
|
17
36
|
// ============================================================================
|
|
@@ -27,9 +46,20 @@ export interface DbVerifyCommandSuccessResult {
|
|
|
27
46
|
readonly codecCoverageSkipped?: VerifyDatabaseResult['codecCoverageSkipped'];
|
|
28
47
|
readonly schema?: {
|
|
29
48
|
readonly summary: string;
|
|
30
|
-
readonly counts: VerifyDatabaseSchemaResult['schema']['counts'];
|
|
31
49
|
readonly strict: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Warn-graded finding messages (observed-policy drift). Informational —
|
|
52
|
+
* present on a passing verify; the full-mode result summarizes them as a
|
|
53
|
+
* flat message list.
|
|
54
|
+
*/
|
|
55
|
+
readonly warnings?: readonly string[];
|
|
32
56
|
};
|
|
57
|
+
/**
|
|
58
|
+
* Live element names no contract space declares. In full success this is
|
|
59
|
+
* only ever non-empty in lenient mode — strict mode fails on it — and is
|
|
60
|
+
* rendered informationally.
|
|
61
|
+
*/
|
|
62
|
+
readonly unclaimed?: readonly string[];
|
|
33
63
|
readonly warning?: string;
|
|
34
64
|
readonly meta?:
|
|
35
65
|
| (NonNullable<VerifyDatabaseResult['meta']> & {
|
|
@@ -71,11 +101,22 @@ export function formatVerifyOutput(
|
|
|
71
101
|
if (result.contract.profileHash) {
|
|
72
102
|
lines.push(`${formatDimText(` profileHash: ${result.contract.profileHash}`)}`);
|
|
73
103
|
}
|
|
74
|
-
if (result.
|
|
75
|
-
lines.push(
|
|
76
|
-
|
|
77
|
-
)
|
|
104
|
+
if (result.schema?.warnings && result.schema.warnings.length > 0) {
|
|
105
|
+
lines.push('');
|
|
106
|
+
lines.push(formatYellow('Schema warnings:'));
|
|
107
|
+
for (const message of result.schema.warnings) {
|
|
108
|
+
lines.push(` ${formatYellow('⚠')} ${message}`);
|
|
109
|
+
}
|
|
78
110
|
}
|
|
111
|
+
|
|
112
|
+
if (result.unclaimed && result.unclaimed.length > 0) {
|
|
113
|
+
lines.push('');
|
|
114
|
+
lines.push(formatYellow('Unclaimed elements (declared by no contract):'));
|
|
115
|
+
for (const name of result.unclaimed) {
|
|
116
|
+
lines.push(` ${formatYellow('⚠')} ${name}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
79
120
|
if (result.warning) {
|
|
80
121
|
lines.push('');
|
|
81
122
|
lines.push(`${formatYellow('⚠')} ${result.warning}`);
|
|
@@ -107,6 +148,7 @@ export function formatVerifyJson(result: DbVerifyCommandSuccessResult): string {
|
|
|
107
148
|
...ifDefined('missingCodecs', result.missingCodecs),
|
|
108
149
|
...ifDefined('codecCoverageSkipped', result.codecCoverageSkipped),
|
|
109
150
|
...ifDefined('schema', result.schema),
|
|
151
|
+
unclaimed: result.unclaimed ?? [],
|
|
110
152
|
...ifDefined('warning', result.warning),
|
|
111
153
|
...ifDefined('meta', result.meta),
|
|
112
154
|
timings: result.timings,
|
|
@@ -124,7 +166,7 @@ export function formatIntrospectJson(result: IntrospectSchemaResult<unknown>): s
|
|
|
124
166
|
|
|
125
167
|
/**
|
|
126
168
|
* Renders a schema tree structure from CoreSchemaView.
|
|
127
|
-
*
|
|
169
|
+
* Status-glyph tree styling shared with the retired verification-tree renderer.
|
|
128
170
|
*/
|
|
129
171
|
function renderSchemaTree(
|
|
130
172
|
node: SchemaTreeNode,
|
|
@@ -310,213 +352,13 @@ export function formatIntrospectOutput(
|
|
|
310
352
|
return lines.join('\n');
|
|
311
353
|
}
|
|
312
354
|
|
|
313
|
-
/**
|
|
314
|
-
* Renders a schema verification tree structure from SchemaVerificationNode.
|
|
315
|
-
* Similar to renderSchemaTree but for verification nodes with status-based colors and glyphs.
|
|
316
|
-
*/
|
|
317
|
-
function renderSchemaVerificationTree(
|
|
318
|
-
node: SchemaVerificationNode,
|
|
319
|
-
flags: GlobalFlags,
|
|
320
|
-
options: {
|
|
321
|
-
readonly isLast: boolean;
|
|
322
|
-
readonly prefix: string;
|
|
323
|
-
readonly useColor: boolean;
|
|
324
|
-
readonly formatDimText: (text: string) => string;
|
|
325
|
-
readonly isRoot?: boolean;
|
|
326
|
-
},
|
|
327
|
-
): string[] {
|
|
328
|
-
const { isLast, prefix, useColor, formatDimText, isRoot = false } = options;
|
|
329
|
-
const lines: string[] = [];
|
|
330
|
-
|
|
331
|
-
// Format status glyph and color based on status
|
|
332
|
-
let statusGlyph = '';
|
|
333
|
-
let statusColor: (text: string) => string = (text) => text;
|
|
334
|
-
if (useColor) {
|
|
335
|
-
switch (node.status) {
|
|
336
|
-
case 'pass':
|
|
337
|
-
statusGlyph = '✔';
|
|
338
|
-
statusColor = green;
|
|
339
|
-
break;
|
|
340
|
-
case 'warn':
|
|
341
|
-
statusGlyph = '⚠';
|
|
342
|
-
statusColor = (text) => (useColor ? yellow(text) : text);
|
|
343
|
-
break;
|
|
344
|
-
case 'fail':
|
|
345
|
-
statusGlyph = '✖';
|
|
346
|
-
statusColor = red;
|
|
347
|
-
break;
|
|
348
|
-
}
|
|
349
|
-
} else {
|
|
350
|
-
switch (node.status) {
|
|
351
|
-
case 'pass':
|
|
352
|
-
statusGlyph = '✔';
|
|
353
|
-
break;
|
|
354
|
-
case 'warn':
|
|
355
|
-
statusGlyph = '⚠';
|
|
356
|
-
break;
|
|
357
|
-
case 'fail':
|
|
358
|
-
statusGlyph = '✖';
|
|
359
|
-
break;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Format node label with color based on kind
|
|
364
|
-
// For column nodes, we need to parse the name to color code different parts
|
|
365
|
-
let labelColor: (text: string) => string = (text) => text;
|
|
366
|
-
let formattedLabel: string = node.name;
|
|
367
|
-
|
|
368
|
-
if (useColor) {
|
|
369
|
-
switch (node.kind) {
|
|
370
|
-
case 'contract':
|
|
371
|
-
case 'schema':
|
|
372
|
-
labelColor = bold;
|
|
373
|
-
formattedLabel = labelColor(node.name);
|
|
374
|
-
break;
|
|
375
|
-
case 'table': {
|
|
376
|
-
// Parse "table tableName" format - color "table" dim, tableName cyan
|
|
377
|
-
const tableMatch = node.name.match(/^table\s+(.+)$/);
|
|
378
|
-
if (tableMatch?.[1]) {
|
|
379
|
-
const tableName = tableMatch[1];
|
|
380
|
-
formattedLabel = `${dim('table')} ${cyan(tableName)}`;
|
|
381
|
-
} else {
|
|
382
|
-
formattedLabel = dim(node.name);
|
|
383
|
-
}
|
|
384
|
-
break;
|
|
385
|
-
}
|
|
386
|
-
case 'columns':
|
|
387
|
-
labelColor = dim;
|
|
388
|
-
formattedLabel = labelColor(node.name);
|
|
389
|
-
break;
|
|
390
|
-
case 'column': {
|
|
391
|
-
// Parse column name format: "columnName: contractType -> nativeType (nullability)"
|
|
392
|
-
// Color code: column name (cyan), contract type (default), native type (dim), nullability (dim)
|
|
393
|
-
const columnMatch = node.name.match(/^([^:]+):\s*(.+)$/);
|
|
394
|
-
if (columnMatch?.[1] && columnMatch[2]) {
|
|
395
|
-
const columnName = columnMatch[1];
|
|
396
|
-
const rest = columnMatch[2];
|
|
397
|
-
// Parse rest: "contractType -> nativeType (nullability)"
|
|
398
|
-
// Match contract type (can contain /, @, etc.), arrow, native type, then nullability in parentheses
|
|
399
|
-
const typeMatch = rest.match(/^([^\s→]+)\s*→\s*([^\s(]+)\s*(\([^)]+\))$/);
|
|
400
|
-
if (typeMatch?.[1] && typeMatch[2] && typeMatch[3]) {
|
|
401
|
-
const contractType = typeMatch[1];
|
|
402
|
-
const nativeType = typeMatch[2];
|
|
403
|
-
const nullability = typeMatch[3];
|
|
404
|
-
formattedLabel = `${cyan(columnName)}: ${contractType} → ${dim(nativeType)} ${dim(nullability)}`;
|
|
405
|
-
} else {
|
|
406
|
-
// Fallback if format doesn't match (e.g., no native type or no nullability)
|
|
407
|
-
formattedLabel = `${cyan(columnName)}: ${rest}`;
|
|
408
|
-
}
|
|
409
|
-
} else {
|
|
410
|
-
formattedLabel = node.name;
|
|
411
|
-
}
|
|
412
|
-
break;
|
|
413
|
-
}
|
|
414
|
-
case 'type':
|
|
415
|
-
case 'nullability':
|
|
416
|
-
labelColor = (text) => text; // Default color
|
|
417
|
-
formattedLabel = labelColor(node.name);
|
|
418
|
-
break;
|
|
419
|
-
case 'primaryKey': {
|
|
420
|
-
// Parse "primary key: columnName" format - color "primary key" dim, columnName cyan
|
|
421
|
-
const pkMatch = node.name.match(/^primary key:\s*(.+)$/);
|
|
422
|
-
if (pkMatch?.[1]) {
|
|
423
|
-
const columnNames = pkMatch[1];
|
|
424
|
-
formattedLabel = `${dim('primary key')}: ${cyan(columnNames)}`;
|
|
425
|
-
} else {
|
|
426
|
-
formattedLabel = dim(node.name);
|
|
427
|
-
}
|
|
428
|
-
break;
|
|
429
|
-
}
|
|
430
|
-
case 'foreignKey':
|
|
431
|
-
case 'unique':
|
|
432
|
-
case 'index':
|
|
433
|
-
labelColor = dim;
|
|
434
|
-
formattedLabel = labelColor(node.name);
|
|
435
|
-
break;
|
|
436
|
-
case 'dependency': {
|
|
437
|
-
// Parse specific extension message formats
|
|
438
|
-
// "database is postgres" -> dim "database is", cyan "postgres"
|
|
439
|
-
const dbMatch = node.name.match(/^database is\s+(.+)$/);
|
|
440
|
-
if (dbMatch?.[1]) {
|
|
441
|
-
const dbName = dbMatch[1];
|
|
442
|
-
formattedLabel = `${dim('database is')} ${cyan(dbName)}`;
|
|
443
|
-
} else {
|
|
444
|
-
// "vector extension is enabled" -> dim everything except extension name
|
|
445
|
-
// Match pattern: "extensionName extension is enabled"
|
|
446
|
-
const extMatch = node.name.match(/^([^\s]+)\s+(extension is enabled)$/);
|
|
447
|
-
if (extMatch?.[1] && extMatch[2]) {
|
|
448
|
-
const extName = extMatch[1];
|
|
449
|
-
const rest = extMatch[2];
|
|
450
|
-
formattedLabel = `${cyan(extName)} ${dim(rest)}`;
|
|
451
|
-
} else {
|
|
452
|
-
// Fallback: color entire name with magenta
|
|
453
|
-
labelColor = magenta;
|
|
454
|
-
formattedLabel = labelColor(node.name);
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
break;
|
|
458
|
-
}
|
|
459
|
-
default:
|
|
460
|
-
formattedLabel = node.name;
|
|
461
|
-
break;
|
|
462
|
-
}
|
|
463
|
-
} else {
|
|
464
|
-
formattedLabel = node.name;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
const statusGlyphColored = statusColor(statusGlyph);
|
|
468
|
-
|
|
469
|
-
// Build the label with optional message for failure/warn nodes
|
|
470
|
-
let nodeLabel = formattedLabel;
|
|
471
|
-
if (
|
|
472
|
-
(node.status === 'fail' || node.status === 'warn') &&
|
|
473
|
-
node.message &&
|
|
474
|
-
node.message.length > 0
|
|
475
|
-
) {
|
|
476
|
-
// Always show message for failure/warn nodes - it provides crucial context
|
|
477
|
-
// For parent nodes, the message summarizes child failures
|
|
478
|
-
// For leaf nodes, the message explains the specific issue
|
|
479
|
-
const messageText = formatDimText(`(${node.message})`);
|
|
480
|
-
nodeLabel = `${formattedLabel} ${messageText}`;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// Root node renders without tree characters or | prefix
|
|
484
|
-
// Root node renders without tree characters or prefix
|
|
485
|
-
if (isRoot) {
|
|
486
|
-
lines.push(`${statusGlyphColored} ${nodeLabel}`);
|
|
487
|
-
} else {
|
|
488
|
-
const treeChar = isLast ? '└' : '├';
|
|
489
|
-
const treePrefix = `${formatDimText(treeChar)}─ `;
|
|
490
|
-
lines.push(`${prefix}${treePrefix}${statusGlyphColored} ${nodeLabel}`);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// Render children if present
|
|
494
|
-
if (node.children && node.children.length > 0) {
|
|
495
|
-
const childPrefix = isRoot ? '' : `${prefix}${isLast ? ' ' : `${formatDimText('│')} `}`;
|
|
496
|
-
for (let i = 0; i < node.children.length; i++) {
|
|
497
|
-
const child = node.children[i];
|
|
498
|
-
if (!child) continue;
|
|
499
|
-
const isLastChild = i === node.children.length - 1;
|
|
500
|
-
const childLines = renderSchemaVerificationTree(child, flags, {
|
|
501
|
-
isLast: isLastChild,
|
|
502
|
-
prefix: childPrefix,
|
|
503
|
-
useColor,
|
|
504
|
-
formatDimText,
|
|
505
|
-
isRoot: false,
|
|
506
|
-
});
|
|
507
|
-
lines.push(...childLines);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
return lines;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
355
|
/**
|
|
515
356
|
* Formats human-readable output for database schema verification.
|
|
516
357
|
*/
|
|
517
358
|
export function formatSchemaVerifyOutput(
|
|
518
359
|
result: VerifyDatabaseSchemaResult,
|
|
519
360
|
flags: GlobalFlags,
|
|
361
|
+
unclaimed: readonly string[] = [],
|
|
520
362
|
): string {
|
|
521
363
|
if (flags.quiet) {
|
|
522
364
|
return '';
|
|
@@ -527,30 +369,45 @@ export function formatSchemaVerifyOutput(
|
|
|
527
369
|
const useColor = flags.color !== false;
|
|
528
370
|
const formatGreen = createColorFormatter(useColor, green);
|
|
529
371
|
const formatRed = createColorFormatter(useColor, red);
|
|
372
|
+
const formatYellow = createColorFormatter(useColor, yellow);
|
|
530
373
|
const formatDimText = (text: string) => formatDim(useColor, text);
|
|
531
374
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
375
|
+
const issueMessages = result.schema.issues.map(formatIssueMessage);
|
|
376
|
+
if (issueMessages.length > 0) {
|
|
377
|
+
lines.push(formatRed('Schema issues:'));
|
|
378
|
+
for (const message of issueMessages) {
|
|
379
|
+
lines.push(` ${formatRed('✖')} ${message}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const warningMessages = (result.schema.warnings?.issues ?? []).map(formatIssueMessage);
|
|
384
|
+
if (warningMessages.length > 0) {
|
|
385
|
+
if (lines.length > 0) lines.push('');
|
|
386
|
+
lines.push(formatYellow('Schema warnings:'));
|
|
387
|
+
for (const message of warningMessages) {
|
|
388
|
+
lines.push(` ${formatYellow('⚠')} ${message}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (unclaimed.length > 0) {
|
|
393
|
+
const strict = result.meta?.strict ?? false;
|
|
394
|
+
if (lines.length > 0) lines.push('');
|
|
545
395
|
lines.push(
|
|
546
|
-
|
|
396
|
+
(strict ? formatRed : formatYellow)('Unclaimed elements (declared by no contract):'),
|
|
547
397
|
);
|
|
398
|
+
for (const name of unclaimed) {
|
|
399
|
+
lines.push(` ${(strict ? formatRed : formatYellow)(strict ? '✖' : '⚠')} ${name}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (isVerbose(flags, 1)) {
|
|
404
|
+
lines.push(`${formatDimText(` Total time: ${result.timings.total}ms`)}`);
|
|
548
405
|
}
|
|
549
406
|
|
|
550
407
|
// Blank line before summary
|
|
551
|
-
lines.push('');
|
|
408
|
+
if (lines.length > 0) lines.push('');
|
|
552
409
|
|
|
553
|
-
// Summary line at the end:
|
|
410
|
+
// Summary line at the end: verdict with status glyph
|
|
554
411
|
if (result.ok) {
|
|
555
412
|
lines.push(`${formatGreen('✔')} ${result.summary}`);
|
|
556
413
|
} else {
|
|
@@ -562,10 +419,15 @@ export function formatSchemaVerifyOutput(
|
|
|
562
419
|
}
|
|
563
420
|
|
|
564
421
|
/**
|
|
565
|
-
* Formats JSON output for database schema verification.
|
|
422
|
+
* Formats JSON output for database schema verification. The unclaimed-elements
|
|
423
|
+
* list is a top-level field alongside the combined result, reported once for
|
|
424
|
+
* the whole database.
|
|
566
425
|
*/
|
|
567
|
-
export function formatSchemaVerifyJson(
|
|
568
|
-
|
|
426
|
+
export function formatSchemaVerifyJson(
|
|
427
|
+
result: VerifyDatabaseSchemaResult,
|
|
428
|
+
unclaimed: readonly string[] = [],
|
|
429
|
+
): string {
|
|
430
|
+
return JSON.stringify({ ...result, unclaimed }, null, 2);
|
|
569
431
|
}
|
|
570
432
|
|
|
571
433
|
// ============================================================================
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { relative, resolve } from 'node:path';
|
|
3
|
+
import { loadConfig } from '@prisma-next/config-loader';
|
|
3
4
|
import { hasMigrations } from '@prisma-next/framework-components/control';
|
|
4
5
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
5
6
|
import type { Command } from 'commander';
|
|
6
|
-
import { loadConfig } from '../config-loader';
|
|
7
7
|
import { createControlClient } from '../control-api/client';
|
|
8
8
|
import type { ControlClient } from '../control-api/types';
|
|
9
9
|
import {
|