mirror of
https://github.com/Start9Labs/start-sdk.git
synced 2026-03-26 10:21:55 +00:00
chore: Do the migrations
This commit is contained in:
@@ -8,13 +8,13 @@ export function setupActions(...createdActions: CreatedAction<any, any>[]) {
|
||||
actions[action.metaData.id] = action.exportedAction;
|
||||
}
|
||||
|
||||
const manifestActions = async (effects: Effects) => {
|
||||
const initializeActions = async (effects: Effects) => {
|
||||
for (const action of createdActions) {
|
||||
action.exportAction(effects);
|
||||
}
|
||||
};
|
||||
return {
|
||||
actions,
|
||||
manifestActions,
|
||||
initializeActions,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { GenericManifest } from "../manifest/ManifestTypes";
|
||||
import * as T from "../types";
|
||||
|
||||
export type BACKUP = "BACKUP";
|
||||
export const DEFAULT_OPTIONS: T.BackupOptions = {
|
||||
delete: true,
|
||||
force: true,
|
||||
ignoreExisting: false,
|
||||
exclude: [],
|
||||
};
|
||||
type BackupSet = {
|
||||
type BackupSet<Volumes extends string> = {
|
||||
srcPath: string;
|
||||
srcVolume: string;
|
||||
srcVolume: Volumes | BACKUP;
|
||||
dstPath: string;
|
||||
dstVolume: string;
|
||||
dstVolume: Volumes | BACKUP;
|
||||
options?: Partial<T.BackupOptions>;
|
||||
};
|
||||
/**
|
||||
@@ -37,13 +38,13 @@ type BackupSet = {
|
||||
* ```
|
||||
*/
|
||||
export class Backups<M extends GenericManifest> {
|
||||
static BACKUP = "BACKUP" as const;
|
||||
static BACKUP: BACKUP = "BACKUP";
|
||||
|
||||
constructor(
|
||||
private options = DEFAULT_OPTIONS,
|
||||
private backupSet = [] as BackupSet[],
|
||||
private backupSet = [] as BackupSet<keyof M["volumes"] & string>[],
|
||||
) {}
|
||||
static volumes<M extends GenericManifest>(
|
||||
static volumes<M extends GenericManifest = never>(
|
||||
...volumeNames: Array<keyof M["volumes"] & string>
|
||||
) {
|
||||
return new Backups().addSets(
|
||||
@@ -55,10 +56,14 @@ export class Backups<M extends GenericManifest> {
|
||||
})),
|
||||
);
|
||||
}
|
||||
static addSets(...options: BackupSet[]) {
|
||||
static addSets<M extends GenericManifest = never>(
|
||||
...options: BackupSet<keyof M["volumes"] & string>[]
|
||||
) {
|
||||
return new Backups().addSets(...options);
|
||||
}
|
||||
static with_options(options?: Partial<T.BackupOptions>) {
|
||||
static with_options<M extends GenericManifest = never>(
|
||||
options?: Partial<T.BackupOptions>,
|
||||
) {
|
||||
return new Backups({ ...DEFAULT_OPTIONS, ...options });
|
||||
}
|
||||
set_options(options?: Partial<T.BackupOptions>) {
|
||||
@@ -78,7 +83,7 @@ export class Backups<M extends GenericManifest> {
|
||||
})),
|
||||
);
|
||||
}
|
||||
addSets(...options: BackupSet[]) {
|
||||
addSets(...options: BackupSet<keyof M["volumes"] & string>[]) {
|
||||
options.forEach((x) =>
|
||||
this.backupSet.push({ ...x, options: { ...this.options, ...x.options } }),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { string } from "ts-matches";
|
||||
import { Backups } from ".";
|
||||
import { GenericManifest } from "../manifest/ManifestTypes";
|
||||
import { BackupOptions } from "../types";
|
||||
import { BackupOptions, ExpectedExports } from "../types";
|
||||
|
||||
export type SetupBackupsParams<M extends GenericManifest> =
|
||||
| [Partial<BackupOptions>, ...Array<keyof M["volumes"] & string>]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Config } from "./builder";
|
||||
import { DeepPartial, Dependencies, Effects, ExpectedExports } from "../types";
|
||||
import { InputSpec } from "./configTypes";
|
||||
import { Utils, nullIfEmpty, utils } from "../util";
|
||||
import { Utils, nullIfEmpty, once, utils } from "../util";
|
||||
import { TypeFromProps } from "../util/propertiesMatcher";
|
||||
|
||||
declare const dependencyProof: unique symbol;
|
||||
@@ -30,11 +30,11 @@ export function setupConfig<WD, A extends Config<InputSpec>>(
|
||||
write: Save<WD, TypeFromProps<A>>,
|
||||
read: Read<WD, TypeFromProps<A>>,
|
||||
) {
|
||||
const validator = spec.validator();
|
||||
const validator = once(() => spec.validator());
|
||||
return {
|
||||
setConfig: (async ({ effects, input }) => {
|
||||
if (!validator.test(input)) {
|
||||
await effects.error(String(validator.errorMessage(input)));
|
||||
if (!validator().test(input)) {
|
||||
await effects.error(String(validator().errorMessage(input)));
|
||||
return { error: "Set config type error for config" };
|
||||
}
|
||||
await write({
|
||||
|
||||
@@ -9,7 +9,9 @@ describe("EmVer", () => {
|
||||
expect(checker.check("1.2.3.4")).toEqual(true);
|
||||
});
|
||||
test("rangeOf('*') invalid", () => {
|
||||
// @ts-expect-error
|
||||
expect(() => checker.check("a")).toThrow();
|
||||
// @ts-expect-error
|
||||
expect(() => checker.check("")).toThrow();
|
||||
expect(() => checker.check("1..3")).toThrow();
|
||||
});
|
||||
@@ -18,6 +20,7 @@ describe("EmVer", () => {
|
||||
{
|
||||
const checker = rangeOf(">1.2.3.4");
|
||||
test(`rangeOf(">1.2.3.4") valid`, () => {
|
||||
expect(checker.check("2-beta123")).toEqual(true);
|
||||
expect(checker.check("2")).toEqual(true);
|
||||
expect(checker.check("1.2.3.5")).toEqual(true);
|
||||
expect(checker.check("1.2.3.4.1")).toEqual(true);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as matches from "ts-matches";
|
||||
|
||||
const starSub = /((\d+\.)*\d+)\.\*/;
|
||||
// prettier-ignore
|
||||
export type ValidEmVer = `${'>' | '<' | '>=' | '<=' | '=' | ''}${number | '*'}${`.${number | '*'}` | ""}${`.${number | '*'}` | ""}${`.${number | '*'}` | ""}${`-${string}` | ""}`;
|
||||
|
||||
function incrementLastNumber(list: number[]) {
|
||||
const newList = [...list];
|
||||
@@ -61,7 +63,7 @@ export class EmVer {
|
||||
* Or an already made emver
|
||||
* IsUnsafe
|
||||
*/
|
||||
static from(range: string | EmVer): EmVer {
|
||||
static from(range: ValidEmVer | EmVer): EmVer {
|
||||
if (range instanceof EmVer) {
|
||||
return range;
|
||||
}
|
||||
@@ -71,22 +73,26 @@ export class EmVer {
|
||||
* Convert the range, should be 1.2.* or * into a emver
|
||||
* IsUnsafe
|
||||
*/
|
||||
static parse(range: string): EmVer {
|
||||
static parse(rangeExtra: string): EmVer {
|
||||
const [range, extra] = rangeExtra.split("-");
|
||||
const values = range.split(".").map((x) => parseInt(x));
|
||||
for (const value of values) {
|
||||
if (isNaN(value)) {
|
||||
throw new Error(`Couldn't parse range: ${range}`);
|
||||
}
|
||||
}
|
||||
return new EmVer(values);
|
||||
return new EmVer(values, extra);
|
||||
}
|
||||
private constructor(public readonly values: number[]) {}
|
||||
private constructor(
|
||||
public readonly values: number[],
|
||||
readonly extra: string | null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Used when we need a new emver that has the last number incremented, used in the 1.* like things
|
||||
*/
|
||||
public withLastIncremented() {
|
||||
return new EmVer(incrementLastNumber(this.values));
|
||||
return new EmVer(incrementLastNumber(this.values), null);
|
||||
}
|
||||
|
||||
public greaterThan(other: EmVer): boolean {
|
||||
@@ -153,6 +159,10 @@ export class EmVer {
|
||||
.when("less", () => -1 as const)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `${this.values.join(".")}${this.extra ? `-${this.extra}` : ""}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -248,7 +258,7 @@ export class Checker {
|
||||
* Check is the function that will be given a emver or unparsed emver and should give if it follows
|
||||
* a pattern
|
||||
*/
|
||||
public readonly check: (value: string | EmVer) => boolean,
|
||||
public readonly check: (value: ValidEmVer | EmVer) => boolean,
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { ExpectedExports } from "../types";
|
||||
|
||||
declare const ActionProof: unique symbol;
|
||||
export type ActionReceipt = {
|
||||
[ActionProof]: never;
|
||||
};
|
||||
export function noActions(): ActionReceipt {
|
||||
return {} as ActionReceipt;
|
||||
}
|
||||
|
||||
declare const MigrationProof: unique symbol;
|
||||
export type MigrationReceipt = {
|
||||
[MigrationProof]: never;
|
||||
};
|
||||
export function noMigrationsUp(): MigrationReceipt {
|
||||
return {} as MigrationReceipt;
|
||||
}
|
||||
export function migrationUp(fn: () => Promise<unknown>): MigrationReceipt {
|
||||
fn();
|
||||
return {} as MigrationReceipt;
|
||||
}
|
||||
|
||||
declare const MigrationDownProof: unique symbol;
|
||||
export type MigrationDownReceipt = {
|
||||
[MigrationDownProof]: never;
|
||||
};
|
||||
export function noMigrationsDown(): MigrationDownReceipt {
|
||||
return {} as MigrationDownReceipt;
|
||||
}
|
||||
export function migrationDown(
|
||||
fn: () => Promise<unknown>,
|
||||
): MigrationDownReceipt {
|
||||
fn();
|
||||
return {} as MigrationDownReceipt;
|
||||
}
|
||||
|
||||
export function setupInit(
|
||||
fn: (
|
||||
...args: Parameters<ExpectedExports.init>
|
||||
) => Promise<[MigrationReceipt, ActionReceipt]>,
|
||||
) {
|
||||
const initFn: ExpectedExports.init = (...args) => fn(...args);
|
||||
return initFn;
|
||||
}
|
||||
|
||||
export function setupUninit(
|
||||
fn: (
|
||||
...args: Parameters<ExpectedExports.uninit>
|
||||
) => Promise<[MigrationDownReceipt]>,
|
||||
) {
|
||||
const uninitFn: ExpectedExports.uninit = (...args) => fn(...args);
|
||||
return uninitFn;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Effects, ExpectedExports } from "../types";
|
||||
import { Utils, utils } from "../util";
|
||||
import { Daemons } from "./Daemons";
|
||||
export * as network from "./exportInterfaces";
|
||||
export { LocalBinding } from "./LocalBinding";
|
||||
@@ -21,14 +22,18 @@ export { Daemons } from "./Daemons";
|
||||
* @param fn
|
||||
* @returns
|
||||
*/
|
||||
export const runningMain: (
|
||||
export const setupMain = <WrapperData>(
|
||||
fn: (o: {
|
||||
effects: Effects;
|
||||
started(onTerm: () => void): null;
|
||||
utils: Utils<WrapperData>;
|
||||
}) => Promise<Daemons<any>>,
|
||||
) => ExpectedExports.main = (fn) => {
|
||||
): ExpectedExports.main => {
|
||||
return async (options) => {
|
||||
const result = await fn(options);
|
||||
const result = await fn({
|
||||
...options,
|
||||
utils: utils<WrapperData>(options.effects),
|
||||
});
|
||||
await result.build().then((x) => x.wait());
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ValidEmVer } from "../emverLite/mod";
|
||||
|
||||
export interface Container {
|
||||
image: string;
|
||||
mounts: Record<string, string>;
|
||||
@@ -5,10 +7,12 @@ export interface Container {
|
||||
sigtermTimeout?: string; // if more than 30s to shutdown
|
||||
}
|
||||
|
||||
export type ManifestVersion = ValidEmVer;
|
||||
|
||||
export interface GenericManifest {
|
||||
id: string;
|
||||
title: string;
|
||||
version: string;
|
||||
version: ManifestVersion;
|
||||
releaseNotes: string;
|
||||
license: string; // name of license
|
||||
replaces: string[];
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { GenericManifest } from "./ManifestTypes";
|
||||
import { GenericManifest, ManifestVersion } from "./ManifestTypes";
|
||||
|
||||
export function setupManifest<
|
||||
M extends GenericManifest & { id: Id },
|
||||
M extends GenericManifest & { id: Id; version: Version },
|
||||
Id extends string,
|
||||
Version extends ManifestVersion,
|
||||
>(manifest: M): M {
|
||||
return manifest;
|
||||
}
|
||||
|
||||
28
lib/migrations/Migration.ts
Normal file
28
lib/migrations/Migration.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { ManifestVersion } from "../manifest/ManifestTypes";
|
||||
import { Effects } from "../types";
|
||||
import { Utils } from "../util";
|
||||
|
||||
export class Migration<Version extends ManifestVersion> {
|
||||
constructor(
|
||||
readonly options: {
|
||||
version: Version;
|
||||
up: (opts: { effects: Effects }) => Promise<void>;
|
||||
down: (opts: { effects: Effects }) => Promise<void>;
|
||||
},
|
||||
) {}
|
||||
static of<Version extends ManifestVersion>(options: {
|
||||
version: Version;
|
||||
up: (opts: { effects: Effects }) => Promise<void>;
|
||||
down: (opts: { effects: Effects }) => Promise<void>;
|
||||
}) {
|
||||
return new Migration(options);
|
||||
}
|
||||
|
||||
async up(opts: { effects: Effects }) {
|
||||
this.up(opts);
|
||||
}
|
||||
|
||||
async down(opts: { effects: Effects }) {
|
||||
this.down(opts);
|
||||
}
|
||||
}
|
||||
52
lib/migrations/setupMigrations.ts
Normal file
52
lib/migrations/setupMigrations.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { setupActions } from "../actions/setupActions";
|
||||
import { EmVer } from "../emverLite/mod";
|
||||
import { GenericManifest } from "../manifest/ManifestTypes";
|
||||
import { ExpectedExports } from "../types";
|
||||
import { once } from "../util/once";
|
||||
import { Migration } from "./Migration";
|
||||
|
||||
export function setupMigrations<Migrations extends Array<Migration<any>>>(
|
||||
manifest: GenericManifest,
|
||||
initializeActions: ReturnType<typeof setupActions>["initializeActions"],
|
||||
...migrations: EnsureUniqueId<Migrations>
|
||||
) {
|
||||
const sortedMigrations = once(() => {
|
||||
const migrationsAsVersions = (migrations as Array<Migration<any>>).map(
|
||||
(x) => [EmVer.parse(x.options.version), x] as const,
|
||||
);
|
||||
migrationsAsVersions.sort((a, b) => a[0].compareForSort(b[0]));
|
||||
return migrationsAsVersions;
|
||||
});
|
||||
const currentVersion = once(() => EmVer.parse(manifest.version));
|
||||
const init: ExpectedExports.init = async ({ effects, previousVersion }) => {
|
||||
await initializeActions(effects);
|
||||
if (!!previousVersion) {
|
||||
const previousVersionEmVer = EmVer.parse(previousVersion);
|
||||
for (const [_, migration] of sortedMigrations()
|
||||
.filter((x) => x[0].greaterThan(previousVersionEmVer))
|
||||
.filter((x) => x[0].lessThanOrEqual(currentVersion()))) {
|
||||
await migration.up({ effects });
|
||||
}
|
||||
}
|
||||
};
|
||||
const uninit: ExpectedExports.uninit = async ({ effects, nextVersion }) => {
|
||||
if (!!nextVersion) {
|
||||
const nextVersionEmVer = EmVer.parse(nextVersion);
|
||||
const reversed = [...sortedMigrations()].reverse();
|
||||
for (const [_, migration] of reversed
|
||||
.filter((x) => x[0].greaterThan(nextVersionEmVer))
|
||||
.filter((x) => x[0].lessThanOrEqual(currentVersion()))) {
|
||||
await migration.down({ effects });
|
||||
}
|
||||
}
|
||||
};
|
||||
return { init, uninit };
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
export type EnsureUniqueId<A, B = A, ids = never> =
|
||||
B extends [] ? A :
|
||||
B extends [Migration<infer id>, ...infer Rest] ? (
|
||||
id extends ids ? "One of the ids are not unique"[] :
|
||||
EnsureUniqueId<A, Rest, id | ids>
|
||||
) : "There exists a migration that is not a Migration"[]
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ExpectedExports, Properties } from "../types";
|
||||
import { Utils, utils } from "../util";
|
||||
import "../util/extensions";
|
||||
import { PropertyGroup } from "./PropertyGroup";
|
||||
import { PropertyString } from "./PropertyString";
|
||||
@@ -18,13 +19,17 @@ export type UnionToIntersection<T> = ((x: T) => any) extends (x: infer R) => any
|
||||
* @param fn
|
||||
* @returns
|
||||
*/
|
||||
export function setupPropertiesExport(
|
||||
fn: (
|
||||
...args: Parameters<ExpectedExports.properties>
|
||||
) => void | Promise<void> | Promise<(PropertyGroup | PropertyString)[]>,
|
||||
export function setupProperties<WrapperData>(
|
||||
fn: (args: {
|
||||
wrapperData: WrapperData;
|
||||
}) => void | Promise<void> | Promise<(PropertyGroup | PropertyString)[]>,
|
||||
): ExpectedExports.properties {
|
||||
return (async (...args) => {
|
||||
const result = await fn(...args);
|
||||
return (async (options) => {
|
||||
const result = await fn(
|
||||
options as {
|
||||
wrapperData: WrapperData & typeof options.wrapperData;
|
||||
},
|
||||
);
|
||||
if (result) {
|
||||
const answer: Properties = result.map((x) => x.data);
|
||||
return answer;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export * as configTypes from "./config/configTypes";
|
||||
import { InputSpec } from "./config/configTypes";
|
||||
import { DependenciesReceipt } from "./config/setupConfig";
|
||||
import { ActionReceipt } from "./init";
|
||||
|
||||
export type ExportedAction = (options: {
|
||||
effects: Effects;
|
||||
@@ -350,7 +349,7 @@ export type Effects = {
|
||||
*
|
||||
* @param options
|
||||
*/
|
||||
exportAction(options: ActionMetaData): Promise<void & ActionReceipt>;
|
||||
exportAction(options: ActionMetaData): Promise<void>;
|
||||
/**
|
||||
* Remove an action that was exported. Used problably during main or during setConfig.
|
||||
*/
|
||||
|
||||
@@ -17,6 +17,7 @@ export { FileHelper } from "./fileHelper";
|
||||
export { getWrapperData } from "./getWrapperData";
|
||||
export { deepEqual } from "./deepEqual";
|
||||
export { deepMerge } from "./deepMerge";
|
||||
export { once } from "./once";
|
||||
|
||||
/** Used to check if the file exists before hand */
|
||||
export const exists = (
|
||||
|
||||
9
lib/util/once.ts
Normal file
9
lib/util/once.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export function once<B>(fn: () => B): () => B {
|
||||
let result: [B] | [] = [];
|
||||
return () => {
|
||||
if (!result.length) {
|
||||
result = [fn()];
|
||||
}
|
||||
return result[0];
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user