mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-17 04:46:05 +00:00
public commit
This commit is contained in:
36
app/src/core/utils/DebugLogger.ts
Normal file
36
app/src/core/utils/DebugLogger.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
export class DebugLogger {
|
||||
public _context: string[] = [];
|
||||
_enabled: boolean = true;
|
||||
private readonly id = Math.random().toString(36).substr(2, 9);
|
||||
private last: number = 0;
|
||||
|
||||
constructor(enabled: boolean = true) {
|
||||
this._enabled = enabled;
|
||||
}
|
||||
|
||||
context(context: string) {
|
||||
//console.log("[ settings context ]", context, this._context);
|
||||
this._context.push(context);
|
||||
return this;
|
||||
}
|
||||
|
||||
clear() {
|
||||
//console.log("[ clear context ]", this._context.pop(), this._context);
|
||||
this._context.pop();
|
||||
return this;
|
||||
}
|
||||
|
||||
log(...args: any[]) {
|
||||
if (!this._enabled) return this;
|
||||
|
||||
const now = performance.now();
|
||||
const time = Number.parseInt(String(now - this.last));
|
||||
const indents = " ".repeat(this._context.length);
|
||||
const context =
|
||||
this._context.length > 0 ? `[${this._context[this._context.length - 1]}]` : "";
|
||||
console.log(indents, context, time, ...args);
|
||||
|
||||
this.last = now;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
20
app/src/core/utils/browser.ts
Normal file
20
app/src/core/utils/browser.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export type TBrowser = "Opera" | "Edge" | "Chrome" | "Safari" | "Firefox" | "IE" | "unknown";
|
||||
export function getBrowser(): TBrowser {
|
||||
if ((navigator.userAgent.indexOf("Opera") || navigator.userAgent.indexOf("OPR")) !== -1) {
|
||||
return "Opera";
|
||||
} else if (navigator.userAgent.indexOf("Edg") !== -1) {
|
||||
return "Edge";
|
||||
} else if (navigator.userAgent.indexOf("Chrome") !== -1) {
|
||||
return "Chrome";
|
||||
} else if (navigator.userAgent.indexOf("Safari") !== -1) {
|
||||
return "Safari";
|
||||
} else if (navigator.userAgent.indexOf("Firefox") !== -1) {
|
||||
return "Firefox";
|
||||
// @ts-ignore
|
||||
} else if (navigator.userAgent.indexOf("MSIE") !== -1 || !!document.documentMode === true) {
|
||||
//IF IE > 10
|
||||
return "IE";
|
||||
} else {
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
29
app/src/core/utils/crypto.ts
Normal file
29
app/src/core/utils/crypto.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export const HashAlgorithms = ["SHA-1", "SHA-256", "SHA-384", "SHA-512"] as const;
|
||||
export type HashAlgorithm = (typeof HashAlgorithms)[number];
|
||||
export async function digest(alg: HashAlgorithm, input: string, salt?: string, pepper?: string) {
|
||||
if (!HashAlgorithms.includes(alg)) {
|
||||
throw new Error(`Invalid hash algorithm: ${alg}`);
|
||||
}
|
||||
|
||||
// convert to Uint8Array
|
||||
const data = new TextEncoder().encode((salt ?? "") + input + (pepper ?? ""));
|
||||
|
||||
// hash to alg
|
||||
const hashBuffer = await crypto.subtle.digest(alg, data);
|
||||
|
||||
// convert hash to hex string
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
return hashArray.map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
export const hash = {
|
||||
sha256: async (input: string, salt?: string, pepper?: string) =>
|
||||
digest("SHA-256", input, salt, pepper),
|
||||
sha1: async (input: string, salt?: string, pepper?: string) =>
|
||||
digest("SHA-1", input, salt, pepper)
|
||||
};
|
||||
|
||||
export async function checksum(s: any) {
|
||||
const o = typeof s === "string" ? s : JSON.stringify(s);
|
||||
return await digest("SHA-1", o);
|
||||
}
|
||||
14
app/src/core/utils/dates.ts
Normal file
14
app/src/core/utils/dates.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import dayjs from "dayjs";
|
||||
import weekOfYear from "dayjs/plugin/weekOfYear.js";
|
||||
|
||||
declare module "dayjs" {
|
||||
interface Dayjs {
|
||||
week(): number;
|
||||
|
||||
week(value: number): dayjs.Dayjs;
|
||||
}
|
||||
}
|
||||
|
||||
dayjs.extend(weekOfYear);
|
||||
|
||||
export { dayjs };
|
||||
13
app/src/core/utils/index.ts
Normal file
13
app/src/core/utils/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export * from "./browser";
|
||||
export * from "./objects";
|
||||
export * from "./strings";
|
||||
export * from "./perf";
|
||||
export * from "./reqres";
|
||||
export * from "./xml";
|
||||
export type { Prettify, PrettifyRec } from "./types";
|
||||
export * from "./typebox";
|
||||
export * from "./dates";
|
||||
export * from "./crypto";
|
||||
export * from "./uuid";
|
||||
export { FromSchema } from "./typebox/from-schema";
|
||||
export * from "./test";
|
||||
198
app/src/core/utils/objects.ts
Normal file
198
app/src/core/utils/objects.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import { pascalToKebab } from "./strings";
|
||||
|
||||
export function _jsonp(obj: any, space = 2): string {
|
||||
return JSON.stringify(obj, null, space);
|
||||
}
|
||||
|
||||
export function safelyParseObjectValues<T extends { [key: string]: any }>(obj: T): T {
|
||||
return Object.entries(obj).reduce((acc, [key, value]) => {
|
||||
try {
|
||||
// @ts-ignore
|
||||
acc[key] = JSON.parse(value);
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {} as T);
|
||||
}
|
||||
|
||||
export function keepChanged<T extends object>(origin: T, updated: T): Partial<T> {
|
||||
return Object.keys(updated).reduce(
|
||||
(acc, key) => {
|
||||
if (updated[key] !== origin[key]) {
|
||||
acc[key] = updated[key];
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Partial<T>
|
||||
);
|
||||
}
|
||||
|
||||
export function objectKeysPascalToKebab(obj: any, ignoreKeys: string[] = []): any {
|
||||
if (obj === null || typeof obj !== "object") {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => objectKeysPascalToKebab(item, ignoreKeys));
|
||||
}
|
||||
|
||||
return Object.keys(obj).reduce(
|
||||
(acc, key) => {
|
||||
const kebabKey = ignoreKeys.includes(key) ? key : pascalToKebab(key);
|
||||
acc[kebabKey] = objectKeysPascalToKebab(obj[key], ignoreKeys);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>
|
||||
);
|
||||
}
|
||||
|
||||
export function filterKeys<Object extends { [key: string]: any }>(
|
||||
obj: Object,
|
||||
keysToFilter: string[]
|
||||
): Object {
|
||||
const result = {} as Object;
|
||||
|
||||
for (const key in obj) {
|
||||
const shouldFilter = keysToFilter.some((filterKey) => key.includes(filterKey));
|
||||
if (!shouldFilter) {
|
||||
if (typeof obj[key] === "object" && obj[key] !== null && !Array.isArray(obj[key])) {
|
||||
result[key] = filterKeys(obj[key], keysToFilter);
|
||||
} else {
|
||||
result[key] = obj[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function transformObject<T extends Record<string, any>, U>(
|
||||
object: T,
|
||||
transform: (value: T[keyof T], key: keyof T) => U | undefined
|
||||
): { [K in keyof T]: U } {
|
||||
return Object.entries(object).reduce(
|
||||
(acc, [key, value]) => {
|
||||
const t = transform(value, key as keyof T);
|
||||
if (typeof t !== "undefined") {
|
||||
acc[key as keyof T] = t;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as { [K in keyof T]: U }
|
||||
);
|
||||
}
|
||||
export const objectTransform = transformObject;
|
||||
|
||||
export function objectEach<T extends Record<string, any>, U>(
|
||||
object: T,
|
||||
each: (value: T[keyof T], key: keyof T) => U
|
||||
): void {
|
||||
Object.entries(object).forEach(
|
||||
([key, value]) => {
|
||||
each(value, key);
|
||||
},
|
||||
{} as { [K in keyof T]: U }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple object check.
|
||||
* @param item
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isObject(item) {
|
||||
return item && typeof item === "object" && !Array.isArray(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep merge two objects.
|
||||
* @param target
|
||||
* @param ...sources
|
||||
*/
|
||||
export function mergeDeep(target, ...sources) {
|
||||
if (!sources.length) return target;
|
||||
const source = sources.shift();
|
||||
|
||||
if (isObject(target) && isObject(source)) {
|
||||
for (const key in source) {
|
||||
if (isObject(source[key])) {
|
||||
if (!target[key]) Object.assign(target, { [key]: {} });
|
||||
mergeDeep(target[key], source[key]);
|
||||
} else {
|
||||
Object.assign(target, { [key]: source[key] });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mergeDeep(target, ...sources);
|
||||
}
|
||||
|
||||
export function getFullPathKeys(obj: any, parentPath: string = ""): string[] {
|
||||
let keys: string[] = [];
|
||||
|
||||
for (const key in obj) {
|
||||
const fullPath = parentPath ? `${parentPath}.${key}` : key;
|
||||
keys.push(fullPath);
|
||||
|
||||
if (typeof obj[key] === "object" && obj[key] !== null) {
|
||||
keys = keys.concat(getFullPathKeys(obj[key], fullPath));
|
||||
}
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
export function flattenObject(obj: any, parentKey = "", result: any = {}): any {
|
||||
for (const key in obj) {
|
||||
if (key in obj) {
|
||||
const newKey = parentKey ? `${parentKey}.${key}` : key;
|
||||
if (typeof obj[key] === "object" && obj[key] !== null && !Array.isArray(obj[key])) {
|
||||
flattenObject(obj[key], newKey, result);
|
||||
} else if (Array.isArray(obj[key])) {
|
||||
obj[key].forEach((item, index) => {
|
||||
const arrayKey = `${newKey}.${index}`;
|
||||
if (typeof item === "object" && item !== null) {
|
||||
flattenObject(item, arrayKey, result);
|
||||
} else {
|
||||
result[arrayKey] = item;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
result[newKey] = obj[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function objectDepth(object: object): number {
|
||||
let level = 1;
|
||||
for (const key in object) {
|
||||
if (typeof object[key] === "object") {
|
||||
const depth = objectDepth(object[key]) + 1;
|
||||
level = Math.max(depth, level);
|
||||
}
|
||||
}
|
||||
return level;
|
||||
}
|
||||
|
||||
export function objectCleanEmpty<Obj extends { [key: string]: any }>(obj: Obj): Obj {
|
||||
return Object.entries(obj).reduce((acc, [key, value]) => {
|
||||
if (value && Array.isArray(value) && value.some((v) => typeof v === "object")) {
|
||||
const nested = value.map(objectCleanEmpty);
|
||||
if (nested.length > 0) {
|
||||
acc[key] = nested;
|
||||
}
|
||||
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
||||
const nested = objectCleanEmpty(value);
|
||||
if (Object.keys(nested).length > 0) {
|
||||
acc[key] = nested;
|
||||
}
|
||||
} else if (value !== "" && value !== null && value !== undefined) {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {} as any);
|
||||
}
|
||||
60
app/src/core/utils/perf.ts
Normal file
60
app/src/core/utils/perf.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
export class Perf {
|
||||
private marks: { mark: string; time: number }[] = [];
|
||||
private startTime: number;
|
||||
private endTime: number | null = null;
|
||||
|
||||
private constructor() {
|
||||
this.startTime = performance.now();
|
||||
}
|
||||
|
||||
static start(): Perf {
|
||||
return new Perf();
|
||||
}
|
||||
|
||||
mark(markName: string): void {
|
||||
if (this.endTime !== null) {
|
||||
throw new Error("Cannot add marks after perf measurement has been closed.");
|
||||
}
|
||||
|
||||
const currentTime = performance.now();
|
||||
const lastMarkTime =
|
||||
this.marks.length > 0 ? this.marks[this.marks.length - 1]!.time : this.startTime;
|
||||
const elapsedTimeSinceLastMark = currentTime - lastMarkTime;
|
||||
|
||||
this.marks.push({ mark: markName, time: elapsedTimeSinceLastMark });
|
||||
}
|
||||
|
||||
close(): void {
|
||||
if (this.endTime !== null) {
|
||||
throw new Error("Perf measurement has already been closed.");
|
||||
}
|
||||
this.endTime = performance.now();
|
||||
}
|
||||
|
||||
result(): { total: number; marks: { mark: string; time: number }[] } {
|
||||
if (this.endTime === null) {
|
||||
throw new Error("Perf measurement has not been closed yet.");
|
||||
}
|
||||
|
||||
const totalTime = this.endTime - this.startTime;
|
||||
return {
|
||||
total: Number.parseFloat(totalTime.toFixed(2)),
|
||||
marks: this.marks.map((mark) => ({
|
||||
mark: mark.mark,
|
||||
time: Number.parseFloat(mark.time.toFixed(2)),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
static async execute(fn: () => Promise<any>, times: number = 1): Promise<any> {
|
||||
const perf = Perf.start();
|
||||
|
||||
for (let i = 0; i < times; i++) {
|
||||
await fn();
|
||||
perf.mark(`iteration-${i}`);
|
||||
}
|
||||
|
||||
perf.close();
|
||||
return perf.result();
|
||||
}
|
||||
}
|
||||
84
app/src/core/utils/reqres.ts
Normal file
84
app/src/core/utils/reqres.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
export function headersToObject(headers: Headers): Record<string, string> {
|
||||
if (!headers) return {};
|
||||
return { ...Object.fromEntries(headers.entries()) };
|
||||
}
|
||||
|
||||
export function pickHeaders(headers: Headers, keys: string[]): Record<string, string> {
|
||||
const obj = headersToObject(headers);
|
||||
const res = {};
|
||||
for (const key of keys) {
|
||||
if (obj[key]) {
|
||||
res[key] = obj[key];
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export const replaceUrlParam = (urlString: string, params: Record<string, string>) => {
|
||||
let newString = urlString;
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
const reg = new RegExp(`/:${k}(?:{[^/]+})?`);
|
||||
newString = newString.replace(reg, `/${v}`);
|
||||
}
|
||||
return newString;
|
||||
};
|
||||
|
||||
export function encodeSearch(obj, options?: { prefix?: string; encode?: boolean }) {
|
||||
let str = "";
|
||||
function _encode(str) {
|
||||
return options?.encode ? encodeURIComponent(str) : str;
|
||||
}
|
||||
|
||||
for (const k in obj) {
|
||||
let tmp = obj[k];
|
||||
if (tmp !== void 0) {
|
||||
if (Array.isArray(tmp)) {
|
||||
for (let i = 0; i < tmp.length; i++) {
|
||||
if (str.length > 0) str += "&";
|
||||
str += `${_encode(k)}=${_encode(tmp[i])}`;
|
||||
}
|
||||
} else {
|
||||
if (typeof tmp === "object") {
|
||||
tmp = JSON.stringify(tmp);
|
||||
}
|
||||
|
||||
if (str.length > 0) str += "&";
|
||||
str += `${_encode(k)}=${_encode(tmp)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (options?.prefix || "") + str;
|
||||
}
|
||||
|
||||
export function decodeSearch(str) {
|
||||
function toValue(mix) {
|
||||
if (!mix) return "";
|
||||
const str = decodeURIComponent(mix);
|
||||
if (str === "false") return false;
|
||||
if (str === "true") return true;
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch (e) {
|
||||
return +str * 0 === 0 ? +str : str;
|
||||
}
|
||||
}
|
||||
|
||||
let tmp: any;
|
||||
let k: string;
|
||||
const out = {};
|
||||
const arr = str.split("&");
|
||||
|
||||
// biome-ignore lint/suspicious/noAssignInExpressions: <explanation>
|
||||
while ((tmp = arr.shift())) {
|
||||
tmp = tmp.split("=");
|
||||
k = tmp.shift();
|
||||
if (out[k] !== void 0) {
|
||||
out[k] = [].concat(out[k], toValue(tmp.shift()));
|
||||
} else {
|
||||
out[k] = toValue(tmp.shift());
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
9
app/src/core/utils/sql.ts
Normal file
9
app/src/core/utils/sql.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { isDebug } from "../env";
|
||||
|
||||
export async function formatSql(sql: string): Promise<string> {
|
||||
if (isDebug()) {
|
||||
const { format } = await import("sql-formatter");
|
||||
return format(sql);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
62
app/src/core/utils/strings.ts
Normal file
62
app/src/core/utils/strings.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
export function objectToKeyValueArray<T extends Record<string, any>>(obj: T) {
|
||||
return Object.keys(obj).map((key) => ({ key, value: obj[key as keyof T] }));
|
||||
}
|
||||
|
||||
export function ucFirst(str: string): string {
|
||||
if (!str || str.length === 0) return str;
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
export function ucFirstAll(str: string, split: string = " "): string {
|
||||
if (!str || str.length === 0) return str;
|
||||
return str
|
||||
.split(split)
|
||||
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
||||
.join(split);
|
||||
}
|
||||
|
||||
export function ucFirstAllSnakeToPascalWithSpaces(str: string, split: string = " "): string {
|
||||
return ucFirstAll(snakeToPascalWithSpaces(str), split);
|
||||
}
|
||||
|
||||
export function randomString(length: number, includeSpecial = false): string {
|
||||
const base = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
const special = "!@#$%^&*()_+{}:\"<>?|[];',./`~";
|
||||
const chars = base + (includeSpecial ? special : "");
|
||||
let result = "";
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars[Math.floor(Math.random() * chars.length)];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a string from snake_case to PascalCase with spaces
|
||||
* Example: `snake_to_pascal` -> `Snake To Pascal`
|
||||
*
|
||||
* @param str
|
||||
*/
|
||||
export function snakeToPascalWithSpaces(str: string): string {
|
||||
if (!str || str.length === 0) return str;
|
||||
|
||||
return str
|
||||
.split("_")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
export function pascalToKebab(pascalStr: string): string {
|
||||
return pascalStr.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace simple mustache like {placeholders} in a string
|
||||
*
|
||||
* @param str
|
||||
* @param vars
|
||||
*/
|
||||
export function replaceSimplePlaceholders(str: string, vars: Record<string, any>): string {
|
||||
return str.replace(/\{\$(\w+)\}/g, (match, key) => {
|
||||
return key in vars ? vars[key] : match;
|
||||
});
|
||||
}
|
||||
18
app/src/core/utils/test.ts
Normal file
18
app/src/core/utils/test.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
type ConsoleSeverity = "log" | "warn" | "error";
|
||||
const _oldConsoles = {
|
||||
log: console.log,
|
||||
warn: console.warn,
|
||||
error: console.error
|
||||
};
|
||||
|
||||
export function disableConsoleLog(severities: ConsoleSeverity[] = ["log"]) {
|
||||
severities.forEach((severity) => {
|
||||
console[severity] = () => null;
|
||||
});
|
||||
}
|
||||
|
||||
export function enableConsoleLog() {
|
||||
Object.entries(_oldConsoles).forEach(([severity, fn]) => {
|
||||
console[severity as ConsoleSeverity] = fn;
|
||||
});
|
||||
}
|
||||
268
app/src/core/utils/typebox/from-schema.ts
Normal file
268
app/src/core/utils/typebox/from-schema.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
/*--------------------------------------------------------------------------
|
||||
|
||||
@sinclair/typebox/prototypes
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017-2024 Haydn Paterson (sinclair) <haydn.developer@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
---------------------------------------------------------------------------*/
|
||||
|
||||
import * as Type from "@sinclair/typebox";
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Schematics
|
||||
// ------------------------------------------------------------------
|
||||
const IsExact = (value: unknown, expect: unknown) => value === expect;
|
||||
const IsSValue = (value: unknown): value is SValue =>
|
||||
Type.ValueGuard.IsString(value) ||
|
||||
Type.ValueGuard.IsNumber(value) ||
|
||||
Type.ValueGuard.IsBoolean(value);
|
||||
const IsSEnum = (value: unknown): value is SEnum =>
|
||||
Type.ValueGuard.IsObject(value) &&
|
||||
Type.ValueGuard.IsArray(value.enum) &&
|
||||
value.enum.every((value) => IsSValue(value));
|
||||
const IsSAllOf = (value: unknown): value is SAllOf =>
|
||||
Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsArray(value.allOf);
|
||||
const IsSAnyOf = (value: unknown): value is SAnyOf =>
|
||||
Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsArray(value.anyOf);
|
||||
const IsSOneOf = (value: unknown): value is SOneOf =>
|
||||
Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsArray(value.oneOf);
|
||||
const IsSTuple = (value: unknown): value is STuple =>
|
||||
Type.ValueGuard.IsObject(value) &&
|
||||
IsExact(value.type, "array") &&
|
||||
Type.ValueGuard.IsArray(value.items);
|
||||
const IsSArray = (value: unknown): value is SArray =>
|
||||
Type.ValueGuard.IsObject(value) &&
|
||||
IsExact(value.type, "array") &&
|
||||
!Type.ValueGuard.IsArray(value.items) &&
|
||||
Type.ValueGuard.IsObject(value.items);
|
||||
const IsSConst = (value: unknown): value is SConst =>
|
||||
Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsObject(value["const"]);
|
||||
const IsSString = (value: unknown): value is SString =>
|
||||
Type.ValueGuard.IsObject(value) && IsExact(value.type, "string");
|
||||
const IsSNumber = (value: unknown): value is SNumber =>
|
||||
Type.ValueGuard.IsObject(value) && IsExact(value.type, "number");
|
||||
const IsSInteger = (value: unknown): value is SInteger =>
|
||||
Type.ValueGuard.IsObject(value) && IsExact(value.type, "integer");
|
||||
const IsSBoolean = (value: unknown): value is SBoolean =>
|
||||
Type.ValueGuard.IsObject(value) && IsExact(value.type, "boolean");
|
||||
const IsSNull = (value: unknown): value is SBoolean =>
|
||||
Type.ValueGuard.IsObject(value) && IsExact(value.type, "null");
|
||||
const IsSProperties = (value: unknown): value is SProperties => Type.ValueGuard.IsObject(value);
|
||||
// prettier-ignore
|
||||
const IsSObject = (value: unknown): value is SObject => Type.ValueGuard.IsObject(value) && IsExact(value.type, 'object') && IsSProperties(value.properties) && (value.required === undefined || Type.ValueGuard.IsArray(value.required) && value.required.every((value: unknown) => Type.ValueGuard.IsString(value)))
|
||||
type SValue = string | number | boolean;
|
||||
type SEnum = Readonly<{ enum: readonly SValue[] }>;
|
||||
type SAllOf = Readonly<{ allOf: readonly unknown[] }>;
|
||||
type SAnyOf = Readonly<{ anyOf: readonly unknown[] }>;
|
||||
type SOneOf = Readonly<{ oneOf: readonly unknown[] }>;
|
||||
type SProperties = Record<PropertyKey, unknown>;
|
||||
type SObject = Readonly<{ type: "object"; properties: SProperties; required?: readonly string[] }>;
|
||||
type STuple = Readonly<{ type: "array"; items: readonly unknown[] }>;
|
||||
type SArray = Readonly<{ type: "array"; items: unknown }>;
|
||||
type SConst = Readonly<{ const: SValue }>;
|
||||
type SString = Readonly<{ type: "string" }>;
|
||||
type SNumber = Readonly<{ type: "number" }>;
|
||||
type SInteger = Readonly<{ type: "integer" }>;
|
||||
type SBoolean = Readonly<{ type: "boolean" }>;
|
||||
type SNull = Readonly<{ type: "null" }>;
|
||||
// ------------------------------------------------------------------
|
||||
// FromRest
|
||||
// ------------------------------------------------------------------
|
||||
// prettier-ignore
|
||||
type TFromRest<T extends readonly unknown[], Acc extends Type.TSchema[] = []> = (
|
||||
T extends readonly [infer L extends unknown, ...infer R extends unknown[]]
|
||||
? TFromSchema<L> extends infer S extends Type.TSchema
|
||||
? TFromRest<R, [...Acc, S]>
|
||||
: TFromRest<R, [...Acc]>
|
||||
: Acc
|
||||
)
|
||||
function FromRest<T extends readonly unknown[]>(T: T): TFromRest<T> {
|
||||
return T.map((L) => FromSchema(L)) as never;
|
||||
}
|
||||
// ------------------------------------------------------------------
|
||||
// FromEnumRest
|
||||
// ------------------------------------------------------------------
|
||||
// prettier-ignore
|
||||
type TFromEnumRest<T extends readonly SValue[], Acc extends Type.TSchema[] = []> = (
|
||||
T extends readonly [infer L extends SValue, ...infer R extends SValue[]]
|
||||
? TFromEnumRest<R, [...Acc, Type.TLiteral<L>]>
|
||||
: Acc
|
||||
)
|
||||
function FromEnumRest<T extends readonly SValue[]>(T: T): TFromEnumRest<T> {
|
||||
return T.map((L) => Type.Literal(L)) as never;
|
||||
}
|
||||
// ------------------------------------------------------------------
|
||||
// AllOf
|
||||
// ------------------------------------------------------------------
|
||||
// prettier-ignore
|
||||
type TFromAllOf<T extends SAllOf> = (
|
||||
TFromRest<T['allOf']> extends infer Rest extends Type.TSchema[]
|
||||
? Type.TIntersectEvaluated<Rest>
|
||||
: Type.TNever
|
||||
)
|
||||
function FromAllOf<T extends SAllOf>(T: T): TFromAllOf<T> {
|
||||
return Type.IntersectEvaluated(FromRest(T.allOf), T);
|
||||
}
|
||||
// ------------------------------------------------------------------
|
||||
// AnyOf
|
||||
// ------------------------------------------------------------------
|
||||
// prettier-ignore
|
||||
type TFromAnyOf<T extends SAnyOf> = (
|
||||
TFromRest<T['anyOf']> extends infer Rest extends Type.TSchema[]
|
||||
? Type.TUnionEvaluated<Rest>
|
||||
: Type.TNever
|
||||
)
|
||||
function FromAnyOf<T extends SAnyOf>(T: T): TFromAnyOf<T> {
|
||||
return Type.UnionEvaluated(FromRest(T.anyOf), T);
|
||||
}
|
||||
// ------------------------------------------------------------------
|
||||
// OneOf
|
||||
// ------------------------------------------------------------------
|
||||
// prettier-ignore
|
||||
type TFromOneOf<T extends SOneOf> = (
|
||||
TFromRest<T['oneOf']> extends infer Rest extends Type.TSchema[]
|
||||
? Type.TUnionEvaluated<Rest>
|
||||
: Type.TNever
|
||||
)
|
||||
function FromOneOf<T extends SOneOf>(T: T): TFromOneOf<T> {
|
||||
return Type.UnionEvaluated(FromRest(T.oneOf), T);
|
||||
}
|
||||
// ------------------------------------------------------------------
|
||||
// Enum
|
||||
// ------------------------------------------------------------------
|
||||
// prettier-ignore
|
||||
type TFromEnum<T extends SEnum> = (
|
||||
TFromEnumRest<T['enum']> extends infer Elements extends Type.TSchema[]
|
||||
? Type.TUnionEvaluated<Elements>
|
||||
: Type.TNever
|
||||
)
|
||||
function FromEnum<T extends SEnum>(T: T): TFromEnum<T> {
|
||||
return Type.UnionEvaluated(FromEnumRest(T.enum));
|
||||
}
|
||||
// ------------------------------------------------------------------
|
||||
// Tuple
|
||||
// ------------------------------------------------------------------
|
||||
// prettier-ignore
|
||||
type TFromTuple<T extends STuple> = (
|
||||
TFromRest<T['items']> extends infer Elements extends Type.TSchema[]
|
||||
? Type.TTuple<Elements>
|
||||
: Type.TTuple<[]>
|
||||
)
|
||||
// prettier-ignore
|
||||
function FromTuple<T extends STuple>(T: T): TFromTuple<T> {
|
||||
return Type.Tuple(FromRest(T.items), T) as never
|
||||
}
|
||||
// ------------------------------------------------------------------
|
||||
// Array
|
||||
// ------------------------------------------------------------------
|
||||
// prettier-ignore
|
||||
type TFromArray<T extends SArray> = (
|
||||
TFromSchema<T['items']> extends infer Items extends Type.TSchema
|
||||
? Type.TArray<Items>
|
||||
: Type.TArray<Type.TUnknown>
|
||||
)
|
||||
// prettier-ignore
|
||||
function FromArray<T extends SArray>(T: T): TFromArray<T> {
|
||||
return Type.Array(FromSchema(T.items), T) as never
|
||||
}
|
||||
// ------------------------------------------------------------------
|
||||
// Const
|
||||
// ------------------------------------------------------------------
|
||||
// prettier-ignore
|
||||
type TFromConst<T extends SConst> = (
|
||||
Type.Ensure<Type.TLiteral<T['const']>>
|
||||
)
|
||||
function FromConst<T extends SConst>(T: T) {
|
||||
return Type.Literal(T.const, T);
|
||||
}
|
||||
// ------------------------------------------------------------------
|
||||
// Object
|
||||
// ------------------------------------------------------------------
|
||||
type TFromPropertiesIsOptional<
|
||||
K extends PropertyKey,
|
||||
R extends string | unknown,
|
||||
> = unknown extends R ? true : K extends R ? false : true;
|
||||
// prettier-ignore
|
||||
type TFromProperties<T extends SProperties, R extends string | unknown> = Type.Evaluate<{
|
||||
-readonly [K in keyof T]: TFromPropertiesIsOptional<K, R> extends true
|
||||
? Type.TOptional<TFromSchema<T[K]>>
|
||||
: TFromSchema<T[K]>
|
||||
}>
|
||||
// prettier-ignore
|
||||
type TFromObject<T extends SObject> = (
|
||||
TFromProperties<T['properties'], Exclude<T['required'], undefined>[number]> extends infer Properties extends Type.TProperties
|
||||
? Type.TObject<Properties>
|
||||
: Type.TObject<{}>
|
||||
)
|
||||
function FromObject<T extends SObject>(T: T): TFromObject<T> {
|
||||
const properties = globalThis.Object.getOwnPropertyNames(T.properties).reduce((Acc, K) => {
|
||||
return {
|
||||
...Acc,
|
||||
[K]:
|
||||
T.required && T.required.includes(K)
|
||||
? FromSchema(T.properties[K])
|
||||
: Type.Optional(FromSchema(T.properties[K])),
|
||||
};
|
||||
}, {} as Type.TProperties);
|
||||
return Type.Object(properties, T) as never;
|
||||
}
|
||||
// ------------------------------------------------------------------
|
||||
// FromSchema
|
||||
// ------------------------------------------------------------------
|
||||
// prettier-ignore
|
||||
export type TFromSchema<T> = (
|
||||
T extends SAllOf ? TFromAllOf<T> :
|
||||
T extends SAnyOf ? TFromAnyOf<T> :
|
||||
T extends SOneOf ? TFromOneOf<T> :
|
||||
T extends SEnum ? TFromEnum<T> :
|
||||
T extends SObject ? TFromObject<T> :
|
||||
T extends STuple ? TFromTuple<T> :
|
||||
T extends SArray ? TFromArray<T> :
|
||||
T extends SConst ? TFromConst<T> :
|
||||
T extends SString ? Type.TString :
|
||||
T extends SNumber ? Type.TNumber :
|
||||
T extends SInteger ? Type.TInteger :
|
||||
T extends SBoolean ? Type.TBoolean :
|
||||
T extends SNull ? Type.TNull :
|
||||
Type.TUnknown
|
||||
)
|
||||
/** Parses a TypeBox type from raw JsonSchema */
|
||||
export function FromSchema<T>(T: T): TFromSchema<T> {
|
||||
// prettier-ignore
|
||||
return (
|
||||
IsSAllOf(T) ? FromAllOf(T) :
|
||||
IsSAnyOf(T) ? FromAnyOf(T) :
|
||||
IsSOneOf(T) ? FromOneOf(T) :
|
||||
IsSEnum(T) ? FromEnum(T) :
|
||||
IsSObject(T) ? FromObject(T) :
|
||||
IsSTuple(T) ? FromTuple(T) :
|
||||
IsSArray(T) ? FromArray(T) :
|
||||
IsSConst(T) ? FromConst(T) :
|
||||
IsSString(T) ? Type.String(T) :
|
||||
IsSNumber(T) ? Type.Number(T) :
|
||||
IsSInteger(T) ? Type.Integer(T) :
|
||||
IsSBoolean(T) ? Type.Boolean(T) :
|
||||
IsSNull(T) ? Type.Null(T) :
|
||||
Type.Unknown(T || {})
|
||||
) as never
|
||||
}
|
||||
206
app/src/core/utils/typebox/index.ts
Normal file
206
app/src/core/utils/typebox/index.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import {
|
||||
Kind,
|
||||
type ObjectOptions,
|
||||
type SchemaOptions,
|
||||
type Static,
|
||||
type StaticDecode,
|
||||
type StringOptions,
|
||||
type TLiteral,
|
||||
type TLiteralValue,
|
||||
type TObject,
|
||||
type TRecord,
|
||||
type TSchema,
|
||||
type TString,
|
||||
Type,
|
||||
TypeRegistry
|
||||
} from "@sinclair/typebox";
|
||||
import {
|
||||
DefaultErrorFunction,
|
||||
Errors,
|
||||
SetErrorFunction,
|
||||
type ValueErrorIterator
|
||||
} from "@sinclair/typebox/errors";
|
||||
import { Check, Default, Value, type ValueError } from "@sinclair/typebox/value";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
|
||||
export type RecursivePartial<T> = {
|
||||
[P in keyof T]?: T[P] extends (infer U)[]
|
||||
? RecursivePartial<U>[]
|
||||
: T[P] extends object | undefined
|
||||
? RecursivePartial<T[P]>
|
||||
: T[P];
|
||||
};
|
||||
|
||||
type ParseOptions = {
|
||||
useDefaults?: boolean;
|
||||
decode?: boolean;
|
||||
onError?: (errors: ValueErrorIterator) => void;
|
||||
forceParse?: boolean;
|
||||
skipMark?: boolean;
|
||||
};
|
||||
|
||||
const validationSymbol = Symbol("tb-parse-validation");
|
||||
|
||||
export class TypeInvalidError extends Error {
|
||||
errors: ValueError[];
|
||||
constructor(
|
||||
public schema: TSchema,
|
||||
public data: unknown,
|
||||
message?: string
|
||||
) {
|
||||
//console.warn("errored schema", JSON.stringify(schema, null, 2));
|
||||
super(message ?? `Invalid: ${JSON.stringify(data)}`);
|
||||
this.errors = [...Errors(schema, data)];
|
||||
}
|
||||
|
||||
first() {
|
||||
return this.errors[0]!;
|
||||
}
|
||||
|
||||
firstToString() {
|
||||
const first = this.first();
|
||||
return `${first.message} at "${first.path}"`;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
message: this.message,
|
||||
schema: this.schema,
|
||||
data: this.data,
|
||||
errors: this.errors
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function stripMark(obj: any) {
|
||||
const newObj = cloneDeep(obj);
|
||||
mark(newObj, false);
|
||||
return newObj;
|
||||
}
|
||||
|
||||
export function mark(obj: any, validated = true) {
|
||||
if (typeof obj === "object" && obj !== null && !Array.isArray(obj)) {
|
||||
if (validated) {
|
||||
obj[validationSymbol] = true;
|
||||
} else {
|
||||
delete obj[validationSymbol];
|
||||
}
|
||||
for (const key in obj) {
|
||||
if (typeof obj[key] === "object" && obj[key] !== null) {
|
||||
mark(obj[key], validated);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function parse<Schema extends TSchema = TSchema>(
|
||||
schema: Schema,
|
||||
data: RecursivePartial<Static<Schema>>,
|
||||
options?: ParseOptions
|
||||
): Static<Schema> {
|
||||
if (!options?.forceParse && typeof data === "object" && validationSymbol in data) {
|
||||
if (options?.useDefaults === false) {
|
||||
return data as Static<typeof schema>;
|
||||
}
|
||||
|
||||
// this is important as defaults are expected
|
||||
return Default(schema, data as any) as Static<Schema>;
|
||||
}
|
||||
|
||||
const parsed = options?.useDefaults === false ? data : Default(schema, data);
|
||||
|
||||
if (Check(schema, parsed)) {
|
||||
options?.skipMark !== true && mark(parsed, true);
|
||||
return parsed as Static<typeof schema>;
|
||||
} else if (options?.onError) {
|
||||
options.onError(Errors(schema, data));
|
||||
} else {
|
||||
throw new TypeInvalidError(schema, data);
|
||||
}
|
||||
|
||||
// @todo: check this
|
||||
return undefined as any;
|
||||
}
|
||||
|
||||
export function parseDecode<Schema extends TSchema = TSchema>(
|
||||
schema: Schema,
|
||||
data: RecursivePartial<StaticDecode<Schema>>
|
||||
): StaticDecode<Schema> {
|
||||
//console.log("parseDecode", schema, data);
|
||||
const parsed = Default(schema, data);
|
||||
|
||||
if (Check(schema, parsed)) {
|
||||
return parsed as StaticDecode<typeof schema>;
|
||||
}
|
||||
//console.log("errors", ...Errors(schema, data));
|
||||
|
||||
throw new TypeInvalidError(schema, data);
|
||||
}
|
||||
|
||||
export function strictParse<Schema extends TSchema = TSchema>(
|
||||
schema: Schema,
|
||||
data: Static<Schema>,
|
||||
options?: ParseOptions
|
||||
): Static<Schema> {
|
||||
return parse(schema, data as any, options);
|
||||
}
|
||||
|
||||
export function registerCustomTypeboxKinds(registry: typeof TypeRegistry) {
|
||||
registry.Set("StringEnum", (schema: any, value: any) => {
|
||||
return typeof value === "string" && schema.enum.includes(value);
|
||||
});
|
||||
}
|
||||
registerCustomTypeboxKinds(TypeRegistry);
|
||||
|
||||
export const StringEnum = <const T extends readonly string[]>(values: T, options?: StringOptions) =>
|
||||
Type.Unsafe<T[number]>({
|
||||
[Kind]: "StringEnum",
|
||||
type: "string",
|
||||
enum: values,
|
||||
...options
|
||||
});
|
||||
|
||||
// key value record compatible with RJSF and typebox inference
|
||||
// acting like a Record, but using an Object with additionalProperties
|
||||
export const StringRecord = <T extends TSchema>(properties: T, options?: ObjectOptions) =>
|
||||
Type.Object({}, { ...options, additionalProperties: properties }) as unknown as TRecord<
|
||||
TString,
|
||||
typeof properties
|
||||
>;
|
||||
|
||||
// fixed value that only be what is given + prefilled
|
||||
export const Const = <T extends TLiteralValue = TLiteralValue>(value: T, options?: SchemaOptions) =>
|
||||
Type.Literal(value, { ...options, default: value, const: value, readOnly: true }) as TLiteral<T>;
|
||||
|
||||
export const StringIdentifier = Type.String({
|
||||
pattern: "^[a-zA-Z_][a-zA-Z0-9_]*$",
|
||||
minLength: 2,
|
||||
maxLength: 150
|
||||
});
|
||||
|
||||
SetErrorFunction((error) => {
|
||||
if (error?.schema?.errorMessage) {
|
||||
return error.schema.errorMessage;
|
||||
}
|
||||
|
||||
if (error?.schema?.[Kind] === "StringEnum") {
|
||||
return `Expected: ${error.schema.enum.map((e) => `"${e}"`).join(", ")}`;
|
||||
}
|
||||
|
||||
return DefaultErrorFunction(error);
|
||||
});
|
||||
|
||||
export {
|
||||
Type,
|
||||
type Static,
|
||||
type StaticDecode,
|
||||
type TSchema,
|
||||
Kind,
|
||||
type TObject,
|
||||
type ValueError,
|
||||
type SchemaOptions,
|
||||
Value,
|
||||
Default,
|
||||
Errors,
|
||||
Check
|
||||
};
|
||||
8
app/src/core/utils/types.d.ts
vendored
Normal file
8
app/src/core/utils/types.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
export type Prettify<T> = {
|
||||
[K in keyof T]: T[K];
|
||||
} & NonNullable<unknown>;
|
||||
|
||||
// prettify recursively
|
||||
export type PrettifyRec<T> = {
|
||||
[K in keyof T]: T[K] extends object ? Prettify<T[K]> : T[K];
|
||||
} & NonNullable<unknown>;
|
||||
4
app/src/core/utils/uuid.ts
Normal file
4
app/src/core/utils/uuid.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// generates v4
|
||||
export function uuid(): string {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
6
app/src/core/utils/xml.ts
Normal file
6
app/src/core/utils/xml.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { XMLParser } from "fast-xml-parser";
|
||||
|
||||
export function xmlToObject(xml: string) {
|
||||
const parser = new XMLParser();
|
||||
return parser.parse(xml);
|
||||
}
|
||||
Reference in New Issue
Block a user