Convert properties to an action (#2751)

* update actions response types and partially implement in UI

* further remove diagnostic ui

* convert action response nested to array

* prepare action res modal for Alex

* ad dproperties action for Bitcoin

* feat: add action success dialog (#2753)

* feat: add action success dialog

* mocks for string action res and hide properties from actions page

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>

* return null

* remove properties from backend

* misc fixes

* make severity separate argument

* rename ActionRequest to ActionRequestOptions

* add clearRequests

* fix s9pk build

* remove config and properties, introduce action requests

* better ux, better moocks, include icons

* fix dependency types

* add variant for versionCompat

* fix dep icon display and patch operation display

* misc fixes

* misc fixes

* alpha 12

* honor provided input to set values in action

* fix: show full descriptions of action success items (#2758)

* fix type

* fix: fix build:deps command on Windows (#2752)

* fix: fix build:deps command on Windows

* fix: add escaped quotes

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>

* misc db compatibility fixes

---------

Co-authored-by: Alex Inkin <alexander@inkin.ru>
Co-authored-by: Aiden McClelland <me@drbonez.dev>
Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>
This commit is contained in:
Matt Hill
2024-10-17 13:31:56 -06:00
committed by GitHub
parent fb074c8c32
commit 2ba56b8c59
105 changed files with 1385 additions and 1578 deletions

View File

@@ -55,7 +55,6 @@ import { getStore } from "./store/getStore"
import { CommandOptions, MountOptions, SubContainer } from "./util/SubContainer"
import { splitCommand } from "./util"
import { Mounts } from "./mainFn/Mounts"
import { Dependency } from "../../base/lib/dependencies/Dependency"
import { setupDependencies } from "../../base/lib/dependencies/setupDependencies"
import * as T from "../../base/lib/types"
import { testTypeVersion } from "../../base/lib/exver"
@@ -86,12 +85,12 @@ type AnyNeverCond<T extends any[], Then, Else> =
T extends [any, ...infer U] ? AnyNeverCond<U,Then, Else> :
never
export class StartSdk<Manifest extends T.Manifest, Store> {
export class StartSdk<Manifest extends T.SDKManifest, Store> {
private constructor(readonly manifest: Manifest) {}
static of() {
return new StartSdk<never, never>(null as never)
}
withManifest<Manifest extends T.Manifest = never>(manifest: Manifest) {
withManifest<Manifest extends T.SDKManifest = never>(manifest: Manifest) {
return new StartSdk<Manifest, Store>(manifest)
}
withStore<Store extends Record<string, any>>() {
@@ -141,17 +140,39 @@ export class StartSdk<Manifest extends T.Manifest, Store> {
...startSdkEffectWrapper,
action: {
run: actions.runAction,
request: actions.requestAction,
requestOwn: <T extends Omit<T.ActionRequest, "packageId">>(
request: <
T extends Action<T.ActionId, any, any, Record<string, unknown>>,
>(
effects: T.Effects,
request: actions.ActionRequest<T> & {
replayId?: string
},
packageId: T.PackageId,
action: T,
severity: T.ActionSeverity,
options?: actions.ActionRequestOptions<T>,
) =>
actions.requestAction({
effects,
request: { ...request, packageId: this.manifest.id },
packageId,
action,
severity,
options: options,
}),
requestOwn: <
T extends Action<T.ActionId, Store, any, Record<string, unknown>>,
>(
effects: T.Effects,
action: T,
severity: T.ActionSeverity,
options?: actions.ActionRequestOptions<T>,
) =>
actions.requestAction({
effects,
packageId: this.manifest.id,
action,
severity,
options: options,
}),
clearRequest: (effects: T.Effects, ...replayIds: string[]) =>
effects.action.clearRequests({ only: replayIds }),
},
checkDependencies: checkDependencies as <
DependencyId extends keyof Manifest["dependencies"] &
@@ -370,17 +391,6 @@ export class StartSdk<Manifest extends T.Manifest, Store> {
return healthCheck(o)
},
},
Dependency: {
/**
* @description Use this function to create a dependency for the service.
* @property {DependencyType} type
* @property {VersionRange} versionRange
* @property {string[]} healthChecks
*/
of(data: Dependency["data"]) {
return new Dependency({ ...data })
},
},
healthCheck: {
checkPortListening,
checkWebUrl,
@@ -566,37 +576,6 @@ export class StartSdk<Manifest extends T.Manifest, Store> {
started(onTerm: () => PromiseLike<void>): PromiseLike<null>
}) => Promise<Daemons<Manifest, any>>,
) => setupMain<Manifest, Store>(fn),
/**
* @description Use this function to determine which information to expose to the UI in the "Properties" section.
*
* Values can be obtained from anywhere: the Store, the upstream service, or another service.
* @example
* In this example, we retrieve the admin password from the Store and expose it, masked and copyable, to
* the UI as "Admin Password".
*
* ```
export const properties = sdk.setupProperties(async ({ effects }) => {
const store = await sdk.store.getOwn(effects, sdk.StorePath).once()
return {
'Admin Password': {
type: 'string',
value: store.adminPassword,
description: 'Used for logging into the admin UI',
copyable: true,
masked: true,
qr: false,
},
}
})
* ```
*/
setupProperties:
(
fn: (options: { effects: Effects }) => Promise<T.SdkPropertiesReturn>,
): T.ExpectedExports.properties =>
(options) =>
fn(options).then(nullifyProperties),
/**
* Use this function to execute arbitrary logic *once*, on uninstall only. Most services will not use this.
*/
@@ -1057,6 +1036,7 @@ export class StartSdk<Manifest extends T.Manifest, Store> {
* ```
*/
list: Value.list,
hidden: Value.hidden,
dynamicToggle: (
a: LazyBuild<
Store,
@@ -1367,7 +1347,7 @@ export class StartSdk<Manifest extends T.Manifest, Store> {
}
}
export async function runCommand<Manifest extends T.Manifest>(
export async function runCommand<Manifest extends T.SDKManifest>(
effects: Effects,
image: { id: keyof Manifest["images"] & T.ImageId; sharedRun?: boolean },
command: string | [string, ...string[]],
@@ -1385,26 +1365,3 @@ export async function runCommand<Manifest extends T.Manifest>(
(subcontainer) => subcontainer.exec(commands),
)
}
function nullifyProperties(value: T.SdkPropertiesReturn): T.PropertiesReturn {
return Object.fromEntries(
Object.entries(value).map(([k, v]) => [k, nullifyProperties_(v)]),
)
}
function nullifyProperties_(value: T.SdkPropertiesValue): T.PropertiesValue {
if (value.type === "string") {
return {
description: null,
copyable: null,
masked: null,
qr: null,
...value,
}
}
return {
description: null,
...value,
value: Object.fromEntries(
Object.entries(value.value).map(([k, v]) => [k, nullifyProperties_(v)]),
),
}
}

View File

@@ -35,7 +35,7 @@ export type BackupSync<Volumes extends string> = {
* ).build()q
* ```
*/
export class Backups<M extends T.Manifest> {
export class Backups<M extends T.SDKManifest> {
private constructor(
private options = DEFAULT_OPTIONS,
private restoreOptions: Partial<T.SyncOptions> = {},
@@ -43,7 +43,7 @@ export class Backups<M extends T.Manifest> {
private backupSet = [] as BackupSync<M["volumes"][number]>[],
) {}
static withVolumes<M extends T.Manifest = never>(
static withVolumes<M extends T.SDKManifest = never>(
...volumeNames: Array<M["volumes"][number]>
): Backups<M> {
return Backups.withSyncs(
@@ -54,13 +54,13 @@ export class Backups<M extends T.Manifest> {
)
}
static withSyncs<M extends T.Manifest = never>(
static withSyncs<M extends T.SDKManifest = never>(
...syncs: BackupSync<M["volumes"][number]>[]
) {
return syncs.reduce((acc, x) => acc.addSync(x), new Backups<M>())
}
static withOptions<M extends T.Manifest = never>(
static withOptions<M extends T.SDKManifest = never>(
options?: Partial<T.SyncOptions>,
) {
return new Backups<M>({ ...DEFAULT_OPTIONS, ...options })

View File

@@ -2,7 +2,7 @@ import { Backups } from "./Backups"
import * as T from "../../../base/lib/types"
import { _ } from "../util"
export type SetupBackupsParams<M extends T.Manifest> =
export type SetupBackupsParams<M extends T.SDKManifest> =
| M["volumes"][number][]
| ((_: { effects: T.Effects }) => Promise<Backups<M>>)
@@ -11,7 +11,7 @@ type SetupBackupsRes = {
restoreBackup: T.ExpectedExports.restoreBackup
}
export function setupBackups<M extends T.Manifest>(
export function setupBackups<M extends T.SDKManifest>(
options: SetupBackupsParams<M>,
) {
let backupsFactory: (_: { effects: T.Effects }) => Promise<Backups<M>>

View File

@@ -28,7 +28,7 @@ export {
export { Daemons } from "./mainFn/Daemons"
export { SubContainer } from "./util/SubContainer"
export { StartSdk } from "./StartSdk"
export { setupManifest } from "./manifest/setupManifest"
export { setupManifest, buildManifest } from "./manifest/setupManifest"
export { FileHelper } from "./util/fileHelper"
export { setupExposeStore } from "./store/setupExposeStore"
export { pathBuilder } from "../../base/lib/util/PathBuilder"

View File

@@ -7,12 +7,14 @@ import { VersionGraph } from "../version/VersionGraph"
import { Install } from "./setupInstall"
import { Uninstall } from "./setupUninstall"
export function setupInit<Manifest extends T.Manifest, Store>(
versions: VersionGraph<Manifest["version"]>,
export function setupInit<Manifest extends T.SDKManifest, Store>(
versions: VersionGraph<string>,
install: Install<Manifest, Store>,
uninstall: Uninstall<Manifest, Store>,
setServiceInterfaces: UpdateServiceInterfaces<any>,
setDependencies: (options: { effects: T.Effects }) => Promise<null>,
setDependencies: (options: {
effects: T.Effects
}) => Promise<null | void | undefined>,
actions: Actions<Store, any>,
exposedStore: ExposedStorePaths,
): {

View File

@@ -1,11 +1,11 @@
import * as T from "../../../base/lib/types"
export type InstallFn<Manifest extends T.Manifest, Store> = (opts: {
export type InstallFn<Manifest extends T.SDKManifest, Store> = (opts: {
effects: T.Effects
}) => Promise<null>
export class Install<Manifest extends T.Manifest, Store> {
}) => Promise<null | void | undefined>
export class Install<Manifest extends T.SDKManifest, Store> {
private constructor(readonly fn: InstallFn<Manifest, Store>) {}
static of<Manifest extends T.Manifest, Store>(
static of<Manifest extends T.SDKManifest, Store>(
fn: InstallFn<Manifest, Store>,
) {
return new Install(fn)
@@ -18,7 +18,7 @@ export class Install<Manifest extends T.Manifest, Store> {
}
}
export function setupInstall<Manifest extends T.Manifest, Store>(
export function setupInstall<Manifest extends T.SDKManifest, Store>(
fn: InstallFn<Manifest, Store>,
) {
return Install.of(fn)

View File

@@ -1,11 +1,11 @@
import * as T from "../../../base/lib/types"
export type UninstallFn<Manifest extends T.Manifest, Store> = (opts: {
export type UninstallFn<Manifest extends T.SDKManifest, Store> = (opts: {
effects: T.Effects
}) => Promise<null>
export class Uninstall<Manifest extends T.Manifest, Store> {
}) => Promise<null | void | undefined>
export class Uninstall<Manifest extends T.SDKManifest, Store> {
private constructor(readonly fn: UninstallFn<Manifest, Store>) {}
static of<Manifest extends T.Manifest, Store>(
static of<Manifest extends T.SDKManifest, Store>(
fn: UninstallFn<Manifest, Store>,
) {
return new Uninstall(fn)
@@ -22,7 +22,7 @@ export class Uninstall<Manifest extends T.Manifest, Store> {
}
}
export function setupUninstall<Manifest extends T.Manifest, Store>(
export function setupUninstall<Manifest extends T.SDKManifest, Store>(
fn: UninstallFn<Manifest, Store>,
) {
return Uninstall.of(fn)

View File

@@ -20,7 +20,7 @@ export class CommandController {
private process: cp.ChildProcessWithoutNullStreams,
readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT,
) {}
static of<Manifest extends T.Manifest>() {
static of<Manifest extends T.SDKManifest>() {
return async <A extends string>(
effects: T.Effects,
subcontainer:

View File

@@ -17,7 +17,7 @@ export class Daemon {
get subContainerHandle(): undefined | ExecSpawnable {
return this.commandController?.subContainerHandle
}
static of<Manifest extends T.Manifest>() {
static of<Manifest extends T.SDKManifest>() {
return async <A extends string>(
effects: T.Effects,
subcontainer:

View File

@@ -27,7 +27,7 @@ export type Ready = {
}
type DaemonsParams<
Manifest extends T.Manifest,
Manifest extends T.SDKManifest,
Ids extends string,
Command extends string,
Id extends string,
@@ -43,7 +43,7 @@ type DaemonsParams<
type ErrorDuplicateId<Id extends string> = `The id '${Id}' is already used`
export const runCommand = <Manifest extends T.Manifest>() =>
export const runCommand = <Manifest extends T.SDKManifest>() =>
CommandController.of<Manifest>()
/**
@@ -69,7 +69,7 @@ Daemons.of({
})
```
*/
export class Daemons<Manifest extends T.Manifest, Ids extends string>
export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
implements T.DaemonBuildable
{
private constructor(
@@ -89,7 +89,7 @@ export class Daemons<Manifest extends T.Manifest, Ids extends string>
* @param options
* @returns
*/
static of<Manifest extends T.Manifest>(options: {
static of<Manifest extends T.SDKManifest>(options: {
effects: T.Effects
started: (onTerm: () => PromiseLike<void>) => PromiseLike<null>
healthReceipts: HealthReceipt[]

View File

@@ -3,7 +3,7 @@ import { MountOptions } from "../util/SubContainer"
type MountArray = { path: string; options: MountOptions }[]
export class Mounts<Manifest extends T.Manifest> {
export class Mounts<Manifest extends T.SDKManifest> {
private constructor(
readonly volumes: {
id: Manifest["volumes"][number]
@@ -25,7 +25,7 @@ export class Mounts<Manifest extends T.Manifest> {
}[],
) {}
static of<Manifest extends T.Manifest>() {
static of<Manifest extends T.SDKManifest>() {
return new Mounts<Manifest>([], [], [])
}
@@ -57,7 +57,7 @@ export class Mounts<Manifest extends T.Manifest> {
return this
}
addDependency<DependencyManifest extends T.Manifest>(
addDependency<DependencyManifest extends T.SDKManifest>(
dependencyId: keyof Manifest["dependencies"] & string,
volumeId: DependencyManifest["volumes"][number],
subpath: string | null,

View File

@@ -14,7 +14,7 @@ export const DEFAULT_SIGTERM_TIMEOUT = 30_000
* @param fn
* @returns
*/
export const setupMain = <Manifest extends T.Manifest, Store>(
export const setupMain = <Manifest extends T.SDKManifest, Store>(
fn: (o: {
effects: T.Effects
started(onTerm: () => PromiseLike<void>): PromiseLike<null>

View File

@@ -14,6 +14,23 @@ import { VersionGraph } from "../version/VersionGraph"
* @param manifest Static properties of the package
*/
export function setupManifest<
Id extends string,
Dependencies extends Record<string, unknown>,
VolumesTypes extends VolumeId,
AssetTypes extends VolumeId,
ImagesTypes extends ImageId,
Manifest extends {
dependencies: Dependencies
id: Id
assets: AssetTypes[]
images: Record<ImagesTypes, SDKImageInputSpec>
volumes: VolumesTypes[]
},
>(manifest: SDKManifest & Manifest): SDKManifest & Manifest {
return manifest
}
export function buildManifest<
Id extends string,
Version extends string,
Dependencies extends Record<string, unknown>,
@@ -27,7 +44,6 @@ export function setupManifest<
images: Record<ImagesTypes, SDKImageInputSpec>
volumes: VolumesTypes[]
},
Satisfies extends string[] = [],
>(
versions: VersionGraph<Version>,
manifest: SDKManifest & Manifest,

View File

@@ -367,48 +367,39 @@ describe("values", () => {
test("datetime", async () => {
const sdk = StartSdk.of()
.withManifest(
setupManifest(
VersionGraph.of(
VersionInfo.of({
version: "1.0.0:0",
releaseNotes: "",
migrations: {},
}),
),
{
id: "testOutput",
title: "",
license: "",
wrapperRepo: "",
upstreamRepo: "",
supportSite: "",
marketingSite: "",
donationUrl: null,
description: {
short: "",
long: "",
},
containers: {},
images: {},
volumes: [],
assets: [],
alerts: {
install: null,
update: null,
uninstall: null,
restore: null,
start: null,
stop: null,
},
dependencies: {
"remote-test": {
description: "",
optional: true,
s9pk: "https://example.com/remote-test.s9pk",
},
setupManifest({
id: "testOutput",
title: "",
license: "",
wrapperRepo: "",
upstreamRepo: "",
supportSite: "",
marketingSite: "",
donationUrl: null,
description: {
short: "",
long: "",
},
containers: {},
images: {},
volumes: [],
assets: [],
alerts: {
install: null,
update: null,
uninstall: null,
restore: null,
start: null,
stop: null,
},
dependencies: {
"remote-test": {
description: "",
optional: true,
s9pk: "https://example.com/remote-test.s9pk",
},
},
),
}),
)
.withStore<{ test: "a" }>()
.build(true)

View File

@@ -6,51 +6,40 @@ import { VersionGraph } from "../version/VersionGraph"
export type Manifest = any
export const sdk = StartSdk.of()
.withManifest(
setupManifest(
VersionGraph.of(
VersionInfo.of({
version: "1.0.0:0",
releaseNotes: "",
migrations: {},
})
.satisfies("#other:1.0.0:0")
.satisfies("#other:2.0.0:0"),
),
{
id: "testOutput",
title: "",
license: "",
replaces: [],
wrapperRepo: "",
upstreamRepo: "",
supportSite: "",
marketingSite: "",
donationUrl: null,
description: {
short: "",
long: "",
},
containers: {},
images: {},
volumes: [],
assets: [],
alerts: {
install: null,
update: null,
uninstall: null,
restore: null,
start: null,
stop: null,
},
dependencies: {
"remote-test": {
description: "",
optional: false,
s9pk: "https://example.com/remote-test.s9pk",
},
setupManifest({
id: "testOutput",
title: "",
license: "",
replaces: [],
wrapperRepo: "",
upstreamRepo: "",
supportSite: "",
marketingSite: "",
donationUrl: null,
description: {
short: "",
long: "",
},
containers: {},
images: {},
volumes: [],
assets: [],
alerts: {
install: null,
update: null,
uninstall: null,
restore: null,
start: null,
stop: null,
},
dependencies: {
"remote-test": {
description: "",
optional: false,
s9pk: "https://example.com/remote-test.s9pk",
},
},
),
}),
)
.withStore<{ storeRoot: { storeLeaf: "value" } }>()
.build(true)

View File

@@ -86,19 +86,21 @@ export class FileHelper<A> {
/**
* Accepts structured data and overwrites the existing file on disk.
*/
async write(data: A) {
async write(data: A): Promise<null> {
const parent = previousPath.exec(this.path)
if (parent) {
await fs.mkdir(parent[1], { recursive: true })
}
await fs.writeFile(this.path, this.writeData(data))
return null
}
/**
* Reads the file from disk and converts it to structured data.
*/
async read() {
private async readOnce(): Promise<A | null> {
if (!(await exists(this.path))) {
return null
}
@@ -107,14 +109,14 @@ export class FileHelper<A> {
)
}
async const(effects: T.Effects) {
const watch = this.watch()
private async readConst(effects: T.Effects): Promise<A | null> {
const watch = this.readWatch()
const res = await watch.next()
watch.next().then(effects.constRetry)
return res.value
}
async *watch() {
private async *readWatch() {
let res
while (true) {
if (await exists(this.path)) {
@@ -123,12 +125,12 @@ export class FileHelper<A> {
persistent: false,
signal: ctrl.signal,
})
res = await this.read()
res = await this.readOnce()
const listen = Promise.resolve()
.then(async () => {
for await (const _ of watch) {
ctrl.abort("finished")
return
return null
}
})
.catch((e) => console.error(asError(e)))
@@ -139,13 +141,22 @@ export class FileHelper<A> {
await onCreated(this.path).catch((e) => console.error(asError(e)))
}
}
return null
}
get read() {
return {
once: () => this.readOnce(),
const: (effects: T.Effects) => this.readConst(effects),
watch: () => this.readWatch(),
}
}
/**
* Accepts structured data and performs a merge with the existing file on disk.
*/
async merge(data: A) {
const fileData = (await this.read().catch(() => ({}))) || {}
const fileData = (await this.readOnce().catch(() => ({}))) || {}
const mergeData = merge({}, fileData, data)
return await this.write(mergeData)
}

View File

@@ -1,12 +1,12 @@
{
"name": "@start9labs/start-sdk",
"version": "0.3.6-alpha8",
"version": "0.3.6-alpha9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@start9labs/start-sdk",
"version": "0.3.6-alpha8",
"version": "0.3.6-alpha9",
"license": "MIT",
"dependencies": {
"@iarna/toml": "^2.2.5",
@@ -14,7 +14,7 @@
"@noble/hashes": "^1.4.0",
"isomorphic-fetch": "^3.0.0",
"lodash.merge": "^4.6.2",
"mime": "^4.0.3",
"mime-types": "^2.1.35",
"ts-matches": "^5.5.1",
"yaml": "^2.2.2"
},
@@ -3136,18 +3136,25 @@
"node": ">=8.6"
}
},
"node_modules/mime": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/mime/-/mime-4.0.3.tgz",
"integrity": "sha512-KgUb15Oorc0NEKPbvfa0wRU+PItIEZmiv+pyAO2i0oTIVTJhlzMclU7w4RXWQrSOVH5ax/p/CkIO7KI4OyFJTQ==",
"funding": [
"https://github.com/sponsors/broofa"
],
"bin": {
"mime": "bin/cli.js"
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">=16"
"node": ">= 0.6"
}
},
"node_modules/mimic-fn": {

View File

@@ -1,6 +1,6 @@
{
"name": "@start9labs/start-sdk",
"version": "0.3.6-alpha8",
"version": "0.3.6-alpha.12",
"description": "Software development kit to facilitate packaging services for StartOS",
"main": "./package/lib/index.js",
"types": "./package/lib/index.d.ts",
@@ -15,7 +15,7 @@
},
"scripts": {
"test": "jest -c ./jest.config.js --coverage",
"buildOutput": "ts-node ./lib/test/makeOutput.ts && npx prettier --write '**/*.ts'",
"buildOutput": "ts-node ./lib/test/makeOutput.ts && npx prettier --write \"**/*.ts\"",
"check": "tsc --noEmit",
"tsc": "tsc"
},
@@ -32,7 +32,7 @@
"dependencies": {
"isomorphic-fetch": "^3.0.0",
"lodash.merge": "^4.6.2",
"mime": "^4.0.3",
"mime-types": "^2.1.35",
"ts-matches": "^5.5.1",
"yaml": "^2.2.2",
"@iarna/toml": "^2.2.5",