-
Notifications
You must be signed in to change notification settings - Fork 312
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
improve: introduce robust client preset utility (#1250)
- Loading branch information
1 parent
e67dab0
commit e73241f
Showing
46 changed files
with
2,080 additions
and
441 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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({}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * as ClientPreset from './ClientPreset.js' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}>() | ||
}) |
Oops, something went wrong.