Skip to main content

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 objectGenericUtils.uuidv4()
Compute a checksum for change detection or token generationGenericUtils.CRC32() or GenericUtils.simpleHash()
Deep-clone an object (including Date instances)GenericUtils.CopyObject()
Normalise a string for comparison or search indexingGenericUtils.Normalize()
Convert between base64 and binary formatsGenericUtils.stringToBase64(), base64ToString(), and friends
Convert between JavaScript Date and MySQL datetime stringsGenericUtils.mysqlToDateStr(), dateStrToMysql()
Resize and encode an image as base64GenericUtils.imageTobase64()
Calculate a price with multiple tax rates and discountsPriceInfo
Validate a Spanish DNI, NIE, or CIFTaxIdValidators.ValidateTaxId()
Serialise async operations by key to prevent racesGlobalMutex

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 str is null, it is treated as an empty string ('').
  • The result is always an unsigned 32-bit integer (range: 0 to 4294967295).
// 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:

  1. Strips diacritics (decomposes via NFD, removes combining marks).
  2. Converts to lowercase.
  3. 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 null if the input is null or 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:

  1. Primitives and null -- returned as-is.
  2. Date instances -- creates a new Date with the same timestamp (prevents the common bug where cloned objects share the same Date reference).
  3. 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: BaseObject uses CopyObject to 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, CopyObject creates 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.

MethodFromToNotes
uint8ArrayToBase64(bytes)Uint8Arraybase64 stringIterates over bytes and uses window.btoa.
base64ToUint8Array(base64)base64 stringUint8ArrayUses window.atob and fills a Uint8Array.
arrayBufferToBase64(buffer)ArrayBufferbase64 stringWraps buffer in Uint8Array, then same as above.
base64ToArrayBuffer(base64)base64 stringArrayBufferReturns .buffer from the resulting Uint8Array.
stringToBase64(str)UTF-8 stringbase64 string or nullUses TextEncoder to get bytes, then arrayBufferToBase64. Returns null on failure.
base64ToString(base64)base64 stringUTF-8 string or nullUses 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 (default 1.0). Use 0.5 to halve dimensions, reducing file size by ~75%.
  • quality -- compression quality for lossy formats like JPEG (default 0.8). Ignored for PNG.

Edge cases:

  • Rejects with an Error if 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 % 23 mapped to the string TRWAGMYFPDXBNJZSQVHLCKE. 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:

  1. First caller calls lock("A"). The lock is not held, so locked is set to true and the promise resolves immediately.
  2. Second caller calls lock("A") while the first still holds it. locked is true, so the resolve callback is pushed onto the queue. The promise suspends.
  3. 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. locked stays true (ownership transferred).
  4. Second caller calls unlock("A"). The queue is empty, so locked is set to false.

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)
ParameterDefaultDescription
total0Initial amount (tax-inclusive).
rateAppConstants.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:

  1. Clone the internal tax map to avoid mutating it (so price can be called multiple times).
  2. 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).
  3. Compute base and tax for each group: base = total / (1 + rate/100), taxes = total - base.
  4. Aggregate totals, base amounts, and tax amounts.
  5. Format the result: integer part, decimal part, comma-separated string.
  6. 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:

FieldTypeDescription
valnumberTotal price after discounts (can be negative for full refunds).
negbooleantrue if the total is negative.
absnumberAbsolute value of the total (always >= 0).
intstringInteger part of abs as a string (e.g. "123").
decstringDecimal part of abs as a string, zero-padded to 2 digits (e.g. "45", "00").
strstringFormatted string with comma as decimal separator (e.g. "123,45"). Uses the European convention because the primary market is Spain.
taxTaxesInfoAggregated tax breakdown of the final amount (after discounts).
dscTaxesInfoAggregated 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

FieldTypeDescription
totalnumberTotal amount (including taxes), rounded to 2 decimal places.
basenumberSum of base amounts (excluding taxes), rounded to 2 decimal places.
taxesnumberSum of tax amounts, rounded to 2 decimal places.
splitTaxesSplit[]Per-rate breakdown -- one entry for each distinct tax rate.

TaxesSplit

FieldTypeDescription
totalnumberTotal for this rate group (tax-inclusive).
ratenumberTax rate as a percentage (e.g. 21).
basenumberBase amount for this rate group (tax-exclusive).
taxesnumberTax 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"

  • 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) and mysqlToUpload (URLs)
  • Ticket System -- the primary consumer of PriceInfo
  • Data Objects -- uses uuidv4, CopyObject, simpleHash, and date conversion
  • Sync and Cache -- uses CRC32 for tokens and mysqlToDateStr for timestamps