Переглянути джерело

feat(core): Add support for custom GraphQL scalars

Closes #1593
Michael Bromley 3 роки тому
батько
коміт
099a36cc65

+ 40 - 0
docs/content/plugins/plugin-examples/extending-graphql-api.md

@@ -224,3 +224,43 @@ This resolver is then passed in to your plugin metadata like any other resolver:
 })
 export class MyPlugin {}
 ```
+
+## Defining custom scalars
+
+By default, Vendure bundles `DateTime` and a `JSON` custom scalars (from the [graphql-scalars library](https://github.com/Urigo/graphql-scalars)). From v1.7.0, you can also define your own custom scalars for use in your schema extensions:
+
+```TypeScript
+import { GraphQLScalarType} from 'graphql';
+import { GraphQLEmailAddress } from 'graphql-scalars';
+
+// Scalars can be custom-built as like this one,
+// or imported from a pre-made scalar library like
+// the GraphQLEmailAddress example.
+const FooScalar = new GraphQLScalarType({
+  name: 'Foo',
+  description: 'A test scalar',
+  serialize(value) {
+    // ...
+  },
+  parseValue(value) {
+    // ...
+  },
+});
+
+@VendurePlugin({
+  imports: [PluginCommonModule],
+  shopApiExtensions: {
+    schema: gql`
+      scalar Foo
+      scalar EmailAddress
+    `,
+    scalars: { 
+      // The key must match the scalar name
+      // given in the schema  
+      Foo: FooScalar,
+      EmailAddress: GraphQLEmailAddress,
+    },
+  },
+})
+export class CustomScalarsPlugin {}
+```

+ 16 - 1
packages/core/e2e/fixtures/test-plugins/with-api-extensions.ts

@@ -1,5 +1,6 @@
 import { Query, Resolver } from '@nestjs/graphql';
 import { VendurePlugin } from '@vendure/core';
+import { GraphQLScalarType } from 'graphql';
 import gql from 'graphql-tag';
 
 @Resolver()
@@ -12,7 +13,7 @@ export class TestAdminPluginResolver {
     @Query()
     barList() {
         return {
-            items: [{ id: 1, name: 'Test' }],
+            items: [{ id: 1, name: 'Test', pizzaType: 'Cheese' }],
             totalItems: 1,
         };
     }
@@ -26,6 +27,17 @@ export class TestShopPluginResolver {
     }
 }
 
+const PizzaScalar = new GraphQLScalarType({
+    name: 'Pizza',
+    description: 'Everything is pizza',
+    serialize(value) {
+        return value.toString() + ' pizza!';
+    },
+    parseValue(value) {
+        return value;
+    },
+});
+
 @VendurePlugin({
     shopApiExtensions: {
         resolvers: [TestShopPluginResolver],
@@ -38,6 +50,7 @@ export class TestShopPluginResolver {
     adminApiExtensions: {
         resolvers: [TestAdminPluginResolver],
         schema: gql`
