Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve: introduce robust client preset utility #1250

Merged
merged 8 commits into from
Nov 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions src/ClientPreset/ClientPreset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import type { CamelCase } from 'type-fest'
import type {
Extension,
ExtensionConstructor,
ExtensionInputParametersNone,
ExtensionInputParametersOptional,
ExtensionInputParametersRequired,
InferExtensionFromConstructor,
} from '../extension/extension.js'
import type { UseExtensionDo } from '../layers/6_client/builderExtensions/use.js'
import { type Client, createWithContext } from '../layers/6_client/client.js'
import { type Context, createContext, type TypeHooksEmpty } from '../layers/6_client/context.js'
import type { InputBase } from '../layers/6_client/Settings/Input.js'
import type { NormalizeInput } from '../layers/6_client/Settings/InputToConfig.js'
import type { Builder } from '../lib/builder/__.js'
import type { ConfigManager } from '../lib/config-manager/__.js'
import { type mergeArrayOfObjects, type ToParametersExact } from '../lib/prelude.js'
import type { GlobalRegistry } from '../types/GlobalRegistry/GlobalRegistry.js'
import { Schema } from '../types/Schema/__.js'
import type { SchemaDrivenDataMap } from '../types/SchemaDrivenDataMap/__.js'

/**
* Create a Client constructor with some initial context.
*
* Extensions constructors can be given. Their constructor parameters will
* be merged into the client constructor under a key matching the name of the extension.
*/
export const create: CreatePrefilled = (args) => {
const constructor = (input: any) => { // todo generic input type
const extensions = args.extensions?.map(extCtor => {
const extCtor_: (args: object | undefined) => Extension = extCtor
const keywordArgs: undefined | object = input?.[extCtor.info.name]
return extCtor_(keywordArgs)
}) ?? []

const scalars = args.scalars ?? Schema.Scalar.Registry.empty
const schemaMap = args.sddm ?? null

const initialState = createContext({
name: args.name,
extensions,
scalars,
schemaMap,
input: {
schema: args.schemaUrl,
// eslint-disable-next-line
// @ts-ignore passes after generation
...input,
name: args.name,
},
// retry: null,
})

const instance = createWithContext(initialState)

return instance
}

return constructor as any
}

// dprint-ignore
type CreatePrefilled = <
const $Name extends string,
$Scalars extends Schema.Scalar.Registry,
const $ExtensionConstructors extends [...ExtensionConstructor<any>[]],
$Params extends {
name: $Name
sddm?: SchemaDrivenDataMap
scalars?: $Scalars
schemaUrl?: URL | undefined
extensions?: $ExtensionConstructors
},
>(keywordArgs: $Params) =>
{
preset: $Params
<$ClientKeywordArgs extends ConstructorParameters<$Name, ConfigManager.OrDefault<$Params['extensions'], []>>>(
...args: ToParametersExact<
$ClientKeywordArgs,
ConstructorParameters<$Name, ConfigManager.OrDefault<$Params['extensions'], []>>
>
): ApplyPrefilledExtensions<
ConfigManager.OrDefault<$Params['extensions'], []>,
// @ts-expect-error fixme
Client<{
input: $ClientKeywordArgs
name: $Params['name']
schemaMap: ConfigManager.OrDefault<$Params['sddm'], null>
scalars: ConfigManager.OrDefault<$Params['scalars'], Schema.Scalar.Registry.Empty>
config: NormalizeInput<$ClientKeywordArgs & { name: $Name; schemaMap: SchemaDrivenDataMap }>
typeHooks: TypeHooksEmpty
// This will be populated by statically applying preset extensions.
extensions: []
// retry: null
}>
>
}

type ConstructorParameters<
$Name extends string,
$Extensions extends [...ExtensionConstructor[]],
> =
& InputBase<GlobalRegistry.GetOrGeneric<$Name>>
& mergeArrayOfObjects<GetParametersContributedByExtensions<$Extensions>>

// dprint-ignore
type GetParametersContributedByExtensions<Extensions extends [...ExtensionConstructor[]]> = {
[$Index in keyof Extensions]:
Extensions[$Index]['info']['configInputParameters'] extends ExtensionInputParametersNone ? {} :
Extensions[$Index]['info']['configInputParameters'] extends ExtensionInputParametersRequired ? { [_ in CamelCase<Extensions[$Index]['info']['name']>]: Extensions[$Index]['info']['configInputParameters'][0] } :
Extensions[$Index]['info']['configInputParameters'] extends ExtensionInputParametersOptional ? { [_ in CamelCase<Extensions[$Index]['info']['name']>]?: Extensions[$Index]['info']['configInputParameters'][0] } :
{}
}

// dprint-ignore
type ApplyPrefilledExtensions<
$ExtensionConstructors extends [...ExtensionConstructor[]],
$Client extends Client<Context>,
> =
$ExtensionConstructors extends []
? $Client
: $ExtensionConstructors extends [infer $ExtensionConstructor extends ExtensionConstructor, ...infer $Rest extends ExtensionConstructor[]]
? ApplyPrefilledExtensions<
$Rest,
// @ts-expect-error fixme
UseExtensionDo<
Builder.Private.Get<$Client>,
InferExtensionFromConstructor<$ExtensionConstructor>
>
>
: never
50 changes: 50 additions & 0 deletions src/ClientPreset/__.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { IntrospectionQuery } from 'graphql'
import { Introspection } from '../extensions/Introspection/Introspection.js'
import { create } from '../layers/6_client/client.js'
import { assertEqual, assertExtends } from '../lib/assert-equal.js'
import { ClientPreset } from './__.js'

