Skip to content

Commit

Permalink
Merge pull request #1030 from interval/multiple-search
Browse files Browse the repository at this point in the history
Add `.multiple()` to `io.search`
  • Loading branch information
jacobmischka authored Jan 11, 2023
2 parents d8571f4 + 7ae6940 commit 12b3c35
Show file tree
Hide file tree
Showing 7 changed files with 850 additions and 129 deletions.
102 changes: 93 additions & 9 deletions src/classes/IOClient.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { v4 } from 'uuid'
import { z } from 'zod'
import * as superjson from 'superjson'
import type {
import {
T_IO_RENDER_INPUT,
T_IO_RESPONSE,
T_IO_PROPS,
T_IO_RETURNS,
T_IO_METHOD_NAMES,
T_IO_DISPLAY_METHOD_NAMES,
T_IO_INPUT_METHOD_NAMES,
T_IO_MULTIPLEABLE_METHOD_NAMES,
supportsMultiple,
} from '../ioSchema'
import Logger from './Logger'
import { AnyIOComponent } from './IOComponent'
Expand All @@ -18,6 +20,7 @@ import {
IOPromiseValidator,
DisplayIOPromise,
InputIOPromise,
MultipleableIOPromise,
} from './IOPromise'
import IOError from './IOError'
import spreadsheet from '../components/spreadsheet'
Expand Down Expand Up @@ -46,6 +49,8 @@ import {
RequiredPropsInputIOComponentFunction,
GroupConfig,
ButtonConfig,
RequiredPropsMultipleableInputIOComponentFunction,
MultipleableInputIOComponentFunction,
} from '../types'
import { stripUndefined } from '../utils/deserialize'
import { IntervalError } from '..'
Expand Down Expand Up @@ -313,14 +318,24 @@ export class IOClient {
getPromiseProps<
MethodName extends T_IO_METHOD_NAMES,
Props extends object = T_IO_PROPS<MethodName>,
Output = T_IO_RETURNS<MethodName>
Output = T_IO_RETURNS<MethodName>,
DefaultValue = Output
>(
methodName: MethodName,
inputProps?: Props,
componentDef?: IOComponentDefinition<MethodName, Props, Output>
componentDef?: IOComponentDefinition<
MethodName,
Props,
Output,
DefaultValue
>
) {
let props = inputProps ? (inputProps as T_IO_PROPS<MethodName>) : {}
let props: T_IO_PROPS<MethodName> = inputProps
? (inputProps as T_IO_PROPS<MethodName>)
: {}
let getValue = (r: T_IO_RETURNS<MethodName>) => r as unknown as Output
let getDefaultValue = (defaultValue: DefaultValue) =>
defaultValue as unknown as Output
let onStateChange: ReturnType<
IOComponentDefinition<MethodName, Props, Output>
>['onStateChange'] = undefined
Expand All @@ -338,6 +353,10 @@ export class IOClient {
getValue = componentGetters.getValue
}

if (componentGetters.getDefaultValue) {
getDefaultValue = componentGetters.getDefaultValue
}

if (componentGetters.onStateChange) {
onStateChange = componentGetters.onStateChange
}
Expand All @@ -347,10 +366,37 @@ export class IOClient {
methodName,
props,
valueGetter: getValue,
defaultValueGetter: getDefaultValue,
onStateChange,
}
}

createIOMethod<
MethodName extends T_IO_MULTIPLEABLE_METHOD_NAMES,
Props extends object = T_IO_PROPS<MethodName>,
Output = T_IO_RETURNS<MethodName>
>(
methodName: MethodName,
config?: {
propsRequired?: false
componentDef?: IOComponentDefinition<MethodName, Props, Output>
}
): MultipleableInputIOComponentFunction<MethodName, Props, Output>
createIOMethod<
MethodName extends T_IO_MULTIPLEABLE_METHOD_NAMES,
Props extends object = T_IO_PROPS<MethodName>,
Output = T_IO_RETURNS<MethodName>
>(
methodName: MethodName,
config: {
propsRequired?: true
componentDef?: IOComponentDefinition<MethodName, Props, Output>
}
): RequiredPropsMultipleableInputIOComponentFunction<
MethodName,
Props,
Output
>
createIOMethod<
MethodName extends T_IO_DISPLAY_METHOD_NAMES,
Props extends object = T_IO_PROPS<MethodName>,
Expand Down Expand Up @@ -409,20 +455,58 @@ export class IOClient {
} = {}
) {
return (label: string, props?: Props) => {
const isDisplay = methodName.startsWith('DISPLAY_')
const promiseProps = this.getPromiseProps(methodName, props, componentDef)
if (supportsMultiple(methodName)) {
return new MultipleableIOPromise({
...this.getPromiseProps(
methodName as T_IO_MULTIPLEABLE_METHOD_NAMES,
props,
componentDef as
| IOComponentDefinition<
T_IO_MULTIPLEABLE_METHOD_NAMES,
Props,
T_IO_RETURNS<T_IO_MULTIPLEABLE_METHOD_NAMES>
>
| undefined
),
methodName: methodName as T_IO_MULTIPLEABLE_METHOD_NAMES,
renderer: this.renderComponents.bind(
this
) as ComponentRenderer<T_IO_MULTIPLEABLE_METHOD_NAMES>,
label,
})
}

return isDisplay
return methodName.startsWith('DISPLAY_')
? new DisplayIOPromise({
...promiseProps,
...this.getPromiseProps(
methodName as T_IO_DISPLAY_METHOD_NAMES,
props,
componentDef as
| IOComponentDefinition<
T_IO_DISPLAY_METHOD_NAMES,
Props,
T_IO_RETURNS<T_IO_DISPLAY_METHOD_NAMES>
>
| undefined
),
methodName: methodName as T_IO_DISPLAY_METHOD_NAMES,
renderer: this.renderComponents.bind(
this
) as ComponentRenderer<T_IO_DISPLAY_METHOD_NAMES>,
label,
})
: new InputIOPromise({
...promiseProps,
...this.getPromiseProps(
methodName as T_IO_INPUT_METHOD_NAMES,
props,
componentDef as
| IOComponentDefinition<
T_IO_INPUT_METHOD_NAMES,
Props,
T_IO_RETURNS<T_IO_INPUT_METHOD_NAMES>
>
| undefined
),
methodName: methodName as T_IO_INPUT_METHOD_NAMES,
renderer: this.renderComponents.bind(
this
Expand Down
105 changes: 77 additions & 28 deletions src/classes/IOComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import {
resolvesImmediately,
T_IO_DISPLAY_METHOD_NAMES,
T_IO_METHOD_NAMES,
T_IO_PROPS,
T_IO_RETURNS,
T_IO_STATE,
} from '../ioSchema'
import { deserializeDates } from '../utils/deserialize'
import IOError from './IOError'
Expand All @@ -14,11 +16,15 @@ type IoSchema = typeof ioSchema
export interface ComponentInstance<MN extends keyof IoSchema> {
methodName: MN
label: string
props?: z.input<IoSchema[MN]['props']>
state: z.infer<IoSchema[MN]['state']>
props?: T_IO_PROPS<MN>
state: T_IO_STATE<MN>
isStateful?: boolean
isOptional?: boolean
isMultiple?: boolean
validationErrorMessage?: string | undefined
multipleProps?: {
defaultValue?: T_IO_RETURNS<MN>[]
}
}

export type ComponentRenderInfo<MN extends keyof IoSchema> = Omit<
Expand All @@ -28,6 +34,10 @@ export type ComponentRenderInfo<MN extends keyof IoSchema> = Omit<

export type ComponentReturnValue<MN extends keyof IoSchema> = T_IO_RETURNS<MN>

export type MaybeMultipleComponentReturnValue<MN extends keyof IoSchema> =
| T_IO_RETURNS<MN>
| T_IO_RETURNS<MN>[]

export type IOComponentMap = {
[MethodName in T_IO_METHOD_NAMES]: IOComponent<MethodName>
}
Expand All @@ -49,9 +59,11 @@ export default class IOComponent<MethodName extends T_IO_METHOD_NAMES> {
schema: IoSchema[MethodName]
instance: ComponentInstance<MethodName>
resolver:
| ((v: ComponentReturnValue<MethodName> | undefined) => void)
| ((v: MaybeMultipleComponentReturnValue<MethodName> | undefined) => void)
| undefined
returnValue: Promise<ComponentReturnValue<MethodName> | undefined>
returnValue: Promise<
MaybeMultipleComponentReturnValue<MethodName> | undefined
>
onStateChangeHandler: (() => void) | undefined
handleStateChange:
| ((
Expand All @@ -60,38 +72,57 @@ export default class IOComponent<MethodName extends T_IO_METHOD_NAMES> {
| undefined

validator:
| IOPromiseValidator<ComponentReturnValue<MethodName> | undefined>
| IOPromiseValidator<
MaybeMultipleComponentReturnValue<MethodName> | undefined
>
| undefined

/**
* @param methodName - The component's method name from ioSchema, used
* @param options.methodName - The component's method name from ioSchema, used
* to determine the valid types for communication with Interval.
* @param label - The UI label to be displayed to the action runner.
* @param initialProps - The properties send to Interval for the initial
* @param options.label - The UI label to be displayed to the action runner.
* @param options.initialProps - The properties send to Interval for the initial
* render call.
* @param handleStateChange - A handler that converts new state received
* @param options.handleStateChange - A handler that converts new state received
* from Interval into a new set of props.
* @param isOptional - If true, the input can be omitted by the action
* @param options.isOptional - If true, the input can be omitted by the action
* runner, in which case the component will accept and return `undefined`.
*/
constructor(
methodName: MethodName,
label: string,
initialProps?: z.input<IoSchema[MethodName]['props']>,
handleStateChange?: (
incomingState: z.infer<IoSchema[MethodName]['state']>
) => Promise<Partial<z.input<IoSchema[MethodName]['props']>>>,
isOptional: boolean = false,
validator?: IOPromiseValidator<ComponentReturnValue<MethodName> | undefined>
) {
this.handleStateChange = handleStateChange
constructor({
methodName,
label,
initialProps,
onStateChange,
isOptional = false,
isMultiple = false,
validator,
multipleProps,
}: {
methodName: MethodName
label: string
initialProps?: T_IO_PROPS<MethodName>
onStateChange?: (
incomingState: T_IO_STATE<MethodName>
) => Promise<Partial<T_IO_PROPS<MethodName>>>
isOptional?: boolean
isMultiple?: boolean
validator?: IOPromiseValidator<
MaybeMultipleComponentReturnValue<MethodName> | undefined
>
multipleProps?: {
defaultValue?: T_IO_RETURNS<MethodName>[]
}
}) {
this.handleStateChange = onStateChange
this.schema = ioSchema[methodName]
this.validator = validator

try {
initialProps = this.schema.props.parse(initialProps ?? {})
} catch (err) {
console.error(`Invalid props found for IO call with label "${label}":`)
console.error(
`[Interval] Invalid props found for IO call with label "${label}":`
)
console.error(err)
throw err
}
Expand All @@ -101,12 +132,14 @@ export default class IOComponent<MethodName extends T_IO_METHOD_NAMES> {
label,
props: initialProps,
state: null,
isStateful: !!handleStateChange,
isStateful: !!onStateChange,
isOptional: isOptional,
isMultiple: isMultiple,
multipleProps,
}

this.returnValue = new Promise<
ComponentReturnValue<MethodName> | undefined
MaybeMultipleComponentReturnValue<MethodName> | undefined
>(resolve => {
this.resolver = resolve
})
Expand All @@ -120,7 +153,7 @@ export default class IOComponent<MethodName extends T_IO_METHOD_NAMES> {
}

async handleValidation(
returnValue: ComponentReturnValue<MethodName> | undefined
returnValue: MaybeMultipleComponentReturnValue<MethodName> | undefined
): Promise<string | undefined> {
if (this.validator) {
const message = await this.validator(returnValue)
Expand All @@ -130,21 +163,35 @@ export default class IOComponent<MethodName extends T_IO_METHOD_NAMES> {
}

setReturnValue(value: z.input<IoSchema[MethodName]['returns']>) {
let requiredReturnSchema:
| IoSchema[MethodName]['returns']
| z.ZodArray<IoSchema[MethodName]['returns']> = this.schema.returns

if (this.instance.isMultiple) {
requiredReturnSchema = z.array(requiredReturnSchema)
}

const returnSchema = this.instance.isOptional
? this.schema.returns
? requiredReturnSchema
.nullable()
.optional()
// JSON.stringify turns undefined into null in arrays
.transform(value => value ?? undefined)
: this.schema.returns
: requiredReturnSchema

try {
let parsed: ReturnType<typeof returnSchema.parse>

if (value && typeof value === 'object') {
// TODO: Remove this when all active SDKs support superjson
if (Array.isArray(value)) {
parsed = returnSchema.parse(value.map(v => deserializeDates<any>(v)))
parsed = returnSchema.parse(
value.map(v =>
typeof v === 'object' && !Array.isArray(v)
? deserializeDates<any>(v)
: v
)
)
} else {
parsed = returnSchema.parse(deserializeDates<any>(value))
}
Expand Down Expand Up @@ -225,7 +272,9 @@ export default class IOComponent<MethodName extends T_IO_METHOD_NAMES> {
props: this.instance.props,
isStateful: this.instance.isStateful,
isOptional: this.instance.isOptional,
isMultiple: this.instance.isMultiple,
validationErrorMessage: this.instance.validationErrorMessage,
multipleProps: this.instance.multipleProps,
}
}
}
Loading

0 comments on commit 12b3c35

Please sign in to comment.