node
We don't current have a node() step... 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.
But what we do have is:
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 resolves to 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,
};