Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

@wayfair/node-froid

wayfair-incubator5.4kMIT3.2.2TypeScript support: included

Federated GQL Relay Object Identification implementation

gql, graphql, federation, federated, relay, api, global, object, identification, id

readme

node-froid: NodeJS - Federated Relay Object Identification

Release Lint codecov Contributor Covenant license: MIT Maintainer

Table of Contents

About The Project

The problem

There isn't good support for the Relay's Object Identification spec in the Federated GraphQL ecosystem. This makes it difficult to support common patterns used to refetch objects from your graph to power things like cache TTLs and cache-miss hydration.

The solution

@wayfair/node-froid provides two key pieces of functionality:

  • id processing: a solution that can be used to run inline as part of a custom Data Source or as a completely separate subgraph (recommended) dedicated to service your object identification implementation .
  • schema generation: a schema generation script that reflects on all subgraphs in your federated graph and generates a valid relay object identification schema.
    • Can run in Federation v1 or v2 mode
    • Supports contracts!

Getting Started

To get a local copy up and running follow these simple steps.

Installation

This module is distributed via npm which is bundled with node and should be installed as one of your project's direct dependencies:

npm install @wayfair/node-froid

or

for installation via yarn

yarn add @wayfair/node-froid

This library has peerDependencies listings for graphql and graphql-relay.

Library API

handleFroidRequest

Parameter Name Required Description Type Default
request Yes The request object passed to the froid subgraph see specific properties
request.query Yes The query string for the request string
request.variables Yes The variables for the request Record<string, unknown>
options | Configuration options available to handleFroidRequest see specific properties {}
options.encode | A callback for encoding the object identify key values (string) => string (keyString) => keyString
options.decode | A callback for decoding an object identifier's key values (string) => string (keyString) => keyString
options.cache | Cache to use to avoid re-parsing query documents FroidCache

Returns Promise<object[]>: A promise representing the list of entity objects containing a relay-spec compliant id value.

generateFroidSchema

Parameter Name Required Description Type Default
subgraphSchemaMap Yes A mapping of subgraph names --> subgraph SDLs used to generate the froid schema Map<string, string>
froidSubgraphName Yes The name of the relay subgraph service string
options | Optional configuration for schema generation see specific properties {}
options.contractTags | A list of supported contract tags string[] []
options.federatedVersion | The version of federation to generate schema for FederationVersion FederationVersion.V2
options.typeExceptions | Types to exclude from id field generation string[] []
options.nodeQualifier | A custom function to qualify whether or not an entity should be included in node-relay schema (node: DefinitionNode, objectTypes: Record<string, ObjectTypeNode>) => boolean

Returns DocumentNode[]: The froid schema

Usage

id Processing

Custom GraphQL Gateway Datasource

import {GraphQLDataSourceProcessOptions} from '@apollo/gateway';
import {GraphQLResponse} from 'apollo-server-types';
import {handleFroidRequest} from '@wayfair/node-froid';
import {Context} from './path/to/your/ContextType';

class FroidDataSource {
  process({
    request,
  }: Pick<
    GraphQLDataSourceProcessOptions<Context>,
    'request'
  >): Promise<GraphQLResponse> {
    return await handleFroidRequest(request);
  }
}

Custom GraphQL Gateway Datasource w/Encryption

// Datasource Implementation
import {GraphQLDataSourceProcessOptions} from '@apollo/gateway';
import {GraphQLResponse} from 'apollo-server-types';
import {
  DecodeCallback,
  EncodeCallback,
  handleFroidRequest,
} from '@wayfair/node-froid';
// You only really need this if you are using context
import {Context} from './path/to/your/ContextType';
// Used to determine which encoder to use
import {FeatureToggleManager} from './path/to/your/FeatureToggleManager';

// Interface we need to match properly encode key values
interface Encoder {
  encode: EncodeCallback;
  decode: DecodeCallback;
}

class FroidLDataSource {
  private encoder1: Encoder;
  private encoder2: Encoder;

  // Take two encoders to support live key rotation
  constructor(encoder1: Encoder, encoder2, Encoder) {
    this.encoder1 = encoder1;
    this.encoder2 = encoder2;
  }

