@nocobase/plugin-multi-portal 2.2.0-alpha.10 → 2.2.0-alpha.12

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.
@@ -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",
@@ -79,8 +81,8 @@ const MAIN_APP_NAME = "main";
79
81
  const UNION_ROLE_KEY = "__union__";
80
82
  const MULTI_PORTAL_MANIFEST_NAMESPACE = "multi-portal";
81
83
  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
+ const DEFAULT_INIT_PORTAL_TYPE = "ai";
85
+ const DEFAULT_INIT_PORTAL_NAME = "main";
84
86
  const DEFAULT_INIT_PORTAL_TEMPLATE = "@nocobase/portal-template-default";
85
87
  const PORTAL_CLIENT_PREFIX = "x";
86
88
  const PORTAL_DEPLOY_UPLOAD_LIMIT = 200 * 1024 * 1024;
@@ -91,8 +93,6 @@ const PORTAL_TEMPLATE_NPM_PACK_TIMEOUT_MS = 3e4;
91
93
  const DEFAULT_MULTI_PORTAL_UID = "__default_portal__";
92
94
  const MULTI_PORTAL_SLUG_PATTERN = /^[a-z0-9_-]+$/;
93
95
  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);
96
96
  const MULTI_PORTAL_MANAGEMENT_ACTIONS = [
97
97
  "multiPortals:list",
98
98
  "multiPortals:get",
@@ -103,14 +103,22 @@ const MULTI_PORTAL_MANAGEMENT_ACTIONS = [
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");
114
122
  const SKIP_CREATE_PORTAL_DIRECTORY = Symbol("skipCreatePortalDirectory");
115
123
  function getRecordField(record, field) {
116
124
  if (!record || typeof record !== "object") {
@@ -174,6 +182,38 @@ function getDefaultMultiPortalRecord() {
174
182
  uiLayoutUid: "admin-layout-model"
175
183
  };
176
184
  }
185
+ function getFreshMultiPortalRecords() {
186
+ return [getDefaultMultiPortalRecord()];
187
+ }
188
+ function getFixedLayoutMultiPortalRecords() {
189
+ return [
190
+ {
191
+ uid: import_constants.DEFAULT_ADMIN_MULTI_PORTAL_UID,
192
+ title: "Desktop layout",
193
+ icon: "DesktopOutlined",
194
+ portalType: "no-code",
195
+ portalName: "admin",
196
+ routePath: "/admin",
197
+ authCheck: true,
198
+ enabled: true,
199
+ uiLayoutUid: "admin-layout-model"
200
+ },
201
+ {
202
+ uid: import_constants.DEFAULT_MOBILE_MULTI_PORTAL_UID,
203
+ title: "Mobile layout",
204
+ icon: "MobileOutlined",
205
+ portalType: "no-code",
206
+ portalName: "mobile",
207
+ routePath: "/mobile",
208
+ authCheck: true,
209
+ enabled: true,
210
+ uiLayoutUid: "mobile-layout-model"
211
+ }
212
+ ];
213
+ }
214
+ function getFixedLayoutMultiPortalRecord(uid) {
215
+ return getFixedLayoutMultiPortalRecords().find((portal) => portal.uid === uid);
216
+ }
177
217
  function isValidPortalDeploySegment(value) {
178
218
  return /^[A-Za-z0-9_-]+$/.test(value);
179
219
  }
@@ -197,6 +237,20 @@ function joinPortalStoragePublicPath(publicPath, pathname) {
197
237
  function isAbsoluteUrl(value) {
198
238
  return /^[a-z][a-z\d+\-.]*:\/\//i.test(value) || value.startsWith("//");
199
239
  }
240
+ function normalizePortalStorageUrlPathname(pathname) {
241
+ const normalized = pathname.replace(/\/+/g, "/");
242
+ return normalized === "/" ? normalized : normalized.replace(/\/+$/, "");
243
+ }
244
+ function resolvePortalStorageEnvApiUrl(apiUrl) {
245
+ try {
246
+ const parsedUrl = new URL(apiUrl.startsWith("//") ? `http:${apiUrl}` : apiUrl);
247
+ return normalizePortalStorageUrlPathname(parsedUrl.pathname);
248
+ } catch {
249
+ const [pathname] = apiUrl.split(/[?#]/, 1);
250
+ const withLeadingSlash = (pathname == null ? void 0 : pathname.startsWith("/")) ? pathname : `/${pathname || "api"}`;
251
+ return normalizePortalStorageUrlPathname(withLeadingSlash);
252
+ }
253
+ }
200
254
  function appendPortalStorageSubAppApiUrl(apiUrl, appName) {
201
255
  if (appName === MAIN_APP_NAME) {
202
256
  return apiUrl;
@@ -387,6 +441,73 @@ async function copyPortalTemplate(sourceDir, targetDir) {
387
441
  filter: (source) => !import_path.default.relative(sourceDir, source).split(import_path.default.sep).some((segment) => segment.startsWith("._") || ignoredSegments.has(segment))
388
442
  });
389
443
  }
444
+ function getPortalStorageConfig(options) {
445
+ const sourceOptions = isRecordLike(options) ? options : {};
446
+ const sourceStorage = trimString(sourceOptions.sourceStorage);
447
+ if (sourceStorage !== "git") {
448
+ return { sourceStorage: "nocobase" };
449
+ }
450
+ const gitOptions = isRecordLike(sourceOptions.git) ? sourceOptions.git : {};
451
+ return {
452
+ sourceStorage,
453
+ git: {
454
+ repo: trimString(gitOptions.repo),
455
+ branch: trimString(gitOptions.branch) || "main",
456
+ path: trimString(gitOptions.path) || "."
457
+ }
458
+ };
459
+ }
460
+ function upsertPortalEnvContent(content, values) {
461
+ const nextValues = { ...values };
462
+ const lines = content ? content.replace(/\r\n/g, "\n").split("\n") : [];
463
+ const result = [];
464
+ for (const line of lines) {
465
+ if (!line && result.length === lines.length - 1) {
466
+ continue;
467
+ }
468
+ const match = line.match(/^(\s*)([A-Za-z_][A-Za-z0-9_]*)\s*=/);
469
+ const key = match == null ? void 0 : match[2];
470
+ if (key && Object.prototype.hasOwnProperty.call(nextValues, key)) {
471
+ result.push(`${key}=${nextValues[key]}`);
472
+ delete nextValues[key];
473
+ continue;
474
+ }
475
+ result.push(line);
476
+ }
477
+ for (const [key, value] of Object.entries(nextValues)) {
478
+ result.push(`${key}=${value}`);
479
+ }
480
+ return `${result.join("\n").replace(/\n*$/, "")}
481
+ `;
482
+ }
483
+ async function upsertPortalEnvFile(filePath, values) {
484
+ let content = "";
485
+ try {
486
+ content = await import_fs.default.promises.readFile(filePath, "utf-8");
487
+ } catch {
488
+ content = "";
489
+ }
490
+ await import_fs.default.promises.writeFile(filePath, upsertPortalEnvContent(content, values), "utf-8");
491
+ }
492
+ async function ensurePortalStorageConfigFiles(portalDir, item) {
493
+ const apiUrl = getPortalStorageApiUrl(item.appName);
494
+ const portalBase = getPortalDeployBasePath(item.appName, item.portalName);
495
+ await import_fs.default.promises.mkdir(portalDir, { recursive: true });
496
+ await upsertPortalEnvFile(import_path.default.join(portalDir, ".env"), {
497
+ NOCOBASE_API_URL: resolvePortalStorageEnvApiUrl(apiUrl),
498
+ NOCOBASE_PORTAL_BASE: portalBase
499
+ });
500
+ await upsertPortalEnvFile(import_path.default.join(portalDir, ".env.local"), {
501
+ NOCOBASE_API_URL: apiUrl,
502
+ NOCOBASE_PORTAL_BASE: portalBase
503
+ });
504
+ await import_fs.default.promises.writeFile(
505
+ import_path.default.join(portalDir, "portal.config.json"),
506
+ `${JSON.stringify(item.config, null, 2)}
507
+ `,
508
+ "utf-8"
509
+ );
510
+ }
390
511
  function sanitizePortalStorageNodeOptions(value) {
391
512
  return trimString(value).split(/\s+/).filter((option) => option !== "--preserve-symlinks" && option !== "--preserve-symlinks-main").join(" ");
392
513
  }
@@ -475,7 +596,8 @@ async function buildPortalStorageItem(portalDir, item) {
475
596
  const logPath = getPortalStorageLogPath(item);
476
597
  const buildEnv = getPortalStorageCommandEnv({
477
598
  NOCOBASE_API_URL: getPortalStorageApiUrl(item.appName),
478
- NOCOBASE_PORTAL_BASE: getPortalDeployBasePath(item.appName, item.portalName)
599
+ NOCOBASE_PORTAL_BASE: getPortalDeployBasePath(item.appName, item.portalName),
600
+ COREPACK_ENABLE_PROJECT_SPEC: "0"
479
601
  });
480
602
  await appendPortalStorageLog(logPath, `Building portal ${item.appName}/${item.portalName}.`);
481
603
  await appendPortalStorageLog(
@@ -709,29 +831,6 @@ async function replacePortalSourceFromArchive(params) {
709
831
  await import_fs.default.promises.rm(tarPath, { force: true });
710
832
  }
711
833
  }
712
- function isDefaultMultiPortalUid(uid) {
713
- return typeof uid === "string" && DEFAULT_MULTI_PORTAL_UID_SET.has(uid);
714
- }
715
- function getDefaultMultiPortalRecordForUid(uid) {
716
- return isDefaultMultiPortalUid(uid) ? getDefaultMultiPortalRecord() : void 0;
717
- }
718
- function getDefaultMultiPortalUiLayoutUid(portal) {
719
- return Object.prototype.hasOwnProperty.call(portal, "uiLayoutUid") ? portal.uiLayoutUid ?? null : null;
720
- }
721
- function withCustomMultiPortalFilter(filter = {}) {
722
- const customFilter = { ...filter };
723
- const uidFilter = customFilter.uid;
724
- if (Array.isArray(uidFilter)) {
725
- customFilter.uid = uidFilter.filter((uid) => !isDefaultMultiPortalUid(uid));
726
- return customFilter;
727
- }
728
- if (isDefaultMultiPortalUid(uidFilter)) {
729
- customFilter.uid = [];
730
- return customFilter;
731
- }
732
- customFilter["uid.$notIn"] = DEFAULT_MULTI_PORTAL_UIDS;
733
- return customFilter;
734
- }
735
834
  function uniqueDesktopRouteIds(routeIds) {
736
835
  const seen = /* @__PURE__ */ new Set();
737
836
  return routeIds.filter((routeId) => {
@@ -786,14 +885,25 @@ function getExplicitRequestedLayoutUid(layout) {
786
885
  return uid;
787
886
  }
788
887
  }
888
+ function hasActionParam(params, key) {
889
+ return isRecordLike(params) && Object.prototype.hasOwnProperty.call(params, key);
890
+ }
789
891
  function getRequestedMultiPortalUid(ctx) {
790
- var _a, _b;
791
- const portalUid = getExplicitRequestedLayoutUid((_a = ctx.action) == null ? void 0 : _a.params.portal);
792
- const layoutUid = getExplicitRequestedLayoutUid((_b = ctx.action) == null ? void 0 : _b.params.layout);
793
- if (portalUid && layoutUid) {
892
+ var _a, _b, _c;
893
+ const hasPortalScope = hasActionParam((_a = ctx.action) == null ? void 0 : _a.params, "portal");
894
+ const hasLayoutScope = hasActionParam((_b = ctx.action) == null ? void 0 : _b.params, "layout");
895
+ const portalUid = getExplicitRequestedLayoutUid((_c = ctx.action) == null ? void 0 : _c.params.portal);
896
+ if (hasPortalScope && hasLayoutScope) {
794
897
  ctx.throw(400, "layout and portal cannot be used together");
795
898
  return;
796
899
  }
900
+ if (!hasPortalScope) {
901
+ return;
902
+ }
903
+ if (!portalUid) {
904
+ ctx.throw(400, "Invalid portal scope");
905
+ return;
906
+ }
797
907
  return portalUid;
798
908
  }
799
909
  function getCurrentRoles(ctx) {
@@ -818,27 +928,88 @@ function getMultiPortalNameFromValues(ctx) {
818
928
  return portalName;
819
929
  }
820
930
  }
821
- function getMultiPortalWriteValues(ctx) {
931
+ function getMultiPortalWriteRecords(ctx) {
822
932
  var _a;
823
933
  const values = (_a = ctx.action) == null ? void 0 : _a.params.values;
824
- if (!values || typeof values !== "object" || Array.isArray(values)) {
825
- return;
934
+ const records = Array.isArray(values) ? values : [values];
935
+ return records.filter(
936
+ (record) => !!record && typeof record === "object" && !Array.isArray(record)
937
+ );
938
+ }
939
+ async function getMultiPortalWriteTargets(ctx, fields) {
940
+ var _a, _b, _c, _d, _e;
941
+ const records = getMultiPortalWriteRecords(ctx);
942
+ if (!records.length || ((_a = ctx.action) == null ? void 0 : _a.actionName) === "create") {
943
+ return records.map((values2) => ({ values: values2 }));
944
+ }
945
+ const repository = ctx.db.getRepository("multiPortals");
946
+ const actionName = (_b = ctx.action) == null ? void 0 : _b.actionName;
947
+ const requestedFilterKeys = (_c = ctx.action) == null ? void 0 : _c.params.filterKeys;
948
+ if ((actionName === "firstOrCreate" || actionName === "updateOrCreate") && Array.isArray(requestedFilterKeys) && requestedFilterKeys.every((filterKey) => typeof filterKey === "string")) {
949
+ const targets = [];
950
+ for (const values2 of records) {
951
+ const existing2 = await repository.findOne({
952
+ filter: import_database.Repository.valuesToFilter(values2, requestedFilterKeys),
953
+ fields
954
+ });
955
+ targets.push({ existing: existing2, values: values2 });
956
+ }
957
+ return targets;
958
+ }
959
+ if (records.length > 1) {
960
+ const targets = [];
961
+ for (const values2 of records) {
962
+ const uid2 = values2.uid;
963
+ const existing2 = typeof uid2 === "string" && uid2 ? await repository.findOne({
964
+ filterByTk: uid2,
965
+ fields
966
+ }) : null;
967
+ targets.push({ existing: existing2, values: values2 });
968
+ }
969
+ return targets;
826
970
  }
827
- return values;
971
+ const values = records[0];
972
+ const filterByTk = (_d = ctx.action) == null ? void 0 : _d.params.filterByTk;
973
+ if (filterByTk !== void 0 && filterByTk !== null) {
974
+ const existingRecords = await repository.find({
975
+ filterByTk,
976
+ fields
977
+ });
978
+ return existingRecords.length ? existingRecords.map((existing2) => ({ existing: existing2, values })) : [{ values }];
979
+ }
980
+ const filter = (_e = ctx.action) == null ? void 0 : _e.params.filter;
981
+ if (isRecordLike(filter) && !Array.isArray(filter)) {
982
+ const existingRecords = await repository.find({
983
+ filter,
984
+ fields
985
+ });
986
+ return existingRecords.length ? existingRecords.map((existing2) => ({ existing: existing2, values })) : [{ values }];
987
+ }
988
+ const uid = values.uid;
989
+ const existing = typeof uid === "string" && uid ? await repository.findOne({
990
+ filterByTk: uid,
991
+ fields
992
+ }) : null;
993
+ return [{ existing, values }];
828
994
  }
829
995
  function isTruthyActionBoolean(value) {
830
996
  return value === true || value === 1 || value === "1" || value === "true";
831
997
  }
832
998
  async function captureSkipCreatePortalDirectory(ctx, next) {
833
- const values = getMultiPortalWriteValues(ctx);
834
- if (!values || !Object.prototype.hasOwnProperty.call(values, "skipCreatePortalDirectory")) {
999
+ const records = getMultiPortalWriteRecords(ctx);
1000
+ const recordsWithOption = records.filter(
1001
+ (record) => Object.prototype.hasOwnProperty.call(record, "skipCreatePortalDirectory")
1002
+ );
1003
+ if (!recordsWithOption.length) {
835
1004
  await next();
836
1005
  return;
837
1006
  }
838
- ctx[SKIP_CREATE_PORTAL_DIRECTORY] = isTruthyActionBoolean(
839
- values.skipCreatePortalDirectory
1007
+ ctx[SKIP_CREATE_PORTAL_DIRECTORY] = recordsWithOption.some(
1008
+ (record) => isTruthyActionBoolean(record.skipCreatePortalDirectory)
840
1009
  );
841
- delete values.skipCreatePortalDirectory;
1010
+ for (const record of recordsWithOption) {
1011
+ delete record.skipCreatePortalDirectory;
1012
+ }
842
1013
  await next();
843
1014
  }
844
1015
  function shouldSkipCreatePortalDirectory(options) {
@@ -846,156 +1017,180 @@ function shouldSkipCreatePortalDirectory(options) {
846
1017
  return ((_a = options == null ? void 0 : options.context) == null ? void 0 : _a[SKIP_CREATE_PORTAL_DIRECTORY]) === true;
847
1018
  }
848
1019
  async function normalizeMultiPortalSlugValues(ctx, next) {
849
- const values = getMultiPortalWriteValues(ctx);
850
- const portalName = values == null ? void 0 : values.portalName;
851
- if (portalName === void 0 || portalName === null) {
852
- await next();
853
- return;
854
- }
855
- if (typeof portalName !== "string") {
856
- await next();
857
- return;
1020
+ for (const values of getMultiPortalWriteRecords(ctx)) {
1021
+ const portalName = values.portalName;
1022
+ if (portalName === void 0 || portalName === null || typeof portalName !== "string") {
1023
+ continue;
1024
+ }
1025
+ const slug = portalName.trim();
1026
+ if (!slug) {
1027
+ continue;
1028
+ }
1029
+ if (!MULTI_PORTAL_SLUG_PATTERN.test(slug)) {
1030
+ ctx.throw(400, "Portal name can only contain lowercase letters, numbers, hyphens, and underscores");
1031
+ return;
1032
+ }
1033
+ values.portalName = slug;
1034
+ values.routePath = `/${slug}`;
858
1035
  }
859
- const slug = portalName.trim();
860
- if (!slug) {
861
- await next();
862
- return;
1036
+ await next();
1037
+ }
1038
+ function isUniqueConstraintViolation(error) {
1039
+ return error instanceof import_database.UniqueConstraintError || isRecordLike(error) && error.name === "SequelizeUniqueConstraintError";
1040
+ }
1041
+ async function createDefaultMultiPortalBestEffort(db, portal) {
1042
+ const repository = db.getRepository("multiPortals");
1043
+ const [uidConflict, portalNameConflict] = await Promise.all([
1044
+ repository.findOne({
1045
+ filterByTk: portal.uid,
1046
+ fields: ["uid"]
1047
+ }),
1048
+ repository.findOne({
1049
+ filter: { portalName: portal.portalName },
1050
+ fields: ["uid"]
1051
+ })
1052
+ ]);
1053
+ if (uidConflict || portalNameConflict) {
1054
+ return false;
863
1055
  }
864
- if (!MULTI_PORTAL_SLUG_PATTERN.test(slug)) {
865
- ctx.throw(400, "Portal name can only contain lowercase letters, numbers, hyphens, and underscores");
866
- return;
1056
+ try {
1057
+ await repository.create({ values: portal });
1058
+ return true;
1059
+ } catch (error) {
1060
+ if (isUniqueConstraintViolation(error)) {
1061
+ return false;
1062
+ }
1063
+ throw error;
867
1064
  }
868
- values.portalName = slug;
869
- values.routePath = `/${slug}`;
870
- await next();
871
1065
  }
872
- async function ensureDefaultMultiPortals(db, options) {
1066
+ async function repairFixedLayoutMultiPortalRecords(db) {
873
1067
  const repository = db.getRepository("multiPortals");
874
- const defaultPortal = getDefaultMultiPortalRecord();
875
- const existing = await repository.findOne({
876
- filterByTk: defaultPortal.uid,
877
- fields: ["uid", "title", "icon", "portalType", "portalName", "routePath", "authCheck", "uiLayoutUid"],
878
- transaction: options == null ? void 0 : options.transaction
879
- });
880
- if (!existing) {
881
- await repository.create({
882
- values: defaultPortal,
883
- transaction: options == null ? void 0 : options.transaction
1068
+ for (const portal of getFixedLayoutMultiPortalRecords()) {
1069
+ const existing = await repository.findOne({
1070
+ filterByTk: portal.uid,
1071
+ fields: ["uid", "portalType", "portalName", "routePath", "uiLayoutUid"]
884
1072
  });
885
- return;
886
- }
887
- const protectedValues = {
888
- uid: defaultPortal.uid,
889
- portalType: defaultPortal.portalType ?? null,
890
- portalName: defaultPortal.portalName,
891
- routePath: defaultPortal.routePath,
892
- authCheck: defaultPortal.authCheck,
893
- uiLayoutUid: getDefaultMultiPortalUiLayoutUid(defaultPortal)
894
- };
895
- const shouldRepair = Object.entries(protectedValues).some(([field, value]) => existing.get(field) !== value);
896
- if (shouldRepair) {
1073
+ 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) {
1074
+ continue;
1075
+ }
897
1076
  await repository.update({
898
- filterByTk: defaultPortal.uid,
899
- values: protectedValues,
900
- transaction: options == null ? void 0 : options.transaction
1077
+ filterByTk: portal.uid,
1078
+ values: {
1079
+ portalType: portal.portalType
1080
+ }
901
1081
  });
902
1082
  }
903
1083
  }
904
- async function protectDefaultMultiPortalUpdate(ctx, next) {
905
- var _a, _b, _c;
906
- const defaultPortal = getDefaultMultiPortalRecordForUid((_a = ctx.action) == null ? void 0 : _a.params.filterByTk);
907
- if (!defaultPortal) {
908
- await next();
909
- return;
910
- }
911
- const params = (_b = ctx.action) == null ? void 0 : _b.params;
912
- const rawValues = isRecordLike(params == null ? void 0 : params.values) ? params.values : {};
913
- const values = {
914
- uid: defaultPortal.uid,
915
- portalType: defaultPortal.portalType ?? null,
916
- portalName: defaultPortal.portalName,
917
- routePath: defaultPortal.routePath,
918
- authCheck: defaultPortal.authCheck,
919
- uiLayoutUid: getDefaultMultiPortalUiLayoutUid(defaultPortal)
920
- };
921
- if (Object.prototype.hasOwnProperty.call(rawValues, "title")) {
922
- values.title = rawValues.title;
1084
+ async function seedFreshMultiPortals(db) {
1085
+ for (const portal of getFreshMultiPortalRecords()) {
1086
+ await createDefaultMultiPortalBestEffort(db, portal);
923
1087
  }
924
- if (Object.prototype.hasOwnProperty.call(rawValues, "icon")) {
925
- values.icon = rawValues.icon;
1088
+ }
1089
+ async function seedHistoricalMultiPortals(db) {
1090
+ if (getInitPortalType() === "ai") {
1091
+ await createDefaultMultiPortalBestEffort(db, getDefaultMultiPortalRecord());
926
1092
  }
927
- if (Object.prototype.hasOwnProperty.call(rawValues, "enabled")) {
928
- values.enabled = rawValues.enabled;
1093
+ for (const portal of getFixedLayoutMultiPortalRecords()) {
1094
+ await createDefaultMultiPortalBestEffort(db, portal);
929
1095
  }
930
- (_c = ctx.action) == null ? void 0 : _c.mergeParams(
931
- {
932
- values
933
- },
934
- {
935
- values: "overwrite"
936
- }
937
- );
938
- await next();
1096
+ await repairFixedLayoutMultiPortalRecords(db);
939
1097
  }
940
1098
  async function preventUiLayoutRouteNameConflict(ctx, next) {
941
- var _a;
942
- if (isDefaultMultiPortalUid((_a = ctx.action) == null ? void 0 : _a.params.filterByTk)) {
943
- await next();
944
- return;
945
- }
946
- const portalName = getMultiPortalNameFromValues(ctx);
947
- if (!portalName) {
948
- await next();
949
- return;
1099
+ const targets = await getMultiPortalWriteTargets(ctx, ["uid", "portalName", "uiLayoutUid"]);
1100
+ for (const { existing, values } of targets) {
1101
+ const existingUid = trimString(existing == null ? void 0 : existing.get("uid"));
1102
+ const uid = trimString(values.uid ?? existingUid);
1103
+ const portalName = trimString(values.portalName ?? (existing == null ? void 0 : existing.get("portalName")));
1104
+ const uiLayoutUid = trimString(values.uiLayoutUid ?? (existing == null ? void 0 : existing.get("uiLayoutUid")));
1105
+ const fixedPortal = getFixedLayoutMultiPortalRecord(existingUid) || getFixedLayoutMultiPortalRecord(uid);
1106
+ const canonicalPortal = (fixedPortal == null ? void 0 : fixedPortal.uid) === uid && (!existingUid || fixedPortal.uid === existingUid) && fixedPortal.portalName === portalName && fixedPortal.uiLayoutUid === uiLayoutUid;
1107
+ if (fixedPortal && !canonicalPortal) {
1108
+ ctx.throw(400, "Reserved Portal UID must match its canonical route and UI layout");
1109
+ return;
1110
+ }
1111
+ if (!portalName) {
1112
+ continue;
1113
+ }
1114
+ if (portalName === "admin" || portalName === "mobile") {
1115
+ continue;
1116
+ }
1117
+ const existingInitPortal = (existing == null ? void 0 : existing.get("uid")) === DEFAULT_MULTI_PORTAL_UID && existing.get("portalName") === portalName;
1118
+ const uiLayout = await ctx.db.getRepository("uiLayouts").findOne({
1119
+ filter: {
1120
+ routeName: portalName
1121
+ },
1122
+ fields: ["uid"]
1123
+ });
1124
+ if (uiLayout && !existingInitPortal) {
1125
+ ctx.throw(400, "Portal route name conflicts with an existing UI layout");
1126
+ return;
1127
+ }
950
1128
  }
951
- const uiLayout = await ctx.db.getRepository("uiLayouts").findOne({
952
- filter: {
953
- routeName: portalName
954
- },
955
- fields: ["uid"]
956
- });
957
- if (uiLayout) {
958
- ctx.throw(400, "Portal route name conflicts with an existing UI layout");
959
- return;
1129
+ await next();
1130
+ }
1131
+ async function preventMultiPortalBackingLayoutChange(ctx, next) {
1132
+ const targets = await getMultiPortalWriteTargets(ctx, ["uiLayoutUid"]);
1133
+ for (const { existing, values } of targets) {
1134
+ if (!Object.prototype.hasOwnProperty.call(values, "uiLayoutUid")) {
1135
+ continue;
1136
+ }
1137
+ if (existing && existing.get("uiLayoutUid") !== values.uiLayoutUid) {
1138
+ ctx.throw(400, "Portal backing UI layout cannot be changed");
1139
+ return;
1140
+ }
960
1141
  }
961
1142
  await next();
962
1143
  }
963
- async function findRequestedMultiPortal(ctx) {
964
- var _a;
1144
+ async function findRequestedMultiPortal(ctx, transaction) {
965
1145
  const portalUid = getRequestedMultiPortalUid(ctx);
966
1146
  if (portalUid === void 0) {
967
1147
  return {
968
1148
  requested: false
969
1149
  };
970
1150
  }
971
- const isExplicitPortalRequest = getExplicitRequestedLayoutUid((_a = ctx.action) == null ? void 0 : _a.params.portal) !== void 0;
972
- if (!isExplicitPortalRequest) {
973
- const uiLayout = await ctx.db.getRepository("uiLayouts").findOne({
974
- filter: {
975
- uid: portalUid
976
- },
977
- fields: ["uid"]
978
- });
979
- if (uiLayout) {
980
- return {
981
- requested: false
982
- };
983
- }
984
- }
985
1151
  const portal = await ctx.db.getRepository("multiPortals").findOne({
986
1152
  filter: {
987
1153
  uid: portalUid
988
1154
  },
989
- fields: ["uid", "uiLayoutUid", "enabled"]
1155
+ fields: ["uid", "portalType", "uiLayoutUid", "enabled"],
1156
+ ...transaction ? { lock: transaction.LOCK.UPDATE } : {},
1157
+ transaction
990
1158
  });
991
- if ((portal == null ? void 0 : portal.get("enabled")) === true) {
992
- return {
993
- portal,
994
- requested: true
995
- };
1159
+ if (!portal || portal.get("enabled") !== true) {
1160
+ return { requested: true };
1161
+ }
1162
+ if (portal.get("portalType") !== "no-code") {
1163
+ ctx.throw(400, `Portal '${portalUid}' does not support desktop routes`);
1164
+ }
1165
+ const uiLayoutUid = portal.get("uiLayoutUid");
1166
+ if (typeof uiLayoutUid !== "string" || !uiLayoutUid) {
1167
+ ctx.throw(400, `Portal '${portalUid}' has no backing UI layout`);
1168
+ }
1169
+ const uiLayout = await ctx.db.getRepository("uiLayouts").findOne({
1170
+ filter: {
1171
+ uid: uiLayoutUid,
1172
+ enabled: true
1173
+ },
1174
+ fields: ["uid"],
1175
+ ...transaction ? { lock: transaction.LOCK.UPDATE } : {},
1176
+ transaction
1177
+ });
1178
+ if (!uiLayout) {
1179
+ ctx.throw(400, `Portal '${portalUid}' has no enabled backing UI layout`);
996
1180
  }
1181
+ const usesLayoutPermissions = (0, import_constants.isDefaultLayoutMultiPortalUid)(portalUid);
997
1182
  return {
998
- requested: !!isExplicitPortalRequest || !!portal
1183
+ portal,
1184
+ requested: true,
1185
+ scope: {
1186
+ portalUid,
1187
+ uiLayoutUid,
1188
+ relation: usesLayoutPermissions ? "uiLayouts" : "multiPortals",
1189
+ uid: usesLayoutPermissions ? uiLayoutUid : portalUid,
1190
+ filter: usesLayoutPermissions ? {
1191
+ "uiLayouts.uid": uiLayoutUid
1192
+ } : getDesktopRoutePortalFilter(portalUid)
1193
+ }
999
1194
  };
1000
1195
  }
1001
1196
  function getDesktopRoutePortalFilter(multiPortalUid) {
@@ -1006,49 +1201,90 @@ function getDesktopRoutePortalFilter(multiPortalUid) {
1006
1201
  function isDesktopRouteCreateValue(value) {
1007
1202
  return !!value && typeof value === "object" && !Array.isArray(value);
1008
1203
  }
1009
- function hasDesktopRouteField(value, field) {
1010
- return !!value && Object.prototype.hasOwnProperty.call(value, field);
1204
+ function withDesktopRouteEffectiveOwner(value, scope) {
1205
+ if (Array.isArray(value)) {
1206
+ return value.map((item) => withDesktopRouteEffectiveOwner(item, scope));
1207
+ }
1208
+ if (!isDesktopRouteCreateValue(value)) {
1209
+ return value;
1210
+ }
1211
+ const { uiLayouts: _uiLayouts, multiPortals: _multiPortals, children, ...route } = value;
1212
+ return {
1213
+ ...route,
1214
+ ...scope.relation === "uiLayouts" ? { uiLayouts: [scope.uid] } : { multiPortals: [scope.uid] },
1215
+ ...Array.isArray(children) ? { children: withDesktopRouteEffectiveOwner(children, scope) } : {}
1216
+ };
1011
1217
  }
1012
- function pickExplicitDesktopRouteUiLayouts(value) {
1218
+ function collectExplicitDesktopRouteParentIds(value) {
1013
1219
  if (Array.isArray(value)) {
1014
- return value.map((item) => pickExplicitDesktopRouteUiLayouts(item));
1220
+ return uniqueDesktopRouteIds(value.flatMap((item) => collectExplicitDesktopRouteParentIds(item)));
1015
1221
  }
1016
1222
  if (!isDesktopRouteCreateValue(value)) {
1017
- return;
1223
+ return [];
1018
1224
  }
1019
- const route = {};
1020
- if (hasDesktopRouteField(value, "uiLayouts")) {
1021
- route.uiLayouts = value.uiLayouts;
1225
+ const parentIds = [];
1226
+ if (typeof value.parentId === "string" || typeof value.parentId === "number") {
1227
+ parentIds.push(value.parentId);
1022
1228
  }
1023
1229
  if (Array.isArray(value.children)) {
1024
- route.children = value.children.map((child) => pickExplicitDesktopRouteUiLayouts(child));
1230
+ parentIds.push(...value.children.flatMap((child) => collectExplicitDesktopRouteParentIds(child)));
1025
1231
  }
1026
- return route;
1232
+ return uniqueDesktopRouteIds(parentIds);
1027
1233
  }
1028
- function withDesktopRouteMultiPortal(value, multiPortalUid, explicitValue = value) {
1029
- if (Array.isArray(value)) {
1030
- const explicitValues = Array.isArray(explicitValue) ? explicitValue : [];
1031
- return value.map((item, index) => withDesktopRouteMultiPortal(item, multiPortalUid, explicitValues[index]));
1234
+ async function assertDesktopRouteParentsBelongToScope(ctx, scope, values = ((_a) => (_a = ctx.action) == null ? void 0 : _a.params.values)(), transaction) {
1235
+ const parentIds = collectExplicitDesktopRouteParentIds(values);
1236
+ if (!parentIds.length) {
1237
+ return;
1032
1238
  }
1033
- if (!isDesktopRouteCreateValue(value)) {
1034
- return value;
1239
+ const parents = await ctx.db.getRepository("desktopRoutes").find({
1240
+ fields: ["id"],
1241
+ filter: {
1242
+ id: parentIds,
1243
+ ...scope.filter
1244
+ },
1245
+ ...transaction ? { lock: transaction.LOCK.UPDATE } : {},
1246
+ transaction
1247
+ });
1248
+ if (new Set(normalizeDesktopRouteTargetIds(parents)).size !== new Set(parentIds.map(String)).size) {
1249
+ ctx.throw(400, "Parent route does not belong to the requested portal scope");
1035
1250
  }
1036
- const explicitRoute = isDesktopRouteCreateValue(explicitValue) ? explicitValue : void 0;
1037
- const existingMultiPortals = Array.isArray(value.multiPortals) ? value.multiPortals.filter((uid) => typeof uid === "string" && !!uid) : [];
1038
- const { uiLayouts: _uiLayouts, children: _children, ...route } = value;
1039
- return {
1040
- ...route,
1041
- ...hasDesktopRouteField(explicitRoute, "uiLayouts") ? { uiLayouts: explicitRoute == null ? void 0 : explicitRoute.uiLayouts } : {},
1042
- multiPortals: Array.from(/* @__PURE__ */ new Set([...existingMultiPortals, multiPortalUid])),
1043
- ...Array.isArray(value.children) ? { children: withDesktopRouteMultiPortal(value.children, multiPortalUid, explicitRoute == null ? void 0 : explicitRoute.children) } : {}
1044
- };
1045
1251
  }
1046
- async function captureExplicitDesktopRouteUiLayouts(ctx, next) {
1047
- var _a;
1048
- ctx[EXPLICIT_DESKTOP_ROUTE_UI_LAYOUTS] = pickExplicitDesktopRouteUiLayouts(
1049
- (_a = ctx.action) == null ? void 0 : _a.params.values
1050
- );
1051
- await next();
1252
+ 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)()) {
1253
+ const filterKeys = Array.isArray(requestedFilterKeys) ? requestedFilterKeys.filter((key) => typeof key === "string" && !!key) : [];
1254
+ if (!filterKeys.length) {
1255
+ return [];
1256
+ }
1257
+ const records = Array.isArray(values) ? values : [values];
1258
+ return records.flatMap((value) => {
1259
+ if (!isDesktopRouteCreateValue(value) || filterKeys.some((key) => !Object.prototype.hasOwnProperty.call(value, key))) {
1260
+ return [];
1261
+ }
1262
+ return [Object.fromEntries(filterKeys.map((key) => [key, value[key]]))];
1263
+ });
1264
+ }
1265
+ 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) {
1266
+ for (const filter of getDesktopRouteUpdateOrCreateFilters(ctx, values, filterKeys)) {
1267
+ const allMatches = await ctx.db.getRepository("desktopRoutes").find({
1268
+ fields: ["id"],
1269
+ filter,
1270
+ ...transaction ? { lock: transaction.LOCK.UPDATE } : {},
1271
+ transaction
1272
+ });
1273
+ const scopedMatches = await ctx.db.getRepository("desktopRoutes").find({
1274
+ fields: ["id"],
1275
+ filter: {
1276
+ ...filter,
1277
+ ...scope.filter
1278
+ },
1279
+ ...transaction ? { lock: transaction.LOCK.UPDATE } : {},
1280
+ transaction
1281
+ });
1282
+ const allMatchIds = normalizeDesktopRouteTargetIds(allMatches);
1283
+ const scopedMatchIds = normalizeDesktopRouteTargetIds(scopedMatches);
1284
+ if (allMatchIds.join(",") !== scopedMatchIds.join(",")) {
1285
+ ctx.throw(400, "Desktop route does not belong to the requested portal scope");
1286
+ }
1287
+ }
1052
1288
  }
1053
1289
  async function canAccessMultiPortal(ctx, multiPortalUid) {
1054
1290
  const currentRoles = getCurrentRoles(ctx);
@@ -1298,25 +1534,17 @@ async function prepareAccessibleDesktopRoutesForMultiPortal(ctx) {
1298
1534
  if (!portalRequest.requested) {
1299
1535
  return;
1300
1536
  }
1301
- const portal = portalRequest.portal;
1302
- if (!portal) {
1537
+ const scope = portalRequest.scope;
1538
+ if (!scope) {
1303
1539
  return null;
1304
1540
  }
1305
- const portalUid = portal.get("uid");
1306
- const uiLayoutUid = portal.get("uiLayoutUid");
1307
- if (typeof portalUid !== "string" || !portalUid || typeof uiLayoutUid !== "string" || !uiLayoutUid) {
1308
- return null;
1309
- }
1310
- if (!await canAccessMultiPortal(ctx, portalUid)) {
1541
+ if (scope.relation === "multiPortals" && !await canAccessMultiPortal(ctx, scope.portalUid)) {
1311
1542
  return null;
1312
1543
  }
1313
1544
  if ((_a = ctx.action) == null ? void 0 : _a.params) {
1314
- ctx.action.params.layout = uiLayoutUid;
1545
+ ctx.action.params.layout = scope.uiLayoutUid;
1315
1546
  }
1316
- return {
1317
- portalUid,
1318
- uiLayoutUid
1319
- };
1547
+ return scope;
1320
1548
  }
1321
1549
  async function addMultiPortalListAccessibleGuard(ctx, next) {
1322
1550
  const portalContext = await prepareAccessibleDesktopRoutesForMultiPortal(ctx);
@@ -1325,10 +1553,13 @@ async function addMultiPortalListAccessibleGuard(ctx, next) {
1325
1553
  return;
1326
1554
  }
1327
1555
  await next();
1328
- if (portalContext) {
1556
+ if ((portalContext == null ? void 0 : portalContext.relation) === "multiPortals") {
1329
1557
  await replaceListAccessibleRoutesWithPortalScopedRoutes(ctx, portalContext);
1330
1558
  return;
1331
1559
  }
1560
+ if ((portalContext == null ? void 0 : portalContext.relation) === "uiLayouts") {
1561
+ return;
1562
+ }
1332
1563
  await removeMultiPortalOwnedRoutesFromListResponse(ctx);
1333
1564
  }
1334
1565
  async function addMultiPortalGetAccessibleGuard(ctx, next) {
@@ -1339,26 +1570,36 @@ async function addMultiPortalGetAccessibleGuard(ctx, next) {
1339
1570
  return;
1340
1571
  }
1341
1572
  await next();
1342
- if (portalContext) {
1573
+ if ((portalContext == null ? void 0 : portalContext.relation) === "multiPortals") {
1343
1574
  await replaceGetAccessibleRouteWithPortalScopedRoute(ctx, portalContext);
1344
1575
  return;
1345
1576
  }
1577
+ if ((portalContext == null ? void 0 : portalContext.relation) === "uiLayouts") {
1578
+ return;
1579
+ }
1346
1580
  await removeMultiPortalOwnedRouteFromGetResponse(ctx);
1347
1581
  }
1348
1582
  async function mapMultiPortalLayoutToUiLayoutForRolePermissionTargets(ctx, next) {
1583
+ var _a;
1349
1584
  const portalRequest = await findRequestedMultiPortal(ctx);
1350
1585
  if (portalRequest.requested && !portalRequest.portal) {
1351
1586
  ctx.status = 200;
1352
1587
  ctx.body = [];
1353
1588
  return;
1354
1589
  }
1355
- const portal = portalRequest.portal;
1356
- const portalUid = portal == null ? void 0 : portal.get("uid");
1357
- if (typeof portalUid === "string" && portalUid) {
1590
+ const scope = portalRequest.scope;
1591
+ if ((scope == null ? void 0 : scope.relation) === "uiLayouts") {
1592
+ if ((_a = ctx.action) == null ? void 0 : _a.params) {
1593
+ ctx.action.params.layout = scope.uiLayoutUid;
1594
+ }
1595
+ await next();
1596
+ return;
1597
+ }
1598
+ if ((scope == null ? void 0 : scope.relation) === "multiPortals") {
1358
1599
  const routes = await ctx.db.getRepository("desktopRoutes").find({
1359
1600
  tree: true,
1360
1601
  sort: "sort",
1361
- filter: getDesktopRoutePortalFilter(portalUid),
1602
+ filter: scope.filter,
1362
1603
  fields: [...DESKTOP_ROUTE_ROLE_PERMISSION_TARGET_FIELDS]
1363
1604
  });
1364
1605
  ctx.status = 200;
@@ -1369,38 +1610,585 @@ async function mapMultiPortalLayoutToUiLayoutForRolePermissionTargets(ctx, next)
1369
1610
  await removeMultiPortalOwnedRoutesFromListResponse(ctx);
1370
1611
  }
1371
1612
  async function addDesktopRouteCreateMultiPortal(ctx, next) {
1372
- var _a, _b;
1613
+ var _a, _b, _c;
1373
1614
  const portalRequest = await findRequestedMultiPortal(ctx);
1374
1615
  if (portalRequest.requested && !portalRequest.portal) {
1375
1616
  ctx.throw(400, "Invalid portal scope");
1376
1617
  return;
1377
1618
  }
1378
- const portal = portalRequest.portal;
1379
- const portalUid = portal == null ? void 0 : portal.get("uid");
1380
- if (typeof portalUid === "string" && portalUid) {
1381
- const explicitValues = ctx[EXPLICIT_DESKTOP_ROUTE_UI_LAYOUTS];
1382
- (_b = ctx.action) == null ? void 0 : _b.mergeParams(
1619
+ const scope = portalRequest.scope;
1620
+ if (scope) {
1621
+ await assertDesktopRouteParentsBelongToScope(ctx, scope);
1622
+ if (((_a = ctx.action) == null ? void 0 : _a.actionName) === "updateOrCreate") {
1623
+ await assertDesktopRouteUpsertMatchesBelongToScope(ctx, scope);
1624
+ }
1625
+ (_c = ctx.action) == null ? void 0 : _c.mergeParams(
1383
1626
  {
1384
- values: withDesktopRouteMultiPortal((_a = ctx.action) == null ? void 0 : _a.params.values, portalUid, explicitValues)
1627
+ values: withDesktopRouteEffectiveOwner((_b = ctx.action) == null ? void 0 : _b.params.values, scope)
1385
1628
  },
1386
1629
  {
1387
1630
  values: "overwrite"
1388
1631
  }
1389
1632
  );
1390
1633
  await next();
1391
- const desktopRouteIds = collectDesktopRouteIds(ctx.body);
1392
- await removeLayoutRoutePermissionsFromPortalOnlyRoutes(ctx, desktopRouteIds);
1393
- await grantDefaultRouteAccessToNewMultiPortalRoutes(ctx, portalUid, desktopRouteIds);
1394
1634
  return;
1395
1635
  }
1396
1636
  await next();
1397
1637
  }
1638
+ async function addDesktopRouteEffectiveScopeFilter(ctx, next) {
1639
+ var _a;
1640
+ const portalRequest = await findRequestedMultiPortal(ctx);
1641
+ if (!portalRequest.requested) {
1642
+ await next();
1643
+ return;
1644
+ }
1645
+ if (!portalRequest.scope) {
1646
+ ctx.throw(400, "Invalid portal scope");
1647
+ return;
1648
+ }
1649
+ (_a = ctx.action) == null ? void 0 : _a.mergeParams({
1650
+ filter: portalRequest.scope.filter
1651
+ });
1652
+ await next();
1653
+ }
1654
+ function withoutDesktopRouteOwnerFields(value) {
1655
+ if (Array.isArray(value)) {
1656
+ return value.map((item) => withoutDesktopRouteOwnerFields(item));
1657
+ }
1658
+ if (!isDesktopRouteCreateValue(value)) {
1659
+ return value;
1660
+ }
1661
+ const { uiLayouts: _uiLayouts, multiPortals: _multiPortals, children, ...route } = value;
1662
+ return {
1663
+ ...route,
1664
+ ...Array.isArray(children) ? { children: withoutDesktopRouteOwnerFields(children) } : {}
1665
+ };
1666
+ }
1667
+ function normalizeDesktopRouteTargetIds(records) {
1668
+ return records.map((record) => record.get("id")).filter((id) => typeof id === "string" || typeof id === "number").map(String).sort();
1669
+ }
1670
+ async function findDesktopRouteMutationTargets(ctx, scopeFilter, transaction, selector) {
1671
+ var _a;
1672
+ const params = selector ?? ((_a = ctx.action) == null ? void 0 : _a.params) ?? {};
1673
+ const values = params.values;
1674
+ const batchUpdateIds = Array.isArray(values) ? values.flatMap((value) => {
1675
+ if (!isDesktopRouteCreateValue(value)) {
1676
+ return [];
1677
+ }
1678
+ const routeId = value.id;
1679
+ return typeof routeId === "string" || typeof routeId === "number" ? [routeId] : [];
1680
+ }) : [];
1681
+ const hasBatchUpdateTargets = batchUpdateIds.length > 0;
1682
+ const filterByTk = hasBatchUpdateTargets ? batchUpdateIds : params.filterByTk;
1683
+ const filter = !hasBatchUpdateTargets && isRecordLike(params.filter) ? params.filter : void 0;
1684
+ if ((filterByTk === void 0 || filterByTk === null) && !filter) {
1685
+ return [];
1686
+ }
1687
+ return ctx.db.getRepository("desktopRoutes").find({
1688
+ fields: ["id"],
1689
+ ...filterByTk !== void 0 && filterByTk !== null ? { filterByTk } : {},
1690
+ filter: {
1691
+ ...filter ?? {},
1692
+ ...scopeFilter ?? {}
1693
+ },
1694
+ ...transaction ? { lock: transaction.LOCK.UPDATE } : {},
1695
+ transaction
1696
+ });
1697
+ }
1698
+ async function assertDesktopRouteMutationTargetsBelongToScope(ctx, scope, transaction, selector) {
1699
+ const allTargets = await findDesktopRouteMutationTargets(ctx, void 0, transaction, selector);
1700
+ const scopedTargets = await findDesktopRouteMutationTargets(ctx, scope.filter, transaction, selector);
1701
+ const allTargetIds = normalizeDesktopRouteTargetIds(allTargets);
1702
+ const scopedTargetIds = normalizeDesktopRouteTargetIds(scopedTargets);
1703
+ if (!allTargetIds.length || allTargetIds.join(",") !== scopedTargetIds.join(",")) {
1704
+ ctx.throw(400, "Desktop route does not belong to the requested portal scope");
1705
+ }
1706
+ }
1707
+ async function guardDesktopRouteUpdateScope(ctx, next) {
1708
+ var _a, _b;
1709
+ const portalRequest = await findRequestedMultiPortal(ctx);
1710
+ if (!portalRequest.requested) {
1711
+ await next();
1712
+ return;
1713
+ }
1714
+ const scope = portalRequest.scope;
1715
+ if (!scope) {
1716
+ ctx.throw(400, "Invalid portal scope");
1717
+ return;
1718
+ }
1719
+ await assertDesktopRouteMutationTargetsBelongToScope(ctx, scope);
1720
+ await assertDesktopRouteParentsBelongToScope(ctx, scope);
1721
+ (_b = ctx.action) == null ? void 0 : _b.mergeParams(
1722
+ {
1723
+ values: withoutDesktopRouteOwnerFields((_a = ctx.action) == null ? void 0 : _a.params.values)
1724
+ },
1725
+ {
1726
+ values: "overwrite"
1727
+ }
1728
+ );
1729
+ await next();
1730
+ }
1731
+ function getDesktopRoutePortalContext(context) {
1732
+ var _a;
1733
+ if (!isRecordLike(context)) {
1734
+ return;
1735
+ }
1736
+ const ctx = context;
1737
+ if (((_a = ctx.action) == null ? void 0 : _a.resourceName) !== "desktopRoutes" || !hasActionParam(ctx.action.params, "portal")) {
1738
+ return;
1739
+ }
1740
+ return ctx;
1741
+ }
1742
+ async function resolveRequiredDesktopRouteScope(ctx, transaction) {
1743
+ const portalRequest = await findRequestedMultiPortal(ctx, transaction);
1744
+ if (!portalRequest.scope) {
1745
+ ctx.throw(400, "Invalid portal scope");
1746
+ }
1747
+ return portalRequest.scope;
1748
+ }
1749
+ function withDesktopRouteScopeFilter(filter, scope) {
1750
+ return {
1751
+ ...isRecordLike(filter) ? filter : {},
1752
+ ...scope.filter
1753
+ };
1754
+ }
1755
+ class MultiPortalDesktopRouteRepository extends import_database.Repository {
1756
+ async withAuthoritativeScopeTransaction(existingTransaction, callback) {
1757
+ if (existingTransaction) {
1758
+ return callback(existingTransaction);
1759
+ }
1760
+ return this.database.sequelize.transaction(callback);
1761
+ }
1762
+ async create(options) {
1763
+ const ctx = getDesktopRoutePortalContext(options.context);
1764
+ if (!ctx) {
1765
+ return super.create(options);
1766
+ }
1767
+ return this.withAuthoritativeScopeTransaction(options.transaction, async (transaction) => {
1768
+ const scope = await resolveRequiredDesktopRouteScope(ctx, transaction);
1769
+ const values = withDesktopRouteEffectiveOwner(options.values, scope);
1770
+ await assertDesktopRouteParentsBelongToScope(ctx, scope, values, transaction);
1771
+ return super.create({
1772
+ ...options,
1773
+ values,
1774
+ transaction
1775
+ });
1776
+ });
1777
+ }
1778
+ async update(options) {
1779
+ const ctx = getDesktopRoutePortalContext(options.context);
1780
+ if (!ctx) {
1781
+ return super.update(options);
1782
+ }
1783
+ return this.withAuthoritativeScopeTransaction(options.transaction, async (transaction) => {
1784
+ const scope = await resolveRequiredDesktopRouteScope(ctx, transaction);
1785
+ await assertDesktopRouteMutationTargetsBelongToScope(ctx, scope, transaction, options);
1786
+ const values = withoutDesktopRouteOwnerFields(options.values);
1787
+ await assertDesktopRouteParentsBelongToScope(ctx, scope, values, transaction);
1788
+ return super.update({
1789
+ ...options,
1790
+ filter: withDesktopRouteScopeFilter(options.filter, scope),
1791
+ values,
1792
+ transaction
1793
+ });
1794
+ });
1795
+ }
1796
+ async updateOrCreate(options) {
1797
+ const ctx = getDesktopRoutePortalContext(options.context);
1798
+ if (!ctx) {
1799
+ return super.updateOrCreate(options);
1800
+ }
1801
+ return this.withAuthoritativeScopeTransaction(options.transaction, async (transaction) => {
1802
+ const scope = await resolveRequiredDesktopRouteScope(ctx, transaction);
1803
+ const values = withDesktopRouteEffectiveOwner(options.values, scope);
1804
+ await assertDesktopRouteParentsBelongToScope(ctx, scope, values, transaction);
1805
+ await assertDesktopRouteUpsertMatchesBelongToScope(ctx, scope, values, options.filterKeys, transaction);
1806
+ return super.updateOrCreate({
1807
+ ...options,
1808
+ values,
1809
+ transaction
1810
+ });
1811
+ });
1812
+ }
1813
+ }
1814
+ async function guardDesktopRouteMoveScope(ctx, next) {
1815
+ var _a;
1816
+ const portalRequest = await findRequestedMultiPortal(ctx);
1817
+ if (!portalRequest.requested) {
1818
+ await next();
1819
+ return;
1820
+ }
1821
+ const params = ((_a = ctx.action) == null ? void 0 : _a.params) ?? {};
1822
+ const sourceId = params.sourceId;
1823
+ const targetId = params.targetId;
1824
+ const hasTargetId = targetId !== void 0 && targetId !== null;
1825
+ if (typeof sourceId !== "string" && typeof sourceId !== "number" || hasTargetId && typeof targetId !== "string" && typeof targetId !== "number" || params.sortField !== void 0 && params.sortField !== "sort") {
1826
+ ctx.throw(400, "Invalid desktop route move scope");
1827
+ return;
1828
+ }
1829
+ await ctx.db.sequelize.transaction(async (transaction) => {
1830
+ const scope = await resolveRequiredDesktopRouteScope(ctx, transaction);
1831
+ const routeIds = uniqueDesktopRouteIds(hasTargetId ? [sourceId, targetId] : [sourceId]);
1832
+ const allRoutes = await ctx.db.getRepository("desktopRoutes").find({
1833
+ fields: ["id", "parentId", "sort"],
1834
+ filter: {
1835
+ id: routeIds
1836
+ },
1837
+ lock: transaction.LOCK.UPDATE,
1838
+ transaction
1839
+ });
1840
+ const scopedRoutes = await ctx.db.getRepository("desktopRoutes").find({
1841
+ fields: ["id", "parentId", "sort"],
1842
+ filter: {
1843
+ id: routeIds,
1844
+ ...scope.filter
1845
+ },
1846
+ lock: transaction.LOCK.UPDATE,
1847
+ transaction
1848
+ });
1849
+ if (normalizeDesktopRouteTargetIds(allRoutes).join(",") !== normalizeDesktopRouteTargetIds(scopedRoutes).join(",") || scopedRoutes.length !== routeIds.length) {
1850
+ ctx.throw(400, "Move target does not belong to the requested portal scope");
1851
+ return;
1852
+ }
1853
+ const routesById = new Map(
1854
+ scopedRoutes.map((route) => [String(route.get("id")), route])
1855
+ );
1856
+ let source = routesById.get(String(sourceId));
1857
+ const target = hasTargetId ? routesById.get(String(targetId)) : void 0;
1858
+ if (!source || hasTargetId && !target) {
1859
+ ctx.throw(400, "Move target does not belong to the requested portal scope");
1860
+ return;
1861
+ }
1862
+ const targetScope = isRecordLike(params.targetScope) ? params.targetScope : void 0;
1863
+ const requestedParentId = targetScope == null ? void 0 : targetScope.parentId;
1864
+ const parentIds = uniqueDesktopRouteIds(
1865
+ [source.get("parentId"), target == null ? void 0 : target.get("parentId"), requestedParentId].filter(
1866
+ (id) => typeof id === "string" || typeof id === "number"
1867
+ )
1868
+ );
1869
+ if (parentIds.length) {
1870
+ const parents = await ctx.db.getRepository("desktopRoutes").find({
1871
+ fields: ["id"],
1872
+ filter: {
1873
+ id: parentIds,
1874
+ ...scope.filter
1875
+ },
1876
+ lock: transaction.LOCK.UPDATE,
1877
+ transaction
1878
+ });
1879
+ if (normalizeDesktopRouteTargetIds(parents).join(",") !== parentIds.map(String).sort().join(",")) {
1880
+ ctx.throw(400, "Move parent does not belong to the requested portal scope");
1881
+ return;
1882
+ }
1883
+ }
1884
+ const updateSource = async (values, silent) => {
1885
+ const [updatedSource] = await ctx.db.getRepository("desktopRoutes").update({
1886
+ filterByTk: sourceId,
1887
+ values,
1888
+ silent,
1889
+ transaction
1890
+ });
1891
+ if (!updatedSource) {
1892
+ ctx.throw(400, "Move source does not belong to the requested portal scope");
1893
+ }
1894
+ source = updatedSource;
1895
+ };
1896
+ if (target) {
1897
+ const targetParentId = target.get("parentId");
1898
+ if (source.get("parentId") !== targetParentId) {
1899
+ await updateSource({ parentId: targetParentId }, false);
1900
+ }
1901
+ const sourceSort = source.get("sort");
1902
+ let targetSort = target.get("sort");
1903
+ if (typeof sourceSort !== "number" || typeof targetSort !== "number") {
1904
+ ctx.throw(400, "Invalid desktop route sort value");
1905
+ return;
1906
+ }
1907
+ if (params.method === "insertAfter") {
1908
+ targetSort += 1;
1909
+ }
1910
+ const movesForward = targetSort > sourceSort;
1911
+ const affectedRoutes = await ctx.db.getRepository("desktopRoutes").find({
1912
+ fields: ["id"],
1913
+ filter: {
1914
+ ...scope.filter,
1915
+ parentId: targetParentId ?? null,
1916
+ sort: movesForward ? {
1917
+ $gt: sourceSort,
1918
+ $lte: targetSort
1919
+ } : {
1920
+ $lt: sourceSort,
1921
+ $gte: targetSort
1922
+ }
1923
+ },
1924
+ lock: transaction.LOCK.UPDATE,
1925
+ transaction
1926
+ });
1927
+ const affectedRouteIds = affectedRoutes.map((route) => route.get("id"));
1928
+ if (affectedRouteIds.length) {
1929
+ await ctx.db.getCollection("desktopRoutes").model.increment("sort", {
1930
+ where: {
1931
+ id: {
1932
+ [import_database.Op.in]: affectedRouteIds
1933
+ }
1934
+ },
1935
+ by: movesForward ? -1 : 1,
1936
+ silent: true,
1937
+ transaction
1938
+ });
1939
+ }
1940
+ await updateSource({ sort: targetSort }, true);
1941
+ }
1942
+ if (requestedParentId && source.get("parentId") !== requestedParentId) {
1943
+ await updateSource({ parentId: requestedParentId }, false);
1944
+ if (params.method === "prepend") {
1945
+ await updateSource({ sort: 0 }, true);
1946
+ }
1947
+ }
1948
+ if (params.sticky) {
1949
+ await updateSource({ sort: 0 }, true);
1950
+ }
1951
+ });
1952
+ ctx.body = "ok";
1953
+ }
1954
+ function normalizeDesktopRouteFilterByTk(filterByTk) {
1955
+ const values = Array.isArray(filterByTk) ? filterByTk : [filterByTk];
1956
+ return uniqueDesktopRouteIds(
1957
+ values.filter((value) => typeof value === "string" || typeof value === "number")
1958
+ );
1959
+ }
1960
+ async function getDesktopRouteDestroyOwnerScope(ctx, transaction) {
1961
+ var _a, _b, _c, _d;
1962
+ const hasPortalScope = hasActionParam((_a = ctx.action) == null ? void 0 : _a.params, "portal");
1963
+ const hasLayoutScope = hasActionParam((_b = ctx.action) == null ? void 0 : _b.params, "layout");
1964
+ const portalUid = getExplicitRequestedLayoutUid((_c = ctx.action) == null ? void 0 : _c.params.portal);
1965
+ const layoutUid = getExplicitRequestedLayoutUid((_d = ctx.action) == null ? void 0 : _d.params.layout);
1966
+ if (hasPortalScope && hasLayoutScope) {
1967
+ ctx.throw(400, "layout and portal cannot be used together");
1968
+ return;
1969
+ }
1970
+ if (hasPortalScope) {
1971
+ if (!portalUid) {
1972
+ ctx.throw(400, "Invalid portal scope");
1973
+ return;
1974
+ }
1975
+ const portalRequest = await findRequestedMultiPortal(ctx, transaction);
1976
+ if (!portalRequest.scope) {
1977
+ ctx.throw(400, "Invalid portal scope");
1978
+ return;
1979
+ }
1980
+ return portalRequest.scope;
1981
+ }
1982
+ if (!hasLayoutScope) {
1983
+ return;
1984
+ }
1985
+ if (!layoutUid) {
1986
+ ctx.throw(400, "Invalid layout scope");
1987
+ return;
1988
+ }
1989
+ const layout = await ctx.db.getRepository("uiLayouts").findOne({
1990
+ fields: ["uid"],
1991
+ filter: {
1992
+ uid: layoutUid
1993
+ },
1994
+ transaction
1995
+ });
1996
+ if (!layout) {
1997
+ ctx.throw(400, "Invalid layout scope");
1998
+ return;
1999
+ }
2000
+ return {
2001
+ filter: {
2002
+ "uiLayouts.uid": layoutUid
2003
+ },
2004
+ relation: "uiLayouts",
2005
+ uid: layoutUid
2006
+ };
2007
+ }
2008
+ async function findDesktopRouteDestroyRoots(ctx, transaction, scopeFilter) {
2009
+ var _a, _b, _c;
2010
+ const filterByTk = normalizeDesktopRouteFilterByTk((_a = ctx.action) == null ? void 0 : _a.params.filterByTk);
2011
+ const actionFilter = isRecordLike((_b = ctx.action) == null ? void 0 : _b.params.filter) ? (_c = ctx.action) == null ? void 0 : _c.params.filter : void 0;
2012
+ if (!filterByTk.length && !actionFilter) {
2013
+ return [];
2014
+ }
2015
+ const records = await ctx.db.getRepository("desktopRoutes").find({
2016
+ fields: ["id"],
2017
+ filter: {
2018
+ ...actionFilter ?? {},
2019
+ ...scopeFilter ?? {},
2020
+ ...filterByTk.length ? { id: filterByTk } : {}
2021
+ },
2022
+ lock: transaction.LOCK.UPDATE,
2023
+ transaction
2024
+ });
2025
+ return collectDesktopRouteIds(records);
2026
+ }
2027
+ async function findDesktopRouteSubtreeIds(ctx, rootId, transaction) {
2028
+ const rootRoutes = await ctx.db.getRepository("desktopRoutes").find({
2029
+ fields: ["id"],
2030
+ filter: {
2031
+ id: rootId
2032
+ },
2033
+ lock: transaction.LOCK.UPDATE,
2034
+ transaction
2035
+ });
2036
+ const routeIds = collectDesktopRouteIds(rootRoutes);
2037
+ let parentIds = [...routeIds];
2038
+ while (parentIds.length) {
2039
+ const children = await ctx.db.getRepository("desktopRoutes").find({
2040
+ fields: ["id"],
2041
+ filter: {
2042
+ parentId: parentIds
2043
+ },
2044
+ lock: transaction.LOCK.UPDATE,
2045
+ sort: ["id"],
2046
+ transaction
2047
+ });
2048
+ const childIds = collectDesktopRouteIds(children).filter((routeId) => !routeIds.includes(routeId));
2049
+ routeIds.push(...childIds);
2050
+ parentIds = childIds;
2051
+ }
2052
+ return routeIds;
2053
+ }
2054
+ function getDesktopRouteOwnerUids(route, relation) {
2055
+ const owners = getRecordField(route, relation);
2056
+ if (!Array.isArray(owners)) {
2057
+ return [];
2058
+ }
2059
+ return owners.map((owner) => getRecordField(owner, "uid")).filter((uid) => typeof uid === "string" && !!uid);
2060
+ }
2061
+ function desktopRouteHasOwnerOutsideScope(route, scope) {
2062
+ const multiPortalUids = getDesktopRouteOwnerUids(route, "multiPortals");
2063
+ const uiLayoutUids = getDesktopRouteOwnerUids(route, "uiLayouts");
2064
+ if (scope.relation === "multiPortals") {
2065
+ return multiPortalUids.some((uid) => uid !== scope.uid) || uiLayoutUids.length > 0;
2066
+ }
2067
+ return uiLayoutUids.some((uid) => uid !== scope.uid) || multiPortalUids.length > 0;
2068
+ }
2069
+ async function detachDesktopRouteTreeOwner(ctx, routeIds, scope, transaction) {
2070
+ if (scope.relation === "multiPortals") {
2071
+ await ctx.db.getRepository("rolesMultiPortalDesktopRoutes").destroy({
2072
+ filter: {
2073
+ multiPortalUid: scope.uid,
2074
+ desktopRouteId: routeIds
2075
+ },
2076
+ transaction
2077
+ });
2078
+ }
2079
+ for (const routeId of routeIds) {
2080
+ await ctx.db.getRepository(`desktopRoutes.${scope.relation}`, routeId).remove({
2081
+ tk: scope.uid,
2082
+ transaction
2083
+ });
2084
+ }
2085
+ }
2086
+ async function detachOwnerAndFindDestroyRoots(ctx, routeIds, scope, transaction) {
2087
+ var _a;
2088
+ const routes = await ctx.db.getRepository("desktopRoutes").find({
2089
+ fields: ["id", "parentId"],
2090
+ appends: ["multiPortals", "uiLayouts"],
2091
+ filter: {
2092
+ id: routeIds
2093
+ },
2094
+ lock: transaction.LOCK.UPDATE,
2095
+ transaction
2096
+ });
2097
+ const routesById = new Map(routes.map((route) => [String(route.get("id")), route]));
2098
+ const ownerlessRouteIds = new Set(
2099
+ routes.filter((route) => !desktopRouteHasOwnerOutsideScope(route, scope)).map((route) => String(route.get("id")))
2100
+ );
2101
+ await detachDesktopRouteTreeOwner(ctx, routeIds, scope, transaction);
2102
+ for (const route of routes) {
2103
+ if (ownerlessRouteIds.has(String(route.get("id")))) {
2104
+ continue;
2105
+ }
2106
+ let parentId = route.get("parentId");
2107
+ const visitedParentIds = /* @__PURE__ */ new Set();
2108
+ while (parentId !== null && parentId !== void 0 && ownerlessRouteIds.has(String(parentId))) {
2109
+ const normalizedParentId = String(parentId);
2110
+ if (visitedParentIds.has(normalizedParentId)) {
2111
+ ctx.throw(400, "Invalid desktop route tree");
2112
+ return [];
2113
+ }
2114
+ visitedParentIds.add(normalizedParentId);
2115
+ parentId = ((_a = routesById.get(normalizedParentId)) == null ? void 0 : _a.get("parentId")) ?? null;
2116
+ }
2117
+ if (route.get("parentId") !== parentId) {
2118
+ await ctx.db.getRepository("desktopRoutes").update({
2119
+ filterByTk: route.get("id"),
2120
+ values: {
2121
+ parentId
2122
+ },
2123
+ transaction
2124
+ });
2125
+ }
2126
+ }
2127
+ return routes.filter((route) => {
2128
+ if (!ownerlessRouteIds.has(String(route.get("id")))) {
2129
+ return false;
2130
+ }
2131
+ const parentId = route.get("parentId");
2132
+ return parentId === null || parentId === void 0 || !ownerlessRouteIds.has(String(parentId));
2133
+ }).map((route) => route.get("id"));
2134
+ }
2135
+ async function destroyScopedDesktopRoutes(ctx, next) {
2136
+ var _a, _b;
2137
+ if (!hasActionParam((_a = ctx.action) == null ? void 0 : _a.params, "portal") && !hasActionParam((_b = ctx.action) == null ? void 0 : _b.params, "layout")) {
2138
+ await next();
2139
+ return;
2140
+ }
2141
+ const result = await ctx.db.sequelize.transaction(async (transaction) => {
2142
+ const scope = await getDesktopRouteDestroyOwnerScope(ctx, transaction);
2143
+ if (!scope) {
2144
+ return true;
2145
+ }
2146
+ const allRootIds = (await findDesktopRouteDestroyRoots(ctx, transaction)).sort(
2147
+ (left, right) => String(left).localeCompare(String(right))
2148
+ );
2149
+ const rootIds = (await findDesktopRouteDestroyRoots(ctx, transaction, scope.filter)).sort(
2150
+ (left, right) => String(left).localeCompare(String(right))
2151
+ );
2152
+ if (allRootIds.map(String).join(",") !== rootIds.map(String).join(",")) {
2153
+ ctx.throw(400, "Desktop route does not belong to the requested portal scope");
2154
+ return true;
2155
+ }
2156
+ const routeTrees = [];
2157
+ for (const rootId of rootIds) {
2158
+ routeTrees.push({
2159
+ rootId,
2160
+ routeIds: await findDesktopRouteSubtreeIds(ctx, rootId, transaction)
2161
+ });
2162
+ }
2163
+ const rootRouteTrees = routeTrees.filter(
2164
+ (routeTree, index) => !routeTrees.some(
2165
+ (candidate, candidateIndex) => candidateIndex !== index && candidate.routeIds.includes(routeTree.rootId)
2166
+ )
2167
+ );
2168
+ const physicalDestroyRootIds = [];
2169
+ for (const routeTree of rootRouteTrees) {
2170
+ physicalDestroyRootIds.push(
2171
+ ...await detachOwnerAndFindDestroyRoots(ctx, routeTree.routeIds, scope, transaction)
2172
+ );
2173
+ }
2174
+ if (!physicalDestroyRootIds.length) {
2175
+ return true;
2176
+ }
2177
+ return ctx.db.getRepository("desktopRoutes").destroy({
2178
+ context: ctx,
2179
+ filterByTk: physicalDestroyRootIds.length === 1 ? physicalDestroyRootIds[0] : physicalDestroyRootIds,
2180
+ transaction
2181
+ });
2182
+ });
2183
+ ctx.status = 200;
2184
+ ctx.body = result;
2185
+ }
1398
2186
  async function listEnabledMultiPortals(ctx, next) {
1399
2187
  const records = await ctx.db.getRepository("multiPortals").find({
1400
- filter: withCustomMultiPortalFilter({
2188
+ filter: {
1401
2189
  enabled: true,
1402
2190
  "uiLayout.enabled": true
1403
- }),
2191
+ },
1404
2192
  fields: [...MULTI_PORTAL_RUNTIME_QUERY_FIELDS],
1405
2193
  appends: ["uiLayout"],
1406
2194
  sort: ["uid"]
@@ -1410,40 +2198,33 @@ async function listEnabledMultiPortals(ctx, next) {
1410
2198
  }
1411
2199
  async function listAccessibleMultiPortals(ctx, next) {
1412
2200
  const accessiblePortalUids = await listCurrentRoleAccessibleMultiPortalUids(ctx);
1413
- if (Array.isArray(accessiblePortalUids) && accessiblePortalUids.length === 0) {
1414
- ctx.body = [];
1415
- await next();
1416
- return;
1417
- }
1418
- const filter = {
1419
- enabled: true,
1420
- "uiLayout.enabled": true
1421
- };
1422
- if (Array.isArray(accessiblePortalUids)) {
1423
- filter.uid = accessiblePortalUids;
1424
- }
1425
2201
  const records = await ctx.db.getRepository("multiPortals").find({
1426
- filter: withCustomMultiPortalFilter(filter),
2202
+ filter: {
2203
+ enabled: true,
2204
+ "uiLayout.enabled": true
2205
+ },
1427
2206
  fields: [...MULTI_PORTAL_ACCESSIBLE_QUERY_FIELDS],
1428
2207
  appends: ["uiLayout"],
1429
2208
  sort: ["uid"]
1430
2209
  });
1431
- ctx.body = records.map((record) => pickMultiPortalAccessibleFields(record));
2210
+ const accessiblePortalUidSet = Array.isArray(accessiblePortalUids) ? new Set(accessiblePortalUids) : void 0;
2211
+ const accessibleRecords = records.filter(
2212
+ (record) => (0, import_constants.isDefaultLayoutMultiPortalUid)(record.get("uid")) || !accessiblePortalUidSet || accessiblePortalUidSet.has(String(record.get("uid")))
2213
+ );
2214
+ ctx.body = accessibleRecords.map((record) => pickMultiPortalAccessibleFields(record));
1432
2215
  await next();
1433
2216
  }
1434
- async function grantDefaultRouteAccessToNewMultiPortalRoutes(ctx, multiPortalUid, desktopRouteIds) {
2217
+ async function grantDefaultRouteAccessToNewMultiPortalRoutes(ctx, multiPortalUid, desktopRouteIds, transaction) {
1435
2218
  if (!desktopRouteIds.length) {
1436
2219
  return;
1437
2220
  }
1438
- if (!ctx.db.getCollection("rolesMultiPortalRoutePolicies") || !ctx.db.getCollection("rolesMultiPortalDesktopRoutes")) {
1439
- return;
1440
- }
1441
2221
  const routePolicies = await ctx.db.getRepository("rolesMultiPortalRoutePolicies").find({
1442
2222
  fields: ["roleName"],
1443
2223
  filter: {
1444
2224
  multiPortalUid,
1445
2225
  allowNewMenu: true
1446
- }
2226
+ },
2227
+ transaction
1447
2228
  });
1448
2229
  const roleNames = routePolicies.map((routePolicy) => routePolicy.get("roleName")).filter((roleName) => typeof roleName === "string" && !!roleName);
1449
2230
  if (!roleNames.length) {
@@ -1459,26 +2240,52 @@ async function grantDefaultRouteAccessToNewMultiPortalRoutes(ctx, multiPortalUid
1459
2240
  multiPortalUid,
1460
2241
  desktopRouteId
1461
2242
  },
1462
- context: ctx
2243
+ context: ctx,
2244
+ transaction
1463
2245
  });
1464
2246
  }
1465
2247
  }
1466
2248
  }
1467
- async function removeLayoutRoutePermissionsFromPortalOnlyRoutes(ctx, desktopRouteIds) {
1468
- if (!desktopRouteIds.length || !ctx.db.getCollection("rolesDesktopRoutes")) {
1469
- return;
1470
- }
1471
- const portalOnlyRouteIds = await getPortalOnlyDesktopRouteIds(ctx, desktopRouteIds);
1472
- if (portalOnlyRouteIds.size === 0) {
2249
+ async function removeLayoutRoutePermissionsFromPortalRoutes(ctx, desktopRouteIds, transaction) {
2250
+ if (!desktopRouteIds.length) {
1473
2251
  return;
1474
2252
  }
1475
2253
  await ctx.db.getRepository("rolesDesktopRoutes").destroy({
1476
2254
  filter: {
1477
- desktopRouteId: Array.from(portalOnlyRouteIds)
2255
+ desktopRouteId: desktopRouteIds
1478
2256
  },
1479
- context: ctx
2257
+ context: ctx,
2258
+ transaction
1480
2259
  });
1481
2260
  }
2261
+ async function reconcileCreatedDesktopRoutePermissions(desktopRoute, options) {
2262
+ const ctx = options == null ? void 0 : options.context;
2263
+ if (!ctx) {
2264
+ return;
2265
+ }
2266
+ const portalRequest = await findRequestedMultiPortal(ctx, options.transaction);
2267
+ if (!portalRequest.requested) {
2268
+ return;
2269
+ }
2270
+ const scope = portalRequest.scope;
2271
+ if (!scope) {
2272
+ ctx.throw(400, "Invalid portal scope");
2273
+ return;
2274
+ }
2275
+ if (scope.relation !== "multiPortals") {
2276
+ return;
2277
+ }
2278
+ const transaction = options.transaction;
2279
+ if (!transaction) {
2280
+ throw new Error("Portal desktop route creation requires a transaction");
2281
+ }
2282
+ const desktopRouteId = desktopRoute.get("id");
2283
+ if (typeof desktopRouteId !== "string" && typeof desktopRouteId !== "number") {
2284
+ throw new Error("Created Portal desktop route has no identifier");
2285
+ }
2286
+ await removeLayoutRoutePermissionsFromPortalRoutes(ctx, [desktopRouteId], transaction);
2287
+ await grantDefaultRouteAccessToNewMultiPortalRoutes(ctx, scope.portalUid, [desktopRouteId], transaction);
2288
+ }
1482
2289
  async function grantDefaultAccessToNewMultiPortal(db, multiPortal, options) {
1483
2290
  if (multiPortal.get("enabled") !== true) {
1484
2291
  return;
@@ -1487,7 +2294,7 @@ async function grantDefaultAccessToNewMultiPortal(db, multiPortal, options) {
1487
2294
  if (typeof multiPortalUid !== "string" || !multiPortalUid) {
1488
2295
  return;
1489
2296
  }
1490
- if (isDefaultMultiPortalUid(multiPortalUid)) {
2297
+ if ((0, import_constants.isDefaultLayoutMultiPortalUid)(multiPortalUid)) {
1491
2298
  return;
1492
2299
  }
1493
2300
  if (!db.getCollection("roles") || !db.getCollection("rolesMultiPortals")) {
@@ -1521,7 +2328,7 @@ async function grantDefaultAccessToNewMultiPortal(db, multiPortal, options) {
1521
2328
  transaction: options == null ? void 0 : options.transaction
1522
2329
  });
1523
2330
  }
1524
- if (!db.getCollection("rolesMultiPortalRoutePolicies")) {
2331
+ if (multiPortal.get("portalType") === "ai" || !db.getCollection("rolesMultiPortalRoutePolicies")) {
1525
2332
  return;
1526
2333
  }
1527
2334
  const routePolicyRepository = db.getRepository("rolesMultiPortalRoutePolicies");
@@ -1555,7 +2362,8 @@ class PluginMultiPortalServer extends import_server.Plugin {
1555
2362
  return {
1556
2363
  appName: this.getAppName(),
1557
2364
  portalName,
1558
- enabled: readField("enabled") === true
2365
+ enabled: readField("enabled") === true,
2366
+ config: getPortalStorageConfig(readField("options"))
1559
2367
  };
1560
2368
  }
1561
2369
  warnPortalStorageSyncFailed(error) {
@@ -1620,6 +2428,8 @@ class PluginMultiPortalServer extends import_server.Plugin {
1620
2428
  await copyPortalTemplate(template.dir, portalDir);
1621
2429
  await appendPortalStorageLog(logPath, `Default portal template copied to ${portalDir}.`);
1622
2430
  }
2431
+ await ensurePortalStorageConfigFiles(portalDir, item);
2432
+ await appendPortalStorageLog(logPath, `Portal configuration files updated in ${portalDir}.`);
1623
2433
  if (item.enabled) {
1624
2434
  await buildPortalStorageItem(portalDir, item);
1625
2435
  return;
@@ -1649,6 +2459,7 @@ class PluginMultiPortalServer extends import_server.Plugin {
1649
2459
  await this.schedulePortalTemplateCopyAndBuild(item, template, portalDir);
1650
2460
  return;
1651
2461
  }
2462
+ await ensurePortalStorageConfigFiles(portalDir, item);
1652
2463
  if (item.enabled) {
1653
2464
  if (!await pathExists(portalIndex)) {
1654
2465
  await buildPortalStorageItem(portalDir, item);
@@ -1828,10 +2639,10 @@ class PluginMultiPortalServer extends import_server.Plugin {
1828
2639
  }
1829
2640
  async listAppPortalManifest(options) {
1830
2641
  const records = await this.db.getRepository("multiPortals").find({
1831
- filter: withCustomMultiPortalFilter({
2642
+ filter: {
1832
2643
  enabled: true,
1833
2644
  "uiLayout.enabled": true
1834
- }),
2645
+ },
1835
2646
  fields: [...MULTI_PORTAL_ACCESSIBLE_QUERY_FIELDS],
1836
2647
  appends: ["uiLayout"],
1837
2648
  sort: ["uid"],
@@ -1867,7 +2678,7 @@ class PluginMultiPortalServer extends import_server.Plugin {
1867
2678
  const portalType = getRecordField(multiPortal, "portalType");
1868
2679
  const routePath = getRecordField(multiPortal, "routePath");
1869
2680
  const enabled = getRecordField(multiPortal, "enabled");
1870
- if (typeof uid !== "string" || !uid || isDefaultMultiPortalUid(uid) || typeof title !== "string" || typeof routePath !== "string" || !routePath || enabled !== true) {
2681
+ if (typeof uid !== "string" || !uid || typeof title !== "string" || typeof routePath !== "string" || !routePath || enabled !== true) {
1871
2682
  return null;
1872
2683
  }
1873
2684
  const uiLayoutUid = getRecordField(multiPortal, "uiLayoutUid");
@@ -1939,9 +2750,9 @@ class PluginMultiPortalServer extends import_server.Plugin {
1939
2750
  return;
1940
2751
  }
1941
2752
  const records = await this.db.getRepository("multiPortals").find({
1942
- filter: withCustomMultiPortalFilter({
2753
+ filter: {
1943
2754
  uiLayoutUid
1944
- }),
2755
+ },
1945
2756
  fields: [...MULTI_PORTAL_ACCESSIBLE_QUERY_FIELDS],
1946
2757
  transaction: options == null ? void 0 : options.transaction
1947
2758
  });
@@ -2000,6 +2811,9 @@ class PluginMultiPortalServer extends import_server.Plugin {
2000
2811
  }
2001
2812
  }
2002
2813
  async beforeLoad() {
2814
+ this.app.db.registerRepositories({ MultiPortalDesktopRouteRepository });
2815
+ this.app.resourceManager.registerPreActionHandler("desktopRoutes:list", addDesktopRouteEffectiveScopeFilter);
2816
+ this.app.resourceManager.registerPreActionHandler("desktopRoutes:get", addDesktopRouteEffectiveScopeFilter);
2003
2817
  this.app.resourceManager.registerPreActionHandler(
2004
2818
  "desktopRoutes:listAccessible",
2005
2819
  addMultiPortalListAccessibleGuard
@@ -2012,22 +2826,23 @@ class PluginMultiPortalServer extends import_server.Plugin {
2012
2826
  this.app.resourceManager.registerPreActionHandler("multiPortals:create", captureSkipCreatePortalDirectory);
2013
2827
  this.app.resourceManager.registerPreActionHandler("multiPortals:create", normalizeMultiPortalSlugValues);
2014
2828
  this.app.resourceManager.registerPreActionHandler("multiPortals:create", preventUiLayoutRouteNameConflict);
2015
- this.app.resourceManager.registerPreActionHandler("multiPortals:update", protectDefaultMultiPortalUpdate);
2829
+ this.app.resourceManager.registerPreActionHandler("multiPortals:update", preventMultiPortalBackingLayoutChange);
2016
2830
  this.app.resourceManager.registerPreActionHandler("multiPortals:update", normalizeMultiPortalSlugValues);
2017
2831
  this.app.resourceManager.registerPreActionHandler("multiPortals:update", preventUiLayoutRouteNameConflict);
2832
+ this.app.resourceManager.registerPreActionHandler(
2833
+ "multiPortals:firstOrCreate",
2834
+ preventMultiPortalBackingLayoutChange
2835
+ );
2018
2836
  this.app.resourceManager.registerPreActionHandler("multiPortals:firstOrCreate", captureSkipCreatePortalDirectory);
2019
2837
  this.app.resourceManager.registerPreActionHandler("multiPortals:firstOrCreate", normalizeMultiPortalSlugValues);
2020
2838
  this.app.resourceManager.registerPreActionHandler("multiPortals:firstOrCreate", preventUiLayoutRouteNameConflict);
2021
- this.app.resourceManager.registerPreActionHandler("desktopRoutes:create", captureExplicitDesktopRouteUiLayouts, {
2022
- before: UI_LAYOUT_DESKTOP_ROUTE_WRITE_LAYOUT_HANDLER_TAG
2023
- });
2024
2839
  this.app.resourceManager.registerPreActionHandler(
2025
- "desktopRoutes:updateOrCreate",
2026
- captureExplicitDesktopRouteUiLayouts,
2027
- {
2028
- before: UI_LAYOUT_DESKTOP_ROUTE_WRITE_LAYOUT_HANDLER_TAG
2029
- }
2840
+ "multiPortals:updateOrCreate",
2841
+ preventMultiPortalBackingLayoutChange
2030
2842
  );
2843
+ this.app.resourceManager.registerPreActionHandler("multiPortals:updateOrCreate", captureSkipCreatePortalDirectory);
2844
+ this.app.resourceManager.registerPreActionHandler("multiPortals:updateOrCreate", normalizeMultiPortalSlugValues);
2845
+ this.app.resourceManager.registerPreActionHandler("multiPortals:updateOrCreate", preventUiLayoutRouteNameConflict);
2031
2846
  this.app.resourceManager.registerPreActionHandler("desktopRoutes:create", addDesktopRouteCreateMultiPortal, {
2032
2847
  after: UI_LAYOUT_DESKTOP_ROUTE_WRITE_LAYOUT_HANDLER_TAG
2033
2848
  });
@@ -2038,6 +2853,9 @@ class PluginMultiPortalServer extends import_server.Plugin {
2038
2853
  after: UI_LAYOUT_DESKTOP_ROUTE_WRITE_LAYOUT_HANDLER_TAG
2039
2854
  }
2040
2855
  );
2856
+ this.app.resourceManager.registerPreActionHandler("desktopRoutes:update", guardDesktopRouteUpdateScope);
2857
+ this.app.resourceManager.registerPreActionHandler("desktopRoutes:move", guardDesktopRouteMoveScope);
2858
+ this.app.resourceManager.registerPreActionHandler("desktopRoutes:destroy", destroyScopedDesktopRoutes);
2041
2859
  }
2042
2860
  async load() {
2043
2861
  this.app.acl.registerSnippet({
@@ -2078,6 +2896,7 @@ class PluginMultiPortalServer extends import_server.Plugin {
2078
2896
  this.app.db.on("roles.beforeCreate", (role) => {
2079
2897
  (0, import_ensureDefaultRoleMultiPortalAccess.applyDefaultRoleMultiPortalAccess)(role);
2080
2898
  });
2899
+ this.app.db.on("desktopRoutes.afterCreate", reconcileCreatedDesktopRoutePermissions);
2081
2900
  this.app.db.on("multiPortals.afterCreate", async (multiPortal, options) => {
2082
2901
  await grantDefaultAccessToNewMultiPortal(this.app.db, multiPortal, options);
2083
2902
  await this.publishAppManifestItem(multiPortal, options);
@@ -2102,14 +2921,18 @@ class PluginMultiPortalServer extends import_server.Plugin {
2102
2921
  });
2103
2922
  }
2104
2923
  async install() {
2105
- await ensureDefaultMultiPortals(this.db);
2106
- await (0, import_ensureDefaultRoleMultiPortalAccess.ensureDefaultRoleMultiPortalAccess)(this.db);
2924
+ const version = await this.app.version.get();
2925
+ if (!version) {
2926
+ await (0, import_ensureDefaultRoleMultiPortalAccess.ensureDefaultRoleMultiPortalAccess)(this.db);
2927
+ await seedFreshMultiPortals(this.db);
2928
+ } else {
2929
+ await seedHistoricalMultiPortals(this.db);
2930
+ }
2107
2931
  await this.reconcilePortalStorage();
2108
2932
  await this.reconcileAppManifest();
2109
2933
  }
2110
2934
  async afterEnable() {
2111
- await ensureDefaultMultiPortals(this.db);
2112
- await (0, import_ensureDefaultRoleMultiPortalAccess.ensureDefaultRoleMultiPortalAccess)(this.db);
2935
+ await repairFixedLayoutMultiPortalRecords(this.db);
2113
2936
  await this.reconcilePortalStorage();
2114
2937
  await this.reconcileAppManifest();
2115
2938
  }