web_lib

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

commit d037e15a3e30fd8b041153a46ea9b18186baf873
parent 9b801589beba464868597350aa08ab890581a95f
Author: triesap <137732411+triesap@users.noreply.github.com>
Date:   Mon, 13 Jan 2025 16:56:48 +0000

utils: refactor utils

Diffstat:
Autils/src/client/database.ts | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Autils/src/client/datastore.ts | 31+++++++++++++++++++++++++++++++
Autils/src/client/geolocation.ts | 16++++++++++++++++
Autils/src/client/gui.ts | 25+++++++++++++++++++++++++
Mutils/src/error.ts | 8++++++--
Mutils/src/geolocation.ts | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Autils/src/guard.ts | 33+++++++++++++++++++++++++++++++++
Autils/src/http.ts | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mutils/src/index.ts | 10++++++++++
Mutils/src/model.ts | 115++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mutils/src/nostr-event-util/lib.ts | 37+++++++++++--------------------------
Mutils/src/nostr-event-util/types.ts | 10+++-------
Mutils/src/nostr-key-util/lib.ts | 46++++++++++------------------------------------
Mutils/src/nostr-key-util/types.ts | 2+-
Autils/src/nostr/event.ts | 46++++++++++++++++++++++++++++++++++++++++++++++
Autils/src/nostr/key.ts | 43+++++++++++++++++++++++++++++++++++++++++++
Autils/src/nostr/ndk.ts | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Autils/src/nostr/types.ts | 20++++++++++++++++++++
Mutils/src/object.ts | 14++++++++++++++
Mutils/src/types.ts | 14++++++++++++--
Mutils/src/window.ts | 9+++++++++
21 files changed, 730 insertions(+), 77 deletions(-)

