@classytic/primitives 0.1.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 +123 -0
- package/LICENSE +21 -0
- package/README.md +136 -0
- package/dist/address.d.mts +38 -0
- package/dist/address.mjs +16 -0
- package/dist/approval.d.mts +133 -0
- package/dist/approval.mjs +146 -0
- package/dist/brand-DApaegoR.d.mts +37 -0
- package/dist/brand.d.mts +2 -0
- package/dist/brand.mjs +1 -0
- package/dist/cadence.d.mts +93 -0
- package/dist/cadence.mjs +139 -0
- package/dist/context.d.mts +48 -0
- package/dist/context.mjs +1 -0
- package/dist/currency.d.mts +110 -0
- package/dist/currency.mjs +116 -0
- package/dist/errors.d.mts +50 -0
- package/dist/errors.mjs +20 -0
- package/dist/events.d.mts +240 -0
- package/dist/events.mjs +90 -0
- package/dist/hold.d.mts +89 -0
- package/dist/hold.mjs +69 -0
- package/dist/money.d.mts +78 -0
- package/dist/money.mjs +131 -0
- package/dist/outbox.d.mts +222 -0
- package/dist/outbox.mjs +30 -0
- package/dist/pagination.d.mts +57 -0
- package/dist/pagination.mjs +22 -0
- package/dist/period.d.mts +46 -0
- package/dist/period.mjs +31 -0
- package/dist/person.d.mts +44 -0
- package/dist/person.mjs +16 -0
- package/dist/reference.d.mts +39 -0
- package/dist/reference.mjs +10 -0
- package/dist/result.d.mts +32 -0
- package/dist/result.mjs +34 -0
- package/dist/sla.d.mts +71 -0
- package/dist/sla.mjs +57 -0
- package/dist/split-allocation.d.mts +84 -0
- package/dist/split-allocation.mjs +115 -0
- package/dist/tenant.d.mts +122 -0
- package/dist/tenant.mjs +54 -0
- package/package.json +142 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
4
|
+
adhering to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
|
+
|
|
6
|
+
## [0.1.0]
|
|
7
|
+
|
|
8
|
+
Initial release. Framework-agnostic TypeScript primitives shared across
|
|
9
|
+
Classytic packages. Zero runtime dependencies. Node 22+. ESM only.
|
|
10
|
+
260 tests (unit + integration) across 20 files.
|
|
11
|
+
|
|
12
|
+
All subpaths are importable independently; there is **no root barrel
|
|
13
|
+
re-export** — consumers must use subpath imports (`@classytic/primitives/money`,
|
|
14
|
+
`/events`, `/tenant`, …) to keep the Node ESM loader + TypeScript compiler
|
|
15
|
+
from walking the full module graph on any convenience import.
|
|
16
|
+
|
|
17
|
+
### Subpaths
|
|
18
|
+
|
|
19
|
+
- **`/money`** — `Money` type + integer-minor-unit arithmetic (`money`,
|
|
20
|
+
`addMoney`, `subtractMoney`, `multiplyMoney`, `sumMoney`, `negateMoney`,
|
|
21
|
+
`absMoney`, `compareMoney`, `equalsMoney`, `isZeroMoney`, `isPositiveMoney`,
|
|
22
|
+
`isNegativeMoney`, `isMoney`, `fromMajor`, `toMajor`); `CurrencyMismatchError`.
|
|
23
|
+
- **`/currency`** — `CurrencyCode` brand; `CURRENCIES` table; `MINOR_UNIT_FACTOR`,
|
|
24
|
+
`minorUnitFactor`, `isCurrencyCode`, `toCurrencyCode`.
|
|
25
|
+
- **`/address`** — `Address`, `ContactAddress`, `GeoPoint`, `GeoJsonPoint`;
|
|
26
|
+
`toGeoJsonPoint`, `fromGeoJsonPoint`.
|
|
27
|
+
- **`/period`** — `DateRange`, `Period`; `isDateRange`, `isWithin`,
|
|
28
|
+
`rangeDurationMs`.
|
|
29
|
+
- **`/pagination`** — `PageParams`, `OffsetPage<T>`, `KeysetPage<T>`,
|
|
30
|
+
`AggregatePage`, `KeysetParams`, `SortSpec`, `SortDirection`;
|
|
31
|
+
`emptyOffsetPage`, `emptyKeysetPage`.
|
|
32
|
+
- **`/reference`** — `ExternalRef`, `DocumentRef`, `ObjectIdLike`, `IdLike`;
|
|
33
|
+
`idToString`, `isExternalRef`.
|
|
34
|
+
- **`/context`** — `OperationContext` (the identity + tracing bag every
|
|
35
|
+
Classytic package accepts); `ActorRef`.
|
|
36
|
+
- **`/events`** — `EventMeta` (arc v2.9-aligned — 13 fields including
|
|
37
|
+
`schemaVersion`, `causationId`, `partitionKey`, `source`, `idempotencyKey`,
|
|
38
|
+
`aggregate`), `DomainEvent<T>`, `EventTransport`, `EventHandler`,
|
|
39
|
+
`EventLogger`, `DeadLetteredEvent<T>`, `PublishManyResult`; `createEvent`,
|
|
40
|
+
`createChildEvent` (auto-chains `causationId`, inherits `correlationId` /
|
|
41
|
+
`userId` / `organizationId` / `source` / `idempotencyKey` — NOT
|
|
42
|
+
`aggregate`), `matchEventPattern`.
|
|
43
|
+
- **`/outbox`** — transactional outbox **contract** mirrored from arc v2.9:
|
|
44
|
+
`OutboxStore`, `OutboxWriteOptions`, `OutboxClaimOptions`,
|
|
45
|
+
`OutboxAcknowledgeOptions`, `OutboxFailOptions`, `OutboxErrorInfo`,
|
|
46
|
+
`OutboxFailureContext`, `OutboxFailureDecision`, `OutboxFailurePolicy`;
|
|
47
|
+
`OutboxOwnershipError`, `InvalidOutboxEventError`. **Contract only** —
|
|
48
|
+
runtime (`EventOutbox`, `MemoryOutboxStore`, `MongoOutboxStore`,
|
|
49
|
+
`exponentialBackoff`) ships in `@classytic/arc/events` and
|
|
50
|
+
`@classytic/arc/events/mongo`. Domain packages implement `OutboxStore`
|
|
51
|
+
without peer-depping arc.
|
|
52
|
+
- **`/tenant`** — `TenantConfig` (with `strategy: 'field' | 'none' | 'custom'`
|
|
53
|
+
and optional `resolve(ctx)` for custom strategy), `TenantFieldType`,
|
|
54
|
+
`TenantStrategy`, `ResolvedTenantConfig`; `DEFAULT_TENANT_CONFIG`,
|
|
55
|
+
`resolveTenantConfig`. Field names match `@classytic/mongokit`'s
|
|
56
|
+
`MultiTenantOptions` so a resolved config threads into
|
|
57
|
+
`multiTenantPlugin({ ... })` without translation.
|
|
58
|
+
- **`/result`** — `Result<T, E>`; `ok`, `err`, `isOk`, `isErr`, `unwrap`,
|
|
59
|
+
`mapResult`, `mapError`.
|
|
60
|
+
- **`/brand`** — `Brand<T, B>`, `Prettify`, `DeepPartial`, `DeepReadonly`,
|
|
61
|
+
`RequireKeys`, `OptionalKeys`, `NonEmptyArray`.
|
|
62
|
+
- **`/errors`** — `ErrorContract`, `ErrorDetail`, `ErrorCode`, `ERROR_CODES`.
|
|
63
|
+
- **`/split-allocation`** — `allocate(total, subjects, method)` returns a
|
|
64
|
+
`SplitResult` whose parts sum exactly to `total` via the largest-remainder
|
|
65
|
+
method. Methods: `equal`, `by-qty`, `by-weight`, `by-volume`, `by-value`,
|
|
66
|
+
`by-percent`. Deterministic — identical inputs produce identical outputs;
|
|
67
|
+
ties broken by input order. Handles negative totals by sign-flipping.
|
|
68
|
+
Exports: `allocate`, `isBalanced`, `SplitAllocationError`, types
|
|
69
|
+
`SplitMethod`, `SplitSubject`, `SplitPart`, `SplitResult`,
|
|
70
|
+
`SplitAllocationErrorCode`.
|
|
71
|
+
- **`/approval`** — `ApprovalChain` value object with pure FSM helpers.
|
|
72
|
+
Supports sequential and parallel chains, per-step quorum
|
|
73
|
+
(`requiredApprovals`), threshold-gated skips. Any step-level rejection
|
|
74
|
+
rejects the chain; all steps `approved`|`skipped` approves it. Exports:
|
|
75
|
+
`createChain`, `applyDecision`, `skipStep`, `nextPendingStep`,
|
|
76
|
+
`pendingSteps`, `isApproved`, `isRejected`, `isPending`, `decisionCount`,
|
|
77
|
+
`ApprovalError`, types `ApprovalChain`, `ApprovalStep`, `ApprovalDecision`,
|
|
78
|
+
`Approver`, `ApprovalStatus`, `ChainOrder`, `ApprovalThreshold`,
|
|
79
|
+
`CreateChainInput`, `CreateStepInput`, `DecisionInput`, `ApprovalErrorCode`.
|
|
80
|
+
- **`/cadence`** — recurrence spec with pure UTC date arithmetic. Kinds:
|
|
81
|
+
`daily`, `weekly`, `monthly`, `yearly`, `cron` (opaque — host parses).
|
|
82
|
+
`dayOfMonth: 31` snaps to the last day of shorter months; Feb 29 snaps
|
|
83
|
+
to Feb 28 in non-leap years. Exports: `nextOccurrence`,
|
|
84
|
+
`occurrencesBetween`, `validateCadence`, `CadenceError`, types `Cadence`,
|
|
85
|
+
`DailyCadence`, `WeeklyCadence`, `MonthlyCadence`, `YearlyCadence`,
|
|
86
|
+
`CronCadence`, `CadenceKind`, `IsoWeekday`, `CadenceErrorCode`.
|
|
87
|
+
- **`/hold`** — `HoldReason` + collection helpers. Host embeds
|
|
88
|
+
`HoldReason[]` on documents. Exports: `addHold`, `resolveHold`,
|
|
89
|
+
`activeHolds`, `resolvedHolds`, `isOnHold`, `hasActiveHoldOfCode`,
|
|
90
|
+
`HoldError`, types `HoldReason`, `HoldActor`, `HoldActorKind`,
|
|
91
|
+
`AddHoldInput`, `ResolveHoldInput`, `HoldErrorCode`.
|
|
92
|
+
- **`/sla`** — target duration + breach-detection arithmetic. Policy is
|
|
93
|
+
carried but not executed — host reads `breachPolicy` (`warn` | `escalate`
|
|
94
|
+
| `block`) and acts. Exports: `breachedAt`, `remainingMs`, `elapsedMs`,
|
|
95
|
+
`isBreached`, `consumedFraction`, `validateSLA`, `SLAError`, types
|
|
96
|
+
`SLA`, `BreachPolicy`, `SLAErrorCode`.
|
|
97
|
+
- **`/person`** — person/contact shapes shared across order, crm, hr.
|
|
98
|
+
`PersonName`, `ContactInfo`, `EmergencyContact`, `Gender`; formatters
|
|
99
|
+
`formatFullName`, `formatDisplayName`.
|
|
100
|
+
|
|
101
|
+
### Design principles
|
|
102
|
+
|
|
103
|
+
- Pure functions on plain data. No I/O, no Mongo, no events.
|
|
104
|
+
- Immutable return values. Helpers return new collections; originals are never mutated.
|
|
105
|
+
- Deterministic. Same inputs → same outputs. Clock sources injected
|
|
106
|
+
explicitly so tests don't flake.
|
|
107
|
+
- Errors are typed. Every module ships an `<Name>Error` class with a
|
|
108
|
+
discriminated `code: <Name>ErrorCode` string union.
|
|
109
|
+
- Subpath-imports only — no root barrel.
|
|
110
|
+
|
|
111
|
+
### Ownership direction
|
|
112
|
+
|
|
113
|
+
Arc is the source of truth for event types. Primitives **mirrors** arc
|
|
114
|
+
(`@classytic/arc/events/EventTransport.ts`, `@classytic/arc/events/outbox.ts`)
|
|
115
|
+
verbatim for `EventMeta`, `DomainEvent`, `EventTransport`,
|
|
116
|
+
`DeadLetteredEvent`, `EventLogger`, `PublishManyResult`, and the full
|
|
117
|
+
`OutboxStore` contract surface. Helpers (`createEvent`, `createChildEvent`)
|
|
118
|
+
are allowed to be more defensive but match arc's signatures.
|
|
119
|
+
|
|
120
|
+
Packages that peer-dep `@classytic/primitives` can write
|
|
121
|
+
`class FooOutboxRepository implements OutboxStore` without pulling arc into
|
|
122
|
+
their dependency graph; hosts import `EventOutbox` + `MongoOutboxStore` from
|
|
123
|
+
arc and wire them against package-provided stores.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Classytic
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# @classytic/primitives
|
|
2
|
+
|
|
3
|
+
Framework-agnostic TypeScript primitives shared across Classytic packages.
|
|
4
|
+
Zero runtime dependencies. ESM only. Node 22+.
|
|
5
|
+
|
|
6
|
+
These are the building blocks every Classytic package speaks in common:
|
|
7
|
+
`Money`, `DomainEvent`, `TenantConfig`, `OperationContext`, pagination shapes,
|
|
8
|
+
polymorphic references. When a package duplicates one of these shapes, it
|
|
9
|
+
drifts — this package exists so it doesn't.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @classytic/primitives
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Use
|
|
18
|
+
|
|
19
|
+
Prefer **subpath imports** — each concern is its own entry point, types erase
|
|
20
|
+
at compile time, runtime helpers are tiny.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { addMoney, fromMajor, type Money } from '@classytic/primitives/money';
|
|
24
|
+
import { createEvent, type DomainEvent, type EventTransport } from '@classytic/primitives/events';
|
|
25
|
+
import { resolveTenantConfig, type TenantConfig } from '@classytic/primitives/tenant';
|
|
26
|
+
import type { OperationContext } from '@classytic/primitives/context';
|
|
27
|
+
import type { OffsetPage, PageParams } from '@classytic/primitives/pagination';
|
|
28
|
+
import { allocate, type SplitResult } from '@classytic/primitives/split-allocation';
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
There is intentionally **no root barrel** — a barrel forces the Node ESM
|
|
32
|
+
loader and TypeScript compiler to walk every module on a single convenience
|
|
33
|
+
import, which slows CI and pulls unused code into the consumer's graph. Use
|
|
34
|
+
subpath imports only.
|
|
35
|
+
|
|
36
|
+
## What's inside
|
|
37
|
+
|
|
38
|
+
| Subpath | Ships |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `/money` | `Money`, `fromMajor`, `toMajor`, `addMoney`, `subtractMoney`, `multiplyMoney`, `sumMoney`, `equalsMoney`, `compareMoney`, `negateMoney`, `absMoney`, `isMoney`, `CurrencyMismatchError` |
|
|
41
|
+
| `/currency` | `CurrencyCode` brand, `CURRENCIES`, `MINOR_UNIT_FACTOR`, `minorUnitFactor`, `toCurrencyCode`, `isCurrencyCode` |
|
|
42
|
+
| `/address` | `Address`, `ContactAddress`, `GeoPoint`, `GeoJsonPoint`, `toGeoJsonPoint`, `fromGeoJsonPoint` |
|
|
43
|
+
| `/period` | `DateRange`, `Period`, `isDateRange`, `isWithin`, `rangeDurationMs` |
|
|
44
|
+
| `/pagination` | `PageParams`, `KeysetParams`, `SortSpec`, `SortDirection`, `OffsetPage<T>`, `KeysetPage<T>`, `AggregatePage<T>`, `emptyOffsetPage`, `emptyKeysetPage` |
|
|
45
|
+
| `/reference` | `ExternalRef`, `ObjectIdLike`, `IdLike`, `DocumentRef`, `idToString`, `isExternalRef` |
|
|
46
|
+
| `/context` | `OperationContext`, `ActorRef` |
|
|
47
|
+
| `/events` | `DomainEvent`, `EventMeta`, `EventHandler`, `EventTransport`, `createEvent`, `matchEventPattern` |
|
|
48
|
+
| `/tenant` | `TenantConfig`, `TenantFieldType`, `DEFAULT_TENANT_CONFIG`, `resolveTenantConfig` |
|
|
49
|
+
| `/result` | `Result<T, E>`, `ok`, `err`, `isOk`, `isErr`, `mapResult`, `mapError`, `unwrap` |
|
|
50
|
+
| `/brand` | `Brand<T, B>`, `Prettify`, `DeepPartial`, `DeepReadonly`, `RequireKeys`, `OptionalKeys`, `Nullable`, `KeysMatching`, `NonEmptyArray` |
|
|
51
|
+
| `/errors` | `ErrorContract`, `ErrorDetail`, `ErrorCode`, `ERROR_CODES` |
|
|
52
|
+
| `/split-allocation` | `allocate`, `isBalanced`, `SplitAllocationError`, `SplitMethod`, `SplitSubject`, `SplitPart`, `SplitResult` |
|
|
53
|
+
| `/approval` | `createChain`, `applyDecision`, `skipStep`, `nextPendingStep`, `pendingSteps`, `isApproved`, `isRejected`, `isPending`, `decisionCount`, `ApprovalError`, `ApprovalChain`, `ApprovalStep`, `ApprovalDecision`, `Approver`, `ApprovalThreshold` |
|
|
54
|
+
| `/cadence` | `nextOccurrence`, `occurrencesBetween`, `validateCadence`, `CadenceError`, `Cadence`, `DailyCadence`, `WeeklyCadence`, `MonthlyCadence`, `YearlyCadence`, `CronCadence`, `IsoWeekday` |
|
|
55
|
+
| `/hold` | `addHold`, `resolveHold`, `activeHolds`, `resolvedHolds`, `isOnHold`, `hasActiveHoldOfCode`, `HoldError`, `HoldReason`, `HoldActor` |
|
|
56
|
+
| `/sla` | `breachedAt`, `remainingMs`, `elapsedMs`, `isBreached`, `consumedFraction`, `validateSLA`, `SLAError`, `SLA`, `BreachPolicy` |
|
|
57
|
+
|
|
58
|
+
## Design
|
|
59
|
+
|
|
60
|
+
### Money — integer minor units, always
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { fromMajor, addMoney, toMajor } from '@classytic/primitives/money';
|
|
64
|
+
|
|
65
|
+
const subtotal = fromMajor(19.99, 'USD'); // { amount: 1999, currency: 'USD' }
|
|
66
|
+
const shipping = fromMajor(5.00, 'USD'); // { amount: 500, currency: 'USD' }
|
|
67
|
+
const total = addMoney(subtotal, shipping); // { amount: 2499, currency: 'USD' }
|
|
68
|
+
toMajor(total); // 24.99
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`Money.amount` is always an integer in the currency's minor unit — `1` = one
|
|
72
|
+
cent for USD, one yen for JPY, one fils for KWD. Mixing currencies throws
|
|
73
|
+
`CurrencyMismatchError`. This eliminates the float-rounding drift that happens
|
|
74
|
+
when every package rolls its own `{ amount: number, currency: string }`.
|
|
75
|
+
|
|
76
|
+
### DomainEvent — structurally identical to Arc
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { createEvent, type EventTransport } from '@classytic/primitives/events';
|
|
80
|
+
|
|
81
|
+
const event = createEvent('order:placed', { orderId: 'ord_123' }, {
|
|
82
|
+
organizationId: ctx.organizationId,
|
|
83
|
+
userId: ctx.actorId,
|
|
84
|
+
correlationId: ctx.correlationId,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await transport.publish(event);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Shape matches `@classytic/arc`'s `DomainEvent` exactly — any Arc transport
|
|
91
|
+
(memory, Redis pub/sub, Redis Streams, Kafka) plugs into a package expecting
|
|
92
|
+
this interface without adapters. See PACKAGE_RULES.md §11.
|
|
93
|
+
|
|
94
|
+
### TenantConfig — one shape, every package
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { resolveTenantConfig } from '@classytic/primitives/tenant';
|
|
98
|
+
|
|
99
|
+
const tenant = resolveTenantConfig(config.tenant);
|
|
100
|
+
// { enabled: true, tenantField: 'organizationId', fieldType: 'objectId',
|
|
101
|
+
// ref: 'organization', contextKey: 'organizationId', required: true }
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Field names match `@classytic/mongokit`'s `MultiTenantOptions` so you can
|
|
105
|
+
forward the resolved config straight into `multiTenantPlugin(resolved)`.
|
|
106
|
+
|
|
107
|
+
Packages stop rolling five flavours of `{ field, type, ref, contextKey,
|
|
108
|
+
enabled }`.
|
|
109
|
+
|
|
110
|
+
### ExternalRef — polymorphic, framework-free
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
interface ExternalRef {
|
|
114
|
+
sourceId: string;
|
|
115
|
+
sourceModel: string;
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
For references that cross trust boundaries (another package, another service,
|
|
120
|
+
Stripe, Postgres) — paired with a host-implemented `SourceBridge`. Works for
|
|
121
|
+
ObjectId, UUID, Stripe ID, anything. See PACKAGE_RULES.md §7.
|
|
122
|
+
|
|
123
|
+
## What's not here (on purpose)
|
|
124
|
+
|
|
125
|
+
- **Mongoose types** — use `ObjectIdLike` from `/reference`. Any Mongoose
|
|
126
|
+
`Types.ObjectId` satisfies it structurally.
|
|
127
|
+
- **Transaction / Order / Cart shapes** — those are domain types, not
|
|
128
|
+
primitives. They live in their respective packages.
|
|
129
|
+
- **Error subclasses** — throw plain `Error`s; hosts serialize them into
|
|
130
|
+
`ErrorContract` at the HTTP boundary.
|
|
131
|
+
- **Runtime currency database** — ship a whitelist at the app layer if you
|
|
132
|
+
need one. `MINOR_UNIT_FACTOR` covers common cases.
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT © Classytic
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//#region src/address.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Postal address.
|
|
4
|
+
*
|
|
5
|
+
* Kept deliberately generic — international postal formats vary too much to
|
|
6
|
+
* enforce per-country rules in a shared type. Consumers can extend or narrow
|
|
7
|
+
* as needed (e.g. a BD-specific schema might add `district`, `upazila`).
|
|
8
|
+
*/
|
|
9
|
+
interface Address {
|
|
10
|
+
line1: string;
|
|
11
|
+
line2?: string;
|
|
12
|
+
city: string;
|
|
13
|
+
/** State / province / region. Optional for countries without subdivisions. */
|
|
14
|
+
state?: string;
|
|
15
|
+
postalCode?: string;
|
|
16
|
+
/** ISO 3166-1 alpha-2 country code (e.g. 'BD', 'US', 'GB'). */
|
|
17
|
+
country: string;
|
|
18
|
+
}
|
|
19
|
+
interface ContactAddress extends Address {
|
|
20
|
+
name?: string;
|
|
21
|
+
phone?: string;
|
|
22
|
+
email?: string;
|
|
23
|
+
}
|
|
24
|
+
/** WGS84 geographic coordinates. */
|
|
25
|
+
interface GeoPoint {
|
|
26
|
+
latitude: number;
|
|
27
|
+
longitude: number;
|
|
28
|
+
}
|
|
29
|
+
/** A GeoJSON-flavoured point — convenient when storing in Mongo `2dsphere` indexes. */
|
|
30
|
+
interface GeoJsonPoint {
|
|
31
|
+
type: 'Point';
|
|
32
|
+
/** [longitude, latitude] — note the GeoJSON ordering. */
|
|
33
|
+
coordinates: readonly [number, number];
|
|
34
|
+
}
|
|
35
|
+
declare function toGeoJsonPoint(p: GeoPoint): GeoJsonPoint;
|
|
36
|
+
declare function fromGeoJsonPoint(g: GeoJsonPoint): GeoPoint;
|
|
37
|
+
//#endregion
|
|
38
|
+
export { Address, ContactAddress, GeoJsonPoint, GeoPoint, fromGeoJsonPoint, toGeoJsonPoint };
|
package/dist/address.mjs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/address.ts
|
|
2
|
+
function toGeoJsonPoint(p) {
|
|
3
|
+
return {
|
|
4
|
+
type: "Point",
|
|
5
|
+
coordinates: [p.longitude, p.latitude]
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
function fromGeoJsonPoint(g) {
|
|
9
|
+
const [longitude, latitude] = g.coordinates;
|
|
10
|
+
return {
|
|
11
|
+
longitude,
|
|
12
|
+
latitude
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
export { fromGeoJsonPoint, toGeoJsonPoint };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
//#region src/approval.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Approval chain value object and pure state-transition helpers.
|
|
4
|
+
*
|
|
5
|
+
* Hosts embed an `ApprovalChain` on domain documents (quotation, return,
|
|
6
|
+
* high-value transaction, etc.) as a subdocument — this primitive is
|
|
7
|
+
* purely data + pure functions; no persistence, no I/O, no events.
|
|
8
|
+
*
|
|
9
|
+
* Supports:
|
|
10
|
+
* - sequential chains (step 2 activates only after step 1 approves)
|
|
11
|
+
* - parallel chains (all pending steps active at once)
|
|
12
|
+
* - per-step quorum (requiredApprovals out of N approvers)
|
|
13
|
+
* - skipping steps when a host-evaluated condition fails
|
|
14
|
+
*
|
|
15
|
+
* Chain status is computed from step statuses:
|
|
16
|
+
* - any step `rejected` → chain `rejected`
|
|
17
|
+
* - all required steps `approved`|`skipped` → chain `approved`
|
|
18
|
+
* - otherwise → chain `pending`
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* import { createChain, applyDecision, isApproved } from '@classytic/primitives/approval';
|
|
22
|
+
*
|
|
23
|
+
* let chain = createChain({
|
|
24
|
+
* order: 'sequential',
|
|
25
|
+
* steps: [
|
|
26
|
+
* { id: 'sales', approvers: [{ id: 'rep1' }] },
|
|
27
|
+
* { id: 'finance', approvers: [{ id: 'cfo1' }, { id: 'cfo2' }], requiredApprovals: 1 },
|
|
28
|
+
* ],
|
|
29
|
+
* });
|
|
30
|
+
* chain = applyDecision(chain, { stepId: 'sales', approverId: 'rep1', decision: 'approved' });
|
|
31
|
+
* chain = applyDecision(chain, { stepId: 'finance', approverId: 'cfo1', decision: 'approved' });
|
|
32
|
+
* isApproved(chain); // → true
|
|
33
|
+
*/
|
|
34
|
+
type ApprovalStatus = 'pending' | 'approved' | 'rejected' | 'skipped';
|
|
35
|
+
type ChainOrder = 'sequential' | 'parallel';
|
|
36
|
+
interface Approver {
|
|
37
|
+
readonly id: string;
|
|
38
|
+
readonly name?: string;
|
|
39
|
+
readonly role?: string;
|
|
40
|
+
}
|
|
41
|
+
interface ApprovalDecision {
|
|
42
|
+
readonly approverId: string;
|
|
43
|
+
readonly decision: 'approved' | 'rejected';
|
|
44
|
+
readonly note?: string;
|
|
45
|
+
readonly decidedAt: Date;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Optional threshold gate. The primitive does not evaluate this — it's
|
|
49
|
+
* metadata the host reads when deciding whether to mark a step `skipped`
|
|
50
|
+
* (via {@link skipStep}). Captures the intent in structured form so the
|
|
51
|
+
* reason for the skip is auditable.
|
|
52
|
+
*/
|
|
53
|
+
interface ApprovalThreshold {
|
|
54
|
+
readonly field: string;
|
|
55
|
+
readonly op: 'gt' | 'gte' | 'lt' | 'lte' | 'eq';
|
|
56
|
+
readonly value: number;
|
|
57
|
+
}
|
|
58
|
+
interface ApprovalStep {
|
|
59
|
+
readonly id: string;
|
|
60
|
+
readonly name?: string;
|
|
61
|
+
readonly approvers: readonly Approver[];
|
|
62
|
+
/** How many distinct approver decisions of `approved` are needed. Default: 1. */
|
|
63
|
+
readonly requiredApprovals: number;
|
|
64
|
+
readonly threshold?: ApprovalThreshold;
|
|
65
|
+
readonly status: ApprovalStatus;
|
|
66
|
+
readonly decisions: readonly ApprovalDecision[];
|
|
67
|
+
/** Populated when status transitions to `skipped`. */
|
|
68
|
+
readonly skippedReason?: string;
|
|
69
|
+
}
|
|
70
|
+
interface ApprovalChain {
|
|
71
|
+
readonly order: ChainOrder;
|
|
72
|
+
readonly steps: readonly ApprovalStep[];
|
|
73
|
+
readonly status: ApprovalStatus;
|
|
74
|
+
}
|
|
75
|
+
interface CreateChainInput {
|
|
76
|
+
readonly order: ChainOrder;
|
|
77
|
+
readonly steps: readonly CreateStepInput[];
|
|
78
|
+
}
|
|
79
|
+
interface CreateStepInput {
|
|
80
|
+
readonly id: string;
|
|
81
|
+
readonly name?: string;
|
|
82
|
+
readonly approvers: readonly Approver[];
|
|
83
|
+
readonly requiredApprovals?: number;
|
|
84
|
+
readonly threshold?: ApprovalThreshold;
|
|
85
|
+
}
|
|
86
|
+
type ApprovalErrorCode = 'EMPTY_STEPS' | 'DUPLICATE_STEP_ID' | 'EMPTY_APPROVERS' | 'DUPLICATE_APPROVER_ID' | 'INVALID_QUORUM' | 'UNKNOWN_STEP' | 'UNAUTHORIZED_APPROVER' | 'STEP_NOT_ACTIVE' | 'STEP_ALREADY_DECIDED_BY_APPROVER';
|
|
87
|
+
declare class ApprovalError extends Error {
|
|
88
|
+
readonly name = "ApprovalError";
|
|
89
|
+
readonly code: ApprovalErrorCode;
|
|
90
|
+
constructor(code: ApprovalErrorCode, message: string);
|
|
91
|
+
}
|
|
92
|
+
/** Build a fresh chain in `pending` state. Validates inputs. */
|
|
93
|
+
declare function createChain(input: CreateChainInput): ApprovalChain;
|
|
94
|
+
/**
|
|
95
|
+
* Returns the next step awaiting a decision — or `null` if none.
|
|
96
|
+
*
|
|
97
|
+
* For `sequential` chains: the first `pending` step in order; later steps
|
|
98
|
+
* remain inactive until earlier ones finalise.
|
|
99
|
+
*
|
|
100
|
+
* For `parallel` chains: returns the first pending step (all pending steps
|
|
101
|
+
* are active simultaneously; host typically iterates with {@link pendingSteps}).
|
|
102
|
+
*/
|
|
103
|
+
declare function nextPendingStep(chain: ApprovalChain): ApprovalStep | null;
|
|
104
|
+
/** All steps currently eligible to receive a decision. */
|
|
105
|
+
declare function pendingSteps(chain: ApprovalChain): readonly ApprovalStep[];
|
|
106
|
+
interface DecisionInput {
|
|
107
|
+
readonly stepId: string;
|
|
108
|
+
readonly approverId: string;
|
|
109
|
+
readonly decision: 'approved' | 'rejected';
|
|
110
|
+
readonly note?: string;
|
|
111
|
+
readonly decidedAt?: Date;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Apply a decision to a step. Returns a new chain — original is not mutated.
|
|
115
|
+
* Throws if the step is unknown, not active, the approver isn't listed, or
|
|
116
|
+
* the approver has already decided.
|
|
117
|
+
*/
|
|
118
|
+
declare function applyDecision(chain: ApprovalChain, input: DecisionInput): ApprovalChain;
|
|
119
|
+
/**
|
|
120
|
+
* Mark a step as skipped — for when a host-evaluated condition determines
|
|
121
|
+
* the step is not required (e.g., amount < threshold). Skipped steps count
|
|
122
|
+
* as "completed" for chain approval.
|
|
123
|
+
*/
|
|
124
|
+
declare function skipStep(chain: ApprovalChain, stepId: string, reason?: string): ApprovalChain;
|
|
125
|
+
declare function isApproved(chain: ApprovalChain): boolean;
|
|
126
|
+
declare function isRejected(chain: ApprovalChain): boolean;
|
|
127
|
+
declare function isPending(chain: ApprovalChain): boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Count total decisions across all steps. Useful for audit summaries.
|
|
130
|
+
*/
|
|
131
|
+
declare function decisionCount(chain: ApprovalChain): number;
|
|
132
|
+
//#endregion
|
|
133
|
+
export { ApprovalChain, ApprovalDecision, ApprovalError, ApprovalErrorCode, ApprovalStatus, ApprovalStep, ApprovalThreshold, Approver, ChainOrder, CreateChainInput, CreateStepInput, DecisionInput, applyDecision, createChain, decisionCount, isApproved, isPending, isRejected, nextPendingStep, pendingSteps, skipStep };
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
//#region src/approval.ts
|
|
2
|
+
var ApprovalError = class extends Error {
|
|
3
|
+
name = "ApprovalError";
|
|
4
|
+
code;
|
|
5
|
+
constructor(code, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
/** Build a fresh chain in `pending` state. Validates inputs. */
|
|
11
|
+
function createChain(input) {
|
|
12
|
+
if (input.steps.length === 0) throw new ApprovalError("EMPTY_STEPS", "chain must have at least one step");
|
|
13
|
+
const seenStepIds = /* @__PURE__ */ new Set();
|
|
14
|
+
const steps = input.steps.map((s) => {
|
|
15
|
+
if (seenStepIds.has(s.id)) throw new ApprovalError("DUPLICATE_STEP_ID", `duplicate step id: ${s.id}`);
|
|
16
|
+
seenStepIds.add(s.id);
|
|
17
|
+
if (s.approvers.length === 0) throw new ApprovalError("EMPTY_APPROVERS", `step ${s.id} has no approvers`);
|
|
18
|
+
const seenApproverIds = /* @__PURE__ */ new Set();
|
|
19
|
+
for (const a of s.approvers) {
|
|
20
|
+
if (seenApproverIds.has(a.id)) throw new ApprovalError("DUPLICATE_APPROVER_ID", `step ${s.id} has duplicate approver id: ${a.id}`);
|
|
21
|
+
seenApproverIds.add(a.id);
|
|
22
|
+
}
|
|
23
|
+
const quorum = s.requiredApprovals ?? 1;
|
|
24
|
+
if (!Number.isInteger(quorum) || quorum < 1 || quorum > s.approvers.length) throw new ApprovalError("INVALID_QUORUM", `step ${s.id}: requiredApprovals must be integer in [1, ${s.approvers.length}], got ${quorum}`);
|
|
25
|
+
return {
|
|
26
|
+
id: s.id,
|
|
27
|
+
...s.name !== void 0 ? { name: s.name } : {},
|
|
28
|
+
approvers: s.approvers,
|
|
29
|
+
requiredApprovals: quorum,
|
|
30
|
+
...s.threshold !== void 0 ? { threshold: s.threshold } : {},
|
|
31
|
+
status: "pending",
|
|
32
|
+
decisions: []
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
order: input.order,
|
|
37
|
+
steps,
|
|
38
|
+
status: "pending"
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Returns the next step awaiting a decision — or `null` if none.
|
|
43
|
+
*
|
|
44
|
+
* For `sequential` chains: the first `pending` step in order; later steps
|
|
45
|
+
* remain inactive until earlier ones finalise.
|
|
46
|
+
*
|
|
47
|
+
* For `parallel` chains: returns the first pending step (all pending steps
|
|
48
|
+
* are active simultaneously; host typically iterates with {@link pendingSteps}).
|
|
49
|
+
*/
|
|
50
|
+
function nextPendingStep(chain) {
|
|
51
|
+
for (const step of chain.steps) if (step.status === "pending") return step;
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
/** All steps currently eligible to receive a decision. */
|
|
55
|
+
function pendingSteps(chain) {
|
|
56
|
+
if (chain.order === "sequential") {
|
|
57
|
+
const next = nextPendingStep(chain);
|
|
58
|
+
return next ? [next] : [];
|
|
59
|
+
}
|
|
60
|
+
return chain.steps.filter((s) => s.status === "pending");
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Apply a decision to a step. Returns a new chain — original is not mutated.
|
|
64
|
+
* Throws if the step is unknown, not active, the approver isn't listed, or
|
|
65
|
+
* the approver has already decided.
|
|
66
|
+
*/
|
|
67
|
+
function applyDecision(chain, input) {
|
|
68
|
+
const stepIndex = chain.steps.findIndex((s) => s.id === input.stepId);
|
|
69
|
+
if (stepIndex === -1) throw new ApprovalError("UNKNOWN_STEP", `no step with id ${input.stepId}`);
|
|
70
|
+
const step = chain.steps[stepIndex];
|
|
71
|
+
if (!step) throw new ApprovalError("UNKNOWN_STEP", `no step with id ${input.stepId}`);
|
|
72
|
+
if (!pendingSteps(chain).some((s) => s.id === input.stepId)) throw new ApprovalError("STEP_NOT_ACTIVE", `step ${input.stepId} is not currently active (status=${step.status})`);
|
|
73
|
+
if (!step.approvers.some((a) => a.id === input.approverId)) throw new ApprovalError("UNAUTHORIZED_APPROVER", `approver ${input.approverId} is not listed on step ${input.stepId}`);
|
|
74
|
+
if (step.decisions.some((d) => d.approverId === input.approverId)) throw new ApprovalError("STEP_ALREADY_DECIDED_BY_APPROVER", `approver ${input.approverId} has already decided on step ${input.stepId}`);
|
|
75
|
+
const decision = {
|
|
76
|
+
approverId: input.approverId,
|
|
77
|
+
decision: input.decision,
|
|
78
|
+
...input.note !== void 0 ? { note: input.note } : {},
|
|
79
|
+
decidedAt: input.decidedAt ?? /* @__PURE__ */ new Date()
|
|
80
|
+
};
|
|
81
|
+
const updatedStep = computeStepStatus({
|
|
82
|
+
...step,
|
|
83
|
+
decisions: [...step.decisions, decision]
|
|
84
|
+
});
|
|
85
|
+
const updatedSteps = chain.steps.map((s, i) => i === stepIndex ? updatedStep : s);
|
|
86
|
+
return {
|
|
87
|
+
order: chain.order,
|
|
88
|
+
steps: updatedSteps,
|
|
89
|
+
status: computeChainStatus(updatedSteps)
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Mark a step as skipped — for when a host-evaluated condition determines
|
|
94
|
+
* the step is not required (e.g., amount < threshold). Skipped steps count
|
|
95
|
+
* as "completed" for chain approval.
|
|
96
|
+
*/
|
|
97
|
+
function skipStep(chain, stepId, reason) {
|
|
98
|
+
const stepIndex = chain.steps.findIndex((s) => s.id === stepId);
|
|
99
|
+
if (stepIndex === -1) throw new ApprovalError("UNKNOWN_STEP", `no step with id ${stepId}`);
|
|
100
|
+
const step = chain.steps[stepIndex];
|
|
101
|
+
if (!step) throw new ApprovalError("UNKNOWN_STEP", `no step with id ${stepId}`);
|
|
102
|
+
if (step.status !== "pending") throw new ApprovalError("STEP_NOT_ACTIVE", `step ${stepId} cannot be skipped (status=${step.status})`);
|
|
103
|
+
const updatedStep = {
|
|
104
|
+
...step,
|
|
105
|
+
status: "skipped",
|
|
106
|
+
...reason !== void 0 ? { skippedReason: reason } : {}
|
|
107
|
+
};
|
|
108
|
+
const updatedSteps = chain.steps.map((s, i) => i === stepIndex ? updatedStep : s);
|
|
109
|
+
return {
|
|
110
|
+
order: chain.order,
|
|
111
|
+
steps: updatedSteps,
|
|
112
|
+
status: computeChainStatus(updatedSteps)
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function isApproved(chain) {
|
|
116
|
+
return chain.status === "approved";
|
|
117
|
+
}
|
|
118
|
+
function isRejected(chain) {
|
|
119
|
+
return chain.status === "rejected";
|
|
120
|
+
}
|
|
121
|
+
function isPending(chain) {
|
|
122
|
+
return chain.status === "pending";
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Count total decisions across all steps. Useful for audit summaries.
|
|
126
|
+
*/
|
|
127
|
+
function decisionCount(chain) {
|
|
128
|
+
return chain.steps.reduce((n, s) => n + s.decisions.length, 0);
|
|
129
|
+
}
|
|
130
|
+
function computeStepStatus(step) {
|
|
131
|
+
if (step.decisions.some((d) => d.decision === "rejected")) return {
|
|
132
|
+
...step,
|
|
133
|
+
status: "rejected"
|
|
134
|
+
};
|
|
135
|
+
if (step.decisions.filter((d) => d.decision === "approved").length >= step.requiredApprovals) return {
|
|
136
|
+
...step,
|
|
137
|
+
status: "approved"
|
|
138
|
+
};
|
|
139
|
+
return step;
|
|
140
|
+
}
|
|
141
|
+
function computeChainStatus(steps) {
|
|
142
|
+
if (steps.some((s) => s.status === "rejected")) return "rejected";
|
|
143
|
+
return steps.every((s) => s.status === "approved" || s.status === "skipped") ? "approved" : "pending";
|
|
144
|
+
}
|
|
145
|
+
//#endregion
|
|
146
|
+
export { ApprovalError, applyDecision, createChain, decisionCount, isApproved, isPending, isRejected, nextPendingStep, pendingSteps, skipStep };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//#region src/brand.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Type utilities for building nominal types and transforming shapes.
|
|
4
|
+
*
|
|
5
|
+
* These have zero runtime cost — all erase at compile time.
|
|
6
|
+
*/
|
|
7
|
+
declare const __brand: unique symbol;
|
|
8
|
+
/**
|
|
9
|
+
* Nominal typing via an intersection brand.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* type UserId = Brand<string, 'UserId'>;
|
|
13
|
+
* type OrderId = Brand<string, 'OrderId'>;
|
|
14
|
+
* const u: UserId = 'abc' as UserId;
|
|
15
|
+
* const o: OrderId = u; // type error — brands differ
|
|
16
|
+
*/
|
|
17
|
+
type Brand<T, B extends string> = T & {
|
|
18
|
+
readonly [__brand]: B;
|
|
19
|
+
};
|
|
20
|
+
/** Flattens intersection types for nicer IDE hover output. */
|
|
21
|
+
type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
|
22
|
+
/** Recursive partial — every leaf becomes optional. */
|
|
23
|
+
type DeepPartial<T> = T extends ((...args: readonly unknown[]) => unknown) ? T : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>> : T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
|
|
24
|
+
/** Recursive readonly — every leaf becomes immutable. */
|
|
25
|
+
type DeepReadonly<T> = T extends ((...args: readonly unknown[]) => unknown) ? T : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepReadonly<U>> : T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T;
|
|
26
|
+
/** Make specific keys required while leaving the rest as-is. */
|
|
27
|
+
type RequireKeys<T, K extends keyof T> = T & { [P in K]-?: T[P] };
|
|
28
|
+
/** Make specific keys optional while leaving the rest as-is. */
|
|
29
|
+
type OptionalKeys<T, K extends keyof T> = Omit<T, K> & { [P in K]?: T[P] };
|
|
30
|
+
/** A value that may be absent in three flavours — `T | null | undefined`. */
|
|
31
|
+
type Nullable<T> = T | null | undefined;
|
|
32
|
+
/** Keys of T whose value type is assignable to V. */
|
|
33
|
+
type KeysMatching<T, V> = { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T];
|
|
34
|
+
/** Non-empty array guard type. */
|
|
35
|
+
type NonEmptyArray<T> = readonly [T, ...T[]];
|
|
36
|
+
//#endregion
|
|
37
|
+
export { NonEmptyArray as a, Prettify as c, KeysMatching as i, RequireKeys as l, DeepPartial as n, Nullable as o, DeepReadonly as r, OptionalKeys as s, Brand as t };
|
package/dist/brand.d.mts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as NonEmptyArray, c as Prettify, i as KeysMatching, l as RequireKeys, n as DeepPartial, o as Nullable, r as DeepReadonly, s as OptionalKeys, t as Brand } from "./brand-DApaegoR.mjs";
|
|
2
|
+
export { Brand, DeepPartial, DeepReadonly, KeysMatching, NonEmptyArray, Nullable, OptionalKeys, Prettify, RequireKeys };
|
package/dist/brand.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|