@classytic/primitives

中文文档
0.37.0发布于 2026/11/06MITTypeScriptES Module零依赖近 30 天下载 3403

Framework-agnostic TypeScript primitives shared across Classytic packages — Money, events, pagination, tenant config, and other building blocks.

原始使用文档
文档目录10 节
  1. Install
  2. Use
  3. What's inside
  4. Design
  5. Money — integer minor units, always
  6. DomainEvent — structurally identical to Arc
  7. TenantConfig — moved to @classytic/repo-core/tenant
  8. ExternalRef — polymorphic, framework-free
  9. What's not here (on purpose)
  10. License

@classytic/primitives

Sponsor

Framework-agnostic TypeScript primitives shared across Classytic packages. Zero runtime dependencies. ESM only. Node 22+.

These are the building blocks every Classytic package speaks in common: Money, DomainEvent, OperationContext, polymorphic references. When a package duplicates one of these shapes, it drifts — this package exists so it doesn't.

Canonical-contract relocations in 0.3.0. Three modules moved out to @classytic/repo-core so kits and arc see one source of truth instead of two:

  • Pagination (OffsetPaginationResult, KeysetPaginationResult, AggregatePaginationResult, PaginationResult, toCanonicalList()) → @classytic/repo-core/pagination.
  • Tenant config (TenantConfig, TenantStrategy, TenantFieldType, resolveTenantConfig, DEFAULT_TENANT_CONFIG, ResolvedTenantConfig) → @classytic/repo-core/tenant. Mongokit and sqlitekit's MultiTenantOptions extends Pick<TenantConfig, ...>.
  • Error contracts (HttpError throwable, ErrorContract wire shape, ErrorDetail, ErrorCode, ERROR_CODES, toErrorContract(), statusToErrorCode()) → @classytic/repo-core/errors. ArcError implements HttpError.

Events stay here. @classytic/primitives/events remains the canonical source for EventMeta, DomainEvent, EventHandler, EventLogger, EventTransport, DeadLetteredEvent, PublishManyResult, createEvent, createChildEvent, matchEventPattern. Arc 2.12 re-exports only the runtime MemoryEventTransport.

Install

npm install @classytic/primitives

Use

Prefer subpath imports — each concern is its own entry point, types erase at compile time, runtime helpers are tiny.

import { addMoney, fromMajor, type Money } from '@classytic/primitives/money';
import { createEvent, type DomainEvent, type EventTransport } from '@classytic/primitives/events';
import type { OperationContext } from '@classytic/primitives/context';
import { allocate, type SplitResult } from '@classytic/primitives/split-allocation';
// Tenant config, error contracts, and pagination now live in repo-core:
import { resolveTenantConfig, type TenantConfig } from '@classytic/repo-core/tenant';
import type { HttpError, ErrorContract } from '@classytic/repo-core/errors';
import type {
  OffsetPaginationResult, KeysetPaginationResult, AggregatePaginationResult,
} from '@classytic/repo-core/pagination';

There is intentionally no root barrel — a barrel forces the Node ESM loader and TypeScript compiler to walk every module on a single convenience import, which slows CI and pulls unused code into the consumer's graph. Use subpath imports only.

What's inside

SubpathShips
/moneyMoney, fromMajor, toMajor, addMoney, subtractMoney, multiplyMoney, sumMoney, equalsMoney, compareMoney, negateMoney, absMoney, isMoney, CurrencyMismatchError
/currencyCurrencyCode brand, CURRENCIES, MINOR_UNIT_FACTOR, minorUnitFactor, isKnownCurrency, toCurrencyCode, currencyCode, isCurrencyCode, InvalidCurrencyCodeError, CurrencyMismatchError, FxSnapshot, convertWithSnapshot, reverseWithSnapshot, FxRounding
/addressAddress, ContactAddress, GeoPoint, GeoJsonPoint, toGeoJsonPoint, fromGeoJsonPoint
/periodDateRange, Period, isDateRange, isWithin, rangeDurationMs, rangesOverlap, DateRangeError — plus timezone-aware resolution: resolvePeriod, resolveDay, resolveMonth, resolveQuarter, resolveYear, resolveDateSpan, dayStart, periodOf, parsePeriod, formatPeriod, granularityOf, periodTimeZone, inclusiveEnd, PeriodError
/referenceExternalRef, ObjectIdLike, IdLike, DocumentRef, idToString, isExternalRef
/contextOperationContext, ActorRef
/eventsDomainEvent, EventMeta, EventHandler, EventLogger, EventTransport, DeadLetteredEvent, PublishManyResult, createEvent, createChildEvent, matchEventPattern
/resultResult<T, E>, ok, err, isOk, isErr, mapResult, mapError, unwrap
/brandBrand<T, B>, Prettify, DeepPartial, DeepReadonly, RequireKeys, OptionalKeys, Nullable, KeysMatching, NonEmptyArray
/split-allocationallocate, isBalanced, SplitAllocationError, SplitMethod, SplitSubject, SplitPart, SplitResult
/idempotencydecideClaim, completeClaim, renewLease, identityKey, sameIdentity, fingerprintRequest, newLeaseToken, isLeaseExpired, holdsLease, isInFlight, DEFAULT_LEASE_MS, IdempotencyClaim, ClaimOutcome, ClaimResult, ClaimState, IdempotencyIdentity, IdempotencyError
/approvalcreateChain, applyDecision, skipStep, nextPendingStep, pendingSteps, isApproved, assertApproved, assertApprovedIfPresent, isRejected, isPending, decisionCount, chainToJson, ApprovalChainJson, ApprovalError, ApprovalChain, ApprovalStep, ApprovalDecision, Approver, ApprovalThreshold
/cadencenextOccurrence, occurrencesBetween, validateCadence, CadenceError, Cadence, DailyCadence, WeeklyCadence, MonthlyCadence, YearlyCadence, CronCadence, IsoWeekday
/holdaddHold, resolveHold, activeHolds, resolvedHolds, isOnHold, hasActiveHoldOfCode, HoldError, HoldReason, HoldActor
/slabreachedAt, remainingMs, elapsedMs, isBreached, consumedFraction, validateSLA, SLAError, SLA, BreachPolicy
/timezonecivil dates and zones (civilDateOf, civilDateToInstant, addCivilDays, addCivilMonths, ...) plus daily wall-clock windows: WallTime, WallWindow, parseWallTime, wallTimeOf, wallInstant, resolveWallWindow, wallWindowAt, wallWindowStartsBetween (overnight, DST-exact, half-open)
/identifiercleanIdentifier (invisible marks removed, NFKC, digits to ASCII, trimmed), asciiDigits (every Unicode decimal digit), stripInvisible (\p{Cf}) for pasted staff numbers, national IDs, e-mail
/concurrencymapBounded(items, concurrency, fn) (worker pool, ordered results, fail-fast after in-flight settle; NaN concurrency throws), chunked(items, size)