  process({
    request,
  }: Pick<
    GraphQLDataSourceProcessOptions<Context>,
    'request'
  >): Promise<GraphQLResponse> {
    const encoder = FeatureToggleManager.useEncoder1()
      ? this.encoder1
      : this.encoder2;

    return await handleFroidRequest(request, {...encoder});
  }
}

// Sample Encoder
import crypto from 'crypto';
import {DecodeCallback, EncodeCallback} from '@wayfair/node-froid';

const ENCRYPTION_ALGORITHM = 'aes-256-cbc';

// Interface we need to match properly encode key values
interface Encoder {
  encode: EncodeCallback;
  decode: DecodeCallback;
}

type CreateEncoderArguments = {
  base64InitializationVector: string;
  base64EncryptionKey: string;
};

export class CustomEncoder implements Encoder {
  private iv: Buffer;
  private key: Buffer;

  constructor({
    base64InitializationVector,
    base64EncryptionKey,
  }: CreateEncoderArguments) {
    this.iv = Buffer.from(base64InitializationVector, 'base64');
    this.key = Buffer.from(base64EncryptionKey, 'base64');
  }

  public encode(value: string): string {
    const cipher = crypto.createCipheriv(
      ENCRYPTION_ALGORITHM,
      this.key,
      this.iv
    );
    const encryptedValue = cipher.update(value);
    const encryptedBuffer = Buffer.concat([encryptedValue, cipher.final()]);

    return encryptedBuffer.toString('base64');
  }

  public decode(value: string): object {
    const decipher = crypto.createDecipheriv(
      ENCRYPTION_ALGORITHM,
      this.key,
      this.iv
    );
    const decryptedValue = decipher.update(Buffer.from(value, 'base64'));
    const decryptedBuffer = Buffer.concat([decryptedValue, decipher.final()]);

    return decryptedBuffer.toString();
  }
}

Custom GraphQL Gateway Datasource w/Cache

import {GraphQLDataSourceProcessOptions} from '@apollo/gateway';
import {GraphQLResponse} from 'apollo-server-types';
import {handleRelayRequest} from '@wayfair/node-froid';
import {Context} from './path/to/your/ContextType';
import LRU from 'lru-cache';

const cache = new LRU({max: 500});

class RelayNodeGraphQLDataSource {
  process({
    request,
  }: Pick<
    GraphQLDataSourceProcessOptions<Context>,
    'request'
  >): Promise<GraphQLResponse> {
    return await handleRelayRequest(request, {
      cache,
    });
  }
}

Subgraph w/Express Server

import express from 'express';
import bodyParser from 'body-parser';
import {handleFroidRequest} from '@wayfair/node-froid';

const port = process.env.PORT || 5000;

const app = express();
app.use(bodyParser.urlencoded({extended: false}));
app.use(bodyParser.json());

// No need to run a full GraphQL server.
// Avoid the additional overhead and manage the route directly instead!
app.post('/graphql', async (req, res) => {
  const result = await handleFroidRequest(req.body);
  res.send(result);
});

app.listen(port, () => {
  console.log(`Froid subgraph listening on port ${port}`);
});

Schema Generation

Basic Script

import fs from 'fs';
import {print} from 'graphql';
import {generateFroidSchema} from '@wayfair/node-froid';
// You have to provide this. Apollo's public API should provide the ability to extract out subgraph SDL
import {getFederatedSchemas} from './getFederatedSchemas';

const froidSubgraphName = 'froid-service';
const variant = 'current';

// Map<string, string> where the key is the subgraph name, and the value is the SDL schema
const subgraphSchemaMap = getFederatedSchemas(variant);

const schemaAst = generateFroidSchema(subgraphSchemaMap, froidSubgraphName);

// persist results to a file to use with rover publish
fs.writeFileSync('schema.graphql', print(schemaAst));

Roadmap

See the open issues for a list of proposed features (and known issues).

Contributing

Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated. For detailed contributing guidelines, please see CONTRIBUTING.md

License