+            scalar Pizza
             extend type Query {
                 foo: [String]!
                 barList(options: BarListOptions): BarList!
@@ -47,12 +60,14 @@ export class TestShopPluginResolver {
             type Bar implements Node {
                 id: ID!
                 name: String!
+                pizzaType: Pizza!
             }
             type BarList implements PaginatedList {
                 items: [Bar!]!
                 totalItems: Int!
             }
         `,
+        scalars: { Pizza: PizzaScalar },
     },
 })
 export class TestAPIExtensionPlugin {}

+ 16 - 0
packages/core/e2e/plugin.e2e-spec.ts

@@ -68,6 +68,22 @@ describe('Plugins', () => {
         expect(result.baz).toEqual(['quux']);
     });
 
+    it('custom scalar', async () => {
+        const result = await adminClient.query(gql`
+            query {
+                barList(options: { skip: 0, take: 1 }) {
+                    items {
+                        id
+                        pizzaType
+                    }
+                }
+            }
+        `);
+        expect(result.barList).toEqual({
+            items: [{ id: 'T_1', pizzaType: 'Cheese pizza!' }],
+        });
+    });
+
     it('allows lazy evaluation of API extension', async () => {
         const result = await shopClient.query(gql`
             query {

+ 15 - 2
packages/core/src/api/config/generate-resolvers.ts

@@ -13,6 +13,7 @@ import { shopErrorOperationTypeResolvers } from '../../common/error/generated-gr
 import { Translatable } from '../../common/types/locale-types';
 import { ConfigService } from '../../config/config.service';
 import { CustomFieldConfig, RelationCustomFieldConfig } from '../../config/custom-field/custom-field-types';
+import { getPluginAPIExtensions } from '../../plugin/plugin-metadata';
 import { CustomFieldRelationResolverService } from '../common/custom-field-relation-resolver.service';
 import { ApiType } from '../common/get-api-type';
 import { RequestContext } from '../common/request-context';
@@ -118,8 +119,8 @@ export function generateResolvers(
 
     const resolvers =
         apiType === 'admin'
-            ? { ...commonResolvers, ...adminResolvers }
-            : { ...commonResolvers, ...shopResolvers };
+            ? { ...commonResolvers, ...adminResolvers, ...getCustomScalars(configService, 'admin') }
+            : { ...commonResolvers, ...shopResolvers, ...getCustomScalars(configService, 'shop') };
     return resolvers;
 }
 
@@ -209,6 +210,18 @@ function generateCustomFieldRelationResolvers(
     return { adminResolvers, shopResolvers };
 }
 
+function getCustomScalars(configService: ConfigService, apiType: 'admin' | 'shop') {
+    return getPluginAPIExtensions(configService.plugins, apiType)
+        .map(e => (typeof e.scalars === 'function' ? e.scalars() : e.scalars ?? {}))
+        .reduce(
+            (all, scalarMap) => ({
+                ...all,
+                ...scalarMap,
+            }),
+            {},
+        );
+}
+
 function isRelationalType(input: CustomFieldConfig): input is RelationCustomFieldConfig {
     return input.type === 'relation';
 }

+ 1 - 1
packages/core/src/plugin/plugin-metadata.ts

@@ -67,7 +67,7 @@ export function graphQLResolversFor(
     return apiExtensions
         ? typeof apiExtensions.resolvers === 'function'
             ? apiExtensions.resolvers()
-            : apiExtensions.resolvers
+            : apiExtensions.resolvers ?? []
         : [];
 }
 

+ 11 - 2
packages/core/src/plugin/vendure-plugin.ts

@@ -4,7 +4,7 @@ import { ModuleMetadata } from '@nestjs/common/interfaces';
 import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
 import { pick } from '@vendure/common/lib/pick';
 import { Type } from '@vendure/common/lib/shared-types';
-import { DocumentNode } from 'graphql';
+import { DocumentNode, GraphQLScalarType } from 'graphql';
 
 import { RuntimeVendureConfig } from '../config/vendure-config';
 
@@ -71,7 +71,16 @@ export interface APIExtensionDefinition {
      * An array of resolvers for the schema extensions. Should be defined as [Nestjs GraphQL resolver](https://docs.nestjs.com/graphql/resolvers-map)
      * classes, i.e. using the Nest `\@Resolver()` decorator etc.
      */
-    resolvers: Array<Type<any>> | (() => Array<Type<any>>);
+    resolvers?: Array<Type<any>> | (() => Array<Type<any>>);
+    /**
+     * @description
+     * A map of GraphQL scalar types which should correspond to any custom scalars defined in your schema.
+     * Read more about defining custom scalars in the
+     * [Apollo Server Custom Scalars docs](https://www.apollographql.com/docs/apollo-server/schema/custom-scalars)
+     *
+     * @since 1.7.0
+     */
+    scalars?: Record<string, GraphQLScalarType> | (() => Record<string, GraphQLScalarType>);
 }
 
 /**