Skip to content

Scalars and conversion

re-export

Re-exported from temporal-polyfill, so that an application names the same copy the framework converts into.

function

declare function registerConverter<Wire, Live>(type: string, converter: Converter<Wire, Live>): void;

Registers a converter for the given GraphQL named type.

Any named type can have one – scalars are the common case (a Timestamp is an ISO-8601 string on the wire and a Temporal.Instant in the application), but object types work just as well, which is how the QueryDocument derived types become QueryDocument instances.

Registering a second converter for a type replaces the first, so applications can override the converters QLive brings along.

What QLive registers for itself, and what an application overrides by registering the same type again:

Type On the wire In the application
Timestamp ISO-8601 string Temporal.Instant
Date ISO-8601 string Temporal.PlainDate
Long, BigInteger decimal string bigint
FooDocument and friends plain JSON object QueryDocument instance

Do it from startup()’s init hook – see Register a converter.

Parameters

type GraphQL type name, e.g. “Timestamp”
converter converter for that type

function

declare function getConverter(type: string): Converter | null;

Returns the converter registered for the given GraphQL type name, if any.

Parameters

type GraphQL type name

Returns converter or null

function

declare function formatValue(value: unknown, type: string): string;

The text a user reads for a live value of the given GraphQL type: nothing for null or undefined, what the type’s converter formats it as where it has a format(), String(value) otherwise.

Parameters

value live value, as in a query result
type GraphQL type name of the value, e.g. “Timestamp”

Returns display text

function

declare function inputText(value: unknown, type: string): string;

Turns a live value into the text an input edits: its converter’s toInput() where it has one, the built-in conversion of String, ID, Boolean, Byte, Short, Int, Number, Float and Currency otherwise. Null and undefined are empty text.

Parameters

value live value, as in a query result or a draft
type GraphQL type name of the value, e.g. “Date”

Returns input text

Throws Error for a type without an input: BigDecimal until a decimal type is chosen, and an application’s own scalar until its converter has toInput() and fromInput()

function

declare function parseInput(text: string, type: string): unknown;

Turns what the user typed into a live value of the type, the reverse of inputText(). Blank text is null, except for a String or an ID, whose text is the value as it is.

Parameters

text input text
type GraphQL type name of the value, e.g. “Date”

Returns live value

Throws Error with a message for the user where the text is no value of the type, and Error for a type without an input, see inputText()

type

type Converter<Wire = any, Live = any> = {
/**
* Converts the JSON value received from the server into a live value. Used on
* query results, which arrive along a conversion map.
*/
fromServer: ConversionFn<Wire, Live>;
/**
* Converts a live value into the JSON format the server expects. Used on the
* variables of a query, which are converted along the schema.
*
* Optional: a type that only ever appears in results needs none, and values of
* it are passed through unchanged on the way out.
*/
toServer?: ConversionFn<Live, Wire>;
/**
* Turns a live value into the text a user reads, e.g. in a grid cell. Never
* called with null or undefined, which display as nothing.
*
* Optional: a value of a type without one displays as String(value).
*/
format?: (value: Live, type: string) => string;
/**
* Orders two live values: negative if the first comes first, positive if the
* second does, 0 for the same value. Never called with null or undefined. What
* a condition evaluated in the browser (`lt`, `between`, ...) and a local sort
* order values of the type by, see scalarCompare().
*
* Optional: numbers, strings and Booleans order without one. A type whose live
* values are objects needs one to be ordered at all.
*/
compare?: (a: Live, b: Live, type: string) => number;
/**
* Turns a live value into the text an input edits, e.g. `2026-10-19` for a
* date input. Not the same as format(): what a user reads and what an input
* takes differ for a Timestamp, and a Currency amount reads as `1.15` either
* way but is held as `11500`. Never called with null or undefined, which edit
* as an empty input.
*
* Optional along with fromInput(): a type without either has no input, see
* inputText().
*/
toInput?: (value: Live, type: string) => string;
/**
* Turns what the user typed back into a live value. Called with the text
* trimmed and never with blank text, which is null. Throws an Error whose
* message the user can read where the text is no value of the type.
*/
fromInput?: (text: string, type: string) => Live;
};

Conversion of one GraphQL named type between its wire format and its live JavaScript representation.

The two directions are separate members instead of two positional arguments so that a converter reads as what it is at the registration site and can grow further members without breaking existing registrations.

type

type ConversionFn<I, O> = (value: I, type: string) => O;

Converts a value between its wire format and its live JavaScript form. Never called with null or undefined: a null converts to null, and an input value the schema declares non-null is rejected before any conversion, so a converter only ever deals with actual values.

Parameters

value value of the GraphQL type the converter is registered for
type GraphQL type name the converter was invoked for. Handed in so that one function can serve a family of types, e.g. all the types derived from QueryDocument.

function

declare function convertToServer(value: any, type: string): any;

Converts a live value into the JSON format the server expects, walking it along its GraphQL type.

Top-down, the reverse of the way results are converted: the converter of the type turns the live value into its wire shape first, then that shape’s fields are converted.

Parameters

value live value
type GraphQL type name of that value, e.g. “FooInput”

Returns wire value

function

declare function convertVariablesToServer(variables: {
[name: string]: any;
}, map: QueryConversionMap): {
[name: string]: any;
};

Converts the variables of a query into their wire format. Only the variables the query declares are converted, along their declared type – everything else is passed on as it is.

Parameters

variables variable values
map conversion map of the query

Returns variables in wire format

function

