Skip to content

Commit

Permalink
feat(compatibility): New configuration option `unsafeCompatibleWhereU…
Browse files Browse the repository at this point in the history
…niqueInput`

close: unlight#177
  • Loading branch information
unlight committed Sep 23, 2023
1 parent 30c8f59 commit 72a3dab
Show file tree
Hide file tree
Showing 9 changed files with 117 additions and 24 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,13 @@ Type: `boolean`
Default: `false`
**Note**: It will break compatiblity between Prisma types and generated classes.

#### `unsafeCompatibleWhereUniqueInput`

This trick TypeScript and set property as non optional for all fields in `*WhereUniqueInput` classes.
See [#177](https://github.com/unlight/prisma-nestjs-graphql/issues/177) for more details.
Type: `boolean`
Default: `false`

#### `useInputType`

Since GraphQL does not support input union type, this setting map
Expand Down
1 change: 1 addition & 0 deletions src/event-names.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const BeforeGenerateField = 'BeforeGenerateField';
3 changes: 2 additions & 1 deletion src/handlers/combine-scalar-filters.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import AwaitEventEmitter from 'await-event-emitter';
import { cloneDeep, keyBy, remove } from 'lodash';

import { BeforeGenerateField } from '../event-names';
import { DMMF, EventArguments, InputType } from '../types';

/**
* Subscribes on 'BeforeInputType'
*/
export function combineScalarFilters(eventEmitter: AwaitEventEmitter) {
eventEmitter.on('BeforeInputType', beforeInputType);
eventEmitter.on('BeforeGenerateField', beforeGenerateField);
eventEmitter.on(BeforeGenerateField, beforeGenerateField);
eventEmitter.on('PostBegin', postBegin);
}

Expand Down
41 changes: 20 additions & 21 deletions src/handlers/input-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { castArray, last } from 'lodash';
import pupa from 'pupa';
import { ClassDeclarationStructure, StructureKind } from 'ts-morph';

import { BeforeGenerateField } from '../event-names';
import { getGraphqlImport } from '../helpers/get-graphql-import';
import { getGraphqlInputType } from '../helpers/get-graphql-input-type';
import { getPropertyType } from '../helpers/get-property-type';
Expand Down Expand Up @@ -72,11 +73,12 @@ export function inputType(
const useInputType = config.useInputType.find(x =>
inputType.name.includes(x.typeName),
);
const isWhereUnique = isWhereUniqueInputType(inputType.name);

for (const field of inputType.fields) {
field.inputTypes = field.inputTypes.filter(t => !removeTypes.has(String(t.type)));

eventEmitter.emitSync('BeforeGenerateField', field, args);
eventEmitter.emitSync(BeforeGenerateField, field, args);

const { inputTypes, isRequired, name } = field;

Expand All @@ -96,10 +98,14 @@ export function inputType(
});
const modelField = model?.fields.find(f => f.name === name);
const isCustomsApplicable = typeName === modelField?.type;
const atLeastKeys = model && getWhereUniqueAtLeastKeys(model);
const whereUniqueInputType =
isWhereUniqueInputType(typeName) &&
model &&
`Prisma.AtLeast<${typeName}, ${getWhereUniqueAtLeastKeys(model)}>`;
atLeastKeys &&
`Prisma.AtLeast<${typeName}, ${atLeastKeys
.map(name => `'${name}'`)
.join(' | ')}>`;

const propertyType = castArray(
propertySettings?.name ||
whereUniqueInputType ||
Expand All @@ -108,12 +114,21 @@ export function inputType(
type: typeName,
}),
);

const hasExclamationToken = Boolean(
isWhereUnique &&
config.unsafeCompatibleWhereUniqueInput &&
atLeastKeys?.includes(name),
);
const property = propertyStructure({
name,
isNullable: !isRequired,
hasExclamationToken: hasExclamationToken || undefined,
hasQuestionToken: hasExclamationToken ? false : undefined,
propertyType,
isList,
});

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
classStructure.properties!.push(property);

Expand All @@ -136,7 +151,7 @@ export function inputType(
config.decorate.some(
d =>
d.name === 'HideField' &&
d.from === '@nestjs/graphql' &&
d.from === moduleSpecifier &&
d.isMatchField(name) &&
d.isMatchType(inputType.name),
);
Expand Down Expand Up @@ -182,7 +197,7 @@ export function inputType(
ok(property.decorators, 'property.decorators is undefined');

if (shouldHideField) {
importDeclarations.add('HideField', '@nestjs/graphql');
importDeclarations.add('HideField', moduleSpecifier);
property.decorators.push({ name: 'HideField', arguments: [] });
} else {
// Generate `@Field()` decorator
Expand All @@ -197,22 +212,6 @@ export function inputType(
],
});

// Debug
// if (classStructure.name === 'XInput') {
// console.log('------------');
// console.log({
// field,
// property,
// modelField,
// graphqlInputType,
// 'args.inputType': args.inputType,
// 'classStructure.name': classStructure.name,
// classTransformerTypeModels,
// modelName,
// graphqlType,
// });
// }

if (graphqlType === 'GraphQLDecimal') {
importDeclarations.add('transformToDecimal', 'prisma-graphql-type-decimal');
importDeclarations.add('Transform', 'class-transformer');
Expand Down
8 changes: 8 additions & 0 deletions src/helpers/create-config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,12 @@ describe('createConfig', () => {
expect(result.emitBlocks.prismaEnums).toEqual(true);
expect(result.emitBlocks.schemaEnums).toEqual(true);
});

it('unsafeCompatibleWhereUniqueInput', () => {
const result = createConfig({
unsafeCompatibleWhereUniqueInput: 'true',
});

expect(result.unsafeCompatibleWhereUniqueInput).toEqual(true);
});
});
3 changes: 3 additions & 0 deletions src/helpers/create-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ export function createConfig(data: Record<string, unknown>) {
requireSingleFieldsInWhereUniqueInput: toBoolean(
config.requireSingleFieldsInWhereUniqueInput,
),
unsafeCompatibleWhereUniqueInput: toBoolean(
config.unsafeCompatibleWhereUniqueInput,
),
graphqlScalars: (config.graphqlScalars || {}) as Record<
string,
ImportNameSpec | undefined
Expand Down
2 changes: 1 addition & 1 deletion src/helpers/get-where-unique-at-least-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function getWhereUniqueAtLeastKeys(model: DMMF.Model) {
names.push(createFieldName(uniqueIndex));
}

return names.map(name => `'${name}'`).join(' | ');
return names;
}

function createFieldName(args: { name?: string | null; fields: string[] }) {
Expand Down
14 changes: 14 additions & 0 deletions src/test/compatibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { UserListRelationFilter } from '../../@generated/user/user-list-relation
import { UserMaxOrderByAggregateInput } from '../../@generated/user/user-max-order-by-aggregate.input';
import { UserScalarFieldEnum } from '../../@generated/user/user-scalar-field.enum';
import { UserWhereInput } from '../../@generated/user/user-where.input';
import { UserWhereUniqueInput } from '../../@generated/user/user-where-unique.input';
import { Field } from '@nestjs/graphql';

let $prisma = new PrismaClient();

Expand Down Expand Up @@ -199,3 +201,15 @@ let $prisma = new PrismaClient();
console.log('result', result);
});
}

