Преглед на файлове

feat(core): Enable GraphQL subscriptions for Apollo driver

David Höck преди 1 година
родител
ревизия
f7d2969869

+ 14 - 0
package-lock.json

@@ -17421,6 +17421,18 @@
         "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0"
       }
     },
+    "node_modules/graphql-subscriptions": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/graphql-subscriptions/-/graphql-subscriptions-2.0.0.tgz",
+      "integrity": "sha512-s6k2b8mmt9gF9pEfkxsaO1lTxaySfKoEJzEfmwguBbQ//Oq23hIXCfR1hm4kdh5hnR20RdwB+s3BCb+0duHSZA==",
+      "license": "MIT",
+      "dependencies": {
+        "iterall": "^1.3.0"
+      },
+      "peerDependencies": {
+        "graphql": "^15.7.2 || ^16.0.0"
+      }
+    },
     "node_modules/graphql-tag": {
       "version": "2.12.6",
       "license": "MIT",
@@ -32170,8 +32182,10 @@
         "graphql": "~16.9.0",
         "graphql-fields": "^2.0.3",
         "graphql-scalars": "^1.22.5",
+        "graphql-subscriptions": "^2.0.0",
         "graphql-tag": "^2.12.6",
         "graphql-upload": "^16.0.2",
+        "graphql-ws": "^5.16.0",
         "http-proxy-middleware": "^2.0.6",
         "i18next": "^23.12.1",
         "i18next-fs-backend": "^2.3.1",

+ 107 - 105
packages/core/package.json

@@ -1,107 +1,109 @@
 {
-    "name": "@vendure/core",
-    "version": "3.1.0-next.3",
-    "description": "A modern, headless ecommerce framework",
-    "repository": {
-        "type": "git",
-        "url": "https://github.com/vendure-ecommerce/vendure/"
-    },
-    "keywords": [
-        "vendure",
-        "ecommerce",
-        "headless",
-        "graphql",
-        "typescript"
-    ],
-    "homepage": "https://www.vendure.io/",
-    "funding": "https://github.com/sponsors/michaelbromley",
-    "private": false,
-    "license": "GPL-3.0-or-later",
-    "type": "commonjs",
-    "scripts": {
-        "tsc:watch": "tsc -p ./build/tsconfig.build.json --watch",
-        "copy:watch": "ts-node build/copy-static.ts watch",
-        "build": "rimraf dist && tsc -p ./build/tsconfig.build.json && tsc -p ./build/tsconfig.cli.json && ts-node build/copy-static.ts build",
-        "watch": "concurrently npm:tsc:watch npm:copy:watch",
-        "lint": "eslint --fix .",
-        "test": "vitest --config vitest.config.mts --run",
-        "e2e": "cross-env PACKAGE=core vitest --config ../../e2e-common/vitest.config.mts --run",
-        "e2e:watch": "cross-env PACKAGE=core vitest --config ../../e2e-common/vitest.config.mts",
-        "bench": "cross-env PACKAGE=core vitest --config ../../e2e-common/vitest.config.bench.ts --run",
-        "ci": "npm run build"
-    },
-    "publishConfig": {
-        "access": "public"
-    },
-    "main": "dist/index.js",
-    "types": "dist/index.d.ts",
-    "files": [
-        "dist/**/*",
-        "cli/**/*"
-    ],
-    "dependencies": {
-        "@apollo/server": "^4.10.4",
-        "@graphql-tools/stitch": "^9.2.10",
-        "@nestjs/apollo": "~12.2.0",
-        "@nestjs/common": "~10.3.10",
-        "@nestjs/core": "~10.3.10",
-        "@nestjs/graphql": "~12.2.0",
-        "@nestjs/platform-express": "~10.3.10",
-        "@nestjs/terminus": "~10.2.3",
-        "@nestjs/testing": "~10.3.10",
-        "@nestjs/typeorm": "~10.0.2",
-        "@types/fs-extra": "^9.0.1",
-        "@vendure/common": "3.1.0-next.3",
-        "bcrypt": "^5.1.1",
-        "body-parser": "^1.20.2",
-        "cookie-session": "^2.1.0",
-        "csv-parse": "^5.5.5",
-        "express": "^4.18.3",
-        "fs-extra": "^11.2.0",
-        "graphql": "~16.9.0",
-        "graphql-fields": "^2.0.3",
-        "graphql-scalars": "^1.22.5",
-        "graphql-tag": "^2.12.6",
-        "graphql-upload": "^16.0.2",
-        "http-proxy-middleware": "^2.0.6",
-        "i18next": "^23.12.1",
-        "i18next-fs-backend": "^2.3.1",
-        "i18next-http-middleware": "^3.5.0",
-        "i18next-icu": "^2.3.0",
-        "image-size": "^1.1.1",
-        "intl-messageformat": "^10.5.11",
-        "mime-types": "^2.1.35",
-        "ms": "^2.1.3",
-        "nanoid": "^3.3.7",
-        "picocolors": "^1.0.0",
-        "progress": "^2.0.3",
-        "reflect-metadata": "^0.2.2",
-        "rxjs": "^7.8.1",
-        "semver": "^7.6.0",
-        "typeorm": "0.3.20"
-    },
-    "devDependencies": {
-        "@types/bcrypt": "^5.0.2",
-        "@types/cookie-session": "^2.0.48",
-        "@types/csv-parse": "^1.2.2",
-        "@types/express": "^4.17.21",
-        "@types/graphql-upload": "^16.0.7",
-        "@types/gulp": "^4.0.17",
-        "@types/mime-types": "^2.1.4",
-        "@types/ms": "^0.7.34",
-        "@types/node": "^18.19.23",
-        "@types/progress": "^2.0.7",
-        "@types/prompts": "^2.4.9",
-        "@types/semver": "^7.5.8",
-        "better-sqlite3": "^11.4.0",
-        "chokidar": "^3.6.0",
-        "fs-extra": "^11.2.0",
-        "glob": "^10.3.10",
-        "mysql": "^2.18.1",
-        "pg": "^8.11.3",
-        "rimraf": "^5.0.5",
-        "sql.js": "1.10.2",
-        "sqlite3": "^5.1.7",
-        "typescript": "5.3.3"
-    }
+  "name": "@vendure/core",
+  "version": "3.1.0-next.3",
+  "description": "A modern, headless ecommerce framework",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/vendure-ecommerce/vendure/"
+  },
+  "keywords": [
+    "vendure",
+    "ecommerce",
+    "headless",
+    "graphql",
+    "typescript"
+  ],
+  "homepage": "https://www.vendure.io/",
+  "funding": "https://github.com/sponsors/michaelbromley",
+  "private": false,
+  "license": "GPL-3.0-or-later",
+  "type": "commonjs",
+  "scripts": {
+    "tsc:watch": "tsc -p ./build/tsconfig.build.json --watch",
+    "copy:watch": "ts-node build/copy-static.ts watch",
+    "build": "rimraf dist && tsc -p ./build/tsconfig.build.json && tsc -p ./build/tsconfig.cli.json && ts-node build/copy-static.ts build",
+    "watch": "concurrently npm:tsc:watch npm:copy:watch",
+    "lint": "eslint --fix .",
+    "test": "vitest --config vitest.config.mts --run",
+    "e2e": "cross-env PACKAGE=core vitest --config ../../e2e-common/vitest.config.mts --run",
+    "e2e:watch": "cross-env PACKAGE=core vitest --config ../../e2e-common/vitest.config.mts",
+    "bench": "cross-env PACKAGE=core vitest --config ../../e2e-common/vitest.config.bench.ts --run",
+    "ci": "npm run build"
+  },
+  "publishConfig": {
+    "access": "public"
+  },
+  "main": "dist/index.js",
+  "types": "dist/index.d.ts",
+  "files": [
+    "dist/**/*",
+    "cli/**/*"
+  ],
+  "dependencies": {
+    "@apollo/server": "^4.10.4",
+    "@graphql-tools/stitch": "^9.2.10",
+    "@nestjs/apollo": "~12.2.0",
+    "@nestjs/common": "~10.3.10",
+    "@nestjs/core": "~10.3.10",
+    "@nestjs/graphql": "~12.2.0",
+    "@nestjs/platform-express": "~10.3.10",
+    "@nestjs/terminus": "~10.2.3",
+    "@nestjs/testing": "~10.3.10",
+    "@nestjs/typeorm": "~10.0.2",
+    "@types/fs-extra": "^9.0.1",
+    "@vendure/common": "3.1.0-next.3",
+    "bcrypt": "^5.1.1",
+    "body-parser": "^1.20.2",
+    "graphql-ws": "^5.16.0",
+    "graphql-subscriptions": "^2.0.0",
+    "cookie-session": "^2.1.0",
+    "csv-parse": "^5.5.5",
+    "express": "^4.18.3",
+    "fs-extra": "^11.2.0",
+    "graphql": "~16.9.0",
+    "graphql-fields": "^2.0.3",
+    "graphql-scalars": "^1.22.5",
+    "graphql-tag": "^2.12.6",
+    "graphql-upload": "^16.0.2",
+    "http-proxy-middleware": "^2.0.6",
+    "i18next": "^23.12.1",
+    "i18next-fs-backend": "^2.3.1",
+    "i18next-http-middleware": "^3.5.0",
+    "i18next-icu": "^2.3.0",
+    "image-size": "^1.1.1",
+    "intl-messageformat": "^10.5.11",
+    "mime-types": "^2.1.35",
+    "ms": "^2.1.3",
+    "nanoid": "^3.3.7",
+    "picocolors": "^1.0.0",
+    "progress": "^2.0.3",
+    "reflect-metadata": "^0.2.2",
+    "rxjs": "^7.8.1",
+    "semver": "^7.6.0",
+    "typeorm": "0.3.20"
+  },
+  "devDependencies": {
+    "@types/bcrypt": "^5.0.2",
+    "@types/cookie-session": "^2.0.48",
+    "@types/csv-parse": "^1.2.2",
+    "@types/express": "^4.17.21",
+    "@types/graphql-upload": "^16.0.7",
+    "@types/gulp": "^4.0.17",
+    "@types/mime-types": "^2.1.4",
+    "@types/ms": "^0.7.34",
+    "@types/node": "^18.19.23",
+    "@types/progress": "^2.0.7",
+    "@types/prompts": "^2.4.9",
+    "@types/semver": "^7.5.8",
+    "better-sqlite3": "^11.4.0",
+    "chokidar": "^3.6.0",
+    "fs-extra": "^11.2.0",
+    "glob": "^10.3.10",
+    "mysql": "^2.18.1",
+    "pg": "^8.11.3",
+    "rimraf": "^5.0.5",
+    "sql.js": "1.10.2",
+    "sqlite3": "^5.1.7",
+    "typescript": "5.3.3"
+  }
 }

