Skip to content

Commit

Permalink
feat(custom decorators): New decorate generator setting
Browse files Browse the repository at this point in the history
close: #48
  • Loading branch information
unlight committed Sep 21, 2021
1 parent 21446a2 commit c5e14b7
Show file tree
Hide file tree
Showing 11 changed files with 328 additions and 71 deletions.
4 changes: 4 additions & 0 deletions @generated/user/create-many-user.args.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { Field } from '@nestjs/graphql';
import { ArgsType } from '@nestjs/graphql';
import { UserCreateManyInput } from './user-create-many.input';
import { ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';

@ArgsType()
export class CreateManyUserArgs {
@Field(() => [UserCreateManyInput], { nullable: false })
@ValidateNested()
@Type(() => UserCreateManyInput)
data!: Array<UserCreateManyInput>;

@Field(() => Boolean, { nullable: true })
Expand Down
4 changes: 4 additions & 0 deletions @generated/user/create-one-user.args.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { Field } from '@nestjs/graphql';
import { ArgsType } from '@nestjs/graphql';
import { UserCreateInput } from './user-create.input';
import { ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';

@ArgsType()
export class CreateOneUserArgs {
@Field(() => UserCreateInput, { nullable: false })
@ValidateNested()
@Type(() => UserCreateInput)
data!: UserCreateInput;
}
120 changes: 103 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,92 @@ export class PostWhereInput {
}
```

#### `decorate`

Allow to attach multiple decorators to any field of any type.

```sh
generator nestgraphql {
decorate_{key}_type = "outmatch pattern"
decorate_{key}_field = "outmatch pattern"
decorate_{key}_from = "module specifier"
decorate_{key}_name = "import name"
decorate_{key}_arguments = "[argument1, argument2]"
decorate_{key}_defaultImport = "default import name" | true
decorate_{key}_namespaceImport = "namespace import name"
decorate_{key}_namedImport = "import name" | true
}
```

Where `{key}` any identifier to group values (written in [flatten](https://github.com/hughsk/flat) style)

- `decorate_{key}_type` - outmatch pattern to match class name
- `decorate_{key}_field` - outmatch pattern to match field name
- `decorate_{key}_from` - module specifier to import from (e.g `class-validator`)
- `decorate_{key}_name` - import name or name with namespace
- `decorate_{key}_defaultImport` - import as default
- `decorate_{key}_namespaceImport` - use this name as import namespace
- `decorate_{key}_namedImport` - named import (without namespace)
- `decorate_{key}_arguments` - arguments for decorator (if decorator need to be called as function)
Special tokens can be used:
- `{propertyType.0}` - field's type (TypeScript type annotation)

Example of generated class:

```ts
@ArgsType()
export class CreateOneUserArgs {
@Field(() => UserCreateInput, { nullable: false })
data!: UserCreateInput;
}
```

To make it validateable (assuming `UserCreateInput` already contains validation decorators from `class-validator`),
it is necessary to add `@ValidateNested()` and `@Type()` from `class-transformer`.

```sh
decorate_1_type = "CreateOneUserArgs"
decorate_1_field = data
decorate_1_name = ValidateNested
decorate_1_from = "class-validator"
decorate_1_arguments = "[]"
decorate_2_type = "CreateOneUserArgs"
decorate_2_field = data
decorate_2_from = "class-transformer"
decorate_2_arguments = "['() => {propertyType.0}']"
decorate_2_name = Type
```

Result:

```ts
import { ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';

@ArgsType()
export class CreateOneUserArgs {
@Field(() => UserCreateInput, { nullable: false })
@ValidateNested()
@Type(() => UserCreateInput)
data!: UserCreateInput;
}
```

Another example:

```sh
decorate_2_namespaceImport = "Transform"
decorate_2_name = "Transform.Type"
```

```ts
import * as Transform from 'class-transformer';

@Transform.Type(() => UserCreateInput)
data!: UserCreateInput;

```

## Field Settings

Special directives in triple slash comments for more precise code generation.
Expand Down Expand Up @@ -249,52 +335,52 @@ Applying custom decorators requires configuration of generator.

```sh
generator nestgraphql {
fields_{Namespace}_from = "module specifier"
fields_{Namespace}_input = true | false
fields_{Namespace}_output = true | false
fields_{Namespace}_defaultImport = "default import name" | true
fields_{Namespace}_namespaceImport = "namespace import name"
fields_{Namespace}_namedImport = true | false
fields_{namespace}_from = "module specifier"
fields_{namespace}_input = true | false
fields_{namespace}_output = true | false
fields_{namespace}_defaultImport = "default import name" | true
fields_{namespace}_namespaceImport = "namespace import name"
fields_{namespace}_namedImport = true | false
}
```

Create configuration map in [flatten](https://github.com/hughsk/flat) style for `{Namespace}`.
Where `{Namespace}` is a namespace used in field triple slash comment.
Create configuration map in [flatten](https://github.com/hughsk/flat) style for `{namespace}`.
Where `{namespace}` is a namespace used in field triple slash comment.

##### `fields_{Namespace}_from`
##### `fields_{namespace}_from`

Required. Name of the module, which will be used in import (`class-validator`, `graphql-scalars`, etc.)
Type: `string`

##### `fields_{Namespace}_input`
##### `fields_{namespace}_input`

Means that it will be applied on input types (classes decorated by `InputType`)
Type: `boolean`
Default: `false`

##### `fields_{Namespace}_output`
##### `fields_{namespace}_output`

Means that it will be applied on output types (classes decorated by `ObjectType`)
Type: `boolean`
Default: `false`

##### `fields_{Namespace}_defaultImport`
##### `fields_{namespace}_defaultImport`

Default import name, if module have no namespace.
Type: `undefined | string | true`
Default: `undefined`
If defined as `true` then import name will be same as `{Namespace}`
If defined as `true` then import name will be same as `{namespace}`

##### `fields_{Namespace}_namespaceImport`
##### `fields_{namespace}_namespaceImport`

Import all as this namespace from module
Type: `undefined | string`
Default: Equals to `{Namespace}`
Default: Equals to `{namespace}`

##### `fields_{Namespace}_namedImport`
##### `fields_{namespace}_namedImport`

If imported module has internal namespace, this allow to generate named import,
imported name will be equal to `{Namespace}`, see [example of usage](#propertytype)
imported name will be equal to `{namespace}`, see [example of usage](#propertytype)
Type: `boolean`
Default: `false`

Expand Down
16 changes: 16 additions & 0 deletions example/user/user.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { UserAggregateArgs } from '../../@generated/user/user-aggregate.args';
import { UserCreateInput } from '../../@generated/user/user-create.input';
import { UserUpdateInput } from '../../@generated/user/user-update.input';
import { UserWhereInput } from '../../@generated/user/user-where.input';
import { CreateOneUserArgs } from '../../@generated/user/create-one-user.args';
import { CreateManyUserArgs } from '../../@generated/user/create-many-user.args';
import { UserDateInput } from './user-date.input';

const prisma = new PrismaClient({
Expand Down Expand Up @@ -54,6 +56,12 @@ export class UserResolver {
return;
}

@Mutation(() => User, { nullable: true })
async createOneUser(@Args() args: CreateOneUserArgs): Promise<any> {
console.log('args', args);
return;
}

@Mutation(() => User, { nullable: true })
async userInfo(@Args('user') user: UserDateInput): Promise<any> {
console.log(
Expand All @@ -65,6 +73,14 @@ export class UserResolver {
return;
}

@Mutation(() => [User], { nullable: true })
async createManyUsers(
@Args() createManyUserArgs: CreateManyUserArgs,
): Promise<any> {
console.log('createManyUserArgs', createManyUserArgs);
return;
}

@Query(() => AggregateUser)
userAggregate(@Args() args: UserAggregateArgs) {
return prisma.user.aggregate(args);
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"build": "sh Taskfile bundle",
"prisma:g": "node node_modules/prisma/build/index.js generate",
"format": "npx prettier ./@generated --write",
"regen": "rm -rf src/@generated && npm run prisma:g && npm run eslint:fix && npm run format",
"regen": "rm -rf @generated && npm run prisma:g && npm run format",
"example": "ts-node-dev example/main.ts",
"clean_cache": "rm -rf node_modules/.cache",
"compatibilty_check": "sh Taskfile compatibilty_check",
Expand Down
11 changes: 11 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ generator nestgraphql {
fields_Scalars_from = "graphql-scalars"
fields_Scalars_input = true
useInputType_WhereInput_ALL = "WhereInput"
decorate_1_type = "Create@(One|Many)UserArgs"
decorate_1_field = data
decorate_1_name = ValidateNested
decorate_1_from = "class-validator"
decorate_1_arguments = "[]"
decorate_2_type = "Create@(One|Many)UserArgs"
decorate_2_field = data
decorate_2_from = "class-transformer"
decorate_2_arguments = "['() => {propertyType.0}']"
decorate_2_name = Type
decorate_2_namedImport = true
}

/// User really
Expand Down
54 changes: 32 additions & 22 deletions src/handlers/input-type.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ok } from 'assert';
import JSON5 from 'json5';
import { castArray, trim } from 'lodash';
import { castArray } from 'lodash';
import pupa from 'pupa';
import { ClassDeclarationStructure, StructureKind } from 'ts-morph';

import { getGraphqlImport } from '../helpers/get-graphql-import';
Expand Down Expand Up @@ -53,15 +54,16 @@ export function inputType(
const modelName = getModelName(inputType.name) || '';
const model = models.get(modelName);
const modelFieldSettings = model && fieldSettings.get(model.name);
const moduleSpecifier = '@nestjs/graphql';

importDeclarations
.set('Field', {
namedImports: [{ name: 'Field' }],
moduleSpecifier: '@nestjs/graphql',
moduleSpecifier,
})
.set(classDecoratorName, {
namedImports: [{ name: classDecoratorName }],
moduleSpecifier: '@nestjs/graphql',
moduleSpecifier,
});

const useInputType = config.useInputType.find(x =>
Expand All @@ -83,15 +85,22 @@ export function inputType(
const propertySettings = settings?.getPropertyType();
const isCustomsApplicable =
typeName === model?.fields.find(f => f.name === name)?.type;

const propertyType = castArray(
propertySettings?.name ||
getPropertyType({
location,
type: typeName,
}),
);

// 'UserCreateInput'
// if (['CreateOneUserArgs'].includes(inputType.name)) {
// console.log('field', field);
// console.log('inputType', inputType);
// console.log('settings', settings);
// console.log('propertyType', propertyType);
// console.log('typeName', typeName);
// console.log('settings', settings);
// }
const property = propertyStructure({
name,
isNullable: !isRequired,
Expand Down Expand Up @@ -133,27 +142,13 @@ export function inputType(
}
}

// if (inputType.name === 'UserCountOrderByAggregateInput') {
// console.dir({
// 'inputType.name': inputType.name,
// 'field.name': field.name,
// typeName,
// field,
// graphqlInputType,
// propertyType,
// graphqlType,
// // graphqlImport,
// settings,
// 'model.field': model?.fields.find(f => f.name === field.name).type,
// });
// }

if (settings?.shouldHideField({ name: inputType.name, input: true })) {
importDeclarations.add('HideField', '@nestjs/graphql');
property.decorators?.push({ name: 'HideField', arguments: [] });
} else {
ok(property.decorators);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
property.decorators!.push({
property.decorators.push({
name: 'Field',
arguments: [
`() => ${isList ? `[${graphqlType}]` : graphqlType}`,
Expand All @@ -169,7 +164,7 @@ export function inputType(
continue;
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
property.decorators!.push({
property.decorators.push({
name: options.name,
arguments: options.arguments,
});
Expand All @@ -180,6 +175,21 @@ export function inputType(
importDeclarations.create(options);
}
}

for (const decorate of config.decorate) {
if (
decorate.isMatchField(name) &&
decorate.isMatchType(inputType.name)
) {
property.decorators.push({
name: decorate.name,
arguments: decorate.arguments?.map(x =>
pupa(x, { propertyType }),
),
});
importDeclarations.create(decorate);
}
}
}

eventEmitter.emitSync('ClassProperty', property, { location, isList });
Expand Down
Loading

0 comments on commit c5e14b7

Please sign in to comment.