feat: builder-style InputSpec API, prefill plumbing, and port forward fix

- Add addKey() and add() builder methods to InputSpec with InputSpecTools
- Move OuterType to last generic param on Value, List, and all dynamic methods
- Plumb prefill through getActionInput end-to-end (core → container-runtime → SDK)
- Filter port_forwards to enabled addresses only
- Bump SDK to 0.4.0-beta.50
This commit is contained in:
Aiden McClelland
2026-02-19 16:44:44 -07:00
parent 4527046f2e
commit 7909941b70
21 changed files with 688 additions and 250 deletions

View File

@@ -38,7 +38,7 @@
},
"../sdk/dist": {
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.49",
"version": "0.4.0-beta.50",
"license": "MIT",
"dependencies": {
"@iarna/toml": "^3.0.0",

View File

@@ -437,6 +437,7 @@ export class RpcListener {
return system.getActionInput(
effects,
procedures[2],
input?.prefill ?? null,
timeout || null,
)
case procedures[1] === "actions" && procedures[3] === "run":

View File

@@ -510,6 +510,7 @@ export class SystemForEmbassy implements System {
async getActionInput(
effects: Effects,
actionId: string,
_prefill: Record<string, unknown> | null,
timeoutMs: number | null,
): Promise<T.ActionInput | null> {
if (actionId === "config") {

View File

@@ -47,11 +47,12 @@ export class SystemForStartOs implements System {
getActionInput(
effects: Effects,
id: string,
prefill: Record<string, unknown> | null,
timeoutMs: number | null,
): Promise<T.ActionInput | null> {
const action = this.abi.actions.get(id)
if (!action) throw new Error(`Action ${id} not found`)
return action.getInput({ effects })
return action.getInput({ effects, prefill })
}
runAction(
effects: Effects,

View File

@@ -33,6 +33,7 @@ export type System = {
getActionInput(
effects: Effects,
actionId: string,
prefill: Record<string, unknown> | null,
timeoutMs: number | null,
): Promise<T.ActionInput | null>

View File

@@ -67,6 +67,10 @@ pub struct GetActionInputParams {
pub package_id: PackageId,
#[arg(help = "help.arg.action-id")]
pub action_id: ActionId,
#[ts(type = "Record<string, unknown> | null")]
#[serde(default)]
#[arg(skip)]
pub prefill: Option<Value>,
}
#[instrument(skip_all)]
@@ -75,6 +79,7 @@ pub async fn get_action_input(
GetActionInputParams {
package_id,
action_id,
prefill,
}: GetActionInputParams,
) -> Result<Option<ActionInput>, Error> {
ctx.services
@@ -82,7 +87,7 @@ pub async fn get_action_input(
.await
.as_ref()
.or_not_found(lazy_format!("Manager for {}", package_id))?
.get_action_input(Guid::new(), action_id)
.get_action_input(Guid::new(), action_id, prefill.unwrap_or(Value::Null))
.await
}

View File

@@ -533,7 +533,7 @@ impl RpcContext {
for (package_id, action_id) in tasks {
if let Some(service) = self.services.get(&package_id).await.as_ref() {
if let Some(input) = service
.get_action_input(procedure_id.clone(), action_id.clone())
.get_action_input(procedure_id.clone(), action_id.clone(), Value::Null)
.await
.log_err()
.flatten()

View File

@@ -298,7 +298,7 @@ impl Model<Host> {
let bindings: Bindings = this.bindings.de()?;
let mut port_forwards = BTreeSet::new();
for bind in bindings.values() {
for addr in &bind.addresses.available {
for addr in bind.addresses.enabled() {
if !addr.public {
continue;
}

View File

@@ -17,6 +17,7 @@ use crate::{ActionId, PackageId, ReplayId};
pub(super) struct GetActionInput {
id: ActionId,
prefill: Value,
}
impl Handler<GetActionInput> for ServiceActor {
type Response = Result<Option<ActionInput>, Error>;
@@ -26,7 +27,10 @@ impl Handler<GetActionInput> for ServiceActor {
async fn handle(
&mut self,
id: Guid,
GetActionInput { id: action_id }: GetActionInput,
GetActionInput {
id: action_id,
prefill,
}: GetActionInput,
_: &BackgroundJobQueue,
) -> Self::Response {
let container = &self.0.persistent_container;
@@ -34,7 +38,7 @@ impl Handler<GetActionInput> for ServiceActor {
.execute::<Option<ActionInput>>(
id,
ProcedureName::GetActionInput(action_id),
Value::Null,
json!({ "prefill": prefill }),
Some(Duration::from_secs(30)),
)
.await
@@ -47,6 +51,7 @@ impl Service {
&self,
id: Guid,
action_id: ActionId,
prefill: Value,
) -> Result<Option<ActionInput>, Error> {
if !self
.seed
@@ -67,7 +72,7 @@ impl Service {
return Ok(None);
}
self.actor
.send(id, GetActionInput { id: action_id })
.send(id, GetActionInput { id: action_id, prefill })
.await?
}
}

View File

@@ -122,6 +122,10 @@ pub struct GetActionInputParams {
package_id: Option<PackageId>,
#[arg(help = "help.arg.action-id")]
action_id: ActionId,
#[ts(type = "Record<string, unknown> | null")]
#[serde(default)]
#[arg(skip)]
prefill: Option<Value>,
}
async fn get_action_input(
context: EffectContext,
@@ -129,9 +133,11 @@ async fn get_action_input(
procedure_id,
package_id,
action_id,
prefill,
}: GetActionInputParams,
) -> Result<Option<ActionInput>, Error> {
let context = context.deref()?;
let prefill = prefill.unwrap_or(Value::Null);
if let Some(package_id) = package_id {
context
@@ -142,10 +148,10 @@ async fn get_action_input(
.await
.as_ref()
.or_not_found(&package_id)?
.get_action_input(procedure_id, action_id)
.get_action_input(procedure_id, action_id, prefill)
.await
} else {
context.get_action_input(procedure_id, action_id).await
context.get_action_input(procedure_id, action_id, prefill).await
}
}
@@ -245,7 +251,7 @@ async fn create_task(
.as_ref()
{
let Some(prev) = service
.get_action_input(procedure_id.clone(), task.action_id.clone())
.get_action_input(procedure_id.clone(), task.action_id.clone(), Value::Null)
.await?
else {
return Err(Error::new(

View File

@@ -534,7 +534,7 @@ impl Service {
.contains_key(&action_id)?
{
if let Some(input) = service
.get_action_input(procedure_id.clone(), action_id.clone())
.get_action_input(procedure_id.clone(), action_id.clone(), Value::Null)
.await
.log_err()
.flatten()

View File

@@ -4,12 +4,14 @@ import { _ } from '../../../util'
import { Effects } from '../../../Effects'
import { Parser, object } from 'ts-matches'
import { DeepPartial } from '../../../types'
import { InputSpecTools, createInputSpecTools } from './inputSpecTools'
export type LazyBuildOptions = {
export type LazyBuildOptions<Type> = {
effects: Effects
prefill: DeepPartial<Type> | null
}
export type LazyBuild<ExpectedOut> = (
options: LazyBuildOptions,
export type LazyBuild<ExpectedOut, Type> = (
options: LazyBuildOptions<Type>,
) => Promise<ExpectedOut> | ExpectedOut
// prettier-ignore
@@ -29,7 +31,7 @@ export type InputSpecOf<A extends Record<string, any>> = {
[K in keyof A]: Value<A[K]>
}
export type MaybeLazyValues<A> = LazyBuild<A> | A
export type MaybeLazyValues<A, T> = LazyBuild<A, T> | A
/**
* InputSpecs are the specs that are used by the os input specification form for this service.
* Here is an example of a simple input specification
@@ -98,7 +100,7 @@ export class InputSpec<
) {}
public _TYPE: Type = null as any as Type
public _PARTIAL: DeepPartial<Type> = null as any as DeepPartial<Type>
async build(options: LazyBuildOptions): Promise<{
async build<OuterType>(options: LazyBuildOptions<OuterType>): Promise<{
spec: {
[K in keyof Type]: ValueSpec
}
@@ -121,6 +123,57 @@ export class InputSpec<
}
}
addKey<Key extends string, V extends Value<any, any, any>>(
key: Key,
build: V | ((tools: InputSpecTools<Type>) => V),
): InputSpec<
Type & { [K in Key]: V extends Value<infer T, any, any> ? T : never },
StaticValidatedAs & {
[K in Key]: V extends Value<any, infer S, any> ? S : never
}
> {
const value =
build instanceof Function ? build(createInputSpecTools<Type>()) : build
const newSpec = { ...this.spec, [key]: value } as any
const newValidator = object(
Object.fromEntries(
Object.entries(newSpec).map(([k, v]) => [
k,
(v as Value<any>).validator,
]),
),
)
return new InputSpec(newSpec, newValidator as any)
}
add<AddSpec extends Record<string, Value<any, any, any>>>(
build: AddSpec | ((tools: InputSpecTools<Type>) => AddSpec),
): InputSpec<
Type & {
[K in keyof AddSpec]: AddSpec[K] extends Value<infer T, any, any>
? T
: never
},
StaticValidatedAs & {
[K in keyof AddSpec]: AddSpec[K] extends Value<any, infer S, any>
? S
: never
}
> {
const addedValues =
build instanceof Function ? build(createInputSpecTools<Type>()) : build
const newSpec = { ...this.spec, ...addedValues } as any
const newValidator = object(
Object.fromEntries(
Object.entries(newSpec).map(([k, v]) => [
k,
(v as Value<any>).validator,
]),
),
)
return new InputSpec(newSpec, newValidator as any)
}
static of<Spec extends Record<string, Value<any, any>>>(spec: Spec) {
const validator = object(
Object.fromEntries(
@@ -129,10 +182,14 @@ export class InputSpec<
)
return new InputSpec<
{
[K in keyof Spec]: Spec[K] extends Value<infer T, any> ? T : never
[K in keyof Spec]: Spec[K] extends Value<infer T, any, unknown>
? T
: never
},
{
[K in keyof Spec]: Spec[K] extends Value<any, infer T> ? T : never
[K in keyof Spec]: Spec[K] extends Value<any, infer T, unknown>
? T
: never
}
>(spec, validator as any)
}

View File

@@ -0,0 +1,274 @@
import { InputSpec, LazyBuild } from './inputSpec'
import { AsRequired, FileInfo, Value } from './value'
import { List } from './list'
import { UnionRes, UnionResStaticValidatedAs, Variants } from './variants'
import {
Pattern,
RandomString,
ValueSpecDatetime,
ValueSpecText,
} from '../inputSpecTypes'
import { DefaultString } from '../inputSpecTypes'
import { Parser } from 'ts-matches'
import { ListValueSpecText } from '../inputSpecTypes'
export interface InputSpecTools<OuterType> {
Value: BoundValue<OuterType>
Variants: typeof Variants
InputSpec: typeof InputSpec
List: BoundList<OuterType>
}
export interface BoundValue<OuterType> {
// Static (non-dynamic) methods — no OuterType involved
toggle: typeof Value.toggle
text: typeof Value.text
textarea: typeof Value.textarea
number: typeof Value.number
color: typeof Value.color
datetime: typeof Value.datetime
select: typeof Value.select
multiselect: typeof Value.multiselect
object: typeof Value.object
file: typeof Value.file
list: typeof Value.list
hidden: typeof Value.hidden
union: typeof Value.union
// Dynamic methods with OuterType pre-bound (last generic param removed)
dynamicToggle(
a: LazyBuild<
{
name: string
description?: string | null
warning?: string | null
default: boolean
disabled?: false | string
},
OuterType
>,
): Value<boolean, boolean, OuterType>
dynamicText<Required extends boolean>(
getA: LazyBuild<
{
name: string
description?: string | null
warning?: string | null
default: DefaultString | null
required: Required
masked?: boolean
placeholder?: string | null
minLength?: number | null
maxLength?: number | null
patterns?: Pattern[]
inputmode?: ValueSpecText['inputmode']
disabled?: string | false
generate?: null | RandomString
},
OuterType
>,
): Value<AsRequired<string, Required>, string | null, OuterType>
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
patterns?: Pattern[]
minRows?: number
maxRows?: number
placeholder?: string | null
disabled?: false | string
},
OuterType
>,
): Value<AsRequired<string, Required>, string | null, OuterType>
dynamicNumber<Required extends boolean>(
getA: LazyBuild<
{
name: string
description?: string | null
warning?: string | null
default: number | null
required: Required
min?: number | null
max?: number | null
step?: number | null
integer: boolean
units?: string | null
placeholder?: string | null
disabled?: false | string
},
OuterType
>,
): Value<AsRequired<number, Required>, number | null, OuterType>
dynamicColor<Required extends boolean>(
getA: LazyBuild<
{
name: string
description?: string | null
warning?: string | null
default: string | null
required: Required
disabled?: false | string
},
OuterType
>,
): Value<AsRequired<string, Required>, string | null, OuterType>
dynamicDatetime<Required extends boolean>(
getA: LazyBuild<
{
name: string
description?: string | null
warning?: string | null
default: string | null
required: Required
inputmode?: ValueSpecDatetime['inputmode']
min?: string | null
max?: string | null
disabled?: false | string
},
OuterType
>,
): Value<AsRequired<string, Required>, string | null, OuterType>
dynamicSelect<Values extends Record<string, string>>(
getA: LazyBuild<
{
name: string
description?: string | null
warning?: string | null
default: string
values: Values
disabled?: false | string | string[]
},
OuterType
>,
): Value<keyof Values & string, keyof Values & string, OuterType>
dynamicMultiselect<Values extends Record<string, string>>(
getA: LazyBuild<
{
name: string
description?: string | null
warning?: string | null
default: string[]
values: Values
minLength?: number | null
maxLength?: number | null
disabled?: false | string | string[]
},
OuterType
>,
): Value<(keyof Values & string)[], (keyof Values & string)[], OuterType>
dynamicFile<Required extends boolean>(
a: LazyBuild<
{
name: string
description?: string | null
warning?: string | null
extensions: string[]
required: Required
},
OuterType
>,
): Value<AsRequired<FileInfo, Required>, FileInfo | null, OuterType>
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
},
OuterType
>,
): Value<UnionRes<VariantValues>, UnionRes<VariantValues>, OuterType>
dynamicUnion<
StaticVariantValues extends {
[K in string]: {
name: string
spec: InputSpec<any, any>
}
},
VariantValues extends StaticVariantValues,
>(
getA: LazyBuild<
{
name: string
description?: string | null
warning?: string | null
variants: Variants<VariantValues>
default: keyof VariantValues & string
disabled: string[] | false | string
},
OuterType
>,
validator: Parser<unknown, UnionResStaticValidatedAs<StaticVariantValues>>,
): Value<
UnionRes<VariantValues>,
UnionResStaticValidatedAs<StaticVariantValues>,
OuterType
>
dynamicHidden<T>(
getParser: LazyBuild<Parser<unknown, T>, OuterType>,
): Value<T, T, OuterType>
}
export interface BoundList<OuterType> {
text: typeof List.text
obj: typeof List.obj
dynamicText(
getA: LazyBuild<
{
name: string
description?: string | null
warning?: string | null
default?: string[]
minLength?: number | null
maxLength?: number | null
disabled?: false | string
generate?: null | RandomString
spec: {
masked?: boolean
placeholder?: string | null
minLength?: number | null
maxLength?: number | null
patterns?: Pattern[]
inputmode?: ListValueSpecText['inputmode']
}
},
OuterType
>,
): List<string[], string[], OuterType>
}
export function createInputSpecTools<OuterType>(): InputSpecTools<OuterType> {
return {
Value: Value as any as BoundValue<OuterType>,
Variants,
InputSpec,
List: List as any as BoundList<OuterType>,
}
}

View File

@@ -9,12 +9,19 @@ import {
} from '../inputSpecTypes'
import { Parser, arrayOf, string } from 'ts-matches'
export class List<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
export class List<
Type extends StaticValidatedAs,
StaticValidatedAs = Type,
OuterType = unknown,
> {
private constructor(
public build: LazyBuild<{
public build: LazyBuild<
{
spec: ValueSpecList
validator: Parser<unknown, Type>
}>,
},
OuterType
>,
public readonly validator: Parser<unknown, StaticValidatedAs>,
) {}
readonly _TYPE: Type = null as any
@@ -90,8 +97,9 @@ export class List<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
}, validator)
}
static dynamicText(
getA: LazyBuild<{
static dynamicText<OuterType = unknown>(
getA: LazyBuild<
{
name: string
description?: string | null
warning?: string | null
@@ -108,10 +116,12 @@ export class List<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
patterns?: Pattern[]
inputmode?: ListValueSpecText['inputmode']
}
}>,
},
OuterType
>,
) {
const validator = arrayOf(string)
return new List<string[]>(async (options) => {
return new List<string[], string[], OuterType>(async (options) => {
const { spec: aSpec, ...a } = await getA(options)
const spec = {
type: 'text' as const,

View File

@@ -32,7 +32,7 @@ export const fileInfoParser = object({
})
export type FileInfo = typeof fileInfoParser._TYPE
type AsRequired<T, Required extends boolean> = Required extends true
export type AsRequired<T, Required extends boolean> = Required extends true
? T
: T | null
@@ -47,12 +47,19 @@ function asRequiredParser<Type, Input extends { required: boolean }>(
return parser.nullable() as any
}
export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
export class Value<
Type extends StaticValidatedAs,
StaticValidatedAs = Type,
OuterType = unknown,
> {
protected constructor(
public build: LazyBuild<{
public build: LazyBuild<
{
spec: ValueSpec
validator: Parser<unknown, Type>
}>,
},
OuterType
>,
public readonly validator: Parser<unknown, StaticValidatedAs>,
) {}
public _TYPE: Type = null as any as Type
@@ -102,17 +109,20 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
validator,
)
}
static dynamicToggle(
a: LazyBuild<{
static dynamicToggle<OuterType = unknown>(
a: LazyBuild<
{
name: string
description?: string | null
warning?: string | null
default: boolean
disabled?: false | string
}>,
},
OuterType
>,
) {
const validator = boolean
return new Value<boolean>(
return new Value<boolean, boolean, OuterType>(
async (options) => ({
spec: {
description: null,
@@ -225,8 +235,9 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
validator,
)
}
static dynamicText<Required extends boolean>(
getA: LazyBuild<{
static dynamicText<Required extends boolean, OuterType = unknown>(
getA: LazyBuild<
{
name: string
description?: string | null
warning?: string | null
@@ -240,9 +251,11 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
inputmode?: ValueSpecText['inputmode']
disabled?: string | false
generate?: null | RandomString
}>,
},
OuterType
>,
) {
return new Value<AsRequired<string, Required>, string | null>(
return new Value<AsRequired<string, Required>, string | null, OuterType>(
async (options) => {
const a = await getA(options)
return {
@@ -342,8 +355,9 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
return { spec: built, validator }
}, validator)
}
static dynamicTextarea<Required extends boolean>(
getA: LazyBuild<{
static dynamicTextarea<Required extends boolean, OuterType = unknown>(
getA: LazyBuild<
{
name: string
description?: string | null
warning?: string | null
@@ -356,9 +370,11 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
maxRows?: number
placeholder?: string | null
disabled?: false | string
}>,
},
OuterType
>,
) {
return new Value<AsRequired<string, Required>, string | null>(
return new Value<AsRequired<string, Required>, string | null, OuterType>(
async (options) => {
const a = await getA(options)
return {
@@ -461,8 +477,9 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
validator,
)
}
static dynamicNumber<Required extends boolean>(
getA: LazyBuild<{
static dynamicNumber<Required extends boolean, OuterType = unknown>(
getA: LazyBuild<
{
name: string
description?: string | null
warning?: string | null
@@ -475,9 +492,11 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
units?: string | null
placeholder?: string | null
disabled?: false | string
}>,
},
OuterType
>,
) {
return new Value<AsRequired<number, Required>, number | null>(
return new Value<AsRequired<number, Required>, number | null, OuterType>(
async (options) => {
const a = await getA(options)
return {
@@ -553,17 +572,20 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
)
}
static dynamicColor<Required extends boolean>(
getA: LazyBuild<{
static dynamicColor<Required extends boolean, OuterType = unknown>(
getA: LazyBuild<
{
name: string
description?: string | null
warning?: string | null
default: string | null
required: Required
disabled?: false | string
}>,
},
OuterType
>,
) {
return new Value<AsRequired<string, Required>, string | null>(
return new Value<AsRequired<string, Required>, string | null, OuterType>(
async (options) => {
const a = await getA(options)
return {
@@ -647,8 +669,9 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
validator,
)
}
static dynamicDatetime<Required extends boolean>(
getA: LazyBuild<{
static dynamicDatetime<Required extends boolean, OuterType = unknown>(
getA: LazyBuild<
{
name: string
description?: string | null
warning?: string | null
@@ -658,9 +681,11 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
min?: string | null
max?: string | null
disabled?: false | string
}>,
},
OuterType
>,
) {
return new Value<AsRequired<string, Required>, string | null>(
return new Value<AsRequired<string, Required>, string | null, OuterType>(
async (options) => {
const a = await getA(options)
return {
@@ -750,17 +775,24 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
validator,
)
}
static dynamicSelect<Values extends Record<string, string>>(
getA: LazyBuild<{
static dynamicSelect<
Values extends Record<string, string>,
OuterType = unknown,
>(
getA: LazyBuild<
{
name: string
description?: string | null
warning?: string | null
default: string
values: Values
disabled?: false | string | string[]
}>,
},
OuterType
>,
) {
return new Value<keyof Values & string, string>(async (options) => {
return new Value<keyof Values & string, keyof Values & string, OuterType>(
async (options) => {
const a = await getA(options)
return {
spec: {
@@ -777,7 +809,9 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
),
),
}
}, string)
},
string,
)
}
/**
* @description Displays a select modal with checkboxes, allowing for multiple selections.
@@ -851,8 +885,12 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
validator,
)
}
static dynamicMultiselect<Values extends Record<string, string>>(
getA: LazyBuild<{
static dynamicMultiselect<
Values extends Record<string, string>,
OuterType = unknown,
>(
getA: LazyBuild<
{
name: string
description?: string | null
warning?: string | null
@@ -861,9 +899,15 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
minLength?: number | null
maxLength?: number | null
disabled?: false | string | string[]
}>,
},
OuterType
>,
) {
return new Value<(keyof Values & string)[], string[]>(async (options) => {
return new Value<
(keyof Values & string)[],
(keyof Values & string)[],
OuterType
>(async (options) => {
const a = await getA(options)
return {
spec: {
@@ -948,17 +992,23 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
asRequiredParser(fileInfoParser, a),
)
}
static dynamicFile<Required extends boolean>(
a: LazyBuild<{
static dynamicFile<Required extends boolean, OuterType = unknown>(
a: LazyBuild<
{
name: string
description?: string | null
warning?: string | null
extensions: string[]
required: Required
}>,
},
OuterType
>,
) {
return new Value<AsRequired<FileInfo, Required>, FileInfo | null>(
async (options) => {
return new Value<
AsRequired<FileInfo, Required>,
FileInfo | null,
OuterType
>(async (options) => {
const spec = {
type: 'file' as const,
description: null,
@@ -969,9 +1019,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
spec,
validator: asRequiredParser(fileInfoParser, spec),
}
},
fileInfoParser.nullable(),
)
}, fileInfoParser.nullable())
}
/**
* @description Displays a dropdown, allowing for a single selection. Depending on the selection, a different object ("sub form") is presented.
@@ -1053,37 +1101,46 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
spec: InputSpec<any>
}
},
OuterType = unknown,
>(
getA: LazyBuild<{
getA: LazyBuild<
{
name: string
description?: string | null
warning?: string | null
variants: Variants<VariantValues>
default: keyof VariantValues & string
disabled: string[] | false | string
}>,
): Value<UnionRes<VariantValues>, unknown>
},
OuterType
>,
): Value<UnionRes<VariantValues>, UnionRes<VariantValues>, OuterType>
static dynamicUnion<
VariantValues extends StaticVariantValues,
StaticVariantValues extends {
[K in string]: {
name: string
spec: InputSpec<any, any>
}
},
VariantValues extends StaticVariantValues,
OuterType = unknown,
>(
getA: LazyBuild<{
getA: LazyBuild<
{
name: string
description?: string | null
warning?: string | null
variants: Variants<VariantValues>
default: keyof VariantValues & string
disabled: string[] | false | string
}>,
},
OuterType
>,
validator: Parser<unknown, UnionResStaticValidatedAs<StaticVariantValues>>,
): Value<
UnionRes<VariantValues>,
UnionResStaticValidatedAs<StaticVariantValues>
UnionResStaticValidatedAs<StaticVariantValues>,
OuterType
>
static dynamicUnion<
VariantValues extends {
@@ -1092,19 +1149,26 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
spec: InputSpec<any>
}
},
OuterType = unknown,
>(
getA: LazyBuild<{
getA: LazyBuild<
{
name: string
description?: string | null
warning?: string | null
variants: Variants<VariantValues>
default: keyof VariantValues & string
disabled: string[] | false | string
}>,
},
OuterType
>,
validator: Parser<unknown, unknown> = any,
) {
return new Value<UnionRes<VariantValues>, typeof validator._TYPE>(
async (options) => {
return new Value<
UnionRes<VariantValues>,
typeof validator._TYPE,
OuterType
>(async (options) => {
const newValues = await getA(options)
const built = await newValues.variants.build(options as any)
return {
@@ -1118,9 +1182,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
},
validator: built.validator,
}
},
validator,
)
}, validator)
}
/**
* @description Presents an interface to add/remove/edit items in a list.
@@ -1196,7 +1258,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
hiddenExample: Value.hidden(),
* ```
*/
static hidden<T>(): Value<T, unknown>
static hidden<T>(): Value<T>
static hidden<T>(parser: Parser<unknown, T>): Value<T>
static hidden<T>(parser: Parser<unknown, T> = any) {
return new Value<T, typeof parser._TYPE>(async () => {
@@ -1216,8 +1278,10 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
hiddenExample: Value.hidden(),
* ```
*/
static dynamicHidden<T>(getParser: LazyBuild<Parser<unknown, T>>) {
return new Value<T, unknown>(async (options) => {
static dynamicHidden<T, OuterType = unknown>(
getParser: LazyBuild<Parser<unknown, T>, OuterType>,
) {
return new Value<T, T, OuterType>(async (options) => {
const validator = await getParser(options)
return {
spec: {
@@ -1228,9 +1292,9 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
}, any)
}
map<U>(fn: (value: StaticValidatedAs) => U): Value<U> {
return new Value(async (effects) => {
const built = await this.build(effects)
map<U>(fn: (value: StaticValidatedAs) => U): Value<U, U, OuterType> {
return new Value<U, U, OuterType>(async (options) => {
const built = await this.build(options)
return {
spec: built.spec,
validator: built.validator.map(fn),

View File

@@ -103,12 +103,16 @@ export class Variants<
spec: InputSpec<any, any>
}
},
OuterType = unknown,
> {
private constructor(
public build: LazyBuild<{
public build: LazyBuild<
{
spec: ValueSpecUnion['variants']
validator: Parser<unknown, UnionRes<VariantValues>>
}>,
},
OuterType
>,
public readonly validator: Parser<
unknown,
UnionResStaticValidatedAs<VariantValues>

View File

@@ -13,6 +13,7 @@ export type Run<A extends Record<string, any>> = (options: {
}) => Promise<(T.ActionResult & { version: '1' }) | null | void | undefined>
export type GetInput<A extends Record<string, any>> = (options: {
effects: T.Effects
prefill: T.DeepPartial<A> | null
}) => Promise<null | void | undefined | T.DeepPartial<A>>
export type MaybeFn<T> = T | ((options: { effects: T.Effects }) => Promise<T>)
@@ -104,7 +105,10 @@ export class Action<Id extends T.ActionId, Type extends Record<string, any>>
await options.effects.action.export({ id: this.id, metadata })
return metadata
}
async getInput(options: { effects: T.Effects }): Promise<T.ActionInput> {
async getInput(options: {
effects: T.Effects
prefill: T.DeepPartial<Type> | null
}): Promise<T.ActionInput> {
let spec = {}
if (this.inputSpec) {
const built = await this.inputSpec.build(options)

View File

@@ -2,4 +2,8 @@
import type { ActionId } from './ActionId'
import type { PackageId } from './PackageId'
export type GetActionInputParams = { packageId?: PackageId; actionId: ActionId }
export type GetActionInputParams = {
packageId?: PackageId
actionId: ActionId
prefill: Record<string, unknown> | null
}

View File

@@ -1,12 +1,12 @@
{
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.49",
"version": "0.4.0-beta.50",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.49",
"version": "0.4.0-beta.50",
"license": "MIT",
"dependencies": {
"@iarna/toml": "^3.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.49",
"version": "0.4.0-beta.50",
"description": "Software development kit to facilitate packaging services for StartOS",
"main": "./package/lib/index.js",
"types": "./package/lib/index.d.ts",

View File

@@ -148,6 +148,7 @@ export class ActionInputModal {
this.api.getActionInput({
packageId: this.pkgInfo.id,
actionId: this.actionId,
prefill: this.context.data.prefill ?? null,
}),
).pipe(
map(res => {