Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce astFromValueUntyped and handle object values within astFromValue correctly #4087

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions src/utilities/__tests__/astFromValue-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,28 @@ describe('astFromValue', () => {
},
});

expect(() => astFromValue('value', returnCustomClassScalar)).to.throw(
'Cannot convert value to AST: {}.',
);
expect(astFromValue('value', returnCustomClassScalar)).to.deep.equal({
kind: 'ObjectValue',
fields: [],
});

const returnObjectScalar = new GraphQLScalarType({
name: 'ReturnObjectScalar',
serialize() {
return { some: 'data' };
},
});

expect(astFromValue('value', returnObjectScalar)).to.deep.equal({
kind: 'ObjectValue',
fields: [
{
kind: 'ObjectField',
name: { kind: 'Name', value: 'some' },
value: { kind: 'StringValue', value: 'data' },
},
],
});
});

it('does not converts NonNull values to NullValue', () => {
Expand Down
178 changes: 178 additions & 0 deletions src/utilities/__tests__/astFromValueUntyped-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';

import { astFromValueUntyped } from '../astFromValueUntyped.js';

describe('astFromValue', () => {
it('converts boolean values to ASTs', () => {
expect(astFromValueUntyped(true)).to.deep.equal({
kind: 'BooleanValue',
value: true,
});

expect(astFromValueUntyped(false)).to.deep.equal({
kind: 'BooleanValue',
value: false,
});
});

it('converts Int values to Int ASTs', () => {
expect(astFromValueUntyped(-1)).to.deep.equal({
kind: 'IntValue',
value: '-1',
});

expect(astFromValueUntyped(123.0)).to.deep.equal({
kind: 'IntValue',
value: '123',
});

expect(astFromValueUntyped(1e4)).to.deep.equal({
kind: 'IntValue',
value: '10000',
});
});

it('converts Float values to Int/Float ASTs', () => {
expect(astFromValueUntyped(-1)).to.deep.equal({
kind: 'IntValue',
value: '-1',
});

expect(astFromValueUntyped(123.0)).to.deep.equal({
kind: 'IntValue',
value: '123',
});

expect(astFromValueUntyped(123.5)).to.deep.equal({
kind: 'FloatValue',
value: '123.5',
});

expect(astFromValueUntyped(1e4)).to.deep.equal({
kind: 'IntValue',
value: '10000',
});

expect(astFromValueUntyped(1e40)).to.deep.equal({
kind: 'FloatValue',
value: '1e+40',
});
});

it('converts String values to String ASTs', () => {
expect(astFromValueUntyped('hello')).to.deep.equal({
kind: 'StringValue',
value: 'hello',
});

expect(astFromValueUntyped('VALUE')).to.deep.equal({
kind: 'StringValue',
value: 'VALUE',
});

expect(astFromValueUntyped('VA\nLUE')).to.deep.equal({
kind: 'StringValue',
value: 'VA\nLUE',
});

expect(astFromValueUntyped(undefined)).to.deep.equal(null);
});

it('converts ID values to Int/String ASTs', () => {
expect(astFromValueUntyped('hello')).to.deep.equal({
kind: 'StringValue',
value: 'hello',
});

expect(astFromValueUntyped('VALUE')).to.deep.equal({
kind: 'StringValue',
value: 'VALUE',
});

// Note: EnumValues cannot contain non-identifier characters
expect(astFromValueUntyped('VA\nLUE')).to.deep.equal({
kind: 'StringValue',
value: 'VA\nLUE',
});

// Note: IntValues are used when possible.
expect(astFromValueUntyped(-1)).to.deep.equal({
kind: 'IntValue',
value: '-1',
});

expect(astFromValueUntyped(123)).to.deep.equal({
kind: 'IntValue',
value: '123',
});

expect(astFromValueUntyped('01')).to.deep.equal({
kind: 'StringValue',
value: '01',
});
});

it('converts array values to List ASTs', () => {
expect(astFromValueUntyped(['FOO', 'BAR'])).to.deep.equal({
kind: 'ListValue',
values: [
{ kind: 'StringValue', value: 'FOO' },
{ kind: 'StringValue', value: 'BAR' },
],
});

function* listGenerator() {
yield 1;
yield 2;
yield 3;
}

expect(astFromValueUntyped(listGenerator())).to.deep.equal({
kind: 'ListValue',
values: [
{ kind: 'IntValue', value: '1' },
{ kind: 'IntValue', value: '2' },
{ kind: 'IntValue', value: '3' },
],
});
});

it('converts list singletons', () => {
expect(astFromValueUntyped('FOO')).to.deep.equal({
kind: 'StringValue',
value: 'FOO',
});
});

it('converts objects', () => {
expect(astFromValueUntyped({ foo: 3, bar: 'HELLO' })).to.deep.equal({
kind: 'ObjectValue',
fields: [
{
kind: 'ObjectField',
name: { kind: 'Name', value: 'foo' },
value: { kind: 'IntValue', value: '3' },
},
{
kind: 'ObjectField',
name: { kind: 'Name', value: 'bar' },
value: { kind: 'StringValue', value: 'HELLO' },
},
],
});
});

it('converts objects with explicit nulls', () => {
expect(astFromValueUntyped({ foo: null })).to.deep.equal({
kind: 'ObjectValue',
fields: [
{
kind: 'ObjectField',
name: { kind: 'Name', value: 'foo' },
value: { kind: 'NullValue' },
},
],
});
});
});
27 changes: 6 additions & 21 deletions src/utilities/astFromValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ import {
} from '../type/definition.js';
import { GraphQLID } from '../type/scalars.js';

import {
astFromValueUntyped,
integerStringRegExp,
} from './astFromValueUntyped.js';

/**
* Produces a GraphQL Value AST given a JavaScript object.
* Function will match JavaScript/JSON values to GraphQL AST schema format
Expand Down Expand Up @@ -105,19 +110,6 @@ export function astFromValue(
return null;
}

// Others serialize based on their corresponding JavaScript scalar types.
if (typeof serialized === 'boolean') {
return { kind: Kind.BOOLEAN, value: serialized };
}

// JavaScript numbers can be Int or Float values.
if (typeof serialized === 'number' && Number.isFinite(serialized)) {
const stringNum = String(serialized);
return integerStringRegExp.test(stringNum)
? { kind: Kind.INT, value: stringNum }
: { kind: Kind.FLOAT, value: stringNum };
}

if (typeof serialized === 'string') {
// Enum types use Enum literals.
if (isEnumType(type)) {
Expand All @@ -135,16 +127,9 @@ export function astFromValue(
};
}

throw new TypeError(`Cannot convert value to AST: ${inspect(serialized)}.`);
return astFromValueUntyped(serialized);
}
/* c8 ignore next 3 */
// Not reachable, all possible types have been considered.
invariant(false, 'Unexpected input type: ' + inspect(type));
}

/**
* IntValue:
* - NegativeSign? 0
* - NegativeSign? NonZeroDigit ( Digit+ )?
*/
const integerStringRegExp = /^-?(?:0|[1-9][0-9]*)$/;
88 changes: 88 additions & 0 deletions src/utilities/astFromValueUntyped.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { inspect } from '../jsutils/inspect.js';
import { isIterableObject } from '../jsutils/isIterableObject.js';
import type { Maybe } from '../jsutils/Maybe.js';

import type { ConstObjectFieldNode, ConstValueNode } from '../language/ast.js';
import { Kind } from '../language/kinds.js';

/**
* Produces a GraphQL Value AST given a JavaScript object.
* Function will match JavaScript/JSON values to GraphQL AST schema format
* by using the following mapping.
*
* | JSON Value | GraphQL Value |
* | ------------- | -------------------- |
* | Object | Input Object |
* | Array | List |
* | Boolean | Boolean |
* | String | String |
* | Number | Int / Float |
* | null | NullValue |
*
*/
export function astFromValueUntyped(value: any): Maybe<ConstValueNode> {
// only explicit null, not undefined, NaN
if (value === null) {
return { kind: Kind.NULL };
}

// undefined
if (value === undefined) {
return null;
}

// Convert JavaScript array to GraphQL list. If the GraphQLType is a list, but
// the value is not an array, convert the value using the list's item type.
if (isIterableObject(value)) {
const valuesNodes: Array<ConstValueNode> = [];
for (const item of value) {
const itemNode = astFromValueUntyped(item);
if (itemNode != null) {
valuesNodes.push(itemNode);
}
}
return { kind: Kind.LIST, values: valuesNodes };
}

if (typeof value === 'object') {
const fieldNodes: Array<ConstObjectFieldNode> = [];
for (const fieldName of Object.getOwnPropertyNames(value)) {
const fieldValue = value[fieldName];
const ast = astFromValueUntyped(fieldValue);
if (ast) {
fieldNodes.push({
kind: Kind.OBJECT_FIELD,
name: { kind: Kind.NAME, value: fieldName },
value: ast,
});
}
}
return { kind: Kind.OBJECT, fields: fieldNodes };
}

// Others serialize based on their corresponding JavaScript scalar types.
if (typeof value === 'boolean') {
return { kind: Kind.BOOLEAN, value };
}

// JavaScript numbers can be Int or Float values.
if (typeof value === 'number' && isFinite(value)) {
const stringNum = String(value);
return integerStringRegExp.test(stringNum)
? { kind: Kind.INT, value: stringNum }
: { kind: Kind.FLOAT, value: stringNum };
}

if (typeof value === 'string') {
return { kind: Kind.STRING, value };
}

throw new TypeError(`Cannot convert value to AST: ${inspect(value)}.`);
}

/**
* IntValue:
* - NegativeSign? 0
* - NegativeSign? NonZeroDigit ( Digit+ )?
*/
export const integerStringRegExp = /^-?(?:0|[1-9][0-9]*)$/;
5 changes: 4 additions & 1 deletion src/utilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,12 @@ export { valueFromAST } from './valueFromAST.js';
// Create a JavaScript value from a GraphQL language AST without a type.
export { valueFromASTUntyped } from './valueFromASTUntyped.js';

// Create a GraphQL language AST from a JavaScript value.
// Create a GraphQL language AST from a JavaScript value with a type.
export { astFromValue } from './astFromValue.js';

// Create a GraphQL language AST from a JavaScript value without a type.
export { astFromValueUntyped } from './astFromValueUntyped.js';

// A helper to use within recursive-descent visitors which need to be aware of the GraphQL type system.
export { TypeInfo, visitWithTypeInfo } from './TypeInfo.js';

Expand Down
Loading