add comments to everything potentially consumer facing (#3127)

* add comments to everything potentially consumer facing

* rework smtp

---------

Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>
This commit is contained in:
Matt Hill
2026-02-24 14:29:09 -07:00
committed by GitHub
parent 3974c09369
commit d4e019c87b
51 changed files with 1796 additions and 116 deletions

View File

@@ -6,19 +6,28 @@ import { z } from 'zod'
import { DeepPartial } from '../../../types'
import { InputSpecTools, createInputSpecTools } from './inputSpecTools'
/** Options passed to a lazy builder function when resolving dynamic form field values. */
export type LazyBuildOptions<Type> = {
/** The effects interface for runtime operations (e.g. reading files, querying state). */
effects: Effects
/** Previously saved form data to pre-fill the form with, or `null` for fresh creation. */
prefill: DeepPartial<Type> | null
}
/**
* A function that lazily produces a value, potentially using effects and prefill data.
* Used by `dynamic*` variants of {@link Value} to compute form field options at runtime.
*/
export type LazyBuild<ExpectedOut, Type> = (
options: LazyBuildOptions<Type>,
) => Promise<ExpectedOut> | ExpectedOut
/** Extracts the runtime type from an {@link InputSpec}. */
// prettier-ignore
export type ExtractInputSpecType<A extends InputSpec<Record<string, any>, any>> =
export type ExtractInputSpecType<A extends InputSpec<Record<string, any>, any>> =
A extends InputSpec<infer B, any> ? B :
never
/** Extracts the static validation type from an {@link InputSpec}. */
export type ExtractInputSpecStaticValidatedAs<
A extends InputSpec<any, Record<string, any>>,
> = A extends InputSpec<any, infer B> ? B : never
@@ -27,10 +36,12 @@ export type ExtractInputSpecStaticValidatedAs<
// A extends Record<string, any> | InputSpec<Record<string, any>>,
// > = A extends InputSpec<infer B> ? DeepPartial<B> : DeepPartial<A>
/** Maps an object type to a record of {@link Value} entries for use with `InputSpec.of`. */
export type InputSpecOf<A extends Record<string, any>> = {
[K in keyof A]: Value<A[K]>
}
/** A value that is either directly provided or lazily computed via a {@link LazyBuild} function. */
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.
@@ -100,6 +111,11 @@ export class InputSpec<
) {}
public _TYPE: Type = null as any as Type
public _PARTIAL: DeepPartial<Type> = null as any as DeepPartial<Type>
/**
* Builds the runtime form specification and combined Zod validator from this InputSpec's fields.
*
* @returns An object containing the resolved `spec` (field specs keyed by name) and a combined `validator`
*/
async build<OuterType>(options: LazyBuildOptions<OuterType>): Promise<{
spec: {
[K in keyof Type]: ValueSpec
@@ -123,6 +139,12 @@ export class InputSpec<
}
}
/**
* Adds a single named field to this spec, returning a new `InputSpec` with the extended type.
*
* @param key - The field key name
* @param build - A {@link Value} instance, or a function receiving typed tools that returns one
*/
addKey<Key extends string, V extends Value<any, any, any>>(
key: Key,
build: V | ((tools: InputSpecTools<Type>) => V),
@@ -146,6 +168,11 @@ export class InputSpec<
return new InputSpec(newSpec, newValidator as any)
}
/**
* Adds multiple fields to this spec at once, returning a new `InputSpec` with extended types.
*
* @param build - A record of {@link Value} entries, or a function receiving typed tools that returns one
*/
add<AddSpec extends Record<string, Value<any, any, any>>>(
build: AddSpec | ((tools: InputSpecTools<Type>) => AddSpec),
): InputSpec<
@@ -174,6 +201,17 @@ export class InputSpec<
return new InputSpec(newSpec, newValidator as any)
}
/**
* Creates an `InputSpec` from a plain record of {@link Value} entries.
*
* @example
* ```ts
* const spec = InputSpec.of({
* username: Value.text({ name: 'Username', required: true, default: null }),
* verbose: Value.toggle({ name: 'Verbose Logging', default: false }),
* })
* ```
*/
static of<Spec extends Record<string, Value<any, any>>>(spec: Spec) {
const validator = z.object(
Object.fromEntries(

View File

@@ -9,6 +9,14 @@ import {
} from '../inputSpecTypes'
import { z } from 'zod'
/**
* Builder class for defining list-type form fields.
*
* A list presents an interface to add, remove, and reorder items. Items can be
* either text strings ({@link List.text}) or structured objects ({@link List.obj}).
*
* Used with {@link Value.list} to include a list field in an {@link InputSpec}.
*/
export class List<
Type extends StaticValidatedAs,
StaticValidatedAs = Type,
@@ -26,6 +34,12 @@ export class List<
) {}
readonly _TYPE: Type = null as any
/**
* Creates a list of text input items.
*
* @param a - List-level options (name, description, min/max length, defaults)
* @param aSpec - Item-level options (patterns, input mode, masking, generation)
*/
static text(
a: {
name: string
@@ -97,6 +111,7 @@ export class List<
}, validator)
}
/** Like {@link List.text} but options are resolved lazily at runtime via a builder function. */
static dynamicText<OuterType = unknown>(
getA: LazyBuild<
{
@@ -150,6 +165,12 @@ export class List<
}, validator)
}
/**
* Creates a list of structured object items, each defined by a nested {@link InputSpec}.
*
* @param a - List-level options (name, description, min/max length)
* @param aSpec - Item-level options (the nested spec, display expression, uniqueness constraint)
*/
static obj<
Type extends StaticValidatedAs,
StaticValidatedAs extends Record<string, any>,

