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

@@ -24,7 +24,7 @@ clean:
package/lib/test/output.ts: package/node_modules package/lib/test/makeOutput.ts package/scripts/oldSpecToBuilder.ts
cd package && npm run buildOutput
bundle: dist baseDist | test fmt
bundle: baseDist dist | test fmt
touch dist
base/lib/exver/exver.ts: base/node_modules base/lib/exver/exver.pegjs
@@ -67,9 +67,8 @@ base/node_modules: base/package.json
node_modules: package/node_modules base/node_modules
publish: bundle package/package.json README.md LICENSE
cd dist
npm publish --access=public
publish: bundle package/package.json package/README.md package/LICENSE
cd dist && npm publish --access=public
link: bundle
cd dist && npm link

View File

@@ -52,7 +52,7 @@ export type Effects = {
options: RequestActionParams,
): Promise<null>
clearRequests(
options: { only: ActionId[] } | { except: ActionId[] },
options: { only: string[] } | { except: string[] },
): Promise<null>
}

View File

@@ -1,5 +1,6 @@
import * as T from "../types"
import * as IST from "../actions/input/inputSpecTypes"
import { Action } from "./setupActions"
export type RunActionInput<Input> =
| Input
@@ -43,23 +44,62 @@ export const runAction = async <
})
}
}
type GetActionInputType<
A extends Action<T.ActionId, any, any, Record<string, unknown>>,
> = A extends Action<T.ActionId, any, any, infer I> ? I : never
// prettier-ignore
export type ActionRequest<T extends Omit<T.ActionRequest, "packageId">> =
T extends { when: { condition: "input-not-matches" } }
? (T extends { input: T.ActionRequestInput } ? T : "input is required for condition 'input-not-matches'")
: T
type ActionRequestBase = {
reason?: string
replayId?: string
}
type ActionRequestInput<
T extends Action<T.ActionId, any, any, Record<string, unknown>>,
> = {
kind: "partial"
value: Partial<GetActionInputType<T>>
}
export type ActionRequestOptions<
T extends Action<T.ActionId, any, any, Record<string, unknown>>,
> = ActionRequestBase &
(
| {
when?: Exclude<
T.ActionRequestTrigger,
{ condition: "input-not-matches" }
>
input?: ActionRequestInput<T>
}
| {
when: T.ActionRequestTrigger & { condition: "input-not-matches" }
input: ActionRequestInput<T>
}
)
const _validate: T.ActionRequest = {} as ActionRequestOptions<any> & {
actionId: string
packageId: string
severity: T.ActionSeverity
}
export const requestAction = <
T extends Omit<T.ActionRequest, "packageId">,
T extends Action<T.ActionId, any, any, Record<string, unknown>>,
>(options: {
effects: T.Effects
request: ActionRequest<T> & { replayId?: string; packageId: T.PackageId }
packageId: T.PackageId
action: T
severity: T.ActionSeverity
options?: ActionRequestOptions<T>
}) => {
const request = options.request
const request = options.options || {}
const actionId = options.action.id
const req = {
...request,
replayId: request.replayId || `${request.packageId}:${request.actionId}`,
actionId,
packageId: options.packageId,
action: undefined,
severity: options.severity,
replayId: request.replayId || `${options.packageId}:${actionId}`,
}
delete req.action
return options.effects.action.request(req)
}

View File

@@ -11,7 +11,7 @@ export type Run<
> = (options: {
effects: T.Effects
input: ExtractInputSpecType<A> & Record<string, any>
}) => Promise<T.ActionResult | null>
}) => Promise<T.ActionResult | null | void | undefined>
export type GetInput<
A extends
| Record<string, any>
@@ -19,7 +19,9 @@ export type GetInput<
| InputSpec<Record<string, any>, never>,
> = (options: {
effects: T.Effects
}) => Promise<null | (ExtractInputSpecType<A> & Record<string, any>)>
}) => Promise<
null | void | undefined | (ExtractInputSpecType<A> & Record<string, any>)
>
export type MaybeFn<T> = T | ((options: { effects: T.Effects }) => Promise<T>)
function callMaybeFn<T>(
@@ -91,7 +93,7 @@ export class Action<
): Action<Id, Store, {}, {}> {
return new Action(
id,
mapMaybeFn(metadata, (m) => ({ ...m, hasInput: true })),
mapMaybeFn(metadata, (m) => ({ ...m, hasInput: false })),
{},
async () => null,
run,
@@ -114,7 +116,7 @@ export class Action<
effects: T.Effects
input: Type
}): Promise<T.ActionResult | null> {
return this.runFn(options)
return (await this.runFn(options)) || null
}
}

