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:
Aiden McClelland
2024-11-19 11:25:43 -07:00
committed by GitHub
parent 46179f5c83
commit 1771797453
24 changed files with 550 additions and 512 deletions

View File

@@ -264,7 +264,6 @@ exports[`transformConfigSpec transformConfigSpec(bitcoind) 1`] = `
"disabled": false, "disabled": false,
"immutable": false, "immutable": false,
"name": "Pruning Mode", "name": "Pruning Mode",
"required": true,
"type": "union", "type": "union",
"variants": { "variants": {
"automatic": { "automatic": {
@@ -524,7 +523,6 @@ exports[`transformConfigSpec transformConfigSpec(embassyPages) 1`] = `
"disabled": false, "disabled": false,
"immutable": false, "immutable": false,
"name": "Type", "name": "Type",
"required": true,
"type": "union", "type": "union",
"variants": { "variants": {
"index": { "index": {
@@ -589,7 +587,6 @@ exports[`transformConfigSpec transformConfigSpec(embassyPages) 1`] = `
"disabled": false, "disabled": false,
"immutable": false, "immutable": false,
"name": "Folder Location", "name": "Folder Location",
"required": false,
"type": "select", "type": "select",
"values": { "values": {
"filebrowser": "filebrowser", "filebrowser": "filebrowser",
@@ -644,7 +641,6 @@ exports[`transformConfigSpec transformConfigSpec(embassyPages) 1`] = `
"disabled": false, "disabled": false,
"immutable": false, "immutable": false,
"name": "Type", "name": "Type",
"required": true,
"type": "union", "type": "union",
"variants": { "variants": {
"redirect": { "redirect": {
@@ -705,7 +701,6 @@ exports[`transformConfigSpec transformConfigSpec(embassyPages) 1`] = `
"disabled": false, "disabled": false,
"immutable": false, "immutable": false,
"name": "Folder Location", "name": "Folder Location",
"required": false,
"type": "select", "type": "select",
"values": { "values": {
"filebrowser": "filebrowser", "filebrowser": "filebrowser",
@@ -758,7 +753,6 @@ exports[`transformConfigSpec transformConfigSpec(nostr2) 1`] = `
"disabled": false, "disabled": false,
"immutable": false, "immutable": false,
"name": "Relay Type", "name": "Relay Type",
"required": true,
"type": "union", "type": "union",
"variants": { "variants": {
"private": { "private": {

View File

@@ -43,7 +43,6 @@ export function transformConfigSpec(oldSpec: OldConfigSpec): IST.InputSpec {
}), }),
{}, {},
), ),
required: false,
disabled: false, disabled: false,
immutable: false, immutable: false,
} }
@@ -127,7 +126,6 @@ export function transformConfigSpec(oldSpec: OldConfigSpec): IST.InputSpec {
{} as Record<string, { name: string; spec: IST.InputSpec }>, {} as Record<string, { name: string; spec: IST.InputSpec }>,
), ),
disabled: false, disabled: false,
required: true,
default: oldVal.default, default: oldVal.default,
immutable: false, immutable: false,
} }

View File

@@ -1,6 +1,7 @@
import * as T from "../types" import * as T from "../types"
import * as IST from "../actions/input/inputSpecTypes" import * as IST from "../actions/input/inputSpecTypes"
import { Action } from "./setupActions" import { Action } from "./setupActions"
import { ExtractInputSpecType } from "./input/builder/inputSpec"
export type RunActionInput<Input> = export type RunActionInput<Input> =
| Input | Input
@@ -44,36 +45,32 @@ export const runAction = async <
}) })
} }
} }
type GetActionInputType< type GetActionInputType<A extends Action<T.ActionId, any, any>> =
A extends Action<T.ActionId, any, any, Record<string, unknown>>, A extends Action<T.ActionId, any, infer I> ? ExtractInputSpecType<I> : never
> = A extends Action<T.ActionId, any, any, infer I> ? I : never
type ActionRequestBase = { type ActionRequestBase = {
reason?: string reason?: string
replayId?: string replayId?: string
} }
type ActionRequestInput< type ActionRequestInput<T extends Action<T.ActionId, any, any>> = {
T extends Action<T.ActionId, any, any, Record<string, unknown>>,
> = {
kind: "partial" kind: "partial"
value: Partial<GetActionInputType<T>> value: Partial<GetActionInputType<T>>
} }
export type ActionRequestOptions< export type ActionRequestOptions<T extends Action<T.ActionId, any, any>> =
T extends Action<T.ActionId, any, any, Record<string, unknown>>, ActionRequestBase &
> = ActionRequestBase & (
( | {
| { when?: Exclude<
when?: Exclude< T.ActionRequestTrigger,
T.ActionRequestTrigger, { condition: "input-not-matches" }
{ condition: "input-not-matches" } >
> input?: ActionRequestInput<T>
input?: ActionRequestInput<T> }
} | {
| { when: T.ActionRequestTrigger & { condition: "input-not-matches" }
when: T.ActionRequestTrigger & { condition: "input-not-matches" } input: ActionRequestInput<T>
input: ActionRequestInput<T> }
} )
)
const _validate: T.ActionRequest = {} as ActionRequestOptions<any> & { const _validate: T.ActionRequest = {} as ActionRequestOptions<any> & {
actionId: string actionId: string
@@ -81,9 +78,7 @@ const _validate: T.ActionRequest = {} as ActionRequestOptions<any> & {
severity: T.ActionSeverity severity: T.ActionSeverity
} }
export const requestAction = < export const requestAction = <T extends Action<T.ActionId, any, any>>(options: {
T extends Action<T.ActionId, any, any, Record<string, unknown>>,
>(options: {
effects: T.Effects effects: T.Effects
packageId: T.PackageId packageId: T.PackageId
action: T action: T

View File

@@ -1,5 +1,5 @@
import { ValueSpec } from "../inputSpecTypes" import { ValueSpec } from "../inputSpecTypes"
import { Value } from "./value" import { PartialValue, Value } from "./value"
import { _ } from "../../../util" import { _ } from "../../../util"
import { Effects } from "../../../Effects" import { Effects } from "../../../Effects"
import { Parser, object } from "ts-matches" 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 extends InputSpec<infer B, any> | InputSpec<infer B, never> ? B :
A 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> = { export type InputSpecOf<A extends Record<string, any>, Store = never> = {
[K in keyof A]: Value<A[K], Store> [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>, 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>) { async build(options: LazyBuildOptions<Store>) {
const answer = {} as { const answer = {} as {
[K in keyof Type]: ValueSpec [K in keyof Type]: ValueSpec

View File

@@ -1,6 +1,6 @@
import { InputSpec, LazyBuild } from "./inputSpec" import { InputSpec, LazyBuild } from "./inputSpec"
import { List } from "./list" import { List } from "./list"
import { Variants } from "./variants" import { PartialUnionRes, UnionRes, Variants } from "./variants"
import { import {
FilePath, FilePath,
Pattern, Pattern,
@@ -26,37 +26,14 @@ import {
string, string,
unknown, unknown,
} from "ts-matches" } from "ts-matches"
import { DeepPartial } from "../../../types"
export type RequiredDefault<A> = type AsRequired<T, Required extends boolean> = Required extends true
| false ? T
| { : T | null | undefined
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
const testForAsRequiredParser = once( const testForAsRequiredParser = once(
() => object({ required: object({ default: unknown }) }).test, () => object({ required: literal(true) }).test,
) )
function asRequiredParser< function asRequiredParser<
Type, Type,
@@ -69,6 +46,13 @@ function asRequiredParser<
return parser.optional() as any 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> { export class Value<Type, Store> {
protected constructor( protected constructor(
public build: LazyBuild<Store, ValueSpec>, public build: LazyBuild<Store, ValueSpec>,
@@ -122,19 +106,19 @@ export class Value<Type, Store> {
boolean, boolean,
) )
} }
static text<Required extends RequiredDefault<DefaultString>>(a: { static text<Required extends boolean>(a: {
name: string name: string
description?: string | null description?: string | null
/** Presents a warning prompt before permitting the value to change. */ /** Presents a warning prompt before permitting the value to change. */
warning?: string | null warning?: string | null
/** /**
* @description Determines if the field is required. If so, optionally provide a default value. * provide a default value.
* @type { false | { default: string | RandomString | null } } * @type { string | RandomString | null }
* @example required: false * @example default: null
* @example required: { default: null } * @example default: 'World'
* @example required: { default: 'World' } * @example default: { charset: 'abcdefg', len: 16 }
* @example required: { default: { charset: 'abcdefg', len: 16 } }
*/ */
default: string | RandomString | null
required: Required required: Required
/** /**
* @description Mask (aka camouflage) text input with dots: ● ● ● * @description Mask (aka camouflage) text input with dots: ● ● ●
@@ -188,7 +172,6 @@ export class Value<Type, Store> {
immutable: a.immutable ?? false, immutable: a.immutable ?? false,
generate: a.generate ?? null, generate: a.generate ?? null,
...a, ...a,
...requiredLikeToAbove(a.required),
}), }),
asRequiredParser(string, a), asRequiredParser(string, a),
) )
@@ -200,7 +183,8 @@ export class Value<Type, Store> {
name: string name: string
description?: string | null description?: string | null
warning?: string | null warning?: string | null
required: RequiredDefault<DefaultString> default: DefaultString | null
required: boolean
masked?: boolean masked?: boolean
placeholder?: string | null placeholder?: string | null
minLength?: number | null minLength?: number | null
@@ -228,19 +212,16 @@ export class Value<Type, Store> {
immutable: false, immutable: false,
generate: a.generate ?? null, generate: a.generate ?? null,
...a, ...a,
...requiredLikeToAbove(a.required),
} }
}, string.optional()) }, string.optional())
} }
static textarea(a: { static textarea<Required extends boolean>(a: {
name: string name: string
description?: string | null description?: string | null
/** Presents a warning prompt before permitting the value to change. */ /** Presents a warning prompt before permitting the value to change. */
warning?: string | null warning?: string | null
/** default: string | null
* @description Unlike other "required" fields, for textarea this is a simple boolean. required: Required
*/
required: boolean
minLength?: number | null minLength?: number | null
maxLength?: number | null maxLength?: number | null
placeholder?: string | null placeholder?: string | null
@@ -250,20 +231,23 @@ export class Value<Type, Store> {
*/ */
immutable?: boolean immutable?: boolean
}) { }) {
return new Value<string, never>(async () => { return new Value<AsRequired<string, Required>, never>(
const built: ValueSpecTextarea = { async () => {
description: null, const built: ValueSpecTextarea = {
warning: null, description: null,
minLength: null, warning: null,
maxLength: null, minLength: null,
placeholder: null, maxLength: null,
type: "textarea" as const, placeholder: null,
disabled: false, type: "textarea" as const,
immutable: a.immutable ?? false, disabled: false,
...a, immutable: a.immutable ?? false,
} ...a,
return built }
}, string) return built
},
asRequiredParser(string, a),
)
} }
static dynamicTextarea<Store = never>( static dynamicTextarea<Store = never>(
getA: LazyBuild< getA: LazyBuild<
@@ -272,6 +256,7 @@ export class Value<Type, Store> {
name: string name: string
description?: string | null description?: string | null
warning?: string | null warning?: string | null
default: string | null
required: boolean required: boolean
minLength?: number | null minLength?: number | null
maxLength?: 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) const a = await getA(options)
return { return {
description: null, description: null,
@@ -293,20 +278,20 @@ export class Value<Type, Store> {
immutable: false, immutable: false,
...a, ...a,
} }
}, string) }, string.optional())
} }
static number<Required extends RequiredDefault<number>>(a: { static number<Required extends boolean>(a: {
name: string name: string
description?: string | null description?: string | null
/** Presents a warning prompt before permitting the value to change. */ /** Presents a warning prompt before permitting the value to change. */
warning?: string | null warning?: string | null
/** /**
* @description Determines if the field is required. If so, optionally provide a default value. * @description optionally provide a default value.
* @type { false | { default: number | null } } * @type { default: number | null }
* @example required: false * @example default: null
* @example required: { default: null } * @example default: 7
* @example required: { default: 7 }
*/ */
default: number | null
required: Required required: Required
min?: number | null min?: number | null
max?: number | null max?: number | null
@@ -343,7 +328,6 @@ export class Value<Type, Store> {
disabled: false, disabled: false,
immutable: a.immutable ?? false, immutable: a.immutable ?? false,
...a, ...a,
...requiredLikeToAbove(a.required),
}), }),
asRequiredParser(number, a), asRequiredParser(number, a),
) )
@@ -355,7 +339,8 @@ export class Value<Type, Store> {
name: string name: string
description?: string | null description?: string | null
warning?: string | null warning?: string | null
required: RequiredDefault<number> default: number | null
required: boolean
min?: number | null min?: number | null
max?: number | null max?: number | null
step?: number | null step?: number | null
@@ -380,22 +365,21 @@ export class Value<Type, Store> {
disabled: false, disabled: false,
immutable: false, immutable: false,
...a, ...a,
...requiredLikeToAbove(a.required),
} }
}, number.optional()) }, number.optional())
} }
static color<Required extends RequiredDefault<string>>(a: { static color<Required extends boolean>(a: {
name: string name: string
description?: string | null description?: string | null
/** Presents a warning prompt before permitting the value to change. */ /** Presents a warning prompt before permitting the value to change. */
warning?: string | null warning?: string | null
/** /**
* @description Determines if the field is required. If so, optionally provide a default value. * @description optionally provide a default value.
* @type { false | { default: string | null } } * @type { default: string | null }
* @example required: false * @example default: null
* @example required: { default: null } * @example default: 'ffffff'
* @example required: { default: 'ffffff' }
*/ */
default: string | null
required: Required required: Required
/** /**
* @description Once set, the value can never be changed. * @description Once set, the value can never be changed.
@@ -411,9 +395,7 @@ export class Value<Type, Store> {
disabled: false, disabled: false,
immutable: a.immutable ?? false, immutable: a.immutable ?? false,
...a, ...a,
...requiredLikeToAbove(a.required),
}), }),
asRequiredParser(string, a), asRequiredParser(string, a),
) )
} }
@@ -425,7 +407,8 @@ export class Value<Type, Store> {
name: string name: string
description?: string | null description?: string | null
warning?: string | null warning?: string | null
required: RequiredDefault<string> default: string | null
required: boolean
disabled?: false | string disabled?: false | string
} }
>, >,
@@ -439,22 +422,21 @@ export class Value<Type, Store> {
disabled: false, disabled: false,
immutable: false, immutable: false,
...a, ...a,
...requiredLikeToAbove(a.required),
} }
}, string.optional()) }, string.optional())
} }
static datetime<Required extends RequiredDefault<string>>(a: { static datetime<Required extends boolean>(a: {
name: string name: string
description?: string | null description?: string | null
/** Presents a warning prompt before permitting the value to change. */ /** Presents a warning prompt before permitting the value to change. */
warning?: string | null warning?: string | null
/** /**
* @description Determines if the field is required. If so, optionally provide a default value. * @description optionally provide a default value.
* @type { false | { default: string | null } } * @type { default: string | null }
* @example required: false * @example default: null
* @example required: { default: null } * @example default: '1985-12-16 18:00:00.000'
* @example required: { default: '1985-12-16 18:00:00.000' }
*/ */
default: string | null
required: Required required: Required
/** /**
* @description Informs the browser how to behave and which date/time component to display. * @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, disabled: false,
immutable: a.immutable ?? false, immutable: a.immutable ?? false,
...a, ...a,
...requiredLikeToAbove(a.required),
}), }),
asRequiredParser(string, a), asRequiredParser(string, a),
) )
@@ -493,7 +474,8 @@ export class Value<Type, Store> {
name: string name: string
description?: string | null description?: string | null
warning?: string | null warning?: string | null
required: RequiredDefault<string> default: string | null
required: boolean
inputmode?: ValueSpecDatetime["inputmode"] inputmode?: ValueSpecDatetime["inputmode"]
min?: string | null min?: string | null
max?: string | null max?: string | null
@@ -513,26 +495,21 @@ export class Value<Type, Store> {
disabled: false, disabled: false,
immutable: false, immutable: false,
...a, ...a,
...requiredLikeToAbove(a.required),
} }
}, string.optional()) }, string.optional())
} }
static select< static select<Values extends Record<string, string>>(a: {
Required extends RequiredDefault<string>,
Values extends Record<string, string>,
>(a: {
name: string name: string
description?: string | null description?: string | null
/** Presents a warning prompt before permitting the value to change. */ /** Presents a warning prompt before permitting the value to change. */
warning?: string | null warning?: string | null
/** /**
* @description Determines if the field is required. If so, optionally provide a default value from the list of values. * @description Determines if the field is required. If so, optionally provide a default value from the list of values.
* @type { false | { default: string | null } } * @type { (keyof Values & string) | null }
* @example required: false * @example default: null
* @example required: { default: null } * @example default: 'radio1'
* @example required: { default: 'radio1' }
*/ */
required: Required default: keyof Values & string
/** /**
* @description A mapping of unique radio options to their human readable display format. * @description A mapping of unique radio options to their human readable display format.
* @example * @example
@@ -551,7 +528,7 @@ export class Value<Type, Store> {
*/ */
immutable?: boolean immutable?: boolean
}) { }) {
return new Value<AsRequired<keyof Values, Required>, never>( return new Value<keyof Values & string, never>(
() => ({ () => ({
description: null, description: null,
warning: null, warning: null,
@@ -559,16 +536,10 @@ export class Value<Type, Store> {
disabled: false, disabled: false,
immutable: a.immutable ?? false, immutable: a.immutable ?? false,
...a, ...a,
...requiredLikeToAbove(a.required),
}), }),
asRequiredParser( anyOf(
anyOf( ...Object.keys(a.values).map((x: keyof Values & string) => literal(x)),
...Object.keys(a.values).map((x: keyof Values & string) => ),
literal(x),
),
),
a,
) as any,
) )
} }
static dynamicSelect<Store = never>( static dynamicSelect<Store = never>(
@@ -578,13 +549,13 @@ export class Value<Type, Store> {
name: string name: string
description?: string | null description?: string | null
warning?: string | null warning?: string | null
required: RequiredDefault<string> default: string
values: Record<string, string> values: Record<string, string>
disabled?: false | 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) const a = await getA(options)
return { return {
description: null, description: null,
@@ -593,9 +564,8 @@ export class Value<Type, Store> {
disabled: false, disabled: false,
immutable: false, immutable: false,
...a, ...a,
...requiredLikeToAbove(a.required),
} }
}, string.optional()) }, string)
} }
static multiselect<Values extends Record<string, string>>(a: { static multiselect<Values extends Record<string, string>>(a: {
name: string name: string
@@ -605,7 +575,7 @@ export class Value<Type, Store> {
/** /**
* @description A simple list of which options should be checked by default. * @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. * @description A mapping of checkbox options to their human readable display format.
* @example * @example
@@ -689,11 +659,11 @@ export class Value<Type, Store> {
} }
}, spec.validator) }, spec.validator)
} }
// static file<Store>(a: { // static file<Store, Required extends boolean>(a: {
// name: string // name: string
// description?: string | null // description?: string | null
// extensions: string[] // extensions: string[]
// required: boolean // required: Required
// }) { // }) {
// const buildValue = { // const buildValue = {
// type: "file" as const, // type: "file" as const,
@@ -701,14 +671,14 @@ export class Value<Type, Store> {
// warning: null, // warning: null,
// ...a, // ...a,
// } // }
// return new Value<FilePath, Store>( // return new Value<AsRequired<FilePath, Required>, Store>(
// () => ({ // () => ({
// ...buildValue, // ...buildValue,
// }), // }),
// asRequiredParser(object({ filePath: string }), a), // asRequiredParser(object({ filePath: string }), a),
// ) // )
// } // }
// static dynamicFile<Required extends boolean, Store>( // static dynamicFile<Store>(
// a: LazyBuild< // a: LazyBuild<
// Store, // Store,
// { // {
@@ -716,43 +686,49 @@ export class Value<Type, Store> {
// description?: string | null // description?: string | null
// warning?: string | null // warning?: string | null
// extensions: string[] // extensions: string[]
// required: Required // required: boolean
// } // }
// >, // >,
// ) { // ) {
// return new Value<string | null | undefined, Store>( // return new Value<FilePath | null | undefined, Store>(
// async (options) => ({ // async (options) => ({
// type: "file" as const, // type: "file" as const,
// description: null, // description: null,
// warning: null, // warning: null,
// ...(await a(options)), // ...(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: { a: {
name: string name: string
description?: string | null description?: string | null
/** Presents a warning prompt before permitting the value to change. */ /** Presents a warning prompt before permitting the value to change. */
warning?: string | null warning?: string | null
/** /**
* @description Determines if the field is required. If so, optionally provide a default value from the list of variants. * @description Provide a default value from the list of variants.
* @type { false | { default: string | null } } * @type { string }
* @example required: false * @example default: 'variant1'
* @example required: { default: null }
* @example required: { default: 'variant1' }
*/ */
required: Required default: keyof VariantValues & string
/** /**
* @description Once set, the value can never be changed. * @description Once set, the value can never be changed.
* @default false * @default false
*/ */
immutable?: boolean 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) => ({ async (options) => ({
type: "union" as const, type: "union" as const,
description: null, description: null,
@@ -760,44 +736,50 @@ export class Value<Type, Store> {
disabled: false, disabled: false,
...a, ...a,
variants: await aVariants.build(options as any), variants: await aVariants.build(options as any),
...requiredLikeToAbove(a.required),
immutable: a.immutable ?? false, immutable: a.immutable ?? false,
}), }),
asRequiredParser(aVariants.validator, a), aVariants.validator,
) )
} }
static filteredUnion< static filteredUnion<
Required extends RequiredDefault<string>, VariantValues extends {
Type extends Record<string, any>, [K in string]: {
Store = never, name: string
spec: InputSpec<any, Store> | InputSpec<any, never>
}
},
Store,
>( >(
getDisabledFn: LazyBuild<Store, string[] | false | string>, getDisabledFn: LazyBuild<Store, string[] | false | string>,
a: { a: {
name: string name: string
description?: string | null description?: string | null
warning?: 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) => ({ async (options) => ({
type: "union" as const, type: "union" as const,
description: null, description: null,
warning: null, warning: null,
...a, ...a,
variants: await aVariants.build(options as any), variants: await aVariants.build(options as any),
...requiredLikeToAbove(a.required),
disabled: (await getDisabledFn(options)) || false, disabled: (await getDisabledFn(options)) || false,
immutable: false, immutable: false,
}), }),
asRequiredParser(aVariants.validator, a), aVariants.validator,
) )
} }
static dynamicUnion< static dynamicUnion<
Required extends RequiredDefault<string>, VariantValues extends {
Type extends Record<string, any>, [K in string]: {
Store = never, name: string
spec: InputSpec<any, Store> | InputSpec<any, never>
}
},
Store,
>( >(
getA: LazyBuild< getA: LazyBuild<
Store, Store,
@@ -805,24 +787,26 @@ export class Value<Type, Store> {
name: string name: string
description?: string | null description?: string | null
warning?: string | null warning?: string | null
required: Required default: keyof VariantValues & string
disabled: string[] | false | 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) => { return new Value<typeof aVariants.validator._TYPE, Store>(
const newValues = await getA(options) async (options) => {
return { const newValues = await getA(options)
type: "union" as const, return {
description: null, type: "union" as const,
warning: null, description: null,
...newValues, warning: null,
variants: await aVariants.build(options as any), ...newValues,
...requiredLikeToAbove(newValues.required), variants: await aVariants.build(options as any),
immutable: false, immutable: false,
} }
}, aVariants.validator.optional()) },
aVariants.validator,
)
} }
static list<Type, Store>(a: List<Type, Store>) { static list<Type, Store>(a: List<Type, Store>) {

View File

@@ -1,6 +1,54 @@
import { DeepPartial } from "../../../types"
import { ValueSpec, ValueSpecUnion } from "../inputSpecTypes" import { ValueSpec, ValueSpecUnion } from "../inputSpecTypes"
import { LazyBuild, InputSpec } from "./inputSpec" import {
import { Parser, anyOf, literals, object } from "ts-matches" 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' } * Used in the the Value.select { @link './value.ts' }
@@ -44,18 +92,24 @@ export const pruning = Value.union(
description: 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', '- 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, warning: null,
required: true,
default: "disabled", default: "disabled",
}, },
pruningSettingsVariants pruningSettingsVariants
); );
``` ```
*/ */
export class Variants<Type, Store> { export class Variants<
static text: any VariantValues extends {
[K in string]: {
name: string
spec: InputSpec<any, Store> | InputSpec<any, never>
}
},
Store,
> {
private constructor( private constructor(
public build: LazyBuild<Store, ValueSpecUnion["variants"]>, public build: LazyBuild<Store, ValueSpecUnion["variants"]>,
public validator: Parser<unknown, Type>, public validator: Parser<unknown, UnionRes<Store, VariantValues>>,
) {} ) {}
static of< static of<
VariantValues extends { VariantValues extends {
@@ -67,26 +121,15 @@ export class Variants<Type, Store> {
Store = never, Store = never,
>(a: VariantValues) { >(a: VariantValues) {
const validator = anyOf( const validator = anyOf(
...Object.entries(a).map(([name, { spec }]) => ...Object.entries(a).map(([id, { spec }]) =>
object({ object({
selection: literals(name), selection: literal(id),
value: spec.validator, value: spec.validator,
}), }),
), ),
) as Parser<unknown, any> ) as Parser<unknown, any>
return new Variants< return new Variants<VariantValues, Store>(async (options) => {
{
[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) => {
const variants = {} as { const variants = {} as {
[K in keyof VariantValues]: { [K in keyof VariantValues]: {
name: string name: string
@@ -118,6 +161,6 @@ export class Variants<Type, Store> {
``` ```
*/ */
withStore<NewStore extends Store extends never ? any : 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>
} }
} }

View File

@@ -10,35 +10,34 @@ import { Variants } from "./builder/variants"
export const customSmtp = InputSpec.of<InputSpecOf<SmtpValue>, never>({ export const customSmtp = InputSpec.of<InputSpecOf<SmtpValue>, never>({
server: Value.text({ server: Value.text({
name: "SMTP Server", name: "SMTP Server",
required: { required: true,
default: null, default: null,
},
}), }),
port: Value.number({ port: Value.number({
name: "Port", name: "Port",
required: { default: 587 }, required: true,
default: 587,
min: 1, min: 1,
max: 65535, max: 65535,
integer: true, integer: true,
}), }),
from: Value.text({ from: Value.text({
name: "From Address", name: "From Address",
required: { required: true,
default: null, default: null,
},
placeholder: "<name>test@example.com", placeholder: "<name>test@example.com",
inputmode: "email", inputmode: "email",
patterns: [Patterns.email], patterns: [Patterns.email],
}), }),
login: Value.text({ login: Value.text({
name: "Login", name: "Login",
required: { required: true,
default: null, default: null,
},
}), }),
password: Value.text({ password: Value.text({
name: "Password", name: "Password",
required: false, required: false,
default: null,
masked: true, masked: true,
}), }),
}) })
@@ -54,7 +53,7 @@ export const smtpInputSpec = Value.filteredUnion(
{ {
name: "SMTP", name: "SMTP",
description: "Optionally provide an SMTP server for sending emails", description: "Optionally provide an SMTP server for sending emails",
required: { default: "disabled" }, default: "disabled",
}, },
Variants.of({ Variants.of({
disabled: { name: "Disabled", spec: InputSpec.of({}) }, disabled: { name: "Disabled", spec: InputSpec.of({}) },
@@ -66,6 +65,7 @@ export const smtpInputSpec = Value.filteredUnion(
description: description:
"A custom from address for this service. If not provided, the system from address will be used.", "A custom from address for this service. If not provided, the system from address will be used.",
required: false, required: false,
default: null,
placeholder: "<name>test@example.com", placeholder: "<name>test@example.com",
inputmode: "email", inputmode: "email",
patterns: [Patterns.email], patterns: [Patterns.email],

View File

@@ -115,7 +115,6 @@ export type ValueSpecSelect = {
description: string | null description: string | null
warning: string | null warning: string | null
type: "select" type: "select"
required: boolean
default: string | null default: string | null
disabled: false | string | string[] disabled: false | string | string[]
immutable: boolean immutable: boolean
@@ -158,7 +157,6 @@ export type ValueSpecUnion = {
} }
> >
disabled: false | string | string[] disabled: false | string | string[]
required: boolean
default: string | null default: string | null
immutable: boolean immutable: boolean
} }

View File

@@ -1,5 +1,8 @@
import { InputSpec } from "./input/builder" import { InputSpec } from "./input/builder"
import { ExtractInputSpecType } from "./input/builder/inputSpec" import {
ExtractInputSpecType,
ExtractPartialInputSpecType,
} from "./input/builder/inputSpec"
import * as T from "../types" import * as T from "../types"
import { once } from "../util" import { once } from "../util"
@@ -20,7 +23,10 @@ export type GetInput<
> = (options: { > = (options: {
effects: T.Effects effects: T.Effects
}) => Promise< }) => 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>) export type MaybeFn<T> = T | ((options: { effects: T.Effects }) => Promise<T>)
@@ -52,15 +58,13 @@ export class Action<
| Record<string, any> | Record<string, any>
| InputSpec<any, Store> | InputSpec<any, Store>
| InputSpec<any, never>, | InputSpec<any, never>,
Type extends
ExtractInputSpecType<InputSpecType> = ExtractInputSpecType<InputSpecType>,
> { > {
private constructor( private constructor(
readonly id: Id, readonly id: Id,
private readonly metadataFn: MaybeFn<T.ActionMetadata>, private readonly metadataFn: MaybeFn<T.ActionMetadata>,
private readonly inputSpec: InputSpecType, private readonly inputSpec: InputSpecType,
private readonly getInputFn: GetInput<Type>, private readonly getInputFn: GetInput<ExtractInputSpecType<InputSpecType>>,
private readonly runFn: Run<Type>, private readonly runFn: Run<ExtractInputSpecType<InputSpecType>>,
) {} ) {}
static withInput< static withInput<
Id extends T.ActionId, Id extends T.ActionId,
@@ -69,15 +73,13 @@ export class Action<
| Record<string, any> | Record<string, any>
| InputSpec<any, Store> | InputSpec<any, Store>
| InputSpec<any, never>, | InputSpec<any, never>,
Type extends
ExtractInputSpecType<InputSpecType> = ExtractInputSpecType<InputSpecType>,
>( >(
id: Id, id: Id,
metadata: MaybeFn<Omit<T.ActionMetadata, "hasInput">>, metadata: MaybeFn<Omit<T.ActionMetadata, "hasInput">>,
inputSpec: InputSpecType, inputSpec: InputSpecType,
getInput: GetInput<Type>, getInput: GetInput<ExtractInputSpecType<InputSpecType>>,
run: Run<Type>, run: Run<ExtractInputSpecType<InputSpecType>>,
): Action<Id, Store, InputSpecType, Type> { ): Action<Id, Store, InputSpecType> {
return new Action( return new Action(
id, id,
mapMaybeFn(metadata, (m) => ({ ...m, hasInput: true })), mapMaybeFn(metadata, (m) => ({ ...m, hasInput: true })),
@@ -90,7 +92,7 @@ export class Action<
id: Id, id: Id,
metadata: MaybeFn<Omit<T.ActionMetadata, "hasInput">>, metadata: MaybeFn<Omit<T.ActionMetadata, "hasInput">>,
run: Run<{}>, run: Run<{}>,
): Action<Id, Store, {}, {}> { ): Action<Id, Store, {}> {
return new Action( return new Action(
id, id,
mapMaybeFn(metadata, (m) => ({ ...m, hasInput: false })), mapMaybeFn(metadata, (m) => ({ ...m, hasInput: false })),
@@ -114,7 +116,7 @@ export class Action<
} }
async run(options: { async run(options: {
effects: T.Effects effects: T.Effects
input: Type input: ExtractInputSpecType<InputSpecType>
}): Promise<T.ActionResult | null> { }): Promise<T.ActionResult | null> {
return (await this.runFn(options)) || null return (await this.runFn(options)) || null
} }
@@ -122,13 +124,13 @@ export class Action<
export class Actions< export class Actions<
Store, 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) {} private constructor(private readonly actions: AllActions) {}
static of<Store>(): Actions<Store, {}> { static of<Store>(): Actions<Store, {}> {
return new Actions({}) return new Actions({})
} }
addAction<A extends Action<T.ActionId, Store, any, any>>( addAction<A extends Action<T.ActionId, Store, any>>(
action: A, action: A,
): Actions<Store, AllActions & { [id in A["id"]]: A }> { ): Actions<Store, AllActions & { [id in A["id"]]: A }> {
return new Actions({ ...this.actions, [action.id]: action }) return new Actions({ ...this.actions, [action.id]: action })

View File

@@ -86,7 +86,7 @@ export namespace ExpectedExports {
export type actions = Actions< export type actions = Actions<
any, any,
Record<ActionId, Action<ActionId, any, any, any>> Record<ActionId, Action<ActionId, any, any>>
> >
} }
export type ABI = { export type ABI = {

View File

@@ -14,7 +14,7 @@
"isomorphic-fetch": "^3.0.0", "isomorphic-fetch": "^3.0.0",
"lodash.merge": "^4.6.2", "lodash.merge": "^4.6.2",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"ts-matches": "^5.5.1", "ts-matches": "^6.0.0",
"yaml": "^2.2.2" "yaml": "^2.2.2"
}, },
"devDependencies": { "devDependencies": {
@@ -3897,9 +3897,10 @@
"dev": true "dev": true
}, },
"node_modules/ts-matches": { "node_modules/ts-matches": {
"version": "5.5.1", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-5.5.1.tgz", "resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-6.0.0.tgz",
"integrity": "sha512-UFYaKgfqlg9FROK7bdpYqFwG1CJvP4kOJdjXuWoqxo9jCmANoDw1GxkSCpJgoTeIiSTaTH5Qr1klSspb8c+ydg==" "integrity": "sha512-vR4hhz9bYMW30qIJUuLaeAWlsR54vse6ZI2riVhVLMBE6/vss43jwrOvbHheiyU7e26ssT/yWx69aJHD2REJSA==",
"license": "MIT"
}, },
"node_modules/ts-morph": { "node_modules/ts-morph": {
"version": "18.0.0", "version": "18.0.0",

View File

@@ -27,7 +27,7 @@
"isomorphic-fetch": "^3.0.0", "isomorphic-fetch": "^3.0.0",
"lodash.merge": "^4.6.2", "lodash.merge": "^4.6.2",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"ts-matches": "^5.5.1", "ts-matches": "^6.0.0",
"yaml": "^2.2.2" "yaml": "^2.2.2"
}, },
"prettier": { "prettier": {

View File

@@ -1,7 +1,4 @@
import { import { Value } from "../../base/lib/actions/input/builder/value"
RequiredDefault,
Value,
} from "../../base/lib/actions/input/builder/value"
import { import {
InputSpec, InputSpec,
ExtractInputSpecType, ExtractInputSpecType,
@@ -141,9 +138,7 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
...startSdkEffectWrapper, ...startSdkEffectWrapper,
action: { action: {
run: actions.runAction, run: actions.runAction,
request: < request: <T extends Action<T.ActionId, any, any>>(
T extends Action<T.ActionId, any, any, Record<string, unknown>>,
>(
effects: T.Effects, effects: T.Effects,
packageId: T.PackageId, packageId: T.PackageId,
action: T, action: T,
@@ -157,9 +152,7 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
severity, severity,
options: options, options: options,
}), }),
requestOwn: < requestOwn: <T extends Action<T.ActionId, Store, any>>(
T extends Action<T.ActionId, Store, any, Record<string, unknown>>,
>(
effects: T.Effects, effects: T.Effects,
action: T, action: T,
severity: T.ActionSeverity, severity: T.ActionSeverity,
@@ -1060,14 +1053,14 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
/** Presents a warning prompt before permitting the value to change. */ /** Presents a warning prompt before permitting the value to change. */
warning?: string | null warning?: string | null
/** /**
* @description Determines if the field is required. If so, optionally provide a default value. * @description optionally provide a default value.
* @type { false | { default: string | RandomString | null } } * @type { string | RandomString | null }
* @example required: false * @example default: null
* @example required: { default: null } * @example default: 'World'
* @example required: { default: 'World' } * @example default: { charset: 'abcdefg', len: 16 }
* @example required: { default: { charset: 'abcdefg', len: 16 } }
*/ */
required: RequiredDefault<DefaultString> default: DefaultString | null
required: boolean
/** /**
* @description Mask (aka camouflage) text input with dots: ● ● ● * @description Mask (aka camouflage) text input with dots: ● ● ●
* @default false * @default false
@@ -1110,15 +1103,12 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
description?: string | null description?: string | null
/** Presents a warning prompt before permitting the value to change. */ /** Presents a warning prompt before permitting the value to change. */
warning?: string | null warning?: string | null
/** default: string | null
* @description Unlike other "required" fields, for textarea this is a simple boolean.
*/
required: boolean required: boolean
minLength?: number | null minLength?: number | null
maxLength?: number | null maxLength?: number | null
placeholder?: string | null placeholder?: string | null
disabled?: false | string disabled?: false | string
generate?: null | RandomString
} }
>, >,
) => Value.dynamicTextarea<Store>(getA), ) => Value.dynamicTextarea<Store>(getA),
@@ -1131,13 +1121,13 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
/** Presents a warning prompt before permitting the value to change. */ /** Presents a warning prompt before permitting the value to change. */
warning?: string | null warning?: string | null
/** /**
* @description Determines if the field is required. If so, optionally provide a default value. * @description optionally provide a default value.
* @type { false | { default: number | null } } * @type { number | null }
* @example required: false * @example default: null
* @example required: { default: null } * @example default: 7
* @example required: { default: 7 }
*/ */
required: RequiredDefault<number> default: number | null
required: boolean
min?: number | null min?: number | null
max?: number | null max?: number | null
/** /**
@@ -1167,13 +1157,13 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
/** Presents a warning prompt before permitting the value to change. */ /** Presents a warning prompt before permitting the value to change. */
warning?: string | null warning?: string | null
/** /**
* @description Determines if the field is required. If so, optionally provide a default value. * @description optionally provide a default value.
* @type { false | { default: string | null } } * @type { string | null }
* @example required: false * @example default: null
* @example required: { default: null } * @example default: 'ffffff'
* @example required: { default: 'ffffff' }
*/ */
required: RequiredDefault<string> default: string | null
required: boolean
disabled?: false | string disabled?: false | string
} }
>, >,
@@ -1187,13 +1177,13 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
/** Presents a warning prompt before permitting the value to change. */ /** Presents a warning prompt before permitting the value to change. */
warning?: string | null warning?: string | null
/** /**
* @description Determines if the field is required. If so, optionally provide a default value. * @description optionally provide a default value.
* @type { false | { default: string | null } } * @type { string | null }
* @example required: false * @example default: null
* @example required: { default: null } * @example default: '1985-12-16 18:00:00.000'
* @example required: { default: '1985-12-16 18:00:00.000' }
*/ */
required: RequiredDefault<string> default: string
required: boolean
/** /**
* @description Informs the browser how to behave and which date/time component to display. * @description Informs the browser how to behave and which date/time component to display.
* @default "datetime-local" * @default "datetime-local"
@@ -1205,7 +1195,7 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
} }
>, >,
) => Value.dynamicDatetime<Store>(getA), ) => Value.dynamicDatetime<Store>(getA),
dynamicSelect: ( dynamicSelect: <Variants extends Record<string, string>>(
getA: LazyBuild< getA: LazyBuild<
Store, Store,
{ {
@@ -1214,13 +1204,12 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
/** Presents a warning prompt before permitting the value to change. */ /** Presents a warning prompt before permitting the value to change. */
warning?: string | null warning?: string | null
/** /**
* @description Determines if the field is required. If so, optionally provide a default value from the list of values. * @description provide a default value from the list of values.
* @type { false | { default: string | null } } * @type { default: string }
* @example required: false * @example default: 'radio1'
* @example required: { default: null }
* @example required: { default: 'radio1' }
*/ */
required: RequiredDefault<string> default: keyof Variants & string
required: boolean
/** /**
* @description A mapping of unique radio options to their human readable display format. * @description A mapping of unique radio options to their human readable display format.
* @example * @example
@@ -1232,7 +1221,7 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
} }
* ``` * ```
*/ */
values: Record<string, string> values: Variants
/** /**
* @options * @options
* - false - The field can be modified. * - false - The field can be modified.
@@ -1282,27 +1271,37 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
>, >,
) => Value.dynamicMultiselect<Store>(getA), ) => Value.dynamicMultiselect<Store>(getA),
filteredUnion: < filteredUnion: <
Required extends RequiredDefault<string>, VariantValues extends {
Type extends Record<string, any>, [K in string]: {
name: string
spec: InputSpec<any, Store> | InputSpec<any, never>
}
},
>( >(
getDisabledFn: LazyBuild<Store, string[]>, getDisabledFn: LazyBuild<Store, string[]>,
a: { a: {
name: string name: string
description?: string | null description?: string | null
warning?: 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>,
) => ) =>
Value.filteredUnion<Required, Type, Store>( Value.filteredUnion<VariantValues, Store>(
getDisabledFn, getDisabledFn,
a, a,
aVariants, aVariants,
), ),
dynamicUnion: < dynamicUnion: <
Required extends RequiredDefault<string>, VariantValues extends {
Type extends Record<string, any>, [K in string]: {
name: string
spec: InputSpec<any, Store> | InputSpec<any, never>
}
},
>( >(
getA: LazyBuild< getA: LazyBuild<
Store, Store,
@@ -1312,13 +1311,12 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
/** Presents a warning prompt before permitting the value to change. */ /** Presents a warning prompt before permitting the value to change. */
warning?: string | null warning?: string | null
/** /**
* @description Determines if the field is required. If so, optionally provide a default value from the list of variants. * @description provide a default value from the list of variants.
* @type { false | { default: string | null } } * @type { string }
* @example required: false * @example default: 'variant1'
* @example required: { default: null }
* @example required: { default: 'variant1' }
*/ */
required: Required default: keyof VariantValues & string
required: boolean
/** /**
* @options * @options
* - false - The field can be modified. * - false - The field can be modified.
@@ -1329,8 +1327,10 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
disabled: false | string | string[] disabled: false | string | string[]
} }
>, >,
aVariants: Variants<Type, Store> | Variants<Type, never>, aVariants:
) => Value.dynamicUnion<Required, Type, Store>(getA, aVariants), | Variants<VariantValues, Store>
| Variants<VariantValues, never>,
) => Value.dynamicUnion<VariantValues, Store>(getA, aVariants),
}, },
Variants: { Variants: {
of: < of: <

View File

@@ -17,7 +17,8 @@ describe("builder tests", () => {
"peer-tor-address": Value.text({ "peer-tor-address": Value.text({
name: "Peer tor address", name: "Peer tor address",
description: "The Tor address of the peer interface", description: "The Tor address of the peer interface",
required: { default: null }, required: true,
default: null,
}), }),
}).build({} as any) }).build({} as any)
expect(bitcoinPropertiesBuilt).toMatchObject({ expect(bitcoinPropertiesBuilt).toMatchObject({
@@ -55,7 +56,8 @@ describe("values", () => {
test("text", async () => { test("text", async () => {
const value = Value.text({ const value = Value.text({
name: "Testing", name: "Testing",
required: { default: null }, required: true,
default: null,
}) })
const validator = value.validator const validator = value.validator
const rawIs = await value.build({} as any) const rawIs = await value.build({} as any)
@@ -66,7 +68,8 @@ describe("values", () => {
test("text with default", async () => { test("text with default", async () => {
const value = Value.text({ const value = Value.text({
name: "Testing", name: "Testing",
required: { default: "this is a default value" }, required: true,
default: "this is a default value",
}) })
const validator = value.validator const validator = value.validator
const rawIs = await value.build({} as any) const rawIs = await value.build({} as any)
@@ -78,6 +81,7 @@ describe("values", () => {
const value = Value.text({ const value = Value.text({
name: "Testing", name: "Testing",
required: false, required: false,
default: null,
}) })
const validator = value.validator const validator = value.validator
const rawIs = await value.build({} as any) const rawIs = await value.build({} as any)
@@ -89,6 +93,7 @@ describe("values", () => {
const value = Value.color({ const value = Value.color({
name: "Testing", name: "Testing",
required: false, required: false,
default: null,
description: null, description: null,
warning: null, warning: null,
}) })
@@ -99,7 +104,8 @@ describe("values", () => {
test("datetime", async () => { test("datetime", async () => {
const value = Value.datetime({ const value = Value.datetime({
name: "Testing", name: "Testing",
required: { default: null }, required: true,
default: null,
description: null, description: null,
warning: null, warning: null,
inputmode: "date", inputmode: "date",
@@ -114,6 +120,7 @@ describe("values", () => {
const value = Value.datetime({ const value = Value.datetime({
name: "Testing", name: "Testing",
required: false, required: false,
default: null,
description: null, description: null,
warning: null, warning: null,
inputmode: "date", inputmode: "date",
@@ -128,6 +135,7 @@ describe("values", () => {
const value = Value.textarea({ const value = Value.textarea({
name: "Testing", name: "Testing",
required: false, required: false,
default: null,
description: null, description: null,
warning: null, warning: null,
minLength: null, minLength: null,
@@ -136,12 +144,13 @@ describe("values", () => {
}) })
const validator = value.validator const validator = value.validator
validator.unsafeCast("test text") validator.unsafeCast("test text")
testOutput<typeof validator._TYPE, string>()(null) testOutput<typeof validator._TYPE, string | null | undefined>()(null)
}) })
test("number", async () => { test("number", async () => {
const value = Value.number({ const value = Value.number({
name: "Testing", name: "Testing",
required: { default: null }, required: true,
default: null,
integer: false, integer: false,
description: null, description: null,
warning: null, warning: null,
@@ -159,6 +168,7 @@ describe("values", () => {
const value = Value.number({ const value = Value.number({
name: "Testing", name: "Testing",
required: false, required: false,
default: null,
integer: false, integer: false,
description: null, description: null,
warning: null, warning: null,
@@ -175,7 +185,7 @@ describe("values", () => {
test("select", async () => { test("select", async () => {
const value = Value.select({ const value = Value.select({
name: "Testing", name: "Testing",
required: { default: null }, default: "a",
values: { values: {
a: "A", a: "A",
b: "B", b: "B",
@@ -192,7 +202,7 @@ describe("values", () => {
test("nullable select", async () => { test("nullable select", async () => {
const value = Value.select({ const value = Value.select({
name: "Testing", name: "Testing",
required: false, default: "a",
values: { values: {
a: "A", a: "A",
b: "B", b: "B",
@@ -203,8 +213,7 @@ describe("values", () => {
const validator = value.validator const validator = value.validator
validator.unsafeCast("a") validator.unsafeCast("a")
validator.unsafeCast("b") validator.unsafeCast("b")
validator.unsafeCast(null) testOutput<typeof validator._TYPE, "a" | "b">()(null)
testOutput<typeof validator._TYPE, "a" | "b" | null | undefined>()(null)
}) })
test("multiselect", async () => { test("multiselect", async () => {
const value = Value.multiselect({ const value = Value.multiselect({
@@ -250,7 +259,7 @@ describe("values", () => {
const value = Value.union( const value = Value.union(
{ {
name: "Testing", name: "Testing",
required: { default: null }, default: "a",
description: null, description: null,
warning: null, warning: null,
}, },
@@ -271,7 +280,16 @@ describe("values", () => {
const validator = value.validator const validator = value.validator
validator.unsafeCast({ selection: "a", value: { b: false } }) validator.unsafeCast({ selection: "a", value: { b: false } })
type Test = typeof validator._TYPE type Test = typeof validator._TYPE
testOutput<Test, { selection: "a"; value: { b: boolean } }>()(null) testOutput<
Test,
{
selection: "a"
value: {
b: boolean
}
other?: {}
}
>()(null)
}) })
describe("dynamic", () => { describe("dynamic", () => {
@@ -301,7 +319,8 @@ describe("values", () => {
test("text", async () => { test("text", async () => {
const value = Value.dynamicText(async () => ({ const value = Value.dynamicText(async () => ({
name: "Testing", name: "Testing",
required: { default: null }, required: true,
default: null,
})) }))
const validator = value.validator const validator = value.validator
const rawIs = await value.build({} as any) const rawIs = await value.build({} as any)
@@ -317,7 +336,8 @@ describe("values", () => {
test("text with default", async () => { test("text with default", async () => {
const value = Value.dynamicText(async () => ({ const value = Value.dynamicText(async () => ({
name: "Testing", name: "Testing",
required: { default: "this is a default value" }, required: true,
default: "this is a default value",
})) }))
const validator = value.validator const validator = value.validator
validator.unsafeCast("test text") validator.unsafeCast("test text")
@@ -333,6 +353,7 @@ describe("values", () => {
const value = Value.dynamicText(async () => ({ const value = Value.dynamicText(async () => ({
name: "Testing", name: "Testing",
required: false, required: false,
default: null,
})) }))
const validator = value.validator const validator = value.validator
const rawIs = await value.build({} as any) const rawIs = await value.build({} as any)
@@ -349,6 +370,7 @@ describe("values", () => {
const value = Value.dynamicColor(async () => ({ const value = Value.dynamicColor(async () => ({
name: "Testing", name: "Testing",
required: false, required: false,
default: null,
description: null, description: null,
warning: null, warning: null,
})) }))
@@ -414,7 +436,8 @@ describe("values", () => {
return { return {
name: "Testing", name: "Testing",
required: { default: null }, required: true,
default: null,
inputmode: "date", inputmode: "date",
} }
}, },
@@ -436,6 +459,7 @@ describe("values", () => {
const value = Value.dynamicTextarea(async () => ({ const value = Value.dynamicTextarea(async () => ({
name: "Testing", name: "Testing",
required: false, required: false,
default: null,
description: null, description: null,
warning: null, warning: null,
minLength: null, minLength: null,
@@ -444,8 +468,7 @@ describe("values", () => {
})) }))
const validator = value.validator const validator = value.validator
validator.unsafeCast("test text") validator.unsafeCast("test text")
expect(() => validator.unsafeCast(null)).toThrowError() testOutput<typeof validator._TYPE, string | null | undefined>()(null)
testOutput<typeof validator._TYPE, string>()(null)
expect(await value.build(fakeOptions)).toMatchObject({ expect(await value.build(fakeOptions)).toMatchObject({
name: "Testing", name: "Testing",
required: false, required: false,
@@ -454,7 +477,8 @@ describe("values", () => {
test("number", async () => { test("number", async () => {
const value = Value.dynamicNumber(() => ({ const value = Value.dynamicNumber(() => ({
name: "Testing", name: "Testing",
required: { default: null }, required: true,
default: null,
integer: false, integer: false,
description: null, description: null,
warning: null, warning: null,
@@ -477,7 +501,7 @@ describe("values", () => {
test("select", async () => { test("select", async () => {
const value = Value.dynamicSelect(() => ({ const value = Value.dynamicSelect(() => ({
name: "Testing", name: "Testing",
required: { default: null }, default: "a",
values: { values: {
a: "A", a: "A",
b: "B", b: "B",
@@ -489,11 +513,9 @@ describe("values", () => {
validator.unsafeCast("a") validator.unsafeCast("a")
validator.unsafeCast("b") validator.unsafeCast("b")
validator.unsafeCast("c") validator.unsafeCast("c")
validator.unsafeCast(null) testOutput<typeof validator._TYPE, string>()(null)
testOutput<typeof validator._TYPE, string | null | undefined>()(null)
expect(await value.build(fakeOptions)).toMatchObject({ expect(await value.build(fakeOptions)).toMatchObject({
name: "Testing", name: "Testing",
required: true,
}) })
}) })
test("multiselect", async () => { test("multiselect", async () => {
@@ -529,7 +551,7 @@ describe("values", () => {
() => ["a", "c"], () => ["a", "c"],
{ {
name: "Testing", name: "Testing",
required: { default: null }, default: "a",
description: null, description: null,
warning: null, warning: null,
}, },
@@ -563,8 +585,28 @@ describe("values", () => {
type Test = typeof validator._TYPE type Test = typeof validator._TYPE
testOutput< testOutput<
Test, Test,
| { selection: "a"; value: { b: boolean } } | {
| { selection: "b"; value: { b: boolean } } selection: "a"
value: {
b: boolean
}
other?: {
b?: {
b?: boolean
}
}
}
| {
selection: "b"
value: {
b: boolean
}
other?: {
a?: {
b?: boolean
}
}
}
>()(null) >()(null)
const built = await value.build({} as any) const built = await value.build({} as any)
@@ -596,7 +638,7 @@ describe("values", () => {
() => ({ () => ({
disabled: ["a", "c"], disabled: ["a", "c"],
name: "Testing", name: "Testing",
required: { default: null }, default: "b",
description: null, description: null,
warning: null, warning: null,
}), }),
@@ -630,10 +672,28 @@ describe("values", () => {
type Test = typeof validator._TYPE type Test = typeof validator._TYPE
testOutput< testOutput<
Test, Test,
| { selection: "a"; value: { b: boolean } } | {
| { selection: "b"; value: { b: boolean } } selection: "a"
| null value: {
| undefined b: boolean
}
other?: {
b?: {
b?: boolean
}
}
}
| {
selection: "b"
value: {
b: boolean
}
other?: {
a?: {
b?: boolean
}
}
}
>()(null) >()(null)
const built = await value.build({} as any) const built = await value.build({} as any)
@@ -728,6 +788,7 @@ describe("Nested nullable values", () => {
description: description:
"If no name is provided, the name from inputSpec will be used", "If no name is provided, the name from inputSpec will be used",
required: false, required: false,
default: null,
}), }),
}) })
const validator = value.validator const validator = value.validator
@@ -743,6 +804,7 @@ describe("Nested nullable values", () => {
description: description:
"If no name is provided, the name from inputSpec will be used", "If no name is provided, the name from inputSpec will be used",
required: false, required: false,
default: null,
warning: null, warning: null,
placeholder: null, placeholder: null,
integer: false, integer: false,
@@ -765,6 +827,7 @@ describe("Nested nullable values", () => {
description: description:
"If no name is provided, the name from inputSpec will be used", "If no name is provided, the name from inputSpec will be used",
required: false, required: false,
default: null,
warning: null, warning: null,
}), }),
}) })
@@ -780,7 +843,7 @@ describe("Nested nullable values", () => {
name: "Temp Name", name: "Temp Name",
description: description:
"If no name is provided, the name from inputSpec will be used", "If no name is provided, the name from inputSpec will be used",
required: false, default: "a",
warning: null, warning: null,
values: { values: {
a: "A", a: "A",
@@ -791,7 +854,7 @@ describe("Nested nullable values", () => {
name: "Temp Name", name: "Temp Name",
description: description:
"If no name is provided, the name from inputSpec will be used", "If no name is provided, the name from inputSpec will be used",
required: false, default: "a",
warning: null, warning: null,
values: { values: {
a: "A", a: "A",
@@ -799,10 +862,9 @@ describe("Nested nullable values", () => {
}).build({} as any) }).build({} as any)
const validator = value.validator const validator = value.validator
validator.unsafeCast({ a: null })
validator.unsafeCast({ a: "a" }) validator.unsafeCast({ a: "a" })
expect(() => validator.unsafeCast({ a: "4" })).toThrowError() expect(() => validator.unsafeCast({ a: "4" })).toThrowError()
testOutput<typeof validator._TYPE, { a: "a" | null | undefined }>()(null) testOutput<typeof validator._TYPE, { a: "a" }>()(null)
}) })
test("Testing multiselect", async () => { test("Testing multiselect", async () => {
const value = InputSpec.of({ const value = InputSpec.of({

View File

@@ -87,7 +87,7 @@ describe("Inputs", () => {
dbcache: 5, dbcache: 5,
pruning: { pruning: {
selection: "disabled", selection: "disabled",
value: {}, value: { disabled: {} },
}, },
blockfilters: { blockfilters: {
blockfilterindex: false, blockfilterindex: false,

View File

@@ -80,7 +80,8 @@ export class FileHelper<A> {
protected constructor( protected constructor(
readonly path: string, readonly path: string,
readonly writeData: (dataIn: A) => string, readonly writeData: (dataIn: A) => string,
readonly readData: (stringValue: string) => A, readonly readData: (stringValue: string) => unknown,
readonly validate: (value: unknown) => A,
) {} ) {}
/** /**
@@ -97,10 +98,7 @@ export class FileHelper<A> {
return null return null
} }
/** private async readFile(): Promise<unknown> {
* Reads the file from disk and converts it to structured data.
*/
private async readOnce(): Promise<A | null> {
if (!(await exists(this.path))) { if (!(await exists(this.path))) {
return null return null
} }
@@ -109,6 +107,15 @@ export class FileHelper<A> {
) )
} }
/**
* Reads the file from disk and converts it to structured data.
*/
private async readOnce(): Promise<A | null> {
const data = await this.readFile()
if (!data) return null
return this.validate(data)
}
private async readConst(effects: T.Effects): Promise<A | null> { private async readConst(effects: T.Effects): Promise<A | null> {
const watch = this.readWatch() const watch = this.readWatch()
const res = await watch.next() const res = await watch.next()
@@ -156,22 +163,22 @@ export class FileHelper<A> {
* Accepts full structured data and performs a merge with the existing file on disk if it exists. * Accepts full structured data and performs a merge with the existing file on disk if it exists.
*/ */
async write(data: A) { async write(data: A) {
const fileData = (await this.readOnce()) || {} const fileData = (await this.readFile()) || {}
const mergeData = merge({}, fileData, data) const mergeData = merge({}, fileData, data)
return await this.writeFile(mergeData) return await this.writeFile(this.validate(mergeData))
} }
/** /**
* Accepts partial structured data and performs a merge with the existing file on disk. * Accepts partial structured data and performs a merge with the existing file on disk.
*/ */
async merge(data: Partial<A>) { async merge(data: T.DeepPartial<A>) {
const fileData = const fileData =
(await this.readOnce()) || (await this.readFile()) ||
(() => { (() => {
throw new Error(`${this.path}: does not exist`) throw new Error(`${this.path}: does not exist`)
})() })()
const mergeData = merge({}, fileData, data) const mergeData = merge({}, fileData, data)
return await this.writeFile(mergeData) return await this.writeFile(this.validate(mergeData))
} }
/** /**
@@ -179,7 +186,7 @@ export class FileHelper<A> {
* Like one behaviour of another dependency or something similar. * Like one behaviour of another dependency or something similar.
*/ */
withPath(path: string) { withPath(path: string) {
return new FileHelper<A>(path, this.writeData, this.readData) return new FileHelper<A>(path, this.writeData, this.readData, this.validate)
} }
/** /**
@@ -190,9 +197,10 @@ export class FileHelper<A> {
static raw<A>( static raw<A>(
path: string, path: string,
toFile: (dataIn: A) => string, toFile: (dataIn: A) => string,
fromFile: (rawData: string) => A, fromFile: (rawData: string) => unknown,
validate: (data: unknown) => A,
) { ) {
return new FileHelper<A>(path, toFile, fromFile) return new FileHelper<A>(path, toFile, fromFile, validate)
} }
/** /**
* Create a File Helper for a .json file. * Create a File Helper for a .json file.
@@ -200,12 +208,9 @@ export class FileHelper<A> {
static json<A>(path: string, shape: matches.Validator<unknown, A>) { static json<A>(path: string, shape: matches.Validator<unknown, A>) {
return new FileHelper<A>( return new FileHelper<A>(
path, path,
(inData) => { (inData) => JSON.stringify(inData, null, 2),
return JSON.stringify(inData, null, 2) (inString) => JSON.parse(inString),
}, (data) => shape.unsafeCast(data),
(inString) => {
return shape.unsafeCast(JSON.parse(inString))
},
) )
} }
/** /**
@@ -217,12 +222,9 @@ export class FileHelper<A> {
) { ) {
return new FileHelper<A>( return new FileHelper<A>(
path, path,
(inData) => { (inData) => TOML.stringify(inData as any),
return TOML.stringify(inData as any) (inString) => TOML.parse(inString),
}, (data) => shape.unsafeCast(data),
(inString) => {
return shape.unsafeCast(TOML.parse(inString))
},
) )
} }
/** /**
@@ -234,12 +236,9 @@ export class FileHelper<A> {
) { ) {
return new FileHelper<A>( return new FileHelper<A>(
path, path,
(inData) => { (inData) => YAML.stringify(inData, null, 2),
return YAML.stringify(inData, null, 2) (inString) => YAML.parse(inString),
}, (data) => shape.unsafeCast(data),
(inString) => {
return shape.unsafeCast(YAML.parse(inString))
},
) )
} }
} }

View File

@@ -1,12 +1,12 @@
{ {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.3.6-alpha.13", "version": "0.3.6-alpha.16",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.3.6-alpha.13", "version": "0.3.6-alpha.16",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",
@@ -15,7 +15,7 @@
"isomorphic-fetch": "^3.0.0", "isomorphic-fetch": "^3.0.0",
"lodash.merge": "^4.6.2", "lodash.merge": "^4.6.2",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"ts-matches": "^5.5.1", "ts-matches": "^6.0.0",
"yaml": "^2.2.2" "yaml": "^2.2.2"
}, },
"devDependencies": { "devDependencies": {
@@ -3918,9 +3918,10 @@
"dev": true "dev": true
}, },
"node_modules/ts-matches": { "node_modules/ts-matches": {
"version": "5.5.1", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-5.5.1.tgz", "resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-6.0.0.tgz",
"integrity": "sha512-UFYaKgfqlg9FROK7bdpYqFwG1CJvP4kOJdjXuWoqxo9jCmANoDw1GxkSCpJgoTeIiSTaTH5Qr1klSspb8c+ydg==" "integrity": "sha512-vR4hhz9bYMW30qIJUuLaeAWlsR54vse6ZI2riVhVLMBE6/vss43jwrOvbHheiyU7e26ssT/yWx69aJHD2REJSA==",
"license": "MIT"
}, },
"node_modules/ts-morph": { "node_modules/ts-morph": {
"version": "18.0.0", "version": "18.0.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.3.6-alpha.13", "version": "0.3.6-alpha.17",
"description": "Software development kit to facilitate packaging services for StartOS", "description": "Software development kit to facilitate packaging services for StartOS",
"main": "./package/lib/index.js", "main": "./package/lib/index.js",
"types": "./package/lib/index.d.ts", "types": "./package/lib/index.d.ts",
@@ -33,7 +33,7 @@
"isomorphic-fetch": "^3.0.0", "isomorphic-fetch": "^3.0.0",
"lodash.merge": "^4.6.2", "lodash.merge": "^4.6.2",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"ts-matches": "^5.5.1", "ts-matches": "^6.0.0",
"yaml": "^2.2.2", "yaml": "^2.2.2",
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",
"@noble/curves": "^1.4.0", "@noble/curves": "^1.4.0",

View File

@@ -85,6 +85,7 @@ const {InputSpec, List, Value, Variants} = sdk
description: value.description || null, description: value.description || null,
warning: value.warning || null, warning: value.warning || null,
required: !(value.nullable || false), required: !(value.nullable || false),
default: value.default,
placeholder: value.placeholder || null, placeholder: value.placeholder || null,
maxLength: null, maxLength: null,
minLength: null, minLength: null,
@@ -96,12 +97,8 @@ const {InputSpec, List, Value, Variants} = sdk
return `${rangeToTodoComment(value?.range)}Value.text(${JSON.stringify( return `${rangeToTodoComment(value?.range)}Value.text(${JSON.stringify(
{ {
name: value.name || null, name: value.name || null,
// prettier-ignore default: value.default || null,
required: ( required: !value.nullable,
value.default != null ? {default: value.default} :
value.nullable === false ? {default: null} :
!value.nullable
),
description: value.description || null, description: value.description || null,
warning: value.warning || null, warning: value.warning || null,
masked: value.masked || false, masked: value.masked || false,
@@ -130,12 +127,8 @@ const {InputSpec, List, Value, Variants} = sdk
name: value.name || null, name: value.name || null,
description: value.description || null, description: value.description || null,
warning: value.warning || null, warning: value.warning || null,
// prettier-ignore default: value.default || null,
required: ( required: !value.nullable,
value.default != null ? {default: value.default} :
value.nullable === false ? {default: null} :
!value.nullable
),
min: null, min: null,
max: null, max: null,
step: null, step: null,
@@ -174,13 +167,7 @@ const {InputSpec, List, Value, Variants} = sdk
name: value.name || null, name: value.name || null,
description: value.description || null, description: value.description || null,
warning: value.warning || null, warning: value.warning || null,
default: value.default,
// prettier-ignore
required:(
value.default != null ? {default: value.default} :
value.nullable === false ? {default: null} :
!value.nullable
),
values, values,
}, },
null, null,
@@ -207,14 +194,7 @@ const {InputSpec, List, Value, Variants} = sdk
name: ${JSON.stringify(value.name || null)}, name: ${JSON.stringify(value.name || null)},
description: ${JSON.stringify(value.tag.description || null)}, description: ${JSON.stringify(value.tag.description || null)},
warning: ${JSON.stringify(value.tag.warning || null)}, warning: ${JSON.stringify(value.tag.warning || null)},
default: ${JSON.stringify(value.default)},
// prettier-ignore
required: ${JSON.stringify(
// prettier-ignore
value.default != null ? {default: value.default} :
value.nullable === false ? {default: null} :
!value.nullable,
)},
}, ${variants})` }, ${variants})`
} }
case "list": { case "list": {
@@ -341,12 +321,7 @@ const {InputSpec, List, Value, Variants} = sdk
value?.spec?.tag?.description || null, value?.spec?.tag?.description || null,
)}, )},
warning: ${JSON.stringify(value?.spec?.tag?.warning || null)}, warning: ${JSON.stringify(value?.spec?.tag?.warning || null)},
required: ${JSON.stringify( default: ${JSON.stringify(value?.spec?.default || null)},
// prettier-ignore
'default' in value?.spec ? {default: value?.spec?.default} :
!!value?.spec?.tag?.nullable || false ? {default: null} :
false,
)},
}, ${variants}) }, ${variants})
`, `,
) )

View File

@@ -268,25 +268,29 @@ const cifsSpec = ISB.InputSpec.of({
'The hostname of your target device on the Local Area Network.', 'The hostname of your target device on the Local Area Network.',
warning: null, warning: null,
placeholder: `e.g. 'My Computer' OR 'my-computer.local'`, placeholder: `e.g. 'My Computer' OR 'my-computer.local'`,
required: { default: null }, required: true,
default: null,
patterns: [], patterns: [],
}), }),
path: ISB.Value.text({ path: ISB.Value.text({
name: 'Path', name: 'Path',
description: `On Windows, this is the fully qualified path to the shared folder, (e.g. /Desktop/my-folder).\n\n On Linux and Mac, this is the literal name of the shared folder (e.g. my-shared-folder).`, description: `On Windows, this is the fully qualified path to the shared folder, (e.g. /Desktop/my-folder).\n\n On Linux and Mac, this is the literal name of the shared folder (e.g. my-shared-folder).`,
placeholder: 'e.g. my-shared-folder or /Desktop/my-folder', placeholder: 'e.g. my-shared-folder or /Desktop/my-folder',
required: { default: null }, required: true,
default: null,
}), }),
username: ISB.Value.text({ username: ISB.Value.text({
name: 'Username', name: 'Username',
description: `On Linux, this is the samba username you created when sharing the folder.\n\n On Mac and Windows, this is the username of the user who is sharing the folder.`, description: `On Linux, this is the samba username you created when sharing the folder.\n\n On Mac and Windows, this is the username of the user who is sharing the folder.`,
required: { default: null }, required: true,
default: null,
placeholder: 'My Network Folder', placeholder: 'My Network Folder',
}), }),
password: ISB.Value.text({ password: ISB.Value.text({
name: 'Password', name: 'Password',
description: `On Linux, this is the samba password you created when sharing the folder.\n\n On Mac and Windows, this is the password of the user who is sharing the folder.`, description: `On Linux, this is the samba password you created when sharing the folder.\n\n On Mac and Windows, this is the password of the user who is sharing the folder.`,
required: false, required: false,
default: null,
masked: true, masked: true,
placeholder: 'My Network Folder', placeholder: 'My Network Folder',
}), }),

View File

@@ -2,13 +2,12 @@
[tuiHintContent]="spec | hint" [tuiHintContent]="spec | hint"
[disabled]="disabled" [disabled]="disabled"
[readOnly]="readOnly" [readOnly]="readOnly"
[tuiTextfieldCleaner]="!spec.required" [tuiTextfieldCleaner]="false"
[pseudoInvalid]="invalid" [pseudoInvalid]="invalid"
[(ngModel)]="selected" [(ngModel)]="selected"
(focusedChange)="onFocus($event)" (focusedChange)="onFocus($event)"
> >
{{ spec.name }} {{ spec.name }}*
<span *ngIf="spec.required">*</span>
<select <select
tuiSelect tuiSelect
[placeholder]="spec.name" [placeholder]="spec.name"

View File

@@ -697,23 +697,20 @@ interface SettingBtn {
const passwordSpec = ISB.InputSpec.of({ const passwordSpec = ISB.InputSpec.of({
currentPassword: ISB.Value.text({ currentPassword: ISB.Value.text({
name: 'Current Password', name: 'Current Password',
required: { required: true,
default: null, default: null,
},
masked: true, masked: true,
}), }),
newPassword1: ISB.Value.text({ newPassword1: ISB.Value.text({
name: 'New Password', name: 'New Password',
required: { required: true,
default: null, default: null,
},
masked: true, masked: true,
}), }),
newPassword2: ISB.Value.text({ newPassword2: ISB.Value.text({
name: 'Retype New Password', name: 'Retype New Password',
required: { required: true,
default: null, default: null,
},
masked: true, masked: true,
}), }),
}) })

View File

@@ -1151,7 +1151,7 @@ export module Mock {
name: 'P2P Settings', name: 'P2P Settings',
description: description:
'<p>The Bitcoin Core node to connect to over the peer-to-peer (P2P) interface:</p><ul><li><strong>Bitcoin Core</strong>: The Bitcoin Core service installed on this device</li><li><strong>External Node</strong>: A Bitcoin node running on a different device</li></ul>', '<p>The Bitcoin Core node to connect to over the peer-to-peer (P2P) interface:</p><ul><li><strong>Bitcoin Core</strong>: The Bitcoin Core service installed on this device</li><li><strong>External Node</strong>: A Bitcoin node running on a different device</li></ul>',
required: { default: 'internal' }, default: 'internal',
}, },
ISB.Variants.of({ ISB.Variants.of({
internal: { name: 'Bitcoin Core', spec: ISB.InputSpec.of({}) }, internal: { name: 'Bitcoin Core', spec: ISB.InputSpec.of({}) },
@@ -1160,9 +1160,8 @@ export module Mock {
spec: ISB.InputSpec.of({ spec: ISB.InputSpec.of({
'p2p-host': ISB.Value.text({ 'p2p-host': ISB.Value.text({
name: 'Public Address', name: 'Public Address',
required: { required: false,
default: null, default: null,
},
description: description:
'The public address of your Bitcoin Core server', 'The public address of your Bitcoin Core server',
}), }),
@@ -1170,9 +1169,8 @@ export module Mock {
name: 'P2P Port', name: 'P2P Port',
description: description:
'The port that your Bitcoin Core P2P server is bound to', 'The port that your Bitcoin Core P2P server is bound to',
required: { required: true,
default: 8333, default: 8333,
},
min: 0, min: 0,
max: 65535, max: 65535,
integer: true, integer: true,
@@ -1186,10 +1184,12 @@ export module Mock {
color: ISB.Value.color({ color: ISB.Value.color({
name: 'Color', name: 'Color',
required: false, required: false,
default: null,
}), }),
datetime: ISB.Value.datetime({ datetime: ISB.Value.datetime({
name: 'Datetime', name: 'Datetime',
required: false, required: false,
default: null,
}), }),
// file: ISB.Value.file({ // file: ISB.Value.file({
// name: 'File', // name: 'File',
@@ -1221,9 +1221,8 @@ export module Mock {
ISB.InputSpec.of({ ISB.InputSpec.of({
rpcuser2: ISB.Value.text({ rpcuser2: ISB.Value.text({
name: 'RPC Username', name: 'RPC Username',
required: { required: false,
default: 'defaultrpcusername', default: 'defaultrpcusername',
},
description: 'rpc username', description: 'rpc username',
patterns: [ patterns: [
{ {
@@ -1234,9 +1233,8 @@ export module Mock {
}), }),
rpcuser: ISB.Value.text({ rpcuser: ISB.Value.text({
name: 'RPC Username', name: 'RPC Username',
required: { required: true,
default: 'defaultrpcusername', default: 'defaultrpcusername',
},
description: 'rpc username', description: 'rpc username',
patterns: [ patterns: [
{ {
@@ -1247,21 +1245,19 @@ export module Mock {
}), }),
rpcpass: ISB.Value.text({ rpcpass: ISB.Value.text({
name: 'RPC User Password', name: 'RPC User Password',
required: { required: true,
default: { default: {
charset: 'a-z,A-Z,2-9', charset: 'a-z,A-Z,2-9',
len: 20, len: 20,
},
}, },
description: 'rpc password', description: 'rpc password',
}), }),
rpcpass2: ISB.Value.text({ rpcpass2: ISB.Value.text({
name: 'RPC User Password', name: 'RPC User Password',
required: { required: true,
default: { default: {
charset: 'a-z,A-Z,2-9', charset: 'a-z,A-Z,2-9',
len: 20, len: 20,
},
}, },
description: 'rpc password', description: 'rpc password',
}), }),
@@ -1294,14 +1290,14 @@ export module Mock {
name: 'First Name', name: 'First Name',
required: false, required: false,
description: 'User first name', description: 'User first name',
default: 'Matt',
}), }),
'last-name': ISB.Value.text({ 'last-name': ISB.Value.text({
name: 'Last Name', name: 'Last Name',
required: { required: true,
default: { default: {
charset: 'a-g,2-9', charset: 'a-g,2-9',
len: 12, len: 12,
},
}, },
description: 'User first name', description: 'User first name',
patterns: [ patterns: [
@@ -1316,6 +1312,7 @@ export module Mock {
description: 'The age of the user', description: 'The age of the user',
warning: 'User must be at least 18.', warning: 'User must be at least 18.',
required: false, required: false,
default: null,
min: 18, min: 18,
integer: false, integer: false,
}), }),
@@ -1343,7 +1340,7 @@ export module Mock {
name: 'Preference', name: 'Preference',
description: null, description: null,
warning: null, warning: null,
required: { default: 'summer' }, default: 'summer',
}, },
ISB.Variants.of({ ISB.Variants.of({
summer: { summer: {
@@ -1351,17 +1348,14 @@ export module Mock {
spec: ISB.InputSpec.of({ spec: ISB.InputSpec.of({
'favorite-tree': ISB.Value.text({ 'favorite-tree': ISB.Value.text({
name: 'Favorite Tree', name: 'Favorite Tree',
required: { required: true,
default: 'Maple', default: 'Maple',
},
description: 'What is your favorite tree?', description: 'What is your favorite tree?',
}), }),
'favorite-flower': ISB.Value.select({ 'favorite-flower': ISB.Value.select({
name: 'Favorite Flower', name: 'Favorite Flower',
description: 'Select your favorite flower', description: 'Select your favorite flower',
required: { default: 'none',
default: 'none',
},
values: { values: {
none: 'none', none: 'none',
red: 'red', red: 'red',
@@ -1392,9 +1386,7 @@ export module Mock {
name: 'Random select', name: 'Random select',
description: 'This is not even real.', description: 'This is not even real.',
warning: 'Be careful changing this!', warning: 'Be careful changing this!',
required: { default: 'option1',
default: null,
},
values: { values: {
option1: 'option1', option1: 'option1',
option2: 'option2', option2: 'option2',
@@ -1409,9 +1401,8 @@ export module Mock {
description: 'Your favorite number of all time', description: 'Your favorite number of all time',
warning: warning:
'Once you set this number, it can never be changed without severe consequences.', 'Once you set this number, it can never be changed without severe consequences.',
required: { required: false,
default: 7, default: 7,
},
integer: false, integer: false,
units: 'BTC', units: 'BTC',
}, },
@@ -1432,11 +1423,13 @@ export module Mock {
name: 'First Law', name: 'First Law',
required: false, required: false,
description: 'the first law', description: 'the first law',
default: null,
}), }),
law2: ISB.Value.text({ law2: ISB.Value.text({
name: 'Second Law', name: 'Second Law',
required: false, required: false,
description: 'the second law', description: 'the second law',
default: null,
}), }),
}), }),
), ),
@@ -1452,19 +1445,17 @@ export module Mock {
spec: ISB.InputSpec.of({ spec: ISB.InputSpec.of({
rulemakername: ISB.Value.text({ rulemakername: ISB.Value.text({
name: 'Rulemaker Name', name: 'Rulemaker Name',
required: { required: true,
default: { default: {
charset: 'a-g,2-9', charset: 'a-g,2-9',
len: 12, len: 12,
},
}, },
description: 'the name of the rule maker', description: 'the name of the rule maker',
}), }),
rulemakerip: ISB.Value.text({ rulemakerip: ISB.Value.text({
name: 'Rulemaker IP', name: 'Rulemaker IP',
required: { required: true,
default: '192.168.1.0', default: '192.168.1.0',
},
description: 'the ip of the rule maker', description: 'the ip of the rule maker',
patterns: [ patterns: [
{ {
@@ -1480,9 +1471,8 @@ export module Mock {
), ),
rpcuser: ISB.Value.text({ rpcuser: ISB.Value.text({
name: 'RPC Username', name: 'RPC Username',
required: { required: true,
default: 'defaultrpcusername', default: 'defaultrpcusername',
},
description: 'rpc username', description: 'rpc username',
patterns: [ patterns: [
{ {
@@ -1493,11 +1483,10 @@ export module Mock {
}), }),
rpcpass: ISB.Value.text({ rpcpass: ISB.Value.text({
name: 'RPC User Password', name: 'RPC User Password',
required: { required: true,
default: { default: {
charset: 'a-z,A-Z,2-9', charset: 'a-z,A-Z,2-9',
len: 20, len: 20,
},
}, },
description: 'rpc password', description: 'rpc password',
masked: true, masked: true,
@@ -1509,7 +1498,7 @@ export module Mock {
name: 'Bitcoin Node', name: 'Bitcoin Node',
description: 'Options<ul><li>Item 1</li><li>Item 2</li></ul>', description: 'Options<ul><li>Item 1</li><li>Item 2</li></ul>',
warning: 'Careful changing this', warning: 'Careful changing this',
required: { default: 'internal' }, default: 'internal',
}, },
ISB.Variants.of({ ISB.Variants.of({
fake: { fake: {
@@ -1531,9 +1520,8 @@ export module Mock {
ISB.InputSpec.of({ ISB.InputSpec.of({
name: ISB.Value.text({ name: ISB.Value.text({
name: 'Name', name: 'Name',
required: { required: false,
default: null, default: null,
},
patterns: [ patterns: [
{ {
regex: '^[a-zA-Z]+$', regex: '^[a-zA-Z]+$',
@@ -1544,17 +1532,15 @@ export module Mock {
email: ISB.Value.text({ email: ISB.Value.text({
name: 'Email', name: 'Email',
inputmode: 'email', inputmode: 'email',
required: { required: false,
default: null, default: null,
},
}), }),
}), }),
), ),
'public-domain': ISB.Value.text({ 'public-domain': ISB.Value.text({
name: 'Public Domain', name: 'Public Domain',
required: { required: true,
default: 'bitcoinnode.com', default: 'bitcoinnode.com',
},
description: 'the public address of the node', description: 'the public address of the node',
patterns: [ patterns: [
{ {
@@ -1565,9 +1551,8 @@ export module Mock {
}), }),
'private-domain': ISB.Value.text({ 'private-domain': ISB.Value.text({
name: 'Private Domain', name: 'Private Domain',
required: { required: false,
default: null, default: null,
},
description: 'the private address of the node', description: 'the private address of the node',
masked: true, masked: true,
inputmode: 'url', inputmode: 'url',
@@ -1580,9 +1565,8 @@ export module Mock {
name: 'Port', name: 'Port',
description: description:
'the default port for your Bitcoin node. default: 8333, testnet: 18333, regtest: 18444', 'the default port for your Bitcoin node. default: 8333, testnet: 18333, regtest: 18444',
required: { required: true,
default: 8333, default: 8333,
},
min: 1, min: 1,
max: 9998, max: 9998,
step: 1, step: 1,
@@ -1595,6 +1579,7 @@ export module Mock {
len: 20, len: 20,
}, },
required: false, required: false,
default: null,
description: description:
'You most favorite slogan in the whole world, used for paying you.', 'You most favorite slogan in the whole world, used for paying you.',
masked: true, masked: true,

View File

@@ -135,7 +135,7 @@ export class FormService {
return this.formBuilder.control(value) return this.formBuilder.control(value)
case 'select': case 'select':
value = currentValue === undefined ? spec.default : currentValue value = currentValue === undefined ? spec.default : currentValue
return this.formBuilder.control(value, selectValidators(spec)) return this.formBuilder.control(value)
case 'multiselect': case 'multiselect':
value = currentValue === undefined ? spec.default : currentValue value = currentValue === undefined ? spec.default : currentValue
return this.formBuilder.control(value, multiselectValidators(spec)) return this.formBuilder.control(value, multiselectValidators(spec))
@@ -231,16 +231,6 @@ function numberValidators(spec: IST.ValueSpecNumber): ValidatorFn[] {
return validators return validators
} }
function selectValidators(spec: IST.ValueSpecSelect): ValidatorFn[] {
const validators: ValidatorFn[] = []
if (spec.required) {
validators.push(Validators.required)
}
return validators
}
function multiselectValidators(spec: IST.ValueSpecMultiselect): ValidatorFn[] { function multiselectValidators(spec: IST.ValueSpecMultiselect): ValidatorFn[] {
const validators: ValidatorFn[] = [] const validators: ValidatorFn[] = []
validators.push(listInRange(spec.minLength, spec.maxLength)) validators.push(listInRange(spec.minLength, spec.maxLength))