mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-04 22:39:46 +00:00
sdk comments
This commit is contained in:
@@ -1,4 +1,40 @@
|
||||
/**
|
||||
* @module inputSpecTypes
|
||||
*
|
||||
* This module defines the type specifications for action input form fields.
|
||||
* These types describe the shape of form fields that appear in the StartOS UI
|
||||
* when users interact with service actions.
|
||||
*
|
||||
* Developers typically don't create these types directly - instead, use the
|
||||
* `Value` class methods (e.g., `Value.text()`, `Value.select()`) which generate
|
||||
* these specifications with proper defaults and validation.
|
||||
*
|
||||
* @see {@link Value} for the builder API
|
||||
*/
|
||||
|
||||
/**
|
||||
* A complete input specification - a record mapping field names to their specifications.
|
||||
* This is the top-level type for an action's input form.
|
||||
*/
|
||||
export type InputSpec = Record<string, ValueSpec>
|
||||
|
||||
/**
|
||||
* All available input field types.
|
||||
*
|
||||
* - `text` - Single-line text input
|
||||
* - `textarea` - Multi-line text input
|
||||
* - `number` - Numeric input with optional min/max/step
|
||||
* - `color` - Color picker
|
||||
* - `datetime` - Date and/or time picker
|
||||
* - `toggle` - Boolean on/off switch
|
||||
* - `select` - Single-selection dropdown/radio
|
||||
* - `multiselect` - Multiple-selection checkboxes
|
||||
* - `list` - Dynamic list of items (text or objects)
|
||||
* - `object` - Nested group of fields (sub-form)
|
||||
* - `file` - File upload
|
||||
* - `union` - Conditional fields based on selection (discriminated union)
|
||||
* - `hidden` - Hidden field (not displayed to user)
|
||||
*/
|
||||
export type ValueType =
|
||||
| "text"
|
||||
| "textarea"
|
||||
@@ -13,188 +49,369 @@ export type ValueType =
|
||||
| "file"
|
||||
| "union"
|
||||
| "hidden"
|
||||
|
||||
/** Union type of all possible value specifications */
|
||||
export type ValueSpec = ValueSpecOf<ValueType>
|
||||
/** core spec types. These types provide the metadata for performing validations */
|
||||
|
||||
/**
|
||||
* Maps a ValueType to its corresponding specification type.
|
||||
* Core spec types that provide metadata for validation and UI rendering.
|
||||
*/
|
||||
// prettier-ignore
|
||||
export type ValueSpecOf<T extends ValueType> =
|
||||
T extends "text" ? ValueSpecText :
|
||||
T extends "textarea" ? ValueSpecTextarea :
|
||||
T extends "number" ? ValueSpecNumber :
|
||||
T extends "color" ? ValueSpecColor :
|
||||
T extends "datetime" ? ValueSpecDatetime :
|
||||
T extends "toggle" ? ValueSpecToggle :
|
||||
T extends "select" ? ValueSpecSelect :
|
||||
T extends "multiselect" ? ValueSpecMultiselect :
|
||||
T extends "list" ? ValueSpecList :
|
||||
T extends "object" ? ValueSpecObject :
|
||||
T extends "file" ? ValueSpecFile :
|
||||
T extends "union" ? ValueSpecUnion :
|
||||
export type ValueSpecOf<T extends ValueType> =
|
||||
T extends "text" ? ValueSpecText :
|
||||
T extends "textarea" ? ValueSpecTextarea :
|
||||
T extends "number" ? ValueSpecNumber :
|
||||
T extends "color" ? ValueSpecColor :
|
||||
T extends "datetime" ? ValueSpecDatetime :
|
||||
T extends "toggle" ? ValueSpecToggle :
|
||||
T extends "select" ? ValueSpecSelect :
|
||||
T extends "multiselect" ? ValueSpecMultiselect :
|
||||
T extends "list" ? ValueSpecList :
|
||||
T extends "object" ? ValueSpecObject :
|
||||
T extends "file" ? ValueSpecFile :
|
||||
T extends "union" ? ValueSpecUnion :
|
||||
T extends "hidden" ? ValueSpecHidden :
|
||||
never
|
||||
|
||||
/**
|
||||
* Specification for a single-line text input field.
|
||||
* Use `Value.text()` to create this specification.
|
||||
*/
|
||||
export type ValueSpecText = {
|
||||
/** Display label for the field */
|
||||
name: string
|
||||
/** Help text shown below the field */
|
||||
description: string | null
|
||||
/** Warning message shown when the value changes (requires user confirmation) */
|
||||
warning: string | null
|
||||
|
||||
type: "text"
|
||||
/** Regex patterns the value must match, with descriptions for validation errors */
|
||||
patterns: Pattern[]
|
||||
/** Minimum character length */
|
||||
minLength: number | null
|
||||
/** Maximum character length */
|
||||
maxLength: number | null
|
||||
/** If true, displays input as dots (●●●) for sensitive data like passwords */
|
||||
masked: boolean
|
||||
|
||||
/** Browser input mode hint for mobile keyboards */
|
||||
inputmode: "text" | "email" | "tel" | "url"
|
||||
/** Placeholder text shown when the field is empty */
|
||||
placeholder: string | null
|
||||
|
||||
/** If true, the field cannot be left empty */
|
||||
required: boolean
|
||||
/** Default value (can be a string or random string generator) */
|
||||
default: DefaultString | null
|
||||
/** If string, the field is disabled with this message explaining why */
|
||||
disabled: false | string
|
||||
/** Configuration for "Generate" button that creates random strings */
|
||||
generate: null | RandomString
|
||||
/** If true, the value cannot be changed after initial set */
|
||||
immutable: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification for a multi-line text area input field.
|
||||
* Use `Value.textarea()` to create this specification.
|
||||
*/
|
||||
export type ValueSpecTextarea = {
|
||||
/** Display label for the field */
|
||||
name: string
|
||||
/** Help text shown below the field */
|
||||
description: string | null
|
||||
/** Warning message shown when the value changes */
|
||||
warning: string | null
|
||||
|
||||
type: "textarea"
|
||||
/** Regex patterns the value must match */
|
||||
patterns: Pattern[]
|
||||
/** Placeholder text shown when the field is empty */
|
||||
placeholder: string | null
|
||||
/** Minimum character length */
|
||||
minLength: number | null
|
||||
/** Maximum character length */
|
||||
maxLength: number | null
|
||||
/** Minimum visible rows before scrolling */
|
||||
minRows: number
|
||||
/** Maximum visible rows before scrolling */
|
||||
maxRows: number
|
||||
/** If true, the field cannot be left empty */
|
||||
required: boolean
|
||||
/** Default value */
|
||||
default: string | null
|
||||
/** If string, the field is disabled with this message */
|
||||
disabled: false | string
|
||||
/** If true, the value cannot be changed after initial set */
|
||||
immutable: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification for a numeric input field.
|
||||
* Use `Value.number()` to create this specification.
|
||||
*/
|
||||
export type ValueSpecNumber = {
|
||||
type: "number"
|
||||
/** Minimum allowed value */
|
||||
min: number | null
|
||||
/** Maximum allowed value */
|
||||
max: number | null
|
||||
/** If true, only whole numbers are allowed */
|
||||
integer: boolean
|
||||
/** Increment/decrement step for arrow controls */
|
||||
step: number | null
|
||||
/** Unit label displayed after the input (e.g., "MB", "seconds") */
|
||||
units: string | null
|
||||
/** Placeholder text shown when the field is empty */
|
||||
placeholder: string | null
|
||||
/** Display label for the field */
|
||||
name: string
|
||||
/** Help text shown below the field */
|
||||
description: string | null
|
||||
/** Warning message shown when the value changes */
|
||||
warning: string | null
|
||||
/** If true, the field cannot be left empty */
|
||||
required: boolean
|
||||
/** Default value */
|
||||
default: number | null
|
||||
/** If string, the field is disabled with this message */
|
||||
disabled: false | string
|
||||
/** If true, the value cannot be changed after initial set */
|
||||
immutable: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification for a color picker field.
|
||||
* Use `Value.color()` to create this specification.
|
||||
*/
|
||||
export type ValueSpecColor = {
|
||||
/** Display label for the field */
|
||||
name: string
|
||||
/** Help text shown below the field */
|
||||
description: string | null
|
||||
/** Warning message shown when the value changes */
|
||||
warning: string | null
|
||||
|
||||
type: "color"
|
||||
/** If true, a color must be selected */
|
||||
required: boolean
|
||||
/** Default color value (hex format, e.g., "ffffff") */
|
||||
default: string | null
|
||||
/** If string, the field is disabled with this message */
|
||||
disabled: false | string
|
||||
/** If true, the value cannot be changed after initial set */
|
||||
immutable: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification for a date/time picker field.
|
||||
* Use `Value.datetime()` to create this specification.
|
||||
*/
|
||||
export type ValueSpecDatetime = {
|
||||
/** Display label for the field */
|
||||
name: string
|
||||
/** Help text shown below the field */
|
||||
description: string | null
|
||||
/** Warning message shown when the value changes */
|
||||
warning: string | null
|
||||
type: "datetime"
|
||||
/** If true, the field cannot be left empty */
|
||||
required: boolean
|
||||
/** Type of datetime picker to display */
|
||||
inputmode: "date" | "time" | "datetime-local"
|
||||
/** Minimum allowed date/time */
|
||||
min: string | null
|
||||
/** Maximum allowed date/time */
|
||||
max: string | null
|
||||
/** Default value */
|
||||
default: string | null
|
||||
/** If string, the field is disabled with this message */
|
||||
disabled: false | string
|
||||
/** If true, the value cannot be changed after initial set */
|
||||
immutable: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification for a single-selection dropdown or radio button group.
|
||||
* Use `Value.select()` to create this specification.
|
||||
*/
|
||||
export type ValueSpecSelect = {
|
||||
/** Map of option values to their display labels */
|
||||
values: Record<string, string>
|
||||
/** Display label for the field */
|
||||
name: string
|
||||
/** Help text shown below the field */
|
||||
description: string | null
|
||||
/** Warning message shown when the value changes */
|
||||
warning: string | null
|
||||
type: "select"
|
||||
/** Default selected option key */
|
||||
default: string | null
|
||||
/** Disabled state: false=enabled, string=disabled with message, string[]=specific options disabled */
|
||||
disabled: false | string | string[]
|
||||
/** If true, the value cannot be changed after initial set */
|
||||
immutable: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification for a multiple-selection checkbox group.
|
||||
* Use `Value.multiselect()` to create this specification.
|
||||
*/
|
||||
export type ValueSpecMultiselect = {
|
||||
/** Map of option values to their display labels */
|
||||
values: Record<string, string>
|
||||
|
||||
/** Display label for the field */
|
||||
name: string
|
||||
/** Help text shown below the field */
|
||||
description: string | null
|
||||
/** Warning message shown when the value changes */
|
||||
warning: string | null
|
||||
|
||||
type: "multiselect"
|
||||
/** Minimum number of selections required */
|
||||
minLength: number | null
|
||||
/** Maximum number of selections allowed */
|
||||
maxLength: number | null
|
||||
/** Disabled state: false=enabled, string=disabled with message, string[]=specific options disabled */
|
||||
disabled: false | string | string[]
|
||||
/** Default selected option keys */
|
||||
default: string[]
|
||||
/** If true, the value cannot be changed after initial set */
|
||||
immutable: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification for a boolean toggle switch.
|
||||
* Use `Value.toggle()` to create this specification.
|
||||
*/
|
||||
export type ValueSpecToggle = {
|
||||
/** Display label for the field */
|
||||
name: string
|
||||
/** Help text shown below the field */
|
||||
description: string | null
|
||||
/** Warning message shown when the value changes */
|
||||
warning: string | null
|
||||
|
||||
type: "toggle"
|
||||
/** Default value (on/off) */
|
||||
default: boolean | null
|
||||
/** If string, the field is disabled with this message */
|
||||
disabled: false | string
|
||||
/** If true, the value cannot be changed after initial set */
|
||||
immutable: boolean
|
||||
}
|
||||
/**
|
||||
* Specification for a discriminated union field (conditional sub-forms).
|
||||
* Shows different fields based on which variant is selected.
|
||||
* Use `Value.union()` with `Variants.of()` to create this specification.
|
||||
*/
|
||||
export type ValueSpecUnion = {
|
||||
/** Display label for the field */
|
||||
name: string
|
||||
/** Help text shown below the field */
|
||||
description: string | null
|
||||
/** Warning message shown when the value changes */
|
||||
warning: string | null
|
||||
|
||||
type: "union"
|
||||
/** Map of variant keys to their display names and nested field specifications */
|
||||
variants: Record<
|
||||
string,
|
||||
{
|
||||
/** Display name for this variant option */
|
||||
name: string
|
||||
/** Fields to show when this variant is selected */
|
||||
spec: InputSpec
|
||||
}
|
||||
>
|
||||
/** Disabled state: false=enabled, string=disabled with message, string[]=specific variants disabled */
|
||||
disabled: false | string | string[]
|
||||
/** Default selected variant key */
|
||||
default: string | null
|
||||
/** If true, the value cannot be changed after initial set */
|
||||
immutable: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification for a file upload field.
|
||||
* Use `Value.file()` to create this specification.
|
||||
*/
|
||||
export type ValueSpecFile = {
|
||||
/** Display label for the field */
|
||||
name: string
|
||||
/** Help text shown below the field */
|
||||
description: string | null
|
||||
/** Warning message shown when the value changes */
|
||||
warning: string | null
|
||||
type: "file"
|
||||
/** Allowed file extensions (e.g., [".json", ".yaml"]) */
|
||||
extensions: string[]
|
||||
/** If true, a file must be uploaded */
|
||||
required: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification for a nested object (sub-form / field group).
|
||||
* Use `Value.object()` to create this specification.
|
||||
*/
|
||||
export type ValueSpecObject = {
|
||||
/** Display label for the field group */
|
||||
name: string
|
||||
/** Help text shown below the field group */
|
||||
description: string | null
|
||||
/** Warning message (not typically used for objects) */
|
||||
warning: string | null
|
||||
type: "object"
|
||||
/** Nested field specifications */
|
||||
spec: InputSpec
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification for a hidden field (not displayed in the UI).
|
||||
* Use `Value.hidden()` to create this specification.
|
||||
* Useful for storing internal state that shouldn't be user-editable.
|
||||
*/
|
||||
export type ValueSpecHidden = {
|
||||
type: "hidden"
|
||||
}
|
||||
|
||||
/** Types of items that can appear in a list */
|
||||
export type ListValueSpecType = "text" | "object"
|
||||
|
||||
/** Maps a list item type to its specification */
|
||||
// prettier-ignore
|
||||
export type ListValueSpecOf<T extends ListValueSpecType> =
|
||||
export type ListValueSpecOf<T extends ListValueSpecType> =
|
||||
T extends "text" ? ListValueSpecText :
|
||||
T extends "object" ? ListValueSpecObject :
|
||||
never
|
||||
|
||||
/** Union of all list specification types */
|
||||
export type ValueSpecList = ValueSpecListOf<ListValueSpecType>
|
||||
|
||||
/**
|
||||
* Specification for a dynamic list of items.
|
||||
* Use `Value.list()` with `List.text()` or `List.obj()` to create this specification.
|
||||
*/
|
||||
export type ValueSpecListOf<T extends ListValueSpecType> = {
|
||||
/** Display label for the list field */
|
||||
name: string
|
||||
/** Help text shown below the list */
|
||||
description: string | null
|
||||
/** Warning message shown when items change */
|
||||
warning: string | null
|
||||
type: "list"
|
||||
/** Specification for individual list items */
|
||||
spec: ListValueSpecOf<T>
|
||||
/** Minimum number of items required */
|
||||
minLength: number | null
|
||||
/** Maximum number of items allowed */
|
||||
maxLength: number | null
|
||||
/** If string, the list is disabled with this message */
|
||||
disabled: false | string
|
||||
/** Default list items */
|
||||
default:
|
||||
| string[]
|
||||
| DefaultString[]
|
||||
@@ -203,28 +420,62 @@ export type ValueSpecListOf<T extends ListValueSpecType> = {
|
||||
| readonly DefaultString[]
|
||||
| readonly Record<string, unknown>[]
|
||||
}
|
||||
|
||||
/**
|
||||
* A validation pattern with a regex and human-readable description.
|
||||
* Used to validate text input and provide meaningful error messages.
|
||||
*/
|
||||
export type Pattern = {
|
||||
/** Regular expression pattern (as a string) */
|
||||
regex: string
|
||||
/** Human-readable description shown when validation fails */
|
||||
description: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification for text items within a list.
|
||||
* Created via `List.text()`.
|
||||
*/
|
||||
export type ListValueSpecText = {
|
||||
type: "text"
|
||||
/** Regex patterns each item must match */
|
||||
patterns: Pattern[]
|
||||
/** Minimum character length per item */
|
||||
minLength: number | null
|
||||
/** Maximum character length per item */
|
||||
maxLength: number | null
|
||||
/** If true, displays items as dots (●●●) */
|
||||
masked: boolean
|
||||
|
||||
/** Configuration for "Generate" button */
|
||||
generate: null | RandomString
|
||||
/** Browser input mode hint */
|
||||
inputmode: "text" | "email" | "tel" | "url"
|
||||
/** Placeholder text for each item */
|
||||
placeholder: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification for object items within a list.
|
||||
* Created via `List.obj()`.
|
||||
*/
|
||||
export type ListValueSpecObject = {
|
||||
type: "object"
|
||||
/** Field specification for each object in the list */
|
||||
spec: InputSpec
|
||||
/** Constraint for ensuring unique items in the list */
|
||||
uniqueBy: UniqueBy
|
||||
/** Template string for how to display each item in the list (e.g., "{name} - {email}") */
|
||||
displayAs: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines how to determine uniqueness for list items.
|
||||
* - `null` - No uniqueness constraint
|
||||
* - `string` - Field name that must be unique (e.g., "email")
|
||||
* - `{ any: UniqueBy[] }` - Any of the specified constraints must be unique
|
||||
* - `{ all: UniqueBy[] }` - All of the specified constraints combined must be unique
|
||||
*/
|
||||
export type UniqueBy =
|
||||
| null
|
||||
| string
|
||||
@@ -234,12 +485,30 @@ export type UniqueBy =
|
||||
| {
|
||||
all: readonly UniqueBy[] | UniqueBy[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Default value for a text field - either a literal string or a random string generator.
|
||||
*/
|
||||
export type DefaultString = string | RandomString
|
||||
|
||||
/**
|
||||
* Configuration for generating random strings (e.g., passwords, tokens).
|
||||
* Used with `Value.text({ generate: ... })` to show a "Generate" button.
|
||||
*/
|
||||
export type RandomString = {
|
||||
/** Characters to use when generating (e.g., "abcdefghijklmnopqrstuvwxyz0123456789") */
|
||||
charset: string
|
||||
/** Length of the generated string */
|
||||
len: number
|
||||
}
|
||||
// sometimes the type checker needs just a little bit of help
|
||||
|
||||
/**
|
||||
* Type guard to check if a ValueSpec is a list of a specific item type.
|
||||
*
|
||||
* @param t - The value specification to check
|
||||
* @param s - The expected list item type ("text" or "object")
|
||||
* @returns True if the spec is a list of the specified type
|
||||
*/
|
||||
export function isValueSpecListOf<S extends ListValueSpecType>(
|
||||
t: ValueSpec,
|
||||
s: S,
|
||||
|
||||
@@ -1,3 +1,31 @@
|
||||
/**
|
||||
* @module setupActions
|
||||
*
|
||||
* This module provides the Action and Actions classes for defining user-callable
|
||||
* operations in StartOS services. Actions appear in the StartOS UI and can be
|
||||
* triggered by users or programmatically by other services.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Action, Actions, InputSpec, Value } from '@start9labs/start-sdk'
|
||||
*
|
||||
* const resetPasswordAction = Action.withInput(
|
||||
* 'reset-password',
|
||||
* { name: 'Reset Password', description: 'Reset the admin password' },
|
||||
* InputSpec.of({
|
||||
* username: Value.text({ name: 'Username', required: true, default: null })
|
||||
* }),
|
||||
* async ({ effects }) => ({ username: 'admin' }), // Pre-fill form
|
||||
* async ({ effects, input }) => {
|
||||
* // Perform the password reset
|
||||
* return { result: { type: 'single', value: 'Password reset successfully' } }
|
||||
* }
|
||||
* )
|
||||
*
|
||||
* export const actions = Actions.of().addAction(resetPasswordAction)
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { InputSpec } from "./input/builder"
|
||||
import { ExtractInputSpecType } from "./input/builder/inputSpec"
|
||||
import * as T from "../types"
|
||||
@@ -5,16 +33,54 @@ import { once } from "../util"
|
||||
import { InitScript } from "../inits"
|
||||
import { Parser } from "ts-matches"
|
||||
|
||||
/** @internal Input spec type or null if the action has no input */
|
||||
type MaybeInputSpec<Type> = {} extends Type ? null : InputSpec<Type>
|
||||
|
||||
/**
|
||||
* Function signature for executing an action.
|
||||
*
|
||||
* @typeParam A - The type of the validated input object
|
||||
* @param options.effects - Effects instance for system operations
|
||||
* @param options.input - The validated user input
|
||||
* @param options.spec - The input specification used to generate the form
|
||||
* @returns Promise resolving to an ActionResult to display to the user, or null/void for no result
|
||||
*/
|
||||
export type Run<A extends Record<string, any>> = (options: {
|
||||
effects: T.Effects
|
||||
input: A
|
||||
spec: T.inputSpecTypes.InputSpec
|
||||
}) => Promise<(T.ActionResult & { version: "1" }) | null | void | undefined>
|
||||
|
||||
/**
|
||||
* Function signature for pre-filling action input forms.
|
||||
* Called before displaying the input form to populate default values.
|
||||
*
|
||||
* @typeParam A - The type of the input object
|
||||
* @param options.effects - Effects instance for system operations
|
||||
* @returns Promise resolving to partial input values to pre-fill, or null for no pre-fill
|
||||
*/
|
||||
export type GetInput<A extends Record<string, any>> = (options: {
|
||||
effects: T.Effects
|
||||
}) => Promise<null | void | undefined | T.DeepPartial<A>>
|
||||
|
||||
/**
|
||||
* A value that can either be static or computed dynamically from Effects.
|
||||
* Used for action metadata that may need to change based on service state.
|
||||
*
|
||||
* @typeParam T - The type of the value
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Static metadata
|
||||
* const metadata: MaybeFn<ActionMetadata> = { name: 'My Action' }
|
||||
*
|
||||
* // Dynamic metadata based on service state
|
||||
* const dynamicMetadata: MaybeFn<ActionMetadata> = async ({ effects }) => {
|
||||
* const isEnabled = await checkSomething(effects)
|
||||
* return { name: isEnabled ? 'Disable Feature' : 'Enable Feature' }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export type MaybeFn<T> = T | ((options: { effects: T.Effects }) => Promise<T>)
|
||||
function callMaybeFn<T>(
|
||||
maybeFn: MaybeFn<T>,
|
||||
@@ -37,29 +103,76 @@ function mapMaybeFn<T, U>(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type information interface for an Action.
|
||||
* Used for type inference in the Actions collection.
|
||||
*
|
||||
* @typeParam Id - The action's unique identifier type
|
||||
* @typeParam Type - The action's input type
|
||||
*/
|
||||
export interface ActionInfo<
|
||||
Id extends T.ActionId,
|
||||
Type extends Record<string, any>,
|
||||
> {
|
||||
/** The unique identifier for this action */
|
||||
readonly id: Id
|
||||
/** @internal Type brand for input type inference */
|
||||
readonly _INPUT: Type
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a user-callable action in a StartOS service.
|
||||
*
|
||||
* Exposed via `sdk.Action`. Actions are operations that users can trigger
|
||||
* from the StartOS UI or that can be invoked programmatically. Each action has:
|
||||
* - A unique ID
|
||||
* - Metadata (name, description, visibility, etc.)
|
||||
* - Optional input specification (form fields)
|
||||
* - A run function that executes the action
|
||||
*
|
||||
* Use `sdk.Action.withInput()` for actions that require user input, or
|
||||
* `sdk.Action.withoutInput()` for actions that run immediately.
|
||||
*
|
||||
* See the SDK documentation for detailed examples.
|
||||
*
|
||||
* @typeParam Id - The action's unique identifier type
|
||||
* @typeParam Type - The action's input type (empty object {} for no input)
|
||||
*/
|
||||
export class Action<Id extends T.ActionId, Type extends Record<string, any>>
|
||||
implements ActionInfo<Id, Type>
|
||||
{
|
||||
/** @internal Type brand for input type inference */
|
||||
readonly _INPUT: Type = null as any as Type
|
||||
|
||||
/** @internal Cache of built input specs by event ID */
|
||||
private prevInputSpec: Record<
|
||||
string,
|
||||
{ spec: T.inputSpecTypes.InputSpec; validator: Parser<unknown, Type> }
|
||||
> = {}
|
||||
|
||||
private constructor(
|
||||
/** The unique identifier for this action */
|
||||
readonly id: Id,
|
||||
private readonly metadataFn: MaybeFn<T.ActionMetadata>,
|
||||
private readonly inputSpec: MaybeInputSpec<Type>,
|
||||
private readonly getInputFn: GetInput<Type>,
|
||||
private readonly runFn: Run<Type>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Creates an action that requires user input before execution.
|
||||
* The input form is defined by an InputSpec.
|
||||
*
|
||||
* @typeParam Id - The action ID type
|
||||
* @typeParam InputSpecType - The input specification type
|
||||
*
|
||||
* @param id - Unique identifier for the action (used in URLs and API calls)
|
||||
* @param metadata - Action metadata (name, description, visibility, etc.) - can be static or dynamic
|
||||
* @param inputSpec - Specification for the input form fields
|
||||
* @param getInput - Function to pre-populate the form with default/previous values
|
||||
* @param run - Function to execute when the action is submitted
|
||||
* @returns A new Action instance
|
||||
*/
|
||||
static withInput<
|
||||
Id extends T.ActionId,
|
||||
InputSpecType extends InputSpec<Record<string, any>>,
|
||||
@@ -78,6 +191,18 @@ export class Action<Id extends T.ActionId, Type extends Record<string, any>>
|
||||
run,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an action that executes immediately without requiring user input.
|
||||
* Use this for simple operations like toggles, restarts, or status checks.
|
||||
*
|
||||
* @typeParam Id - The action ID type
|
||||
*
|
||||
* @param id - Unique identifier for the action
|
||||
* @param metadata - Action metadata (name, description, visibility, etc.) - can be static or dynamic
|
||||
* @param run - Function to execute when the action is triggered
|
||||
* @returns A new Action instance with no input
|
||||
*/
|
||||
static withoutInput<Id extends T.ActionId>(
|
||||
id: Id,
|
||||
metadata: MaybeFn<Omit<T.ActionMetadata, "hasInput">>,
|
||||
@@ -91,6 +216,14 @@ export class Action<Id extends T.ActionId, Type extends Record<string, any>>
|
||||
run,
|
||||
)
|
||||
}
|
||||
/**
|
||||
* Exports the action's metadata to StartOS, making it visible in the UI.
|
||||
* Called automatically during initialization by the Actions collection.
|
||||
*
|
||||
* @param options.effects - Effects instance for system operations
|
||||
* @returns Promise resolving to the exported metadata
|
||||
* @internal
|
||||
*/
|
||||
async exportMetadata(options: {
|
||||
effects: T.Effects
|
||||
}): Promise<T.ActionMetadata> {
|
||||
@@ -104,6 +237,15 @@ export class Action<Id extends T.ActionId, Type extends Record<string, any>>
|
||||
await options.effects.action.export({ id: this.id, metadata })
|
||||
return metadata
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and returns the input specification and pre-filled values for this action.
|
||||
* Called by StartOS when a user clicks on the action to display the input form.
|
||||
*
|
||||
* @param options.effects - Effects instance for system operations
|
||||
* @returns Promise resolving to the input specification and pre-filled values
|
||||
* @internal
|
||||
*/
|
||||
async getInput(options: { effects: T.Effects }): Promise<T.ActionInput> {
|
||||
let spec = {}
|
||||
if (this.inputSpec) {
|
||||
@@ -121,6 +263,16 @@ export class Action<Id extends T.ActionId, Type extends Record<string, any>>
|
||||
| undefined) || null,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the action with the provided input.
|
||||
* Called by StartOS when a user submits the action form.
|
||||
*
|
||||
* @param options.effects - Effects instance for system operations
|
||||
* @param options.input - The user-provided input (validated against the input spec)
|
||||
* @returns Promise resolving to the action result to display, or null for no result
|
||||
* @internal
|
||||
*/
|
||||
async run(options: {
|
||||
effects: T.Effects
|
||||
input: Type
|
||||
@@ -146,19 +298,77 @@ export class Action<Id extends T.ActionId, Type extends Record<string, any>>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A collection of actions for a StartOS service.
|
||||
*
|
||||
* Exposed via `sdk.Actions`. The Actions class manages the registration and
|
||||
* lifecycle of all actions in a service. It implements InitScript so it can
|
||||
* be included in the initialization pipeline to automatically register actions
|
||||
* with StartOS.
|
||||
*
|
||||
* @typeParam AllActions - Record type mapping action IDs to Action instances
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Create an actions collection
|
||||
* export const actions = sdk.Actions.of()
|
||||
* .addAction(createUserAction)
|
||||
* .addAction(resetPasswordAction)
|
||||
* .addAction(restartAction)
|
||||
*
|
||||
* // Include in init pipeline
|
||||
* export const init = sdk.setupInit(
|
||||
* versionGraph,
|
||||
* setInterfaces,
|
||||
* actions, // Actions are registered here
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
export class Actions<
|
||||
AllActions extends Record<T.ActionId, Action<T.ActionId, any>>,
|
||||
> implements InitScript
|
||||
{
|
||||
private constructor(private readonly actions: AllActions) {}
|
||||
|
||||
/**
|
||||
* Creates a new empty Actions collection.
|
||||
* Use `addAction()` to add actions to the collection.
|
||||
*
|
||||
* @returns A new empty Actions instance
|
||||
*/
|
||||
static of(): Actions<{}> {
|
||||
return new Actions({})
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an action to the collection.
|
||||
* Returns a new Actions instance with the action included (immutable pattern).
|
||||
*
|
||||
* @typeParam A - The action type being added
|
||||
* @param action - The action to add
|
||||
* @returns A new Actions instance containing all previous actions plus the new one
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const actions = Actions.of()
|
||||
* .addAction(action1)
|
||||
* .addAction(action2)
|
||||
* ```
|
||||
*/
|
||||
addAction<A extends Action<T.ActionId, any>>(
|
||||
action: A, // TODO: prevent duplicates
|
||||
): Actions<AllActions & { [id in A["id"]]: A }> {
|
||||
return new Actions({ ...this.actions, [action.id]: action })
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes all actions by exporting their metadata to StartOS.
|
||||
* Called automatically when included in the init pipeline.
|
||||
* Also clears any previously registered actions that are no longer in the collection.
|
||||
*
|
||||
* @param effects - Effects instance for system operations
|
||||
* @internal
|
||||
*/
|
||||
async init(effects: T.Effects): Promise<void> {
|
||||
for (let action of Object.values(this.actions)) {
|
||||
const fn = async () => {
|
||||
@@ -180,6 +390,15 @@ export class Actions<
|
||||
}
|
||||
await effects.action.clear({ except: Object.keys(this.actions) })
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an action from the collection by its ID.
|
||||
* Useful for programmatically invoking actions or inspecting their configuration.
|
||||
*
|
||||
* @typeParam Id - The action ID type
|
||||
* @param actionId - The ID of the action to retrieve
|
||||
* @returns The action instance
|
||||
*/
|
||||
get<Id extends T.ActionId>(actionId: Id): AllActions[Id] {
|
||||
return this.actions[actionId]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user