declare function convertResultFromServer<T = any>(result: any, map: QueryConversionMap): T;

Converts a complete GraphQL result, whose keys are the top-level selections of the query. Result keys the map does not mention are passed through unconverted.

Parameters

result data of the GraphQL response
map conversion map of the query

Returns converted result

function

declare function convertSelectionFromServer<T = any>(value: any, node: SelectionNode): T;

Converts the value of a single selection, which is what an injection delivers.

Parameters

value wire value of the selection
node selection node for it

Returns converted value

type

type SelectionNode = {
/**
* GraphQL type name of the value at this position, modifiers stripped
*/
type: string;
/**
* Nodes of the selection below this one, keyed by result key. Absent for leaves,
* which includes scalars whose wire format is an object graph of its own.
*/
fields?: {
[key: string]: SelectionNode;
};
};

One selection of a conversion map: the type of the value at one position of a query result, plus the nodes of whatever was selected below it.

The types are resolved when the map is generated, so converting along a map needs neither the schema nor the query. That is what takes aliases out of the picture: the child nodes are keyed by the key the value actually appears under in the result, and each one names its type outright, so a field selected as “desc: description” converts as the Timestamp, String, … it is.

List modifiers are not represented. A list is recognized by the value being an array and its elements convert against the same node, which covers nested lists without the map having to spell out a depth.

type

type QueryConversionMap = {
/**
* Nodes of the top-level selections, keyed by result key. An injected query has
* exactly one, a query executed at runtime may have several.
*/
selections: {
[key: string]: SelectionNode;
};
/**
* GraphQL type name per variable of the query, e.g. { config: "QueryConfig" }.
* Converting the variables needs nothing but the type: input values have neither
* aliases nor a selection, so the schema describes them completely.
*/
variables?: {
[name: string]: string;
};
};

Conversion map of one query, as built from its source by buildConversionMap().

function

declare function scalarEqual(a: unknown, b: unknown): boolean;

Whether two values of the same scalar type are the same value.

A converted value is an object rather than a primitive – a Timestamp is a Temporal.Instant – and two of them holding the same instant are not the same object. What knows they are equal is the value itself, so an equals() is asked wherever there is one.

A scalar that converts to a plain object or an array has neither identity nor an equals(), and comes back false here. That is the safe direction for what this is used for – a value the user typed stays a change rather than being dropped as one – and the cost is a write and a version for a value that did not move.

Parameters

a one value, in the live form of its type
b the other

function

declare function scalarCompare(type: string, a: unknown, b: unknown): number;

Orders two values of a scalar type: negative if the first comes first, positive if the second does, 0 for the same value. What a condition evaluated in the browser compares by, and a local sort orders by.

  • A type whose converter has a compare() is ordered by it, see Converter. QLive registers one for Timestamp.
  • Numbers of the number types compare by value across their forms, see isNumericType().
  • String and ID values follow the user’s locale, so "a" < "B". A database orders by its collation, which the browser doesn’t know; the locale is the nearest guess, and where they differ a server sort and a local one differ too. Text of other types, a Date among them, compares by character code, which orders ISO dates right.
  • Booleans order false first.

Nulls aren’t values here; a caller decides where they go.

Parameters

type GraphQL scalar type name the values are of
a one value, in the live form of its type
b the other

Throws Error for values that type has no order for

function

declare function genericScalarEqual(a: GenericScalar | null, b: GenericScalar | null): boolean;

Whether two scalars are the same type carrying the same value.

The type names have to agree: “Int” 1 and “Float” 1 are two values a domain distinguishes, and what the server coerces the value along is the name. Two nulls are the same value, one null is not.

Parameters

a one scalar, or null
b the other

type

type GenericScalar = {
/**
* Name of the scalar type the value is of, e.g. "Timestamp". A list of them is written the way GraphQL
* writes it, e.g. "[String]".
*/
type: string;
/**
* The value, in the live form of its type -- a Timestamp is a Temporal.Instant here and an ISO-8601
* string on the wire, the same as everywhere else in QLive.
*/
value: any;
};

A value of any scalar type the domain has, traveling as the name of that type plus a value of it.

That is what lets one GraphQL field accept every scalar the application has: the server coerces the value along the named type, so a mutation taking a GenericScalar needs no input type per domain type. QLive’s merge mutation is the reason it exists here – a field change is a field name and one of these.

The type name is open on purpose. Which scalars there are is the domain’s decision, so “String”, “Int” and whatever the application registered are as valid here as the ones below, and a closed union would exclude most of what is actually sent. The aliases below narrow this for the types QLive knows, and an application that knows what it is holding can name one of them.

type

type GenericBigDecimal = {
type: "BigDecimal";
/** the decimal as the string it travels as, no decimal type being chosen for it yet */
value: string;
};

type

type GenericByte = {
type: "Byte";
value: number;
};

type

type GenericComputedValue = {
type: "ComputedValue";
value: {
name: string;
args: [Scalar];
};
};

type

type GenericCondition = {
type: "Condition";
value: ConditionNode;
};

type

type GenericDate = {
type: "Date";
value: Temporal.PlainDate;
};

type

type GenericDomainObject = {
type: "DomainObject";
value: object;
};

type

type GenericFieldExpression = {
type: "FieldExpression";
value: string | FieldNode;
};

type

type GenericJSONB = {
type: "JSONB";
value: RawValue;
};

type

type GenericLong = {
type: "Long";
value: bigint;
};

type

type GenericTimestamp = {
type: "Timestamp";
value: Temporal.Instant;
};