@classytic/primitives 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +160 -160
- package/dist/item-identity.d.mts +71 -0
- package/dist/item-identity.mjs +50 -0
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
4
4
|
adhering to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
5
|
|
|
6
|
+
## [0.25.0] - 2026-08-24
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`@classytic/primitives/item-identity` subpath** — `SkuFields`, `stockKeyOf`, `skuDisplay`. `sku` is a label; `skuRef` is the stock identity — the two fall back in opposite directions and getting one backwards type-checks while returning a plausible wrong string. `stockKeyOf` resolves `skuRef ?? sku ?? fallback` (the key to reserve, decrement and post against); `skuDisplay` resolves `skuLabel ?? sku ?? skuRef` (what a human reads). Canonicalises the per-package `variantSku || String(productId)` derivations into one place.
|
|
11
|
+
|
|
6
12
|
## [0.24.0] - 2026-08-24
|
|
7
13
|
|
|
8
14
|
### Added — `@classytic/primitives/address`: capture-format system
|
package/README.md
CHANGED
|
@@ -1,160 +1,160 @@
|
|
|
1
|
-
# @classytic/primitives
|
|
2
|
-
|
|
3
|
-
[](https://github.com/sponsors/classytic)
|
|
4
|
-
|
|
5
|
-
Framework-agnostic TypeScript primitives shared across Classytic packages.
|
|
6
|
-
Zero runtime dependencies. ESM only. Node 22+.
|
|
7
|
-
|
|
8
|
-
These are the building blocks every Classytic package speaks in common:
|
|
9
|
-
`Money`, `DomainEvent`, `OperationContext`, polymorphic references. When a
|
|
10
|
-
package duplicates one of these shapes, it drifts — this package exists so it
|
|
11
|
-
doesn't.
|
|
12
|
-
|
|
13
|
-
> **Canonical-contract relocations in 0.3.0.** Three modules moved out to
|
|
14
|
-
> `@classytic/repo-core` so kits and arc see one source of truth instead of
|
|
15
|
-
> two:
|
|
16
|
-
> - **Pagination** (`OffsetPaginationResult`, `KeysetPaginationResult`,
|
|
17
|
-
> `AggregatePaginationResult`, `PaginationResult`, `toCanonicalList()`) →
|
|
18
|
-
> `@classytic/repo-core/pagination`.
|
|
19
|
-
> - **Tenant config** (`TenantConfig`, `TenantStrategy`, `TenantFieldType`,
|
|
20
|
-
> `resolveTenantConfig`, `DEFAULT_TENANT_CONFIG`, `ResolvedTenantConfig`) →
|
|
21
|
-
> `@classytic/repo-core/tenant`. Mongokit and sqlitekit's
|
|
22
|
-
> `MultiTenantOptions extends Pick<TenantConfig, ...>`.
|
|
23
|
-
> - **Error contracts** (`HttpError` throwable, `ErrorContract` wire shape,
|
|
24
|
-
> `ErrorDetail`, `ErrorCode`, `ERROR_CODES`, `toErrorContract()`,
|
|
25
|
-
> `statusToErrorCode()`) → `@classytic/repo-core/errors`.
|
|
26
|
-
> `ArcError implements HttpError`.
|
|
27
|
-
>
|
|
28
|
-
> Events stay here. `@classytic/primitives/events` remains the canonical
|
|
29
|
-
> source for `EventMeta`, `DomainEvent`, `EventHandler`, `EventLogger`,
|
|
30
|
-
> `EventTransport`, `DeadLetteredEvent`, `PublishManyResult`, `createEvent`,
|
|
31
|
-
> `createChildEvent`, `matchEventPattern`. Arc 2.12 re-exports only the
|
|
32
|
-
> runtime `MemoryEventTransport`.
|
|
33
|
-
|
|
34
|
-
## Install
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
npm install @classytic/primitives
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## Use
|
|
41
|
-
|
|
42
|
-
Prefer **subpath imports** — each concern is its own entry point, types erase
|
|
43
|
-
at compile time, runtime helpers are tiny.
|
|
44
|
-
|
|
45
|
-
```ts
|
|
46
|
-
import { addMoney, fromMajor, type Money } from '@classytic/primitives/money';
|
|
47
|
-
import { createEvent, type DomainEvent, type EventTransport } from '@classytic/primitives/events';
|
|
48
|
-
import type { OperationContext } from '@classytic/primitives/context';
|
|
49
|
-
import { allocate, type SplitResult } from '@classytic/primitives/split-allocation';
|
|
50
|
-
// Tenant config, error contracts, and pagination now live in repo-core:
|
|
51
|
-
import { resolveTenantConfig, type TenantConfig } from '@classytic/repo-core/tenant';
|
|
52
|
-
import type { HttpError, ErrorContract } from '@classytic/repo-core/errors';
|
|
53
|
-
import type {
|
|
54
|
-
OffsetPaginationResult, KeysetPaginationResult, AggregatePaginationResult,
|
|
55
|
-
} from '@classytic/repo-core/pagination';
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
There is intentionally **no root barrel** — a barrel forces the Node ESM
|
|
59
|
-
loader and TypeScript compiler to walk every module on a single convenience
|
|
60
|
-
import, which slows CI and pulls unused code into the consumer's graph. Use
|
|
61
|
-
subpath imports only.
|
|
62
|
-
|
|
63
|
-
## What's inside
|
|
64
|
-
|
|
65
|
-
| Subpath | Ships |
|
|
66
|
-
|---|---|
|
|
67
|
-
| `/money` | `Money`, `fromMajor`, `toMajor`, `addMoney`, `subtractMoney`, `multiplyMoney`, `sumMoney`, `equalsMoney`, `compareMoney`, `negateMoney`, `absMoney`, `isMoney`, `CurrencyMismatchError` |
|
|
68
|
-
| `/currency` | `CurrencyCode` brand, `CURRENCIES`, `MINOR_UNIT_FACTOR`, `minorUnitFactor`, `isKnownCurrency`, `toCurrencyCode`, `currencyCode`, `isCurrencyCode`, `InvalidCurrencyCodeError`, `CurrencyMismatchError`, `FxSnapshot`, `convertWithSnapshot`, `reverseWithSnapshot`, `FxRounding` |
|
|
69
|
-
| `/address` | `Address`, `ContactAddress`, `GeoPoint`, `GeoJsonPoint`, `toGeoJsonPoint`, `fromGeoJsonPoint` |
|
|
70
|
-
| `/period` | `DateRange`, `Period`, `isDateRange`, `isWithin`, `rangeDurationMs`, `rangesOverlap`, `DateRangeError` — **plus timezone-aware resolution**: `resolvePeriod`, `resolveDay`, `resolveMonth`, `resolveQuarter`, `resolveYear`, `resolveDateSpan`, `dayStart`, `periodOf`, `parsePeriod`, `formatPeriod`, `granularityOf`, `periodTimeZone`, `inclusiveEnd`, `PeriodError` |
|
|
71
|
-
| `/reference` | `ExternalRef`, `ObjectIdLike`, `IdLike`, `DocumentRef`, `idToString`, `isExternalRef` |
|
|
72
|
-
| `/context` | `OperationContext`, `ActorRef` |
|
|
73
|
-
| `/events` | `DomainEvent`, `EventMeta`, `EventHandler`, `EventLogger`, `EventTransport`, `DeadLetteredEvent`, `PublishManyResult`, `createEvent`, `createChildEvent`, `matchEventPattern` |
|
|
74
|
-
| `/result` | `Result<T, E>`, `ok`, `err`, `isOk`, `isErr`, `mapResult`, `mapError`, `unwrap` |
|
|
75
|
-
| `/brand` | `Brand<T, B>`, `Prettify`, `DeepPartial`, `DeepReadonly`, `RequireKeys`, `OptionalKeys`, `Nullable`, `KeysMatching`, `NonEmptyArray` |
|
|
76
|
-
| `/split-allocation` | `allocate`, `isBalanced`, `SplitAllocationError`, `SplitMethod`, `SplitSubject`, `SplitPart`, `SplitResult` |
|
|
77
|
-
| `/idempotency` | `decideClaim`, `completeClaim`, `renewLease`, `identityKey`, `sameIdentity`, `fingerprintRequest`, `newLeaseToken`, `isLeaseExpired`, `holdsLease`, `isInFlight`, `DEFAULT_LEASE_MS`, `IdempotencyClaim`, `ClaimOutcome`, `ClaimResult`, `ClaimState`, `IdempotencyIdentity`, `IdempotencyError` |
|
|
78
|
-
| `/approval` | `createChain`, `applyDecision`, `skipStep`, `nextPendingStep`, `pendingSteps`, `isApproved`, `assertApproved`, `assertApprovedIfPresent`, `isRejected`, `isPending`, `decisionCount`, `ApprovalError`, `ApprovalChain`, `ApprovalStep`, `ApprovalDecision`, `Approver`, `ApprovalThreshold` |
|
|
79
|
-
| `/cadence` | `nextOccurrence`, `occurrencesBetween`, `validateCadence`, `CadenceError`, `Cadence`, `DailyCadence`, `WeeklyCadence`, `MonthlyCadence`, `YearlyCadence`, `CronCadence`, `IsoWeekday` |
|
|
80
|
-
| `/hold` | `addHold`, `resolveHold`, `activeHolds`, `resolvedHolds`, `isOnHold`, `hasActiveHoldOfCode`, `HoldError`, `HoldReason`, `HoldActor` |
|
|
81
|
-
| `/sla` | `breachedAt`, `remainingMs`, `elapsedMs`, `isBreached`, `consumedFraction`, `validateSLA`, `SLAError`, `SLA`, `BreachPolicy` |
|
|
82
|
-
|
|
83
|
-
## Design
|
|
84
|
-
|
|
85
|
-
### Money — integer minor units, always
|
|
86
|
-
|
|
87
|
-
```ts
|
|
88
|
-
import { fromMajor, addMoney, toMajor } from '@classytic/primitives/money';
|
|
89
|
-
|
|
90
|
-
const subtotal = fromMajor(19.99, 'USD'); // { amount: 1999, currency: 'USD' }
|
|
91
|
-
const shipping = fromMajor(5.00, 'USD'); // { amount: 500, currency: 'USD' }
|
|
92
|
-
const total = addMoney(subtotal, shipping); // { amount: 2499, currency: 'USD' }
|
|
93
|
-
toMajor(total); // 24.99
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
`Money.amount` is always an integer in the currency's minor unit — `1` = one
|
|
97
|
-
cent for USD, one yen for JPY, one fils for KWD. Mixing currencies throws
|
|
98
|
-
`CurrencyMismatchError`. This eliminates the float-rounding drift that happens
|
|
99
|
-
when every package rolls its own `{ amount: number, currency: string }`.
|
|
100
|
-
|
|
101
|
-
### DomainEvent — structurally identical to Arc
|
|
102
|
-
|
|
103
|
-
```ts
|
|
104
|
-
import { createEvent, type EventTransport } from '@classytic/primitives/events';
|
|
105
|
-
|
|
106
|
-
const event = createEvent('order:placed', { orderId: 'ord_123' }, {
|
|
107
|
-
organizationId: ctx.organizationId,
|
|
108
|
-
userId: ctx.actorId,
|
|
109
|
-
correlationId: ctx.correlationId,
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
await transport.publish(event);
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
Shape matches `@classytic/arc`'s `DomainEvent` exactly — any Arc transport
|
|
116
|
-
(memory, Redis pub/sub, Redis Streams, Kafka) plugs into a package expecting
|
|
117
|
-
this interface without adapters. See PACKAGE_RULES.md §11.
|
|
118
|
-
|
|
119
|
-
### TenantConfig — moved to `@classytic/repo-core/tenant`
|
|
120
|
-
|
|
121
|
-
```ts
|
|
122
|
-
import { resolveTenantConfig } from '@classytic/repo-core/tenant';
|
|
123
|
-
|
|
124
|
-
const tenant = resolveTenantConfig(config.tenant);
|
|
125
|
-
// { enabled: true, tenantField: 'organizationId', fieldType: 'objectId',
|
|
126
|
-
// ref: 'organization', contextKey: 'organizationId', required: true }
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
Mongokit's `MultiTenantOptions` and sqlitekit's equivalent both
|
|
130
|
-
`extends Pick<TenantConfig, ...>` from repo-core — pass the resolved config
|
|
131
|
-
straight into `multiTenantPlugin(resolved)`.
|
|
132
|
-
|
|
133
|
-
### ExternalRef — polymorphic, framework-free
|
|
134
|
-
|
|
135
|
-
```ts
|
|
136
|
-
interface ExternalRef {
|
|
137
|
-
sourceId: string;
|
|
138
|
-
sourceModel: string;
|
|
139
|
-
}
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
For references that cross trust boundaries (another package, another service,
|
|
143
|
-
Stripe, Postgres) — paired with a host-implemented `SourceBridge`. Works for
|
|
144
|
-
ObjectId, UUID, Stripe ID, anything. See PACKAGE_RULES.md §7.
|
|
145
|
-
|
|
146
|
-
## What's not here (on purpose)
|
|
147
|
-
|
|
148
|
-
- **Mongoose types** — use `ObjectIdLike` from `/reference`. Any Mongoose
|
|
149
|
-
`Types.ObjectId` satisfies it structurally.
|
|
150
|
-
- **Transaction / Order / Cart shapes** — those are domain types, not
|
|
151
|
-
primitives. They live in their respective packages.
|
|
152
|
-
- **Error subclasses** — throw plain `Error`s; hosts serialize them into
|
|
153
|
-
`ErrorContract` (from `@classytic/repo-core/errors`) at the HTTP boundary
|
|
154
|
-
via `toErrorContract(err)`.
|
|
155
|
-
- **Runtime currency database** — ship a whitelist at the app layer if you
|
|
156
|
-
need one. `MINOR_UNIT_FACTOR` covers common cases.
|
|
157
|
-
|
|
158
|
-
## License
|
|
159
|
-
|
|
160
|
-
MIT © Classytic
|
|
1
|
+
# @classytic/primitives
|
|
2
|
+
|
|
3
|
+
[](https://github.com/sponsors/classytic)
|
|
4
|
+
|
|
5
|
+
Framework-agnostic TypeScript primitives shared across Classytic packages.
|
|
6
|
+
Zero runtime dependencies. ESM only. Node 22+.
|
|
7
|
+
|
|
8
|
+
These are the building blocks every Classytic package speaks in common:
|
|
9
|
+
`Money`, `DomainEvent`, `OperationContext`, polymorphic references. When a
|
|
10
|
+
package duplicates one of these shapes, it drifts — this package exists so it
|
|
11
|
+
doesn't.
|
|
12
|
+
|
|
13
|
+
> **Canonical-contract relocations in 0.3.0.** Three modules moved out to
|
|
14
|
+
> `@classytic/repo-core` so kits and arc see one source of truth instead of
|
|
15
|
+
> two:
|
|
16
|
+
> - **Pagination** (`OffsetPaginationResult`, `KeysetPaginationResult`,
|
|
17
|
+
> `AggregatePaginationResult`, `PaginationResult`, `toCanonicalList()`) →
|
|
18
|
+
> `@classytic/repo-core/pagination`.
|
|
19
|
+
> - **Tenant config** (`TenantConfig`, `TenantStrategy`, `TenantFieldType`,
|
|
20
|
+
> `resolveTenantConfig`, `DEFAULT_TENANT_CONFIG`, `ResolvedTenantConfig`) →
|
|
21
|
+
> `@classytic/repo-core/tenant`. Mongokit and sqlitekit's
|
|
22
|
+
> `MultiTenantOptions extends Pick<TenantConfig, ...>`.
|
|
23
|
+
> - **Error contracts** (`HttpError` throwable, `ErrorContract` wire shape,
|
|
24
|
+
> `ErrorDetail`, `ErrorCode`, `ERROR_CODES`, `toErrorContract()`,
|
|
25
|
+
> `statusToErrorCode()`) → `@classytic/repo-core/errors`.
|
|
26
|
+
> `ArcError implements HttpError`.
|
|
27
|
+
>
|
|
28
|
+
> Events stay here. `@classytic/primitives/events` remains the canonical
|
|
29
|
+
> source for `EventMeta`, `DomainEvent`, `EventHandler`, `EventLogger`,
|
|
30
|
+
> `EventTransport`, `DeadLetteredEvent`, `PublishManyResult`, `createEvent`,
|
|
31
|
+
> `createChildEvent`, `matchEventPattern`. Arc 2.12 re-exports only the
|
|
32
|
+
> runtime `MemoryEventTransport`.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install @classytic/primitives
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Use
|
|
41
|
+
|
|
42
|
+
Prefer **subpath imports** — each concern is its own entry point, types erase
|
|
43
|
+
at compile time, runtime helpers are tiny.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { addMoney, fromMajor, type Money } from '@classytic/primitives/money';
|
|
47
|
+
import { createEvent, type DomainEvent, type EventTransport } from '@classytic/primitives/events';
|
|
48
|
+
import type { OperationContext } from '@classytic/primitives/context';
|
|
49
|
+
import { allocate, type SplitResult } from '@classytic/primitives/split-allocation';
|
|
50
|
+
// Tenant config, error contracts, and pagination now live in repo-core:
|
|
51
|
+
import { resolveTenantConfig, type TenantConfig } from '@classytic/repo-core/tenant';
|
|
52
|
+
import type { HttpError, ErrorContract } from '@classytic/repo-core/errors';
|
|
53
|
+
import type {
|
|
54
|
+
OffsetPaginationResult, KeysetPaginationResult, AggregatePaginationResult,
|
|
55
|
+
} from '@classytic/repo-core/pagination';
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
There is intentionally **no root barrel** — a barrel forces the Node ESM
|
|
59
|
+
loader and TypeScript compiler to walk every module on a single convenience
|
|
60
|
+
import, which slows CI and pulls unused code into the consumer's graph. Use
|
|
61
|
+
subpath imports only.
|
|
62
|
+
|
|
63
|
+
## What's inside
|
|
64
|
+
|
|
65
|
+
| Subpath | Ships |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `/money` | `Money`, `fromMajor`, `toMajor`, `addMoney`, `subtractMoney`, `multiplyMoney`, `sumMoney`, `equalsMoney`, `compareMoney`, `negateMoney`, `absMoney`, `isMoney`, `CurrencyMismatchError` |
|
|
68
|
+
| `/currency` | `CurrencyCode` brand, `CURRENCIES`, `MINOR_UNIT_FACTOR`, `minorUnitFactor`, `isKnownCurrency`, `toCurrencyCode`, `currencyCode`, `isCurrencyCode`, `InvalidCurrencyCodeError`, `CurrencyMismatchError`, `FxSnapshot`, `convertWithSnapshot`, `reverseWithSnapshot`, `FxRounding` |
|
|
69
|
+
| `/address` | `Address`, `ContactAddress`, `GeoPoint`, `GeoJsonPoint`, `toGeoJsonPoint`, `fromGeoJsonPoint` |
|
|
70
|
+
| `/period` | `DateRange`, `Period`, `isDateRange`, `isWithin`, `rangeDurationMs`, `rangesOverlap`, `DateRangeError` — **plus timezone-aware resolution**: `resolvePeriod`, `resolveDay`, `resolveMonth`, `resolveQuarter`, `resolveYear`, `resolveDateSpan`, `dayStart`, `periodOf`, `parsePeriod`, `formatPeriod`, `granularityOf`, `periodTimeZone`, `inclusiveEnd`, `PeriodError` |
|
|
71
|
+
| `/reference` | `ExternalRef`, `ObjectIdLike`, `IdLike`, `DocumentRef`, `idToString`, `isExternalRef` |
|
|
72
|
+
| `/context` | `OperationContext`, `ActorRef` |
|
|
73
|
+
| `/events` | `DomainEvent`, `EventMeta`, `EventHandler`, `EventLogger`, `EventTransport`, `DeadLetteredEvent`, `PublishManyResult`, `createEvent`, `createChildEvent`, `matchEventPattern` |
|
|
74
|
+
| `/result` | `Result<T, E>`, `ok`, `err`, `isOk`, `isErr`, `mapResult`, `mapError`, `unwrap` |
|
|
75
|
+
| `/brand` | `Brand<T, B>`, `Prettify`, `DeepPartial`, `DeepReadonly`, `RequireKeys`, `OptionalKeys`, `Nullable`, `KeysMatching`, `NonEmptyArray` |
|
|
76
|
+
| `/split-allocation` | `allocate`, `isBalanced`, `SplitAllocationError`, `SplitMethod`, `SplitSubject`, `SplitPart`, `SplitResult` |
|
|
77
|
+
| `/idempotency` | `decideClaim`, `completeClaim`, `renewLease`, `identityKey`, `sameIdentity`, `fingerprintRequest`, `newLeaseToken`, `isLeaseExpired`, `holdsLease`, `isInFlight`, `DEFAULT_LEASE_MS`, `IdempotencyClaim`, `ClaimOutcome`, `ClaimResult`, `ClaimState`, `IdempotencyIdentity`, `IdempotencyError` |
|
|
78
|
+
| `/approval` | `createChain`, `applyDecision`, `skipStep`, `nextPendingStep`, `pendingSteps`, `isApproved`, `assertApproved`, `assertApprovedIfPresent`, `isRejected`, `isPending`, `decisionCount`, `ApprovalError`, `ApprovalChain`, `ApprovalStep`, `ApprovalDecision`, `Approver`, `ApprovalThreshold` |
|
|
79
|
+
| `/cadence` | `nextOccurrence`, `occurrencesBetween`, `validateCadence`, `CadenceError`, `Cadence`, `DailyCadence`, `WeeklyCadence`, `MonthlyCadence`, `YearlyCadence`, `CronCadence`, `IsoWeekday` |
|
|
80
|
+
| `/hold` | `addHold`, `resolveHold`, `activeHolds`, `resolvedHolds`, `isOnHold`, `hasActiveHoldOfCode`, `HoldError`, `HoldReason`, `HoldActor` |
|
|
81
|
+
| `/sla` | `breachedAt`, `remainingMs`, `elapsedMs`, `isBreached`, `consumedFraction`, `validateSLA`, `SLAError`, `SLA`, `BreachPolicy` |
|
|
82
|
+
|
|
83
|
+
## Design
|
|
84
|
+
|
|
85
|
+
### Money — integer minor units, always
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { fromMajor, addMoney, toMajor } from '@classytic/primitives/money';
|
|
89
|
+
|
|
90
|
+
const subtotal = fromMajor(19.99, 'USD'); // { amount: 1999, currency: 'USD' }
|
|
91
|
+
const shipping = fromMajor(5.00, 'USD'); // { amount: 500, currency: 'USD' }
|
|
92
|
+
const total = addMoney(subtotal, shipping); // { amount: 2499, currency: 'USD' }
|
|
93
|
+
toMajor(total); // 24.99
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`Money.amount` is always an integer in the currency's minor unit — `1` = one
|
|
97
|
+
cent for USD, one yen for JPY, one fils for KWD. Mixing currencies throws
|
|
98
|
+
`CurrencyMismatchError`. This eliminates the float-rounding drift that happens
|
|
99
|
+
when every package rolls its own `{ amount: number, currency: string }`.
|
|
100
|
+
|
|
101
|
+
### DomainEvent — structurally identical to Arc
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { createEvent, type EventTransport } from '@classytic/primitives/events';
|
|
105
|
+
|
|
106
|
+
const event = createEvent('order:placed', { orderId: 'ord_123' }, {
|
|
107
|
+
organizationId: ctx.organizationId,
|
|
108
|
+
userId: ctx.actorId,
|
|
109
|
+
correlationId: ctx.correlationId,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await transport.publish(event);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Shape matches `@classytic/arc`'s `DomainEvent` exactly — any Arc transport
|
|
116
|
+
(memory, Redis pub/sub, Redis Streams, Kafka) plugs into a package expecting
|
|
117
|
+
this interface without adapters. See PACKAGE_RULES.md §11.
|
|
118
|
+
|
|
119
|
+
### TenantConfig — moved to `@classytic/repo-core/tenant`
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { resolveTenantConfig } from '@classytic/repo-core/tenant';
|
|
123
|
+
|
|
124
|
+
const tenant = resolveTenantConfig(config.tenant);
|
|
125
|
+
// { enabled: true, tenantField: 'organizationId', fieldType: 'objectId',
|
|
126
|
+
// ref: 'organization', contextKey: 'organizationId', required: true }
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Mongokit's `MultiTenantOptions` and sqlitekit's equivalent both
|
|
130
|
+
`extends Pick<TenantConfig, ...>` from repo-core — pass the resolved config
|
|
131
|
+
straight into `multiTenantPlugin(resolved)`.
|
|
132
|
+
|
|
133
|
+
### ExternalRef — polymorphic, framework-free
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
interface ExternalRef {
|
|
137
|
+
sourceId: string;
|
|
138
|
+
sourceModel: string;
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
For references that cross trust boundaries (another package, another service,
|
|
143
|
+
Stripe, Postgres) — paired with a host-implemented `SourceBridge`. Works for
|
|
144
|
+
ObjectId, UUID, Stripe ID, anything. See PACKAGE_RULES.md §7.
|
|
145
|
+
|
|
146
|
+
## What's not here (on purpose)
|
|
147
|
+
|
|
148
|
+
- **Mongoose types** — use `ObjectIdLike` from `/reference`. Any Mongoose
|
|
149
|
+
`Types.ObjectId` satisfies it structurally.
|
|
150
|
+
- **Transaction / Order / Cart shapes** — those are domain types, not
|
|
151
|
+
primitives. They live in their respective packages.
|
|
152
|
+
- **Error subclasses** — throw plain `Error`s; hosts serialize them into
|
|
153
|
+
`ErrorContract` (from `@classytic/repo-core/errors`) at the HTTP boundary
|
|
154
|
+
via `toErrorContract(err)`.
|
|
155
|
+
- **Runtime currency database** — ship a whitelist at the app layer if you
|
|
156
|
+
need one. `MINOR_UNIT_FACTOR` covers common cases.
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
MIT © Classytic
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
//#region src/identity/item-identity.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* `sku` is a LABEL; `skuRef` is the stock IDENTITY. Never one field.
|
|
4
|
+
*
|
|
5
|
+
* | field | mutable? | job |
|
|
6
|
+
* |---|---|---|
|
|
7
|
+
* | `skuRef` | never | keys quants, moves and ledger rows |
|
|
8
|
+
* | `sku` | operator edits it | what a human reads, types, searches |
|
|
9
|
+
*
|
|
10
|
+
* Use `stockKeyOf` for anything that reserves or moves stock, `skuDisplay` for
|
|
11
|
+
* anything a human reads. Never hand-roll either: both fall back, in OPPOSITE
|
|
12
|
+
* directions, and getting one backwards type-checks and returns a plausible
|
|
13
|
+
* string.
|
|
14
|
+
*
|
|
15
|
+
* - stock key = `skuRef ?? sku` — absent `skuRef` means a pre-split producer
|
|
16
|
+
* whose `sku` still IS the key.
|
|
17
|
+
* - display = `skuLabel ?? sku ?? skuRef` — the ref last, and flagged, because
|
|
18
|
+
* it can be an opaque id with no merchandising meaning.
|
|
19
|
+
*
|
|
20
|
+
* Pinned by `tests/unit/item-identity.test.ts`.
|
|
21
|
+
*/
|
|
22
|
+
/** Identifier fields a record may carry — each shape populates a different subset. */
|
|
23
|
+
interface SkuFields {
|
|
24
|
+
/** Merchandising label — mutable, human-facing. */
|
|
25
|
+
sku?: string | undefined;
|
|
26
|
+
/** Stock/ledger identity — immutable, keys quants and moves. */
|
|
27
|
+
skuRef?: string | undefined;
|
|
28
|
+
/** Label resolved server-side for a read model keyed by `skuRef`. */
|
|
29
|
+
skuLabel?: string | undefined;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The STOCK key to reserve, decrement and post against: `skuRef ?? sku`,
|
|
33
|
+
* falling back to `fallback` (typically the owning entity's `_id`) when the
|
|
34
|
+
* record carries neither — the shape every "variant, else the product"
|
|
35
|
+
* ledger key already re-derives by hand across three packages.
|
|
36
|
+
*
|
|
37
|
+
* `undefined` = the record carries neither AND no `fallback` was given —
|
|
38
|
+
* malformed, not untracked. Use {@link requireStockKey} where that must stop
|
|
39
|
+
* the write.
|
|
40
|
+
*/
|
|
41
|
+
declare function stockKeyOf(item: SkuFields, fallback?: string | {
|
|
42
|
+
toString(): string;
|
|
43
|
+
}): string | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* {@link stockKeyOf} that THROWS. Use wherever the key feeds a stock movement:
|
|
46
|
+
* a reservation on `undefined` returns "out of stock" instead of failing.
|
|
47
|
+
*/
|
|
48
|
+
declare function requireStockKey(item: SkuFields, context?: string): string;
|
|
49
|
+
/** What a surface should render for an item, and whether it had a real label. */
|
|
50
|
+
interface SkuDisplay {
|
|
51
|
+
/** Best human-facing value. `undefined` = no identifier at all; render a dash. */
|
|
52
|
+
label: string | undefined;
|
|
53
|
+
/** The stock identity, for subtext or copy-to-clipboard. */
|
|
54
|
+
ref: string | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* TRUE when `label` is really the `ref` (nothing resolved). Style it as an id
|
|
57
|
+
* — monospace, muted — never as a SKU.
|
|
58
|
+
*/
|
|
59
|
+
isRefFallback: boolean;
|
|
60
|
+
}
|
|
61
|
+
/** What to SHOW: `skuLabel ?? sku ?? skuRef`. The ref is returned alongside,
|
|
62
|
+
* never dropped — it is the join key an operator quotes. */
|
|
63
|
+
declare function skuDisplay(item: SkuFields): SkuDisplay;
|
|
64
|
+
/**
|
|
65
|
+
* Looks like an opaque id (24-char hex) rather than a merchandising code.
|
|
66
|
+
* PRESENTATION HINT ONLY — never branch stock logic on it; a deployment may key
|
|
67
|
+
* quants by any stable string.
|
|
68
|
+
*/
|
|
69
|
+
declare function looksLikeOpaqueId(value: string | undefined): boolean;
|
|
70
|
+
//#endregion
|
|
71
|
+
export { SkuDisplay, SkuFields, looksLikeOpaqueId, requireStockKey, skuDisplay, stockKeyOf };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
//#region src/identity/item-identity.ts
|
|
2
|
+
const nonEmpty = (value) => typeof value === "string" && value.trim().length > 0 ? value : void 0;
|
|
3
|
+
/**
|
|
4
|
+
* The STOCK key to reserve, decrement and post against: `skuRef ?? sku`,
|
|
5
|
+
* falling back to `fallback` (typically the owning entity's `_id`) when the
|
|
6
|
+
* record carries neither — the shape every "variant, else the product"
|
|
7
|
+
* ledger key already re-derives by hand across three packages.
|
|
8
|
+
*
|
|
9
|
+
* `undefined` = the record carries neither AND no `fallback` was given —
|
|
10
|
+
* malformed, not untracked. Use {@link requireStockKey} where that must stop
|
|
11
|
+
* the write.
|
|
12
|
+
*/
|
|
13
|
+
function stockKeyOf(item, fallback) {
|
|
14
|
+
return nonEmpty(item.skuRef) ?? nonEmpty(item.sku) ?? (fallback !== void 0 ? String(fallback) : void 0);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* {@link stockKeyOf} that THROWS. Use wherever the key feeds a stock movement:
|
|
18
|
+
* a reservation on `undefined` returns "out of stock" instead of failing.
|
|
19
|
+
*/
|
|
20
|
+
function requireStockKey(item, context) {
|
|
21
|
+
const key = stockKeyOf(item);
|
|
22
|
+
if (key === void 0) throw new Error(`stock key missing: neither skuRef nor sku is set${context ? ` (${context})` : ""}`);
|
|
23
|
+
return key;
|
|
24
|
+
}
|
|
25
|
+
/** What to SHOW: `skuLabel ?? sku ?? skuRef`. The ref is returned alongside,
|
|
26
|
+
* never dropped — it is the join key an operator quotes. */
|
|
27
|
+
function skuDisplay(item) {
|
|
28
|
+
const ref = nonEmpty(item.skuRef);
|
|
29
|
+
const label = nonEmpty(item.skuLabel) ?? nonEmpty(item.sku);
|
|
30
|
+
if (label !== void 0) return {
|
|
31
|
+
label,
|
|
32
|
+
ref,
|
|
33
|
+
isRefFallback: false
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
label: ref,
|
|
37
|
+
ref,
|
|
38
|
+
isRefFallback: ref !== void 0
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Looks like an opaque id (24-char hex) rather than a merchandising code.
|
|
43
|
+
* PRESENTATION HINT ONLY — never branch stock logic on it; a deployment may key
|
|
44
|
+
* quants by any stable string.
|
|
45
|
+
*/
|
|
46
|
+
function looksLikeOpaqueId(value) {
|
|
47
|
+
return typeof value === "string" && /^[0-9a-f]{24}$/i.test(value);
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { looksLikeOpaqueId, requireStockKey, skuDisplay, stockKeyOf };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/primitives",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Framework-agnostic TypeScript primitives shared across Classytic packages — Money, events, pagination, tenant config, and other building blocks.",
|
|
5
5
|
"author": "Classytic",
|
|
6
6
|
"license": "MIT",
|
|
@@ -209,6 +209,10 @@
|
|
|
209
209
|
"./subject": {
|
|
210
210
|
"types": "./dist/subject.d.mts",
|
|
211
211
|
"default": "./dist/subject.mjs"
|
|
212
|
+
},
|
|
213
|
+
"./item-identity": {
|
|
214
|
+
"types": "./dist/item-identity.d.mts",
|
|
215
|
+
"default": "./dist/item-identity.mjs"
|
|
212
216
|
}
|
|
213
217
|
},
|
|
214
218
|
"files": [
|