View File

@@ -1,21 +0,0 @@
import { VersionRange } from "../exver"
export class Dependency {
constructor(
readonly data:
| {
/** Either "running" or "exists". Does the dependency need to be running, or does it only need to exist? */
type: "running"
/** The acceptable version range of the dependency. */
versionRange: VersionRange
/** A list of the dependency's health check IDs that must be passing for the service to be satisfied. */
healthChecks: string[]
}
| {
/** Either "running" or "exists". Does the dependency need to be running, or does it only need to exist? */
type: "exists"
/** The acceptable version range of the dependency. */
versionRange: VersionRange
},
) {}
}

View File

@@ -1,22 +1,11 @@
import * as T from "../types"
import { once } from "../util"
import { Dependency } from "./Dependency"
type DependencyType<Manifest extends T.Manifest> = {
[K in keyof {
[K in keyof Manifest["dependencies"]]: Manifest["dependencies"][K]["optional"] extends false
? K
: never
}]: Dependency
} & {
[K in keyof {
[K in keyof Manifest["dependencies"]]: Manifest["dependencies"][K]["optional"] extends true
? K
: never
}]?: Dependency
type DependencyType<Manifest extends T.SDKManifest> = {
[K in keyof Manifest["dependencies"]]: Omit<T.DependencyRequirement, "id">
}
export function setupDependencies<Manifest extends T.Manifest>(
export function setupDependencies<Manifest extends T.SDKManifest>(
fn: (options: { effects: T.Effects }) => Promise<DependencyType<Manifest>>,
): (options: { effects: T.Effects }) => Promise<null> {
const cell = { updater: async (_: { effects: T.Effects }) => null }
@@ -30,24 +19,12 @@ export function setupDependencies<Manifest extends T.Manifest>(
const dependencyType = await fn(options)
return await options.effects.setDependencies({
dependencies: Object.entries(dependencyType).map(
([
id,
{
data: { versionRange, ...x },
},
]) => ({
id,
...x,
...(x.type === "running"
? {
kind: "running",
healthChecks: x.healthChecks,
}
: {
kind: "exists",
}),
versionRange: versionRange.toString(),
}),
([id, { versionRange, ...x }, ,]) =>
({
id,
...x,
versionRange: versionRange.toString(),
}) as T.DependencyRequirement,
),
})
}

View File

@@ -2,12 +2,14 @@
import type { ActionId } from "./ActionId"
import type { ActionRequestInput } from "./ActionRequestInput"
import type { ActionRequestTrigger } from "./ActionRequestTrigger"
import type { ActionSeverity } from "./ActionSeverity"
import type { PackageId } from "./PackageId"
export type ActionRequest = {
packageId: PackageId
actionId: ActionId
description?: string
severity: ActionSeverity
reason?: string
when?: ActionRequestTrigger
input?: ActionRequestInput
}

View File

@@ -0,0 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ActionResultV0 } from "./ActionResultV0"
import type { ActionResultV1 } from "./ActionResultV1"
export type ActionResult =
| ({ version: "0" } & ActionResultV0)
| ({ version: "1" } & ActionResultV1)

View File

@@ -0,0 +1,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ActionResultV0 = {
message: string
value: string | null
copyable: boolean
qr: boolean
}

View File

@@ -0,0 +1,18 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ActionResultV1 =
| {
type: "string"
name: string
value: string
description: string | null
copyable: boolean
qr: boolean
masked: boolean
}
| {
type: "object"
name: string
value: Array<ActionResultV1>
description?: string
}

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ActionSeverity = "critical" | "important"

View File

@@ -1,6 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ActionVisibility =
| "hidden"
| { disabled: { reason: string } }
| "enabled"
export type ActionVisibility = "hidden" | { disabled: string } | "enabled"

View File

@@ -2,6 +2,7 @@
import type { ActionId } from "./ActionId"
import type { ActionRequestInput } from "./ActionRequestInput"
import type { ActionRequestTrigger } from "./ActionRequestTrigger"
import type { ActionSeverity } from "./ActionSeverity"
import type { PackageId } from "./PackageId"
import type { ReplayId } from "./ReplayId"
@@ -9,7 +10,8 @@ export type RequestActionParams = {
replayId: ReplayId
packageId: PackageId
actionId: ActionId
description?: string
severity: ActionSeverity
reason?: string
when?: ActionRequestTrigger
input?: ActionRequestInput
}

View File

@@ -7,6 +7,10 @@ export { ActionRequestEntry } from "./ActionRequestEntry"
export { ActionRequestInput } from "./ActionRequestInput"
export { ActionRequestTrigger } from "./ActionRequestTrigger"
export { ActionRequest } from "./ActionRequest"
export { ActionResult } from "./ActionResult"
export { ActionResultV0 } from "./ActionResultV0"
export { ActionResultV1 } from "./ActionResultV1"
export { ActionSeverity } from "./ActionSeverity"
export { ActionVisibility } from "./ActionVisibility"
export { AddAdminParams } from "./AddAdminParams"
export { AddAssetParams } from "./AddAssetParams"

View File

@@ -33,10 +33,6 @@ export const SIGKILL: Signals = "SIGKILL"
export const NO_TIMEOUT = -1
export type PathMaker = (options: { volume: string; path: string }) => string
export type ExportedAction = (options: {
effects: Effects
input?: Record<string, unknown>
}) => Promise<ActionResult>
export type MaybePromise<A> = Promise<A> | A
export namespace ExpectedExports {
version: 1
@@ -86,10 +82,6 @@ export namespace ExpectedExports {
nextVersion: null | string
}) => Promise<unknown>
export type properties = (options: {
effects: Effects
}) => Promise<PropertiesReturn>
export type manifest = Manifest
export type actions = Actions<
@@ -105,7 +97,6 @@ export type ABI = {
containerInit: ExpectedExports.containerInit
packageInit: ExpectedExports.packageInit
packageUninit: ExpectedExports.packageUninit
properties: ExpectedExports.properties
manifest: ExpectedExports.manifest
actions: ExpectedExports.actions
}
@@ -177,58 +168,6 @@ export type ExposeServicePaths<Store = never> = {
paths: ExposedStorePaths
}
export type SdkPropertiesValue =
| {
type: "object"
value: { [k: string]: SdkPropertiesValue }
description?: string
}
| {
type: "string"
/** The value to display to the user */
value: string
/** A human readable description or explanation of the value */
description?: string
/** Whether or not to mask the value, for example, when displaying a password */
masked?: boolean
/** Whether or not to include a button for copying the value to clipboard */
copyable?: boolean
/** Whether or not to include a button for displaying the value as a QR code */
qr?: boolean
}
export type SdkPropertiesReturn = {
[key: string]: SdkPropertiesValue
}
export type PropertiesValue =
| {
/** The type of this value, either "string" or "object" */
type: "object"
/** A nested mapping of values. The user will experience this as a nested page with back button */
value: { [k: string]: PropertiesValue }
/** (optional) A human readable description of the new set of values */
description: string | null
}
| {
/** The type of this value, either "string" or "object" */
type: "string"
/** The value to display to the user */
value: string
/** A human readable description of the value */
description: string | null
/** Whether or not to mask the value, for example, when displaying a password */
masked: boolean | null
/** Whether or not to include a button for copying the value to clipboard */
copyable: boolean | null
/** Whether or not to include a button for displaying the value as a QR code */
qr: boolean | null
}
export type PropertiesReturn = {
[key: string]: PropertiesValue
}
export type EffectMethod<T extends StringObject = Effects> = {
[K in keyof T]-?: K extends string
? T[K] extends Function
@@ -264,13 +203,6 @@ export type Metadata = {
mode: number
}
export type ActionResult = {
version: "0"
message: string
value: string | null
copyable: boolean
qr: boolean
}
export type SetResult = {
dependsOn: DependsOn
signal: Signals

View File

@@ -4,9 +4,9 @@
"types": "./index.d.ts",
"sideEffects": true,
"scripts": {
"peggy": "peggy --allowed-start-rules '*' --plugin ./node_modules/ts-pegjs/dist/tspegjs -o lib/exver/exver.ts lib/exver/exver.pegjs",
"peggy": "peggy --allowed-start-rules \"*\" --plugin ./node_modules/ts-pegjs/dist/tspegjs -o lib/exver/exver.ts lib/exver/exver.pegjs",
"test": "jest -c ./jest.config.js --coverage",
"buildOutput": "npx prettier --write '**/*.ts'",
"buildOutput": "npx prettier --write \"**/*.ts\"",
"check": "tsc --noEmit",
"tsc": "tsc"
},

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",