diff --git a/README.md b/README.md index 984bdd0..d51b25b 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ ```ts { - startSdk: "start-sdk/lib", + StartSdk: "start-sdk/lib", } ``` diff --git a/lib/StartSdk.ts b/lib/StartSdk.ts new file mode 100644 index 0000000..b0365d3 --- /dev/null +++ b/lib/StartSdk.ts @@ -0,0 +1,487 @@ +import { ManifestVersion, SDKManifest } from "./manifest/ManifestTypes" +import { RequiredDefault, Value } from "./config/builder/value" +import { Config, ExtractConfigType, LazyBuild } from "./config/builder/config" +import { + DefaultString, + ListValueSpecText, + Pattern, + RandomString, + UniqueBy, + ValueSpecDatetime, + ValueSpecText, +} from "./config/configTypes" +import { Variants } from "./config/builder/variants" +import { CreatedAction, createAction } from "./actions/createAction" +import { + ActionMetadata, + Effects, + ActionResult, + Metadata, + BackupOptions, + DeepPartial, +} from "./types" +import { Utils } from "./util/utils" +import { DependencyConfig } from "./dependencyConfig/DependencyConfig" +import { BackupSet, Backups } from "./backup/Backups" +import { smtpConfig } from "./config/configConstants" +import { Daemons } from "./mainFn/Daemons" +import { healthCheck } from "./health/HealthCheck" +import { + checkPortListening, + containsAddress, +} from "./health/checkFns/checkPortListening" +import { checkWebUrl, runHealthScript } from "./health/checkFns" +import { List } from "./config/builder/list" +import { Migration } from "./inits/migrations/Migration" +import { Install, InstallFn, setupInstall } from "./inits/setupInstall" +import { setupActions } from "./actions/setupActions" +import { setupDependencyConfig } from "./dependencyConfig/setupDependencyConfig" +import { SetupBackupsParams, setupBackups } from "./backup/setupBackups" +import { setupInit } from "./inits/setupInit" +import { + EnsureUniqueId, + Migrations, + setupMigrations, +} from "./inits/migrations/setupMigrations" +import { Uninstall, UninstallFn, setupUninstall } from "./inits/setupUninstall" +import { setupMain } from "./mainFn" +import { defaultTrigger } from "./trigger/defaultTrigger" +import { changeOnFirstSuccess, cooldownTrigger } from "./trigger" +import setupConfig, { Read, Save } from "./config/setupConfig" +import { setupDependencyMounts } from "./dependency/setupDependencyMounts" + +// prettier-ignore +type AnyNeverCond = + T extends [] ? Else : + T extends [never, ...Array] ? Then : + T extends [any, ...infer U] ? AnyNeverCond : + never + +export class StartSdk { + private constructor(readonly manifest: Manifest) {} + static of() { + return new StartSdk(null as never) + } + withManifest(manifest: Manifest) { + return new StartSdk(manifest) + } + withStore>() { + return new StartSdk(this.manifest) + } + withVault>() { + return new StartSdk(this.manifest) + } + + build( + isReady: AnyNeverCond<[Manifest, Store, Vault], "Build not ready", true>, + ) { + return { + configConstants: { smtpConfig }, + createAction: < + Store, + ConfigType extends + | Record + | Config + | Config, + Type extends Record = ExtractConfigType, + >( + metaData: Omit & { + input: Config | Config + }, + fn: (options: { + effects: Effects + utils: Utils + input: Type + }) => Promise, + ) => createAction(metaData, fn), + healthCheck: { + checkPortListening, + checkWebUrl, + of: healthCheck, + runHealthScript, + }, + setupActions: (...createdActions: CreatedAction[]) => + setupActions(...createdActions), + setupBackups: (...args: SetupBackupsParams) => + setupBackups(...args), + setupConfig: < + ConfigType extends + | Config + | Config, + Type extends Record = ExtractConfigType, + >( + spec: ConfigType, + write: Save, + read: Read, + ) => + setupConfig( + spec, + write, + read, + ), + setupConfigRead: < + ConfigSpec extends + | Config, any, any> + | Config, never, never>, + >( + _configSpec: ConfigSpec, + fn: Read, + ) => fn, + setupConfigSave: < + ConfigSpec extends + | Config, any, any> + | Config, never, never>, + >( + _configSpec: ConfigSpec, + fn: Save, + ) => fn, + setupDependencyConfig: >( + config: Config, + autoConfigs: { + [K in keyof Manifest["dependencies"]]: DependencyConfig< + Store, + Vault, + Input, + any + > + }, + ) => + setupDependencyConfig( + config, + autoConfigs, + ), + setupDependencyMounts, + setupInit: ( + migrations: Migrations, + install: Install, + uninstall: Uninstall, + ) => setupInit(migrations, install, uninstall), + setupInstall: (fn: InstallFn) => Install.of(fn), + setupMain: ( + fn: (o: { + effects: Effects + started(onTerm: () => void): null + utils: Utils + }) => Promise>, + ) => setupMain(fn), + setupMigrations: >>( + ...migrations: EnsureUniqueId + ) => + setupMigrations(this.manifest, ...migrations), + setupUninstall: (fn: UninstallFn) => + setupUninstall(fn), + trigger: { + defaultTrigger, + cooldownTrigger, + changeOnFirstSuccess, + }, + + Backups: { + volumes: (...volumeNames: Array) => + Backups.volumes(...volumeNames), + addSets: ( + ...options: BackupSet[] + ) => Backups.addSets(...options), + withOptions: (options?: Partial) => + Backups.with_options(options), + }, + Config: { + of: < + Spec extends Record< + string, + Value | Value + >, + >( + spec: Spec, + ) => Config.of(spec), + }, + Daemons: { of: Daemons.of }, + DependencyConfig: { + of< + LocalConfig extends Record, + RemoteConfig extends Record, + >({ + localConfig, + remoteConfig, + dependencyConfig, + }: { + localConfig: Config + remoteConfig: Config + dependencyConfig: (options: { + effects: Effects + localConfig: LocalConfig + remoteConfig: RemoteConfig + utils: Utils + }) => Promise> + }) { + return new DependencyConfig( + dependencyConfig, + ) + }, + }, + List: { + text: List.text, + number: List.number, + obj: >( + a: { + name: string + description?: string | null + warning?: string | null + /** Default [] */ + default?: [] + minLength?: number | null + maxLength?: number | null + }, + aSpec: { + spec: Config + displayAs?: null | string + uniqueBy?: null | UniqueBy + }, + ) => List.obj(a, aSpec), + dynamicText: ( + getA: LazyBuild< + Store, + Vault, + { + name: string + description?: string | null + warning?: string | null + /** Default = [] */ + default?: string[] + minLength?: number | null + maxLength?: number | null + disabled?: false | string + generate?: null | RandomString + spec: { + /** Default = false */ + masked?: boolean + placeholder?: string | null + minLength?: number | null + maxLength?: number | null + patterns: Pattern[] + /** Default = "text" */ + inputmode?: ListValueSpecText["inputmode"] + } + } + >, + ) => List.dynamicText(getA), + dynamicNumber: ( + getA: LazyBuild< + Store, + Vault, + { + name: string + description?: string | null + warning?: string | null + /** Default = [] */ + default?: string[] + minLength?: number | null + maxLength?: number | null + disabled?: false | string + spec: { + integer: boolean + min?: number | null + max?: number | null + step?: string | null + units?: string | null + placeholder?: string | null + } + } + >, + ) => List.dynamicNumber(getA), + }, + Migration: { + of: (options: { + version: Version + up: (opts: { + effects: Effects + utils: Utils + }) => Promise + down: (opts: { + effects: Effects + utils: Utils + }) => Promise + }) => Migration.of(options), + }, + Value: { + toggle: Value.toggle, + text: Value.text, + textarea: Value.textarea, + number: Value.number, + color: Value.color, + datetime: Value.datetime, + select: Value.select, + multiselect: Value.multiselect, + object: Value.object, + union: Value.union, + list: Value.list, + dynamicToggle: ( + a: LazyBuild< + Store, + Vault, + { + name: string + description?: string | null + warning?: string | null + default: boolean + disabled?: false | string + } + >, + ) => Value.dynamicToggle(a), + dynamicText: ( + getA: LazyBuild< + Store, + Vault, + { + name: string + description?: string | null + warning?: string | null + required: RequiredDefault + + /** Default = false */ + masked?: boolean + placeholder?: string | null + minLength?: number | null + maxLength?: number | null + patterns?: Pattern[] + /** Default = 'text' */ + inputmode?: ValueSpecText["inputmode"] + generate?: null | RandomString + } + >, + ) => Value.dynamicText(getA), + dynamicTextarea: ( + getA: LazyBuild< + Store, + Vault, + { + name: string + description?: string | null + warning?: string | null + required: boolean + minLength?: number | null + maxLength?: number | null + placeholder?: string | null + disabled?: false | string + generate?: null | RandomString + } + >, + ) => Value.dynamicTextarea(getA), + dynamicNumber: ( + getA: LazyBuild< + Store, + Vault, + { + name: string + description?: string | null + warning?: string | null + required: RequiredDefault + min?: number | null + max?: number | null + /** Default = '1' */ + step?: string | null + integer: boolean + units?: string | null + placeholder?: string | null + disabled?: false | string + } + >, + ) => Value.dynamicNumber(getA), + dynamicColor: ( + getA: LazyBuild< + Store, + Vault, + { + name: string + description?: string | null + warning?: string | null + required: RequiredDefault + + disabled?: false | string + } + >, + ) => Value.dynamicColor(getA), + dynamicDatetime: ( + getA: LazyBuild< + Store, + Vault, + { + name: string + description?: string | null + warning?: string | null + required: RequiredDefault + /** Default = 'datetime-local' */ + inputmode?: ValueSpecDatetime["inputmode"] + min?: string | null + max?: string | null + step?: string | null + disabled?: false | string + } + >, + ) => Value.dynamicDatetime(getA), + dynamicSelect: ( + getA: LazyBuild< + Store, + Vault, + { + name: string + description?: string | null + warning?: string | null + required: RequiredDefault + values: Record + disabled?: false | string + } + >, + ) => Value.dynamicSelect(getA), + dynamicMultiselect: ( + getA: LazyBuild< + Store, + Vault, + { + name: string + description?: string | null + warning?: string | null + default: string[] + values: Record + minLength?: number | null + maxLength?: number | null + disabled?: false | string + } + >, + ) => Value.dynamicMultiselect(getA), + filteredUnion: < + Required extends RequiredDefault, + Type extends Record, + >( + getDisabledFn: LazyBuild, + a: { + name: string + description?: string | null + warning?: string | null + required: Required + }, + aVariants: + | Variants + | Variants, + ) => + Value.filteredUnion( + getDisabledFn, + a, + aVariants, + ), + }, + Variants: { + of: < + VariantValues extends { + [K in string]: { + name: string + spec: Config + } + }, + >( + a: VariantValues, + ) => Variants.of(a), + }, + } + } +} diff --git a/lib/actions/createAction.ts b/lib/actions/createAction.ts index 8889ffc..7e0979c 100644 --- a/lib/actions/createAction.ts +++ b/lib/actions/createAction.ts @@ -1,51 +1,58 @@ import { Config, ExtractConfigType } from "../config/builder/config" -import { ActionMetaData, ActionResult, Effects, ExportedAction } from "../types" -import { Utils, utils } from "../util" +import { ActionMetadata, ActionResult, Effects, ExportedAction } from "../types" +import { createUtils } from "../util" +import { Utils, utils } from "../util/utils" export class CreatedAction< - WrapperData, + Store, + Vault, ConfigType extends | Record - | Config - | Config, + | Config + | Config, Type extends Record = ExtractConfigType, > { private constructor( - public readonly myMetaData: ActionMetaData, + public readonly myMetaData: Omit, readonly fn: (options: { effects: Effects - utils: Utils + utils: Utils input: Type }) => Promise, - readonly input: Config | Config, + readonly input: Config, ) {} public validator = this.input.validator static of< - WrapperData, + Store, + Vault, ConfigType extends | Record - | Config - | Config, + | Config + | Config, Type extends Record = ExtractConfigType, >( - metaData: Omit & { - input: Config | Config + metaData: Omit & { + input: Config | Config }, fn: (options: { effects: Effects - utils: Utils + utils: Utils input: Type }) => Promise, ) { const { input, ...rest } = metaData - return new CreatedAction(rest, fn, input) + return new CreatedAction( + rest, + fn, + input as Config, + ) } exportedAction: ExportedAction = ({ effects, input }) => { return this.fn({ effects, - utils: utils(effects), + utils: createUtils(effects), input: this.validator.unsafeCast(input), }) } @@ -53,15 +60,25 @@ export class CreatedAction< run = async ({ effects, input }: { effects: Effects; input?: Type }) => { return this.fn({ effects, - utils: utils(effects), + utils: createUtils(effects), input: this.validator.unsafeCast(input), }) } + async ActionMetadata(options: { + effects: Effects + utils: Utils + }): Promise { + return { + ...this.myMetaData, + input: await this.input.build(options), + } + } + async getConfig({ effects }: { effects: Effects }) { return this.input.build({ effects, - utils: utils(effects) as any, + utils: createUtils(effects) as any, }) } } diff --git a/lib/actions/setupActions.ts b/lib/actions/setupActions.ts index 5efc844..4f3264d 100644 --- a/lib/actions/setupActions.ts +++ b/lib/actions/setupActions.ts @@ -1,11 +1,13 @@ -import { Effects, ExpectedExports, ExportedAction } from "../types" -import { ActionMetaData } from "../types" +import { Effects, ExpectedExports } from "../types" +import { createUtils } from "../util" import { once } from "../util/once" import { CreatedAction } from "./createAction" -export function setupActions(...createdActions: CreatedAction[]) { +export function setupActions( + ...createdActions: CreatedAction[] +) { const myActions = once(() => { - const actions: Record> = {} + const actions: Record> = {} for (const action of createdActions) { actions[action.myMetaData.id] = action } @@ -15,8 +17,14 @@ export function setupActions(...createdActions: CreatedAction[]) { get actions() { return myActions() }, - get actionsMetadata() { - return createdActions.map((x) => x.myMetaData) + async actionsMetadata({ effects }: { effects: Effects }) { + const utils = createUtils(effects) + return Promise.all( + createdActions.map((x) => x.ActionMetadata({ effects, utils })), + ) }, + } satisfies { + actions: ExpectedExports.actions + actionsMetadata: ExpectedExports.actionsMetadata } } diff --git a/lib/autoconfig/AutoConfig.ts b/lib/autoconfig/AutoConfig.ts deleted file mode 100644 index 2b91d38..0000000 --- a/lib/autoconfig/AutoConfig.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { AutoConfigure, DeepPartial, Effects, ExpectedExports } from "../types" -import { Utils, utils } from "../util" -import { deepEqual } from "../util/deepEqual" -import { deepMerge } from "../util/deepMerge" - -export type AutoConfigFrom = { - [key in keyof NestedConfigs & string]: (options: { - effects: Effects - localConfig: Input - remoteConfig: NestedConfigs[key] - utils: Utils - }) => Promise> -} -export class AutoConfig { - constructor( - readonly configs: AutoConfigFrom, - readonly path: keyof AutoConfigFrom, - ) {} - - async check( - options: Parameters[0], - ): ReturnType { - const origConfig = JSON.parse(JSON.stringify(options.localConfig)) - const newOptions = { - ...options, - utils: utils(options.effects), - localConfig: options.localConfig as Input, - remoteConfig: options.remoteConfig as any, - } - if ( - !deepEqual( - origConfig, - deepMerge( - {}, - options.localConfig, - await this.configs[this.path](newOptions), - ), - ) - ) - throw new Error(`Check failed for ${this.path}`) - } - async autoConfigure( - options: Parameters[0], - ): ReturnType { - const newOptions = { - ...options, - utils: utils(options.effects), - localConfig: options.localConfig as Input, - remoteConfig: options.remoteConfig as any, - } - return deepMerge( - {}, - options.localConfig, - await this.configs[this.path](newOptions), - ) - } -} diff --git a/lib/autoconfig/setupAutoConfig.ts b/lib/autoconfig/setupAutoConfig.ts deleted file mode 100644 index ca02420..0000000 --- a/lib/autoconfig/setupAutoConfig.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { SDKManifest } from "../manifest/ManifestTypes" -import { AutoConfig, AutoConfigFrom } from "./AutoConfig" - -export function setupAutoConfig< - WD, - Input, - Manifest extends SDKManifest, - NestedConfigs extends { - [key in keyof Manifest["dependencies"]]: unknown - }, ->(configs: AutoConfigFrom) { - type C = typeof configs - const answer = { ...configs } as unknown as { - [k in keyof C]: AutoConfig - } - for (const key in configs) { - answer[key as keyof typeof configs] = new AutoConfig< - WD, - Input, - NestedConfigs - >(configs, key as keyof typeof configs) - } - return answer -} diff --git a/lib/backup/Backups.ts b/lib/backup/Backups.ts index e088b80..0e1f4cb 100644 --- a/lib/backup/Backups.ts +++ b/lib/backup/Backups.ts @@ -1,6 +1,5 @@ import { SDKManifest } from "../manifest/ManifestTypes" import * as T from "../types" -import fs from "fs" export type BACKUP = "BACKUP" export const DEFAULT_OPTIONS: T.BackupOptions = { @@ -9,7 +8,7 @@ export const DEFAULT_OPTIONS: T.BackupOptions = { ignoreExisting: false, exclude: [], } -type BackupSet = { +export type BackupSet = { srcPath: string srcVolume: Volumes | BACKUP dstPath: string @@ -41,7 +40,7 @@ type BackupSet = { export class Backups { static BACKUP: BACKUP = "BACKUP" - constructor( + private constructor( private options = DEFAULT_OPTIONS, private backupSet = [] as BackupSet[], ) {} @@ -67,7 +66,9 @@ export class Backups { ) { return new Backups({ ...DEFAULT_OPTIONS, ...options }) } - set_options(options?: Partial) { + + static withOptions = Backups.with_options + setOptions(options?: Partial) { this.options = { ...this.options, ...options, diff --git a/lib/config/builder/config.ts b/lib/config/builder/config.ts index 4fe6052..e9cd9fd 100644 --- a/lib/config/builder/config.ts +++ b/lib/config/builder/config.ts @@ -1,28 +1,32 @@ import { ValueSpec } from "../configTypes" -import { Utils } from "../../util" +import { Utils } from "../../util/utils" import { Value } from "./value" import { _ } from "../../util" import { Effects } from "../../types" import { Parser, object } from "ts-matches" -export type LazyBuildOptions = { +export type LazyBuildOptions = { effects: Effects - utils: Utils + utils: Utils } -export type LazyBuild = ( - options: LazyBuildOptions, +export type LazyBuild = ( + options: LazyBuildOptions, ) => Promise | ExpectedOut // prettier-ignore -export type ExtractConfigType | Config, any> | Config, never>> = - A extends Config | Config ? B : +export type ExtractConfigType | Config, any, any> | Config, never, never>> = + A extends Config | Config ? B : A -export type ConfigSpecOf, WD = never> = { - [K in keyof A]: Value +export type ConfigSpecOf< + A extends Record, + Store = never, + Vault = never, +> = { + [K in keyof A]: Value } -export type MaybeLazyValues = LazyBuild | A +export type MaybeLazyValues = LazyBuild | A /** * Configs are the specs that are used by the os configuration form for this service. * Here is an example of a simple configuration @@ -79,14 +83,16 @@ export const addNodesSpec = Config.of({ hostname: hostname, port: port }); ``` */ -export class Config, WD> { +export class Config, Store, Vault> { private constructor( private readonly spec: { - [K in keyof Type]: Value | Value + [K in keyof Type]: + | Value + | Value }, public validator: Parser, ) {} - async build(options: LazyBuildOptions) { + async build(options: LazyBuildOptions) { const answer = {} as { [K in keyof Type]: ValueSpec } @@ -96,9 +102,14 @@ export class Config, WD> { return answer } - static of | Value>>( - spec: Spec, - ) { + static of< + Spec extends Record< + string, + Value | Value + >, + Store, + Vault, + >(spec: Spec) { const validatorObj = {} as { [K in keyof Spec]: Parser } @@ -109,14 +120,13 @@ export class Config, WD> { return new Config< { [K in keyof Spec]: Spec[K] extends - | Value - | Value + | Value + | Value ? T : never }, - { - [K in keyof Spec]: Spec[K] extends Value ? WD : never - }[keyof Spec] + Store, + Vault >(spec, validator as any) } @@ -129,12 +139,12 @@ export class Config, WD> { required: false, }) - return Config.of()({ - myValue: a.withWrapperData(), + return Config.of()({ + myValue: a.withStore(), }) ``` */ - withWrapperData() { - return this as any as Config + withStore() { + return this as any as Config } } diff --git a/lib/config/builder/list.ts b/lib/config/builder/list.ts index 7157780..e31e623 100644 --- a/lib/config/builder/list.ts +++ b/lib/config/builder/list.ts @@ -22,9 +22,9 @@ export const authorizationList = List.string({ export const auth = Value.list(authorizationList); ``` */ -export class List { +export class List { private constructor( - public build: LazyBuild, + public build: LazyBuild, public validator: Parser, ) {} static text( @@ -49,7 +49,7 @@ export class List { generate?: null | RandomString }, ) { - return new List(() => { + return new List(() => { const spec = { type: "text" as const, placeholder: null, @@ -73,9 +73,10 @@ export class List { } satisfies ValueSpecListOf<"text"> }, arrayOf(string)) } - static dynamicText( + static dynamicText( getA: LazyBuild< - WD, + Store, + Vault, { name: string description?: string | null @@ -99,7 +100,7 @@ export class List { } >, ) { - return new List(async (options) => { + return new List(async (options) => { const { spec: aSpec, ...a } = await getA(options) const spec = { type: "text" as const, @@ -143,7 +144,7 @@ export class List { placeholder?: string | null }, ) { - return new List(() => { + return new List(() => { const spec = { type: "number" as const, placeholder: null, @@ -166,9 +167,10 @@ export class List { } satisfies ValueSpecListOf<"number"> }, arrayOf(number)) } - static dynamicNumber( + static dynamicNumber( getA: LazyBuild< - WD, + Store, + Vault, { name: string description?: string | null @@ -189,7 +191,7 @@ export class List { } >, ) { - return new List(async (options) => { + return new List(async (options) => { const { spec: aSpec, ...a } = await getA(options) const spec = { type: "number" as const, @@ -213,7 +215,7 @@ export class List { } }, arrayOf(number)) } - static obj, WrapperData>( + static obj, Store, Vault>( a: { name: string description?: string | null @@ -224,12 +226,12 @@ export class List { maxLength?: number | null }, aSpec: { - spec: Config + spec: Config 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 = { @@ -265,12 +267,12 @@ export class List { required: false, }) - return Config.of()({ - myValue: a.withWrapperData(), + return Config.of()({ + myValue: a.withStore(), }) ``` */ - withWrapperData() { - return this as any as List + withStore() { + return this as any as List } } diff --git a/lib/config/builder/value.ts b/lib/config/builder/value.ts index 0eb564f..315e378 100644 --- a/lib/config/builder/value.ts +++ b/lib/config/builder/value.ts @@ -25,7 +25,7 @@ import { } from "ts-matches" import { once } from "../../util/once" -type RequiredDefault = +export type RequiredDefault = | false | { default: A | null @@ -94,9 +94,9 @@ const username = Value.string({ }); ``` */ -export class Value { - private constructor( - public build: LazyBuild, +export class Value { + protected constructor( + public build: LazyBuild, public validator: Parser, ) {} static toggle(a: { @@ -108,7 +108,7 @@ export class Value { Default is false */ immutable?: boolean }) { - return new Value( + return new Value( async () => ({ description: null, warning: null, @@ -120,9 +120,10 @@ export class Value { boolean, ) } - static dynamicToggle( + static dynamicToggle( a: LazyBuild< - WD, + Store, + Vault, { name: string description?: string | null @@ -132,7 +133,7 @@ export class Value { } >, ) { - return new Value( + return new Value( async (options) => ({ description: null, warning: null, @@ -163,7 +164,7 @@ export class Value { immutable?: boolean generate?: null | RandomString }) { - return new Value, never>( + return new Value, never, never>( async () => ({ type: "text" as const, description: null, @@ -183,9 +184,10 @@ export class Value { asRequiredParser(string, a), ) } - static dynamicText( + static dynamicText( getA: LazyBuild< - WD, + Store, + Vault, { name: string description?: string | null @@ -204,25 +206,28 @@ export class Value { } >, ) { - return new Value(async (options) => { - const a = await getA(options) - return { - type: "text" as const, - description: null, - warning: null, - masked: false, - placeholder: null, - minLength: null, - maxLength: null, - patterns: [], - inputmode: "text", - disabled: false, - immutable: false, - generate: a.generate ?? null, - ...a, - ...requiredLikeToAbove(a.required), - } - }, string.optional()) + return new Value( + async (options) => { + const a = await getA(options) + return { + type: "text" as const, + description: null, + warning: null, + masked: false, + placeholder: null, + minLength: null, + maxLength: null, + patterns: [], + inputmode: "text", + disabled: false, + immutable: false, + generate: a.generate ?? null, + ...a, + ...requiredLikeToAbove(a.required), + } + }, + string.optional(), + ) } static textarea(a: { name: string @@ -237,7 +242,7 @@ export class Value { immutable?: boolean generate?: null | RandomString }) { - return new Value( + return new Value( async () => ({ description: null, @@ -254,9 +259,10 @@ export class Value { string, ) } - static dynamicTextarea( + static dynamicTextarea( getA: LazyBuild< - WD, + Store, + Vault, { name: string description?: string | null @@ -270,7 +276,7 @@ export class Value { } >, ) { - return new Value(async (options) => { + return new Value(async (options) => { const a = await getA(options) return { description: null, @@ -302,7 +308,7 @@ export class Value { Default is false */ immutable?: boolean }) { - return new Value, never>( + return new Value, never, never>( () => ({ type: "number" as const, description: null, @@ -320,9 +326,10 @@ export class Value { asRequiredParser(number, a), ) } - static dynamicNumber( + static dynamicNumber( getA: LazyBuild< - WD, + Store, + Vault, { name: string description?: string | null @@ -339,23 +346,26 @@ export class Value { } >, ) { - return new Value(async (options) => { - const a = await getA(options) - return { - type: "number" as const, - description: null, - warning: null, - min: null, - max: null, - step: null, - units: null, - placeholder: null, - disabled: false, - immutable: false, - ...a, - ...requiredLikeToAbove(a.required), - } - }, number.optional()) + return new Value( + async (options) => { + const a = await getA(options) + return { + type: "number" as const, + description: null, + warning: null, + min: null, + max: null, + step: null, + units: null, + placeholder: null, + disabled: false, + immutable: false, + ...a, + ...requiredLikeToAbove(a.required), + } + }, + number.optional(), + ) } static color>(a: { name: string @@ -366,7 +376,7 @@ export class Value { Default is false */ immutable?: boolean }) { - return new Value, never>( + return new Value, never, never>( () => ({ type: "color" as const, description: null, @@ -381,9 +391,10 @@ export class Value { ) } - static dynamicColor( + static dynamicColor( getA: LazyBuild< - WD, + Store, + Vault, { name: string description?: string | null @@ -394,18 +405,21 @@ export class Value { } >, ) { - return new Value(async (options) => { - const a = await getA(options) - return { - type: "color" as const, - description: null, - warning: null, - disabled: false, - immutable: false, - ...a, - ...requiredLikeToAbove(a.required), - } - }, string.optional()) + return new Value( + async (options) => { + const a = await getA(options) + return { + type: "color" as const, + description: null, + warning: null, + disabled: false, + immutable: false, + ...a, + ...requiredLikeToAbove(a.required), + } + }, + string.optional(), + ) } static datetime>(a: { name: string @@ -421,7 +435,7 @@ export class Value { Default is false */ immutable?: boolean }) { - return new Value, never>( + return new Value, never, never>( () => ({ type: "datetime" as const, description: null, @@ -438,9 +452,10 @@ export class Value { asRequiredParser(string, a), ) } - static dynamicDatetime( + static dynamicDatetime( getA: LazyBuild< - WD, + Store, + Vault, { name: string description?: string | null @@ -455,22 +470,25 @@ export class Value { } >, ) { - return new Value(async (options) => { - const a = await getA(options) - return { - type: "datetime" as const, - description: null, - warning: null, - inputmode: "datetime-local", - min: null, - max: null, - step: null, - disabled: false, - immutable: false, - ...a, - ...requiredLikeToAbove(a.required), - } - }, string.optional()) + return new Value( + async (options) => { + const a = await getA(options) + return { + type: "datetime" as const, + description: null, + warning: null, + inputmode: "datetime-local", + min: null, + max: null, + step: null, + disabled: false, + immutable: false, + ...a, + ...requiredLikeToAbove(a.required), + } + }, + string.optional(), + ) } static select< Required extends RequiredDefault, @@ -485,7 +503,7 @@ export class Value { Default is false */ immutable?: boolean }) { - return new Value, never>( + return new Value, never, never>( () => ({ description: null, warning: null, @@ -503,9 +521,10 @@ export class Value { ) as any, ) } - static dynamicSelect( + static dynamicSelect( getA: LazyBuild< - WD, + Store, + Vault, { name: string description?: string | null @@ -516,18 +535,21 @@ export class Value { } >, ) { - return new Value(async (options) => { - const a = await getA(options) - return { - description: null, - warning: null, - type: "select" as const, - disabled: false, - immutable: false, - ...a, - ...requiredLikeToAbove(a.required), - } - }, string.optional()) + return new Value( + async (options) => { + const a = await getA(options) + return { + description: null, + warning: null, + type: "select" as const, + disabled: false, + immutable: false, + ...a, + ...requiredLikeToAbove(a.required), + } + }, + string.optional(), + ) } static multiselect>(a: { name: string @@ -541,7 +563,7 @@ export class Value { Default is false */ immutable?: boolean }) { - return new Value<(keyof Values)[], never>( + return new Value<(keyof Values)[], never, never>( () => ({ type: "multiselect" as const, minLength: null, @@ -557,9 +579,10 @@ export class Value { ), ) } - static dynamicMultiselect( + static dynamicMultiselect( getA: LazyBuild< - WD, + Store, + Vault, { name: string description?: string | null @@ -572,7 +595,7 @@ export class Value { } >, ) { - return new Value(async (options) => { + return new Value(async (options) => { const a = await getA(options) return { type: "multiselect" as const, @@ -586,15 +609,15 @@ export class Value { } }, arrayOf(string)) } - static object, WrapperData>( + static object, Store, Vault>( a: { name: string description?: string | null warning?: string | null }, - previousSpec: Config, + previousSpec: Config, ) { - return new Value(async (options) => { + return new Value(async (options) => { const spec = await previousSpec.build(options as any) return { type: "object" as const, @@ -605,7 +628,7 @@ export class Value { } }, previousSpec.validator) } - static union, Type, WrapperData>( + static union, Type, Store, Vault>( a: { name: string description?: string | null @@ -615,9 +638,9 @@ export class Value { Default is false */ immutable?: boolean }, - aVariants: Variants, + aVariants: Variants, ) { - return new Value, WrapperData>( + return new Value, Store, Vault>( async (options) => ({ type: "union" as const, description: null, @@ -630,39 +653,38 @@ export class Value { asRequiredParser(aVariants.validator, a), ) } - static filteredUnion( - getDisabledFn: LazyBuild, + static filteredUnion< + Required extends RequiredDefault, + Type extends Record, + Store = never, + Vault = never, + >( + getDisabledFn: LazyBuild, + a: { + name: string + description?: string | null + warning?: string | null + required: Required + }, + aVariants: Variants | Variants, ) { - return < - Required extends RequiredDefault, - Type extends Record, - >( - a: { - name: string - description?: string | null - warning?: string | null - required: Required - }, - aVariants: Variants | Variants, - ) => { - return new Value, WrapperData>( - async (options) => ({ - type: "union" as const, - description: null, - warning: null, - ...a, - variants: await aVariants.build(options as any), - ...requiredLikeToAbove(a.required), - disabled: (await getDisabledFn(options)) || [], - immutable: false, - }), - asRequiredParser(aVariants.validator, a), - ) - } + return new Value, Store, Vault>( + async (options) => ({ + type: "union" as const, + description: null, + warning: null, + ...a, + variants: await aVariants.build(options as any), + ...requiredLikeToAbove(a.required), + disabled: (await getDisabledFn(options)) || [], + immutable: false, + }), + asRequiredParser(aVariants.validator, a), + ) } - static list(a: List) { - return new Value( + static list(a: List) { + return new Value( (options) => a.build(options), a.validator, ) @@ -677,42 +699,12 @@ export class Value { required: false, }) - return Config.of()({ - myValue: a.withWrapperData(), + return Config.of()({ + myValue: a.withStore(), }) ``` */ - withWrapperData() { - return this as any as Value + withStore() { + return this as any as Value } } - -type Wrapper = { test: 1 | "5" } -const valueA = Value.dynamicText(() => ({ - name: "a", - required: false, -})) -const variantForC = Variants.of({ - lnd: { - name: "lnd Name", - spec: Config.of({ - name: Value.text({ - name: "Node Name", - required: false, - }), - }), - }, -}) -const valueC = Value.filteredUnion(() => [])( - { name: "a", required: false }, - variantForC, -) -const valueB = Value.text({ - name: "a", - required: false, -}) -const test = Config.of({ - a: valueA, - b: valueB, - c: valueC, -}) diff --git a/lib/config/builder/variants.ts b/lib/config/builder/variants.ts index b30b3f1..06ee57c 100644 --- a/lib/config/builder/variants.ts +++ b/lib/config/builder/variants.ts @@ -51,24 +51,21 @@ export const pruning = Value.union( ); ``` */ -export class Variants { +export class Variants { + static text: any private constructor( - public build: LazyBuild, + public build: LazyBuild, public validator: Parser, ) {} - // A extends { - // [key: string]: { - // name: string - // spec: InputSpec - // } - // }, static of< VariantValues extends { [K in string]: { name: string - spec: Config | Config + spec: Config | Config } }, + Store, + Vault, >(a: VariantValues) { const validator = anyOf( ...Object.entries(a).map(([name, { spec }]) => @@ -83,21 +80,14 @@ export class Variants { { [K in keyof VariantValues]: { unionSelectKey: K - unionValueKey: VariantValues[K]["spec"] extends - | Config - | Config - ? B - : never + // prettier-ignore + unionValueKey: + VariantValues[K]["spec"] extends (Config | Config) ? B : + never } }[keyof VariantValues], - { - [K in keyof VariantValues]: VariantValues[K] extends Config< - any, - infer C - > - ? C - : never - }[keyof VariantValues] + Store, + Vault >(async (options) => { const variants = {} as { [K in keyof VariantValues]: { name: string; spec: InputSpec } @@ -121,12 +111,12 @@ export class Variants { required: false, }) - return Config.of()({ - myValue: a.withWrapperData(), + return Config.of()({ + myValue: a.withStore(), }) ``` */ - withWrapperData() { - return this as any as Variants + withStore() { + return this as any as Variants } } diff --git a/lib/config/constants.ts b/lib/config/configConstants.ts similarity index 87% rename from lib/config/constants.ts rename to lib/config/configConstants.ts index b2e4002..54e0e62 100644 --- a/lib/config/constants.ts +++ b/lib/config/configConstants.ts @@ -3,10 +3,11 @@ import { Config, ConfigSpecOf } from "./builder/config" import { Value } from "./builder/value" import { Variants } from "./builder/variants" -export const smtpConfig = Value.filteredUnion(async ({ effects, utils }) => { - const smtp = await utils.getSystemSmtp().once() - return smtp ? [] : ["system"] -})( +export const smtpConfig = Value.filteredUnion( + async ({ effects, utils }) => { + const smtp = await utils.getSystemSmtp().once() + return smtp ? [] : ["system"] + }, { name: "SMTP", description: "Optionally provide an SMTP server for sending email", @@ -17,7 +18,7 @@ export const smtpConfig = Value.filteredUnion(async ({ effects, utils }) => { system: { name: "System Credentials", spec: Config.of({}) }, custom: { name: "Custom Credentials", - spec: Config.of>({ + spec: Config.of, never, never>({ server: Value.text({ name: "SMTP Server", required: { diff --git a/lib/config/dependencies.ts b/lib/config/configDependencies.ts similarity index 73% rename from lib/config/dependencies.ts rename to lib/config/configDependencies.ts index b974160..6b31abc 100644 --- a/lib/config/dependencies.ts +++ b/lib/config/configDependencies.ts @@ -1,12 +1,14 @@ import { SDKManifest } from "../manifest/ManifestTypes" import { Dependency } from "../types" -export type Dependencies = { +export type ConfigDependencies = { exists(id: keyof T["dependencies"]): Dependency running(id: keyof T["dependencies"]): Dependency } -export const dependenciesSet = (): Dependencies => ({ +export const configDependenciesSet = < + T extends SDKManifest, +>(): ConfigDependencies => ({ exists(id: keyof T["dependencies"]) { return { id, diff --git a/lib/config/index.ts b/lib/config/index.ts index ab95a64..510dc1c 100644 --- a/lib/config/index.ts +++ b/lib/config/index.ts @@ -1,5 +1,5 @@ import "./builder" import "./setupConfig" -import "./dependencies" -import "./constants" +import "./configDependencies" +import "./configConstants" diff --git a/lib/config/setupConfig.ts b/lib/config/setupConfig.ts index 07d0057..50cb93b 100644 --- a/lib/config/setupConfig.ts +++ b/lib/config/setupConfig.ts @@ -1,8 +1,8 @@ import { Effects, ExpectedExports } from "../types" import { SDKManifest } from "../manifest/ManifestTypes" -import * as D from "./dependencies" +import * as D from "./configDependencies" import { Config, ExtractConfigType } from "./builder/config" -import { Utils, utils } from "../util" +import { Utils, utils } from "../util/utils" import nullIfEmpty from "../util/nullIfEmpty" declare const dependencyProof: unique symbol @@ -11,30 +11,32 @@ export type DependenciesReceipt = void & { } export type Save< - WD, + Store, + Vault, A extends | Record - | Config, any> - | Config, never>, + | Config, any, any> + | Config, never, never>, Manifest extends SDKManifest, > = (options: { effects: Effects input: ExtractConfigType & Record - utils: Utils - dependencies: D.Dependencies + utils: Utils + dependencies: D.ConfigDependencies }) => Promise<{ dependenciesReceipt: DependenciesReceipt restart: boolean }> export type Read< - WD, + Store, + Vault, A extends | Record - | Config, any> - | Config, never>, + | Config, any, any> + | Config, never, never>, > = (options: { effects: Effects - utils: Utils + utils: Utils }) => Promise & Record)> /** * We want to setup a config export with a get and set, this @@ -44,17 +46,18 @@ export type Read< * @returns */ export function setupConfig< - WD, + Store, + Vault, ConfigType extends | Record - | Config - | Config, + | Config + | Config, Manifest extends SDKManifest, Type extends Record = ExtractConfigType, >( - spec: Config | Config, - write: Save, - read: Read, + spec: Config | Config, + write: Save, + read: Read, ) { const validator = spec.validator return { @@ -66,15 +69,15 @@ export function setupConfig< const { restart } = await write({ input: JSON.parse(JSON.stringify(input)), effects, - utils: utils(effects), - dependencies: D.dependenciesSet(), + utils: utils(effects), + dependencies: D.configDependenciesSet(), }) if (restart) { await effects.restart() } }) as ExpectedExports.setConfig, getConfig: (async ({ effects }) => { - const myUtils = utils(effects) + const myUtils = utils(effects) const configValue = nullIfEmpty( (await read({ effects, utils: myUtils })) || null, ) diff --git a/lib/dependency/mountDependencies.ts b/lib/dependency/mountDependencies.ts new file mode 100644 index 0000000..4721d26 --- /dev/null +++ b/lib/dependency/mountDependencies.ts @@ -0,0 +1,43 @@ +import { Effects } from "../types" +import { + Path, + ManifestId, + VolumeName, + NamedPath, + matchPath, +} from "./setupDependencyMounts" + +export type MountDependenciesOut = + // prettier-ignore + A extends Path ? string : A extends Record ? { + [P in keyof A]: MountDependenciesOut; + } : never +export async function mountDependencies< + In extends + | Record>> + | Record> + | Record + | Path, +>(effects: Effects, value: In): Promise> { + if (matchPath.test(value)) { + const mountPath = `${value.manifest.id}/${value.volume}/${value.name}` + + return (await effects.mount({ + location: { + path: mountPath, + }, + target: { + packageId: value.manifest.id, + path: value.path, + readonly: value.readonly, + volumeId: value.volume, + }, + })) as MountDependenciesOut + } + return Object.fromEntries( + Object.entries(value).map(([key, value]) => [ + key, + mountDependencies(effects, value), + ]), + ) as Record as MountDependenciesOut +} diff --git a/lib/dependency/setupDependencyMounts.ts b/lib/dependency/setupDependencyMounts.ts new file mode 100644 index 0000000..e3c8cdd --- /dev/null +++ b/lib/dependency/setupDependencyMounts.ts @@ -0,0 +1,70 @@ +import { boolean, object, string } from "ts-matches" +import { SDKManifest } from "../manifest/ManifestTypes" +import { deepMerge } from "../util/deepMerge" + +export type VolumeName = string +export type NamedPath = string +export type ManifestId = string + +export const matchPath = object({ + name: string, + volume: string, + path: string, + manifest: object({ + id: string, + }), + readonly: boolean, +}) +export type Path = typeof matchPath._TYPE +export type BuildPath = { + [PId in M["manifest"]["id"]]: { + [V in M["volume"]]: { + [N in M["name"]]: M + } + } +} +type ValidIfNotInNested< + Building, + M extends Path, +> = Building extends BuildPath ? never : M +class SetupDependencyMounts { + private constructor(readonly building: Building) {} + + static of() { + return new SetupDependencyMounts({}) + } + + addPath< + NamedPath extends string, + VolumeName extends string, + PathNamed extends string, + M extends SDKManifest, + >( + newPath: ValidIfNotInNested< + Building, + { + name: NamedPath + volume: VolumeName + path: PathNamed + manifest: M + readonly: boolean + } + >, + ) { + const building = deepMerge(this.building, { + [newPath.manifest.id]: { + [newPath.volume]: { + [newPath.name]: newPath, + }, + }, + }) as Building & BuildPath + return new SetupDependencyMounts(building) + } + build() { + return this.building + } +} + +export function setupDependencyMounts() { + return SetupDependencyMounts.of() +} diff --git a/lib/dependencyConfig/DependencyConfig.ts b/lib/dependencyConfig/DependencyConfig.ts new file mode 100644 index 0000000..ecde744 --- /dev/null +++ b/lib/dependencyConfig/DependencyConfig.ts @@ -0,0 +1,62 @@ +import { + DependencyConfig as DependencyConfigType, + DeepPartial, + Effects, +} from "../types" +import { Utils, utils } from "../util/utils" +import { deepEqual } from "../util/deepEqual" +import { deepMerge } from "../util/deepMerge" + +export class DependencyConfig< + Store, + Vault, + Input extends Record, + RemoteConfig extends Record, +> { + constructor( + readonly dependencyConfig: (options: { + effects: Effects + localConfig: Input + remoteConfig: RemoteConfig + utils: Utils + }) => Promise>, + ) {} + + async check( + options: Parameters[0], + ): ReturnType { + const origConfig = JSON.parse(JSON.stringify(options.localConfig)) + const newOptions = { + ...options, + utils: utils(options.effects), + localConfig: options.localConfig as Input, + remoteConfig: options.remoteConfig as RemoteConfig, + } + if ( + !deepEqual( + origConfig, + deepMerge( + {}, + options.localConfig, + await this.dependencyConfig(newOptions), + ), + ) + ) + throw new Error(`Check failed`) + } + async autoConfigure( + options: Parameters[0], + ): ReturnType { + const newOptions = { + ...options, + utils: utils(options.effects), + localConfig: options.localConfig as Input, + remoteConfig: options.remoteConfig as any, + } + return deepMerge( + {}, + options.remoteConfig, + await this.dependencyConfig(newOptions), + ) + } +} diff --git a/lib/autoconfig/index.ts b/lib/dependencyConfig/index.ts similarity index 78% rename from lib/autoconfig/index.ts rename to lib/dependencyConfig/index.ts index 031733b..3fe78b4 100644 --- a/lib/autoconfig/index.ts +++ b/lib/dependencyConfig/index.ts @@ -5,5 +5,5 @@ export type ReadonlyDeep = export type MaybePromise = Promise | A export type Message = string -import "./AutoConfig" -import "./setupAutoConfig" +import "./DependencyConfig" +import "./setupDependencyConfig" diff --git a/lib/dependencyConfig/setupDependencyConfig.ts b/lib/dependencyConfig/setupDependencyConfig.ts new file mode 100644 index 0000000..536b257 --- /dev/null +++ b/lib/dependencyConfig/setupDependencyConfig.ts @@ -0,0 +1,23 @@ +import { Config } from "../config/builder/config" +import { SDKManifest } from "../manifest/ManifestTypes" +import { ExpectedExports } from "../types" +import { DependencyConfig } from "./DependencyConfig" + +export function setupDependencyConfig< + Store, + Vault, + Input extends Record, + Manifest extends SDKManifest, +>( + _config: Config, + autoConfigs: { + [key in keyof Manifest["dependencies"] & string]: DependencyConfig< + Store, + Vault, + Input, + any + > + }, +): ExpectedExports.dependencyConfig { + return autoConfigs +} diff --git a/lib/health/HealthCheck.ts b/lib/health/HealthCheck.ts index 7e2b54b..bb13313 100644 --- a/lib/health/HealthCheck.ts +++ b/lib/health/HealthCheck.ts @@ -2,9 +2,9 @@ import { InterfaceReceipt } from "../mainFn/interfaceReceipt" import { Daemon, Effects } from "../types" import { CheckResult } from "./checkFns/CheckResult" import { HealthReceipt } from "./HealthReceipt" -import { Trigger } from "./trigger" -import { TriggerInput } from "./trigger/TriggerInput" -import { defaultTrigger } from "./trigger/defaultTrigger" +import { Trigger } from "../trigger" +import { TriggerInput } from "../trigger/TriggerInput" +import { defaultTrigger } from "../trigger/defaultTrigger" import { once } from "../util/once" export function healthCheck(o: { diff --git a/lib/index.ts b/lib/index.ts index 85c32e1..f8a076a 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -11,7 +11,7 @@ import "@iarna/toml" import "./types" import "./util" import "yaml" -import "./autoconfig" +import "./dependencyConfig" import "./actions" import "./manifest" import "./inits" diff --git a/lib/inits/migrations/Migration.ts b/lib/inits/migrations/Migration.ts index 3cb20dd..2aed622 100644 --- a/lib/inits/migrations/Migration.ts +++ b/lib/inits/migrations/Migration.ts @@ -1,27 +1,40 @@ import { ManifestVersion } from "../../manifest/ManifestTypes" import { Effects } from "../../types" +import { Utils } from "../../util/utils" -export class Migration { +export class Migration { constructor( readonly options: { version: Version - up: (opts: { effects: Effects }) => Promise - down: (opts: { effects: Effects }) => Promise + up: (opts: { + effects: Effects + utils: Utils + }) => Promise + down: (opts: { + effects: Effects + utils: Utils + }) => Promise }, ) {} - static of(options: { + static of(options: { version: Version - up: (opts: { effects: Effects }) => Promise - down: (opts: { effects: Effects }) => Promise + up: (opts: { + effects: Effects + utils: Utils + }) => Promise + down: (opts: { + effects: Effects + utils: Utils + }) => Promise }) { - return new Migration(options) + return new Migration(options) } - async up(opts: { effects: Effects }) { + async up(opts: { effects: Effects; utils: Utils }) { this.up(opts) } - async down(opts: { effects: Effects }) { + async down(opts: { effects: Effects; utils: Utils }) { this.down(opts) } } diff --git a/lib/inits/migrations/setupMigrations.ts b/lib/inits/migrations/setupMigrations.ts index 7034eca..a7278c7 100644 --- a/lib/inits/migrations/setupMigrations.ts +++ b/lib/inits/migrations/setupMigrations.ts @@ -1,39 +1,44 @@ -import { setupActions } from "../../actions/setupActions" import { EmVer } from "../../emverLite/mod" import { SDKManifest } from "../../manifest/ManifestTypes" import { ExpectedExports } from "../../types" +import { createUtils } from "../../util" import { once } from "../../util/once" import { Migration } from "./Migration" -export class Migrations { +export class Migrations { private constructor( readonly manifest: SDKManifest, - readonly migrations: Array>, + readonly migrations: Array>, ) {} private sortedMigrations = once(() => { - const migrationsAsVersions = (this.migrations as Array>).map( - (x) => [EmVer.parse(x.options.version), x] as const, - ) + const migrationsAsVersions = ( + this.migrations as Array> + ).map((x) => [EmVer.parse(x.options.version), x] as const) migrationsAsVersions.sort((a, b) => a[0].compareForSort(b[0])) return migrationsAsVersions }) private currentVersion = once(() => EmVer.parse(this.manifest.version)) - static of>>( - manifest: SDKManifest, - ...migrations: EnsureUniqueId - ) { - return new Migrations(manifest, migrations as Array>) + static of< + Store, + Vault, + Migrations extends Array>, + >(manifest: SDKManifest, ...migrations: EnsureUniqueId) { + return new Migrations( + manifest, + migrations as Array>, + ) } async init({ effects, previousVersion, }: Parameters[0]) { + const utils = createUtils(effects) if (!!previousVersion) { const previousVersionEmVer = EmVer.parse(previousVersion) for (const [_, migration] of this.sortedMigrations() .filter((x) => x[0].greaterThan(previousVersionEmVer)) .filter((x) => x[0].lessThanOrEqual(this.currentVersion()))) { - await migration.up({ effects }) + await migration.up({ effects, utils }) } } } @@ -41,29 +46,31 @@ export class Migrations { effects, nextVersion, }: Parameters[0]) { + const utils = createUtils(effects) if (!!nextVersion) { const nextVersionEmVer = EmVer.parse(nextVersion) const reversed = [...this.sortedMigrations()].reverse() for (const [_, migration] of reversed .filter((x) => x[0].greaterThan(nextVersionEmVer)) .filter((x) => x[0].lessThanOrEqual(this.currentVersion()))) { - await migration.down({ effects }) + await migration.down({ effects, utils }) } } } } -export function setupMigrations>>( - manifest: SDKManifest, - ...migrations: EnsureUniqueId -) { - return Migrations.of(manifest, ...migrations) +export function setupMigrations< + Store, + Vault, + Migrations extends Array>, +>(manifest: SDKManifest, ...migrations: EnsureUniqueId) { + return Migrations.of(manifest, ...migrations) } // prettier-ignore export type EnsureUniqueId = B extends [] ? A : - B extends [Migration, ...infer Rest] ? ( + B extends [Migration, ...infer Rest] ? ( id extends ids ? "One of the ids are not unique"[] : EnsureUniqueId ) : "There exists a migration that is not a Migration"[] diff --git a/lib/inits/setupInit.ts b/lib/inits/setupInit.ts index c782b2c..ad5414b 100644 --- a/lib/inits/setupInit.ts +++ b/lib/inits/setupInit.ts @@ -3,10 +3,10 @@ import { Migrations } from "./migrations/setupMigrations" import { Install } from "./setupInstall" import { Uninstall } from "./setupUninstall" -export function setupInit( - migrations: Migrations, - install: Install, - uninstall: Uninstall, +export function setupInit( + migrations: Migrations, + install: Install, + uninstall: Uninstall, ): { init: ExpectedExports.init uninit: ExpectedExports.uninit diff --git a/lib/inits/setupInstall.ts b/lib/inits/setupInstall.ts index 67c9569..a24f0e3 100644 --- a/lib/inits/setupInstall.ts +++ b/lib/inits/setupInstall.ts @@ -1,13 +1,13 @@ import { Effects, ExpectedExports } from "../types" -import { Utils, utils } from "../util" +import { Utils, utils } from "../util/utils" -export type InstallFn = (opts: { +export type InstallFn = (opts: { effects: Effects - utils: Utils + utils: Utils }) => Promise -export class Install { - private constructor(readonly fn: InstallFn) {} - static of(fn: InstallFn) { +export class Install { + private constructor(readonly fn: InstallFn) {} + static of(fn: InstallFn) { return new Install(fn) } @@ -15,10 +15,14 @@ export class Install { effects, previousVersion, }: Parameters[0]) { - if (!previousVersion) await this.fn({ effects, utils: utils(effects) }) + if (!previousVersion) + await this.fn({ + effects, + utils: utils(effects), + }) } } -export function setupInstall(fn: InstallFn) { +export function setupInstall(fn: InstallFn) { return Install.of(fn) } diff --git a/lib/inits/setupUninstall.ts b/lib/inits/setupUninstall.ts index 5352001..209ffd9 100644 --- a/lib/inits/setupUninstall.ts +++ b/lib/inits/setupUninstall.ts @@ -1,13 +1,13 @@ import { Effects, ExpectedExports } from "../types" -import { Utils, utils } from "../util" +import { Utils, utils } from "../util/utils" -export type UninstallFn = (opts: { +export type UninstallFn = (opts: { effects: Effects - utils: Utils + utils: Utils }) => 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) } @@ -15,10 +15,14 @@ export class Uninstall { effects, nextVersion, }: Parameters[0]) { - if (!nextVersion) await this.fn({ effects, utils: utils(effects) }) + if (!nextVersion) + await this.fn({ + effects, + utils: utils(effects), + }) } } -export function setupUninstall(fn: UninstallFn) { +export function setupUninstall(fn: UninstallFn) { return Uninstall.of(fn) } diff --git a/lib/mainFn/Daemons.ts b/lib/mainFn/Daemons.ts index 247a83e..f489cfb 100644 --- a/lib/mainFn/Daemons.ts +++ b/lib/mainFn/Daemons.ts @@ -1,8 +1,8 @@ import { HealthReceipt } from "../health/HealthReceipt" import { CheckResult } from "../health/checkFns" -import { Trigger } from "../health/trigger" -import { TriggerInput } from "../health/trigger/TriggerInput" -import { defaultTrigger } from "../health/trigger/defaultTrigger" +import { Trigger } from "../trigger" +import { TriggerInput } from "../trigger/TriggerInput" +import { defaultTrigger } from "../trigger/defaultTrigger" import { DaemonReturned, Effects, ValidIfNoStupidEscape } from "../types" import { InterfaceReceipt } from "./interfaceReceipt" type Daemon = { diff --git a/lib/mainFn/index.ts b/lib/mainFn/index.ts index 21b1fd3..833107e 100644 --- a/lib/mainFn/index.ts +++ b/lib/mainFn/index.ts @@ -1,5 +1,6 @@ import { Effects, ExpectedExports } from "../types" -import { Utils, utils } from "../util" +import { createMainUtils } from "../util" +import { Utils, utils } from "../util/utils" import { Daemons } from "./Daemons" import "./exportInterfaces" import "./LocalBinding" @@ -22,17 +23,17 @@ import "./Daemons" * @param fn * @returns */ -export const setupMain = ( +export const setupMain = ( fn: (o: { effects: Effects started(onTerm: () => void): null - utils: Utils + utils: Utils }) => Promise>, ): ExpectedExports.main => { return async (options) => { const result = await fn({ ...options, - utils: utils(options.effects), + utils: createMainUtils(options.effects), }) await result.build().then((x) => x.wait()) } diff --git a/lib/manifest/ManifestTypes.ts b/lib/manifest/ManifestTypes.ts index f5da303..50c97c2 100644 --- a/lib/manifest/ManifestTypes.ts +++ b/lib/manifest/ManifestTypes.ts @@ -1,5 +1,5 @@ import { ValidEmVer } from "../emverLite/mod" -import { ActionMetaData } from "../types" +import { ActionMetadata } from "../types" export interface Container { /** This should be pointing to a docker container name */ @@ -14,65 +14,68 @@ export interface Container { export type ManifestVersion = ValidEmVer -export interface SDKManifest { +export type SDKManifest = { /** The package identifier used by the OS. This must be unique amongst all other known packages */ - id: string + readonly id: string /** A human readable service title */ - title: string + readonly title: string /** Service version - accepts up to four digits, where the last confirms to revisions necessary for StartOs * - see documentation: https://github.com/Start9Labs/emver-rs. This value will change with each release of * the service */ - version: ManifestVersion + readonly version: ManifestVersion /** Release notes for the update - can be a string, paragraph or URL */ - releaseNotes: string + readonly releaseNotes: string /** The type of license for the project. Include the LICENSE in the root of the project directory. A license is required for a Start9 package.*/ - license: string // name of license + readonly license: string // name of license /** A list of normie (hosted, SaaS, custodial, etc) services this services intends to replace */ - replaces: string[] + readonly replaces: Readonly /** The Start9 wrapper repository URL for the package. This repo contains the manifest file (this), * any scripts necessary for configuration, backups, actions, or health checks (more below). This key * must exist. But could be embedded into the source repository */ - wrapperRepo: string + readonly wrapperRepo: string /** The original project repository URL. There is no upstream repo in this example */ - upstreamRepo: string + readonly upstreamRepo: string /** URL to the support site / channel for the project. This key can be omitted if none exists, or it can link to the original project repository issues */ - supportSite: string + readonly supportSite: string /** URL to the marketing site for the project. If there is no marketing site, it can link to the original project repository */ - marketingSite: string + readonly marketingSite: string /** URL where users can donate to the upstream project */ - donationUrl: string | null + readonly donationUrl: string | null /**Human readable descriptions for the service. These are used throughout the StartOS user interface, primarily in the marketplace. */ - description: { + readonly description: { /**This is the first description visible to the user in the marketplace */ - short: string + readonly short: string /** This description will display with additional details in the service's individual marketplace page */ - long: string + readonly long: string } /** These assets are static files necessary for packaging the service for Start9 (into an s9pk). * Each value is a path to the specified asset. If an asset is missing from this list, or otherwise * denoted, it will be defaulted to the values denoted below. */ - assets: { - icon: string // file path - instructions: string // file path - license: string // file path + readonly assets: { + /** This is the file path for the icon that will be this packages icon on the ui */ + readonly icon: string + /** Instructions path to be seen in the ui section of the package */ + readonly instructions: string + /** license path */ + readonly license: string } /** Defines the containers needed to run the main and mounted volumes */ - containers: Record + readonly containers: Record /** This denotes any data, asset, or pointer volumes that should be connected when the "docker run" command is invoked */ - volumes: Record - actions: Array - alerts: { - install: string | null - update: string | null - uninstall: string | null - restore: string | null - start: string | null - stop: string | null + readonly volumes: Record + + readonly alerts: { + readonly install: string | null + readonly update: string | null + readonly uninstall: string | null + readonly restore: string | null + readonly start: string | null + readonly stop: string | null } - dependencies: Record + readonly dependencies: Readonly> } export interface ManifestDependency { diff --git a/lib/store/getStore.ts b/lib/store/getStore.ts new file mode 100644 index 0000000..4ea3a94 --- /dev/null +++ b/lib/store/getStore.ts @@ -0,0 +1,61 @@ +import { Effects, EnsureStorePath } from "../types" + +export class GetStore { + constructor( + readonly effects: Effects, + readonly path: Path & EnsureStorePath, + readonly options: { + /** Defaults to what ever the package currently in */ + packageId?: string | undefined + } = {}, + ) {} + + /** + * Returns the value of Store at the provided path. Restart the service if the value changes + */ + const() { + return this.effects.store.get({ + ...this.options, + path: this.path as any, + callback: this.effects.restart, + }) + } + /** + * Returns the value of Store at the provided path. Does nothing if the value changes + */ + once() { + return this.effects.store.get({ + ...this.options, + path: this.path as any, + callback: () => {}, + }) + } + + /** + * Watches the value of Store at the provided path. Takes a custom callback function to run whenever the value changes + */ + async *watch() { + while (true) { + let callback: () => void + const waitForNext = new Promise((resolve) => { + callback = resolve + }) + yield await this.effects.store.get({ + ...this.options, + path: this.path as any, + callback: () => callback(), + }) + await waitForNext + } + } +} +export function getStore( + effects: Effects, + path: Path & EnsureStorePath, + options: { + /** Defaults to what ever the package currently in */ + packageId?: string | undefined + } = {}, +) { + return new GetStore(effects, path as any, options) +} diff --git a/lib/test/configBuilder.test.ts b/lib/test/configBuilder.test.ts index d196742..1ea8835 100644 --- a/lib/test/configBuilder.test.ts +++ b/lib/test/configBuilder.test.ts @@ -4,9 +4,7 @@ import { List } from "../config/builder/list" import { Value } from "../config/builder/value" import { Variants } from "../config/builder/variants" import { ValueSpec } from "../config/configTypes" -import { Parser } from "ts-matches" -type test = unknown | { test: 5 } describe("builder tests", () => { test("text", async () => { const bitcoinPropertiesBuilt: { @@ -299,7 +297,7 @@ describe("values", () => { utils: "utils", } as any test("toggle", async () => { - const value = Value.dynamicToggle<{}>(async () => ({ + const value = Value.dynamicToggle(async () => ({ name: "Testing", description: null, warning: null, @@ -364,7 +362,7 @@ describe("values", () => { }) }) test("color", async () => { - const value = Value.dynamicColor(async () => ({ + const value = Value.dynamicColor(async () => ({ name: "Testing", required: false, description: null, @@ -385,7 +383,7 @@ describe("values", () => { test("datetime", async () => { const value = Value.dynamicDatetime<{ test: "a" }>(async ({ utils }) => { ;async () => { - ;(await utils.getOwnWrapperData("/test").once()) satisfies "a" + ;(await utils.store.getOwn("/test").once()) satisfies "a" } return { @@ -500,7 +498,8 @@ describe("values", () => { }) describe("filtering", () => { test("union", async () => { - const value = Value.filteredUnion(() => ["a", "c"])( + const value = Value.filteredUnion( + () => ["a", "c"], { name: "Testing", required: { default: null }, diff --git a/lib/test/makeOutput.ts b/lib/test/makeOutput.ts index d4eb063..cef17a7 100644 --- a/lib/test/makeOutput.ts +++ b/lib/test/makeOutput.ts @@ -423,7 +423,6 @@ oldSpecToBuilder( }, { // convert this to `start-sdk/lib` for conversions - startSdk: "../..", - wrapperData: "./output.wrapperData", + StartSdk: "./output.sdk", }, ) diff --git a/lib/test/mountDependencies.test.ts b/lib/test/mountDependencies.test.ts new file mode 100644 index 0000000..c6b9e01 --- /dev/null +++ b/lib/test/mountDependencies.test.ts @@ -0,0 +1,139 @@ +import { setupManifest } from "../manifest/setupManifest" +import { mountDependencies } from "../dependency/mountDependencies" +import { + BuildPath, + setupDependencyMounts, +} from "../dependency/setupDependencyMounts" + +describe("mountDependencies", () => { + const clnManifest = setupManifest({ + id: "cln", + title: "", + version: "1", + releaseNotes: "", + license: "", + replaces: [], + wrapperRepo: "", + upstreamRepo: "", + supportSite: "", + marketingSite: "", + donationUrl: null, + description: { + short: "", + long: "", + }, + assets: { + icon: "", + instructions: "", + license: "", + }, + containers: {}, + volumes: { main: "data" }, + alerts: { + install: null, + update: null, + uninstall: null, + restore: null, + start: null, + stop: null, + }, + dependencies: {}, + }) + const lndManifest = setupManifest({ + id: "lnd", + title: "", + version: "1", + releaseNotes: "", + license: "", + replaces: [], + wrapperRepo: "", + upstreamRepo: "", + supportSite: "", + marketingSite: "", + donationUrl: null, + description: { + short: "", + long: "", + }, + assets: { + icon: "", + instructions: "", + license: "", + }, + containers: {}, + volumes: {}, + alerts: { + install: null, + update: null, + uninstall: null, + restore: null, + start: null, + stop: null, + }, + dependencies: {}, + }) + clnManifest.id + type test = BuildPath<{ + name: "root" + manifest: typeof clnManifest + volume: "main" + path: "/" + readonly: true + }> extends BuildPath<{ + name: "root" + manifest: typeof clnManifest + volume: "main2" + path: "/" + readonly: true + }> + ? true + : false + + test("Types work", () => { + const dependencyMounts = setupDependencyMounts() + .addPath({ + name: "root", + volume: "main", + path: "/", + manifest: clnManifest, + readonly: true, + }) + .addPath({ + name: "root", + manifest: lndManifest, + volume: "main", + path: "/", + readonly: true, + }) + .build() + ;() => { + const test = mountDependencies( + null as any, + dependencyMounts, + ) satisfies Promise<{ + cln: { + main: { + root: string + } + } + lnd: { + main: { + root: string + } + } + }> + const test2 = mountDependencies( + null as any, + dependencyMounts.cln, + ) satisfies Promise<{ + main: { root: string } + }> + const test3 = mountDependencies( + null as any, + dependencyMounts.cln.main, + ) satisfies Promise<{ + root: string + }> + } + }) +}) diff --git a/lib/test/output.sdk.ts b/lib/test/output.sdk.ts new file mode 100644 index 0000000..4802317 --- /dev/null +++ b/lib/test/output.sdk.ts @@ -0,0 +1,49 @@ +import { StartSdk } from "../StartSdk" +import { setupManifest } from "../manifest/setupManifest" + +export type Manifest = any +export const sdk = StartSdk.of() + .withManifest( + setupManifest({ + id: "testOutput", + title: "", + version: "1.0", + releaseNotes: "", + license: "", + replaces: [], + wrapperRepo: "", + upstreamRepo: "", + supportSite: "", + marketingSite: "", + donationUrl: null, + description: { + short: "", + long: "", + }, + assets: { + icon: "", + instructions: "", + license: "", + }, + containers: {}, + volumes: {}, + alerts: { + install: null, + update: null, + uninstall: null, + restore: null, + start: null, + stop: null, + }, + dependencies: { + remoteTest: { + description: "", + requirement: { how: "", type: "opt-in" }, + version: "1.0", + }, + }, + }), + ) + .withStore<{ storeRoot: { storeLeaf: "value" } }>() + .withVault<{ vaultRoot: "value" }>() + .build(true) diff --git a/lib/test/output.wrapperData.ts b/lib/test/output.wrapperData.ts deleted file mode 100644 index 74bb0bf..0000000 --- a/lib/test/output.wrapperData.ts +++ /dev/null @@ -1 +0,0 @@ -export type WrapperData = {} diff --git a/lib/test/setupDependencyConfig.test.ts b/lib/test/setupDependencyConfig.test.ts new file mode 100644 index 0000000..4fac5d0 --- /dev/null +++ b/lib/test/setupDependencyConfig.test.ts @@ -0,0 +1,27 @@ +import { sdk } from "./output.sdk" + +describe("setupDependencyConfig", () => { + test("test", () => { + const testConfig = sdk.Config.of({ + test: sdk.Value.text({ + name: "testValue", + required: false, + }), + }) + + const testConfig2 = sdk.Config.of({ + test2: sdk.Value.text({ + name: "testValue2", + required: false, + }), + }) + const remoteTest = sdk.DependencyConfig.of({ + localConfig: testConfig, + remoteConfig: testConfig2, + dependencyConfig: async ({}) => {}, + }) + sdk.setupDependencyConfig(testConfig, { + remoteTest, + }) + }) +}) diff --git a/lib/test/wrapperData.test.ts b/lib/test/store.test.ts similarity index 50% rename from lib/test/wrapperData.test.ts rename to lib/test/store.test.ts index 76e6360..e3d7931 100644 --- a/lib/test/wrapperData.test.ts +++ b/lib/test/store.test.ts @@ -1,109 +1,110 @@ import { Effects } from "../types" -import { utils } from "../util" +import { createMainUtils } from "../util" +import { utils } from "../util/utils" -type WrapperType = { +type Store = { config: { someValue: "a" | "b" } } +type Vault = { + hello: string +} const todo = (): A => { throw new Error("not implemented") } const noop = () => {} -describe("wrapperData", () => { +describe("Store", () => { test("types", async () => { ;async () => { - utils(todo()).setOwnWrapperData("/config", { + utils(todo()).store.setOwn("/config", { someValue: "a", }) - utils(todo()).setOwnWrapperData( - "/config/someValue", - "b", - ) - utils(todo()).setOwnWrapperData("", { + utils(todo()).store.setOwn("/config/someValue", "b") + utils(todo()).store.setOwn("", { config: { someValue: "b" }, }) - utils(todo()).setOwnWrapperData( + utils(todo()).store.setOwn( "/config/someValue", // @ts-expect-error Type is wrong for the setting value 5, ) - utils(todo()).setOwnWrapperData( + utils(todo()).store.setOwn( // @ts-expect-error Path is wrong "/config/someVae3lue", "someValue", ) - todo().setWrapperData({ + todo().store.set({ path: "/config/someValue", value: "b", }) - todo().setWrapperData({ + todo().store.set({ //@ts-expect-error Path is wrong path: "/config/someValue", //@ts-expect-error Path is wrong value: "someValueIn", }) - todo().setWrapperData({ + todo().store.set({ //@ts-expect-error Path is wrong path: "/config/some2Value", value: "a", }) - ;(await utils(todo()) - .getOwnWrapperData("/config/someValue") + ;(await createMainUtils(todo()) + .store.getOwn("/config/someValue") .const()) satisfies string - ;(await utils(todo()) - .getOwnWrapperData("/config") - .const()) satisfies WrapperType["config"] - await utils(todo()) + ;(await createMainUtils(todo()) + .store.getOwn("/config") + .const()) satisfies Store["config"] + await createMainUtils(todo()) // @ts-expect-error Path is wrong - .getOwnWrapperData("/config/somdsfeValue") + .store.getOwn("/config/somdsfeValue") .const() /// ----------------- ERRORS ----------------- - utils(todo()).setOwnWrapperData("", { + utils(todo()).store.setOwn("", { // @ts-expect-error Type is wrong for the setting value config: { someValue: "notInAOrB" }, }) - utils(todo()).setOwnWrapperData( + utils(todo()).store.setOwn( "/config/someValue", // @ts-expect-error Type is wrong for the setting value "notInAOrB", ) - ;(await utils(todo()) - .getOwnWrapperData("/config/someValue") + ;(await utils(todo()) + .store.getOwn("/config/someValue") // @ts-expect-error Const should normally not be callable .const()) satisfies string - ;(await utils(todo()) - .getOwnWrapperData("/config") + ;(await utils(todo()) + .store.getOwn("/config") // @ts-expect-error Const should normally not be callable - .const()) satisfies WrapperType["config"] - await utils(todo()) + .const()) satisfies Store["config"] + await utils(todo()) // @ts-expect-error Path is wrong - .getOwnWrapperData("/config/somdsfeValue") + .store.getOwn("/config/somdsfeValue") // @ts-expect-error Const should normally not be callable .const() /// - ;(await utils(todo()) - .getOwnWrapperData("/config/someValue") + ;(await utils(todo()) + .store.getOwn("/config/someValue") // @ts-expect-error satisfies type is wrong .const()) satisfies number - ;(await utils(todo()) + ;(await createMainUtils(todo()) // @ts-expect-error Path is wrong - .getOwnWrapperData("/config/") - .const()) satisfies WrapperType["config"] - ;(await todo().getWrapperData({ + .store.getOwn("/config/") + .const()) satisfies Store["config"] + ;(await todo().store.get({ path: "/config/someValue", callback: noop, })) satisfies string - await todo().getWrapperData({ + await todo().store.get({ // @ts-expect-error Path is wrong as in it doesn't match above path: "/config/someV2alue", callback: noop, }) - await todo().getWrapperData({ + await todo().store.get({ // @ts-expect-error Path is wrong as in it doesn't exists in wrapper type path: "/config/someV2alue", callback: noop, diff --git a/lib/health/trigger/TriggerInput.ts b/lib/trigger/TriggerInput.ts similarity index 65% rename from lib/health/trigger/TriggerInput.ts rename to lib/trigger/TriggerInput.ts index 6c07c71..9a52d8c 100644 --- a/lib/health/trigger/TriggerInput.ts +++ b/lib/trigger/TriggerInput.ts @@ -1,4 +1,4 @@ -import { HealthStatus } from "../../types" +import { HealthStatus } from "../types" export type TriggerInput = { lastResult?: HealthStatus diff --git a/lib/health/trigger/changeOnFirstSuccess.ts b/lib/trigger/changeOnFirstSuccess.ts similarity index 94% rename from lib/health/trigger/changeOnFirstSuccess.ts rename to lib/trigger/changeOnFirstSuccess.ts index 010d2c6..28129e3 100644 --- a/lib/health/trigger/changeOnFirstSuccess.ts +++ b/lib/trigger/changeOnFirstSuccess.ts @@ -1,4 +1,3 @@ -import { TriggerInput } from "./TriggerInput" import { Trigger } from "./index" export function changeOnFirstSuccess(o: { diff --git a/lib/health/trigger/cooldownTrigger.ts b/lib/trigger/cooldownTrigger.ts similarity index 100% rename from lib/health/trigger/cooldownTrigger.ts rename to lib/trigger/cooldownTrigger.ts diff --git a/lib/health/trigger/defaultTrigger.ts b/lib/trigger/defaultTrigger.ts similarity index 100% rename from lib/health/trigger/defaultTrigger.ts rename to lib/trigger/defaultTrigger.ts diff --git a/lib/health/trigger/index.ts b/lib/trigger/index.ts similarity index 100% rename from lib/health/trigger/index.ts rename to lib/trigger/index.ts diff --git a/lib/types.ts b/lib/types.ts index 1d7744a..e6a749e 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -45,6 +45,10 @@ export namespace ExpectedExports { } } + export type actionsMetadata = (options: { + effects: Effects + }) => Promise> + /** * This is the entrypoint for the main container. Used to start up something like the service that the * package represents, like running a bitcoind in a bitcoind-wrapper. @@ -81,7 +85,7 @@ export namespace ExpectedExports { /** Auto configure is used to make sure that other dependencies have the values t * that this service could use. */ - export type autoConfig = Record + export type dependencyConfig = Record } export type TimeMs = number export type VersionString = string @@ -90,7 +94,7 @@ export type VersionString = string * AutoConfigure is used as the value to the key of package id, * this is used to make sure that other dependencies have the values that this service could use. */ -export type AutoConfigure = { +export type DependencyConfig = { /** Checks are called to make sure that our dependency is in the correct shape. If a known error is returned we know that the dependency needs modification */ check(options: { effects: Effects @@ -150,10 +154,11 @@ export type DaemonReturned = { term(): Promise } -export type ActionMetaData = { +export type ActionMetadata = { name: string description: string id: string + input: InputSpec allowedStatuses: "only-running" | "only-stopped" | "any" /** * So the ordering of the actions is by alphabetical order of the group, then followed by the alphabetical of the actions @@ -251,26 +256,27 @@ export type Effects = { progress: () => Promise } - /** Get a value in a json like data, can be observed and subscribed */ - getWrapperData(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 & EnsureWrapperDataPath - callback: (config: unknown, previousConfig: unknown) => void - }): Promise> + 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: Path & EnsureStorePath + callback: (config: unknown, previousConfig: unknown) => 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: Path & EnsureStorePath + value: ExtractStore + }): Promise + } getSystemSmtp(input: { callback: (config: unknown, previousConfig: unknown) => void }): Promise - /** Used to store values that can be accessed and subscribed to */ - setWrapperData(options: { - /** Sets the value for the wrapper at the path, it will override, using the [JsonPath](https://jsonpath.com/) */ - path: Path & EnsureWrapperDataPath - value: ExtractWrapperData - }): Promise - getLocalHostname(): Promise getIPHostname(): Promise /** Get the address for another service for tor interfaces */ @@ -314,7 +320,7 @@ export type Effects = { * * @param options */ - exportAction(options: ActionMetaData): Promise + exportAction(options: ActionMetadata): Promise /** * Remove an action that was exported. Used problably during main or during setConfig. */ @@ -377,7 +383,8 @@ export type Effects = { mount(options: { location: { - volumeId: string + /** If there is no volumeId then we mount to runMedia a special mounting location */ + volumeId?: string path: string } target: { @@ -386,13 +393,13 @@ export type Effects = { path: string readonly: boolean } - }): Promise + }): Promise stopped(packageId?: string): Promise vault: { list(): Promise - get(opt: { key: string }): Promise + get(opt: { key: string; callback: () => void }): Promise set(opt: { key: string; value: string }): Promise move(opt: { fromKey: string; toKey: string }): Promise delete(opt: { key: string }): Promise @@ -400,20 +407,20 @@ export type Effects = { } // prettier-ignore -export type ExtractWrapperData = - Path extends `/${infer A }/${infer Rest }` ? (A extends keyof WrapperData ? ExtractWrapperData : never) : - Path extends `/${infer A }` ? (A extends keyof WrapperData ? WrapperData[A] : never) : - Path extends '' ? WrapperData : +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 _EnsureWrapperDataPath = - Path extends`/${infer A }/${infer Rest}` ? (WrapperData extends {[K in A & string]: infer NextWrapperData} ? _EnsureWrapperDataPath : never) : - Path extends `/${infer A }` ? (WrapperData extends {[K in A]: infer B} ? Origin : never) : +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 EnsureWrapperDataPath = _EnsureWrapperDataPath +export type EnsureStorePath = _EnsureStorePath /** rsync options: https://linux.die.net/man/1/rsync */ diff --git a/lib/util/getVault.ts b/lib/util/getVault.ts new file mode 100644 index 0000000..b88a08d --- /dev/null +++ b/lib/util/getVault.ts @@ -0,0 +1,44 @@ +import { Effects, EnsureStorePath } from "../types" + +export class GetVault { + constructor(readonly effects: Effects, readonly key: keyof Vault & string) {} + + /** + * Returns the value of Store at the provided path. Restart the service if the value changes + */ + const() { + return this.effects.vault.get({ + key: this.key, + callback: this.effects.restart, + }) + } + /** + * Returns the value of Store at the provided path. Does nothing if the value changes + */ + once() { + return this.effects.vault.get({ + key: this.key, + callback: () => {}, + }) + } + + /** + * Watches the value of Store at the provided path. Takes a custom callback function to run whenever the value changes + */ + async *watch() { + while (true) { + let callback: () => void + const waitForNext = new Promise((resolve) => { + callback = resolve + }) + yield await this.effects.vault.get({ + key: this.key, + callback: () => callback(), + }) + await waitForNext + } + } +} +export function getVault(effects: Effects, key: keyof Vault & string) { + return new GetVault(effects, key) +} diff --git a/lib/util/getWrapperData.ts b/lib/util/getWrapperData.ts deleted file mode 100644 index f4c8fb9..0000000 --- a/lib/util/getWrapperData.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Parser } from "ts-matches" -import { Effects, EnsureWrapperDataPath, ExtractWrapperData } from "../types" -import { NoAny } from "." - -export class GetWrapperData { - constructor( - readonly effects: Effects, - readonly path: Path & EnsureWrapperDataPath, - readonly options: { - /** Defaults to what ever the package currently in */ - packageId?: string | undefined - } = {}, - ) {} - - /** - * Returns the value of WrapperData at the provided path. Restart the service if the value changes - */ - const() { - return this.effects.getWrapperData({ - ...this.options, - path: this.path as any, - callback: this.effects.restart, - }) - } - /** - * Returns the value of WrapperData at the provided path. Does nothing if the value changes - */ - once() { - return this.effects.getWrapperData({ - ...this.options, - path: this.path as any, - callback: () => {}, - }) - } - - /** - * Watches the value of WrapperData at the provided path. Takes a custom callback function to run whenever the value changes - */ - async *watch() { - while (true) { - let callback: () => void - const waitForNext = new Promise((resolve) => { - callback = resolve - }) - yield await this.effects.getWrapperData({ - ...this.options, - path: this.path as any, - callback: () => callback(), - }) - await waitForNext - } - } -} -export function getWrapperData( - effects: Effects, - path: Path & EnsureWrapperDataPath, - options: { - /** Defaults to what ever the package currently in */ - packageId?: string | undefined - } = {}, -) { - return new GetWrapperData(effects, path as any, options) -} diff --git a/lib/util/index.ts b/lib/util/index.ts index 19eceb5..03dac09 100644 --- a/lib/util/index.ts +++ b/lib/util/index.ts @@ -1,28 +1,12 @@ -import { Parser, string } from "ts-matches" import * as T from "../types" -import FileHelper from "./fileHelper" -import nullIfEmpty from "./nullIfEmpty" -import { GetWrapperData, getWrapperData } from "./getWrapperData" -import { - CheckResult, - checkPortListening, - checkWebUrl, -} from "../health/checkFns" -import { ExtractWrapperData } from "../types" -import { GetSystemSmtp } from "./GetSystemSmtp" import "./nullIfEmpty" import "./fileHelper" -import "./getWrapperData" +import "../store/getStore" import "./deepEqual" import "./deepMerge" import "./once" -import { LocalBinding } from "../mainFn/LocalBinding" -import { LocalPort } from "../mainFn/LocalPort" -import { NetworkBuilder } from "../mainFn/NetworkBuilder" -import { TorHostname } from "../mainFn/TorHostname" -import { DefaultString } from "../config/configTypes" -import { getDefaultString } from "./getDefaultString" +import { utils } from "./utils" // prettier-ignore export type FlattenIntersection = @@ -37,106 +21,9 @@ export const isKnownError = (e: unknown): e is T.KnownError => declare const affine: unique symbol -export type WrapperDataOptionals = { - validator?: Parser> - /** Defaults to what ever the package currently in */ - packageId?: string | undefined -} - -export type Utils = { - createOrUpdateVault: (opts: { - key: string - value: string | null | undefined - generator: DefaultString - }) => Promise - readFile: (fileHelper: FileHelper) => ReturnType["read"]> - writeFile: ( - fileHelper: FileHelper, - data: A, - ) => ReturnType["write"]> - getSystemSmtp: () => GetSystemSmtp & WrapperOverWrite - getWrapperData: ( - packageId: string, - path: T.EnsureWrapperDataPath, - ) => GetWrapperData & WrapperOverWrite - getOwnWrapperData: ( - path: T.EnsureWrapperDataPath, - ) => GetWrapperData & WrapperOverWrite - setOwnWrapperData: ( - path: T.EnsureWrapperDataPath, - value: ExtractWrapperData, - ) => Promise - checkPortListening( - port: number, - options: { - errorMessage: string - successMessage: string - timeoutMessage?: string - timeout?: number - }, - ): Promise - checkWebUrl( - url: string, - options?: { - timeout?: number - successMessage?: string - errorMessage?: string - }, - ): Promise - bindLan: (port: number) => Promise - networkBuilder: () => NetworkBuilder - torHostName: (id: string) => TorHostname - nullIfEmpty: typeof nullIfEmpty -} -export const utils = ( - effects: T.Effects, -): Utils => ({ - createOrUpdateVault: async ({ - key, - value, - generator, - }: { - key: string - value: string | null | undefined - generator: DefaultString - }) => { - if (value) { - await effects.vault.set({ key, value }) - return value - } - if (await effects.vault.get({ key })) { - return null - } - const newValue = getDefaultString(generator) - await effects.vault.set({ key, value: newValue }) - return newValue - }, - getSystemSmtp: () => - new GetSystemSmtp(effects) as GetSystemSmtp & WrapperOverWrite, - readFile: (fileHelper: FileHelper) => fileHelper.read(effects), - writeFile: (fileHelper: FileHelper, data: A) => - fileHelper.write(data, effects), - nullIfEmpty, - getWrapperData: ( - packageId: string, - path: T.EnsureWrapperDataPath, - ) => - getWrapperData(effects, path as any, { - packageId, - }) as any, - getOwnWrapperData: ( - path: T.EnsureWrapperDataPath, - ) => getWrapperData(effects, path as any) as any, - setOwnWrapperData: ( - path: T.EnsureWrapperDataPath, - value: ExtractWrapperData, - ) => effects.setWrapperData({ value, path: path as any }), - checkPortListening: checkPortListening.bind(null, effects), - checkWebUrl: checkWebUrl.bind(null, effects), - bindLan: async (port: number) => LocalPort.bindLan(effects, port), - networkBuilder: () => NetworkBuilder.of(effects), - torHostName: (id: string) => TorHostname.of(effects, id), -}) +export const createUtils = utils +export const createMainUtils = (effects: T.Effects) => + createUtils(effects) type NeverPossible = { [affine]: string } export type NoAny = NeverPossible extends A diff --git a/lib/util/utils.ts b/lib/util/utils.ts new file mode 100644 index 0000000..696a80c --- /dev/null +++ b/lib/util/utils.ts @@ -0,0 +1,160 @@ +import * as T from "../types" +import FileHelper from "./fileHelper" +import nullIfEmpty from "./nullIfEmpty" +import { + CheckResult, + checkPortListening, + checkWebUrl, +} from "../health/checkFns" +import { ExtractStore } from "../types" +import { GetSystemSmtp } from "./GetSystemSmtp" +import { LocalBinding } from "../mainFn/LocalBinding" +import { LocalPort } from "../mainFn/LocalPort" +import { NetworkBuilder } from "../mainFn/NetworkBuilder" +import { TorHostname } from "../mainFn/TorHostname" +import { DefaultString } from "../config/configTypes" +import { getDefaultString } from "./getDefaultString" +import { GetStore, getStore } from "../store/getStore" +import { GetVault, getVault } from "./getVault" +import { + MountDependenciesOut, + mountDependencies, +} from "../dependency/mountDependencies" +import { + ManifestId, + VolumeName, + NamedPath, + Path, +} from "../dependency/setupDependencyMounts" + +export type Utils = { + createOrUpdateVault: (opts: { + key: string + value: string | null | undefined + generator: DefaultString + }) => Promise + readFile: (fileHelper: FileHelper) => ReturnType["read"]> + writeFile: ( + fileHelper: FileHelper, + data: A, + ) => ReturnType["write"]> + getSystemSmtp: () => GetSystemSmtp & WrapperOverWrite + store: { + get: ( + packageId: string, + path: T.EnsureStorePath, + ) => GetStore & WrapperOverWrite + getOwn: ( + path: T.EnsureStorePath, + ) => GetStore & WrapperOverWrite + setOwn: ( + path: T.EnsureStorePath, + value: ExtractStore, + ) => Promise + } + vault: { + get: (key: keyof Vault & string) => GetVault & WrapperOverWrite + set: (key: keyof Vault & string, value: string) => Promise + } + checkPortListening( + port: number, + options: { + errorMessage: string + successMessage: string + timeoutMessage?: string + timeout?: number + }, + ): Promise + checkWebUrl( + url: string, + options?: { + timeout?: number + successMessage?: string + errorMessage?: string + }, + ): Promise + bindLan: (port: number) => Promise + networkBuilder: () => NetworkBuilder + torHostName: (id: string) => TorHostname + nullIfEmpty: typeof nullIfEmpty + mountDependencies: < + In extends + | Record>> + | Record> + | Record + | Path, + >( + value: In, + ) => Promise> +} +export const utils = < + Store = never, + Vault = never, + WrapperOverWrite = { const: never }, +>( + effects: T.Effects, +): Utils => ({ + createOrUpdateVault: async ({ + key, + value, + generator, + }: { + key: string + value: string | null | undefined + generator: DefaultString + }) => { + if (value) { + await effects.vault.set({ key, value }) + return value + } + if (await effects.vault.get({ key, callback: noop })) { + return null + } + const newValue = getDefaultString(generator) + await effects.vault.set({ key, value: newValue }) + return newValue + }, + getSystemSmtp: () => + new GetSystemSmtp(effects) as GetSystemSmtp & WrapperOverWrite, + readFile: (fileHelper: FileHelper) => fileHelper.read(effects), + writeFile: (fileHelper: FileHelper, data: A) => + fileHelper.write(data, effects), + nullIfEmpty, + store: { + get: ( + packageId: string, + path: T.EnsureStorePath, + ) => + getStore(effects, path as any, { + packageId, + }) as any, + getOwn: (path: T.EnsureStorePath) => + getStore(effects, path as any) as any, + setOwn: ( + path: T.EnsureStorePath, + value: ExtractStore, + ) => effects.store.set({ value, path: path as any }), + }, + checkPortListening: checkPortListening.bind(null, effects), + checkWebUrl: checkWebUrl.bind(null, effects), + bindLan: async (port: number) => LocalPort.bindLan(effects, port), + networkBuilder: () => NetworkBuilder.of(effects), + torHostName: (id: string) => TorHostname.of(effects, id), + + vault: { + get: (key: keyof Vault & string) => + getVault(effects, key) as GetVault & WrapperOverWrite, + set: (key: keyof Vault & string, value: string) => + effects.vault.set({ key, value }), + }, + mountDependencies: < + In extends + | Record>> + | Record> + | Record + | Path, + >( + value: In, + ) => mountDependencies(effects, value), +}) +function noop(): void {} diff --git a/package-lock.json b/package-lock.json index 4cfd866..2788e93 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@iarna/toml": "^2.2.5", "ts-matches": "^5.4.1", - "yaml": "^2.2.1" + "yaml": "^2.2.2" }, "devDependencies": { "@types/jest": "^29.4.0", @@ -20,6 +20,7 @@ "ts-node": "^10.9.1", "tsc-multi": "^0.6.1", "tsconfig-paths": "^3.14.2", + "typescript": "^5.0.4", "vitest": "^0.29.2" } }, @@ -4878,17 +4879,16 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=12.20" } }, "node_modules/ufo": { @@ -5219,9 +5219,9 @@ "dev": true }, "node_modules/yaml": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.1.tgz", - "integrity": "sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", + "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==", "engines": { "node": ">= 14" } diff --git a/package.json b/package.json index 8c35c33..c3d8b66 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "dependencies": { "@iarna/toml": "^2.2.5", "ts-matches": "^5.4.1", - "yaml": "^2.2.1" + "yaml": "^2.2.2" }, "prettier": { "trailingComma": "all", @@ -37,6 +37,7 @@ "ts-node": "^10.9.1", "tsc-multi": "^0.6.1", "tsconfig-paths": "^3.14.2", + "typescript": "^5.0.4", "vitest": "^0.29.2" }, "declaration": true diff --git a/scripts/oldSpecToBuilder.ts b/scripts/oldSpecToBuilder.ts index 1bbaf07..ce8ea4e 100644 --- a/scripts/oldSpecToBuilder.ts +++ b/scripts/oldSpecToBuilder.ts @@ -28,19 +28,12 @@ function isString(x: unknown): x is string { export default async function makeFileContentFromOld( inputData: Promise | any, - { - startSdk = "start-sdk", - nested = true, - wrapperData = "../../wrapperData", - } = {}, + { StartSdk = "start-sdk", nested = true } = {}, ) { const outputLines: string[] = [] outputLines.push(` - import { Config } from "${startSdk}/lib/config/builder/config" -import { List } from "${startSdk}/lib/config/builder/list" -import { Value } from "${startSdk}/lib/config/builder/value" -import { Variants } from "${startSdk}/lib/config/builder/variants" - import {WrapperData} from '${wrapperData}' +import { sdk } from "${StartSdk}" +const {Config, List, Value, Variants} = sdk `) const data = await inputData