@rytass/bpm-core-nestjs-module 0.1.4 → 0.1.6

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 CHANGED
@@ -9,6 +9,89 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
  Releases are managed by [`nx release`](https://nx.dev/recipes/nx-release) with
10
10
  Conventional Commits — see `nx.json` for the release config.
11
11
 
12
+ ## 0.1.6 — 2026-05-28
13
+
14
+ ### Added
15
+
16
+ - **`MemberNotFoundException`** is now exported from the root barrel
17
+ (and `/identity` subpath). The `BPMMemberResolver.resolve` JSDoc has
18
+ referenced this class for two releases without actually shipping it;
19
+ hosts can now `throw new MemberNotFoundException(memberId)` directly.
20
+
21
+ ### Documentation
22
+
23
+ - **README "Mapping from a host RBAC system (e.g. Casbin)"**: worked
24
+ example showing how to project a host's grouping policy
25
+ (`enforcer.getRolesForUser(memberId)`) into BPM's exact-string role
26
+ literals inside `authContextFactory`.
27
+ - **README "Cross-origin authentication"** under Attachment Storage:
28
+ documents that signed URLs carry auth inside the URL (not via cookie),
29
+ what the TTL controls, and the production-grade defenses on top.
30
+ - **README "Disabling the `/auth/test-members` endpoint in production"**:
31
+ clarifies that the endpoint is wrapper-host-owned (BPM does not
32
+ register it) and that the React client degrades gracefully when the
33
+ endpoint is absent.
34
+ - **README Worked Example rewritten** to call the actual published
35
+ `@rytass/bpm-core-client/organization` exports
36
+ (`readOrganizationDashboard`, flat-input `createOrgUnit` /
37
+ `updateOrgUnit`) — the 0.1.5 example called functions that didn't
38
+ exist. Adds an "Atomicity caveat" note clarifying that only
39
+ `commitOrgUnitTreeDraft` is transactional; sequential
40
+ `createMembership` calls are independent.
41
+
42
+ ### Why a patch
43
+
44
+ `MemberNotFoundException` is purely additive. Other changes are docs.
45
+
46
+ ## 0.1.5 — 2026-05-28
47
+
48
+ ### Documentation
49
+
50
+ - **Identity contract clarifications.** `BPMMemberResolver.resolve` JSDoc
51
+ now states the unknown-id contract explicitly (throw
52
+ `MemberNotFoundException`); `resolveMany` documents the diverging
53
+ partial-success contract (omit unknown ids from the returned `Map`).
54
+ Both methods cross-link `identityMemberMetadataCacheTtlMs`
55
+ (default 5 min) so consumers know BPM caches their resolver responses.
56
+ - **`BPMRootModule` JSDoc dangling refs removed.** The previous
57
+ references to `docs/10-bpm-embedding-auth.md` and
58
+ `docs/11-consumer-quickstart.md` (files that never shipped) are now
59
+ inline pointers to the README sections.
60
+ - **New "Organization data ownership" README section.** Documents that
61
+ BPM is authoritative for `org_units`, `positions`, `memberships`, and
62
+ `manager_resolutions` (no host-injectable resolver pattern, unlike
63
+ members), the rationale (approver-routing / tree-diff hot path), the
64
+ mirror pattern using `OrgUnit.code` as the natural key and the
65
+ `metadata` JSON for host foreign-keys, and an idempotent worked
66
+ example using only `@rytass/bpm-core-client/organization` exports.
67
+
68
+ ### Why a patch
69
+
70
+ No public type signatures change. JSDoc + README only.
71
+
72
+ ## 0.1.4 — 2026-05-27
73
+
74
+ ### Fixed
75
+
76
+ - **Re-publish of the 0.1.3 release.** The 0.1.3 tarball was inadvertently
77
+ published from `libs/bpm-core/` instead of from `dist/libs/bpm-core/`,
78
+ shipping only TypeScript sources with `main: "./src/index.js"` pointing
79
+ at non-existent files. 0.1.3 has been **deprecated on npm**. The
80
+ publish procedure (`docs/api-reference.md` Publish Procedure section
81
+ and `tools/publish/finalize-dist-package.mjs` header) now codifies the
82
+ correct flow: `nx build` → `finalize-dist-package.mjs` →
83
+ `cd dist/libs/<pkg> && npm publish`.
84
+
85
+ ### Documentation
86
+
87
+ - Publish-procedure-from-dist section added to `docs/api-reference.md`.
88
+ - `tools/publish/finalize-dist-package.mjs` header warning expanded.
89
+
90
+ ## 0.1.3 — 2026-05-27 (deprecated on npm)
91
+
92
+ **DO NOT INSTALL — broken release.** Published from source dir instead
93
+ of dist dir; tarball contains only `.ts` files. Use 0.1.4 or newer.
94
+
12
95
  ## 0.1.2 — 2026-05-27
13
96
 
14
97
  ### Fixed
package/README.md CHANGED
@@ -215,6 +215,60 @@ clear admin / designer guards; otherwise BPM resolvers reject with
215
215
  `BPMAuthenticatedGuard`. The host does not need to add `@BPMAuthenticated()`
216
216
  on top of them.
217
217
 
218
+ ### Mapping from a host RBAC system (e.g. Casbin)
219
+
220
+ BPM does not call the host's RBAC engine — it reads exact-string `roles`
221
+ and `permissions` arrays off `BPMAuthContext`. Hosts using Casbin /
222
+ Permify / OPA must **project their grouping policy** into the BPM
223
+ literals inside their `authContextFactory`. A worked Casbin example:
224
+
225
+ ```ts
226
+ import { Inject, Injectable, type ExecutionContext } from '@nestjs/common';
227
+ import { GqlExecutionContext } from '@nestjs/graphql';
228
+ import { CASBIN_ENFORCER, type CasbinEnforcer } from '@your-host/auth';
229
+ import type { BPMAuthContext } from '@rytass/bpm-core-nestjs-module';
230
+
231
+ const HOST_TO_BPM_ROLES: Readonly<Record<string, string>> = {
232
+ approval_admin: 'BPM_ADMIN',
233
+ approval_designer: 'BPM_DESIGNER',
234
+ };
235
+
236
+ @Injectable()
237
+ export class BPMAuthContextFactory {
238
+ constructor(@Inject(CASBIN_ENFORCER) private readonly enforcer: CasbinEnforcer) {}
239
+
240
+ async build(ctx: ExecutionContext): Promise<BPMAuthContext | null> {
241
+ const req = GqlExecutionContext.create(ctx).getContext<{
242
+ req?: { member?: { id: string; email: string; name: string } };
243
+ }>().req;
244
+ const member = req?.member;
245
+ if (!member) return null;
246
+
247
+ // 1. Look up the host's Casbin role assignments for this member.
248
+ const hostRoles = await this.enforcer.getRolesForUser(member.id);
249
+
250
+ // 2. Translate into BPM's exact-string literals. Unknown host roles
251
+ // do nothing — BPM only checks for the documented strings.
252
+ const bpmRoles = hostRoles
253
+ .map((r) => HOST_TO_BPM_ROLES[r])
254
+ .filter((r): r is string => Boolean(r));
255
+
256
+ return {
257
+ memberId: member.id,
258
+ email: member.email,
259
+ name: member.name,
260
+ roles: bpmRoles,
261
+ permissions: [], // Host can also use `bpm:*` style permissions if preferred.
262
+ metadata: {},
263
+ };
264
+ }
265
+ }
266
+ ```
267
+
268
+ > The host owns the mapping table. Keep it in one place (a constant
269
+ > module or a database table) so adding a new BPM tier in a future
270
+ > BPMCore release becomes a one-line addition.
271
+
218
272
  ## Member Resolver
219
273
 
220
274
  The package resolves display names, email addresses, and approver candidates
@@ -275,6 +329,171 @@ export interface BPMMemberResolver {
275
329
  }
276
330
  ```
277
331
 
332
+ ## Organization Data Ownership
333
+
334
+ > **Read this before integrating BPM into a host that already has its own
335
+ > organization model.** The org-integration shape is **asymmetric** with
336
+ > member integration above — knowing which side owns what saves a day of
337
+ > source spelunking.
338
+
339
+ ### Ownership claim
340
+
341
+ BPM is the **sole authority** for four organization tables:
342
+
343
+ | Table | Owns |
344
+ |---|---|
345
+ | `org_units` | Department / division / company nodes (typed by `OrgUnitType`) |
346
+ | `positions` | Job titles, with optional org-unit scoping |
347
+ | `memberships` | Member × org-unit × position relations (the join table that puts a member at a position inside a unit) |
348
+ | `manager_resolutions` | "Who is whose manager" rules (per-member, per-org-unit, or per-position scope), consumed by the `ORG_MANAGER` approver resolver |
349
+
350
+ There is **no** host-injectable `OrgUnitResolver` / `PositionResolver` /
351
+ `MembershipResolver` pattern, deliberately so. If you went looking for
352
+ the symmetric counterpart to `BPMMemberResolver`, stop now — it doesn't
353
+ exist, and the rest of this section explains why and what to do instead.
354
+
355
+ ### Why no resolver
356
+
357
+ The BPM workflow engine reads the org graph on hot paths:
358
+
359
+ - **Approver routing** — every running instance evaluates `UserTaskNode`
360
+ candidates (resolved via `DIRECT`, `POSITION`, `ORG_MANAGER`, or
361
+ `CANDIDATE_GROUP`) against the current org snapshot. A `ParallelGateway`
362
+ fanning out to "all managers in the IT division" may touch dozens of
363
+ membership rows in one step.
364
+ - **Tree-diff commits** — `commitOrgUnitTreeDraft` writes batched moves
365
+ inside a single transaction, with referential integrity against
366
+ `memberships` and `manager_resolutions`. Round-tripping each lookup
367
+ through a host adapter would make this prohibitively chatty.
368
+ - **Reporting** — admin dashboards aggregate counts across the full graph.
369
+
370
+ A read-through adapter would force the host to mirror BPM's tree
371
+ semantics (ltree paths, soft-delete masking, position-vs-org-unit join
372
+ shape) anyway. We chose to make BPM authoritative so the contract is one
373
+ direction only.
374
+
375
+ ### Mirror pattern
376
+
377
+ Host applications that already maintain their own org structure should
378
+ **mirror their data into BPM** through the GraphQL mutations exposed by
379
+ `@rytass/bpm-core-client/organization`. Three rules keep this idempotent
380
+ and observable:
381
+
382
+ 1. **`OrgUnit.code` is your natural key.** Every `createOrgUnit` /
383
+ `createPosition` accepts a `code` field that you control. BPM enforces
384
+ uniqueness; use your host's stable identifier here (e.g. an LDAP DN
385
+ slug or a internal ERP code). On a re-sync, look up existing entities
386
+ by `code` before deciding INSERT vs UPDATE.
387
+ 2. **`metadata` is the host-FK stash.** Both `OrgUnit` and `Membership`
388
+ carry an opaque `metadata` JSON object that BPM never introspects.
389
+ Store your host's primary key here (e.g. `{ shuttleOrgUnitId: 12345 }`)
390
+ so you can chase the back-pointer when reconciling.
391
+ 3. **Soft delete via `deleteOrgUnit` / `deleteMembership`.** Deletes set
392
+ `deletedAt` rather than removing rows. BPM's query layer hides
393
+ soft-deleted rows automatically; reading `orgUnitCount()` will report
394
+ the live count.
395
+
396
+ ### Worked example: idempotent sync
397
+
398
+ The example below uses only published exports from
399
+ `@rytass/bpm-core-client/organization`. It loads the entire BPM-side
400
+ state once via `readOrganizationDashboard()`, builds a `code → record`
401
+ index, then upserts each host node. Verified exports (all with flat
402
+ input shapes — no `{id, input: {...}}` wrapping):
403
+
404
+ - `readOrganizationDashboard({ orgUnitPageSize, orgUnitSearchText, ... })`
405
+ - `createOrgUnit({ code, name, type, parentId, metadataJson })`
406
+ - `updateOrgUnit({ id, code, name, type, parentId, metadataJson })`
407
+ - `deleteOrgUnit(id)` (soft-delete)
408
+ - `commitOrgUnitTreeDraft({ baseUpdatedAt, draft })` (transactional batch moves)
409
+ - `createPosition` / `updatePosition` (key on `code`)
410
+ - `createMembership` / `updateMembership` (unique by `(memberId, orgUnitId, positionId)`)
411
+ - `createManagerResolution` / `updateManagerResolution`
412
+
413
+ ```ts
414
+ import {
415
+ readOrganizationDashboard,
416
+ createOrgUnit,
417
+ updateOrgUnit,
418
+ type OrgUnitRecord,
419
+ } from '@rytass/bpm-core-client/organization';
420
+ import type { OrgUnitType } from '@rytass/bpm-core-shared';
421
+
422
+ interface HostOrgUnit {
423
+ readonly hostId: string; // e.g. ERP primary key — your host-FK
424
+ readonly code: string; // stable across sync runs
425
+ readonly name: string;
426
+ readonly type: OrgUnitType; // 'COMPANY' | 'DIVISION' | 'DEPARTMENT' | 'TEAM'
427
+ readonly parentCode: string | null;
428
+ }
429
+
430
+ async function syncOrgTree(
431
+ hostNodes: readonly HostOrgUnit[],
432
+ ): Promise<void> {
433
+ // 1. Single round trip: load every existing BPM org unit, index by code.
434
+ const dashboard = await readOrganizationDashboard({ orgUnitPageSize: null });
435
+ const byCode = new Map<string, OrgUnitRecord>(
436
+ dashboard.orgUnits.map((unit) => [unit.code, unit]),
437
+ );
438
+
439
+ // 2. Topo-sort by parent so a parent always exists before its child
440
+ // is upserted (createOrgUnit needs the parentId to be valid).
441
+ const ordered = topoSortByParent(hostNodes);
442
+
443
+ for (const host of ordered) {
444
+ const parentId =
445
+ host.parentCode != null ? (byCode.get(host.parentCode)?.id ?? null) : null;
446
+ const metadataJson = JSON.stringify({ hostId: host.hostId });
447
+ const existing = byCode.get(host.code);
448
+
449
+ const saved = existing
450
+ ? await updateOrgUnit({
451
+ id: existing.id,
452
+ code: host.code,
453
+ name: host.name,
454
+ type: host.type,
455
+ parentId,
456
+ metadataJson,
457
+ })
458
+ : await createOrgUnit({
459
+ code: host.code,
460
+ name: host.name,
461
+ type: host.type,
462
+ parentId,
463
+ metadataJson,
464
+ });
465
+
466
+ byCode.set(saved.code, saved);
467
+ }
468
+ }
469
+ ```
470
+
471
+ The same pattern works for `Position` (key on `code`), `Membership`
472
+ (unique by `memberId × orgUnitId × positionId`), and
473
+ `ManagerResolution` (unique by scope shape + active flag). For bulk
474
+ tree-shape changes (multi-node moves under a single parent),
475
+ `commitOrgUnitTreeDraft` accepts the full draft in one transaction.
476
+
477
+ > **Atomicity caveat.** Only `commitOrgUnitTreeDraft` is transactional.
478
+ > Sequential `createMembership` / `createPosition` calls are independent
479
+ > mutations — if the 401st call in a batch of 500 fails, the prior 400
480
+ > stay committed. Wrap your sync loop in a "from-cursor" resume strategy
481
+ > if you need at-least-once semantics across crashes.
482
+
483
+ > **Programmatic base URL.** Server-side scripts that aren't running
484
+ > under Next.js (cron workers, one-off seeds) cannot rely on
485
+ > `NEXT_PUBLIC_API_URL` resolution — call `configureBPMClient({ baseUrl,
486
+ > fetch, headers })` from `@rytass/bpm-core-client` once at startup to
487
+ > point the transport at the right host and inject a session cookie or
488
+ > bearer token.
489
+
490
+ ### What you do NOT mirror
491
+
492
+ Member identity itself stays in your host's user table — BPM reaches it
493
+ through `BPMMemberResolver` (see the previous section). Only the
494
+ **org structure** (who reports where, what unit they sit in, who manages
495
+ them) needs to live inside BPM.
496
+
278
497
  ## Root Module Configuration
279
498
 
280
499
  Use `BPMRootModule.forRoot()` for static configuration or
@@ -517,6 +736,43 @@ bootstrap, `attachmentRoutePrefix` must be supplied at module wiring time:
517
736
  - A process can only mount BPM under a single prefix. Multi-tenant hosts that
518
737
  want different prefixes per tenant should run separate processes.
519
738
 
739
+ ### Cross-origin authentication
740
+
741
+ Signed attachment URLs carry the auth **inside the URL** itself —
742
+ specifically a query-string signature derived from
743
+ `attachmentSignedUrlSecret` and `attachmentSignedUrlTtlSeconds`. The
744
+ browser does **not** send the session cookie to fetch the attachment;
745
+ BPM's controller verifies the signature instead.
746
+
747
+ This means:
748
+
749
+ - The attachment URL is safe to embed in `<img>` / `<a href>` /
750
+ `<iframe>` tags across origins; CORS is not in the auth path.
751
+ - Anyone who possesses the URL (until TTL expiry) can fetch the file.
752
+ Choose `attachmentSignedUrlTtlSeconds` short enough that a leaked URL
753
+ expires before exploitation (we suggest **600s** in production).
754
+ - Hosts that need true cross-tenant isolation should keep
755
+ `attachmentPublicBaseUrl` pointing at a CDN edge that enforces its own
756
+ authz on top — BPM's signature is freshness, not authorization.
757
+
758
+ ### Disabling the `/auth/test-members` endpoint in production
759
+
760
+ Hosts wired with the demo seed (typical for `pnpm demo:reset` /
761
+ `pnpm staging:reset`) expose `GET /auth/test-members` so the login form
762
+ shows a clickable list of fixture accounts. **Do not ship this in
763
+ production.** Two ways to keep it out:
764
+
765
+ 1. The endpoint is **owned by the wrapper host**, not by BPM —
766
+ `@rytass/bpm-core-nestjs-module` does not register it. Simply omit
767
+ the wrapper module / controller that serves
768
+ `/auth/test-members`. The reference implementation in BPMCore's
769
+ `apps/api` only exposes it when `NODE_ENV !== 'production'`; copy
770
+ that pattern in your own auth controller.
771
+ 2. The browser-side `listApiTestMembers()` client function tolerates a
772
+ `404` / `401` and returns an empty array, so removing the endpoint
773
+ degrades gracefully — the login form simply hides the demo-account
774
+ picker.
775
+
520
776
  ## Notifications and SLA
521
777
 
522
778
  BPM creates in-app notifications by default. Email and webhook delivery are
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rytass/bpm-core-nestjs-module",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Embeddable NestJS BPM approval workflow module: workflow engine, approval templates, forms, organization/member contracts, attachments, signatures, notifications, delegation, and TypeORM migrations.",
5
5
  "keywords": [
6
6
  "bpm",
@@ -141,7 +141,7 @@
141
141
  "@nestjs/core": "^11.0.0",
142
142
  "@nestjs/graphql": "^13.4.0",
143
143
  "@nestjs/typeorm": "^11.0.1",
144
- "@rytass/bpm-core-shared": "^0.1.4",
144
+ "@rytass/bpm-core-shared": "^0.1.6",
145
145
  "@rytass/secret-adapter-vault-nestjs": "^0.4.5 || ^0.5.0",
146
146
  "express": "^4.0.0 || ^5.0.0",
147
147
  "graphql": "^16.0.0",
@@ -112,9 +112,9 @@ export interface BPMRootModuleAsyncOptions extends Pick<ModuleMetadata, 'imports
112
112
  *
113
113
  * Hosts call `forRoot` or `forRootAsync` once from their root `AppModule`.
114
114
  * BPMCore does **not** own login, GraphQL setup, TypeORM bootstrap, or
115
- * Vault loading — those remain the host's responsibility. See
116
- * `docs/10-bpm-embedding-auth.md` and `docs/11-consumer-quickstart.md` for
117
- * the full contract.
115
+ * Vault loading — those remain the host's responsibility. See the
116
+ * package README sections "Embedding & auth", "Organization data
117
+ * ownership", and "Consumer quickstart" for the full contract.
118
118
  *
119
119
  * The host must avoid Nest's `app.setGlobalPrefix(...)`; BPM controllers
120
120
  * mount at relative paths driven by their respective options (notably
@@ -24,9 +24,9 @@ const workflow_engine_module_1 = require("../workflow-engine/workflow-engine.mod
24
24
  *
25
25
  * Hosts call `forRoot` or `forRootAsync` once from their root `AppModule`.
26
26
  * BPMCore does **not** own login, GraphQL setup, TypeORM bootstrap, or
27
- * Vault loading — those remain the host's responsibility. See
28
- * `docs/10-bpm-embedding-auth.md` and `docs/11-consumer-quickstart.md` for
29
- * the full contract.
27
+ * Vault loading — those remain the host's responsibility. See the
28
+ * package README sections "Embedding & auth", "Organization data
29
+ * ownership", and "Consumer quickstart" for the full contract.
30
30
  *
31
31
  * The host must avoid Nest's `app.setGlobalPrefix(...)`; BPM controllers
32
32
  * mount at relative paths driven by their respective options (notably
@@ -1,3 +1,4 @@
1
1
  export * from './identity-options';
2
2
  export * from './member-base.adapter';
3
+ export * from './member-not-found.exception';
3
4
  export * from './member-resolver.interface';
@@ -3,5 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
4
  tslib_1.__exportStar(require("./identity-options"), exports);
5
5
  tslib_1.__exportStar(require("./member-base.adapter"), exports);
6
+ tslib_1.__exportStar(require("./member-not-found.exception"), exports);
6
7
  tslib_1.__exportStar(require("./member-resolver.interface"), exports);
7
8
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../libs/bpm-core/src/lib/identity/index.ts"],"names":[],"mappings":";;;AAAA,6DAAmC;AACnC,gEAAsC;AACtC,sEAA4C"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../libs/bpm-core/src/lib/identity/index.ts"],"names":[],"mappings":";;;AAAA,6DAAmC;AACnC,gEAAsC;AACtC,uEAA6C;AAC7C,sEAA4C"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Thrown by `BPMMemberResolver.resolve(memberId)` when the host's user
3
+ * directory has no entry for the requested id. BPM surfaces the error
4
+ * as a GraphQL `errors[]` entry on whichever query triggered the
5
+ * lookup; the caller receives a structured response rather than a
6
+ * synthetic placeholder member.
7
+ *
8
+ * Note: `BPMMemberResolver.resolveMany(ids)` has the **opposite**
9
+ * contract — unknown ids are silently omitted from the returned `Map`.
10
+ * Use this exception only inside the single-id `resolve` path.
11
+ *
12
+ * Hosts may throw any `Error` subclass; this class is provided as a
13
+ * recommended default so error messages stay consistent in BPM's logs.
14
+ */
15
+ export declare class MemberNotFoundException extends Error {
16
+ readonly memberId: string;
17
+ constructor(memberId: string, message?: string);
18
+ }
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MemberNotFoundException = void 0;
4
+ /**
5
+ * Thrown by `BPMMemberResolver.resolve(memberId)` when the host's user
6
+ * directory has no entry for the requested id. BPM surfaces the error
7
+ * as a GraphQL `errors[]` entry on whichever query triggered the
8
+ * lookup; the caller receives a structured response rather than a
9
+ * synthetic placeholder member.
10
+ *
11
+ * Note: `BPMMemberResolver.resolveMany(ids)` has the **opposite**
12
+ * contract — unknown ids are silently omitted from the returned `Map`.
13
+ * Use this exception only inside the single-id `resolve` path.
14
+ *
15
+ * Hosts may throw any `Error` subclass; this class is provided as a
16
+ * recommended default so error messages stay consistent in BPM's logs.
17
+ */
18
+ class MemberNotFoundException extends Error {
19
+ constructor(memberId, message) {
20
+ super(message ?? `BPM member not found: ${memberId}`);
21
+ this.name = 'MemberNotFoundException';
22
+ this.memberId = memberId;
23
+ }
24
+ }
25
+ exports.MemberNotFoundException = MemberNotFoundException;
26
+ //# sourceMappingURL=member-not-found.exception.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"member-not-found.exception.js","sourceRoot":"","sources":["../../../../../../libs/bpm-core/src/lib/identity/member-not-found.exception.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;GAaG;AACH,MAAa,uBAAwB,SAAQ,KAAK;IAGhD,YAAY,QAAgB,EAAE,OAAgB;QAC5C,KAAK,CAAC,OAAO,IAAI,yBAAyB,QAAQ,EAAE,CAAC,CAAC;QACtD,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;QACtC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;CACF;AARD,0DAQC"}
@@ -21,17 +21,55 @@ export declare const MEMBER_RESOLVER: symbol;
21
21
  * BPM never reads the host's user table directly — it only knows
22
22
  * `memberId` strings and asks this resolver for any additional information.
23
23
  *
24
- * - `resolve` is called per missing id; throwing here surfaces as a BPM
25
- * GraphQL error.
26
- * - `resolveMany` is the batched form BPM prefers when many ids are needed.
27
- * Implementations may de-duplicate / cache as they see fit; missing ids
28
- * should be omitted from the result `Map`.
29
- * - `search` is optional. When omitted, BPM's `searchMembers` GraphQL
30
- * query simply returns an empty array.
24
+ * ## Caching
25
+ *
26
+ * BPM caches resolver responses in the `member_metadata_cache` table
27
+ * with a configurable TTL (`identityOptions.memberMetadataCacheTtlMs`,
28
+ * default 5 minutes `IDENTITY_MEMBER_METADATA_CACHE_TTL_MS_DEFAULT`).
29
+ * Hosts do **not** need to add their own cache layer for the common
30
+ * case; implementations may layer additional caching if they have a
31
+ * faster source of truth (e.g. an in-process LRU over a remote auth
32
+ * service). The two layers do not coordinate — BPM's row-level cache
33
+ * is invalidated only by TTL expiry, never by host signals.
34
+ *
35
+ * ## Unknown-id contract (the two methods diverge intentionally)
36
+ *
37
+ * - `resolve(id)` is the **single-id, must-succeed** form. Throw
38
+ * `MemberNotFoundException` (or any error) when `memberId` is unknown
39
+ * — the caller is asking about one specific member and a partial
40
+ * answer is meaningless. BPM surfaces the error as a GraphQL
41
+ * `errors[]` entry on the originating query.
42
+ * - `resolveMany(ids)` is the **batched, partial-success** form BPM
43
+ * prefers when many ids are needed (e.g. resolving the assignee list
44
+ * on a task page). Implementations should **omit** unknown ids from
45
+ * the returned `Map` rather than throwing; BPM treats missing entries
46
+ * as deleted/anonymized members and renders a placeholder. Throwing
47
+ * here aborts the entire batch and fails the GraphQL query — almost
48
+ * never the right choice.
49
+ * - `search(text)` is optional. When omitted, BPM's `searchMembers`
50
+ * GraphQL query returns an empty array (the picker UI degrades to
51
+ * id-only entry).
52
+ *
53
+ * Implementations may de-duplicate within a single call freely.
31
54
  */
32
55
  export interface BPMMemberResolver {
56
+ /**
57
+ * Single-id lookup. **Throw** (e.g. `MemberNotFoundException`) when
58
+ * the id is unknown — BPM surfaces the error to the caller. Result is
59
+ * row-cached by BPM with TTL `identityOptions.memberMetadataCacheTtlMs`.
60
+ */
33
61
  resolve(memberId: string): Promise<MemberMetadata>;
62
+ /**
63
+ * Batched lookup. **Omit** unknown ids from the returned `Map`
64
+ * (do not throw) — BPM renders missing entries as deleted/anonymized
65
+ * members. Cached per-id under the same TTL as `resolve`.
66
+ */
34
67
  resolveMany(memberIds: readonly string[]): Promise<ReadonlyMap<string, MemberMetadata>>;
68
+ /**
69
+ * Optional free-text search. Return up to ~50 best matches; BPM does
70
+ * not paginate the result. Omit the method entirely to disable
71
+ * member-picker search.
72
+ */
35
73
  search?(searchText: string): Promise<readonly MemberMetadata[]>;
36
74
  }
37
75
  /**