Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a rule to enforce TypeORM's Relation<...>-wrapper #16

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 100 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ import tseslint from 'typescript-eslint';
import typeormTypescriptRecommended from 'eslint-plugin-typeorm-typescript/recommended';

export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
typeormTypescriptRecommended,
eslint.configs.recommended,
...tseslint.configs.recommended,
typeormTypescriptRecommended,
);
```

Expand All @@ -35,18 +35,17 @@ import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import typeormTypescriptPlugin from 'eslint-plugin-typeorm-typescript';

export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
{
plugins: {'typeorm-typescript': typeormTypescriptPlugin},
export default tseslint.config(eslint.configs.recommended, ...tseslint.configs.recommended, {
plugins: { 'typeorm-typescript': typeormTypescriptPlugin },
rules: {
"typeorm-typescript/enforce-column-types": "error",
"typeorm-typescript/enforce-relation-types": "warn",
"typeorm-typescript/enforce-consistent-nullability": ["error", { "specifyNullable": "always" }]
}
}
);
'typeorm-typescript/enforce-column-types': 'error',
'typeorm-typescript/enforce-relation-types': 'warn',
'typeorm-typescript/enforce-consistent-nullability': [
'error',
{ specifyNullable: 'always' },
],
},
});
```

For more information, see an example of the [recommended configuration](./examples/recommended/).
Expand All @@ -57,12 +56,12 @@ If you are still on legacy ESLint, update `.eslintrc.json` with the plugin to th

```json
{
"plugins": ["typeorm-typescript"],
"rules": {
"typeorm-typescript/enforce-column-types": "error",
"typeorm-typescript/enforce-relation-types": "error",
"typeorm-typescript/enforce-consistent-nullability": "error"
}
"plugins": ["typeorm-typescript"],
"rules": {
"typeorm-typescript/enforce-column-types": "error",
"typeorm-typescript/enforce-relation-types": "error",
"typeorm-typescript/enforce-consistent-nullability": "error"
}
}
```

Expand All @@ -87,9 +86,9 @@ It also handle primary columns (`number` by default), create and update columns

```json
{
"rules": {
"typeorm-typescript/enforce-column-types": "error"
}
"rules": {
"typeorm-typescript/enforce-column-types": "error"
}
}
```

Expand All @@ -100,11 +99,11 @@ Examples of **incorrect code** for this rule:
```ts
class Entity {
// Should be string
@Column({ type: "varchar" })
@Column({ type: 'varchar' })
name: number;

// Should be string | null
@Column({ type: "varchar", nullable: true })
@Column({ type: 'varchar', nullable: true })
name: string;

// Should be Date | null
Expand All @@ -118,11 +117,11 @@ Examples of **correct code** for this rule:
```ts
class Entity {
// TypeORM data type and TypeScript type are consistent
@Column({ type: "varchar" })
@Column({ type: 'varchar' })
name: string;

// Nullability is correct
@Column({ type: "varchar", nullable: true })
@Column({ type: 'varchar', nullable: true })
name: string | null;
}
```
Expand All @@ -137,9 +136,9 @@ which is an easy mistake to make.

```json
{
"rules": {
"typeorm-typescript/enforce-relation-types": "error"
}
"rules": {
"typeorm-typescript/enforce-relation-types": "error"
}
}
```

Expand Down Expand Up @@ -177,6 +176,8 @@ class Entity {
Examples of **correct code** for this rule:

```ts
import { Relation } from 'typeorm';

