From fe2e95622f69ca8bd5c41ac0ea550f98ebcd4a91 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Wed, 19 Jun 2019 17:19:47 -0700 Subject: [PATCH] GraphQL Support (#5674) * GraphQL boilerplate * Create GraphQL schema without using gql * Introducing loaders * Generic create mutation * create mutation is now working for any data type * Create mutation for each parse class - partial * Adding more data types to the class * Get parse class query * Generic get query * Generic delete mutation * Parse class delete mutation * Parse class find mutation * Generic update mutation * Parse class update mutation * Fixing initialization problems * Installing node-fetch again * Basic implementation for Pointer * Constructor tests * API tests boilerplate * _getGraphQLOptions * applyGraphQL tests * GraphQL API initial tests * applyPlayground tests * createSubscriptions tests * ParseGrapjQLSchema tests file * ParseGraphQLSchema tests * TypeValidationError * TypeValidationError * parseStringValue test * parseIntValue tests * parseBooleanValue tests * parseDateValue tests * parseValue tests * parseListValues tests * parseObjectFields tests * Default types tests * Get tests * First permission test at generic Get operation * Fixing prepare data * ApolloClient does not work well with different queries runnning in paralell with different headers * ApolloClient does not work well with different queries runnning in paralell with different headers * User 3 tests * User 3 tests * Get level permission tests * Get User specific tests * Get now support keys argument * Get now supports include argument * Get now supports read preferences * Adding tests for read preference enum type * Find basic test * Find permissions test * Find where argument test * Order, skip and limit tests * Error handler * Find now supports count * Test for FindResult type * Improving find count * Find max limit test * Find now supports keys, include and includeAll * Find now supports read preferences * Basic Create test * Generic create mutation tests * Basic update test * UpdateResult object type test * Update level permissions tests * Error handler for default mutations * Delete mutation basic test * Delete mutation level permission tests * Test for string * String test * Date test * Pointer test * Relation tests * Changing objects mutations location * Changing objects queries location * Create file mutation * Test for file fields * Test for null values * Changing parse classes operations location * Objects mutations refactoring * Class specific create object mutation now working * Update class specific mutation now working * Specific class delete mutation now working * Get class specific mutation now working * Find class specific query now working without where and sort * Find query for custom classes working with where partially * Almost all data types working for specfic class find where * Now only missing relation, geopoint, file and ACL * Additional tests with Parse classes queries and mutations * Now only missing relation, geopoint, file and ACL * Files * Fiels are now working * Excluding missing order test temporarly * Refactoring dates * Refactoring files * Default types review * Refeactoring object queries * Refactoring class scalar type * Refactoring class types * Geo queries are now working * Fixing centerSphere * Allow sort on class specific queries * Supporting bytes * ACL constraint * Temporarly removing xit tests * Fixing some tests because of schema cache * Removing session token from users * Parse.User queries and mutations * Remove test using fit * Fixing include test that was failing because of schema cache * Fixing count test for postgres. Postgres does not count with where={} (legacy problem). We should solve it later * Fix null values test for postgres. It is evaluating null as undefined (legacy problem) and we should fix is later. * Fixing schema change test that was failing because of schema cache * Add GraphQL File type parseLiteral tests * Refeactoring users * Including sign up mutation * Fix failing test * Improve default GraphQL types tests coverage * Including some tests for data types * Including additional pointer test: * Fixing some tests * more data type tests * Include Bytes and Polygon data types tests * Polygons test * Merging other tests * Fixing some postgres tests --- package-lock.json | 872 ++- package.json | 16 + spec/.eslintrc.json | 3 +- spec/ParseGraphQLSchema.spec.js | 65 + spec/ParseGraphQLServer.spec.js | 4950 +++++++++++++++++ spec/defaultGraphQLTypes.spec.js | 715 +++ src/GraphQL/ParseGraphQLSchema.js | 117 + src/GraphQL/ParseGraphQLServer.js | 110 + .../loaders/defaultGraphQLMutations.js | 11 + src/GraphQL/loaders/defaultGraphQLQueries.js | 17 + src/GraphQL/loaders/defaultGraphQLTypes.js | 1125 ++++ src/GraphQL/loaders/filesMutations.js | 93 + src/GraphQL/loaders/objectsMutations.js | 148 + src/GraphQL/loaders/objectsQueries.js | 367 ++ src/GraphQL/loaders/parseClassMutations.js | 121 + src/GraphQL/loaders/parseClassQueries.js | 102 + src/GraphQL/loaders/parseClassTypes.js | 557 ++ src/GraphQL/loaders/usersMutations.js | 110 + src/GraphQL/loaders/usersQueries.js | 36 + src/index.js | 2 + 20 files changed, 9532 insertions(+), 5 deletions(-) create mode 100644 spec/ParseGraphQLSchema.spec.js create mode 100644 spec/ParseGraphQLServer.spec.js create mode 100644 spec/defaultGraphQLTypes.spec.js create mode 100644 src/GraphQL/ParseGraphQLSchema.js create mode 100644 src/GraphQL/ParseGraphQLServer.js create mode 100644 src/GraphQL/loaders/defaultGraphQLMutations.js create mode 100644 src/GraphQL/loaders/defaultGraphQLQueries.js create mode 100644 src/GraphQL/loaders/defaultGraphQLTypes.js create mode 100644 src/GraphQL/loaders/filesMutations.js create mode 100644 src/GraphQL/loaders/objectsMutations.js create mode 100644 src/GraphQL/loaders/objectsQueries.js create mode 100644 src/GraphQL/loaders/parseClassMutations.js create mode 100644 src/GraphQL/loaders/parseClassQueries.js create mode 100644 src/GraphQL/loaders/parseClassTypes.js create mode 100644 src/GraphQL/loaders/usersMutations.js create mode 100644 src/GraphQL/loaders/usersQueries.js diff --git a/package-lock.json b/package-lock.json index c25b540d..57414a5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,19 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@apollographql/apollo-tools": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@apollographql/apollo-tools/-/apollo-tools-0.3.6.tgz", + "integrity": "sha512-j59jXpFACU1WY5+O2T7qg5OgIPIiOoynO+UlOsDWiazmqc1dOe597VlIraj1w+XClYrerx6NhhLY2yHXECYFVA==", + "requires": { + "apollo-env": "0.5.0" + } + }, + "@apollographql/graphql-playground-html": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.6.tgz", + "integrity": "sha512-lqK94b+caNtmKFs5oUVXlSpN3sm5IXZ+KfhMxOtr0LR2SqErzkoJilitjDvJ1WbjHlxLI7WtCjRmOLdOGJqtMQ==" + }, "@babel/cli": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.4.4.tgz", @@ -1331,6 +1344,60 @@ "mailgun-js": "0.18.0" } }, + "@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "requires": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + }, "@samverschueren/stream-to-observable": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz", @@ -1391,6 +1458,73 @@ } } }, + "@types/accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==", + "requires": { + "@types/node": "*" + } + }, + "@types/body-parser": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz", + "integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.32", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", + "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", + "requires": { + "@types/node": "*" + } + }, + "@types/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-GmK8AKu8i+s+EChK/uZ5IbrXPcPaQKWaNSGevDT/7o3gFObwSUQwqb1jMqxuo+YPvj0ckGzINI+EO7EHcmJjKg==", + "requires": { + "@types/express": "*" + } + }, + "@types/events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", + "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==" + }, + "@types/express": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.16.1.tgz", + "integrity": "sha512-V0clmJow23WeyblmACoxbHBu2JKlE5TiIme6Lem14FnPW9gsttyHtk6wq7njcdIWH1njAaFgR8gW09lgY98gQg==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.4.tgz", + "integrity": "sha512-x/8h6FHm14rPWnW2HP5likD/rsqJ3t/77OWx2PLxym0hXbeBWQmcPyHmwX+CtCQpjIfgrUdEoDFcLPwPZWiqzQ==", + "requires": { + "@types/node": "*", + "@types/range-parser": "*" + } + }, + "@types/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.0.tgz", + "integrity": "sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q==" + }, + "@types/mime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", + "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==" + }, "@types/node": { "version": "8.10.45", "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.45.tgz", @@ -1402,6 +1536,35 @@ "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", "dev": true }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + }, + "@types/serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==", + "requires": { + "@types/express-serve-static-core": "*", + "@types/mime": "*" + } + }, + "@types/ws": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.1.tgz", + "integrity": "sha512-EzH8k1gyZ4xih/MaZTXwT2xOkPiIMSrhQ9b8wrlX88L0T02eYsddatQlwVFlEPyEqV0ChpdpNnE51QPH6NVT4Q==", + "requires": { + "@types/events": "*", + "@types/node": "*" + } + }, + "@types/zen-observable": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.0.tgz", + "integrity": "sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg==", + "dev": true + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -1572,6 +1735,346 @@ "verror": "^1.10.0" } }, + "apollo-cache": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/apollo-cache/-/apollo-cache-1.2.1.tgz", + "integrity": "sha512-nzFmep/oKlbzUuDyz6fS6aYhRmfpcHWqNkkA9Bbxwk18RD6LXC4eZkuE0gXRX0IibVBHNjYVK+Szi0Yied4SpQ==", + "dev": true, + "requires": { + "apollo-utilities": "^1.2.1", + "tslib": "^1.9.3" + } + }, + "apollo-cache-control": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/apollo-cache-control/-/apollo-cache-control-0.5.2.tgz", + "integrity": "sha512-uehXDUrd3Qim+nzxqqN7XT1YTbNSyumW3/FY5BxbKZTI8d4oPG4eyVQKqaggooSjswKQnOoIQVes3+qg9tGAkw==", + "requires": { + "apollo-server-env": "2.2.0", + "graphql-extensions": "0.5.4" + }, + "dependencies": { + "graphql-extensions": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/graphql-extensions/-/graphql-extensions-0.5.4.tgz", + "integrity": "sha512-qLThJGVMqcItE7GDf/xX/E40m/aeqFheEKiR5bfra4q5eHxQKGjnIc20P9CVqjOn9I0FkEiU9ypOobfmIf7t6g==", + "requires": { + "@apollographql/apollo-tools": "^0.3.3" + } + } + } + }, + "apollo-cache-inmemory": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/apollo-cache-inmemory/-/apollo-cache-inmemory-1.5.1.tgz", + "integrity": "sha512-D3bdpPmWfaKQkWy8lfwUg+K8OBITo3sx0BHLs1B/9vIdOIZ7JNCKq3EUcAgAfInomJUdN0QG1yOfi8M8hxkN1g==", + "dev": true, + "requires": { + "apollo-cache": "^1.2.1", + "apollo-utilities": "^1.2.1", + "optimism": "^0.6.9", + "ts-invariant": "^0.2.1", + "tslib": "^1.9.3" + }, + "dependencies": { + "ts-invariant": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.2.1.tgz", + "integrity": "sha512-Z/JSxzVmhTo50I+LKagEISFJW3pvPCqsMWLamCTX8Kr3N5aMrnGOqcflbe5hLUzwjvgPfnLzQtHZv0yWQ+FIHg==", + "dev": true, + "requires": { + "tslib": "^1.9.3" + } + } + } + }, + "apollo-client": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/apollo-client/-/apollo-client-2.5.1.tgz", + "integrity": "sha512-MNcQKiqLHdGmNJ0rZ0NXaHrToXapJgS/5kPk0FygXt+/FmDCdzqcujI7OPxEC6e9Yw5S/8dIvOXcRNuOMElHkA==", + "dev": true, + "requires": { + "@types/zen-observable": "^0.8.0", + "apollo-cache": "1.2.1", + "apollo-link": "^1.0.0", + "apollo-link-dedup": "^1.0.0", + "apollo-utilities": "1.2.1", + "symbol-observable": "^1.0.2", + "ts-invariant": "^0.2.1", + "tslib": "^1.9.3", + "zen-observable": "^0.8.0" + }, + "dependencies": { + "ts-invariant": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.2.1.tgz", + "integrity": "sha512-Z/JSxzVmhTo50I+LKagEISFJW3pvPCqsMWLamCTX8Kr3N5aMrnGOqcflbe5hLUzwjvgPfnLzQtHZv0yWQ+FIHg==", + "dev": true, + "requires": { + "tslib": "^1.9.3" + } + } + } + }, + "apollo-datasource": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/apollo-datasource/-/apollo-datasource-0.3.1.tgz", + "integrity": "sha512-qdEUeonc9pPZvYwXK36h2NZoT7Pddmy0HYOzdV0ON5pcG1YtNmUyyYi83Q60V5wTWjuaCjyJ9hOY6wr0BMvQuA==", + "requires": { + "apollo-server-caching": "0.3.1", + "apollo-server-env": "2.2.0" + } + }, + "apollo-engine-reporting": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/apollo-engine-reporting/-/apollo-engine-reporting-1.0.7.tgz", + "integrity": "sha512-mFsXvd+1/o5jSa9tI2RoXYGcvCLcwwcfLwchjSTxqUd4ViB8RbqYKynzEZ+Omji7PBRM0azioBm43f7PSsQPqA==", + "requires": { + "apollo-engine-reporting-protobuf": "0.2.1", + "apollo-graphql": "^0.1.0", + "apollo-server-core": "2.4.8", + "apollo-server-env": "2.2.0", + "async-retry": "^1.2.1", + "graphql-extensions": "0.5.7" + } + }, + "apollo-engine-reporting-protobuf": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.2.1.tgz", + "integrity": "sha512-5pYR84uWeylRS2OJowtkTczT3bWTwOErWtfnkRKccUi/wZ/AZJBP+D5HKNzM7xoFcz9XvrJyS+wBTz1oBi0Jiw==", + "requires": { + "protobufjs": "^6.8.6" + } + }, + "apollo-env": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/apollo-env/-/apollo-env-0.5.0.tgz", + "integrity": "sha512-yzajZupxouVtSUJiqkjhiQZKnagfwZeHjqRHkgV+rTCNuJkfdcoskSQm7zk5hhcS1JMunuD6INC1l4PHq+o+wQ==", + "requires": { + "core-js": "3.0.0-beta.13", + "node-fetch": "^2.2.0", + "sha.js": "^2.4.11" + }, + "dependencies": { + "core-js": { + "version": "3.0.0-beta.13", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.0.0-beta.13.tgz", + "integrity": "sha512-16Q43c/3LT9NyePUJKL8nRIQgYWjcBhjJSMWg96PVSxoS0PeE0NHitPI3opBrs9MGGHjte1KoEVr9W63YKlTXQ==" + } + } + }, + "apollo-graphql": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/apollo-graphql/-/apollo-graphql-0.1.3.tgz", + "integrity": "sha512-bYgDh71jFfHKO9ioGlxnnoSYgpNp6LRl+/QHTx6tktQEN0Z+AdpkOKFNCHO/pRU/4vSqV5wuIhxhnCecxJQrMA==", + "requires": { + "apollo-env": "0.4.0", + "lodash.sortby": "^4.7.0" + }, + "dependencies": { + "apollo-env": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/apollo-env/-/apollo-env-0.4.0.tgz", + "integrity": "sha512-TZpk59RTbXd8cEqwmI0KHFoRrgBRplvPAP4bbRrX4uDSxXvoiY0Y6tQYUlJ35zi398Hob45mXfrZxeRDzoFMkQ==", + "requires": { + "core-js": "3.0.0-beta.13", + "node-fetch": "^2.2.0", + "sha.js": "^2.4.11" + } + }, + "core-js": { + "version": "3.0.0-beta.13", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.0.0-beta.13.tgz", + "integrity": "sha512-16Q43c/3LT9NyePUJKL8nRIQgYWjcBhjJSMWg96PVSxoS0PeE0NHitPI3opBrs9MGGHjte1KoEVr9W63YKlTXQ==" + } + } + }, + "apollo-link": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.11.tgz", + "integrity": "sha512-PQvRCg13VduLy3X/0L79M6uOpTh5iHdxnxYuo8yL7sJlWybKRJwsv4IcRBJpMFbChOOaHY7Og9wgPo6DLKDKDA==", + "requires": { + "apollo-utilities": "^1.2.1", + "ts-invariant": "^0.3.2", + "tslib": "^1.9.3", + "zen-observable-ts": "^0.8.18" + } + }, + "apollo-link-dedup": { + "version": "1.0.18", + "resolved": "https://registry.npmjs.org/apollo-link-dedup/-/apollo-link-dedup-1.0.18.tgz", + "integrity": "sha512-1rr54wyMTuqUmbWvcXbwduIcaCDcuIgU6MqQ599nAMuTrbSOXthGfoAD8BDTxBGQ9roVlM7ABP0VZVEWRoHWSg==", + "dev": true, + "requires": { + "apollo-link": "^1.2.11", + "tslib": "^1.9.3" + } + }, + "apollo-link-http": { + "version": "1.5.14", + "resolved": "https://registry.npmjs.org/apollo-link-http/-/apollo-link-http-1.5.14.tgz", + "integrity": "sha512-XEoPXmGpxFG3wioovgAlPXIarWaW4oWzt8YzjTYZ87R4R7d1A3wKR/KcvkdMV1m5G7YSAHcNkDLe/8hF2nH6cg==", + "dev": true, + "requires": { + "apollo-link": "^1.2.11", + "apollo-link-http-common": "^0.2.13", + "tslib": "^1.9.3" + } + }, + "apollo-link-http-common": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/apollo-link-http-common/-/apollo-link-http-common-0.2.13.tgz", + "integrity": "sha512-Uyg1ECQpTTA691Fwx5e6Rc/6CPSu4TB4pQRTGIpwZ4l5JDOQ+812Wvi/e3IInmzOZpwx5YrrOfXrtN8BrsDXoA==", + "dev": true, + "requires": { + "apollo-link": "^1.2.11", + "ts-invariant": "^0.3.2", + "tslib": "^1.9.3" + } + }, + "apollo-link-ws": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/apollo-link-ws/-/apollo-link-ws-1.0.17.tgz", + "integrity": "sha512-0PKgahM2BOcUiI3QSJMYXOoUylWKzar5NTZLgMLEW4K/CczOTzC4CTXvKMjh/cx57Jto/U2xzKRy9BEoNfnK5Q==", + "dev": true, + "requires": { + "apollo-link": "^1.2.11", + "tslib": "^1.9.3" + } + }, + "apollo-server-caching": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/apollo-server-caching/-/apollo-server-caching-0.3.1.tgz", + "integrity": "sha512-mfxzikYXbB/OoEms77AGYwRh7FF3Oim5v5XWAL+VL49FrkbZt5lopVa4bABi7Mz8Nt3Htl9EBJN8765s/yh8IA==", + "requires": { + "lru-cache": "^5.0.0" + } + }, + "apollo-server-core": { + "version": "2.4.8", + "resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.4.8.tgz", + "integrity": "sha512-N+5UOzHhMOnHizEiArJtNvEe/cGhSHQyTn5tlU4RJ36FDBJ/WlYZfPbGDMLISSUCJ6t+aP8GLL4Mnudt9d2PDQ==", + "requires": { + "@apollographql/apollo-tools": "^0.3.3", + "@apollographql/graphql-playground-html": "^1.6.6", + "@types/ws": "^6.0.0", + "apollo-cache-control": "0.5.2", + "apollo-datasource": "0.3.1", + "apollo-engine-reporting": "1.0.7", + "apollo-server-caching": "0.3.1", + "apollo-server-env": "2.2.0", + "apollo-server-errors": "2.2.1", + "apollo-server-plugin-base": "0.3.7", + "apollo-tracing": "0.5.2", + "fast-json-stable-stringify": "^2.0.0", + "graphql-extensions": "0.5.7", + "graphql-subscriptions": "^1.0.0", + "graphql-tag": "^2.9.2", + "graphql-tools": "^4.0.0", + "graphql-upload": "^8.0.2", + "sha.js": "^2.4.11", + "subscriptions-transport-ws": "^0.9.11", + "ws": "^6.0.0" + }, + "dependencies": { + "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" + } + } + } + }, + "apollo-server-env": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-2.2.0.tgz", + "integrity": "sha512-wjJiI5nQWPBpNmpiLP389Ezpstp71szS6DHAeTgYLb/ulCw3CTuuA+0/E1bsThVWiQaDeHZE0sE3yI8q2zrYiA==", + "requires": { + "node-fetch": "^2.1.2", + "util.promisify": "^1.0.0" + } + }, + "apollo-server-errors": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/apollo-server-errors/-/apollo-server-errors-2.2.1.tgz", + "integrity": "sha512-wY/YE3iJVMYC+WYIf8QODBjIP4jhI+oc7kiYo9mrz7LdYPKAgxr/he+NteGcqn/0Ea9K5/ZFTGJDbEstSMeP8g==" + }, + "apollo-server-express": { + "version": "2.4.8", + "resolved": "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-2.4.8.tgz", + "integrity": "sha512-i60l32mfVe33jnKDPNYgUKUKu4Al0xEm2HLOSMgtJ9Wbpe/MbOx5X8M5F27fnHYdM+G5XfAErsakAyRGnQJ48Q==", + "requires": { + "@apollographql/graphql-playground-html": "^1.6.6", + "@types/accepts": "^1.3.5", + "@types/body-parser": "1.17.0", + "@types/cors": "^2.8.4", + "@types/express": "4.16.1", + "accepts": "^1.3.5", + "apollo-server-core": "2.4.8", + "body-parser": "^1.18.3", + "cors": "^2.8.4", + "graphql-subscriptions": "^1.0.0", + "graphql-tools": "^4.0.0", + "type-is": "^1.6.16" + } + }, + "apollo-server-plugin-base": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/apollo-server-plugin-base/-/apollo-server-plugin-base-0.3.7.tgz", + "integrity": "sha512-hW1jaLKf9qNOxMTwRq2CSqz3eqXsZuEiCc8/mmEtOciiVBq1GMtxFf19oIYM9HQuPvQU2RWpns1VrYN59L3vbg==" + }, + "apollo-tracing": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/apollo-tracing/-/apollo-tracing-0.5.2.tgz", + "integrity": "sha512-2FdwRvPIq9uuF6OzONroXep6VBGqzHOkP6LlcFQe7SdwxfRP+SD/ycHNSC1acVg2b8d+am9Kzqg2vV54UpOIKA==", + "requires": { + "apollo-server-env": "2.2.0", + "graphql-extensions": "0.5.4" + }, + "dependencies": { + "graphql-extensions": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/graphql-extensions/-/graphql-extensions-0.5.4.tgz", + "integrity": "sha512-qLThJGVMqcItE7GDf/xX/E40m/aeqFheEKiR5bfra4q5eHxQKGjnIc20P9CVqjOn9I0FkEiU9ypOobfmIf7t6g==", + "requires": { + "@apollographql/apollo-tools": "^0.3.3" + } + } + } + }, + "apollo-upload-client": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/apollo-upload-client/-/apollo-upload-client-10.0.0.tgz", + "integrity": "sha512-N0SENiEkZXoY4nl9xxrXFcj/cL0AVkSNQ4aYXSaruCBWE0aKpK6aCe4DBmiEHrK3FAsMxZPEJxBRIWNbsXT8dw==", + "dev": true, + "requires": { + "apollo-link": "^1.2.6", + "apollo-link-http-common": "^0.2.8", + "extract-files": "^5.0.0" + } + }, + "apollo-utilities": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.2.1.tgz", + "integrity": "sha512-Zv8Udp9XTSFiN8oyXOjf6PMHepD4yxxReLsl6dPUy5Ths7jti3nmlBzZUOxuTWRwZn0MoclqL7RQ5UEJN8MAxg==", + "requires": { + "fast-json-stable-stringify": "^2.0.0", + "ts-invariant": "^0.2.1", + "tslib": "^1.9.3" + }, + "dependencies": { + "ts-invariant": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.2.1.tgz", + "integrity": "sha512-Z/JSxzVmhTo50I+LKagEISFJW3pvPCqsMWLamCTX8Kr3N5aMrnGOqcflbe5hLUzwjvgPfnLzQtHZv0yWQ+FIHg==", + "requires": { + "tslib": "^1.9.3" + } + } + } + }, "append-transform": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", @@ -1721,6 +2224,14 @@ "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" }, + "async-retry": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.2.3.tgz", + "integrity": "sha512-tfDb02Th6CE6pJUF2gjW5ZVjsgwlucVXOEQMvEX9JgSJMs9gAX+Nz3xRuJBKuUYjTSYORqvDBORdAQ3LU59g7Q==", + "requires": { + "retry": "0.12.0" + } + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1781,6 +2292,11 @@ "regenerator-runtime": "^0.11.0" } }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -2114,6 +2630,14 @@ "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==" }, + "busboy": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.3.1.tgz", + "integrity": "sha512-y7tTxhGKXcyBxRKAni+awqx8uqaJKrSFSNFSeRG5CsWNdmy2BIK+6VGWEW7TZnIO/533mtMEA4rOevQV815YJw==", + "requires": { + "dicer": "0.3.0" + } + }, "bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", @@ -2685,6 +3209,15 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "cosmiconfig": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.0.tgz", @@ -3009,6 +3542,14 @@ "strip-bom": "^3.0.0" } }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "requires": { + "object-keys": "^1.0.12" + } + }, "define-property": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", @@ -3089,6 +3630,11 @@ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" }, + "deprecated-decorator": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/deprecated-decorator/-/deprecated-decorator-0.1.6.tgz", + "integrity": "sha1-AJZjF7ehL+kvPMgx91g68ym4bDc=" + }, "destroy": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", @@ -3110,6 +3656,14 @@ "kuler": "1.0.x" } }, + "dicer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz", + "integrity": "sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==", + "requires": { + "streamsearch": "0.1.2" + } + }, "dir-glob": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", @@ -3330,6 +3884,29 @@ } } }, + "es-abstract": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", + "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", + "requires": { + "es-to-primitive": "^1.2.0", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "is-callable": "^1.1.4", + "is-regex": "^1.0.4", + "object-keys": "^1.0.12" + } + }, + "es-to-primitive": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", + "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, "es5-ext": { "version": "0.10.49", "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.49.tgz", @@ -3648,6 +4225,11 @@ } } }, + "eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" + }, "events": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", @@ -3931,6 +4513,12 @@ } } }, + "extract-files": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/extract-files/-/extract-files-5.0.1.tgz", + "integrity": "sha512-qRW6y9eKF0VbCyOoOEtFhzJ3uykAw8GKwQVXyAIqwocyEWW4m+v+evec34RwtUkkxxHh7NKBLJ6AnXM8W4dH5w==", + "dev": true + }, "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -4235,6 +4823,11 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, + "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", @@ -4833,6 +5426,11 @@ } } }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, "functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", @@ -5081,6 +5679,82 @@ "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", "dev": true }, + "graphql": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-14.2.1.tgz", + "integrity": "sha512-2PL1UbvKeSjy/lUeJqHk+eR9CvuErXoCNwJI4jm3oNFEeY+9ELqHNKO1ZuSxAkasPkpWbmT/iMRMFxd3cEL3tQ==", + "requires": { + "iterall": "^1.2.2" + } + }, + "graphql-extensions": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/graphql-extensions/-/graphql-extensions-0.5.7.tgz", + "integrity": "sha512-HrU6APE1PiehZ46scMB3S5DezSeCATd8v+e4mmg2bqszMyCFkmAnmK6hR1b5VjHxhzt5/FX21x1WsXfqF4FwdQ==", + "requires": { + "@apollographql/apollo-tools": "^0.3.3" + } + }, + "graphql-list-fields": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/graphql-list-fields/-/graphql-list-fields-2.0.2.tgz", + "integrity": "sha512-9TSAwcVA3KWw7JWYep5NCk2aw3wl1ayLtbMpmG7l26vh1FZ+gZexNPP+XJfUFyJa71UU0zcKSgtgpsrsA3Xv9Q==" + }, + "graphql-subscriptions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/graphql-subscriptions/-/graphql-subscriptions-1.1.0.tgz", + "integrity": "sha512-6WzlBFC0lWmXJbIVE8OgFgXIP4RJi3OQgTPa0DVMsDXdpRDjTsM1K9wfl5HSYX7R87QAGlvcv2Y4BIZa/ItonA==", + "requires": { + "iterall": "^1.2.1" + } + }, + "graphql-tag": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.10.1.tgz", + "integrity": "sha512-jApXqWBzNXQ8jYa/HLkZJaVw9jgwNqZkywa2zfFn16Iv1Zb7ELNHkJaXHR7Quvd5SIGsy6Ny7SUKATgnu05uEg==" + }, + "graphql-tools": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/graphql-tools/-/graphql-tools-4.0.4.tgz", + "integrity": "sha512-chF12etTIGVVGy3fCTJ1ivJX2KB7OSG4c6UOJQuqOHCmBQwTyNgCDuejZKvpYxNZiEx7bwIjrodDgDe9RIkjlw==", + "requires": { + "apollo-link": "^1.2.3", + "apollo-utilities": "^1.0.1", + "deprecated-decorator": "^0.1.6", + "iterall": "^1.1.3", + "uuid": "^3.1.0" + } + }, + "graphql-upload": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/graphql-upload/-/graphql-upload-8.0.5.tgz", + "integrity": "sha512-iv8R/E1b0GJ203Z2sdPgnCnU8tl9hQY+jkebiTNAjsWBT3j/I5VLBnPJdDhJSKIreWJ4/1LZjgOt60qjnH4/EQ==", + "requires": { + "busboy": "^0.3.0", + "fs-capacitor": "^2.0.1", + "http-errors": "^1.7.2", + "object-path": "^0.11.4" + }, + "dependencies": { + "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" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + } + } + }, "handlebars": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", @@ -5107,6 +5781,14 @@ "har-schema": "^2.0.0" } }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, "has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", @@ -5128,6 +5810,11 @@ "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==", "dev": true }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=" + }, "has-to-string-tag-x": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", @@ -5362,6 +6049,12 @@ "minimatch": "^3.0.4" } }, + "immutable-tuple": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/immutable-tuple/-/immutable-tuple-0.4.10.tgz", + "integrity": "sha512-45jheDbc3Kr5Cw8EtDD+4woGRUV0utIrJBZT8XH0TPZRfm8tzT0/sLGGzyyCCFqFMG5Pv5Igf3WY/arn6+8V9Q==", + "dev": true + }, "import-fresh": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.0.0.tgz", @@ -5560,6 +6253,11 @@ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", "dev": true }, + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==" + }, "is-ci": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", @@ -5589,6 +6287,11 @@ } } }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" + }, "is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", @@ -5815,6 +6518,14 @@ "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", "dev": true }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "requires": { + "has": "^1.0.1" + } + }, "is-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", @@ -5832,6 +6543,14 @@ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" }, + "is-symbol": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", + "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "requires": { + "has-symbols": "^1.0.0" + } + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -6010,6 +6729,11 @@ "is-object": "^1.0.1" } }, + "iterall": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.2.2.tgz", + "integrity": "sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA==" + }, "jasmine": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.4.0.tgz", @@ -6626,6 +7350,11 @@ "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=", "dev": true }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" + }, "lodash.startswith": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/lodash.startswith/-/lodash.startswith-4.2.1.tgz", @@ -6664,6 +7393,11 @@ "triple-beam": "^1.3.0" } }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -7433,6 +8167,11 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node-fetch": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.5.0.tgz", + "integrity": "sha512-YuZKluhWGJwCcUu4RlZstdAxr8bFfOVHakc1mplwHkk8J+tqM1Y5yraYvIUpeX8aY7+crCwiELJq7Vl0o0LWXw==" + }, "node-forge": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz", @@ -7724,6 +8463,16 @@ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz", "integrity": "sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==" }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, + "object-path": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.4.tgz", + "integrity": "sha1-NwrnUvvzfePqcKhhwju6iRVpGUk=" + }, "object-visit": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", @@ -7733,6 +8482,15 @@ "isobject": "^3.0.0" } }, + "object.getownpropertydescriptors": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", + "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.5.1" + } + }, "object.pick": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", @@ -7772,6 +8530,15 @@ "mimic-fn": "^1.0.0" } }, + "optimism": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.6.9.tgz", + "integrity": "sha512-xoQm2lvXbCA9Kd7SCx6y713Y7sZ6fUc5R6VYpoL5M6svKJbTuvtNopexK8sO8K4s0EOUYHuPN2+yAEsNyRggkQ==", + "dev": true, + "requires": { + "immutable-tuple": "^0.4.9" + } + }, "optimist": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", @@ -8289,6 +9056,33 @@ "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", "dev": true }, + "protobufjs": { + "version": "6.8.8", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz", + "integrity": "sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==", + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.0", + "@types/node": "^10.1.0", + "long": "^4.0.0" + }, + "dependencies": { + "@types/node": { + "version": "10.14.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.6.tgz", + "integrity": "sha512-Fvm24+u85lGmV4hT5G++aht2C5I4Z4dYlWZIh62FAfFO/TfzXtPpoLI6I7AuBWkIFqZCnhFOoTT7RjjaIL5Fjg==" + } + } + }, "proxy-addr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", @@ -8706,6 +9500,11 @@ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", "dev": true }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=" + }, "rimraf": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", @@ -8905,6 +9704,15 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -9307,6 +10115,11 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, "string-argv": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.0.2.tgz", @@ -9385,6 +10198,28 @@ "escape-string-regexp": "^1.0.2" } }, + "subscriptions-transport-ws": { + "version": "0.9.16", + "resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.16.tgz", + "integrity": "sha512-pQdoU7nC+EpStXnCfh/+ho0zE0Z+ma+i7xvj7bkXKb1dvYHSZxgRPaU6spRP+Bjzow67c/rRDoix5RT0uU9omw==", + "requires": { + "backo2": "^1.0.2", + "eventemitter3": "^3.1.0", + "iterall": "^1.2.1", + "symbol-observable": "^1.0.4", + "ws": "^5.2.0" + }, + "dependencies": { + "ws": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", + "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", + "requires": { + "async-limiter": "~1.0.0" + } + } + } + }, "supports-color": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.0.0.tgz", @@ -9405,8 +10240,7 @@ "symbol-observable": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", - "dev": true + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" }, "synchronous-promise": { "version": "2.0.9", @@ -9718,11 +10552,18 @@ "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" }, + "ts-invariant": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.3.3.tgz", + "integrity": "sha512-UReOKsrJFGC9tUblgSRWo+BsVNbEd77Cl6WiV/XpMlkifXwNIJbknViCucHvVZkXSC/mcWeRnIGdY7uprcwvdQ==", + "requires": { + "tslib": "^1.9.3" + } + }, "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==" }, "tsscmp": { "version": "1.0.6", @@ -10029,6 +10870,15 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, + "util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "requires": { + "define-properties": "^1.1.2", + "object.getownpropertydescriptors": "^2.0.3" + } + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -10392,6 +11242,20 @@ "synchronous-promise": "^2.0.6", "toposort": "^2.0.2" } + }, + "zen-observable": { + "version": "0.8.14", + "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.14.tgz", + "integrity": "sha512-kQz39uonEjEESwh+qCi83kcC3rZJGh4mrZW7xjkSQYXkq//JZHTtKo+6yuVloTgMtzsIWOJrjIrKvk/dqm0L5g==" + }, + "zen-observable-ts": { + "version": "0.8.18", + "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.8.18.tgz", + "integrity": "sha512-q7d05s75Rn1j39U5Oapg3HI2wzriVwERVo4N7uFGpIYuHB9ff02P/E92P9B8T7QVC93jCMHpbXH7X0eVR5LA7A==", + "requires": { + "tslib": "^1.9.3", + "zen-observable": "^0.8.0" + } } } } diff --git a/package.json b/package.json index 30e00627..e2d5bed2 100644 --- a/package.json +++ b/package.json @@ -19,16 +19,22 @@ ], "license": "BSD-3-Clause", "dependencies": { + "@apollographql/graphql-playground-html": "1.6.6", "@parse/fs-files-adapter": "1.0.1", "@parse/push-adapter": "3.0.0", "@parse/s3-files-adapter": "1.2.1", "@parse/simple-mailgun-adapter": "1.1.0", + "apollo-server-express": "2.4.8", "bcryptjs": "2.4.3", "body-parser": "1.19.0", "commander": "2.20.0", + "cors": "2.8.5", "deepcopy": "2.0.0", "express": "4.17.1", "follow-redirects": "1.7.0", + "graphql": "14.2.1", + "graphql-list-fields": "2.0.2", + "graphql-upload": "8.0.5", "intersect": "1.0.1", "jsonwebtoken": "8.5.1", "lodash": "4.17.11", @@ -40,6 +46,7 @@ "pg-promise": "8.7.2", "redis": "2.8.0", "semver": "6.1.1", + "subscriptions-transport-ws": "0.9.16", "tv4": "1.3.0", "uuid": "3.3.2", "winston": "3.2.1", @@ -53,6 +60,13 @@ "@babel/plugin-transform-flow-strip-types": "7.4.4", "@babel/preset-env": "7.4.5", "@parse/minami": "1.0.0", + "apollo-cache-inmemory": "1.5.1", + "apollo-client": "2.5.1", + "apollo-link": "1.2.11", + "apollo-link-http": "1.5.14", + "apollo-link-ws": "1.0.17", + "apollo-upload-client": "10.0.0", + "apollo-utilities": "1.2.1", "babel-eslint": "10.0.2", "bcrypt-nodejs": "0.0.3", "cross-env": "5.2.0", @@ -60,6 +74,7 @@ "eslint": "5.16.0", "eslint-plugin-flowtype": "3.10.3", "flow-bin": "0.101.0", + "form-data": "2.3.3", "gaze": "1.1.3", "husky": "2.4.1", "jasmine": "3.4.0", @@ -68,6 +83,7 @@ "jsdoc-babel": "0.5.0", "lint-staged": "8.2.1", "mongodb-runner": "4.3.2", + "node-fetch": "2.5.0", "nyc": "14.1.1", "prettier": "1.18.2", "supports-color": "7.0.0" diff --git a/spec/.eslintrc.json b/spec/.eslintrc.json index d5b3e02c..e11949b7 100644 --- a/spec/.eslintrc.json +++ b/spec/.eslintrc.json @@ -24,7 +24,8 @@ "range": true, "jequal": true, "create": true, - "arrayContains": true + "arrayContains": true, + "expectAsync": true }, "rules": { "no-console": [0], diff --git a/spec/ParseGraphQLSchema.spec.js b/spec/ParseGraphQLSchema.spec.js new file mode 100644 index 00000000..7437183f --- /dev/null +++ b/spec/ParseGraphQLSchema.spec.js @@ -0,0 +1,65 @@ +const defaultLogger = require('../lib/logger').default; +const { ParseGraphQLSchema } = require('../lib/GraphQL/ParseGraphQLSchema'); + +describe('ParseGraphQLSchema', () => { + let parseServer; + let databaseController; + let parseGraphQLSchema; + + beforeAll(async () => { + parseServer = await global.reconfigureServer({ + schemaCacheTTL: 100, + }); + databaseController = parseServer.config.databaseController; + parseGraphQLSchema = new ParseGraphQLSchema( + databaseController, + defaultLogger + ); + }); + + describe('constructor', () => { + it('should require a databaseController and a log instance', () => { + expect(() => new ParseGraphQLSchema()).toThrow( + 'You must provide a databaseController instance!' + ); + expect(() => new ParseGraphQLSchema({})).toThrow( + 'You must provide a log instance!' + ); + expect(() => new ParseGraphQLSchema({}, {})).not.toThrow(); + }); + }); + + describe('load', () => { + it('should cache schema', async () => { + const graphQLSchema = await parseGraphQLSchema.load(); + expect(graphQLSchema).toBe(await parseGraphQLSchema.load()); + await new Promise(resolve => setTimeout(resolve, 200)); + expect(graphQLSchema).toBe(await parseGraphQLSchema.load()); + }); + + it('should load a brand new GraphQL Schema if Parse Schema changes', async () => { + await parseGraphQLSchema.load(); + const parseClasses = parseGraphQLSchema.parseClasses; + const parseClassesString = parseGraphQLSchema.parseClasses; + const parseClassTypes = parseGraphQLSchema.parseClasses; + const graphQLSchema = parseGraphQLSchema.parseClasses; + const graphQLTypes = parseGraphQLSchema.parseClasses; + const graphQLQueries = parseGraphQLSchema.parseClasses; + const graphQLMutations = parseGraphQLSchema.parseClasses; + const graphQLSubscriptions = parseGraphQLSchema.parseClasses; + const newClassObject = new Parse.Object('NewClass'); + await newClassObject.save(); + await databaseController.schemaCache.clear(); + await new Promise(resolve => setTimeout(resolve, 200)); + await parseGraphQLSchema.load(); + expect(parseClasses).not.toBe(parseGraphQLSchema.parseClasses); + expect(parseClassesString).not.toBe(parseGraphQLSchema.parseClasses); + expect(parseClassTypes).not.toBe(parseGraphQLSchema.parseClasses); + expect(graphQLSchema).not.toBe(parseGraphQLSchema.parseClasses); + expect(graphQLTypes).not.toBe(parseGraphQLSchema.parseClasses); + expect(graphQLQueries).not.toBe(parseGraphQLSchema.parseClasses); + expect(graphQLMutations).not.toBe(parseGraphQLSchema.parseClasses); + expect(graphQLSubscriptions).not.toBe(parseGraphQLSchema.parseClasses); + }); + }); +}); diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js new file mode 100644 index 00000000..74fc3b6f --- /dev/null +++ b/spec/ParseGraphQLServer.spec.js @@ -0,0 +1,4950 @@ +const http = require('http'); +const express = require('express'); +const req = require('../lib/request'); +const fetch = require('node-fetch'); +const FormData = require('form-data'); +const ws = require('ws'); +const { getMainDefinition } = require('apollo-utilities'); +const { ApolloLink, split } = require('apollo-link'); +const { createHttpLink } = require('apollo-link-http'); +const { InMemoryCache } = require('apollo-cache-inmemory'); +const { createUploadLink } = require('apollo-upload-client'); +const { SubscriptionClient } = require('subscriptions-transport-ws'); +const { WebSocketLink } = require('apollo-link-ws'); +const ApolloClient = require('apollo-client').default; +const gql = require('graphql-tag'); +const { ParseServer } = require('../'); +const { ParseGraphQLServer } = require('../lib/GraphQL/ParseGraphQLServer'); +const ReadPreference = require('mongodb').ReadPreference; + +describe('ParseGraphQLServer', () => { + let parseServer; + let parseGraphQLServer; + + beforeAll(async () => { + parseServer = await global.reconfigureServer({}); + parseGraphQLServer = new ParseGraphQLServer(parseServer, { + graphQLPath: '/graphql', + playgroundPath: '/playground', + subscriptionsPath: '/subscriptions', + }); + }); + + describe('constructor', () => { + it('should require a parseServer instance', () => { + expect(() => new ParseGraphQLServer()).toThrow( + 'You must provide a parseServer instance!' + ); + }); + + it('should require config.graphQLPath', () => { + expect(() => new ParseGraphQLServer(parseServer)).toThrow( + 'You must provide a config.graphQLPath!' + ); + expect(() => new ParseGraphQLServer(parseServer, {})).toThrow( + 'You must provide a config.graphQLPath!' + ); + }); + + it('should only require parseServer and config.graphQLPath args', () => { + let parseGraphQLServer; + expect(() => { + parseGraphQLServer = new ParseGraphQLServer(parseServer, { + graphQLPath: 'graphql', + }); + }).not.toThrow(); + expect(parseGraphQLServer.parseGraphQLSchema).toBeDefined(); + expect(parseGraphQLServer.parseGraphQLSchema.databaseController).toEqual( + parseServer.config.databaseController + ); + }); + + it('should initialize parseGraphQLSchema with a log controller', async () => { + const loggerAdapter = { + log: () => {}, + error: () => {}, + }; + const parseServer = await global.reconfigureServer({ + loggerAdapter, + }); + const parseGraphQLServer = new ParseGraphQLServer(parseServer, { + graphQLPath: 'graphql', + }); + expect(parseGraphQLServer.parseGraphQLSchema.log.adapter).toBe( + loggerAdapter + ); + }); + }); + + describe('_getGraphQLOptions', () => { + const req = { + info: new Object(), + config: new Object(), + auth: new Object(), + }; + + it("should return schema and context with req's info, config and auth", async () => { + const options = await parseGraphQLServer._getGraphQLOptions(req); + expect(options.schema).toEqual( + parseGraphQLServer.parseGraphQLSchema.graphQLSchema + ); + expect(options.context.info).toEqual(req.info); + expect(options.context.config).toEqual(req.config); + expect(options.context.auth).toEqual(req.auth); + }); + + it('should load GraphQL schema in every call', async () => { + const originalLoad = parseGraphQLServer.parseGraphQLSchema.load; + let counter = 0; + parseGraphQLServer.parseGraphQLSchema.load = () => ++counter; + expect((await parseGraphQLServer._getGraphQLOptions(req)).schema).toEqual( + 1 + ); + expect((await parseGraphQLServer._getGraphQLOptions(req)).schema).toEqual( + 2 + ); + expect((await parseGraphQLServer._getGraphQLOptions(req)).schema).toEqual( + 3 + ); + parseGraphQLServer.parseGraphQLSchema.load = originalLoad; + }); + }); + + describe('applyGraphQL', () => { + it('should require an Express.js app instance', () => { + expect(() => parseGraphQLServer.applyGraphQL()).toThrow( + 'You must provide an Express.js app instance!' + ); + expect(() => parseGraphQLServer.applyGraphQL({})).toThrow( + 'You must provide an Express.js app instance!' + ); + expect(() => + parseGraphQLServer.applyGraphQL(new express()) + ).not.toThrow(); + }); + + it('should apply middlewares at config.graphQLPath', () => { + let useCount = 0; + expect(() => + new ParseGraphQLServer(parseServer, { + graphQLPath: 'somepath', + }).applyGraphQL({ + use: path => { + useCount++; + expect(path).toEqual('somepath'); + }, + }) + ).not.toThrow(); + expect(useCount).toBeGreaterThan(0); + }); + }); + + describe('applyPlayground', () => { + it('should require an Express.js app instance', () => { + expect(() => parseGraphQLServer.applyPlayground()).toThrow( + 'You must provide an Express.js app instance!' + ); + expect(() => parseGraphQLServer.applyPlayground({})).toThrow( + 'You must provide an Express.js app instance!' + ); + expect(() => + parseGraphQLServer.applyPlayground(new express()) + ).not.toThrow(); + }); + + it('should require initialization with config.playgroundPath', () => { + expect(() => + new ParseGraphQLServer(parseServer, { + graphQLPath: 'graphql', + }).applyPlayground(new express()) + ).toThrow('You must provide a config.playgroundPath to applyPlayground!'); + }); + + it('should apply middlewares at config.playgroundPath', () => { + let useCount = 0; + expect(() => + new ParseGraphQLServer(parseServer, { + graphQLPath: 'graphQL', + playgroundPath: 'somepath', + }).applyPlayground({ + get: path => { + useCount++; + expect(path).toEqual('somepath'); + }, + }) + ).not.toThrow(); + expect(useCount).toBeGreaterThan(0); + }); + }); + + describe('createSubscriptions', () => { + it('should require initialization with config.subscriptionsPath', () => { + expect(() => + new ParseGraphQLServer(parseServer, { + graphQLPath: 'graphql', + }).createSubscriptions({}) + ).toThrow( + 'You must provide a config.subscriptionsPath to createSubscriptions!' + ); + }); + }); + + describe('API', () => { + const headers = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Javascript-Key': 'test', + }; + + let apolloClient; + + let user1; + let user2; + let user3; + let user4; + let user5; + let role; + let object1; + let object2; + let object3; + let object4; + let objects = []; + + async function prepareData() { + user1 = new Parse.User(); + user1.setUsername('user1'); + user1.setPassword('user1'); + await user1.signUp(); + + user2 = new Parse.User(); + user2.setUsername('user2'); + user2.setPassword('user2'); + await user2.signUp(); + + user3 = new Parse.User(); + user3.setUsername('user3'); + user3.setPassword('user3'); + await user3.signUp(); + + user4 = new Parse.User(); + user4.setUsername('user4'); + user4.setPassword('user4'); + await user4.signUp(); + + user5 = new Parse.User(); + user5.setUsername('user5'); + user5.setPassword('user5'); + await user5.signUp(); + + const roleACL = new Parse.ACL(); + roleACL.setPublicReadAccess(true); + role = new Parse.Role(); + role.setName('role'); + role.setACL(roleACL); + role.getUsers().add(user1); + role.getUsers().add(user3); + role = await role.save(); + + const schemaController = await parseServer.config.databaseController.loadSchema(); + try { + await schemaController.addClassIfNotExists( + 'GraphQLClass', + { + someField: { type: 'String' }, + pointerToUser: { type: 'Pointer', targetClass: '_User' }, + }, + { + find: { + 'role:role': true, + [user1.id]: true, + [user2.id]: true, + }, + create: { + 'role:role': true, + [user1.id]: true, + [user2.id]: true, + }, + get: { + 'role:role': true, + [user1.id]: true, + [user2.id]: true, + }, + update: { + 'role:role': true, + [user1.id]: true, + [user2.id]: true, + }, + addField: { + 'role:role': true, + [user1.id]: true, + [user2.id]: true, + }, + delete: { + 'role:role': true, + [user1.id]: true, + [user2.id]: true, + }, + readUserFields: ['pointerToUser'], + writeUserFields: ['pointerToUser'], + }, + {} + ); + } catch (err) { + if ( + !(err instanceof Parse.Error) || + err.message !== 'Class GraphQLClass already exists.' + ) { + throw err; + } + } + + object1 = new Parse.Object('GraphQLClass'); + object1.set('someField', 'someValue1'); + const object1ACL = new Parse.ACL(); + object1ACL.setPublicReadAccess(false); + object1ACL.setPublicWriteAccess(false); + object1ACL.setRoleReadAccess(role, true); + object1ACL.setRoleWriteAccess(role, true); + object1ACL.setReadAccess(user1.id, true); + object1ACL.setWriteAccess(user1.id, true); + object1ACL.setReadAccess(user2.id, true); + object1ACL.setWriteAccess(user2.id, true); + object1.setACL(object1ACL); + await object1.save(undefined, { useMasterKey: true }); + + object2 = new Parse.Object('GraphQLClass'); + object2.set('someField', 'someValue2'); + const object2ACL = new Parse.ACL(); + object2ACL.setPublicReadAccess(false); + object2ACL.setPublicWriteAccess(false); + object2ACL.setReadAccess(user1.id, true); + object2ACL.setWriteAccess(user1.id, true); + object2ACL.setReadAccess(user2.id, true); + object2ACL.setWriteAccess(user2.id, true); + object2ACL.setReadAccess(user5.id, true); + object2ACL.setWriteAccess(user5.id, true); + object2.setACL(object2ACL); + await object2.save(undefined, { useMasterKey: true }); + + object3 = new Parse.Object('GraphQLClass'); + object3.set('someField', 'someValue3'); + object3.set('pointerToUser', user5); + await object3.save(undefined, { useMasterKey: true }); + + object4 = new Parse.Object('PublicClass'); + object4.set('someField', 'someValue4'); + await object4.save(); + + objects = []; + objects.push(object1, object2, object3, object4); + } + + beforeAll(async () => { + const expressApp = express(); + const httpServer = http.createServer(expressApp); + expressApp.use('/parse', parseServer.app); + ParseServer.createLiveQueryServer(httpServer, { + port: 1338, + }); + parseGraphQLServer.applyGraphQL(expressApp); + parseGraphQLServer.applyPlayground(expressApp); + parseGraphQLServer.createSubscriptions(httpServer); + await new Promise(resolve => httpServer.listen({ port: 13377 }, resolve)); + + const subscriptionClient = new SubscriptionClient( + 'ws://localhost:13377/subscriptions', + { + reconnect: true, + connectionParams: headers, + }, + ws + ); + const wsLink = new WebSocketLink(subscriptionClient); + const httpLink = createUploadLink({ + uri: 'http://localhost:13377/graphql', + fetch, + headers, + }); + apolloClient = new ApolloClient({ + link: split( + ({ query }) => { + const { kind, operation } = getMainDefinition(query); + return ( + kind === 'OperationDefinition' && operation === 'subscription' + ); + }, + wsLink, + httpLink + ), + cache: new InMemoryCache(), + defaultOptions: { + query: { + fetchPolicy: 'no-cache', + }, + }, + }); + }); + + describe('GraphQL', () => { + it('should be healthy', async () => { + const health = (await apolloClient.query({ + query: gql` + query Health { + health + } + `, + })).data.health; + expect(health).toBeTruthy(); + }); + + it('should be cors enabled', async () => { + let checked = false; + const apolloClient = new ApolloClient({ + link: new ApolloLink((operation, forward) => { + return forward(operation).map(response => { + const context = operation.getContext(); + const { + response: { headers }, + } = context; + expect(headers.get('access-control-allow-origin')).toEqual('*'); + checked = true; + return response; + }); + }).concat( + createHttpLink({ + uri: 'http://localhost:13377/graphql', + fetch, + headers: { + ...headers, + Origin: 'http://someorigin.com', + }, + }) + ), + cache: new InMemoryCache(), + }); + const healthResponse = await apolloClient.query({ + query: gql` + query Health { + health + } + `, + }); + expect(healthResponse.data.health).toBeTruthy(); + expect(checked).toBeTruthy(); + }); + + it('should handle Parse headers', async () => { + let checked = false; + const originalGetGraphQLOptions = parseGraphQLServer._getGraphQLOptions; + parseGraphQLServer._getGraphQLOptions = async req => { + expect(req.info).toBeDefined(); + expect(req.config).toBeDefined(); + expect(req.auth).toBeDefined(); + checked = true; + return await originalGetGraphQLOptions.bind(parseGraphQLServer)(req); + }; + const health = (await apolloClient.query({ + query: gql` + query Health { + health + } + `, + })).data.health; + expect(health).toBeTruthy(); + expect(checked).toBeTruthy(); + parseGraphQLServer._getGraphQLOptions = originalGetGraphQLOptions; + }); + }); + + describe('Playground', () => { + it('should mount playground', async () => { + const res = await req({ + method: 'GET', + url: 'http://localhost:13377/playground', + }); + expect(res.status).toEqual(200); + }); + }); + + describe('Schema', () => { + describe('Default Types', () => { + it('should have Object scalar type', async () => { + const objectType = (await apolloClient.query({ + query: gql` + query ObjectType { + __type(name: "Object") { + kind + } + } + `, + })).data['__type']; + expect(objectType.kind).toEqual('SCALAR'); + }); + + it('should have Date scalar type', async () => { + const dateType = (await apolloClient.query({ + query: gql` + query DateType { + __type(name: "Date") { + kind + } + } + `, + })).data['__type']; + expect(dateType.kind).toEqual('SCALAR'); + }); + + it('should have File object type', async () => { + const fileType = (await apolloClient.query({ + query: gql` + query FileType { + __type(name: "FileInfo") { + kind + fields { + name + } + } + } + `, + })).data['__type']; + expect(fileType.kind).toEqual('OBJECT'); + expect(fileType.fields.map(field => field.name).sort()).toEqual([ + 'name', + 'url', + ]); + }); + + it('should have CreateResult object type', async () => { + const createResultType = (await apolloClient.query({ + query: gql` + query CreateResultType { + __type(name: "CreateResult") { + kind + fields { + name + } + } + } + `, + })).data['__type']; + expect(createResultType.kind).toEqual('OBJECT'); + expect( + createResultType.fields.map(field => field.name).sort() + ).toEqual(['createdAt', 'objectId']); + }); + + it('should have UpdateResult object type', async () => { + const updateResultType = (await apolloClient.query({ + query: gql` + query UpdateResultType { + __type(name: "UpdateResult") { + kind + fields { + name + } + } + } + `, + })).data['__type']; + expect(updateResultType.kind).toEqual('OBJECT'); + expect(updateResultType.fields.map(field => field.name)).toEqual([ + 'updatedAt', + ]); + }); + + it('should have Class interface type', async () => { + const classType = (await apolloClient.query({ + query: gql` + query ClassType { + __type(name: "Class") { + kind + fields { + name + } + } + } + `, + })).data['__type']; + expect(classType.kind).toEqual('INTERFACE'); + expect(classType.fields.map(field => field.name).sort()).toEqual([ + 'ACL', + 'createdAt', + 'objectId', + 'updatedAt', + ]); + }); + + it('should have ReadPreference enum type', async () => { + const readPreferenceType = (await apolloClient.query({ + query: gql` + query ReadPreferenceType { + __type(name: "ReadPreference") { + kind + enumValues { + name + } + } + } + `, + })).data['__type']; + expect(readPreferenceType.kind).toEqual('ENUM'); + expect( + readPreferenceType.enumValues.map(value => value.name).sort() + ).toEqual([ + 'NEAREST', + 'PRIMARY', + 'PRIMARY_PREFERRED', + 'SECONDARY', + 'SECONDARY_PREFERRED', + ]); + }); + + it('should have FindResult object type', async () => { + const findResultType = (await apolloClient.query({ + query: gql` + query FindResultType { + __type(name: "FindResult") { + kind + fields { + name + } + } + } + `, + })).data['__type']; + expect(findResultType.kind).toEqual('OBJECT'); + expect(findResultType.fields.map(name => name.name).sort()).toEqual([ + 'count', + 'results', + ]); + }); + + it('should have GraphQLUpload object type', async () => { + const graphQLUploadType = (await apolloClient.query({ + query: gql` + query GraphQLUploadType { + __type(name: "Upload") { + kind + fields { + name + } + } + } + `, + })).data['__type']; + expect(graphQLUploadType.kind).toEqual('SCALAR'); + }); + + it('should have all expected types', async () => { + const schemaTypes = (await apolloClient.query({ + query: gql` + query SchemaTypes { + __schema { + types { + name + } + } + } + `, + })).data['__schema'].types.map(type => type.name); + + const expectedTypes = [ + 'Class', + 'CreateResult', + 'Date', + 'File', + 'FilesMutation', + 'FindResult', + 'ObjectsMutation', + 'ObjectsQuery', + 'ReadPreference', + 'UpdateResult', + 'Upload', + ]; + expect( + expectedTypes.every(type => schemaTypes.indexOf(type) !== -1) + ).toBeTruthy(JSON.stringify(schemaTypes.types)); + }); + }); + + describe('Parse Class Types', () => { + it('should have all expected types', async () => { + await parseServer.config.databaseController.loadSchema(); + + const schemaTypes = (await apolloClient.query({ + query: gql` + query SchemaTypes { + __schema { + types { + name + } + } + } + `, + })).data['__schema'].types.map(type => type.name); + + const expectedTypes = [ + '_RoleClass', + '_RoleConstraints', + '_RoleFields', + '_RoleFindResult', + '_UserClass', + '_UserConstraints', + '_UserFindResult', + '_UserFields', + ]; + expect( + expectedTypes.every(type => schemaTypes.indexOf(type) !== -1) + ).toBeTruthy(JSON.stringify(schemaTypes)); + }); + + it('should update schema when it changes', async () => { + const schemaController = await parseServer.config.databaseController.loadSchema(); + await schemaController.updateClass('_User', { + foo: { type: 'String' }, + }); + + const userFields = (await apolloClient.query({ + query: gql` + query UserType { + __type(name: "_UserClass") { + fields { + name + } + } + } + `, + })).data['__type'].fields.map(field => field.name); + expect(userFields.indexOf('foo') !== -1).toBeTruthy(); + }); + }); + + describe('Objects Queries', () => { + describe('Get', () => { + it('should return a class object using generic query', async () => { + const obj = new Parse.Object('SomeClass'); + obj.set('someField', 'someValue'); + await obj.save(); + + const result = (await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!) { + objects { + get(className: "SomeClass", objectId: $objectId) + } + } + `, + variables: { + objectId: obj.id, + }, + })).data.objects.get; + + expect(result.objectId).toEqual(obj.id); + expect(result.someField).toEqual('someValue'); + expect(new Date(result.createdAt)).toEqual(obj.createdAt); + expect(new Date(result.updatedAt)).toEqual(obj.updatedAt); + }); + + it('should return a class object using class specific query', async () => { + const obj = new Parse.Object('Customer'); + obj.set('someField', 'someValue'); + await obj.save(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const result = (await apolloClient.query({ + query: gql` + query GetCustomer($objectId: ID!) { + objects { + getCustomer(objectId: $objectId) { + objectId + someField + createdAt + updatedAt + } + } + } + `, + variables: { + objectId: obj.id, + }, + })).data.objects.getCustomer; + + expect(result.objectId).toEqual(obj.id); + expect(result.someField).toEqual('someValue'); + expect(new Date(result.createdAt)).toEqual(obj.createdAt); + expect(new Date(result.updatedAt)).toEqual(obj.updatedAt); + }); + + it('should respect level permissions', async () => { + await prepareData(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + async function getObject(className, objectId, headers) { + const specificQueryResult = await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!) { + objects { + get${className}(objectId: $objectId) { + objectId + createdAt + } + } + } + `, + variables: { + objectId, + }, + context: { + headers, + }, + }); + + const genericQueryResult = await apolloClient.query({ + query: gql` + query GetSomeObject($className: String!, $objectId: ID!) { + objects { + get(className: $className, objectId: $objectId) + } + } + `, + variables: { + className, + objectId, + }, + context: { + headers, + }, + }); + + expect(genericQueryResult.objectId).toEqual( + specificQueryResult.objectId + ); + expect(genericQueryResult.createdAt).toEqual( + specificQueryResult.createdAt + ); + return genericQueryResult; + } + + await Promise.all( + objects + .slice(0, 3) + .map(obj => + expectAsync( + getObject(obj.className, obj.id) + ).toBeRejectedWith(jasmine.stringMatching('Object not found')) + ) + ); + expect( + (await getObject(object4.className, object4.id)).data.objects.get + .someField + ).toEqual('someValue4'); + await Promise.all( + objects.map(async obj => + expect( + (await getObject(obj.className, obj.id, { + 'X-Parse-Master-Key': 'test', + })).data.objects.get.someField + ).toEqual(obj.get('someField')) + ) + ); + await Promise.all( + objects.map(async obj => + expect( + (await getObject(obj.className, obj.id, { + 'X-Parse-Session-Token': user1.getSessionToken(), + })).data.objects.get.someField + ).toEqual(obj.get('someField')) + ) + ); + await Promise.all( + objects.map(async obj => + expect( + (await getObject(obj.className, obj.id, { + 'X-Parse-Session-Token': user2.getSessionToken(), + })).data.objects.get.someField + ).toEqual(obj.get('someField')) + ) + ); + await expectAsync( + getObject(object2.className, object2.id, { + 'X-Parse-Session-Token': user3.getSessionToken(), + }) + ).toBeRejectedWith(jasmine.stringMatching('Object not found')); + await Promise.all( + [object1, object3, object4].map(async obj => + expect( + (await getObject(obj.className, obj.id, { + 'X-Parse-Session-Token': user3.getSessionToken(), + })).data.objects.get.someField + ).toEqual(obj.get('someField')) + ) + ); + await Promise.all( + objects.slice(0, 3).map(obj => + expectAsync( + getObject(obj.className, obj.id, { + 'X-Parse-Session-Token': user4.getSessionToken(), + }) + ).toBeRejectedWith(jasmine.stringMatching('Object not found')) + ) + ); + expect( + (await getObject(object4.className, object4.id, { + 'X-Parse-Session-Token': user4.getSessionToken(), + })).data.objects.get.someField + ).toEqual('someValue4'); + await Promise.all( + objects.slice(0, 2).map(obj => + expectAsync( + getObject(obj.className, obj.id, { + 'X-Parse-Session-Token': user5.getSessionToken(), + }) + ).toBeRejectedWith(jasmine.stringMatching('Object not found')) + ) + ); + expect( + (await getObject(object3.className, object3.id, { + 'X-Parse-Session-Token': user5.getSessionToken(), + })).data.objects.get.someField + ).toEqual('someValue3'); + expect( + (await getObject(object4.className, object4.id, { + 'X-Parse-Session-Token': user5.getSessionToken(), + })).data.objects.get.someField + ).toEqual('someValue4'); + }); + + it('should not bring session token of another user', async () => { + await prepareData(); + + const result = await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!) { + objects { + get(className: "_User", objectId: $objectId) + } + } + `, + variables: { + objectId: user2.id, + }, + context: { + headers: { + 'X-Parse-Session-Token': user1.getSessionToken(), + }, + }, + }); + expect(result.data.objects.get.sessionToken).toBeUndefined(); + }); + + it('should not bring session token of current user', async () => { + await prepareData(); + + const result = await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!) { + objects { + get(className: "_User", objectId: $objectId) + } + } + `, + variables: { + objectId: user1.id, + }, + context: { + headers: { + 'X-Parse-Session-Token': user1.getSessionToken(), + }, + }, + }); + expect(result.data.objects.get.sessionToken).toBeUndefined(); + }); + + it('should support keys argument', async () => { + await prepareData(); + + const result1 = await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!) { + objects { + get( + className: "GraphQLClass" + objectId: $objectId + keys: "someField" + ) + } + } + `, + variables: { + objectId: object3.id, + }, + context: { + headers: { + 'X-Parse-Session-Token': user1.getSessionToken(), + }, + }, + }); + + const result2 = await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!) { + objects { + get( + className: "GraphQLClass" + objectId: $objectId + keys: "someField,pointerToUser" + ) + } + } + `, + variables: { + objectId: object3.id, + }, + context: { + headers: { + 'X-Parse-Session-Token': user1.getSessionToken(), + }, + }, + }); + + expect(result1.data.objects.get.someField).toBeDefined(); + expect(result1.data.objects.get.pointerToUser).toBeUndefined(); + expect(result2.data.objects.get.someField).toBeDefined(); + expect(result2.data.objects.get.pointerToUser).toBeDefined(); + }); + + it('should support include argument', async () => { + await prepareData(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const result1 = await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!) { + objects { + get(className: "GraphQLClass", objectId: $objectId) + } + } + `, + variables: { + objectId: object3.id, + }, + context: { + headers: { + 'X-Parse-Session-Token': user1.getSessionToken(), + }, + }, + }); + + const result2 = await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!) { + objects { + get( + className: "GraphQLClass" + objectId: $objectId + include: "pointerToUser" + ) + getGraphQLClass(objectId: $objectId) { + pointerToUser { + username + } + } + } + } + `, + variables: { + objectId: object3.id, + }, + context: { + headers: { + 'X-Parse-Session-Token': user1.getSessionToken(), + }, + }, + }); + + expect( + result1.data.objects.get.pointerToUser.username + ).toBeUndefined(); + expect( + result2.data.objects.get.pointerToUser.username + ).toBeDefined(); + expect( + result2.data.objects.getGraphQLClass.pointerToUser.username + ).toBeDefined(); + }); + + describe_only_db('mongo')('read preferences', () => { + it('should read from primary by default', async () => { + await prepareData(); + + const databaseAdapter = + parseServer.config.databaseController.adapter; + spyOn( + databaseAdapter.database.serverConfig, + 'cursor' + ).and.callThrough(); + + await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!) { + objects { + get( + className: "GraphQLClass" + objectId: $objectId + include: "pointerToUser" + ) + } + } + `, + variables: { + objectId: object3.id, + }, + context: { + headers: { + 'X-Parse-Session-Token': user1.getSessionToken(), + }, + }, + }); + + let foundGraphQLClassReadPreference = false; + let foundUserClassReadPreference = false; + databaseAdapter.database.serverConfig.cursor.calls + .all() + .forEach(call => { + if (call.args[0].indexOf('GraphQLClass') >= 0) { + foundGraphQLClassReadPreference = true; + expect(call.args[2].readPreference).toBe(null); + } else if (call.args[0].indexOf('_User') >= 0) { + foundUserClassReadPreference = true; + expect(call.args[2].readPreference).toBe(null); + } + }); + + expect(foundGraphQLClassReadPreference).toBe(true); + expect(foundUserClassReadPreference).toBe(true); + }); + + it('should support readPreference argument', async () => { + await prepareData(); + + const databaseAdapter = + parseServer.config.databaseController.adapter; + spyOn( + databaseAdapter.database.serverConfig, + 'cursor' + ).and.callThrough(); + + await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!) { + objects { + get( + className: "GraphQLClass" + objectId: $objectId + include: "pointerToUser" + readPreference: SECONDARY + ) + } + } + `, + variables: { + objectId: object3.id, + }, + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, + }, + }); + + let foundGraphQLClassReadPreference = false; + let foundUserClassReadPreference = false; + databaseAdapter.database.serverConfig.cursor.calls + .all() + .forEach(call => { + if (call.args[0].indexOf('GraphQLClass') >= 0) { + foundGraphQLClassReadPreference = true; + expect(call.args[2].readPreference.preference).toBe( + ReadPreference.SECONDARY + ); + } else if (call.args[0].indexOf('_User') >= 0) { + foundUserClassReadPreference = true; + expect(call.args[2].readPreference.preference).toBe( + ReadPreference.SECONDARY + ); + } + }); + + expect(foundGraphQLClassReadPreference).toBe(true); + expect(foundUserClassReadPreference).toBe(true); + }); + + it('should support includeReadPreference argument', async () => { + await prepareData(); + + const databaseAdapter = + parseServer.config.databaseController.adapter; + spyOn( + databaseAdapter.database.serverConfig, + 'cursor' + ).and.callThrough(); + + await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!) { + objects { + get( + className: "GraphQLClass" + objectId: $objectId + include: "pointerToUser" + readPreference: SECONDARY + includeReadPreference: NEAREST + ) + } + } + `, + variables: { + objectId: object3.id, + }, + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, + }, + }); + + let foundGraphQLClassReadPreference = false; + let foundUserClassReadPreference = false; + databaseAdapter.database.serverConfig.cursor.calls + .all() + .forEach(call => { + if (call.args[0].indexOf('GraphQLClass') >= 0) { + foundGraphQLClassReadPreference = true; + expect(call.args[2].readPreference.preference).toBe( + ReadPreference.SECONDARY + ); + } else if (call.args[0].indexOf('_User') >= 0) { + foundUserClassReadPreference = true; + expect(call.args[2].readPreference.preference).toBe( + ReadPreference.NEAREST + ); + } + }); + + expect(foundGraphQLClassReadPreference).toBe(true); + expect(foundUserClassReadPreference).toBe(true); + }); + }); + }); + + describe('Find', () => { + it('should return class objects using generic query', async () => { + const obj1 = new Parse.Object('SomeClass'); + obj1.set('someField', 'someValue1'); + await obj1.save(); + const obj2 = new Parse.Object('SomeClass'); + obj2.set('someField', 'someValue1'); + await obj2.save(); + + const result = await apolloClient.query({ + query: gql` + query FindSomeObjects { + objects { + find(className: "SomeClass") { + results + } + } + } + `, + }); + + expect(result.data.objects.find.results.length).toEqual(2); + + result.data.objects.find.results.forEach(resultObj => { + const obj = resultObj.objectId === obj1.id ? obj1 : obj2; + expect(resultObj.objectId).toEqual(obj.id); + expect(resultObj.someField).toEqual(obj.get('someField')); + expect(new Date(resultObj.createdAt)).toEqual(obj.createdAt); + expect(new Date(resultObj.updatedAt)).toEqual(obj.updatedAt); + }); + }); + + it('should return class objects using class specific query', async () => { + const obj1 = new Parse.Object('Customer'); + obj1.set('someField', 'someValue1'); + await obj1.save(); + const obj2 = new Parse.Object('Customer'); + obj2.set('someField', 'someValue1'); + await obj2.save(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const result = await apolloClient.query({ + query: gql` + query FindCustomer { + objects { + findCustomer { + results { + objectId + someField + createdAt + updatedAt + } + } + } + } + `, + }); + + expect(result.data.objects.findCustomer.results.length).toEqual(2); + + result.data.objects.findCustomer.results.forEach(resultObj => { + const obj = resultObj.objectId === obj1.id ? obj1 : obj2; + expect(resultObj.objectId).toEqual(obj.id); + expect(resultObj.someField).toEqual(obj.get('someField')); + expect(new Date(resultObj.createdAt)).toEqual(obj.createdAt); + expect(new Date(resultObj.updatedAt)).toEqual(obj.updatedAt); + }); + }); + + it('should respect level permissions', async () => { + await prepareData(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + async function findObjects(className, headers) { + const result = await apolloClient.query({ + query: gql` + query FindSomeObjects($className: String!) { + objects { + find(className: $className) { + results + } + find${className} { + results { + objectId + someField + } + } + } + } + `, + variables: { + className, + }, + context: { + headers, + }, + }); + + const genericFindResults = result.data.objects.find.results; + const specificFindResults = + result.data.objects[`find${className}`].results; + genericFindResults.forEach(({ objectId, someField }) => { + expect( + specificFindResults.some( + ({ + objectId: specificObjectId, + someField: specificSomeField, + }) => + objectId === specificObjectId && + someField === specificSomeField + ) + ); + }); + return result; + } + + expect( + (await findObjects('GraphQLClass')).data.objects.find.results.map( + object => object.someField + ) + ).toEqual([]); + expect( + (await findObjects('PublicClass')).data.objects.find.results.map( + object => object.someField + ) + ).toEqual(['someValue4']); + expect( + (await findObjects('GraphQLClass', { + 'X-Parse-Master-Key': 'test', + })).data.objects.find.results + .map(object => object.someField) + .sort() + ).toEqual(['someValue1', 'someValue2', 'someValue3']); + expect( + (await findObjects('PublicClass', { + 'X-Parse-Master-Key': 'test', + })).data.objects.find.results.map(object => object.someField) + ).toEqual(['someValue4']); + expect( + (await findObjects('GraphQLClass', { + 'X-Parse-Session-Token': user1.getSessionToken(), + })).data.objects.find.results + .map(object => object.someField) + .sort() + ).toEqual(['someValue1', 'someValue2', 'someValue3']); + expect( + (await findObjects('PublicClass', { + 'X-Parse-Session-Token': user1.getSessionToken(), + })).data.objects.find.results.map(object => object.someField) + ).toEqual(['someValue4']); + expect( + (await findObjects('GraphQLClass', { + 'X-Parse-Session-Token': user2.getSessionToken(), + })).data.objects.find.results + .map(object => object.someField) + .sort() + ).toEqual(['someValue1', 'someValue2', 'someValue3']); + expect( + (await findObjects('GraphQLClass', { + 'X-Parse-Session-Token': user3.getSessionToken(), + })).data.objects.find.results + .map(object => object.someField) + .sort() + ).toEqual(['someValue1', 'someValue3']); + expect( + (await findObjects('GraphQLClass', { + 'X-Parse-Session-Token': user4.getSessionToken(), + })).data.objects.find.results.map(object => object.someField) + ).toEqual([]); + expect( + (await findObjects('GraphQLClass', { + 'X-Parse-Session-Token': user5.getSessionToken(), + })).data.objects.find.results.map(object => object.someField) + ).toEqual(['someValue3']); + }); + + it('should support where argument using generic query', async () => { + await prepareData(); + + const result = await apolloClient.query({ + query: gql` + query FindSomeObjects($where: Object) { + objects { + find(className: "GraphQLClass", where: $where) { + results + } + } + } + `, + variables: { + where: { + someField: { + $in: ['someValue1', 'someValue2', 'someValue3'], + }, + $or: [ + { + pointerToUser: { + __type: 'Pointer', + className: '_User', + objectId: user5.id, + }, + }, + { + objectId: object1.id, + }, + ], + }, + }, + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, + }, + }); + + expect( + result.data.objects.find.results + .map(object => object.someField) + .sort() + ).toEqual(['someValue1', 'someValue3']); + }); + + it('should support where argument using class specific query', async () => { + await prepareData(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const result = await apolloClient.query({ + query: gql` + query FindSomeObjects($where: GraphQLClassConstraints) { + objects { + findGraphQLClass(where: $where) { + results { + someField + } + } + } + } + `, + variables: { + where: { + someField: { + _in: ['someValue1', 'someValue2', 'someValue3'], + }, + _or: [ + { + pointerToUser: { + _eq: { + __type: 'Pointer', + className: '_User', + objectId: user5.id, + }, + }, + }, + { + objectId: { + _eq: object1.id, + }, + }, + ], + }, + }, + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, + }, + }); + + expect( + result.data.objects.findGraphQLClass.results + .map(object => object.someField) + .sort() + ).toEqual(['someValue1', 'someValue3']); + }); + + it('should support order, skip and limit arguments', async () => { + const promises = []; + for (let i = 0; i < 100; i++) { + const obj = new Parse.Object('SomeClass'); + obj.set('someField', `someValue${i < 10 ? '0' : ''}${i}`); + obj.set('numberField', i % 3); + promises.push(obj.save()); + } + await Promise.all(promises); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const result = await apolloClient.query({ + query: gql` + query FindSomeObjects( + $className: String! + $where: Object + $whereCustom: SomeClassConstraints + $order: String + $orderCustom: [SomeClassOrder!] + $skip: Int + $limit: Int + ) { + objects { + find( + className: $className + where: $where + order: $order + skip: $skip + limit: $limit + ) { + results + } + findSomeClass( + where: $whereCustom + order: $orderCustom + skip: $skip + limit: $limit + ) { + results { + someField + } + } + } + } + `, + variables: { + className: 'SomeClass', + where: { + someField: { + $regex: '^someValue', + }, + }, + whereCustom: { + someField: { + _regex: '^someValue', + }, + }, + order: '-numberField,someField', + orderCustom: ['numberField_DESC', 'someField_ASC'], + skip: 4, + limit: 2, + }, + }); + + expect( + result.data.objects.find.results.map(obj => obj.someField) + ).toEqual(['someValue14', 'someValue17']); + expect( + result.data.objects.findSomeClass.results.map( + obj => obj.someField + ) + ).toEqual(['someValue14', 'someValue17']); + }); + + it('should support count', async () => { + await prepareData(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const where = { + someField: { + _in: ['someValue1', 'someValue2', 'someValue3'], + }, + _or: [ + { + pointerToUser: { + _eq: { + __type: 'Pointer', + className: '_User', + objectId: user5.id, + }, + }, + }, + { + objectId: { + _eq: object1.id, + }, + }, + ], + }; + + const result = await apolloClient.query({ + query: gql` + query FindSomeObjects( + $where1: Object + $where2: GraphQLClassConstraints + $limit: Int + ) { + objects { + find( + className: "GraphQLClass" + where: $where1 + limit: $limit + ) { + results + count + } + findGraphQLClass(where: $where2, limit: $limit) { + results { + objectId + } + count + } + } + } + `, + variables: { + where1: where, + where2: where, + limit: 0, + }, + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, + }, + }); + + expect(result.data.objects.find.results).toEqual([]); + expect(result.data.objects.find.count).toEqual(2); + expect(result.data.objects.findGraphQLClass.results).toEqual([]); + expect(result.data.objects.findGraphQLClass.count).toEqual(2); + }); + + it('should only count', async () => { + await prepareData(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const where = { + someField: { + _in: ['someValue1', 'someValue2', 'someValue3'], + }, + _or: [ + { + pointerToUser: { + _eq: { + __type: 'Pointer', + className: '_User', + objectId: user5.id, + }, + }, + }, + { + objectId: { + _eq: object1.id, + }, + }, + ], + }; + + const result = await apolloClient.query({ + query: gql` + query FindSomeObjects( + $where1: Object + $where2: GraphQLClassConstraints + ) { + objects { + find(className: "GraphQLClass", where: $where1) { + count + } + findGraphQLClass(where: $where2) { + count + } + } + } + `, + variables: { + where1: where, + where2: where, + }, + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, + }, + }); + + expect(result.data.objects.find.results).toBeUndefined(); + expect(result.data.objects.find.count).toEqual(2); + expect( + result.data.objects.findGraphQLClass.results + ).toBeUndefined(); + expect(result.data.objects.findGraphQLClass.count).toEqual(2); + }); + + it('should respect max limit', async () => { + parseServer = await global.reconfigureServer({ + maxLimit: 10, + }); + + const promises = []; + for (let i = 0; i < 100; i++) { + const obj = new Parse.Object('SomeClass'); + promises.push(obj.save()); + } + await Promise.all(promises); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const result = await apolloClient.query({ + query: gql` + query FindSomeObjects($limit: Int) { + objects { + find( + className: "SomeClass" + where: { objectId: { _exists: true } } + limit: $limit + ) { + results + count + } + findSomeClass( + where: { objectId: { _exists: true } } + limit: $limit + ) { + results { + objectId + } + count + } + } + } + `, + variables: { + limit: 50, + }, + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, + }, + }); + + expect(result.data.objects.find.results.length).toEqual(10); + expect(result.data.objects.find.count).toEqual(100); + expect(result.data.objects.findSomeClass.results.length).toEqual( + 10 + ); + expect(result.data.objects.findSomeClass.count).toEqual(100); + }); + + it('should support keys argument', async () => { + await prepareData(); + + const result1 = await apolloClient.query({ + query: gql` + query FindSomeObject($where: Object) { + objects { + find( + className: "GraphQLClass" + where: $where + keys: "someField" + ) { + results + } + } + } + `, + variables: { + where: { + objectId: object3.id, + }, + }, + context: { + headers: { + 'X-Parse-Session-Token': user1.getSessionToken(), + }, + }, + }); + + const result2 = await apolloClient.query({ + query: gql` + query FindSomeObject($where: Object) { + objects { + find( + className: "GraphQLClass" + where: $where + keys: "someField,pointerToUser" + ) { + results + } + } + } + `, + variables: { + where: { + objectId: object3.id, + }, + }, + context: { + headers: { + 'X-Parse-Session-Token': user1.getSessionToken(), + }, + }, + }); + + expect( + result1.data.objects.find.results[0].someField + ).toBeDefined(); + expect( + result1.data.objects.find.results[0].pointerToUser + ).toBeUndefined(); + expect( + result2.data.objects.find.results[0].someField + ).toBeDefined(); + expect( + result2.data.objects.find.results[0].pointerToUser + ).toBeDefined(); + }); + + it('should support include argument', async () => { + await prepareData(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const result1 = await apolloClient.query({ + query: gql` + query FindSomeObject($where: Object) { + objects { + find(className: "GraphQLClass", where: $where) { + results + } + } + } + `, + variables: { + where: { + objectId: object3.id, + }, + }, + context: { + headers: { + 'X-Parse-Session-Token': user1.getSessionToken(), + }, + }, + }); + + const where = { + objectId: { + _eq: object3.id, + }, + }; + + const result2 = await apolloClient.query({ + query: gql` + query FindSomeObject( + $where1: Object + $where2: GraphQLClassConstraints + ) { + objects { + find( + className: "GraphQLClass" + where: $where1 + include: "pointerToUser" + ) { + results + } + findGraphQLClass(where: $where2) { + results { + pointerToUser { + username + } + } + } + } + } + `, + variables: { + where1: where, + where2: where, + }, + context: { + headers: { + 'X-Parse-Session-Token': user1.getSessionToken(), + }, + }, + }); + + expect( + result1.data.objects.find.results[0].pointerToUser.username + ).toBeUndefined(); + expect( + result2.data.objects.find.results[0].pointerToUser.username + ).toBeDefined(); + expect( + result2.data.objects.findGraphQLClass.results[0].pointerToUser + .username + ).toBeDefined(); + }); + + it('should support includeAll argument', async () => { + const obj1 = new Parse.Object('SomeClass1'); + obj1.set('someField1', 'someValue1'); + const obj2 = new Parse.Object('SomeClass2'); + obj2.set('someField2', 'someValue2'); + const obj3 = new Parse.Object('SomeClass3'); + obj3.set('obj1', obj1); + obj3.set('obj2', obj2); + await Promise.all([obj1.save(), obj2.save(), obj3.save()]); + + const result1 = await apolloClient.query({ + query: gql` + query FindSomeObject { + objects { + find(className: "SomeClass3") { + results + } + } + } + `, + }); + + const result2 = await apolloClient.query({ + query: gql` + query FindSomeObject { + objects { + find(className: "SomeClass3", includeAll: true) { + results + } + } + } + `, + }); + + expect( + result1.data.objects.find.results[0].obj1.someField1 + ).toBeUndefined(); + expect( + result1.data.objects.find.results[0].obj2.someField2 + ).toBeUndefined(); + expect( + result2.data.objects.find.results[0].obj1.someField1 + ).toEqual('someValue1'); + expect( + result2.data.objects.find.results[0].obj2.someField2 + ).toEqual('someValue2'); + }); + + describe_only_db('mongo')('read preferences', () => { + it('should read from primary by default', async () => { + await prepareData(); + + const databaseAdapter = + parseServer.config.databaseController.adapter; + spyOn( + databaseAdapter.database.serverConfig, + 'cursor' + ).and.callThrough(); + + await apolloClient.query({ + query: gql` + query FindSomeObjects { + objects { + find( + className: "GraphQLClass" + include: "pointerToUser" + ) { + results + } + } + } + `, + context: { + headers: { + 'X-Parse-Session-Token': user1.getSessionToken(), + }, + }, + }); + + let foundGraphQLClassReadPreference = false; + let foundUserClassReadPreference = false; + databaseAdapter.database.serverConfig.cursor.calls + .all() + .forEach(call => { + if (call.args[0].indexOf('GraphQLClass') >= 0) { + foundGraphQLClassReadPreference = true; + expect(call.args[2].readPreference).toBe(null); + } else if (call.args[0].indexOf('_User') >= 0) { + foundUserClassReadPreference = true; + expect(call.args[2].readPreference).toBe(null); + } + }); + + expect(foundGraphQLClassReadPreference).toBe(true); + expect(foundUserClassReadPreference).toBe(true); + }); + + it('should support readPreference argument', async () => { + await prepareData(); + + const databaseAdapter = + parseServer.config.databaseController.adapter; + spyOn( + databaseAdapter.database.serverConfig, + 'cursor' + ).and.callThrough(); + + await apolloClient.query({ + query: gql` + query FindSomeObjects { + objects { + find( + className: "GraphQLClass" + include: "pointerToUser" + readPreference: SECONDARY + ) { + results + } + } + } + `, + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, + }, + }); + + let foundGraphQLClassReadPreference = false; + let foundUserClassReadPreference = false; + databaseAdapter.database.serverConfig.cursor.calls + .all() + .forEach(call => { + if (call.args[0].indexOf('GraphQLClass') >= 0) { + foundGraphQLClassReadPreference = true; + expect(call.args[2].readPreference.preference).toBe( + ReadPreference.SECONDARY + ); + } else if (call.args[0].indexOf('_User') >= 0) { + foundUserClassReadPreference = true; + expect(call.args[2].readPreference.preference).toBe( + ReadPreference.SECONDARY + ); + } + }); + + expect(foundGraphQLClassReadPreference).toBe(true); + expect(foundUserClassReadPreference).toBe(true); + }); + + it('should support includeReadPreference argument', async () => { + await prepareData(); + + const databaseAdapter = + parseServer.config.databaseController.adapter; + spyOn( + databaseAdapter.database.serverConfig, + 'cursor' + ).and.callThrough(); + + await apolloClient.query({ + query: gql` + query FindSomeObjects { + objects { + find( + className: "GraphQLClass" + include: "pointerToUser" + readPreference: SECONDARY + includeReadPreference: NEAREST + ) { + results + } + } + } + `, + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, + }, + }); + + let foundGraphQLClassReadPreference = false; + let foundUserClassReadPreference = false; + databaseAdapter.database.serverConfig.cursor.calls + .all() + .forEach(call => { + if (call.args[0].indexOf('GraphQLClass') >= 0) { + foundGraphQLClassReadPreference = true; + expect(call.args[2].readPreference.preference).toBe( + ReadPreference.SECONDARY + ); + } else if (call.args[0].indexOf('_User') >= 0) { + foundUserClassReadPreference = true; + expect(call.args[2].readPreference.preference).toBe( + ReadPreference.NEAREST + ); + } + }); + + expect(foundGraphQLClassReadPreference).toBe(true); + expect(foundUserClassReadPreference).toBe(true); + }); + + it('should support subqueryReadPreference argument', async () => { + await prepareData(); + + const databaseAdapter = + parseServer.config.databaseController.adapter; + spyOn( + databaseAdapter.database.serverConfig, + 'cursor' + ).and.callThrough(); + + await apolloClient.query({ + query: gql` + query FindSomeObjects($where: Object) { + objects { + find( + className: "GraphQLClass" + where: $where + readPreference: SECONDARY + subqueryReadPreference: NEAREST + ) { + count + } + } + } + `, + variables: { + where: { + pointerToUser: { + $inQuery: { where: {}, className: '_User' }, + }, + }, + }, + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, + }, + }); + + let foundGraphQLClassReadPreference = false; + let foundUserClassReadPreference = false; + databaseAdapter.database.serverConfig.cursor.calls + .all() + .forEach(call => { + if (call.args[0].indexOf('GraphQLClass') >= 0) { + foundGraphQLClassReadPreference = true; + expect(call.args[2].readPreference.preference).toBe( + ReadPreference.SECONDARY + ); + } else if (call.args[0].indexOf('_User') >= 0) { + foundUserClassReadPreference = true; + expect(call.args[2].readPreference.preference).toBe( + ReadPreference.NEAREST + ); + } + }); + + expect(foundGraphQLClassReadPreference).toBe(true); + expect(foundUserClassReadPreference).toBe(true); + }); + }); + }); + }); + + describe('Objects Mutations', () => { + describe('Create', () => { + it('should return CreateResult object using generic mutation', async () => { + const result = await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject($fields: Object) { + objects { + create(className: "SomeClass", fields: $fields) { + objectId + createdAt + } + } + } + `, + variables: { + fields: { + someField: 'someValue', + }, + }, + }); + + expect(result.data.objects.create.objectId).toBeDefined(); + + const obj = await new Parse.Query('SomeClass').get( + result.data.objects.create.objectId + ); + + expect(obj.createdAt).toEqual( + new Date(result.data.objects.create.createdAt) + ); + expect(obj.get('someField')).toEqual('someValue'); + }); + + it('should return CreateResult object using class specific mutation', async () => { + const customerSchema = new Parse.Schema('Customer'); + customerSchema.addString('someField'); + await customerSchema.save(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const result = await apolloClient.mutate({ + mutation: gql` + mutation CreateCustomer($fields: CustomerFields) { + objects { + createCustomer(fields: $fields) { + objectId + createdAt + } + } + } + `, + variables: { + fields: { + someField: 'someValue', + }, + }, + }); + + expect(result.data.objects.createCustomer.objectId).toBeDefined(); + + const customer = await new Parse.Query('Customer').get( + result.data.objects.createCustomer.objectId + ); + + expect(customer.createdAt).toEqual( + new Date(result.data.objects.createCustomer.createdAt) + ); + expect(customer.get('someField')).toEqual('someValue'); + }); + + it('should respect level permissions', async () => { + await prepareData(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + async function createObject(className, headers) { + const result = await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject($className: String!) { + objects { + create(className: $className) { + objectId + createdAt + } + create${className} { + objectId + createdAt + } + } + } + `, + variables: { + className, + }, + context: { + headers, + }, + }); + + const { create } = result.data.objects; + expect(create.objectId).toBeDefined(); + expect(create.createdAt).toBeDefined(); + + const specificCreate = result.data.objects[`create${className}`]; + expect(specificCreate.objectId).toBeDefined(); + expect(specificCreate.createdAt).toBeDefined(); + + return result; + } + + await expectAsync(createObject('GraphQLClass')).toBeRejectedWith( + jasmine.stringMatching( + 'Permission denied for action create on class GraphQLClass' + ) + ); + await expectAsync(createObject('PublicClass')).toBeResolved(); + await expectAsync( + createObject('GraphQLClass', { 'X-Parse-Master-Key': 'test' }) + ).toBeResolved(); + await expectAsync( + createObject('PublicClass', { 'X-Parse-Master-Key': 'test' }) + ).toBeResolved(); + await expectAsync( + createObject('GraphQLClass', { + 'X-Parse-Session-Token': user1.getSessionToken(), + }) + ).toBeResolved(); + await expectAsync( + createObject('PublicClass', { + 'X-Parse-Session-Token': user1.getSessionToken(), + }) + ).toBeResolved(); + await expectAsync( + createObject('GraphQLClass', { + 'X-Parse-Session-Token': user2.getSessionToken(), + }) + ).toBeResolved(); + await expectAsync( + createObject('PublicClass', { + 'X-Parse-Session-Token': user2.getSessionToken(), + }) + ).toBeResolved(); + await expectAsync( + createObject('GraphQLClass', { + 'X-Parse-Session-Token': user4.getSessionToken(), + }) + ).toBeRejectedWith( + jasmine.stringMatching( + 'Permission denied for action create on class GraphQLClass' + ) + ); + await expectAsync( + createObject('PublicClass', { + 'X-Parse-Session-Token': user4.getSessionToken(), + }) + ).toBeResolved(); + }); + }); + + describe('Update', () => { + it('should return UpdateResult object using generic mutation', async () => { + const obj = new Parse.Object('SomeClass'); + obj.set('someField1', 'someField1Value1'); + obj.set('someField2', 'someField2Value1'); + await obj.save(); + + const result = await apolloClient.mutate({ + mutation: gql` + mutation UpdateSomeObject($objectId: ID!, $fields: Object) { + objects { + update( + className: "SomeClass" + objectId: $objectId + fields: $fields + ) { + updatedAt + } + } + } + `, + variables: { + objectId: obj.id, + fields: { + someField1: 'someField1Value2', + }, + }, + }); + + expect(result.data.objects.update.updatedAt).toBeDefined(); + + await obj.fetch(); + + expect(obj.get('someField1')).toEqual('someField1Value2'); + expect(obj.get('someField2')).toEqual('someField2Value1'); + }); + + it('should return UpdateResult object using class specific mutation', async () => { + const obj = new Parse.Object('Customer'); + obj.set('someField1', 'someField1Value1'); + obj.set('someField2', 'someField2Value1'); + await obj.save(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const result = await apolloClient.mutate({ + mutation: gql` + mutation UpdateCustomer( + $objectId: ID! + $fields: CustomerFields + ) { + objects { + updateCustomer(objectId: $objectId, fields: $fields) { + updatedAt + } + } + } + `, + variables: { + objectId: obj.id, + fields: { + someField1: 'someField1Value2', + }, + }, + }); + + expect(result.data.objects.updateCustomer.updatedAt).toBeDefined(); + + await obj.fetch(); + + expect(obj.get('someField1')).toEqual('someField1Value2'); + expect(obj.get('someField2')).toEqual('someField2Value1'); + }); + + it('should respect level permissions', async () => { + await prepareData(); + + function updateObject(className, objectId, fields, headers) { + return apolloClient.mutate({ + mutation: gql` + mutation UpdateSomeObject( + $className: String! + $objectId: ID! + $fields: Object + ) { + objects { + update( + className: $className + objectId: $objectId + fields: $fields + ) { + updatedAt + } + } + } + `, + variables: { + className, + objectId, + fields, + }, + context: { + headers, + }, + }); + } + + await Promise.all( + objects.slice(0, 3).map(async obj => { + const originalFieldValue = obj.get('someField'); + await expectAsync( + updateObject(obj.className, obj.id, { + someField: 'changedValue1', + }) + ).toBeRejectedWith(jasmine.stringMatching('Object not found')); + await obj.fetch({ useMasterKey: true }); + expect(obj.get('someField')).toEqual(originalFieldValue); + }) + ); + expect( + (await updateObject(object4.className, object4.id, { + someField: 'changedValue1', + })).data.objects.update.updatedAt + ).toBeDefined(); + await object4.fetch({ useMasterKey: true }); + expect(object4.get('someField')).toEqual('changedValue1'); + await Promise.all( + objects.map(async obj => { + expect( + (await updateObject( + obj.className, + obj.id, + { someField: 'changedValue2' }, + { 'X-Parse-Master-Key': 'test' } + )).data.objects.update.updatedAt + ).toBeDefined(); + await obj.fetch({ useMasterKey: true }); + expect(obj.get('someField')).toEqual('changedValue2'); + }) + ); + await Promise.all( + objects.map(async obj => { + expect( + (await updateObject( + obj.className, + obj.id, + { someField: 'changedValue3' }, + { 'X-Parse-Session-Token': user1.getSessionToken() } + )).data.objects.update.updatedAt + ).toBeDefined(); + await obj.fetch({ useMasterKey: true }); + expect(obj.get('someField')).toEqual('changedValue3'); + }) + ); + await Promise.all( + objects.map(async obj => { + expect( + (await updateObject( + obj.className, + obj.id, + { someField: 'changedValue4' }, + { 'X-Parse-Session-Token': user2.getSessionToken() } + )).data.objects.update.updatedAt + ).toBeDefined(); + await obj.fetch({ useMasterKey: true }); + expect(obj.get('someField')).toEqual('changedValue4'); + }) + ); + await Promise.all( + [object1, object3, object4].map(async obj => { + expect( + (await updateObject( + obj.className, + obj.id, + { someField: 'changedValue5' }, + { 'X-Parse-Session-Token': user3.getSessionToken() } + )).data.objects.update.updatedAt + ).toBeDefined(); + await obj.fetch({ useMasterKey: true }); + expect(obj.get('someField')).toEqual('changedValue5'); + }) + ); + const originalFieldValue = object2.get('someField'); + await expectAsync( + updateObject( + object2.className, + object2.id, + { someField: 'changedValue5' }, + { 'X-Parse-Session-Token': user3.getSessionToken() } + ) + ).toBeRejectedWith(jasmine.stringMatching('Object not found')); + await object2.fetch({ useMasterKey: true }); + expect(object2.get('someField')).toEqual(originalFieldValue); + await Promise.all( + objects.slice(0, 3).map(async obj => { + const originalFieldValue = obj.get('someField'); + await expectAsync( + updateObject( + obj.className, + obj.id, + { someField: 'changedValue6' }, + { 'X-Parse-Session-Token': user4.getSessionToken() } + ) + ).toBeRejectedWith(jasmine.stringMatching('Object not found')); + await obj.fetch({ useMasterKey: true }); + expect(obj.get('someField')).toEqual(originalFieldValue); + }) + ); + expect( + (await updateObject( + object4.className, + object4.id, + { someField: 'changedValue6' }, + { 'X-Parse-Session-Token': user4.getSessionToken() } + )).data.objects.update.updatedAt + ).toBeDefined(); + await object4.fetch({ useMasterKey: true }); + expect(object4.get('someField')).toEqual('changedValue6'); + await Promise.all( + objects.slice(0, 2).map(async obj => { + const originalFieldValue = obj.get('someField'); + await expectAsync( + updateObject( + obj.className, + obj.id, + { someField: 'changedValue7' }, + { 'X-Parse-Session-Token': user5.getSessionToken() } + ) + ).toBeRejectedWith(jasmine.stringMatching('Object not found')); + await obj.fetch({ useMasterKey: true }); + expect(obj.get('someField')).toEqual(originalFieldValue); + }) + ); + expect( + (await updateObject( + object3.className, + object3.id, + { someField: 'changedValue7' }, + { 'X-Parse-Session-Token': user5.getSessionToken() } + )).data.objects.update.updatedAt + ).toBeDefined(); + await object3.fetch({ useMasterKey: true }); + expect(object3.get('someField')).toEqual('changedValue7'); + expect( + (await updateObject( + object4.className, + object4.id, + { someField: 'changedValue7' }, + { 'X-Parse-Session-Token': user5.getSessionToken() } + )).data.objects.update.updatedAt + ).toBeDefined(); + await object4.fetch({ useMasterKey: true }); + expect(object4.get('someField')).toEqual('changedValue7'); + }); + + it('should respect level permissions with specific class mutation', async () => { + await prepareData(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + function updateObject(className, objectId, fields, headers) { + return apolloClient.mutate({ + mutation: gql` + mutation UpdateSomeObject( + $objectId: ID! + $fields: ${className}Fields + ) { + objects { + update${className}( + objectId: $objectId + fields: $fields + ) { + updatedAt + } + } + } + `, + variables: { + objectId, + fields, + }, + context: { + headers, + }, + }); + } + + await Promise.all( + objects.slice(0, 3).map(async obj => { + const originalFieldValue = obj.get('someField'); + await expectAsync( + updateObject(obj.className, obj.id, { + someField: 'changedValue1', + }) + ).toBeRejectedWith(jasmine.stringMatching('Object not found')); + await obj.fetch({ useMasterKey: true }); + expect(obj.get('someField')).toEqual(originalFieldValue); + }) + ); + expect( + (await updateObject(object4.className, object4.id, { + someField: 'changedValue1', + })).data.objects[`update${object4.className}`].updatedAt + ).toBeDefined(); + await object4.fetch({ useMasterKey: true }); + expect(object4.get('someField')).toEqual('changedValue1'); + await Promise.all( + objects.map(async obj => { + expect( + (await updateObject( + obj.className, + obj.id, + { someField: 'changedValue2' }, + { 'X-Parse-Master-Key': 'test' } + )).data.objects[`update${obj.className}`].updatedAt + ).toBeDefined(); + await obj.fetch({ useMasterKey: true }); + expect(obj.get('someField')).toEqual('changedValue2'); + }) + ); + await Promise.all( + objects.map(async obj => { + expect( + (await updateObject( + obj.className, + obj.id, + { someField: 'changedValue3' }, + { 'X-Parse-Session-Token': user1.getSessionToken() } + )).data.objects[`update${obj.className}`].updatedAt + ).toBeDefined(); + await obj.fetch({ useMasterKey: true }); + expect(obj.get('someField')).toEqual('changedValue3'); + }) + ); + await Promise.all( + objects.map(async obj => { + expect( + (await updateObject( + obj.className, + obj.id, + { someField: 'changedValue4' }, + { 'X-Parse-Session-Token': user2.getSessionToken() } + )).data.objects[`update${obj.className}`].updatedAt + ).toBeDefined(); + await obj.fetch({ useMasterKey: true }); + expect(obj.get('someField')).toEqual('changedValue4'); + }) + ); + await Promise.all( + [object1, object3, object4].map(async obj => { + expect( + (await updateObject( + obj.className, + obj.id, + { someField: 'changedValue5' }, + { 'X-Parse-Session-Token': user3.getSessionToken() } + )).data.objects[`update${obj.className}`].updatedAt + ).toBeDefined(); + await obj.fetch({ useMasterKey: true }); + expect(obj.get('someField')).toEqual('changedValue5'); + }) + ); + const originalFieldValue = object2.get('someField'); + await expectAsync( + updateObject( + object2.className, + object2.id, + { someField: 'changedValue5' }, + { 'X-Parse-Session-Token': user3.getSessionToken() } + ) + ).toBeRejectedWith(jasmine.stringMatching('Object not found')); + await object2.fetch({ useMasterKey: true }); + expect(object2.get('someField')).toEqual(originalFieldValue); + await Promise.all( + objects.slice(0, 3).map(async obj => { + const originalFieldValue = obj.get('someField'); + await expectAsync( + updateObject( + obj.className, + obj.id, + { someField: 'changedValue6' }, + { 'X-Parse-Session-Token': user4.getSessionToken() } + ) + ).toBeRejectedWith(jasmine.stringMatching('Object not found')); + await obj.fetch({ useMasterKey: true }); + expect(obj.get('someField')).toEqual(originalFieldValue); + }) + ); + expect( + (await updateObject( + object4.className, + object4.id, + { someField: 'changedValue6' }, + { 'X-Parse-Session-Token': user4.getSessionToken() } + )).data.objects[`update${object4.className}`].updatedAt + ).toBeDefined(); + await object4.fetch({ useMasterKey: true }); + expect(object4.get('someField')).toEqual('changedValue6'); + await Promise.all( + objects.slice(0, 2).map(async obj => { + const originalFieldValue = obj.get('someField'); + await expectAsync( + updateObject( + obj.className, + obj.id, + { someField: 'changedValue7' }, + { 'X-Parse-Session-Token': user5.getSessionToken() } + ) + ).toBeRejectedWith(jasmine.stringMatching('Object not found')); + await obj.fetch({ useMasterKey: true }); + expect(obj.get('someField')).toEqual(originalFieldValue); + }) + ); + expect( + (await updateObject( + object3.className, + object3.id, + { someField: 'changedValue7' }, + { 'X-Parse-Session-Token': user5.getSessionToken() } + )).data.objects[`update${object3.className}`].updatedAt + ).toBeDefined(); + await object3.fetch({ useMasterKey: true }); + expect(object3.get('someField')).toEqual('changedValue7'); + expect( + (await updateObject( + object4.className, + object4.id, + { someField: 'changedValue7' }, + { 'X-Parse-Session-Token': user5.getSessionToken() } + )).data.objects[`update${object4.className}`].updatedAt + ).toBeDefined(); + await object4.fetch({ useMasterKey: true }); + expect(object4.get('someField')).toEqual('changedValue7'); + }); + }); + + describe('Delete', () => { + it('should return a boolean confirmation using generic mutation', async () => { + const obj = new Parse.Object('SomeClass'); + await obj.save(); + + const result = await apolloClient.mutate({ + mutation: gql` + mutation DeleteSomeObject($objectId: ID!) { + objects { + delete(className: "SomeClass", objectId: $objectId) + } + } + `, + variables: { + objectId: obj.id, + }, + }); + + expect(result.data.objects.delete).toEqual(true); + + await expectAsync( + obj.fetch({ useMasterKey: true }) + ).toBeRejectedWith(jasmine.stringMatching('Object not found')); + }); + + it('should return a boolean confirmation using class specific mutation', async () => { + const obj = new Parse.Object('Customer'); + await obj.save(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const result = await apolloClient.mutate({ + mutation: gql` + mutation DeleteCustomer($objectId: ID!) { + objects { + deleteCustomer(objectId: $objectId) + } + } + `, + variables: { + objectId: obj.id, + }, + }); + + expect(result.data.objects.deleteCustomer).toEqual(true); + + await expectAsync( + obj.fetch({ useMasterKey: true }) + ).toBeRejectedWith(jasmine.stringMatching('Object not found')); + }); + + it('should respect level permissions', async () => { + await prepareData(); + + function deleteObject(className, objectId, headers) { + return apolloClient.mutate({ + mutation: gql` + mutation DeleteSomeObject( + $className: String! + $objectId: ID! + ) { + objects { + delete(className: $className, objectId: $objectId) + } + } + `, + variables: { + className, + objectId, + }, + context: { + headers, + }, + }); + } + + await Promise.all( + objects.slice(0, 3).map(async obj => { + const originalFieldValue = obj.get('someField'); + await expectAsync( + deleteObject(obj.className, obj.id) + ).toBeRejectedWith(jasmine.stringMatching('Object not found')); + await obj.fetch({ useMasterKey: true }); + expect(obj.get('someField')).toEqual(originalFieldValue); + }) + ); + await Promise.all( + objects.slice(0, 3).map(async obj => { + const originalFieldValue = obj.get('someField'); + await expectAsync( + deleteObject(obj.className, obj.id, { + 'X-Parse-Session-Token': user4.getSessionToken(), + }) + ).toBeRejectedWith(jasmine.stringMatching('Object not found')); + await obj.fetch({ useMasterKey: true }); + expect(obj.get('someField')).toEqual(originalFieldValue); + }) + ); + expect( + (await deleteObject(object4.className, object4.id)).data.objects + .delete + ).toEqual(true); + await expectAsync( + object4.fetch({ useMasterKey: true }) + ).toBeRejectedWith(jasmine.stringMatching('Object not found')); + expect( + (await deleteObject(object1.className, object1.id, { + 'X-Parse-Master-Key': 'test', + })).data.objects.delete + ).toEqual(true); + await expectAsync( + object1.fetch({ useMasterKey: true }) + ).toBeRejectedWith(jasmine.stringMatching('Object not found')); + expect( + (await deleteObject(object2.className, object2.id, { + 'X-Parse-Session-Token': user2.getSessionToken(), + })).data.objects.delete + ).toEqual(true); + await expectAsync( + object2.fetch({ useMasterKey: true }) + ).toBeRejectedWith(jasmine.stringMatching('Object not found')); + expect( + (await deleteObject(object3.className, object3.id, { + 'X-Parse-Session-Token': user5.getSessionToken(), + })).data.objects.delete + ).toEqual(true); + await expectAsync( + object3.fetch({ useMasterKey: true }) + ).toBeRejectedWith(jasmine.stringMatching('Object not found')); + }); + + it('should respect level permissions with specific class mutation', async () => { + await prepareData(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + function deleteObject(className, objectId, headers) { + return apolloClient.mutate({ + mutation: gql` + mutation DeleteSomeObject( + $objectId: ID! + ) { + objects { + delete${className}(objectId: $objectId) + } + } + `, + variables: { + objectId, + }, + context: { + headers, + }, + }); + } + + await Promise.all( + objects.slice(0, 3).map(async obj => { + const originalFieldValue = obj.get('someField'); + await expectAsync( + deleteObject(obj.className, obj.id) + ).toBeRejectedWith(jasmine.stringMatching('Object not found')); + await obj.fetch({ useMasterKey: true }); + expect(obj.get('someField')).toEqual(originalFieldValue); + }) + ); + await Promise.all( + objects.slice(0, 3).map(async obj => { + const originalFieldValue = obj.get('someField'); + await expectAsync( + deleteObject(obj.className, obj.id, { + 'X-Parse-Session-Token': user4.getSessionToken(), + }) + ).toBeRejectedWith(jasmine.stringMatching('Object not found')); + await obj.fetch({ useMasterKey: true }); + expect(obj.get('someField')).toEqual(originalFieldValue); + }) + ); + expect( + (await deleteObject(object4.className, object4.id)).data.objects[ + `delete${object4.className}` + ] + ).toEqual(true); + await expectAsync( + object4.fetch({ useMasterKey: true }) + ).toBeRejectedWith(jasmine.stringMatching('Object not found')); + expect( + (await deleteObject(object1.className, object1.id, { + 'X-Parse-Master-Key': 'test', + })).data.objects[`delete${object1.className}`] + ).toEqual(true); + await expectAsync( + object1.fetch({ useMasterKey: true }) + ).toBeRejectedWith(jasmine.stringMatching('Object not found')); + expect( + (await deleteObject(object2.className, object2.id, { + 'X-Parse-Session-Token': user2.getSessionToken(), + })).data.objects[`delete${object2.className}`] + ).toEqual(true); + await expectAsync( + object2.fetch({ useMasterKey: true }) + ).toBeRejectedWith(jasmine.stringMatching('Object not found')); + expect( + (await deleteObject(object3.className, object3.id, { + 'X-Parse-Session-Token': user5.getSessionToken(), + })).data.objects[`delete${object3.className}`] + ).toEqual(true); + await expectAsync( + object3.fetch({ useMasterKey: true }) + ).toBeRejectedWith(jasmine.stringMatching('Object not found')); + }); + }); + }); + + describe('Files Mutations', () => { + describe('Create', () => { + it('should return File object', async () => { + parseServer = await global.reconfigureServer({ + publicServerURL: 'http://localhost:13377/parse', + }); + + const body = new FormData(); + body.append( + 'operations', + JSON.stringify({ + query: ` + mutation CreateFile($file: Upload!) { + files { + create(file: $file) { + name + url + } + } + } + `, + variables: { + file: null, + }, + }) + ); + body.append('map', JSON.stringify({ 1: ['variables.file'] })); + body.append('1', 'My File Content', { + filename: 'myFileName.txt', + contentType: 'text/plain', + }); + + let res = await fetch('http://localhost:13377/graphql', { + method: 'POST', + headers, + body, + }); + + expect(res.status).toEqual(200); + + const result = JSON.parse(await res.text()); + + expect(result.data.files.create.name).toEqual( + jasmine.stringMatching(/_myFileName.txt$/) + ); + expect(result.data.files.create.url).toEqual( + jasmine.stringMatching(/_myFileName.txt$/) + ); + + res = await fetch(result.data.files.create.url); + + expect(res.status).toEqual(200); + expect(await res.text()).toEqual('My File Content'); + }); + }); + }); + + describe('Users Queries', () => { + it('should return current logged user', async () => { + const userName = 'user1', + password = 'user1', + email = 'emailUser1@parse.com'; + + const user = new Parse.User(); + user.setUsername(userName); + user.setPassword(password); + user.setEmail(email); + await user.signUp(); + + const session = await Parse.Session.current(); + const result = await apolloClient.query({ + query: gql` + query GetCurrentUser { + users { + me { + objectId + username + email + } + } + } + `, + context: { + headers: { + 'X-Parse-Session-Token': session.getSessionToken(), + }, + }, + }); + + const { + objectId, + username: resultUserName, + email: resultEmail, + } = result.data.users.me; + expect(objectId).toBeDefined(); + expect(resultUserName).toEqual(userName); + expect(resultEmail).toEqual(email); + }); + }); + + describe('Users Mutations', () => { + it('should sign user up', async () => { + const result = await apolloClient.mutate({ + mutation: gql` + mutation SignUp($fields: _UserFields) { + users { + signUp(fields: $fields) { + sessionToken + } + } + } + `, + variables: { + fields: { + username: 'user1', + password: 'user1', + }, + }, + }); + + expect(result.data.users.signUp.sessionToken).toBeDefined(); + expect(typeof result.data.users.signUp.sessionToken).toBe('string'); + }); + + it('should log the user in', async () => { + const user = new Parse.User(); + user.setUsername('user1'); + user.setPassword('user1'); + await user.signUp(); + await Parse.User.logOut(); + + const result = await apolloClient.mutate({ + mutation: gql` + mutation LogInUser($username: String!, $password: String!) { + users { + logIn(username: $username, password: $password) { + sessionToken + } + } + } + `, + variables: { + username: 'user1', + password: 'user1', + }, + }); + + expect(result.data.users.logIn.sessionToken).toBeDefined(); + expect(typeof result.data.users.logIn.sessionToken).toBe('string'); + }); + + it('should log the user out', async () => { + const user = new Parse.User(); + user.setUsername('user1'); + user.setPassword('user1'); + await user.signUp(); + await Parse.User.logOut(); + + const logIn = await apolloClient.mutate({ + mutation: gql` + mutation LogInUser($username: String!, $password: String!) { + users { + logIn(username: $username, password: $password) { + sessionToken + } + } + } + `, + variables: { + username: 'user1', + password: 'user1', + }, + }); + + const sessionToken = logIn.data.users.logIn.sessionToken; + + const logOut = await apolloClient.mutate({ + mutation: gql` + mutation LogOutUser { + users { + logOut + } + } + `, + context: { + headers: { + 'X-Parse-Session-Token': sessionToken, + }, + }, + }); + expect(logOut.data.users.logOut).toBeTruthy(); + + await expectAsync( + apolloClient.query({ + query: gql` + query GetCurrentUser { + users { + me { + username + } + } + } + `, + context: { + headers: { + 'X-Parse-Session-Token': sessionToken, + }, + }, + }) + ).toBeRejected(); + }); + }); + + describe('Data Types', () => { + it('should support String', async () => { + const someFieldValue = 'some string'; + + const createResult = await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject($fields: Object) { + objects { + create(className: "SomeClass", fields: $fields) { + objectId + } + } + } + `, + variables: { + fields: { + someField: someFieldValue, + }, + }, + }); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const schema = await new Parse.Schema('SomeClass').get(); + expect(schema.fields.someField.type).toEqual('String'); + + await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject($fields: SomeClassFields) { + objects { + createSomeClass(fields: $fields) { + objectId + } + } + } + `, + variables: { + fields: { + someField: someFieldValue, + }, + }, + }); + + const getResult = await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!, $someFieldValue: String) { + objects { + get(className: "SomeClass", objectId: $objectId) + findSomeClass( + where: { someField: { _eq: $someFieldValue } } + ) { + results { + someField + } + } + } + } + `, + variables: { + objectId: createResult.data.objects.create.objectId, + someFieldValue, + }, + }); + + expect(typeof getResult.data.objects.get.someField).toEqual('string'); + expect(getResult.data.objects.get.someField).toEqual(someFieldValue); + expect(getResult.data.objects.findSomeClass.results.length).toEqual( + 2 + ); + }); + + it('should support Int numbers', async () => { + const someFieldValue = 123; + + const createResult = await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject($fields: Object) { + objects { + create(className: "SomeClass", fields: $fields) { + objectId + } + } + } + `, + variables: { + fields: { + someField: someFieldValue, + }, + }, + }); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject($fields: SomeClassFields) { + objects { + createSomeClass(fields: $fields) { + objectId + } + } + } + `, + variables: { + fields: { + someField: someFieldValue, + }, + }, + }); + + const schema = await new Parse.Schema('SomeClass').get(); + expect(schema.fields.someField.type).toEqual('Number'); + + const getResult = await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!, $someFieldValue: Float) { + objects { + get(className: "SomeClass", objectId: $objectId) + findSomeClass( + where: { someField: { _eq: $someFieldValue } } + ) { + results { + someField + } + } + } + } + `, + variables: { + objectId: createResult.data.objects.create.objectId, + someFieldValue, + }, + }); + + expect(typeof getResult.data.objects.get.someField).toEqual('number'); + expect(getResult.data.objects.get.someField).toEqual(someFieldValue); + expect(getResult.data.objects.findSomeClass.results.length).toEqual( + 2 + ); + }); + + it('should support Float numbers', async () => { + const someFieldValue = 123.4; + + const createResult = await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject($fields: Object) { + objects { + create(className: "SomeClass", fields: $fields) { + objectId + } + } + } + `, + variables: { + fields: { + someField: someFieldValue, + }, + }, + }); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const schema = await new Parse.Schema('SomeClass').get(); + expect(schema.fields.someField.type).toEqual('Number'); + + await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject($fields: SomeClassFields) { + objects { + createSomeClass(fields: $fields) { + objectId + } + } + } + `, + variables: { + fields: { + someField: someFieldValue, + }, + }, + }); + + const getResult = await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!, $someFieldValue: Float) { + objects { + get(className: "SomeClass", objectId: $objectId) + findSomeClass( + where: { someField: { _eq: $someFieldValue } } + ) { + results { + someField + } + } + } + } + `, + variables: { + objectId: createResult.data.objects.create.objectId, + someFieldValue, + }, + }); + + expect(typeof getResult.data.objects.get.someField).toEqual('number'); + expect(getResult.data.objects.get.someField).toEqual(someFieldValue); + expect(getResult.data.objects.findSomeClass.results.length).toEqual( + 2 + ); + }); + + it('should support Boolean', async () => { + const someFieldValueTrue = true; + const someFieldValueFalse = false; + + const createResult = await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject($fields: Object) { + objects { + create(className: "SomeClass", fields: $fields) { + objectId + } + } + } + `, + variables: { + fields: { + someFieldTrue: someFieldValueTrue, + someFieldFalse: someFieldValueFalse, + }, + }, + }); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const schema = await new Parse.Schema('SomeClass').get(); + expect(schema.fields.someFieldTrue.type).toEqual('Boolean'); + expect(schema.fields.someFieldFalse.type).toEqual('Boolean'); + + await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject($fields: SomeClassFields) { + objects { + createSomeClass(fields: $fields) { + objectId + } + } + } + `, + variables: { + fields: { + someFieldTrue: someFieldValueTrue, + someFieldFalse: someFieldValueFalse, + }, + }, + }); + + const getResult = await apolloClient.query({ + query: gql` + query GetSomeObject( + $objectId: ID! + $someFieldValueTrue: Boolean + $someFieldValueFalse: Boolean + ) { + objects { + get(className: "SomeClass", objectId: $objectId) + findSomeClass( + where: { + someFieldTrue: { _eq: $someFieldValueTrue } + someFieldFalse: { _eq: $someFieldValueFalse } + } + ) { + results { + objectId + } + } + } + } + `, + variables: { + objectId: createResult.data.objects.create.objectId, + someFieldValueTrue, + someFieldValueFalse, + }, + }); + + expect(typeof getResult.data.objects.get.someFieldTrue).toEqual( + 'boolean' + ); + expect(typeof getResult.data.objects.get.someFieldFalse).toEqual( + 'boolean' + ); + expect(getResult.data.objects.get.someFieldTrue).toEqual(true); + expect(getResult.data.objects.get.someFieldFalse).toEqual(false); + expect(getResult.data.objects.findSomeClass.results.length).toEqual( + 2 + ); + }); + + it('should support Date', async () => { + const someFieldValue = { + __type: 'Date', + iso: new Date().toISOString(), + }; + + const createResult = await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject($fields: Object) { + objects { + create(className: "SomeClass", fields: $fields) { + objectId + } + } + } + `, + variables: { + fields: { + someField: someFieldValue, + }, + }, + }); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const schema = await new Parse.Schema('SomeClass').get(); + expect(schema.fields.someField.type).toEqual('Date'); + + await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject($fields: SomeClassFields) { + objects { + createSomeClass(fields: $fields) { + objectId + } + } + } + `, + variables: { + fields: { + someField: someFieldValue, + }, + }, + }); + + const getResult = await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!) { + objects { + get(className: "SomeClass", objectId: $objectId) + findSomeClass(where: { someField: { _exists: true } }) { + results { + objectId + } + } + } + } + `, + variables: { + objectId: createResult.data.objects.create.objectId, + }, + }); + + expect(typeof getResult.data.objects.get.someField).toEqual('object'); + expect(getResult.data.objects.get.someField).toEqual(someFieldValue); + expect(getResult.data.objects.findSomeClass.results.length).toEqual( + 2 + ); + }); + + it('should support createdAt', async () => { + const createResult = await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject($fields: Object) { + objects { + create(className: "SomeClass", fields: $fields) { + createdAt + } + } + } + `, + }); + + const schema = await new Parse.Schema('SomeClass').get(); + expect(schema.fields.createdAt.type).toEqual('Date'); + + const { createdAt } = createResult.data.objects.create; + expect(Date.parse(createdAt)).not.toEqual(NaN); + }); + + it('should support updatedAt', async () => { + const createResult = await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject($fields: Object) { + objects { + create(className: "SomeClass", fields: $fields) { + objectId + } + } + } + `, + }); + + const schema = await new Parse.Schema('SomeClass').get(); + expect(schema.fields.updatedAt.type).toEqual('Date'); + + const getResult = await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!) { + objects { + get(className: "SomeClass", objectId: $objectId) + } + } + `, + variables: { + objectId: createResult.data.objects.create.objectId, + }, + }); + + expect(typeof getResult.data.objects.get.updatedAt).toEqual('string'); + expect(Date.parse(getResult.data.objects.get.updatedAt)).not.toEqual( + NaN + ); + }); + + it('should support pointer values', async () => { + const parent = new Parse.Object('ParentClass'); + await parent.save(); + + const pointerFieldValue = { + __type: 'Pointer', + className: 'ParentClass', + objectId: parent.id, + }; + + const createResult = await apolloClient.mutate({ + mutation: gql` + mutation CreateChildObject($fields: Object) { + objects { + create(className: "ChildClass", fields: $fields) { + objectId + } + } + } + `, + variables: { + fields: { + pointerField: pointerFieldValue, + }, + }, + }); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const schema = await new Parse.Schema('ChildClass').get(); + expect(schema.fields.pointerField.type).toEqual('Pointer'); + expect(schema.fields.pointerField.targetClass).toEqual('ParentClass'); + + await apolloClient.mutate({ + mutation: gql` + mutation CreateChildObject( + $fields1: ChildClassFields + $fields2: ChildClassFields + ) { + objects { + createChildClass1: createChildClass(fields: $fields1) { + objectId + } + createChildClass2: createChildClass(fields: $fields2) { + objectId + } + } + } + `, + variables: { + fields1: { + pointerField: pointerFieldValue, + }, + fields2: { + pointerField: pointerFieldValue.objectId, + }, + }, + }); + + const getResult = await apolloClient.query({ + query: gql` + query GetChildObject( + $objectId: ID! + $pointerFieldValue1: ParentClassPointer + $pointerFieldValue2: ParentClassPointer + ) { + objects { + get(className: "ChildClass", objectId: $objectId) + findChildClass1: findChildClass( + where: { pointerField: { _eq: $pointerFieldValue1 } } + ) { + results { + pointerField { + objectId + createdAt + } + } + } + findChildClass2: findChildClass( + where: { pointerField: { _eq: $pointerFieldValue2 } } + ) { + results { + pointerField { + objectId + createdAt + } + } + } + } + } + `, + variables: { + objectId: createResult.data.objects.create.objectId, + pointerFieldValue1: pointerFieldValue, + pointerFieldValue2: pointerFieldValue.objectId, + }, + }); + + expect(typeof getResult.data.objects.get.pointerField).toEqual( + 'object' + ); + expect(getResult.data.objects.get.pointerField).toEqual( + pointerFieldValue + ); + expect(getResult.data.objects.findChildClass1.results.length).toEqual( + 3 + ); + expect(getResult.data.objects.findChildClass2.results.length).toEqual( + 3 + ); + }); + + it_only_db('mongo')('should support relation', async () => { + const someObject1 = new Parse.Object('SomeClass'); + await someObject1.save(); + const someObject2 = new Parse.Object('SomeClass'); + await someObject2.save(); + + const pointerValue1 = { + __type: 'Pointer', + className: 'SomeClass', + objectId: someObject1.id, + }; + const pointerValue2 = { + __type: 'Pointer', + className: 'SomeClass', + objectId: someObject2.id, + }; + + const createResult = await apolloClient.mutate({ + mutation: gql` + mutation CreateMainObject($fields: Object) { + objects { + create(className: "MainClass", fields: $fields) { + objectId + } + } + } + `, + variables: { + fields: { + relationField: { + __op: 'Batch', + ops: [ + { + __op: 'AddRelation', + objects: [pointerValue1], + }, + { + __op: 'AddRelation', + objects: [pointerValue2], + }, + ], + }, + }, + }, + }); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const schema = await new Parse.Schema('MainClass').get(); + expect(schema.fields.relationField.type).toEqual('Relation'); + expect(schema.fields.relationField.targetClass).toEqual('SomeClass'); + + await apolloClient.mutate({ + mutation: gql` + mutation CreateMainObject($fields: MainClassFields) { + objects { + createMainClass(fields: $fields) { + objectId + } + } + } + `, + variables: { + fields: { + relationField: { + _op: 'Batch', + ops: [ + { + _op: 'AddRelation', + objects: [pointerValue1], + }, + { + _op: 'RemoveRelation', + objects: [pointerValue1], + }, + { + _op: 'AddRelation', + objects: [pointerValue2], + }, + ], + }, + }, + }, + }); + + const getResult = await apolloClient.query({ + query: gql` + query GetMainObject($objectId: ID!) { + objects { + get(className: "MainClass", objectId: $objectId) + getMainClass(objectId: $objectId) { + relationField { + results { + objectId + createdAt + } + count + } + } + } + } + `, + variables: { + objectId: createResult.data.objects.create.objectId, + }, + }); + + expect(typeof getResult.data.objects.get.relationField).toEqual( + 'object' + ); + expect(getResult.data.objects.get.relationField).toEqual({ + __type: 'Relation', + className: 'SomeClass', + }); + expect( + getResult.data.objects.getMainClass.relationField.results.length + ).toEqual(2); + expect( + getResult.data.objects.getMainClass.relationField.count + ).toEqual(2); + + const findResult = await apolloClient.query({ + query: gql` + query FindSomeObjects($where: Object) { + objects { + find(className: "SomeClass", where: $where) { + results + } + } + } + `, + variables: { + where: { + $relatedTo: { + object: { + __type: 'Pointer', + className: 'MainClass', + objectId: createResult.data.objects.create.objectId, + }, + key: 'relationField', + }, + }, + }, + }); + + const compare = (obj1, obj2) => + obj1.createdAt > obj2.createdAt ? 1 : -1; + + expect(findResult.data.objects.find.results).toEqual( + jasmine.any(Array) + ); + expect(findResult.data.objects.find.results.sort(compare)).toEqual( + [ + { + objectId: someObject1.id, + createdAt: someObject1.createdAt.toISOString(), + updatedAt: someObject1.updatedAt.toISOString(), + }, + { + objectId: someObject2.id, + createdAt: someObject2.createdAt.toISOString(), + updatedAt: someObject2.updatedAt.toISOString(), + }, + ].sort(compare) + ); + }); + + it('should support files', async () => { + parseServer = await global.reconfigureServer({ + publicServerURL: 'http://localhost:13377/parse', + }); + + const body = new FormData(); + body.append( + 'operations', + JSON.stringify({ + query: ` + mutation CreateFile($file: Upload!) { + files { + create(file: $file) { + name + url + } + } + } + `, + variables: { + file: null, + }, + }) + ); + body.append('map', JSON.stringify({ 1: ['variables.file'] })); + body.append('1', 'My File Content', { + filename: 'myFileName.txt', + contentType: 'text/plain', + }); + + let res = await fetch('http://localhost:13377/graphql', { + method: 'POST', + headers, + body, + }); + + expect(res.status).toEqual(200); + + const result = JSON.parse(await res.text()); + + expect(result.data.files.create.name).toEqual( + jasmine.stringMatching(/_myFileName.txt$/) + ); + expect(result.data.files.create.url).toEqual( + jasmine.stringMatching(/_myFileName.txt$/) + ); + + const someFieldValue = { + __type: 'File', + name: result.data.files.create.name, + url: result.data.files.create.url, + }; + + const createResult = await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject($fields: Object) { + objects { + create(className: "SomeClass", fields: $fields) { + objectId + } + } + } + `, + variables: { + fields: { + someField: someFieldValue, + }, + }, + }); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject( + $fields1: SomeClassFields + $fields2: SomeClassFields + ) { + objects { + createSomeClass1: createSomeClass(fields: $fields1) { + objectId + } + createSomeClass2: createSomeClass(fields: $fields2) { + objectId + } + } + } + `, + variables: { + fields1: { + someField: someFieldValue, + }, + fields2: { + someField: someFieldValue.name, + }, + }, + }); + + const schema = await new Parse.Schema('SomeClass').get(); + expect(schema.fields.someField.type).toEqual('File'); + + const getResult = await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!) { + objects { + get(className: "SomeClass", objectId: $objectId) + findSomeClass1: findSomeClass( + where: { someField: { _exists: true } } + ) { + results { + someField { + name + url + } + } + } + findSomeClass2: findSomeClass( + where: { someField: { _exists: true } } + ) { + results { + someField { + name + url + } + } + } + } + } + `, + variables: { + objectId: createResult.data.objects.create.objectId, + }, + }); + + expect(typeof getResult.data.objects.get.someField).toEqual('object'); + expect(getResult.data.objects.get.someField).toEqual(someFieldValue); + expect(getResult.data.objects.findSomeClass1.results.length).toEqual( + 3 + ); + expect(getResult.data.objects.findSomeClass2.results.length).toEqual( + 3 + ); + + res = await fetch(getResult.data.objects.get.someField.url); + + expect(res.status).toEqual(200); + expect(await res.text()).toEqual('My File Content'); + }); + + it('should support object values', async () => { + const someFieldValue = { foo: 'bar' }; + + const createResult = await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject($fields: Object) { + objects { + create(className: "SomeClass", fields: $fields) { + objectId + } + } + } + `, + variables: { + fields: { + someField: someFieldValue, + }, + }, + }); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const schema = await new Parse.Schema('SomeClass').get(); + expect(schema.fields.someField.type).toEqual('Object'); + + await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject($fields: SomeClassFields) { + objects { + createSomeClass(fields: $fields) { + objectId + } + } + } + `, + variables: { + fields: { + someField: someFieldValue, + }, + }, + }); + + const getResult = await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!) { + objects { + get(className: "SomeClass", objectId: $objectId) + findSomeClass(where: { someField: { _exists: true } }) { + results { + objectId + } + } + } + } + `, + variables: { + objectId: createResult.data.objects.create.objectId, + }, + }); + + const { someField } = getResult.data.objects.get; + expect(typeof someField).toEqual('object'); + expect(someField).toEqual(someFieldValue); + expect(getResult.data.objects.findSomeClass.results.length).toEqual( + 2 + ); + }); + + it('should support array values', async () => { + const someFieldValue = [1, 'foo', ['bar'], { lorem: 'ipsum' }, true]; + + const createResult = await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject($fields: Object) { + objects { + create(className: "SomeClass", fields: $fields) { + objectId + } + } + } + `, + variables: { + fields: { + someField: someFieldValue, + }, + }, + }); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const schema = await new Parse.Schema('SomeClass').get(); + expect(schema.fields.someField.type).toEqual('Array'); + + await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject($fields: SomeClassFields) { + objects { + createSomeClass(fields: $fields) { + objectId + } + } + } + `, + variables: { + fields: { + someField: someFieldValue, + }, + }, + }); + + const getResult = await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!) { + objects { + get(className: "SomeClass", objectId: $objectId) + findSomeClass(where: { someField: { _exists: true } }) { + results { + objectId + someField + } + } + } + } + `, + variables: { + objectId: createResult.data.objects.create.objectId, + }, + }); + + const { someField } = getResult.data.objects.get; + expect(Array.isArray(someField)).toBeTruthy(); + expect(someField).toEqual(someFieldValue); + expect(getResult.data.objects.findSomeClass.results.length).toEqual( + 2 + ); + }); + + it('should support null values', async () => { + const createResult = await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject($fields: Object) { + objects { + create(className: "SomeClass", fields: $fields) { + objectId + } + } + } + `, + variables: { + fields: { + someStringField: 'some string', + someNumberField: 123, + someBooleanField: true, + someObjectField: { someField: 'some value' }, + someNullField: null, + }, + }, + }); + + await apolloClient.mutate({ + mutation: gql` + mutation UpdateSomeObject($objectId: ID!, $fields: Object) { + objects { + update( + className: "SomeClass" + objectId: $objectId + fields: $fields + ) { + updatedAt + } + } + } + `, + variables: { + objectId: createResult.data.objects.create.objectId, + fields: { + someStringField: null, + someNumberField: null, + someBooleanField: null, + someObjectField: null, + someNullField: 'now it has a string', + }, + }, + }); + + const getResult = await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!) { + objects { + get(className: "SomeClass", objectId: $objectId) + } + } + `, + variables: { + objectId: createResult.data.objects.create.objectId, + }, + }); + + expect(getResult.data.objects.get.someStringField).toBeFalsy(); + expect(getResult.data.objects.get.someNumberField).toBeFalsy(); + expect(getResult.data.objects.get.someBooleanField).toBeFalsy(); + expect(getResult.data.objects.get.someObjectField).toBeFalsy(); + expect(getResult.data.objects.get.someNullField).toEqual( + 'now it has a string' + ); + }); + + it('should support Bytes', async () => { + const someFieldValue = { + __type: 'Bytes', + base64: 'aGVsbG8gd29ybGQ=', + }; + + const createResult = await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject($fields: Object) { + objects { + create(className: "SomeClass", fields: $fields) { + objectId + } + } + } + `, + variables: { + fields: { + someField: someFieldValue, + }, + }, + }); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const schema = await new Parse.Schema('SomeClass').get(); + expect(schema.fields.someField.type).toEqual('Bytes'); + + await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject( + $fields1: SomeClassFields + $fields2: SomeClassFields + ) { + objects { + createSomeClass1: createSomeClass(fields: $fields1) { + objectId + } + createSomeClass2: createSomeClass(fields: $fields2) { + objectId + } + } + } + `, + variables: { + fields1: { + someField: someFieldValue, + }, + fields2: { + someField: someFieldValue.base64, + }, + }, + }); + + const getResult = await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!, $someFieldValue: Bytes) { + objects { + get(className: "SomeClass", objectId: $objectId) + findSomeClass( + where: { someField: { _eq: $someFieldValue } } + ) { + results { + objectId + someField + } + } + } + } + `, + variables: { + objectId: createResult.data.objects.create.objectId, + someFieldValue, + }, + }); + + expect(typeof getResult.data.objects.get.someField).toEqual('object'); + expect(getResult.data.objects.get.someField).toEqual(someFieldValue); + expect(getResult.data.objects.findSomeClass.results.length).toEqual( + 3 + ); + }); + + it('should support Geo Points', async () => { + const someFieldValue = { + __type: 'GeoPoint', + latitude: 45, + longitude: 45, + }; + + const createResult = await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject($fields: Object) { + objects { + create(className: "SomeClass", fields: $fields) { + objectId + } + } + } + `, + variables: { + fields: { + someField: someFieldValue, + }, + }, + }); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const schema = await new Parse.Schema('SomeClass').get(); + expect(schema.fields.someField.type).toEqual('GeoPoint'); + + await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject($fields: SomeClassFields) { + objects { + createSomeClass(fields: $fields) { + objectId + } + } + } + `, + variables: { + fields: { + someField: { + latitude: someFieldValue.latitude, + longitude: someFieldValue.longitude, + }, + }, + }, + }); + + const getResult = await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!) { + objects { + get(className: "SomeClass", objectId: $objectId) + findSomeClass(where: { someField: { _exists: true } }) { + results { + objectId + someField { + latitude + longitude + } + } + } + } + } + `, + variables: { + objectId: createResult.data.objects.create.objectId, + }, + }); + + expect(typeof getResult.data.objects.get.someField).toEqual('object'); + expect(getResult.data.objects.get.someField).toEqual(someFieldValue); + expect(getResult.data.objects.findSomeClass.results.length).toEqual( + 2 + ); + }); + + it('should support Polygons', async () => { + const someFieldValue = { + __type: 'Polygon', + coordinates: [[44, 45], [46, 47], [48, 49], [44, 45]], + }; + + const createResult = await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject($fields: Object) { + objects { + create(className: "SomeClass", fields: $fields) { + objectId + } + } + } + `, + variables: { + fields: { + somePolygonField: someFieldValue, + }, + }, + }); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const schema = await new Parse.Schema('SomeClass').get(); + expect(schema.fields.somePolygonField.type).toEqual('Polygon'); + + await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject($fields: SomeClassFields) { + objects { + createSomeClass(fields: $fields) { + objectId + } + } + } + `, + variables: { + fields: { + somePolygonField: someFieldValue.coordinates.map(point => ({ + latitude: point[0], + longitude: point[1], + })), + }, + }, + }); + + const getResult = await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!) { + objects { + get(className: "SomeClass", objectId: $objectId) + findSomeClass( + where: { somePolygonField: { _exists: true } } + ) { + results { + objectId + somePolygonField { + latitude + longitude + } + } + } + } + } + `, + variables: { + objectId: createResult.data.objects.create.objectId, + }, + }); + + expect(typeof getResult.data.objects.get.somePolygonField).toEqual( + 'object' + ); + expect(getResult.data.objects.get.somePolygonField).toEqual( + someFieldValue + ); + expect(getResult.data.objects.findSomeClass.results.length).toEqual( + 2 + ); + }); + + it('should support polygon values', async () => { + const someFieldValue = { + __type: 'Polygon', + coordinates: [[1.0, 2.1], [3.2, 4.3], [5.4, 6.5], [1.0, 2.1]], + }; + + const createResult = await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject($fields: Object) { + objects { + create(className: "SomeClass", fields: $fields) { + objectId + } + } + } + `, + variables: { + fields: { + somePolygonField: someFieldValue, + }, + }, + }); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const getResult = await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!) { + objects { + getSomeClass(objectId: $objectId) { + somePolygonField { + latitude + longitude + } + } + } + } + `, + variables: { + objectId: createResult.data.objects.create.objectId, + }, + }); + + const schema = await new Parse.Schema('SomeClass').get(); + expect(schema.fields.somePolygonField.type).toEqual('Polygon'); + + const { somePolygonField } = getResult.data.objects.getSomeClass; + expect(Array.isArray(somePolygonField)).toBeTruthy(); + somePolygonField.forEach((coord, i) => { + expect(coord.latitude).toEqual(someFieldValue.coordinates[i][0]); + expect(coord.longitude).toEqual(someFieldValue.coordinates[i][1]); + }); + }); + + it_only_db('mongo')('should support bytes values', async () => { + const SomeClass = Parse.Object.extend('SomeClass'); + const someClass = new SomeClass(); + someClass.set('someField', { + __type: 'Bytes', + base64: 'foo', + }); + await someClass.save(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + const schema = await new Parse.Schema('SomeClass').get(); + expect(schema.fields.someField.type).toEqual('Bytes'); + + const someFieldValue = { + __type: 'Bytes', + base64: 'bytesContent', + }; + + const createResult = await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject($fields: SomeClassFields) { + objects { + createSomeClass(fields: $fields) { + objectId + } + } + } + `, + variables: { + fields: { + someField: someFieldValue, + }, + }, + }); + + const getResult = await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!) { + objects { + getSomeClass(objectId: $objectId) { + someField + } + } + } + `, + variables: { + objectId: createResult.data.objects.createSomeClass.objectId, + }, + }); + + expect(getResult.data.objects.getSomeClass.someField).toEqual( + someFieldValue.base64 + ); + + const updatedSomeFieldValue = { + __type: 'Bytes', + base64: 'newBytesContent', + }; + + const updatedResult = await apolloClient.mutate({ + mutation: gql` + mutation UpdateSomeObject( + $objectId: ID! + $fields: SomeClassFields + ) { + objects { + updateSomeClass(objectId: $objectId, fields: $fields) { + updatedAt + } + } + } + `, + variables: { + objectId: createResult.data.objects.createSomeClass.objectId, + fields: { + someField: updatedSomeFieldValue, + }, + }, + }); + + const { updatedAt } = updatedResult.data.objects.updateSomeClass; + expect(updatedAt).toBeDefined(); + + const findResult = await apolloClient.query({ + query: gql` + query FindSomeObject($where: SomeClassConstraints!) { + objects { + findSomeClass(where: $where) { + results { + objectId + } + } + } + } + `, + variables: { + where: { + someField: { + _eq: updatedSomeFieldValue.base64, + }, + }, + }, + }); + const findResults = findResult.data.objects.findSomeClass.results; + expect(findResults.length).toBe(1); + expect(findResults[0].objectId).toBe( + createResult.data.objects.createSomeClass.objectId + ); + }); + }); + + describe('Special Classes', () => { + it('should support User class', async () => { + const user = new Parse.User(); + user.setUsername('user1'); + user.setPassword('user1'); + await user.signUp(); + + const getResult = await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!) { + objects { + get(className: "_User", objectId: $objectId) + } + } + `, + variables: { + objectId: user.id, + }, + }); + + expect(getResult.data.objects.get.objectId).toEqual(user.id); + }); + + it('should support Installation class', async () => { + const installation = new Parse.Installation(); + await installation.save({ + deviceType: 'foo', + }); + + const getResult = await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!) { + objects { + get(className: "_Installation", objectId: $objectId) + } + } + `, + variables: { + objectId: installation.id, + }, + }); + + expect(getResult.data.objects.get.objectId).toEqual(installation.id); + }); + + it('should support Role class', async () => { + const roleACL = new Parse.ACL(); + roleACL.setPublicReadAccess(true); + const role = new Parse.Role('MyRole', roleACL); + await role.save(); + + const getResult = await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!) { + objects { + get(className: "_Role", objectId: $objectId) + } + } + `, + variables: { + objectId: role.id, + }, + }); + + expect(getResult.data.objects.get.objectId).toEqual(role.id); + }); + + it('should support Session class', async () => { + const user = new Parse.User(); + user.setUsername('user1'); + user.setPassword('user1'); + await user.signUp(); + + const session = await Parse.Session.current(); + const getResult = await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!) { + objects { + get(className: "_Session", objectId: $objectId) + } + } + `, + variables: { + objectId: session.id, + }, + context: { + headers: { + 'X-Parse-Session-Token': session.getSessionToken(), + }, + }, + }); + + expect(getResult.data.objects.get.objectId).toEqual(session.id); + }); + + it('should support Product class', async () => { + const Product = Parse.Object.extend('_Product'); + const product = new Product(); + await product.save( + { + productIdentifier: 'foo', + icon: new Parse.File('icon', ['foo']), + order: 1, + title: 'Foo', + subtitle: 'My product', + }, + { useMasterKey: true } + ); + + const getResult = await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!) { + objects { + get(className: "_Product", objectId: $objectId) + } + } + `, + variables: { + objectId: product.id, + }, + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, + }, + }); + + expect(getResult.data.objects.get.objectId).toEqual(product.id); + }); + + it('should support PushStatus class', async () => { + const PushStatus = Parse.Object.extend('_PushStatus'); + const pushStatus = new PushStatus(); + await pushStatus.save(undefined, { useMasterKey: true }); + + const getResult = await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!) { + objects { + get(className: "_PushStatus", objectId: $objectId) + } + } + `, + variables: { + objectId: pushStatus.id, + }, + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, + }, + }); + + expect(getResult.data.objects.get.objectId).toEqual(pushStatus.id); + }); + + it('should support JobStatus class', async () => { + const JobStatus = Parse.Object.extend('_JobStatus'); + const jobStatus = new JobStatus(); + await jobStatus.save(undefined, { useMasterKey: true }); + + const getResult = await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!) { + objects { + get(className: "_JobStatus", objectId: $objectId) + } + } + `, + variables: { + objectId: jobStatus.id, + }, + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, + }, + }); + + expect(getResult.data.objects.get.objectId).toEqual(jobStatus.id); + }); + + it('should support JobSchedule class', async () => { + const JobSchedule = Parse.Object.extend('_JobSchedule'); + const jobSchedule = new JobSchedule(); + await jobSchedule.save(undefined, { useMasterKey: true }); + + const getResult = await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!) { + objects { + get(className: "_JobSchedule", objectId: $objectId) + } + } + `, + variables: { + objectId: jobSchedule.id, + }, + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, + }, + }); + + expect(getResult.data.objects.get.objectId).toEqual(jobSchedule.id); + }); + + it('should support Hooks class', async () => { + const functionName = 'fooHook'; + await parseServer.config.hooksController.saveHook({ + functionName, + url: 'http://foo.bar', + }); + + const getResult = await apolloClient.query({ + query: gql` + query FindSomeObject { + objects { + find(className: "_Hooks") { + results + } + } + } + `, + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, + }, + }); + + const { results } = getResult.data.objects.find; + expect(results.length).toEqual(1); + expect(results[0].functionName).toEqual(functionName); + }); + + it('should support Audience class', async () => { + const Audience = Parse.Object.extend('_Audience'); + const audience = new Audience(); + await audience.save(); + + const getResult = await apolloClient.query({ + query: gql` + query GetSomeObject($objectId: ID!) { + objects { + get(className: "_Audience", objectId: $objectId) + } + } + `, + variables: { + objectId: audience.id, + }, + }); + + expect(getResult.data.objects.get.objectId).toEqual(audience.id); + }); + }); + }); + }); +}); diff --git a/spec/defaultGraphQLTypes.spec.js b/spec/defaultGraphQLTypes.spec.js new file mode 100644 index 00000000..de968ccf --- /dev/null +++ b/spec/defaultGraphQLTypes.spec.js @@ -0,0 +1,715 @@ +const { Kind } = require('graphql'); +const { + TypeValidationError, + parseStringValue, + parseIntValue, + parseFloatValue, + parseBooleanValue, + parseDateIsoValue, + parseValue, + parseListValues, + parseObjectFields, + BYTES, + DATE, + FILE, +} = require('../lib/GraphQL/loaders/defaultGraphQLTypes'); + +function createValue(kind, value, values, fields) { + return { + kind, + value, + values, + fields, + }; +} + +function createObjectField(name, value) { + return { + name: { + value: name, + }, + value, + }; +} + +describe('defaultGraphQLTypes', () => { + describe('TypeValidationError', () => { + it('should be an error with specific message', () => { + const typeValidationError = new TypeValidationError( + 'somevalue', + 'sometype' + ); + expect(typeValidationError).toEqual(jasmine.any(Error)); + expect(typeValidationError.message).toEqual( + 'somevalue is not a valid sometype' + ); + }); + }); + + describe('parseStringValue', () => { + it('should return itself if a string', () => { + const myString = 'myString'; + expect(parseStringValue(myString)).toBe(myString); + }); + + it('should fail if not a string', () => { + expect(() => parseStringValue()).toThrow( + jasmine.stringMatching('is not a valid String') + ); + expect(() => parseStringValue({})).toThrow( + jasmine.stringMatching('is not a valid String') + ); + expect(() => parseStringValue([])).toThrow( + jasmine.stringMatching('is not a valid String') + ); + expect(() => parseStringValue(123)).toThrow( + jasmine.stringMatching('is not a valid String') + ); + }); + }); + + describe('parseIntValue', () => { + it('should parse to number if a string', () => { + const myString = '123'; + expect(parseIntValue(myString)).toBe(123); + }); + + it('should fail if not a string', () => { + expect(() => parseIntValue()).toThrow( + jasmine.stringMatching('is not a valid Int') + ); + expect(() => parseIntValue({})).toThrow( + jasmine.stringMatching('is not a valid Int') + ); + expect(() => parseIntValue([])).toThrow( + jasmine.stringMatching('is not a valid Int') + ); + expect(() => parseIntValue(123)).toThrow( + jasmine.stringMatching('is not a valid Int') + ); + }); + + it('should fail if not an integer string', () => { + expect(() => parseIntValue('a123')).toThrow( + jasmine.stringMatching('is not a valid Int') + ); + expect(() => parseIntValue('123.4')).toThrow( + jasmine.stringMatching('is not a valid Int') + ); + }); + }); + + describe('parseFloatValue', () => { + it('should parse to number if a string', () => { + expect(parseFloatValue('123')).toBe(123); + expect(parseFloatValue('123.4')).toBe(123.4); + }); + + it('should fail if not a string', () => { + expect(() => parseFloatValue()).toThrow( + jasmine.stringMatching('is not a valid Float') + ); + expect(() => parseFloatValue({})).toThrow( + jasmine.stringMatching('is not a valid Float') + ); + expect(() => parseFloatValue([])).toThrow( + jasmine.stringMatching('is not a valid Float') + ); + }); + + it('should fail if not a float string', () => { + expect(() => parseIntValue('a123')).toThrow( + jasmine.stringMatching('is not a valid Int') + ); + }); + }); + + describe('parseBooleanValue', () => { + it('should return itself if a boolean', () => { + let myBoolean = true; + expect(parseBooleanValue(myBoolean)).toBe(myBoolean); + myBoolean = false; + expect(parseBooleanValue(myBoolean)).toBe(myBoolean); + }); + + it('should fail if not a boolean', () => { + expect(() => parseBooleanValue()).toThrow( + jasmine.stringMatching('is not a valid Boolean') + ); + expect(() => parseBooleanValue({})).toThrow( + jasmine.stringMatching('is not a valid Boolean') + ); + expect(() => parseBooleanValue([])).toThrow( + jasmine.stringMatching('is not a valid Boolean') + ); + expect(() => parseBooleanValue(123)).toThrow( + jasmine.stringMatching('is not a valid Boolean') + ); + expect(() => parseBooleanValue('true')).toThrow( + jasmine.stringMatching('is not a valid Boolean') + ); + }); + }); + + describe('parseDateValue', () => { + it('should parse to date if a string', () => { + const myDateString = '2019-05-09T23:12:00.000Z'; + const myDate = new Date(Date.UTC(2019, 4, 9, 23, 12, 0, 0)); + expect(parseDateIsoValue(myDateString)).toEqual(myDate); + }); + + it('should fail if not a string', () => { + expect(() => parseDateIsoValue()).toThrow( + jasmine.stringMatching('is not a valid Date') + ); + expect(() => parseDateIsoValue({})).toThrow( + jasmine.stringMatching('is not a valid Date') + ); + expect(() => parseDateIsoValue([])).toThrow( + jasmine.stringMatching('is not a valid Date') + ); + expect(() => parseDateIsoValue(123)).toThrow( + jasmine.stringMatching('is not a valid Date') + ); + }); + + it('should fail if not a date string', () => { + expect(() => parseDateIsoValue('not a date')).toThrow( + jasmine.stringMatching('is not a valid Date') + ); + }); + }); + + describe('parseValue', () => { + const someString = createValue(Kind.STRING, 'somestring'); + const someInt = createValue(Kind.INT, '123'); + const someFloat = createValue(Kind.FLOAT, '123.4'); + const someBoolean = createValue(Kind.BOOLEAN, true); + const someOther = createValue(undefined, new Object()); + const someObject = createValue(Kind.OBJECT, undefined, undefined, [ + createObjectField('someString', someString), + createObjectField('someInt', someInt), + createObjectField('someFloat', someFloat), + createObjectField('someBoolean', someBoolean), + createObjectField('someOther', someOther), + createObjectField( + 'someList', + createValue(Kind.LIST, undefined, [ + createValue(Kind.OBJECT, undefined, undefined, [ + createObjectField('someString', someString), + ]), + ]) + ), + createObjectField( + 'someObject', + createValue(Kind.OBJECT, undefined, undefined, [ + createObjectField('someString', someString), + ]) + ), + ]); + const someList = createValue(Kind.LIST, undefined, [ + someString, + someInt, + someFloat, + someBoolean, + someObject, + someOther, + createValue(Kind.LIST, undefined, [ + someString, + someInt, + someFloat, + someBoolean, + someObject, + someOther, + ]), + ]); + + it('should parse string', () => { + expect(parseValue(someString)).toEqual('somestring'); + }); + + it('should parse int', () => { + expect(parseValue(someInt)).toEqual(123); + }); + + it('should parse float', () => { + expect(parseValue(someFloat)).toEqual(123.4); + }); + + it('should parse boolean', () => { + expect(parseValue(someBoolean)).toEqual(true); + }); + + it('should parse list', () => { + expect(parseValue(someList)).toEqual([ + 'somestring', + 123, + 123.4, + true, + { + someString: 'somestring', + someInt: 123, + someFloat: 123.4, + someBoolean: true, + someOther: {}, + someList: [ + { + someString: 'somestring', + }, + ], + someObject: { + someString: 'somestring', + }, + }, + {}, + [ + 'somestring', + 123, + 123.4, + true, + { + someString: 'somestring', + someInt: 123, + someFloat: 123.4, + someBoolean: true, + someOther: {}, + someList: [ + { + someString: 'somestring', + }, + ], + someObject: { + someString: 'somestring', + }, + }, + {}, + ], + ]); + }); + + it('should parse object', () => { + expect(parseValue(someObject)).toEqual({ + someString: 'somestring', + someInt: 123, + someFloat: 123.4, + someBoolean: true, + someOther: {}, + someList: [ + { + someString: 'somestring', + }, + ], + someObject: { + someString: 'somestring', + }, + }); + }); + + it('should return value otherwise', () => { + expect(parseValue(someOther)).toEqual(new Object()); + }); + }); + + describe('parseListValues', () => { + it('should parse to list if an array', () => { + expect( + parseListValues([ + { kind: Kind.STRING, value: 'someString' }, + { kind: Kind.INT, value: '123' }, + ]) + ).toEqual(['someString', 123]); + }); + + it('should fail if not an array', () => { + expect(() => parseListValues()).toThrow( + jasmine.stringMatching('is not a valid List') + ); + expect(() => parseListValues({})).toThrow( + jasmine.stringMatching('is not a valid List') + ); + expect(() => parseListValues('some string')).toThrow( + jasmine.stringMatching('is not a valid List') + ); + expect(() => parseListValues(123)).toThrow( + jasmine.stringMatching('is not a valid List') + ); + }); + }); + + describe('parseObjectFields', () => { + it('should parse to list if an array', () => { + expect( + parseObjectFields([ + { + name: { value: 'someString' }, + value: { kind: Kind.STRING, value: 'someString' }, + }, + { + name: { value: 'someInt' }, + value: { kind: Kind.INT, value: '123' }, + }, + ]) + ).toEqual({ + someString: 'someString', + someInt: 123, + }); + }); + + it('should fail if not an array', () => { + expect(() => parseObjectFields()).toThrow( + jasmine.stringMatching('is not a valid Object') + ); + expect(() => parseObjectFields({})).toThrow( + jasmine.stringMatching('is not a valid Object') + ); + expect(() => parseObjectFields('some string')).toThrow( + jasmine.stringMatching('is not a valid Object') + ); + expect(() => parseObjectFields(123)).toThrow( + jasmine.stringMatching('is not a valid Object') + ); + }); + }); + + describe('Date', () => { + describe('parse literal', () => { + const { parseLiteral } = DATE; + + it('should parse to date if string', () => { + const date = '2019-05-09T23:12:00.000Z'; + expect(parseLiteral(createValue(Kind.STRING, date))).toEqual({ + __type: 'Date', + iso: new Date(date), + }); + }); + + it('should parse to date if object', () => { + const date = '2019-05-09T23:12:00.000Z'; + expect( + parseLiteral( + createValue(Kind.OBJECT, undefined, undefined, [ + createObjectField('__type', { value: 'Date' }), + createObjectField('iso', { value: date, kind: Kind.STRING }), + ]) + ) + ).toEqual({ + __type: 'Date', + iso: new Date(date), + }); + }); + + it('should fail if not an valid object or string', () => { + expect(() => parseLiteral({})).toThrow( + jasmine.stringMatching('is not a valid Date') + ); + expect(() => + parseLiteral( + createValue(Kind.OBJECT, undefined, undefined, [ + createObjectField('__type', { value: 'Foo' }), + createObjectField('iso', { value: '2019-05-09T23:12:00.000Z' }), + ]) + ) + ).toThrow(jasmine.stringMatching('is not a valid Date')); + expect(() => parseLiteral([])).toThrow( + jasmine.stringMatching('is not a valid Date') + ); + expect(() => parseLiteral(123)).toThrow( + jasmine.stringMatching('is not a valid Date') + ); + }); + }); + + describe('parse value', () => { + const { parseValue } = DATE; + + it('should parse string value', () => { + const date = '2019-05-09T23:12:00.000Z'; + expect(parseValue(date)).toEqual({ + __type: 'Date', + iso: new Date(date), + }); + }); + + it('should parse object value', () => { + const input = { + __type: 'Date', + iso: new Date('2019-05-09T23:12:00.000Z'), + }; + expect(parseValue(input)).toEqual(input); + }); + + it('should fail if not an valid object or string', () => { + expect(() => parseValue({})).toThrow( + jasmine.stringMatching('is not a valid Date') + ); + expect(() => + parseValue({ + __type: 'Foo', + iso: '2019-05-09T23:12:00.000Z', + }) + ).toThrow(jasmine.stringMatching('is not a valid Date')); + expect(() => + parseValue({ + __type: 'Date', + iso: 'foo', + }) + ).toThrow(jasmine.stringMatching('is not a valid Date')); + expect(() => parseValue([])).toThrow( + jasmine.stringMatching('is not a valid Date') + ); + expect(() => parseValue(123)).toThrow( + jasmine.stringMatching('is not a valid Date') + ); + }); + }); + + describe('serialize date type', () => { + const { serialize } = DATE; + + it('should do nothing if string', () => { + const str = '2019-05-09T23:12:00.000Z'; + expect(serialize(str)).toBe(str); + }); + + it('should serialize date', () => { + const date = new Date(); + expect(serialize(date)).toBe(date.toUTCString()); + }); + + it('should return iso value if object', () => { + const iso = '2019-05-09T23:12:00.000Z'; + const date = { + __type: 'Date', + iso, + }; + expect(serialize(date)).toEqual(iso); + }); + + it('should fail if not an valid object or string', () => { + expect(() => serialize({})).toThrow( + jasmine.stringMatching('is not a valid Date') + ); + expect(() => + serialize({ + __type: 'Foo', + iso: '2019-05-09T23:12:00.000Z', + }) + ).toThrow(jasmine.stringMatching('is not a valid Date')); + expect(() => serialize([])).toThrow( + jasmine.stringMatching('is not a valid Date') + ); + expect(() => serialize(123)).toThrow( + jasmine.stringMatching('is not a valid Date') + ); + }); + }); + }); + + describe('Bytes', () => { + describe('parse literal', () => { + const { parseLiteral } = BYTES; + + it('should parse to bytes if string', () => { + expect(parseLiteral(createValue(Kind.STRING, 'bytesContent'))).toEqual({ + __type: 'Bytes', + base64: 'bytesContent', + }); + }); + + it('should parse to bytes if object', () => { + expect( + parseLiteral( + createValue(Kind.OBJECT, undefined, undefined, [ + createObjectField('__type', { value: 'Bytes' }), + createObjectField('base64', { value: 'bytesContent' }), + ]) + ) + ).toEqual({ + __type: 'Bytes', + base64: 'bytesContent', + }); + }); + + it('should fail if not an valid object or string', () => { + expect(() => parseLiteral({})).toThrow( + jasmine.stringMatching('is not a valid Bytes') + ); + expect(() => + parseLiteral( + createValue(Kind.OBJECT, undefined, undefined, [ + createObjectField('__type', { value: 'Foo' }), + createObjectField('base64', { value: 'bytesContent' }), + ]) + ) + ).toThrow(jasmine.stringMatching('is not a valid Bytes')); + expect(() => parseLiteral([])).toThrow( + jasmine.stringMatching('is not a valid Bytes') + ); + expect(() => parseLiteral(123)).toThrow( + jasmine.stringMatching('is not a valid Bytes') + ); + }); + }); + + describe('parse value', () => { + const { parseValue } = BYTES; + + it('should parse string value', () => { + expect(parseValue('bytesContent')).toEqual({ + __type: 'Bytes', + base64: 'bytesContent', + }); + }); + + it('should parse object value', () => { + const input = { + __type: 'Bytes', + base64: 'bytesContent', + }; + expect(parseValue(input)).toEqual(input); + }); + + it('should fail if not an valid object or string', () => { + expect(() => parseValue({})).toThrow( + jasmine.stringMatching('is not a valid Bytes') + ); + expect(() => + parseValue({ + __type: 'Foo', + base64: 'bytesContent', + }) + ).toThrow(jasmine.stringMatching('is not a valid Bytes')); + expect(() => parseValue([])).toThrow( + jasmine.stringMatching('is not a valid Bytes') + ); + expect(() => parseValue(123)).toThrow( + jasmine.stringMatching('is not a valid Bytes') + ); + }); + }); + + describe('serialize bytes type', () => { + const { serialize } = BYTES; + + it('should do nothing if string', () => { + const str = 'foo'; + expect(serialize(str)).toBe(str); + }); + + it('should return base64 value if object', () => { + const base64Content = 'bytesContent'; + const bytes = { + __type: 'Bytes', + base64: base64Content, + }; + expect(serialize(bytes)).toEqual(base64Content); + }); + + it('should fail if not an valid object or string', () => { + expect(() => serialize({})).toThrow( + jasmine.stringMatching('is not a valid Bytes') + ); + expect(() => + serialize({ + __type: 'Foo', + base64: 'bytesContent', + }) + ).toThrow(jasmine.stringMatching('is not a valid Bytes')); + expect(() => serialize([])).toThrow( + jasmine.stringMatching('is not a valid Bytes') + ); + expect(() => serialize(123)).toThrow( + jasmine.stringMatching('is not a valid Bytes') + ); + }); + }); + }); + + describe('File', () => { + describe('parse literal', () => { + const { parseLiteral } = FILE; + + it('should parse to file if string', () => { + expect(parseLiteral(createValue(Kind.STRING, 'parsefile'))).toEqual({ + __type: 'File', + name: 'parsefile', + }); + }); + + it('should parse to file if object', () => { + expect( + parseLiteral( + createValue(Kind.OBJECT, undefined, undefined, [ + createObjectField('__type', { value: 'File' }), + createObjectField('name', { value: 'parsefile' }), + createObjectField('url', { value: 'myurl' }), + ]) + ) + ).toEqual({ + __type: 'File', + name: 'parsefile', + url: 'myurl', + }); + }); + + it('should fail if not an valid object or string', () => { + expect(() => parseLiteral({})).toThrow( + jasmine.stringMatching('is not a valid File') + ); + expect(() => + parseLiteral( + createValue(Kind.OBJECT, undefined, undefined, [ + createObjectField('__type', { value: 'Foo' }), + createObjectField('name', { value: 'parsefile' }), + createObjectField('url', { value: 'myurl' }), + ]) + ) + ).toThrow(jasmine.stringMatching('is not a valid File')); + expect(() => parseLiteral([])).toThrow( + jasmine.stringMatching('is not a valid File') + ); + expect(() => parseLiteral(123)).toThrow( + jasmine.stringMatching('is not a valid File') + ); + }); + }); + + describe('serialize file type', () => { + const { serialize } = FILE; + + it('should do nothing if string', () => { + const str = 'foo'; + expect(serialize(str)).toBe(str); + }); + + it('should return file name if object', () => { + const fileName = 'parsefile'; + const file = { + __type: 'File', + name: fileName, + url: 'myurl', + }; + expect(serialize(file)).toEqual(fileName); + }); + + it('should fail if not an valid object or string', () => { + expect(() => serialize({})).toThrow( + jasmine.stringMatching('is not a valid File') + ); + expect(() => + serialize({ + __type: 'Foo', + name: 'parsefile', + url: 'myurl', + }) + ).toThrow(jasmine.stringMatching('is not a valid File')); + expect(() => serialize([])).toThrow( + jasmine.stringMatching('is not a valid File') + ); + expect(() => serialize(123)).toThrow( + jasmine.stringMatching('is not a valid File') + ); + }); + }); + }); +}); diff --git a/src/GraphQL/ParseGraphQLSchema.js b/src/GraphQL/ParseGraphQLSchema.js new file mode 100644 index 00000000..7664eef5 --- /dev/null +++ b/src/GraphQL/ParseGraphQLSchema.js @@ -0,0 +1,117 @@ +import Parse from 'parse/node'; +import { GraphQLSchema, GraphQLObjectType } from 'graphql'; +import { ApolloError } from 'apollo-server-core'; +import requiredParameter from '../requiredParameter'; +import * as defaultGraphQLTypes from './loaders/defaultGraphQLTypes'; +import * as parseClassTypes from './loaders/parseClassTypes'; +import * as parseClassQueries from './loaders/parseClassQueries'; +import * as parseClassMutations from './loaders/parseClassMutations'; +import * as defaultGraphQLQueries from './loaders/defaultGraphQLQueries'; +import * as defaultGraphQLMutations from './loaders/defaultGraphQLMutations'; + +class ParseGraphQLSchema { + constructor(databaseController, log) { + this.databaseController = + databaseController || + requiredParameter('You must provide a databaseController instance!'); + this.log = log || requiredParameter('You must provide a log instance!'); + } + + async load() { + const schemaController = await this.databaseController.loadSchema(); + const parseClasses = await schemaController.getAllClasses(); + const parseClassesString = JSON.stringify(parseClasses); + + if (this.graphQLSchema) { + if (this.parseClasses === parseClasses) { + return this.graphQLSchema; + } + + if (this.parseClassesString === parseClassesString) { + this.parseClasses = parseClasses; + return this.graphQLSchema; + } + } + + this.parseClasses = parseClasses; + this.parseClassesString = parseClassesString; + this.parseClassTypes = {}; + this.meType = null; + this.graphQLSchema = null; + this.graphQLTypes = []; + this.graphQLObjectsQueries = {}; + this.graphQLQueries = {}; + this.graphQLObjectsMutations = {}; + this.graphQLMutations = {}; + this.graphQLSubscriptions = {}; + + defaultGraphQLTypes.load(this); + + parseClasses.forEach(parseClass => { + parseClassTypes.load(this, parseClass); + + parseClassQueries.load(this, parseClass); + + parseClassMutations.load(this, parseClass); + }); + + defaultGraphQLQueries.load(this); + + defaultGraphQLMutations.load(this); + + let graphQLQuery = undefined; + if (Object.keys(this.graphQLQueries).length > 0) { + graphQLQuery = new GraphQLObjectType({ + name: 'Query', + description: 'Query is the top level type for queries.', + fields: this.graphQLQueries, + }); + this.graphQLTypes.push(graphQLQuery); + } + + let graphQLMutation = undefined; + if (Object.keys(this.graphQLMutations).length > 0) { + graphQLMutation = new GraphQLObjectType({ + name: 'Mutation', + description: 'Mutation is the top level type for mutations.', + fields: this.graphQLMutations, + }); + this.graphQLTypes.push(graphQLMutation); + } + + let graphQLSubscription = undefined; + if (Object.keys(this.graphQLSubscriptions).length > 0) { + graphQLSubscription = new GraphQLObjectType({ + name: 'Subscription', + description: 'Subscription is the top level type for subscriptions.', + fields: this.graphQLSubscriptions, + }); + this.graphQLTypes.push(graphQLSubscription); + } + + this.graphQLSchema = new GraphQLSchema({ + types: this.graphQLTypes, + query: graphQLQuery, + mutation: graphQLMutation, + subscription: graphQLSubscription, + }); + + return this.graphQLSchema; + } + + handleError(error) { + let code, message; + if (error instanceof Parse.Error) { + this.log.error('Parse error: ', error); + code = error.code; + message = error.message; + } else { + this.log.error('Uncaught internal server error.', error, error.stack); + code = Parse.Error.INTERNAL_SERVER_ERROR; + message = 'Internal server error.'; + } + throw new ApolloError(message, code); + } +} + +export { ParseGraphQLSchema }; diff --git a/src/GraphQL/ParseGraphQLServer.js b/src/GraphQL/ParseGraphQLServer.js new file mode 100644 index 00000000..aefaed76 --- /dev/null +++ b/src/GraphQL/ParseGraphQLServer.js @@ -0,0 +1,110 @@ +import corsMiddleware from 'cors'; +import bodyParser from 'body-parser'; +import { graphqlUploadExpress } from 'graphql-upload'; +import { graphqlExpress } from 'apollo-server-express/dist/expressApollo'; +import { renderPlaygroundPage } from '@apollographql/graphql-playground-html'; +import { execute, subscribe } from 'graphql'; +import { SubscriptionServer } from 'subscriptions-transport-ws'; +import { handleParseHeaders } from '../middlewares'; +import requiredParameter from '../requiredParameter'; +import defaultLogger from '../logger'; +import { ParseGraphQLSchema } from './ParseGraphQLSchema'; + +class ParseGraphQLServer { + constructor(parseServer, config) { + this.parseServer = + parseServer || + requiredParameter('You must provide a parseServer instance!'); + if (!config || !config.graphQLPath) { + requiredParameter('You must provide a config.graphQLPath!'); + } + this.config = config; + this.parseGraphQLSchema = new ParseGraphQLSchema( + this.parseServer.config.databaseController, + (this.parseServer.config && this.parseServer.config.loggerController) || + defaultLogger + ); + } + + async _getGraphQLOptions(req) { + return { + schema: await this.parseGraphQLSchema.load(), + context: { + info: req.info, + config: req.config, + auth: req.auth, + }, + }; + } + + applyGraphQL(app) { + if (!app || !app.use) { + requiredParameter('You must provide an Express.js app instance!'); + } + + const maxUploadSize = this.parseServer.config.maxUploadSize || '20mb'; + const maxFileSize = + (Number(maxUploadSize.slice(0, -2)) * 1024) ^ + { + kb: 1, + mb: 2, + gb: 3, + }[maxUploadSize.slice(-2).toLowerCase()]; + + app.use(this.config.graphQLPath, graphqlUploadExpress({ maxFileSize })); + app.use(this.config.graphQLPath, corsMiddleware()); + app.use(this.config.graphQLPath, bodyParser.json()); + app.use(this.config.graphQLPath, handleParseHeaders); + app.use( + this.config.graphQLPath, + graphqlExpress(async req => await this._getGraphQLOptions(req)) + ); + } + + applyPlayground(app) { + if (!app || !app.get) { + requiredParameter('You must provide an Express.js app instance!'); + } + app.get( + this.config.playgroundPath || + requiredParameter( + 'You must provide a config.playgroundPath to applyPlayground!' + ), + (_req, res) => { + res.setHeader('Content-Type', 'text/html'); + res.write( + renderPlaygroundPage({ + endpoint: this.config.graphQLPath, + subscriptionEndpoint: this.config.subscriptionsPath, + }) + ); + res.end(); + } + ); + } + + createSubscriptions(server) { + SubscriptionServer.create( + { + execute, + subscribe, + onOperation: async (_message, params, webSocket) => + Object.assign( + {}, + params, + await this._getGraphQLOptions(webSocket.upgradeReq) + ), + }, + { + server, + path: + this.config.subscriptionsPath || + requiredParameter( + 'You must provide a config.subscriptionsPath to createSubscriptions!' + ), + } + ); + } +} + +export { ParseGraphQLServer }; diff --git a/src/GraphQL/loaders/defaultGraphQLMutations.js b/src/GraphQL/loaders/defaultGraphQLMutations.js new file mode 100644 index 00000000..4d36c9e5 --- /dev/null +++ b/src/GraphQL/loaders/defaultGraphQLMutations.js @@ -0,0 +1,11 @@ +import * as objectsMutations from './objectsMutations'; +import * as filesMutations from './filesMutations'; +import * as usersMutations from './usersMutations'; + +const load = parseGraphQLSchema => { + objectsMutations.load(parseGraphQLSchema); + filesMutations.load(parseGraphQLSchema); + usersMutations.load(parseGraphQLSchema); +}; + +export { load }; diff --git a/src/GraphQL/loaders/defaultGraphQLQueries.js b/src/GraphQL/loaders/defaultGraphQLQueries.js new file mode 100644 index 00000000..3618bf15 --- /dev/null +++ b/src/GraphQL/loaders/defaultGraphQLQueries.js @@ -0,0 +1,17 @@ +import { GraphQLNonNull, GraphQLBoolean } from 'graphql'; +import * as objectsQueries from './objectsQueries'; +import * as usersQueries from './usersQueries'; + +const load = parseGraphQLSchema => { + parseGraphQLSchema.graphQLQueries.health = { + description: + 'The health query can be used to check if the server is up and running.', + type: new GraphQLNonNull(GraphQLBoolean), + resolve: () => true, + }; + + objectsQueries.load(parseGraphQLSchema); + usersQueries.load(parseGraphQLSchema); +}; + +export { load }; diff --git a/src/GraphQL/loaders/defaultGraphQLTypes.js b/src/GraphQL/loaders/defaultGraphQLTypes.js new file mode 100644 index 00000000..ad7b6033 --- /dev/null +++ b/src/GraphQL/loaders/defaultGraphQLTypes.js @@ -0,0 +1,1125 @@ +import { + Kind, + GraphQLNonNull, + GraphQLScalarType, + GraphQLID, + GraphQLString, + GraphQLObjectType, + GraphQLInterfaceType, + GraphQLEnumType, + GraphQLInt, + GraphQLFloat, + GraphQLList, + GraphQLInputObjectType, + GraphQLBoolean, +} from 'graphql'; +import { GraphQLUpload } from 'graphql-upload'; + +class TypeValidationError extends Error { + constructor(value, type) { + super(`${value} is not a valid ${type}`); + } +} + +const parseStringValue = value => { + if (typeof value === 'string') { + return value; + } + + throw new TypeValidationError(value, 'String'); +}; + +const parseIntValue = value => { + if (typeof value === 'string') { + const int = Number(value); + if (Number.isInteger(int)) { + return int; + } + } + + throw new TypeValidationError(value, 'Int'); +}; + +const parseFloatValue = value => { + if (typeof value === 'string') { + const float = Number(value); + if (!isNaN(float)) { + return float; + } + } + + throw new TypeValidationError(value, 'Float'); +}; + +const parseBooleanValue = value => { + if (typeof value === 'boolean') { + return value; + } + + throw new TypeValidationError(value, 'Boolean'); +}; + +const parseValue = value => { + switch (value.kind) { + case Kind.STRING: + return parseStringValue(value.value); + + case Kind.INT: + return parseIntValue(value.value); + + case Kind.FLOAT: + return parseFloatValue(value.value); + + case Kind.BOOLEAN: + return parseBooleanValue(value.value); + + case Kind.LIST: + return parseListValues(value.values); + + case Kind.OBJECT: + return parseObjectFields(value.fields); + + default: + return value.value; + } +}; + +const parseListValues = values => { + if (Array.isArray(values)) { + return values.map(value => parseValue(value)); + } + + throw new TypeValidationError(values, 'List'); +}; + +const parseObjectFields = fields => { + if (Array.isArray(fields)) { + return fields.reduce( + (object, field) => ({ + ...object, + [field.name.value]: parseValue(field.value), + }), + {} + ); + } + + throw new TypeValidationError(fields, 'Object'); +}; + +const ANY = new GraphQLScalarType({ + name: 'Any', + description: + 'The Any scalar type is used in operations and types that involve any type of value.', + parseValue: value => value, + serialize: value => value, + parseLiteral: ast => parseValue(ast), +}); + +const OBJECT = new GraphQLScalarType({ + name: 'Object', + description: + 'The Object scalar type is used in operations and types that involve objects.', + parseValue(value) { + if (typeof value === 'object') { + return value; + } + + throw new TypeValidationError(value, 'Object'); + }, + serialize(value) { + if (typeof value === 'object') { + return value; + } + + throw new TypeValidationError(value, 'Object'); + }, + parseLiteral(ast) { + if (ast.kind === Kind.OBJECT) { + return parseObjectFields(ast.fields); + } + + throw new TypeValidationError(ast.kind, 'Object'); + }, +}); + +const parseDateIsoValue = value => { + if (typeof value === 'string') { + const date = new Date(value); + if (!isNaN(date)) { + return date; + } + } else if (value instanceof Date) { + return value; + } + + throw new TypeValidationError(value, 'Date'); +}; + +const serializeDateIso = value => { + if (typeof value === 'string') { + return value; + } + if (value instanceof Date) { + return value.toUTCString(); + } + + throw new TypeValidationError(value, 'Date'); +}; + +const parseDateIsoLiteral = ast => { + if (ast.kind === Kind.STRING) { + return parseDateIsoValue(ast.value); + } + + throw new TypeValidationError(ast.kind, 'Date'); +}; + +const DATE = new GraphQLScalarType({ + name: 'Date', + description: + 'The Date scalar type is used in operations and types that involve dates.', + parseValue(value) { + if (typeof value === 'string' || value instanceof Date) { + return { + __type: 'Date', + iso: parseDateIsoValue(value), + }; + } else if ( + typeof value === 'object' && + value.__type === 'Date' && + value.iso + ) { + return { + __type: value.__type, + iso: parseDateIsoValue(value.iso), + }; + } + + throw new TypeValidationError(value, 'Date'); + }, + serialize(value) { + if (typeof value === 'string' || value instanceof Date) { + return serializeDateIso(value); + } else if ( + typeof value === 'object' && + value.__type === 'Date' && + value.iso + ) { + return serializeDateIso(value.iso); + } + + throw new TypeValidationError(value, 'Date'); + }, + parseLiteral(ast) { + if (ast.kind === Kind.STRING) { + return { + __type: 'Date', + iso: parseDateIsoLiteral(ast), + }; + } else if (ast.kind === Kind.OBJECT) { + const __type = ast.fields.find(field => field.name.value === '__type'); + const iso = ast.fields.find(field => field.name.value === 'iso'); + if (__type && __type.value && __type.value.value === 'Date' && iso) { + return { + __type: __type.value.value, + iso: parseDateIsoLiteral(iso.value), + }; + } + } + + throw new TypeValidationError(ast.kind, 'Date'); + }, +}); + +const BYTES = new GraphQLScalarType({ + name: 'Bytes', + description: + 'The Bytes scalar type is used in operations and types that involve base 64 binary data.', + parseValue(value) { + if (typeof value === 'string') { + return { + __type: 'Bytes', + base64: value, + }; + } else if ( + typeof value === 'object' && + value.__type === 'Bytes' && + typeof value.base64 === 'string' + ) { + return value; + } + + throw new TypeValidationError(value, 'Bytes'); + }, + serialize(value) { + if (typeof value === 'string') { + return value; + } else if ( + typeof value === 'object' && + value.__type === 'Bytes' && + typeof value.base64 === 'string' + ) { + return value.base64; + } + + throw new TypeValidationError(value, 'Bytes'); + }, + parseLiteral(ast) { + if (ast.kind === Kind.STRING) { + return { + __type: 'Bytes', + base64: ast.value, + }; + } else if (ast.kind === Kind.OBJECT) { + const __type = ast.fields.find(field => field.name.value === '__type'); + const base64 = ast.fields.find(field => field.name.value === 'base64'); + if ( + __type && + __type.value && + __type.value.value === 'Bytes' && + base64 && + base64.value && + typeof base64.value.value === 'string' + ) { + return { + __type: __type.value.value, + base64: base64.value.value, + }; + } + } + + throw new TypeValidationError(ast.kind, 'Bytes'); + }, +}); + +const parseFileValue = value => { + if (typeof value === 'string') { + return { + __type: 'File', + name: value, + }; + } else if ( + typeof value === 'object' && + value.__type === 'File' && + typeof value.name === 'string' && + (value.url === undefined || typeof value.url === 'string') + ) { + return value; + } + + throw new TypeValidationError(value, 'File'); +}; + +const FILE = new GraphQLScalarType({ + name: 'File', + description: + 'The File scalar type is used in operations and types that involve files.', + parseValue: parseFileValue, + serialize: value => { + if (typeof value === 'string') { + return value; + } else if ( + typeof value === 'object' && + value.__type === 'File' && + typeof value.name === 'string' && + (value.url === undefined || typeof value.url === 'string') + ) { + return value.name; + } + + throw new TypeValidationError(value, 'File'); + }, + parseLiteral(ast) { + if (ast.kind === Kind.STRING) { + return parseFileValue(ast.value); + } else if (ast.kind === Kind.OBJECT) { + const __type = ast.fields.find(field => field.name.value === '__type'); + const name = ast.fields.find(field => field.name.value === 'name'); + const url = ast.fields.find(field => field.name.value === 'url'); + if (__type && __type.value && name && name.value) { + return parseFileValue({ + __type: __type.value.value, + name: name.value.value, + url: url && url.value ? url.value.value : undefined, + }); + } + } + + throw new TypeValidationError(ast.kind, 'File'); + }, +}); + +const FILE_INFO = new GraphQLObjectType({ + name: 'FileInfo', + description: + 'The FileInfo object type is used to return the information about files.', + fields: { + name: { + description: 'This is the file name.', + type: new GraphQLNonNull(GraphQLString), + }, + url: { + description: 'This is the url in which the file can be downloaded.', + type: new GraphQLNonNull(GraphQLString), + }, + }, +}); + +const GEO_POINT_FIELDS = { + latitude: { + description: 'This is the latitude.', + type: new GraphQLNonNull(GraphQLFloat), + }, + longitude: { + description: 'This is the longitude.', + type: new GraphQLNonNull(GraphQLFloat), + }, +}; + +const GEO_POINT = new GraphQLInputObjectType({ + name: 'GeoPoint', + description: + 'The GeoPoint input type is used in operations that involve inputting fields of type geo point.', + fields: GEO_POINT_FIELDS, +}); + +const GEO_POINT_INFO = new GraphQLObjectType({ + name: 'GeoPointInfo', + description: + 'The GeoPointInfo object type is used to return the information about geo points.', + fields: GEO_POINT_FIELDS, +}); + +const POLYGON = new GraphQLList(new GraphQLNonNull(GEO_POINT)); + +const POLYGON_INFO = new GraphQLList(new GraphQLNonNull(GEO_POINT_INFO)); + +const RELATION_OP = new GraphQLEnumType({ + name: 'RelationOp', + description: + 'The RelationOp enum type is used to specify which kind of operation should be executed to a relation.', + values: { + Batch: { value: 'Batch' }, + AddRelation: { value: 'AddRelation' }, + RemoveRelation: { value: 'RemoveRelation' }, + }, +}); + +const CLASS_NAME_ATT = { + description: 'This is the class name of the object.', + type: new GraphQLNonNull(GraphQLString), +}; + +const FIELDS_ATT = { + description: 'These are the fields of the object.', + type: OBJECT, +}; + +const OBJECT_ID_ATT = { + description: 'This is the object id.', + type: new GraphQLNonNull(GraphQLID), +}; + +const CREATED_AT_ATT = { + description: 'This is the date in which the object was created.', + type: new GraphQLNonNull(DATE), +}; + +const UPDATED_AT_ATT = { + description: 'This is the date in which the object was las updated.', + type: new GraphQLNonNull(DATE), +}; + +const ACL_ATT = { + description: 'This is the access control list of the object.', + type: OBJECT, +}; + +const INPUT_FIELDS = { + ACL: ACL_ATT, +}; + +const CREATE_RESULT_FIELDS = { + objectId: OBJECT_ID_ATT, + createdAt: CREATED_AT_ATT, +}; + +const CREATE_RESULT = new GraphQLObjectType({ + name: 'CreateResult', + description: + 'The CreateResult object type is used in the create mutations to return the data of the recent created object.', + fields: CREATE_RESULT_FIELDS, +}); + +const UPDATE_RESULT_FIELDS = { + updatedAt: UPDATED_AT_ATT, +}; + +const UPDATE_RESULT = new GraphQLObjectType({ + name: 'UpdateResult', + description: + 'The UpdateResult object type is used in the update mutations to return the data of the recent updated object.', + fields: UPDATE_RESULT_FIELDS, +}); + +const CLASS_FIELDS = { + ...CREATE_RESULT_FIELDS, + ...UPDATE_RESULT_FIELDS, + ...INPUT_FIELDS, +}; + +const CLASS = new GraphQLInterfaceType({ + name: 'Class', + description: + 'The Class interface type is used as a base type for the auto generated class types.', + fields: CLASS_FIELDS, +}); + +const SESSION_TOKEN_ATT = { + description: 'The user session token', + type: new GraphQLNonNull(GraphQLString), +}; + +const KEYS_ATT = { + description: 'The keys of the objects that will be returned.', + type: GraphQLString, +}; + +const INCLUDE_ATT = { + description: 'The pointers of the objects that will be returned.', + type: GraphQLString, +}; + +const READ_PREFERENCE = new GraphQLEnumType({ + name: 'ReadPreference', + description: + 'The ReadPreference enum type is used in queries in order to select in which database replica the operation must run.', + values: { + PRIMARY: { value: 'PRIMARY' }, + PRIMARY_PREFERRED: { value: 'PRIMARY_PREFERRED' }, + SECONDARY: { value: 'SECONDARY' }, + SECONDARY_PREFERRED: { value: 'SECONDARY_PREFERRED' }, + NEAREST: { value: 'NEAREST' }, + }, +}); + +const READ_PREFERENCE_ATT = { + description: 'The read preference for the main query to be executed.', + type: READ_PREFERENCE, +}; + +const INCLUDE_READ_PREFERENCE_ATT = { + description: + 'The read preference for the queries to be executed to include fields.', + type: READ_PREFERENCE, +}; + +const SUBQUERY_READ_PREFERENCE_ATT = { + description: 'The read preference for the subqueries that may be required.', + type: READ_PREFERENCE, +}; + +const WHERE_ATT = { + description: + 'These are the conditions that the objects need to match in order to be found', + type: OBJECT, +}; + +const SKIP_ATT = { + description: 'This is the number of objects that must be skipped to return.', + type: GraphQLInt, +}; + +const LIMIT_ATT = { + description: 'This is the limit number of objects that must be returned.', + type: GraphQLInt, +}; + +const COUNT_ATT = { + description: + 'This is the total matched objecs count that is returned when the count flag is set.', + type: new GraphQLNonNull(GraphQLInt), +}; + +const SUBQUERY = new GraphQLInputObjectType({ + name: 'Subquery', + description: + 'The Subquery input type is used to specific a different query to a different class.', + fields: { + className: CLASS_NAME_ATT, + where: Object.assign({}, WHERE_ATT, { + type: new GraphQLNonNull(WHERE_ATT.type), + }), + }, +}); + +const SELECT_OPERATOR = new GraphQLInputObjectType({ + name: 'SelectOperator', + description: + 'The SelectOperator input type is used to specify a $select operation on a constraint.', + fields: { + query: { + description: 'This is the subquery to be executed.', + type: new GraphQLNonNull(SUBQUERY), + }, + key: { + description: + 'This is the key in the result of the subquery that must match (not match) the field.', + type: new GraphQLNonNull(GraphQLString), + }, + }, +}); + +const SEARCH_OPERATOR = new GraphQLInputObjectType({ + name: 'SearchOperator', + description: + 'The SearchOperator input type is used to specifiy a $search operation on a full text search.', + fields: { + _term: { + description: 'This is the term to be searched.', + type: new GraphQLNonNull(GraphQLString), + }, + _language: { + description: + 'This is the language to tetermine the list of stop words and the rules for tokenizer.', + type: GraphQLString, + }, + _caseSensitive: { + description: + 'This is the flag to enable or disable case sensitive search.', + type: GraphQLBoolean, + }, + _diacriticSensitive: { + description: + 'This is the flag to enable or disable diacritic sensitive search.', + type: GraphQLBoolean, + }, + }, +}); + +const TEXT_OPERATOR = new GraphQLInputObjectType({ + name: 'TextOperator', + description: + 'The TextOperator input type is used to specify a $text operation on a constraint.', + fields: { + _search: { + description: 'This is the search to be executed.', + type: new GraphQLNonNull(SEARCH_OPERATOR), + }, + }, +}); + +const BOX_OPERATOR = new GraphQLInputObjectType({ + name: 'BoxOperator', + description: + 'The BoxOperator input type is used to specifiy a $box operation on a within geo query.', + fields: { + bottomLeft: { + description: 'This is the bottom left coordinates of the box.', + type: new GraphQLNonNull(GEO_POINT), + }, + upperRight: { + description: 'This is the upper right coordinates of the box.', + type: new GraphQLNonNull(GEO_POINT), + }, + }, +}); + +const WITHIN_OPERATOR = new GraphQLInputObjectType({ + name: 'WithinOperator', + description: + 'The WithinOperator input type is used to specify a $within operation on a constraint.', + fields: { + _box: { + description: 'This is the box to be specified.', + type: new GraphQLNonNull(BOX_OPERATOR), + }, + }, +}); + +const CENTER_SPHERE_OPERATOR = new GraphQLInputObjectType({ + name: 'CenterSphereOperator', + description: + 'The CenterSphereOperator input type is used to specifiy a $centerSphere operation on a geoWithin query.', + fields: { + center: { + description: 'This is the center of the sphere.', + type: new GraphQLNonNull(GEO_POINT), + }, + distance: { + description: 'This is the radius of the sphere.', + type: new GraphQLNonNull(GraphQLFloat), + }, + }, +}); + +const GEO_WITHIN_OPERATOR = new GraphQLInputObjectType({ + name: 'GeoWithinOperator', + description: + 'The GeoWithinOperator input type is used to specify a $geoWithin operation on a constraint.', + fields: { + _polygon: { + description: 'This is the polygon to be specified.', + type: POLYGON, + }, + _centerSphere: { + description: 'This is the sphere to be specified.', + type: CENTER_SPHERE_OPERATOR, + }, + }, +}); + +const GEO_INTERSECTS = new GraphQLInputObjectType({ + name: 'GeoIntersectsOperator', + description: + 'The GeoIntersectsOperator input type is used to specify a $geoIntersects operation on a constraint.', + fields: { + _point: { + description: 'This is the point to be specified.', + type: GEO_POINT, + }, + }, +}); + +const _eq = type => ({ + description: + 'This is the $eq operator to specify a constraint to select the objects where the value of a field equals to a specified value.', + type, +}); + +const _ne = type => ({ + description: + 'This is the $ne operator to specify a constraint to select the objects where the value of a field do not equal to a specified value.', + type, +}); + +const _lt = type => ({ + description: + 'This is the $lt operator to specify a constraint to select the objects where the value of a field is less than a specified value.', + type, +}); + +const _lte = type => ({ + description: + 'This is the $lte operator to specify a constraint to select the objects where the value of a field is less than or equal to a specified value.', + type, +}); + +const _gt = type => ({ + description: + 'This is the $gt operator to specify a constraint to select the objects where the value of a field is greater than a specified value.', + type, +}); + +const _gte = type => ({ + description: + 'This is the $gte operator to specify a constraint to select the objects where the value of a field is greater than or equal to a specified value.', + type, +}); + +const _in = type => ({ + description: + 'This is the $in operator to specify a constraint to select the objects where the value of a field equals any value in the specified array.', + type: new GraphQLList(type), +}); + +const _nin = type => ({ + description: + 'This is the $nin operator to specify a constraint to select the objects where the value of a field do not equal any value in the specified array.', + type: new GraphQLList(type), +}); + +const _exists = { + description: + 'This is the $exists operator to specify a constraint to select the objects where a field exists (or do not exist).', + type: GraphQLBoolean, +}; + +const _select = { + description: + 'This is the $select operator to specify a constraint to select the objects where a field equals to a key in the result of a different query.', + type: SELECT_OPERATOR, +}; + +const _dontSelect = { + description: + 'This is the $dontSelect operator to specify a constraint to select the objects where a field do not equal to a key in the result of a different query.', + type: SELECT_OPERATOR, +}; + +const _regex = { + description: + 'This is the $regex operator to specify a constraint to select the objects where the value of a field matches a specified regular expression.', + type: GraphQLString, +}; + +const _options = { + description: + 'This is the $options operator to specify optional flags (such as "i" and "m") to be added to a $regex operation in the same set of constraints.', + type: GraphQLString, +}; + +const STRING_CONSTRAINT = new GraphQLInputObjectType({ + name: 'StringConstraint', + description: + 'The StringConstraint input type is used in operations that involve filtering objects by a field of type String.', + fields: { + _eq: _eq(GraphQLString), + _ne: _ne(GraphQLString), + _lt: _lt(GraphQLString), + _lte: _lte(GraphQLString), + _gt: _gt(GraphQLString), + _gte: _gte(GraphQLString), + _in: _in(GraphQLString), + _nin: _nin(GraphQLString), + _exists, + _select, + _dontSelect, + _regex, + _options, + _text: { + description: + 'This is the $text operator to specify a full text search constraint.', + type: TEXT_OPERATOR, + }, + }, +}); + +const NUMBER_CONSTRAINT = new GraphQLInputObjectType({ + name: 'NumberConstraint', + description: + 'The NumberConstraint input type is used in operations that involve filtering objects by a field of type Number.', + fields: { + _eq: _eq(GraphQLFloat), + _ne: _ne(GraphQLFloat), + _lt: _lt(GraphQLFloat), + _lte: _lte(GraphQLFloat), + _gt: _gt(GraphQLFloat), + _gte: _gte(GraphQLFloat), + _in: _in(GraphQLFloat), + _nin: _nin(GraphQLFloat), + _exists, + _select, + _dontSelect, + }, +}); + +const BOOLEAN_CONSTRAINT = new GraphQLInputObjectType({ + name: 'BooleanConstraint', + description: + 'The BooleanConstraint input type is used in operations that involve filtering objects by a field of type Boolean.', + fields: { + _eq: _eq(GraphQLBoolean), + _ne: _ne(GraphQLBoolean), + _exists, + _select, + _dontSelect, + }, +}); + +const ARRAY_CONSTRAINT = new GraphQLInputObjectType({ + name: 'ArrayConstraint', + description: + 'The ArrayConstraint input type is used in operations that involve filtering objects by a field of type Array.', + fields: { + _eq: _eq(ANY), + _ne: _ne(ANY), + _lt: _lt(ANY), + _lte: _lte(ANY), + _gt: _gt(ANY), + _gte: _gte(ANY), + _in: _in(ANY), + _nin: _nin(ANY), + _exists, + _select, + _dontSelect, + _containedBy: { + description: + 'This is the $containedBy operator to specify a constraint to select the objects where the values of an array field is contained by another specified array.', + type: new GraphQLList(ANY), + }, + _all: { + description: + 'This is the $all operator to specify a constraint to select the objects where the values of an array field contain all elements of another specified array.', + type: new GraphQLList(ANY), + }, + }, +}); + +const OBJECT_CONSTRAINT = new GraphQLInputObjectType({ + name: 'ObjectConstraint', + description: + 'The ObjectConstraint input type is used in operations that involve filtering objects by a field of type Object.', + fields: { + _eq: _eq(OBJECT), + _ne: _ne(OBJECT), + _in: _in(OBJECT), + _nin: _nin(OBJECT), + _exists, + _select, + _dontSelect, + }, +}); + +const DATE_CONSTRAINT = new GraphQLInputObjectType({ + name: 'DateConstraint', + description: + 'The DateConstraint input type is used in operations that involve filtering objects by a field of type Date.', + fields: { + _eq: _eq(DATE), + _ne: _ne(DATE), + _lt: _lt(DATE), + _lte: _lte(DATE), + _gt: _gt(DATE), + _gte: _gte(DATE), + _in: _in(DATE), + _nin: _nin(DATE), + _exists, + _select, + _dontSelect, + }, +}); + +const BYTES_CONSTRAINT = new GraphQLInputObjectType({ + name: 'BytesConstraint', + description: + 'The BytesConstraint input type is used in operations that involve filtering objects by a field of type Bytes.', + fields: { + _eq: _eq(BYTES), + _ne: _ne(BYTES), + _lt: _lt(BYTES), + _lte: _lte(BYTES), + _gt: _gt(BYTES), + _gte: _gte(BYTES), + _in: _in(BYTES), + _nin: _nin(BYTES), + _exists, + _select, + _dontSelect, + }, +}); + +const FILE_CONSTRAINT = new GraphQLInputObjectType({ + name: 'FileConstraint', + description: + 'The FILE_CONSTRAINT input type is used in operations that involve filtering objects by a field of type File.', + fields: { + _eq: _eq(FILE), + _ne: _ne(FILE), + _lt: _lt(FILE), + _lte: _lte(FILE), + _gt: _gt(FILE), + _gte: _gte(FILE), + _in: _in(FILE), + _nin: _nin(FILE), + _exists, + _select, + _dontSelect, + _regex, + _options, + }, +}); + +const GEO_POINT_CONSTRAINT = new GraphQLInputObjectType({ + name: 'GeoPointConstraint', + description: + 'The GeoPointConstraint input type is used in operations that involve filtering objects by a field of type GeoPoint.', + fields: { + _exists, + _nearSphere: { + description: + 'This is the $nearSphere operator to specify a constraint to select the objects where the values of a geo point field is near to another geo point.', + type: GEO_POINT, + }, + _maxDistance: { + description: + 'This is the $maxDistance operator to specify a constraint to select the objects where the values of a geo point field is at a max distance (in radians) from the geo point specified in the $nearSphere operator.', + type: GraphQLFloat, + }, + _maxDistanceInRadians: { + description: + 'This is the $maxDistanceInRadians operator to specify a constraint to select the objects where the values of a geo point field is at a max distance (in radians) from the geo point specified in the $nearSphere operator.', + type: GraphQLFloat, + }, + _maxDistanceInMiles: { + description: + 'This is the $maxDistanceInMiles operator to specify a constraint to select the objects where the values of a geo point field is at a max distance (in miles) from the geo point specified in the $nearSphere operator.', + type: GraphQLFloat, + }, + _maxDistanceInKilometers: { + description: + 'This is the $maxDistanceInKilometers operator to specify a constraint to select the objects where the values of a geo point field is at a max distance (in kilometers) from the geo point specified in the $nearSphere operator.', + type: GraphQLFloat, + }, + _within: { + description: + 'This is the $within operator to specify a constraint to select the objects where the values of a geo point field is within a specified box.', + type: WITHIN_OPERATOR, + }, + _geoWithin: { + description: + 'This is the $geoWithin operator to specify a constraint to select the objects where the values of a geo point field is within a specified polygon or sphere.', + type: GEO_WITHIN_OPERATOR, + }, + }, +}); + +const POLYGON_CONSTRAINT = new GraphQLInputObjectType({ + name: 'PolygonConstraint', + description: + 'The PolygonConstraint input type is used in operations that involve filtering objects by a field of type Polygon.', + fields: { + _exists, + _geoIntersects: { + description: + 'This is the $geoIntersects operator to specify a constraint to select the objects where the values of a polygon field intersect a specified point.', + type: GEO_INTERSECTS, + }, + }, +}); + +const FIND_RESULT = new GraphQLObjectType({ + name: 'FindResult', + description: + 'The FindResult object type is used in the find queries to return the data of the matched objects.', + fields: { + results: { + description: 'This is the objects returned by the query', + type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(OBJECT))), + }, + count: COUNT_ATT, + }, +}); + +const SIGN_UP_RESULT = new GraphQLObjectType({ + name: 'SignUpResult', + description: + 'The SignUpResult object type is used in the users sign up mutation to return the data of the recent created user.', + fields: { + ...CREATE_RESULT_FIELDS, + sessionToken: SESSION_TOKEN_ATT, + }, +}); + +const load = parseGraphQLSchema => { + parseGraphQLSchema.graphQLTypes.push(GraphQLUpload); + parseGraphQLSchema.graphQLTypes.push(ANY); + parseGraphQLSchema.graphQLTypes.push(OBJECT); + parseGraphQLSchema.graphQLTypes.push(DATE); + parseGraphQLSchema.graphQLTypes.push(BYTES); + parseGraphQLSchema.graphQLTypes.push(FILE); + parseGraphQLSchema.graphQLTypes.push(FILE_INFO); + parseGraphQLSchema.graphQLTypes.push(GEO_POINT); + parseGraphQLSchema.graphQLTypes.push(GEO_POINT_INFO); + parseGraphQLSchema.graphQLTypes.push(RELATION_OP); + parseGraphQLSchema.graphQLTypes.push(CREATE_RESULT); + parseGraphQLSchema.graphQLTypes.push(UPDATE_RESULT); + parseGraphQLSchema.graphQLTypes.push(CLASS); + parseGraphQLSchema.graphQLTypes.push(READ_PREFERENCE); + parseGraphQLSchema.graphQLTypes.push(SUBQUERY); + parseGraphQLSchema.graphQLTypes.push(SELECT_OPERATOR); + parseGraphQLSchema.graphQLTypes.push(SEARCH_OPERATOR); + parseGraphQLSchema.graphQLTypes.push(TEXT_OPERATOR); + parseGraphQLSchema.graphQLTypes.push(BOX_OPERATOR); + parseGraphQLSchema.graphQLTypes.push(WITHIN_OPERATOR); + parseGraphQLSchema.graphQLTypes.push(CENTER_SPHERE_OPERATOR); + parseGraphQLSchema.graphQLTypes.push(GEO_WITHIN_OPERATOR); + parseGraphQLSchema.graphQLTypes.push(GEO_INTERSECTS); + parseGraphQLSchema.graphQLTypes.push(STRING_CONSTRAINT); + parseGraphQLSchema.graphQLTypes.push(NUMBER_CONSTRAINT); + parseGraphQLSchema.graphQLTypes.push(BOOLEAN_CONSTRAINT); + parseGraphQLSchema.graphQLTypes.push(ARRAY_CONSTRAINT); + parseGraphQLSchema.graphQLTypes.push(OBJECT_CONSTRAINT); + parseGraphQLSchema.graphQLTypes.push(DATE_CONSTRAINT); + parseGraphQLSchema.graphQLTypes.push(BYTES_CONSTRAINT); + parseGraphQLSchema.graphQLTypes.push(FILE_CONSTRAINT); + parseGraphQLSchema.graphQLTypes.push(GEO_POINT_CONSTRAINT); + parseGraphQLSchema.graphQLTypes.push(POLYGON_CONSTRAINT); + parseGraphQLSchema.graphQLTypes.push(FIND_RESULT); + parseGraphQLSchema.graphQLTypes.push(SIGN_UP_RESULT); +}; + +export { + TypeValidationError, + parseStringValue, + parseIntValue, + parseFloatValue, + parseBooleanValue, + parseValue, + parseListValues, + parseObjectFields, + ANY, + OBJECT, + parseDateIsoValue, + serializeDateIso, + DATE, + BYTES, + parseFileValue, + FILE, + FILE_INFO, + GEO_POINT_FIELDS, + GEO_POINT, + GEO_POINT_INFO, + POLYGON, + POLYGON_INFO, + RELATION_OP, + CLASS_NAME_ATT, + FIELDS_ATT, + OBJECT_ID_ATT, + UPDATED_AT_ATT, + CREATED_AT_ATT, + ACL_ATT, + INPUT_FIELDS, + CREATE_RESULT_FIELDS, + CREATE_RESULT, + UPDATE_RESULT_FIELDS, + UPDATE_RESULT, + CLASS_FIELDS, + CLASS, + SESSION_TOKEN_ATT, + KEYS_ATT, + INCLUDE_ATT, + READ_PREFERENCE, + READ_PREFERENCE_ATT, + INCLUDE_READ_PREFERENCE_ATT, + SUBQUERY_READ_PREFERENCE_ATT, + WHERE_ATT, + SKIP_ATT, + LIMIT_ATT, + COUNT_ATT, + SUBQUERY, + SELECT_OPERATOR, + SEARCH_OPERATOR, + TEXT_OPERATOR, + BOX_OPERATOR, + WITHIN_OPERATOR, + CENTER_SPHERE_OPERATOR, + GEO_WITHIN_OPERATOR, + GEO_INTERSECTS, + _eq, + _ne, + _lt, + _lte, + _gt, + _gte, + _in, + _nin, + _exists, + _select, + _dontSelect, + _regex, + _options, + STRING_CONSTRAINT, + NUMBER_CONSTRAINT, + BOOLEAN_CONSTRAINT, + ARRAY_CONSTRAINT, + OBJECT_CONSTRAINT, + DATE_CONSTRAINT, + BYTES_CONSTRAINT, + FILE_CONSTRAINT, + GEO_POINT_CONSTRAINT, + POLYGON_CONSTRAINT, + FIND_RESULT, + SIGN_UP_RESULT, + load, +}; diff --git a/src/GraphQL/loaders/filesMutations.js b/src/GraphQL/loaders/filesMutations.js new file mode 100644 index 00000000..68ad4e86 --- /dev/null +++ b/src/GraphQL/loaders/filesMutations.js @@ -0,0 +1,93 @@ +import { GraphQLObjectType, GraphQLNonNull } from 'graphql'; +import { GraphQLUpload } from 'graphql-upload'; +import Parse from 'parse/node'; +import * as defaultGraphQLTypes from './defaultGraphQLTypes'; +import logger from '../../logger'; + +const load = parseGraphQLSchema => { + const fields = {}; + + fields.create = { + description: + 'The create mutation can be used to create and upload a new file.', + args: { + file: { + description: 'This is the new file to be created and uploaded', + type: new GraphQLNonNull(GraphQLUpload), + }, + }, + type: new GraphQLNonNull(defaultGraphQLTypes.FILE_INFO), + async resolve(_source, args, context) { + try { + const { file } = args; + const { config } = context; + + const { createReadStream, filename, mimetype } = await file; + let data = null; + if (createReadStream) { + const stream = createReadStream(); + data = await new Promise((resolve, reject) => { + let data = ''; + stream + .on('error', reject) + .on('data', chunk => (data += chunk)) + .on('end', () => resolve(data)); + }); + } + + if (!data || !data.length) { + throw new Parse.Error( + Parse.Error.FILE_SAVE_ERROR, + 'Invalid file upload.' + ); + } + + if (filename.length > 128) { + throw new Parse.Error( + Parse.Error.INVALID_FILE_NAME, + 'Filename too long.' + ); + } + + if (!filename.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) { + throw new Parse.Error( + Parse.Error.INVALID_FILE_NAME, + 'Filename contains invalid characters.' + ); + } + + try { + return await config.filesController.createFile( + config, + filename, + data, + mimetype + ); + } catch (e) { + logger.error('Error creating a file: ', e); + throw new Parse.Error( + Parse.Error.FILE_SAVE_ERROR, + `Could not store file: ${filename}.` + ); + } + } catch (e) { + parseGraphQLSchema.handleError(e); + } + }, + }; + + const filesMutation = new GraphQLObjectType({ + name: 'FilesMutation', + description: 'FilesMutation is the top level type for files mutations.', + fields, + }); + parseGraphQLSchema.graphQLTypes.push(filesMutation); + + parseGraphQLSchema.graphQLMutations.files = { + description: 'This is the top level for files mutations.', + type: filesMutation, + resolve: () => new Object(), + }; +}; + +export { load }; diff --git a/src/GraphQL/loaders/objectsMutations.js b/src/GraphQL/loaders/objectsMutations.js new file mode 100644 index 00000000..7623b6ee --- /dev/null +++ b/src/GraphQL/loaders/objectsMutations.js @@ -0,0 +1,148 @@ +import { GraphQLNonNull, GraphQLBoolean, GraphQLObjectType } from 'graphql'; +import * as defaultGraphQLTypes from './defaultGraphQLTypes'; +import rest from '../../rest'; + +const parseMap = { + _op: '__op', +}; + +const transformToParse = fields => { + if (!fields || typeof fields !== 'object') { + return; + } + Object.keys(fields).forEach(fieldName => { + const fieldValue = fields[fieldName]; + if (parseMap[fieldName]) { + delete fields[fieldName]; + fields[parseMap[fieldName]] = fieldValue; + } + if (typeof fieldValue === 'object') { + transformToParse(fieldValue); + } + }); +}; + +const createObject = async (className, fields, config, auth, info) => { + if (!fields) { + fields = {}; + } + + transformToParse(fields); + + return (await rest.create(config, auth, className, fields, info.clientSDK)) + .response; +}; + +const updateObject = async ( + className, + objectId, + fields, + config, + auth, + info +) => { + if (!fields) { + fields = {}; + } + + transformToParse(fields); + + return (await rest.update( + config, + auth, + className, + { objectId }, + fields, + info.clientSDK + )).response; +}; + +const deleteObject = async (className, objectId, config, auth, info) => { + await rest.del(config, auth, className, objectId, info.clientSDK); + return true; +}; + +const load = parseGraphQLSchema => { + parseGraphQLSchema.graphQLObjectsMutations.create = { + description: + 'The create mutation can be used to create a new object of a certain class.', + args: { + className: defaultGraphQLTypes.CLASS_NAME_ATT, + fields: defaultGraphQLTypes.FIELDS_ATT, + }, + type: new GraphQLNonNull(defaultGraphQLTypes.CREATE_RESULT), + async resolve(_source, args, context) { + try { + const { className, fields } = args; + const { config, auth, info } = context; + + return await createObject(className, fields, config, auth, info); + } catch (e) { + parseGraphQLSchema.handleError(e); + } + }, + }; + + parseGraphQLSchema.graphQLObjectsMutations.update = { + description: + 'The update mutation can be used to update an object of a certain class.', + args: { + className: defaultGraphQLTypes.CLASS_NAME_ATT, + objectId: defaultGraphQLTypes.OBJECT_ID_ATT, + fields: defaultGraphQLTypes.FIELDS_ATT, + }, + type: new GraphQLNonNull(defaultGraphQLTypes.UPDATE_RESULT), + async resolve(_source, args, context) { + try { + const { className, objectId, fields } = args; + const { config, auth, info } = context; + + return await updateObject( + className, + objectId, + fields, + config, + auth, + info + ); + } catch (e) { + parseGraphQLSchema.handleError(e); + } + }, + }; + + parseGraphQLSchema.graphQLObjectsMutations.delete = { + description: + 'The delete mutation can be used to delete an object of a certain class.', + args: { + className: defaultGraphQLTypes.CLASS_NAME_ATT, + objectId: defaultGraphQLTypes.OBJECT_ID_ATT, + }, + type: new GraphQLNonNull(GraphQLBoolean), + async resolve(_source, args, context) { + try { + const { className, objectId } = args; + const { config, auth, info } = context; + + return await deleteObject(className, objectId, config, auth, info); + } catch (e) { + parseGraphQLSchema.handleError(e); + } + }, + }; + + const objectsMutation = new GraphQLObjectType({ + name: 'ObjectsMutation', + description: 'ObjectsMutation is the top level type for objects mutations.', + fields: parseGraphQLSchema.graphQLObjectsMutations, + }); + parseGraphQLSchema.graphQLTypes.push(objectsMutation); + + parseGraphQLSchema.graphQLMutations.objects = { + description: 'This is the top level for objects mutations.', + type: objectsMutation, + resolve: () => new Object(), + }; +}; + +export { createObject, updateObject, deleteObject, load }; diff --git a/src/GraphQL/loaders/objectsQueries.js b/src/GraphQL/loaders/objectsQueries.js new file mode 100644 index 00000000..210c8461 --- /dev/null +++ b/src/GraphQL/loaders/objectsQueries.js @@ -0,0 +1,367 @@ +import { + GraphQLNonNull, + GraphQLBoolean, + GraphQLString, + GraphQLObjectType, +} from 'graphql'; +import getFieldNames from 'graphql-list-fields'; +import Parse from 'parse/node'; +import * as defaultGraphQLTypes from './defaultGraphQLTypes'; +import rest from '../../rest'; + +const getObject = async ( + className, + objectId, + keys, + include, + readPreference, + includeReadPreference, + config, + auth, + info +) => { + const options = {}; + if (keys) { + options.keys = keys; + } + if (include) { + options.include = include; + if (includeReadPreference) { + options.includeReadPreference = includeReadPreference; + } + } + if (readPreference) { + options.readPreference = readPreference; + } + + const response = await rest.get( + config, + auth, + className, + objectId, + options, + info.clientSDK + ); + + if (!response.results || response.results.length == 0) { + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.'); + } + + if (className === '_User') { + delete response.results[0].sessionToken; + } + + return response.results[0]; +}; + +const parseMap = { + _or: '$or', + _and: '$and', + _nor: '$nor', + _relatedTo: '$relatedTo', + _eq: '$eq', + _ne: '$ne', + _lt: '$lt', + _lte: '$lte', + _gt: '$gt', + _gte: '$gte', + _in: '$in', + _nin: '$nin', + _exists: '$exists', + _select: '$select', + _dontSelect: '$dontSelect', + _inQuery: '$inQuery', + _notInQuery: '$notInQuery', + _containedBy: '$containedBy', + _all: '$all', + _regex: '$regex', + _options: '$options', + _text: '$text', + _search: '$search', + _term: '$term', + _language: '$language', + _caseSensitive: '$caseSensitive', + _diacriticSensitive: '$diacriticSensitive', + _nearSphere: '$nearSphere', + _maxDistance: '$maxDistance', + _maxDistanceInRadians: '$maxDistanceInRadians', + _maxDistanceInMiles: '$maxDistanceInMiles', + _maxDistanceInKilometers: '$maxDistanceInKilometers', + _within: '$within', + _box: '$box', + _geoWithin: '$geoWithin', + _polygon: '$polygon', + _centerSphere: '$centerSphere', + _geoIntersects: '$geoIntersects', + _point: '$point', +}; + +const transformToParse = constraints => { + if (!constraints || typeof constraints !== 'object') { + return; + } + Object.keys(constraints).forEach(fieldName => { + let fieldValue = constraints[fieldName]; + if (parseMap[fieldName]) { + delete constraints[fieldName]; + fieldName = parseMap[fieldName]; + constraints[fieldName] = fieldValue; + } + switch (fieldName) { + case '$point': + case '$nearSphere': + if (typeof fieldValue === 'object' && !fieldValue.__type) { + fieldValue.__type = 'GeoPoint'; + } + break; + case '$box': + if ( + typeof fieldValue === 'object' && + fieldValue.bottomLeft && + fieldValue.upperRight + ) { + fieldValue = [ + { + __type: 'GeoPoint', + ...fieldValue.bottomLeft, + }, + { + __type: 'GeoPoint', + ...fieldValue.upperRight, + }, + ]; + constraints[fieldName] = fieldValue; + } + break; + case '$polygon': + if (fieldValue instanceof Array) { + fieldValue.forEach(geoPoint => { + if (typeof geoPoint === 'object' && !geoPoint.__type) { + geoPoint.__type = 'GeoPoint'; + } + }); + } + break; + case '$centerSphere': + if ( + typeof fieldValue === 'object' && + fieldValue.center && + fieldValue.distance + ) { + fieldValue = [ + { + __type: 'GeoPoint', + ...fieldValue.center, + }, + fieldValue.distance, + ]; + constraints[fieldName] = fieldValue; + } + break; + } + if (typeof fieldValue === 'object') { + transformToParse(fieldValue); + } + }); +}; + +const findObjects = async ( + className, + where, + order, + skip, + limit, + keys, + include, + includeAll, + readPreference, + includeReadPreference, + subqueryReadPreference, + config, + auth, + info, + selectedFields +) => { + if (!where) { + where = {}; + } + + transformToParse(where); + + const options = {}; + + if (selectedFields.includes('results')) { + if (limit || limit === 0) { + options.limit = limit; + } + if (options.limit !== 0) { + if (order) { + options.order = order; + } + if (skip) { + options.skip = skip; + } + if (config.maxLimit && options.limit > config.maxLimit) { + // Silently replace the limit on the query with the max configured + options.limit = config.maxLimit; + } + if (keys) { + options.keys = keys; + } + if (includeAll === true) { + options.includeAll = includeAll; + } + if (!options.includeAll && include) { + options.include = include; + } + if ((options.includeAll || options.include) && includeReadPreference) { + options.includeReadPreference = includeReadPreference; + } + } + } else { + options.limit = 0; + } + + if (selectedFields.includes('count')) { + options.count = true; + } + + if (readPreference) { + options.readPreference = readPreference; + } + if (Object.keys(where).length > 0 && subqueryReadPreference) { + options.subqueryReadPreference = subqueryReadPreference; + } + + return await rest.find( + config, + auth, + className, + where, + options, + info.clientSDK + ); +}; + +const load = parseGraphQLSchema => { + parseGraphQLSchema.graphQLObjectsQueries.get = { + description: + 'The get query can be used to get an object of a certain class by its objectId.', + args: { + className: defaultGraphQLTypes.CLASS_NAME_ATT, + objectId: defaultGraphQLTypes.OBJECT_ID_ATT, + keys: defaultGraphQLTypes.KEYS_ATT, + include: defaultGraphQLTypes.INCLUDE_ATT, + readPreference: defaultGraphQLTypes.READ_PREFERENCE_ATT, + includeReadPreference: defaultGraphQLTypes.INCLUDE_READ_PREFERENCE_ATT, + }, + type: new GraphQLNonNull(defaultGraphQLTypes.OBJECT), + async resolve(_source, args, context) { + try { + const { + className, + objectId, + keys, + include, + readPreference, + includeReadPreference, + } = args; + const { config, auth, info } = context; + + return await getObject( + className, + objectId, + keys, + include, + readPreference, + includeReadPreference, + config, + auth, + info + ); + } catch (e) { + parseGraphQLSchema.handleError(e); + } + }, + }; + + parseGraphQLSchema.graphQLObjectsQueries.find = { + description: + 'The find query can be used to find objects of a certain class.', + args: { + className: defaultGraphQLTypes.CLASS_NAME_ATT, + where: defaultGraphQLTypes.WHERE_ATT, + order: { + description: + 'This is the order in which the objects should be returned', + type: GraphQLString, + }, + skip: defaultGraphQLTypes.SKIP_ATT, + limit: defaultGraphQLTypes.LIMIT_ATT, + keys: defaultGraphQLTypes.KEYS_ATT, + include: defaultGraphQLTypes.INCLUDE_ATT, + includeAll: { + description: 'All pointers will be returned', + type: GraphQLBoolean, + }, + readPreference: defaultGraphQLTypes.READ_PREFERENCE_ATT, + includeReadPreference: defaultGraphQLTypes.INCLUDE_READ_PREFERENCE_ATT, + subqueryReadPreference: defaultGraphQLTypes.SUBQUERY_READ_PREFERENCE_ATT, + }, + type: new GraphQLNonNull(defaultGraphQLTypes.FIND_RESULT), + async resolve(_source, args, context, queryInfo) { + try { + const { + className, + where, + order, + skip, + limit, + keys, + include, + includeAll, + readPreference, + includeReadPreference, + subqueryReadPreference, + } = args; + const { config, auth, info } = context; + const selectedFields = getFieldNames(queryInfo); + + return await findObjects( + className, + where, + order, + skip, + limit, + keys, + include, + includeAll, + readPreference, + includeReadPreference, + subqueryReadPreference, + config, + auth, + info, + selectedFields + ); + } catch (e) { + parseGraphQLSchema.handleError(e); + } + }, + }; + + const objectsQuery = new GraphQLObjectType({ + name: 'ObjectsQuery', + description: 'ObjectsQuery is the top level type for objects queries.', + fields: parseGraphQLSchema.graphQLObjectsQueries, + }); + parseGraphQLSchema.graphQLTypes.push(objectsQuery); + + parseGraphQLSchema.graphQLQueries.objects = { + description: 'This is the top level for objects queries.', + type: objectsQuery, + resolve: () => new Object(), + }; +}; + +export { getObject, findObjects, load }; diff --git a/src/GraphQL/loaders/parseClassMutations.js b/src/GraphQL/loaders/parseClassMutations.js new file mode 100644 index 00000000..403faa43 --- /dev/null +++ b/src/GraphQL/loaders/parseClassMutations.js @@ -0,0 +1,121 @@ +import { GraphQLNonNull, GraphQLBoolean } from 'graphql'; +import * as defaultGraphQLTypes from './defaultGraphQLTypes'; +import * as objectsMutations from './objectsMutations'; + +const load = (parseGraphQLSchema, parseClass) => { + const className = parseClass.className; + + const classGraphQLInputType = + parseGraphQLSchema.parseClassTypes[className].classGraphQLInputType; + const fields = { + description: 'These are the fields of the object.', + type: classGraphQLInputType, + }; + const classGraphQLInputTypeFields = classGraphQLInputType.getFields(); + + const transformTypes = fields => { + if (fields) { + Object.keys(fields).forEach(field => { + if (classGraphQLInputTypeFields[field]) { + switch (classGraphQLInputTypeFields[field].type) { + case defaultGraphQLTypes.GEO_POINT: + fields[field].__type = 'GeoPoint'; + break; + case defaultGraphQLTypes.POLYGON: + fields[field] = { + __type: 'Polygon', + coordinates: fields[field].map(geoPoint => [ + geoPoint.latitude, + geoPoint.longitude, + ]), + }; + break; + } + } + }); + } + }; + + const createGraphQLMutationName = `create${className}`; + parseGraphQLSchema.graphQLObjectsMutations[createGraphQLMutationName] = { + description: `The ${createGraphQLMutationName} mutation can be used to create a new object of the ${className} class.`, + args: { + fields, + }, + type: new GraphQLNonNull(defaultGraphQLTypes.CREATE_RESULT), + async resolve(_source, args, context) { + try { + const { fields } = args; + const { config, auth, info } = context; + + transformTypes(fields); + + return await objectsMutations.createObject( + className, + fields, + config, + auth, + info + ); + } catch (e) { + parseGraphQLSchema.handleError(e); + } + }, + }; + + const updateGraphQLMutationName = `update${className}`; + parseGraphQLSchema.graphQLObjectsMutations[updateGraphQLMutationName] = { + description: `The ${updateGraphQLMutationName} mutation can be used to update an object of the ${className} class.`, + args: { + objectId: defaultGraphQLTypes.OBJECT_ID_ATT, + fields, + }, + type: defaultGraphQLTypes.UPDATE_RESULT, + async resolve(_source, args, context) { + try { + const { objectId, fields } = args; + const { config, auth, info } = context; + + transformTypes(fields); + + return await objectsMutations.updateObject( + className, + objectId, + fields, + config, + auth, + info + ); + } catch (e) { + parseGraphQLSchema.handleError(e); + } + }, + }; + + const deleteGraphQLMutationName = `delete${className}`; + parseGraphQLSchema.graphQLObjectsMutations[deleteGraphQLMutationName] = { + description: `The ${deleteGraphQLMutationName} mutation can be used to delete an object of the ${className} class.`, + args: { + objectId: defaultGraphQLTypes.OBJECT_ID_ATT, + }, + type: new GraphQLNonNull(GraphQLBoolean), + async resolve(_source, args, context) { + try { + const { objectId } = args; + const { config, auth, info } = context; + + return await objectsMutations.deleteObject( + className, + objectId, + config, + auth, + info + ); + } catch (e) { + parseGraphQLSchema.handleError(e); + } + }, + }; +}; + +export { load }; diff --git a/src/GraphQL/loaders/parseClassQueries.js b/src/GraphQL/loaders/parseClassQueries.js new file mode 100644 index 00000000..7c8a0484 --- /dev/null +++ b/src/GraphQL/loaders/parseClassQueries.js @@ -0,0 +1,102 @@ +import { GraphQLNonNull } from 'graphql'; +import getFieldNames from 'graphql-list-fields'; +import * as defaultGraphQLTypes from './defaultGraphQLTypes'; +import * as objectsQueries from './objectsQueries'; +import * as parseClassTypes from './parseClassTypes'; + +const load = (parseGraphQLSchema, parseClass) => { + const className = parseClass.className; + + const { + classGraphQLOutputType, + classGraphQLFindArgs, + classGraphQLFindResultType, + } = parseGraphQLSchema.parseClassTypes[className]; + + const getGraphQLQueryName = `get${className}`; + parseGraphQLSchema.graphQLObjectsQueries[getGraphQLQueryName] = { + description: `The ${getGraphQLQueryName} query can be used to get an object of the ${className} class by its id.`, + args: { + objectId: defaultGraphQLTypes.OBJECT_ID_ATT, + readPreference: defaultGraphQLTypes.READ_PREFERENCE_ATT, + includeReadPreference: defaultGraphQLTypes.INCLUDE_READ_PREFERENCE_ATT, + }, + type: new GraphQLNonNull(classGraphQLOutputType), + async resolve(_source, args, context, queryInfo) { + try { + const { objectId, readPreference, includeReadPreference } = args; + const { config, auth, info } = context; + const selectedFields = getFieldNames(queryInfo); + + const { keys, include } = parseClassTypes.extractKeysAndInclude( + selectedFields + ); + + return await objectsQueries.getObject( + className, + objectId, + keys, + include, + readPreference, + includeReadPreference, + config, + auth, + info + ); + } catch (e) { + parseGraphQLSchema.handleError(e); + } + }, + }; + + const findGraphQLQueryName = `find${className}`; + parseGraphQLSchema.graphQLObjectsQueries[findGraphQLQueryName] = { + description: `The ${findGraphQLQueryName} query can be used to find objects of the ${className} class.`, + args: classGraphQLFindArgs, + type: new GraphQLNonNull(classGraphQLFindResultType), + async resolve(_source, args, context, queryInfo) { + try { + const { + where, + order, + skip, + limit, + readPreference, + includeReadPreference, + subqueryReadPreference, + } = args; + const { config, auth, info } = context; + const selectedFields = getFieldNames(queryInfo); + + const { keys, include } = parseClassTypes.extractKeysAndInclude( + selectedFields + .filter(field => field.includes('.')) + .map(field => field.slice(field.indexOf('.') + 1)) + ); + const parseOrder = order && order.join(','); + + return await objectsQueries.findObjects( + className, + where, + parseOrder, + skip, + limit, + keys, + include, + false, + readPreference, + includeReadPreference, + subqueryReadPreference, + config, + auth, + info, + selectedFields.map(field => field.split('.', 1)[0]) + ); + } catch (e) { + parseGraphQLSchema.handleError(e); + } + }, + }; +}; + +export { load }; diff --git a/src/GraphQL/loaders/parseClassTypes.js b/src/GraphQL/loaders/parseClassTypes.js new file mode 100644 index 00000000..09e620a2 --- /dev/null +++ b/src/GraphQL/loaders/parseClassTypes.js @@ -0,0 +1,557 @@ +import { + Kind, + GraphQLObjectType, + GraphQLString, + GraphQLFloat, + GraphQLBoolean, + GraphQLList, + GraphQLInputObjectType, + GraphQLNonNull, + GraphQLScalarType, + GraphQLEnumType, +} from 'graphql'; +import getFieldNames from 'graphql-list-fields'; +import * as defaultGraphQLTypes from './defaultGraphQLTypes'; +import * as objectsQueries from './objectsQueries'; + +const mapInputType = (parseType, targetClass, parseClassTypes) => { + switch (parseType) { + case 'String': + return GraphQLString; + case 'Number': + return GraphQLFloat; + case 'Boolean': + return GraphQLBoolean; + case 'Array': + return new GraphQLList(defaultGraphQLTypes.ANY); + case 'Object': + return defaultGraphQLTypes.OBJECT; + case 'Date': + return defaultGraphQLTypes.DATE; + case 'Pointer': + if (parseClassTypes[targetClass]) { + return parseClassTypes[targetClass].classGraphQLScalarType; + } else { + return defaultGraphQLTypes.OBJECT; + } + case 'Relation': + if (parseClassTypes[targetClass]) { + return parseClassTypes[targetClass].classGraphQLRelationOpType; + } else { + return defaultGraphQLTypes.OBJECT; + } + case 'File': + return defaultGraphQLTypes.FILE; + case 'GeoPoint': + return defaultGraphQLTypes.GEO_POINT; + case 'Polygon': + return defaultGraphQLTypes.POLYGON; + case 'Bytes': + return defaultGraphQLTypes.BYTES; + case 'ACL': + return defaultGraphQLTypes.OBJECT; + default: + return undefined; + } +}; + +const mapOutputType = (parseType, targetClass, parseClassTypes) => { + switch (parseType) { + case 'String': + return GraphQLString; + case 'Number': + return GraphQLFloat; + case 'Boolean': + return GraphQLBoolean; + case 'Array': + return new GraphQLList(defaultGraphQLTypes.ANY); + case 'Object': + return defaultGraphQLTypes.OBJECT; + case 'Date': + return defaultGraphQLTypes.DATE; + case 'Pointer': + if (parseClassTypes[targetClass]) { + return parseClassTypes[targetClass].classGraphQLOutputType; + } else { + return defaultGraphQLTypes.OBJECT; + } + case 'Relation': + if (parseClassTypes[targetClass]) { + return new GraphQLNonNull( + parseClassTypes[targetClass].classGraphQLFindResultType + ); + } else { + return new GraphQLNonNull(defaultGraphQLTypes.FIND_RESULT); + } + case 'File': + return defaultGraphQLTypes.FILE_INFO; + case 'GeoPoint': + return defaultGraphQLTypes.GEO_POINT_INFO; + case 'Polygon': + return defaultGraphQLTypes.POLYGON_INFO; + case 'Bytes': + return defaultGraphQLTypes.BYTES; + case 'ACL': + return defaultGraphQLTypes.OBJECT; + default: + return undefined; + } +}; + +const mapConstraintType = (parseType, targetClass, parseClassTypes) => { + switch (parseType) { + case 'String': + return defaultGraphQLTypes.STRING_CONSTRAINT; + case 'Number': + return defaultGraphQLTypes.NUMBER_CONSTRAINT; + case 'Boolean': + return defaultGraphQLTypes.BOOLEAN_CONSTRAINT; + case 'Array': + return defaultGraphQLTypes.ARRAY_CONSTRAINT; + case 'Object': + return defaultGraphQLTypes.OBJECT_CONSTRAINT; + case 'Date': + return defaultGraphQLTypes.DATE_CONSTRAINT; + case 'Pointer': + if (parseClassTypes[targetClass]) { + return parseClassTypes[targetClass].classGraphQLConstraintType; + } else { + return defaultGraphQLTypes.OBJECT; + } + case 'File': + return defaultGraphQLTypes.FILE_CONSTRAINT; + case 'GeoPoint': + return defaultGraphQLTypes.GEO_POINT_CONSTRAINT; + case 'Polygon': + return defaultGraphQLTypes.POLYGON_CONSTRAINT; + case 'Bytes': + return defaultGraphQLTypes.BYTES_CONSTRAINT; + case 'ACL': + return defaultGraphQLTypes.OBJECT_CONSTRAINT; + case 'Relation': + default: + return undefined; + } +}; + +const extractKeysAndInclude = selectedFields => { + selectedFields = selectedFields.filter( + field => !field.includes('__typename') + ); + let keys = undefined; + let include = undefined; + if (selectedFields && selectedFields.length > 0) { + keys = selectedFields.join(','); + include = selectedFields + .reduce((fields, field) => { + fields = fields.slice(); + let pointIndex = field.lastIndexOf('.'); + while (pointIndex > 0) { + const lastField = field.slice(pointIndex + 1); + field = field.slice(0, pointIndex); + if (!fields.includes(field) && lastField !== 'objectId') { + fields.push(field); + } + pointIndex = field.lastIndexOf('.'); + } + return fields; + }, []) + .join(','); + } + return { keys, include }; +}; + +const load = (parseGraphQLSchema, parseClass) => { + const className = parseClass.className; + + const classFields = Object.keys(parseClass.fields); + + const classCustomFields = classFields.filter( + field => !Object.keys(defaultGraphQLTypes.CLASS_FIELDS).includes(field) + ); + + const classGraphQLScalarTypeName = `${className}Pointer`; + const parseScalarValue = value => { + if (typeof value === 'string') { + return { + __type: 'Pointer', + className, + objectId: value, + }; + } else if ( + typeof value === 'object' && + value.__type === 'Pointer' && + value.className === className && + typeof value.objectId === 'string' + ) { + return value; + } + + throw new defaultGraphQLTypes.TypeValidationError( + value, + classGraphQLScalarTypeName + ); + }; + const classGraphQLScalarType = new GraphQLScalarType({ + name: classGraphQLScalarTypeName, + description: `The ${classGraphQLScalarTypeName} is used in operations that involve ${className} pointers.`, + parseValue: parseScalarValue, + serialize(value) { + if (typeof value === 'string') { + return value; + } else if ( + typeof value === 'object' && + value.__type === 'Pointer' && + value.className === className && + typeof value.objectId === 'string' + ) { + return value.objectId; + } + + throw new defaultGraphQLTypes.TypeValidationError( + value, + classGraphQLScalarTypeName + ); + }, + parseLiteral(ast) { + if (ast.kind === Kind.STRING) { + return parseScalarValue(ast.value); + } else if (ast.kind === Kind.OBJECT) { + const __type = ast.fields.find(field => field.name.value === '__type'); + const className = ast.fields.find( + field => field.name.value === 'className' + ); + const objectId = ast.fields.find( + field => field.name.value === 'objectId' + ); + if ( + __type && + __type.value && + className && + className.value && + objectId && + objectId.value + ) { + return parseScalarValue({ + __type: __type.value.value, + className: className.value.value, + objectId: objectId.value.value, + }); + } + } + + throw new defaultGraphQLTypes.TypeValidationError( + ast.kind, + classGraphQLScalarTypeName + ); + }, + }); + parseGraphQLSchema.graphQLTypes.push(classGraphQLScalarType); + + const classGraphQLRelationOpTypeName = `${className}RelationOp`; + const classGraphQLRelationOpType = new GraphQLInputObjectType({ + name: classGraphQLRelationOpTypeName, + description: `The ${classGraphQLRelationOpTypeName} input type is used in operations that involve relations with the ${className} class.`, + fields: () => ({ + _op: { + description: 'This is the operation to be executed.', + type: new GraphQLNonNull(defaultGraphQLTypes.RELATION_OP), + }, + ops: { + description: + 'In the case of a Batch operation, this is the list of operations to be executed.', + type: new GraphQLList(new GraphQLNonNull(classGraphQLRelationOpType)), + }, + objects: { + description: + 'In the case of a AddRelation or RemoveRelation operation, this is the list of objects to be added/removed.', + type: new GraphQLList(new GraphQLNonNull(classGraphQLScalarType)), + }, + }), + }); + parseGraphQLSchema.graphQLTypes.push(classGraphQLRelationOpType); + + const classGraphQLInputTypeName = `${className}Fields`; + const classGraphQLInputType = new GraphQLInputObjectType({ + name: classGraphQLInputTypeName, + description: `The ${classGraphQLInputTypeName} input type is used in operations that involve inputting objects of ${className} class.`, + fields: () => + classCustomFields.reduce( + (fields, field) => { + const type = mapInputType( + parseClass.fields[field].type, + parseClass.fields[field].targetClass, + parseGraphQLSchema.parseClassTypes + ); + if (type) { + return { + ...fields, + [field]: { + description: `This is the object ${field}.`, + type, + }, + }; + } else { + return fields; + } + }, + { + ACL: defaultGraphQLTypes.ACL_ATT, + } + ), + }); + parseGraphQLSchema.graphQLTypes.push(classGraphQLInputType); + + const classGraphQLConstraintTypeName = `${className}PointerConstraint`; + const classGraphQLConstraintType = new GraphQLInputObjectType({ + name: classGraphQLConstraintTypeName, + description: `The ${classGraphQLConstraintTypeName} input type is used in operations that involve filtering objects by a pointer field to ${className} class.`, + fields: { + _eq: defaultGraphQLTypes._eq(classGraphQLScalarType), + _ne: defaultGraphQLTypes._ne(classGraphQLScalarType), + _in: defaultGraphQLTypes._in(classGraphQLScalarType), + _nin: defaultGraphQLTypes._nin(classGraphQLScalarType), + _exists: defaultGraphQLTypes._exists, + _select: defaultGraphQLTypes._select, + _dontSelect: defaultGraphQLTypes._dontSelect, + _inQuery: { + description: + 'This is the $inQuery operator to specify a constraint to select the objects where a field equals to any of the ids in the result of a different query.', + type: defaultGraphQLTypes.SUBQUERY, + }, + _notInQuery: { + description: + 'This is the $notInQuery operator to specify a constraint to select the objects where a field do not equal to any of the ids in the result of a different query.', + type: defaultGraphQLTypes.SUBQUERY, + }, + }, + }); + parseGraphQLSchema.graphQLTypes.push(classGraphQLConstraintType); + + const classGraphQLConstraintsTypeName = `${className}Constraints`; + const classGraphQLConstraintsType = new GraphQLInputObjectType({ + name: classGraphQLConstraintsTypeName, + description: `The ${classGraphQLConstraintsTypeName} input type is used in operations that involve filtering objects of ${className} class.`, + fields: () => ({ + ...classFields.reduce((fields, field) => { + const type = mapConstraintType( + parseClass.fields[field].type, + parseClass.fields[field].targetClass, + parseGraphQLSchema.parseClassTypes + ); + if (type) { + return { + ...fields, + [field]: { + description: `This is the object ${field}.`, + type, + }, + }; + } else { + return fields; + } + }, {}), + _or: { + description: 'This is the $or operator to compound constraints.', + type: new GraphQLList(new GraphQLNonNull(classGraphQLConstraintsType)), + }, + _and: { + description: 'This is the $and operator to compound constraints.', + type: new GraphQLList(new GraphQLNonNull(classGraphQLConstraintsType)), + }, + _nor: { + description: 'This is the $nor operator to compound constraints.', + type: new GraphQLList(new GraphQLNonNull(classGraphQLConstraintsType)), + }, + }), + }); + parseGraphQLSchema.graphQLTypes.push(classGraphQLConstraintsType); + + const classGraphQLOrderTypeName = `${className}Order`; + const classGraphQLOrderType = new GraphQLEnumType({ + name: classGraphQLOrderTypeName, + description: `The ${classGraphQLOrderTypeName} input type is used when sorting objects of the ${className} class.`, + values: classFields.reduce((orderFields, field) => { + return { + ...orderFields, + [`${field}_ASC`]: { value: field }, + [`${field}_DESC`]: { value: `-${field}` }, + }; + }, {}), + }); + parseGraphQLSchema.graphQLTypes.push(classGraphQLOrderType); + + const classGraphQLFindArgs = { + where: { + description: + 'These are the conditions that the objects need to match in order to be found.', + type: classGraphQLConstraintsType, + }, + order: { + description: 'The fields to be used when sorting the data fetched.', + type: new GraphQLList(new GraphQLNonNull(classGraphQLOrderType)), + }, + skip: defaultGraphQLTypes.SKIP_ATT, + limit: defaultGraphQLTypes.LIMIT_ATT, + readPreference: defaultGraphQLTypes.READ_PREFERENCE_ATT, + includeReadPreference: defaultGraphQLTypes.INCLUDE_READ_PREFERENCE_ATT, + subqueryReadPreference: defaultGraphQLTypes.SUBQUERY_READ_PREFERENCE_ATT, + }; + + const classGraphQLOutputTypeName = `${className}Class`; + const outputFields = () => { + return classCustomFields.reduce((fields, field) => { + const type = mapOutputType( + parseClass.fields[field].type, + parseClass.fields[field].targetClass, + parseGraphQLSchema.parseClassTypes + ); + if (parseClass.fields[field].type === 'Relation') { + const targetParseClassTypes = + parseGraphQLSchema.parseClassTypes[ + parseClass.fields[field].targetClass + ]; + const args = targetParseClassTypes + ? targetParseClassTypes.classGraphQLFindArgs + : undefined; + return { + ...fields, + [field]: { + description: `This is the object ${field}.`, + args, + type, + async resolve(source, args, context, queryInfo) { + try { + const { + where, + order, + skip, + limit, + readPreference, + includeReadPreference, + subqueryReadPreference, + } = args; + const { config, auth, info } = context; + const selectedFields = getFieldNames(queryInfo); + + const { keys, include } = extractKeysAndInclude( + selectedFields + .filter(field => field.includes('.')) + .map(field => field.slice(field.indexOf('.') + 1)) + ); + + return await objectsQueries.findObjects( + source[field].className, + { + _relatedTo: { + object: { + __type: 'Pointer', + className, + objectId: source.objectId, + }, + key: field, + }, + ...(where || {}), + }, + order, + skip, + limit, + keys, + include, + false, + readPreference, + includeReadPreference, + subqueryReadPreference, + config, + auth, + info, + selectedFields.map(field => field.split('.', 1)[0]) + ); + } catch (e) { + parseGraphQLSchema.handleError(e); + } + }, + }, + }; + } else if (parseClass.fields[field].type === 'Polygon') { + return { + ...fields, + [field]: { + description: `This is the object ${field}.`, + type, + async resolve(source) { + if (source[field] && source[field].coordinates) { + return source[field].coordinates.map(coordinate => ({ + latitude: coordinate[0], + longitude: coordinate[1], + })); + } else { + return null; + } + }, + }, + }; + } else if (type) { + return { + ...fields, + [field]: { + description: `This is the object ${field}.`, + type, + }, + }; + } else { + return fields; + } + }, defaultGraphQLTypes.CLASS_FIELDS); + }; + const classGraphQLOutputType = new GraphQLObjectType({ + name: classGraphQLOutputTypeName, + description: `The ${classGraphQLOutputTypeName} object type is used in operations that involve outputting objects of ${className} class.`, + interfaces: [defaultGraphQLTypes.CLASS], + fields: outputFields, + }); + parseGraphQLSchema.graphQLTypes.push(classGraphQLOutputType); + + const classGraphQLFindResultTypeName = `${className}FindResult`; + const classGraphQLFindResultType = new GraphQLObjectType({ + name: classGraphQLFindResultTypeName, + description: `The ${classGraphQLFindResultTypeName} object type is used in the ${className} find query to return the data of the matched objects.`, + fields: { + results: { + description: 'This is the objects returned by the query', + type: new GraphQLNonNull( + new GraphQLList(new GraphQLNonNull(classGraphQLOutputType)) + ), + }, + count: defaultGraphQLTypes.COUNT_ATT, + }, + }); + parseGraphQLSchema.graphQLTypes.push(classGraphQLFindResultType); + + parseGraphQLSchema.parseClassTypes[className] = { + classGraphQLScalarType, + classGraphQLRelationOpType, + classGraphQLInputType, + classGraphQLConstraintType, + classGraphQLConstraintsType, + classGraphQLFindArgs, + classGraphQLOutputType, + classGraphQLFindResultType, + }; + + if (className === '_User') { + const meType = new GraphQLObjectType({ + name: 'Me', + description: `The Me object type is used in operations that involve outputting the current user data.`, + interfaces: [defaultGraphQLTypes.CLASS], + fields: () => ({ + ...outputFields(), + sessionToken: defaultGraphQLTypes.SESSION_TOKEN_ATT, + }), + }); + parseGraphQLSchema.meType = meType; + parseGraphQLSchema.graphQLTypes.push(meType); + } +}; + +export { extractKeysAndInclude, load }; diff --git a/src/GraphQL/loaders/usersMutations.js b/src/GraphQL/loaders/usersMutations.js new file mode 100644 index 00000000..1bbe85cf --- /dev/null +++ b/src/GraphQL/loaders/usersMutations.js @@ -0,0 +1,110 @@ +import { + GraphQLBoolean, + GraphQLNonNull, + GraphQLObjectType, + GraphQLString, +} from 'graphql'; +import UsersRouter from '../../Routers/UsersRouter'; +import * as defaultGraphQLTypes from './defaultGraphQLTypes'; +import * as objectsMutations from './objectsMutations'; + +const usersRouter = new UsersRouter(); + +const load = parseGraphQLSchema => { + const fields = {}; + + fields.signUp = { + description: 'The signUp mutation can be used to sign the user up.', + args: { + fields: { + descriptions: 'These are the fields of the user.', + type: parseGraphQLSchema.parseClassTypes['_User'].classGraphQLInputType, + }, + }, + type: new GraphQLNonNull(defaultGraphQLTypes.SIGN_UP_RESULT), + async resolve(_source, args, context) { + try { + const { fields } = args; + const { config, auth, info } = context; + + return await objectsMutations.createObject( + '_User', + fields, + config, + auth, + info + ); + } catch (e) { + parseGraphQLSchema.handleError(e); + } + }, + }; + + fields.logIn = { + description: 'The logIn mutation can be used to log the user in.', + args: { + username: { + description: 'This is the username used to log the user in.', + type: new GraphQLNonNull(GraphQLString), + }, + password: { + description: 'This is the password used to log the user in.', + type: new GraphQLNonNull(GraphQLString), + }, + }, + type: new GraphQLNonNull(parseGraphQLSchema.meType), + async resolve(_source, args, context) { + try { + const { username, password } = args; + const { config, auth, info } = context; + + return (await usersRouter.handleLogIn({ + body: { + username, + password, + }, + query: {}, + config, + auth, + info, + })).response; + } catch (e) { + parseGraphQLSchema.handleError(e); + } + }, + }; + + fields.logOut = { + description: 'The logOut mutation can be used to log the user out.', + type: new GraphQLNonNull(GraphQLBoolean), + async resolve(_source, _args, context) { + try { + const { config, auth, info } = context; + + await usersRouter.handleLogOut({ + config, + auth, + info, + }); + return true; + } catch (e) { + parseGraphQLSchema.handleError(e); + } + }, + }; + + const usersMutation = new GraphQLObjectType({ + name: 'UsersMutation', + description: 'UsersMutation is the top level type for files mutations.', + fields, + }); + parseGraphQLSchema.graphQLTypes.push(usersMutation); + + parseGraphQLSchema.graphQLMutations.users = { + description: 'This is the top level for users mutations.', + type: usersMutation, + resolve: () => new Object(), + }; +}; + +export { load }; diff --git a/src/GraphQL/loaders/usersQueries.js b/src/GraphQL/loaders/usersQueries.js new file mode 100644 index 00000000..1c139ee4 --- /dev/null +++ b/src/GraphQL/loaders/usersQueries.js @@ -0,0 +1,36 @@ +import { GraphQLNonNull, GraphQLObjectType } from 'graphql'; +import UsersRouter from '../../Routers/UsersRouter'; + +const usersRouter = new UsersRouter(); + +const load = parseGraphQLSchema => { + const fields = {}; + + fields.me = { + description: 'The Me query can be used to return the current user data.', + type: new GraphQLNonNull(parseGraphQLSchema.meType), + async resolve(_source, _args, context) { + try { + const { config, auth, info } = context; + return (await usersRouter.handleMe({ config, auth, info })).response; + } catch (e) { + parseGraphQLSchema.handleError(e); + } + }, + }; + + const usersQuery = new GraphQLObjectType({ + name: 'UsersQuery', + description: 'UsersQuery is the top level type for users queries.', + fields, + }); + parseGraphQLSchema.graphQLTypes.push(usersQuery); + + parseGraphQLSchema.graphQLQueries.users = { + description: 'This is the top level for users queries.', + type: usersQuery, + resolve: () => new Object(), + }; +}; + +export { load }; diff --git a/src/index.js b/src/index.js index 5082eb3a..4bfc2934 100644 --- a/src/index.js +++ b/src/index.js @@ -10,6 +10,7 @@ import { useExternal } from './deprecated'; import { getLogger } from './logger'; import { PushWorker } from './Push/PushWorker'; import { ParseServerOptions } from './Options'; +import { ParseGraphQLServer } from './GraphQL/ParseGraphQLServer'; // Factory function const _ParseServer = function(options: ParseServerOptions) { @@ -37,5 +38,6 @@ export { LRUCacheAdapter, TestUtils, PushWorker, + ParseGraphQLServer, _ParseServer as ParseServer, };