diff --git a/lib/StartSdk.ts b/lib/StartSdk.ts index 659983c..40cbb70 100644 --- a/lib/StartSdk.ts +++ b/lib/StartSdk.ts @@ -47,6 +47,7 @@ import { setupMain } from "./mainFn" import { defaultTrigger } from "./trigger/defaultTrigger" import { changeOnFirstSuccess, cooldownTrigger } from "./trigger" import setupConfig, { Read, Save } from "./config/setupConfig" +import { setupDependencyMounts } from "./dependency/setupDependencyMounts" // prettier-ignore type AnyNeverCond = @@ -254,6 +255,7 @@ export class StartSdk { _configSpec: ConfigSpec, fn: Save, ) => fn, + setupDependencyMounts, setupInit: ( migrations: Migrations, install: Install, diff --git a/lib/dependency/mountDependencies.ts b/lib/dependency/mountDependencies.ts new file mode 100644 index 0000000..4721d26 --- /dev/null +++ b/lib/dependency/mountDependencies.ts @@ -0,0 +1,43 @@ +import { Effects } from "../types" +import { + Path, + ManifestId, + VolumeName, + NamedPath, + matchPath, +} from "./setupDependencyMounts" + +export type MountDependenciesOut = + // prettier-ignore + A extends Path ? string : A extends Record ? { + [P in keyof A]: MountDependenciesOut; + } : never +export async function mountDependencies< + In extends + | Record>> + | Record> + | Record + | Path, +>(effects: Effects, value: In): Promise> { + if (matchPath.test(value)) { + const mountPath = `${value.manifest.id}/${value.volume}/${value.name}` + + return (await effects.mount({ + location: { + path: mountPath, + }, + target: { + packageId: value.manifest.id, + path: value.path, + readonly: value.readonly, + volumeId: value.volume, + }, + })) as MountDependenciesOut + } + return Object.fromEntries( + Object.entries(value).map(([key, value]) => [ + key, + mountDependencies(effects, value), + ]), + ) as Record as MountDependenciesOut +} diff --git a/lib/dependency/setupDependencyMounts.ts b/lib/dependency/setupDependencyMounts.ts new file mode 100644 index 0000000..e3c8cdd --- /dev/null +++ b/lib/dependency/setupDependencyMounts.ts @@ -0,0 +1,70 @@ +import { boolean, object, string } from "ts-matches" +import { SDKManifest } from "../manifest/ManifestTypes" +import { deepMerge } from "../util/deepMerge" + +export type VolumeName = string +export type NamedPath = string +export type ManifestId = string + +export const matchPath = object({ + name: string, + volume: string, + path: string, + manifest: object({ + id: string, + }), + readonly: boolean, +}) +export type Path = typeof matchPath._TYPE +export type BuildPath = { + [PId in M["manifest"]["id"]]: { + [V in M["volume"]]: { + [N in M["name"]]: M + } + } +} +type ValidIfNotInNested< + Building, + M extends Path, +> = Building extends BuildPath ? never : M +class SetupDependencyMounts { + private constructor(readonly building: Building) {} + + static of() { + return new SetupDependencyMounts({}) + } + + addPath< + NamedPath extends string, + VolumeName extends string, + PathNamed extends string, + M extends SDKManifest, + >( + newPath: ValidIfNotInNested< + Building, + { + name: NamedPath + volume: VolumeName + path: PathNamed + manifest: M + readonly: boolean + } + >, + ) { + const building = deepMerge(this.building, { + [newPath.manifest.id]: { + [newPath.volume]: { + [newPath.name]: newPath, + }, + }, + }) as Building & BuildPath + return new SetupDependencyMounts(building) + } + build() { + return this.building + } +} + +export function setupDependencyMounts() { + return SetupDependencyMounts.of() +} diff --git a/lib/manifest/ManifestTypes.ts b/lib/manifest/ManifestTypes.ts index 65c36d9..50c97c2 100644 --- a/lib/manifest/ManifestTypes.ts +++ b/lib/manifest/ManifestTypes.ts @@ -14,64 +14,68 @@ export interface Container { export type ManifestVersion = ValidEmVer -export interface SDKManifest { +export type SDKManifest = { /** The package identifier used by the OS. This must be unique amongst all other known packages */ - id: string + readonly id: string /** A human readable service title */ - title: string + readonly title: string /** Service version - accepts up to four digits, where the last confirms to revisions necessary for StartOs * - see documentation: https://github.com/Start9Labs/emver-rs. This value will change with each release of * the service */ - version: ManifestVersion + readonly version: ManifestVersion /** Release notes for the update - can be a string, paragraph or URL */ - releaseNotes: string + readonly releaseNotes: string /** The type of license for the project. Include the LICENSE in the root of the project directory. A license is required for a Start9 package.*/ - license: string // name of license + readonly license: string // name of license /** A list of normie (hosted, SaaS, custodial, etc) services this services intends to replace */ - replaces: string[] + readonly replaces: Readonly /** The Start9 wrapper repository URL for the package. This repo contains the manifest file (this), * any scripts necessary for configuration, backups, actions, or health checks (more below). This key * must exist. But could be embedded into the source repository */ - wrapperRepo: string + readonly wrapperRepo: string /** The original project repository URL. There is no upstream repo in this example */ - upstreamRepo: string + readonly upstreamRepo: string /** URL to the support site / channel for the project. This key can be omitted if none exists, or it can link to the original project repository issues */ - supportSite: string + readonly supportSite: string /** URL to the marketing site for the project. If there is no marketing site, it can link to the original project repository */ - marketingSite: string + readonly marketingSite: string /** URL where users can donate to the upstream project */ - donationUrl: string | null + readonly donationUrl: string | null /**Human readable descriptions for the service. These are used throughout the StartOS user interface, primarily in the marketplace. */ - description: { + readonly description: { /**This is the first description visible to the user in the marketplace */ - short: string + readonly short: string /** This description will display with additional details in the service's individual marketplace page */ - long: string + readonly long: string } /** These assets are static files necessary for packaging the service for Start9 (into an s9pk). * Each value is a path to the specified asset. If an asset is missing from this list, or otherwise * denoted, it will be defaulted to the values denoted below. */ - assets: { - icon: string // file path - instructions: string // file path - license: string // file path + readonly assets: { + /** This is the file path for the icon that will be this packages icon on the ui */ + readonly icon: string + /** Instructions path to be seen in the ui section of the package */ + readonly instructions: string + /** license path */ + readonly license: string } /** Defines the containers needed to run the main and mounted volumes */ - containers: Record + readonly containers: Record /** This denotes any data, asset, or pointer volumes that should be connected when the "docker run" command is invoked */ - volumes: Record - alerts: { - install: string | null - update: string | null - uninstall: string | null - restore: string | null - start: string | null - stop: string | null + readonly volumes: Record + + readonly alerts: { + readonly install: string | null + readonly update: string | null + readonly uninstall: string | null + readonly restore: string | null + readonly start: string | null + readonly stop: string | null } - dependencies: Record + readonly dependencies: Readonly> } export interface ManifestDependency { diff --git a/lib/test/mountDependencies.test.ts b/lib/test/mountDependencies.test.ts new file mode 100644 index 0000000..c6b9e01 --- /dev/null +++ b/lib/test/mountDependencies.test.ts @@ -0,0 +1,139 @@ +import { setupManifest } from "../manifest/setupManifest" +import { mountDependencies } from "../dependency/mountDependencies" +import { + BuildPath, + setupDependencyMounts, +} from "../dependency/setupDependencyMounts" + +describe("mountDependencies", () => { + const clnManifest = setupManifest({ + id: "cln", + title: "", + version: "1", + releaseNotes: "", + license: "", + replaces: [], + wrapperRepo: "", + upstreamRepo: "", + supportSite: "", + marketingSite: "", + donationUrl: null, + description: { + short: "", + long: "", + }, + assets: { + icon: "", + instructions: "", + license: "", + }, + containers: {}, + volumes: { main: "data" }, + alerts: { + install: null, + update: null, + uninstall: null, + restore: null, + start: null, + stop: null, + }, + dependencies: {}, + }) + const lndManifest = setupManifest({ + id: "lnd", + title: "", + version: "1", + releaseNotes: "", + license: "", + replaces: [], + wrapperRepo: "", + upstreamRepo: "", + supportSite: "", + marketingSite: "", + donationUrl: null, + description: { + short: "", + long: "", + }, + assets: { + icon: "", + instructions: "", + license: "", + }, + containers: {}, + volumes: {}, + alerts: { + install: null, + update: null, + uninstall: null, + restore: null, + start: null, + stop: null, + }, + dependencies: {}, + }) + clnManifest.id + type test = BuildPath<{ + name: "root" + manifest: typeof clnManifest + volume: "main" + path: "/" + readonly: true + }> extends BuildPath<{ + name: "root" + manifest: typeof clnManifest + volume: "main2" + path: "/" + readonly: true + }> + ? true + : false + + test("Types work", () => { + const dependencyMounts = setupDependencyMounts() + .addPath({ + name: "root", + volume: "main", + path: "/", + manifest: clnManifest, + readonly: true, + }) + .addPath({ + name: "root", + manifest: lndManifest, + volume: "main", + path: "/", + readonly: true, + }) + .build() + ;() => { + const test = mountDependencies( + null as any, + dependencyMounts, + ) satisfies Promise<{ + cln: { + main: { + root: string + } + } + lnd: { + main: { + root: string + } + } + }> + const test2 = mountDependencies( + null as any, + dependencyMounts.cln, + ) satisfies Promise<{ + main: { root: string } + }> + const test3 = mountDependencies( + null as any, + dependencyMounts.cln.main, + ) satisfies Promise<{ + root: string + }> + } + }) +}) diff --git a/lib/types.ts b/lib/types.ts index 907c308..28feb6e 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -383,7 +383,8 @@ export type Effects = { mount(options: { location: { - volumeId: string + /** If there is no volumeId then we mount to runMedia a special mounting location */ + volumeId?: string path: string } target: { @@ -392,7 +393,7 @@ export type Effects = { path: string readonly: boolean } - }): Promise + }): Promise stopped(packageId?: string): Promise diff --git a/lib/util/utils.ts b/lib/util/utils.ts index 5e5e54d..95cd1f0 100644 --- a/lib/util/utils.ts +++ b/lib/util/utils.ts @@ -16,7 +16,22 @@ import { DefaultString } from "../config/configTypes" import { getDefaultString } from "./getDefaultString" import { GetStore, getStore } from "../store/getStore" import { GetVault, getVault } from "./getVault" +import { + MountDependenciesOut, + mountDependencies, +} from "../dependency/mountDependencies" +import { + ManifestId, + VolumeName, + NamedPath, + Path, +} from "../dependency/setupDependencyMounts" +// prettier-ignore +type skipFirstParam = + A extends [any, ...infer B] ? B : + A extends [] ? [] : + never export type Utils = { createOrUpdateVault: (opts: { key: string @@ -67,6 +82,15 @@ export type Utils = { networkBuilder: () => NetworkBuilder torHostName: (id: string) => TorHostname nullIfEmpty: typeof nullIfEmpty + mountDependencies: < + In extends + | Record>> + | Record> + | Record + | Path, + >( + value: In, + ) => Promise> } export const utils = < Store = never, @@ -128,5 +152,14 @@ export const utils = < set: (key: keyof Vault & string, value: string) => effects.vault.set({ key, value }), }, + mountDependencies: < + In extends + | Record>> + | Record> + | Record + | Path, + >( + value: In, + ) => mountDependencies(effects, value), }) function noop(): void {} diff --git a/package-lock.json b/package-lock.json index e2b1626..3575f69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "ts-node": "^10.9.1", "tsc-multi": "^0.6.1", "tsconfig-paths": "^3.14.2", + "typescript": "^5.0.0", "vitest": "^0.29.2" } }, @@ -4878,17 +4879,16 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=12.20" } }, "node_modules/ufo": { diff --git a/package.json b/package.json index c25cd26..c3d8b66 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "ts-node": "^10.9.1", "tsc-multi": "^0.6.1", "tsconfig-paths": "^3.14.2", + "typescript": "^5.0.4", "vitest": "^0.29.2" }, "declaration": true