mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
sdk input spec improvements (#2785)
* sdk input spec improvements * more sdk changes * fe changes * alpha.14 * fix tests * separate validator in filehelper * use deeppartial for getinput * fix union type and update ts-matches * alpha.15 * alpha.16 * alpha.17 --------- Co-authored-by: Matt Hill <mattnine@protonmail.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import * as T from "../types"
|
||||
import * as IST from "../actions/input/inputSpecTypes"
|
||||
import { Action } from "./setupActions"
|
||||
import { ExtractInputSpecType } from "./input/builder/inputSpec"
|
||||
|
||||
export type RunActionInput<Input> =
|
||||
| Input
|
||||
@@ -44,36 +45,32 @@ export const runAction = async <
|
||||
})
|
||||
}
|
||||
}
|
||||
type GetActionInputType<
|
||||
A extends Action<T.ActionId, any, any, Record<string, unknown>>,
|
||||
> = A extends Action<T.ActionId, any, any, infer I> ? I : never
|
||||
type GetActionInputType<A extends Action<T.ActionId, any, any>> =
|
||||
A extends Action<T.ActionId, any, infer I> ? ExtractInputSpecType<I> : never
|
||||
|
||||
type ActionRequestBase = {
|
||||
reason?: string
|
||||
replayId?: string
|
||||
}
|
||||
type ActionRequestInput<
|
||||
T extends Action<T.ActionId, any, any, Record<string, unknown>>,
|
||||
> = {
|
||||
type ActionRequestInput<T extends Action<T.ActionId, any, any>> = {
|
||||
kind: "partial"
|
||||
value: Partial<GetActionInputType<T>>
|
||||
}
|
||||
export type ActionRequestOptions<
|
||||
T extends Action<T.ActionId, any, any, Record<string, unknown>>,
|
||||
> = ActionRequestBase &
|
||||
(
|
||||
| {
|
||||
when?: Exclude<
|
||||
T.ActionRequestTrigger,
|
||||
{ condition: "input-not-matches" }
|
||||
>
|
||||
input?: ActionRequestInput<T>
|
||||
}
|
||||
| {
|
||||
when: T.ActionRequestTrigger & { condition: "input-not-matches" }
|
||||
input: ActionRequestInput<T>
|
||||
}
|
||||
)
|
||||
export type ActionRequestOptions<T extends Action<T.ActionId, any, any>> =
|
||||
ActionRequestBase &
|
||||
(
|
||||
| {
|
||||
when?: Exclude<
|
||||
T.ActionRequestTrigger,
|
||||
{ condition: "input-not-matches" }
|
||||
>
|
||||
input?: ActionRequestInput<T>
|
||||
}
|
||||
| {
|
||||
when: T.ActionRequestTrigger & { condition: "input-not-matches" }
|
||||
input: ActionRequestInput<T>
|
||||
}
|
||||
)
|
||||
|
||||
const _validate: T.ActionRequest = {} as ActionRequestOptions<any> & {
|
||||
actionId: string
|
||||
@@ -81,9 +78,7 @@ const _validate: T.ActionRequest = {} as ActionRequestOptions<any> & {
|
||||
severity: T.ActionSeverity
|
||||
}
|
||||
|
||||
export const requestAction = <
|
||||
T extends Action<T.ActionId, any, any, Record<string, unknown>>,
|
||||
>(options: {
|
||||
export const requestAction = <T extends Action<T.ActionId, any, any>>(options: {
|
||||
effects: T.Effects
|
||||
packageId: T.PackageId
|
||||
action: T
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ValueSpec } from "../inputSpecTypes"
|
||||
import { Value } from "./value"
|
||||
import { PartialValue, Value } from "./value"
|
||||
import { _ } from "../../../util"
|
||||
import { Effects } from "../../../Effects"
|
||||
import { Parser, object } from "ts-matches"
|
||||
@@ -16,6 +16,15 @@ export type ExtractInputSpecType<A extends Record<string, any> | InputSpec<Recor
|
||||
A extends InputSpec<infer B, any> | InputSpec<infer B, never> ? B :
|
||||
A
|
||||
|
||||
export type ExtractPartialInputSpecType<
|
||||
A extends
|
||||
| Record<string, any>
|
||||
| InputSpec<Record<string, any>, any>
|
||||
| InputSpec<Record<string, any>, never>,
|
||||
> = A extends InputSpec<infer B, any> | InputSpec<infer B, never>
|
||||
? PartialValue<B>
|
||||
: PartialValue<A>
|
||||
|
||||
export type InputSpecOf<A extends Record<string, any>, Store = never> = {
|
||||
[K in keyof A]: Value<A[K], Store>
|
||||
}
|
||||
@@ -84,6 +93,8 @@ export class InputSpec<Type extends Record<string, any>, Store = never> {
|
||||
},
|
||||
public validator: Parser<unknown, Type>,
|
||||
) {}
|
||||
_TYPE: Type = null as any as Type
|
||||
_PARTIAL: PartialValue<Type> = null as any as PartialValue<Type>
|
||||
async build(options: LazyBuildOptions<Store>) {
|
||||
const answer = {} as {
|
||||
[K in keyof Type]: ValueSpec
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { InputSpec, LazyBuild } from "./inputSpec"
|
||||
import { List } from "./list"
|
||||
import { Variants } from "./variants"
|
||||
import { PartialUnionRes, UnionRes, Variants } from "./variants"
|
||||
import {
|
||||
FilePath,
|
||||
Pattern,
|
||||
@@ -26,37 +26,14 @@ import {
|
||||
string,
|
||||
unknown,
|
||||
} from "ts-matches"
|
||||
import { DeepPartial } from "../../../types"
|
||||
|
||||
export type RequiredDefault<A> =
|
||||
| false
|
||||
| {
|
||||
default: A | null
|
||||
}
|
||||
|
||||
function requiredLikeToAbove<Input extends RequiredDefault<A>, A>(
|
||||
requiredLike: Input,
|
||||
) {
|
||||
// prettier-ignore
|
||||
return {
|
||||
required: (typeof requiredLike === 'object' ? true : requiredLike) as (
|
||||
Input extends { default: unknown} ? true:
|
||||
Input extends true ? true :
|
||||
false
|
||||
),
|
||||
default:(typeof requiredLike === 'object' ? requiredLike.default : null) as (
|
||||
Input extends { default: infer Default } ? Default :
|
||||
null
|
||||
)
|
||||
};
|
||||
}
|
||||
type AsRequired<Type, MaybeRequiredType> = MaybeRequiredType extends
|
||||
| { default: unknown }
|
||||
| never
|
||||
? Type
|
||||
: Type | null | undefined
|
||||
type AsRequired<T, Required extends boolean> = Required extends true
|
||||
? T
|
||||
: T | null | undefined
|
||||
|
||||
const testForAsRequiredParser = once(
|
||||
() => object({ required: object({ default: unknown }) }).test,
|
||||
() => object({ required: literal(true) }).test,
|
||||
)
|
||||
function asRequiredParser<
|
||||
Type,
|
||||
@@ -69,6 +46,13 @@ function asRequiredParser<
|
||||
return parser.optional() as any
|
||||
}
|
||||
|
||||
export type PartialValue<T> =
|
||||
T extends UnionRes<infer A, infer B>
|
||||
? PartialUnionRes<A, B>
|
||||
: T extends {}
|
||||
? { [P in keyof T]?: PartialValue<T[P]> }
|
||||
: T
|
||||
|
||||
export class Value<Type, Store> {
|
||||
protected constructor(
|
||||
public build: LazyBuild<Store, ValueSpec>,
|
||||
@@ -122,19 +106,19 @@ export class Value<Type, Store> {
|
||||
boolean,
|
||||
)
|
||||
}
|
||||
static text<Required extends RequiredDefault<DefaultString>>(a: {
|
||||
static text<Required extends boolean>(a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
/** Presents a warning prompt before permitting the value to change. */
|
||||
warning?: string | null
|
||||
/**
|
||||
* @description Determines if the field is required. If so, optionally provide a default value.
|
||||
* @type { false | { default: string | RandomString | null } }
|
||||
* @example required: false
|
||||
* @example required: { default: null }
|
||||
* @example required: { default: 'World' }
|
||||
* @example required: { default: { charset: 'abcdefg', len: 16 } }
|
||||
* provide a default value.
|
||||
* @type { string | RandomString | null }
|
||||
* @example default: null
|
||||
* @example default: 'World'
|
||||
* @example default: { charset: 'abcdefg', len: 16 }
|
||||
*/
|
||||
default: string | RandomString | null
|
||||
required: Required
|
||||
/**
|
||||
* @description Mask (aka camouflage) text input with dots: ● ● ●
|
||||
@@ -188,7 +172,6 @@ export class Value<Type, Store> {
|
||||
immutable: a.immutable ?? false,
|
||||
generate: a.generate ?? null,
|
||||
...a,
|
||||
...requiredLikeToAbove(a.required),
|
||||
}),
|
||||
asRequiredParser(string, a),
|
||||
)
|
||||
@@ -200,7 +183,8 @@ export class Value<Type, Store> {
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
required: RequiredDefault<DefaultString>
|
||||
default: DefaultString | null
|
||||
required: boolean
|
||||
masked?: boolean
|
||||
placeholder?: string | null
|
||||
minLength?: number | null
|
||||
@@ -228,19 +212,16 @@ export class Value<Type, Store> {
|
||||
immutable: false,
|
||||
generate: a.generate ?? null,
|
||||
...a,
|
||||
...requiredLikeToAbove(a.required),
|
||||
}
|
||||
}, string.optional())
|
||||
}
|
||||
static textarea(a: {
|
||||
static textarea<Required extends boolean>(a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
/** Presents a warning prompt before permitting the value to change. */
|
||||
warning?: string | null
|
||||
/**
|
||||
* @description Unlike other "required" fields, for textarea this is a simple boolean.
|
||||
*/
|
||||
required: boolean
|
||||
default: string | null
|
||||
required: Required
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
placeholder?: string | null
|
||||
@@ -250,20 +231,23 @@ export class Value<Type, Store> {
|
||||
*/
|
||||
immutable?: boolean
|
||||
}) {
|
||||
return new Value<string, never>(async () => {
|
||||
const built: ValueSpecTextarea = {
|
||||
description: null,
|
||||
warning: null,
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
placeholder: null,
|
||||
type: "textarea" as const,
|
||||
disabled: false,
|
||||
immutable: a.immutable ?? false,
|
||||
...a,
|
||||
}
|
||||
return built
|
||||
}, string)
|
||||
return new Value<AsRequired<string, Required>, never>(
|
||||
async () => {
|
||||
const built: ValueSpecTextarea = {
|
||||
description: null,
|
||||
warning: null,
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
placeholder: null,
|
||||
type: "textarea" as const,
|
||||
disabled: false,
|
||||
immutable: a.immutable ?? false,
|
||||
...a,
|
||||
}
|
||||
return built
|
||||
},
|
||||
asRequiredParser(string, a),
|
||||
)
|
||||
}
|
||||
static dynamicTextarea<Store = never>(
|
||||
getA: LazyBuild<
|
||||
@@ -272,6 +256,7 @@ export class Value<Type, Store> {
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: string | null
|
||||
required: boolean
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
@@ -280,7 +265,7 @@ export class Value<Type, Store> {
|
||||
}
|
||||
>,
|
||||
) {
|
||||
return new Value<string, Store>(async (options) => {
|
||||
return new Value<string | null | undefined, Store>(async (options) => {
|
||||
const a = await getA(options)
|
||||
return {
|
||||
description: null,
|
||||
@@ -293,20 +278,20 @@ export class Value<Type, Store> {
|
||||
immutable: false,
|
||||
...a,
|
||||
}
|
||||
}, string)
|
||||
}, string.optional())
|
||||
}
|
||||
static number<Required extends RequiredDefault<number>>(a: {
|
||||
static number<Required extends boolean>(a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
/** Presents a warning prompt before permitting the value to change. */
|
||||
warning?: string | null
|
||||
/**
|
||||
* @description Determines if the field is required. If so, optionally provide a default value.
|
||||
* @type { false | { default: number | null } }
|
||||
* @example required: false
|
||||
* @example required: { default: null }
|
||||
* @example required: { default: 7 }
|
||||
* @description optionally provide a default value.
|
||||
* @type { default: number | null }
|
||||
* @example default: null
|
||||
* @example default: 7
|
||||
*/
|
||||
default: number | null
|
||||
required: Required
|
||||
min?: number | null
|
||||
max?: number | null
|
||||
@@ -343,7 +328,6 @@ export class Value<Type, Store> {
|
||||
disabled: false,
|
||||
immutable: a.immutable ?? false,
|
||||
...a,
|
||||
...requiredLikeToAbove(a.required),
|
||||
}),
|
||||
asRequiredParser(number, a),
|
||||
)
|
||||
@@ -355,7 +339,8 @@ export class Value<Type, Store> {
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
required: RequiredDefault<number>
|
||||
default: number | null
|
||||
required: boolean
|
||||
min?: number | null
|
||||
max?: number | null
|
||||
step?: number | null
|
||||
@@ -380,22 +365,21 @@ export class Value<Type, Store> {
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
...a,
|
||||
...requiredLikeToAbove(a.required),
|
||||
}
|
||||
}, number.optional())
|
||||
}
|
||||
static color<Required extends RequiredDefault<string>>(a: {
|
||||
static color<Required extends boolean>(a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
/** Presents a warning prompt before permitting the value to change. */
|
||||
warning?: string | null
|
||||
/**
|
||||
* @description Determines if the field is required. If so, optionally provide a default value.
|
||||
* @type { false | { default: string | null } }
|
||||
* @example required: false
|
||||
* @example required: { default: null }
|
||||
* @example required: { default: 'ffffff' }
|
||||
* @description optionally provide a default value.
|
||||
* @type { default: string | null }
|
||||
* @example default: null
|
||||
* @example default: 'ffffff'
|
||||
*/
|
||||
default: string | null
|
||||
required: Required
|
||||
/**
|
||||
* @description Once set, the value can never be changed.
|
||||
@@ -411,9 +395,7 @@ export class Value<Type, Store> {
|
||||
disabled: false,
|
||||
immutable: a.immutable ?? false,
|
||||
...a,
|
||||
...requiredLikeToAbove(a.required),
|
||||
}),
|
||||
|
||||
asRequiredParser(string, a),
|
||||
)
|
||||
}
|
||||
@@ -425,7 +407,8 @@ export class Value<Type, Store> {
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
required: RequiredDefault<string>
|
||||
default: string | null
|
||||
required: boolean
|
||||
disabled?: false | string
|
||||
}
|
||||
>,
|
||||
@@ -439,22 +422,21 @@ export class Value<Type, Store> {
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
...a,
|
||||
...requiredLikeToAbove(a.required),
|
||||
}
|
||||
}, string.optional())
|
||||
}
|
||||
static datetime<Required extends RequiredDefault<string>>(a: {
|
||||
static datetime<Required extends boolean>(a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
/** Presents a warning prompt before permitting the value to change. */
|
||||
warning?: string | null
|
||||
/**
|
||||
* @description Determines if the field is required. If so, optionally provide a default value.
|
||||
* @type { false | { default: string | null } }
|
||||
* @example required: false
|
||||
* @example required: { default: null }
|
||||
* @example required: { default: '1985-12-16 18:00:00.000' }
|
||||
* @description optionally provide a default value.
|
||||
* @type { default: string | null }
|
||||
* @example default: null
|
||||
* @example default: '1985-12-16 18:00:00.000'
|
||||
*/
|
||||
default: string | null
|
||||
required: Required
|
||||
/**
|
||||
* @description Informs the browser how to behave and which date/time component to display.
|
||||
@@ -481,7 +463,6 @@ export class Value<Type, Store> {
|
||||
disabled: false,
|
||||
immutable: a.immutable ?? false,
|
||||
...a,
|
||||
...requiredLikeToAbove(a.required),
|
||||
}),
|
||||
asRequiredParser(string, a),
|
||||
)
|
||||
@@ -493,7 +474,8 @@ export class Value<Type, Store> {
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
required: RequiredDefault<string>
|
||||
default: string | null
|
||||
required: boolean
|
||||
inputmode?: ValueSpecDatetime["inputmode"]
|
||||
min?: string | null
|
||||
max?: string | null
|
||||
@@ -513,26 +495,21 @@ export class Value<Type, Store> {
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
...a,
|
||||
...requiredLikeToAbove(a.required),
|
||||
}
|
||||
}, string.optional())
|
||||
}
|
||||
static select<
|
||||
Required extends RequiredDefault<string>,
|
||||
Values extends Record<string, string>,
|
||||
>(a: {
|
||||
static select<Values extends Record<string, string>>(a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
/** Presents a warning prompt before permitting the value to change. */
|
||||
warning?: string | null
|
||||
/**
|
||||
* @description Determines if the field is required. If so, optionally provide a default value from the list of values.
|
||||
* @type { false | { default: string | null } }
|
||||
* @example required: false
|
||||
* @example required: { default: null }
|
||||
* @example required: { default: 'radio1' }
|
||||
* @type { (keyof Values & string) | null }
|
||||
* @example default: null
|
||||
* @example default: 'radio1'
|
||||
*/
|
||||
required: Required
|
||||
default: keyof Values & string
|
||||
/**
|
||||
* @description A mapping of unique radio options to their human readable display format.
|
||||
* @example
|
||||
@@ -551,7 +528,7 @@ export class Value<Type, Store> {
|
||||
*/
|
||||
immutable?: boolean
|
||||
}) {
|
||||
return new Value<AsRequired<keyof Values, Required>, never>(
|
||||
return new Value<keyof Values & string, never>(
|
||||
() => ({
|
||||
description: null,
|
||||
warning: null,
|
||||
@@ -559,16 +536,10 @@ export class Value<Type, Store> {
|
||||
disabled: false,
|
||||
immutable: a.immutable ?? false,
|
||||
...a,
|
||||
...requiredLikeToAbove(a.required),
|
||||
}),
|
||||
asRequiredParser(
|
||||
anyOf(
|
||||
...Object.keys(a.values).map((x: keyof Values & string) =>
|
||||
literal(x),
|
||||
),
|
||||
),
|
||||
a,
|
||||
) as any,
|
||||
anyOf(
|
||||
...Object.keys(a.values).map((x: keyof Values & string) => literal(x)),
|
||||
),
|
||||
)
|
||||
}
|
||||
static dynamicSelect<Store = never>(
|
||||
@@ -578,13 +549,13 @@ export class Value<Type, Store> {
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
required: RequiredDefault<string>
|
||||
default: string
|
||||
values: Record<string, string>
|
||||
disabled?: false | string | string[]
|
||||
}
|
||||
>,
|
||||
) {
|
||||
return new Value<string | null | undefined, Store>(async (options) => {
|
||||
return new Value<string, Store>(async (options) => {
|
||||
const a = await getA(options)
|
||||
return {
|
||||
description: null,
|
||||
@@ -593,9 +564,8 @@ export class Value<Type, Store> {
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
...a,
|
||||
...requiredLikeToAbove(a.required),
|
||||
}
|
||||
}, string.optional())
|
||||
}, string)
|
||||
}
|
||||
static multiselect<Values extends Record<string, string>>(a: {
|
||||
name: string
|
||||
@@ -605,7 +575,7 @@ export class Value<Type, Store> {
|
||||
/**
|
||||
* @description A simple list of which options should be checked by default.
|
||||
*/
|
||||
default: string[]
|
||||
default: (keyof Values & string)[]
|
||||
/**
|
||||
* @description A mapping of checkbox options to their human readable display format.
|
||||
* @example
|
||||
@@ -689,11 +659,11 @@ export class Value<Type, Store> {
|
||||
}
|
||||
}, spec.validator)
|
||||
}
|
||||
// static file<Store>(a: {
|
||||
// static file<Store, Required extends boolean>(a: {
|
||||
// name: string
|
||||
// description?: string | null
|
||||
// extensions: string[]
|
||||
// required: boolean
|
||||
// required: Required
|
||||
// }) {
|
||||
// const buildValue = {
|
||||
// type: "file" as const,
|
||||
@@ -701,14 +671,14 @@ export class Value<Type, Store> {
|
||||
// warning: null,
|
||||
// ...a,
|
||||
// }
|
||||
// return new Value<FilePath, Store>(
|
||||
// return new Value<AsRequired<FilePath, Required>, Store>(
|
||||
// () => ({
|
||||
// ...buildValue,
|
||||
// }),
|
||||
// asRequiredParser(object({ filePath: string }), a),
|
||||
// )
|
||||
// }
|
||||
// static dynamicFile<Required extends boolean, Store>(
|
||||
// static dynamicFile<Store>(
|
||||
// a: LazyBuild<
|
||||
// Store,
|
||||
// {
|
||||
@@ -716,43 +686,49 @@ export class Value<Type, Store> {
|
||||
// description?: string | null
|
||||
// warning?: string | null
|
||||
// extensions: string[]
|
||||
// required: Required
|
||||
// required: boolean
|
||||
// }
|
||||
// >,
|
||||
// ) {
|
||||
// return new Value<string | null | undefined, Store>(
|
||||
// return new Value<FilePath | null | undefined, Store>(
|
||||
// async (options) => ({
|
||||
// type: "file" as const,
|
||||
// description: null,
|
||||
// warning: null,
|
||||
// ...(await a(options)),
|
||||
// }),
|
||||
// string.optional(),
|
||||
// object({ filePath: string }).optional(),
|
||||
// )
|
||||
// }
|
||||
static union<Required extends RequiredDefault<string>, Type, Store>(
|
||||
static union<
|
||||
VariantValues extends {
|
||||
[K in string]: {
|
||||
name: string
|
||||
spec: InputSpec<any, Store> | InputSpec<any, never>
|
||||
}
|
||||
},
|
||||
Store,
|
||||
>(
|
||||
a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
/** Presents a warning prompt before permitting the value to change. */
|
||||
warning?: string | null
|
||||
/**
|
||||
* @description Determines if the field is required. If so, optionally provide a default value from the list of variants.
|
||||
* @type { false | { default: string | null } }
|
||||
* @example required: false
|
||||
* @example required: { default: null }
|
||||
* @example required: { default: 'variant1' }
|
||||
* @description Provide a default value from the list of variants.
|
||||
* @type { string }
|
||||
* @example default: 'variant1'
|
||||
*/
|
||||
required: Required
|
||||
default: keyof VariantValues & string
|
||||
/**
|
||||
* @description Once set, the value can never be changed.
|
||||
* @default false
|
||||
*/
|
||||
immutable?: boolean
|
||||
},
|
||||
aVariants: Variants<Type, Store>,
|
||||
aVariants: Variants<VariantValues, Store>,
|
||||
) {
|
||||
return new Value<AsRequired<Type, Required>, Store>(
|
||||
return new Value<typeof aVariants.validator._TYPE, Store>(
|
||||
async (options) => ({
|
||||
type: "union" as const,
|
||||
description: null,
|
||||
@@ -760,44 +736,50 @@ export class Value<Type, Store> {
|
||||
disabled: false,
|
||||
...a,
|
||||
variants: await aVariants.build(options as any),
|
||||
...requiredLikeToAbove(a.required),
|
||||
immutable: a.immutable ?? false,
|
||||
}),
|
||||
asRequiredParser(aVariants.validator, a),
|
||||
aVariants.validator,
|
||||
)
|
||||
}
|
||||
static filteredUnion<
|
||||
Required extends RequiredDefault<string>,
|
||||
Type extends Record<string, any>,
|
||||
Store = never,
|
||||
VariantValues extends {
|
||||
[K in string]: {
|
||||
name: string
|
||||
spec: InputSpec<any, Store> | InputSpec<any, never>
|
||||
}
|
||||
},
|
||||
Store,
|
||||
>(
|
||||
getDisabledFn: LazyBuild<Store, string[] | false | string>,
|
||||
a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
required: Required
|
||||
default: keyof VariantValues & string
|
||||
},
|
||||
aVariants: Variants<Type, Store> | Variants<Type, never>,
|
||||
aVariants: Variants<VariantValues, Store> | Variants<VariantValues, never>,
|
||||
) {
|
||||
return new Value<AsRequired<Type, Required>, Store>(
|
||||
return new Value<typeof aVariants.validator._TYPE, Store>(
|
||||
async (options) => ({
|
||||
type: "union" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
...a,
|
||||
variants: await aVariants.build(options as any),
|
||||
...requiredLikeToAbove(a.required),
|
||||
disabled: (await getDisabledFn(options)) || false,
|
||||
immutable: false,
|
||||
}),
|
||||
asRequiredParser(aVariants.validator, a),
|
||||
aVariants.validator,
|
||||
)
|
||||
}
|
||||
static dynamicUnion<
|
||||
Required extends RequiredDefault<string>,
|
||||
Type extends Record<string, any>,
|
||||
Store = never,
|
||||
VariantValues extends {
|
||||
[K in string]: {
|
||||
name: string
|
||||
spec: InputSpec<any, Store> | InputSpec<any, never>
|
||||
}
|
||||
},
|
||||
Store,
|
||||
>(
|
||||
getA: LazyBuild<
|
||||
Store,
|
||||
@@ -805,24 +787,26 @@ export class Value<Type, Store> {
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
required: Required
|
||||
default: keyof VariantValues & string
|
||||
disabled: string[] | false | string
|
||||
}
|
||||
>,
|
||||
aVariants: Variants<Type, Store> | Variants<Type, never>,
|
||||
aVariants: Variants<VariantValues, Store> | Variants<VariantValues, never>,
|
||||
) {
|
||||
return new Value<Type | null | undefined, Store>(async (options) => {
|
||||
const newValues = await getA(options)
|
||||
return {
|
||||
type: "union" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
...newValues,
|
||||
variants: await aVariants.build(options as any),
|
||||
...requiredLikeToAbove(newValues.required),
|
||||
immutable: false,
|
||||
}
|
||||
}, aVariants.validator.optional())
|
||||
return new Value<typeof aVariants.validator._TYPE, Store>(
|
||||
async (options) => {
|
||||
const newValues = await getA(options)
|
||||
return {
|
||||
type: "union" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
...newValues,
|
||||
variants: await aVariants.build(options as any),
|
||||
immutable: false,
|
||||
}
|
||||
},
|
||||
aVariants.validator,
|
||||
)
|
||||
}
|
||||
|
||||
static list<Type, Store>(a: List<Type, Store>) {
|
||||
|
||||
@@ -1,6 +1,54 @@
|
||||
import { DeepPartial } from "../../../types"
|
||||
import { ValueSpec, ValueSpecUnion } from "../inputSpecTypes"
|
||||
import { LazyBuild, InputSpec } from "./inputSpec"
|
||||
import { Parser, anyOf, literals, object } from "ts-matches"
|
||||
import {
|
||||
LazyBuild,
|
||||
InputSpec,
|
||||
ExtractInputSpecType,
|
||||
ExtractPartialInputSpecType,
|
||||
} from "./inputSpec"
|
||||
import { Parser, anyOf, literal, object } from "ts-matches"
|
||||
|
||||
export type UnionRes<
|
||||
Store,
|
||||
VariantValues extends {
|
||||
[K in string]: {
|
||||
name: string
|
||||
spec: InputSpec<any, Store> | InputSpec<any, never>
|
||||
}
|
||||
},
|
||||
K extends keyof VariantValues & string = keyof VariantValues & string,
|
||||
> = {
|
||||
[key in keyof VariantValues]: {
|
||||
selection: key
|
||||
value: ExtractInputSpecType<VariantValues[key]["spec"]>
|
||||
other?: {
|
||||
[key2 in Exclude<keyof VariantValues & string, key>]?: DeepPartial<
|
||||
ExtractInputSpecType<VariantValues[key2]["spec"]>
|
||||
>
|
||||
}
|
||||
}
|
||||
}[K]
|
||||
|
||||
export type PartialUnionRes<
|
||||
Store,
|
||||
VariantValues extends {
|
||||
[K in string]: {
|
||||
name: string
|
||||
spec: InputSpec<any, Store> | InputSpec<any, never>
|
||||
}
|
||||
},
|
||||
K extends keyof VariantValues & string = keyof VariantValues & string,
|
||||
> = {
|
||||
[key in keyof VariantValues]: {
|
||||
selection?: key
|
||||
value?: ExtractPartialInputSpecType<VariantValues[key]["spec"]>
|
||||
other?: {
|
||||
[key2 in Exclude<keyof VariantValues & string, key>]?: DeepPartial<
|
||||
ExtractInputSpecType<VariantValues[key2]["spec"]>
|
||||
>
|
||||
}
|
||||
}
|
||||
}[K]
|
||||
|
||||
/**
|
||||
* Used in the the Value.select { @link './value.ts' }
|
||||
@@ -44,18 +92,24 @@ export const pruning = Value.union(
|
||||
description:
|
||||
'- Disabled: Disable pruning\n- Automatic: Limit blockchain size on disk to a certain number of megabytes\n- Manual: Prune blockchain with the "pruneblockchain" RPC\n',
|
||||
warning: null,
|
||||
required: true,
|
||||
default: "disabled",
|
||||
},
|
||||
pruningSettingsVariants
|
||||
);
|
||||
```
|
||||
*/
|
||||
export class Variants<Type, Store> {
|
||||
static text: any
|
||||
export class Variants<
|
||||
VariantValues extends {
|
||||
[K in string]: {
|
||||
name: string
|
||||
spec: InputSpec<any, Store> | InputSpec<any, never>
|
||||
}
|
||||
},
|
||||
Store,
|
||||
> {
|
||||
private constructor(
|
||||
public build: LazyBuild<Store, ValueSpecUnion["variants"]>,
|
||||
public validator: Parser<unknown, Type>,
|
||||
public validator: Parser<unknown, UnionRes<Store, VariantValues>>,
|
||||
) {}
|
||||
static of<
|
||||
VariantValues extends {
|
||||
@@ -67,26 +121,15 @@ export class Variants<Type, Store> {
|
||||
Store = never,
|
||||
>(a: VariantValues) {
|
||||
const validator = anyOf(
|
||||
...Object.entries(a).map(([name, { spec }]) =>
|
||||
...Object.entries(a).map(([id, { spec }]) =>
|
||||
object({
|
||||
selection: literals(name),
|
||||
selection: literal(id),
|
||||
value: spec.validator,
|
||||
}),
|
||||
),
|
||||
) as Parser<unknown, any>
|
||||
|
||||
return new Variants<
|
||||
{
|
||||
[K in keyof VariantValues]: {
|
||||
selection: K
|
||||
// prettier-ignore
|
||||
value:
|
||||
VariantValues[K]["spec"] extends (InputSpec<infer B, Store> | InputSpec<infer B, never>) ? B :
|
||||
never
|
||||
}
|
||||
}[keyof VariantValues],
|
||||
Store
|
||||
>(async (options) => {
|
||||
return new Variants<VariantValues, Store>(async (options) => {
|
||||
const variants = {} as {
|
||||
[K in keyof VariantValues]: {
|
||||
name: string
|
||||
@@ -118,6 +161,6 @@ export class Variants<Type, Store> {
|
||||
```
|
||||
*/
|
||||
withStore<NewStore extends Store extends never ? any : Store>() {
|
||||
return this as any as Variants<Type, NewStore>
|
||||
return this as any as Variants<VariantValues, NewStore>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,35 +10,34 @@ import { Variants } from "./builder/variants"
|
||||
export const customSmtp = InputSpec.of<InputSpecOf<SmtpValue>, never>({
|
||||
server: Value.text({
|
||||
name: "SMTP Server",
|
||||
required: {
|
||||
default: null,
|
||||
},
|
||||
required: true,
|
||||
default: null,
|
||||
}),
|
||||
port: Value.number({
|
||||
name: "Port",
|
||||
required: { default: 587 },
|
||||
required: true,
|
||||
default: 587,
|
||||
min: 1,
|
||||
max: 65535,
|
||||
integer: true,
|
||||
}),
|
||||
from: Value.text({
|
||||
name: "From Address",
|
||||
required: {
|
||||
default: null,
|
||||
},
|
||||
required: true,
|
||||
default: null,
|
||||
placeholder: "<name>test@example.com",
|
||||
inputmode: "email",
|
||||
patterns: [Patterns.email],
|
||||
}),
|
||||
login: Value.text({
|
||||
name: "Login",
|
||||
required: {
|
||||
default: null,
|
||||
},
|
||||
required: true,
|
||||
default: null,
|
||||
}),
|
||||
password: Value.text({
|
||||
name: "Password",
|
||||
required: false,
|
||||
default: null,
|
||||
masked: true,
|
||||
}),
|
||||
})
|
||||
@@ -54,7 +53,7 @@ export const smtpInputSpec = Value.filteredUnion(
|
||||
{
|
||||
name: "SMTP",
|
||||
description: "Optionally provide an SMTP server for sending emails",
|
||||
required: { default: "disabled" },
|
||||
default: "disabled",
|
||||
},
|
||||
Variants.of({
|
||||
disabled: { name: "Disabled", spec: InputSpec.of({}) },
|
||||
@@ -66,6 +65,7 @@ export const smtpInputSpec = Value.filteredUnion(
|
||||
description:
|
||||
"A custom from address for this service. If not provided, the system from address will be used.",
|
||||
required: false,
|
||||
default: null,
|
||||
placeholder: "<name>test@example.com",
|
||||
inputmode: "email",
|
||||
patterns: [Patterns.email],
|
||||
|
||||
@@ -115,7 +115,6 @@ export type ValueSpecSelect = {
|
||||
description: string | null
|
||||
warning: string | null
|
||||
type: "select"
|
||||
required: boolean
|
||||
default: string | null
|
||||
disabled: false | string | string[]
|
||||
immutable: boolean
|
||||
@@ -158,7 +157,6 @@ export type ValueSpecUnion = {
|
||||
}
|
||||
>
|
||||
disabled: false | string | string[]
|
||||
required: boolean
|
||||
default: string | null
|
||||
immutable: boolean
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { InputSpec } from "./input/builder"
|
||||
import { ExtractInputSpecType } from "./input/builder/inputSpec"
|
||||
import {
|
||||
ExtractInputSpecType,
|
||||
ExtractPartialInputSpecType,
|
||||
} from "./input/builder/inputSpec"
|
||||
import * as T from "../types"
|
||||
import { once } from "../util"
|
||||
|
||||
@@ -20,7 +23,10 @@ export type GetInput<
|
||||
> = (options: {
|
||||
effects: T.Effects
|
||||
}) => Promise<
|
||||
null | void | undefined | (ExtractInputSpecType<A> & Record<string, any>)
|
||||
| null
|
||||
| void
|
||||
| undefined
|
||||
| (ExtractPartialInputSpecType<A> & Record<string, any>)
|
||||
>
|
||||
|
||||
export type MaybeFn<T> = T | ((options: { effects: T.Effects }) => Promise<T>)
|
||||
@@ -52,15 +58,13 @@ export class Action<
|
||||
| Record<string, any>
|
||||
| InputSpec<any, Store>
|
||||
| InputSpec<any, never>,
|
||||
Type extends
|
||||
ExtractInputSpecType<InputSpecType> = ExtractInputSpecType<InputSpecType>,
|
||||
> {
|
||||
private constructor(
|
||||
readonly id: Id,
|
||||
private readonly metadataFn: MaybeFn<T.ActionMetadata>,
|
||||
private readonly inputSpec: InputSpecType,
|
||||
private readonly getInputFn: GetInput<Type>,
|
||||
private readonly runFn: Run<Type>,
|
||||
private readonly getInputFn: GetInput<ExtractInputSpecType<InputSpecType>>,
|
||||
private readonly runFn: Run<ExtractInputSpecType<InputSpecType>>,
|
||||
) {}
|
||||
static withInput<
|
||||
Id extends T.ActionId,
|
||||
@@ -69,15 +73,13 @@ export class Action<
|
||||
| Record<string, any>
|
||||
| InputSpec<any, Store>
|
||||
| InputSpec<any, never>,
|
||||
Type extends
|
||||
ExtractInputSpecType<InputSpecType> = ExtractInputSpecType<InputSpecType>,
|
||||
>(
|
||||
id: Id,
|
||||
metadata: MaybeFn<Omit<T.ActionMetadata, "hasInput">>,
|
||||
inputSpec: InputSpecType,
|
||||
getInput: GetInput<Type>,
|
||||
run: Run<Type>,
|
||||
): Action<Id, Store, InputSpecType, Type> {
|
||||
getInput: GetInput<ExtractInputSpecType<InputSpecType>>,
|
||||
run: Run<ExtractInputSpecType<InputSpecType>>,
|
||||
): Action<Id, Store, InputSpecType> {
|
||||
return new Action(
|
||||
id,
|
||||
mapMaybeFn(metadata, (m) => ({ ...m, hasInput: true })),
|
||||
@@ -90,7 +92,7 @@ export class Action<
|
||||
id: Id,
|
||||
metadata: MaybeFn<Omit<T.ActionMetadata, "hasInput">>,
|
||||
run: Run<{}>,
|
||||
): Action<Id, Store, {}, {}> {
|
||||
): Action<Id, Store, {}> {
|
||||
return new Action(
|
||||
id,
|
||||
mapMaybeFn(metadata, (m) => ({ ...m, hasInput: false })),
|
||||
@@ -114,7 +116,7 @@ export class Action<
|
||||
}
|
||||
async run(options: {
|
||||
effects: T.Effects
|
||||
input: Type
|
||||
input: ExtractInputSpecType<InputSpecType>
|
||||
}): Promise<T.ActionResult | null> {
|
||||
return (await this.runFn(options)) || null
|
||||
}
|
||||
@@ -122,13 +124,13 @@ export class Action<
|
||||
|
||||
export class Actions<
|
||||
Store,
|
||||
AllActions extends Record<T.ActionId, Action<T.ActionId, Store, any, any>>,
|
||||
AllActions extends Record<T.ActionId, Action<T.ActionId, Store, any>>,
|
||||
> {
|
||||
private constructor(private readonly actions: AllActions) {}
|
||||
static of<Store>(): Actions<Store, {}> {
|
||||
return new Actions({})
|
||||
}
|
||||
addAction<A extends Action<T.ActionId, Store, any, any>>(
|
||||
addAction<A extends Action<T.ActionId, Store, any>>(
|
||||
action: A,
|
||||
): Actions<Store, AllActions & { [id in A["id"]]: A }> {
|
||||
return new Actions({ ...this.actions, [action.id]: action })
|
||||
|
||||
@@ -86,7 +86,7 @@ export namespace ExpectedExports {
|
||||
|
||||
export type actions = Actions<
|
||||
any,
|
||||
Record<ActionId, Action<ActionId, any, any, any>>
|
||||
Record<ActionId, Action<ActionId, any, any>>
|
||||
>
|
||||
}
|
||||
export type ABI = {
|
||||
|
||||
9
sdk/base/package-lock.json
generated
9
sdk/base/package-lock.json
generated
@@ -14,7 +14,7 @@
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"mime-types": "^2.1.35",
|
||||
"ts-matches": "^5.5.1",
|
||||
"ts-matches": "^6.0.0",
|
||||
"yaml": "^2.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -3897,9 +3897,10 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ts-matches": {
|
||||
"version": "5.5.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-5.5.1.tgz",
|
||||
"integrity": "sha512-UFYaKgfqlg9FROK7bdpYqFwG1CJvP4kOJdjXuWoqxo9jCmANoDw1GxkSCpJgoTeIiSTaTH5Qr1klSspb8c+ydg=="
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-6.0.0.tgz",
|
||||
"integrity": "sha512-vR4hhz9bYMW30qIJUuLaeAWlsR54vse6ZI2riVhVLMBE6/vss43jwrOvbHheiyU7e26ssT/yWx69aJHD2REJSA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ts-morph": {
|
||||
"version": "18.0.0",
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"mime-types": "^2.1.35",
|
||||
"ts-matches": "^5.5.1",
|
||||
"ts-matches": "^6.0.0",
|
||||
"yaml": "^2.2.2"
|
||||
},
|
||||
"prettier": {
|
||||
|
||||
Reference in New Issue
Block a user