Merge pull request #8 from Start9Labs/sdk-builder

Sdk builder
This commit is contained in:
J H
2023-05-10 16:53:42 -06:00
committed by GitHub
51 changed files with 1720 additions and 756 deletions

View File

@@ -9,7 +9,7 @@
```ts ```ts
{ {
startSdk: "start-sdk/lib", StartSdk: "start-sdk/lib",
} }
``` ```

487
lib/StartSdk.ts Normal file
View File

@@ -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 any[], Then, Else> =
T extends [] ? Else :
T extends [never, ...Array<any>] ? Then :
T extends [any, ...infer U] ? AnyNeverCond<U,Then, Else> :
never
export class StartSdk<Manifest extends SDKManifest, Store, Vault> {
private constructor(readonly manifest: Manifest) {}
static of() {
return new StartSdk<never, never, never>(null as never)
}
withManifest<Manifest extends SDKManifest = never>(manifest: Manifest) {
return new StartSdk<Manifest, Store, Vault>(manifest)
}
withStore<Store extends Record<string, any>>() {
return new StartSdk<Manifest, Store, Vault>(this.manifest)
}
withVault<Vault extends Record<string, string>>() {
return new StartSdk<Manifest, Store, Vault>(this.manifest)
}
build(
isReady: AnyNeverCond<[Manifest, Store, Vault], "Build not ready", true>,
) {
return {
configConstants: { smtpConfig },
createAction: <
Store,
ConfigType extends
| Record<string, any>
| Config<any, any, any>
| Config<any, never, never>,
Type extends Record<string, any> = ExtractConfigType<ConfigType>,
>(
metaData: Omit<ActionMetadata, "input"> & {
input: Config<Type, Store, Vault> | Config<Type, never, never>
},
fn: (options: {
effects: Effects
utils: Utils<Store, Vault>
input: Type
}) => Promise<ActionResult>,
) => createAction<Store, Vault, ConfigType, Type>(metaData, fn),
healthCheck: {
checkPortListening,
checkWebUrl,
of: healthCheck,
runHealthScript,
},
setupActions: (...createdActions: CreatedAction<any, any, any>[]) =>
setupActions<Store, Vault>(...createdActions),
setupBackups: (...args: SetupBackupsParams<Manifest>) =>
setupBackups<Manifest>(...args),
setupConfig: <
ConfigType extends
| Config<any, Store, Vault>
| Config<any, never, never>,
Type extends Record<string, any> = ExtractConfigType<ConfigType>,
>(
spec: ConfigType,
write: Save<Store, Vault, Type, Manifest>,
read: Read<Store, Vault, Type>,
) =>
setupConfig<Store, Vault, ConfigType, Manifest, Type>(
spec,
write,
read,
),
setupConfigRead: <
ConfigSpec extends
| Config<Record<string, any>, any, any>
| Config<Record<string, never>, never, never>,
>(
_configSpec: ConfigSpec,
fn: Read<Store, Vault, ConfigSpec>,
) => fn,
setupConfigSave: <
ConfigSpec extends
| Config<Record<string, any>, any, any>
| Config<Record<string, never>, never, never>,
>(
_configSpec: ConfigSpec,
fn: Save<Store, Vault, ConfigSpec, Manifest>,
) => fn,
setupDependencyConfig: <Input extends Record<string, any>>(
config: Config<Input, Store, Vault>,
autoConfigs: {
[K in keyof Manifest["dependencies"]]: DependencyConfig<
Store,
Vault,
Input,
any
>
},
) =>
setupDependencyConfig<Store, Vault, Input, Manifest>(
config,
autoConfigs,
),
setupDependencyMounts,
setupInit: (
migrations: Migrations<Store, Vault>,
install: Install<Store, Vault>,
uninstall: Uninstall<Store, Vault>,
) => setupInit<Store, Vault>(migrations, install, uninstall),
setupInstall: (fn: InstallFn<Store, Vault>) => Install.of(fn),
setupMain: (
fn: (o: {
effects: Effects
started(onTerm: () => void): null
utils: Utils<Store, Vault, {}>
}) => Promise<Daemons<any>>,
) => setupMain<Store, Vault>(fn),
setupMigrations: <Migrations extends Array<Migration<Store, Vault, any>>>(
...migrations: EnsureUniqueId<Migrations>
) =>
setupMigrations<Store, Vault, Migrations>(this.manifest, ...migrations),
setupUninstall: (fn: UninstallFn<Store, Vault>) =>
setupUninstall<Store, Vault>(fn),
trigger: {
defaultTrigger,
cooldownTrigger,
changeOnFirstSuccess,
},
Backups: {
volumes: (...volumeNames: Array<keyof Manifest["volumes"] & string>) =>
Backups.volumes<Manifest>(...volumeNames),
addSets: (
...options: BackupSet<keyof Manifest["volumes"] & string>[]
) => Backups.addSets<Manifest>(...options),
withOptions: (options?: Partial<BackupOptions>) =>
Backups.with_options<Manifest>(options),
},
Config: {
of: <
Spec extends Record<
string,
Value<any, Store, Vault> | Value<any, never, never>
>,
>(
spec: Spec,
) => Config.of<Spec, Store, Vault>(spec),
},
Daemons: { of: Daemons.of },
DependencyConfig: {
of<
LocalConfig extends Record<string, any>,
RemoteConfig extends Record<string, any>,
>({
localConfig,
remoteConfig,
dependencyConfig,
}: {
localConfig: Config<LocalConfig, Store, Vault>
remoteConfig: Config<RemoteConfig, any, any>
dependencyConfig: (options: {
effects: Effects
localConfig: LocalConfig
remoteConfig: RemoteConfig
utils: Utils<Store, Vault>
}) => Promise<void | DeepPartial<RemoteConfig>>
}) {
return new DependencyConfig<Store, Vault, LocalConfig, RemoteConfig>(
dependencyConfig,
)
},
},
List: {
text: List.text,
number: List.number,
obj: <Type extends Record<string, any>>(
a: {
name: string
description?: string | null
warning?: string | null
/** Default [] */
default?: []
minLength?: number | null
maxLength?: number | null
},
aSpec: {
spec: Config<Type, Store, Vault>
displayAs?: null | string
uniqueBy?: null | UniqueBy
},
) => List.obj<Type, Store, Vault>(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<Store, Vault>(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<Store, Vault>(getA),
},
Migration: {
of: <Version extends ManifestVersion>(options: {
version: Version
up: (opts: {
effects: Effects
utils: Utils<Store, Vault>
}) => Promise<void>
down: (opts: {
effects: Effects
utils: Utils<Store, Vault>
}) => Promise<void>
}) => Migration.of<Store, Vault, Version>(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<Store, Vault>(a),
dynamicText: (
getA: LazyBuild<
Store,
Vault,
{
name: string
description?: string | null
warning?: string | null
required: RequiredDefault<DefaultString>
/** 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<Store, Vault>(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<Store, Vault>(getA),
dynamicNumber: (
getA: LazyBuild<
Store,
Vault,
{
name: string
description?: string | null
warning?: string | null
required: RequiredDefault<number>
min?: number | null
max?: number | null
/** Default = '1' */
step?: string | null
integer: boolean
units?: string | null
placeholder?: string | null
disabled?: false | string
}
>,
) => Value.dynamicNumber<Store, Vault>(getA),
dynamicColor: (
getA: LazyBuild<
Store,
Vault,
{
name: string
description?: string | null
warning?: string | null
required: RequiredDefault<string>
disabled?: false | string
}
>,
) => Value.dynamicColor<Store, Vault>(getA),
dynamicDatetime: (
getA: LazyBuild<
Store,
Vault,
{
name: string
description?: string | null
warning?: string | null
required: RequiredDefault<string>
/** Default = 'datetime-local' */
inputmode?: ValueSpecDatetime["inputmode"]
min?: string | null
max?: string | null
step?: string | null
disabled?: false | string
}
>,
) => Value.dynamicDatetime<Store, Vault>(getA),
dynamicSelect: (
getA: LazyBuild<
Store,
Vault,
{
name: string
description?: string | null
warning?: string | null
required: RequiredDefault<string>
values: Record<string, string>
disabled?: false | string
}
>,
) => Value.dynamicSelect<Store, Vault>(getA),
dynamicMultiselect: (
getA: LazyBuild<
Store,
Vault,
{
name: string
description?: string | null
warning?: string | null
default: string[]
values: Record<string, string>
minLength?: number | null
maxLength?: number | null
disabled?: false | string
}
>,
) => Value.dynamicMultiselect<Store, Vault>(getA),
filteredUnion: <
Required extends RequiredDefault<string>,
Type extends Record<string, any>,
>(
getDisabledFn: LazyBuild<Store, Vault, string[]>,
a: {
name: string
description?: string | null
warning?: string | null
required: Required
},
aVariants:
| Variants<Type, Store, Vault>
| Variants<Type, never, never>,
) =>
Value.filteredUnion<Required, Type, Store, Vault>(
getDisabledFn,
a,
aVariants,
),
},
Variants: {
of: <
VariantValues extends {
[K in string]: {
name: string
spec: Config<any, Store, Vault>
}
},
>(
a: VariantValues,
) => Variants.of<VariantValues, Store, Vault>(a),
},
}
}
}

View File

@@ -1,51 +1,58 @@
import { Config, ExtractConfigType } from "../config/builder/config" import { Config, ExtractConfigType } from "../config/builder/config"
import { ActionMetaData, ActionResult, Effects, ExportedAction } from "../types" import { ActionMetadata, ActionResult, Effects, ExportedAction } from "../types"
import { Utils, utils } from "../util" import { createUtils } from "../util"
import { Utils, utils } from "../util/utils"
export class CreatedAction< export class CreatedAction<
WrapperData, Store,
Vault,
ConfigType extends ConfigType extends
| Record<string, any> | Record<string, any>
| Config<any, WrapperData> | Config<any, Store, Vault>
| Config<any, never>, | Config<any, never, never>,
Type extends Record<string, any> = ExtractConfigType<ConfigType>, Type extends Record<string, any> = ExtractConfigType<ConfigType>,
> { > {
private constructor( private constructor(
public readonly myMetaData: ActionMetaData, public readonly myMetaData: Omit<ActionMetadata, "input">,
readonly fn: (options: { readonly fn: (options: {
effects: Effects effects: Effects
utils: Utils<WrapperData> utils: Utils<Store, Vault>
input: Type input: Type
}) => Promise<ActionResult>, }) => Promise<ActionResult>,
readonly input: Config<Type, WrapperData> | Config<Type, never>, readonly input: Config<Type, Store, Vault>,
) {} ) {}
public validator = this.input.validator public validator = this.input.validator
static of< static of<
WrapperData, Store,
Vault,
ConfigType extends ConfigType extends
| Record<string, any> | Record<string, any>
| Config<any, any> | Config<any, any, any>
| Config<any, never>, | Config<any, never, never>,
Type extends Record<string, any> = ExtractConfigType<ConfigType>, Type extends Record<string, any> = ExtractConfigType<ConfigType>,
>( >(
metaData: Omit<ActionMetaData, "input"> & { metaData: Omit<ActionMetadata, "input"> & {
input: Config<Type, WrapperData> | Config<Type, never> input: Config<Type, Store, Vault> | Config<Type, never, never>
}, },
fn: (options: { fn: (options: {
effects: Effects effects: Effects
utils: Utils<WrapperData> utils: Utils<Store, Vault>
input: Type input: Type
}) => Promise<ActionResult>, }) => Promise<ActionResult>,
) { ) {
const { input, ...rest } = metaData const { input, ...rest } = metaData
return new CreatedAction<WrapperData, ConfigType, Type>(rest, fn, input) return new CreatedAction<Store, Vault, ConfigType, Type>(
rest,
fn,
input as Config<Type, Store, Vault>,
)
} }
exportedAction: ExportedAction = ({ effects, input }) => { exportedAction: ExportedAction = ({ effects, input }) => {
return this.fn({ return this.fn({
effects, effects,
utils: utils<WrapperData>(effects), utils: createUtils(effects),
input: this.validator.unsafeCast(input), input: this.validator.unsafeCast(input),
}) })
} }
@@ -53,15 +60,25 @@ export class CreatedAction<
run = async ({ effects, input }: { effects: Effects; input?: Type }) => { run = async ({ effects, input }: { effects: Effects; input?: Type }) => {
return this.fn({ return this.fn({
effects, effects,
utils: utils<WrapperData>(effects), utils: createUtils(effects),
input: this.validator.unsafeCast(input), input: this.validator.unsafeCast(input),
}) })
} }
async ActionMetadata(options: {
effects: Effects
utils: Utils<Store, Vault>
}): Promise<ActionMetadata> {
return {
...this.myMetaData,
input: await this.input.build(options),
}
}
async getConfig({ effects }: { effects: Effects }) { async getConfig({ effects }: { effects: Effects }) {
return this.input.build({ return this.input.build({
effects, effects,
utils: utils<WrapperData>(effects) as any, utils: createUtils(effects) as any,
}) })
} }
} }

View File

@@ -1,11 +1,13 @@
import { Effects, ExpectedExports, ExportedAction } from "../types" import { Effects, ExpectedExports } from "../types"
import { ActionMetaData } from "../types" import { createUtils } from "../util"
import { once } from "../util/once" import { once } from "../util/once"
import { CreatedAction } from "./createAction" import { CreatedAction } from "./createAction"
export function setupActions(...createdActions: CreatedAction<any, any>[]) { export function setupActions<Store, Vault>(
...createdActions: CreatedAction<Store, Vault, any>[]
) {
const myActions = once(() => { const myActions = once(() => {
const actions: Record<string, CreatedAction<any, any>> = {} const actions: Record<string, CreatedAction<Store, Vault, any>> = {}
for (const action of createdActions) { for (const action of createdActions) {
actions[action.myMetaData.id] = action actions[action.myMetaData.id] = action
} }
@@ -15,8 +17,14 @@ export function setupActions(...createdActions: CreatedAction<any, any>[]) {
get actions() { get actions() {
return myActions() return myActions()
}, },
get actionsMetadata() { async actionsMetadata({ effects }: { effects: Effects }) {
return createdActions.map((x) => x.myMetaData) const utils = createUtils<Store, Vault>(effects)
return Promise.all(
createdActions.map((x) => x.ActionMetadata({ effects, utils })),
)
}, },
} satisfies {
actions: ExpectedExports.actions
actionsMetadata: ExpectedExports.actionsMetadata
} }
} }

View File

@@ -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<WD, Input, NestedConfigs> = {
[key in keyof NestedConfigs & string]: (options: {
effects: Effects
localConfig: Input
remoteConfig: NestedConfigs[key]
utils: Utils<WD>
}) => Promise<void | DeepPartial<NestedConfigs[key]>>
}
export class AutoConfig<WD, Input, NestedConfigs> {
constructor(
readonly configs: AutoConfigFrom<WD, Input, NestedConfigs>,
readonly path: keyof AutoConfigFrom<WD, Input, NestedConfigs>,
) {}
async check(
options: Parameters<AutoConfigure["check"]>[0],
): ReturnType<AutoConfigure["check"]> {
const origConfig = JSON.parse(JSON.stringify(options.localConfig))
const newOptions = {
...options,
utils: utils<WD>(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<AutoConfigure["autoConfigure"]>[0],
): ReturnType<AutoConfigure["autoConfigure"]> {
const newOptions = {
...options,
utils: utils<WD>(options.effects),
localConfig: options.localConfig as Input,
remoteConfig: options.remoteConfig as any,
}
return deepMerge(
{},
options.localConfig,
await this.configs[this.path](newOptions),
)
}
}

View File

@@ -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<WD, Input, NestedConfigs>) {
type C = typeof configs
const answer = { ...configs } as unknown as {
[k in keyof C]: AutoConfig<WD, Input, NestedConfigs>
}
for (const key in configs) {
answer[key as keyof typeof configs] = new AutoConfig<
WD,
Input,
NestedConfigs
>(configs, key as keyof typeof configs)
}
return answer
}

View File

@@ -1,6 +1,5 @@
import { SDKManifest } from "../manifest/ManifestTypes" import { SDKManifest } from "../manifest/ManifestTypes"
import * as T from "../types" import * as T from "../types"
import fs from "fs"
export type BACKUP = "BACKUP" export type BACKUP = "BACKUP"
export const DEFAULT_OPTIONS: T.BackupOptions = { export const DEFAULT_OPTIONS: T.BackupOptions = {
@@ -9,7 +8,7 @@ export const DEFAULT_OPTIONS: T.BackupOptions = {
ignoreExisting: false, ignoreExisting: false,
exclude: [], exclude: [],
} }
type BackupSet<Volumes extends string> = { export type BackupSet<Volumes extends string> = {
srcPath: string srcPath: string
srcVolume: Volumes | BACKUP srcVolume: Volumes | BACKUP
dstPath: string dstPath: string
@@ -41,7 +40,7 @@ type BackupSet<Volumes extends string> = {
export class Backups<M extends SDKManifest> { export class Backups<M extends SDKManifest> {
static BACKUP: BACKUP = "BACKUP" static BACKUP: BACKUP = "BACKUP"
constructor( private constructor(
private options = DEFAULT_OPTIONS, private options = DEFAULT_OPTIONS,
private backupSet = [] as BackupSet<keyof M["volumes"] & string>[], private backupSet = [] as BackupSet<keyof M["volumes"] & string>[],
) {} ) {}
@@ -67,7 +66,9 @@ export class Backups<M extends SDKManifest> {
) { ) {
return new Backups({ ...DEFAULT_OPTIONS, ...options }) return new Backups({ ...DEFAULT_OPTIONS, ...options })
} }
set_options(options?: Partial<T.BackupOptions>) {
static withOptions = Backups.with_options
setOptions(options?: Partial<T.BackupOptions>) {
this.options = { this.options = {
...this.options, ...this.options,
...options, ...options,

View File

@@ -1,28 +1,32 @@
import { ValueSpec } from "../configTypes" import { ValueSpec } from "../configTypes"
import { Utils } from "../../util" import { Utils } from "../../util/utils"
import { Value } from "./value" import { Value } from "./value"
import { _ } from "../../util" import { _ } from "../../util"
import { Effects } from "../../types" import { Effects } from "../../types"
import { Parser, object } from "ts-matches" import { Parser, object } from "ts-matches"
export type LazyBuildOptions<WD> = { export type LazyBuildOptions<Store, Vault> = {
effects: Effects effects: Effects
utils: Utils<WD> utils: Utils<Store, Vault>
} }
export type LazyBuild<WD, ExpectedOut> = ( export type LazyBuild<Store, Vault, ExpectedOut> = (
options: LazyBuildOptions<WD>, options: LazyBuildOptions<Store, Vault>,
) => Promise<ExpectedOut> | ExpectedOut ) => Promise<ExpectedOut> | ExpectedOut
// prettier-ignore // prettier-ignore
export type ExtractConfigType<A extends Record<string, any> | Config<Record<string, any>, any> | Config<Record<string, any>, never>> = export type ExtractConfigType<A extends Record<string, any> | Config<Record<string, any>, any, any> | Config<Record<string, any>, never, never>> =
A extends Config<infer B, any> | Config<infer B, never> ? B : A extends Config<infer B, any, any> | Config<infer B, never, never> ? B :
A A
export type ConfigSpecOf<A extends Record<string, any>, WD = never> = { export type ConfigSpecOf<
[K in keyof A]: Value<A[K], WD> A extends Record<string, any>,
Store = never,
Vault = never,
> = {
[K in keyof A]: Value<A[K], Store, Vault>
} }
export type MaybeLazyValues<A> = LazyBuild<any, A> | A export type MaybeLazyValues<A> = LazyBuild<any, any, A> | A
/** /**
* Configs are the specs that are used by the os configuration form for this service. * Configs are the specs that are used by the os configuration form for this service.
* Here is an example of a simple configuration * Here is an example of a simple configuration
@@ -79,14 +83,16 @@ export const addNodesSpec = Config.of({ hostname: hostname, port: port });
``` ```
*/ */
export class Config<Type extends Record<string, any>, WD> { export class Config<Type extends Record<string, any>, Store, Vault> {
private constructor( private constructor(
private readonly spec: { private readonly spec: {
[K in keyof Type]: Value<Type[K], WD> | Value<Type[K], never> [K in keyof Type]:
| Value<Type[K], Store, Vault>
| Value<Type[K], never, never>
}, },
public validator: Parser<unknown, Type>, public validator: Parser<unknown, Type>,
) {} ) {}
async build(options: LazyBuildOptions<WD>) { async build(options: LazyBuildOptions<Store, Vault>) {
const answer = {} as { const answer = {} as {
[K in keyof Type]: ValueSpec [K in keyof Type]: ValueSpec
} }
@@ -96,9 +102,14 @@ export class Config<Type extends Record<string, any>, WD> {
return answer return answer
} }
static of<Spec extends Record<string, Value<any, any> | Value<any, never>>>( static of<
spec: Spec, Spec extends Record<
) { string,
Value<any, Store, Vault> | Value<any, never, never>
>,
Store,
Vault,
>(spec: Spec) {
const validatorObj = {} as { const validatorObj = {} as {
[K in keyof Spec]: Parser<unknown, any> [K in keyof Spec]: Parser<unknown, any>
} }
@@ -109,14 +120,13 @@ export class Config<Type extends Record<string, any>, WD> {
return new Config< return new Config<
{ {
[K in keyof Spec]: Spec[K] extends [K in keyof Spec]: Spec[K] extends
| Value<infer T, any> | Value<infer T, Store, Vault>
| Value<infer T, never> | Value<infer T, never, never>
? T ? T
: never : never
}, },
{ Store,
[K in keyof Spec]: Spec[K] extends Value<any, infer WD> ? WD : never Vault
}[keyof Spec]
>(spec, validator as any) >(spec, validator as any)
} }
@@ -129,12 +139,12 @@ export class Config<Type extends Record<string, any>, WD> {
required: false, required: false,
}) })
return Config.of<WrapperData>()({ return Config.of<Store>()({
myValue: a.withWrapperData(), myValue: a.withStore(),
}) })
``` ```
*/ */
withWrapperData<NewWrapperData extends WD extends never ? any : WD>() { withStore<NewStore extends Store extends never ? any : Store>() {
return this as any as Config<Type, NewWrapperData> return this as any as Config<Type, NewStore, Vault>
} }
} }

View File

@@ -22,9 +22,9 @@ export const authorizationList = List.string({
export const auth = Value.list(authorizationList); export const auth = Value.list(authorizationList);
``` ```
*/ */
export class List<Type, WD> { export class List<Type, Store, Vault> {
private constructor( private constructor(
public build: LazyBuild<WD, ValueSpecList>, public build: LazyBuild<Store, Vault, ValueSpecList>,
public validator: Parser<unknown, Type>, public validator: Parser<unknown, Type>,
) {} ) {}
static text( static text(
@@ -49,7 +49,7 @@ export class List<Type, WD> {
generate?: null | RandomString generate?: null | RandomString
}, },
) { ) {
return new List<string[], never>(() => { return new List<string[], never, never>(() => {
const spec = { const spec = {
type: "text" as const, type: "text" as const,
placeholder: null, placeholder: null,
@@ -73,9 +73,10 @@ export class List<Type, WD> {
} satisfies ValueSpecListOf<"text"> } satisfies ValueSpecListOf<"text">
}, arrayOf(string)) }, arrayOf(string))
} }
static dynamicText<WD = never>( static dynamicText<Store = never, Vault = never>(
getA: LazyBuild< getA: LazyBuild<
WD, Store,
Vault,
{ {
name: string name: string
description?: string | null description?: string | null
@@ -99,7 +100,7 @@ export class List<Type, WD> {
} }
>, >,
) { ) {
return new List<string[], WD>(async (options) => { return new List<string[], Store, Vault>(async (options) => {
const { spec: aSpec, ...a } = await getA(options) const { spec: aSpec, ...a } = await getA(options)
const spec = { const spec = {
type: "text" as const, type: "text" as const,
@@ -143,7 +144,7 @@ export class List<Type, WD> {
placeholder?: string | null placeholder?: string | null
}, },
) { ) {
return new List<number[], never>(() => { return new List<number[], never, never>(() => {
const spec = { const spec = {
type: "number" as const, type: "number" as const,
placeholder: null, placeholder: null,
@@ -166,9 +167,10 @@ export class List<Type, WD> {
} satisfies ValueSpecListOf<"number"> } satisfies ValueSpecListOf<"number">
}, arrayOf(number)) }, arrayOf(number))
} }
static dynamicNumber<WD = never>( static dynamicNumber<Store = never, Vault = never>(
getA: LazyBuild< getA: LazyBuild<
WD, Store,
Vault,
{ {
name: string name: string
description?: string | null description?: string | null
@@ -189,7 +191,7 @@ export class List<Type, WD> {
} }
>, >,
) { ) {
return new List<number[], WD>(async (options) => { return new List<number[], Store, Vault>(async (options) => {
const { spec: aSpec, ...a } = await getA(options) const { spec: aSpec, ...a } = await getA(options)
const spec = { const spec = {
type: "number" as const, type: "number" as const,
@@ -213,7 +215,7 @@ export class List<Type, WD> {
} }
}, arrayOf(number)) }, arrayOf(number))
} }
static obj<Type extends Record<string, any>, WrapperData>( static obj<Type extends Record<string, any>, Store, Vault>(
a: { a: {
name: string name: string
description?: string | null description?: string | null
@@ -224,12 +226,12 @@ export class List<Type, WD> {
maxLength?: number | null maxLength?: number | null
}, },
aSpec: { aSpec: {
spec: Config<Type, WrapperData> spec: Config<Type, Store, Vault>
displayAs?: null | string displayAs?: null | string
uniqueBy?: null | UniqueBy uniqueBy?: null | UniqueBy
}, },
) { ) {
return new List<Type[], WrapperData>(async (options) => { return new List<Type[], Store, Vault>(async (options) => {
const { spec: previousSpecSpec, ...restSpec } = aSpec const { spec: previousSpecSpec, ...restSpec } = aSpec
const specSpec = await previousSpecSpec.build(options) const specSpec = await previousSpecSpec.build(options)
const spec = { const spec = {
@@ -265,12 +267,12 @@ export class List<Type, WD> {
required: false, required: false,
}) })
return Config.of<WrapperData>()({ return Config.of<Store>()({
myValue: a.withWrapperData(), myValue: a.withStore(),
}) })
``` ```
*/ */
withWrapperData<NewWrapperData extends WD extends never ? any : WD>() { withStore<NewStore extends Store extends never ? any : Store>() {
return this as any as List<Type, NewWrapperData> return this as any as List<Type, NewStore, Vault>
} }
} }

View File

@@ -25,7 +25,7 @@ import {
} from "ts-matches" } from "ts-matches"
import { once } from "../../util/once" import { once } from "../../util/once"
type RequiredDefault<A> = export type RequiredDefault<A> =
| false | false
| { | {
default: A | null default: A | null
@@ -94,9 +94,9 @@ const username = Value.string({
}); });
``` ```
*/ */
export class Value<Type, WD> { export class Value<Type, Store, Vault> {
private constructor( protected constructor(
public build: LazyBuild<WD, ValueSpec>, public build: LazyBuild<Store, Vault, ValueSpec>,
public validator: Parser<unknown, Type>, public validator: Parser<unknown, Type>,
) {} ) {}
static toggle(a: { static toggle(a: {
@@ -108,7 +108,7 @@ export class Value<Type, WD> {
Default is false */ Default is false */
immutable?: boolean immutable?: boolean
}) { }) {
return new Value<boolean, never>( return new Value<boolean, never, never>(
async () => ({ async () => ({
description: null, description: null,
warning: null, warning: null,
@@ -120,9 +120,10 @@ export class Value<Type, WD> {
boolean, boolean,
) )
} }
static dynamicToggle<WD = never>( static dynamicToggle<Store = never, Vault = never>(
a: LazyBuild< a: LazyBuild<
WD, Store,
Vault,
{ {
name: string name: string
description?: string | null description?: string | null
@@ -132,7 +133,7 @@ export class Value<Type, WD> {
} }
>, >,
) { ) {
return new Value<boolean, WD>( return new Value<boolean, Store, Vault>(
async (options) => ({ async (options) => ({
description: null, description: null,
warning: null, warning: null,
@@ -163,7 +164,7 @@ export class Value<Type, WD> {
immutable?: boolean immutable?: boolean
generate?: null | RandomString generate?: null | RandomString
}) { }) {
return new Value<AsRequired<string, Required>, never>( return new Value<AsRequired<string, Required>, never, never>(
async () => ({ async () => ({
type: "text" as const, type: "text" as const,
description: null, description: null,
@@ -183,9 +184,10 @@ export class Value<Type, WD> {
asRequiredParser(string, a), asRequiredParser(string, a),
) )
} }
static dynamicText<WD = never>( static dynamicText<Store = never, Vault = never>(
getA: LazyBuild< getA: LazyBuild<
WD, Store,
Vault,
{ {
name: string name: string
description?: string | null description?: string | null
@@ -204,25 +206,28 @@ export class Value<Type, WD> {
} }
>, >,
) { ) {
return new Value<string | null | undefined, WD>(async (options) => { return new Value<string | null | undefined, Store, Vault>(
const a = await getA(options) async (options) => {
return { const a = await getA(options)
type: "text" as const, return {
description: null, type: "text" as const,
warning: null, description: null,
masked: false, warning: null,
placeholder: null, masked: false,
minLength: null, placeholder: null,
maxLength: null, minLength: null,
patterns: [], maxLength: null,
inputmode: "text", patterns: [],
disabled: false, inputmode: "text",
immutable: false, disabled: false,
generate: a.generate ?? null, immutable: false,
...a, generate: a.generate ?? null,
...requiredLikeToAbove(a.required), ...a,
} ...requiredLikeToAbove(a.required),
}, string.optional()) }
},
string.optional(),
)
} }
static textarea(a: { static textarea(a: {
name: string name: string
@@ -237,7 +242,7 @@ export class Value<Type, WD> {
immutable?: boolean immutable?: boolean
generate?: null | RandomString generate?: null | RandomString
}) { }) {
return new Value<string, never>( return new Value<string, never, never>(
async () => async () =>
({ ({
description: null, description: null,
@@ -254,9 +259,10 @@ export class Value<Type, WD> {
string, string,
) )
} }
static dynamicTextarea<WD = never>( static dynamicTextarea<Store = never, Vault = never>(
getA: LazyBuild< getA: LazyBuild<
WD, Store,
Vault,
{ {
name: string name: string
description?: string | null description?: string | null
@@ -270,7 +276,7 @@ export class Value<Type, WD> {
} }
>, >,
) { ) {
return new Value<string, WD>(async (options) => { return new Value<string, Store, Vault>(async (options) => {
const a = await getA(options) const a = await getA(options)
return { return {
description: null, description: null,
@@ -302,7 +308,7 @@ export class Value<Type, WD> {
Default is false */ Default is false */
immutable?: boolean immutable?: boolean
}) { }) {
return new Value<AsRequired<number, Required>, never>( return new Value<AsRequired<number, Required>, never, never>(
() => ({ () => ({
type: "number" as const, type: "number" as const,
description: null, description: null,
@@ -320,9 +326,10 @@ export class Value<Type, WD> {
asRequiredParser(number, a), asRequiredParser(number, a),
) )
} }
static dynamicNumber<WD = never>( static dynamicNumber<Store = never, Vault = never>(
getA: LazyBuild< getA: LazyBuild<
WD, Store,
Vault,
{ {
name: string name: string
description?: string | null description?: string | null
@@ -339,23 +346,26 @@ export class Value<Type, WD> {
} }
>, >,
) { ) {
return new Value<number | null | undefined, WD>(async (options) => { return new Value<number | null | undefined, Store, Vault>(
const a = await getA(options) async (options) => {
return { const a = await getA(options)
type: "number" as const, return {
description: null, type: "number" as const,
warning: null, description: null,
min: null, warning: null,
max: null, min: null,
step: null, max: null,
units: null, step: null,
placeholder: null, units: null,
disabled: false, placeholder: null,
immutable: false, disabled: false,
...a, immutable: false,
...requiredLikeToAbove(a.required), ...a,
} ...requiredLikeToAbove(a.required),
}, number.optional()) }
},
number.optional(),
)
} }
static color<Required extends RequiredDefault<string>>(a: { static color<Required extends RequiredDefault<string>>(a: {
name: string name: string
@@ -366,7 +376,7 @@ export class Value<Type, WD> {
Default is false */ Default is false */
immutable?: boolean immutable?: boolean
}) { }) {
return new Value<AsRequired<string, Required>, never>( return new Value<AsRequired<string, Required>, never, never>(
() => ({ () => ({
type: "color" as const, type: "color" as const,
description: null, description: null,
@@ -381,9 +391,10 @@ export class Value<Type, WD> {
) )
} }
static dynamicColor<WD = never>( static dynamicColor<Store = never, Vault = never>(
getA: LazyBuild< getA: LazyBuild<
WD, Store,
Vault,
{ {
name: string name: string
description?: string | null description?: string | null
@@ -394,18 +405,21 @@ export class Value<Type, WD> {
} }
>, >,
) { ) {
return new Value<string | null | undefined, WD>(async (options) => { return new Value<string | null | undefined, Store, Vault>(
const a = await getA(options) async (options) => {
return { const a = await getA(options)
type: "color" as const, return {
description: null, type: "color" as const,
warning: null, description: null,
disabled: false, warning: null,
immutable: false, disabled: false,
...a, immutable: false,
...requiredLikeToAbove(a.required), ...a,
} ...requiredLikeToAbove(a.required),
}, string.optional()) }
},
string.optional(),
)
} }
static datetime<Required extends RequiredDefault<string>>(a: { static datetime<Required extends RequiredDefault<string>>(a: {
name: string name: string
@@ -421,7 +435,7 @@ export class Value<Type, WD> {
Default is false */ Default is false */
immutable?: boolean immutable?: boolean
}) { }) {
return new Value<AsRequired<string, Required>, never>( return new Value<AsRequired<string, Required>, never, never>(
() => ({ () => ({
type: "datetime" as const, type: "datetime" as const,
description: null, description: null,
@@ -438,9 +452,10 @@ export class Value<Type, WD> {
asRequiredParser(string, a), asRequiredParser(string, a),
) )
} }
static dynamicDatetime<WD = never>( static dynamicDatetime<Store = never, Vault = never>(
getA: LazyBuild< getA: LazyBuild<
WD, Store,
Vault,
{ {
name: string name: string
description?: string | null description?: string | null
@@ -455,22 +470,25 @@ export class Value<Type, WD> {
} }
>, >,
) { ) {
return new Value<string | null | undefined, WD>(async (options) => { return new Value<string | null | undefined, Store, Vault>(
const a = await getA(options) async (options) => {
return { const a = await getA(options)
type: "datetime" as const, return {
description: null, type: "datetime" as const,
warning: null, description: null,
inputmode: "datetime-local", warning: null,
min: null, inputmode: "datetime-local",
max: null, min: null,
step: null, max: null,
disabled: false, step: null,
immutable: false, disabled: false,
...a, immutable: false,
...requiredLikeToAbove(a.required), ...a,
} ...requiredLikeToAbove(a.required),
}, string.optional()) }
},
string.optional(),
)
} }
static select< static select<
Required extends RequiredDefault<string>, Required extends RequiredDefault<string>,
@@ -485,7 +503,7 @@ export class Value<Type, WD> {
Default is false */ Default is false */
immutable?: boolean immutable?: boolean
}) { }) {
return new Value<AsRequired<keyof B, Required>, never>( return new Value<AsRequired<keyof B, Required>, never, never>(
() => ({ () => ({
description: null, description: null,
warning: null, warning: null,
@@ -503,9 +521,10 @@ export class Value<Type, WD> {
) as any, ) as any,
) )
} }
static dynamicSelect<WD = never>( static dynamicSelect<Store = never, Vault = never>(
getA: LazyBuild< getA: LazyBuild<
WD, Store,
Vault,
{ {
name: string name: string
description?: string | null description?: string | null
@@ -516,18 +535,21 @@ export class Value<Type, WD> {
} }
>, >,
) { ) {
return new Value<string | null | undefined, WD>(async (options) => { return new Value<string | null | undefined, Store, Vault>(
const a = await getA(options) async (options) => {
return { const a = await getA(options)
description: null, return {
warning: null, description: null,
type: "select" as const, warning: null,
disabled: false, type: "select" as const,
immutable: false, disabled: false,
...a, immutable: false,
...requiredLikeToAbove(a.required), ...a,
} ...requiredLikeToAbove(a.required),
}, string.optional()) }
},
string.optional(),
)
} }
static multiselect<Values extends Record<string, string>>(a: { static multiselect<Values extends Record<string, string>>(a: {
name: string name: string
@@ -541,7 +563,7 @@ export class Value<Type, WD> {
Default is false */ Default is false */
immutable?: boolean immutable?: boolean
}) { }) {
return new Value<(keyof Values)[], never>( return new Value<(keyof Values)[], never, never>(
() => ({ () => ({
type: "multiselect" as const, type: "multiselect" as const,
minLength: null, minLength: null,
@@ -557,9 +579,10 @@ export class Value<Type, WD> {
), ),
) )
} }
static dynamicMultiselect<WD = never>( static dynamicMultiselect<Store = never, Vault = never>(
getA: LazyBuild< getA: LazyBuild<
WD, Store,
Vault,
{ {
name: string name: string
description?: string | null description?: string | null
@@ -572,7 +595,7 @@ export class Value<Type, WD> {
} }
>, >,
) { ) {
return new Value<string[], WD>(async (options) => { return new Value<string[], Store, Vault>(async (options) => {
const a = await getA(options) const a = await getA(options)
return { return {
type: "multiselect" as const, type: "multiselect" as const,
@@ -586,15 +609,15 @@ export class Value<Type, WD> {
} }
}, arrayOf(string)) }, arrayOf(string))
} }
static object<Type extends Record<string, any>, WrapperData>( static object<Type extends Record<string, any>, Store, Vault>(
a: { a: {
name: string name: string
description?: string | null description?: string | null
warning?: string | null warning?: string | null
}, },
previousSpec: Config<Type, WrapperData>, previousSpec: Config<Type, Store, Vault>,
) { ) {
return new Value<Type, WrapperData>(async (options) => { return new Value<Type, Store, Vault>(async (options) => {
const spec = await previousSpec.build(options as any) const spec = await previousSpec.build(options as any)
return { return {
type: "object" as const, type: "object" as const,
@@ -605,7 +628,7 @@ export class Value<Type, WD> {
} }
}, previousSpec.validator) }, previousSpec.validator)
} }
static union<Required extends RequiredDefault<string>, Type, WrapperData>( static union<Required extends RequiredDefault<string>, Type, Store, Vault>(
a: { a: {
name: string name: string
description?: string | null description?: string | null
@@ -615,9 +638,9 @@ export class Value<Type, WD> {
Default is false */ Default is false */
immutable?: boolean immutable?: boolean
}, },
aVariants: Variants<Type, WrapperData>, aVariants: Variants<Type, Store, Vault>,
) { ) {
return new Value<AsRequired<Type, Required>, WrapperData>( return new Value<AsRequired<Type, Required>, Store, Vault>(
async (options) => ({ async (options) => ({
type: "union" as const, type: "union" as const,
description: null, description: null,
@@ -630,39 +653,38 @@ export class Value<Type, WD> {
asRequiredParser(aVariants.validator, a), asRequiredParser(aVariants.validator, a),
) )
} }
static filteredUnion<WrapperData = never>( static filteredUnion<
getDisabledFn: LazyBuild<WrapperData, string[]>, Required extends RequiredDefault<string>,
Type extends Record<string, any>,
Store = never,
Vault = never,
>(
getDisabledFn: LazyBuild<Store, Vault, string[]>,
a: {
name: string
description?: string | null
warning?: string | null
required: Required
},
aVariants: Variants<Type, Store, Vault> | Variants<Type, never, never>,
) { ) {
return < return new Value<AsRequired<Type, Required>, Store, Vault>(
Required extends RequiredDefault<string>, async (options) => ({
Type extends Record<string, any>, type: "union" as const,
>( description: null,
a: { warning: null,
name: string ...a,
description?: string | null variants: await aVariants.build(options as any),
warning?: string | null ...requiredLikeToAbove(a.required),
required: Required disabled: (await getDisabledFn(options)) || [],
}, immutable: false,
aVariants: Variants<Type, WrapperData> | Variants<Type, never>, }),
) => { asRequiredParser(aVariants.validator, a),
return new Value<AsRequired<Type, Required>, 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),
)
}
} }
static list<Type, WrapperData>(a: List<Type, WrapperData>) { static list<Type, Store, Vault>(a: List<Type, Store, Vault>) {
return new Value<Type, WrapperData>( return new Value<Type, Store, Vault>(
(options) => a.build(options), (options) => a.build(options),
a.validator, a.validator,
) )
@@ -677,42 +699,12 @@ export class Value<Type, WD> {
required: false, required: false,
}) })
return Config.of<WrapperData>()({ return Config.of<Store>()({
myValue: a.withWrapperData(), myValue: a.withStore(),
}) })
``` ```
*/ */
withWrapperData<NewWrapperData extends WD extends never ? any : WD>() { withStore<NewStore extends Store extends never ? any : Store>() {
return this as any as Value<Type, NewWrapperData> return this as any as Value<Type, NewStore, Vault>
} }
} }
type Wrapper = { test: 1 | "5" }
const valueA = Value.dynamicText<Wrapper>(() => ({
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<Wrapper>(() => [])(
{ name: "a", required: false },
variantForC,
)
const valueB = Value.text({
name: "a",
required: false,
})
const test = Config.of({
a: valueA,
b: valueB,
c: valueC,
})

View File

@@ -51,24 +51,21 @@ export const pruning = Value.union(
); );
``` ```
*/ */
export class Variants<Type, WD> { export class Variants<Type, Store, Vault> {
static text: any
private constructor( private constructor(
public build: LazyBuild<WD, ValueSpecUnion["variants"]>, public build: LazyBuild<Store, Vault, ValueSpecUnion["variants"]>,
public validator: Parser<unknown, Type>, public validator: Parser<unknown, Type>,
) {} ) {}
// A extends {
// [key: string]: {
// name: string
// spec: InputSpec
// }
// },
static of< static of<
VariantValues extends { VariantValues extends {
[K in string]: { [K in string]: {
name: string name: string
spec: Config<any, any> | Config<any, never> spec: Config<any, Store, Vault> | Config<any, never, never>
} }
}, },
Store,
Vault,
>(a: VariantValues) { >(a: VariantValues) {
const validator = anyOf( const validator = anyOf(
...Object.entries(a).map(([name, { spec }]) => ...Object.entries(a).map(([name, { spec }]) =>
@@ -83,21 +80,14 @@ export class Variants<Type, WD> {
{ {
[K in keyof VariantValues]: { [K in keyof VariantValues]: {
unionSelectKey: K unionSelectKey: K
unionValueKey: VariantValues[K]["spec"] extends // prettier-ignore
| Config<infer B, any> unionValueKey:
| Config<infer B, never> VariantValues[K]["spec"] extends (Config<infer B, Store,Vault> | Config<infer B, never, never>) ? B :
? B never
: never
} }
}[keyof VariantValues], }[keyof VariantValues],
{ Store,
[K in keyof VariantValues]: VariantValues[K] extends Config< Vault
any,
infer C
>
? C
: never
}[keyof VariantValues]
>(async (options) => { >(async (options) => {
const variants = {} as { const variants = {} as {
[K in keyof VariantValues]: { name: string; spec: InputSpec } [K in keyof VariantValues]: { name: string; spec: InputSpec }
@@ -121,12 +111,12 @@ export class Variants<Type, WD> {
required: false, required: false,
}) })
return Config.of<WrapperData>()({ return Config.of<Store>()({
myValue: a.withWrapperData(), myValue: a.withStore(),
}) })
``` ```
*/ */
withWrapperData<NewWrapperData extends WD extends never ? any : WD>() { withStore<NewStore extends Store extends never ? any : Store>() {
return this as any as Variants<Type, NewWrapperData> return this as any as Variants<Type, NewStore, Vault>
} }
} }

View File

@@ -3,10 +3,11 @@ import { Config, ConfigSpecOf } from "./builder/config"
import { Value } from "./builder/value" import { Value } from "./builder/value"
import { Variants } from "./builder/variants" import { Variants } from "./builder/variants"
export const smtpConfig = Value.filteredUnion(async ({ effects, utils }) => { export const smtpConfig = Value.filteredUnion(
const smtp = await utils.getSystemSmtp().once() async ({ effects, utils }) => {
return smtp ? [] : ["system"] const smtp = await utils.getSystemSmtp().once()
})( return smtp ? [] : ["system"]
},
{ {
name: "SMTP", name: "SMTP",
description: "Optionally provide an SMTP server for sending email", 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({}) }, system: { name: "System Credentials", spec: Config.of({}) },
custom: { custom: {
name: "Custom Credentials", name: "Custom Credentials",
spec: Config.of<ConfigSpecOf<SmtpValue>>({ spec: Config.of<ConfigSpecOf<SmtpValue>, never, never>({
server: Value.text({ server: Value.text({
name: "SMTP Server", name: "SMTP Server",
required: { required: {

View File

@@ -1,12 +1,14 @@
import { SDKManifest } from "../manifest/ManifestTypes" import { SDKManifest } from "../manifest/ManifestTypes"
import { Dependency } from "../types" import { Dependency } from "../types"
export type Dependencies<T extends SDKManifest> = { export type ConfigDependencies<T extends SDKManifest> = {
exists(id: keyof T["dependencies"]): Dependency exists(id: keyof T["dependencies"]): Dependency
running(id: keyof T["dependencies"]): Dependency running(id: keyof T["dependencies"]): Dependency
} }
export const dependenciesSet = <T extends SDKManifest>(): Dependencies<T> => ({ export const configDependenciesSet = <
T extends SDKManifest,
>(): ConfigDependencies<T> => ({
exists(id: keyof T["dependencies"]) { exists(id: keyof T["dependencies"]) {
return { return {
id, id,

View File

@@ -1,5 +1,5 @@
import "./builder" import "./builder"
import "./setupConfig" import "./setupConfig"
import "./dependencies" import "./configDependencies"
import "./constants" import "./configConstants"

View File

@@ -1,8 +1,8 @@
import { Effects, ExpectedExports } from "../types" import { Effects, ExpectedExports } from "../types"
import { SDKManifest } from "../manifest/ManifestTypes" import { SDKManifest } from "../manifest/ManifestTypes"
import * as D from "./dependencies" import * as D from "./configDependencies"
import { Config, ExtractConfigType } from "./builder/config" import { Config, ExtractConfigType } from "./builder/config"
import { Utils, utils } from "../util" import { Utils, utils } from "../util/utils"
import nullIfEmpty from "../util/nullIfEmpty" import nullIfEmpty from "../util/nullIfEmpty"
declare const dependencyProof: unique symbol declare const dependencyProof: unique symbol
@@ -11,30 +11,32 @@ export type DependenciesReceipt = void & {
} }
export type Save< export type Save<
WD, Store,
Vault,
A extends A extends
| Record<string, any> | Record<string, any>
| Config<Record<string, any>, any> | Config<Record<string, any>, any, any>
| Config<Record<string, never>, never>, | Config<Record<string, never>, never, never>,
Manifest extends SDKManifest, Manifest extends SDKManifest,
> = (options: { > = (options: {
effects: Effects effects: Effects
input: ExtractConfigType<A> & Record<string, any> input: ExtractConfigType<A> & Record<string, any>
utils: Utils<WD> utils: Utils<Store, Vault>
dependencies: D.Dependencies<Manifest> dependencies: D.ConfigDependencies<Manifest>
}) => Promise<{ }) => Promise<{
dependenciesReceipt: DependenciesReceipt dependenciesReceipt: DependenciesReceipt
restart: boolean restart: boolean
}> }>
export type Read< export type Read<
WD, Store,
Vault,
A extends A extends
| Record<string, any> | Record<string, any>
| Config<Record<string, any>, any> | Config<Record<string, any>, any, any>
| Config<Record<string, any>, never>, | Config<Record<string, any>, never, never>,
> = (options: { > = (options: {
effects: Effects effects: Effects
utils: Utils<WD> utils: Utils<Store, Vault>
}) => Promise<void | (ExtractConfigType<A> & Record<string, any>)> }) => Promise<void | (ExtractConfigType<A> & Record<string, any>)>
/** /**
* We want to setup a config export with a get and set, this * We want to setup a config export with a get and set, this
@@ -44,17 +46,18 @@ export type Read<
* @returns * @returns
*/ */
export function setupConfig< export function setupConfig<
WD, Store,
Vault,
ConfigType extends ConfigType extends
| Record<string, any> | Record<string, any>
| Config<any, any> | Config<any, any, any>
| Config<any, never>, | Config<any, never, never>,
Manifest extends SDKManifest, Manifest extends SDKManifest,
Type extends Record<string, any> = ExtractConfigType<ConfigType>, Type extends Record<string, any> = ExtractConfigType<ConfigType>,
>( >(
spec: Config<Type, WD> | Config<Type, never>, spec: Config<Type, Store, Vault> | Config<Type, never, never>,
write: Save<WD, Type, Manifest>, write: Save<Store, Vault, Type, Manifest>,
read: Read<WD, Type>, read: Read<Store, Vault, Type>,
) { ) {
const validator = spec.validator const validator = spec.validator
return { return {
@@ -66,15 +69,15 @@ export function setupConfig<
const { restart } = await write({ const { restart } = await write({
input: JSON.parse(JSON.stringify(input)), input: JSON.parse(JSON.stringify(input)),
effects, effects,
utils: utils<WD>(effects), utils: utils(effects),
dependencies: D.dependenciesSet<Manifest>(), dependencies: D.configDependenciesSet<Manifest>(),
}) })
if (restart) { if (restart) {
await effects.restart() await effects.restart()
} }
}) as ExpectedExports.setConfig, }) as ExpectedExports.setConfig,
getConfig: (async ({ effects }) => { getConfig: (async ({ effects }) => {
const myUtils = utils<WD>(effects) const myUtils = utils<Store, Vault>(effects)
const configValue = nullIfEmpty( const configValue = nullIfEmpty(
(await read({ effects, utils: myUtils })) || null, (await read({ effects, utils: myUtils })) || null,
) )

View File

@@ -0,0 +1,43 @@
import { Effects } from "../types"
import {
Path,
ManifestId,
VolumeName,
NamedPath,
matchPath,
} from "./setupDependencyMounts"
export type MountDependenciesOut<A> =
// prettier-ignore
A extends Path ? string : A extends Record<string, unknown> ? {
[P in keyof A]: MountDependenciesOut<A[P]>;
} : never
export async function mountDependencies<
In extends
| Record<ManifestId, Record<VolumeName, Record<NamedPath, Path>>>
| Record<VolumeName, Record<NamedPath, Path>>
| Record<NamedPath, Path>
| Path,
>(effects: Effects, value: In): Promise<MountDependenciesOut<In>> {
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<In>
}
return Object.fromEntries(
Object.entries(value).map(([key, value]) => [
key,
mountDependencies(effects, value),
]),
) as Record<string, unknown> as MountDependenciesOut<In>
}

View File

@@ -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<M extends Path> = {
[PId in M["manifest"]["id"]]: {
[V in M["volume"]]: {
[N in M["name"]]: M
}
}
}
type ValidIfNotInNested<
Building,
M extends Path,
> = Building extends BuildPath<M> ? never : M
class SetupDependencyMounts<Building> {
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<typeof newPath>
return new SetupDependencyMounts(building)
}
build() {
return this.building
}
}
export function setupDependencyMounts() {
return SetupDependencyMounts.of()
}

View File

@@ -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<string, any>,
RemoteConfig extends Record<string, any>,
> {
constructor(
readonly dependencyConfig: (options: {
effects: Effects
localConfig: Input
remoteConfig: RemoteConfig
utils: Utils<Store, Vault>
}) => Promise<void | DeepPartial<RemoteConfig>>,
) {}
async check(
options: Parameters<DependencyConfigType["check"]>[0],
): ReturnType<DependencyConfigType["check"]> {
const origConfig = JSON.parse(JSON.stringify(options.localConfig))
const newOptions = {
...options,
utils: utils<Store, Vault>(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<DependencyConfigType["autoConfigure"]>[0],
): ReturnType<DependencyConfigType["autoConfigure"]> {
const newOptions = {
...options,
utils: utils<Store, Vault>(options.effects),
localConfig: options.localConfig as Input,
remoteConfig: options.remoteConfig as any,
}
return deepMerge(
{},
options.remoteConfig,
await this.dependencyConfig(newOptions),
)
}
}

View File

@@ -5,5 +5,5 @@ export type ReadonlyDeep<A> =
export type MaybePromise<A> = Promise<A> | A export type MaybePromise<A> = Promise<A> | A
export type Message = string export type Message = string
import "./AutoConfig" import "./DependencyConfig"
import "./setupAutoConfig" import "./setupDependencyConfig"

View File

@@ -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<string, any>,
Manifest extends SDKManifest,
>(
_config: Config<Input, Store, Vault>,
autoConfigs: {
[key in keyof Manifest["dependencies"] & string]: DependencyConfig<
Store,
Vault,
Input,
any
>
},
): ExpectedExports.dependencyConfig {
return autoConfigs
}

View File

@@ -2,9 +2,9 @@ import { InterfaceReceipt } from "../mainFn/interfaceReceipt"
import { Daemon, Effects } from "../types" import { Daemon, Effects } from "../types"
import { CheckResult } from "./checkFns/CheckResult" import { CheckResult } from "./checkFns/CheckResult"
import { HealthReceipt } from "./HealthReceipt" import { HealthReceipt } from "./HealthReceipt"
import { Trigger } from "./trigger" import { Trigger } from "../trigger"
import { TriggerInput } from "./trigger/TriggerInput" import { TriggerInput } from "../trigger/TriggerInput"
import { defaultTrigger } from "./trigger/defaultTrigger" import { defaultTrigger } from "../trigger/defaultTrigger"
import { once } from "../util/once" import { once } from "../util/once"
export function healthCheck(o: { export function healthCheck(o: {

View File

@@ -11,7 +11,7 @@ import "@iarna/toml"
import "./types" import "./types"
import "./util" import "./util"
import "yaml" import "yaml"
import "./autoconfig" import "./dependencyConfig"
import "./actions" import "./actions"
import "./manifest" import "./manifest"
import "./inits" import "./inits"

View File

@@ -1,27 +1,40 @@
import { ManifestVersion } from "../../manifest/ManifestTypes" import { ManifestVersion } from "../../manifest/ManifestTypes"
import { Effects } from "../../types" import { Effects } from "../../types"
import { Utils } from "../../util/utils"
export class Migration<Version extends ManifestVersion> { export class Migration<Store, Vault, Version extends ManifestVersion> {
constructor( constructor(
readonly options: { readonly options: {
version: Version version: Version
up: (opts: { effects: Effects }) => Promise<void> up: (opts: {
down: (opts: { effects: Effects }) => Promise<void> effects: Effects
utils: Utils<Store, Vault>
}) => Promise<void>
down: (opts: {
effects: Effects
utils: Utils<Store, Vault>
}) => Promise<void>
}, },
) {} ) {}
static of<Version extends ManifestVersion>(options: { static of<Store, Vault, Version extends ManifestVersion>(options: {
version: Version version: Version
up: (opts: { effects: Effects }) => Promise<void> up: (opts: {
down: (opts: { effects: Effects }) => Promise<void> effects: Effects
utils: Utils<Store, Vault>
}) => Promise<void>
down: (opts: {
effects: Effects
utils: Utils<Store, Vault>
}) => Promise<void>
}) { }) {
return new Migration(options) return new Migration<Store, Vault, Version>(options)
} }
async up(opts: { effects: Effects }) { async up(opts: { effects: Effects; utils: Utils<Store, Vault> }) {
this.up(opts) this.up(opts)
} }
async down(opts: { effects: Effects }) { async down(opts: { effects: Effects; utils: Utils<Store, Vault> }) {
this.down(opts) this.down(opts)
} }
} }

View File

@@ -1,39 +1,44 @@
import { setupActions } from "../../actions/setupActions"
import { EmVer } from "../../emverLite/mod" import { EmVer } from "../../emverLite/mod"
import { SDKManifest } from "../../manifest/ManifestTypes" import { SDKManifest } from "../../manifest/ManifestTypes"
import { ExpectedExports } from "../../types" import { ExpectedExports } from "../../types"
import { createUtils } from "../../util"
import { once } from "../../util/once" import { once } from "../../util/once"
import { Migration } from "./Migration" import { Migration } from "./Migration"
export class Migrations { export class Migrations<Store, Vault> {
private constructor( private constructor(
readonly manifest: SDKManifest, readonly manifest: SDKManifest,
readonly migrations: Array<Migration<any>>, readonly migrations: Array<Migration<Store, Vault, any>>,
) {} ) {}
private sortedMigrations = once(() => { private sortedMigrations = once(() => {
const migrationsAsVersions = (this.migrations as Array<Migration<any>>).map( const migrationsAsVersions = (
(x) => [EmVer.parse(x.options.version), x] as const, this.migrations as Array<Migration<Store, Vault, any>>
) ).map((x) => [EmVer.parse(x.options.version), x] as const)
migrationsAsVersions.sort((a, b) => a[0].compareForSort(b[0])) migrationsAsVersions.sort((a, b) => a[0].compareForSort(b[0]))
return migrationsAsVersions return migrationsAsVersions
}) })
private currentVersion = once(() => EmVer.parse(this.manifest.version)) private currentVersion = once(() => EmVer.parse(this.manifest.version))
static of<Migrations extends Array<Migration<any>>>( static of<
manifest: SDKManifest, Store,
...migrations: EnsureUniqueId<Migrations> Vault,
) { Migrations extends Array<Migration<Store, Vault, any>>,
return new Migrations(manifest, migrations as Array<Migration<any>>) >(manifest: SDKManifest, ...migrations: EnsureUniqueId<Migrations>) {
return new Migrations(
manifest,
migrations as Array<Migration<Store, Vault, any>>,
)
} }
async init({ async init({
effects, effects,
previousVersion, previousVersion,
}: Parameters<ExpectedExports.init>[0]) { }: Parameters<ExpectedExports.init>[0]) {
const utils = createUtils<Store, Vault>(effects)
if (!!previousVersion) { if (!!previousVersion) {
const previousVersionEmVer = EmVer.parse(previousVersion) const previousVersionEmVer = EmVer.parse(previousVersion)
for (const [_, migration] of this.sortedMigrations() for (const [_, migration] of this.sortedMigrations()
.filter((x) => x[0].greaterThan(previousVersionEmVer)) .filter((x) => x[0].greaterThan(previousVersionEmVer))
.filter((x) => x[0].lessThanOrEqual(this.currentVersion()))) { .filter((x) => x[0].lessThanOrEqual(this.currentVersion()))) {
await migration.up({ effects }) await migration.up({ effects, utils })
} }
} }
} }
@@ -41,29 +46,31 @@ export class Migrations {
effects, effects,
nextVersion, nextVersion,
}: Parameters<ExpectedExports.uninit>[0]) { }: Parameters<ExpectedExports.uninit>[0]) {
const utils = createUtils<Store, Vault>(effects)
if (!!nextVersion) { if (!!nextVersion) {
const nextVersionEmVer = EmVer.parse(nextVersion) const nextVersionEmVer = EmVer.parse(nextVersion)
const reversed = [...this.sortedMigrations()].reverse() const reversed = [...this.sortedMigrations()].reverse()
for (const [_, migration] of reversed for (const [_, migration] of reversed
.filter((x) => x[0].greaterThan(nextVersionEmVer)) .filter((x) => x[0].greaterThan(nextVersionEmVer))
.filter((x) => x[0].lessThanOrEqual(this.currentVersion()))) { .filter((x) => x[0].lessThanOrEqual(this.currentVersion()))) {
await migration.down({ effects }) await migration.down({ effects, utils })
} }
} }
} }
} }
export function setupMigrations<Migrations extends Array<Migration<any>>>( export function setupMigrations<
manifest: SDKManifest, Store,
...migrations: EnsureUniqueId<Migrations> Vault,
) { Migrations extends Array<Migration<Store, Vault, any>>,
return Migrations.of(manifest, ...migrations) >(manifest: SDKManifest, ...migrations: EnsureUniqueId<Migrations>) {
return Migrations.of<Store, Vault, Migrations>(manifest, ...migrations)
} }
// prettier-ignore // prettier-ignore
export type EnsureUniqueId<A, B = A, ids = never> = export type EnsureUniqueId<A, B = A, ids = never> =
B extends [] ? A : B extends [] ? A :
B extends [Migration<infer id>, ...infer Rest] ? ( B extends [Migration<any,any, infer id>, ...infer Rest] ? (
id extends ids ? "One of the ids are not unique"[] : id extends ids ? "One of the ids are not unique"[] :
EnsureUniqueId<A, Rest, id | ids> EnsureUniqueId<A, Rest, id | ids>
) : "There exists a migration that is not a Migration"[] ) : "There exists a migration that is not a Migration"[]

View File

@@ -3,10 +3,10 @@ import { Migrations } from "./migrations/setupMigrations"
import { Install } from "./setupInstall" import { Install } from "./setupInstall"
import { Uninstall } from "./setupUninstall" import { Uninstall } from "./setupUninstall"
export function setupInit<WrapperData>( export function setupInit<Store, Vault>(
migrations: Migrations, migrations: Migrations<Store, Vault>,
install: Install<WrapperData>, install: Install<Store, Vault>,
uninstall: Uninstall<WrapperData>, uninstall: Uninstall<Store, Vault>,
): { ): {
init: ExpectedExports.init init: ExpectedExports.init
uninit: ExpectedExports.uninit uninit: ExpectedExports.uninit

View File

@@ -1,13 +1,13 @@
import { Effects, ExpectedExports } from "../types" import { Effects, ExpectedExports } from "../types"
import { Utils, utils } from "../util" import { Utils, utils } from "../util/utils"
export type InstallFn<WrapperData> = (opts: { export type InstallFn<Store, Vault> = (opts: {
effects: Effects effects: Effects
utils: Utils<WrapperData> utils: Utils<Store, Vault>
}) => Promise<void> }) => Promise<void>
export class Install<WrapperData> { export class Install<Store, Vault> {
private constructor(readonly fn: InstallFn<WrapperData>) {} private constructor(readonly fn: InstallFn<Store, Vault>) {}
static of<WrapperData>(fn: InstallFn<WrapperData>) { static of<Store, Vault>(fn: InstallFn<Store, Vault>) {
return new Install(fn) return new Install(fn)
} }
@@ -15,10 +15,14 @@ export class Install<WrapperData> {
effects, effects,
previousVersion, previousVersion,
}: Parameters<ExpectedExports.init>[0]) { }: Parameters<ExpectedExports.init>[0]) {
if (!previousVersion) await this.fn({ effects, utils: utils(effects) }) if (!previousVersion)
await this.fn({
effects,
utils: utils(effects),
})
} }
} }
export function setupInstall<WrapperData>(fn: InstallFn<WrapperData>) { export function setupInstall<Store, Vault>(fn: InstallFn<Store, Vault>) {
return Install.of(fn) return Install.of(fn)
} }

View File

@@ -1,13 +1,13 @@
import { Effects, ExpectedExports } from "../types" import { Effects, ExpectedExports } from "../types"
import { Utils, utils } from "../util" import { Utils, utils } from "../util/utils"
export type UninstallFn<WrapperData> = (opts: { export type UninstallFn<Store, Vault> = (opts: {
effects: Effects effects: Effects
utils: Utils<WrapperData> utils: Utils<Store, Vault>
}) => Promise<void> }) => Promise<void>
export class Uninstall<WrapperData> { export class Uninstall<Store, Vault> {
private constructor(readonly fn: UninstallFn<WrapperData>) {} private constructor(readonly fn: UninstallFn<Store, Vault>) {}
static of<WrapperData>(fn: UninstallFn<WrapperData>) { static of<Store, Vault>(fn: UninstallFn<Store, Vault>) {
return new Uninstall(fn) return new Uninstall(fn)
} }
@@ -15,10 +15,14 @@ export class Uninstall<WrapperData> {
effects, effects,
nextVersion, nextVersion,
}: Parameters<ExpectedExports.uninit>[0]) { }: Parameters<ExpectedExports.uninit>[0]) {
if (!nextVersion) await this.fn({ effects, utils: utils(effects) }) if (!nextVersion)
await this.fn({
effects,
utils: utils(effects),
})
} }
} }
export function setupUninstall<WrapperData>(fn: UninstallFn<WrapperData>) { export function setupUninstall<Store, Vault>(fn: UninstallFn<Store, Vault>) {
return Uninstall.of(fn) return Uninstall.of(fn)
} }

View File

@@ -1,8 +1,8 @@
import { HealthReceipt } from "../health/HealthReceipt" import { HealthReceipt } from "../health/HealthReceipt"
import { CheckResult } from "../health/checkFns" import { CheckResult } from "../health/checkFns"
import { Trigger } from "../health/trigger" import { Trigger } from "../trigger"
import { TriggerInput } from "../health/trigger/TriggerInput" import { TriggerInput } from "../trigger/TriggerInput"
import { defaultTrigger } from "../health/trigger/defaultTrigger" import { defaultTrigger } from "../trigger/defaultTrigger"
import { DaemonReturned, Effects, ValidIfNoStupidEscape } from "../types" import { DaemonReturned, Effects, ValidIfNoStupidEscape } from "../types"
import { InterfaceReceipt } from "./interfaceReceipt" import { InterfaceReceipt } from "./interfaceReceipt"
type Daemon<Ids extends string, Command extends string, Id extends string> = { type Daemon<Ids extends string, Command extends string, Id extends string> = {

View File

@@ -1,5 +1,6 @@
import { Effects, ExpectedExports } from "../types" 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 { Daemons } from "./Daemons"
import "./exportInterfaces" import "./exportInterfaces"
import "./LocalBinding" import "./LocalBinding"
@@ -22,17 +23,17 @@ import "./Daemons"
* @param fn * @param fn
* @returns * @returns
*/ */
export const setupMain = <WrapperData>( export const setupMain = <Store, Vault>(
fn: (o: { fn: (o: {
effects: Effects effects: Effects
started(onTerm: () => void): null started(onTerm: () => void): null
utils: Utils<WrapperData, {}> utils: Utils<Store, Vault, {}>
}) => Promise<Daemons<any>>, }) => Promise<Daemons<any>>,
): ExpectedExports.main => { ): ExpectedExports.main => {
return async (options) => { return async (options) => {
const result = await fn({ const result = await fn({
...options, ...options,
utils: utils<WrapperData, {}>(options.effects), utils: createMainUtils<Store, Vault>(options.effects),
}) })
await result.build().then((x) => x.wait()) await result.build().then((x) => x.wait())
} }

View File

@@ -1,5 +1,5 @@
import { ValidEmVer } from "../emverLite/mod" import { ValidEmVer } from "../emverLite/mod"
import { ActionMetaData } from "../types" import { ActionMetadata } from "../types"
export interface Container { export interface Container {
/** This should be pointing to a docker container name */ /** This should be pointing to a docker container name */
@@ -14,65 +14,68 @@ export interface Container {
export type ManifestVersion = ValidEmVer 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 */ /** 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 */ /** 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 /** 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 * - see documentation: https://github.com/Start9Labs/emver-rs. This value will change with each release of
* the service * the service
*/ */
version: ManifestVersion readonly version: ManifestVersion
/** Release notes for the update - can be a string, paragraph or URL */ /** 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.*/ /** 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 */ /** A list of normie (hosted, SaaS, custodial, etc) services this services intends to replace */
replaces: string[] readonly replaces: Readonly<string[]>
/** The Start9 wrapper repository URL for the package. This repo contains the manifest file (this), /** 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 * any scripts necessary for configuration, backups, actions, or health checks (more below). This key
* must exist. But could be embedded into the source repository * 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 */ /** 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 */ /** 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 */ /** 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 */ /** 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. */ /**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 */ /**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 */ /** 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). /** 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 * 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. * denoted, it will be defaulted to the values denoted below.
*/ */
assets: { readonly assets: {
icon: string // file path /** This is the file path for the icon that will be this packages icon on the ui */
instructions: string // file path readonly icon: string
license: string // file path /** 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 */ /** Defines the containers needed to run the main and mounted volumes */
containers: Record<string, Container> readonly containers: Record<string, Container>
/** This denotes any data, asset, or pointer volumes that should be connected when the "docker run" command is invoked */ /** This denotes any data, asset, or pointer volumes that should be connected when the "docker run" command is invoked */
volumes: Record<string, "data" | "assets"> readonly volumes: Record<string, "data" | "assets">
actions: Array<ActionMetaData>
alerts: { readonly alerts: {
install: string | null readonly install: string | null
update: string | null readonly update: string | null
uninstall: string | null readonly uninstall: string | null
restore: string | null readonly restore: string | null
start: string | null readonly start: string | null
stop: string | null readonly stop: string | null
} }
dependencies: Record<string, ManifestDependency> readonly dependencies: Readonly<Record<string, ManifestDependency>>
} }
export interface ManifestDependency { export interface ManifestDependency {

61
lib/store/getStore.ts Normal file
View File

@@ -0,0 +1,61 @@
import { Effects, EnsureStorePath } from "../types"
export class GetStore<Store, Path extends string> {
constructor(
readonly effects: Effects,
readonly path: Path & EnsureStorePath<Store, Path>,
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<Store, Path>({
...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<Store, Path>({
...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<void>((resolve) => {
callback = resolve
})
yield await this.effects.store.get<Store, Path>({
...this.options,
path: this.path as any,
callback: () => callback(),
})
await waitForNext
}
}
}
export function getStore<Store, Path extends string>(
effects: Effects,
path: Path & EnsureStorePath<Store, Path>,
options: {
/** Defaults to what ever the package currently in */
packageId?: string | undefined
} = {},
) {
return new GetStore<Store, Path>(effects, path as any, options)
}

View File

@@ -4,9 +4,7 @@ import { List } from "../config/builder/list"
import { Value } from "../config/builder/value" import { Value } from "../config/builder/value"
import { Variants } from "../config/builder/variants" import { Variants } from "../config/builder/variants"
import { ValueSpec } from "../config/configTypes" import { ValueSpec } from "../config/configTypes"
import { Parser } from "ts-matches"
type test = unknown | { test: 5 }
describe("builder tests", () => { describe("builder tests", () => {
test("text", async () => { test("text", async () => {
const bitcoinPropertiesBuilt: { const bitcoinPropertiesBuilt: {
@@ -299,7 +297,7 @@ describe("values", () => {
utils: "utils", utils: "utils",
} as any } as any
test("toggle", async () => { test("toggle", async () => {
const value = Value.dynamicToggle<{}>(async () => ({ const value = Value.dynamicToggle(async () => ({
name: "Testing", name: "Testing",
description: null, description: null,
warning: null, warning: null,
@@ -364,7 +362,7 @@ describe("values", () => {
}) })
}) })
test("color", async () => { test("color", async () => {
const value = Value.dynamicColor<null>(async () => ({ const value = Value.dynamicColor(async () => ({
name: "Testing", name: "Testing",
required: false, required: false,
description: null, description: null,
@@ -385,7 +383,7 @@ describe("values", () => {
test("datetime", async () => { test("datetime", async () => {
const value = Value.dynamicDatetime<{ test: "a" }>(async ({ utils }) => { const value = Value.dynamicDatetime<{ test: "a" }>(async ({ utils }) => {
;async () => { ;async () => {
;(await utils.getOwnWrapperData("/test").once()) satisfies "a" ;(await utils.store.getOwn("/test").once()) satisfies "a"
} }
return { return {
@@ -500,7 +498,8 @@ describe("values", () => {
}) })
describe("filtering", () => { describe("filtering", () => {
test("union", async () => { test("union", async () => {
const value = Value.filteredUnion(() => ["a", "c"])( const value = Value.filteredUnion(
() => ["a", "c"],
{ {
name: "Testing", name: "Testing",
required: { default: null }, required: { default: null },

View File

@@ -423,7 +423,6 @@ oldSpecToBuilder(
}, },
{ {
// convert this to `start-sdk/lib` for conversions // convert this to `start-sdk/lib` for conversions
startSdk: "../..", StartSdk: "./output.sdk",
wrapperData: "./output.wrapperData",
}, },
) )

View File

@@ -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
}>
}
})
})

49
lib/test/output.sdk.ts Normal file
View File

@@ -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)

View File

@@ -1 +0,0 @@
export type WrapperData = {}

View File

@@ -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,
})
})
})

View File

@@ -1,109 +1,110 @@
import { Effects } from "../types" import { Effects } from "../types"
import { utils } from "../util" import { createMainUtils } from "../util"
import { utils } from "../util/utils"
type WrapperType = { type Store = {
config: { config: {
someValue: "a" | "b" someValue: "a" | "b"
} }
} }
type Vault = {
hello: string
}
const todo = <A>(): A => { const todo = <A>(): A => {
throw new Error("not implemented") throw new Error("not implemented")
} }
const noop = () => {} const noop = () => {}
describe("wrapperData", () => { describe("Store", () => {
test("types", async () => { test("types", async () => {
;async () => { ;async () => {
utils<WrapperType>(todo<Effects>()).setOwnWrapperData("/config", { utils<Store>(todo<Effects>()).store.setOwn("/config", {
someValue: "a", someValue: "a",
}) })
utils<WrapperType>(todo<Effects>()).setOwnWrapperData( utils<Store>(todo<Effects>()).store.setOwn("/config/someValue", "b")
"/config/someValue", utils<Store>(todo<Effects>()).store.setOwn("", {
"b",
)
utils<WrapperType>(todo<Effects>()).setOwnWrapperData("", {
config: { someValue: "b" }, config: { someValue: "b" },
}) })
utils<WrapperType>(todo<Effects>()).setOwnWrapperData( utils<Store>(todo<Effects>()).store.setOwn(
"/config/someValue", "/config/someValue",
// @ts-expect-error Type is wrong for the setting value // @ts-expect-error Type is wrong for the setting value
5, 5,
) )
utils<WrapperType>(todo<Effects>()).setOwnWrapperData( utils(todo<Effects>()).store.setOwn(
// @ts-expect-error Path is wrong // @ts-expect-error Path is wrong
"/config/someVae3lue", "/config/someVae3lue",
"someValue", "someValue",
) )
todo<Effects>().setWrapperData<WrapperType, "/config/someValue">({ todo<Effects>().store.set<Store, "/config/someValue">({
path: "/config/someValue", path: "/config/someValue",
value: "b", value: "b",
}) })
todo<Effects>().setWrapperData<WrapperType, "/config/some2Value">({ todo<Effects>().store.set<Store, "/config/some2Value">({
//@ts-expect-error Path is wrong //@ts-expect-error Path is wrong
path: "/config/someValue", path: "/config/someValue",
//@ts-expect-error Path is wrong //@ts-expect-error Path is wrong
value: "someValueIn", value: "someValueIn",
}) })
todo<Effects>().setWrapperData<WrapperType, "/config/someValue">({ todo<Effects>().store.set<Store, "/config/someValue">({
//@ts-expect-error Path is wrong //@ts-expect-error Path is wrong
path: "/config/some2Value", path: "/config/some2Value",
value: "a", value: "a",
}) })
;(await utils<WrapperType, {}>(todo<Effects>()) ;(await createMainUtils<Store, Vault>(todo<Effects>())
.getOwnWrapperData("/config/someValue") .store.getOwn("/config/someValue")
.const()) satisfies string .const()) satisfies string
;(await utils<WrapperType, {}>(todo<Effects>()) ;(await createMainUtils<Store, Vault>(todo<Effects>())
.getOwnWrapperData("/config") .store.getOwn("/config")
.const()) satisfies WrapperType["config"] .const()) satisfies Store["config"]
await utils<WrapperType, {}>(todo<Effects>()) await createMainUtils(todo<Effects>())
// @ts-expect-error Path is wrong // @ts-expect-error Path is wrong
.getOwnWrapperData("/config/somdsfeValue") .store.getOwn("/config/somdsfeValue")
.const() .const()
/// ----------------- ERRORS ----------------- /// ----------------- ERRORS -----------------
utils<WrapperType>(todo<Effects>()).setOwnWrapperData("", { utils<Store>(todo<Effects>()).store.setOwn("", {
// @ts-expect-error Type is wrong for the setting value // @ts-expect-error Type is wrong for the setting value
config: { someValue: "notInAOrB" }, config: { someValue: "notInAOrB" },
}) })
utils<WrapperType>(todo<Effects>()).setOwnWrapperData( utils<Store>(todo<Effects>()).store.setOwn(
"/config/someValue", "/config/someValue",
// @ts-expect-error Type is wrong for the setting value // @ts-expect-error Type is wrong for the setting value
"notInAOrB", "notInAOrB",
) )
;(await utils<WrapperType>(todo<Effects>()) ;(await utils<Store>(todo<Effects>())
.getOwnWrapperData("/config/someValue") .store.getOwn("/config/someValue")
// @ts-expect-error Const should normally not be callable // @ts-expect-error Const should normally not be callable
.const()) satisfies string .const()) satisfies string
;(await utils<WrapperType>(todo<Effects>()) ;(await utils<Store>(todo<Effects>())
.getOwnWrapperData("/config") .store.getOwn("/config")
// @ts-expect-error Const should normally not be callable // @ts-expect-error Const should normally not be callable
.const()) satisfies WrapperType["config"] .const()) satisfies Store["config"]
await utils<WrapperType>(todo<Effects>()) await utils<Store>(todo<Effects>())
// @ts-expect-error Path is wrong // @ts-expect-error Path is wrong
.getOwnWrapperData("/config/somdsfeValue") .store.getOwn("/config/somdsfeValue")
// @ts-expect-error Const should normally not be callable // @ts-expect-error Const should normally not be callable
.const() .const()
/// ///
;(await utils<WrapperType>(todo<Effects>()) ;(await utils<Store>(todo<Effects>())
.getOwnWrapperData("/config/someValue") .store.getOwn("/config/someValue")
// @ts-expect-error satisfies type is wrong // @ts-expect-error satisfies type is wrong
.const()) satisfies number .const()) satisfies number
;(await utils<WrapperType, {}>(todo<Effects>()) ;(await createMainUtils(todo<Effects>())
// @ts-expect-error Path is wrong // @ts-expect-error Path is wrong
.getOwnWrapperData("/config/") .store.getOwn("/config/")
.const()) satisfies WrapperType["config"] .const()) satisfies Store["config"]
;(await todo<Effects>().getWrapperData<WrapperType, "/config/someValue">({ ;(await todo<Effects>().store.get<Store, "/config/someValue">({
path: "/config/someValue", path: "/config/someValue",
callback: noop, callback: noop,
})) satisfies string })) satisfies string
await todo<Effects>().getWrapperData<WrapperType, "/config/someValue">({ await todo<Effects>().store.get<Store, "/config/someValue">({
// @ts-expect-error Path is wrong as in it doesn't match above // @ts-expect-error Path is wrong as in it doesn't match above
path: "/config/someV2alue", path: "/config/someV2alue",
callback: noop, callback: noop,
}) })
await todo<Effects>().getWrapperData<WrapperType, "/config/someV2alue">({ await todo<Effects>().store.get<Store, "/config/someV2alue">({
// @ts-expect-error Path is wrong as in it doesn't exists in wrapper type // @ts-expect-error Path is wrong as in it doesn't exists in wrapper type
path: "/config/someV2alue", path: "/config/someV2alue",
callback: noop, callback: noop,

View File

@@ -1,4 +1,4 @@
import { HealthStatus } from "../../types" import { HealthStatus } from "../types"
export type TriggerInput = { export type TriggerInput = {
lastResult?: HealthStatus lastResult?: HealthStatus

View File

@@ -1,4 +1,3 @@
import { TriggerInput } from "./TriggerInput"
import { Trigger } from "./index" import { Trigger } from "./index"
export function changeOnFirstSuccess(o: { export function changeOnFirstSuccess(o: {

View File

@@ -45,6 +45,10 @@ export namespace ExpectedExports {
} }
} }
export type actionsMetadata = (options: {
effects: Effects
}) => Promise<Array<ActionMetadata>>
/** /**
* This is the entrypoint for the main container. Used to start up something like the service that the * 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. * 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 /** Auto configure is used to make sure that other dependencies have the values t
* that this service could use. * that this service could use.
*/ */
export type autoConfig = Record<PackageId, AutoConfigure> export type dependencyConfig = Record<PackageId, DependencyConfig>
} }
export type TimeMs = number export type TimeMs = number
export type VersionString = string export type VersionString = string
@@ -90,7 +94,7 @@ export type VersionString = string
* AutoConfigure is used as the value to the key of package id, * 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. * 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 */ /** 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: { check(options: {
effects: Effects effects: Effects
@@ -150,10 +154,11 @@ export type DaemonReturned = {
term(): Promise<void> term(): Promise<void>
} }
export type ActionMetaData = { export type ActionMetadata = {
name: string name: string
description: string description: string
id: string id: string
input: InputSpec
allowedStatuses: "only-running" | "only-stopped" | "any" 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 * 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<number> progress: () => Promise<number>
} }
/** Get a value in a json like data, can be observed and subscribed */ store: {
getWrapperData<WrapperData = never, Path extends string = never>(options: { /** Get a value in a json like data, can be observed and subscribed */
/** If there is no packageId it is assumed the current package */ get<Store = never, Path extends string = never>(options: {
packageId?: string /** If there is no packageId it is assumed the current package */
/** The path defaults to root level, using the [JsonPath](https://jsonpath.com/) */ packageId?: string
path: Path & EnsureWrapperDataPath<WrapperData, Path> /** The path defaults to root level, using the [JsonPath](https://jsonpath.com/) */
callback: (config: unknown, previousConfig: unknown) => void path: Path & EnsureStorePath<Store, Path>
}): Promise<ExtractWrapperData<WrapperData, Path>> callback: (config: unknown, previousConfig: unknown) => void
}): Promise<ExtractStore<Store, Path>>
/** Used to store values that can be accessed and subscribed to */
set<Store = never, Path extends string = never>(options: {
/** Sets the value for the wrapper at the path, it will override, using the [JsonPath](https://jsonpath.com/) */
path: Path & EnsureStorePath<Store, Path>
value: ExtractStore<Store, Path>
}): Promise<void>
}
getSystemSmtp(input: { getSystemSmtp(input: {
callback: (config: unknown, previousConfig: unknown) => void callback: (config: unknown, previousConfig: unknown) => void
}): Promise<SmtpValue> }): Promise<SmtpValue>
/** Used to store values that can be accessed and subscribed to */
setWrapperData<WrapperData = never, Path extends string = never>(options: {
/** Sets the value for the wrapper at the path, it will override, using the [JsonPath](https://jsonpath.com/) */
path: Path & EnsureWrapperDataPath<WrapperData, Path>
value: ExtractWrapperData<WrapperData, Path>
}): Promise<void>
getLocalHostname(): Promise<string> getLocalHostname(): Promise<string>
getIPHostname(): Promise<string[]> getIPHostname(): Promise<string[]>
/** Get the address for another service for tor interfaces */ /** Get the address for another service for tor interfaces */
@@ -314,7 +320,7 @@ export type Effects = {
* *
* @param options * @param options
*/ */
exportAction(options: ActionMetaData): Promise<void> exportAction(options: ActionMetadata): Promise<void>
/** /**
* Remove an action that was exported. Used problably during main or during setConfig. * Remove an action that was exported. Used problably during main or during setConfig.
*/ */
@@ -377,7 +383,8 @@ export type Effects = {
mount(options: { mount(options: {
location: { location: {
volumeId: string /** If there is no volumeId then we mount to runMedia a special mounting location */
volumeId?: string
path: string path: string
} }
target: { target: {
@@ -386,13 +393,13 @@ export type Effects = {
path: string path: string
readonly: boolean readonly: boolean
} }
}): Promise<void> }): Promise<string>
stopped(packageId?: string): Promise<boolean> stopped(packageId?: string): Promise<boolean>
vault: { vault: {
list(): Promise<string[]> list(): Promise<string[]>
get(opt: { key: string }): Promise<string> get(opt: { key: string; callback: () => void }): Promise<string>
set(opt: { key: string; value: string }): Promise<void> set(opt: { key: string; value: string }): Promise<void>
move(opt: { fromKey: string; toKey: string }): Promise<void> move(opt: { fromKey: string; toKey: string }): Promise<void>
delete(opt: { key: string }): Promise<void> delete(opt: { key: string }): Promise<void>
@@ -400,20 +407,20 @@ export type Effects = {
} }
// prettier-ignore // prettier-ignore
export type ExtractWrapperData<WrapperData, Path extends string> = export type ExtractStore<Store, Path extends string> =
Path extends `/${infer A }/${infer Rest }` ? (A extends keyof WrapperData ? ExtractWrapperData<WrapperData[A], `/${Rest}`> : never) : Path extends `/${infer A }/${infer Rest }` ? (A extends keyof Store ? ExtractStore<Store[A], `/${Rest}`> : never) :
Path extends `/${infer A }` ? (A extends keyof WrapperData ? WrapperData[A] : never) : Path extends `/${infer A }` ? (A extends keyof Store ? Store[A] : never) :
Path extends '' ? WrapperData : Path extends '' ? Store :
never never
// prettier-ignore // prettier-ignore
type _EnsureWrapperDataPath<WrapperData, Path extends string, Origin extends string> = type _EnsureStorePath<Store, Path extends string, Origin extends string> =
Path extends`/${infer A }/${infer Rest}` ? (WrapperData extends {[K in A & string]: infer NextWrapperData} ? _EnsureWrapperDataPath<NextWrapperData, `/${Rest}`, Origin> : never) : Path extends`/${infer A }/${infer Rest}` ? (Store extends {[K in A & string]: infer NextStore} ? _EnsureStorePath<NextStore, `/${Rest}`, Origin> : never) :
Path extends `/${infer A }` ? (WrapperData extends {[K in A]: infer B} ? Origin : never) : Path extends `/${infer A }` ? (Store extends {[K in A]: infer B} ? Origin : never) :
Path extends '' ? Origin : Path extends '' ? Origin :
never never
// prettier-ignore // prettier-ignore
export type EnsureWrapperDataPath<WrapperData, Path extends string> = _EnsureWrapperDataPath<WrapperData, Path, Path> export type EnsureStorePath<Store, Path extends string> = _EnsureStorePath<Store, Path, Path>
/** rsync options: https://linux.die.net/man/1/rsync /** rsync options: https://linux.die.net/man/1/rsync
*/ */

44
lib/util/getVault.ts Normal file
View File

@@ -0,0 +1,44 @@
import { Effects, EnsureStorePath } from "../types"
export class GetVault<Vault> {
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<void>((resolve) => {
callback = resolve
})
yield await this.effects.vault.get({
key: this.key,
callback: () => callback(),
})
await waitForNext
}
}
}
export function getVault<Vault>(effects: Effects, key: keyof Vault & string) {
return new GetVault<Vault>(effects, key)
}

View File

@@ -1,63 +0,0 @@
import { Parser } from "ts-matches"
import { Effects, EnsureWrapperDataPath, ExtractWrapperData } from "../types"
import { NoAny } from "."
export class GetWrapperData<WrapperData, Path extends string> {
constructor(
readonly effects: Effects,
readonly path: Path & EnsureWrapperDataPath<WrapperData, Path>,
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<WrapperData, Path>({
...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<WrapperData, Path>({
...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<void>((resolve) => {
callback = resolve
})
yield await this.effects.getWrapperData<WrapperData, Path>({
...this.options,
path: this.path as any,
callback: () => callback(),
})
await waitForNext
}
}
}
export function getWrapperData<WrapperData, Path extends string>(
effects: Effects,
path: Path & EnsureWrapperDataPath<WrapperData, Path>,
options: {
/** Defaults to what ever the package currently in */
packageId?: string | undefined
} = {},
) {
return new GetWrapperData<WrapperData, Path>(effects, path as any, options)
}

View File

@@ -1,28 +1,12 @@
import { Parser, string } from "ts-matches"
import * as T from "../types" 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 "./nullIfEmpty"
import "./fileHelper" import "./fileHelper"
import "./getWrapperData" import "../store/getStore"
import "./deepEqual" import "./deepEqual"
import "./deepMerge" import "./deepMerge"
import "./once" import "./once"
import { LocalBinding } from "../mainFn/LocalBinding" import { utils } from "./utils"
import { LocalPort } from "../mainFn/LocalPort"
import { NetworkBuilder } from "../mainFn/NetworkBuilder"
import { TorHostname } from "../mainFn/TorHostname"
import { DefaultString } from "../config/configTypes"
import { getDefaultString } from "./getDefaultString"
// prettier-ignore // prettier-ignore
export type FlattenIntersection<T> = export type FlattenIntersection<T> =
@@ -37,106 +21,9 @@ export const isKnownError = (e: unknown): e is T.KnownError =>
declare const affine: unique symbol declare const affine: unique symbol
export type WrapperDataOptionals<WrapperData, Path extends string> = { export const createUtils = utils
validator?: Parser<unknown, ExtractWrapperData<WrapperData, Path>> export const createMainUtils = <Store, Vault>(effects: T.Effects) =>
/** Defaults to what ever the package currently in */ createUtils<Store, Vault, {}>(effects)
packageId?: string | undefined
}
export type Utils<WD, WrapperOverWrite = { const: never }> = {
createOrUpdateVault: (opts: {
key: string
value: string | null | undefined
generator: DefaultString
}) => Promise<null | string>
readFile: <A>(fileHelper: FileHelper<A>) => ReturnType<FileHelper<A>["read"]>
writeFile: <A>(
fileHelper: FileHelper<A>,
data: A,
) => ReturnType<FileHelper<A>["write"]>
getSystemSmtp: () => GetSystemSmtp & WrapperOverWrite
getWrapperData: <Path extends string>(
packageId: string,
path: T.EnsureWrapperDataPath<WD, Path>,
) => GetWrapperData<WD, Path> & WrapperOverWrite
getOwnWrapperData: <Path extends string>(
path: T.EnsureWrapperDataPath<WD, Path>,
) => GetWrapperData<WD, Path> & WrapperOverWrite
setOwnWrapperData: <Path extends string | never>(
path: T.EnsureWrapperDataPath<WD, Path>,
value: ExtractWrapperData<WD, Path>,
) => Promise<void>
checkPortListening(
port: number,
options: {
errorMessage: string
successMessage: string
timeoutMessage?: string
timeout?: number
},
): Promise<CheckResult>
checkWebUrl(
url: string,
options?: {
timeout?: number
successMessage?: string
errorMessage?: string
},
): Promise<CheckResult>
bindLan: (port: number) => Promise<LocalBinding>
networkBuilder: () => NetworkBuilder
torHostName: (id: string) => TorHostname
nullIfEmpty: typeof nullIfEmpty
}
export const utils = <WrapperData = never, WrapperOverWrite = { const: never }>(
effects: T.Effects,
): Utils<WrapperData, WrapperOverWrite> => ({
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: <A>(fileHelper: FileHelper<A>) => fileHelper.read(effects),
writeFile: <A>(fileHelper: FileHelper<A>, data: A) =>
fileHelper.write(data, effects),
nullIfEmpty,
getWrapperData: <WrapperData = never, Path extends string = never>(
packageId: string,
path: T.EnsureWrapperDataPath<WrapperData, Path>,
) =>
getWrapperData<WrapperData, Path>(effects, path as any, {
packageId,
}) as any,
getOwnWrapperData: <Path extends string>(
path: T.EnsureWrapperDataPath<WrapperData, Path>,
) => getWrapperData<WrapperData, Path>(effects, path as any) as any,
setOwnWrapperData: <Path extends string | never>(
path: T.EnsureWrapperDataPath<WrapperData, Path>,
value: ExtractWrapperData<WrapperData, Path>,
) => effects.setWrapperData<WrapperData, Path>({ 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),
})
type NeverPossible = { [affine]: string } type NeverPossible = { [affine]: string }
export type NoAny<A> = NeverPossible extends A export type NoAny<A> = NeverPossible extends A

160
lib/util/utils.ts Normal file
View File

@@ -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<Store, Vault, WrapperOverWrite = { const: never }> = {
createOrUpdateVault: (opts: {
key: string
value: string | null | undefined
generator: DefaultString
}) => Promise<null | string>
readFile: <A>(fileHelper: FileHelper<A>) => ReturnType<FileHelper<A>["read"]>
writeFile: <A>(
fileHelper: FileHelper<A>,
data: A,
) => ReturnType<FileHelper<A>["write"]>
getSystemSmtp: () => GetSystemSmtp & WrapperOverWrite
store: {
get: <Path extends string>(
packageId: string,
path: T.EnsureStorePath<Store, Path>,
) => GetStore<Store, Path> & WrapperOverWrite
getOwn: <Path extends string>(
path: T.EnsureStorePath<Store, Path>,
) => GetStore<Store, Path> & WrapperOverWrite
setOwn: <Path extends string | never>(
path: T.EnsureStorePath<Store, Path>,
value: ExtractStore<Store, Path>,
) => Promise<void>
}
vault: {
get: (key: keyof Vault & string) => GetVault<Vault> & WrapperOverWrite
set: (key: keyof Vault & string, value: string) => Promise<void>
}
checkPortListening(
port: number,
options: {
errorMessage: string
successMessage: string
timeoutMessage?: string
timeout?: number
},
): Promise<CheckResult>
checkWebUrl(
url: string,
options?: {
timeout?: number
successMessage?: string
errorMessage?: string
},
): Promise<CheckResult>
bindLan: (port: number) => Promise<LocalBinding>
networkBuilder: () => NetworkBuilder
torHostName: (id: string) => TorHostname
nullIfEmpty: typeof nullIfEmpty
mountDependencies: <
In extends
| Record<ManifestId, Record<VolumeName, Record<NamedPath, Path>>>
| Record<VolumeName, Record<NamedPath, Path>>
| Record<NamedPath, Path>
| Path,
>(
value: In,
) => Promise<MountDependenciesOut<In>>
}
export const utils = <
Store = never,
Vault = never,
WrapperOverWrite = { const: never },
>(
effects: T.Effects,
): Utils<Store, Vault, WrapperOverWrite> => ({
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: <A>(fileHelper: FileHelper<A>) => fileHelper.read(effects),
writeFile: <A>(fileHelper: FileHelper<A>, data: A) =>
fileHelper.write(data, effects),
nullIfEmpty,
store: {
get: <Path extends string = never>(
packageId: string,
path: T.EnsureStorePath<Store, Path>,
) =>
getStore<Store, Path>(effects, path as any, {
packageId,
}) as any,
getOwn: <Path extends string>(path: T.EnsureStorePath<Store, Path>) =>
getStore<Store, Path>(effects, path as any) as any,
setOwn: <Path extends string | never>(
path: T.EnsureStorePath<Store, Path>,
value: ExtractStore<Store, Path>,
) => effects.store.set<Store, Path>({ 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<Vault>(effects, key) as GetVault<Vault> & WrapperOverWrite,
set: (key: keyof Vault & string, value: string) =>
effects.vault.set({ key, value }),
},
mountDependencies: <
In extends
| Record<ManifestId, Record<VolumeName, Record<NamedPath, Path>>>
| Record<VolumeName, Record<NamedPath, Path>>
| Record<NamedPath, Path>
| Path,
>(
value: In,
) => mountDependencies(effects, value),
})
function noop(): void {}

18
package-lock.json generated
View File

@@ -11,7 +11,7 @@
"dependencies": { "dependencies": {
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",
"ts-matches": "^5.4.1", "ts-matches": "^5.4.1",
"yaml": "^2.2.1" "yaml": "^2.2.2"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^29.4.0", "@types/jest": "^29.4.0",
@@ -20,6 +20,7 @@
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"tsc-multi": "^0.6.1", "tsc-multi": "^0.6.1",
"tsconfig-paths": "^3.14.2", "tsconfig-paths": "^3.14.2",
"typescript": "^5.0.4",
"vitest": "^0.29.2" "vitest": "^0.29.2"
} }
}, },
@@ -4878,17 +4879,16 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "4.9.5", "version": "5.0.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==",
"dev": true, "dev": true,
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
}, },
"engines": { "engines": {
"node": ">=4.2.0" "node": ">=12.20"
} }
}, },
"node_modules/ufo": { "node_modules/ufo": {
@@ -5219,9 +5219,9 @@
"dev": true "dev": true
}, },
"node_modules/yaml": { "node_modules/yaml": {
"version": "2.2.1", "version": "2.2.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.1.tgz", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz",
"integrity": "sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw==", "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==",
"engines": { "engines": {
"node": ">= 14" "node": ">= 14"
} }

View File

@@ -22,7 +22,7 @@
"dependencies": { "dependencies": {
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",
"ts-matches": "^5.4.1", "ts-matches": "^5.4.1",
"yaml": "^2.2.1" "yaml": "^2.2.2"
}, },
"prettier": { "prettier": {
"trailingComma": "all", "trailingComma": "all",
@@ -37,6 +37,7 @@
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"tsc-multi": "^0.6.1", "tsc-multi": "^0.6.1",
"tsconfig-paths": "^3.14.2", "tsconfig-paths": "^3.14.2",
"typescript": "^5.0.4",
"vitest": "^0.29.2" "vitest": "^0.29.2"
}, },
"declaration": true "declaration": true

View File

@@ -28,19 +28,12 @@ function isString(x: unknown): x is string {
export default async function makeFileContentFromOld( export default async function makeFileContentFromOld(
inputData: Promise<any> | any, inputData: Promise<any> | any,
{ { StartSdk = "start-sdk", nested = true } = {},
startSdk = "start-sdk",
nested = true,
wrapperData = "../../wrapperData",
} = {},
) { ) {
const outputLines: string[] = [] const outputLines: string[] = []
outputLines.push(` outputLines.push(`
import { Config } from "${startSdk}/lib/config/builder/config" import { sdk } from "${StartSdk}"
import { List } from "${startSdk}/lib/config/builder/list" const {Config, List, Value, Variants} = sdk
import { Value } from "${startSdk}/lib/config/builder/value"
import { Variants } from "${startSdk}/lib/config/builder/variants"
import {WrapperData} from '${wrapperData}'
`) `)
const data = await inputData const data = await inputData