chore: Do the migrations

This commit is contained in:
BluJ
2023-04-24 15:52:43 -06:00
parent 75ec297be1
commit 50cfc7aa43
19 changed files with 167 additions and 98 deletions

View File

@@ -3,14 +3,17 @@ version = $(shell git tag --sort=committerdate | tail -1)
test: $(TS_FILES) lib/test/output.ts test: $(TS_FILES) lib/test/output.ts
npm test npm test
make clean: clean:
rm -rf dist rm -rf dist/* | true
lib/test/output.ts: lib/test/makeOutput.ts scripts/oldSpecToBuilder.ts lib/test/output.ts: lib/test/makeOutput.ts scripts/oldSpecToBuilder.ts
npm run buildOutput npm run buildOutput
bundle: fmt $(TS_FILES) package.json .FORCE node_modules test bundle: clean fmt $(TS_FILES) package.json .FORCE node_modules test
npx tsc npx tsc
cp package.json dist/package.json
cp README.md dist/README.md
cp LICENSE dist/LICENSE
check: check:
npm run check npm run check
@@ -22,9 +25,6 @@ node_modules: package.json
npm install npm install
publish: clean bundle package.json README.md LICENSE publish: clean bundle package.json README.md LICENSE
cp package.json dist/package.json
cp README.md dist/README.md
cp LICENSE dist/LICENSE
cd dist && npm publish cd dist && npm publish
link: bundle link: bundle
cp package.json dist/package.json cp package.json dist/package.json

View File

@@ -8,13 +8,13 @@ export function setupActions(...createdActions: CreatedAction<any, any>[]) {
actions[action.metaData.id] = action.exportedAction; actions[action.metaData.id] = action.exportedAction;
} }
const manifestActions = async (effects: Effects) => { const initializeActions = async (effects: Effects) => {
for (const action of createdActions) { for (const action of createdActions) {
action.exportAction(effects); action.exportAction(effects);
} }
}; };
return { return {
actions, actions,
manifestActions, initializeActions,
}; };
} }

View File

@@ -1,17 +1,18 @@
import { GenericManifest } from "../manifest/ManifestTypes"; import { GenericManifest } from "../manifest/ManifestTypes";
import * as T from "../types"; import * as T from "../types";
export type BACKUP = "BACKUP";
export const DEFAULT_OPTIONS: T.BackupOptions = { export const DEFAULT_OPTIONS: T.BackupOptions = {
delete: true, delete: true,
force: true, force: true,
ignoreExisting: false, ignoreExisting: false,
exclude: [], exclude: [],
}; };
type BackupSet = { type BackupSet<Volumes extends string> = {
srcPath: string; srcPath: string;
srcVolume: string; srcVolume: Volumes | BACKUP;
dstPath: string; dstPath: string;
dstVolume: string; dstVolume: Volumes | BACKUP;
options?: Partial<T.BackupOptions>; options?: Partial<T.BackupOptions>;
}; };
/** /**
@@ -37,13 +38,13 @@ type BackupSet = {
* ``` * ```
*/ */
export class Backups<M extends GenericManifest> { export class Backups<M extends GenericManifest> {
static BACKUP = "BACKUP" as const; static BACKUP: BACKUP = "BACKUP";
constructor( constructor(
private options = DEFAULT_OPTIONS, 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> ...volumeNames: Array<keyof M["volumes"] & string>
) { ) {
return new Backups().addSets( 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); 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 }); return new Backups({ ...DEFAULT_OPTIONS, ...options });
} }
set_options(options?: Partial<T.BackupOptions>) { 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) => options.forEach((x) =>
this.backupSet.push({ ...x, options: { ...this.options, ...x.options } }), this.backupSet.push({ ...x, options: { ...this.options, ...x.options } }),
); );

View File

@@ -1,7 +1,7 @@
import { string } from "ts-matches"; import { string } from "ts-matches";
import { Backups } from "."; import { Backups } from ".";
import { GenericManifest } from "../manifest/ManifestTypes"; import { GenericManifest } from "../manifest/ManifestTypes";
import { BackupOptions } from "../types"; import { BackupOptions, ExpectedExports } from "../types";
export type SetupBackupsParams<M extends GenericManifest> = export type SetupBackupsParams<M extends GenericManifest> =
| [Partial<BackupOptions>, ...Array<keyof M["volumes"] & string>] | [Partial<BackupOptions>, ...Array<keyof M["volumes"] & string>]

View File