View File

@@ -15,12 +15,15 @@ import { _, once } from '../../../util'
import { z } from 'zod'
import { DeepPartial } from '../../../types'
/** Zod schema for a file upload result — validates `{ path, commitment: { hash, size } }`. */
export const fileInfoParser = z.object({
path: z.string(),
commitment: z.object({ hash: z.string(), size: z.number() }),
})
/** The parsed result of a file upload, containing the file path and its content commitment (hash + size). */
export type FileInfo = z.infer<typeof fileInfoParser>
/** Conditional type: returns `T` if `Required` is `true`, otherwise `T | null`. */
export type AsRequired<T, Required extends boolean> = Required extends true
? T
: T | null
@@ -37,6 +40,19 @@ function asRequiredParser<Type, Input extends { required: boolean }>(
return parser.nullable() as any
}
/**
* Core builder class for defining a single form field in a service configuration spec.
*
* Each static factory method (e.g. `Value.text()`, `Value.toggle()`, `Value.select()`) creates
* a typed `Value` instance representing a specific field type. Dynamic variants (e.g. `Value.dynamicText()`)
* allow the field options to be computed lazily at runtime.
*
* Use with {@link InputSpec} to compose complete form specifications.
*
* @typeParam Type - The runtime type this field produces when filled in
* @typeParam StaticValidatedAs - The compile-time validated type (usually same as Type)
* @typeParam OuterType - The parent form's type context (used by dynamic variants)
*/
export class Value<
Type extends StaticValidatedAs,
StaticValidatedAs = Type,
@@ -99,6 +115,7 @@ export class Value<
validator,
)
}
/** Like {@link Value.toggle} but options are resolved lazily at runtime via a builder function. */
static dynamicToggle<OuterType = unknown>(
a: LazyBuild<
{
@@ -225,6 +242,7 @@ export class Value<
validator,
)
}
/** Like {@link Value.text} but options are resolved lazily at runtime via a builder function. */
static dynamicText<Required extends boolean, OuterType = unknown>(
getA: LazyBuild<
{
@@ -345,6 +363,7 @@ export class Value<
return { spec: built, validator }
}, validator)
}
/** Like {@link Value.textarea} but options are resolved lazily at runtime via a builder function. */
static dynamicTextarea<Required extends boolean, OuterType = unknown>(
getA: LazyBuild<
{
@@ -467,6 +486,7 @@ export class Value<
validator,
)
}
/** Like {@link Value.number} but options are resolved lazily at runtime via a builder function. */
static dynamicNumber<Required extends boolean, OuterType = unknown>(
getA: LazyBuild<
{
@@ -562,6 +582,7 @@ export class Value<
)
}
/** Like {@link Value.color} but options are resolved lazily at runtime via a builder function. */
static dynamicColor<Required extends boolean, OuterType = unknown>(
getA: LazyBuild<
{
@@ -659,6 +680,7 @@ export class Value<
validator,
)
}
/** Like {@link Value.datetime} but options are resolved lazily at runtime via a builder function. */
static dynamicDatetime<Required extends boolean, OuterType = unknown>(
getA: LazyBuild<
{
@@ -769,6 +791,7 @@ export class Value<
validator,
)
}
/** Like {@link Value.select} but options are resolved lazily at runtime via a builder function. */
static dynamicSelect<
Values extends Record<string, string>,
OuterType = unknown,
@@ -889,6 +912,7 @@ export class Value<
validator,
)
}
/** Like {@link Value.multiselect} but options are resolved lazily at runtime via a builder function. */
static dynamicMultiselect<
Values extends Record<string, string>,
OuterType = unknown,
@@ -977,6 +1001,12 @@ export class Value<
}
}, spec.validator)
}
/**
* Displays a file upload input field.
*
* @param a.extensions - Allowed file extensions (e.g. `[".pem", ".crt"]`)
* @param a.required - Whether a file must be selected
*/
static file<Required extends boolean>(a: {
name: string
description?: string | null
@@ -1000,6 +1030,7 @@ export class Value<
asRequiredParser(fileInfoParser, a),
)
}
/** Like {@link Value.file} but options are resolved lazily at runtime via a builder function. */
static dynamicFile<Required extends boolean, OuterType = unknown>(
a: LazyBuild<
{
@@ -1102,6 +1133,7 @@ export class Value<
}
}, a.variants.validator)
}
/** Like {@link Value.union} but options (including which variants are available) are resolved lazily at runtime. */
static dynamicUnion<
VariantValues extends {
[K in string]: {
@@ -1123,6 +1155,7 @@ export class Value<
OuterType
>,
): Value<UnionRes<VariantValues>, UnionRes<VariantValues>, OuterType>
/** Like {@link Value.union} but options are resolved lazily, with an explicit static validator type. */
static dynamicUnion<
StaticVariantValues extends {
[K in string]: {
@@ -1300,6 +1333,12 @@ export class Value<
}, z.any())
}
/**
* Transforms the validated output value using a mapping function.
* The form field itself remains unchanged, but the value is transformed after validation.
*
* @param fn - A function to transform the validated value
*/
map<U>(fn: (value: StaticValidatedAs) => U): Value<U, U, OuterType> {
return new Value<U, U, OuterType>(async (options) => {
const built = await this.build(options)

View File

@@ -8,6 +8,11 @@ import {
} from './inputSpec'
import { z } from 'zod'
/**
* The runtime result type of a discriminated union form field.
* Contains `selection` (the chosen variant key), `value` (the variant's form data),
* and optionally `other` (partial data from previously selected variants).
*/
export type UnionRes<
VariantValues extends {
[K in string]: {
@@ -28,6 +33,7 @@ export type UnionRes<
}
}[K]
/** Like {@link UnionRes} but using the static (Zod-inferred) validated types. */
export type UnionResStaticValidatedAs<
VariantValues extends {
[K in string]: {
@@ -118,6 +124,11 @@ export class Variants<
>,
) {}
readonly _TYPE: UnionRes<VariantValues> = null as any
/**
* Creates a `Variants` instance from a record mapping variant keys to their display name and form spec.
*
* @param a - A record of `{ name: string, spec: InputSpec }` entries, one per variant
*/
static of<
VariantValues extends {
[K in string]: {

View File

@@ -5,42 +5,124 @@ import { Value } from './builder/value'
import { Variants } from './builder/variants'
/**
* Base SMTP settings, to be used by StartOS for system wide SMTP
* Creates an SMTP field spec with provider-specific defaults pre-filled.
*/
export const customSmtp: InputSpec<SmtpValue> = InputSpec.of<
InputSpecOf<SmtpValue>
>({
server: Value.text({
name: 'SMTP Server',
required: true,
default: null,
}),
port: Value.number({
name: 'Port',
required: true,
default: 587,
min: 1,
max: 65535,
integer: true,
}),
from: Value.text({
name: 'From Address',
required: true,
default: null,
placeholder: 'Example Name <test@example.com>',
inputmode: 'email',
patterns: [Patterns.emailWithName],
}),
login: Value.text({
name: 'Login',
required: true,
default: null,
}),
password: Value.text({
name: 'Password',
required: false,
default: null,
masked: true,
function smtpFields(
defaults: {
host?: string
port?: number
security?: 'starttls' | 'tls'
} = {},
): InputSpec<SmtpValue> {
return InputSpec.of<InputSpecOf<SmtpValue>>({
host: Value.text({
name: 'Host',
required: true,
default: defaults.host ?? null,
placeholder: 'smtp.example.com',
}),
port: Value.number({
name: 'Port',
required: true,
default: defaults.port ?? 587,
min: 1,
max: 65535,
integer: true,
}),
security: Value.select({
name: 'Connection Security',
default: defaults.security ?? 'starttls',
values: {
starttls: 'STARTTLS',
tls: 'TLS',
},
}),
from: Value.text({
name: 'From Address',
required: true,
default: null,
placeholder: 'Example Name <test@example.com>',
patterns: [Patterns.emailWithName],
}),
username: Value.text({
name: 'Username',
required: true,
default: null,
}),
password: Value.text({
name: 'Password',
required: false,
default: null,
masked: true,
}),
})
}
/**
* Base SMTP settings with no provider-specific defaults.
*/
export const customSmtp = smtpFields()
/**
* Provider presets for SMTP configuration.
* Each variant has SMTP fields pre-filled with the provider's recommended settings.
*/
export const smtpProviderVariants = Variants.of({
gmail: {
name: 'Gmail',
spec: smtpFields({
host: 'smtp.gmail.com',
port: 587,
security: 'starttls',
}),
},
ses: {
name: 'Amazon SES',
spec: smtpFields({
host: 'email-smtp.us-east-1.amazonaws.com',
port: 587,
security: 'starttls',
}),
},
sendgrid: {
name: 'SendGrid',
spec: smtpFields({
host: 'smtp.sendgrid.net',
port: 587,
security: 'starttls',
}),
},
mailgun: {
name: 'Mailgun',
spec: smtpFields({
host: 'smtp.mailgun.org',
port: 587,
security: 'starttls',
}),
},
protonmail: {
name: 'Proton Mail',
spec: smtpFields({
host: 'smtp.protonmail.ch',
port: 587,
security: 'starttls',
}),
},
other: {
name: 'Other',
spec: customSmtp,
},
})
/**
* System SMTP settings with provider presets.
* Wraps smtpProviderVariants in a union for use by the system email settings page.
*/
export const systemSmtpSpec = InputSpec.of({
provider: Value.union({
name: 'Provider',
default: null as any,
variants: smtpProviderVariants,
}),
})
@@ -55,19 +137,24 @@ const smtpVariants = Variants.of({
'A custom from address for this service. If not provided, the system from address will be used.',
required: false,
default: null,
placeholder: '<name>test@example.com',
inputmode: 'email',
patterns: [Patterns.email],
placeholder: 'Name <test@example.com>',
patterns: [Patterns.emailWithName],
}),
}),
},
custom: {
name: 'Custom Credentials',
spec: customSmtp,
spec: InputSpec.of({
provider: Value.union({
name: 'Provider',
default: null as any,
variants: smtpProviderVariants,
}),
}),
},
})
/**
* For service inputSpec. Gives users 3 options for SMTP: (1) disabled, (2) use system SMTP settings, (3) use custom SMTP settings
* For service inputSpec. Gives users 3 options for SMTP: (1) disabled, (2) use system SMTP settings, (3) use custom SMTP settings with provider presets
*/
export const smtpInputSpec = Value.dynamicUnion(async ({ effects }) => {
const smtp = await new GetSystemSmtp(effects).once()

View File

@@ -1,4 +1,12 @@
/**
* A record mapping field keys to their {@link ValueSpec} definitions.
* This is the root shape of a dynamic form specification — it defines the complete set
* of configurable fields for a service or action.
*/
export type InputSpec = Record<string, ValueSpec>
/**
* The discriminator for all supported form field types.
*/
export type ValueType =
| 'text'
| 'textarea'
@@ -13,6 +21,7 @@ export type ValueType =
| 'file'
| 'union'
| 'hidden'
/** Union of all concrete form field spec types. Discriminate on the `type` field. */
export type ValueSpec = ValueSpecOf<ValueType>
/** core spec types. These types provide the metadata for performing validations */
// prettier-ignore
@@ -32,37 +41,56 @@ export type ValueSpecOf<T extends ValueType> =
T extends "hidden" ? ValueSpecHidden :
never
/** Spec for a single-line text input field. */
export type ValueSpecText = {
/** Display label for the field. */
name: string
/** Optional help text displayed below the field. */
description: string | null
/** Optional warning message displayed to the user. */
warning: string | null
type: 'text'
/** Regex patterns used to validate the input value. */
patterns: Pattern[]
/** Minimum character length, or `null` for no minimum. */
minLength: number | null
/** Maximum character length, or `null` for no maximum. */
maxLength: number | null
/** Whether the field should obscure input (e.g. for passwords). */
masked: boolean
/** HTML input mode hint for mobile keyboards. */
inputmode: 'text' | 'email' | 'tel' | 'url'
/** Placeholder text shown when the field is empty. */
placeholder: string | null
/** Whether the field must have a value. */
required: boolean
/** Default value, which may be a literal string or a {@link RandomString} generation spec. */
default: DefaultString | null
/** `false` if editable, or a string message explaining why the field is disabled. */
disabled: false | string
/** If set, provides a "generate" button that fills the field with a random string matching this spec. */
generate: null | RandomString
/** Whether the field value cannot be changed after initial configuration. */
immutable: boolean
}
/** Spec for a multi-line textarea input field. */
export type ValueSpecTextarea = {
name: string
description: string | null
warning: string | null
type: 'textarea'
/** Regex patterns used to validate the input value. */
patterns: Pattern[]
placeholder: string | null
minLength: number | null
maxLength: number | null
/** Minimum number of visible rows. */
minRows: number
/** Maximum number of visible rows before scrolling. */
maxRows: number
required: boolean
default: string | null
@@ -70,12 +98,18 @@ export type ValueSpecTextarea = {
immutable: boolean
}
/** Spec for a numeric input field. */
export type ValueSpecNumber = {
type: 'number'
/** Minimum allowed value, or `null` for unbounded. */
min: number | null
/** Maximum allowed value, or `null` for unbounded. */
max: number | null
/** Whether only whole numbers are accepted. */
integer: boolean
/** Step increment for the input spinner, or `null` for any precision. */
step: number | null
/** Display label for the unit (e.g. `"MB"`, `"seconds"`), shown next to the field. */
units: string | null
placeholder: string | null
name: string
@@ -86,6 +120,7 @@ export type ValueSpecNumber = {
disabled: false | string
immutable: boolean
}
/** Spec for a browser-native color picker field. */
export type ValueSpecColor = {
name: string
description: string | null
@@ -93,34 +128,44 @@ export type ValueSpecColor = {
type: 'color'
required: boolean
/** Default hex color string (e.g. `"#ff0000"`), or `null`. */
default: string | null
disabled: false | string
immutable: boolean
}
/** Spec for a date, time, or datetime input field. */
export type ValueSpecDatetime = {
name: string
description: string | null
warning: string | null
type: 'datetime'
required: boolean
/** Controls which kind of picker is displayed. */
inputmode: 'date' | 'time' | 'datetime-local'
/** Minimum selectable date/time as an ISO string, or `null`. */
min: string | null
/** Maximum selectable date/time as an ISO string, or `null`. */
max: string | null
default: string | null
disabled: false | string
immutable: boolean
}
/** Spec for a single-select field displayed as radio buttons in a modal. */
export type ValueSpecSelect = {
/** Map of option keys to display labels. */
values: Record<string, string>
name: string
description: string | null
warning: string | null
type: 'select'
default: string | null
/** `false` if all enabled, a string disabling the whole field, or an array of disabled option keys. */
disabled: false | string | string[]
immutable: boolean
}
/** Spec for a multi-select field displayed as checkboxes in a modal. */
export type ValueSpecMultiselect = {
/** Map of option keys to display labels. */
values: Record<string, string>
name: string
@@ -128,12 +173,17 @@ export type ValueSpecMultiselect = {
warning: string | null
type: 'multiselect'
/** Minimum number of selections required, or `null`. */
minLength: number | null
/** Maximum number of selections allowed, or `null`. */
maxLength: number | null
/** `false` if all enabled, a string disabling the whole field, or an array of disabled option keys. */
disabled: false | string | string[]
/** Array of option keys selected by default. */
default: string[]
immutable: boolean
}
/** Spec for a boolean toggle (on/off switch). */
export type ValueSpecToggle = {
name: string
description: string | null
@@ -144,57 +194,81 @@ export type ValueSpecToggle = {
disabled: false | string
immutable: boolean
}
/**
* Spec for a discriminated union field — displays a dropdown for variant selection,
* and each variant can have its own nested sub-form.
*/
export type ValueSpecUnion = {
name: string
description: string | null
warning: string | null
type: 'union'
/** Map of variant keys to their display name and nested form spec. */
variants: Record<
string,
{
/** Display name for this variant in the dropdown. */
name: string
/** Nested form spec shown when this variant is selected. */
spec: InputSpec
}
>
/** `false` if all enabled, a string disabling the whole field, or an array of disabled variant keys. */
disabled: false | string | string[]
default: string | null
immutable: boolean
}
/** Spec for a file upload input field. */
export type ValueSpecFile = {
name: string
description: string | null
warning: string | null
type: 'file'
/** Allowed file extensions (e.g. `[".pem", ".crt"]`). */
extensions: string[]
required: boolean
}
/** Spec for a collapsible grouping of nested fields (a "sub-form"). */
export type ValueSpecObject = {
name: string
description: string | null
warning: string | null
type: 'object'
/** The nested form spec containing this object's fields. */
spec: InputSpec
}
/** Spec for a hidden field — not displayed to the user but included in the form data. */
export type ValueSpecHidden = {
type: 'hidden'
}
/** The two supported list item types. */
export type ListValueSpecType = 'text' | 'object'
/** Maps a {@link ListValueSpecType} to its concrete list item spec. */
// prettier-ignore
export type ListValueSpecOf<T extends ListValueSpecType> =
export type ListValueSpecOf<T extends ListValueSpecType> =
T extends "text" ? ListValueSpecText :
T extends "object" ? ListValueSpecObject :
never
/** A list field spec — union of text-list and object-list variants. */
export type ValueSpecList = ValueSpecListOf<ListValueSpecType>
/**
* Spec for a list field — an interface to add, remove, and edit items in an ordered collection.
* The `spec` field determines whether list items are text strings or structured objects.
*/
export type ValueSpecListOf<T extends ListValueSpecType> = {
name: string
description: string | null
warning: string | null
type: 'list'
/** The item spec — determines whether this is a list of text values or objects. */
spec: ListValueSpecOf<T>
/** Minimum number of items, or `null` for no minimum. */
minLength: number | null
/** Maximum number of items, or `null` for no maximum. */
maxLength: number | null
disabled: false | string
/** Default list items to populate on creation. */
default:
| string[]
| DefaultString[]
@@ -203,10 +277,14 @@ export type ValueSpecListOf<T extends ListValueSpecType> = {
| readonly DefaultString[]
| readonly Record<string, unknown>[]
}
/** A regex validation pattern with a human-readable description of what it enforces. */
export type Pattern = {
/** The regex pattern string (without delimiters). */
regex: string
/** A user-facing explanation shown when validation fails (e.g. `"Must be a valid email"`). */
description: string
}
/** Spec for text items within a list field. */
export type ListValueSpecText = {
type: 'text'
patterns: Pattern[]
@@ -218,13 +296,24 @@ export type ListValueSpecText = {
inputmode: 'text' | 'email' | 'tel' | 'url'
placeholder: string | null
}
/** Spec for object items within a list field. */
export type ListValueSpecObject = {
type: 'object'
/** The form spec for each object item. */
spec: InputSpec
/** Defines how uniqueness is determined among list items. */
uniqueBy: UniqueBy
/** An expression used to generate the display string for each item in the list summary (e.g. a key path). */
displayAs: string | null
}
/**
* Describes how list items determine uniqueness.
* - `null`: no uniqueness constraint
* - `string`: unique by a specific field key
* - `{ any: UniqueBy[] }`: unique if any of the sub-constraints match
* - `{ all: UniqueBy[] }`: unique if all sub-constraints match together
*/
export type UniqueBy =
| null
| string
@@ -234,12 +323,21 @@ export type UniqueBy =
| {
all: readonly UniqueBy[] | UniqueBy[]
}
/** A default value that is either a literal string or a {@link RandomString} generation spec. */
export type DefaultString = string | RandomString
/** Spec for generating a random string — used for default passwords, API keys, etc. */
export type RandomString = {
/** The character set to draw from (e.g. `"a-zA-Z0-9"`). */
charset: string
/** The length of the generated string. */
len: number
}
// sometimes the type checker needs just a little bit of help
/**
* Type guard that narrows a {@link ValueSpec} to a {@link ValueSpecListOf} of a specific item type.
*
* @param t - The value spec to check
* @param s - The list item type to narrow to (`"text"` or `"object"`)
*/
export function isValueSpecListOf<S extends ListValueSpecType>(
t: ValueSpec,
s: S,