diff --git a/src/entrypoints/alpha/client.ts b/src/entrypoints/alpha/client.ts index e619e2979..9215ba1a5 100644 --- a/src/entrypoints/alpha/client.ts +++ b/src/entrypoints/alpha/client.ts @@ -1,2 +1,2 @@ -export * from '../../layers/4_client/client.js' -export { create as createSelect, select } from '../../layers/4_select/select.js' +export * from '../../layers/5_client/client.js' +export { create as createSelect, select } from '../../layers/5_select/select.js' diff --git a/src/entrypoints/alpha/schema.ts b/src/entrypoints/alpha/schema.ts index 16d24cbe4..f7b20fd98 100644 --- a/src/entrypoints/alpha/schema.ts +++ b/src/entrypoints/alpha/schema.ts @@ -1,3 +1,3 @@ export * from '../../layers/1_Schema/__.js' -export { ResultSet } from '../../layers/3_IO/ResultSet/__.js' -export { SelectionSet } from '../../layers/3_IO/SelectionSet/__.js' +export { SelectionSet } from '../../layers/3_SelectionSet/__.js' +export { ResultSet } from '../../layers/4_ResultSet/__.js' diff --git a/src/layers/3_IO/ResultSet/__.ts b/src/layers/3_IO/ResultSet/__.ts deleted file mode 100644 index fb546c609..000000000 --- a/src/layers/3_IO/ResultSet/__.ts +++ /dev/null @@ -1 +0,0 @@ -export * as ResultSet from './ResultSet.js' diff --git a/src/layers/3_IO/SelectionSet/_.ts b/src/layers/3_IO/SelectionSet/_.ts deleted file mode 100644 index 81516ae41..000000000 --- a/src/layers/3_IO/SelectionSet/_.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './SelectionSet.js' -export * as Print from './toGraphQLDocumentString.js' diff --git a/src/layers/3_IO/SelectionSet/toGraphQLDocumentString.ts b/src/layers/3_IO/SelectionSet/toGraphQLDocumentString.ts deleted file mode 100644 index e23763293..000000000 --- a/src/layers/3_IO/SelectionSet/toGraphQLDocumentString.ts +++ /dev/null @@ -1,329 +0,0 @@ -import { RootTypeName } from '../../../lib/graphql.js' -import { lowerCaseFirstLetter } from '../../../lib/prelude.js' -import { Schema } from '../../1_Schema/__.js' -import { readMaybeThunk } from '../../1_Schema/core/helpers.js' -import type { ReturnModeType } from '../../4_client/Config.js' -import type { SelectionSet } from './__.js' -import { aliasPattern, fragmentPattern } from './SelectionSet.js' - -type SpecialFields = { - // todo - this requires having the schema at runtime to know which fields to select. - // $scalars?: SelectionSet.Indicator - $include?: SelectionSet.Directive.Include['$include'] - $skip?: SelectionSet.Directive.Skip['$skip'] - $defer?: SelectionSet.Directive.Defer['$defer'] - $stream?: SelectionSet.Directive.Stream['$stream'] - $?: Args -} - -type Args = { [k: string]: Args_ } - -type Args_ = string | boolean | null | number | Args - -type Indicator = 0 | 1 | boolean - -export type DocumentObject = Record - -export type GraphQLRootSelection = { query: GraphQLObjectSelection } | { mutation: GraphQLObjectSelection } - -export type GraphQLObjectSelection = Record - -export type SS = { - [k: string]: Indicator | SS -} & SpecialFields - -export interface Context { - schemaIndex: Schema.Index - config: { - returnMode: ReturnModeType - } -} - -export const rootTypeSelectionSet = ( - context: Context, - schemaObject: Schema.Object$2, - ss: GraphQLObjectSelection, - name?: string, -) => { - return `${lowerCaseFirstLetter(schemaObject.fields.__typename.type.type)} ${name ?? ``} { ${ - selectionSet(context, schemaObject, ss) - } }` -} - -const directiveArgs = (config: object) => { - return Object.entries(config).filter(([_, v]) => v !== undefined).map(([k, v]) => { - return `${k}: ${JSON.stringify(v)}` - }).join(`, `) -} - -const resolveDirectives = (ss: Indicator | SS) => { - if (isIndicator(ss)) return `` - const { $include, $skip, $defer, $stream } = ss - - let directives = `` - - if ($stream !== undefined) { - const config = { - if: typeof $stream === `boolean` ? $stream : $stream.if === undefined ? true : $stream.if, - label: typeof $stream === `boolean` ? undefined : $stream.label, - initialCount: typeof $stream === `boolean` ? undefined : $stream.initialCount, - } - directives += `@stream(${directiveArgs(config)})` - } - - if ($defer !== undefined) { - const config = { - if: typeof $defer === `boolean` ? $defer : $defer.if === undefined ? true : $defer.if, - label: typeof $defer === `boolean` ? undefined : $defer.label, - } - directives += `@defer(${directiveArgs(config)})` - } - - if ($include !== undefined) { - directives += `@include(if: ${ - String(typeof $include === `boolean` ? $include : $include.if === undefined ? true : $include.if) - })` - } - - if ($skip !== undefined) { - directives += `@skip(if: ${String(typeof $skip === `boolean` ? $skip : $skip.if === undefined ? true : $skip.if)})` - } - - return directives -} - -const resolveArgs = (schemaField: Schema.SomeField, ss: Indicator | SS) => { - if (isIndicator(ss)) return `` - const { $ } = ss - let args = `` - if ($ !== undefined) { - const schemaArgs = schemaField.args - if (!schemaArgs) throw new Error(`Field has no args`) - - const entries = Object.entries($) - args = entries.length === 0 ? `` : `(${ - entries.map(([argName, v]) => { - const schemaArg = schemaArgs.fields[argName] as Schema.Input.Any | undefined // eslint-disable-line - if (!schemaArg) throw new Error(`Arg ${argName} not found in schema field`) - if (schemaArg.kind === `Enum`) { - return `${argName}: ${String(v)}` - } else { - // todo if enum, do not quote, requires schema index - return `${argName}: ${JSON.stringify(v)}` - } - }).join(`, `) - })` - } - return args -} -const pruneNonSelections = (ss: SS) => { - const entries = Object.entries(ss) - const selectEntries = entries.filter(_ => !_[0].startsWith(`$`)) - return Object.fromEntries(selectEntries) -} - -const indicatorOrSelectionSet = ( - context: Context, - schemaField: Schema.SomeField, - ss: null | Indicator | SS, -): string => { - if (ss === null) return `null` // todo test this case - if (isIndicator(ss)) return `` - - const entries = Object.entries(ss) - const selectEntries = entries.filter(_ => !_[0].startsWith(`$`)) - const directives = resolveDirectives(ss) - const args = resolveArgs(schemaField, ss) - - if (selectEntries.length === 0) { - return `${args} ${directives}` - } - - const selection = Object.fromEntries(selectEntries) as GraphQLObjectSelection - - // eslint-disable-next-line - // @ts-ignore ID error - const schemaNamedOutputType = Schema.Output.unwrapToNamed(schemaField.type) as Schema.Object$2 - return `${args} ${directives} { - ${selectionSet(context, readMaybeThunk(schemaNamedOutputType), selection)} - }` -} - -export const selectionSet = ( - context: Context, - schemaItem: Schema.Object$2 | Schema.Union | Schema.Interface, - ss: Indicator | SS, -): string => { - // todo optimize by doing single loop - const applicableSelections = Object.entries(ss).filter(([_, ss]) => isPositiveIndicator(ss)) as [ - string, - SS | Indicator, - ][] - switch (schemaItem.kind) { - case `Object`: { - const rootTypeName = (RootTypeName as Record)[schemaItem.fields.__typename.type.type] - ?? null - return applicableSelections.map(([fieldExpression, ss]) => { - const fieldName = parseFieldName(fieldExpression) - const schemaField = schemaItem.fields[fieldName.actual] - if (!schemaField) throw new Error(`Field ${fieldExpression} not found in schema object`) - // dprint-ignore - if (rootTypeName&&context.config.returnMode===`successData`&&context.schemaIndex.error.rootResultFields[rootTypeName][fieldName.actual]) { - (ss as Record)[`__typename`] = true - } - return `${resolveFragment(resolveAlias(fieldExpression))} ${indicatorOrSelectionSet(context, schemaField, ss)}` - }).join(`\n`) + `\n` - } - case `Interface`: { - return applicableSelections.map(([fieldExpression, ss]) => { - const fieldItem = parseFieldItem(fieldExpression) - switch (fieldItem._tag) { - case `FieldName`: { - if (fieldItem.actual === `__typename`) { - return `${renderFieldName(fieldItem)} ${resolveDirectives(ss)}` - } - const schemaField = schemaItem.fields[fieldItem.actual] - if (!schemaField) throw new Error(`Field ${fieldExpression} not found in schema object`) - // dprint-ignore - return `${resolveFragment(resolveAlias(fieldExpression))} ${ indicatorOrSelectionSet(context, schemaField, ss) }` - } - case `FieldOn`: { - const schemaObject = context.schemaIndex[`objects`][fieldItem.typeOrFragmentName] - if (!schemaObject) throw new Error(`Fragment ${fieldItem.typeOrFragmentName} not found in schema`) - return `${renderOn(fieldItem)} ${resolveDirectives(ss)} { ${selectionSet(context, schemaObject, ss)} }` - } - default: { - throw new Error(`Unknown field item tag`) - } - } - }).join(`\n`) + `\n` - } - case `Union`: { - return applicableSelections.map(([fieldExpression, ss]) => { - const fieldItem = parseFieldItem(fieldExpression) - switch (fieldItem._tag) { - case `FieldName`: { - if (fieldItem.actual === `__typename`) { - return `${renderFieldName(fieldItem)} ${resolveDirectives(ss)}` - } - // todo - throw new Error(`todo resolve common interface fields from unions`) - } - case `FieldOn`: { - const schemaObject = context.schemaIndex[`objects`][fieldItem.typeOrFragmentName] - if (!schemaObject) throw new Error(`Fragment ${fieldItem.typeOrFragmentName} not found in schema`) - // if (isIndicator(ss)) throw new Error(`Union field must have selection set`) - return `${renderOn(fieldItem)} ${resolveDirectives(ss)} { ${ - // @ts-expect-error fixme - selectionSet(context, schemaObject, pruneNonSelections(ss))} }` - } - default: { - throw new Error(`Unknown field item tag`) - } - } - }).join(`\n`) + `\n` - } - default: - throw new Error(`Unknown schema item kind`) - } -} - -type FieldItem = FieldOn | FieldName - -const parseFieldItem = (field: string): FieldItem => { - const on = parseOnExpression(field) - if (on) return on - return parseFieldName(field) -} - -interface FieldOn { - _tag: 'FieldOn' - typeOrFragmentName: string -} - -const parseOnExpression = (field: string): null | FieldOn => { - const match = field.match(fragmentPattern) - if (match?.groups) { - return { - _tag: `FieldOn`, - typeOrFragmentName: match.groups[`name`]!, - } - } - return null -} - -const renderOn = (on: FieldOn) => { - return `...on ${on.typeOrFragmentName}` -} - -// todo use a given schema to ensure that field is actually a fragment and not just happened to be using pattern onX -const resolveFragment = (field: string) => { - const match = field.match(fragmentPattern) - if (match?.groups) { - return `...on ${match.groups[`name`]!}` - } - return field -} - -interface FieldName { - _tag: 'FieldName' - actual: string - alias: string | null -} -// todo use a given schema to ensure that field is actually a fragment and not just happened to be using pattern onX -const parseFieldName = (field: string): FieldName => { - const match = field.match(aliasPattern) - if (match?.groups) { - return { - _tag: `FieldName`, - actual: match.groups[`actual`]!, - alias: match.groups[`alias`]!, - } - } - return { - _tag: `FieldName`, - actual: field, - alias: null, - } -} - -const renderFieldName = (fieldName: FieldName) => { - if (fieldName.alias) { - return `${fieldName.actual}: ${fieldName.alias}` - } else { - return fieldName.actual - } -} - -// todo use a given schema to ensure that field is actually a fragment and not just happened to be using pattern onX -const resolveAlias = (field: string) => { - const match = field.match(aliasPattern) - if (match?.groups) { - return `${match.groups[`actual`]!}: ${match.groups[`alias`]!}` - } - return field -} - -const isIndicator = (v: any): v is Indicator => { - return String(v) in indicator -} - -const isPositiveIndicator = (v: any): v is SelectionSet.ClientIndicatorPositive => { - return !(String(v) in negativeIndicator) -} - -const negativeIndicator = { - '0': 0, - 'false': false, - 'undefined': undefined, -} - -const positiveIndicator = { - '1': 1, - 'true': true, -} - -const indicator = { - ...negativeIndicator, - ...positiveIndicator, -} diff --git a/src/layers/3_SelectionSet/_.ts b/src/layers/3_SelectionSet/_.ts new file mode 100644 index 000000000..4871ff9d0 --- /dev/null +++ b/src/layers/3_SelectionSet/_.ts @@ -0,0 +1,2 @@ +export * as Print from './encode.js' +export * from './types.js' diff --git a/src/layers/3_IO/SelectionSet/__.ts b/src/layers/3_SelectionSet/__.ts similarity index 100% rename from src/layers/3_IO/SelectionSet/__.ts rename to src/layers/3_SelectionSet/__.ts diff --git a/src/layers/3_IO/SelectionSet/__snapshots__/toGraphQLDocumentString.test.ts.snap b/src/layers/3_SelectionSet/__snapshots__/encode.test.ts.snap similarity index 100% rename from src/layers/3_IO/SelectionSet/__snapshots__/toGraphQLDocumentString.test.ts.snap rename to src/layers/3_SelectionSet/__snapshots__/encode.test.ts.snap diff --git a/src/layers/3_IO/SelectionSet/toGraphQLDocumentString.test.ts b/src/layers/3_SelectionSet/encode.test.ts similarity index 94% rename from src/layers/3_IO/SelectionSet/toGraphQLDocumentString.test.ts rename to src/layers/3_SelectionSet/encode.test.ts index 61d6252c5..ffabbc168 100644 --- a/src/layers/3_IO/SelectionSet/toGraphQLDocumentString.test.ts +++ b/src/layers/3_SelectionSet/encode.test.ts @@ -1,10 +1,10 @@ import { parse, print } from 'graphql' import { describe, expect, test } from 'vitest' -import type { Index } from '../../../../tests/_/schema/generated/Index.js' -import { $Index as schemaIndex } from '../../../../tests/_/schema/generated/SchemaRuntime.js' +import type { Index } from '../../../tests/_/schema/generated/Index.js' +import { $Index as schemaIndex } from '../../../tests/_/schema/generated/SchemaRuntime.js' import type { SelectionSet } from './__.js' -import type { Context } from './toGraphQLDocumentString.js' -import { rootTypeSelectionSet } from './toGraphQLDocumentString.js' +import type { Context } from './encode.js' +import { rootTypeSelectionSet } from './encode.js' // eslint-disable-next-line // @ts-ignore diff --git a/src/layers/3_SelectionSet/encode.ts b/src/layers/3_SelectionSet/encode.ts new file mode 100644 index 000000000..72fbb8c12 --- /dev/null +++ b/src/layers/3_SelectionSet/encode.ts @@ -0,0 +1,234 @@ +import { RootTypeName } from '../../lib/graphql.js' +import { lowerCaseFirstLetter } from '../../lib/prelude.js' +import { Schema } from '../1_Schema/__.js' +import { readMaybeThunk } from '../1_Schema/core/helpers.js' +import type { ReturnModeType } from '../5_client/Config.js' +import type { SelectionSet } from './__.js' +import { isSelectFieldName } from './helpers.js' +import { parseClientDirectiveDefer } from './runtime/directives/defer.js' +import { toGraphQLDirective } from './runtime/directives/directive.js' +import { parseClientDirectiveInclude } from './runtime/directives/include.js' +import { parseClientDirectiveSkip } from './runtime/directives/skip.js' +import { parseClientDirectiveStream } from './runtime/directives/stream.js' +import { parseClientFieldItem } from './runtime/FieldItem.js' +import { parseClientFieldName, toGraphQLFieldName } from './runtime/FieldName.js' +import type { Indicator } from './runtime/indicator.js' +import { isIndicator, isPositiveIndicator } from './runtime/indicator.js' +import { parseClientOn, toGraphQLOn } from './runtime/on.js' + +type SpecialFields = { + // todo - this requires having the schema at runtime to know which fields to select. + // $scalars?: SelectionSet.Indicator + $include?: SelectionSet.Directive.Include['$include'] + $skip?: SelectionSet.Directive.Skip['$skip'] + $defer?: SelectionSet.Directive.Defer['$defer'] + $stream?: SelectionSet.Directive.Stream['$stream'] + $?: Args +} + +type Args = { [k: string]: Args_ } + +type Args_ = string | boolean | null | number | Args + +export type DocumentObject = Record + +export type GraphQLRootSelection = { query: GraphQLObjectSelection } | { mutation: GraphQLObjectSelection } + +export type GraphQLObjectSelection = Record + +export type SS = { + [k: string]: Indicator | SS +} & SpecialFields + +type FieldValue = SS | Indicator + +export interface Context { + schemaIndex: Schema.Index + config: { + returnMode: ReturnModeType + } +} + +export const rootTypeSelectionSet = ( + context: Context, + schemaObject: Schema.Object$2, + ss: GraphQLObjectSelection, + operationName: string = ``, +) => { + const operationTypeName = lowerCaseFirstLetter(schemaObject.fields.__typename.type.type) + return `${operationTypeName} ${operationName} { ${resolveObjectLikeFieldValue(context, schemaObject, ss)} }` +} + +const resolveDirectives = (fieldValue: FieldValue) => { + if (isIndicator(fieldValue)) return `` + + const { $include, $skip, $defer, $stream } = fieldValue + + let directives = `` + + if ($stream !== undefined) { + directives += toGraphQLDirective(parseClientDirectiveStream($stream)) + } + + if ($defer !== undefined) { + directives += toGraphQLDirective(parseClientDirectiveDefer($defer)) + } + + if ($include !== undefined) { + directives += toGraphQLDirective(parseClientDirectiveInclude($include)) + } + + if ($skip !== undefined) { + directives += toGraphQLDirective(parseClientDirectiveSkip($skip)) + } + + return directives +} + +const resolveArgs = (schemaField: Schema.SomeField, ss: Indicator | SS) => { + if (isIndicator(ss)) return `` + + const { $ } = ss + + let args = `` + if ($ !== undefined) { + const schemaArgs = schemaField.args + if (!schemaArgs) throw new Error(`Field has no args`) + + const entries = Object.entries($) + args = entries.length === 0 ? `` : `(${ + entries.map(([argName, v]) => { + const schemaArg = schemaArgs.fields[argName] as Schema.Input.Any | undefined // eslint-disable-line + if (!schemaArg) throw new Error(`Arg ${argName} not found in schema field`) + if (schemaArg.kind === `Enum`) { + return `${argName}: ${String(v)}` + } else { + // todo if enum, do not quote, requires schema index + return `${argName}: ${JSON.stringify(v)}` + } + }).join(`, `) + })` + } + + return args +} +const pruneNonSelections = (ss: SS) => { + const entries = Object.entries(ss) + const selectEntries = entries.filter(_ => !_[0].startsWith(`$`)) + return Object.fromEntries(selectEntries) +} + +const resolveFieldValue = ( + context: Context, + schemaField: Schema.SomeField, + fieldValue: null | FieldValue, +): string => { + if (fieldValue === null) return `null` // todo test this case + + if (isIndicator(fieldValue)) return `` + + const entries = Object.entries(fieldValue) + const directives = resolveDirectives(fieldValue) + const args = resolveArgs(schemaField, fieldValue) + const selects = entries.filter(_ => isSelectFieldName(_[0])) + + if (selects.length === 0) { + return `${args} ${directives}` + } + + const selection = Object.fromEntries(selects) as GraphQLObjectSelection + + // eslint-disable-next-line + // @ts-ignore ID error + const schemaNamedOutputType = Schema.Output.unwrapToNamed(schemaField.type) as Schema.Object$2 + return `${args} ${directives} { + ${resolveObjectLikeFieldValue(context, readMaybeThunk(schemaNamedOutputType), selection)} + }` +} + +export const resolveObjectLikeFieldValue = ( + context: Context, + schemaItem: Schema.Object$2 | Schema.Union | Schema.Interface, + fieldValue: FieldValue, +): string => { + // todo optimize by doing single loop + const applicableSelections = Object.entries(fieldValue).filter(([_, ss]) => isPositiveIndicator(ss)) as [ + string, + FieldValue, + ][] + switch (schemaItem.kind) { + case `Object`: { + const rootTypeName = (RootTypeName as Record)[schemaItem.fields.__typename.type.type] + ?? null + return applicableSelections.map(([clientFieldName, ss]) => { + const fieldName = parseClientFieldName(clientFieldName) + const schemaField = schemaItem.fields[fieldName.actual] + if (!schemaField) throw new Error(`Field ${clientFieldName} not found in schema object`) + // dprint-ignore + if (rootTypeName && context.config.returnMode === `successData` && context.schemaIndex.error.rootResultFields[rootTypeName][fieldName.actual]) { + (ss as Record)[`__typename`] = true + } + return `${toGraphQLFieldName(fieldName)} ${resolveFieldValue(context, schemaField, ss)}` + }).join(`\n`) + `\n` + } + case `Interface`: { + return applicableSelections.map(([ClientFieldName, ss]) => { + const fieldItem = parseClientFieldItem(ClientFieldName) + + switch (fieldItem._tag) { + case `FieldName`: { + if (fieldItem.actual === `__typename`) { + return `${toGraphQLFieldName(fieldItem)} ${resolveDirectives(ss)}` + } + const schemaField = schemaItem.fields[fieldItem.actual] + if (!schemaField) throw new Error(`Field ${ClientFieldName} not found in schema object`) + return `${toGraphQLFieldName(fieldItem)} ${resolveFieldValue(context, schemaField, ss)}` + } + case `On`: { + const schemaObject = context.schemaIndex[`objects`][fieldItem.typeOrFragmentName] + if (!schemaObject) throw new Error(`Fragment ${fieldItem.typeOrFragmentName} not found in schema`) + return `${toGraphQLOn(fieldItem)} ${resolveDirectives(ss)} { ${ + resolveObjectLikeFieldValue(context, schemaObject, ss) + } }` + } + default: { + throw new Error(`Unknown field item tag`) + } + } + }).join(`\n`) + `\n` + } + case `Union`: { + return applicableSelections.map(([fieldExpression, ss]) => { + const fieldItem = parseClientFieldItem(fieldExpression) + switch (fieldItem._tag) { + case `FieldName`: { + if (fieldItem.actual === `__typename`) { + return `${toGraphQLFieldName(fieldItem)} ${resolveDirectives(ss)}` + } + // todo + throw new Error(`todo resolve common interface fields from unions`) + } + case `On`: { + const schemaObject = context.schemaIndex[`objects`][fieldItem.typeOrFragmentName] + if (!schemaObject) throw new Error(`Fragment ${fieldItem.typeOrFragmentName} not found in schema`) + // if (isIndicator(ss)) throw new Error(`Union field must have selection set`) + return `${toGraphQLOn(fieldItem)} ${resolveDirectives(ss)} { ${ + // @ts-expect-error fixme + resolveObjectLikeFieldValue(context, schemaObject, pruneNonSelections(ss))} }` + } + default: { + throw new Error(`Unknown field item tag`) + } + } + }).join(`\n`) + `\n` + } + default: + throw new Error(`Unknown schema item kind`) + } +} + +export const resolveOn = (field: string) => { + const on = parseClientOn(field) + if (on) return toGraphQLOn(on) + return field +} diff --git a/src/layers/3_SelectionSet/helpers.ts b/src/layers/3_SelectionSet/helpers.ts new file mode 100644 index 000000000..5d8341da3 --- /dev/null +++ b/src/layers/3_SelectionSet/helpers.ts @@ -0,0 +1,3 @@ +export const isSpecialFieldName = (fieldName: string) => fieldName.startsWith(`$`) + +export const isSelectFieldName = (fieldName: string) => !isSpecialFieldName(fieldName) diff --git a/src/layers/3_SelectionSet/runtime/FieldItem.ts b/src/layers/3_SelectionSet/runtime/FieldItem.ts new file mode 100644 index 000000000..af4bda18e --- /dev/null +++ b/src/layers/3_SelectionSet/runtime/FieldItem.ts @@ -0,0 +1,12 @@ +import type { FieldName } from './FieldName.js' +import { parseClientFieldName } from './FieldName.js' +import type { On } from './on.js' +import { parseClientOn } from './on.js' + +export type FieldItem = On | FieldName + +export const parseClientFieldItem = (field: string): FieldItem => { + const on = parseClientOn(field) + if (on) return on + return parseClientFieldName(field) +} diff --git a/src/layers/3_SelectionSet/runtime/FieldName.ts b/src/layers/3_SelectionSet/runtime/FieldName.ts new file mode 100644 index 000000000..1637063bb --- /dev/null +++ b/src/layers/3_SelectionSet/runtime/FieldName.ts @@ -0,0 +1,36 @@ +export interface FieldName { + _tag: 'FieldName' + actual: string + alias: string | null +} + +// todo use a given schema to ensure that field is actually a fragment and not just happened to be using pattern onX +export const parseClientFieldName = (field: string): FieldName => { + const match = field.match(aliasPattern) + if (match?.groups) { + return { + _tag: `FieldName`, + actual: match.groups[`actual`]!, + alias: match.groups[`alias`]!, + } + } + return { + _tag: `FieldName`, + actual: field, + alias: null, + } +} + +export const toGraphQLFieldName = (fieldName: FieldName) => { + if (fieldName.alias) { + return `${fieldName.actual}: ${fieldName.alias}` + } else { + return fieldName.actual + } +} + +/** + * @see https://regex101.com/r/XfOTMX/1 + * @see http://spec.graphql.org/draft/#sec-Names + */ +export const aliasPattern = /^(?[A-z][A-z_0-9]*)_as_(?[A-z][A-z_0-9]*)$/ diff --git a/src/layers/3_SelectionSet/runtime/directives/defer.ts b/src/layers/3_SelectionSet/runtime/directives/defer.ts new file mode 100644 index 000000000..5a5a4a55d --- /dev/null +++ b/src/layers/3_SelectionSet/runtime/directives/defer.ts @@ -0,0 +1,14 @@ +import type { Directive } from '../../types.js' + +const name = `defer` + +export const parseClientDirectiveDefer = (input: Directive.Defer['$defer']) => { + const args = { + if: typeof input === `boolean` ? input : input.if === undefined ? true : input.if, + label: typeof input === `boolean` ? undefined : input.label, + } + return { + name, + args, + } +} diff --git a/src/layers/3_SelectionSet/runtime/directives/directive.ts b/src/layers/3_SelectionSet/runtime/directives/directive.ts new file mode 100644 index 000000000..3ff29830f --- /dev/null +++ b/src/layers/3_SelectionSet/runtime/directives/directive.ts @@ -0,0 +1,16 @@ +export interface DirectiveLike { + name: string + args: Record +} + +export const toGraphQLDirective = (directive: DirectiveLike) => { + return `@${directive.name}(${toGraphQLDirectiveArgs(directive.args)})` +} + +export const toGraphQLDirectiveArgs = (args: object) => { + return Object.entries(args).filter(([_, v]) => v !== undefined).map(([k, clientValue]) => { + // todo can directives receive custom scalars? + const value = JSON.stringify(clientValue) + return `${k}: ${value}` + }).join(`, `) +} diff --git a/src/layers/3_SelectionSet/runtime/directives/include.ts b/src/layers/3_SelectionSet/runtime/directives/include.ts new file mode 100644 index 000000000..fa7c33d24 --- /dev/null +++ b/src/layers/3_SelectionSet/runtime/directives/include.ts @@ -0,0 +1,13 @@ +import type { Directive } from '../../types.js' + +const name = `include` + +export const parseClientDirectiveInclude = (input: Directive.Include['$include']) => { + const args = { + if: typeof input === `boolean` ? input : input.if === undefined ? true : input.if, + } + return { + name, + args, + } +} diff --git a/src/layers/3_SelectionSet/runtime/directives/skip.ts b/src/layers/3_SelectionSet/runtime/directives/skip.ts new file mode 100644 index 000000000..a10102b61 --- /dev/null +++ b/src/layers/3_SelectionSet/runtime/directives/skip.ts @@ -0,0 +1,13 @@ +import type { Directive } from '../../types.js' + +const name = `skip` + +export const parseClientDirectiveSkip = (input: Directive.Skip['$skip']) => { + const args = { + if: typeof input === `boolean` ? input : input.if === undefined ? true : input.if, + } + return { + name, + args, + } +} diff --git a/src/layers/3_SelectionSet/runtime/directives/stream.ts b/src/layers/3_SelectionSet/runtime/directives/stream.ts new file mode 100644 index 000000000..84c06c448 --- /dev/null +++ b/src/layers/3_SelectionSet/runtime/directives/stream.ts @@ -0,0 +1,15 @@ +import type { Directive } from '../../types.js' + +const name = `stream` + +export const parseClientDirectiveStream = (input: Directive.Stream['$stream']) => { + const args = { + if: typeof input === `boolean` ? input : input.if === undefined ? true : input.if, + label: typeof input === `boolean` ? undefined : input.label, + initialCount: typeof input === `boolean` ? undefined : input.initialCount, + } + return { + name, + args, + } +} diff --git a/src/layers/3_SelectionSet/runtime/indicator.ts b/src/layers/3_SelectionSet/runtime/indicator.ts new file mode 100644 index 000000000..fe1b5a611 --- /dev/null +++ b/src/layers/3_SelectionSet/runtime/indicator.ts @@ -0,0 +1,27 @@ +import type { ClientIndicatorPositive } from '../types.js' + +export type Indicator = 0 | 1 | boolean + +export const isIndicator = (v: any): v is Indicator => { + return String(v) in indicator +} + +export const isPositiveIndicator = (v: any): v is ClientIndicatorPositive => { + return !(String(v) in negativeIndicator) +} + +const negativeIndicator = { + '0': 0, + 'false': false, + 'undefined': undefined, +} + +const positiveIndicator = { + '1': 1, + 'true': true, +} + +const indicator = { + ...negativeIndicator, + ...positiveIndicator, +} diff --git a/src/layers/3_SelectionSet/runtime/on.ts b/src/layers/3_SelectionSet/runtime/on.ts new file mode 100644 index 000000000..6d1b58e39 --- /dev/null +++ b/src/layers/3_SelectionSet/runtime/on.ts @@ -0,0 +1,22 @@ +export const onPattern = /^on(?[A-Z][A-z_0-9]*)$/ + +export interface On { + _tag: 'On' + typeOrFragmentName: string +} + +// todo use a given schema to ensure that field is actually a fragment and not just happened to be using pattern onX +export const parseClientOn = (field: string): null | On => { + const match = field.match(onPattern) + if (match?.groups) { + return { + _tag: `On`, + typeOrFragmentName: match.groups[`name`]!, + } + } + return null +} + +export const toGraphQLOn = (on: On) => { + return `...on ${on.typeOrFragmentName}` +} diff --git a/src/layers/3_IO/SelectionSet/SelectionSet.test-d.ts b/src/layers/3_SelectionSet/types.test-d.ts similarity index 99% rename from src/layers/3_IO/SelectionSet/SelectionSet.test-d.ts rename to src/layers/3_SelectionSet/types.test-d.ts index 2b46c88bf..dd670977d 100644 --- a/src/layers/3_IO/SelectionSet/SelectionSet.test-d.ts +++ b/src/layers/3_SelectionSet/types.test-d.ts @@ -1,5 +1,5 @@ import { assertType, expectTypeOf, test } from 'vitest' -import type { Index } from '../../../../tests/_/schema/generated/Index.js' +import type { Index } from '../../../tests/_/schema/generated/Index.js' import type { SelectionSet } from './__.js' type Q = SelectionSet.Query diff --git a/src/layers/3_IO/SelectionSet/SelectionSet.ts b/src/layers/3_SelectionSet/types.ts similarity index 92% rename from src/layers/3_IO/SelectionSet/SelectionSet.ts rename to src/layers/3_SelectionSet/types.ts index 37fb7f481..8546ae94a 100644 --- a/src/layers/3_IO/SelectionSet/SelectionSet.ts +++ b/src/layers/3_SelectionSet/types.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/ban-types */ -import type { MaybeList, StringNonEmpty, Values } from '../../../lib/prelude.js' -import type { TSError } from '../../../lib/TSError.js' +import type { MaybeList, StringNonEmpty, Values } from '../../lib/prelude.js' +import type { TSError } from '../../lib/TSError.js' import type { InputFieldsAllNullable, OmitNullableFields, @@ -9,7 +9,7 @@ import type { Schema, SomeField, SomeFields, -} from '../../1_Schema/__.js' +} from '../1_Schema/__.js' export type Query<$Index extends Schema.Index> = Root<$Index, 'Query'> @@ -215,13 +215,6 @@ export type ClientIndicator = ClientIndicatorPositive | ClientIndicatorNegative export type ClientIndicatorPositive = true | 1 export type ClientIndicatorNegative = false | 0 | undefined -/** - * @see https://regex101.com/r/XfOTMX/1 - * @see http://spec.graphql.org/draft/#sec-Names - */ -export const aliasPattern = /^(?[A-z][A-z_0-9]*)_as_(?[A-z][A-z_0-9]*)$/ -export const fragmentPattern = /^on(?[A-Z][A-z_0-9]*)$/ - export type OmitNegativeIndicators<$SelectionSet> = { [K in keyof $SelectionSet as $SelectionSet[K] extends ClientIndicatorNegative ? never : K]: $SelectionSet[K] } @@ -235,10 +228,10 @@ export type NoArgsIndicator = ClientIndicator | FieldDirectives // dprint-ignore export type Indicator<$Field extends SomeField> = -$Field['args'] extends Schema.Args ? InputFieldsAllNullable<$Field['args']['fields']> extends true - ? ({ $?: Args<$Field['args']> } & FieldDirectives) | ClientIndicator : - { $: Args<$Field['args']> } & FieldDirectives : - NoArgsIndicator + $Field['args'] extends Schema.Args ? InputFieldsAllNullable<$Field['args']['fields']> extends true + ? ({ $?: Args<$Field['args']> } & FieldDirectives) | ClientIndicator : + { $: Args<$Field['args']> } & FieldDirectives : + NoArgsIndicator // dprint-ignore export type Args<$Args extends Schema.Args> = ArgFields<$Args['fields']> diff --git a/src/layers/4_ResultSet/__.ts b/src/layers/4_ResultSet/__.ts new file mode 100644 index 000000000..6ad17db77 --- /dev/null +++ b/src/layers/4_ResultSet/__.ts @@ -0,0 +1 @@ +export * as ResultSet from './types.js' diff --git a/src/layers/4_ResultSet/runtime.ts b/src/layers/4_ResultSet/runtime.ts new file mode 100644 index 000000000..045ff507a --- /dev/null +++ b/src/layers/4_ResultSet/runtime.ts @@ -0,0 +1,13 @@ +import { assertObject } from '../../lib/prelude.js' + +// eslint-disable-next-line +export function assertGraphQLObject(v: unknown): asserts v is GraphQLObject { + assertObject(v) + if (`__typename` in v && typeof v.__typename !== `string`) { + throw new Error(`Expected string __typename or undefined. Got: ${String(v.__typename)}`) + } +} + +export type GraphQLObject = { + __typename?: string +} diff --git a/src/layers/3_IO/ResultSet/ResultSet.test-d.ts b/src/layers/4_ResultSet/types.test-d.ts similarity index 97% rename from src/layers/3_IO/ResultSet/ResultSet.test-d.ts rename to src/layers/4_ResultSet/types.test-d.ts index ad270537f..24aa76a34 100644 --- a/src/layers/3_IO/ResultSet/ResultSet.test-d.ts +++ b/src/layers/4_ResultSet/types.test-d.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/ban-types */ import { expectTypeOf, test } from 'vitest' -import type { Index } from '../../../../tests/_/schema/generated/Index.js' -import type * as Schema from '../../../../tests/_/schema/generated/SchemaBuildtime.js' -import type { SelectionSet } from '../SelectionSet/__.js' +import type { Index } from '../../../tests/_/schema/generated/Index.js' +import type * as Schema from '../../../tests/_/schema/generated/SchemaBuildtime.js' +import type { SelectionSet } from '../3_SelectionSet/__.js' import type { ResultSet } from './__.js' type I = Index diff --git a/src/layers/3_IO/ResultSet/ResultSet.ts b/src/layers/4_ResultSet/types.ts similarity index 94% rename from src/layers/3_IO/ResultSet/ResultSet.ts rename to src/layers/4_ResultSet/types.ts index 9d2e59537..96a29e7f2 100644 --- a/src/layers/3_IO/ResultSet/ResultSet.ts +++ b/src/layers/4_ResultSet/types.ts @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/ban-types */ import type { Simplify } from 'type-fest' -import type { ExcludeNull, GetKeyOr, SimplifyDeep } from '../../../lib/prelude.js' -import type { TSError } from '../../../lib/TSError.js' -import type { Schema, SomeField } from '../../1_Schema/__.js' -import type { PickScalarFields } from '../../1_Schema/Output/Output.js' -import type { SelectionSet } from '../SelectionSet/__.js' +import type { ExcludeNull, GetKeyOr, SimplifyDeep } from '../../lib/prelude.js' +import type { TSError } from '../../lib/TSError.js' +import type { Schema, SomeField } from '../1_Schema/__.js' +import type { PickScalarFields } from '../1_Schema/Output/Output.js' +import type { SelectionSet } from '../3_SelectionSet/__.js' export type Query<$SelectionSet extends object, $Index extends Schema.Index> = Root<$SelectionSet, $Index, 'Query'> diff --git a/src/layers/4_client/Config.ts b/src/layers/5_client/Config.ts similarity index 100% rename from src/layers/4_client/Config.ts rename to src/layers/5_client/Config.ts diff --git a/src/layers/4_client/RootTypeMethods.ts b/src/layers/5_client/RootTypeMethods.ts similarity index 97% rename from src/layers/4_client/RootTypeMethods.ts rename to src/layers/5_client/RootTypeMethods.ts index 694d207d1..e471e25e5 100644 --- a/src/layers/4_client/RootTypeMethods.ts +++ b/src/layers/5_client/RootTypeMethods.ts @@ -2,8 +2,8 @@ import type { OperationName } from '../../lib/graphql.js' import type { Exact } from '../../lib/prelude.js' import type { TSError } from '../../lib/TSError.js' import type { InputFieldsAllNullable, Schema } from '../1_Schema/__.js' -import type { ResultSet } from '../3_IO/ResultSet/__.js' -import type { SelectionSet } from '../3_IO/SelectionSet/__.js' +import type { SelectionSet } from '../3_SelectionSet/__.js' +import type { ResultSet } from '../4_ResultSet/__.js' import type { AugmentRootTypeSelectionWithTypename, Config, diff --git a/src/layers/4_client/client.customScalar.test.ts b/src/layers/5_client/client.customScalar.test.ts similarity index 100% rename from src/layers/4_client/client.customScalar.test.ts rename to src/layers/5_client/client.customScalar.test.ts diff --git a/src/layers/4_client/client.document.test-d.ts b/src/layers/5_client/client.document.test-d.ts similarity index 100% rename from src/layers/4_client/client.document.test-d.ts rename to src/layers/5_client/client.document.test-d.ts diff --git a/src/layers/4_client/client.document.test.ts b/src/layers/5_client/client.document.test.ts similarity index 100% rename from src/layers/4_client/client.document.test.ts rename to src/layers/5_client/client.document.test.ts diff --git a/src/layers/4_client/client.error.test-d.ts b/src/layers/5_client/client.error.test-d.ts similarity index 100% rename from src/layers/4_client/client.error.test-d.ts rename to src/layers/5_client/client.error.test-d.ts diff --git a/src/layers/4_client/client.input.test-d.ts b/src/layers/5_client/client.input.test-d.ts similarity index 100% rename from src/layers/4_client/client.input.test-d.ts rename to src/layers/5_client/client.input.test-d.ts diff --git a/src/layers/4_client/client.returnMode.test-d.ts b/src/layers/5_client/client.returnMode.test-d.ts similarity index 100% rename from src/layers/4_client/client.returnMode.test-d.ts rename to src/layers/5_client/client.returnMode.test-d.ts diff --git a/src/layers/4_client/client.returnMode.test.ts b/src/layers/5_client/client.returnMode.test.ts similarity index 100% rename from src/layers/4_client/client.returnMode.test.ts rename to src/layers/5_client/client.returnMode.test.ts diff --git a/src/layers/4_client/client.rootTypeMethods.test-d.ts b/src/layers/5_client/client.rootTypeMethods.test-d.ts similarity index 100% rename from src/layers/4_client/client.rootTypeMethods.test-d.ts rename to src/layers/5_client/client.rootTypeMethods.test-d.ts diff --git a/src/layers/4_client/client.rootTypeMethods.test.ts b/src/layers/5_client/client.rootTypeMethods.test.ts similarity index 100% rename from src/layers/4_client/client.rootTypeMethods.test.ts rename to src/layers/5_client/client.rootTypeMethods.test.ts diff --git a/src/layers/4_client/client.ts b/src/layers/5_client/client.ts similarity index 98% rename from src/layers/4_client/client.ts rename to src/layers/5_client/client.ts index 893a68510..3e5fa8eae 100644 --- a/src/layers/4_client/client.ts +++ b/src/layers/5_client/client.ts @@ -3,14 +3,14 @@ import { type DocumentNode, execute, graphql, type GraphQLSchema } from 'graphql import type { ExcludeUndefined } from 'type-fest/source/required-deep.js' import request from '../../entrypoints/main.js' import { Errors } from '../../lib/errors/__.js' -import type { RootTypeName, Variables } from '../../lib/graphql.js' +import { type RootTypeName, rootTypeNameToOperationName, type Variables } from '../../lib/graphql.js' import { isPlainObject } from '../../lib/prelude.js' import type { Object$2 } from '../1_Schema/__.js' import { Schema } from '../1_Schema/__.js' import { readMaybeThunk } from '../1_Schema/core/helpers.js' import type { GlobalRegistry } from '../2_generator/globalRegistry.js' -import { SelectionSet } from '../3_IO/SelectionSet/__.js' -import type { Context, DocumentObject, GraphQLObjectSelection } from '../3_IO/SelectionSet/toGraphQLDocumentString.js' +import { SelectionSet } from '../3_SelectionSet/__.js' +import type { Context, DocumentObject, GraphQLObjectSelection } from '../3_SelectionSet/encode.js' import type { ApplyInputDefaults, Config, @@ -20,7 +20,7 @@ import type { } from './Config.js' import * as CustomScalars from './customScalars.js' import type { DocumentFn } from './document.js' -import { rootTypeNameToOperationName, toDocumentString } from './document.js' +import { toDocumentString } from './document.js' import type { GetRootTypeMethods } from './RootTypeMethods.js' // dprint-ignore diff --git a/src/layers/4_client/customScalars.ts b/src/layers/5_client/customScalars.ts similarity index 85% rename from src/layers/4_client/customScalars.ts rename to src/layers/5_client/customScalars.ts index af306ed14..e90b7e4ee 100644 --- a/src/layers/4_client/customScalars.ts +++ b/src/layers/5_client/customScalars.ts @@ -1,12 +1,14 @@ import type { ExecutionResult } from 'graphql' import { standardScalarTypeNames } from '../../lib/graphql.js' -import { mapValues } from '../../lib/prelude.js' +import { assertArray, mapValues } from '../../lib/prelude.js' import type { Object$2, Schema } from '../1_Schema/__.js' import { Output } from '../1_Schema/__.js' import { readMaybeThunk } from '../1_Schema/core/helpers.js' -import type { SelectionSet } from '../3_IO/SelectionSet/__.js' -import type { Args } from '../3_IO/SelectionSet/SelectionSet.js' -import type { GraphQLObjectSelection } from '../3_IO/SelectionSet/toGraphQLDocumentString.js' +import type { SelectionSet } from '../3_SelectionSet/__.js' +import type { GraphQLObjectSelection } from '../3_SelectionSet/encode.js' +import type { Args } from '../3_SelectionSet/types.js' +import type { GraphQLObject } from '../4_ResultSet/runtime.js' +import { assertGraphQLObject } from '../4_ResultSet/runtime.js' namespace SSValue { export type Obj = { @@ -149,25 +151,3 @@ const decodeCustomScalarValue = ( return fieldValue } - -// eslint-disable-next-line -function assertArray(v: unknown): asserts v is unknown[] { - if (!Array.isArray(v)) throw new Error(`Expected array. Got: ${String(v)}`) -} - -// eslint-disable-next-line -function assertObject(v: unknown): asserts v is object { - if (v === null || typeof v !== `object`) throw new Error(`Expected object. Got: ${String(v)}`) -} - -// eslint-disable-next-line -function assertGraphQLObject(v: unknown): asserts v is GraphQLObject { - assertObject(v) - if (`__typename` in v && typeof v.__typename !== `string`) { - throw new Error(`Expected string __typename or undefined. Got: ${String(v.__typename)}`) - } -} - -type GraphQLObject = { - __typename?: string -} diff --git a/src/layers/4_client/document.ts b/src/layers/5_client/document.ts similarity index 90% rename from src/layers/4_client/document.ts rename to src/layers/5_client/document.ts index cd627f5ce..fc55b3806 100644 --- a/src/layers/4_client/document.ts +++ b/src/layers/5_client/document.ts @@ -1,10 +1,11 @@ import type { MergeExclusive, NonEmptyObject } from 'type-fest' +import { operationTypeToRootType } from '../../lib/graphql.js' import type { IsMultipleKeys } from '../../lib/prelude.js' import type { TSError } from '../../lib/TSError.js' import type { Schema } from '../1_Schema/__.js' -import type { ResultSet } from '../3_IO/ResultSet/__.js' -import { SelectionSet } from '../3_IO/SelectionSet/__.js' -import type { Context, DocumentObject } from '../3_IO/SelectionSet/toGraphQLDocumentString.js' +import { SelectionSet } from '../3_SelectionSet/__.js' +import type { Context, DocumentObject } from '../3_SelectionSet/encode.js' +import type { ResultSet } from '../4_ResultSet/__.js' import type { AugmentRootTypeSelectionWithTypename, Config, OrThrowifyConfig, ReturnModeRootType } from './Config.js' // dprint-ignore @@ -91,15 +92,3 @@ type GetRootType<$Selection extends object> = $Selection extends {query:any} ? 'Query' : $Selection extends {mutation:any} ? 'Mutation' : never - -export const operationTypeToRootType = { - query: `Query`, - mutation: `Mutation`, - subscription: `Subscription`, -} as const - -export const rootTypeNameToOperationName = { - Query: `query`, - Mutation: `mutation`, - Subscription: `subscription`, -} as const diff --git a/src/layers/4_select/select.test-d.ts b/src/layers/5_select/select.test-d.ts similarity index 94% rename from src/layers/4_select/select.test-d.ts rename to src/layers/5_select/select.test-d.ts index 348ba65fe..d15d2a5c0 100644 --- a/src/layers/4_select/select.test-d.ts +++ b/src/layers/5_select/select.test-d.ts @@ -1,7 +1,7 @@ import { describe, expect, expectTypeOf, it, test } from 'vitest' import type { Index } from '../../../tests/_/schema/schema.js' import type * as SchemaQueryOnly from '../../../tests/_/schemaQueryOnly/generated/Index.js' -import type { SelectionSet } from '../3_IO/SelectionSet/__.js' +import type { SelectionSet } from '../3_SelectionSet/__.js' import { create } from './select.js' describe(`select`, () => { diff --git a/src/layers/4_select/select.test.ts b/src/layers/5_select/select.test.ts similarity index 100% rename from src/layers/4_select/select.test.ts rename to src/layers/5_select/select.test.ts diff --git a/src/layers/4_select/select.ts b/src/layers/5_select/select.ts similarity index 95% rename from src/layers/4_select/select.ts rename to src/layers/5_select/select.ts index eb5108649..90629db1f 100644 --- a/src/layers/4_select/select.ts +++ b/src/layers/5_select/select.ts @@ -1,7 +1,7 @@ import type { RootTypeName } from '../../lib/graphql.js' import type { Exact } from '../../lib/prelude.js' import type { Schema } from '../1_Schema/__.js' -import type { SelectionSet } from '../3_IO/SelectionSet/__.js' +import type { SelectionSet } from '../3_SelectionSet/__.js' // dprint-ignore type TypeSelectionSets<$Index extends Schema.Index> = diff --git a/src/lib/graphql.ts b/src/lib/graphql.ts index d3fd2dd33..166b46120 100644 --- a/src/lib/graphql.ts +++ b/src/lib/graphql.ts @@ -35,6 +35,18 @@ export const RootTypeName = { Subscription: `Subscription`, } as const +export const operationTypeToRootType = { + query: `Query`, + mutation: `Mutation`, + subscription: `Subscription`, +} as const + +export const rootTypeNameToOperationName = { + Query: `query`, + Mutation: `mutation`, + Subscription: `subscription`, +} as const + export type RootTypeName = keyof typeof RootTypeName export const isStandardScalarType = (type: GraphQLScalarType) => { diff --git a/src/lib/prelude.ts b/src/lib/prelude.ts index 1ae0337cf..8a33c4619 100644 --- a/src/lib/prelude.ts +++ b/src/lib/prelude.ts @@ -237,3 +237,11 @@ export type SetProperty<$Obj extends object, $Prop extends keyof $Obj, $Type ext export const lowerCaseFirstLetter = (s: string) => { return s.charAt(0).toLowerCase() + s.slice(1) } + +export function assertArray(v: unknown): asserts v is unknown[] { + if (!Array.isArray(v)) throw new Error(`Expected array. Got: ${String(v)}`) +} + +export function assertObject(v: unknown): asserts v is object { + if (v === null || typeof v !== `object`) throw new Error(`Expected object. Got: ${String(v)}`) +}