From 1b1d1e6424e8cd96edeacc9519b176b199ea6d43 Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 26 Nov 2024 00:06:35 +0100 Subject: [PATCH] Handle bigint and decimal types --- CHANGELOG.md | 8 ++ README.md | 11 ++- src/rules/enforce-column-types.test.ts | 116 +++++++++++++++++++++++++ src/rules/enforce-column-types.ts | 36 +++++++- src/rules/enforce-relation-types.ts | 14 +-- src/utils/columnType.ts | 15 ++-- 6 files changed, 179 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ddd825..5d9ea50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## [Unreleased] +### Changed + +- **Breaking**: `bigint` and `decimal` are now parsed correctly according to the driver ([#5](https://github.com/daniel7grant/eslint-plugin-typeorm-typescript/issues/5#issuecomment-2452988205)) + - There is new option `driver` for `enforce-column-types`: can be 'postgres', 'mysql' or 'sqlite' + - If the driver is empty (default) or set to MySQL and PostgreSQL, bigint and decimal are parsed to be strings + - If the driver is set to SQLite, bigint and decimal are parsed to be numbers + - For more information, see [#5](https://github.com/daniel7grant/eslint-plugin-typeorm-typescript/issues/5#issuecomment-2455779084) + ## [0.4.1] - 2024-11-24 ### Added diff --git a/README.md b/README.md index ce935c7..5b1f3ed 100644 --- a/README.md +++ b/README.md @@ -76,9 +76,10 @@ but columns aren't), it makes it easy to make mistakes. These ESLint rules will ### typeorm-typescript/enforce-column-types -TypeORM data types and TypeScript types should be consistent. It includes the primitive types (e.g. `VARCHAR` -> `string`) -and the nullability. By default columns are non-nullable, but if the `nullable: true` option is set, it should be unioned -with `null` in the TypeScript types too. +TypeORM data types and TypeScript types should be consistent. It checks the primitive types (e.g. `VARCHAR` -> `string`) +and driver-specific types. By most drivers, `bigint` and `decimal` are parsed as string, except in SQLite (set the `driver` +option, if you use SQLite). This rule checks the nullability too: by default columns are non-nullable, but if the `nullable: true` +option is set, it should be unioned with `null` in the TypeScript types as well. It also handle primary columns (`number` by default), create and update columns (`date` by default) and delete columns (`date` and nullable by default). @@ -88,7 +89,9 @@ It also handle primary columns (`number` by default), create and update columns ```json { "rules": { - "typeorm-typescript/enforce-column-types": "error" + "typeorm-typescript/enforce-column-types": "error", + // If you are using SQLite, set the driver + "typeorm-typescript/enforce-relation-types": ["error", { "driver": "sqlite" }], } } ``` diff --git a/src/rules/enforce-column-types.test.ts b/src/rules/enforce-column-types.test.ts index 19c3061..9a8af1c 100644 --- a/src/rules/enforce-column-types.test.ts +++ b/src/rules/enforce-column-types.test.ts @@ -49,6 +49,36 @@ ruleTester.run('enforce-column-types', enforceColumnTypes, { num: number; }`, }, + { + name: 'should allow matching decimal column types', + code: `class Entity { + @Column({ type: 'decimal' }) + decimal: string; + }`, + }, + { + name: 'should allow matching bigint column types', + code: `class Entity { + @Column({ type: 'bigint' }) + big: string; + }`, + }, + { + name: 'should allow matching decimal column types in SQLite', + code: `class Entity { + @Column({ type: 'decimal' }) + decimal: number; + }`, + options: [{ driver: 'sqlite' }], + }, + { + name: 'should allow matching bigint column types in SQLite', + code: `class Entity { + @Column({ type: 'bigint' }) + big: number; + }`, + options: [{ driver: 'sqlite' }], + }, { name: 'should allow matching bool column types', code: `class Entity { @@ -366,6 +396,92 @@ ruleTester.run('enforce-column-types', enforceColumnTypes, { }, ], }, + { + name: 'should fail on non-string TypeScript type with decimal type', + code: `class Entity { + @Column({ type: 'decimal' }) + num: number; + }`, + errors: [ + { + messageId: 'typescript_typeorm_column_mismatch', + suggestions: [ + { + messageId: 'typescript_typeorm_column_suggestion', + output: `class Entity { + @Column({ type: 'decimal' }) + num: string; + }`, + }, + ], + }, + ], + }, + { + name: 'should fail on non-string TypeScript type with bigint type', + code: `class Entity { + @Column({ type: 'bigint' }) + num: number; + }`, + errors: [ + { + messageId: 'typescript_typeorm_column_mismatch', + suggestions: [ + { + messageId: 'typescript_typeorm_column_suggestion', + output: `class Entity { + @Column({ type: 'bigint' }) + num: string; + }`, + }, + ], + }, + ], + }, + { + name: 'should fail on non-number TypeScript type with decimal type in SQLite', + code: `class Entity { + @Column({ type: 'decimal' }) + num: string; + }`, + options: [{ driver: 'sqlite' }], + errors: [ + { + messageId: 'typescript_typeorm_column_mismatch', + suggestions: [ + { + messageId: 'typescript_typeorm_column_suggestion', + output: `class Entity { + @Column({ type: 'decimal' }) + num: number; + }`, + }, + ], + }, + ], + }, + { + name: 'should fail on non-number TypeScript type with bigint type in SQLite', + code: `class Entity { + @Column({ type: 'bigint' }) + num: string; + }`, + options: [{ driver: 'sqlite' }], + errors: [ + { + messageId: 'typescript_typeorm_column_mismatch', + suggestions: [ + { + messageId: 'typescript_typeorm_column_suggestion', + output: `class Entity { + @Column({ type: 'bigint' }) + num: number; + }`, + }, + ], + }, + ], + }, { name: 'should fail on non-date TypeScript type', code: `class Entity { diff --git a/src/rules/enforce-column-types.ts b/src/rules/enforce-column-types.ts index 4db42bc..8639d41 100644 --- a/src/rules/enforce-column-types.ts +++ b/src/rules/enforce-column-types.ts @@ -19,9 +19,20 @@ const createRule = ESLintUtils.RuleCreator( `https://github.com/daniel7grant/eslint-plugin-typeorm-typescript#typeorm-typescript${name}`, ); -const enforceColumnTypes = createRule({ +type EnforceColumnMessages = + | 'typescript_typeorm_column_mismatch' + | 'typescript_typeorm_column_mismatch_weird' + | 'typescript_typeorm_column_suggestion'; + +type EnforceColumnOptions = [ + { + driver?: 'postgres' | 'mysql' | 'sqlite'; + }, +]; + +const enforceColumnTypes = createRule({ name: 'enforce-column-types', - defaultOptions: [], + defaultOptions: [{}], meta: { type: 'problem', docs: { @@ -31,10 +42,23 @@ const enforceColumnTypes = createRule({ messages: { typescript_typeorm_column_mismatch: 'Type of {{ propertyName }}{{ className }} is not matching the TypeORM column type{{ expectedValue }}.', + typescript_typeorm_column_mismatch_weird: + 'Type of {{ propertyName }}{{ className }} should be string, as decimals and bigints are encoded as strings by PostgreSQL and MySQL drivers. If you are using SQLite, change the driver option to sqlite.', typescript_typeorm_column_suggestion: 'Change the type of {{ propertyName }} to {{ expectedValue }}.', }, - schema: [], + schema: [ + { + type: 'object', + properties: { + driver: { + type: 'string', + enum: ['postgres', 'mysql', 'sqlite'], + }, + }, + additionalProperties: false, + }, + ], }, create(context) { return { @@ -52,7 +76,11 @@ const enforceColumnTypes = createRule({ return; } const [column, colArguments] = columnArguments; - const typeormType = convertArgumentToColumnType(column, colArguments); + const typeormType = convertArgumentToColumnType( + column, + colArguments, + context.options?.[0]?.driver, + ); const { typeAnnotation } = node.typeAnnotation; let typescriptType: ColumnType; diff --git a/src/rules/enforce-relation-types.ts b/src/rules/enforce-relation-types.ts index c32e93e..8ae298e 100644 --- a/src/rules/enforce-relation-types.ts +++ b/src/rules/enforce-relation-types.ts @@ -19,7 +19,7 @@ const createRule = ESLintUtils.RuleCreator( `https://github.com/daniel7grant/eslint-plugin-typeorm-typescript#typeorm-typescript${name}`, ); -type EnforceColumnMessages = +type EnforceRelationMessages = | 'typescript_typeorm_relation_missing' | 'typescript_typeorm_relation_mismatch' | 'typescript_typeorm_relation_array_to_many' @@ -27,13 +27,13 @@ type EnforceColumnMessages = | 'typescript_typeorm_relation_nullable_by_default' | 'typescript_typeorm_relation_nullable_by_default_suggestion' | 'typescript_typeorm_relation_specify_relation_always'; -type Options = [ +type EnforceRelationOptions = [ { specifyRelation?: 'always'; }, ]; -const enforceColumnTypes = createRule({ +const enforceRelationTypes = createRule({ name: 'enforce-relation-types', defaultOptions: [{}], meta: { @@ -111,8 +111,8 @@ const enforceColumnTypes = createRule({ const typescriptType = convertTypeToRelationType(typeAnnotation); if (!isTypesEqual(typeormType, typescriptType)) { - let messageId: EnforceColumnMessages = 'typescript_typeorm_relation_mismatch'; - const suggestions: ReportSuggestionArray = []; + let messageId: EnforceRelationMessages = 'typescript_typeorm_relation_mismatch'; + const suggestions: ReportSuggestionArray = []; const fixReplace = typeToString(typeormType, typescriptType); // Construct strings for error message @@ -181,7 +181,7 @@ const enforceColumnTypes = createRule({ }); const expectedValue = fixReplace ? ` (expected type: ${fixReplace})` : ''; - const suggestions: ReportSuggestionArray = []; + const suggestions: ReportSuggestionArray = []; if (fixReplace) { suggestions.push({ messageId: 'typescript_typeorm_relation_suggestion', @@ -210,4 +210,4 @@ const enforceColumnTypes = createRule({ }, }); -export default enforceColumnTypes; +export default enforceRelationTypes; diff --git a/src/utils/columnType.ts b/src/utils/columnType.ts index 5e750e0..c76deee 100644 --- a/src/utils/columnType.ts +++ b/src/utils/columnType.ts @@ -38,9 +38,6 @@ interface ColumnParameter { // From: https://github.com/typeorm/typeorm/blob/master/src/driver/types/ColumnTypes.ts const booleanLike = ['boolean', 'bool']; const numberLike = [ - 'bigint', - 'dec', - 'decimal', 'fixed', 'int', 'int2', @@ -54,7 +51,6 @@ const numberLike = [ 'smallint', 'tinyint', 'dec', - 'decimal', 'double precision', 'double', 'fixed', @@ -64,6 +60,9 @@ const numberLike = [ 'real', 'smalldecimal', ]; +// These are numbers that depend on the driver if parsed to a string (MySQL, PostgreSQL) or number (SQLite) +// @see https://typeorm.io/entities#column-types, https://github.com/daniel7grant/eslint-plugin-typeorm-typescript/issues/5 +const weirdNumberLike = ['bigint', 'dec', 'decimal']; const stringLike = [ 'character varying', 'varying character', @@ -107,13 +106,16 @@ const dateLike = [ 'year', ]; -function convertTypeOrmToColumnType(arg: string): ColumnTypeString { +function convertTypeOrmToColumnType(arg: string, driver?: string): ColumnTypeString { if (booleanLike.includes(arg)) { return 'boolean'; } if (numberLike.includes(arg)) { return 'number'; } + if (weirdNumberLike.includes(arg)) { + return driver === 'sqlite' ? 'number' : 'string'; + } if (stringLike.includes(arg)) { return 'string'; } @@ -143,6 +145,7 @@ export function getDefaultColumnTypeForDecorator(column: Column): ColumnParamete export function convertArgumentToColumnType( column: Column, args: TSESTree.CallExpressionArgument[], + driver?: string, ): ColumnType { const parsed = args.reduce((prev, arg) => { switch (arg.type) { @@ -160,7 +163,7 @@ export function convertArgumentToColumnType( return { columnType: parsed.type && !parsed.transformer - ? convertTypeOrmToColumnType(parsed.type) + ? convertTypeOrmToColumnType(parsed.type, driver) : 'unknown', nullable: parsed.nullable ?? false, literal: false,