/** @category Values */
import { inspect } from '../jsutils/inspect.ts';
import { isIterableObject } from '../jsutils/isIterableObject.ts';
import { isObjectLike } from '../jsutils/isObjectLike.ts';
import type { ConstObjectFieldNode, ConstValueNode } from '../language/ast.ts';
import { Kind } from '../language/kinds.ts';
import type { GraphQLInputType } from '../type/definition.ts';
import {
assertLeafType,
isInputObjectType,
isListType,
isNonNullType,
isRequiredInputField,
} from '../type/definition.ts';
/**
* Produces a GraphQL Value AST given a JavaScript value and a GraphQL type.
*
* Scalar types are converted by calling the `valueToLiteral` method on that
* type, otherwise the default scalar `valueToLiteral` method is used, defined
* below.
*
* Provided value is a non-coerced "input" value. This function does not
* perform any coercion, however it does perform validation. Provided values
* which are invalid for the given type will result in an `undefined` return
* value.
* @param value - JavaScript value to convert.
* @param type - GraphQL input type to convert the value against.
* @returns A GraphQL value AST, or undefined if the value is invalid.
* @example
* ```ts
* import { print } from 'graphql/language';
* import {
* GraphQLInputObjectType,
* GraphQLInt,
* GraphQLList,
* GraphQLNonNull,
* GraphQLString,
* } from 'graphql/type';
* import { valueToLiteral } from 'graphql/utilities';
*
* const ReviewInput = new GraphQLInputObjectType({
* name: 'ReviewInput',
* fields: {
* stars: { type: new GraphQLNonNull(GraphQLInt) },
* tags: { type: new GraphQLList(GraphQLString) },
* },
* });
*
* const literal = valueToLiteral({ stars: 5, tags: ['featured'] }, ReviewInput);
*
* print(literal); // => '{ stars: 5, tags: ["featured"] }'
* valueToLiteral({ tags: ['missing stars'] }, ReviewInput); // => undefined
* ```
*/
export function valueToLiteral(
value: unknown,
type: GraphQLInputType,
): ConstValueNode | undefined {
if (isNonNullType(type)) {
if (value == null) {
return; // Invalid: intentionally return no value.
}
return valueToLiteral(value, type.ofType);
}
// Like JSON, a null literal is produced for both null and undefined.
if (value == null) {
return { kind: Kind.NULL };
}
if (isListType(type)) {
if (!isIterableObject(value)) {
return valueToLiteral(value, type.ofType);
}
const values: Array<ConstValueNode> = [];
for (const itemValue of value) {
const itemNode = valueToLiteral(itemValue, type.ofType);
if (!itemNode) {
return; // Invalid: intentionally return no value.
}
values.push(itemNode);
}
return { kind: Kind.LIST, values };
}
if (isInputObjectType(type)) {
if (!isObjectLike(value)) {
return; // Invalid: intentionally return no value.
}
const fields: Array<ConstObjectFieldNode> = [];
const fieldDefs = type.getFields();
const hasUndefinedField = Object.keys(value).some(
(name) => value[name] !== undefined && !Object.hasOwn(fieldDefs, name),
);
if (hasUndefinedField) {
return; // Invalid: intentionally return no value.
}
for (const field of Object.values(type.getFields())) {
const fieldValue = value[field.name];
if (fieldValue === undefined) {
if (isRequiredInputField(field)) {
return; // Invalid: intentionally return no value.
}
} else {
const fieldNode = valueToLiteral(value[field.name], field.type);
if (!fieldNode) {
return; // Invalid: intentionally return no value.
}
fields.push({
kind: Kind.OBJECT_FIELD,
name: { kind: Kind.NAME, value: field.name },
value: fieldNode,
});
}
}
return { kind: Kind.OBJECT, fields };
}
const leafType = assertLeafType(type);
if (leafType.valueToLiteral) {
try {
return leafType.valueToLiteral(value);
} catch (_error) {
return; // Invalid: intentionally ignore error and return no value.
}
}
return defaultScalarValueToLiteral(value);
}
/**
* The default implementation to convert scalar values to literals.
*
* | JavaScript Value | GraphQL Value |
* | ----------------- | -------------------- |
* | Object | Input Object |
* | Array | List |
* | Boolean | Boolean |
* | String | String |
* | Number | Int / Float |
* | BigInt | Int |
* | null / undefined | Null |
*
* @internal
*/
export function defaultScalarValueToLiteral(value: unknown): ConstValueNode {
// Like JSON, a null literal is produced for both null and undefined.
if (value == null) {
return { kind: Kind.NULL };
}
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
switch (typeof value) {
case 'boolean':
return { kind: Kind.BOOLEAN, value };
case 'string':
return { kind: Kind.STRING, value, block: false };
case 'bigint':
return { kind: Kind.INT, value: value.toString() };
case 'number': {
if (!Number.isFinite(value)) {
// Like JSON, a null literal is produced for non-finite values.
return { kind: Kind.NULL };
}
const stringValue = String(value);
// Will parse as an IntValue.
return /^-?(?:0|[1-9][0-9]*)$/.test(stringValue)
? { kind: Kind.INT, value: stringValue }
: { kind: Kind.FLOAT, value: stringValue };
}
case 'object': {
if (isIterableObject(value)) {
return {
kind: Kind.LIST,
values: Array.from(value, defaultScalarValueToLiteral),
};
}
const objValue = value as { [prop: string]: unknown };
const fields: Array<ConstObjectFieldNode> = [];
for (const fieldName of Object.keys(objValue)) {
const fieldValue = objValue[fieldName];
// Like JSON, undefined fields are not included in the literal result.
if (fieldValue !== undefined) {
fields.push({
kind: Kind.OBJECT_FIELD,
name: { kind: Kind.NAME, value: fieldName },
value: defaultScalarValueToLiteral(fieldValue),
});
}
}
return { kind: Kind.OBJECT, fields };
}
}
throw new TypeError(`Cannot convert value to AST: ${inspect(value)}.`);
}