diff --git a/packages/core/src/common/index.ts b/packages/core/src/common/index.ts index 3a85efa9f..a132017a9 100644 --- a/packages/core/src/common/index.ts +++ b/packages/core/src/common/index.ts @@ -2,3 +2,4 @@ export * from './deep-partial.type'; export * from './class.type'; export * from './reflect.utils'; export * from './class.utils'; +export * from './misc.utils'; diff --git a/packages/core/src/common/misc.utils.ts b/packages/core/src/common/misc.utils.ts new file mode 100644 index 000000000..f8ad358de --- /dev/null +++ b/packages/core/src/common/misc.utils.ts @@ -0,0 +1,5 @@ +export type NamedType = { name: string }; +// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/explicit-module-boundary-types +export const isNamed = (SomeType: any): SomeType is NamedType => + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + 'name' in SomeType && typeof SomeType.name === 'string'; diff --git a/packages/query-graphql/__tests__/__fixtures__/delete-one-custom-id-input-type.graphql b/packages/query-graphql/__tests__/__fixtures__/delete-one-custom-id-input-type.graphql new file mode 100644 index 000000000..f032d21df --- /dev/null +++ b/packages/query-graphql/__tests__/__fixtures__/delete-one-custom-id-input-type.graphql @@ -0,0 +1,8 @@ +type Query { + test(input: DeleteOneCustomId!): Int! +} + +input DeleteOneCustomId { + """The id of the record to delete.""" + id: String! +} diff --git a/packages/query-graphql/__tests__/__fixtures__/find-one-args-type.graphql b/packages/query-graphql/__tests__/__fixtures__/find-one-args-type.graphql new file mode 100644 index 000000000..44a75596c --- /dev/null +++ b/packages/query-graphql/__tests__/__fixtures__/find-one-args-type.graphql @@ -0,0 +1,6 @@ +type Query { + test( + """The id of the record to find.""" + id: ID! + ): Int! +} diff --git a/packages/query-graphql/__tests__/__fixtures__/find-one-custom-id-args-type.graphql b/packages/query-graphql/__tests__/__fixtures__/find-one-custom-id-args-type.graphql new file mode 100644 index 000000000..5be7d3c65 --- /dev/null +++ b/packages/query-graphql/__tests__/__fixtures__/find-one-custom-id-args-type.graphql @@ -0,0 +1,6 @@ +type Query { + test( + """The id of the record to find.""" + id: String! + ): Int! +} diff --git a/packages/query-graphql/__tests__/__fixtures__/index.ts b/packages/query-graphql/__tests__/__fixtures__/index.ts index 481a957c7..412439f6d 100644 --- a/packages/query-graphql/__tests__/__fixtures__/index.ts +++ b/packages/query-graphql/__tests__/__fixtures__/index.ts @@ -30,12 +30,36 @@ export const updateManyResponseTypeSDL = readGraphql(resolve(__dirname, './updat export const createOneInputTypeSDL = readGraphql(resolve(__dirname, './create-one-input-type.graphql')); export const createManyInputTypeSDL = readGraphql(resolve(__dirname, './create-many-input-type.graphql')); export const updateOneInputTypeSDL = readGraphql(resolve(__dirname, './update-one-input-type.graphql')); +export const updateOneCustomIdInputTypeSDL = readGraphql( + resolve(__dirname, './update-one-custom-id-input-type.graphql'), +); export const updateManyInputTypeSDL = readGraphql(resolve(__dirname, './update-many-input-type.graphql')); export const deleteOneInputTypeSDL = readGraphql(resolve(__dirname, './delete-one-input-type.graphql')); +export const deleteOneCustomIdInputTypeSDL = readGraphql( + resolve(__dirname, './delete-one-custom-id-input-type.graphql'), +); export const deleteManyInputTypeSDL = readGraphql(resolve(__dirname, './delete-many-input-type.graphql')); export const mutationArgsTypeSDL = readGraphql(resolve(__dirname, './mutation-args-type.graphql')); export const relationInputTypeSDL = readGraphql(resolve(__dirname, './relation-input-type.graphql')); +export const relationCustomParentIdInputTypeSDL = readGraphql( + resolve(__dirname, './relation-custom-parent-id-input-type.graphql'), +); +export const relationCustomRelationIdInputTypeSDL = readGraphql( + resolve(__dirname, './relation-custom-relation-id-input-type.graphql'), +); +export const relationCustomParentRelationIdInputTypeSDL = readGraphql( + resolve(__dirname, './relation-custom-parent-relation-id-input-type.graphql'), +); export const relationsInputTypeSDL = readGraphql(resolve(__dirname, './relations-input-type.graphql')); +export const relationsCustomParentIdInputTypeSDL = readGraphql( + resolve(__dirname, './relations-custom-parent-id-input-type.graphql'), +); +export const relationsCustomRelationIdInputTypeSDL = readGraphql( + resolve(__dirname, './relations-custom-relation-id-input-type.graphql'), +); +export const relationsCustomParentRelationIdInputTypeSDL = readGraphql( + resolve(__dirname, './relations-custom-parent-relation-id-input-type.graphql'), +); export const filterInputTypeSDL = readGraphql(resolve(__dirname, './filter-input-type.graphql')); export const filterAllowedComparisonsInputTypeSDL = readGraphql( resolve(__dirname, './filter-allowed-comparisons-input-type.graphql'), @@ -52,6 +76,8 @@ export const filterAllowedBooleanExpressionsOnlyOrTypeSDL = readGraphql( export const filterNoBooleanExpressionsTypeSDL = readGraphql( resolve(__dirname, './filter-no-boolean-expression-type.graphql'), ); +export const findOneArgsTypeSDL = readGraphql(resolve(__dirname, './find-one-args-type.graphql')); +export const findOneCustomIdArgsTypeSDL = readGraphql(resolve(__dirname, './find-one-custom-id-args-type.graphql')); export const updateFilterInputTypeSDL = readGraphql(resolve(__dirname, './update-filter-input-type.graphql')); export const deleteFilterInputTypeSDL = readGraphql(resolve(__dirname, './delete-filter-input-type.graphql')); export const subscriptionFilterInputTypeSDL = readGraphql( diff --git a/packages/query-graphql/__tests__/__fixtures__/relation-custom-parent-id-input-type.graphql b/packages/query-graphql/__tests__/__fixtures__/relation-custom-parent-id-input-type.graphql new file mode 100644 index 000000000..2ff040a83 --- /dev/null +++ b/packages/query-graphql/__tests__/__fixtures__/relation-custom-parent-id-input-type.graphql @@ -0,0 +1,11 @@ +type Query { + test(input: RelationCustomParentIdInput!): Int! +} + +input RelationCustomParentIdInput { + """The id of the record.""" + id: String! + + """The id of relation.""" + relationId: ID! +} diff --git a/packages/query-graphql/__tests__/__fixtures__/relation-custom-parent-relation-id-input-type.graphql b/packages/query-graphql/__tests__/__fixtures__/relation-custom-parent-relation-id-input-type.graphql new file mode 100644 index 000000000..85c6ab42d --- /dev/null +++ b/packages/query-graphql/__tests__/__fixtures__/relation-custom-parent-relation-id-input-type.graphql @@ -0,0 +1,11 @@ +type Query { + test(input: RelationCustomParentAndRelationIdInput!): Int! +} + +input RelationCustomParentAndRelationIdInput { + """The id of the record.""" + id: String! + + """The id of relation.""" + relationId: String! +} diff --git a/packages/query-graphql/__tests__/__fixtures__/relation-custom-relation-id-input-type.graphql b/packages/query-graphql/__tests__/__fixtures__/relation-custom-relation-id-input-type.graphql new file mode 100644 index 000000000..9aa995978 --- /dev/null +++ b/packages/query-graphql/__tests__/__fixtures__/relation-custom-relation-id-input-type.graphql @@ -0,0 +1,11 @@ +type Query { + test(input: RelationCustomRelationIdInput!): Int! +} + +input RelationCustomRelationIdInput { + """The id of the record.""" + id: ID! + + """The id of relation.""" + relationId: String! +} diff --git a/packages/query-graphql/__tests__/__fixtures__/relations-custom-parent-id-input-type.graphql b/packages/query-graphql/__tests__/__fixtures__/relations-custom-parent-id-input-type.graphql new file mode 100644 index 000000000..a8f6e1724 --- /dev/null +++ b/packages/query-graphql/__tests__/__fixtures__/relations-custom-parent-id-input-type.graphql @@ -0,0 +1,11 @@ +type Query { + test(input: RelationsCustomParentIdInput!): Int! +} + +input RelationsCustomParentIdInput { + """The id of the record.""" + id: String! + + """The ids of the relations.""" + relationIds: [ID!]! +} diff --git a/packages/query-graphql/__tests__/__fixtures__/relations-custom-parent-relation-id-input-type.graphql b/packages/query-graphql/__tests__/__fixtures__/relations-custom-parent-relation-id-input-type.graphql new file mode 100644 index 000000000..14abe29b4 --- /dev/null +++ b/packages/query-graphql/__tests__/__fixtures__/relations-custom-parent-relation-id-input-type.graphql @@ -0,0 +1,11 @@ +type Query { + test(input: RelationsCustomParentRelationIdInput!): Int! +} + +input RelationsCustomParentRelationIdInput { + """The id of the record.""" + id: String! + + """The ids of the relations.""" + relationIds: [String!]! +} diff --git a/packages/query-graphql/__tests__/__fixtures__/relations-custom-relation-id-input-type.graphql b/packages/query-graphql/__tests__/__fixtures__/relations-custom-relation-id-input-type.graphql new file mode 100644 index 000000000..d0cf43c28 --- /dev/null +++ b/packages/query-graphql/__tests__/__fixtures__/relations-custom-relation-id-input-type.graphql @@ -0,0 +1,11 @@ +type Query { + test(input: RelationsCustomRelationIdInput!): Int! +} + +input RelationsCustomRelationIdInput { + """The id of the record.""" + id: ID! + + """The ids of the relations.""" + relationIds: [String!]! +} diff --git a/packages/query-graphql/__tests__/__fixtures__/update-one-custom-id-input-type.graphql b/packages/query-graphql/__tests__/__fixtures__/update-one-custom-id-input-type.graphql new file mode 100644 index 000000000..64d184d3e --- /dev/null +++ b/packages/query-graphql/__tests__/__fixtures__/update-one-custom-id-input-type.graphql @@ -0,0 +1,15 @@ +type Query { + updateTest(input: UpdateOneCustomId!): Int! +} + +input UpdateOneCustomId { + """The id of the record to update""" + id: String! + + """The update to apply.""" + update: FakeUpdateOneType! +} + +input FakeUpdateOneType { + name: String! +} diff --git a/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-basic.resolver.graphql b/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-basic.resolver.graphql index d35184896..9ab89905f 100644 --- a/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-basic.resolver.graphql +++ b/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-basic.resolver.graphql @@ -18,11 +18,11 @@ type Query { } type Mutation { - deleteOneTestResolverDTO(input: DeleteOneInput!): TestResolverDTODeleteResponse! + deleteOneTestResolverDTO(input: DeleteOneTestResolverDTOInput!): TestResolverDTODeleteResponse! deleteManyTestResolverDTOS(input: DeleteManyTestResolverDTOSInput!): DeleteManyResponse! } -input DeleteOneInput { +input DeleteOneTestResolverDTOInput { """The id of the record to delete.""" id: ID! } diff --git a/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-custom-many-input.resolver.graphql b/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-custom-many-input.resolver.graphql index 10dbb7d11..122cf12dc 100644 --- a/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-custom-many-input.resolver.graphql +++ b/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-custom-many-input.resolver.graphql @@ -18,11 +18,11 @@ type Query { } type Mutation { - deleteOneTestResolverDTO(input: DeleteOneInput!): TestResolverDTODeleteResponse! + deleteOneTestResolverDTO(input: DeleteOneTestResolverDTOInput!): TestResolverDTODeleteResponse! deleteManyTestResolverDTOS(input: CustomDeleteManyInput!): DeleteManyResponse! } -input DeleteOneInput { +input DeleteOneTestResolverDTOInput { """The id of the record to delete.""" id: ID! } diff --git a/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-custom-many-mutation.resolver.graphql b/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-custom-many-mutation.resolver.graphql index b70c242b4..5381a419f 100644 --- a/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-custom-many-mutation.resolver.graphql +++ b/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-custom-many-mutation.resolver.graphql @@ -18,11 +18,11 @@ type Query { } type Mutation { - deleteOneTestResolverDTO(input: DeleteOneInput!): TestResolverDTODeleteResponse! + deleteOneTestResolverDTO(input: DeleteOneTestResolverDTOInput!): TestResolverDTODeleteResponse! delete_many_test(input: DeleteManyTestResolverDTOSInput!): DeleteManyResponse! } -input DeleteOneInput { +input DeleteOneTestResolverDTOInput { """The id of the record to delete.""" id: ID! } diff --git a/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-custom-name.resolver.graphql b/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-custom-name.resolver.graphql index 640f114ab..8698eed1e 100644 --- a/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-custom-name.resolver.graphql +++ b/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-custom-name.resolver.graphql @@ -18,11 +18,11 @@ type Query { } type Mutation { - deleteOneTest(input: DeleteOneInput!): TestDeleteResponse! + deleteOneTest(input: DeleteOneTestInput!): TestDeleteResponse! deleteManyTests(input: DeleteManyTestsInput!): DeleteManyResponse! } -input DeleteOneInput { +input DeleteOneTestInput { """The id of the record to delete.""" id: ID! } diff --git a/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-custom-one-mutation.resolver.graphql b/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-custom-one-mutation.resolver.graphql index 7eb7002cb..6dca609e9 100644 --- a/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-custom-one-mutation.resolver.graphql +++ b/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-custom-one-mutation.resolver.graphql @@ -18,11 +18,11 @@ type Query { } type Mutation { - delete_one_test(input: DeleteOneInput!): TestResolverDTODeleteResponse! + delete_one_test(input: DeleteOneTestResolverDTOInput!): TestResolverDTODeleteResponse! deleteManyTestResolverDTOS(input: DeleteManyTestResolverDTOSInput!): DeleteManyResponse! } -input DeleteOneInput { +input DeleteOneTestResolverDTOInput { """The id of the record to delete.""" id: ID! } diff --git a/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-many-disabled.resolver.graphql b/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-many-disabled.resolver.graphql index a0cb13b39..64467b2eb 100644 --- a/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-many-disabled.resolver.graphql +++ b/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-many-disabled.resolver.graphql @@ -13,10 +13,10 @@ type Query { } type Mutation { - deleteOneTestResolverDTO(input: DeleteOneInput!): TestResolverDTODeleteResponse! + deleteOneTestResolverDTO(input: DeleteOneTestResolverDTOInput!): TestResolverDTODeleteResponse! } -input DeleteOneInput { +input DeleteOneTestResolverDTOInput { """The id of the record to delete.""" id: ID! } diff --git a/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-many-subscription.resolver.graphql b/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-many-subscription.resolver.graphql index b048e1ada..72036abae 100644 --- a/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-many-subscription.resolver.graphql +++ b/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-many-subscription.resolver.graphql @@ -18,11 +18,11 @@ type Query { } type Mutation { - deleteOneTestResolverDTO(input: DeleteOneInput!): TestResolverDTODeleteResponse! + deleteOneTestResolverDTO(input: DeleteOneTestResolverDTOInput!): TestResolverDTODeleteResponse! deleteManyTestResolverDTOS(input: DeleteManyTestResolverDTOSInput!): DeleteManyResponse! } -input DeleteOneInput { +input DeleteOneTestResolverDTOInput { """The id of the record to delete.""" id: ID! } diff --git a/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-one-subscription.resolver.graphql b/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-one-subscription.resolver.graphql index 6e60a4c02..a2da32674 100644 --- a/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-one-subscription.resolver.graphql +++ b/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-one-subscription.resolver.graphql @@ -18,11 +18,11 @@ type Query { } type Mutation { - deleteOneTestResolverDTO(input: DeleteOneInput!): TestResolverDTODeleteResponse! + deleteOneTestResolverDTO(input: DeleteOneTestResolverDTOInput!): TestResolverDTODeleteResponse! deleteManyTestResolverDTOS(input: DeleteManyTestResolverDTOSInput!): DeleteManyResponse! } -input DeleteOneInput { +input DeleteOneTestResolverDTOInput { """The id of the record to delete.""" id: ID! } diff --git a/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-subscription.resolver.graphql b/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-subscription.resolver.graphql index 7af503c1f..807d236b1 100644 --- a/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-subscription.resolver.graphql +++ b/packages/query-graphql/__tests__/resolvers/__fixtures__/delete/delete-subscription.resolver.graphql @@ -18,11 +18,11 @@ type Query { } type Mutation { - deleteOneTestResolverDTO(input: DeleteOneInput!): TestResolverDTODeleteResponse! + deleteOneTestResolverDTO(input: DeleteOneTestResolverDTOInput!): TestResolverDTODeleteResponse! deleteManyTestResolverDTOS(input: DeleteManyTestResolverDTOSInput!): DeleteManyResponse! } -input DeleteOneInput { +input DeleteOneTestResolverDTOInput { """The id of the record to delete.""" id: ID! } diff --git a/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/remove/remove-relation-many-custom-name.resolver.graphql b/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/remove/remove-relation-many-custom-name.resolver.graphql index 5aea3c762..3037e1035 100644 --- a/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/remove/remove-relation-many-custom-name.resolver.graphql +++ b/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/remove/remove-relation-many-custom-name.resolver.graphql @@ -8,10 +8,10 @@ type Query { } type Mutation { - removeTestsFromTestResolverDTO(input: RelationsInput!): TestResolverDTO! + removeTestsFromTestResolverDTO(input: RemoveTestsFromTestResolverDTOInput!): TestResolverDTO! } -input RelationsInput { +input RemoveTestsFromTestResolverDTOInput { """The id of the record.""" id: ID! diff --git a/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/remove/remove-relation-many.resolver.graphql b/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/remove/remove-relation-many.resolver.graphql index 1a706cc2e..783e4096b 100644 --- a/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/remove/remove-relation-many.resolver.graphql +++ b/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/remove/remove-relation-many.resolver.graphql @@ -8,10 +8,10 @@ type Query { } type Mutation { - removeRelationsFromTestResolverDTO(input: RelationsInput!): TestResolverDTO! + removeRelationsFromTestResolverDTO(input: RemoveRelationsFromTestResolverDTOInput!): TestResolverDTO! } -input RelationsInput { +input RemoveRelationsFromTestResolverDTOInput { """The id of the record.""" id: ID! diff --git a/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/remove/remove-relation-one-custom-name.resolver.graphql b/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/remove/remove-relation-one-custom-name.resolver.graphql index 716cb74e7..e29c94b29 100644 --- a/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/remove/remove-relation-one-custom-name.resolver.graphql +++ b/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/remove/remove-relation-one-custom-name.resolver.graphql @@ -8,10 +8,10 @@ type Query { } type Mutation { - removeTestFromTestResolverDTO(input: RelationInput!): TestResolverDTO! + removeTestFromTestResolverDTO(input: RemoveTestFromTestResolverDTOInput!): TestResolverDTO! } -input RelationInput { +input RemoveTestFromTestResolverDTOInput { """The id of the record.""" id: ID! diff --git a/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/remove/remove-relation-one.resolver.graphql b/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/remove/remove-relation-one.resolver.graphql index 57b2f4d81..547f82fae 100644 --- a/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/remove/remove-relation-one.resolver.graphql +++ b/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/remove/remove-relation-one.resolver.graphql @@ -8,10 +8,10 @@ type Query { } type Mutation { - removeRelationFromTestResolverDTO(input: RelationInput!): TestResolverDTO! + removeRelationFromTestResolverDTO(input: RemoveRelationFromTestResolverDTOInput!): TestResolverDTO! } -input RelationInput { +input RemoveRelationFromTestResolverDTOInput { """The id of the record.""" id: ID! diff --git a/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/update/update-relation-many-custom-name.resolver.graphql b/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/update/update-relation-many-custom-name.resolver.graphql index f22a5df70..41993d0e5 100644 --- a/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/update/update-relation-many-custom-name.resolver.graphql +++ b/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/update/update-relation-many-custom-name.resolver.graphql @@ -8,11 +8,19 @@ type Query { } type Mutation { - addTestsToTestResolverDTO(input: RelationsInput!): TestResolverDTO! - setTestsOnTestResolverDTO(input: RelationsInput!): TestResolverDTO! + addTestsToTestResolverDTO(input: AddTestsToTestResolverDTOInput!): TestResolverDTO! + setTestsOnTestResolverDTO(input: SetTestsOnTestResolverDTOInput!): TestResolverDTO! } -input RelationsInput { +input AddTestsToTestResolverDTOInput { + """The id of the record.""" + id: ID! + + """The ids of the relations.""" + relationIds: [ID!]! +} + +input SetTestsOnTestResolverDTOInput { """The id of the record.""" id: ID! diff --git a/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/update/update-relation-many.resolver.graphql b/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/update/update-relation-many.resolver.graphql index 7efbcfb6b..82edcd9f8 100644 --- a/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/update/update-relation-many.resolver.graphql +++ b/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/update/update-relation-many.resolver.graphql @@ -8,11 +8,19 @@ type Query { } type Mutation { - addRelationsToTestResolverDTO(input: RelationsInput!): TestResolverDTO! - setRelationsOnTestResolverDTO(input: RelationsInput!): TestResolverDTO! + addRelationsToTestResolverDTO(input: AddRelationsToTestResolverDTOInput!): TestResolverDTO! + setRelationsOnTestResolverDTO(input: SetRelationsOnTestResolverDTOInput!): TestResolverDTO! } -input RelationsInput { +input AddRelationsToTestResolverDTOInput { + """The id of the record.""" + id: ID! + + """The ids of the relations.""" + relationIds: [ID!]! +} + +input SetRelationsOnTestResolverDTOInput { """The id of the record.""" id: ID! diff --git a/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/update/update-relation-one-custom-name.resolver.graphql b/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/update/update-relation-one-custom-name.resolver.graphql index b6fc3390a..db89cfbf1 100644 --- a/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/update/update-relation-one-custom-name.resolver.graphql +++ b/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/update/update-relation-one-custom-name.resolver.graphql @@ -8,10 +8,10 @@ type Query { } type Mutation { - setTestOnTestResolverDTO(input: RelationInput!): TestResolverDTO! + setTestOnTestResolverDTO(input: SetTestOnTestResolverDTOInput!): TestResolverDTO! } -input RelationInput { +input SetTestOnTestResolverDTOInput { """The id of the record.""" id: ID! diff --git a/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/update/update-relation-one.resolver.graphql b/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/update/update-relation-one.resolver.graphql index 51a33f51c..bbe3f4204 100644 --- a/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/update/update-relation-one.resolver.graphql +++ b/packages/query-graphql/__tests__/resolvers/relations/__fixtures__/update/update-relation-one.resolver.graphql @@ -8,10 +8,10 @@ type Query { } type Mutation { - setRelationOnTestResolverDTO(input: RelationInput!): TestResolverDTO! + setRelationOnTestResolverDTO(input: SetRelationOnTestResolverDTOInput!): TestResolverDTO! } -input RelationInput { +input SetRelationOnTestResolverDTOInput { """The id of the record.""" id: ID! diff --git a/packages/query-graphql/__tests__/resolvers/update.resolver.spec.ts b/packages/query-graphql/__tests__/resolvers/update.resolver.spec.ts index d679c6064..5652256ec 100644 --- a/packages/query-graphql/__tests__/resolvers/update.resolver.spec.ts +++ b/packages/query-graphql/__tests__/resolvers/update.resolver.spec.ts @@ -75,7 +75,7 @@ describe('UpdateResolver', () => { describe('#updateOne', () => { it('should use the provided UpdateOneInput type', () => { @InputType() - class CustomUpdateOneInput extends UpdateOneInputType(TestResolverInputDTO) { + class CustomUpdateOneInput extends UpdateOneInputType(TestResolverDTO, TestResolverInputDTO) { @Field() other!: string; } diff --git a/packages/query-graphql/__tests__/types/delete-one-input.type.spec.ts b/packages/query-graphql/__tests__/types/delete-one-input.type.spec.ts index 91099e4b2..59460dac3 100644 --- a/packages/query-graphql/__tests__/types/delete-one-input.type.spec.ts +++ b/packages/query-graphql/__tests__/types/delete-one-input.type.spec.ts @@ -1,14 +1,21 @@ +// eslint-disable-next-line max-classes-per-file import { plainToClass } from 'class-transformer'; import { validateSync } from 'class-validator'; -import { Resolver, Query, Args, Int, InputType } from '@nestjs/graphql'; -import { DeleteOneInputType } from '../../src'; -import { deleteOneInputTypeSDL, expectSDL } from '../__fixtures__'; +import { Resolver, Query, Args, Int, InputType, ObjectType } from '@nestjs/graphql'; +import { DeleteOneInputType, FilterableField, IDField } from '../../src'; +import { deleteOneInputTypeSDL, deleteOneCustomIdInputTypeSDL, expectSDL } from '../__fixtures__'; describe('DeleteOneInputType', (): void => { + @ObjectType() + class DeleteOneDTO { + @FilterableField() + field!: string; + } + @InputType() - class DeleteOne extends DeleteOneInputType() {} + class DeleteOne extends DeleteOneInputType(DeleteOneDTO) {} - it('should create an args type with the field as the type', async () => { + it('should create an input type with id field as the type', async () => { @Resolver() class DeleteOneInputTypeSpec { @Query(() => Int) @@ -20,6 +27,27 @@ describe('DeleteOneInputType', (): void => { return expectSDL([DeleteOneInputTypeSpec], deleteOneInputTypeSDL); }); + it('should create an input type with a custom ID type', async () => { + @ObjectType() + class DeleteOneCustomIDDTO { + @IDField(() => String) + field!: string; + } + + @InputType() + class DeleteOneCustomId extends DeleteOneInputType(DeleteOneCustomIDDTO) {} + + @Resolver() + class DeleteOneInputTypeSpec { + @Query(() => Int) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + test(@Args('input') input: DeleteOneCustomId): number { + return 1; + } + } + return expectSDL([DeleteOneInputTypeSpec], deleteOneCustomIdInputTypeSDL); + }); + describe('validation', () => { it('should validate the id is defined', () => { const input = {}; diff --git a/packages/query-graphql/__tests__/types/find-one-args.type.spec.ts b/packages/query-graphql/__tests__/types/find-one-args.type.spec.ts new file mode 100644 index 000000000..968ffdc9b --- /dev/null +++ b/packages/query-graphql/__tests__/types/find-one-args.type.spec.ts @@ -0,0 +1,85 @@ +// eslint-disable-next-line max-classes-per-file +import { plainToClass } from 'class-transformer'; +import { validateSync } from 'class-validator'; +import { Resolver, Query, Args, Int, ArgsType, ObjectType } from '@nestjs/graphql'; +import { FilterableField, FindOneArgsType, IDField } from '../../src'; +import { findOneArgsTypeSDL, findOneCustomIdArgsTypeSDL, expectSDL } from '../__fixtures__'; + +describe('FindOneArgsType', (): void => { + @ObjectType() + class FindOneDTO { + @FilterableField() + field!: string; + } + + @ArgsType() + class FindOne extends FindOneArgsType(FindOneDTO) {} + + it('should create an args type with id field as the type', async () => { + @Resolver() + class FindOneArgsTypeSpec { + @Query(() => Int) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + test(@Args() input: FindOne): number { + return 1; + } + } + return expectSDL([FindOneArgsTypeSpec], findOneArgsTypeSDL); + }); + + it('should create an args type with a custom ID type', async () => { + @ObjectType() + class FindOneCustomIDDTO { + @IDField(() => String) + field!: string; + } + + @ArgsType() + class FindOneCustomId extends FindOneArgsType(FindOneCustomIDDTO) {} + + @Resolver() + class FindOneArgsTypeSpec { + @Query(() => Int) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + test(@Args() input: FindOneCustomId): number { + return 1; + } + } + return expectSDL([FindOneArgsTypeSpec], findOneCustomIdArgsTypeSDL); + }); + + describe('validation', () => { + it('should validate the id is defined', () => { + const input = {}; + const it = plainToClass(FindOne, input); + const errors = validateSync(it); + expect(errors).toEqual([ + { + children: [], + constraints: { + isNotEmpty: 'id should not be empty', + }, + property: 'id', + target: input, + }, + ]); + }); + + it('should validate the id is not empty', () => { + const input = { id: '' }; + const it = plainToClass(FindOne, input); + const errors = validateSync(it); + expect(errors).toEqual([ + { + children: [], + constraints: { + isNotEmpty: 'id should not be empty', + }, + property: 'id', + target: input, + value: '', + }, + ]); + }); + }); +}); diff --git a/packages/query-graphql/__tests__/types/relation-input.type.spec.ts b/packages/query-graphql/__tests__/types/relation-input.type.spec.ts index d2d0786b5..5e11225e7 100644 --- a/packages/query-graphql/__tests__/types/relation-input.type.spec.ts +++ b/packages/query-graphql/__tests__/types/relation-input.type.spec.ts @@ -1,14 +1,45 @@ +// eslint-disable-next-line max-classes-per-file import { plainToClass } from 'class-transformer'; import { validateSync } from 'class-validator'; -import { InputType, Resolver, Query, Args, Int } from '@nestjs/graphql'; -import { RelationInputType } from '../../src'; -import { expectSDL, relationInputTypeSDL } from '../__fixtures__'; +import { InputType, Resolver, Query, Args, Int, ObjectType } from '@nestjs/graphql'; +import { FilterableField, IDField, RelationInputType } from '../../src'; +import { + expectSDL, + relationInputTypeSDL, + relationCustomParentIdInputTypeSDL, + relationCustomRelationIdInputTypeSDL, + relationCustomParentRelationIdInputTypeSDL, +} from '../__fixtures__'; describe('RelationInputType', (): void => { + @ObjectType() + class ParentDTO { + @FilterableField() + field!: string; + } + + @ObjectType() + class ParentCustomIDDTO { + @IDField(() => String) + id!: string; + } + + @ObjectType() + class RelationDTO { + @FilterableField() + relationField!: string; + } + + @ObjectType() + class RelationCustomIDDTO { + @IDField(() => String) + relationId!: string; + } + @InputType() - class RelationInput extends RelationInputType() {} + class RelationInput extends RelationInputType(ParentDTO, RelationDTO) {} - it('should create an args type with an array field', () => { + it('should create an input type with an id and relationId', () => { @Resolver() class RelationInputTypeSpec { @Query(() => Int) @@ -20,6 +51,51 @@ describe('RelationInputType', (): void => { return expectSDL([RelationInputTypeSpec], relationInputTypeSDL); }); + it('should create an input type with a custom id for the parent', () => { + @InputType() + class RelationCustomParentIdInput extends RelationInputType(ParentCustomIDDTO, RelationDTO) {} + + @Resolver() + class RelationCustomIdInputTypeSpec { + @Query(() => Int) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + test(@Args('input') input: RelationCustomParentIdInput): number { + return 1; + } + } + return expectSDL([RelationCustomIdInputTypeSpec], relationCustomParentIdInputTypeSDL); + }); + + it('should create an input type with a custom id for the relation', () => { + @InputType() + class RelationCustomRelationIdInput extends RelationInputType(ParentDTO, RelationCustomIDDTO) {} + + @Resolver() + class RelationCustomIdInputTypeSpec { + @Query(() => Int) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + test(@Args('input') input: RelationCustomRelationIdInput): number { + return 1; + } + } + return expectSDL([RelationCustomIdInputTypeSpec], relationCustomRelationIdInputTypeSDL); + }); + + it('should create an input type with a custom id for the parent and relation', () => { + @InputType() + class RelationCustomParentAndRelationIdInput extends RelationInputType(ParentCustomIDDTO, RelationCustomIDDTO) {} + + @Resolver() + class RelationCustomIdInputTypeSpec { + @Query(() => Int) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + test(@Args('input') input: RelationCustomParentAndRelationIdInput): number { + return 1; + } + } + return expectSDL([RelationCustomIdInputTypeSpec], relationCustomParentRelationIdInputTypeSDL); + }); + it('should return the input when accessing the update field', () => { const input: RelationInputType = { id: 1, relationId: 2 }; const it = plainToClass(RelationInput, input); diff --git a/packages/query-graphql/__tests__/types/relations-input.type.spec.ts b/packages/query-graphql/__tests__/types/relations-input.type.spec.ts index 73748222b..a6a7cdf45 100644 --- a/packages/query-graphql/__tests__/types/relations-input.type.spec.ts +++ b/packages/query-graphql/__tests__/types/relations-input.type.spec.ts @@ -1,14 +1,45 @@ +// eslint-disable-next-line max-classes-per-file import { plainToClass } from 'class-transformer'; import { validateSync } from 'class-validator'; -import { Args, Query, Resolver, Int, InputType } from '@nestjs/graphql'; -import { RelationsInputType } from '../../src'; -import { expectSDL, relationsInputTypeSDL } from '../__fixtures__'; +import { Args, Query, Resolver, Int, InputType, ObjectType } from '@nestjs/graphql'; +import { FilterableField, IDField, RelationsInputType } from '../../src'; +import { + expectSDL, + relationsInputTypeSDL, + relationsCustomParentIdInputTypeSDL, + relationsCustomRelationIdInputTypeSDL, + relationsCustomParentRelationIdInputTypeSDL, +} from '../__fixtures__'; describe('RelationsInputType', (): void => { + @ObjectType() + class ParentDTO { + @FilterableField() + field!: string; + } + + @ObjectType() + class ParentCustomIDDTO { + @IDField(() => String) + id!: string; + } + + @ObjectType() + class RelationDTO { + @FilterableField() + relationField!: string; + } + + @ObjectType() + class RelationCustomIDDTO { + @IDField(() => String) + relationId!: string; + } + @InputType() - class RelationsInput extends RelationsInputType() {} + class RelationsInput extends RelationsInputType(ParentDTO, RelationDTO) {} - it('should create an args type with an array field', () => { + it('should create an input type with an id and relationIds fields', () => { @Resolver() class RelationsInputTypeSpec { @Query(() => Int) @@ -20,6 +51,51 @@ describe('RelationsInputType', (): void => { return expectSDL([RelationsInputTypeSpec], relationsInputTypeSDL); }); + it('should create an input type with a custom parent id', () => { + @InputType() + class RelationsCustomParentIdInput extends RelationsInputType(ParentCustomIDDTO, RelationDTO) {} + + @Resolver() + class RelationsCustomIdInputTypeSpec { + @Query(() => Int) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + test(@Args('input') input: RelationsCustomParentIdInput): number { + return 1; + } + } + return expectSDL([RelationsCustomIdInputTypeSpec], relationsCustomParentIdInputTypeSDL); + }); + + it('should create an input type with a custom relation id', () => { + @InputType() + class RelationsCustomRelationIdInput extends RelationsInputType(ParentDTO, RelationCustomIDDTO) {} + + @Resolver() + class RelationsCustomIdInputTypeSpec { + @Query(() => Int) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + test(@Args('input') input: RelationsCustomRelationIdInput): number { + return 1; + } + } + return expectSDL([RelationsCustomIdInputTypeSpec], relationsCustomRelationIdInputTypeSDL); + }); + + it('should create an input type with a custom parent and relation id', () => { + @InputType() + class RelationsCustomParentRelationIdInput extends RelationsInputType(ParentCustomIDDTO, RelationCustomIDDTO) {} + + @Resolver() + class RelationsCustomIdInputTypeSpec { + @Query(() => Int) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + test(@Args('input') input: RelationsCustomParentRelationIdInput): number { + return 1; + } + } + return expectSDL([RelationsCustomIdInputTypeSpec], relationsCustomParentRelationIdInputTypeSDL); + }); + it('should return the input when accessing the update field', () => { const input: RelationsInputType = { id: 1, relationIds: [2, 3, 4] }; const it = plainToClass(RelationsInput, input); diff --git a/packages/query-graphql/__tests__/types/update-one-input.type.spec.ts b/packages/query-graphql/__tests__/types/update-one-input.type.spec.ts index 78583c5d7..cdc18faaf 100644 --- a/packages/query-graphql/__tests__/types/update-one-input.type.spec.ts +++ b/packages/query-graphql/__tests__/types/update-one-input.type.spec.ts @@ -1,10 +1,17 @@ +// eslint-disable-next-line max-classes-per-file import { plainToClass } from 'class-transformer'; import { validateSync, MinLength } from 'class-validator'; -import { InputType, Resolver, Args, Field, Query, Int } from '@nestjs/graphql'; -import { UpdateOneInputType } from '../../src'; -import { expectSDL, updateOneInputTypeSDL } from '../__fixtures__'; +import { InputType, Resolver, Args, Field, Query, Int, ID, ObjectType } from '@nestjs/graphql'; +import { IDField, UpdateOneInputType } from '../../src'; +import { expectSDL, updateOneInputTypeSDL, updateOneCustomIdInputTypeSDL } from '../__fixtures__'; describe('UpdateOneInputType', (): void => { + @ObjectType() + class FakeDTO { + @Field(() => ID) + id!: string; + } + @InputType() class FakeUpdateOneType { @Field() @@ -13,9 +20,9 @@ describe('UpdateOneInputType', (): void => { } @InputType() - class UpdateOne extends UpdateOneInputType(FakeUpdateOneType) {} + class UpdateOne extends UpdateOneInputType(FakeDTO, FakeUpdateOneType) {} - it('should create an args type with the field as the type', async () => { + it('should create an input type with the id and update type as fields', async () => { @Resolver() class UpdateOneInputTypeSpec { @Query(() => Int) @@ -27,9 +34,30 @@ describe('UpdateOneInputType', (): void => { return expectSDL([UpdateOneInputTypeSpec], updateOneInputTypeSDL); }); + it('should create an input type with a custom id and update type as fields', async () => { + @ObjectType() + class FakeIDDTO { + @IDField(() => String) + id!: string; + } + + @InputType() + class UpdateOneCustomId extends UpdateOneInputType(FakeIDDTO, FakeUpdateOneType) {} + + @Resolver() + class UpdateOneCustomIdInputTypeSpec { + @Query(() => Int) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + updateTest(@Args('input') input: UpdateOneCustomId): number { + return 1; + } + } + return expectSDL([UpdateOneCustomIdInputTypeSpec], updateOneCustomIdInputTypeSDL); + }); + describe('validation', () => { it('should validate id is defined is not empty', () => { - const Type = UpdateOneInputType(FakeUpdateOneType); + const Type = UpdateOneInputType(FakeDTO, FakeUpdateOneType); const input = { update: { name: 'hello world' } }; const it = plainToClass(Type, input); const errors = validateSync(it); @@ -46,7 +74,7 @@ describe('UpdateOneInputType', (): void => { }); it('should validate id is not empty is defined is not empty', () => { - const Type = UpdateOneInputType(FakeUpdateOneType); + const Type = UpdateOneInputType(FakeDTO, FakeUpdateOneType); const input = { id: '', update: { name: 'hello world' } }; const it = plainToClass(Type, input); const errors = validateSync(it); @@ -64,7 +92,7 @@ describe('UpdateOneInputType', (): void => { }); it('should validate the update input', () => { - const Type = UpdateOneInputType(FakeUpdateOneType); + const Type = UpdateOneInputType(FakeDTO, FakeUpdateOneType); const input = { id: 'id-1', update: {} }; const it = plainToClass(Type, input); const errors = validateSync(it); diff --git a/packages/query-graphql/src/common/get-dto-names.ts b/packages/query-graphql/src/common/dto.utils.ts similarity index 68% rename from packages/query-graphql/src/common/get-dto-names.ts rename to packages/query-graphql/src/common/dto.utils.ts index e01951454..3141753cf 100644 --- a/packages/query-graphql/src/common/get-dto-names.ts +++ b/packages/query-graphql/src/common/dto.utils.ts @@ -2,7 +2,9 @@ import { Class } from '@nestjs-query/core'; import { plural } from 'pluralize'; import { upperCaseFirst } from 'upper-case-first'; import { lowerCaseFirst } from 'lower-case-first'; +import { ID, ReturnTypeFuncValue } from '@nestjs/graphql'; import { findGraphqlObjectMetadata } from './external.utils'; +import { getIDField } from '../decorators'; export interface DTONamesOpts { dtoName?: string; @@ -29,3 +31,14 @@ export const getDTONames = (DTOClass: Class, opts?: DTONamesOpts): DTO pluralBaseNameLower, }; }; + +export const getDTOIdTypeOrDefault = ( + DTOS: Class[], + defaultType: ReturnTypeFuncValue = ID, +): ReturnTypeFuncValue => { + const dtoWithIDField = DTOS.find((dto) => !!getIDField(dto)); + if (dtoWithIDField) { + return getIDField(dtoWithIDField)?.returnTypeFunc() ?? defaultType; + } + return defaultType; +}; diff --git a/packages/query-graphql/src/common/index.ts b/packages/query-graphql/src/common/index.ts index 1a86d251b..59fcea0a6 100644 --- a/packages/query-graphql/src/common/index.ts +++ b/packages/query-graphql/src/common/index.ts @@ -1,4 +1,4 @@ -export { DTONamesOpts, getDTONames, DTONames } from './get-dto-names'; +export { DTONamesOpts, getDTONames, DTONames, getDTOIdTypeOrDefault } from './dto.utils'; export * from './external.utils'; export * from './resolver.utils'; export * from './object.utils'; diff --git a/packages/query-graphql/src/decorators/constants.ts b/packages/query-graphql/src/decorators/constants.ts index 97066756e..ac09240a6 100644 --- a/packages/query-graphql/src/decorators/constants.ts +++ b/packages/query-graphql/src/decorators/constants.ts @@ -1,4 +1,5 @@ export const FILTERABLE_FIELD_KEY = 'nestjs-query:filterable-field'; +export const ID_FIELD_KEY = 'nestjs-query:id-field'; export const RELATION_KEY = 'nestjs-query:relation'; export const REFERENCE_KEY = 'nestjs-query:reference'; diff --git a/packages/query-graphql/src/decorators/id-field.decorator.ts b/packages/query-graphql/src/decorators/id-field.decorator.ts new file mode 100644 index 000000000..610e4c2ea --- /dev/null +++ b/packages/query-graphql/src/decorators/id-field.decorator.ts @@ -0,0 +1,57 @@ +import { Class, MetaValue, ValueReflector } from '@nestjs-query/core'; +import { Field, FieldOptions, ReturnTypeFunc } from '@nestjs/graphql'; +import { ID_FIELD_KEY } from './constants'; +import { FilterableField, FilterableFieldOptions } from './filterable-field.decorator'; + +const reflector = new ValueReflector(ID_FIELD_KEY); +type NoFilterIDFieldOptions = { + disableFilter: false; +} & FieldOptions; +export type IDFieldOptions = FilterableFieldOptions | NoFilterIDFieldOptions; + +export interface IDFieldDescriptor { + propertyName: string; + returnTypeFunc: ReturnTypeFunc; +} + +/** + * Decorator for Fields that should be filterable through a [[FilterType]] + * + * @example + * + * In the following DTO `id`, `title` and `completed` are filterable. + * + * ```ts + * import { IDField } from '@nestjs-query/query-graphql'; + * import { ObjectType, ID } from '@nestjs/graphql'; + * + * @ObjectType('TodoItem') + * export class TodoItemDTO { + * @IDField(() => ID) + * id!: string; + * } + * ``` + */ +export function IDField(returnTypeFunc: ReturnTypeFunc, options?: IDFieldOptions): PropertyDecorator & MethodDecorator { + return ( + // eslint-disable-next-line @typescript-eslint/ban-types + target: Object, + propertyName: string | symbol, + descriptor?: TypedPropertyDescriptor, + ): TypedPropertyDescriptor | void => { + reflector.set(target.constructor as Class, { + propertyName: propertyName.toString(), + returnTypeFunc, + }); + const disableFilter = options && 'disableFilter' in options; + const FieldDecorator = disableFilter ? Field : FilterableField; + if (descriptor) { + return FieldDecorator(returnTypeFunc, options)(target, propertyName, descriptor); + } + return FieldDecorator(returnTypeFunc, options)(target, propertyName); + }; +} + +export function getIDField(DTOClass: Class): MetaValue { + return reflector.get(DTOClass, true); +} diff --git a/packages/query-graphql/src/decorators/index.ts b/packages/query-graphql/src/decorators/index.ts index d7b333c3a..455d737b6 100644 --- a/packages/query-graphql/src/decorators/index.ts +++ b/packages/query-graphql/src/decorators/index.ts @@ -34,3 +34,4 @@ export * from './inject-authorizer.decorator'; export * from './key-set.decorator'; export * from './authorize-filter.decorator'; export * from './query-options.decorator'; +export * from './id-field.decorator'; diff --git a/packages/query-graphql/src/index.ts b/packages/query-graphql/src/index.ts index 2681974a2..0e27a0a2a 100644 --- a/packages/query-graphql/src/index.ts +++ b/packages/query-graphql/src/index.ts @@ -33,6 +33,8 @@ export { MutationHookArgs, KeySet, QueryOptions, + IDField, + IDFieldOptions, } from './decorators'; export * from './resolvers'; export * from './federation'; diff --git a/packages/query-graphql/src/resolvers/crud.resolver.ts b/packages/query-graphql/src/resolvers/crud.resolver.ts index 499d97083..23f53c8fc 100644 --- a/packages/query-graphql/src/resolvers/crud.resolver.ts +++ b/packages/query-graphql/src/resolvers/crud.resolver.ts @@ -51,6 +51,51 @@ export interface CRUDResolver< DeleteResolver, AggregateResolver {} +function extractRelatableOpts( + opts: CRUDResolverOpts, PagingStrategies>, +): RelatableOpts { + const { enableTotalCount, enableAggregate } = opts; + return mergeBaseResolverOpts({ enableAggregate, enableTotalCount }, opts); +} + +function extractAggregateResolverOpts( + opts: CRUDResolverOpts, PagingStrategies>, +): AggregateResolverOpts { + const { enableAggregate, aggregate } = opts; + return mergeBaseResolverOpts({ enabled: enableAggregate, ...aggregate }, opts); +} + +function extractCreateResolverOpts( + opts: CRUDResolverOpts, PagingStrategies>, +): CreateResolverOpts { + const { CreateDTOClass, enableSubscriptions, create } = opts; + return mergeBaseResolverOpts>({ CreateDTOClass, enableSubscriptions, ...create }, opts); +} + +function extractReadResolverOpts, PS extends PagingStrategies>( + opts: CRUDResolverOpts, +): MergePagingStrategyOpts { + const { enableTotalCount, pagingStrategy, read } = opts; + return mergeBaseResolverOpts( + { enableTotalCount, pagingStrategy, ...read } as MergePagingStrategyOpts, + opts, + ); +} + +function extractUpdateResolverOpts( + opts: CRUDResolverOpts, PagingStrategies>, +): UpdateResolverOpts { + const { UpdateDTOClass, enableSubscriptions, update } = opts; + return mergeBaseResolverOpts>({ UpdateDTOClass, enableSubscriptions, ...update }, opts); +} + +function extractDeleteResolverOpts( + opts: CRUDResolverOpts, PagingStrategies>, +): DeleteResolverOpts { + const { enableSubscriptions, delete: deleteArgs } = opts; + return mergeBaseResolverOpts>({ enableSubscriptions, ...deleteArgs }, opts); +} + /** * Factory to create a resolver that includes all CRUD methods from [[CreateResolver]], [[ReadResolver]], * [[UpdateResolver]], and [[DeleteResolver]]. @@ -82,46 +127,13 @@ export const CRUDResolver = < DTOClass: Class, opts: CRUDResolverOpts = {}, ): ResolverClass, CRUDResolver>> => { - const { - CreateDTOClass, - UpdateDTOClass, - enableSubscriptions, - pagingStrategy, - enableTotalCount, - enableAggregate, - create = {}, - read = {}, - update = {}, - delete: deleteArgs = {}, - referenceBy = {}, - aggregate, - } = opts; - - const referencable = Referenceable(DTOClass, referenceBy); - const relatable = Relatable( - DTOClass, - mergeBaseResolverOpts({ enableTotalCount, enableAggregate } as RelatableOpts, opts), - ); - const aggregateable = Aggregateable(DTOClass, { - enabled: enableAggregate, - ...mergeBaseResolverOpts(aggregate ?? {}, opts), - }); - const creatable = Creatable(DTOClass, { - CreateDTOClass, - enableSubscriptions, - ...mergeBaseResolverOpts(create ?? {}, opts), - }); - const readable = Readable(DTOClass, { - enableTotalCount, - pagingStrategy, - ...mergeBaseResolverOpts(read, opts), - } as MergePagingStrategyOpts); - const updateable = Updateable(DTOClass, { - UpdateDTOClass, - enableSubscriptions, - ...mergeBaseResolverOpts(update, opts), - }); - const deleteResolver = DeleteResolver(DTOClass, { enableSubscriptions, ...mergeBaseResolverOpts(deleteArgs, opts) }); + const referencable = Referenceable(DTOClass, opts.referenceBy ?? {}); + const relatable = Relatable(DTOClass, extractRelatableOpts(opts)); + const aggregateable = Aggregateable(DTOClass, extractAggregateResolverOpts(opts)); + const creatable = Creatable(DTOClass, extractCreateResolverOpts(opts)); + const readable = Readable(DTOClass, extractReadResolverOpts(opts)); + const updatable = Updateable(DTOClass, extractUpdateResolverOpts(opts)); + const deleteResolver = DeleteResolver(DTOClass, extractDeleteResolverOpts(opts)); - return referencable(relatable(aggregateable(creatable(readable(updateable(deleteResolver)))))); + return referencable(relatable(aggregateable(creatable(readable(updatable(deleteResolver)))))); }; diff --git a/packages/query-graphql/src/resolvers/delete.resolver.ts b/packages/query-graphql/src/resolvers/delete.resolver.ts index 1a3f7de9c..1f82b31dd 100644 --- a/packages/query-graphql/src/resolvers/delete.resolver.ts +++ b/packages/query-graphql/src/resolvers/delete.resolver.ts @@ -52,6 +52,14 @@ const defaultDeleteManyInput = (dtoNames: DTONames, DTOClass: Class): return DM; }; +/** @internal */ +const defaultDeleteOneInput = (dtoNames: DTONames, DTOClass: Class): Class => { + const { baseName } = dtoNames; + @InputType(`DeleteOne${baseName}Input`) + class DM extends DeleteOneInputType(DTOClass) {} + return DM; +}; + /** * @internal * Mixin to add `delete` graphql endpoints. @@ -67,7 +75,10 @@ export const Deletable = >( const enableManySubscriptions = opts.many?.enableSubscriptions ?? enableSubscriptions; const deletedOneEvent = getDTOEventName(EventType.DELETED_ONE, DTOClass); const deletedManyEvent = getDTOEventName(EventType.DELETED_MANY, DTOClass); - const { DeleteOneInput = DeleteOneInputType(), DeleteManyInput = defaultDeleteManyInput(dtoNames, DTOClass) } = opts; + const { + DeleteOneInput = defaultDeleteOneInput(dtoNames, DTOClass), + DeleteManyInput = defaultDeleteManyInput(dtoNames, DTOClass), + } = opts; const deleteOneMutationName = opts.one?.name ?? `deleteOne${baseName}`; const deleteManyMutationName = opts.many?.name ?? `deleteMany${pluralBaseName}`; const DMR = DeleteManyResponseType(); diff --git a/packages/query-graphql/src/resolvers/read.resolver.ts b/packages/query-graphql/src/resolvers/read.resolver.ts index ea6fac56a..e03d53a01 100644 --- a/packages/query-graphql/src/resolvers/read.resolver.ts +++ b/packages/query-graphql/src/resolvers/read.resolver.ts @@ -63,7 +63,7 @@ export const Readable = , QS extends class QA extends QueryArgs {} @ArgsType() - class FO extends FindOneArgsType() {} + class FO extends FindOneArgsType(DTOClass) {} @Resolver(() => DTOClass, { isAbstract: true }) class ReadResolverBase extends BaseClass { diff --git a/packages/query-graphql/src/resolvers/relations/remove-relations.resolver.ts b/packages/query-graphql/src/resolvers/relations/remove-relations.resolver.ts index 4559cd948..e0ee67bc9 100644 --- a/packages/query-graphql/src/resolvers/relations/remove-relations.resolver.ts +++ b/packages/query-graphql/src/resolvers/relations/remove-relations.resolver.ts @@ -1,5 +1,6 @@ +// eslint-disable-next-line max-classes-per-file import { Class, ModifyRelationOptions, QueryService } from '@nestjs-query/core'; -import { Resolver, ArgsType, Args } from '@nestjs/graphql'; +import { Resolver, ArgsType, Args, InputType } from '@nestjs/graphql'; import { OperationGroup } from '../../auth'; import { getDTONames } from '../../common'; import { ModifyRelationAuthorizerFilter, ResolverMutation } from '../../decorators'; @@ -23,8 +24,10 @@ const RemoveOneRelationMixin = (DTOClass: Class, relation: R const dtoNames = getDTONames(DTOClass); const { baseNameLower, baseName } = getDTONames(relationDTO, { dtoName: relation.dtoName }); const relationName = relation.relationName ?? baseNameLower; + @InputType(`Remove${baseName}From${dtoNames.baseName}Input`) + class RIT extends RelationInputType(DTOClass, relationDTO) {} @ArgsType() - class SetArgs extends MutationArgsType(RelationInputType()) {} + class SetArgs extends MutationArgsType(RIT) {} @Resolver(() => DTOClass, { isAbstract: true }) class RemoveOneMixin extends Base { @@ -57,8 +60,10 @@ const RemoveManyRelationsMixin = (DTOClass: Class, relation: const dtoNames = getDTONames(DTOClass); const { pluralBaseNameLower, pluralBaseName } = getDTONames(relationDTO, { dtoName: relation.dtoName }); const relationName = relation.relationName ?? pluralBaseNameLower; + @InputType(`Remove${pluralBaseName}From${dtoNames.baseName}Input`) + class RIT extends RelationsInputType(DTOClass, relationDTO) {} @ArgsType() - class AddArgs extends MutationArgsType(RelationsInputType()) {} + class AddArgs extends MutationArgsType(RIT) {} @Resolver(() => DTOClass, { isAbstract: true }) class Mixin extends Base { diff --git a/packages/query-graphql/src/resolvers/relations/update-relations.resolver.ts b/packages/query-graphql/src/resolvers/relations/update-relations.resolver.ts index 8ab77ac3c..0fd26115a 100644 --- a/packages/query-graphql/src/resolvers/relations/update-relations.resolver.ts +++ b/packages/query-graphql/src/resolvers/relations/update-relations.resolver.ts @@ -1,5 +1,6 @@ +// eslint-disable-next-line max-classes-per-file import { Class, ModifyRelationOptions, QueryService } from '@nestjs-query/core'; -import { Resolver, ArgsType, Args } from '@nestjs/graphql'; +import { Resolver, ArgsType, Args, InputType } from '@nestjs/graphql'; import { AuthorizerInterceptor } from '../../interceptors'; import { getDTONames } from '../../common'; import { ModifyRelationAuthorizerFilter, ResolverMutation } from '../../decorators'; @@ -23,8 +24,10 @@ const UpdateOneRelationMixin = (DTOClass: Class, relation: R const dtoNames = getDTONames(DTOClass); const { baseNameLower, baseName } = getDTONames(relationDTO, { dtoName: relation.dtoName }); const relationName = relation.relationName ?? baseNameLower; + @InputType(`Set${baseName}On${dtoNames.baseName}Input`) + class RIT extends RelationInputType(DTOClass, relationDTO) {} @ArgsType() - class SetArgs extends MutationArgsType(RelationInputType()) {} + class SetArgs extends MutationArgsType(RIT) {} @Resolver(() => DTOClass, { isAbstract: true }) class UpdateOneMixin extends Base { @@ -59,8 +62,15 @@ const UpdateManyRelationMixin = (DTOClass: Class, relation: const dtoNames = getDTONames(DTOClass); const { pluralBaseNameLower, pluralBaseName } = getDTONames(relationDTO, { dtoName: relation.dtoName }); const relationName = relation.relationName ?? pluralBaseNameLower; + @InputType(`Add${pluralBaseName}To${dtoNames.baseName}Input`) + class AddRelationInput extends RelationsInputType(DTOClass, relationDTO) {} @ArgsType() - class AddArgs extends MutationArgsType(RelationsInputType()) {} + class AddArgs extends MutationArgsType(AddRelationInput) {} + + @InputType(`Set${pluralBaseName}On${dtoNames.baseName}Input`) + class SetRelationInput extends RelationsInputType(DTOClass, relationDTO) {} + @ArgsType() + class SetArgs extends MutationArgsType(SetRelationInput) {} @Resolver(() => DTOClass, { isAbstract: true }) class UpdateManyMixin extends Base { @@ -83,7 +93,7 @@ const UpdateManyRelationMixin = (DTOClass: Class, relation: interceptors: [AuthorizerInterceptor(DTOClass)], }) async [`set${pluralBaseName}On${dtoNames.baseName}`]( - @Args() addArgs: AddArgs, + @Args() addArgs: SetArgs, @ModifyRelationAuthorizerFilter(pluralBaseNameLower, { operationGroup: OperationGroup.UPDATE, many: true, diff --git a/packages/query-graphql/src/resolvers/update.resolver.ts b/packages/query-graphql/src/resolvers/update.resolver.ts index 019f834d3..aeccc4bec 100644 --- a/packages/query-graphql/src/resolvers/update.resolver.ts +++ b/packages/query-graphql/src/resolvers/update.resolver.ts @@ -58,11 +58,15 @@ const defaultUpdateInput = (dtoNames: DTONames, DTOClass: Class): C }; /** @internal */ -const defaultUpdateOneInput = (dtoNames: DTONames, UpdateDTO: Class): Class> => { +const defaultUpdateOneInput = ( + dtoNames: DTONames, + DTOClass: Class, + UpdateDTO: Class, +): Class> => { const { baseName } = dtoNames; @InputType(`UpdateOne${baseName}Input`) - class UM extends UpdateOneInputType(UpdateDTO) {} + class UM extends UpdateOneInputType(DTOClass, UpdateDTO) {} return UM; }; @@ -99,7 +103,7 @@ export const Updateable = >( const updateManyEvent = getDTOEventName(EventType.UPDATED_MANY, DTOClass); const { UpdateDTOClass = defaultUpdateInput(dtoNames, DTOClass), - UpdateOneInput = defaultUpdateOneInput(dtoNames, UpdateDTOClass), + UpdateOneInput = defaultUpdateOneInput(dtoNames, DTOClass, UpdateDTOClass), UpdateManyInput = defaultUpdateManyInput(dtoNames, DTOClass, UpdateDTOClass), } = opts; const updateOneMutationName = opts.one?.name ?? `updateOne${baseName}`; diff --git a/packages/query-graphql/src/types/delete-one-input.type.ts b/packages/query-graphql/src/types/delete-one-input.type.ts index 1e23d1519..318af915f 100644 --- a/packages/query-graphql/src/types/delete-one-input.type.ts +++ b/packages/query-graphql/src/types/delete-one-input.type.ts @@ -1,28 +1,23 @@ import { Class } from '@nestjs-query/core'; -import { Field, ID, InputType } from '@nestjs/graphql'; +import { Field, InputType } from '@nestjs/graphql'; import { IsNotEmpty } from 'class-validator'; +import { getDTOIdTypeOrDefault } from '../common'; export interface DeleteOneInputType { id: string | number; } -/** @internal */ -let deleteOneInputType: Class | null = null; - /** * The input type for delete one endpoints. */ // eslint-disable-next-line @typescript-eslint/no-redeclare -- intentional -export function DeleteOneInputType(): Class { - if (deleteOneInputType) { - return deleteOneInputType; - } - @InputType() +export function DeleteOneInputType(DTOClass: Class): Class { + const IDType = getDTOIdTypeOrDefault([DTOClass]); + @InputType({ isAbstract: true }) class DeleteOneInput implements DeleteOneInputType { @IsNotEmpty() - @Field(() => ID, { description: 'The id of the record to delete.' }) + @Field(() => IDType, { description: 'The id of the record to delete.' }) id!: string | number; } - deleteOneInputType = DeleteOneInput; - return deleteOneInputType; + return DeleteOneInput; } diff --git a/packages/query-graphql/src/types/find-one-args.type.ts b/packages/query-graphql/src/types/find-one-args.type.ts index ff6681d51..f7f91f73f 100644 --- a/packages/query-graphql/src/types/find-one-args.type.ts +++ b/packages/query-graphql/src/types/find-one-args.type.ts @@ -1,28 +1,22 @@ import { Class } from '@nestjs-query/core'; -import { ArgsType, Field, ID } from '@nestjs/graphql'; +import { ArgsType, Field } from '@nestjs/graphql'; import { IsNotEmpty } from 'class-validator'; +import { getDTOIdTypeOrDefault } from '../common'; export interface FindOneArgsType { id: string | number; } - -/** @internal */ -let findOneType: Class | null = null; - /** * The input type for delete one endpoints. */ // eslint-disable-next-line @typescript-eslint/no-redeclare -- intentional -export function FindOneArgsType(): Class { - if (findOneType) { - return findOneType; - } +export function FindOneArgsType(DTOClass: Class): Class { + const IDType = getDTOIdTypeOrDefault([DTOClass]); @ArgsType() class FindOneArgs implements FindOneArgsType { @IsNotEmpty() - @Field(() => ID, { description: 'The id of the record to find.' }) + @Field(() => IDType, { description: 'The id of the record to find.' }) id!: string | number; } - findOneType = FindOneArgs; - return findOneType; + return FindOneArgs; } diff --git a/packages/query-graphql/src/types/query/field-comparison/field-comparison.factory.ts b/packages/query-graphql/src/types/query/field-comparison/field-comparison.factory.ts index d28ef9745..ebd0533a3 100644 --- a/packages/query-graphql/src/types/query/field-comparison/field-comparison.factory.ts +++ b/packages/query-graphql/src/types/query/field-comparison/field-comparison.factory.ts @@ -1,4 +1,4 @@ -import { Class, FilterFieldComparison, FilterComparisonOperators } from '@nestjs-query/core'; +import { Class, FilterFieldComparison, FilterComparisonOperators, isNamed } from '@nestjs-query/core'; import { IsBoolean, IsOptional } from 'class-validator'; import { upperCaseFirst } from 'upper-case-first'; import { @@ -48,10 +48,6 @@ const knownTypes: Set = new Set([ GraphQLTimestamp, ]); -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const isNamed = (SomeType: any): SomeType is { name: string } => - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - 'name' in SomeType && typeof SomeType.name === 'string'; /** @internal */ const getTypeName = (SomeType: ReturnTypeFuncValue): string => { if (knownTypes.has(SomeType) || isNamed(SomeType)) { diff --git a/packages/query-graphql/src/types/query/helpers.ts b/packages/query-graphql/src/types/query/helpers.ts index 1b658ccc4..44e8562e8 100644 --- a/packages/query-graphql/src/types/query/helpers.ts +++ b/packages/query-graphql/src/types/query/helpers.ts @@ -1 +1 @@ -export const isInAllowedList = (arr: T[] | undefined, val: T) => arr?.includes(val) ?? true; +export const isInAllowedList = (arr: T[] | undefined, val: T): boolean => arr?.includes(val) ?? true; diff --git a/packages/query-graphql/src/types/relation-input.type.ts b/packages/query-graphql/src/types/relation-input.type.ts index ae276705f..7c18b6c9a 100644 --- a/packages/query-graphql/src/types/relation-input.type.ts +++ b/packages/query-graphql/src/types/relation-input.type.ts @@ -1,29 +1,26 @@ import { Class } from '@nestjs-query/core'; -import { Field, ID, InputType } from '@nestjs/graphql'; +import { Field, InputType } from '@nestjs/graphql'; import { IsNotEmpty } from 'class-validator'; +import { getDTOIdTypeOrDefault } from '../common'; export interface RelationInputType { id: string | number; relationId: string | number; } -/** @internal */ -let relationInputType: Class | null = null; // eslint-disable-next-line @typescript-eslint/no-redeclare -- intentional -export function RelationInputType(): Class { - if (relationInputType) { - return relationInputType; - } - @InputType() +export function RelationInputType(DTOClass: Class, RelationClass: Class): Class { + const DTOIDType = getDTOIdTypeOrDefault([DTOClass]); + const RelationIDType = getDTOIdTypeOrDefault([RelationClass]); + @InputType({ isAbstract: true }) class RelationInput implements RelationInputType { - @Field(() => ID, { description: 'The id of the record.' }) + @Field(() => DTOIDType, { description: 'The id of the record.' }) @IsNotEmpty() id!: string | number; - @Field(() => ID, { description: 'The id of relation.' }) + @Field(() => RelationIDType, { description: 'The id of relation.' }) @IsNotEmpty() relationId!: string | number; } - relationInputType = RelationInput; - return relationInputType; + return RelationInput; } diff --git a/packages/query-graphql/src/types/relations-input.type.ts b/packages/query-graphql/src/types/relations-input.type.ts index 8d07c8407..308552ac3 100644 --- a/packages/query-graphql/src/types/relations-input.type.ts +++ b/packages/query-graphql/src/types/relations-input.type.ts @@ -1,30 +1,27 @@ import { Class } from '@nestjs-query/core'; -import { Field, ID, InputType } from '@nestjs/graphql'; +import { Field, InputType } from '@nestjs/graphql'; import { IsNotEmpty, ArrayUnique } from 'class-validator'; +import { getDTOIdTypeOrDefault } from '../common'; export interface RelationsInputType { id: string | number; relationIds: (string | number)[]; } -/** @internal */ -let relationsInputType: Class | null = null; // eslint-disable-next-line @typescript-eslint/no-redeclare -- intentional -export function RelationsInputType(): Class { - if (relationsInputType) { - return relationsInputType; - } - @InputType() +export function RelationsInputType(DTOClass: Class, RelationClass: Class): Class { + const DTOIDType = getDTOIdTypeOrDefault([DTOClass]); + const RelationIDType = getDTOIdTypeOrDefault([RelationClass]); + @InputType({ isAbstract: true }) class RelationsInput implements RelationsInputType { - @Field(() => ID, { description: 'The id of the record.' }) + @Field(() => DTOIDType, { description: 'The id of the record.' }) @IsNotEmpty() id!: string | number; - @Field(() => [ID], { description: 'The ids of the relations.' }) + @Field(() => [RelationIDType], { description: 'The ids of the relations.' }) @ArrayUnique() @IsNotEmpty({ each: true }) relationIds!: (string | number)[]; } - relationsInputType = RelationsInput; - return relationsInputType; + return RelationsInput; } diff --git a/packages/query-graphql/src/types/update-one-input.type.ts b/packages/query-graphql/src/types/update-one-input.type.ts index e9153527d..ea4e17c6a 100644 --- a/packages/query-graphql/src/types/update-one-input.type.ts +++ b/packages/query-graphql/src/types/update-one-input.type.ts @@ -1,7 +1,8 @@ import { Class } from '@nestjs-query/core'; -import { Field, ID, InputType } from '@nestjs/graphql'; +import { Field, InputType } from '@nestjs/graphql'; import { IsNotEmpty, ValidateNested } from 'class-validator'; import { Type } from 'class-transformer'; +import { getDTOIdTypeOrDefault } from '../common'; export interface UpdateOneInputType { id: string | number; @@ -10,14 +11,16 @@ export interface UpdateOneInputType { /** * The abstract input type for update one endpoints. + * @param DTOClass - The base DTO class the UpdateType is based on. * @param UpdateType - The InputType to use for the update field. */ // eslint-disable-next-line @typescript-eslint/no-redeclare -- intentional -export function UpdateOneInputType(UpdateType: Class): Class> { +export function UpdateOneInputType(DTOClass: Class, UpdateType: Class): Class> { + const IDType = getDTOIdTypeOrDefault([DTOClass, UpdateType]); @InputType({ isAbstract: true }) class UpdateOneInput implements UpdateOneInputType { @IsNotEmpty() - @Field(() => ID, { description: 'The id of the record to update' }) + @Field(() => IDType, { description: 'The id of the record to update' }) id!: string | number; @Type(() => UpdateType)