From 495f5b593a3d99f42b5df40613138078f69d3e82 Mon Sep 17 00:00:00 2001
From: Arda TANRIKULU <ardatanrikulu@gmail.com>
Date: Wed, 8 May 2024 17:13:22 +0300
Subject: [PATCH 1/3] astFromValue fails with a custom scalar serializing to an
 object value

---
 src/utilities/__tests__/astFromValue-test.ts | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/src/utilities/__tests__/astFromValue-test.ts b/src/utilities/__tests__/astFromValue-test.ts
index 0f9d474256..195dfc153f 100644
--- a/src/utilities/__tests__/astFromValue-test.ts
+++ b/src/utilities/__tests__/astFromValue-test.ts
@@ -233,6 +233,24 @@ describe('astFromValue', () => {
     expect(() => astFromValue('value', returnCustomClassScalar)).to.throw(
       'Cannot convert value to AST: {}.',
     );
+
+    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', () => {

From 8ae6e7cfccf270ba7c7f9ae07e0786e41e885c73 Mon Sep 17 00:00:00 2001
From: Arda TANRIKULU <ardatanrikulu@gmail.com>
Date: Wed, 8 May 2024 17:32:39 +0300
Subject: [PATCH 2/3] Fix astFromValue to handle custom scalars with object
 values

---
 src/utilities/__tests__/astFromValue-test.ts |  7 +-
 src/utilities/astFromValue.ts                | 22 +----
 src/utilities/astFromValueUntyped.ts         | 88 ++++++++++++++++++++
 3 files changed, 95 insertions(+), 22 deletions(-)
 create mode 100644 src/utilities/astFromValueUntyped.ts

diff --git a/src/utilities/__tests__/astFromValue-test.ts b/src/utilities/__tests__/astFromValue-test.ts
index 195dfc153f..642f85fcc3 100644
--- a/src/utilities/__tests__/astFromValue-test.ts
+++ b/src/utilities/__tests__/astFromValue-test.ts
@@ -230,9 +230,10 @@ 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',
diff --git a/src/utilities/astFromValue.ts b/src/utilities/astFromValue.ts
index bb03baf232..d72896c33f 100644
--- a/src/utilities/astFromValue.ts
+++ b/src/utilities/astFromValue.ts
@@ -17,6 +17,8 @@ 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
@@ -105,18 +107,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.
@@ -135,16 +125,10 @@ 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]*)$/;
diff --git a/src/utilities/astFromValueUntyped.ts b/src/utilities/astFromValueUntyped.ts
new file mode 100644
index 0000000000..94cfe4a614
--- /dev/null
+++ b/src/utilities/astFromValueUntyped.ts
@@ -0,0 +1,88 @@
+import { inspect } from '../jsutils/inspect.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 (Array.isArray(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]*)$/;

From fae69d467b0ff8f9607a6b76b3f41b11ee90200d Mon Sep 17 00:00:00 2001
From: Arda TANRIKULU <ardatanrikulu@gmail.com>
Date: Wed, 8 May 2024 17:41:50 +0300
Subject: [PATCH 3/3] Introduce astFromValueUntyped

---
 .../__tests__/astFromValueUntyped-test.ts     | 178 ++++++++++++++++++
 src/utilities/astFromValue.ts                 |   7 +-
 src/utilities/astFromValueUntyped.ts          |  98 +++++-----
 src/utilities/index.ts                        |   5 +-
 4 files changed, 235 insertions(+), 53 deletions(-)
 create mode 100644 src/utilities/__tests__/astFromValueUntyped-test.ts

diff --git a/src/utilities/__tests__/astFromValueUntyped-test.ts b/src/utilities/__tests__/astFromValueUntyped-test.ts
new file mode 100644
index 0000000000..3ab80d9ffa
--- /dev/null
+++ b/src/utilities/__tests__/astFromValueUntyped-test.ts
@@ -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' },
+        },
+      ],
+    });
+  });
+});
diff --git a/src/utilities/astFromValue.ts b/src/utilities/astFromValue.ts
index d72896c33f..14cde1e15c 100644
--- a/src/utilities/astFromValue.ts
+++ b/src/utilities/astFromValue.ts
@@ -17,7 +17,10 @@ import {
 } from '../type/definition.js';
 import { GraphQLID } from '../type/scalars.js';
 
-import { astFromValueUntyped, integerStringRegExp } from './astFromValueUntyped.js';
+import {
+  astFromValueUntyped,
+  integerStringRegExp,
+} from './astFromValueUntyped.js';
 
 /**
  * Produces a GraphQL Value AST given a JavaScript object.
@@ -107,7 +110,6 @@ export function astFromValue(
       return null;
     }
 
-
     if (typeof serialized === 'string') {
       // Enum types use Enum literals.
       if (isEnumType(type)) {
@@ -131,4 +133,3 @@ export function astFromValue(
   // Not reachable, all possible types have been considered.
   invariant(false, 'Unexpected input type: ' + inspect(type));
 }
-
diff --git a/src/utilities/astFromValueUntyped.ts b/src/utilities/astFromValueUntyped.ts
index 94cfe4a614..90876a07ae 100644
--- a/src/utilities/astFromValueUntyped.ts
+++ b/src/utilities/astFromValueUntyped.ts
@@ -1,4 +1,5 @@
 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';
@@ -20,64 +21,63 @@ import { Kind } from '../language/kinds.js';
  *
  */
 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;
-    }
+  // only explicit null, not undefined, NaN
+  if (value === null) {
+    return { kind: Kind.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 (Array.isArray(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 };
-    }
+  // undefined
+  if (value === undefined) {
+    return null;
+  }
 
-    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 };
+  // 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 };
+  }
 
-    // Others serialize based on their corresponding JavaScript scalar types.
-    if (typeof value === 'boolean') {
-        return { kind: Kind.BOOLEAN, value };
+  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 };
+  }
 
-    // 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 };
-    }
+  // Others serialize based on their corresponding JavaScript scalar types.
+  if (typeof value === 'boolean') {
+    return { kind: Kind.BOOLEAN, value };
+  }
 
-    if (typeof value === 'string') {
-        return { kind: Kind.STRING, 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)}.`);
+  throw new TypeError(`Cannot convert value to AST: ${inspect(value)}.`);
 }
 
 /**
diff --git a/src/utilities/index.ts b/src/utilities/index.ts
index 6968dca4d3..d821961786 100644
--- a/src/utilities/index.ts
+++ b/src/utilities/index.ts
@@ -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';