From 6cba81da74335b99d44354a68ce169c55f186362 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Fri, 22 Nov 2024 10:10:01 -0500 Subject: [PATCH] fix(document-builder): non-deterministic fields --- .../InferResult/OutputField.ts | 18 +-- .../InferResult/OutputObject.ts | 110 ++++++++++++------ src/documentBuilder/InferResult/__.test-d.ts | 58 +++++---- src/documentBuilder/InferResult/directive.ts | 16 +-- .../Select/Indicator/indicator.ts | 8 ++ src/documentBuilder/Select/InlineFragment.ts | 4 + src/lib/assert-equal.ts | 13 ++- src/lib/prelude.ts | 5 + 8 files changed, 147 insertions(+), 85 deletions(-) diff --git a/src/documentBuilder/InferResult/OutputField.ts b/src/documentBuilder/InferResult/OutputField.ts index b2e05202e..4602a0da2 100644 --- a/src/documentBuilder/InferResult/OutputField.ts +++ b/src/documentBuilder/InferResult/OutputField.ts @@ -1,26 +1,16 @@ import type { TSErrorDescriptive } from '../../lib/ts-error.js' import type { Schema } from '../../types/Schema/__.js' import type { InlineType } from '../../types/SchemaDrivenDataMap/InlineType.js' -import type { Select } from '../Select/__.js' -import type { FieldDirectiveInclude, FieldDirectiveSkip } from './directive.js' import type { Interface } from './Interface.js' import type { OutputObject } from './OutputObject.js' import type { Union } from './Union.js' // dprint-ignore export type OutputField<$SelectionSet, $Field extends Schema.OutputField, $Schema extends Schema> = - $SelectionSet extends Select.Directive.Include.FieldStates.Negative | Select.Directive.Skip.FieldStates.Positive - ? null - : ( - | FieldDirectiveInclude<$SelectionSet> - | FieldDirectiveSkip<$SelectionSet> - | //SimplifyNullable< - InlineType.Infer< - $Field['inlineType'], - FieldType<$Schema, Omit<$SelectionSet, '$'>, $Field['namedType']> - > - //> - ) + InlineType.Infer< + $Field['inlineType'], + FieldType<$Schema, Omit<$SelectionSet, '$'>, $Field['namedType']> + > // dprint-ignore type FieldType< diff --git a/src/documentBuilder/InferResult/OutputObject.ts b/src/documentBuilder/InferResult/OutputObject.ts index 7e0a17b74..ba08982b5 100644 --- a/src/documentBuilder/InferResult/OutputObject.ts +++ b/src/documentBuilder/InferResult/OutputObject.ts @@ -1,11 +1,16 @@ import type { IsNever } from 'type-fest' import { assertEqual } from '../../lib/assert-equal.js' -import type { AssertExtendsObject, GetOrNever, StringKeyof } from '../../lib/prelude.js' +import type { AssertExtendsObject, GetOrNever, PropertyKeyToString, StringKeyof } from '../../lib/prelude.js' import type { TSErrorDescriptive } from '../../lib/ts-error.js' import type { Schema } from '../../types/Schema/__.js' import type { Select } from '../Select/__.js' import type { Alias } from './Alias.js' -import type { IsNeverViaDirective, IsOptionalViaDirective, OmitDirectiveAndArgumentKeys } from './directive.js' +import type { + IsArgumentsOrDirectiveKey, + IsNeverViaDirective, + IsOptionalViaDirective, + OmitDirectiveAndArgumentKeys, +} from './directive.js' import type { OutputField } from './OutputField.js' import type { ScalarsWildcard } from './ScalarsWildcard.js' @@ -15,11 +20,8 @@ export type OutputObject< $Schema extends Schema, $Node extends Schema.OutputObject > = - // SimplifyExcept< - // $Schema['scalars']['typesDecoded'], & OutputObject_<$SelectionSet, $Schema, $Node> & InlineFragmentKeys<$SelectionSet, $Schema, $Node> -// > // dprint-ignore type OutputObject_< @@ -27,24 +29,69 @@ type OutputObject_< $Schema extends Schema, $Node extends Schema.OutputObject, > = - // Simplify< Select.SelectScalarsWildcard.IsSelectScalarsWildcard<$SelectionSet> extends true // todo this needs to be an extension and/or only available when sddm is present // todo what about when scalars wildcard is combined with other fields like relations? ? ScalarsWildcard<$SelectionSet, $Schema, $Node> : - & NonAliasKeys<$SelectionSet, $Schema, $Node> - & Alias<$Schema, $Node, $SelectionSet> -// > + & OtherKeys<$SelectionSet, $Schema, $Node> + & Alias<$Schema, $Node, $SelectionSet> // dprint-ignore -type NonAliasKeys<$SelectionSet, $Schema extends Schema, $Node extends Schema.OutputObject> = { - [$Key in PickPositiveIndicatorAndNotAlias<$SelectionSet>]: - $Key extends keyof $Node['fields'] - ? OutputField<$SelectionSet[$Key], $Node['fields'][$Key], $Schema> - : Errors.UnknownFieldName<$Key, $Node> -} +type OtherKeys<$SelectionSet, $Schema extends Schema, $Node extends Schema.OutputObject> = + { + [ + $Field in keyof $SelectionSet as + IsFieldKey<$Field> extends true + ? Select.Indicator.IsOptionalIndicator<$SelectionSet[$Field]> extends true + ? $Field + : IsOptionalViaDirective<$SelectionSet[$Field]> extends true + ? $Field + : never + : never + ]?: + $Field extends keyof $Node['fields'] + ? OutputField<$SelectionSet[$Field], $Node['fields'][$Field], $Schema> + : Errors.UnknownKey<$Field, $Node> + } + & + { + [$Field in PickApplicableFieldKeys<$SelectionSet>]: + $Field extends keyof $Node['fields'] + ? OutputField<$SelectionSet[$Field], $Node['fields'][$Field], $Schema> + : Errors.UnknownKey<$Field, $Node> + } +// dprint-ignore +type PickApplicableFieldKeys<$SelectionSet> = StringKeyof< + { + [ + $Key in keyof $SelectionSet as + // field is e.g. foo: boolean + // We handle non-deterministic fields elsewhere + Select.Indicator.IsOptionalIndicator<$SelectionSet[$Key]> extends true + ? never + // field is e.g. foo: false + : $SelectionSet[$Key] extends Select.Indicator.Negative + ? never + // We handle aliases elsewhere + : $SelectionSet[$Key] extends any[] + ? never + // We handle inline fragments elsewhere + : $Key extends Select.InlineFragment.Key + ? never + : IsNeverViaDirective<$SelectionSet[$Key]> extends true + ? never + // We handle non-deterministic directives elsewhere + : IsOptionalViaDirective<$SelectionSet[$Key]> extends true + ? never + // Not a field key + : IsArgumentsOrDirectiveKey<$Key> extends true + ? never + : $Key + ]: 0 + } +> // dprint-ignore type InlineFragmentKeys<$SelectionSet extends object, $Schema extends Schema, $Node extends Schema.OutputObject> = InlineFragmentKey_< @@ -67,28 +114,21 @@ type InlineFragmentKey_<$SelectionSet extends object, $Schema extends Schema, $N > : OutputObject_, $Schema, $Node> -// dprint-ignore -type PickPositiveIndicatorAndNotAlias<$SelectionSet> = StringKeyof< - { - [ - $Key in keyof $SelectionSet as $SelectionSet[$Key] extends Select.Indicator.Negative - ? never - : $SelectionSet[$Key] extends any[] - ? never - : $Key extends Select.InlineFragment.Key - ? never - : $Key - ]: 0 - } -> - export namespace Errors { - export type UnknownFieldName< - $FieldName extends string, + export type UnknownKey< + $Key extends PropertyKey, $Object extends Schema.OutputObject, - > = TSErrorDescriptive<'Object', `field "${$FieldName}" does not exist on object "${$Object['name']}"`> + > = TSErrorDescriptive<'Object', `field "${PropertyKeyToString<$Key>}" does not exist on object "${$Object['name']}"`> } +// dprint-ignore +export type IsFieldKey<$Key extends PropertyKey> = + IsArgumentsOrDirectiveKey<$Key> extends true + ? false + : Select.InlineFragment.IsInlineFragmentKey<$Key> extends true + ? false + : true + // // // @@ -99,7 +139,7 @@ export namespace Errors { // dprint-ignore { -assertEqual , 'a'>() -assertEqual , 'b'>() +assertEqual , 'a'>() +assertEqual , 'b'>() } diff --git a/src/documentBuilder/InferResult/__.test-d.ts b/src/documentBuilder/InferResult/__.test-d.ts index f4b540320..d727ae1e4 100644 --- a/src/documentBuilder/InferResult/__.test-d.ts +++ b/src/documentBuilder/InferResult/__.test-d.ts @@ -23,14 +23,26 @@ type $WithDate<$SelectionSet extends SelectionSets.Query<$Registry>> = InferResu assertEqual<$<{ __typename: true }>, { __typename: 'Query' }>() -// Scalar -assertEqual<$<{ id: true }>, { id: null | string }>() -// AssertEqual, { id: null | string }>() -// non-nullable -assertEqual<$<{ idNonNull: true }>, { idNonNull: string }>() +// Scalar nullable indicator positive +assertEqual<$<{ id: true }> , { id: null | string }>() +// Scalar nullable indicator negative +assertEqual<$<{ id: false }> , {}>() +assertEqual<$<{ id: undefined }> , {}>() +// scalar nullable indicator non-deterministic +assertEqual<$<{ id: true | undefined }> , { id?: null | string }>() +assertEqual<$<{ id: boolean }> , { id?: null | string }>() + +// scalar non-null indicator positive +assertEqual<$<{ idNonNull: true }> , { idNonNull: string }>() +// scalar non-null indicator negative +assertEqual<$<{ idNonNull: false }> , {}>() +assertEqual<$<{ idNonNull: undefined }> , {}>() +// scalar non-null indicator non-deterministic +assertEqual<$<{ idNonNull: true | undefined }> , { idNonNull?: string }>() +assertEqual<$<{ idNonNull: boolean }> , { idNonNull?: string }>() + // indicator negative assertEqual<$<{ id: true; string: false }>, { id: null | string }>() -// AssertEqual, { id: null | string }>() assertEqual<$<{ id: true; string: undefined }>, { id: null | string }>() // Custom Scalar @@ -109,29 +121,34 @@ assertEqual<$<{ unionFooBar: { ___on_Foo: { id: ['id2', true] } } }>, { unionFoo // Directive @include // On scalar non-nullable -assertEqual<$<{ idNonNull: { $include: boolean } }>, { idNonNull: null|string }>() -assertEqual<$<{ idNonNull: { $include: {if:boolean} } }>, { idNonNull: null|string }>() +assertEqual<$<{ idNonNull: { $include: boolean } }>, { idNonNull?: string }>() +assertEqual<$<{ idNonNull: { $include: {if:boolean} } }>, { idNonNull?: string }>() assertEqual<$<{ idNonNull: { $include: true } }>, { idNonNull: string }>() assertEqual<$<{ idNonNull: { $include: {if:true} } }>, { idNonNull: string }>() -assertEqual<$<{ idNonNull: { $include: false } }>, { idNonNull: null }>() -assertEqual<$<{ idNonNull: { $include: {if:false} } }>, { idNonNull: null }>() +assertEqual<$<{ idNonNull: { $include: false } }>, {}>() +assertEqual<$<{ idNonNull: { $include: {if:false} } }>, {}>() // On scalar nullable -assertEqual<$<{ id: { $include: boolean } }>, { id: null|string }>() -assertEqual<$<{ id: { $include: false } }>, { id: null }>() +assertEqual<$<{ id: { $include: boolean } }>, { id?: null|string }>() +assertEqual<$<{ id: { $include: false } }>, {}>() assertEqual<$<{ id: { $include: true } }>, { id: null|string }>() // Directive @skip // On scalar non-nullable -assertEqual<$<{ idNonNull: { $skip: boolean } }>, { idNonNull: null|string }>() -assertEqual<$<{ idNonNull: { $skip: {if:boolean} } }>, { idNonNull: null|string }>() -assertEqual<$<{ idNonNull: { $skip: true } }>, { idNonNull: null }>() -assertEqual<$<{ idNonNull: { $skip: {if:true} } }>, { idNonNull: null }>() +assertEqual<$<{ idNonNull: { $skip: boolean } }>, { idNonNull?: string }>() +assertEqual<$<{ idNonNull: { $skip: {if:boolean} } }>, { idNonNull?: string }>() +assertEqual<$<{ idNonNull: { $skip: true } }>, {}>() +assertEqual<$<{ idNonNull: { $skip: {if:true} } }>, {}>() assertEqual<$<{ idNonNull: { $skip: false } }>, { idNonNull: string }>() assertEqual<$<{ idNonNull: { $skip: {if:false} } }>, { idNonNull: string }>() // On scalar nullable -assertEqual<$<{ id: { $skip: boolean } }>, { id: null|string }>() +assertEqual<$<{ id: { $skip: boolean } }>, { id?: null|string }>() assertEqual<$<{ id: { $skip: false } }>, { id: null|string }>() -assertEqual<$<{ id: { $skip: true } }>, { id: null }>() +assertEqual<$<{ id: { $skip: true } }>, {}>() + +// Directive @include +assertEqual<$<{ objectNested: { $include: false } }> , {}>() +assertEqual<$<{ objectNested: { $include: true } }> , { objectNested: {} | null}>() +assertEqual<$<{ objectNested: { $include: boolean } }> , { objectNested?: {} | null}>() // Directive @defer // todo @@ -139,9 +156,6 @@ assertEqual<$<{ id: { $skip: true } }>, { id: null }>() // Directive @stream // todo -// Field Group -// todo - // Arguments // scalar assertEqual<$<{ stringWithArgs: true }>, { stringWithArgs: null | string }>() @@ -161,6 +175,6 @@ assertEqual<$<{ ___: { $skip: false; id: true }}> // @ts-expect-error invalid query type Result = $<{ id2: true }> // unknown field -assertEqual }>() +assertEqual }>() } diff --git a/src/documentBuilder/InferResult/directive.ts b/src/documentBuilder/InferResult/directive.ts index d9e3a83d3..36eaacda8 100644 --- a/src/documentBuilder/InferResult/directive.ts +++ b/src/documentBuilder/InferResult/directive.ts @@ -28,20 +28,10 @@ type OptionalSelection = | Select.Directive.Include.FieldStates.Variable | Select.Directive.Skip.FieldStates.Variable -// dprint-ignore -export type FieldDirectiveInclude<$SelectionSet> = - $SelectionSet extends Select.Directive.Include.Field ? $SelectionSet extends Select.Directive.Include.FieldStates.Positive ? never - : null - : never - -// dprint-ignore -export type FieldDirectiveSkip<$SelectionSet> = - $SelectionSet extends Select.Directive.Skip.Field ? $SelectionSet extends Select.Directive.Skip.FieldStates.Negative ? never - : null - : never - export type OmitDirectiveAndArgumentKeys<$SelectionSet extends object> = { [$Key in keyof $SelectionSet as IsArgumentsOrDirectiveKey<$Key> extends true ? never : $Key]: $SelectionSet[$Key] } -type IsArgumentsOrDirectiveKey<$Key extends PropertyKey> = $Key extends `$${string}` ? true : false +// dprint-ignore +export type IsArgumentsOrDirectiveKey<$Key extends PropertyKey> = + $Key extends `$${string}` ? true : false diff --git a/src/documentBuilder/Select/Indicator/indicator.ts b/src/documentBuilder/Select/Indicator/indicator.ts index 70d7adca3..e5f5ee2c7 100644 --- a/src/documentBuilder/Select/Indicator/indicator.ts +++ b/src/documentBuilder/Select/Indicator/indicator.ts @@ -6,6 +6,14 @@ import { isPositiveIndicator, type Positive } from './positive.js' export type Indicator = UnionExpanded +// dprint-ignore +export type IsOptionalIndicator<$SelectionSet> = + true | undefined extends $SelectionSet + ? true + : boolean extends $SelectionSet + ? true + : false + export const isIndicator = (v: any): v is Indicator => { return isPositiveIndicator(v) || isNegativeIndicator(v) } diff --git a/src/documentBuilder/Select/InlineFragment.ts b/src/documentBuilder/Select/InlineFragment.ts index 89af3c6f8..b7c7e4d7d 100644 --- a/src/documentBuilder/Select/InlineFragment.ts +++ b/src/documentBuilder/Select/InlineFragment.ts @@ -8,6 +8,10 @@ export const typeConditionPRefix = `${prefix}on_` export type TypeConditionalKeyPrefix = typeof typeConditionPRefix +// dprint-ignore +export type IsInlineFragmentKey<$Key extends PropertyKey> = + $Key extends `${Key}${string}` ? true : false + export interface On { _tag: 'On' typeOrFragmentName: string diff --git a/src/lib/assert-equal.ts b/src/lib/assert-equal.ts index df8ef6604..3d0483bbb 100644 --- a/src/lib/assert-equal.ts +++ b/src/lib/assert-equal.ts @@ -2,10 +2,21 @@ import type { SimplifyDeep } from 'type-fest' export type IsExtends = [A] extends [B] ? true : false -export type IsEqual = [A] extends [B] ? [B] extends [A] ? true : false : false +// dprint-ignore +export type IsEqual = + [A] extends [B] + ? [B] extends [A] + ? [keyof A] extends [keyof B] + ? [keyof B] extends [keyof A] + ? true + : false + : false + : false + : false type _______IsEqual = [ IsFalse>, IsFalse>, + IsFalse>, ] export type assertEqual = IsEqual extends true ? true : never diff --git a/src/lib/prelude.ts b/src/lib/prelude.ts index 754897d79..16590d8d0 100644 --- a/src/lib/prelude.ts +++ b/src/lib/prelude.ts @@ -762,3 +762,8 @@ type IsAnyUnionMemberExtends_ = export type AnyAndUnknownToNever = IsAny extends true ? never : IsUnknown extends true ? never : T export type t = T extends null ? {} | null : {} + +export type PropertyKeyToString<$Key extends PropertyKey> = $Key extends string ? $Key + : $Key extends number ? $Key + : $Key extends symbol ? '' + : never