node
There is no node() step currently; it's not needed since you can typically
just return the ID verbatim and rely on the Node interface's planType()
method to resolve the type.
However, we do have other functions for helping work with GraphQL Global Object Identification (Node) identifiers:
specFromNodeId
function specFromNodeId<THandler extends NodeIdHandler<any>>(
handler: THandler,
$id: Step<Maybe<string>>,
): ReturnType<THandler["getSpec"]>;
If you already know which object type the ID should represent (for example in a
mutation such as updateUser(id: ID!, ...)), you can pass that type's
NodeIdHandler along with the ID to specFromNodeId(handler, $id), which will
then return a specifier object, skipping the polymorphic resolution:
import { specFromNodeId } from "grafast";
const specifier = specFromNodeId(userHandler, $id);
const $update = userResource.update(specifier, $changes);
Note that the specifier returned is not necessarily a step itself - typically
it's an object that contains keyed steps, e.g. { id: Step<string> }. This will
vary based on the needs of the NodeIdHandler.
nodeIdFromNode
function nodeIdFromNode(handler: NodeIdHandler, $node: Step): Step<string>;
Given a NodeIdHandler for a given GraphQL object type, and a $node step
representing that same object type, return a Step representing the GraphQL ID
for $node.
NodeIdHandler
A NodeIdHandler describes how a single GraphQL object type encodes and decodes
its Global Object Identifier. Handlers are plain objects; the essential fields
are:
typeName– GraphQL object type name (string) this handler serves.codec– theNodeIdCodec(see below) used to encode/decode the NodeID string.match(decoded)– returnstruewhen the decoded value belongs to this type.getIdentifiers(decoded)– extracts the underlying identifier tuple from the decoded value.getSpec($decoded)– converts the decoded value into whatever specifier your application expects. Useful for referencing a node without fetching it.get(spec)– given the specifier fromgetSpec, returns a step that yields the original node.plan($node)– produces the value that will be passed tocodec.encode. Feeding the result intomatchshould yieldtrue.deprecationReason(optional) – indicates that the Node implementation is deprecated.
Here's an example NodeIdHandler for a User type where the NodeID encodes a
tuple of ["User", userId] using a base64-encoded JSON array:
import { constant, list } from "grafast";
import type { NodeIdHandler } from "grafast";
import { base64JSONCodec } from "./nodeIdCodecs"; // see NodeIdCodec section
export const userHandler: NodeIdHandler<[number]> = {
typeName: "User",
codec: base64JSONCodec,
match(decoded) {
const [typeName, id] = decoded;
return typeName === "User";
},
getIdentifiers(decoded) {
const [typeName, id] = decoded;
return [id];
},
plan($user) {
const $typeName = constant("User", true);
const $id = $user.get("id");
return list([$typeName, $id]);
},
getSpec($decoded) {
const $id = access($decoded, 1); // e.g. `decoded[1]`
return { id: inhibitOnNull($id) };
},
get(spec) {
return userResource.get(spec);
},
};
Here's how we might retrieve a user from a UserID:
const $id = constant("WyJVc2VyIiwxMjNd"); // base64(JSON.stringify(["User",123"]))
const spec = specFromNodeId(userHandler, $id);
const $user = userHandler.get(spec);
And given that we now have a User in $user, we can get back to the $id:
const $specifier = handler.plan($user);
const $id = lambda($specifier, userHandler.codec.encode);
Matching an ID:
const id = "WyJVc2VyIiwxMjNd"; // base64(JSON.stringify(["User", 123]))
const decoded = userHandler.codec.decode(id); // ["User", 123]
const isMatch = userHandler.match(decoded); // true
const identifiers = userHandler.getIdentifiers(decoded); // [123]
Encoding an ID:
const planResult = ["User", 123];
const id = userHandler.codec.encode(planResult); // "WyJVc2VyIiwxMjNd"
NodeIdCodec
NodeIdCodec objects are responsible for converting a specifier to a string and
back again. Implement { name, encode, decode } and set
encode.isSyncAndSafe = true / decode.isSyncAndSafe = true when the
operations are synchronous and side-effect free so Grafast can inline them.
Typically the same codec will be used for all IDs across your schema, but that
is not a requirement. If in doubt, base64JSONCodec is a good default.
base64JSONCodec
This is a fairly popular and safe way of encoding IDs; essentially it's a base64 encoded JSON-stringified value, and should work with all identifiers.
export const base64JSONCodec = {
name: "base64JSON",
encode(value: any) {
return Buffer.from(JSON.stringify(value), "utf8").toString("base64");
},
decode(value: string) {
return JSON.parse(Buffer.from(value, "base64").toString("utf8"));
},
};
base64JSONCodec.encode.isSyncAndSafe = true;
base64JSONCodec.decode.isSyncAndSafe = true;
e.g. WyJVc2VyIiwxMjNd might encode a User identified by 123.
pipeStringCodec
This is a more concise and less opaque encoding, using a pipe symbol to separate the various components, but it is not appropriate to use if any of the components may themselves contain a pipe symbol. It is purely presented as an example, not a recommendation.
export const pipeStringCodec = {
name: "pipeString",
encode(values: any[]) {
return Array.isArray(values) ? values.join("|") : null;
},
decode(value: string) {
return typeof value === "string" ? value.split("|") : null;
},
};
pipeStringCodec.encode.isSyncAndSafe = true;
pipeStringCodec.decode.isSyncAndSafe = true;
e.g. User|123 might encode a User identified by 123.
Possible types
The possibleTypes object is a generally useful object to have around your
schema, a single place in which to look up all of your NodeIdHandlers. It's
simply a map from type name to handler:
const handlers = {
User: userHandler,
Article: articleHandler,
};