convex-helpers
Version:
A collection of useful code to complement the official convex package.
303 lines (286 loc) • 13.5 kB
TypeScript
/**
* A helper to run migrations over all documents in a table.
*
* This helper allows you to:
*
* - Define a function to migrate one document, and run that function over
* all documents in a table, in batch.
* - Run migrations manually from the CLI or dashboard.
* - Run migrations directly from a function.
* - Run many migrations in series from a function or CLI / dashboard.
* - Get the status of a migration.
* - Resume a migration from where it left off. E.g. if you read too much data
* in a batch you can start it over with a smaller batch size.
* - Cancel an in-progress migration.
* - Run a dry run to see what the migration would do without committing.
* - Start a migration from an explicit cursor.
*
## Usage:
In `convex/schema.ts` (if you want persistence):
```ts
// In convex/schema.ts
import { migrationsTable } from "convex-helpers/server/migrations";
export default defineSchema({
migrations: migrationsTable,
// other tables...
});
```
You can pick any table name for this, but it should match `migrationTable` used below.
In `convex/migrations.ts` (or wherever you want to define them):
```ts
import { makeMigration } from "convex-helpers/server/migrations";
import { internalMutation } from "./_generated/server";
const migration = makeMigration(internalMutation, {
migrationTable: "migrations",
});
export const myMigration = migration({
table: "users",
migrateOne: async (ctx, doc) => {
await ctx.db.patch(doc._id, { newField: "value" });
},
});
```
To run from the CLI / dashboard:
You can run this manually from the CLI or dashboard:
```sh
# Start or resume a migration. No-ops if it's already done:
npx convex run migrations:myMigration '{fn: "migrations:myMigration"}'
```
Or call it directly within a function:
```ts
import { startMigration } from "convex-helpers/server/migrations";
//... within a mutation or action
await startMigration(ctx, internal.migrations.myMigration, {
startCursor: null, // optional override
batchSize: 10, // optional override
});
```
Or define many to run in series (skips already completed migrations / rows):
```ts
import { startMigrationsSerially } from "convex-helpers/server/migrations";
import { internalMutation } from "./_generated/server";
export default internalMutation(async (ctx) => {
await startMigrationsSerially(ctx, [
internal.migrations.myMigration,
internal.migrations.myOtherMigration,
//...
]);
});
```
If this default export is in `convex/migrations.ts` you can run:
```sh
npx convex run migrations --prod
```
*
* Ideas for the future:
* - Allow scheduling multiple batches at once. Maybe partition by time.
*/
import type { DocumentByInfo, DocumentByName, FunctionReference, GenericDatabaseReader, GenericDataModel, GenericMutationCtx, MutationBuilder, NamedTableInfo, OrderedQuery, QueryInitializer, RegisteredMutation, Scheduler, TableNamesInDataModel } from "convex/server";
import type { GenericId, ObjectType } from "convex/values";
import type { ErrorMessage } from "../index.js";
export declare const DEFAULT_BATCH_SIZE = 100;
declare const migrationsFields: {
name: import("convex/values").VString<string, "required">;
table: import("convex/values").VString<string, "required">;
cursor: import("convex/values").VUnion<string | null, [import("convex/values").VString<string, "required">, import("convex/values").VNull<null, "required">], "required", never>;
isDone: import("convex/values").VBoolean<boolean, "required">;
workerId: import("convex/values").VId<GenericId<"_scheduled_functions"> | undefined, "optional">;
processed: import("convex/values").VFloat64<number, "required">;
latestStart: import("convex/values").VFloat64<number, "required">;
latestEnd: import("convex/values").VFloat64<number | undefined, "optional">;
};
type MigrationMetadata = ObjectType<typeof migrationsFields>;
type MigrationMetadataDoc<TableName extends string> = MigrationMetadata & {
_id: GenericId<TableName>;
_creationTime: number;
};
export declare const migrationsTable: import("convex/server").TableDefinition<import("convex/values").VObject<{
workerId?: GenericId<"_scheduled_functions"> | undefined;
latestEnd?: number | undefined;
cursor: string | null;
isDone: boolean;
name: string;
table: string;
processed: number;
latestStart: number;
}, {
name: import("convex/values").VString<string, "required">;
table: import("convex/values").VString<string, "required">;
cursor: import("convex/values").VUnion<string | null, [import("convex/values").VString<string, "required">, import("convex/values").VNull<null, "required">], "required", never>;
isDone: import("convex/values").VBoolean<boolean, "required">;
workerId: import("convex/values").VId<GenericId<"_scheduled_functions"> | undefined, "optional">;
processed: import("convex/values").VFloat64<number, "required">;
latestStart: import("convex/values").VFloat64<number, "required">;
latestEnd: import("convex/values").VFloat64<number | undefined, "optional">;
}, "required", "cursor" | "isDone" | "name" | "table" | "workerId" | "processed" | "latestStart" | "latestEnd">, {
name: ["name", "_creationTime"];
}, {}, {}>;
declare const migrationArgs: {
fn: import("convex/values").VString<string, "required">;
cursor: import("convex/values").VUnion<string | null | undefined, [import("convex/values").VString<string, "required">, import("convex/values").VNull<null, "required">], "optional", never>;
batchSize: import("convex/values").VFloat64<number | undefined, "optional">;
next: import("convex/values").VArray<string[] | undefined, import("convex/values").VString<string, "required">, "optional">;
dryRun: import("convex/values").VBoolean<boolean | undefined, "optional">;
};
type MigrationArgs = ObjectType<typeof migrationArgs>;
type MigrationTableNames<DataModel extends GenericDataModel> = {
[K in TableNamesInDataModel<DataModel>]: DocumentByInfo<NamedTableInfo<DataModel, K>> extends MigrationMetadata ? K : ErrorMessage<"Add migrationsTable to your schema">;
}[TableNamesInDataModel<DataModel>];
/**
* Makes the migration wrapper, with types for your own tables.
*
* It will keep track of migration state if you specify a migration table.
* If you don't specify a table, it will not check for active migrations.
* e.g. in your schema:
* ```ts
* import { migrationsTable } from "convex-helpers/server/migrations";
* export default defineSchema({
* migrations: migrationsTable,
* // other tables...
* })
* ```
* And in convex/migrations.ts for example:
* ```ts
* import { makeMigration } from "convex-helpers/server/migrations";
* import { internalMutation } from "./_generated/server";
* const migration = makeMigration(internalMutation, {
* migrationTable: "migrations",
* });
*
* export const myMigration = migration({
* table: "users",
* migrateOne: async (ctx, doc) => {
* await ctx.db.patch(doc._id, { newField: "value" });
* }
* });
* ```
* @param internalMutation - The internal mutation to use for the migration.
* @param opts - For stateful migrations, set migrationTable.
* @param opts.migrationTable - The name of the table you added to your schema,
* importing the migrationTable from this file.
*/
export declare function makeMigration<DataModel extends GenericDataModel, MigrationTable extends MigrationTableNames<DataModel>>(internalMutation: MutationBuilder<DataModel, "internal">, opts?: {
migrationTable?: MigrationTable;
defaultBatchSize?: number;
}): <TableName extends TableNamesInDataModel<DataModel>>({ table, migrateOne, customRange, batchSize: functionDefaultBatchSize, }: {
table: TableName;
migrateOne: (ctx: GenericMutationCtx<DataModel>, doc: DocumentByName<DataModel, TableName>) => void | Partial<DocumentByName<DataModel, TableName>> | Promise<Partial<DocumentByName<DataModel, TableName>> | void>;
customRange?: (q: QueryInitializer<NamedTableInfo<DataModel, TableName>>) => OrderedQuery<NamedTableInfo<DataModel, TableName>>;
batchSize?: number;
}) => RegisteredMutation<"internal", MigrationArgs, Promise<MigrationMetadataDoc<MigrationTable>>>;
/**
* Start a migration from a server function via a function reference.
*
* Overrides any options you passed in, such as resetting the cursor.
* If it's already in progress, it will no-op.
* If you run a migration that had previously failed which was part of a series,
* it will not resume the series.
* To resume a series, call the series again: {@link startMigrationsSerially}.
*
* Note: It's up to you to determine if it's safe to run a migration while
* others are in progress. It won't run multiple instance of the same migration
* but it currently allows running multiple migrations on the same table.
*
* @param ctx ctx from an action or mutation. It only uses the scheduler.
* @param fnRef The migration function to run. Like internal.migrations.foo.
* @param opts Options to start the migration.
* @param opts.startCursor The cursor to start from.
* null: start from the beginning.
* undefined: start or resume from where it failed. If done, it won't restart.
* @param opts.batchSize The number of documents to process in a batch.
* @param opts.dryRun If true, it will run a batch and then throw an error.
* It's helpful to see what it would do without committing the transaction.
*/
export declare function startMigration(ctx: {
scheduler: Scheduler;
}, fnRef: FunctionReference<"mutation", "internal", MigrationArgs>, opts?: {
startCursor?: string | null;
batchSize?: number;
dryRun?: boolean;
}): Promise<void>;
/**
* Start a series of migrations, running one a time. Each call starts a series.
*
* If a migration has previously completed it will skip it.
* If a migration had partial progress, it will resume from where it left off.
* If a migration is already in progress when attempted, it will no-op.
* If a migration fails or is canceled, it will stop executing and NOT execute
* any subsequent migrations in the series. Call the series again to retry.
*
* This is useful to run as an post-deploy script where you specify all the
* live migrations that should be run.
*
* Note: if you start multiple serial migrations, the behavior is:
* - If they don't overlap on functions, they will happily run in parallel.
* - If they have a function in common and one completes before the other
* attempts it, the second will just skip it.
* - If they have a function in common and one is in progress, the second will
* no-op and not run any further migrations in its series.
*
* To stop a migration in progress, see {@link cancelMigration}.
*
* @param ctx ctx from an action or mutation. Only needs the scheduler.
* @param fnRefs The migrations to run in order. Like [internal.migrations.foo].
*/
export declare function startMigrationsSerially(ctx: {
scheduler: Scheduler;
}, fnRefs: FunctionReference<"mutation", "internal", MigrationArgs>[]): Promise<void>;
export type MigrationStatus<TableName extends string> = (MigrationMetadataDoc<TableName> | {
name: string;
status: "not found";
workerId: undefined;
isDone: false;
}) & {
workerStatus?: "pending" | "inProgress" | "success" | "failed" | "canceled" | undefined;
batchSize?: any;
next?: any;
};
/**
* Get the status of a migration or all migrations.
* @param ctx Context from a mutation or query. Only needs the db.
* @param migrationTable Where the migration state is stored.
* Should match the argument to {@link makeMigration}, if set.
* @param migrations The migrations to get the status of. Defaults to all.
* @returns The status of the migrations, in the order of the input.
*/
export declare function getStatus<DataModel extends GenericDataModel, MigrationTable extends MigrationTableNames<DataModel>>(ctx: {
db: GenericDatabaseReader<DataModel>;
}, { migrationTable, migrations, limit, }: {
migrationTable: MigrationTable;
migrations?: FunctionReference<"mutation", "internal", MigrationArgs>[];
limit?: number;
}): Promise<MigrationStatus<MigrationTable>[]>;
/**
* Cancels a migration if it's in progress.
* You can resume it later by calling the migration without an explicit cursor.
* If the migration had "next" migrations, e.g. from startMigrationsSerially,
* they will not run. To resume, call the series again or manually pass "next".
* @param ctx Context from a query or mutation. Only needs the db and scheduler.
* @param migrationId Migration to cancel. Get from status or logs.
* @returns The status of the migration after attempting to cancel it.
*/
export declare function cancelMigration<DataModel extends GenericDataModel>(ctx: {
db: GenericDatabaseReader<DataModel>;
scheduler: Scheduler;
}, migrationTable: MigrationTableNames<DataModel>, migration: FunctionReference<"mutation", "internal", MigrationArgs> | string): Promise<{
workerId?: GenericId<"_scheduled_functions"> | undefined;
latestEnd?: number | undefined;
cursor: string | null;
isDone: boolean;
name: string;
table: string;
processed: number;
latestStart: number;
} | {
workerStatus: string;
workerId?: GenericId<"_scheduled_functions"> | undefined;
latestEnd?: number | undefined;
cursor: string | null;
isDone: boolean;
name: string;
table: string;
processed: number;
latestStart: number;
}>;
export {};
//# sourceMappingURL=migrations.d.ts.map