mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
213 lines
6.6 KiB
TypeScript
213 lines
6.6 KiB
TypeScript
import { cloneDeep, get, has, mergeWith, omit, set } from "lodash-es";
|
|
import {
|
|
Default,
|
|
type Static,
|
|
type TObject,
|
|
getFullPathKeys,
|
|
mark,
|
|
parse,
|
|
stripMark
|
|
} from "../utils";
|
|
|
|
export type SchemaObjectOptions<Schema extends TObject> = {
|
|
onUpdate?: (config: Static<Schema>) => void | Promise<void>;
|
|
onBeforeUpdate?: (
|
|
from: Static<Schema>,
|
|
to: Static<Schema>
|
|
) => Static<Schema> | Promise<Static<Schema>>;
|
|
restrictPaths?: string[];
|
|
overwritePaths?: (RegExp | string)[];
|
|
forceParse?: boolean;
|
|
};
|
|
|
|
export class SchemaObject<Schema extends TObject> {
|
|
private readonly _default: Partial<Static<Schema>>;
|
|
private _value: Static<Schema>;
|
|
private _config: Static<Schema>;
|
|
private _restriction_bypass: boolean = false;
|
|
|
|
constructor(
|
|
private _schema: Schema,
|
|
initial?: Partial<Static<Schema>>,
|
|
private options?: SchemaObjectOptions<Schema>
|
|
) {
|
|
this._default = Default(_schema, {} as any) as any;
|
|
this._value = initial
|
|
? parse(_schema, cloneDeep(initial as any), {
|
|
forceParse: this.isForceParse(),
|
|
skipMark: this.isForceParse()
|
|
})
|
|
: this._default;
|
|
this._config = Object.freeze(this._value);
|
|
}
|
|
|
|
protected isForceParse(): boolean {
|
|
return this.options?.forceParse ?? true;
|
|
}
|
|
|
|
default(): Static<Schema> {
|
|
return this._default;
|
|
}
|
|
|
|
private async onBeforeUpdate(from: Static<Schema>, to: Static<Schema>): Promise<Static<Schema>> {
|
|
if (this.options?.onBeforeUpdate) {
|
|
return this.options.onBeforeUpdate(from, to);
|
|
}
|
|
return to;
|
|
}
|
|
|
|
get(options?: { stripMark?: boolean }): Static<Schema> {
|
|
if (options?.stripMark) {
|
|
return stripMark(this._config);
|
|
}
|
|
|
|
return this._config;
|
|
}
|
|
|
|
async set(config: Static<Schema>, noEmit?: boolean): Promise<Static<Schema>> {
|
|
const valid = parse(this._schema, cloneDeep(config) as any, {
|
|
forceParse: true,
|
|
skipMark: this.isForceParse()
|
|
});
|
|
const updatedConfig = noEmit ? valid : await this.onBeforeUpdate(this._config, valid);
|
|
|
|
this._value = updatedConfig;
|
|
this._config = Object.freeze(updatedConfig);
|
|
|
|
if (noEmit !== true) {
|
|
await this.options?.onUpdate?.(this._config);
|
|
}
|
|
|
|
return this._config;
|
|
}
|
|
|
|
bypass() {
|
|
this._restriction_bypass = true;
|
|
return this;
|
|
}
|
|
|
|
throwIfRestricted(object: object): void;
|
|
throwIfRestricted(path: string): void;
|
|
throwIfRestricted(pathOrObject: string | object): void {
|
|
// only bypass once
|
|
if (this._restriction_bypass) {
|
|
this._restriction_bypass = false;
|
|
return;
|
|
}
|
|
|
|
const paths = this.options?.restrictPaths ?? [];
|
|
if (Array.isArray(paths) && paths.length > 0) {
|
|
for (const path of paths) {
|
|
const restricted =
|
|
typeof pathOrObject === "string"
|
|
? pathOrObject.startsWith(path)
|
|
: has(pathOrObject, path);
|
|
|
|
if (restricted) {
|
|
throw new Error(`Path "${path}" is restricted`);
|
|
}
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
async patch(path: string, value: any): Promise<[Partial<Static<Schema>>, Static<Schema>]> {
|
|
const current = cloneDeep(this._config);
|
|
const partial = path.length > 0 ? (set({}, path, value) as Partial<Static<Schema>>) : value;
|
|
|
|
this.throwIfRestricted(partial);
|
|
//console.log(getFullPathKeys(value).map((k) => path + "." + k));
|
|
|
|
// overwrite arrays and primitives, only deep merge objects
|
|
// @ts-ignore
|
|
const config = mergeWith(current, partial, (objValue, srcValue) => {
|
|
if (Array.isArray(objValue) && Array.isArray(srcValue)) {
|
|
return srcValue;
|
|
}
|
|
});
|
|
|
|
//console.log("overwritePaths", this.options?.overwritePaths);
|
|
if (this.options?.overwritePaths) {
|
|
const keys = getFullPathKeys(value).map((k) => path + "." + k);
|
|
const overwritePaths = keys.filter((k) => {
|
|
return this.options?.overwritePaths?.some((p) => {
|
|
if (typeof p === "string") {
|
|
return k === p;
|
|
} else {
|
|
return p.test(k);
|
|
}
|
|
});
|
|
});
|
|
//console.log("overwritePaths", keys, overwritePaths);
|
|
|
|
if (overwritePaths.length > 0) {
|
|
// filter out less specific paths (but only if more than 1)
|
|
const specific =
|
|
overwritePaths.length > 1
|
|
? overwritePaths.filter((k) =>
|
|
overwritePaths.some((k2) => {
|
|
//console.log("keep?", { k, k2 }, k2 !== k && k2.startsWith(k));
|
|
return k2 !== k && k2.startsWith(k);
|
|
})
|
|
)
|
|
: overwritePaths;
|
|
//console.log("specific", specific);
|
|
|
|
for (const p of specific) {
|
|
set(config, p, get(partial, p));
|
|
}
|
|
}
|
|
}
|
|
|
|
//console.log("patch", { path, value, partial, config, current });
|
|
|
|
const newConfig = await this.set(config);
|
|
return [partial, newConfig];
|
|
}
|
|
|
|
async overwrite(path: string, value: any): Promise<[Partial<Static<Schema>>, Static<Schema>]> {
|
|
const current = cloneDeep(this._config);
|
|
const partial = path.length > 0 ? (set({}, path, value) as Partial<Static<Schema>>) : value;
|
|
|
|
this.throwIfRestricted(partial);
|
|
//console.log(getFullPathKeys(value).map((k) => path + "." + k));
|
|
|
|
// overwrite arrays and primitives, only deep merge objects
|
|
// @ts-ignore
|
|
const config = set(current, path, value);
|
|
|
|
//console.log("overwrite", { path, value, partial, config, current });
|
|
|
|
const newConfig = await this.set(config);
|
|
return [partial, newConfig];
|
|
}
|
|
|
|
has(path: string): boolean {
|
|
const p = path.split(".");
|
|
if (p.length > 1) {
|
|
const parent = p.slice(0, -1).join(".");
|
|
if (!has(this._config, parent)) {
|
|
console.log("parent", parent, JSON.stringify(this._config, null, 2));
|
|
throw new Error(`Parent path "${parent}" does not exist`);
|
|
}
|
|
}
|
|
|
|
return has(this._config, path);
|
|
}
|
|
|
|
async remove(path: string): Promise<[Partial<Static<Schema>>, Static<Schema>]> {
|
|
this.throwIfRestricted(path);
|
|
|
|
if (!this.has(path)) {
|
|
throw new Error(`Path "${path}" does not exist`);
|
|
}
|
|
|
|
const current = cloneDeep(this._config);
|
|
const removed = get(current, path) as Partial<Static<Schema>>;
|
|
const config = omit(current, path);
|
|
const newConfig = await this.set(config);
|
|
return [removed, newConfig];
|
|
}
|
|
}
|