class Entity {
// Join is correctly nullable...
@OneToOne(() => Other)
Expand All @@ -191,6 +192,11 @@ class Entity {
// *ToMany rules are an array
@OneToMany(() => Other, (other) => other.entity)
others: Other[];

// Even accepts if you wrap the relation type with the typeorm Relation<...>-wrapper
@OneToOne(() => Other, { nullable: true })
@JoinColumn()
other: Relation<Other> | null;
}
```

Expand All @@ -207,9 +213,15 @@ that either only the non-default value is set (no parameters or `non-default`) o
"rules": {
// If you want to report an error for unnecessary nullables
"typeorm-typescript/enforce-consistent-nullability": "error", // or
"typeorm-typescript/enforce-consistent-nullability": ["error", { "specifyNullable": "non-default" }],
"typeorm-typescript/enforce-consistent-nullability": [
"error",
{ "specifyNullable": "non-default" },
],
// If you want to force setting nullable everywhere to avoid confusion
"typeorm-typescript/enforce-consistent-nullability": ["error", { "specifyNullable": "always" }],
"typeorm-typescript/enforce-consistent-nullability": [
"error",
{ "specifyNullable": "always" },
],
},
}
```
Expand All @@ -223,7 +235,7 @@ With `{ "specifyNullable": "non-default" }`:
```ts
class Entity {
// Columns are non-nullable by default, remove it
@Column({ type: "varchar", nullable: false })
@Column({ type: 'varchar', nullable: false })
name: number;

// Relations are nullable by default, remove it
Expand All @@ -238,7 +250,7 @@ With `{ "specifyNullable": "always" }`:
```ts
class Entity {
// Mark this to nullable false to make it clear
@Column({ type: "varchar" })
@Column({ type: 'varchar' })
name: number;

// Mark this to nullable true to make it clear
Expand All @@ -255,10 +267,10 @@ With `{ "specifyNullable": "non-default" }`:
```ts
class Entity {
// Nullability only defined when it is different than default
@Column({ type: "varchar", nullable: true })
@Column({ type: 'varchar', nullable: true })
middleName: number | null;

@Column({ type: "varchar" })
@Column({ type: 'varchar' })
name: number;

@OneToOne(() => Other)
Expand All @@ -272,14 +284,64 @@ With `{ "specifyNullable": "always" }`:
```ts
class Entity {
// Nullable is set everywhere, no default behaviour is implied
@Column({ type: "varchar", nullable: true })
@Column({ type: 'varchar', nullable: true })
middleName: number | null;

@Column({ type: "varchar", nullable: false })
@Column({ type: 'varchar', nullable: false })
name: number;

@OneToOne(() => Other, { nullable: true })
@JoinColumn()
other: Other | null;
}
```

### typeorm-typescript/enforce-relation-wrapper

TypeORM offers a Relation wrapper which is required when migrating to ESM, to avoid dependency issues. See also [Relations in ESM projects](https://typeorm.io/#relations-in-esm-projects). However, esp. during migration to ESM it may happen that all relations must be updated by hand, which might be very cumbersome. This rule allows to enforce the Relation<...>-wrapper and also may fix your code to do the migration for you.

#### Configuration

```json
{
"rules": {
"typeorm-typescript/enforce-relation-wrapper": "error"
}
}
```

#### Examples

Examples of **incorrect code** for this rule:

```ts
class Entity {
// Should be Relation<Other> | null
@OneToOne(() => Other)
@JoinColumn()
other: Other | null;

// Should be Relation<Other[]>
@OneToMany(() => Other, (other) => other.entity)
other: Other[];

// Should be Relation<Other> | null
@ManyToOne(() => Other)
other: Other;
}
```

Examples of **correct code** for this rule:

```ts
class Entity {
// Join is correctly nullable relation ....
@OneToOne(() => Other)
@JoinColumn()
other: Relation<Other> | null;

// *ToMany rules are an array relation
@OneToMany(() => Other, (other) => other.entity)
others: Relation<Other[]>;
}
```
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { FlatConfig } from '@typescript-eslint/utils/ts-eslint';
import enforceColumnTypes from './rules/enforce-column-types.js';
import enforceConsistentNullability from './rules/enforce-consistent-nullability.js';
import enforceRelationTypes from './rules/enforce-relation-types.js';
import enforceRelationWrapper from './rules/enforce-relation-wrapper.js';
import recommended from './recommended.js';

export const rules = {
'enforce-column-types': enforceColumnTypes,
'enforce-consistent-nullability': enforceConsistentNullability,
'enforce-relation-types': enforceRelationTypes,
'enforce-relation-wrapper': enforceRelationWrapper,
};

export const plugin: FlatConfig.Plugin = {
Expand Down
45 changes: 45 additions & 0 deletions src/rules/enforce-relation-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,51 @@ ruleTester.run('enforce-relation-types', enforceRelationTypes, {
others: Promise<Other[]>;
}`,
},
{
name: 'should allow lazy relations wrapped with Relation<...> wrapper array',
code: `import { Relation } from 'typeorm';
class Entity {
@ManyToMany(() => Other)
@JoinTable()
others: Promise<Relation<Other>[]>;
}`,
},
{
name: 'should allow lazy relations array with Relation<...> wrapper',
code: `import { Relation } from 'typeorm';
class Entity {
@ManyToMany(() => Other)
@JoinTable()
others: Promise<Relation<Other[]>>;
}`,
},
{
name: 'should allow scalar relations wrapped with Relation<...> wrapper',
code: `import { Relation } from 'typeorm';
class Entity {
@OneToOne(() => Other)
@JoinTable()
others: Relation<Other> | null;
}`,
},
{
name: 'should allow nullable relations wrapped with Relation<...> wrapper covering null',
code: `import { Relation } from 'typeorm';
class Entity {
@OneToOne(() => Other, { nullable: true })
@JoinTable()
others: Relation<Other | null>;
}`,
},
{
name: 'should allow nullable relations wrapped with Relation<...> wrapper not covering null',
code: `import { Relation } from 'typeorm';
class Entity {
@OneToOne(() => Other, { nullable: true })
@JoinTable()
others: Relation<Other> | null;
}`,
},
],
invalid: [
{
Expand Down
21 changes: 19 additions & 2 deletions src/rules/enforce-relation-types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils';
import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils';
import { ReportSuggestionArray } from '@typescript-eslint/utils/ts-eslint';
import {
findEitherDecoratorArguments,
Expand Down Expand Up @@ -53,7 +53,21 @@ const enforceColumnTypes = createRule({
schema: [],
},
create(context) {
let relationWrapperAlias: string | undefined;

return {
ImportDeclaration(node) {
if (node.source.value !== 'typeorm') return;

for (const specifier of node.specifiers) {
if (specifier.type !== TSESTree.AST_NODE_TYPES.ImportSpecifier) continue;
const { imported } = specifier;
if (imported.name === 'Relation') {
relationWrapperAlias = specifier.local.name;
return;
}
}
},
PropertyDefinition(node) {
const relationArguments = findEitherDecoratorArguments(node.decorators, [
'OneToOne',
Expand Down Expand Up @@ -89,7 +103,10 @@ const enforceColumnTypes = createRule({
return;
}
const { typeAnnotation } = node.typeAnnotation;
const typescriptType = convertTypeToRelationType(typeAnnotation);
const typescriptType = convertTypeToRelationType(
typeAnnotation,
relationWrapperAlias,
);

if (!isTypesEqual(typeormType, typescriptType)) {
let messageId: EnforceColumnMessages = 'typescript_typeorm_relation_mismatch';
Expand Down
Loading