From 38fd7011dacd40b20d37d0d142648fbfc72aa379 Mon Sep 17 00:00:00 2001 From: Dariusz Kuc <9501705+dariuszkuc@users.noreply.github.com> Date: Tue, 25 Oct 2022 15:15:36 -0500 Subject: [PATCH] Dgraph integration (#253) Dgraph currently has very limited federation support. Due to the limitations in schema declaration it is not possible to implement the expected schema: * `ID` fields are autogenerated and cannot have custom values (in order to set custom ID we need to use `String @id`) * unable to create type with just `ID` column (as it doesn't make sense to have a table with a single ID column) * unable to disable auto generated field aggregate query (`Product.reserach`) * only single field `@key` values are supported, multi column or complex type keys are not supported * external fields have to be nullable --- .github/workflows/test-subgraph-dgraph.yaml | 14 +++++ README.md | 3 +- implementations/dgraph/docker-compose.yaml | 62 ++++++++++++++++++++ implementations/dgraph/metadata.yaml | 3 + implementations/dgraph/populateData.json | 65 +++++++++++++++++++++ implementations/dgraph/products.graphql | 54 +++++++++++++++++ implementations/dgraph/proxy.conf.template | 8 +++ implementations/dgraph/resolvers.js | 43 ++++++++++++++ src/index.ts | 2 +- 9 files changed, 252 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/test-subgraph-dgraph.yaml create mode 100644 implementations/dgraph/docker-compose.yaml create mode 100644 implementations/dgraph/metadata.yaml create mode 100644 implementations/dgraph/populateData.json create mode 100644 implementations/dgraph/products.graphql create mode 100644 implementations/dgraph/proxy.conf.template create mode 100644 implementations/dgraph/resolvers.js diff --git a/.github/workflows/test-subgraph-dgraph.yaml b/.github/workflows/test-subgraph-dgraph.yaml new file mode 100644 index 000000000..a4ae2d99e --- /dev/null +++ b/.github/workflows/test-subgraph-dgraph.yaml @@ -0,0 +1,14 @@ +name: Dgraph Test + +on: + pull_request: + branches: + - main + paths: + - 'implementations/dgraph/**' + +jobs: + compatibility: + uses: ./.github/workflows/test-subgraph.yaml + with: + library: "dgraph" \ No newline at end of file diff --git a/README.md b/README.md index 89c395271..bd581d5bd 100644 --- a/README.md +++ b/README.md @@ -159,8 +159,9 @@ The following open-source GraphQL server libraries and hosted subgraphs provide AWS AppSync
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
-StepZen
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🔲
repeatable @key🔲
@requires🟢
@provides🟢
federated tracing🔲
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
+Dgraph
_service
@key (single)🟢
@key (multi)🔲
@key (composite)🔲
repeatable @key🔲
@requires🔲
@provides🔲
federated tracing🔲
@link
@shareable🔲
@tag🔲
@override🔲
@inaccessible🔲
GraphQL Mesh
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
+StepZen
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🔲
repeatable @key🔲
@requires🟢
@provides🟢
federated tracing🔲
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
diff --git a/implementations/dgraph/docker-compose.yaml b/implementations/dgraph/docker-compose.yaml new file mode 100644 index 000000000..bd2734f31 --- /dev/null +++ b/implementations/dgraph/docker-compose.yaml @@ -0,0 +1,62 @@ +# all path must be relative to the root of the project +services: + dgraph: + image: dgraph/standalone:v21.03.2 + environment: + DGRAPH_ALPHA_GRAPHQL: 'lambda-url=http://dgraph_lambda:8686/graphql-worker' + ports: + - "8080:8080" + - "9080:9080" + - "8000:8000" + volumes: + - dgraph:/dgraph + healthcheck: + test: ["CMD", "curl", "-X", "GET", "http://dgraph:8080/admin?query=\\{health\\{status\\}\\}"] + interval: 15s + timeout: 10s + retries: 20 + + dgraph_lambda: + image: dgraph/dgraph-lambda:1.4.0 + depends_on: + - dgraph + ports: + - "8686:8686" + environment: + DGRAPH_URL: http://dgraph:8080 + MAX_MEMORY_LIMIT: 192M + volumes: + - ./implementations/dgraph/resolvers.js:/app/script/script.js:ro + + dgraph_data: + image: curlimages/curl:7.85.0 + depends_on: + dgraph: + condition: service_healthy + restart: "no" + working_dir: /app + volumes: + - ./implementations/dgraph/products.graphql:/app/products.graphql:ro + - ./implementations/dgraph/populateData.json:/app/populateData.json:ro + command: + - /bin/sh + - -c + - | + curl -v -X POST http://dgraph:8080/admin/schema --data-binary '@products.graphql' + curl -v -X POST -H 'Content-Type:application/json' http://dgraph:8080/graphql --data-binary '@populateData.json' + + products: + image: nginx:alpine + ports: + - 4001:4001 + depends_on: + dgraph: + condition: service_healthy + environment: + DGRAPH_URL: http://dgraph:8080/graphql + volumes: + - ./implementations/dgraph/proxy.conf.template:/etc/nginx/conf.d/proxy.conf.template + command: /bin/sh -c "envsubst < /etc/nginx/conf.d/proxy.conf.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'" + +volumes: + dgraph: {} \ No newline at end of file diff --git a/implementations/dgraph/metadata.yaml b/implementations/dgraph/metadata.yaml new file mode 100644 index 000000000..3e7fa4af4 --- /dev/null +++ b/implementations/dgraph/metadata.yaml @@ -0,0 +1,3 @@ +fullName: Dgraph +language: Other Solutions +documentation: https://dgraph.io/docs/graphql/overview/ diff --git a/implementations/dgraph/populateData.json b/implementations/dgraph/populateData.json new file mode 100644 index 000000000..639069747 --- /dev/null +++ b/implementations/dgraph/populateData.json @@ -0,0 +1,65 @@ +{ + "query": "mutation populateData($products: [AddProductInput!]!, $deprecatedProd: [AddDeprecatedProductInput!]!) { addProduct(input: $products) { numUids }, addDeprecatedProduct(input: $deprecatedProd) { numUids } }", + "variables": { + "products": [{ + "id": "apollo-federation", + "sku": "federation", + "package": "@apollo/federation", + "variation": { + "id": "OSS" + }, + "dimensions": { + "size": "small", + "weight": 1, + "unit": "kg" + }, + "research": [ + { + "study": { + "caseNumber": "1234", + "description": "Federation Study" + } + } + ], + "createdBy": { + "email": "support@apollographql.com", + "name": "Jane Smith", + "totalProductsCreated": 1337 + } + },{ + "id": "apollo-studio", + "sku": "studi", + "variation": { + "id": "platform" + }, + "dimensions": { + "size": "small", + "weight": 1, + "unit": "kg" + }, + "research": [ + { + "study": { + "caseNumber": "1235", + "description": "Studio Study" + } + } + ], + "createdBy": { + "email": "support@apollographql.com", + "name": "Jane Smith", + "totalProductsCreated": 1337 + } + }], + "deprecatedProd": [{ + "sku": "apollo-federation-v1", + "package": "@apollo/federation-v1", + "reason": "Migrate to Federation V2", + "createdBy": { + "email": "support@apollographql.com", + "name": "Jane Smith", + "totalProductsCreated": 1337 + } + }] + } +} diff --git a/implementations/dgraph/products.graphql b/implementations/dgraph/products.graphql new file mode 100644 index 000000000..a08deee66 --- /dev/null +++ b/implementations/dgraph/products.graphql @@ -0,0 +1,54 @@ +type Product @key(fields: "id") @generate(query: { get: true, query: false, aggregate: false }) { + # dgraph does not support custom values for ID fields + id: String! @id + sku: String + package: String + variation: ProductVariation + dimensions: ProductDimension + createdBy: User @provides(fields: "totalProductsCreated") + notes: String + research: [ProductResearch!]! +} + +type DeprecatedProduct @generate(query: { get: true, query: false, aggregate: false }) { + sku: String! @id + package: String! @id + reason: String + createdBy: User +} + +type ProductVariation @generate(query: { get: false, query: false, aggregate: false }) { + id: String! @id + # dgraph does not allow creating types with just ID value + dummy: String +} + +type ProductResearch @generate(query: { get: false, query: false, aggregate: false }) { + study: CaseStudy! + outcome: String +} + +type CaseStudy @generate(query: { get: false, query: false, aggregate: false }) { + caseNumber: String! @id + description: String +} + +type ProductDimension @generate(query: { get: false, query: false, aggregate: false }) { + size: String + weight: Float + unit: String +} + +type Query { + product(id: String!): Product @lambda + deprecatedProduct(sku: String!, package: String!): DeprecatedProduct @deprecated(reason: "Use product query instead") @lambda +} + +extend type User @key(fields: "email") @generate(query: { get: false, query: false, aggregate: false }) { + averageProductsCreatedPerYear: Int @requires(fields: "totalProductsCreated yearsOfEmployment") @lambda + email: String! @id + name: String + totalProductsCreated: Int @external + # dgraph does not support non-nullable external types + yearsOfEmployment: Int @external +} diff --git a/implementations/dgraph/proxy.conf.template b/implementations/dgraph/proxy.conf.template new file mode 100644 index 000000000..97cdc5625 --- /dev/null +++ b/implementations/dgraph/proxy.conf.template @@ -0,0 +1,8 @@ +server { + listen 4001; + server_name products; + location / { + proxy_ssl_server_name on; + proxy_pass ${DGRAPH_URL}; + } +} \ No newline at end of file diff --git a/implementations/dgraph/resolvers.js b/implementations/dgraph/resolvers.js new file mode 100644 index 000000000..46bfe5172 --- /dev/null +++ b/implementations/dgraph/resolvers.js @@ -0,0 +1,43 @@ +async function productById({ args, graphql }) { + const results = await graphql(`query GetProductById($productId: String!) { + getProduct(id: $productId) { + id + sku + package + variation { id } + dimensions { size weight unit } + createdBy { email name totalProductsCreated } + research { study { caseNumber description }} + } + }`, {"productId": args.id}) + return results.data.getProduct + } + +async function deprecatedProductBySkuAndPackage({ args, graphql }) { + console.log("executing deprecated product by sku and package", args) + const results = await graphql(`query GetDeprecatedProductBySkuAndPackage($sku: String!, $pkg: String!) { + getDeprecatedProduct(sku: $sku, package: $pkg) { + sku + package + reason + createdBy { email name totalProductsCreated } + } + }`, {"sku": args.sku, "pkg": args.package}) + console.log("executed query") + console.log("results", results) + return results.data.getDeprecatedProduct +} + +async function userAverageProductsCreatedPerYear({parent: {totalProductsCreated, yearsOfEmployment}}) { + if (totalProductsCreated) { + Math.round(totalProductsCreated / yearsOfEmployment) + } else { + null + } +} + +self.addGraphQLResolvers({ + "Query.product": productById, + "Query.deprecatedProduct": deprecatedProductBySkuAndPackage, + "User.averageProductsCreatedPerYear": userAverageProductsCreatedPerYear +}) diff --git a/src/index.ts b/src/index.ts index 2418d14c7..4ecc9f4ba 100644 --- a/src/index.ts +++ b/src/index.ts @@ -54,7 +54,7 @@ async function runDockerCompose(libraryName: string, librariesPath: string) { return async () => { console.log(`Stopping ${libraryName} and related containers...`); - await execa("docker-compose", ["down", "--remove-orphans"]); + await execa("docker-compose", ["down", "--remove-orphans", "-v"]); }; }