@classytic/party 0.1.0 → 0.2.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 +59 -0
- package/LICENSE +1 -1
- package/README.md +29 -4
- package/dist/events.d.ts +3 -8
- package/dist/events.js +21 -31
- package/dist/index.d.ts +253 -90
- package/dist/index.js +641 -329
- package/dist/maintenance.d.ts +165 -0
- package/dist/maintenance.js +353 -0
- package/package.json +15 -8
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import { OperationOptions, PaginationConfig, PluginType, Repository } from "@classytic/mongokit";
|
|
1
|
+
import { ModelBlueprint, OperationOptions, PaginationConfig, PluginType, Repository } from "@classytic/mongokit";
|
|
2
|
+
import { ResolvedTenantConfig, TenantConfig } from "@classytic/repo-core/tenant";
|
|
3
|
+
import { InProcessEventBus } from "@classytic/primitives/event-infra";
|
|
2
4
|
import { Connection, Model, Schema, SchemaDefinitionProperty, Types } from "mongoose";
|
|
3
5
|
import { DomainEvent, EventHandler, EventMeta, EventTransport } from "@classytic/primitives/events";
|
|
6
|
+
import { ExternalRef, ExternalRef as ExternalRef$1, IdLike } from "@classytic/primitives/reference";
|
|
4
7
|
import { z } from "zod";
|
|
5
8
|
import { OutboxStore } from "@classytic/primitives/outbox";
|
|
6
9
|
import { OperationContext } from "@classytic/primitives/context";
|
|
7
|
-
import { ExternalRef, ExternalRef as ExternalRef$1 } from "@classytic/primitives/reference";
|
|
8
10
|
import { PersonName, PersonName as PersonName$1 } from "@classytic/primitives/person";
|
|
9
11
|
import { ContactAddress } from "@classytic/primitives/address";
|
|
12
|
+
import { SubjectRef } from "@classytic/primitives/subject";
|
|
10
13
|
import { HttpError } from "@classytic/repo-core/errors";
|
|
11
14
|
//#region src/domain/enums.d.ts
|
|
12
15
|
/**
|
|
@@ -65,9 +68,22 @@ interface PartyAddress extends ContactAddress {
|
|
|
65
68
|
*/
|
|
66
69
|
interface ContactPoint {
|
|
67
70
|
type: ContactPointType;
|
|
71
|
+
/**
|
|
72
|
+
* The CANONICAL form — a lowercased email, or an E.164 phone. Identity is the
|
|
73
|
+
* normalized value only; this is also what a notification transport should
|
|
74
|
+
* dial or mail, so a stored local spelling is a second bug waiting behind the
|
|
75
|
+
* dedup one.
|
|
76
|
+
*/
|
|
68
77
|
value: string;
|
|
69
78
|
/** Normalized dedup key — the `identityKeys` index entry. */
|
|
70
79
|
key: string;
|
|
80
|
+
/**
|
|
81
|
+
* What the operator actually TYPED, kept only when it differs from `value`.
|
|
82
|
+
* Support-facing provenance ("the till shows 01712345678, the invoice shows
|
|
83
|
+
* +8801712345678 — same number"). Never an identity signal: it is not keyed,
|
|
84
|
+
* not indexed and not matched on.
|
|
85
|
+
*/
|
|
86
|
+
input?: string;
|
|
71
87
|
isPrimary: boolean;
|
|
72
88
|
verified: boolean;
|
|
73
89
|
}
|
|
@@ -177,13 +193,7 @@ interface PartyLogger {
|
|
|
177
193
|
//#region src/models/tenant-field.d.ts
|
|
178
194
|
type TenantFieldType = 'objectId' | 'string';
|
|
179
195
|
interface SchemaBuildConfig {
|
|
180
|
-
|
|
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;
|
|
196
|
+
tenant: ResolvedTenantConfig;
|
|
187
197
|
}
|
|
188
198
|
/**
|
|
189
199
|
* Tenant-column schema definition, or `undefined` when the engine is
|
|
@@ -226,15 +236,17 @@ declare function buildPartySchema(config: SchemaBuildConfig): Schema<IPartyDocum
|
|
|
226
236
|
interface PartyModels {
|
|
227
237
|
Party: Model<IPartyDocument>;
|
|
228
238
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
interface CreateModelsOptions extends SchemaBuildConfig {
|
|
232
|
-
/** On a name collision: `false` (default) throws; `true` deletes + re-registers (tests). */
|
|
239
|
+
interface DefinePartyModelsConfig extends SchemaBuildConfig {
|
|
240
|
+
/** On a name collision: `false` (default) throws; `true` re-registers (test/dev fixtures). */
|
|
233
241
|
forceRecreate?: boolean;
|
|
234
242
|
/** Mongoose auto-index builds. Default `true` (dev); `false` in prod (§35). */
|
|
235
243
|
autoIndex?: boolean;
|
|
236
244
|
}
|
|
237
|
-
|
|
245
|
+
/**
|
|
246
|
+
* Describe the Party model set as a connection-free blueprint. Pure — builds no schema and
|
|
247
|
+
* registers nothing until `.bind(connection)`.
|
|
248
|
+
*/
|
|
249
|
+
declare function definePartyModels(config: DefinePartyModelsConfig): ModelBlueprint<PartyModels>;
|
|
238
250
|
//#endregion
|
|
239
251
|
//#region src/events/dispatch.d.ts
|
|
240
252
|
/** Context key carrying a post-commit publish queue (attached by `withTransaction`). */
|
|
@@ -271,9 +283,26 @@ declare function flushPending(dispatcher: PartyEventDispatcher, queue: DomainEve
|
|
|
271
283
|
interface PartyRepositoryDeps {
|
|
272
284
|
events: PartyEventDispatcher;
|
|
273
285
|
logger?: PartyLogger | undefined;
|
|
286
|
+
/**
|
|
287
|
+
* ISO 3166-1 alpha-2 region used to interpret phone input that carries no
|
|
288
|
+
* country code. Comes from `PartyShape.defaultPhoneRegion`, i.e. the
|
|
289
|
+
* deployment — this kernel names no country.
|
|
290
|
+
*
|
|
291
|
+
* Held on the repository rather than passed per call so `resolveOrCreate` and
|
|
292
|
+
* `syncIdentity` cannot disagree: a caller-supplied region would let the
|
|
293
|
+
* create path and the sync path key the same human differently, which is the
|
|
294
|
+
* split this whole change removes.
|
|
295
|
+
*/
|
|
296
|
+
defaultPhoneRegion?: string | undefined;
|
|
297
|
+
/** Deployment-wide contact channels used as authoritative identity keys. */
|
|
298
|
+
identityContactTypes?: readonly ContactPointType[] | undefined;
|
|
274
299
|
}
|
|
275
300
|
declare class PartyRepository extends Repository<IPartyDocument> implements PartyPort<IPartyDocument> {
|
|
276
301
|
private readonly events;
|
|
302
|
+
/** The ONE interpretation of free-form contact input in this deployment. */
|
|
303
|
+
private readonly normalize;
|
|
304
|
+
/** Contact data can exist without being strong enough to collapse two parties. */
|
|
305
|
+
private readonly identityContactTypes;
|
|
277
306
|
constructor(model: Model<IPartyDocument>, deps: PartyRepositoryDeps, plugins?: PluginType[]);
|
|
278
307
|
/** `update` that never returns null — the party was loaded first; a null means a concurrent delete. */
|
|
279
308
|
private mustUpdate;
|
|
@@ -297,9 +326,17 @@ declare class PartyRepository extends Repository<IPartyDocument> implements Part
|
|
|
297
326
|
restore(id: string, ctx: PartyContext): Promise<IPartyDocument>;
|
|
298
327
|
/**
|
|
299
328
|
* Merge `sourceId` into `targetId`: the survivor absorbs the loser's identity
|
|
300
|
-
* keys, contact points, addresses, roles, external refs,
|
|
301
|
-
*
|
|
302
|
-
*
|
|
329
|
+
* keys, contact points, addresses, roles, external refs, user links, tags, and
|
|
330
|
+
* metadata, and FILLS its empty scalar slots (`taxIdentity`, `personName`,
|
|
331
|
+
* `legalName`, `parentId`) from the loser — never overwriting a populated
|
|
332
|
+
* survivor field. The loser becomes a terminal `merged` redirect
|
|
333
|
+
* (`mergedInto`). Two-document write → runs in a transaction (falls back to
|
|
334
|
+
* sequential on standalone Mongo).
|
|
335
|
+
*
|
|
336
|
+
* Concurrency: the loser is claimed `active|archived → merged` via CAS
|
|
337
|
+
* (`repo.claim`), so a second concurrent merge of the same source loses the
|
|
338
|
+
* race and surfaces a typed `PartyMergeError` — never a raw `E11000`. A
|
|
339
|
+
* duplicate-key collision while adopting the loser's keys maps to the same.
|
|
303
340
|
*/
|
|
304
341
|
merge(sourceId: string, targetId: string, ctx: PartyContext): Promise<IPartyDocument>;
|
|
305
342
|
/** Load a party by ObjectId hex, or throw the typed not-found (§P9-style). */
|
|
@@ -311,44 +348,15 @@ declare class PartyRepository extends Repository<IPartyDocument> implements Part
|
|
|
311
348
|
}
|
|
312
349
|
//#endregion
|
|
313
350
|
//#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
351
|
interface ResolvedPartyConfig {
|
|
347
352
|
connection: Connection;
|
|
348
|
-
|
|
349
|
-
tenantFieldType: TenantFieldType;
|
|
353
|
+
tenant: Readonly<ResolvedTenantConfig>;
|
|
350
354
|
forceRecreate: boolean;
|
|
351
355
|
autoIndex: boolean;
|
|
356
|
+
/** ISO 3166-1 alpha-2 region for national-format phone input; unset ⇒ E.164 only. */
|
|
357
|
+
defaultPhoneRegion?: string;
|
|
358
|
+
/** Contact channels used as authoritative unique identity keys. */
|
|
359
|
+
identityContactTypes: readonly ContactPointType[];
|
|
352
360
|
allowNonTransactional: boolean;
|
|
353
361
|
logger?: PartyLogger;
|
|
354
362
|
pagination?: PaginationConfig;
|
|
@@ -380,16 +388,141 @@ interface PartyEngine {
|
|
|
380
388
|
withTransaction<T>(ctx: PartyContext, body: (txCtx: PartyContext) => Promise<T>): Promise<T>;
|
|
381
389
|
/** Build/align indexes. Call once per deploy when booted `autoIndex: false` (§35). */
|
|
382
390
|
syncIndexes(): Promise<void>;
|
|
383
|
-
/** Release resources. Idempotent. */
|
|
384
|
-
|
|
391
|
+
/** Release resources owned by this engine. Idempotent. The kernel-standard lifecycle name. */
|
|
392
|
+
close(): Promise<void>;
|
|
385
393
|
}
|
|
386
394
|
//#endregion
|
|
387
|
-
//#region src/engine/
|
|
388
|
-
|
|
395
|
+
//#region src/engine/define-party.d.ts
|
|
396
|
+
/**
|
|
397
|
+
* DESCRIBE-time shape — everything that determines the persistence SHAPE and needs no
|
|
398
|
+
* connection or I/O to inspect. Nothing here opens a socket or reads a secret.
|
|
399
|
+
*/
|
|
400
|
+
interface PartyShape {
|
|
401
|
+
/** Company-wide identity (`false`, no tenant column) vs isolated (`true`, default). */
|
|
402
|
+
tenant?: boolean | TenantConfig;
|
|
403
|
+
/** Mongoose auto-index builds. Default `true` (dev); pass `false` in prod (§35). */
|
|
404
|
+
autoIndex?: boolean;
|
|
405
|
+
/**
|
|
406
|
+
* On a model-name collision: default throws; `true` re-registers (test/dev fixtures).
|
|
407
|
+
* Maps to mongokit's `replace` policy, which itself throws under `NODE_ENV=production`.
|
|
408
|
+
*/
|
|
409
|
+
forceRecreate?: boolean;
|
|
410
|
+
/**
|
|
411
|
+
* ISO 3166-1 alpha-2 region used to read phone input that carries no country
|
|
412
|
+
* code — `'BD'` for a Bangladeshi deployment, `'GB'` for a British one. THIS
|
|
413
|
+
* KERNEL NAMES NO COUNTRY: the value is the deployment's, and belongs in
|
|
414
|
+
* `deployment.config.ts`.
|
|
415
|
+
*
|
|
416
|
+
* It is describe-time because it determines the persisted `identityKeys`, not
|
|
417
|
+
* just a request's interpretation — two boots with different regions would key
|
|
418
|
+
* the same human differently and fork the identity.
|
|
419
|
+
*
|
|
420
|
+
* Unset ⇒ only explicit E.164 phones are accepted; a national-format number is
|
|
421
|
+
* REFUSED rather than guessed at (FAIL LOUD rule 2). That is the safe default:
|
|
422
|
+
* before this option existed, a rejected parse silently degraded to
|
|
423
|
+
* `+${digits}`, and `01309000993` / `+8801309000993` became two parties for one
|
|
424
|
+
* member with nothing reporting it.
|
|
425
|
+
*/
|
|
426
|
+
defaultPhoneRegion?: string;
|
|
427
|
+
/**
|
|
428
|
+
* Contact channels authoritative enough to resolve two records as the same
|
|
429
|
+
* party. Defaults to both `email` and `phone` for backward compatibility.
|
|
430
|
+
*
|
|
431
|
+
* Set `[]` when shared household/company contact details are candidate-match
|
|
432
|
+
* signals rather than proof of identity. Contacts are still normalized and
|
|
433
|
+
* stored; tax identifiers and external references remain identity keys.
|
|
434
|
+
*
|
|
435
|
+
* Persistence-shaping configuration: changing it does not rewrite existing
|
|
436
|
+
* `identityKeys`; migrate existing rows before changing a live deployment.
|
|
437
|
+
*/
|
|
438
|
+
identityContactTypes?: readonly ContactPointType[];
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* BIND-time runtime collaborators — the live connection is the first `bind` arg; these are the
|
|
442
|
+
* rest. A provider client, transport, logger or store belongs here, not in the shape.
|
|
443
|
+
*/
|
|
444
|
+
interface PartyRuntime {
|
|
445
|
+
eventTransport?: EventTransport;
|
|
446
|
+
outbox?: OutboxStore;
|
|
447
|
+
logger?: PartyLogger;
|
|
448
|
+
pagination?: PaginationConfig;
|
|
449
|
+
/** Downgrade the boot `transactions` capability requirement to a warning (standalone Mongo). */
|
|
450
|
+
allowNonTransactional?: boolean;
|
|
451
|
+
/** Extra mongokit plugins applied after the defaults. */
|
|
452
|
+
plugins?: PluginType[];
|
|
453
|
+
}
|
|
454
|
+
interface PartyBlueprint {
|
|
455
|
+
readonly id: 'party';
|
|
456
|
+
/** The model names this blueprint registers — known without a connection. */
|
|
457
|
+
readonly modelNames: readonly string[];
|
|
458
|
+
/**
|
|
459
|
+
* Compile + register the model on `connection`, wire the engine, verify capabilities.
|
|
460
|
+
* Synchronous: party's construction performs no async work (model registration and repo
|
|
461
|
+
* wiring are synchronous). The `KernelBlueprint` contract still permits async binds.
|
|
462
|
+
*/
|
|
463
|
+
bind(connection: Connection, runtime?: PartyRuntime): PartyEngine;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Describe the party engine. Pure — registers no model, builds no schema, performs no I/O.
|
|
467
|
+
*/
|
|
468
|
+
declare function defineParty(shape?: PartyShape): PartyBlueprint;
|
|
389
469
|
//#endregion
|
|
390
470
|
//#region src/repositories/repo-options.d.ts
|
|
391
471
|
declare function repoOptionsFromCtx(ctx: PartyContext): OperationOptions;
|
|
392
472
|
//#endregion
|
|
473
|
+
//#region src/domain/subject.d.ts
|
|
474
|
+
/**
|
|
475
|
+
* The canonical `subjectModel` value for a Party.
|
|
476
|
+
*
|
|
477
|
+
* Import this — never write `'Party'` in a host, a module, or a test.
|
|
478
|
+
*/
|
|
479
|
+
declare const PARTY_SUBJECT_MODEL = "Party";
|
|
480
|
+
/**
|
|
481
|
+
* A Party as other kernels reference it.
|
|
482
|
+
*
|
|
483
|
+
* `subjectModel` is narrowed to the literal type so assigning a hand-typed
|
|
484
|
+
* string is a compile error at the call site, not a runtime mismatch discovered
|
|
485
|
+
* when a card fails to deactivate.
|
|
486
|
+
*/
|
|
487
|
+
interface PartySubject extends SubjectRef {
|
|
488
|
+
/**
|
|
489
|
+
* Narrowed to the literal so assigning a hand-typed string is a COMPILE error
|
|
490
|
+
* at the call site, not a runtime mismatch discovered when a card fails to
|
|
491
|
+
* deactivate.
|
|
492
|
+
*
|
|
493
|
+
* Extends `SubjectRef` from `@classytic/primitives/subject` rather than
|
|
494
|
+
* re-declaring the pair: that primitive is the canonical shape for "who a record
|
|
495
|
+
* is about", shared by attendance, access, credentials and loyalty — and by the
|
|
496
|
+
* SDK, so a UI cannot bind a card to a different model than entitlements are
|
|
497
|
+
* granted against. (Deliberately NOT `ExternalRef`, which is the same shape for a
|
|
498
|
+
* different question — provenance, not identity. A grant carries both at once.)
|
|
499
|
+
*/
|
|
500
|
+
subjectModel: typeof PARTY_SUBJECT_MODEL;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Build the subject pair for a Party.
|
|
504
|
+
*
|
|
505
|
+
* Accepts anything id-shaped (`ObjectId`, hex string, a document's `_id`) and
|
|
506
|
+
* stringifies through primitives' `idToString`, because the receiving kernels
|
|
507
|
+
* store `subjectRef` as a String — a raw `ObjectId` would stringify implicitly
|
|
508
|
+
* somewhere downstream and the two forms would not compare equal.
|
|
509
|
+
*
|
|
510
|
+
* Returns `null` for a missing id rather than a pair containing `"undefined"`.
|
|
511
|
+
* That string is a real hazard here: it is a *valid* `subjectRef` as far as the
|
|
512
|
+
* schema is concerned, so it persists, matches nothing, and looks like data.
|
|
513
|
+
*/
|
|
514
|
+
declare function partySubject(id: IdLike | null | undefined): PartySubject | null;
|
|
515
|
+
/**
|
|
516
|
+
* Does this subject pair refer to a Party?
|
|
517
|
+
*
|
|
518
|
+
* For a resolver that fans a mixed subject stream out to the right kernel — an
|
|
519
|
+
* attendance list can hold Party members and, in an HR deployment, employees
|
|
520
|
+
* from another identity source.
|
|
521
|
+
*/
|
|
522
|
+
declare function isPartySubject(subject: {
|
|
523
|
+
subjectModel?: string | undefined;
|
|
524
|
+
}): boolean;
|
|
525
|
+
//#endregion
|
|
393
526
|
//#region src/domain/state-machine.d.ts
|
|
394
527
|
/**
|
|
395
528
|
* Party identity lifecycle FSM — the compile-time twin of the `repo.claim()`
|
|
@@ -404,20 +537,62 @@ declare function repoOptionsFromCtx(ctx: PartyContext): OperationOptions;
|
|
|
404
537
|
*/
|
|
405
538
|
declare const PARTY_STATE_MACHINE: import("@classytic/primitives/state-machine").StateMachine<"active" | "archived" | "merged">;
|
|
406
539
|
//#endregion
|
|
540
|
+
//#region src/domain/normalize.d.ts
|
|
541
|
+
/**
|
|
542
|
+
* How free-form contact input is interpreted. Threaded from the engine's
|
|
543
|
+
* resolved config so every path — `resolveOrCreate`, `syncIdentity`, merge —
|
|
544
|
+
* reads the same answer.
|
|
545
|
+
*/
|
|
546
|
+
interface NormalizeOptions {
|
|
547
|
+
/**
|
|
548
|
+
* ISO 3166-1 alpha-2 region applied to phone input that carries no country
|
|
549
|
+
* code (`01712345678` on a Bangladeshi till). Omitted ⇒ E.164 only.
|
|
550
|
+
*/
|
|
551
|
+
defaultRegion?: string | undefined;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Canonical email for the dedup key. Trim + lowercase, and deliberately NOT
|
|
555
|
+
* gmail dot / `+tag` folding — `a+shop@x.com` is a real, distinct inbox, and
|
|
556
|
+
* folding it MERGES two people, which is the irreversible direction.
|
|
557
|
+
*
|
|
558
|
+
* The rule moved to `@classytic/contact/email` unchanged for the canonical case,
|
|
559
|
+
* with one addition: an address that cannot be canonicalised now THROWS instead of
|
|
560
|
+
* becoming an identity key. Same reasoning as the phone side — a malformed address
|
|
561
|
+
* still makes a perfectly stable key, so admitting it does not fail, it forks.
|
|
562
|
+
*/
|
|
563
|
+
declare function normalizeEmail(raw: string): string;
|
|
564
|
+
/** The stable dedup key for a contact point (`email:ada@x.com`, `phone:+8801...`). */
|
|
565
|
+
declare function contactPointKey(type: ContactPointType, value: string, options?: NormalizeOptions): string;
|
|
566
|
+
/** A tax-identity dedup key (`vat:...` / `bin:...`) — cross-scope-stable statutory id. */
|
|
567
|
+
declare function taxIdentityKey(kind: 'vat' | 'bin', value: string): string;
|
|
568
|
+
//#endregion
|
|
407
569
|
//#region src/domain/identity.d.ts
|
|
570
|
+
interface IdentityKeyOptions {
|
|
571
|
+
/** Contact channels authoritative enough to resolve two records as one party. Default: both. */
|
|
572
|
+
contactTypes?: readonly ContactPointType[];
|
|
573
|
+
}
|
|
408
574
|
/** De-duplicate an array preserving first-seen order. */
|
|
409
575
|
declare function dedupe<T>(arr: readonly T[]): T[];
|
|
410
576
|
/**
|
|
411
577
|
* Turn the input's `emails[]` / `phones[]` into normalized `ContactPoint`s. The
|
|
412
578
|
* first email and first phone are marked primary; blanks are skipped.
|
|
579
|
+
*
|
|
580
|
+
* THE SEAM. Both `resolveOrCreate` and `syncIdentity` funnel here, so this is
|
|
581
|
+
* the one place normalization can be enforced — a fix applied at N call sites
|
|
582
|
+
* is the same bug waiting at N+1.
|
|
583
|
+
*
|
|
584
|
+
* Two spellings of one number in a single input collapse to ONE point (they
|
|
585
|
+
* share a key). An unparseable phone THROWS rather than being skipped: skipping
|
|
586
|
+
* would create a party with no phone identity at all, which the next
|
|
587
|
+
* well-formed visit would not resolve onto — a silent fork by a different route.
|
|
413
588
|
*/
|
|
414
|
-
declare function buildContactPoints(input: ResolvePartyInput): ContactPoint[];
|
|
589
|
+
declare function buildContactPoints(input: ResolvePartyInput, options?: NormalizeOptions): ContactPoint[];
|
|
415
590
|
/**
|
|
416
591
|
* The de-dup keys a party is resolved on: normalized emails/phones, VAT/BIN
|
|
417
592
|
* statutory ids, and external-store refs. Blank/degenerate keys are dropped so
|
|
418
593
|
* they never poison the unique index.
|
|
419
594
|
*/
|
|
420
|
-
declare function deriveIdentityKeys(contactPoints: readonly ContactPoint[], input: ResolvePartyInput): string[];
|
|
595
|
+
declare function deriveIdentityKeys(contactPoints: readonly ContactPoint[], input: ResolvePartyInput, options?: IdentityKeyOptions): string[];
|
|
421
596
|
/** Best display name from the available signals — never empty. */
|
|
422
597
|
declare function resolveDisplayName(input: ResolvePartyInput, contactPoints: readonly ContactPoint[]): string;
|
|
423
598
|
/** The primary (or first) contact-point value of a channel type. */
|
|
@@ -435,22 +610,6 @@ declare function unionRefs(a: readonly ExternalRef[], b: readonly ExternalRef[])
|
|
|
435
610
|
*/
|
|
436
611
|
declare function unionContacts(a: readonly ContactPoint[], b: readonly ContactPoint[]): ContactPoint[];
|
|
437
612
|
//#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
613
|
//#region src/domain/errors.d.ts
|
|
455
614
|
/** Base class so hosts can `instanceof PartyError` across every party throwable. */
|
|
456
615
|
declare class PartyError extends Error implements HttpError {
|
|
@@ -484,18 +643,28 @@ declare class PartyResolveConflictError extends PartyError {
|
|
|
484
643
|
declare class PartyVersionConflictError extends PartyError {
|
|
485
644
|
constructor(id: string, expected: number);
|
|
486
645
|
}
|
|
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
646
|
/** Input failed a domain-verb precondition (before any DB write). */
|
|
496
647
|
declare class PartyValidationError extends PartyError {
|
|
497
648
|
constructor(message: string, meta?: Record<string, unknown>);
|
|
498
649
|
}
|
|
650
|
+
/**
|
|
651
|
+
* A phone contact point could not be normalized to E.164 — so it cannot become
|
|
652
|
+
* an identity key.
|
|
653
|
+
*
|
|
654
|
+
* This REFUSES rather than degrades, and that is the whole point. The previous
|
|
655
|
+
* behaviour fell back to `+${digits}`, which produced keys like
|
|
656
|
+
* `phone:+01309000993` — neither a national number nor E.164. Two spellings of
|
|
657
|
+
* one member therefore keyed differently and `resolveOrCreate` forked the
|
|
658
|
+
* identity, silently: every screen looked correct and the membership history
|
|
659
|
+
* simply split in two (FAIL LOUD rule 2 — an input we do not understand must
|
|
660
|
+
* fail, not widen).
|
|
661
|
+
*
|
|
662
|
+
* `meta` names the offending value and the region it was interpreted in, so an
|
|
663
|
+
* operator can fix the data instead of guessing which field a 400 refers to.
|
|
664
|
+
*/
|
|
665
|
+
declare class PartyPhoneFormatError extends PartyError {
|
|
666
|
+
constructor(value: string, defaultRegion: string | undefined, detail: string);
|
|
667
|
+
}
|
|
499
668
|
//#endregion
|
|
500
669
|
//#region src/events/event-constants.d.ts
|
|
501
670
|
/** PACKAGE_RULES §12: `package:resource.verb`. Emit ONLY these catalog names. */
|
|
@@ -577,16 +746,10 @@ declare const partyEventDefinitions: ReadonlyArray<PartyEventDefinition>;
|
|
|
577
746
|
declare function createEvent<T>(type: string, payload: T, ctx?: PartyContext, meta?: Partial<EventMeta>): DomainEvent<T>;
|
|
578
747
|
//#endregion
|
|
579
748
|
//#region src/events/in-process-bus.d.ts
|
|
580
|
-
declare class InProcessPartyBus
|
|
581
|
-
readonly name = "in-process-party";
|
|
582
|
-
private subs;
|
|
583
|
-
private readonly logger;
|
|
749
|
+
declare class InProcessPartyBus extends InProcessEventBus {
|
|
584
750
|
constructor(options?: {
|
|
585
|
-
logger?: PartyLogger
|
|
751
|
+
logger?: PartyLogger;
|
|
586
752
|
});
|
|
587
|
-
publish(event: DomainEvent): Promise<void>;
|
|
588
|
-
subscribe(pattern: string, handler: EventHandler): Promise<() => void>;
|
|
589
|
-
close(): Promise<void>;
|
|
590
753
|
}
|
|
591
754
|
//#endregion
|
|
592
|
-
export { ADDRESS_TYPES, type AddressType, CONTACT_POINT_TYPES, type ContactPoint, type ContactPointType, type ExternalRef, type IPartyDocument, InProcessPartyBus, PARTY_EVENTS, PARTY_KINDS,
|
|
755
|
+
export { ADDRESS_TYPES, type AddressType, CONTACT_POINT_TYPES, type ContactPoint, type ContactPointType, type ExternalRef, type IPartyDocument, type IdentityKeyOptions, InProcessPartyBus, type NormalizeOptions, PARTY_EVENTS, PARTY_KINDS, PARTY_ROLES, PARTY_STATE_MACHINE, PARTY_STATUSES, PARTY_SUBJECT_MODEL, PENDING_EVENTS, type PartyAddress, type PartyBlueprint, type PartyContext, type PartyEngine, PartyError, PartyEventDispatcher, type PartyEventName, type PartyEventPayloads, type PartyKind, type PartyLogger, PartyMergeError, type PartyModels, PartyNotFoundError, PartyPhoneFormatError, type PartyPort, type PartyRepositories, PartyRepository, type PartyRepositoryDeps, PartyResolveConflictError, type PartyRole, type PartyRuntime, type PartyShape, type PartySnapshot, type PartyStatus, type PartySubject, PartyValidationError, PartyVersionConflictError, type PersonName, type ResolvePartyInput, type ResolveResult, type ResolvedPartyConfig, type SchemaBuildConfig, type TaxIdentity, type TenantFieldType, buildContactPoints, buildPartySchema, contactPointKey, createEvent, dedupe, defineParty, definePartyModels, deriveIdentityKeys, flushPending, isPartySubject, normalizeEmail, partyEventDefinitions, partySubject, pickPrimary, repoOptionsFromCtx, resolveDisplayName, taxIdentityKey, tenantFieldDef, unionContacts, unionRefs, unionStrings };
|