From f034839867fa438da866bd87548b4a18246dee21 Mon Sep 17 00:00:00 2001 From: Yiming Date: Mon, 4 Sep 2023 10:53:36 +0800 Subject: [PATCH] fix: support for string escaping in ZModel (#668) --- packages/language/src/generated/grammar.ts | 2 +- packages/language/src/zmodel.langium | 2 +- .../src/plugins/prisma/prisma-builder.ts | 3 ++- packages/schema/tests/schema/parser.test.ts | 18 ++++++++++++++++++ packages/testtools/src/schema.ts | 9 +++++++-- .../tests/regression/issue-416.test.ts | 17 +++++++++++++++++ 6 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 tests/integration/tests/regression/issue-416.test.ts diff --git a/packages/language/src/generated/grammar.ts b/packages/language/src/generated/grammar.ts index 40d95905a..f29f11fdf 100644 --- a/packages/language/src/generated/grammar.ts +++ b/packages/language/src/generated/grammar.ts @@ -3429,7 +3429,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "name": "STRING", "definition": { "$type": "RegexToken", - "regex": "\\"[^\\"]*\\"|'[^']*'" + "regex": "\\"(\\\\\\\\.|[^\\"\\\\\\\\])*\\"|'(\\\\\\\\.|[^'\\\\\\\\])*'" }, "fragment": false, "hidden": false diff --git a/packages/language/src/zmodel.langium b/packages/language/src/zmodel.langium index 7354a525f..8a920cecb 100644 --- a/packages/language/src/zmodel.langium +++ b/packages/language/src/zmodel.langium @@ -275,7 +275,7 @@ hidden terminal WS: /\s+/; terminal NULL: 'null'; terminal THIS: 'this'; terminal ID: /[_a-zA-Z][\w_]*/; -terminal STRING: /"[^"]*"|'[^']*'/; +terminal STRING: /"(\\.|[^"\\])*"|'(\\.|[^'\\])*'/; terminal NUMBER: /[+-]?[0-9]+(\.[0-9]+)?/; terminal TRIPLE_SLASH_COMMENT: /\/\/\/[^\n\r]*/; hidden terminal ML_COMMENT: /\/\*[\s\S]*?\*\//; diff --git a/packages/schema/src/plugins/prisma/prisma-builder.ts b/packages/schema/src/plugins/prisma/prisma-builder.ts index 493e7c46d..15b9f0b20 100644 --- a/packages/schema/src/plugins/prisma/prisma-builder.ts +++ b/packages/schema/src/plugins/prisma/prisma-builder.ts @@ -285,7 +285,8 @@ export class AttributeArgValue { toString(): string { switch (this.type) { case 'String': - return `"${this.value}"`; + // use JSON.stringify to escape quotes + return JSON.stringify(this.value); case 'Number': return this.value.toString(); case 'FieldReference': { diff --git a/packages/schema/tests/schema/parser.test.ts b/packages/schema/tests/schema/parser.test.ts index dacf8e8f4..b72749126 100644 --- a/packages/schema/tests/schema/parser.test.ts +++ b/packages/schema/tests/schema/parser.test.ts @@ -102,6 +102,24 @@ describe('Parsing Tests', () => { expect(m.fields[2].attributes[0].args[0].value.$resolvedType?.decl).toBe(firstEnum); }); + it('string escape', async () => { + const content = ` + model Example { + id Int @id + doubleQuote String @default("s\\"1") + singleQuote String @default('s\\'1') + json Json @default("{\\"theme\\": \\"light\\", \\"consoleDrawer\\": false}") + } + `; + const doc = await loadModel(content, false); + const model = doc.declarations[0] as DataModel; + expect((model.fields[1].attributes[0].args[0].value as StringLiteral).value).toBe('s"1'); + expect((model.fields[2].attributes[0].args[0].value as StringLiteral).value).toBe("s'1"); + expect((model.fields[3].attributes[0].args[0].value as StringLiteral).value).toBe( + '{"theme": "light", "consoleDrawer": false}' + ); + }); + it('model field types', async () => { const content = ` model User { diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index 42d30df04..6a6a80137 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -67,10 +67,16 @@ export function getWorkspaceNpmCacheFolder(start: string) { } function makePrelude(options: SchemaLoadOptions) { + let dbUrl = options.dbUrl ?? (options.provider === 'postgresql' ? 'env("DATABASE_URL")' : 'file:./dev.db'); + + if (!dbUrl.includes('env(') && !dbUrl.startsWith("'") && !dbUrl.startsWith('"')) { + dbUrl = `'${dbUrl}'`; + } + return ` datasource db { provider = '${options.provider}' - url = '${options.dbUrl}' + url = ${dbUrl} } generator js { @@ -106,7 +112,6 @@ const defaultOptions: SchemaLoadOptions = { compile: false, logPrismaQuery: false, provider: 'sqlite', - dbUrl: 'file:./test.db', }; export async function loadSchemaFromFile(schemaFile: string, options?: SchemaLoadOptions) { diff --git a/tests/integration/tests/regression/issue-416.test.ts b/tests/integration/tests/regression/issue-416.test.ts new file mode 100644 index 000000000..d911effd9 --- /dev/null +++ b/tests/integration/tests/regression/issue-416.test.ts @@ -0,0 +1,17 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('Regression: issue 416', () => { + it('regression', async () => { + await loadSchema( + ` +model Example { + id Int @id + doubleQuote String @default("s\\"1") + singleQuote String @default('s\\'1') + json Json @default("{\\"theme\\": \\"light\\", \\"consoleDrawer\\": false}") +} + `, + { provider: 'postgresql', dbUrl: 'env("DATABASE_URL")', pushDb: false } + ); + }); +});