@xixixao/convex-migrations 0.3.1

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 (57) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +523 -0
  3. package/dist/client/index.d.ts +383 -0
  4. package/dist/client/index.d.ts.map +1 -0
  5. package/dist/client/index.js +528 -0
  6. package/dist/client/index.js.map +1 -0
  7. package/dist/client/log.d.ts +8 -0
  8. package/dist/client/log.d.ts.map +1 -0
  9. package/dist/client/log.js +74 -0
  10. package/dist/client/log.js.map +1 -0
  11. package/dist/component/_generated/api.d.ts +34 -0
  12. package/dist/component/_generated/api.d.ts.map +1 -0
  13. package/dist/component/_generated/api.js +31 -0
  14. package/dist/component/_generated/api.js.map +1 -0
  15. package/dist/component/_generated/component.d.ts +95 -0
  16. package/dist/component/_generated/component.d.ts.map +1 -0
  17. package/dist/component/_generated/component.js +11 -0
  18. package/dist/component/_generated/component.js.map +1 -0
  19. package/dist/component/_generated/dataModel.d.ts +46 -0
  20. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  21. package/dist/component/_generated/dataModel.js +11 -0
  22. package/dist/component/_generated/dataModel.js.map +1 -0
  23. package/dist/component/_generated/server.d.ts +121 -0
  24. package/dist/component/_generated/server.d.ts.map +1 -0
  25. package/dist/component/_generated/server.js +78 -0
  26. package/dist/component/_generated/server.js.map +1 -0
  27. package/dist/component/convex.config.d.ts +3 -0
  28. package/dist/component/convex.config.d.ts.map +1 -0
  29. package/dist/component/convex.config.js +3 -0
  30. package/dist/component/convex.config.js.map +1 -0
  31. package/dist/component/lib.d.ts +74 -0
  32. package/dist/component/lib.d.ts.map +1 -0
  33. package/dist/component/lib.js +290 -0
  34. package/dist/component/lib.js.map +1 -0
  35. package/dist/component/schema.d.ts +28 -0
  36. package/dist/component/schema.d.ts.map +1 -0
  37. package/dist/component/schema.js +20 -0
  38. package/dist/component/schema.js.map +1 -0
  39. package/dist/shared.d.ts +40 -0
  40. package/dist/shared.d.ts.map +1 -0
  41. package/dist/shared.js +22 -0
  42. package/dist/shared.js.map +1 -0
  43. package/package.json +95 -0
  44. package/src/client/index.test.ts +16 -0
  45. package/src/client/index.ts +748 -0
  46. package/src/client/log.ts +76 -0
  47. package/src/component/_generated/api.ts +50 -0
  48. package/src/component/_generated/component.ts +116 -0
  49. package/src/component/_generated/dataModel.ts +60 -0
  50. package/src/component/_generated/server.ts +161 -0
  51. package/src/component/convex.config.ts +3 -0
  52. package/src/component/lib.test.ts +110 -0
  53. package/src/component/lib.ts +356 -0
  54. package/src/component/schema.ts +20 -0
  55. package/src/component/setup.test.ts +5 -0
  56. package/src/shared.ts +37 -0
  57. package/src/test.ts +18 -0
