@flusys/nestjs-shared 9.1.0 → 9.1.1

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/README.md CHANGED
@@ -209,6 +209,10 @@ async publicList() { }
209
209
 
210
210
  `matchesPermission(granted, required)` (`@flusys/nestjs-shared/utils`) is the one wildcard matcher (`*`, `prefix.*`) behind `PermissionGuard` and `SharedPermissionCacheService.matchPermission`. For a check inside a handler, `SharedPermissionCacheService.assertPermission(user, code)` throws `InsufficientPermissionsException` (403) unless the user holds the code in their company/branch scope, and `PermissionSystemUnavailableException` when no cache is wired up - never re-implement that check. `PERMISSION_RESOLVER` / `IPermissionResolver` (`@flusys/nestjs-shared/interfaces`) is the provider interface for reading a user's codes in any company/branch scope, including one not loaded into the cache yet; nestjs-iam implements it, feature packages inject it `@Optional()`.
211
211
 
212
+ `evaluatePermissionLogic(logic, granted)` evaluates an AND / OR `ILogicNode` tree with the same matcher and returns `{ passed, missingPermissions, operator }` (a group without children fails closed); `permissionLogicCodes(logic)` lists the codes a tree names. `PermissionGuard` uses it for `@RequirePermissionLogic`, and `SharedPermissionCacheService.assertPermissionLogic(user, logic)` is the throwing form for code outside a guard (403 naming what is missing, 500 when no cache is wired up) - used by `nestjs-entity-builder` for flow URLs with a permission rule.
213
+
214
+ `permissionLogicProblem(value)` validates a stored rule (`'invalid' | 'empty' | 'too_many' | null`; at most `MAX_PERMISSION_LOGIC_DEPTH` (5) group levels and `MAX_PERMISSION_LOGIC_CODES` (50) codes) - use it for any DTO or config that accepts an `ILogicNode`. `loadPermissionCodes(scope, { resolver, cache })` reads a user's codes from `PERMISSION_RESOLVER` when wired, else from a configured `SharedPermissionCacheService`, else throws `PermissionSystemUnavailableException` - never re-implement that fallback.
215
+
212
216
  ## 5. Response DTOs
213
217
 
214
218
  All controllers return one of these shapes:
@@ -763,6 +767,46 @@ Injecting `EVENT_BUS_INSTANCE` gives direct
763
767
  `publish` / `subscribe` access - mark it `@Optional()`, so the service still resolves in an
764
768
  app that never registers `EventBusModule`.
765
769
 
770
+ ## 9. Branch Hierarchy
771
+
772
+ Branches nest through `CompanyBranch.parentId` (always inside one company). Two layers, so any package can use them without importing nestjs-auth:
773
+
774
+ **Pure helpers** (`@flusys/nestjs-shared/utils`) on a flat `{ id, parentId }` list. They tolerate bad data: repeated ids keep their first entry, empty ids are ignored, a branch that is its own parent is a root, and parent cycles never loop.
775
+
776
+ | Function | Returns |
777
+ | --- | --- |
778
+ | `collectBranchDescendantIds(nodes, ids, { includeSelf?, maxDepth? })` | every branch under `ids`, level by level; with `includeSelf` the given ids come first |
779
+ | `collectBranchAncestorIds(nodes, id, { includeSelf?, maxDepth? })` | the parent chain, nearest parent first, stopping at a parent not in `nodes` |
780
+ | `isBranchWithin(nodes, id, ancestorId)` | `id` is `ancestorId` or anywhere under it |
781
+ | `getBranchPath(nodes, id)` | the nodes from the top-most known ancestor down to `id` |
782
+ | `normalizeBranchIds(ids)` | each id once, in order, without empty values |
783
+ | `applyBranchFilter(query, { entityAlias, columnName?, includeUnassigned? }, ids)` | adds `alias.branchId IN (:...ids)`; `includeUnassigned` also keeps rows with no branch; no ids matches nothing (or only unassigned rows) |
784
+
785
+ `maxDepth: 1` limits a walk to direct children or the direct parent.
786
+
787
+ **`BRANCH_HIERARCHY_RESOLVER`** (`@flusys/nestjs-shared/interfaces`), implemented by nestjs-auth's `BranchHierarchyResolverAdapter` when the company feature is on. Inject it `@Optional()`:
788
+
789
+ | Method | Use it for |
790
+ | --- | --- |
791
+ | `getDescendantIds(ids, { includeSelf? })` | structure only (every sub-branch, grants ignored) - e.g. admin screens; never to scope a user's data |
792
+ | `getAncestorIds(id, { includeSelf? })` | breadcrumbs, inherited settings |
793
+ | `isWithinBranch(id, ancestorId)` | "does this record's branch sit under mine?" |
794
+ | `getGrantedBranchIds(userId, companyId?)` | the branches the user is granted - exactly what company select offers |
795
+ | `getGrantedDescendantIds(userId, ids, { includeSelf? })` | the current branch sees its granted sub-branches' data; a branch dropdown filter that also matches granted sub-branches |
796
+ | `hasBranchAccess(userId, branchId)` | the user is granted that branch itself (a parent grant does not count) |
797
+
798
+ Results only hold existing, not soft-deleted branches (inactive ones are included), and empty, malformed or unknown ids match nothing. A soft-deleted branch cuts the chain, so what sits below it is no longer under its old ancestors. A user's granted branches are exactly what company select offers (`COMPANY_ACCESS_RESOLVER`, whose `getUserAccess(userId)` lists each as `{ id, companyId, parentId, name }`). Grants are explicit: a grant on a parent never reaches a child that is not granted itself, in either direction - with Parent and Level 2 granted but not Level 1, `getGrantedDescendantIds(userId, parent, { includeSelf: true })` is `[parent, level2]`.
799
+
800
+ ```typescript
801
+ constructor(@Optional() @Inject(BRANCH_HIERARCHY_RESOLVER) private readonly branches?: IBranchHierarchyResolver) {}
802
+
803
+ async listForCurrentBranch(query: SelectQueryBuilder<Invoice>, user: ILoggedUserInfo) {
804
+ if (!this.branches || !user.branchId) return query;
805
+ const ids = await this.branches.getGrantedDescendantIds(user.id, user.branchId, { includeSelf: true });
806
+ return applyBranchFilter(query, { entityAlias: 'invoice', columnName: 'branch_id' }, ids);
807
+ }
808
+ ```
809
+
766
810
  ## License
767
811
 
768
812
  MIT © FLUSYS
@@ -113,7 +113,6 @@ export declare const FLOW_DEFINITION_PERMISSIONS: {
113
113
  readonly UPDATE: "entity_builder.flow_definition.update";
114
114
  readonly DELETE: "entity_builder.flow_definition.delete";
115
115
  readonly TEST: "entity_builder.flow_definition.test";
116
- readonly SYSTEM: "entity_builder.flow_definition.system";
117
116
  readonly CODE: "entity_builder.flow_definition.code";
118
117
  };
119
118
  export declare const ENTITY_BUILDER_ALL_DYNAMIC: {
@@ -299,7 +298,6 @@ export declare const PERMISSIONS: {
299
298
  readonly UPDATE: "entity_builder.flow_definition.update";
300
299
  readonly DELETE: "entity_builder.flow_definition.delete";
301
300
  readonly TEST: "entity_builder.flow_definition.test";
302
- readonly SYSTEM: "entity_builder.flow_definition.system";
303
301
  readonly CODE: "entity_builder.flow_definition.code";
304
302
  };
305
303
  readonly ENTITY_BUILDER_ALL_DYNAMIC: {