From 04413478b47a6baf003e16d58e0ca2707891e698 Mon Sep 17 00:00:00 2001 From: mitchellhamilton Date: Wed, 10 Nov 2021 14:51:54 +1000 Subject: [PATCH 01/12] Shorter many to many relation names --- .changeset/tasty-buses-jog.md | 72 +++++++++++++++++++ .../graphql-api-endpoint/schema.prisma | 4 +- .../src/lib/core/resolve-relationships.ts | 4 +- 3 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 .changeset/tasty-buses-jog.md diff --git a/.changeset/tasty-buses-jog.md b/.changeset/tasty-buses-jog.md new file mode 100644 index 00000000000..e70152b6cbb --- /dev/null +++ b/.changeset/tasty-buses-jog.md @@ -0,0 +1,72 @@ +--- +'@keystone-next/keystone': major +--- + +The names of two-sided many-many relationships now only include the list key and field key for one side of the relationship and one-sided many relationships(which are many-many) no longer have `_many` at the end of them for consistency with two-sided relationships. This allows having many-many relationships with long list and field keys without hitting Postgres's 63 character limit. + +This will require migrations if you have many-many relationships. + +Given a schema like this: + +```ts +Post: list({ + fields: { + tags: relationship({ ref: 'Tag.posts', many: true }), + }, +}), +Tag: list({ + fields: { + posts: relationship({ ref: 'Post.tags', many: true }), + }, +}), +``` + +When you run Keystone after updating, Keystone will prompt you to create a migration, you should do this but **DO NOT APPLY IT**, it needs to be modified before being applied. + +Prisma will generate a migration that looks something like this: + + +```sql +/* + Warnings: + + - You are about to drop the `_Post_tags_Tag_posts` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "_Post_tags_Tag_posts" DROP CONSTRAINT "_Post_tags_Tag_posts_A_fkey"; + +-- DropForeignKey +ALTER TABLE "_Post_tags_Tag_posts" DROP CONSTRAINT "_Post_tags_Tag_posts_B_fkey"; + +-- DropTable +DROP TABLE "_Post_tags_Tag_posts"; + +-- CreateTable +CREATE TABLE "_Post_tags" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "_Post_tags_AB_unique" ON "_Post_tags"("A", "B"); + +-- CreateIndex +CREATE INDEX "_Post_tags_B_index" ON "_Post_tags"("B"); + +-- AddForeignKey +ALTER TABLE "_Post_tags" ADD FOREIGN KEY ("A") REFERENCES "Post"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_Post_tags" ADD FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE; +``` + +You need to modify it so that it looks like this with the old and new table names for your schema substituted: + +```sql +ALTER TABLE "_Post_tags_Tag_posts" RENAME TO "_Post_tags"; +ALTER INDEX "_Post_tags_Tag_posts_AB_unique" RENAME TO "_Post_tags_AB_unique"; +ALTER INDEX "_Post_tags_Tag_posts_B_index" RENAME TO "_Post_tags_B_index"; +ALTER TABLE "_Post_tags" RENAME CONSTRAINT "_Post_tags_Tag_posts_A_fkey" TO "_Post_tags_A_fkey"; +ALTER TABLE "_Post_tags" RENAME CONSTRAINT "_Post_tags_Tag_posts_B_fkey" TO "_Post_tags_B_fkey"; +``` diff --git a/examples-staging/graphql-api-endpoint/schema.prisma b/examples-staging/graphql-api-endpoint/schema.prisma index 93657eb7c2e..6b7cd7f4613 100644 --- a/examples-staging/graphql-api-endpoint/schema.prisma +++ b/examples-staging/graphql-api-endpoint/schema.prisma @@ -28,7 +28,7 @@ model Post { publishDate DateTime? author User? @relation("Post_author", fields: [authorId], references: [id]) authorId String? @map("author") - tags Tag[] @relation("Post_tags_Tag_posts") + tags Tag[] @relation("Post_tags") @@index([authorId]) } @@ -36,5 +36,5 @@ model Post { model Tag { id String @id @default(cuid()) name String @default("") - posts Post[] @relation("Post_tags_Tag_posts") + posts Post[] @relation("Post_tags") } \ No newline at end of file diff --git a/packages/keystone/src/lib/core/resolve-relationships.ts b/packages/keystone/src/lib/core/resolve-relationships.ts index e34d8313ed9..7c1214dd702 100644 --- a/packages/keystone/src/lib/core/resolve-relationships.ts +++ b/packages/keystone/src/lib/core/resolve-relationships.ts @@ -166,7 +166,7 @@ export function resolveRelationships( continue; } if (leftRel.field.mode === 'many' && rightRel.field.mode === 'many') { - const relationName = `${leftRel.listKey}_${leftRel.fieldPath}_${rightRel.listKey}_${rightRel.fieldPath}`; + const relationName = `${leftRel.listKey}_${leftRel.fieldPath}`; resolvedLists[leftRel.listKey][leftRel.fieldPath] = { kind: 'relation', mode: 'many', @@ -215,7 +215,7 @@ export function resolveRelationships( } if (field.mode === 'many') { - const relationName = `${listKey}_${fieldPath}_many`; + const relationName = `${listKey}_${fieldPath}`; resolvedLists[field.list][foreignFieldPath] = { kind: 'relation', mode: 'many', From 40de35e54789866234220823f6dfff9785b38b18 Mon Sep 17 00:00:00 2001 From: mitchellhamilton Date: Thu, 11 Nov 2021 11:17:07 +1000 Subject: [PATCH 02/12] Update changeset --- .changeset/tasty-buses-jog.md | 43 +++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/.changeset/tasty-buses-jog.md b/.changeset/tasty-buses-jog.md index e70152b6cbb..d71ef2b5e40 100644 --- a/.changeset/tasty-buses-jog.md +++ b/.changeset/tasty-buses-jog.md @@ -23,8 +23,7 @@ Tag: list({ When you run Keystone after updating, Keystone will prompt you to create a migration, you should do this but **DO NOT APPLY IT**, it needs to be modified before being applied. -Prisma will generate a migration that looks something like this: - +On Postgres, Prisma will generate a migration that looks something like this: ```sql /* @@ -70,3 +69,43 @@ ALTER INDEX "_Post_tags_Tag_posts_B_index" RENAME TO "_Post_tags_B_index"; ALTER TABLE "_Post_tags" RENAME CONSTRAINT "_Post_tags_Tag_posts_A_fkey" TO "_Post_tags_A_fkey"; ALTER TABLE "_Post_tags" RENAME CONSTRAINT "_Post_tags_Tag_posts_B_fkey" TO "_Post_tags_B_fkey"; ``` + +On SQLite, Prisma will generate a migration that looks something like this: + + +```sql +/* + Warnings: + + - You are about to drop the `_Post_tags_Tag_posts` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropTable +PRAGMA foreign_keys=off; +DROP TABLE "_Post_tags_Tag_posts"; +PRAGMA foreign_keys=on; + +-- CreateTable +CREATE TABLE "_Post_tags" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + FOREIGN KEY ("A") REFERENCES "Post" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY ("B") REFERENCES "Tag" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "_Post_tags_AB_unique" ON "_Post_tags"("A", "B"); + +-- CreateIndex +CREATE INDEX "_Post_tags_B_index" ON "_Post_tags"("B"); +``` + +You need to modify it so that it looks like this with the old and new table names for your schema substituted: + +```sql +ALTER TABLE "_Post_tags_Tag_posts" RENAME TO "_Post_tags"; +DROP INDEX "_Post_tags_Tag_posts_AB_unique"; +DROP INDEX "_Post_tags_Tag_posts_B_index"; +CREATE UNIQUE INDEX "_Post_tags_AB_unique" ON "_Post_tags"("A", "B"); +CREATE INDEX "_Post_tags_B_index" ON "_Post_tags"("B"); +``` \ No newline at end of file From d6b9a27111b97edb372ff07d79a2646a6a5af7f7 Mon Sep 17 00:00:00 2001 From: mitchellhamilton Date: Wed, 17 Nov 2021 13:59:47 +1000 Subject: [PATCH 03/12] Add experimental.legacyManyRelationNames --- .changeset/tasty-buses-jog.md | 5 ++++- packages/keystone/src/artifacts.ts | 4 ++-- .../keystone/src/lib/core/resolve-relationships.ts | 9 ++++++--- packages/keystone/src/lib/core/types-for-lists.ts | 13 +++++++------ packages/keystone/src/lib/createSystem.ts | 10 +++++----- packages/keystone/src/scripts/run/dev.ts | 4 ++-- packages/keystone/src/types/config/index.ts | 5 +++++ 7 files changed, 31 insertions(+), 19 deletions(-) diff --git a/.changeset/tasty-buses-jog.md b/.changeset/tasty-buses-jog.md index d71ef2b5e40..3190e58426e 100644 --- a/.changeset/tasty-buses-jog.md +++ b/.changeset/tasty-buses-jog.md @@ -4,7 +4,10 @@ The names of two-sided many-many relationships now only include the list key and field key for one side of the relationship and one-sided many relationships(which are many-many) no longer have `_many` at the end of them for consistency with two-sided relationships. This allows having many-many relationships with long list and field keys without hitting Postgres's 63 character limit. -This will require migrations if you have many-many relationships. +There are two ways to update: + +1. Enable the `experimental.legacyManyRelationNames` option at the root of your config. This will make Keystone revert to the old relation names. This option will be removed in a future release. +2. Apply a migration to rename your many relation tables Given a schema like this: diff --git a/packages/keystone/src/artifacts.ts b/packages/keystone/src/artifacts.ts index 816aaafe86f..403fea81d43 100644 --- a/packages/keystone/src/artifacts.ts +++ b/packages/keystone/src/artifacts.ts @@ -35,7 +35,7 @@ export async function getCommittedArtifacts( graphQLSchema: GraphQLSchema, config: KeystoneConfig ): Promise { - const lists = initialiseLists(config.lists, config.db.provider); + const lists = initialiseLists(config); const prismaSchema = printPrismaSchema( lists, config.db.provider, @@ -183,7 +183,7 @@ export async function generateNodeModulesArtifactsWithoutPrismaClient( config: KeystoneConfig, cwd: string ) { - const lists = initialiseLists(config.lists, config.db.provider); + const lists = initialiseLists(config); const printedSchema = printSchema(graphQLSchema); const dotKeystoneDir = path.join(cwd, 'node_modules/.keystone'); diff --git a/packages/keystone/src/lib/core/resolve-relationships.ts b/packages/keystone/src/lib/core/resolve-relationships.ts index 7c1214dd702..333c4b7248a 100644 --- a/packages/keystone/src/lib/core/resolve-relationships.ts +++ b/packages/keystone/src/lib/core/resolve-relationships.ts @@ -77,7 +77,8 @@ function sortRelationships(left: Rel, right: Rel): [Rel, RelWithoutForeignKey] { // (note that this means that there are "fields" in the returned ListsWithResolvedRelations // which are not actually proper Keystone fields, they are just a db field and nothing else) export function resolveRelationships( - lists: Record }> + lists: Record }>, + useLegacyManyRelationNames: boolean ): ListsWithResolvedRelations { const alreadyResolvedTwoSidedRelationships = new Set(); const resolvedLists: Record> = Object.fromEntries( @@ -166,7 +167,9 @@ export function resolveRelationships( continue; } if (leftRel.field.mode === 'many' && rightRel.field.mode === 'many') { - const relationName = `${leftRel.listKey}_${leftRel.fieldPath}`; + const relationName = `${leftRel.listKey}_${leftRel.fieldPath}${ + useLegacyManyRelationNames ? `_${rightRel.listKey}_${rightRel.fieldPath}` : '' + }`; resolvedLists[leftRel.listKey][leftRel.fieldPath] = { kind: 'relation', mode: 'many', @@ -215,7 +218,7 @@ export function resolveRelationships( } if (field.mode === 'many') { - const relationName = `${listKey}_${fieldPath}`; + const relationName = `${listKey}_${fieldPath}${useLegacyManyRelationNames ? '_many' : ''}`; resolvedLists[field.list][foreignFieldPath] = { kind: 'relation', mode: 'many', diff --git a/packages/keystone/src/lib/core/types-for-lists.ts b/packages/keystone/src/lib/core/types-for-lists.ts index 4d7067390a2..34b9b3d792b 100644 --- a/packages/keystone/src/lib/core/types-for-lists.ts +++ b/packages/keystone/src/lib/core/types-for-lists.ts @@ -8,7 +8,6 @@ import { ListInfo, ListHooks, KeystoneConfig, - DatabaseProvider, FindManyArgs, CacheHintArgs, MaybePromise, @@ -69,10 +68,9 @@ export type InitialisedList = { }; }; -export function initialiseLists( - listsConfig: KeystoneConfig['lists'], - provider: DatabaseProvider -): Record { +export function initialiseLists(config: KeystoneConfig): Record { + const listsConfig = config.lists; + const { provider } = config.db; const listInfos: Record = {}; const isEnabled: Record< string, @@ -414,7 +412,10 @@ export function initialiseLists( ]) ); - const listsWithResolvedDBFields = resolveRelationships(listsWithInitialisedFields); + const listsWithResolvedDBFields = resolveRelationships( + listsWithInitialisedFields, + config.experimental?.legacyManyRelationNames ?? false + ); const listsWithInitialisedFieldsAndResolvedDbFields = Object.fromEntries( Object.entries(listsWithInitialisedFields).map(([listKey, list]) => { diff --git a/packages/keystone/src/lib/createSystem.ts b/packages/keystone/src/lib/createSystem.ts index 1f7b42790e1..6b0107791ad 100644 --- a/packages/keystone/src/lib/createSystem.ts +++ b/packages/keystone/src/lib/createSystem.ts @@ -1,4 +1,4 @@ -import { FieldData, KeystoneConfig, DatabaseProvider, getGqlNames } from '../types'; +import { FieldData, KeystoneConfig, getGqlNames } from '../types'; import { createAdminMeta } from '../admin-ui/system/createAdminMeta'; import { createGraphQLSchema } from './createGraphQLSchema'; @@ -6,7 +6,7 @@ import { makeCreateContext } from './context/createContext'; import { initialiseLists } from './core/types-for-lists'; import { CloudAssetsAPI, getCloudAssetsAPI } from './cloud/assets'; -function getSudoGraphQLSchema(config: KeystoneConfig, provider: DatabaseProvider) { +function getSudoGraphQLSchema(config: KeystoneConfig) { // This function creates a GraphQLSchema based on a modified version of the provided config. // The modifications are: // * All list level access control is disabled @@ -54,19 +54,19 @@ function getSudoGraphQLSchema(config: KeystoneConfig, provider: DatabaseProvider }) ), }; - const lists = initialiseLists(transformedConfig.lists, provider); + const lists = initialiseLists(transformedConfig); const adminMeta = createAdminMeta(transformedConfig, lists); return createGraphQLSchema(transformedConfig, lists, adminMeta); } export function createSystem(config: KeystoneConfig, isLiveReload?: boolean) { - const lists = initialiseLists(config.lists, config.db.provider); + const lists = initialiseLists(config); const adminMeta = createAdminMeta(config, lists); const graphQLSchema = createGraphQLSchema(config, lists, adminMeta); - const sudoGraphQLSchema = getSudoGraphQLSchema(config, config.db.provider); + const sudoGraphQLSchema = getSudoGraphQLSchema(config); return { graphQLSchema, diff --git a/packages/keystone/src/scripts/run/dev.ts b/packages/keystone/src/scripts/run/dev.ts index ca2b5f61238..2addd408b34 100644 --- a/packages/keystone/src/scripts/run/dev.ts +++ b/packages/keystone/src/scripts/run/dev.ts @@ -97,7 +97,7 @@ exports.default = function (req, res) { return res.send(x.toString()) } let lastVersion = ''; let lastError = undefined; const originalPrismaSchema = printPrismaSchema( - initialiseLists(config.lists, config.db.provider), + initialiseLists(config), config.db.provider, config.db.prismaPreviewFeatures ); @@ -132,7 +132,7 @@ exports.default = function (req, res) { return res.send(x.toString()) } const uninitializedConfig = (await apiRouteModule.getConfig()).default; const newConfig = initConfig(uninitializedConfig); const newPrismaSchema = printPrismaSchema( - initialiseLists(newConfig.lists, newConfig.db.provider), + initialiseLists(newConfig), newConfig.db.provider, newConfig.db.prismaPreviewFeatures ); diff --git a/packages/keystone/src/types/config/index.ts b/packages/keystone/src/types/config/index.ts index be793ad1737..2e0844e777a 100644 --- a/packages/keystone/src/types/config/index.ts +++ b/packages/keystone/src/types/config/index.ts @@ -42,6 +42,11 @@ export type KeystoneConfig = { * This is not a stable API and may contain breaking changes in `patch` level releases. */ contextInitialisedLists?: boolean; + /** + * Uses the old many relation names in the format of `ListKeyA_fieldKeyA_ListKeyB_fieldKeyB` instead of the newer format of `ListKeyA_fieldKeyA`. + * You may want to use this to avoid a migration when upgrading. This option will be removed in a future release. + */ + legacyManyRelationNames?: boolean; }; }; From f45d21e96a93fbe5f052d3b402cf443bbb3a0ae5 Mon Sep 17 00:00:00 2001 From: mitchellhamilton Date: Wed, 17 Nov 2021 14:19:50 +1000 Subject: [PATCH 04/12] rename option to useLegacyRelationNames --- .changeset/tasty-buses-jog.md | 2 +- packages/keystone/src/lib/core/types-for-lists.ts | 2 +- packages/keystone/src/types/config/index.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/tasty-buses-jog.md b/.changeset/tasty-buses-jog.md index 3190e58426e..8ba737502d7 100644 --- a/.changeset/tasty-buses-jog.md +++ b/.changeset/tasty-buses-jog.md @@ -6,7 +6,7 @@ The names of two-sided many-many relationships now only include the list key and There are two ways to update: -1. Enable the `experimental.legacyManyRelationNames` option at the root of your config. This will make Keystone revert to the old relation names. This option will be removed in a future release. +1. Enable the `experimental.useLegacyManyRelationNames` option at the root of your config. This will make Keystone revert to the old relation names. This option will be removed in a future release. 2. Apply a migration to rename your many relation tables Given a schema like this: diff --git a/packages/keystone/src/lib/core/types-for-lists.ts b/packages/keystone/src/lib/core/types-for-lists.ts index 34b9b3d792b..a789f44a933 100644 --- a/packages/keystone/src/lib/core/types-for-lists.ts +++ b/packages/keystone/src/lib/core/types-for-lists.ts @@ -414,7 +414,7 @@ export function initialiseLists(config: KeystoneConfig): Record Date: Thu, 18 Nov 2021 15:40:28 +1100 Subject: [PATCH 05/12] default destructive actions to no --- packages/keystone/src/lib/migrations.ts | 6 +++--- packages/keystone/src/lib/prompts.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/keystone/src/lib/migrations.ts b/packages/keystone/src/lib/migrations.ts index 6f2473a48ac..ac5d4703ce0 100644 --- a/packages/keystone/src/lib/migrations.ts +++ b/packages/keystone/src/lib/migrations.ts @@ -99,7 +99,7 @@ export async function pushPrismaSchemaToDatabase( 'If you want to keep the data in your database, set db.useMigrations to true in your config or change the data in your database so the migration can be applied' ); if ( - !(await confirmPrompt(`Do you want to continue? ${chalk.red('All data will be lost')}.`)) + !(await confirmPrompt(`Do you want to continue? ${chalk.red('All data will be lost')}.`, false)) ) { console.log('Reset cancelled'); throw new ExitError(0); @@ -115,7 +115,7 @@ export async function pushPrismaSchemaToDatabase( if (migration.warnings.length) { logWarnings(migration.warnings); if ( - !(await confirmPrompt(`Do you want to continue? ${chalk.red('Some data will be lost')}.`)) + !(await confirmPrompt(`Do you want to continue? ${chalk.red('Some data will be lost')}.`, false)) ) { console.log('Push cancelled.'); throw new ExitError(0); @@ -252,7 +252,7 @@ We need to reset the ${credentials.type} database "${credentials.database}" at $ console.log(`✨ A migration has been created at migrations/${generatedMigrationName}`); let shouldApplyMigration = - migrationCanBeApplied && (await confirmPrompt('Would you like to apply this migration?')); + migrationCanBeApplied && (await confirmPrompt('Would you like to apply this migration?', false)); if (shouldApplyMigration) { await runMigrateWithDbUrl(dbUrl, () => migrate.applyMigrations()); console.log('✅ The migration has been applied'); diff --git a/packages/keystone/src/lib/prompts.ts b/packages/keystone/src/lib/prompts.ts index ca180d684cd..1aec083f230 100644 --- a/packages/keystone/src/lib/prompts.ts +++ b/packages/keystone/src/lib/prompts.ts @@ -3,12 +3,12 @@ import prompts from 'prompts'; // prompts is badly typed so we have some more specific typed APIs // prompts also returns an undefined value on SIGINT which we really just want to exit on -async function confirmPromptImpl(message: string): Promise { +async function confirmPromptImpl(message: string, initial: boolean = true): Promise { const { value } = await prompts({ name: 'value', type: 'confirm', message, - initial: true, + initial, }); if (value === undefined) { process.exit(1); From 1c9d9987e5cd10fdbb9a99b298c4cc6d1594d7b9 Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Thu, 18 Nov 2021 16:50:56 +1100 Subject: [PATCH 06/12] add changeset suggestions from review --- .changeset/tasty-buses-jog.md | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/.changeset/tasty-buses-jog.md b/.changeset/tasty-buses-jog.md index 8ba737502d7..3b8759bfc60 100644 --- a/.changeset/tasty-buses-jog.md +++ b/.changeset/tasty-buses-jog.md @@ -2,14 +2,18 @@ '@keystone-next/keystone': major --- -The names of two-sided many-many relationships now only include the list key and field key for one side of the relationship and one-sided many relationships(which are many-many) no longer have `_many` at the end of them for consistency with two-sided relationships. This allows having many-many relationships with long list and field keys without hitting Postgres's 63 character limit. +The names of one-sided and two-sided, many-many relationships has been shortened. Two-sided many-many relationship names contain only the left-hand side names now; and the `_many` suffix has been dropped from one-sided many-many relationships. + +This reduces the probability that you will exceed [PostgreSQL's 63 character limit for identifiers](https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS) with typical usage. + +This is a breaking change. There are two ways to update: -1. Enable the `experimental.useLegacyManyRelationNames` option at the root of your config. This will make Keystone revert to the old relation names. This option will be removed in a future release. -2. Apply a migration to rename your many relation tables +1. Set `experimental.useLegacyManyRelationNames: true` at the root of your config. This will make Keystone revert to the old relation names. This feature will be removed in a future release. +2. Or, rename your many relation tables using a migration (see below) -Given a schema like this: +For example, given a schema like this: ```ts Post: list({ @@ -24,9 +28,13 @@ Tag: list({ }), ``` -When you run Keystone after updating, Keystone will prompt you to create a migration, you should do this but **DO NOT APPLY IT**, it needs to be modified before being applied. +When updating to this change, and running `yarn dev`, Keystone will prompt you to update your schema. + +- If you are using `useMigrations: true`, Keystone will follow the typical migration flow offer to apply an automatically generated migration. **DO NOT APPLY THE AUTOMATICALLY GENERATED MIGRATION** - unless you want to `DROP` your data. -On Postgres, Prisma will generate a migration that looks something like this: +- If you are using `useMigrations: false`, Keystone will follow the typical flow and offer to automatically migrate your schema. Again, **DO NOT RUN THE AUTOMATIC MIGRATION** - unless you want to `DROP` your data. + +On PostgreSQL, Prisma will generate a migration that looks something like this: ```sql /* @@ -75,7 +83,6 @@ ALTER TABLE "_Post_tags" RENAME CONSTRAINT "_Post_tags_Tag_posts_B_fkey" TO "_Po On SQLite, Prisma will generate a migration that looks something like this: - ```sql /* Warnings: @@ -111,4 +118,4 @@ DROP INDEX "_Post_tags_Tag_posts_AB_unique"; DROP INDEX "_Post_tags_Tag_posts_B_index"; CREATE UNIQUE INDEX "_Post_tags_AB_unique" ON "_Post_tags"("A", "B"); CREATE INDEX "_Post_tags_B_index" ON "_Post_tags"("B"); -``` \ No newline at end of file +``` From 160fad5a5fa4e9c66f37e6146c89fcbaa1fe6929 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Thu, 18 Nov 2021 17:06:47 +1100 Subject: [PATCH 07/12] yarn format --- packages/keystone/src/lib/migrations.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/keystone/src/lib/migrations.ts b/packages/keystone/src/lib/migrations.ts index ac5d4703ce0..1118f63c4a8 100644 --- a/packages/keystone/src/lib/migrations.ts +++ b/packages/keystone/src/lib/migrations.ts @@ -99,7 +99,10 @@ export async function pushPrismaSchemaToDatabase( 'If you want to keep the data in your database, set db.useMigrations to true in your config or change the data in your database so the migration can be applied' ); if ( - !(await confirmPrompt(`Do you want to continue? ${chalk.red('All data will be lost')}.`, false)) + !(await confirmPrompt( + `Do you want to continue? ${chalk.red('All data will be lost')}.`, + false + )) ) { console.log('Reset cancelled'); throw new ExitError(0); @@ -115,7 +118,10 @@ export async function pushPrismaSchemaToDatabase( if (migration.warnings.length) { logWarnings(migration.warnings); if ( - !(await confirmPrompt(`Do you want to continue? ${chalk.red('Some data will be lost')}.`, false)) + !(await confirmPrompt( + `Do you want to continue? ${chalk.red('Some data will be lost')}.`, + false + )) ) { console.log('Push cancelled.'); throw new ExitError(0); @@ -252,7 +258,8 @@ We need to reset the ${credentials.type} database "${credentials.database}" at $ console.log(`✨ A migration has been created at migrations/${generatedMigrationName}`); let shouldApplyMigration = - migrationCanBeApplied && (await confirmPrompt('Would you like to apply this migration?', false)); + migrationCanBeApplied && + (await confirmPrompt('Would you like to apply this migration?', false)); if (shouldApplyMigration) { await runMigrateWithDbUrl(dbUrl, () => migrate.applyMigrations()); console.log('✅ The migration has been applied'); From 6f4a7d0d1958a526bae976b7e3c6b358c7fbec14 Mon Sep 17 00:00:00 2001 From: mitchellhamilton Date: Mon, 22 Nov 2021 14:34:07 +1000 Subject: [PATCH 08/12] Implement db.relationName --- .changeset/giant-cycles-retire.md | 5 +++ .../src/fields/types/relationship/index.ts | 4 +++ .../src/lib/core/resolve-relationships.ts | 35 +++++++++++++------ .../keystone/src/lib/core/types-for-lists.ts | 5 +-- packages/keystone/src/types/config/index.ts | 5 --- packages/keystone/src/types/next-fields.ts | 10 ++---- 6 files changed, 38 insertions(+), 26 deletions(-) create mode 100644 .changeset/giant-cycles-retire.md diff --git a/.changeset/giant-cycles-retire.md b/.changeset/giant-cycles-retire.md new file mode 100644 index 00000000000..8621a2761a3 --- /dev/null +++ b/.changeset/giant-cycles-retire.md @@ -0,0 +1,5 @@ +--- +'@keystone-next/keystone': minor +--- + +Added `db.relationName` option to many to many `relationship` fields to allow explicitly setting the relation name. diff --git a/packages/keystone/src/fields/types/relationship/index.ts b/packages/keystone/src/fields/types/relationship/index.ts index a7c0aebfbfb..cacbb645050 100644 --- a/packages/keystone/src/fields/types/relationship/index.ts +++ b/packages/keystone/src/fields/types/relationship/index.ts @@ -61,6 +61,9 @@ type OneDbConfig = { type ManyDbConfig = { many: true; + db?: { + relationName?: string; + }; }; export type RelationshipFieldConfig = @@ -156,6 +159,7 @@ export const relationship = mode: 'many', list: foreignListKey, field: foreignFieldKey, + relationName: config.db?.relationName, })({ ...commonConfig, input: { diff --git a/packages/keystone/src/lib/core/resolve-relationships.ts b/packages/keystone/src/lib/core/resolve-relationships.ts index 333c4b7248a..1013be837f4 100644 --- a/packages/keystone/src/lib/core/resolve-relationships.ts +++ b/packages/keystone/src/lib/core/resolve-relationships.ts @@ -37,11 +37,11 @@ type Rel = { field: RelationDBField<'many' | 'one'>; }; -type RelWithoutForeignKey = Omit & { - field: Omit, 'foreignKey'>; +type RelWithoutForeignKeyAndName = Omit & { + field: Omit, 'foreignKey' | 'relationName'>; }; -function sortRelationships(left: Rel, right: Rel): [Rel, RelWithoutForeignKey] { +function sortRelationships(left: Rel, right: Rel): readonly [Rel, RelWithoutForeignKeyAndName] { if (left.field.mode === 'one' && right.field.mode === 'one') { if (left.field.foreignKey !== undefined && right.field.foreignKey !== undefined) { throw new Error( @@ -54,7 +54,24 @@ function sortRelationships(left: Rel, right: Rel): [Rel, RelWithoutForeignKey] { } } else if (left.field.mode === 'one' || right.field.mode === 'one') { // many relationships will never have a foreign key, so return the one relationship first - return left.field.mode === 'one' ? [left, right] : [right, left]; + const rels = left.field.mode === 'one' ? ([left, right] as const) : ([right, left] as const); + // we're only doing this for rels[1] because: + // - rels[1] is the many side + // - for the one side, TypeScript will already disallow relationName + if (rels[1].field.relationName !== undefined) { + throw new Error( + `You can only set db.relationName on one side of a many to many relationship, but db.relationName is set on ${rels[1].listKey}.${rels[1].fieldPath} which is the many side of a many to one relationship with ${rels[0].listKey}.${rels[0].fieldPath}` + ); + } + return rels; + } + if (left.field.mode === 'many' && right.field.mode === 'many') { + if (left.field.relationName !== undefined && right.field.relationName !== undefined) { + throw new Error( + `You can only set db.relationName on one side of a many to many relationship, but db.relationName is set on both ${left.listKey}.${left.fieldPath} and ${right.listKey}.${right.fieldPath}` + ); + } + return left.field.relationName !== undefined ? [left, right] : [right, left]; } const order = left.listKey.localeCompare(right.listKey); if (order > 0) { @@ -77,8 +94,7 @@ function sortRelationships(left: Rel, right: Rel): [Rel, RelWithoutForeignKey] { // (note that this means that there are "fields" in the returned ListsWithResolvedRelations // which are not actually proper Keystone fields, they are just a db field and nothing else) export function resolveRelationships( - lists: Record }>, - useLegacyManyRelationNames: boolean + lists: Record }> ): ListsWithResolvedRelations { const alreadyResolvedTwoSidedRelationships = new Set(); const resolvedLists: Record> = Object.fromEntries( @@ -167,9 +183,8 @@ export function resolveRelationships( continue; } if (leftRel.field.mode === 'many' && rightRel.field.mode === 'many') { - const relationName = `${leftRel.listKey}_${leftRel.fieldPath}${ - useLegacyManyRelationNames ? `_${rightRel.listKey}_${rightRel.fieldPath}` : '' - }`; + const relationName = + leftRel.field.relationName ?? `${leftRel.listKey}_${leftRel.fieldPath}`; resolvedLists[leftRel.listKey][leftRel.fieldPath] = { kind: 'relation', mode: 'many', @@ -218,7 +233,7 @@ export function resolveRelationships( } if (field.mode === 'many') { - const relationName = `${listKey}_${fieldPath}${useLegacyManyRelationNames ? '_many' : ''}`; + const relationName = field.relationName ?? `${listKey}_${fieldPath}`; resolvedLists[field.list][foreignFieldPath] = { kind: 'relation', mode: 'many', diff --git a/packages/keystone/src/lib/core/types-for-lists.ts b/packages/keystone/src/lib/core/types-for-lists.ts index a789f44a933..62e270750ad 100644 --- a/packages/keystone/src/lib/core/types-for-lists.ts +++ b/packages/keystone/src/lib/core/types-for-lists.ts @@ -412,10 +412,7 @@ export function initialiseLists(config: KeystoneConfig): Record { diff --git a/packages/keystone/src/types/config/index.ts b/packages/keystone/src/types/config/index.ts index 3aa5df2ef5c..be793ad1737 100644 --- a/packages/keystone/src/types/config/index.ts +++ b/packages/keystone/src/types/config/index.ts @@ -42,11 +42,6 @@ export type KeystoneConfig = { * This is not a stable API and may contain breaking changes in `patch` level releases. */ contextInitialisedLists?: boolean; - /** - * Uses the old many relation names in the format of `ListKeyA_fieldKeyA_ListKeyB_fieldKeyB` instead of the newer format of `ListKeyA_fieldKeyA`. - * You may want to use this to avoid a migration when upgrading. This option will be removed in a future release. - */ - useLegacyManyRelationNames?: boolean; }; }; diff --git a/packages/keystone/src/types/next-fields.ts b/packages/keystone/src/types/next-fields.ts index 8b6a6aff19a..ae23453433f 100644 --- a/packages/keystone/src/types/next-fields.ts +++ b/packages/keystone/src/types/next-fields.ts @@ -96,13 +96,8 @@ export type ScalarDBField< default?: ScalarDBFieldDefault; index?: 'unique' | 'index'; map?: string; -} & (Scalar extends 'DateTime' - ? { - updatedAt?: boolean; - } - : { - updatedAt?: undefined; - }); + updatedAt?: Scalar extends 'DateTime' ? boolean : undefined; +}; export const orderDirectionEnum = graphql.enum({ name: 'OrderDirection', @@ -120,6 +115,7 @@ export type RelationDBField = { field?: string; mode: Mode; foreignKey?: { one: true | { map: string }; many: undefined }[Mode]; + relationName?: { one: undefined; many: string }[Mode]; }; export type EnumDBField = { From d1cb8a74fd5b41b516b6091095ab3d8c1859e789 Mon Sep 17 00:00:00 2001 From: mitchellhamilton Date: Mon, 22 Nov 2021 15:01:21 +1000 Subject: [PATCH 09/12] Update changeset --- .changeset/tasty-buses-jog.md | 66 +++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/.changeset/tasty-buses-jog.md b/.changeset/tasty-buses-jog.md index 3b8759bfc60..4077bb2b8a6 100644 --- a/.changeset/tasty-buses-jog.md +++ b/.changeset/tasty-buses-jog.md @@ -10,8 +10,70 @@ This is a breaking change. There are two ways to update: -1. Set `experimental.useLegacyManyRelationNames: true` at the root of your config. This will make Keystone revert to the old relation names. This feature will be removed in a future release. -2. Or, rename your many relation tables using a migration (see below) +### Set `db.relationName` on many to many relation + +To avoid doing a migration, you can set `db.relationName` to the previous relation name on one side of a many to many relationship. + +For example, given a schema like this: + +```ts +Post: list({ + fields: { + tags: relationship({ ref: 'Tag.posts', many: true }), + }, +}), +Tag: list({ + fields: { + posts: relationship({ ref: 'Post.tags', many: true }), + }, +}), +``` + +Before this release, the generated Prisma schema looked like this: + +```prisma +// This file is automatically generated by Keystone, do not modify it manually. +// Modify your Keystone config when you want to change this. + +datasource postgresql { + url = env("DATABASE_URL") + provider = "postgresql" +} + +generator client { + provider = "prisma-client-js" + output = "node_modules/.prisma/client" + engineType = "binary" +} + +model Post { + id String @id @default(cuid()) + tags Tag[] @relation("Post_tags_Tag_posts") +} + +model Tag { + id String @id @default(cuid()) + posts Post[] @relation("Post_tags_Tag_posts") +} +``` + +You can add `db: { relationName: 'Post_tags_Tag_posts' }` to one side of the many to many relationship. It doesn't matter which side of the relationship it's on but it must only be on one side. + +```ts +Post: list({ + fields: { + tags: relationship({ ref: 'Tag.posts', many: true, db: { relationName: 'Post_tags_Tag_posts' } }), + }, +}), +Tag: list({ + fields: { + posts: relationship({ ref: 'Post.tags', many: true }), + }, +}), +``` + + +### Rename your many relation tables using a migration For example, given a schema like this: From 6c18181dc3d8e4c4633a58292b4e8c0895832a5d Mon Sep 17 00:00:00 2001 From: mitchellhamilton Date: Mon, 22 Nov 2021 15:18:56 +1000 Subject: [PATCH 10/12] Tests --- .../relationships/relation-name.test.ts | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 tests/api-tests/relationships/relation-name.test.ts diff --git a/tests/api-tests/relationships/relation-name.test.ts b/tests/api-tests/relationships/relation-name.test.ts new file mode 100644 index 00000000000..7392dc8f14d --- /dev/null +++ b/tests/api-tests/relationships/relation-name.test.ts @@ -0,0 +1,180 @@ +import { list } from '@keystone-next/keystone'; +import { relationship } from '@keystone-next/keystone/fields'; +import { getPrismaSchema, apiTestConfig, dbProvider } from '../utils'; + +test('when not specifying relationName in a many to many relationship, the name is picked based on the lexicographic list key + field key ordering', async () => { + const prismaSchema = await getPrismaSchema( + apiTestConfig({ + lists: { + A: list({ + fields: { + b: relationship({ ref: 'B.a', many: true }), + }, + }), + B: list({ + fields: { + a: relationship({ ref: 'A.b', many: true }), + }, + }), + }, + }) + ); + expect(prismaSchema) + .toEqual(`// This file is automatically generated by Keystone, do not modify it manually. +// Modify your Keystone config when you want to change this. + +datasource ${dbProvider} { + url = env("DATABASE_URL") + provider = "${dbProvider}" +} + +generator client { + provider = "prisma-client-js" + output = "node_modules/.prisma/client" + engineType = "binary" +} + +model A { + id String @id @default(cuid()) + b B[] @relation("B_a") +} + +model B { + id String @id @default(cuid()) + a A[] @relation("B_a") +}`); +}); + +test("the ordering of the lists doesn't affect the relation name", async () => { + const prismaSchema = await getPrismaSchema( + apiTestConfig({ + lists: { + A: list({ + fields: { + b: relationship({ ref: 'B.a', many: true }), + }, + }), + B: list({ + fields: { + a: relationship({ ref: 'A.b', many: true }), + }, + }), + }, + }) + ); + expect(prismaSchema) + .toEqual(`// This file is automatically generated by Keystone, do not modify it manually. +// Modify your Keystone config when you want to change this. + +datasource ${dbProvider} { + url = env("DATABASE_URL") + provider = "${dbProvider}" +} + +generator client { + provider = "prisma-client-js" + output = "node_modules/.prisma/client" + engineType = "binary" +} + +model A { + id String @id @default(cuid()) + b B[] @relation("B_a") +} + +model B { + id String @id @default(cuid()) + a A[] @relation("B_a") +}`); +}); + +test('when specifying relationName in a many to many relationship, the relation name is set to that', async () => { + const prismaSchema = await getPrismaSchema( + apiTestConfig({ + lists: { + A: list({ + fields: { + b: relationship({ ref: 'B.a', many: true }), + }, + }), + B: list({ + fields: { + a: relationship({ ref: 'A.b', many: true, db: { relationName: 'the_relation_name' } }), + }, + }), + }, + }) + ); + expect(prismaSchema) + .toEqual(`// This file is automatically generated by Keystone, do not modify it manually. +// Modify your Keystone config when you want to change this. + +datasource ${dbProvider} { + url = env("DATABASE_URL") + provider = "${dbProvider}" +} + +generator client { + provider = "prisma-client-js" + output = "node_modules/.prisma/client" + engineType = "binary" +} + +model A { + id String @id @default(cuid()) + b B[] @relation("the_relation_name") +} + +model B { + id String @id @default(cuid()) + a A[] @relation("the_relation_name") +}`); +}); + +test('when specifying relationName on both sides of a many to many relationship, an error is thrown', async () => { + await expect( + getPrismaSchema( + apiTestConfig({ + lists: { + A: list({ + fields: { + b: relationship({ ref: 'B.a', many: true, db: { relationName: 'blah' } }), + }, + }), + + B: list({ + fields: { + a: relationship({ ref: 'A.b', many: true, db: { relationName: 'blah' } }), + }, + }), + }, + }) + ) + ).rejects.toMatchInlineSnapshot( + `[Error: You can only set db.relationName on one side of a many to many relationship, but db.relationName is set on both A.b and B.a]` + ); +}); + +test('when specifying relationName on the many side of a one to many relationship, an error is thrown', async () => { + await expect( + getPrismaSchema( + apiTestConfig({ + lists: { + A: list({ + fields: { + b: relationship({ ref: 'B.a', many: true, db: { relationName: 'blah' } }), + }, + }), + + B: list({ + fields: { + a: relationship({ ref: 'A.b' }), + }, + }), + }, + }) + ) + ).rejects.toMatchInlineSnapshot( + `[Error: You can only set db.relationName on one side of a many to many relationship, but db.relationName is set on A.b which is the many side of a many to one relationship with B.a]` + ); +}); From 4c0ebafcf4016efb413ffa92f331dfe7183a2213 Mon Sep 17 00:00:00 2001 From: Mitchell Hamilton Date: Mon, 22 Nov 2021 15:35:00 +1000 Subject: [PATCH 11/12] Apply suggestions from code review Co-authored-by: Daniel Cousens <413395+dcousens@users.noreply.github.com> --- .changeset/tasty-buses-jog.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.changeset/tasty-buses-jog.md b/.changeset/tasty-buses-jog.md index 4077bb2b8a6..29a1f1d0fb0 100644 --- a/.changeset/tasty-buses-jog.md +++ b/.changeset/tasty-buses-jog.md @@ -12,7 +12,8 @@ There are two ways to update: ### Set `db.relationName` on many to many relation -To avoid doing a migration, you can set `db.relationName` to the previous relation name on one side of a many to many relationship. +Rather than doing a migration, you can set the new field property `db.relationName`, for either side of a many-to-many relationship field. +If set to the existing relation name, your database will remain unchanged. For example, given a schema like this: @@ -57,7 +58,9 @@ model Tag { } ``` -You can add `db: { relationName: 'Post_tags_Tag_posts' }` to one side of the many to many relationship. It doesn't matter which side of the relationship it's on but it must only be on one side. +By adding `db: { relationName: 'Post_tags_Tag_posts' }` to one side of the many-to-many relationship; you can preclude yourself from a migration. + +**Note:** It doesn't matter which side of the relationship you put this property, but it should be only on one side; otherwise you will receive an error. ```ts Post: list({ From fce187fe5a4a2631171ad5c0a919c2ce94c95e0b Mon Sep 17 00:00:00 2001 From: mitchellhamilton Date: Mon, 22 Nov 2021 15:51:31 +1000 Subject: [PATCH 12/12] Fix things --- .../src/lib/core/resolve-relationships.ts | 6 ++++- .../relationships/relation-name.test.ts | 23 ++++++++----------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/keystone/src/lib/core/resolve-relationships.ts b/packages/keystone/src/lib/core/resolve-relationships.ts index 1013be837f4..3ad694935fe 100644 --- a/packages/keystone/src/lib/core/resolve-relationships.ts +++ b/packages/keystone/src/lib/core/resolve-relationships.ts @@ -65,7 +65,11 @@ function sortRelationships(left: Rel, right: Rel): readonly [Rel, RelWithoutFore } return rels; } - if (left.field.mode === 'many' && right.field.mode === 'many') { + if ( + left.field.mode === 'many' && + right.field.mode === 'many' && + (left.field.relationName !== undefined || right.field.relationName !== undefined) + ) { if (left.field.relationName !== undefined && right.field.relationName !== undefined) { throw new Error( `You can only set db.relationName on one side of a many to many relationship, but db.relationName is set on both ${left.listKey}.${left.fieldPath} and ${right.listKey}.${right.fieldPath}` diff --git a/tests/api-tests/relationships/relation-name.test.ts b/tests/api-tests/relationships/relation-name.test.ts index 7392dc8f14d..44021a90254 100644 --- a/tests/api-tests/relationships/relation-name.test.ts +++ b/tests/api-tests/relationships/relation-name.test.ts @@ -29,19 +29,18 @@ datasource ${dbProvider} { } generator client { - provider = "prisma-client-js" - output = "node_modules/.prisma/client" - engineType = "binary" + provider = "prisma-client-js" + output = "node_modules/.prisma/client" } model A { id String @id @default(cuid()) - b B[] @relation("B_a") + b B[] @relation("A_b") } model B { id String @id @default(cuid()) - a A[] @relation("B_a") + a A[] @relation("A_b") }`); }); @@ -72,19 +71,18 @@ datasource ${dbProvider} { } generator client { - provider = "prisma-client-js" - output = "node_modules/.prisma/client" - engineType = "binary" + provider = "prisma-client-js" + output = "node_modules/.prisma/client" } model A { id String @id @default(cuid()) - b B[] @relation("B_a") + b B[] @relation("A_b") } model B { id String @id @default(cuid()) - a A[] @relation("B_a") + a A[] @relation("A_b") }`); }); @@ -115,9 +113,8 @@ datasource ${dbProvider} { } generator client { - provider = "prisma-client-js" - output = "node_modules/.prisma/client" - engineType = "binary" + provider = "prisma-client-js" + output = "node_modules/.prisma/client" } model A {