@flusys/nestjs-shared 9.1.0 → 9.1.2
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 +69 -0
- package/constants/permissions.d.ts +0 -2
- package/fesm/282.js +1580 -0
- package/fesm/470.js +5 -40
- package/fesm/681.js +78 -2
- package/fesm/948.js +0 -1
- package/fesm/constants/index.js +0 -1
- package/fesm/guards/index.js +83 -42
- package/fesm/index.js +100 -541
- package/fesm/interceptors/index.js +0 -1
- package/fesm/interfaces/index.js +102 -4
- package/fesm/utils/index.js +144 -537
- package/guards/permission.guard.d.ts +0 -1
- package/interfaces/branch-hierarchy-resolver.interface.d.ts +10 -0
- package/interfaces/company-access-resolver.interface.d.ts +19 -0
- package/interfaces/email-adapter.interface.d.ts +27 -0
- package/interfaces/expression.interface.d.ts +45 -0
- package/interfaces/index.d.ts +4 -0
- package/modules/shared-permission-cache/shared-permission-cache.service.d.ts +6 -1
- package/package.json +4 -3
- package/utils/branch-hierarchy.util.d.ts +15 -0
- package/utils/expression.util.d.ts +35 -0
- package/utils/index.d.ts +3 -0
- package/utils/permission-match.util.d.ts +18 -0
- package/utils/query-helpers.util.d.ts +6 -0
- package/utils/safe-regex.util.d.ts +10 -0
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,71 @@ 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
|
+
|
|
810
|
+
## 10. Expressions
|
|
811
|
+
|
|
812
|
+
A JSON expression tree any package can store and evaluate (`@flusys/nestjs-shared/interfaces` for the types, `/utils` for the engine). Pure functions, no Nest dependencies; every slot that takes a value takes another expression.
|
|
813
|
+
|
|
814
|
+
| `type` | Shape | Result |
|
|
815
|
+
| --- | --- | --- |
|
|
816
|
+
| `literal` | `{ value }` | the value |
|
|
817
|
+
| `field_ref` | `{ ref }` | `ctx.resolveRef(ref)` |
|
|
818
|
+
| `arithmetic` | `{ op, operands[] }` | `sum`, `subtract`, `multiply`, `divide`, `modulo`, `power`, `average`, `min`, `max`; blank operands count as 0 |
|
|
819
|
+
| `concat` | `{ parts[], separator? }` | joined text; with a separator, blank parts are skipped |
|
|
820
|
+
| `template` | `{ template }` | `"{{ ref }}"` placeholders filled through `resolveRef` |
|
|
821
|
+
| `function` | `{ name, args[] }` | see `EXPRESSION_FUNCTIONS`: text (incl. `REGEX_EXTRACT` / `REGEX_REPLACE` / `REGEX_TEST`), number, date (UTC) and `COALESCE` |
|
|
822
|
+
| `if` | `{ condition, whenTrue, whenFalse? }` | `condition` is an AND / OR group of `{ left, comparison, right? }`, groups nest |
|
|
823
|
+
|
|
824
|
+
```typescript
|
|
825
|
+
import { evaluateExpression, assertValidExpression, isExpressionError } from '@flusys/nestjs-shared/utils';
|
|
826
|
+
|
|
827
|
+
assertValidExpression(expr, { isKnownRef: (ref) => fieldIds.has(ref) }); // on save
|
|
828
|
+
const value = evaluateExpression(expr, { resolveRef: (ref) => data[ref] });
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
- Errors are `ExpressionError { code, variables }` (plain, not HTTP) - map `code` to your own message key; check with `isExpressionError()`, not `instanceof`.
|
|
832
|
+
- Limits (`EXPRESSION_LIMITS`): depth 16, 500 nodes, 4000-char templates, 100k-char text results.
|
|
833
|
+
- Regex runs on RE2 via `safeRegexProblem()` / `compileSafeRegex()` (`SAFE_REGEX_LIMITS`): linear time, pattern length / counted-repeat caps. Never `new RegExp` on author input.
|
|
834
|
+
|
|
766
835
|
## License
|
|
767
836
|
|
|
768
837
|
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: {
|