@freshpointcz/fresh-core
v0.0.24
Published
Contains core mechanisms for our custom framework
Keywords
Readme
@freshpointcz/fresh-core
Core framework mechanisms shared by every FreshPoint microservice — base TypeORM entities, a DAO base class, typed HTTP errors, pagination helpers, a cron-job base class, and a grab-bag of type guards, date utilities, and shared enums.
This package is the foundation the other @freshpointcz/* packages and every service build on top of. It doesn't own any business domain — it only standardizes the plumbing (entities, errors, pagination, scheduling) so every service does it the same way.
Installation
npm install @freshpointcz/fresh-coreRequires Node.js ≥ 20. The package declares several peer dependencies — install whichever ones your service actually needs:
npm install typeorm typeorm-naming-strategies dayjs node-schedule
npm install -D eslint eslint-config-prettier eslint-plugin-prettier jiti prettier typescript-eslint| Peer dependency | Needed for |
| --- | --- |
| typeorm | FreshEntity, FreshHyperEntity, FreshDao, subscribers, PgDataSourceOptions |
| typeorm-naming-strategies | PgDataSourceOptions (snake_case column naming) |
| dayjs | DateUtils, StatusDto |
| node-schedule | FreshJob |
| eslint, eslint-config-prettier, eslint-plugin-prettier, jiti, prettier, typescript-eslint | freshEslintConfig |
TypeScript decorators must be enabled (experimentalDecorators + emitDecoratorMetadata) since TypeORM entities rely on them — see tsconfig.base.json at the repo root.
Module map
src/
├── core/ FreshJob (cron), DataHelper, FreshError + HTTP error subclasses, BusinessWarning
├── database/ FreshEntity / FreshHyperEntity / FreshTranslationBase, FreshDao, @TimestampColumn, BaseEntityChangeSubscriber
├── common/ Pagination, typeguards, date utils, Singleton, Deferred, StatusDto, amount units
├── config/ freshEslintConfig, PgDataSourceOptions (starter TypeORM config)
├── enums/ HttpStatus, LanguageCode, TransactionType, PaymentMethod, DepotPoolStatus, ActionCommandCode
├── interfaces/ HealthCheckResult
└── types/ Maybe<T>, Status, CardNumberEverything is re-exported from the package root, so import { FreshEntity, HttpStatus, getPaginationParams } from "@freshpointcz/fresh-core" works regardless of which submodule it actually lives in.
Errors
FreshError — base HTTP error
FreshError carries a status code, a machine-readable Status string, and an optional detail message. error.name is set to the concrete subclass name, so logs and stack traces read NotFoundError instead of Error.
import { NotFoundError, HttpStatus } from "@freshpointcz/fresh-core";
throw new NotFoundError("Product 42 does not exist");
// preserve the original error for logging
try {
await db.findUser(id);
} catch (err) {
throw new InternalServerError("DB query failed", { cause: err });
}Every subclass hardcodes its HTTP status code and Status string — construct the one matching your case instead of the base FreshError:
| Class | Status code | Status |
| --- | --- | --- |
| BadRequestError | 400 | validation-error |
| UnauthorizedError | 401 | not-authenticated |
| PaymentRequiredError | 402 | error |
| ForbiddenError | 403 | not-authorized |
| NotFoundError | 404 | error |
| MethodNotAllowedError | 405 | error |
| NotAcceptableError | 406 | error |
| ProxyAuthenticationRequiredError | 407 | not-authenticated |
| RequestTimeoutError | 408 | error |
| ConflictError | 409 | error |
| GoneError | 410 | error |
| LengthRequiredError | 411 | error |
| PreconditionFailedError | 412 | error |
| PayloadTooLargeError | 413 | error |
| UriTooLongError | 414 | error |
| UnsupportedMediaTypeError | 415 | error |
| RangeNotSatisfiableError | 416 | error |
| ExpectationFailedError | 417 | error |
| ImATeapotError | 418 | error |
| MisdirectedRequestError | 421 | error |
| UnprocessableEntityError | 422 | validation-error |
| LockedError | 423 | error |
| FailedDependencyError | 424 | error |
| TooEarlyError | 425 | error |
| UpgradeRequiredError | 426 | error |
| PreconditionRequiredError | 428 | error |
| TooManyRequestsError | 429 | error |
| RequestHeaderFieldsTooLargeError | 431 | error |
| UnavailableForLegalReasonsError | 451 | error |
| InternalServerError | 500 | internal-server-error |
| NotImplementedError | 501 | internal-server-error |
| BadGatewayError | 502 | internal-server-error |
| ServiceUnavailableError | 503 | internal-server-error |
| GatewayTimeoutError | 504 | internal-server-error |
| HttpVersionNotSupportedError | 505 | internal-server-error |
| VariantAlsoNegotiatesError | 506 | internal-server-error |
| InsufficientStorageError | 507 | internal-server-error |
| LoopDetectedError | 508 | internal-server-error |
| NotExtendedError | 510 | internal-server-error |
| NetworkAuthenticationRequiredError | 511 | internal-server-error |
Each error exposes statusCode and statusDto (a StatusDto built from the status + detail), ready to send straight back to the client from an error-handling middleware.
ApiErrorstill exists but is deprecated — useFreshError(or a subclass) instead.
BusinessWarning
For non-fatal business-rule violations that should reach the caller without being treated as a system error (no HTTP status attached):
import { BusinessWarning } from "@freshpointcz/fresh-core";
throw new BusinessWarning("ORDER_ALREADY_SENT", "Tato objednávka již byla odeslána.");Database
Base entities
| Class | Use for | Adds |
| --- | --- | --- |
| FreshEntity | Regular tables | id (auto-increment PK), uuid (unique, gen_random_uuid()), created_at, updated_at, deleted_at (soft delete) |
| FreshHyperEntity | TimescaleDB hypertables | timestamp (primary key), created_at |
| FreshTranslationBase<T> | i18n side-tables | id, languageCode (checked against LanguageCode), abstract baseEntity: T |
import { Entity, Column } from "typeorm";
import { FreshEntity } from "@freshpointcz/fresh-core";
@Entity()
export class Product extends FreshEntity {
@Column()
name: string;
}FreshHyperEntity expects the table to actually be converted to a hypertable in a migration:
// up
await queryRunner.query(`SELECT create_hypertable('my_hyper_table', 'timestamp', if_not_exists => TRUE);`);
// down
await queryRunner.query(`SELECT drop_hypertable('my_hyper_table', if_exists => TRUE);`);FreshTranslationBase requires the concrete relation to be added by the subclass:
@Entity()
export class ProductTranslation extends FreshTranslationBase<Product> {
@ManyToOne(() => Product, { onDelete: "CASCADE" })
baseEntity: Product;
}@TimestampColumn()
A @Column preset for timestamptz columns defaulting to CURRENT_TIMESTAMP — used internally by FreshHyperEntity.created_at, and available for any other "inserted at" column.
class Delivery extends FreshHyperEntity {
@TimestampColumn({ nullable: true })
confirmedAt: Date | null;
}FreshDao<T>
Thin abstract base for DAO classes. Supplies getRepo(), which returns the injected repository or, when a transactional EntityManager is passed in, the repository scoped to that transaction.
import { FreshDao } from "@freshpointcz/fresh-core";
import { Repository, EntityManager } from "typeorm";
import { Product } from "./product.entity";
export class ProductDao extends FreshDao<Product> {
protected repo = Product.getRepository ? undefined! : (undefined as any); // wire up your own repo injection
protected entity = Product;
async findOne(id: number, manager?: EntityManager) {
return this.getRepo(manager).findOneBy({ id });
}
}BaseEntityChangeSubscriber<Entity, IdType>
Abstract TypeORM EntitySubscriberInterface base that batches insert/update/soft-remove events per transaction and fires a single notification per entity after commit — so a row updated three times in one transaction only notifies once (created wins over everything, deleted wins over updated).
import { EntityManager } from "typeorm";
import { BaseEntityChangeSubscriber, EntityChangeEvent } from "@freshpointcz/fresh-core";
import { Product } from "./product.entity";
export class ProductChangeSubscriber extends BaseEntityChangeSubscriber<Product> {
protected readonly PENDING_KEY = "product-change-subscriber";
protected readonly SUBSCRIBER_NAME = "ProductChangeSubscriber";
listenTo() {
return Product;
}
protected async handleNotification(
id: number,
changeEvent: EntityChangeEvent,
manager: EntityManager
): Promise<void> {
await Messenger.publisher.publish(`product.${changeEvent}`, { id });
}
}If a write happens outside an active transaction, the notification fires immediately and a warning is logged (Notification sent outside transaction for id=...) — subscribers are meant to run inside transactional writes.
Pagination
Offset-based pagination helpers shared across DAO, service, and controller layers — PaginationParams, PaginationMeta, PaginatedList<T>, getPaginationParams, constructTypeormPagination, getPaginationMeta, parsePaginationFromURL, listAll, plus find-options builders (buildDateRange, buildOrder, buildPagination).
import {
getPaginationParams,
constructTypeormPagination,
getPaginationMeta,
PaginatedList,
} from "@freshpointcz/fresh-core";
async function findMany(options: { pagination?: Partial<PaginationParams> }): Promise<PaginatedList<Product>> {
const pagination = getPaginationParams(options.pagination); // → { page, limit, skip }
const [data, total] = await repo.findAndCount({
...constructTypeormPagination(pagination), // → { take, skip }
});
return { data, meta: getPaginationMeta(total, pagination.page, pagination.limit) };
}For the full walkthrough (controller → service → DAO → response), default values, and listAll for bulk/background jobs, see src/common/pagination/README.md.
Scheduled jobs — FreshJob<TResponse, TParams>
Abstract base class for cron-scheduled singletons, built on node-schedule. Each subclass:
- exists exactly once per process (extends the internal
Singletonpattern), - validates its cron expression at construction time (fails loudly on invalid cron),
- always runs in the
Europe/Praguetimezone, - exposes a single
invoke(input)entry point — called by the cron trigger withdefaultInput, or directly by external callers with any compatible input.
import { FreshJob } from "@freshpointcz/fresh-core";
class NightlyCreditResetJob extends FreshJob<void, { dryRun: boolean }> {
private static _instance: NightlyCreditResetJob;
static getInstance() {
return (this._instance ??= new NightlyCreditResetJob());
}
private constructor() {
super("nightly-credit-reset", "0 3 * * *", true, { dryRun: false });
}
async invoke(input: { dryRun: boolean }): Promise<void> {
// business logic here
}
}
NightlyCreditResetJob.getInstance();Do not override the constructor beyond calling super(...) — all business logic belongs in invoke. Use reschedule(newCron) to change the cron expression at runtime, and the protected defaultInput setter to change what the next scheduled tick receives.
DataHelper<T>
Abstract base for lazily-loaded, memoized data with request de-duplication: calling getData() while a load is already in flight returns the same in-flight promise instead of triggering a second fetch.
import { DataHelper, Maybe } from "@freshpointcz/fresh-core";
class ActiveDeviceIdsHelper extends DataHelper<number[]> {
async startDataRetrieval(): Promise<Maybe<number[]>> {
return deviceDao.findActiveIds();
}
}
const helper = new ActiveDeviceIdsHelper(true, Promise.resolve([]));
const ids = await helper.getData(); // fetches once; concurrent calls share the same promiseCommon utilities
Singleton
Per-subclass singleton base class — each subclass of Singleton gets exactly one instance, tracked in an internal registry keyed by constructor. Subclasses must not override the constructor and must put initialization logic in onInit() instead. FreshJob is built on top of this.
Promise helpers
| Export | Purpose |
| --- | --- |
| createDeferred<T>() | Returns { promise, resolve, reject } — a promise you can resolve/reject from outside its executor. Useful as a placeholder before the real async operation is known. |
| SinglePromiseWaiter<T> | Singleton holding at most one in-flight promise for a process-wide flow; the stored promise clears itself once it settles. |
const deferred = createDeferred<string>();
startExternalProcess((result) => deferred.resolve(result));
const result = await deferred.promise;Date utilities — DateUtils
Static helper class wrapping dayjs (with utc, timezone, isBetween, customParseFormat plugins pre-loaded).
| Method | Description |
| --- | --- |
| DateUtils.HOLIDAYS_STR / DateUtils.HOLIDAYS | Czech public holidays 2025–2030, as ISO date strings / UTC epoch millis |
| fromISOtoSQLtimestamp(ts?) | ISO string → "YYYY-MM-DD HH:mm:ss" (UTC) |
| toSQLtimestamp(ts) / fromSQLtimestamp(str) | Dayjs ↔ SQL timestamp string |
| getSqlTimestampFromNowCzech() | Current Czech time as a SQL timestamp string |
| fromISO(isoDate) | Parses an ISO string as UTC |
| getNow() / getNowCzech() | Current time (UTC) / current time in Europe/Prague, returned as UTC Dayjs |
| getLastSunday(weeksOffset?) | Midnight of the most recent Sunday, optionally further back N weeks |
| dayInWeek(ts?) | ISO weekday (Monday=1 … Sunday=7, unlike dayjs's native Sunday=0) |
| isWorkdayDay(ts) | false for weekends and Czech public holidays |
| getDiffInMinutesWithNow(ts) | Minutes between now and ts |
| isInLastDays(numOfDays, ts) | Whether ts falls within the last N days |
Type guards
import {
isObject, hasOwn,
isNumber, isString, isFlag01, isNumberInRange, TO_BINARY_FLAG,
isEnumValue,
isDecimal, toDecimal,
isMaybe,
} from "@freshpointcz/fresh-core";| Function | Narrows to / returns | Notes |
| --- | --- | --- |
| isObject(v) | Record<string, unknown> | Non-null objects only (arrays/functions technically pass too) |
| hasOwn(obj, key) | boolean | Safe wrapper around Object.prototype.hasOwnProperty |
| isNumber(v) | number | Rejects NaN/Infinity |
| isString(v) | string | |
| isFlag01(v) | 0 \| 1 | |
| isNumberInRange(v, min, max, opts?) | number | Inclusive by default; { includeMin, includeMax } to exclude bounds |
| TO_BINARY_FLAG(v) | 0 \| 1 | Converts number \| boolean \| null \| undefined to a binary flag |
| isEnumValue(enumObj, v) | E[keyof E] | Falls back to accepting any string \| number if enumObj is undefined |
| isDecimal(v, options?) | number \| string | Validates finite decimals (number or string form); supports allowedType, decimalPoint (./,), precision, scale — see overloads for full narrowing behavior |
| toDecimal(num, precision?, scale?) | string | Formats a number as a DB-safe decimal string; throws if it exceeds precision/scale (defaults 9,2) |
| isMaybe(v, inner) | Maybe<T> | true for null or values satisfying inner — does not accept undefined |
Other utils
| Function | Purpose |
| --- | --- |
| runWithConcurrency(items, concurrency, worker) | Runs an async worker over items with a bounded concurrency pool |
| resolvePathParameterId(id) | tsoa path params are always strings — returns number if purely numeric, otherwise the original string (for numeric-ID-or-UUID routes) |
| isValidCron(expr) | Structural validation of a 5–6 field numeric cron expression |
| buildPatch(data, keys, transforms?) | Builds a partial patch object from a DTO, only including defined keys, with optional per-key transform functions |
const patch = buildPatch(updateDto, ["name", "active"] as const, {
active: (v) => (v ? 1 : 0),
});StatusDto / UserJwtDto
StatusDto is the structured { status, timestamp, details? } body attached to every FreshError (timestamp defaults to now in Europe/Prague, ISO-formatted). UserJwtDto describes the shape of a decoded user JWT (id, firstName, lastName, email, roles, iat?, exp?).
AMOUNT_UNIT
Lookup table for product amount units, keyed by numeric unit id, with symbol and Czech/English translations:
AMOUNT_UNIT[2]; // → { symbol: "kg", translations: { en: "kilogram", cs: "kilogram" } }Schema
src/common/schema ships thin Category, Subcategory, Manufacturer, Device, and Product TypeORM entities (each just extends FreshEntity) plus COMMON_DATA_SOURCE — a ready-to-use Postgres DataSource pointed at the shared common schema, driven by POSTGRE_SQL_* env vars (HOST, PORT, USER, PASSWORD, DB, SCHEMA) and RUN_MIGRATIONS.
These are the canonical, shared reference entities (categories, manufacturers, devices, products) that multiple services read — extend them in your own service only if you need additional columns via a translation/side table.
Config
| Export | Description |
| --- | --- |
| freshEslintConfig | The workspace-wide ESLint flat config (Prettier integration, naming conventions, prefer-const, eqeqeq, etc.) — re-exported at the monorepo root's eslint.config.mts and consumable by any service |
| PgDataSourceOptions | A starter TypeORM DataSourceOptions template for new services — snake_case naming strategy, UTC, POSTGRE_SQL_* env vars, migrations enabled via RUN_MIGRATIONS. Placeholder values (<service-name>, <host>, …) are meant to be adjusted per service. Call dotenv.config() before importing this. |
import { config } from "dotenv";
config();
import { PgDataSourceOptions } from "@freshpointcz/fresh-core";Enums
| Enum | Values |
| --- | --- |
| HttpStatus | Full set of standard HTTP status codes (100–511) |
| LanguageCode | CS, EN, DE, PL, SK |
| TransactionType | WRITEOFF, SALE, LOST, ADD, DELIVERY, DEPOT_WRITEOFF, DEPOT_ADD, RETURN, INVENTORY_SURPLUS, INVENTORY_MISSING |
| PaymentMethod | CODE, CARD, NONE, CREDIT |
| DepotPoolStatus | NEW, PICKED, WRITTEN_OFF, ACTIVE, TERMINATED |
| ActionCommandCode | RESTART_PC, RESTART_APPLICATION, RESTART_ROUTER, RESTART_REMOTE_CONTROL, SYNCHRONIZE, DIAGNOSE_LOCKS, SYNCHRONIZE_CONFIG |
Interfaces & types
| Export | Description |
| --- | --- |
| HealthCheckResult | Standard shape for service health checks — server: boolean, optional db (bool or per-driver breakdown), optional extras map for additional checked dependencies |
| Maybe<T> | T \| null (see isMaybe above) |
| Status | Union of the machine-readable statuses used by StatusDto/FreshError: "ok" \| "error" \| "not-authorized" \| "not-authenticated" \| "internal-server-error" \| "validation-error" |
| CardNumber | Template-literal type for a 4-digit card number segment |
Project structure
src/
├── index.ts Public barrel — re-exports common, core, types, database, config, enums, interfaces
├── common/
│ ├── constants/amount-unit.ts AMOUNT_UNIT lookup table
│ ├── date-utils.ts DateUtils (dayjs-based)
│ ├── dto/status-dto.ts, UserJwtDto.ts
│ ├── pagination/ Pagination building blocks (own README)
│ ├── patterns/singleton.ts Singleton base class
│ ├── promise-magic/ createDeferred, SinglePromiseWaiter
│ ├── schema/ Shared entities + COMMON_DATA_SOURCE
│ ├── typeguards/ decimal, enums, objects, primitives guards
│ └── utils/ async, patch, cron-validity, path-param resolver
├── core/
│ ├── class/fresh-job.ts FreshJob
│ ├── data-helper.ts DataHelper
│ └── errors/ FreshError + subclasses, BusinessWarning, deprecated ApiError
├── database/
│ ├── entities/ FreshEntity, FreshHyperEntity, FreshTranslationBase
│ ├── decorators/timestamp-column.ts @TimestampColumn
│ ├── dao/fresh-dao.ts FreshDao
│ └── subscribers/ BaseEntityChangeSubscriber
├── config/ freshEslintConfig, PgDataSourceOptions
├── enums/ HttpStatus, LanguageCode, TransactionType, PaymentMethod, DepotPoolStatus, ActionCommandCode
├── interfaces/healthcheck-result.ts
└── types/ Maybe, Status, CardNumberLicense
ISC
