chore: Add the tools for packaging npm

This commit is contained in:
BluJ
2023-02-16 14:08:53 -07:00
parent 93e27d8bf4
commit df89119193
17 changed files with 477 additions and 209 deletions

3
.gitignore vendored
View File

@@ -1 +1,2 @@
.vscode
.vscode
lib

View File

@@ -1,5 +1,15 @@
TEST_FILES := $(shell find ./**/*.ts)
test: $(TEST_FILES)
TS_FILES := $(shell find ./**/*.ts )
version = $(shell git tag --sort=committerdate | tail -1)
test: $(TS_FILES)
deno test test.ts
deno check mod.ts
bundle: test fmt $(TS_FILES)
echo "Version: $(version)"
deno run --allow-net --allow-write --allow-env --allow-run --allow-read build.ts $(version)
fmt:
deno fmt
publish: bundle
cd lib && npm publish

30
build.ts Normal file
View File

@@ -0,0 +1,30 @@
// ex. scripts/build_npm.ts
import { build, emptyDir } from "https://deno.land/x/dnt@0.33.1/mod.ts";
await emptyDir("./lib");
await build({
entryPoints: ["./mod.ts"],
outDir: "./lib",
shims: {
// see JS docs for overview and more options
deno: true,
},
package: {
// package.json properties
name: "embassy-sdk-ts",
version: Deno.args[0],
description: "Sdk that is used by the embassy packages, and the OS.",
license: "MIT",
sideEffects: false,
repository: {
type: "git",
url: "git+https://github.com/Start9Labs/embassy-sdk-ts.git",
},
bugs: {
url: "https://github.com/Start9Labs/embassy-sdk-ts/issues",
},
},
});
// post build steps
Deno.copyFileSync("./README.md", "lib/README.md");

View File

