@prisma-next/cli 0.12.0-dev.21 → 0.12.0-dev.22
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/cli.mjs +8 -8
- package/dist/{client-Cdxcme1x.mjs → client-BHe8szOW.mjs} +4 -4
- package/dist/{client-Cdxcme1x.mjs.map → client-BHe8szOW.mjs.map} +1 -1
- package/dist/commands/contract-infer.mjs +1 -1
- package/dist/commands/db-init.mjs +2 -2
- package/dist/commands/db-schema.mjs +1 -1
- package/dist/commands/db-sign.mjs +1 -1
- package/dist/commands/db-update.mjs +2 -2
- package/dist/commands/db-verify.mjs +1 -1
- package/dist/commands/migrate.d.mts +1 -1
- package/dist/commands/migrate.mjs +1 -1
- package/dist/commands/migration-graph.d.mts +4 -4
- package/dist/commands/migration-graph.mjs +1 -1
- package/dist/commands/migration-list.d.mts +3 -3
- package/dist/commands/migration-list.mjs +1 -1
- package/dist/commands/migration-log.d.mts +7 -19
- package/dist/commands/migration-log.d.mts.map +1 -1
- package/dist/commands/migration-log.mjs +1 -137
- package/dist/commands/migration-plan.d.mts +1 -1
- package/dist/commands/migration-plan.mjs +1 -1
- package/dist/commands/migration-show.d.mts +1 -1
- package/dist/commands/migration-show.mjs +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/telemetry/index.mjs +1 -1
- package/dist/{contract-infer-DaFPNrZH.mjs → contract-infer-OCn12Zvn.mjs} +2 -2
- package/dist/{contract-infer-DaFPNrZH.mjs.map → contract-infer-OCn12Zvn.mjs.map} +1 -1
- package/dist/{db-verify-BSA1a_W_.mjs → db-verify-DJxengYP.mjs} +2 -2
- package/dist/{db-verify-BSA1a_W_.mjs.map → db-verify-DJxengYP.mjs.map} +1 -1
- package/dist/exports/control-api.d.mts +1 -1
- package/dist/exports/control-api.mjs +1 -1
- package/dist/exports/init-output.mjs +1 -1
- package/dist/{global-flags-DG4uY5tV.d.mts → global-flags-DSkV6iYT.d.mts} +1 -1
- package/dist/{global-flags-DG4uY5tV.d.mts.map → global-flags-DSkV6iYT.d.mts.map} +1 -1
- package/dist/{init-B6kKrmf7.mjs → init-S2vxszo_.mjs} +2 -2
- package/dist/{init-B6kKrmf7.mjs.map → init-S2vxszo_.mjs.map} +1 -1
- package/dist/{inspect-live-schema-Dn56wDhG.mjs → inspect-live-schema-DVZlDlnF.mjs} +2 -2
- package/dist/{inspect-live-schema-Dn56wDhG.mjs.map → inspect-live-schema-DVZlDlnF.mjs.map} +1 -1
- package/dist/{migration-command-scaffold-V52dV2Tv.mjs → migration-command-scaffold-Cs7Ky-m5.mjs} +2 -2
- package/dist/{migration-command-scaffold-V52dV2Tv.mjs.map → migration-command-scaffold-Cs7Ky-m5.mjs.map} +1 -1
- package/dist/{migration-graph-DKl_IYsF.mjs → migration-graph-CeBB07Cc.mjs} +2 -2
- package/dist/{migration-graph-DKl_IYsF.mjs.map → migration-graph-CeBB07Cc.mjs.map} +1 -1
- package/dist/{migration-list-styler-COQbZmXk.mjs → migration-list-styler-CsMECsY4.mjs} +2 -2
- package/dist/{migration-list-styler-COQbZmXk.mjs.map → migration-list-styler-CsMECsY4.mjs.map} +1 -1
- package/dist/migration-log-Cj-T-r0o.mjs +203 -0
- package/dist/migration-log-Cj-T-r0o.mjs.map +1 -0
- package/dist/{migration-plan-CaeKCKp4.mjs → migration-plan-BQAbZkj_.mjs} +1 -1
- package/dist/{migration-plan-CaeKCKp4.mjs.map → migration-plan-BQAbZkj_.mjs.map} +1 -1
- package/dist/{migration-types-CAQ-0TEE.d.mts → migration-types-Bhmj0RSa.d.mts} +1 -1
- package/dist/{migration-types-CAQ-0TEE.d.mts.map → migration-types-Bhmj0RSa.d.mts.map} +1 -1
- package/dist/{output-CF_hqzI-.mjs → output-BD61elic.mjs} +1 -1
- package/dist/{output-CF_hqzI-.mjs.map → output-BD61elic.mjs.map} +1 -1
- package/dist/{telemetry-Q88WHwlv.mjs → telemetry-Bu85x2Gy.mjs} +1 -1
- package/dist/{telemetry-Q88WHwlv.mjs.map → telemetry-Bu85x2Gy.mjs.map} +1 -1
- package/dist/{terminal-ui-C3xGyxW-.d.mts → terminal-ui-BgLiAOYi.d.mts} +1 -1
- package/dist/{terminal-ui-C3xGyxW-.d.mts.map → terminal-ui-BgLiAOYi.d.mts.map} +1 -1
- package/dist/{types-DiC683UW.d.mts → types-C8OcDFBe.d.mts} +1 -1
- package/dist/{types-DiC683UW.d.mts.map → types-C8OcDFBe.d.mts.map} +1 -1
- package/package.json +18 -18
- package/src/commands/migration-log.ts +23 -89
- package/src/control-api/client.ts +3 -3
- package/src/utils/formatters/migration-log-table.ts +190 -0
- package/dist/commands/migration-log.mjs.map +0 -1
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
|
|
1
|
+
import type { LedgerEntryRecord } from '@prisma-next/contract/types';
|
|
3
2
|
import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
|
|
4
|
-
import { findPath } from '@prisma-next/migration-tools/migration-graph';
|
|
5
3
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
6
|
-
import { cyan, dim } from 'colorette';
|
|
7
4
|
import { Command } from 'commander';
|
|
8
5
|
import { loadConfig } from '../config-loader';
|
|
9
6
|
import { createControlClient } from '../control-api/client';
|
|
@@ -23,7 +20,12 @@ import {
|
|
|
23
20
|
setCommandSeeAlso,
|
|
24
21
|
targetSupportsMigrations,
|
|
25
22
|
} from '../utils/command-helpers';
|
|
26
|
-
import {
|
|
23
|
+
import { createAnsiMigrationListStyler } from '../utils/formatters/migration-list-styler';
|
|
24
|
+
import {
|
|
25
|
+
MIGRATION_LOG_EMPTY_MESSAGE,
|
|
26
|
+
renderMigrationLogTable,
|
|
27
|
+
serializeLedgerEntriesForJson,
|
|
28
|
+
} from '../utils/formatters/migration-log-table';
|
|
27
29
|
import { formatStyledHeader } from '../utils/formatters/styled';
|
|
28
30
|
import type { CommonCommandOptions } from '../utils/global-flags';
|
|
29
31
|
import { type GlobalFlags, parseGlobalFlagsOrExit } from '../utils/global-flags';
|
|
@@ -33,34 +35,16 @@ import { createTerminalUI, type TerminalUI } from '../utils/terminal-ui';
|
|
|
33
35
|
interface MigrationLogOptions extends CommonCommandOptions {
|
|
34
36
|
readonly db?: string;
|
|
35
37
|
readonly config?: string;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
export interface MigrationLogEntry {
|
|
39
|
-
readonly dirName: string;
|
|
40
|
-
readonly from: string;
|
|
41
|
-
readonly to: string;
|
|
42
|
-
readonly migrationHash: string;
|
|
43
|
-
readonly operationCount: number;
|
|
44
|
-
readonly createdAt: string;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface MigrationLogResult {
|
|
48
|
-
readonly ok: true;
|
|
49
|
-
readonly markerHash: string | null;
|
|
50
|
-
readonly applied: readonly MigrationLogEntry[];
|
|
51
|
-
readonly summary: string;
|
|
38
|
+
readonly utc?: boolean;
|
|
52
39
|
}
|
|
53
40
|
|
|
54
41
|
export async function executeMigrationLogCommand(
|
|
55
42
|
options: MigrationLogOptions,
|
|
56
43
|
flags: GlobalFlags,
|
|
57
44
|
ui: TerminalUI,
|
|
58
|
-
): Promise<Result<
|
|
45
|
+
): Promise<Result<readonly LedgerEntryRecord[], CliStructuredError>> {
|
|
59
46
|
const config = await loadConfig(options.config);
|
|
60
|
-
const { configPath
|
|
61
|
-
options.config,
|
|
62
|
-
config,
|
|
63
|
-
);
|
|
47
|
+
const { configPath } = resolveMigrationPaths(options.config, config);
|
|
64
48
|
|
|
65
49
|
const dbConnection = options.db ?? config.db?.connection;
|
|
66
50
|
if (!dbConnection) {
|
|
@@ -81,10 +65,9 @@ export async function executeMigrationLogCommand(
|
|
|
81
65
|
if (!flags.json && !flags.quiet) {
|
|
82
66
|
const header = formatStyledHeader({
|
|
83
67
|
command: 'migration log',
|
|
84
|
-
description: 'Show executed migration history',
|
|
68
|
+
description: 'Show executed migration history from the database ledger',
|
|
85
69
|
details: [
|
|
86
70
|
{ label: 'config', value: configPath },
|
|
87
|
-
{ label: 'migrations', value: appMigrationsRelative },
|
|
88
71
|
...(typeof dbConnection === 'string'
|
|
89
72
|
? [{ label: 'database', value: maskConnectionUrl(dbConnection) }]
|
|
90
73
|
: []),
|
|
@@ -94,13 +77,6 @@ export async function executeMigrationLogCommand(
|
|
|
94
77
|
ui.stderr(header);
|
|
95
78
|
}
|
|
96
79
|
|
|
97
|
-
const loaded = await buildReadAggregate(config, { migrationsDir });
|
|
98
|
-
if (!loaded.ok) {
|
|
99
|
-
return loaded;
|
|
100
|
-
}
|
|
101
|
-
const graph = loaded.value.aggregate.app.graph();
|
|
102
|
-
const bundles = loaded.value.aggregate.app.packages;
|
|
103
|
-
|
|
104
80
|
const client = createControlClient({
|
|
105
81
|
family: config.family,
|
|
106
82
|
target: config.target,
|
|
@@ -111,47 +87,8 @@ export async function executeMigrationLogCommand(
|
|
|
111
87
|
|
|
112
88
|
try {
|
|
113
89
|
await client.connect(dbConnection);
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (!markerHash) {
|
|
118
|
-
return ok({
|
|
119
|
-
ok: true,
|
|
120
|
-
markerHash: null,
|
|
121
|
-
applied: [],
|
|
122
|
-
summary: 'No migrations applied (database has no marker)',
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const appliedPath = findPath(graph, EMPTY_CONTRACT_HASH, markerHash);
|
|
127
|
-
if (appliedPath === null) {
|
|
128
|
-
return notOk(
|
|
129
|
-
errorUnexpected('Database marker is not reachable from migration history', {
|
|
130
|
-
why: `Marker hash ${markerHash} is not reachable from the root of the on-disk migration graph.`,
|
|
131
|
-
fix: 'The database may have been migrated outside this project. Use `migration status` to inspect the current state.',
|
|
132
|
-
}),
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
const pkgByDirName = new Map(bundles.map((p) => [p.dirName, p]));
|
|
136
|
-
const entries: MigrationLogEntry[] = appliedPath.map((edge) => {
|
|
137
|
-
const pkg = pkgByDirName.get(edge.dirName);
|
|
138
|
-
const ops = (pkg?.ops ?? []) as readonly MigrationPlanOperation[];
|
|
139
|
-
return {
|
|
140
|
-
dirName: edge.dirName,
|
|
141
|
-
from: edge.from,
|
|
142
|
-
to: edge.to,
|
|
143
|
-
migrationHash: edge.migrationHash,
|
|
144
|
-
operationCount: ops.length,
|
|
145
|
-
createdAt: edge.createdAt,
|
|
146
|
-
};
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
return ok({
|
|
150
|
-
ok: true,
|
|
151
|
-
markerHash,
|
|
152
|
-
applied: entries,
|
|
153
|
-
summary: `${entries.length} migration(s) applied`,
|
|
154
|
-
});
|
|
90
|
+
const ledger = await client.readLedger();
|
|
91
|
+
return ok(ledger);
|
|
155
92
|
} catch (error) {
|
|
156
93
|
if (CliStructuredError.is(error)) return notOk(error);
|
|
157
94
|
if (MigrationToolsError.is(error)) return notOk(mapMigrationToolsError(error));
|
|
@@ -170,11 +107,12 @@ export function createMigrationLogCommand(): Command {
|
|
|
170
107
|
setCommandDescriptions(
|
|
171
108
|
command,
|
|
172
109
|
'Show executed migration history',
|
|
173
|
-
'Reads the database
|
|
174
|
-
'
|
|
110
|
+
'Reads the database ledger and displays every applied migration edge\n' +
|
|
111
|
+
'in chronological order, including rollbacks and re-applies.',
|
|
175
112
|
);
|
|
176
113
|
setCommandExamples(command, [
|
|
177
114
|
'prisma-next migration log --db $DATABASE_URL',
|
|
115
|
+
'prisma-next migration log --utc --db $DATABASE_URL',
|
|
178
116
|
'prisma-next migration log --json --db $DATABASE_URL',
|
|
179
117
|
]);
|
|
180
118
|
setCommandSeeAlso(command, [
|
|
@@ -186,24 +124,20 @@ export function createMigrationLogCommand(): Command {
|
|
|
186
124
|
addGlobalOptions(command)
|
|
187
125
|
.option('--db <url>', 'Database connection string')
|
|
188
126
|
.option('--config <path>', 'Path to prisma-next.config.ts')
|
|
127
|
+
.option('--utc', 'Render human timestamps in UTC instead of local time')
|
|
189
128
|
.action(async (options: MigrationLogOptions) => {
|
|
190
129
|
const flags = parseGlobalFlagsOrExit(options);
|
|
191
130
|
const ui = createTerminalUI(flags);
|
|
192
131
|
const result = await executeMigrationLogCommand(options, flags, ui);
|
|
193
|
-
const exitCode = handleResult(result, flags, ui, (
|
|
132
|
+
const exitCode = handleResult(result, flags, ui, (entries) => {
|
|
194
133
|
if (flags.json) {
|
|
195
|
-
ui.output(JSON.stringify(
|
|
134
|
+
ui.output(JSON.stringify(serializeLedgerEntriesForJson(entries), null, 2));
|
|
196
135
|
} else if (!flags.quiet) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
ui.log(logResult.summary);
|
|
136
|
+
if (entries.length === 0) {
|
|
137
|
+
ui.output(MIGRATION_LOG_EMPTY_MESSAGE);
|
|
200
138
|
} else {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
`${c(cyan, '✓')} ${entry.dirName} ${c(dim, entry.migrationHash.slice(0, 16) + '…')} ${entry.operationCount} op(s)`,
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
ui.log(`\n${logResult.summary}`);
|
|
139
|
+
const styler = createAnsiMigrationListStyler({ useColor: ui.useColor });
|
|
140
|
+
ui.output(renderMigrationLogTable(entries, { utc: options.utc === true, styler }));
|
|
207
141
|
}
|
|
208
142
|
}
|
|
209
143
|
});
|
|
@@ -451,10 +451,10 @@ class ControlClientImpl implements ControlClient {
|
|
|
451
451
|
return familyInstance.readAllMarkers({ driver });
|
|
452
452
|
}
|
|
453
453
|
|
|
454
|
-
/** Reads the per-migration journal
|
|
455
|
-
async readLedger(space
|
|
454
|
+
/** Reads the per-migration journal; omit `space` to return every space. */
|
|
455
|
+
async readLedger(space?: string): Promise<readonly LedgerEntryRecord[]> {
|
|
456
456
|
const { driver, familyInstance } = await this.ensureConnected();
|
|
457
|
-
return familyInstance.readLedger({ driver, space });
|
|
457
|
+
return familyInstance.readLedger({ driver, ...ifDefined('space', space) });
|
|
458
458
|
}
|
|
459
459
|
|
|
460
460
|
async migrationApply(options: MigrationApplyOptions): Promise<MigrationApplyResult> {
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type { LedgerEntryRecord } from '@prisma-next/contract/types';
|
|
2
|
+
import stringWidth from 'string-width';
|
|
3
|
+
import {
|
|
4
|
+
abbreviateContractHash,
|
|
5
|
+
MIGRATION_LIST_EMPTY_SOURCE,
|
|
6
|
+
MIGRATION_LIST_FORWARD_EDGE_GLYPH,
|
|
7
|
+
} from './migration-list-data-column';
|
|
8
|
+
import { IDENTITY_MIGRATION_LIST_STYLER, type MigrationListStyler } from './migration-list-render';
|
|
9
|
+
|
|
10
|
+
export type LedgerTimestampMode = 'local' | 'utc' | 'iso';
|
|
11
|
+
|
|
12
|
+
export interface RenderMigrationLogTableOptions {
|
|
13
|
+
readonly utc?: boolean;
|
|
14
|
+
readonly styler?: MigrationListStyler;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SerializedLedgerEntryRecord {
|
|
18
|
+
readonly space: string;
|
|
19
|
+
readonly migrationName: string;
|
|
20
|
+
readonly migrationHash: string;
|
|
21
|
+
readonly from: string | null;
|
|
22
|
+
readonly to: string;
|
|
23
|
+
readonly appliedAt: string;
|
|
24
|
+
readonly operationCount: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const HEADING_APPLIED_AT = 'Applied at';
|
|
28
|
+
const HEADING_SPACE = 'Space';
|
|
29
|
+
const HEADING_MIGRATION = 'Migration';
|
|
30
|
+
const HEADING_CHANGE = 'Change';
|
|
31
|
+
const HEADING_OPS = 'Ops';
|
|
32
|
+
const COLUMN_SEPARATOR = ' ';
|
|
33
|
+
const DIVIDER_CHAR = '─';
|
|
34
|
+
|
|
35
|
+
export function sortLedgerEntries(entries: readonly LedgerEntryRecord[]): LedgerEntryRecord[] {
|
|
36
|
+
return [...entries].sort((left, right) => {
|
|
37
|
+
const timeDiff = left.appliedAt.getTime() - right.appliedAt.getTime();
|
|
38
|
+
if (timeDiff !== 0) {
|
|
39
|
+
return timeDiff;
|
|
40
|
+
}
|
|
41
|
+
const spaceDiff = left.space.localeCompare(right.space);
|
|
42
|
+
if (spaceDiff !== 0) {
|
|
43
|
+
return spaceDiff;
|
|
44
|
+
}
|
|
45
|
+
return left.migrationName.localeCompare(right.migrationName);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function pad2(value: number): string {
|
|
50
|
+
return String(value).padStart(2, '0');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function formatLedgerAppliedAt(date: Date, mode: LedgerTimestampMode): string {
|
|
54
|
+
if (mode === 'iso') {
|
|
55
|
+
return date.toISOString();
|
|
56
|
+
}
|
|
57
|
+
if (mode === 'utc') {
|
|
58
|
+
return `${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(date.getUTCDate())} ${pad2(date.getUTCHours())}:${pad2(date.getUTCMinutes())}:${pad2(date.getUTCSeconds())}Z`;
|
|
59
|
+
}
|
|
60
|
+
const offsetMinutes = -date.getTimezoneOffset();
|
|
61
|
+
const sign = offsetMinutes >= 0 ? '+' : '-';
|
|
62
|
+
const absoluteOffset = Math.abs(offsetMinutes);
|
|
63
|
+
const offsetHours = pad2(Math.floor(absoluteOffset / 60));
|
|
64
|
+
const offsetMins = pad2(absoluteOffset % 60);
|
|
65
|
+
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())} ${sign}${offsetHours}:${offsetMins}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function formatHashEndpoint(hash: string | null): string {
|
|
69
|
+
if (hash === null) {
|
|
70
|
+
return MIGRATION_LIST_EMPTY_SOURCE;
|
|
71
|
+
}
|
|
72
|
+
return abbreviateContractHash(hash);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function formatHashTransition(from: string | null, to: string): string {
|
|
76
|
+
return `${formatHashEndpoint(from)} ${MIGRATION_LIST_FORWARD_EDGE_GLYPH} ${abbreviateContractHash(to)}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function styleHashTransition(
|
|
80
|
+
from: string | null,
|
|
81
|
+
to: string,
|
|
82
|
+
styler: MigrationListStyler,
|
|
83
|
+
): string {
|
|
84
|
+
const fromPart =
|
|
85
|
+
from === null
|
|
86
|
+
? styler.glyph(MIGRATION_LIST_EMPTY_SOURCE)
|
|
87
|
+
: styler.sourceHash(abbreviateContractHash(from));
|
|
88
|
+
const arrow = styler.glyph(MIGRATION_LIST_FORWARD_EDGE_GLYPH);
|
|
89
|
+
const dest = styler.destHash(abbreviateContractHash(to));
|
|
90
|
+
return `${fromPart} ${arrow} ${dest}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function padVisible(text: string, targetWidth: number): string {
|
|
94
|
+
const padding = Math.max(0, targetWidth - stringWidth(text));
|
|
95
|
+
return text + ' '.repeat(padding);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function columnWidth(values: readonly string[]): number {
|
|
99
|
+
return values.reduce((max, value) => Math.max(max, stringWidth(value)), 0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function padDividerCell(valueWidth: number): string {
|
|
103
|
+
return DIVIDER_CHAR.repeat(valueWidth + 2);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function padTextCell(value: string, valueWidth: number): string {
|
|
107
|
+
return ` ${padVisible(value, valueWidth)} `;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function padOpsCell(value: string, valueWidth: number): string {
|
|
111
|
+
const padding = Math.max(0, valueWidth - stringWidth(value));
|
|
112
|
+
return ` ${' '.repeat(padding)}${value} `;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function renderMigrationLogTable(
|
|
116
|
+
entries: readonly LedgerEntryRecord[],
|
|
117
|
+
options: RenderMigrationLogTableOptions = {},
|
|
118
|
+
): string {
|
|
119
|
+
const sorted = sortLedgerEntries(entries);
|
|
120
|
+
if (sorted.length === 0) {
|
|
121
|
+
return '';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const styler = options.styler ?? IDENTITY_MIGRATION_LIST_STYLER;
|
|
125
|
+
const showSpace = new Set(sorted.map((entry) => entry.space)).size > 1;
|
|
126
|
+
const timestampMode: LedgerTimestampMode = options.utc ? 'utc' : 'local';
|
|
127
|
+
const rows = sorted.map((entry) => ({
|
|
128
|
+
appliedAt: formatLedgerAppliedAt(entry.appliedAt, timestampMode),
|
|
129
|
+
space: entry.space,
|
|
130
|
+
migrationName: entry.migrationName,
|
|
131
|
+
transition: formatHashTransition(entry.from, entry.to),
|
|
132
|
+
ops: `${entry.operationCount} ops`,
|
|
133
|
+
from: entry.from,
|
|
134
|
+
to: entry.to,
|
|
135
|
+
}));
|
|
136
|
+
|
|
137
|
+
const appliedAtWidth = columnWidth([HEADING_APPLIED_AT, ...rows.map((row) => row.appliedAt)]);
|
|
138
|
+
const spaceWidth = showSpace ? columnWidth([HEADING_SPACE, ...rows.map((row) => row.space)]) : 0;
|
|
139
|
+
const nameWidth = columnWidth([HEADING_MIGRATION, ...rows.map((row) => row.migrationName)]);
|
|
140
|
+
const transitionWidth = columnWidth([HEADING_CHANGE, ...rows.map((row) => row.transition)]);
|
|
141
|
+
const opsWidth = columnWidth([HEADING_OPS, ...rows.map((row) => row.ops)]);
|
|
142
|
+
|
|
143
|
+
const headingParts = [padTextCell(HEADING_APPLIED_AT, appliedAtWidth)];
|
|
144
|
+
if (showSpace) {
|
|
145
|
+
headingParts.push(padTextCell(HEADING_SPACE, spaceWidth));
|
|
146
|
+
}
|
|
147
|
+
headingParts.push(
|
|
148
|
+
padTextCell(HEADING_MIGRATION, nameWidth),
|
|
149
|
+
padTextCell(HEADING_CHANGE, transitionWidth),
|
|
150
|
+
padOpsCell(HEADING_OPS, opsWidth),
|
|
151
|
+
);
|
|
152
|
+
const heading = headingParts.join(COLUMN_SEPARATOR);
|
|
153
|
+
|
|
154
|
+
const dividerParts = [padDividerCell(appliedAtWidth)];
|
|
155
|
+
if (showSpace) {
|
|
156
|
+
dividerParts.push(padDividerCell(spaceWidth));
|
|
157
|
+
}
|
|
158
|
+
dividerParts.push(
|
|
159
|
+
padDividerCell(nameWidth),
|
|
160
|
+
padDividerCell(transitionWidth),
|
|
161
|
+
padDividerCell(opsWidth),
|
|
162
|
+
);
|
|
163
|
+
const divider = dividerParts.map((cell) => styler.summary(cell)).join(COLUMN_SEPARATOR);
|
|
164
|
+
|
|
165
|
+
const dataRows = rows.map((row) => {
|
|
166
|
+
const parts = [padTextCell(row.appliedAt, appliedAtWidth)];
|
|
167
|
+
if (showSpace) {
|
|
168
|
+
parts.push(padTextCell(row.space, spaceWidth));
|
|
169
|
+
}
|
|
170
|
+
parts.push(
|
|
171
|
+
padTextCell(styler.dirName(row.migrationName), nameWidth),
|
|
172
|
+
padTextCell(styleHashTransition(row.from, row.to, styler), transitionWidth),
|
|
173
|
+
padOpsCell(row.ops, opsWidth),
|
|
174
|
+
);
|
|
175
|
+
return parts.join(COLUMN_SEPARATOR);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return [heading, divider, ...dataRows].join('\n');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function serializeLedgerEntriesForJson(
|
|
182
|
+
entries: readonly LedgerEntryRecord[],
|
|
183
|
+
): SerializedLedgerEntryRecord[] {
|
|
184
|
+
return sortLedgerEntries(entries).map(({ appliedAt, ...rest }) => ({
|
|
185
|
+
...rest,
|
|
186
|
+
appliedAt: formatLedgerAppliedAt(appliedAt, 'iso'),
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export const MIGRATION_LOG_EMPTY_MESSAGE = 'No migrations have been applied to this database.';
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"migration-log.mjs","names":[],"sources":["../../src/commands/migration-log.ts"],"sourcesContent":["import type { MigrationPlanOperation } from '@prisma-next/framework-components/control';\nimport { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';\nimport { MigrationToolsError } from '@prisma-next/migration-tools/errors';\nimport { findPath } from '@prisma-next/migration-tools/migration-graph';\nimport { notOk, ok, type Result } from '@prisma-next/utils/result';\nimport { cyan, dim } from 'colorette';\nimport { Command } from 'commander';\nimport { loadConfig } from '../config-loader';\nimport { createControlClient } from '../control-api/client';\nimport {\n CliStructuredError,\n errorDatabaseConnectionRequired,\n errorDriverRequired,\n errorUnexpected,\n mapMigrationToolsError,\n} from '../utils/cli-errors';\nimport {\n addGlobalOptions,\n maskConnectionUrl,\n resolveMigrationPaths,\n setCommandDescriptions,\n setCommandExamples,\n setCommandSeeAlso,\n targetSupportsMigrations,\n} from '../utils/command-helpers';\nimport { buildReadAggregate } from '../utils/contract-space-aggregate-loader';\nimport { formatStyledHeader } from '../utils/formatters/styled';\nimport type { CommonCommandOptions } from '../utils/global-flags';\nimport { type GlobalFlags, parseGlobalFlagsOrExit } from '../utils/global-flags';\nimport { handleResult } from '../utils/result-handler';\nimport { createTerminalUI, type TerminalUI } from '../utils/terminal-ui';\n\ninterface MigrationLogOptions extends CommonCommandOptions {\n readonly db?: string;\n readonly config?: string;\n}\n\nexport interface MigrationLogEntry {\n readonly dirName: string;\n readonly from: string;\n readonly to: string;\n readonly migrationHash: string;\n readonly operationCount: number;\n readonly createdAt: string;\n}\n\nexport interface MigrationLogResult {\n readonly ok: true;\n readonly markerHash: string | null;\n readonly applied: readonly MigrationLogEntry[];\n readonly summary: string;\n}\n\nexport async function executeMigrationLogCommand(\n options: MigrationLogOptions,\n flags: GlobalFlags,\n ui: TerminalUI,\n): Promise<Result<MigrationLogResult, CliStructuredError>> {\n const config = await loadConfig(options.config);\n const { configPath, appMigrationsRelative, migrationsDir } = resolveMigrationPaths(\n options.config,\n config,\n );\n\n const dbConnection = options.db ?? config.db?.connection;\n if (!dbConnection) {\n return notOk(\n errorDatabaseConnectionRequired({\n why: `Database connection is required for migration log (set db.connection in ${configPath}, or pass --db <url>)`,\n commandName: 'migration log',\n }),\n );\n }\n if (!config.driver) {\n return notOk(errorDriverRequired({ why: 'Config.driver is required for migration log' }));\n }\n if (!targetSupportsMigrations(config.target)) {\n return notOk(errorUnexpected('Target does not support migrations'));\n }\n\n if (!flags.json && !flags.quiet) {\n const header = formatStyledHeader({\n command: 'migration log',\n description: 'Show executed migration history',\n details: [\n { label: 'config', value: configPath },\n { label: 'migrations', value: appMigrationsRelative },\n ...(typeof dbConnection === 'string'\n ? [{ label: 'database', value: maskConnectionUrl(dbConnection) }]\n : []),\n ],\n flags,\n });\n ui.stderr(header);\n }\n\n const loaded = await buildReadAggregate(config, { migrationsDir });\n if (!loaded.ok) {\n return loaded;\n }\n const graph = loaded.value.aggregate.app.graph();\n const bundles = loaded.value.aggregate.app.packages;\n\n const client = createControlClient({\n family: config.family,\n target: config.target,\n adapter: config.adapter,\n driver: config.driver,\n extensionPacks: config.extensionPacks ?? [],\n });\n\n try {\n await client.connect(dbConnection);\n const marker = await client.readMarker();\n const markerHash = marker?.storageHash ?? null;\n\n if (!markerHash) {\n return ok({\n ok: true,\n markerHash: null,\n applied: [],\n summary: 'No migrations applied (database has no marker)',\n });\n }\n\n const appliedPath = findPath(graph, EMPTY_CONTRACT_HASH, markerHash);\n if (appliedPath === null) {\n return notOk(\n errorUnexpected('Database marker is not reachable from migration history', {\n why: `Marker hash ${markerHash} is not reachable from the root of the on-disk migration graph.`,\n fix: 'The database may have been migrated outside this project. Use `migration status` to inspect the current state.',\n }),\n );\n }\n const pkgByDirName = new Map(bundles.map((p) => [p.dirName, p]));\n const entries: MigrationLogEntry[] = appliedPath.map((edge) => {\n const pkg = pkgByDirName.get(edge.dirName);\n const ops = (pkg?.ops ?? []) as readonly MigrationPlanOperation[];\n return {\n dirName: edge.dirName,\n from: edge.from,\n to: edge.to,\n migrationHash: edge.migrationHash,\n operationCount: ops.length,\n createdAt: edge.createdAt,\n };\n });\n\n return ok({\n ok: true,\n markerHash,\n applied: entries,\n summary: `${entries.length} migration(s) applied`,\n });\n } catch (error) {\n if (CliStructuredError.is(error)) return notOk(error);\n if (MigrationToolsError.is(error)) return notOk(mapMigrationToolsError(error));\n return notOk(\n errorUnexpected(error instanceof Error ? error.message : String(error), {\n why: `Failed to read migration log: ${error instanceof Error ? error.message : String(error)}`,\n }),\n );\n } finally {\n await client.close();\n }\n}\n\nexport function createMigrationLogCommand(): Command {\n const command = new Command('log');\n setCommandDescriptions(\n command,\n 'Show executed migration history',\n 'Reads the database marker and displays the applied migration chain\\n' +\n 'from the initial state to the current marker position.',\n );\n setCommandExamples(command, [\n 'prisma-next migration log --db $DATABASE_URL',\n 'prisma-next migration log --json --db $DATABASE_URL',\n ]);\n setCommandSeeAlso(command, [\n { verb: 'migration status', oneLiner: 'Show migration path and pending status' },\n { verb: 'migration list', oneLiner: 'List on-disk migrations' },\n { verb: 'migration graph', oneLiner: 'Show the migration graph topology' },\n { verb: 'migration show', oneLiner: 'Display migration package contents' },\n ]);\n addGlobalOptions(command)\n .option('--db <url>', 'Database connection string')\n .option('--config <path>', 'Path to prisma-next.config.ts')\n .action(async (options: MigrationLogOptions) => {\n const flags = parseGlobalFlagsOrExit(options);\n const ui = createTerminalUI(flags);\n const result = await executeMigrationLogCommand(options, flags, ui);\n const exitCode = handleResult(result, flags, ui, (logResult) => {\n if (flags.json) {\n ui.output(JSON.stringify(logResult, null, 2));\n } else if (!flags.quiet) {\n const c = (fn: (s: string) => string, s: string) => (flags.color !== false ? fn(s) : s);\n if (logResult.applied.length === 0) {\n ui.log(logResult.summary);\n } else {\n for (const entry of logResult.applied) {\n ui.log(\n `${c(cyan, '✓')} ${entry.dirName} ${c(dim, entry.migrationHash.slice(0, 16) + '…')} ${entry.operationCount} op(s)`,\n );\n }\n ui.log(`\\n${logResult.summary}`);\n }\n }\n });\n process.exit(exitCode);\n });\n return command;\n}\n"],"mappings":"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"}
|