Utilities Reference
Introduction
This page documents every public class, method, and type exported from libs/upp-defs/src/app.utils.ts. These utilities solve recurring problems that appear across the entire monorepo: generating unique identifiers, computing checksums, converting between data formats, calculating prices with tax breakdowns, validating Spanish tax IDs, and serialising concurrent async operations.
Because upp-defs has zero internal dependencies, these utilities are available everywhere -- from data objects in upp-data to feature components in libs/feature/* -- without pulling in Angular, HTTP clients, or any heavyweight framework code.
import {
GenericUtils, PriceInfo, PriceType,
TaxIdValidators, GlobalMutex
} from '@unpispas/upp-defs';
When to use these utilities
| You need to... | Use |
|---|---|
| Generate a unique identifier for a new data object | GenericUtils.uuidv4() |
| Compute a checksum for change detection or token generation | GenericUtils.CRC32() or GenericUtils.simpleHash() |
Deep-clone an object (including Date instances) | GenericUtils.CopyObject() |
| Normalise a string for comparison or search indexing | GenericUtils.Normalize() |
| Convert between base64 and binary formats | GenericUtils.stringToBase64(), base64ToString(), and friends |
Convert between JavaScript Date and MySQL datetime strings | GenericUtils.mysqlToDateStr(), dateStrToMysql() |
| Resize and encode an image as base64 | GenericUtils.imageTobase64() |
| Calculate a price with multiple tax rates and discounts | PriceInfo |
| Validate a Spanish DNI, NIE, or CIF | TaxIdValidators.ValidateTaxId() |
| Serialise async operations by key to prevent races | GlobalMutex |
GenericUtils
A static utility class with zero mutable state (except internal caches for performance). Every method is public static -- you never instantiate GenericUtils.
UUID generation
uuidv4()
static uuidv4(): string
Generates a random v4 UUID in the standard xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx format. Uses Math.random() for entropy.
Problem it solves: Every data object in the system (products, tickets, places, etc.) needs a unique identifier that can be generated client-side without a server round-trip. UUIDs are used as primary keys (objid) in both the local model and the backend database.
Where it is used: The BaseObject class in upp-data assigns a UUID to every new object via its _uuid getter. This means every product, ticket, discount, or place created in the frontend gets a globally unique ID before it ever reaches the server.
// BaseObject in upp-data uses this for every new data object
get _uuid(): string {
if (!this.__uuid) {
this.__uuid = GenericUtils.uuidv4();
}
return this.__uuid;
}
// Direct usage
const newId = GenericUtils.uuidv4();
// "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
Checksums and hashing
CRC32(str)
static CRC32(str: string | null): number
Calculates the CRC-32 checksum of a string. Returns an unsigned 32-bit integer. The 256-entry lookup table is lazily built on the first call and reused for all subsequent calls.
Problem it solves: The application needs fast, deterministic checksums for several purposes -- generating authentication tokens, creating cache keys, and detecting data changes. CRC32 is not cryptographically secure, but it is fast and has good distribution for these use cases.
Where it is used:
- Login tokens: The login module generates session tokens by computing
CRC32(session + device)and formatting the result as a hex string. - Password encoding: Passwords are hashed client-side using chained CRC32 computations before being sent to the server.
- Data fingerprinting: Used in the sync module to create compact identifiers for change detection.
Edge cases:
- If
strisnull, it is treated as an empty string (''). - The result is always an unsigned 32-bit integer (range:
0to4294967295).
// Generate a session token from session + device IDs
const token = '_' + GenericUtils.CRC32(sessionId + deviceId).toString(16);
// e.g. "_3f2a1b4c"
// Password encoding (chained CRC32)
const hash1 = GenericUtils.CRC32(password).toString(16);
const hash2 = GenericUtils.CRC32(hash1).toString(16);
// Null handling
GenericUtils.CRC32(null); // same as CRC32('')
simpleHash(str)
static simpleHash(str: string): string
Produces a fast, non-cryptographic hash by computing a shift-based hash on both the original string and its reverse, then encoding each as a zero-padded base-36 string. The result is a 14-character string (two 7-character segments).
Problem it solves: Data objects need a compact fingerprint to detect whether their content has changed. simpleHash is faster than CRC32 for this purpose and produces a string that can be compared directly (no need for number-to-string conversion).
Where it is used: The BaseObject class in upp-data computes simpleHash(JSON.stringify(info)) to create a fingerprint of the object's current state. This fingerprint is compared on each sync cycle to determine whether the object has unsaved changes.
// In BaseObject: detect changes by comparing hashes
this._simplehash = GenericUtils.simpleHash(JSON.stringify(objectInfo));
// Later, check if the object has changed
const currentHash = GenericUtils.simpleHash(JSON.stringify(this.Info));
const hasChanged = currentHash !== this._simplehash;
String helpers
Normalize(str)
static Normalize(str: string | null): string | null
Normalises a string for use in comparisons and search indexing:
- Strips diacritics (decomposes via NFD, removes combining marks).
- Converts to lowercase.
- Replaces all whitespace with underscores.
Results are memoised in an internal Map, so repeated calls with the same input are essentially free after the first computation.
Problem it solves: When searching for products or comparing names, accented characters and case differences should not prevent matches. "Cafe" should match "Cafe", "CAFE", and "cafe". The memoisation is important because normalisation happens on every keystroke during search.
Edge cases:
- Returns
nullif the input isnullor an empty string (falsy). - The memoisation cache grows unboundedly. In practice this is fine because the number of unique strings in a POS catalog is small.
GenericUtils.Normalize('Cafe con Leche'); // "cafe_con_leche"
GenericUtils.Normalize('MENU DEL DIA'); // "menu_del_dia"
GenericUtils.Normalize(null); // null
GenericUtils.Normalize(''); // null
VerboseLogging(level)
static VerboseLogging(level: number): boolean
Returns true when level < 4. Used as a gate for console output -- only messages at level 3 or below are logged.
if (GenericUtils.VerboseLogging(debugLevel)) {
console.log('[DEBUG]', message);
}
DelayedRefresh()
static DelayedRefresh(): boolean
Always returns true (the production value). Controls whether view refreshes are deferred to the next clock tick rather than applied immediately. This batches multiple rapid updates into a single render cycle, improving performance.
Deep copy
CopyObject(obj)
static CopyObject(obj: any): any
Creates a recursive deep clone of an object. Handles three special cases:
- Primitives and
null-- returned as-is. Dateinstances -- creates a newDatewith the same timestamp (prevents the common bug where cloned objects share the sameDatereference).- Arrays and plain objects -- recursively clones each property.
Problem it solves: JavaScript's object assignment and spread operator create shallow copies. When a data object contains nested objects or Date fields (which is extremely common in this codebase -- tickets, products, and events all have timestamps), a shallow copy leads to shared references that cause hard-to-debug mutation bugs. CopyObject guarantees complete isolation.
Where it is used:
- Snapshotting data objects:
BaseObjectusesCopyObjectto create an independent copy of an object's data before sending it to the sync queue, ensuring later local edits do not corrupt the queued version. - Duplicating items: When a user duplicates a product or ticket line,
CopyObjectcreates the clone.
// Snapshot an object's data for sync
const snapshot = GenericUtils.CopyObject(this.Info);
// Clone a complex nested object
const original = {
name: 'Product A',
prices: [{ rate: 21, amount: 100 }],
created: new Date()
};
const clone = GenericUtils.CopyObject(original);
clone.prices[0].amount = 200;
console.log(original.prices[0].amount); // still 100
clone.created.setFullYear(2000);
console.log(original.created.getFullYear()); // unchanged
Edge case: This does not handle Map, Set, RegExp, or circular references. The data objects in this codebase use plain objects and arrays, so this is not a limitation in practice.
Base64 / ArrayBuffer conversion
These methods convert between binary representations (Uint8Array, ArrayBuffer) and base64 strings. They are essential for handling binary data in a web environment where many APIs (localStorage, JSON, HTTP bodies) only accept strings.
| Method | From | To | Notes |
|---|---|---|---|
uint8ArrayToBase64(bytes) | Uint8Array | base64 string | Iterates over bytes and uses window.btoa. |
base64ToUint8Array(base64) | base64 string | Uint8Array | Uses window.atob and fills a Uint8Array. |
arrayBufferToBase64(buffer) | ArrayBuffer | base64 string | Wraps buffer in Uint8Array, then same as above. |
base64ToArrayBuffer(base64) | base64 string | ArrayBuffer | Returns .buffer from the resulting Uint8Array. |
stringToBase64(str) | UTF-8 string | base64 string or null | Uses TextEncoder to get bytes, then arrayBufferToBase64. Returns null on failure. |
base64ToString(base64) | base64 string | UTF-8 string or null | Uses base64ToArrayBuffer, then TextDecoder. Returns null on failure. |
Problem they solve: The application handles binary data in several contexts -- push notification VAPID keys (must be Uint8Array), encrypted payloads, file uploads encoded as base64. These methods provide a consistent conversion layer instead of repeating atob/btoa boilerplate with manual byte manipulation.
// Encode a string payload as base64 for transport
const encoded = GenericUtils.stringToBase64('UnPisPas rocks');
// "VW5QaXNQYXMgcm9ja3M="
// Decode it back
const decoded = GenericUtils.base64ToString(encoded!);
// "UnPisPas rocks"
// Convert a VAPID key (base64) to Uint8Array for push subscription
const vapidBytes = GenericUtils.base64ToUint8Array(vapidKeyBase64);
Image conversion
imageTobase64(url, format?, resize?, quality?)
static imageTobase64(
url: string,
format: string = 'image/png',
resize: number = 1.0,
quality: number = 0.8
): Promise<string>
Loads an image from a URL, draws it on an off-screen canvas (optionally resized), and returns a base64 data URL. The image is loaded with crossOrigin = "Anonymous" to allow cross-origin canvas reads.
Problem it solves: Product images uploaded to the server need to be displayed in the catalog, printed on receipts, and sometimes stored locally for offline access. This method handles the full pipeline: fetch, resize, re-encode as base64.
Parameters:
url-- the image source URL.format-- output MIME type (default'image/png'). Use'image/jpeg'for smaller file sizes with photographic content.resize-- scale factor (default1.0). Use0.5to halve dimensions, reducing file size by ~75%.quality-- compression quality for lossy formats like JPEG (default0.8). Ignored for PNG.
Edge cases:
- Rejects with an
Errorif the image fails to load (network error, 404, CORS block). - Requires a DOM environment (
document.createElement,Image). Does not work in Node.js/SSR.
// Load and convert a product image at half size, as JPEG
try {
const dataUrl = await GenericUtils.imageTobase64(
AppConstants.baseURL + AppConstants.uploadPath + 'product.jpg',
'image/jpeg',
0.5,
0.7
);
// dataUrl is "data:image/jpeg;base64,/9j/4AAQ..."
} catch (err) {
console.error('Failed to load image:', err.message);
}
MySQL date conversion
The backend stores all dates in MySQL's YYYY-MM-DD HH:MM:SS format in UTC. The frontend works with JavaScript Date objects and ISO 8601 strings. These methods bridge the gap.
mysqlToDateStr(date)
static mysqlToDateStr(date: string | null): string | null
Converts a MySQL datetime string ("2024-12-19 12:34:56") to an ISO 8601 UTC string ("2024-12-19T12:34:56.000Z"). This can then be passed to new Date() for parsing.
Where it is used: Every time the sync module receives data from the backend, timestamps arrive in MySQL format and must be converted to ISO 8601 before being parsed into Date objects.
const mysql = '2024-12-19 14:30:00';
const iso = GenericUtils.mysqlToDateStr(mysql);
// "2024-12-19T14:30:00.000Z"
const date = new Date(iso!);
// Thu Dec 19 2024 14:30:00 UTC
dateStrToMysql(date)
static dateStrToMysql(date: Date | null | undefined): string | null
Formats a JavaScript Date object as a MySQL UTC datetime string ("2024-12-19 14:30:00"). All components are zero-padded to two digits.
Where it is used: When sending events or updates to the backend, timestamps must be formatted as MySQL datetime strings. The events module uses this for the created field of every event.
const now = new Date();
const mysql = GenericUtils.dateStrToMysql(now);
// "2024-12-19 14:30:00"
GenericUtils.dateStrToMysql(null); // null
GenericUtils.dateStrToMysql(undefined); // null
uploadToMysql(file)
static uploadToMysql(file: string | null): string | null
Extracts the filename from a full file path, stripping the directory portion. Used when saving a file reference to the database -- the database stores just the filename, not the full URL.
GenericUtils.uploadToMysql('/uploads/images/photo_123.jpg');
// "photo_123.jpg"
GenericUtils.uploadToMysql('photo_123.jpg');
// "photo_123.jpg" (no directory to strip)
GenericUtils.uploadToMysql(null);
// null
mysqlToUpload(file)
static mysqlToUpload(file: string | null): string | null
The reverse of uploadToMysql: takes a filename stored in the database and builds the full upload URL by prepending AppConstants.baseURL + AppConstants.uploadPath.
GenericUtils.mysqlToUpload('photo_123.jpg');
// "https://api.unpispas.com/upload/photo_123.jpg"
GenericUtils.mysqlToUpload(null);
// null
TaxIdValidators
A static class for validating Spanish tax identification numbers. Spain uses three types of tax identifiers:
- DNI (Documento Nacional de Identidad) -- for individual citizens. Format: 8 digits + 1 letter (e.g.
12345678Z). - NIE (Numero de Identidad de Extranjero) -- for foreign residents. Format: letter (
X/Y/Z) + 7 digits + 1 letter (e.g.X1234567L). - CIF (Certificado de Identificacion Fiscal) -- for legal entities (companies). Format: letter + 7 digits + control character (e.g.
B70659198).
Problem it solves: The application collects tax IDs when generating invoices (required by Spanish law for invoices over 3000 EUR) and during business registration. Invalid tax IDs cause fiscal submissions to be rejected. Client-side validation prevents this by catching errors before the data reaches the backend.
Methods
ToTaxId(taxid)
static ToTaxId(taxid: string): string | null
Normalises a raw tax ID input: converts to uppercase and strips all non-alphanumeric characters. Returns null if the input is falsy.
This is useful for cleaning user input before validation or storage. Users often type tax IDs with dashes, dots, or spaces that need to be removed.
TaxIdValidators.ToTaxId(' b-7065.9198 '); // "B70659198"
TaxIdValidators.ToTaxId('12.345.678-z'); // "12345678Z"
TaxIdValidators.ToTaxId(''); // null
TaxIdValidators.ToTaxId(null as any); // null
ValidateTaxId(value)
static ValidateTaxId(value: string | null): boolean
Validates a Spanish tax ID (DNI, NIE, or CIF). Internally normalises the input via ToTaxId and then runs both the DNI and CIF validation algorithms. Returns true if the identifier passes either validation.
Internal algorithms:
- DNI validation: The last letter is a check digit computed by
number % 23mapped to the stringTRWAGMYFPDXBNJZSQVHLCKE. If the letter matches, the DNI is valid. - NIE handling: The prefix letter is converted to a number (
X->'',Y->1,Z->2) and then validated as a DNI. - CIF type 1: A weighted digit-sum algorithm where even-position digits are doubled (with digit-sum reduction if >9). The control character is either a digit or a letter depending on the CIF prefix (
ABEH-> digit,NPQRSW-> letter, others -> either). - CIF type 2: An alternate weighted algorithm for additional CIF formats.
// Valid identifiers
TaxIdValidators.ValidateTaxId('12345678Z'); // true (DNI)
TaxIdValidators.ValidateTaxId('B70659198'); // true (CIF)
TaxIdValidators.ValidateTaxId('X1234567L'); // true (NIE)
// Invalid identifiers
TaxIdValidators.ValidateTaxId('12345678A'); // false (wrong check letter)
TaxIdValidators.ValidateTaxId('B12345678'); // false (wrong control digit)
TaxIdValidators.ValidateTaxId(null); // false
TaxIdValidators.ValidateTaxId(''); // false
// Works with messy input (normalises internally)
TaxIdValidators.ValidateTaxId(' b-7065.9198 '); // true
Common pattern: validate before submission
function onInvoiceSubmit(taxId: string): boolean {
const normalised = TaxIdValidators.ToTaxId(taxId);
if (!normalised) {
showError('Tax ID is required');
return false;
}
if (!TaxIdValidators.ValidateTaxId(normalised)) {
showError('Invalid tax ID format');
return false;
}
submitInvoice({ taxId: normalised, ... });
return true;
}
GlobalMutex
A key-based asynchronous mutex. Each key has an independent lock queue, so locking "resource1" does not block "resource2". This is an instance class (not static) -- you create a GlobalMutex and share it across the code that needs synchronisation.
Problem it solves: JavaScript is single-threaded but asynchronous. When multiple async operations read and write the same resource (e.g. sync queue, local database, shared state), interleaving can corrupt data. A mutex serialises access so that only one async operation at a time can execute the critical section for a given key.
Methods
lock(key)
async lock(key: string): Promise<void>
Acquires the lock for the given key. If the lock is not held, it resolves immediately. If the lock is already held by another caller, the returned promise suspends until the lock is released. Callers are served in FIFO order.
unlock(key)
unlock(key: string): void
Releases the lock for the given key. If other callers are queued, the next one in line is unblocked immediately (the lock ownership transfers without an intermediate "unlocked" state). If no callers are queued, the lock becomes available.
Usage pattern
Always use try/finally to ensure the lock is released even if the critical section throws:
const mutex = new GlobalMutex();
async function updateResource(key: string, data: any) {
await mutex.lock(key);
try {
const current = await readFromDatabase(key);
const merged = merge(current, data);
await writeToDatabase(key, merged);
} finally {
mutex.unlock(key);
}
}
How the queue works internally
The mutex maintains a Map<string, { locked: boolean, queue: (() => void)[] }>. Each key gets its own entry:
- First caller calls
lock("A"). The lock is not held, solockedis set totrueand the promise resolves immediately. - Second caller calls
lock("A")while the first still holds it.lockedistrue, so the resolve callback is pushed onto the queue. The promise suspends. - First caller calls
unlock("A"). The queue is not empty, so the first queued resolve callback is shifted off and called. The second caller's promise resolves.lockedstaystrue(ownership transferred). - Second caller calls
unlock("A"). The queue is empty, solockedis set tofalse.
Crucially, locking "A" and "B" are completely independent:
// These run concurrently -- different keys, different lock queues
await Promise.all([
updateResource('tickets', ticketData),
updateResource('products', productData)
]);
// These run sequentially -- same key, serialised by the mutex
updateResource('tickets', data1);
updateResource('tickets', data2);
PriceInfo
A class that accumulates price amounts with their tax rates, applies proportional discounts, and produces a fully-computed PriceType result. This is the core price calculation engine of the application, used by the ticket system to compute totals, tax breakdowns, and discount distributions for every ticket.
Problem it solves: A POS ticket can contain products at different tax rates (4%, 10%, 21%), extras (delivery charges, service fees), and discounts. The discounts must be distributed proportionally across tax groups -- you cannot simply subtract a discount from the total without adjusting the tax breakdown, because that would produce incorrect fiscal data. PriceInfo handles all of this correctly.
Constructor
new PriceInfo(total?: number, rate?: number)
| Parameter | Default | Description |
|---|---|---|
total | 0 | Initial amount (tax-inclusive). |
rate | AppConstants.defaultTaxRate (10) | Tax rate for the initial amount as a percentage. |
The constructor calls Add(total, rate) internally, so new PriceInfo(100, 21) is equivalent to creating an empty PriceInfo and then calling Add(100, 21).
Methods
Add(amount, taxrate)
Add(amount: number, taxrate: number): void
Adds a tax-inclusive amount at the given rate. If multiple amounts are added with the same tax rate, they are aggregated into a single tax group. This means Add(50, 21) followed by Add(30, 21) produces one group of 80 at 21%, not two separate groups.
Del(amount)
Del(amount: number): void
Queues a discount. Discounts are not applied immediately -- they are stored and applied when the price getter is accessed. This deferred application is important: it ensures all prices are added before the proportional distribution is calculated.
Multiple discounts are applied sequentially in FIFO order. Each discount is distributed proportionally across tax groups based on their share of the remaining total at that point.
price getter
get price(): PriceType
Computes and returns the final price information. The computation works as follows:
- Clone the internal tax map to avoid mutating it (so
pricecan be called multiple times). - Apply discounts in order. For each discount, distribute the amount across tax groups proportionally to each group's share of the current total. Each group's total is reduced by its share of the discount (capped at the group's remaining amount).
- Compute base and tax for each group:
base = total / (1 + rate/100),taxes = total - base. - Aggregate totals, base amounts, and tax amounts.
- Format the result: integer part, decimal part, comma-separated string.
- Round all monetary values to 2 decimal places.
How proportional discount distribution works
Suppose a ticket has:
- 100 EUR at 21% VAT
- 50 EUR at 10% VAT
- A 30 EUR discount
The discount is distributed by each group's weight in the total:
- 21% group weight: 100 / 150 = 66.67%
- 10% group weight: 50 / 150 = 33.33%
So the 30 EUR discount becomes:
- 20 EUR off the 21% group (leaving 80 EUR)
- 10 EUR off the 10% group (leaving 40 EUR)
This ensures the tax breakdown remains correct for fiscal reporting.
PriceType
The shape returned by PriceInfo.price:
| Field | Type | Description |
|---|---|---|
val | number | Total price after discounts (can be negative for full refunds). |
neg | boolean | true if the total is negative. |
abs | number | Absolute value of the total (always >= 0). |
int | string | Integer part of abs as a string (e.g. "123"). |
dec | string | Decimal part of abs as a string, zero-padded to 2 digits (e.g. "45", "00"). |
str | string | Formatted string with comma as decimal separator (e.g. "123,45"). Uses the European convention because the primary market is Spain. |
tax | TaxesInfo | Aggregated tax breakdown of the final amount (after discounts). |
dsc | TaxesInfo | Aggregated tax breakdown of the discount amount. |
The int and dec fields are useful for UI rendering where the integer and decimal parts are styled differently (e.g. the decimal part in a smaller font on a receipt).
TaxesInfo
| Field | Type | Description |
|---|---|---|
total | number | Total amount (including taxes), rounded to 2 decimal places. |
base | number | Sum of base amounts (excluding taxes), rounded to 2 decimal places. |
taxes | number | Sum of tax amounts, rounded to 2 decimal places. |
split | TaxesSplit[] | Per-rate breakdown -- one entry for each distinct tax rate. |
TaxesSplit
| Field | Type | Description |
|---|---|---|
total | number | Total for this rate group (tax-inclusive). |
rate | number | Tax rate as a percentage (e.g. 21). |
base | number | Base amount for this rate group (tax-exclusive). |
taxes | number | Tax amount for this rate group. |
Usage examples
Simple product price
// A single product at 21% VAT
const price = new PriceInfo(12.10, 21);
const info = price.price;
console.log(info.str); // "12,10"
console.log(info.tax.base); // 10.00
console.log(info.tax.taxes); // 2.10
Ticket with multiple products and a discount
This mirrors how TicketView._PriceInfo() works in the real codebase:
const price = new PriceInfo();
// Add products (tax-inclusive amounts)
price.Add(15.00, 10); // Salad at 10% VAT
price.Add(22.00, 10); // Main course at 10% VAT
price.Add(8.00, 21); // Wine at 21% VAT
price.Add(3.50, 21); // Coffee at 21% VAT
// Apply a 10 EUR discount (e.g. a promotional offer)
price.Del(10);
const info = price.price;
console.log(info.val); // 38.50
console.log(info.str); // "38,50"
console.log(info.tax.total); // 38.50
console.log(info.tax.base); // ~34.83
console.log(info.tax.taxes); // ~3.67
console.log(info.tax.split.length); // 2 (one for 10%, one for 21%)
// The discount is also broken down by tax rate
console.log(info.dsc.total); // 10.00
console.log(info.dsc.split.length); // 2
Quick price display in a component
// In a product component, get a formatted price for display
const priceInfo = new PriceInfo(product.charge, product.taxrate || AppConstants.defaultTaxRate).price;
// Use in a template: "12,50 EUR"
this.displayPrice = priceInfo.str + ' EUR';
// Or split integer and decimal for styled display
// <span class="price-int">12</span><span class="price-dec">,50</span>
this.priceInt = priceInfo.int;
this.priceDec = priceInfo.dec;
Checking if an invoice is required
const ticketPrice = new PriceInfo();
// ... add products, extras, discounts ...
const info = ticketPrice.price;
if (info.val >= AppConstants.invoiceMaxPrice) {
// Spanish law requires a full invoice with customer tax ID
requireCustomerTaxId();
}
Common patterns across the codebase
Pattern: data object creation with UUID
Every new data object created client-side gets a UUID from GenericUtils.uuidv4(). This happens automatically through BaseObject:
// You rarely call uuidv4() directly -- BaseObject handles it
const product = new Product();
console.log(product._uuid); // "a1b2c3d4-e5f6-4a7b-8c9d-..."
Pattern: change detection with simpleHash
Data objects track whether they have unsaved changes by comparing hash fingerprints:
// After loading from server
this._savedHash = GenericUtils.simpleHash(JSON.stringify(this.Info));
// Before sync: has anything changed?
const currentHash = GenericUtils.simpleHash(JSON.stringify(this.Info));
if (currentHash !== this._savedHash) {
// Queue for sync
}
Pattern: MySQL date round-trip
When data flows between the frontend and backend, dates are converted at the boundary:
// Receiving from backend (sync response)
const jsDate = new Date(GenericUtils.mysqlToDateStr(row.updated)!);
// Sending to backend (event creation)
const event = {
created: GenericUtils.dateStrToMysql(new Date()),
// "2024-12-19 14:30:00"
};
Pattern: file URL resolution
When displaying images stored in the database:
// Database stores just the filename: "photo_123.jpg"
// Frontend needs the full URL
const displayUrl = GenericUtils.mysqlToUpload(row.image);
// "https://api.unpispas.com/upload/photo_123.jpg"
// When saving, strip back to just the filename
const dbValue = GenericUtils.uploadToMysql(fullUrl);
// "photo_123.jpg"
Related
- upp-defs Overview -- what the library provides and where it sits in the dependency graph
- AppConstants Reference -- the constants used by
PriceInfo(tax rates,defaultTaxRate,invoiceMaxPrice) andmysqlToUpload(URLs) - Ticket System -- the primary consumer of
PriceInfo - Data Objects -- uses
uuidv4,CopyObject,simpleHash, and date conversion - Sync and Cache -- uses
CRC32for tokens andmysqlToDateStrfor timestamps