diff --git a/utils/src/client/database.ts b/utils/src/client/database.ts @@ -0,0 +1,96 @@ + + +export type IClientDatabase< + TILocationGcsCreate extends object, + TLocationGcsCreateResolve extends object, + TILocationGcsRead extends object, + TLocationGcsReadResolve extends object, + TILocationGcsReadList extends object, + TLocationGcsReadListResolve extends object, + TILocationGcsDelete extends object, + TLocationGcsDeleteResolve extends object, + TILocationGcsUpdate extends object, + TLocationGcsUpdateResolve extends object, + TITradeProductCreate extends object, + TTradeProductCreateResolve extends object, + TITradeProductRead extends object, + TTradeProductReadResolve extends object, + TITradeProductReadList extends object, + TTradeProductReadListResolve extends object, + TITradeProductDelete extends object, + TTradeProductDeleteResolve extends object, + TITradeProductUpdate extends object, + TTradeProductUpdateResolve extends object, + TINostrProfileCreate extends object, + TNostrProfileCreateResolve extends object, + TINostrProfileRead extends object, + TNostrProfileReadResolve extends object, + TINostrProfileReadList extends object, + TNostrProfileReadListResolve extends object, + TINostrProfileDelete extends object, + TNostrProfileDeleteResolve extends object, + TINostrProfileUpdate extends object, + TNostrProfileUpdateResolve extends object, + TINostrRelayCreate extends object, + TNostrRelayCreateResolve extends object, + TINostrRelayRead extends object, + TNostrRelayReadResolve extends object, + TINostrRelayReadList extends object, + TNostrRelayReadListResolve extends object, + TINostrRelayDelete extends object, + TNostrRelayDeleteResolve extends object, + TINostrRelayUpdate extends object, + TNostrRelayUpdateResolve extends object, + TIMediaImageCreate extends object, + TMediaImageCreateResolve extends object, + TIMediaImageRead extends object, + TMediaImageReadResolve extends object, + TIMediaImageReadList extends object, + TMediaImageReadListResolve extends object, + TIMediaImageDelete extends object, + TMediaImageDeleteResolve extends object, + TIMediaImageUpdate extends object, + TMediaImageUpdateResolve extends object, + TILogErrorCreate extends object, + TLogErrorCreateResolve extends object, + TILogErrorRead extends object, + TLogErrorReadResolve extends object, + TILogErrorReadList extends object, + TLogErrorReadListResolve extends object, + TILogErrorDelete extends object, + TLogErrorDeleteResolve extends object, + TILogErrorUpdate extends object, + TLogErrorUpdateResolve extends object +> = { + location_gcs_create(opts: TILocationGcsCreate): Promise<TLocationGcsCreateResolve>; + location_gcs_read(opts: TILocationGcsRead): Promise<TLocationGcsReadResolve>; + location_gcs_read_list(opts: TILocationGcsReadList): Promise<TLocationGcsReadListResolve>; + location_gcs_delete(opts: TILocationGcsDelete): Promise<TLocationGcsDeleteResolve>; + location_gcs_update(opts: TILocationGcsUpdate): Promise<TLocationGcsUpdateResolve>; + trade_product_create(opts: TITradeProductCreate): Promise<TTradeProductCreateResolve>; + trade_product_read(opts: TITradeProductRead): Promise<TTradeProductReadResolve>; + trade_product_read_list(opts: TITradeProductReadList): Promise<TTradeProductReadListResolve>; + trade_product_delete(opts: TITradeProductDelete): Promise<TTradeProductDeleteResolve>; + trade_product_update(opts: TITradeProductUpdate): Promise<TTradeProductUpdateResolve>; + nostr_profile_create(opts: TINostrProfileCreate): Promise<TNostrProfileCreateResolve>; + nostr_profile_read(opts: TINostrProfileRead): Promise<TNostrProfileReadResolve>; + nostr_profile_read_list(opts: TINostrProfileReadList): Promise<TNostrProfileReadListResolve>; + nostr_profile_delete(opts: TINostrProfileDelete): Promise<TNostrProfileDeleteResolve>; + nostr_profile_update(opts: TINostrProfileUpdate): Promise<TNostrProfileUpdateResolve>; + nostr_relay_create(opts: TINostrRelayCreate): Promise<TNostrRelayCreateResolve>; + nostr_relay_read(opts: TINostrRelayRead): Promise<TNostrRelayReadResolve>; + nostr_relay_read_list(opts: TINostrRelayReadList): Promise<TNostrRelayReadListResolve>; + nostr_relay_delete(opts: TINostrRelayDelete): Promise<TNostrRelayDeleteResolve>; + nostr_relay_update(opts: TINostrRelayUpdate): Promise<TNostrRelayUpdateResolve>; + media_image_create(opts: TIMediaImageCreate): Promise<TMediaImageCreateResolve>; + media_image_read(opts: TIMediaImageRead): Promise<TMediaImageReadResolve>; + media_image_read_list(opts: TIMediaImageReadList): Promise<TMediaImageReadListResolve>; + media_image_delete(opts: TIMediaImageDelete): Promise<TMediaImageDeleteResolve>; + media_image_update(opts: TIMediaImageUpdate): Promise<TMediaImageUpdateResolve>; + log_error_create(opts: TILogErrorCreate): Promise<TLogErrorCreateResolve>; + log_error_read(opts: TILogErrorRead): Promise<TLogErrorReadResolve>; + log_error_read_list(opts: TILogErrorReadList): Promise<TLogErrorReadListResolve>; + log_error_delete(opts: TILogErrorDelete): Promise<TLogErrorDeleteResolve>; + log_error_update(opts: TILogErrorUpdate): Promise<TLogErrorUpdateResolve>; + +}; +\ No newline at end of file diff --git a/utils/src/client/datastore.ts b/utils/src/client/datastore.ts @@ -0,0 +1,30 @@ +import type { ErrorMessage, ResultObj, ResultPass, ResultsList } from ".."; + +export const ds_map = { + key_nostr: `nostr:publickey`, + role: `config:role`, + eula: `eula:date` +}; + +export const ds_map_param = { + radroots_profile: (public_key: string) => `radroots:profile:${public_key}` +}; + +export type IClientDatastoreSetResolve = ResultPass | ErrorMessage<string>; +export type IClientDatastoreGetResolve = ResultObj<string> | ErrorMessage<string>; +export type IClientDatastoreSetPResolve = ResultPass | ErrorMessage<string>; +export type IClientDatastoreGetPResolve = ResultObj<string> | ErrorMessage<string>; +export type IClientDatastoreKeysResolve = ResultsList<string> | ErrorMessage<string>; +export type IClientDatastoreEntriesResolve = ResultsList<[string, unknown]> | ErrorMessage<string>; +export type IClientDatastoreRemoveResolve = ResultPass | ErrorMessage<string>; + +export type IClientDatastore = { + init(): Promise<void>; + set(key: keyof typeof ds_map, value: string): Promise<IClientDatastoreSetResolve>; + get(key: keyof typeof ds_map): Promise<IClientDatastoreGetResolve>; + setp(key: keyof typeof ds_map_param, key_param: string, value: string): Promise<IClientDatastoreSetPResolve>; + getp(key: keyof typeof ds_map_param, key_param: string): Promise<IClientDatastoreGetPResolve>; + keys(): Promise<IClientDatastoreKeysResolve>; + entries(): Promise<IClientDatastoreEntriesResolve> + remove(key: string): Promise<IClientDatastoreRemoveResolve>; +}; +\ No newline at end of file diff --git a/utils/src/client/geolocation.ts b/utils/src/client/geolocation.ts @@ -0,0 +1,15 @@ +import { type ErrorMessage } from ".."; + +export type IGeolocationErrorMessage = `*-permissions` | `*`; + +export type IClientGeolocationPosition = { + lat: number; + lng: number; + accuracy?: number; + altitude?: number; + altitude_accuracy?: number; +}; + +export type IClientGeolocation = { + current(): Promise<IClientGeolocationPosition | ErrorMessage<IGeolocationErrorMessage>>; +}; +\ No newline at end of file diff --git a/utils/src/client/gui.ts b/utils/src/client/gui.ts @@ -0,0 +1,24 @@ +import { type ResultsList } from ".."; + +export type IClientGuiDialogKind = "info" | "warning" | "error"; + +export type IClientGuiDialogAlertOpts = string; + +export type IClientGuiDialogConfirmOpts = string | { title?: string, kind?: IClientGuiDialogKind; message: string; cancel?: string; ok?: string; }; + +export type IClientGuiDialogResolve = ResultsList<string>; + +export type IClientGuiNotifyPermission = "default" | "denied" | "granted"; +export type IClientGuiNotifySendOptions = { + id?: number; + channel_id?: string; + title: string; + body?: string; +}; + +export type IClientGui = { + alert(opts: IClientGuiDialogAlertOpts): Promise<boolean>; + confirm(opts: IClientGuiDialogConfirmOpts): Promise<boolean>; + notify_init(): Promise<IClientGuiNotifyPermission | undefined>; + notify_send(opts: string | IClientGuiNotifySendOptions): Promise<void>; +}; +\ No newline at end of file diff --git a/utils/src/error.ts b/utils/src/error.ts @@ -6,6 +6,11 @@ export const handle_error = (e: unknown, append?: string): ErrorMessage<string> return { err }; }; +export const throw_err = (param: string | ErrorMessage<string>): undefined => { + if (typeof param === `string`) throw new Error(param); + else throw new Error(param.err); +}; + export const err_msg = <T extends string>(err: T): ErrorMessage<T> => { return { err }; }; @@ -16,4 +21,4 @@ export const err_res = <T extends object>(error: T): ErrorResponse<T> => { export const err_system = (message: string): boolean => { return message.split(` `).length > 1 -}; -\ No newline at end of file +}; diff --git a/utils/src/geolocation.ts b/utils/src/geolocation.ts @@ -7,6 +7,9 @@ export type GeolocationCoordinatesPoint = { lng: number; } +export type GeolocationLatitudeFmtOption = 'dms' | 'd' | 'dm'; + + export const geohash_encode = (opts: { lat: string | number; lng: string | number; @@ -33,4 +36,52 @@ export const location_geohash = (point: GeolocationCoordinatesPoint): string => const { lat, lng } = point; const res = geohash_encode({ lat, lng }); return res; +}; + +export const geol_lat_fmt = (lat: number, fmt_opt: GeolocationLatitudeFmtOption, locale: string, precision: number = 5): string => { + const options: Intl.NumberFormatOptions = { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }; + const fmt_deg = new Intl.NumberFormat(locale, { maximumFractionDigits: 0 }); + const fmt_min = new Intl.NumberFormat(locale, options); + const fmt_sec = new Intl.NumberFormat(locale, options); + if (fmt_opt === 'dms') { + const deg = Math.floor(Math.abs(lat)); + const min = Math.floor((Math.abs(lat) - deg) * 60); + const sec = ((Math.abs(lat) - deg - min / 60) * 3600); + return `${fmt_deg.format(deg)}° ${fmt_min.format(min)}' ${fmt_sec.format(sec)}" ${lat >= 0 ? 'N' : 'S'}`; + } else if (fmt_opt === 'dm') { + const deg = Math.floor(Math.abs(lat)); + const min = (Math.abs(lat) - deg) * 60; + return `${fmt_deg.format(deg)}° ${fmt_min.format(min)}' ${lat >= 0 ? 'N' : 'S'}`; + } else { + return `${lat.toLocaleString(locale, { maximumFractionDigits: precision })}° ${lat >= 0 ? 'N' : 'S'}`; + } +}; + +export const geol_lng_fmt = (lng: number, fmt_opt: GeolocationLatitudeFmtOption, locale: string, precision: number = 5): string => { + const options: Intl.NumberFormatOptions = { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }; + const fmt_deg = new Intl.NumberFormat(locale, { maximumFractionDigits: 0 }); + const fmt_min = new Intl.NumberFormat(locale, options); + const fmt_sec = new Intl.NumberFormat(locale, options); + if (fmt_opt === 'dms') { + const degrees = Math.floor(Math.abs(lng)); + const minutes = Math.floor((Math.abs(lng) - degrees) * 60); + const seconds = ((Math.abs(lng) - degrees - minutes / 60) * 3600); + return `${fmt_deg.format(degrees)}° ${fmt_min.format(minutes)}' ${fmt_sec.format(seconds)}" ${lng >= 0 ? 'E' : 'W'}`; + } else if (fmt_opt === 'dm') { + const degrees = Math.floor(Math.abs(lng)); + const minutes = (Math.abs(lng) - degrees) * 60; + return `${fmt_deg.format(degrees)}° ${fmt_min.format(minutes)}' ${lng >= 0 ? 'E' : 'W'}`; + } else { + return `${lng.toLocaleString(locale, { maximumFractionDigits: precision })}° ${lng >= 0 ? 'E' : 'W'}`; + } +}; + +export const parse_geol_coords = (number: number): number => { + return Math.round(number * 1e7) / 1e7; }; \ No newline at end of file diff --git a/utils/src/guard.ts b/utils/src/guard.ts @@ -0,0 +1,32 @@ +import { type NotifyMessage } from "."; + +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 && typeof response.result === "string"; +} + +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") + ); +}; +\ No newline at end of file diff --git a/utils/src/http.ts b/utils/src/http.ts @@ -0,0 +1,105 @@ +import { type FieldRecord, is_error_response, is_message_response, type NotifyMessage } from "."; + +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; +}; + +const lib_http_to_bodyinit = (data: any): BodyInit => { + 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); +} + +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 headers: FieldRecord = { + ...opts.headers, + }; + if (opts.authorization) headers['Authorization'] = `Bearer ${encodeURIComponent(opts.authorization)}`; + const options: RequestInit = { + method: opts.method ? opts.method.toUpperCase() : `GET`, + headers, + } + if (opts.data) options.body = lib_http_to_bodyinit(opts.data); + if (opts.data_bin) options.body = opts.data_bin; + + return { + url, + options + } +}; + +export const http_parse_response = async (res: Response): Promise<Promise<IHttpResponse>> => { + let data: any = null; + try { + const res_json = await res.json(); + if (typeof res_json === `string`) data = JSON.parse(res_json); + else data = res_json; + } catch { } + if (!data) data = await res.text(); + 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 response_data: any = null; + try { + const res_json = await response.json(); + response_data = typeof res_json === `string` ? JSON.parse(res_json) : res_json; + } catch { } + if (!response_data) { + try { + const res_text = await response.text(); + response_data = res_text; + } catch { } + } + return { + status: response.status, + url: response.url, + headers: lib_http_parse_headers(response.headers) + }; +}; + diff --git a/utils/src/index.ts b/utils/src/index.ts @@ -1,13 +1,23 @@ export * from "./ascii" +export * from "./client/database" +export * from "./client/datastore" +export * from "./client/geolocation" +export * from "./client/gui" export * from "./currency" export * from "./error" export * from "./file" export * from "./format" export * from "./geolocation" +export * from "./guard" export * from "./hash" +export * from "./http" export * from "./list" export * from "./math" export * from "./model" +export * from "./nostr/event" +export * from "./nostr/key" +export * from "./nostr/ndk" +export * from "./nostr/types" export * from "./nostr-event-util/lib" export * from "./nostr-event-util/types" export * from "./nostr-key-util/lib" diff --git a/utils/src/model.ts b/utils/src/model.ts @@ -1,7 +1,11 @@ -export type IModelsQueryValue = string | number | boolean; -export type IModelsQueryBindValue = string | number | boolean | null; -export type IModelsQueryBindValueTuple = [string, IModelsQueryBindValue]; +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]; @@ -18,3 +22,108 @@ export type IModelsForm = { optional?: boolean; default?: string | number; }; + +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/nostr-event-util/lib.ts b/utils/src/nostr-event-util/lib.ts @@ -1,10 +1,9 @@ -import { schnorr } from '@noble/curves/secp256k1'; -import { hexToBytes } from "@noble/hashes/utils"; import { type NDKEvent } from "@nostr-dev-kit/ndk"; import ngeotags, { type GeoTags as NostrGeotagsGeotags, type InputData as NostrGeotagsInputData } from "nostr-geotags"; -import { finalizeEvent, getEventHash, nip19, type NostrEvent as NostrToolsEvent } from "nostr-tools"; -import { uuidv4 } from ".."; -import type { INostrEventUtil, INostrEventUtilEventSign, INostrEventUtilFormatTagsBasisNip99, INostrEventUtilNeventEncode, NostrEventTagClient, NostrEventTagLocation, NostrEventTagMediaUpload, NostrEventTagPrice, NostrEventTagQuantity } from "./types"; +import { type NostrEvent as NostrToolsEvent } from "nostr-tools"; +import type { INostrEventEventSign } from ".."; +import { lib_nostr_event_sign, lib_nostr_event_sign_attest, lib_nostr_event_verify, lib_nostr_event_verify_serialized, lib_nostr_nevent_encode } from '../nostr/event'; +import type { INostrEventUtil, INostrEventUtilFormatTagsBasisNip99, INostrEventUtilNeventEncode, NostrEventTagClient, NostrEventTagLocation, NostrEventTagMediaUpload, NostrEventTagPrice, NostrEventTagQuantity } from "./types"; export class NostrEventUtil implements INostrEventUtil { public first_tag_value = (event: NDKEvent, tag_name: string): string => { @@ -76,38 +75,24 @@ export class NostrEventUtil implements INostrEventUtil { return tags; }; - public nostr_event_sign = (opts: INostrEventUtilEventSign): NostrToolsEvent => { - return finalizeEvent(opts.event, hexToBytes(opts.secret_key)) + public nostr_event_sign = (opts: INostrEventEventSign): NostrToolsEvent => { + return lib_nostr_event_sign(opts); }; public nostr_event_sign_attest = (secret_key: string): NostrToolsEvent => { - return this.nostr_event_sign({ - secret_key, - event: { - kind: 1, - created_at: Math.floor(Date.now() / 1000), - tags: [], - content: uuidv4(), - }, - }); + return lib_nostr_event_sign_attest(secret_key); }; public nostr_event_verify = (event: NostrToolsEvent): boolean => { - const hash = getEventHash(event); - if (hash !== event.id) return false - const valid = schnorr.verify(event.sig, hash, event.pubkey); - return valid; + return lib_nostr_event_verify(event); }; public nostr_event_verify_serialized = (event_serialized: string): boolean => { - const event = JSON.parse(event_serialized); - const hash = getEventHash(event); - if (hash !== event.id) return false - const valid = schnorr.verify(event.sig, hash, event.pubkey); - return valid; + const result = lib_nostr_event_verify_serialized(event_serialized); + return !!result; }; public nevent_encode = (opts: INostrEventUtilNeventEncode): string => { - return nip19.neventEncode(opts); + return lib_nostr_nevent_encode(opts); }; } \ No newline at end of file diff --git a/utils/src/nostr-event-util/types.ts b/utils/src/nostr-event-util/types.ts @@ -1,5 +1,6 @@ import { NDKEvent } from "@nostr-dev-kit/ndk"; -import { type NostrEvent as NostrToolsEvent, type EventTemplate as NostrToolsEventTemplate } from "nostr-tools"; +import { type NostrEvent as NostrToolsEvent } from "nostr-tools"; +import type { INostrEventEventSign } from ".."; export type INostrEventUtilFormatTagsBasisNip99 = { d_tag: string; @@ -18,15 +19,10 @@ export type INostrEventUtilNeventEncode = { kind: number; }; -export type INostrEventUtilEventSign = { - secret_key: string; - event: NostrToolsEventTemplate; -} - export type INostrEventUtil = { first_tag_value(event: NDKEvent, tag_name: string): string; fmt_tags_basis_nip99: (opts: INostrEventUtilFormatTagsBasisNip99) => string[][]; - nostr_event_sign: (opts: INostrEventUtilEventSign) => NostrToolsEvent; + nostr_event_sign: (opts: INostrEventEventSign) => NostrToolsEvent; nostr_event_sign_attest: (secret_key: string) => NostrToolsEvent; nostr_event_verify_serialized: (event_serialized: string) => boolean; nostr_event_verify: (event: NostrToolsEvent) => boolean; diff --git a/utils/src/nostr-key-util/lib.ts b/utils/src/nostr-key-util/lib.ts @@ -1,41 +1,25 @@ -import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; -import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools'; +import { getPublicKey, nip19 } from 'nostr-tools'; +import { lib_nostr_get_key_bytes, lib_nostr_key_generate, lib_nostr_nsec_decode, lib_nostr_nsec_encode } from '../nostr/key'; import type { INostrKeyUtil } from './types'; export class NostrKeyUtil implements INostrKeyUtil { - private generate_key_bytes(): Uint8Array { - const secret_key = generateSecretKey(); - return secret_key; - }; - - private get_key_hex(bytes: Uint8Array): string { - const hex = bytesToHex(bytes); - return hex; - }; - - private get_key_bytes(hex: string): Uint8Array { - const bytes = hexToBytes(hex); - return bytes; - }; - /** * * @returns nostr secret key hex */ public generate_key(): string { - const bytes = this.generate_key_bytes(); - const hex = this.get_key_hex(bytes); - return hex; + return lib_nostr_key_generate(); }; + /** * - * @returns nostr public key hex + * @returns nostr public key hex from secret key */ public public_key(secret_key_hex: string | undefined): string { try { if (!secret_key_hex) return ``; - const bytes = this.get_key_bytes(secret_key_hex); + const bytes = lib_nostr_get_key_bytes(secret_key_hex); const hex = getPublicKey(bytes) return hex; } catch (e) { @@ -50,7 +34,7 @@ export class NostrKeyUtil implements INostrKeyUtil { public publickey_decode(secret_key_hex?: string): string | undefined { try { if (secret_key_hex) { - return getPublicKey(this.get_key_bytes(secret_key_hex)) + return getPublicKey(lib_nostr_get_key_bytes(secret_key_hex)) } return undefined; } catch (e) { @@ -83,11 +67,8 @@ export class NostrKeyUtil implements INostrKeyUtil { * * @returns nostr secret key nsec */ - public nsec(secret_key_hex: string | undefined): string { - if (!secret_key_hex) return ``; - const bytes = this.get_key_bytes(secret_key_hex); - const nsec = nip19.nsecEncode(bytes); - return nsec; + public nsec(secret_key_hex?: string): string | undefined { + return lib_nostr_nsec_encode(secret_key_hex); } /** @@ -95,14 +76,7 @@ export class NostrKeyUtil implements INostrKeyUtil { * @returns nostr secret key hex from nsec */ public nsec_decode(nsec: string): string | undefined { - try { - if (!nsec) return undefined; - const decode = nip19.decode(nsec); - if (decode && decode.type === `nsec` && decode.data) return bytesToHex(decode.data); - return undefined; - } catch (e) { - return undefined; - } + return lib_nostr_nsec_decode(nsec); } /** diff --git a/utils/src/nostr-key-util/types.ts b/utils/src/nostr-key-util/types.ts @@ -3,7 +3,7 @@ export type INostrKeyUtil = { public_key(secret_key_hex: string | undefined): string; npub(public_key_hex: string | undefined): string; npub_decode(npub: string): string; - nsec(secret_key_hex: string | undefined): string; + nsec(secret_key_hex: string | undefined): string | undefined; nsec_decode(nsec: string): string | undefined; nprofile(public_key_hex: string, relays: string[]): string; nprofile_decode(nprofile: string): [string, string[]] | undefined; diff --git a/utils/src/nostr/event.ts b/utils/src/nostr/event.ts @@ -0,0 +1,45 @@ +import { schnorr } from "@noble/curves/secp256k1"; +import { hexToBytes } from "@noble/hashes/utils"; +import { finalizeEvent, getEventHash, nip19, type NostrEvent as NostrToolsEvent } from "nostr-tools"; +import { type INostrEventEventSign, type INostrEventUtilNeventEncode, uuidv4 } from ".."; + +export const lib_nostr_event_verify = (event: NostrToolsEvent): boolean => { + const hash = getEventHash(event); + if (hash !== event.id) return false + const valid = schnorr.verify(event.sig, hash, event.pubkey); + return valid; +}; + +export const lib_nostr_event_sign = (opts: INostrEventEventSign): NostrToolsEvent => { + return finalizeEvent(opts.event, hexToBytes(opts.secret_key)) +}; + +export const lib_nostr_event_sign_attest = (secret_key: string): NostrToolsEvent => { + return lib_nostr_event_sign({ + secret_key, + event: { + kind: 1, + created_at: Math.floor(Date.now() / 1000), + tags: [], + content: uuidv4(), + }, + }); +}; + + +export const lib_nostr_event_verify_serialized = async (event_serialized: string): Promise<{ public_key: string } | undefined> => { + try { + const event = JSON.parse(event_serialized); + const hash = getEventHash(event); + if (hash !== event.id) return undefined; + const valid = schnorr.verify(event.sig, hash, event.pubkey); + if (valid) return { public_key: String(event.pubkey) }; + return undefined; + } catch { + return undefined; + } +}; + +export const lib_nostr_nevent_encode = (opts: INostrEventUtilNeventEncode): string => { + return nip19.neventEncode(opts); +}; +\ No newline at end of file diff --git a/utils/src/nostr/key.ts b/utils/src/nostr/key.ts @@ -0,0 +1,42 @@ +import { bytesToHex, hexToBytes } from "@noble/hashes/utils"; +import { generateSecretKey, getPublicKey, nip19 } from "nostr-tools"; + +export const lib_nostr_get_key_bytes = (hex: string): Uint8Array => { + return hexToBytes(hex); +}; + +export const lib_nostr_get_key_hex = (bytes: Uint8Array): string => { + return bytesToHex(bytes); +}; + +export const lib_nostr_key_generate = (): string => { + const bytes = generateSecretKey(); + return lib_nostr_get_key_hex(bytes); +}; + +export const lib_nostr_nsec_encode = (secret_key_hex?: string): string | undefined => { + if (!secret_key_hex) return undefined; + const bytes = lib_nostr_get_key_bytes(secret_key_hex); + return nip19.nsecEncode(bytes); +}; + +export const lib_nostr_nsec_decode = (nsec?: string): string | undefined => { + if (!nsec) return undefined; + const decode = nip19.decode(nsec); + if (decode && decode.type === `nsec` && decode.data) return bytesToHex(decode.data); + return undefined; +}; + + +export const lib_nostr_public_key = (secret_key_hex: string): string => { + const bytes = lib_nostr_get_key_bytes(secret_key_hex); + return getPublicKey(bytes); +}; + +export const lib_nostr_secret_key_validate = (secret_key: string): string | undefined => { + const is_valid_hex = lib_nostr_public_key(secret_key); + if (is_valid_hex) return secret_key; + const is_valid_nsec = lib_nostr_nsec_decode(secret_key); + if (is_valid_nsec) return is_valid_nsec; + return undefined; +}; +\ No newline at end of file diff --git a/utils/src/nostr/ndk.ts b/utils/src/nostr/ndk.ts @@ -0,0 +1,75 @@ +import NDK, { NDKEvent, NDKPrivateKeySigner, NDKUser } from '@nostr-dev-kit/ndk'; +import { time_now_ms } from '..'; +import type { NostrMetadataTmp } from './types'; + +export const ndk_init = async (opts: { + $ndk: NDK; + secret_key: string; +}): Promise<NDKUser | undefined> => { + try { + const { $ndk: ndk, secret_key } = opts; + const signer = new NDKPrivateKeySigner(secret_key); + ndk.signer = signer; + + const user = await signer.user(); + if (user) { + user.ndk = ndk; + return user; + } + } catch (e) { + console.log(`(error) ndk_init `, e); + }; +}; + +export const ndk_event_metadata = async (opts: { + $ndk: NDK; + $ndk_user: NDKUser; + metadata: NostrMetadataTmp +}): Promise<NDKEvent | undefined> => { + try { + const { $ndk, $ndk_user } = opts; + const ev = await ndk_event({ + $ndk, + $ndk_user, + basis: { + kind: 0, + content: JSON.stringify(opts.metadata), + }, + }); + return ev; + } catch (e) { + console.log(`(error) ndk_event_metadata `, e); + } +}; + +export const ndk_event = async (opts: { + $ndk: NDK; + $ndk_user: NDKUser; + basis: { + kind: number; + content: string; + tags?: string[][]; + } +}): Promise<NDKEvent | undefined> => { + try { + const { $ndk: ndk, $ndk_user: ndk_user, basis } = opts; + const time_now = time_now_ms(); + + const tags: string[][] = [ + ['published_at', time_now.toString()], + ]; + + if (basis.tags && basis.tags?.length > 0) for (const tag of basis.tags) tags.push(tag); + + const event: NDKEvent = new NDKEvent(ndk, { + kind: basis.kind, + pubkey: ndk_user.pubkey, + content: basis.content, + created_at: time_now, + tags + }); + return event; + } catch (e) { + console.log(`(error) ndk_event `, e); + }; +}; diff --git a/utils/src/nostr/types.ts b/utils/src/nostr/types.ts @@ -0,0 +1,19 @@ +import { type EventTemplate as NostrToolsEventTemplate } from "nostr-tools"; + +export type NostrMetadataTmp = { + name?: string; + display_name?: string; + about?: string; + website?: string; + picture?: string; + banner?: string; + nip05?: string; + lud06?: string; + lud16?: string; + bot?: boolean; +}; + +export type INostrEventEventSign = { + secret_key: string; + event: NostrToolsEventTemplate; +} +\ No newline at end of file diff --git a/utils/src/object.ts b/utils/src/object.ts @@ -1,3 +1,17 @@ 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; }; \ No newline at end of file diff --git a/utils/src/types.ts b/utils/src/types.ts @@ -3,12 +3,14 @@ import type { GeolocationCoordinatesPoint } from "./geolocation"; export type ErrorResponse<T extends object> = { error: T; }; export type ErrorMessage<T extends string> = { err: T }; +export type FieldRecord = Record<string, string>; + export type ResultId = { id: string; }; export type ResultPass = { pass: true; }; export type ResultsList<T> = { results: T[]; }; export type ResultObj<T> = { result: T; }; - -export type FieldRecord = Record<string, string>; +export type ResultPublicKey = { public_key: string; }; +export type ResultSecretKey = { secret_key: string; }; export type LocationPoint = GeolocationCoordinatesPoint & { lat: number; @@ -21,3 +23,10 @@ export type NumberTuple = [number, number]; export type FileBytesFormat = `kb` | `mb` | `gb`; export type FileMimeType = string; export type FilePath = { file_path: string; file_name: string; mime_type: FileMimeType; } + + +export type NotifyMessage = { + message: string; + ok?: string; + cancel?: string; +}; +\ No newline at end of file diff --git a/utils/src/window.ts b/utils/src/window.ts @@ -1,4 +1,13 @@ export type AppLayoutKey = 'mobile_base' | 'mobile_y'; +export type AppLayout<T1 extends string, T2 extends string> = `${T1}_${T2}`; +export type AppHeightsResponsiveKey = + | `tabs` + | `nav` + | `lo_view` + | `view` + | `view_offset` + | `trellis_centered`; +export type AppHeightsResponsive = AppLayout<AppHeightsResponsiveKey, AppLayoutKey>; type ClientWindow = { app: {