@nocobase/plugin-multi-portal 2.2.0-alpha.11 → 2.4.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.
Files changed (38) hide show
  1. package/README.md +14 -6
  2. package/dist/client-v2/354.87f18462949b5c23.js +10 -0
  3. package/dist/client-v2/499.245037eea4cc7266.js +10 -0
  4. package/dist/client-v2/556.443e73c9ca4c18b9.js +10 -0
  5. package/dist/client-v2/778.affaa69b23857ef2.js +10 -0
  6. package/dist/client-v2/993.7a1cc7aa6cec0160.js +10 -0
  7. package/dist/client-v2/PortalAccessBoundary.d.ts +49 -0
  8. package/dist/client-v2/RootLanding.d.ts +18 -0
  9. package/dist/client-v2/index.js +1 -1
  10. package/dist/client-v2/interceptor.d.ts +13 -0
  11. package/dist/client-v2/layoutRegistration.d.ts +5 -7
  12. package/dist/client-v2/models/MultiPortalLayoutModels.d.ts +14 -0
  13. package/dist/client-v2/models/MultiPortalMobilePageModels.d.ts +5 -11
  14. package/dist/client-v2/pages/MultiPortalsPage.d.ts +13 -6
  15. package/dist/client-v2/pages/PortalRoutesDrawer.d.ts +13 -0
  16. package/dist/client-v2/permissions/MultiPortalPermissionsTab.d.ts +1 -0
  17. package/dist/client-v2/portalAccess.d.ts +73 -0
  18. package/dist/client-v2/routeRepositoryScope.d.ts +5 -2
  19. package/dist/client-v2/routeUrl.d.ts +1 -0
  20. package/dist/constants.d.ts +19 -0
  21. package/dist/constants.js +76 -0
  22. package/dist/externalVersion.js +11 -9
  23. package/dist/locale/en-US.json +93 -11
  24. package/dist/locale/zh-CN.json +113 -31
  25. package/dist/node_modules/tar/package.json +1 -1
  26. package/dist/server/collections/desktopRoutes.js +1 -0
  27. package/dist/server/collections/multiPortals.js +12 -11
  28. package/dist/server/migrations/20260730120000-fix-fixed-layout-portal-type.d.ts +13 -0
  29. package/dist/server/migrations/20260730120000-fix-fixed-layout-portal-type.js +73 -0
  30. package/dist/server/plugin.d.ts +7 -5
  31. package/dist/server/plugin.js +1415 -502
  32. package/dist/swagger/index.d.ts +152 -0
  33. package/dist/swagger/index.js +142 -0
  34. package/package.json +6 -7
  35. package/dist/client-v2/354.ead3e96143164cd5.js +0 -10
  36. package/dist/client-v2/499.ef2ff0c022687485.js +0 -10
  37. package/dist/client-v2/778.23ba409a924ee34a.js +0 -10
  38. package/dist/client-v2/845.f2b0ce44ae4be707.js +0 -10
@@ -41,6 +41,7 @@ __export(plugin_exports, {
41
41
  });
42
42
  module.exports = __toCommonJS(plugin_exports);
43
43
  var import_server = require("@nocobase/server");
44
+ var import_database = require("@nocobase/database");
44
45
  var import_utils = require("@nocobase/utils");
45
46
  var import_child_process = require("child_process");
46
47
  var import_fs = __toESM(require("fs"));
@@ -51,6 +52,7 @@ var import_promises = require("stream/promises");
51
52
  var tar = __toESM(require("tar"));
52
53
  var import_zlib = require("zlib");
53
54
  var import_ensureDefaultRoleMultiPortalAccess = require("./ensureDefaultRoleMultiPortalAccess");
