@classytic/party 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 +31 -0
- package/LICENSE +21 -0
- package/README.md +113 -0
- package/dist/events.d.ts +238 -0
- package/dist/events.js +255 -0
- package/dist/index.d.ts +592 -0
- package/dist/index.js +1269 -0
- package/dist/schemas.d.ts +125 -0
- package/dist/schemas.js +79 -0
- package/package.json +78 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
import { OperationOptions, PaginationConfig, PluginType, Repository } from "@classytic/mongokit";
|
|
2
|
+
import { Connection, Model, Schema, SchemaDefinitionProperty, Types } from "mongoose";
|
|
3
|
+
import { DomainEvent, EventHandler, EventMeta, EventTransport } from "@classytic/primitives/events";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { OutboxStore } from "@classytic/primitives/outbox";
|
|
6
|
+
import { OperationContext } from "@classytic/primitives/context";
|
|
7
|
+
import { ExternalRef, ExternalRef as ExternalRef$1 } from "@classytic/primitives/reference";
|
|
8
|
+
import { PersonName, PersonName as PersonName$1 } from "@classytic/primitives/person";
|
|
9
|
+
import { ContactAddress } from "@classytic/primitives/address";
|
|
10
|
+
import { HttpError } from "@classytic/repo-core/errors";
|
|
11
|
+
//#region src/domain/enums.d.ts
|
|
12
|
+
/**
|
|
13
|
+
* Party-native enumerations.
|
|
14
|
+
*
|
|
15
|
+
* These are identity-kernel concepts (kind, role, lifecycle) — they have no
|
|
16
|
+
* one-to-one primitive to subset, so they are declared here as the source of
|
|
17
|
+
* truth. Domain FACETS (finance terms, CRM stage, loyalty tier) live in their
|
|
18
|
+
* OWN packages keyed by `partyId`; they are deliberately NOT modelled here —
|
|
19
|
+
* that separation is the whole point of the SAP-style thin identity kernel
|
|
20
|
+
* (vs. Odoo's `res.partner` god-object).
|
|
21
|
+
*/
|
|
22
|
+
/** What kind of real-world entity a party is. */
|
|
23
|
+
declare const PARTY_KINDS: readonly ['person', 'company', 'group'];
|
|
24
|
+
type PartyKind = (typeof PARTY_KINDS)[number];
|
|
25
|
+
/**
|
|
26
|
+
* Roles a party plays. NON-exclusive — one party can be a customer AND a
|
|
27
|
+
* supplier AND a contact simultaneously (the exact de-duplication a shared
|
|
28
|
+
* identity buys you). Roles are additive flags, never separate entities.
|
|
29
|
+
*/
|
|
30
|
+
declare const PARTY_ROLES: readonly ['customer', 'supplier', 'contact', 'employee', 'partner'];
|
|
31
|
+
type PartyRole = (typeof PARTY_ROLES)[number];
|
|
32
|
+
/**
|
|
33
|
+
* Identity lifecycle. `merged` is terminal — the row survives as a redirect
|
|
34
|
+
* (`mergedInto`) so historical references (orders, invoices) still resolve.
|
|
35
|
+
*/
|
|
36
|
+
declare const PARTY_STATUSES: readonly ['active', 'archived', 'merged'];
|
|
37
|
+
type PartyStatus = (typeof PARTY_STATUSES)[number];
|
|
38
|
+
/** Channel kind for a contact point. Drives normalization + dedup keys. */
|
|
39
|
+
declare const CONTACT_POINT_TYPES: readonly ['email', 'phone'];
|
|
40
|
+
type ContactPointType = (typeof CONTACT_POINT_TYPES)[number];
|
|
41
|
+
/**
|
|
42
|
+
* Address role. Mirrors Odoo's `type` (contact/invoice/delivery/other) but
|
|
43
|
+
* as a first-class enum. A party carries many; `isDefault` picks the fallback.
|
|
44
|
+
*/
|
|
45
|
+
declare const ADDRESS_TYPES: readonly ['billing', 'shipping', 'contact', 'other'];
|
|
46
|
+
type AddressType = (typeof ADDRESS_TYPES)[number];
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/domain/value-objects.d.ts
|
|
49
|
+
/**
|
|
50
|
+
* A postal address on a party. Extends primitives' `ContactAddress` (recipient
|
|
51
|
+
* name/phone/email over the base `Address`) with a role + default flag —
|
|
52
|
+
* mirrors Odoo's per-address `type` without the child-partner overhead.
|
|
53
|
+
*/
|
|
54
|
+
interface PartyAddress extends ContactAddress {
|
|
55
|
+
type: AddressType;
|
|
56
|
+
isDefault: boolean;
|
|
57
|
+
/** Human label ("Head office", "Warehouse 2"). */
|
|
58
|
+
label?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A reachable identity channel. `value` is what the user typed; `key` is the
|
|
62
|
+
* normalized dedup form (`email:ada@x.com`, `phone:+8801...`). Structurally a
|
|
63
|
+
* normalized, verifiable superset of primitives' flat `ContactInfo` (§P5) — the
|
|
64
|
+
* kernel dedups on `key` but never mangles the display `value`.
|
|
65
|
+
*/
|
|
66
|
+
interface ContactPoint {
|
|
67
|
+
type: ContactPointType;
|
|
68
|
+
value: string;
|
|
69
|
+
/** Normalized dedup key — the `identityKeys` index entry. */
|
|
70
|
+
key: string;
|
|
71
|
+
isPrimary: boolean;
|
|
72
|
+
verified: boolean;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Statutory tax identity. Country-agnostic slots — a BD deployment fills
|
|
76
|
+
* `bin`/`vat`; `fiscalPositionCode` is an opaque host-policy string the active
|
|
77
|
+
* jurisdiction pack interprets. Finance TERMS (AR/AP accounts, credit, payment
|
|
78
|
+
* terms) are NOT here — they are the finance facet keyed by `partyId`, not
|
|
79
|
+
* identity.
|
|
80
|
+
*/
|
|
81
|
+
interface TaxIdentity {
|
|
82
|
+
vat?: string;
|
|
83
|
+
/** Business Identification Number (BD) or equivalent national registry id. */
|
|
84
|
+
bin?: string;
|
|
85
|
+
/** Opaque fiscal-position code the active jurisdiction pack resolves. */
|
|
86
|
+
fiscalPositionCode?: string;
|
|
87
|
+
/** ISO 3166-1 alpha-2 tax residence country. */
|
|
88
|
+
countryCode?: string;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The read-model a consuming domain freezes onto its own document (orders,
|
|
92
|
+
* invoices) at write time — the party equivalent of order's `customerSnapshot`.
|
|
93
|
+
* Never a live join; capture-at-time so a later merge/rename can't rewrite
|
|
94
|
+
* history.
|
|
95
|
+
*/
|
|
96
|
+
interface PartySnapshot {
|
|
97
|
+
partyId: string;
|
|
98
|
+
kind: PartyKind;
|
|
99
|
+
displayName: string;
|
|
100
|
+
legalName?: string;
|
|
101
|
+
primaryEmail?: string;
|
|
102
|
+
primaryPhone?: string;
|
|
103
|
+
roles: PartyRole[];
|
|
104
|
+
status: PartyStatus;
|
|
105
|
+
taxIdentity?: TaxIdentity;
|
|
106
|
+
}
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/domain/ports.d.ts
|
|
109
|
+
/**
|
|
110
|
+
* Operation context threaded through every domain verb. Extends primitives'
|
|
111
|
+
* `OperationContext`; narrows the id fields to what Mongoose stores cleanly.
|
|
112
|
+
*/
|
|
113
|
+
interface PartyContext extends OperationContext {
|
|
114
|
+
organizationId?: string | Types.ObjectId;
|
|
115
|
+
actorId?: string | Types.ObjectId;
|
|
116
|
+
/** Mongo transaction session, passed straight to mongokit. */
|
|
117
|
+
session?: unknown;
|
|
118
|
+
/** Cooperative cancellation — threaded to mongokit at choke points. */
|
|
119
|
+
signal?: AbortSignal;
|
|
120
|
+
/** Retry policy for driver round-trips (mongokit `RetryPolicy`). */
|
|
121
|
+
retryPolicy?: unknown;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Identity signals a consumer already knows about a party. `resolveOrCreate`
|
|
125
|
+
* matches on ANY of the normalized keys derived from `emails`/`phones`/`vat`/
|
|
126
|
+
* `bin`/`externalRefs` and falls back to creating a fresh party from the
|
|
127
|
+
* display fields. At least one identity signal OR `displayName` is required.
|
|
128
|
+
*/
|
|
129
|
+
interface ResolvePartyInput {
|
|
130
|
+
kind?: PartyKind;
|
|
131
|
+
displayName?: string;
|
|
132
|
+
legalName?: string;
|
|
133
|
+
/** Structured name for `kind: 'person'` — primitives' `PersonName` (§P3). */
|
|
134
|
+
personName?: PersonName$1;
|
|
135
|
+
emails?: string[];
|
|
136
|
+
phones?: string[];
|
|
137
|
+
addresses?: PartyAddress[];
|
|
138
|
+
taxIdentity?: TaxIdentity;
|
|
139
|
+
/** Roles to ensure on the resolved/created party (additive, deduped). */
|
|
140
|
+
roles?: PartyRole[];
|
|
141
|
+
/** External-store links to attach (migration seam) — primitives' `ExternalRef`. */
|
|
142
|
+
externalRefs?: ExternalRef$1[];
|
|
143
|
+
/** Auth user id to link (1:many — a company party has many portal users). */
|
|
144
|
+
userId?: string;
|
|
145
|
+
parentId?: string;
|
|
146
|
+
metadata?: Record<string, unknown>;
|
|
147
|
+
}
|
|
148
|
+
/** Result of a resolve — the party plus whether this call created it. */
|
|
149
|
+
interface ResolveResult<TParty> {
|
|
150
|
+
party: TParty;
|
|
151
|
+
created: boolean;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The reusable identity resolver. Domains depend on THIS, not on the party
|
|
155
|
+
* models — so the kernel can evolve its storage without touching consumers, and
|
|
156
|
+
* a non-Mongo host could implement the same port over a different backend.
|
|
157
|
+
*
|
|
158
|
+
* The kernel's `PartyRepository` implements it; hosts wire it into order / crm /
|
|
159
|
+
* ledger / commerce as the single identity source.
|
|
160
|
+
*/
|
|
161
|
+
interface PartyPort<TParty = unknown> {
|
|
162
|
+
/** Find an existing party by any identity signal, else create one. Race-safe. */
|
|
163
|
+
resolveOrCreate(input: ResolvePartyInput, ctx: PartyContext): Promise<ResolveResult<TParty>>;
|
|
164
|
+
/** Batch identity resolution — avoids N+1 when enriching a list. */
|
|
165
|
+
resolveMany(inputs: ResolvePartyInput[], ctx: PartyContext): Promise<Array<ResolveResult<TParty>>>;
|
|
166
|
+
/** The capture-at-time read model to freeze onto a consuming document. */
|
|
167
|
+
snapshot(partyId: string, ctx: PartyContext): Promise<PartySnapshot>;
|
|
168
|
+
}
|
|
169
|
+
/** Minimal logger surface the in-process bus + repositories log through. */
|
|
170
|
+
interface PartyLogger {
|
|
171
|
+
info?(msg: string, meta?: Record<string, unknown>): void;
|
|
172
|
+
warn?(msg: string, meta?: Record<string, unknown>): void;
|
|
173
|
+
error?(msg: string, meta?: Record<string, unknown>): void;
|
|
174
|
+
debug?(msg: string, meta?: Record<string, unknown>): void;
|
|
175
|
+
}
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/models/tenant-field.d.ts
|
|
178
|
+
type TenantFieldType = 'objectId' | 'string';
|
|
179
|
+
interface SchemaBuildConfig {
|
|
180
|
+
/** Schema type for the tenant column. Engine plumbs this everywhere. */
|
|
181
|
+
tenantFieldType: TenantFieldType;
|
|
182
|
+
/**
|
|
183
|
+
* When `false`, models carry NO tenant column (company-wide identity). The
|
|
184
|
+
* engine wires no `multiTenantPlugin` either. Default `true`.
|
|
185
|
+
*/
|
|
186
|
+
multiTenant: boolean;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Tenant-column schema definition, or `undefined` when the engine is
|
|
190
|
+
* company-wide (`multiTenant: false`) so the field is simply not added.
|
|
191
|
+
*/
|
|
192
|
+
declare function tenantFieldDef(config: SchemaBuildConfig): SchemaDefinitionProperty | undefined;
|
|
193
|
+
//#endregion
|
|
194
|
+
//#region src/models/party.model.d.ts
|
|
195
|
+
interface IPartyDocument {
|
|
196
|
+
_id: Types.ObjectId;
|
|
197
|
+
/** Present only when the engine runs multi-tenant; absent for company-wide identity. */
|
|
198
|
+
organizationId?: Types.ObjectId | string;
|
|
199
|
+
kind: PartyKind;
|
|
200
|
+
displayName: string;
|
|
201
|
+
legalName?: string;
|
|
202
|
+
personName?: PersonName$1;
|
|
203
|
+
contactPoints: ContactPoint[];
|
|
204
|
+
/** Normalized dedup keys derived from contactPoints + taxIdentity — the resolve index. */
|
|
205
|
+
identityKeys: string[];
|
|
206
|
+
addresses: PartyAddress[];
|
|
207
|
+
taxIdentity?: TaxIdentity;
|
|
208
|
+
roles: PartyRole[];
|
|
209
|
+
externalRefs: ExternalRef$1[];
|
|
210
|
+
/** Auth user ids linked to this party (1:many — a company has many portal logins). */
|
|
211
|
+
userLinks: string[];
|
|
212
|
+
parentId?: Types.ObjectId | string;
|
|
213
|
+
status: PartyStatus;
|
|
214
|
+
/** Set when `status: 'merged'` — the survivor this party redirects to. */
|
|
215
|
+
mergedInto?: Types.ObjectId | string;
|
|
216
|
+
tags: string[];
|
|
217
|
+
metadata?: Record<string, unknown>;
|
|
218
|
+
/** Optimistic-concurrency counter (drives `claimVersion`). */
|
|
219
|
+
version: number;
|
|
220
|
+
createdAt: Date;
|
|
221
|
+
updatedAt: Date;
|
|
222
|
+
}
|
|
223
|
+
declare function buildPartySchema(config: SchemaBuildConfig): Schema<IPartyDocument>;
|
|
224
|
+
//#endregion
|
|
225
|
+
//#region src/models/create-models.d.ts
|
|
226
|
+
interface PartyModels {
|
|
227
|
+
Party: Model<IPartyDocument>;
|
|
228
|
+
}
|
|
229
|
+
/** Mongoose model names this package registers — drives the §21 collision check. */
|
|
230
|
+
declare const PARTY_MODEL_NAMES: readonly ['Party'];
|
|
231
|
+
interface CreateModelsOptions extends SchemaBuildConfig {
|
|
232
|
+
/** On a name collision: `false` (default) throws; `true` deletes + re-registers (tests). */
|
|
233
|
+
forceRecreate?: boolean;
|
|
234
|
+
/** Mongoose auto-index builds. Default `true` (dev); `false` in prod (§35). */
|
|
235
|
+
autoIndex?: boolean;
|
|
236
|
+
}
|
|
237
|
+
declare function createPartyModels(connection: Connection, config: CreateModelsOptions): PartyModels;
|
|
238
|
+
//#endregion
|
|
239
|
+
//#region src/events/dispatch.d.ts
|
|
240
|
+
/** Context key carrying a post-commit publish queue (attached by `withTransaction`). */
|
|
241
|
+
declare const PENDING_EVENTS: unique symbol;
|
|
242
|
+
interface PartyEventDispatcherOptions {
|
|
243
|
+
transport: EventTransport;
|
|
244
|
+
outbox?: OutboxStore | undefined;
|
|
245
|
+
logger?: PartyLogger | undefined;
|
|
246
|
+
}
|
|
247
|
+
declare class PartyEventDispatcher {
|
|
248
|
+
readonly name: string;
|
|
249
|
+
private readonly transport;
|
|
250
|
+
private readonly outbox;
|
|
251
|
+
private readonly logger;
|
|
252
|
+
constructor(opts: PartyEventDispatcherOptions);
|
|
253
|
+
/** Raw publish — hosts and the unit-of-work flush use this. */
|
|
254
|
+
publish(event: DomainEvent): Promise<void>;
|
|
255
|
+
subscribe(pattern: string, handler: EventHandler): Promise<() => void>;
|
|
256
|
+
/**
|
|
257
|
+
* Route a domain event with full §P8 discipline. Reads the pending queue and
|
|
258
|
+
* session off `ctx`.
|
|
259
|
+
*/
|
|
260
|
+
dispatch(event: DomainEvent, ctx: PartyContext): Promise<void>;
|
|
261
|
+
close(): Promise<void>;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Drain a pending-events queue to the transport AFTER the owning transaction
|
|
265
|
+
* commits. Per-event failures are swallowed — durable delivery is the outbox's
|
|
266
|
+
* job (PACKAGE_RULES §P8.1).
|
|
267
|
+
*/
|
|
268
|
+
declare function flushPending(dispatcher: PartyEventDispatcher, queue: DomainEvent[], logger?: PartyLogger): Promise<void>;
|
|
269
|
+
//#endregion
|
|
270
|
+
//#region src/repositories/party.repository.d.ts
|
|
271
|
+
interface PartyRepositoryDeps {
|
|
272
|
+
events: PartyEventDispatcher;
|
|
273
|
+
logger?: PartyLogger | undefined;
|
|
274
|
+
}
|
|
275
|
+
declare class PartyRepository extends Repository<IPartyDocument> implements PartyPort<IPartyDocument> {
|
|
276
|
+
private readonly events;
|
|
277
|
+
constructor(model: Model<IPartyDocument>, deps: PartyRepositoryDeps, plugins?: PluginType[]);
|
|
278
|
+
/** `update` that never returns null — the party was loaded first; a null means a concurrent delete. */
|
|
279
|
+
private mustUpdate;
|
|
280
|
+
resolveOrCreate(input: ResolvePartyInput, ctx: PartyContext): Promise<ResolveResult<IPartyDocument>>;
|
|
281
|
+
resolveMany(inputs: ResolvePartyInput[], ctx: PartyContext): Promise<Array<ResolveResult<IPartyDocument>>>;
|
|
282
|
+
snapshot(partyId: string, ctx: PartyContext): Promise<PartySnapshot>;
|
|
283
|
+
/**
|
|
284
|
+
* Sync identity onto a party — the canonical "identity changed" verb. The
|
|
285
|
+
* party OWNS identity mutation (name, contact points, addresses, tax), so
|
|
286
|
+
* hosts push a facet's current identity here instead of editing it locally.
|
|
287
|
+
* NEW contact points fold in (deduped, deriving their keys + extending
|
|
288
|
+
* `identityKeys`); existing primaries are preserved. Idempotent — syncing the
|
|
289
|
+
* same identity is a no-op. Emits `party:party.updated`.
|
|
290
|
+
*/
|
|
291
|
+
syncIdentity(id: string, input: ResolvePartyInput, ctx: PartyContext): Promise<IPartyDocument>;
|
|
292
|
+
addRole(id: string, role: PartyRole, ctx: PartyContext): Promise<IPartyDocument>;
|
|
293
|
+
removeRole(id: string, role: PartyRole, ctx: PartyContext): Promise<IPartyDocument>;
|
|
294
|
+
linkUser(id: string, userId: string, ctx: PartyContext): Promise<IPartyDocument>;
|
|
295
|
+
attachExternalRef(id: string, ref: ExternalRef, ctx: PartyContext): Promise<IPartyDocument>;
|
|
296
|
+
archive(id: string, ctx: PartyContext): Promise<IPartyDocument>;
|
|
297
|
+
restore(id: string, ctx: PartyContext): Promise<IPartyDocument>;
|
|
298
|
+
/**
|
|
299
|
+
* Merge `sourceId` into `targetId`: the survivor absorbs the loser's identity
|
|
300
|
+
* keys, contact points, addresses, roles, external refs, and user links; the
|
|
301
|
+
* loser becomes a terminal `merged` redirect (`mergedInto`). Two-document
|
|
302
|
+
* write → runs in a transaction (falls back to sequential on standalone Mongo).
|
|
303
|
+
*/
|
|
304
|
+
merge(sourceId: string, targetId: string, ctx: PartyContext): Promise<IPartyDocument>;
|
|
305
|
+
/** Load a party by ObjectId hex, or throw the typed not-found (§P9-style). */
|
|
306
|
+
loadParty(id: string, ctx: PartyContext): Promise<IPartyDocument>;
|
|
307
|
+
/** Add any new roles/refs/user/keys from `input` onto an already-resolved party. */
|
|
308
|
+
private enrich;
|
|
309
|
+
/** Route a domain event with meta stamped from ctx + the party's org. */
|
|
310
|
+
private dispatch;
|
|
311
|
+
}
|
|
312
|
+
//#endregion
|
|
313
|
+
//#region src/engine/engine-types.d.ts
|
|
314
|
+
interface PartyConfig {
|
|
315
|
+
/** Mongoose connection — the package creates its model on this connection. */
|
|
316
|
+
connection: Connection;
|
|
317
|
+
/**
|
|
318
|
+
* Multi-tenant scoping. Identity is FREQUENTLY company-wide (one customer is
|
|
319
|
+
* the same person in every branch) — such hosts pass `false` and the model
|
|
320
|
+
* carries no tenant column. SaaS / true-isolation hosts pass `true` (default).
|
|
321
|
+
*/
|
|
322
|
+
multiTenant?: boolean;
|
|
323
|
+
/**
|
|
324
|
+
* Tenant column shape when `multiTenant` is on. `'objectId'` (default)
|
|
325
|
+
* matches Better Auth orgs (unlocks `$lookup`/`.populate()`); `'string'`
|
|
326
|
+
* matches UUID/slug hosts. Threaded into schema + plugin (§9.2).
|
|
327
|
+
*/
|
|
328
|
+
tenantFieldType?: TenantFieldType;
|
|
329
|
+
/** Arc-compatible event transport. Default: in-process bus. */
|
|
330
|
+
eventTransport?: EventTransport;
|
|
331
|
+
/** Transactional outbox (§P8). When wired, events persist in the write's session. */
|
|
332
|
+
outbox?: OutboxStore;
|
|
333
|
+
/** Logger surface for the in-process bus + repository. */
|
|
334
|
+
logger?: PartyLogger;
|
|
335
|
+
/** Pagination defaults for the repository (mongokit). */
|
|
336
|
+
pagination?: PaginationConfig;
|
|
337
|
+
/** Extra mongokit plugins applied after the defaults. */
|
|
338
|
+
plugins?: PluginType[];
|
|
339
|
+
/** On a model-name collision: `false` (default) throws; `true` re-registers (tests, §21). */
|
|
340
|
+
forceRecreate?: boolean;
|
|
341
|
+
/** Mongoose auto-index builds. Default `true` (dev); `false` in prod (§35). */
|
|
342
|
+
autoIndex?: boolean;
|
|
343
|
+
/** Downgrade the boot `transactions` capability requirement to a warning (standalone Mongo). */
|
|
344
|
+
allowNonTransactional?: boolean;
|
|
345
|
+
}
|
|
346
|
+
interface ResolvedPartyConfig {
|
|
347
|
+
connection: Connection;
|
|
348
|
+
multiTenant: boolean;
|
|
349
|
+
tenantFieldType: TenantFieldType;
|
|
350
|
+
forceRecreate: boolean;
|
|
351
|
+
autoIndex: boolean;
|
|
352
|
+
allowNonTransactional: boolean;
|
|
353
|
+
logger?: PartyLogger;
|
|
354
|
+
pagination?: PaginationConfig;
|
|
355
|
+
plugins: PluginType[];
|
|
356
|
+
}
|
|
357
|
+
interface PartyRepositories {
|
|
358
|
+
party: PartyRepository;
|
|
359
|
+
}
|
|
360
|
+
interface PartyEngine {
|
|
361
|
+
/** Repositories ARE the API surface (§1). */
|
|
362
|
+
readonly repositories: Readonly<PartyRepositories>;
|
|
363
|
+
/**
|
|
364
|
+
* The reusable identity contract other domains depend on (order / crm /
|
|
365
|
+
* ledger / commerce resolve identity through THIS, not the models). It IS the
|
|
366
|
+
* party repository, narrowed to the port — swap the storage without touching
|
|
367
|
+
* a single consumer.
|
|
368
|
+
*/
|
|
369
|
+
readonly port: PartyPort<IPartyDocument>;
|
|
370
|
+
/** Arc-compatible event transport (a dispatcher wrapping the configured one). */
|
|
371
|
+
readonly events: PartyEventDispatcher;
|
|
372
|
+
/** Mongoose models — for arc adapter wiring. */
|
|
373
|
+
readonly models: Readonly<PartyModels>;
|
|
374
|
+
/** Resolved configuration (frozen). */
|
|
375
|
+
readonly config: Readonly<ResolvedPartyConfig>;
|
|
376
|
+
/**
|
|
377
|
+
* Run `body` inside a transaction with post-commit event discipline (§P8.1):
|
|
378
|
+
* events dispatched through `txCtx` flush to the transport only after commit.
|
|
379
|
+
*/
|
|
380
|
+
withTransaction<T>(ctx: PartyContext, body: (txCtx: PartyContext) => Promise<T>): Promise<T>;
|
|
381
|
+
/** Build/align indexes. Call once per deploy when booted `autoIndex: false` (§35). */
|
|
382
|
+
syncIndexes(): Promise<void>;
|
|
383
|
+
/** Release resources. Idempotent. */
|
|
384
|
+
dispose(): Promise<void>;
|
|
385
|
+
}
|
|
386
|
+
//#endregion
|
|
387
|
+
//#region src/engine/create-party.d.ts
|
|
388
|
+
declare function createParty(config: PartyConfig): Promise<PartyEngine>;
|
|
389
|
+
//#endregion
|
|
390
|
+
//#region src/repositories/repo-options.d.ts
|
|
391
|
+
declare function repoOptionsFromCtx(ctx: PartyContext): OperationOptions;
|
|
392
|
+
//#endregion
|
|
393
|
+
//#region src/domain/state-machine.d.ts
|
|
394
|
+
/**
|
|
395
|
+
* Party identity lifecycle FSM — the compile-time twin of the `repo.claim()`
|
|
396
|
+
* CAS the repository uses for status transitions (PACKAGE_RULES §Concurrency).
|
|
397
|
+
*
|
|
398
|
+
* active ──archive──▶ archived ──restore──▶ active
|
|
399
|
+
* active ──merge────▶ merged (terminal; row survives as a `mergedInto` redirect)
|
|
400
|
+
*
|
|
401
|
+
* `merged` is terminal: a merged party is never resurrected — historical
|
|
402
|
+
* references resolve THROUGH it to the survivor, so the identity graph stays
|
|
403
|
+
* append-only and auditable.
|
|
404
|
+
*/
|
|
405
|
+
declare const PARTY_STATE_MACHINE: import("@classytic/primitives/state-machine").StateMachine<"active" | "archived" | "merged">;
|
|
406
|
+
//#endregion
|
|
407
|
+
//#region src/domain/identity.d.ts
|
|
408
|
+
/** De-duplicate an array preserving first-seen order. */
|
|
409
|
+
declare function dedupe<T>(arr: readonly T[]): T[];
|
|
410
|
+
/**
|
|
411
|
+
* Turn the input's `emails[]` / `phones[]` into normalized `ContactPoint`s. The
|
|
412
|
+
* first email and first phone are marked primary; blanks are skipped.
|
|
413
|
+
*/
|
|
414
|
+
declare function buildContactPoints(input: ResolvePartyInput): ContactPoint[];
|
|
415
|
+
/**
|
|
416
|
+
* The de-dup keys a party is resolved on: normalized emails/phones, VAT/BIN
|
|
417
|
+
* statutory ids, and external-store refs. Blank/degenerate keys are dropped so
|
|
418
|
+
* they never poison the unique index.
|
|
419
|
+
*/
|
|
420
|
+
declare function deriveIdentityKeys(contactPoints: readonly ContactPoint[], input: ResolvePartyInput): string[];
|
|
421
|
+
/** Best display name from the available signals — never empty. */
|
|
422
|
+
declare function resolveDisplayName(input: ResolvePartyInput, contactPoints: readonly ContactPoint[]): string;
|
|
423
|
+
/** The primary (or first) contact-point value of a channel type. */
|
|
424
|
+
declare function pickPrimary(contactPoints: readonly ContactPoint[], type: 'email' | 'phone'): string | undefined;
|
|
425
|
+
//#endregion
|
|
426
|
+
//#region src/domain/merge.d.ts
|
|
427
|
+
/** Union two string arrays, first-seen order, no duplicates. */
|
|
428
|
+
declare function unionStrings(a: readonly string[], b: readonly string[]): string[];
|
|
429
|
+
/** Union external refs by `(sourceModel, sourceId)` identity. */
|
|
430
|
+
declare function unionRefs(a: readonly ExternalRef[], b: readonly ExternalRef[]): ExternalRef[];
|
|
431
|
+
/**
|
|
432
|
+
* Union contact points by their normalized `key`. The survivor keeps its own
|
|
433
|
+
* primaries; absorbed points come in as non-primary (a merge must never create
|
|
434
|
+
* two primaries of the same channel).
|
|
435
|
+
*/
|
|
436
|
+
declare function unionContacts(a: readonly ContactPoint[], b: readonly ContactPoint[]): ContactPoint[];
|
|
437
|
+
//#endregion
|
|
438
|
+
//#region src/domain/normalize.d.ts
|
|
439
|
+
/** Lowercase + trim an email. Conservative — never strips dots/plus tags. */
|
|
440
|
+
declare function normalizeEmail(raw: string): string;
|
|
441
|
+
/**
|
|
442
|
+
* Canonical phone form for dedup. Uses primitives' `parsePhone`: a valid E.164
|
|
443
|
+
* input collapses to its canonical `+<cc><national>`; anything the strict parser
|
|
444
|
+
* rejects (local formats without a country code) falls back to a digit-only
|
|
445
|
+
* key so at least same-format duplicates still collapse. Hosts wanting global
|
|
446
|
+
* dedup pass already-E.164 numbers.
|
|
447
|
+
*/
|
|
448
|
+
declare function normalizePhone(raw: string): string;
|
|
449
|
+
/** The stable dedup key for a contact point (`email:ada@x.com`, `phone:+8801...`). */
|
|
450
|
+
declare function contactPointKey(type: ContactPointType, value: string): string;
|
|
451
|
+
/** A tax-identity dedup key (`vat:...` / `bin:...`) — cross-scope-stable statutory id. */
|
|
452
|
+
declare function taxIdentityKey(kind: 'vat' | 'bin', value: string): string;
|
|
453
|
+
//#endregion
|
|
454
|
+
//#region src/domain/errors.d.ts
|
|
455
|
+
/** Base class so hosts can `instanceof PartyError` across every party throwable. */
|
|
456
|
+
declare class PartyError extends Error implements HttpError {
|
|
457
|
+
readonly status: number;
|
|
458
|
+
readonly code: string;
|
|
459
|
+
readonly meta?: Record<string, unknown>;
|
|
460
|
+
constructor(status: number, code: string, message: string, meta?: Record<string, unknown>);
|
|
461
|
+
}
|
|
462
|
+
/** A referenced party id does not exist (or is out of the caller's scope). */
|
|
463
|
+
declare class PartyNotFoundError extends PartyError {
|
|
464
|
+
constructor(id: string);
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* A merge was attempted against an illegal target — self-merge, merging into an
|
|
468
|
+
* already-merged (redirected) party, or a cross-scope pair. Carries both ids.
|
|
469
|
+
*/
|
|
470
|
+
declare class PartyMergeError extends PartyError {
|
|
471
|
+
constructor(message: string, sourceId: string, targetId: string);
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* `resolveOrCreate` lost the create race and the winning row could not be
|
|
475
|
+
* re-read (should be transient — surfaced rather than swallowed).
|
|
476
|
+
*/
|
|
477
|
+
declare class PartyResolveConflictError extends PartyError {
|
|
478
|
+
constructor(identityKey: string);
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Optimistic-concurrency guard failed — the party was modified by another
|
|
482
|
+
* writer between read and write. Callers re-fetch and retry.
|
|
483
|
+
*/
|
|
484
|
+
declare class PartyVersionConflictError extends PartyError {
|
|
485
|
+
constructor(id: string, expected: number);
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* A Mongoose model name this package registers is already on the connection.
|
|
489
|
+
* Default init throws this; `forceRecreate: true` opts into delete+re-register
|
|
490
|
+
* (test fixtures / hot reload only). PACKAGE_RULES §21.
|
|
491
|
+
*/
|
|
492
|
+
declare class PartyModelCollisionError extends PartyError {
|
|
493
|
+
constructor(modelName: string);
|
|
494
|
+
}
|
|
495
|
+
/** Input failed a domain-verb precondition (before any DB write). */
|
|
496
|
+
declare class PartyValidationError extends PartyError {
|
|
497
|
+
constructor(message: string, meta?: Record<string, unknown>);
|
|
498
|
+
}
|
|
499
|
+
//#endregion
|
|
500
|
+
//#region src/events/event-constants.d.ts
|
|
501
|
+
/** PACKAGE_RULES §12: `package:resource.verb`. Emit ONLY these catalog names. */
|
|
502
|
+
declare const PARTY_EVENTS: {
|
|
503
|
+
readonly CREATED: 'party:party.created';
|
|
504
|
+
readonly UPDATED: 'party:party.updated';
|
|
505
|
+
readonly ROLE_ADDED: 'party:party.role_added';
|
|
506
|
+
readonly ROLE_REMOVED: 'party:party.role_removed';
|
|
507
|
+
readonly USER_LINKED: 'party:party.user_linked';
|
|
508
|
+
readonly EXTERNAL_REF_ATTACHED: 'party:party.external_ref_attached';
|
|
509
|
+
readonly ARCHIVED: 'party:party.archived';
|
|
510
|
+
readonly RESTORED: 'party:party.restored';
|
|
511
|
+
readonly MERGED: 'party:party.merged';
|
|
512
|
+
};
|
|
513
|
+
type PartyEventName = (typeof PARTY_EVENTS)[keyof typeof PARTY_EVENTS];
|
|
514
|
+
//#endregion
|
|
515
|
+
//#region src/events/event-payloads.d.ts
|
|
516
|
+
interface PartyCreatedPayload {
|
|
517
|
+
partyId: string;
|
|
518
|
+
kind: PartyKind;
|
|
519
|
+
displayName: string;
|
|
520
|
+
roles: PartyRole[];
|
|
521
|
+
organizationId?: string;
|
|
522
|
+
}
|
|
523
|
+
interface PartyUpdatedPayload {
|
|
524
|
+
partyId: string;
|
|
525
|
+
/** Top-level fields that changed on this write. */
|
|
526
|
+
changed: string[];
|
|
527
|
+
}
|
|
528
|
+
interface PartyRoleChangedPayload {
|
|
529
|
+
partyId: string;
|
|
530
|
+
role: PartyRole;
|
|
531
|
+
}
|
|
532
|
+
interface PartyUserLinkedPayload {
|
|
533
|
+
partyId: string;
|
|
534
|
+
userId: string;
|
|
535
|
+
}
|
|
536
|
+
interface PartyExternalRefAttachedPayload {
|
|
537
|
+
partyId: string;
|
|
538
|
+
sourceModel: string;
|
|
539
|
+
sourceId: string;
|
|
540
|
+
}
|
|
541
|
+
interface PartyLifecyclePayload {
|
|
542
|
+
partyId: string;
|
|
543
|
+
}
|
|
544
|
+
interface PartyMergedPayload {
|
|
545
|
+
/** The party that was merged away (now a redirect). */
|
|
546
|
+
sourceId: string;
|
|
547
|
+
/** The surviving party. */
|
|
548
|
+
targetId: string;
|
|
549
|
+
/** How many identity keys the survivor absorbed. */
|
|
550
|
+
absorbedKeys: number;
|
|
551
|
+
}
|
|
552
|
+
/** Name → payload map — subscribers get exact typing from the event name. */
|
|
553
|
+
interface PartyEventPayloads {
|
|
554
|
+
[PARTY_EVENTS.CREATED]: PartyCreatedPayload;
|
|
555
|
+
[PARTY_EVENTS.UPDATED]: PartyUpdatedPayload;
|
|
556
|
+
[PARTY_EVENTS.ROLE_ADDED]: PartyRoleChangedPayload;
|
|
557
|
+
[PARTY_EVENTS.ROLE_REMOVED]: PartyRoleChangedPayload;
|
|
558
|
+
[PARTY_EVENTS.USER_LINKED]: PartyUserLinkedPayload;
|
|
559
|
+
[PARTY_EVENTS.EXTERNAL_REF_ATTACHED]: PartyExternalRefAttachedPayload;
|
|
560
|
+
[PARTY_EVENTS.ARCHIVED]: PartyLifecyclePayload;
|
|
561
|
+
[PARTY_EVENTS.RESTORED]: PartyLifecyclePayload;
|
|
562
|
+
[PARTY_EVENTS.MERGED]: PartyMergedPayload;
|
|
563
|
+
}
|
|
564
|
+
//#endregion
|
|
565
|
+
//#region src/events/event-catalog.d.ts
|
|
566
|
+
interface PartyEventDefinition<TSchema extends z.ZodType = z.ZodType> {
|
|
567
|
+
name: string;
|
|
568
|
+
version: number;
|
|
569
|
+
description: string;
|
|
570
|
+
zodSchema: TSchema;
|
|
571
|
+
schema: Record<string, unknown>;
|
|
572
|
+
create(payload: z.infer<TSchema>, meta?: Partial<EventMeta>): DomainEvent<z.infer<TSchema>>;
|
|
573
|
+
}
|
|
574
|
+
declare const partyEventDefinitions: ReadonlyArray<PartyEventDefinition>;
|
|
575
|
+
//#endregion
|
|
576
|
+
//#region src/events/helpers.d.ts
|
|
577
|
+
declare function createEvent<T>(type: string, payload: T, ctx?: PartyContext, meta?: Partial<EventMeta>): DomainEvent<T>;
|
|
578
|
+
//#endregion
|
|
579
|
+
//#region src/events/in-process-bus.d.ts
|
|
580
|
+
declare class InProcessPartyBus implements EventTransport {
|
|
581
|
+
readonly name = "in-process-party";
|
|
582
|
+
private subs;
|
|
583
|
+
private readonly logger;
|
|
584
|
+
constructor(options?: {
|
|
585
|
+
logger?: PartyLogger | undefined;
|
|
586
|
+
});
|
|
587
|
+
publish(event: DomainEvent): Promise<void>;
|
|
588
|
+
subscribe(pattern: string, handler: EventHandler): Promise<() => void>;
|
|
589
|
+
close(): Promise<void>;
|
|
590
|
+
}
|
|
591
|
+
//#endregion
|
|
592
|
+
export { ADDRESS_TYPES, type AddressType, CONTACT_POINT_TYPES, type ContactPoint, type ContactPointType, type ExternalRef, type IPartyDocument, InProcessPartyBus, PARTY_EVENTS, PARTY_KINDS, PARTY_MODEL_NAMES, PARTY_ROLES, PARTY_STATE_MACHINE, PARTY_STATUSES, PENDING_EVENTS, type PartyAddress, type PartyConfig, type PartyContext, type PartyEngine, PartyError, PartyEventDispatcher, type PartyEventName, type PartyEventPayloads, type PartyKind, type PartyLogger, PartyMergeError, PartyModelCollisionError, type PartyModels, PartyNotFoundError, type PartyPort, type PartyRepositories, PartyRepository, type PartyRepositoryDeps, PartyResolveConflictError, type PartyRole, type PartySnapshot, type PartyStatus, PartyValidationError, PartyVersionConflictError, type PersonName, type ResolvePartyInput, type ResolveResult, type ResolvedPartyConfig, type SchemaBuildConfig, type TaxIdentity, type TenantFieldType, buildContactPoints, buildPartySchema, contactPointKey, createEvent, createParty, createParty as default, createPartyModels, dedupe, deriveIdentityKeys, flushPending, normalizeEmail, normalizePhone, partyEventDefinitions, pickPrimary, repoOptionsFromCtx, resolveDisplayName, taxIdentityKey, tenantFieldDef, unionContacts, unionRefs, unionStrings };
|