From 8da9d76cb4fbedefac07e39eb147104dce3eba1f Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Wed, 25 Feb 2026 13:35:52 -0700 Subject: [PATCH] feat: add zod-deep-partial, partialValidator on InputSpec, and z.deepPartial re-export --- container-runtime/package-lock.json | 5 +- .../lib/actions/input/builder/inputSpec.ts | 302 ++++++++++++++++-- sdk/base/lib/actions/input/builder/value.ts | 28 +- sdk/base/lib/actions/setupActions.ts | 35 +- sdk/base/lib/index.ts | 16 +- sdk/base/lib/types.ts | 6 + sdk/base/package-lock.json | 13 +- sdk/base/package.json | 3 +- sdk/package/lib/util/fileHelper.ts | 47 ++- sdk/package/package-lock.json | 13 +- sdk/package/package.json | 3 +- web/package-lock.json | 3 +- .../src/app/services/state.service.ts | 1 - .../ui/src/app/utils/configBuilderToSpec.ts | 4 +- 14 files changed, 412 insertions(+), 67 deletions(-) diff --git a/container-runtime/package-lock.json b/container-runtime/package-lock.json index b9d142a3e..6462a9575 100644 --- a/container-runtime/package-lock.json +++ b/container-runtime/package-lock.json @@ -37,7 +37,7 @@ }, "../sdk/dist": { "name": "@start9labs/start-sdk", - "version": "0.4.0-beta.51", + "version": "0.4.0-beta.52", "license": "MIT", "dependencies": { "@iarna/toml": "^3.0.0", @@ -49,7 +49,8 @@ "isomorphic-fetch": "^3.0.0", "mime": "^4.0.7", "yaml": "^2.7.1", - "zod": "^4.3.6" + "zod": "^4.3.6", + "zod-deep-partial": "^1.2.0" }, "devDependencies": { "@types/jest": "^29.4.0", diff --git a/sdk/base/lib/actions/input/builder/inputSpec.ts b/sdk/base/lib/actions/input/builder/inputSpec.ts index 6da5bb346..109c3844c 100644 --- a/sdk/base/lib/actions/input/builder/inputSpec.ts +++ b/sdk/base/lib/actions/input/builder/inputSpec.ts @@ -3,6 +3,7 @@ import { Value } from './value' import { _ } from '../../../util' import { Effects } from '../../../Effects' import { z } from 'zod' +import { zodDeepPartial } from 'zod-deep-partial' import { DeepPartial } from '../../../types' import { InputSpecTools, createInputSpecTools } from './inputSpecTools' @@ -21,6 +22,41 @@ export type LazyBuild = ( options: LazyBuildOptions, ) => Promise | ExpectedOut +/** + * Defines which keys to keep when filtering an InputSpec. + * Use `true` to keep a field as-is, or a nested object to filter sub-fields of an object-typed field. + */ +export type FilterKeys = { + [K in keyof T]?: T[K] extends Record + ? true | FilterKeys + : true +} + +/** + * Computes the resulting type after applying a {@link FilterKeys} shape to a type. + */ +export type ApplyFilter = { + [K in Extract]: F[K] extends true + ? T[K] + : T[K] extends Record + ? F[K] extends FilterKeys + ? ApplyFilter + : T[K] + : T[K] +} + +/** + * Computes the union of all valid key-path tuples through a nested type. + * Each tuple represents a path from root to a field, recursing into object-typed sub-fields. + */ +export type KeyPaths = { + [K in keyof T & string]: T[K] extends any[] + ? [K] + : T[K] extends Record + ? [K] | [K, ...KeyPaths] + : [K] +}[keyof T & string] + /** Extracts the runtime type from an {@link InputSpec}. */ // prettier-ignore export type ExtractInputSpecType, any>> = @@ -111,6 +147,8 @@ export class InputSpec< ) {} public _TYPE: Type = null as any as Type public _PARTIAL: DeepPartial = null as any as DeepPartial + public readonly partialValidator: z.ZodType> = + zodDeepPartial(this.validator) as any /** * Builds the runtime form specification and combined Zod validator from this InputSpec's fields. * @@ -139,35 +177,6 @@ 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: Key, - build: V | ((tools: InputSpecTools) => V), - ): InputSpec< - Type & { [K in Key]: V extends Value ? T : never }, - StaticValidatedAs & { - [K in Key]: V extends Value ? S : never - } - > { - const value = - build instanceof Function ? build(createInputSpecTools()) : build - const newSpec = { ...this.spec, [key]: value } as any - const newValidator = z.object( - Object.fromEntries( - Object.entries(newSpec).map(([k, v]) => [ - k, - (v as Value).validator, - ]), - ), - ) - return new InputSpec(newSpec, newValidator as any) - } - /** * Adds multiple fields to this spec at once, returning a new `InputSpec` with extended types. * @@ -201,6 +210,241 @@ export class InputSpec< return new InputSpec(newSpec, newValidator as any) } + /** + * Returns a new InputSpec containing only the specified keys. + * Use `true` to keep a field as-is, or a nested object to filter sub-fields of object-typed fields. + * + * @example + * ```ts + * const full = InputSpec.of({ + * name: Value.text({ name: 'Name', required: true, default: null }), + * settings: Value.object({ name: 'Settings' }, InputSpec.of({ + * debug: Value.toggle({ name: 'Debug', default: false }), + * port: Value.number({ name: 'Port', required: true, default: 8080, integer: true }), + * })), + * }) + * const filtered = full.filter({ name: true, settings: { debug: true } }) + * ``` + */ + filter>( + keys: F, + ): InputSpec< + ApplyFilter & ApplyFilter, + ApplyFilter + > { + const newSpec: Record> = {} + for (const k of Object.keys(keys)) { + const filterVal = (keys as any)[k] + const value = (this.spec as any)[k] as Value | undefined + if (!value) continue + if (filterVal === true) { + newSpec[k] = value + } else if (typeof filterVal === 'object' && filterVal !== null) { + const objectMeta = value._objectSpec + if (objectMeta) { + const filteredInner = objectMeta.inputSpec.filter(filterVal) + newSpec[k] = Value.object(objectMeta.params, filteredInner) + } else { + newSpec[k] = value + } + } + } + const newValidator = z.object( + Object.fromEntries( + Object.entries(newSpec).map(([k, v]) => [k, v.validator]), + ), + ) + return new InputSpec(newSpec as any, newValidator as any) as any + } + + /** + * Returns a new InputSpec with the specified keys disabled. + * Use `true` to disable a field, or a nested object to disable sub-fields of object-typed fields. + * All fields remain in the spec — disabled fields simply cannot be edited by the user. + * + * @param keys - Which fields to disable, using the same shape as {@link FilterKeys} + * @param message - The reason the fields are disabled, displayed to the user + * + * @example + * ```ts + * const spec = InputSpec.of({ + * name: Value.text({ name: 'Name', required: true, default: null }), + * settings: Value.object({ name: 'Settings' }, InputSpec.of({ + * debug: Value.toggle({ name: 'Debug', default: false }), + * port: Value.number({ name: 'Port', required: true, default: 8080, integer: true }), + * })), + * }) + * const disabled = spec.disable({ name: true, settings: { debug: true } }, 'Managed by the system') + * ``` + */ + disable( + keys: FilterKeys, + message: string, + ): InputSpec { + const newSpec: Record> = {} + for (const k in this.spec) { + const filterVal = (keys as any)[k] + const value = (this.spec as any)[k] as Value + if (!filterVal) { + newSpec[k] = value + } else if (filterVal === true) { + newSpec[k] = value.withDisabled(message) + } else if (typeof filterVal === 'object' && filterVal !== null) { + const objectMeta = value._objectSpec + if (objectMeta) { + const disabledInner = objectMeta.inputSpec.disable(filterVal, message) + newSpec[k] = Value.object(objectMeta.params, disabledInner) + } else { + newSpec[k] = value.withDisabled(message) + } + } + } + const newValidator = z.object( + Object.fromEntries( + Object.entries(newSpec).map(([k, v]) => [k, v.validator]), + ), + ) + return new InputSpec(newSpec as any, newValidator as any) as any + } + + /** + * Resolves a key path to its corresponding display name path. + * Each key is mapped to the `name` property of its built {@link ValueSpec}. + * Recurses into `Value.object` sub-specs for nested paths. + * + * @param path - Typed tuple of field keys (e.g. `["settings", "debug"]`) + * @param options - Build options providing effects and prefill data + * @returns Array of display names (e.g. `["Settings", "Debug"]`) + */ + async namePath( + path: KeyPaths, + options: LazyBuildOptions, + ): Promise { + if (path.length === 0) return [] + const [key, ...rest] = path as [string, ...string[]] + const value = (this.spec as any)[key] as Value | undefined + if (!value) return [] + const built = await value.build(options as any) + const name = + 'name' in built.spec ? (built.spec as { name: string }).name : key + if (rest.length === 0) return [name] + const objectMeta = value._objectSpec + if (objectMeta) { + const innerNames = await objectMeta.inputSpec.namePath( + rest as any, + options, + ) + return [name, ...innerNames] + } + return [name] + } + + /** + * Resolves a key path to the description of the target field. + * Recurses into `Value.object` sub-specs for nested paths. + * + * @param path - Typed tuple of field keys (e.g. `["settings", "debug"]`) + * @param options - Build options providing effects and prefill data + * @returns The description string, or `null` if the field has no description or was not found + */ + async description( + path: KeyPaths, + options: LazyBuildOptions, + ): Promise { + if (path.length === 0) return null + const [key, ...rest] = path as [string, ...string[]] + const value = (this.spec as any)[key] as Value | undefined + if (!value) return null + if (rest.length === 0) { + const built = await value.build(options as any) + return 'description' in built.spec + ? (built.spec as { description: string | null }).description + : null + } + const objectMeta = value._objectSpec + if (objectMeta) { + return objectMeta.inputSpec.description(rest as any, options) + } + return null + } + + /** + * Returns a new InputSpec filtered to only include keys present in the given partial object. + * For nested `Value.object` fields, recurses into the partial value to filter sub-fields. + * + * @param partial - A deep-partial object whose defined keys determine which fields to keep + */ + filterFromPartial( + partial: DeepPartial, + ): InputSpec< + DeepPartial & DeepPartial, + DeepPartial + > { + const newSpec: Record> = {} + for (const k of Object.keys(partial)) { + const value = (this.spec as any)[k] as Value | undefined + if (!value) continue + const objectMeta = value._objectSpec + if (objectMeta) { + const partialVal = (partial as any)[k] + if (typeof partialVal === 'object' && partialVal !== null) { + const filteredInner = + objectMeta.inputSpec.filterFromPartial(partialVal) + newSpec[k] = Value.object(objectMeta.params, filteredInner) + continue + } + } + newSpec[k] = value + } + const newValidator = z.object( + Object.fromEntries( + Object.entries(newSpec).map(([k, v]) => [k, v.validator]), + ), + ) + return new InputSpec(newSpec as any, newValidator as any) as any + } + + /** + * Returns a new InputSpec with fields disabled based on which keys are present in the given partial object. + * For nested `Value.object` fields, recurses into the partial value to disable sub-fields. + * All fields remain in the spec — disabled fields simply cannot be edited by the user. + * + * @param partial - A deep-partial object whose defined keys determine which fields to disable + * @param message - The reason the fields are disabled, displayed to the user + */ + disableFromPartial( + partial: DeepPartial, + message: string, + ): InputSpec { + const newSpec: Record> = {} + for (const k in this.spec) { + const value = (this.spec as any)[k] as Value + if (!(k in (partial as any))) { + newSpec[k] = value + continue + } + const objectMeta = value._objectSpec + if (objectMeta) { + const partialVal = (partial as any)[k] + if (typeof partialVal === 'object' && partialVal !== null) { + const disabledInner = objectMeta.inputSpec.disableFromPartial( + partialVal, + message, + ) + newSpec[k] = Value.object(objectMeta.params, disabledInner) + continue + } + } + newSpec[k] = value.withDisabled(message) + } + const newValidator = z.object( + Object.fromEntries( + Object.entries(newSpec).map(([k, v]) => [k, v.validator]), + ), + ) + return new InputSpec(newSpec as any, newValidator as any) as any + } + /** * Creates an `InputSpec` from a plain record of {@link Value} entries. * diff --git a/sdk/base/lib/actions/input/builder/value.ts b/sdk/base/lib/actions/input/builder/value.ts index 6335754f9..e916e9ecf 100644 --- a/sdk/base/lib/actions/input/builder/value.ts +++ b/sdk/base/lib/actions/input/builder/value.ts @@ -70,6 +70,11 @@ export class Value< ) {} public _TYPE: Type = null as any as Type public _PARTIAL: DeepPartial = null as any as DeepPartial + /** @internal Used by {@link InputSpec.filter} to support nested filtering of object-typed fields. */ + _objectSpec?: { + inputSpec: InputSpec + params: { name: string; description?: string | null } + } /** * @description Displays a boolean toggle to enable/disable @@ -987,7 +992,7 @@ export class Value< }, spec: InputSpec, ) { - return new Value(async (options) => { + const value = new Value(async (options) => { const built = await spec.build(options as any) return { spec: { @@ -1000,6 +1005,8 @@ export class Value< validator: built.validator, } }, spec.validator) + value._objectSpec = { inputSpec: spec, params: a } + return value } /** * Displays a file upload input field. @@ -1333,6 +1340,25 @@ export class Value< }, z.any()) } + /** + * Returns a new Value that produces the same field spec but with `disabled` set to the given message. + * The field remains in the form but cannot be edited by the user. + * + * @param message - The reason the field is disabled, displayed to the user + */ + withDisabled(message: string): Value { + const original = this + const v = new Value(async (options) => { + const built = await original.build(options) + return { + spec: { ...built.spec, disabled: message } as ValueSpec, + validator: built.validator, + } + }, this.validator) + v._objectSpec = this._objectSpec + return v + } + /** * Transforms the validated output value using a mapping function. * The form field itself remains unchanged, but the value is transformed after validation. diff --git a/sdk/base/lib/actions/setupActions.ts b/sdk/base/lib/actions/setupActions.ts index 8c05a7e71..87e1a9d48 100644 --- a/sdk/base/lib/actions/setupActions.ts +++ b/sdk/base/lib/actions/setupActions.ts @@ -16,10 +16,12 @@ export type GetInput> = (options: { prefill: T.DeepPartial | null }) => Promise> -export type MaybeFn = T | ((options: { effects: T.Effects }) => Promise) -function callMaybeFn( - maybeFn: MaybeFn, - options: { effects: T.Effects }, +export type MaybeFn = + | T + | ((options: Opts) => Promise) +function callMaybeFn( + maybeFn: MaybeFn, + options: Opts, ): Promise { if (maybeFn instanceof Function) { return maybeFn(options) @@ -57,7 +59,13 @@ export class Action> private constructor( readonly id: Id, private readonly metadataFn: MaybeFn, - private readonly inputSpec: MaybeInputSpec, + private readonly inputSpec: MaybeFn< + MaybeInputSpec, + { + effects: T.Effects + prefill: unknown | null + } + >, private readonly getInputFn: GetInput, private readonly runFn: Run, ) {} @@ -67,7 +75,13 @@ export class Action> >( id: Id, metadata: MaybeFn>, - inputSpec: InputSpecType, + inputSpec: MaybeFn< + InputSpecType, + { + effects: T.Effects + prefill: unknown | null + } + >, getInput: GetInput>, run: Run>, ): Action> { @@ -111,9 +125,12 @@ export class Action> }): Promise { let spec = {} if (this.inputSpec) { - const built = await this.inputSpec.build(options) - this.prevInputSpec[options.effects.eventId!] = built - spec = built.spec + const inputSpec = await callMaybeFn(this.inputSpec, options) + const built = await inputSpec?.build(options) + if (built) { + this.prevInputSpec[options.effects.eventId!] = built + spec = built.spec + } } return { eventId: options.effects.eventId!, diff --git a/sdk/base/lib/index.ts b/sdk/base/lib/index.ts index bf2cc1e4b..5909ad7d1 100644 --- a/sdk/base/lib/index.ts +++ b/sdk/base/lib/index.ts @@ -8,6 +8,20 @@ export * as types from './types' export * as T from './types' export * as yaml from 'yaml' export * as inits from './inits' -export { z } from 'zod' +import { z as _z } from 'zod' +import { zodDeepPartial } from 'zod-deep-partial' +import { DeepPartial } from './types' + +type ZodDeepPartial = (a: _z.ZodType) => _z.ZodType> + +export const z: typeof _z & { + deepPartial: ZodDeepPartial +} = Object.assign(_z, { deepPartial: zodDeepPartial as ZodDeepPartial }) +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace z { + export type infer = T['_zod']['output'] + export type input = T['_zod']['input'] + export type output = T['_zod']['output'] +} export * as utils from './util' diff --git a/sdk/base/lib/types.ts b/sdk/base/lib/types.ts index ccf371d61..41259d0d2 100644 --- a/sdk/base/lib/types.ts +++ b/sdk/base/lib/types.ts @@ -1,4 +1,5 @@ export * as inputSpecTypes from './actions/input/inputSpecTypes' +import { InputSpec as InputSpecClass } from './actions/input/builder/inputSpec' import { DependencyRequirement, @@ -267,3 +268,8 @@ export type AllowReadonly = | { readonly [P in keyof T]: AllowReadonly } + +export type InputSpec< + Type extends StaticValidatedAs, + StaticValidatedAs extends Record = Type, +> = InputSpecClass diff --git a/sdk/base/package-lock.json b/sdk/base/package-lock.json index d20579b22..e96432bed 100644 --- a/sdk/base/package-lock.json +++ b/sdk/base/package-lock.json @@ -14,7 +14,8 @@ "isomorphic-fetch": "^3.0.0", "mime": "^4.0.7", "yaml": "^2.7.1", - "zod": "^4.3.6" + "zod": "^4.3.6", + "zod-deep-partial": "^1.2.0" }, "devDependencies": { "@types/jest": "^29.4.0", @@ -5006,9 +5007,19 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zod-deep-partial": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/zod-deep-partial/-/zod-deep-partial-1.2.0.tgz", + "integrity": "sha512-dXfte+/YN0aFYs0kMGz6xfPQWEYNaKz/LsbfxrbwL+oY3l/aR9HOBTyWCpHZ5AJXMGWKSq+0X0oVPpRliUFcjQ==", + "license": "MIT", + "peerDependencies": { + "zod": "^4.1.13" + } } } } diff --git a/sdk/base/package.json b/sdk/base/package.json index dce2ca2db..59d6d5455 100644 --- a/sdk/base/package.json +++ b/sdk/base/package.json @@ -28,7 +28,8 @@ "isomorphic-fetch": "^3.0.0", "mime": "^4.0.7", "yaml": "^2.7.1", - "zod": "^4.3.6" + "zod": "^4.3.6", + "zod-deep-partial": "^1.2.0" }, "prettier": { "trailingComma": "all", diff --git a/sdk/package/lib/util/fileHelper.ts b/sdk/package/lib/util/fileHelper.ts index de1934f28..13a45922c 100644 --- a/sdk/package/lib/util/fileHelper.ts +++ b/sdk/package/lib/util/fileHelper.ts @@ -91,11 +91,15 @@ function filterUndefined(a: A): A { * @typeParam Raw - The native type the file format parses to (e.g. `Record` for JSON) * @typeParam Transformed - The application-level type after transformation */ -export type Transformers = { +export type Transformers< + Raw = unknown, + Transformed = unknown, + Validated extends Transformed = Transformed, +> = { /** Transform raw parsed data into the application type */ onRead: (value: Raw) => Transformed /** Transform application data back into the raw format for writing */ - onWrite: (value: Transformed) => Raw + onWrite: (value: Validated) => Raw } type ToPath = string | { base: PathBase; subpath: string } @@ -483,7 +487,7 @@ export class FileHelper { toFile: (dataIn: Raw) => string, fromFile: (rawData: string) => Raw, validate: (data: Transformed) => A, - transformers: Transformers | undefined, + transformers: Transformers | undefined, ) { return FileHelper.raw( path, @@ -493,7 +497,12 @@ export class FileHelper { } return toFile(inData as any as Raw) }, - fromFile, + (fileData) => { + if (transformers) { + return transformers.onRead(fromFile(fileData)) + } + return fromFile(fileData) + }, validate as (a: unknown) => A, ) } @@ -509,12 +518,12 @@ export class FileHelper { static string( path: ToPath, shape: Validator, - transformers: Transformers, + transformers: Transformers, ): FileHelper static string( path: ToPath, shape?: Validator, - transformers?: Transformers, + transformers?: Transformers, ) { return FileHelper.rawTransformed( path, @@ -531,10 +540,16 @@ export class FileHelper { /** * Create a File Helper for a .json file. */ - static json( + static json(path: ToPath, shape: Validator): FileHelper + static json( path: ToPath, shape: Validator, - transformers?: Transformers, + transformers: Transformers, + ): FileHelper + static json( + path: ToPath, + shape: Validator, + transformers?: Transformers, ) { return FileHelper.rawTransformed( path, @@ -555,12 +570,12 @@ export class FileHelper { static yaml>( path: ToPath, shape: Validator, - transformers: Transformers, Transformed>, + transformers: Transformers, Transformed, A>, ): FileHelper static yaml>( path: ToPath, shape: Validator, - transformers?: Transformers, Transformed>, + transformers?: Transformers, Transformed, A>, ) { return FileHelper.rawTransformed, Transformed>( path, @@ -581,12 +596,12 @@ export class FileHelper { static toml>( path: ToPath, shape: Validator, - transformers: Transformers, Transformed>, + transformers: Transformers, Transformed, A>, ): FileHelper static toml>( path: ToPath, shape: Validator, - transformers?: Transformers, Transformed>, + transformers?: Transformers, Transformed, A>, ) { return FileHelper.rawTransformed, Transformed>( path, @@ -611,13 +626,13 @@ export class FileHelper { path: ToPath, shape: Validator, options: INI.EncodeOptions & INI.DecodeOptions, - transformers: Transformers, Transformed>, + transformers: Transformers, Transformed, A>, ): FileHelper static ini>( path: ToPath, shape: Validator, options?: INI.EncodeOptions & INI.DecodeOptions, - transformers?: Transformers, Transformed>, + transformers?: Transformers, Transformed, A>, ): FileHelper { return FileHelper.rawTransformed, Transformed>( path, @@ -640,12 +655,12 @@ export class FileHelper { static env>( path: ToPath, shape: Validator, - transformers: Transformers, Transformed>, + transformers: Transformers, Transformed, A>, ): FileHelper static env>( path: ToPath, shape: Validator, - transformers?: Transformers, Transformed>, + transformers?: Transformers, Transformed, A>, ) { return FileHelper.rawTransformed, Transformed>( path, diff --git a/sdk/package/package-lock.json b/sdk/package/package-lock.json index 793d92229..357403d6b 100644 --- a/sdk/package/package-lock.json +++ b/sdk/package/package-lock.json @@ -18,7 +18,8 @@ "isomorphic-fetch": "^3.0.0", "mime": "^4.0.7", "yaml": "^2.7.1", - "zod": "^4.3.6" + "zod": "^4.3.6", + "zod-deep-partial": "^1.2.0" }, "devDependencies": { "@types/jest": "^29.4.0", @@ -5232,9 +5233,19 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zod-deep-partial": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/zod-deep-partial/-/zod-deep-partial-1.4.4.tgz", + "integrity": "sha512-aWkPl7hVStgE01WzbbSxCgX4O+sSpgt8JOjvFUtMTF75VgL6MhWQbiZi+AWGN85SfSTtI9gsOtL1vInoqfDVaA==", + "license": "MIT", + "peerDependencies": { + "zod": "^4.1.13" + } } } } diff --git a/sdk/package/package.json b/sdk/package/package.json index 3a1481a21..17e33f606 100644 --- a/sdk/package/package.json +++ b/sdk/package/package.json @@ -40,7 +40,8 @@ "isomorphic-fetch": "^3.0.0", "mime": "^4.0.7", "yaml": "^2.7.1", - "zod": "^4.3.6" + "zod": "^4.3.6", + "zod-deep-partial": "^1.2.0" }, "prettier": { "trailingComma": "all", diff --git a/web/package-lock.json b/web/package-lock.json index e32f3dea3..869b99834 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -126,7 +126,8 @@ "isomorphic-fetch": "^3.0.0", "mime": "^4.0.7", "yaml": "^2.7.1", - "zod": "^4.3.6" + "zod": "^4.3.6", + "zod-deep-partial": "^1.2.0" }, "devDependencies": { "@types/jest": "^29.4.0", diff --git a/web/projects/setup-wizard/src/app/services/state.service.ts b/web/projects/setup-wizard/src/app/services/state.service.ts index abc0535b4..a36f08d3e 100644 --- a/web/projects/setup-wizard/src/app/services/state.service.ts +++ b/web/projects/setup-wizard/src/app/services/state.service.ts @@ -82,7 +82,6 @@ export class StateService { await this.api.execute({ guid: this.dataDriveGuid, - // @ts-expect-error TODO: backend should make password optional for restore/transfer password: password ? await this.api.encrypt(password) : null, name, hostname, diff --git a/web/projects/ui/src/app/utils/configBuilderToSpec.ts b/web/projects/ui/src/app/utils/configBuilderToSpec.ts index b2eee7dbf..2ffa249f5 100644 --- a/web/projects/ui/src/app/utils/configBuilderToSpec.ts +++ b/web/projects/ui/src/app/utils/configBuilderToSpec.ts @@ -1,7 +1,5 @@ import { ISB } from '@start9labs/start-sdk' -export async function configBuilderToSpec( - builder: ISB.InputSpec>, -) { +export async function configBuilderToSpec(builder: ISB.InputSpec) { return builder.build({} as any).then(a => a.spec) }