more utility functions

This commit is contained in:
Aiden McClelland
2022-07-18 15:08:54 -06:00
parent 47cf6de393
commit 3a7e0989b9
8 changed files with 241 additions and 98 deletions

121
compat/migrations.ts Normal file
View File

@@ -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 extends string, type extends "up" | "down"> {
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<version, type>,
noFail = false,
): M.MigrationFn<version, type> {
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<version, type> | undefined,
fn: () => Promise<void>,
): Promise<void> {
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<versions extends string>(
effects: T.Effects,
migrations: M.MigrationMapping<versions>,
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<versions extends string>(
migrations: M.MigrationMapping<versions>,
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);
};
}

View File

@@ -1,3 +1,4 @@
export { properties } from "./properties.ts"; export { properties } from "./properties.ts";
export { setConfig } from "./setConfig.ts"; export { setConfig } from "./setConfig.ts";
export { getConfig } from "./getConfig.ts"; export { getConfig } from "./getConfig.ts";
export * as migrations from "./migrations.ts";

View File

@@ -1,38 +1,40 @@
import { YAML } from "../dependencies.ts"; import { YAML } from "../dependencies.ts";
import { exists } from "../exists.ts"; import { exists } from "../util.ts";
import { ResultType, Properties, ExpectedExports, Effects } from "../types.ts"; import { Effects, ExpectedExports, Properties, ResultType } from "../types.ts";
// deno-lint-ignore no-explicit-any // 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<Properties> = { const noPropertiesFound: ResultType<Properties> = {
result: { result: {
version: 2, version: 2,
data: { data: {
"Not Ready": { "Not Ready": {
type: "string", type: "string",
value: "Could not find properties. The service might still be starting", value: "Could not find properties. The service might still be starting",
qr: false, qr: false,
copyable: false, copyable: false,
masked: false, masked: false,
description: "Fallback Message When Properties could not be found" description: "Fallback Message When Properties could not be found",
} },
} },
} },
} as const } as const;
/** /**
* Default will pull from a file (start9/stats.yaml) expected to be made on the main volume * Default will pull from a file (start9/stats.yaml) expected to be made on the main volume
* @param effects * @param effects
* @returns * @returns
*/ */
export const properties: ExpectedExports.properties = async ( export const properties: ExpectedExports.properties = async (
effects: Effects, effects: Effects,
) => { ) => {
if (await exists(effects, { path: "start9/stats.yaml", volumeId: "main" }) === false) { if (
return noPropertiesFound; await exists(effects, { path: "start9/stats.yaml", volumeId: "main" }) ===
} false
return await effects.readFile({ ) {
path: "start9/stats.yaml", return noPropertiesFound;
volumeId: "main", }
}).then(YAML.parse).then(asResult) return await effects.readFile({
path: "start9/stats.yaml",
volumeId: "main",
}).then(YAML.parse).then(asResult);
}; };

View File

@@ -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);

View File

