-
Notifications
You must be signed in to change notification settings - Fork 174
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(client, generator): Fix TS issues with generated client and add t…
…ests (#1117) Relevant issues: #1001 #925 #1114 Only one thing is fixed in this PR, the `Relation` is no longer imported if the data model has no relations in order to avoid `unused imports` TS errors. Main changes are moving all of the generator tests into their own test directory rather than inside the source code, for consistency with the TS client, ease of use, and improved reusability of test utilities. I've also added a sort of integration test for the CLI generation step, which is crucial to getting a working, compileable TS client as it modifies some of the Prisma generated typings. This test generates the client given a Prisma schema and performs all appropriate modifications to it, and then attempts to compile it with TS and specified compiler options. Also found that with TS version >=5.4 we have [stricter conditional type checking](https://devblogs.microsoft.com/typescript/announcing-typescript-5-4/#notable-behavioral-changes) which breaks clients generated with Prisma versions earlier than 4.16.0 (see issue #1114), however 4.16.0 introduces breaking changes with the "extensions" feature, so I've added a hacky workaround for it: https://github.com/electric-sql/electric/pull/1117/files#diff-fb0a50df4fc86f09b81a210162984d7822276fe19c8ae6496cf5ffe7d09f5adb We can add more tests that would test various compiler options, making it easier to avoid regressions when users report them with more complicated schemas and TS compiler options. Finally, I've added a Caution banner in our Usage section in the docs about requiring TS `strict` mode, since Zod has that as a requirement as well and its the reason why we see some inconsistencies in bug reports.
- Loading branch information
Showing
66 changed files
with
831 additions
and
421 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@electric-sql/prisma-generator": patch | ||
--- | ||
|
||
Do not import `Relation` class if data model does not have any relations - fixes `unused import` TS errors. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
203 changes: 203 additions & 0 deletions
203
clients/typescript/test/cli/migrations/migrate.generation.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
import test from 'ava' | ||
import fs from 'fs' | ||
import ts from 'typescript' | ||
import { generateClient } from '../../../src/cli/migrations/migrate' | ||
import path from 'path' | ||
|
||
const tempDir = `.tmp` | ||
const generatedFilePrefix = '_generation_test' | ||
|
||
const defaultTsCompilerOptions: ts.CompilerOptions = { | ||
target: ts.ScriptTarget.ES2020, | ||
useDefineForClassFields: true, | ||
module: ts.ModuleKind.ESNext, | ||
skipLibCheck: true, | ||
|
||
/* Bundler mode */ | ||
moduleResolution: ts.ModuleResolutionKind.Bundler, | ||
allowImportingTsExtensions: true, | ||
resolveJsonModule: true, | ||
isolatedModules: true, | ||
noEmit: true, | ||
|
||
/* Linting */ | ||
strict: true, | ||
noUnusedLocals: true, | ||
noUnusedParameters: true, | ||
noFallthroughCasesInSwitch: true, | ||
} | ||
|
||
const dbSnippet = ` | ||
datasource db { | ||
provider = "postgresql" | ||
url = env("PRISMA_DB_URL") | ||
} | ||
` | ||
|
||
const simpleSchema = ` | ||
${dbSnippet} | ||
model Items { | ||
value String @id | ||
} | ||
` | ||
|
||
const relationalSchema = ` | ||
${dbSnippet} | ||
model Items { | ||
value String @id | ||
nbr Int? | ||
} | ||
model User { | ||
id Int @id | ||
name String? | ||
posts Post[] | ||
profile Profile? | ||
} | ||
model Post { | ||
id Int @id | ||
title String @unique | ||
contents String | ||
nbr Int? | ||
authorId Int | ||
author User? @relation(fields: [authorId], references: [id]) | ||
} | ||
model Profile { | ||
id Int @id | ||
bio String | ||
userId Int @unique | ||
user User? @relation(fields: [userId], references: [id]) | ||
} | ||
` | ||
|
||
const dataTypesSchema = ` | ||
${dbSnippet} | ||
model DataTypes { | ||
id Int @id | ||
date DateTime? @db.Date | ||
time DateTime? @db.Time(3) | ||
timetz DateTime? @db.Timetz(3) | ||
timestamp DateTime? @unique @db.Timestamp(3) | ||
timestamptz DateTime? @db.Timestamptz(3) | ||
bool Boolean? | ||
uuid String? @db.Uuid | ||
int2 Int? @db.SmallInt | ||
int4 Int? | ||
int8 BigInt? | ||
float4 Float? @db.Real | ||
float8 Float? @db.DoublePrecision | ||
json Json? | ||
bytea Bytes? | ||
enum KindOfCategory? | ||
relatedId Int? | ||
related Dummy? @relation(fields: [relatedId], references: [id]) | ||
} | ||
model Dummy { | ||
id Int @id | ||
timestamp DateTime? @db.Timestamp(3) | ||
datatype DataTypes[] | ||
} | ||
enum KindOfCategory { | ||
FIRST | ||
SECOND | ||
RANDOM | ||
} | ||
` | ||
|
||
/** | ||
* Checks if the generated client from the Prisma schema can | ||
* be compiled using TypeScript without emitting any errors. | ||
* @param options compiler options to use | ||
* @returns whether the generated client compiles successfully | ||
*/ | ||
function checkGeneratedClientCompiles( | ||
clientPath: string, | ||
options: ts.CompilerOptions = defaultTsCompilerOptions | ||
) { | ||
const sourceFiles = fs | ||
.readdirSync(clientPath, { withFileTypes: true }) | ||
.filter((entry) => entry.isFile() && entry.name.endsWith('.ts')) | ||
.map((file) => path.join(clientPath, file.name)) | ||
const program = ts.createProgram({ | ||
rootNames: sourceFiles, | ||
options, | ||
}) | ||
// Check if the program compiles successfully | ||
const diagnostics = ts.getPreEmitDiagnostics(program) | ||
return diagnostics.length === 0 | ||
} | ||
|
||
/** | ||
* Generates the type-safe TS client for the specified Prisma schema, | ||
* following all steps performed by the CLI generation process. | ||
* @param inlinePrismaSchema the inline Prisma schema to generate the client for | ||
* @param token unique token to use for the generated schema and client dirs | ||
* @returns the path to the generated client | ||
*/ | ||
const generateClientFromPrismaSchema = async ( | ||
inlinePrismaSchema: string, | ||
token: string | ||
): Promise<string> => { | ||
const schemaFilePath = path.join( | ||
tempDir, | ||
`${generatedFilePrefix}_schema_${token}.prisma` | ||
) | ||
const generatedClientPath = path.join( | ||
tempDir, | ||
`${generatedFilePrefix}_client_${token}` | ||
) | ||
const migrationsPath = path.join(generatedClientPath, 'migrations.ts') | ||
fs.writeFileSync(schemaFilePath, inlinePrismaSchema) | ||
// clean up the generated client if present | ||
fs.rmSync(generatedClientPath, { recursive: true, force: true }) | ||
await generateClient(schemaFilePath, generatedClientPath) | ||
await fs.writeFileSync(migrationsPath, 'export default []') | ||
return generatedClientPath | ||
} | ||
|
||
test.before(() => { | ||
if (!fs.existsSync(tempDir)) { | ||
fs.mkdirSync(tempDir) | ||
} | ||
}) | ||
|
||
test.after(() => { | ||
// avoid deleting whole temp directory as it might be used by | ||
// other tests as well | ||
const files = fs.readdirSync(tempDir) | ||
for (const file of files) { | ||
if (file.startsWith(generatedFilePrefix)) { | ||
fs.rmSync(path.join(tempDir, file), { recursive: true, force: true }) | ||
} | ||
} | ||
}) | ||
|
||
test('should generate valid TS client for simple schema', async (t) => { | ||
const clientPath = await generateClientFromPrismaSchema( | ||
simpleSchema, | ||
'simple' | ||
) | ||
t.true(checkGeneratedClientCompiles(clientPath)) | ||
}) | ||
|
||
test('should generate valid TS client for relational schema', async (t) => { | ||
const clientPath = await generateClientFromPrismaSchema( | ||
relationalSchema, | ||
'relational' | ||
) | ||
t.true(checkGeneratedClientCompiles(clientPath)) | ||
}) | ||
|
||
test('should generate valid TS client for schema with all data types', async (t) => { | ||
const clientPath = await generateClientFromPrismaSchema( | ||
dataTypesSchema, | ||
'datatypes' | ||
) | ||
t.true(checkGeneratedClientCompiles(clientPath)) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.