@@ -3,7 +3,7 @@ import { YAML } from "../dependencies.ts";
import { matches } from "../dependencies.ts";
import { ExpectedExports } from "../types.ts";
import { ConfigSpec } from "../types/config-types.ts";
import { typeFromProps, TypeFromProps } from "../utils/propertiesMatcher.ts";
import { TypeFromProps, typeFromProps } from "../utils/propertiesMatcher.ts";
const { any, string, dictionary } = matches;
@@ -18,8 +18,7 @@ const matchConfig = dictionary([string, any]);
* @returns
*/
export const getConfig =
(spec: ConfigSpec): ExpectedExports.getConfig =>
async (effects) => {
(spec: ConfigSpec): ExpectedExports.getConfig => async (effects) => {
const config = await effects
.readFile({
path: "start9/config.yaml",
@@ -49,8 +48,11 @@ export const getConfig =
* @returns A funnction for getConfig and the matcher for the spec sent in
*/
export const getConfigAndMatcher = <Spec extends ConfigSpec>(
spec: Config<Spec> | Spec
): [ExpectedExports.getConfig, matches.Parser<unknown, TypeFromProps<Spec>>] => {
spec: Config<Spec> | Spec,
): [
ExpectedExports.getConfig,
matches.Parser<unknown, TypeFromProps<Spec>>,
] => {
const specBuilt: Spec = spec instanceof Config ? spec.build() : spec;
return [

View File

@@ -17,11 +17,17 @@ export interface NoRepeat<version extends string, type extends "up" | "down"> {
* @param noFail (optional, default:false) whether or not to fail the migration if fn throws an error
* @returns a migraion function
*/
export function updateConfig<version extends string, type extends "up" | "down">(
fn: (config: ConfigSpec, effects: T.Effects) => ConfigSpec | Promise<ConfigSpec>,
export function updateConfig<
version extends string,
type extends "up" | "down",
>(
fn: (
config: ConfigSpec,
effects: T.Effects,
) => ConfigSpec | Promise<ConfigSpec>,
configured: boolean,
noRepeat?: NoRepeat<version, type>,
noFail = false
noFail = false,
): M.MigrationFn<version, type> {
return M.migrationFn(async (effects: T.Effects) => {
await noRepeatGuard(effects, noRepeat, async () => {
@@ -43,15 +49,23 @@ export function updateConfig<version extends string, type extends "up" | "down">
});
}
export async function noRepeatGuard<version extends string, type extends "up" | "down">(
export async function noRepeatGuard<
version extends string,
type extends "up" | "down",
>(
effects: T.Effects,
noRepeat: NoRepeat<version, type> | undefined,
fn: () => Promise<void>
fn: () => Promise<void>,
): Promise<void> {
if (!noRepeat) {
return fn();
}
if (!(await util.exists(effects, { path: "start9/migrations", volumeId: "main" }))) {
if (
!(await util.exists(effects, {
path: "start9/migrations",
volumeId: "main",
}))
) {
await effects.createDir({ path: "start9/migrations", volumeId: "main" });
}
const migrationPath = {
@@ -74,9 +88,14 @@ export async function noRepeatGuard<version extends string, type extends "up" |
export async function initNoRepeat<versions extends string>(
effects: T.Effects,
migrations: M.MigrationMapping<versions>,
startingVersion: string
startingVersion: string,
) {
if (!(await util.exists(effects, { path: "start9/migrations", volumeId: "main" }))) {
if (
!(await util.exists(effects, {
path: "start9/migrations",
volumeId: "main",
}))
) {
const starting = EmVer.parse(startingVersion);
await effects.createDir({ path: "start9/migrations", volumeId: "main" });
for (const version in migrations) {
@@ -94,11 +113,15 @@ export async function initNoRepeat<versions extends string>(
export function fromMapping<versions extends string>(
migrations: M.MigrationMapping<versions>,
currentVersion: string
currentVersion: string,
): T.ExpectedExports.migration {
const inner = M.fromMapping(migrations, currentVersion);
return async (effects: T.Effects, version: string, direction?: unknown) => {
await initNoRepeat(effects, migrations, direction === "from" ? version : currentVersion);
await initNoRepeat(
effects,
migrations,
direction === "from" ? version : currentVersion,
);
return inner(effects, version, direction);
};
}

View File

@@ -10,7 +10,11 @@ import { ConfigSpec } from "../types/config-types.ts";
* @param depends_on This would be the depends on for condition depends_on
* @returns
*/
export const setConfig = async (effects: Effects, newConfig: ConfigSpec, dependsOn: DependsOn = {}) => {
export const setConfig = async (
effects: Effects,
newConfig: ConfigSpec,
dependsOn: DependsOn = {},
) => {
await effects.createDir({
path: "start9",
volumeId: "main",

View File

@@ -6,10 +6,16 @@ export class Config<A extends ConfigSpec> extends IBuilder<A> {
static empty() {
return new Config({});
}
static withValue<K extends string, B extends ValueSpec>(key: K, value: Value<B>) {
static withValue<K extends string, B extends ValueSpec>(
key: K,
value: Value<B>,
) {
return Config.empty().withValue(key, value);
}
static addValue<K extends string, B extends ValueSpec>(key: K, value: Value<B>) {
static addValue<K extends string, B extends ValueSpec>(
key: K,
value: Value<B>,
) {
return Config.empty().withValue(key, value);
}
@@ -23,15 +29,19 @@ export class Config<A extends ConfigSpec> extends IBuilder<A> {
return new Config(answer);
}
withValue<K extends string, B extends ValueSpec>(key: K, value: Value<B>) {
return new Config({
...this.a,
[key]: value.build(),
} as A & { [key in K]: B });
return new Config(
{
...this.a,
[key]: value.build(),
} as A & { [key in K]: B },
);
}
addValue<K extends string, B extends ValueSpec>(key: K, value: Value<B>) {
return new Config({
...this.a,
[key]: value.build(),
} as A & { [key in K]: B });
return new Config(
{
...this.a,
[key]: value.build(),
} as A & { [key in K]: B },
);
}
}

View File

@@ -24,8 +24,8 @@ test("Pointer", () => {
"package-id": "bitcoind",
interface: "peer",
warning: null,
})
)
}),
),
).build();
expect(JSON.stringify(bitcoinPropertiesBuilt)).toEqual(
/*json*/ `{
@@ -41,6 +41,6 @@ test("Pointer", () => {
}}`
.replaceAll("\n", " ")
.replaceAll(/\s{2,}/g, "")
.replaceAll(": ", ":")
.replaceAll(": ", ":"),
);
});

View File

@@ -4,7 +4,12 @@ import { Default, NullableDefault, NumberSpec, StringSpec } from "./value.ts";
import { Description } from "./value.ts";
import * as T from "../types.ts";
import { Variants } from "./variants.ts";
import { ConfigSpec, UniqueBy, ValueSpecList, ValueSpecListOf } from "../types/config-types.ts";
import {
ConfigSpec,
UniqueBy,
ValueSpecList,
ValueSpecListOf,
} from "../types/config-types.ts";
export class List<A extends ValueSpecList> extends IBuilder<A> {
// // deno-lint-ignore ban-types
@@ -16,14 +21,24 @@ export class List<A extends ValueSpecList> extends IBuilder<A> {
// });
// }
static string<A extends Description & Default<string[]> & { range: string; spec: StringSpec }>(a: A) {
static string<
A extends Description & Default<string[]> & {
range: string;
spec: StringSpec;
},
>(a: A) {
return new List({
type: "list" as const,
subtype: "string" as const,
...a,
} as ValueSpecListOf<"string">);
}
static number<A extends Description & Default<number[]> & { range: string; spec: NumberSpec }>(a: A) {
static number<
A extends Description & Default<number[]> & {
range: string;
spec: NumberSpec;
},
>(a: A) {
return new List({
type: "list" as const,
subtype: "number" as const,
@@ -31,8 +46,10 @@ export class List<A extends ValueSpecList> extends IBuilder<A> {
});
}
static enum<
A extends Description &
Default<string[]> & {
A extends
& Description
& Default<string[]>
& {
range: string;
spec: {
values: string[];
@@ -40,7 +57,7 @@ export class List<A extends ValueSpecList> extends IBuilder<A> {
[key: string]: string;
};
};
}
},
>(a: A) {
return new List({
type: "list" as const,
@@ -49,19 +66,23 @@ export class List<A extends ValueSpecList> extends IBuilder<A> {
});
}
static objectV<
A extends Description &
Default<Record<string, unknown>[]> & {
A extends
& Description
& Default<Record<string, unknown>[]>
& {
range: string;
spec: {
spec: Config<ConfigSpec>;
"display-as": null | string;
"unique-by": null | UniqueBy;
};
}
},
>(a: A) {
const { spec: previousSpec, ...rest } = a;
const { spec: previousSpecSpec, ...restSpec } = previousSpec;
const specSpec = previousSpecSpec.build() as BuilderExtract<A["spec"]["spec"]>;
const specSpec = previousSpecSpec.build() as BuilderExtract<
A["spec"]["spec"]
>;
const spec = {
...restSpec,
spec: specSpec,
@@ -77,8 +98,10 @@ export class List<A extends ValueSpecList> extends IBuilder<A> {
});
}
static union<
A extends Description &
Default<string[]> & {
A extends
& Description
& Default<string[]>
& {
range: string;
spec: {
tag: {
@@ -94,11 +117,13 @@ export class List<A extends ValueSpecList> extends IBuilder<A> {
"unique-by": UniqueBy;
default: string;
};
}
},
>(a: A) {
const { spec: previousSpec, ...rest } = a;
const { variants: previousVariants, ...restSpec } = previousSpec;
const variants = previousVariants.build() as BuilderExtract<A["spec"]["variants"]>;
const variants = previousVariants.build() as BuilderExtract<
A["spec"]["variants"]
>;
const spec = {
...restSpec,
variants,

View File

@@ -3,7 +3,9 @@ import { IBuilder } from "./builder.ts";
import { Description } from "./value.ts";
export class Pointer<A extends ValueSpec> extends IBuilder<A> {
static packageTorKey<A extends Description & { "package-id": string; interface: string }>(a: A) {
static packageTorKey<
A extends Description & { "package-id": string; interface: string },
>(a: A) {
return new Pointer({
type: "pointer" as const,
subtype: "package" as const,
@@ -11,7 +13,9 @@ export class Pointer<A extends ValueSpec> extends IBuilder<A> {
...a,
});
}
static packageTorAddress<A extends Description & { "package-id": string; interface: string }>(a: A) {
static packageTorAddress<
A extends Description & { "package-id": string; interface: string },
>(a: A) {
return new Pointer({
type: "pointer" as const,
subtype: "package" as const,
@@ -19,7 +23,9 @@ export class Pointer<A extends ValueSpec> extends IBuilder<A> {
...a,
});
}
static packageLanAddress<A extends Description & { "package-id": string; interface: string }>(a: A) {
static packageLanAddress<
A extends Description & { "package-id": string; interface: string },
>(a: A) {
return new Pointer({
type: "pointer" as const,
subtype: "package" as const,
@@ -28,7 +34,12 @@ export class Pointer<A extends ValueSpec> extends IBuilder<A> {
});
}
static packageConfig<
A extends Description & { "package-id": string; selector: string; multi: boolean; interface: string }
A extends Description & {
"package-id": string;
selector: string;
multi: boolean;
interface: string;
},
>(a: A) {
return new Pointer({
type: "pointer" as const,
@@ -37,8 +48,15 @@ export class Pointer<A extends ValueSpec> extends IBuilder<A> {
...a,
});
}
static system<A extends Description & { "package-id": string; selector: string; multi: boolean; interface: string }>(
a: A
static system<
A extends Description & {
"package-id": string;
selector: string;
multi: boolean;
interface: string;
},
>(
a: A,
) {
return new Pointer({
type: "pointer" as const,

View File

@@ -9,16 +9,15 @@ import {
ValueSpec,
ValueSpecList,
ValueSpecNumber,
ValueSpecObject,
ValueSpecString,
} from "../types/config-types.ts";
export type DefaultString =
| string
| {
charset: string | null | undefined;
len: number;
};
charset: string | null | undefined;
len: number;
};
export type Description = {
name: string;
description: string | null;
@@ -31,18 +30,20 @@ export type NullableDefault<A> = {
default?: A;
};
export type StringSpec = {
copyable: boolean | null;
masked: boolean | null;
placeholder: string | null;
} & (
| {
export type StringSpec =
& {
copyable: boolean | null;
masked: boolean | null;
placeholder: string | null;
}
& (
| {
pattern: string;
"pattern-description": string;
}
// deno-lint-ignore ban-types
| {}
);
// deno-lint-ignore ban-types
| {}
);
export type NumberSpec = {
range: string;
integral: boolean;
@@ -60,21 +61,34 @@ export class Value<A extends ValueSpec> extends IBuilder<A> {
...a,
});
}
static string<A extends Description & NullableDefault<DefaultString> & Nullable & StringSpec>(a: A) {
static string<
A extends
& Description
& NullableDefault<DefaultString>
& Nullable
& StringSpec,
>(a: A) {
return new Value({
type: "string" as const,
...a,
} as ValueSpecString);
}
static number<A extends Description & NullableDefault<number> & Nullable & NumberSpec>(a: A) {
static number<
A extends Description & NullableDefault<number> & Nullable & NumberSpec,
>(a: A) {
return new Value({
type: "number" as const,
...a,
} as ValueSpecNumber);
}
static enum<
A extends Description &
Default<string> & { values: readonly string[] | string[]; "value-names": Record<string, string> }
A extends
& Description
& Default<string>
& {
values: readonly string[] | string[];
"value-names": Record<string, string>;
},
>(a: A) {
return new Value({
type: "enum" as const,
@@ -91,7 +105,7 @@ export class Value<A extends ValueSpec> extends IBuilder<A> {
"unique-by": null | string;
spec: Config<ConfigSpec>;
"value-names": Record<string, string>;
}
},
>(a: A) {
const { spec: previousSpec, ...rest } = a;
const spec = previousSpec.build() as BuilderExtract<A["spec"]>;
@@ -102,8 +116,10 @@ export class Value<A extends ValueSpec> extends IBuilder<A> {
});
}
static union<
A extends Description &
Default<string> & {
A extends
& Description
& Default<string>
& {
tag: {
id: string;
name: string;
@@ -116,7 +132,7 @@ export class Value<A extends ValueSpec> extends IBuilder<A> {
variants: Variants<{ [key: string]: ConfigSpec }>;
"display-as": string | null;
"unique-by": UniqueBy;
}
},
>(a: A) {
const { variants: previousVariants, ...rest } = a;
const variants = previousVariants.build() as BuilderExtract<A["variants"]>;

View File

@@ -2,11 +2,12 @@ import { ConfigSpec } from "../types/config-types.ts";
import { BuilderExtract, IBuilder } from "./builder.ts";
import { Config } from "./mod.ts";
export class Variants<A extends { [key: string]: ConfigSpec }> extends IBuilder<A> {
export class Variants<A extends { [key: string]: ConfigSpec }>
extends IBuilder<A> {
static of<
A extends {
[key: string]: Config<ConfigSpec>;
}
},
>(a: A) {
// deno-lint-ignore no-explicit-any
const variants: { [K in keyof A]: BuilderExtract<A[K]> } = {} as any;
@@ -20,14 +21,22 @@ export class Variants<A extends { [key: string]: ConfigSpec }> extends IBuilder<
static empty() {
return Variants.of({});
}
static withVariant<K extends string, B extends ConfigSpec>(key: K, value: Config<B>) {
static withVariant<K extends string, B extends ConfigSpec>(
key: K,
value: Config<B>,
) {
return Variants.empty().withVariant(key, value);
}
withVariant<K extends string, B extends ConfigSpec>(key: K, value: Config<B>) {
return new Variants({
...this.a,
[key]: value.build(),
} as A & { [key in K]: B });
withVariant<K extends string, B extends ConfigSpec>(
key: K,
value: Config<B>,
) {
return new Variants(
{
...this.a,
[key]: value.build(),
} as A & { [key in K]: B },
);
}
}

View File

@@ -4,7 +4,10 @@ import { ConfigSpec } from "./types/config-types.ts";
// deno-lint-ignore no-namespace
export namespace ExpectedExports {
/** Set configuration is called after we have modified and saved the configuration in the embassy ui. Use this to make a file for the docker to read from for configuration. */
export type setConfig = (effects: Effects, input: ConfigSpec) => Promise<ResultType<SetResult>>;
export type setConfig = (
effects: Effects,
input: ConfigSpec,
) => Promise<ResultType<SetResult>>;
/** Get configuration returns a shape that describes the format that the embassy ui will generate, and later send to the set config */
export type getConfig = (effects: Effects) => Promise<ResultType<ConfigRes>>;
/** These are how we make sure the our dependency configurations are valid and if not how to fix them. */
@@ -12,23 +15,34 @@ export namespace ExpectedExports {
/** For backing up service data though the embassyOS UI */
export type createBackup = (effects: Effects) => Promise<ResultType<unknown>>;
/** For restoring service data that was previously backed up using the embassyOS UI create backup flow. Backup restores are also triggered via the embassyOS UI, or doing a system restore flow during setup. */
export type restoreBackup = (effects: Effects) => Promise<ResultType<unknown>>;
export type restoreBackup = (
effects: Effects,
) => Promise<ResultType<unknown>>;
/** Properties are used to get values from the docker, like a username + password, what ports we are hosting from */
export type properties = (effects: Effects) => Promise<ResultType<Properties>>;
export type properties = (
effects: Effects,
) => Promise<ResultType<Properties>>;
/** Health checks are used to determine if the service is working properly after starting
* A good use case is if we are using a web server, seeing if we can get to the web server.
*/
export type health = {
/** Should be the health check id */
[id: string]: (effects: Effects, dateMs: number) => Promise<ResultType<unknown>>;
[id: string]: (
effects: Effects,
dateMs: number,
) => Promise<ResultType<unknown>>;
};
/**
* Migrations are used when we are changing versions when updating/ downgrading.
* There are times that we need to move files around, and do other operations during a migration.
*/
export type migration = (effects: Effects, version: string, ...args: unknown[]) => Promise<ResultType<MigrationRes>>;
export type migration = (
effects: Effects,
version: string,
...args: unknown[]
) => Promise<ResultType<MigrationRes>>;
/**
* Actions are used so we can effect the service, like deleting a directory.
@@ -36,7 +50,10 @@ export namespace ExpectedExports {
* service starting, and that file would indicate that it would rescan all the data.
*/
export type action = {
[id: string]: (effects: Effects, config?: ConfigSpec) => Promise<ResultType<ActionResult>>;
[id: string]: (
effects: Effects,
config?: ConfigSpec,
) => Promise<ResultType<ActionResult>>;
};
/**
@@ -55,7 +72,9 @@ export type ConfigRes = {
/** Used to reach out from the pure js runtime */
export type Effects = {
/** Usable when not sandboxed */
writeFile(input: { path: string; volumeId: string; toWrite: string }): Promise<void>;
writeFile(
input: { path: string; volumeId: string; toWrite: string },
): Promise<void>;
readFile(input: { volumeId: string; path: string }): Promise<string>;
metadata(input: { volumeId: string; path: string }): Promise<Metadata>;
/** Create a directory. Usable when not sandboxed */
@@ -67,12 +86,18 @@ export type Effects = {
removeFile(input: { volumeId: string; path: string }): Promise<void>;
/** Write a json file into an object. Usable when not sandboxed */
writeJsonFile(input: { volumeId: string; path: string; toWrite: Record<string, unknown> }): Promise<void>;
writeJsonFile(
input: { volumeId: string; path: string; toWrite: Record<string, unknown> },
): Promise<void>;
/** Read a json file into an object */
readJsonFile(input: { volumeId: string; path: string }): Promise<Record<string, unknown>>;
readJsonFile(
input: { volumeId: string; path: string },
): Promise<Record<string, unknown>>;
runCommand(input: { command: string; args?: string[]; timeoutMillis?: number }): Promise<ResultType<string>>;
runCommand(
input: { command: string; args?: string[]; timeoutMillis?: number },
): Promise<ResultType<string>>;
runDaemon(input: { command: string; args?: string[] }): {
wait(): Promise<ResultType<string>>;
term(): Promise<void>;
@@ -102,7 +127,7 @@ export type Effects = {
method?: "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "PATCH";
headers?: Record<string, string>;
body?: string;
}
},
): Promise<{
method: string;
ok: boolean;
@@ -208,8 +233,8 @@ export type DependsOn = {
export type KnownError =
| { error: string }
| {
"error-code": [number, string] | readonly [number, string];
};
"error-code": [number, string] | readonly [number, string];
};
export type ResultType<T> = KnownError | { result: T };
export type PackagePropertiesV2 = {
@@ -241,8 +266,14 @@ export type Dependencies = {
/** Id is the id of the package, should be the same as the manifest */
[id: string]: {
/** 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: ConfigSpec): Promise<ResultType<void | null>>;
check(
effects: Effects,
input: ConfigSpec,
): Promise<ResultType<void | null>>;
/** 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: ConfigSpec): Promise<ResultType<ConfigSpec>>;
autoConfigure(
effects: Effects,
input: ConfigSpec,
): Promise<ResultType<ConfigSpec>>;
};
};

View File

@@ -1,26 +1,27 @@
// deno-lint-ignore-file ban-types
export type ConfigSpec = Record<string, ValueSpec>;
export type ValueType = "string" | "number" | "boolean" | "enum" | "list" | "object" | "pointer" | "union";
export type ValueType =
| "string"
| "number"
| "boolean"
| "enum"
| "list"
| "object"
| "pointer"
| "union";
export type ValueSpec = ValueSpecOf<ValueType>;
// core spec types. These types provide the metadata for performing validations
export type ValueSpecOf<T extends ValueType> = T extends "string"
? ValueSpecString
: T extends "number"
? ValueSpecNumber
: T extends "boolean"
? ValueSpecBoolean
: T extends "enum"
? ValueSpecEnum
: T extends "list"
? ValueSpecList
: T extends "object"
? ValueSpecObject
: T extends "pointer"
? ValueSpecPointer
: T extends "union"
? ValueSpecUnion
: T extends "number" ? ValueSpecNumber
: T extends "boolean" ? ValueSpecBoolean
: T extends "enum" ? ValueSpecEnum
: T extends "list" ? ValueSpecList
: T extends "object" ? ValueSpecObject
: T extends "pointer" ? ValueSpecPointer
: T extends "union" ? ValueSpecUnion
: never;
export interface ValueSpecString extends ListValueSpecString, WithStandalone {
@@ -75,24 +76,26 @@ export interface WithStandalone {
}
// no lists of booleans, lists, pointers
export type ListValueSpecType = "string" | "number" | "enum" | "object" | "union";
export type ListValueSpecType =
| "string"
| "number"
| "enum"
| "object"
| "union";
// represents a spec for the values of a list
export type ListValueSpecOf<T extends ListValueSpecType> = T extends "string"
? ListValueSpecString
: T extends "number"
? ListValueSpecNumber
: T extends "enum"
? ListValueSpecEnum
: T extends "object"
? ListValueSpecObject
: T extends "union"
? ListValueSpecUnion
: T extends "number" ? ListValueSpecNumber
: T extends "enum" ? ListValueSpecEnum
: T extends "object" ? ListValueSpecObject
: T extends "union" ? ListValueSpecUnion
: never;
// represents a spec for a list
export type ValueSpecList = ValueSpecListOf<ListValueSpecType>;
export interface ValueSpecListOf<T extends ListValueSpecType> extends WithStandalone {
export interface ValueSpecListOf<T extends ListValueSpecType>
extends WithStandalone {
type: "list";
subtype: T;
spec: ListValueSpecOf<T>;
@@ -109,7 +112,10 @@ export interface ValueSpecListOf<T extends ListValueSpecType> extends WithStanda
}
// sometimes the type checker needs just a little bit of help
export function isValueSpecListOf<S extends ListValueSpecType>(t: ValueSpecList, s: S): t is ValueSpecListOf<S> {
export function isValueSpecListOf<S extends ListValueSpecType>(
t: ValueSpecList,
s: S,
): t is ValueSpecListOf<S> {
return t.subtype === s;
}

View File

@@ -13,10 +13,13 @@ export function unwrapResultType<T>(res: T.ResultType<T>): T {
}
/** Used to check if the file exists before hand */
export const exists = (effects: T.Effects, props: { path: string; volumeId: string }) =>
export const exists = (
effects: T.Effects,
props: { path: string; volumeId: string },
) =>
effects.metadata(props).then(
(_) => true,
(_) => false
(_) => false,
);
export const errorCode = (code: number, error: string) => ({

View File

@@ -76,14 +76,16 @@ const bitcoinProperties = {
default: Array<string>(),
spec: {
pattern: "^[a-zA-Z0-9_-]+:([0-9a-fA-F]{2})+\\$([0-9a-fA-F]{2})+$",
"pattern-description": 'Each item must be of the form "<USERNAME>:<SALT>$<HASH>".',
"pattern-description":
'Each item must be of the form "<USERNAME>:<SALT>$<HASH>".',
masked: false,
},
range: "[0,*)",
},
serialversion: {
name: "Serialization Version",
description: "Return raw transaction or block hex with Segwit or non-SegWit serialization.",
description:
"Return raw transaction or block hex with Segwit or non-SegWit serialization.",
type: "enum",
values: ["non-segwit", "segwit"],
"value-names": {},
@@ -91,7 +93,8 @@ const bitcoinProperties = {
},
servertimeout: {
name: "Rpc Server Timeout",
description: "Number of seconds after which an uncompleted RPC call will time out.",
description:
"Number of seconds after which an uncompleted RPC call will time out.",
type: "number",
nullable: false,
range: "[5,300]",
@@ -195,7 +198,8 @@ const bitcoinProperties = {
type: "number",
nullable: false,
name: "Max Mempool Size",
description: "Keep the transaction memory pool below <n> megabytes.",
description:
"Keep the transaction memory pool below <n> megabytes.",
range: "[1,*)",
integral: true,
units: "MiB",
@@ -205,7 +209,8 @@ const bitcoinProperties = {
type: "number",
nullable: false,
name: "Mempool Expiration",
description: "Do not keep transactions in the mempool longer than <n> hours.",
description:
"Do not keep transactions in the mempool longer than <n> hours.",
range: "[1,*)",
integral: true,
units: "Hr",
@@ -221,7 +226,8 @@ const bitcoinProperties = {
listen: {
type: "boolean",
name: "Make Public",
description: "Allow other nodes to find your server on the network.",
description:
"Allow other nodes to find your server on the network.",
default: true,
},
onlyconnect: {
@@ -261,7 +267,8 @@ const bitcoinProperties = {
type: "number",
nullable: true,
name: "Port",
description: "Port that peer is listening on for inbound p2p connections",
description:
"Port that peer is listening on for inbound p2p connections",
range: "[0,65535]",
integral: true,
},
@@ -285,7 +292,8 @@ const bitcoinProperties = {
pruning: {
type: "union",
name: "Pruning Settings",
description: "Blockchain Pruning Options\nReduce the blockchain size on disk\n",
description:
"Blockchain Pruning Options\nReduce the blockchain size on disk\n",
warning:
"If you set pruning to Manual and your disk is smaller than the total size of the blockchain, you MUST have something running that prunes these blocks or you may overfill your disk!\nDisabling pruning will convert your node into a full archival node. This requires a resync of the entire blockchain, a process that may take several days. Make sure you have enough free disk space or you may fill up your disk.\n",
tag: {
@@ -307,7 +315,8 @@ const bitcoinProperties = {
nullable: false,
name: "Max Chain Size",
description: "Limit of blockchain size on disk.",
warning: "Increasing this value will require re-syncing your node.",
warning:
"Increasing this value will require re-syncing your node.",
default: 550,
range: "[550,1000000)",
integral: true,
@@ -360,7 +369,8 @@ const bitcoinProperties = {
name: "Serve Bloom Filters to Peers",
description:
"Peers have the option of setting filters on each connection they make after the version handshake has completed. Bloom filters are for clients implementing SPV (Simplified Payment Verification) that want to check that block headers connect together correctly, without needing to verify the full blockchain. The client must trust that the transactions in the chain are in fact valid. It is highly recommended AGAINST using for anything except Bisq integration.",
warning: "This is ONLY for use with Bisq integration, please use Block Filters for all other applications.",
warning:
"This is ONLY for use with Bisq integration, please use Block Filters for all other applications.",
default: false,
},
},
@@ -371,22 +381,36 @@ const bitcoinProperties = {
} as const;
type BitcoinProperties = typeof bitcoinProperties;
const anyValue: unknown = "";
const _testBoolean: boolean = anyValue as PM.GuardAll<BitcoinProperties["rpc"]["spec"]["enable"]>;
const _testBoolean: boolean = anyValue as PM.GuardAll<
BitcoinProperties["rpc"]["spec"]["enable"]
>;
// @ts-expect-error Boolean can't be a string
const _testBooleanBad: string = anyValue as PM.GuardAll<BitcoinProperties["rpc"]["spec"]["enable"]>;
const _testString: string = anyValue as PM.GuardAll<BitcoinProperties["rpc"]["spec"]["username"]>;
const _testBooleanBad: string = anyValue as PM.GuardAll<
BitcoinProperties["rpc"]["spec"]["enable"]
>;
const _testString: string = anyValue as PM.GuardAll<
BitcoinProperties["rpc"]["spec"]["username"]
>;
// @ts-expect-error string can't be a boolean
const _testStringBad: boolean = anyValue as PM.GuardAll<BitcoinProperties["rpc"]["spec"]["username"]>;
const _testNumber: number = anyValue as PM.GuardAll<BitcoinProperties["advanced"]["spec"]["dbcache"]>;
const _testStringBad: boolean = anyValue as PM.GuardAll<
BitcoinProperties["rpc"]["spec"]["username"]
>;
const _testNumber: number = anyValue as PM.GuardAll<
BitcoinProperties["advanced"]["spec"]["dbcache"]
>;
// @ts-expect-error Number can't be string
const _testNumberBad: string = anyValue as PM.GuardAll<BitcoinProperties["advanced"]["spec"]["dbcache"]>;
const _testNumberBad: string = anyValue as PM.GuardAll<
BitcoinProperties["advanced"]["spec"]["dbcache"]
>;
const _testObject: {
enable: boolean;
avoidpartialspends: boolean;
discardfee: number;
} = anyValue as PM.GuardAll<BitcoinProperties["wallet"]>;
// @ts-expect-error Boolean can't be object
const _testObjectBad: boolean = anyValue as PM.GuardAll<BitcoinProperties["wallet"]>;
const _testObjectBad: boolean = anyValue as PM.GuardAll<
BitcoinProperties["wallet"]
>;
const _testObjectNested: { test: { a: boolean } } = anyValue as PM.GuardAll<{
readonly type: "object";
readonly spec: {
@@ -411,7 +435,7 @@ const _testListBad: readonly number[] = anyValue as PM.GuardAll<{
subtype: "string";
default: [];
}>;
const _testPointer: { _UNKNOWN: "Pointer" } = anyValue as PM.GuardAll<{
const _testPointer: string | null = anyValue as PM.GuardAll<{
type: "pointer";
}>;
const testUnionValue = anyValue as PM.GuardAll<{
@@ -419,7 +443,8 @@ const testUnionValue = anyValue as PM.GuardAll<{
tag: {
id: "mode";
name: "Pruning Mode";
description: '- Disabled: Disable pruning\n- Automatic: Limit blockchain size on disk to a certain number of megabytes\n- Manual: Prune blockchain with the "pruneblockchain" RPC\n';
description:
'- Disabled: Disable pruning\n- Automatic: Limit blockchain size on disk to a certain number of megabytes\n- Manual: Prune blockchain with the "pruneblockchain" RPC\n';
"variant-names": {
disabled: "Disabled";
automatic: "Automatic";
@@ -460,17 +485,17 @@ const _testUnion:
| { mode: "disabled" }
| { mode: "automatic"; size: number }
| {
mode: "manual";
size: number;
} = testUnionValue;
mode: "manual";
size: number;
} = testUnionValue;
//@ts-expect-error Bad mode name
const _testUnionBadUnion:
| { mode: "disabled" }
| { mode: "bad"; size: number }
| {
mode: "manual";
size: number;
} = testUnionValue;
mode: "manual";
size: number;
} = testUnionValue;
const _testAll: PM.TypeFromProps<BitcoinProperties> = anyValue as {
// deno-lint-ignore no-explicit-any
"peer-tor-address": any;
@@ -514,9 +539,9 @@ const _testAll: PM.TypeFromProps<BitcoinProperties> = anyValue as {
| { mode: "disabled" }
| { mode: "automatic"; size: number }
| {
mode: "manual";
size: number;
};
mode: "manual";
size: number;
};
blockfilters: {
blockfilterindex: boolean;
peerblockfilters: boolean;
@@ -572,24 +597,28 @@ const { test } = Deno;
test("Generate 1", () => {
const random = randWithSeed(1);
const options = { random };
const generated = PM.generateDefault({ charset: "a-z,B-X,2-5", len: 100 }, options);
const generated = PM.generateDefault(
{ charset: "a-z,B-X,2-5", len: 100 },
options,
);
expect(generated.length).toBe(100);
expect(generated).toBe(
"WwwgjGRkvDaGQSLeKTtlOmdDbXoCBkOn3dxUvkKkrlOFd4FbKuvIosvfPTQhbWCTQakqnwpoHmPnbgyK5CGtSQyGhxEGLjS3oKko"
"WwwgjGRkvDaGQSLeKTtlOmdDbXoCBkOn3dxUvkKkrlOFd4FbKuvIosvfPTQhbWCTQakqnwpoHmPnbgyK5CGtSQyGhxEGLjS3oKko",
);
});
test("Generate Tests", () => {
const random = randWithSeed(2);
const options = { random };
expect(PM.generateDefault({ charset: "0-1", len: 100 }, options)).toBe(
"0000110010000000000011110000010010000011101111001000000000000000100001101000010000001000010000010110"
"0000110010000000000011110000010010000011101111001000000000000000100001101000010000001000010000010110",
);
expect(PM.generateDefault({ charset: "a-z", len: 100 }, options)).toBe(
"qipnycbqmqdtflrhnckgrhftrqnvxbhyyfehpvficljseasxwdyleacmjqemmpnuotkwzlsqdumuaaksxykchljgdoslrfubhepr"
);
expect(PM.generateDefault({ charset: "a,b,c,d,f,g", len: 100 }, options)).toBe(
"bagbafcgaaddcabdfadccaadfbddffdcfccfbafbddbbfcdggfcgaffdbcgcagcfbdbfaagbfgfccdbfdfbdagcfdcabbdffaffc"
"qipnycbqmqdtflrhnckgrhftrqnvxbhyyfehpvficljseasxwdyleacmjqemmpnuotkwzlsqdumuaaksxykchljgdoslrfubhepr",
);
expect(PM.generateDefault({ charset: "a,b,c,d,f,g", len: 100 }, options))
.toBe(
"bagbafcgaaddcabdfadccaadfbddffdcfccfbafbddbbfcdggfcgaffdbcgcagcfbdbfaagbfgfccdbfdfbdagcfdcabbdffaffc",
);
});
}
@@ -601,24 +630,26 @@ const { test } = Deno;
});
test("A default that is invalid according to the tests", () => {
const checker = PM.typeFromProps({
pubkey_whitelist: {
name: "Pubkey Whitelist (hex)",
description:
"A list of pubkeys that are permitted to publish through your relay. A minimum, you need to enter your own Nostr hex (not npub) pubkey. Go to https://damus.io/key/ to convert from npub to hex.",
type: "list",
range: "[1,*)",
subtype: "string",
spec: {
masked: false,
placeholder: "hex (not npub) pubkey",
pattern: "[0-9a-fA-F]{3}",
"pattern-description":
"Must be a valid 64-digit hexadecimal value (ie a Nostr hex pubkey, not an npub). Go to https://damus.io/key/ to convert npub to hex.",
const checker = PM.typeFromProps(
{
pubkey_whitelist: {
name: "Pubkey Whitelist (hex)",
description:
"A list of pubkeys that are permitted to publish through your relay. A minimum, you need to enter your own Nostr hex (not npub) pubkey. Go to https://damus.io/key/ to convert from npub to hex.",
type: "list",
range: "[1,*)",
subtype: "string",
spec: {
masked: false,
placeholder: "hex (not npub) pubkey",
pattern: "[0-9a-fA-F]{3}",
"pattern-description":
"Must be a valid 64-digit hexadecimal value (ie a Nostr hex pubkey, not an npub). Go to https://damus.io/key/ to convert npub to hex.",
},
default: [] as string[], // [] as string []
},
default: [] as string[], // [] as string []
},
} as const);
} as const,
);
checker.unsafeCast({
pubkey_whitelist: ["aaa"],

View File

@@ -1,5 +1,8 @@
import { matches } from "../dependencies.ts";
import { ConfigSpec, ValueSpec as ValueSpecAny } from "../types/config-types.ts";
import {
ConfigSpec,
ValueSpec as ValueSpecAny,
} from "../types/config-types.ts";
type TypeBoolean = "boolean";
type TypeString = "string";
@@ -66,14 +69,15 @@ type GuardUnion<A> =
unknown
type _<T> = T;
export type GuardAll<A> = GuardNumber<A> &
GuardString<A> &
GuardBoolean<A> &
GuardObject<A> &
GuardList<A> &
GuardPointer<A> &
GuardUnion<A> &
GuardEnum<A>;
export type GuardAll<A> =
& GuardNumber<A>
& GuardString<A>
& GuardBoolean<A>
& GuardObject<A>
& GuardList<A>
& GuardPointer<A>
& GuardUnion<A>
& GuardEnum<A>;
// prettier-ignore
// deno-fmt-ignore
export type TypeFromProps<A> =
@@ -109,23 +113,32 @@ function charRange(value = "") {
}
/**
*
* @param generate.charset Pattern like "a-z" or "a-z,1-5"
* @param generate.len Length to make random variable
* @param param1
* @returns
*/
export function generateDefault(generate: { charset: string; len: number }, { random = () => Math.random() } = {}) {
const validCharSets: number[][] = generate.charset.split(",").map(charRange).filter(Array.isArray);
if (validCharSets.length === 0) throw new Error("Expecing that we have a valid charset");
const max = validCharSets.reduce((acc, x) => x.reduce((x, y) => Math.max(x, y), acc), 0);
export function generateDefault(
generate: { charset: string; len: number },
{ random = () => Math.random() } = {},
) {
const validCharSets: number[][] = generate.charset.split(",").map(charRange)
.filter(Array.isArray);
if (validCharSets.length === 0) {
throw new Error("Expecing that we have a valid charset");
}
const max = validCharSets.reduce(
(acc, x) => x.reduce((x, y) => Math.max(x, y), acc),
0,
);
let i = 0;
const answer: string[] = Array(generate.len);
while (i < generate.len) {
const nextValue = Math.round(random() * max);
const inRange = validCharSets.reduce(
(acc, [lower, upper]) => acc || (nextValue >= lower && nextValue <= upper),
false
(acc, [lower, upper]) =>
acc || (nextValue >= lower && nextValue <= upper),
false,
);
if (!inRange) continue;
answer[i] = String.fromCharCode(nextValue);
@@ -144,12 +157,28 @@ export function matchNumberWithRange(range: string) {
const [, left, leftValue, , rightValue, , right] = matched;
return matches.number
.validate(
leftValue === "*" ? (_) => true : left === "[" ? (x) => x >= Number(leftValue) : (x) => x > Number(leftValue),
leftValue === "*" ? "any" : left === "[" ? `greaterThanOrEqualTo${leftValue}` : `greaterThan${leftValue}`
leftValue === "*"
? (_) => true
: left === "["
? (x) => x >= Number(leftValue)
: (x) => x > Number(leftValue),
leftValue === "*"
? "any"
: left === "["
? `greaterThanOrEqualTo${leftValue}`
: `greaterThan${leftValue}`,
)
.validate(
rightValue === "*" ? (_) => true : right === "]" ? (x) => x <= Number(rightValue) : (x) => x < Number(rightValue),
rightValue === "*" ? "any" : right === "]" ? `lessThanOrEqualTo${rightValue}` : `lessThan${rightValue}`
rightValue === "*"
? (_) => true
: right === "]"
? (x) => x <= Number(rightValue)
: (x) => x < Number(rightValue),
rightValue === "*"
? "any"
: right === "]"
? `lessThanOrEqualTo${rightValue}`
: `lessThan${rightValue}`,
);
}
function withIntegral(parser: matches.Parser<unknown, number>, value: unknown) {
@@ -164,10 +193,18 @@ function withRange(value: unknown) {
}
return matches.number;
}
const isGenerator = matches.shape({ charset: matches.string, len: matches.number }).test;
function defaultNullable<A>(parser: matches.Parser<unknown, A>, value: unknown) {
const isGenerator =
matches.shape({ charset: matches.string, len: matches.number }).test;
function defaultNullable<A>(
parser: matches.Parser<unknown, A>,
value: unknown,
) {
if (matchDefault.test(value)) {
if (isGenerator(value.default)) return parser.defaultTo(parser.unsafeCast(generateDefault(value.default)));
if (isGenerator(value.default)) {
return parser.defaultTo(
parser.unsafeCast(generateDefault(value.default)),
);
}
return parser.defaultTo(value.default);
}
if (matchNullable.test(value)) return parser.optional();
@@ -182,7 +219,9 @@ function defaultNullable<A>(parser: matches.Parser<unknown, A>, value: unknown)
* @param value
* @returns
*/
export function guardAll<A extends ValueSpecAny>(value: A): matches.Parser<unknown, GuardAll<A>> {
export function guardAll<A extends ValueSpecAny>(
value: A,
): matches.Parser<unknown, GuardAll<A>> {
if (!isType.test(value)) {
// deno-lint-ignore no-explicit-any
return matches.unknown as any;
@@ -199,7 +238,7 @@ export function guardAll<A extends ValueSpecAny>(value: A): matches.Parser<unkno
case "number":
return defaultNullable(
withIntegral(withRange(value), value),
value
value,
// deno-lint-ignore no-explicit-any
) as any;
@@ -213,7 +252,9 @@ export function guardAll<A extends ValueSpecAny>(value: A): matches.Parser<unkno
case "list": {
const spec = (matchSpec.test(value) && value.spec) || {};
const rangeValidate = (matchRange.test(value) && matchNumberWithRange(value.range).test) || (() => true);
const rangeValidate =
(matchRange.test(value) && matchNumberWithRange(value.range).test) ||
(() => true);
const subtype = matchSubType.unsafeCast(value).subtype;
return defaultNullable(
@@ -221,7 +262,7 @@ export function guardAll<A extends ValueSpecAny>(value: A): matches.Parser<unkno
// deno-lint-ignore no-explicit-any
.arrayOf(guardAll({ type: subtype, ...spec } as any))
.validate((x) => rangeValidate(x.length), "valid length"),
value
value,
// deno-lint-ignore no-explicit-any
) as any;
}
@@ -229,7 +270,7 @@ export function guardAll<A extends ValueSpecAny>(value: A): matches.Parser<unkno
if (matchValues.test(value)) {
return defaultNullable(
matches.literals(value.values[0], ...value.values),
value
value,
// deno-lint-ignore no-explicit-any
) as any;
}
@@ -242,8 +283,10 @@ export function guardAll<A extends ValueSpecAny>(value: A): matches.Parser<unkno
if (matchUnion.test(value)) {
return matches.some(
...Object.entries(value.variants).map(([variant, spec]) =>
matches.shape({ [value.tag.id]: matches.literal(variant) }).concat(typeFromProps(spec))
) // deno-lint-ignore no-explicit-any
matches.shape({ [value.tag.id]: matches.literal(variant) }).concat(
typeFromProps(spec),
)
), // deno-lint-ignore no-explicit-any
) as any;
}
// deno-lint-ignore no-explicit-any
@@ -261,11 +304,17 @@ export function guardAll<A extends ValueSpecAny>(value: A): matches.Parser<unkno
* @param valueDictionary
* @returns
*/
export function typeFromProps<A extends ConfigSpec>(valueDictionary: A): matches.Parser<unknown, TypeFromProps<A>> {
export function typeFromProps<A extends ConfigSpec>(
valueDictionary: A,
): matches.Parser<unknown, TypeFromProps<A>> {
// deno-lint-ignore no-explicit-any
if (!recordString.test(valueDictionary)) return matches.unknown as any;
return matches.shape(
Object.fromEntries(Object.entries(valueDictionary).map(([key, value]) => [key, guardAll(value)]))
Object.fromEntries(
Object.entries(valueDictionary).map((
[key, value],
) => [key, guardAll(value)]),
),
// deno-lint-ignore no-explicit-any
) as any;
}