@@ -2,19 +2,21 @@ import { types as T } from "./mod.ts";
import { EmVer } from "./emver-lite/mod.ts"; import { EmVer } from "./emver-lite/mod.ts";
import { matches } from "./dependencies.ts"; import { matches } from "./dependencies.ts";
export type MigrationFn = (effects: T.Effects) => Promise<T.MigrationRes>; export type MigrationFn<version extends string, type extends "up" | "down"> = (
effects: T.Effects,
) => Promise<T.MigrationRes>;
export interface Migration { export interface Migration<version extends string> {
up: MigrationFn; up: MigrationFn<version, "up">;
down: MigrationFn; down: MigrationFn<version, "down">;
} }
export interface MigrationMapping { export type MigrationMapping<versions extends string> = {
[version: string]: Migration; [version in versions]: Migration<version>;
} };
export function fromMapping( export function fromMapping<versions extends string>(
migrations: MigrationMapping, migrations: MigrationMapping<versions>,
currentVersion: string, currentVersion: string,
): T.ExpectedExports.migration { ): T.ExpectedExports.migration {
const directionShape = matches.literals("from", "to"); const directionShape = matches.literals("from", "to");
@@ -32,24 +34,26 @@ export function fromMapping(
const current = EmVer.parse(currentVersion); const current = EmVer.parse(currentVersion);
const other = EmVer.parse(version); const other = EmVer.parse(version);
const filteredMigrations =
(Object.entries(migrations) as [
keyof MigrationMapping<string>,
Migration<string>,
][])
.map(([version, migration]) => ({
version: EmVer.parse(version),
migration,
})).filter(({ version }) =>
version.greaterThan(other) && version.lessThanOrEqual(current)
);
const migrationsToRun = matches.matches(direction) const migrationsToRun = matches.matches(direction)
.when("from", () => .when("from", () =>
Object.entries(migrations) filteredMigrations
.map(([version, migration]) => ({ .sort((a, b) => a.version.compareForSort(b.version)) // low to high
version: EmVer.parse(version),
migration,
})).filter(({ version }) =>
version.greaterThan(other) && version.lessThanOrEqual(current)
).sort((a, b) => a.version.compareForSort(b.version))
.map(({ migration }) => migration.up)) .map(({ migration }) => migration.up))
.when("to", () => .when("to", () =>
Object.entries(migrations) filteredMigrations
.map(([version, migration]) => ({ .sort((a, b) => b.version.compareForSort(a.version)) // high to low
version: EmVer.parse(version),
migration,
})).filter(({ version }) =>
version.lessThanOrEqual(other) && version.greaterThan(current)
).sort((a, b) => b.version.compareForSort(a.version))
.map(({ migration }) => migration.down)) .map(({ migration }) => migration.down))
.unwrap(); .unwrap();

2
mod.ts
View File

@@ -1,6 +1,6 @@
export { matches, YAML } from "./dependencies.ts"; export { matches, YAML } from "./dependencies.ts";
export * as types from "./types.ts"; export * as types from "./types.ts";
export { exists } from "./exists.ts";
export * as compat from "./compat/mod.ts"; export * as compat from "./compat/mod.ts";
export * as migrations from "./migrations.ts"; export * as migrations from "./migrations.ts";
export * as util from "./util.ts";

View File

@@ -24,6 +24,7 @@ export namespace ExpectedExports {
export type migration = ( export type migration = (
effects: Effects, effects: Effects,
version: string, version: string,
...args: unknown[]
) => Promise<ResultType<MigrationRes>>; ) => Promise<ResultType<MigrationRes>>;
export type action = { export type action = {
[id: string]: ( [id: string]: (
@@ -75,12 +76,12 @@ export type Effects = {
fetch(url: string, options?: { fetch(url: string, options?: {
method?: method?:
| "GET" | "GET"
| "POST" | "POST"
| "PUT" | "PUT"
| "DELETE" | "DELETE"
| "HEAD" | "HEAD"
| "PATCH"; | "PATCH";
headers?: Record<string, string>; headers?: Record<string, string>;
body?: string; body?: string;
}): Promise<{ }): Promise<{
@@ -143,7 +144,6 @@ export type WithNullableDefault<T, Default> = T & {
default?: Default; default?: Default;
}; };
export type WithDescription<T> = T & { export type WithDescription<T> = T & {
description?: string; description?: string;
name: string; name: string;
@@ -213,7 +213,9 @@ export type ValueSpecAny =
| Tag<"boolean", WithDescription<WithDefault<ValueSpecBoolean, boolean>>> | Tag<"boolean", WithDescription<WithDefault<ValueSpecBoolean, boolean>>>
| Tag< | Tag<
"string", "string",
WithDescription<WithNullableDefault<WithNullable<ValueSpecString>, DefaultString>> WithDescription<
WithNullableDefault<WithNullable<ValueSpecString>, DefaultString>
>
> >
| Tag< | Tag<
"number", "number",
@@ -338,39 +340,39 @@ export type ValueSpecEnum = {
export type SetResult = { export type SetResult = {
/** These are the unix process signals */ /** These are the unix process signals */
signal: signal:
| "SIGTERM" | "SIGTERM"
| "SIGHUP" | "SIGHUP"
| "SIGINT" | "SIGINT"
| "SIGQUIT" | "SIGQUIT"
| "SIGILL" | "SIGILL"
| "SIGTRAP" | "SIGTRAP"
| "SIGABRT" | "SIGABRT"
| "SIGBUS" | "SIGBUS"
| "SIGFPE" | "SIGFPE"
| "SIGKILL" | "SIGKILL"
| "SIGUSR1" | "SIGUSR1"
| "SIGSEGV" | "SIGSEGV"
| "SIGUSR2" | "SIGUSR2"
| "SIGPIPE" | "SIGPIPE"
| "SIGALRM" | "SIGALRM"
| "SIGSTKFLT" | "SIGSTKFLT"
| "SIGCHLD" | "SIGCHLD"
| "SIGCONT" | "SIGCONT"
| "SIGSTOP" | "SIGSTOP"
| "SIGTSTP" | "SIGTSTP"
| "SIGTTIN" | "SIGTTIN"
| "SIGTTOU" | "SIGTTOU"
| "SIGURG" | "SIGURG"
| "SIGXCPU" | "SIGXCPU"
| "SIGXFSZ" | "SIGXFSZ"
| "SIGVTALRM" | "SIGVTALRM"
| "SIGPROF" | "SIGPROF"
| "SIGWINCH" | "SIGWINCH"
| "SIGIO" | "SIGIO"
| "SIGPWR" | "SIGPWR"
| "SIGSYS" | "SIGSYS"
| "SIGEMT" | "SIGEMT"
| "SIGINFO"; | "SIGINFO";
"depends-on": DependsOn; "depends-on": DependsOn;
}; };

17
util.ts Normal file
View File

@@ -0,0 +1,17 @@
import * as T from "./types.ts";
export function unwrapResultType<T>(res: T.ResultType<T>): 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);