mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 04:23:40 +00:00
Feature/more dynamic unions (#2972)
* with validators * more dynamic unions * fixes from v31 * better constructor for dynamic unions * version bump * fix build
This commit is contained in:
@@ -46,7 +46,7 @@ export const runAction = async <
|
||||
}
|
||||
}
|
||||
type GetActionInputType<A extends ActionInfo<T.ActionId, any>> =
|
||||
A extends Action<T.ActionId, infer I> ? ExtractInputSpecType<I> : never
|
||||
A extends Action<T.ActionId, infer I> ? I : never
|
||||
|
||||
type TaskBase = {
|
||||
reason?: string
|
||||
|
||||
@@ -13,13 +13,17 @@ export type LazyBuild<ExpectedOut> = (
|
||||
) => Promise<ExpectedOut> | ExpectedOut
|
||||
|
||||
// prettier-ignore
|
||||
export type ExtractInputSpecType<A extends Record<string, any> | InputSpec<Record<string, any>>> =
|
||||
A extends InputSpec<infer B> ? B :
|
||||
A
|
||||
export type ExtractInputSpecType<A extends InputSpec<Record<string, any>, any>> =
|
||||
A extends InputSpec<infer B, any> ? B :
|
||||
never
|
||||
|
||||
export type ExtractPartialInputSpecType<
|
||||
A extends Record<string, any> | InputSpec<Record<string, any>>,
|
||||
> = A extends InputSpec<infer B> ? DeepPartial<B> : DeepPartial<A>
|
||||
export type ExtractInputSpecStaticValidatedAs<
|
||||
A extends InputSpec<any, Record<string, any>>,
|
||||
> = A extends InputSpec<any, infer B> ? B : never
|
||||
|
||||
// export type ExtractPartialInputSpecType<
|
||||
// A extends Record<string, any> | InputSpec<Record<string, any>>,
|
||||
// > = A extends InputSpec<infer B> ? DeepPartial<B> : DeepPartial<A>
|
||||
|
||||
export type InputSpecOf<A extends Record<string, any>> = {
|
||||
[K in keyof A]: Value<A[K]>
|
||||
@@ -82,35 +86,54 @@ export const addNodesSpec = InputSpec.of({ hostname: hostname, port: port });
|
||||
|
||||
```
|
||||
*/
|
||||
export class InputSpec<Type extends Record<string, any>> {
|
||||
export class InputSpec<
|
||||
Type extends StaticValidatedAs,
|
||||
StaticValidatedAs extends Record<string, any> = Type,
|
||||
> {
|
||||
private constructor(
|
||||
private readonly spec: {
|
||||
[K in keyof Type]: Value<Type[K]>
|
||||
},
|
||||
public validator: Parser<unknown, Type>,
|
||||
public readonly validator: Parser<unknown, StaticValidatedAs>,
|
||||
) {}
|
||||
public _TYPE: Type = null as any as Type
|
||||
public _PARTIAL: DeepPartial<Type> = null as any as DeepPartial<Type>
|
||||
async build(options: LazyBuildOptions) {
|
||||
async build(options: LazyBuildOptions): Promise<{
|
||||
spec: {
|
||||
[K in keyof Type]: ValueSpec
|
||||
}
|
||||
validator: Parser<unknown, Type>
|
||||
}> {
|
||||
const answer = {} as {
|
||||
[K in keyof Type]: ValueSpec
|
||||
}
|
||||
for (const k in this.spec) {
|
||||
answer[k] = await this.spec[k].build(options as any)
|
||||
const validator = {} as {
|
||||
[K in keyof Type]: Parser<unknown, any>
|
||||
}
|
||||
for (const k in this.spec) {
|
||||
const built = await this.spec[k].build(options as any)
|
||||
answer[k] = built.spec
|
||||
validator[k] = built.validator
|
||||
}
|
||||
return {
|
||||
spec: answer,
|
||||
validator: object(validator) as any,
|
||||
}
|
||||
return answer
|
||||
}
|
||||
|
||||
static of<Spec extends Record<string, Value<any>>>(spec: Spec) {
|
||||
const validatorObj = {} as {
|
||||
[K in keyof Spec]: Parser<unknown, any>
|
||||
}
|
||||
for (const key in spec) {
|
||||
validatorObj[key] = spec[key].validator
|
||||
}
|
||||
const validator = object(validatorObj)
|
||||
return new InputSpec<{
|
||||
[K in keyof Spec]: Spec[K] extends Value<infer T> ? T : never
|
||||
}>(spec, validator as any)
|
||||
static of<Spec extends Record<string, Value<any, any>>>(spec: Spec) {
|
||||
const validator = object(
|
||||
Object.fromEntries(
|
||||
Object.entries(spec).map(([k, v]) => [k, v.validator]),
|
||||
),
|
||||
)
|
||||
return new InputSpec<
|
||||
{
|
||||
[K in keyof Spec]: Spec[K] extends Value<infer T, any> ? T : never
|
||||
},
|
||||
{
|
||||
[K in keyof Spec]: Spec[K] extends Value<any, infer T> ? T : never
|
||||
}
|
||||
>(spec, validator as any)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,15 @@ import {
|
||||
} from "../inputSpecTypes"
|
||||
import { Parser, arrayOf, string } from "ts-matches"
|
||||
|
||||
export class List<Type> {
|
||||
export class List<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
private constructor(
|
||||
public build: LazyBuild<ValueSpecList>,
|
||||
public validator: Parser<unknown, Type>,
|
||||
public build: LazyBuild<{
|
||||
spec: ValueSpecList
|
||||
validator: Parser<unknown, Type>
|
||||
}>,
|
||||
public readonly validator: Parser<unknown, StaticValidatedAs>,
|
||||
) {}
|
||||
readonly _TYPE: Type = null as any
|
||||
|
||||
static text(
|
||||
a: {
|
||||
@@ -58,6 +62,7 @@ export class List<Type> {
|
||||
generate?: null | RandomString
|
||||
},
|
||||
) {
|
||||
const validator = arrayOf(string)
|
||||
return new List<string[]>(() => {
|
||||
const spec = {
|
||||
type: "text" as const,
|
||||
@@ -81,8 +86,8 @@ export class List<Type> {
|
||||
...a,
|
||||
spec,
|
||||
}
|
||||
return built
|
||||
}, arrayOf(string))
|
||||
return { spec: built, validator }
|
||||
}, validator)
|
||||
}
|
||||
|
||||
static dynamicText(
|
||||
@@ -105,6 +110,7 @@ export class List<Type> {
|
||||
}
|
||||
}>,
|
||||
) {
|
||||
const validator = arrayOf(string)
|
||||
return new List<string[]>(async (options) => {
|
||||
const { spec: aSpec, ...a } = await getA(options)
|
||||
const spec = {
|
||||
@@ -129,11 +135,15 @@ export class List<Type> {
|
||||
...a,
|
||||
spec,
|
||||
}
|
||||
return built
|
||||
}, arrayOf(string))
|
||||
|
||||
return { spec: built, validator }
|
||||
}, validator)
|
||||
}
|
||||
|
||||
static obj<Type extends Record<string, any>>(
|
||||
static obj<
|
||||
Type extends StaticValidatedAs,
|
||||
StaticValidatedAs extends Record<string, any>,
|
||||
>(
|
||||
a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
@@ -143,20 +153,20 @@ export class List<Type> {
|
||||
maxLength?: number | null
|
||||
},
|
||||
aSpec: {
|
||||
spec: InputSpec<Type>
|
||||
spec: InputSpec<Type, StaticValidatedAs>
|
||||
displayAs?: null | string
|
||||
uniqueBy?: null | UniqueBy
|
||||
},
|
||||
) {
|
||||
return new List<Type[]>(async (options) => {
|
||||
return new List<Type[], StaticValidatedAs[]>(async (options) => {
|
||||
const { spec: previousSpecSpec, ...restSpec } = aSpec
|
||||
const specSpec = await previousSpecSpec.build(options)
|
||||
const built = await previousSpecSpec.build(options)
|
||||
const spec = {
|
||||
type: "object" as const,
|
||||
displayAs: null,
|
||||
uniqueBy: null,
|
||||
...restSpec,
|
||||
spec: specSpec,
|
||||
spec: built.spec,
|
||||
}
|
||||
const value = {
|
||||
spec,
|
||||
@@ -164,13 +174,16 @@ export class List<Type> {
|
||||
...a,
|
||||
}
|
||||
return {
|
||||
description: null,
|
||||
warning: null,
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
type: "list" as const,
|
||||
disabled: false,
|
||||
...value,
|
||||
spec: {
|
||||
description: null,
|
||||
warning: null,
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
type: "list" as const,
|
||||
disabled: false,
|
||||
...value,
|
||||
},
|
||||
validator: arrayOf(built.validator),
|
||||
}
|
||||
}, arrayOf(aSpec.spec.validator))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { InputSpec, LazyBuild } from "./inputSpec"
|
||||
import { ExtractInputSpecType, InputSpec, LazyBuild } from "./inputSpec"
|
||||
import { List } from "./list"
|
||||
import { Variants } from "./variants"
|
||||
import { UnionRes, UnionResStaticValidatedAs, Variants } from "./variants"
|
||||
import {
|
||||
FilePath,
|
||||
Pattern,
|
||||
@@ -34,19 +34,21 @@ type AsRequired<T, Required extends boolean> = Required extends true
|
||||
const testForAsRequiredParser = once(
|
||||
() => object({ required: literal(true) }).test,
|
||||
)
|
||||
function asRequiredParser<
|
||||
Type,
|
||||
Input,
|
||||
Return extends Parser<unknown, Type> | Parser<unknown, Type | null>,
|
||||
>(parser: Parser<unknown, Type>, input: Input): Return {
|
||||
function asRequiredParser<Type, Input extends { required: boolean }>(
|
||||
parser: Parser<unknown, Type>,
|
||||
input: Input,
|
||||
): Parser<unknown, AsRequired<Type, Input["required"]>> {
|
||||
if (testForAsRequiredParser()(input)) return parser as any
|
||||
return parser.nullable() as any
|
||||
}
|
||||
|
||||
export class Value<Type> {
|
||||
export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
protected constructor(
|
||||
public build: LazyBuild<ValueSpec>,
|
||||
public validator: Parser<unknown, Type>,
|
||||
public build: LazyBuild<{
|
||||
spec: ValueSpec
|
||||
validator: Parser<unknown, Type>
|
||||
}>,
|
||||
public readonly validator: Parser<unknown, StaticValidatedAs>,
|
||||
) {}
|
||||
public _TYPE: Type = null as any as Type
|
||||
public _PARTIAL: DeepPartial<Type> = null as any as DeepPartial<Type>
|
||||
@@ -79,16 +81,20 @@ export class Value<Type> {
|
||||
*/
|
||||
immutable?: boolean
|
||||
}) {
|
||||
const validator = boolean
|
||||
return new Value<boolean>(
|
||||
async () => ({
|
||||
description: null,
|
||||
warning: null,
|
||||
type: "toggle" as const,
|
||||
disabled: false,
|
||||
immutable: a.immutable ?? false,
|
||||
...a,
|
||||
spec: {
|
||||
description: null,
|
||||
warning: null,
|
||||
type: "toggle" as const,
|
||||
disabled: false,
|
||||
immutable: a.immutable ?? false,
|
||||
...a,
|
||||
},
|
||||
validator,
|
||||
}),
|
||||
boolean,
|
||||
validator,
|
||||
)
|
||||
}
|
||||
static dynamicToggle(
|
||||
@@ -100,16 +106,20 @@ export class Value<Type> {
|
||||
disabled?: false | string
|
||||
}>,
|
||||
) {
|
||||
const validator = boolean
|
||||
return new Value<boolean>(
|
||||
async (options) => ({
|
||||
description: null,
|
||||
warning: null,
|
||||
type: "toggle" as const,
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
...(await a(options)),
|
||||
spec: {
|
||||
description: null,
|
||||
warning: null,
|
||||
type: "toggle" as const,
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
...(await a(options)),
|
||||
},
|
||||
validator,
|
||||
}),
|
||||
boolean,
|
||||
validator,
|
||||
)
|
||||
}
|
||||
/**
|
||||
@@ -187,32 +197,36 @@ export class Value<Type> {
|
||||
*/
|
||||
generate?: RandomString | null
|
||||
}) {
|
||||
const validator = asRequiredParser(string, a)
|
||||
return new Value<AsRequired<string, Required>>(
|
||||
async () => ({
|
||||
type: "text" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
masked: false,
|
||||
placeholder: null,
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
patterns: [],
|
||||
inputmode: "text",
|
||||
disabled: false,
|
||||
immutable: a.immutable ?? false,
|
||||
generate: a.generate ?? null,
|
||||
...a,
|
||||
spec: {
|
||||
type: "text" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
masked: false,
|
||||
placeholder: null,
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
patterns: [],
|
||||
inputmode: "text",
|
||||
disabled: false,
|
||||
immutable: a.immutable ?? false,
|
||||
generate: a.generate ?? null,
|
||||
...a,
|
||||
},
|
||||
validator,
|
||||
}),
|
||||
asRequiredParser(string, a),
|
||||
validator,
|
||||
)
|
||||
}
|
||||
static dynamicText(
|
||||
static dynamicText<Required extends boolean>(
|
||||
getA: LazyBuild<{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: DefaultString | null
|
||||
required: boolean
|
||||
required: Required
|
||||
masked?: boolean
|
||||
placeholder?: string | null
|
||||
minLength?: number | null
|
||||
@@ -223,24 +237,30 @@ export class Value<Type> {
|
||||
generate?: null | RandomString
|
||||
}>,
|
||||
) {
|
||||
return new Value<string | null>(async (options) => {
|
||||
const a = await getA(options)
|
||||
return {
|
||||
type: "text" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
masked: false,
|
||||
placeholder: null,
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
patterns: [],
|
||||
inputmode: "text",
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
generate: a.generate ?? null,
|
||||
...a,
|
||||
}
|
||||
}, string.nullable())
|
||||
return new Value<AsRequired<string, Required>, string | null>(
|
||||
async (options) => {
|
||||
const a = await getA(options)
|
||||
return {
|
||||
spec: {
|
||||
type: "text" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
masked: false,
|
||||
placeholder: null,
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
patterns: [],
|
||||
inputmode: "text",
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
generate: a.generate ?? null,
|
||||
...a,
|
||||
},
|
||||
validator: asRequiredParser(string, a),
|
||||
}
|
||||
},
|
||||
string.nullable(),
|
||||
)
|
||||
}
|
||||
/**
|
||||
* @description Displays a large textarea field for long form entry.
|
||||
@@ -278,40 +298,9 @@ export class Value<Type> {
|
||||
*/
|
||||
immutable?: boolean
|
||||
}) {
|
||||
return new Value<AsRequired<string, Required>>(
|
||||
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(
|
||||
getA: LazyBuild<{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: string | null
|
||||
required: boolean
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
placeholder?: string | null
|
||||
disabled?: false | string
|
||||
}>,
|
||||
) {
|
||||
return new Value<string | null>(async (options) => {
|
||||
const a = await getA(options)
|
||||
return {
|
||||
const validator = asRequiredParser(string, a)
|
||||
return new Value<AsRequired<string, Required>>(async () => {
|
||||
const built: ValueSpecTextarea = {
|
||||
description: null,
|
||||
warning: null,
|
||||
minLength: null,
|
||||
@@ -319,10 +308,45 @@ export class Value<Type> {
|
||||
placeholder: null,
|
||||
type: "textarea" as const,
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
immutable: a.immutable ?? false,
|
||||
...a,
|
||||
}
|
||||
}, string.nullable())
|
||||
return { spec: built, validator }
|
||||
}, validator)
|
||||
}
|
||||
static dynamicTextarea<Required extends boolean>(
|
||||
getA: LazyBuild<{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: string | null
|
||||
required: Required
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
placeholder?: string | null
|
||||
disabled?: false | string
|
||||
}>,
|
||||
) {
|
||||
return new Value<AsRequired<string, Required>, string | null>(
|
||||
async (options) => {
|
||||
const a = await getA(options)
|
||||
return {
|
||||
spec: {
|
||||
description: null,
|
||||
warning: null,
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
placeholder: null,
|
||||
type: "textarea" as const,
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
...a,
|
||||
},
|
||||
validator: asRequiredParser(string, a),
|
||||
}
|
||||
},
|
||||
string.nullable(),
|
||||
)
|
||||
}
|
||||
/**
|
||||
* @description Displays a number input field
|
||||
@@ -382,30 +406,34 @@ export class Value<Type> {
|
||||
*/
|
||||
immutable?: boolean
|
||||
}) {
|
||||
const validator = asRequiredParser(number, a)
|
||||
return new Value<AsRequired<number, Required>>(
|
||||
() => ({
|
||||
type: "number" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
min: null,
|
||||
max: null,
|
||||
step: null,
|
||||
units: null,
|
||||
placeholder: null,
|
||||
disabled: false,
|
||||
immutable: a.immutable ?? false,
|
||||
...a,
|
||||
spec: {
|
||||
type: "number" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
min: null,
|
||||
max: null,
|
||||
step: null,
|
||||
units: null,
|
||||
placeholder: null,
|
||||
disabled: false,
|
||||
immutable: a.immutable ?? false,
|
||||
...a,
|
||||
},
|
||||
validator,
|
||||
}),
|
||||
asRequiredParser(number, a),
|
||||
validator,
|
||||
)
|
||||
}
|
||||
static dynamicNumber(
|
||||
static dynamicNumber<Required extends boolean>(
|
||||
getA: LazyBuild<{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: number | null
|
||||
required: boolean
|
||||
required: Required
|
||||
min?: number | null
|
||||
max?: number | null
|
||||
step?: number | null
|
||||
@@ -415,22 +443,28 @@ export class Value<Type> {
|
||||
disabled?: false | string
|
||||
}>,
|
||||
) {
|
||||
return new Value<number | null>(async (options) => {
|
||||
const a = await getA(options)
|
||||
return {
|
||||
type: "number" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
min: null,
|
||||
max: null,
|
||||
step: null,
|
||||
units: null,
|
||||
placeholder: null,
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
...a,
|
||||
}
|
||||
}, number.nullable())
|
||||
return new Value<AsRequired<number, Required>, number | null>(
|
||||
async (options) => {
|
||||
const a = await getA(options)
|
||||
return {
|
||||
spec: {
|
||||
type: "number" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
min: null,
|
||||
max: null,
|
||||
step: null,
|
||||
units: null,
|
||||
placeholder: null,
|
||||
disabled: false as const,
|
||||
immutable: false,
|
||||
...a,
|
||||
},
|
||||
validator: asRequiredParser(number, a),
|
||||
}
|
||||
},
|
||||
number.nullable(),
|
||||
)
|
||||
}
|
||||
/**
|
||||
* @description Displays a browser-native color selector.
|
||||
@@ -468,40 +502,50 @@ export class Value<Type> {
|
||||
*/
|
||||
immutable?: boolean
|
||||
}) {
|
||||
const validator = asRequiredParser(string, a)
|
||||
return new Value<AsRequired<string, Required>>(
|
||||
() => ({
|
||||
type: "color" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
disabled: false,
|
||||
immutable: a.immutable ?? false,
|
||||
...a,
|
||||
spec: {
|
||||
type: "color" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
disabled: false,
|
||||
immutable: a.immutable ?? false,
|
||||
...a,
|
||||
},
|
||||
validator,
|
||||
}),
|
||||
asRequiredParser(string, a),
|
||||
validator,
|
||||
)
|
||||
}
|
||||
|
||||
static dynamicColor(
|
||||
static dynamicColor<Required extends boolean>(
|
||||
getA: LazyBuild<{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: string | null
|
||||
required: boolean
|
||||
required: Required
|
||||
disabled?: false | string
|
||||
}>,
|
||||
) {
|
||||
return new Value<string | null>(async (options) => {
|
||||
const a = await getA(options)
|
||||
return {
|
||||
type: "color" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
...a,
|
||||
}
|
||||
}, string.nullable())
|
||||
return new Value<AsRequired<string, Required>, string | null>(
|
||||
async (options) => {
|
||||
const a = await getA(options)
|
||||
return {
|
||||
spec: {
|
||||
type: "color" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
...a,
|
||||
},
|
||||
validator: asRequiredParser(string, a),
|
||||
}
|
||||
},
|
||||
string.nullable(),
|
||||
)
|
||||
}
|
||||
/**
|
||||
* @description Displays a browser-native date/time selector.
|
||||
@@ -549,49 +593,59 @@ export class Value<Type> {
|
||||
*/
|
||||
immutable?: boolean
|
||||
}) {
|
||||
const validator = asRequiredParser(string, a)
|
||||
return new Value<AsRequired<string, Required>>(
|
||||
() => ({
|
||||
type: "datetime" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
inputmode: "datetime-local",
|
||||
min: null,
|
||||
max: null,
|
||||
step: null,
|
||||
disabled: false,
|
||||
immutable: a.immutable ?? false,
|
||||
...a,
|
||||
spec: {
|
||||
type: "datetime" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
inputmode: "datetime-local",
|
||||
min: null,
|
||||
max: null,
|
||||
step: null,
|
||||
disabled: false,
|
||||
immutable: a.immutable ?? false,
|
||||
...a,
|
||||
},
|
||||
validator,
|
||||
}),
|
||||
asRequiredParser(string, a),
|
||||
validator,
|
||||
)
|
||||
}
|
||||
static dynamicDatetime(
|
||||
static dynamicDatetime<Required extends boolean>(
|
||||
getA: LazyBuild<{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: string | null
|
||||
required: boolean
|
||||
required: Required
|
||||
inputmode?: ValueSpecDatetime["inputmode"]
|
||||
min?: string | null
|
||||
max?: string | null
|
||||
disabled?: false | string
|
||||
}>,
|
||||
) {
|
||||
return new Value<string | null>(async (options) => {
|
||||
const a = await getA(options)
|
||||
return {
|
||||
type: "datetime" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
inputmode: "datetime-local",
|
||||
min: null,
|
||||
max: null,
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
...a,
|
||||
}
|
||||
}, string.nullable())
|
||||
return new Value<AsRequired<string, Required>, string | null>(
|
||||
async (options) => {
|
||||
const a = await getA(options)
|
||||
return {
|
||||
spec: {
|
||||
type: "datetime" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
inputmode: "datetime-local",
|
||||
min: null,
|
||||
max: null,
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
...a,
|
||||
},
|
||||
validator: asRequiredParser(string, a),
|
||||
}
|
||||
},
|
||||
string.nullable(),
|
||||
)
|
||||
}
|
||||
/**
|
||||
* @description Displays a select modal with radio buttons, allowing for a single selection.
|
||||
@@ -644,39 +698,50 @@ export class Value<Type> {
|
||||
*/
|
||||
immutable?: boolean
|
||||
}) {
|
||||
const validator = anyOf(
|
||||
...Object.keys(a.values).map((x: keyof Values & string) => literal(x)),
|
||||
)
|
||||
return new Value<keyof Values & string>(
|
||||
() => ({
|
||||
description: null,
|
||||
warning: null,
|
||||
type: "select" as const,
|
||||
disabled: false,
|
||||
immutable: a.immutable ?? false,
|
||||
...a,
|
||||
spec: {
|
||||
description: null,
|
||||
warning: null,
|
||||
type: "select" as const,
|
||||
disabled: false,
|
||||
immutable: a.immutable ?? false,
|
||||
...a,
|
||||
},
|
||||
validator,
|
||||
}),
|
||||
anyOf(
|
||||
...Object.keys(a.values).map((x: keyof Values & string) => literal(x)),
|
||||
),
|
||||
validator,
|
||||
)
|
||||
}
|
||||
static dynamicSelect(
|
||||
static dynamicSelect<Values extends Record<string, string>>(
|
||||
getA: LazyBuild<{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: string
|
||||
values: Record<string, string>
|
||||
values: Values
|
||||
disabled?: false | string | string[]
|
||||
}>,
|
||||
) {
|
||||
return new Value<string>(async (options) => {
|
||||
return new Value<keyof Values & string, string>(async (options) => {
|
||||
const a = await getA(options)
|
||||
return {
|
||||
description: null,
|
||||
warning: null,
|
||||
type: "select" as const,
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
...a,
|
||||
spec: {
|
||||
description: null,
|
||||
warning: null,
|
||||
type: "select" as const,
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
...a,
|
||||
},
|
||||
validator: anyOf(
|
||||
...Object.keys(a.values).map((x: keyof Values & string) =>
|
||||
literal(x),
|
||||
),
|
||||
),
|
||||
}
|
||||
}, string)
|
||||
}
|
||||
@@ -732,45 +797,56 @@ export class Value<Type> {
|
||||
*/
|
||||
immutable?: boolean
|
||||
}) {
|
||||
return new Value<(keyof Values)[]>(
|
||||
const validator = arrayOf(
|
||||
literals(...(Object.keys(a.values) as any as [keyof Values & string])),
|
||||
)
|
||||
return new Value<(keyof Values & string)[]>(
|
||||
() => ({
|
||||
type: "multiselect" as const,
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
warning: null,
|
||||
description: null,
|
||||
disabled: false,
|
||||
immutable: a.immutable ?? false,
|
||||
...a,
|
||||
spec: {
|
||||
type: "multiselect" as const,
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
warning: null,
|
||||
description: null,
|
||||
disabled: false,
|
||||
immutable: a.immutable ?? false,
|
||||
...a,
|
||||
},
|
||||
validator,
|
||||
}),
|
||||
arrayOf(
|
||||
literals(...(Object.keys(a.values) as any as [keyof Values & string])),
|
||||
),
|
||||
validator,
|
||||
)
|
||||
}
|
||||
static dynamicMultiselect(
|
||||
static dynamicMultiselect<Values extends Record<string, string>>(
|
||||
getA: LazyBuild<{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: string[]
|
||||
values: Record<string, string>
|
||||
values: Values
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
disabled?: false | string | string[]
|
||||
}>,
|
||||
) {
|
||||
return new Value<string[]>(async (options) => {
|
||||
return new Value<(keyof Values & string)[], string[]>(async (options) => {
|
||||
const a = await getA(options)
|
||||
return {
|
||||
type: "multiselect" as const,
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
warning: null,
|
||||
description: null,
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
...a,
|
||||
spec: {
|
||||
type: "multiselect" as const,
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
warning: null,
|
||||
description: null,
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
...a,
|
||||
},
|
||||
validator: arrayOf(
|
||||
literals(
|
||||
...(Object.keys(a.values) as any as [keyof Values & string]),
|
||||
),
|
||||
),
|
||||
}
|
||||
}, arrayOf(string))
|
||||
}
|
||||
@@ -791,21 +867,27 @@ export class Value<Type> {
|
||||
),
|
||||
* ```
|
||||
*/
|
||||
static object<Type extends Record<string, any>>(
|
||||
static object<
|
||||
Type extends StaticValidatedAs,
|
||||
StaticValidatedAs extends Record<string, any>,
|
||||
>(
|
||||
a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
},
|
||||
spec: InputSpec<Type>,
|
||||
spec: InputSpec<Type, StaticValidatedAs>,
|
||||
) {
|
||||
return new Value<Type>(async (options) => {
|
||||
return new Value<Type, StaticValidatedAs>(async (options) => {
|
||||
const built = await spec.build(options as any)
|
||||
return {
|
||||
type: "object" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
...a,
|
||||
spec: built,
|
||||
spec: {
|
||||
type: "object" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
...a,
|
||||
spec: built.spec,
|
||||
},
|
||||
validator: built.validator,
|
||||
}
|
||||
}, spec.validator)
|
||||
}
|
||||
@@ -886,38 +968,42 @@ export class Value<Type> {
|
||||
spec: InputSpec<any>
|
||||
}
|
||||
},
|
||||
>(
|
||||
a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
/** Presents a warning prompt before permitting the value to change. */
|
||||
warning?: string | null
|
||||
/**
|
||||
* @description Provide a default value from the list of variants.
|
||||
* @type { string }
|
||||
* @example default: 'variant1'
|
||||
*/
|
||||
default: keyof VariantValues & string
|
||||
/**
|
||||
* @description Once set, the value can never be changed.
|
||||
* @default false
|
||||
*/
|
||||
immutable?: boolean
|
||||
},
|
||||
aVariants: Variants<VariantValues>,
|
||||
) {
|
||||
return new Value<typeof aVariants.validator._TYPE>(
|
||||
async (options) => ({
|
||||
type: "union" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
disabled: false,
|
||||
...a,
|
||||
variants: await aVariants.build(options as any),
|
||||
immutable: a.immutable ?? false,
|
||||
}),
|
||||
aVariants.validator,
|
||||
)
|
||||
>(a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
/** Presents a warning prompt before permitting the value to change. */
|
||||
warning?: string | null
|
||||
variants: Variants<VariantValues>
|
||||
/**
|
||||
* @description Provide a default value from the list of variants.
|
||||
* @type { string }
|
||||
* @example default: 'variant1'
|
||||
*/
|
||||
default: keyof VariantValues & string
|
||||
/**
|
||||
* @description Once set, the value can never be changed.
|
||||
* @default false
|
||||
*/
|
||||
immutable?: boolean
|
||||
}) {
|
||||
return new Value<
|
||||
typeof a.variants._TYPE,
|
||||
typeof a.variants.validator._TYPE
|
||||
>(async (options) => {
|
||||
const built = await a.variants.build(options as any)
|
||||
return {
|
||||
spec: {
|
||||
type: "union" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
disabled: false,
|
||||
...a,
|
||||
variants: built.spec,
|
||||
immutable: a.immutable ?? false,
|
||||
},
|
||||
validator: built.validator,
|
||||
}
|
||||
}, a.variants.validator)
|
||||
}
|
||||
static dynamicUnion<
|
||||
VariantValues extends {
|
||||
@@ -931,22 +1017,69 @@ export class Value<Type> {
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
variants: Variants<VariantValues>
|
||||
default: keyof VariantValues & string
|
||||
disabled: string[] | false | string
|
||||
}>,
|
||||
aVariants: Variants<VariantValues>,
|
||||
) {
|
||||
return new Value<typeof aVariants.validator._TYPE>(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,
|
||||
): Value<UnionRes<VariantValues>, unknown>
|
||||
static dynamicUnion<
|
||||
VariantValues extends StaticVariantValues,
|
||||
StaticVariantValues extends {
|
||||
[K in string]: {
|
||||
name: string
|
||||
spec: InputSpec<any, any>
|
||||
}
|
||||
}, aVariants.validator)
|
||||
},
|
||||
>(
|
||||
getA: LazyBuild<{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
variants: Variants<VariantValues>
|
||||
default: keyof VariantValues & string
|
||||
disabled: string[] | false | string
|
||||
}>,
|
||||
validator: Parser<unknown, UnionResStaticValidatedAs<StaticVariantValues>>,
|
||||
): Value<
|
||||
UnionRes<VariantValues>,
|
||||
UnionResStaticValidatedAs<StaticVariantValues>
|
||||
>
|
||||
static dynamicUnion<
|
||||
VariantValues extends {
|
||||
[K in string]: {
|
||||
name: string
|
||||
spec: InputSpec<any>
|
||||
}
|
||||
},
|
||||
>(
|
||||
getA: LazyBuild<{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
variants: Variants<VariantValues>
|
||||
default: keyof VariantValues & string
|
||||
disabled: string[] | false | string
|
||||
}>,
|
||||
validator: Parser<unknown, unknown> = any,
|
||||
) {
|
||||
return new Value<UnionRes<VariantValues>, typeof validator._TYPE>(
|
||||
async (options) => {
|
||||
const newValues = await getA(options)
|
||||
const built = await newValues.variants.build(options as any)
|
||||
return {
|
||||
spec: {
|
||||
type: "union" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
...newValues,
|
||||
variants: built.spec,
|
||||
immutable: false,
|
||||
},
|
||||
validator: built.validator,
|
||||
}
|
||||
},
|
||||
validator,
|
||||
)
|
||||
}
|
||||
/**
|
||||
* @description Presents an interface to add/remove/edit items in a list.
|
||||
@@ -1022,16 +1155,45 @@ export class Value<Type> {
|
||||
hiddenExample: Value.hidden(),
|
||||
* ```
|
||||
*/
|
||||
static hidden<T>(): Value<T, unknown>
|
||||
static hidden<T>(parser: Parser<unknown, T>): Value<T>
|
||||
static hidden<T>(parser: Parser<unknown, T> = any) {
|
||||
return new Value<T>(async () => {
|
||||
const built: ValueSpecHidden = {
|
||||
type: "hidden" as const,
|
||||
return new Value<T, typeof parser._TYPE>(async () => {
|
||||
return {
|
||||
spec: {
|
||||
type: "hidden" as const,
|
||||
} as ValueSpecHidden,
|
||||
validator: parser,
|
||||
}
|
||||
return built
|
||||
}, parser)
|
||||
}
|
||||
|
||||
map<U>(fn: (value: Type) => U): Value<U> {
|
||||
return new Value(this.build, this.validator.map(fn))
|
||||
/**
|
||||
* @description Provides a way to define a hidden field with a static value. Useful for tracking
|
||||
* @example
|
||||
* ```
|
||||
hiddenExample: Value.hidden(),
|
||||
* ```
|
||||
*/
|
||||
static dynamicHidden<T>(getParser: LazyBuild<Parser<unknown, T>>) {
|
||||
return new Value<T, unknown>(async (options) => {
|
||||
const validator = await getParser(options)
|
||||
return {
|
||||
spec: {
|
||||
type: "hidden" as const,
|
||||
} as ValueSpecHidden,
|
||||
validator,
|
||||
}
|
||||
}, any)
|
||||
}
|
||||
|
||||
map<U>(fn: (value: StaticValidatedAs) => U): Value<U> {
|
||||
return new Value(async (effects) => {
|
||||
const built = await this.build(effects)
|
||||
return {
|
||||
spec: built.spec,
|
||||
validator: built.validator.map(fn),
|
||||
}
|
||||
}, this.validator.map(fn))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import {
|
||||
LazyBuild,
|
||||
InputSpec,
|
||||
ExtractInputSpecType,
|
||||
ExtractPartialInputSpecType,
|
||||
ExtractInputSpecStaticValidatedAs,
|
||||
} from "./inputSpec"
|
||||
import { Parser, anyOf, literal, object } from "ts-matches"
|
||||
import { Parser, any, anyOf, literal, object } from "ts-matches"
|
||||
|
||||
export type UnionRes<
|
||||
VariantValues extends {
|
||||
@@ -28,6 +28,26 @@ export type UnionRes<
|
||||
}
|
||||
}[K]
|
||||
|
||||
export type UnionResStaticValidatedAs<
|
||||
VariantValues extends {
|
||||
[K in string]: {
|
||||
name: string
|
||||
spec: InputSpec<any>
|
||||
}
|
||||
},
|
||||
K extends keyof VariantValues & string = keyof VariantValues & string,
|
||||
> = {
|
||||
[key in keyof VariantValues]: {
|
||||
selection: key
|
||||
value: ExtractInputSpecStaticValidatedAs<VariantValues[key]["spec"]>
|
||||
other?: {
|
||||
[key2 in Exclude<keyof VariantValues & string, key>]?: DeepPartial<
|
||||
ExtractInputSpecStaticValidatedAs<VariantValues[key2]["spec"]>
|
||||
>
|
||||
}
|
||||
}
|
||||
}[K]
|
||||
|
||||
/**
|
||||
* Used in the the Value.select { @link './value.ts' }
|
||||
* to indicate the type of select variants that are available. The key for the record passed in will be the
|
||||
@@ -80,14 +100,21 @@ export class Variants<
|
||||
VariantValues extends {
|
||||
[K in string]: {
|
||||
name: string
|
||||
spec: InputSpec<any>
|
||||
spec: InputSpec<any, any>
|
||||
}
|
||||
},
|
||||
> {
|
||||
private constructor(
|
||||
public build: LazyBuild<ValueSpecUnion["variants"]>,
|
||||
public validator: Parser<unknown, UnionRes<VariantValues>>,
|
||||
public build: LazyBuild<{
|
||||
spec: ValueSpecUnion["variants"]
|
||||
validator: Parser<unknown, UnionRes<VariantValues>>
|
||||
}>,
|
||||
public readonly validator: Parser<
|
||||
unknown,
|
||||
UnionResStaticValidatedAs<VariantValues>
|
||||
>,
|
||||
) {}
|
||||
readonly _TYPE: UnionRes<VariantValues> = null as any
|
||||
static of<
|
||||
VariantValues extends {
|
||||
[K in string]: {
|
||||
@@ -96,30 +123,71 @@ export class Variants<
|
||||
}
|
||||
},
|
||||
>(a: VariantValues) {
|
||||
const validator = anyOf(
|
||||
...Object.entries(a).map(([id, { spec }]) =>
|
||||
object({
|
||||
selection: literal(id),
|
||||
value: spec.validator,
|
||||
}),
|
||||
const staticValidators = {} as {
|
||||
[K in keyof VariantValues]: Parser<
|
||||
unknown,
|
||||
ExtractInputSpecStaticValidatedAs<VariantValues[K]["spec"]>
|
||||
>
|
||||
}
|
||||
for (const key in a) {
|
||||
const value = a[key]
|
||||
staticValidators[key] = value.spec.validator
|
||||
}
|
||||
const other = object(
|
||||
Object.fromEntries(
|
||||
Object.entries(staticValidators).map(([k, v]) => [k, any.optional()]),
|
||||
),
|
||||
) as Parser<unknown, any>
|
||||
|
||||
return new Variants<VariantValues>(async (options) => {
|
||||
const variants = {} as {
|
||||
[K in keyof VariantValues]: {
|
||||
name: string
|
||||
spec: Record<string, ValueSpec>
|
||||
).optional()
|
||||
return new Variants<VariantValues>(
|
||||
async (options) => {
|
||||
const validators = {} as {
|
||||
[K in keyof VariantValues]: Parser<
|
||||
unknown,
|
||||
ExtractInputSpecType<VariantValues[K]["spec"]>
|
||||
>
|
||||
}
|
||||
}
|
||||
for (const key in a) {
|
||||
const value = a[key]
|
||||
variants[key] = {
|
||||
name: value.name,
|
||||
spec: await value.spec.build(options as any),
|
||||
const variants = {} as {
|
||||
[K in keyof VariantValues]: {
|
||||
name: string
|
||||
spec: Record<string, ValueSpec>
|
||||
}
|
||||
}
|
||||
}
|
||||
return variants
|
||||
}, validator)
|
||||
for (const key in a) {
|
||||
const value = a[key]
|
||||
const built = await value.spec.build(options as any)
|
||||
variants[key] = {
|
||||
name: value.name,
|
||||
spec: built.spec,
|
||||
}
|
||||
validators[key] = built.validator
|
||||
}
|
||||
const other = object(
|
||||
Object.fromEntries(
|
||||
Object.entries(validators).map(([k, v]) => [k, any.optional()]),
|
||||
),
|
||||
).optional()
|
||||
return {
|
||||
spec: variants,
|
||||
validator: anyOf(
|
||||
...Object.entries(validators).map(([k, v]) =>
|
||||
object({
|
||||
selection: literal(k),
|
||||
value: v,
|
||||
other,
|
||||
}),
|
||||
),
|
||||
) as any,
|
||||
}
|
||||
},
|
||||
anyOf(
|
||||
...Object.entries(staticValidators).map(([k, v]) =>
|
||||
object({
|
||||
selection: literal(k),
|
||||
value: v,
|
||||
other,
|
||||
}),
|
||||
),
|
||||
) as any,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ import { Variants } from "./builder/variants"
|
||||
/**
|
||||
* Base SMTP settings, to be used by StartOS for system wide SMTP
|
||||
*/
|
||||
export const customSmtp = InputSpec.of<InputSpecOf<SmtpValue>>({
|
||||
export const customSmtp: InputSpec<SmtpValue> = InputSpec.of<
|
||||
InputSpecOf<SmtpValue>
|
||||
>({
|
||||
server: Value.text({
|
||||
name: "SMTP Server",
|
||||
required: true,
|
||||
@@ -42,40 +44,39 @@ export const customSmtp = InputSpec.of<InputSpecOf<SmtpValue>>({
|
||||
}),
|
||||
})
|
||||
|
||||
const smtpVariants = Variants.of({
|
||||
disabled: { name: "Disabled", spec: InputSpec.of({}) },
|
||||
system: {
|
||||
name: "System Credentials",
|
||||
spec: InputSpec.of({
|
||||
customFrom: Value.text({
|
||||
name: "Custom From Address",
|
||||
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],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
custom: {
|
||||
name: "Custom Credentials",
|
||||
spec: customSmtp,
|
||||
},
|
||||
})
|
||||
/**
|
||||
* For service inputSpec. Gives users 3 options for SMTP: (1) disabled, (2) use system SMTP settings, (3) use custom SMTP settings
|
||||
*/
|
||||
export const smtpInputSpec = Value.dynamicUnion(
|
||||
async ({ effects }) => {
|
||||
const smtp = await new GetSystemSmtp(effects).once()
|
||||
const disabled = smtp ? [] : ["system"]
|
||||
return {
|
||||
name: "SMTP",
|
||||
description: "Optionally provide an SMTP server for sending emails",
|
||||
default: "disabled",
|
||||
disabled,
|
||||
}
|
||||
},
|
||||
Variants.of({
|
||||
disabled: { name: "Disabled", spec: InputSpec.of({}) },
|
||||
system: {
|
||||
name: "System Credentials",
|
||||
spec: InputSpec.of({
|
||||
customFrom: Value.text({
|
||||
name: "Custom From Address",
|
||||
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],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
custom: {
|
||||
name: "Custom Credentials",
|
||||
spec: customSmtp,
|
||||
},
|
||||
}),
|
||||
)
|
||||
export const smtpInputSpec = Value.dynamicUnion(async ({ effects }) => {
|
||||
const smtp = await new GetSystemSmtp(effects).once()
|
||||
const disabled = smtp ? [] : ["system"]
|
||||
return {
|
||||
name: "SMTP",
|
||||
description: "Optionally provide an SMTP server for sending emails",
|
||||
default: "disabled",
|
||||
disabled,
|
||||
variants: smtpVariants,
|
||||
}
|
||||
}, smtpVariants.validator)
|
||||
|
||||
@@ -1,23 +1,17 @@
|
||||
import { InputSpec } from "./input/builder"
|
||||
import {
|
||||
ExtractInputSpecType,
|
||||
ExtractPartialInputSpecType,
|
||||
} from "./input/builder/inputSpec"
|
||||
import { ExtractInputSpecType } from "./input/builder/inputSpec"
|
||||
import * as T from "../types"
|
||||
import { once } from "../util"
|
||||
import { InitScript } from "../inits"
|
||||
import { Parser } from "ts-matches"
|
||||
|
||||
export type Run<
|
||||
A extends Record<string, any> | InputSpec<Record<string, any>>,
|
||||
> = (options: {
|
||||
export type Run<A extends Record<string, any>> = (options: {
|
||||
effects: T.Effects
|
||||
input: ExtractInputSpecType<A>
|
||||
input: A
|
||||
}) => Promise<(T.ActionResult & { version: "1" }) | null | void | undefined>
|
||||
export type GetInput<
|
||||
A extends Record<string, any> | InputSpec<Record<string, any>>,
|
||||
> = (options: {
|
||||
export type GetInput<A extends Record<string, any>> = (options: {
|
||||
effects: T.Effects
|
||||
}) => Promise<null | void | undefined | ExtractPartialInputSpecType<A>>
|
||||
}) => Promise<null | void | undefined | T.DeepPartial<A>>
|
||||
|
||||
export type MaybeFn<T> = T | ((options: { effects: T.Effects }) => Promise<T>)
|
||||
function callMaybeFn<T>(
|
||||
@@ -43,39 +37,38 @@ function mapMaybeFn<T, U>(
|
||||
|
||||
export interface ActionInfo<
|
||||
Id extends T.ActionId,
|
||||
InputSpecType extends Record<string, any> | InputSpec<any>,
|
||||
Type extends Record<string, any>,
|
||||
> {
|
||||
readonly id: Id
|
||||
readonly _INPUT: InputSpecType
|
||||
readonly _INPUT: Type
|
||||
}
|
||||
|
||||
export class Action<
|
||||
Id extends T.ActionId,
|
||||
InputSpecType extends Record<string, any> | InputSpec<any>,
|
||||
> implements ActionInfo<Id, InputSpecType>
|
||||
export class Action<Id extends T.ActionId, Type extends Record<string, any>>
|
||||
implements ActionInfo<Id, Type>
|
||||
{
|
||||
readonly _INPUT: InputSpecType = null as any as InputSpecType
|
||||
readonly _INPUT: Type = null as any as Type
|
||||
private cachedParser?: Parser<unknown, Type>
|
||||
private constructor(
|
||||
readonly id: Id,
|
||||
private readonly metadataFn: MaybeFn<T.ActionMetadata>,
|
||||
private readonly inputSpec: InputSpecType,
|
||||
private readonly getInputFn: GetInput<InputSpecType>,
|
||||
private readonly runFn: Run<InputSpecType>,
|
||||
private readonly inputSpec: InputSpec<Type>,
|
||||
private readonly getInputFn: GetInput<Type>,
|
||||
private readonly runFn: Run<Type>,
|
||||
) {}
|
||||
static withInput<
|
||||
Id extends T.ActionId,
|
||||
InputSpecType extends Record<string, any> | InputSpec<any>,
|
||||
InputSpecType extends InputSpec<Record<string, any>>,
|
||||
>(
|
||||
id: Id,
|
||||
metadata: MaybeFn<Omit<T.ActionMetadata, "hasInput">>,
|
||||
inputSpec: InputSpecType,
|
||||
getInput: GetInput<InputSpecType>,
|
||||
run: Run<InputSpecType>,
|
||||
): Action<Id, InputSpecType> {
|
||||
return new Action(
|
||||
getInput: GetInput<ExtractInputSpecType<InputSpecType>>,
|
||||
run: Run<ExtractInputSpecType<InputSpecType>>,
|
||||
): Action<Id, ExtractInputSpecType<InputSpecType>> {
|
||||
return new Action<Id, ExtractInputSpecType<InputSpecType>>(
|
||||
id,
|
||||
mapMaybeFn(metadata, (m) => ({ ...m, hasInput: true })),
|
||||
inputSpec,
|
||||
inputSpec as any,
|
||||
getInput,
|
||||
run,
|
||||
)
|
||||
@@ -88,7 +81,7 @@ export class Action<
|
||||
return new Action(
|
||||
id,
|
||||
mapMaybeFn(metadata, (m) => ({ ...m, hasInput: false })),
|
||||
{},
|
||||
InputSpec.of({}),
|
||||
async () => null,
|
||||
run,
|
||||
)
|
||||
@@ -107,16 +100,27 @@ export class Action<
|
||||
return metadata
|
||||
}
|
||||
async getInput(options: { effects: T.Effects }): Promise<T.ActionInput> {
|
||||
const built = await this.inputSpec.build(options)
|
||||
this.cachedParser = built.validator
|
||||
return {
|
||||
spec: await this.inputSpec.build(options),
|
||||
spec: built.spec,
|
||||
value: (await this.getInputFn(options)) || null,
|
||||
}
|
||||
}
|
||||
async run(options: {
|
||||
effects: T.Effects
|
||||
input: ExtractInputSpecType<InputSpecType>
|
||||
input: Type
|
||||
}): Promise<T.ActionResult | null> {
|
||||
return (await this.runFn(options)) || null
|
||||
const parser =
|
||||
this.cachedParser ?? (await this.inputSpec.build(options)).validator
|
||||
return (
|
||||
(await this.runFn({
|
||||
effects: options.effects,
|
||||
input: this.cachedParser
|
||||
? this.cachedParser.unsafeCast(options.input)
|
||||
: options.input,
|
||||
})) || null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,10 +15,10 @@ describe("InputSpec Types", () => {
|
||||
{ spec: InputSpec.of({}) } as any,
|
||||
) as any
|
||||
const someList = await Value.list(test).build({} as any)
|
||||
if (isValueSpecListOf(someList, "text")) {
|
||||
someList.spec satisfies ListValueSpecOf<"text">
|
||||
} else if (isValueSpecListOf(someList, "object")) {
|
||||
someList.spec satisfies ListValueSpecOf<"object">
|
||||
if (isValueSpecListOf(someList.spec, "text")) {
|
||||
someList.spec.spec satisfies ListValueSpecOf<"text">
|
||||
} else if (isValueSpecListOf(someList.spec, "object")) {
|
||||
someList.spec.spec satisfies ListValueSpecOf<"object">
|
||||
} else {
|
||||
throw new Error(
|
||||
"Failed to figure out the type: " + JSON.stringify(someList),
|
||||
|
||||
Reference in New Issue
Block a user