@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 +83 -0
- package/README.md +256 -0
- package/package.json +2 -2
- package/src/lib/bpm/bpm-root.module.d.ts +3 -3
- package/src/lib/bpm/bpm-root.module.js +3 -3
- package/src/lib/identity/index.d.ts +1 -0
- package/src/lib/identity/index.js +1 -0
- package/src/lib/identity/index.js.map +1 -1
- package/src/lib/identity/member-not-found.exception.d.ts +18 -0
- package/src/lib/identity/member-not-found.exception.js +26 -0
- package/src/lib/identity/member-not-found.exception.js.map +1 -0
- package/src/lib/identity/member-resolver.interface.d.ts +45 -7
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.
|
|
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.
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
@@ -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
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
/**
|