Skip to content

Commit

Permalink
fix(document-builder): non-deterministic fields
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonkuhrt committed Nov 22, 2024
1 parent 629a1fe commit 6cba81d
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 85 deletions.
18 changes: 4 additions & 14 deletions src/documentBuilder/InferResult/OutputField.ts
Original file line number Diff line number Diff line change
@@ -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<
Expand Down
110 changes: 75 additions & 35 deletions src/documentBuilder/InferResult/OutputObject.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -15,36 +20,78 @@ 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_<
$SelectionSet extends object,
$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_<
Expand All @@ -67,28 +114,21 @@ type InlineFragmentKey_<$SelectionSet extends object, $Schema extends Schema, $N
>
: OutputObject_<OmitDirectiveAndArgumentKeys<$SelectionSet>, $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

//
//
//
Expand All @@ -99,7 +139,7 @@ export namespace Errors {
// dprint-ignore
{

assertEqual<PickPositiveIndicatorAndNotAlias<{ a: true }> , 'a'>()
assertEqual<PickPositiveIndicatorAndNotAlias<{ a: ['b', true]; b: true }> , 'b'>()
assertEqual<PickApplicableFieldKeys<{ a: true }> , 'a'>()
assertEqual<PickApplicableFieldKeys<{ a: ['b', true]; b: true }> , 'b'>()

}
58 changes: 36 additions & 22 deletions src/documentBuilder/InferResult/__.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<RS<{ id: 1 }>, { 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<RS<{ id: true; string: 0 }>, { id: null | string }>()
assertEqual<$<{ id: true; string: undefined }>, { id: null | string }>()

// Custom Scalar
Expand Down Expand Up @@ -109,39 +121,41 @@ 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

// Directive @stream
// todo

// Field Group
// todo

// Arguments
// scalar
assertEqual<$<{ stringWithArgs: true }>, { stringWithArgs: null | string }>()
Expand All @@ -161,6 +175,6 @@ assertEqual<$<{ ___: { $skip: false; id: true }}>
// @ts-expect-error invalid query
type Result = $<{ id2: true }>
// unknown field
assertEqual<Result, { id2: InferResult.Errors.UnknownFieldName<'id2', Schema.Query> }>()
assertEqual<Result, { id2: InferResult.Errors.UnknownKey<'id2', Schema.Query> }>()

}
16 changes: 3 additions & 13 deletions src/documentBuilder/InferResult/directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 8 additions & 0 deletions src/documentBuilder/Select/Indicator/indicator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ import { isPositiveIndicator, type Positive } from './positive.js'

export type Indicator = UnionExpanded<Positive | Negative>

// 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)
}
Expand Down
4 changes: 4 additions & 0 deletions src/documentBuilder/Select/InlineFragment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 12 additions & 1 deletion src/lib/assert-equal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,21 @@ import type { SimplifyDeep } from 'type-fest'

export type IsExtends<A, B> = [A] extends [B] ? true : false

export type IsEqual<A, B> = [A] extends [B] ? [B] extends [A] ? true : false : false
// dprint-ignore
export type IsEqual<A, B> =
[A] extends [B]
? [B] extends [A]
? [keyof A] extends [keyof B]
? [keyof B] extends [keyof A]
? true
: false
: false
: false
: false
type _______IsEqual = [
IsFalse<IsEqual<string, 'a'>>,
IsFalse<IsEqual<'a', string>>,
IsFalse<IsEqual<{}, { a?: string }>>,
]

export type assertEqual<A, B> = IsEqual<A, B> extends true ? true : never
Expand Down
5 changes: 5 additions & 0 deletions src/lib/prelude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -762,3 +762,8 @@ type IsAnyUnionMemberExtends_<T, U> =
export type AnyAndUnknownToNever<T> = IsAny<T> extends true ? never : IsUnknown<T> extends true ? never : T

export type t<T> = T extends null ? {} | null : {}

export type PropertyKeyToString<$Key extends PropertyKey> = $Key extends string ? $Key
: $Key extends number ? $Key
: $Key extends symbol ? '<symbol>'
: never

0 comments on commit 6cba81d

Please sign in to comment.