From 7750e33f821749aa5ea9f0fdc62673ab3cc08134 Mon Sep 17 00:00:00 2001 From: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Date: Fri, 9 May 2025 15:10:51 -0600 Subject: [PATCH] misc sdk changes (#2934) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * misc sdk changes * delete the store ☠️ * port comments * fix build * fix removing * fix tests * beta.20 --------- Co-authored-by: Matt Hill --- container-runtime/package-lock.json | 13 +- container-runtime/package.json | 2 +- .../src/Adapters/EffectCreator.ts | 46 +- container-runtime/src/Adapters/RpcListener.ts | 153 ++--- .../DockerProcedureContainer.ts | 6 +- .../Systems/SystemForEmbassy/index.ts | 261 ++++---- .../Systems/SystemForEmbassy/matchManifest.ts | 212 +++--- .../Systems/SystemForEmbassy/matchVolume.ts | 11 +- .../SystemForEmbassy/polyfillEffects.ts | 4 +- .../SystemForEmbassy/transformConfigSpec.ts | 216 +++--- .../src/Adapters/Systems/SystemForStartOs.ts | 3 +- .../src/Models/DockerProcedure.ts | 46 +- core/models/src/errors.rs | 19 + core/models/src/id/volume.rs | 12 +- core/startos/src/install/mod.rs | 29 +- core/startos/src/s9pk/v2/compat.rs | 3 +- core/startos/src/service/effects/callbacks.rs | 58 -- .../startos/src/service/effects/dependency.rs | 20 +- core/startos/src/service/effects/mod.rs | 17 +- core/startos/src/service/effects/store.rs | 143 ---- core/startos/src/service/effects/version.rs | 51 ++ core/startos/src/service/service_map.rs | 54 +- core/startos/src/ssh.rs | 14 +- core/startos/src/system.rs | 9 +- sdk/base/lib/Effects.ts | 20 - sdk/base/lib/actions/index.ts | 10 +- .../lib/actions/input/builder/inputSpec.ts | 67 +- sdk/base/lib/actions/input/builder/list.ts | 67 +- sdk/base/lib/actions/input/builder/value.ts | 619 ++++++++++++------ .../lib/actions/input/builder/variants.ts | 32 +- .../lib/actions/input/inputSpecConstants.ts | 2 +- sdk/base/lib/actions/setupActions.ts | 39 +- sdk/base/lib/backup/Backups.ts | 2 +- .../osBindings/ExposeForDependentsParams.ts | 3 - sdk/base/lib/osBindings/GetStoreParams.ts | 9 - sdk/base/lib/osBindings/SetStoreParams.ts | 3 - sdk/base/lib/osBindings/index.ts | 3 - .../lib/test/startosTypeValidation.test.ts | 6 - sdk/base/lib/types.ts | 11 +- sdk/base/lib/util/PathBuilder.ts | 38 -- sdk/base/lib/util/index.ts | 1 - .../lcov-report/lib/mainFn/Mounts.ts.html | 6 +- sdk/package/lib/StartSdk.ts | 515 +-------------- sdk/package/lib/backup/Backups.ts | 21 +- sdk/package/lib/health/HealthCheck.ts | 2 +- sdk/package/lib/index.ts | 2 - sdk/package/lib/inits/setupInit.ts | 19 +- sdk/package/lib/inits/setupInstall.ts | 40 +- sdk/package/lib/inits/setupUninstall.ts | 14 +- sdk/package/lib/mainFn/Daemons.ts | 2 + sdk/package/lib/mainFn/Mounts.ts | 24 +- sdk/package/lib/mainFn/index.ts | 2 +- sdk/package/lib/store/getStore.ts | 95 --- sdk/package/lib/store/setupExposeStore.ts | 27 - sdk/package/lib/test/inputSpecBuilder.test.ts | 25 +- sdk/package/lib/test/output.sdk.ts | 2 - sdk/package/lib/test/store.test.ts | 111 ---- sdk/package/lib/util/SubContainer.ts | 18 +- sdk/package/lib/util/fileHelper.ts | 116 +++- sdk/package/package-lock.json | 4 +- sdk/package/package.json | 2 +- .../ui/src/app/utils/configBuilderToSpec.ts | 4 +- 62 files changed, 1255 insertions(+), 2130 deletions(-) delete mode 100644 core/startos/src/service/effects/store.rs create mode 100644 core/startos/src/service/effects/version.rs delete mode 100644 sdk/base/lib/osBindings/ExposeForDependentsParams.ts delete mode 100644 sdk/base/lib/osBindings/GetStoreParams.ts delete mode 100644 sdk/base/lib/osBindings/SetStoreParams.ts delete mode 100644 sdk/base/lib/util/PathBuilder.ts delete mode 100644 sdk/package/lib/store/getStore.ts delete mode 100644 sdk/package/lib/store/setupExposeStore.ts delete mode 100644 sdk/package/lib/test/store.test.ts diff --git a/container-runtime/package-lock.json b/container-runtime/package-lock.json index 3b6564041..6c1f000c2 100644 --- a/container-runtime/package-lock.json +++ b/container-runtime/package-lock.json @@ -18,7 +18,7 @@ "jsonpath": "^1.1.1", "lodash.merge": "^4.6.2", "node-fetch": "^3.1.0", - "ts-matches": "^5.5.1", + "ts-matches": "^6.3.2", "tslib": "^2.5.3", "typescript": "^5.1.3", "yaml": "^2.3.1" @@ -37,12 +37,13 @@ }, "../sdk/dist": { "name": "@start9labs/start-sdk", - "version": "0.4.0-beta.18", + "version": "0.4.0-beta.20", "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", @@ -51,8 +52,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", @@ -6476,9 +6475,9 @@ } }, "node_modules/ts-matches": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-5.6.1.tgz", - "integrity": "sha512-1QXWQUa14MCgbz7vMg7i7eVPhMKB/5w8808nkN2sfnDkbG9nWYr9IwuTxX+h99yyawHYS53DewShA2RYCbSW4Q==", + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-6.3.2.tgz", + "integrity": "sha512-UhSgJymF8cLd4y0vV29qlKVCkQpUtekAaujXbQVc729FezS8HwqzepqvtjzQ3HboatIqN/Idor85O2RMwT7lIQ==", "license": "MIT" }, "node_modules/tslib": { diff --git a/container-runtime/package.json b/container-runtime/package.json index 0a8e4afa8..fff13ba30 100644 --- a/container-runtime/package.json +++ b/container-runtime/package.json @@ -27,7 +27,7 @@ "jsonpath": "^1.1.1", "lodash.merge": "^4.6.2", "node-fetch": "^3.1.0", - "ts-matches": "^5.5.1", + "ts-matches": "^6.3.2", "tslib": "^2.5.3", "typescript": "^5.1.3", "yaml": "^2.3.1" diff --git a/container-runtime/src/Adapters/EffectCreator.ts b/container-runtime/src/Adapters/EffectCreator.ts index 014c4b0be..aed7e68a1 100644 --- a/container-runtime/src/Adapters/EffectCreator.ts +++ b/container-runtime/src/Adapters/EffectCreator.ts @@ -6,23 +6,19 @@ import { Effects } from "../Models/Effects" import { CallbackHolder } from "../Models/CallbackHolder" import { asError } from "@start9labs/start-sdk/base/lib/util" const matchRpcError = object({ - error: object( - { - code: number, - message: string, - data: some( - string, - object( - { - details: string, - debug: string, - }, - ["debug"], - ), - ), - }, - ["data"], - ), + error: object({ + code: number, + message: string, + data: some( + string, + object({ + details: string, + debug: string.nullable().optional(), + }), + ) + .nullable() + .optional(), + }), }) const testRpcError = matchRpcError.test const testRpcResult = object({ @@ -197,13 +193,6 @@ export function makeEffects(context: EffectContext): Effects { T.Effects["exportServiceInterface"] > }) as Effects["exportServiceInterface"], - exposeForDependents( - ...[options]: Parameters - ) { - return rpcRound("expose-for-dependents", options) as ReturnType< - T.Effects["exposeForDependents"] - > - }, getContainerIp(...[options]: Parameters) { return rpcRound("get-container-ip", options) as ReturnType< T.Effects["getContainerIp"] @@ -305,15 +294,6 @@ export function makeEffects(context: EffectContext): Effects { shutdown(...[]: Parameters) { return rpcRound("shutdown", {}) as ReturnType }, - store: { - get: async (options: any) => - rpcRound("store.get", { - ...options, - callback: context.callbacks?.addCallback(options.callback) || null, - }) as any, - set: async (options: any) => - rpcRound("store.set", options) as ReturnType, - } as T.Effects["store"], getDataVersion() { return rpcRound("get-data-version", {}) as ReturnType< T.Effects["getDataVersion"] diff --git a/container-runtime/src/Adapters/RpcListener.ts b/container-runtime/src/Adapters/RpcListener.ts index 59572ba28..23893a95f 100644 --- a/container-runtime/src/Adapters/RpcListener.ts +++ b/container-runtime/src/Adapters/RpcListener.ts @@ -26,20 +26,16 @@ type MaybePromise = T | Promise export const matchRpcResult = anyOf( object({ result: any }), object({ - error: object( - { - code: number, - message: string, - data: object( - { - details: string, - debug: any, - }, - ["details", "debug"], - ), - }, - ["data"], - ), + error: object({ + code: number, + message: string, + data: object({ + details: string.optional(), + debug: any.optional(), + }) + .nullable() + .optional(), + }), }), ) @@ -54,38 +50,26 @@ const isResult = object({ result: any }).test const idType = some(string, number, literal(null)) type IdType = null | string | number | undefined -const runType = object( - { - id: idType, - method: literal("execute"), - params: object( - { - id: string, - procedure: string, - input: any, - timeout: number, - }, - ["timeout"], - ), - }, - ["id"], -) -const sandboxRunType = object( - { - id: idType, - method: literal("sandbox"), - params: object( - { - id: string, - procedure: string, - input: any, - timeout: number, - }, - ["timeout"], - ), - }, - ["id"], -) +const runType = object({ + id: idType.optional(), + method: literal("execute"), + params: object({ + id: string, + procedure: string, + input: any, + timeout: number.nullable().optional(), + }), +}) +const sandboxRunType = object({ + id: idType.optional(), + method: literal("sandbox"), + params: object({ + id: string, + procedure: string, + input: any, + timeout: number.nullable().optional(), + }), +}) const callbackType = object({ method: literal("callback"), params: object({ @@ -93,44 +77,29 @@ const callbackType = object({ args: array, }), }) -const initType = object( - { - id: idType, - method: literal("init"), - }, - ["id"], -) -const startType = object( - { - id: idType, - method: literal("start"), - }, - ["id"], -) -const stopType = object( - { - id: idType, - method: literal("stop"), - }, - ["id"], -) -const exitType = object( - { - id: idType, - method: literal("exit"), - }, - ["id"], -) -const evalType = object( - { - id: idType, - method: literal("eval"), - params: object({ - script: string, - }), - }, - ["id"], -) +const initType = object({ + id: idType.optional(), + method: literal("init"), +}) +const startType = object({ + id: idType.optional(), + method: literal("start"), +}) +const stopType = object({ + id: idType.optional(), + method: literal("stop"), +}) +const exitType = object({ + id: idType.optional(), + method: literal("exit"), +}) +const evalType = object({ + id: idType.optional(), + method: literal("eval"), + params: object({ + script: string, + }), +}) const jsonParse = (x: string) => JSON.parse(x) @@ -365,7 +334,7 @@ export class RpcListener { ) }) .when( - shape({ id: idType, method: string }, ["id"]), + shape({ id: idType.optional(), method: string }), ({ id, method }) => ({ jsonrpc, id, @@ -400,7 +369,7 @@ export class RpcListener { procedure: typeof jsonPath._TYPE, system: System, procedureId: string, - timeout: number | undefined, + timeout: number | null | undefined, input: any, ) { const ensureResultTypeShape = ( @@ -449,14 +418,10 @@ export class RpcListener { })().then(ensureResultTypeShape, (error) => matches(error) .when( - object( - { - error: string, - code: number, - }, - ["code"], - { code: 0 }, - ), + object({ + error: string, + code: number.defaultTo(0), + }), (error) => ({ error: { code: error.code, diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts index c48891ff2..d82f7aad0 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts @@ -64,7 +64,7 @@ export class DockerProcedureContainer extends Drop { const volumeMount = volumes[mount] if (volumeMount.type === "data") { await subcontainer.mount( - Mounts.of().addVolume({ + Mounts.of().mountVolume({ volumeId: mount, subpath: null, mountpoint: mounts[mount], @@ -73,7 +73,7 @@ export class DockerProcedureContainer extends Drop { ) } else if (volumeMount.type === "assets") { await subcontainer.mount( - Mounts.of().addAssets({ + Mounts.of().mountAssets({ subpath: mount, mountpoint: mounts[mount], }), @@ -119,7 +119,7 @@ export class DockerProcedureContainer extends Drop { }) } else if (volumeMount.type === "backup") { await subcontainer.mount( - Mounts.of().addBackups({ + Mounts.of().mountBackups({ subpath: null, mountpoint: mounts[mount], }), diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts index db54efda9..9a4cad3c1 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -1,5 +1,6 @@ import { ExtendedVersion, + FileHelper, types as T, utils, VersionRange, @@ -46,7 +47,7 @@ import { transformNewConfigToOld, transformOldConfigToNew, } from "./transformConfigSpec" -import { partialDiff, StorePath } from "@start9labs/start-sdk/base/lib/util" +import { partialDiff } from "@start9labs/start-sdk/base/lib/util" type Optional = A | undefined | null function todo(): never { @@ -55,8 +56,21 @@ function todo(): never { const MANIFEST_LOCATION = "/usr/lib/startos/package/embassyManifest.json" export const EMBASSY_JS_LOCATION = "/usr/lib/startos/package/embassy.js" -const EMBASSY_POINTER_PATH_PREFIX = "/embassyConfig" as utils.StorePath -const EMBASSY_DEPENDS_ON_PATH_PREFIX = "/embassyDependsOn" as utils.StorePath + +const configFile = FileHelper.json( + { + volumeId: "embassy", + subpath: "config.json", + }, + matches.any, +) +const dependsOnFile = FileHelper.json( + { + volumeId: "embassy", + subpath: "dependsOn.json", + }, + dictionary([string, array(string)]), +) const matchResult = object({ result: any, @@ -94,47 +108,48 @@ const fromReturnType = (a: U.ResultType): A => { return assertNever(a) } -const matchSetResult = object( - { - "depends-on": dictionary([string, array(string)]), - dependsOn: dictionary([string, array(string)]), - signal: literals( - "SIGTERM", - "SIGHUP", - "SIGINT", - "SIGQUIT", - "SIGILL", - "SIGTRAP", - "SIGABRT", - "SIGBUS", - "SIGFPE", - "SIGKILL", - "SIGUSR1", - "SIGSEGV", - "SIGUSR2", - "SIGPIPE", - "SIGALRM", - "SIGSTKFLT", - "SIGCHLD", - "SIGCONT", - "SIGSTOP", - "SIGTSTP", - "SIGTTIN", - "SIGTTOU", - "SIGURG", - "SIGXCPU", - "SIGXFSZ", - "SIGVTALRM", - "SIGPROF", - "SIGWINCH", - "SIGIO", - "SIGPWR", - "SIGSYS", - "SIGINFO", - ), - }, - ["depends-on", "dependsOn"], -) +const matchSetResult = object({ + "depends-on": dictionary([string, array(string)]) + .nullable() + .optional(), + dependsOn: dictionary([string, array(string)]) + .nullable() + .optional(), + signal: literals( + "SIGTERM", + "SIGHUP", + "SIGINT", + "SIGQUIT", + "SIGILL", + "SIGTRAP", + "SIGABRT", + "SIGBUS", + "SIGFPE", + "SIGKILL", + "SIGUSR1", + "SIGSEGV", + "SIGUSR2", + "SIGPIPE", + "SIGALRM", + "SIGSTKFLT", + "SIGCHLD", + "SIGCONT", + "SIGSTOP", + "SIGTSTP", + "SIGTTIN", + "SIGTTOU", + "SIGURG", + "SIGXCPU", + "SIGXFSZ", + "SIGVTALRM", + "SIGPROF", + "SIGWINCH", + "SIGIO", + "SIGPWR", + "SIGSYS", + "SIGINFO", + ), +}) type OldGetConfigRes = { config?: null | Record @@ -174,14 +189,14 @@ export type PackagePropertiesV2 = { } export type PackagePropertyString = { type: "string" - description?: string + description?: string | null value: string /** Let's the ui make this copyable button */ - copyable?: boolean + copyable?: boolean | null /** Let the ui create a qr for this field */ - qr?: boolean + qr?: boolean | null /** Hiding the value unless toggled off for field */ - masked?: boolean + masked?: boolean | null } export type PackagePropertyObject = { value: PackagePropertiesV2 @@ -225,17 +240,14 @@ const matchPackagePropertyObject: Parser = }) const matchPackagePropertyString: Parser = - object( - { - type: literal("string"), - description: string, - value: string, - copyable: boolean, - qr: boolean, - masked: boolean, - }, - ["copyable", "description", "qr", "masked"], - ) + object({ + type: literal("string"), + description: string.nullable().optional(), + value: string, + copyable: boolean.nullable().optional(), + qr: boolean.nullable().optional(), + masked: boolean.nullable().optional(), + }) setMatchPackageProperties( dictionary([ string, @@ -300,7 +312,7 @@ export class SystemForEmbassy implements System { async containerInit(effects: Effects): Promise { for (let depId in this.manifest.dependencies) { - if (this.manifest.dependencies[depId].config) { + if (this.manifest.dependencies[depId]?.config) { await this.dependenciesAutoconfig(effects, depId, null) } } @@ -355,10 +367,7 @@ export class SystemForEmbassy implements System { ) { await effects.action.clearRequests({ only: ["needs-config"] }) } - await effects.store.set({ - path: EMBASSY_POINTER_PATH_PREFIX, - value: this.getConfig(effects, timeoutMs), - }) + await configFile.write(effects, await this.getConfig(effects, timeoutMs)) } else if (this.manifest.config) { await effects.action.request({ packageId: this.manifest.id, @@ -587,11 +596,6 @@ export class SystemForEmbassy implements System { const moduleCode = await this.moduleCode await moduleCode.createBackup?.(polyfillEffects(effects, this.manifest)) } - await fs.writeFile( - "/media/startos/backup/store.json", - JSON.stringify(await effects.store.get({ path: "" as StorePath })), - { encoding: "utf-8" }, - ) const dataVersion = await effects.getDataVersion() if (dataVersion) await fs.writeFile("/media/startos/backup/dataVersion.txt", dataVersion, { @@ -607,11 +611,6 @@ export class SystemForEmbassy implements System { encoding: "utf-8", }) .catch((_) => null) - if (store) - await effects.store.set({ - path: "" as StorePath, - value: JSON.parse(store), - }) const restoreBackup = this.manifest.backup.restore if (restoreBackup.type === "docker") { const commands = [restoreBackup.entrypoint, ...restoreBackup.args] @@ -686,10 +685,7 @@ export class SystemForEmbassy implements System { structuredClone(newConfigWithoutPointers as Record), ) await updateConfig(effects, this.manifest, spec, newConfig) - await effects.store.set({ - path: EMBASSY_POINTER_PATH_PREFIX, - value: newConfig, - }) + await configFile.write(effects, newConfig) const setConfigValue = this.manifest.config?.set if (!setConfigValue) return if (setConfigValue.type === "docker") { @@ -743,15 +739,11 @@ export class SystemForEmbassy implements System { rawDepends: { [x: string]: readonly string[] }, configuring: boolean, ) { - const storedDependsOn = (await effects.store.get({ - packageId: this.manifest.id, - path: EMBASSY_DEPENDS_ON_PATH_PREFIX, - })) as Record - + const storedDependsOn = await dependsOnFile.read().once() const requiredDeps = { ...Object.fromEntries( - Object.entries(this.manifest.dependencies || {}) - ?.filter((x) => x[1].requirement.type === "required") + Object.entries(this.manifest.dependencies ?? {}) + .filter(([k, v]) => v?.requirement.type === "required") .map((x) => [x[0], []]) || [], ), } @@ -765,10 +757,7 @@ export class SystemForEmbassy implements System { ? storedDependsOn : requiredDeps - await effects.store.set({ - path: EMBASSY_DEPENDS_ON_PATH_PREFIX, - value: dependsOn, - }) + await dependsOnFile.write(effects, dependsOn) await effects.setDependencies({ dependencies: Object.entries(dependsOn).flatMap( @@ -1006,43 +995,50 @@ export class SystemForEmbassy implements System { timeoutMs: number | null, ): Promise { // TODO: docker - const oldConfig = (await effects.store.get({ - packageId: id, - path: EMBASSY_POINTER_PATH_PREFIX, - callback: () => { - this.dependenciesAutoconfig(effects, id, timeoutMs) - }, - })) as U.Config - if (!oldConfig) return - const moduleCode = await this.moduleCode - const method = moduleCode?.dependencies?.[id]?.autoConfigure - if (!method) return - const newConfig = (await method( - polyfillEffects(effects, this.manifest), - JSON.parse(JSON.stringify(oldConfig)), - ).then((x) => { - if ("result" in x) return x.result - if ("error" in x) throw new Error("Error getting config: " + x.error) - throw new Error("Error getting config: " + x["error-code"][1]) - })) as any - const diff = partialDiff(oldConfig, newConfig) - if (diff) { - await effects.action.request({ - actionId: "config", + await effects.mount({ + location: `/media/embassy/${id}`, + target: { packageId: id, - replayId: `${id}/config`, - severity: "important", - reason: `Configure this dependency for the needs of ${this.manifest.title}`, - input: { - kind: "partial", - value: diff.diff, - }, - when: { - condition: "input-not-matches", - once: false, - }, + volumeId: "embassy", + subpath: null, + readonly: true, + }, + }) + configFile + .withPath(`/media/embassy/${id}/config.json`) + .read() + .onChange(effects, async (oldConfig: U.Config) => { + if (!oldConfig) return + const moduleCode = await this.moduleCode + const method = moduleCode?.dependencies?.[id]?.autoConfigure + if (!method) return + const newConfig = (await method( + polyfillEffects(effects, this.manifest), + JSON.parse(JSON.stringify(oldConfig)), + ).then((x) => { + if ("result" in x) return x.result + if ("error" in x) throw new Error("Error getting config: " + x.error) + throw new Error("Error getting config: " + x["error-code"][1]) + })) as any + const diff = partialDiff(oldConfig, newConfig) + if (diff) { + await effects.action.request({ + actionId: "config", + packageId: id, + replayId: `${id}/config`, + severity: "important", + reason: `Configure this dependency for the needs of ${this.manifest.title}`, + input: { + kind: "partial", + value: diff.diff, + }, + when: { + condition: "input-not-matches", + once: false, + }, + }) + } }) - } } } @@ -1144,11 +1140,20 @@ async function updateConfig( ) { if (specValue.target === "config") { const jp = require("jsonpath") - const remoteConfig = await effects.store.get({ - packageId: specValue["package-id"], - callback: () => effects.restart(), - path: EMBASSY_POINTER_PATH_PREFIX, + const depId = specValue["package-id"] + await effects.mount({ + location: `/media/embassy/${depId}`, + target: { + packageId: depId, + volumeId: "embassy", + subpath: null, + readonly: true, + }, }) + const remoteConfig = configFile + .withPath(`/media/embassy/${depId}/config.json`) + .read() + .once() console.debug(remoteConfig) const configValue = specValue.multi ? jp.query(remoteConfig, specValue.selector) diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchManifest.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchManifest.ts index 5bda20de0..8c680cbd6 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchManifest.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchManifest.ts @@ -14,123 +14,113 @@ import { import { matchVolume } from "./matchVolume" import { matchDockerProcedure } from "../../../Models/DockerProcedure" -const matchJsProcedure = object( - { - type: literal("script"), - args: array(unknown), - }, - ["args"], - { - args: [], - }, -) +const matchJsProcedure = object({ + type: literal("script"), + args: array(unknown).nullable().optional().defaultTo([]), +}) const matchProcedure = some(matchDockerProcedure, matchJsProcedure) export type Procedure = typeof matchProcedure._TYPE -const matchAction = object( - { - name: string, - description: string, - warning: string, - implementation: matchProcedure, - "allowed-statuses": array(literals("running", "stopped")), - "input-spec": unknown, - }, - ["warning", "input-spec", "input-spec"], -) -export const matchManifest = object( - { - id: string, - title: string, - version: string, - main: matchDockerProcedure, - assets: object( - { - assets: string, - scripts: string, - }, - ["assets", "scripts"], +const matchAction = object({ + name: string, + description: string, + warning: string.nullable().optional(), + implementation: matchProcedure, + "allowed-statuses": array(literals("running", "stopped")), + "input-spec": unknown.nullable().optional(), +}) +export const matchManifest = object({ + id: string, + title: string, + version: string, + main: matchDockerProcedure, + assets: object({ + assets: string.nullable().optional(), + scripts: string.nullable().optional(), + }) + .nullable() + .optional(), + "health-checks": dictionary([ + string, + every( + matchProcedure, + object({ + name: string, + ["success-message"]: string.nullable().optional(), + }), ), - "health-checks": dictionary([ - string, - every( - matchProcedure, - object( - { - name: string, - ["success-message"]: string, - }, - ["success-message"], - ), - ), - ]), - config: object({ - get: matchProcedure, - set: matchProcedure, + ]), + config: object({ + get: matchProcedure, + set: matchProcedure, + }) + .nullable() + .optional(), + properties: matchProcedure.nullable().optional(), + volumes: dictionary([string, matchVolume]), + interfaces: dictionary([ + string, + object({ + name: string, + description: string, + "tor-config": object({ + "port-mapping": dictionary([string, string]), + }) + .nullable() + .optional(), + "lan-config": dictionary([ + string, + object({ + ssl: boolean, + internal: number, + }), + ]) + .nullable() + .optional(), + ui: boolean, + protocols: array(string), }), - properties: matchProcedure, - volumes: dictionary([string, matchVolume]), - interfaces: dictionary([ - string, - object( - { - name: string, - description: string, - "tor-config": object({ - "port-mapping": dictionary([string, string]), - }), - "lan-config": dictionary([ - string, - object({ - ssl: boolean, - internal: number, - }), - ]), - ui: boolean, - protocols: array(string), - }, - ["lan-config", "tor-config"], + ]), + backup: object({ + create: matchProcedure, + restore: matchProcedure, + }), + migrations: object({ + to: dictionary([string, matchProcedure]), + from: dictionary([string, matchProcedure]), + }) + .nullable() + .optional(), + dependencies: dictionary([ + string, + object({ + version: string, + requirement: some( + object({ + type: literal("opt-in"), + how: string, + }), + object({ + type: literal("opt-out"), + how: string, + }), + object({ + type: literal("required"), + }), ), - ]), - backup: object({ - create: matchProcedure, - restore: matchProcedure, - }), - migrations: object({ - to: dictionary([string, matchProcedure]), - from: dictionary([string, matchProcedure]), - }), - dependencies: dictionary([ - string, - object( - { - version: string, - requirement: some( - object({ - type: literal("opt-in"), - how: string, - }), - object({ - type: literal("opt-out"), - how: string, - }), - object({ - type: literal("required"), - }), - ), - description: string, - config: object({ - check: matchProcedure, - "auto-configure": matchProcedure, - }), - }, - ["description", "config"], - ), - ]), + description: string.nullable().optional(), + config: object({ + check: matchProcedure, + "auto-configure": matchProcedure, + }) + .nullable() + .optional(), + }) + .nullable() + .optional(), + ]), - actions: dictionary([string, matchAction]), - }, - ["config", "actions", "properties", "migrations", "dependencies"], -) + actions: dictionary([string, matchAction]), +}) export type Manifest = typeof matchManifest._TYPE diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchVolume.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchVolume.ts index 7aa579ecf..baffbdd12 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchVolume.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchVolume.ts @@ -1,12 +1,9 @@ import { object, literal, string, boolean, some } from "ts-matches" -const matchDataVolume = object( - { - type: literal("data"), - readonly: boolean, - }, - ["readonly"], -) +const matchDataVolume = object({ + type: literal("data"), + readonly: boolean.optional(), +}) const matchAssetVolume = object({ type: literal("assets"), }) diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts index 0dc6dac24..58a170d23 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts @@ -169,7 +169,7 @@ export const polyfillEffects = ( { imageId: manifest.main.image }, commands, { - mounts: Mounts.of().addVolume({ + mounts: Mounts.of().mountVolume({ volumeId: input.volumeId, subpath: null, mountpoint: "/drive", @@ -206,7 +206,7 @@ export const polyfillEffects = ( { imageId: manifest.main.image }, commands, { - mounts: Mounts.of().addVolume({ + mounts: Mounts.of().mountVolume({ volumeId: input.volumeId, subpath: null, mountpoint: "/drive", diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.ts index 0b16e9d72..2f63daf83 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.ts @@ -203,6 +203,7 @@ export function transformNewConfigToOld( spec: OldConfigSpec, config: Record, ): Record { + if (!config) return config return Object.entries(spec).reduce((obj, [key, val]) => { let newVal = config[key] @@ -396,100 +397,71 @@ export const matchOldDefaultString = anyOf( ) type OldDefaultString = typeof matchOldDefaultString._TYPE -export const matchOldValueSpecString = object( - { - type: literals("string"), - name: string, - masked: boolean, - copyable: boolean, - nullable: boolean, - placeholder: string, - pattern: string, - "pattern-description": string, - default: matchOldDefaultString, - textarea: boolean, - description: string, - warning: string, - }, - [ - "masked", - "copyable", - "nullable", - "placeholder", - "pattern", - "pattern-description", - "default", - "textarea", - "description", - "warning", - ], -) +export const matchOldValueSpecString = object({ + type: literals("string"), + name: string, + masked: boolean.nullable().optional(), + copyable: boolean.nullable().optional(), + nullable: boolean.nullable().optional(), + placeholder: string.nullable().optional(), + pattern: string.nullable().optional(), + "pattern-description": string.nullable().optional(), + default: matchOldDefaultString.nullable().optional(), + textarea: boolean.nullable().optional(), + description: string.nullable().optional(), + warning: string.nullable().optional(), +}) -export const matchOldValueSpecNumber = object( - { - type: literals("number"), - nullable: boolean, - name: string, - range: string, - integral: boolean, - default: number, - description: string, - warning: string, - units: string, - placeholder: anyOf(number, string), - }, - ["default", "description", "warning", "units", "placeholder"], -) +export const matchOldValueSpecNumber = object({ + type: literals("number"), + nullable: boolean, + name: string, + range: string, + integral: boolean, + default: number.nullable().optional(), + description: string.nullable().optional(), + warning: string.nullable().optional(), + units: string.nullable().optional(), + placeholder: anyOf(number, string).nullable().optional(), +}) type OldValueSpecNumber = typeof matchOldValueSpecNumber._TYPE -export const matchOldValueSpecBoolean = object( - { - type: literals("boolean"), - default: boolean, - name: string, - description: string, - warning: string, - }, - ["description", "warning"], -) +export const matchOldValueSpecBoolean = object({ + type: literals("boolean"), + default: boolean, + name: string, + description: string.nullable().optional(), + warning: string.nullable().optional(), +}) type OldValueSpecBoolean = typeof matchOldValueSpecBoolean._TYPE -const matchOldValueSpecObject = object( - { - type: literals("object"), - spec: _matchOldConfigSpec, - name: string, - description: string, - warning: string, - }, - ["description", "warning"], -) +const matchOldValueSpecObject = object({ + type: literals("object"), + spec: _matchOldConfigSpec, + name: string, + description: string.nullable().optional(), + warning: string.nullable().optional(), +}) type OldValueSpecObject = typeof matchOldValueSpecObject._TYPE -const matchOldValueSpecEnum = object( - { - values: array(string), - "value-names": dictionary([string, string]), - type: literals("enum"), - default: string, - name: string, - description: string, - warning: string, - }, - ["description", "warning"], -) +const matchOldValueSpecEnum = object({ + values: array(string), + "value-names": dictionary([string, string]), + type: literals("enum"), + default: string, + name: string, + description: string.nullable().optional(), + warning: string.nullable().optional(), +}) type OldValueSpecEnum = typeof matchOldValueSpecEnum._TYPE -const matchOldUnionTagSpec = object( - { - id: string, // The name of the field containing one of the union variants - "variant-names": dictionary([string, string]), // The name of each variant - name: string, - description: string, - warning: string, - }, - ["description", "warning"], -) +const matchOldUnionTagSpec = object({ + id: string, // The name of the field containing one of the union variants + "variant-names": dictionary([string, string]), // The name of each variant + name: string, + description: string.nullable().optional(), + warning: string.nullable().optional(), +}) const matchOldValueSpecUnion = object({ type: literals("union"), tag: matchOldUnionTagSpec, @@ -514,57 +486,45 @@ setOldUniqueBy( ), ) -const matchOldListValueSpecObject = object( - { - spec: _matchOldConfigSpec, // this is a mapped type of the config object at this level, replacing the object's values with specs on those values - "unique-by": matchOldUniqueBy, // indicates whether duplicates can be permitted in the list - "display-as": string, // this should be a handlebars template which can make use of the entire config which corresponds to 'spec' - }, - ["display-as", "unique-by"], -) -const matchOldListValueSpecString = object( - { - masked: boolean, - copyable: boolean, - pattern: string, - "pattern-description": string, - placeholder: string, - }, - ["pattern", "pattern-description", "placeholder", "copyable", "masked"], -) +const matchOldListValueSpecObject = object({ + spec: _matchOldConfigSpec, // this is a mapped type of the config object at this level, replacing the object's values with specs on those values + "unique-by": matchOldUniqueBy.nullable().optional(), // indicates whether duplicates can be permitted in the list + "display-as": string.nullable().optional(), // this should be a handlebars template which can make use of the entire config which corresponds to 'spec' +}) +const matchOldListValueSpecString = object({ + masked: boolean.nullable().optional(), + copyable: boolean.nullable().optional(), + pattern: string.nullable().optional(), + "pattern-description": string.nullable().optional(), + placeholder: string.nullable().optional(), +}) const matchOldListValueSpecEnum = object({ values: array(string), "value-names": dictionary([string, string]), }) -const matchOldListValueSpecNumber = object( - { - range: string, - integral: boolean, - units: string, - placeholder: anyOf(number, string), - }, - ["units", "placeholder"], -) +const matchOldListValueSpecNumber = object({ + range: string, + integral: boolean, + units: string.nullable().optional(), + placeholder: anyOf(number, string).nullable().optional(), +}) // represents a spec for a list const matchOldValueSpecList = every( - object( - { - type: literals("list"), - range: string, // '[0,1]' (inclusive) OR '[0,*)' (right unbounded), normal math rules - default: anyOf( - array(string), - array(number), - array(matchOldDefaultString), - array(object), - ), - name: string, - description: string, - warning: string, - }, - ["description", "warning"], - ), + object({ + type: literals("list"), + range: string, // '[0,1]' (inclusive) OR '[0,*)' (right unbounded), normal math rules + default: anyOf( + array(string), + array(number), + array(matchOldDefaultString), + array(object), + ), + name: string, + description: string.nullable().optional(), + warning: string.nullable().optional(), + }), anyOf( object({ subtype: literals("string"), diff --git a/container-runtime/src/Adapters/Systems/SystemForStartOs.ts b/container-runtime/src/Adapters/Systems/SystemForStartOs.ts index 1d38c83e6..0635e2921 100644 --- a/container-runtime/src/Adapters/Systems/SystemForStartOs.ts +++ b/container-runtime/src/Adapters/Systems/SystemForStartOs.ts @@ -1,7 +1,6 @@ import { System } from "../../Interfaces/System" import { Effects } from "../../Models/Effects" import { T, utils } from "@start9labs/start-sdk" -import { Optional } from "ts-matches/lib/parsers/interfaces" export const STARTOS_JS_LOCATION = "/usr/lib/startos/package/index.js" @@ -30,7 +29,7 @@ export class SystemForStartOs implements System { } async packageUninit( effects: Effects, - nextVersion: Optional = null, + nextVersion: string | null = null, timeoutMs: number | null = null, ): Promise { return void (await this.abi.packageUninit({ effects, nextVersion })) diff --git a/container-runtime/src/Models/DockerProcedure.ts b/container-runtime/src/Models/DockerProcedure.ts index 20c8145ab..5bcd9d5a9 100644 --- a/container-runtime/src/Models/DockerProcedure.ts +++ b/container-runtime/src/Models/DockerProcedure.ts @@ -17,31 +17,25 @@ const Path = string export type VolumeId = string export type Path = string -export const matchDockerProcedure = object( - { - type: literal("docker"), - image: string, - system: boolean, - entrypoint: string, - args: array(string), - mounts: dictionary([VolumeId, Path]), - "io-format": literals( - "json", - "json-pretty", - "yaml", - "cbor", - "toml", - "toml-pretty", - ), - "sigterm-timeout": some(number, matchDuration), - inject: boolean, - }, - ["io-format", "sigterm-timeout", "system", "args", "inject", "mounts"], - { - "sigterm-timeout": 30, - inject: false, - args: [], - }, -) +export const matchDockerProcedure = object({ + type: literal("docker"), + image: string, + system: boolean.optional(), + entrypoint: string, + args: array(string).defaultTo([]), + mounts: dictionary([VolumeId, Path]).optional(), + "io-format": literals( + "json", + "json-pretty", + "yaml", + "cbor", + "toml", + "toml-pretty", + ) + .nullable() + .optional(), + "sigterm-timeout": some(number, matchDuration).onMismatch(30), + inject: boolean.defaultTo(false), +}) export type DockerProcedure = typeof matchDockerProcedure._TYPE diff --git a/core/models/src/errors.rs b/core/models/src/errors.rs index eda968644..b9295131c 100644 --- a/core/models/src/errors.rs +++ b/core/models/src/errors.rs @@ -10,6 +10,7 @@ use rpc_toolkit::yajrc::{ RpcError, INVALID_PARAMS_ERROR, INVALID_REQUEST_ERROR, METHOD_NOT_FOUND_ERROR, PARSE_ERROR, }; use serde::{Deserialize, Serialize}; +use tokio::task::JoinHandle; use crate::InvalidId; @@ -189,6 +190,7 @@ pub struct Error { pub source: color_eyre::eyre::Error, pub kind: ErrorKind, pub revision: Option, + pub task: Option>, } impl Display for Error { @@ -202,6 +204,7 @@ impl Error { source: source.into(), kind, revision: None, + task: None, } } pub fn clone_output(&self) -> Self { @@ -213,8 +216,20 @@ impl Error { .into(), kind: self.kind, revision: self.revision.clone(), + task: None, } } + pub fn with_task(mut self, task: JoinHandle<()>) -> Self { + self.task = Some(task); + self + } + pub async fn wait(mut self) -> Self { + if let Some(task) = &mut self.task { + task.await.log_err(); + } + self.task.take(); + self + } } impl axum::response::IntoResponse for Error { fn into_response(self) -> axum::response::Response { @@ -530,6 +545,7 @@ where source: e.into(), kind, revision: None, + task: None, }) } @@ -543,6 +559,7 @@ where kind, source, revision: None, + task: None, } }) } @@ -565,6 +582,7 @@ impl ResultExt for Result { source: e.source, kind, revision: e.revision, + task: e.task, }) } @@ -578,6 +596,7 @@ impl ResultExt for Result { kind, source, revision: e.revision, + task: e.task, } }) } diff --git a/core/models/src/id/volume.rs b/core/models/src/id/volume.rs index 7425c79c6..ac99fdea3 100644 --- a/core/models/src/id/volume.rs +++ b/core/models/src/id/volume.rs @@ -1,10 +1,11 @@ use std::borrow::Borrow; use std::path::Path; +use std::str::FromStr; use serde::{Deserialize, Deserializer, Serialize}; use ts_rs::TS; -use crate::Id; +use crate::{Id, InvalidId}; #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, TS)] #[ts(type = "string")] @@ -12,6 +13,15 @@ pub enum VolumeId { Backup, Custom(Id), } +impl FromStr for VolumeId { + type Err = InvalidId; + fn from_str(s: &str) -> Result { + Ok(match s { + "BACKUP" => VolumeId::Backup, + s => VolumeId::Custom(Id::try_from(s.to_owned())?), + }) + } +} impl std::fmt::Display for VolumeId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/core/startos/src/install/mod.rs b/core/startos/src/install/mod.rs index 06c60afdf..7b9fe7e72 100644 --- a/core/startos/src/install/mod.rs +++ b/core/startos/src/install/mod.rs @@ -550,33 +550,18 @@ pub struct UninstallParams { pub async fn uninstall( ctx: RpcContext, UninstallParams { id, soft, force }: UninstallParams, -) -> Result { - ctx.db - .mutate(|db| { - let entry = db - .as_public_mut() - .as_package_data_mut() - .as_idx_mut(&id) - .or_not_found(&id)?; - entry.as_state_info_mut().map_mutate(|s| match s { - PackageState::Installed(s) => Ok(PackageState::Removing(s)), - _ => Err(Error::new( - eyre!("Package {id} is not installed."), - crate::ErrorKind::NotFound, - )), - }) - }) - .await - .result?; - - let return_id = id.clone(); +) -> Result<(), Error> { + let fut = ctx + .services + .uninstall(ctx.clone(), id.clone(), soft, force) + .await?; tokio::spawn(async move { - if let Err(e) = ctx.services.uninstall(&ctx, &id, soft, force).await { + if let Err(e) = fut.await { tracing::error!("Error uninstalling service {id}: {e}"); tracing::debug!("{e:?}"); } }); - Ok(return_id) + Ok(()) } diff --git a/core/startos/src/s9pk/v2/compat.rs b/core/startos/src/s9pk/v2/compat.rs index 5b33006de..6204046a9 100644 --- a/core/startos/src/s9pk/v2/compat.rs +++ b/core/startos/src/s9pk/v2/compat.rs @@ -4,7 +4,7 @@ use std::str::FromStr; use std::sync::Arc; use exver::{ExtendedVersion, VersionRange}; -use models::ImageId; +use models::{Id, ImageId, VolumeId}; use tokio::io::{AsyncRead, AsyncSeek, AsyncWriteExt}; use tokio::process::Command; @@ -213,6 +213,7 @@ impl TryFrom for Manifest { .iter() .filter(|(_, v)| v.get("type").and_then(|v| v.as_str()) == Some("data")) .map(|(id, _)| id.clone()) + .chain([VolumeId::from_str("embassy").unwrap()]) .collect(), alerts: value.alerts, dependencies: Dependencies( diff --git a/core/startos/src/service/effects/callbacks.rs b/core/startos/src/service/effects/callbacks.rs index 9f676d12e..4ae45b451 100644 --- a/core/startos/src/service/effects/callbacks.rs +++ b/core/startos/src/service/effects/callbacks.rs @@ -8,10 +8,7 @@ use futures::future::join_all; use helpers::NonDetachingJoinHandle; use imbl::{vector, Vector}; use imbl_value::InternedString; -use lazy_static::lazy_static; use models::{HostId, PackageId, ServiceInterfaceId}; -use patch_db::json_ptr::JsonPointer; -use patch_db::Revision; use serde::{Deserialize, Serialize}; use tracing::warn; use ts_rs::TS; @@ -37,7 +34,6 @@ struct ServiceCallbackMap { (BTreeSet, FullchainCertData, Algorithm), (NonDetachingJoinHandle<()>, Vec), >, - get_store: BTreeMap>>, get_status: BTreeMap>, get_container_ip: BTreeMap>, } @@ -68,13 +64,6 @@ impl ServiceCallbacks { v.retain(|h| h.handle.is_active() && h.seed.strong_count() > 0); !v.is_empty() }); - this.get_store.retain(|_, v| { - v.retain(|_, v| { - v.retain(|h| h.handle.is_active() && h.seed.strong_count() > 0); - !v.is_empty() - }); - !v.is_empty() - }); this.get_status.retain(|_, v| { v.retain(|h| h.handle.is_active() && h.seed.strong_count() > 0); !v.is_empty() @@ -243,53 +232,6 @@ impl ServiceCallbacks { }) } - pub(super) fn add_get_store( - &self, - package_id: PackageId, - path: JsonPointer, - handler: CallbackHandler, - ) { - self.mutate(|this| { - this.get_store - .entry(package_id) - .or_default() - .entry(path) - .or_default() - .push(handler) - }) - } - - #[must_use] - pub fn get_store( - &self, - package_id: &PackageId, - revision: &Revision, - ) -> Option { - lazy_static! { - static ref BASE: JsonPointer = "/private/packageStores".parse().unwrap(); - } - let for_pkg = BASE.clone().join_end(&**package_id); - self.mutate(|this| { - if let Some(watched) = this.get_store.get_mut(package_id) { - let mut res = Vec::new(); - watched.retain(|ptr, cbs| { - let mut full_ptr = for_pkg.clone(); - full_ptr.append(ptr); - if revision.patch.affects_path(&full_ptr) { - res.append(cbs); - false - } else { - true - } - }); - Some(CallbackHandlers(res)) - } else { - None - } - .filter(|cb| !cb.0.is_empty()) - }) - } - pub(super) fn add_get_container_ip(&self, package_id: PackageId, handler: CallbackHandler) { self.mutate(|this| { this.get_container_ip diff --git a/core/startos/src/service/effects/dependency.rs b/core/startos/src/service/effects/dependency.rs index e2b84be90..2b0d2c416 100644 --- a/core/startos/src/service/effects/dependency.rs +++ b/core/startos/src/service/effects/dependency.rs @@ -7,7 +7,6 @@ use exver::VersionRange; use imbl::OrdMap; use imbl_value::InternedString; use models::{FromStrParser, HealthCheckId, PackageId, ReplayId, VersionString, VolumeId}; -use patch_db::json_ptr::JsonPointer; use tokio::process::Command; use crate::db::model::package::{ @@ -17,6 +16,7 @@ use crate::db::model::package::{ use crate::disk::mount::filesystem::bind::Bind; use crate::disk::mount::filesystem::idmapped::IdMapped; use crate::disk::mount::filesystem::{FileSystem, MountType}; +use crate::disk::mount::util::{is_mountpoint, unmount}; use crate::service::effects::prelude::*; use crate::status::health_check::NamedHealthCheckResult; use crate::util::Invoke; @@ -110,6 +110,9 @@ pub async fn mount( } tokio::fs::create_dir_all(&mountpoint).await?; + if is_mountpoint(&mountpoint).await? { + unmount(&mountpoint, true).await?; + } Command::new("chown") .arg("100000:100000") .arg(&mountpoint) @@ -142,21 +145,6 @@ pub async fn get_installed_packages(context: EffectContext) -> Result, -} -pub async fn expose_for_dependents( - context: EffectContext, - ExposeForDependentsParams { paths }: ExposeForDependentsParams, -) -> Result<(), Error> { - // TODO - Ok(()) -} - #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export)] diff --git a/core/startos/src/service/effects/mod.rs b/core/startos/src/service/effects/mod.rs index 474d1c818..ab1092ec5 100644 --- a/core/startos/src/service/effects/mod.rs +++ b/core/startos/src/service/effects/mod.rs @@ -15,9 +15,9 @@ mod dependency; mod health; mod net; mod prelude; -mod store; mod subcontainer; mod system; +mod version; pub fn handler() -> ParentHandler { ParentHandler::new() @@ -88,10 +88,6 @@ pub fn handler() -> ParentHandler { "get-installed-packages", from_fn_async(dependency::get_installed_packages).no_cli(), ) - .subcommand( - "expose-for-dependents", - from_fn_async(dependency::expose_for_dependents).no_cli(), - ) // health .subcommand("set-health", from_fn_async(health::set_health).no_cli()) // subcontainer @@ -167,22 +163,15 @@ pub fn handler() -> ParentHandler { from_fn_async(net::ssl::get_ssl_certificate).no_cli(), ) .subcommand("get-ssl-key", from_fn_async(net::ssl::get_ssl_key).no_cli()) - // store - .subcommand( - "store", - ParentHandler::::new() - .subcommand("get", from_fn_async(store::get_store).no_cli()) - .subcommand("set", from_fn_async(store::set_store).no_cli()), - ) .subcommand( "set-data-version", - from_fn_async(store::set_data_version) + from_fn_async(version::set_data_version) .no_display() .with_call_remote::(), ) .subcommand( "get-data-version", - from_fn_async(store::get_data_version) + from_fn_async(version::get_data_version) .with_custom_display_fn(|_, v| { if let Some(v) = v { println!("{v}") diff --git a/core/startos/src/service/effects/store.rs b/core/startos/src/service/effects/store.rs deleted file mode 100644 index 6ea28488e..000000000 --- a/core/startos/src/service/effects/store.rs +++ /dev/null @@ -1,143 +0,0 @@ -use imbl::vector; -use imbl_value::json; -use models::{PackageId, VersionString}; -use patch_db::json_ptr::JsonPointer; - -use crate::service::effects::callbacks::CallbackHandler; -use crate::service::effects::prelude::*; -use crate::service::rpc::CallbackId; - -#[derive(Debug, Clone, Serialize, Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -pub struct GetStoreParams { - #[ts(optional)] - package_id: Option, - #[ts(type = "string")] - path: JsonPointer, - #[ts(optional)] - callback: Option, -} -pub async fn get_store( - context: EffectContext, - GetStoreParams { - package_id, - path, - callback, - }: GetStoreParams, -) -> Result { - crate::dbg!(&callback); - let context = context.deref()?; - let peeked = context.seed.ctx.db.peek().await; - let package_id = package_id.unwrap_or(context.seed.id.clone()); - let value = peeked - .as_private() - .as_package_stores() - .as_idx(&package_id) - .map(|s| s.de()) - .transpose()? - .unwrap_or_default(); - - if let Some(callback) = callback { - let callback = callback.register(&context.seed.persistent_container); - context.seed.ctx.callbacks.add_get_store( - package_id, - path.clone(), - CallbackHandler::new(&context, callback), - ); - } - - Ok(path.get(&value).cloned().unwrap_or_default()) -} - -#[derive(Debug, Clone, Serialize, Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -pub struct SetStoreParams { - #[ts(type = "any")] - value: Value, - #[ts(type = "string")] - path: JsonPointer, -} -pub async fn set_store( - context: EffectContext, - SetStoreParams { value, path }: SetStoreParams, -) -> Result<(), Error> { - let context = context.deref()?; - let package_id = &context.seed.id; - let res = context - .seed - .ctx - .db - .mutate(|db| { - let model = db - .as_private_mut() - .as_package_stores_mut() - .upsert(package_id, || Ok(json!({})))?; - let mut model_value = model.de()?; - if model_value.is_null() { - model_value = json!({}); - } - path.set(&mut model_value, value, true) - .with_kind(ErrorKind::ParseDbField)?; - model.ser(&model_value) - }) - .await; - res.result?; - - if let Some(revision) = res.revision { - if let Some(callbacks) = context.seed.ctx.callbacks.get_store(package_id, &revision) { - callbacks.call(vector![]).await?; - } - } - - Ok(()) -} - -#[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -pub struct SetDataVersionParams { - #[ts(type = "string")] - version: VersionString, -} -pub async fn set_data_version( - context: EffectContext, - SetDataVersionParams { version }: SetDataVersionParams, -) -> Result<(), Error> { - let context = context.deref()?; - let package_id = &context.seed.id; - context - .seed - .ctx - .db - .mutate(|db| { - db.as_public_mut() - .as_package_data_mut() - .as_idx_mut(package_id) - .or_not_found(package_id)? - .as_data_version_mut() - .ser(&Some(version)) - }) - .await - .result?; - - Ok(()) -} - -pub async fn get_data_version(context: EffectContext) -> Result, Error> { - let context = context.deref()?; - let package_id = &context.seed.id; - context - .seed - .ctx - .db - .peek() - .await - .as_public() - .as_package_data() - .as_idx(package_id) - .or_not_found(package_id)? - .as_data_version() - .de() -} diff --git a/core/startos/src/service/effects/version.rs b/core/startos/src/service/effects/version.rs new file mode 100644 index 000000000..604d5d9b4 --- /dev/null +++ b/core/startos/src/service/effects/version.rs @@ -0,0 +1,51 @@ +use models::VersionString; + +use crate::service::effects::prelude::*; + +#[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct SetDataVersionParams { + #[ts(type = "string")] + version: VersionString, +} +pub async fn set_data_version( + context: EffectContext, + SetDataVersionParams { version }: SetDataVersionParams, +) -> Result<(), Error> { + let context = context.deref()?; + let package_id = &context.seed.id; + context + .seed + .ctx + .db + .mutate(|db| { + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(package_id) + .or_not_found(package_id)? + .as_data_version_mut() + .ser(&Some(version)) + }) + .await + .result?; + + Ok(()) +} + +pub async fn get_data_version(context: EffectContext) -> Result, Error> { + let context = context.deref()?; + let package_id = &context.seed.id; + context + .seed + .ctx + .db + .peek() + .await + .as_public() + .as_package_data() + .as_idx(package_id) + .or_not_found(package_id)? + .as_data_version() + .de() +} diff --git a/core/startos/src/service/service_map.rs b/core/startos/src/service/service_map.rs index 235f36f01..fea8184df 100644 --- a/core/startos/src/service/service_map.rs +++ b/core/startos/src/service/service_map.rs @@ -5,7 +5,7 @@ use std::time::Duration; use color_eyre::eyre::eyre; use futures::future::{BoxFuture, Fuse}; use futures::stream::FuturesUnordered; -use futures::{Future, FutureExt, StreamExt}; +use futures::{Future, FutureExt, StreamExt, TryFutureExt}; use helpers::NonDetachingJoinHandle; use imbl::OrdMap; use imbl_value::InternedString; @@ -332,22 +332,48 @@ impl ServiceMap { #[instrument(skip_all)] pub async fn uninstall( &self, - ctx: &RpcContext, - id: &PackageId, + ctx: RpcContext, + id: PackageId, soft: bool, force: bool, - ) -> Result<(), Error> { - let mut guard = self.get_mut(id).await; - if let Some(service) = guard.take() { + ) -> Result> + Send, Error> { + let mut guard = self.get_mut(&id).await; + ctx.db + .mutate(|db| { + let entry = db + .as_public_mut() + .as_package_data_mut() + .as_idx_mut(&id) + .or_not_found(&id)?; + entry.as_state_info_mut().map_mutate(|s| match s { + PackageState::Installed(s) => Ok(PackageState::Removing(s)), + _ => Err(Error::new( + eyre!("Package {id} is not installed."), + crate::ErrorKind::NotFound, + )), + }) + }) + .await + .result?; + Ok(async move { ServiceRefReloadCancelGuard::new(ctx.clone(), id.clone(), "Uninstall", None) .handle_last(async move { - let res = service.uninstall(None, soft, force).await; - drop(guard); - res + if let Some(service) = guard.take() { + let res = service.uninstall(None, soft, force).await; + drop(guard); + res + } else { + Err(Error::new( + eyre!("service {id} failed to initialize - cannot remove gracefully"), + ErrorKind::Uninitialized, + )) + } }) .await?; + + Ok(()) } - Ok(()) + .or_else(|e: Error| e.wait().map(Err))) } pub async fn shutdown_all(&self) -> Result<(), Error> { @@ -412,9 +438,13 @@ impl ServiceRefReloadCancelGuard { Ok(a) => Ok(a), Err(e) => { if let Some(info) = self.0.take() { - tokio::spawn(info.reload(Some(e.clone_output()))); + let task_e = e.clone_output(); + Err(e.with_task(tokio::spawn(async move { + info.reload(Some(task_e)).await.log_err(); + }))) + } else { + Err(e) } - Err(e) } } } diff --git a/core/startos/src/ssh.rs b/core/startos/src/ssh.rs index 0f236b813..89cabd1cd 100644 --- a/core/startos/src/ssh.rs +++ b/core/startos/src/ssh.rs @@ -80,11 +80,9 @@ impl std::fmt::Display for SshKeyResponse { impl std::str::FromStr for SshPubKey { type Err = Error; fn from_str(s: &str) -> Result { - s.parse().map(|pk| SshPubKey(pk)).map_err(|e| Error { - source: e.into(), - kind: crate::ErrorKind::ParseSshKey, - revision: None, - }) + s.parse() + .map(|pk| SshPubKey(pk)) + .with_kind(ErrorKind::ParseSshKey) } } @@ -171,11 +169,7 @@ pub async fn remove( if keys_ref.remove(&fingerprint)?.is_some() { keys_ref.de() } else { - Err(Error { - source: color_eyre::eyre::eyre!("SSH Key Not Found"), - kind: crate::error::ErrorKind::NotFound, - revision: None, - }) + Err(Error::new(eyre!("SSH Key Not Found"), ErrorKind::NotFound)) } }) .await diff --git a/core/startos/src/system.rs b/core/startos/src/system.rs index 9a2876295..b4e9015e9 100644 --- a/core/startos/src/system.rs +++ b/core/startos/src/system.rs @@ -740,14 +740,13 @@ async fn get_proc_stat() -> Result { .collect::, Error>>()?; if stats.len() < 10 { - Err(Error { - source: color_eyre::eyre::eyre!( + Err(Error::new( + eyre!( "Columns missing from /proc/stat. Need 10, found {}", stats.len() ), - kind: ErrorKind::ParseSysInfo, - revision: None, - }) + ErrorKind::ParseSysInfo, + )) } else { Ok(ProcStat { user: stats[0], diff --git a/sdk/base/lib/Effects.ts b/sdk/base/lib/Effects.ts index 8ebb59ab5..10c7e4c66 100644 --- a/sdk/base/lib/Effects.ts +++ b/sdk/base/lib/Effects.ts @@ -15,7 +15,6 @@ import { RequestActionParams, MainStatus, } from "./osBindings" -import { StorePath } from "./util/PathBuilder" import { PackageId, Dependencies, @@ -93,8 +92,6 @@ export type Effects = { }): Promise /** Returns a list of the ids of all installed packages */ getInstalledPackages(): Promise - /** grants access to certain paths in the store to dependents */ - exposeForDependents(options: { paths: string[] }): Promise // health /** sets the result of a health check */ @@ -170,23 +167,6 @@ export type Effects = { algorithm?: "ecdsa" | "ed25519" }) => Promise - // store - store: { - /** Get a value in a json like data, can be observed and subscribed */ - get(options: { - /** If there is no packageId it is assumed the current package */ - packageId?: string - /** The path defaults to root level, using the [JsonPath](https://jsonpath.com/) */ - path: StorePath - callback?: () => void - }): Promise - /** Used to store values that can be accessed and subscribed to */ - set(options: { - /** Sets the value for the wrapper at the path, it will override, using the [JsonPath](https://jsonpath.com/) */ - path: StorePath - value: ExtractStore - }): Promise - } /** sets the version that this service's data has been migrated to */ setDataVersion(options: { version: string }): Promise /** returns the version that this service's data has been migrated to */ diff --git a/sdk/base/lib/actions/index.ts b/sdk/base/lib/actions/index.ts index 0701e00e0..0dd3d99dc 100644 --- a/sdk/base/lib/actions/index.ts +++ b/sdk/base/lib/actions/index.ts @@ -45,18 +45,18 @@ export const runAction = async < }) } } -type GetActionInputType> = - A extends Action ? ExtractInputSpecType : never +type GetActionInputType> = + A extends Action ? ExtractInputSpecType : never type ActionRequestBase = { reason?: string replayId?: string } -type ActionRequestInput> = { +type ActionRequestInput> = { kind: "partial" value: T.DeepPartial> } -export type ActionRequestOptions> = +export type ActionRequestOptions> = ActionRequestBase & ( | { @@ -78,7 +78,7 @@ const _validate: T.ActionRequest = {} as ActionRequestOptions & { severity: T.ActionSeverity } -export const requestAction = >(options: { +export const requestAction = >(options: { effects: T.Effects packageId: T.PackageId action: T diff --git a/sdk/base/lib/actions/input/builder/inputSpec.ts b/sdk/base/lib/actions/input/builder/inputSpec.ts index 5d4d5c6bb..cd3a6e5d8 100644 --- a/sdk/base/lib/actions/input/builder/inputSpec.ts +++ b/sdk/base/lib/actions/input/builder/inputSpec.ts @@ -5,32 +5,27 @@ import { Effects } from "../../../Effects" import { Parser, object } from "ts-matches" import { DeepPartial } from "../../../types" -export type LazyBuildOptions = { +export type LazyBuildOptions = { effects: Effects } -export type LazyBuild = ( - options: LazyBuildOptions, +export type LazyBuild = ( + options: LazyBuildOptions, ) => Promise | ExpectedOut // prettier-ignore -export type ExtractInputSpecType | InputSpec, any> | InputSpec, never>> = - A extends InputSpec | InputSpec ? B : +export type ExtractInputSpecType | InputSpec>> = + A extends InputSpec ? B : A export type ExtractPartialInputSpecType< - A extends - | Record - | InputSpec, any> - | InputSpec, never>, -> = A extends InputSpec | InputSpec - ? DeepPartial - : DeepPartial + A extends Record | InputSpec>, +> = A extends InputSpec ? DeepPartial : DeepPartial -export type InputSpecOf, Store = never> = { - [K in keyof A]: Value +export type InputSpecOf> = { + [K in keyof A]: Value } -export type MaybeLazyValues = LazyBuild | A +export type MaybeLazyValues = LazyBuild | A /** * InputSpecs are the specs that are used by the os input specification form for this service. * Here is an example of a simple input specification @@ -87,16 +82,16 @@ export const addNodesSpec = InputSpec.of({ hostname: hostname, port: port }); ``` */ -export class InputSpec, Store = never> { +export class InputSpec> { private constructor( private readonly spec: { - [K in keyof Type]: Value | Value + [K in keyof Type]: Value }, public validator: Parser, ) {} public _TYPE: Type = null as any as Type public _PARTIAL: DeepPartial = null as any as DeepPartial - async build(options: LazyBuildOptions) { + async build(options: LazyBuildOptions) { const answer = {} as { [K in keyof Type]: ValueSpec } @@ -106,10 +101,7 @@ export class InputSpec, Store = never> { return answer } - static of< - Spec extends Record | Value>, - Store = never, - >(spec: Spec) { + static of>>(spec: Spec) { const validatorObj = {} as { [K in keyof Spec]: Parser } @@ -117,33 +109,8 @@ export class InputSpec, Store = never> { validatorObj[key] = spec[key].validator } const validator = object(validatorObj) - return new InputSpec< - { - [K in keyof Spec]: Spec[K] extends - | Value - | Value - ? T - : never - }, - Store - >(spec, validator as any) - } - - /** - * Use this during the times that the input needs a more specific type. - * Used in types that the value/ variant/ list/ inputSpec is constructed somewhere else. - ```ts - const a = InputSpec.text({ - name: "a", - required: false, - }) - - return InputSpec.of()({ - myValue: a.withStore(), - }) - ``` - */ - withStore() { - return this as any as InputSpec + return new InputSpec<{ + [K in keyof Spec]: Spec[K] extends Value ? T : never + }>(spec, validator as any) } } diff --git a/sdk/base/lib/actions/input/builder/list.ts b/sdk/base/lib/actions/input/builder/list.ts index 726dc961e..0639c0b3f 100644 --- a/sdk/base/lib/actions/input/builder/list.ts +++ b/sdk/base/lib/actions/input/builder/list.ts @@ -9,9 +9,9 @@ import { } from "../inputSpecTypes" import { Parser, arrayOf, string } from "ts-matches" -export class List { +export class List { private constructor( - public build: LazyBuild, + public build: LazyBuild, public validator: Parser, ) {} @@ -58,7 +58,7 @@ export class List { generate?: null | RandomString }, ) { - return new List(() => { + return new List(() => { const spec = { type: "text" as const, placeholder: null, @@ -85,30 +85,27 @@ export class List { }, arrayOf(string)) } - static dynamicText( - getA: LazyBuild< - Store, - { - name: string - description?: string | null - warning?: string | null - default?: string[] + static dynamicText( + getA: LazyBuild<{ + name: string + description?: string | null + warning?: string | null + default?: string[] + minLength?: number | null + maxLength?: number | null + disabled?: false | string + generate?: null | RandomString + spec: { + masked?: boolean + placeholder?: string | null minLength?: number | null maxLength?: number | null - disabled?: false | string - generate?: null | RandomString - spec: { - masked?: boolean - placeholder?: string | null - minLength?: number | null - maxLength?: number | null - patterns?: Pattern[] - inputmode?: ListValueSpecText["inputmode"] - } + patterns?: Pattern[] + inputmode?: ListValueSpecText["inputmode"] } - >, + }>, ) { - return new List(async (options) => { + return new List(async (options) => { const { spec: aSpec, ...a } = await getA(options) const spec = { type: "text" as const, @@ -136,7 +133,7 @@ export class List { }, arrayOf(string)) } - static obj, Store>( + static obj>( a: { name: string description?: string | null @@ -146,12 +143,12 @@ export class List { maxLength?: number | null }, aSpec: { - spec: InputSpec + spec: InputSpec displayAs?: null | string uniqueBy?: null | UniqueBy }, ) { - return new List(async (options) => { + return new List(async (options) => { const { spec: previousSpecSpec, ...restSpec } = aSpec const specSpec = await previousSpecSpec.build(options) const spec = { @@ -177,22 +174,4 @@ export class List { } }, arrayOf(aSpec.spec.validator)) } - - /** - * Use this during the times that the input needs a more specific type. - * Used in types that the value/ variant/ list/ inputSpec is constructed somewhere else. - ```ts - const a = InputSpec.text({ - name: "a", - required: false, - }) - - return InputSpec.of()({ - myValue: a.withStore(), - }) - ``` - */ - withStore() { - return this as any as List - } } diff --git a/sdk/base/lib/actions/input/builder/value.ts b/sdk/base/lib/actions/input/builder/value.ts index 3ff3c2d24..3ce0c8daa 100644 --- a/sdk/base/lib/actions/input/builder/value.ts +++ b/sdk/base/lib/actions/input/builder/value.ts @@ -24,7 +24,6 @@ import { number, object, string, - unknown, } from "ts-matches" import { DeepPartial } from "../../../types" @@ -44,14 +43,30 @@ function asRequiredParser< return parser.nullable() as any } -export class Value { +export class Value { protected constructor( - public build: LazyBuild, + public build: LazyBuild, public validator: Parser, ) {} public _TYPE: Type = null as any as Type public _PARTIAL: DeepPartial = null as any as DeepPartial + /** + * @description Displays a boolean toggle to enable/disable + * @example + * ``` + toggleExample: Value.toggle({ + // required + name: 'Toggle Example', + default: true, + + // optional + description: null, + warning: null, + immutable: false, + }), + * ``` + */ static toggle(a: { name: string description?: string | null @@ -64,7 +79,7 @@ export class Value { */ immutable?: boolean }) { - return new Value( + return new Value( async () => ({ description: null, warning: null, @@ -76,19 +91,16 @@ export class Value { boolean, ) } - static dynamicToggle( - a: LazyBuild< - Store, - { - name: string - description?: string | null - warning?: string | null - default: boolean - disabled?: false | string - } - >, + static dynamicToggle( + a: LazyBuild<{ + name: string + description?: string | null + warning?: string | null + default: boolean + disabled?: false | string + }>, ) { - return new Value( + return new Value( async (options) => ({ description: null, warning: null, @@ -100,6 +112,30 @@ export class Value { boolean, ) } + /** + * @description Displays a text input field + * @example + * ``` + textExample: Value.text({ + // required + name: 'Text Example', + required: false, + default: null, + + // optional + description: null, + placeholder: null, + warning: null, + generate: null, + inputmode: 'text', + masked: false, + minLength: null, + maxLength: null, + patterns: [], + immutable: false, + }), + * ``` + */ static text(a: { name: string description?: string | null @@ -151,7 +187,7 @@ export class Value { */ generate?: RandomString | null }) { - return new Value, never>( + return new Value>( async () => ({ type: "text" as const, description: null, @@ -170,27 +206,24 @@ export class Value { asRequiredParser(string, a), ) } - static dynamicText( - getA: LazyBuild< - Store, - { - name: string - description?: string | null - warning?: string | null - default: DefaultString | null - required: boolean - masked?: boolean - placeholder?: string | null - minLength?: number | null - maxLength?: number | null - patterns?: Pattern[] - inputmode?: ValueSpecText["inputmode"] - disabled?: string | false - generate?: null | RandomString - } - >, + static dynamicText( + getA: LazyBuild<{ + name: string + description?: string | null + warning?: string | null + default: DefaultString | null + required: boolean + masked?: boolean + placeholder?: string | null + minLength?: number | null + maxLength?: number | null + patterns?: Pattern[] + inputmode?: ValueSpecText["inputmode"] + disabled?: string | false + generate?: null | RandomString + }>, ) { - return new Value(async (options) => { + return new Value(async (options) => { const a = await getA(options) return { type: "text" as const, @@ -209,6 +242,26 @@ export class Value { } }, string.nullable()) } + /** + * @description Displays a large textarea field for long form entry. + * @example + * ``` + textareaExample: Value.textarea({ + // required + name: 'Textarea Example', + required: false, + default: null, + + // optional + description: null, + placeholder: null, + warning: null, + minLength: null, + maxLength: null, + immutable: false, + }), + * ``` + */ static textarea(a: { name: string description?: string | null @@ -225,7 +278,7 @@ export class Value { */ immutable?: boolean }) { - return new Value, never>( + return new Value>( async () => { const built: ValueSpecTextarea = { description: null, @@ -243,23 +296,20 @@ export class Value { asRequiredParser(string, a), ) } - static dynamicTextarea( - getA: LazyBuild< - Store, - { - name: string - description?: string | null - warning?: string | null - default: string | null - required: boolean - minLength?: number | null - maxLength?: number | null - placeholder?: string | null - disabled?: false | string - } - >, + static dynamicTextarea( + getA: LazyBuild<{ + name: string + description?: string | null + warning?: string | null + default: string | null + required: boolean + minLength?: number | null + maxLength?: number | null + placeholder?: string | null + disabled?: false | string + }>, ) { - return new Value(async (options) => { + return new Value(async (options) => { const a = await getA(options) return { description: null, @@ -274,6 +324,29 @@ export class Value { } }, string.nullable()) } + /** + * @description Displays a number input field + * @example + * ``` + numberExample: Value.number({ + // required + name: 'Number Example', + required: false, + default: null, + integer: true, + + // optional + description: null, + placeholder: null, + warning: null, + min: null, + max: null, + immutable: false, + step: null, + units: null, + }), + * ``` + */ static number(a: { name: string description?: string | null @@ -309,7 +382,7 @@ export class Value { */ immutable?: boolean }) { - return new Value, never>( + return new Value>( () => ({ type: "number" as const, description: null, @@ -326,26 +399,23 @@ export class Value { asRequiredParser(number, a), ) } - static dynamicNumber( - getA: LazyBuild< - Store, - { - name: string - description?: string | null - warning?: string | null - default: number | null - required: boolean - min?: number | null - max?: number | null - step?: number | null - integer: boolean - units?: string | null - placeholder?: string | null - disabled?: false | string - } - >, + static dynamicNumber( + getA: LazyBuild<{ + name: string + description?: string | null + warning?: string | null + default: number | null + required: boolean + min?: number | null + max?: number | null + step?: number | null + integer: boolean + units?: string | null + placeholder?: string | null + disabled?: false | string + }>, ) { - return new Value(async (options) => { + return new Value(async (options) => { const a = await getA(options) return { type: "number" as const, @@ -362,6 +432,23 @@ export class Value { } }, number.nullable()) } + /** + * @description Displays a browser-native color selector. + * @example + * ``` + colorExample: Value.color({ + // required + name: 'Color Example', + required: false, + default: null, + + // optional + description: null, + warning: null, + immutable: false, + }), + * ``` + */ static color(a: { name: string description?: string | null @@ -381,7 +468,7 @@ export class Value { */ immutable?: boolean }) { - return new Value, never>( + return new Value>( () => ({ type: "color" as const, description: null, @@ -394,20 +481,17 @@ export class Value { ) } - static dynamicColor( - getA: LazyBuild< - Store, - { - name: string - description?: string | null - warning?: string | null - default: string | null - required: boolean - disabled?: false | string - } - >, + static dynamicColor( + getA: LazyBuild<{ + name: string + description?: string | null + warning?: string | null + default: string | null + required: boolean + disabled?: false | string + }>, ) { - return new Value(async (options) => { + return new Value(async (options) => { const a = await getA(options) return { type: "color" as const, @@ -419,6 +503,26 @@ export class Value { } }, string.nullable()) } + /** + * @description Displays a browser-native date/time selector. + * @example + * ``` + datetimeExample: Value.datetime({ + // required + name: 'Datetime Example', + required: false, + default: null, + + // optional + description: null, + warning: null, + immutable: false, + inputmode: 'datetime-local', + min: null, + max: null, + }), + * ``` + */ static datetime(a: { name: string description?: string | null @@ -445,7 +549,7 @@ export class Value { */ immutable?: boolean }) { - return new Value, never>( + return new Value>( () => ({ type: "datetime" as const, description: null, @@ -461,23 +565,20 @@ export class Value { asRequiredParser(string, a), ) } - static dynamicDatetime( - getA: LazyBuild< - Store, - { - name: string - description?: string | null - warning?: string | null - default: string | null - required: boolean - inputmode?: ValueSpecDatetime["inputmode"] - min?: string | null - max?: string | null - disabled?: false | string - } - >, + static dynamicDatetime( + getA: LazyBuild<{ + name: string + description?: string | null + warning?: string | null + default: string | null + required: boolean + inputmode?: ValueSpecDatetime["inputmode"] + min?: string | null + max?: string | null + disabled?: false | string + }>, ) { - return new Value(async (options) => { + return new Value(async (options) => { const a = await getA(options) return { type: "datetime" as const, @@ -492,6 +593,27 @@ export class Value { } }, string.nullable()) } + /** + * @description Displays a select modal with radio buttons, allowing for a single selection. + * @example + * ``` + selectExample: Value.select({ + // required + name: 'Select Example', + default: 'radio1', + values: { + radio1: 'Radio 1', + radio2: 'Radio 2', + }, + + // optional + description: null, + warning: null, + immutable: false, + disabled: false, + }), + * ``` + */ static select>(a: { name: string description?: string | null @@ -522,7 +644,7 @@ export class Value { */ immutable?: boolean }) { - return new Value( + return new Value( () => ({ description: null, warning: null, @@ -536,20 +658,17 @@ export class Value { ), ) } - static dynamicSelect( - getA: LazyBuild< - Store, - { - name: string - description?: string | null - warning?: string | null - default: string - values: Record - disabled?: false | string | string[] - } - >, + static dynamicSelect( + getA: LazyBuild<{ + name: string + description?: string | null + warning?: string | null + default: string + values: Record + disabled?: false | string | string[] + }>, ) { - return new Value(async (options) => { + return new Value(async (options) => { const a = await getA(options) return { description: null, @@ -561,6 +680,29 @@ export class Value { } }, string) } + /** + * @description Displays a select modal with checkboxes, allowing for multiple selections. + * @example + * ``` + multiselectExample: Value.multiselect({ + // required + name: 'Multiselect Example', + values: { + option1: 'Option 1', + option2: 'Option 2', + }, + default: [], + + // optional + description: null, + warning: null, + immutable: false, + disabled: false, + minlength: null, + maxLength: null, + }), + * ``` + */ static multiselect>(a: { name: string description?: string | null @@ -590,7 +732,7 @@ export class Value { */ immutable?: boolean }) { - return new Value<(keyof Values)[], never>( + return new Value<(keyof Values)[]>( () => ({ type: "multiselect" as const, minLength: null, @@ -606,22 +748,19 @@ export class Value { ), ) } - static dynamicMultiselect( - getA: LazyBuild< - Store, - { - name: string - description?: string | null - warning?: string | null - default: string[] - values: Record - minLength?: number | null - maxLength?: number | null - disabled?: false | string | string[] - } - >, + static dynamicMultiselect( + getA: LazyBuild<{ + name: string + description?: string | null + warning?: string | null + default: string[] + values: Record + minLength?: number | null + maxLength?: number | null + disabled?: false | string | string[] + }>, ) { - return new Value(async (options) => { + return new Value(async (options) => { const a = await getA(options) return { type: "multiselect" as const, @@ -635,14 +774,31 @@ export class Value { } }, arrayOf(string)) } - static object, Store>( + /** + * @description Display a collapsable grouping of additional fields, a "sub form". The second value is the inputSpec spec for the sub form. + * @example + * ``` + objectExample: Value.object( + { + // required + name: 'Object Example', + + // optional + description: null, + warning: null, + }, + InputSpec.of({}), + ), + * ``` + */ + static object>( a: { name: string description?: string | null }, - spec: InputSpec, + spec: InputSpec, ) { - return new Value(async (options) => { + return new Value(async (options) => { const built = await spec.build(options as any) return { type: "object" as const, @@ -694,14 +850,42 @@ export class Value { // object({ filePath: string }).nullable(), // ) // } + /** + * @description Displays a dropdown, allowing for a single selection. Depending on the selection, a different object ("sub form") is presented. + * @example + * ``` + unionExample: Value.union( + { + // required + name: 'Union Example', + default: 'option1', + + // optional + description: null, + warning: null, + disabled: false, + immutable: false, + }, + Variants.of({ + option1: { + name: 'Option 1', + spec: InputSpec.of({}), + }, + option2: { + name: 'Option 2', + spec: InputSpec.of({}), + }, + }), + ), + * ``` + */ static union< VariantValues extends { [K in string]: { name: string - spec: InputSpec | InputSpec + spec: InputSpec } }, - Store, >( a: { name: string @@ -720,9 +904,9 @@ export class Value { */ immutable?: boolean }, - aVariants: Variants, + aVariants: Variants, ) { - return new Value( + return new Value( async (options) => ({ type: "union" as const, description: null, @@ -739,21 +923,20 @@ export class Value { VariantValues extends { [K in string]: { name: string - spec: InputSpec | InputSpec + spec: InputSpec } }, - Store, >( - getDisabledFn: LazyBuild, + getDisabledFn: LazyBuild, a: { name: string description?: string | null warning?: string | null default: keyof VariantValues & string }, - aVariants: Variants | Variants, + aVariants: Variants, ) { - return new Value( + return new Value( async (options) => ({ type: "union" as const, description: null, @@ -770,45 +953,107 @@ export class Value { VariantValues extends { [K in string]: { name: string - spec: InputSpec | InputSpec + spec: InputSpec } }, - Store, >( - getA: LazyBuild< - Store, - { - name: string - description?: string | null - warning?: string | null - default: keyof VariantValues & string - disabled: string[] | false | string - } - >, - aVariants: Variants | Variants, + getA: LazyBuild<{ + name: string + description?: string | null + warning?: string | null + default: keyof VariantValues & string + disabled: string[] | false | string + }>, + aVariants: Variants, ) { - return new Value( - async (options) => { - const newValues = await getA(options) - return { - type: "union" as const, + return new Value(async (options) => { + const newValues = await getA(options) + return { + type: "union" as const, + description: null, + warning: null, + ...newValues, + variants: await aVariants.build(options as any), + immutable: false, + } + }, aVariants.validator) + } + /** + * @description Presents an interface to add/remove/edit items in a list. + * @example + * In this example, we create a list of text inputs. + * + * ``` + listExampleText: Value.list( + List.text( + { + // required + name: 'Text List', + + // optional description: null, warning: null, - ...newValues, - variants: await aVariants.build(options as any), - immutable: false, - } - }, - aVariants.validator, - ) - } - - static list(a: List) { - return new Value((options) => a.build(options), a.validator) + default: [], + minLength: null, + maxLength: null, + }, + { + // required + patterns: [], + + // optional + placeholder: null, + generate: null, + inputmode: 'url', + masked: false, + minLength: null, + maxLength: null, + }, + ), + ), + * ``` + * @example + * In this example, we create a list of objects. + * + * ``` + listExampleObject: Value.list( + List.obj( + { + // required + name: 'Object List', + + // optional + description: null, + warning: null, + default: [], + minLength: null, + maxLength: null, + }, + { + // required + spec: InputSpec.of({}), + + // optional + displayAs: null, + uniqueBy: null, + }, + ), + ), + * ``` + */ + static list(a: List) { + return new Value((options) => a.build(options), a.validator) } + /** + * @description Provides a way to define a hidden field with a static value. Useful for tracking + * @example + * ``` + hiddenExample: Value.hidden(), + * ``` + */ static hidden(parser: Parser = any) { - return new Value(async () => { + return new Value(async () => { const built: ValueSpecHidden = { type: "hidden" as const, } @@ -816,25 +1061,7 @@ export class Value { }, parser) } - map(fn: (value: Type) => U): Value { + map(fn: (value: Type) => U): Value { return new Value(this.build, this.validator.map(fn)) } - - /** - * Use this during the times that the input needs a more specific type. - * Used in types that the value/ variant/ list/ inputSpec is constructed somewhere else. - ```ts - const a = InputSpec.text({ - name: "a", - required: false, - }) - - return InputSpec.of()({ - myValue: a.withStore(), - }) - ``` - */ - withStore() { - return this as any as Value - } } diff --git a/sdk/base/lib/actions/input/builder/variants.ts b/sdk/base/lib/actions/input/builder/variants.ts index 93453d73c..9830f7346 100644 --- a/sdk/base/lib/actions/input/builder/variants.ts +++ b/sdk/base/lib/actions/input/builder/variants.ts @@ -9,11 +9,10 @@ import { import { Parser, anyOf, literal, object } from "ts-matches" export type UnionRes< - Store, VariantValues extends { [K in string]: { name: string - spec: InputSpec | InputSpec + spec: InputSpec } }, K extends keyof VariantValues & string = keyof VariantValues & string, @@ -81,23 +80,21 @@ export class Variants< VariantValues extends { [K in string]: { name: string - spec: InputSpec | InputSpec + spec: InputSpec } }, - Store, > { private constructor( - public build: LazyBuild, - public validator: Parser>, + public build: LazyBuild, + public validator: Parser>, ) {} static of< VariantValues extends { [K in string]: { name: string - spec: InputSpec | InputSpec + spec: InputSpec } }, - Store = never, >(a: VariantValues) { const validator = anyOf( ...Object.entries(a).map(([id, { spec }]) => @@ -108,7 +105,7 @@ export class Variants< ), ) as Parser - return new Variants(async (options) => { + return new Variants(async (options) => { const variants = {} as { [K in keyof VariantValues]: { name: string @@ -125,21 +122,4 @@ export class Variants< return variants }, validator) } - /** - * Use this during the times that the input needs a more specific type. - * Used in types that the value/ variant/ list/ inputSpec is constructed somewhere else. - ```ts - const a = InputSpec.text({ - name: "a", - required: false, - }) - - return InputSpec.of()({ - myValue: a.withStore(), - }) - ``` - */ - withStore() { - return this as any as Variants - } } diff --git a/sdk/base/lib/actions/input/inputSpecConstants.ts b/sdk/base/lib/actions/input/inputSpecConstants.ts index 64857d419..725b0630a 100644 --- a/sdk/base/lib/actions/input/inputSpecConstants.ts +++ b/sdk/base/lib/actions/input/inputSpecConstants.ts @@ -7,7 +7,7 @@ import { Variants } from "./builder/variants" /** * Base SMTP settings, to be used by StartOS for system wide SMTP */ -export const customSmtp = InputSpec.of, never>({ +export const customSmtp = InputSpec.of>({ server: Value.text({ name: "SMTP Server", required: true, diff --git a/sdk/base/lib/actions/setupActions.ts b/sdk/base/lib/actions/setupActions.ts index 290a1c0f0..c931f981f 100644 --- a/sdk/base/lib/actions/setupActions.ts +++ b/sdk/base/lib/actions/setupActions.ts @@ -7,19 +7,13 @@ import * as T from "../types" import { once } from "../util" export type Run< - A extends - | Record - | InputSpec, any> - | InputSpec, never>, + A extends Record | InputSpec>, > = (options: { effects: T.Effects input: ExtractInputSpecType }) => Promise<(T.ActionResult & { version: "1" }) | null | void | undefined> export type GetInput< - A extends - | Record - | InputSpec, any> - | InputSpec, never>, + A extends Record | InputSpec>, > = (options: { effects: T.Effects }) => Promise> @@ -48,11 +42,7 @@ function mapMaybeFn( export class Action< Id extends T.ActionId, - Store, - InputSpecType extends - | Record - | InputSpec - | InputSpec, + InputSpecType extends Record | InputSpec, > { private constructor( readonly id: Id, @@ -63,18 +53,14 @@ export class Action< ) {} static withInput< Id extends T.ActionId, - Store, - InputSpecType extends - | Record - | InputSpec - | InputSpec, + InputSpecType extends Record | InputSpec, >( id: Id, metadata: MaybeFn>, inputSpec: InputSpecType, getInput: GetInput, run: Run, - ): Action { + ): Action { return new Action( id, mapMaybeFn(metadata, (m) => ({ ...m, hasInput: true })), @@ -83,11 +69,11 @@ export class Action< run, ) } - static withoutInput( + static withoutInput( id: Id, metadata: MaybeFn>, run: Run<{}>, - ): Action { + ): Action { return new Action( id, mapMaybeFn(metadata, (m) => ({ ...m, hasInput: false })), @@ -124,16 +110,15 @@ export class Action< } export class Actions< - Store, - AllActions extends Record>, + AllActions extends Record>, > { private constructor(private readonly actions: AllActions) {} - static of(): Actions { + static of(): Actions<{}> { return new Actions({}) } - addAction>( - action: A, - ): Actions { + addAction>( + action: A, // TODO: prevent duplicates + ): Actions { return new Actions({ ...this.actions, [action.id]: action }) } async update(options: { effects: T.Effects }): Promise { diff --git a/sdk/base/lib/backup/Backups.ts b/sdk/base/lib/backup/Backups.ts index 3e644014a..93adaebcd 100644 --- a/sdk/base/lib/backup/Backups.ts +++ b/sdk/base/lib/backup/Backups.ts @@ -90,7 +90,7 @@ export class Backups { return this } - addVolume( + mountVolume( volume: M["volumes"][number], options?: Partial<{ options: T.SyncOptions diff --git a/sdk/base/lib/osBindings/ExposeForDependentsParams.ts b/sdk/base/lib/osBindings/ExposeForDependentsParams.ts deleted file mode 100644 index 5b55368b9..000000000 --- a/sdk/base/lib/osBindings/ExposeForDependentsParams.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ExposeForDependentsParams = { paths: string[] } diff --git a/sdk/base/lib/osBindings/GetStoreParams.ts b/sdk/base/lib/osBindings/GetStoreParams.ts deleted file mode 100644 index e134cd4a6..000000000 --- a/sdk/base/lib/osBindings/GetStoreParams.ts +++ /dev/null @@ -1,9 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { CallbackId } from "./CallbackId" -import type { PackageId } from "./PackageId" - -export type GetStoreParams = { - packageId?: PackageId - path: string - callback?: CallbackId -} diff --git a/sdk/base/lib/osBindings/SetStoreParams.ts b/sdk/base/lib/osBindings/SetStoreParams.ts deleted file mode 100644 index ecdd7b042..000000000 --- a/sdk/base/lib/osBindings/SetStoreParams.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type SetStoreParams = { value: any; path: string } diff --git a/sdk/base/lib/osBindings/index.ts b/sdk/base/lib/osBindings/index.ts index bc9413ba5..56b115b39 100644 --- a/sdk/base/lib/osBindings/index.ts +++ b/sdk/base/lib/osBindings/index.ts @@ -77,7 +77,6 @@ export { EditSignerParams } from "./EditSignerParams" export { EncryptedWire } from "./EncryptedWire" export { ExportActionParams } from "./ExportActionParams" export { ExportServiceInterfaceParams } from "./ExportServiceInterfaceParams" -export { ExposeForDependentsParams } from "./ExposeForDependentsParams" export { FileType } from "./FileType" export { ForgetInterfaceParams } from "./ForgetInterfaceParams" export { FullIndex } from "./FullIndex" @@ -95,7 +94,6 @@ export { GetServicePortForwardParams } from "./GetServicePortForwardParams" export { GetSslCertificateParams } from "./GetSslCertificateParams" export { GetSslKeyParams } from "./GetSslKeyParams" export { GetStatusParams } from "./GetStatusParams" -export { GetStoreParams } from "./GetStoreParams" export { GetSystemSmtpParams } from "./GetSystemSmtpParams" export { GigaBytes } from "./GigaBytes" export { GitHash } from "./GitHash" @@ -195,7 +193,6 @@ export { SetIconParams } from "./SetIconParams" export { SetMainStatusStatus } from "./SetMainStatusStatus" export { SetMainStatus } from "./SetMainStatus" export { SetNameParams } from "./SetNameParams" -export { SetStoreParams } from "./SetStoreParams" export { SetupExecuteParams } from "./SetupExecuteParams" export { SetupProgress } from "./SetupProgress" export { SetupResult } from "./SetupResult" diff --git a/sdk/base/lib/test/startosTypeValidation.test.ts b/sdk/base/lib/test/startosTypeValidation.test.ts index 58918ec2b..fc87b70cd 100644 --- a/sdk/base/lib/test/startosTypeValidation.test.ts +++ b/sdk/base/lib/test/startosTypeValidation.test.ts @@ -19,7 +19,6 @@ import { DestroySubcontainerFsParams } from ".././osBindings" import { BindParams } from ".././osBindings" import { GetHostInfoParams } from ".././osBindings" import { SetHealth } from ".././osBindings" -import { ExposeForDependentsParams } from ".././osBindings" import { GetSslCertificateParams } from ".././osBindings" import { GetSslKeyParams } from ".././osBindings" import { GetServiceInterfaceParams } from ".././osBindings" @@ -71,15 +70,10 @@ describe("startosTypeValidation ", () => { setDataVersion: {} as SetDataVersionParams, getDataVersion: undefined, setHealth: {} as SetHealth, - exposeForDependents: {} as ExposeForDependentsParams, getSslCertificate: {} as WithCallback, getSslKey: {} as GetSslKeyParams, getServiceInterface: {} as WithCallback, setDependencies: {} as SetDependenciesParams, - store: { - get: {} as any, // as GetStoreParams, - set: {} as any, // as SetStoreParams, - }, getSystemSmtp: {} as WithCallback, getContainerIp: {} as WithCallback, getOsIp: undefined, diff --git a/sdk/base/lib/types.ts b/sdk/base/lib/types.ts index ed82489be..3b989c229 100644 --- a/sdk/base/lib/types.ts +++ b/sdk/base/lib/types.ts @@ -19,8 +19,6 @@ export { CurrentDependenciesResult, } from "./dependencies/setupDependencies" -export type ExposedStorePaths = string[] & Affine<"ExposedStorePaths"> - export type DaemonBuildable = { build(): Promise<{ term(): Promise @@ -85,10 +83,7 @@ export namespace ExpectedExports { export type manifest = Manifest - export type actions = Actions< - any, - Record> - > + export type actions = Actions>> } export type ABI = { createBackup: ExpectedExports.createBackup @@ -141,10 +136,6 @@ export type Hostname = string & { [hostName]: never } export type ServiceInterfaceId = string export { ServiceInterface } -export type ExposeServicePaths = { - /** The path to the value in the Store. [JsonPath](https://jsonpath.com/) */ - paths: ExposedStorePaths -} export type EffectMethod = { [K in keyof T]-?: K extends string diff --git a/sdk/base/lib/util/PathBuilder.ts b/sdk/base/lib/util/PathBuilder.ts deleted file mode 100644 index 038fa5ac2..000000000 --- a/sdk/base/lib/util/PathBuilder.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Affine } from "../util" - -const pathValue = Symbol("pathValue") -export type PathValue = typeof pathValue - -export type PathBuilderStored = { - [K in PathValue]: [AllStore, Store] -} - -export type PathBuilder = (Store extends Record< - string, - unknown -> - ? { - [K in keyof Store]: PathBuilder - } - : {}) & - PathBuilderStored - -export type StorePath = string & Affine<"StorePath"> -const privateSymbol = Symbol("jsonPath") -export const extractJsonPath = (builder: PathBuilder) => { - return (builder as any)[privateSymbol] as StorePath -} - -export const pathBuilder = ( - paths: string[] = [], -): PathBuilder => { - return new Proxy({} as PathBuilder, { - get(target, prop) { - if (prop === privateSymbol) { - if (paths.length === 0) return "" - return `/${paths.join("/")}` - } - return pathBuilder([...paths, prop as string]) - }, - }) as PathBuilder -} diff --git a/sdk/base/lib/util/index.ts b/sdk/base/lib/util/index.ts index 4c9e803bb..2f3e981e6 100644 --- a/sdk/base/lib/util/index.ts +++ b/sdk/base/lib/util/index.ts @@ -17,6 +17,5 @@ export { nullIfEmpty } from "./nullIfEmpty" export { deepMerge, partialDiff } from "./deepMerge" export { deepEqual } from "./deepEqual" export { hostnameInfoToAddress } from "./Hostname" -export { PathBuilder, extractJsonPath, StorePath } from "./PathBuilder" export * as regexes from "./regexes" export { stringFromStdErrOut } from "./stringFromStdErrOut" diff --git a/sdk/lib/coverage/lcov-report/lib/mainFn/Mounts.ts.html b/sdk/lib/coverage/lcov-report/lib/mainFn/Mounts.ts.html index 63e96f44e..29d21179d 100644 --- a/sdk/lib/coverage/lcov-report/lib/mainFn/Mounts.ts.html +++ b/sdk/lib/coverage/lcov-report/lib/mainFn/Mounts.ts.html @@ -340,7 +340,7 @@ export class Mounts<Manifest extends T.Manifest> { return new Mounts<Manifest>([], [], []) }   - addVolume( + mountVolume( id: Manifest["volumes"][number], subpath: string | null, mountpoint: string, @@ -355,7 +355,7 @@ export class Mounts<Manifest extends T.Manifest> { return this }   - addAssets( + mountAssets( id: Manifest["assets"][number], subpath: string | null, mountpoint: string, @@ -368,7 +368,7 @@ export class Mounts<Manifest extends T.Manifest> { return this }   - addDependency<DependencyManifest extends T.Manifest>( + mountDependency<DependencyManifest extends T.Manifest>( dependencyId: keyof Manifest["dependencies"] & string, volumeId: DependencyManifest["volumes"][number], subpath: string | null, diff --git a/sdk/package/lib/StartSdk.ts b/sdk/package/lib/StartSdk.ts index 7640ff1c2..bb0e9ee19 100644 --- a/sdk/package/lib/StartSdk.ts +++ b/sdk/package/lib/StartSdk.ts @@ -1,18 +1,5 @@ import { Value } from "../../base/lib/actions/input/builder/value" -import { - InputSpec, - ExtractInputSpecType, - LazyBuild, -} from "../../base/lib/actions/input/builder/inputSpec" -import { - DefaultString, - ListValueSpecText, - Pattern, - RandomString, - UniqueBy, - ValueSpecDatetime, - ValueSpecText, -} from "../../base/lib/actions/input/inputSpecTypes" +import { InputSpec } from "../../base/lib/actions/input/builder/inputSpec" import { Variants } from "../../base/lib/actions/input/builder/variants" import { Action, Actions } from "../../base/lib/actions/setupActions" import { @@ -25,17 +12,12 @@ import { import * as patterns from "../../base/lib/util/patterns" import { BackupSync, Backups } from "./backup/Backups" import { smtpInputSpec } from "../../base/lib/actions/input/inputSpecConstants" -import { CommandController, Daemon, Daemons } from "./mainFn/Daemons" +import { Daemon, Daemons } from "./mainFn/Daemons" import { HealthCheck } from "./health/HealthCheck" import { checkPortListening } from "./health/checkFns/checkPortListening" import { checkWebUrl, runHealthScript } from "./health/checkFns" import { List } from "../../base/lib/actions/input/builder/list" -import { - Install, - InstallFn, - PostInstall, - PreInstall, -} from "./inits/setupInstall" +import { InstallFn, PostInstall, PreInstall } from "./inits/setupInstall" import { SetupBackupsParams, setupBackups } from "./backup/setupBackups" import { UninstallFn, setupUninstall } from "./inits/setupUninstall" import { setupMain } from "./mainFn" @@ -51,24 +33,12 @@ import { ServiceInterfaceBuilder } from "../../base/lib/interfaces/ServiceInterf import { GetSystemSmtp } from "./util" import { nullIfEmpty } from "./util" import { getServiceInterface, getServiceInterfaces } from "./util" -import { getStore } from "./store/getStore" -import { - CommandOptions, - ExitError, - MountOptions, - SubContainer, -} from "./util/SubContainer" +import { CommandOptions, ExitError, SubContainer } from "./util/SubContainer" import { splitCommand } from "./util" import { Mounts } from "./mainFn/Mounts" import { setupDependencies } from "../../base/lib/dependencies/setupDependencies" import * as T from "../../base/lib/types" import { testTypeVersion } from "../../base/lib/exver" -import { ExposedStorePaths } from "./store/setupExposeStore" -import { - PathBuilder, - extractJsonPath, - pathBuilder, -} from "../../base/lib/util/PathBuilder" import { CheckDependencies, checkDependencies, @@ -91,19 +61,16 @@ type AnyNeverCond = T extends [any, ...infer U] ? AnyNeverCond : never -export class StartSdk { +export class StartSdk { private constructor(readonly manifest: Manifest) {} static of() { - return new StartSdk(null as never) + return new StartSdk(null as never) } withManifest(manifest: Manifest) { - return new StartSdk(manifest) - } - withStore>() { - return new StartSdk(this.manifest) + return new StartSdk(manifest) } - build(isReady: AnyNeverCond<[Manifest, Store], "Build not ready", true>) { + build(isReady: AnyNeverCond<[Manifest], "Build not ready", true>) { type NestedEffects = "subcontainer" | "store" | "action" type InterfaceEffects = | "getServiceInterface" @@ -136,8 +103,6 @@ export class StartSdk { mount: (effects, ...args) => effects.mount(...args), getInstalledPackages: (effects, ...args) => effects.getInstalledPackages(...args), - exposeForDependents: (effects, ...args) => - effects.exposeForDependents(...args), getServicePortForward: (effects, ...args) => effects.getServicePortForward(...args), clearBindings: (effects, ...args) => effects.clearBindings(...args), @@ -155,7 +120,7 @@ export class StartSdk { ...startSdkEffectWrapper, action: { run: actions.runAction, - request: >( + request: >( effects: T.Effects, packageId: T.PackageId, action: T, @@ -169,7 +134,7 @@ export class StartSdk { severity, options: options, }), - requestOwn: >( + requestOwn: >( effects: T.Effects, action: T, severity: T.ActionSeverity, @@ -268,29 +233,6 @@ export class StartSdk { }, } }, - store: { - get: ( - effects: E, - packageId: string, - path: PathBuilder, - ) => - getStore(effects, path, { - packageId, - }), - getOwn: ( - effects: E, - path: PathBuilder, - ) => getStore(effects, path), - setOwn: >( - effects: E, - path: Path, - value: Path extends PathBuilder ? Value : never, - ) => - effects.store.set({ - value, - path: extractJsonPath(path), - }), - }, MultiHost: { of: (effects: Effects, id: string) => new MultiHost({ id, effects }), @@ -362,10 +304,7 @@ export class StartSdk { */ withInput: < Id extends T.ActionId, - InputSpecType extends - | Record - | InputSpec - | InputSpec, + InputSpecType extends Record | InputSpec, >( id: Id, metadata: MaybeFn>, @@ -382,6 +321,7 @@ export class StartSdk { * In this example, we create an action that returns a secret phrase for the user to see. * * ``` + import { store } from '../file-models/store.json' import { sdk } from '../sdk' export const showSecretPhrase = sdk.Action.withoutInput( @@ -406,9 +346,7 @@ export class StartSdk { 'Below is your secret phrase. Use it to gain access to extraordinary places', result: { type: 'single', - value: await sdk.store - .getOwn(effects, sdk.StorePath.secretPhrase) - .const(), + value: (await store.read.once())?.secretPhrase, copyable: true, qr: true, masked: true, @@ -499,7 +437,7 @@ export class StartSdk { export const actions = sdk.Actions.of().addAction(config).addAction(nameToLogs) * ``` */ - Actions: Actions, + Actions: Actions<{}>, /** * @description Use this function to determine which volumes are backed up when a user creates a backup, including advanced options. * @example @@ -530,11 +468,11 @@ export class StartSdk { /** * @description Use this function to set dependency information. * @example - * In this example, we create a perpetual dependency on Hello World >=1.0.0:0, where Hello World must be running and passing its "primary" health check. + * In this example, we create a dependency on Hello World >=1.0.0:0, where Hello World must be running and passing its "primary" health check. * * ``` export const setDependencies = sdk.setupDependencies( - async ({ effects, input }) => { + async ({ effects }) => { return { 'hello-world': { kind: 'running', @@ -545,29 +483,9 @@ export class StartSdk { }, ) * ``` - * @example - * In this example, we create a conditional dependency on Hello World based on a hypothetical "needsWorld" boolean in our Store. - * Using .const() ensures that if the "needsWorld" boolean changes, setupDependencies will re-run. - * - * ``` - export const setDependencies = sdk.setupDependencies( - async ({ effects }) => { - if (sdk.store.getOwn(sdk.StorePath.needsWorld).const()) { - return { - 'hello-world': { - kind: 'running', - versionRange: '>=1.0.0', - healthChecks: ['primary'], - }, - } - } - return {} - }, - ) - * ``` */ setupDependencies: setupDependencies, - setupInit: setupInit, + setupInit: setupInit, /** * @description Use this function to execute arbitrary logic *once*, on initial install *before* interfaces, actions, and dependencies are updated. * @example @@ -579,26 +497,21 @@ export class StartSdk { }) * ``` */ - setupPreInstall: (fn: InstallFn) => PreInstall.of(fn), + setupPreInstall: (fn: InstallFn) => PreInstall.of(fn), /** * @description Use this function to execute arbitrary logic *once*, on initial install *after* interfaces, actions, and dependencies are updated. * @example - * In the this example, we bootstrap our Store with a random, 16-char admin password. + * In the this example, we create a task for the user to perform. * * ``` const postInstall = sdk.setupPostInstall(async ({ effects }) => { - await sdk.store.setOwn( - effects, - sdk.StorePath.adminPassword, - utils.getDefaultString({ - charset: 'a-z,A-Z,1-9,!,@,$,%,&,', - len: 16, - }), - ) + await sdk.action.requestOwn(effects, showSecretPhrase, 'important', { + reason: 'Check out your secret phrase!', + }) }) * ``` */ - setupPostInstall: (fn: InstallFn) => PostInstall.of(fn), + setupPostInstall: (fn: InstallFn) => PostInstall.of(fn), /** * @description Use this function to determine how this service will be hosted and served. The function executes on service install, service update, and inputSpec save. * @param inputSpec - The inputSpec spec of this service as exported from /inputSpec/spec. @@ -673,12 +586,12 @@ export class StartSdk { effects: Effects started(onTerm: () => PromiseLike): PromiseLike }) => Promise>, - ) => setupMain(fn), + ) => setupMain(fn), /** * Use this function to execute arbitrary logic *once*, on uninstall only. Most services will not use this. */ - setupUninstall: (fn: UninstallFn) => - setupUninstall(fn), + setupUninstall: (fn: UninstallFn) => + setupUninstall(fn), trigger: { defaultTrigger, cooldownTrigger, @@ -728,11 +641,8 @@ export class StartSdk { }) * ``` */ - of: < - Spec extends Record | Value>, - >( - spec: Spec, - ) => InputSpec.of(spec), + of: >>(spec: Spec) => + InputSpec.of(spec), }, Daemon: { get of() { @@ -787,372 +697,9 @@ export class StartSdk { return SubContainer.withTemp(effects, image, mounts, name, fn) }, }, - List: { - /** - * @description Create a list of text inputs. - * @param a - attributes of the list itself. - * @param aSpec - attributes describing each member of the list. - */ - text: List.text, - /** - * @description Create a list of objects. - * @param a - attributes of the list itself. - * @param aSpec - attributes describing each member of the list. - */ - obj: >( - a: Parameters>[0], - aSpec: Parameters>[1], - ) => List.obj(a, aSpec), - /** - * @description Create a list of dynamic text inputs. - * @param a - attributes of the list itself. - * @param aSpec - attributes describing each member of the list. - */ - dynamicText: List.dynamicText, - }, - StorePath: pathBuilder(), - Value: { - /** - * @description Displays a boolean toggle to enable/disable - * @example - * ``` - toggleExample: Value.toggle({ - // required - name: 'Toggle Example', - default: true, - - // optional - description: null, - warning: null, - immutable: false, - }), - * ``` - */ - toggle: Value.toggle, - /** - * @description Displays a text input field - * @example - * ``` - textExample: Value.text({ - // required - name: 'Text Example', - required: false, - default: null, - - // optional - description: null, - placeholder: null, - warning: null, - generate: null, - inputmode: 'text', - masked: false, - minLength: null, - maxLength: null, - patterns: [], - immutable: false, - }), - * ``` - */ - text: Value.text, - /** - * @description Displays a large textarea field for long form entry. - * @example - * ``` - textareaExample: Value.textarea({ - // required - name: 'Textarea Example', - required: false, - default: null, - - // optional - description: null, - placeholder: null, - warning: null, - minLength: null, - maxLength: null, - immutable: false, - }), - * ``` - */ - textarea: Value.textarea, - /** - * @description Displays a number input field - * @example - * ``` - numberExample: Value.number({ - // required - name: 'Number Example', - required: false, - default: null, - integer: true, - - // optional - description: null, - placeholder: null, - warning: null, - min: null, - max: null, - immutable: false, - step: null, - units: null, - }), - * ``` - */ - number: Value.number, - /** - * @description Displays a browser-native color selector. - * @example - * ``` - colorExample: Value.color({ - // required - name: 'Color Example', - required: false, - default: null, - - // optional - description: null, - warning: null, - immutable: false, - }), - * ``` - */ - color: Value.color, - /** - * @description Displays a browser-native date/time selector. - * @example - * ``` - datetimeExample: Value.datetime({ - // required - name: 'Datetime Example', - required: false, - default: null, - - // optional - description: null, - warning: null, - immutable: false, - inputmode: 'datetime-local', - min: null, - max: null, - }), - * ``` - */ - datetime: Value.datetime, - /** - * @description Displays a select modal with radio buttons, allowing for a single selection. - * @example - * ``` - selectExample: Value.select({ - // required - name: 'Select Example', - default: 'radio1', - values: { - radio1: 'Radio 1', - radio2: 'Radio 2', - }, - - // optional - description: null, - warning: null, - immutable: false, - disabled: false, - }), - * ``` - */ - select: Value.select, - /** - * @description Displays a select modal with checkboxes, allowing for multiple selections. - * @example - * ``` - multiselectExample: Value.multiselect({ - // required - name: 'Multiselect Example', - values: { - option1: 'Option 1', - option2: 'Option 2', - }, - default: [], - - // optional - description: null, - warning: null, - immutable: false, - disabled: false, - minlength: null, - maxLength: null, - }), - * ``` - */ - multiselect: Value.multiselect, - /** - * @description Display a collapsable grouping of additional fields, a "sub form". The second value is the inputSpec spec for the sub form. - * @example - * ``` - objectExample: Value.object( - { - // required - name: 'Object Example', - - // optional - description: null, - warning: null, - }, - InputSpec.of({}), - ), - * ``` - */ - object: Value.object, - /** - * @description Displays a dropdown, allowing for a single selection. Depending on the selection, a different object ("sub form") is presented. - * @example - * ``` - unionExample: Value.union( - { - // required - name: 'Union Example', - default: 'option1', - - // optional - description: null, - warning: null, - disabled: false, - immutable: false, - }, - Variants.of({ - option1: { - name: 'Option 1', - spec: InputSpec.of({}), - }, - option2: { - name: 'Option 2', - spec: InputSpec.of({}), - }, - }), - ), - * ``` - */ - union: Value.union, - /** - * @description Presents an interface to add/remove/edit items in a list. - * @example - * In this example, we create a list of text inputs. - * - * ``` - listExampleText: Value.list( - List.text( - { - // required - name: 'Text List', - - // optional - description: null, - warning: null, - default: [], - minLength: null, - maxLength: null, - }, - { - // required - patterns: [], - - // optional - placeholder: null, - generate: null, - inputmode: 'url', - masked: false, - minLength: null, - maxLength: null, - }, - ), - ), - * ``` - * @example - * In this example, we create a list of objects. - * - * ``` - listExampleObject: Value.list( - List.obj( - { - // required - name: 'Object List', - - // optional - description: null, - warning: null, - default: [], - minLength: null, - maxLength: null, - }, - { - // required - spec: InputSpec.of({}), - - // optional - displayAs: null, - uniqueBy: null, - }, - ), - ), - * ``` - */ - list: Value.list, - hidden: Value.hidden, - dynamicToggle: Value.dynamicToggle, - dynamicText: Value.dynamicText, - dynamicTextarea: Value.dynamicTextarea, - dynamicNumber: Value.dynamicNumber, - dynamicColor: Value.dynamicColor, - dynamicDatetime: Value.dynamicDatetime, - dynamicSelect: Value.dynamicSelect, - dynamicMultiselect: Value.dynamicMultiselect, - filteredUnion: < - VariantValues extends { - [K in string]: { - name: string - spec: InputSpec | InputSpec - } - }, - >( - getDisabledFn: Parameters< - typeof Value.filteredUnion - >[0], - a: Parameters>[1], - aVariants: Parameters< - typeof Value.filteredUnion - >[2], - ) => - Value.filteredUnion( - getDisabledFn, - a, - aVariants, - ), - - dynamicUnion: < - VariantValues extends { - [K in string]: { - name: string - spec: InputSpec | InputSpec - } - }, - >( - getA: Parameters>[0], - aVariants: Parameters< - typeof Value.dynamicUnion - >[1], - ) => Value.dynamicUnion(getA, aVariants), - }, - Variants: { - of: < - VariantValues extends { - [K in string]: { - name: string - spec: InputSpec - } - }, - >( - a: VariantValues, - ) => Variants.of(a), - }, + List, + Value, + Variants, } } } diff --git a/sdk/package/lib/backup/Backups.ts b/sdk/package/lib/backup/Backups.ts index fe525f2fb..fdf4f0c4b 100644 --- a/sdk/package/lib/backup/Backups.ts +++ b/sdk/package/lib/backup/Backups.ts @@ -1,7 +1,7 @@ import * as T from "../../../base/lib/types" import * as child_process from "child_process" import * as fs from "fs/promises" -import { Affine, asError, StorePath } from "../util" +import { Affine, asError } from "../util" export const DEFAULT_OPTIONS: T.SyncOptions = { delete: true, @@ -96,7 +96,7 @@ export class Backups { return this } - addVolume( + mountVolume( volume: M["volumes"][number], options?: Partial<{ options: T.SyncOptions @@ -133,11 +133,7 @@ export class Backups { }) await rsyncResults.wait() } - await fs.writeFile( - "/media/startos/backup/store.json", - JSON.stringify(await effects.store.get({ path: "" as StorePath })), - { encoding: "utf-8" }, - ) + const dataVersion = await effects.getDataVersion() if (dataVersion) await fs.writeFile("/media/startos/backup/dataVersion.txt", dataVersion, { @@ -149,16 +145,7 @@ export class Backups { async restoreBackup(effects: T.Effects) { this.preRestore(effects as BackupEffects) - const store = await fs - .readFile("/media/startos/backup/store.json", { - encoding: "utf-8", - }) - .catch((_) => null) - if (store) - await effects.store.set({ - path: "" as StorePath, - value: JSON.parse(store), - }) + for (const item of this.backupSet) { const rsyncResults = await runRsync({ srcPath: item.backupPath, diff --git a/sdk/package/lib/health/HealthCheck.ts b/sdk/package/lib/health/HealthCheck.ts index 07a9179a5..54c68e977 100644 --- a/sdk/package/lib/health/HealthCheck.ts +++ b/sdk/package/lib/health/HealthCheck.ts @@ -30,7 +30,7 @@ export class HealthCheck extends Drop { super() this.promise = Promise.resolve().then(async () => { const getCurrentValue = () => this.currentValue - const gracePeriod = o.gracePeriod ?? 5000 + const gracePeriod = o.gracePeriod ?? 10_000 const trigger = (o.trigger ?? defaultTrigger)(getCurrentValue) const triggerFirstSuccess = once(() => Promise.resolve( diff --git a/sdk/package/lib/index.ts b/sdk/package/lib/index.ts index dd301c6ee..ca689d5eb 100644 --- a/sdk/package/lib/index.ts +++ b/sdk/package/lib/index.ts @@ -29,8 +29,6 @@ export { SubContainer } from "./util/SubContainer" export { StartSdk } from "./StartSdk" export { setupManifest, buildManifest } from "./manifest/setupManifest" export { FileHelper } from "./util/fileHelper" -export { setupExposeStore } from "./store/setupExposeStore" -export { pathBuilder } from "../../base/lib/util/PathBuilder" export * as actions from "../../base/lib/actions" export * as backup from "./backup" diff --git a/sdk/package/lib/inits/setupInit.ts b/sdk/package/lib/inits/setupInit.ts index 476dd8cd4..cf3af27ea 100644 --- a/sdk/package/lib/inits/setupInit.ts +++ b/sdk/package/lib/inits/setupInit.ts @@ -1,25 +1,21 @@ import { Actions } from "../../../base/lib/actions/setupActions" import { ExtendedVersion } from "../../../base/lib/exver" import { UpdateServiceInterfaces } from "../../../base/lib/interfaces/setupInterfaces" -import { ExposedStorePaths } from "../../../base/lib/types" import * as T from "../../../base/lib/types" -import { StorePath } from "../util" import { VersionGraph } from "../version/VersionGraph" import { PostInstall, PreInstall } from "./setupInstall" import { Uninstall } from "./setupUninstall" -export function setupInit( +export function setupInit( versions: VersionGraph, - preInstall: PreInstall, - postInstall: PostInstall, - uninstall: Uninstall, + preInstall: PreInstall, + postInstall: PostInstall, + uninstall: Uninstall, setServiceInterfaces: UpdateServiceInterfaces, setDependencies: (options: { effects: T.Effects }) => Promise, - actions: Actions, - initStore: Store, - exposedStore: ExposedStorePaths, + actions: Actions, ): { packageInit: T.ExpectedExports.packageInit packageUninit: T.ExpectedExports.packageUninit @@ -58,17 +54,12 @@ export function setupInit( containerInit: async (opts) => { const prev = await opts.effects.getDataVersion() if (!prev) { - await opts.effects.store.set({ - path: "" as StorePath, - value: initStore, - }) await preInstall.preInstall(opts) } await setServiceInterfaces({ ...opts, }) await actions.update({ effects: opts.effects }) - await opts.effects.exposeForDependents({ paths: exposedStore }) await setDependencies({ effects: opts.effects }) }, } diff --git a/sdk/package/lib/inits/setupInstall.ts b/sdk/package/lib/inits/setupInstall.ts index 884c61fec..9a4c7b42b 100644 --- a/sdk/package/lib/inits/setupInstall.ts +++ b/sdk/package/lib/inits/setupInstall.ts @@ -1,22 +1,19 @@ import * as T from "../../../base/lib/types" -export type InstallFn = (opts: { +export type InstallFn = (opts: { effects: T.Effects }) => Promise -export class Install { - protected constructor(readonly fn: InstallFn) {} +export class Install { + protected constructor(readonly fn: InstallFn) {} } -export class PreInstall extends Install< - Manifest, - Store -> { - private constructor(fn: InstallFn) { +export class PreInstall< + Manifest extends T.SDKManifest, +> extends Install { + private constructor(fn: InstallFn) { super(fn) } - static of( - fn: InstallFn, - ) { + static of(fn: InstallFn) { return new PreInstall(fn) } @@ -27,22 +24,19 @@ export class PreInstall extends Install< } } -export function setupPreInstall( - fn: InstallFn, +export function setupPreInstall( + fn: InstallFn, ) { return PreInstall.of(fn) } -export class PostInstall extends Install< - Manifest, - Store -> { - private constructor(fn: InstallFn) { +export class PostInstall< + Manifest extends T.SDKManifest, +> extends Install { + private constructor(fn: InstallFn) { super(fn) } - static of( - fn: InstallFn, - ) { + static of(fn: InstallFn) { return new PostInstall(fn) } @@ -53,8 +47,8 @@ export class PostInstall extends Install< } } -export function setupPostInstall( - fn: InstallFn, +export function setupPostInstall( + fn: InstallFn, ) { return PostInstall.of(fn) } diff --git a/sdk/package/lib/inits/setupUninstall.ts b/sdk/package/lib/inits/setupUninstall.ts index fc4a71b8e..bc298dd6f 100644 --- a/sdk/package/lib/inits/setupUninstall.ts +++ b/sdk/package/lib/inits/setupUninstall.ts @@ -1,13 +1,11 @@ import * as T from "../../../base/lib/types" -export type UninstallFn = (opts: { +export type UninstallFn = (opts: { effects: T.Effects }) => Promise -export class Uninstall { - private constructor(readonly fn: UninstallFn) {} - static of( - fn: UninstallFn, - ) { +export class Uninstall { + private constructor(readonly fn: UninstallFn) {} + static of(fn: UninstallFn) { return new Uninstall(fn) } @@ -22,8 +20,8 @@ export class Uninstall { } } -export function setupUninstall( - fn: UninstallFn, +export function setupUninstall( + fn: UninstallFn, ) { return Uninstall.of(fn) } diff --git a/sdk/package/lib/mainFn/Daemons.ts b/sdk/package/lib/mainFn/Daemons.ts index 2b16afb26..e12f8a9a6 100644 --- a/sdk/package/lib/mainFn/Daemons.ts +++ b/sdk/package/lib/mainFn/Daemons.ts @@ -56,6 +56,8 @@ type NewDaemonParams = { subcontainer: SubContainer runAsInit?: boolean env?: Record + cwd?: string + user?: string sigtermTimeout?: number onStdout?: (chunk: Buffer | string | any) => void onStderr?: (chunk: Buffer | string | any) => void diff --git a/sdk/package/lib/mainFn/Mounts.ts b/sdk/package/lib/mainFn/Mounts.ts index e9d4dcd60..c9bbf4afe 100644 --- a/sdk/package/lib/mainFn/Mounts.ts +++ b/sdk/package/lib/mainFn/Mounts.ts @@ -8,8 +8,12 @@ type SharedOptions = { subpath: string | null /** Where to mount the resource. e.g. /data */ mountpoint: string - /** Whether to mount this as a file or directory */ - type?: "file" | "directory" + /** + * Whether to mount this as a file or directory + * + * defaults to "directory" + * */ + type?: "file" | "directory" | "infer" } type VolumeOpts = { @@ -43,7 +47,7 @@ export class Mounts< return new Mounts([], [], [], []) } - addVolume(options: VolumeOpts) { + mountVolume(options: VolumeOpts) { return new Mounts( [...this.volumes, options], [...this.assets], @@ -52,7 +56,7 @@ export class Mounts< ) } - addAssets(options: SharedOptions) { + mountAssets(options: SharedOptions) { return new Mounts( [...this.volumes], [...this.assets, options], @@ -61,7 +65,7 @@ export class Mounts< ) } - addDependency( + mountDependency( options: DependencyOpts, ) { return new Mounts( @@ -72,7 +76,7 @@ export class Mounts< ) } - addBackups(options: SharedOptions) { + mountBackups(options: SharedOptions) { return new Mounts< Manifest, { @@ -109,7 +113,7 @@ export class Mounts< volumeId: v.volumeId, subpath: v.subpath, readonly: v.readonly, - filetype: v.type, + filetype: v.type ?? "directory", }, })), ) @@ -119,7 +123,7 @@ export class Mounts< options: { type: "assets", subpath: a.subpath, - filetype: a.type, + filetype: a.type ?? "directory", }, })), ) @@ -132,13 +136,13 @@ export class Mounts< volumeId: d.volumeId, subpath: d.subpath, readonly: d.readonly, - filetype: d.type, + filetype: d.type ?? "directory", }, })), ) } } -const a = Mounts.of().addBackups({ subpath: null, mountpoint: "" }) +const a = Mounts.of().mountBackups({ subpath: null, mountpoint: "" }) // @ts-expect-error const m: Mounts = a diff --git a/sdk/package/lib/mainFn/index.ts b/sdk/package/lib/mainFn/index.ts index be30c652d..e09f8532f 100644 --- a/sdk/package/lib/mainFn/index.ts +++ b/sdk/package/lib/mainFn/index.ts @@ -14,7 +14,7 @@ export const DEFAULT_SIGTERM_TIMEOUT = 30_000 * @param fn * @returns */ -export const setupMain = ( +export const setupMain = ( fn: (o: { effects: T.Effects started(onTerm: () => PromiseLike): PromiseLike diff --git a/sdk/package/lib/store/getStore.ts b/sdk/package/lib/store/getStore.ts deleted file mode 100644 index c2901ee7a..000000000 --- a/sdk/package/lib/store/getStore.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { Effects } from "../../../base/lib/Effects" -import { PathBuilder, extractJsonPath } from "../util" - -export class GetStore { - constructor( - readonly effects: Effects, - readonly path: PathBuilder, - readonly options: { - /** Defaults to what ever the package currently in */ - packageId?: string | undefined - } = {}, - ) {} - - /** - * Returns the value of Store at the provided path. Reruns the context from which it has been called if the underlying value changes - */ - const() { - return this.effects.store.get({ - ...this.options, - path: extractJsonPath(this.path), - callback: - this.effects.constRetry && - (() => this.effects.constRetry && this.effects.constRetry()), - }) - } - /** - * Returns the value of Store at the provided path. Does nothing if the value changes - */ - once() { - return this.effects.store.get({ - ...this.options, - path: extractJsonPath(this.path), - }) - } - - /** - * Watches the value of Store at the provided path. Returns an async iterator that yields whenever the value changes - */ - async *watch() { - const resolveCell = { resolve: () => {} } - this.effects.onLeaveContext(() => { - resolveCell.resolve() - }) - while (this.effects.isInContext) { - let callback: () => void = () => {} - const waitForNext = new Promise((resolve) => { - callback = resolve - resolveCell.resolve = resolve - }) - yield await this.effects.store.get({ - ...this.options, - path: extractJsonPath(this.path), - callback: () => callback(), - }) - await waitForNext - } - } - - /** - * Watches the value of Store at the provided path. Takes a custom callback function to run whenever the value changes - */ - onChange( - callback: (value: StoreValue | null, error?: Error) => void | Promise, - ) { - ;(async () => { - for await (const value of this.watch()) { - try { - await callback(value) - } catch (e) { - console.error( - "callback function threw an error @ GetStore.onChange", - e, - ) - } - } - })() - .catch((e) => callback(null, e)) - .catch((e) => - console.error( - "callback function threw an error @ GetStore.onChange", - e, - ), - ) - } -} -export function getStore( - effects: Effects, - path: PathBuilder, - options: { - /** Defaults to what ever the package currently in */ - packageId?: string | undefined - } = {}, -) { - return new GetStore(effects, path, options) -} diff --git a/sdk/package/lib/store/setupExposeStore.ts b/sdk/package/lib/store/setupExposeStore.ts deleted file mode 100644 index 7f5415bd7..000000000 --- a/sdk/package/lib/store/setupExposeStore.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ExposedStorePaths } from "../../../base/lib/types" -import { - PathBuilder, - extractJsonPath, - pathBuilder, -} from "../../../base/lib/util/PathBuilder" - -/** - * @description Use this function to determine which Store values to expose and make available to other services running on StartOS. Store values not exposed here will be kept private. Use the type safe pathBuilder to traverse your Store's structure. - * @example - * In this example, we expose the hypothetical Store values "adminPassword" and "nameLastUpdatedAt". - * - * ``` - export const exposedStore = setupExposeStore((pathBuilder) => [ - pathBuilder.adminPassword - pathBuilder.nameLastUpdatedAt, - ]) - * ``` - */ -export const setupExposeStore = >( - fn: (pathBuilder: PathBuilder) => PathBuilder[], -) => { - return fn(pathBuilder()).map( - (x) => extractJsonPath(x) as string, - ) as ExposedStorePaths -} -export { ExposedStorePaths } diff --git a/sdk/package/lib/test/inputSpecBuilder.test.ts b/sdk/package/lib/test/inputSpecBuilder.test.ts index 4179a9f04..0f1a8510f 100644 --- a/sdk/package/lib/test/inputSpecBuilder.test.ts +++ b/sdk/package/lib/test/inputSpecBuilder.test.ts @@ -421,25 +421,16 @@ describe("values", () => { }, }), ) - .withStore<{ test: "a" }>() .build(true) - const value = Value.dynamicDatetime<{ test: "a" }>( - async ({ effects }) => { - ;async () => { - ;(await sdk.store - .getOwn(effects, sdk.StorePath.test) - .once()) satisfies "a" - } - - return { - name: "Testing", - required: true, - default: null, - inputmode: "date", - } - }, - ) + const value = Value.dynamicDatetime(async ({ effects }) => { + return { + name: "Testing", + required: true, + default: null, + inputmode: "date", + } + }) const validator = value.validator validator.unsafeCast("2021-01-01") validator.unsafeCast(null) diff --git a/sdk/package/lib/test/output.sdk.ts b/sdk/package/lib/test/output.sdk.ts index 3f3bb5411..f29316ec9 100644 --- a/sdk/package/lib/test/output.sdk.ts +++ b/sdk/package/lib/test/output.sdk.ts @@ -1,4 +1,3 @@ -import { CurrentDependenciesResult } from "../../../base/lib/dependencies/setupDependencies" import { StartSdk } from "../StartSdk" import { setupManifest } from "../manifest/setupManifest" import { VersionGraph } from "../version/VersionGraph" @@ -49,5 +48,4 @@ export const sdk = StartSdk.of() }, }), ) - .withStore<{ storeRoot: { storeLeaf: "value" } }>() .build(true) diff --git a/sdk/package/lib/test/store.test.ts b/sdk/package/lib/test/store.test.ts deleted file mode 100644 index f876e76d8..000000000 --- a/sdk/package/lib/test/store.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { Effects } from "../../../base/lib/types" -import { extractJsonPath } from "../../../base/lib/util/PathBuilder" -import { StartSdk } from "../StartSdk" - -type Store = { - inputSpec: { - someValue: "a" | "b" - } -} -type Manifest = any -const todo = (): A => { - throw new Error("not implemented") -} -const noop = () => {} - -const sdk = StartSdk.of() - .withManifest({} as Manifest) - .withStore() - .build(true) - -const storePath = sdk.StorePath - -describe("Store", () => { - test("types", async () => { - ;async () => { - sdk.store.setOwn(todo(), storePath.inputSpec, { - someValue: "a", - }) - sdk.store.setOwn(todo(), storePath.inputSpec.someValue, "b") - sdk.store.setOwn(todo(), storePath, { - inputSpec: { someValue: "b" }, - }) - sdk.store.setOwn( - todo(), - storePath.inputSpec.someValue, - - // @ts-expect-error Type is wrong for the setting value - 5, - ) - sdk.store.setOwn( - todo(), - // @ts-expect-error Path is wrong - "/inputSpec/someVae3lue", - "someValue", - ) - - todo().store.set({ - path: extractJsonPath(storePath.inputSpec.someValue), - value: "b", - }) - todo().store.set({ - path: extractJsonPath(storePath.inputSpec.someValue), - //@ts-expect-error Path is wrong - value: "someValueIn", - }) - ;(await sdk.store - .getOwn(todo(), storePath.inputSpec.someValue) - .const()) satisfies string - ;(await sdk.store - .getOwn(todo(), storePath.inputSpec) - .const()) satisfies Store["inputSpec"] - await sdk.store // @ts-expect-error Path is wrong - .getOwn(todo(), "/inputSpec/somdsfeValue") - .const() - /// ----------------- ERRORS ----------------- - - sdk.store.setOwn(todo(), storePath, { - // @ts-expect-error Type is wrong for the setting value - inputSpec: { someValue: "notInAOrB" }, - }) - sdk.store.setOwn( - todo(), - sdk.StorePath.inputSpec.someValue, - // @ts-expect-error Type is wrong for the setting value - "notInAOrB", - ) - ;(await sdk.store - .getOwn(todo(), storePath.inputSpec.someValue) - .const()) satisfies string - ;(await sdk.store - .getOwn(todo(), storePath.inputSpec) - .const()) satisfies Store["inputSpec"] - await sdk.store // @ts-expect-error Path is wrong - .getOwn("/inputSpec/somdsfeValue") - .const() - - /// - ;(await sdk.store - .getOwn(todo(), storePath.inputSpec.someValue) - // @ts-expect-error satisfies type is wrong - .const()) satisfies number - await sdk.store // @ts-expect-error Path is wrong - .getOwn(todo(), extractJsonPath(storePath.inputSpec)) - .const() - ;(await todo().store.get({ - path: extractJsonPath(storePath.inputSpec.someValue), - callback: noop, - })) satisfies string - await todo().store.get({ - // @ts-expect-error Path is wrong as in it doesn't match above - path: "/inputSpec/someV2alue", - callback: noop, - }) - await todo().store.get({ - // @ts-expect-error Path is wrong as in it doesn't exists in wrapper type - path: "/inputSpec/someV2alue", - callback: noop, - }) - } - }) -}) diff --git a/sdk/package/lib/util/SubContainer.ts b/sdk/package/lib/util/SubContainer.ts index 08a13c453..277db5ae1 100644 --- a/sdk/package/lib/util/SubContainer.ts +++ b/sdk/package/lib/util/SubContainer.ts @@ -27,12 +27,12 @@ const TIMES_TO_WAIT_FOR_PROC = 100 async function prepBind( from: string | null, to: string, - type?: "file" | "directory", + type: "file" | "directory" | "infer", ) { const fromMeta = from ? await fs.stat(from).catch((_) => null) : null const toMeta = await fs.stat(to).catch((_) => null) - if (type === "file" || (!type && from && fromMeta?.isFile())) { + if (type === "file" || (type === "infer" && from && fromMeta?.isFile())) { if (toMeta && toMeta.isDirectory()) await fs.rmdir(to, { recursive: false }) if (from && !fromMeta) { await fs.mkdir(from.replace(/\/[^\/]*\/?$/, ""), { recursive: true }) @@ -49,7 +49,11 @@ async function prepBind( } } -async function bind(from: string, to: string, type?: "file" | "directory") { +async function bind( + from: string, + to: string, + type: "file" | "directory" | "infer", +) { await prepBind(from, to, type) await execFile("mount", ["--bind", from, to]) @@ -589,13 +593,13 @@ export type MountOptionsVolume = { volumeId: string subpath: string | null readonly: boolean - filetype?: "file" | "directory" + filetype: "file" | "directory" | "infer" } export type MountOptionsAssets = { type: "assets" subpath: string | null - filetype?: "file" | "directory" + filetype: "file" | "directory" | "infer" } export type MountOptionsPointer = { @@ -604,13 +608,13 @@ export type MountOptionsPointer = { volumeId: string subpath: string | null readonly: boolean - filetype?: "file" | "directory" + filetype: "file" | "directory" | "infer" } export type MountOptionsBackup = { type: "backup" subpath: string | null - filetype?: "file" | "directory" + filetype: "file" | "directory" | "infer" } function wait(time: number) { return new Promise((resolve) => setTimeout(resolve, time)) diff --git a/sdk/package/lib/util/fileHelper.ts b/sdk/package/lib/util/fileHelper.ts index 9ba00777d..01de88c0b 100644 --- a/sdk/package/lib/util/fileHelper.ts +++ b/sdk/package/lib/util/fileHelper.ts @@ -48,6 +48,8 @@ function fileMerge(...args: any[]): any { for (const arg of args) { if (res === arg) continue else if ( + res && + arg && typeof res === "object" && typeof arg === "object" && !Array.isArray(res) && @@ -81,8 +83,25 @@ export type Transformers = { onWrite: (value: Transformed) => Raw } +type ToPath = string | { volumeId: T.VolumeId; subpath: string } +function toPath(path: ToPath): string { + return typeof path === "string" + ? path + : `/media/startos/volumes/${path.volumeId}/${path.subpath}` +} + type Validator = matches.Validator | matches.Validator +type ReadType = { + once: () => Promise + const: (effects: T.Effects) => Promise + watch: (effects: T.Effects) => AsyncGenerator + onChange: ( + effects: T.Effects, + callback: (value: A | null, error?: Error) => void | Promise, + ) => void +} + /** * @description Use this class to read/write an underlying configuration file belonging to the upstream service. * @@ -174,8 +193,12 @@ export class FileHelper { return this.validate(data) } - private async readConst(effects: T.Effects): Promise { - const watch = this.readWatch(effects) + private async readConst( + effects: T.Effects, + map: (value: A) => B, + eq: (left: B | null | undefined, right: B | null) => boolean, + ): Promise { + const watch = this.readWatch(effects, map, eq) const res = await watch.next() if (effects.constRetry) { if (!this.consts.includes(effects.constRetry)) @@ -188,7 +211,11 @@ export class FileHelper { return res.value } - private async *readWatch(effects: T.Effects) { + private async *readWatch( + effects: T.Effects, + map: (value: A) => B, + eq: (left: B | null | undefined, right: B | null) => boolean, + ) { let res while (effects.isInContext) { if (await exists(this.path)) { @@ -197,7 +224,8 @@ export class FileHelper { persistent: false, signal: ctrl.signal, }) - res = await this.readOnce() + const newResFull = await this.readOnce() + const newRes = newResFull ? map(newResFull) : null const listen = Promise.resolve() .then(async () => { for await (const _ of watch) { @@ -206,7 +234,8 @@ export class FileHelper { } }) .catch((e) => console.error(asError(e))) - yield res + if (!eq(res, newRes)) yield newRes + res = newRes await listen } else { yield null @@ -216,12 +245,14 @@ export class FileHelper { return null } - private readOnChange( + private readOnChange( effects: T.Effects, - callback: (value: A | null, error?: Error) => void | Promise, + callback: (value: B | null, error?: Error) => void | Promise, + map: (value: A) => B, + eq: (left: B | null | undefined, right: B | null) => boolean, ) { ;(async () => { - for await (const value of this.readWatch(effects)) { + for await (const value of this.readWatch(effects, map, eq)) { try { await callback(value) } catch (e) { @@ -241,15 +272,25 @@ export class FileHelper { ) } - get read() { + read(): ReadType + read( + map: (value: A) => B, + eq?: (left: B | null | undefined, right: B | null) => boolean, + ): ReadType + read( + map?: (value: A) => any, + eq?: (left: any, right: any) => boolean, + ): ReadType { + map = map ?? ((a: A) => a) + eq = eq ?? ((left: any, right: any) => !partialDiff(left, right)) return { once: () => this.readOnce(), - const: (effects: T.Effects) => this.readConst(effects), - watch: (effects: T.Effects) => this.readWatch(effects), + const: (effects: T.Effects) => this.readConst(effects, map, eq), + watch: (effects: T.Effects) => this.readWatch(effects, map, eq), onChange: ( effects: T.Effects, callback: (value: A | null, error?: Error) => void | Promise, - ) => this.readOnChange(effects, callback), + ) => this.readOnChange(effects, callback, map, eq), } } @@ -291,8 +332,13 @@ export class FileHelper { * We wanted to be able to have a fileHelper, and just modify the path later in time. * Like one behavior of another dependency or something similar. */ - withPath(path: string) { - return new FileHelper(path, this.writeData, this.readData, this.validate) + withPath(path: ToPath) { + return new FileHelper( + toPath(path), + this.writeData, + this.readData, + this.validate, + ) } /** @@ -301,22 +347,22 @@ export class FileHelper { * Provide custom functions for translating data to/from the file format. */ static raw( - path: string, + path: ToPath, toFile: (dataIn: A) => string, fromFile: (rawData: string) => unknown, validate: (data: unknown) => A, ) { - return new FileHelper(path, toFile, fromFile, validate) + return new FileHelper(toPath(path), toFile, fromFile, validate) } private static rawTransformed( - path: string, + path: ToPath, toFile: (dataIn: Raw) => string, fromFile: (rawData: string) => Raw, validate: (data: Transformed) => A, transformers: Transformers | undefined, ) { - return new FileHelper( + return FileHelper.raw( path, (inData) => { if (transformers) { @@ -332,18 +378,18 @@ export class FileHelper { /** * Create a File Helper for a text file */ - static string(path: string): FileHelper + static string(path: ToPath): FileHelper static string( - path: string, + path: ToPath, shape: Validator, ): FileHelper static string( - path: string, + path: ToPath, shape: Validator, transformers: Transformers, ): FileHelper static string( - path: string, + path: ToPath, shape?: Validator, transformers?: Transformers, ) { @@ -363,7 +409,7 @@ export class FileHelper { * Create a File Helper for a .json file. */ static json( - path: string, + path: ToPath, shape: Validator, transformers?: Transformers, ) { @@ -380,16 +426,16 @@ export class FileHelper { * Create a File Helper for a .yaml file */ static yaml>( - path: string, + path: ToPath, shape: Validator, A>, ): FileHelper static yaml>( - path: string, + path: ToPath, shape: Validator, transformers: Transformers, Transformed>, ): FileHelper static yaml>( - path: string, + path: ToPath, shape: Validator, transformers?: Transformers, Transformed>, ) { @@ -406,16 +452,16 @@ export class FileHelper { * Create a File Helper for a .toml file */ static toml( - path: string, + path: ToPath, shape: Validator, ): FileHelper static toml( - path: string, + path: ToPath, shape: Validator, transformers: Transformers, ): FileHelper static toml( - path: string, + path: ToPath, shape: Validator, transformers?: Transformers, ) { @@ -429,18 +475,18 @@ export class FileHelper { } static ini>( - path: string, + path: ToPath, shape: Validator, A>, options?: INI.EncodeOptions & INI.DecodeOptions, ): FileHelper static ini>( - path: string, + path: ToPath, shape: Validator, options: INI.EncodeOptions & INI.DecodeOptions, transformers: Transformers, Transformed>, ): FileHelper static ini>( - path: string, + path: ToPath, shape: Validator, options?: INI.EncodeOptions & INI.DecodeOptions, transformers?: Transformers, Transformed>, @@ -455,16 +501,16 @@ export class FileHelper { } static env>( - path: string, + path: ToPath, shape: Validator, A>, ): FileHelper static env>( - path: string, + path: ToPath, shape: Validator, transformers: Transformers, Transformed>, ): FileHelper static env>( - path: string, + path: ToPath, shape: Validator, transformers?: Transformers, Transformed>, ) { diff --git a/sdk/package/package-lock.json b/sdk/package/package-lock.json index b014885ce..dc4846df8 100644 --- a/sdk/package/package-lock.json +++ b/sdk/package/package-lock.json @@ -1,12 +1,12 @@ { "name": "@start9labs/start-sdk", - "version": "0.4.0-beta.18", + "version": "0.4.0-beta.20", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@start9labs/start-sdk", - "version": "0.4.0-beta.18", + "version": "0.4.0-beta.20", "license": "MIT", "dependencies": { "@iarna/toml": "^3.0.0", diff --git a/sdk/package/package.json b/sdk/package/package.json index db2528f03..3e7e9734e 100644 --- a/sdk/package/package.json +++ b/sdk/package/package.json @@ -1,6 +1,6 @@ { "name": "@start9labs/start-sdk", - "version": "0.4.0-beta.18", + "version": "0.4.0-beta.20", "description": "Software development kit to facilitate packaging services for StartOS", "main": "./package/lib/index.js", "types": "./package/lib/index.d.ts", diff --git a/web/projects/ui/src/app/utils/configBuilderToSpec.ts b/web/projects/ui/src/app/utils/configBuilderToSpec.ts index 108bf9468..19ffa1d5e 100644 --- a/web/projects/ui/src/app/utils/configBuilderToSpec.ts +++ b/web/projects/ui/src/app/utils/configBuilderToSpec.ts @@ -1,9 +1,7 @@ import { ISB } from '@start9labs/start-sdk' export async function configBuilderToSpec( - builder: - | ISB.InputSpec, unknown> - | ISB.InputSpec, never>, + builder: ISB.InputSpec>, ) { return builder.build({} as any) }