@rytass/bpm-core-nestjs-module 0.1.4 → 0.1.5
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,55 @@ 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.5 — 2026-05-28
|
|
13
|
+
|
|
14
|
+
### Documentation
|
|
15
|
+
|
|
16
|
+
- **Identity contract clarifications.** `BPMMemberResolver.resolve` JSDoc
|
|
17
|
+
now states the unknown-id contract explicitly (throw
|
|
18
|
+
`MemberNotFoundException`); `resolveMany` documents the diverging
|
|
19
|
+
partial-success contract (omit unknown ids from the returned `Map`).
|
|
20
|
+
Both methods cross-link `identityMemberMetadataCacheTtlMs`
|
|
21
|
+
(default 5 min) so consumers know BPM caches their resolver responses.
|
|
22
|
+
- **`BPMRootModule` JSDoc dangling refs removed.** The previous
|
|
23
|
+
references to `docs/10-bpm-embedding-auth.md` and
|
|
24
|
+
`docs/11-consumer-quickstart.md` (files that never shipped) are now
|
|
25
|
+
inline pointers to the README sections.
|
|
26
|
+
- **New "Organization data ownership" README section.** Documents that
|
|
27
|
+
BPM is authoritative for `org_units`, `positions`, `memberships`, and
|
|
28
|
+
`manager_resolutions` (no host-injectable resolver pattern, unlike
|
|
29
|
+
members), the rationale (approver-routing / tree-diff hot path), the
|
|
30
|
+
mirror pattern using `OrgUnit.code` as the natural key and the
|
|
31
|
+
`metadata` JSON for host foreign-keys, and an idempotent worked
|
|
32
|
+
example using only `@rytass/bpm-core-client/organization` exports.
|
|
33
|
+
|
|
34
|
+
### Why a patch
|
|
35
|
+
|
|
36
|
+
No public type signatures change. JSDoc + README only.
|
|
37
|
+
|
|
38
|
+
## 0.1.4 — 2026-05-27
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- **Re-publish of the 0.1.3 release.** The 0.1.3 tarball was inadvertently
|
|
43
|
+
published from `libs/bpm-core/` instead of from `dist/libs/bpm-core/`,
|
|
44
|
+
shipping only TypeScript sources with `main: "./src/index.js"` pointing
|
|
45
|
+
at non-existent files. 0.1.3 has been **deprecated on npm**. The
|
|
46
|
+
publish procedure (`docs/api-reference.md` Publish Procedure section
|
|
47
|
+
and `tools/publish/finalize-dist-package.mjs` header) now codifies the
|
|
48
|
+
correct flow: `nx build` → `finalize-dist-package.mjs` →
|
|
49
|
+
`cd dist/libs/<pkg> && npm publish`.
|
|
50
|
+
|
|
51
|
+
### Documentation
|
|
52
|
+
|
|
53
|
+
- Publish-procedure-from-dist section added to `docs/api-reference.md`.
|
|
54
|
+
- `tools/publish/finalize-dist-package.mjs` header warning expanded.
|
|
55
|
+
|
|
56
|
+
## 0.1.3 — 2026-05-27 (deprecated on npm)
|
|
57
|
+
|
|
58
|
+
**DO NOT INSTALL — broken release.** Published from source dir instead
|
|
59
|
+
of dist dir; tarball contains only `.ts` files. Use 0.1.4 or newer.
|
|
60
|
+
|
|
12
61
|
## 0.1.2 — 2026-05-27
|
|
13
62
|
|
|
14
63
|
### Fixed
|
package/README.md
CHANGED
|
@@ -275,6 +275,149 @@ export interface BPMMemberResolver {
|
|
|
275
275
|
}
|
|
276
276
|
```
|
|
277
277
|
|
|
278
|
+
## Organization Data Ownership
|
|
279
|
+
|
|
280
|
+
> **Read this before integrating BPM into a host that already has its own
|
|
281
|
+
> organization model.** The org-integration shape is **asymmetric** with
|
|
282
|
+
> member integration above — knowing which side owns what saves a day of
|
|
283
|
+
> source spelunking.
|
|
284
|
+
|
|
285
|
+
### Ownership claim
|
|
286
|
+
|
|
287
|
+
BPM is the **sole authority** for four organization tables:
|
|
288
|
+
|
|
289
|
+
| Table | Owns |
|
|
290
|
+
|---|---|
|
|
291
|
+
| `org_units` | Department / division / company nodes (typed by `OrgUnitType`) |
|
|
292
|
+
| `positions` | Job titles, with optional org-unit scoping |
|
|
293
|
+
| `memberships` | Member × org-unit × position relations (the join table that puts a member at a position inside a unit) |
|
|
294
|
+
| `manager_resolutions` | "Who is whose manager" rules (per-member, per-org-unit, or per-position scope), consumed by the `ORG_MANAGER` approver resolver |
|
|
295
|
+
|
|
296
|
+
There is **no** host-injectable `OrgUnitResolver` / `PositionResolver` /
|
|
297
|
+
`MembershipResolver` pattern, deliberately so. If you went looking for
|
|
298
|
+
the symmetric counterpart to `BPMMemberResolver`, stop now — it doesn't
|
|
299
|
+
exist, and the rest of this section explains why and what to do instead.
|
|
300
|
+
|
|
301
|
+
### Why no resolver
|
|
302
|
+
|
|
303
|
+
The BPM workflow engine reads the org graph on hot paths:
|
|
304
|
+
|
|
305
|
+
- **Approver routing** — every running instance evaluates `UserTaskNode`
|
|
306
|
+
candidates (resolved via `DIRECT`, `POSITION`, `ORG_MANAGER`, or
|
|
307
|
+
`CANDIDATE_GROUP`) against the current org snapshot. A `ParallelGateway`
|
|
308
|
+
fanning out to "all managers in the IT division" may touch dozens of
|
|
309
|
+
membership rows in one step.
|
|
310
|
+
- **Tree-diff commits** — `commitOrgUnitTreeDraft` writes batched moves
|
|
311
|
+
inside a single transaction, with referential integrity against
|
|
312
|
+
`memberships` and `manager_resolutions`. Round-tripping each lookup
|
|
313
|
+
through a host adapter would make this prohibitively chatty.
|
|
314
|
+
- **Reporting** — admin dashboards aggregate counts across the full graph.
|
|
315
|
+
|
|
316
|
+
A read-through adapter would force the host to mirror BPM's tree
|
|
317
|
+
semantics (ltree paths, soft-delete masking, position-vs-org-unit join
|
|
318
|
+
shape) anyway. We chose to make BPM authoritative so the contract is one
|
|
319
|
+
direction only.
|
|
320
|
+
|
|
321
|
+
### Mirror pattern
|
|
322
|
+
|
|
323
|
+
Host applications that already maintain their own org structure should
|
|
324
|
+
**mirror their data into BPM** through the GraphQL mutations exposed by
|
|
325
|
+
`@rytass/bpm-core-client/organization`. Three rules keep this idempotent
|
|
326
|
+
and observable:
|
|
327
|
+
|
|
328
|
+
1. **`OrgUnit.code` is your natural key.** Every `createOrgUnit` /
|
|
329
|
+
`createPosition` accepts a `code` field that you control. BPM enforces
|
|
330
|
+
uniqueness; use your host's stable identifier here (e.g. an LDAP DN
|
|
331
|
+
slug or a internal ERP code). On a re-sync, look up existing entities
|
|
332
|
+
by `code` before deciding INSERT vs UPDATE.
|
|
333
|
+
2. **`metadata` is the host-FK stash.** Both `OrgUnit` and `Membership`
|
|
334
|
+
carry an opaque `metadata` JSON object that BPM never introspects.
|
|
335
|
+
Store your host's primary key here (e.g. `{ shuttleOrgUnitId: 12345 }`)
|
|
336
|
+
so you can chase the back-pointer when reconciling.
|
|
337
|
+
3. **Soft delete via `deleteOrgUnit` / `deleteMembership`.** Deletes set
|
|
338
|
+
`deletedAt` rather than removing rows. BPM's query layer hides
|
|
339
|
+
soft-deleted rows automatically; reading `orgUnitCount()` will report
|
|
340
|
+
the live count.
|
|
341
|
+
|
|
342
|
+
### Worked example: idempotent sync
|
|
343
|
+
|
|
344
|
+
The example below uses only published exports from
|
|
345
|
+
`@rytass/bpm-core-client/organization`. It pushes a host org tree into
|
|
346
|
+
BPM, handling the create-or-update path by checking `code` matches.
|
|
347
|
+
Verified exports: `orgUnits`, `createOrgUnit`, `updateOrgUnit`,
|
|
348
|
+
`deleteOrgUnit`, `createPosition`, `createMembership`, `updateMembership`,
|
|
349
|
+
`createManagerResolution`, `commitOrgUnitTreeDraft`.
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
import {
|
|
353
|
+
createOrgUnit,
|
|
354
|
+
updateOrgUnit,
|
|
355
|
+
createPosition,
|
|
356
|
+
createMembership,
|
|
357
|
+
orgUnits,
|
|
358
|
+
type OrgUnitRecord,
|
|
359
|
+
} from '@rytass/bpm-core-client/organization';
|
|
360
|
+
|
|
361
|
+
interface HostOrgUnit {
|
|
362
|
+
readonly hostId: string; // e.g. ERP primary key — your host-FK
|
|
363
|
+
readonly code: string; // stable across sync runs
|
|
364
|
+
readonly name: string;
|
|
365
|
+
readonly type: 'COMPANY' | 'DIVISION' | 'DEPARTMENT' | 'TEAM';
|
|
366
|
+
readonly parentCode: string | null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function syncOrgUnit(host: HostOrgUnit): Promise<OrgUnitRecord> {
|
|
370
|
+
// 1. Look up by host's stable code; BPM enforces uniqueness.
|
|
371
|
+
const existing = (
|
|
372
|
+
await orgUnits({ page: 1, pageSize: 1, searchText: host.code })
|
|
373
|
+
).find((u) => u.code === host.code);
|
|
374
|
+
|
|
375
|
+
const parentId = host.parentCode
|
|
376
|
+
? (await orgUnits({ searchText: host.parentCode })).find(
|
|
377
|
+
(u) => u.code === host.parentCode,
|
|
378
|
+
)?.id ?? null
|
|
379
|
+
: null;
|
|
380
|
+
|
|
381
|
+
// 2. Stash the host-FK back into metadata so future reconciliations
|
|
382
|
+
// can chase the pointer either direction.
|
|
383
|
+
const metadataJson = JSON.stringify({ hostId: host.hostId });
|
|
384
|
+
|
|
385
|
+
if (existing) {
|
|
386
|
+
return updateOrgUnit({
|
|
387
|
+
id: existing.id,
|
|
388
|
+
input: {
|
|
389
|
+
code: host.code,
|
|
390
|
+
name: host.name,
|
|
391
|
+
type: host.type,
|
|
392
|
+
parentId,
|
|
393
|
+
metadataJson,
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return createOrgUnit({
|
|
399
|
+
code: host.code,
|
|
400
|
+
name: host.name,
|
|
401
|
+
type: host.type,
|
|
402
|
+
parentId,
|
|
403
|
+
metadataJson,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
The same pattern works for `Position` (key on `code`), `Membership`
|
|
409
|
+
(unique by `memberId × orgUnitId × positionId`), and
|
|
410
|
+
`ManagerResolution` (unique by scope shape + active flag). For bulk
|
|
411
|
+
tree-shape changes (multi-node moves under a single parent),
|
|
412
|
+
`commitOrgUnitTreeDraft` accepts the full draft in one transaction.
|
|
413
|
+
|
|
414
|
+
### What you do NOT mirror
|
|
415
|
+
|
|
416
|
+
Member identity itself stays in your host's user table — BPM reaches it
|
|
417
|
+
through `BPMMemberResolver` (see the previous section). Only the
|
|
418
|
+
**org structure** (who reports where, what unit they sit in, who manages
|
|
419
|
+
them) needs to live inside BPM.
|
|
420
|
+
|
|
278
421
|
## Root Module Configuration
|
|
279
422
|
|
|
280
423
|
Use `BPMRootModule.forRoot()` for static configuration or
|
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.5",
|
|
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.5",
|
|
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
|
|
@@ -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
|
/**
|