mirror of
https://github.com/Start9Labs/start-sdk.git
synced 2026-03-26 10:21:55 +00:00
feat: add autoConfig/ better types for wrapperData
This commit is contained in:
@@ -1,70 +1,42 @@
|
||||
import deepmerge from "deepmerge";
|
||||
import { AutoConfigure, Effects } from "../types";
|
||||
import { Message, MaybePromise, ReadonlyDeep } from ".";
|
||||
import { AutoConfigure, Effects, ExpectedExports } from "../types";
|
||||
import { deepEqual, deepMerge } from "../util";
|
||||
|
||||
class AutoConfigBuilt<Config> implements AutoConfigure<Config> {
|
||||
constructor(private autoConfig: AutoConfig<Config>) {}
|
||||
|
||||
async check(effects: Effects, config: Config): Promise<void> {
|
||||
for (const [message, configure] of this.autoConfig.getConfigures()) {
|
||||
const value = await configure({ effects, config });
|
||||
if (value !== null) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
/** This is called after we know that the dependency package needs a new configuration, this would be a transform for defaults */
|
||||
async autoConfigure(effects: Effects, config: Config): Promise<Config> {
|
||||
const input = { effects, config };
|
||||
const newOverwrites = (
|
||||
await Promise.all(this.autoConfig.getConfigures().map((x) => x[1](input)))
|
||||
).filter((x): x is NonNullable<typeof x> => x !== null);
|
||||
return deepmerge.all([config, ...newOverwrites]);
|
||||
}
|
||||
}
|
||||
export class AutoConfig<Config> {
|
||||
private constructor(
|
||||
private configures: Array<
|
||||
[
|
||||
Message,
|
||||
(
|
||||
options: Readonly<{ config: Config; effects: Effects }>
|
||||
) => MaybePromise<null | Partial<Config>>
|
||||
]
|
||||
>
|
||||
export type AutoConfigFrom = {
|
||||
[key: string]: (options: {
|
||||
effects: Effects;
|
||||
localConfig: unknown;
|
||||
remoteConfig: unknown;
|
||||
}) => Promise<void | Record<string, unknown>>;
|
||||
};
|
||||
export class AutoConfig {
|
||||
constructor(
|
||||
readonly configs: AutoConfigFrom,
|
||||
readonly path: keyof AutoConfigFrom,
|
||||
) {}
|
||||
getConfigures(): ReadonlyDeep<
|
||||
Array<
|
||||
[
|
||||
Message,
|
||||
(
|
||||
options: Readonly<{ config: Config; effects: Effects }>
|
||||
) => MaybePromise<null | Partial<Config>>
|
||||
]
|
||||
>
|
||||
> {
|
||||
return this.configures;
|
||||
}
|
||||
|
||||
static autoConfig<Config>(
|
||||
message: Message,
|
||||
configure: (
|
||||
options: Readonly<{ config: Config; effects: Effects }>
|
||||
) => MaybePromise<null | Partial<Config>>
|
||||
): AutoConfig<Config> {
|
||||
return new AutoConfig([[message, configure]]);
|
||||
async check(
|
||||
options: Parameters<AutoConfigure["check"]>[0],
|
||||
): ReturnType<AutoConfigure["check"]> {
|
||||
const origConfig = JSON.parse(JSON.stringify(options.localConfig));
|
||||
if (
|
||||
!deepEqual(
|
||||
origConfig,
|
||||
deepMerge(
|
||||
{},
|
||||
options.localConfig,
|
||||
await this.configs[this.path](options),
|
||||
),
|
||||
)
|
||||
)
|
||||
throw new Error(`Check failed for ${this.path}`);
|
||||
}
|
||||
autoConfig(
|
||||
message: Message,
|
||||
configure: (
|
||||
options: Readonly<{ config: Config; effects: Effects }>
|
||||
) => MaybePromise<null | Partial<Config>>
|
||||
): AutoConfig<Config> {
|
||||
this.configures.push([message, configure]);
|
||||
return this;
|
||||
}
|
||||
|
||||
build() {
|
||||
return new AutoConfigBuilt(this);
|
||||
async autoConfigure(
|
||||
options: Parameters<AutoConfigure["autoConfigure"]>[0],
|
||||
): ReturnType<AutoConfigure["autoConfigure"]> {
|
||||
return deepMerge(
|
||||
{},
|
||||
options.localConfig,
|
||||
await this.configs[this.path](options),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { ExpectedExports, PackageId } from "../types";
|
||||
import { AutoConfig } from "./AutoConfig";
|
||||
|
||||
export function autoconfigSetup<Config>(
|
||||
autoconfigs: Record<PackageId, AutoConfig<Config>>
|
||||
) {
|
||||
const autoconfig: ExpectedExports.autoConfig<Config> = {};
|
||||
|
||||
for (const [id, autoconfigValue] of Object.entries(autoconfigs)) {
|
||||
autoconfig[id] = autoconfigValue.build();
|
||||
}
|
||||
|
||||
return autoconfig;
|
||||
}
|
||||
@@ -6,4 +6,4 @@ export type MaybePromise<A> = Promise<A> | A;
|
||||
export type Message = string;
|
||||
|
||||
export { AutoConfig } from "./AutoConfig";
|
||||
export { autoconfigSetup } from "./autoconfigSetup";
|
||||
export { setupAutoConfig } from "./setupAutoConfig";
|
||||
|
||||
9
lib/autoconfig/setupAutoConfig.ts
Normal file
9
lib/autoconfig/setupAutoConfig.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { AutoConfig, AutoConfigFrom } from "./AutoConfig";
|
||||
|
||||
export function setupAutoConfig<C extends AutoConfigFrom>(configs: C) {
|
||||
const answer = { ...configs } as unknown as { [k in keyof C]: AutoConfig };
|
||||
for (const key in configs) {
|
||||
answer[key] = new AutoConfig(configs, key);
|
||||
}
|
||||
return answer;
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export class Backups {
|
||||
|
||||
constructor(
|
||||
private options = DEFAULT_OPTIONS,
|
||||
private backupSet = [] as BackupSet[]
|
||||
private backupSet = [] as BackupSet[],
|
||||
) {}
|
||||
static volumes(...volumeNames: string[]) {
|
||||
return new Backups().addSets(
|
||||
@@ -49,7 +49,7 @@ export class Backups {
|
||||
srcPath: "./",
|
||||
dstPath: `./${srcVolume}/`,
|
||||
dstVolume: Backups.BACKUP,
|
||||
}))
|
||||
})),
|
||||
);
|
||||
}
|
||||
static addSets(...options: BackupSet[]) {
|
||||
@@ -72,12 +72,12 @@ export class Backups {
|
||||
srcPath: "./",
|
||||
dstPath: `./${srcVolume}/`,
|
||||
dstVolume: Backups.BACKUP,
|
||||
}))
|
||||
})),
|
||||
);
|
||||
}
|
||||
addSets(...options: BackupSet[]) {
|
||||
options.forEach((x) =>
|
||||
this.backupSet.push({ ...x, options: { ...this.options, ...x.options } })
|
||||
this.backupSet.push({ ...x, options: { ...this.options, ...x.options } }),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
@@ -98,7 +98,7 @@ export class Backups {
|
||||
.map((x) => x.dstPath)
|
||||
.map((x) => x.replace(/\.\/([^]*)\//, "$1"));
|
||||
const filteredItems = previousItems.filter(
|
||||
(x) => backupPaths.indexOf(x) === -1
|
||||
(x) => backupPaths.indexOf(x) === -1,
|
||||
);
|
||||
for (const itemToRemove of filteredItems) {
|
||||
effects.error(`Trying to remove ${itemToRemove}`);
|
||||
@@ -111,7 +111,7 @@ export class Backups {
|
||||
effects.removeFile({
|
||||
volumeId: Backups.BACKUP,
|
||||
path: itemToRemove,
|
||||
})
|
||||
}),
|
||||
)
|
||||
.catch(() => {
|
||||
effects.warn(`Failed to remove ${itemToRemove} from backup volume`);
|
||||
|
||||
@@ -65,13 +65,13 @@ export class Config<A extends InputSpec> extends IBuilder<A> {
|
||||
}
|
||||
static withValue<K extends string, B extends ValueSpec>(
|
||||
key: K,
|
||||
value: Value<B>
|
||||
value: Value<B>,
|
||||
) {
|
||||
return Config.empty().withValue(key, value);
|
||||
}
|
||||
static addValue<K extends string, B extends ValueSpec>(
|
||||
key: K,
|
||||
value: Value<B>
|
||||
value: Value<B>,
|
||||
) {
|
||||
return Config.empty().withValue(key, value);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export class List<A extends ValueSpecList> extends IBuilder<A> {
|
||||
patternDescription?: string | null;
|
||||
/** Default = "text" */
|
||||
inputmode?: ListValueSpecString["inputmode"];
|
||||
}
|
||||
},
|
||||
) {
|
||||
const spec = {
|
||||
type: "string" as const,
|
||||
@@ -77,7 +77,7 @@ export class List<A extends ValueSpecList> extends IBuilder<A> {
|
||||
range?: string;
|
||||
units?: string | null;
|
||||
placeholder?: string | null;
|
||||
}
|
||||
},
|
||||
) {
|
||||
const spec = {
|
||||
type: "number" as const,
|
||||
@@ -111,7 +111,7 @@ export class List<A extends ValueSpecList> extends IBuilder<A> {
|
||||
spec: Spec;
|
||||
displayAs?: null | string;
|
||||
uniqueBy?: null | UniqueBy;
|
||||
}
|
||||
},
|
||||
) {
|
||||
const { spec: previousSpecSpec, ...restSpec } = aSpec;
|
||||
const specSpec = previousSpecSpec.build() as BuilderExtract<Spec>;
|
||||
|
||||
@@ -154,7 +154,7 @@ export class Value<A extends ValueSpec> extends IBuilder<A> {
|
||||
description?: string | null;
|
||||
warning?: string | null;
|
||||
},
|
||||
previousSpec: Spec
|
||||
previousSpec: Spec,
|
||||
) {
|
||||
const spec = previousSpec.build() as BuilderExtract<Spec>;
|
||||
return new Value({
|
||||
@@ -166,7 +166,7 @@ export class Value<A extends ValueSpec> extends IBuilder<A> {
|
||||
});
|
||||
}
|
||||
static union<
|
||||
V extends Variants<{ [key: string]: { name: string; spec: InputSpec } }>
|
||||
V extends Variants<{ [key: string]: { name: string; spec: InputSpec } }>,
|
||||
>(
|
||||
a: {
|
||||
name: string;
|
||||
@@ -175,7 +175,7 @@ export class Value<A extends ValueSpec> extends IBuilder<A> {
|
||||
required: boolean;
|
||||
default?: string | null;
|
||||
},
|
||||
aVariants: V
|
||||
aVariants: V,
|
||||
) {
|
||||
const variants = aVariants.build() as BuilderExtract<V>;
|
||||
return new Value({
|
||||
|
||||
@@ -57,12 +57,12 @@ export class Variants<
|
||||
name: string;
|
||||
spec: InputSpec;
|
||||
};
|
||||
}
|
||||
},
|
||||
> extends IBuilder<A> {
|
||||
static of<
|
||||
A extends {
|
||||
[key: string]: { name: string; spec: Config<InputSpec> };
|
||||
}
|
||||
},
|
||||
>(a: A) {
|
||||
const variants: {
|
||||
[K in keyof A]: { name: string; spec: BuilderExtract<A[K]["spec"]> };
|
||||
@@ -82,7 +82,7 @@ export class Variants<
|
||||
}
|
||||
static withVariant<K extends string, B extends InputSpec>(
|
||||
key: K,
|
||||
value: Config<B>
|
||||
value: Config<B>,
|
||||
) {
|
||||
return Variants.empty().withVariant(key, value);
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ export type DefaultString =
|
||||
// sometimes the type checker needs just a little bit of help
|
||||
export function isValueSpecListOf<S extends ListValueSpecType>(
|
||||
t: ValueSpecListOf<ListValueSpecType>,
|
||||
s: S
|
||||
s: S,
|
||||
): t is ValueSpecListOf<S> & { spec: ListValueSpecOf<S> } {
|
||||
return t.spec.type === s;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import { Config } from "./builder";
|
||||
import { DeepPartial, Dependencies, DependsOn, Effects, ExpectedExports } from "../types";
|
||||
import {
|
||||
DeepPartial,
|
||||
Dependencies,
|
||||
DependsOn,
|
||||
Effects,
|
||||
ExpectedExports,
|
||||
} from "../types";
|
||||
import { InputSpec } from "./configTypes";
|
||||
import { nullIfEmpty } from "../util";
|
||||
import { TypeFromProps } from "../util/propertiesMatcher";
|
||||
|
||||
export type Write<A, ConfigType> = (options: { effects: Effects; input: TypeFromProps<A> }) => Promise<ConfigType>;
|
||||
export type Read<A, ConfigType> = (options: {
|
||||
effects: Effects;
|
||||
config: ConfigType;
|
||||
}) => Promise<null | DeepPartial<TypeFromProps<A>>>;
|
||||
export type DependenciesFn<A, ConfigType> = (options: {
|
||||
export type Write<A> = (options: {
|
||||
effects: Effects;
|
||||
input: TypeFromProps<A>;
|
||||
}) => Promise<void>;
|
||||
export type Read<A> = (options: {
|
||||
effects: Effects;
|
||||
}) => Promise<null | DeepPartial<TypeFromProps<A>>>;
|
||||
export type DependenciesFn<A> = (options: {
|
||||
effects: Effects;
|
||||
input: TypeFromProps<A>;
|
||||
config: ConfigType;
|
||||
}) => Promise<Dependencies | void>;
|
||||
/**
|
||||
* We want to setup a config export with a get and set, this
|
||||
@@ -21,31 +28,30 @@ export type DependenciesFn<A, ConfigType> = (options: {
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
export function setupConfigExports<A extends InputSpec, ConfigType>(options: {
|
||||
spec: Config<A>;
|
||||
write: Write<A, ConfigType>;
|
||||
read: Read<A, ConfigType>;
|
||||
dependencies: DependenciesFn<A, ConfigType>;
|
||||
}) {
|
||||
const validator = options.spec.validator();
|
||||
export function setupConfigExports<A extends InputSpec>(
|
||||
spec: Config<A>,
|
||||
write: Write<A>,
|
||||
read: Read<A>,
|
||||
dependencies: DependenciesFn<A>,
|
||||
) {
|
||||
const validator = spec.validator();
|
||||
return {
|
||||
setConfig: (async ({ effects, input }) => {
|
||||
if (!validator.test(input)) {
|
||||
await effects.error(String(validator.errorMessage(input)));
|
||||
return { error: "Set config type error for config" };
|
||||
}
|
||||
const config = await options.write({
|
||||
await write({
|
||||
input: JSON.parse(JSON.stringify(input)),
|
||||
effects,
|
||||
});
|
||||
const dependencies = (await options.dependencies({ effects, input, config })) || [];
|
||||
await effects.setDependencies(dependencies);
|
||||
await effects.setWrapperData({ path: "config", value: config || null });
|
||||
const dependenciesToSet = (await dependencies({ effects, input })) || [];
|
||||
await effects.setDependencies(dependenciesToSet);
|
||||
}) as ExpectedExports.setConfig,
|
||||
getConfig: (async ({ effects, config }) => {
|
||||
return {
|
||||
spec: options.spec.build(),
|
||||
config: nullIfEmpty(await options.read({ effects, config: config as ConfigType })),
|
||||
spec: spec.build(),
|
||||
config: nullIfEmpty(await read({ effects })),
|
||||
};
|
||||
}) as ExpectedExports.getConfig,
|
||||
};
|
||||
|
||||
@@ -6,15 +6,15 @@ import * as C from "./configTypes";
|
||||
export async function specToBuilderFile(
|
||||
file: string,
|
||||
inputData: Promise<InputSpec> | InputSpec,
|
||||
options: Parameters<typeof specToBuilder>[1]
|
||||
options: Parameters<typeof specToBuilder>[1],
|
||||
) {
|
||||
await fs.writeFile(file, await specToBuilder(inputData, options), (err) =>
|
||||
console.error(err)
|
||||
console.error(err),
|
||||
);
|
||||
}
|
||||
export async function specToBuilder(
|
||||
inputData: Promise<InputSpec> | InputSpec,
|
||||
{ startSdk = "start-sdk" } = {}
|
||||
{ startSdk = "start-sdk" } = {},
|
||||
) {
|
||||
const outputLines: string[] = [];
|
||||
outputLines.push(`
|
||||
@@ -26,10 +26,10 @@ export async function specToBuilder(
|
||||
const configName = newConst("InputSpec", convertInputSpec(data));
|
||||
const configMatcherName = newConst(
|
||||
"matchInputSpec",
|
||||
`${configName}.validator()`
|
||||
`${configName}.validator()`,
|
||||
);
|
||||
outputLines.push(
|
||||
`export type InputSpec = typeof ${configMatcherName}._TYPE;`
|
||||
`export type InputSpec = typeof ${configMatcherName}._TYPE;`,
|
||||
);
|
||||
|
||||
return outputLines.join("\n");
|
||||
@@ -73,7 +73,7 @@ export async function specToBuilder(
|
||||
const { variants, type, ...rest } = value;
|
||||
const variantVariable = newConst(
|
||||
value.name + "_variants",
|
||||
convertVariants(variants)
|
||||
convertVariants(variants),
|
||||
);
|
||||
|
||||
return `Value.union(${JSON.stringify(rest)}, ${variantVariable})`;
|
||||
@@ -101,7 +101,7 @@ export async function specToBuilder(
|
||||
warning: value.warning || null,
|
||||
},
|
||||
null,
|
||||
2
|
||||
2,
|
||||
)}, ${JSON.stringify({
|
||||
masked: spec?.masked || false,
|
||||
placeholder: spec?.placeholder || null,
|
||||
@@ -120,7 +120,7 @@ export async function specToBuilder(
|
||||
warning: value.warning || null,
|
||||
},
|
||||
null,
|
||||
2
|
||||
2,
|
||||
)}, ${JSON.stringify({
|
||||
range: spec?.range || null,
|
||||
integral: spec?.integral || false,
|
||||
@@ -131,7 +131,7 @@ export async function specToBuilder(
|
||||
case "object": {
|
||||
const specName = newConst(
|
||||
value.name + "_spec",
|
||||
convertInputSpec(spec.spec)
|
||||
convertInputSpec(spec.spec),
|
||||
);
|
||||
return `List.obj({
|
||||
name: ${JSON.stringify(value.name || null)},
|
||||
@@ -155,7 +155,7 @@ export async function specToBuilder(
|
||||
name: string;
|
||||
spec: C.InputSpec;
|
||||
}
|
||||
>
|
||||
>,
|
||||
): string {
|
||||
let answer = "Variants.of({";
|
||||
for (const [key, { name, spec }] of Object.entries(variants)) {
|
||||
|
||||
@@ -248,7 +248,7 @@ export class Checker {
|
||||
* Check is the function that will be given a emver or unparsed emver and should give if it follows
|
||||
* a pattern
|
||||
*/
|
||||
public readonly check: (value: string | EmVer) => boolean
|
||||
public readonly check: (value: string | EmVer) => boolean,
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,7 +22,7 @@ export async function checkPortListening(
|
||||
{
|
||||
error = `Port ${port} is not listening`,
|
||||
message = `Port ${port} is available`,
|
||||
} = {}
|
||||
} = {},
|
||||
): Promise<CheckResult> {
|
||||
const hasAddress =
|
||||
containsAddress(await effects.runCommand(`cat /proc/net/tcp`), port) ||
|
||||
|
||||
@@ -15,7 +15,7 @@ export const checkWebUrl = async (
|
||||
timeout = 1000,
|
||||
successMessage = `Reached ${url}`,
|
||||
errorMessage = `Error while fetching URL: ${url}`,
|
||||
} = {}
|
||||
} = {},
|
||||
): Promise<CheckResult> => {
|
||||
return Promise.race([effects.fetch(url), timeoutPromise(timeout)])
|
||||
.then((x) => ({
|
||||
|
||||
@@ -5,7 +5,7 @@ export { checkWebUrl } from "./checkWebUrl";
|
||||
|
||||
export function timeoutPromise(ms: number, { message = "Timed out" } = {}) {
|
||||
return new Promise<never>((resolve, reject) =>
|
||||
setTimeout(() => reject(new Error(message)), ms)
|
||||
setTimeout(() => reject(new Error(message)), ms),
|
||||
);
|
||||
}
|
||||
export { runHealthScript };
|
||||
|
||||
@@ -17,7 +17,7 @@ export const runHealthScript = async <A extends string>(
|
||||
errorMessage = `Error while running command: ${runCommand}`,
|
||||
message = (res: string) =>
|
||||
`Have ran script ${runCommand} and the result: ${res}`,
|
||||
} = {}
|
||||
} = {},
|
||||
): Promise<CheckResult> => {
|
||||
const res = await Promise.race([
|
||||
effects.runCommand(runCommand, { timeoutMillis: timeout }),
|
||||
|
||||
@@ -28,7 +28,7 @@ export function noMigrationsDown(): MigrationDownReceipt {
|
||||
return {} as MigrationDownReceipt;
|
||||
}
|
||||
export function migrationDown(
|
||||
fn: () => Promise<unknown>
|
||||
fn: () => Promise<unknown>,
|
||||
): MigrationDownReceipt {
|
||||
fn();
|
||||
return {} as MigrationDownReceipt;
|
||||
@@ -37,7 +37,7 @@ export function migrationDown(
|
||||
export function setupInit(
|
||||
fn: (
|
||||
...args: Parameters<ExpectedExports.init>
|
||||
) => Promise<[MigrationReceipt, ActionReceipt]>
|
||||
) => Promise<[MigrationReceipt, ActionReceipt]>,
|
||||
) {
|
||||
const initFn: ExpectedExports.init = (...args) => fn(...args);
|
||||
return initFn;
|
||||
@@ -46,7 +46,7 @@ export function setupInit(
|
||||
export function setupUninit(
|
||||
fn: (
|
||||
...args: Parameters<ExpectedExports.uninit>
|
||||
) => Promise<[MigrationDownReceipt]>
|
||||
) => Promise<[MigrationDownReceipt]>,
|
||||
) {
|
||||
const uninitFn: ExpectedExports.uninit = (...args) => fn(...args);
|
||||
return uninitFn;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { InterfaceReceipt } from "./interfaceReceipt";
|
||||
type Daemon<
|
||||
Ids extends string | never,
|
||||
Command extends string,
|
||||
Id extends string
|
||||
Id extends string,
|
||||
> = {
|
||||
id: Id;
|
||||
command: ValidIfNoStupidEscape<Command> | [string, ...string[]];
|
||||
@@ -52,7 +52,7 @@ export class Daemons<Ids extends string | never> {
|
||||
private constructor(
|
||||
readonly effects: Effects,
|
||||
readonly started: (onTerm: () => void) => null,
|
||||
readonly daemons?: Daemon<Ids, "command", Ids>[]
|
||||
readonly daemons?: Daemon<Ids, "command", Ids>[],
|
||||
) {}
|
||||
|
||||
static of(config: {
|
||||
@@ -64,7 +64,7 @@ export class Daemons<Ids extends string | never> {
|
||||
return new Daemons<never>(config.effects, config.started);
|
||||
}
|
||||
addDaemon<Id extends string, Command extends string>(
|
||||
newDaemon: Daemon<Ids, Command, Id>
|
||||
newDaemon: Daemon<Ids, Command, Id>,
|
||||
) {
|
||||
const daemons = ((this?.daemons ?? []) as any[]).concat(newDaemon);
|
||||
return new Daemons<Ids | Id>(this.effects, this.started, daemons);
|
||||
@@ -76,7 +76,7 @@ export class Daemons<Ids extends string | never> {
|
||||
const daemons = this.daemons ?? [];
|
||||
for (const daemon of daemons) {
|
||||
const requiredPromise = Promise.all(
|
||||
daemon.requires?.map((id) => daemonsStarted[id]) ?? []
|
||||
daemon.requires?.map((id) => daemonsStarted[id]) ?? [],
|
||||
);
|
||||
daemonsStarted[daemon.id] = requiredPromise.then(async () => {
|
||||
const { command } = daemon;
|
||||
@@ -100,15 +100,15 @@ export class Daemons<Ids extends string | never> {
|
||||
async term() {
|
||||
await Promise.all(
|
||||
Object.values<Promise<DaemonReturned>>(daemonsStarted).map((x) =>
|
||||
x.then((x) => x.term())
|
||||
)
|
||||
x.then((x) => x.term()),
|
||||
),
|
||||
);
|
||||
},
|
||||
async wait() {
|
||||
await Promise.all(
|
||||
Object.values<Promise<DaemonReturned>>(daemonsStarted).map((x) =>
|
||||
x.then((x) => x.wait())
|
||||
)
|
||||
x.then((x) => x.wait()),
|
||||
),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ export class NetworkInterfaceBuilder {
|
||||
basic?: null | { password: string; username: string };
|
||||
path?: string;
|
||||
search?: Record<string, string>;
|
||||
}
|
||||
},
|
||||
) {}
|
||||
|
||||
async exportAddresses(addresses: Iterable<Origin>) {
|
||||
|
||||
@@ -8,7 +8,7 @@ export class Origin {
|
||||
username: string;
|
||||
}
|
||||
| null
|
||||
| undefined
|
||||
| undefined,
|
||||
) {
|
||||
// prettier-ignore
|
||||
const urlAuth = !!(origin) ? `${origin.username}:${origin.password}@` :
|
||||
|
||||
@@ -25,7 +25,7 @@ export const runningMain: (
|
||||
fn: (o: {
|
||||
effects: Effects;
|
||||
started(onTerm: () => void): null;
|
||||
}) => Promise<Daemons<any>>
|
||||
}) => Promise<Daemons<any>>,
|
||||
) => ExpectedExports.main = (fn) => {
|
||||
return async (options) => {
|
||||
const result = await fn(options);
|
||||
|
||||
@@ -21,7 +21,7 @@ export type UnionToIntersection<T> = ((x: T) => any) extends (x: infer R) => any
|
||||
export function setupPropertiesExport(
|
||||
fn: (
|
||||
...args: Parameters<ExpectedExports.properties>
|
||||
) => void | Promise<void> | Promise<(PropertyGroup | PropertyString)[]>
|
||||
) => void | Promise<void> | Promise<(PropertyGroup | PropertyString)[]>,
|
||||
): ExpectedExports.properties {
|
||||
return (async (...args) => {
|
||||
const result = await fn(...args);
|
||||
|
||||
@@ -43,7 +43,7 @@ describe("builder tests", () => {
|
||||
}}`
|
||||
.replaceAll("\n", " ")
|
||||
.replaceAll(/\s{2,}/g, "")
|
||||
.replaceAll(": ", ":")
|
||||
.replaceAll(": ", ":"),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -121,7 +121,7 @@ describe("values", () => {
|
||||
a: Value.boolean({
|
||||
name: "test",
|
||||
}),
|
||||
})
|
||||
}),
|
||||
);
|
||||
const validator = value.validator();
|
||||
validator.unsafeCast({ a: true });
|
||||
@@ -138,7 +138,7 @@ describe("values", () => {
|
||||
name: "a",
|
||||
spec: Config.of({ b: Value.boolean({ name: "b" }) }),
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
const validator = value.validator();
|
||||
validator.unsafeCast({ unionSelectKey: "a", unionValueKey: { b: false } });
|
||||
@@ -156,8 +156,8 @@ describe("values", () => {
|
||||
},
|
||||
{
|
||||
integral: false,
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
);
|
||||
const validator = value.validator();
|
||||
validator.unsafeCast([1, 2, 3]);
|
||||
@@ -174,8 +174,8 @@ describe("Builder List", () => {
|
||||
},
|
||||
{
|
||||
spec: Config.of({ test: Value.boolean({ name: "test" }) }),
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
);
|
||||
const validator = value.validator();
|
||||
validator.unsafeCast([{ test: true }]);
|
||||
@@ -187,8 +187,8 @@ describe("Builder List", () => {
|
||||
{
|
||||
name: "test",
|
||||
},
|
||||
{}
|
||||
)
|
||||
{},
|
||||
),
|
||||
);
|
||||
const validator = value.validator();
|
||||
validator.unsafeCast(["test", "text"]);
|
||||
@@ -200,8 +200,8 @@ describe("Builder List", () => {
|
||||
{
|
||||
name: "test",
|
||||
},
|
||||
{ integral: true }
|
||||
)
|
||||
{ integral: true },
|
||||
),
|
||||
);
|
||||
const validator = value.validator();
|
||||
validator.unsafeCast([12, 45]);
|
||||
|
||||
@@ -21,7 +21,7 @@ describe("Config Types", () => {
|
||||
someList.spec satisfies ListValueSpecOf<"object">;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Failed to figure out the type: " + JSON.stringify(someList)
|
||||
"Failed to figure out the type: " + JSON.stringify(someList),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,5 +407,5 @@ writeConvertedFile(
|
||||
},
|
||||
{
|
||||
startSdk: "../",
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -20,7 +20,7 @@ function isObject(item: unknown): item is object {
|
||||
return !!(item && typeof item === "object" && !Array.isArray(item));
|
||||
}
|
||||
type UnionToIntersection<T> = (T extends any ? (x: T) => any : never) extends (
|
||||
x: infer R
|
||||
x: infer R,
|
||||
) => any
|
||||
? R
|
||||
: never;
|
||||
@@ -61,14 +61,14 @@ testOutput<
|
||||
>()(null);
|
||||
testOutput<InputSpec["rpc"]["advanced"]["servertimeout"], number>()(null);
|
||||
testOutput<InputSpec["advanced"]["peers"]["addnode"][0]["hostname"], string>()(
|
||||
null
|
||||
null,
|
||||
);
|
||||
testOutput<
|
||||
InputSpec["testListUnion"][0]["union"][UnionValueKey]["name"],
|
||||
string
|
||||
>()(null);
|
||||
testOutput<InputSpec["testListUnion"][0]["union"][UnionSelectKey], "lnd">()(
|
||||
null
|
||||
null,
|
||||
);
|
||||
// prettier-ignore
|
||||
// @ts-expect-error Expect that the string is the one above
|
||||
@@ -139,19 +139,19 @@ describe("Inputs", () => {
|
||||
test("test errors", () => {
|
||||
expect(() =>
|
||||
matchInputSpec.unsafeCast(
|
||||
mergeDeep(validInput, { rpc: { advanced: { threads: 0 } } })
|
||||
)
|
||||
mergeDeep(validInput, { rpc: { advanced: { threads: 0 } } }),
|
||||
),
|
||||
).toThrowError();
|
||||
expect(() =>
|
||||
matchInputSpec.unsafeCast(mergeDeep(validInput, { rpc: { enable: 2 } }))
|
||||
matchInputSpec.unsafeCast(mergeDeep(validInput, { rpc: { enable: 2 } })),
|
||||
).toThrowError();
|
||||
|
||||
expect(() =>
|
||||
matchInputSpec.unsafeCast(
|
||||
mergeDeep(validInput, {
|
||||
rpc: { advanced: { serialversion: "testing" } },
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
81
lib/test/wrapperData.test.ts
Normal file
81
lib/test/wrapperData.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { T } from "..";
|
||||
import { utils } from "../util";
|
||||
|
||||
type WrapperType = {
|
||||
config: {
|
||||
someValue: string;
|
||||
};
|
||||
};
|
||||
const todo = <A>(): A => {
|
||||
throw new Error("not implemented");
|
||||
};
|
||||
const noop = () => {};
|
||||
describe("wrapperData", () => {
|
||||
test.skip("types", async () => {
|
||||
utils<WrapperType>(todo<T.Effects>()).setWrapperData(
|
||||
"/config/someValue",
|
||||
"someValue",
|
||||
);
|
||||
utils<WrapperType>(todo<T.Effects>()).setWrapperData(
|
||||
"/config/someValue",
|
||||
|
||||
// @ts-expect-error Type is wrong for the setting value
|
||||
5,
|
||||
);
|
||||
utils<WrapperType>(todo<T.Effects>()).setWrapperData(
|
||||
// @ts-expect-error Path is wrong
|
||||
"/config/someVae3lue",
|
||||
"someValue",
|
||||
);
|
||||
|
||||
todo<T.Effects>().setWrapperData<WrapperType, "/config/someValue">({
|
||||
path: "/config/someValue",
|
||||
value: "someValueIn",
|
||||
});
|
||||
todo<T.Effects>().setWrapperData<WrapperType, "/config/some2Value">({
|
||||
//@ts-expect-error Path is wrong
|
||||
path: "/config/someValue",
|
||||
//@ts-expect-error Path is wrong
|
||||
value: "someValueIn",
|
||||
});
|
||||
todo<T.Effects>().setWrapperData<WrapperType, "/config/someValue">({
|
||||
//@ts-expect-error Path is wrong
|
||||
path: "/config/some2Value",
|
||||
value: "someValueIn",
|
||||
});
|
||||
|
||||
(await utils<WrapperType>(todo<T.Effects>())
|
||||
.getWrapperData("/config/someValue")
|
||||
.const()) satisfies string;
|
||||
(await utils<WrapperType>(todo<T.Effects>())
|
||||
.getWrapperData("/config")
|
||||
.const()) satisfies WrapperType["config"];
|
||||
await utils<WrapperType>(todo<T.Effects>())
|
||||
// @ts-expect-error Path is wrong
|
||||
.getWrapperData("/config/somdsfeValue")
|
||||
.const();
|
||||
(await utils<WrapperType>(todo<T.Effects>())
|
||||
.getWrapperData("/config/someValue")
|
||||
// @ts-expect-error satisfies type is wrong
|
||||
.const()) satisfies number;
|
||||
(await utils<WrapperType>(todo<T.Effects>())
|
||||
// @ts-expect-error Path is wrong
|
||||
.getWrapperData("/config/")
|
||||
.const()) satisfies WrapperType["config"];
|
||||
|
||||
(await todo<T.Effects>().getWrapperData<WrapperType, "/config/someValue">({
|
||||
path: "/config/someValue",
|
||||
callback: noop,
|
||||
})) satisfies string;
|
||||
await todo<T.Effects>().getWrapperData<WrapperType, "/config/someValue">({
|
||||
// @ts-expect-error Path is wrong as in it doesn't match above
|
||||
path: "/config/someV2alue",
|
||||
callback: noop,
|
||||
});
|
||||
await todo<T.Effects>().getWrapperData<WrapperType, "/config/someV2alue">({
|
||||
// @ts-expect-error Path is wrong as in it doesn't exists in wrapper type
|
||||
path: "/config/someV2alue",
|
||||
callback: noop,
|
||||
});
|
||||
});
|
||||
});
|
||||
54
lib/types.ts
54
lib/types.ts
@@ -85,7 +85,7 @@ export namespace ExpectedExports {
|
||||
/** Auto configure is used to make sure that other dependencies have the values t
|
||||
* that this service could use.
|
||||
*/
|
||||
export type autoConfig<Config> = Record<PackageId, AutoConfigure<Config>>;
|
||||
export type autoConfig = Record<PackageId, AutoConfigure>;
|
||||
}
|
||||
export type TimeMs = number;
|
||||
export type VersionString = string;
|
||||
@@ -94,11 +94,19 @@ export type VersionString = string;
|
||||
* AutoConfigure is used as the value to the key of package id,
|
||||
* this is used to make sure that other dependencies have the values that this service could use.
|
||||
*/
|
||||
export type AutoConfigure<Config> = {
|
||||
export type AutoConfigure = {
|
||||
/** Checks are called to make sure that our dependency is in the correct shape. If a known error is returned we know that the dependency needs modification */
|
||||
check(effects: Effects, input: Config): Promise<void>;
|
||||
check(options: {
|
||||
effects: Effects;
|
||||
localConfig: unknown;
|
||||
remoteConfig: unknown;
|
||||
}): Promise<void>;
|
||||
/** This is called after we know that the dependency package needs a new configuration, this would be a transform for defaults */
|
||||
autoConfigure(effects: Effects, input: Config): Promise<Config>;
|
||||
autoConfigure(options: {
|
||||
effects: Effects;
|
||||
localConfig: unknown;
|
||||
remoteConfig: unknown;
|
||||
}): Promise<unknown>;
|
||||
};
|
||||
|
||||
export type ValidIfNoStupidEscape<A> = A extends
|
||||
@@ -172,14 +180,14 @@ export type Effects = {
|
||||
command: ValidIfNoStupidEscape<A> | [string, ...string[]],
|
||||
input?: {
|
||||
timeoutMillis?: number;
|
||||
}
|
||||
},
|
||||
): Promise<string>;
|
||||
runShellDaemon(command: string): {
|
||||
wait(): Promise<string>;
|
||||
term(): Promise<void>;
|
||||
};
|
||||
runDaemon<A extends string>(
|
||||
command: ValidIfNoStupidEscape<A> | [string, ...string[]]
|
||||
command: ValidIfNoStupidEscape<A> | [string, ...string[]],
|
||||
): DaemonReturned;
|
||||
|
||||
/** Uses the chown on the system */
|
||||
@@ -225,7 +233,7 @@ export type Effects = {
|
||||
method?: "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "PATCH";
|
||||
headers?: Record<string, string>;
|
||||
body?: string;
|
||||
}
|
||||
},
|
||||
): Promise<{
|
||||
method: string;
|
||||
ok: boolean;
|
||||
@@ -256,19 +264,19 @@ export type Effects = {
|
||||
};
|
||||
|
||||
/** Get a value in a json like data, can be observed and subscribed */
|
||||
getWrapperData(options: {
|
||||
getWrapperData<WrapperData = never, Path extends string = never>(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?: string;
|
||||
path: Path & EnsureWrapperDataPath<WrapperData, Path>;
|
||||
callback: (config: unknown, previousConfig: unknown) => void;
|
||||
}): Promise<unknown>;
|
||||
}): Promise<ExtractWrapperData<WrapperData, Path>>;
|
||||
|
||||
/** Used to store values that can be accessed and subscribed to */
|
||||
setWrapperData(options: {
|
||||
setWrapperData<WrapperData = never, Path extends string = never>(options: {
|
||||
/** Sets the value for the wrapper at the path, it will override, using the [JsonPath](https://jsonpath.com/) */
|
||||
path?: string;
|
||||
value: unknown;
|
||||
path: Path & EnsureWrapperDataPath<WrapperData, Path>;
|
||||
value: ExtractWrapperData<WrapperData, Path>;
|
||||
}): Promise<void>;
|
||||
|
||||
getLocalHostname(): Promise<string>;
|
||||
@@ -276,14 +284,14 @@ export type Effects = {
|
||||
/** Get the address for another service for tor interfaces */
|
||||
getServiceTorHostname(
|
||||
interfaceId: string,
|
||||
packageId?: string
|
||||
packageId?: string,
|
||||
): Promise<string>;
|
||||
/**
|
||||
* Get the port address for another service
|
||||
*/
|
||||
getServicePortForward(
|
||||
internalPort: number,
|
||||
packageId?: string
|
||||
packageId?: string,
|
||||
): Promise<number>;
|
||||
|
||||
/** When we want to create a link in the front end interfaces, and example is
|
||||
@@ -356,7 +364,7 @@ export type Effects = {
|
||||
*/
|
||||
getSslCertificate: (
|
||||
packageId: string,
|
||||
algorithm?: "ecdsa" | "ed25519"
|
||||
algorithm?: "ecdsa" | "ed25519",
|
||||
) => [string, string, string];
|
||||
/**
|
||||
* @returns PEM encoded ssl key (ecdsa)
|
||||
@@ -379,6 +387,20 @@ export type Effects = {
|
||||
shutdown(): void;
|
||||
};
|
||||
|
||||
// prettier-ignore
|
||||
export type ExtractWrapperData<WrapperData, Path extends string> =
|
||||
Path extends `/${infer A }/${infer Rest }` ? (A extends keyof WrapperData ? ExtractWrapperData<WrapperData[A], `/${Rest}`> : never) :
|
||||
Path extends `/${infer A }` ? (A extends keyof WrapperData ? WrapperData[A] : never) :
|
||||
never
|
||||
|
||||
// prettier-ignore
|
||||
type _EnsureWrapperDataPath<WrapperData, Path extends string, Origin extends string> =
|
||||
Path extends `/${infer A }/${infer Rest }` ? (A extends keyof WrapperData ? ExtractWrapperData<WrapperData[A], `/${Rest}`> : never) :
|
||||
Path extends `/${infer A }` ? (A extends keyof WrapperData ? Origin : never) :
|
||||
never
|
||||
// prettier-ignore
|
||||
export type EnsureWrapperDataPath<WrapperData, Path extends string> = _EnsureWrapperDataPath<WrapperData, Path, Path>
|
||||
|
||||
/* rsync options: https://linux.die.net/man/1/rsync
|
||||
*/
|
||||
export type BackupOptions = {
|
||||
|
||||
19
lib/util/deepEqual.ts
Normal file
19
lib/util/deepEqual.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { object } from "ts-matches";
|
||||
|
||||
export function deepEqual(...args: unknown[]) {
|
||||
if (!object.test(args[args.length - 1])) return args[args.length - 1];
|
||||
const objects = args.filter(object.test);
|
||||
if (objects.length === 0) {
|
||||
for (const x of args) if (x !== args[0]) return false;
|
||||
return true;
|
||||
}
|
||||
if (objects.length !== args.length) return false;
|
||||
const allKeys = new Set(objects.flatMap((x) => Object.keys(x)));
|
||||
for (const key of allKeys) {
|
||||
for (const x of objects) {
|
||||
if (!(key in x)) return false;
|
||||
if (!deepEqual((objects[0] as any)[key], (x as any)[key])) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
16
lib/util/deepMerge.ts
Normal file
16
lib/util/deepMerge.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { object } from "ts-matches";
|
||||
|
||||
export function deepMerge(...args: unknown[]): unknown {
|
||||
const lastItem = (args as any)[args.length - 1];
|
||||
if (!object.test(lastItem)) return lastItem;
|
||||
const objects = args.filter(object.test).filter((x) => !Array.isArray(x));
|
||||
if (objects.length === 0) return lastItem as any;
|
||||
const allKeys = new Set(objects.flatMap((x) => Object.keys(x)));
|
||||
for (const key of allKeys) {
|
||||
const filteredValues = objects.flatMap((x) =>
|
||||
key in x ? [(x as any)[key]] : [],
|
||||
);
|
||||
(objects as any)[0][key] = deepMerge(...filteredValues);
|
||||
}
|
||||
return objects[0] as any;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ type UnReadonly<A> = { -readonly [k in keyof A]: A[k] };
|
||||
declare global {
|
||||
interface Object {
|
||||
entries<T extends {}>(
|
||||
this: T
|
||||
this: T,
|
||||
): Array<{ -readonly [K in keyof T]: [K, T[K]] }[keyof T]>;
|
||||
values<T extends {}>(this: T): Array<T[keyof T]>;
|
||||
keys<T extends {}>(this: T): Array<keyof T>;
|
||||
|
||||
@@ -56,7 +56,7 @@ export class FileHelper<A> {
|
||||
readonly path: string,
|
||||
readonly volume: string,
|
||||
readonly writeData: (dataIn: A) => string,
|
||||
readonly readData: (stringValue: string) => A
|
||||
readonly readData: (stringValue: string) => A,
|
||||
) {}
|
||||
async write(data: A, effects: T.Effects) {
|
||||
let matched;
|
||||
@@ -86,21 +86,21 @@ export class FileHelper<A> {
|
||||
await effects.readFile({
|
||||
path: this.path,
|
||||
volumeId: this.volume,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
static raw<A>(
|
||||
path: string,
|
||||
volume: string,
|
||||
toFile: (dataIn: A) => string,
|
||||
fromFile: (rawData: string) => A
|
||||
fromFile: (rawData: string) => A,
|
||||
) {
|
||||
return new FileHelper<A>(path, volume, toFile, fromFile);
|
||||
}
|
||||
static json<A>(
|
||||
path: string,
|
||||
volume: string,
|
||||
shape: matches.Validator<unknown, A>
|
||||
shape: matches.Validator<unknown, A>,
|
||||
) {
|
||||
return new FileHelper<A>(
|
||||
path,
|
||||
@@ -110,13 +110,13 @@ export class FileHelper<A> {
|
||||
},
|
||||
(inString) => {
|
||||
return shape.unsafeCast(JSON.parse(inString));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
static toml<A extends Record<string, unknown>>(
|
||||
path: string,
|
||||
volume: string,
|
||||
shape: matches.Validator<unknown, A>
|
||||
shape: matches.Validator<unknown, A>,
|
||||
) {
|
||||
return new FileHelper<A>(
|
||||
path,
|
||||
@@ -126,13 +126,13 @@ export class FileHelper<A> {
|
||||
},
|
||||
(inString) => {
|
||||
return shape.unsafeCast(TOML.parse(inString));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
static yaml<A extends Record<string, unknown>>(
|
||||
path: string,
|
||||
volume: string,
|
||||
shape: matches.Validator<unknown, A>
|
||||
shape: matches.Validator<unknown, A>,
|
||||
) {
|
||||
return new FileHelper<A>(
|
||||
path,
|
||||
@@ -142,7 +142,7 @@ export class FileHelper<A> {
|
||||
},
|
||||
(inString) => {
|
||||
return shape.unsafeCast(YAML.parse(inString));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,53 @@
|
||||
import { Parser } from "ts-matches";
|
||||
import { Effects } from "../types";
|
||||
import { Effects, EnsureWrapperDataPath, ExtractWrapperData } from "../types";
|
||||
import { NoAny } from ".";
|
||||
|
||||
export function getWrapperData<A>(
|
||||
export class WrapperData<WrapperData, Path extends string> {
|
||||
constructor(
|
||||
readonly effects: Effects,
|
||||
readonly path: Path & EnsureWrapperDataPath<WrapperData, Path>,
|
||||
readonly options: {
|
||||
/** Defaults to what ever the package currently in */
|
||||
packageId?: string | undefined;
|
||||
} = {},
|
||||
) {}
|
||||
|
||||
const() {
|
||||
return this.effects.getWrapperData<WrapperData, Path>({
|
||||
...this.options,
|
||||
path: this.path as any,
|
||||
callback: this.effects.restart,
|
||||
});
|
||||
}
|
||||
first() {
|
||||
return this.effects.getWrapperData<WrapperData, Path>({
|
||||
...this.options,
|
||||
path: this.path as any,
|
||||
callback: () => {},
|
||||
});
|
||||
}
|
||||
async *overTime() {
|
||||
while (true) {
|
||||
let callback: () => void;
|
||||
const waitForNext = new Promise<void>((resolve) => {
|
||||
callback = resolve;
|
||||
});
|
||||
yield await this.effects.getWrapperData<WrapperData, Path>({
|
||||
...this.options,
|
||||
path: this.path as any,
|
||||
callback: () => callback(),
|
||||
});
|
||||
await waitForNext;
|
||||
}
|
||||
}
|
||||
}
|
||||
export function getWrapperData<WrapperData, Path extends string>(
|
||||
effects: Effects,
|
||||
validator: Parser<unknown, A>,
|
||||
path: Path & EnsureWrapperDataPath<WrapperData, Path>,
|
||||
options: {
|
||||
/** Defaults to what ever the package currently in */
|
||||
packageId?: string | undefined;
|
||||
/** JsonPath */
|
||||
path?: string | undefined;
|
||||
} = {}
|
||||
} = {},
|
||||
) {
|
||||
return {
|
||||
const: () =>
|
||||
effects
|
||||
.getWrapperData({
|
||||
...options,
|
||||
callback: effects.restart,
|
||||
})
|
||||
.then(validator.unsafeCast),
|
||||
first: () =>
|
||||
effects
|
||||
.getWrapperData({
|
||||
...options,
|
||||
callback: () => {},
|
||||
})
|
||||
.then(validator.unsafeCast),
|
||||
overTime: async function* <A>() {
|
||||
while (true) {
|
||||
let callback: () => void;
|
||||
const waitForNext = new Promise<void>((resolve) => {
|
||||
callback = resolve;
|
||||
});
|
||||
yield await effects
|
||||
.getWrapperData({
|
||||
...options,
|
||||
callback: () => callback(),
|
||||
})
|
||||
.then(validator.unsafeCast);
|
||||
await waitForNext;
|
||||
}
|
||||
},
|
||||
};
|
||||
return new WrapperData<WrapperData, Path>(effects, path as any, options);
|
||||
}
|
||||
|
||||
@@ -5,20 +5,23 @@ import nullIfEmpty from "./nullIfEmpty";
|
||||
import { getWrapperData } from "./getWrapperData";
|
||||
import { checkPortListening, checkWebUrl } from "../health/checkFns";
|
||||
import { LocalPort, NetworkBuilder, TorHostname } from "../mainFn";
|
||||
import { ExtractWrapperData } from "../types";
|
||||
|
||||
export { guardAll, typeFromProps } from "./propertiesMatcher";
|
||||
export { default as nullIfEmpty } from "./nullIfEmpty";
|
||||
export { FileHelper } from "./fileHelper";
|
||||
export { getWrapperData } from "./getWrapperData";
|
||||
export { deepEqual } from "./deepEqual";
|
||||
export { deepMerge } from "./deepMerge";
|
||||
|
||||
/** Used to check if the file exists before hand */
|
||||
export const exists = (
|
||||
effects: T.Effects,
|
||||
props: { path: string; volumeId: string }
|
||||
props: { path: string; volumeId: string },
|
||||
) =>
|
||||
effects.metadata(props).then(
|
||||
(_) => true,
|
||||
(_) => false
|
||||
(_) => false,
|
||||
);
|
||||
|
||||
export const isKnownError = (e: unknown): e is T.KnownError =>
|
||||
@@ -26,28 +29,40 @@ export const isKnownError = (e: unknown): e is T.KnownError =>
|
||||
|
||||
type Cdr<A> = A extends [unknown, ...infer Cdr] ? Cdr : [];
|
||||
|
||||
export const utils = (effects: T.Effects) => ({
|
||||
declare const affine: unique symbol;
|
||||
|
||||
function withAffine<B>() {
|
||||
return {} as { [affine]: B };
|
||||
}
|
||||
|
||||
export const utils = <WrapperData = never>(effects: T.Effects) => ({
|
||||
readFile: <A>(fileHelper: FileHelper<A>) => fileHelper.read(effects),
|
||||
writeFile: <A>(fileHelper: FileHelper<A>, data: A) =>
|
||||
fileHelper.write(data, effects),
|
||||
exists: (props: { path: string; volumeId: string }) => exists(effects, props),
|
||||
nullIfEmpty,
|
||||
getWrapperData: <A>(
|
||||
validator: Parser<unknown, A>,
|
||||
getWrapperData: <Path extends string>(
|
||||
path: T.EnsureWrapperDataPath<WrapperData, Path>,
|
||||
options: {
|
||||
validator?: Parser<unknown, ExtractWrapperData<WrapperData, Path>>;
|
||||
/** Defaults to what ever the package currently in */
|
||||
packageId?: string | undefined;
|
||||
/** JsonPath */
|
||||
path?: string | undefined;
|
||||
} = {}
|
||||
) => getWrapperData(effects, validator, options),
|
||||
setWrapperData: <A>(
|
||||
value: A,
|
||||
options: { packageId?: string | undefined; path?: string | undefined } = {}
|
||||
) => effects.setWrapperData({ ...options, value }),
|
||||
} = {},
|
||||
) => getWrapperData<WrapperData, Path>(effects, path as any, options),
|
||||
setWrapperData: <Path extends string | never>(
|
||||
path: T.EnsureWrapperDataPath<WrapperData, Path>,
|
||||
value: ExtractWrapperData<WrapperData, Path>,
|
||||
) => effects.setWrapperData<WrapperData, Path>({ value, path: path as any }),
|
||||
checkPortListening: checkPortListening.bind(null, effects),
|
||||
checkWebUrl: checkWebUrl.bind(null, effects),
|
||||
localPort: LocalPort.bind(null, effects),
|
||||
networkBuilder: NetworkBuilder.of.bind(null, effects),
|
||||
torHostName: TorHostname.of.bind(null, effects),
|
||||
});
|
||||
|
||||
type NeverPossible = { [affine]: string };
|
||||
export type NoAny<A> = NeverPossible extends A
|
||||
? keyof NeverPossible extends keyof A
|
||||
? never
|
||||
: A
|
||||
: A;
|
||||
|
||||
@@ -141,7 +141,7 @@ function charRange(value = "") {
|
||||
*/
|
||||
export function generateDefault(
|
||||
generate: { charset: string; len: number },
|
||||
{ random = () => Math.random() } = {}
|
||||
{ random = () => Math.random() } = {},
|
||||
) {
|
||||
const validCharSets: number[][] = generate.charset
|
||||
.split(",")
|
||||
@@ -152,7 +152,7 @@ export function generateDefault(
|
||||
}
|
||||
const max = validCharSets.reduce(
|
||||
(acc, x) => x.reduce((x, y) => Math.max(x, y), acc),
|
||||
0
|
||||
0,
|
||||
);
|
||||
let i = 0;
|
||||
const answer: string[] = Array(generate.len);
|
||||
@@ -161,7 +161,7 @@ export function generateDefault(
|
||||
const inRange = validCharSets.reduce(
|
||||
(acc, [lower, upper]) =>
|
||||
acc || (nextValue >= lower && nextValue <= upper),
|
||||
false
|
||||
false,
|
||||
);
|
||||
if (!inRange) continue;
|
||||
answer[i] = String.fromCharCode(nextValue);
|
||||
@@ -186,7 +186,7 @@ export function matchNumberWithRange(range: string) {
|
||||
? "any"
|
||||
: left === "["
|
||||
? `greaterThanOrEqualTo${leftValue}`
|
||||
: `greaterThan${leftValue}`
|
||||
: `greaterThan${leftValue}`,
|
||||
)
|
||||
.validate(
|
||||
// prettier-ignore
|
||||
@@ -196,7 +196,7 @@ export function matchNumberWithRange(range: string) {
|
||||
// prettier-ignore
|
||||
rightValue === "*" ? "any" :
|
||||
right === "]" ? `lessThanOrEqualTo${rightValue}` :
|
||||
`lessThan${rightValue}`
|
||||
`lessThan${rightValue}`,
|
||||
);
|
||||
}
|
||||
function withIntegral(parser: Parser<unknown, number>, value: unknown) {
|
||||
@@ -219,7 +219,7 @@ function defaultRequired<A>(parser: Parser<unknown, A>, value: unknown) {
|
||||
if (matchDefault.test(value)) {
|
||||
if (isGenerator(value.default)) {
|
||||
return parser.defaultTo(
|
||||
parser.unsafeCast(generateDefault(value.default))
|
||||
parser.unsafeCast(generateDefault(value.default)),
|
||||
);
|
||||
}
|
||||
return parser.defaultTo(value.default);
|
||||
@@ -237,7 +237,7 @@ function defaultRequired<A>(parser: Parser<unknown, A>, value: unknown) {
|
||||
* @returns
|
||||
*/
|
||||
export function guardAll<A extends ValueSpecAny>(
|
||||
value: A
|
||||
value: A,
|
||||
): Parser<unknown, GuardAll<A>> {
|
||||
if (!isType.test(value)) {
|
||||
return unknown as any;
|
||||
@@ -255,7 +255,7 @@ export function guardAll<A extends ValueSpecAny>(
|
||||
case "number":
|
||||
return defaultRequired(
|
||||
withIntegral(withRange(value), value),
|
||||
value
|
||||
value,
|
||||
) as any;
|
||||
|
||||
case "object":
|
||||
@@ -274,7 +274,7 @@ export function guardAll<A extends ValueSpecAny>(
|
||||
matches
|
||||
.arrayOf(guardAll(spec as any))
|
||||
.validate((x) => rangeValidate(x.length), "valid length"),
|
||||
value
|
||||
value,
|
||||
) as any;
|
||||
}
|
||||
case "select":
|
||||
@@ -282,7 +282,7 @@ export function guardAll<A extends ValueSpecAny>(
|
||||
const valueKeys = Object.keys(value.values);
|
||||
return defaultRequired(
|
||||
literals(valueKeys[0], ...valueKeys),
|
||||
value
|
||||
value,
|
||||
) as any;
|
||||
}
|
||||
return unknown as any;
|
||||
@@ -290,19 +290,19 @@ export function guardAll<A extends ValueSpecAny>(
|
||||
case "multiselect":
|
||||
if (matchValues.test(value)) {
|
||||
const maybeAddRangeValidate = <X extends Validator<unknown, B[]>, B>(
|
||||
x: X
|
||||
x: X,
|
||||
) => {
|
||||
if (!matchRange.test(value)) return x;
|
||||
return x.validate(
|
||||
(x) => matchNumberWithRange(value.range).test(x.length),
|
||||
"validLength"
|
||||
"validLength",
|
||||
);
|
||||
};
|
||||
|
||||
const valueKeys = Object.keys(value.values);
|
||||
return defaultRequired(
|
||||
maybeAddRangeValidate(arrayOf(literals(valueKeys[0], ...valueKeys))),
|
||||
value
|
||||
value,
|
||||
) as any;
|
||||
}
|
||||
return unknown as any;
|
||||
@@ -316,8 +316,8 @@ export function guardAll<A extends ValueSpecAny>(
|
||||
object({
|
||||
unionSelectKey: literals(name),
|
||||
unionValueKey: typeFromProps(spec),
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
) as any;
|
||||
}
|
||||
return unknown as any;
|
||||
@@ -334,7 +334,7 @@ export function guardAll<A extends ValueSpecAny>(
|
||||
* @returns
|
||||
*/
|
||||
export function typeFromProps<A extends InputSpec>(
|
||||
valueDictionary: A
|
||||
valueDictionary: A,
|
||||
): Parser<unknown, TypeFromProps<A>> {
|
||||
if (!recordString.test(valueDictionary)) return unknown as any;
|
||||
return object(
|
||||
@@ -342,7 +342,7 @@ export function typeFromProps<A extends InputSpec>(
|
||||
Object.entries(valueDictionary).map(([key, value]) => [
|
||||
key,
|
||||
guardAll(value),
|
||||
])
|
||||
)
|
||||
]),
|
||||
),
|
||||
) as any;
|
||||
}
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "start-sdk",
|
||||
"version": "0.4.0-lib0.charlie30",
|
||||
"version": "0.4.0-lib0.charlie31",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "start-sdk",
|
||||
"version": "0.4.0-lib0.charlie30",
|
||||
"version": "0.4.0-lib0.charlie31",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^2.2.5",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "start-sdk",
|
||||
"version": "0.4.0-lib0.charlie30",
|
||||
"version": "0.4.0-lib0.charlie31",
|
||||
"description": "For making the patterns that are wanted in making services for the startOS.",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
@@ -26,6 +26,12 @@
|
||||
"ts-matches": "^5.4.1",
|
||||
"yaml": "^2.2.1"
|
||||
},
|
||||
"prettier": {
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": false
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.4.0",
|
||||
"@types/lodash": "^4.14.191",
|
||||
|
||||
@@ -6,16 +6,16 @@ import { unionSelectKey } from "../lib/config/configTypes";
|
||||
export async function writeConvertedFile(
|
||||
file: string,
|
||||
inputData: Promise<any> | any,
|
||||
options: Parameters<typeof makeFileContent>[1]
|
||||
options: Parameters<typeof makeFileContent>[1],
|
||||
) {
|
||||
await fs.writeFile(file, await makeFileContent(inputData, options), (err) =>
|
||||
console.error(err)
|
||||
console.error(err),
|
||||
);
|
||||
}
|
||||
|
||||
export default async function makeFileContent(
|
||||
inputData: Promise<any> | any,
|
||||
{ startSdk = "start-sdk" } = {}
|
||||
{ startSdk = "start-sdk" } = {},
|
||||
) {
|
||||
const outputLines: string[] = [];
|
||||
outputLines.push(`
|
||||
@@ -27,10 +27,10 @@ export default async function makeFileContent(
|
||||
const configName = newConst("InputSpec", convertInputSpec(data));
|
||||
const configMatcherName = newConst(
|
||||
"matchInputSpec",
|
||||
`${configName}.validator()`
|
||||
`${configName}.validator()`,
|
||||
);
|
||||
outputLines.push(
|
||||
`export type InputSpec = typeof ${configMatcherName}._TYPE;`
|
||||
`export type InputSpec = typeof ${configMatcherName}._TYPE;`,
|
||||
);
|
||||
|
||||
return outputLines.join("\n");
|
||||
@@ -62,7 +62,7 @@ export default async function makeFileContent(
|
||||
placeholder: value.placeholder || null,
|
||||
},
|
||||
null,
|
||||
2
|
||||
2,
|
||||
)})`;
|
||||
}
|
||||
return `Value.string(${JSON.stringify(
|
||||
@@ -78,7 +78,7 @@ export default async function makeFileContent(
|
||||
patternDescription: value["pattern-description"] || null,
|
||||
},
|
||||
null,
|
||||
2
|
||||
2,
|
||||
)})`;
|
||||
}
|
||||
case "number": {
|
||||
@@ -95,7 +95,7 @@ export default async function makeFileContent(
|
||||
placeholder: value.placeholder || null,
|
||||
},
|
||||
null,
|
||||
2
|
||||
2,
|
||||
)})`;
|
||||
}
|
||||
case "boolean": {
|
||||
@@ -107,7 +107,7 @@ export default async function makeFileContent(
|
||||
warning: value.warning || null,
|
||||
},
|
||||
null,
|
||||
2
|
||||
2,
|
||||
)})`;
|
||||
}
|
||||
case "enum": {
|
||||
@@ -118,7 +118,7 @@ export default async function makeFileContent(
|
||||
const values = Object.fromEntries(
|
||||
Array.from(allValueNames)
|
||||
.filter(string.test)
|
||||
.map((key) => [key, value?.spec?.["value-names"]?.[key] || key])
|
||||
.map((key) => [key, value?.spec?.["value-names"]?.[key] || key]),
|
||||
);
|
||||
return `Value.select(${JSON.stringify(
|
||||
{
|
||||
@@ -130,13 +130,13 @@ export default async function makeFileContent(
|
||||
values,
|
||||
},
|
||||
null,
|
||||
2
|
||||
2,
|
||||
)} as const)`;
|
||||
}
|
||||
case "object": {
|
||||
const specName = newConst(
|
||||
value.name + "_spec",
|
||||
convertInputSpec(value.spec)
|
||||
convertInputSpec(value.spec),
|
||||
);
|
||||
return `Value.object({
|
||||
name: ${JSON.stringify(value.name || null)},
|
||||
@@ -147,7 +147,7 @@ export default async function makeFileContent(
|
||||
case "union": {
|
||||
const variants = newConst(
|
||||
value.name + "_variants",
|
||||
convertVariants(value.variants, value.tag["variant-names"] || {})
|
||||
convertVariants(value.variants, value.tag["variant-names"] || {}),
|
||||
);
|
||||
|
||||
return `Value.union({
|
||||
@@ -183,7 +183,7 @@ export default async function makeFileContent(
|
||||
warning: value.warning || null,
|
||||
},
|
||||
null,
|
||||
2
|
||||
2,
|
||||
)}, ${JSON.stringify({
|
||||
masked: value?.spec?.masked || false,
|
||||
placeholder: value?.spec?.placeholder || null,
|
||||
@@ -201,7 +201,7 @@ export default async function makeFileContent(
|
||||
warning: value.warning || null,
|
||||
},
|
||||
null,
|
||||
2
|
||||
2,
|
||||
)}, ${JSON.stringify({
|
||||
range: value?.spec?.range || null,
|
||||
integral: value?.spec?.integral || false,
|
||||
@@ -212,7 +212,7 @@ export default async function makeFileContent(
|
||||
case "enum": {
|
||||
const allValueNames = new Set(
|
||||
...(value?.spec?.["values"] || []),
|
||||
...Object.keys(value?.spec?.["value-names"] || {})
|
||||
...Object.keys(value?.spec?.["value-names"] || {}),
|
||||
);
|
||||
const values = Object.fromEntries(
|
||||
Array.from(allValueNames)
|
||||
@@ -220,7 +220,7 @@ export default async function makeFileContent(
|
||||
.map((key: string) => [
|
||||
key,
|
||||
value?.spec?.["value-names"]?.[key] || key,
|
||||
])
|
||||
]),
|
||||
);
|
||||
return `Value.multiselect(${JSON.stringify(
|
||||
{
|
||||
@@ -232,13 +232,13 @@ export default async function makeFileContent(
|
||||
values,
|
||||
},
|
||||
null,
|
||||
2
|
||||
2,
|
||||
)})`;
|
||||
}
|
||||
case "object": {
|
||||
const specName = newConst(
|
||||
value.name + "_spec",
|
||||
convertInputSpec(value.spec.spec)
|
||||
convertInputSpec(value.spec.spec),
|
||||
);
|
||||
return `List.obj({
|
||||
name: ${JSON.stringify(value.name || null)},
|
||||
@@ -257,8 +257,8 @@ export default async function makeFileContent(
|
||||
value.name + "_variants",
|
||||
convertVariants(
|
||||
value.spec.variants,
|
||||
value.spec["variant-names"] || {}
|
||||
)
|
||||
value.spec["variant-names"] || {},
|
||||
),
|
||||
);
|
||||
const unionValueName = newConst(
|
||||
value.name + "_union",
|
||||
@@ -266,13 +266,13 @@ export default async function makeFileContent(
|
||||
Value.union({
|
||||
name: ${JSON.stringify(value?.spec?.tag?.name || null)},
|
||||
description: ${JSON.stringify(
|
||||
value?.spec?.tag?.description || null
|
||||
value?.spec?.tag?.description || null,
|
||||
)},
|
||||
warning: ${JSON.stringify(value?.spec?.tag?.warning || null)},
|
||||
required: ${JSON.stringify(!(value?.spec?.tag?.nullable || false))},
|
||||
default: ${JSON.stringify(value?.spec?.default || null)},
|
||||
}, ${variants})
|
||||
`
|
||||
`,
|
||||
);
|
||||
const listConfig = newConst(
|
||||
value.name + "_list_config",
|
||||
@@ -280,7 +280,7 @@ export default async function makeFileContent(
|
||||
Config.of({
|
||||
"union": ${unionValueName}
|
||||
})
|
||||
`
|
||||
`,
|
||||
);
|
||||
return `List.obj({
|
||||
name:${JSON.stringify(value.name || null)},
|
||||
@@ -300,7 +300,7 @@ export default async function makeFileContent(
|
||||
|
||||
function convertVariants(
|
||||
variants: Record<string, unknown>,
|
||||
variantNames: Record<string, string>
|
||||
variantNames: Record<string, string>,
|
||||
): string {
|
||||
let answer = "Variants.of({";
|
||||
for (const [key, value] of Object.entries(variants)) {
|
||||
|
||||
Reference in New Issue
Block a user