@nocobase/plugin-multi-portal 2.2.0-alpha.11 → 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
  }
@@ -556,7 +596,8 @@ async function buildPortalStorageItem(portalDir, item) {
556
596
  const logPath = getPortalStorageLogPath(item);
557
597
  const buildEnv = getPortalStorageCommandEnv({
558
598
  NOCOBASE_API_URL: getPortalStorageApiUrl(item.appName),
559
- NOCOBASE_PORTAL_BASE: getPortalDeployBasePath(item.appName, item.portalName)
599
+ NOCOBASE_PORTAL_BASE: getPortalDeployBasePath(item.appName, item.portalName),
600
+ COREPACK_ENABLE_PROJECT_SPEC: "0"
560
601
  });
561
602
  await appendPortalStorageLog(logPath, `Building portal ${item.appName}/${item.portalName}.`);
562
603
  await appendPortalStorageLog(
@@ -790,29 +831,6 @@ async function replacePortalSourceFromArchive(params) {
790
831
  await import_fs.default.promises.rm(tarPath, { force: true });
791
832
  }
792
833
  }
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
834
  function uniqueDesktopRouteIds(routeIds) {
817
835
  const seen = /* @__PURE__ */ new Set();
818
836
  return routeIds.filter((routeId) => {
@@ -867,14 +885,25 @@ function getExplicitRequestedLayoutUid(layout) {
867
885
  return uid;
868
886
  }
869
887
  }
888
+ function hasActionParam(params, key) {
889
+ return isRecordLike(params) && Object.prototype.hasOwnProperty.call(params, key);
890
+ }
870
891
  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) {
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) {
875
897
  ctx.throw(400, "layout and portal cannot be used together");
876
898
  return;
877
899
  }
900
+ if (!hasPortalScope) {
901
+ return;
902
+ }
903
+ if (!portalUid) {
904
+ ctx.throw(400, "Invalid portal scope");
905
+ return;
906
+ }
878
907
  return portalUid;
879
908
  }
880
909
  function getCurrentRoles(ctx) {
@@ -899,27 +928,88 @@ function getMultiPortalNameFromValues(ctx) {
899
928
  return portalName;
900
929
  }
901
930
  }
902
- function getMultiPortalWriteValues(ctx) {
931
+ function getMultiPortalWriteRecords(ctx) {
903
932
  var _a;
904
933
  const values = (_a = ctx.action) == null ? void 0 : _a.params.values;
905
- if (!values || typeof values !== "object" || Array.isArray(values)) {
906
- 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;
970
+ }
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 }];
907
987
  }
908
- return values;
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 }];
909
994
  }
910
995
  function isTruthyActionBoolean(value) {
911
996
  return value === true || value === 1 || value === "1" || value === "true";
912
997
  }
913
998
  async function captureSkipCreatePortalDirectory(ctx, next) {
914
- const values = getMultiPortalWriteValues(ctx);
915
- 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) {
916
1004
  await next();
917
1005
  return;
918
1006
  }
919
- ctx[SKIP_CREATE_PORTAL_DIRECTORY] = isTruthyActionBoolean(
920
- values.skipCreatePortalDirectory
1007
+ ctx[SKIP_CREATE_PORTAL_DIRECTORY] = recordsWithOption.some(
1008
+ (record) => isTruthyActionBoolean(record.skipCreatePortalDirectory)
921
1009
  );
922
- delete values.skipCreatePortalDirectory;
1010
+ for (const record of recordsWithOption) {
1011
+ delete record.skipCreatePortalDirectory;
1012
+ }
923
1013
  await next();
924
1014
  }
