ui5-lib-guard-router 1.3.1 → 1.3.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 +36 -1
- package/dist/.ui5/build-manifest.json +14 -2
- package/dist/index.d.ts +3 -2
- package/dist/resources/ui5/guard/router/.library +1 -1
- package/dist/resources/ui5/guard/router/GuardPipeline-dbg.js +279 -0
- package/dist/resources/ui5/guard/router/GuardPipeline-dbg.js.map +1 -0
- package/dist/resources/ui5/guard/router/GuardPipeline.d.ts +97 -0
- package/dist/resources/ui5/guard/router/GuardPipeline.d.ts.map +1 -0
- package/dist/resources/ui5/guard/router/GuardPipeline.js +2 -0
- package/dist/resources/ui5/guard/router/GuardPipeline.js.map +1 -0
- package/dist/resources/ui5/guard/router/Router-dbg.js +107 -289
- package/dist/resources/ui5/guard/router/Router-dbg.js.map +1 -1
- package/dist/resources/ui5/guard/router/Router.d.ts +31 -79
- package/dist/resources/ui5/guard/router/Router.d.ts.map +1 -1
- package/dist/resources/ui5/guard/router/Router.js +1 -1
- package/dist/resources/ui5/guard/router/Router.js.map +1 -1
- package/dist/resources/ui5/guard/router/library-dbg.js +1 -1
- package/dist/resources/ui5/guard/router/library-dbg.js.map +1 -1
- package/dist/resources/ui5/guard/router/library-preload.js +4 -3
- package/dist/resources/ui5/guard/router/library-preload.js.map +1 -1
- package/dist/resources/ui5/guard/router/library.js +1 -1
- package/dist/resources/ui5/guard/router/library.js.map +1 -1
- package/dist/resources/ui5/guard/router/manifest.json +1 -1
- package/package.json +1 -1
- package/src/GuardPipeline.ts +315 -0
- package/src/Router.ts +106 -332
- package/src/manifest.json +1 -1
package/README.md
CHANGED
|
@@ -659,7 +659,7 @@ Or set the global log level via URL parameter (per-component filtering is only a
|
|
|
659
659
|
| warning | `Guard redirect target "{route}" did not produce a navigation, treating as blocked` | Redirect target did not trigger a follow-up navigation (most commonly an unknown route name) |
|
|
660
660
|
| error | `Guard pipeline failed for "{hash}", blocking navigation` | Async guard pipeline rejected in `parse()` fallback path |
|
|
661
661
|
| error | `Async preflight guard failed for route "{route}", blocking navigation` | Async guard pipeline rejected in `navTo()` preflight path |
|
|
662
|
-
| error | `Enter guard [{n}]
|
|
662
|
+
| error | `Enter guard [{n}] on route "{route}" threw, blocking navigation` | Sync or async enter guard threw an exception |
|
|
663
663
|
| error | `Leave guard [{n}] on route "{route}" threw, blocking navigation` | Sync or async leave guard threw an exception |
|
|
664
664
|
| debug | `Async guard result discarded (superseded by newer navigation)` | A newer navigation invalidated the pending async result (`parse()` path) |
|
|
665
665
|
| debug | `Async preflight result discarded (superseded by newer navigation)` | A newer navigation invalidated the pending async result (`navTo()` path) |
|
|
@@ -676,6 +676,41 @@ Or set the global log level via URL parameter (per-component filtering is only a
|
|
|
676
676
|
|
|
677
677
|
**URL bar shows target hash, then reverts**: This is expected for async guards. The `HashChanger` updates the URL before `parse()` runs. See [URL bar shows target hash during async guards](#url-bar-shows-target-hash-during-async-guards) for the architectural explanation and the busy-indicator pattern.
|
|
678
678
|
|
|
679
|
+
## Testing
|
|
680
|
+
|
|
681
|
+
### Running tests
|
|
682
|
+
|
|
683
|
+
The library ships three QUnit test suites that run in headless Chrome via WebdriverIO:
|
|
684
|
+
|
|
685
|
+
```bash
|
|
686
|
+
# All three suites (Router, NativeRouterCompat, UpstreamParity)
|
|
687
|
+
npm run test:qunit
|
|
688
|
+
|
|
689
|
+
# Full matrix including OpenUI5 1.120 compatibility lane and E2E
|
|
690
|
+
npm run test:full
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
### Accessing private members in tests
|
|
694
|
+
|
|
695
|
+
The router's internal state (phase model, guard registries, generation counter) uses TypeScript's `private` keyword for encapsulation. Tests inspect these internals via [`Reflect.get`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/get):
|
|
696
|
+
|
|
697
|
+
```typescript
|
|
698
|
+
const phase = Reflect.get(router, "_phase");
|
|
699
|
+
const generation = Reflect.get(router, "_parseGeneration");
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
This works because TypeScript `private` is a [compile-time-only constraint](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html). The property exists as a regular enumerable property at runtime, so `Reflect.get` reads it without any cast or `as any`. The alternative -- ECMAScript `#private` fields -- enforces privacy at runtime via `WeakMap`-backed storage. Properties declared with `#` are invisible to `Reflect.get`, bracket notation, and any other external access. The trade-off:
|
|
703
|
+
|
|
704
|
+
| | TypeScript `private` | ECMAScript `#private` |
|
|
705
|
+
| -------------------------------- | ----------------------------------- | ---------------------------------------------------------- |
|
|
706
|
+
| Enforcement | Compile-time only | Runtime (engine-level) |
|
|
707
|
+
| `Reflect.get` access | Works | Returns `undefined` |
|
|
708
|
+
| Bracket notation (`obj["prop"]`) | Works | Returns `undefined` |
|
|
709
|
+
| Test inspection | Straightforward | Requires dedicated accessors |
|
|
710
|
+
| UI5 compatibility | Full (prototype-based class system) | Limited (UI5 metadata introspection cannot see `#` fields) |
|
|
711
|
+
|
|
712
|
+
This library uses TypeScript `private` because UI5's `ManagedObject.extend()` class system relies on prototype-based inheritance and runtime metadata introspection, which is incompatible with ECMAScript `#private` fields. `Reflect.get` is the established pattern across the test suite for inspecting internal state without type-safety escape hatches.
|
|
713
|
+
|
|
679
714
|
## Compatibility
|
|
680
715
|
|
|
681
716
|
> [!IMPORTANT]
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"buildManifest": {
|
|
18
18
|
"manifestVersion": "0.2",
|
|
19
|
-
"timestamp": "2026-03-
|
|
19
|
+
"timestamp": "2026-03-22T11:02:53.747Z",
|
|
20
20
|
"versions": {
|
|
21
21
|
"builderVersion": "4.1.4",
|
|
22
22
|
"projectVersion": "4.0.13",
|
|
@@ -31,9 +31,21 @@
|
|
|
31
31
|
"includedTasks": [],
|
|
32
32
|
"excludedTasks": []
|
|
33
33
|
},
|
|
34
|
-
"version": "1.3.
|
|
34
|
+
"version": "1.3.2",
|
|
35
35
|
"namespace": "ui5/guard/router",
|
|
36
36
|
"tags": {
|
|
37
|
+
"/resources/ui5/guard/router/GuardPipeline-dbg.js": {
|
|
38
|
+
"ui5:IsDebugVariant": true
|
|
39
|
+
},
|
|
40
|
+
"/resources/ui5/guard/router/GuardPipeline-dbg.js.map": {
|
|
41
|
+
"ui5:IsDebugVariant": true
|
|
42
|
+
},
|
|
43
|
+
"/resources/ui5/guard/router/GuardPipeline.js": {
|
|
44
|
+
"ui5:HasDebugVariant": true
|
|
45
|
+
},
|
|
46
|
+
"/resources/ui5/guard/router/GuardPipeline.js.map": {
|
|
47
|
+
"ui5:HasDebugVariant": true
|
|
48
|
+
},
|
|
37
49
|
"/resources/ui5/guard/router/library-dbg.js": {
|
|
38
50
|
"ui5:IsDebugVariant": true
|
|
39
51
|
},
|
package/dist/index.d.ts
CHANGED
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
// - @types/istanbul-lib-report@3.0.3
|
|
33
33
|
// - @types/istanbul-lib-coverage@2.0.6
|
|
34
34
|
// - @types/qunit@2.19.13
|
|
35
|
+
/// <reference path="./resources/ui5/guard/router/NavigationOutcome.d.ts"/>
|
|
36
|
+
/// <reference path="./resources/ui5/guard/router/types.d.ts"/>
|
|
35
37
|
/// <reference path="./resources/ui5/guard/router/Router.d.ts"/>
|
|
36
38
|
/// <reference path="./resources/ui5/guard/router/library.d.ts"/>
|
|
37
|
-
/// <reference path="./resources/ui5/guard/router/
|
|
38
|
-
/// <reference path="./resources/ui5/guard/router/NavigationOutcome.d.ts"/>
|
|
39
|
+
/// <reference path="./resources/ui5/guard/router/GuardPipeline.d.ts"/>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<library xmlns="http://www.sap.com/sap.ui.library.xsd">
|
|
3
3
|
<name>ui5.guard.router</name>
|
|
4
4
|
<vendor>Marco</vendor>
|
|
5
|
-
<version>1.3.
|
|
5
|
+
<version>1.3.2</version>
|
|
6
6
|
<copyright></copyright>
|
|
7
7
|
<title>UI5 Router extension with async navigation guards</title>
|
|
8
8
|
<documentation>Extends sap.m.routing.Router with async navigation guards, running before route matching begins.</documentation>
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
sap.ui.define(["sap/base/Log"], function (Log) {
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const LOG_COMPONENT = "ui5.guard.router.Router";
|
|
5
|
+
function isGuardRedirect(value) {
|
|
6
|
+
if (typeof value !== "object" || value === null) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
const {
|
|
10
|
+
route
|
|
11
|
+
} = value;
|
|
12
|
+
return typeof route === "string" && route.length > 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Promises/A+ thenable detection via duck typing.
|
|
17
|
+
*
|
|
18
|
+
* We intentionally do not use `instanceof Promise` because that misses
|
|
19
|
+
* cross-realm Promises and PromiseLike/thenable objects.
|
|
20
|
+
*/
|
|
21
|
+
function isPromiseLike(value) {
|
|
22
|
+
if (typeof value !== "object" && typeof value !== "function" || value === null) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
return typeof value.then === "function";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Normalized result of the guard decision pipeline.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Standalone guard evaluation pipeline.
|
|
34
|
+
*
|
|
35
|
+
* Owns guard storage (global, enter, leave) and runs the full
|
|
36
|
+
* leave -> global-enter -> route-enter pipeline. Pure logic with
|
|
37
|
+
* no dependency on Router state beyond the current route name
|
|
38
|
+
* passed into evaluate().
|
|
39
|
+
*
|
|
40
|
+
* @namespace ui5.guard.router
|
|
41
|
+
*/
|
|
42
|
+
class GuardPipeline {
|
|
43
|
+
_globalGuards = [];
|
|
44
|
+
_enterGuards = new Map();
|
|
45
|
+
_leaveGuards = new Map();
|
|
46
|
+
addGlobalGuard(guard) {
|
|
47
|
+
this._globalGuards.push(guard);
|
|
48
|
+
}
|
|
49
|
+
removeGlobalGuard(guard) {
|
|
50
|
+
const index = this._globalGuards.indexOf(guard);
|
|
51
|
+
if (index !== -1) {
|
|
52
|
+
this._globalGuards.splice(index, 1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
addEnterGuard(route, guard) {
|
|
56
|
+
this._addToGuardMap(this._enterGuards, route, guard);
|
|
57
|
+
}
|
|
58
|
+
removeEnterGuard(route, guard) {
|
|
59
|
+
this._removeFromGuardMap(this._enterGuards, route, guard);
|
|
60
|
+
}
|
|
61
|
+
addLeaveGuard(route, guard) {
|
|
62
|
+
this._addToGuardMap(this._leaveGuards, route, guard);
|
|
63
|
+
}
|
|
64
|
+
removeLeaveGuard(route, guard) {
|
|
65
|
+
this._removeFromGuardMap(this._leaveGuards, route, guard);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Remove all registered guards.
|
|
70
|
+
*/
|
|
71
|
+
clear() {
|
|
72
|
+
this._globalGuards = [];
|
|
73
|
+
this._enterGuards.clear();
|
|
74
|
+
this._leaveGuards.clear();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Run the full guard pipeline (leave -> global enter -> route enter) and
|
|
79
|
+
* return a normalized decision. Stays synchronous when all guards return
|
|
80
|
+
* plain values; returns a Promise only when an async guard is encountered.
|
|
81
|
+
*
|
|
82
|
+
* @param context - Complete guard context including AbortSignal.
|
|
83
|
+
* `context.fromRoute` controls leave-guard lookup: empty string skips leave guards.
|
|
84
|
+
*/
|
|
85
|
+
evaluate(context) {
|
|
86
|
+
const hasLeaveGuards = context.fromRoute !== "" && this._leaveGuards.has(context.fromRoute);
|
|
87
|
+
const hasEnterGuards = this._globalGuards.length > 0 || context.toRoute !== "" && this._enterGuards.has(context.toRoute);
|
|
88
|
+
if (!hasLeaveGuards && !hasEnterGuards) {
|
|
89
|
+
return {
|
|
90
|
+
action: "allow"
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const processEnterResult = enterResult => {
|
|
94
|
+
if (isPromiseLike(enterResult)) {
|
|
95
|
+
return enterResult.then(r => {
|
|
96
|
+
if (r === true) return {
|
|
97
|
+
action: "allow"
|
|
98
|
+
};
|
|
99
|
+
if (r === false) return {
|
|
100
|
+
action: "block"
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
action: "redirect",
|
|
104
|
+
target: r
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (enterResult === true) return {
|
|
109
|
+
action: "allow"
|
|
110
|
+
};
|
|
111
|
+
if (enterResult === false) return {
|
|
112
|
+
action: "block"
|
|
113
|
+
};
|
|
114
|
+
return {
|
|
115
|
+
action: "redirect",
|
|
116
|
+
target: enterResult
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
const runEnterPhase = () => {
|
|
120
|
+
const enterResult = this._runEnterGuards(context.toRoute, context);
|
|
121
|
+
return processEnterResult(enterResult);
|
|
122
|
+
};
|
|
123
|
+
if (hasLeaveGuards) {
|
|
124
|
+
const leaveResult = this._runLeaveGuards(context);
|
|
125
|
+
if (isPromiseLike(leaveResult)) {
|
|
126
|
+
return leaveResult.then(allowed => {
|
|
127
|
+
if (allowed !== true) return {
|
|
128
|
+
action: "block"
|
|
129
|
+
};
|
|
130
|
+
if (context.signal.aborted) return {
|
|
131
|
+
action: "block"
|
|
132
|
+
};
|
|
133
|
+
return runEnterPhase();
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (leaveResult !== true) return {
|
|
137
|
+
action: "block"
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return runEnterPhase();
|
|
141
|
+
}
|
|
142
|
+
_addToGuardMap(map, key, guard) {
|
|
143
|
+
let guards = map.get(key);
|
|
144
|
+
if (!guards) {
|
|
145
|
+
guards = [];
|
|
146
|
+
map.set(key, guards);
|
|
147
|
+
}
|
|
148
|
+
guards.push(guard);
|
|
149
|
+
}
|
|
150
|
+
_removeFromGuardMap(map, key, guard) {
|
|
151
|
+
const guards = map.get(key);
|
|
152
|
+
if (!guards) return;
|
|
153
|
+
const index = guards.indexOf(guard);
|
|
154
|
+
if (index !== -1) guards.splice(index, 1);
|
|
155
|
+
if (guards.length === 0) map.delete(key);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Run leave guards for the current route. Returns boolean (no redirects).
|
|
160
|
+
*
|
|
161
|
+
* The guard array is snapshot-copied before iteration so that guards
|
|
162
|
+
* may safely add/remove themselves (e.g. one-shot guards) without
|
|
163
|
+
* affecting the current pipeline run.
|
|
164
|
+
*/
|
|
165
|
+
_runLeaveGuards(context) {
|
|
166
|
+
const registered = this._leaveGuards.get(context.fromRoute);
|
|
167
|
+
if (!registered || registered.length === 0) return true;
|
|
168
|
+
const guards = registered.slice();
|
|
169
|
+
for (let i = 0; i < guards.length; i++) {
|
|
170
|
+
try {
|
|
171
|
+
const result = guards[i](context);
|
|
172
|
+
if (isPromiseLike(result)) {
|
|
173
|
+
return this._continueGuardsAsync(result, guards, i, context, candidate => this._validateLeaveGuardResult(candidate), "Leave guard", true);
|
|
174
|
+
}
|
|
175
|
+
if (result !== true) return this._validateLeaveGuardResult(result);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
Log.error(`Leave guard [${i}] on route "${context.fromRoute}" threw, blocking navigation`, String(error), LOG_COMPONENT);
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Run global guards, then route-specific guards. Stays sync when possible. */
|
|
185
|
+
_runEnterGuards(toRoute, context) {
|
|
186
|
+
const globalResult = this._runGuards(this._globalGuards, context);
|
|
187
|
+
if (isPromiseLike(globalResult)) {
|
|
188
|
+
return globalResult.then(result => {
|
|
189
|
+
if (result !== true) return result;
|
|
190
|
+
if (context.signal.aborted) return false;
|
|
191
|
+
return this._runRouteGuards(toRoute, context);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (globalResult !== true) return globalResult;
|
|
195
|
+
return this._runRouteGuards(toRoute, context);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Run route-specific guards if any are registered. */
|
|
199
|
+
_runRouteGuards(toRoute, context) {
|
|
200
|
+
if (!toRoute || !this._enterGuards.has(toRoute)) return true;
|
|
201
|
+
return this._runGuards(this._enterGuards.get(toRoute), context);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Run guards sync; switch to async path if a Promise is returned.
|
|
206
|
+
*
|
|
207
|
+
* The guard array is snapshot-copied before iteration so that guards
|
|
208
|
+
* may safely add/remove themselves (e.g. one-shot guards) without
|
|
209
|
+
* affecting the current pipeline run.
|
|
210
|
+
*/
|
|
211
|
+
_runGuards(guards, context) {
|
|
212
|
+
guards = guards.slice();
|
|
213
|
+
for (let i = 0; i < guards.length; i++) {
|
|
214
|
+
try {
|
|
215
|
+
const result = guards[i](context);
|
|
216
|
+
if (isPromiseLike(result)) {
|
|
217
|
+
return this._continueGuardsAsync(result, guards, i, context, candidate => this._validateGuardResult(candidate), "Enter guard", false);
|
|
218
|
+
}
|
|
219
|
+
if (result !== true) return this._validateGuardResult(result);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
Log.error(`Enter guard [${i}] on route "${context.toRoute}" threw, blocking navigation`, String(error), LOG_COMPONENT);
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Continue guard array async from the first Promise onward.
|
|
230
|
+
*
|
|
231
|
+
* Shared by both enter and leave guard pipelines. The `onBlock` callback
|
|
232
|
+
* determines what to return for non-true results: leave guards always
|
|
233
|
+
* return `false`, enter guards validate and may return redirects.
|
|
234
|
+
*
|
|
235
|
+
* `guards` is typed as `GuardFn[]` for reuse. Leave guard callers
|
|
236
|
+
* pass `LeaveGuardFn[]` which is assignable (narrower return type).
|
|
237
|
+
*
|
|
238
|
+
* @param isLeaveGuard - When true, error logs reference `fromRoute`; otherwise `toRoute`.
|
|
239
|
+
*/
|
|
240
|
+
async _continueGuardsAsync(pendingResult, guards, currentIndex, context, onBlock, label, isLeaveGuard) {
|
|
241
|
+
let guardIndex = currentIndex;
|
|
242
|
+
try {
|
|
243
|
+
const result = await pendingResult;
|
|
244
|
+
if (result !== true) return onBlock(result);
|
|
245
|
+
for (let i = currentIndex + 1; i < guards.length; i++) {
|
|
246
|
+
if (context.signal.aborted) return false;
|
|
247
|
+
guardIndex = i;
|
|
248
|
+
const nextResult = await guards[i](context);
|
|
249
|
+
if (nextResult !== true) return onBlock(nextResult);
|
|
250
|
+
}
|
|
251
|
+
return true;
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (!context.signal.aborted) {
|
|
254
|
+
const route = isLeaveGuard ? context.fromRoute : context.toRoute;
|
|
255
|
+
Log.error(`${label} [${guardIndex}] on route "${route}" threw, blocking navigation`, String(error), LOG_COMPONENT);
|
|
256
|
+
}
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Validate a non-true guard result; invalid values become false. */
|
|
262
|
+
_validateGuardResult(result) {
|
|
263
|
+
if (typeof result === "boolean") return result;
|
|
264
|
+
if (typeof result === "string" && result.length > 0) return result;
|
|
265
|
+
if (isGuardRedirect(result)) return result;
|
|
266
|
+
Log.warning("Guard returned invalid value, treating as block", String(result), LOG_COMPONENT);
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Validate a leave guard result; non-boolean values log a warning and block. */
|
|
271
|
+
_validateLeaveGuardResult(result) {
|
|
272
|
+
if (typeof result === "boolean") return result;
|
|
273
|
+
Log.warning("Leave guard returned non-boolean value, treating as block", String(result), LOG_COMPONENT);
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return GuardPipeline;
|
|
278
|
+
});
|
|
279
|
+
//# sourceMappingURL=GuardPipeline-dbg.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GuardPipeline-dbg.js","names":["LOG_COMPONENT","isGuardRedirect","value","route","length","isPromiseLike","then","GuardPipeline","_globalGuards","_enterGuards","Map","_leaveGuards","addGlobalGuard","guard","push","removeGlobalGuard","index","indexOf","splice","addEnterGuard","_addToGuardMap","removeEnterGuard","_removeFromGuardMap","addLeaveGuard","removeLeaveGuard","clear","evaluate","context","hasLeaveGuards","fromRoute","has","hasEnterGuards","toRoute","action","processEnterResult","enterResult","r","target","runEnterPhase","_runEnterGuards","leaveResult","_runLeaveGuards","allowed","signal","aborted","map","key","guards","get","set","delete","registered","slice","i","result","_continueGuardsAsync","candidate","_validateLeaveGuardResult","error","Log","String","globalResult","_runGuards","_runRouteGuards","_validateGuardResult","pendingResult","currentIndex","onBlock","label","isLeaveGuard","guardIndex","nextResult","warning"],"sources":["GuardPipeline.ts"],"sourcesContent":["import Log from \"sap/base/Log\";\nimport type { GuardFn, GuardContext, GuardResult, GuardRedirect, LeaveGuardFn } from \"./types\";\n\nconst LOG_COMPONENT = \"ui5.guard.router.Router\";\n\nfunction isGuardRedirect(value: unknown): value is GuardRedirect {\n\tif (typeof value !== \"object\" || value === null) {\n\t\treturn false;\n\t}\n\n\tconst { route } = value as GuardRedirect;\n\treturn typeof route === \"string\" && route.length > 0;\n}\n\n/**\n * Promises/A+ thenable detection via duck typing.\n *\n * We intentionally do not use `instanceof Promise` because that misses\n * cross-realm Promises and PromiseLike/thenable objects.\n */\nfunction isPromiseLike<T>(value: unknown): value is PromiseLike<T> {\n\tif ((typeof value !== \"object\" && typeof value !== \"function\") || value === null) {\n\t\treturn false;\n\t}\n\n\treturn typeof (value as PromiseLike<T>).then === \"function\";\n}\n\n/**\n * Normalized result of the guard decision pipeline.\n */\nexport type GuardDecision =\n\t| { action: \"allow\" }\n\t| { action: \"block\" }\n\t| { action: \"redirect\"; target: string | GuardRedirect };\n\n/**\n * Standalone guard evaluation pipeline.\n *\n * Owns guard storage (global, enter, leave) and runs the full\n * leave -> global-enter -> route-enter pipeline. Pure logic with\n * no dependency on Router state beyond the current route name\n * passed into evaluate().\n *\n * @namespace ui5.guard.router\n */\nexport default class GuardPipeline {\n\tprivate _globalGuards: GuardFn[] = [];\n\tprivate _enterGuards = new Map<string, GuardFn[]>();\n\tprivate _leaveGuards = new Map<string, LeaveGuardFn[]>();\n\n\taddGlobalGuard(guard: GuardFn): void {\n\t\tthis._globalGuards.push(guard);\n\t}\n\n\tremoveGlobalGuard(guard: GuardFn): void {\n\t\tconst index = this._globalGuards.indexOf(guard);\n\t\tif (index !== -1) {\n\t\t\tthis._globalGuards.splice(index, 1);\n\t\t}\n\t}\n\n\taddEnterGuard(route: string, guard: GuardFn): void {\n\t\tthis._addToGuardMap(this._enterGuards, route, guard);\n\t}\n\n\tremoveEnterGuard(route: string, guard: GuardFn): void {\n\t\tthis._removeFromGuardMap(this._enterGuards, route, guard);\n\t}\n\n\taddLeaveGuard(route: string, guard: LeaveGuardFn): void {\n\t\tthis._addToGuardMap(this._leaveGuards, route, guard);\n\t}\n\n\tremoveLeaveGuard(route: string, guard: LeaveGuardFn): void {\n\t\tthis._removeFromGuardMap(this._leaveGuards, route, guard);\n\t}\n\n\t/**\n\t * Remove all registered guards.\n\t */\n\tclear(): void {\n\t\tthis._globalGuards = [];\n\t\tthis._enterGuards.clear();\n\t\tthis._leaveGuards.clear();\n\t}\n\n\t/**\n\t * Run the full guard pipeline (leave -> global enter -> route enter) and\n\t * return a normalized decision. Stays synchronous when all guards return\n\t * plain values; returns a Promise only when an async guard is encountered.\n\t *\n\t * @param context - Complete guard context including AbortSignal.\n\t * `context.fromRoute` controls leave-guard lookup: empty string skips leave guards.\n\t */\n\tevaluate(context: GuardContext): GuardDecision | Promise<GuardDecision> {\n\t\tconst hasLeaveGuards = context.fromRoute !== \"\" && this._leaveGuards.has(context.fromRoute);\n\t\tconst hasEnterGuards =\n\t\t\tthis._globalGuards.length > 0 || (context.toRoute !== \"\" && this._enterGuards.has(context.toRoute));\n\n\t\tif (!hasLeaveGuards && !hasEnterGuards) {\n\t\t\treturn { action: \"allow\" };\n\t\t}\n\n\t\tconst processEnterResult = (\n\t\t\tenterResult: GuardResult | Promise<GuardResult>,\n\t\t): GuardDecision | Promise<GuardDecision> => {\n\t\t\tif (isPromiseLike(enterResult)) {\n\t\t\t\treturn enterResult.then((r: GuardResult): GuardDecision => {\n\t\t\t\t\tif (r === true) return { action: \"allow\" };\n\t\t\t\t\tif (r === false) return { action: \"block\" };\n\t\t\t\t\treturn { action: \"redirect\", target: r };\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (enterResult === true) return { action: \"allow\" };\n\t\t\tif (enterResult === false) return { action: \"block\" };\n\t\t\treturn { action: \"redirect\", target: enterResult };\n\t\t};\n\n\t\tconst runEnterPhase = (): GuardDecision | Promise<GuardDecision> => {\n\t\t\tconst enterResult = this._runEnterGuards(context.toRoute, context);\n\t\t\treturn processEnterResult(enterResult);\n\t\t};\n\n\t\tif (hasLeaveGuards) {\n\t\t\tconst leaveResult = this._runLeaveGuards(context);\n\n\t\t\tif (isPromiseLike(leaveResult)) {\n\t\t\t\treturn leaveResult.then((allowed: boolean): GuardDecision | Promise<GuardDecision> => {\n\t\t\t\t\tif (allowed !== true) return { action: \"block\" };\n\t\t\t\t\tif (context.signal.aborted) return { action: \"block\" };\n\t\t\t\t\treturn runEnterPhase();\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (leaveResult !== true) return { action: \"block\" };\n\t\t}\n\n\t\treturn runEnterPhase();\n\t}\n\n\tprivate _addToGuardMap<T>(map: Map<string, T[]>, key: string, guard: T): void {\n\t\tlet guards = map.get(key);\n\t\tif (!guards) {\n\t\t\tguards = [];\n\t\t\tmap.set(key, guards);\n\t\t}\n\t\tguards.push(guard);\n\t}\n\n\tprivate _removeFromGuardMap<T>(map: Map<string, T[]>, key: string, guard: T): void {\n\t\tconst guards = map.get(key);\n\t\tif (!guards) return;\n\t\tconst index = guards.indexOf(guard);\n\t\tif (index !== -1) guards.splice(index, 1);\n\t\tif (guards.length === 0) map.delete(key);\n\t}\n\n\t/**\n\t * Run leave guards for the current route. Returns boolean (no redirects).\n\t *\n\t * The guard array is snapshot-copied before iteration so that guards\n\t * may safely add/remove themselves (e.g. one-shot guards) without\n\t * affecting the current pipeline run.\n\t */\n\tprivate _runLeaveGuards(context: GuardContext): boolean | Promise<boolean> {\n\t\tconst registered = this._leaveGuards.get(context.fromRoute);\n\t\tif (!registered || registered.length === 0) return true;\n\n\t\tconst guards = registered.slice();\n\t\tfor (let i = 0; i < guards.length; i++) {\n\t\t\ttry {\n\t\t\t\tconst result = guards[i](context);\n\t\t\t\tif (isPromiseLike(result)) {\n\t\t\t\t\treturn this._continueGuardsAsync(\n\t\t\t\t\t\tresult,\n\t\t\t\t\t\tguards,\n\t\t\t\t\t\ti,\n\t\t\t\t\t\tcontext,\n\t\t\t\t\t\t(candidate) => this._validateLeaveGuardResult(candidate),\n\t\t\t\t\t\t\"Leave guard\",\n\t\t\t\t\t\ttrue,\n\t\t\t\t\t) as Promise<boolean>;\n\t\t\t\t}\n\t\t\t\tif (result !== true) return this._validateLeaveGuardResult(result);\n\t\t\t} catch (error) {\n\t\t\t\tLog.error(\n\t\t\t\t\t`Leave guard [${i}] on route \"${context.fromRoute}\" threw, blocking navigation`,\n\t\t\t\t\tString(error),\n\t\t\t\t\tLOG_COMPONENT,\n\t\t\t\t);\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t/** Run global guards, then route-specific guards. Stays sync when possible. */\n\tprivate _runEnterGuards(toRoute: string, context: GuardContext): GuardResult | Promise<GuardResult> {\n\t\tconst globalResult = this._runGuards(this._globalGuards, context);\n\n\t\tif (isPromiseLike(globalResult)) {\n\t\t\treturn globalResult.then((result: GuardResult) => {\n\t\t\t\tif (result !== true) return result;\n\t\t\t\tif (context.signal.aborted) return false;\n\t\t\t\treturn this._runRouteGuards(toRoute, context);\n\t\t\t});\n\t\t}\n\t\tif (globalResult !== true) return globalResult;\n\t\treturn this._runRouteGuards(toRoute, context);\n\t}\n\n\t/** Run route-specific guards if any are registered. */\n\tprivate _runRouteGuards(toRoute: string, context: GuardContext): GuardResult | Promise<GuardResult> {\n\t\tif (!toRoute || !this._enterGuards.has(toRoute)) return true;\n\t\treturn this._runGuards(this._enterGuards.get(toRoute)!, context);\n\t}\n\n\t/**\n\t * Run guards sync; switch to async path if a Promise is returned.\n\t *\n\t * The guard array is snapshot-copied before iteration so that guards\n\t * may safely add/remove themselves (e.g. one-shot guards) without\n\t * affecting the current pipeline run.\n\t */\n\tprivate _runGuards(guards: GuardFn[], context: GuardContext): GuardResult | Promise<GuardResult> {\n\t\tguards = guards.slice();\n\t\tfor (let i = 0; i < guards.length; i++) {\n\t\t\ttry {\n\t\t\t\tconst result = guards[i](context);\n\t\t\t\tif (isPromiseLike(result)) {\n\t\t\t\t\treturn this._continueGuardsAsync(\n\t\t\t\t\t\tresult,\n\t\t\t\t\t\tguards,\n\t\t\t\t\t\ti,\n\t\t\t\t\t\tcontext,\n\t\t\t\t\t\t(candidate) => this._validateGuardResult(candidate),\n\t\t\t\t\t\t\"Enter guard\",\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (result !== true) return this._validateGuardResult(result);\n\t\t\t} catch (error) {\n\t\t\t\tLog.error(\n\t\t\t\t\t`Enter guard [${i}] on route \"${context.toRoute}\" threw, blocking navigation`,\n\t\t\t\t\tString(error),\n\t\t\t\t\tLOG_COMPONENT,\n\t\t\t\t);\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * Continue guard array async from the first Promise onward.\n\t *\n\t * Shared by both enter and leave guard pipelines. The `onBlock` callback\n\t * determines what to return for non-true results: leave guards always\n\t * return `false`, enter guards validate and may return redirects.\n\t *\n\t * `guards` is typed as `GuardFn[]` for reuse. Leave guard callers\n\t * pass `LeaveGuardFn[]` which is assignable (narrower return type).\n\t *\n\t * @param isLeaveGuard - When true, error logs reference `fromRoute`; otherwise `toRoute`.\n\t */\n\tprivate async _continueGuardsAsync(\n\t\tpendingResult: PromiseLike<GuardResult>,\n\t\tguards: GuardFn[],\n\t\tcurrentIndex: number,\n\t\tcontext: GuardContext,\n\t\tonBlock: (result: unknown) => GuardResult,\n\t\tlabel: string,\n\t\tisLeaveGuard: boolean,\n\t): Promise<GuardResult> {\n\t\tlet guardIndex = currentIndex;\n\t\ttry {\n\t\t\tconst result = await pendingResult;\n\t\t\tif (result !== true) return onBlock(result);\n\n\t\t\tfor (let i = currentIndex + 1; i < guards.length; i++) {\n\t\t\t\tif (context.signal.aborted) return false;\n\t\t\t\tguardIndex = i;\n\t\t\t\tconst nextResult = await guards[i](context);\n\t\t\t\tif (nextResult !== true) return onBlock(nextResult);\n\t\t\t}\n\t\t\treturn true;\n\t\t} catch (error) {\n\t\t\tif (!context.signal.aborted) {\n\t\t\t\tconst route = isLeaveGuard ? context.fromRoute : context.toRoute;\n\t\t\t\tLog.error(\n\t\t\t\t\t`${label} [${guardIndex}] on route \"${route}\" threw, blocking navigation`,\n\t\t\t\t\tString(error),\n\t\t\t\t\tLOG_COMPONENT,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/** Validate a non-true guard result; invalid values become false. */\n\tprivate _validateGuardResult(result: unknown): GuardResult {\n\t\tif (typeof result === \"boolean\") return result;\n\t\tif (typeof result === \"string\" && result.length > 0) return result;\n\t\tif (isGuardRedirect(result)) return result;\n\t\tLog.warning(\"Guard returned invalid value, treating as block\", String(result), LOG_COMPONENT);\n\t\treturn false;\n\t}\n\n\t/** Validate a leave guard result; non-boolean values log a warning and block. */\n\tprivate _validateLeaveGuardResult(result: unknown): boolean {\n\t\tif (typeof result === \"boolean\") return result;\n\t\tLog.warning(\"Leave guard returned non-boolean value, treating as block\", String(result), LOG_COMPONENT);\n\t\treturn false;\n\t}\n}\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
declare module "ui5/guard/router/GuardPipeline" {
|
|
2
|
+
import type { GuardFn, GuardContext, GuardRedirect, LeaveGuardFn } from "ui5/guard/router/types";
|
|
3
|
+
const LOG_COMPONENT = "ui5.guard.router.Router";
|
|
4
|
+
function isGuardRedirect(value: unknown): value is GuardRedirect;
|
|
5
|
+
/**
|
|
6
|
+
* Promises/A+ thenable detection via duck typing.
|
|
7
|
+
*
|
|
8
|
+
* We intentionally do not use `instanceof Promise` because that misses
|
|
9
|
+
* cross-realm Promises and PromiseLike/thenable objects.
|
|
10
|
+
*/
|
|
11
|
+
function isPromiseLike<T>(value: unknown): value is PromiseLike<T>;
|
|
12
|
+
/**
|
|
13
|
+
* Normalized result of the guard decision pipeline.
|
|
14
|
+
*/
|
|
15
|
+
type GuardDecision = {
|
|
16
|
+
action: "allow";
|
|
17
|
+
} | {
|
|
18
|
+
action: "block";
|
|
19
|
+
} | {
|
|
20
|
+
action: "redirect";
|
|
21
|
+
target: string | GuardRedirect;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Standalone guard evaluation pipeline.
|
|
25
|
+
*
|
|
26
|
+
* Owns guard storage (global, enter, leave) and runs the full
|
|
27
|
+
* leave -> global-enter -> route-enter pipeline. Pure logic with
|
|
28
|
+
* no dependency on Router state beyond the current route name
|
|
29
|
+
* passed into evaluate().
|
|
30
|
+
*
|
|
31
|
+
* @namespace ui5.guard.router
|
|
32
|
+
*/
|
|
33
|
+
export default class GuardPipeline {
|
|
34
|
+
private _globalGuards;
|
|
35
|
+
private _enterGuards;
|
|
36
|
+
private _leaveGuards;
|
|
37
|
+
addGlobalGuard(guard: GuardFn): void;
|
|
38
|
+
removeGlobalGuard(guard: GuardFn): void;
|
|
39
|
+
addEnterGuard(route: string, guard: GuardFn): void;
|
|
40
|
+
removeEnterGuard(route: string, guard: GuardFn): void;
|
|
41
|
+
addLeaveGuard(route: string, guard: LeaveGuardFn): void;
|
|
42
|
+
removeLeaveGuard(route: string, guard: LeaveGuardFn): void;
|
|
43
|
+
/**
|
|
44
|
+
* Remove all registered guards.
|
|
45
|
+
*/
|
|
46
|
+
clear(): void;
|
|
47
|
+
/**
|
|
48
|
+
* Run the full guard pipeline (leave -> global enter -> route enter) and
|
|
49
|
+
* return a normalized decision. Stays synchronous when all guards return
|
|
50
|
+
* plain values; returns a Promise only when an async guard is encountered.
|
|
51
|
+
*
|
|
52
|
+
* @param context - Complete guard context including AbortSignal.
|
|
53
|
+
* `context.fromRoute` controls leave-guard lookup: empty string skips leave guards.
|
|
54
|
+
*/
|
|
55
|
+
evaluate(context: GuardContext): GuardDecision | Promise<GuardDecision>;
|
|
56
|
+
private _addToGuardMap;
|
|
57
|
+
private _removeFromGuardMap;
|
|
58
|
+
/**
|
|
59
|
+
* Run leave guards for the current route. Returns boolean (no redirects).
|
|
60
|
+
*
|
|
61
|
+
* The guard array is snapshot-copied before iteration so that guards
|
|
62
|
+
* may safely add/remove themselves (e.g. one-shot guards) without
|
|
63
|
+
* affecting the current pipeline run.
|
|
64
|
+
*/
|
|
65
|
+
private _runLeaveGuards;
|
|
66
|
+
/** Run global guards, then route-specific guards. Stays sync when possible. */
|
|
67
|
+
private _runEnterGuards;
|
|
68
|
+
/** Run route-specific guards if any are registered. */
|
|
69
|
+
private _runRouteGuards;
|
|
70
|
+
/**
|
|
71
|
+
* Run guards sync; switch to async path if a Promise is returned.
|
|
72
|
+
*
|
|
73
|
+
* The guard array is snapshot-copied before iteration so that guards
|
|
74
|
+
* may safely add/remove themselves (e.g. one-shot guards) without
|
|
75
|
+
* affecting the current pipeline run.
|
|
76
|
+
*/
|
|
77
|
+
private _runGuards;
|
|
78
|
+
/**
|
|
79
|
+
* Continue guard array async from the first Promise onward.
|
|
80
|
+
*
|
|
81
|
+
* Shared by both enter and leave guard pipelines. The `onBlock` callback
|
|
82
|
+
* determines what to return for non-true results: leave guards always
|
|
83
|
+
* return `false`, enter guards validate and may return redirects.
|
|
84
|
+
*
|
|
85
|
+
* `guards` is typed as `GuardFn[]` for reuse. Leave guard callers
|
|
86
|
+
* pass `LeaveGuardFn[]` which is assignable (narrower return type).
|
|
87
|
+
*
|
|
88
|
+
* @param isLeaveGuard - When true, error logs reference `fromRoute`; otherwise `toRoute`.
|
|
89
|
+
*/
|
|
90
|
+
private _continueGuardsAsync;
|
|
91
|
+
/** Validate a non-true guard result; invalid values become false. */
|
|
92
|
+
private _validateGuardResult;
|
|
93
|
+
/** Validate a leave guard result; non-boolean values log a warning and block. */
|
|
94
|
+
private _validateLeaveGuardResult;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=GuardPipeline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GuardPipeline.d.ts","sourceRoot":"../../../../..","sources":["src/GuardPipeline.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,gCAAgC,CAAC;IAEhD,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAe,aAAa,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;IAE9G,MAAM,aAAa,4BAA4B,CAAC;IAEhD,SAAS,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,aAAa,CAO/D;IAED;;;;;OAKG;IACH,SAAS,aAAa,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,CAAC,CAAC,CAAC,CAMjE;IAED;;OAEG;IACH,KAAY,aAAa,GACtB;QAAE,MAAM,EAAE,OAAO,CAAA;KAAE,GACnB;QAAE,MAAM,EAAE,OAAO,CAAA;KAAE,GACnB;QAAE,MAAM,EAAE,UAAU,CAAC;QAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CAAA;KAAE,CAAC;IAE1D;;;;;;;;;OASG;IACH,MAAM,CAAC,OAAO,OAAO,aAAa;QACjC,OAAO,CAAC,aAAa,CAAiB;QACtC,OAAO,CAAC,YAAY,CAAgC;QACpD,OAAO,CAAC,YAAY,CAAqC;QAEzD,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;QAIpC,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;QAOvC,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;QAIlD,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;QAIrD,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,IAAI;QAIvD,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,IAAI;QAI1D;;WAEG;QACH,KAAK,IAAI,IAAI;QAMb;;;;;;;WAOG;QACH,QAAQ,CAAC,OAAO,EAAE,YAAY,GAAG,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;QA6CvE,OAAO,CAAC,cAAc;QAStB,OAAO,CAAC,mBAAmB;QAQ3B;;;;;;WAMG;QACH,OAAO,CAAC,eAAe;QAgCvB,+EAA+E;QAC/E,OAAO,CAAC,eAAe;QAcvB,uDAAuD;QACvD,OAAO,CAAC,eAAe;QAKvB;;;;;;WAMG;QACH,OAAO,CAAC,UAAU;QA6BlB;;;;;;;;;;;WAWG;gBACW,oBAAoB;QAkClC,qEAAqE;QACrE,OAAO,CAAC,oBAAoB;QAQ5B,iFAAiF;QACjF,OAAO,CAAC,yBAAyB;KAKjC;CAEA"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
sap.ui.define(["sap/base/Log"],function(r){"use strict";const t="ui5.guard.router.Router";function e(r){if(typeof r!=="object"||r===null){return false}const{route:t}=r;return typeof t==="string"&&t.length>0}function a(r){if(typeof r!=="object"&&typeof r!=="function"||r===null){return false}return typeof r.then==="function"}class u{_globalGuards=[];_enterGuards=new Map;_leaveGuards=new Map;addGlobalGuard(r){this._globalGuards.push(r)}removeGlobalGuard(r){const t=this._globalGuards.indexOf(r);if(t!==-1){this._globalGuards.splice(t,1)}}addEnterGuard(r,t){this._addToGuardMap(this._enterGuards,r,t)}removeEnterGuard(r,t){this._removeFromGuardMap(this._enterGuards,r,t)}addLeaveGuard(r,t){this._addToGuardMap(this._leaveGuards,r,t)}removeLeaveGuard(r,t){this._removeFromGuardMap(this._leaveGuards,r,t)}clear(){this._globalGuards=[];this._enterGuards.clear();this._leaveGuards.clear()}evaluate(r){const t=r.fromRoute!==""&&this._leaveGuards.has(r.fromRoute);const e=this._globalGuards.length>0||r.toRoute!==""&&this._enterGuards.has(r.toRoute);if(!t&&!e){return{action:"allow"}}const u=r=>{if(a(r)){return r.then(r=>{if(r===true)return{action:"allow"};if(r===false)return{action:"block"};return{action:"redirect",target:r}})}if(r===true)return{action:"allow"};if(r===false)return{action:"block"};return{action:"redirect",target:r}};const n=()=>{const t=this._runEnterGuards(r.toRoute,r);return u(t)};if(t){const t=this._runLeaveGuards(r);if(a(t)){return t.then(t=>{if(t!==true)return{action:"block"};if(r.signal.aborted)return{action:"block"};return n()})}if(t!==true)return{action:"block"}}return n()}_addToGuardMap(r,t,e){let a=r.get(t);if(!a){a=[];r.set(t,a)}a.push(e)}_removeFromGuardMap(r,t,e){const a=r.get(t);if(!a)return;const u=a.indexOf(e);if(u!==-1)a.splice(u,1);if(a.length===0)r.delete(t)}_runLeaveGuards(e){const u=this._leaveGuards.get(e.fromRoute);if(!u||u.length===0)return true;const n=u.slice();for(let u=0;u<n.length;u++){try{const r=n[u](e);if(a(r)){return this._continueGuardsAsync(r,n,u,e,r=>this._validateLeaveGuardResult(r),"Leave guard",true)}if(r!==true)return this._validateLeaveGuardResult(r)}catch(a){r.error(`Leave guard [${u}] on route "${e.fromRoute}" threw, blocking navigation`,String(a),t);return false}}return true}_runEnterGuards(r,t){const e=this._runGuards(this._globalGuards,t);if(a(e)){return e.then(e=>{if(e!==true)return e;if(t.signal.aborted)return false;return this._runRouteGuards(r,t)})}if(e!==true)return e;return this._runRouteGuards(r,t)}_runRouteGuards(r,t){if(!r||!this._enterGuards.has(r))return true;return this._runGuards(this._enterGuards.get(r),t)}_runGuards(e,u){e=e.slice();for(let n=0;n<e.length;n++){try{const r=e[n](u);if(a(r)){return this._continueGuardsAsync(r,e,n,u,r=>this._validateGuardResult(r),"Enter guard",false)}if(r!==true)return this._validateGuardResult(r)}catch(e){r.error(`Enter guard [${n}] on route "${u.toRoute}" threw, blocking navigation`,String(e),t);return false}}return true}async _continueGuardsAsync(e,a,u,n,o,i,s){let l=u;try{const r=await e;if(r!==true)return o(r);for(let r=u+1;r<a.length;r++){if(n.signal.aborted)return false;l=r;const t=await a[r](n);if(t!==true)return o(t)}return true}catch(e){if(!n.signal.aborted){const a=s?n.fromRoute:n.toRoute;r.error(`${i} [${l}] on route "${a}" threw, blocking navigation`,String(e),t)}return false}}_validateGuardResult(a){if(typeof a==="boolean")return a;if(typeof a==="string"&&a.length>0)return a;if(e(a))return a;r.warning("Guard returned invalid value, treating as block",String(a),t);return false}_validateLeaveGuardResult(e){if(typeof e==="boolean")return e;r.warning("Leave guard returned non-boolean value, treating as block",String(e),t);return false}}return u});
|
|
2
|
+
//# sourceMappingURL=GuardPipeline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GuardPipeline.js","names":["LOG_COMPONENT","isGuardRedirect","value","route","length","isPromiseLike","then","GuardPipeline","_globalGuards","_enterGuards","Map","_leaveGuards","addGlobalGuard","guard","this","push","removeGlobalGuard","index","indexOf","splice","addEnterGuard","_addToGuardMap","removeEnterGuard","_removeFromGuardMap","addLeaveGuard","removeLeaveGuard","clear","evaluate","context","hasLeaveGuards","fromRoute","has","hasEnterGuards","toRoute","action","processEnterResult","enterResult","r","target","runEnterPhase","_runEnterGuards","leaveResult","_runLeaveGuards","allowed","signal","aborted","map","key","guards","get","set","delete","registered","slice","i","result","_continueGuardsAsync","candidate","_validateLeaveGuardResult","error","Log","String","globalResult","_runGuards","_runRouteGuards","_validateGuardResult","pendingResult","currentIndex","onBlock","label","isLeaveGuard","guardIndex","nextResult","warning"],"sources":["GuardPipeline.ts"],"sourcesContent":["import Log from \"sap/base/Log\";\nimport type { GuardFn, GuardContext, GuardResult, GuardRedirect, LeaveGuardFn } from \"./types\";\n\nconst LOG_COMPONENT = \"ui5.guard.router.Router\";\n\nfunction isGuardRedirect(value: unknown): value is GuardRedirect {\n\tif (typeof value !== \"object\" || value === null) {\n\t\treturn false;\n\t}\n\n\tconst { route } = value as GuardRedirect;\n\treturn typeof route === \"string\" && route.length > 0;\n}\n\n/**\n * Promises/A+ thenable detection via duck typing.\n *\n * We intentionally do not use `instanceof Promise` because that misses\n * cross-realm Promises and PromiseLike/thenable objects.\n */\nfunction isPromiseLike<T>(value: unknown): value is PromiseLike<T> {\n\tif ((typeof value !== \"object\" && typeof value !== \"function\") || value === null) {\n\t\treturn false;\n\t}\n\n\treturn typeof (value as PromiseLike<T>).then === \"function\";\n}\n\n/**\n * Normalized result of the guard decision pipeline.\n */\nexport type GuardDecision =\n\t| { action: \"allow\" }\n\t| { action: \"block\" }\n\t| { action: \"redirect\"; target: string | GuardRedirect };\n\n/**\n * Standalone guard evaluation pipeline.\n *\n * Owns guard storage (global, enter, leave) and runs the full\n * leave -> global-enter -> route-enter pipeline. Pure logic with\n * no dependency on Router state beyond the current route name\n * passed into evaluate().\n *\n * @namespace ui5.guard.router\n */\nexport default class GuardPipeline {\n\tprivate _globalGuards: GuardFn[] = [];\n\tprivate _enterGuards = new Map<string, GuardFn[]>();\n\tprivate _leaveGuards = new Map<string, LeaveGuardFn[]>();\n\n\taddGlobalGuard(guard: GuardFn): void {\n\t\tthis._globalGuards.push(guard);\n\t}\n\n\tremoveGlobalGuard(guard: GuardFn): void {\n\t\tconst index = this._globalGuards.indexOf(guard);\n\t\tif (index !== -1) {\n\t\t\tthis._globalGuards.splice(index, 1);\n\t\t}\n\t}\n\n\taddEnterGuard(route: string, guard: GuardFn): void {\n\t\tthis._addToGuardMap(this._enterGuards, route, guard);\n\t}\n\n\tremoveEnterGuard(route: string, guard: GuardFn): void {\n\t\tthis._removeFromGuardMap(this._enterGuards, route, guard);\n\t}\n\n\taddLeaveGuard(route: string, guard: LeaveGuardFn): void {\n\t\tthis._addToGuardMap(this._leaveGuards, route, guard);\n\t}\n\n\tremoveLeaveGuard(route: string, guard: LeaveGuardFn): void {\n\t\tthis._removeFromGuardMap(this._leaveGuards, route, guard);\n\t}\n\n\t/**\n\t * Remove all registered guards.\n\t */\n\tclear(): void {\n\t\tthis._globalGuards = [];\n\t\tthis._enterGuards.clear();\n\t\tthis._leaveGuards.clear();\n\t}\n\n\t/**\n\t * Run the full guard pipeline (leave -> global enter -> route enter) and\n\t * return a normalized decision. Stays synchronous when all guards return\n\t * plain values; returns a Promise only when an async guard is encountered.\n\t *\n\t * @param context - Complete guard context including AbortSignal.\n\t * `context.fromRoute` controls leave-guard lookup: empty string skips leave guards.\n\t */\n\tevaluate(context: GuardContext): GuardDecision | Promise<GuardDecision> {\n\t\tconst hasLeaveGuards = context.fromRoute !== \"\" && this._leaveGuards.has(context.fromRoute);\n\t\tconst hasEnterGuards =\n\t\t\tthis._globalGuards.length > 0 || (context.toRoute !== \"\" && this._enterGuards.has(context.toRoute));\n\n\t\tif (!hasLeaveGuards && !hasEnterGuards) {\n\t\t\treturn { action: \"allow\" };\n\t\t}\n\n\t\tconst processEnterResult = (\n\t\t\tenterResult: GuardResult | Promise<GuardResult>,\n\t\t): GuardDecision | Promise<GuardDecision> => {\n\t\t\tif (isPromiseLike(enterResult)) {\n\t\t\t\treturn enterResult.then((r: GuardResult): GuardDecision => {\n\t\t\t\t\tif (r === true) return { action: \"allow\" };\n\t\t\t\t\tif (r === false) return { action: \"block\" };\n\t\t\t\t\treturn { action: \"redirect\", target: r };\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (enterResult === true) return { action: \"allow\" };\n\t\t\tif (enterResult === false) return { action: \"block\" };\n\t\t\treturn { action: \"redirect\", target: enterResult };\n\t\t};\n\n\t\tconst runEnterPhase = (): GuardDecision | Promise<GuardDecision> => {\n\t\t\tconst enterResult = this._runEnterGuards(context.toRoute, context);\n\t\t\treturn processEnterResult(enterResult);\n\t\t};\n\n\t\tif (hasLeaveGuards) {\n\t\t\tconst leaveResult = this._runLeaveGuards(context);\n\n\t\t\tif (isPromiseLike(leaveResult)) {\n\t\t\t\treturn leaveResult.then((allowed: boolean): GuardDecision | Promise<GuardDecision> => {\n\t\t\t\t\tif (allowed !== true) return { action: \"block\" };\n\t\t\t\t\tif (context.signal.aborted) return { action: \"block\" };\n\t\t\t\t\treturn runEnterPhase();\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (leaveResult !== true) return { action: \"block\" };\n\t\t}\n\n\t\treturn runEnterPhase();\n\t}\n\n\tprivate _addToGuardMap<T>(map: Map<string, T[]>, key: string, guard: T): void {\n\t\tlet guards = map.get(key);\n\t\tif (!guards) {\n\t\t\tguards = [];\n\t\t\tmap.set(key, guards);\n\t\t}\n\t\tguards.push(guard);\n\t}\n\n\tprivate _removeFromGuardMap<T>(map: Map<string, T[]>, key: string, guard: T): void {\n\t\tconst guards = map.get(key);\n\t\tif (!guards) return;\n\t\tconst index = guards.indexOf(guard);\n\t\tif (index !== -1) guards.splice(index, 1);\n\t\tif (guards.length === 0) map.delete(key);\n\t}\n\n\t/**\n\t * Run leave guards for the current route. Returns boolean (no redirects).\n\t *\n\t * The guard array is snapshot-copied before iteration so that guards\n\t * may safely add/remove themselves (e.g. one-shot guards) without\n\t * affecting the current pipeline run.\n\t */\n\tprivate _runLeaveGuards(context: GuardContext): boolean | Promise<boolean> {\n\t\tconst registered = this._leaveGuards.get(context.fromRoute);\n\t\tif (!registered || registered.length === 0) return true;\n\n\t\tconst guards = registered.slice();\n\t\tfor (let i = 0; i < guards.length; i++) {\n\t\t\ttry {\n\t\t\t\tconst result = guards[i](context);\n\t\t\t\tif (isPromiseLike(result)) {\n\t\t\t\t\treturn this._continueGuardsAsync(\n\t\t\t\t\t\tresult,\n\t\t\t\t\t\tguards,\n\t\t\t\t\t\ti,\n\t\t\t\t\t\tcontext,\n\t\t\t\t\t\t(candidate) => this._validateLeaveGuardResult(candidate),\n\t\t\t\t\t\t\"Leave guard\",\n\t\t\t\t\t\ttrue,\n\t\t\t\t\t) as Promise<boolean>;\n\t\t\t\t}\n\t\t\t\tif (result !== true) return this._validateLeaveGuardResult(result);\n\t\t\t} catch (error) {\n\t\t\t\tLog.error(\n\t\t\t\t\t`Leave guard [${i}] on route \"${context.fromRoute}\" threw, blocking navigation`,\n\t\t\t\t\tString(error),\n\t\t\t\t\tLOG_COMPONENT,\n\t\t\t\t);\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t/** Run global guards, then route-specific guards. Stays sync when possible. */\n\tprivate _runEnterGuards(toRoute: string, context: GuardContext): GuardResult | Promise<GuardResult> {\n\t\tconst globalResult = this._runGuards(this._globalGuards, context);\n\n\t\tif (isPromiseLike(globalResult)) {\n\t\t\treturn globalResult.then((result: GuardResult) => {\n\t\t\t\tif (result !== true) return result;\n\t\t\t\tif (context.signal.aborted) return false;\n\t\t\t\treturn this._runRouteGuards(toRoute, context);\n\t\t\t});\n\t\t}\n\t\tif (globalResult !== true) return globalResult;\n\t\treturn this._runRouteGuards(toRoute, context);\n\t}\n\n\t/** Run route-specific guards if any are registered. */\n\tprivate _runRouteGuards(toRoute: string, context: GuardContext): GuardResult | Promise<GuardResult> {\n\t\tif (!toRoute || !this._enterGuards.has(toRoute)) return true;\n\t\treturn this._runGuards(this._enterGuards.get(toRoute)!, context);\n\t}\n\n\t/**\n\t * Run guards sync; switch to async path if a Promise is returned.\n\t *\n\t * The guard array is snapshot-copied before iteration so that guards\n\t * may safely add/remove themselves (e.g. one-shot guards) without\n\t * affecting the current pipeline run.\n\t */\n\tprivate _runGuards(guards: GuardFn[], context: GuardContext): GuardResult | Promise<GuardResult> {\n\t\tguards = guards.slice();\n\t\tfor (let i = 0; i < guards.length; i++) {\n\t\t\ttry {\n\t\t\t\tconst result = guards[i](context);\n\t\t\t\tif (isPromiseLike(result)) {\n\t\t\t\t\treturn this._continueGuardsAsync(\n\t\t\t\t\t\tresult,\n\t\t\t\t\t\tguards,\n\t\t\t\t\t\ti,\n\t\t\t\t\t\tcontext,\n\t\t\t\t\t\t(candidate) => this._validateGuardResult(candidate),\n\t\t\t\t\t\t\"Enter guard\",\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (result !== true) return this._validateGuardResult(result);\n\t\t\t} catch (error) {\n\t\t\t\tLog.error(\n\t\t\t\t\t`Enter guard [${i}] on route \"${context.toRoute}\" threw, blocking navigation`,\n\t\t\t\t\tString(error),\n\t\t\t\t\tLOG_COMPONENT,\n\t\t\t\t);\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * Continue guard array async from the first Promise onward.\n\t *\n\t * Shared by both enter and leave guard pipelines. The `onBlock` callback\n\t * determines what to return for non-true results: leave guards always\n\t * return `false`, enter guards validate and may return redirects.\n\t *\n\t * `guards` is typed as `GuardFn[]` for reuse. Leave guard callers\n\t * pass `LeaveGuardFn[]` which is assignable (narrower return type).\n\t *\n\t * @param isLeaveGuard - When true, error logs reference `fromRoute`; otherwise `toRoute`.\n\t */\n\tprivate async _continueGuardsAsync(\n\t\tpendingResult: PromiseLike<GuardResult>,\n\t\tguards: GuardFn[],\n\t\tcurrentIndex: number,\n\t\tcontext: GuardContext,\n\t\tonBlock: (result: unknown) => GuardResult,\n\t\tlabel: string,\n\t\tisLeaveGuard: boolean,\n\t): Promise<GuardResult> {\n\t\tlet guardIndex = currentIndex;\n\t\ttry {\n\t\t\tconst result = await pendingResult;\n\t\t\tif (result !== true) return onBlock(result);\n\n\t\t\tfor (let i = currentIndex + 1; i < guards.length; i++) {\n\t\t\t\tif (context.signal.aborted) return false;\n\t\t\t\tguardIndex = i;\n\t\t\t\tconst nextResult = await guards[i](context);\n\t\t\t\tif (nextResult !== true) return onBlock(nextResult);\n\t\t\t}\n\t\t\treturn true;\n\t\t} catch (error) {\n\t\t\tif (!context.signal.aborted) {\n\t\t\t\tconst route = isLeaveGuard ? context.fromRoute : context.toRoute;\n\t\t\t\tLog.error(\n\t\t\t\t\t`${label} [${guardIndex}] on route \"${route}\" threw, blocking navigation`,\n\t\t\t\t\tString(error),\n\t\t\t\t\tLOG_COMPONENT,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/** Validate a non-true guard result; invalid values become false. */\n\tprivate _validateGuardResult(result: unknown): GuardResult {\n\t\tif (typeof result === \"boolean\") return result;\n\t\tif (typeof result === \"string\" && result.length > 0) return result;\n\t\tif (isGuardRedirect(result)) return result;\n\t\tLog.warning(\"Guard returned invalid value, treating as block\", String(result), LOG_COMPONENT);\n\t\treturn false;\n\t}\n\n\t/** Validate a leave guard result; non-boolean values log a warning and block. */\n\tprivate _validateLeaveGuardResult(result: unknown): boolean {\n\t\tif (typeof result === \"boolean\") return result;\n\t\tLog.warning(\"Leave guard returned non-boolean value, treating as block\", String(result), LOG_COMPONENT);\n\t\treturn false;\n\t}\n}\n"],"mappings":"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","ignoreList":[]}
|