Skip to content

Commit

Permalink
Add sqlite support to the prisma adapter (#3946)
Browse files Browse the repository at this point in the history
Co-authored-by: Jed Watson <[email protected]>
  • Loading branch information
timleslie and JedWatson authored Mar 15, 2021
1 parent 95cc08c commit 8e9b04e
Show file tree
Hide file tree
Showing 36 changed files with 445 additions and 215 deletions.
21 changes: 21 additions & 0 deletions .changeset/curly-chefs-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'@keystone-next/website': minor
'@keystone-next/fields': minor
'@keystone-next/fields-document': minor
'@keystone-next/keystone': minor
'@keystone-next/types': minor
'@keystone-next/adapter-prisma-legacy': minor
'@keystone-next/fields-legacy': minor
'@keystone-next/fields-auto-increment-legacy': minor
'@keystone-next/fields-cloudinary-image-legacy': minor
'@keystone-next/fields-color-legacy': minor
'@keystone-next/fields-markdown-legacy': minor
'@keystone-next/fields-oembed-legacy': minor
'@keystone-next/fields-unsplash-legacy': minor
'@keystone-next/fields-wysiwyg-tinymce-legacy': minor
'@keystone-next/keystone-legacy': minor
'@keystone-next/test-utils-legacy': minor
'@keystone-next/api-tests-legacy': minor
---

Added experimental support for Prisma + SQLite as a database adapter.
5 changes: 2 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,11 @@ jobs:
POSTGRES_DB: test_db
ports:
- 5432:5432
env:
DATABASE_URL: 'postgres://keystone5:k3yst0n3@localhost:5432/test_db'
strategy:
fail-fast: false
matrix:
index: [0, 1, 2, 3, 4, 5, 6, 7, 8]
adapter: ['mongoose', 'knex', 'prisma_postgresql']
adapter: ['mongoose', 'knex', 'prisma_postgresql', 'prisma_sqlite']
steps:
- name: Checkout Repo
uses: actions/checkout@v2
Expand Down Expand Up @@ -163,6 +161,7 @@ jobs:
UNSPLASH_KEY: ${{ secrets.UNSPLASH_KEY}}
UNSPLASH_SECRET: ${{ secrets.UNSPLASH_SECRET}}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
DATABASE_URL: ${{ matrix.adapter == 'prisma_sqlite' && 'file:./dev.db' || 'postgres://keystone5:k3yst0n3@localhost:5432/test_db' }}

non-api-tests:
name: Package Unit Tests
Expand Down
66 changes: 64 additions & 2 deletions docs-next/pages/apis/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default config({
session: () => { /* ... */ },
graphql: { /* ... */ },
extendGraphqlSchema: { /* ... */ },
experimental: { /* ... */ },
});
```

Expand Down Expand Up @@ -56,8 +57,9 @@ import type { DatabaseConfig } from '@keystone-next/types';
The `db` config option configures the database used to store data in your Keystone system.
It has a TypeScript type of `DatabaseConfig`.
Keystone supports three different database types; **Prisma**, **PostgreSQL**, and **MongoDB**.
These database types are powered by their corresponding Keystone database adapter; `prisma_postgresql`, `knex`, and `mongoose`.
The `prisma_postgresql` adapter includes support for the **migrate** commands of the Keystone [command line](../guides/cli).
Prisma in turn support both **PostgreSQL** and **SQLite** databases.
These database types are powered by their corresponding Keystone database adapter; `prisma_postgresql`, `prisma_sqlite`, `knex`, and `mongoose`.
The `prisma_postgresql` and `prisma_sqlite` adapters includes support for the **migrate** commands of the Keystone [command line](../guides/cli).

All database adapters require the `url` argument, which defines the connection URL for your database.
They also all have an optional `onConnect` async function, which takes a [`KeystoneContext`](./context) object, and lets perform any actions you might need at startup, such as data seeding.
Expand Down Expand Up @@ -89,6 +91,43 @@ export default config({
});
```

### prisma_sqlite

Support for SQLite with Prisma is still in preview.
To use this option you must also set `{ experimental: { prismaSqlite: true } }`.

Advanced configuration:

- `enableLogging` (default: `false`): Enable logging from the Prisma client.
- `getPrismaPath` (default: `() => '.keystone/prisma'` ): Set the location of the generated Prisma schema and client.

The function for `getPrismaPath` is provided with the generated Prisma schema as a `string` in the `{ prismaSchema }` argument.

```typescript
export default config({
db: {
adapter: 'prisma_sqlite',
url: 'file:./keystone.db',
onConnect: async context => { /* ... */ },
// Optional advanced configuration
enableLogging: true,
getPrismaPath: ({ prismaSchema }) => '.prisma',
},
/* ... */
});
```

#### Limitations

The `prisma_sqlite` is not intended to be used in production systems, and has certain limitations:

- `document`: The `document` field type is not supported.
- `decimal`: The `decimal` field type is not supported.
- `timestamp`: The `timestamp` field type only supports times within the range `1970 - 2038`.
- `text`: The `text` field type does not support the advanced filtering operations `contains`, `starts_with`, `ends_with`, or case insensitive filtering.
- `autoincrement`: The `autoincrement` field type can only be used as an `id` field.
- `select`: Using the `dataType: 'enum'` will use a GraphQL `String` type, rather than an `Enum` type.

### knex

Advanced configuration:
Expand Down Expand Up @@ -260,4 +299,27 @@ export default config({

See the [schema extension guide](../guides/schema-extension) for more details on how to use `graphQLSchemaExtension()` to extend your GraphQL API.

## experimental

The following flags allow you to enable features which are still in preview.
These features are not guaranteed to work, and should be used with caution.

```typescript
import { config } from '@keystone-next/keystone/schema';

export default config({
experimental: {
enableNextJsGraphqlApiEndpoint: true,
prismaSqlite: true,
}
/* ... */
});
```

Options:

- `enableNextJsGraphqlApiEndpoint`: (coming soon)
- `prismaSqlite`: Enables the use of SQLite with Prisma.
This flag is required when setting `{ db: { adapter: 'prisma_sqlite' } }`.

export default ({ children }) => <Markdown>{children}</Markdown>;
6 changes: 5 additions & 1 deletion packages-next/fields-document/src/Implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,11 @@ export class KnexDocumentInterface extends CommonDocumentInterface(KnexFieldAdap
export class PrismaDocumentInterface extends CommonDocumentInterface(PrismaFieldAdapter) {
constructor() {
super(...arguments);

if (this.listAdapter.parentAdapter.provider === 'sqlite') {
throw new Error(
`PrismaAdapter provider "sqlite" does not support field type "${this.field.constructor.name}"`
);
}
// Error rather than ignoring invalid config
// We totally can index these values, it's just not trivial. See issue #1297
if (this.config.isIndexed) {
Expand Down
1 change: 1 addition & 0 deletions packages-next/fields-document/src/test-fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const exampleValue2 = () => [
export const supportsUnique = false;
export const fieldName = 'content';
export const subfieldName = 'document';
export const unSupportedAdapterList = ['prisma_sqlite'];

export const fieldConfig = () => ({ ___validateAndNormalize: x => x });

Expand Down
1 change: 1 addition & 0 deletions packages-next/fields/src/types/text/views/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export const controller = (
Label({ label, value }) {
return `${label.toLowerCase()}: "${value}"`;
},
// FIXME: Not all of these options will work with prisma_sqlite
types: {
contains_i: {
label: 'Contains',
Expand Down
9 changes: 6 additions & 3 deletions packages-next/keystone/src/lib/applyIdFieldDefaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ export function applyIdFieldDefaults(config: KeystoneConfig): KeystoneConfig['li
}
let idField =
config.lists[key].idField ??
{ mongoose: mongoId({}), knex: autoIncrement({}), prisma_postgresql: autoIncrement({}) }[
config.db.adapter
];
{
mongoose: mongoId({}),
knex: autoIncrement({}),
prisma_postgresql: autoIncrement({}),
prisma_sqlite: autoIncrement({}),
}[config.db.adapter];
idField = {
...idField,
config: {
Expand Down
13 changes: 13 additions & 0 deletions packages-next/keystone/src/lib/createKeystone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ export function createKeystone(
migrationMode,
prismaClient,
...db,
provider: 'postgresql',
});
} else if (db.adapter === 'prisma_sqlite') {
if (!config.experimental?.prismaSqlite) {
throw new Error(
'SQLite support is still experimental. You must set { experimental: { prismaSqlite: true } } in your config to use this feature.'
);
}
adapter = new PrismaAdapter({
getPrismaPath: () => path.join(dotKeystonePath, 'prisma'),
prismaClient,
...db,
provider: 'sqlite',
});
}
// @ts-ignore The @types/keystonejs__keystone package has the wrong type for KeystoneOptions
Expand Down
7 changes: 7 additions & 0 deletions packages-next/types/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export type KeystoneConfig = {
experimental?: {
/** Enables nextjs graphql api route mode */
enableNextJsGraphqlApiEndpoint?: boolean;
/** Enable Prisma+SQLite support */
prismaSqlite?: boolean;
};
};

Expand Down Expand Up @@ -65,6 +67,11 @@ export type DatabaseConfig = DatabaseCommon &
getPrismaPath?: (arg: { prismaSchema: any }) => string;
getDbSchemaName?: (arg: { prismaSchema: any }) => string;
}
| {
adapter: 'prisma_sqlite';
enableLogging?: boolean;
getPrismaPath?: (arg: { prismaSchema: any }) => string;
}
| { adapter: 'knex'; dropDatabase?: boolean; schemaName?: string }
| { adapter: 'mongoose'; mongooseOptions?: { mongoUri?: string } & ConnectOptions }
);
Expand Down
14 changes: 12 additions & 2 deletions packages/adapter-prisma/src/adapter-prisma.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ class PrismaAdapter extends BaseKeystoneAdapter {
// TODO: Should we default to 'public' or null?
if (this.provider === 'postgresql') {
return this.dbSchemaName ? `${this.url}?schema=${this.dbSchemaName}` : this.url;
} else if (this.provider === 'sqlite') {
return this.url;
}
}

Expand Down Expand Up @@ -272,6 +274,13 @@ class PrismaAdapter extends BaseKeystoneAdapter {
this._runPrismaCmd(`migrate reset --force --preview-feature`);
await runPrototypeMigrations(this._url(), this.prismaSchema, path.resolve(this.schemaPath));
}
} else if (this.provider === 'sqlite') {
const tables = await this.prisma.$queryRaw(
"SELECT name FROM sqlite_master WHERE type='table';"
);
for (const { name } of tables) {
await this.prisma.$queryRaw(`DELETE FROM "${name}";`);
}
} else {
this._runPrismaCmd(`migrate reset --force --preview-feature`);
}
Expand Down Expand Up @@ -437,11 +446,12 @@ class PrismaListAdapter extends BaseListAdapter {
if (search !== undefined && search !== '' && searchField) {
if (searchField.fieldName === 'Text') {
// FIXME: Think about regex
const mode = this.parentAdapter.provider === 'sqlite' ? undefined : 'insensitive';
if (!ret.where) {
ret.where = { [searchFieldName]: { contains: search, mode: 'insensitive' } };
ret.where = { [searchFieldName]: { contains: search, mode } };
} else {
ret.where = {
AND: [ret.where, { [searchFieldName]: { contains: search, mode: 'insensitive' } }],
AND: [ret.where, { [searchFieldName]: { contains: search, mode } }],
};
}
// const f = escapeRegExp;
Expand Down
6 changes: 5 additions & 1 deletion packages/fields-auto-increment/src/Implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,11 @@ export class KnexAutoIncrementInterface extends KnexFieldAdapter {
export class PrismaAutoIncrementInterface extends PrismaFieldAdapter {
constructor() {
super(...arguments);

if (this.listAdapter.parentAdapter.provider === 'sqlite' && !this.field.isPrimaryKey) {
throw new Error(
`PrismaAdapter provider "sqlite" does not support field type "${this.field.constructor.name}"`
);
}
// Default isUnique to true if not specified
this.isUnique = typeof this.config.isUnique === 'undefined' ? true : !!this.config.isUnique;
this.isIndexed = !!this.config.isIndexed && !this.config.isUnique;
Expand Down
2 changes: 1 addition & 1 deletion packages/fields-auto-increment/src/test-fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const skipCreateTest = false;
export const skipUpdateTest = true;

// `AutoIncrement` field type is not supported by `mongoose`. So, we need to filter it out while performing `API` tests.
export const unSupportedAdapterList = ['mongoose'];
export const unSupportedAdapterList = ['mongoose', 'prisma_sqlite'];

// Be default, `AutoIncrement` are read-only. But for `isRequired` test purpose, we need to bypass these restrictions.
export const fieldConfig = matrixValue => ({
Expand Down
2 changes: 1 addition & 1 deletion packages/fields-cloudinary-image/src/test-fixtures.skip.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,5 @@ export const storedValues = () => [

export const supportedFilters = adapterName => [
'null_equality',
adapterName !== 'prisma_postgresql' && 'in_empty_null',
!['prisma_postgresql', 'prisma_sqlite'].includes(adapterName) && 'in_empty_null',
];
8 changes: 4 additions & 4 deletions packages/fields-color/src/test-fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ export const storedValues = () => [
{ name: 'g', testField: null },
];

export const supportedFilters = () => [
export const supportedFilters = adapterName => [
'null_equality',
'equality',
'equality_case_insensitive',
adapterName !== 'prisma_sqlite' && 'equality_case_insensitive',
'in_empty_null',
'in_value',
'string',
'string_case_insensitive',
adapterName !== 'prisma_sqlite' && 'string',
adapterName !== 'prisma_sqlite' && 'string_case_insensitive',
];
8 changes: 4 additions & 4 deletions packages/fields-markdown/src/test-fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ export const storedValues = () => [
{ name: 'g', testField: null },
];

export const supportedFilters = () => [
export const supportedFilters = adapterName => [
'null_equality',
'equality',
'equality_case_insensitive',
adapterName !== 'prisma_sqlite' && 'equality_case_insensitive',
'in_empty_null',
'in_value',
'string',
'string_case_insensitive',
adapterName !== 'prisma_sqlite' && 'string',
adapterName !== 'prisma_sqlite' && 'string_case_insensitive',
];
6 changes: 5 additions & 1 deletion packages/fields-oembed/src/Implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,11 @@ export class KnexOEmbedInterface extends CommonOEmbedInterface(KnexFieldAdapter)
export class PrismaOEmbedInterface extends CommonOEmbedInterface(PrismaFieldAdapter) {
constructor() {
super(...arguments);

if (this.listAdapter.parentAdapter.provider === 'sqlite') {
throw new Error(
`PrismaAdapter provider "sqlite" does not support field type "${this.field.constructor.name}"`
);
}
// Error rather than ignoring invalid config
// We totally can index these values, it's just not trivial. See issue #1297
if (this.config.isIndexed) {
Expand Down
3 changes: 2 additions & 1 deletion packages/fields-oembed/src/test-fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const exampleValue2 = () => 'https://codesandbox.io';
export const supportsUnique = false;
export const fieldName = 'portfolio';
export const subfieldName = 'originalUrl';
export const unSupportedAdapterList = ['prisma_sqlite'];

const iframelyAdapter = new IframelyOEmbedAdapter({
apiKey: process.env.IFRAMELY_API_KEY || 'iframely_api_key',
Expand Down Expand Up @@ -47,5 +48,5 @@ export const storedValues = () => [

export const supportedFilters = adapterName => [
'null_equality',
adapterName !== 'prisma_postgresql' && 'in_empty_null',
!['prisma_postgresql'].includes(adapterName) && 'in_empty_null',
];
2 changes: 1 addition & 1 deletion packages/fields-unsplash/src/test-fixtures.skip.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,5 @@ export const storedValues = () => [

export const supportedFilters = adapterName => [
'null_equality',
adapterName !== 'prisma_postgresql' && 'in_empty_null',
!['prisma_postgresql', 'prisma_sqlite'].includes(adapterName) && 'in_empty_null',
];
10 changes: 5 additions & 5 deletions packages/fields-wysiwyg-tinymce/src/test-fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ export const storedValues = () => [
{ name: 'g', content: null },
];

export const supportedFilters = () => [
export const supportedFilters = adapterName => [
'null_equality',
'equality',
'equality_case_insensitive',
adapterName !== 'prisma_sqlite' && 'equality_case_insensitive',
'in_empty_null',
'in_equal',
'string',
'string_case_insensitive',
'in_value',
adapterName !== 'prisma_sqlite' && 'string',
adapterName !== 'prisma_sqlite' && 'string_case_insensitive',
];
6 changes: 5 additions & 1 deletion packages/fields/src/types/DateTime/Implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,11 @@ export class KnexDateTimeInterface extends CommonDateTimeInterface(KnexFieldAdap
export class PrismaDateTimeInterface extends CommonDateTimeInterface(PrismaFieldAdapter) {
constructor() {
super(...arguments);

if (this.listAdapter.parentAdapter.provider === 'sqlite') {
throw new Error(
`PrismaAdapter provider "sqlite" does not support field type "${this.field.constructor.name}"`
);
}
this.utcPath = `${this.path}_utc`;
this.offsetPath = `${this.path}_offset`;
this.realKeys = [this.utcPath, this.offsetPath];
Expand Down
1 change: 1 addition & 0 deletions packages/fields/src/types/DateTime/test-fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const exampleValue = () => '1990-12-31T12:34:56.789+01:23';
export const exampleValue2 = () => '2000-01-20T00:08:00.000+10:00';
export const supportsUnique = true;
export const fieldName = 'lastOnline';
export const unSupportedAdapterList = ['prisma_sqlite'];

export const getTestFields = () => ({ name: { type: Text }, lastOnline: { type } });

Expand Down
Loading

1 comment on commit 8e9b04e

@vercel
Copy link

@vercel vercel bot commented on 8e9b04e Mar 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.