925
1015
  function shouldSkipCreatePortalDirectory(options) {
@@ -927,156 +1017,180 @@ function shouldSkipCreatePortalDirectory(options) {
927
1017
  return ((_a = options == null ? void 0 : options.context) == null ? void 0 : _a[SKIP_CREATE_PORTAL_DIRECTORY]) === true;
928
1018
  }
929
1019
  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;
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}`;
939
1035
  }
940
- const slug = portalName.trim();
941
- if (!slug) {
942
- await next();
943
- 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;
944
1055
  }
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;
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;
948
1064
  }
949
- values.portalName = slug;
950
- values.routePath = `/${slug}`;
951
- await next();
952
1065
  }
953
- async function ensureDefaultMultiPortals(db, options) {
1066
+ async function repairFixedLayoutMultiPortalRecords(db) {
954
1067
  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
1068
+ for (const portal of getFixedLayoutMultiPortalRecords()) {
1069
+ const existing = await repository.findOne({
1070
+ filterByTk: portal.uid,
1071
+ fields: ["uid", "portalType", "portalName", "routePath", "uiLayoutUid"]
965
1072
  });
966
- return;
967
- }
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) {
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
+ }
978
1076
  await repository.update({
979
- filterByTk: defaultPortal.uid,
980
- values: protectedValues,
981
- transaction: options == null ? void 0 : options.transaction
1077
+ filterByTk: portal.uid,
1078
+ values: {
1079
+ portalType: portal.portalType
1080
+ }
982
1081
  });
983
1082
  }
984
1083
  }
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;
991
- }
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;
1084
+ async function seedFreshMultiPortals(db) {
1085
+ for (const portal of getFreshMultiPortalRecords()) {
1086
+ await createDefaultMultiPortalBestEffort(db, portal);
1004
1087
  }
1005
- if (Object.prototype.hasOwnProperty.call(rawValues, "icon")) {
1006
- values.icon = rawValues.icon;
1088
+ }
1089
+ async function seedHistoricalMultiPortals(db) {
1090
+ if (getInitPortalType() === "ai") {
1091
+ await createDefaultMultiPortalBestEffort(db, getDefaultMultiPortalRecord());
1007
1092
  }
1008
- if (Object.prototype.hasOwnProperty.call(rawValues, "enabled")) {
1009
- values.enabled = rawValues.enabled;
1093
+ for (const portal of getFixedLayoutMultiPortalRecords()) {
1094
+ await createDefaultMultiPortalBestEffort(db, portal);
1010
1095
  }
1011
- (_c = ctx.action) == null ? void 0 : _c.mergeParams(
1012
- {
1013
- values
1014
- },
1015
- {
1016
- values: "overwrite"
1017
- }
1018
- );
1019
- await next();
1096
+ await repairFixedLayoutMultiPortalRecords(db);
1020
1097
  }
1021
1098
  async function preventUiLayoutRouteNameConflict(ctx, next) {
1022
- 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;
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
+ }
1031
1128
  }
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;
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
+ }
1041
1141
  }
1042
1142
  await next();
1043
1143
  }
1044
- async function findRequestedMultiPortal(ctx) {
1045
- var _a;
1144
+ async function findRequestedMultiPortal(ctx, transaction) {
1046
1145
  const portalUid = getRequestedMultiPortalUid(ctx);
1047
1146
  if (portalUid === void 0) {
1048
1147
  return {
1049
1148
  requested: false
1050
1149
  };
1051
1150
  }
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
1151
  const portal = await ctx.db.getRepository("multiPortals").findOne({
1067
1152
  filter: {
1068
1153
  uid: portalUid
1069
1154
  },
1070
- fields: ["uid", "uiLayoutUid", "enabled"]
1155
+ fields: ["uid", "portalType", "uiLayoutUid", "enabled"],
1156
+ ...transaction ? { lock: transaction.LOCK.UPDATE } : {},
1157
+ transaction
1071
1158
  });
1072
- if ((portal == null ? void 0 : portal.get("enabled")) === true) {
1073
- return {
1074
- portal,
1075
- requested: true
1076
- };
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`);
1077
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`);
1180
+ }
1181
+ const usesLayoutPermissions = (0, import_constants.isDefaultLayoutMultiPortalUid)(portalUid);
1078
1182
  return {
1079
- 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
+ }
1080
1194
  };
1081
1195
  }
1082
1196
  function getDesktopRoutePortalFilter(multiPortalUid) {
@@ -1087,49 +1201,90 @@ function getDesktopRoutePortalFilter(multiPortalUid) {
1087
1201
  function isDesktopRouteCreateValue(value) {
1088
1202
  return !!value && typeof value === "object" && !Array.isArray(value);
1089
1203
  }
1090
- function hasDesktopRouteField(value, field) {
1091
- 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
+ };
1092
1217
  }
1093
- function pickExplicitDesktopRouteUiLayouts(value) {
1218
+ function collectExplicitDesktopRouteParentIds(value) {
1094
1219
  if (Array.isArray(value)) {
1095
- return value.map((item) => pickExplicitDesktopRouteUiLayouts(item));
1220
+ return uniqueDesktopRouteIds(value.flatMap((item) => collectExplicitDesktopRouteParentIds(item)));
1096
1221
  }
1097
1222
  if (!isDesktopRouteCreateValue(value)) {
1098
- return;
1223
+ return [];
1099
1224
  }
1100
- const route = {};
1101
- if (hasDesktopRouteField(value, "uiLayouts")) {
1102
- route.uiLayouts = value.uiLayouts;
1225
+ const parentIds = [];
1226
+ if (typeof value.parentId === "string" || typeof value.parentId === "number") {
1227
+ parentIds.push(value.parentId);
1103
1228
  }
1104
1229
  if (Array.isArray(value.children)) {
1105
- route.children = value.children.map((child) => pickExplicitDesktopRouteUiLayouts(child));
1230
+ parentIds.push(...value.children.flatMap((child) => collectExplicitDesktopRouteParentIds(child)));
1106
1231
  }
1107
- return route;
1232
+ return uniqueDesktopRouteIds(parentIds);
1108
1233
  }
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]));
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;
1113
1238
  }
1114
- if (!isDesktopRouteCreateValue(value)) {
1115
- 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");
1116
1250
  }
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
1251
  }
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();
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
+ }
1133
1288
  }
1134
1289
  async function canAccessMultiPortal(ctx, multiPortalUid) {
1135
1290
  const currentRoles = getCurrentRoles(ctx);
@@ -1379,25 +1534,17 @@ async function prepareAccessibleDesktopRoutesForMultiPortal(ctx) {
1379
1534
  if (!portalRequest.requested) {
1380
1535
  return;
1381
1536
  }
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) {
1537
+ const scope = portalRequest.scope;
1538
+ if (!scope) {
1389
1539
  return null;
1390
1540
  }
1391
- if (!await canAccessMultiPortal(ctx, portalUid)) {
1541
+ if (scope.relation === "multiPortals" && !await canAccessMultiPortal(ctx, scope.portalUid)) {
1392
1542
  return null;
1393
1543
  }
1394
1544
  if ((_a = ctx.action) == null ? void 0 : _a.params) {
1395
- ctx.action.params.layout = uiLayoutUid;
1545
+ ctx.action.params.layout = scope.uiLayoutUid;
1396
1546
  }
1397
- return {
1398
- portalUid,
1399
- uiLayoutUid
1400
- };
1547
+ return scope;
1401
1548
  }
1402
1549
  async function addMultiPortalListAccessibleGuard(ctx, next) {
1403
1550
  const portalContext = await prepareAccessibleDesktopRoutesForMultiPortal(ctx);
@@ -1406,10 +1553,13 @@ async function addMultiPortalListAccessibleGuard(ctx, next) {
1406
1553
  return;
1407
1554
  }
1408
1555
  await next();
1409
- if (portalContext) {
1556
+ if ((portalContext == null ? void 0 : portalContext.relation) === "multiPortals") {
1410
1557
  await replaceListAccessibleRoutesWithPortalScopedRoutes(ctx, portalContext);
1411
1558
  return;
1412
1559
  }
1560
+ if ((portalContext == null ? void 0 : portalContext.relation) === "uiLayouts") {
1561
+ return;
1562
+ }
1413
1563
  await removeMultiPortalOwnedRoutesFromListResponse(ctx);
1414
1564
  }
1415
1565
  async function addMultiPortalGetAccessibleGuard(ctx, next) {
@@ -1420,26 +1570,36 @@ async function addMultiPortalGetAccessibleGuard(ctx, next) {
1420
1570
  return;
1421
1571
  }
1422
1572
  await next();
1423
- if (portalContext) {
1573
+ if ((portalContext == null ? void 0 : portalContext.relation) === "multiPortals") {
1424
1574
  await replaceGetAccessibleRouteWithPortalScopedRoute(ctx, portalContext);
1425
1575
  return;
1426
1576
  }
1577
+ if ((portalContext == null ? void 0 : portalContext.relation) === "uiLayouts") {
1578
+ return;
1579
+ }
1427
1580
  await removeMultiPortalOwnedRouteFromGetResponse(ctx);
1428
1581
  }
1429
1582
  async function mapMultiPortalLayoutToUiLayoutForRolePermissionTargets(ctx, next) {
1583
+ var _a;
1430
1584
  const portalRequest = await findRequestedMultiPortal(ctx);
1431
1585
  if (portalRequest.requested && !portalRequest.portal) {
1432
1586
  ctx.status = 200;
1433
1587
  ctx.body = [];
1434
1588
  return;
1435
1589
  }
1436
- const portal = portalRequest.portal;
1437
- const portalUid = portal == null ? void 0 : portal.get("uid");
1438
- 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") {
1439
1599
  const routes = await ctx.db.getRepository("desktopRoutes").find({
1440
1600
  tree: true,
1441
1601
  sort: "sort",
1442
- filter: getDesktopRoutePortalFilter(portalUid),
1602
+ filter: scope.filter,
1443
1603
  fields: [...DESKTOP_ROUTE_ROLE_PERMISSION_TARGET_FIELDS]
1444
1604
  });
1445
1605
  ctx.status = 200;
@@ -1450,38 +1610,585 @@ async function mapMultiPortalLayoutToUiLayoutForRolePermissionTargets(ctx, next)
1450
1610
  await removeMultiPortalOwnedRoutesFromListResponse(ctx);
1451
1611
  }
1452
1612
  async function addDesktopRouteCreateMultiPortal(ctx, next) {
1453
- var _a, _b;
1613
+ var _a, _b, _c;
1454
1614
  const portalRequest = await findRequestedMultiPortal(ctx);
1455
1615
  if (portalRequest.requested && !portalRequest.portal) {
1456
1616
  ctx.throw(400, "Invalid portal scope");
1457
1617
  return;
1458
1618
  }
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(
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(
1464
1626
  {
1465
- 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)
1466
1628
  },
1467
1629
  {
1468
1630
  values: "overwrite"
1469
1631
  }
1470
1632
  );
1471
1633
  await next();
1472
- const desktopRouteIds = collectDesktopRouteIds(ctx.body);
1473
- await removeLayoutRoutePermissionsFromPortalOnlyRoutes(ctx, desktopRouteIds);
1474
- await grantDefaultRouteAccessToNewMultiPortalRoutes(ctx, portalUid, desktopRouteIds);
1475
1634
  return;
1476
1635
  }
1477
1636
  await next();
1478
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
+ }
1479
2186
  async function listEnabledMultiPortals(ctx, next) {
1480
2187
  const records = await ctx.db.getRepository("multiPortals").find({
1481
- filter: withCustomMultiPortalFilter({
2188
+ filter: {
1482
2189
  enabled: true,
1483
2190
  "uiLayout.enabled": true
1484
- }),
2191
+ },
1485
2192
  fields: [...MULTI_PORTAL_RUNTIME_QUERY_FIELDS],
1486
2193
  appends: ["uiLayout"],
1487
2194
  sort: ["uid"]
@@ -1491,40 +2198,33 @@ async function listEnabledMultiPortals(ctx, next) {
1491
2198
  }
1492
2199
  async function listAccessibleMultiPortals(ctx, next) {
1493
2200
  const accessiblePortalUids = await listCurrentRoleAccessibleMultiPortalUids(ctx);
1494
- if (Array.isArray(accessiblePortalUids) && accessiblePortalUids.length === 0) {
1495
- ctx.body = [];
1496
- await next();
1497
- return;
1498
- }
1499
- const filter = {
1500
- enabled: true,
1501
- "uiLayout.enabled": true
1502
- };
1503
- if (Array.isArray(accessiblePortalUids)) {
1504
- filter.uid = accessiblePortalUids;
1505
- }
1506
2201
  const records = await ctx.db.getRepository("multiPortals").find({
1507
- filter: withCustomMultiPortalFilter(filter),
2202
+ filter: {
2203
+ enabled: true,
2204
+ "uiLayout.enabled": true
2205
+ },
1508
2206
  fields: [...MULTI_PORTAL_ACCESSIBLE_QUERY_FIELDS],
1509
2207
  appends: ["uiLayout"],
1510
2208
  sort: ["uid"]
1511
2209
  });
1512
- 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));
1513
2215
  await next();
1514
2216
  }
1515
- async function grantDefaultRouteAccessToNewMultiPortalRoutes(ctx, multiPortalUid, desktopRouteIds) {
2217
+ async function grantDefaultRouteAccessToNewMultiPortalRoutes(ctx, multiPortalUid, desktopRouteIds, transaction) {
1516
2218
  if (!desktopRouteIds.length) {
1517
2219
  return;
1518
2220
  }
1519
- if (!ctx.db.getCollection("rolesMultiPortalRoutePolicies") || !ctx.db.getCollection("rolesMultiPortalDesktopRoutes")) {
1520
- return;
1521
- }
1522
2221
  const routePolicies = await ctx.db.getRepository("rolesMultiPortalRoutePolicies").find({
1523
2222
  fields: ["roleName"],
1524
2223
  filter: {
1525
2224
  multiPortalUid,
1526
2225
  allowNewMenu: true
1527
- }
2226
+ },
2227
+ transaction
1528
2228
  });
1529
2229
  const roleNames = routePolicies.map((routePolicy) => routePolicy.get("roleName")).filter((roleName) => typeof roleName === "string" && !!roleName);
1530
2230
  if (!roleNames.length) {
@@ -1540,26 +2240,52 @@ async function grantDefaultRouteAccessToNewMultiPortalRoutes(ctx, multiPortalUid
1540
2240
  multiPortalUid,
1541
2241
  desktopRouteId
1542
2242
  },
1543
- context: ctx
2243
+ context: ctx,
2244
+ transaction
1544
2245
  });
1545
2246
  }
1546
2247
  }
1547
2248
  }
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) {
2249
+ async function removeLayoutRoutePermissionsFromPortalRoutes(ctx, desktopRouteIds, transaction) {
2250
+ if (!desktopRouteIds.length) {
1554
2251
  return;
1555
2252
  }
1556
2253
  await ctx.db.getRepository("rolesDesktopRoutes").destroy({
1557
2254
  filter: {
1558
- desktopRouteId: Array.from(portalOnlyRouteIds)
2255
+ desktopRouteId: desktopRouteIds
1559
2256
  },
1560
- context: ctx
2257
+ context: ctx,
2258
+ transaction
1561
2259
  });
1562
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
+ }
1563
2289
  async function grantDefaultAccessToNewMultiPortal(db, multiPortal, options) {
1564
2290
  if (multiPortal.get("enabled") !== true) {
1565
2291
  return;
@@ -1568,7 +2294,7 @@ async function grantDefaultAccessToNewMultiPortal(db, multiPortal, options) {
1568
2294
  if (typeof multiPortalUid !== "string" || !multiPortalUid) {
1569
2295
  return;
1570
2296
  }
1571
- if (isDefaultMultiPortalUid(multiPortalUid)) {
2297
+ if ((0, import_constants.isDefaultLayoutMultiPortalUid)(multiPortalUid)) {
1572
2298
  return;
1573
2299
  }
1574
2300
  if (!db.getCollection("roles") || !db.getCollection("rolesMultiPortals")) {
@@ -1602,7 +2328,7 @@ async function grantDefaultAccessToNewMultiPortal(db, multiPortal, options) {
1602
2328
  transaction: options == null ? void 0 : options.transaction
1603
2329
  });
1604
2330
  }
1605
- if (!db.getCollection("rolesMultiPortalRoutePolicies")) {
2331
+ if (multiPortal.get("portalType") === "ai" || !db.getCollection("rolesMultiPortalRoutePolicies")) {
1606
2332
  return;
1607
2333
  }
1608
2334
  const routePolicyRepository = db.getRepository("rolesMultiPortalRoutePolicies");
@@ -1913,10 +2639,10 @@ class PluginMultiPortalServer extends import_server.Plugin {
1913
2639
  }
1914
2640
  async listAppPortalManifest(options) {
1915
2641
  const records = await this.db.getRepository("multiPortals").find({
1916
- filter: withCustomMultiPortalFilter({
2642
+ filter: {
1917
2643
  enabled: true,
1918
2644
  "uiLayout.enabled": true
1919
- }),
2645
+ },
1920
2646
  fields: [...MULTI_PORTAL_ACCESSIBLE_QUERY_FIELDS],
1921
2647
  appends: ["uiLayout"],
1922
2648
  sort: ["uid"],
@@ -1952,7 +2678,7 @@ class PluginMultiPortalServer extends import_server.Plugin {
1952
2678
  const portalType = getRecordField(multiPortal, "portalType");
1953
2679
  const routePath = getRecordField(multiPortal, "routePath");
1954
2680
  const enabled = getRecordField(multiPortal, "enabled");
1955
- 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) {
1956
2682
  return null;
1957
2683
  }
1958
2684
  const uiLayoutUid = getRecordField(multiPortal, "uiLayoutUid");
@@ -2024,9 +2750,9 @@ class PluginMultiPortalServer extends import_server.Plugin {
2024
2750
  return;
2025
2751
  }
2026
2752
  const records = await this.db.getRepository("multiPortals").find({
2027
- filter: withCustomMultiPortalFilter({
2753
+ filter: {
2028
2754
  uiLayoutUid
2029
- }),
2755
+ },
2030
2756
  fields: [...MULTI_PORTAL_ACCESSIBLE_QUERY_FIELDS],
2031
2757
  transaction: options == null ? void 0 : options.transaction
2032
2758
  });
@@ -2085,6 +2811,9 @@ class PluginMultiPortalServer extends import_server.Plugin {
2085
2811
  }
2086
2812
  }
2087
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);
2088
2817
  this.app.resourceManager.registerPreActionHandler(
2089
2818
  "desktopRoutes:listAccessible",
2090
2819
  addMultiPortalListAccessibleGuard
@@ -2097,22 +2826,23 @@ class PluginMultiPortalServer extends import_server.Plugin {
2097
2826
  this.app.resourceManager.registerPreActionHandler("multiPortals:create", captureSkipCreatePortalDirectory);
2098
2827
  this.app.resourceManager.registerPreActionHandler("multiPortals:create", normalizeMultiPortalSlugValues);
2099
2828
  this.app.resourceManager.registerPreActionHandler("multiPortals:create", preventUiLayoutRouteNameConflict);
2100
- this.app.resourceManager.registerPreActionHandler("multiPortals:update", protectDefaultMultiPortalUpdate);
2829
+ this.app.resourceManager.registerPreActionHandler("multiPortals:update", preventMultiPortalBackingLayoutChange);
2101
2830
  this.app.resourceManager.registerPreActionHandler("multiPortals:update", normalizeMultiPortalSlugValues);
2102
2831
  this.app.resourceManager.registerPreActionHandler("multiPortals:update", preventUiLayoutRouteNameConflict);
2832
+ this.app.resourceManager.registerPreActionHandler(
2833
+ "multiPortals:firstOrCreate",
2834
+ preventMultiPortalBackingLayoutChange
2835
+ );
2103
2836
  this.app.resourceManager.registerPreActionHandler("multiPortals:firstOrCreate", captureSkipCreatePortalDirectory);
2104
2837
  this.app.resourceManager.registerPreActionHandler("multiPortals:firstOrCreate", normalizeMultiPortalSlugValues);
2105
2838
  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
2839
  this.app.resourceManager.registerPreActionHandler(
2110
- "desktopRoutes:updateOrCreate",
2111
- captureExplicitDesktopRouteUiLayouts,
2112
- {
2113
- before: UI_LAYOUT_DESKTOP_ROUTE_WRITE_LAYOUT_HANDLER_TAG
2114
- }
2840
+ "multiPortals:updateOrCreate",
2841
+ preventMultiPortalBackingLayoutChange
2115
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);
2116
2846
  this.app.resourceManager.registerPreActionHandler("desktopRoutes:create", addDesktopRouteCreateMultiPortal, {
2117
2847
  after: UI_LAYOUT_DESKTOP_ROUTE_WRITE_LAYOUT_HANDLER_TAG
2118
2848
  });
@@ -2123,6 +2853,9 @@ class PluginMultiPortalServer extends import_server.Plugin {
2123
2853
  after: UI_LAYOUT_DESKTOP_ROUTE_WRITE_LAYOUT_HANDLER_TAG
2124
2854
  }
2125
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);
2126
2859
  }
2127
2860
  async load() {
2128
2861
  this.app.acl.registerSnippet({
@@ -2163,6 +2896,7 @@ class PluginMultiPortalServer extends import_server.Plugin {
2163
2896
  this.app.db.on("roles.beforeCreate", (role) => {
2164
2897
  (0, import_ensureDefaultRoleMultiPortalAccess.applyDefaultRoleMultiPortalAccess)(role);
2165
2898
  });
2899
+ this.app.db.on("desktopRoutes.afterCreate", reconcileCreatedDesktopRoutePermissions);
2166
2900
  this.app.db.on("multiPortals.afterCreate", async (multiPortal, options) => {
2167
2901
  await grantDefaultAccessToNewMultiPortal(this.app.db, multiPortal, options);
2168
2902
  await this.publishAppManifestItem(multiPortal, options);
@@ -2187,14 +2921,18 @@ class PluginMultiPortalServer extends import_server.Plugin {
2187
2921
  });
2188
2922
  }
2189
2923
  async install() {
2190
- await ensureDefaultMultiPortals(this.db);
2191
- 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
+ }
2192
2931
  await this.reconcilePortalStorage();
2193
2932
  await this.reconcileAppManifest();
2194
2933
  }
2195
2934
  async afterEnable() {
2196
- await ensureDefaultMultiPortals(this.db);
2197
- await (0, import_ensureDefaultRoleMultiPortalAccess.ensureDefaultRoleMultiPortalAccess)(this.db);
2935
+ await repairFixedLayoutMultiPortalRecords(this.db);
2198
2936
  await this.reconcilePortalStorage();
2199
2937
  await this.reconcileAppManifest();
2200
2938
  }