Skip to content

Commit

Permalink
fix(custom-scalars): decode arrays (#1210)
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonkuhrt authored Oct 23, 2024
1 parent 0a80885 commit 1eb0129
Show file tree
Hide file tree
Showing 40 changed files with 561 additions and 224 deletions.
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

0 comments on commit 1eb0129

Please sign in to comment.