Distributed under the MIT License. See LICENSE for more information.

Contact

Project Link: https://github.com/wayfair-incubator/node-froid

Acknowledgements

This template was adapted from https://github.com/othneildrew/Best-README-Template.

changelog

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[v3.2.2] - 2024-08-20

Fixed

  • Improves the error message surfaced in cases where FROID generates an empty key.

[v3.2.1] - 2024-04-29

Fixed

  • Fixes a case where sub-entity keys failed to roll up properly to referencing entity keys when a value type was in-between.

[v3.2.0] - 2024-03-22

Added

  • Added a new option for an omittedEntityQualifier to re-evaluate and include entities that may have been erroneously omitted by the nodeQualifier. This provided the flexibility to fix missing entities while preserving previous behavior

[v3.1.1] - 2024-02-15

Fix

  • The FroidSchema class does not include all enum values found across subgraphs when enum definitions differ.

[v3.1.0] - 2023-11-09

  • Added a new FroidSchema class to test the next version of FROID schema generation.
  • FROID schema is now sorted (both new and old version) and include documentation string.

[v3.0.1] - 2023-08-17

Fix

  • Applying the @tag directive to an entity's id field could fail if the only @tag directive in the entity was applied to a field argument. This fix now considers field argument @tags as well as field @tags when selecting the id field's @tag(s).

[v3.0.0] - 2023-08-16

Breaking

  • The federation version is no longer provided as an enum value. It must now be provided as a string of either v1 or a valid v2.x version (examples: v2.1, v2.3, etc.).
  • Fixes to complex key schema generation and federation v1 value type schema generation could effect the generated schema. Please carefully compare schema generated with the previous version against schema generated after upgrading.

Added

  • Added support for a custom key sorter. This allows for a custom key preference to be applied prior to selecting the first key.
  • Added support for explicitly defining the federation version, either v1 or a valid v2.x version (examples: v2.1, v2.3, etc.)

Fix

  • In some cases, if a complex key included a nested entity but was not using the entity's key, schema generation would fail to include the nested entity's key field(s).
  • In some cases, if a type appeared in multiple subgraphs and was being used in multiple complex keys but with different field selections, not all fields would be included in the generated schema.
  • When generating Federation v1 schema, value types would erroneously receive the extend keyword and their fields would erroneously receive the @external directive.

[v2.2.0] - 2023-06-27

Added

  • Add support for a custom schema node qualifier when generating node-relay schema so users can determine whether or not to include an entity in the generated node-relay schema based on custom criteria

[v2.1.0] - 2023-06-01

Added

  • Omit @interfaceObjects prior to generating node-relay schema to avoid breaking federation composition

[v2.0.1] - 2022-11-22

Fix

  • Sort the key values encoded in an id to ensure they are deterministically generated

[v2.0.0] - 2022-11-22

Breaking

  • @key directive selection in schema generation now picks the first directive where fields doesn't specify a nested complex key.

Fix

  • Fix applied to when entities are specified in a nested complex key field, they were generated as non-entity types in the Froid schema.
  • Fix issue where non-resolvable entity references in Federation 2 were processed during Froid schema generation

[v1.1.0] - 2022-11-22

Fix

  • Support enum generation specified as @key fields.

[Unreleased]

[v1.0.1] - 2022-10-10

Added

  • cleanup distributed files when publishing packages

[v1.0.0] - 2022-10-04

Added

  • Public release
  • feat: add better error handling support

[v0.2.0] - 2022-10-02

Breaking

  • Updated handleFroidRequest.options.decode API from string -> object to string -> string to remove inconsistency across the encode/decode APIs. Now all JSON parsing happens in the core implementation.

[v0.1.1] - 2022-10-01

Added

  • Properly export typescript types in published package
  • Include dist directory in published package

[v0.1.0] - 2022-09-28

Added

  • Add initial library API:
    • handleFroidRequest: Handles both id generation as well as node field parsing of Object Identification requests in Federation.
    • generateFroidSchema: Generates schema for the Froid subgraph, complete with Federation 1 & 2 support, as well as Apollo contracts!