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": { "../sdk/dist": {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.4.0-beta.49", "version": "0.4.0-beta.50",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@iarna/toml": "^3.0.0", "@iarna/toml": "^3.0.0",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -533,7 +533,7 @@ impl RpcContext {
for (package_id, action_id) in tasks { for (package_id, action_id) in tasks {
if let Some(service) = self.services.get(&package_id).await.as_ref() { if let Some(service) = self.services.get(&package_id).await.as_ref() {
if let Some(input) = service 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 .await
.log_err() .log_err()
.flatten() .flatten()

View File

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

View File

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

View File

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

View File

@@ -534,7 +534,7 @@ impl Service {
.contains_key(&action_id)? .contains_key(&action_id)?
{ {
if let Some(input) = service 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 .await
.log_err() .log_err()
.flatten() .flatten()

View File

@@ -4,12 +4,14 @@ import { _ } from '../../../util'
import { Effects } from '../../../Effects' import { Effects } from '../../../Effects'
import { Parser, object } from 'ts-matches' import { Parser, object } from 'ts-matches'
import { DeepPartial } from '../../../types' import { DeepPartial } from '../../../types'
import { InputSpecTools, createInputSpecTools } from './inputSpecTools'
export type LazyBuildOptions = { export type LazyBuildOptions<Type> = {
effects: Effects effects: Effects
prefill: DeepPartial<Type> | null
} }
export type LazyBuild<ExpectedOut> = ( export type LazyBuild<ExpectedOut, Type> = (
options: LazyBuildOptions, options: LazyBuildOptions<Type>,
) => Promise<ExpectedOut> | ExpectedOut ) => Promise<ExpectedOut> | ExpectedOut
// prettier-ignore // prettier-ignore
@@ -29,7 +31,7 @@ export type InputSpecOf<A extends Record<string, any>> = {
[K in keyof A]: Value<A[K]> [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. * 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 * 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 _TYPE: Type = null as any as Type
public _PARTIAL: DeepPartial<Type> = null as any as DeepPartial<Type> public _PARTIAL: DeepPartial<Type> = null as any as DeepPartial<Type>
async build(options: LazyBuildOptions): Promise<{ async build<OuterType>(options: LazyBuildOptions<OuterType>): Promise<{
spec: { spec: {
[K in keyof Type]: ValueSpec [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) { static of<Spec extends Record<string, Value<any, any>>>(spec: Spec) {
const validator = object( const validator = object(
Object.fromEntries( Object.fromEntries(
@@ -129,10 +182,14 @@ export class InputSpec<
) )
return new 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) >(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' } from '../inputSpecTypes'
import { Parser, arrayOf, string } from 'ts-matches' 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( private constructor(
public build: LazyBuild<{ public build: LazyBuild<
spec: ValueSpecList {
validator: Parser<unknown, Type> spec: ValueSpecList
}>, validator: Parser<unknown, Type>
},
OuterType
>,
public readonly validator: Parser<unknown, StaticValidatedAs>, public readonly validator: Parser<unknown, StaticValidatedAs>,
) {} ) {}
readonly _TYPE: Type = null as any readonly _TYPE: Type = null as any
@@ -90,28 +97,31 @@ export class List<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
}, validator) }, validator)
} }
static dynamicText( static dynamicText<OuterType = unknown>(
getA: LazyBuild<{ getA: LazyBuild<
name: string {
description?: string | null name: string
warning?: string | null description?: string | null
default?: string[] warning?: string | null
minLength?: number | null default?: string[]
maxLength?: number | null
disabled?: false | string
generate?: null | RandomString
spec: {
masked?: boolean
placeholder?: string | null
minLength?: number | null minLength?: number | null
maxLength?: number | null maxLength?: number | null
patterns?: Pattern[] disabled?: false | string
inputmode?: ListValueSpecText['inputmode'] generate?: null | RandomString
} spec: {
}>, masked?: boolean
placeholder?: string | null
minLength?: number | null
maxLength?: number | null
patterns?: Pattern[]
inputmode?: ListValueSpecText['inputmode']
}
},
OuterType
>,
) { ) {
const validator = arrayOf(string) 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: aSpec, ...a } = await getA(options)
const spec = { const spec = {
type: 'text' as const, type: 'text' as const,

View File

@@ -32,7 +32,7 @@ export const fileInfoParser = object({
}) })
export type FileInfo = typeof fileInfoParser._TYPE 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
: T | null : T | null
@@ -47,12 +47,19 @@ function asRequiredParser<Type, Input extends { required: boolean }>(
return parser.nullable() as any 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( protected constructor(
public build: LazyBuild<{ public build: LazyBuild<
spec: ValueSpec {
validator: Parser<unknown, Type> spec: ValueSpec
}>, validator: Parser<unknown, Type>
},
OuterType
>,
public readonly validator: Parser<unknown, StaticValidatedAs>, public readonly validator: Parser<unknown, StaticValidatedAs>,
) {} ) {}
public _TYPE: Type = null as any as Type public _TYPE: Type = null as any as Type
@@ -102,17 +109,20 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
validator, validator,
) )
} }
static dynamicToggle( static dynamicToggle<OuterType = unknown>(
a: LazyBuild<{ a: LazyBuild<
name: string {
description?: string | null name: string
warning?: string | null description?: string | null
default: boolean warning?: string | null
disabled?: false | string default: boolean
}>, disabled?: false | string
},
OuterType
>,
) { ) {
const validator = boolean const validator = boolean
return new Value<boolean>( return new Value<boolean, boolean, OuterType>(
async (options) => ({ async (options) => ({
spec: { spec: {
description: null, description: null,
@@ -225,24 +235,27 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
validator, validator,
) )
} }
static dynamicText<Required extends boolean>( static dynamicText<Required extends boolean, OuterType = unknown>(
getA: LazyBuild<{ getA: LazyBuild<
name: string {
description?: string | null name: string
warning?: string | null description?: string | null
default: DefaultString | null warning?: string | null
required: Required default: DefaultString | null
masked?: boolean required: Required
placeholder?: string | null masked?: boolean
minLength?: number | null placeholder?: string | null
maxLength?: number | null minLength?: number | null
patterns?: Pattern[] maxLength?: number | null
inputmode?: ValueSpecText['inputmode'] patterns?: Pattern[]
disabled?: string | false inputmode?: ValueSpecText['inputmode']
generate?: null | RandomString 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) => { async (options) => {
const a = await getA(options) const a = await getA(options)
return { return {
@@ -342,23 +355,26 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
return { spec: built, validator } return { spec: built, validator }
}, validator) }, validator)
} }
static dynamicTextarea<Required extends boolean>( static dynamicTextarea<Required extends boolean, OuterType = unknown>(
getA: LazyBuild<{ getA: LazyBuild<
name: string {
description?: string | null name: string
warning?: string | null description?: string | null
default: string | null warning?: string | null
required: Required default: string | null
minLength?: number | null required: Required
maxLength?: number | null minLength?: number | null
patterns?: Pattern[] maxLength?: number | null
minRows?: number patterns?: Pattern[]
maxRows?: number minRows?: number
placeholder?: string | null maxRows?: number
disabled?: false | string 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) => { async (options) => {
const a = await getA(options) const a = await getA(options)
return { return {
@@ -461,23 +477,26 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
validator, validator,
) )
} }
static dynamicNumber<Required extends boolean>( static dynamicNumber<Required extends boolean, OuterType = unknown>(
getA: LazyBuild<{ getA: LazyBuild<
name: string {
description?: string | null name: string
warning?: string | null description?: string | null
default: number | null warning?: string | null
required: Required default: number | null
min?: number | null required: Required
max?: number | null min?: number | null
step?: number | null max?: number | null
integer: boolean step?: number | null
units?: string | null integer: boolean
placeholder?: string | null units?: string | null
disabled?: false | string 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) => { async (options) => {
const a = await getA(options) const a = await getA(options)
return { return {
@@ -553,17 +572,20 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
) )
} }
static dynamicColor<Required extends boolean>( static dynamicColor<Required extends boolean, OuterType = unknown>(
getA: LazyBuild<{ getA: LazyBuild<
name: string {
description?: string | null name: string
warning?: string | null description?: string | null
default: string | null warning?: string | null
required: Required default: string | null
disabled?: false | string 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) => { async (options) => {
const a = await getA(options) const a = await getA(options)
return { return {
@@ -647,20 +669,23 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
validator, validator,
) )
} }
static dynamicDatetime<Required extends boolean>( static dynamicDatetime<Required extends boolean, OuterType = unknown>(
getA: LazyBuild<{ getA: LazyBuild<
name: string {
description?: string | null name: string
warning?: string | null description?: string | null
default: string | null warning?: string | null
required: Required default: string | null
inputmode?: ValueSpecDatetime['inputmode'] required: Required
min?: string | null inputmode?: ValueSpecDatetime['inputmode']
max?: string | null min?: string | null
disabled?: false | string 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) => { async (options) => {
const a = await getA(options) const a = await getA(options)
return { return {
@@ -750,34 +775,43 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
validator, validator,
) )
} }
static dynamicSelect<Values extends Record<string, string>>( static dynamicSelect<
getA: LazyBuild<{ Values extends Record<string, string>,
name: string OuterType = unknown,
description?: string | null >(
warning?: string | null getA: LazyBuild<
default: string {
values: Values name: string
disabled?: false | string | 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>(
const a = await getA(options) async (options) => {
return { const a = await getA(options)
spec: { return {
description: null, spec: {
warning: null, description: null,
type: 'select' as const, warning: null,
disabled: false, type: 'select' as const,
immutable: false, disabled: false,
...a, immutable: false,
}, ...a,
validator: anyOf( },
...Object.keys(a.values).map((x: keyof Values & string) => validator: anyOf(
literal(x), ...Object.keys(a.values).map((x: keyof Values & string) =>
literal(x),
),
), ),
), }
} },
}, string) string,
)
} }
/** /**
* @description Displays a select modal with checkboxes, allowing for multiple selections. * @description Displays a select modal with checkboxes, allowing for multiple selections.
@@ -851,19 +885,29 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
validator, validator,
) )
} }
static dynamicMultiselect<Values extends Record<string, string>>( static dynamicMultiselect<
getA: LazyBuild<{ Values extends Record<string, string>,
name: string OuterType = unknown,
description?: string | null >(
warning?: string | null getA: LazyBuild<
default: string[] {
values: Values name: string
minLength?: number | null description?: string | null
maxLength?: number | null warning?: string | null
disabled?: false | string | string[] default: string[]
}>, values: Values
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) const a = await getA(options)
return { return {
spec: { spec: {
@@ -948,30 +992,34 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
asRequiredParser(fileInfoParser, a), asRequiredParser(fileInfoParser, a),
) )
} }
static dynamicFile<Required extends boolean>( static dynamicFile<Required extends boolean, OuterType = unknown>(
a: LazyBuild<{ a: LazyBuild<
name: string {
description?: string | null name: string
warning?: string | null description?: string | null
extensions: string[] warning?: string | null
required: Required extensions: string[]
}>, required: Required
) {
return new Value<AsRequired<FileInfo, Required>, FileInfo | null>(
async (options) => {
const spec = {
type: 'file' as const,
description: null,
warning: null,
...(await a(options)),
}
return {
spec,
validator: asRequiredParser(fileInfoParser, spec),
}
}, },
fileInfoParser.nullable(), OuterType
) >,
) {
return new Value<
AsRequired<FileInfo, Required>,
FileInfo | null,
OuterType
>(async (options) => {
const spec = {
type: 'file' as const,
description: null,
warning: null,
...(await a(options)),
}
return {
spec,
validator: asRequiredParser(fileInfoParser, spec),
}
}, fileInfoParser.nullable())
} }
/** /**
* @description Displays a dropdown, allowing for a single selection. Depending on the selection, a different object ("sub form") is presented. * @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> spec: InputSpec<any>
} }
}, },
OuterType = unknown,
>( >(
getA: LazyBuild<{ getA: LazyBuild<
name: string {
description?: string | null name: string
warning?: string | null description?: string | null
variants: Variants<VariantValues> warning?: string | null
default: keyof VariantValues & string variants: Variants<VariantValues>
disabled: string[] | false | string default: keyof VariantValues & string
}>, disabled: string[] | false | string
): Value<UnionRes<VariantValues>, unknown> },
OuterType
>,
): Value<UnionRes<VariantValues>, UnionRes<VariantValues>, OuterType>
static dynamicUnion< static dynamicUnion<
VariantValues extends StaticVariantValues,
StaticVariantValues extends { StaticVariantValues extends {
[K in string]: { [K in string]: {
name: string name: string
spec: InputSpec<any, any> spec: InputSpec<any, any>
} }
}, },
VariantValues extends StaticVariantValues,
OuterType = unknown,
>( >(
getA: LazyBuild<{ getA: LazyBuild<
name: string {
description?: string | null name: string
warning?: string | null description?: string | null
variants: Variants<VariantValues> warning?: string | null
default: keyof VariantValues & string variants: Variants<VariantValues>
disabled: string[] | false | string default: keyof VariantValues & string
}>, disabled: string[] | false | string
},
OuterType
>,
validator: Parser<unknown, UnionResStaticValidatedAs<StaticVariantValues>>, validator: Parser<unknown, UnionResStaticValidatedAs<StaticVariantValues>>,
): Value< ): Value<
UnionRes<VariantValues>, UnionRes<VariantValues>,
UnionResStaticValidatedAs<StaticVariantValues> UnionResStaticValidatedAs<StaticVariantValues>,
OuterType
> >
static dynamicUnion< static dynamicUnion<
VariantValues extends { VariantValues extends {
@@ -1092,35 +1149,40 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
spec: InputSpec<any> spec: InputSpec<any>
} }
}, },
OuterType = unknown,
>( >(
getA: LazyBuild<{ getA: LazyBuild<
name: string {
description?: string | null name: string
warning?: string | null description?: string | null
variants: Variants<VariantValues> warning?: string | null
default: keyof VariantValues & string variants: Variants<VariantValues>
disabled: string[] | false | string default: keyof VariantValues & string
}>, disabled: string[] | false | string
},
OuterType
>,
validator: Parser<unknown, unknown> = any, validator: Parser<unknown, unknown> = any,
) { ) {
return new Value<UnionRes<VariantValues>, typeof validator._TYPE>( return new Value<
async (options) => { UnionRes<VariantValues>,
const newValues = await getA(options) typeof validator._TYPE,
const built = await newValues.variants.build(options as any) OuterType
return { >(async (options) => {
spec: { const newValues = await getA(options)
type: 'union' as const, const built = await newValues.variants.build(options as any)
description: null, return {
warning: null, spec: {
...newValues, type: 'union' as const,
variants: built.spec, description: null,
immutable: false, warning: null,
}, ...newValues,
validator: built.validator, variants: built.spec,
} immutable: false,
}, },
validator, validator: built.validator,
) }
}, validator)
} }
/** /**
* @description Presents an interface to add/remove/edit items in a list. * @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(), 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>): Value<T>
static hidden<T>(parser: Parser<unknown, T> = any) { static hidden<T>(parser: Parser<unknown, T> = any) {
return new Value<T, typeof parser._TYPE>(async () => { return new Value<T, typeof parser._TYPE>(async () => {
@@ -1216,8 +1278,10 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
hiddenExample: Value.hidden(), hiddenExample: Value.hidden(),
* ``` * ```
*/ */
static dynamicHidden<T>(getParser: LazyBuild<Parser<unknown, T>>) { static dynamicHidden<T, OuterType = unknown>(
return new Value<T, unknown>(async (options) => { getParser: LazyBuild<Parser<unknown, T>, OuterType>,
) {
return new Value<T, T, OuterType>(async (options) => {
const validator = await getParser(options) const validator = await getParser(options)
return { return {
spec: { spec: {
@@ -1228,9 +1292,9 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
}, any) }, any)
} }
map<U>(fn: (value: StaticValidatedAs) => U): Value<U> { map<U>(fn: (value: StaticValidatedAs) => U): Value<U, U, OuterType> {
return new Value(async (effects) => { return new Value<U, U, OuterType>(async (options) => {
const built = await this.build(effects) const built = await this.build(options)
return { return {
spec: built.spec, spec: built.spec,
validator: built.validator.map(fn), validator: built.validator.map(fn),

View File

@@ -103,12 +103,16 @@ export class Variants<
spec: InputSpec<any, any> spec: InputSpec<any, any>
} }
}, },
OuterType = unknown,
> { > {
private constructor( private constructor(
public build: LazyBuild<{ public build: LazyBuild<
spec: ValueSpecUnion['variants'] {
validator: Parser<unknown, UnionRes<VariantValues>> spec: ValueSpecUnion['variants']
}>, validator: Parser<unknown, UnionRes<VariantValues>>
},
OuterType
>,
public readonly validator: Parser< public readonly validator: Parser<
unknown, unknown,
UnionResStaticValidatedAs<VariantValues> 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> }) => Promise<(T.ActionResult & { version: '1' }) | null | void | undefined>
export type GetInput<A extends Record<string, any>> = (options: { export type GetInput<A extends Record<string, any>> = (options: {
effects: T.Effects effects: T.Effects
prefill: T.DeepPartial<A> | null
}) => Promise<null | void | undefined | T.DeepPartial<A>> }) => Promise<null | void | undefined | T.DeepPartial<A>>
export type MaybeFn<T> = T | ((options: { effects: T.Effects }) => Promise<T>) 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 }) await options.effects.action.export({ id: this.id, metadata })
return 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 = {} let spec = {}
if (this.inputSpec) { if (this.inputSpec) {
const built = await this.inputSpec.build(options) const built = await this.inputSpec.build(options)

View File

@@ -2,4 +2,8 @@
import type { ActionId } from './ActionId' import type { ActionId } from './ActionId'
import type { PackageId } from './PackageId' 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", "name": "@start9labs/start-sdk",
"version": "0.4.0-beta.49", "version": "0.4.0-beta.50",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.4.0-beta.49", "version": "0.4.0-beta.50",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@iarna/toml": "^3.0.0", "@iarna/toml": "^3.0.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@start9labs/start-sdk", "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", "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",

View File

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