// Baseline tests of the base client constructor.
// These are here for easy comparison to the preset client.
{
const graffle = create({
name: `test`,
// @ts-expect-error not available
introspection: {
options: {
descriptions: true,
},
},
})
assertEqual<typeof graffle._.name, string>()
}

// Preset Without Extensions.
{
const create = ClientPreset.create({ name: `test` })
assertEqual<typeof create.preset, { name: 'test' }>()
const graffle = create()
assertEqual<typeof graffle._.name, 'test'>()
assertEqual<typeof graffle._.typeHooks.onRequestResult, []>()
}

// Preset With Extensions.
{
const create = ClientPreset.create({
name: `test`,
extensions: [Introspection],
})
assertEqual<typeof create.preset, { name: 'test'; extensions: [any] }>()
const graffle = create({
// Extension config is available here
introspection: {
options: {
descriptions: true,
},
},
})
assertEqual<typeof graffle._.typeHooks.onRequestResult, []>()
assertExtends<typeof graffle._, { name: 'test'; extensions: [{ name: 'Introspection' }] }>()
const result = await graffle.introspect()
assertEqual<typeof result, IntrospectionQuery | null>()
}
44 changes: 44 additions & 0 deletions src/ClientPreset/__.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { expect, test } from 'vitest'
import { createExtension } from '../entrypoints/extensionkit.js'
import { Introspection } from '../extensions/Introspection/Introspection.js'
import { ClientPreset } from './__.js'

test(`Preset extension is used on constructed client`, () => {
const create = ClientPreset.create({ name: `test`, extensions: [Introspection] })
const graffle = create({ introspection: { options: { descriptions: true } } })
expect(typeof graffle.introspect).toBe(`function`)
})

test(`If extension required input then client constructor input property is NOT optional`, () => {
const Ex = createExtension({
name: `test`,
normalizeConfig: (_: { a: 1; b?: 2 }) => {
return { a: 11, b: 22 }
},
create: () => {
return {}
},
})
const create = ClientPreset.create({ name: `test`, extensions: [Ex] })
// @ts-expect-error Arguments required.
create()
// @ts-expect-error Arguments required.
create({})
create({ test: { a: 1 } })
})

test(`If extension has no required input then client constructor input property IS optional`, () => {
const Ex = createExtension({
name: `test`,
normalizeConfig: (_?: { a?: 1; b?: 2 }) => {
return { a: 11, b: 22 }
},
create: () => {
return {}
},
})
const create = ClientPreset.create({ name: `test`, extensions: [Ex] })
// OK. Arguments NOT required.
create()
create({})
})
1 change: 1 addition & 0 deletions src/ClientPreset/__.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * as ClientPreset from './ClientPreset.js'
2 changes: 1 addition & 1 deletion src/entrypoints/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { ClientPreset } from '../ClientPreset/__.js'
export { create as createSelect, select } from '../layers/5_select/select.js'
export { type Client, create } from '../layers/6_client/client.js'
export { createPrefilled } from '../layers/6_client/clientPrefilled.js'
export { type InputStatic } from '../layers/6_client/Settings/Input.js'
79 changes: 79 additions & 0 deletions src/extension/extension.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { describe, expectTypeOf, test } from 'vitest'
import { createExtension } from './extension.js'

describe(`constructor arguments`, () => {
test(`normalizeConfig undefined -> constructor input forbidden`, () => {
const Ex = createExtension({
name: `test`,
create: () => {
return {}
},
})
Ex()
// @ts-expect-error Arguments forbidden.
Ex({})
})
test(`normalizeConfig with optional keys -> constructor input optional`, () => {
const Ex = createExtension({
name: `test`,
normalizeConfig: (_?: { a?: 1; b?: 2 }) => {
return { a: 11, b: 22 }
},
create: () => {
return {}
},
})
Ex()
Ex({})
Ex({ a: 1 })
})
test(`normalizeConfig with required input (but optional keys) -> constructor input required`, () => {
const Ex = createExtension({
name: `test`,
normalizeConfig: (_: { a?: 1; b?: 2 }) => {
return { a: 11, b: 22 }
},
create: () => {
return {}
},
})
// @ts-expect-error Arguments required.
Ex()
Ex({})
Ex({ a: 1 })
})
test(`normalizeConfig with required keys -> constructor input required`, () => {
const Ex = createExtension({
name: `test`,
normalizeConfig: (_: { a: 1; b?: 2 }) => {
return { a: 11, b: 22 }
},
create: () => {
return {}
},
})
// @ts-expect-error Arguments required.
Ex()
// @ts-expect-error Arguments required.
Ex({})
Ex({ a: 1 })
})
})

test(`type hooks is empty by default`, () => {
const Ex = createExtension({
name: `test`,
create: () => {
return {}
},
})
expectTypeOf(Ex.info.typeHooks).toEqualTypeOf<{
onRequestResult: undefined
onRequestDocumentRootType: undefined
}>()
const ex = Ex()
expectTypeOf(ex.typeHooks).toEqualTypeOf<{
onRequestResult: undefined
onRequestDocumentRootType: undefined
}>()
})
Loading