Skip to content

Commit 153d972

Browse files
committed
allow running migrations inline from the CLI
1 parent 12e697e commit 153d972

File tree

1 file changed

+103
-35
lines changed

1 file changed

+103
-35
lines changed

src/client/index.ts

Lines changed: 103 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
type GenericQueryCtx,
99
getFunctionAddress,
1010
getFunctionName,
11+
internalActionGeneric,
1112
internalMutationGeneric,
1213
makeFunctionReference,
1314
type MutationBuilder,
@@ -25,7 +26,7 @@ import {
2526
} from "../shared.js";
2627
export type { MigrationArgs, MigrationResult, MigrationStatus };
2728

28-
import { ConvexError, type GenericId } from "convex/values";
29+
import { ConvexError, v, type GenericId } from "convex/values";
2930
import type { ComponentApi } from "../component/_generated/component.js";
3031
import { logStatusAndInstructions } from "./log.js";
3132
import type { MigrationFunctionHandle } from "../component/lib.js";
@@ -124,8 +125,11 @@ export class Migrations<DataModel extends GenericDataModel> {
124125
| MigrationFunctionReference
125126
| MigrationFunctionReference[],
126127
) {
127-
return internalMutationGeneric({
128-
args: migrationArgs,
128+
return internalActionGeneric({
129+
args: {
130+
...migrationArgs,
131+
inline: v.optional(v.boolean()),
132+
},
129133
handler: async (ctx, args) => {
130134
const [specificMigration, next] = Array.isArray(
131135
specificMigrationOrSeries,
@@ -157,9 +161,9 @@ export class Migrations<DataModel extends GenericDataModel> {
157161

158162
private async _runInteractive(
159163
ctx: MutationCtx | ActionCtx,
160-
args: MigrationArgs,
164+
args: MigrationArgs & { inline?: boolean },
161165
fnRef?: MigrationFunctionReference,
162-
next?: { name: string; fnHandle: string }[],
166+
next?: { name: string; fnHandle: MigrationFunctionHandle }[],
163167
) {
164168
const name = args.fn ? this.prefixedName(args.fn) : getFunctionName(fnRef!);
165169
async function makeFn(fn: string) {
@@ -190,14 +194,34 @@ export class Migrations<DataModel extends GenericDataModel> {
190194
}
191195
let status: MigrationStatus;
192196
try {
193-
status = await ctx.runMutation(this.component.lib.migrate, {
194-
name,
195-
fnHandle,
196-
cursor: args.cursor,
197-
batchSize: args.batchSize,
198-
next,
199-
dryRun: args.dryRun ?? false,
200-
});
197+
if (args.inline) {
198+
if (!("runAction" in ctx)) {
199+
throw new Error("Cannot run inline migration from a mutation");
200+
}
201+
return await _runToCompletionInline(ctx, this.component, [
202+
{
203+
fnHandle,
204+
name,
205+
batchSize: args.batchSize,
206+
cursor: args.cursor,
207+
dryRun: args.dryRun,
208+
},
209+
...(next ?? []).map(({ name, fnHandle }) => ({
210+
fnHandle,
211+
name,
212+
dryRun: args.dryRun,
213+
})),
214+
]);
215+
} else {
216+
status = await ctx.runMutation(this.component.lib.migrate, {
217+
name,
218+
fnHandle,
219+
cursor: args.cursor,
220+
batchSize: args.batchSize,
221+
next,
222+
dryRun: args.dryRun ?? false,
223+
});
224+
}
201225
} catch (e) {
202226
if (
203227
args.dryRun &&
@@ -640,28 +664,7 @@ export async function runToCompletion(
640664
ctx: ActionCtx,
641665
component: ComponentApi,
642666
fnRef: MigrationFunctionReference | MigrationFunctionHandle,
643-
opts?: {
644-
/**
645-
* The name of the migration function, generated with getFunctionName.
646-
*/
647-
name?: string;
648-
/**
649-
* The cursor to start from.
650-
* null: start from the beginning.
651-
* undefined: start, or resume from where it failed. No-ops if already done.
652-
*/
653-
cursor?: string | null;
654-
/**
655-
* The number of documents to process in a batch.
656-
* Overrides the migrations's configured batch size.
657-
*/
658-
batchSize?: number;
659-
/**
660-
* If true, it will run a batch and then throw an error.
661-
* It's helpful to see what it would do without committing the transaction.
662-
*/
663-
dryRun?: boolean;
664-
},
667+
opts?: RunToCompletionOptions,
665668
): Promise<MigrationStatus> {
666669
let cursor = opts?.cursor;
667670
const {
@@ -697,6 +700,71 @@ export async function runToCompletion(
697700
}
698701
}
699702

703+
type RunToCompletionOptions = {
704+
/**
705+
* The name of the migration function, generated with getFunctionName.
706+
*/
707+
name?: string;
708+
/**
709+
* The cursor to start from.
710+
* null: start from the beginning.
711+
* undefined: start, or resume from where it failed. No-ops if already done.
712+
*/
713+
cursor?: string | null;
714+
/**
715+
* The number of documents to process in a batch.
716+
* Overrides the migrations's configured batch size.
717+
*/
718+
batchSize?: number;
719+
/**
720+
* If true, it will run a batch and then throw an error.
721+
* It's helpful to see what it would do without committing the transaction.
722+
*/
723+
dryRun?: boolean;
724+
};
725+
726+
async function _runToCompletionInline(
727+
ctx: ActionCtx,
728+
component: ComponentApi,
729+
migrations: (RunToCompletionOptions & {
730+
name: string;
731+
fnHandle: MigrationFunctionHandle;
732+
})[],
733+
) {
734+
console.warn(
735+
`Running migration${
736+
migrations.length > 1
737+
? "s " + migrations.map((m) => m.name).join(", ")
738+
: " " + migrations[0].name
739+
} inline. ` +
740+
"Note: If this action times out, transiently fails, or is canceled, the migration will not continue.",
741+
);
742+
const totalStatus: Record<string, Record<string, unknown>> = {};
743+
for (const { fnHandle, ...args } of migrations) {
744+
const { name } = args;
745+
if (migrations.length > 1) {
746+
console.log("Starting ", name);
747+
}
748+
const status = await runToCompletion(ctx, component, fnHandle, args);
749+
const {
750+
toCancel: _1,
751+
toMonitorStatus: _2,
752+
toStartOver: _3,
753+
...log
754+
} = logStatusAndInstructions(name, status, args);
755+
totalStatus[name] = log;
756+
if (migrations.length > 1) {
757+
console.log(
758+
totalStatus[name] +
759+
": " +
760+
totalStatus[name].Status +
761+
`(${status.processed} documents processed)`,
762+
);
763+
}
764+
}
765+
return totalStatus;
766+
}
767+
700768
/* Type utils follow */
701769

702770
type QueryCtx = Pick<GenericQueryCtx<GenericDataModel>, "runQuery">;

0 commit comments

Comments
 (0)