web_lib

Common web application libraries
git clone https://radroots.dev/git/web_lib.git
Log | Files | Refs | LICENSE

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:
Autils/src/async/index.ts | 19+++++++++++++++++++
Autils/src/binary/index.ts | 6++++++
Dutils/src/currency.ts | 47-----------------------------------------------
Autils/src/currency/index.ts | 46++++++++++++++++++++++++++++++++++++++++++++++
Rutils/src/errors/lib.ts -> utils/src/errors/index.ts | 0
Rutils/src/geo.ts -> utils/src/geo/index.ts | 0
Dutils/src/http.ts | 143-------------------------------------------------------------------------------
Autils/src/http/index.ts | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rutils/src/id/lib.ts -> utils/src/id/index.ts | 0
Mutils/src/index.ts | 32+++++++++++++++++---------------
Dutils/src/lib.ts | 63---------------------------------------------------------------
Autils/src/media/index.ts | 17+++++++++++++++++
Dutils/src/model.ts | 130-------------------------------------------------------------------------------
Autils/src/model/index.ts | 130+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rutils/src/numbers/lib.ts -> utils/src/numbers/index.ts | 0
Autils/src/object/index.ts | 20++++++++++++++++++++
Dutils/src/schema/media.ts | 9---------
Autils/src/text/index.ts | 19+++++++++++++++++++
Dutils/src/text/lib.ts | 10----------
Rutils/src/time/lib.ts -> utils/src/time/index.ts | 0
Dutils/src/types.ts | 29-----------------------------
Autils/src/types/index.ts | 48++++++++++++++++++++++++++++++++++++++++++++++++
Dutils/src/types/lib.ts | 19-------------------
Dutils/src/unit.ts | 56--------------------------------------------------------
Autils/src/unit/index.ts | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Autils/src/validation/index.ts | 2++
Autils/src/validation/schema.ts | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dutils/src/validation/schemas.ts | 58----------------------------------------------------------
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