Skip to content

Commit

Permalink
Update PrismaAdapter with new migration support (#4691)
Browse files Browse the repository at this point in the history
  • Loading branch information
timleslie authored Jan 20, 2021
1 parent 6b95cb6 commit fc2b710
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 158 deletions.
5 changes: 5 additions & 0 deletions .changeset/little-tomatoes-eat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystonejs/test-utils': patch
---

Use the PrismaAdapter `prototype` migration mode.
5 changes: 5 additions & 0 deletions .changeset/neat-gifts-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystonejs/adapter-prisma': major
---

Upgraded Prisma to `2.15.0`, which includes the new migration framework. Added the `migrationMode` config option to the `PrismaAdapter` constructor to control how migrations are applied.
2 changes: 1 addition & 1 deletion docs/discussions/prisma.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ The Prisma adapter currently only supports PostgreSQL databases. Future releases

## Migrations

[Prisma Migrate](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-migrate) is currently considered experimental.
[Prisma Migrate](https://www.prisma.io/docs/concepts/components/prisma-migrate) is currently in preview mode.
The Prisma adapter will not be considered production ready until Prisma Migrate is also production ready.

## Field types
Expand Down
8 changes: 7 additions & 1 deletion packages/adapter-prisma/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The Prisma adapter allows Keystone to connect a database using Prisma Client, a

> **Tip:** Want to get started with Keystone + Prisma? [Follow the guide](/docs/guides/prisma.md)!
>
> **Warning:** The Keystone Prisma adapter is not currently production-ready. It depends on the [Prisma Migrate](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-migrate) system which is currently flagged as `EXPERIMENTAL`. Once Prisma Migrate is out of experimental mode, we will release a production-ready version of this package.
> **Warning:** The Keystone Prisma adapter is not currently production-ready. It depends on the [Prisma Migrate](https://www.prisma.io/docs/concepts/components/prisma-migrate) system which is currently flagged as `Preview`. Once Prisma Migrate is out of preview mode, we will release a production-ready version of this package.
>
> **Note:** This adapter currently only supports PostgreSQL databases, and has other limitations. For more details, see our [Prisma Adapter - Production Ready Checklist](/docs/discussions/prisma.md)
Expand Down Expand Up @@ -61,6 +61,12 @@ _**Default:**_ `false`

Allow the adapter to drop the entire database and recreate the tables / foreign keys based on the list schema in your application. This option is ignored in production, i.e. when the environment variable NODE_ENV === 'production'.

### `migrationMode`

_**Default:**_ `'dev'`

Controls how and when migrations are applied. One of `'dev'`, `'prototype'`, `'createOnly'`, or `'none'`. In `prototype` mode, `prisma db push` is used to sync the database tables without generating migration files. In `dev` mode, migrations files are generated and applied whenever the schema changes. In `createOnly` mode, migrations are generated but not applied. In `none` mode, no migrations are generated or applied.

## Setup

Before running Keystone with the Prisma adapter you will need to have a PostgreSQL database to connect to.
Expand Down
95 changes: 50 additions & 45 deletions packages/adapter-prisma/lib/adapter-prisma.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class PrismaAdapter extends BaseKeystoneAdapter {
super(...arguments);
this.name = 'prisma';
this.provider = this.config.provider || 'postgresql';
this.migrationMode = this.config.migrationMode || 'dev';

this.getPrismaPath = this.config.getPrismaPath || (() => '.prisma');
this.getDbSchemaName = this.config.getDbSchemaName || (() => 'public');
Expand Down Expand Up @@ -46,6 +47,12 @@ class PrismaAdapter extends BaseKeystoneAdapter {
});
}

async deploy(rels) {
// Apply any migrations which haven't already been applied
await this._prepareSchema(rels);
this._runPrismaCmd(`migrate deploy --preview-feature`);
}

async _connect({ rels }) {
await this._generateClient(rels);
const { PrismaClient } = require(this.clientPath);
Expand Down Expand Up @@ -75,37 +82,35 @@ class PrismaAdapter extends BaseKeystoneAdapter {
if (fs.existsSync(this.clientPath)) {
const existing = fs.readFileSync(this.schemaPath, { encoding: 'utf-8' });
if (existing === prismaSchema) {
// 2a1. If they're the same, we're golden
} else {
// 2a2. If they're different, generate and run a migration
// Write prisma file
this._writePrismaSchema({ prismaSchema });

// Generate prisma client
await this._generatePrismaClient();

this._saveMigration({ name: `keystone-${cuid()}` });
this._executeMigrations();
// If they're the same, we're golden
return;
}
} else {
this._writePrismaSchema({ prismaSchema });
}
this._writePrismaSchema({ prismaSchema });

// Generate prisma client
await this._generatePrismaClient();
// Generate prisma client
await this._generatePrismaClient();

// Need to generate and run a migration!!!
this._saveMigration({ name: 'init' });
this._executeMigrations();
}
// Run prisma migrations
await this._runMigrations();
}
}

_saveMigration({ name }) {
this._runPrismaCmd(`migrate save --name ${name} --experimental`);
}

_executeMigrations() {
this._runPrismaCmd('migrate up --experimental');
async _runMigrations() {
if (this.migrationMode === 'prototype') {
// Sync the database directly, without generating any migration
this._runPrismaCmd(`db push --force --preview-feature`);
} else if (this.migrationMode === 'createOnly') {
// Generate a migration, but do not apply it
this._runPrismaCmd(`migrate dev --create-only --name keystone-${cuid()} --preview-feature`);
} else if (this.migrationMode === 'dev') {
// Generate and apply a migration if required.
this._runPrismaCmd(`migrate dev --name keystone-${cuid()} --preview-feature`);
} else if (this.migrationMode === 'none') {
// Explicitly disable running any migrations
} else {
throw new Error(`migrationMode must be one of 'dev', 'prototype', 'createOnly', or 'none`);
}
}

async _writePrismaSchema({ prismaSchema }) {
Expand Down Expand Up @@ -211,32 +216,32 @@ class PrismaAdapter extends BaseKeystoneAdapter {

// This will drop all the tables in the backing database. Use wisely.
async dropDatabase() {
let migrationNeeded = true;
if (this.provider === 'postgresql') {
for (const { tablename } of await this.prisma.$queryRaw(
`SELECT tablename FROM pg_tables WHERE schemaname='${this.dbSchemaName}'`
)) {
if (tablename.includes('_Migration')) {
migrationNeeded = false;
if (this.migrationMode === 'prototype') {
if (this.provider === 'postgresql') {
// Special fast path to drop data from a postgres database.
// This is an optimization which is particularly crucial in a unit testing context.
// This code path takes milliseconds, vs ~7 seconds for a migrate reset + db push
for (const { tablename } of await this.prisma.$queryRaw(
`SELECT tablename FROM pg_tables WHERE schemaname='${this.dbSchemaName}'`
)) {
await this.prisma.$queryRaw(
`TRUNCATE TABLE \"${this.dbSchemaName}\".\"${tablename}\" CASCADE;`
);
}
await this.prisma.$queryRaw(
`TRUNCATE TABLE \"${this.dbSchemaName}\".\"${tablename}\" CASCADE;`
);
}
for (const { relname } of await this.prisma.$queryRaw(
`SELECT c.relname FROM pg_class AS c JOIN pg_namespace AS n ON c.relnamespace = n.oid WHERE c.relkind='S' AND n.nspname='${this.dbSchemaName}';`
)) {
if (!relname.includes('_Migration')) {
for (const { relname } of await this.prisma.$queryRaw(
`SELECT c.relname FROM pg_class AS c JOIN pg_namespace AS n ON c.relnamespace = n.oid WHERE c.relkind='S' AND n.nspname='${this.dbSchemaName}';`
)) {
await this.prisma.$queryRaw(
`ALTER SEQUENCE \"${this.dbSchemaName}\".\"${relname}\" RESTART WITH 1;`
);
}
} else {
// If we're in prototype mode then we need to rebuild the tables after a reset
this._runPrismaCmd(`migrate reset --force --preview-feature`);
this._runPrismaCmd(`db push --force --preview-feature`);
}
}

if (migrationNeeded) {
this._saveMigration({ name: 'init' });
this._executeMigrations();
} else {
this._runPrismaCmd(`migrate reset --force --preview-feature`);
}
}

Expand Down
6 changes: 3 additions & 3 deletions packages/adapter-prisma/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
"@keystonejs/fields-auto-increment": "^8.1.1",
"@keystonejs/keystone": "^18.1.0",
"@keystonejs/utils": "^6.0.0",
"@prisma/cli": "2.12.1",
"@prisma/client": "2.12.1",
"@prisma/sdk": "2.12.1",
"@prisma/cli": "2.15.0",
"@prisma/client": "2.15.0",
"@prisma/sdk": "2.15.0",
"cuid": "^2.1.8"
},
"repository": "https://github.com/keystonejs/keystone/tree/master/packages/adapter-prisma"
Expand Down
1 change: 1 addition & 0 deletions packages/test-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const argGenerator = {
},
}),
prisma_postgresql: () => ({
migrationMode: 'prototype',
dropDatabase: true,
url: process.env.DATABASE_URL || '',
provider: 'postgresql',
Expand Down
Loading

0 comments on commit fc2b710

Please sign in to comment.