55
+ var import_constants = require("../constants");
54
56
  const MULTI_PORTAL_RUNTIME_FIELDS = [
55
57
  "uid",
56
58
  "title",
@@ -58,9 +60,9 @@ const MULTI_PORTAL_RUNTIME_FIELDS = [
58
60
  "portalName",
59
61
  "routePath",
60
62
  "authCheck",
61
- "enabled"
63
+ "enabled",
64
+ "uiLayoutUid"
62
65
  ];
63
- const MULTI_PORTAL_RUNTIME_QUERY_FIELDS = [...MULTI_PORTAL_RUNTIME_FIELDS, "uiLayoutUid"];
64
66
  const MULTI_PORTAL_ACCESSIBLE_FIELDS = [
65
67
  "uid",
66
68
  "title",
@@ -69,18 +71,15 @@ const MULTI_PORTAL_ACCESSIBLE_FIELDS = [
69
71
  "portalName",
70
72
  "routePath",
71
73
  "authCheck",
72
- "enabled"
74
+ "enabled",
75
+ "uiLayoutUid"
73
76
  ];
74
- const MULTI_PORTAL_ACCESSIBLE_QUERY_FIELDS = [...MULTI_PORTAL_ACCESSIBLE_FIELDS, "uiLayoutUid"];
75
- const MULTI_PORTAL_UI_LAYOUT_RUNTIME_FIELDS = ["layoutType"];
76
77
  const DESKTOP_ROUTE_ROLE_PERMISSION_TARGET_FIELDS = ["id", "title", "hidden", "parentId", "options"];
77
78
  const UI_LAYOUT_DESKTOP_ROUTE_WRITE_LAYOUT_HANDLER_TAG = "plugin-ui-layout:desktop-route-write-layout";
78
79
  const MAIN_APP_NAME = "main";
79
80
  const UNION_ROLE_KEY = "__union__";
80
81
  const MULTI_PORTAL_MANIFEST_NAMESPACE = "multi-portal";
81
82
  const MULTI_PORTAL_MANIFEST_SYNC_MESSAGE_TYPE = "multi-portal:app-manifest-changed";
82
- const DEFAULT_INIT_PORTAL_TYPE = "no-code";
83
- const DEFAULT_INIT_PORTAL_NAME = "admin";
84
83
  const DEFAULT_INIT_PORTAL_TEMPLATE = "@nocobase/portal-template-default";
85
84
  const PORTAL_CLIENT_PREFIX = "x";
86
85
  const PORTAL_DEPLOY_UPLOAD_LIMIT = 200 * 1024 * 1024;
@@ -90,9 +89,9 @@ const PORTAL_PUBLIC_FILE_MODE = 420;
90
89
  const PORTAL_TEMPLATE_NPM_PACK_TIMEOUT_MS = 3e4;
91
90
  const DEFAULT_MULTI_PORTAL_UID = "__default_portal__";
92
91
  const MULTI_PORTAL_SLUG_PATTERN = /^[a-z0-9_-]+$/;
93
- const INIT_PORTAL_TYPES = ["no-code", "ai"];
94
- const DEFAULT_MULTI_PORTAL_UIDS = [DEFAULT_MULTI_PORTAL_UID];
95
- const DEFAULT_MULTI_PORTAL_UID_SET = new Set(DEFAULT_MULTI_PORTAL_UIDS);
92
+ const PORTAL_ACCESS_DENIED_CODE = "PORTAL_ACCESS_DENIED";
93
+ const PORTAL_CONTEXT_INVALID_CODE = "PORTAL_CONTEXT_INVALID";
94
+ const PORTAL_NOT_FOUND_CODE = "PORTAL_NOT_FOUND";
96
95
  const MULTI_PORTAL_MANAGEMENT_ACTIONS = [
97
96
  "multiPortals:list",
98
97
  "multiPortals:get",
@@ -100,17 +99,31 @@ const MULTI_PORTAL_MANAGEMENT_ACTIONS = [
100
99
  "multiPortals:create",
101
100
  "multiPortals:update",
102
101
  "multiPortals:firstOrCreate",
102
+ "multiPortals:setDefault",
103
103
  "multiPortals:destroy",
104
104
  "multiPortals:deploy",
105
105
  "multiPortals:pullSource",
106
- "multiPortals:pushSource"
106
+ "multiPortals:pushSource",
107
+ "desktopRoutes:list",
108
+ "desktopRoutes:get",
109
+ "desktopRoutes:create",
110
+ "desktopRoutes:update",
111
+ "desktopRoutes:move",
112
+ "desktopRoutes:destroy",
113
+ "desktopRoutes:updateOrCreate",
114
+ "registry:list",
115
+ "registry:get"
107
116
  ];
108
117
  const ROLE_MULTI_PORTAL_PERMISSION_ACTIONS = [
109
118
  "roles.multiPortals:*",
110
119
  "rolesMultiPortalDesktopRoutes:*",
111
120
  "rolesMultiPortalRoutePolicies:*"
112
121
  ];
113
- const EXPLICIT_DESKTOP_ROUTE_UI_LAYOUTS = Symbol("explicitDesktopRouteUiLayouts");
122
+ const MULTI_PORTAL_SEED_TYPES = ["no-code", "ai"];
123
+ const MULTI_PORTAL_SEED_TYPE_SET = new Set(MULTI_PORTAL_SEED_TYPES);
124
+ function isMultiPortalSeedType(value) {
125
+ return typeof value === "string" && MULTI_PORTAL_SEED_TYPE_SET.has(value);
126
+ }
114
127
  const SKIP_CREATE_PORTAL_DIRECTORY = Symbol("skipCreatePortalDirectory");
115
128
  function getRecordField(record, field) {
116
129
  if (!record || typeof record !== "object") {
@@ -136,44 +149,58 @@ async function pathExists(filePath) {
136
149
  function trimString(value) {
137
150
  return String(value ?? "").trim();
138
151
  }
139
- function isInitPortalType(value) {
140
- return INIT_PORTAL_TYPES.includes(value);
141
- }
142
- function getInitPortalType() {
143
- const portalType = trimString(process.env.INIT_PORTAL_TYPE) || DEFAULT_INIT_PORTAL_TYPE;
144
- if (!isInitPortalType(portalType)) {
145
- throw new Error('INIT_PORTAL_TYPE must be either "no-code" or "ai".');
146
- }
147
- return portalType;
148
- }
149
- function getInitPortalName() {
150
- const portalName = trimString(process.env.INIT_PORTAL_NAME) || DEFAULT_INIT_PORTAL_NAME;
151
- if (!MULTI_PORTAL_SLUG_PATTERN.test(portalName)) {
152
- throw new Error("INIT_PORTAL_NAME can only contain lowercase letters, numbers, hyphens, and underscores.");
153
- }
154
- return portalName;
155
- }
156
152
  function getInitPortalTemplate() {
157
153
  return trimString(process.env.INIT_PORTAL_TEMPLATE) || DEFAULT_INIT_PORTAL_TEMPLATE;
158
154
  }
159
- function formatInitPortalTitle(portalName) {
160
- const title = portalName.split(/[-_]/).filter(Boolean).map((segment) => `${segment.charAt(0).toUpperCase()}${segment.slice(1)}`).join(" ");
161
- return title || portalName;
155
+ function hasDeprecatedInitPortalEnv() {
156
+ return Boolean(trimString(process.env.INIT_PORTAL_TYPE) || trimString(process.env.INIT_PORTAL_NAME));
162
157
  }
163
- function getDefaultMultiPortalRecord() {
164
- const portalName = getInitPortalName();
158
+ function getDefaultAiMultiPortalRecord(options = {}) {
165
159
  return {
166
160
  uid: DEFAULT_MULTI_PORTAL_UID,
167
- title: formatInitPortalTitle(portalName),
161
+ title: "Main",
168
162
  icon: "DesktopOutlined",
169
- portalType: getInitPortalType(),
170
- portalName,
171
- routePath: `/${portalName}`,
163
+ portalType: "ai",
164
+ portalName: "main",
165
+ routePath: "/main",
172
166
  authCheck: true,
173
167
  enabled: true,
174
- uiLayoutUid: "admin-layout-model"
168
+ ...options.isDefault ? { isDefault: true } : {},
169
+ uiLayoutUid: import_constants.ADMIN_UI_LAYOUT_UID
175
170
  };
176
171
  }
172
+ function getFreshMultiPortalRecords() {
173
+ return [getDefaultAiMultiPortalRecord({ isDefault: true }), ...getFixedLayoutMultiPortalRecords()];
174
+ }
175
+ function getFixedLayoutMultiPortalRecords() {
176
+ return [
177
+ {
178
+ uid: import_constants.DEFAULT_ADMIN_MULTI_PORTAL_UID,
179
+ title: "Desktop layout",
180
+ icon: "DesktopOutlined",
181
+ portalType: "no-code",
182
+ portalName: "admin",
183
+ routePath: "/admin",
184
+ authCheck: true,
185
+ enabled: true,
186
+ uiLayoutUid: import_constants.ADMIN_UI_LAYOUT_UID
187
+ },
188
+ {
189
+ uid: import_constants.DEFAULT_MOBILE_MULTI_PORTAL_UID,
190
+ title: "Mobile layout",
191
+ icon: "MobileOutlined",
192
+ portalType: "no-code",
193
+ portalName: "mobile",
194
+ routePath: "/mobile",
195
+ authCheck: true,
196
+ enabled: true,
197
+ uiLayoutUid: import_constants.MOBILE_UI_LAYOUT_UID
198
+ }
199
+ ];
200
+ }
201
+ function getFixedLayoutMultiPortalRecord(uid) {
202
+ return getFixedLayoutMultiPortalRecords().find((portal) => portal.uid === uid);
203
+ }
177
204
  function isValidPortalDeploySegment(value) {
178
205
  return /^[A-Za-z0-9_-]+$/.test(value);
179
206
  }
@@ -252,6 +279,13 @@ function getPortalDeployBasePath(appName, portalName) {
252
279
  const portalPath = appName === MAIN_APP_NAME ? `/${PORTAL_CLIENT_PREFIX}/${portalName}/` : `/${PORTAL_CLIENT_PREFIX}/apps/${appName}/${portalName}/`;
253
280
  return resolvePortalStoragePublicPath(joinPortalStoragePublicPath(process.env.APP_PUBLIC_PATH || "/", portalPath));
254
281
  }
282
+ function getPortalDeployBasePathCandidates(appName, portalName) {
283
+ const expectedBasePaths = [getPortalDeployBasePath(appName, portalName)];
284
+ if (appName !== MAIN_APP_NAME) {
285
+ expectedBasePaths.push(getPortalDeployBasePath(MAIN_APP_NAME, portalName));
286
+ }
287
+ return expectedBasePaths;
288
+ }
255
289
  function createPortalDeployUploadMiddleware() {
256
290
  const storage = import_utils.koaMulter.diskStorage({
257
291
  destination: import_os.default.tmpdir(),
@@ -394,80 +428,13 @@ async function resolvePortalTemplate(templateSource, logPath) {
394
428
  return downloadPortalTemplatePackage(templateSource, logPath);
395
429
  }
396
430
  async function copyPortalTemplate(sourceDir, targetDir) {
397
- const ignoredSegments = /* @__PURE__ */ new Set([".git", "node_modules", ".DS_Store"]);
431
+ const ignoredSegments = /* @__PURE__ */ new Set([".git", "node_modules", ".DS_Store", ".env", ".env.local"]);
398
432
  await import_fs.default.promises.mkdir(import_path.default.dirname(targetDir), { recursive: true });
399
433
  await import_fs.default.promises.cp(sourceDir, targetDir, {
400
434
  recursive: true,
401
435
  filter: (source) => !import_path.default.relative(sourceDir, source).split(import_path.default.sep).some((segment) => segment.startsWith("._") || ignoredSegments.has(segment))
402
436
  });
403
437
  }
404
- function getPortalStorageConfig(options) {
405
- const sourceOptions = isRecordLike(options) ? options : {};
406
- const sourceStorage = trimString(sourceOptions.sourceStorage);
407
- if (sourceStorage !== "git") {
408
- return { sourceStorage: "nocobase" };
409
- }
410
- const gitOptions = isRecordLike(sourceOptions.git) ? sourceOptions.git : {};
411
- return {
412
- sourceStorage,
413
- git: {
414
- repo: trimString(gitOptions.repo),
415
- branch: trimString(gitOptions.branch) || "main",
416
- path: trimString(gitOptions.path) || "."
417
- }
418
- };
419
- }
420
- function upsertPortalEnvContent(content, values) {
421
- const nextValues = { ...values };
422
- const lines = content ? content.replace(/\r\n/g, "\n").split("\n") : [];
423
- const result = [];
424
- for (const line of lines) {
425
- if (!line && result.length === lines.length - 1) {
426
- continue;
427
- }
428
- const match = line.match(/^(\s*)([A-Za-z_][A-Za-z0-9_]*)\s*=/);
429
- const key = match == null ? void 0 : match[2];
430
- if (key && Object.prototype.hasOwnProperty.call(nextValues, key)) {
431
- result.push(`${key}=${nextValues[key]}`);
432
- delete nextValues[key];
433
- continue;
434
- }
435
- result.push(line);
436
- }
437
- for (const [key, value] of Object.entries(nextValues)) {
438
- result.push(`${key}=${value}`);
439
- }
440
- return `${result.join("\n").replace(/\n*$/, "")}
441
- `;
442
- }
443
- async function upsertPortalEnvFile(filePath, values) {
444
- let content = "";
445
- try {
446
- content = await import_fs.default.promises.readFile(filePath, "utf-8");
447
- } catch {
448
- content = "";
449
- }
450
- await import_fs.default.promises.writeFile(filePath, upsertPortalEnvContent(content, values), "utf-8");
451
- }
452
- async function ensurePortalStorageConfigFiles(portalDir, item) {
453
- const apiUrl = getPortalStorageApiUrl(item.appName);
454
- const portalBase = getPortalDeployBasePath(item.appName, item.portalName);
455
- await import_fs.default.promises.mkdir(portalDir, { recursive: true });
456
- await upsertPortalEnvFile(import_path.default.join(portalDir, ".env"), {
457
- NOCOBASE_API_URL: resolvePortalStorageEnvApiUrl(apiUrl),
458
- NOCOBASE_PORTAL_BASE: portalBase
459
- });
460
- await upsertPortalEnvFile(import_path.default.join(portalDir, ".env.local"), {
461
- NOCOBASE_API_URL: apiUrl,
462
- NOCOBASE_PORTAL_BASE: portalBase
463
- });
464
- await import_fs.default.promises.writeFile(
465
- import_path.default.join(portalDir, "portal.config.json"),
466
- `${JSON.stringify(item.config, null, 2)}
467
- `,
468
- "utf-8"
469
- );
470
- }
471
438
  function sanitizePortalStorageNodeOptions(value) {
472
439
  return trimString(value).split(/\s+/).filter((option) => option !== "--preserve-symlinks" && option !== "--preserve-symlinks-main").join(" ");
473
440
  }
@@ -552,16 +519,24 @@ async function runPortalStorageCommandOnce(command, args, options) {
552
519
  throw error;
553
520
  }
554
521
  }
555
- async function buildPortalStorageItem(portalDir, item) {
522
+ function hasRequestAppHeader(options) {
523
+ var _a;
524
+ return Boolean(trimString((_a = options == null ? void 0 : options.context) == null ? void 0 : _a.get("X-App")));
525
+ }
526
+ async function buildPortalStorageItem(portalDir, item, options) {
556
527
  const logPath = getPortalStorageLogPath(item);
528
+ const buildAppName = hasRequestAppHeader(options) ? item.appName : MAIN_APP_NAME;
557
529
  const buildEnv = getPortalStorageCommandEnv({
558
- NOCOBASE_API_URL: getPortalStorageApiUrl(item.appName),
559
- NOCOBASE_PORTAL_BASE: getPortalDeployBasePath(item.appName, item.portalName)
530
+ NOCOBASE_API_URL: getPortalStorageApiUrl(buildAppName),
531
+ NOCOBASE_PORTAL_BASE: getPortalDeployBasePath(buildAppName, item.portalName),
532
+ SKIP_YARN_COREPACK_CHECK: "1",
533
+ COREPACK_ENABLE_STRICT: "0",
534
+ COREPACK_ENABLE_PROJECT_SPEC: "0"
560
535
  });
561
536
  await appendPortalStorageLog(logPath, `Building portal ${item.appName}/${item.portalName}.`);
562
537
  await appendPortalStorageLog(
563
538
  logPath,
564
- `Build environment: NOCOBASE_API_URL=${buildEnv.NOCOBASE_API_URL || ""} NOCOBASE_PORTAL_BASE=${buildEnv.NOCOBASE_PORTAL_BASE || ""} APP_PUBLIC_PATH=${buildEnv.APP_PUBLIC_PATH || ""}`
539
+ `Build environment: NOCOBASE_API_URL=${buildEnv.NOCOBASE_API_URL || ""} NOCOBASE_PORTAL_BASE=${buildEnv.NOCOBASE_PORTAL_BASE || ""} APP_PUBLIC_PATH=${buildEnv.APP_PUBLIC_PATH || ""} SKIP_YARN_COREPACK_CHECK=${buildEnv.SKIP_YARN_COREPACK_CHECK || ""} COREPACK_ENABLE_STRICT=${buildEnv.COREPACK_ENABLE_STRICT || ""} COREPACK_ENABLE_PROJECT_SPEC=${buildEnv.COREPACK_ENABLE_PROJECT_SPEC || ""}`
565
540
  );
566
541
  await runPortalStorageCommandOnce("yarn", ["build:html"], {
567
542
  cwd: portalDir,
@@ -575,9 +550,9 @@ function validatePortalDeployBasePath(appName, portalName, basePath) {
575
550
  if (normalizedBasePath.includes("..")) {
576
551
  throw new Error('basePath cannot contain ".."');
577
552
  }
578
- const expectedBasePath = getPortalDeployBasePath(appName, portalName);
579
- if (normalizedBasePath !== expectedBasePath) {
580
- throw new Error(`basePath must be ${expectedBasePath}`);
553
+ const expectedBasePaths = getPortalDeployBasePathCandidates(appName, portalName);
554
+ if (!expectedBasePaths.includes(normalizedBasePath)) {
555
+ throw new Error(`basePath must be ${expectedBasePaths.join(" or ")}`);
581
556
  }
582
557
  }
583
558
  function isPortalDeployTarEntry(entry) {
@@ -790,29 +765,6 @@ async function replacePortalSourceFromArchive(params) {
790
765
  await import_fs.default.promises.rm(tarPath, { force: true });
791
766
  }
792
767
  }
793
- function isDefaultMultiPortalUid(uid) {
794
- return typeof uid === "string" && DEFAULT_MULTI_PORTAL_UID_SET.has(uid);
795
- }
796
- function getDefaultMultiPortalRecordForUid(uid) {
797
- return isDefaultMultiPortalUid(uid) ? getDefaultMultiPortalRecord() : void 0;
798
- }
799
- function getDefaultMultiPortalUiLayoutUid(portal) {
800
- return Object.prototype.hasOwnProperty.call(portal, "uiLayoutUid") ? portal.uiLayoutUid ?? null : null;
801
- }
802
- function withCustomMultiPortalFilter(filter = {}) {
803
- const customFilter = { ...filter };
804
- const uidFilter = customFilter.uid;
805
- if (Array.isArray(uidFilter)) {
806
- customFilter.uid = uidFilter.filter((uid) => !isDefaultMultiPortalUid(uid));
807
- return customFilter;
808
- }
809
- if (isDefaultMultiPortalUid(uidFilter)) {
810
- customFilter.uid = [];
811
- return customFilter;
812
- }
813
- customFilter["uid.$notIn"] = DEFAULT_MULTI_PORTAL_UIDS;
814
- return customFilter;
815
- }
816
768
  function uniqueDesktopRouteIds(routeIds) {
817
769
  const seen = /* @__PURE__ */ new Set();
818
770
  return routeIds.filter((routeId) => {
@@ -838,19 +790,11 @@ function collectDesktopRouteIds(record) {
838
790
  }
839
791
  return uniqueDesktopRouteIds(routeIds);
840
792
  }
841
- function pickMultiPortalUiLayoutRuntimeFields(record) {
842
- const result = {};
843
- for (const field of MULTI_PORTAL_UI_LAYOUT_RUNTIME_FIELDS) {
844
- result[field] = getRecordField(record, field);
845
- }
846
- return result;
847
- }
848
793
  function pickMultiPortalRuntimeFields(record) {
849
794
  const result = {};
850
795
  for (const field of MULTI_PORTAL_RUNTIME_FIELDS) {
851
796
  result[field] = getRecordField(record, field);
852
797
  }
853
- result.uiLayout = pickMultiPortalUiLayoutRuntimeFields(getRecordField(record, "uiLayout"));
854
798
  return result;
855
799
  }
856
800
  function pickMultiPortalAccessibleFields(record) {
@@ -858,7 +802,6 @@ function pickMultiPortalAccessibleFields(record) {
858
802
  for (const field of MULTI_PORTAL_ACCESSIBLE_FIELDS) {
859
803
  result[field] = getRecordField(record, field) ?? null;
860
804
  }
861
- result.uiLayout = pickMultiPortalUiLayoutRuntimeFields(getRecordField(record, "uiLayout"));
862
805
  return result;
863
806
  }
864
807
  function getExplicitRequestedLayoutUid(layout) {
@@ -867,14 +810,25 @@ function getExplicitRequestedLayoutUid(layout) {
867
810
  return uid;
868
811
  }
869
812
  }
813
+ function hasActionParam(params, key) {
814
+ return isRecordLike(params) && Object.prototype.hasOwnProperty.call(params, key);
815
+ }
870
816
  function getRequestedMultiPortalUid(ctx) {
871
- var _a, _b;
872
- const portalUid = getExplicitRequestedLayoutUid((_a = ctx.action) == null ? void 0 : _a.params.portal);
873
- const layoutUid = getExplicitRequestedLayoutUid((_b = ctx.action) == null ? void 0 : _b.params.layout);
874
- if (portalUid && layoutUid) {
817
+ var _a, _b, _c;
818
+ const hasPortalScope = hasActionParam((_a = ctx.action) == null ? void 0 : _a.params, "portal");
819
+ const hasLayoutScope = hasActionParam((_b = ctx.action) == null ? void 0 : _b.params, "layout");
820
+ const portalUid = getExplicitRequestedLayoutUid((_c = ctx.action) == null ? void 0 : _c.params.portal);
821
+ if (hasPortalScope && hasLayoutScope) {
875
822
  ctx.throw(400, "layout and portal cannot be used together");
876
823
  return;
877
824
  }
825
+ if (!hasPortalScope) {
826
+ return;
827
+ }
828
+ if (!portalUid) {
829
+ ctx.throw(400, "Invalid portal scope");
830
+ return;
831
+ }
878
832
  return portalUid;
879
833
  }
880
834
  function getCurrentRoles(ctx) {
@@ -899,27 +853,88 @@ function getMultiPortalNameFromValues(ctx) {
899
853
  return portalName;
900
854
  }
901
855
  }
902
- function getMultiPortalWriteValues(ctx) {
856
+ function getMultiPortalWriteRecords(ctx) {
903
857
  var _a;
904
858
  const values = (_a = ctx.action) == null ? void 0 : _a.params.values;
905
- if (!values || typeof values !== "object" || Array.isArray(values)) {
906
- return;
859
+ const records = Array.isArray(values) ? values : [values];
860
+ return records.filter(
861
+ (record) => !!record && typeof record === "object" && !Array.isArray(record)
862
+ );
863
+ }
864
+ async function getMultiPortalWriteTargets(ctx, fields) {
865
+ var _a, _b, _c, _d, _e;
866
+ const records = getMultiPortalWriteRecords(ctx);
867
+ if (!records.length || ((_a = ctx.action) == null ? void 0 : _a.actionName) === "create") {
868
+ return records.map((values2) => ({ values: values2 }));
869
+ }
870
+ const repository = ctx.db.getRepository("multiPortals");
871
+ const actionName = (_b = ctx.action) == null ? void 0 : _b.actionName;
872
+ const requestedFilterKeys = (_c = ctx.action) == null ? void 0 : _c.params.filterKeys;
873
+ if ((actionName === "firstOrCreate" || actionName === "updateOrCreate") && Array.isArray(requestedFilterKeys) && requestedFilterKeys.every((filterKey) => typeof filterKey === "string")) {
874
+ const targets = [];
875
+ for (const values2 of records) {
876
+ const existing2 = await repository.findOne({
877
+ filter: import_database.Repository.valuesToFilter(values2, requestedFilterKeys),
878
+ fields
879
+ });
880
+ targets.push({ existing: existing2, values: values2 });
881
+ }
882
+ return targets;
883
+ }
884
+ if (records.length > 1) {
885
+ const targets = [];
886
+ for (const values2 of records) {
887
+ const uid2 = values2.uid;
888
+ const existing2 = typeof uid2 === "string" && uid2 ? await repository.findOne({
889
+ filterByTk: uid2,
890
+ fields
891
+ }) : null;
892
+ targets.push({ existing: existing2, values: values2 });
893
+ }
894
+ return targets;
895
+ }
896
+ const values = records[0];
897
+ const filterByTk = (_d = ctx.action) == null ? void 0 : _d.params.filterByTk;
898
+ if (filterByTk !== void 0 && filterByTk !== null) {
899
+ const existingRecords = await repository.find({
900
+ filterByTk,
901
+ fields
902
+ });
903
+ return existingRecords.length ? existingRecords.map((existing2) => ({ existing: existing2, values })) : [{ values }];
904
+ }
905
+ const filter = (_e = ctx.action) == null ? void 0 : _e.params.filter;
906
+ if (isRecordLike(filter) && !Array.isArray(filter)) {
907
+ const existingRecords = await repository.find({
908
+ filter,
909
+ fields
910
+ });
911
+ return existingRecords.length ? existingRecords.map((existing2) => ({ existing: existing2, values })) : [{ values }];
907
912
  }
908
- return values;
913
+ const uid = values.uid;
914
+ const existing = typeof uid === "string" && uid ? await repository.findOne({
915
+ filterByTk: uid,
916
+ fields
917
+ }) : null;
918
+ return [{ existing, values }];
909
919
  }
910
920
  function isTruthyActionBoolean(value) {
911
921
  return value === true || value === 1 || value === "1" || value === "true";
912
922
  }
913
923
  async function captureSkipCreatePortalDirectory(ctx, next) {
914
- const values = getMultiPortalWriteValues(ctx);
915
- if (!values || !Object.prototype.hasOwnProperty.call(values, "skipCreatePortalDirectory")) {
924
+ const records = getMultiPortalWriteRecords(ctx);
925
+ const recordsWithOption = records.filter(
926
+ (record) => Object.prototype.hasOwnProperty.call(record, "skipCreatePortalDirectory")
927
+ );
928
+ if (!recordsWithOption.length) {
916
929
  await next();
917
930
  return;
918
931
  }
919
- ctx[SKIP_CREATE_PORTAL_DIRECTORY] = isTruthyActionBoolean(
920
- values.skipCreatePortalDirectory
932
+ ctx[SKIP_CREATE_PORTAL_DIRECTORY] = recordsWithOption.some(
933
+ (record) => isTruthyActionBoolean(record.skipCreatePortalDirectory)
921
934
  );
922
- delete values.skipCreatePortalDirectory;
935
+ for (const record of recordsWithOption) {
936
+ delete record.skipCreatePortalDirectory;
937
+ }
923
938
  await next();
924
939
  }
925
940
  function shouldSkipCreatePortalDirectory(options) {
@@ -927,156 +942,177 @@ function shouldSkipCreatePortalDirectory(options) {
927
942
  return ((_a = options == null ? void 0 : options.context) == null ? void 0 : _a[SKIP_CREATE_PORTAL_DIRECTORY]) === true;
928
943
  }
929
944
  async function normalizeMultiPortalSlugValues(ctx, next) {
930
- const values = getMultiPortalWriteValues(ctx);
931
- const portalName = values == null ? void 0 : values.portalName;
932
- if (portalName === void 0 || portalName === null) {
933
- await next();
934
- return;
935
- }
936
- if (typeof portalName !== "string") {
937
- await next();
938
- return;
945
+ for (const values of getMultiPortalWriteRecords(ctx)) {
946
+ const portalName = values.portalName;
947
+ if (portalName === void 0 || portalName === null || typeof portalName !== "string") {
948
+ continue;
949
+ }
950
+ const slug = portalName.trim();
951
+ if (!slug) {
952
+ continue;
953
+ }
954
+ if (!MULTI_PORTAL_SLUG_PATTERN.test(slug)) {
955
+ ctx.throw(400, "Portal name can only contain lowercase letters, numbers, hyphens, and underscores");
956
+ return;
957
+ }
958
+ values.portalName = slug;
959
+ values.routePath = `/${slug}`;
939
960
  }
940
- const slug = portalName.trim();
941
- if (!slug) {
942
- await next();
943
- return;
961
+ const repository = ctx.db.getRepository("multiPortals");
962
+ const pendingPortalNames = /* @__PURE__ */ new Set();
963
+ const targets = await getMultiPortalWriteTargets(ctx, ["uid"]);
964
+ for (const { existing, values } of targets) {
965
+ const portalName = values.portalName;
966
+ if (typeof portalName !== "string" || !portalName) {
967
+ continue;
968
+ }
969
+ if (pendingPortalNames.has(portalName)) {
970
+ ctx.throw(409, ctx.t('Portal name "{{portalName}}" already exists', { ns: import_constants.NAMESPACE, portalName }));
971
+ return;
972
+ }
973
+ pendingPortalNames.add(portalName);
974
+ const conflict = await repository.findOne({
975
+ filter: { portalName },
976
+ fields: ["uid"]
977
+ });
978
+ if (conflict && conflict.get("uid") !== (existing == null ? void 0 : existing.get("uid"))) {
979
+ ctx.throw(409, ctx.t('Portal name "{{portalName}}" already exists', { ns: import_constants.NAMESPACE, portalName }));
980
+ return;
981
+ }
944
982
  }
945
- if (!MULTI_PORTAL_SLUG_PATTERN.test(slug)) {
946
- ctx.throw(400, "Portal name can only contain lowercase letters, numbers, hyphens, and underscores");
947
- return;
983
+ await next();
984
+ }
985
+ async function preventDirectDefaultPortalMutation(ctx, next) {
986
+ for (const values of getMultiPortalWriteRecords(ctx)) {
987
+ if (Object.prototype.hasOwnProperty.call(values, "isDefault")) {
988
+ ctx.throw(400, ctx.t("Use multiPortals:setDefault to update the default Portal", { ns: import_constants.NAMESPACE }));
989
+ return;
990
+ }
948
991
  }
949
- values.portalName = slug;
950
- values.routePath = `/${slug}`;
951
992
  await next();
952
993
  }
953
- async function ensureDefaultMultiPortals(db, options) {
994
+ function isUniqueConstraintViolation(error) {
995
+ return error instanceof import_database.UniqueConstraintError || isRecordLike(error) && error.name === "SequelizeUniqueConstraintError";
996
+ }
997
+ async function createDefaultMultiPortalBestEffort(db, portal) {
954
998
  const repository = db.getRepository("multiPortals");
955
- const defaultPortal = getDefaultMultiPortalRecord();
956
- const existing = await repository.findOne({
957
- filterByTk: defaultPortal.uid,
958
- fields: ["uid", "title", "icon", "portalType", "portalName", "routePath", "authCheck", "uiLayoutUid"],
959
- transaction: options == null ? void 0 : options.transaction
960
- });
961
- if (!existing) {
962
- await repository.create({
963
- values: defaultPortal,
964
- transaction: options == null ? void 0 : options.transaction
965
- });
966
- return;
999
+ const [uidConflict, portalNameConflict] = await Promise.all([
1000
+ repository.findOne({
1001
+ filterByTk: portal.uid,
1002
+ fields: ["uid"]
1003
+ }),
1004
+ repository.findOne({
1005
+ filter: { portalName: portal.portalName },
1006
+ fields: ["uid"]
1007
+ })
1008
+ ]);
1009
+ if (uidConflict || portalNameConflict) {
1010
+ return false;
967
1011
  }
968
- const protectedValues = {
969
- uid: defaultPortal.uid,
970
- portalType: defaultPortal.portalType ?? null,
971
- portalName: defaultPortal.portalName,
972
- routePath: defaultPortal.routePath,
973
- authCheck: defaultPortal.authCheck,
974
- uiLayoutUid: getDefaultMultiPortalUiLayoutUid(defaultPortal)
975
- };
976
- const shouldRepair = Object.entries(protectedValues).some(([field, value]) => existing.get(field) !== value);
977
- if (shouldRepair) {
1012
+ try {
1013
+ await repository.create({ values: portal });
1014
+ return true;
1015
+ } catch (error) {
1016
+ if (isUniqueConstraintViolation(error)) {
1017
+ return false;
1018
+ }
1019
+ throw error;
1020
+ }
1021
+ }
1022
+ async function repairFixedLayoutMultiPortalRecords(db) {
1023
+ const repository = db.getRepository("multiPortals");
1024
+ for (const portal of getFixedLayoutMultiPortalRecords()) {
1025
+ const existing = await repository.findOne({
1026
+ filterByTk: portal.uid,
1027
+ fields: ["uid", "portalType", "portalName", "routePath", "uiLayoutUid"]
1028
+ });
1029
+ if ((existing == null ? void 0 : existing.get("portalName")) !== portal.portalName || existing.get("routePath") !== portal.routePath || existing.get("uiLayoutUid") !== portal.uiLayoutUid || existing.get("portalType") === portal.portalType) {
1030
+ continue;
1031
+ }
978
1032
  await repository.update({
979
- filterByTk: defaultPortal.uid,
980
- values: protectedValues,
981
- transaction: options == null ? void 0 : options.transaction
1033
+ filterByTk: portal.uid,
1034
+ values: {
1035
+ portalType: portal.portalType
1036
+ }
982
1037
  });
983
1038
  }
984
1039
  }
985
- async function protectDefaultMultiPortalUpdate(ctx, next) {
986
- var _a, _b, _c;
987
- const defaultPortal = getDefaultMultiPortalRecordForUid((_a = ctx.action) == null ? void 0 : _a.params.filterByTk);
988
- if (!defaultPortal) {
989
- await next();
990
- return;
1040
+ async function seedFreshMultiPortals(db) {
1041
+ for (const portal of getFreshMultiPortalRecords()) {
1042
+ await createDefaultMultiPortalBestEffort(db, portal);
991
1043
  }
992
- const params = (_b = ctx.action) == null ? void 0 : _b.params;
993
- const rawValues = isRecordLike(params == null ? void 0 : params.values) ? params.values : {};
994
- const values = {
995
- uid: defaultPortal.uid,
996
- portalType: defaultPortal.portalType ?? null,
997
- portalName: defaultPortal.portalName,
998
- routePath: defaultPortal.routePath,
999
- authCheck: defaultPortal.authCheck,
1000
- uiLayoutUid: getDefaultMultiPortalUiLayoutUid(defaultPortal)
1001
- };
1002
- if (Object.prototype.hasOwnProperty.call(rawValues, "title")) {
1003
- values.title = rawValues.title;
1004
- }
1005
- if (Object.prototype.hasOwnProperty.call(rawValues, "icon")) {
1006
- values.icon = rawValues.icon;
1007
- }
1008
- if (Object.prototype.hasOwnProperty.call(rawValues, "enabled")) {
1009
- values.enabled = rawValues.enabled;
1044
+ }
1045
+ async function seedHistoricalMultiPortals(db) {
1046
+ await createDefaultMultiPortalBestEffort(db, getDefaultAiMultiPortalRecord());
1047
+ for (const portal of getFixedLayoutMultiPortalRecords()) {
1048
+ await createDefaultMultiPortalBestEffort(db, portal);
1010
1049
  }
1011
- (_c = ctx.action) == null ? void 0 : _c.mergeParams(
1012
- {
1013
- values
1014
- },
1015
- {
1016
- values: "overwrite"
1017
- }
1018
- );
1019
- await next();
1050
+ await repairFixedLayoutMultiPortalRecords(db);
1020
1051
  }
1021
- async function preventUiLayoutRouteNameConflict(ctx, next) {
1052
+ async function validateMultiPortalUiLayoutUidWrite(ctx, next) {
1022
1053
  var _a;
1023
- if (isDefaultMultiPortalUid((_a = ctx.action) == null ? void 0 : _a.params.filterByTk)) {
1024
- await next();
1025
- return;
1026
- }
1027
- const portalName = getMultiPortalNameFromValues(ctx);
1028
- if (!portalName) {
1029
- await next();
1030
- return;
1031
- }
1032
- const uiLayout = await ctx.db.getRepository("uiLayouts").findOne({
1033
- filter: {
1034
- routeName: portalName
1035
- },
1036
- fields: ["uid"]
1037
- });
1038
- if (uiLayout) {
1039
- ctx.throw(400, "Portal route name conflicts with an existing UI layout");
1040
- return;
1054
+ const targets = await getMultiPortalWriteTargets(ctx, ["uiLayoutUid"]);
1055
+ const actionName = (_a = ctx.action) == null ? void 0 : _a.actionName;
1056
+ const createsWhenMissing = actionName === "create" || actionName === "firstOrCreate" || actionName === "updateOrCreate";
1057
+ for (const { existing, values } of targets) {
1058
+ const hasUiLayoutUid = Object.prototype.hasOwnProperty.call(values, "uiLayoutUid");
1059
+ if (!existing && createsWhenMissing && !hasUiLayoutUid) {
1060
+ ctx.throw(400, `Portal UI layout must be one of: ${import_constants.MULTI_PORTAL_UI_LAYOUT_UIDS.join(", ")}`);
1061
+ return;
1062
+ }
1063
+ if (!hasUiLayoutUid) {
1064
+ continue;
1065
+ }
1066
+ if (!(0, import_constants.isMultiPortalUiLayoutUid)(values.uiLayoutUid)) {
1067
+ ctx.throw(400, `Portal UI layout must be one of: ${import_constants.MULTI_PORTAL_UI_LAYOUT_UIDS.join(", ")}`);
1068
+ return;
1069
+ }
1070
+ const existingUiLayoutUid = existing == null ? void 0 : existing.get("uiLayoutUid");
1071
+ if (existing && existingUiLayoutUid !== values.uiLayoutUid) {
1072
+ ctx.throw(400, "Portal UI layout cannot be changed");
1073
+ return;
1074
+ }
1041
1075
  }
1042
1076
  await next();
1043
1077
  }
1044
- async function findRequestedMultiPortal(ctx) {
1045
- var _a;
1078
+ async function findRequestedMultiPortal(ctx, transaction) {
1046
1079
  const portalUid = getRequestedMultiPortalUid(ctx);
1047
1080
  if (portalUid === void 0) {
1048
1081
  return {
1049
1082
  requested: false
1050
1083
  };
1051
1084
  }
1052
- const isExplicitPortalRequest = getExplicitRequestedLayoutUid((_a = ctx.action) == null ? void 0 : _a.params.portal) !== void 0;
1053
- if (!isExplicitPortalRequest) {
1054
- const uiLayout = await ctx.db.getRepository("uiLayouts").findOne({
1055
- filter: {
1056
- uid: portalUid
1057
- },
1058
- fields: ["uid"]
1059
- });
1060
- if (uiLayout) {
1061
- return {
1062
- requested: false
1063
- };
1064
- }
1065
- }
1066
1085
  const portal = await ctx.db.getRepository("multiPortals").findOne({
1067
1086
  filter: {
1068
1087
  uid: portalUid
1069
1088
  },
1070
- fields: ["uid", "uiLayoutUid", "enabled"]
1089
+ fields: ["uid", "portalType", "uiLayoutUid", "enabled"],
1090
+ ...transaction ? { lock: transaction.LOCK.UPDATE } : {},
1091
+ transaction
1071
1092
  });
1072
- if ((portal == null ? void 0 : portal.get("enabled")) === true) {
1073
- return {
1074
- portal,
1075
- requested: true
1076
- };
1093
+ if (!portal || portal.get("enabled") !== true) {
1094
+ return { requested: true };
1095
+ }
1096
+ if (portal.get("portalType") !== "no-code") {
1097
+ ctx.throw(400, `Portal '${portalUid}' does not support desktop routes`);
1077
1098
  }
1099
+ const uiLayoutUid = portal.get("uiLayoutUid");
1100
+ if (!(0, import_constants.isMultiPortalUiLayoutUid)(uiLayoutUid)) {
1101
+ ctx.throw(400, `Portal '${portalUid}' has an unsupported UI layout UID`);
1102
+ }
1103
+ const usesLayoutPermissions = (0, import_constants.isDefaultLayoutMultiPortalUid)(portalUid);
1078
1104
  return {
1079
- requested: !!isExplicitPortalRequest || !!portal
1105
+ portal,
1106
+ requested: true,
1107
+ scope: {
1108
+ portalUid,
1109
+ uiLayoutUid,
1110
+ relation: usesLayoutPermissions ? "uiLayouts" : "multiPortals",
1111
+ uid: usesLayoutPermissions ? uiLayoutUid : portalUid,
1112
+ filter: usesLayoutPermissions ? {
1113
+ "uiLayouts.uid": uiLayoutUid
1114
+ } : getDesktopRoutePortalFilter(portalUid)
1115
+ }
1080
1116
  };
1081
1117
  }
1082
1118
  function getDesktopRoutePortalFilter(multiPortalUid) {
@@ -1087,49 +1123,88 @@ function getDesktopRoutePortalFilter(multiPortalUid) {
1087
1123
  function isDesktopRouteCreateValue(value) {
1088
1124
  return !!value && typeof value === "object" && !Array.isArray(value);
1089
1125
  }
1090
- function hasDesktopRouteField(value, field) {
1091
- return !!value && Object.prototype.hasOwnProperty.call(value, field);
1126
+ function withDesktopRouteEffectiveOwner(value, scope) {
1127
+ if (Array.isArray(value)) {
1128
+ return value.map((item) => withDesktopRouteEffectiveOwner(item, scope));
1129
+ }
1130
+ if (!isDesktopRouteCreateValue(value)) {
1131
+ return value;
1132
+ }
1133
+ const { uiLayouts: _uiLayouts, multiPortals: _multiPortals, children, ...route } = value;
1134
+ return {
1135
+ ...route,
1136
+ ...scope.relation === "uiLayouts" ? { uiLayouts: [scope.uid] } : { multiPortals: [scope.uid] },
1137
+ ...Array.isArray(children) ? { children: withDesktopRouteEffectiveOwner(children, scope) } : {}
1138
+ };
1092
1139
  }
1093
- function pickExplicitDesktopRouteUiLayouts(value) {
1140
+ function collectExplicitDesktopRouteParentIds(value) {
1094
1141
  if (Array.isArray(value)) {
1095
- return value.map((item) => pickExplicitDesktopRouteUiLayouts(item));
1142
+ return uniqueDesktopRouteIds(value.flatMap((item) => collectExplicitDesktopRouteParentIds(item)));
1096
1143
  }
1097
1144
  if (!isDesktopRouteCreateValue(value)) {
1098
- return;
1145
+ return [];
1099
1146
  }
1100
- const route = {};
1101
- if (hasDesktopRouteField(value, "uiLayouts")) {
1102
- route.uiLayouts = value.uiLayouts;
1147
+ const parentIds = [];
1148
+ if (typeof value.parentId === "string" || typeof value.parentId === "number") {
1149
+ parentIds.push(value.parentId);
1103
1150
  }
1104
1151
  if (Array.isArray(value.children)) {
1105
- route.children = value.children.map((child) => pickExplicitDesktopRouteUiLayouts(child));
1152
+ parentIds.push(...value.children.flatMap((child) => collectExplicitDesktopRouteParentIds(child)));
1106
1153
  }
1107
- return route;
1154
+ return uniqueDesktopRouteIds(parentIds);
1108
1155
  }
1109
- function withDesktopRouteMultiPortal(value, multiPortalUid, explicitValue = value) {
1110
- if (Array.isArray(value)) {
1111
- const explicitValues = Array.isArray(explicitValue) ? explicitValue : [];
1112
- return value.map((item, index) => withDesktopRouteMultiPortal(item, multiPortalUid, explicitValues[index]));
1156
+ async function assertDesktopRouteParentsBelongToScope(ctx, scope, values = ((_a) => (_a = ctx.action) == null ? void 0 : _a.params.values)(), transaction) {
1157
+ const parentIds = collectExplicitDesktopRouteParentIds(values);
1158
+ if (!parentIds.length) {
1159
+ return;
1113
1160
  }
1114
- if (!isDesktopRouteCreateValue(value)) {
1115
- return value;
1161
+ const parents = await ctx.db.getRepository("desktopRoutes").find({
1162
+ fields: ["id"],
1163
+ filter: {
1164
+ id: parentIds,
1165
+ ...scope.filter
1166
+ },
1167
+ transaction
1168
+ });
1169
+ if (new Set(normalizeDesktopRouteTargetIds(parents)).size !== new Set(parentIds.map(String)).size) {
1170
+ ctx.throw(400, "Parent route does not belong to the requested portal scope");
1116
1171
  }
1117
- const explicitRoute = isDesktopRouteCreateValue(explicitValue) ? explicitValue : void 0;
1118
- const existingMultiPortals = Array.isArray(value.multiPortals) ? value.multiPortals.filter((uid) => typeof uid === "string" && !!uid) : [];
1119
- const { uiLayouts: _uiLayouts, children: _children, ...route } = value;
1120
- return {
1121
- ...route,
1122
- ...hasDesktopRouteField(explicitRoute, "uiLayouts") ? { uiLayouts: explicitRoute == null ? void 0 : explicitRoute.uiLayouts } : {},
1123
- multiPortals: Array.from(/* @__PURE__ */ new Set([...existingMultiPortals, multiPortalUid])),
1124
- ...Array.isArray(value.children) ? { children: withDesktopRouteMultiPortal(value.children, multiPortalUid, explicitRoute == null ? void 0 : explicitRoute.children) } : {}
1125
- };
1126
1172
  }
1127
- async function captureExplicitDesktopRouteUiLayouts(ctx, next) {
1128
- var _a;
1129
- ctx[EXPLICIT_DESKTOP_ROUTE_UI_LAYOUTS] = pickExplicitDesktopRouteUiLayouts(
1130
- (_a = ctx.action) == null ? void 0 : _a.params.values
1131
- );
1132
- await next();
1173
+ function getDesktopRouteUpdateOrCreateFilters(ctx, values = ((_b) => (_b = ctx.action) == null ? void 0 : _b.params.values)(), requestedFilterKeys = ((_c) => (_c = ctx.action) == null ? void 0 : _c.params.filterKeys)()) {
1174
+ const filterKeys = Array.isArray(requestedFilterKeys) ? requestedFilterKeys.filter((key) => typeof key === "string" && !!key) : [];
1175
+ if (!filterKeys.length) {
1176
+ return [];
1177
+ }
1178
+ const records = Array.isArray(values) ? values : [values];
1179
+ return records.flatMap((value) => {
1180
+ if (!isDesktopRouteCreateValue(value) || filterKeys.some((key) => !Object.prototype.hasOwnProperty.call(value, key))) {
1181
+ return [];
1182
+ }
1183
+ return [Object.fromEntries(filterKeys.map((key) => [key, value[key]]))];
1184
+ });
1185
+ }
1186
+ async function assertDesktopRouteUpsertMatchesBelongToScope(ctx, scope, values = ((_d) => (_d = ctx.action) == null ? void 0 : _d.params.values)(), filterKeys = ((_e) => (_e = ctx.action) == null ? void 0 : _e.params.filterKeys)(), transaction) {
1187
+ for (const filter of getDesktopRouteUpdateOrCreateFilters(ctx, values, filterKeys)) {
1188
+ const allMatches = await ctx.db.getRepository("desktopRoutes").find({
1189
+ fields: ["id"],
1190
+ filter,
1191
+ ...transaction ? { lock: transaction.LOCK.UPDATE } : {},
1192
+ transaction
1193
+ });
1194
+ const scopedMatches = await ctx.db.getRepository("desktopRoutes").find({
1195
+ fields: ["id"],
1196
+ filter: {
1197
+ ...filter,
1198
+ ...scope.filter
1199
+ },
1200
+ transaction
1201
+ });
1202
+ const allMatchIds = normalizeDesktopRouteTargetIds(allMatches);
1203
+ const scopedMatchIds = normalizeDesktopRouteTargetIds(scopedMatches);
1204
+ if (allMatchIds.join(",") !== scopedMatchIds.join(",")) {
1205
+ ctx.throw(400, "Desktop route does not belong to the requested portal scope");
1206
+ }
1207
+ }
1133
1208
  }
1134
1209
  async function canAccessMultiPortal(ctx, multiPortalUid) {
1135
1210
  const currentRoles = getCurrentRoles(ctx);
@@ -1139,13 +1214,77 @@ async function canAccessMultiPortal(ctx, multiPortalUid) {
1139
1214
  if (!currentRoles.length) {
1140
1215
  return false;
1141
1216
  }
1142
- const count = await ctx.db.getRepository("rolesMultiPortals").count({
1217
+ const access = await ctx.db.getRepository("rolesMultiPortals").findOne({
1218
+ fields: ["id"],
1143
1219
  filter: {
1144
1220
  roleName: currentRoles,
1145
1221
  multiPortalUid
1146
1222
  }
1147
1223
  });
1148
- return count > 0;
1224
+ return access !== null;
1225
+ }
1226
+ function throwPortalAccessGateError(ctx, status, code, message) {
1227
+ ctx.throw(status, ctx.t(message, { ns: import_constants.NAMESPACE }), { code });
1228
+ throw new Error(message);
1229
+ }
1230
+ function getRequestedPortalNameFromHeader(ctx) {
1231
+ var _a;
1232
+ const headers = (_a = ctx.request) == null ? void 0 : _a.headers;
1233
+ if (!isRecordLike(headers) || !Object.prototype.hasOwnProperty.call(headers, "x-portal")) {
1234
+ return;
1235
+ }
1236
+ const rawPortalName = headers["x-portal"];
1237
+ if (typeof rawPortalName !== "string") {
1238
+ throwPortalAccessGateError(ctx, 400, PORTAL_CONTEXT_INVALID_CODE, "Invalid Portal context");
1239
+ }
1240
+ const portalName = rawPortalName.startsWith("/x/") ? rawPortalName.slice("/x/".length) : rawPortalName;
1241
+ if (!MULTI_PORTAL_SLUG_PATTERN.test(portalName)) {
1242
+ throwPortalAccessGateError(ctx, 400, PORTAL_CONTEXT_INVALID_CODE, "Invalid Portal context");
1243
+ }
1244
+ return portalName;
1245
+ }
1246
+ function pickPortalAccessDeniedData(ctx, portalName) {
1247
+ const roleCheckBody = isRecordLike(ctx.body) ? ctx.body : {};
1248
+ return {
1249
+ portalName,
1250
+ role: typeof roleCheckBody.role === "string" ? roleCheckBody.role : "",
1251
+ roles: Array.isArray(roleCheckBody.roles) ? roleCheckBody.roles.filter((role) => typeof role === "string") : [],
1252
+ roleMode: typeof roleCheckBody.roleMode === "string" ? roleCheckBody.roleMode : "default",
1253
+ allowAnonymous: roleCheckBody.allowAnonymous === true
1254
+ };
1255
+ }
1256
+ async function checkMultiPortalAccessForRolesCheck(ctx, next) {
1257
+ const portalName = getRequestedPortalNameFromHeader(ctx);
1258
+ if (portalName === void 0) {
1259
+ await next();
1260
+ return;
1261
+ }
1262
+ const portal = await ctx.db.getRepository("multiPortals").findOne({
1263
+ filter: {
1264
+ portalName
1265
+ },
1266
+ fields: ["uid", "portalType", "enabled", "uiLayoutUid"]
1267
+ });
1268
+ if (!portal || portal.get("enabled") !== true || !getDefaultMultiPortalType(portal) || !(0, import_constants.isMultiPortalUiLayoutUid)(portal.get("uiLayoutUid"))) {
1269
+ throwPortalAccessGateError(ctx, 404, PORTAL_NOT_FOUND_CODE, "Portal not found");
1270
+ }
1271
+ const portalUid = String(portal.get("uid"));
1272
+ if ((0, import_constants.isDefaultLayoutMultiPortalUid)(portalUid) || await canAccessMultiPortal(ctx, portalUid)) {
1273
+ await next();
1274
+ return;
1275
+ }
1276
+ await next();
1277
+ ctx.status = 403;
1278
+ ctx.withoutDataWrapping = true;
1279
+ ctx.body = {
1280
+ errors: [
1281
+ {
1282
+ code: PORTAL_ACCESS_DENIED_CODE,
1283
+ message: ctx.t("You do not have access to this Portal", { ns: import_constants.NAMESPACE })
1284
+ }
1285
+ ],
1286
+ data: pickPortalAccessDeniedData(ctx, portalName)
1287
+ };
1149
1288
  }
1150
1289
  async function listCurrentRoleAccessibleMultiPortalUids(ctx) {
1151
1290
  const currentRoles = getCurrentRoles(ctx);
@@ -1208,7 +1347,7 @@ async function removeRouteIdsWithUnauthorizedAncestors(ctx, routeIds) {
1208
1347
  }
1209
1348
  }
1210
1349
  }
1211
- async function getMultiPortalAccessibleRouteIds(ctx, multiPortalUid) {
1350
+ async function getMultiPortalAccessibleRouteIds(ctx, portalContext) {
1212
1351
  const currentRoles = getCurrentRoles(ctx);
1213
1352
  if (currentRoles.includes("root")) {
1214
1353
  return;
@@ -1216,11 +1355,11 @@ async function getMultiPortalAccessibleRouteIds(ctx, multiPortalUid) {
1216
1355
  if (!currentRoles.length) {
1217
1356
  return /* @__PURE__ */ new Set();
1218
1357
  }
1219
- const routePermissions = await ctx.db.getRepository("rolesMultiPortalDesktopRoutes").find({
1358
+ const routePermissions = await ctx.db.getRepository(portalContext.relation === "uiLayouts" ? "rolesDesktopRoutes" : "rolesMultiPortalDesktopRoutes").find({
1220
1359
  fields: ["desktopRouteId"],
1221
1360
  filter: {
1222
1361
  roleName: currentRoles,
1223
- multiPortalUid
1362
+ ...portalContext.relation === "multiPortals" ? { multiPortalUid: portalContext.portalUid } : {}
1224
1363
  }
1225
1364
  });
1226
1365
  const routeIds = /* @__PURE__ */ new Set();
@@ -1236,7 +1375,7 @@ async function getMultiPortalAccessibleRouteIds(ctx, multiPortalUid) {
1236
1375
  const portalRoutes = await ctx.db.getRepository("desktopRoutes").find({
1237
1376
  fields: ["id"],
1238
1377
  filter: {
1239
- ...getDesktopRoutePortalFilter(multiPortalUid),
1378
+ ...portalContext.filter,
1240
1379
  id: Array.from(routeIds)
1241
1380
  }
1242
1381
  });
@@ -1266,25 +1405,123 @@ function setDesktopRouteChildren(route, children) {
1266
1405
  const maybeModel = route;
1267
1406
  if (typeof maybeModel.setDataValue === "function") {
1268
1407
  maybeModel.setDataValue("children", children);
1408
+ } else {
1409
+ maybeModel.children = children;
1410
+ }
1411
+ if (!maybeModel._options) {
1412
+ return;
1413
+ }
1414
+ if (!maybeModel._options.includeNames) {
1415
+ maybeModel._options.includeNames = ["children"];
1269
1416
  return;
1270
1417
  }
1271
- route.children = children;
1418
+ if (!maybeModel._options.includeNames.includes("children")) {
1419
+ maybeModel._options.includeNames.push("children");
1420
+ }
1272
1421
  }
1273
- function removeDesktopRoutesByIds(routes, routeIds) {
1274
- return routes.map((route) => {
1275
- const routeId = getRecordField(route, "id");
1276
- const normalizedRouteId = routeId === null || routeId === void 0 ? void 0 : String(routeId);
1277
- if (normalizedRouteId && routeIds.has(normalizedRouteId)) {
1278
- return null;
1422
+ function getDesktopRouteId(route) {
1423
+ const id = getRecordField(route, "id");
1424
+ return id === null || id === void 0 ? void 0 : String(id);
1425
+ }
1426
+ function getDesktopRouteParentId(route) {
1427
+ const parentId = getRecordField(route, "parentId");
1428
+ return parentId === null || parentId === void 0 ? void 0 : String(parentId);
1429
+ }
1430
+ function collectDesktopRouteStringIds(routes, routeIds) {
1431
+ for (const route of routes) {
1432
+ const routeId = getDesktopRouteId(route);
1433
+ if (routeId) {
1434
+ routeIds.add(routeId);
1279
1435
  }
1280
1436
  const children = getRecordField(route, "children");
1281
1437
  if (Array.isArray(children)) {
1282
- setDesktopRouteChildren(route, removeDesktopRoutesByIds(children, routeIds));
1438
+ collectDesktopRouteStringIds(children, routeIds);
1283
1439
  }
1284
- return route;
1285
- }).filter((route) => route !== null);
1440
+ }
1286
1441
  }
1287
- async function getPortalOnlyDesktopRouteIds(ctx, routeIds) {
1442
+ function removeNestedRootDesktopRoutes(routes) {
1443
+ if (!Array.isArray(routes)) {
1444
+ return [];
1445
+ }
1446
+ const routeIds = /* @__PURE__ */ new Set();
1447
+ collectDesktopRouteStringIds(routes, routeIds);
1448
+ return routes.filter((route) => {
1449
+ const parentId = getDesktopRouteParentId(route);
1450
+ return !parentId || !routeIds.has(parentId);
1451
+ });
1452
+ }
1453
+ function buildAccessibleDesktopRouteTreeWithAncestors(routes, accessibleRouteIds) {
1454
+ const routeById = /* @__PURE__ */ new Map();
1455
+ const childrenByParentId = /* @__PURE__ */ new Map();
1456
+ const roots = [];
1457
+ for (const route of routes) {
1458
+ const routeId = getDesktopRouteId(route);
1459
+ if (!routeId) {
1460
+ continue;
1461
+ }
1462
+ setDesktopRouteChildren(route, void 0);
1463
+ routeById.set(routeId, route);
1464
+ }
1465
+ for (const route of routes) {
1466
+ const routeId = getDesktopRouteId(route);
1467
+ if (!routeId || !routeById.has(routeId)) {
1468
+ continue;
1469
+ }
1470
+ const parentId = getDesktopRouteParentId(route);
1471
+ if (!parentId || !routeById.has(parentId)) {
1472
+ roots.push(route);
1473
+ continue;
1474
+ }
1475
+ const children = childrenByParentId.get(parentId) ?? [];
1476
+ children.push(route);
1477
+ childrenByParentId.set(parentId, children);
1478
+ }
1479
+ const visitRoute = (route, visitingRouteIds) => {
1480
+ const routeId = getDesktopRouteId(route);
1481
+ if (!routeId || visitingRouteIds.has(routeId)) {
1482
+ return void 0;
1483
+ }
1484
+ visitingRouteIds.add(routeId);
1485
+ const visibleChildren = (childrenByParentId.get(routeId) ?? []).map((child) => visitRoute(child, visitingRouteIds)).filter((child) => child !== void 0);
1486
+ visitingRouteIds.delete(routeId);
1487
+ if (!accessibleRouteIds.has(routeId) && visibleChildren.length === 0) {
1488
+ return void 0;
1489
+ }
1490
+ setDesktopRouteChildren(route, visibleChildren.length ? visibleChildren : void 0);
1491
+ return route;
1492
+ };
1493
+ return roots.map((route) => visitRoute(route, /* @__PURE__ */ new Set())).filter((route) => route !== void 0);
1494
+ }
1495
+ async function includeDesktopRouteAncestorsForListAccessible(ctx, routes, portalFilter) {
1496
+ if (!Array.isArray(routes)) {
1497
+ return routes;
1498
+ }
1499
+ const accessibleRouteIds = /* @__PURE__ */ new Set();
1500
+ collectDesktopRouteStringIds(routes, accessibleRouteIds);
1501
+ if (!accessibleRouteIds.size) {
1502
+ return routes;
1503
+ }
1504
+ const portalRoutes = await ctx.db.getRepository("desktopRoutes").find({
1505
+ sort: "sort",
1506
+ filter: portalFilter
1507
+ });
1508
+ return buildAccessibleDesktopRouteTreeWithAncestors(portalRoutes, accessibleRouteIds);
1509
+ }
1510
+ function removeDesktopRoutesByIds(routes, routeIds) {
1511
+ return routes.map((route) => {
1512
+ const routeId = getRecordField(route, "id");
1513
+ const normalizedRouteId = routeId === null || routeId === void 0 ? void 0 : String(routeId);
1514
+ if (normalizedRouteId && routeIds.has(normalizedRouteId)) {
1515
+ return null;
1516
+ }
1517
+ const children = getRecordField(route, "children");
1518
+ if (Array.isArray(children)) {
1519
+ setDesktopRouteChildren(route, removeDesktopRoutesByIds(children, routeIds));
1520
+ }
1521
+ return route;
1522
+ }).filter((route) => route !== null);
1523
+ }
1524
+ async function getPortalOnlyDesktopRouteIds(ctx, routeIds) {
1288
1525
  if (!routeIds.length) {
1289
1526
  return /* @__PURE__ */ new Set();
1290
1527
  }
@@ -1335,23 +1572,26 @@ async function removeMultiPortalOwnedRouteFromGetResponse(ctx) {
1335
1572
  ctx.body = void 0;
1336
1573
  }
1337
1574
  async function replaceListAccessibleRoutesWithPortalScopedRoutes(ctx, portalContext) {
1338
- const routeIds = await getMultiPortalAccessibleRouteIds(ctx, portalContext.portalUid);
1575
+ const routeIds = await getMultiPortalAccessibleRouteIds(ctx, portalContext);
1339
1576
  if (routeIds && routeIds.size === 0) {
1340
1577
  ctx.body = [];
1341
1578
  return;
1342
1579
  }
1343
- ctx.body = await ctx.db.getRepository("desktopRoutes").find({
1580
+ const routes = await ctx.db.getRepository("desktopRoutes").find({
1344
1581
  tree: true,
1345
1582
  sort: "sort",
1346
1583
  filter: {
1347
- ...getDesktopRoutePortalFilter(portalContext.portalUid),
1584
+ ...portalContext.filter,
1348
1585
  ...routeIds ? { id: Array.from(routeIds) } : {}
1349
1586
  }
1350
1587
  });
1588
+ ctx.body = removeNestedRootDesktopRoutes(
1589
+ await includeDesktopRouteAncestorsForListAccessible(ctx, routes, portalContext.filter)
1590
+ );
1351
1591
  }
1352
1592
  async function replaceGetAccessibleRouteWithPortalScopedRoute(ctx, portalContext) {
1353
1593
  var _a;
1354
- const routeIds = await getMultiPortalAccessibleRouteIds(ctx, portalContext.portalUid);
1594
+ const routeIds = await getMultiPortalAccessibleRouteIds(ctx, portalContext);
1355
1595
  if (routeIds && routeIds.size === 0) {
1356
1596
  ctx.status = 204;
1357
1597
  ctx.body = void 0;
@@ -1361,7 +1601,7 @@ async function replaceGetAccessibleRouteWithPortalScopedRoute(ctx, portalContext
1361
1601
  sort: "sort",
1362
1602
  filterByTk: (_a = ctx.action) == null ? void 0 : _a.params.filterByTk,
1363
1603
  filter: {
1364
- ...getDesktopRoutePortalFilter(portalContext.portalUid),
1604
+ ...portalContext.filter,
1365
1605
  ...routeIds ? { id: Array.from(routeIds) } : {}
1366
1606
  }
1367
1607
  });
@@ -1379,25 +1619,17 @@ async function prepareAccessibleDesktopRoutesForMultiPortal(ctx) {
1379
1619
  if (!portalRequest.requested) {
1380
1620
  return;
1381
1621
  }
1382
- const portal = portalRequest.portal;
1383
- if (!portal) {
1384
- return null;
1385
- }
1386
- const portalUid = portal.get("uid");
1387
- const uiLayoutUid = portal.get("uiLayoutUid");
1388
- if (typeof portalUid !== "string" || !portalUid || typeof uiLayoutUid !== "string" || !uiLayoutUid) {
1622
+ const scope = portalRequest.scope;
1623
+ if (!scope) {
1389
1624
  return null;
1390
1625
  }
1391
- if (!await canAccessMultiPortal(ctx, portalUid)) {
1626
+ if (scope.relation === "multiPortals" && !await canAccessMultiPortal(ctx, scope.portalUid)) {
1392
1627
  return null;
1393
1628
  }
1394
1629
  if ((_a = ctx.action) == null ? void 0 : _a.params) {
1395
- ctx.action.params.layout = uiLayoutUid;
1630
+ ctx.action.params.layout = scope.uiLayoutUid;
1396
1631
  }
1397
- return {
1398
- portalUid,
1399
- uiLayoutUid
1400
- };
1632
+ return scope;
1401
1633
  }
1402
1634
  async function addMultiPortalListAccessibleGuard(ctx, next) {
1403
1635
  const portalContext = await prepareAccessibleDesktopRoutesForMultiPortal(ctx);
@@ -1433,98 +1665,740 @@ async function mapMultiPortalLayoutToUiLayoutForRolePermissionTargets(ctx, next)
1433
1665
  ctx.body = [];
1434
1666
  return;
1435
1667
  }
1436
- const portal = portalRequest.portal;
1437
- const portalUid = portal == null ? void 0 : portal.get("uid");
1438
- if (typeof portalUid === "string" && portalUid) {
1668
+ const scope = portalRequest.scope;
1669
+ if (scope) {
1439
1670
  const routes = await ctx.db.getRepository("desktopRoutes").find({
1440
- tree: true,
1441
1671
  sort: "sort",
1442
- filter: getDesktopRoutePortalFilter(portalUid),
1672
+ filter: scope.filter,
1443
1673
  fields: [...DESKTOP_ROUTE_ROLE_PERMISSION_TARGET_FIELDS]
1444
1674
  });
1675
+ const routeIds = /* @__PURE__ */ new Set();
1676
+ collectDesktopRouteStringIds(routes, routeIds);
1677
+ const routeTree = buildAccessibleDesktopRouteTreeWithAncestors(routes, routeIds);
1445
1678
  ctx.status = 200;
1446
- ctx.body = routes.map((route) => pickDesktopRouteRolePermissionTargetFields(route));
1679
+ ctx.body = removeNestedRootDesktopRoutes(routeTree).map(
1680
+ (route) => pickDesktopRouteRolePermissionTargetFields(route)
1681
+ );
1447
1682
  return;
1448
1683
  }
1449
1684
  await next();
1450
1685
  await removeMultiPortalOwnedRoutesFromListResponse(ctx);
1451
1686
  }
1452
1687
  async function addDesktopRouteCreateMultiPortal(ctx, next) {
1453
- var _a, _b;
1688
+ var _a, _b, _c;
1454
1689
  const portalRequest = await findRequestedMultiPortal(ctx);
1455
1690
  if (portalRequest.requested && !portalRequest.portal) {
1456
1691
  ctx.throw(400, "Invalid portal scope");
1457
1692
  return;
1458
1693
  }
1459
- const portal = portalRequest.portal;
1460
- const portalUid = portal == null ? void 0 : portal.get("uid");
1461
- if (typeof portalUid === "string" && portalUid) {
1462
- const explicitValues = ctx[EXPLICIT_DESKTOP_ROUTE_UI_LAYOUTS];
1463
- (_b = ctx.action) == null ? void 0 : _b.mergeParams(
1694
+ const scope = portalRequest.scope;
1695
+ if (scope) {
1696
+ await assertDesktopRouteParentsBelongToScope(ctx, scope);
1697
+ if (((_a = ctx.action) == null ? void 0 : _a.actionName) === "updateOrCreate") {
1698
+ await assertDesktopRouteUpsertMatchesBelongToScope(ctx, scope);
1699
+ }
1700
+ (_c = ctx.action) == null ? void 0 : _c.mergeParams(
1464
1701
  {
1465
- values: withDesktopRouteMultiPortal((_a = ctx.action) == null ? void 0 : _a.params.values, portalUid, explicitValues)
1702
+ values: withDesktopRouteEffectiveOwner((_b = ctx.action) == null ? void 0 : _b.params.values, scope)
1466
1703
  },
1467
1704
  {
1468
1705
  values: "overwrite"
1469
1706
  }
1470
1707
  );
1471
1708
  await next();
1472
- const desktopRouteIds = collectDesktopRouteIds(ctx.body);
1473
- await removeLayoutRoutePermissionsFromPortalOnlyRoutes(ctx, desktopRouteIds);
1474
- await grantDefaultRouteAccessToNewMultiPortalRoutes(ctx, portalUid, desktopRouteIds);
1475
1709
  return;
1476
1710
  }
1477
1711
  await next();
1478
1712
  }
1713
+ async function addDesktopRouteEffectiveScopeFilter(ctx, next) {
1714
+ var _a;
1715
+ const portalRequest = await findRequestedMultiPortal(ctx);
1716
+ if (!portalRequest.requested) {
1717
+ await next();
1718
+ return;
1719
+ }
1720
+ if (!portalRequest.scope) {
1721
+ ctx.throw(400, "Invalid portal scope");
1722
+ return;
1723
+ }
1724
+ (_a = ctx.action) == null ? void 0 : _a.mergeParams({
1725
+ filter: portalRequest.scope.filter
1726
+ });
1727
+ await next();
1728
+ }
1729
+ function withoutDesktopRouteOwnerFields(value) {
1730
+ if (Array.isArray(value)) {
1731
+ return value.map((item) => withoutDesktopRouteOwnerFields(item));
1732
+ }
1733
+ if (!isDesktopRouteCreateValue(value)) {
1734
+ return value;
1735
+ }
1736
+ const { uiLayouts: _uiLayouts, multiPortals: _multiPortals, children, ...route } = value;
1737
+ return {
1738
+ ...route,
1739
+ ...Array.isArray(children) ? { children: withoutDesktopRouteOwnerFields(children) } : {}
1740
+ };
1741
+ }
1742
+ function normalizeDesktopRouteTargetIds(records) {
1743
+ return records.map((record) => record.get("id")).filter((id) => typeof id === "string" || typeof id === "number").map(String).sort();
1744
+ }
1745
+ async function findDesktopRouteMutationTargets(ctx, scopeFilter, transaction, selector) {
1746
+ var _a;
1747
+ const params = selector ?? ((_a = ctx.action) == null ? void 0 : _a.params) ?? {};
1748
+ const values = params.values;
1749
+ const batchUpdateIds = Array.isArray(values) ? values.flatMap((value) => {
1750
+ if (!isDesktopRouteCreateValue(value)) {
1751
+ return [];
1752
+ }
1753
+ const routeId = value.id;
1754
+ return typeof routeId === "string" || typeof routeId === "number" ? [routeId] : [];
1755
+ }) : [];
1756
+ const hasBatchUpdateTargets = batchUpdateIds.length > 0;
1757
+ const filterByTk = hasBatchUpdateTargets ? batchUpdateIds : params.filterByTk;
1758
+ const filter = !hasBatchUpdateTargets && isRecordLike(params.filter) ? params.filter : void 0;
1759
+ if ((filterByTk === void 0 || filterByTk === null) && !filter) {
1760
+ return [];
1761
+ }
1762
+ return ctx.db.getRepository("desktopRoutes").find({
1763
+ fields: ["id"],
1764
+ ...filterByTk !== void 0 && filterByTk !== null ? { filterByTk } : {},
1765
+ filter: {
1766
+ ...filter ?? {},
1767
+ ...scopeFilter ?? {}
1768
+ },
1769
+ ...transaction && !scopeFilter ? { lock: transaction.LOCK.UPDATE } : {},
1770
+ transaction
1771
+ });
1772
+ }
1773
+ async function assertDesktopRouteMutationTargetsBelongToScope(ctx, scope, transaction, selector) {
1774
+ const allTargets = await findDesktopRouteMutationTargets(ctx, void 0, transaction, selector);
1775
+ const scopedTargets = await findDesktopRouteMutationTargets(ctx, scope.filter, transaction, selector);
1776
+ const allTargetIds = normalizeDesktopRouteTargetIds(allTargets);
1777
+ const scopedTargetIds = normalizeDesktopRouteTargetIds(scopedTargets);
1778
+ if (!allTargetIds.length || allTargetIds.join(",") !== scopedTargetIds.join(",")) {
1779
+ ctx.throw(400, "Desktop route does not belong to the requested portal scope");
1780
+ }
1781
+ }
1782
+ async function guardDesktopRouteUpdateScope(ctx, next) {
1783
+ var _a, _b;
1784
+ const portalRequest = await findRequestedMultiPortal(ctx);
1785
+ if (!portalRequest.requested) {
1786
+ await next();
1787
+ return;
1788
+ }
1789
+ const scope = portalRequest.scope;
1790
+ if (!scope) {
1791
+ ctx.throw(400, "Invalid portal scope");
1792
+ return;
1793
+ }
1794
+ await assertDesktopRouteMutationTargetsBelongToScope(ctx, scope);
1795
+ await assertDesktopRouteParentsBelongToScope(ctx, scope);
1796
+ (_b = ctx.action) == null ? void 0 : _b.mergeParams(
1797
+ {
1798
+ values: withoutDesktopRouteOwnerFields((_a = ctx.action) == null ? void 0 : _a.params.values)
1799
+ },
1800
+ {
1801
+ values: "overwrite"
1802
+ }
1803
+ );
1804
+ await next();
1805
+ }
1806
+ function getDesktopRoutePortalContext(context) {
1807
+ var _a;
1808
+ if (!isRecordLike(context)) {
1809
+ return;
1810
+ }
1811
+ const ctx = context;
1812
+ if (((_a = ctx.action) == null ? void 0 : _a.resourceName) !== "desktopRoutes" || !hasActionParam(ctx.action.params, "portal")) {
1813
+ return;
1814
+ }
1815
+ return ctx;
1816
+ }
1817
+ async function resolveRequiredDesktopRouteScope(ctx, transaction) {
1818
+ const portalRequest = await findRequestedMultiPortal(ctx, transaction);
1819
+ if (!portalRequest.scope) {
1820
+ ctx.throw(400, "Invalid portal scope");
1821
+ }
1822
+ return portalRequest.scope;
1823
+ }
1824
+ function withDesktopRouteScopeFilter(filter, scope) {
1825
+ return {
1826
+ ...isRecordLike(filter) ? filter : {},
1827
+ ...scope.filter
1828
+ };
1829
+ }
1830
+ class MultiPortalDesktopRouteRepository extends import_database.Repository {
1831
+ async withAuthoritativeScopeTransaction(existingTransaction, callback) {
1832
+ if (existingTransaction) {
1833
+ return callback(existingTransaction);
1834
+ }
1835
+ return this.database.sequelize.transaction(callback);
1836
+ }
1837
+ async create(options) {
1838
+ const ctx = getDesktopRoutePortalContext(options.context);
1839
+ if (!ctx) {
1840
+ return super.create(options);
1841
+ }
1842
+ return this.withAuthoritativeScopeTransaction(options.transaction, async (transaction) => {
1843
+ const scope = await resolveRequiredDesktopRouteScope(ctx, transaction);
1844
+ const values = withDesktopRouteEffectiveOwner(options.values, scope);
1845
+ await assertDesktopRouteParentsBelongToScope(ctx, scope, values, transaction);
1846
+ return super.create({
1847
+ ...options,
1848
+ values,
1849
+ transaction
1850
+ });
1851
+ });
1852
+ }
1853
+ async update(options) {
1854
+ const ctx = getDesktopRoutePortalContext(options.context);
1855
+ if (!ctx) {
1856
+ return super.update(options);
1857
+ }
1858
+ return this.withAuthoritativeScopeTransaction(options.transaction, async (transaction) => {
1859
+ const scope = await resolveRequiredDesktopRouteScope(ctx, transaction);
1860
+ await assertDesktopRouteMutationTargetsBelongToScope(ctx, scope, transaction, options);
1861
+ const values = withoutDesktopRouteOwnerFields(options.values);
1862
+ await assertDesktopRouteParentsBelongToScope(ctx, scope, values, transaction);
1863
+ return super.update({
1864
+ ...options,
1865
+ filter: withDesktopRouteScopeFilter(options.filter, scope),
1866
+ values,
1867
+ transaction
1868
+ });
1869
+ });
1870
+ }
1871
+ async updateOrCreate(options) {
1872
+ const ctx = getDesktopRoutePortalContext(options.context);
1873
+ if (!ctx) {
1874
+ return super.updateOrCreate(options);
1875
+ }
1876
+ return this.withAuthoritativeScopeTransaction(options.transaction, async (transaction) => {
1877
+ const scope = await resolveRequiredDesktopRouteScope(ctx, transaction);
1878
+ const values = withDesktopRouteEffectiveOwner(options.values, scope);
1879
+ await assertDesktopRouteParentsBelongToScope(ctx, scope, values, transaction);
1880
+ await assertDesktopRouteUpsertMatchesBelongToScope(ctx, scope, values, options.filterKeys, transaction);
1881
+ return super.updateOrCreate({
1882
+ ...options,
1883
+ values,
1884
+ transaction
1885
+ });
1886
+ });
1887
+ }
1888
+ }
1889
+ async function guardDesktopRouteMoveScope(ctx, next) {
1890
+ var _a;
1891
+ const portalRequest = await findRequestedMultiPortal(ctx);
1892
+ if (!portalRequest.requested) {
1893
+ await next();
1894
+ return;
1895
+ }
1896
+ const params = ((_a = ctx.action) == null ? void 0 : _a.params) ?? {};
1897
+ const sourceId = params.sourceId;
1898
+ const targetId = params.targetId;
1899
+ const hasTargetId = targetId !== void 0 && targetId !== null;
1900
+ if (typeof sourceId !== "string" && typeof sourceId !== "number" || hasTargetId && typeof targetId !== "string" && typeof targetId !== "number" || params.sortField !== void 0 && params.sortField !== "sort") {
1901
+ ctx.throw(400, "Invalid desktop route move scope");
1902
+ return;
1903
+ }
1904
+ await ctx.db.sequelize.transaction(async (transaction) => {
1905
+ const scope = await resolveRequiredDesktopRouteScope(ctx, transaction);
1906
+ const routeIds = uniqueDesktopRouteIds(hasTargetId ? [sourceId, targetId] : [sourceId]);
1907
+ const allRoutes = await ctx.db.getRepository("desktopRoutes").find({
1908
+ fields: ["id", "parentId", "sort"],
1909
+ filter: {
1910
+ id: routeIds
1911
+ },
1912
+ lock: transaction.LOCK.UPDATE,
1913
+ transaction
1914
+ });
1915
+ const scopedRoutes = await ctx.db.getRepository("desktopRoutes").find({
1916
+ fields: ["id", "parentId", "sort"],
1917
+ filter: {
1918
+ id: routeIds,
1919
+ ...scope.filter
1920
+ },
1921
+ transaction
1922
+ });
1923
+ if (normalizeDesktopRouteTargetIds(allRoutes).join(",") !== normalizeDesktopRouteTargetIds(scopedRoutes).join(",") || scopedRoutes.length !== routeIds.length) {
1924
+ ctx.throw(400, "Move target does not belong to the requested portal scope");
1925
+ return;
1926
+ }
1927
+ const routesById = new Map(
1928
+ scopedRoutes.map((route) => [String(route.get("id")), route])
1929
+ );
1930
+ let source = routesById.get(String(sourceId));
1931
+ const target = hasTargetId ? routesById.get(String(targetId)) : void 0;
1932
+ if (!source || hasTargetId && !target) {
1933
+ ctx.throw(400, "Move target does not belong to the requested portal scope");
1934
+ return;
1935
+ }
1936
+ const targetScope = isRecordLike(params.targetScope) ? params.targetScope : void 0;
1937
+ const requestedParentId = targetScope == null ? void 0 : targetScope.parentId;
1938
+ const parentIds = uniqueDesktopRouteIds(
1939
+ [source.get("parentId"), target == null ? void 0 : target.get("parentId"), requestedParentId].filter(
1940
+ (id) => typeof id === "string" || typeof id === "number"
1941
+ )
1942
+ );
1943
+ if (parentIds.length) {
1944
+ const parents = await ctx.db.getRepository("desktopRoutes").find({
1945
+ fields: ["id"],
1946
+ filter: {
1947
+ id: parentIds,
1948
+ ...scope.filter
1949
+ },
1950
+ transaction
1951
+ });
1952
+ if (normalizeDesktopRouteTargetIds(parents).join(",") !== parentIds.map(String).sort().join(",")) {
1953
+ ctx.throw(400, "Move parent does not belong to the requested portal scope");
1954
+ return;
1955
+ }
1956
+ }
1957
+ const updateSource = async (values, silent) => {
1958
+ const [updatedSource] = await ctx.db.getRepository("desktopRoutes").update({
1959
+ filterByTk: sourceId,
1960
+ values,
1961
+ silent,
1962
+ transaction
1963
+ });
1964
+ if (!updatedSource) {
1965
+ ctx.throw(400, "Move source does not belong to the requested portal scope");
1966
+ }
1967
+ source = updatedSource;
1968
+ };
1969
+ if (target) {
1970
+ const targetParentId = target.get("parentId");
1971
+ if (source.get("parentId") !== targetParentId) {
1972
+ await updateSource({ parentId: targetParentId }, false);
1973
+ }
1974
+ const sourceSort = source.get("sort");
1975
+ let targetSort = target.get("sort");
1976
+ if (typeof sourceSort !== "number" || typeof targetSort !== "number") {
1977
+ ctx.throw(400, "Invalid desktop route sort value");
1978
+ return;
1979
+ }
1980
+ if (params.method === "insertAfter") {
1981
+ targetSort += 1;
1982
+ }
1983
+ const movesForward = targetSort > sourceSort;
1984
+ const affectedRoutes = await ctx.db.getRepository("desktopRoutes").find({
1985
+ fields: ["id"],
1986
+ filter: {
1987
+ ...scope.filter,
1988
+ parentId: targetParentId ?? null,
1989
+ sort: movesForward ? {
1990
+ $gt: sourceSort,
1991
+ $lte: targetSort
1992
+ } : {
1993
+ $lt: sourceSort,
1994
+ $gte: targetSort
1995
+ }
1996
+ },
1997
+ transaction
1998
+ });
1999
+ const affectedRouteIds = affectedRoutes.map((route) => route.get("id"));
2000
+ if (affectedRouteIds.length) {
2001
+ await ctx.db.getCollection("desktopRoutes").model.increment("sort", {
2002
+ where: {
2003
+ id: {
2004
+ [import_database.Op.in]: affectedRouteIds
2005
+ }
2006
+ },
2007
+ by: movesForward ? -1 : 1,
2008
+ silent: true,
2009
+ transaction
2010
+ });
2011
+ }
2012
+ await updateSource({ sort: targetSort }, true);
2013
+ }
2014
+ if (requestedParentId && source.get("parentId") !== requestedParentId) {
2015
+ await updateSource({ parentId: requestedParentId }, false);
2016
+ if (params.method === "prepend") {
2017
+ await updateSource({ sort: 0 }, true);
2018
+ }
2019
+ }
2020
+ if (params.sticky) {
2021
+ await updateSource({ sort: 0 }, true);
2022
+ }
2023
+ });
2024
+ ctx.body = "ok";
2025
+ }
2026
+ function normalizeDesktopRouteFilterByTk(filterByTk) {
2027
+ const values = Array.isArray(filterByTk) ? filterByTk : [filterByTk];
2028
+ return uniqueDesktopRouteIds(
2029
+ values.filter((value) => typeof value === "string" || typeof value === "number")
2030
+ );
2031
+ }
2032
+ async function getDesktopRouteDestroyOwnerScope(ctx, transaction) {
2033
+ var _a, _b, _c, _d;
2034
+ const hasPortalScope = hasActionParam((_a = ctx.action) == null ? void 0 : _a.params, "portal");
2035
+ const hasLayoutScope = hasActionParam((_b = ctx.action) == null ? void 0 : _b.params, "layout");
2036
+ const portalUid = getExplicitRequestedLayoutUid((_c = ctx.action) == null ? void 0 : _c.params.portal);
2037
+ const layoutUid = getExplicitRequestedLayoutUid((_d = ctx.action) == null ? void 0 : _d.params.layout);
2038
+ if (hasPortalScope && hasLayoutScope) {
2039
+ ctx.throw(400, "layout and portal cannot be used together");
2040
+ return;
2041
+ }
2042
+ if (hasPortalScope) {
2043
+ if (!portalUid) {
2044
+ ctx.throw(400, "Invalid portal scope");
2045
+ return;
2046
+ }
2047
+ const portalRequest = await findRequestedMultiPortal(ctx, transaction);
2048
+ if (!portalRequest.scope) {
2049
+ ctx.throw(400, "Invalid portal scope");
2050
+ return;
2051
+ }
2052
+ return portalRequest.scope;
2053
+ }
2054
+ if (!hasLayoutScope) {
2055
+ return;
2056
+ }
2057
+ if (!layoutUid) {
2058
+ ctx.throw(400, "Invalid layout scope");
2059
+ return;
2060
+ }
2061
+ const layout = await ctx.db.getRepository("uiLayouts").findOne({
2062
+ fields: ["uid"],
2063
+ filter: {
2064
+ uid: layoutUid
2065
+ },
2066
+ transaction
2067
+ });
2068
+ if (!layout) {
2069
+ ctx.throw(400, "Invalid layout scope");
2070
+ return;
2071
+ }
2072
+ return {
2073
+ filter: {
2074
+ "uiLayouts.uid": layoutUid
2075
+ },
2076
+ relation: "uiLayouts",
2077
+ uid: layoutUid
2078
+ };
2079
+ }
2080
+ async function findDesktopRouteDestroyRoots(ctx, transaction, scopeFilter) {
2081
+ var _a, _b, _c;
2082
+ const filterByTk = normalizeDesktopRouteFilterByTk((_a = ctx.action) == null ? void 0 : _a.params.filterByTk);
2083
+ const actionFilter = isRecordLike((_b = ctx.action) == null ? void 0 : _b.params.filter) ? (_c = ctx.action) == null ? void 0 : _c.params.filter : void 0;
2084
+ if (!filterByTk.length && !actionFilter) {
2085
+ return [];
2086
+ }
2087
+ const records = await ctx.db.getRepository("desktopRoutes").find({
2088
+ fields: ["id"],
2089
+ filter: {
2090
+ ...actionFilter ?? {},
2091
+ ...scopeFilter ?? {},
2092
+ ...filterByTk.length ? { id: filterByTk } : {}
2093
+ },
2094
+ ...scopeFilter ? {} : { lock: transaction.LOCK.UPDATE },
2095
+ transaction
2096
+ });
2097
+ return collectDesktopRouteIds(records);
2098
+ }
2099
+ async function findDesktopRouteSubtreeIds(ctx, rootId, transaction) {
2100
+ const rootRoutes = await ctx.db.getRepository("desktopRoutes").find({
2101
+ fields: ["id"],
2102
+ filter: {
2103
+ id: rootId
2104
+ },
2105
+ lock: transaction.LOCK.UPDATE,
2106
+ transaction
2107
+ });
2108
+ const routeIds = collectDesktopRouteIds(rootRoutes);
2109
+ let parentIds = [...routeIds];
2110
+ while (parentIds.length) {
2111
+ const children = await ctx.db.getRepository("desktopRoutes").find({
2112
+ fields: ["id"],
2113
+ filter: {
2114
+ parentId: parentIds
2115
+ },
2116
+ lock: transaction.LOCK.UPDATE,
2117
+ sort: ["id"],
2118
+ transaction
2119
+ });
2120
+ const childIds = collectDesktopRouteIds(children).filter((routeId) => !routeIds.includes(routeId));
2121
+ routeIds.push(...childIds);
2122
+ parentIds = childIds;
2123
+ }
2124
+ return routeIds;
2125
+ }
2126
+ function getDesktopRouteOwnerUids(route, relation) {
2127
+ const owners = getRecordField(route, relation);
2128
+ if (!Array.isArray(owners)) {
2129
+ return [];
2130
+ }
2131
+ return owners.map((owner) => getRecordField(owner, "uid")).filter((uid) => typeof uid === "string" && !!uid);
2132
+ }
2133
+ function desktopRouteHasOwnerOutsideScope(route, scope) {
2134
+ const multiPortalUids = getDesktopRouteOwnerUids(route, "multiPortals");
2135
+ const uiLayoutUids = getDesktopRouteOwnerUids(route, "uiLayouts");
2136
+ if (scope.relation === "multiPortals") {
2137
+ return multiPortalUids.some((uid) => uid !== scope.uid) || uiLayoutUids.length > 0;
2138
+ }
2139
+ return uiLayoutUids.some((uid) => uid !== scope.uid) || multiPortalUids.length > 0;
2140
+ }
2141
+ async function detachDesktopRouteTreeOwner(ctx, routeIds, scope, transaction) {
2142
+ if (scope.relation === "multiPortals") {
2143
+ await ctx.db.getRepository("rolesMultiPortalDesktopRoutes").destroy({
2144
+ filter: {
2145
+ multiPortalUid: scope.uid,
2146
+ desktopRouteId: routeIds
2147
+ },
2148
+ transaction
2149
+ });
2150
+ }
2151
+ for (const routeId of routeIds) {
2152
+ await ctx.db.getRepository(`desktopRoutes.${scope.relation}`, routeId).remove({
2153
+ tk: scope.uid,
2154
+ transaction
2155
+ });
2156
+ }
2157
+ }
2158
+ async function detachOwnerAndFindDestroyRoots(ctx, routeIds, scope, transaction) {
2159
+ var _a;
2160
+ await ctx.db.getRepository("desktopRoutes").find({
2161
+ fields: ["id"],
2162
+ filter: {
2163
+ id: routeIds
2164
+ },
2165
+ lock: transaction.LOCK.UPDATE,
2166
+ transaction
2167
+ });
2168
+ const routes = await ctx.db.getRepository("desktopRoutes").find({
2169
+ fields: ["id", "parentId"],
2170
+ appends: ["multiPortals", "uiLayouts"],
2171
+ filter: {
2172
+ id: routeIds
2173
+ },
2174
+ transaction
2175
+ });
2176
+ const routesById = new Map(routes.map((route) => [String(route.get("id")), route]));
2177
+ const ownerlessRouteIds = new Set(
2178
+ routes.filter((route) => !desktopRouteHasOwnerOutsideScope(route, scope)).map((route) => String(route.get("id")))
2179
+ );
2180
+ await detachDesktopRouteTreeOwner(ctx, routeIds, scope, transaction);
2181
+ for (const route of routes) {
2182
+ if (ownerlessRouteIds.has(String(route.get("id")))) {
2183
+ continue;
2184
+ }
2185
+ let parentId = route.get("parentId");
2186
+ const visitedParentIds = /* @__PURE__ */ new Set();
2187
+ while (parentId !== null && parentId !== void 0 && ownerlessRouteIds.has(String(parentId))) {
2188
+ const normalizedParentId = String(parentId);
2189
+ if (visitedParentIds.has(normalizedParentId)) {
2190
+ ctx.throw(400, "Invalid desktop route tree");
2191
+ return [];
2192
+ }
2193
+ visitedParentIds.add(normalizedParentId);
2194
+ parentId = ((_a = routesById.get(normalizedParentId)) == null ? void 0 : _a.get("parentId")) ?? null;
2195
+ }
2196
+ if (route.get("parentId") !== parentId) {
2197
+ await ctx.db.getRepository("desktopRoutes").update({
2198
+ filterByTk: route.get("id"),
2199
+ values: {
2200
+ parentId
2201
+ },
2202
+ transaction
2203
+ });
2204
+ }
2205
+ }
2206
+ return routes.filter((route) => {
2207
+ if (!ownerlessRouteIds.has(String(route.get("id")))) {
2208
+ return false;
2209
+ }
2210
+ const parentId = route.get("parentId");
2211
+ return parentId === null || parentId === void 0 || !ownerlessRouteIds.has(String(parentId));
2212
+ }).map((route) => route.get("id"));
2213
+ }
2214
+ async function destroyScopedDesktopRoutes(ctx, next) {
2215
+ var _a, _b;
2216
+ if (!hasActionParam((_a = ctx.action) == null ? void 0 : _a.params, "portal") && !hasActionParam((_b = ctx.action) == null ? void 0 : _b.params, "layout")) {
2217
+ await next();
2218
+ return;
2219
+ }
2220
+ const result = await ctx.db.sequelize.transaction(async (transaction) => {
2221
+ const scope = await getDesktopRouteDestroyOwnerScope(ctx, transaction);
2222
+ if (!scope) {
2223
+ return true;
2224
+ }
2225
+ const allRootIds = (await findDesktopRouteDestroyRoots(ctx, transaction)).sort(
2226
+ (left, right) => String(left).localeCompare(String(right))
2227
+ );
2228
+ const rootIds = (await findDesktopRouteDestroyRoots(ctx, transaction, scope.filter)).sort(
2229
+ (left, right) => String(left).localeCompare(String(right))
2230
+ );
2231
+ if (allRootIds.map(String).join(",") !== rootIds.map(String).join(",")) {
2232
+ ctx.throw(400, "Desktop route does not belong to the requested portal scope");
2233
+ return true;
2234
+ }
2235
+ const routeTrees = [];
2236
+ for (const rootId of rootIds) {
2237
+ routeTrees.push({
2238
+ rootId,
2239
+ routeIds: await findDesktopRouteSubtreeIds(ctx, rootId, transaction)
2240
+ });
2241
+ }
2242
+ const rootRouteTrees = routeTrees.filter(
2243
+ (routeTree, index) => !routeTrees.some(
2244
+ (candidate, candidateIndex) => candidateIndex !== index && candidate.routeIds.includes(routeTree.rootId)
2245
+ )
2246
+ );
2247
+ const physicalDestroyRootIds = [];
2248
+ for (const routeTree of rootRouteTrees) {
2249
+ physicalDestroyRootIds.push(
2250
+ ...await detachOwnerAndFindDestroyRoots(ctx, routeTree.routeIds, scope, transaction)
2251
+ );
2252
+ }
2253
+ if (!physicalDestroyRootIds.length) {
2254
+ return true;
2255
+ }
2256
+ return ctx.db.getRepository("desktopRoutes").destroy({
2257
+ context: ctx,
2258
+ filterByTk: physicalDestroyRootIds.length === 1 ? physicalDestroyRootIds[0] : physicalDestroyRootIds,
2259
+ transaction
2260
+ });
2261
+ });
2262
+ ctx.status = 200;
2263
+ ctx.body = result;
2264
+ }
1479
2265
  async function listEnabledMultiPortals(ctx, next) {
1480
2266
  const records = await ctx.db.getRepository("multiPortals").find({
1481
- filter: withCustomMultiPortalFilter({
2267
+ filter: {
1482
2268
  enabled: true,
1483
- "uiLayout.enabled": true
1484
- }),
1485
- fields: [...MULTI_PORTAL_RUNTIME_QUERY_FIELDS],
1486
- appends: ["uiLayout"],
2269
+ uiLayoutUid: {
2270
+ $in: [...import_constants.MULTI_PORTAL_UI_LAYOUT_UIDS]
2271
+ }
2272
+ },
2273
+ fields: [...MULTI_PORTAL_RUNTIME_FIELDS],
1487
2274
  sort: ["uid"]
1488
2275
  });
1489
2276
  ctx.body = records.map((record) => pickMultiPortalRuntimeFields(record));
1490
2277
  await next();
1491
2278
  }
1492
- async function listAccessibleMultiPortals(ctx, next) {
1493
- const accessiblePortalUids = await listCurrentRoleAccessibleMultiPortalUids(ctx);
1494
- if (Array.isArray(accessiblePortalUids) && accessiblePortalUids.length === 0) {
1495
- ctx.body = [];
1496
- await next();
1497
- return;
2279
+ const DEFAULT_MULTI_PORTAL_RESPONSE_FIELDS = ["uid", "portalType", "routePath"];
2280
+ function getDefaultMultiPortalType(record) {
2281
+ const portalType = record.get("portalType");
2282
+ if (portalType === null || portalType === void 0) {
2283
+ return "no-code";
1498
2284
  }
1499
- const filter = {
1500
- enabled: true,
1501
- "uiLayout.enabled": true
2285
+ return isMultiPortalSeedType(portalType) ? portalType : null;
2286
+ }
2287
+ function pickDefaultMultiPortalFields(record) {
2288
+ return {
2289
+ uid: String(record.get("uid")),
2290
+ portalType: getDefaultMultiPortalType(record) || "no-code",
2291
+ routePath: String(record.get("routePath"))
1502
2292
  };
1503
- if (Array.isArray(accessiblePortalUids)) {
1504
- filter.uid = accessiblePortalUids;
2293
+ }
2294
+ async function findEnabledDefaultMultiPortal(ctx, transaction) {
2295
+ const record = await ctx.db.getRepository("multiPortals").findOne({
2296
+ filter: {
2297
+ enabled: true,
2298
+ isDefault: true
2299
+ },
2300
+ fields: [...DEFAULT_MULTI_PORTAL_RESPONSE_FIELDS, "uiLayoutUid"],
2301
+ transaction
2302
+ });
2303
+ if (!record) {
2304
+ return null;
2305
+ }
2306
+ const portalType = getDefaultMultiPortalType(record);
2307
+ if (!portalType) {
2308
+ return null;
2309
+ }
2310
+ if (!(0, import_constants.isMultiPortalUiLayoutUid)(record.get("uiLayoutUid"))) {
2311
+ return null;
2312
+ }
2313
+ return record;
2314
+ }
2315
+ async function getDefaultMultiPortal(ctx, next) {
2316
+ const record = await findEnabledDefaultMultiPortal(ctx);
2317
+ ctx.body = record ? pickDefaultMultiPortalFields(record) : null;
2318
+ ctx.status = 200;
2319
+ await next();
2320
+ }
2321
+ async function setDefaultMultiPortal(ctx, next) {
2322
+ var _a;
2323
+ const filterByTk = (_a = ctx.action) == null ? void 0 : _a.params.filterByTk;
2324
+ if (filterByTk === void 0 || filterByTk === null || filterByTk === "") {
2325
+ ctx.throw(400, ctx.t("filterByTk is required", { ns: import_constants.NAMESPACE }));
2326
+ return;
1505
2327
  }
2328
+ const repository = ctx.db.getRepository("multiPortals");
2329
+ const updated = await ctx.db.sequelize.transaction(async (transaction) => {
2330
+ const target = await repository.findOne({
2331
+ filterByTk,
2332
+ fields: [...DEFAULT_MULTI_PORTAL_RESPONSE_FIELDS, "enabled", "uiLayoutUid"],
2333
+ lock: transaction.LOCK.UPDATE,
2334
+ transaction
2335
+ });
2336
+ if (!target) {
2337
+ ctx.throw(404, ctx.t("Portal not found", { ns: import_constants.NAMESPACE }));
2338
+ return null;
2339
+ }
2340
+ if (target.get("enabled") !== true) {
2341
+ ctx.throw(400, ctx.t("Disabled Portal cannot be set as default", { ns: import_constants.NAMESPACE }));
2342
+ return null;
2343
+ }
2344
+ const portalType = getDefaultMultiPortalType(target);
2345
+ if (!portalType) {
2346
+ ctx.throw(400, ctx.t("Unsupported Portal type cannot be set as default", { ns: import_constants.NAMESPACE }));
2347
+ return null;
2348
+ }
2349
+ if (!(0, import_constants.isMultiPortalUiLayoutUid)(target.get("uiLayoutUid"))) {
2350
+ ctx.throw(400, ctx.t("Portal device configuration is invalid", { ns: import_constants.NAMESPACE }));
2351
+ return null;
2352
+ }
2353
+ await repository.update({
2354
+ filter: { isDefault: true },
2355
+ values: { isDefault: null },
2356
+ transaction
2357
+ });
2358
+ await repository.update({
2359
+ filterByTk,
2360
+ values: { isDefault: true },
2361
+ transaction
2362
+ });
2363
+ return repository.findOne({
2364
+ filterByTk,
2365
+ fields: [...DEFAULT_MULTI_PORTAL_RESPONSE_FIELDS],
2366
+ transaction
2367
+ });
2368
+ });
2369
+ ctx.body = updated ? pickDefaultMultiPortalFields(updated) : null;
2370
+ await next();
2371
+ }
2372
+ async function listAccessibleMultiPortals(ctx, next) {
2373
+ const accessiblePortalUids = await listCurrentRoleAccessibleMultiPortalUids(ctx);
1506
2374
  const records = await ctx.db.getRepository("multiPortals").find({
1507
- filter: withCustomMultiPortalFilter(filter),
1508
- fields: [...MULTI_PORTAL_ACCESSIBLE_QUERY_FIELDS],
1509
- appends: ["uiLayout"],
2375
+ filter: {
2376
+ enabled: true,
2377
+ uiLayoutUid: {
2378
+ $in: [...import_constants.MULTI_PORTAL_UI_LAYOUT_UIDS]
2379
+ }
2380
+ },
2381
+ fields: [...MULTI_PORTAL_ACCESSIBLE_FIELDS],
1510
2382
  sort: ["uid"]
1511
2383
  });
1512
- ctx.body = records.map((record) => pickMultiPortalAccessibleFields(record));
2384
+ const accessiblePortalUidSet = Array.isArray(accessiblePortalUids) ? new Set(accessiblePortalUids) : void 0;
2385
+ const accessibleRecords = records.filter(
2386
+ (record) => (0, import_constants.isDefaultLayoutMultiPortalUid)(record.get("uid")) || !accessiblePortalUidSet || accessiblePortalUidSet.has(String(record.get("uid")))
2387
+ );
2388
+ ctx.body = accessibleRecords.map((record) => pickMultiPortalAccessibleFields(record));
1513
2389
  await next();
1514
2390
  }
1515
- async function grantDefaultRouteAccessToNewMultiPortalRoutes(ctx, multiPortalUid, desktopRouteIds) {
2391
+ async function grantDefaultRouteAccessToNewMultiPortalRoutes(ctx, multiPortalUid, desktopRouteIds, transaction) {
1516
2392
  if (!desktopRouteIds.length) {
1517
2393
  return;
1518
2394
  }
1519
- if (!ctx.db.getCollection("rolesMultiPortalRoutePolicies") || !ctx.db.getCollection("rolesMultiPortalDesktopRoutes")) {
1520
- return;
1521
- }
1522
2395
  const routePolicies = await ctx.db.getRepository("rolesMultiPortalRoutePolicies").find({
1523
2396
  fields: ["roleName"],
1524
2397
  filter: {
1525
2398
  multiPortalUid,
1526
2399
  allowNewMenu: true
1527
- }
2400
+ },
2401
+ transaction
1528
2402
  });
1529
2403
  const roleNames = routePolicies.map((routePolicy) => routePolicy.get("roleName")).filter((roleName) => typeof roleName === "string" && !!roleName);
1530
2404
  if (!roleNames.length) {
@@ -1540,26 +2414,52 @@ async function grantDefaultRouteAccessToNewMultiPortalRoutes(ctx, multiPortalUid
1540
2414
  multiPortalUid,
1541
2415
  desktopRouteId
1542
2416
  },
1543
- context: ctx
2417
+ context: ctx,
2418
+ transaction
1544
2419
  });
1545
2420
  }
1546
2421
  }
1547
2422
  }
1548
- async function removeLayoutRoutePermissionsFromPortalOnlyRoutes(ctx, desktopRouteIds) {
1549
- if (!desktopRouteIds.length || !ctx.db.getCollection("rolesDesktopRoutes")) {
1550
- return;
1551
- }
1552
- const portalOnlyRouteIds = await getPortalOnlyDesktopRouteIds(ctx, desktopRouteIds);
1553
- if (portalOnlyRouteIds.size === 0) {
2423
+ async function removeLayoutRoutePermissionsFromPortalRoutes(ctx, desktopRouteIds, transaction) {
2424
+ if (!desktopRouteIds.length) {
1554
2425
  return;
1555
2426
  }
1556
2427
  await ctx.db.getRepository("rolesDesktopRoutes").destroy({
1557
2428
  filter: {
1558
- desktopRouteId: Array.from(portalOnlyRouteIds)
2429
+ desktopRouteId: desktopRouteIds
1559
2430
  },
1560
- context: ctx
2431
+ context: ctx,
2432
+ transaction
1561
2433
  });
1562
2434
  }
2435
+ async function reconcileCreatedDesktopRoutePermissions(desktopRoute, options) {
2436
+ const ctx = options == null ? void 0 : options.context;
2437
+ if (!ctx) {
2438
+ return;
2439
+ }
2440
+ const portalRequest = await findRequestedMultiPortal(ctx, options.transaction);
2441
+ if (!portalRequest.requested) {
2442
+ return;
2443
+ }
2444
+ const scope = portalRequest.scope;
2445
+ if (!scope) {
2446
+ ctx.throw(400, "Invalid portal scope");
2447
+ return;
2448
+ }
2449
+ if (scope.relation !== "multiPortals") {
2450
+ return;
2451
+ }
2452
+ const transaction = options.transaction;
2453
+ if (!transaction) {
2454
+ throw new Error("Portal desktop route creation requires a transaction");
2455
+ }
2456
+ const desktopRouteId = desktopRoute.get("id");
2457
+ if (typeof desktopRouteId !== "string" && typeof desktopRouteId !== "number") {
2458
+ throw new Error("Created Portal desktop route has no identifier");
2459
+ }
2460
+ await removeLayoutRoutePermissionsFromPortalRoutes(ctx, [desktopRouteId], transaction);
2461
+ await grantDefaultRouteAccessToNewMultiPortalRoutes(ctx, scope.portalUid, [desktopRouteId], transaction);
2462
+ }
1563
2463
  async function grantDefaultAccessToNewMultiPortal(db, multiPortal, options) {
1564
2464
  if (multiPortal.get("enabled") !== true) {
1565
2465
  return;
@@ -1568,7 +2468,7 @@ async function grantDefaultAccessToNewMultiPortal(db, multiPortal, options) {
1568
2468
  if (typeof multiPortalUid !== "string" || !multiPortalUid) {
1569
2469
  return;
1570
2470
  }
1571
- if (isDefaultMultiPortalUid(multiPortalUid)) {
2471
+ if ((0, import_constants.isDefaultLayoutMultiPortalUid)(multiPortalUid)) {
1572
2472
  return;
1573
2473
  }
1574
2474
  if (!db.getCollection("roles") || !db.getCollection("rolesMultiPortals")) {
@@ -1602,7 +2502,7 @@ async function grantDefaultAccessToNewMultiPortal(db, multiPortal, options) {
1602
2502
  transaction: options == null ? void 0 : options.transaction
1603
2503
  });
1604
2504
  }
1605
- if (!db.getCollection("rolesMultiPortalRoutePolicies")) {
2505
+ if (multiPortal.get("portalType") === "ai" || !db.getCollection("rolesMultiPortalRoutePolicies")) {
1606
2506
  return;
1607
2507
  }
1608
2508
  const routePolicyRepository = db.getRepository("rolesMultiPortalRoutePolicies");
@@ -1617,11 +2517,15 @@ async function grantDefaultAccessToNewMultiPortal(db, multiPortal, options) {
1617
2517
  class PluginMultiPortalServer extends import_server.Plugin {
1618
2518
  portalStorageTaskKeys = /* @__PURE__ */ new Set();
1619
2519
  portalStorageTasks = /* @__PURE__ */ new Map();
2520
+ deprecatedInitPortalEnvWarningEmitted = false;
1620
2521
  async afterAdd() {
1621
2522
  }
1622
2523
  getAppName() {
1623
2524
  return this.app.name || MAIN_APP_NAME;
1624
2525
  }
2526
+ getCurrentStorageAppName() {
2527
+ return normalizePortalStorageName(this.getAppName()) || MAIN_APP_NAME;
2528
+ }
1625
2529
  getMultiPortalStorageItem(multiPortal, previous = false) {
1626
2530
  const record = multiPortal;
1627
2531
  const readField = (field) => previous && typeof record.previous === "function" ? record.previous(field) : getRecordField(record, field);
@@ -1636,8 +2540,7 @@ class PluginMultiPortalServer extends import_server.Plugin {
1636
2540
  return {
1637
2541
  appName: this.getAppName(),
1638
2542
  portalName,
1639
- enabled: readField("enabled") === true,
1640
- config: getPortalStorageConfig(readField("options"))
2543
+ enabled: readField("enabled") === true
1641
2544
  };
1642
2545
  }
1643
2546
  warnPortalStorageSyncFailed(error) {
@@ -1647,6 +2550,14 @@ class PluginMultiPortalServer extends import_server.Plugin {
1647
2550
  error
1648
2551
  });
1649
2552
  }
2553
+ logPortalBuildHtml(item, status, reason) {
2554
+ var _a, _b;
2555
+ (_b = (_a = this.app.logger) == null ? void 0 : _a.info) == null ? void 0 : _b.call(_a, `Portal yarn build:html ${status} for ${item.appName}/${item.portalName}`, {
2556
+ appName: item.appName,
2557
+ portalName: item.portalName,
2558
+ reason
2559
+ });
2560
+ }
1650
2561
  getPortalStorageTaskKey(item) {
1651
2562
  return `${item.appName}/${item.portalName}`;
1652
2563
  }
@@ -1665,10 +2576,9 @@ class PluginMultiPortalServer extends import_server.Plugin {
1665
2576
  }
1666
2577
  await run();
1667
2578
  }
1668
- async removePortalStorageDist(item) {
1669
- await import_fs.default.promises.rm((0, import_utils.storagePathJoin)("portals", item.appName, item.portalName, "dist"), {
1670
- force: true,
1671
- recursive: true
2579
+ async removePortalStorageIndexHtml(item) {
2580
+ await import_fs.default.promises.rm((0, import_utils.storagePathJoin)("portals", item.appName, item.portalName, "dist", "index.html"), {
2581
+ force: true
1672
2582
  });
1673
2583
  }
1674
2584
  waitForPortalStorageTasks() {
@@ -1678,10 +2588,11 @@ class PluginMultiPortalServer extends import_server.Plugin {
1678
2588
  }
1679
2589
  return Promise.allSettled(tasks).then(() => void 0);
1680
2590
  }
1681
- async schedulePortalTemplateCopyAndBuild(item, template, portalDir) {
2591
+ async schedulePortalTemplateCopyAndBuild(item, template, portalDir, options) {
1682
2592
  var _a;
1683
2593
  const taskKey = this.getPortalStorageTaskKey(item);
1684
2594
  if (this.portalStorageTaskKeys.has(taskKey)) {
2595
+ this.logPortalBuildHtml(item, "skipped", "a storage task is already running");
1685
2596
  await ((_a = template.cleanup) == null ? void 0 : _a.call(template));
1686
2597
  return;
1687
2598
  }
@@ -1702,14 +2613,13 @@ class PluginMultiPortalServer extends import_server.Plugin {
1702
2613
  await copyPortalTemplate(template.dir, portalDir);
1703
2614
  await appendPortalStorageLog(logPath, `Default portal template copied to ${portalDir}.`);
1704
2615
  }
1705
- await ensurePortalStorageConfigFiles(portalDir, item);
1706
- await appendPortalStorageLog(logPath, `Portal configuration files updated in ${portalDir}.`);
1707
2616
  if (item.enabled) {
1708
- await buildPortalStorageItem(portalDir, item);
2617
+ this.logPortalBuildHtml(item, "requested", "storage directory was initialized");
2618
+ await buildPortalStorageItem(portalDir, item, options);
2619
+ this.logPortalBuildHtml(item, "completed", "yarn build:html finished successfully");
1709
2620
  return;
1710
2621
  }
1711
- await this.removePortalStorageDist(item);
1712
- await appendPortalStorageLog(logPath, `Portal dist directory removed for ${item.appName}/${item.portalName}.`);
2622
+ this.logPortalBuildHtml(item, "skipped", "the portal is disabled");
1713
2623
  } catch (error) {
1714
2624
  await appendPortalStorageLog(
1715
2625
  logPath,
@@ -1725,32 +2635,41 @@ class PluginMultiPortalServer extends import_server.Plugin {
1725
2635
  this.portalStorageTasks.set(taskKey, task);
1726
2636
  await task;
1727
2637
  }
1728
- async ensurePortalStorageItem(item) {
2638
+ async ensurePortalStorageItem(item, options = {}) {
1729
2639
  const portalDir = (0, import_utils.storagePathJoin)("portals", item.appName, item.portalName);
1730
2640
  const portalIndex = import_path.default.join(portalDir, "dist", "index.html");
2641
+ const logPath = getPortalStorageLogPath(item);
1731
2642
  if (!await pathExists(portalDir)) {
1732
- const template = await resolvePortalTemplate(getInitPortalTemplate(), getPortalStorageLogPath(item));
1733
- await this.schedulePortalTemplateCopyAndBuild(item, template, portalDir);
2643
+ const template = await resolvePortalTemplate(getInitPortalTemplate(), logPath);
2644
+ await this.schedulePortalTemplateCopyAndBuild(item, template, portalDir, options);
1734
2645
  return;
1735
2646
  }
1736
- await ensurePortalStorageConfigFiles(portalDir, item);
1737
2647
  if (item.enabled) {
1738
- if (!await pathExists(portalIndex)) {
1739
- await buildPortalStorageItem(portalDir, item);
2648
+ const hasPortalIndex = await pathExists(portalIndex);
2649
+ if (options.forceBuild || !hasPortalIndex) {
2650
+ this.logPortalBuildHtml(
2651
+ item,
2652
+ "requested",
2653
+ options.forceBuild ? "forceBuild is enabled" : "dist/index.html does not exist"
2654
+ );
2655
+ await buildPortalStorageItem(portalDir, item, options);
2656
+ this.logPortalBuildHtml(item, "completed", "yarn build:html finished successfully");
2657
+ } else {
2658
+ this.logPortalBuildHtml(item, "skipped", "dist/index.html already exists");
1740
2659
  }
1741
2660
  return;
1742
2661
  }
1743
- await this.removePortalStorageDist(item);
2662
+ this.logPortalBuildHtml(item, "skipped", "the portal is disabled");
1744
2663
  }
1745
- async syncMultiPortalStorageItem(multiPortal, options, syncPrevious = false) {
2664
+ async syncMultiPortalStorageItem(multiPortal, options, syncPrevious = false, forceBuild = false) {
1746
2665
  const currentItem = this.getMultiPortalStorageItem(multiPortal);
1747
2666
  const previousItem = syncPrevious ? this.getMultiPortalStorageItem(multiPortal, true) : null;
1748
2667
  await this.runPortalStorageTask(async () => {
1749
- if (previousItem && (!currentItem || previousItem.appName !== currentItem.appName || previousItem.portalName !== currentItem.portalName || !currentItem.enabled)) {
1750
- await this.removePortalStorageDist(previousItem);
2668
+ if (previousItem && (!currentItem || previousItem.appName !== currentItem.appName || previousItem.portalName !== currentItem.portalName)) {
2669
+ await this.removePortalStorageIndexHtml(previousItem);
1751
2670
  }
1752
2671
  if (currentItem) {
1753
- await this.ensurePortalStorageItem(currentItem);
2672
+ await this.ensurePortalStorageItem(currentItem, { ...options, forceBuild });
1754
2673
  }
1755
2674
  }, options);
1756
2675
  }
@@ -1793,12 +2712,12 @@ class PluginMultiPortalServer extends import_server.Plugin {
1793
2712
  await next();
1794
2713
  }
1795
2714
  async deployPortalDist(ctx, next) {
1796
- var _a, _b, _c, _d;
2715
+ var _a, _b, _c;
1797
2716
  const deployCtx = ctx;
1798
- const appName = normalizePortalStorageName(((_a = deployCtx.request.body) == null ? void 0 : _a.app) || MAIN_APP_NAME) || MAIN_APP_NAME;
1799
- const portalName = normalizePortalStorageName((_b = deployCtx.request.body) == null ? void 0 : _b.portal);
1800
- const basePath = trimString((_c = deployCtx.request.body) == null ? void 0 : _c.basePath);
1801
- const filePath = trimString((_d = deployCtx.request.file) == null ? void 0 : _d.path);
2717
+ const appName = this.getCurrentStorageAppName();
2718
+ const portalName = normalizePortalStorageName((_a = deployCtx.request.body) == null ? void 0 : _a.portal);
2719
+ const basePath = trimString((_b = deployCtx.request.body) == null ? void 0 : _b.basePath);
2720
+ const filePath = trimString((_c = deployCtx.request.file) == null ? void 0 : _c.path);
1802
2721
  try {
1803
2722
  if (!isValidPortalDeploySegment(appName)) {
1804
2723
  throw new Error("Invalid app");
@@ -1835,9 +2754,9 @@ class PluginMultiPortalServer extends import_server.Plugin {
1835
2754
  }
1836
2755
  }
1837
2756
  async pullPortalSource(ctx, next) {
1838
- var _a, _b;
1839
- const appName = normalizePortalStorageName(((_a = ctx.action.params.values) == null ? void 0 : _a.app) || MAIN_APP_NAME) || MAIN_APP_NAME;
1840
- const portalName = normalizePortalStorageName((_b = ctx.action.params.values) == null ? void 0 : _b.portal);
2757
+ var _a;
2758
+ const appName = this.getCurrentStorageAppName();
2759
+ const portalName = normalizePortalStorageName((_a = ctx.action.params.values) == null ? void 0 : _a.portal);
1841
2760
  try {
1842
2761
  if (!isValidPortalDeploySegment(appName)) {
1843
2762
  throw new Error("Invalid app");
@@ -1857,11 +2776,11 @@ class PluginMultiPortalServer extends import_server.Plugin {
1857
2776
  }
1858
2777
  }
1859
2778
  async pushPortalSource(ctx, next) {
1860
- var _a, _b, _c;
2779
+ var _a, _b;
1861
2780
  const sourceCtx = ctx;
1862
- const appName = normalizePortalStorageName(((_a = sourceCtx.request.body) == null ? void 0 : _a.app) || MAIN_APP_NAME) || MAIN_APP_NAME;
1863
- const portalName = normalizePortalStorageName((_b = sourceCtx.request.body) == null ? void 0 : _b.portal);
1864
- const filePath = trimString((_c = sourceCtx.request.file) == null ? void 0 : _c.path);
2781
+ const appName = this.getCurrentStorageAppName();
2782
+ const portalName = normalizePortalStorageName((_a = sourceCtx.request.body) == null ? void 0 : _a.portal);
2783
+ const filePath = trimString((_b = sourceCtx.request.file) == null ? void 0 : _b.path);
1865
2784
  try {
1866
2785
  if (!isValidPortalDeploySegment(appName)) {
1867
2786
  throw new Error("Invalid app");
@@ -1913,62 +2832,43 @@ class PluginMultiPortalServer extends import_server.Plugin {
1913
2832
  }
1914
2833
  async listAppPortalManifest(options) {
1915
2834
  const records = await this.db.getRepository("multiPortals").find({
1916
- filter: withCustomMultiPortalFilter({
2835
+ filter: {
1917
2836
  enabled: true,
1918
- "uiLayout.enabled": true
1919
- }),
1920
- fields: [...MULTI_PORTAL_ACCESSIBLE_QUERY_FIELDS],
1921
- appends: ["uiLayout"],
2837
+ uiLayoutUid: {
2838
+ $in: [...import_constants.MULTI_PORTAL_UI_LAYOUT_UIDS]
2839
+ }
2840
+ },
2841
+ fields: [...MULTI_PORTAL_ACCESSIBLE_FIELDS],
1922
2842
  sort: ["uid"],
1923
2843
  transaction: options == null ? void 0 : options.transaction
1924
2844
  });
1925
- return records.map((record) => {
1926
- const portal = pickMultiPortalAccessibleFields(record);
1927
- const uiLayout = portal.uiLayout;
1928
- const uid = typeof portal.uid === "string" ? portal.uid : "";
1929
- const title = typeof portal.title === "string" ? portal.title : "";
1930
- return {
1931
- uid,
1932
- title,
1933
- icon: typeof portal.icon === "string" ? portal.icon : null,
1934
- portalType: typeof portal.portalType === "string" ? portal.portalType : null,
1935
- routePath: String(portal.routePath || ""),
1936
- layout: typeof (uiLayout == null ? void 0 : uiLayout.layoutType) === "string" ? uiLayout.layoutType : null
1937
- };
1938
- });
1939
- }
1940
- async getUiLayout(uid, options) {
1941
- if (typeof uid !== "string" || !uid) {
1942
- return null;
1943
- }
1944
- return this.db.getRepository("uiLayouts").findOne({
1945
- filterByTk: uid,
1946
- transaction: options == null ? void 0 : options.transaction
2845
+ return records.flatMap((record) => {
2846
+ const item = this.toAppPortalManifestItem(record);
2847
+ return item ? [item] : [];
1947
2848
  });
1948
2849
  }
1949
- async toAppPortalManifestItem(multiPortal, options) {
2850
+ toAppPortalManifestItem(multiPortal) {
1950
2851
  const uid = getRecordField(multiPortal, "uid");
1951
2852
  const title = getRecordField(multiPortal, "title");
1952
2853
  const portalType = getRecordField(multiPortal, "portalType");
1953
2854
  const routePath = getRecordField(multiPortal, "routePath");
1954
2855
  const enabled = getRecordField(multiPortal, "enabled");
1955
- if (typeof uid !== "string" || !uid || isDefaultMultiPortalUid(uid) || typeof title !== "string" || typeof routePath !== "string" || !routePath || enabled !== true) {
2856
+ if (typeof uid !== "string" || !uid || typeof title !== "string" || typeof routePath !== "string" || !routePath || enabled !== true) {
1956
2857
  return null;
1957
2858
  }
1958
2859
  const uiLayoutUid = getRecordField(multiPortal, "uiLayoutUid");
1959
- const uiLayout = getRecordField(multiPortal, "uiLayout") || await this.getUiLayout(uiLayoutUid, options);
1960
- if (!uiLayout || getRecordField(uiLayout, "enabled") !== true) {
2860
+ const layout = (0, import_constants.getMultiPortalLayoutType)(uiLayoutUid);
2861
+ if (!layout) {
1961
2862
  return null;
1962
2863
  }
1963
2864
  const icon = getRecordField(multiPortal, "icon");
1964
- const layoutType = getRecordField(uiLayout, "layoutType");
1965
2865
  return {
1966
2866
  uid,
1967
2867
  title,
1968
2868
  icon: typeof icon === "string" ? icon : null,
1969
2869
  portalType: typeof portalType === "string" ? portalType : null,
1970
2870
  routePath,
1971
- layout: typeof layoutType === "string" ? layoutType : null
2871
+ layout
1972
2872
  };
1973
2873
  }
1974
2874
  async setAppManifestItem(item, options) {
@@ -2011,30 +2911,13 @@ class PluginMultiPortalServer extends import_server.Plugin {
2011
2911
  if (typeof uid !== "string" || !uid) {
2012
2912
  return;
2013
2913
  }
2014
- const item = await this.toAppPortalManifestItem(multiPortal, options);
2914
+ const item = this.toAppPortalManifestItem(multiPortal);
2015
2915
  if (item) {
2016
2916
  await this.setAppManifestItem(item, options);
2017
2917
  return;
2018
2918
  }
2019
2919
  await this.removeAppManifestItem(uid, options);
2020
2920
  }
2021
- async publishUiLayoutManifestItems(uiLayout, options) {
2022
- const uiLayoutUid = getRecordField(uiLayout, "uid");
2023
- if (typeof uiLayoutUid !== "string" || !uiLayoutUid) {
2024
- return;
2025
- }
2026
- const records = await this.db.getRepository("multiPortals").find({
2027
- filter: withCustomMultiPortalFilter({
2028
- uiLayoutUid
2029
- }),
2030
- fields: [...MULTI_PORTAL_ACCESSIBLE_QUERY_FIELDS],
2031
- transaction: options == null ? void 0 : options.transaction
2032
- });
2033
- for (const record of records) {
2034
- record.set("uiLayout", uiLayout);
2035
- await this.publishAppManifestItem(record, options);
2036
- }
2037
- }
2038
2921
  async reconcilePortalStorage(options) {
2039
2922
  const records = await this.db.getRepository("multiPortals").find({
2040
2923
  filter: {
@@ -2085,6 +2968,10 @@ class PluginMultiPortalServer extends import_server.Plugin {
2085
2968
  }
2086
2969
  }
2087
2970
  async beforeLoad() {
2971
+ this.app.db.registerRepositories({ MultiPortalDesktopRouteRepository });
2972
+ this.app.resourceManager.registerPreActionHandler("roles:check", checkMultiPortalAccessForRolesCheck);
2973
+ this.app.resourceManager.registerPreActionHandler("desktopRoutes:list", addDesktopRouteEffectiveScopeFilter);
2974
+ this.app.resourceManager.registerPreActionHandler("desktopRoutes:get", addDesktopRouteEffectiveScopeFilter);
2088
2975
  this.app.resourceManager.registerPreActionHandler(
2089
2976
  "desktopRoutes:listAccessible",
2090
2977
  addMultiPortalListAccessibleGuard
@@ -2094,25 +2981,30 @@ class PluginMultiPortalServer extends import_server.Plugin {
2094
2981
  "desktopRoutes:listRolePermissionTargets",
2095
2982
  mapMultiPortalLayoutToUiLayoutForRolePermissionTargets
2096
2983
  );
2984
+ this.app.resourceManager.registerPreActionHandler("multiPortals:create", preventDirectDefaultPortalMutation);
2985
+ this.app.resourceManager.registerPreActionHandler("multiPortals:create", validateMultiPortalUiLayoutUidWrite);
2097
2986
  this.app.resourceManager.registerPreActionHandler("multiPortals:create", captureSkipCreatePortalDirectory);
2098
2987
  this.app.resourceManager.registerPreActionHandler("multiPortals:create", normalizeMultiPortalSlugValues);
2099
- this.app.resourceManager.registerPreActionHandler("multiPortals:create", preventUiLayoutRouteNameConflict);
2100
- this.app.resourceManager.registerPreActionHandler("multiPortals:update", protectDefaultMultiPortalUpdate);
2988
+ this.app.resourceManager.registerPreActionHandler("multiPortals:update", preventDirectDefaultPortalMutation);
2989
+ this.app.resourceManager.registerPreActionHandler("multiPortals:update", validateMultiPortalUiLayoutUidWrite);
2101
2990
  this.app.resourceManager.registerPreActionHandler("multiPortals:update", normalizeMultiPortalSlugValues);
2102
- this.app.resourceManager.registerPreActionHandler("multiPortals:update", preventUiLayoutRouteNameConflict);
2991
+ this.app.resourceManager.registerPreActionHandler("multiPortals:firstOrCreate", preventDirectDefaultPortalMutation);
2992
+ this.app.resourceManager.registerPreActionHandler(
2993
+ "multiPortals:firstOrCreate",
2994
+ validateMultiPortalUiLayoutUidWrite
2995
+ );
2103
2996
  this.app.resourceManager.registerPreActionHandler("multiPortals:firstOrCreate", captureSkipCreatePortalDirectory);
2104
2997
  this.app.resourceManager.registerPreActionHandler("multiPortals:firstOrCreate", normalizeMultiPortalSlugValues);
2105
- this.app.resourceManager.registerPreActionHandler("multiPortals:firstOrCreate", preventUiLayoutRouteNameConflict);
2106
- this.app.resourceManager.registerPreActionHandler("desktopRoutes:create", captureExplicitDesktopRouteUiLayouts, {
2107
- before: UI_LAYOUT_DESKTOP_ROUTE_WRITE_LAYOUT_HANDLER_TAG
2108
- });
2109
2998
  this.app.resourceManager.registerPreActionHandler(
2110
- "desktopRoutes:updateOrCreate",
2111
- captureExplicitDesktopRouteUiLayouts,
2112
- {
2113
- before: UI_LAYOUT_DESKTOP_ROUTE_WRITE_LAYOUT_HANDLER_TAG
2114
- }
2999
+ "multiPortals:updateOrCreate",
3000
+ preventDirectDefaultPortalMutation
3001
+ );
3002
+ this.app.resourceManager.registerPreActionHandler(
3003
+ "multiPortals:updateOrCreate",
3004
+ validateMultiPortalUiLayoutUidWrite
2115
3005
  );
3006
+ this.app.resourceManager.registerPreActionHandler("multiPortals:updateOrCreate", captureSkipCreatePortalDirectory);
3007
+ this.app.resourceManager.registerPreActionHandler("multiPortals:updateOrCreate", normalizeMultiPortalSlugValues);
2116
3008
  this.app.resourceManager.registerPreActionHandler("desktopRoutes:create", addDesktopRouteCreateMultiPortal, {
2117
3009
  after: UI_LAYOUT_DESKTOP_ROUTE_WRITE_LAYOUT_HANDLER_TAG
2118
3010
  });
@@ -2123,6 +3015,9 @@ class PluginMultiPortalServer extends import_server.Plugin {
2123
3015
  after: UI_LAYOUT_DESKTOP_ROUTE_WRITE_LAYOUT_HANDLER_TAG
2124
3016
  }
2125
3017
  );
3018
+ this.app.resourceManager.registerPreActionHandler("desktopRoutes:update", guardDesktopRouteUpdateScope);
3019
+ this.app.resourceManager.registerPreActionHandler("desktopRoutes:move", guardDesktopRouteMoveScope);
3020
+ this.app.resourceManager.registerPreActionHandler("desktopRoutes:destroy", destroyScopedDesktopRoutes);
2126
3021
  }
2127
3022
  async load() {
2128
3023
  this.app.acl.registerSnippet({
@@ -2134,12 +3029,15 @@ class PluginMultiPortalServer extends import_server.Plugin {
2134
3029
  actions: ROLE_MULTI_PORTAL_PERMISSION_ACTIONS
2135
3030
  });
2136
3031
  this.app.acl.allow("multiPortals", "listEnabled", "public");
3032
+ this.app.acl.allow("multiPortals", "getDefault", "public");
2137
3033
  this.app.acl.allow("multiPortals", "listAccessible", "loggedIn");
2138
3034
  this.app.resourceManager.use(createPortalDeployUploadMiddleware(), {
2139
3035
  tag: "multiPortalDeployUpload",
2140
3036
  after: "acl"
2141
3037
  });
2142
3038
  this.app.resourceManager.registerActionHandler("multiPortals:listEnabled", listEnabledMultiPortals);
3039
+ this.app.resourceManager.registerActionHandler("multiPortals:getDefault", getDefaultMultiPortal);
3040
+ this.app.resourceManager.registerActionHandler("multiPortals:setDefault", setDefaultMultiPortal);
2143
3041
  this.app.resourceManager.registerActionHandler("multiPortals:listAccessible", listAccessibleMultiPortals);
2144
3042
  this.app.resourceManager.registerActionHandler("multiPortals:deploy", async (ctx, next) => {
2145
3043
  await this.deployPortalDist(ctx, next);
@@ -2163,13 +3061,19 @@ class PluginMultiPortalServer extends import_server.Plugin {
2163
3061
  this.app.db.on("roles.beforeCreate", (role) => {
2164
3062
  (0, import_ensureDefaultRoleMultiPortalAccess.applyDefaultRoleMultiPortalAccess)(role);
2165
3063
  });
3064
+ this.app.db.on("desktopRoutes.afterCreate", reconcileCreatedDesktopRoutePermissions);
3065
+ this.app.db.on("multiPortals.beforeUpdate", (multiPortal) => {
3066
+ if (multiPortal.get("enabled") === false && multiPortal.get("isDefault") === true) {
3067
+ multiPortal.set("isDefault", null);
3068
+ }
3069
+ });
2166
3070
  this.app.db.on("multiPortals.afterCreate", async (multiPortal, options) => {
2167
3071
  await grantDefaultAccessToNewMultiPortal(this.app.db, multiPortal, options);
2168
3072
  await this.publishAppManifestItem(multiPortal, options);
2169
3073
  if (shouldSkipCreatePortalDirectory(options)) {
2170
3074
  return;
2171
3075
  }
2172
- await this.syncMultiPortalStorageItem(multiPortal, options);
3076
+ await this.syncMultiPortalStorageItem(multiPortal, options, false, true);
2173
3077
  });
2174
3078
  this.app.db.on("multiPortals.afterUpdate", async (multiPortal, options) => {
2175
3079
  await this.publishAppManifestItem(multiPortal, options);
@@ -2182,19 +3086,28 @@ class PluginMultiPortalServer extends import_server.Plugin {
2182
3086
  }
2183
3087
  await this.removeMultiPortalStorageItem(multiPortal, options);
2184
3088
  });
2185
- this.app.db.on("uiLayouts.afterUpdate", async (uiLayout, options) => {
2186
- await this.publishUiLayoutManifestItems(uiLayout, options);
2187
- });
2188
3089
  }
2189
3090
  async install() {
2190
- await ensureDefaultMultiPortals(this.db);
2191
- await (0, import_ensureDefaultRoleMultiPortalAccess.ensureDefaultRoleMultiPortalAccess)(this.db);
3091
+ var _a, _b;
3092
+ if (!this.deprecatedInitPortalEnvWarningEmitted && hasDeprecatedInitPortalEnv()) {
3093
+ this.deprecatedInitPortalEnvWarningEmitted = true;
3094
+ (_b = (_a = this.app.logger) == null ? void 0 : _a.warn) == null ? void 0 : _b.call(
3095
+ _a,
3096
+ 'INIT_PORTAL_TYPE and INIT_PORTAL_NAME are deprecated and no longer affect multi-portal seeding; NocoBase now creates the AI Portal "main" plus the fixed no-code "admin" and "mobile" portals by default.'
3097
+ );
3098
+ }
3099
+ const version = await this.app.version.get();
3100
+ if (!version) {
3101
+ await (0, import_ensureDefaultRoleMultiPortalAccess.ensureDefaultRoleMultiPortalAccess)(this.db);
3102
+ await seedFreshMultiPortals(this.db);
3103
+ } else {
3104
+ await seedHistoricalMultiPortals(this.db);
3105
+ }
2192
3106
  await this.reconcilePortalStorage();
2193
3107
  await this.reconcileAppManifest();
2194
3108
  }
2195
3109
  async afterEnable() {
2196
- await ensureDefaultMultiPortals(this.db);
2197
- await (0, import_ensureDefaultRoleMultiPortalAccess.ensureDefaultRoleMultiPortalAccess)(this.db);
3110
+ await repairFixedLayoutMultiPortalRecords(this.db);
2198
3111
  await this.reconcilePortalStorage();
2199
3112
  await this.reconcileAppManifest();
2200
3113
  }