+ 8 - 0
packages/core/src/api/config/configure-graphql-module.ts

@@ -134,6 +134,14 @@ async function createGraphQLOptions(
         plugins: apolloServerPlugins,
         validationRules: options.validationRules,
         introspection: configService.apiOptions.introspection ?? true,
+        subscriptions: {
+            'graphql-ws': {
+                path: '/' + options.apiPath,
+            },
+            'subscriptions-transport-ws': {
+                path: '/' + options.apiPath,
+            },
+        },
     } as ApolloDriverConfig;
 
     /**

+ 4 - 0
packages/core/src/api/schema/admin-api/administrator.api.graphql

@@ -19,6 +19,10 @@ type Mutation {
     assignRoleToAdministrator(administratorId: ID!, roleId: ID!): Administrator!
 }
 
+type Subscription {
+    ping: String!
+}
+
 # generated by generateListOptions function
 input AdministratorListOptions
 

+ 4 - 0
packages/core/src/api/schema/shop-api/shop.api.graphql

@@ -147,6 +147,10 @@ type Mutation {
     resetPassword(token: String!, password: String!): ResetPasswordResult!
 }
 
+type Subscription {
+    ping: String!
+}
+
 # Populated at run-time
 input AuthenticationInput
 

+ 8 - 0
packages/core/src/plugin/plugin-common.module.ts

@@ -1,4 +1,5 @@
 import { Module } from '@nestjs/common';
+import { PubSub } from 'graphql-subscriptions';
 
 import { CacheModule } from '../cache/cache.module';
 import { ConfigModule } from '../config/config.module';
@@ -51,6 +52,13 @@ import { ServiceModule } from '../service/service.module';
         I18nModule,
         ProcessContextModule,
         DataImportModule,
+        'PUB_SUB',
+    ],
+    providers: [
+        {
+            provide: 'PUB_SUB',
+            useValue: new PubSub(),
+        },
     ],
 })
 export class PluginCommonModule {}

+ 2 - 0
packages/dev-server/dev-config.ts

@@ -22,6 +22,7 @@ import path from 'path';
 import { DataSourceOptions } from 'typeorm';
 
 import { MultivendorPlugin } from './example-plugins/multivendor-plugin/multivendor.plugin';
+import { GraphqlSubscriptionsPlugin } from './test-plugins/graphql-subscriptions/graphql-subscriptions-plugin';
 
 /**
  * Config settings used during development
@@ -137,6 +138,7 @@ export const devConfig: VendureConfig = {
             //     devMode: true,
             // }),
         }),
+        GraphqlSubscriptionsPlugin,
     ],
 };
 

+ 58 - 0
packages/dev-server/test-plugins/graphql-subscriptions/graphql-subscriptions-plugin.ts

@@ -0,0 +1,58 @@
+import { Args, Mutation, Resolver, Subscription } from '@nestjs/graphql';
+import {
+    Ctx,
+    ID,
+    Order,
+    OrderService,
+    PluginCommonModule,
+    RequestContext,
+    TransactionalConnection,
+    VendurePlugin,
+} from '@vendure/core';
+import { PubSub } from 'graphql-subscriptions';
+import { gql } from 'graphql-tag';
+import { Inject } from '@nestjs/common';
+
+@Resolver()
+class OrderStateResolver {
+    constructor(
+        @Inject('PUB_SUB') private readonly pubSub: PubSub,
+        private readonly db: TransactionalConnection,
+    ) {}
+
+    @Subscription(() => Order, {
+        filter: (payload, variables) => {
+            return payload.orderStateUpdated.id === variables.orderId;
+        },
+    })
+    async orderStateUpdated(@Args('orderId') orderId: ID) {
+        return this.pubSub.asyncIterator('orderStateUpdated');
+    }
+
+    @Mutation(() => Order)
+    async triggerOrderStateUpdated(@Ctx() ctx: RequestContext, @Args('orderId') orderId: ID) {
+        const order = await this.db.getEntityOrThrow(ctx, Order, orderId);
+
+        return this.pubSub.publish('orderStateUpdated', { orderStateUpdated: order });
+    }
+}
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    shopApiExtensions: {
+        schema: gql`
+            extend type Subscription {
+                orderStateUpdated(orderId: ID!): Order!
+            }
+
+            extend type Mutation {
+                triggerOrderStateUpdated(orderId: ID!): Order
+            }
+        `,
+        resolvers: [OrderStateResolver],
+    },
+    configuration: config => {
+        return config;
+    },
+})
+export class GraphqlSubscriptionsPlugin {}