node
A step to get a Node by its global object identifier (string). Accepts two parameters:
handlers
: a map from typeName to handler spec (codec to use, how to find the record, etc) - see below$id
: the step (typically supplied from a field argument) representing the Node ID
Returns a polymorphic-capable step representing the record this $id
represents.
Usage:
const $nodeIdString = fieldArgs.get("id");
const $node = node(handlers, $nodeIdString);
codecs
A node identifier is a string that uniquely identifies an entity in the GraphQL schema for the lifetime of that entity.
A codec is responsible for parsing and deparsing this string. There are many different ways of encoding node identifiers, so we allow for many different codecs.
A code is made of a name
(string) and two methods:
encode
takes an intermediate representation and turns it into the final node identifier stringdecode
takes the final node identifier string and turns it back into an intermediate representation
This intermediate representation is produced by and consumed by the handlers (see below).
Here's an example base64JSONCodec
which simply JSON stringifies the
intermediate representation and then base64 encodes it:
function base64JSONEncode(value: any): string | null {
return Buffer.from(JSON.stringify(value), "utf8").toString("base64");
}
base64JSONEncode.isSyncAndSafe = true; // Optimization
function base64JSONDecode(value: string): any {
return JSON.parse(Buffer.from(value, "base64").toString("utf8"));
}
base64JSONDecode.isSyncAndSafe = true; // Optimization
const base64JSONCodec = {
name: "base64JSON",
encode: base64JSONEncode,
decode: base64JSONDecode,
};
handlers
Each GraphQL object type that supports the Node
interface must have its own
NodeIdHandler
. This handler specifies:
typeName
- the GraphQL object type name to which the handler appliescodec
- the NodeID codec (see above) to use with this handlermatch
- determines whether a given intermediate representation of a node identifier string (i.e. the result ofcodec.decode()
) relates to this type or notplan
- takes an entity of the given object type and return a step representing the intermediate representation for this entity (ready to be fed tocodec.encode()
)getSpec
- builds a "specification" of the entity from a step representing a matching intermediate representationget
- given the specification fromgetSpec
above, returns a step representing the entity identified by the matching node identifier string, if it exists
Specifications (returned from getSpec
) may differ for each object type. They
could be something simple like just a step that represents the numeric primary
key in the database, or they may be more complex such as an object with
multiple keys where each key's value is a step representing a related value to
match in the remote source.
Here's an example of a userHandler
, which could be used across a schema when
handling node identifiers for the User
type:
const USER = "User";
const userHandler = {
typeName: USER,
codec: base64JSONCodec,
// Given a User record, return a step describing the data to be encoded by
// the codec:
plan($user: PgSelectSingleStep) {
return list([constant(USER), $user.get("id")]);
},
// Given the data decoded by the codec, determine if the data is for our
// type. In this particular handler, the check looks at the first entry in
// the list to see if it matches our type name.
match(list) {
return list[0] === USER;
},
// Given a step representing decoded data that passes the `match` test above,
// return a specifier object that can be used to retrieve or reference
// this entity.
getSpec($list: ListStep<any[]>) {
return {
id: access($list, 1),
};
},
// Given a spec (the result of `getSpec` above), return a step that resolves
// to the entity (if found).
get(spec: any) {
return pgResource.get(spec);
},
};
const handlers = {
User: userHandler,
// Add more handlers here
};
specFromNodeId
Given you have a Node ID represented by the step $id
and you already know
what type it should be (e.g. for an updateUser
mutation you might know that
the $id
should represent a User
), you can use specFromNodeId
passing the
relevant handler to get a specification for the entity in question. This is
typically useful when you want to mutate an entity without having to actually
retrieve it (if you want to retrieve it then use node()
above instead).
If the handler doesn't match then the executable steps inside the resulting spec will resolve to null-ish values (or maybe raise an error).
function specFromNodeId(
handler: NodeIdHandler<any>,
$id: ExecutableStep<string> | AnyInputStep,
): any;
Here's an example of an updateUser
mutation that uses the userHandler
example handler from above:
const typeDefs = /* GraphQL */ `
extend type Mutation {
updateUser(id: ID!, patch: UserPatch!): UpdateUserPayload
}
`;
const plans = {
Mutation: {
updateUser(parent, { $id }) {
// Turn the $id into a specifier:
const spec = specFromNodeId(userHandler, $id);
// Now use this specifier to plan an update for this user:
const $result = pgUpdateSingle(userSource, spec);
// Leave space in our result so we can add more properties later:
const $payload = object({ result: $result });
// Apply all the plans from the 'patch' argument (omitted for brevity):
fieldArgs.apply($payload);
// Return the payload plan:
return $payload;
},
},
};
nodeIdFromNode
Given you have a step representing a node and you know the handler for it, this helper method will return a step representing the Node ID for this node.
const typeDefs = /* GraphQL */ `
extend type User {
id: ID!
}
`;
const planResolvers = {
User: {
id($user) {
return nodeIdFromNode(handlers.User, $user);
},
},
};