From 68955c29cbdc80e7d0ec3a2830a195854e1ff8bd Mon Sep 17 00:00:00 2001 From: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Date: Tue, 6 May 2025 11:04:11 -0600 Subject: [PATCH] add transformers to file helpers (#2922) * fix undefined handling in INI * beta.14 * Partial -> DeepPartial in action request * boolean laziness kills * beta.16 * misc fixes * file transformers * infer validator source argument * simplify validator * readd toml * beta.17 * filter undefined instead of parse/stringify * handle arrays of objects in filterUndefined --- container-runtime/package-lock.json | 2 +- .../src/Adapters/EffectCreator.ts | 1 + container-runtime/src/Adapters/RpcListener.ts | 3 +- .../src/Models/CallbackHolder.ts | 11 +- core/startos/src/service/action.rs | 2 +- sdk/base/lib/actions/index.ts | 2 +- sdk/package/lib/mainFn/Daemon.ts | 4 +- sdk/package/lib/util/fileHelper.ts | 135 +++++++++++++++--- sdk/package/package-lock.json | 15 +- sdk/package/package.json | 5 +- 10 files changed, 139 insertions(+), 41 deletions(-) diff --git a/container-runtime/package-lock.json b/container-runtime/package-lock.json index d2436d4fb..ecbcdb415 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.10", + "version": "0.4.0-beta.17", "license": "MIT", "dependencies": { "@iarna/toml": "^3.0.0", diff --git a/container-runtime/src/Adapters/EffectCreator.ts b/container-runtime/src/Adapters/EffectCreator.ts index 4cc29c0d3..014c4b0be 100644 --- a/container-runtime/src/Adapters/EffectCreator.ts +++ b/container-runtime/src/Adapters/EffectCreator.ts @@ -265,6 +265,7 @@ export function makeEffects(context: EffectContext): Effects { return rpcRound("mount", options) as ReturnType }, restart(...[]: Parameters) { + console.log("Restarting service...") return rpcRound("restart", {}) as ReturnType }, setDependencies( diff --git a/container-runtime/src/Adapters/RpcListener.ts b/container-runtime/src/Adapters/RpcListener.ts index fb1ee1e75..59572ba28 100644 --- a/container-runtime/src/Adapters/RpcListener.ts +++ b/container-runtime/src/Adapters/RpcListener.ts @@ -289,7 +289,8 @@ export class RpcListener { return null }) .when(startType, async ({ id }) => { - const callbacks = this.callbacks?.child("main") + const callbacks = + this.callbacks?.getChild("main") || this.callbacks?.child("main") const effects = makeEffects({ procedureId: null, callbacks, diff --git a/container-runtime/src/Models/CallbackHolder.ts b/container-runtime/src/Models/CallbackHolder.ts index e33398f53..4a3d24b0f 100644 --- a/container-runtime/src/Models/CallbackHolder.ts +++ b/container-runtime/src/Models/CallbackHolder.ts @@ -35,10 +35,15 @@ export class CallbackHolder { } child(name: string): CallbackHolder { this.removeChild(name) - const child = new CallbackHolder() + const child = new CallbackHolder(this.effects) this.children.set(name, child) return child } + + getChild(name: string): CallbackHolder | null { + return this.children.get(name) || null + } + removeChild(name: string) { const child = this.children.get(name) if (child) { @@ -60,7 +65,9 @@ export class CallbackHolder { callCallback(index: number, args: any[]): Promise { const callback = this.getCallback(index) if (!callback) return Promise.resolve() - return Promise.resolve().then(() => callback(...args)) + return Promise.resolve() + .then(() => callback(...args)) + .catch((e) => console.error("callback failed", e)) } onLeaveContext(fn: Function) { this.onLeaveContextCallbacks.push(fn) diff --git a/core/startos/src/service/action.rs b/core/startos/src/service/action.rs index bb1af6432..65687f35b 100644 --- a/core/startos/src/service/action.rs +++ b/core/startos/src/service/action.rs @@ -147,7 +147,7 @@ impl Handler for ServiceActor { .into_idx(&action_id) .or_not_found(lazy_format!("{package_id} action {action_id}"))? .de()?; - if !matches!(&action.visibility, ActionVisibility::Enabled) { + if matches!(&action.visibility, ActionVisibility::Disabled(_)) { return Err(Error::new( eyre!("action {action_id} is disabled"), ErrorKind::Action, diff --git a/sdk/base/lib/actions/index.ts b/sdk/base/lib/actions/index.ts index 4bcbec8b1..0701e00e0 100644 --- a/sdk/base/lib/actions/index.ts +++ b/sdk/base/lib/actions/index.ts @@ -54,7 +54,7 @@ type ActionRequestBase = { } type ActionRequestInput> = { kind: "partial" - value: Partial> + value: T.DeepPartial> } export type ActionRequestOptions> = ActionRequestBase & diff --git a/sdk/package/lib/mainFn/Daemon.ts b/sdk/package/lib/mainFn/Daemon.ts index 30af2ce92..eb4e94f5e 100644 --- a/sdk/package/lib/mainFn/Daemon.ts +++ b/sdk/package/lib/mainFn/Daemon.ts @@ -69,14 +69,14 @@ export class Daemon extends Drop { .catch((err) => console.error(err)) this.commandController = await this.startCommand() if ( - this.oneshot && (await this.commandController.wait({ keepSubcontainer: true }).then( (_) => true, (err) => { console.error(err) return false }, - )) + )) && + this.oneshot ) { for (const fn of this.onExitSuccessFns) { try { diff --git a/sdk/package/lib/util/fileHelper.ts b/sdk/package/lib/util/fileHelper.ts index ca505148b..58729cb1d 100644 --- a/sdk/package/lib/util/fileHelper.ts +++ b/sdk/package/lib/util/fileHelper.ts @@ -61,6 +61,28 @@ function fileMerge(...args: any[]): any { return res } +function filterUndefined(a: A): A { + if (a && typeof a === "object") { + if (Array.isArray(a)) { + return a.map(filterUndefined) as A + } + return Object.entries(a).reduce>((acc, [k, v]) => { + if (v !== undefined) { + acc[k] = filterUndefined(v) + } + return acc + }, {}) as A + } + return a +} + +export type Transformers = { + onRead: (value: Raw) => Transformed + onWrite: (value: Transformed) => Raw +} + +type Validator = matches.Validator | matches.Validator + /** * @description Use this class to read/write an underlying configuration file belonging to the upstream service. * @@ -287,30 +309,40 @@ export class FileHelper { return new FileHelper(path, toFile, fromFile, validate) } - /** - * Create a File Helper for a .json file. - */ - static json(path: string, shape: matches.Validator) { + private static rawTransformed( + path: string, + toFile: (dataIn: Raw) => string, + fromFile: (rawData: string) => Raw, + validate: (data: Transformed) => A, + transformers: Transformers | undefined, + ) { return new FileHelper( path, - (inData) => JSON.stringify(inData, null, 2), - (inString) => JSON.parse(inString), - (data) => shape.unsafeCast(data), + (inData) => { + if (transformers) { + return toFile(transformers.onWrite(inData)) + } + return toFile(inData as any as Raw) + }, + fromFile, + validate as (a: unknown) => A, ) } /** - * Create a File Helper for a .toml file + * Create a File Helper for a .json file. */ - static toml>( + static json( path: string, - shape: matches.Validator, + shape: Validator, + transformers?: Transformers, ) { - return new FileHelper( + return FileHelper.rawTransformed( path, - (inData) => TOML.stringify(inData as any), - (inString) => TOML.parse(inString), + (inData) => JSON.stringify(inData, null, 2), + (inString) => JSON.parse(inString), (data) => shape.unsafeCast(data), + transformers, ) } @@ -319,34 +351,94 @@ export class FileHelper { */ static yaml>( path: string, - shape: matches.Validator, + shape: Validator, A>, + ): FileHelper + static yaml>( + path: string, + shape: Validator, + transformers: Transformers, Transformed>, + ): FileHelper + static yaml>( + path: string, + shape: Validator, + transformers?: Transformers, Transformed>, ) { - return new FileHelper( + return FileHelper.rawTransformed, Transformed>( path, (inData) => YAML.stringify(inData, null, 2), (inString) => YAML.parse(inString), (data) => shape.unsafeCast(data), + transformers, + ) + } + + /** + * Create a File Helper for a .toml file + */ + static toml( + path: string, + shape: Validator, + ): FileHelper + static toml( + path: string, + shape: Validator, + transformers: Transformers, + ): FileHelper + static toml( + path: string, + shape: Validator, + transformers?: Transformers, + ) { + return FileHelper.rawTransformed( + path, + (inData) => TOML.stringify(inData), + (inString) => TOML.parse(inString), + (data) => shape.unsafeCast(data), + transformers, ) } static ini>( path: string, - shape: matches.Validator, + shape: Validator, A>, options?: INI.EncodeOptions & INI.DecodeOptions, - ) { - return new FileHelper( + ): FileHelper + static ini>( + path: string, + shape: Validator, + options: INI.EncodeOptions & INI.DecodeOptions, + transformers: Transformers, Transformed>, + ): FileHelper + static ini>( + path: string, + shape: Validator, + options?: INI.EncodeOptions & INI.DecodeOptions, + transformers?: Transformers, Transformed>, + ): FileHelper { + return FileHelper.rawTransformed, Transformed>( path, - (inData) => INI.stringify(inData, options), + (inData) => INI.stringify(filterUndefined(inData), options), (inString) => INI.parse(inString, options), (data) => shape.unsafeCast(data), + transformers, ) } static env>( path: string, - shape: matches.Validator, + shape: Validator, A>, + ): FileHelper + static env>( + path: string, + shape: Validator, + transformers: Transformers, Transformed>, + ): FileHelper + static env>( + path: string, + shape: Validator, + transformers?: Transformers, Transformed>, ) { - return new FileHelper( + return FileHelper.rawTransformed, Transformed>( path, (inData) => Object.entries(inData) @@ -361,6 +453,7 @@ export class FileHelper { .map((line) => line.split("=", 2)), ), (data) => shape.unsafeCast(data), + transformers, ) } } diff --git a/sdk/package/package-lock.json b/sdk/package/package-lock.json index 94783fa06..227b1e450 100644 --- a/sdk/package/package-lock.json +++ b/sdk/package/package-lock.json @@ -1,17 +1,18 @@ { "name": "@start9labs/start-sdk", - "version": "0.4.0-beta.13", + "version": "0.4.0-beta.17", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@start9labs/start-sdk", - "version": "0.4.0-beta.13", + "version": "0.4.0-beta.17", "license": "MIT", "dependencies": { "@iarna/toml": "^3.0.0", "@noble/curves": "^1.8.2", "@noble/hashes": "^1.7.2", + "@types/ini": "^4.1.1", "deep-equality-data-structures": "^2.0.0", "ini": "^5.0.0", "isomorphic-fetch": "^3.0.0", @@ -20,8 +21,6 @@ "yaml": "^2.7.1" }, "devDependencies": { - "@iarna/toml": "^2.2.5", - "@types/ini": "^4.1.1", "@types/jest": "^29.4.0", "copyfiles": "^2.4.1", "jest": "^29.4.3", @@ -987,10 +986,9 @@ } }, "node_modules/@iarna/toml": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", - "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==", - "dev": true, + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q==", "license": "ISC" }, "node_modules/@istanbuljs/load-nyc-config": { @@ -1599,7 +1597,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/@types/ini/-/ini-4.1.1.tgz", "integrity": "sha512-MIyNUZipBTbyUNnhvuXJTY7B6qNI78meck9Jbv3wk0OgNwRyOOVEKDutAkOs1snB/tx0FafyR6/SN4Ps0hZPeg==", - "dev": true, "license": "MIT" }, "node_modules/@types/istanbul-lib-coverage": { diff --git a/sdk/package/package.json b/sdk/package/package.json index 92c33ed3d..320d465c1 100644 --- a/sdk/package/package.json +++ b/sdk/package/package.json @@ -1,6 +1,6 @@ { "name": "@start9labs/start-sdk", - "version": "0.4.0-beta.13", + "version": "0.4.0-beta.17", "description": "Software development kit to facilitate packaging services for StartOS", "main": "./package/lib/index.js", "types": "./package/lib/index.d.ts", @@ -37,6 +37,7 @@ "yaml": "^2.7.1", "deep-equality-data-structures": "^2.0.0", "ini": "^5.0.0", + "@types/ini": "^4.1.1", "@iarna/toml": "^3.0.0", "@noble/curves": "^1.8.2", "@noble/hashes": "^1.7.2" @@ -48,9 +49,7 @@ "singleQuote": false }, "devDependencies": { - "@iarna/toml": "^2.2.5", "@types/jest": "^29.4.0", - "@types/ini": "^4.1.1", "copyfiles": "^2.4.1", "jest": "^29.4.3", "peggy": "^3.0.2",