diff --git a/src/layers/5_core/_.ts b/src/layers/5_core/_.ts new file mode 100644 index 000000000..237287b63 --- /dev/null +++ b/src/layers/5_core/_.ts @@ -0,0 +1,2 @@ +export * from './core.js' +export * as Hooks from './hooks.js' diff --git a/src/layers/5_core/__.ts b/src/layers/5_core/__.ts index 994058c36..8b0f2fcfc 100644 --- a/src/layers/5_core/__.ts +++ b/src/layers/5_core/__.ts @@ -1 +1 @@ -export * as Core from './core.js' +export * as Core from './_.js' diff --git a/src/layers/5_core/core.ts b/src/layers/5_core/core.ts index 42df747b2..b12ff15f7 100644 --- a/src/layers/5_core/core.ts +++ b/src/layers/5_core/core.ts @@ -1,9 +1,6 @@ -import type { DocumentNode, ExecutionResult, GraphQLSchema } from 'graphql' -import { print } from 'graphql' +import { type ExecutionResult, print } from 'graphql' import { Anyware } from '../../lib/anyware/__.js' import { - type GraphQLRequestEncoded, - type GraphQLRequestInput, OperationTypeAccessTypeMap, parseGraphQLOperationType, type StandardScalarVariables, @@ -16,12 +13,10 @@ import { postRequestHeadersRec, } from '../../lib/graphqlHTTP.js' import { mergeRequestInit, searchParamsAppendAll } from '../../lib/http.js' -import { casesExhausted, throwNull } from '../../lib/prelude.js' +import { casesExhausted, getOptionalNullablePropertyOrThrow, throwNull } from '../../lib/prelude.js' import { execute } from '../0_functions/execute.js' -import type { Schema } from '../1_Schema/__.js' import { SelectionSet } from '../2_SelectionSet/__.js' -import type { GraphQLObjectSelection } from '../2_SelectionSet/print.js' -import * as Result from '../3_ResultSet/customScalars.js' +import * as CustomScalars from '../3_ResultSet/customScalars.js' import type { GraffleExecutionResultVar } from '../6_client/client.js' import type { Config } from '../6_client/Settings/Config.js' import { @@ -30,121 +25,7 @@ import { MethodMode, type MethodModeGetReads, } from '../6_client/transportHttp/request.js' -import type { - ContextInterfaceRaw, - ContextInterfaceTyped, - InterfaceRaw, - InterfaceTyped, - TransportHttp, - TransportMemory, -} from './types.js' - -const getRootIndexOrThrow = (context: ContextInterfaceTyped, rootTypeName: string) => { - // @ts-expect-error - - const rootIndex = context.schemaIndex.Root[rootTypeName] - if (!rootIndex) throw new Error(`Root type not found: ${rootTypeName}`) - return rootIndex -} - -type InterfaceInput = - | ({ - interface: InterfaceTyped - context: ContextInterfaceTyped - rootTypeName: Schema.RootTypeName - } & TypedProperties) - | ({ - interface: InterfaceRaw - context: ContextInterfaceRaw - } & RawProperties) - -// dprint-ignore - -type TransportInput<$Config extends Config, $HttpProperties = {}, $MemoryProperties = {}> = - | ( - TransportHttp extends $Config['transport']['type'] - ? ({ - transport: TransportHttp - - } & $HttpProperties) - : never - ) - | ( - TransportMemory extends $Config['transport']['type'] - ? ({ - transport: TransportMemory - } & $MemoryProperties) - : never - ) - -export const hookNamesOrderedBySequence = [`encode`, `pack`, `exchange`, `unpack`, `decode`] as const - -export type HookSequence = typeof hookNamesOrderedBySequence - -export type HookDefEncode<$Config extends Config> = { - input: - & InterfaceInput<{ selection: GraphQLObjectSelection }, GraphQLRequestInput> - & TransportInput<$Config, { schema: string | URL }, { schema: GraphQLSchema }> -} - -export type HookDefPack<$Config extends Config> = { - input: - & GraphQLRequestEncoded - & InterfaceInput - // todo why is headers here but not other http request properties? - & TransportInput<$Config, { url: string | URL; headers?: HeadersInit }, { - schema: GraphQLSchema - }> - 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. - */ - searchParams: getRequestEncodeSearchParameters - /** - * When request will be sent using POST this slot is called to create the value that will be used for the HTTP body. - */ - body: postRequestEncodeBody - } -} - -export type HookDefExchange<$Config extends Config> = { - slots: { - fetch: (request: Request) => Response | Promise - } - input: - & InterfaceInput - & TransportInput<$Config, { - request: CoreExchangePostRequest | CoreExchangeGetRequest - }, { - schema: GraphQLSchema - query: string | DocumentNode - variables?: StandardScalarVariables - operationName?: string - }> -} - -export type HookDefUnpack<$Config extends Config> = { - input: - & InterfaceInput - & TransportInput<$Config, { response: Response }, { - result: ExecutionResult - }> -} - -export type HookDefDecode<$Config extends Config> = { - input: - & { result: ExecutionResult } - & InterfaceInput - & TransportInput<$Config, { response: Response }> -} - -export type HookMap<$Config extends Config = Config> = { - encode: HookDefEncode<$Config> - pack: HookDefPack<$Config> - exchange: HookDefExchange<$Config> - unpack: HookDefUnpack<$Config> - decode: HookDefDecode<$Config> -} +import { type HookMap, hookNamesOrderedBySequence, type HookSequence } from './hooks.js' export const anyware = Anyware.create({ hookNamesOrderedBySequence, @@ -169,7 +50,7 @@ export const anyware = Anyware.create({ variables = undefined document = SelectionSet.Print.resolveRootType( input.context, - getRootIndexOrThrow(input.context, input.rootTypeName), + getOptionalNullablePropertyOrThrow(input.context.schemaIndex.Root, input.rootTypeName), input.selection, ) break @@ -346,7 +227,10 @@ export const anyware = Anyware.create({ // 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 - const dataDecoded = Result.decode(getRootIndexOrThrow(input.context, input.rootTypeName), input.result.data) + const dataDecoded = CustomScalars.decode( + getOptionalNullablePropertyOrThrow(input.context.schemaIndex.Root, input.rootTypeName), + input.result.data, + ) switch (input.transport) { case `memory`: { return { ...input.result, data: dataDecoded } diff --git a/src/layers/5_core/hooks.ts b/src/layers/5_core/hooks.ts new file mode 100644 index 000000000..256a821ce --- /dev/null +++ b/src/layers/5_core/hooks.ts @@ -0,0 +1,108 @@ +import type { DocumentNode, ExecutionResult, GraphQLSchema } from 'graphql' +import type { GraphQLRequestEncoded, GraphQLRequestInput, StandardScalarVariables } from '../../lib/graphql.js' +import type { getRequestEncodeSearchParameters, postRequestEncodeBody } from '../../lib/graphqlHTTP.js' +import type { Schema } from '../1_Schema/__.js' +import type { GraphQLObjectSelection } from '../2_SelectionSet/print.js' +import type { InterfaceTypedRequestContext, RequestContext } from '../6_client/client.js' +import type { Config } from '../6_client/Settings/Config.js' +import type { CoreExchangeGetRequest, CoreExchangePostRequest } from '../6_client/transportHttp/request.js' +import type { InterfaceRaw, InterfaceTyped, TransportHttp, TransportMemory } from './types.js' + +type InterfaceInput = + | ({ + interface: InterfaceTyped + context: InterfaceTypedRequestContext + rootTypeName: Schema.RootTypeName + } & TypedProperties) + | ({ + interface: InterfaceRaw + context: RequestContext + } & RawProperties) + +// dprint-ignore + +type TransportInput<$Config extends Config, $HttpProperties = {}, $MemoryProperties = {}> = + | ( + TransportHttp extends $Config['transport']['type'] + ? ({ + transport: TransportHttp + + } & $HttpProperties) + : never + ) + | ( + TransportMemory extends $Config['transport']['type'] + ? ({ + transport: TransportMemory + } & $MemoryProperties) + : never + ) + +export const hookNamesOrderedBySequence = [`encode`, `pack`, `exchange`, `unpack`, `decode`] as const + +export type HookSequence = typeof hookNamesOrderedBySequence + +export type HookDefEncode<$Config extends Config> = { + input: + & InterfaceInput<{ selection: GraphQLObjectSelection }, GraphQLRequestInput> + & TransportInput<$Config, { schema: string | URL }, { schema: GraphQLSchema }> +} + +export type HookDefPack<$Config extends Config> = { + input: + & GraphQLRequestEncoded + & InterfaceInput + // todo why is headers here but not other http request properties? + & TransportInput<$Config, { url: string | URL; headers?: HeadersInit }, { + schema: GraphQLSchema + }> + 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. + */ + searchParams: getRequestEncodeSearchParameters + /** + * When request will be sent using POST this slot is called to create the value that will be used for the HTTP body. + */ + body: postRequestEncodeBody + } +} + +export type HookDefExchange<$Config extends Config> = { + slots: { + fetch: (request: Request) => Response | Promise + } + input: + & InterfaceInput + & TransportInput<$Config, { + request: CoreExchangePostRequest | CoreExchangeGetRequest + }, { + schema: GraphQLSchema + query: string | DocumentNode + variables?: StandardScalarVariables + operationName?: string + }> +} + +export type HookDefUnpack<$Config extends Config> = { + input: + & InterfaceInput + & TransportInput<$Config, { response: Response }, { + result: ExecutionResult + }> +} + +export type HookDefDecode<$Config extends Config> = { + input: + & { result: ExecutionResult } + & InterfaceInput + & TransportInput<$Config, { response: Response }> +} + +export type HookMap<$Config extends Config = Config> = { + encode: HookDefEncode<$Config> + pack: HookDefPack<$Config> + exchange: HookDefExchange<$Config> + unpack: HookDefUnpack<$Config> + decode: HookDefDecode<$Config> +} diff --git a/src/layers/5_core/types.ts b/src/layers/5_core/types.ts index ffd32dc61..6b7085273 100644 --- a/src/layers/5_core/types.ts +++ b/src/layers/5_core/types.ts @@ -1,6 +1,3 @@ -import type { Schema } from '../1_Schema/__.js' -import type { Config } from '../6_client/Settings/Config.js' - export type Transport = TransportMemory | TransportHttp export type TransportMemory = typeof Transport.memory @@ -17,13 +14,3 @@ export type Interface = InterfaceRaw | InterfaceTyped export type InterfaceRaw = 'raw' export type InterfaceTyped = 'typed' - -type BaseContext = { - config: Config -} - -export type ContextInterfaceTyped = - & BaseContext - & ({ schemaIndex: Schema.Index }) - -export type ContextInterfaceRaw = BaseContext diff --git a/src/layers/6_client/client.ts b/src/layers/6_client/client.ts index 6358cdb36..faa0e7ea4 100644 --- a/src/layers/6_client/client.ts +++ b/src/layers/6_client/client.ts @@ -1,5 +1,4 @@ import { type ExecutionResult, GraphQLSchema } from 'graphql' -import type { Anyware } from '../../lib/anyware/__.js' import type { Errors } from '../../lib/errors/__.js' import type { Fluent } from '../../lib/fluent/__.js' import { type RootTypeName, RootTypeNameToOperationName } from '../../lib/graphql.js' @@ -10,11 +9,9 @@ import { readMaybeThunk } from '../1_Schema/core/helpers.js' import type { DocumentObject, GraphQLObjectSelection } from '../2_SelectionSet/print.js' import type { GlobalRegistry } from '../4_generator/globalRegistry.js' import { Core } from '../5_core/__.js' -import { type HookDefEncode } from '../5_core/core.js' import { type InterfaceRaw, type TransportHttp } from '../5_core/types.js' -import { type Extension } from './extension/extension.js' import { type UseFn, useProperties } from './extension/use.js' -import type { ClientContext, CreateState, FnParametersProperty } from './fluent.js' +import type { ClientContext, FnParametersProperty, State } from './fluent.js' import { handleOutput } from './handleOutput.js' import { anywareProperties, type FnAnyware } from './properties/anyware.js' import type { FnInternal } from './properties/internal.js' @@ -58,22 +55,16 @@ export type SelectionSetOrIndicator = boolean | object // todo get type from selectionset module export type SelectionSetOrArgs = object -export interface Context { - retry: null | Anyware.Extension2 - extensions: Extension[] +export interface RequestContext { config: Config + state: State } -export type TypedContext = Context & { +export interface InterfaceTypedRequestContext extends RequestContext { schemaIndex: Schema.Index } type RawParameters = [BaseInput_] -// | [ -// document: BaseInput['document'], -// options?: Omit, -// ] -// const resolveRawParameters = (parameters: RawParameters) => { // return parameters.length === 2 @@ -129,14 +120,13 @@ export const create: Create = (input) => { } const createWithState = ( - state: CreateState, + state: State, ) => { // todo lazily compute config, not every fluent call uses it. - const context: Context = { - retry: state.retry, - extensions: state.extensions, + const context: RequestContext = { // @ts-expect-error fixme config: inputToConfig(state.input), + state, } /** @@ -148,30 +138,25 @@ const createWithState = ( // const returnMode = input.returnMode ?? `data` as ReturnModeType const executeRootType = async ( - context: TypedContext, + context: InterfaceTypedRequestContext, rootTypeName: RootTypeName, rootTypeSelectionSet: GraphQLObjectSelection, ) => { const transport = state.input.schema instanceof GraphQLSchema ? `memory` : `http` const interface_ = `typed` - const initialInput = { + const anywareInitialInput = { interface: interface_, transport, selection: rootTypeSelectionSet, rootTypeName, schema: state.input.schema, - context: { - config: context.config, - transportInputOptions: state.input.transport, - interface: interface_, - schemaIndex: context.schemaIndex, - }, - } as HookDefEncode['input'] - return await run(context, initialInput) + context, + } as Core.Hooks.HookDefEncode['input'] + return await run(context, anywareInitialInput) } const executeRootTypeField = async ( - context: TypedContext, + context: InterfaceTypedRequestContext, rootTypeName: RootTypeName, rootTypeFieldName: string, argsOrSelectionSet?: object, @@ -209,7 +194,7 @@ const createWithState = ( : result[rootTypeFieldName] } - const createRootTypeMethods = (context: TypedContext, rootTypeName: RootTypeName) => { + const createRootTypeMethods = (context: InterfaceTypedRequestContext, rootTypeName: RootTypeName) => { return new Proxy({}, { get: (_, key) => { if (typeof key === `symbol`) throw new Error(`Symbols not supported.`) @@ -226,17 +211,16 @@ const createWithState = ( }) } - const run = async (context: Context, initialInput: HookDefEncode['input']) => { + const run = async (context: RequestContext, initialInput: Core.Hooks.HookDefEncode['input']) => { const result = await Core.anyware.run({ initialInput, - retryingExtension: context.retry as any, - extensions: context.extensions.filter(_ => _.onRequest !== undefined).map(_ => _.onRequest!) as any, - }) as GraffleExecutionResultVar - + retryingExtension: context.state.retry as any, + extensions: context.state.extensions.filter(_ => _.onRequest !== undefined).map(_ => _.onRequest!) as any, + }) return handleOutput(context, result) } - const runRaw = async (context: Context, rawInput: BaseInput_) => { + const runRaw = async (context: RequestContext, rawInput: BaseInput_) => { const interface_: InterfaceRaw = `raw` const transport = state.input.schema instanceof GraphQLSchema ? `memory` : `http` const initialInput = { @@ -244,12 +228,10 @@ const createWithState = ( transport, document: rawInput.document, schema: state.input.schema, - context: { - config: context.config, - }, + context, variables: rawInput.variables, operationName: rawInput.operationName, - } as HookDefEncode['input'] + } as Core.Hooks.HookDefEncode['input'] return await run(context, initialInput) } @@ -275,7 +257,7 @@ const createWithState = ( // todo extract this into constructor "create typed client" if (state.input.schemaIndex) { - const typedContext: TypedContext = { + const typedContext: InterfaceTypedRequestContext = { ...context, schemaIndex: state.input.schemaIndex, } diff --git a/src/layers/6_client/extension/extension.ts b/src/layers/6_client/extension/extension.ts index f386656f0..79fe24da6 100644 --- a/src/layers/6_client/extension/extension.ts +++ b/src/layers/6_client/extension/extension.ts @@ -3,7 +3,7 @@ import type { FnProperty } from '../../../lib/fluent/Fluent.js' import type { HKT } from '../../../lib/hkt/__.js' import type { Fn } from '../../../lib/hkt/hkt.js' import type { Core } from '../../5_core/__.js' -import type { Client, Context } from '../client.js' +import type { Client, RequestContext } from '../client.js' import type { Config } from '../Settings/Config.js' export interface TypeHooks { @@ -41,7 +41,7 @@ interface Base { */ onBuilderGet?: ( input: { - context: Context + context: RequestContext path: string[] property: string client: Client<{ schemaIndex: null; config: Config }> diff --git a/src/layers/6_client/fluent.ts b/src/layers/6_client/fluent.ts index 680df8d88..45bf559b7 100644 --- a/src/layers/6_client/fluent.ts +++ b/src/layers/6_client/fluent.ts @@ -20,13 +20,13 @@ export type FnParametersProperty = Fluent.FnParametersProperty -export type Builder = (input: CreateState) => Builder +export type Builder = (state: State) => Builder type PropertyDefinitions = Record Builder)> export const defineProperties = ( - definition: (builder: Builder, state: CreateState) => PropertyDefinitions, -): (builder: Builder, state: CreateState) => PropertyDefinitions => { + definition: (builder: Builder, state: State) => PropertyDefinitions, +): (builder: Builder, state: State) => PropertyDefinitions => { return (builder, state) => { return definition(builder, state) as any } @@ -46,7 +46,7 @@ export const defineProperties = ( // } // } -export interface CreateState { +export interface State { input: InputStatic retry: Anyware.Extension2 | null extensions: Extension[] diff --git a/src/layers/6_client/handleOutput.ts b/src/layers/6_client/handleOutput.ts index 0ec3a8434..1f694aa04 100644 --- a/src/layers/6_client/handleOutput.ts +++ b/src/layers/6_client/handleOutput.ts @@ -1,12 +1,11 @@ import type { GraphQLError } from 'graphql' import type { Simplify } from 'type-fest' -import type { ConditionalSimplify } from 'type-fest/source/conditional-simplify.js' import { Errors } from '../../lib/errors/__.js' import type { GraphQLExecutionResultError } from '../../lib/graphql.js' import { isRecordLikeObject, type SimplifyExceptError, type Values } from '../../lib/prelude.js' import type { Schema } from '../1_Schema/__.js' import { Transport } from '../5_core/types.js' -import type { Context, ErrorsOther, GraffleExecutionResultVar, TypedContext } from './client.js' +import type { ErrorsOther, GraffleExecutionResultVar, InterfaceTypedRequestContext, RequestContext } from './client.js' import { type Config, type ErrorCategory, @@ -16,7 +15,7 @@ import { } from './Settings/Config.js' export const handleOutput = ( - context: Context, + context: RequestContext, result: GraffleExecutionResultVar, ) => { // If core errors caused by an abort error then raise it as a direct error. @@ -65,57 +64,59 @@ export const handleOutput = ( ) if (isThrowExecution) throw error if (isReturnExecution) return error - return isEnvelope ? result : error + return isEnvelope ? { ...result, errors: [...result.errors ?? [], error] } : error } - { - if (isTypedContext(context)) { - if (c.errors.schema !== false) { - if (!isRecordLikeObject(result.data)) throw new Error(`Expected data to be an object.`) - const schemaErrors = Object.entries(result.data).map(([rootFieldName, rootFieldValue]) => { - // todo this check would be nice but it doesn't account for aliases right now. To achieve this we would - // need to have the selection set available to use and then do a costly analysis for all fields that were aliases. - // So costly that we would probably instead want to create an index of them on the initial encoding step and - // then make available down stream. Also, note, here, the hardcoding of Query, needs to be any root type. - // const isResultField = Boolean(schemaIndex.error.rootResultFields.Query[rootFieldName]) - // if (!isResultField) return null - // if (!isPlainObject(rootFieldValue)) return new Error(`Expected result field to be an object.`) - if (!isRecordLikeObject(rootFieldValue)) return null - const __typename = rootFieldValue[`__typename`] - if (typeof __typename !== `string`) throw new Error(`Expected __typename to be selected and a string.`) - const isErrorObject = Boolean( - context.schemaIndex.error.objectsTypename[__typename], - ) - if (!isErrorObject) return null - // todo extract message - // todo allow mapping error instances to schema errors - return new Error(`Failure on field ${rootFieldName}: ${__typename}`) - }).filter((_): _ is Error => _ !== null) - - const error = (schemaErrors.length === 1) - ? schemaErrors[0]! - : schemaErrors.length > 0 - ? new Errors.ContextualAggregateError( - `Two or more schema errors in the execution result.`, - {}, - schemaErrors, - ) - : null - if (error) { - if (isThrowSchema) throw error - if (isReturnSchema) { - return isEnvelope ? { ...result, errors: [...result.errors ?? [], error] } : error - } + if (isTypedContext(context)) { + if (c.errors.schema !== false) { + if (!isRecordLikeObject(result.data)) throw new Error(`Expected data to be an object.`) + const schemaErrors = Object.entries(result.data).map(([rootFieldName, rootFieldValue]) => { + // todo this check would be nice but it doesn't account for aliases right now. To achieve this we would + // need to have the selection set available to use and then do a costly analysis for all fields that were aliases. + // So costly that we would probably instead want to create an index of them on the initial encoding step and + // then make available down stream. Also, note, here, the hardcoding of Query, needs to be any root type. + // const isResultField = Boolean(schemaIndex.error.rootResultFields.Query[rootFieldName]) + // if (!isResultField) return null + // if (!isPlainObject(rootFieldValue)) return new Error(`Expected result field to be an object.`) + if (!isRecordLikeObject(rootFieldValue)) return null + + const __typename = rootFieldValue[`__typename`] + if (typeof __typename !== `string`) { + return new Error(`Expected __typename to be selected and a string.`) } - } - } - if (isEnvelope) { - return result + const isErrorObject = Boolean( + context.schemaIndex.error.objectsTypename[__typename], + ) + if (!isErrorObject) return null + // todo extract message + // todo allow mapping error instances to schema errors + return new Error(`Failure on field ${rootFieldName}: ${__typename}`) + }).filter((_): _ is Error => _ !== null) + + const error = (schemaErrors.length === 1) + ? schemaErrors[0]! + : schemaErrors.length > 0 + ? new Errors.ContextualAggregateError( + `Two or more schema errors in the execution result.`, + {}, + schemaErrors, + ) + : null + if (error) { + if (isThrowSchema) throw error + if (isReturnSchema) { + return isEnvelope ? { ...result, errors: [...result.errors ?? [], error] } : error + } + } } + } - return result.data + if (isEnvelope) { + return result } + + return result.data } const isAbortError = (error: any): error is DOMException & { name: 'AbortError' } => { @@ -125,7 +126,11 @@ const isAbortError = (error: any): error is DOMException & { name: 'AbortError' || (error instanceof Error && error.message.startsWith(`AbortError:`)) } -const isTypedContext = (context: Context): context is TypedContext => `schemaIndex` in context +const isTypedContext = (context: RequestContext): context is InterfaceTypedRequestContext => `schemaIndex` in context + +/** + * Types for output handling. + */ // dprint-ignore export type RawResolveOutputReturnRootType<$Config extends Config, $Data> = @@ -187,7 +192,7 @@ type IfConfiguredStripSchemaErrorsFromDataRootField<$Config extends Config, $Ind : ExcludeSchemaErrors<$Index, $Data> // dprint-ignore -export type ExcludeSchemaErrors<$Index extends Schema.Index, $Data> = +type ExcludeSchemaErrors<$Index extends Schema.Index, $Data> = Exclude< $Data, $Index['error']['objectsTypename'][keyof $Index['error']['objectsTypename']] @@ -200,12 +205,12 @@ export type ConfigGetOutputError<$Config extends Config, $ErrorCategory extends : ConfigResolveOutputErrorChannel<$Config, $Config['output']['errors'][$ErrorCategory]> // dprint-ignore -export type ConfigGetOutputEnvelopeErrorChannel<$Config extends Config, $ErrorCategory extends ErrorCategory> = +type ConfigGetOutputEnvelopeErrorChannel<$Config extends Config, $ErrorCategory extends ErrorCategory> = $Config['output']['envelope']['errors'][$ErrorCategory] extends true ? false : ConfigResolveOutputErrorChannel<$Config, $Config['output']['errors'][$ErrorCategory]> -export type ConfigResolveOutputErrorChannel<$Config extends Config, $Channel extends OutputChannelConfig | false> = +type ConfigResolveOutputErrorChannel<$Config extends Config, $Channel extends OutputChannelConfig | false> = $Channel extends 'default' ? $Config['output']['defaults']['errorChannel'] : $Channel extends false ? false : $Channel @@ -246,7 +251,3 @@ type IsEnvelopeWithoutErrors<$Config extends Config> = ? true : false : false - -export type SimplifyOutput = ConditionalSimplify - -export type SimplifyOutputUnion = T extends any ? SimplifyOutput : never diff --git a/src/lib/prelude.ts b/src/lib/prelude.ts index 99e575fa4..ad1dea1fc 100644 --- a/src/lib/prelude.ts +++ b/src/lib/prelude.ts @@ -210,6 +210,7 @@ export type LastOf = UnionToIntersection T : never> ext // export type IsMultiple = T extends 0 ? false : T extends 1 ? false : true export type ExcludeNull = Exclude +export type ExcludeNullAndUndefined = Exclude export const mapValues = < $Obj extends Record, @@ -555,5 +556,18 @@ export type OmitKeysWithPrefix<$Object extends object, $Prefix extends string> = : $Key ]: $Object[$Key] } + AssertIsEqual, { a: 1; b: 2 }>() AssertIsEqual, { b: 2 }>() + +export const getOptionalNullablePropertyOrThrow = < + $Record extends { [_ in keyof $Record]: unknown }, + $Key extends keyof $Record, +>( + record: $Record, + key: $Key, +): ExcludeNullAndUndefined<$Record[$Key]> => { + const value = record[key] + if (value === undefined || value === null) throw new Error(`Key not found: ${String(key)}`) + return value as ExcludeNullAndUndefined<$Record[$Key]> +}