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, BackupOptions, DeepPartial, MaybePromise, ServiceInterfaceId, PackageId, } from "./types" import * as patterns from "./util/patterns" import { DependencyConfig, Update } from "./dependencies/DependencyConfig" import { BackupSet, Backups } from "./backup/Backups" import { smtpConfig } from "./config/configConstants" import { Daemons } from "./mainFn/Daemons" import { healthCheck, HealthCheckParams } from "./health/HealthCheck" import { checkPortListening } 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 } from "./inits/setupInstall" import { setupActions } from "./actions/setupActions" import { setupDependencyConfig } from "./dependencies/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, { DependenciesReceipt, Read, Save, } from "./config/setupConfig" import { InterfacesReceipt, SetInterfaces, setupInterfaces, } from "./interfaces/setupInterfaces" import { successFailure } from "./trigger/successFailure" import { HealthReceipt } from "./health/HealthReceipt" import { MultiHost, Scheme } from "./interfaces/Host" import { ServiceInterfaceBuilder } from "./interfaces/ServiceInterfaceBuilder" import { GetSystemSmtp } from "./util/GetSystemSmtp" import nullIfEmpty from "./util/nullIfEmpty" import { GetServiceInterface, getServiceInterface, } from "./util/getServiceInterface" import { getServiceInterfaces } from "./util/getServiceInterfaces" import { getStore } from "./store/getStore" import { CommandOptions, MountOptions, Overlay } from "./util/Overlay" import { splitCommand } from "./util/splitCommand" import { Mounts } from "./mainFn/Mounts" import { Dependency } from "./Dependency" import * as T from "./types" import { testTypeVersion, ValidateExVer } from "./exver" import { ExposedStorePaths } from "./store/setupExposeStore" import { PathBuilder, extractJsonPath, pathBuilder } from "./store/PathBuilder" import { checkAllDependencies } from "./dependencies/dependencies" import { health } from "." import { GetSslCertificate } from "./util/GetSslCertificate" export const SDKVersion = testTypeVersion("0.3.6") // prettier-ignore type AnyNeverCond = T extends [] ? Else : T extends [never, ...Array] ? Then : T extends [any, ...infer U] ? AnyNeverCond : never export type ServiceInterfaceType = "ui" | "p2p" | "api" export type MainEffects = Effects & { _type: "main" clearCallbacks: () => Promise } export type Signals = NodeJS.Signals export const SIGTERM: Signals = "SIGTERM" export const SIGKILL: Signals = "SIGKILL" export const NO_TIMEOUT = -1 function removeCallbackTypes(effects: E) { return (t: T) => { if ("_type" in effects && effects._type === "main") { return t as E extends MainEffects ? T : Omit } else { if ("const" in t) { delete t.const } if ("watch" in t) { delete t.watch } return t as E extends MainEffects ? T : Omit } } } 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) } build(isReady: AnyNeverCond<[Manifest, Store], "Build not ready", true>) { type DependencyType = { [K in keyof { [K in keyof Manifest["dependencies"]]: Manifest["dependencies"][K]["optional"] extends false ? K : never }]: Dependency } & { [K in keyof { [K in keyof Manifest["dependencies"]]: Manifest["dependencies"][K]["optional"] extends true ? K : never }]?: Dependency } return { checkAllDependencies, serviceInterface: { getOwn: (effects: E, id: ServiceInterfaceId) => removeCallbackTypes(effects)( getServiceInterface(effects, { id, }), ), get: ( effects: E, opts: { id: ServiceInterfaceId; packageId: PackageId }, ) => removeCallbackTypes(effects)(getServiceInterface(effects, opts)), getAllOwn: (effects: E) => removeCallbackTypes(effects)(getServiceInterfaces(effects, {})), getAll: ( effects: E, opts: { packageId: PackageId }, ) => removeCallbackTypes(effects)(getServiceInterfaces(effects, opts)), }, store: { get: ( effects: E, packageId: string, path: PathBuilder, ) => removeCallbackTypes(effects)( getStore(effects, path, { packageId, }), ), getOwn: ( effects: E, path: PathBuilder, ) => removeCallbackTypes(effects)( getStore(effects, path), ), setOwn: >( effects: E, path: Path, value: Path extends PathBuilder ? Value : never, ) => effects.store.set({ value, path: extractJsonPath(path), }), }, host: { // static: (effects: Effects, id: string) => // new StaticHost({ id, effects }), // single: (effects: Effects, id: string) => // new SingleHost({ id, effects }), multi: (effects: Effects, id: string) => new MultiHost({ id, effects }), }, nullIfEmpty, runCommand: async ( effects: Effects, image: { id: keyof Manifest["images"] & T.ImageId sharedRun?: boolean }, command: T.CommandType, options: CommandOptions & { mounts?: { path: string; options: MountOptions }[] }, ): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> => { return runCommand(effects, image, command, options) }, createAction: < ConfigType extends | Record | Config | Config, Type extends Record = ExtractConfigType, >( id: string, metaData: Omit & { input: Config | Config }, fn: (options: { effects: Effects input: Type }) => Promise, ) => { const { input, ...rest } = metaData return createAction( id, rest, fn, input, ) }, configConstants: { smtpConfig }, createInterface: ( effects: Effects, options: { name: string id: string description: string hasPrimary: boolean disabled: boolean type: ServiceInterfaceType username: null | string path: string search: Record schemeOverride: { ssl: Scheme; noSsl: Scheme } | null masked: boolean }, ) => new ServiceInterfaceBuilder({ ...options, effects }), getSystemSmtp: (effects: E) => removeCallbackTypes(effects)(new GetSystemSmtp(effects)), getSslCerificate: ( effects: E, hostnames: string[], algorithm?: T.Algorithm, ) => removeCallbackTypes(effects)( new GetSslCertificate(effects, hostnames, algorithm), ), createDynamicAction: < ConfigType extends | Record | Config | Config, Type extends Record = ExtractConfigType, >( id: string, metaData: (options: { effects: Effects }) => MaybePromise>, fn: (options: { effects: Effects input: Type }) => Promise, input: Config | Config, ) => { return createAction( id, metaData, fn, input, ) }, HealthCheck: { of(o: HealthCheckParams) { return healthCheck(o) }, }, Dependency: { of(data: Dependency["data"]) { return new Dependency({ ...data }) }, }, healthCheck: { checkPortListening, checkWebUrl, runHealthScript, }, patterns, 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> | Config, never>, >( _configSpec: ConfigSpec, fn: Read, ) => fn, setupConfigSave: < ConfigSpec extends | Config, any> | Config, never>, >( _configSpec: ConfigSpec, fn: Save, ) => fn, setupDependencyConfig: >( config: Config | Config, autoConfigs: { [K in keyof Manifest["dependencies"]]: DependencyConfig< Manifest, Store, Input, any > | null }, ) => setupDependencyConfig(config, autoConfigs), setupDependencies: >( fn: (options: { effects: Effects input: Input | null }) => Promise, ) => { return async (options: { effects: Effects; input: Input }) => { const dependencyType = await fn(options) return await options.effects.setDependencies({ dependencies: Object.entries(dependencyType).map( ([ id, { data: { versionRange, ...x }, }, ]) => ({ id, ...x, ...(x.type === "running" ? { kind: "running", healthChecks: x.healthChecks, } : { kind: "exists", }), versionRange: versionRange.toString(), }), ), }) } }, setupInit: ( migrations: Migrations, install: Install, uninstall: Uninstall, setInterfaces: SetInterfaces, setDependencies: (options: { effects: Effects input: any }) => Promise, exposedStore: ExposedStorePaths, ) => setupInit( migrations, install, uninstall, setInterfaces, setDependencies, exposedStore, ), setupInstall: (fn: InstallFn) => Install.of(fn), setupInterfaces: < ConfigInput extends Record, Output extends InterfacesReceipt, >( config: Config, fn: SetInterfaces, ) => setupInterfaces(config, fn), setupMain: ( fn: (o: { effects: MainEffects started(onTerm: () => PromiseLike): PromiseLike }) => Promise>, ) => setupMain(fn), setupMigrations: < Migrations extends Array>, >( ...migrations: EnsureUniqueId ) => setupMigrations( this.manifest, ...migrations, ), setupProperties: ( fn: (options: { effects: Effects }) => Promise, ): T.ExpectedExports.properties => (options) => fn(options).then(nullifyProperties), setupUninstall: (fn: UninstallFn) => setupUninstall(fn), trigger: { defaultTrigger, cooldownTrigger, changeOnFirstSuccess, successFailure, }, Mounts: { of() { return Mounts.of() }, }, 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 | Value>, >( spec: Spec, ) => Config.of(spec), }, Daemons: { of(config: { effects: Effects started: (onTerm: () => PromiseLike) => PromiseLike healthReceipts: HealthReceipt[] }) { return Daemons.of(config) }, }, DependencyConfig: { of< LocalConfig extends Record, RemoteConfig extends Record, >({ localConfigSpec, remoteConfigSpec, dependencyConfig, update, }: { localConfigSpec: | Config | Config remoteConfigSpec: | Config | Config dependencyConfig: (options: { effects: Effects localConfig: LocalConfig }) => Promise> update?: Update, RemoteConfig> }) { return new DependencyConfig< Manifest, Store, LocalConfig, RemoteConfig >(dependencyConfig, update) }, }, List: { text: List.text, 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, { 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), }, Migration: { of: (options: { version: Version & ValidateExVer up: (opts: { effects: Effects }) => Promise down: (opts: { effects: Effects }) => Promise }) => Migration.of(options), }, StorePath: pathBuilder(), 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, { name: string description?: string | null warning?: string | null default: boolean disabled?: false | string } >, ) => Value.dynamicToggle(a), dynamicText: ( getA: LazyBuild< Store, { 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, { 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, { name: string description?: string | null warning?: string | null required: RequiredDefault min?: number | null max?: number | null /** Default = '1' */ step?: number | null integer: boolean units?: string | null placeholder?: string | null disabled?: false | string } >, ) => Value.dynamicNumber(getA), dynamicColor: ( getA: LazyBuild< Store, { name: string description?: string | null warning?: string | null required: RequiredDefault disabled?: false | string } >, ) => Value.dynamicColor(getA), dynamicDatetime: ( getA: LazyBuild< Store, { name: string description?: string | null warning?: string | null required: RequiredDefault /** Default = 'datetime-local' */ inputmode?: ValueSpecDatetime["inputmode"] min?: string | null max?: string | null disabled?: false | string } >, ) => Value.dynamicDatetime(getA), dynamicSelect: ( getA: LazyBuild< Store, { name: string description?: string | null warning?: string | null required: RequiredDefault values: Record disabled?: false | string } >, ) => Value.dynamicSelect(getA), dynamicMultiselect: ( getA: LazyBuild< Store, { name: string description?: string | null warning?: string | null default: string[] values: Record minLength?: number | null maxLength?: number | null disabled?: false | string } >, ) => 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, ), dynamicUnion: < Required extends RequiredDefault, Type extends Record, >( getA: LazyBuild< Store, { disabled: string[] | false | string name: string description?: string | null warning?: string | null required: Required } >, aVariants: Variants | Variants, ) => Value.dynamicUnion(getA, aVariants), }, Variants: { of: < VariantValues extends { [K in string]: { name: string spec: Config } }, >( a: VariantValues, ) => Variants.of(a), }, } } } export async function runCommand( effects: Effects, image: { id: keyof Manifest["images"] & T.ImageId; sharedRun?: boolean }, command: string | [string, ...string[]], options: CommandOptions & { mounts?: { path: string; options: MountOptions }[] }, ): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> { const commands = splitCommand(command) const overlay = await Overlay.of(effects, image) try { for (let mount of options.mounts || []) { await overlay.mount(mount.options, mount.path) } return await overlay.exec(commands) } finally { await overlay.destroy() } } function nullifyProperties(value: T.SdkPropertiesReturn): T.PropertiesReturn { return Object.fromEntries( Object.entries(value).map(([k, v]) => [k, nullifyProperties_(v)]), ) } function nullifyProperties_(value: T.SdkPropertiesValue): T.PropertiesValue { if (value.type === "string") { return { description: null, copyable: null, qr: null, ...value } } return { description: null, ...value, value: Object.fromEntries( Object.entries(value.value).map(([k, v]) => [k, nullifyProperties_(v)]), ), } }