diff --git a/core/startos/src/service/service_effect_handler.rs b/core/startos/src/service/service_effect_handler.rs index 65217ecd5..0f5fd889e 100644 --- a/core/startos/src/service/service_effect_handler.rs +++ b/core/startos/src/service/service_effect_handler.rs @@ -257,7 +257,7 @@ struct RemoveAddressParams { #[ts(export)] #[serde(rename_all = "kebab-case")] enum AllowedStatuses { - OnlyRunning, + OnlyRunning, // onlyRunning OnlyStopped, Any, Disabled, @@ -1073,20 +1073,19 @@ enum DependencyKind { #[serde(rename_all = "camelCase", tag = "kind")] #[ts(export)] enum DependencyRequirement { + #[serde(rename_all = "camelCase")] Running { #[ts(type = "string")] id: PackageId, #[ts(type = "string[]")] - #[serde(rename = "healthChecks")] health_checks: BTreeSet, - #[serde(rename = "versionSpec")] version_spec: String, url: String, }, + #[serde(rename_all = "camelCase")] Exists { #[ts(type = "string")] id: PackageId, - #[serde(rename = "versionSpec")] version_spec: String, url: String, }, diff --git a/sdk/lib/StartSdk.ts b/sdk/lib/StartSdk.ts index 77c791f55..f78aad2f6 100644 --- a/sdk/lib/StartSdk.ts +++ b/sdk/lib/StartSdk.ts @@ -21,9 +21,6 @@ import { MaybePromise, ServiceInterfaceId, PackageId, - EnsureStorePath, - ExtractStore, - DaemonReturned, ValidIfNoStupidEscape, } from "./types" import * as patterns from "./util/patterns" @@ -61,7 +58,6 @@ import { setupInterfaces, } from "./interfaces/setupInterfaces" import { successFailure } from "./trigger/successFailure" -import { SetupExports } from "./inits/setupExports" import { HealthReceipt } from "./health/HealthReceipt" import { MultiHost, Scheme, SingleHost, StaticHost } from "./interfaces/Host" import { ServiceInterfaceBuilder } from "./interfaces/ServiceInterfaceBuilder" @@ -79,6 +75,8 @@ import { Mounts } from "./mainFn/Mounts" import { Dependency } from "./Dependency" import * as T from "./types" import { Checker, EmVer } from "./emverLite/mod" +import { ExposedStorePaths } from "./store/setupExposeStore" +import { PathBuilder, extractJsonPath, pathBuilder } from "./store/PathBuilder" // prettier-ignore type AnyNeverCond = @@ -151,25 +149,29 @@ export class StartSdk { }, store: { - get: ( + get: ( effects: E, packageId: string, - path: EnsureStorePath, + path: PathBuilder, ) => removeConstType()( - getStore(effects, path as any, { + getStore(effects, path, { packageId, }), ), - getOwn: ( + getOwn: ( effects: E, - path: EnsureStorePath, - ) => removeConstType()(getStore(effects, path as any)), - setOwn: ( + path: PathBuilder, + ) => removeConstType()(getStore(effects, path)), + setOwn: >( effects: E, - path: EnsureStorePath, - value: ExtractStore, - ) => effects.store.set({ value, path: path as any }), + path: Path, + value: Path extends PathBuilder ? Value : never, + ) => + effects.store.set({ + value, + path: extractJsonPath(path), + }), }, host: { @@ -180,24 +182,17 @@ export class StartSdk { multi: (effects: Effects, id: string) => new MultiHost({ id, effects }), }, nullIfEmpty, - - configConstants: { smtpConfig }, - createInterface: ( + runCommand: async ( effects: Effects, - options: { - name: string - id: string - description: string - hasPrimary: boolean - disabled: boolean - type: ServiceInterfaceType - username: null | string - path: string - search: Record - schemeOverride: { ssl: Scheme; noSsl: Scheme } | null - masked: boolean + imageId: Manifest["images"][number], + command: ValidIfNoStupidEscape | [string, ...string[]], + options: CommandOptions & { + mounts?: { path: string; options: MountOptions }[] }, - ) => new ServiceInterfaceBuilder({ ...options, effects }), + ): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> => { + return runCommand(effects, imageId, command, options) + }, + createAction: < ConfigType extends | Record @@ -216,18 +211,25 @@ export class StartSdk { const { input, ...rest } = metaData return createAction(rest, fn, input) }, + configConstants: { smtpConfig }, + createInterface: ( + effects: Effects, + options: { + name: string + id: string + description: string + hasPrimary: boolean + disabled: boolean + type: ServiceInterfaceType + username: null | string + path: string + search: Record + schemeOverride: { ssl: Scheme; noSsl: Scheme } | null + masked: boolean + }, + ) => new ServiceInterfaceBuilder({ ...options, effects }), getSystemSmtp: (effects: E) => removeConstType()(new GetSystemSmtp(effects)), - runCommand: async ( - effects: Effects, - imageId: Manifest["images"][number], - command: ValidIfNoStupidEscape | [string, ...string[]], - options: CommandOptions & { - mounts?: { path: string; options: MountOptions }[] - }, - ): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> => { - return runCommand(effects, imageId, command, options) - }, createDynamicAction: < ConfigType extends @@ -336,7 +338,6 @@ export class StartSdk { }) } }, - setupExports: (fn: SetupExports) => fn, setupInit: ( migrations: Migrations, install: Install, @@ -346,15 +347,15 @@ export class StartSdk { effects: Effects input: any }) => Promise, - setupExports: SetupExports, + exposedStore: ExposedStorePaths, ) => setupInit( migrations, install, uninstall, setInterfaces, - setupExports, setDependencies, + exposedStore, ), setupInstall: (fn: InstallFn) => Install.of(fn), setupInterfaces: < @@ -379,6 +380,12 @@ export class StartSdk { this.manifest, ...migrations, ), + setupProperties: + ( + fn: (options: { effects: Effects }) => Promise, + ): T.ExpectedExports.Properties => + (options) => + fn(options).then(nullifyProperties), setupUninstall: (fn: UninstallFn) => setupUninstall(fn), trigger: { @@ -531,6 +538,7 @@ export class StartSdk { down: (opts: { effects: Effects }) => Promise }) => Migration.of(options), }, + StorePath: pathBuilder(), Value: { toggle: Value.toggle, text: Value.text, @@ -739,3 +747,20 @@ export async function runCommand( await overlay.destroy() } } +function nullifyProperties(value: T.SdkPropertiesReturn): T.PropertiesReturn { + return Object.fromEntries( + Object.entries(value).map(([k, v]) => [k, nullifyProperties_(v)]), + ) +} +function nullifyProperties_(value: T.SdkPropertiesValue): T.PropertiesValue { + if (value.type === "string") { + return { description: null, copyable: null, qr: null, ...value } + } + return { + description: null, + ...value, + value: Object.fromEntries( + Object.entries(value.value).map(([k, v]) => [k, nullifyProperties_(v)]), + ), + } +} diff --git a/sdk/lib/index.ts b/sdk/lib/index.ts index 35c58411f..aeeddb6f1 100644 --- a/sdk/lib/index.ts +++ b/sdk/lib/index.ts @@ -1,9 +1,12 @@ +import matches from "ts-matches" + export { Daemons } from "./mainFn/Daemons" export { EmVer } from "./emverLite/mod" export { Overlay } from "./util/Overlay" export { StartSdk } from "./StartSdk" export { setupManifest } from "./manifest/setupManifest" export { FileHelper } from "./util/fileHelper" +export { setupExposeStore } from "./store/setupExposeStore" export * as actions from "./actions" export * as backup from "./backup" export * as config from "./config" diff --git a/sdk/lib/inits/setupExports.ts b/sdk/lib/inits/setupExports.ts deleted file mode 100644 index 089f8a41d..000000000 --- a/sdk/lib/inits/setupExports.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Effects, ExposeServicePaths, ExposeUiPaths } from "../types" - -export type SetupExports = (opts: { effects: Effects }) => - | { - ui: { [k: string]: ExposeUiPaths } - services: ExposeServicePaths["paths"] - } - | Promise<{ - ui: { [k: string]: ExposeUiPaths } - services: ExposeServicePaths["paths"] - }> - -export const setupExports = (fn: (opts: SetupExports) => void) => - fn diff --git a/sdk/lib/inits/setupInit.ts b/sdk/lib/inits/setupInit.ts index af085e17b..03a7085c5 100644 --- a/sdk/lib/inits/setupInit.ts +++ b/sdk/lib/inits/setupInit.ts @@ -1,14 +1,9 @@ import { DependenciesReceipt } from "../config/setupConfig" import { SetInterfaces } from "../interfaces/setupInterfaces" import { SDKManifest } from "../manifest/ManifestTypes" -import { - Effects, - ExpectedExports, - ExposeUiPaths, - ExposeUiPathsAll, -} from "../types" +import { ExposedStorePaths } from "../store/setupExposeStore" +import { Effects, ExpectedExports } from "../types" import { Migrations } from "./migrations/setupMigrations" -import { SetupExports } from "./setupExports" import { Install } from "./setupInstall" import { Uninstall } from "./setupUninstall" @@ -17,11 +12,11 @@ export function setupInit( install: Install, uninstall: Uninstall, setInterfaces: SetInterfaces, - setupExports: SetupExports, setDependencies: (options: { effects: Effects input: any }) => Promise, + exposedStore: ExposedStorePaths, ): { init: ExpectedExports.init uninit: ExpectedExports.uninit @@ -34,9 +29,7 @@ export function setupInit( ...opts, input: null, }) - const { services, ui } = await setupExports(opts) - await opts.effects.exposeForDependents({ paths: services }) - await opts.effects.exposeUi(forExpose(ui)) + await opts.effects.exposeForDependents({ paths: exposedStore }) await setDependencies({ effects: opts.effects, input: null }) }, uninit: async (opts) => { @@ -45,30 +38,3 @@ export function setupInit( }, } } -function forExpose(ui: { [key: string]: ExposeUiPaths }) { - return Object.fromEntries( - Object.entries(ui).map(([key, value]) => [key, forExpose_(value)]), - ) -} - -function forExpose_(ui: ExposeUiPaths): ExposeUiPathsAll { - if (ui.type === ("object" as const)) { - return { - type: "object" as const, - value: Object.fromEntries( - Object.entries(ui.value).map(([key, value]) => [ - key, - forExpose_(value), - ]), - ), - description: ui.description ?? null, - } - } - return { - description: null, - - copyable: null, - qr: null, - ...ui, - } -} diff --git a/sdk/lib/store/PathBuilder.ts b/sdk/lib/store/PathBuilder.ts new file mode 100644 index 000000000..038fa5ac2 --- /dev/null +++ b/sdk/lib/store/PathBuilder.ts @@ -0,0 +1,38 @@ +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/lib/store/getStore.ts b/sdk/lib/store/getStore.ts index 4ea3a9419..38265c7fd 100644 --- a/sdk/lib/store/getStore.ts +++ b/sdk/lib/store/getStore.ts @@ -1,9 +1,10 @@ -import { Effects, EnsureStorePath } from "../types" +import { Effects } from "../types" +import { PathBuilder, extractJsonPath } from "./PathBuilder" -export class GetStore { +export class GetStore { constructor( readonly effects: Effects, - readonly path: Path & EnsureStorePath, + readonly path: PathBuilder, readonly options: { /** Defaults to what ever the package currently in */ packageId?: string | undefined @@ -14,9 +15,9 @@ export class GetStore { * Returns the value of Store at the provided path. Restart the service if the value changes */ const() { - return this.effects.store.get({ + return this.effects.store.get({ ...this.options, - path: this.path as any, + path: extractJsonPath(this.path), callback: this.effects.restart, }) } @@ -24,9 +25,9 @@ export class GetStore { * Returns the value of Store at the provided path. Does nothing if the value changes */ once() { - return this.effects.store.get({ + return this.effects.store.get({ ...this.options, - path: this.path as any, + path: extractJsonPath(this.path), callback: () => {}, }) } @@ -40,22 +41,22 @@ export class GetStore { const waitForNext = new Promise((resolve) => { callback = resolve }) - yield await this.effects.store.get({ + yield await this.effects.store.get({ ...this.options, - path: this.path as any, + path: extractJsonPath(this.path), callback: () => callback(), }) await waitForNext } } } -export function getStore( +export function getStore( effects: Effects, - path: Path & EnsureStorePath, + path: PathBuilder, options: { /** Defaults to what ever the package currently in */ packageId?: string | undefined } = {}, ) { - return new GetStore(effects, path as any, options) + return new GetStore(effects, path, options) } diff --git a/sdk/lib/store/setupExposeStore.ts b/sdk/lib/store/setupExposeStore.ts new file mode 100644 index 000000000..9272a9a6b --- /dev/null +++ b/sdk/lib/store/setupExposeStore.ts @@ -0,0 +1,12 @@ +import { Affine, _ } from "../util" +import { PathBuilder, extractJsonPath, pathBuilder } from "./PathBuilder" + +export type ExposedStorePaths = string[] & Affine<"ExposedStorePaths"> + +export const setupExposeStore = >( + fn: (pathBuilder: PathBuilder) => PathBuilder[], +) => { + return fn(pathBuilder()).map( + (x) => extractJsonPath(x) as string, + ) as ExposedStorePaths +} diff --git a/sdk/lib/test/configBuilder.test.ts b/sdk/lib/test/configBuilder.test.ts index cd14d6a18..2df40b95c 100644 --- a/sdk/lib/test/configBuilder.test.ts +++ b/sdk/lib/test/configBuilder.test.ts @@ -425,7 +425,9 @@ describe("values", () => { const value = Value.dynamicDatetime<{ test: "a" }>( async ({ effects }) => { ;async () => { - ;(await sdk.store.getOwn(effects, "/test").once()) satisfies "a" + ;(await sdk.store + .getOwn(effects, sdk.StorePath.test) + .once()) satisfies "a" } return { diff --git a/sdk/lib/test/startosTypeValidation.test.ts b/sdk/lib/test/startosTypeValidation.test.ts index 0bd78defa..f2e86de74 100644 --- a/sdk/lib/test/startosTypeValidation.test.ts +++ b/sdk/lib/test/startosTypeValidation.test.ts @@ -49,7 +49,6 @@ describe("startosTypeValidation ", () => { setConfigured: {} as SetConfigured, setHealth: {} as SetHealth, exposeForDependents: {} as ExposeForDependentsParams, - exposeUi: {} as { [key: string]: ExposedUI }, getSslCertificate: {} as GetSslCertificateParams, getSslKey: {} as GetSslKeyParams, getServiceInterface: {} as GetServiceInterfaceParams, diff --git a/sdk/lib/test/store.test.ts b/sdk/lib/test/store.test.ts index c41f3c85a..fa7bc4a4c 100644 --- a/sdk/lib/test/store.test.ts +++ b/sdk/lib/test/store.test.ts @@ -1,4 +1,5 @@ import { MainEffects, StartSdk } from "../StartSdk" +import { extractJsonPath } from "../store/PathBuilder" import { Effects } from "../types" type Store = { @@ -17,19 +18,21 @@ const sdk = StartSdk.of() .withStore() .build(true) +const storePath = sdk.StorePath + describe("Store", () => { test("types", async () => { ;async () => { - sdk.store.setOwn(todo(), "/config", { + sdk.store.setOwn(todo(), storePath.config, { someValue: "a", }) - sdk.store.setOwn(todo(), "/config/someValue", "b") - sdk.store.setOwn(todo(), "", { + sdk.store.setOwn(todo(), storePath.config.someValue, "b") + sdk.store.setOwn(todo(), storePath, { config: { someValue: "b" }, }) sdk.store.setOwn( todo(), - "/config/someValue", + storePath.config.someValue, // @ts-expect-error Type is wrong for the setting value 5, @@ -41,48 +44,42 @@ describe("Store", () => { "someValue", ) - todo().store.set({ - path: "/config/someValue", + todo().store.set({ + path: extractJsonPath(storePath.config.someValue), value: "b", }) todo().store.set({ - //@ts-expect-error Path is wrong - path: "/config/someValue", + path: extractJsonPath(storePath.config.someValue), //@ts-expect-error Path is wrong value: "someValueIn", }) - todo().store.set({ - //@ts-expect-error Path is wrong - path: "/config/some2Value", - value: "a", - }) ;(await sdk.store - .getOwn(todo(), "/config/someValue") + .getOwn(todo(), storePath.config.someValue) .const()) satisfies string ;(await sdk.store - .getOwn(todo(), "/config") + .getOwn(todo(), storePath.config) .const()) satisfies Store["config"] await sdk.store // @ts-expect-error Path is wrong .getOwn(todo(), "/config/somdsfeValue") .const() /// ----------------- ERRORS ----------------- - sdk.store.setOwn(todo(), "", { + sdk.store.setOwn(todo(), storePath, { // @ts-expect-error Type is wrong for the setting value config: { someValue: "notInAOrB" }, }) sdk.store.setOwn( todo(), - "/config/someValue", + sdk.StorePath.config.someValue, // @ts-expect-error Type is wrong for the setting value "notInAOrB", ) ;(await sdk.store - .getOwn(todo(), "/config/someValue") + .getOwn(todo(), storePath.config.someValue) // @ts-expect-error Const should normally not be callable .const()) satisfies string ;(await sdk.store - .getOwn(todo(), "/config") + .getOwn(todo(), storePath.config) // @ts-expect-error Const should normally not be callable .const()) satisfies Store["config"] await sdk.store // @ts-expect-error Path is wrong @@ -92,14 +89,14 @@ describe("Store", () => { /// ;(await sdk.store - .getOwn(todo(), "/config/someValue") + .getOwn(todo(), storePath.config.someValue) // @ts-expect-error satisfies type is wrong .const()) satisfies number - ;(await sdk.store // @ts-expect-error Path is wrong - .getOwn(todo(), "/config/") - .const()) satisfies Store["config"] - ;(await todo().store.get({ - path: "/config/someValue", + await sdk.store // @ts-expect-error Path is wrong + .getOwn(todo(), extractJsonPath(storePath.config)) + .const() + ;(await todo().store.get({ + path: extractJsonPath(storePath.config.someValue), callback: noop, })) satisfies string await todo().store.get({ diff --git a/sdk/lib/types.ts b/sdk/lib/types.ts index c6ab16e64..f8dfd4874 100644 --- a/sdk/lib/types.ts +++ b/sdk/lib/types.ts @@ -5,6 +5,8 @@ import { InputSpec } from "./config/configTypes" import { DependenciesReceipt } from "./config/setupConfig" import { BindOptions, Scheme } from "./interfaces/Host" import { Daemons } from "./mainFn/Daemons" +import { PathBuilder, StorePath } from "./store/PathBuilder" +import { ExposedStorePaths } from "./store/setupExposeStore" import { UrlString } from "./util/getServiceInterface" export { SDKManifest } from "./manifest/ManifestTypes" @@ -93,6 +95,10 @@ export namespace ExpectedExports { * that this service could use. */ export type dependencyConfig = Record + + export type Properties = (options: { + effects: Effects + }) => Promise } export type TimeMs = number export type VersionString = string @@ -248,31 +254,21 @@ export type ServiceInterfaceWithHostInfo = ServiceInterface & { hostInfo: HostInfo } -// prettier-ignore -export type ExposeAllServicePaths = - Store extends never ? string : - Store extends Record ? {[K in keyof Store & string]: ExposeAllServicePaths}[keyof Store & string] : - PreviousPath -// prettier-ignore -export type ExposeAllUiPaths = - Store extends Record ? {[K in keyof Store & string]: ExposeAllUiPaths}[keyof Store & string] : - Store extends string ? PreviousPath : - never export type ExposeServicePaths = { /** The path to the value in the Store. [JsonPath](https://jsonpath.com/) */ - paths: Store extends never ? string[] : ExposeAllServicePaths[] + paths: ExposedStorePaths } -export type ExposeUiPaths = +export type SdkPropertiesValue = | { type: "object" - value: { [k: string]: ExposeUiPaths } + value: { [k: string]: SdkPropertiesValue } description?: string } | { type: "string" - /** The path to the value in the Store. [JsonPath](https://jsonpath.com/) */ - path: ExposeAllUiPaths + /** Value */ + value: string /** A human readable description or explanation of the value */ description?: string /** (string/number only) Whether or not to mask the value, for example, when displaying a password */ @@ -282,16 +278,21 @@ export type ExposeUiPaths = /** (string/number only) Whether or not to include a button for displaying the value as a QR code */ qr?: boolean } -export type ExposeUiPathsAll = + +export type SdkPropertiesReturn = { + [key: string]: SdkPropertiesValue +} + +export type PropertiesValue = | { type: "object" - value: { [k: string]: ExposeUiPathsAll } + value: { [k: string]: PropertiesValue } description: string | null } | { type: "string" - /** The path to the value in the Store. [JsonPath](https://jsonpath.com/) */ - path: string + /** Value */ + value: string /** A human readable description or explanation of the value */ description: string | null /** (string/number only) Whether or not to mask the value, for example, when displaying a password */ @@ -302,6 +303,10 @@ export type ExposeUiPathsAll = qr: boolean | null } +export type PropertiesReturn = { + [key: string]: PropertiesValue +} + /** Used to reach out from the pure js runtime */ export type Effects = { executeAction(opts: { @@ -361,18 +366,18 @@ export type Effects = { store: { /** Get a value in a json like data, can be observed and subscribed */ - get(options: { + 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: Path & EnsureStorePath + path: StorePath callback: (config: unknown, previousConfig: unknown) => void - }): Promise> + }): Promise /** Used to store values that can be accessed and subscribed to */ - set(options: { + set(options: { /** Sets the value for the wrapper at the path, it will override, using the [JsonPath](https://jsonpath.com/) */ - path: Path & EnsureStorePath - value: ExtractStore + path: StorePath + value: ExtractStore }): Promise } @@ -399,7 +404,6 @@ export type Effects = { exposeForDependents(options: { paths: string[] }): Promise - exposeUi(options: { [key: string]: ExposeUiPathsAll }): Promise /** * There are times that we want to see the addresses that where exported * @param options.addressId If we want to filter the address id @@ -524,22 +528,6 @@ export type Effects = { stopped(options: { packageId: string | null }): Promise } -// prettier-ignore -export type ExtractStore = - Path extends `/${infer A }/${infer Rest }` ? (A extends keyof Store ? ExtractStore : never) : - Path extends `/${infer A }` ? (A extends keyof Store ? Store[A] : never) : - Path extends '' ? Store : - never - -// prettier-ignore -type _EnsureStorePath = - Path extends`/${infer A }/${infer Rest}` ? (Store extends {[K in A & string]: infer NextStore} ? _EnsureStorePath : never) : - Path extends `/${infer A }` ? (Store extends {[K in A]: infer B} ? Origin : never) : - Path extends '' ? Origin : - never -// prettier-ignore -export type EnsureStorePath = _EnsureStorePath - /** rsync options: https://linux.die.net/man/1/rsync */ export type BackupOptions = { diff --git a/sdk/lib/util/index.ts b/sdk/lib/util/index.ts index aa69b9dc7..bd144f35a 100644 --- a/sdk/lib/util/index.ts +++ b/sdk/lib/util/index.ts @@ -23,6 +23,8 @@ export const isKnownError = (e: unknown): e is T.KnownError => declare const affine: unique symbol +export type Affine = { [affine]: A } + type NeverPossible = { [affine]: string } export type NoAny = NeverPossible extends A ? keyof NeverPossible extends keyof A