@nocobase/plugin-flow-engine 2.2.0-beta.9 → 3.0.0-alpha.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/dist/ai/ai-employees/nathan/prompt.md +2 -0
- package/dist/ai/ai-employees/nathan/skills/frontend-developer/SKILLS.md +1 -0
- package/dist/ai/ai-employees/nathan/skills/frontend-developer/tools/getContextApis.js +2 -2
- package/dist/ai/ai-employees/nathan/skills/frontend-developer/tools/getContextEnvs.js +2 -2
- package/dist/ai/ai-employees/nathan/skills/frontend-developer/tools/getContextVars.js +2 -2
- package/dist/ai/ai-employees/nathan/skills/frontend-developer/tools/lintAndTestJS.js +2 -2
- package/dist/ai/ai-employees/nathan/skills/frontend-developer/tools/patchJSCode.js +4 -4
- package/dist/ai/ai-employees/nathan/skills/frontend-developer/tools/readJSCode.js +2 -2
- package/dist/ai/ai-employees/nathan/skills/frontend-developer/tools/writeJSCode.js +2 -2
- package/dist/ai/docs/runjs/context/ai.md +396 -0
- package/dist/externalVersion.js +9 -9
- package/dist/locale/en-US.json +2 -0
- package/dist/locale/index.d.ts +4 -0
- package/dist/locale/zh-CN.json +2 -0
- package/dist/node_modules/@ant-design/icons-svg/package.json +1 -1
- package/dist/node_modules/acorn/package.json +1 -1
- package/dist/node_modules/acorn-jsx/package.json +1 -1
- package/dist/node_modules/acorn-walk/package.json +1 -1
- package/dist/node_modules/ses/package.json +1 -1
- package/dist/node_modules/zod/package.json +1 -1
- package/dist/server/flow-surfaces/authoring-validation.d.ts +2 -1
- package/dist/server/flow-surfaces/authoring-validation.js +167 -19
- package/dist/server/flow-surfaces/blueprint/compile-blocks.js +7 -3
- package/dist/server/flow-surfaces/blueprint/compile-plan.js +19 -15
- package/dist/server/flow-surfaces/blueprint/normalize-document.js +18 -2
- package/dist/server/flow-surfaces/blueprint/public-types.d.ts +1 -0
- package/dist/server/flow-surfaces/builder.js +3 -0
- package/dist/server/flow-surfaces/constants.d.ts +9 -2
- package/dist/server/flow-surfaces/constants.js +1 -0
- package/dist/server/flow-surfaces/contract-guard.js +20 -10
- package/dist/server/flow-surfaces/errors.d.ts +2 -2
- package/dist/server/flow-surfaces/errors.js +4 -4
- package/dist/server/flow-surfaces/navigation-targets.d.ts +80 -0
- package/dist/server/flow-surfaces/navigation-targets.js +546 -0
- package/dist/server/flow-surfaces/runjs-authoring/runtime/constants.js +1 -0
- package/dist/server/flow-surfaces/service.d.ts +21 -2
- package/dist/server/flow-surfaces/service.js +610 -121
- package/dist/server/variables/records.js +7 -2
- package/dist/swagger/flow-surfaces.d.ts +96 -0
- package/dist/swagger/flow-surfaces.js +88 -7
- package/dist/swagger/index.d.ts +96 -0
- package/package.json +2 -2
|
@@ -37,7 +37,7 @@ export declare class FlowSurfaceBadRequestError extends FlowSurfaceError {
|
|
|
37
37
|
constructor(message: string, code?: string, options?: FlowSurfaceErrorOptions);
|
|
38
38
|
}
|
|
39
39
|
export declare class FlowSurfaceForbiddenError extends FlowSurfaceError {
|
|
40
|
-
constructor(message: string, code?: string);
|
|
40
|
+
constructor(message: string, code?: string, options?: FlowSurfaceErrorOptions);
|
|
41
41
|
}
|
|
42
42
|
export declare class FlowSurfaceConflictError extends FlowSurfaceError {
|
|
43
43
|
constructor(message: string, code?: string);
|
|
@@ -76,6 +76,6 @@ export declare function toFlowSurfaceErrorItem(error: FlowSurfaceError): FlowSur
|
|
|
76
76
|
export declare function normalizeFlowSurfaceError(error: unknown): FlowSurfaceError;
|
|
77
77
|
export declare function throwBadRequest(message: string, codeOrOptions?: string | FlowSurfaceErrorOptions, options?: FlowSurfaceErrorOptions): never;
|
|
78
78
|
export declare function throwAggregateBadRequest(errors: FlowSurfaceErrorItemInput[]): never;
|
|
79
|
-
export declare function throwForbidden(message: string, code?: string): never;
|
|
79
|
+
export declare function throwForbidden(message: string, code?: string, options?: FlowSurfaceErrorOptions): never;
|
|
80
80
|
export declare function throwConflict(message: string, code?: string): never;
|
|
81
81
|
export declare function throwInternalError(message: string, code?: string): never;
|
|
@@ -69,8 +69,8 @@ class FlowSurfaceBadRequestError extends FlowSurfaceError {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
class FlowSurfaceForbiddenError extends FlowSurfaceError {
|
|
72
|
-
constructor(message, code = "FLOW_SURFACE_FORBIDDEN") {
|
|
73
|
-
super(message, 403, "forbidden", code);
|
|
72
|
+
constructor(message, code = "FLOW_SURFACE_FORBIDDEN", options = {}) {
|
|
73
|
+
super(message, 403, "forbidden", code, options);
|
|
74
74
|
this.name = "FlowSurfaceForbiddenError";
|
|
75
75
|
}
|
|
76
76
|
}
|
|
@@ -159,8 +159,8 @@ function throwBadRequest(message, codeOrOptions, options = {}) {
|
|
|
159
159
|
function throwAggregateBadRequest(errors) {
|
|
160
160
|
throw new FlowSurfaceAggregateError(errors);
|
|
161
161
|
}
|
|
162
|
-
function throwForbidden(message, code) {
|
|
163
|
-
throw new FlowSurfaceForbiddenError(message, code);
|
|
162
|
+
function throwForbidden(message, code, options = {}) {
|
|
163
|
+
throw new FlowSurfaceForbiddenError(message, code, options);
|
|
164
164
|
}
|
|
165
165
|
function throwConflict(message, code) {
|
|
166
166
|
throw new FlowSurfaceConflictError(message, code);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
import type { Database, Transaction } from '@nocobase/database';
|
|
10
|
+
export declare const DEFAULT_ADMIN_UI_LAYOUT_UID = "admin-layout-model";
|
|
11
|
+
export declare const DEFAULT_MOBILE_UI_LAYOUT_UID = "mobile-layout-model";
|
|
12
|
+
export declare const DEFAULT_ADMIN_MULTI_PORTAL_UID = "__default_admin__";
|
|
13
|
+
export declare const DEFAULT_MOBILE_MULTI_PORTAL_UID = "__default_mobile__";
|
|
14
|
+
export type FlowSurfaceNavigationRequestRoles = readonly string[] | string;
|
|
15
|
+
type FlowSurfacePortalRouteScopeKind = 'layout' | 'portal';
|
|
16
|
+
export type FlowSurfaceResolvedMultiPortal = {
|
|
17
|
+
uid: string;
|
|
18
|
+
title: string;
|
|
19
|
+
icon?: string | null;
|
|
20
|
+
routeName?: string;
|
|
21
|
+
routePath?: string;
|
|
22
|
+
authCheck?: boolean;
|
|
23
|
+
enabled: true;
|
|
24
|
+
layoutUid: string;
|
|
25
|
+
layoutType?: string;
|
|
26
|
+
portalType: 'no-code';
|
|
27
|
+
routeScopeKind: FlowSurfacePortalRouteScopeKind;
|
|
28
|
+
};
|
|
29
|
+
export type FlowSurfaceNavigationTarget = {
|
|
30
|
+
kind: 'layout' | 'portal';
|
|
31
|
+
uid: string;
|
|
32
|
+
title: string;
|
|
33
|
+
icon?: string | null;
|
|
34
|
+
layoutUid: string;
|
|
35
|
+
layoutType?: string;
|
|
36
|
+
routeName?: string;
|
|
37
|
+
routePath?: string;
|
|
38
|
+
authCheck?: boolean;
|
|
39
|
+
default?: boolean;
|
|
40
|
+
portalUid?: string;
|
|
41
|
+
};
|
|
42
|
+
export type FlowSurfaceNavigationTargetsResult = {
|
|
43
|
+
version: '1';
|
|
44
|
+
capabilities: {
|
|
45
|
+
multiPortal: boolean;
|
|
46
|
+
};
|
|
47
|
+
targets: FlowSurfaceNavigationTarget[];
|
|
48
|
+
};
|
|
49
|
+
type PortalResolveOptions = {
|
|
50
|
+
actionName: string;
|
|
51
|
+
path: string;
|
|
52
|
+
currentRoles?: FlowSurfaceNavigationRequestRoles;
|
|
53
|
+
transaction?: Transaction;
|
|
54
|
+
};
|
|
55
|
+
export declare class FlowSurfaceNavigationTargetsService {
|
|
56
|
+
private readonly db;
|
|
57
|
+
constructor(db: Database);
|
|
58
|
+
hasMultiPortalCapability(): boolean;
|
|
59
|
+
hasUiLayoutCapability(): boolean;
|
|
60
|
+
normalizePortalUid(value: unknown): string;
|
|
61
|
+
listNavigationTargets(currentRoles?: FlowSurfaceNavigationRequestRoles, transaction?: Transaction): Promise<FlowSurfaceNavigationTargetsResult>;
|
|
62
|
+
resolveDefaultPortal(options: Omit<PortalResolveOptions, 'path'>): Promise<FlowSurfaceResolvedMultiPortal>;
|
|
63
|
+
resolvePortal(portalUidValue: unknown, options: PortalResolveOptions): Promise<FlowSurfaceResolvedMultiPortal>;
|
|
64
|
+
readRoutePortalUids(routeId: unknown, transaction?: Transaction): Promise<string[]>;
|
|
65
|
+
assertRouteBelongsToPortal(actionName: string, routeId: unknown, portalUid: string, path: string, transaction?: Transaction): Promise<void>;
|
|
66
|
+
attachRouteTreeToPortals(routeId: unknown, portalUids: string[], options: {
|
|
67
|
+
portalOnly: boolean;
|
|
68
|
+
transaction?: Transaction;
|
|
69
|
+
}): Promise<void>;
|
|
70
|
+
private listLayoutTargets;
|
|
71
|
+
private getEnabledPortalInventory;
|
|
72
|
+
private listAccessiblePortalTargets;
|
|
73
|
+
private comparePortalPriority;
|
|
74
|
+
private getPortalPriority;
|
|
75
|
+
private canAccessPortal;
|
|
76
|
+
private collectRouteTreeIds;
|
|
77
|
+
private removeDefaultLayoutRoutePermissions;
|
|
78
|
+
private grantDefaultPortalRouteAccess;
|
|
79
|
+
}
|
|
80
|
+
export {};
|
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
var __defProp = Object.defineProperty;
|
|
11
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
12
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __export = (target, all) => {
|
|
15
|
+
for (var name in all)
|
|
16
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
17
|
+
};
|
|
18
|
+
var __copyProps = (to, from, except, desc) => {
|
|
19
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
20
|
+
for (let key of __getOwnPropNames(from))
|
|
21
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
22
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
23
|
+
}
|
|
24
|
+
return to;
|
|
25
|
+
};
|
|
26
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
27
|
+
var navigation_targets_exports = {};
|
|
28
|
+
__export(navigation_targets_exports, {
|
|
29
|
+
DEFAULT_ADMIN_MULTI_PORTAL_UID: () => DEFAULT_ADMIN_MULTI_PORTAL_UID,
|
|
30
|
+
DEFAULT_ADMIN_UI_LAYOUT_UID: () => DEFAULT_ADMIN_UI_LAYOUT_UID,
|
|
31
|
+
DEFAULT_MOBILE_MULTI_PORTAL_UID: () => DEFAULT_MOBILE_MULTI_PORTAL_UID,
|
|
32
|
+
DEFAULT_MOBILE_UI_LAYOUT_UID: () => DEFAULT_MOBILE_UI_LAYOUT_UID,
|
|
33
|
+
FlowSurfaceNavigationTargetsService: () => FlowSurfaceNavigationTargetsService
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(navigation_targets_exports);
|
|
36
|
+
var import_errors = require("./errors");
|
|
37
|
+
const DEFAULT_ADMIN_UI_LAYOUT_UID = "admin-layout-model";
|
|
38
|
+
const DEFAULT_MOBILE_UI_LAYOUT_UID = "mobile-layout-model";
|
|
39
|
+
const DEFAULT_ADMIN_MULTI_PORTAL_UID = "__default_admin__";
|
|
40
|
+
const DEFAULT_MOBILE_MULTI_PORTAL_UID = "__default_mobile__";
|
|
41
|
+
function readRecordField(record, field) {
|
|
42
|
+
if (!record || typeof record !== "object") {
|
|
43
|
+
return void 0;
|
|
44
|
+
}
|
|
45
|
+
const model = record;
|
|
46
|
+
if (typeof model.get === "function") {
|
|
47
|
+
return model.get(field);
|
|
48
|
+
}
|
|
49
|
+
return record[field];
|
|
50
|
+
}
|
|
51
|
+
function readStringField(record, field) {
|
|
52
|
+
const value = readRecordField(record, field);
|
|
53
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
54
|
+
}
|
|
55
|
+
function normalizeRoles(currentRoles) {
|
|
56
|
+
const roles = Array.isArray(currentRoles) ? currentRoles : typeof currentRoles === "string" ? [currentRoles] : [];
|
|
57
|
+
return roles.map((role) => String(role || "").trim()).filter(Boolean);
|
|
58
|
+
}
|
|
59
|
+
function normalizeRelationRouteId(routeId) {
|
|
60
|
+
return routeId === null || typeof routeId === "undefined" ? void 0 : routeId;
|
|
61
|
+
}
|
|
62
|
+
function isDefaultLayoutMultiPortalUid(uid) {
|
|
63
|
+
return uid === DEFAULT_ADMIN_MULTI_PORTAL_UID || uid === DEFAULT_MOBILE_MULTI_PORTAL_UID;
|
|
64
|
+
}
|
|
65
|
+
class FlowSurfaceNavigationTargetsService {
|
|
66
|
+
constructor(db) {
|
|
67
|
+
this.db = db;
|
|
68
|
+
}
|
|
69
|
+
hasMultiPortalCapability() {
|
|
70
|
+
var _a, _b;
|
|
71
|
+
try {
|
|
72
|
+
return !!this.db.getCollection("multiPortals") && !!((_b = (_a = this.db.getCollection("desktopRoutes")) == null ? void 0 : _a.getField) == null ? void 0 : _b.call(_a, "multiPortals"));
|
|
73
|
+
} catch (error) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
hasUiLayoutCapability() {
|
|
78
|
+
var _a, _b;
|
|
79
|
+
try {
|
|
80
|
+
return !!this.db.getCollection("uiLayouts") && !!((_b = (_a = this.db.getCollection("desktopRoutes")) == null ? void 0 : _a.getField) == null ? void 0 : _b.call(_a, "uiLayouts"));
|
|
81
|
+
} catch (error) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
normalizePortalUid(value) {
|
|
86
|
+
const portalUid = String(value || "").trim();
|
|
87
|
+
return portalUid || void 0;
|
|
88
|
+
}
|
|
89
|
+
async listNavigationTargets(currentRoles, transaction) {
|
|
90
|
+
var _a;
|
|
91
|
+
let layoutTargets = await this.listLayoutTargets(transaction);
|
|
92
|
+
const multiPortal = this.hasMultiPortalCapability();
|
|
93
|
+
const portalInventory = multiPortal ? await this.getEnabledPortalInventory(currentRoles, transaction) : { accessible: [] };
|
|
94
|
+
const portalTargets = multiPortal ? await this.listAccessiblePortalTargets(portalInventory.accessible, transaction) : [];
|
|
95
|
+
if (multiPortal) {
|
|
96
|
+
layoutTargets = layoutTargets.map(({ default: _default, ...target }) => target);
|
|
97
|
+
if (portalInventory.accessible.length === 1 && ((_a = portalTargets[0]) == null ? void 0 : _a.uid) === readStringField(portalInventory.accessible[0], "uid")) {
|
|
98
|
+
portalTargets[0] = { ...portalTargets[0], default: true };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
version: "1",
|
|
103
|
+
capabilities: {
|
|
104
|
+
multiPortal
|
|
105
|
+
},
|
|
106
|
+
targets: [...layoutTargets, ...portalTargets]
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
async resolveDefaultPortal(options) {
|
|
110
|
+
if (!this.hasMultiPortalCapability()) {
|
|
111
|
+
(0, import_errors.throwBadRequest)(`flowSurfaces ${options.actionName} requires the Multi-portal capability`, {
|
|
112
|
+
ruleId: "navigation-portal-unsupported",
|
|
113
|
+
path: "navigation"
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const inventory = await this.getEnabledPortalInventory(options.currentRoles, options.transaction);
|
|
117
|
+
if (!inventory.enabled.length) {
|
|
118
|
+
(0, import_errors.throwBadRequest)(`flowSurfaces ${options.actionName} requires an enabled portal`, {
|
|
119
|
+
ruleId: "navigation-portal-not-found",
|
|
120
|
+
path: "navigation",
|
|
121
|
+
details: {
|
|
122
|
+
uiBuilderAllowed: false,
|
|
123
|
+
adminLayoutFallbackAllowed: false,
|
|
124
|
+
agentInstruction: "Stop this Flow Surfaces write. Ask the user to create and enable a Portal before retrying; do not fall back to the Admin layout."
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (!inventory.accessible.length) {
|
|
129
|
+
(0, import_errors.throwForbidden)(
|
|
130
|
+
`flowSurfaces ${options.actionName} current roles cannot access an enabled portal`,
|
|
131
|
+
"FLOW_SURFACE_NAVIGATION_PORTAL_FORBIDDEN",
|
|
132
|
+
{
|
|
133
|
+
ruleId: "navigation-portal-forbidden",
|
|
134
|
+
path: "navigation"
|
|
135
|
+
}
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (inventory.accessible.length > 1) {
|
|
139
|
+
(0, import_errors.throwBadRequest)(`flowSurfaces ${options.actionName} requires an explicit portal selection`, {
|
|
140
|
+
ruleId: "navigation-portal-selection-required",
|
|
141
|
+
path: "navigation",
|
|
142
|
+
details: {
|
|
143
|
+
candidates: inventory.accessible.map((portal) => {
|
|
144
|
+
const uid = readStringField(portal, "uid");
|
|
145
|
+
return {
|
|
146
|
+
uid,
|
|
147
|
+
portalName: readStringField(portal, "portalName") || null,
|
|
148
|
+
title: readStringField(portal, "title") || uid,
|
|
149
|
+
portalType: readStringField(portal, "portalType") || null
|
|
150
|
+
};
|
|
151
|
+
}),
|
|
152
|
+
uiBuilderAllowed: false,
|
|
153
|
+
adminLayoutFallbackAllowed: false,
|
|
154
|
+
agentInstruction: "Stop this Flow Surfaces write and ask the user to select one candidate before retrying; do not choose by Portal type or fall back to the Admin layout."
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return this.resolvePortal(readStringField(inventory.accessible[0], "uid"), {
|
|
159
|
+
...options,
|
|
160
|
+
path: "navigation"
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
async resolvePortal(portalUidValue, options) {
|
|
164
|
+
const portalUid = this.normalizePortalUid(portalUidValue);
|
|
165
|
+
if (!portalUid) {
|
|
166
|
+
(0, import_errors.throwBadRequest)(`flowSurfaces ${options.actionName} ${options.path} must be a non-empty string`, {
|
|
167
|
+
ruleId: "navigation-portal-invalid",
|
|
168
|
+
path: options.path
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if (!this.hasMultiPortalCapability()) {
|
|
172
|
+
(0, import_errors.throwBadRequest)(`flowSurfaces ${options.actionName} ${options.path} requires the Multi-portal capability`, {
|
|
173
|
+
ruleId: "navigation-portal-unsupported",
|
|
174
|
+
path: options.path,
|
|
175
|
+
details: { portalUid }
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
const portal = await this.db.getRepository("multiPortals").findOne({
|
|
179
|
+
filter: { uid: portalUid },
|
|
180
|
+
fields: ["uid", "title", "icon", "portalType", "portalName", "routePath", "authCheck", "enabled", "uiLayoutUid"],
|
|
181
|
+
transaction: options.transaction
|
|
182
|
+
});
|
|
183
|
+
if (!portal) {
|
|
184
|
+
(0, import_errors.throwBadRequest)(`flowSurfaces ${options.actionName} ${options.path} references missing portal '${portalUid}'`, {
|
|
185
|
+
ruleId: "navigation-portal-not-found",
|
|
186
|
+
path: options.path,
|
|
187
|
+
details: { portalUid }
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
const routeScopeKind = isDefaultLayoutMultiPortalUid(portalUid) ? "layout" : "portal";
|
|
191
|
+
if (routeScopeKind === "portal" && !await this.canAccessPortal(portalUid, options.currentRoles, options.transaction)) {
|
|
192
|
+
(0, import_errors.throwForbidden)(
|
|
193
|
+
`flowSurfaces ${options.actionName} current roles cannot access portal '${portalUid}'`,
|
|
194
|
+
"FLOW_SURFACE_NAVIGATION_PORTAL_FORBIDDEN",
|
|
195
|
+
{
|
|
196
|
+
ruleId: "navigation-portal-forbidden",
|
|
197
|
+
path: options.path,
|
|
198
|
+
details: { portalUid }
|
|
199
|
+
}
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
if (readRecordField(portal, "enabled") !== true) {
|
|
203
|
+
(0, import_errors.throwBadRequest)(`flowSurfaces ${options.actionName} portal '${portalUid}' is disabled`, {
|
|
204
|
+
ruleId: "navigation-portal-disabled",
|
|
205
|
+
path: options.path,
|
|
206
|
+
details: { portalUid }
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
const portalType = readStringField(portal, "portalType");
|
|
210
|
+
if (portalType !== "no-code") {
|
|
211
|
+
if (portalType === "ai") {
|
|
212
|
+
(0, import_errors.throwBadRequest)(
|
|
213
|
+
`flowSurfaces ${options.actionName} cannot use UI Builder or Flow Surfaces for AI Portal '${portalUid}'; stop and implement the request in the Portal source code`,
|
|
214
|
+
{
|
|
215
|
+
ruleId: "navigation-portal-type-unsupported",
|
|
216
|
+
path: options.path,
|
|
217
|
+
details: {
|
|
218
|
+
portalUid,
|
|
219
|
+
portalType,
|
|
220
|
+
portalName: readStringField(portal, "portalName") || null,
|
|
221
|
+
expectedPortalType: "no-code",
|
|
222
|
+
uiBuilderAllowed: false,
|
|
223
|
+
adminLayoutFallbackAllowed: false,
|
|
224
|
+
implementationPath: "ai-portal-source",
|
|
225
|
+
agentInstruction: "Stop this Flow Surfaces write. Run nb portal info <portalName> -j to resolve localPath, then implement the request in that AI Portal source code. Do not retry with an Admin layout."
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
(0, import_errors.throwBadRequest)(`flowSurfaces ${options.actionName} portal '${portalUid}' does not support no-code routes`, {
|
|
231
|
+
ruleId: "navigation-portal-type-unsupported",
|
|
232
|
+
path: options.path,
|
|
233
|
+
details: { portalUid, portalType: portalType || null }
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
const layoutUid = readStringField(portal, "uiLayoutUid");
|
|
237
|
+
if (!layoutUid || !this.db.getCollection("uiLayouts")) {
|
|
238
|
+
(0, import_errors.throwBadRequest)(`flowSurfaces ${options.actionName} portal '${portalUid}' has no available backing UI layout`, {
|
|
239
|
+
ruleId: "navigation-portal-layout-not-found",
|
|
240
|
+
path: options.path,
|
|
241
|
+
details: { portalUid, layoutUid: layoutUid || null }
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
const layout = await this.db.getRepository("uiLayouts").findOne({
|
|
245
|
+
filter: { uid: layoutUid },
|
|
246
|
+
fields: ["uid", "layoutType", "enabled"],
|
|
247
|
+
transaction: options.transaction
|
|
248
|
+
});
|
|
249
|
+
if (!layout) {
|
|
250
|
+
(0, import_errors.throwBadRequest)(
|
|
251
|
+
`flowSurfaces ${options.actionName} portal '${portalUid}' backing UI layout '${layoutUid}' does not exist`,
|
|
252
|
+
{
|
|
253
|
+
ruleId: "navigation-portal-layout-not-found",
|
|
254
|
+
path: options.path,
|
|
255
|
+
details: { portalUid, layoutUid }
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
if (readRecordField(layout, "enabled") !== true) {
|
|
260
|
+
(0, import_errors.throwBadRequest)(
|
|
261
|
+
`flowSurfaces ${options.actionName} portal '${portalUid}' backing UI layout '${layoutUid}' is disabled`,
|
|
262
|
+
{
|
|
263
|
+
ruleId: "navigation-portal-layout-disabled",
|
|
264
|
+
path: options.path,
|
|
265
|
+
details: { portalUid, layoutUid }
|
|
266
|
+
}
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
uid: portalUid,
|
|
271
|
+
title: readStringField(portal, "title") || portalUid,
|
|
272
|
+
icon: readStringField(portal, "icon") || null,
|
|
273
|
+
routeName: readStringField(portal, "portalName"),
|
|
274
|
+
routePath: readStringField(portal, "routePath"),
|
|
275
|
+
authCheck: readRecordField(portal, "authCheck") === true,
|
|
276
|
+
enabled: true,
|
|
277
|
+
layoutUid,
|
|
278
|
+
layoutType: readStringField(layout, "layoutType"),
|
|
279
|
+
portalType,
|
|
280
|
+
routeScopeKind
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
async readRoutePortalUids(routeId, transaction) {
|
|
284
|
+
const relationRouteId = normalizeRelationRouteId(routeId);
|
|
285
|
+
if (typeof relationRouteId === "undefined" || !this.hasMultiPortalCapability()) {
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
const portals = await this.db.getRepository("desktopRoutes.multiPortals", relationRouteId).find({ fields: ["uid"], transaction });
|
|
289
|
+
return Array.from(
|
|
290
|
+
new Set(
|
|
291
|
+
portals.map((portal) => readStringField(portal, "uid")).filter((portalUid) => !!portalUid)
|
|
292
|
+
)
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
async assertRouteBelongsToPortal(actionName, routeId, portalUid, path, transaction) {
|
|
296
|
+
const routePortalUids = await this.readRoutePortalUids(routeId, transaction);
|
|
297
|
+
if (routePortalUids.includes(portalUid)) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
(0, import_errors.throwBadRequest)(`flowSurfaces ${actionName} ${path} does not belong to portal '${portalUid}'`, {
|
|
301
|
+
ruleId: "navigation-route-portal-mismatch",
|
|
302
|
+
path,
|
|
303
|
+
details: {
|
|
304
|
+
routeId,
|
|
305
|
+
portalUid,
|
|
306
|
+
routePortalUids
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
async attachRouteTreeToPortals(routeId, portalUids, options) {
|
|
311
|
+
const normalizedPortalUids = Array.from(new Set(portalUids.map((uid) => String(uid || "").trim()).filter(Boolean)));
|
|
312
|
+
const relationRouteId = normalizeRelationRouteId(routeId);
|
|
313
|
+
if (typeof relationRouteId === "undefined" || !normalizedPortalUids.length || !this.hasMultiPortalCapability()) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const routeIds = await this.collectRouteTreeIds(relationRouteId, options.transaction);
|
|
317
|
+
for (const currentRouteId of routeIds) {
|
|
318
|
+
await this.db.getRepository("desktopRoutes.multiPortals", currentRouteId).set({ tk: normalizedPortalUids, transaction: options.transaction });
|
|
319
|
+
if (options.portalOnly && this.hasUiLayoutCapability()) {
|
|
320
|
+
await this.db.getRepository("desktopRoutes.uiLayouts", currentRouteId).set({ tk: [], transaction: options.transaction });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (options.portalOnly) {
|
|
324
|
+
await this.removeDefaultLayoutRoutePermissions(routeIds, options.transaction);
|
|
325
|
+
}
|
|
326
|
+
for (const portalUid of normalizedPortalUids) {
|
|
327
|
+
await this.grantDefaultPortalRouteAccess(portalUid, routeIds, options.transaction);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
async listLayoutTargets(transaction) {
|
|
331
|
+
const targets = [];
|
|
332
|
+
if (this.db.getCollection("uiLayouts")) {
|
|
333
|
+
const layouts = await this.db.getRepository("uiLayouts").find({
|
|
334
|
+
filter: { enabled: true },
|
|
335
|
+
fields: ["uid", "title", "layoutType", "routeName", "routePath", "authCheck"],
|
|
336
|
+
sort: ["uid"],
|
|
337
|
+
transaction
|
|
338
|
+
});
|
|
339
|
+
for (const layout of layouts) {
|
|
340
|
+
const layoutUid = readStringField(layout, "uid");
|
|
341
|
+
if (!layoutUid) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
targets.push({
|
|
345
|
+
kind: "layout",
|
|
346
|
+
uid: layoutUid,
|
|
347
|
+
title: readStringField(layout, "title") || layoutUid,
|
|
348
|
+
layoutUid,
|
|
349
|
+
layoutType: readStringField(layout, "layoutType"),
|
|
350
|
+
routeName: readStringField(layout, "routeName"),
|
|
351
|
+
routePath: readStringField(layout, "routePath"),
|
|
352
|
+
authCheck: readRecordField(layout, "authCheck") === true,
|
|
353
|
+
...layoutUid === DEFAULT_ADMIN_UI_LAYOUT_UID ? { default: true } : {}
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (!targets.some((target) => target.uid === DEFAULT_ADMIN_UI_LAYOUT_UID)) {
|
|
358
|
+
targets.unshift({
|
|
359
|
+
kind: "layout",
|
|
360
|
+
uid: DEFAULT_ADMIN_UI_LAYOUT_UID,
|
|
361
|
+
title: "Admin",
|
|
362
|
+
layoutUid: DEFAULT_ADMIN_UI_LAYOUT_UID,
|
|
363
|
+
layoutType: "desktop",
|
|
364
|
+
routeName: "admin",
|
|
365
|
+
routePath: "/admin",
|
|
366
|
+
authCheck: true,
|
|
367
|
+
default: true
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
return targets.sort((left, right) => {
|
|
371
|
+
if (left.default) {
|
|
372
|
+
return -1;
|
|
373
|
+
}
|
|
374
|
+
if (right.default) {
|
|
375
|
+
return 1;
|
|
376
|
+
}
|
|
377
|
+
return left.uid.localeCompare(right.uid);
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
async getEnabledPortalInventory(currentRoles, transaction) {
|
|
381
|
+
const enabled = await this.db.getRepository("multiPortals").find({
|
|
382
|
+
filter: { enabled: true },
|
|
383
|
+
fields: ["uid", "title", "icon", "portalType", "portalName", "routePath", "authCheck", "enabled", "uiLayoutUid"],
|
|
384
|
+
sort: ["uid"],
|
|
385
|
+
transaction
|
|
386
|
+
});
|
|
387
|
+
enabled.sort(
|
|
388
|
+
(left, right) => String(readStringField(left, "uid") || "").localeCompare(String(readStringField(right, "uid") || ""))
|
|
389
|
+
);
|
|
390
|
+
const roles = normalizeRoles(currentRoles);
|
|
391
|
+
const isRoot = roles.includes("root");
|
|
392
|
+
const accessiblePortalUids = /* @__PURE__ */ new Set();
|
|
393
|
+
if (!isRoot && roles.length && this.db.getCollection("rolesMultiPortals")) {
|
|
394
|
+
const grants = await this.db.getRepository("rolesMultiPortals").find({
|
|
395
|
+
filter: { roleName: roles },
|
|
396
|
+
fields: ["multiPortalUid"],
|
|
397
|
+
transaction
|
|
398
|
+
});
|
|
399
|
+
for (const grant of grants) {
|
|
400
|
+
const portalUid = readStringField(grant, "multiPortalUid");
|
|
401
|
+
if (portalUid) {
|
|
402
|
+
accessiblePortalUids.add(portalUid);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const accessible = enabled.filter((portal) => {
|
|
407
|
+
const portalUid = readStringField(portal, "uid");
|
|
408
|
+
return !!portalUid && (isRoot || isDefaultLayoutMultiPortalUid(portalUid) || accessiblePortalUids.has(portalUid));
|
|
409
|
+
});
|
|
410
|
+
return { enabled, accessible };
|
|
411
|
+
}
|
|
412
|
+
async listAccessiblePortalTargets(portals, transaction) {
|
|
413
|
+
const targets = [];
|
|
414
|
+
for (const portal of portals) {
|
|
415
|
+
const portalUid = readStringField(portal, "uid");
|
|
416
|
+
const layoutUid = readStringField(portal, "uiLayoutUid");
|
|
417
|
+
const portalType = readStringField(portal, "portalType");
|
|
418
|
+
if (!portalUid || portalType !== "no-code") {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (!layoutUid || !this.db.getCollection("uiLayouts")) {
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
const layout = await this.db.getRepository("uiLayouts").findOne({
|
|
425
|
+
filter: { uid: layoutUid, enabled: true },
|
|
426
|
+
fields: ["layoutType"],
|
|
427
|
+
transaction
|
|
428
|
+
});
|
|
429
|
+
if (!layout) {
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
targets.push({
|
|
433
|
+
kind: "portal",
|
|
434
|
+
uid: portalUid,
|
|
435
|
+
portalUid,
|
|
436
|
+
title: readStringField(portal, "title") || portalUid,
|
|
437
|
+
icon: readStringField(portal, "icon") || null,
|
|
438
|
+
layoutUid,
|
|
439
|
+
layoutType: readStringField(layout, "layoutType"),
|
|
440
|
+
routeName: readStringField(portal, "portalName"),
|
|
441
|
+
routePath: readStringField(portal, "routePath"),
|
|
442
|
+
authCheck: readRecordField(portal, "authCheck") === true
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
return targets.sort((left, right) => this.comparePortalPriority(left, right));
|
|
446
|
+
}
|
|
447
|
+
comparePortalPriority(left, right) {
|
|
448
|
+
const priorityDelta = this.getPortalPriority(left) - this.getPortalPriority(right);
|
|
449
|
+
if (priorityDelta) {
|
|
450
|
+
return priorityDelta;
|
|
451
|
+
}
|
|
452
|
+
return String(readStringField(left, "uid") || "").localeCompare(String(readStringField(right, "uid") || ""));
|
|
453
|
+
}
|
|
454
|
+
getPortalPriority(portal) {
|
|
455
|
+
const uiLayout = readRecordField(portal, "uiLayout");
|
|
456
|
+
const layoutType = readStringField(portal, "layoutType") || readStringField(uiLayout, "layoutType");
|
|
457
|
+
if (readStringField(portal, "uid") === DEFAULT_ADMIN_MULTI_PORTAL_UID) {
|
|
458
|
+
return 0;
|
|
459
|
+
}
|
|
460
|
+
if (layoutType !== "mobile") {
|
|
461
|
+
return 1;
|
|
462
|
+
}
|
|
463
|
+
return 2;
|
|
464
|
+
}
|
|
465
|
+
async canAccessPortal(portalUid, currentRoles, transaction) {
|
|
466
|
+
const roles = normalizeRoles(currentRoles);
|
|
467
|
+
if (roles.includes("root")) {
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
if (!roles.length || !this.db.getCollection("rolesMultiPortals")) {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
const count = await this.db.getRepository("rolesMultiPortals").count({
|
|
474
|
+
filter: {
|
|
475
|
+
roleName: roles,
|
|
476
|
+
multiPortalUid: portalUid
|
|
477
|
+
},
|
|
478
|
+
transaction
|
|
479
|
+
});
|
|
480
|
+
return count > 0;
|
|
481
|
+
}
|
|
482
|
+
async collectRouteTreeIds(routeId, transaction) {
|
|
483
|
+
const routeIds = [routeId];
|
|
484
|
+
const children = await this.db.getRepository("desktopRoutes").find({
|
|
485
|
+
fields: ["id"],
|
|
486
|
+
filter: { parentId: routeId },
|
|
487
|
+
transaction
|
|
488
|
+
});
|
|
489
|
+
for (const child of children) {
|
|
490
|
+
const childId = normalizeRelationRouteId(readRecordField(child, "id"));
|
|
491
|
+
if (typeof childId !== "undefined") {
|
|
492
|
+
routeIds.push(...await this.collectRouteTreeIds(childId, transaction));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return routeIds;
|
|
496
|
+
}
|
|
497
|
+
async removeDefaultLayoutRoutePermissions(routeIds, transaction) {
|
|
498
|
+
if (!routeIds.length || !this.db.getCollection("rolesDesktopRoutes")) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
await this.db.getRepository("rolesDesktopRoutes").destroy({
|
|
502
|
+
filter: { desktopRouteId: routeIds },
|
|
503
|
+
transaction
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
async grantDefaultPortalRouteAccess(portalUid, routeIds, transaction) {
|
|
507
|
+
if (!routeIds.length || !this.db.getCollection("rolesMultiPortalRoutePolicies") || !this.db.getCollection("rolesMultiPortalDesktopRoutes")) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
const policies = await this.db.getRepository("rolesMultiPortalRoutePolicies").find({
|
|
511
|
+
fields: ["roleName"],
|
|
512
|
+
filter: {
|
|
513
|
+
multiPortalUid: portalUid,
|
|
514
|
+
allowNewMenu: true
|
|
515
|
+
},
|
|
516
|
+
transaction
|
|
517
|
+
});
|
|
518
|
+
const roleNames = Array.from(
|
|
519
|
+
new Set(
|
|
520
|
+
policies.map((policy) => readStringField(policy, "roleName")).filter((roleName) => !!roleName)
|
|
521
|
+
)
|
|
522
|
+
);
|
|
523
|
+
const repository = this.db.getRepository("rolesMultiPortalDesktopRoutes");
|
|
524
|
+
for (const roleName of roleNames) {
|
|
525
|
+
for (const desktopRouteId of routeIds) {
|
|
526
|
+
await repository.firstOrCreate({
|
|
527
|
+
filterKeys: ["roleName", "multiPortalUid", "desktopRouteId"],
|
|
528
|
+
values: {
|
|
529
|
+
roleName,
|
|
530
|
+
multiPortalUid: portalUid,
|
|
531
|
+
desktopRouteId
|
|
532
|
+
},
|
|
533
|
+
transaction
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
540
|
+
0 && (module.exports = {
|
|
541
|
+
DEFAULT_ADMIN_MULTI_PORTAL_UID,
|
|
542
|
+
DEFAULT_ADMIN_UI_LAYOUT_UID,
|
|
543
|
+
DEFAULT_MOBILE_MULTI_PORTAL_UID,
|
|
544
|
+
DEFAULT_MOBILE_UI_LAYOUT_UID,
|
|
545
|
+
FlowSurfaceNavigationTargetsService
|
|
546
|
+
});
|