{"id":13725561,"url":"https://github.com/zth/relay-utils","last_synced_at":"2025-03-24T02:32:03.416Z","repository":{"id":57144942,"uuid":"185052440","full_name":"zth/relay-utils","owner":"zth","description":"Utilities for working with Relay (modern) in general, and the Relay store in particular.","archived":false,"fork":false,"pushed_at":"2019-05-05T16:08:26.000Z","size":134,"stargazers_count":35,"open_issues_count":0,"forks_count":3,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-10-06T10:47:43.963Z","etag":null,"topics":["graphql","reactjs","relay","relay-modern"],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zth.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-05-05T15:56:56.000Z","updated_at":"2023-09-18T15:24:29.000Z","dependencies_parsed_at":"2022-09-05T18:40:22.463Z","dependency_job_id":null,"html_url":"https://github.com/zth/relay-utils","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zth%2Frelay-utils","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zth%2Frelay-utils/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zth%2Frelay-utils/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zth%2Frelay-utils/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zth","download_url":"https://codeload.github.com/zth/relay-utils/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":221931696,"owners_count":16903799,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["graphql","reactjs","relay","relay-modern"],"created_at":"2024-08-03T01:02:27.424Z","updated_at":"2024-10-28T20:36:27.440Z","avatar_url":"https://github.com/zth.png","language":"JavaScript","readme":"# relay-utils\n\nThis package contains utilities for working with Relay (modern) in general, and the Relay store + updates to the store in particular.\n\n**Looking for contributions for TypeScript definitions**. I don't actively use TypeScript myself, but I'll gladly accept PRs for TS type definitions.\n\n## Installation\n\n`yarn add relay-utils`\n\n## Table of Contents\n\n- [collectConnectionNodes](#collectconnectionnodes) - Type safe way of collecting all nodes from a connection.\n- [createRelayDataId](#createrelaydataid) - Create Relay data IDs.\n- [resolveNestedRecord](#resolvenestedrecord) - Resolves a record in the store from a path.\n- [setFieldsOnRecord](#setfieldsonrecord) - Sets fields of provided object on a record.\n- [createAndAddNodeToStore](#createandaddnodetostore) - Creates a node of given type and shape and adds it to the store.\n- [createAndAddEdgeToConnections](#createandaddedgetoconnections) - Creates an edge for a node and adds that edge + node to one or more connections.\n- [createAndAddEdgeToLinkedRecords](#createandaddedgetolinkedrecords) - Same as `createAndAddEdgeToConnection` but for linked records.\n- [removeNodeFromStore](#removenodefromstore) - Removes a node and cleans it + edges from connections/linked records.\n- [createMissingFieldsHandler](#createmissingfieldshandler) - Creates handlers for resolving missing fields in the store.\n\n## Usage\n\n_NOTE_ A lot of these helpers assume that you're using `graphql-relay-js` style data IDs that are a combination of a database id and a typename.\nPlease file an issue if you don't and would like to use this package still.\n\n### collectConnectionNodes\n\nTakes a connection, collects all nodes from the edges, and filters out nulls.\n\n#### Example\n\nWhenever you use a connection (with or without the `@connection`-annotation)...\n\n```graphql\nfragment SomeFragment_user on User {\n  pets(first: 10) @connection(key: \"SomeFragment_user_pets\") {\n    pageInfo {\n      hasNextPage\n      endCursor\n    }\n    edges {\n      node {\n        id\n        name\n      }\n    }\n  }\n}\n```\n\n...and you want to map over or just use the connection nodes, you can use `collectConnectionNodes`:\n\n```javascript\ntype Props = {|\n  user: SomeFragment_user\n|};\n\nconst MyFragmentComponent = ({ user }: Props) =\u003e {\n  // This is fully type safe, so pets is now $ReadOnlyArray\u003c{| +id: string, +name: string |}\u003e\n  const pets = collectConnectionNodes(user ? user.pets : null);\n\n  return pets.map(pet =\u003e (\n    \u003cdiv key={pet.id}\u003e\n      \u003ch3\u003e{pet.name}\u003c/h3\u003e\n    \u003c/div\u003e\n  ));\n};\n```\n\n### createRelayDataId\n\nA simple function to create data IDs for Relay. Useful for a number of scenarios.\n\n#### Example\n\nImagine you have a field in your schema for each type that represents the database id for that object, like this:\n\n```graphql\ntype Pet {\n  id: ID! # The Relay data id from the server\n  _id: ID! # Your internal database ID that you use when querying for just this node, using it in mutations etc\n}\n```\n\nNow, imagine you want to look for a `Pet` in the store without passing its data id all the way to your updater when you already have the `_id` database id:\n\n```javascript\nconst updater = store =\u003e {\n  // This gets the Pet with database id petInput.id\n  const petNode = store.get(\n    createRelayDataId(\n      petInput.id, // The database id\n      'Pet'\n    )\n  );\n};\n```\n\n#### Signature\n\n```javascript\nfunction createRelayDataId(\n  databaseId: string,\n  typename: string,\n  base64Encode?: (data: string) =\u003e string // If you're doing SSR or similar, you can pass your own base64 function here. If nothing is passed, it defaults to trying to use the browser built-in btoa.\n  ): string\n```\n\n##### Hint: Pair with `graphql-generate-flow-schema-assets` for type safety\n\n`graphql-generate-flow-schema-assets` generates Flow types for your enums and object types from your `schema.graphql`. You can use that output to create your own, type safe variant of `createRelayDataId` like this:\n\n```javascript\nimport type { ObjectTypesEnum } from '../path/to/the/generated/object-types.js';\n\n// Ensures only object types in your schema can be passed to the creator function\nexport function myCreateRelayDataId(\n  databaseId: string,\n  objName: ObjectTypesEnum\n): string {\n  return createRelayDataId(databaseId, objName);\n}\n```\n\n### resolveNestedRecord\n\nTries to resolve linked records from a `path` you give it.\n\n#### Example\n\nFor this data structure:\n\n```graphql\ntype RootQuery {\n  viewer: User\n}\n\ntype User {\n  name: string\n  favoritePet: Pet\n}\n\ntype Pet {\n  owner: User!\n}\n\nquery ViewerFavoritePetQuery {\n  viewer {\n    favoritePet {\n      owner {\n        name\n      }\n    }\n  }\n}\n```\n\n...this accessor would work:\n\n```javascript\nconst updater = store =\u003e {\n  const petOwner = resolveNestedRecord(store.getRoot(), [\n    'viewer',\n    'favoritePet',\n    'owner'\n  ]);\n\n  // You can use it on any record\n  const viewerNode = store.get(viewerDataId);\n  const petOwner = resolveNestedRecord(viewerNode, ['favoritePet', 'owner']);\n};\n```\n\n#### Signature\n\n```javascript\nfunction resolveNestedRecord(\n  rootNode: RecordProxy, // store.getRoot() for the root, but could be any record retrieved from the store\n  path: Array\u003cstring\u003e // Array of strings making up a path, like ['viewer', 'favoritePet', 'owner']\n): ?RecordProxy\n```\n\n### setFieldsOnRecord\n\nSimple helper to set values of object on record.\n\n#### Example\n\n```javascript\nimport { commitLocalUpdate } from 'relay-runtime';\n\ncommitLocalUpdate(environment, store =\u003e {\n  const someUserNode = store.get(someUserDataId);\n\n  setFieldsOnRecord(someUserNode, {\n    name: 'Some Name',\n    age: 59\n  });\n});\n```\n\n#### Signature\n\n```javascript\nfunction setFieldsOnRecord(\n  record: RecordProxy,\n  fieldsObj: { [key: string]: mixed }\n): void\n```\n\n### createAndAddNodeToStore\n\nCreates a node of a given type and shape, and adds it to the store.\n\n#### Example\n\n```javascript\nimport { commitLocalUpdate } from 'relay-runtime';\n\ncommitLocalUpdate(environment, store =\u003e {\n  const newUser = createAndAddNodeToStore(store, newUserDatabaseId, 'User', {\n    name: 'Some Name',\n    age: 59\n  });\n});\n```\n\n#### Signature\n\n```javascript\nfunction createAndAddNodeToStore(\n  store: RecordSourceProxy,\n  uniqueId: string, // Typically the database ID\n  typename: string,\n  objShape: { [key: string]: mixed },\n  base64encode?: (data: string) =\u003e string // Check out createRelayDataId for an explanation of when you'd want to pass this\n): RecordProxy\n```\n\n### createAndAddEdgeToConnections\n\nIt's pretty common that GraphQL API:s return just the newly created node on mutations that lets you create nodes. Imagine something like:\n\n```graphql\nmutation AddPetMutation($input: AddPetInput!) {\n  addPet(input: $input) {\n    addedPet {\n      id\n      name\n    }\n  }\n}\n```\n\nNow, often you'll end up in a scenario where you want to attach that node to a list of some sort, like a connection or a linked list.\n`createAndAddEdgeToConnection` makes adding that created node to a connection easier:\n\n#### Example\n\n```javascript\nconst updater = store =\u003e {\n  // Get the mutation payload from the store\n  const addedPet = resolveNestedRecord(store.getRootField('addPet'), [\n    'addedPet'\n  ]);\n\n  if (addedPet) {\n    createAndAddEdgeToConnections(store, {\n      node: addedPet,\n      edgeName: 'PetEdge',\n      connections: [\n        {\n          parentID: petOwnerDataId,\n          key: 'SomeFragment_user_pets',\n          insertAt: 'START'\n        }\n      ]\n    });\n  }\n};\n```\n\n#### Signature\n\n```javascript\nfunction createAndAddEdgeToConnections(\n  store: RecordSourceProxy, // The Relay store\n  config: CreateAndAddEdgeToConnectionConfig\n): void\n\ntype CreateAndAddEdgeToConnectionConfig = {|\n  node: RecordProxy, // From the Relay store\n  connections: Array\u003cConnectionConfig\u003e,\n  edgeName: string,\n  insertAt?: 'START' | 'END'\n|};\n\ntype ConnectionConfig = {|\n  parentID: string,\n  key: string,\n  filters?: Object\n|};\n```\n\n### createAndAddEdgeToLinkedRecords\n\nA close sibling of `createAndAddEdgeToConnections`, `createAndAddEdgeToLinkedRecords` does the same thing, but for connections that are not annotated with `@connection`.\n\n#### Example\n\nFollowing the example of `createAndAddEdgeToConnections`, you'd write an updater like this instead for `createAndAddEdgeToLinkedRecords`:\n\n```javascript\nconst updater = store =\u003e {\n  // Get the mutation payload from the store\n  const addedPet = resolveNestedRecord(store.getRootField('addPet'), [\n    'addedPet'\n  ]);\n\n  const petOwner = store.get(petOwnerDataId); // Omitted since it's an example, but the node that owns the connection\n\n  if (addedPet) {\n    createAndAddEdgeToLinkedRecords(store, {\n      node: addedPet,\n      edgeName: 'PetEdge',\n      linkedRecords: [\n        {\n          parentID: petOwner.getDataId(),\n          key: 'pets', // The field key this connection is at on the owner record\n          filters: { first: 2 }, // Remember, when there's no @connection annotation, the first/after arguments matter for record access in the store.\n          insertAt: 'START'\n        }\n      ]\n    });\n  }\n};\n```\n\n#### Signature\n\n```javascript\nfunction createAndAddEdgeToLinkedRecords(\n  store: RecordSourceProxy, // The Relay store\n  config: CreateAndAddEdgeToLinkedRecordConfig\n): void\n\ntype CreateAndAddEdgeToLinkedRecordConfig = {|\n  node: RecordProxy,\n  linkedRecords: Array\u003cLinkedRecordConfig\u003e,\n  edgeName: string,\n  insertAt?: 'START' | 'END'\n|};\n\ntype LinkedRecordConfig = {|\n  parentID: string,\n  key: string,\n  filters?: Object\n|};\n```\n\n### removeNodeFromStore\n\nWhenever you delete something through your GraphQL API, and you can't/don't want to refetch everything that might've been affected by the delete,\nyou'll need to do some cleanup in the store to remove the deleted record. This involves deleting the record, as well as removing it + its edges from connections\nor other linked records that might have the node in the store. `removeNodeFromStore` helps with that.\n\n#### Example\n\nYou delete something:\n\n```graphql\nmutation DeletePet($input: DeletePetInput!) {\n  deletePet(input: $input) {\n    ok\n  }\n}\n```\n\nNow, you want to clean up that node + things that use it:\n\n```javascript\nconst updater = store =\u003e {\n  const deletedPetNode = store.get(deletedPetId); // The data ID of the deleted pet node\n\n  // This might exist in the viewers pets connection\n  const viewer = store.get(viewerDataId);\n\n  // It might also exist in a second users pets list. This list however is not annotated with @connection, so we'll need to remove it as a linked record.\n  const secondPossibleOwner = store.get(secondPossibleOwnerDataId);\n\n  removeNodeFromStore(store, {\n    node: deletedPetNode,\n    connections: [\n      {\n        parentID: viewer.getDataID(),\n        key: 'SomeFragment_user_pets',\n        filters: {} // Add filters here if you have any\n      }\n    ],\n    linkedRecords: [\n      {\n        parentID: secondPossibleOwner.getDataID(),\n        key: 'pets', // Field key of the edges to look in\n        filters: { first: 10 } // Remember, when things aren't annotated as a @connection, first/after is treated as a filter\n      }\n    ]\n  });\n};\n```\n\n_PLEASE NOTE_ that `removeNodeFromStore` does no automatic deletion - it relies on you providing it with all connections/linked record owner that might have the node.\n\n#### Signature\n\n```javascript\nfunction removeNodeFromStore(\n  store: RecordSourceProxy,\n  config: RemoveNodeAndConnectionsConfig\n): void\n\ntype RemoveNodeAndConnectionsConfig = {|\n  node: RecordProxy,\n  connections?: Array\u003cConnectionConfig\u003e,\n  linkedRecords?: Array\u003cLinkedRecordConfig\u003e\n|};\n\ntype ConnectionConfig = {|\n  parentID: string,\n  key: string,\n  filters?: Object\n|};\n\ntype LinkedRecordConfig = {|\n  parentID: string,\n  key: string,\n  filters?: Object\n|};\n```\n\n### createMissingFieldsHandler\n\nSets up resolvers for missing fields in the graph. Uses the built in `missingFieldHandlers` on the Relay `Environment`.\n\n#### Example\n\nA fairly common thing is to have two separate id's for your types in your schema - one that's a Relay data id for global uniqueness, and one that's your database id or similar, that you use when updating/deleting and otherwise referencing the object when interacting with the API/backend.\nIn this example we'll say that you have a type that looks like this:\n\n```graphql\ntype Pet {\n  id: ID! # The Relay data ID, globally unique and generated by the backend\n  _id: ID! # The database ID of this object, used when updating/deleting etc\n}\n```\n\nIt's easier to use the database ID when interacting with the backend. Imagine you have a query that looks like this:\n\n```graphql\nquery SinglePetQuery($id: ID!) {\n  # id here is not the Relay data ID, but the actual database ID\n  Pet(id: $id) {\n    name\n  }\n}\n```\n\nLets say you want to use `SinglePetQuery` to get the `Pet` with id `123`. But, in your store, there's already\nenough data for the `Pet` with id `123` to be resolved without a round-trip to the database, since you've fetched that `Pet` as part of another query.\n\nRelay should just resolve this from the cache then, right? Well, Relay has no clue how to use your `id` query argument (that's a database id) to look up a `Pet` in the local store.\nLets use `createMissingFieldsHandler` to teach it how to do that so we won't have to make a round-trip to the database:\n\n```javascript\n// Missing field handlers are defined when creating the Environment\nconst environment = new Environment({\n  network,\n  store,\n  missingFieldHandlers: createMissingFieldsHandler({\n    /**\n     * You can define handlers for scalar fields, linked fields, and plural linked fields.\n     * More about scalar/plural linked fields in the Signature section, here we'll focus on\n     * the linked field, since that's what our top-level Pet field is.\n     */\n    handleLinkedField: [\n      // See the full signature in the Signature section\n      ({\n        name, // Name of the field\n        ownerTypename, // Typename of the owning node\n        args // Arguments used for this query\n      }) =\u003e {\n        /**\n         * Here we want to match whenever the Pet field on the root level cannot be resolved from the store.\n         * We then want to extract the `id` used, which is the database id in our case, and turn it into a\n         * data id that Relay can use to look for the Pet in the store.\n         */\n        if (\n          ownerTypename === ROOT_TYPE \u0026\u0026 // ROOT_TYPE comes from relay-runtime, and is the typename for the root of the store.\n          name === 'Pet' \u0026\u0026 // The name of the field we're interested in\n          args \u0026\u0026\n          args.id // The id argument provided with the query, which here is our database id.\n        ) {\n          // We create and return a Relay data id from our database id\n          return createRelayDataId(args.id, 'Pet');\n        }\n      }\n    ]\n  })\n});\n```\n\nThis teaches Relay how to use your query argument `id` on your root-level field `Pet` to resolve a `Pet` already in the store.\n\n#### Signature\n\n```javascript\nfunction createMissingFieldsHandler(config: {|\n  /**\n   * handleScalarField allows you to provide missing values for scalar fields like\n   * string/number/boolean and other scalars in your schema.\n   */\n  handleScalarField?: $ReadOnlyArray\u003cScalarMissingFieldReplacerFn\u003e,\n\n  /**\n   * handleLinkedField handles linked fields, which basically means links between records.\n   */\n  handleLinkedField?: $ReadOnlyArray\u003cLinkedFieldMissingFieldReplacerFn\u003e,\n\n  /**\n   * handlePluralLinkedField handles lists of linked fields, which means lists of records.\n   */\n  handlePluralLinkedField?: $ReadOnlyArray\u003cPluralLinkedFieldMissingFieldReplacerFn\u003e\n|}): $ReadOnlyArray\u003cMissingFieldHandler\u003e\n\ntype DataID = string;\n\ntype ScalarMissingFieldReplacerFn = (\n  config: MissingFieldReplacerConfig\n) =\u003e ?mixed; // Return whatever you want as value for the scalar field here, or undefined if you want the field to remain missing.\n\ntype LinkedFieldMissingFieldReplacerFn = (\n  config: MissingFieldReplacerConfig\n) =\u003e ?DataID;\n\ntype PluralLinkedFieldMissingFieldReplacerFn = (\n  config: MissingFieldReplacerConfig\n) =\u003e ?Array\u003c?DataID\u003e;\n\ntype MissingFieldReplacerConfig = {|\n  name: string, // The name of the original field, regardless of alias\n  alias: ?string, // If the field is aliased to something, this will be set\n  args: ?Variables, // Arguments for the operation/query the field is found in\n  fieldArgs: ?$ReadOnlyArray\u003cNormalizationArgument\u003e, // Arguments for this specific field. Check out NormalizationArgument in the Relay code base for more info\n  ownerTypename: ?string, // The typename of the record owning the field. This will be ROOT_TYPE imported from relay-runtime when the field is a root field\n  owner: ?Record, // The record that owns this field\n  store: ReadOnlyRecordSourceProxy // The full Relay store\n|};\n```\n\n#### Notes\n\nThis is an opinionated abstraction on top of Relays own `missingFieldHandlers`. You might not want to use `createMissingFieldHandlers`,\nbut use Relays API directly. [Check out Relays own type definition for missingFieldHandlers here](https://github.com/facebook/relay/blob/cccc3d62e662a11e58721bcadf5e727d8ceb42a1/packages/relay-runtime/store/RelayStoreTypes.js#L504). The `missingFieldHandlers` is then attached when creating the `Environment`, as illustrated in the example above.\n","funding_links":[],"categories":["JavaScript"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzth%2Frelay-utils","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzth%2Frelay-utils","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzth%2Frelay-utils/lists"}