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