@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.4",
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.4",
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
- * `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
@@ -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
  /**