commit dc2a0c27fe20fd351398183fb2b12c6a946e9f0b
parent 857f59c7c299c6151f67cfc19d091a9b1bf515da
Author: triesap <triesap@radroots.dev>
Date: Sun, 21 Dec 2025 23:10:42 +0000
utils: unified exports behind index modules, adding async iteration, buffer conversion, media schemas, and shared utilities
Diffstat:
28 files changed, 580 insertions(+), 579 deletions(-)
diff --git a/utils/src/async/index.ts b/utils/src/async/index.ts
@@ -0,0 +1,19 @@
+import type { CallbackPromise } from "../types/index.js";
+
+export const exe_iter = async (callback: CallbackPromise, num: number = 1, delay: number = 400): Promise<void> => {
+ try {
+ const iter_fn = (count: number) => {
+ if (count > 0) {
+ callback();
+ if (count > 1) {
+ setTimeout(() => {
+ iter_fn(count - 1);
+ }, delay);
+ }
+ }
+ };
+ iter_fn(num);
+ } catch (e) {
+ console.log(`(error) exe_iter `, e);
+ }
+};
diff --git a/utils/src/binary/index.ts b/utils/src/binary/index.ts
@@ -0,0 +1,6 @@
+export function as_array_buffer(u8: Uint8Array): ArrayBuffer {
+ if (u8.byteOffset === 0 && u8.buffer instanceof ArrayBuffer && u8.byteLength === u8.buffer.byteLength) {
+ return u8.buffer;
+ }
+ return u8.slice().buffer;
+}
diff --git a/utils/src/currency.ts b/utils/src/currency.ts
@@ -1,46 +0,0 @@
-import { util_rxp } from "./validation/regex.js";
-
-export type FiatCurrency = `usd` | `eur`;
-export const fiat_currencies: FiatCurrency[] = [`usd`, `eur`] as const;
-
-// @todo
-export const price_to_formatted = (n: number, _currency: string) => Math.round(n * 100) / 100;
-
-export const parse_currency = (val?: string): FiatCurrency => {
- const cur = val?.trim().toLowerCase()
- switch (cur) {
- case `usd`:
- case `eur`:
- return cur;
- default:
- return `usd`;
- };
-};
-
-export const fmt_price = (locale: string, value: string, currency: string): string => {
- const fmt = new Intl.NumberFormat(locale, {
- style: 'currency',
- currency: currency.toUpperCase(),
- minimumFractionDigits: 2,
- maximumFractionDigits: 2
- });
- return fmt.format(parseFloat(value));
-};
-
-export const parse_currency_marker = (locale: string, currency: string): string => {
- const cur = parse_currency(currency);
- const fmt = new Intl.NumberFormat(locale, {
- style: 'currency',
- currency: cur.toUpperCase(),
- minimumFractionDigits: 2,
- });
- const fmt_basis = fmt.format(1);
- let fmt_res: string | undefined = undefined;
- fmt_res = fmt_basis.match(util_rxp.currency_marker)?.[0];
- if (fmt_res) return fmt_res;
- fmt_res = fmt_basis.match(util_rxp.currency_symbol)?.[0];
- if (fmt_res) return fmt_res;
- fmt_res = fmt_basis.match(new RegExp(cur, `i`))?.[0];
- if (fmt_res) return fmt_res;
- return cur.toUpperCase();
-};
-\ No newline at end of file
diff --git a/utils/src/currency/index.ts b/utils/src/currency/index.ts
@@ -0,0 +1,46 @@
+import { util_rxp } from "../validation/regex.js";
+
+export type FiatCurrency = `usd` | `eur`;
+export const fiat_currencies: FiatCurrency[] = [`usd`, `eur`] as const;
+
+// @todo
+export const price_to_formatted = (n: number, _currency: string) => Math.round(n * 100) / 100;
+
+export const parse_currency = (val?: string): FiatCurrency => {
+ const cur = val?.trim().toLowerCase()
+ switch (cur) {
+ case `usd`:
+ case `eur`:
+ return cur;
+ default:
+ return `usd`;
+ };
+};
+
+export const fmt_price = (locale: string, value: string, currency: string): string => {
+ const fmt = new Intl.NumberFormat(locale, {
+ style: 'currency',
+ currency: currency.toUpperCase(),
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2
+ });
+ return fmt.format(parseFloat(value));
+};
+
+export const parse_currency_marker = (locale: string, currency: string): string => {
+ const cur = parse_currency(currency);
+ const fmt = new Intl.NumberFormat(locale, {
+ style: 'currency',
+ currency: cur.toUpperCase(),
+ minimumFractionDigits: 2,
+ });
+ const fmt_basis = fmt.format(1);
+ let fmt_res: string | undefined = undefined;
+ fmt_res = fmt_basis.match(util_rxp.currency_marker)?.[0];
+ if (fmt_res) return fmt_res;
+ fmt_res = fmt_basis.match(util_rxp.currency_symbol)?.[0];
+ if (fmt_res) return fmt_res;
+ fmt_res = fmt_basis.match(new RegExp(cur, `i`))?.[0];
+ if (fmt_res) return fmt_res;
+ return cur.toUpperCase();
+};
diff --git a/utils/src/errors/lib.ts b/utils/src/errors/index.ts
diff --git a/utils/src/geo.ts b/utils/src/geo/index.ts
diff --git a/utils/src/http.ts b/utils/src/http.ts
@@ -1,143 +0,0 @@
-import type { IError } from "@radroots/types-bindings";
-import { FieldRecord, NotifyMessage } from "./types.js";
-
-export const is_err_response = (response: any): response is { err: string } => {
- return "err" in response && typeof response.err === "string";
-}
-
-export const is_pass_response = (response: any): response is { pass: true } => {
- return "pass" in response && response.pass === true;
-}
-
-export const is_result_response = (response: any): response is { result: string } => {
- return "result" in response && typeof response.result === "string";
-}
-
-export const is_results_response = (response: any): response is { results: string[] } => {
- return "results" in response && Array.isArray(response.results);
-}
-
-export const is_error_response = (response: any): response is { error: string } => {
- return "error" in response && response.error;
-}
-
-export const is_message_response = (response: any): response is NotifyMessage => {
- return (
- typeof response === "object" &&
- response !== null &&
- "message" in response &&
- typeof response.message === "string" &&
- (response.ok === undefined || typeof response.ok === "string") &&
- (response.cancel === undefined || typeof response.cancel === "string")
- );
-};
-export type IClientHttp = {
- fetch(opts: IHttpOpts): Promise<IHttpResponse | IError<string>>;
-};
-
-export type IHttpImageResponse = {
- status: number;
- blob?: Blob;
- headers: FieldRecord;
- url: string;
-};
-
-export type IHttpResponse = {
- status: number;
- data?: any;
- error?: string;
- message?: NotifyMessage;
- headers: FieldRecord;
- url: string;
-};
-
-export type IHttpOptsData = any;
-export type IHttpOptsParams = Record<string, string | string[]>;
-
-export type IHttpOpts = {
- url: string;
- method?: `get` | `post` | `put`;
- params?: IHttpOptsParams;
- data?: IHttpOptsData;
- data_bin?: Uint8Array;
- authorization?: string;
- headers?: FieldRecord;
- connect_timeout?: number;
-};
-
-export const lib_http_to_bodyinit = (data: any): RequestInit["body"] => {
- if (typeof data === 'string') return data;
- else if (data instanceof FormData) return data;
- else if (data instanceof Blob) return data;
- else if (data instanceof ArrayBuffer) return data;
- else if (data instanceof URLSearchParams) return data;
- return JSON.stringify(data);
-}
-
-export const lib_http_parse_headers = (headers: Headers): FieldRecord => {
- const acc: FieldRecord = {};
- headers.forEach((value, key) => acc[key] = value);
- return acc;
-};
-
-export const http_fetch_opts = (opts: IHttpOpts): { url: string; options: RequestInit; } => {
- const { url } = opts;
- const method = opts.method ? opts.method.toUpperCase() : `GET`;
- const headers = new Headers();
- if (method === `POST`) headers.append(`Content-Type`, `application/json`);
- if (opts.authorization) headers.append(`Authorization`, `Bearer ${encodeURIComponent(opts.authorization)}`);
- if (opts.headers) Object.entries(opts.headers).forEach(([k, v]) => headers.append(k, v))
- const options: RequestInit = {
- method,
- headers,
- }
- if (opts.data) options.body = lib_http_to_bodyinit(opts.data);
- if (opts.data_bin) options.body = opts.data_bin as any;
- return {
- url,
- options
- }
-};
-
-export const lib_http_parse_response = async (res: Response): Promise<IHttpResponse> => {
- let data: unknown = null;
- try {
- const res_json = await res.clone().json();
- data = typeof res_json === `string` ? JSON.parse(res_json) : res_json;
- } catch { }
- if (!data) {
- try {
- data = await res.text();
- } catch { }
- }
- return {
- status: res.status,
- url: res.url,
- data: res.ok && data ? data : null,
- error: !res.ok && is_error_response(data) ? data.error : undefined,
- message: res.ok && is_message_response(data) ? data : undefined,
- headers: lib_http_parse_headers(res.headers)
- };
-};
-
-export const http_fetch = async (opts: IHttpOpts): Promise<IHttpResponse> => {
- const { url, options } = http_fetch_opts(opts);
- const response = await fetch(url, options);
- let data: any = null;
- try {
- const res_json = await response.json();
- data = typeof res_json === `string` ? JSON.parse(res_json) : res_json;
- } catch { }
- if (!data) {
- try {
- const res_text = await response.text();
- data = res_text;
- } catch { }
- }
- return {
- status: response.status,
- url: response.url,
- data,
- headers: lib_http_parse_headers(response.headers)
- };
-};
diff --git a/utils/src/http/index.ts b/utils/src/http/index.ts
@@ -0,0 +1,143 @@
+import type { IError } from "@radroots/types-bindings";
+import { FieldRecord, NotifyMessage } from "../types/index.js";
+
+export const is_err_response = (response: any): response is { err: string } => {
+ return "err" in response && typeof response.err === "string";
+}
+
+export const is_pass_response = (response: any): response is { pass: true } => {
+ return "pass" in response && response.pass === true;
+}
+
+export const is_result_response = (response: any): response is { result: string } => {
+ return "result" in response && typeof response.result === "string";
+}
+
+export const is_results_response = (response: any): response is { results: string[] } => {
+ return "results" in response && Array.isArray(response.results);
+}
+
+export const is_error_response = (response: any): response is { error: string } => {
+ return "error" in response && response.error;
+}
+
+export const is_message_response = (response: any): response is NotifyMessage => {
+ return (
+ typeof response === "object" &&
+ response !== null &&
+ "message" in response &&
+ typeof response.message === "string" &&
+ (response.ok === undefined || typeof response.ok === "string") &&
+ (response.cancel === undefined || typeof response.cancel === "string")
+ );
+};
+export type IClientHttp = {
+ fetch(opts: IHttpOpts): Promise<IHttpResponse | IError<string>>;
+};
+
+export type IHttpImageResponse = {
+ status: number;
+ blob?: Blob;
+ headers: FieldRecord;
+ url: string;
+};
+
+export type IHttpResponse = {
+ status: number;
+ data?: any;
+ error?: string;
+ message?: NotifyMessage;
+ headers: FieldRecord;
+ url: string;
+};
+
+export type IHttpOptsData = any;
+export type IHttpOptsParams = Record<string, string | string[]>;
+
+export type IHttpOpts = {
+ url: string;
+ method?: `get` | `post` | `put`;
+ params?: IHttpOptsParams;
+ data?: IHttpOptsData;
+ data_bin?: Uint8Array;
+ authorization?: string;
+ headers?: FieldRecord;
+ connect_timeout?: number;
+};
+
+export const lib_http_to_bodyinit = (data: any): RequestInit["body"] => {
+ if (typeof data === 'string') return data;
+ else if (data instanceof FormData) return data;
+ else if (data instanceof Blob) return data;
+ else if (data instanceof ArrayBuffer) return data;
+ else if (data instanceof URLSearchParams) return data;
+ return JSON.stringify(data);
+}
+
+export const lib_http_parse_headers = (headers: Headers): FieldRecord => {
+ const acc: FieldRecord = {};
+ headers.forEach((value, key) => acc[key] = value);
+ return acc;
+};
+
+export const http_fetch_opts = (opts: IHttpOpts): { url: string; options: RequestInit; } => {
+ const { url } = opts;
+ const method = opts.method ? opts.method.toUpperCase() : `GET`;
+ const headers = new Headers();
+ if (method === `POST`) headers.append(`Content-Type`, `application/json`);
+ if (opts.authorization) headers.append(`Authorization`, `Bearer ${encodeURIComponent(opts.authorization)}`);
+ if (opts.headers) Object.entries(opts.headers).forEach(([k, v]) => headers.append(k, v))
+ const options: RequestInit = {
+ method,
+ headers,
+ }
+ if (opts.data) options.body = lib_http_to_bodyinit(opts.data);
+ if (opts.data_bin) options.body = opts.data_bin as any;
+ return {
+ url,
+ options
+ }
+};
+
+export const lib_http_parse_response = async (res: Response): Promise<IHttpResponse> => {
+ let data: unknown = null;
+ try {
+ const res_json = await res.clone().json();
+ data = typeof res_json === `string` ? JSON.parse(res_json) : res_json;
+ } catch { }
+ if (!data) {
+ try {
+ data = await res.text();
+ } catch { }
+ }
+ return {
+ status: res.status,
+ url: res.url,
+ data: res.ok && data ? data : null,
+ error: !res.ok && is_error_response(data) ? data.error : undefined,
+ message: res.ok && is_message_response(data) ? data : undefined,
+ headers: lib_http_parse_headers(res.headers)
+ };
+};
+
+export const http_fetch = async (opts: IHttpOpts): Promise<IHttpResponse> => {
+ const { url, options } = http_fetch_opts(opts);
+ const response = await fetch(url, options);
+ let data: any = null;
+ try {
+ const res_json = await response.json();
+ data = typeof res_json === `string` ? JSON.parse(res_json) : res_json;
+ } catch { }
+ if (!data) {
+ try {
+ const res_text = await response.text();
+ data = res_text;
+ } catch { }
+ }
+ return {
+ status: response.status,
+ url: response.url,
+ data,
+ headers: lib_http_parse_headers(response.headers)
+ };
+};
diff --git a/utils/src/id/lib.ts b/utils/src/id/index.ts
diff --git a/utils/src/index.ts b/utils/src/index.ts
@@ -1,17 +1,19 @@
-export * from "./currency.js";
-export * from "./errors/lib.js";
-export * from "./geo.js";
-export * from "./http.js";
-export * from "./id/lib.js";
+export * from "./async/index.js";
+export * from "./binary/index.js";
+export * from "./currency/index.js";
+export * from "./errors/index.js";
+export * from "./geo/index.js";
+export * from "./http/index.js";
+export * from "./id/index.js";
export * from "./index.js";
-export * from "./lib.js";
-export * from "./model.js";
-export * from "./numbers/lib.js";
-export * from "./schema/media.js";
-export * from "./text/lib.js";
-export * from "./time/lib.js";
-export * from "./types/lib.js";
-export * from "./types.js";
-export * from "./unit.js";
+export * from "./media/index.js";
+export * from "./model/index.js";
+export * from "./numbers/index.js";
+export * from "./object/index.js";
+export * from "./text/index.js";
+export * from "./time/index.js";
+export * from "./types/index.js";
+export * from "./unit/index.js";
+export * from "./validation/index.js";
export * from "./validation/regex.js";
-export * from "./validation/schemas.js";
+export * from "./validation/schema.js";
diff --git a/utils/src/lib.ts b/utils/src/lib.ts
@@ -1,62 +0,0 @@
-import { CallbackPromise } from "./types/lib.js";
-
-export const exe_iter = async (callback: CallbackPromise, num: number = 1, delay: number = 400): Promise<void> => {
- try {
- const iter_fn = (count: number) => {
- if (count > 0) {
- callback();
- if (count > 1) {
- setTimeout(() => {
- iter_fn(count - 1);
- }, delay);
- }
- }
- };
- iter_fn(num);
- } catch (e) {
- console.log(`(error) exe_iter `, e);
- }
-};
-
-export type MediaImageUploadResult = {
- base_url: string;
- file_hash: string;
- file_ext: string;
-};
-
-export const fmt_media_image_upload_result_url = (res: MediaImageUploadResult): string => `${res.base_url}/${res.file_hash}.${res.file_ext}`;
-
-export const obj_en = <KeyType extends string, ValType>(object: Record<string, ValType>, parse_function: (key: string) => KeyType = (i) => i as KeyType): [KeyType, ValType][] => {
- return Object.entries(object).map<[KeyType, ValType]>(([k, v]) => [parse_function(k), v])
-};
-
-export const obj_truthy_fields = (obj: Record<string, string>): boolean => {
- return Object.values(obj).every(Boolean);
-};
-
-export const obj_result = (obj: any): string | undefined => {
- if (`result` in obj && typeof obj.result === `string`) return obj.result;
- return undefined;
-};
-
-export const obj_results_str = (obj: any): string[] | undefined => {
- if (Array.isArray(obj.results)) return obj.results.map(String);
- return undefined;
-};
-
-export const str_cap = (val?: string): string => {
- if (!val) return ``;
- return `${val[0].toUpperCase()}${val.slice(1)}`;
-};
-
-export const str_cap_words = (val?: string): string => {
- if (!val) return ``;
- return val.split(` `).map(i => i ? str_cap(i) : ``).filter(i => !!i).join(` `);
-};
-
-export function as_array_buffer(u8: Uint8Array): ArrayBuffer {
- if (u8.byteOffset === 0 && u8.buffer instanceof ArrayBuffer && u8.byteLength === u8.buffer.byteLength) {
- return u8.buffer;
- }
- return u8.slice().buffer;
-}
-\ No newline at end of file
diff --git a/utils/src/media/index.ts b/utils/src/media/index.ts
@@ -0,0 +1,17 @@
+import z from "zod";
+
+export const schema_media_resource = z.object({
+ base_url: z.string(),
+ hash: z.string(),
+ ext: z.string(),
+
+});
+export type MediaResource = z.infer<typeof schema_media_resource>;
+
+export type MediaImageUploadResult = {
+ base_url: string;
+ file_hash: string;
+ file_ext: string;
+};
+
+export const fmt_media_image_upload_result_url = (res: MediaImageUploadResult): string => `${res.base_url}/${res.file_hash}.${res.file_ext}`;
diff --git a/utils/src/model.ts b/utils/src/model.ts
@@ -1,130 +0,0 @@
-import { ValidationRegex } from "./types.js";
-
-export type IModelsQueryValue = string | number | boolean | null;
-export type IModelsQueryBindValue = string | number | boolean | null;;
-export type IModelsQueryBindValueTuple = [string, IModelsQueryValue];
-export type IModelsQueryBindValueOpt = (IModelsQueryBindValue | null);
-export type IModelsQueryFilterOption = `equals` | `starts-with` | `ends-with` | `contains` | `ne`;
-export type IModelsQueryFilterOptionList = `between` | `in`;
-export type IModelsQueryFilterCondition = `and` | `or` | `not`
-
-export type IModelsSortCreatedAt = 'newest' | 'oldest';
-export type IModelsQueryParam = { query: string; bind_values: IModelsQueryBindValue[] };
-export type IModelsFormErrorTuple = [boolean, string];
-export type IModelsFormValidationTuple = [RegExp, string];
-export type IModelsSchemaErrors = { err_s: string[]; };
-export type IModelsForm = {
- label?: string;
- placeholder?: string;
- validateKeypress?: boolean;
- preventFocusRest?: boolean;
- hidden?: boolean;
- optional?: boolean;
- default?: string | number;
- rxpv: ValidationRegex;
-};
-
-export type IModelQueryFilterMapValuesTuplesOption = [IModelsQueryValue, IModelsQueryFilterOption];
-export type IModelQueryFilterMapValuesTuplesOptionList = [IModelsQueryValue[], IModelsQueryFilterOptionList];
-export type IModelQueryFilterMapValuesTuples = ModelQueryFilterMapTuple<IModelQueryFilterMapValuesTuplesOption> | ModelQueryFilterMapTuple<IModelQueryFilterMapValuesTuplesOptionList>;
-export type IModelQueryFilterMapValues = IModelsQueryValue | IModelQueryFilterMapValuesTuples;
-
-export type ModelQueryFilterMapTupleBasis =
- | [IModelsQueryValue, IModelsQueryFilterOption]
- | [IModelsQueryValue, IModelsQueryFilterOption, IModelsQueryFilterCondition]
- | [IModelsQueryValue[], IModelsQueryFilterOptionList]
- | [IModelsQueryValue[], IModelsQueryFilterOptionList, IModelsQueryFilterCondition];
-
-export type ModelQueryFilterMapTuple<T extends ModelQueryFilterMapTupleBasis> =
- T extends [IModelsQueryValue, IModelsQueryFilterOption]
- ? [IModelsQueryValue, IModelsQueryFilterOption, IModelsQueryFilterCondition]
- : T extends [IModelsQueryValue[], IModelsQueryFilterOptionList]
- ? [IModelsQueryValue[], IModelsQueryFilterOptionList, IModelsQueryFilterCondition]
- : T;
-
-export type IModelQueryFilterMap<ModelFilter extends object> = {
- [K in keyof ModelFilter]: ModelFilter[K] | [ModelFilter[K], IModelsQueryFilterOption] | [ModelFilter[K], IModelsQueryFilterOption, IModelsQueryFilterCondition] | [ModelFilter[K][], IModelsQueryFilterOptionList] | [ModelFilter[K][], IModelsQueryFilterOptionList, IModelsQueryFilterCondition];
-};
-export type IModelQueryFilterMapParsed = { query_values: string[]; bind_values: IModelsQueryValue[]; };
-
-export const parse_model_query_value = (val: IModelsQueryValue): IModelsQueryBindValue => {
- if (typeof val === `boolean`) return val ? '1' : '0';
- else if (typeof val === `number`) return val;
- else if (typeof val === `string` && val) return val;
- return null;
-};
-
-export const is_model_query_filter_option = (value: string): value is IModelsQueryFilterOption => {
- return ['equals', 'starts-with', 'ends-with', 'contains', 'ne'].includes(value);
-}
-
-export const is_model_query_filter_option_list = (value: string): value is IModelsQueryFilterOptionList => {
- return ['between', 'in'].includes(value);
-}
-
-export const is_model_query_values = (value: unknown): value is IModelsQueryValue => {
- return typeof value === `string` || typeof value === `number` || typeof value === `boolean`;
-}
-
-export const list_model_query_values_assert = (arr: (IModelsQueryValue | undefined)[]): (IModelsQueryValue)[] => {
- return arr.filter((item): item is string | number | boolean => item !== undefined);
-}
-
-export const parse_model_filter_map = <T extends object>(opts: IModelQueryFilterMap<T>): IModelQueryFilterMapParsed => {
- const bind_values: IModelsQueryValue[] = [];
- const query_values: string[] = [];
-
- for (const [index, entry] of Object.entries(opts).entries()) {
- const [field, filters] = entry as [string, IModelQueryFilterMapValues];
-
- if (is_model_query_values(filters)) {
- query_values.push(`${field} = ?`);
- bind_values.push(filters);
- } else if (Array.isArray(filters)) {
- const [filters_val, filters_opt] = filters;
- const filter_condition = index === 0 ? `` : typeof filters[2] === `undefined` ? `AND ` : ` ${filters[2]}`;
- if (is_model_query_values(filters_val) && is_model_query_filter_option(filters_opt)) {
- switch (filters_opt) {
- case `starts-with`: {
- query_values.push(`${filter_condition}${field} LIKE ?`);
- bind_values.push(`${filters[0]}%`);
- } break;
- case `ends-with`: {
- query_values.push(`${filter_condition}${field} LIKE ?`);
- bind_values.push(`%${filters[0]}`);
- } break;
- case `contains`: {
- query_values.push(`${filter_condition}${field} LIKE ?`);
- bind_values.push(`%${filters[0]}%`);
- } break;
- case `ne`: {
- query_values.push(`${filter_condition}${field} != ?`);
- bind_values.push(`${filters[0]}`);
- } break;
- case `equals`: {
- query_values.push(`${filter_condition}${field} = ?`);
- bind_values.push(filters[0]);
- } break;
- default:
- throw new Error("util.model.parse_model_filter_map.invalid_condition");
- };
- } else if (is_model_query_filter_option_list(filters_opt)) {
- switch (filters_opt) {
- case `between`: {
- query_values.push(`${filter_condition}${field} BETWEEN ? AND ?`);
- bind_values.push(...filters[0].slice(0, 2));
- } break;
- case `in`: {
- query_values.push(`${filter_condition}${field} IN (${`? `.repeat(filters[0].length).trim().split(" ").join(", ")})`);
- bind_values.push(...list_model_query_values_assert(filters[0]));
- } break;
- default:
- throw new Error("util.model.parse_model_filter_map.invalid_condition");
- };
- }
- }
- }
- if (!query_values.length) throw new Error("Error: invalid filter.");
- if (!bind_values.length) throw new Error("Error: invalid filter.");
- return { query_values, bind_values };
-};
diff --git a/utils/src/model/index.ts b/utils/src/model/index.ts
@@ -0,0 +1,130 @@
+import { ValidationRegex } from "../types/index.js";
+
+export type IModelsQueryValue = string | number | boolean | null;
+export type IModelsQueryBindValue = string | number | boolean | null;;
+export type IModelsQueryBindValueTuple = [string, IModelsQueryValue];
+export type IModelsQueryBindValueOpt = (IModelsQueryBindValue | null);
+export type IModelsQueryFilterOption = `equals` | `starts-with` | `ends-with` | `contains` | `ne`;
+export type IModelsQueryFilterOptionList = `between` | `in`;
+export type IModelsQueryFilterCondition = `and` | `or` | `not`
+
+export type IModelsSortCreatedAt = 'newest' | 'oldest';
+export type IModelsQueryParam = { query: string; bind_values: IModelsQueryBindValue[] };
+export type IModelsFormErrorTuple = [boolean, string];
+export type IModelsFormValidationTuple = [RegExp, string];
+export type IModelsSchemaErrors = { err_s: string[]; };
+export type IModelsForm = {
+ label?: string;
+ placeholder?: string;
+ validateKeypress?: boolean;
+ preventFocusRest?: boolean;
+ hidden?: boolean;
+ optional?: boolean;
+ default?: string | number;
+ rxpv: ValidationRegex;
+};
+
+export type IModelQueryFilterMapValuesTuplesOption = [IModelsQueryValue, IModelsQueryFilterOption];
+export type IModelQueryFilterMapValuesTuplesOptionList = [IModelsQueryValue[], IModelsQueryFilterOptionList];
+export type IModelQueryFilterMapValuesTuples = ModelQueryFilterMapTuple<IModelQueryFilterMapValuesTuplesOption> | ModelQueryFilterMapTuple<IModelQueryFilterMapValuesTuplesOptionList>;
+export type IModelQueryFilterMapValues = IModelsQueryValue | IModelQueryFilterMapValuesTuples;
+
+export type ModelQueryFilterMapTupleBasis =
+ | [IModelsQueryValue, IModelsQueryFilterOption]
+ | [IModelsQueryValue, IModelsQueryFilterOption, IModelsQueryFilterCondition]
+ | [IModelsQueryValue[], IModelsQueryFilterOptionList]
+ | [IModelsQueryValue[], IModelsQueryFilterOptionList, IModelsQueryFilterCondition];
+
+export type ModelQueryFilterMapTuple<T extends ModelQueryFilterMapTupleBasis> =
+ T extends [IModelsQueryValue, IModelsQueryFilterOption]
+ ? [IModelsQueryValue, IModelsQueryFilterOption, IModelsQueryFilterCondition]
+ : T extends [IModelsQueryValue[], IModelsQueryFilterOptionList]
+ ? [IModelsQueryValue[], IModelsQueryFilterOptionList, IModelsQueryFilterCondition]
+ : T;
+
+export type IModelQueryFilterMap<ModelFilter extends object> = {
+ [K in keyof ModelFilter]: ModelFilter[K] | [ModelFilter[K], IModelsQueryFilterOption] | [ModelFilter[K], IModelsQueryFilterOption, IModelsQueryFilterCondition] | [ModelFilter[K][], IModelsQueryFilterOptionList] | [ModelFilter[K][], IModelsQueryFilterOptionList, IModelsQueryFilterCondition];
+};
+export type IModelQueryFilterMapParsed = { query_values: string[]; bind_values: IModelsQueryValue[]; };
+
+export const parse_model_query_value = (val: IModelsQueryValue): IModelsQueryBindValue => {
+ if (typeof val === `boolean`) return val ? '1' : '0';
+ else if (typeof val === `number`) return val;
+ else if (typeof val === `string` && val) return val;
+ return null;
+};
+
+export const is_model_query_filter_option = (value: string): value is IModelsQueryFilterOption => {
+ return ['equals', 'starts-with', 'ends-with', 'contains', 'ne'].includes(value);
+}
+
+export const is_model_query_filter_option_list = (value: string): value is IModelsQueryFilterOptionList => {
+ return ['between', 'in'].includes(value);
+}
+
+export const is_model_query_values = (value: unknown): value is IModelsQueryValue => {
+ return typeof value === `string` || typeof value === `number` || typeof value === `boolean`;
+}
+
+export const list_model_query_values_assert = (arr: (IModelsQueryValue | undefined)[]): (IModelsQueryValue)[] => {
+ return arr.filter((item): item is string | number | boolean => item !== undefined);
+}
+
+export const parse_model_filter_map = <T extends object>(opts: IModelQueryFilterMap<T>): IModelQueryFilterMapParsed => {
+ const bind_values: IModelsQueryValue[] = [];
+ const query_values: string[] = [];
+
+ for (const [index, entry] of Object.entries(opts).entries()) {
+ const [field, filters] = entry as [string, IModelQueryFilterMapValues];
+
+ if (is_model_query_values(filters)) {
+ query_values.push(`${field} = ?`);
+ bind_values.push(filters);
+ } else if (Array.isArray(filters)) {
+ const [filters_val, filters_opt] = filters;
+ const filter_condition = index === 0 ? `` : typeof filters[2] === `undefined` ? `AND ` : ` ${filters[2]}`;
+ if (is_model_query_values(filters_val) && is_model_query_filter_option(filters_opt)) {
+ switch (filters_opt) {
+ case `starts-with`: {
+ query_values.push(`${filter_condition}${field} LIKE ?`);
+ bind_values.push(`${filters[0]}%`);
+ } break;
+ case `ends-with`: {
+ query_values.push(`${filter_condition}${field} LIKE ?`);
+ bind_values.push(`%${filters[0]}`);
+ } break;
+ case `contains`: {
+ query_values.push(`${filter_condition}${field} LIKE ?`);
+ bind_values.push(`%${filters[0]}%`);
+ } break;
+ case `ne`: {
+ query_values.push(`${filter_condition}${field} != ?`);
+ bind_values.push(`${filters[0]}`);
+ } break;
+ case `equals`: {
+ query_values.push(`${filter_condition}${field} = ?`);
+ bind_values.push(filters[0]);
+ } break;
+ default:
+ throw new Error("util.model.parse_model_filter_map.invalid_condition");
+ };
+ } else if (is_model_query_filter_option_list(filters_opt)) {
+ switch (filters_opt) {
+ case `between`: {
+ query_values.push(`${filter_condition}${field} BETWEEN ? AND ?`);
+ bind_values.push(...filters[0].slice(0, 2));
+ } break;
+ case `in`: {
+ query_values.push(`${filter_condition}${field} IN (${`? `.repeat(filters[0].length).trim().split(" ").join(", ")})`);
+ bind_values.push(...list_model_query_values_assert(filters[0]));
+ } break;
+ default:
+ throw new Error("util.model.parse_model_filter_map.invalid_condition");
+ };
+ }
+ }
+ }
+ if (!query_values.length) throw new Error("Error: invalid filter.");
+ if (!bind_values.length) throw new Error("Error: invalid filter.");
+ return { query_values, bind_values };
+};
diff --git a/utils/src/numbers/lib.ts b/utils/src/numbers/index.ts
diff --git a/utils/src/object/index.ts b/utils/src/object/index.ts
@@ -0,0 +1,20 @@
+export const obj_en = <KeyType extends string, ValType>(
+ object: Record<string, ValType>,
+ parse_function: (key: string) => KeyType = (i) => i as KeyType
+): [KeyType, ValType][] => {
+ return Object.entries(object).map<[KeyType, ValType]>(([k, v]) => [parse_function(k), v])
+};
+
+export const obj_truthy_fields = (obj: Record<string, string>): boolean => {
+ return Object.values(obj).every(Boolean);
+};
+
+export const obj_result = (obj: any): string | undefined => {
+ if (`result` in obj && typeof obj.result === `string`) return obj.result;
+ return undefined;
+};
+
+export const obj_results_str = (obj: any): string[] | undefined => {
+ if (Array.isArray(obj.results)) return obj.results.map(String);
+ return undefined;
+};
diff --git a/utils/src/schema/media.ts b/utils/src/schema/media.ts
@@ -1,9 +0,0 @@
-import z from "zod";
-
-export const schema_media_resource = z.object({
- base_url: z.string(),
- hash: z.string(),
- ext: z.string(),
-
-});
-export type MediaResource = z.infer<typeof schema_media_resource>;
diff --git a/utils/src/text/index.ts b/utils/src/text/index.ts
@@ -0,0 +1,19 @@
+export const root_symbol = "»`,-";
+
+export function text_enc(data: string): Uint8Array {
+ return new TextEncoder().encode(data);
+}
+
+export function text_dec(data: Uint8Array): string {
+ return new TextDecoder().decode(data);
+}
+
+export const str_cap = (val?: string): string => {
+ if (!val) return ``;
+ return `${val[0].toUpperCase()}${val.slice(1)}`;
+};
+
+export const str_cap_words = (val?: string): string => {
+ if (!val) return ``;
+ return val.split(` `).map(i => i ? str_cap(i) : ``).filter(i => !!i).join(` `);
+};
diff --git a/utils/src/text/lib.ts b/utils/src/text/lib.ts
@@ -1,9 +0,0 @@
-export const root_symbol = "»`,-";
-
-export function text_enc(data: string): Uint8Array {
- return new TextEncoder().encode(data);
-}
-
-export function text_dec(data: Uint8Array): string {
- return new TextDecoder().decode(data);
-}
-\ No newline at end of file
diff --git a/utils/src/time/lib.ts b/utils/src/time/index.ts
diff --git a/utils/src/types.ts b/utils/src/types.ts
@@ -1,29 +0,0 @@
-export type FieldRecord = Record<string, string>;
-
-export type ResolveStatus = "info" | "warning" | "error" | "success";
-
-export type NotifyMessage = {
- message: string;
- ok?: string;
- cancel?: string;
-};
-
-export type FileBytesFormat = `kb` | `mb` | `gb`;
-export type FileMimeType = string;
-export type FilePath = { file_path: string; file_name: string; mime_type: FileMimeType; }
-export type FilePathBlob = { blob_path: string; blob_name: string; mime_type?: FileMimeType; }
-
-export type WebFilePath = FilePath | FilePathBlob;
-
-export type ValStr = string | undefined | null;
-
-export type IdbClientConfig = {
- database: string;
- store: string;
-};
-
-export type ValidationRegex = {
- value: RegExp;
- charset: RegExp;
-}
-
diff --git a/utils/src/types/index.ts b/utils/src/types/index.ts
@@ -0,0 +1,48 @@
+import type { IError } from "@radroots/types-bindings";
+
+export type FieldRecord = Record<string, string>;
+
+export type ResolveStatus = "info" | "warning" | "error" | "success";
+
+export type NotifyMessage = {
+ message: string;
+ ok?: string;
+ cancel?: string;
+};
+
+export type FileBytesFormat = `kb` | `mb` | `gb`;
+export type FileMimeType = string;
+export type FilePath = { file_path: string; file_name: string; mime_type: FileMimeType; }
+export type FilePathBlob = { blob_path: string; blob_name: string; mime_type?: FileMimeType; }
+
+export type WebFilePath = FilePath | FilePathBlob;
+
+export type ValStr = string | undefined | null;
+
+export type IdbClientConfig = {
+ database: string;
+ store: string;
+};
+
+export type ValidationRegex = {
+ value: RegExp;
+ charset: RegExp;
+}
+
+export type CallbackPromise = () => Promise<void>;
+export type CallbackPromiseFigureResult<Ti, Tr> = (value: Ti) => Promise<Tr | undefined>;
+export type CallbackPromiseFull<Ti, Tr> = (value: Ti) => Promise<Tr>;
+export type CallbackPromiseGeneric<T> = (value: T) => Promise<void>;
+export type CallbackPromiseReturn<T> = () => Promise<T>;
+export type CallbackPromiseResult<Tr> = () => Promise<Tr | undefined>;
+
+export type ResultId = { id: string; };
+export type ResultPass = { pass: true; };
+export type ResultBool = ResultObj<boolean>;
+export type ResultsList<T> = { results: T[]; };
+export type ResultObj<T> = { result: T; };
+export type ResultPublicKey = { public_key: string; };
+export type ResultSecretKey = { secret_key: string; };
+
+export type ResolveError<T> = T | IError<string>;
+export type ResolveErrorMsg<TRes, TMsg extends string> = TRes | IError<TMsg>;
diff --git a/utils/src/types/lib.ts b/utils/src/types/lib.ts
@@ -1,19 +0,0 @@
-import type { IError } from "@radroots/types-bindings";
-
-export type CallbackPromise = () => Promise<void>;
-export type CallbackPromiseFigureResult<Ti, Tr> = (value: Ti) => Promise<Tr | undefined>;
-export type CallbackPromiseFull<Ti, Tr> = (value: Ti) => Promise<Tr>;
-export type CallbackPromiseGeneric<T> = (value: T) => Promise<void>;
-export type CallbackPromiseReturn<T> = () => Promise<T>;
-export type CallbackPromiseResult<Tr> = () => Promise<Tr | undefined>;
-
-export type ResultId = { id: string; };
-export type ResultPass = { pass: true; };
-export type ResultBool = ResultObj<boolean>;
-export type ResultsList<T> = { results: T[]; };
-export type ResultObj<T> = { result: T; };
-export type ResultPublicKey = { public_key: string; };
-export type ResultSecretKey = { secret_key: string; };
-
-export type ResolveError<T> = T | IError<string>;
-export type ResolveErrorMsg<TRes, TMsg extends string> = TRes | IError<TMsg>;
diff --git a/utils/src/unit.ts b/utils/src/unit.ts
@@ -1,56 +0,0 @@
-import { z } from "zod";
-import { zf_area_unit, zf_mass_unit } from "./validation/schemas.js";
-
-export type AreaUnit = z.infer<typeof zf_area_unit>;
-export const area_units: AreaUnit[] = [`ac`, `ha`, `ft2`, `m2`] as const;
-
-export type MassUnit = z.infer<typeof zf_mass_unit>;
-export const mass_units: MassUnit[] = [`kg`, `lb`, `g`] as const;
-
-export function parse_mass_unit_default(val?: string): MassUnit {
- const unit = parse_mass_unit(val);
- return unit ?? `kg`
-}
-
-export function parse_mass_unit(val?: string): MassUnit | undefined {
- switch (val) {
- case `kg`:
- case `lb`:
- case `g`:
- return val;
- default:
- return undefined;
- };
-};
-
-export function mass_to_g(val: number, unit: string): number {
- const mass_unit = parse_mass_unit(unit);
- switch (mass_unit) {
- case `kg`:
- return val * 1000;
- case `lb`:
- return val * 453.592;
- case `g`:
- return val;
- default:
- throw new Error(`unsupported unit ${unit}`);
- }
-}
-
-
-export function parse_area_unit_default(val?: string): AreaUnit {
- const unit = parse_area_unit(val);
- return unit ?? `ac`
-}
-
-export function parse_area_unit(val?: string): AreaUnit | undefined {
- switch (val) {
- case `ac`:
- case `ha`:
- case `ft2`:
- case `m2`:
- return val;
- default:
- return undefined;
- };
-};
diff --git a/utils/src/unit/index.ts b/utils/src/unit/index.ts
@@ -0,0 +1,56 @@
+import { z } from "zod";
+import { zf_area_unit, zf_mass_unit } from "../validation/schema.js";
+
+export type AreaUnit = z.infer<typeof zf_area_unit>;
+export const area_units: AreaUnit[] = [`ac`, `ha`, `ft2`, `m2`] as const;
+
+export type MassUnit = z.infer<typeof zf_mass_unit>;
+export const mass_units: MassUnit[] = [`kg`, `lb`, `g`] as const;
+
+export function parse_mass_unit_default(val?: string): MassUnit {
+ const unit = parse_mass_unit(val);
+ return unit ?? `kg`
+}
+
+export function parse_mass_unit(val?: string): MassUnit | undefined {
+ switch (val) {
+ case `kg`:
+ case `lb`:
+ case `g`:
+ return val;
+ default:
+ return undefined;
+ };
+};
+
+export function mass_to_g(val: number, unit: string): number {
+ const mass_unit = parse_mass_unit(unit);
+ switch (mass_unit) {
+ case `kg`:
+ return val * 1000;
+ case `lb`:
+ return val * 453.592;
+ case `g`:
+ return val;
+ default:
+ throw new Error(`unsupported unit ${unit}`);
+ }
+}
+
+
+export function parse_area_unit_default(val?: string): AreaUnit {
+ const unit = parse_area_unit(val);
+ return unit ?? `ac`
+}
+
+export function parse_area_unit(val?: string): AreaUnit | undefined {
+ switch (val) {
+ case `ac`:
+ case `ha`:
+ case `ft2`:
+ case `m2`:
+ return val;
+ default:
+ return undefined;
+ };
+};
diff --git a/utils/src/validation/index.ts b/utils/src/validation/index.ts
@@ -0,0 +1,2 @@
+export * from "./regex.js";
+export * from "./schema.js";
diff --git a/utils/src/validation/schema.ts b/utils/src/validation/schema.ts
@@ -0,0 +1,57 @@
+import { z } from "zod";
+import { GeocoderReverseResult, GeolocationAddress, GeolocationPoint } from "../geo/index.js";
+import { parse_int } from "../numbers/index.js";
+import { util_rxp } from "./regex.js";
+
+export const zf_area_unit = z.union([
+ z.literal(`ac`),
+ z.literal(`ft2`),
+ z.literal(`ha`),
+ z.literal(`m2`),
+]);
+
+export const zf_mass_unit = z.union([
+ z.literal(`kg`),
+ z.literal(`lb`),
+ z.literal(`g`),
+]);
+
+export const schema_geolocation_address: z.ZodSchema<GeolocationAddress> = z.object({
+ primary: z.string().regex(util_rxp.addr_primary),
+ admin: z.string().regex(util_rxp.addr_admin),
+ country: z.string().regex(util_rxp.country_code_a2)
+});
+
+export const schema_geocode_result: z.ZodSchema<GeocoderReverseResult> = z.object({
+ id: z.number(),
+ name: z.string(),
+ admin1_id: z.union([z.string(), z.number()]),
+ admin1_name: z.string(),
+ country_id: z.string(),
+ country_name: z.string(),
+ latitude: z.number(),
+ longitude: z.number(),
+});
+
+export const schema_geolocation_point: z.ZodSchema<GeolocationPoint> = z.object({
+ lat: z.number().min(-90).max(90),
+ lng: z.number().min(-180).max(180),
+});
+
+export const zf_price_amount = z.preprocess((input) => {
+ return parse_int(String(input), 1.00);
+}, z.number().positive().multipleOf(0.01));
+
+export const zf_quantity_amount = z.preprocess((input) => {
+ return parse_int(String(input), 1);
+}, z.number().int().positive());
+
+export const zf_price = z.number().positive().multipleOf(0.01);
+
+export const zf_numi_pos = z.number().int().positive();
+
+export const zf_numf_pos = z.number().positive();
+
+export const zf_email = z.string().email();
+
+export const zf_username = z.string().regex(util_rxp.profile_name);
diff --git a/utils/src/validation/schemas.ts b/utils/src/validation/schemas.ts
@@ -1,57 +0,0 @@
-import { z } from "zod";
-import { GeocoderReverseResult, GeolocationAddress, GeolocationPoint } from "../geo.js";
-import { parse_int } from "../numbers/lib.js";
-import { util_rxp } from "./regex.js";
-
-export const zf_area_unit = z.union([
- z.literal(`ac`),
- z.literal(`ft2`),
- z.literal(`ha`),
- z.literal(`m2`),
-]);
-
-export const zf_mass_unit = z.union([
- z.literal(`kg`),
- z.literal(`lb`),
- z.literal(`g`),
-]);
-
-export const schema_geolocation_address: z.ZodSchema<GeolocationAddress> = z.object({
- primary: z.string().regex(util_rxp.addr_primary),
- admin: z.string().regex(util_rxp.addr_admin),
- country: z.string().regex(util_rxp.country_code_a2)
-});
-
-export const schema_geocode_result: z.ZodSchema<GeocoderReverseResult> = z.object({
- id: z.number(),
- name: z.string(),
- admin1_id: z.union([z.string(), z.number()]),
- admin1_name: z.string(),
- country_id: z.string(),
- country_name: z.string(),
- latitude: z.number(),
- longitude: z.number(),
-});
-
-export const schema_geolocation_point: z.ZodSchema<GeolocationPoint> = z.object({
- lat: z.number().min(-90).max(90),
- lng: z.number().min(-180).max(180),
-});
-
-export const zf_price_amount = z.preprocess((input) => {
- return parse_int(String(input), 1.00);
-}, z.number().positive().multipleOf(0.01));
-
-export const zf_quantity_amount = z.preprocess((input) => {
- return parse_int(String(input), 1);
-}, z.number().int().positive());
-
-export const zf_price = z.number().positive().multipleOf(0.01);
-
-export const zf_numi_pos = z.number().int().positive();
-
-export const zf_numf_pos = z.number().positive();
-
-export const zf_email = z.string().email();
-
-export const zf_username = z.string().regex(util_rxp.profile_name);
-\ No newline at end of file