Skip to content

Commit

Permalink
fix(document-builder): strip $ on enum-typed variables (#1288)
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonkuhrt authored Dec 22, 2024
1 parent 4e8b69c commit 04d98c6
Show file tree
Hide file tree
Showing 24 changed files with 892 additions and 133 deletions.
121 changes: 121 additions & 0 deletions examples/70_type_level/selection-sets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* This example shows how to work with the selection set types generated by Graffle. Selection set types reflect your GraphQL schema.
*/

import { Graffle } from '../$/graffle/__.js'
import { showJson } from '../$/helpers.js'

type _ = [
//
// ========================
// Primary TypeScript Types
// ========================
//
//
// 1. GraphQL Types
// --------------
// The SelectionSets namespace contains a TypeScript type for each GraphQL type.
//
Graffle.SelectionSets.Query,
Graffle.SelectionSets.Mutation,
Graffle.SelectionSets.Battle,
// ...
//
// Each type contains the selection set for that type.
//
Graffle.SelectionSets.Query['pokemons'],
Graffle.SelectionSets.Query['trainers'],
Graffle.SelectionSets.Mutation['addPokemon'],
Graffle.SelectionSets.Battle['___on_BattleRoyale'],

// 2. GraphQL Type Fields
// -------------------
// Each name is also overloaded with a namespace. Within it, you will find more TypeScript types.
// One per selection set field in the respective GraphQL type.
//
Graffle.SelectionSets.Query.pokemons,
Graffle.SelectionSets.Query.trainers,
Graffle.SelectionSets.Mutation.addPokemon,
// ...
//
// Each GraphQL type field type has properties about the selection set set for that field.
//
Graffle.SelectionSets.Query.pokemons['$'],
Graffle.SelectionSets.Query.trainers['$skip'],
Graffle.SelectionSets.Mutation.addPokemon['id'],
// ...
//
// You may have already noticed but there is a relationship between these two things:
// - The GraphQL Type type properties
// - The GraphQL Type Field types.
//
// Use the kind of type that suites your use case.
//
Graffle.SelectionSets.Query['pokemons'],
Graffle.SelectionSets.Query.pokemons,
//
// 3. TypeScript Types for Arguments
// ------------------------------
// There are type definitions for GraphQL Type Field Arguments
//
Graffle.SelectionSets.Query.pokemons$Arguments,
Graffle.SelectionSets.Query.trainerByName$Arguments,
Graffle.SelectionSets.Mutation.addPokemon$Arguments,
// ...
//
//
// ======================
// Niche TypeScript Types
// ======================
//
//
// 4. "Expanded" Variants
// -------------------
// You will find various type definitions with the suffix `$Expanded`.
// From a type-checking point of view they are identical to their non-expanded form.
// They differ in how they will be displayed in tooling, namely IDEs.
// You can leverage them to improve DX in your use-cases.
// For more details, refer to their JSDoc.
//
Graffle.SelectionSets.Query.pokemons$Expanded,
Graffle.SelectionSets.Mutation.addPokemon$Expanded,
// ...
//
// 5. Inline Fragments
// ----------------
//
Graffle.SelectionSets.Query$FragmentInline,
Graffle.SelectionSets.Battle$FragmentInline,
// ...
//
// 6. Selection Sets Sans Union with Indicators
// -----------------------------------------
// If the GraphQL field is a non-scalar OR scalar-with-arguments, then its type will be synonymous
// with its explicit selection set type. For example:
//
Graffle.SelectionSets.Query.beings,
Graffle.SelectionSets.Query.beings$SelectionSet,
// However, if the GraphQL field IS a scalar-without-arguments, then its type will become an
// indicator unioned with the "meta" selection set (meaning stuff like field directives).
//
// Meanwhile, the explicit selection set type will _only_ have those meta things, not the
// indicator. For example:
//
Graffle.SelectionSets.Pokemon.name,
Graffle.SelectionSets.Pokemon.name$SelectionSet,
]

const graffle = Graffle.create()

const getPokemonsLike = async (filter: Graffle.SelectionSets.Query.pokemons$Arguments['filter']) =>
graffle.query.pokemons({
$: { filter },
hp: true,
name: true,
})

// todo add test coverage for $ stripping on arguments.
const pokemons = await getPokemonsLike({ $type: `water` })

// We don't lose any type safety. :)
showJson(pokemons)
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': '1734835236654'
'x-sent-at-time': '1734877667965'
},
method: 'post',
url: 'http://localhost:3000/graphql',
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 @@ -16,7 +16,7 @@
headers: Headers {
'content-type': 'application/graphql-response+json; charset=utf-8',
'content-length': '142',
date: 'Sun, 22 Dec 2024 02:40:37 GMT',
date: 'Sun, 22 Dec 2024 14:27:48 GMT',
connection: 'keep-alive',
'keep-alive': 'timeout=5'
},
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: 'e8d5df13d914fbbd4ccbdcfacd957ab0',
parentId: '8e35291fef6405ac',
traceId: '0e52a2a1fb3ccb88d38cd44dded63dec',
parentId: '6fcf4ddd316eb7dd',
traceState: undefined,
name: 'encode',
id: '57064bac33995f52',
id: 'e30897dc9cb51142',
kind: 0,
timestamp: 1734835237807000,
duration: 985.375,
timestamp: 1734877668621000,
duration: 1060.917,
attributes: {},
status: { code: 0 },
events: [],
Expand All @@ -33,14 +33,14 @@
}
},
instrumentationScope: { name: 'graffle', version: undefined, schemaUrl: undefined },
traceId: 'e8d5df13d914fbbd4ccbdcfacd957ab0',
parentId: '8e35291fef6405ac',
traceId: '0e52a2a1fb3ccb88d38cd44dded63dec',
parentId: '6fcf4ddd316eb7dd',
traceState: undefined,
name: 'pack',
id: '4160ec4bd017e345',
id: '893c1fed7b2256dd',
kind: 0,
timestamp: 1734835237808000,
duration: 12389.166,
timestamp: 1734877668623000,
duration: 18450.083,
attributes: {},
status: { code: 0 },
events: [],
Expand All @@ -57,14 +57,14 @@
}
},
instrumentationScope: { name: 'graffle', version: undefined, schemaUrl: undefined },
traceId: 'e8d5df13d914fbbd4ccbdcfacd957ab0',
parentId: '8e35291fef6405ac',
traceId: '0e52a2a1fb3ccb88d38cd44dded63dec',
parentId: '6fcf4ddd316eb7dd',
traceState: undefined,
name: 'exchange',
id: 'b605384e7351522a',
id: 'b0c30685f178d075',
kind: 0,
timestamp: 1734835237821000,
duration: 21988.041,
timestamp: 1734877668642000,
duration: 25750.5,
attributes: {},
status: { code: 0 },
events: [],
Expand All @@ -81,14 +81,14 @@
}
},
instrumentationScope: { name: 'graffle', version: undefined, schemaUrl: undefined },
traceId: 'e8d5df13d914fbbd4ccbdcfacd957ab0',
parentId: '8e35291fef6405ac',
traceId: '0e52a2a1fb3ccb88d38cd44dded63dec',
parentId: '6fcf4ddd316eb7dd',
traceState: undefined,
name: 'unpack',
id: '3dc693f1496eb20f',
id: 'c148fb01cd7693fa',
kind: 0,
timestamp: 1734835237844000,
duration: 1031.333,
timestamp: 1734877668668000,
duration: 1080.291,
attributes: {},
status: { code: 0 },
events: [],
Expand All @@ -105,14 +105,14 @@
}
},
instrumentationScope: { name: 'graffle', version: undefined, schemaUrl: undefined },
traceId: 'e8d5df13d914fbbd4ccbdcfacd957ab0',
parentId: '8e35291fef6405ac',
traceId: '0e52a2a1fb3ccb88d38cd44dded63dec',
parentId: '6fcf4ddd316eb7dd',
traceState: undefined,
name: 'decode',
id: '41886c6abbbe3566',
id: '634824deddb746e7',
kind: 0,
timestamp: 1734835237845000,
duration: 457.25,
timestamp: 1734877668670000,
duration: 516.708,
attributes: {},
status: { code: 0 },
events: [],
Expand All @@ -129,14 +129,14 @@
}
},
instrumentationScope: { name: 'graffle', version: undefined, schemaUrl: undefined },
traceId: 'e8d5df13d914fbbd4ccbdcfacd957ab0',
traceId: '0e52a2a1fb3ccb88d38cd44dded63dec',
parentId: undefined,
traceState: undefined,
name: 'request',
id: '8e35291fef6405ac',
id: '6fcf4ddd316eb7dd',
kind: 0,
timestamp: 1734835237806000,
duration: 39409.5,
timestamp: 1734877668621000,
duration: 49549.459,
attributes: {},
status: { code: 0 },
events: [],
Expand Down
7 changes: 7 additions & 0 deletions examples/__outputs__/70_type_level/selection-sets.output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---------------------------------------- SHOW ----------------------------------------
[
{
"hp": 44,
"name": "Squirtle"
}
]
16 changes: 8 additions & 8 deletions examples/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@
"serve:pokemon": "tsx tests/_/services/pokemonManual.ts",
"gen:graffle": "pnpm gen:graffle:tests && pnpm build && cd website && pnpm gen:graffle",
"gen:graffle:tests": "tsx tests/_/schemas/generate.ts && pnpm graffle --project src/extensions/SchemaErrors/tests/fixture",
"gen:graffle:examples": "pnpm build && cd examples && pnpm i && pnpm gen:graffle",
"examples:link-mode": "cd examples && pnpm add ..",
"graffle": "tsx ./src/generator/cli/generate.ts",
"gen:examples": "tsx scripts/generate-examples-derivatives/generate.ts && pnpm format",
"gen:examples:outputs": "tsx scripts/generate-examples-derivatives/generate.ts --outputs",
Expand Down
25 changes: 25 additions & 0 deletions src/lib/grafaid/request.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isPlainObject } from 'es-toolkit'
import type { GraphQLError, OperationDefinitionNode, OperationTypeNode } from 'graphql'
import type { Errors } from '../errors/__.js'
import type { Grafaid } from './__.js'
Expand Down Expand Up @@ -49,6 +50,7 @@ export const normalizeRequestToNode = <$R extends RequestInput | RequestAnalyzed
never => {

const query = normalizeDocumentToNode(request.query)
// we have to strip the $ from the variables keys (enum types)

if (`operation` in request) {
const operation = getOperationDefinition({
Expand All @@ -68,3 +70,26 @@ export const normalizeRequestToNode = <$R extends RequestInput | RequestAnalyzed
query,
} as any
}

// todo: refactor into concise visitor pattern.
export const normalizeVariables = (variables?: Variables): Variables => {
return normalizeVariables_(variables)
}

const normalizeVariables_ = (value: unknown): any => {
if (value === undefined) return undefined
if (value === null) return null
if (typeof value !== `object`) return value
if (Array.isArray(value)) return value.map(normalizeVariables_)
if (!isPlainObject(value)) return value // todo: optimize

const normalized: Variables = {}

for (const key in value) {
const normalizedKey = key.replace(/^\$/, ``)
const normalizedValue = normalizeVariables_(value[key])
normalized[normalizedKey] = normalizedValue
}

return normalized
}
4 changes: 3 additions & 1 deletion src/requestPipeline/RequestPipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { GraffleExecutionResultEnvelope } from '../client/handleOutput.js'
import { Anyware } from '../lib/anyware/__.js'
import type { Config } from '../lib/anyware/PipelineDef/Config.js'
import type { Grafaid } from '../lib/grafaid/__.js'
import { normalizeRequestToNode } from '../lib/grafaid/request.js'
import { normalizeRequestToNode, normalizeVariables } from '../lib/grafaid/request.js'
import { isAbortError } from '../lib/prelude.js'
import type { Context } from '../types/context.js'
import { decodeResultData } from './CustomScalars/decode.js'
Expand Down Expand Up @@ -90,8 +90,10 @@ export const requestPipelineBaseDefinition: RequestPipelineBaseDefinition = Anyw
input.request.query = request.query

encodeRequestVariables({ sddm, scalars, request })
// input.request.variables = request.variables // enum $ stripping
}

input.request.variables = normalizeVariables(input.request.variables)
return input
},
})
Expand Down
Loading

0 comments on commit 04d98c6

Please sign in to comment.