@@ -1,7 +1,7 @@
import { Config } from "./builder"; import { Config } from "./builder";
import { DeepPartial, Dependencies, Effects, ExpectedExports } from "../types"; import { DeepPartial, Dependencies, Effects, ExpectedExports } from "../types";
import { InputSpec } from "./configTypes"; import { InputSpec } from "./configTypes";
import { Utils, nullIfEmpty, utils } from "../util"; import { Utils, nullIfEmpty, once, utils } from "../util";
import { TypeFromProps } from "../util/propertiesMatcher"; import { TypeFromProps } from "../util/propertiesMatcher";
declare const dependencyProof: unique symbol; declare const dependencyProof: unique symbol;
@@ -30,11 +30,11 @@ export function setupConfig<WD, A extends Config<InputSpec>>(
write: Save<WD, TypeFromProps<A>>, write: Save<WD, TypeFromProps<A>>,
read: Read<WD, TypeFromProps<A>>, read: Read<WD, TypeFromProps<A>>,
) { ) {
const validator = spec.validator(); const validator = once(() => spec.validator());
return { return {
setConfig: (async ({ effects, input }) => { setConfig: (async ({ effects, input }) => {
if (!validator.test(input)) { if (!validator().test(input)) {
await effects.error(String(validator.errorMessage(input))); await effects.error(String(validator().errorMessage(input)));
return { error: "Set config type error for config" }; return { error: "Set config type error for config" };
} }
await write({ await write({

View File

@@ -9,7 +9,9 @@ describe("EmVer", () => {
expect(checker.check("1.2.3.4")).toEqual(true); expect(checker.check("1.2.3.4")).toEqual(true);
}); });
test("rangeOf('*') invalid", () => { test("rangeOf('*') invalid", () => {
// @ts-expect-error
expect(() => checker.check("a")).toThrow(); expect(() => checker.check("a")).toThrow();
// @ts-expect-error
expect(() => checker.check("")).toThrow(); expect(() => checker.check("")).toThrow();
expect(() => checker.check("1..3")).toThrow(); expect(() => checker.check("1..3")).toThrow();
}); });
@@ -18,6 +20,7 @@ describe("EmVer", () => {
{ {
const checker = rangeOf(">1.2.3.4"); const checker = rangeOf(">1.2.3.4");
test(`rangeOf(">1.2.3.4") valid`, () => { test(`rangeOf(">1.2.3.4") valid`, () => {
expect(checker.check("2-beta123")).toEqual(true);
expect(checker.check("2")).toEqual(true); expect(checker.check("2")).toEqual(true);
expect(checker.check("1.2.3.5")).toEqual(true); expect(checker.check("1.2.3.5")).toEqual(true);
expect(checker.check("1.2.3.4.1")).toEqual(true); expect(checker.check("1.2.3.4.1")).toEqual(true);

View File

@@ -1,6 +1,8 @@
import * as matches from "ts-matches"; import * as matches from "ts-matches";
const starSub = /((\d+\.)*\d+)\.\*/; const starSub = /((\d+\.)*\d+)\.\*/;
// prettier-ignore
export type ValidEmVer = `${'>' | '<' | '>=' | '<=' | '=' | ''}${number | '*'}${`.${number | '*'}` | ""}${`.${number | '*'}` | ""}${`.${number | '*'}` | ""}${`-${string}` | ""}`;
function incrementLastNumber(list: number[]) { function incrementLastNumber(list: number[]) {
const newList = [...list]; const newList = [...list];
@@ -61,7 +63,7 @@ export class EmVer {
* Or an already made emver * Or an already made emver
* IsUnsafe * IsUnsafe
*/ */
static from(range: string | EmVer): EmVer { static from(range: ValidEmVer | EmVer): EmVer {
if (range instanceof EmVer) { if (range instanceof EmVer) {
return range; return range;
} }
@@ -71,22 +73,26 @@ export class EmVer {
* Convert the range, should be 1.2.* or * into a emver * Convert the range, should be 1.2.* or * into a emver
* IsUnsafe * IsUnsafe
*/ */
static parse(range: string): EmVer { static parse(rangeExtra: string): EmVer {
const [range, extra] = rangeExtra.split("-");
const values = range.split(".").map((x) => parseInt(x)); const values = range.split(".").map((x) => parseInt(x));
for (const value of values) { for (const value of values) {
if (isNaN(value)) { if (isNaN(value)) {
throw new Error(`Couldn't parse range: ${range}`); 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 * Used when we need a new emver that has the last number incremented, used in the 1.* like things
*/ */
public withLastIncremented() { public withLastIncremented() {
return new EmVer(incrementLastNumber(this.values)); return new EmVer(incrementLastNumber(this.values), null);
} }
public greaterThan(other: EmVer): boolean { public greaterThan(other: EmVer): boolean {
@@ -153,6 +159,10 @@ export class EmVer {
.when("less", () => -1 as const) .when("less", () => -1 as const)
.unwrap(); .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 * Check is the function that will be given a emver or unparsed emver and should give if it follows
* a pattern * a pattern
*/ */
public readonly check: (value: string | EmVer) => boolean, public readonly check: (value: ValidEmVer | EmVer) => boolean,
) {} ) {}
/** /**

View File

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

View File

@@ -1,4 +1,5 @@
import { Effects, ExpectedExports } from "../types"; import { Effects, ExpectedExports } from "../types";
import { Utils, utils } from "../util";
import { Daemons } from "./Daemons"; import { Daemons } from "./Daemons";
export * as network from "./exportInterfaces"; export * as network from "./exportInterfaces";
export { LocalBinding } from "./LocalBinding"; export { LocalBinding } from "./LocalBinding";
@@ -21,14 +22,18 @@ export { Daemons } from "./Daemons";
* @param fn * @param fn
* @returns * @returns
*/ */
export const runningMain: ( export const setupMain = <WrapperData>(
fn: (o: { fn: (o: {
effects: Effects; effects: Effects;
started(onTerm: () => void): null; started(onTerm: () => void): null;
utils: Utils<WrapperData>;
}) => Promise<Daemons<any>>, }) => Promise<Daemons<any>>,
) => ExpectedExports.main = (fn) => { ): ExpectedExports.main => {
return async (options) => { 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()); await result.build().then((x) => x.wait());
}; };
}; };

View File

@@ -1,3 +1,5 @@
import { ValidEmVer } from "../emverLite/mod";
export interface Container { export interface Container {
image: string; image: string;
mounts: Record<string, string>; mounts: Record<string, string>;
@@ -5,10 +7,12 @@ export interface Container {
sigtermTimeout?: string; // if more than 30s to shutdown sigtermTimeout?: string; // if more than 30s to shutdown
} }
export type ManifestVersion = ValidEmVer;
export interface GenericManifest { export interface GenericManifest {
id: string; id: string;
title: string; title: string;
version: string; version: ManifestVersion;
releaseNotes: string; releaseNotes: string;
license: string; // name of license license: string; // name of license
replaces: string[]; replaces: string[];

View File

@@ -1,8 +1,9 @@
import { GenericManifest } from "./ManifestTypes"; import { GenericManifest, ManifestVersion } from "./ManifestTypes";
export function setupManifest< export function setupManifest<
M extends GenericManifest & { id: Id }, M extends GenericManifest & { id: Id; version: Version },
Id extends string, Id extends string,
Version extends ManifestVersion,
>(manifest: M): M { >(manifest: M): M {
return manifest; return manifest;
} }

View 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);
}
}

View 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"[]

View File

@@ -1,4 +1,5 @@
import { ExpectedExports, Properties } from "../types"; import { ExpectedExports, Properties } from "../types";
import { Utils, utils } from "../util";
import "../util/extensions"; import "../util/extensions";
import { PropertyGroup } from "./PropertyGroup"; import { PropertyGroup } from "./PropertyGroup";
import { PropertyString } from "./PropertyString"; import { PropertyString } from "./PropertyString";
@@ -18,13 +19,17 @@ export type UnionToIntersection<T> = ((x: T) => any) extends (x: infer R) => any
* @param fn * @param fn
* @returns * @returns
*/ */
export function setupPropertiesExport( export function setupProperties<WrapperData>(
fn: ( fn: (args: {
...args: Parameters<ExpectedExports.properties> wrapperData: WrapperData;
) => void | Promise<void> | Promise<(PropertyGroup | PropertyString)[]>, }) => void | Promise<void> | Promise<(PropertyGroup | PropertyString)[]>,
): ExpectedExports.properties { ): ExpectedExports.properties {
return (async (...args) => { return (async (options) => {
const result = await fn(...args); const result = await fn(
options as {
wrapperData: WrapperData & typeof options.wrapperData;
},
);
if (result) { if (result) {
const answer: Properties = result.map((x) => x.data); const answer: Properties = result.map((x) => x.data);
return answer; return answer;

View File

@@ -1,7 +1,6 @@
export * as configTypes from "./config/configTypes"; export * as configTypes from "./config/configTypes";
import { InputSpec } from "./config/configTypes"; import { InputSpec } from "./config/configTypes";
import { DependenciesReceipt } from "./config/setupConfig"; import { DependenciesReceipt } from "./config/setupConfig";
import { ActionReceipt } from "./init";
export type ExportedAction = (options: { export type ExportedAction = (options: {
effects: Effects; effects: Effects;
@@ -350,7 +349,7 @@ export type Effects = {
* *
* @param options * @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. * Remove an action that was exported. Used problably during main or during setConfig.
*/ */

View File

@@ -17,6 +17,7 @@ export { FileHelper } from "./fileHelper";
export { getWrapperData } from "./getWrapperData"; export { getWrapperData } from "./getWrapperData";
export { deepEqual } from "./deepEqual"; export { deepEqual } from "./deepEqual";
export { deepMerge } from "./deepMerge"; export { deepMerge } from "./deepMerge";
export { once } from "./once";
/** Used to check if the file exists before hand */ /** Used to check if the file exists before hand */
export const exists = ( export const exists = (

9
lib/util/once.ts Normal file
View File

@@ -0,0 +1,9 @@
export function once<B>(fn: () => B): () => B {
let result: [B] | [] = [];
return () => {
if (!result.length) {
result = [fn()];
}
return result[0];
};
}

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "start-sdk", "name": "start-sdk",
"version": "0.4.0-lib0.charlie33", "version": "0.4.0-lib0.charlie34",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "start-sdk", "name": "start-sdk",
"version": "0.4.0-lib0.charlie33", "version": "0.4.0-lib0.charlie34",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",

View File

@@ -1,6 +1,6 @@
{ {
"name": "start-sdk", "name": "start-sdk",
"version": "0.4.0-lib0.charlie33", "version": "0.4.0-lib0.charlie34",
"description": "For making the patterns that are wanted in making services for the startOS.", "description": "For making the patterns that are wanted in making services for the startOS.",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",