package/README.md ADDED
@@ -0,0 +1,523 @@
1
+ # Convex Stateful Migrations Component
2
+
3
+ [![npm version](https://badge.fury.io/js/@convex-dev%2Fmigrations.svg)](https://badge.fury.io/js/@convex-dev%2Fmigrations)
4
+
5
+ <!-- START: Include on https://convex.dev/components -->
6
+
7
+ Define and run migrations, like this one setting a default value for users:
8
+
9
+ ```ts
10
+ export const setDefaultValue = migrations.define({
11
+ table: "users",
12
+ migrateOne: async (ctx, user) => {
13
+ if (user.optionalField === undefined) {
14
+ await ctx.db.patch(user._id, { optionalField: "default" });
15
+ }
16
+ },
17
+ });
18
+ ```
19
+
20
+ You can then run it programmatically or from the CLI. See
21
+ [below](#running-migrations-one-at-a-time).
22
+
23
+ Migrations allow you to define functions that run on all documents in a table
24
+ (or a specified subset). They run in batches asynchronously (online migration).
25
+
26
+ The component tracks the migrations state so it can avoid running twice, pick up
27
+ where it left off (in the case of a bug or failure along the way), and expose
28
+ the migration state in realtime via Convex queries.
29
+
30
+ See the [migration primer post](https://stack.convex.dev/intro-to-migrations)
31
+ for a conceptual overview of online vs. offline migrations. If your migration is
32
+ trivial and you're moving fast, also check out
33
+ [lightweight migrations in the dashboard](https://stack.convex.dev/lightweight-zero-downtime-migrations).
34
+
35
+ Typical steps for doing a migration:
36
+
37
+ 1. Modify your schema to allow old and new values. Typically this is adding a
38
+ new optional field or marking a field as optional so it can be deleted. As
39
+ part of this, update your code to handle both versions.
40
+ 2. Define a migration to change the data to the new schema.
41
+ 3. Push the migration and schema changes.
42
+ 4. Run the migration(s) to completion.
43
+ 5. Modify your schema and code to assume the new value. Pushing this change will
44
+ only succeed once all the data matches the new schema. This is the default
45
+ behavior for Convex, unless you disable schema validation.
46
+
47
+ See [this Stack post](https://stack.convex.dev/migrating-data-with-mutations)
48
+ for walkthroughs of common use cases.
49
+
50
+ ## Pre-requisite: Convex
51
+
52
+ You'll need an existing Convex project to use the component. Convex is a hosted
53
+ backend platform, including a database, serverless functions, and a ton more you
54
+ can learn about [here](https://docs.convex.dev/get-started).
55
+
56
+ Run `npm create convex` or follow any of the
57
+ [quickstarts](https://docs.convex.dev/home) to set one up.
58
+
59
+ ## Installation
60
+
61
+ Install the component package:
62
+
63
+ ```ts
64
+ npm install @convex-dev/migrations
65
+ ```
66
+
67
+ Create a `convex.config.ts` file in your app's `convex/` folder and install the
68
+ component by calling `use`:
69
+
70
+ ```ts
71
+ // convex/convex.config.ts
72
+ import { defineApp } from "convex/server";
73
+ import migrations from "@convex-dev/migrations/convex.config.js";
74
+
75
+ const app = defineApp();
76
+ app.use(migrations);
77
+
78
+ export default app;
79
+ ```
80
+
81
+ ## Initialization
82
+
83
+ Examples below are assuming the code is in `convex/migrations.ts`. This is not a
84
+ requirement. If you want to use a different file, make sure to change the
85
+ examples below from `internal.migrations.*` to your new file name, like
86
+ `internal.myFolder.myMigrationsFile.*` or CLI arguments like `migrations:*` to
87
+ `myFolder/myMigrationsFile:*`.
88
+
89
+ ```ts
90
+ import { Migrations } from "@convex-dev/migrations";
91
+ import { components } from "./_generated/api.js";
92
+ import { DataModel } from "./_generated/dataModel.js";
93
+
94
+ export const migrations = new Migrations<DataModel>(components.migrations);
95
+ export const run = migrations.runner();
96
+ ```
97
+
98
+ The type parameter `DataModel` is optional. It provides type safety for
99
+ migration definitions. As always, database operations in migrations will abide
100
+ by your schema definition at runtime. **Note**: if you use
101
+ [custom functions](https://stack.convex.dev/custom-functions) to override
102
+ `internalMutation`, see
103
+ [below](#override-the-internalmutation-to-apply-custom-db-behavior).
104
+
105
+ ## Define migrations
106
+
107
+ Within the `migrateOne` function, you can write code to modify a single document
108
+ in the specified table. Making changes is optional, and you can also read and
109
+ write to other tables from this function.
110
+
111
+ ```ts
112
+ export const setDefaultValue = migrations.define({
113
+ table: "myTable",
114
+ migrateOne: async (ctx, doc) => {
115
+ if (doc.optionalField === undefined) {
116
+ await ctx.db.patch(doc._id, { optionalField: "default" });
117
+ }
118
+ },
119
+ });
120
+ ```
121
+
122
+ ### Shorthand syntax
123
+
124
+ Since the most common migration involves patching each document, if you return
125
+ an object, it will be applied as a patch automatically.
126
+
127
+ ```ts
128
+ export const clearField = migrations.define({
129
+ table: "myTable",
130
+ migrateOne: () => ({ optionalField: undefined }),
131
+ });
132
+ // is equivalent to `await ctx.db.patch(doc._id, { optionalField: undefined })`
133
+ ```
134
+
135
+ ### Runtime migration arguments
136
+
137
+ If you need to configure a migration at run time, define validated args and
138
+ consume them in `migrateOne`.
139
+
140
+ ```ts
141
+ export const deleteByTag = migrations.define({
142
+ table: "events",
143
+ args: v.object({ tag: v.string() }),
144
+ migrateOne: async (ctx, doc, args) => {
145
+ if (doc.tags.includes(args.tag)) {
146
+ await ctx.db.delete(doc._id);
147
+ }
148
+ },
149
+ });
150
+ ```
151
+
152
+ Then pass `args` when starting the migration:
153
+
154
+ ```sh
155
+ npx convex run migrations:run '{"fn": "migrations:deleteByTag", "args": {"tag": "important"}}'
156
+ ```
157
+
158
+ ### Migrating a subset of a table using an index
159
+
160
+ If you only want to migrate a range of documents, you can avoid processing the
161
+ whole table by specifying a `customRange`. You can use any existing index you
162
+ have on the table, or the built-in `by_creation_time` index. The `customRange`
163
+ callback receives `(query, args)` so you can parameterize the range using
164
+ migration args passed from the CLI or runner.
165
+
166
+ ```ts
167
+ export const validateRequiredField = migrations.define({
168
+ table: "myTable",
169
+ customRange: (query, _args) =>
170
+ query.withIndex("by_requiredField", (q) => q.eq("requiredField", "")),
171
+ migrateOne: async (_ctx, doc) => {
172
+ console.log("Needs fixup: " + doc._id);
173
+ // Shorthand for patching
174
+ return { requiredField: "<unknown>" };
175
+ },
176
+ });
177
+ ```
178
+
179
+ ## Running migrations one at a time
180
+
181
+ ### Using the Dashboard or CLI
182
+
183
+ To define a one-off function to run a single migration, pass a reference to it:
184
+
185
+ ```ts
186
+ export const runIt = migrations.runner(internal.migrations.setDefaultValue);
187
+ ```
188
+
189
+ To run it from the CLI:
190
+
191
+ ```sh
192
+ npx convex run convex/migrations.ts:runIt # or shorthand: migrations:runIt
193
+ ```
194
+
195
+ **Note**: pass the `--prod` argument to run this and below commands in
196
+ production
197
+
198
+ Running it from the dashboard:
199
+
200
+ ![Dashboard screenshot](./dashboard_screenshot.png)
201
+
202
+ You can also expose a general-purpose function to run your migrations. For
203
+ example, in `convex/migrations.ts` add:
204
+
205
+ ```ts
206
+ export const run = migrations.runner();
207
+ ```
208
+
209
+ Then run it with the
210
+ [function name](https://docs.convex.dev/functions/query-functions#query-names):
211
+
212
+ ```sh
213
+ npx convex run migrations:run '{fn: "migrations:setDefaultValue"}'
214
+ ```
215
+
216
+ See [below](#shorthand-running-syntax) for a way to just pass `setDefaultValue`.
217
+
218
+ ### Programmatically
219
+
220
+ You can also run migrations from other Convex mutations or actions:
221
+
222
+ ```ts
223
+ await migrations.runOne(ctx, internal.example.setDefaultValue);
224
+ ```
225
+
226
+ ### Behavior
227
+
228
+ - If it is already running it will refuse to start another duplicate worker.
229
+ - If it had previously failed on some batch, it will continue from that batch
230
+ unless you manually specify `cursor`.
231
+ - If you provide an explicit `cursor` (`null` means to start at the beginning),
232
+ it will start from there.
233
+ - If you pass `true` for `dryRun` then it will run one batch and then throw, so
234
+ no changes are committed, and you can see what it would have done. See
235
+ [below](#test-a-migration-with-dryrun) This is good for validating it does
236
+ what you expect.
237
+
238
+ ## Running migrations serially
239
+
240
+ You can run a series of migrations in order. This is useful if some migrations
241
+ depend on previous ones, or if you keep a running list of all migrations that
242
+ should run on the next deployment.
243
+
244
+ ### Using the Dashboard or CLI
245
+
246
+ You can also pass a list of migrations to `runner` to have it run a series of
247
+ migrations instead of just one:
248
+
249
+ ```ts
250
+ export const runAll = migrations.runner([
251
+ internal.migrations.setDefaultValue,
252
+ internal.migrations.validateRequiredField,
253
+ internal.migrations.convertUnionField,
254
+ ]);
255
+ ```
256
+
257
+ Then just run:
258
+
259
+ ```sh
260
+ npx convex run migrations:runAll # migrations:runAll is equivalent to convex/migrations.ts:runAll on the CLI
261
+ ```
262
+
263
+ With the `runner` functions, you can pass a "next" argument to run a series of
264
+ migrations after the first:
265
+
266
+ ```sh
267
+ npx convex run migrations:runIt '{next:["migrations:clearField"]}'
268
+ # OR
269
+ npx convex run migrations:run '{fn: "migrations:setDefaultValue", next:["migrations:clearField"]}'
270
+ ```
271
+
272
+ ### Programmatically
273
+
274
+ ```ts
275
+ await migrations.runSerially(ctx, [
276
+ internal.migrations.setDefaultValue,
277
+ internal.migrations.validateRequiredField,
278
+ internal.migrations.convertUnionField,
279
+ ]);
280
+ ```
281
+
282
+ ### Behavior
283
+
284
+ - If a migration is already in progress when attempted, it will no-op.
285
+ - If a migration had already completed, it will skip it.
286
+ - If a migration had partial progress, it will resume from where it left off.
287
+ - If a migration fails or is canceled, it will not continue on, in case you had
288
+ some dependencies between the migrations. Call the series again to retry.
289
+
290
+ Note: if you start multiple serial migrations, the behavior is:
291
+
292
+ - If they don't overlap on functions, they will happily run in parallel.
293
+ - If they have a function in common and one completes before the other attempts
294
+ it, the second will just skip it.
295
+ - If they have a function in common and one is in progress, the second will
296
+ no-op and not run any further migrations in its series.
297
+
298
+ ## Operations
299
+
300
+ ### Test a migration with dryRun
301
+
302
+ Before running a migration that may irreversibly change data, you can validate a
303
+ batch by passing `dryRun` to any `runner` or `runOne` command:
304
+
305
+ ```sh
306
+ npx convex run migrations:runIt '{dryRun: true}'
307
+ ```
308
+
309
+ ### Restart a migration
310
+
311
+ Pass `null` for the `cursor` to force a migration to start over.
312
+
313
+ ```sh
314
+ npx convex run migrations:runIt '{cursor: null}'
315
+ ```
316
+
317
+ You can also pass in any valid cursor to start from. You can find valid cursors
318
+ in the response of calls to `getStatus`. This can allow retrying a migration
319
+ from a known good point as you iterate on the code.
320
+
321
+ ### Stop a migration
322
+
323
+ You can stop a migration from the CLI or dashboard, calling the component API
324
+ directly:
325
+
326
+ ```sh
327
+ npx convex run --component migrations lib:cancel '{name: "migrations:myMigration"}'
328
+ ```
329
+
330
+ Or via `migrations.cancel` programatically.
331
+
332
+ ```ts
333
+ await migrations.cancel(ctx, internal.migrations.myMigration);
334
+ ```
335
+
336
+ ### Get the status of migrations
337
+
338
+ To see the live status of migrations as they progress, you can query it via the
339
+ CLI:
340
+
341
+ ```sh
342
+ npx convex run --component migrations lib:getStatus --watch
343
+ ```
344
+
345
+ The `--watch` will live-update the status as it changes. Or programmatically:
346
+
347
+ ```ts
348
+ const status: MigrationStatus[] = await migrations.getStatus(ctx, {
349
+ limit: 10,
350
+ });
351
+ // or
352
+ const status: MigrationStatus[] = await migrations.getStatus(ctx, {
353
+ migrations: [
354
+ internal.migrations.setDefaultValue,
355
+ internal.migrations.validateRequiredField,
356
+ internal.migrations.convertUnionField,
357
+ ],
358
+ });
359
+ ```
360
+
361
+ The type is annotated to avoid circular type dependencies, for instance if you
362
+ are returning the result from a query that is defined in the same file as the
363
+ referenced migrations.
364
+
365
+ ### Running migrations as part of a production deploy
366
+
367
+ As part of your build and deploy command, you can chain the corresponding
368
+ `npx convex run` command, such as:
369
+
370
+ ```sh
371
+ npx convex deploy --cmd 'npm run build' && npx convex run convex/migrations.ts:runAll --prod
372
+ ```
373
+
374
+ ## Configuration options
375
+
376
+ ### Override the internalMutation to apply custom DB behavior
377
+
378
+ You can customize which `internalMutation` implementation the underly migration
379
+ should use.
380
+
381
+ This might be important if you use
382
+ [custom functions](https://stack.convex.dev/custom-functions) to intercept
383
+ database writes to apply validation or
384
+ [trigger operations on changes](https://stack.convex.dev/triggers).
385
+
386
+ Assuming you define your own `internalMutation` in `convex/functions.ts`:
387
+
388
+ ```ts
389
+ import { internalMutation } from "./functions";
390
+ import { Migrations } from "@convex-dev/migrations";
391
+ import { components } from "./_generated/api";
392
+
393
+ export const migrations = new Migrations(components.migrations, {
394
+ internalMutation,
395
+ });
396
+ ```
397
+
398
+ See [this article](https://stack.convex.dev/migrating-data-with-mutations) for
399
+ more information on usage and advanced patterns.
400
+
401
+ ### Custom batch size
402
+
403
+ The component will fetch your data in batches of 100, and call your function on
404
+ each document in a batch. If you want to change the batch size, you can specify
405
+ it. This can be useful if your documents are large, to avoid running over the
406
+ [transaction limit](https://docs.convex.dev/production/state/limits#transactions),
407
+ or if your documents are updating frequently and you are seeing OCC conflicts
408
+ while migrating.
409
+
410
+ ```ts
411
+ export const clearField = migrations.define({
412
+ table: "myTable",
413
+ batchSize: 10,
414
+ migrateOne: () => ({ optionalField: undefined }),
415
+ });
416
+ ```
417
+
418
+ You can also override this batch size for an individual invocation:
419
+
420
+ ```ts
421
+ await migrations.runOne(ctx, internal.migrations.clearField, {
422
+ batchSize: 1,
423
+ });
424
+ ```
425
+
426
+ ### Parallelizing batches
427
+
428
+ Each batch is processed serially, but within a batch you can have each
429
+ `migrateOne` call run in parallel if you pass `parallelize: true`. If you do so,
430
+ ensure your callback doesn't assume that each call is isolated. For instance, if
431
+ each call reads then updates the same counter, then multiple functions in the
432
+ same batch could read the same counter value, and get off by one. As a result,
433
+ migrations are run serially by default.
434
+
435
+ ```ts
436
+ export const clearField = migrations.define({
437
+ table: "myTable",
438
+ parallelize: true,
439
+ migrateOne: () => ({ optionalField: undefined }),
440
+ });
441
+ ```
442
+
443
+ ### Shorthand running syntax:
444
+
445
+ For those that don't want to type out `migrations:myNewMigration` every time
446
+ they run a migration from the CLI or dashboard, especially if you define your
447
+ migrations elsewhere like `ops/db/migrations:myNewMigration`, you can configure
448
+ a prefix:
449
+
450
+ ```ts
451
+ export const migrations = new Migrations(components.migrations, {
452
+ internalMigration,
453
+ migrationsLocationPrefix: "migrations:",
454
+ });
455
+ ```
456
+
457
+ And then just call:
458
+
459
+ ```sh
460
+ npx convex run migrations:run '{fn: "myNewMutation", next: ["myNextMutation"]}'
461
+ ```
462
+
463
+ Or in code:
464
+
465
+ ```ts
466
+ await migrations.getStatus(ctx, { migrations: ["myNewMutation"] });
467
+ await migrations.cancel(ctx, "myNewMutation");
468
+ ```
469
+
470
+ ## Running migrations synchronously
471
+
472
+ If you want to run a migration synchronously from a test or action, you can use
473
+ `runToCompletion`. Note that if the action crashes or is canceled, it will not
474
+ continue migrating in the background.
475
+
476
+ From an action:
477
+
478
+ ```ts
479
+ import { components, internal } from "./_generated/api";
480
+ import { internalAction } from "./_generated/server";
481
+ import { runToCompletion } from "@convex-dev/migrations";
482
+
483
+ export const myAction = internalAction({
484
+ args: {},
485
+ handler: async (ctx) => {
486
+ //...
487
+ const toRun = internal.example.setDefaultValue;
488
+ await runToCompletion(ctx, components.migrations, toRun);
489
+ },
490
+ });
491
+ ```
492
+
493
+ In a test:
494
+
495
+ ```ts
496
+ import { test } from "vitest";
497
+ import { convexTest } from "convex-test";
498
+ import component from "@convex-dev/migrations/test";
499
+ import { runToCompletion } from "@convex-dev/migrations";
500
+ import { components, internal } from "./_generated/api";
501
+ import schema from "./schema";
502
+
503
+ test("test setDefaultValue migration", async () => {
504
+ const t = convexTest(schema);
505
+ // Register the component in the test instance
506
+ component.register(t);
507
+
508
+ await t.run(async (ctx) => {
509
+ // Add sample data to migrate
510
+ await ctx.db.insert("myTable", { optionalField: undefined });
511
+
512
+ // Run the migration to completion
513
+ const migrationToTest = internal.example.setDefaultValue;
514
+ await runToCompletion(ctx, components.migrations, migrationToTest);
515
+
516
+ // Assert that the migration was successful by checking the data
517
+ const docs = await ctx.db.query("myTable").collect();
518
+ expect(docs.every((doc) => doc.optionalField !== undefined)).toBe(true);
519
+ });
520
+ });
521
+ ```
522
+
523
+ <!-- END: Include on https://convex.dev/components -->