mirror of
https://github.com/Start9Labs/start-sdk.git
synced 2026-03-30 12:21:57 +00:00
more utility functions
This commit is contained in:
121
compat/migrations.ts
Normal file
121
compat/migrations.ts
Normal 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);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
|
||||||
@@ -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
2
mod.ts
@@ -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";
|
||||||
|
|||||||
84
types.ts
84
types.ts
@@ -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
17
util.ts
Normal 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);
|
||||||
Reference in New Issue
Block a user