From 3a7e0989b97ae3c844e98e770249721f571c73fd Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Mon, 18 Jul 2022 15:08:54 -0600 Subject: [PATCH 1/2] more utility functions --- compat/migrations.ts | 121 +++++++++++++++++++++++++++++++++++++++++++ compat/mod.ts | 3 +- compat/properties.ts | 58 +++++++++++---------- exists.ts | 4 -- migrations.ts | 50 ++++++++++-------- mod.ts | 2 +- types.ts | 84 +++++++++++++++--------------- util.ts | 17 ++++++ 8 files changed, 241 insertions(+), 98 deletions(-) create mode 100644 compat/migrations.ts delete mode 100644 exists.ts create mode 100644 util.ts diff --git a/compat/migrations.ts b/compat/migrations.ts new file mode 100644 index 0000000..5603923 --- /dev/null +++ b/compat/migrations.ts @@ -0,0 +1,121 @@ +import { getConfig, setConfig } from "./mod.ts"; +import * as T from "../types.ts"; +import * as M from "../migrations.ts"; +import * as util from "../util.ts"; +import { EmVer } from "../emver-lite/mod.ts"; + +export interface NoRepeat { + version: version; + type: type; +} + +/** + * @param fn function making desired modifications to the config + * @param configured whether or not the service should be considered "configured" + * @param noRepeat (optional) supply the version and type of the migration + * @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: T.Config) => T.Config, + configured: boolean, + noRepeat?: NoRepeat, + noFail = false, +): M.MigrationFn { + return async (effects: T.Effects) => { + await noRepeatGuard(effects, noRepeat, async () => { + let config = util.unwrapResultType(await getConfig({})(effects)).config; + if (config) { + try { + config = fn(config); + } catch (e) { + if (!noFail) { + throw e; + } else { + configured = false; + } + } + util.unwrapResultType(await setConfig(effects, config)); + } + }); + return { configured }; + }; +} + +export async function noRepeatGuard< + version extends string, + type extends "up" | "down", +>( + effects: T.Effects, + noRepeat: NoRepeat | undefined, + fn: () => Promise, +): Promise { + if (!noRepeat) { + return fn(); + } + if ( + !await util.exists(effects, { path: "start9/migrations", volumeId: "main" }) + ) { + await effects.createDir({ path: "start9/migrations", volumeId: "main" }); + } + const migrationPath = { + path: `start9/migrations/${noRepeat.version}.complete`, + volumeId: "main", + }; + if (noRepeat.type === "up") { + if (!await util.exists(effects, migrationPath)) { + await fn(); + await effects.writeFile({ ...migrationPath, toWrite: "" }); + } + } else if (noRepeat.type === "down") { + if (await util.exists(effects, migrationPath)) { + await fn(); + await effects.removeFile(migrationPath); + } + } +} + +export async function initNoRepeat( + effects: T.Effects, + migrations: M.MigrationMapping, + startingVersion: string, +) { + 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) { + const migrationVersion = EmVer.parse(version); + if (migrationVersion.lessThanOrEqual(starting)) { + await effects.writeFile({ + path: `start9/migrations/${version}.complete`, + volumeId: "main", + toWrite: "", + }); + } + } + } +} + +export function fromMapping( + migrations: M.MigrationMapping, + 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, + ); + return inner(effects, version, direction); + }; +} diff --git a/compat/mod.ts b/compat/mod.ts index 7c66a2a..7a1babc 100644 --- a/compat/mod.ts +++ b/compat/mod.ts @@ -1,3 +1,4 @@ export { properties } from "./properties.ts"; export { setConfig } from "./setConfig.ts"; -export { getConfig } from "./getConfig.ts"; \ No newline at end of file +export { getConfig } from "./getConfig.ts"; +export * as migrations from "./migrations.ts"; diff --git a/compat/properties.ts b/compat/properties.ts index 5de57d1..be792eb 100644 --- a/compat/properties.ts +++ b/compat/properties.ts @@ -1,38 +1,40 @@ import { YAML } from "../dependencies.ts"; -import { exists } from "../exists.ts"; -import { ResultType, Properties, ExpectedExports, Effects } from "../types.ts"; - +import { exists } from "../util.ts"; +import { Effects, ExpectedExports, Properties, ResultType } from "../types.ts"; // deno-lint-ignore no-explicit-any -const asResult = (result: any) => ({ result: result as Properties }) +const asResult = (result: any) => ({ result: result as Properties }); const noPropertiesFound: ResultType = { - result: { - version: 2, - data: { - "Not Ready": { - type: "string", - value: "Could not find properties. The service might still be starting", - qr: false, - copyable: false, - masked: false, - description: "Fallback Message When Properties could not be found" - } - } - } -} as const + result: { + version: 2, + data: { + "Not Ready": { + type: "string", + value: "Could not find properties. The service might still be starting", + qr: false, + copyable: false, + masked: false, + description: "Fallback Message When Properties could not be found", + }, + }, + }, +} as const; /** * Default will pull from a file (start9/stats.yaml) expected to be made on the main volume - * @param effects - * @returns + * @param effects + * @returns */ export const properties: ExpectedExports.properties = async ( - effects: Effects, + effects: Effects, ) => { - if (await exists(effects, { path: "start9/stats.yaml", volumeId: "main" }) === false) { - return noPropertiesFound; - } - return await effects.readFile({ - path: "start9/stats.yaml", - volumeId: "main", - }).then(YAML.parse).then(asResult) + if ( + await exists(effects, { path: "start9/stats.yaml", volumeId: "main" }) === + false + ) { + return noPropertiesFound; + } + return await effects.readFile({ + path: "start9/stats.yaml", + volumeId: "main", + }).then(YAML.parse).then(asResult); }; diff --git a/exists.ts b/exists.ts deleted file mode 100644 index 69443aa..0000000 --- a/exists.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Effects } from "./types.ts"; - -/** Used to check if the file exists before hand */ -export const exists = (effects: Effects, props: { path: string, volumeId: string }) => effects.metadata(props).then(_ => true, _ => false); \ No newline at end of file diff --git a/migrations.ts b/migrations.ts index c7f516c..5d49edb 100644 --- a/migrations.ts +++ b/migrations.ts @@ -2,19 +2,21 @@ import { types as T } from "./mod.ts"; import { EmVer } from "./emver-lite/mod.ts"; import { matches } from "./dependencies.ts"; -export type MigrationFn = (effects: T.Effects) => Promise; +export type MigrationFn = ( + effects: T.Effects, +) => Promise; -export interface Migration { - up: MigrationFn; - down: MigrationFn; +export interface Migration { + up: MigrationFn; + down: MigrationFn; } -export interface MigrationMapping { - [version: string]: Migration; -} +export type MigrationMapping = { + [version in versions]: Migration; +}; -export function fromMapping( - migrations: MigrationMapping, +export function fromMapping( + migrations: MigrationMapping, currentVersion: string, ): T.ExpectedExports.migration { const directionShape = matches.literals("from", "to"); @@ -32,24 +34,26 @@ export function fromMapping( const current = EmVer.parse(currentVersion); const other = EmVer.parse(version); + const filteredMigrations = + (Object.entries(migrations) as [ + keyof MigrationMapping, + Migration, + ][]) + .map(([version, migration]) => ({ + version: EmVer.parse(version), + migration, + })).filter(({ version }) => + version.greaterThan(other) && version.lessThanOrEqual(current) + ); + const migrationsToRun = matches.matches(direction) .when("from", () => - Object.entries(migrations) - .map(([version, migration]) => ({ - version: EmVer.parse(version), - migration, - })).filter(({ version }) => - version.greaterThan(other) && version.lessThanOrEqual(current) - ).sort((a, b) => a.version.compareForSort(b.version)) + filteredMigrations + .sort((a, b) => a.version.compareForSort(b.version)) // low to high .map(({ migration }) => migration.up)) .when("to", () => - Object.entries(migrations) - .map(([version, migration]) => ({ - version: EmVer.parse(version), - migration, - })).filter(({ version }) => - version.lessThanOrEqual(other) && version.greaterThan(current) - ).sort((a, b) => b.version.compareForSort(a.version)) + filteredMigrations + .sort((a, b) => b.version.compareForSort(a.version)) // high to low .map(({ migration }) => migration.down)) .unwrap(); diff --git a/mod.ts b/mod.ts index 0ab748d..ac913c8 100644 --- a/mod.ts +++ b/mod.ts @@ -1,6 +1,6 @@ export { matches, YAML } from "./dependencies.ts"; export * as types from "./types.ts"; -export { exists } from "./exists.ts"; export * as compat from "./compat/mod.ts"; export * as migrations from "./migrations.ts"; +export * as util from "./util.ts"; diff --git a/types.ts b/types.ts index ca4c1cd..1dfad4b 100644 --- a/types.ts +++ b/types.ts @@ -24,6 +24,7 @@ export namespace ExpectedExports { export type migration = ( effects: Effects, version: string, + ...args: unknown[] ) => Promise>; export type action = { [id: string]: ( @@ -75,12 +76,12 @@ export type Effects = { fetch(url: string, options?: { method?: - | "GET" - | "POST" - | "PUT" - | "DELETE" - | "HEAD" - | "PATCH"; + | "GET" + | "POST" + | "PUT" + | "DELETE" + | "HEAD" + | "PATCH"; headers?: Record; body?: string; }): Promise<{ @@ -143,7 +144,6 @@ export type WithNullableDefault = T & { default?: Default; }; - export type WithDescription = T & { description?: string; name: string; @@ -213,7 +213,9 @@ export type ValueSpecAny = | Tag<"boolean", WithDescription>> | Tag< "string", - WithDescription, DefaultString>> + WithDescription< + WithNullableDefault, DefaultString> + > > | Tag< "number", @@ -338,39 +340,39 @@ export type ValueSpecEnum = { export type SetResult = { /** These are the unix process signals */ signal: - | "SIGTERM" - | "SIGHUP" - | "SIGINT" - | "SIGQUIT" - | "SIGILL" - | "SIGTRAP" - | "SIGABRT" - | "SIGBUS" - | "SIGFPE" - | "SIGKILL" - | "SIGUSR1" - | "SIGSEGV" - | "SIGUSR2" - | "SIGPIPE" - | "SIGALRM" - | "SIGSTKFLT" - | "SIGCHLD" - | "SIGCONT" - | "SIGSTOP" - | "SIGTSTP" - | "SIGTTIN" - | "SIGTTOU" - | "SIGURG" - | "SIGXCPU" - | "SIGXFSZ" - | "SIGVTALRM" - | "SIGPROF" - | "SIGWINCH" - | "SIGIO" - | "SIGPWR" - | "SIGSYS" - | "SIGEMT" - | "SIGINFO"; + | "SIGTERM" + | "SIGHUP" + | "SIGINT" + | "SIGQUIT" + | "SIGILL" + | "SIGTRAP" + | "SIGABRT" + | "SIGBUS" + | "SIGFPE" + | "SIGKILL" + | "SIGUSR1" + | "SIGSEGV" + | "SIGUSR2" + | "SIGPIPE" + | "SIGALRM" + | "SIGSTKFLT" + | "SIGCHLD" + | "SIGCONT" + | "SIGSTOP" + | "SIGTSTP" + | "SIGTTIN" + | "SIGTTOU" + | "SIGURG" + | "SIGXCPU" + | "SIGXFSZ" + | "SIGVTALRM" + | "SIGPROF" + | "SIGWINCH" + | "SIGIO" + | "SIGPWR" + | "SIGSYS" + | "SIGEMT" + | "SIGINFO"; "depends-on": DependsOn; }; diff --git a/util.ts b/util.ts new file mode 100644 index 0000000..5ac30c4 --- /dev/null +++ b/util.ts @@ -0,0 +1,17 @@ +import * as T from "./types.ts"; + +export function unwrapResultType(res: T.ResultType): T { + if ("error-code" in res) { + throw new Error(res["error-code"][1]); + } else if ("error" in res) { + throw new Error(res["error"]); + } else { + return res.result; + } +} + +/** Used to check if the file exists before hand */ +export const exists = ( + effects: T.Effects, + props: { path: string; volumeId: string }, +) => effects.metadata(props).then((_) => true, (_) => false); From 3173b96fc9cfd6e57e15fc2a6858f0dc5b59b5ee Mon Sep 17 00:00:00 2001 From: BluJ Date: Mon, 18 Jul 2022 15:27:36 -0600 Subject: [PATCH 2/2] chore: Magic phantom types --- compat/migrations.ts | 4 ++-- migrations.ts | 31 +++++++++++++++++++------------ 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/compat/migrations.ts b/compat/migrations.ts index 5603923..156a263 100644 --- a/compat/migrations.ts +++ b/compat/migrations.ts @@ -25,7 +25,7 @@ export function updateConfig< noRepeat?: NoRepeat, noFail = false, ): M.MigrationFn { - return async (effects: T.Effects) => { + return M.migrationFn(async (effects: T.Effects) => { await noRepeatGuard(effects, noRepeat, async () => { let config = util.unwrapResultType(await getConfig({})(effects)).config; if (config) { @@ -42,7 +42,7 @@ export function updateConfig< } }); return { configured }; - }; + }); } export async function noRepeatGuard< diff --git a/migrations.ts b/migrations.ts index 5d49edb..2aab863 100644 --- a/migrations.ts +++ b/migrations.ts @@ -4,7 +4,15 @@ import { matches } from "./dependencies.ts"; export type MigrationFn = ( effects: T.Effects, -) => Promise; +) => Promise & { _type: type; _version: version }; + +export function migrationFn( + fn: ( + effects: T.Effects, + ) => Promise, +): MigrationFn { + return fn as MigrationFn; +} export interface Migration { up: MigrationFn; @@ -34,17 +42,16 @@ export function fromMapping( const current = EmVer.parse(currentVersion); const other = EmVer.parse(version); - const filteredMigrations = - (Object.entries(migrations) as [ - keyof MigrationMapping, - Migration, - ][]) - .map(([version, migration]) => ({ - version: EmVer.parse(version), - migration, - })).filter(({ version }) => - version.greaterThan(other) && version.lessThanOrEqual(current) - ); + const filteredMigrations = (Object.entries(migrations) as [ + keyof MigrationMapping, + Migration, + ][]) + .map(([version, migration]) => ({ + version: EmVer.parse(version), + migration, + })).filter(({ version }) => + version.greaterThan(other) && version.lessThanOrEqual(current) + ); const migrationsToRun = matches.matches(direction) .when("from", () =>