-
Notifications
You must be signed in to change notification settings - Fork 3
feat(mongo-migration): add hand-authored migration authoring #344
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
0fe8d29
Add mongo-migration-authoring project spec and design reference
wmadden 199e307
Add Mongo migration factory functions
wmadden 59e73cb
Add target-agnostic Migration base class
wmadden b3cbc56
Add Mongo-specific Migration alias
wmadden 16bd779
Add validatedCollection strategy and end-to-end tests
wmadden a65957f
Document migration authoring in target-mongo README
wmadden a49285c
Add migration.json output to Migration base class
wmadden d12b3e6
Move MongoMigration from target-mongo to family-mongo
wmadden f2d6276
Add E2E tests for migration authoring against real MongoDB
wmadden e1307cd
Update READMEs to reflect migration authoring entrypoint move
wmadden f7632fe
Add hand-authored TS migration to mongo-demo
wmadden b4d7f9d
Add hand-authored TS migration to retail-store
wmadden a797692
fix(mongo-migration): add override modifier to Migration subclass met…
wmadden 04ef6e8
feat(migration): print confirmation after writing migration files
wmadden b14e769
docs: add specs for migration subsystem refactor and planner dual output
wmadden 07fb8bd
Add plan.md
wmadden bc3daf7
fix(mongo-migration): address code review findings F01-F06
wmadden 987f466
docs: update spec to reflect implementation decisions
wmadden 2a174b7
refactor(mongo-migration): replace collMod with setValidation, merge …
wmadden be9e596
fix(migration): improve Migration.run() API and add describe() valida…
wmadden 3c02233
fix(demo-tests): use static imports and load artifacts from disk
wmadden 1a5a527
refactor(migration): pass class constructor to Migration.run() instea…
wmadden 7096833
fix(target-mongo): narrow setValidation option types to match CollMod…
wmadden 41626d3
fix(integration): fix typecheck errors in migration-authoring E2E test
wmadden d5c7a78
fix(examples): fix typecheck errors in mongo-demo and retail-store tests
wmadden 67327c5
docs(mongo-migration-authoring): add data migrations spec and plan
wmadden 00d2d8b
fix(migration-tools): increase subprocess test timeout for CI
wmadden ae9bf64
docs(mongo-migration-authoring): rewrite data migrations spec for cla…
wmadden e8830a3
docs(data-migrations): use typed filter proxy in check example
wmadden d42276f
fix(target-mongo): preserve explicit unique:false in createIndex factory
wmadden 4ecabe1
fix(integration): use Promise.allSettled in afterAll teardown
wmadden eabd838
Merge branch 'main' into mongo-manual-migrations
wmadden File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
8 changes: 8 additions & 0 deletions
8
examples/mongo-demo/migrations/20260415_add-posts-author-index/migration.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| { | ||
| "migrationId": null, | ||
| "from": "sha256:358522152ebe3ca9db3d573471c656778c1845f4cdd424caf06632352b9772fe", | ||
| "to": "sha256:358522152ebe3ca9db3d573471c656778c1845f4cdd424caf06632352b9772fe", | ||
| "kind": "regular", | ||
| "labels": ["add-posts-author-index"], | ||
| "createdAt": "2026-04-15T17:17:30.570Z" | ||
| } |
25 changes: 25 additions & 0 deletions
25
examples/mongo-demo/migrations/20260415_add-posts-author-index/migration.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { Migration } from '@prisma-next/family-mongo/migration'; | ||
| import { createIndex } from '@prisma-next/target-mongo/migration'; | ||
|
|
||
| class AddPostsAuthorIndex extends Migration { | ||
| override describe() { | ||
| return { | ||
| from: 'sha256:358522152ebe3ca9db3d573471c656778c1845f4cdd424caf06632352b9772fe', | ||
| to: 'sha256:358522152ebe3ca9db3d573471c656778c1845f4cdd424caf06632352b9772fe', | ||
| labels: ['add-posts-author-index'], | ||
| }; | ||
| } | ||
|
|
||
| override plan() { | ||
| return [ | ||
| createIndex('posts', [{ field: 'authorId', direction: 1 }]), | ||
| createIndex('posts', [ | ||
| { field: 'createdAt', direction: -1 }, | ||
| { field: 'authorId', direction: 1 }, | ||
| ]), | ||
| ]; | ||
| } | ||
| } | ||
|
|
||
| export default AddPostsAuthorIndex; | ||
| Migration.run(import.meta.url, AddPostsAuthorIndex); |
122 changes: 122 additions & 0 deletions
122
examples/mongo-demo/migrations/20260415_add-posts-author-index/ops.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| [ | ||
| { | ||
| "id": "index.posts.create(authorId:1)", | ||
| "label": "Create index on posts (authorId:1)", | ||
| "operationClass": "additive", | ||
| "precheck": [ | ||
| { | ||
| "description": "index does not already exist on posts", | ||
| "source": { | ||
| "kind": "listIndexes", | ||
| "collection": "posts" | ||
| }, | ||
| "filter": { | ||
| "kind": "field", | ||
| "field": "key", | ||
| "op": "$eq", | ||
| "value": { | ||
| "authorId": 1 | ||
| } | ||
| }, | ||
| "expect": "notExists" | ||
| } | ||
| ], | ||
| "execute": [ | ||
| { | ||
| "description": "create index on posts", | ||
| "command": { | ||
| "kind": "createIndex", | ||
| "collection": "posts", | ||
| "keys": [ | ||
| { | ||
| "field": "authorId", | ||
| "direction": 1 | ||
| } | ||
| ], | ||
| "name": "authorId_1" | ||
| } | ||
| } | ||
| ], | ||
| "postcheck": [ | ||
| { | ||
| "description": "index exists on posts", | ||
| "source": { | ||
| "kind": "listIndexes", | ||
| "collection": "posts" | ||
| }, | ||
| "filter": { | ||
| "kind": "field", | ||
| "field": "key", | ||
| "op": "$eq", | ||
| "value": { | ||
| "authorId": 1 | ||
| } | ||
| }, | ||
| "expect": "exists" | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "id": "index.posts.create(createdAt:-1,authorId:1)", | ||
| "label": "Create index on posts (createdAt:-1, authorId:1)", | ||
| "operationClass": "additive", | ||
| "precheck": [ | ||
| { | ||
| "description": "index does not already exist on posts", | ||
| "source": { | ||
| "kind": "listIndexes", | ||
| "collection": "posts" | ||
| }, | ||
| "filter": { | ||
| "kind": "field", | ||
| "field": "key", | ||
| "op": "$eq", | ||
| "value": { | ||
| "createdAt": -1, | ||
| "authorId": 1 | ||
| } | ||
| }, | ||
| "expect": "notExists" | ||
| } | ||
| ], | ||
| "execute": [ | ||
| { | ||
| "description": "create index on posts", | ||
| "command": { | ||
| "kind": "createIndex", | ||
| "collection": "posts", | ||
| "keys": [ | ||
| { | ||
| "field": "createdAt", | ||
| "direction": -1 | ||
| }, | ||
| { | ||
| "field": "authorId", | ||
| "direction": 1 | ||
| } | ||
| ], | ||
| "name": "createdAt_-1_authorId_1" | ||
| } | ||
| } | ||
| ], | ||
| "postcheck": [ | ||
| { | ||
| "description": "index exists on posts", | ||
| "source": { | ||
| "kind": "listIndexes", | ||
| "collection": "posts" | ||
| }, | ||
| "filter": { | ||
| "kind": "field", | ||
| "field": "key", | ||
| "op": "$eq", | ||
| "value": { | ||
| "createdAt": -1, | ||
| "authorId": 1 | ||
| } | ||
| }, | ||
| "expect": "exists" | ||
| } | ||
| ] | ||
| } | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| import { readFileSync } from 'node:fs'; | ||
| import { deserializeMongoOps, MongoMigrationRunner } from '@prisma-next/adapter-mongo/control'; | ||
| import mongoControlDriver from '@prisma-next/driver-mongo/control'; | ||
| import { timeouts } from '@prisma-next/test-utils'; | ||
| import { type Db, MongoClient } from 'mongodb'; | ||
| import { MongoMemoryReplSet } from 'mongodb-memory-server'; | ||
| import { resolve } from 'pathe'; | ||
| import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; | ||
| import AddPostsAuthorIndex from '../migrations/20260415_add-posts-author-index/migration'; | ||
|
|
||
| const ALL_POLICY = { | ||
| allowedOperationClasses: ['additive', 'widening', 'destructive'] as const, | ||
| }; | ||
|
|
||
| const migrationDir = resolve(import.meta.dirname, '../migrations/20260415_add-posts-author-index'); | ||
|
|
||
| describe( | ||
| 'hand-authored migration (20260415_add-posts-author-index)', | ||
| { timeout: timeouts.spinUpMongoMemoryServer }, | ||
| () => { | ||
| let replSet: MongoMemoryReplSet; | ||
| let client: MongoClient; | ||
| let db: Db; | ||
| const dbName = 'manual_migration_test'; | ||
|
|
||
| beforeAll(async () => { | ||
| replSet = await MongoMemoryReplSet.create({ | ||
| instanceOpts: [ | ||
| { launchTimeout: timeouts.spinUpMongoMemoryServer, storageEngine: 'wiredTiger' }, | ||
| ], | ||
| replSet: { count: 1, storageEngine: 'wiredTiger' }, | ||
| }); | ||
| client = new MongoClient(replSet.getUri()); | ||
| await client.connect(); | ||
| db = client.db(dbName); | ||
| }, timeouts.spinUpMongoMemoryServer); | ||
|
|
||
| beforeEach(async () => { | ||
| await db.dropDatabase(); | ||
| }); | ||
|
|
||
| afterAll(async () => { | ||
| try { | ||
| await client?.close(); | ||
| await replSet?.stop(); | ||
| } catch { | ||
| // ignore cleanup errors | ||
| } | ||
| }, timeouts.spinUpMongoMemoryServer); | ||
|
|
||
| it('migration class can be imported and plan() called directly', () => { | ||
| const instance = new AddPostsAuthorIndex(); | ||
| const ops = instance.plan(); | ||
| expect(ops).toHaveLength(2); | ||
| expect(ops[0]!.id).toBe('index.posts.create(authorId:1)'); | ||
| expect(ops[1]!.id).toBe('index.posts.create(createdAt:-1,authorId:1)'); | ||
| }); | ||
|
|
||
| it('migration.json has expected structure', () => { | ||
| const manifest = JSON.parse(readFileSync(resolve(migrationDir, 'migration.json'), 'utf-8')); | ||
|
|
||
| expect(manifest.migrationId).toBeNull(); | ||
| expect(manifest.kind).toBe('regular'); | ||
| expect(manifest.labels).toEqual(['add-posts-author-index']); | ||
| expect(manifest.from).toMatch(/^sha256:/); | ||
| expect(manifest.to).toMatch(/^sha256:/); | ||
| expect(manifest.createdAt).toMatch(/^\d{4}-\d{2}-\d{2}T/); | ||
| }); | ||
|
|
||
| it('ops.json deserializes and applies against real MongoDB', async () => { | ||
| await db.createCollection('posts'); | ||
|
|
||
| const opsJson = readFileSync(resolve(migrationDir, 'ops.json'), 'utf-8'); | ||
| const ops = deserializeMongoOps(JSON.parse(opsJson)); | ||
| expect(ops).toHaveLength(2); | ||
|
|
||
| const controlDriver = await mongoControlDriver.create(replSet.getUri(dbName)); | ||
| try { | ||
| const runner = new MongoMigrationRunner(); | ||
| const result = await runner.execute({ | ||
| plan: { | ||
| targetId: 'mongo', | ||
| destination: { | ||
| storageHash: | ||
| 'sha256:358522152ebe3ca9db3d573471c656778c1845f4cdd424caf06632352b9772fe', | ||
| }, | ||
| operations: JSON.parse(opsJson), | ||
| }, | ||
| driver: controlDriver, | ||
| destinationContract: {}, | ||
| policy: ALL_POLICY, | ||
| frameworkComponents: [], | ||
| }); | ||
|
|
||
| expect(result.ok).toBe(true); | ||
| if (!result.ok) return; | ||
| expect(result.value.operationsExecuted).toBe(2); | ||
|
|
||
| const indexes = await db.collection('posts').listIndexes().toArray(); | ||
|
|
||
| const authorIdIndex = indexes.find( | ||
| (idx) => | ||
| idx['key'] && | ||
| (idx['key'] as Record<string, number>)['authorId'] === 1 && | ||
| !('createdAt' in (idx['key'] as Record<string, number>)), | ||
| ); | ||
| expect(authorIdIndex).toBeDefined(); | ||
|
|
||
| const compoundIndex = indexes.find( | ||
| (idx) => | ||
| idx['key'] && | ||
| (idx['key'] as Record<string, number>)['createdAt'] === -1 && | ||
| (idx['key'] as Record<string, number>)['authorId'] === 1, | ||
| ); | ||
| expect(compoundIndex).toBeDefined(); | ||
| } finally { | ||
| await controlDriver.close(); | ||
| } | ||
| }); | ||
| }, | ||
| ); | ||
8 changes: 8 additions & 0 deletions
8
examples/retail-store/migrations/20260415_add-product-validation/migration.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| { | ||
| "migrationId": null, | ||
| "from": "sha256:e5cfc21670435e53a4af14a665d61d8ba716d5e2e67b63c1443affdcad86985d", | ||
| "to": "sha256:e5cfc21670435e53a4af14a665d61d8ba716d5e2e67b63c1443affdcad86985d", | ||
| "kind": "regular", | ||
| "labels": ["add-product-validation"], | ||
| "createdAt": "2026-04-15T18:46:18.776Z" | ||
| } |
37 changes: 37 additions & 0 deletions
37
examples/retail-store/migrations/20260415_add-product-validation/migration.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import { Migration } from '@prisma-next/family-mongo/migration'; | ||
| import { createIndex, setValidation } from '@prisma-next/target-mongo/migration'; | ||
|
|
||
| class AddProductValidation extends Migration { | ||
| override describe() { | ||
| return { | ||
| from: 'sha256:e5cfc21670435e53a4af14a665d61d8ba716d5e2e67b63c1443affdcad86985d', | ||
| to: 'sha256:e5cfc21670435e53a4af14a665d61d8ba716d5e2e67b63c1443affdcad86985d', | ||
| labels: ['add-product-validation'], | ||
| }; | ||
| } | ||
|
|
||
| override plan() { | ||
| return [ | ||
| setValidation( | ||
| 'products', | ||
| { | ||
| bsonType: 'object', | ||
| required: ['name', 'price', 'category'], | ||
| properties: { | ||
| name: { bsonType: 'string' }, | ||
| price: { bsonType: 'number', minimum: 0 }, | ||
| category: { bsonType: 'string' }, | ||
| }, | ||
| }, | ||
| { validationLevel: 'moderate', validationAction: 'warn' }, | ||
| ), | ||
| createIndex('products', [ | ||
| { field: 'category', direction: 1 }, | ||
| { field: 'price', direction: 1 }, | ||
| ]), | ||
| ]; | ||
| } | ||
| } | ||
|
|
||
| export default AddProductValidation; | ||
| Migration.run(import.meta.url, AddProductValidation); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.