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

fix(custom-scalars): decode arrays #1210

Merged
merged 3 commits into from
Oct 23, 2024
Merged
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
2 changes: 1 addition & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import tsEslint from 'typescript-eslint'

export default tsEslint.config({
ignores: [
'examples/55_document-buildergenerated_document__document.ts',
'examples/35_custom-scalar/custom-scalar.ts',
'eslint.config.js',
'vite.config.ts',
'vitest*.config.ts',
Expand Down
26 changes: 26 additions & 0 deletions examples/35_custom-scalar/custom-scalar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* This example shows how to add a client-side custom scalar codec to
* have arguments and data automatically encoded and decoded respectively.
*/

import { Graffle } from '../../src/entrypoints/__Graffle.js'
import { Pokemon } from '../../tests/_/schemas/pokemon/graffle/__.js'
import { show } from '../$/helpers.js'

const graffle = Pokemon
.create()
.scalar(Graffle.Scalars.create(`Date`, {
encode: (value: globalThis.Date) => value.toISOString(),
decode: (value: string) => {
return new globalThis.Date(value)
},
}))

const pokemons = await graffle.query.pokemons({
$: { filter: { birthday: { lte: new Date(`1987-01-13`) } } },
name: true,
birthday: true,
})

show(`pokemons[0].birthday instanceof Date = ${String(pokemons?.[0]?.birthday instanceof Date)}`)
show(pokemons)
4 changes: 2 additions & 2 deletions examples/55_document-builder/document-builder_alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ const pokemon = Pokemon.create()

const day = 1000 * 60 * 60 * 24
const year = day * 365.25
const yearsAgo100 = new Date(Date.now() - year * 100).getTime()
const yearsAgo1 = new Date(Date.now() - year).getTime()
const yearsAgo100 = new Date(Date.now() - year * 100).toISOString()
const yearsAgo1 = new Date(Date.now() - year).toISOString()

// dprint-ignore
const pokemons = await pokemon.query.$batch({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
headers: Headers {
accept: 'application/graphql-response+json; charset=utf-8, application/json; charset=utf-8',
'content-type': 'application/json',
'x-sent-at-time': '1729649902337'
'x-sent-at-time': '1729654429348'
},
signal: undefined,
method: 'post',
Expand Down
2 changes: 1 addition & 1 deletion examples/__outputs__/20_output/output_envelope.output.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
headers: Headers {
'content-type': 'application/graphql-response+json; charset=utf-8',
'content-length': '142',
date: 'Wed, 23 Oct 2024 02:18:22 GMT',
date: 'Wed, 23 Oct 2024 03:33:49 GMT',
connection: 'keep-alive',
'keep-alive': 'timeout=5'
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---------------------------------------- SHOW ----------------------------------------
pokemons[0].birthday instanceof Date = true
---------------------------------------- SHOW ----------------------------------------
[
{ name: 'Pikachu', birthday: 1850-01-01T00:00:00.000Z },
{ name: 'Squirtle', birthday: 1910-01-01T00:00:00.000Z }
]
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,16 @@
enumValues: null,
possibleTypes: null
},
{
kind: 'SCALAR',
name: 'Date',
description: 'A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar.This scalar is serialized to a string in ISO 8601 format and parsed from a string in ISO 8601 format.',
fields: null,
inputFields: null,
interfaces: null,
enumValues: null,
possibleTypes: null
},
{
kind: 'INPUT_OBJECT',
name: 'DateFilter',
Expand All @@ -341,13 +351,13 @@
{
name: 'gte',
description: null,
type: { kind: 'SCALAR', name: 'Float', ofType: null },
type: { kind: 'SCALAR', name: 'Date', ofType: null },
defaultValue: null
},
{
name: 'lte',
description: null,
type: { kind: 'SCALAR', name: 'Float', ofType: null },
type: { kind: 'SCALAR', name: 'Date', ofType: null },
defaultValue: null
}
],
Expand Down Expand Up @@ -495,7 +505,7 @@
name: 'birthday',
description: null,
args: [],
type: { kind: 'SCALAR', name: 'Int', ofType: null },
type: { kind: 'SCALAR', name: 'Date', ofType: null },
isDeprecated: false,
deprecationReason: null
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
}
},
instrumentationScope: { name: 'graffle', version: undefined, schemaUrl: undefined },
traceId: '2259f0e0e3b890cac862a813639e9c90',
parentId: 'f68c78e8a4e9ae99',
traceId: '4db92619c2c83794eea58ac3b0f81739',
parentId: '0441c81c457852b2',
traceState: undefined,
name: 'encode',
id: 'c8b432c64c6d23ce',
id: '0fed2ea32096273f',
kind: 0,
timestamp: 1729649903315000,
duration: 1492.792,
timestamp: 1729654429675000,
duration: 2681.792,
attributes: {},
status: { code: 0 },
events: [],
Expand All @@ -33,14 +33,14 @@
}
},
instrumentationScope: { name: 'graffle', version: undefined, schemaUrl: undefined },
traceId: '2259f0e0e3b890cac862a813639e9c90',
parentId: 'f68c78e8a4e9ae99',
traceId: '4db92619c2c83794eea58ac3b0f81739',
parentId: '0441c81c457852b2',
traceState: undefined,
name: 'pack',
id: '385c61790e55b6f4',
id: 'ca490dca8e6606c9',
kind: 0,
timestamp: 1729649903318000,
duration: 12042.5,
timestamp: 1729654429680000,
duration: 48326.792,
attributes: {},
status: { code: 0 },
events: [],
Expand All @@ -57,14 +57,14 @@
}
},
instrumentationScope: { name: 'graffle', version: undefined, schemaUrl: undefined },
traceId: '2259f0e0e3b890cac862a813639e9c90',
parentId: 'f68c78e8a4e9ae99',
traceId: '4db92619c2c83794eea58ac3b0f81739',
parentId: '0441c81c457852b2',
traceState: undefined,
name: 'exchange',
id: '67a764a3a78c819c',
id: '11066eaa0e07c122',
kind: 0,
timestamp: 1729649903331000,
duration: 21272.625,
timestamp: 1729654429729000,
duration: 47351.292,
attributes: {},
status: { code: 0 },
events: [],
Expand All @@ -81,14 +81,14 @@
}
},
instrumentationScope: { name: 'graffle', version: undefined, schemaUrl: undefined },
traceId: '2259f0e0e3b890cac862a813639e9c90',
parentId: 'f68c78e8a4e9ae99',
traceId: '4db92619c2c83794eea58ac3b0f81739',
parentId: '0441c81c457852b2',
traceState: undefined,
name: 'unpack',
id: 'a92c1ad25a186aa9',
id: 'c527bbb4b62958fa',
kind: 0,
timestamp: 1729649903352000,
duration: 1640.25,
timestamp: 1729654429777000,
duration: 1775.75,
attributes: {},
status: { code: 0 },
events: [],
Expand All @@ -105,14 +105,14 @@
}
},
instrumentationScope: { name: 'graffle', version: undefined, schemaUrl: undefined },
traceId: '2259f0e0e3b890cac862a813639e9c90',
parentId: 'f68c78e8a4e9ae99',
traceId: '4db92619c2c83794eea58ac3b0f81739',
parentId: '0441c81c457852b2',
traceState: undefined,
name: 'decode',
id: 'fbdf501193493665',
id: '20eb54a71997e91e',
kind: 0,
timestamp: 1729649903354000,
duration: 314.583,
timestamp: 1729654429779000,
duration: 669.25,
attributes: {},
status: { code: 0 },
events: [],
Expand All @@ -129,14 +129,14 @@
}
},
instrumentationScope: { name: 'graffle', version: undefined, schemaUrl: undefined },
traceId: '2259f0e0e3b890cac862a813639e9c90',
traceId: '4db92619c2c83794eea58ac3b0f81739',
parentId: undefined,
traceState: undefined,
name: 'request',
id: 'f68c78e8a4e9ae99',
id: '0441c81c457852b2',
kind: 0,
timestamp: 1729649903314000,
duration: 40837.167,
timestamp: 1729654429631000,
duration: 148602.75,
attributes: {},
status: { code: 0 },
events: [],
Expand Down
107 changes: 75 additions & 32 deletions src/extensions/CustomScalars/decode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,49 +27,88 @@ export const decodeResultData = ({ request, data, sddm, scalars }: {
}) => {
const sddmOutputObject = sddm.roots[request.rootType]
if (!sddmOutputObject) return
if (!data) return

for (const [key, value] of Object.entries(data)) {
const documentField = findDocumentField(request.operation.selectionSet, key)
const kSchema = documentField?.name.value ?? key
const sddmOutputField = sddmOutputObject.f[kSchema]
if (!sddmOutputField?.nt) continue

decodeResultData_({
data,
sddmOutputObject,
documentPart: request.operation.selectionSet,
scalars,
})
decodeResultValue({
parentContext: { type: `object`, object: data, key },
value,
sddmNode: sddmOutputField.nt,
documentPart: documentField?.selectionSet ?? null,
scalars,
})
}
}

