From 43b6a2cdcda2a489aa0015fadae9d5e63058d62a Mon Sep 17 00:00:00 2001 From: Vinh Date: Fri, 16 Aug 2019 04:03:32 +0700 Subject: [PATCH] [CORL-149] Persisted Queries (#2445) * feat: enable persisted queries on the client * fix: use `id` inside websocket message * feat: initial server support for PQ * feat: deeper server support * feat: abstracted persisted query replacing logic --- .gitignore | 1 + config/watcher.ts | 10 +- package-lock.json | 783 +++++++++++++----- package.json | 23 +- scripts/compileRelay.ts | 29 +- .../createManagedSubscriptionClient.ts | 41 +- .../framework/lib/network/createNetwork.ts | 8 +- .../persistedQueriesGetMethodMiddleware.ts | 46 + src/core/common/errors.ts | 13 + src/core/server/app/handlers/api/graphql.ts | 87 +- src/core/server/app/index.ts | 11 +- .../server/app/middleware/graphql/batch.ts | 97 --- .../middleware/graphql/graphqlMiddleware.ts | 85 ++ .../server/app/middleware/graphql/index.ts | 89 +- .../graphql/persistedQueryMiddleware.ts | 53 ++ src/core/server/app/router/api/index.ts | 2 + src/core/server/errors/index.ts | 27 +- src/core/server/errors/translations.ts | 2 + .../server/graph/tenant/persisted/index.ts | 2 + .../server/graph/tenant/persisted/loader.ts | 56 ++ .../server/graph/tenant/persisted/mapper.ts | 41 + .../graph/tenant/subscriptions/server.ts | 36 +- src/core/server/index.ts | 19 + src/core/server/locales/en-US/errors.ftl | 4 +- src/core/server/models/queries/cache.ts | 112 +++ src/core/server/models/queries/index.ts | 2 + src/core/server/models/queries/queries.ts | 48 ++ src/core/server/services/mongodb/indexes.ts | 2 + src/types/react-relay-network-modern.d.ts | 2 +- tsconfig.json | 2 +- 30 files changed, 1268 insertions(+), 465 deletions(-) create mode 100644 src/core/client/framework/lib/network/persistedQueriesGetMethodMiddleware.ts delete mode 100644 src/core/server/app/middleware/graphql/batch.ts create mode 100644 src/core/server/app/middleware/graphql/graphqlMiddleware.ts create mode 100644 src/core/server/app/middleware/graphql/persistedQueryMiddleware.ts create mode 100644 src/core/server/graph/tenant/persisted/index.ts create mode 100644 src/core/server/graph/tenant/persisted/loader.ts create mode 100644 src/core/server/graph/tenant/persisted/mapper.ts create mode 100644 src/core/server/models/queries/cache.ts create mode 100644 src/core/server/models/queries/index.ts create mode 100644 src/core/server/models/queries/queries.ts diff --git a/.gitignore b/.gitignore index 8f2d61ab9..5f07a743c 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ dist *.css.d.ts __generated__ README.md.orig +persisted-queries.json diff --git a/config/watcher.ts b/config/watcher.ts index 71810a902..05392d26d 100644 --- a/config/watcher.ts +++ b/config/watcher.ts @@ -27,7 +27,7 @@ const config: Config = { "**/test/**/*", "core/**/*.spec.*", ], - executor: new CommandExecutor("npm run --silent generate:relay-stream", { + executor: new CommandExecutor("npm run --silent generate:relay:stream", { runOnInit: true, }), }, @@ -44,7 +44,7 @@ const config: Config = { "**/test/**/*", "core/**/*.spec.*", ], - executor: new CommandExecutor("npm run generate:relay-account", { + executor: new CommandExecutor("npm run generate:relay:account", { runOnInit: true, }), }, @@ -61,7 +61,7 @@ const config: Config = { "**/test/**/*", "core/**/*.spec.*", ], - executor: new CommandExecutor("npm run --silent generate:relay-admin", { + executor: new CommandExecutor("npm run --silent generate:relay:admin", { runOnInit: true, }), }, @@ -78,7 +78,7 @@ const config: Config = { "**/test/**/*", "core/**/*.spec.*", ], - executor: new CommandExecutor("npm run --silent generate:relay-auth", { + executor: new CommandExecutor("npm run --silent generate:relay:auth", { runOnInit: true, }), }, @@ -95,7 +95,7 @@ const config: Config = { "**/test/**/*", "core/**/*.spec.*", ], - executor: new CommandExecutor("npm run --silent generate:relay-install", { + executor: new CommandExecutor("npm run --silent generate:relay:install", { runOnInit: true, }), }, diff --git a/package-lock.json b/package-lock.json index cb158a810..1dc609734 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,20 +4,18 @@ "lockfileVersion": 1, "requires": true, "dependencies": { - "@apollographql/apollo-upload-server": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@apollographql/apollo-upload-server/-/apollo-upload-server-5.0.3.tgz", - "integrity": "sha512-tGAp3ULNyoA8b5o9LsU2Lq6SwgVPUOKAqKywu2liEtTvrFSGPrObwanhYwArq3GPeOqp2bi+JknSJCIU3oQN1Q==", + "@apollographql/apollo-tools": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@apollographql/apollo-tools/-/apollo-tools-0.4.0.tgz", + "integrity": "sha512-7wEO+S+zgz/wVe3ilFQqICufRBYYDSNUkd1V03JWvXuSydbYq2SM5EgvWmFF+04iadt+aQ0XCCsRzCzRPQODfQ==", "requires": { - "@babel/runtime-corejs2": "^7.0.0-rc.1", - "busboy": "^0.2.14", - "object-path": "^0.11.4" + "apollo-env": "0.5.1" } }, "@apollographql/graphql-playground-html": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.4.tgz", - "integrity": "sha512-gwvaQO6/Hv4DEwhDLmmu2tzCU9oPjC5Xl9Kk8Yd0IxyKhYLlLalmkMMjsZLzU5H3fGaalLD96OYfxHL0ClVUDQ==" + "version": "1.6.24", + "resolved": "https://registry.npmjs.org/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.24.tgz", + "integrity": "sha512-8GqG48m1XqyXh4mIZrtB5xOhUwSsh1WsrrsaZQOEYYql3YN9DEu9OOSg0ILzXHZo/h2Q74777YE4YzlArQzQEQ==" }, "@babel/code-frame": { "version": "7.0.0", @@ -2033,22 +2031,6 @@ } } }, - "@babel/runtime-corejs2": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.1.2.tgz", - "integrity": "sha512-drxaPByExlcRDKW4ZLubUO4ZkI8/8ax9k9wve1aEthdLKFzjB7XRkOQ0xoTIWGxqdDnWDElkjYq77bt7yrcYJQ==", - "requires": { - "core-js": "^2.5.7", - "regenerator-runtime": "^0.12.0" - }, - "dependencies": { - "regenerator-runtime": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", - "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==" - } - } - }, "@babel/template": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.1.2.tgz", @@ -3617,6 +3599,17 @@ "@types/express": "*" } }, + "@types/cookies": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.7.2.tgz", + "integrity": "sha512-jnihWgshWystcJKrz8C9hV+Ot9lqOUyAh2RF+o3BEo6K6AS2l4zYCb9GYaBuZ3C6Il59uIGqpE3HvCun4KKeJA==", + "requires": { + "@types/connect": "*", + "@types/express": "*", + "@types/keygrip": "*", + "@types/node": "*" + } + }, "@types/cors": { "version": "2.8.4", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.4.tgz", @@ -3741,8 +3734,17 @@ "@types/graphql": { "version": "0.13.3", "resolved": "https://registry.npmjs.org/@types/graphql/-/graphql-0.13.3.tgz", - "integrity": "sha512-YNGkX5HtcDtufyQquww05yoWYiDdZPPubLafXqukYqGmpawHjodAXwufhTemqDdgGk48WU7RX2Ouj0VTc9b3AA==", - "dev": true + "integrity": "sha512-YNGkX5HtcDtufyQquww05yoWYiDdZPPubLafXqukYqGmpawHjodAXwufhTemqDdgGk48WU7RX2Ouj0VTc9b3AA==" + }, + "@types/graphql-upload": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@types/graphql-upload/-/graphql-upload-8.0.0.tgz", + "integrity": "sha512-xeDYfZb0SeRpCRuivN9TXLEVsbG0F4inFtx03yadZeaTXr1kC224/ZvlV6NKqQ//HNvUxneYcEoUB5ugJc8dnA==", + "requires": { + "@types/express": "*", + "@types/graphql": "*", + "@types/koa": "*" + } }, "@types/html-minifier": { "version": "3.5.2", @@ -3772,6 +3774,11 @@ "@types/webpack": "*" } }, + "@types/http-assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.0.tgz", + "integrity": "sha512-8CBLG8RmxSvoY07FE6M/QpvJ7J5KzeKqF8eWN7Dq6Ks+lBTQae8Roc2G81lUu2Kw5Ju1gymOuvgyUsussbjAaA==" + }, "@types/http-proxy": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.0.tgz", @@ -3881,6 +3888,32 @@ "@types/node": "*" } }, + "@types/keygrip": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.1.tgz", + "integrity": "sha1-/1QEYtL7TQqIRBzq8n0oewHD2Hg=" + }, + "@types/koa": { + "version": "2.0.49", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.0.49.tgz", + "integrity": "sha512-WQWpCH8O4Dslk8IcXfazff40aM1jXX7BQRbADIj/fKozVPu76P/wQE4sRe2SCWMn8yNkOcare2MkDrnZqLMkPQ==", + "requires": { + "@types/accepts": "*", + "@types/cookies": "*", + "@types/http-assert": "*", + "@types/keygrip": "*", + "@types/koa-compose": "*", + "@types/node": "*" + } + }, + "@types/koa-compose": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.4.tgz", + "integrity": "sha512-ioou0rxkuWL+yBQYsHUQAzRTfVxAg8Y2VfMftU+Y3RA03/MzuFL0x/M2sXXj3PkfnENbHsjeHR1aMdezLYpTeA==", + "requires": { + "@types/koa": "*" + } + }, "@types/linkify-it": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-2.1.0.tgz", @@ -3907,6 +3940,12 @@ "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.0.tgz", "integrity": "sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q==" }, + "@types/lru-cache": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.0.tgz", + "integrity": "sha512-RaE0B+14ToE4l6UqdarKPnXwVDuigfFv+5j9Dze/Nqr23yyuqdNvzcZi3xB+3Agvi5R4EOgAksfv3lXX4vBt9w==", + "dev": true + }, "@types/luxon": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-1.12.0.tgz", @@ -4382,6 +4421,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-5.1.2.tgz", "integrity": "sha512-NkTXUKTYdXdnPE2aUUbGOXE1XfMK527SCvU/9bj86kyFF6kZ9ZnOQ3mK5jADn98Y2vEUD/7wKDgZa7Qst2wYOg==", + "dev": true, "requires": { "@types/events": "*", "@types/node": "*" @@ -4600,6 +4640,14 @@ } } }, + "@wry/equality": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.1.9.tgz", + "integrity": "sha512-mB6ceGjpMGz1ZTza8HYnrPGos2mC6So4NhS1PtZ8s4Qt0K7fBiIGhpSxUbQmhwcSWE3no+bYxmI2OL6KuXYmoQ==", + "requires": { + "tslib": "^1.9.3" + } + }, "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -4851,43 +4899,124 @@ } }, "apollo-cache-control": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/apollo-cache-control/-/apollo-cache-control-0.2.5.tgz", - "integrity": "sha512-xEDrUvo3U2mQKSzA8NzQmgeqK4ytwFnTGl2YKGKPfG0+r8fZdswKp6CDBue29KLO8KkSuqW/hntveWrAdK51FQ==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/apollo-cache-control/-/apollo-cache-control-0.8.1.tgz", + "integrity": "sha512-yQy5KB/OuX90PsdztWc4vfc4R//ZmW/AxNgXKWga0xW5OzEsysdJWHAsTzb40/rkJ9VNeQ+0N5wGikiS+jSCzg==", "requires": { - "apollo-server-env": "^2.0.3", - "graphql-extensions": "^0.2.1" + "apollo-server-env": "2.4.1", + "graphql-extensions": "0.8.1" + }, + "dependencies": { + "apollo-server-env": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-2.4.1.tgz", + "integrity": "sha512-J4G1Q6qyb7KjjqvQdVM5HUH3QDb52VK1Rv+MWL0rHcstJx9Fh/NK0sS+nujrMfKw57NVUs2d4KuYtl/EnW/txg==", + "requires": { + "node-fetch": "^2.1.2", + "util.promisify": "^1.0.0" + } + }, + "graphql-extensions": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/graphql-extensions/-/graphql-extensions-0.8.1.tgz", + "integrity": "sha512-d/L4x7/PPWhviJqi7jIWOVJPzfzagYgPizSQUpa+3hozbWhwpWEnfxwgL5/If5MnPUikBnqlkOLCyjHMNdipYA==", + "requires": { + "@apollographql/apollo-tools": "^0.4.0", + "apollo-server-env": "2.4.1", + "apollo-server-types": "0.2.1" + } + } } }, "apollo-datasource": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/apollo-datasource/-/apollo-datasource-0.1.3.tgz", - "integrity": "sha512-yEGEe5Cjzqqu5ml1VV3O8+C+thzdknZri9Ny0P3daTGNO+45J3vBOMcmaANeeI2+OOeWxdqUNa5aPOx/35kniw==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/apollo-datasource/-/apollo-datasource-0.6.1.tgz", + "integrity": "sha512-oy7c+9Up8PSZwJ1qTK9Idh1acDpIocvw+C0zcHg14ycvNz7qWHSwLUSaAjuQMd9SYFzB3sxfyEhyfyhIogT2+Q==", "requires": { - "apollo-server-caching": "0.1.2", - "apollo-server-env": "2.0.3" + "apollo-server-caching": "0.5.0", + "apollo-server-env": "2.4.1" + }, + "dependencies": { + "apollo-server-env": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-2.4.1.tgz", + "integrity": "sha512-J4G1Q6qyb7KjjqvQdVM5HUH3QDb52VK1Rv+MWL0rHcstJx9Fh/NK0sS+nujrMfKw57NVUs2d4KuYtl/EnW/txg==", + "requires": { + "node-fetch": "^2.1.2", + "util.promisify": "^1.0.0" + } + } } }, "apollo-engine-reporting": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/apollo-engine-reporting/-/apollo-engine-reporting-0.0.6.tgz", - "integrity": "sha512-JmfNJ9v3QEJQ8ZhLfCKEDiww53n5kj5HarP85p8LreoYNojbvcWN8Qm6RgvSG5N/ZJrAYHeTRQbSxm1vWwGubw==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/apollo-engine-reporting/-/apollo-engine-reporting-1.4.3.tgz", + "integrity": "sha512-xv27qfc9dhi1yaWOhNQRmfF+SoLy74hl+M42arpIWdkoDe22fVTmTIqxqGwo4TFR3Z2OkAV5tNzuuOI/icd0Rg==", "requires": { - "apollo-engine-reporting-protobuf": "^0.0.1", - "apollo-server-env": "^2.0.3", + "apollo-engine-reporting-protobuf": "0.4.0", + "apollo-graphql": "^0.3.3", + "apollo-server-caching": "0.5.0", + "apollo-server-env": "2.4.1", + "apollo-server-types": "0.2.1", "async-retry": "^1.2.1", - "graphql-extensions": "^0.2.1", - "lodash": "^4.17.10" + "graphql-extensions": "0.9.1" + }, + "dependencies": { + "apollo-server-env": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-2.4.1.tgz", + "integrity": "sha512-J4G1Q6qyb7KjjqvQdVM5HUH3QDb52VK1Rv+MWL0rHcstJx9Fh/NK0sS+nujrMfKw57NVUs2d4KuYtl/EnW/txg==", + "requires": { + "node-fetch": "^2.1.2", + "util.promisify": "^1.0.0" + } + }, + "graphql-extensions": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/graphql-extensions/-/graphql-extensions-0.9.1.tgz", + "integrity": "sha512-JR/KStdwALd48B/xSG/Mi85zamuJd8THvVlzGM5juznPDN0wTYG5SARGzzvoqHxgxuUHYdzpvESwMAisORJdCQ==", + "requires": { + "@apollographql/apollo-tools": "^0.4.0", + "apollo-server-env": "2.4.1", + "apollo-server-types": "0.2.1" + } + } } }, "apollo-engine-reporting-protobuf": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.0.1.tgz", - "integrity": "sha512-AySoDgog2p1Nph44FyyqaU4AfRZOXx8XZxRsVHvYY4dHlrMmDDhhjfF3Jswa7Wr8X/ivvx3xA0jimRn6rsG8Ew==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.4.0.tgz", + "integrity": "sha512-cXHZSienkis8v4RhqB3YG3DkaksqLpcxApRLTpRMs7IXNozgV7CUPYGFyFBEra1ZFgUyHXx4G9MpelV+n2cCfA==", "requires": { "protobufjs": "^6.8.6" } }, + "apollo-env": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/apollo-env/-/apollo-env-0.5.1.tgz", + "integrity": "sha512-fndST2xojgSdH02k5hxk1cbqA9Ti8RX4YzzBoAB4oIe1Puhq7+YlhXGXfXB5Y4XN0al8dLg+5nAkyjNAR2qZTw==", + "requires": { + "core-js": "^3.0.1", + "node-fetch": "^2.2.0", + "sha.js": "^2.4.11" + }, + "dependencies": { + "core-js": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.1.4.tgz", + "integrity": "sha512-YNZN8lt82XIMLnLirj9MhKDFZHalwzzrL9YLt6eb0T5D0EDl4IQ90IGkua8mHbnxNrkj1d8hbdizMc0Qmg1WnQ==" + } + } + }, + "apollo-graphql": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/apollo-graphql/-/apollo-graphql-0.3.3.tgz", + "integrity": "sha512-t3CO/xIDVsCG2qOvx2MEbuu4b/6LzQjcBBwiVnxclmmFyAxYCIe7rpPlnLHSq7HyOMlCWDMozjoeWfdqYSaLqQ==", + "requires": { + "apollo-env": "0.5.1", + "lodash.sortby": "^4.7.0" + } + }, "apollo-link": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.2.tgz", @@ -4906,43 +5035,119 @@ } }, "apollo-server-caching": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/apollo-server-caching/-/apollo-server-caching-0.1.2.tgz", - "integrity": "sha512-jBRnsTgXN0m8yVpumoelaUq9mXR7YpJ3EE+y/alI7zgXY+0qFDqksRApU8dEfg3q6qUnO7rFxRhdG5eyc0+1ig==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/apollo-server-caching/-/apollo-server-caching-0.5.0.tgz", + "integrity": "sha512-l7ieNCGxUaUAVAAp600HjbUJxVaxjJygtPV0tPTe1Q3HkPy6LEWoY6mNHV7T268g1hxtPTxcdRu7WLsJrg7ufw==", "requires": { - "lru-cache": "^4.1.3" + "lru-cache": "^5.0.0" } }, "apollo-server-core": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.1.0.tgz", - "integrity": "sha512-D1Tw0o3NzCQ2KGM8EWh9AHELHmn/SE361dtlqJxkbelxXqAkCIGIFywF30h+0ezhMbgbO7eqBBJfvRilF/oJHA==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.8.1.tgz", + "integrity": "sha512-BpvhKdycTI1v5n8biJ5c/DVF7MCbTL3JtB9llHGkqYgHaTH1gXguh2qD8Vcki+rpUNO5P1lcj5V6oVXoSUFXlA==", "requires": { - "@apollographql/apollo-upload-server": "^5.0.3", - "@types/ws": "^5.1.2", - "apollo-cache-control": "^0.2.5", - "apollo-datasource": "^0.1.3", - "apollo-engine-reporting": "^0.0.6", - "apollo-server-caching": "^0.1.2", - "apollo-server-env": "^2.0.3", - "apollo-server-errors": "^2.0.2", - "apollo-tracing": "^0.2.5", - "graphql-extensions": "^0.2.1", - "graphql-subscriptions": "^0.5.8", + "@apollographql/apollo-tools": "^0.4.0", + "@apollographql/graphql-playground-html": "1.6.24", + "@types/graphql-upload": "^8.0.0", + "@types/ws": "^6.0.0", + "apollo-cache-control": "0.8.1", + "apollo-datasource": "0.6.1", + "apollo-engine-reporting": "1.4.3", + "apollo-server-caching": "0.5.0", + "apollo-server-env": "2.4.1", + "apollo-server-errors": "2.3.1", + "apollo-server-plugin-base": "0.6.1", + "apollo-server-types": "0.2.1", + "apollo-tracing": "0.8.1", + "fast-json-stable-stringify": "^2.0.0", + "graphql-extensions": "0.9.1", "graphql-tag": "^2.9.2", - "graphql-tools": "^3.0.4", - "hash.js": "^1.1.3", - "lodash": "^4.17.10", + "graphql-tools": "^4.0.0", + "graphql-upload": "^8.0.2", + "sha.js": "^2.4.11", "subscriptions-transport-ws": "^0.9.11", - "ws": "^5.2.0" + "ws": "^6.0.0" }, "dependencies": { - "graphql-subscriptions": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/graphql-subscriptions/-/graphql-subscriptions-0.5.8.tgz", - "integrity": "sha512-0CaZnXKBw2pwnIbvmVckby5Ge5e2ecmjofhYCdyeACbCly2j3WXDP/pl+s+Dqd2GQFC7y99NB+53jrt55CKxYQ==", + "@types/ws": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.2.tgz", + "integrity": "sha512-22XiR1ox9LftTaAtn/c5JCninwc7moaqbkJfaDUb7PkaUitcf5vbTZHdq9dxSMviCm9C3W85rzB8e6yNR70apQ==", "requires": { - "iterall": "^1.2.1" + "@types/node": "*" + } + }, + "apollo-link": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.12.tgz", + "integrity": "sha512-fsgIAXPKThyMVEMWQsUN22AoQI+J/pVXcjRGAShtk97h7D8O+SPskFinCGEkxPeQpE83uKaqafB2IyWdjN+J3Q==", + "requires": { + "apollo-utilities": "^1.3.0", + "ts-invariant": "^0.4.0", + "tslib": "^1.9.3", + "zen-observable-ts": "^0.8.19" + }, + "dependencies": { + "apollo-utilities": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.3.2.tgz", + "integrity": "sha512-JWNHj8XChz7S4OZghV6yc9FNnzEXj285QYp/nLNh943iObycI5GTDO3NGR9Dth12LRrSFMeDOConPfPln+WGfg==", + "requires": { + "@wry/equality": "^0.1.2", + "fast-json-stable-stringify": "^2.0.0", + "ts-invariant": "^0.4.0", + "tslib": "^1.9.3" + } + } + } + }, + "apollo-server-env": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-2.4.1.tgz", + "integrity": "sha512-J4G1Q6qyb7KjjqvQdVM5HUH3QDb52VK1Rv+MWL0rHcstJx9Fh/NK0sS+nujrMfKw57NVUs2d4KuYtl/EnW/txg==", + "requires": { + "node-fetch": "^2.1.2", + "util.promisify": "^1.0.0" + } + }, + "graphql-extensions": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/graphql-extensions/-/graphql-extensions-0.9.1.tgz", + "integrity": "sha512-JR/KStdwALd48B/xSG/Mi85zamuJd8THvVlzGM5juznPDN0wTYG5SARGzzvoqHxgxuUHYdzpvESwMAisORJdCQ==", + "requires": { + "@apollographql/apollo-tools": "^0.4.0", + "apollo-server-env": "2.4.1", + "apollo-server-types": "0.2.1" + } + }, + "graphql-tools": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/graphql-tools/-/graphql-tools-4.0.5.tgz", + "integrity": "sha512-kQCh3IZsMqquDx7zfIGWBau42xe46gmqabwYkpPlCLIjcEY1XK+auP7iGRD9/205BPyoQdY8hT96MPpgERdC9Q==", + "requires": { + "apollo-link": "^1.2.3", + "apollo-utilities": "^1.0.1", + "deprecated-decorator": "^0.1.6", + "iterall": "^1.1.3", + "uuid": "^3.1.0" + } + }, + "ws": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", + "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", + "requires": { + "async-limiter": "~1.0.0" + } + }, + "zen-observable-ts": { + "version": "0.8.19", + "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.8.19.tgz", + "integrity": "sha512-u1a2rpE13G+jSzrg3aiCqXU5tN2kw41b+cBZGmnc+30YimdkKiDj9bTowcB41eL77/17RF/h+393AuVgShyheQ==", + "requires": { + "tslib": "^1.9.3", + "zen-observable": "^0.8.0" } } } @@ -4957,88 +5162,237 @@ } }, "apollo-server-errors": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/apollo-server-errors/-/apollo-server-errors-2.0.2.tgz", - "integrity": "sha512-zyWDqAVDCkj9espVsoUpZr9PwDznM8UW6fBfhV+i1br//s2AQb07N6ektZ9pRIEvkhykDZW+8tQbDwAO0vUROg==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/apollo-server-errors/-/apollo-server-errors-2.3.1.tgz", + "integrity": "sha512-errZvnh0vUQChecT7M4A/h94dnBSRL213dNxpM5ueMypaLYgnp4hiCTWIEaooo9E4yMGd1qA6WaNbLDG2+bjcg==" }, "apollo-server-express": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-2.1.0.tgz", - "integrity": "sha512-jLFIz1VLduMA/rme4OAy3IPeoaMEZOPoQXpio8AhfjIqCijRPPfoWJ2QMqz56C/g3vas7rZtgcVOrHpjBKudjw==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-2.8.1.tgz", + "integrity": "sha512-XoWqSuNQkL8ivBq5LXJW6wV0/Ef+m8w4fAK/7PBspLHVfDAbHRyRr6zraotim2Kl7NOnzcqHtb6sB9yozjL0hA==", "requires": { - "@apollographql/apollo-upload-server": "^5.0.3", - "@apollographql/graphql-playground-html": "^1.6.0", + "@apollographql/graphql-playground-html": "1.6.24", "@types/accepts": "^1.3.5", "@types/body-parser": "1.17.0", "@types/cors": "^2.8.4", - "@types/express": "4.16.0", + "@types/express": "4.17.0", "accepts": "^1.3.5", - "apollo-server-core": "^2.1.0", + "apollo-server-core": "2.8.1", + "apollo-server-types": "0.2.1", "body-parser": "^1.18.3", "cors": "^2.8.4", - "graphql-subscriptions": "^0.5.8", - "graphql-tools": "^3.0.4", + "graphql-subscriptions": "^1.0.0", + "graphql-tools": "^4.0.0", + "subscriptions-transport-ws": "^0.9.16", "type-is": "^1.6.16" }, "dependencies": { - "body-parser": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", - "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", + "@types/express": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.0.tgz", + "integrity": "sha512-CjaMu57cjgjuZbh9DpkloeGxV45CnMGlVd+XpG7Gm9QgVrd7KFq+X4HY0vM+2v0bczS48Wg7bvnMY5TN+Xmcfw==", "requires": { - "bytes": "3.0.0", + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "apollo-link": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.12.tgz", + "integrity": "sha512-fsgIAXPKThyMVEMWQsUN22AoQI+J/pVXcjRGAShtk97h7D8O+SPskFinCGEkxPeQpE83uKaqafB2IyWdjN+J3Q==", + "requires": { + "apollo-utilities": "^1.3.0", + "ts-invariant": "^0.4.0", + "tslib": "^1.9.3", + "zen-observable-ts": "^0.8.19" + }, + "dependencies": { + "apollo-utilities": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.3.2.tgz", + "integrity": "sha512-JWNHj8XChz7S4OZghV6yc9FNnzEXj285QYp/nLNh943iObycI5GTDO3NGR9Dth12LRrSFMeDOConPfPln+WGfg==", + "requires": { + "@wry/equality": "^0.1.2", + "fast-json-stable-stringify": "^2.0.0", + "ts-invariant": "^0.4.0", + "tslib": "^1.9.3" + } + } + } + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", "content-type": "~1.0.4", "debug": "2.6.9", "depd": "~1.1.2", - "http-errors": "~1.6.3", - "iconv-lite": "0.4.23", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", "on-finished": "~2.3.0", - "qs": "6.5.2", - "raw-body": "2.3.3", - "type-is": "~1.6.16" + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "dependencies": { + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + } } }, - "graphql-subscriptions": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/graphql-subscriptions/-/graphql-subscriptions-0.5.8.tgz", - "integrity": "sha512-0CaZnXKBw2pwnIbvmVckby5Ge5e2ecmjofhYCdyeACbCly2j3WXDP/pl+s+Dqd2GQFC7y99NB+53jrt55CKxYQ==", + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "graphql-tools": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/graphql-tools/-/graphql-tools-4.0.5.tgz", + "integrity": "sha512-kQCh3IZsMqquDx7zfIGWBau42xe46gmqabwYkpPlCLIjcEY1XK+auP7iGRD9/205BPyoQdY8hT96MPpgERdC9Q==", "requires": { - "iterall": "^1.2.1" + "apollo-link": "^1.2.3", + "apollo-utilities": "^1.0.1", + "deprecated-decorator": "^0.1.6", + "iterall": "^1.1.3", + "uuid": "^3.1.0" + } + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" } }, "iconv-lite": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", - "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "requires": { "safer-buffer": ">= 2.1.2 < 3" } }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "requires": { + "mime-db": "1.40.0" + } + }, "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, "raw-body": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", - "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", "requires": { - "bytes": "3.0.0", - "http-errors": "1.6.3", - "iconv-lite": "0.4.23", + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", "unpipe": "1.0.0" } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "zen-observable-ts": { + "version": "0.8.19", + "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.8.19.tgz", + "integrity": "sha512-u1a2rpE13G+jSzrg3aiCqXU5tN2kw41b+cBZGmnc+30YimdkKiDj9bTowcB41eL77/17RF/h+393AuVgShyheQ==", + "requires": { + "tslib": "^1.9.3", + "zen-observable": "^0.8.0" + } + } + } + }, + "apollo-server-plugin-base": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/apollo-server-plugin-base/-/apollo-server-plugin-base-0.6.1.tgz", + "integrity": "sha512-gLLF0kz4QOOyczDGWuR2ZNDfa1nHfyFNG76ue8Es0/0ujnMT9KoSokXkx1hDh0X7FFTMj/MelYYoNEqgTH88zw==", + "requires": { + "apollo-server-types": "0.2.1" + } + }, + "apollo-server-types": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/apollo-server-types/-/apollo-server-types-0.2.1.tgz", + "integrity": "sha512-ls26d6jjY7x91ctLWtbpQHGW0lcFR1LcOpDvBQUC2aCwQzuW/6yV7F3hfcEdLR9pjIxcA4yAtFQcKf5olDWVkA==", + "requires": { + "apollo-engine-reporting-protobuf": "0.4.0", + "apollo-server-caching": "0.5.0", + "apollo-server-env": "2.4.1" + }, + "dependencies": { + "apollo-server-env": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-2.4.1.tgz", + "integrity": "sha512-J4G1Q6qyb7KjjqvQdVM5HUH3QDb52VK1Rv+MWL0rHcstJx9Fh/NK0sS+nujrMfKw57NVUs2d4KuYtl/EnW/txg==", + "requires": { + "node-fetch": "^2.1.2", + "util.promisify": "^1.0.0" + } } } }, "apollo-tracing": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/apollo-tracing/-/apollo-tracing-0.2.5.tgz", - "integrity": "sha512-DZO7pfL5LATHeJdVFoTZ/N3HwA+IMf1YnIt5K+uMQW+/MrRgYOtTszUv5tYX2cUIqHYHcbdDaBQUuIXwSpaV2Q==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/apollo-tracing/-/apollo-tracing-0.8.1.tgz", + "integrity": "sha512-zhVNC7N6hg9IJEeSEXFDxcnXD5GJQAbHxaoKVBKEolcIIsz6EGd700ORdagJgFKLReVp9O65HPrZJCg66sVx7g==", "requires": { - "apollo-server-env": "^2.0.3", - "graphql-extensions": "^0.2.1" + "apollo-server-env": "2.4.1", + "graphql-extensions": "0.8.1" + }, + "dependencies": { + "apollo-server-env": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-2.4.1.tgz", + "integrity": "sha512-J4G1Q6qyb7KjjqvQdVM5HUH3QDb52VK1Rv+MWL0rHcstJx9Fh/NK0sS+nujrMfKw57NVUs2d4KuYtl/EnW/txg==", + "requires": { + "node-fetch": "^2.1.2", + "util.promisify": "^1.0.0" + } + }, + "graphql-extensions": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/graphql-extensions/-/graphql-extensions-0.8.1.tgz", + "integrity": "sha512-d/L4x7/PPWhviJqi7jIWOVJPzfzagYgPizSQUpa+3hozbWhwpWEnfxwgL5/If5MnPUikBnqlkOLCyjHMNdipYA==", + "requires": { + "@apollographql/apollo-tools": "^0.4.0", + "apollo-server-env": "2.4.1", + "apollo-server-types": "0.2.1" + } + } } }, "apollo-utilities": { @@ -7070,35 +7424,11 @@ } }, "busboy": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", - "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.3.1.tgz", + "integrity": "sha512-y7tTxhGKXcyBxRKAni+awqx8uqaJKrSFSNFSeRG5CsWNdmy2BIK+6VGWEW7TZnIO/533mtMEA4rOevQV815YJw==", "requires": { - "dicer": "0.2.5", - "readable-stream": "1.1.x" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } + "dicer": "0.3.0" } }, "bytes": { @@ -8526,7 +8856,8 @@ "core-js": { "version": "2.5.7", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", - "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==" + "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==", + "dev": true }, "core-js-compat": { "version": "3.1.3", @@ -10190,35 +10521,11 @@ } }, "dicer": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", - "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz", + "integrity": "sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==", "requires": { - "readable-stream": "1.1.x", "streamsearch": "0.1.2" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } } }, "diff": { @@ -12993,6 +13300,18 @@ "lru-cache": "^4.0.1", "shebang-command": "^1.2.0", "which": "^1.2.9" + }, + "dependencies": { + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + } } } } @@ -14602,6 +14921,11 @@ "readable-stream": "^2.0.0" } }, + "fs-capacitor": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fs-capacitor/-/fs-capacitor-2.0.4.tgz", + "integrity": "sha512-8S4f4WsCryNw2mJJchi46YgB6CR5Ze+4L1h8ewl9tEpL4SJ3ZO+c/bS4BWhB8bK+O3TMqhuZarTitd0S0eh2pA==" + }, "fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -16462,9 +16786,9 @@ } }, "graphql-tag": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.10.0.tgz", - "integrity": "sha512-9FD6cw976TLLf9WYIUPCaaTpniawIjHWZSwIRZSjrfufJamcXbVVYfN2TWvJYbw0Xf2JjYbl1/f2+wDnBVw3/w==" + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.10.1.tgz", + "integrity": "sha512-jApXqWBzNXQ8jYa/HLkZJaVw9jgwNqZkywa2zfFn16Iv1Zb7ELNHkJaXHR7Quvd5SIGsy6Ny7SUKATgnu05uEg==" }, "graphql-tools": { "version": "3.0.5", @@ -16478,6 +16802,46 @@ "uuid": "^3.1.0" } }, + "graphql-upload": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/graphql-upload/-/graphql-upload-8.0.7.tgz", + "integrity": "sha512-gi2yygbDPXbHPC7H0PNPqP++VKSoNoJO4UrXWq4T0Bi4IhyUd3Ycop/FSxhx2svWIK3jdXR/i0vi91yR1aAF0g==", + "requires": { + "busboy": "^0.3.1", + "fs-capacitor": "^2.0.4", + "http-errors": "^1.7.2", + "object-path": "^0.11.4" + }, + "dependencies": { + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + } + } + }, "growly": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", @@ -17027,6 +17391,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.4.tgz", "integrity": "sha512-A6RlQvvZEtFS5fLU43IDu0QUmBy+fDO9VMdTXvufKwIkt/rFfvICAViCax5fbDO4zdNzaC3/27ZhKUok5bAJyw==", + "dev": true, "requires": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.0" @@ -21197,12 +21562,18 @@ } }, "lru-cache": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", - "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" + "yallist": "^3.0.2" + }, + "dependencies": { + "yallist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" + } } }, "lru-memoizer": { @@ -21775,7 +22146,8 @@ "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true }, "minimalistic-crypto-utils": { "version": "1.0.1", @@ -27110,6 +27482,15 @@ "requires": { "ms": "^2.1.1" } + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } } } }, @@ -28639,9 +29020,20 @@ } }, "react-relay-network-modern": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/react-relay-network-modern/-/react-relay-network-modern-3.0.4.tgz", - "integrity": "sha512-d1ttgGGRrjvntUeStKOGO4tsJy2P7hQ+T98DzdTa8QFEx8//0+/IPc1TUtFk0GVTUQJXG66cQw7EknwDRmRLfg==" + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-relay-network-modern/-/react-relay-network-modern-4.0.4.tgz", + "integrity": "sha512-bi4MxHxdehwziGPR9se3c3LfqH7duu5sz5K6sTunQbCsvi/xKeWcHqMhHPJcim9VcABGDOV5CzB9pDHcAOHbsA==", + "requires": { + "@types/relay-runtime": "^5.0.3" + }, + "dependencies": { + "@types/relay-runtime": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/relay-runtime/-/relay-runtime-5.0.3.tgz", + "integrity": "sha512-JSnnY+Qfc5/airey+gXxRgfmBX7PzABH0G8+nYYfhtBdioD+mzl2X9VQCgJLiwFGnFvdZXZcGS3p91OrZEsdWQ==", + "optional": true + } + } }, "react-responsive": { "version": "7.0.0", @@ -30184,7 +30576,6 @@ "version": "2.4.11", "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dev": true, "requires": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -32191,6 +32582,11 @@ "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=", "dev": true }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, "topo": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/topo/-/topo-3.0.0.tgz", @@ -32276,6 +32672,14 @@ "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==", "dev": true }, + "ts-invariant": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.4.4.tgz", + "integrity": "sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA==", + "requires": { + "tslib": "^1.9.3" + } + }, "ts-jest": { "version": "23.1.4", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-23.1.4.tgz", @@ -32456,8 +32860,7 @@ "tslib": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", - "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", - "dev": true + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" }, "tslint": { "version": "5.16.0", diff --git a/package.json b/package.json index e6f8f276c..148cad47f 100644 --- a/package.json +++ b/package.json @@ -20,18 +20,21 @@ ], "description": "A better commenting experience from Mozilla, The Washington Post, and The New York Times.", "scripts": { - "build": "NODE_ENV=production npm-run-all generate --parallel lint:client build:client build:server", + "build": "NODE_ENV=production npm-run-all generate-persist --parallel lint:client build:client build:server", "build:development": "NODE_ENV=development npm-run-all generate --parallel lint:client build:client build:server", "build:client": "ts-node --transpile-only ./scripts/build.ts", "build:server": "gulp server", "doctoc": "doctoc --title='## Table of Contents' --github README.md", - "generate": "npm-run-all --parallel generate:*", + "generate": "npm-run-all generate:css-types generate:schema generate:relay", + "generate-persist": "npm-run-all generate:css-types generate:schema generate:relay-persist", "generate:css-types": "tcm src/core/client/", - "generate:relay-stream": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/stream --schema tenant", - "generate:relay-account": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/account --schema tenant", - "generate:relay-auth": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/auth --schema tenant", - "generate:relay-install": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/install --schema tenant", - "generate:relay-admin": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/admin --schema tenant", + "generate:relay": "npm-run-all --parallel generate:relay:*", + "generate:relay-persist": "npm-run-all --parallel 'generate:relay:* -- --persist'", + "generate:relay:stream": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/stream --schema tenant", + "generate:relay:account": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/account --schema tenant", + "generate:relay:auth": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/auth --schema tenant", + "generate:relay:install": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/install --schema tenant", + "generate:relay:admin": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/admin --schema tenant", "generate:schema": "node ./scripts/generateSchemaTypes.js", "docz": "docz", "start": "NODE_ENV=production node dist/index.js", @@ -55,7 +58,7 @@ "@coralproject/bunyan-prettystream": "^0.1.4", "@types/archiver": "^3.0.0", "akismet-api": "^4.2.0", - "apollo-server-express": "^2.1.0", + "apollo-server-express": "^2.8.1", "archiver": "^3.0.3", "basic-auth": "^2.0.1", "bcryptjs": "^2.4.3", @@ -102,6 +105,7 @@ "linkify-it": "^2.1.0", "linkifyjs": "^2.1.8", "lodash": "^4.17.10", + "lru-cache": "^5.1.1", "luxon": "^1.12.0", "metascraper-author": "^3.11.8", "metascraper-date": "^3.11.4", @@ -126,7 +130,7 @@ "prom-client": "^11.3.0", "proxy-agent": "^3.1.0", "querystringify": "^2.1.0", - "react-relay-network-modern": "^3.0.4", + "react-relay-network-modern": "^4.0.4", "source-map-support": "^0.5.12", "stack-utils": "^1.0.2", "striptags": "^3.1.1", @@ -184,6 +188,7 @@ "@types/linkify-it": "^2.1.0", "@types/linkifyjs": "^2.1.1", "@types/lodash": "^4.14.118", + "@types/lru-cache": "^5.1.0", "@types/luxon": "^1.12.0", "@types/marked": "^0.6.0", "@types/mini-css-extract-plugin": "^0.2.0", diff --git a/scripts/compileRelay.ts b/scripts/compileRelay.ts index 81b1aaab8..1c8c85b93 100644 --- a/scripts/compileRelay.ts +++ b/scripts/compileRelay.ts @@ -2,7 +2,7 @@ import program from "commander"; import spawn from "cross-spawn"; -import fs from "fs"; +import fs from "fs-extra"; import path from "path"; const config = JSON.parse( @@ -14,6 +14,7 @@ program .usage("--src ./src/core/client/stream --schema tenant") .option("--src ", "Find gql recursively in this folder") .option("--schema ", "Identifier of schema") + .option("--persist", "Use persisted queries") .description("Compile relay gql data") .parse(process.argv); @@ -57,8 +58,30 @@ const args = [ `${program.src}/__generated__`, "--schema", config.projects[program.schema].schemaPath, - // "--persist-output", - // `${program.src}/persisted-queries.json`, ]; +// Set the persisted query path. +const persist = program.persist + ? `${program.src}/persisted-queries.json` + : null; +if (persist) { + args.push("--persist-output", persist); +} + spawn.sync("relay-compiler", args, { stdio: "inherit" }); + +if (persist) { + if (fs.existsSync(persist)) { + // Create the new filename. + const name = path.basename(program.src); + const generated = "./src/core/server/graph/tenant/persisted/__generated__"; + + // Create the generated directory if it doesn't exist. + fs.ensureDirSync(generated); + + // Copy the file over to the destination directory. + fs.copySync(persist, `${generated}/${name}.json`, { + overwrite: true, + }); + } +} diff --git a/src/core/client/framework/lib/network/createManagedSubscriptionClient.ts b/src/core/client/framework/lib/network/createManagedSubscriptionClient.ts index d24983217..29d227a8c 100644 --- a/src/core/client/framework/lib/network/createManagedSubscriptionClient.ts +++ b/src/core/client/framework/lib/network/createManagedSubscriptionClient.ts @@ -4,13 +4,16 @@ import { Disposable, Variables, } from "react-relay-network-modern/es"; -import { SubscriptionClient } from "subscriptions-transport-ws"; +import { + OperationOptions, + SubscriptionClient, +} from "subscriptions-transport-ws"; import { ACCESS_TOKEN_PARAM, CLIENT_ID_PARAM } from "coral-common/constants"; import { ERROR_CODES } from "coral-common/errors"; /** - * SubscriptionRequest containts the subscription + * SubscriptionRequest contains the subscription * request data that comes from Relay. */ export interface SubscriptionRequest { @@ -109,17 +112,29 @@ export default function createManagedSubscriptionClient( }, }); } - const subscription = subscriptionClient - .request({ - operationName: operation.name, - query: operation.text!, - variables, - }) - .subscribe({ - next({ data }) { - observer.onNext({ data }); - }, - }); + if (!operation.text && !operation.id) { + throw Error("Neither subscription query nor id was provided."); + } + + const opts: OperationOptions = { + operationName: operation.name, + // subscriptions-transport-ws requires `query` to be set to an non-empty string. + // With persisted queries we only have the id, so set this to + // "PERSISTED_QUERY" to get around validation. + query: operation.text || "PERSISTED_QUERY", + variables, + }; + + // Query is not available which means we can use the id from persisted queries. + if (!operation.text) { + opts.id = operation.id; + } + + const subscription = subscriptionClient.request(opts).subscribe({ + next({ data }) { + observer.onNext({ data }); + }, + }); request.unsubscribe = () => { subscription.unsubscribe(); }; diff --git a/src/core/client/framework/lib/network/createNetwork.ts b/src/core/client/framework/lib/network/createNetwork.ts index 62b2b2d45..672ab3b0e 100644 --- a/src/core/client/framework/lib/network/createNetwork.ts +++ b/src/core/client/framework/lib/network/createNetwork.ts @@ -1,6 +1,5 @@ import { authMiddleware, - batchMiddleware, cacheMiddleware, RelayNetworkLayer, retryMiddleware, @@ -11,6 +10,7 @@ import { import clientIDMiddleware from "./clientIDMiddleware"; import { ManagedSubscriptionClient } from "./createManagedSubscriptionClient"; import customErrorMiddleware from "./customErrorMiddleware"; +import persistedQueriesGetMethodMiddleware from "./persistedQueriesGetMethodMiddleware"; export type TokenGetter = () => string; @@ -51,11 +51,6 @@ export default function createNetwork( urlMiddleware({ url: () => Promise.resolve(graphqlURL), }), - batchMiddleware({ - batchUrl: (requestMap: any) => Promise.resolve(graphqlURL), - batchTimeout: 0, - allowMutations: true, - }), retryMiddleware({ fetchTimeout: 15000, retryDelays: (attempt: number) => Math.pow(2, attempt + 4) * 100, @@ -66,6 +61,7 @@ export default function createNetwork( token: tokenGetter, }), clientIDMiddleware(clientID), + persistedQueriesGetMethodMiddleware, ], { subscribeFn: createSubscriptionFunction(subscriptionClient) } ); diff --git a/src/core/client/framework/lib/network/persistedQueriesGetMethodMiddleware.ts b/src/core/client/framework/lib/network/persistedQueriesGetMethodMiddleware.ts new file mode 100644 index 000000000..b59588f46 --- /dev/null +++ b/src/core/client/framework/lib/network/persistedQueriesGetMethodMiddleware.ts @@ -0,0 +1,46 @@ +import { Middleware, RelayRequestAny } from "react-relay-network-modern/es"; + +import { modifyQuery } from "coral-framework/utils"; + +function hasMutations(req: RelayRequestAny): boolean { + return req.isMutation(); +} + +function queriesAreEmpty(req: RelayRequestAny): boolean { + return req.getQueryString() === ""; +} + +/** + * persistedQueriesGetMethodMiddleware will use the GET method instead of POST for + * all request excluding mutations when persisted queries are used. + * The request data will be encoded in base64url and set in the GET query string under + * the variable "d=". + */ +const persistedQueriesGetMethodMiddleware: Middleware = next => async req => { + if (queriesAreEmpty(req) && !hasMutations(req)) { + // Pull the body out (serializing it) and delete it off of the original + // fetch options. + const body: Record = JSON.parse(req.fetchOpts.body as string); + delete req.fetchOpts.body; + + // Reconfigure the fetch for GET. + req.fetchOpts.method = "GET"; + + // Rebuild the query parameters for GET. + const params: Record = { query: "" }; + for (const key in body) { + if (!body.hasOwnProperty(key)) { + continue; + } + + const value = body[key]; + params[key] = typeof value === "string" ? value : JSON.stringify(value); + } + + // Combine the new parameters onto the URL. + req.fetchOpts.url = modifyQuery(req.fetchOpts.url as string, params); + } + return next(req); +}; + +export default persistedQueriesGetMethodMiddleware; diff --git a/src/core/common/errors.ts b/src/core/common/errors.ts index 60aa6b69f..009a61d67 100644 --- a/src/core/common/errors.ts +++ b/src/core/common/errors.ts @@ -295,4 +295,17 @@ export enum ERROR_CODES { * someone now allowed when it is disabled on the tenant level. */ LIVE_UPDATES_DISABLED = "LIVE_UPDATES_DISABLED", + + /** + * PERSISTED_QUERY_NOT_FOUND is returned when a query is executed specifying a + * persisted query that can not be found. + */ + PERSISTED_QUERY_NOT_FOUND = "PERSISTED_QUERY_NOT_FOUND", + + /** + * RAW_QUERY_NOT_AUTHORIZED is returned when a query is executed that is not a + * persisted query when the server has configured such queries are required by + * all non-admin users. + */ + RAW_QUERY_NOT_AUTHORIZED = "RAW_QUERY_NOT_AUTHORIZED", } diff --git a/src/core/server/app/handlers/api/graphql.ts b/src/core/server/app/handlers/api/graphql.ts index 3a61d7b41..76046e4b5 100644 --- a/src/core/server/app/handlers/api/graphql.ts +++ b/src/core/server/app/handlers/api/graphql.ts @@ -1,9 +1,6 @@ import { CLIENT_ID_HEADER } from "coral-common/constants"; import { AppOptions } from "coral-server/app"; -import { - graphqlBatchMiddleware, - graphqlMiddleware, -} from "coral-server/app/middleware/graphql"; +import { graphqlMiddleware } from "coral-server/app/middleware/graphql"; import TenantContext, { TenantContextOptions, } from "coral-server/graph/tenant/context"; @@ -30,53 +27,51 @@ export const graphQLHandler = ({ metrics, ...options }: GraphMiddlewareOptions): RequestHandler => - graphqlBatchMiddleware( - graphqlMiddleware( - config, - async (req: Request) => { - if (!req.coral) { - throw new Error("coral was not set"); - } + graphqlMiddleware( + config, + async (req: Request) => { + if (!req.coral) { + throw new Error("coral was not set"); + } - // Pull out some useful properties from Coral. - const { id, now, tenant, cache, logger } = req.coral; + // Pull out some useful properties from Coral. + const { id, now, tenant, cache, logger } = req.coral; - if (!cache) { - throw new Error("cache was not set"); - } + if (!cache) { + throw new Error("cache was not set"); + } - if (!tenant) { - throw new Error("tenant was not set"); - } + if (!tenant) { + throw new Error("tenant was not set"); + } - // Create some new options to store the tenant context details inside. - const opts: TenantContextOptions = { - ...options, - id, - now, - req, - config, - tenant, - logger, - }; + // Create some new options to store the tenant context details inside. + const opts: TenantContextOptions = { + ...options, + id, + now, + req, + config, + tenant, + logger, + }; - // Add the user if there is one. - if (req.user) { - opts.user = req.user; - } + // Add the user if there is one. + if (req.user) { + opts.user = req.user; + } - // Add the clientID if there is one on the request. - const clientID = req.get(CLIENT_ID_HEADER); - if (clientID) { - // TODO: (wyattjoh) validate length - opts.clientID = clientID; - } + // Add the clientID if there is one on the request. + const clientID = req.get(CLIENT_ID_HEADER); + if (clientID) { + // TODO: (wyattjoh) validate length + opts.clientID = clientID; + } - return { - schema, - context: new TenantContext(opts), - }; - }, - metrics - ) + return { + schema, + context: new TenantContext(opts), + }; + }, + metrics ); diff --git a/src/core/server/app/index.ts b/src/core/server/app/index.ts index 1961f3491..a6c853222 100644 --- a/src/core/server/app/index.ts +++ b/src/core/server/app/index.ts @@ -13,6 +13,7 @@ import { HTMLErrorHandler } from "coral-server/app/middleware/error"; import { notFoundMiddleware } from "coral-server/app/middleware/notFound"; import { createPassport } from "coral-server/app/middleware/passport"; import { Config } from "coral-server/config"; +import { PersistedQueryCache } from "coral-server/models/queries"; import { MailerQueue } from "coral-server/queue/tasks/mailer"; import { ScraperQueue } from "coral-server/queue/tasks/scraper"; import { I18n } from "coral-server/services/i18n"; @@ -28,18 +29,20 @@ import { createRouter } from "./router"; export interface AppOptions { config: Config; + disableClientRoutes: boolean; i18n: I18n; mailerQueue: MailerQueue; - scraperQueue: ScraperQueue; + metrics?: Metrics; mongo: Db; parent: Express; + persistedQueryCache: PersistedQueryCache; + persistedQueriesRequired: boolean; + pubsub: RedisPubSub; redis: AugmentedRedis; schema: GraphQLSchema; + scraperQueue: ScraperQueue; signingConfig: JWTSigningConfig; tenantCache: TenantCache; - disableClientRoutes: boolean; - metrics?: Metrics; - pubsub: RedisPubSub; } /** diff --git a/src/core/server/app/middleware/graphql/batch.ts b/src/core/server/app/middleware/graphql/batch.ts deleted file mode 100644 index 4337fa175..000000000 --- a/src/core/server/app/middleware/graphql/batch.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { RequestHandler, Response } from "express"; - -import logger from "coral-server/logger"; -import { Request } from "coral-server/types/express"; - -function wrapResponse(req: Request, res: Response) { - // If the request is not an array, or has no elements, we should skip it. - if (!Array.isArray(req.body) || req.body.length === 0) { - return res; - } - - // If the request is an array, but it does not have an ID field, then we - // should skip it. - const needsUpgrade = Boolean(typeof req.body[0].id !== "undefined"); - if (!needsUpgrade) { - return res; - } - - // Grab all the existing ID's. - const ids: string[] = req.body.map(({ id }) => id); - - // Save a reference to the old setHeader function. - const setHeader = res.setHeader.bind(res); - - // Capture all the headers that are sent to this, in case we need to use it. - const setHeaders: Record = {}; - res.setHeader = (name: string, value: any) => { - setHeaders[name] = value; - return res; - }; - - // Save a reference to the old write function. - const write = res.write.bind(res); - - // Create a flush function that will be used to flush the response to the - // underlying response. - const flush = (chunk: any, headers: Record = setHeaders) => { - for (const name in headers) { - if (!headers.hasOwnProperty(name)) { - continue; - } - - setHeader(name, headers[name]); - } - - return write(chunk); - }; - - // Override the response writer to parse the response to determine if it needs - // to be rewritten. - res.write = (chunk: string) => { - try { - // If there is no response, forward it, or if we peek at the first - // character and it's not an array opening, then skip it. - if (chunk.length <= 0 || chunk[0] !== "[") { - return flush(chunk); - } - - // Parse the responses, if it's not an array, then skip it. - const responses: object[] | any = JSON.parse(chunk); - if (!Array.isArray(responses) || responses.length === 0) { - return flush(chunk); - } - - // If the length of responses do not equal the length of id's collected, - // then skip it. - if (responses.length !== ids.length) { - return flush(chunk); - } - - // For each of the responses, zip up their id's into the objects, and - // string concat them together to ensure we get the right request. - const gqlResponse = responses.reduce((body: object[], payload, idx) => { - const id = ids[idx]; - body.push({ id, payload }); - return body; - }, []); - - const response = JSON.stringify(gqlResponse); - - return flush(response, { - "Content-Type": "application/json", - "Content-Length": Buffer.byteLength(response, "utf8").toString(), - }); - } catch (err) { - logger.error({ err }, "could not parse chunk as JSON"); - return flush(chunk); - } - }; - - return res; -} - -export const graphqlBatchMiddleware = ( - graphqlRequestHandler: RequestHandler -): RequestHandler => (req: Request, res, next) => - graphqlRequestHandler(req, wrapResponse(req, res), next); diff --git a/src/core/server/app/middleware/graphql/graphqlMiddleware.ts b/src/core/server/app/middleware/graphql/graphqlMiddleware.ts new file mode 100644 index 000000000..e0ecba4d3 --- /dev/null +++ b/src/core/server/app/middleware/graphql/graphqlMiddleware.ts @@ -0,0 +1,85 @@ +import { GraphQLExtension, GraphQLOptions } from "apollo-server-express"; +import { Handler } from "express"; +import { FieldDefinitionNode, GraphQLError, ValidationContext } from "graphql"; + +// TODO: when https://github.com/apollographql/apollo-server/pull/1907 is merged, update this import path +import { + ExpressGraphQLOptionsFunction, + graphqlExpress, +} from "apollo-server-express/dist/expressApollo"; + +import { Omit } from "coral-common/types"; +import { Config } from "coral-server/config"; +import { + ErrorWrappingExtension, + LoggerExtension, + MetricsExtension, +} from "coral-server/graph/common/extensions"; +import { Metrics } from "coral-server/services/metrics"; + +// Sourced from: https://github.com/apollographql/apollo-server/blob/958846887598491fadea57b3f9373d129300f250/packages/apollo-server-core/src/ApolloServer.ts#L46-L57 +const NoIntrospection = (context: ValidationContext) => ({ + Field(node: FieldDefinitionNode) { + if (node.name.value === "__schema" || node.name.value === "__type") { + context.reportError( + new GraphQLError( + "GraphQL introspection is not allowed in production, but the query contained __schema or __type.", + [node] + ) + ); + } + }, +}); + +/** + * graphqlMiddleware wraps the GraphQL middleware server with some custom + * extension management. + * + * @param config application configuration + * @param requestOptions options to pass to the graphql server + */ +const graphqlMiddleware = ( + config: Config, + requestOptions: ExpressGraphQLOptionsFunction, + metrics?: Metrics +): Handler => { + const extensions: Array<() => GraphQLExtension> = [ + () => new ErrorWrappingExtension(), + () => new LoggerExtension(), + ]; + + // Add the metrics extension if provided. + if (metrics) { + extensions.push( + () => + // Pass the metrics to the extension so it can increment. + new MetricsExtension(metrics) + ); + } + + // Create a new baseOptions that will be merged into the new options. + const baseOptions: Omit = { + // Disable the debug mode, as we already add in our logging function. + debug: false, + extensions, + }; + + if (config.get("env") === "production" && !config.get("enable_graphiql")) { + // Disable introspection in production. + baseOptions.validationRules = [NoIntrospection]; + } + + // Generate the actual middleware. + return graphqlExpress(async (req, res) => { + // Resolve the options for the GraphQL middleware. + const options = await requestOptions(req, res); + + // Provide the options. + return { + ...options, + ...baseOptions, + }; + }); +}; + +export default graphqlMiddleware; diff --git a/src/core/server/app/middleware/graphql/index.ts b/src/core/server/app/middleware/graphql/index.ts index 7ccc61671..7aacc35c9 100644 --- a/src/core/server/app/middleware/graphql/index.ts +++ b/src/core/server/app/middleware/graphql/index.ts @@ -1,85 +1,4 @@ -import { GraphQLExtension, GraphQLOptions } from "apollo-server-express"; -import { Handler } from "express"; -import { FieldDefinitionNode, GraphQLError, ValidationContext } from "graphql"; - -// TODO: when https://github.com/apollographql/apollo-server/pull/1907 is merged, update this import path -import { - ExpressGraphQLOptionsFunction, - graphqlExpress, -} from "apollo-server-express/dist/expressApollo"; - -import { Omit } from "coral-common/types"; -import { Config } from "coral-server/config"; -import { - ErrorWrappingExtension, - LoggerExtension, - MetricsExtension, -} from "coral-server/graph/common/extensions"; -import { Metrics } from "coral-server/services/metrics"; - -export * from "./batch"; - -// Sourced from: https://github.com/apollographql/apollo-server/blob/958846887598491fadea57b3f9373d129300f250/packages/apollo-server-core/src/ApolloServer.ts#L46-L57 -const NoIntrospection = (context: ValidationContext) => ({ - Field(node: FieldDefinitionNode) { - if (node.name.value === "__schema" || node.name.value === "__type") { - context.reportError( - new GraphQLError( - "GraphQL introspection is not allowed in production, but the query contained __schema or __type.", - [node] - ) - ); - } - }, -}); - -/** - * graphqlMiddleware wraps the GraphQL middleware server with some custom - * extension management. - * - * @param config application configuration - * @param requestOptions options to pass to the graphql server - */ -export const graphqlMiddleware = ( - config: Config, - requestOptions: ExpressGraphQLOptionsFunction, - metrics?: Metrics -): Handler => { - const extensions: Array<() => GraphQLExtension> = [ - () => new ErrorWrappingExtension(), - () => new LoggerExtension(), - ]; - - // Add the metrics extension if provided. - if (metrics) { - extensions.push( - () => - // Pass the metrics to the extension so it can increment. - new MetricsExtension(metrics) - ); - } - - // Create a new baseOptions that will be merged into the new options. - const baseOptions: Omit = { - // Disable the debug mode, as we already add in our logging function. - debug: false, - extensions, - }; - - if (config.get("env") === "production" && !config.get("enable_graphiql")) { - // Disable introspection in production. - baseOptions.validationRules = [NoIntrospection]; - } - - // Generate the actual middleware. - return graphqlExpress(async (req, res) => { - // Resolve the options for the GraphQL middleware. - const options = await requestOptions(req, res); - - // Provide the options. - return { - ...options, - ...baseOptions, - }; - }); -}; +export { default as graphqlMiddleware } from "./graphqlMiddleware"; +export { + default as persistedQueryMiddleware, +} from "./persistedQueryMiddleware"; diff --git a/src/core/server/app/middleware/graphql/persistedQueryMiddleware.ts b/src/core/server/app/middleware/graphql/persistedQueryMiddleware.ts new file mode 100644 index 000000000..85dac9860 --- /dev/null +++ b/src/core/server/app/middleware/graphql/persistedQueryMiddleware.ts @@ -0,0 +1,53 @@ +import { AppOptions } from "coral-server/app"; +import { RawQueryNotAuthorized } from "coral-server/errors"; +import { getPersistedQuery } from "coral-server/graph/tenant/persisted"; +import { GQLUSER_ROLE } from "coral-server/graph/tenant/schema/__generated__/types"; +import { RequestHandler } from "coral-server/types/express"; + +type PersistedQueryMiddlewareOptions = Pick< + AppOptions, + "config" | "persistedQueryCache" | "persistedQueriesRequired" +>; + +const persistedQueryMiddleware = ( + options: PersistedQueryMiddlewareOptions +): RequestHandler => async (req, res, next) => { + try { + if (!req.coral) { + throw new Error("coral was not set"); + } + + // Pull out some useful properties from Coral. + const { tenant } = req.coral; + if (!tenant) { + throw new Error("tenant was not set"); + } + + // Handle the payload if it is a persisted query. + const body = req.method === "GET" ? req.query : req.body; + const query = await getPersistedQuery(options.persistedQueryCache, body); + if (!query) { + // Check to see if this is from an ADMIN token which is allowed to run + // un-persisted queries. + if ( + options.persistedQueriesRequired && + (!req.user || req.user.role !== GQLUSER_ROLE.ADMIN) + ) { + throw new RawQueryNotAuthorized( + tenant.id, + req.user ? req.user.id : null + ); + } + } else { + // The query was found for this operation, replace the query with the one + // provided. + body.query = query.query; + } + + return next(); + } catch (err) { + return next(err); + } +}; + +export default persistedQueryMiddleware; diff --git a/src/core/server/app/router/api/index.ts b/src/core/server/app/router/api/index.ts index 96ec8d626..bdf778c9e 100644 --- a/src/core/server/app/router/api/index.ts +++ b/src/core/server/app/router/api/index.ts @@ -9,6 +9,7 @@ import { versionHandler, } from "coral-server/app/handlers"; import { JSONErrorHandler } from "coral-server/app/middleware/error"; +import { persistedQueryMiddleware } from "coral-server/app/middleware/graphql"; import { jsonMiddleware } from "coral-server/app/middleware/json"; import { errorLogger } from "coral-server/app/middleware/logging"; import { notFoundMiddleware } from "coral-server/app/middleware/notFound"; @@ -60,6 +61,7 @@ export function createAPIRouter(app: AppOptions, options: RouterOptions) { "/graphql", authenticate(options.passport), jsonMiddleware, + persistedQueryMiddleware(app), graphQLHandler(app) ); diff --git a/src/core/server/errors/index.ts b/src/core/server/errors/index.ts index b49cbd74f..2ad268272 100644 --- a/src/core/server/errors/index.ts +++ b/src/core/server/errors/index.ts @@ -50,6 +50,11 @@ export interface CoralErrorExtensions { } export interface CoralErrorContext { + /** + * tenantID is the ID of the tenant that this Error is associated with. + */ + tenantID?: string; + /** * pub stores information that is used by the translation framework * to provide context to the error being emitted to pass publicly. Sensitive @@ -165,8 +170,8 @@ export class CoralError extends VError { this.status = status; // Capture the context for the error. - const { pub = {}, pvt = {} } = context; - this.context = { pub, pvt }; + const { pub = {}, pvt = {}, tenantID } = context; + this.context = { tenantID, pub, pvt }; // Capture the extension parameters. this.id = id; @@ -679,3 +684,21 @@ export class PasswordIncorrect extends CoralError { }); } } + +export class PersistedQueryNotFound extends CoralError { + constructor(id: string) { + super({ + code: ERROR_CODES.PERSISTED_QUERY_NOT_FOUND, + context: { pub: { id } }, + }); + } +} + +export class RawQueryNotAuthorized extends CoralError { + constructor(tenantID: string, userID: string | null) { + super({ + code: ERROR_CODES.RAW_QUERY_NOT_AUTHORIZED, + context: { tenantID, pvt: { userID } }, + }); + } +} diff --git a/src/core/server/errors/translations.ts b/src/core/server/errors/translations.ts index 2780d36ec..d7d63a926 100644 --- a/src/core/server/errors/translations.ts +++ b/src/core/server/errors/translations.ts @@ -51,4 +51,6 @@ export const ERROR_TRANSLATIONS: Record = { LIVE_UPDATES_DISABLED: "error-liveUpdatesDisabled", PASSWORD_INCORRECT: "error-passwordIncorrect", USERNAME_UPDATED_WITHIN_WINDOW: "error-usernameAlreadyUpdated", + PERSISTED_QUERY_NOT_FOUND: "error-persistedQueryNotFound", + RAW_QUERY_NOT_AUTHORIZED: "error-rawQueryNotAuthorized", }; diff --git a/src/core/server/graph/tenant/persisted/index.ts b/src/core/server/graph/tenant/persisted/index.ts new file mode 100644 index 000000000..42b831430 --- /dev/null +++ b/src/core/server/graph/tenant/persisted/index.ts @@ -0,0 +1,2 @@ +export * from "./loader"; +export * from "./mapper"; diff --git a/src/core/server/graph/tenant/persisted/loader.ts b/src/core/server/graph/tenant/persisted/loader.ts new file mode 100644 index 000000000..58bfb2bd9 --- /dev/null +++ b/src/core/server/graph/tenant/persisted/loader.ts @@ -0,0 +1,56 @@ +import fs from "fs-extra"; +import { parse } from "graphql"; +import path from "path"; + +import { version } from "coral-common/version"; +import { getOperationMetadata } from "coral-server/graph/common/extensions/helpers"; +import { PersistedQuery } from "coral-server/models/queries"; + +export function loadPersistedQueries() { + // Load the files in the persisted queries folder. + const dir = path.join(__dirname, "__generated__"); + const files = fs.readdirSync(dir); + + // Load each of the persisted queries. + const queries: PersistedQuery[] = []; + for (const filePath of files) { + // Parse the bundle name from the file. + const bundle = filePath.split(".")[0]; + + // Load the queries from this file. + const fullFilePath = path.join(dir, filePath); + const persistedQueries: Record = fs.readJSONSync( + fullFilePath + ); + + // Go over each of the persisted queries and collect the ID and query to + // merge in. + for (const id in persistedQueries) { + if (!persistedQueries.hasOwnProperty(id)) { + continue; + } + + // Grab the actual query out of the file. + const query = persistedQueries[id]; + + // Parse the file to extract the GraphQL Operation Name. + const { operation, operationName } = getOperationMetadata(parse(query)); + if (!operation || !operationName) { + throw new Error( + `operation in ${fullFilePath} with ID ${id} does not have valid operation metadata` + ); + } + + queries.push({ + id, + operation, + operationName, + query, + bundle, + version, + }); + } + } + + return queries; +} diff --git a/src/core/server/graph/tenant/persisted/mapper.ts b/src/core/server/graph/tenant/persisted/mapper.ts new file mode 100644 index 000000000..8310863f1 --- /dev/null +++ b/src/core/server/graph/tenant/persisted/mapper.ts @@ -0,0 +1,41 @@ +import { PersistedQueryNotFound } from "coral-server/errors"; +import { PersistedQuery } from "coral-server/models/queries"; + +interface Payload { + id?: string; + query?: string; +} + +interface Cache { + get(id: string): Promise; +} + +/** + * getPersistedQuery will try to get the persisted query referenced by the + * payload and return it if one exists. If a persisted query is referenced, but + * non is available, it will throw an error. + * + * @param cache the cache to pull the query from + * @param payload the payload that references the query that should be read + */ +export async function getPersistedQuery( + cache: Cache, + payload?: Readonly +) { + if ( + !payload || + !payload.id || + // Persisted queries can either have a query set to `PERSISTED_QUERY` or is + // empty. + !(payload.query === "PERSISTED_QUERY" || payload.query === "") + ) { + return null; + } + + const query = await cache.get(payload.id); + if (!query) { + throw new PersistedQueryNotFound(payload.id); + } + + return query; +} diff --git a/src/core/server/graph/tenant/subscriptions/server.ts b/src/core/server/graph/tenant/subscriptions/server.ts index 35d32d12a..e48c8b396 100644 --- a/src/core/server/graph/tenant/subscriptions/server.ts +++ b/src/core/server/graph/tenant/subscriptions/server.ts @@ -9,6 +9,7 @@ import http, { IncomingMessage } from "http"; import { ConnectionContext, ExecutionParams, + OperationMessage, OperationMessagePayload, SubscriptionServer, } from "subscriptions-transport-ws"; @@ -25,6 +26,7 @@ import { CoralError, InternalError, LiveUpdatesDisabled, + RawQueryNotAuthorized, TenantNotFoundError, } from "coral-server/errors"; import { @@ -33,6 +35,8 @@ import { logQuery, } from "coral-server/graph/common/extensions"; import { getOperationMetadata } from "coral-server/graph/common/extensions/helpers"; +import { getPersistedQuery } from "coral-server/graph/tenant/persisted"; +import { GQLUSER_ROLE } from "coral-server/graph/tenant/schema/__generated__/types"; import logger from "coral-server/logger"; import { userIsStaff } from "coral-server/models/user/helpers"; import { extractTokenFromRequest } from "coral-server/services/jwt"; @@ -205,13 +209,41 @@ export function formatResponse({ metrics }: FormatResponseOptions) { }; } -export type OnOperationOptions = FormatResponseOptions; +export type OnOperationOptions = FormatResponseOptions & + Pick; export function onOperation(options: OnOperationOptions) { - return (message: any, params: ExecutionParams) => { + return async ( + message: OperationMessage, + params: ExecutionParams + ) => { // Attach the response formatter. params.formatResponse = formatResponse(options); + // Handle the payload if it is a persisted query. + const query = await getPersistedQuery( + options.persistedQueryCache, + message.payload + ); + if (!query) { + // Check to see if this is from an ADMIN token which is allowed to run + // un-persisted queries. + if ( + options.persistedQueriesRequired && + (!params.context.user || + params.context.user.role !== GQLUSER_ROLE.ADMIN) + ) { + throw new RawQueryNotAuthorized( + params.context.tenant.id, + params.context.user ? params.context.user.id : null + ); + } + } else { + // The query was found for this operation, replace the query with the one + // provided. + params.query = query.query; + } + return params; }; } diff --git a/src/core/server/index.ts b/src/core/server/index.ts index dd0d77ba3..7a64016be 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -19,6 +19,7 @@ import { createPubSubClient } from "coral-server/graph/common/subscriptions/pubs import getTenantSchema from "coral-server/graph/tenant/schema"; import { createSubscriptionServer } from "coral-server/graph/tenant/subscriptions/server"; import logger from "coral-server/logger"; +import { PersistedQueryCache } from "coral-server/models/queries"; import { createQueue, TaskQueue } from "coral-server/queue"; import { I18n } from "coral-server/services/i18n"; import { createJWTSigningConfig } from "coral-server/services/jwt"; @@ -260,6 +261,20 @@ class Server { // Webpack Dev Server. const disableClientRoutes = this.config.get("disable_client_routes"); + // Load and upsert the persisted queries. + const persistedQueryCache = new PersistedQueryCache({ mongo: this.mongo }); + + // Prime the queries in the database. + await persistedQueryCache.prime(); + + logger.info( + { queries: persistedQueryCache.size }, + "loaded persisted queries" + ); + if (persistedQueryCache.size === 0) { + logger.warn("no persisted queries loaded, did you run `npm run build`?"); + } + const options: AppOptions = { parent, pubsub: this.pubsub, @@ -273,6 +288,10 @@ class Server { mailerQueue: this.tasks.mailer, scraperQueue: this.tasks.scraper, disableClientRoutes, + persistedQueryCache, + persistedQueriesRequired: + this.config.get("env") === "production" && + !this.config.get("enable_graphiql"), }; // Only enable the metrics server if concurrency is set to 1. diff --git a/src/core/server/locales/en-US/errors.ftl b/src/core/server/locales/en-US/errors.ftl index ec27f26c0..5acc743a8 100644 --- a/src/core/server/locales/en-US/errors.ftl +++ b/src/core/server/locales/en-US/errors.ftl @@ -54,4 +54,6 @@ error-rateLimitExceeded = Rate limit exceeded. error-inviteTokenExpired = Invite link has expired. error-inviteRequiresEmailAddresses = Please add an email address to send invitations. error-passwordIncorrect = Password provided was incorrect. -error-usernameAlreadyUpdated = You may only change your username once every { framework-timeago-time }. \ No newline at end of file +error-usernameAlreadyUpdated = You may only change your username once every { framework-timeago-time }. +error-persistedQueryNotFound = The persisted query with ID { $id } was not found. +error-rawQueryNotAuthorized = You are not authorized to execute this query. diff --git a/src/core/server/models/queries/cache.ts b/src/core/server/models/queries/cache.ts new file mode 100644 index 000000000..3855d3a23 --- /dev/null +++ b/src/core/server/models/queries/cache.ts @@ -0,0 +1,112 @@ +import DataLoader from "dataloader"; +import LRU from "lru-cache"; +import { Db } from "mongodb"; + +import { loadPersistedQueries } from "coral-server/graph/tenant/persisted"; +import logger from "coral-server/logger"; + +import { getQueries, PersistedQuery, primeQueries } from "./queries"; + +interface PersistedQueryCacheOptions { + mongo: Db; +} + +/** + * PersistedQueryCache abstracts the persisted query management. + */ +export class PersistedQueryCache { + private mongo: Db; + private queries: Map; + private cache: LRU; + private loader: DataLoader; + + constructor(options: PersistedQueryCacheOptions) { + const queries = loadPersistedQueries(); + + this.mongo = options.mongo; + this.loader = new DataLoader( + (ids: string[]) => getQueries(this.mongo, ids), + { + // Turn off caching as we're using the LRU cache here instead. + cache: false, + } + ); + this.queries = new Map(); + this.cache = new LRU({ + // We'll only retain the amount of queries we have right now so we could + // possibly hold the previous version in memory if need be. Ideally, we'll + // always have the keys we need in memory. + max: queries.length, + dispose: (id, query) => { + logger.warn( + { queryID: id, queryVersion: query.version }, + "cache full, dropping query from cache" + ); + }, + }); + + // Insert all the queries into the local query cache. + for (const query of queries) { + this.queries.set(query.id, query); + } + } + + public get size() { + return this.queries.size + this.cache.length; + } + + /** + * prime will load the local queries into the database so every time that the + * server starts, the queries will be available to other instances. + */ + public async prime() { + if (this.queries.size === 0) { + return; + } + + const queries: PersistedQuery[] = []; + for (const query of this.queries.values()) { + queries.push(query); + } + + logger.debug({ queries: queries.length }, "priming queries"); + + await primeQueries(this.mongo, queries); + } + + /** + * get will retrieve a given PersistedQuery by ID. + * + * @param id the ID of the persisted query to load + */ + public async get(id: string) { + // Try to get the query from the local query cache. + let query: PersistedQuery | null | undefined = this.queries.get(id); + if (query) { + return query; + } + + // Try to get the query from the remote cache. + query = this.cache.get(id); + if (query) { + return query; + } + + // Try to get the query from the loader. + query = await this.loader.load(id); + if (query) { + logger.warn( + { queryID: id, queryVersion: query.version }, + "query did not exist in cache, retrieved from MongoDB" + ); + + // Cache this query in the memory cache. + this.cache.set(query.id, query); + return query; + } + + logger.warn({ queryID: id }, "query did not exist in cache or MongoDB"); + + return null; + } +} diff --git a/src/core/server/models/queries/index.ts b/src/core/server/models/queries/index.ts new file mode 100644 index 000000000..7723b008a --- /dev/null +++ b/src/core/server/models/queries/index.ts @@ -0,0 +1,2 @@ +export * from "./cache"; +export * from "./queries"; diff --git a/src/core/server/models/queries/queries.ts b/src/core/server/models/queries/queries.ts new file mode 100644 index 000000000..9adc229e8 --- /dev/null +++ b/src/core/server/models/queries/queries.ts @@ -0,0 +1,48 @@ +import { Db } from "mongodb"; + +import { createIndexFactory } from "../helpers/indexing"; + +function collection(mongo: Db) { + return mongo.collection>("queries"); +} + +export interface PersistedQuery { + id: string; + operation: string; + operationName: string; + query: string; + bundle: string; + version: string; +} + +export async function createQueriesIndexes(mongo: Db) { + const createIndex = createIndexFactory(collection(mongo)); + + // UNIQUE { id } + await createIndex({ id: 1 }, { unique: true }); +} + +export async function primeQueries(mongo: Db, queries: PersistedQuery[]) { + // Setup persisting these queries. + const bulk = collection(mongo).initializeUnorderedBulkOp({}); + + // Upsert each query. + for (const query of queries) { + const { id } = query; + + // Add to the bulk operation for MongoDB. + bulk + .find({ id }) + .upsert() + .replaceOne(query); + } + + // Execute the bulk operations. + await bulk.execute(); +} + +export async function getQueries(mongo: Db, ids: string[]) { + const cursor = await collection(mongo).find({ id: { $in: ids } }); + const queries = await cursor.toArray(); + return ids.map(id => queries.find(query => query.id === id) || null); +} diff --git a/src/core/server/services/mongodb/indexes.ts b/src/core/server/services/mongodb/indexes.ts index 6aa4908f8..98fe3f99b 100644 --- a/src/core/server/services/mongodb/indexes.ts +++ b/src/core/server/services/mongodb/indexes.ts @@ -5,6 +5,7 @@ import { createCommentActionIndexes } from "coral-server/models/action/comment"; import { createCommentModerationActionIndexes } from "coral-server/models/action/moderation/comment"; import { createCommentIndexes } from "coral-server/models/comment"; import { createInviteIndexes } from "coral-server/models/invite"; +import { createQueriesIndexes } from "coral-server/models/queries"; import { createStoryCountIndexes, createStoryIndexes, @@ -23,6 +24,7 @@ const indexes: Array<[string, IndexCreationFunction]> = [ ["stories", createStoryCountIndexes], ["commentActions", createCommentActionIndexes], ["commentModerationActions", createCommentModerationActionIndexes], + ["queries", createQueriesIndexes], ]; /** diff --git a/src/types/react-relay-network-modern.d.ts b/src/types/react-relay-network-modern.d.ts index a602259aa..afe4e9312 100644 --- a/src/types/react-relay-network-modern.d.ts +++ b/src/types/react-relay-network-modern.d.ts @@ -142,7 +142,7 @@ declare module "react-relay-network-modern/es" { export type Requests = RelayRequest[]; - export default class RelayRequestBatch { + export class RelayRequestBatch { public fetchOpts: Partial; public requests: Requests; diff --git a/tsconfig.json b/tsconfig.json index 6b4a4e4e3..48c130889 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "ES2018", "module": "commonjs", "allowJs": true, "noEmit": true,