From df89119193a2f5ea1751704a7fb14b3b1e102feb Mon Sep 17 00:00:00 2001 From: BluJ Date: Thu, 16 Feb 2023 14:08:53 -0700 Subject: [PATCH] chore: Add the tools for packaging npm --- .gitignore | 3 +- Makefile | 16 +++- build.ts | 30 +++++++ compat/getConfig.ts | 12 +-- compat/migrations.ts | 43 +++++++--- compat/setConfig.ts | 6 +- config/config.ts | 30 ++++--- config/index.test.ts | 6 +- config/list.ts | 53 +++++++++---- config/pointer.ts | 30 +++++-- config/value.ts | 58 +++++++++----- config/variants.ts | 25 ++++-- types.ts | 61 +++++++++++---- types/config-types.ts | 58 ++++++++------ util.ts | 7 +- utils/propertiesMatcher.test.ts | 135 ++++++++++++++++++++------------ utils/propertiesMatcher.ts | 113 ++++++++++++++++++-------- 17 files changed, 477 insertions(+), 209 deletions(-) create mode 100644 build.ts diff --git a/.gitignore b/.gitignore index 600d2d3..3777f4f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.vscode \ No newline at end of file +.vscode +lib \ No newline at end of file diff --git a/Makefile b/Makefile index 736c55f..395f5aa 100644 --- a/Makefile +++ b/Makefile @@ -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 \ No newline at end of file diff --git a/build.ts b/build.ts new file mode 100644 index 0000000..ca53df1 --- /dev/null +++ b/build.ts @@ -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"); diff --git a/compat/getConfig.ts b/compat/getConfig.ts index 0ab4036..866c82a 100644 --- a/compat/getConfig.ts +++ b/compat/getConfig.ts @@ -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: Config | Spec -): [ExpectedExports.getConfig, matches.Parser>] => { + spec: Config | Spec, +): [ + ExpectedExports.getConfig, + matches.Parser>, +] => { const specBuilt: Spec = spec instanceof Config ? spec.build() : spec; return [ diff --git a/compat/migrations.ts b/compat/migrations.ts index 01e8185..d74b8f9 100644 --- a/compat/migrations.ts +++ b/compat/migrations.ts @@ -17,11 +17,17 @@ export interface NoRepeat { * @param noFail (optional, default:false) whether or not to fail the migration if fn throws an error * @returns a migraion function */ -export function updateConfig( - fn: (config: ConfigSpec, effects: T.Effects) => ConfigSpec | Promise, +export function updateConfig< + version extends string, + type extends "up" | "down", +>( + fn: ( + config: ConfigSpec, + effects: T.Effects, + ) => ConfigSpec | Promise, configured: boolean, noRepeat?: NoRepeat, - noFail = false + noFail = false, ): M.MigrationFn { return M.migrationFn(async (effects: T.Effects) => { await noRepeatGuard(effects, noRepeat, async () => { @@ -43,15 +49,23 @@ export function updateConfig }); } -export async function noRepeatGuard( +export async function noRepeatGuard< + version extends string, + type extends "up" | "down", +>( effects: T.Effects, noRepeat: NoRepeat | undefined, - fn: () => Promise + fn: () => Promise, ): Promise { 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( effects: T.Effects, migrations: M.MigrationMapping, - 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( export function fromMapping( migrations: M.MigrationMapping, - 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); }; } diff --git a/compat/setConfig.ts b/compat/setConfig.ts index 1381238..64d1bc7 100644 --- a/compat/setConfig.ts +++ b/compat/setConfig.ts @@ -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", diff --git a/config/config.ts b/config/config.ts index c5777a1..76decb5 100644 --- a/config/config.ts +++ b/config/config.ts @@ -6,10 +6,16 @@ export class Config extends IBuilder { static empty() { return new Config({}); } - static withValue(key: K, value: Value) { + static withValue( + key: K, + value: Value, + ) { return Config.empty().withValue(key, value); } - static addValue(key: K, value: Value) { + static addValue( + key: K, + value: Value, + ) { return Config.empty().withValue(key, value); } @@ -23,15 +29,19 @@ export class Config extends IBuilder { return new Config(answer); } withValue(key: K, value: Value) { - 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(key: K, value: Value) { - 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 }, + ); } } diff --git a/config/index.test.ts b/config/index.test.ts index f66ccb3..0e965a2 100644 --- a/config/index.test.ts +++ b/config/index.test.ts @@ -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(": ", ":"), ); }); diff --git a/config/list.ts b/config/list.ts index 1ed3a4b..72bdd04 100644 --- a/config/list.ts +++ b/config/list.ts @@ -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 extends IBuilder { // // deno-lint-ignore ban-types @@ -16,14 +21,24 @@ export class List extends IBuilder { // }); // } - static string & { range: string; spec: StringSpec }>(a: A) { + static string< + A extends Description & Default & { + range: string; + spec: StringSpec; + }, + >(a: A) { return new List({ type: "list" as const, subtype: "string" as const, ...a, } as ValueSpecListOf<"string">); } - static number & { range: string; spec: NumberSpec }>(a: A) { + static number< + A extends Description & Default & { + range: string; + spec: NumberSpec; + }, + >(a: A) { return new List({ type: "list" as const, subtype: "number" as const, @@ -31,8 +46,10 @@ export class List extends IBuilder { }); } static enum< - A extends Description & - Default & { + A extends + & Description + & Default + & { range: string; spec: { values: string[]; @@ -40,7 +57,7 @@ export class List extends IBuilder { [key: string]: string; }; }; - } + }, >(a: A) { return new List({ type: "list" as const, @@ -49,19 +66,23 @@ export class List extends IBuilder { }); } static objectV< - A extends Description & - Default[]> & { + A extends + & Description + & Default[]> + & { range: string; spec: { spec: Config; "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; + const specSpec = previousSpecSpec.build() as BuilderExtract< + A["spec"]["spec"] + >; const spec = { ...restSpec, spec: specSpec, @@ -77,8 +98,10 @@ export class List extends IBuilder { }); } static union< - A extends Description & - Default & { + A extends + & Description + & Default + & { range: string; spec: { tag: { @@ -94,11 +117,13 @@ export class List extends IBuilder { "unique-by": UniqueBy; default: string; }; - } + }, >(a: A) { const { spec: previousSpec, ...rest } = a; const { variants: previousVariants, ...restSpec } = previousSpec; - const variants = previousVariants.build() as BuilderExtract; + const variants = previousVariants.build() as BuilderExtract< + A["spec"]["variants"] + >; const spec = { ...restSpec, variants, diff --git a/config/pointer.ts b/config/pointer.ts index a3ddb6a..b07d566 100644 --- a/config/pointer.ts +++ b/config/pointer.ts @@ -3,7 +3,9 @@ import { IBuilder } from "./builder.ts"; import { Description } from "./value.ts"; export class Pointer extends IBuilder { - static packageTorKey(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 extends IBuilder { ...a, }); } - static packageTorAddress(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 extends IBuilder { ...a, }); } - static packageLanAddress(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 extends IBuilder { }); } 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 extends IBuilder { ...a, }); } - static system( - 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, diff --git a/config/value.ts b/config/value.ts index 61dd24e..8d27f5d 100644 --- a/config/value.ts +++ b/config/value.ts @@ -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 = { 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 extends IBuilder { ...a, }); } - static string & Nullable & StringSpec>(a: A) { + static string< + A extends + & Description + & NullableDefault + & Nullable + & StringSpec, + >(a: A) { return new Value({ type: "string" as const, ...a, } as ValueSpecString); } - static number & Nullable & NumberSpec>(a: A) { + static number< + A extends Description & NullableDefault & Nullable & NumberSpec, + >(a: A) { return new Value({ type: "number" as const, ...a, } as ValueSpecNumber); } static enum< - A extends Description & - Default & { values: readonly string[] | string[]; "value-names": Record } + A extends + & Description + & Default + & { + values: readonly string[] | string[]; + "value-names": Record; + }, >(a: A) { return new Value({ type: "enum" as const, @@ -91,7 +105,7 @@ export class Value extends IBuilder { "unique-by": null | string; spec: Config; "value-names": Record; - } + }, >(a: A) { const { spec: previousSpec, ...rest } = a; const spec = previousSpec.build() as BuilderExtract; @@ -102,8 +116,10 @@ export class Value extends IBuilder { }); } static union< - A extends Description & - Default & { + A extends + & Description + & Default + & { tag: { id: string; name: string; @@ -116,7 +132,7 @@ export class Value extends IBuilder { 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; diff --git a/config/variants.ts b/config/variants.ts index 5512fa5..daa06e9 100644 --- a/config/variants.ts +++ b/config/variants.ts @@ -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 extends IBuilder { +export class Variants + extends IBuilder { static of< A extends { [key: string]: Config; - } + }, >(a: A) { // deno-lint-ignore no-explicit-any const variants: { [K in keyof A]: BuilderExtract } = {} as any; @@ -20,14 +21,22 @@ export class Variants extends IBuilder< static empty() { return Variants.of({}); } - static withVariant(key: K, value: Config) { + static withVariant( + key: K, + value: Config, + ) { return Variants.empty().withVariant(key, value); } - withVariant(key: K, value: Config) { - return new Variants({ - ...this.a, - [key]: value.build(), - } as A & { [key in K]: B }); + withVariant( + key: K, + value: Config, + ) { + return new Variants( + { + ...this.a, + [key]: value.build(), + } as A & { [key in K]: B }, + ); } } diff --git a/types.ts b/types.ts index b50dbab..0e9613d 100644 --- a/types.ts +++ b/types.ts @@ -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>; + export type setConfig = ( + effects: Effects, + input: ConfigSpec, + ) => Promise>; /** 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>; /** 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>; /** 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>; + export type restoreBackup = ( + effects: Effects, + ) => Promise>; /** 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>; + export type properties = ( + effects: Effects, + ) => Promise>; /** 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>; + [id: string]: ( + effects: Effects, + dateMs: number, + ) => Promise>; }; /** * 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>; + export type migration = ( + effects: Effects, + version: string, + ...args: unknown[] + ) => Promise>; /** * 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>; + [id: string]: ( + effects: Effects, + config?: ConfigSpec, + ) => Promise>; }; /** @@ -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; + writeFile( + input: { path: string; volumeId: string; toWrite: string }, + ): Promise; readFile(input: { volumeId: string; path: string }): Promise; metadata(input: { volumeId: string; path: string }): Promise; /** Create a directory. Usable when not sandboxed */ @@ -67,12 +86,18 @@ export type Effects = { removeFile(input: { volumeId: string; path: string }): Promise; /** Write a json file into an object. Usable when not sandboxed */ - writeJsonFile(input: { volumeId: string; path: string; toWrite: Record }): Promise; + writeJsonFile( + input: { volumeId: string; path: string; toWrite: Record }, + ): Promise; /** Read a json file into an object */ - readJsonFile(input: { volumeId: string; path: string }): Promise>; + readJsonFile( + input: { volumeId: string; path: string }, + ): Promise>; - runCommand(input: { command: string; args?: string[]; timeoutMillis?: number }): Promise>; + runCommand( + input: { command: string; args?: string[]; timeoutMillis?: number }, + ): Promise>; runDaemon(input: { command: string; args?: string[] }): { wait(): Promise>; term(): Promise; @@ -102,7 +127,7 @@ export type Effects = { method?: "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "PATCH"; headers?: Record; 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 = 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>; + check( + effects: Effects, + input: ConfigSpec, + ): Promise>; /** 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>; + autoConfigure( + effects: Effects, + input: ConfigSpec, + ): Promise>; }; }; diff --git a/types/config-types.ts b/types/config-types.ts index e7d2c8c..b93bcb4 100644 --- a/types/config-types.ts +++ b/types/config-types.ts @@ -1,26 +1,27 @@ // deno-lint-ignore-file ban-types export type ConfigSpec = Record; -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; // core spec types. These types provide the metadata for performing validations export type ValueSpecOf = 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 "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; -export interface ValueSpecListOf extends WithStandalone { +export interface ValueSpecListOf + extends WithStandalone { type: "list"; subtype: T; spec: ListValueSpecOf; @@ -109,7 +112,10 @@ export interface ValueSpecListOf extends WithStanda } // sometimes the type checker needs just a little bit of help -export function isValueSpecListOf(t: ValueSpecList, s: S): t is ValueSpecListOf { +export function isValueSpecListOf( + t: ValueSpecList, + s: S, +): t is ValueSpecListOf { return t.subtype === s; } diff --git a/util.ts b/util.ts index 9c8e351..341bff9 100644 --- a/util.ts +++ b/util.ts @@ -13,10 +13,13 @@ export function unwrapResultType(res: T.ResultType): 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) => ({ diff --git a/utils/propertiesMatcher.test.ts b/utils/propertiesMatcher.test.ts index 04f61e7..292d5ef 100644 --- a/utils/propertiesMatcher.test.ts +++ b/utils/propertiesMatcher.test.ts @@ -76,14 +76,16 @@ const bitcoinProperties = { default: Array(), 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 ":$".', + "pattern-description": + 'Each item must be of the form ":$".', 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 megabytes.", + description: + "Keep the transaction memory pool below 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 hours.", + description: + "Do not keep transactions in the mempool longer than 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; +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; -const _testString: string = anyValue as PM.GuardAll; +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; -const _testNumber: number = anyValue as PM.GuardAll; +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; +const _testNumberBad: string = anyValue as PM.GuardAll< + BitcoinProperties["advanced"]["spec"]["dbcache"] +>; const _testObject: { enable: boolean; avoidpartialspends: boolean; discardfee: number; } = anyValue as PM.GuardAll; // @ts-expect-error Boolean can't be object -const _testObjectBad: boolean = anyValue as PM.GuardAll; +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 = anyValue as { // deno-lint-ignore no-explicit-any "peer-tor-address": any; @@ -514,9 +539,9 @@ const _testAll: PM.TypeFromProps = 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"], diff --git a/utils/propertiesMatcher.ts b/utils/propertiesMatcher.ts index b0c4395..af0284a 100644 --- a/utils/propertiesMatcher.ts +++ b/utils/propertiesMatcher.ts @@ -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 = unknown type _ = T; -export type GuardAll = GuardNumber & - GuardString & - GuardBoolean & - GuardObject & - GuardList & - GuardPointer & - GuardUnion & - GuardEnum; +export type GuardAll = + & GuardNumber + & GuardString + & GuardBoolean + & GuardObject + & GuardList + & GuardPointer + & GuardUnion + & GuardEnum; // prettier-ignore // deno-fmt-ignore export type TypeFromProps = @@ -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, 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(parser: matches.Parser, value: unknown) { +const isGenerator = + matches.shape({ charset: matches.string, len: matches.number }).test; +function defaultNullable( + parser: matches.Parser, + 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(parser: matches.Parser, value: unknown) * @param value * @returns */ -export function guardAll(value: A): matches.Parser> { +export function guardAll( + value: A, +): matches.Parser> { if (!isType.test(value)) { // deno-lint-ignore no-explicit-any return matches.unknown as any; @@ -199,7 +238,7 @@ export function guardAll(value: A): matches.Parser(value: A): matches.Parser 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(value: A): matches.Parser rangeValidate(x.length), "valid length"), - value + value, // deno-lint-ignore no-explicit-any ) as any; } @@ -229,7 +270,7 @@ export function guardAll(value: A): matches.Parser(value: A): matches.Parser - 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(value: A): matches.Parser(valueDictionary: A): matches.Parser> { +export function typeFromProps( + valueDictionary: A, +): matches.Parser> { // 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; }