const decodeResultData_ = (input: {
data: Grafaid.SomeObjectData | null | undefined
sddmOutputObject: SchemaDrivenDataMap.OutputObject
const decodeResultValue = (input: {
parentContext: { type: `object`; object: Record<string, any>; key: string } | {
type: `list`
object: any[]
key: number
}
value: Value
sddmNode: SchemaDrivenDataMap.OutputNodes
documentPart: null | Grafaid.Document.SelectionSetNode
scalars: RegisteredScalars
}): void => {
const { data, sddmOutputObject, documentPart, scalars } = input
if (!data) return
const { parentContext, value, sddmNode, documentPart, scalars } = input

for (const [k, v] of Object.entries(data)) {
if (value === null) {
// todo: test case of a custom scalar whose encoded value would be falsy in JS, like 0 or empty string
if (v === null) continue

const documentField = findDocumentField(documentPart, k)

const kSchema = documentField?.name.value ?? k

const sddmOutputField = sddmOutputObject.f[kSchema]
if (!sddmOutputField) continue

const sddmNode = sddmOutputField.nt

if (SchemaDrivenDataMap.isScalar(sddmNode)) {
data[k] = Schema.Scalar.applyCodec(sddmNode.codec.decode, v)
} else if (SchemaDrivenDataMap.isCustomScalarName(sddmNode)) {
const scalar = Schema.Scalar.lookupCustomScalarOrFallbackToString(scalars, sddmNode)
data[k] = Schema.Scalar.applyCodec(scalar.codec.decode, v)
} else if (SchemaDrivenDataMap.isOutputObject(sddmNode)) {
decodeResultData_({
data: v,
sddmOutputObject: sddmNode,
return // do nothing
} else if (Array.isArray(value)) {
// todo test case of array data of objects
// todo test case of array data of scalars
value.forEach((item, index) => {
decodeResultValue({
parentContext: { type: `list`, object: value, key: index },
value: item,
sddmNode,
documentPart,
scalars,
})
})
} else if (typeof value === `object`) {
if (!SchemaDrivenDataMap.isOutputObject(sddmNode)) {
return
// something went wrong
// todo in strict mode throw error that sddmNode is inconsistent with data shape.
}
const object = value
for (const [key, value] of Object.entries(object)) {
const documentField = findDocumentField(documentPart, key)
const kSchema = documentField?.name.value ?? key
const sddmOutputField = sddmNode.f[kSchema]
if (!sddmOutputField?.nt) continue
decodeResultValue({
parentContext: { type: `object`, object, key },
value,
sddmNode: sddmOutputField.nt,
documentPart: documentField?.selectionSet ?? null,
scalars,
})
}
} else {
if (SchemaDrivenDataMap.isScalar(sddmNode)) {
const decodedValue = Schema.Scalar.applyCodec(sddmNode.codec.decode, value)
if (parentContext.type === `object`) {
parentContext.object[parentContext.key] = decodedValue
} else {
parentContext.object[parentContext.key] = decodedValue
}
} else if (SchemaDrivenDataMap.isCustomScalarName(sddmNode)) {
const scalar = Schema.Scalar.lookupCustomScalarOrFallbackToString(scalars, sddmNode)
const decodedValue = Schema.Scalar.applyCodec(scalar.codec.decode, value)
if (parentContext.type === `object`) {
parentContext.object[parentContext.key] = decodedValue
} else {
parentContext.object[parentContext.key] = decodedValue
}
} else {
// enums not decoded.
}
Expand All @@ -94,3 +133,7 @@ const findDocumentField = (

return null
}

type Value = Grafaid.Schema.StandardScalarRuntimeTypes[] | Grafaid.Schema.StandardScalarRuntimeTypes | null | {
[k: string]: Value
}
2 changes: 1 addition & 1 deletion src/extensions/CustomScalars/encode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const encodeInputFieldLike = (
args: Record<string, any>,
argName: any,
argValue: any,
sddmNode: SchemaDrivenDataMap.InputLike,
sddmNode: SchemaDrivenDataMap.InputNodes,
scalars: RegisteredScalars,
) => {
/**
Expand Down
11 changes: 8 additions & 3 deletions src/generator/config/__snapshots__/config.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,14 @@ type CombatantSinglePokemon {
trainer: Trainer
}

"""
A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the \`date-time\` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar.This scalar is serialized to a string in ISO 8601 format and parsed from a string in ISO 8601 format.
"""
scalar Date

input DateFilter {
gte: Float
lte: Float
gte: Date
lte: Date
}

type Mutation {
Expand All @@ -65,7 +70,7 @@ type Patron implements Being {

type Pokemon implements Being {
attack: Int
birthday: Int
birthday: Date
defense: Int
hp: Int
id: ID
Expand Down
Loading