{"id":15594294,"url":"https://github.com/orta/redwood-object-identification","last_synced_at":"2025-07-31T08:40:09.132Z","repository":{"id":66647238,"uuid":"437356496","full_name":"orta/redwood-object-identification","owner":"orta","description":null,"archived":false,"fork":false,"pushed_at":"2021-12-12T08:39:45.000Z","size":330,"stargazers_count":13,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-05-06T22:57:58.635Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","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/orta.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,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-12-11T18:16:09.000Z","updated_at":"2025-03-16T21:14:36.000Z","dependencies_parsed_at":"2023-02-22T17:30:48.283Z","dependency_job_id":null,"html_url":"https://github.com/orta/redwood-object-identification","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/orta/redwood-object-identification","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/orta%2Fredwood-object-identification","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/orta%2Fredwood-object-identification/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/orta%2Fredwood-object-identification/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/orta%2Fredwood-object-identification/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/orta","download_url":"https://codeload.github.com/orta/redwood-object-identification/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/orta%2Fredwood-object-identification/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":268014541,"owners_count":24181431,"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","status":"online","status_checked_at":"2025-07-31T02:00:08.723Z","response_time":66,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":[],"created_at":"2024-10-03T00:37:08.648Z","updated_at":"2025-07-31T08:40:09.093Z","avatar_url":"https://github.com/orta.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Redwood Object Identification Pattern Example\n\nThe [GraphQL Object Identification Pattern](https://relay.dev/graphql/objectidentification.htm) is a design pattern where you ensure that every object in your GraphQL schema conforms to a single interface:\n\n```graphql\ninterface Node {\n  id: ID!\n}\n```\n\nWhich means you can write something like:\n\n```graphql\ntype Query {\n  node(id: ID): Node! @skipAuth\n}\n```\n\nThis is cool, because now you have a guaranteed query to be able to get the info for any object in your graph! This feature gives you a [bunch of caching super-powers in Relay](https://relay.dev/docs/guided-tour/reusing-cached-data/) and probably with Apollo (I don't know their caching strats intimately, but it would make re-fetching any object trivial).\n\n## This Repo\n\nThat said, for my case this repo currently handles `Node` in a different place, I wanted to create the anti-`node` resolver:\n\n```graphql\ntype Mutation {\n  deleteNode(id: ID): Node! @requireAuth\n}\n```\n\nThis is useful for the sample because I only need one model to be useful and also because [queries](https://github.com/redwoodjs/redwood/issues/3873) with inline fragments crash with RedwoodJS' `gql` ATM, [I sent a fix](https://github.com/redwoodjs/redwood/pull/3891).\n\n## Getting Set Up\n\n### 1. SDL + Resolvers\n\nWe're going to need some GraphQL SDL and corresponding resolvers\n\n\u003e [`api/src/graphql/objectIdentification.sdl.ts`](./api/src/graphql/objectIdentification.sdl.ts):\n\n```graphql\nexport const schema = gql`\n  scalar ID\n\n  interface Node {\n    id: ID!\n  }\n\n  type Query {\n    node(id: ID): Node! @skipAuth\n  }\n\n  type Mutation {\n    deleteNode(id: ID): Node! @requireAuth\n  }\n`\n```\n\nThis sets up some new graphql fields, and declares the new primitive `ID` which is an arbitrary string under the hood.\n\nTo understand the `ID`, let's look at how I implement it in the `createUser` resolver\n\n\u003e [`./api/src/services/users/users.ts`](./api/src/services/users/users.ts`):\n```ts\nimport cuid from \"cuid\"\nimport { db } from \"src/lib/db\"\n\nexport const createUser = ({ input }: CreateUserArgs) =\u003e {\n  input.id = cuid() + \":user\"\n  input.slug = cuid.slug()\n\n  return db.user.create({\n    data: input,\n  })\n}\n```\n\nPrior to setting up for Object Identification, I would have made a prisma schema like:\n\n```prisma\nmodel User {\n  id String @id @default(cuid())\n}\n```\n\nThis... doesn't _really_ work in the Object Identification era because a `cuid` is as good UUID, but there's no (safe/simple/easy) way of going from the UUID string back to the original object because it's basically random digits. A route we use at Artsy was to base64 encode that [metadata into the id](https://github.com/artsy/README/blob/main/playbooks/graphql-schema-design.md#global-object-identification).\n\n\u003cdetails\u003e\n  \u003csummary markdown=\"span\"\u003eReally though?\u003c/summary\u003e\n\nI had a few ideas for generating thse keys within the framework of letting prisma handle it, starting with making an object-identification query that looks in all potential db tables via a custom query... That's a bit dangerous and then you need to figure out which table you found the object in and _then_ start thinking about that objects access rights. That's tricky.\n\nAnother alternative I explored was having prisma generate a `dbID` via  `dbID String @id @default(cuid())` then have a postgres function run on a row write to generate an `id` with the suffix indicating the type. This kinda worked, but was a bit meh answer to me. At that point I gave up on letting prisma handle it at all.\n\nSo, I recommend _you_ taking control of generating the id in your app's code by having a totally globally unique `id` via a cuid + prefix, and then have a `slug` if you ever need to present it to the user via a URL.\n\nTo handle this case, I've been using this for resolving a single item:\n\n```ts\nexport const user = async (args: { id: string }) =\u003e {\n  // Allow looking up with the same function with either slug or id\n  const query = args.id.length \u003e 10 ? { id: args.id } : { slug: args.id }\n  const user = await db.user.findUnique({ where: query })\n\n  return user\n}\n```\n\nWhich allows you to resolve a user with either `slug` or `id`.\n\n\u003c/details\u003e\n\nSo instead now it looks like:\n\n```diff\nmodel User {\n+ id String  @id @unique\n- id String @id @default(cuid())\n}\n```\n\n### 2. ID Implementation\n\nUnder the hood `ID` is a real `cuid` mixed with an identifier prefix which lets you know which model it came from. The simplest implementation would of the `node` resolver look like this:\n\n```ts\nimport { user } from \"./users/users\"\n\nexport const node = (args: { id: string }) =\u003e {\n  if (args.id.endsWith(\":user\")) {\n    return user({ id: args.id })\n  }\n\n  throw new Error(`Did not find a resolver for node with ${args.id}`)\n}\n```\n\nBasically, by looking at the end of the `ID` we can know which underlying graphql resolver we should forward the request to, this means no duplication of access control inside the `node` function - it just forwards to the other existing GraphQL resolvers.\n\n### 3. Disambiguation\n\nThe next thing you would hit is kind of only something you hit when you try this in practice. We're now writing to `interface`s and not concrete types, which means there are new GraphQL things to handle. We need to have [a way in](https://github.com/graphql/graphql-js/issues/876#issuecomment-304398882) the GraphQL server to go from an `interface` (or `union`) to the concrete type.\n\nThat is done by one of two methods, depending on your needs:\n\n- A single function on the interface which can disambiguate the types ( `Node.resolveType` )\n- Or each concrete type can have a way to declare if the JS object / ID is one of it's own GraphQL type ( `User.isTypeOf` (and for every other model) )\n\nNow, today (as of RedwoodJS v1.0rc), doing either of these things isn't possible via the normal RedwoodJS APIs, it's complicated but roughly the `*.sdl.ts` files only let you create resolvers and not manipulate the schema objects in your app. So, we'll write a quick `envelop` plugin do handle that for us:\n\n```ts\nexport const createNodeResolveEnvelopPlugin = (): Plugin =\u003e {\n  return {\n    onSchemaChange({ schema }) {\n      const node: { resolveType?: (obj: { id: string }) =\u003e string } = schema.getType(\"Node\") as unknown\n      node.resolveType = (obj) =\u003e {\n        if (obj.id.endsWith(\":user\")) {\n          return \"User\"\n        }\n\n        throw new Error(`Did not find a resolver for deleteNode with ${args.id}`)\n      }\n    }\n  }\n}\n```\n\nAnd then add that to the graphql function:\n\n```diff\n+ import { createNodeResolveEnvelopPlugin } from \"src/services/objectIdentification\"\n\nexport const handler = createGraphQLHandler({\n  loggerConfig: { logger, options: {} },\n  directives,\n  sdls,\n  services,\n+  extraPlugins: [createNodeResolveEnvelopPlugin()],\n  onException: () =\u003e {\n    // Disconnect from your database with an unhandled exception.\n    db.$disconnect()\n  },\n})\n\n```\n\nThe real implementation in this app is a little more abstract [`/api/src/services/objectIdentification.ts](./api/src/services/objectIdentification.ts) but it does the work well.\n\n### 4. Usage\n\nFinally, an actual outcome, you can see the new `DeleteButton` which I added in this repo using the `deleteNode` resolver which has a lot of similar patterns as the `node` resolver under the hood:\n\n```ts\nimport { navigate, routes } from \"@redwoodjs/router\"\nimport { useMutation } from \"@redwoodjs/web\"\nimport { toast } from \"@redwoodjs/web/dist/toast\"\n\nconst DELETE_NODE_MUTATION = gql`\n  mutation DeleteNodeMutation($id: ID!) {\n    deleteNode(id: $id) {\n      id\n    }\n  }\n`\n\nexport const DeleteButton = (props: { id: string; displayName: string }) =\u003e {\n  const [deleteUser] = useMutation(DELETE_NODE_MUTATION, {\n    onCompleted: () =\u003e {\n      toast.success(`${props.displayName} deleted`)\n      navigate(routes.users())\n    },\n    onError: (error) =\u003e {\n      toast.error(error.message)\n    },\n  })\n\n  const onDeleteClick = () =\u003e {\n    if (confirm(`Are you sure you want to delete ${props.displayName}?`)) {\n      deleteUser({ variables: { id: props.id } })\n    }\n  }\n  return (\n    \u003cbutton type=\"button\" title={`Delete ${props.displayName}`} className=\"rw-button rw-button-small rw-button-red\" onClick={onDeleteClick}\u003e\n      Delete\n    \u003c/button\u003e\n  )\n}\n```\n\nIt can delete any object which conforms to the `Node` protocol in your app, making it DRY and type-safe - and because it also forwards to each model's \"delete node\" resolver then it also gets all of the access control right checks in those functions too. :+1:\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Forta%2Fredwood-object-identification","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Forta%2Fredwood-object-identification","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Forta%2Fredwood-object-identification/lists"}