{
class UserWhereUniqueInput1 extends UserWhereUniqueInput {
@Field(() => String, { nullable: true })
id!: string;
}
const userWhereUniqueInput1: UserWhereUniqueInput1 = { id: '1' };

$prisma.user.findUnique({
where: userWhereUniqueInput1,
});
}
62 changes: 61 additions & 1 deletion src/test/generate.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ describe('model with one id int', () => {
it('aggregate user output count', () => {
const s = testSourceFile({
project,
file: 'aggregate-user.output.ts',
class: 'AggregateUser',
property: '_count',
});
expect(s.property?.type).toEqual('UserCountAggregate');
Expand Down Expand Up @@ -2411,3 +2411,63 @@ describe('deprecation reason', () => {
);
});
});

describe('unsafeCompatibleWhereUniqueInput', () => {
it('user id', async () => {
({ project, sourceFiles } = await testGenerate({
schema: `
model User {
id String @id
}`,
options: [
`outputFilePattern = "{name}.{type}.ts"`,
`unsafeCompatibleWhereUniqueInput = true`,
],
}));
const s = testSourceFile({
project,
class: 'UserWhereUniqueInput',
property: 'id',
});
expect(s.property?.hasExclamationToken).toBe(true);
});

it('user id email', async () => {
({ project, sourceFiles } = await testGenerate({
schema: `
model User {
id String @id @default(cuid())
name String
email String @unique @db.VarChar(255)
password String
@@unique([email, name])
}`,
options: [
`outputFilePattern = "{name}.{type}.ts"`,
`unsafeCompatibleWhereUniqueInput = true`,
],
}));

const id = testSourceFile({
project,
class: 'UserWhereUniqueInput',
property: 'id',
});
expect(id.property?.hasExclamationToken).toBe(true);

const email = testSourceFile({
project,
class: 'UserWhereUniqueInput',
property: 'email',
});
expect(email.property?.hasExclamationToken).toBe(true);

const emailName = testSourceFile({
project,
class: 'UserWhereUniqueInput',
property: 'email_name',
});
expect(emailName.property?.hasExclamationToken).toBe(true);
});
});

0 comments on commit 72a3dab

Please sign in to comment.