diff --git a/examples/__outputs__/20_output/output_return-error_return-error-execution__return-error-execution.output.txt b/examples/__outputs__/20_output/output_return-error_return-error-execution__return-error-execution.output.txt index d7470d59..881f9db8 100644 --- a/examples/__outputs__/20_output/output_return-error_return-error-execution__return-error-execution.output.txt +++ b/examples/__outputs__/20_output/output_return-error_return-error-execution__return-error-execution.output.txt @@ -21,9 +21,9 @@ ContextualAggregateError: One or more errors in the execution result. ] } ] - at (/some/path/to/graphqlHTTP.ts:XX:XX:47) + at (/some/path/to/http.ts:XX:XX:47) at Array.map () - at parseExecutionResult (/some/path/to/graphqlHTTP.ts:XX:XX:28) + at parseExecutionResult (/some/path/to/http.ts:XX:XX:28) at Object.unpack (/some/path/to/core.ts:XX:XX:26) at process.processTicksAndRejections (node:internal/process/task_queues:XX:XX) at async runHook (/some/path/to/runHook.ts:XX:XX:16) { diff --git a/examples/__outputs__/30_gql/gql_gql-document-node_gql-typed_gql-typed-graphql-document-node__gql-typed-graphql-document-node.output.txt b/examples/__outputs__/30_gql/gql_gql-document-node_gql-typed_gql-typed-graphql-document-node__gql-typed-graphql-document-node.output.txt new file mode 100644 index 00000000..04ee0750 --- /dev/null +++ b/examples/__outputs__/30_gql/gql_gql-document-node_gql-typed_gql-typed-graphql-document-node__gql-typed-graphql-document-node.output.txt @@ -0,0 +1,20 @@ + +node:internal/modules/run_main:123 + triggerUncaughtException( + ^ +Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/some/path/to/gql_gql-document-node_gql-typed_gql-typed-graphql-document-node__gql-typed-graphql-document-node.ts:XX:XX' imported from /Users/jasonkuhrt/projects/jasonkuhrt/graffle/ + at finalizeResolution (node:internal/modules/esm/resolve:XX:XX) + at moduleResolve (node:internal/modules/esm/resolve:XX:XX) + at defaultResolve (node:internal/modules/esm/resolve:XX:XX) + at nextResolve (node:internal/modules/esm/hooks:XX:XX) + at resolveBase (file:///Users/jasonkuhrt/projects/jasonkuhrt/graffle/node_modules/.pnpm/tsx@4.19.1/node_modules/tsx/dist/esm/index.mjs?1728404771051:XX:XX) + at resolveDirectory (file:///Users/jasonkuhrt/projects/jasonkuhrt/graffle/node_modules/.pnpm/tsx@4.19.1/node_modules/tsx/dist/esm/index.mjs?1728404771051:XX:XX) + at resolveTsPaths (file:///Users/jasonkuhrt/projects/jasonkuhrt/graffle/node_modules/.pnpm/tsx@4.19.1/node_modules/tsx/dist/esm/index.mjs?1728404771051:XX:XX) + at resolve (file:///Users/jasonkuhrt/projects/jasonkuhrt/graffle/node_modules/.pnpm/tsx@4.19.1/node_modules/tsx/dist/esm/index.mjs?1728404771051:XX:XX) + at nextResolve (node:internal/modules/esm/hooks:XX:XX) + at Hooks.resolve (node:internal/modules/esm/hooks:XX:XX) { + code: 'ERR_MODULE_NOT_FOUND', + url: 'file:/some/path/to/gql_gql-document-node_gql-typed_gql-typed-graphql-document-node__gql-typed-graphql-document-node.ts:XX:XX' +} + +Node.js vXX.XX.XX \ No newline at end of file diff --git a/examples/__outputs__/30_raw 11-54-53-229/raw_rawDocumentNode__raw-document-node.output.txt b/examples/__outputs__/30_raw 11-54-53-229/raw_rawDocumentNode__raw-document-node.output.txt new file mode 100644 index 00000000..6191da0e --- /dev/null +++ b/examples/__outputs__/30_raw 11-54-53-229/raw_rawDocumentNode__raw-document-node.output.txt @@ -0,0 +1,20 @@ + +node:internal/modules/run_main:123 + triggerUncaughtException( + ^ +Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/some/path/to/raw_rawDocumentNode__raw-document-node.ts:XX:XX' imported from /Users/jasonkuhrt/projects/jasonkuhrt/graffle/ + at finalizeResolution (node:internal/modules/esm/resolve:XX:XX) + at moduleResolve (node:internal/modules/esm/resolve:XX:XX) + at defaultResolve (node:internal/modules/esm/resolve:XX:XX) + at nextResolve (node:internal/modules/esm/hooks:XX:XX) + at resolveBase (file:///Users/jasonkuhrt/projects/jasonkuhrt/graffle/node_modules/.pnpm/tsx@4.19.1/node_modules/tsx/dist/esm/index.mjs?1728316475175:XX:XX) + at resolveDirectory (file:///Users/jasonkuhrt/projects/jasonkuhrt/graffle/node_modules/.pnpm/tsx@4.19.1/node_modules/tsx/dist/esm/index.mjs?1728316475175:XX:XX) + at resolveTsPaths (file:///Users/jasonkuhrt/projects/jasonkuhrt/graffle/node_modules/.pnpm/tsx@4.19.1/node_modules/tsx/dist/esm/index.mjs?1728316475175:XX:XX) + at resolve (file:///Users/jasonkuhrt/projects/jasonkuhrt/graffle/node_modules/.pnpm/tsx@4.19.1/node_modules/tsx/dist/esm/index.mjs?1728316475175:XX:XX) + at nextResolve (node:internal/modules/esm/hooks:XX:XX) + at Hooks.resolve (node:internal/modules/esm/hooks:XX:XX) { + code: 'ERR_MODULE_NOT_FOUND', + url: 'file:/some/path/to/raw_rawDocumentNode__raw-document-node.ts:XX:XX' +} + +Node.js vXX.XX.XX \ No newline at end of file diff --git a/examples/__outputs__/30_raw 11-54-53-229/raw_rawDocumentNode_rawTyped__raw-document-node-typed.output.txt b/examples/__outputs__/30_raw 11-54-53-229/raw_rawDocumentNode_rawTyped__raw-document-node-typed.output.txt new file mode 100644 index 00000000..ff498301 --- /dev/null +++ b/examples/__outputs__/30_raw 11-54-53-229/raw_rawDocumentNode_rawTyped__raw-document-node-typed.output.txt @@ -0,0 +1,20 @@ + +node:internal/modules/run_main:123 + triggerUncaughtException( + ^ +Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/some/path/to/raw_rawDocumentNode_rawTyped__raw-document-node-typed.ts:XX:XX' imported from /Users/jasonkuhrt/projects/jasonkuhrt/graffle/ + at finalizeResolution (node:internal/modules/esm/resolve:XX:XX) + at moduleResolve (node:internal/modules/esm/resolve:XX:XX) + at defaultResolve (node:internal/modules/esm/resolve:XX:XX) + at nextResolve (node:internal/modules/esm/hooks:XX:XX) + at resolveBase (file:///Users/jasonkuhrt/projects/jasonkuhrt/graffle/node_modules/.pnpm/tsx@4.19.1/node_modules/tsx/dist/esm/index.mjs?1728316472737:XX:XX) + at resolveDirectory (file:///Users/jasonkuhrt/projects/jasonkuhrt/graffle/node_modules/.pnpm/tsx@4.19.1/node_modules/tsx/dist/esm/index.mjs?1728316472737:XX:XX) + at resolveTsPaths (file:///Users/jasonkuhrt/projects/jasonkuhrt/graffle/node_modules/.pnpm/tsx@4.19.1/node_modules/tsx/dist/esm/index.mjs?1728316472737:XX:XX) + at resolve (file:///Users/jasonkuhrt/projects/jasonkuhrt/graffle/node_modules/.pnpm/tsx@4.19.1/node_modules/tsx/dist/esm/index.mjs?1728316472737:XX:XX) + at nextResolve (node:internal/modules/esm/hooks:XX:XX) + at Hooks.resolve (node:internal/modules/esm/hooks:XX:XX) { + code: 'ERR_MODULE_NOT_FOUND', + url: 'file:/some/path/to/raw_rawDocumentNode_rawTyped__raw-document-node-typed.ts:XX:XX' +} + +Node.js vXX.XX.XX \ No newline at end of file diff --git a/examples/__outputs__/30_raw 11-54-53-229/raw_rawString__rawString.output.txt b/examples/__outputs__/30_raw 11-54-53-229/raw_rawString__rawString.output.txt new file mode 100644 index 00000000..9cfdad84 --- /dev/null +++ b/examples/__outputs__/30_raw 11-54-53-229/raw_rawString__rawString.output.txt @@ -0,0 +1,20 @@ + +node:internal/modules/run_main:123 + triggerUncaughtException( + ^ +Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/some/path/to/raw_rawString__rawString.ts:XX:XX' imported from /Users/jasonkuhrt/projects/jasonkuhrt/graffle/ + at finalizeResolution (node:internal/modules/esm/resolve:XX:XX) + at moduleResolve (node:internal/modules/esm/resolve:XX:XX) + at defaultResolve (node:internal/modules/esm/resolve:XX:XX) + at nextResolve (node:internal/modules/esm/hooks:XX:XX) + at resolveBase (file:///Users/jasonkuhrt/projects/jasonkuhrt/graffle/node_modules/.pnpm/tsx@4.19.1/node_modules/tsx/dist/esm/index.mjs?1728316478033:XX:XX) + at resolveDirectory (file:///Users/jasonkuhrt/projects/jasonkuhrt/graffle/node_modules/.pnpm/tsx@4.19.1/node_modules/tsx/dist/esm/index.mjs?1728316478033:XX:XX) + at resolveTsPaths (file:///Users/jasonkuhrt/projects/jasonkuhrt/graffle/node_modules/.pnpm/tsx@4.19.1/node_modules/tsx/dist/esm/index.mjs?1728316478033:XX:XX) + at resolve (file:///Users/jasonkuhrt/projects/jasonkuhrt/graffle/node_modules/.pnpm/tsx@4.19.1/node_modules/tsx/dist/esm/index.mjs?1728316478033:XX:XX) + at nextResolve (node:internal/modules/esm/hooks:XX:XX) + at Hooks.resolve (node:internal/modules/esm/hooks:XX:XX) { + code: 'ERR_MODULE_NOT_FOUND', + url: 'file:/some/path/to/raw_rawString__rawString.ts:XX:XX' +} + +Node.js vXX.XX.XX \ No newline at end of file diff --git a/examples/__outputs__/30_raw 11-54-53-229/raw_rawString_rawTyped__rawString-typed.output.txt b/examples/__outputs__/30_raw 11-54-53-229/raw_rawString_rawTyped__rawString-typed.output.txt new file mode 100644 index 00000000..7b3dd276 --- /dev/null +++ b/examples/__outputs__/30_raw 11-54-53-229/raw_rawString_rawTyped__rawString-typed.output.txt @@ -0,0 +1,20 @@ + +node:internal/modules/run_main:123 + triggerUncaughtException( + ^ +Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/some/path/to/raw_rawString_rawTyped__rawString-typed.ts:XX:XX' imported from /Users/jasonkuhrt/projects/jasonkuhrt/graffle/ + at finalizeResolution (node:internal/modules/esm/resolve:XX:XX) + at moduleResolve (node:internal/modules/esm/resolve:XX:XX) + at defaultResolve (node:internal/modules/esm/resolve:XX:XX) + at nextResolve (node:internal/modules/esm/hooks:XX:XX) + at resolveBase (file:///Users/jasonkuhrt/projects/jasonkuhrt/graffle/node_modules/.pnpm/tsx@4.19.1/node_modules/tsx/dist/esm/index.mjs?1728316474983:XX:XX) + at resolveDirectory (file:///Users/jasonkuhrt/projects/jasonkuhrt/graffle/node_modules/.pnpm/tsx@4.19.1/node_modules/tsx/dist/esm/index.mjs?1728316474983:XX:XX) + at resolveTsPaths (file:///Users/jasonkuhrt/projects/jasonkuhrt/graffle/node_modules/.pnpm/tsx@4.19.1/node_modules/tsx/dist/esm/index.mjs?1728316474983:XX:XX) + at resolve (file:///Users/jasonkuhrt/projects/jasonkuhrt/graffle/node_modules/.pnpm/tsx@4.19.1/node_modules/tsx/dist/esm/index.mjs?1728316474983:XX:XX) + at nextResolve (node:internal/modules/esm/hooks:XX:XX) + at Hooks.resolve (node:internal/modules/esm/hooks:XX:XX) { + code: 'ERR_MODULE_NOT_FOUND', + url: 'file:/some/path/to/raw_rawString_rawTyped__rawString-typed.ts:XX:XX' +} + +Node.js vXX.XX.XX \ No newline at end of file diff --git a/src/entrypoints/main.ts b/src/entrypoints/main.ts index b116974e..6a44f797 100644 --- a/src/entrypoints/main.ts +++ b/src/entrypoints/main.ts @@ -1,5 +1,5 @@ export { createExtension, type Extension } from '../layers/6_client/extension/extension.js' -export { type TypedDocument } from '../lib/typed-document/__.js' +export { type TypedDocument } from '../lib/grafaid/typed-document/__.js' // todo figure this export out. Was just put there to resolve a type error about "...cannot be named..." export { type Config as BuilderConfig } from '../layers/6_client/Settings/Config.js' export * from '../layers/6_client/Settings/Input.js' diff --git a/src/layers/1_Schema/Output/types/Object.ts b/src/layers/1_Schema/Output/types/Object.ts index 176eaffe..772aac42 100644 --- a/src/layers/1_Schema/Output/types/Object.ts +++ b/src/layers/1_Schema/Output/types/Object.ts @@ -1,23 +1,19 @@ -import type { - RootTypeNameMutation, - RootTypeNameQuery, - RootTypeNameSubscription, -} from '../../../../lib/graphql-plus/graphql.js' +import type { Grafaid } from '../../../../lib/grafaid/__.js' import { __typename } from './__typename.js' import type { Field, SomeFields } from './Field.js' import { field } from './Field.js' export interface ObjectQuery< $Fields extends SomeFields = SomeFields, -> extends Object$2 {} +> extends Object$2 {} export interface ObjectMutation< $Fields extends SomeFields = SomeFields, -> extends Object$2 {} +> extends Object$2 {} export interface ObjectSubscription< $Fields extends SomeFields = SomeFields, -> extends Object$2 {} +> extends Object$2 {} export type RootType = ObjectQuery | ObjectMutation | ObjectSubscription diff --git a/src/layers/2_Select/document.ts b/src/layers/2_Select/document.ts index e1df028d..4ac519b4 100644 --- a/src/layers/2_Select/document.ts +++ b/src/layers/2_Select/document.ts @@ -1,10 +1,5 @@ import type { OperationTypeNode } from 'graphql' -import { RootTypeName, RootTypeNameToOperationName } from '../../lib/graphql-plus/graphql.js' -import { - type OperationType, - type RootTypeNameMutation, - type RootTypeNameQuery, -} from '../../lib/graphql-plus/graphql.js' +import { Grafaid } from '../../lib/grafaid/__.js' import type { FirstNonUnknownNever, IsKeyInObjectOptional, Values } from '../../lib/prelude.js' import type { Select } from './__.js' @@ -39,8 +34,8 @@ export type GetOperationNames<$Document extends SomeDocument> = Values< // dprint-ignore export type GetRootTypeNameOfOperation<$Document extends SomeDocument, $Name extends OperationName> = - IsKeyInObjectOptional<$Document[OperationType.Mutation], $Name> extends true ? RootTypeNameMutation : - IsKeyInObjectOptional<$Document[OperationType.Query], $Name> extends true ? RootTypeNameQuery : + IsKeyInObjectOptional<$Document[OperationTypeNode.MUTATION], $Name> extends true ? Grafaid.Schema.RootTypeNameMutation : + IsKeyInObjectOptional<$Document[OperationTypeNode.QUERY], $Name> extends true ? Grafaid.Schema.RootTypeNameQuery : never // dprint-ignore @@ -55,7 +50,7 @@ export type GetOperation<$Document extends SomeDocument, $Name extends string> = export interface OperationNormalized { name: string | null type: OperationTypeNode - rootType: RootTypeName + rootType: Grafaid.Schema.RootTypeName selectionSet: Select.SelectionSet.AnySelectionSet } @@ -74,14 +69,14 @@ export interface DocumentNormalized { export const createDocumentNormalized = (document: DocumentNormalized) => document export const createDocumentNormalizedFromRootTypeSelection = ( - rootTypeName: RootTypeName, + rootTypeName: Grafaid.Schema.RootTypeName, selectionSet: Select.SelectionSet.AnySelectionSet, ) => createDocumentNormalized({ operations: { [defaultOperationName]: { name: null, - type: RootTypeNameToOperationName[rootTypeName], + type: Grafaid.RootTypeNameToOperationName[rootTypeName], rootType: rootTypeName, selectionSet, }, @@ -89,9 +84,9 @@ export const createDocumentNormalizedFromRootTypeSelection = ( facts: { hasMultipleOperations: false, hasRootType: { - query: rootTypeName === RootTypeName.Query, - mutation: rootTypeName === RootTypeName.Mutation, - subscription: rootTypeName === RootTypeName.Subscription, + query: rootTypeName === Grafaid.Schema.RootTypeName.Query, + mutation: rootTypeName === Grafaid.Schema.RootTypeName.Mutation, + subscription: rootTypeName === Grafaid.Schema.RootTypeName.Subscription, }, }, }) @@ -101,16 +96,16 @@ export const normalizeOrThrow = (document: DocumentObject): DocumentNormalized = [name, selectionSet], ): [name: string, OperationNormalized] => [name, { name, - type: RootTypeNameToOperationName[RootTypeName.Query], - rootType: RootTypeName.Query, + type: Grafaid.RootTypeNameToOperationName[Grafaid.Schema.RootTypeName.Query], + rootType: Grafaid.Schema.RootTypeName.Query, selectionSet, }]) const mutationOperations = Object.entries(document.mutation ?? {}).map(( [name, selectionSet], ): [name: string, OperationNormalized] => [name, { name, - type: RootTypeNameToOperationName[RootTypeName.Mutation], - rootType: RootTypeName.Mutation, + type: Grafaid.RootTypeNameToOperationName[Grafaid.Schema.RootTypeName.Mutation], + rootType: Grafaid.Schema.RootTypeName.Mutation, selectionSet, }]) const operations = [ diff --git a/src/layers/3_Result/_.ts b/src/layers/3_Result/_.ts index 6ae16fe2..be3b07ae 100644 --- a/src/layers/3_Result/_.ts +++ b/src/layers/3_Result/_.ts @@ -1,2 +1 @@ -export { decode } from './decode.js' export * from './infer/_.js' diff --git a/src/layers/3_Result/decode.test.ts b/src/layers/3_Result/decode.test.ts deleted file mode 100644 index 9e5dd773..00000000 --- a/src/layers/3_Result/decode.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { expect, test } from 'vitest' -import { $Index } from '../../../tests/_/schemas/kitchen-sink/graffle/modules/SchemaRuntime.js' -import type * as SelectionSets from '../../../tests/_/schemas/kitchen-sink/graffle/modules/SelectionSets.js' -import { decode } from './decode.js' - -test.each<[selectionSet: SelectionSets.Query, data: object]>([ - [{ object: { id: true } }, { object: { id: `x` } }], - [{ id: [`x`, true] }, { x: `foo` }], - [{ listInt: [`x`, true] }, { x: [1] }], - [{ objectNested: { object: { id: [`x`, true] } } }, { objectNested: { object: { x: `x` } } }], - [{ ___: { __typename: true } }, { __typename: `Query` }], - [{ ___: { __typename: [`type`, true] } }, { type: `Query` }], - [{ unionFooBar: { ___on_Foo: { id: true } } }, { unionFooBar: { id: `x` } }], - [{ unionFooBar: { ___on_Foo: { id: true } } }, { unionFooBar: null }], - [{ interface: { ___on_Object1ImplementingInterface: { id: true } } }, { interface: { id: `x` } }], - [{ abcEnum: true }, { abcEnum: `foo` }], -])(`%s -----> %s`, (selectionSet, data) => { - // @ts-expect-error fixme - expect(decode($Index.Root.Query, selectionSet, data)).toEqual(data) -}) diff --git a/src/layers/3_Result/decode.ts b/src/layers/3_Result/decode.ts deleted file mode 100644 index fc131e55..00000000 --- a/src/layers/3_Result/decode.ts +++ /dev/null @@ -1,197 +0,0 @@ -import type { ExecutionResult } from 'graphql' -import { StandardScalarTypeNames } from '../../lib/graphql-plus/graphql.js' -import { assertObject } from '../../lib/prelude.js' -import { assertArray, casesExhausted, mapValues } from '../../lib/prelude.js' -import type { Object$2, Schema } from '../1_Schema/__.js' -import { Output } from '../1_Schema/__.js' -import { readMaybeThunk } from '../1_Schema/core/helpers.js' -import { Select } from '../2_Select/__.js' - -const getAliasesField = (fieldName: string, ss: Select.SelectionSet.AnySelectionSet) => { - for (const [schemaFieldName, selection] of Object.entries(ss)) { - if (Select.SelectAlias.isSelectAlias(selection)) { - const selectAliasMultiple = Select.SelectAlias.normalizeSelectAlias(selection) - for (const [aliasName, aliasSelectionSet] of selectAliasMultiple) { - if (aliasName === fieldName) { - return { - fieldName: schemaFieldName, - selectionSet: aliasSelectionSet, - } - } - } - } - } - - return null -} - -// --- - -const getDataFieldInSelectionSet = ( - fieldName: string, - selectionSet: Select.SelectionSet.AnySelectionSet, -): null | { - fieldName: string - selectionSet: Select.SelectionSet.AnyExceptAlias -} => { - const result = getDataFieldInSelectionSet_(fieldName, selectionSet) - if (result) return result - - return null - // throw new Error( - // `Cannot decode field "${fieldName}" in result data. That field was not found in the selection set.`, - // ) -} - -const getDataFieldInSelectionSet_ = ( - fieldName: string, - selectionSet: Select.SelectionSet.AnySelectionSet, -): null | { - fieldName: string - selectionSet: Select.SelectionSet.AnyExceptAlias -} => { - const fromDirect = selectionSet[fieldName] - if (fromDirect) { - return { - fieldName: fieldName, - selectionSet: fromDirect, - } - } - - const fromAlias = getAliasesField(fieldName, selectionSet) - if (fromAlias) return fromAlias - - for (const group of getInlineFragmentGroups(selectionSet)) { - const fromGroup = getDataFieldInSelectionSet_(fieldName, group) - if (fromGroup) return fromGroup - } - - for (const typeCase of getInlineFragmentTypeCases(selectionSet)) { - const fromTypeCase = getDataFieldInSelectionSet_(fieldName, typeCase.selectionSet) - if (fromTypeCase) return fromTypeCase - } - - return null -} - -const getInlineFragmentTypeCases = (selectionSet: Select.SelectionSet.AnySelectionSet) => { - return Object.entries(selectionSet).map(([key, expression]) => { - const typeName = key.match(/___on_(.+)/)?.[0] - if (typeName) { - return { - typeName, - selectionSet: expression as Select.SelectionSet.AnySelectionSet, - } - } - return null - }).filter(_ => _ !== null) -} - -const getInlineFragmentGroups = (selectionSet: Select.SelectionSet.AnySelectionSet) => { - const maybeGroupOrGroups = selectionSet[`___`] - if (!maybeGroupOrGroups) return [] - return Array.isArray(maybeGroupOrGroups) ? maybeGroupOrGroups : [maybeGroupOrGroups] -} - -/** - * Decode custom scalars in the result data. - */ -export const decode = <$Data extends ExecutionResult['data']>( - objectType: Schema.Object$2, - selectionSet: Select.SelectionSet.AnySelectionSet, - data: $Data, -): $Data => { - if (!data) return data - - return mapValues(data, (value, fieldName) => { - const selectionSetField = getDataFieldInSelectionSet(fieldName, selectionSet) - if (!selectionSetField) return value - - const schemaField = objectType.fields[selectionSetField.fieldName] - if (!schemaField) throw new Error(`Field not found in schema: ${String(selectionSetField.fieldName)}`) - const schemaFieldType = readMaybeThunk(schemaField.type) - const schemaFieldTypeSansNonNull = Output.unwrapNullable(schemaFieldType) as Output.Named | Output.List - const v2 = decodeCustomScalarValue(schemaFieldTypeSansNonNull, selectionSetField.selectionSet, value as any) - return v2 - }) as $Data -} - -const decodeCustomScalarValue = ( - fieldType: Output.Any, - selectionSet: Select.SelectionSet.Any, - fieldValue: string | boolean | null | number | GraphQLObject | GraphQLObject[], -) => { - if (fieldValue === null) return null - - const schemaTypeDethunked = readMaybeThunk(fieldType) - const schemaTypeWithoutNonNull = Output.unwrapNullable(schemaTypeDethunked) as Exclude< - Output.Any, - Output.Nullable - > - - if (schemaTypeWithoutNonNull.kind === `list`) { - assertArray(fieldValue) - return fieldValue.map((v2: any): any => { - return decodeCustomScalarValue(schemaTypeWithoutNonNull.type, selectionSet, v2) - }) - } - - if (schemaTypeWithoutNonNull.kind === `Scalar`) { - if ((schemaTypeWithoutNonNull.name in StandardScalarTypeNames)) { - // todo test this case - return fieldValue - } - if (typeof fieldValue === `object`) throw new Error(`Expected scalar. Got: ${String(fieldValue)}`) - // @ts-expect-error fixme - return schemaTypeWithoutNonNull.codec.decode(fieldValue) - } - - if (schemaTypeWithoutNonNull.kind === `typename`) { - return fieldValue - } - - if (schemaTypeWithoutNonNull.kind === `Enum`) { - return fieldValue - } - - assertGraphQLObject(fieldValue) - - if (schemaTypeWithoutNonNull.kind === `Object`) { - // todo fix any cast - return decode(schemaTypeWithoutNonNull, selectionSet as any, fieldValue) - } - - if (schemaTypeWithoutNonNull.kind === `Interface` || schemaTypeWithoutNonNull.kind === `Union`) { // eslint-disable-line - const possibleObjects = schemaTypeWithoutNonNull.kind === `Interface` - ? schemaTypeWithoutNonNull.implementors - : schemaTypeWithoutNonNull.members - // todo handle aliases -- will require having the selection set available for reference too :/ - // eslint-disable-next-line - // @ts-ignore infinite depth issue - // eslint-disable-next-line - const ObjectType = possibleObjects.find((ObjectType) => { - if (fieldValue.__typename === ObjectType.fields.__typename.type.type) return true - if (Object.keys(fieldValue).every(fieldName => ObjectType.fields[fieldName] !== undefined)) return true - return false - }) as undefined | Object$2 - if (!ObjectType) throw new Error(`Could not pick object for ${schemaTypeWithoutNonNull.kind} selection`) - // todo fix any cast - return decode(ObjectType, selectionSet as any, fieldValue) - } - - casesExhausted(schemaTypeWithoutNonNull) - - return fieldValue -} - -// eslint-disable-next-line -export function assertGraphQLObject(v: unknown): asserts v is GraphQLObject { - assertObject(v) - if (`__typename` in v && typeof v.__typename !== `string`) { - throw new Error(`Expected string __typename or undefined. Got: ${String(v.__typename)}`) - } -} - -export type GraphQLObject = { - __typename?: string -} diff --git a/src/layers/3_SelectGraphQLMapper/helpers.ts b/src/layers/3_SelectGraphQLMapper/helpers.ts index 0559329e..264a5489 100644 --- a/src/layers/3_SelectGraphQLMapper/helpers.ts +++ b/src/layers/3_SelectGraphQLMapper/helpers.ts @@ -1,16 +1,14 @@ import type { Select } from '../2_Select/__.js' -import type { CustomScalarsIndex, SchemaIndex } from '../4_generator/generators/SchemaIndex.js' +import type { CustomScalarsIndex } from '../4_generator/generators/SchemaIndex.js' import { toGraphQLDocument } from './nodes/Document.js' export const toGraphQL = (input: { - schema: SchemaIndex document: Select.Document.DocumentNormalized customScalarsIndex?: CustomScalarsIndex // we can probably remove this. was an idea that was aborted. Do we need scalar hook? // hooks?: Context['hooks'] }) => { const context = { - schema: input.schema, captures: { customScalarOutputs: [], variables: [], @@ -18,7 +16,6 @@ export const toGraphQL = (input: { // hooks: input.hooks, } - // const location: Location = [] const customScalarsIndex: CustomScalarsIndex = input.customScalarsIndex ?? {} return toGraphQLDocument(context, customScalarsIndex, input.document) diff --git a/src/layers/3_SelectGraphQLMapper/nodes/Argument.ts b/src/layers/3_SelectGraphQLMapper/nodes/Argument.ts index 2aaea397..57812e7b 100644 --- a/src/layers/3_SelectGraphQLMapper/nodes/Argument.ts +++ b/src/layers/3_SelectGraphQLMapper/nodes/Argument.ts @@ -1,4 +1,4 @@ -import { Nodes } from '../../../lib/graphql-plus/_Nodes.js' +import { Nodes } from '../../../lib/grafaid/_Nodes.js' import { Select } from '../../2_Select/__.js' import { advanceIndex, type GraphQLNodeMapper } from '../types.js' import { toGraphQLValue } from './Value.js' diff --git a/src/layers/3_SelectGraphQLMapper/nodes/Directive.ts b/src/layers/3_SelectGraphQLMapper/nodes/Directive.ts index 6aed0388..3ad8e666 100644 --- a/src/layers/3_SelectGraphQLMapper/nodes/Directive.ts +++ b/src/layers/3_SelectGraphQLMapper/nodes/Directive.ts @@ -1,4 +1,4 @@ -import { Nodes } from '../../../lib/graphql-plus/_Nodes.js' +import { Nodes } from '../../../lib/grafaid/_Nodes.js' import { getFromEnumLooselyOrThrow } from '../../../lib/prelude.js' import { Select } from '../../2_Select/__.js' import type { GraphQLNodeMapper } from '../types.js' diff --git a/src/layers/3_SelectGraphQLMapper/nodes/Document.test.ts b/src/layers/3_SelectGraphQLMapper/nodes/Document.test.ts index b0fe346b..099aa56a 100644 --- a/src/layers/3_SelectGraphQLMapper/nodes/Document.test.ts +++ b/src/layers/3_SelectGraphQLMapper/nodes/Document.test.ts @@ -2,7 +2,6 @@ import { print } from 'graphql' import { describe, expect, test } from 'vitest' import { db } from '../../../../tests/_/schemas/db.js' import { $index as customScalarsIndex } from '../../../../tests/_/schemas/kitchen-sink/graffle/modules/RuntimeCustomScalars.js' -import { $Index as SchemaIndex } from '../../../../tests/_/schemas/kitchen-sink/graffle/modules/SchemaRuntime.js' import type * as SelectionSets from '../../../../tests/_/schemas/kitchen-sink/graffle/modules/SelectionSets.js' import { Select } from '../../2_Select/__.js' import type { Context } from '../types.js' @@ -18,18 +17,10 @@ const testEachArguments = [ const [description, selectionSet] = args.length === 1 ? [undefined, args[0]] : args const context: Context = { - schema: SchemaIndex, captures: { variables: [], customScalarOutputs: [], }, - // config: { - // output: outputConfigDefault, - // transport: { type: `memory`, config: { methodMode: `post` } }, - // name: schemaIndex[`name`], - - // initialInput: {} as any, - // }, } const documentNormalized = Select.Document.createDocumentNormalizedFromRootTypeSelection( `Query`, diff --git a/src/layers/3_SelectGraphQLMapper/nodes/Document.ts b/src/layers/3_SelectGraphQLMapper/nodes/Document.ts index a9332fc1..beaee9a6 100644 --- a/src/layers/3_SelectGraphQLMapper/nodes/Document.ts +++ b/src/layers/3_SelectGraphQLMapper/nodes/Document.ts @@ -1,4 +1,4 @@ -import { Nodes } from '../../../lib/graphql-plus/_Nodes.js' +import { Nodes } from '../../../lib/grafaid/_Nodes.js' import type { Select } from '../../2_Select/__.js' import { advanceIndex, type GraphQLNodeMapper } from '../types.js' import { toGraphQLOperationDefinition } from './OperationDefinition.js' diff --git a/src/layers/3_SelectGraphQLMapper/nodes/Field.ts b/src/layers/3_SelectGraphQLMapper/nodes/Field.ts index d2546c1d..5963b570 100644 --- a/src/layers/3_SelectGraphQLMapper/nodes/Field.ts +++ b/src/layers/3_SelectGraphQLMapper/nodes/Field.ts @@ -1,14 +1,12 @@ -import { Nodes } from '../../../lib/graphql-plus/_Nodes.js' -import type { Schema } from '../../1_Schema/__.js' +import { Nodes } from '../../../lib/grafaid/_Nodes.js' import { Select } from '../../2_Select/__.js' import { advanceIndex, type Field } from '../types.js' import type { GraphQLNodeMapper } from '../types.js' import { type SelectionSetContext, toGraphQLSelectionSet } from './SelectionSet.js' -export const toGraphQLField: GraphQLNodeMapper = ( +export const toGraphQLField: GraphQLNodeMapper = ( context, index, - type, field, ) => { const alias = field.alias @@ -26,13 +24,11 @@ export const toGraphQLField: GraphQLNodeMapper = ( context, location, - type, inlineFragment, ) => { const typeCondition = inlineFragment.typeCondition @@ -25,11 +23,10 @@ export const toGraphQLInlineFragment: GraphQLNodeMapper< const selectionSetContext: SelectionSetContext = { kind: `InlineFragment`, - type, directives: graphqlDirectives, } - const selectionSet = toGraphQLSelectionSet(context, location, type, inlineFragment.selectionSet, selectionSetContext) + const selectionSet = toGraphQLSelectionSet(context, location, inlineFragment.selectionSet, selectionSetContext) return Nodes.InlineFragment({ typeCondition, diff --git a/src/layers/3_SelectGraphQLMapper/nodes/OperationDefinition.ts b/src/layers/3_SelectGraphQLMapper/nodes/OperationDefinition.ts index dab4b7e9..6ee19610 100644 --- a/src/layers/3_SelectGraphQLMapper/nodes/OperationDefinition.ts +++ b/src/layers/3_SelectGraphQLMapper/nodes/OperationDefinition.ts @@ -1,5 +1,4 @@ -import { Nodes } from '../../../lib/graphql-plus/_Nodes.js' -import { getOptionalNullablePropertyOrThrow } from '../../../lib/prelude.js' +import { Nodes } from '../../../lib/grafaid/_Nodes.js' import type { Select } from '../../2_Select/__.js' import type { GraphQLNodeMapper } from '../types.js' import { toGraphQLSelectionSet } from './SelectionSet.js' @@ -12,8 +11,7 @@ export const toGraphQLOperationDefinition: GraphQLNodeMapper< index, operation, ) => { - const type = getOptionalNullablePropertyOrThrow(context.schema.Root, operation.rootType) - const selectionSet = toGraphQLSelectionSet(context, index, type, operation.selectionSet, undefined) + const selectionSet = toGraphQLSelectionSet(context, index, operation.selectionSet, undefined) const name = operation.name ? Nodes.Name({ value: operation.name }) diff --git a/src/layers/3_SelectGraphQLMapper/nodes/SelectionSet.ts b/src/layers/3_SelectGraphQLMapper/nodes/SelectionSet.ts index 6fec0317..b550c6c1 100644 --- a/src/layers/3_SelectGraphQLMapper/nodes/SelectionSet.ts +++ b/src/layers/3_SelectGraphQLMapper/nodes/SelectionSet.ts @@ -1,6 +1,5 @@ -import { Nodes } from '../../../lib/graphql-plus/_Nodes.js' +import { Nodes } from '../../../lib/grafaid/_Nodes.js' import { casesExhausted } from '../../../lib/prelude.js' -import type { Schema } from '../../1_Schema/__.js' import { Select } from '../../2_Select/__.js' import { advanceIndex, type GraphQLNodeMapper } from '../types.js' import { toGraphQLArgument } from './Argument.js' @@ -10,26 +9,22 @@ import { toGraphQLInlineFragment } from './InlineFragment.js' export type SelectionSetContext = { kind: `Field` - type: Schema.SomeField arguments: Nodes.ArgumentNode[] directives: Nodes.DirectiveNode[] } | { kind: `InlineFragment` - type: Schema.Output.ObjectLike directives: Nodes.DirectiveNode[] } export const toGraphQLSelectionSet: GraphQLNodeMapper< Nodes.SelectionSetNode, [ - type: Schema.Output.ObjectLike, selectionSet: Select.SelectionSet.AnySelectionSet, graphqlFieldProperties: SelectionSetContext | undefined, ] > = ( context, index, - type, selectionSet, selectionSetContext, ) => { @@ -46,15 +41,15 @@ export const toGraphQLSelectionSet: GraphQLNodeMapper< if (selectionSetContext.kind === `InlineFragment`) { throw new Error(`Cannot have arguments on an inline fragment.`) } - const index_ = advanceIndex(index, Select.Arguments.key) + // todo import constant from generator for "i" + // ... do NOT use namespace since that would drag generator code into tree-shake output. + const index_ = advanceIndex(index, `i`) for (const argName in keyParsed.arguments) { const argValue = keyParsed.arguments[argName] // We don't do client side validation, let server handle schema errors. - const argType = selectionSetContext.type.args?.fields[argName]?.type const arg = { name: argName, value: argValue, - type: argType, } selectionSetContext.arguments.push(toGraphQLArgument(context, index_, arg)) } @@ -80,14 +75,14 @@ export const toGraphQLSelectionSet: GraphQLNodeMapper< case `InlineFragment`: { for (const selectionSet of keyParsed.selectionSets) { selections.push( - toGraphQLInlineFragment(context, index, type, { typeCondition: keyParsed.typeCondition, selectionSet }), + toGraphQLInlineFragment(context, index, { typeCondition: keyParsed.typeCondition, selectionSet }), ) } continue } case `Alias`: { for (const alias of keyParsed.aliases) { - selections.push(toGraphQLField(context, index, type, { + selections.push(toGraphQLField(context, index, { name: key, alias: alias[0], value: alias[1], @@ -97,7 +92,7 @@ export const toGraphQLSelectionSet: GraphQLNodeMapper< } case `SelectionSet`: { selections.push( - toGraphQLField(context, index, type, { alias: null, name: keyParsed.name, value: keyParsed.selectionSet }), + toGraphQLField(context, index, { alias: null, name: keyParsed.name, value: keyParsed.selectionSet }), ) continue } diff --git a/src/layers/3_SelectGraphQLMapper/nodes/Value.ts b/src/layers/3_SelectGraphQLMapper/nodes/Value.ts index 4b4c50cf..f33ee151 100644 --- a/src/layers/3_SelectGraphQLMapper/nodes/Value.ts +++ b/src/layers/3_SelectGraphQLMapper/nodes/Value.ts @@ -1,5 +1,5 @@ import type { ValueNode } from 'graphql' -import { Nodes } from '../../../lib/graphql-plus/_Nodes.js' +import { Nodes } from '../../../lib/grafaid/_Nodes.js' import { advanceIndex, type CodecString, type GraphQLNodeMapper, isCodec } from '../types.js' export const toGraphQLValue: ValueMapper = (context, index, value) => { diff --git a/src/layers/3_SelectGraphQLMapper/types.ts b/src/layers/3_SelectGraphQLMapper/types.ts index 6d4008c1..0f4db081 100644 --- a/src/layers/3_SelectGraphQLMapper/types.ts +++ b/src/layers/3_SelectGraphQLMapper/types.ts @@ -1,11 +1,10 @@ -import type { Nodes } from '../../lib/graphql-plus/_Nodes.js' +import type { Nodes } from '../../lib/grafaid/_Nodes.js' import type { Codec } from '../1_Schema/Hybrid/types/Scalar/codec.js' import type { Select } from '../2_Select/__.js' -import type { CustomScalarsIndex, SchemaIndex } from '../4_generator/generators/SchemaIndex.js' +import type { CustomScalarsIndex } from '../4_generator/generators/SchemaIndex.js' import type { ValueMapper } from './nodes/Value.js' export interface Context { - schema: SchemaIndex captures: Captures hooks?: { value?: ValueMapper @@ -59,12 +58,21 @@ export type CodecString = Codec export const isCodec = (value: unknown): value is CodecString => typeof value === `object` && value !== null && `encode` in value && typeof value.encode === `function` -type IndexPointer = CustomScalarsIndex | CodecString | null +type RootIndexPointer = + | CustomScalarsIndex + | IndexPointer -export const advanceIndex = (pointer: IndexPointer, key: string) => { +type IndexPointer = + | CustomScalarsIndex + | CustomScalarsIndex.OutputObject + | CustomScalarsIndex.InputObject + | CustomScalarsIndex.InputField + | null + +export const advanceIndex = (pointer: RootIndexPointer, rootTypeOrSomeKey: string): IndexPointer => { if (pointer === null) return pointer if (isCodec(pointer)) return pointer - return pointer[key] ?? null + return (pointer as any)[rootTypeOrSomeKey] ?? null } export type GraphQLNodeMapper< diff --git a/src/layers/4_generator/__snapshots__/generate.test.ts.snap b/src/layers/4_generator/__snapshots__/generate.test.ts.snap index 4456ffb1..abc2b2b9 100644 --- a/src/layers/4_generator/__snapshots__/generate.test.ts.snap +++ b/src/layers/4_generator/__snapshots__/generate.test.ts.snap @@ -366,6 +366,7 @@ export namespace Root { > dateInterface1: $.Field<'dateInterface1', $.Output.Nullable, null> dateList: $.Field<'dateList', $.Output.Nullable<$.Output.List<$Scalar.Date>>, null> + dateListList: $.Field<'dateListList', $.Output.Nullable<$.Output.List<$.Output.List<$Scalar.Date>>>, null> dateListNonNull: $.Field<'dateListNonNull', $.Output.List<$Scalar.Date>, null> dateNonNull: $.Field<'dateNonNull', $Scalar.Date, null> dateObject1: $.Field<'dateObject1', $.Output.Nullable, null> @@ -840,6 +841,7 @@ export const Query = $.Object$(\`Query\`, { // @ts-ignore - circular types cannot infer. Ignore in case there are any. This comment is always added, it does not indicate if this particular type could infer or not. dateInterface1: $.field('dateInterface1', $.Output.Nullable(() => DateInterface1)), dateList: $.field('dateList', $.Output.Nullable($.Output.List($Scalar.Date))), + dateListList: $.field('dateListList', $.Output.Nullable($.Output.List($.Output.List($Scalar.Date)))), dateListNonNull: $.field('dateListNonNull', $.Output.List($Scalar.Date)), dateNonNull: $.field('dateNonNull', $Scalar.Date), // @ts-ignore - circular types cannot infer. Ignore in case there are any. This comment is always added, it does not indicate if this particular type could infer or not. diff --git a/src/layers/4_generator/config.ts b/src/layers/4_generator/config.ts index 8a12ad7b..b1b18d72 100644 --- a/src/layers/4_generator/config.ts +++ b/src/layers/4_generator/config.ts @@ -4,7 +4,7 @@ import fs from 'node:fs/promises' import * as Path from 'node:path' import { Graffle } from '../../entrypoints/__Graffle.js' import { Introspection } from '../../entrypoints/extensions.js' -import { getTypeMapByKind, type TypeMapByKind } from '../../lib/graphql-plus/graphql.js' +import { Nodes } from '../../lib/grafaid/_Nodes.js' import { omitUndefinedKeys } from '../../lib/prelude.js' import { fileExists, isPathToADirectory } from './helpers/fs.js' @@ -74,7 +74,7 @@ export interface Config { schema: { sdl: string instance: GraphQLSchema - typeMapByKind: TypeMapByKind + typeMapByKind: Nodes.$Schema.KindMap.KindMap error: { objects: GraphQLObjectType[] enabled: boolean @@ -132,7 +132,7 @@ export const createConfig = async (input: Input): Promise => { const sourceSchema = await resolveSourceSchema(input) const schema = buildSchema(sourceSchema.content) - const typeMapByKind = getTypeMapByKind(schema) + const typeMapByKind = Nodes.$Schema.KindMap.getKindMap(schema) const errorObjects = errorTypeNamePattern ? Object.values(typeMapByKind.GraphQLObjectType).filter(_ => _.name.match(errorTypeNamePattern)) : [] diff --git a/src/layers/4_generator/generators/MethodsRoot.ts b/src/layers/4_generator/generators/MethodsRoot.ts index 3afc998f..5734b795 100644 --- a/src/layers/4_generator/generators/MethodsRoot.ts +++ b/src/layers/4_generator/generators/MethodsRoot.ts @@ -1,6 +1,6 @@ // todo remove use of Utils.Aug when schema errors not in use import { getNamedType, type GraphQLObjectType, isScalarType } from 'graphql' -import { isAllArgsNullable, RootTypeNameToOperationName } from '../../../lib/graphql-plus/graphql.js' +import { Grafaid } from '../../../lib/grafaid/__.js' import { createModuleGenerator } from '../helpers/moduleGenerator.js' import { createCodeGenerator } from '../helpers/moduleGeneratorRunner.js' import { renderDocumentation, renderName } from '../helpers/render.js' @@ -27,7 +27,8 @@ export const ModuleGeneratorMethodsRoot = createModuleGenerator( export interface BuilderMethodsRoot<$Config extends Utils.Config> { ${ config.schema.typeMapByKind.GraphQLRootType.map(node => { - const operationName = RootTypeNameToOperationName[node.name as keyof typeof RootTypeNameToOperationName] + const operationName = + Grafaid.RootTypeNameToOperationName[node.name as keyof typeof Grafaid.RootTypeNameToOperationName] return `${operationName}: ${node.name}Methods<$Config>` }).join(`\n`) } @@ -85,7 +86,7 @@ const renderFieldMethods = createCodeGenerator<{ node: GraphQLObjectType }>(({ n const fieldTypeUnwrapped = getNamedType(field.type) if (isScalarType(fieldTypeUnwrapped)) { - const isArgsAllNullable_ = isAllArgsNullable(field.args) + const isArgsAllNullable_ = Grafaid.Schema.Args.isAllArgsNullable(field.args) const parametersCode = field.args.length > 0 ? `<$SelectionSet>(args${isArgsAllNullable_ ? `?` : ``}: Utils.Exact<$SelectionSet, SelectionSet.${ renderName(node) diff --git a/src/layers/4_generator/generators/MethodsSelect.ts b/src/layers/4_generator/generators/MethodsSelect.ts index db8fab2b..60704082 100644 --- a/src/layers/4_generator/generators/MethodsSelect.ts +++ b/src/layers/4_generator/generators/MethodsSelect.ts @@ -1,5 +1,5 @@ // todo jsdoc -import { getNodeNameAndKind, isRootType } from '../../../lib/graphql-plus/graphql.js' +import { Grafaid } from '../../../lib/grafaid/__.js' import { createModuleGenerator } from '../helpers/moduleGenerator.js' import { renderName, title1 } from '../helpers/render.js' import { ModuleGeneratorSelectionSets } from './SelectionSets.js' @@ -32,8 +32,8 @@ export const ModuleGeneratorMethodsSelect = createModuleGenerator( code() for (const graphqlTypeGroup of graphqlTypeGroups) { - const { kind } = getNodeNameAndKind(graphqlTypeGroup[0]!) - const titleText = isRootType(graphqlTypeGroup[0]!) ? `Root` : kind + const { kind } = Grafaid.getTypeNameAndKind(graphqlTypeGroup[0]!) + const titleText = Grafaid.Schema.isRootType(graphqlTypeGroup[0]!) ? `Root` : kind code(title1(titleText)) code() diff --git a/src/layers/4_generator/generators/RuntimeIndexCustomScalars.ts b/src/layers/4_generator/generators/RuntimeIndexCustomScalars.ts index a2b66e1c..0105fb4d 100644 --- a/src/layers/4_generator/generators/RuntimeIndexCustomScalars.ts +++ b/src/layers/4_generator/generators/RuntimeIndexCustomScalars.ts @@ -1,11 +1,9 @@ // todo we are going to run into recursion with input types such as two input // objects each having their own custom scalars and also referencing one another. // to solve this we'll need to either use thunks or some kind of indirect look up table? -import { isInputObjectType } from 'graphql' import { Code } from '../../../lib/Code.js' -import { Nodes } from '../../../lib/graphql-plus/graphql.js' +import { Nodes } from '../../../lib/grafaid/graphql.js' import { entries } from '../../../lib/prelude.js' -import { Select } from '../../2_Select/__.js' import { createModuleGenerator } from '../helpers/moduleGenerator.js' import { createCodeGenerator } from '../helpers/moduleGeneratorRunner.js' import { title1 } from '../helpers/render.js' @@ -22,9 +20,11 @@ export const ModuleGeneratorRuntimeCustomScalars = createModuleGenerator( // dprint-ignore const kindsFiltered = { - GraphQLInputObjectType: config.schema.typeMapByKind.GraphQLInputObjectType.filter(Nodes.$Schema.isHasCustomScalarInputs), - GraphQLObjectType: config.schema.typeMapByKind.GraphQLObjectType.filter(Nodes.$Schema.isHasCustomScalarInputs), - GraphQLRootType: config.schema.typeMapByKind.GraphQLRootType.filter(Nodes.$Schema.isHasCustomScalarInputs), + GraphQLInputObjectType: config.schema.typeMapByKind.GraphQLInputObjectType.filter(Nodes.$Schema.CustomScalars.isHasCustomScalars), + GraphQLObjectType: config.schema.typeMapByKind.GraphQLObjectType.filter(Nodes.$Schema.CustomScalars.isHasCustomScalars), + GraphQLInterfaceType: config.schema.typeMapByKind.GraphQLInterfaceType.filter(Nodes.$Schema.CustomScalars.isHasCustomScalars), + GraphQLUnionType: config.schema.typeMapByKind.GraphQLUnionType.filter(Nodes.$Schema.CustomScalars.isHasCustomScalars), + GraphQLRootType: config.schema.typeMapByKind.GraphQLRootType.filter(Nodes.$Schema.CustomScalars.isHasCustomScalars), } for (const [kindName, nodes] of entries(kindsFiltered)) { @@ -61,23 +61,60 @@ export const ModuleGeneratorRuntimeCustomScalars = createModuleGenerator( // // +const UnionType = createCodeGenerator< + { type: Nodes.$Schema.GraphQLUnionType } +>( + ({ code, type }) => { + // This takes advantage of the fact that in GraphQL, in a union type, all members that happen + // to have fields of the same name, those fields MUST be the same type. + // See: + // - https://github.com/graphql/graphql-js/issues/1361 + // - https://stackoverflow.com/questions/44170603/graphql-using-same-field-names-in-different-types-within-union + // + // So what we do is inline all the custom scalar paths of all union members knowing + // that they could never conflict. + code(`const ${type.name} = {`) + for (const memberType of type.getTypes()) { + if (Nodes.$Schema.CustomScalars.isHasCustomScalars(memberType)) { + code(`...${memberType.name},`) + } + } + code(`}`) + }, +) + +const InterfaceType = createCodeGenerator< + { type: Nodes.$Schema.GraphQLInterfaceType } +>( + ({ code, type, config }) => { + const implementorTypes = Nodes.$Schema.KindMap.getInterfaceImplementors(config.schema.typeMapByKind, type) + code(`const ${type.name} = {`) + for (const implementorType of implementorTypes) { + if (Nodes.$Schema.CustomScalars.isHasCustomScalars(implementorType)) { + code(`...${implementorType.name},`) + } + } + code(`}`) + }, +) + const ObjectType = createCodeGenerator<{ type: Nodes.$Schema.GraphQLObjectType }>( ({ code, type }) => { code(`const ${type.name} = {`) - const fields = Object.values(type.getFields()).filter(Nodes.$Schema.isHasCustomScalarInputs) + const fields = Object.values(type.getFields()).filter(Nodes.$Schema.CustomScalars.isHasCustomScalars) for (const field of fields) { code(Code.termField(field.name, `{`, { comma: false })) // Field Arguments - const args = field.args.filter(Nodes.$Schema.isHasCustomScalarInputs) + const args = field.args.filter(Nodes.$Schema.CustomScalars.isHasCustomScalarInputs) if (args.length > 0) { - code(Code.termField(Select.Arguments.key, `{`, { comma: false })) + code(Code.termField(`i`, `{`, { comma: false })) for (const arg of args) { const argType = Nodes.getNamedType(arg.type) if (Nodes.$Schema.isScalarTypeAndCustom(argType)) { code(Code.termField(arg.name, `${identifiers.$CustomScalars}.${argType.name}.codec`)) - } else if (isInputObjectType(argType)) { + } else if (Nodes.$Schema.isInputObjectType(argType)) { code(Code.termField(arg.name, argType.name)) } else { throw new Error(`Failed to complete index for argument ${arg.name} of ${argType.toString()}`) @@ -86,13 +123,25 @@ const ObjectType = createCodeGenerator<{ type: Nodes.$Schema.GraphQLObjectType } code(`},`) } - // todo make kitchen sink schema have a pattern where this code path will be traversed. - // We just need to have arguments on a field on a nested object. - // Nested objects that in turn have custom scalar arguments const fieldType = Nodes.getNamedType(field.type) - if (Nodes.$Schema.isObjectType(fieldType) && Nodes.$Schema.isHasCustomScalarInputs(fieldType)) { - code(Code.termField(field.name, fieldType.name)) + + if (Nodes.$Schema.CustomScalars.isHasCustomScalars(fieldType)) { + if (Nodes.$Schema.isScalarTypeAndCustom(fieldType)) { + code(Code.termField(`o`, `${identifiers.$CustomScalars}.${fieldType.name}.codec`)) + } else if ( + Nodes.$Schema.isUnionType(fieldType) || Nodes.$Schema.isObjectType(fieldType) + || Nodes.$Schema.isInterfaceType(fieldType) + ) { + code(Code.termField(`r`, fieldType.name)) + // // todo make kitchen sink schema have a pattern where this code path will be traversed. + // // We just need to have arguments on a field on a nested object. + // // Nested objects that in turn have custom scalar arguments + // if (Nodes.$Schema.isObjectType(fieldType) && Nodes.$Schema.isHasCustomScalars(fieldType)) { + // code(Code.termField(field.name, fieldType.name)) + // } + } } + code(`},`) } code(`}`) @@ -107,7 +156,7 @@ const InputObjectType = createCodeGenerator<{ type: Nodes.$Schema.GraphQLInputOb const type = Nodes.getNamedType(field.type) if (Nodes.$Schema.isScalarTypeAndCustom(type)) { code(Code.termField(field.name, `${identifiers.$CustomScalars}.${type.name}.codec`)) - } else if (isInputObjectType(type) && Nodes.$Schema.isHasCustomScalarInputs(type)) { + } else if (Nodes.$Schema.isInputObjectType(type) && Nodes.$Schema.CustomScalars.isHasCustomScalarInputs(type)) { code(Code.termField(field.name, type.name)) } } @@ -117,6 +166,8 @@ const InputObjectType = createCodeGenerator<{ type: Nodes.$Schema.GraphQLInputOb ) const kindRenders = { + GraphQLUnionType: UnionType, + GraphQLInterfaceType: InterfaceType, GraphQLInputObjectType: InputObjectType, GraphQLObjectType: ObjectType, GraphQLRootType: ObjectType, diff --git a/src/layers/4_generator/generators/Scalar.ts b/src/layers/4_generator/generators/Scalar.ts index 7e8a6fb2..95200c1c 100644 --- a/src/layers/4_generator/generators/Scalar.ts +++ b/src/layers/4_generator/generators/Scalar.ts @@ -1,4 +1,4 @@ -import { hasCustomScalars } from '../../../lib/graphql-plus/graphql.js' +import { Grafaid } from '../../../lib/grafaid/__.js' import { createModuleGenerator } from '../helpers/moduleGenerator.js' import { typeTitle2 } from '../helpers/render.js' @@ -6,7 +6,7 @@ export const ModuleGeneratorScalar = createModuleGenerator( `Scalar`, ({ config, code }) => { // todo test case for when this is true - const needsDefaultCustomScalarImplementation = hasCustomScalars(config.schema.typeMapByKind) + const needsDefaultCustomScalarImplementation = Grafaid.Schema.KindMap.hasCustomScalars(config.schema.typeMapByKind) && !config.options.customScalars const CustomScalarsNamespace = `CustomScalars` @@ -16,7 +16,7 @@ export const ModuleGeneratorScalar = createModuleGenerator( code(`import * as ${StandardScalarNamespace} from '${config.paths.imports.grafflePackage.scalars}'`) } - if (hasCustomScalars(config.schema.typeMapByKind)) { + if (Grafaid.Schema.KindMap.hasCustomScalars(config.schema.typeMapByKind)) { code(`import type { Schema } from '${config.paths.imports.grafflePackage.schema}'`) code(`import * as ${CustomScalarsNamespace} from '${config.paths.imports.customScalarCodecs}'`) code() diff --git a/src/layers/4_generator/generators/SchemaBuildtime.ts b/src/layers/4_generator/generators/SchemaBuildtime.ts index b8d7f152..911a0866 100644 --- a/src/layers/4_generator/generators/SchemaBuildtime.ts +++ b/src/layers/4_generator/generators/SchemaBuildtime.ts @@ -7,25 +7,19 @@ import type { } from 'graphql' import { getNullableType, isListType, isNamedType, isNullableType } from 'graphql' import { Code } from '../../../lib/Code.js' -import type { - AnyClass, - AnyField, - AnyNamedClassName, - ClassToName, - NamedNameToClass, - NameToClassNamedType, -} from '../../../lib/graphql-plus/graphql.js' +import { Grafaid } from '../../../lib/grafaid/__.js' +import type { Nodes } from '../../../lib/grafaid/graphql.js' import { - isAllArgsNullable, - isAllInputObjectFieldsNullable, - isGraphQLOutputField, - type NameToClass, - RootTypeName, -} from '../../../lib/graphql-plus/graphql.js' + type AnyClass, + type AnyField, + type AnyNamedClassName, + type ClassToName, + type NamedNameToClass, +} from '../../../lib/grafaid/graphql.js' import { entries, values } from '../../../lib/prelude.js' import type { Config } from '../config.js' import { createModuleGenerator } from '../helpers/moduleGenerator.js' -import { getDocumentation, getInterfaceImplementors } from '../helpers/render.js' +import { getDocumentation } from '../helpers/render.js' import { ModuleGeneratorScalar } from './Scalar.js' const namespaceNames = { @@ -55,18 +49,19 @@ const defineReferenceRenderers = < ) => renderers const defineConcreteRenderers = < - $Renderers extends { [ClassName in keyof NameToClassNamedType]: any }, + $Renderers extends { [ClassName in keyof Nodes.$Schema.NameToClassNamedType]: any }, >( renderers: { [ClassName in keyof $Renderers]: ( config: Config, - node: ClassName extends keyof NameToClassNamedType ? InstanceType + node: ClassName extends keyof Nodes.$Schema.NameToClassNamedType + ? InstanceType : never, ) => string }, ): { [ClassName in keyof $Renderers]: ( - node: ClassName extends keyof NameToClass ? InstanceType | null | undefined + node: ClassName extends keyof Grafaid.NameToClass ? InstanceType | null | undefined : never, ) => string } => { @@ -133,7 +128,7 @@ const concreteRenderers = defineConcreteRenderers({ ), GraphQLInputObjectType: (config, node) => { const doc = getDocumentation(config, node) - const isAllFieldsNullable = isAllInputObjectFieldsNullable(node) + const isAllFieldsNullable = Grafaid.Schema.isAllInputObjectFieldsNullable(node) const source = Code.export$( Code.type( node.name, @@ -145,7 +140,7 @@ const concreteRenderers = defineConcreteRenderers({ return Code.TSDocWithBlock(doc, source) }, GraphQLInterfaceType: (config, node) => { - const implementors = getInterfaceImplementors(config.schema.typeMapByKind, node) + const implementors = Grafaid.Schema.KindMap.getInterfaceImplementors(config.schema.typeMapByKind, node) return Code.TSDocWithBlock( getDocumentation(config, node), Code.export$(Code.type( @@ -157,7 +152,7 @@ const concreteRenderers = defineConcreteRenderers({ ) }, GraphQLObjectType: (config, node) => { - const maybeRootTypeName = (RootTypeName as Record)[node.name] + const maybeRootTypeName = (Grafaid.Schema.RootTypeName as Record)[node.name] const type = maybeRootTypeName ? `$.Output.Object${maybeRootTypeName}<${renderOutputFields(config, node)}>` : `$.Object$2<${Code.string(node.name)}, ${renderOutputFields(config, node)}>` @@ -211,7 +206,7 @@ const renderInputFields = (config: Config, node: AnyGraphQLFieldsType): string = const renderOutputField = (config: Config, field: AnyField): string => { const type = buildType(`output`, config, field.type) - const args = isGraphQLOutputField(field) && field.args.length > 0 + const args = Grafaid.Schema.isGraphQLOutputField(field) && field.args.length > 0 ? renderArgs(config, field.args) : null @@ -253,7 +248,7 @@ const renderArgs = (config: Config, args: readonly GraphQLArgument[]) => { args.map((arg) => renderArg(config, arg)), ), ) - }, ${Code.boolean(isAllArgsNullable(args))}>` + }, ${Code.boolean(Grafaid.Schema.Args.isAllArgsNullable(args))}>` return code } diff --git a/src/layers/4_generator/generators/SchemaIndex.ts b/src/layers/4_generator/generators/SchemaIndex.ts index 722d1348..9e9952a8 100644 --- a/src/layers/4_generator/generators/SchemaIndex.ts +++ b/src/layers/4_generator/generators/SchemaIndex.ts @@ -1,6 +1,6 @@ import { getNamedType, isUnionType } from 'graphql' import { Code } from '../../../lib/Code.js' -import { hasMutation, hasQuery, hasSubscription } from '../../../lib/graphql-plus/graphql.js' +import { Grafaid } from '../../../lib/grafaid/__.js' import type { Schema } from '../../1_Schema/__.js' import type { CodecString } from '../../3_SelectGraphQLMapper/types.js' import type { GlobalRegistry } from '../globalRegistry.js' @@ -9,8 +9,28 @@ import { ModuleGeneratorData } from './Data.js' import { ModuleGeneratorMethodsRoot } from './MethodsRoot.js' import { ModuleGeneratorSchemaBuildtime } from './SchemaBuildtime.js' -export type CustomScalarsIndex = { - [key: string]: CodecString | CustomScalarsIndex +export interface CustomScalarsIndex { + [Grafaid.Schema.RootTypeName.Mutation]?: CustomScalarsIndex.OutputObject + [Grafaid.Schema.RootTypeName.Query]?: CustomScalarsIndex.OutputObject + [Grafaid.Schema.RootTypeName.Subscription]?: CustomScalarsIndex.OutputObject +} + +export namespace CustomScalarsIndex { + export interface OutputObject { + [key: string]: OutputField + } + + export interface OutputField { + o?: CodecString + i?: InputObject + r?: OutputObject + } + + export interface InputObject { + [key: string]: InputField + } + + export type InputField = CodecString | InputObject } /** @@ -72,9 +92,9 @@ export const ModuleGeneratorSchemaIndex = createModuleGenerator( code() const rootTypesPresence = { - Query: hasQuery(config.schema.typeMapByKind), - Mutation: hasMutation(config.schema.typeMapByKind), - Subscription: hasSubscription(config.schema.typeMapByKind), + Query: Grafaid.Schema.KindMap.hasQuery(config.schema.typeMapByKind), + Mutation: Grafaid.Schema.KindMap.hasMutation(config.schema.typeMapByKind), + Subscription: Grafaid.Schema.KindMap.hasSubscription(config.schema.typeMapByKind), } const root = config.schema.typeMapByKind.GraphQLRootType.map(_ => @@ -133,9 +153,9 @@ export const ModuleGeneratorSchemaIndex = createModuleGenerator( config.schema.error.objects.map(_ => [_.name, `{ __typename: "${_.name}" }`]), ), rootResultFields: `{ - ${!hasQuery(config.schema.typeMapByKind) ? `Query: {}` : ``} - ${!hasMutation(config.schema.typeMapByKind) ? `Mutation: {}` : ``} - ${!hasSubscription(config.schema.typeMapByKind) ? `Subscription: {}` : ``} + ${!Grafaid.Schema.KindMap.hasQuery(config.schema.typeMapByKind) ? `Query: {}` : ``} + ${!Grafaid.Schema.KindMap.hasMutation(config.schema.typeMapByKind) ? `Mutation: {}` : ``} + ${!Grafaid.Schema.KindMap.hasSubscription(config.schema.typeMapByKind) ? `Subscription: {}` : ``} ${ Object.values(config.schema.typeMapByKind.GraphQLRootType).map((rootType) => { const resultFields = Object.values(rootType.getFields()).filter((field) => { diff --git a/src/layers/4_generator/generators/SchemaRuntime.ts b/src/layers/4_generator/generators/SchemaRuntime.ts index a7386fc2..18ca5c48 100644 --- a/src/layers/4_generator/generators/SchemaRuntime.ts +++ b/src/layers/4_generator/generators/SchemaRuntime.ts @@ -25,14 +25,8 @@ import { isUnionType, } from 'graphql' import { Code } from '../../../lib/Code.js' -import type { AnyClass, AnyGraphQLOutputField } from '../../../lib/graphql-plus/graphql.js' -import { - hasMutation, - hasQuery, - hasSubscription, - isAllArgsNullable, - isAllInputObjectFieldsNullable, -} from '../../../lib/graphql-plus/graphql.js' +import { Grafaid } from '../../../lib/grafaid/__.js' +import type { AnyClass, AnyGraphQLOutputField } from '../../../lib/grafaid/graphql.js' import type { Config } from '../config.js' import { createModuleGenerator } from '../helpers/moduleGenerator.js' import { ModuleGeneratorData } from './Data.js' @@ -83,9 +77,9 @@ export const ModuleGeneratorSchemaRuntime = createModuleGenerator( const index = (config: Config) => { const rootTypesPresence = { - Query: hasQuery(config.schema.typeMapByKind), - Mutation: hasMutation(config.schema.typeMapByKind), - Subscription: hasSubscription(config.schema.typeMapByKind), + Query: Grafaid.Schema.KindMap.hasQuery(config.schema.typeMapByKind), + Mutation: Grafaid.Schema.KindMap.hasMutation(config.schema.typeMapByKind), + Subscription: Grafaid.Schema.KindMap.hasSubscription(config.schema.typeMapByKind), } // todo input objects for decode/encode input object fields const unions = config.schema.typeMapByKind.GraphQLUnionType.map(type => type.name).join(`,\n`) @@ -136,9 +130,9 @@ const index = (config: Config) => { ${config.schema.error.objects.map(_ => `${_.name}: { __typename: "${_.name}" }`).join(`,\n`)} }, rootResultFields: { - ${!hasQuery(config.schema.typeMapByKind) ? `Query: {},` : ``} - ${!hasMutation(config.schema.typeMapByKind) ? `Mutation: {},` : ``} - ${!hasSubscription(config.schema.typeMapByKind) ? `Subscription: {},` : ``} + ${!Grafaid.Schema.KindMap.hasQuery(config.schema.typeMapByKind) ? `Query: {},` : ``} + ${!Grafaid.Schema.KindMap.hasMutation(config.schema.typeMapByKind) ? `Mutation: {},` : ``} + ${!Grafaid.Schema.KindMap.hasSubscription(config.schema.typeMapByKind) ? `Subscription: {},` : ``} ${ Object.values(config.schema.typeMapByKind.GraphQLRootType).map((rootType) => { const resultFields = Object.values(rootType.getFields()).filter((field) => { @@ -202,7 +196,7 @@ const object = (config: Config, type: GraphQLObjectType) => { } const inputObject = (config: Config, type: GraphQLInputObjectType) => { - const isFieldsAllNullable = isAllInputObjectFieldsNullable(type) + const isFieldsAllNullable = Grafaid.Schema.isAllInputObjectFieldsNullable(type) const fields = Object.values(type.getFields()).map((field) => `${field.name}: ${inputField(config, field)}`).join( `,\n`, ) @@ -227,7 +221,7 @@ const outputField = (config: Config, field: AnyGraphQLOutputField): string => { } const renderArgs = (config: Config, args: readonly GraphQLArgument[]) => { - const isFieldsAllNullable = isAllArgsNullable(args) + const isFieldsAllNullable = Grafaid.Schema.Args.isAllArgsNullable(args) return `$.Args({${args.map(arg => renderArg(config, arg)).join(`, `)}}, ${Code.boolean(isFieldsAllNullable)})` } diff --git a/src/layers/4_generator/generators/SelectionSets.ts b/src/layers/4_generator/generators/SelectionSets.ts index 20b1da7e..c1ace451 100644 --- a/src/layers/4_generator/generators/SelectionSets.ts +++ b/src/layers/4_generator/generators/SelectionSets.ts @@ -19,29 +19,19 @@ import { } from 'graphql' import { getNamedType, isNullableType, isScalarType } from 'graphql' import { Code } from '../../../lib/Code.js' +import { Grafaid } from '../../../lib/grafaid/__.js' import { - analyzeArgsNullability, - getNodeKind, - getNodeNameAndKind, - hasCustomScalars, - hasMutation, - hasQuery, + getTypeNameAndKind, Nodes, - RootTypeName, + type StandardScalarTypeNames, StandardScalarTypeTypeScriptMapping, -} from '../../../lib/graphql-plus/graphql.js' -import type { StandardScalarTypeNames } from '../../../lib/graphql-plus/graphql.js' +} from '../../../lib/grafaid/graphql.js' +import { analyzeArgsNullability } from '../../../lib/grafaid/schema/args.js' +import { RootTypeName } from '../../../lib/grafaid/schema/schema.js' import { Select } from '../../2_Select/__.js' import { createModuleGenerator } from '../helpers/moduleGenerator.js' import { createCodeGenerator } from '../helpers/moduleGeneratorRunner.js' -import { - getDocumentation, - getInterfaceImplementors, - renderDocumentation, - renderName, - title1, - typeTitle2SelectionSet, -} from '../helpers/render.js' +import { getDocumentation, renderDocumentation, renderName, title1, typeTitle2SelectionSet } from '../helpers/render.js' import { ModuleGeneratorScalar } from './Scalar.js' export const ModuleGeneratorSelectionSets = createModuleGenerator( @@ -51,7 +41,7 @@ export const ModuleGeneratorSelectionSets = createModuleGenerator( code(`import type { Select as $Select } from '${config.paths.imports.grafflePackage.schema}'`) code(`import type * as $Utilities from '${config.paths.imports.grafflePackage.utilitiesForGenerated}'`) - if (hasCustomScalars(config.schema.typeMapByKind)) { + if (Grafaid.Schema.KindMap.hasCustomScalars(config.schema.typeMapByKind)) { code(`import type * as $Scalar from './${ModuleGeneratorScalar.name}.js'`) } code() @@ -63,8 +53,8 @@ export const ModuleGeneratorSelectionSets = createModuleGenerator( ) code( `export interface $Document {`, - hasQuery(config.schema.typeMapByKind) ? `query?: Record` : null, - hasMutation(config.schema.typeMapByKind) ? `mutation?: Record` : null, + Grafaid.Schema.KindMap.hasQuery(config.schema.typeMapByKind) ? `query?: Record` : null, + Grafaid.Schema.KindMap.hasMutation(config.schema.typeMapByKind) ? `mutation?: Record` : null, `}`, ) code() @@ -79,7 +69,7 @@ export const ModuleGeneratorSelectionSets = createModuleGenerator( ].filter(_ => _.length > 0) typesToRender.forEach((nodes) => { - const kind = getNodeKind(nodes[0]!) + const kind = Grafaid.getTypeKind(nodes[0]!) code(title1(`${kind} Types`)) code() @@ -170,7 +160,7 @@ const renderInterface = createCodeGenerator<{ node: GraphQLInterfaceType }>( const fieldsRendered = fields.map(field => { return Helpers.outputField(field.name, `${renderName(node)}.${renderName(field)}`) }).join(`\n`) - const implementorTypes = getInterfaceImplementors(config.schema.typeMapByKind, node) + const implementorTypes = Nodes.$Schema.KindMap.getInterfaceImplementors(config.schema.typeMapByKind, node) const onTypesRendered = implementorTypes.map(type => Helpers.outputField(`${Select.InlineFragment.typeConditionPRefix}${type.name}`, renderName(type)) ).join( @@ -219,7 +209,7 @@ const renderObject = createCodeGenerator<{ node: GraphQLObjectType }>( code() const propertiesRendered = fields.map(field => { - const nodeWhat = getNodeNameAndKind(getNamedType(field.type)) + const nodeWhat = getTypeNameAndKind(getNamedType(field.type)) const type = nodeWhat.kind === `Scalar` ? `\`${nodeWhat.name}\` (a \`Scalar\`)` : nodeWhat.kind const doc = Code.TSDoc(` Select the \`${field.name}\` field on the \`${node.name}\` object. Its type is ${type}. diff --git a/src/layers/4_generator/generators/global.ts b/src/layers/4_generator/generators/global.ts index 02025d81..7cca8f83 100644 --- a/src/layers/4_generator/generators/global.ts +++ b/src/layers/4_generator/generators/global.ts @@ -1,5 +1,5 @@ import { Code } from '../../../lib/Code.js' -import { hasCustomScalars } from '../../../lib/graphql-plus/graphql.js' +import { Grafaid } from '../../../lib/grafaid/__.js' import { createModuleGenerator } from '../helpers/moduleGenerator.js' import { ModuleGeneratorData } from './Data.js' import { ModuleGeneratorMethodsDocument } from './MethodsDocument.js' @@ -12,7 +12,7 @@ export const ModuleGeneratorGlobal = createModuleGenerator( `Global`, ({ config, code }) => { const StandardScalarNamespace = `StandardScalar` - const needsDefaultCustomScalarImplementation = hasCustomScalars(config.schema.typeMapByKind) + const needsDefaultCustomScalarImplementation = Grafaid.Schema.KindMap.hasCustomScalars(config.schema.typeMapByKind) && !config.options.customScalars code( diff --git a/src/layers/4_generator/helpers/render.ts b/src/layers/4_generator/helpers/render.ts index f24418ff..a1651620 100644 --- a/src/layers/4_generator/helpers/render.ts +++ b/src/layers/4_generator/helpers/render.ts @@ -1,14 +1,12 @@ -import type { GraphQLInterfaceType } from 'graphql' import { type GraphQLEnumValue, type GraphQLField, type GraphQLNamedType, isEnumType } from 'graphql' import { Code } from '../../../lib/Code.js' import { type Describable, getNodeDisplayName, - getNodeNameAndKind, + getTypeNameAndKind, isDeprecatableNode, - type TypeMapByKind, type TypeMapKind, -} from '../../../lib/graphql-plus/graphql.js' +} from '../../../lib/grafaid/graphql.js' import { borderThickFullWidth, borderThinFullWidth, centerTo } from '../../../lib/text.js' import type { Config } from '../config.js' @@ -34,7 +32,7 @@ export const title1 = (title: string) => { } export const typeTitle2 = (category: string) => (node: GraphQLNamedType) => { - const nameKind = getNodeNameAndKind(node) + const nameKind = getTypeNameAndKind(node) const nameOrKind = nameKind.kind === `Scalar` ? nameKind.name : nameKind.kind const typeLabel = nameOrKind const title = ` @@ -131,9 +129,3 @@ export const renderName = (type: GraphQLNamedType | GraphQLField) => { } return type.name } - -export const getInterfaceImplementors = (typeMap: TypeMapByKind, interfaceTypeSearch: GraphQLInterfaceType) => { - return typeMap.GraphQLObjectType.filter(objectType => - objectType.getInterfaces().filter(interfaceType => interfaceType.name === interfaceTypeSearch.name).length > 0 - ) -} diff --git a/src/layers/5_request/core.ts b/src/layers/5_request/core.ts index bcbde1b6..71f25962 100644 --- a/src/layers/5_request/core.ts +++ b/src/layers/5_request/core.ts @@ -1,24 +1,22 @@ -import { type ExecutionResult, parse, print } from 'graphql' +import { type ExecutionResult, parse } from 'graphql' import { Anyware } from '../../lib/anyware/__.js' +import type { Grafaid } from '../../lib/grafaid/__.js' +import { OperationTypeToAccessKind, print } from '../../lib/grafaid/document.js' +import { execute } from '../../lib/grafaid/execute.js' +import type { Nodes } from '../../lib/grafaid/graphql.js' +import { parseOperationType, type Variables } from '../../lib/grafaid/graphql.js' import { getRequestEncodeSearchParameters, getRequestHeadersRec, parseExecutionResult, postRequestEncodeBody, postRequestHeadersRec, -} from '../../lib/graphql-http/graphqlHTTP.js' -import { execute } from '../../lib/graphql-plus/execute.js' -import type { Nodes } from '../../lib/graphql-plus/graphql.js' -import { - OperationTypeAccessTypeMap, - parseGraphQLOperationType, - type Variables, -} from '../../lib/graphql-plus/graphql.js' +} from '../../lib/grafaid/http/http.js' +import type { TypedDocument } from '../../lib/grafaid/typed-document/__.js' import { mergeRequestInit, searchParamsAppendAll } from '../../lib/http.js' -import { casesExhausted, getOptionalNullablePropertyOrThrow, isString, throwNull } from '../../lib/prelude.js' -import { Select } from '../2_Select/__.js' -import { ResultSet } from '../3_Result/__.js' +import { casesExhausted, isString, throwNull } from '../../lib/prelude.js' import { SelectionSetGraphqlMapper } from '../3_SelectGraphQLMapper/__.js' +import { decode } from '../6_client/customScalar/decode.js' import type { GraffleExecutionResultVar } from '../6_client/handleOutput.js' import type { Config } from '../6_client/Settings/Config.js' import { MethodMode, type MethodModeGetReads } from '../6_client/transportHttp/request.js' @@ -44,39 +42,34 @@ export const anyware = Anyware.create({ hookNamesOrderedBySequence, hooks: { encode: ({ input }) => { - let documentString: string + let document: string | TypedDocument.TypedDocument // todo: the other case where we're going to need to parse document is for custom scalar support of raw const isWillInjectTypename = input.state.config.output.errors.schema && input.schemaIndex if (isWillInjectTypename) { - const documentObject: Nodes.DocumentNode = input.interfaceType === `raw` + document = input.interfaceType === `raw` ? isString(input.request.query) ? parse(input.request.query) : input.request.query as Nodes.DocumentNode : SelectionSetGraphqlMapper.toGraphQL({ - schema: input.schemaIndex!, document: input.request.document, customScalarsIndex: input.schemaIndex!.customScalars.input, }) injectTypenameOnRootResultFields({ - document: documentObject, + document, operationName: input.request.operationName, schema: input.schemaIndex!, }) - - documentString = print(documentObject) } else { - documentString = input.interfaceType === `raw` - ? isString(input.request.query) - ? input.request.query - : print(input.request.query as Nodes.DocumentNode) - : print(SelectionSetGraphqlMapper.toGraphQL({ - schema: input.schemaIndex!, + document = input.interfaceType === `raw` + ? input.request.query + : SelectionSetGraphqlMapper.toGraphQL({ + // schema: input.schemaIndex!, document: input.request.document, customScalarsIndex: input.schemaIndex!.customScalars.input, - })) + }) } const variables: Variables | undefined = input.interfaceType === `raw` @@ -87,7 +80,7 @@ export const anyware = Anyware.create({ return { ...input, request: { - query: documentString, + query: document, variables, operationName: input.request.operationName, }, @@ -99,17 +92,18 @@ export const anyware = Anyware.create({ body: postRequestEncodeBody, }, run: ({ input, slots }) => { + const graphqlRequest: Grafaid.HTTP.RequestConfig = { + ...input.request, + query: print(input.request.query), + } + // TODO thrown error here is swallowed in examples. switch (input.transportType) { case `memory`: { - return input - // return { - // ...input, - // request: { - // ...input.request, - // schema: input.schema, - // }, - // } + return { + ...input, + request: graphqlRequest, + } } case `http`: { if (input.state.config.transport.type !== Transport.http) throw new Error(`transport type is not http`) @@ -119,11 +113,11 @@ export const anyware = Anyware.create({ // 1. If using TS interface then work with initially submitted structured data to already know the operation type // 2. Maybe: Memoize over request.{ operationName, query } // 3. Maybe: Keep a cache of parsed request.{ query } - const operationType = throwNull(parseGraphQLOperationType(input.request)) // todo better feedback here than throwNull + const operationType = throwNull(parseOperationType(input.request)) // todo better feedback here than throwNull const requestMethod = methodMode === MethodMode.post ? `post` : methodMode === MethodMode.getReads // eslint-disable-line - ? OperationTypeAccessTypeMap[operationType] === `read` ? `get` : `post` + ? OperationTypeToAccessKind[operationType] === `read` ? `get` : `post` : casesExhausted(methodMode) const baseProperties = mergeRequestInit( @@ -148,14 +142,14 @@ export const anyware = Anyware.create({ methodMode: methodMode as MethodModeGetReads, ...baseProperties, method: `get`, - url: searchParamsAppendAll(input.url, slots.searchParams(input.request)), + url: searchParamsAppendAll(input.url, slots.searchParams(graphqlRequest)), } : { methodMode: methodMode, ...baseProperties, method: `post`, url: input.url, - body: slots.body(input.request), + body: slots.body(graphqlRequest), } return { ...input, @@ -218,13 +212,21 @@ export const anyware = Anyware.create({ throw casesExhausted(input) } }, - // todo - // Given that we manipulate the selection set in encode, and given decode relies on the sent selection set - // it follows that the decode hook depends on the output of the encode hook. that means we need to plumb - // through the hooks that data built during encode. Yet encode doesn't output it currently, but rather prints it. - // Hooks could have a new optional field "schema". When present certain enhanced features would be allowed. - // like custom scalars and result fields. - decode: ({ input }) => { + decode: ({ input, previous }) => { + const data = input.result.data + + // If there has been an error and we definitely don't have any data, such as when + // giving an operation name that doesn't match any in the document, + // then don't attempt to decode. + const isError = !input.result.data && (input.result.errors?.length ?? 0) > 0 + if (input.schemaIndex && !isError) { + decode({ + data, + customScalarsIndex: input.schemaIndex.customScalars.input, // todo drop input/output separation + request: previous.pack.input.request, + }) + } + switch (input.interfaceType) { // todo this depends on the return mode case `raw`: { @@ -245,23 +247,18 @@ export const anyware = Anyware.create({ case `typed`: { if (!input.schemaIndex) throw new Error(`schemaIndex is required for typed decode`) - const operation = Select.Document.getOperationOrThrow(input.document!, input.operationName) // todo optimize // 1. Generate a map of possible custom scalar paths (tree structure) // 2. When traversing the result, skip keys that are not in the map // console.log(input.context.schemaIndex.Root) // console.log(getOptionalNullablePropertyOrThrow(input.context.schemaIndex.Root, operation.rootType)) - const dataDecoded = ResultSet.decode( - getOptionalNullablePropertyOrThrow(input.schemaIndex.Root, operation.rootType), - operation.selectionSet, - input.result.data, - ) + switch (input.transportType) { case `memory`: { - return { ...input.result, data: dataDecoded } + return { ...input.result, data } } case `http`: { - return { ...input.result, data: dataDecoded, response: input.response } + return { ...input.result, data, response: input.response } } default: throw casesExhausted(input) diff --git a/src/layers/5_request/hooks.ts b/src/layers/5_request/hooks.ts index 94e33736..48fd85eb 100644 --- a/src/layers/5_request/hooks.ts +++ b/src/layers/5_request/hooks.ts @@ -1,7 +1,6 @@ import type { ExecutionResult, GraphQLSchema } from 'graphql' -import type { GraphQLHTTP } from '../../lib/graphql-http/__.js' -import type { getRequestEncodeSearchParameters, postRequestEncodeBody } from '../../lib/graphql-http/graphqlHTTP.js' -import type { GraphQLRequestInput } from '../../lib/graphql-plus/graphql.js' +import type { Grafaid } from '../../lib/grafaid/__.js' +import type { getRequestEncodeSearchParameters, postRequestEncodeBody } from '../../lib/grafaid/http/http.js' import type { httpMethodGet, httpMethodPost } from '../../lib/http.js' import type { Select } from '../2_Select/__.js' import type { SchemaIndex } from '../4_generator/generators/SchemaIndex.js' @@ -51,7 +50,7 @@ export type HookDefEncode<$Config extends Config> = { & HookInputBase & InterfaceInput< { request: { document: Select.Document.DocumentNormalized; operationName?: string } }, - { request: GraphQLRequestInput } + { request: Grafaid.RequestInput } > & TransportInput<$Config> } @@ -65,7 +64,7 @@ export type HookDefPack<$Config extends Config> = { // todo why is headers here but not other http request properties? { headers?: HeadersInit } > - & { request: GraphQLHTTP.RequestInput } + & { request: Grafaid.RequestInput } slots: { /** * When request will be sent using GET this slot is called to create the value that will be used for the HTTP Search Parameters. @@ -87,8 +86,8 @@ export type HookDefExchange<$Config extends Config> = { & InterfaceInput & TransportInput< $Config, - { request: CoreExchangePostRequest | CoreExchangeGetRequest }, - { request: GraphQLHTTP.RequestInput } + { request: CoreExchangePostRequest | CoreExchangeGetRequest; headers?: HeadersInit }, + { request: Grafaid.HTTP.RequestConfig } > } diff --git a/src/layers/5_request/schemaErrors.test.ts b/src/layers/5_request/schemaErrors.test.ts index 35e2a59c..b43d6cec 100644 --- a/src/layers/5_request/schemaErrors.test.ts +++ b/src/layers/5_request/schemaErrors.test.ts @@ -22,11 +22,9 @@ test.each([ [`root field alias `, { resultNonNull: [`x`, {}] }, { resultNonNull: [`x`, { __typename: true }] }], ])(`Query %s`, (_, queryWithoutTypenameInput, queryWithTypenameInput) => { const documentWithTypename = SelectionSetGraphqlMapper.toGraphQL({ - schema, document: Select.Document.normalizeOrThrow({ query: { x: queryWithTypenameInput as any } }) }) const documentWithoutTypename = SelectionSetGraphqlMapper.toGraphQL({ - schema, document: Select.Document.normalizeOrThrow({ query: { x: queryWithoutTypenameInput as any } }) }) injectTypenameOnRootResultFields({ diff --git a/src/layers/5_request/schemaErrors.ts b/src/layers/5_request/schemaErrors.ts index 6c955ba4..d73c0dbc 100644 --- a/src/layers/5_request/schemaErrors.ts +++ b/src/layers/5_request/schemaErrors.ts @@ -1,4 +1,5 @@ -import { Nodes, operationTypeNameToRootTypeName, type RootTypeName } from '../../lib/graphql-plus/graphql.js' +import type { Grafaid } from '../../lib/grafaid/__.js' +import { Nodes, operationTypeNameToRootTypeName } from '../../lib/grafaid/graphql.js' import type { SchemaIndex } from '../4_generator/generators/SchemaIndex.js' export const injectTypenameOnRootResultFields = ( @@ -26,7 +27,7 @@ export const injectTypenameOnRootResultFields = ( const injectTypenameOnRootResultFields_ = ( { selectionSet, schema, rootTypeName }: { schema: SchemaIndex - rootTypeName: RootTypeName + rootTypeName: Grafaid.Schema.RootTypeName selectionSet: Nodes.SelectionSetNode }, ): void => { diff --git a/src/layers/6_client/Settings/client.create.config.output.test-d.ts b/src/layers/6_client/Settings/client.create.config.output.test-d.ts index 8d12df06..57dde711 100644 --- a/src/layers/6_client/Settings/client.create.config.output.test-d.ts +++ b/src/layers/6_client/Settings/client.create.config.output.test-d.ts @@ -5,7 +5,7 @@ import { expectTypeOf, test } from 'vitest' import { Graffle } from '../../../../tests/_/schemas/kitchen-sink/graffle/__.js' import { schema } from '../../../../tests/_/schemas/kitchen-sink/schema.js' import { AssertEqual } from '../../../lib/assert-equal.js' -import { type GraphQLExecutionResultError } from '../../../lib/graphql-plus/graphql.js' +import { type GraphQLExecutionResultError } from '../../../lib/grafaid/graphql.js' import type { Envelope, ErrorsOther } from '../handleOutput.js' const G = Graffle.create diff --git a/src/layers/6_client/client.customScalar.test.ts b/src/layers/6_client/client.customScalar.test.ts deleted file mode 100644 index 71636e7e..00000000 --- a/src/layers/6_client/client.customScalar.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { describe, expect } from 'vitest' -import { createResponse, test } from '../../../tests/_/helpers.js' -import { db } from '../../../tests/_/schemas/db.js' -import type { Graffle } from '../../../tests/_/schemas/kitchen-sink/graffle/__.js' - -const date0Encoded = db.date0.toISOString() - -type TestCase = [ - describe: string, - query: Graffle.SelectionSets.Query, - responseData: object, - expectedData: object, -] - -// dprint-ignore -test.for([ - [`field`, { date: true }, { date: date0Encoded }, { date: db.date0 }], - [`field in non-null`, { dateNonNull: true }, { dateNonNull: date0Encoded }, { dateNonNull: db.date0 }], - [`field in list`, { dateList: true }, { dateList: [0, 1] }, { dateList: [db.date0, new Date(1)] }], - [`field in list non-null`, { dateListNonNull: true }, { dateListNonNull: [0, 1] }, { dateListNonNull: [db.date0, new Date(1)] }], - [`object field`, { dateObject1: { date1: true } }, { dateObject1: { date1: date0Encoded } }, { dateObject1: { date1: db.date0 } }], - [`object field in interface`, { dateInterface1: { date1: true } }, { dateInterface1: { date1: date0Encoded } }, { dateInterface1: { date1: db.date0 } }], -])(`query %s`, async ([_, query, responseData, expectedData], { fetch, kitchenSinkHttp: kitchenSink }) => { - fetch.mockResolvedValueOnce(createResponse({ data: responseData })) - expect(await kitchenSink.query.$batch(query)).toEqual(expectedData) -}) - -// dprint-ignore -describe(`object field in union`, () => { - test.for([ - [`case 1 with __typename`, - { dateUnion: { __typename: true, ___on_DateObject1: { date1: true } } }, - { dateUnion: { __typename: `DateObject1`, date1: 0 } }, - { dateUnion: { __typename: `DateObject1`, date1: db.date0 }} - ], - [`case 1 without __typename`, - { dateUnion: { ___on_DateObject1: { date1: true } } }, - { dateUnion: { date1: date0Encoded } }, - { dateUnion: { date1: db.date0 } } - ], - [`case 2`, - { dateUnion: { ___on_DateObject1: { date1: true }, ___on_DateObject2: { date2: true } } }, - { dateUnion: { date2: date0Encoded } }, - { dateUnion: { date2: db.date0 } } - ], - [`case 2 miss`, - { dateUnion: { ___on_DateObject1: { date1: true }, ___on_DateObject2: { date2: true } } }, - { dateUnion: null }, - { dateUnion: null } - ], - ])(`%s`, async ([_, query, responseData, expectedData], { fetch, kitchenSinkHttp: kitchenSink }) => { - fetch.mockResolvedValueOnce(createResponse({ data: responseData })) - expect(await kitchenSink.query.$batch(query)).toEqual(expectedData) - }) -}) diff --git a/src/layers/6_client/client.transport-http.test.ts b/src/layers/6_client/client.transport-http.test.ts index be47ca73..668cff6c 100644 --- a/src/layers/6_client/client.transport-http.test.ts +++ b/src/layers/6_client/client.transport-http.test.ts @@ -3,7 +3,7 @@ import { createResponse, test } from '../../../tests/_/helpers.js' import { serveSchema } from '../../../tests/_/lib/serveSchema.js' import { Pokemon } from '../../../tests/_/schemas/pokemon/graffle/__.js' import { Graffle } from '../../entrypoints/main.js' -import { ACCEPT_REC, CONTENT_TYPE_REC } from '../../lib/graphql-http/graphqlHTTP.js' +import { ACCEPT_REC, CONTENT_TYPE_REC } from '../../lib/grafaid/http/http.js' import { Transport } from '../5_request/types.js' const schema = new URL(`https://foo.io/api/graphql`) diff --git a/src/layers/6_client/customScalar/decode.test.ts b/src/layers/6_client/customScalar/decode.test.ts new file mode 100644 index 00000000..1fdf9a33 --- /dev/null +++ b/src/layers/6_client/customScalar/decode.test.ts @@ -0,0 +1,133 @@ +import { print } from 'graphql' +import { describe, expect } from 'vitest' +import { createResponse, test } from '../../../../tests/_/helpers.js' +import { db } from '../../../../tests/_/schemas/db.js' +import type { Graffle } from '../../../../tests/_/schemas/kitchen-sink/graffle/__.js' +import { Select } from '../../2_Select/__.js' +import { SelectionSetGraphqlMapper } from '../../3_SelectGraphQLMapper/__.js' + +const date0Encoded = db.date0.toISOString() + +type TestCase = [ + describe: string, + query: Graffle.SelectionSets.Query, + responseData: object, + expectedData: object, +] + +type TestCaseWith = Parameters>> + +const withBatch: TestCaseWith = [ + `$batch query %s`, + {}, + async ([_, query, responseData, expectedData], { fetch, kitchenSinkHttp: kitchenSink }) => { + fetch.mockResolvedValueOnce(createResponse({ data: responseData })) + expect(await kitchenSink.query.$batch(query)).toEqual(expectedData) + }, +] + +const withGqlDocument: TestCaseWith = [ + `gql document - %s`, + {}, + async ([_, query, responseData, expectedData], { fetch, kitchenSinkHttp: kitchenSink }) => { + fetch.mockResolvedValueOnce(createResponse({ data: responseData })) + const document = SelectionSetGraphqlMapper.toGraphQL({ + document: Select.Document.normalizeOrThrow({ query: { foo: query as any } }), + }) + expect(await kitchenSink.gql(document).send()).toEqual(expectedData) + }, +] + +const withGqlString: TestCaseWith = [ + `gql string - %s`, + {}, + async ([_, query, responseData, expectedData], { fetch, kitchenSinkHttp: kitchenSink }) => { + fetch.mockResolvedValueOnce(createResponse({ data: responseData })) + const document = SelectionSetGraphqlMapper.toGraphQL({ + document: Select.Document.normalizeOrThrow({ query: { foo: query as any } }), + }) + expect(await kitchenSink.gql(print(document)).send()).toEqual(expectedData) + }, +] + +// dprint-ignore +const testGeneralCases = test.for([ + [`nullable null`, { date: true }, { date: null }, { date: null }], + [`nullable value`, { date: true }, { date: date0Encoded }, { date: db.date0 }], + [`non-null`, { dateNonNull: true }, { dateNonNull: date0Encoded }, { dateNonNull: db.date0 }], + [`list`, { dateList: true }, { dateList: [0, 1] }, { dateList: [db.date0, new Date(1)] }], + [`list list`, { dateListList: true }, { dateListList: [[0, 1],[0,1]] }, { dateListList: [[db.date0, new Date(1)],[db.date0, new Date(1)]] }], + [`list non-null`, { dateListNonNull: true }, { dateListNonNull: [0, 1] }, { dateListNonNull: [db.date0, new Date(1)] }], + [`object field`, { dateObject1: { date1: true } }, { dateObject1: { date1: date0Encoded } }, { dateObject1: { date1: db.date0 } }], + [`interface field`, { dateInterface1: { date1: true } }, { dateInterface1: { date1: date0Encoded } }, { dateInterface1: { date1: db.date0 } }], + [`interface inline fragment`, { dateInterface1: { ___on_DateObject1: { date1: true } } }, { dateInterface1: { date1: date0Encoded } }, { dateInterface1: { date1: db.date0 } }], +]) + +// dprint-ignore +const testAliasCases = test.for([ + [`alias`, { date: [`x`, true] }, { x: date0Encoded }, { x: db.date0 }], + [`interface inline fragment alias`, { dateInterface1: { ___on_DateObject1: { date1: [`x`, true] } } }, { dateInterface1: { x: date0Encoded }}, { dateInterface1: { x: db.date0 }}], + [`interface inline fragment nested alias`, { dateInterface1: { ___on_DateObject1: { ___: { date1: [`x`, true] } } } }, { dateInterface1: { x: date0Encoded }}, { dateInterface1: { x: db.date0 }}], + [`inline fragment interface alias`, { ___: { dateInterface1: { ___on_DateObject1: { date1: [`x`, true] } } } }, { dateInterface1: { x: date0Encoded }}, { dateInterface1: { x: db.date0 }}], + [`inline fragment x2 interface alias & nullable value`, { ___: [{ dateInterface1: { ___on_DateObject1: { date1: [`x`, true] } } }, {date: [`y`,true]}] }, { dateInterface1: { x: date0Encoded }, y: date0Encoded }, { dateInterface1: { x: db.date0 }, y: db.date0 }], +]) + +// dprint-ignore +const testUnionCases = test.for([ + [`case 1with __typename`, + { dateUnion: { __typename: true, ___on_DateObject1: { date1: true } } }, + { dateUnion: { __typename: `DateObject1`, date1: date0Encoded } }, + { dateUnion: { __typename: `DateObject1`, date1: db.date0 }} + ], + [`case 1 without __typename`, + { dateUnion: { ___on_DateObject1: { date1: true } } }, + { dateUnion: { date1: date0Encoded } }, + { dateUnion: { date1: db.date0 } } + ], + [`case 2`, + { dateUnion: { ___on_DateObject1: { date1: true }, ___on_DateObject2: { date2: true } } }, + { dateUnion: { date2: date0Encoded } }, + { dateUnion: { date2: db.date0 } } + ], + [`case 2 miss`, + { dateUnion: { ___on_DateObject1: { date1: true }, ___on_DateObject2: { date2: true } } }, + { dateUnion: null }, + { dateUnion: null } + ], +]) + +describe(`$batch`, () => { + testGeneralCases(...withBatch) + testAliasCases(...withBatch) + // dprint-ignore + describe(`object field in union`, () => { + testUnionCases(`%s`, async ([_, query, responseData, expectedData], { fetch, kitchenSinkHttp: kitchenSink }) => { + fetch.mockResolvedValueOnce(createResponse({ data: responseData })) + expect(await kitchenSink.query.$batch(query)).toEqual(expectedData) + }) + }) +}) + +describe(`gql document`, () => { + testGeneralCases(...withGqlDocument) + testAliasCases(...withGqlDocument) + testUnionCases(...withGqlDocument) +}) + +describe(`gql string`, () => { + testGeneralCases(...withGqlString) + testUnionCases(...withGqlString) + testAliasCases(...withGqlString) + // todo make this test with the option of opting out of string document parsing once we add that optimization feature. + // testAliasCases( + // `aliases _not_ decoded - %s`, + // async ([_, query, responseData, __], { fetch, kitchenSinkHttp: kitchenSink }) => { + // fetch.mockResolvedValueOnce(createResponse({ data: responseData })) + + // const document = SelectionSetGraphqlMapper.toGraphQL({ + // document: Select.Document.normalizeOrThrow({ query: { foo: query as any } }), + // }) + // expect(await kitchenSink.gql(print(document)).send()).toEqual(responseData) + // }, + // ) +}) diff --git a/src/layers/6_client/customScalar/decode.ts b/src/layers/6_client/customScalar/decode.ts new file mode 100644 index 00000000..67a089e3 --- /dev/null +++ b/src/layers/6_client/customScalar/decode.ts @@ -0,0 +1,102 @@ +import { Kind, parse } from 'graphql' +import type { Grafaid } from '../../../lib/grafaid/__.js' +import { operationTypeNameToRootTypeName, parseOperationType } from '../../../lib/grafaid/graphql.js' +import { unType } from '../../../lib/grafaid/typed-document/TypedDocument.js' +import { isString } from '../../../lib/prelude.js' +import type { CodecString } from '../../3_SelectGraphQLMapper/types.js' +import type { CustomScalarsIndex } from '../../4_generator/generators/SchemaIndex.js' + +/** + * If a document is given then aliases will be decoded as well. + */ +export const decode = (input: { + data: Grafaid.SomeData | null | undefined + customScalarsIndex: CustomScalarsIndex + request: Grafaid.RequestInput +}) => { + const rootType = parseOperationType(input.request) + if (!rootType) return + + const customScalarsIndex = input.customScalarsIndex[operationTypeNameToRootTypeName[rootType]] + if (!customScalarsIndex) return + + const queryUntyped = unType(input.request.query) + // todo expose an option to optimize string interface by not parsing it. Explain the caveat of losing support for custom scalars in aliased positions. + // const document = isString(queryUntyped) ? null : queryUntyped + const document = (isString(queryUntyped) ? parse(queryUntyped) : queryUntyped) as Grafaid.Nodes.DocumentNode | null + const documentOperations = document?.definitions.filter(d => d.kind === Kind.OPERATION_DEFINITION) + const selectionSet = (documentOperations?.length === 1 ? documentOperations[0] : documentOperations?.find(d => { + return d.name?.value === input.request.operationName + }))?.selectionSet ?? null + + decode_({ + data: input.data, + customScalarsIndex, + documentPart: selectionSet, + }) +} + +const decode_ = (input: { + data: Grafaid.SomeData | null | undefined + customScalarsIndex: CustomScalarsIndex.OutputObject + documentPart: null | Grafaid.Nodes.SelectionSetNode +}): void => { + const { data, customScalarsIndex, documentPart } = input + if (!data) return + + for (const [k, v] of Object.entries(data)) { + // 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 indexField = customScalarsIndex[kSchema] + if (!indexField) continue + + const codec = indexField.o + if (codec) { + data[k] = decodeValue(v, codec) + continue + } + + const indexFieldType = indexField.r + if (indexFieldType) { + decode_({ + data: v, + customScalarsIndex: indexFieldType, + documentPart: documentField?.selectionSet ?? null, + }) + continue + } + + throw new Error(`Unknown index item: ${String(indexField)}`) + } +} + +const decodeValue = (value: any, codec: CodecString): any => { + if (Array.isArray(value)) { + return value.map(item => decodeValue(item, codec)) + } + return codec.decode(value) +} + +const findDocumentField = ( + selectionSet: null | Grafaid.Nodes.SelectionSetNode, + k: string, +): Grafaid.Nodes.FieldNode | null => { + if (!selectionSet) return null + + for (const selection of selectionSet.selections) { + if (selection.kind === Kind.FIELD && (selection.alias?.value ?? selection.name.value) === k) { + return selection + } + if (selection.kind === Kind.INLINE_FRAGMENT) { + const result = findDocumentField(selection.selectionSet, k) + if (result !== null) return result + } + } + + return null +} diff --git a/src/layers/6_client/customScalar/encode.ts b/src/layers/6_client/customScalar/encode.ts new file mode 100644 index 00000000..419f1df7 --- /dev/null +++ b/src/layers/6_client/customScalar/encode.ts @@ -0,0 +1,17 @@ +// import { describe, test } from 'vitest' +// import { db } from '../../../../tests/_/schemas/db.js' + +// // dprint-ignore +// describe(`custom scalars`, () => { +// test.each([ +// [`arg field`, { dateArg: { $: { date: db.date0 } } }], +// [`arg field in non-null`, { dateArgNonNull: { $: { date: db.date0 } } }], +// [`arg field in list`, { dateArgList: { $: { date: [db.date0, new Date(1)] } } }], +// [`arg field in list (null)`, { dateArgList: { $: { date: null } } }], +// [`arg field in non-null list (with list)`, { dateArgNonNullList: { $: { date: [db.date0, new Date(1)] } } }], +// [`arg field in non-null list (with null)`, { dateArgNonNullList: { $: { date: [null, db.date0] } } }], +// [`arg field in non-null list non-null`, { dateArgNonNullListNonNull: { $: { date: [db.date0, new Date(1)] } } }], +// [`input object field`, { dateArgInputObject: { $: { input: { idRequired: ``, dateRequired: db.date0, date: new Date(1) } } } }], +// [`nested input object field`, { InputObjectNested: { $: { input: { InputObject: { idRequired: ``, dateRequired: db.date0, date: new Date(1) } } } } }] +// ])(...testEachArguments) +// }) diff --git a/src/layers/6_client/gql/gql.test-d.ts b/src/layers/6_client/gql/gql.test-d.ts index 3dd0a94e..a6d96077 100644 --- a/src/layers/6_client/gql/gql.test-d.ts +++ b/src/layers/6_client/gql/gql.test-d.ts @@ -1,10 +1,10 @@ import { kitchenSink as g } from '../../../../tests/_/helpers.js' import { AssertTypeOf } from '../../../lib/assert-equal.js' -import type { TypedDocument } from '../../../lib/typed-document/__.js' +import type { Grafaid } from '../../../lib/grafaid/__.js' type D = { id: 0 } -const d1 = 0 as any as TypedDocument.Node<{ id: 0 }, {}> +const d1 = 0 as any as Grafaid.Nodes.Typed.Node<{ id: 0 }, {}> AssertTypeOf(await g.gql(d1).send()) // @ts-expect-error - variables not allowed. @@ -14,7 +14,7 @@ await g.gql(d1).send({}) // // -const d2 = 0 as any as TypedDocument.Node<{ id: 0 }, { x?: 0 }> +const d2 = 0 as any as Grafaid.Nodes.Typed.Node<{ id: 0 }, { x?: 0 }> AssertTypeOf(await g.gql(d2).send()) AssertTypeOf(await g.gql(d2).send({})) @@ -27,7 +27,7 @@ await g.gql(d2).send({ filter: `wrong type` }) // // -const d3 = 0 as any as TypedDocument.Node<{ id: 0 }, { x: 0 }> +const d3 = 0 as any as Grafaid.Nodes.Typed.Node<{ id: 0 }, { x: 0 }> AssertTypeOf(await g.gql(d3).send({ x: 0 })) // @ts-expect-error - missing argument @@ -39,34 +39,34 @@ AssertTypeOf(await g.gql(d3).send({})) // dprint-ignore { - AssertTypeOf(await g.gql >``.send({ x: 1 })) - AssertTypeOf(await g.gql >``.send()) - AssertTypeOf(await g.gql >``.send({ x: 1 })) - AssertTypeOf(await g.gql >``.send()) - AssertTypeOf(await g.gql>``.send()) - AssertTypeOf(await g.gql>``.send({ x: 1 })) - AssertTypeOf(await g.gql>``.send(`abc`, { x: 1 })) + AssertTypeOf(await g.gql >``.send({ x: 1 })) + AssertTypeOf(await g.gql >``.send()) + AssertTypeOf(await g.gql >``.send({ x: 1 })) + AssertTypeOf(await g.gql >``.send()) + AssertTypeOf(await g.gql>``.send()) + AssertTypeOf(await g.gql>``.send({ x: 1 })) + AssertTypeOf(await g.gql>``.send(`abc`, { x: 1 })) // @ts-expect-error - wrong argument type - await g.gql >``.send({ x: 2 }) + await g.gql >``.send({ x: 2 }) - AssertTypeOf(await g.gql >``.send({ x: 1 })) - AssertTypeOf(await g.gql >``.send()) - AssertTypeOf(await g.gql >``.send({ x: 1 })) - AssertTypeOf(await g.gql >``.send()) - AssertTypeOf(await g.gql>``.send()) // eslint-disable-line - AssertTypeOf(await g.gql>``.send({ x: 1 })) // eslint-disable-line - AssertTypeOf(await g.gql>``.send(`abc`, { x: 1 })) // eslint-disable-line + AssertTypeOf(await g.gql >``.send({ x: 1 })) + AssertTypeOf(await g.gql >``.send()) + AssertTypeOf(await g.gql >``.send({ x: 1 })) + AssertTypeOf(await g.gql >``.send()) + AssertTypeOf(await g.gql>``.send()) // eslint-disable-line + AssertTypeOf(await g.gql>``.send({ x: 1 })) // eslint-disable-line + AssertTypeOf(await g.gql>``.send(`abc`, { x: 1 })) // eslint-disable-line // @ts-expect-error - wrong argument type await g.gql >``.send({ x: 2 }) - AssertTypeOf(await g.gql >``.send({ x: 1 })) - AssertTypeOf(await g.gql >``.send()) - AssertTypeOf(await g.gql >``.send({ x: 1 })) - AssertTypeOf(await g.gql >``.send()) - AssertTypeOf(await g.gql>``.send()) // eslint-disable-line - AssertTypeOf(await g.gql>``.send({ x: 1 })) // eslint-disable-line - AssertTypeOf(await g.gql>``.send(`abc`, { x: 1 })) // eslint-disable-line + AssertTypeOf(await g.gql >``.send({ x: 1 })) + AssertTypeOf(await g.gql >``.send()) + AssertTypeOf(await g.gql >``.send({ x: 1 })) + AssertTypeOf(await g.gql >``.send()) + AssertTypeOf(await g.gql>``.send()) // eslint-disable-line + AssertTypeOf(await g.gql>``.send({ x: 1 })) // eslint-disable-line + AssertTypeOf(await g.gql>``.send(`abc`, { x: 1 })) // eslint-disable-line // @ts-expect-error - wrong argument type - await g.gql >``.send({ x: 2 }) + await g.gql >``.send({ x: 2 }) } diff --git a/src/layers/6_client/gql/gql.ts b/src/layers/6_client/gql/gql.ts index 4520aeaf..f7fa4a48 100644 --- a/src/layers/6_client/gql/gql.ts +++ b/src/layers/6_client/gql/gql.ts @@ -1,5 +1,5 @@ import type { Fluent } from '../../../lib/fluent/__.js' -import type { TypedDocument } from '../../../lib/typed-document/__.js' +import type { Grafaid } from '../../../lib/grafaid/__.js' import { RequestCore } from '../../5_request/__.js' import type { InterfaceRaw } from '../../5_request/types.js' import { defineTerminus } from '../fluent.js' @@ -9,13 +9,13 @@ import { type DocumentController, resolveSendArguments, type sendArgumentsImplem // dprint-ignore export interface gql<$Config extends Config = Config> { - <$Document extends TypedDocument.TypedDocument>(document: $Document ): DocumentController<$Config, $Document> - <$Document extends TypedDocument.TypedDocument>(parts: TemplateStringsArray, ...args: unknown[]): DocumentController<$Config, $Document> + <$Document extends Grafaid.Nodes.Typed.TypedDocument>(document: $Document ): DocumentController<$Config, $Document> + <$Document extends Grafaid.Nodes.Typed.TypedDocument>(parts: TemplateStringsArray, ...args: unknown[]): DocumentController<$Config, $Document> } type TemplateStringsArguments = [TemplateStringsArray, ...unknown[]] -type gqlArguments = [TypedDocument.TypedDocument] | TemplateStringsArguments +type gqlArguments = [Grafaid.Nodes.Typed.TypedDocument] | TemplateStringsArguments const resolveGqlArguments = (args: gqlArguments) => { const document = isTemplateStringArguments(args) ? joinTemplateStringArrayAndArgs(args) : args[0] diff --git a/src/layers/6_client/gql/send.test-d.ts b/src/layers/6_client/gql/send.test-d.ts index 225ae710..0361834e 100644 --- a/src/layers/6_client/gql/send.test-d.ts +++ b/src/layers/6_client/gql/send.test-d.ts @@ -1,20 +1,20 @@ import { AssertEqual } from '../../../lib/assert-equal.js' -import type { TypedDocument } from '../../../lib/typed-document/__.js' +import type { Grafaid } from '../../../lib/grafaid/__.js' import type { SendArguments } from './send.js' AssertEqual< - SendArguments>, + SendArguments>, [string, { x: 1 }] | [{ x: 1 }] >() AssertEqual< - SendArguments>, + SendArguments>, [x?: string] | [x?: string, x?: { x?: 1 }] | [x?: { x?: 1 }] >() AssertEqual< - SendArguments>, + SendArguments>, [x?: string] >() AssertEqual< - SendArguments>, - [x?: string] | [x?: string, x?: TypedDocument.Variables] | [x?: TypedDocument.Variables] + SendArguments>, + [x?: string] | [x?: string, x?: Grafaid.Nodes.Typed.Variables] | [x?: Grafaid.Nodes.Typed.Variables] >() diff --git a/src/layers/6_client/gql/send.ts b/src/layers/6_client/gql/send.ts index a8a87fbd..46481f56 100644 --- a/src/layers/6_client/gql/send.ts +++ b/src/layers/6_client/gql/send.ts @@ -1,29 +1,36 @@ +import type { Grafaid } from '../../../lib/grafaid/__.js' import { isString } from '../../../lib/prelude.js' -import type { TypedDocument } from '../../../lib/typed-document/__.js' import type { ResolveOutputGql } from '../handleOutput.js' import type { Config } from '../Settings/Config.js' // dprint-ignore -export type SendArguments<$TypedDocument extends TypedDocument.TypedDocument> = - SendArguments_> +export type SendArguments<$TypedDocument extends Grafaid.Nodes.Typed.TypedDocument> = + SendArguments_> // dprint-ignore -type SendArguments_<$Variables extends TypedDocument.Variables> = - SendArguments__<$Variables, TypedDocument.GetVariablesInputKind<$Variables>> +type SendArguments_<$Variables extends Grafaid.Nodes.Typed.Variables> = + SendArguments__<$Variables, Grafaid.Nodes.Typed.GetVariablesInputKind<$Variables>> // dprint-ignore -type SendArguments__<$Variables extends TypedDocument.Variables, $VariablesKind extends TypedDocument.VariablesInputKind> = +type SendArguments__<$Variables extends Grafaid.Nodes.Typed.Variables, $VariablesKind extends Grafaid.Nodes.Typed.VariablesInputKind> = $VariablesKind extends 'none' ? ([operationName?: string]) : $VariablesKind extends 'optional' ? ([operationName?: string] | [operationName?: string, variables?: $Variables] | [variables?: $Variables]) : $VariablesKind extends 'required' ? ([operationName: string, variables: $Variables] | [variables: $Variables]) : never // dprint-ignore -export interface DocumentController<$Config extends Config, $TypedDocument extends TypedDocument.TypedDocument> { - send(...args: SendArguments<$TypedDocument>): Promise>> +export interface DocumentController<$Config extends Config, $TypedDocument extends Grafaid.Nodes.Typed.TypedDocument> { + send(...args: SendArguments<$TypedDocument>): Promise>> } -export type sendArgumentsImplementation = [] | [string] | [TypedDocument.Variables] | [string, TypedDocument.Variables] +export type sendArgumentsImplementation = + | [] + | [string] + | [Grafaid.Nodes.Typed.Variables] + | [ + string, + Grafaid.Nodes.Typed.Variables, + ] export const resolveSendArguments = (args: sendArgumentsImplementation) => { const operationName = isString(args[0]) ? args[0] : undefined diff --git a/src/layers/6_client/handleOutput.ts b/src/layers/6_client/handleOutput.ts index b1b43900..298499f7 100644 --- a/src/layers/6_client/handleOutput.ts +++ b/src/layers/6_client/handleOutput.ts @@ -1,7 +1,7 @@ import type { ExecutionResult, GraphQLError } from 'graphql' import type { Simplify } from 'type-fest' import { Errors } from '../../lib/errors/__.js' -import type { GraphQLExecutionResultError } from '../../lib/graphql-plus/graphql.js' +import type { GraphQLExecutionResultError } from '../../lib/grafaid/graphql.js' import { isRecordLikeObject, type SimplifyExceptError, type Values } from '../../lib/prelude.js' import type { SchemaIndex } from '../4_generator/generators/SchemaIndex.js' import type { TransportHttp } from '../5_request/types.js' diff --git a/src/layers/6_client/requestMethods/document.test.ts b/src/layers/6_client/requestMethods/document.test.ts index 392a4e3f..31fb9ae7 100644 --- a/src/layers/6_client/requestMethods/document.test.ts +++ b/src/layers/6_client/requestMethods/document.test.ts @@ -40,25 +40,20 @@ describe(`document with two queries`, () => { test(`error if no operation name is provided`, async () => { const { run } = withTwo // @ts-expect-error - const error = await run().catch((e: unknown) => e) as Errors.ContextualError - // todo it doesn't really make sense that this happens in decode. If the schema didn't reject it than nor should we, - // and if schema will reject it than pre-send validation is what this really is. - expect(error).toMatchObject({ - message: `There was an error in the core implementation of hook "decode".`, - cause: { - message: `Must provide operation name if query contains multiple operations.`, - }, - }) + const error = await run().catch((e: unknown) => e) as Errors.ContextualAggregateError + expect(error.message).toEqual(`One or more errors in the execution result.`) + expect(error.errors[0]?.message).toEqual( + `Must provide operation name if query contains multiple operations.`, + ) }) test(`error if wrong operation name is provided`, async () => { const { run } = withTwo // @ts-expect-error - await expect(run(`boo`)).rejects.toMatchObject({ - message: `There was an error in the core implementation of hook "decode".`, - cause: { - message: `Unknown operation named "boo".`, - }, - }) + const error = await run(`boo`).catch((e: unknown) => e) as Errors.ContextualAggregateError + expect(error.message).toEqual(`One or more errors in the execution result.`) + expect(error.errors[0]?.message).toEqual( + `Unknown operation named "boo".`, + ) }) test(`error if no operations provided`, () => { expect(() => { diff --git a/src/layers/6_client/requestMethods/requestMethods.ts b/src/layers/6_client/requestMethods/requestMethods.ts index 2063d160..2b103495 100644 --- a/src/layers/6_client/requestMethods/requestMethods.ts +++ b/src/layers/6_client/requestMethods/requestMethods.ts @@ -1,6 +1,6 @@ import type { HKT } from '../../../entrypoints/utilities-for-generated.js' import type { Fluent } from '../../../lib/fluent/__.js' -import type { RootTypeName, Variables } from '../../../lib/graphql-plus/graphql.js' +import type { Grafaid } from '../../../lib/grafaid/__.js' import { readMaybeThunk } from '../../1_Schema/_.js' import { Schema } from '../../1_Schema/__.js' import { Select } from '../../2_Select/__.js' @@ -52,7 +52,7 @@ export const createMethodDocument = (state: State) => (document: Select.Document } } -const createMethodRootType = (state: State, rootTypeName: RootTypeName) => { +const createMethodRootType = (state: State, rootTypeName: Grafaid.Schema.RootTypeName) => { return new Proxy({}, { get: (_, key) => { if (typeof key === `symbol`) throw new Error(`Symbols not supported.`) @@ -71,7 +71,7 @@ const createMethodRootType = (state: State, rootTypeName: RootTypeName) => { const executeRootTypeField = async ( state: State, - rootTypeName: RootTypeName, + rootTypeName: Grafaid.Schema.RootTypeName, rootTypeFieldName: string, argsOrSelectionSet?: Select.SelectionSet.AnySelectionSet | Select.Arguments.ArgsObject, ) => { @@ -113,7 +113,7 @@ const executeRootTypeField = async ( const executeRootType = async ( state: State, - rootTypeName: RootTypeName, + rootTypeName: Grafaid.Schema.RootTypeName, rootTypeSelectionSet: Select.SelectionSet.AnySelectionSet, ) => { return executeDocument( @@ -129,7 +129,7 @@ export const executeDocument = async ( state: State, document: Select.Document.DocumentNormalized, operationName?: string, - variables?: Variables, + variables?: Grafaid.Variables, ) => { const transportType = state.config.transport.type const interfaceType = `typed` diff --git a/src/layers/7_extensions/Upload/Upload.ts b/src/layers/7_extensions/Upload/Upload.ts index 79d2693c..b13020eb 100644 --- a/src/layers/7_extensions/Upload/Upload.ts +++ b/src/layers/7_extensions/Upload/Upload.ts @@ -1,5 +1,5 @@ import { createExtension } from '../../../entrypoints/main.js' -import type { Variables } from '../../../lib/graphql-plus/graphql.js' +import type { Variables } from '../../../lib/grafaid/graphql.js' import { createBody } from './createBody.js' /** @@ -9,6 +9,11 @@ export const Upload = () => createExtension({ name: `Upload`, onRequest: async ({ pack }) => { + // TODO we can probably get file upload working for in-memory schemas too :) + if (pack.input.transportType !== `http`) { + throw new Error(`Must be using http transport to use "Upload" scalar.`) + } + // Remove the content-type header so that fetch sets it automatically upon seeing the body is a FormData instance. // @see https://muffinman.io/blog/uploading-files-using-fetch-multipart-form-data/ // @see https://stackoverflow.com/questions/3508338/what-is-the-boundary-in-multipart-form-data diff --git a/src/layers/7_extensions/Upload/createBody.ts b/src/layers/7_extensions/Upload/createBody.ts index 27261918..b46e5c58 100644 --- a/src/layers/7_extensions/Upload/createBody.ts +++ b/src/layers/7_extensions/Upload/createBody.ts @@ -1,9 +1,12 @@ -import type { RequestInput } from '../../../lib/graphql-http/graphqlHTTP.js' +import type { RequestConfig } from '../../../lib/grafaid/http/http.js' import extractFiles from './extractFiles.js' -export const createBody = (input: RequestInput): FormData => { +export const createBody = (input: RequestConfig): FormData => { const { clone, files } = extractFiles( - { query: input.query, variables: input.variables }, + { + query: input.query, + variables: input.variables, + }, (value: unknown) => value instanceof Blob, ``, ) diff --git a/src/layers/7_extensions/Upload/extractFiles.ts b/src/layers/7_extensions/Upload/extractFiles.ts index 9b3e0856..55bce7b6 100644 --- a/src/layers/7_extensions/Upload/extractFiles.ts +++ b/src/layers/7_extensions/Upload/extractFiles.ts @@ -2,76 +2,10 @@ import isPlainObject from 'is-plain-obj' -/** @typedef {import("./isExtractableFile.mjs").default} isExtractableFile */ - -/** - * Recursively extracts files and their {@link ObjectPath object paths} within a - * value, replacing them with `null` in a deep clone without mutating the - * original value. - * [`FileList`](https://developer.mozilla.org/en-US/docs/Web/API/Filelist) - * instances are treated as - * [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) instance - * arrays. - * @template Extractable Extractable file type. - * @param {unknown} value Value to extract files from. Typically an object tree. - * @param {(value: unknown) => value is Extractable} isExtractable Matches - * extractable files. Typically {@linkcode isExtractableFile}. - * @param {ObjectPath} [path] Prefix for object paths for extracted files. - * Defaults to `""`. - * @returns {Extraction} Extraction result. - * @example - * Extracting files from an object. - * - * For the following: - * - * ```js - * import extractFiles from "extract-files/extractFiles.mjs"; - * import isExtractableFile from "extract-files/isExtractableFile.mjs"; - * - * const file1 = new File(["1"], "1.txt", { type: "text/plain" }); - * const file2 = new File(["2"], "2.txt", { type: "text/plain" }); - * const value = { - * a: file1, - * b: [file1, file2], - * }; - * - * const { clone, files } = extractFiles(value, isExtractableFile, "prefix"); - * ``` - * - * `value` remains the same. - * - * `clone` is: - * - * ```json - * { - * "a": null, - * "b": [null, null] - * } - * ``` - * - * `files` is a - * [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) - * instance containing: - * - * | Key | Value | - * | :------ | :--------------------------- | - * | `file1` | `["prefix.a", "prefix.b.0"]` | - * | `file2` | `["prefix.b.1"]` | - */ -export default function extractFiles(value: any, isExtractable: any, path = ``): { +const extractFiles = (value: any, isExtractable: (value: unknown) => boolean, path: string): { clone: object files: Map -} { - if (!arguments.length) throw new TypeError(`Argument 1 \`value\` is required.`) - - if (typeof isExtractable !== `function`) { - throw new TypeError(`Argument 2 \`isExtractable\` must be a function.`) - } - - if (typeof path !== `string`) { - throw new TypeError(`Argument 3 \`path\` must be a string.`) - } - +} => { /** * Deeply clonable value. * @typedef {Array | FileList | { @@ -105,7 +39,7 @@ export default function extractFiles(value: any, isExtractable: any, path = ``): * recursion of circular references within the input value. * @returns {unknown} Clone of the value with files replaced with `null`. */ - function recurse(value: any, path: any, recursed: any) { + function recurse(value: any, path: string, recursed: Set) { if (isExtractable(value)) { const filePaths = files.get(value) @@ -116,6 +50,7 @@ export default function extractFiles(value: any, isExtractable: any, path = ``): const valueIsList = Array.isArray(value) || (typeof FileList !== `undefined` && value instanceof FileList) + const valueIsPlainObject = isPlainObject(value) if (valueIsList || valueIsPlainObject) { @@ -178,6 +113,64 @@ export default function extractFiles(value: any, isExtractable: any, path = ``): } } +/** @typedef {import("./isExtractableFile.mjs").default} isExtractableFile */ + +/** + * Recursively extracts files and their {@link ObjectPath object paths} within a + * value, replacing them with `null` in a deep clone without mutating the + * original value. + * [`FileList`](https://developer.mozilla.org/en-US/docs/Web/API/Filelist) + * instances are treated as + * [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) instance + * arrays. + * @template Extractable Extractable file type. + * @param {unknown} value Value to extract files from. Typically an object tree. + * @param {(value: unknown) => value is Extractable} isExtractable Matches + * extractable files. Typically {@linkcode isExtractableFile}. + * @param {ObjectPath} [path] Prefix for object paths for extracted files. + * Defaults to `""`. + * @returns {Extraction} Extraction result. + * @example + * Extracting files from an object. + * + * For the following: + * + * ```js + * import extractFiles from "extract-files/extractFiles.mjs"; + * import isExtractableFile from "extract-files/isExtractableFile.mjs"; + * + * const file1 = new File(["1"], "1.txt", { type: "text/plain" }); + * const file2 = new File(["2"], "2.txt", { type: "text/plain" }); + * const value = { + * a: file1, + * b: [file1, file2], + * }; + * + * const { clone, files } = extractFiles(value, isExtractableFile, "prefix"); + * ``` + * + * `value` remains the same. + * + * `clone` is: + * + * ```json + * { + * "a": null, + * "b": [null, null] + * } + * ``` + * + * `files` is a + * [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) + * instance containing: + * + * | Key | Value | + * | :------ | :--------------------------- | + * | `file1` | `["prefix.a", "prefix.b.0"]` | + * | `file2` | `["prefix.b.1"]` | + */ +export default extractFiles + /** * An extraction result. * @template [Extractable=unknown] Extractable file type. diff --git a/src/lib/anyware/__.test-d.ts b/src/lib/anyware/__.test-d.ts index f071c935..b29047dc 100644 --- a/src/lib/anyware/__.test-d.ts +++ b/src/lib/anyware/__.test-d.ts @@ -5,7 +5,7 @@ import { Result } from '../../../tests/_/schemas/kitchen-sink/graffle/modules/Sc import { ContextualError } from '../errors/ContextualError.js' import { type MaybePromise } from '../prelude.js' import { Anyware } from './__.js' -import { type SomeHook } from './main.js' +import type { PublicHook } from './hook/public.js' type InputA = { valueA: string } type InputB = { valueB: string } @@ -19,8 +19,8 @@ describe('without slots', () => { (input: { hookNamesOrderedBySequence: ['a', 'b'] hooks: { - a: (input: { input: InputA }) => InputB - b: (input: { input: InputB }) => Result + a: (input: { input: InputA; previous: {} }) => InputB + b: (input: { input: InputB; previous: { a: { input: InputA } } }) => Result } }) => any >() @@ -34,22 +34,24 @@ describe('without slots', () => { initialInput: InputA options?: Anyware.Options retryingExtension?: (input: { - a: SomeHook< - (input?: { input?: InputA }) => MaybePromise< + a: PublicHook< + (params?: { input?: InputA }) => MaybePromise< Error | { - b: SomeHook<(input?: { input?: InputB }) => MaybePromise> + b: PublicHook< + (params?: { input?: InputB }) => MaybePromise + > } > > - b: SomeHook<(input?: { input?: InputB }) => MaybePromise> + b: PublicHook<(params?: { input?: InputB }) => MaybePromise> }) => Promise extensions: ((input: { - a: SomeHook< - (input?: { input?: InputA }) => MaybePromise<{ - b: SomeHook<(input?: { input?: InputB }) => MaybePromise> + a: PublicHook< + (params?: { input?: InputA }) => MaybePromise<{ + b: PublicHook<(params?: { input?: InputB }) => MaybePromise> }> > - b: SomeHook<(input?: { input?: InputB }) => MaybePromise> + b: PublicHook<(params?: { input?: InputB }) => MaybePromise> }) => Promise)[] }) => Promise >() @@ -65,7 +67,7 @@ describe('withSlots', () => { hookNamesOrderedBySequence: ['a'] hooks: { a: { - run: (input: { input: InputA }) => Result + run: (input: { input: InputA; previous: {} }) => Result slots: { x: (x: boolean) => number } @@ -83,14 +85,14 @@ describe('withSlots', () => { initialInput: InputA options?: Anyware.Options extensions: ((input: { - a: SomeHook< + a: PublicHook< ( input?: { input?: InputA; using?: { x?: (x: boolean) => number | undefined } }, ) => MaybePromise > }) => Promise)[] retryingExtension?: (input: { - a: SomeHook< + a: PublicHook< (input?: { input?: InputA; using?: { x?: (x: boolean) => number | undefined } }) => MaybePromise< Error | Result > diff --git a/src/lib/anyware/getEntrypoint.ts b/src/lib/anyware/getEntrypoint.ts index 2680954d..539d458c 100644 --- a/src/lib/anyware/getEntrypoint.ts +++ b/src/lib/anyware/getEntrypoint.ts @@ -1,7 +1,8 @@ // import type { Extension, HookName } from '../../layers/5_client/extension/types.js' import { analyzeFunction } from '../analyzeFunction.js' import { ContextualError } from '../errors/ContextualError.js' -import type { HookName, NonRetryingExtensionInput } from './main.js' +import type { HookName } from './hook/definition.js' +import type { NonRetryingExtensionInput } from './main.js' export class ErrorAnywareExtensionEntrypoint extends ContextualError< 'ErrorGraffleExtensionEntryHook', diff --git a/src/lib/anyware/hook/definition.ts b/src/lib/anyware/hook/definition.ts new file mode 100644 index 00000000..4ed5ccf0 --- /dev/null +++ b/src/lib/anyware/hook/definition.ts @@ -0,0 +1,61 @@ +import type { FindValueAfter, IsLastValue } from '../../prelude.js' + +import type { MaybePromise } from '../../prelude.js' +import type { HookResultError, InferPrivateHookInput } from './private.js' + +export type HookSequence = readonly [string, ...string[]] + +export type HookDefinitionMap<$HookSequence extends HookSequence> = Record< + $HookSequence[number], + HookDefinition +> + +export type HookDefinition = { + input: any /* object <- type error but more accurate */ + slots?: any /* object <- type error but more accurate */ +} + +export type HookName = string + +export type InferDefinition< + $HookSequence extends HookSequence = HookSequence, + $HookMap extends HookDefinitionMap<$HookSequence> = HookDefinitionMap<$HookSequence>, + $Result = unknown, +> = { + hookNamesOrderedBySequence: $HookSequence + // dprint-ignore + hooks: { + [$HookName in $HookSequence[number]]: + keyof $HookMap[$HookName]['slots'] extends never + ? (input: InferPrivateHookInput<$HookSequence, $HookMap, $HookName>) => + MaybePromise< + IsLastValue<$HookName, $HookSequence> extends true + ? $Result + : $HookMap[FindValueAfter<$HookName, $HookSequence>]['input'] + > + : { + slots: $HookMap[$HookName]['slots'] + run: (input: InferPrivateHookInput<$HookSequence, $HookMap, $HookName>) => + MaybePromise< + IsLastValue<$HookName, $HookSequence> extends true ? $Result + : $HookMap[FindValueAfter<$HookName, $HookSequence>]['input'] + > + } + } + /** + * If a hook results in a thrown error but is an instance of one of these classes then return it as-is + * rather than wrapping it in a ContextualError. + * + * This can be useful when there are known kinds of errors such as Abort Errors from AbortController + * which are actually a signaling mechanism. + */ + passthroughErrorInstanceOf?: Function[] + /** + * If a hook results in a thrown error but returns true from this function then return the error as-is + * rather than wrapping it in a ContextualError. + * + * This can be useful when there are known kinds of errors such as Abort Errors from AbortController + * which are actually a signaling mechanism. + */ + passthroughErrorWith?: (signal: HookResultError) => boolean +} diff --git a/src/lib/anyware/hook/private.ts b/src/lib/anyware/hook/private.ts new file mode 100644 index 00000000..67036588 --- /dev/null +++ b/src/lib/anyware/hook/private.ts @@ -0,0 +1,81 @@ +import type { Errors } from '../../errors/__.js' +import type { Deferred, MaybePromise, SomeFunction, TakeValuesBefore } from '../../prelude.js' +import type { Extension } from '../main.js' +import type { HookDefinitionMap, HookSequence } from './definition.js' + +export type InferPrivateHookInput< + $HookSequence extends HookSequence, + $HookMap extends HookDefinitionMap<$HookSequence>, + $HookName extends string, +> = HookPrivateInput< + $HookMap[$HookName]['input'], + $HookMap[$HookName]['slots'], + { + [$PreviousHookName in TakeValuesBefore<$HookName, $HookSequence>[number]]: { + input: $HookMap[$PreviousHookName]['input'] + } + } +> + +export type PrivateHook<$Slots extends Slots, $Input extends HookPrivateInput, $Return> = { + slots: $Slots + run: (input: $Input) => MaybePromise<$Return> +} + +export type HookPrivateInput< + $Input extends object | undefined = object | undefined, + $Slots extends Slots | undefined = Slots | undefined, + $Previous extends object = object, +> = { + input: $Input + slots: $Slots + previous: $Previous +} + +export type Slots = Record + +export type HookResult = + | HookResultCompleted + | HookResultShortCircuited + | HookResultErrorUser + | HookResultErrorImplementation + | HookResultErrorExtension + +export interface HookResultShortCircuited { + type: 'shortCircuited' + result: unknown +} + +export interface HookResultCompleted { + type: 'completed' + effectiveInput: object + result: unknown + nextExtensionsStack: readonly Extension[] +} + +export type HookResultError = HookResultErrorExtension | HookResultErrorImplementation | HookResultErrorUser + +export interface HookResultErrorUser { + type: 'error' + hookName: string + source: 'user' + error: Errors.ContextualError + extensionName: string +} + +export interface HookResultErrorExtension { + type: 'error' + hookName: string + source: 'extension' + error: Error + extensionName: string +} + +export interface HookResultErrorImplementation { + type: 'error' + hookName: string + source: 'implementation' + error: Error +} + +export type HookResultErrorAsync = Deferred diff --git a/src/lib/anyware/hook/public.ts b/src/lib/anyware/hook/public.ts new file mode 100644 index 00000000..8aee5364 --- /dev/null +++ b/src/lib/anyware/hook/public.ts @@ -0,0 +1,104 @@ +import type { FindValueAfter, IsLastValue } from '../../prelude.js' +import type { ExtensionOptions } from '../main.js' +import type { HookDefinition, HookDefinitionMap, HookSequence } from './definition.js' + +export type InferPublicHooks< + $HookSequence extends HookSequence, + $HookMap extends Record<$HookSequence[number], HookDefinition> = Record<$HookSequence[number], HookDefinition>, + $Result = unknown, + $Options extends ExtensionOptions = ExtensionOptions, +> = { + [$HookName in $HookSequence[number]]: InferPublicHook<$HookSequence, $HookMap, $Result, $HookName, $Options> +} + +type InferPublicHook< + $HookSequence extends HookSequence, + $HookMap extends HookDefinitionMap<$HookSequence> = HookDefinitionMap<$HookSequence>, + $Result = unknown, + $Name extends $HookSequence[number] = $HookSequence[number], + $Options extends ExtensionOptions = ExtensionOptions, +> = PublicHook< + // (<$$Input extends $HookMap[$Name]['input']>( + (( + input?: + & { + input?: $HookMap[$Name]['input'] + } + & (keyof $HookMap[$Name]['slots'] extends never ? {} : { using?: SlotInputify<$HookMap[$Name]['slots']> }), + ) => InferPublicHookReturn<$HookSequence, $HookMap, $Result, $Name, $Options>), + $HookMap[$Name]['input'] +> + +// & (<$$Input extends $HookMap[$Name]['input']>( +// input?: // InferHookPrivateInput<$HookSequence,$HookMap,$Name> +// { +// input?: $$Input +// } & (keyof $HookMap[$Name]['slots'] extends never ? {} : { using?: SlotInputify<$HookMap[$Name]['slots']> }), +// ) => PublicHookReturn<$HookSequence, $HookMap, $Result, $Name, $Options>) +// & { +// [hookSymbol]: HookSymbol +// input: $HookMap[$Name]['input'] +// } + +type InferPublicHookReturn< + $HookSequence extends HookSequence, + $HookMap extends HookDefinitionMap<$HookSequence> = HookDefinitionMap<$HookSequence>, + $Result = unknown, + $Name extends $HookSequence[number] = $HookSequence[number], + $Options extends ExtensionOptions = ExtensionOptions, +> = Promise< + | ($Options['retrying'] extends true ? Error : never) + | (IsLastValue<$Name, $HookSequence> extends true ? $Result : { + [$NameNext in FindValueAfter<$Name, $HookSequence>]: InferPublicHook< + $HookSequence, + $HookMap, + $Result, + $NameNext + > + }) +> + +type SlotInputify<$Slots extends Record any>> = { + [K in keyof $Slots]?: SlotInput<$Slots[K]> +} + +type SlotInput any> = (...args: Parameters) => ReturnType | undefined + +const hookSymbol = Symbol(`hook`) + +type HookSymbol = typeof hookSymbol + +export type SomePublicHookEnvelope = { + [name: string]: PublicHook +} + +export const createPublicHook = <$OriginalInput, $Fn extends PublicHookFn>( + originalInput: $OriginalInput, + fn: $Fn, +): PublicHook<$Fn> => { + // ): $Hook & { input: $OriginalInput } => { + // @ts-expect-error + fn.input = originalInput + // @ts-expect-error + return fn +} + +export type PublicHook< + $Fn extends PublicHookFn = PublicHookFn, + $OriginalInput extends object = object, // Exclude[0], undefined>['input'], +> = + & $Fn + & { + [hookSymbol]: HookSymbol + // todo the result is unknown, but if we build a EndEnvelope, then we can work with this type more logically and put it here. + // E.g. adding `| unknown` would destroy the knowledge of hook envelope case + // todo this is not strictly true, it could also be the final result + input: $OriginalInput + } + +type PublicHookFn = (input?: HookPublicInput) => any + +interface HookPublicInput { + input?: any + using?: any +} diff --git a/tests/_/schemas/kitchen-sink/graffle/modules/SchemaCustomScalarIndex.ts b/src/lib/anyware/hook/types.ts similarity index 100% rename from tests/_/schemas/kitchen-sink/graffle/modules/SchemaCustomScalarIndex.ts rename to src/lib/anyware/hook/types.ts diff --git a/src/lib/anyware/main.test.ts b/src/lib/anyware/main.test.ts index c77b407a..d143692e 100644 --- a/src/lib/anyware/main.test.ts +++ b/src/lib/anyware/main.test.ts @@ -5,7 +5,7 @@ import { Errors } from '../errors/__.js' import type { ContextualError } from '../errors/ContextualError.js' import { Anyware } from './__.js' import { createRetryingExtension } from './main.js' -import { core, createHook, type Input, oops, run, runWithOptions } from './specHelpers.js' +import { core, createHook, initialInput, oops, run, runWithOptions } from './specHelpers.js' describe(`no extensions`, () => { test(`passthrough to implementation`, async () => { @@ -255,7 +255,7 @@ describe(`errors`, () => { }) test('via passthroughErrorInstanceOf (one)', async () => { - const anyware = Anyware.create<['a'], Anyware.HookMap<['a']>>({ + const anyware = Anyware.create<['a'], Anyware.HookDefinitionMap<['a']>>({ hookNamesOrderedBySequence: [`a`], hooks: { a }, passthroughErrorInstanceOf: [SpecialError1], @@ -266,7 +266,7 @@ describe(`errors`, () => { expect(anyware.run({ initialInput: { throws: new SpecialError1('oops') }, extensions: [] })).resolves.toBeInstanceOf(SpecialError1) }) test('via passthroughErrorInstanceOf (multiple)', async () => { - const anyware = Anyware.create<['a'], Anyware.HookMap<['a']>>({ + const anyware = Anyware.create<['a'], Anyware.HookDefinitionMap<['a']>>({ hookNamesOrderedBySequence: [`a`], hooks: { a }, passthroughErrorInstanceOf: [SpecialError1, SpecialError2], @@ -277,7 +277,7 @@ describe(`errors`, () => { expect(anyware.run({ initialInput: { throws: new SpecialError2('oops') }, extensions: [] })).resolves.toBeInstanceOf(SpecialError2) }) test('via passthroughWith', async () => { - const anyware = Anyware.create<['a'], Anyware.HookMap<['a']>>({ + const anyware = Anyware.create<['a'], Anyware.HookDefinitionMap<['a']>>({ hookNamesOrderedBySequence: [`a`], hooks: { a }, // todo type-safe hook name according to values passed to constructor @@ -390,3 +390,22 @@ describe('slots', () => { expect(result).toEqual({ value: 'initial+x+y' }) }) }) + +describe('private hook parameter - previous', () => { + test('contains inputs of previous hooks', async () => { + await run(async ({ a }) => { + return a() + }) + expect(core.hooks.a.run.mock.calls[0]?.[0].previous).toEqual({}) + expect(core.hooks.b.run.mock.calls[0]?.[0].previous).toEqual({ a: { input: initialInput } }) + }) + + test('contains the final input actually passed to the hook', async () => { + const customInput = { value: 'custom' } + await run(async ({ a }) => { + return a({ input: customInput }) + }) + expect(core.hooks.a.run.mock.calls[0]?.[0].previous).toEqual({}) + expect(core.hooks.b.run.mock.calls[0]?.[0].previous).toEqual({ a: { input: customInput } }) + }) +}) diff --git a/src/lib/anyware/main.ts b/src/lib/anyware/main.ts index 223d65be..b31f9495 100644 --- a/src/lib/anyware/main.ts +++ b/src/lib/anyware/main.ts @@ -3,12 +3,14 @@ import { partitionAndAggregateErrors } from '../errors/ContextualAggregateError. import type { Deferred, FindValueAfter, IsLastValue, MaybePromise } from '../prelude.js' import { casesExhausted, createDeferred } from '../prelude.js' import { getEntrypoint } from './getEntrypoint.js' -import type { HookResultError, HookResultErrorExtension } from './runHook.js' +import type { HookDefinitionMap, HookName, HookSequence, InferDefinition } from './hook/definition.js' +import type { HookPrivateInput, HookResultErrorExtension, PrivateHook } from './hook/private.js' +import type { InferPublicHooks, SomePublicHookEnvelope } from './hook/public.js' import { runPipeline } from './runPipeline.js' -type HookSequence = readonly [string, ...string[]] +export { type HookDefinitionMap } from './hook/definition.js' -type ExtensionOptions = { +export type ExtensionOptions = { retrying: boolean } @@ -16,7 +18,7 @@ export type Extension2< $Core extends Core = Core, $Options extends ExtensionOptions = ExtensionOptions, > = ( - hooks: ExtensionHooks< + hooks: InferPublicHooks< $Core[PrivateTypesSymbol]['hookSequence'], $Core[PrivateTypesSymbol]['hookMap'], $Core[PrivateTypesSymbol]['result'], @@ -24,18 +26,9 @@ export type Extension2< >, ) => Promise< | $Core[PrivateTypesSymbol]['result'] - | SomeHookEnvelope + | SomePublicHookEnvelope > -type ExtensionHooks< - $HookSequence extends HookSequence, - $HookMap extends Record<$HookSequence[number], HookDef> = Record<$HookSequence[number], HookDef>, - $Result = unknown, - $Options extends ExtensionOptions = ExtensionOptions, -> = { - [$HookName in $HookSequence[number]]: Hook<$HookSequence, $HookMap, $Result, $HookName, $Options> -} - type CoreInitialInput<$Core extends Core> = $Core[PrivateTypesSymbol]['hookMap'][$Core[PrivateTypesSymbol]['hookSequence'][0]]['input'] @@ -43,77 +36,9 @@ const PrivateTypesSymbol = Symbol(`private`) export type PrivateTypesSymbol = typeof PrivateTypesSymbol -const hookSymbol = Symbol(`hook`) - -type HookSymbol = typeof hookSymbol - -export type SomeHookEnvelope = { - [name: string]: SomeHook -} - -export type SomeHook< - fn extends (input?: { input?: any; using?: any }) => any = (input?: { input?: any; using?: any }) => any, -> = fn & { - [hookSymbol]: HookSymbol - // todo the result is unknown, but if we build a EndEnvelope, then we can work with this type more logically and put it here. - // E.g. adding `| unknown` would destroy the knowledge of hook envelope case - // todo this is not strictly true, it could also be the final result - input: Exclude[0], undefined>['input'] -} - -export type HookMap<$HookSequence extends HookSequence> = Record< - $HookSequence[number], - HookDef -> -export type HookDef = { - input: any /* object <- type error but more accurate */ - slots?: any /* object <- type error but more accurate */ -} - -type Hook< - $HookSequence extends HookSequence, - $HookMap extends HookMap<$HookSequence> = HookMap<$HookSequence>, - $Result = unknown, - $Name extends $HookSequence[number] = $HookSequence[number], - $Options extends ExtensionOptions = ExtensionOptions, -> = - & (<$$Input extends $HookMap[$Name]['input']>( - input?: { - input?: $$Input - } & (keyof $HookMap[$Name]['slots'] extends never ? {} : { using?: SlotInputify<$HookMap[$Name]['slots']> }), - ) => HookReturn<$HookSequence, $HookMap, $Result, $Name, $Options>) - & { - [hookSymbol]: HookSymbol - input: $HookMap[$Name]['input'] - } - -type SlotInputify<$Slots extends Record any>> = { - [K in keyof $Slots]?: SlotInput<$Slots[K]> -} - -type SlotInput any> = (...args: Parameters) => ReturnType | undefined - -type HookReturn< - $HookSequence extends HookSequence, - $HookMap extends HookMap<$HookSequence> = HookMap<$HookSequence>, - $Result = unknown, - $Name extends $HookSequence[number] = $HookSequence[number], - $Options extends ExtensionOptions = ExtensionOptions, -> = Promise< - | ($Options['retrying'] extends true ? Error : never) - | (IsLastValue<$Name, $HookSequence> extends true ? $Result : { - [$NameNext in FindValueAfter<$Name, $HookSequence>]: Hook< - $HookSequence, - $HookMap, - $Result, - $NameNext - > - }) -> - export type Core< $HookSequence extends HookSequence = HookSequence, - $HookMap extends HookMap<$HookSequence> = HookMap<$HookSequence>, + $HookMap extends HookDefinitionMap<$HookSequence> = HookDefinitionMap<$HookSequence>, $Result = unknown, > = { [PrivateTypesSymbol]: { @@ -122,68 +47,36 @@ export type Core< result: $Result } hookNamesOrderedBySequence: $HookSequence + // dprint-ignore hooks: { - [$HookName in $HookSequence[number]]: { - slots: $HookMap[$HookName]['slots'] - run: (input: { - input: $HookMap[$HookName]['input'] - slots: $HookMap[$HookName]['slots'] - }) => MaybePromise< - IsLastValue<$HookName, $HookSequence> extends true ? $Result + [$HookName in $HookSequence[number]]: + PrivateHook< + $HookMap[$HookName]['slots'], + HookPrivateInput< + $HookMap[$HookName]['input'], + $HookMap[$HookName]['slots'] + >, + IsLastValue<$HookName, $HookSequence> extends true + ? $Result : $HookMap[FindValueAfter<$HookName, $HookSequence>] > - } + // [$HookName in $HookSequence[number]]: { + // slots: $HookMap[$HookName]['slots'] + // run: ( + // input: HookPrivateInput< + // $HookMap[$HookName]['input'], + // $HookMap[$HookName]['slots'] + // >, + // ) => MaybePromise< + // IsLastValue<$HookName, $HookSequence> extends true ? $Result + // : $HookMap[FindValueAfter<$HookName, $HookSequence>] + // > + // } } - passthroughErrorInstanceOf?: CoreInput['passthroughErrorInstanceOf'] - passthroughErrorWith?: CoreInput['passthroughErrorWith'] + passthroughErrorInstanceOf?: InferDefinition['passthroughErrorInstanceOf'] + passthroughErrorWith?: InferDefinition['passthroughErrorWith'] } -export type CoreInput< - $HookSequence extends HookSequence = HookSequence, - $HookMap extends HookMap<$HookSequence> = HookMap<$HookSequence>, - $Result = unknown, -> = { - hookNamesOrderedBySequence: $HookSequence - hooks: { - [$HookName in $HookSequence[number]]: keyof $HookMap[$HookName]['slots'] extends never ? (input: { - input: $HookMap[$HookName]['input'] - slots: $HookMap[$HookName]['slots'] - } - ) => MaybePromise< - IsLastValue<$HookName, $HookSequence> extends true ? $Result - : $HookMap[FindValueAfter<$HookName, $HookSequence>]['input'] - > - : { - slots: $HookMap[$HookName]['slots'] - run: (input: { - input: $HookMap[$HookName]['input'] - slots: $HookMap[$HookName]['slots'] - }) => MaybePromise< - IsLastValue<$HookName, $HookSequence> extends true ? $Result - : $HookMap[FindValueAfter<$HookName, $HookSequence>]['input'] - > - } - } - /** - * If a hook results in a thrown error but is an instance of one of these classes then return it as-is - * rather than wrapping it in a ContextualError. - * - * This can be useful when there are known kinds of errors such as Abort Errors from AbortController - * which are actually a signaling mechanism. - */ - passthroughErrorInstanceOf?: Function[] - /** - * If a hook results in a thrown error but returns true from this function then return the error as-is - * rather than wrapping it in a ContextualError. - * - * This can be useful when there are known kinds of errors such as Abort Errors from AbortController - * which are actually a signaling mechanism. - */ - passthroughErrorWith?: (signal: HookResultError) => boolean -} - -export type HookName = string - export type Extension = NonRetryingExtension | RetryingExtension export type NonRetryingExtension = { @@ -191,7 +84,7 @@ export type NonRetryingExtension = { name: string entrypoint: string body: Deferred - currentChunk: Deferred + currentChunk: Deferred } export type RetryingExtension = { @@ -199,7 +92,7 @@ export type RetryingExtension = { name: string entrypoint: string body: Deferred - currentChunk: Deferred + currentChunk: Deferred } export const createRetryingExtension = (extension: NonRetryingExtensionInput): RetryingExtensionInput => { @@ -237,7 +130,7 @@ export const createResultEnvelope = (result: T): ResultEnvelop => ({ result, }) -const createPassthrough = (hookName: string) => async (hookEnvelope: SomeHookEnvelope) => { +const createPassthrough = (hookName: string) => async (hookEnvelope: SomePublicHookEnvelope) => { const hook = hookEnvelope[hookName] if (!hook) { throw new Errors.ContextualError(`Hook not found in hook envelope`, { hookName }) @@ -274,17 +167,17 @@ export type Builder<$Core extends Core = Core> = { export const create = < $HookSequence extends HookSequence = HookSequence, - $HookMap extends HookMap<$HookSequence> = HookMap<$HookSequence>, + $HookMap extends HookDefinitionMap<$HookSequence> = HookDefinitionMap<$HookSequence>, $Result = unknown, >( - coreInput: CoreInput<$HookSequence, $HookMap, $Result>, + definition: InferDefinition<$HookSequence, $HookMap, $Result>, ): Builder> => { type $Core = Core<$HookSequence, $HookMap, $Result> const core = { - ...coreInput, + ...definition, hooks: Object.fromEntries( - Object.entries(coreInput.hooks).map(([k, v]) => { + Object.entries(definition.hooks).map(([k, v]) => { return [k, typeof v === `function` ? { slots: {}, run: v } : v] }), ), @@ -304,10 +197,11 @@ export const create = < const result = await runPipeline({ core, hookNamesOrderedBySequence: core.hookNamesOrderedBySequence, - originalInput: initialInput, - // @ts-expect-error fixme - extensionsStack: initialHookStack, + originalInputOrResult: initialInput, + // todo fix any + extensionsStack: initialHookStack as any, asyncErrorDeferred, + previous: {}, }) if (result instanceof Error) return result @@ -319,7 +213,7 @@ export const create = < } const toInternalExtension = (core: Core, config: Config, extension: ExtensionInput) => { - const currentChunk = createDeferred() + const currentChunk = createDeferred() const body = createDeferred() const extensionRun = typeof extension === `function` ? extension : extension.run const retrying = typeof extension === `function` ? false : extension.retrying diff --git a/src/lib/anyware/runHook.ts b/src/lib/anyware/runHook.ts index 555ccf08..2539723e 100644 --- a/src/lib/anyware/runHook.ts +++ b/src/lib/anyware/runHook.ts @@ -1,51 +1,20 @@ import { Errors } from '../errors/__.js' -import type { Deferred, SomeFunction } from '../prelude.js' import { casesExhausted, createDeferred, debugSub, errorFromMaybeError } from '../prelude.js' -import type { Core, Extension, ResultEnvelop, SomeHookEnvelope } from './main.js' +import type { HookResult, HookResultErrorAsync, Slots } from './hook/private.js' +import { createPublicHook, type SomePublicHookEnvelope } from './hook/public.js' +import type { Core, Extension, ResultEnvelop } from './main.js' type HookDoneResolver = (input: HookResult) => void -export type HookResultErrorAsync = Deferred - -export type HookResult = - | { type: 'completed'; result: unknown; nextExtensionsStack: readonly Extension[] } - | { type: 'shortCircuited'; result: unknown } - | HookResultErrorUser - | HookResultErrorImplementation - | HookResultErrorExtension - -export type HookResultError = HookResultErrorExtension | HookResultErrorImplementation | HookResultErrorUser - -export type HookResultErrorUser = { - type: 'error' - hookName: string - source: 'user' - error: Errors.ContextualError - extensionName: string -} - -export type HookResultErrorExtension = { - type: 'error' - hookName: string - source: 'extension' - error: Error - extensionName: string -} - -export type HookResultErrorImplementation = { - type: 'error' - hookName: string - source: 'implementation' - error: Error -} - -type Slots = Record - -type Input = { +interface Input { core: Core name: string done: HookDoneResolver - originalInput: unknown + inputOriginalOrFromExtension: object + /** + * Information about previous hook executions, like what their input was. + */ + previous: object customSlots: Slots /** * The extensions that are at this hook awaiting. @@ -64,11 +33,21 @@ type Input = { const createExecutableChunk = <$Extension extends Extension>(extension: $Extension) => ({ ...extension, - currentChunk: createDeferred(), + currentChunk: createDeferred(), }) export const runHook = async ( - { core, name, done, originalInput, extensionsStack, nextExtensionsStack, asyncErrorDeferred, customSlots }: Input, + { + core, + name, + done, + inputOriginalOrFromExtension, + previous, + extensionsStack, + nextExtensionsStack, + asyncErrorDeferred, + customSlots, + }: Input, ) => { const debugHook = debugSub(`hook ${name}:`) @@ -100,10 +79,10 @@ export const runHook = async ( debugExtension(`start`) let hookFailed = false - const hook = createHook(originalInput, (extensionInput) => { + const hook = createPublicHook(inputOriginalOrFromExtension, (extensionInput) => { debugExtension(`extension calls this hook`, extensionInput) - const inputResolved = extensionInput?.input ?? originalInput + const inputResolved = extensionInput?.input ?? inputOriginalOrFromExtension const customSlotsResolved = { ...customSlots, ...extensionInput?.using, @@ -148,20 +127,22 @@ export const runHook = async ( core, name, done, - originalInput, + previous, + inputOriginalOrFromExtension, asyncErrorDeferred, extensionsStack: [extensionRetry], nextExtensionsStack, customSlots: customSlotsResolved, }) return extensionRetry.currentChunk.promise.then(async (envelope) => { - const envelop_ = envelope as SomeHookEnvelope // todo ... better way? - const hook = envelop_[name] + const envelop_ = envelope as SomePublicHookEnvelope // todo ... better way? + const hook = envelop_[name] // as (params:{input:object;previous:object;using:Slots}) => if (!hook) throw new Error(`Hook not found in envelope: ${name}`) // todo use inputResolved ? - const result = await hook({ ...extensionInput, input: extensionInput?.input ?? originalInput }) as Promise< - SomeHookEnvelope | Error | ResultEnvelop - > + const result = await hook({ + ...extensionInput, + input: extensionInput?.input ?? inputOriginalOrFromExtension, + }) as Promise return result }) } @@ -173,8 +154,9 @@ export const runHook = async ( core, name, done, + previous, asyncErrorDeferred, - originalInput: inputResolved, + inputOriginalOrFromExtension: inputResolved, extensionsStack: extensionsStackRest, nextExtensionsStack: nextNextHookStack, customSlots: customSlotsResolved, @@ -227,7 +209,8 @@ export const runHook = async ( core, name, done, - originalInput, + previous, + inputOriginalOrFromExtension, asyncErrorDeferred, extensionsStack: extensionsStackRest, nextExtensionsStack, @@ -276,7 +259,11 @@ export const runHook = async ( ...implementation.slots as Slots, // todo is this cast needed, can we Slots type the property? ...customSlots, } - result = await implementation.run({ input: originalInput, slots: slotsResolved }) + result = await implementation.run({ + input: inputOriginalOrFromExtension, + slots: slotsResolved, + previous: previous, + }) } catch (error) { debugHook(`implementation error`) const lastExtension = nextExtensionsStack[nextExtensionsStack.length - 1] @@ -292,21 +279,11 @@ export const runHook = async ( debugHook(`completed`) - done({ type: `completed`, result, nextExtensionsStack: nextExtensionsStack }) + done({ + type: `completed`, + result, + effectiveInput: inputOriginalOrFromExtension, + nextExtensionsStack: nextExtensionsStack, + }) } } - -const createHook = <$X, $F extends (input?: HookInput) => any>( - originalInput: $X, - fn: $F, -): $F & { input: $X } => { - // @ts-expect-error - fn.input = originalInput - // @ts-expect-error - return fn -} - -type HookInput = { - input?: object - using?: Slots -} diff --git a/src/lib/anyware/runPipeline.ts b/src/lib/anyware/runPipeline.ts index d89c7661..03720bfc 100644 --- a/src/lib/anyware/runPipeline.ts +++ b/src/lib/anyware/runPipeline.ts @@ -1,26 +1,29 @@ import type { Errors } from '../errors/__.js' import { ContextualError } from '../errors/ContextualError.js' import { casesExhausted, createDeferred, debug } from '../prelude.js' +import type { HookResult, HookResultErrorAsync } from './hook/private.js' import { defaultFunctionName } from './lib.js' import type { Core, Extension, ResultEnvelop } from './main.js' import { createResultEnvelope } from './main.js' -import type { HookResult, HookResultErrorAsync } from './runHook.js' import { runHook } from './runHook.js' +interface Input { + core: Core + hookNamesOrderedBySequence: readonly string[] + originalInputOrResult: unknown + extensionsStack: readonly Extension[] + asyncErrorDeferred: HookResultErrorAsync + previous: object +} + export const runPipeline = async ( - { core, hookNamesOrderedBySequence, originalInput, extensionsStack, asyncErrorDeferred }: { - core: Core - hookNamesOrderedBySequence: readonly string[] - originalInput: unknown - extensionsStack: readonly Extension[] - asyncErrorDeferred: HookResultErrorAsync - }, + { core, hookNamesOrderedBySequence, originalInputOrResult, extensionsStack, asyncErrorDeferred, previous }: Input, ): Promise => { const [hookName, ...hookNamesRest] = hookNamesOrderedBySequence if (!hookName) { debug(`pipeline: ending`) - const result = await runPipelineEnd({ extensionsStack, result: originalInput }) + const result = await runPipelineEnd({ extensionsStack, result: originalInputOrResult }) debug(`pipeline: returning`) return createResultEnvelope(result) } @@ -33,7 +36,8 @@ export const runPipeline = async ( core, name: hookName, done: done.resolve, - originalInput, + inputOriginalOrFromExtension: originalInputOrResult as object, + previous, extensionsStack, asyncErrorDeferred, customSlots: {}, @@ -46,12 +50,19 @@ export const runPipeline = async ( switch (signal.type) { case `completed`: { - const { result, nextExtensionsStack } = signal + const { result, effectiveInput, nextExtensionsStack } = signal + const nextPrevious = { + ...previous, + [hookName]: { + input: effectiveInput, + }, + } return await runPipeline({ core, hookNamesOrderedBySequence: hookNamesRest, - originalInput: result, + originalInputOrResult: result, extensionsStack: nextExtensionsStack, + previous: nextPrevious, asyncErrorDeferred, }) } diff --git a/src/lib/anyware/specHelpers.ts b/src/lib/anyware/specHelpers.ts index 68e8f3ab..dd575414 100644 --- a/src/lib/anyware/specHelpers.ts +++ b/src/lib/anyware/specHelpers.ts @@ -3,24 +3,27 @@ import { beforeEach, vi } from 'vitest' import { Anyware } from './__.js' import { type ExtensionInput, type Options } from './main.js' -export type Input = { +type PrivateHookRunnerInput = { input: { value: string } slots: { append: (hookName: string) => string; appendExtra: (hookName: string) => string } + previous: object } -export const initialInput: Input['input'] = { value: `initial` } +type PrivateHookRunner = (input: PrivateHookRunnerInput) => any + +export const initialInput: PrivateHookRunnerInput['input'] = { value: `initial` } type $Core = ReturnType & { hooks: { a: { - run: Mock + run: Mock slots: { append: Mock<(hookName: string) => string> appendExtra: Mock<(hookName: string) => string> } } b: { - run: Mock + run: Mock slots: { append: Mock<(hookName: string) => string> appendExtra: Mock<(hookName: string) => string> @@ -46,7 +49,7 @@ export const createAnyware = () => { return `` }), }, - run: vi.fn().mockImplementation(({ input, slots }: Input) => { + run: vi.fn().mockImplementation(({ input, slots }: PrivateHookRunnerInput) => { const extra = slots.appendExtra(`a`) return { value: input.value + `+` + slots.append(`a`) + extra } }), @@ -60,13 +63,13 @@ export const createAnyware = () => { return `` }), }, - run: vi.fn().mockImplementation(({ input, slots }: Input) => { + run: vi.fn().mockImplementation(({ input, slots }: PrivateHookRunnerInput) => { const extra = slots.appendExtra(`b`) return { value: input.value + `+` + slots.append(`b`) + extra } }), }) - return Anyware.create<['a', 'b'], Anyware.HookMap<['a', 'b']>, Input>({ + return Anyware.create<['a', 'b'], Anyware.HookDefinitionMap<['a', 'b']>, PrivateHookRunnerInput>({ hookNamesOrderedBySequence: [`a`, `b`], hooks: { a, b }, }) diff --git a/src/lib/grafaid/_.ts b/src/lib/grafaid/_.ts new file mode 100644 index 00000000..971fcc91 --- /dev/null +++ b/src/lib/grafaid/_.ts @@ -0,0 +1,4 @@ +export { $Schema as Schema } from './document.js' +export * as Nodes from './document.js' +export * from './graphql.js' +export * as HTTP from './http/http.js' diff --git a/src/lib/grafaid/_Nodes.ts b/src/lib/grafaid/_Nodes.ts new file mode 100644 index 00000000..22f922a0 --- /dev/null +++ b/src/lib/grafaid/_Nodes.ts @@ -0,0 +1 @@ +export * as Nodes from './document.js' diff --git a/src/lib/grafaid/__.ts b/src/lib/grafaid/__.ts new file mode 100644 index 00000000..8c894ca8 --- /dev/null +++ b/src/lib/grafaid/__.ts @@ -0,0 +1 @@ +export * as Grafaid from './_.js' diff --git a/src/lib/graphql-plus/nodes.ts b/src/lib/grafaid/document.ts similarity index 89% rename from src/lib/graphql-plus/nodes.ts rename to src/lib/grafaid/document.ts index d13330a7..58a2e015 100644 --- a/src/lib/graphql-plus/nodes.ts +++ b/src/lib/grafaid/document.ts @@ -16,6 +16,7 @@ import { type ObjectFieldNode, type ObjectValueNode, type OperationDefinitionNode, + print as graphqlPrint, type SelectionSetNode, type StringValueNode, type ValueNode, @@ -23,6 +24,8 @@ import { type VariableNode, } from 'graphql' import type { HasRequiredKeys } from 'type-fest' +import { isString } from '../prelude.js' +import { TypedDocument } from './typed-document/__.js' export type { ArgumentNode, @@ -49,9 +52,11 @@ export type { export { Kind } from 'graphql' +export * as Typed from './typed-document/TypedDocument.js' + export { getNamedType } from 'graphql' -export * as $Schema from './nodesSchema.js' +export * as $Schema from './schema/schema.js' export type $Any = | DirectiveNode @@ -234,3 +239,14 @@ export const ObjectField: Constructor = (objectField) => { ...objectField, } } + +export const OperationTypeToAccessKind = { + query: `read`, + mutation: `write`, + subscription: `read`, +} as const + +export const print = (document: TypedDocument.TypedDocument): string => { + const documentUntyped = TypedDocument.unType(document) + return isString(documentUntyped) ? documentUntyped : graphqlPrint(documentUntyped) +} diff --git a/src/lib/graphql-plus/execute.ts b/src/lib/grafaid/execute.ts similarity index 84% rename from src/lib/graphql-plus/execute.ts rename to src/lib/grafaid/execute.ts index 407ea232..8a17abe0 100644 --- a/src/lib/graphql-plus/execute.ts +++ b/src/lib/grafaid/execute.ts @@ -1,10 +1,10 @@ import type { ExecutionResult, GraphQLSchema } from 'graphql' import { execute as graphqlExecute, graphql } from 'graphql' -import { TypedDocument } from '../typed-document/__.js' -import type { GraphQLRequestInput } from './graphql.js' +import type { RequestInput } from './graphql.js' +import { TypedDocument } from './typed-document/__.js' export type ExecuteInput = { - request: GraphQLRequestInput + request: RequestInput schema: GraphQLSchema } diff --git a/src/lib/graphql-plus/graphql.test.ts b/src/lib/grafaid/graphql.test.ts similarity index 78% rename from src/lib/graphql-plus/graphql.test.ts rename to src/lib/grafaid/graphql.test.ts index bdea6e4d..5f14dd87 100644 --- a/src/lib/graphql-plus/graphql.test.ts +++ b/src/lib/grafaid/graphql.test.ts @@ -1,5 +1,6 @@ +import { OperationTypeNode } from 'graphql' import { describe, expect, test } from 'vitest' -import { type GraphQLRequestInput, type OperationTypeNameAll, parseGraphQLOperationType } from './graphql.js' +import { Grafaid } from './__.js' const operationNameOne = `one` const operationNameTwo = `two` @@ -10,8 +11,8 @@ const docOverloadedTerms = `query { queryX }` type CaseParameters = [ description: string, - request: GraphQLRequestInput, - result: null | OperationTypeNameAll, + request: Grafaid.RequestInput, + result: null | OperationTypeNode, ] describe(`parseGraphQLOperationType`, () => { @@ -22,13 +23,13 @@ describe(`parseGraphQLOperationType`, () => { [ `null if multiple defined operations and no operation name given`, { query: docMultipleQueryOperations }, null ], [ `null if multiple defined operations and no operation name given (empty string)`, { query: docMultipleQueryOperations, operationName: `` }, null ], [ `null if multiple defined operations and operation name given not found`, { query: docMultipleQueryOperations, operationName: `foo` }, null ], - [ `assume query if no defined operations and no operation name given `, { query: docNoDefinedOps }, `query` ], - [ `query if multiple defined query operations and no query operation name given `, { query: docMultipleQueryOperations, operationName: operationNameOne }, `query` ], - [ `query if multiple defined mixed operations and no mutation operation name given `, { query: docMultipleMixedOperations, operationName: operationNameTwo }, `query` ], - [ `mutation if multiple defined mixed operations and no query operation name given `, { query: docMultipleMixedOperations, operationName: operationNameOne }, `mutation` ], - [ `mutation if only operation without name and no operation given `, { query: `mutation { user { name } }` }, `mutation` ], - [ `overloaded terms do not confuse parser`, { query: docOverloadedTerms }, `query` ], + [ `assume query if no defined operations and no operation name given `, { query: docNoDefinedOps }, OperationTypeNode.QUERY ], + [ `query if multiple defined query operations and no query operation name given `, { query: docMultipleQueryOperations, operationName: operationNameOne }, OperationTypeNode.QUERY ], + [ `query if multiple defined mixed operations and no mutation operation name given `, { query: docMultipleMixedOperations, operationName: operationNameTwo }, OperationTypeNode.QUERY ], + [ `mutation if multiple defined mixed operations and no query operation name given `, { query: docMultipleMixedOperations, operationName: operationNameOne }, OperationTypeNode.MUTATION ], + [ `mutation if only operation without name and no operation given `, { query: `mutation { user { name } }` }, OperationTypeNode.MUTATION ], + [ `overloaded terms do not confuse parser`, { query: docOverloadedTerms }, OperationTypeNode.QUERY ], ])(`%s`, (_, request, result) => { - expect(parseGraphQLOperationType(request)).toEqual(result) + expect(Grafaid.parseOperationType(request)).toEqual(result) }) }) diff --git a/src/lib/graphql-plus/graphql.ts b/src/lib/grafaid/graphql.ts similarity index 51% rename from src/lib/graphql-plus/graphql.ts rename to src/lib/grafaid/graphql.ts index 94934d37..f795073e 100644 --- a/src/lib/graphql-plus/graphql.ts +++ b/src/lib/grafaid/graphql.ts @@ -1,12 +1,4 @@ -import type { - GraphQLArgument, - GraphQLEnumValue, - GraphQLError, - GraphQLField, - GraphQLInputField, - GraphQLNamedType, - GraphQLSchema, -} from 'graphql' +import type { GraphQLEnumValue, GraphQLError, GraphQLField, GraphQLInputField, GraphQLNamedType } from 'graphql' import { GraphQLEnumType, GraphQLInputObjectType, @@ -19,7 +11,6 @@ import { isEnumType, isInputObjectType, isInterfaceType, - isNonNullType, isObjectType, isScalarType, isUnionType, @@ -27,19 +18,11 @@ import { } from 'graphql' import type { Errors } from '../errors/__.js' import { isString } from '../prelude.js' -import type { TypedDocument } from '../typed-document/__.js' -import { isScalarTypeAndCustom } from './nodesSchema.js' +import { Nodes } from './_Nodes.js' +import { TypedDocument } from './typed-document/__.js' export * from './_Nodes.js' -export type TypeMapByKind = - & { - [Name in keyof NameToClassNamedType]: InstanceType[] - } - & { GraphQLRootType: GraphQLObjectType[] } - & { GraphQLScalarTypeCustom: GraphQLScalarType[] } - & { GraphQLScalarTypeStandard: GraphQLScalarType[] } - export const StandardScalarTypeNames = { String: `String`, ID: `ID`, @@ -72,20 +55,6 @@ export type TypeNamedKind = `Enum` | `InputObject` | `Interface` | `Object` | `S export type TypeMapKind = TypeNamedKind | `Root` -export const RootTypeName = { - Query: `Query`, - Mutation: `Mutation`, - Subscription: `Subscription`, -} as const - -export const isRootType = (value: unknown): value is GraphQLObjectType => { - return isObjectType(value) && value.name in RootTypeName -} - -export type RootTypeNameQuery = typeof RootTypeName['Query'] -export type RootTypeNameMutation = typeof RootTypeName['Mutation'] -export type RootTypeNameSubscription = typeof RootTypeName['Subscription'] - export const operationTypeNameToRootTypeName = { query: `Query`, mutation: `Mutation`, @@ -100,64 +69,10 @@ export const RootTypeNameToOperationName = { export type RootTypeNameToOperationName = typeof RootTypeNameToOperationName -export type RootTypeName = keyof typeof RootTypeName - export const isStandardScalarType = (type: GraphQLScalarType) => { return type.name in StandardScalarTypeNames } -export const getTypeMapByKind = (schema: GraphQLSchema) => { - const typeMap = schema.getTypeMap() - const typeMapValues = Object.values(typeMap) - const typeMapByKind: TypeMapByKind = { - GraphQLRootType: [], - GraphQLScalarType: [], - GraphQLScalarTypeCustom: [], - GraphQLScalarTypeStandard: [], - GraphQLEnumType: [], - GraphQLInputObjectType: [], - GraphQLInterfaceType: [], - GraphQLObjectType: [], - GraphQLUnionType: [], - } - for (const type of typeMapValues) { - if (type.name.startsWith(`__`)) continue - switch (true) { - case type instanceof GraphQLScalarType: - typeMapByKind.GraphQLScalarType.push(type) - if (isScalarTypeAndCustom(type)) { - typeMapByKind.GraphQLScalarTypeCustom.push(type) - } else { - typeMapByKind.GraphQLScalarTypeStandard.push(type) - } - break - case type instanceof GraphQLEnumType: - typeMapByKind.GraphQLEnumType.push(type) - break - case type instanceof GraphQLInputObjectType: - typeMapByKind.GraphQLInputObjectType.push(type) - break - case type instanceof GraphQLInterfaceType: - typeMapByKind.GraphQLInterfaceType.push(type) - break - case type instanceof GraphQLObjectType: - if (type.name === `Query` || type.name === `Mutation` || type.name === `Subscription`) { - typeMapByKind.GraphQLRootType.push(type) - } else { - typeMapByKind.GraphQLObjectType.push(type) - } - break - case type instanceof GraphQLUnionType: - typeMapByKind.GraphQLUnionType.push(type) - break - default: - // skip - break - } - } - return typeMapByKind -} - export type ClassToName = C extends GraphQLScalarType ? `GraphQLScalarType` : C extends GraphQLObjectType ? `GraphQLObjectType` : C extends GraphQLInterfaceType ? `GraphQLInterfaceType` @@ -168,17 +83,6 @@ export type ClassToName = C extends GraphQLScalarType ? `GraphQLScalarType` : C extends GraphQLNonNull ? `GraphQLNonNull` : never -export const NameToClassNamedType = { - GraphQLScalarType: GraphQLScalarType, - GraphQLObjectType: GraphQLObjectType, - GraphQLInterfaceType: GraphQLInterfaceType, - GraphQLUnionType: GraphQLUnionType, - GraphQLEnumType: GraphQLEnumType, - GraphQLInputObjectType: GraphQLInputObjectType, -} - -export type NameToClassNamedType = typeof NameToClassNamedType - export const NamedNameToClass = { GraphQLScalarType: GraphQLScalarType, GraphQLObjectType: GraphQLObjectType, @@ -210,14 +114,6 @@ export type AnyNamedClassName = keyof NamedNameToClass export type AnyClass = InstanceType -export const isGraphQLOutputField = (object: object): object is AnyGraphQLOutputField => { - return `args` in object -} - -export const hasCustomScalars = (typeMapByKind: TypeMapByKind) => { - return typeMapByKind.GraphQLScalarTypeCustom.length > 0 -} - /** * Groups */ @@ -226,7 +122,7 @@ export type Describable = | GraphQLNamedType | AnyField -export const getNodeNameAndKind = ( +export const getTypeNameAndKind = ( node: GraphQLNamedType, ): { name: string; kind: 'Object' | 'Interface' | 'Union' | 'Enum' | 'Scalar' } => { const name = node.name @@ -239,7 +135,7 @@ export const getNodeNameAndKind = ( return { name, kind } } -export const getNodeKind = <$Node extends GraphQLNamedType>(node: $Node): ClassToName<$Node> => { +export const getTypeKind = <$Node extends GraphQLNamedType>(node: $Node): ClassToName<$Node> => { switch (true) { case isObjectType(node): return `GraphQLObjectType` as ClassToName<$Node> @@ -277,17 +173,6 @@ export const getNodeKindOld = (node: Describable): NodeNamePlus => { } } -// const displayNames = { -// GraphQLEnumType: `Enum`, -// GraphQLInputObjectType: `InputObject`, -// GraphQLInterfaceType: `Interface`, -// GraphQLList: `List`, -// GraphQLNonNull: `NonNull`, -// GraphQLObjectType: `Object`, -// GraphQLScalarType: `Scalar`, -// GraphQLUnionType: `Union`, -// } satisfies Record - export const getNodeDisplayName = (node: Describable) => { return toDisplayName(getNodeKindOld(node)) } @@ -300,43 +185,7 @@ export const isDeprecatableNode = (node: object): node is GraphQLEnumValue | Any return `deprecationReason` in node } -export const hasQuery = (typeMapByKind: TypeMapByKind) => typeMapByKind.GraphQLRootType.find((_) => _.name === `Query`) - -export const hasMutation = (typeMapByKind: TypeMapByKind) => - typeMapByKind.GraphQLRootType.find((_) => _.name === `Mutation`) - -export const hasSubscription = (typeMapByKind: TypeMapByKind) => - typeMapByKind.GraphQLRootType.find((_) => _.name === `Subscription`) - -export const OperationTypes = { - query: `query`, - mutation: `mutation`, - subscription: `subscription`, -} as const - -export namespace OperationType { - export type Query = typeof OperationTypes['query'] - export type Mutation = typeof OperationTypes['mutation'] - export type Subscription = typeof OperationTypes['subscription'] -} - -type OperationTypeQuery = typeof OperationTypes['query'] -type OperationTypeMutation = typeof OperationTypes['mutation'] -type OperationTypeSubscription = typeof OperationTypes['subscription'] - -export type OperationTypeName = OperationTypeQuery | OperationTypeMutation -export type OperationTypeNameAll = OperationTypeName | OperationTypeSubscription - -export const OperationTypeAccessTypeMap = { - query: `read`, - mutation: `write`, - subscription: `read`, -} as const - -export const isOperationTypeName = (value: unknown): value is OperationTypeName => - value === `query` || value === `mutation` - -export interface GraphQLRequestInput { +export interface RequestInput { query: string | TypedDocument.TypedDocument variables?: Variables operationName?: string @@ -350,20 +199,30 @@ export type SomeData = Record export type GraphQLExecutionResultError = Errors.ContextualAggregateError -const definedOperationPattern = new RegExp(`^\\b(${Object.values(OperationTypes).join(`|`)})\\b`) - -export const parseGraphQLOperationType = (request: GraphQLRequestInput): OperationTypeNameAll | null => { - // todo support DocumentNode too - if (!isString(request.query)) return null +const definedOperationPattern = new RegExp(`^\\b(${Object.values(OperationTypeNode).join(`|`)})\\b`) +export const parseOperationType = (request: RequestInput): OperationTypeNode | null => { const { operationName, query: document } = request - const definedOperations = document.split(/[{}\n]+/).map(s => s.trim()).map(line => { + const documentUntyped = TypedDocument.unType(document) + + if (!isString(documentUntyped)) { + for (const node of documentUntyped.definitions) { + if (node.kind === Nodes.Kind.OPERATION_DEFINITION) { + if (operationName ? node.name?.value === operationName : true) { + return node.operation + } + } + } + throw new Error(`Could not parse operation type from document.`) + } + + const definedOperations = documentUntyped.split(/[{}\n]+/).map(s => s.trim()).map(line => { const match = line.match(definedOperationPattern) if (!match) return null return { line, - operationType: match[0] as OperationTypeNameAll, + operationType: match[0] as OperationTypeNode, } }).filter(_ => _ !== null) // console.log(definedOperations) @@ -382,7 +241,7 @@ export const parseGraphQLOperationType = (request: GraphQLRequestInput): Operati // Assume that the implicit query syntax is being used. // This is a non-validated optimistic approach for performance, not aimed at correctness. // For example its not checked if the document is actually of the syntactic form `{ ... }` - return OperationTypes.query + return OperationTypeNode.QUERY } // Continue to the full search. @@ -396,34 +255,3 @@ export const parseGraphQLOperationType = (request: GraphQLRequestInput): Operati return definedOperationToAnalyze.operationType } - -export const isAllArgsNonNullType = (args: readonly GraphQLArgument[]) => { - return args.every(_ => isNonNullType(_.type)) -} - -export const isAllArgsNullable = (args: readonly GraphQLArgument[]) => { - return !args.some(_ => isNonNullType(_.type)) -} -export const analyzeArgsNullability = (args: readonly GraphQLArgument[]) => { - let required = 0 - let optional = 0 - const total = args.length - args.forEach(_ => { - if (isNonNullType(_.type)) { - required++ - } else { - optional++ - } - }) - return { - hasAny: total > 0, - isAllNullable: optional === total, - required, - optional, - total, - } -} - -export const isAllInputObjectFieldsNullable = (node: GraphQLInputObjectType) => { - return Object.values(node.getFields()).some(_ => !isNonNullType(_.type)) -} diff --git a/src/lib/grafaid/http/__.ts b/src/lib/grafaid/http/__.ts new file mode 100644 index 00000000..0ae926b3 --- /dev/null +++ b/src/lib/grafaid/http/__.ts @@ -0,0 +1 @@ +export * as GraphQLHTTP from './http.js' diff --git a/src/lib/graphql-http/graphqlHTTP.ts b/src/lib/grafaid/http/http.ts similarity index 89% rename from src/lib/graphql-http/graphqlHTTP.ts rename to src/lib/grafaid/http/http.ts index de871a4a..74054291 100644 --- a/src/lib/graphql-http/graphqlHTTP.ts +++ b/src/lib/grafaid/http/http.ts @@ -1,10 +1,10 @@ import type { GraphQLFormattedError } from 'graphql' import { type ExecutionResult, GraphQLError } from 'graphql' -import type { Variables } from '../graphql-plus/graphql.js' -import { CONTENT_TYPE_GQL, CONTENT_TYPE_JSON } from '../http.js' -import { isRecordLikeObject } from '../prelude.js' +import { CONTENT_TYPE_GQL, CONTENT_TYPE_JSON } from '../../http.js' +import { isRecordLikeObject } from '../../prelude.js' +import type { Variables } from '../graphql.js' -export interface RequestInput { +export interface RequestConfig { query: string variables?: Variables operationName?: string @@ -75,7 +75,7 @@ export const getRequestHeadersRec = { accept: ACCEPT_REC, } -export const getRequestEncodeSearchParameters = (request: RequestInput): Record => { +export const getRequestEncodeSearchParameters = (request: RequestConfig): Record => { return { query: request.query, ...(request.variables ? { variables: JSON.stringify(request.variables) } : {}), @@ -84,7 +84,7 @@ export const getRequestEncodeSearchParameters = (request: RequestInput): Record< } export type getRequestEncodeSearchParameters = typeof getRequestEncodeSearchParameters -export const postRequestEncodeBody = (input: RequestInput): BodyInit => { +export const postRequestEncodeBody = (input: RequestConfig): BodyInit => { return JSON.stringify({ query: input.query, variables: input.variables, diff --git a/src/lib/grafaid/schema/args.ts b/src/lib/grafaid/schema/args.ts new file mode 100644 index 00000000..69f97c10 --- /dev/null +++ b/src/lib/grafaid/schema/args.ts @@ -0,0 +1,29 @@ +import { type GraphQLArgument, isNonNullType } from 'graphql' + +export const isAllArgsNonNullType = (args: readonly GraphQLArgument[]) => { + return args.every(_ => isNonNullType(_.type)) +} + +export const isAllArgsNullable = (args: readonly GraphQLArgument[]) => { + return !args.some(_ => isNonNullType(_.type)) +} + +export const analyzeArgsNullability = (args: readonly GraphQLArgument[]) => { + let required = 0 + let optional = 0 + const total = args.length + args.forEach(_ => { + if (isNonNullType(_.type)) { + required++ + } else { + optional++ + } + }) + return { + hasAny: total > 0, + isAllNullable: optional === total, + required, + optional, + total, + } +} diff --git a/src/lib/grafaid/schema/customScalars.ts b/src/lib/grafaid/schema/customScalars.ts new file mode 100644 index 00000000..6121841a --- /dev/null +++ b/src/lib/grafaid/schema/customScalars.ts @@ -0,0 +1,123 @@ +import { + getNamedType, + type GraphQLArgument, + type GraphQLField, + type GraphQLInputField, + isEnumType, + isInputObjectType, + isInterfaceType, + isObjectType, + isScalarType, + isUnionType, +} from 'graphql' +import type { GraphQLInputObjectType, GraphQLNamedOutputType } from 'graphql' +import { casesExhausted } from '../../prelude.js' +import { isGraphQLArgumentOrInputField, isGraphQLField, isScalarTypeAndCustom } from './schema.js' + +export const isHasCustomScalars = ( + node: GraphQLNamedOutputType | GraphQLField | GraphQLInputObjectType, +): boolean => { + if (isInputObjectType(node)) { + return isHasCustomScalarInputs(node) + } + + return isHasCustomScalarOutputs(node) || isHasCustomScalarInputs(node) +} + +export const isHasCustomScalarOutputs = ( + node: + | GraphQLNamedOutputType + | GraphQLField, +): boolean => { + return isHasCustomScalarOutputs_(node, []) +} + +const isHasCustomScalarOutputs_ = ( + node: GraphQLNamedOutputType | GraphQLField, + typePath: string[], +): boolean => { + if (isGraphQLField(node)) { + const fieldType = getNamedType(node.type) + return isHasCustomScalarOutputs_(fieldType, typePath) + } + + if (isEnumType(node)) { + return false + } + + if (isScalarType(node)) { + return isScalarTypeAndCustom(node) + } + + if (typePath.includes(node.name)) { + // End Via Short-Circuit: We've already come from this type. + return false + } else { + typePath = [...typePath, node.name] + } + + if (isObjectType(node) || isInterfaceType(node)) { + return Object.values(node.getFields()).some(field => isHasCustomScalarOutputs_(field, typePath)) + } + + if (isUnionType(node)) { + return node.getTypes().some(type => isHasCustomScalarOutputs_(type, typePath)) + } + + throw casesExhausted(node) +} + +export const isHasCustomScalarInputs = ( + node: + | GraphQLNamedOutputType + | GraphQLInputObjectType + | GraphQLInputField + | GraphQLArgument + | GraphQLField, +): boolean => { + return isHasCustomScalarInputs_(node, []) +} + +const isHasCustomScalarInputs_ = ( + node: GraphQLInputObjectType | GraphQLNamedOutputType | GraphQLArgument | GraphQLField, + typePath: string[], +): boolean => { + if (isGraphQLArgumentOrInputField(node)) { + return isHasCustomScalarInputs_(getNamedType(node.type), typePath) + } + + if (isGraphQLField(node)) { + const fieldType = getNamedType(node.type) + return node.args.some(arg => isHasCustomScalarInputs_(arg, typePath)) + || (isObjectType(fieldType) && isHasCustomScalarInputs_(fieldType, typePath)) + } + + if (isEnumType(node)) { + return false + } + + if (isScalarType(node)) { + return isScalarTypeAndCustom(node) + } + + if (typePath.includes(node.name)) { + // End Via Short-Circuit: We've already come from this type. + return false + } else { + typePath = [...typePath, node.name] + } + + if (isInputObjectType(node)) { + return Object.values(node.getFields()).some(field => isHasCustomScalarInputs_(field, typePath)) + } + + if (isObjectType(node) || isInterfaceType(node)) { + return Object.values(node.getFields()).some(field => isHasCustomScalarInputs_(field, typePath)) + } + + if (isUnionType(node)) { + return false + } + + throw casesExhausted(node) +} diff --git a/src/lib/grafaid/schema/kindMap.ts b/src/lib/grafaid/schema/kindMap.ts new file mode 100644 index 00000000..e5d20f5c --- /dev/null +++ b/src/lib/grafaid/schema/kindMap.ts @@ -0,0 +1,87 @@ +import { + GraphQLEnumType, + GraphQLInputObjectType, + GraphQLInterfaceType, + GraphQLObjectType, + GraphQLScalarType, + type GraphQLSchema, + GraphQLUnionType, +} from 'graphql' +import { isScalarTypeAndCustom, type NameToClassNamedType } from './schema.js' + +export type KindMap = + & { + [Name in keyof NameToClassNamedType]: InstanceType[] + } + & { GraphQLRootType: GraphQLObjectType[] } + & { GraphQLScalarTypeCustom: GraphQLScalarType[] } + & { GraphQLScalarTypeStandard: GraphQLScalarType[] } + +export const getKindMap = (schema: GraphQLSchema): KindMap => { + const typeMap = schema.getTypeMap() + const typeMapValues = Object.values(typeMap) + const typeMapByKind: KindMap = { + GraphQLRootType: [], + GraphQLScalarType: [], + GraphQLScalarTypeCustom: [], + GraphQLScalarTypeStandard: [], + GraphQLEnumType: [], + GraphQLInputObjectType: [], + GraphQLInterfaceType: [], + GraphQLObjectType: [], + GraphQLUnionType: [], + } + for (const type of typeMapValues) { + if (type.name.startsWith(`__`)) continue + switch (true) { + case type instanceof GraphQLScalarType: + typeMapByKind.GraphQLScalarType.push(type) + if (isScalarTypeAndCustom(type)) { + typeMapByKind.GraphQLScalarTypeCustom.push(type) + } else { + typeMapByKind.GraphQLScalarTypeStandard.push(type) + } + break + case type instanceof GraphQLEnumType: + typeMapByKind.GraphQLEnumType.push(type) + break + case type instanceof GraphQLInputObjectType: + typeMapByKind.GraphQLInputObjectType.push(type) + break + case type instanceof GraphQLInterfaceType: + typeMapByKind.GraphQLInterfaceType.push(type) + break + case type instanceof GraphQLObjectType: + if (type.name === `Query` || type.name === `Mutation` || type.name === `Subscription`) { + typeMapByKind.GraphQLRootType.push(type) + } else { + typeMapByKind.GraphQLObjectType.push(type) + } + break + case type instanceof GraphQLUnionType: + typeMapByKind.GraphQLUnionType.push(type) + break + default: + // skip + break + } + } + return typeMapByKind +} + +export const hasMutation = (typeMapByKind: KindMap) => typeMapByKind.GraphQLRootType.find((_) => _.name === `Mutation`) + +export const hasSubscription = (typeMapByKind: KindMap) => + typeMapByKind.GraphQLRootType.find((_) => _.name === `Subscription`) + +export const hasQuery = (typeMapByKind: KindMap) => typeMapByKind.GraphQLRootType.find((_) => _.name === `Query`) + +export const getInterfaceImplementors = (typeMap: KindMap, interfaceTypeSearch: GraphQLInterfaceType) => { + return typeMap.GraphQLObjectType.filter(objectType => + objectType.getInterfaces().filter(interfaceType => interfaceType.name === interfaceTypeSearch.name).length > 0 + ) +} + +export const hasCustomScalars = (typeMapByKind: KindMap) => { + return typeMapByKind.GraphQLScalarTypeCustom.length > 0 +} diff --git a/src/lib/grafaid/schema/schema.ts b/src/lib/grafaid/schema/schema.ts new file mode 100644 index 00000000..c5542dc1 --- /dev/null +++ b/src/lib/grafaid/schema/schema.ts @@ -0,0 +1,91 @@ +import { + type GraphQLArgument, + GraphQLEnumType, + type GraphQLField, + type GraphQLInputField, + GraphQLInterfaceType, + GraphQLObjectType, + GraphQLScalarType, + GraphQLUnionType, + isNonNullType, + isObjectType, +} from 'graphql' +import { GraphQLInputObjectType, isScalarType } from 'graphql' +import type { AnyGraphQLOutputField } from '../graphql.js' + +export { + getNullableType, + GraphQLInputObjectType, + GraphQLInterfaceType, + GraphQLList, + GraphQLNonNull, + GraphQLObjectType, + GraphQLScalarType, + GraphQLSchema, + type GraphQLType, + GraphQLUnionType, + isInputObjectType, + isInterfaceType, + isListType, + isNullableType, + isObjectType, + isScalarType, + isUnionType, +} from 'graphql' + +export * as Args from './args.js' +export * as CustomScalars from './customScalars.js' +export * as KindMap from './kindMap.js' + +export const isGraphQLOutputField = (object: object): object is AnyGraphQLOutputField => { + return `args` in object +} + +export const isGraphQLArgumentOrInputField = (value: object): value is GraphQLArgument | GraphQLInputField => { + return `defaultValue` in value +} + +export const isGraphQLField = (value: object): value is GraphQLField => { + return `args` in value +} + +export const isScalarTypeCustom = (node: GraphQLScalarType): boolean => { + return node.astNode !== undefined +} + +export const isScalarTypeAndCustom = (node: unknown): node is GraphQLScalarType => { + return isScalarType(node) && node.astNode !== undefined +} + +export const isAllInputObjectFieldsNullable = (node: GraphQLInputObjectType) => { + return Object.values(node.getFields()).some(_ => !isNonNullType(_.type)) +} + +export const NameToClassNamedType = { + GraphQLScalarType: GraphQLScalarType, + GraphQLObjectType: GraphQLObjectType, + GraphQLInterfaceType: GraphQLInterfaceType, + GraphQLUnionType: GraphQLUnionType, + GraphQLEnumType: GraphQLEnumType, + GraphQLInputObjectType: GraphQLInputObjectType, +} + +export type NameToClassNamedType = typeof NameToClassNamedType + +export const RootTypeName = { + Query: `Query`, + Mutation: `Mutation`, + Subscription: `Subscription`, +} as const + +export type RootTypeName = keyof typeof RootTypeName + +export type RootTypeNameQuery = typeof RootTypeName['Query'] + +export type RootTypeNameMutation = typeof RootTypeName['Mutation'] + +export type RootTypeNameSubscription = typeof RootTypeName['Subscription'] + +export const isRootType = (value: unknown): value is GraphQLObjectType => { + return isObjectType(value) && value.name in RootTypeName +} diff --git a/src/lib/typed-document/TypedDocument.test-d.ts b/src/lib/grafaid/typed-document/TypedDocument.test-d.ts similarity index 96% rename from src/lib/typed-document/TypedDocument.test-d.ts rename to src/lib/grafaid/typed-document/TypedDocument.test-d.ts index fdbfa890..5c59e78e 100644 --- a/src/lib/typed-document/TypedDocument.test-d.ts +++ b/src/lib/grafaid/typed-document/TypedDocument.test-d.ts @@ -1,5 +1,5 @@ import type { DocumentNode } from 'graphql' -import { AssertEqual } from '../assert-equal.js' +import { AssertEqual } from '../../assert-equal.js' import type { GetVariablesInputKind, Node, diff --git a/src/lib/typed-document/TypedDocument.ts b/src/lib/grafaid/typed-document/TypedDocument.ts similarity index 93% rename from src/lib/typed-document/TypedDocument.ts rename to src/lib/grafaid/typed-document/TypedDocument.ts index 7741054a..21b00755 100644 --- a/src/lib/typed-document/TypedDocument.ts +++ b/src/lib/grafaid/typed-document/TypedDocument.ts @@ -1,12 +1,12 @@ import type { DocumentTypeDecoration } from '@graphql-typed-document-node/core' import type { DocumentNode, TypedQueryDocumentNode } from 'graphql' import type { HasRequiredKeys, IsNever, IsUnknown } from 'type-fest' -import { type HasKeys, type IsHasIndexType } from '../../lib/prelude.js' -import type { SomeData, Variables } from '../graphql-plus/graphql.js' +import { type HasKeys, type IsHasIndexType } from '../../prelude.js' +import type { SomeData, Variables } from '../graphql.js' -export { type TypedQueryDocumentNode as Query } from 'graphql' +export type { SomeData, Variables } from '../graphql.js' -export type { SomeData, Variables } from '../graphql-plus/graphql.js' +export { type TypedQueryDocumentNode as Query } from 'graphql' // We default to `any` because otherwise when this type is used as a constraint // it will reject apparent subtypes. The reason I think has to do with co/contra-variant stuff @@ -77,6 +77,8 @@ export const isString = <$TypedDocument extends TypedDocument>( document: $TypedDocument, ): document is Exclude<$TypedDocument, TypedDocumentNode | TypedQueryDocumentNode> => typeof document === `string` +export const unType = (document: TypedDocument): string | DocumentNode => document as any + // dprint-ignore export type ResultOf<$Document extends TypedDocument> = $Document extends TypedQueryDocumentNode ? $R : diff --git a/src/lib/typed-document/__.ts b/src/lib/grafaid/typed-document/__.ts similarity index 100% rename from src/lib/typed-document/__.ts rename to src/lib/grafaid/typed-document/__.ts diff --git a/src/lib/graphql-http/__.ts b/src/lib/graphql-http/__.ts deleted file mode 100644 index 945c9910..00000000 --- a/src/lib/graphql-http/__.ts +++ /dev/null @@ -1 +0,0 @@ -export * as GraphQLHTTP from './graphqlHTTP.js' diff --git a/src/lib/graphql-plus/_Nodes.ts b/src/lib/graphql-plus/_Nodes.ts deleted file mode 100644 index c3dab4d2..00000000 --- a/src/lib/graphql-plus/_Nodes.ts +++ /dev/null @@ -1 +0,0 @@ -export * as Nodes from './nodes.js' diff --git a/src/lib/graphql-plus/nodesSchema.ts b/src/lib/graphql-plus/nodesSchema.ts deleted file mode 100644 index e413be44..00000000 --- a/src/lib/graphql-plus/nodesSchema.ts +++ /dev/null @@ -1,112 +0,0 @@ -import type { - GraphQLArgument, - GraphQLEnumType, - GraphQLField, - GraphQLInputField, - GraphQLNamedOutputType, - GraphQLObjectType, - GraphQLScalarType, -} from 'graphql' -import { - getNamedType, - type GraphQLInputObjectType, - isEnumType, - isInputObjectType, - isInterfaceType, - isObjectType, - isScalarType, - isUnionType, -} from 'graphql' -import { casesExhausted } from '../prelude.js' - -export { - getNullableType, - GraphQLInputObjectType, - GraphQLList, - GraphQLNonNull, - GraphQLObjectType, - GraphQLScalarType, - GraphQLSchema, - type GraphQLType, - isInputObjectType, - isListType, - isNullableType, - isObjectType, - isScalarType, -} from 'graphql' - -export const isHasCustomScalarInputs = ( - node: - | GraphQLInputObjectType - | GraphQLObjectType - | GraphQLScalarType - | GraphQLEnumType - | GraphQLInputField - | GraphQLArgument - | GraphQLField, -): boolean => { - return isHasCustomScalarInputs_(node, []) -} - -const isHasCustomScalarInputs_ = ( - node: GraphQLInputObjectType | GraphQLNamedOutputType | GraphQLArgument | GraphQLField, - typePath: string[], -): boolean => { - if (isGraphQLArgumentOrInputField(node)) { - return isHasCustomScalarInputs_(getNamedType(node.type), typePath) - } - - if (isGraphQLField(node)) { - const fieldType = getNamedType(node.type) - return node.args.some(arg => isHasCustomScalarInputs_(arg, typePath)) - || (isObjectType(fieldType) && isHasCustomScalarInputs_(fieldType, typePath)) - } - - if (isEnumType(node)) { - return false - } - - if (isScalarType(node)) { - // End - return isScalarTypeAndCustom(node) - } - - if (typePath.includes(node.name)) { - // End Via Short-Circuit: We've already come from this type. - return false - } else { - typePath = [...typePath, node.name] - } - - if (isInputObjectType(node)) { - return Object.values(node.getFields()).some(field => isHasCustomScalarInputs_(field, typePath)) - } - - if (isObjectType(node) || isInterfaceType(node)) { - return Object.values(node.getFields()).some(field => isHasCustomScalarInputs_(field, typePath)) - } - - if (isUnionType(node)) { - return false - } - - throw casesExhausted(node) -} - -// const isOutput - -export const isGraphQLArgumentOrInputField = (value: object): value is GraphQLArgument | GraphQLInputField => { - return `defaultValue` in value -} - -export const isGraphQLField = (value: object): value is GraphQLField => { - return `args` in value -} - -export const isScalarTypeCustom = (node: GraphQLScalarType): boolean => { - return node.astNode !== undefined -} - -export const isScalarTypeAndCustom = (node: unknown): node is GraphQLScalarType => { - return isScalarType(node) && node.astNode !== undefined -} diff --git a/src/lib/prelude.ts b/src/lib/prelude.ts index 57f9da42..e3b0b7dc 100644 --- a/src/lib/prelude.ts +++ b/src/lib/prelude.ts @@ -314,13 +314,30 @@ export type MinusOneUpToTen = n extends 10 ? 9 : n extends 1 ? 0 : never -export type findIndexForValue = findIndexForValue_ -type findIndexForValue_ = value extends list[i] ? i - : findIndexForValue_> +// dprint-ignore +export type findIndexForValue = + findIndexForValue_ + +// dprint-ignore +type findIndexForValue_ = + value extends list[i] + ? i + : findIndexForValue_> -export type FindValueAfter = +export type FindValueAfter = list[PlusOneUpToTen>] +// dprint-ignore +export type TakeValuesBefore<$Value, $List extends AnyReadOnlyList> = + $List extends readonly [infer $ListFirst, ...infer $ListRest] + ? $Value extends $ListFirst + ? [] + : [$ListFirst, ...TakeValuesBefore<$Value, $ListRest>] + : [] + +type AnyReadOnlyListNonEmpty = readonly [any, ...any[]] +type AnyReadOnlyList = readonly [...any[]] + export type ValueOr = value extends undefined ? orValue : value export type FindValueAfterOr = ValueOr< diff --git a/tests/_/helpers.ts b/tests/_/helpers.ts index faf39347..38085ceb 100644 --- a/tests/_/helpers.ts +++ b/tests/_/helpers.ts @@ -3,7 +3,7 @@ import { test as testBase, vi } from 'vitest' import { Graffle } from '../../src/entrypoints/main.js' import type { Config } from '../../src/entrypoints/utilities-for-generated.js' import type { Client } from '../../src/layers/6_client/client.js' -import { CONTENT_TYPE_REC } from '../../src/lib/graphql-http/graphqlHTTP.js' +import { CONTENT_TYPE_REC } from '../../src/lib/grafaid/http/http.js' import { type SchemaService, serveSchema } from './lib/serveSchema.js' import { db } from './schemas/db.js' import { Graffle as KitchenSink } from './schemas/kitchen-sink/graffle/__.js' diff --git a/tests/_/schemas/kitchen-sink/graffle/modules/MethodsRoot.ts b/tests/_/schemas/kitchen-sink/graffle/modules/MethodsRoot.ts index b5c614ef..1e35f6e1 100644 --- a/tests/_/schemas/kitchen-sink/graffle/modules/MethodsRoot.ts +++ b/tests/_/schemas/kitchen-sink/graffle/modules/MethodsRoot.ts @@ -180,6 +180,14 @@ export interface QueryMethods<$Config extends Utils.Config> { ResultSet.InferField > > + dateListList: () => Promise< + Utils.ResolveOutputReturnRootField< + $Config, + Index, + 'dateListList', + ResultSet.InferField + > + > dateListNonNull: () => Promise< Utils.ResolveOutputReturnRootField< $Config, diff --git a/tests/_/schemas/kitchen-sink/graffle/modules/RuntimeCustomScalars.ts b/tests/_/schemas/kitchen-sink/graffle/modules/RuntimeCustomScalars.ts index 48a51533..ba01287a 100644 --- a/tests/_/schemas/kitchen-sink/graffle/modules/RuntimeCustomScalars.ts +++ b/tests/_/schemas/kitchen-sink/graffle/modules/RuntimeCustomScalars.ts @@ -44,7 +44,58 @@ const InputObjectNestedNonNull = { // // -// None of your GraphQLObjectTypes have custom scalars. +const DateObject1 = { + date1: { + o: CustomScalars.Date.codec, + }, +} + +const DateObject2 = { + date2: { + o: CustomScalars.Date.codec, + }, +} + +// +// +// +// +// +// +// ================================================================================================== +// GraphQLInterfaceType +// ================================================================================================== +// +// +// +// +// +// + +const DateInterface1 = { + ...DateObject1, +} + +// +// +// +// +// +// +// ================================================================================================== +// GraphQLUnionType +// ================================================================================================== +// +// +// +// +// +// + +const DateUnion = { + ...DateObject1, + ...DateObject2, +} // // @@ -64,52 +115,82 @@ const InputObjectNestedNonNull = { const Query = { InputObjectNested: { - $: { + i: { input: InputObjectNested, }, }, InputObjectNestedNonNull: { - $: { + i: { input: InputObjectNestedNonNull, }, }, + date: { + o: CustomScalars.Date.codec, + }, dateArg: { - $: { + i: { date: CustomScalars.Date.codec, }, + o: CustomScalars.Date.codec, }, dateArgInputObject: { - $: { + i: { input: InputObject, }, + o: CustomScalars.Date.codec, }, dateArgList: { - $: { + i: { date: CustomScalars.Date.codec, }, + o: CustomScalars.Date.codec, }, dateArgNonNull: { - $: { + i: { date: CustomScalars.Date.codec, }, + o: CustomScalars.Date.codec, }, dateArgNonNullList: { - $: { + i: { date: CustomScalars.Date.codec, }, + o: CustomScalars.Date.codec, }, dateArgNonNullListNonNull: { - $: { + i: { date: CustomScalars.Date.codec, }, + o: CustomScalars.Date.codec, + }, + dateInterface1: { + r: DateInterface1, + }, + dateList: { + o: CustomScalars.Date.codec, + }, + dateListList: { + o: CustomScalars.Date.codec, + }, + dateListNonNull: { + o: CustomScalars.Date.codec, + }, + dateNonNull: { + o: CustomScalars.Date.codec, + }, + dateObject1: { + r: DateObject1, + }, + dateUnion: { + r: DateUnion, }, stringWithArgInputObject: { - $: { + i: { input: InputObject, }, }, stringWithArgInputObjectRequired: { - $: { + i: { input: InputObject, }, }, diff --git a/tests/_/schemas/kitchen-sink/graffle/modules/SchemaBuildtime.ts b/tests/_/schemas/kitchen-sink/graffle/modules/SchemaBuildtime.ts index b0926e9c..36a8c72f 100644 --- a/tests/_/schemas/kitchen-sink/graffle/modules/SchemaBuildtime.ts +++ b/tests/_/schemas/kitchen-sink/graffle/modules/SchemaBuildtime.ts @@ -74,6 +74,7 @@ export namespace Root { > dateInterface1: $.Field<'dateInterface1', $.Output.Nullable, null> dateList: $.Field<'dateList', $.Output.Nullable<$.Output.List<$Scalar.Date>>, null> + dateListList: $.Field<'dateListList', $.Output.Nullable<$.Output.List<$.Output.List<$Scalar.Date>>>, null> dateListNonNull: $.Field<'dateListNonNull', $.Output.List<$Scalar.Date>, null> dateNonNull: $.Field<'dateNonNull', $Scalar.Date, null> dateObject1: $.Field<'dateObject1', $.Output.Nullable, null> diff --git a/tests/_/schemas/kitchen-sink/graffle/modules/SchemaRuntime.ts b/tests/_/schemas/kitchen-sink/graffle/modules/SchemaRuntime.ts index 4ab1a573..b525726c 100644 --- a/tests/_/schemas/kitchen-sink/graffle/modules/SchemaRuntime.ts +++ b/tests/_/schemas/kitchen-sink/graffle/modules/SchemaRuntime.ts @@ -169,6 +169,7 @@ export const Query = $.Object$(`Query`, { // @ts-ignore - circular types cannot infer. Ignore in case there are any. This comment is always added, it does not indicate if this particular type could infer or not. dateInterface1: $.field('dateInterface1', $.Output.Nullable(() => DateInterface1)), dateList: $.field('dateList', $.Output.Nullable($.Output.List($Scalar.Date))), + dateListList: $.field('dateListList', $.Output.Nullable($.Output.List($.Output.List($Scalar.Date)))), dateListNonNull: $.field('dateListNonNull', $.Output.List($Scalar.Date)), dateNonNull: $.field('dateNonNull', $Scalar.Date), // @ts-ignore - circular types cannot infer. Ignore in case there are any. This comment is always added, it does not indicate if this particular type could infer or not. diff --git a/tests/_/schemas/kitchen-sink/graffle/modules/SelectionSets.ts b/tests/_/schemas/kitchen-sink/graffle/modules/SelectionSets.ts index 4b8fffc1..3f0c4bd2 100644 --- a/tests/_/schemas/kitchen-sink/graffle/modules/SelectionSets.ts +++ b/tests/_/schemas/kitchen-sink/graffle/modules/SelectionSets.ts @@ -175,6 +175,10 @@ export interface Query { * Select the `dateList` field on the `Query` object. Its type is `Date` (a `Scalar`). */ dateList?: Query.dateList$Expanded | $Select.SelectAlias.SelectAlias + /** + * Select the `dateListList` field on the `Query` object. Its type is `Date` (a `Scalar`). + */ + dateListList?: Query.dateListList$Expanded | $Select.SelectAlias.SelectAlias /** * Select the `dateListNonNull` field on the `Query` object. Its type is `Date` (a `Scalar`). */ @@ -516,6 +520,15 @@ export namespace Query { export type dateList = $Select.Indicator.NoArgsIndicator + /** + * This is the "expanded" version of the `dateListList` type. It is identical except for the fact + * that IDEs will display its contents (a union type) directly, rather than the name of this type. + * In some cases, this is a preferable DX, making the types easier to read for users. + */ + export type dateListList$Expanded = $Select.Indicator.NoArgsIndicator$Expanded + + export type dateListList = $Select.Indicator.NoArgsIndicator + /** * This is the "expanded" version of the `dateListNonNull` type. It is identical except for the fact * that IDEs will display its contents (a union type) directly, rather than the name of this type. diff --git a/tests/_/schemas/kitchen-sink/schema.graphql b/tests/_/schemas/kitchen-sink/schema.graphql index f4072bc1..91c8d86f 100644 --- a/tests/_/schemas/kitchen-sink/schema.graphql +++ b/tests/_/schemas/kitchen-sink/schema.graphql @@ -125,6 +125,7 @@ type Query { dateArgNonNullListNonNull(date: [Date!]!): Date dateInterface1: DateInterface1 dateList: [Date!] + dateListList: [[Date!]!] dateListNonNull: [Date!]! dateNonNull: Date! dateObject1: DateObject1 diff --git a/tests/_/schemas/kitchen-sink/schema.ts b/tests/_/schemas/kitchen-sink/schema.ts index 99e32d44..c6e9b71d 100644 --- a/tests/_/schemas/kitchen-sink/schema.ts +++ b/tests/_/schemas/kitchen-sink/schema.ts @@ -218,6 +218,10 @@ builder.queryType({ type: t.listRef(`Date`), resolve: () => [db.date0, db.date1], }), + dateListList: t.field({ + type: t.listRef(t.listRef(`Date`)), + resolve: () => [[db.date0, db.date1], [db.date0, db.date1]], + }), dateObject1: t.field({ type: DateObject1, resolve: () => ({ date1: db.date0 }) }), dateUnion: t.field({ type: DateUnion, resolve: () => ({ date1: db.date0 }) }), dateInterface1: t.field({ diff --git a/tests/_/schemas/mutation-only/graffle/modules/RuntimeCustomScalars.ts b/tests/_/schemas/mutation-only/graffle/modules/RuntimeCustomScalars.ts index f76b2632..13aee9e4 100644 --- a/tests/_/schemas/mutation-only/graffle/modules/RuntimeCustomScalars.ts +++ b/tests/_/schemas/mutation-only/graffle/modules/RuntimeCustomScalars.ts @@ -35,6 +35,42 @@ import * as CustomScalars from './Scalar.js' // None of your GraphQLObjectTypes have custom scalars. +// +// +// +// +// +// +// ================================================================================================== +// GraphQLInterfaceType +// ================================================================================================== +// +// +// +// +// +// + +// None of your GraphQLInterfaceTypes have custom scalars. + +// +// +// +// +// +// +// ================================================================================================== +// GraphQLUnionType +// ================================================================================================== +// +// +// +// +// +// + +// None of your GraphQLUnionTypes have custom scalars. + // // // diff --git a/tests/_/schemas/pokemon/graffle/modules/RuntimeCustomScalars.ts b/tests/_/schemas/pokemon/graffle/modules/RuntimeCustomScalars.ts index f76b2632..13aee9e4 100644 --- a/tests/_/schemas/pokemon/graffle/modules/RuntimeCustomScalars.ts +++ b/tests/_/schemas/pokemon/graffle/modules/RuntimeCustomScalars.ts @@ -35,6 +35,42 @@ import * as CustomScalars from './Scalar.js' // None of your GraphQLObjectTypes have custom scalars. +// +// +// +// +// +// +// ================================================================================================== +// GraphQLInterfaceType +// ================================================================================================== +// +// +// +// +// +// + +// None of your GraphQLInterfaceTypes have custom scalars. + +// +// +// +// +// +// +// ================================================================================================== +// GraphQLUnionType +// ================================================================================================== +// +// +// +// +// +// + +// None of your GraphQLUnionTypes have custom scalars. + // // // diff --git a/tests/_/schemas/query-only/graffle/modules/RuntimeCustomScalars.ts b/tests/_/schemas/query-only/graffle/modules/RuntimeCustomScalars.ts index f76b2632..13aee9e4 100644 --- a/tests/_/schemas/query-only/graffle/modules/RuntimeCustomScalars.ts +++ b/tests/_/schemas/query-only/graffle/modules/RuntimeCustomScalars.ts @@ -35,6 +35,42 @@ import * as CustomScalars from './Scalar.js' // None of your GraphQLObjectTypes have custom scalars. +// +// +// +// +// +// +// ================================================================================================== +// GraphQLInterfaceType +// ================================================================================================== +// +// +// +// +// +// + +// None of your GraphQLInterfaceTypes have custom scalars. + +// +// +// +// +// +// +// ================================================================================================== +// GraphQLUnionType +// ================================================================================================== +// +// +// +// +// +// + +// None of your GraphQLUnionTypes have custom scalars. + // // //