Design

Money — integer minor units, always

import { fromMajor, addMoney, toMajor } from '@classytic/primitives/money';

const subtotal = fromMajor(19.99, 'USD');     // { amount: 1999, currency: 'USD' }
const shipping = fromMajor(5.00,  'USD');     // { amount: 500,  currency: 'USD' }
const total    = addMoney(subtotal, shipping); // { amount: 2499, currency: 'USD' }
toMajor(total);                                // 24.99

Money.amount is always an integer in the currency's minor unit — 1 = one cent for USD, one yen for JPY, one fils for KWD. Mixing currencies throws CurrencyMismatchError. This eliminates the float-rounding drift that happens when every package rolls its own { amount: number, currency: string }.

DomainEvent — structurally identical to Arc

import { createEvent, type EventTransport } from '@classytic/primitives/events';

const event = createEvent('order:placed', { orderId: 'ord_123' }, {
  organizationId: ctx.organizationId,
  userId: ctx.actorId,
  correlationId: ctx.correlationId,
});

await transport.publish(event);

Shape matches @classytic/arc's DomainEvent exactly — any Arc transport (memory, Redis pub/sub, Redis Streams, Kafka) plugs into a package expecting this interface without adapters. See PACKAGE_RULES.md §11.

TenantConfig — moved to @classytic/repo-core/tenant

import { resolveTenantConfig } from '@classytic/repo-core/tenant';

const tenant = resolveTenantConfig(config.tenant);
// { enabled: true, tenantField: 'organizationId', fieldType: 'objectId',
//   ref: 'organization', contextKey: 'organizationId', required: true }

Mongokit's MultiTenantOptions and sqlitekit's equivalent both extends Pick<TenantConfig, ...> from repo-core — pass the resolved config straight into multiTenantPlugin(resolved).

ExternalRef — polymorphic, framework-free

interface ExternalRef {
  sourceId: string;
  sourceModel: string;
}

For references that cross trust boundaries (another package, another service, Stripe, Postgres) — paired with a host-implemented SourceBridge. Works for ObjectId, UUID, Stripe ID, anything. See PACKAGE_RULES.md §7.

What's not here (on purpose)

  • Mongoose types — use ObjectIdLike from /reference. Any Mongoose Types.ObjectId satisfies it structurally.
  • Transaction / Order / Cart shapes — those are domain types, not primitives. They live in their respective packages.
  • Error subclasses — throw plain Errors; hosts serialize them into ErrorContract (from @classytic/repo-core/errors) at the HTTP boundary via toErrorContract(err).
  • Runtime currency database — ship a whitelist at the app layer if you need one. MINOR_UNIT_FACTOR covers common cases.

License

MIT © Classytic

版本与发布记录

@classytic/primitives 共发布 43 个版本,首次发布于 2026/04/21;过去 12 个月发布了 43 个版本。

版本发布日期距今
0.37.0latest2026/11/063 天前
0.36.02026/11/036 天前
0.35.02026/10/2714 天前
0.34.02026/10/1723 天前
0.33.02026/10/1426 天前
0.32.02026/10/1229 天前

关于 @classytic/primitives 的常见问题

@classytic/primitives 是什么?

Framework-agnostic TypeScript primitives shared across Classytic packages — Money, events, pagination, tenant config, and other building blocks.

如何安装 @classytic/primitives?

在项目目录运行 npm install @classytic/primitives,或使用 pnpm add / yarn add / bun add。

@classytic/primitives 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@classytic/primitives 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@classytic/primitives 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@classytic/primitives 可以商用吗?

@classytic/primitives 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@classytic/primitives 的下载量有多少?

根据 npm 官方下载统计,@classytic/primitives 在 2026/10/10 – 2026/11/08 这 30 天内被下载 3,403 次,日均约 113 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@classytic/primitives 还在维护吗?

最新版本 0.37.0 发布于 2026/11/06(3 天前);至今共发布 43 个版本,其中过去 12 个月发布了 43 个;首次发布于 2026/04/21。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。