From a5d892c1382e762e5555e1c1aa82130d6f6df5f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=ABl=20Galeran?= Date: Mon, 23 Aug 2021 16:18:34 +0200 Subject: [PATCH] fix: completion for referentialActions (Cascade,...) (#847) --- .../src/completion/completionUtil.ts | 16 ++ .../src/completion/completions.ts | 82 ++++++-- .../src/test/completion.test.ts | 68 ++++++- .../prisma-fmt-referentialActions.test.ts | 192 ++++++++++++++++++ .../completions/relationDirective.prisma | 27 +++ packages/vscode/src/test/completion.test.ts | 51 +++++ .../completions/relationDirective.prisma | 27 +++ 7 files changed, 439 insertions(+), 24 deletions(-) create mode 100644 packages/language-server/src/test/prisma-fmt-referentialActions.test.ts diff --git a/packages/language-server/src/completion/completionUtil.ts b/packages/language-server/src/completion/completionUtil.ts index 38243a3dcd..3873b74c10 100644 --- a/packages/language-server/src/completion/completionUtil.ts +++ b/packages/language-server/src/completion/completionUtil.ts @@ -123,6 +123,22 @@ export const relationArguments: CompletionItem[] = (label: string) => label.replace('[]', '[$0]').replace('""', '"$0"'), ) +export const relationOnDeleteArguments: CompletionItem[] = + convertToCompletionItems( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + completions.relationArguments.find((item) => item.label === 'onDelete: ')! + .params, + CompletionItemKind.Enum, + ) + +export const relationOnUpdateArguments: CompletionItem[] = + convertToCompletionItems( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + completions.relationArguments.find((item) => item.label === 'onUpdate: ')! + .params, + CompletionItemKind.Enum, + ) + export const dataSourceUrlArguments: CompletionItem[] = convertAttributesToCompletionItems( completions.datasourceUrlArguments, diff --git a/packages/language-server/src/completion/completions.ts b/packages/language-server/src/completion/completions.ts index 8ed81a860f..41576f5921 100644 --- a/packages/language-server/src/completion/completions.ts +++ b/packages/language-server/src/completion/completions.ts @@ -14,6 +14,8 @@ import { supportedDataSourceFields, supportedGeneratorFields, relationArguments, + relationOnDeleteArguments, + relationOnUpdateArguments, dataSourceUrlArguments, dataSourceProviders, dataSourceProviderArguments, @@ -24,7 +26,7 @@ import { import { klona } from 'klona' import { extractModelName } from '../rename/renameUtil' import previewFeatures from '../prisma-fmt/previewFeatures' -import referentialActions from '../prisma-fmt/referentialActions' +// import referentialActions from '../prisma-fmt/referentialActions' import nativeTypeConstructors, { NativeTypeConstructors, } from '../prisma-fmt/nativeTypes' @@ -778,16 +780,6 @@ function isInsideFieldsOrReferences( return false } -function definingReferentialAction( - wordsBeforePosition: Array, -): boolean { - const lastWord = wordsBeforePosition[wordsBeforePosition.length - 2] - return ( - lastWord != undefined && - (lastWord.includes('onDelete') || lastWord.includes('onUpdate')) - ) -} - function getFieldsFromCurrentBlock( lines: Array, block: Block, @@ -865,40 +857,86 @@ function getFieldType(line: string): string | undefined { return undefined } +// function definingReferentialAction( +// wordsBeforePosition: Array, +// ): boolean { +// const lastWord = wordsBeforePosition[wordsBeforePosition.length - 2] +// return ( +// lastWord != undefined && +// (lastWord.includes('onDelete') || lastWord.includes('onUpdate')) +// ) +// } + +// @relation function getSuggestionsForRelationDirective( wordsBeforePosition: string[], currentLineUntrimmed: string, lines: string[], - document: TextDocument, + document: TextDocument, // eslint-disable-line @typescript-eslint/no-unused-vars block: Block, position: Position, - binPath: string, + binPath: string, // eslint-disable-line @typescript-eslint/no-unused-vars ): CompletionList | undefined { // create deep copy const suggestions: CompletionItem[] = klona(relationArguments) - const wordBeforePosition = wordsBeforePosition[wordsBeforePosition.length - 1] + const firstWordBeforePosition = + wordsBeforePosition[wordsBeforePosition.length - 1] + const secondWordBeforePosition = + wordsBeforePosition[wordsBeforePosition.length - 2] + const wordBeforePosition = + firstWordBeforePosition === '' + ? secondWordBeforePosition + : firstWordBeforePosition const stringTilPosition = currentLineUntrimmed .slice(0, position.character) .trim() - // If we are right after @relation( - if (wordBeforePosition.includes('@relation')) { + // This is basically hardcoding the suggestions + // because prisma-format referential-actions returns an empty array [] most of the time + // Main issue is that "Restrict" should be excluded if on SQL Server + // + // Note: needs to be before @relation condition because + // `@relation(onUpdate: |)` means wordBeforePosition = '@relation(onUpdate:' + if (wordBeforePosition.includes('onDelete:')) { return { - items: suggestions, + items: relationOnDeleteArguments, + isIncomplete: false, + } + } + if (wordBeforePosition.includes('onUpdate:')) { + return { + items: relationOnUpdateArguments, isIncomplete: false, } } - if (definingReferentialAction(wordsBeforePosition)) { - const suggestions: CompletionItem[] = referentialActions( - binPath, - document.getText(), - ).map((action) => CompletionItem.create(action)) + // If we are right after @relation( + if (wordBeforePosition.includes('@relation')) { return { items: suggestions, isIncomplete: false, } } + + // Doesn't really work because prisma-fmt returns nothing when the schema is "invalid" + // but that also means that the schema is considered invalid when trying to autocomplete... + // + // if lastWord = onUpdate or onDelete + // then get suggestions by passing `referential-actions` arg to `prisma-fmt` + // if (definingReferentialAction(wordsBeforePosition)) { + // const suggestionsForReferentialActions: CompletionItem[] = referentialActions( + // binPath, + // document.getText(), + // ).map((action) => { + // return CompletionItem.create(action) + // }) + + // return { + // items: suggestionsForReferentialActions, + // isIncomplete: false, + // } + // } + if ( isInsideFieldsOrReferences( currentLineUntrimmed, diff --git a/packages/language-server/src/test/completion.test.ts b/packages/language-server/src/test/completion.test.ts index 4767f68bf2..23a2837ae7 100644 --- a/packages/language-server/src/test/completion.test.ts +++ b/packages/language-server/src/test/completion.test.ts @@ -33,12 +33,28 @@ function assertCompletion( ) assert.ok(completionResult !== undefined) - assert.deepStrictEqual(completionResult.isIncomplete, expected.isIncomplete) - assert.deepStrictEqual(completionResult.items.length, expected.items.length) + + assert.deepStrictEqual( + completionResult.isIncomplete, + expected.isIncomplete, + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Expected isIncomplete to be ${expected.isIncomplete} suggestions and got ${completionResult.isIncomplete}`, + ) + + assert.deepStrictEqual( + completionResult.items.length, + expected.items.length, + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Expected ${expected.items.length} suggestions and got ${ + completionResult.items.length + }: ${JSON.stringify(completionResult.items, undefined, 2)}`, + ) + assert.deepStrictEqual( completionResult.items.map((items) => items.label), expected.items.map((items) => items.label), ) + assert.deepStrictEqual( completionResult.items.map((item) => item.kind), expected.items.map((item) => item.kind), @@ -749,6 +765,54 @@ suite('Completions', () => { }, ) }) + test('@relation(onDelete: |)', () => { + assertCompletion( + relationDirectiveUri, + { line: 66, character: 36 }, + { + isIncomplete: false, + items: [ + { label: 'Cascade', kind: CompletionItemKind.Enum }, + { label: 'Restrict', kind: CompletionItemKind.Enum }, + { label: 'NoAction', kind: CompletionItemKind.Enum }, + { label: 'SetNull', kind: CompletionItemKind.Enum }, + { label: 'SetDefault', kind: CompletionItemKind.Enum }, + ], + }, + ) + }) + test('@relation(onUpdate: |)', () => { + assertCompletion( + relationDirectiveUri, + { line: 75, character: 36 }, + { + isIncomplete: false, + items: [ + { label: 'Cascade', kind: CompletionItemKind.Enum }, + { label: 'Restrict', kind: CompletionItemKind.Enum }, + { label: 'NoAction', kind: CompletionItemKind.Enum }, + { label: 'SetNull', kind: CompletionItemKind.Enum }, + { label: 'SetDefault', kind: CompletionItemKind.Enum }, + ], + }, + ) + }) + test('@relation(fields: [orderId], references: [id], onDelete: |)', () => { + assertCompletion( + relationDirectiveUri, + { line: 84, character: 73 }, + { + isIncomplete: false, + items: [ + { label: 'Cascade', kind: CompletionItemKind.Enum }, + { label: 'Restrict', kind: CompletionItemKind.Enum }, + { label: 'NoAction', kind: CompletionItemKind.Enum }, + { label: 'SetNull', kind: CompletionItemKind.Enum }, + { label: 'SetDefault', kind: CompletionItemKind.Enum }, + ], + }, + ) + }) }) }) }) diff --git a/packages/language-server/src/test/prisma-fmt-referentialActions.test.ts b/packages/language-server/src/test/prisma-fmt-referentialActions.test.ts new file mode 100644 index 0000000000..ef86b70eee --- /dev/null +++ b/packages/language-server/src/test/prisma-fmt-referentialActions.test.ts @@ -0,0 +1,192 @@ +import assert from 'assert' +import referentialActions from '../prisma-fmt/referentialActions' +import * as util from '../prisma-fmt/util' + +suite('prisma-fmt subcommand: referential-actions', () => { + test('SQLite', async () => { + const binPath = await util.getBinPath() + + const schema = ` + datasource db { + provider = "sqlite" + url = env("DATABASE_URL") + } + + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + }` + + const actions = referentialActions(binPath, schema) + + assert.deepStrictEqual(actions, [ + 'Cascade', + 'Restrict', + 'NoAction', + 'SetNull', + 'SetDefault', + ]) + }) + + test('PostgreSQL - minimal', async () => { + const binPath = await util.getBinPath() + + const schema = ` + datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + } + + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + }` + + const actions = referentialActions(binPath, schema) + + assert.deepStrictEqual(actions, [ + 'Cascade', + 'Restrict', + 'NoAction', + 'SetNull', + 'SetDefault', + ]) + }) + + test('PostgreSQL - example', async () => { + const binPath = await util.getBinPath() + + const schema = ` + datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + } + + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + } + + model User { + id Int @id @default(autoincrement()) + posts Post[] + } + + model Post { + id Int @id @default(autoincrement()) + title String + tags TagOnPosts[] + User User? @relation(fields: [userId], references: [id]) + userId Int? + } + + model TagOnPosts { + id Int @id @default(autoincrement()) + post Post? @relation(fields: [postId], references: [id], onUpdate: Cascade, onDelete: Cascade) + tag Tag? @relation(fields: [tagId], references: [id], onDelete: Cascade, onUpdate: Cascade) + postId Int? + tagId Int? + } + + model Tag { + id Int @id @default(autoincrement()) + name String @unique + posts TagOnPosts[] + }` + + const actions = referentialActions(binPath, schema) + + assert.deepStrictEqual(actions, [ + 'Cascade', + 'Restrict', + 'NoAction', + 'SetNull', + 'SetDefault', + ]) + }) + + test('MySQL', async () => { + const binPath = await util.getBinPath() + + const schema = ` + datasource db { + provider = "mysql" + url = env("DATABASE_URL") + } + + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + }` + + const actions = referentialActions(binPath, schema) + + assert.deepStrictEqual(actions, [ + 'Cascade', + 'Restrict', + 'NoAction', + 'SetNull', + 'SetDefault', + ]) + }) + + test('SQL Server', async () => { + const binPath = await util.getBinPath() + + const schema = ` + datasource db { + provider = "sqlserver" + url = env("DATABASE_URL") + } + + generator client { + provider = "prisma-client-js" + previewFeatures = ["microsoftSqlServer", "referentialActions"] + }` + + const actions = referentialActions(binPath, schema) + + assert.deepStrictEqual(actions, [ + 'Cascade', + 'NoAction', + 'SetNull', + 'SetDefault', + ]) + }) + + test('no datasource should return empty []', async () => { + const binPath = await util.getBinPath() + + const schema = ` + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + }` + + const actions = referentialActions(binPath, schema) + + assert.deepStrictEqual(actions, []) + }) + + test('invalid schema should return empty []', async () => { + const binPath = await util.getBinPath() + + const schema = ` + datasource db { + provider = "sqlite" + url = env("DATABASE_URL") + } + + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + } + + model { sss } // invalid model + ` + + const actions = referentialActions(binPath, schema) + + assert.deepStrictEqual(actions, []) + }) +}) diff --git a/packages/language-server/test/fixtures/completions/relationDirective.prisma b/packages/language-server/test/fixtures/completions/relationDirective.prisma index 1a75f10568..800a77a3eb 100644 --- a/packages/language-server/test/fixtures/completions/relationDirective.prisma +++ b/packages/language-server/test/fixtures/completions/relationDirective.prisma @@ -57,3 +57,30 @@ model OrderItemSix { orderId Int order Order @relation(fields: [orderId], references: [id], ) } + +model OrderItemSeven { + id Int @id @default(autoincrement()) + productName String + productPrice Int + quantity Int + orderId Int + order Order @relation(onDelete: ) +} + +model OrderItemEight { + id Int @id @default(autoincrement()) + productName String + productPrice Int + quantity Int + orderId Int + order Order @relation(onUpdate: ) +} + +model OrderItemNine { + id Int @id @default(autoincrement()) + productName String + productPrice Int + quantity Int + orderId Int + order Order @relation(fields: [orderId], references: [id], onDelete: ) +} diff --git a/packages/vscode/src/test/completion.test.ts b/packages/vscode/src/test/completion.test.ts index 72607ddd3d..5e6979d55b 100644 --- a/packages/vscode/src/test/completion.test.ts +++ b/packages/vscode/src/test/completion.test.ts @@ -24,15 +24,24 @@ async function testCompletion( assert.deepStrictEqual( actualCompletions.isIncomplete, expectedCompletionList.isIncomplete, + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Expected isIncomplete to be ${expectedCompletionList.isIncomplete} suggestions and got ${actualCompletions.isIncomplete}`, ) + assert.deepStrictEqual( actualCompletions.items.length, expectedCompletionList.items.length, + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Expected ${expectedCompletionList.items.length} suggestions and got ${ + actualCompletions.items.length + }: ${JSON.stringify(actualCompletions.items, undefined, 2)}`, ) + assert.deepStrictEqual( actualCompletions.items.map((items) => items.label).sort(), expectedCompletionList.items.map((items) => items.label).sort(), ) + assert.deepStrictEqual( actualCompletions.items.map((item) => item.kind).sort(), expectedCompletionList.items.map((item) => item.kind).sort(), @@ -640,6 +649,48 @@ suite('Completions', () => { true, ) }) + test('@relation(onDelete: |)', async () => { + await testCompletion( + relationDirectiveUri, + new vscode.Position(66, 36), + new vscode.CompletionList([ + { label: 'Cascade', kind: vscode.CompletionItemKind.Enum }, + { label: 'Restrict', kind: vscode.CompletionItemKind.Enum }, + { label: 'NoAction', kind: vscode.CompletionItemKind.Enum }, + { label: 'SetNull', kind: vscode.CompletionItemKind.Enum }, + { label: 'SetDefault', kind: vscode.CompletionItemKind.Enum }, + ]), + true, + ) + }) + test('@relation(onUpdate: |)', async () => { + await testCompletion( + relationDirectiveUri, + new vscode.Position(75, 36), + new vscode.CompletionList([ + { label: 'Cascade', kind: vscode.CompletionItemKind.Enum }, + { label: 'Restrict', kind: vscode.CompletionItemKind.Enum }, + { label: 'NoAction', kind: vscode.CompletionItemKind.Enum }, + { label: 'SetNull', kind: vscode.CompletionItemKind.Enum }, + { label: 'SetDefault', kind: vscode.CompletionItemKind.Enum }, + ]), + true, + ) + }) + test('@relation(fields: [orderId], references: [id], onDelete: |)', async () => { + await testCompletion( + relationDirectiveUri, + new vscode.Position(84, 73), + new vscode.CompletionList([ + { label: 'Cascade', kind: vscode.CompletionItemKind.Enum }, + { label: 'Restrict', kind: vscode.CompletionItemKind.Enum }, + { label: 'NoAction', kind: vscode.CompletionItemKind.Enum }, + { label: 'SetNull', kind: vscode.CompletionItemKind.Enum }, + { label: 'SetDefault', kind: vscode.CompletionItemKind.Enum }, + ]), + true, + ) + }) }) }) }) diff --git a/packages/vscode/testFixture/completions/relationDirective.prisma b/packages/vscode/testFixture/completions/relationDirective.prisma index 1a75f10568..800a77a3eb 100644 --- a/packages/vscode/testFixture/completions/relationDirective.prisma +++ b/packages/vscode/testFixture/completions/relationDirective.prisma @@ -57,3 +57,30 @@ model OrderItemSix { orderId Int order Order @relation(fields: [orderId], references: [id], ) } + +model OrderItemSeven { + id Int @id @default(autoincrement()) + productName String + productPrice Int + quantity Int + orderId Int + order Order @relation(onDelete: ) +} + +model OrderItemEight { + id Int @id @default(autoincrement()) + productName String + productPrice Int + quantity Int + orderId Int + order Order @relation(onUpdate: ) +} + +model OrderItemNine { + id Int @id @default(autoincrement()) + productName String + productPrice Int + quantity Int + orderId Int + order Order @relation(fields: [orderId], references: [id], onDelete: ) +}