@gzl10/nexus-backend 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var init_package = __esm({
14
14
  "package.json"() {
15
15
  package_default = {
16
16
  name: "@gzl10/nexus-backend",
17
- version: "0.17.0",
17
+ version: "0.18.0",
18
18
  description: "Backend as a Service (BaaS) with Express 5, Knex and CASL",
19
19
  type: "module",
20
20
  main: "./dist/index.js",
@@ -70,7 +70,7 @@ var init_package = __esm({
70
70
  "jwt"
71
71
  ],
72
72
  scripts: {
73
- dev: "tsx watch src/main.ts",
73
+ dev: "node --watch-path=./src --import tsx/esm src/main.ts",
74
74
  build: "tsup",
75
75
  start: "node dist/main.js",
76
76
  nexus: "tsx src/cli.ts",
@@ -126,26 +126,6 @@ var init_package = __esm({
126
126
  zod: "^3.24.0"
127
127
  },
128
128
  devDependencies: {
129
- "@gzl10/nexus-plugin-ai": "workspace:^",
130
- "@gzl10/nexus-plugin-auth-providers": "workspace:^",
131
- "@gzl10/nexus-plugin-charts": "workspace:^",
132
- "@gzl10/nexus-plugin-cms": "workspace:^",
133
- "@gzl10/nexus-plugin-compliance": "workspace:^",
134
- "@gzl10/nexus-plugin-docker": "workspace:^",
135
- "@gzl10/nexus-plugin-feeds": "workspace:^",
136
- "@gzl10/nexus-plugin-importer": "workspace:^",
137
- "@gzl10/nexus-plugin-links": "workspace:*",
138
- "@gzl10/nexus-plugin-notifications": "workspace:*",
139
- "@gzl10/nexus-plugin-oidc-server": "workspace:^",
140
- "@gzl10/nexus-plugin-plane": "workspace:^",
141
- "@gzl10/nexus-plugin-prisma": "workspace:^",
142
- "@gzl10/nexus-plugin-remote": "workspace:*",
143
- "@gzl10/nexus-plugin-schedules": "workspace:*",
144
- "@gzl10/nexus-plugin-scim": "workspace:^",
145
- "@gzl10/nexus-plugin-scraper": "workspace:^",
146
- "@gzl10/nexus-plugin-secrets": "workspace:^",
147
- "@gzl10/nexus-plugin-tags": "workspace:*",
148
- "@gzl10/nexus-plugin-webhooks": "workspace:*",
149
129
  "@types/bcryptjs": "^2.4.0",
150
130
  "@types/compression": "^1.8.1",
151
131
  "@types/cookie-parser": "^1.4.10",
@@ -158,7 +138,16 @@ var init_package = __esm({
158
138
  "pino-pretty": "^13.1.3",
159
139
  "socket.io-client": "^4.8.3",
160
140
  supertest: "^7.2.2",
161
- tsx: "^4.21.0"
141
+ tsx: "^4.21.0",
142
+ vite: "^8.0.3"
143
+ },
144
+ peerDependencies: {
145
+ vite: ">=6.0.0"
146
+ },
147
+ peerDependenciesMeta: {
148
+ vite: {
149
+ optional: true
150
+ }
162
151
  },
163
152
  publishConfig: {
164
153
  access: "public",
@@ -1160,7 +1149,7 @@ var init_table_prefix = __esm({
1160
1149
  }
1161
1150
  });
1162
1151
 
1163
- // src/engine/store.ts
1152
+ // src/engine/module-store.ts
1164
1153
  function resetStore() {
1165
1154
  moduleStore.modules.length = 0;
1166
1155
  moduleStore.plugins.clear();
@@ -1171,8 +1160,8 @@ function resetStore() {
1171
1160
  moduleStore.tableToSubject.clear();
1172
1161
  }
1173
1162
  var moduleStore;
1174
- var init_store = __esm({
1175
- "src/engine/store.ts"() {
1163
+ var init_module_store = __esm({
1164
+ "src/engine/module-store.ts"() {
1176
1165
  "use strict";
1177
1166
  moduleStore = {
1178
1167
  /** Registered modules with source metadata */
@@ -1225,7 +1214,7 @@ var init_id = __esm({
1225
1214
  }
1226
1215
  });
1227
1216
 
1228
- // src/engine/extractors.ts
1217
+ // src/engine/definition-extractors.ts
1229
1218
  function getTableAndSubject(def) {
1230
1219
  const caslSubject = def.casl?.subject;
1231
1220
  if (!TYPES_WITH_TABLE.has(def.type)) {
@@ -1293,8 +1282,8 @@ function validateModuleDependencies(modules) {
1293
1282
  }
1294
1283
  }
1295
1284
  var TYPES_WITH_TABLE;
1296
- var init_extractors = __esm({
1297
- "src/engine/extractors.ts"() {
1285
+ var init_definition_extractors = __esm({
1286
+ "src/engine/definition-extractors.ts"() {
1298
1287
  "use strict";
1299
1288
  TYPES_WITH_TABLE = /* @__PURE__ */ new Set(["collection", "reference", "event", "config", "temp", "view", void 0]);
1300
1289
  }
@@ -1414,9 +1403,9 @@ var init_registry = __esm({
1414
1403
  "use strict";
1415
1404
  init_plugin_ops();
1416
1405
  init_table_prefix();
1417
- init_store();
1406
+ init_module_store();
1418
1407
  init_id();
1419
- init_extractors();
1408
+ init_definition_extractors();
1420
1409
  }
1421
1410
  });
1422
1411
 
@@ -1479,7 +1468,28 @@ var init_paths = __esm({
1479
1468
  }
1480
1469
  });
1481
1470
 
1482
- // src/engine/queries.ts
1471
+ // src/engine/module-queries.ts
1472
+ var module_queries_exports = {};
1473
+ __export(module_queries_exports, {
1474
+ getCoreManifest: () => getCoreManifest,
1475
+ getCoreModules: () => getCoreModules,
1476
+ getModule: () => getModule,
1477
+ getModules: () => getModules,
1478
+ getOrderedModules: () => getOrderedModules,
1479
+ getOrderedModulesInternal: () => getOrderedModulesInternal,
1480
+ getPlugin: () => getPlugin,
1481
+ getPluginByCode: () => getPluginByCode,
1482
+ getPlugins: () => getPlugins,
1483
+ getRegisteredSubjects: () => getRegisteredSubjects,
1484
+ getSubjectForTable: () => getSubjectForTable,
1485
+ getUserManifest: () => getUserManifest,
1486
+ getUserModules: () => getUserModules,
1487
+ hasModule: () => hasModule,
1488
+ hasPlugin: () => hasPlugin,
1489
+ hasPluginByCode: () => hasPluginByCode,
1490
+ hasUserApp: () => hasUserApp,
1491
+ isValidSubject: () => isValidSubject
1492
+ });
1483
1493
  import { join as join4 } from "path";
1484
1494
  import { readFileSync as readFileSync3 } from "fs";
1485
1495
  function readPackageJson(dir) {
@@ -1575,10 +1585,10 @@ function getPluginByCode(code) {
1575
1585
  function hasPluginByCode(code) {
1576
1586
  return moduleStore.pluginsByCode.has(code);
1577
1587
  }
1578
- var init_queries = __esm({
1579
- "src/engine/queries.ts"() {
1588
+ var init_module_queries = __esm({
1589
+ "src/engine/module-queries.ts"() {
1580
1590
  "use strict";
1581
- init_store();
1591
+ init_module_store();
1582
1592
  init_paths();
1583
1593
  }
1584
1594
  });
@@ -2063,12 +2073,16 @@ var init_definitions = __esm({
2063
2073
  labelPlural: { en: "Masters", es: "Maestros" },
2064
2074
  labelField: "label",
2065
2075
  timestamps: true,
2076
+ availableDisplayModes: ["board"],
2077
+ groupBy: "type",
2078
+ groupableFields: ["type"],
2079
+ //columnDragFields: ['is_active'],
2066
2080
  fields: {
2067
2081
  id: useTextField2({
2068
2082
  label: { en: "ID", es: "ID" },
2069
2083
  required: true,
2070
2084
  size: 100,
2071
- hint: { en: "Format: type:code (e.g. countries:ES)", es: "Formato: type:code (ej. countries:ES)" },
2085
+ hidden: true,
2072
2086
  meta: { sortable: true }
2073
2087
  }),
2074
2088
  type: useTextField2({
@@ -2089,9 +2103,20 @@ var init_definitions = __esm({
2089
2103
  is_active: isActiveField,
2090
2104
  metadata: useJsonField({
2091
2105
  label: { en: "Metadata", es: "Metadatos" },
2092
- hint: { en: "Type-specific fields (symbol, flag, etc.)", es: "Campos espec\xEDficos del tipo" }
2106
+ hint: {
2107
+ en: "Type-specific fields (symbol, flag, etc.)",
2108
+ es: "Campos espec\xEDficos del tipo"
2109
+ }
2093
2110
  })
2094
2111
  },
2112
+ hooks: () => ({
2113
+ beforeCreate: async (data) => {
2114
+ if (data["type"] && data["code"] && !data["id"]) {
2115
+ data["id"] = `${data["type"]}:${data["code"]}`;
2116
+ }
2117
+ return data;
2118
+ }
2119
+ }),
2095
2120
  defaultSort: { field: "order", order: "asc" },
2096
2121
  indexes: [{ columns: ["type", "code"], unique: true }],
2097
2122
  casl: { subject: "Master", permissions: masterCaslPermissions }
@@ -2100,6 +2125,10 @@ var init_definitions = __esm({
2100
2125
  });
2101
2126
 
2102
2127
  // src/modules/masters/registry.ts
2128
+ var registry_exports = {};
2129
+ __export(registry_exports, {
2130
+ createMasterRegistry: () => createMasterRegistry
2131
+ });
2103
2132
  function createMasterRegistry() {
2104
2133
  const registrations = [];
2105
2134
  return {
@@ -8778,15 +8807,18 @@ function toEntityDefinitionDTO(def, _engine, moduleName) {
8778
8807
  const meta = field["meta"];
8779
8808
  return meta?.["sortable"] === true && !field["hidden"];
8780
8809
  });
8810
+ const explicitGroupable = def["groupableFields"];
8781
8811
  const groupableInputTypes = ["select", "switch", "checkbox", "radio", "tags"];
8782
- const groupableFields = fieldEntries.filter(([, f]) => {
8812
+ const autoGroupableFields = fieldEntries.filter(([, f]) => {
8783
8813
  const field = f;
8784
8814
  const inputType = field["input"];
8785
8815
  return groupableInputTypes.includes(inputType ?? "") && !field["hidden"];
8786
8816
  });
8817
+ const resolvedGroupableFields = explicitGroupable ?? (autoGroupableFields.length > 0 ? autoGroupableFields.map(([name]) => name) : void 0);
8787
8818
  const entityType = def["type"] ?? "collection";
8788
8819
  const explicitDisplayMode = def["displayMode"];
8789
- const defaultDisplayMode = explicitDisplayMode ?? (["tree", "dag"].includes(entityType) ? "tree" : "table");
8820
+ const explicitAvailableModes = def["availableDisplayModes"];
8821
+ const defaultDisplayMode = explicitDisplayMode ?? (explicitAvailableModes?.length === 1 ? explicitAvailableModes[0] : void 0) ?? (["tree", "dag"].includes(entityType) ? "tree" : "table");
8790
8822
  const entityIdent = def["table"] ?? def["key"];
8791
8823
  return {
8792
8824
  id: def._id,
@@ -8806,9 +8838,11 @@ function toEntityDefinitionDTO(def, _engine, moduleName) {
8806
8838
  displayMode: explicitDisplayMode,
8807
8839
  defaultDisplayMode,
8808
8840
  availableDisplayModes: (() => {
8841
+ const explicit = def["availableDisplayModes"];
8842
+ if (explicit?.length) return explicit;
8809
8843
  const modes = ["table", "list", "masonry"];
8810
8844
  if (["tree", "dag"].includes(entityType)) modes.push("tree");
8811
- if (groupableFields.length > 0) modes.push("board");
8845
+ if ((resolvedGroupableFields?.length ?? 0) > 0) modes.push("board");
8812
8846
  if (def["calendarFrom"]) modes.push("calendar");
8813
8847
  return modes;
8814
8848
  })(),
@@ -8818,10 +8852,8 @@ function toEntityDefinitionDTO(def, _engine, moduleName) {
8818
8852
  value: name,
8819
8853
  label: f["label"]
8820
8854
  })),
8821
- groupableFields: groupableFields.length > 0 ? groupableFields.map(([name, f]) => ({
8822
- value: name,
8823
- label: f["label"]
8824
- })) : void 0,
8855
+ groupableFields: resolvedGroupableFields,
8856
+ columnDragFields: def["columnDragFields"],
8825
8857
  groupBy: def["groupBy"],
8826
8858
  subgroupBy: def["subgroupBy"],
8827
8859
  calendarFrom: def["calendarFrom"],
@@ -8861,21 +8893,6 @@ function toModuleDTO(mod, ctx) {
8861
8893
  hasInit: !!mod.init
8862
8894
  };
8863
8895
  }
8864
- function getOrderedModulesViaContext(ctx) {
8865
- return ctx.engine.getModules();
8866
- }
8867
- async function runModuleSeedViaContext(mod, ctx) {
8868
- if (!mod.seed) {
8869
- return false;
8870
- }
8871
- try {
8872
- await mod.seed(ctx);
8873
- return true;
8874
- } catch (err) {
8875
- ctx.core.logger.error({ module: mod.name, err }, "Seed failed");
8876
- return false;
8877
- }
8878
- }
8879
8896
  var init_system_helpers = __esm({
8880
8897
  "src/modules/system/system.helpers.ts"() {
8881
8898
  "use strict";
@@ -9110,7 +9127,8 @@ function createSystemController(ctx) {
9110
9127
  const plugins = engine.getPlugins();
9111
9128
  const body = {
9112
9129
  version: manifest.version,
9113
- plugins: plugins.map((p) => p.code)
9130
+ plugins: plugins.map((p) => p.code),
9131
+ locales: ctx.locales
9114
9132
  };
9115
9133
  res.json(body);
9116
9134
  }
@@ -9997,7 +10015,6 @@ var SYSTEM_TABLES, factoryResetAction;
9997
10015
  var init_factory_reset_action = __esm({
9998
10016
  "src/modules/system/actions/factory-reset.action.ts"() {
9999
10017
  "use strict";
10000
- init_system_helpers();
10001
10018
  SYSTEM_TABLES = /* @__PURE__ */ new Set([
10002
10019
  "_nexus_migrations",
10003
10020
  "_nexus_migration_lock",
@@ -10040,8 +10057,8 @@ var init_factory_reset_action = __esm({
10040
10057
  source: "core:system",
10041
10058
  action: "factory_reset",
10042
10059
  actorId: authReq.user?.id,
10043
- ip: req.ip,
10044
- userAgent: req.headers["user-agent"]
10060
+ ip: req?.ip,
10061
+ userAgent: req?.headers["user-agent"]
10045
10062
  });
10046
10063
  await new Promise((resolve2) => setImmediate(resolve2));
10047
10064
  const allTables = await getAllTables(knex3);
@@ -10079,11 +10096,11 @@ var init_factory_reset_action = __esm({
10079
10096
  } catch {
10080
10097
  }
10081
10098
  ctx.core.logger.info({ tables: dataTables.length }, "All data tables cleared");
10082
- const modules = getOrderedModulesViaContext(ctx);
10099
+ const modules = ctx.engine.getModules();
10083
10100
  let modulesSeeded = 0;
10084
10101
  for (const mod of modules) {
10085
10102
  try {
10086
- const seeded = await runModuleSeedViaContext(mod, ctx);
10103
+ const seeded = await ctx.db.seedModule(mod);
10087
10104
  if (seeded) modulesSeeded++;
10088
10105
  } catch (err) {
10089
10106
  ctx.core.logger.error({ module: mod.name, err }, "Seed failed during factory reset");
@@ -10137,8 +10154,8 @@ var init_restart_server_action = __esm({
10137
10154
  source: "core:system",
10138
10155
  action: "server_restart",
10139
10156
  actorId: authReq.user?.id,
10140
- ip: req.ip,
10141
- userAgent: req.headers["user-agent"]
10157
+ ip: req?.ip,
10158
+ userAgent: req?.headers["user-agent"]
10142
10159
  });
10143
10160
  setTimeout(() => process.exit(0), 500);
10144
10161
  return { success: true, message: "Server is restarting..." };
@@ -11760,7 +11777,7 @@ function createUploadMiddleware(ctx, options) {
11760
11777
  const rateLimit2 = ctx.core.middleware.rateLimit({
11761
11778
  windowMs: 60 * 1e3,
11762
11779
  max: 20,
11763
- message: "Demasiados uploads, intenta en 1 minuto"
11780
+ message: "Too many uploads, try again in 1 minute"
11764
11781
  });
11765
11782
  const upload = multer({
11766
11783
  storage: multer.memoryStorage(),
@@ -12007,7 +12024,7 @@ function createStorageRoutes(ctx) {
12007
12024
  }
12008
12025
  res.status(201).json(results);
12009
12026
  };
12010
- const uploadRateLimit = ctx.core.middleware.rateLimit({ windowMs: 60 * 1e3, max: 20, message: "Demasiados uploads, intenta en 1 minuto" });
12027
+ const uploadRateLimit = ctx.core.middleware.rateLimit({ windowMs: 60 * 1e3, max: 20, message: "Too many uploads, try again in 1 minute" });
12011
12028
  if (auth) {
12012
12029
  router.post("/upload/multiple", uploadRateLimit, auth, upload.array("files", 10), uploadMultiple);
12013
12030
  } else {
@@ -12305,7 +12322,7 @@ var init_users_entity = __esm({
12305
12322
  ],
12306
12323
  nullable: true,
12307
12324
  meta: { sortable: true },
12308
- defaultValue: "es"
12325
+ defaultValue: "en"
12309
12326
  }),
12310
12327
  timezone: useSelectField9({
12311
12328
  label: { en: "Timezone", es: "Zona horaria" },
@@ -12383,7 +12400,7 @@ var init_users_entity = __esm({
12383
12400
  middleware: (ctx) => ctx.core.middleware.rateLimit({
12384
12401
  windowMs: 15 * 60 * 1e3,
12385
12402
  max: 5,
12386
- message: "Demasiados intentos, intenta en 15 minutos"
12403
+ message: "Too many attempts, try again in 15 minutes"
12387
12404
  }),
12388
12405
  handler: async (ctx, input) => {
12389
12406
  const {
@@ -16594,7 +16611,7 @@ var init_plugins_entity = __esm({
16594
16611
  label: "Plugins",
16595
16612
  icon: "mdi:puzzle",
16596
16613
  labelField: "code",
16597
- routePrefix: "/plugins",
16614
+ routePrefix: "/",
16598
16615
  defaultSort: { field: "name", order: "asc" },
16599
16616
  fields: {
16600
16617
  name: useTextField12({
@@ -17007,12 +17024,12 @@ var init_loader = __esm({
17007
17024
  "src/engine/loader.ts"() {
17008
17025
  "use strict";
17009
17026
  init_registry();
17010
- init_extractors();
17027
+ init_definition_extractors();
17011
17028
  init_modules();
17012
17029
  }
17013
17030
  });
17014
17031
 
17015
- // src/engine/subjectExtractor.ts
17032
+ // src/engine/subject-extractor.ts
17016
17033
  function getModuleSubjects(mod) {
17017
17034
  const subjects = /* @__PURE__ */ new Set();
17018
17035
  for (const def of mod.definitions ?? []) {
@@ -17021,10 +17038,10 @@ function getModuleSubjects(mod) {
17021
17038
  }
17022
17039
  return [...subjects];
17023
17040
  }
17024
- var init_subjectExtractor = __esm({
17025
- "src/engine/subjectExtractor.ts"() {
17041
+ var init_subject_extractor = __esm({
17042
+ "src/engine/subject-extractor.ts"() {
17026
17043
  "use strict";
17027
- init_extractors();
17044
+ init_definition_extractors();
17028
17045
  }
17029
17046
  });
17030
17047
 
@@ -17102,7 +17119,7 @@ function initSocketIO(httpServer, options) {
17102
17119
  maxHttpBufferSize: 1e6
17103
17120
  // 1MB - match Express json body limit
17104
17121
  });
17105
- logger.info({ maxHttpBufferSize: 1e6, cors: corsOrigin }, "Socket.IO initialized");
17122
+ logger.info({ cors: corsOrigin }, "Socket.IO initialized");
17106
17123
  io.use((socket, next) => {
17107
17124
  const token = socket.handshake.auth?.["token"] || socket.handshake.query?.["token"];
17108
17125
  if (token && typeof token === "string" && jwtSecret) {
@@ -17125,7 +17142,6 @@ function initSocketIO(httpServer, options) {
17125
17142
  logger.warn({ code: err.code, message: err.message }, "Socket.IO connection error");
17126
17143
  });
17127
17144
  io.on("connection", handleConnection);
17128
- logger.info("Socket.IO initialized");
17129
17145
  nexusEvents.emitEvent("socket.initialized");
17130
17146
  return io;
17131
17147
  }
@@ -17720,6 +17736,12 @@ var init_app_error = __esm({
17720
17736
 
17721
17737
  // src/core/abilities/ability.factory.ts
17722
17738
  import { AbilityBuilder, createMongoAbility } from "@casl/ability";
17739
+ function setSeedPermissions(perms) {
17740
+ seedPermissions = perms;
17741
+ }
17742
+ function clearSeedPermissions() {
17743
+ seedPermissions = null;
17744
+ }
17723
17745
  function setCustomCaslRules(fn) {
17724
17746
  customCaslRules = fn;
17725
17747
  }
@@ -17777,6 +17799,17 @@ async function defineAbilityFor(user, roleNames) {
17777
17799
  if (customCaslRules) {
17778
17800
  await customCaslRules(user, { can, cannot });
17779
17801
  }
17802
+ if (seedPermissions && !isSuperuser) {
17803
+ for (const roleName of roleNames) {
17804
+ const rolePerms = seedPermissions.get(roleName);
17805
+ if (!rolePerms) continue;
17806
+ for (const [subject2, actions] of rolePerms) {
17807
+ for (const action of actions) {
17808
+ can(action, subject2);
17809
+ }
17810
+ }
17811
+ }
17812
+ }
17780
17813
  return build();
17781
17814
  }
17782
17815
  function packRules(ability) {
@@ -17785,11 +17818,12 @@ function packRules(ability) {
17785
17818
  function unpackRules(rules) {
17786
17819
  return createMongoAbility(rules);
17787
17820
  }
17788
- var customCaslRules, entityDefinitionsRegistry, SUPERUSER_ROLES;
17821
+ var customCaslRules, seedPermissions, entityDefinitionsRegistry, SUPERUSER_ROLES;
17789
17822
  var init_ability_factory = __esm({
17790
17823
  "src/core/abilities/ability.factory.ts"() {
17791
17824
  "use strict";
17792
17825
  init_logger();
17826
+ seedPermissions = null;
17793
17827
  entityDefinitionsRegistry = null;
17794
17828
  SUPERUSER_ROLES = ["ADMIN", "OWNER"];
17795
17829
  }
@@ -18355,7 +18389,7 @@ var init_sequence = __esm({
18355
18389
  }
18356
18390
  });
18357
18391
 
18358
- // src/core/utils/error-handler.ts
18392
+ // src/core/utils/safe-json.ts
18359
18393
  function safeJsonParse(logger2, jsonString, fallback, context) {
18360
18394
  try {
18361
18395
  return JSON.parse(jsonString);
@@ -18364,8 +18398,8 @@ function safeJsonParse(logger2, jsonString, fallback, context) {
18364
18398
  return fallback;
18365
18399
  }
18366
18400
  }
18367
- var init_error_handler = __esm({
18368
- "src/core/utils/error-handler.ts"() {
18401
+ var init_safe_json = __esm({
18402
+ "src/core/utils/safe-json.ts"() {
18369
18403
  "use strict";
18370
18404
  }
18371
18405
  });
@@ -18374,14 +18408,15 @@ var init_error_handler = __esm({
18374
18408
  import express from "express";
18375
18409
  import { resolve, join as join9 } from "path";
18376
18410
  import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
18377
- function createServeSPA(app) {
18378
- return (endpoint, distPath, options = {}) => {
18411
+ function createServeSPA(app, httpServer) {
18412
+ return async (endpoint, distPath, options = {}) => {
18379
18413
  const {
18380
18414
  maxAge = "1d",
18381
18415
  etag = true,
18382
18416
  immutable = false,
18383
18417
  index = "index.html",
18384
- absolute = false
18418
+ absolute = false,
18419
+ viteSrc
18385
18420
  } = options;
18386
18421
  if (endpoint === "/api" || endpoint.startsWith("/api/")) {
18387
18422
  logger.error(`Cannot mount SPA on ${endpoint} - reserved for API routes`);
@@ -18392,58 +18427,117 @@ function createServeSPA(app) {
18392
18427
  return;
18393
18428
  }
18394
18429
  registeredEndpoints.add(endpoint);
18395
- let resolvedPath;
18396
- if (absolute) {
18397
- resolvedPath = distPath;
18398
- } else {
18399
- const projectPath2 = resolve(getProjectPath(), distPath);
18400
- if (existsSync9(projectPath2)) {
18401
- resolvedPath = projectPath2;
18430
+ if (env.NODE_ENV === "development" && viteSrc) {
18431
+ const srcPath = resolve(getProjectPath(), viteSrc);
18432
+ if (!existsSync9(srcPath)) {
18433
+ logger.warn({ endpoint, viteSrc, resolved: srcPath }, "Vite source not found \u2014 falling back to static");
18402
18434
  } else {
18403
- resolvedPath = resolve(getLibPath(), distPath);
18435
+ const mounted = await mountViteDevMiddleware(app, endpoint, srcPath, httpServer);
18436
+ if (mounted) return;
18404
18437
  }
18405
18438
  }
18406
- if (!existsSync9(resolvedPath)) {
18407
- logger.warn({ endpoint, distPath, hint: "Build the frontend first" }, `SPA directory not found: ${resolvedPath}`);
18408
- return;
18439
+ mountStaticSPA(app, endpoint, distPath, { maxAge, etag, immutable, index, absolute });
18440
+ };
18441
+ }
18442
+ async function mountViteDevMiddleware(app, endpoint, srcPath, httpServer) {
18443
+ try {
18444
+ const vite = await import("vite");
18445
+ const apiUrl = env.BACKEND_URL ? `${env.BACKEND_URL}/api/v1` : "/api/v1";
18446
+ const server2 = await vite.createServer({
18447
+ root: srcPath,
18448
+ server: {
18449
+ middlewareMode: true,
18450
+ allowedHosts: true,
18451
+ hmr: httpServer ? { server: httpServer } : true
18452
+ },
18453
+ plugins: [{
18454
+ name: "nexus-config-inject",
18455
+ transformIndexHtml(html) {
18456
+ const config3 = JSON.stringify({ apiUrl });
18457
+ return html.replace("</head>", `<script>window.__NEXUS__=${config3}</script>
18458
+ </head>`);
18459
+ }
18460
+ }],
18461
+ appType: "spa",
18462
+ clearScreen: false
18463
+ });
18464
+ viteServers.push(server2);
18465
+ if (endpoint === "/") {
18466
+ app.use(server2.middlewares);
18467
+ } else {
18468
+ app.use(endpoint, server2.middlewares);
18409
18469
  }
18410
- const indexPath = join9(resolvedPath, index);
18411
- if (!existsSync9(indexPath)) {
18412
- logger.warn({ endpoint, index }, `Index file not found: ${indexPath}`);
18413
- }
18414
- app.use(endpoint, express.static(resolvedPath, { maxAge, etag, immutable }));
18415
- let injectedHtml = "";
18416
- if (existsSync9(indexPath)) {
18417
- const rawHtml = readFileSync5(indexPath, "utf-8");
18418
- const apiUrl = env.BACKEND_URL ? `${env.BACKEND_URL}/api/v1` : "/api/v1";
18419
- const nexusConfig = JSON.stringify({ apiUrl });
18420
- injectedHtml = rawHtml.replace(
18421
- "</head>",
18422
- `<script>window.__NEXUS__=${nexusConfig}</script>
18423
- </head>`
18424
- );
18470
+ logger.info({ path: srcPath }, `Vite dev server mounted at ${endpoint} (HMR enabled)`);
18471
+ return true;
18472
+ } catch (err) {
18473
+ if (err.code === "ERR_MODULE_NOT_FOUND" || err.code === "MODULE_NOT_FOUND") {
18474
+ logger.warn(`vite not installed \u2014 falling back to static serving for ${endpoint}`);
18475
+ return false;
18425
18476
  }
18426
- const fallbackHandler = (_req, res) => {
18427
- if (!injectedHtml) {
18428
- res.status(404).send("index.html not found");
18429
- return;
18430
- }
18431
- res.set("Cache-Control", "no-cache, no-store, must-revalidate");
18432
- res.type("html").send(injectedHtml);
18433
- };
18434
- if (endpoint === "/") {
18435
- app.get("{*splat}", fallbackHandler);
18477
+ logger.error({ err }, `Failed to mount Vite dev server at ${endpoint}`);
18478
+ return false;
18479
+ }
18480
+ }
18481
+ function mountStaticSPA(app, endpoint, distPath, options) {
18482
+ const { maxAge, etag, immutable, index, absolute } = options;
18483
+ let resolvedPath;
18484
+ if (absolute) {
18485
+ resolvedPath = distPath;
18486
+ } else {
18487
+ const projectPath2 = resolve(getProjectPath(), distPath);
18488
+ if (existsSync9(projectPath2)) {
18489
+ resolvedPath = projectPath2;
18436
18490
  } else {
18437
- app.get(endpoint, fallbackHandler);
18438
- app.get(`${endpoint}/{*splat}`, fallbackHandler);
18491
+ resolvedPath = resolve(getLibPath(), distPath);
18492
+ }
18493
+ }
18494
+ if (!existsSync9(resolvedPath)) {
18495
+ logger.warn({ endpoint, distPath, hint: "Build the frontend first" }, `SPA directory not found: ${resolvedPath}`);
18496
+ return;
18497
+ }
18498
+ const indexPath = join9(resolvedPath, index);
18499
+ if (!existsSync9(indexPath)) {
18500
+ logger.warn({ endpoint, index }, `Index file not found: ${indexPath}`);
18501
+ }
18502
+ app.use(endpoint, express.static(resolvedPath, { maxAge, etag, immutable }));
18503
+ let injectedHtml = "";
18504
+ if (existsSync9(indexPath)) {
18505
+ const rawHtml = readFileSync5(indexPath, "utf-8");
18506
+ const apiUrl = env.BACKEND_URL ? `${env.BACKEND_URL}/api/v1` : "/api/v1";
18507
+ const nexusConfig = JSON.stringify({ apiUrl });
18508
+ injectedHtml = rawHtml.replace(
18509
+ "</head>",
18510
+ `<script>window.__NEXUS__=${nexusConfig}</script>
18511
+ </head>`
18512
+ );
18513
+ }
18514
+ const fallbackHandler = (_req, res) => {
18515
+ if (!injectedHtml) {
18516
+ res.status(404).send("index.html not found");
18517
+ return;
18439
18518
  }
18440
- logger.info({ path: resolvedPath }, `SPA mounted at ${endpoint}`);
18519
+ res.set("Cache-Control", "no-cache, no-store, must-revalidate");
18520
+ res.type("html").send(injectedHtml);
18441
18521
  };
18522
+ if (endpoint === "/") {
18523
+ app.get("{*splat}", fallbackHandler);
18524
+ } else {
18525
+ app.get(endpoint, fallbackHandler);
18526
+ app.get(`${endpoint}/{*splat}`, fallbackHandler);
18527
+ }
18528
+ logger.info({ path: resolvedPath }, `SPA mounted at ${endpoint}`);
18442
18529
  }
18443
- function resetServeSPAEndpoints() {
18530
+ async function resetServeSPA() {
18531
+ for (const server2 of viteServers) {
18532
+ try {
18533
+ await server2.close();
18534
+ } catch {
18535
+ }
18536
+ }
18537
+ viteServers.length = 0;
18444
18538
  registeredEndpoints.clear();
18445
18539
  }
18446
- var registeredEndpoints;
18540
+ var registeredEndpoints, viteServers;
18447
18541
  var init_spa_handler = __esm({
18448
18542
  "src/core/spa-handler.ts"() {
18449
18543
  "use strict";
@@ -18451,6 +18545,7 @@ var init_spa_handler = __esm({
18451
18545
  init_logger();
18452
18546
  init_env();
18453
18547
  registeredEndpoints = /* @__PURE__ */ new Set();
18548
+ viteServers = [];
18454
18549
  }
18455
18550
  });
18456
18551
 
@@ -19021,7 +19116,7 @@ var init_core = __esm({
19021
19116
  init_id();
19022
19117
  init_sequence();
19023
19118
  init_paths();
19024
- init_error_handler();
19119
+ init_safe_json();
19025
19120
  init_spa_handler();
19026
19121
  init_cache();
19027
19122
  init_jwt();
@@ -19736,7 +19831,9 @@ var init_base_service = __esm({
19736
19831
  const countResult = await qb.clone().count("* as count").first();
19737
19832
  const total = Number(countResult?.count ?? 0);
19738
19833
  qb = this.applySortingWithDefaults(qb, query);
19739
- qb = qb.limit(limit).offset(offset);
19834
+ if (limit > 0) {
19835
+ qb = qb.limit(limit).offset(offset);
19836
+ }
19740
19837
  const rawItems = await qb;
19741
19838
  const items = this.parseJsonFieldsFromArray(rawItems);
19742
19839
  const processedItems = await this.afterFindAll(items);
@@ -19821,7 +19918,7 @@ var init_base_service = __esm({
19821
19918
  });
19822
19919
  }
19823
19920
  const total = result.length;
19824
- const paginatedItems = result.slice(offset, offset + limit);
19921
+ const paginatedItems = limit === 0 ? result : result.slice(offset, offset + limit);
19825
19922
  return this.buildPaginatedResult(paginatedItems, total, page, limit);
19826
19923
  }
19827
19924
  /**
@@ -20076,14 +20173,14 @@ var init_base_service = __esm({
20076
20173
  * Build paginated result from items and count
20077
20174
  */
20078
20175
  buildPaginatedResult(items, total, page, limit) {
20079
- const totalPages = Math.ceil(total / limit);
20176
+ const totalPages = limit === 0 ? 1 : Math.ceil(total / limit);
20080
20177
  return {
20081
20178
  items,
20082
20179
  total,
20083
- page,
20084
- limit,
20180
+ page: limit === 0 ? 1 : page,
20181
+ limit: limit === 0 ? total : limit,
20085
20182
  totalPages,
20086
- hasNext: page < totalPages
20183
+ hasNext: limit === 0 ? false : page < totalPages
20087
20184
  };
20088
20185
  }
20089
20186
  /**
@@ -20092,8 +20189,8 @@ var init_base_service = __esm({
20092
20189
  getPagination(query) {
20093
20190
  const maxLimit = query?.maxLimit ?? 100;
20094
20191
  const page = Math.max(1, query?.page ?? 1);
20095
- const limit = Math.min(maxLimit, Math.max(1, query?.limit ?? 20));
20096
- const offset = (page - 1) * limit;
20192
+ const limit = query?.limit === 0 ? 0 : Math.min(maxLimit, Math.max(1, query?.limit ?? 20));
20193
+ const offset = limit === 0 ? 0 : (page - 1) * limit;
20097
20194
  return { page, limit, offset };
20098
20195
  }
20099
20196
  /**
@@ -21740,7 +21837,8 @@ function createEntityController(service, definition, ctx) {
21740
21837
  async list(req, res) {
21741
21838
  checkPermission(req, "read");
21742
21839
  const page = Math.max(1, parseInt(req.query["page"]) || 1);
21743
- const limit = Math.min(100, Math.max(1, parseInt(req.query["limit"]) || 20));
21840
+ const rawLimit = parseInt(req.query["limit"]);
21841
+ const limit = rawLimit === 0 ? 0 : Math.min(100, Math.max(1, rawLimit || 20));
21744
21842
  let filters;
21745
21843
  if (req.query["filters"]) {
21746
21844
  filters = parseFilters(req.query["filters"], ctx.core.errors);
@@ -23221,17 +23319,18 @@ async function createModuleRouters(ctx, definitions, modulePrefix) {
23221
23319
  const runtime = await createEntityRuntimeAsync(ctx, definition);
23222
23320
  const route = inferEntityRoutePath(definition);
23223
23321
  const entityLabel = resolveLocalized7(definition.label, "en");
23224
- if (modulePrefix && route === modulePrefix) {
23322
+ const isExposed = !("expose" in definition && definition.expose === false);
23323
+ if (isExposed && modulePrefix && route === modulePrefix) {
23225
23324
  ctx.core.logger.warn(
23226
23325
  `Entity "${entityLabel}" inferred route "${route}" duplicates module prefix \u2014 add routePrefix to the entity definition to fix`
23227
23326
  );
23228
23327
  }
23229
- if (routeMap.has(route)) {
23328
+ if (isExposed && routeMap.has(route)) {
23230
23329
  ctx.core.logger.warn(
23231
23330
  `Entity "${entityLabel}" route "${route}" collides with "${routeMap.get(route)}" in the same module`
23232
23331
  );
23233
23332
  }
23234
- routeMap.set(route, entityLabel);
23333
+ if (isExposed) routeMap.set(route, entityLabel);
23235
23334
  router.use(route, runtime.router);
23236
23335
  const key = getServiceKey(definition);
23237
23336
  if (ctx.services.has(key)) {
@@ -23447,7 +23546,7 @@ var init_runtime = __esm({
23447
23546
  }
23448
23547
  });
23449
23548
 
23450
- // src/db/module-runner.ts
23549
+ // src/db/seed-runner.ts
23451
23550
  import { existsSync as existsSync10 } from "fs";
23452
23551
  import { join as join10 } from "path";
23453
23552
  import { pathToFileURL } from "url";
@@ -23515,8 +23614,8 @@ function hasSeedData(seed5) {
23515
23614
  if (!Array.isArray(seed5) && "source" in seed5 && seed5.source === "url") return true;
23516
23615
  return Array.isArray(seed5) && seed5.length > 0;
23517
23616
  }
23518
- var init_module_runner = __esm({
23519
- "src/db/module-runner.ts"() {
23617
+ var init_seed_runner = __esm({
23618
+ "src/db/seed-runner.ts"() {
23520
23619
  "use strict";
23521
23620
  init_runtime();
23522
23621
  init_paths();
@@ -23619,7 +23718,7 @@ var init_migration_sources = __esm({
23619
23718
  init_shared();
23620
23719
  init_paths();
23621
23720
  init_plugin_ops();
23622
- init_store();
23721
+ init_module_store();
23623
23722
  }
23624
23723
  });
23625
23724
 
@@ -23952,7 +24051,7 @@ async function runMigrations(knexInstance, sources) {
23952
24051
  const executedMigrations = await knex3("_nexus_migrations").where({ status: "completed" }).select("name").then((rows) => new Set(rows.map((r) => r.name)));
23953
24052
  const pendingMigrations = migrationFiles.filter((m2) => !executedMigrations.has(m2.name));
23954
24053
  if (pendingMigrations.length === 0) {
23955
- logger.info("No pending migrations");
24054
+ logger.debug("No pending migrations");
23956
24055
  return;
23957
24056
  }
23958
24057
  const batch = await getNextBatch(knex3);
@@ -24352,6 +24451,27 @@ var init_migration_helpers = __esm({
24352
24451
  import path2 from "path";
24353
24452
  import fs2 from "fs/promises";
24354
24453
  import { readFileSync as readFileSync6, mkdirSync as mkdirSync5, realpathSync } from "fs";
24454
+ function getColumnIndexBytes(field) {
24455
+ if (!field?.db) return 255 * MYSQL_BYTES_PER_CHAR;
24456
+ const size = field.db.size ?? 255;
24457
+ if (field.db.type === "text") return 0;
24458
+ if (field.db.type === "string") return size * MYSQL_BYTES_PER_CHAR;
24459
+ if (field.db.type === "integer") return 4;
24460
+ if (field.db.type === "boolean") return 1;
24461
+ if (field.db.type === "datetime" || field.db.type === "date") return 8;
24462
+ if (field.db.type === "uuid") return 16;
24463
+ return size * MYSQL_BYTES_PER_CHAR;
24464
+ }
24465
+ function warnIfIndexExceedsMySQLLimit(table, columns, unique, fields) {
24466
+ if (!fields) return;
24467
+ const totalBytes = columns.reduce((sum, col) => sum + getColumnIndexBytes(fields[col]), 0);
24468
+ if (totalBytes > MYSQL_MAX_INDEX_BYTES) {
24469
+ const indexType = unique ? "UNIQUE" : "INDEX";
24470
+ logger.warn(
24471
+ `${indexType} on ${table}(${columns.join(", ")}) requires ${totalBytes} bytes \u2014 exceeds MySQL utf8mb4 limit of ${MYSQL_MAX_INDEX_BYTES} bytes. Reduce column sizes or restructure the index for MySQL compatibility.`
24472
+ );
24473
+ }
24474
+ }
24355
24475
  async function detectSchemaDrift(knexInstance) {
24356
24476
  const knex3 = knexInstance ?? getDb();
24357
24477
  const entities = getAllPersistentEntities();
@@ -24459,6 +24579,7 @@ function computeSchemaDiff(entities, currentSchema, options) {
24459
24579
  for (const idx of entityIndexes) {
24460
24580
  const key = normalizeKey(idx.columns, !!idx.unique);
24461
24581
  if (!currentKeys.has(key)) {
24582
+ warnIfIndexExceedsMySQLLimit(tableName, idx.columns, !!idx.unique, entity.fields);
24462
24583
  diff.newIndexes.push({ columns: idx.columns, unique: !!idx.unique });
24463
24584
  }
24464
24585
  }
@@ -24630,7 +24751,7 @@ function formatDriftMessage(drift) {
24630
24751
  lines.push('Run "pnpm migrate:dev" to generate and apply migrations.');
24631
24752
  return lines.join("\n");
24632
24753
  }
24633
- var PERSISTENT_TYPES;
24754
+ var MYSQL_MAX_INDEX_BYTES, MYSQL_BYTES_PER_CHAR, PERSISTENT_TYPES;
24634
24755
  var init_migration_generator = __esm({
24635
24756
  "src/db/migration-generator.ts"() {
24636
24757
  "use strict";
@@ -24639,9 +24760,11 @@ var init_migration_generator = __esm({
24639
24760
  init_paths();
24640
24761
  init_schema_reader();
24641
24762
  init_engine();
24642
- init_queries();
24763
+ init_module_queries();
24643
24764
  init_migration_helpers();
24644
- init_store();
24765
+ init_module_store();
24766
+ MYSQL_MAX_INDEX_BYTES = 3072;
24767
+ MYSQL_BYTES_PER_CHAR = 4;
24645
24768
  PERSISTENT_TYPES = /* @__PURE__ */ new Set([
24646
24769
  "collection",
24647
24770
  "tree",
@@ -26215,7 +26338,7 @@ var init_db = __esm({
26215
26338
  init_schema_helpers();
26216
26339
  init_sql_utils();
26217
26340
  init_sqlite_compat();
26218
- init_module_runner();
26341
+ init_seed_runner();
26219
26342
  init_ensure_system_tables();
26220
26343
  init_migration_sources();
26221
26344
  init_migration_runner();
@@ -26479,8 +26602,11 @@ var init_events_api = __esm({
26479
26602
 
26480
26603
  // src/engine/context.ts
26481
26604
  import { ForbiddenError as CASLForbiddenError3, subject } from "@casl/ability";
26482
- import { DEFAULT_TENANT_ID as DEFAULT_TENANT_ID2 } from "@gzl10/nexus-sdk";
26605
+ import { DEFAULT_TENANT_ID as DEFAULT_TENANT_ID2, DEFAULT_LOCALES } from "@gzl10/nexus-sdk";
26483
26606
  import { Redis as Redis2 } from "ioredis";
26607
+ function setLocales(locales) {
26608
+ platformLocales = locales;
26609
+ }
26484
26610
  function getSharedCacheManager() {
26485
26611
  if (!sharedCacheManager) {
26486
26612
  const redisUrl = env.REDIS_URL;
@@ -26556,7 +26682,7 @@ function createModuleContext() {
26556
26682
  const defaultAdapter = createKnexAdapter(knex3);
26557
26683
  const defaultSchemaAdapter = createKnexSchemaAdapter(knex3);
26558
26684
  adaptersRegistry["temp"] = { data: getSharedTempAdapter() };
26559
- logger.debug(env.REDIS_URL ? "Temp adapter: Redis (shared)" : "Temp adapter: InMemory (shared)");
26685
+ logger.trace(env.REDIS_URL ? "Temp adapter: Redis (shared)" : "Temp adapter: InMemory (shared)");
26560
26686
  const middleware = {
26561
26687
  validate,
26562
26688
  rateLimit: createRateLimit,
@@ -26664,7 +26790,9 @@ function createModuleContext() {
26664
26790
  throw new Error(`Knex connection for adapter "${adapter}" not found. Available: ${Object.keys(knexConnections).join(", ")}`);
26665
26791
  }
26666
26792
  return conn;
26667
- }
26793
+ },
26794
+ // Placeholder — bound after ctx construction (needs full ModuleContext)
26795
+ seedModule: null
26668
26796
  };
26669
26797
  const configContext = {
26670
26798
  env,
@@ -26757,7 +26885,8 @@ function createModuleContext() {
26757
26885
  adapters: adaptersContext,
26758
26886
  // Root-level shortcuts for frequently used utilities
26759
26887
  events: createEventsApi(nexusEvents, logger),
26760
- createRouter: () => createRouter()
26888
+ createRouter: () => createRouter(),
26889
+ locales: platformLocales
26761
26890
  };
26762
26891
  servicesRegistry["cacheManager"] = getSharedCacheManager();
26763
26892
  ctx.runtime = {
@@ -26766,9 +26895,10 @@ function createModuleContext() {
26766
26895
  createEntityController: (service, def) => createEntityController(service, def, ctx),
26767
26896
  createEntityRouter: (controller, def) => createEntityRouter(controller, def, ctx)
26768
26897
  };
26898
+ ctx.db.seedModule = (mod) => runModuleSeed(mod, ctx);
26769
26899
  return ctx;
26770
26900
  }
26771
- var sharedCacheManager, sharedTempAdapter;
26901
+ var platformLocales, sharedCacheManager, sharedTempAdapter;
26772
26902
  var init_context = __esm({
26773
26903
  "src/engine/context.ts"() {
26774
26904
  "use strict";
@@ -26781,7 +26911,9 @@ var init_context = __esm({
26781
26911
  init_plugin_ops();
26782
26912
  init_load_config();
26783
26913
  init_events_api();
26914
+ init_seed_runner();
26784
26915
  init_cache_manager();
26916
+ platformLocales = DEFAULT_LOCALES;
26785
26917
  sharedCacheManager = null;
26786
26918
  sharedTempAdapter = null;
26787
26919
  }
@@ -26792,11 +26924,11 @@ var init_engine = __esm({
26792
26924
  "src/engine/index.ts"() {
26793
26925
  "use strict";
26794
26926
  init_registry();
26795
- init_queries();
26927
+ init_module_queries();
26796
26928
  init_loader();
26797
- init_store();
26798
- init_subjectExtractor();
26799
- init_extractors();
26929
+ init_module_store();
26930
+ init_subject_extractor();
26931
+ init_definition_extractors();
26800
26932
  init_context();
26801
26933
  }
26802
26934
  });
@@ -27338,7 +27470,7 @@ async function createApp(options = {}) {
27338
27470
  // Only accept arrays and objects
27339
27471
  }));
27340
27472
  app.use(cookieParser());
27341
- const serveSPA = createServeSPA(app);
27473
+ const serveSPA = createServeSPA(app, options.httpServer);
27342
27474
  if (options.beforeRoutes) {
27343
27475
  const result = options.beforeRoutes(app, serveSPA);
27344
27476
  if (result instanceof Promise) {
@@ -27432,11 +27564,11 @@ async function createApp(options = {}) {
27432
27564
  });
27433
27565
  const sortedSpas = [...servedSpas].sort((a, b) => b.endpoint.length - a.endpoint.length);
27434
27566
  for (const spa of sortedSpas) {
27435
- serveSPA(spa.endpoint, spa.path, spa);
27567
+ await serveSPA(spa.endpoint, spa.path, { ...spa, viteSrc: spa.viteSrc });
27436
27568
  }
27437
27569
  const { ui } = getConfig();
27438
27570
  if (ui.enabled) {
27439
- serveSPA(ui.base, ui.path);
27571
+ await serveSPA(ui.base, ui.path, { viteSrc: "../ui" });
27440
27572
  }
27441
27573
  app.use(errorMiddleware);
27442
27574
  return app;
@@ -27462,58 +27594,32 @@ var init_app = __esm({
27462
27594
  }
27463
27595
  });
27464
27596
 
27465
- // src/core/utils/net.ts
27597
+ // src/core/utils/port-check.ts
27466
27598
  import net from "net";
27467
27599
  import { execSync } from "child_process";
27468
27600
  function findProcessOnPort(port) {
27469
27601
  try {
27470
27602
  const output = execSync(`lsof -ti :${port} 2>/dev/null`, { encoding: "utf-8" });
27471
27603
  const pid = parseInt(output.trim().split("\n")[0] ?? "", 10);
27472
- return isNaN(pid) ? null : pid;
27604
+ if (isNaN(pid)) return null;
27605
+ let name = "unknown";
27606
+ try {
27607
+ name = execSync(`ps -o comm= -p ${pid} 2>/dev/null`, { encoding: "utf-8" }).trim();
27608
+ } catch {
27609
+ }
27610
+ return { pid, name };
27473
27611
  } catch {
27474
27612
  return null;
27475
27613
  }
27476
27614
  }
27477
- function isSameProcessGroup(pid) {
27478
- if (pid === process.pid || pid === process.ppid) return true;
27479
- try {
27480
- const ourPgid = execSync(`ps -o pgid= -p ${process.pid} 2>/dev/null`, { encoding: "utf-8" }).trim();
27481
- const targetPgid = execSync(`ps -o pgid= -p ${pid} 2>/dev/null`, { encoding: "utf-8" }).trim();
27482
- return ourPgid === targetPgid;
27483
- } catch {
27484
- return false;
27485
- }
27486
- }
27487
- function killProcessOnPort(port) {
27488
- const pid = findProcessOnPort(port);
27489
- if (!pid) return false;
27490
- if (isSameProcessGroup(pid)) {
27491
- logger.warn({ port, pid }, `Skipping same process group (PID ${pid}) on port ${port}`);
27492
- return false;
27493
- }
27494
- try {
27495
- process.kill(pid, "SIGTERM");
27496
- logger.warn({ port, pid }, `Killed process ${pid} on port ${port}`);
27497
- return true;
27498
- } catch {
27499
- return false;
27500
- }
27501
- }
27502
27615
  async function checkPortAvailable(port, host = "0.0.0.0") {
27503
27616
  return new Promise((resolve2, reject) => {
27504
27617
  const server2 = net.createServer();
27505
27618
  server2.once("error", (err) => {
27506
27619
  if (err.code === "EADDRINUSE") {
27507
- if (process.env["NODE_ENV"] !== "production") {
27508
- if (killProcessOnPort(port)) {
27509
- setTimeout(() => {
27510
- checkPortAvailable(port, host).then(resolve2).catch(reject);
27511
- }, 500);
27512
- return;
27513
- }
27514
- }
27515
- logger.error({ port }, `Port ${port} is already in use`);
27516
- reject(new Error(`Port ${port} is already in use`));
27620
+ const proc = findProcessOnPort(port);
27621
+ const msg = proc ? `Port ${port} is already in use by "${proc.name}" (PID ${proc.pid}). Stop that process first.` : `Port ${port} is already in use`;
27622
+ reject(new Error(msg));
27517
27623
  } else {
27518
27624
  reject(err);
27519
27625
  }
@@ -27524,10 +27630,9 @@ async function checkPortAvailable(port, host = "0.0.0.0") {
27524
27630
  server2.listen(port, host);
27525
27631
  });
27526
27632
  }
27527
- var init_net = __esm({
27528
- "src/core/utils/net.ts"() {
27633
+ var init_port_check = __esm({
27634
+ "src/core/utils/port-check.ts"() {
27529
27635
  "use strict";
27530
- init_logger();
27531
27636
  }
27532
27637
  });
27533
27638
 
@@ -27622,6 +27727,150 @@ var init_tunnel = __esm({
27622
27727
  }
27623
27728
  });
27624
27729
 
27730
+ // src/db/seed-context.ts
27731
+ var seed_context_exports = {};
27732
+ __export(seed_context_exports, {
27733
+ createSeedContext: () => createSeedContext
27734
+ });
27735
+ function createSeedContext(deps) {
27736
+ const { knex: db2, generateId: generateId4, hashPassword: hashPassword2, pluginPrefixes, logger: logger2 } = deps;
27737
+ const permissionsMap = /* @__PURE__ */ new Map();
27738
+ const seedContext = {
27739
+ masters: {
27740
+ register(type2, entries, options) {
27741
+ deps.masterRegistry.register(type2, entries, options);
27742
+ }
27743
+ },
27744
+ roles: {
27745
+ async add(role) {
27746
+ const hasTable = await db2.schema.hasTable("roles");
27747
+ if (!hasTable) return;
27748
+ const existing = await db2("roles").where({ name: role.name }).first();
27749
+ if (!existing) {
27750
+ const description = role.description ? JSON.stringify(typeof role.description === "string" ? { en: role.description } : role.description) : null;
27751
+ await db2("roles").insert({
27752
+ id: generateId4(),
27753
+ name: role.name,
27754
+ description,
27755
+ is_system: role.is_system ?? false
27756
+ });
27757
+ logger2.info({ role: role.name }, "Seeded role");
27758
+ }
27759
+ if (role.permissions) {
27760
+ let rolePerms = permissionsMap.get(role.name);
27761
+ if (!rolePerms) {
27762
+ rolePerms = /* @__PURE__ */ new Map();
27763
+ permissionsMap.set(role.name, rolePerms);
27764
+ }
27765
+ for (const [subject2, actions] of Object.entries(role.permissions)) {
27766
+ const normalized = subject2.charAt(0).toUpperCase() + subject2.slice(1);
27767
+ rolePerms.set(normalized, actions);
27768
+ }
27769
+ }
27770
+ }
27771
+ },
27772
+ users: {
27773
+ async add(user) {
27774
+ const hasTable = await db2.schema.hasTable("users");
27775
+ if (!hasTable) return;
27776
+ const existing = await db2("users").where({ email: user.email }).first();
27777
+ if (existing) return;
27778
+ const userId = generateId4();
27779
+ await db2("users").insert({
27780
+ id: userId,
27781
+ type: user.type ?? "human",
27782
+ email: user.email,
27783
+ password: await hashPassword2(user.password),
27784
+ name: user.name ?? user.email
27785
+ });
27786
+ if (user.roles?.length) {
27787
+ for (const roleName of user.roles) {
27788
+ const role = await db2("roles").where({ name: roleName }).first();
27789
+ if (role) {
27790
+ await db2("user_roles").insert({
27791
+ id: generateId4(),
27792
+ user_id: userId,
27793
+ role_id: role.id
27794
+ });
27795
+ } else {
27796
+ logger2.warn({ role: roleName, user: user.email }, "Role not found, skipping assignment");
27797
+ }
27798
+ }
27799
+ }
27800
+ logger2.info({ email: user.email, roles: user.roles }, "Seeded user");
27801
+ }
27802
+ },
27803
+ plugin(pluginName) {
27804
+ return {
27805
+ entity(entityName) {
27806
+ return {
27807
+ async upsert(data, options) {
27808
+ let prefix = pluginPrefixes.get(pluginName) ?? "";
27809
+ if (!prefix) {
27810
+ for (const [key2, pfx] of pluginPrefixes) {
27811
+ if (pluginName === key2 || pluginName.endsWith(`-${key2}`)) {
27812
+ prefix = pfx;
27813
+ break;
27814
+ }
27815
+ }
27816
+ }
27817
+ const fullTable = `${prefix}${entityName}`;
27818
+ const hasTable = await db2.schema.hasTable(fullTable);
27819
+ if (!hasTable) {
27820
+ logger2.warn({ table: fullTable, plugin: pluginName }, "Table not found for plugin entity seed");
27821
+ return 0;
27822
+ }
27823
+ const key = options?.key ?? "id";
27824
+ let seeded = 0;
27825
+ for (const row of data) {
27826
+ const keyValue = row[key];
27827
+ if (keyValue != null) {
27828
+ const existing = await db2(fullTable).where({ [key]: keyValue }).first();
27829
+ if (existing) continue;
27830
+ }
27831
+ const insertData = { ...row };
27832
+ if (key === "id" && !insertData["id"]) {
27833
+ insertData["id"] = generateId4();
27834
+ }
27835
+ await db2(fullTable).insert(insertData);
27836
+ seeded++;
27837
+ }
27838
+ if (seeded > 0) {
27839
+ logger2.info({ table: fullTable, seeded }, "Seeded plugin entity data");
27840
+ }
27841
+ return seeded;
27842
+ }
27843
+ };
27844
+ }
27845
+ };
27846
+ },
27847
+ async raw(table, data) {
27848
+ logger2.warn({ table }, "Using seed.raw() \u2014 prefer typed helpers when available");
27849
+ const hasTable = await db2.schema.hasTable(table);
27850
+ if (!hasTable) {
27851
+ logger2.warn({ table }, "Table not found for raw seed");
27852
+ return 0;
27853
+ }
27854
+ const rows = Array.isArray(data) ? data : [data];
27855
+ await db2(table).insert(rows);
27856
+ return rows.length;
27857
+ }
27858
+ };
27859
+ return {
27860
+ ctx: seedContext,
27861
+ flushPermissions: () => {
27862
+ if (permissionsMap.size > 0 && deps.onPermissionsCollected) {
27863
+ deps.onPermissionsCollected(permissionsMap);
27864
+ }
27865
+ }
27866
+ };
27867
+ }
27868
+ var init_seed_context = __esm({
27869
+ "src/db/seed-context.ts"() {
27870
+ "use strict";
27871
+ }
27872
+ });
27873
+
27625
27874
  // src/instrumentation.ts
27626
27875
  var instrumentation_exports = {};
27627
27876
  __export(instrumentation_exports, {
@@ -27686,7 +27935,9 @@ var init_instrumentation = __esm({
27686
27935
  });
27687
27936
 
27688
27937
  // src/core/server.ts
27938
+ import http from "http";
27689
27939
  import { entityRoom as entityRoom6 } from "@gzl10/nexus-sdk";
27940
+ import { DEFAULT_LOCALES as DEFAULT_LOCALES2 } from "@gzl10/nexus-sdk";
27690
27941
  async function runMigrationsAndSeeds(config3) {
27691
27942
  initLoggerService(getLoggerConfig());
27692
27943
  setLoggerInstance(getPinoLogger());
@@ -27739,7 +27990,7 @@ async function runMigrationsAndSeeds(config3) {
27739
27990
  const sources = buildMigrationSources();
27740
27991
  const migrationFiles = await loadAllMigrationFiles(sources);
27741
27992
  if (migrationFiles.length > 0) {
27742
- logger.info({ count: migrationFiles.length }, "Deploying pending migrations...");
27993
+ logger.info({ sources: sources.length, files: migrationFiles.length }, "Running migration deploy...");
27743
27994
  try {
27744
27995
  await runMigrations(void 0, sources);
27745
27996
  } catch (err) {
@@ -27748,7 +27999,7 @@ async function runMigrationsAndSeeds(config3) {
27748
27999
  throw err;
27749
28000
  }
27750
28001
  }
27751
- logger.info("Checking schema drift...");
28002
+ logger.debug("Checking schema drift...");
27752
28003
  const drift = await detectSchemaDrift();
27753
28004
  if (drift && (drift.newTables.length > 0 || drift.alteredTables.length > 0)) {
27754
28005
  const message = formatDriftMessage(drift);
@@ -27795,7 +28046,7 @@ ${dirs}`);
27795
28046
  }
27796
28047
  const allDefinitions = modules.flatMap((m2) => m2.definitions ?? []);
27797
28048
  await createMemoryTables(allDefinitions);
27798
- logger.info("Running seeds...");
28049
+ logger.debug("Running seeds...");
27799
28050
  for (const mod of modules) {
27800
28051
  try {
27801
28052
  await runModuleSeed(mod, ctx);
@@ -27803,12 +28054,39 @@ ${dirs}`);
27803
28054
  logger.error({ module: mod.name, err }, "Seed failed - continuing with next module");
27804
28055
  }
27805
28056
  }
28057
+ if (config3?.onSeed) {
28058
+ const { getPlugins: getPlugins2 } = await Promise.resolve().then(() => (init_module_queries(), module_queries_exports));
28059
+ const { createMasterRegistry: createMasterRegistry2 } = await Promise.resolve().then(() => (init_registry2(), registry_exports));
28060
+ const masterRegistry = ctx.services.has("masters") ? ctx.services.get("masters") : createMasterRegistry2();
28061
+ const pluginPrefixes = /* @__PURE__ */ new Map();
28062
+ for (const plugin of getPlugins2()) {
28063
+ pluginPrefixes.set(plugin.code, `${plugin.code}_`);
28064
+ const shortName = plugin.name.replace(/^@[^/]+\/nexus-plugin-/, "");
28065
+ if (shortName !== plugin.code) {
28066
+ pluginPrefixes.set(shortName, `${plugin.code}_`);
28067
+ }
28068
+ }
28069
+ const { createSeedContext: createSeedContext2 } = await Promise.resolve().then(() => (init_seed_context(), seed_context_exports));
28070
+ const { ctx: seedCtx, flushPermissions } = createSeedContext2({
28071
+ knex: ctx.db.knex,
28072
+ generateId: ctx.core.generateId,
28073
+ hashPassword: ctx.core.crypto.hashPassword,
28074
+ masterRegistry,
28075
+ pluginPrefixes,
28076
+ logger: ctx.core.logger,
28077
+ onPermissionsCollected: (perms) => setSeedPermissions(perms)
28078
+ });
28079
+ await config3.onSeed(seedCtx);
28080
+ await masterRegistry.seed(ctx);
28081
+ flushPermissions();
28082
+ }
27806
28083
  }
27807
28084
  async function start(config3) {
27808
28085
  if (server) {
27809
28086
  throw new Error("Server already running. Call stop() first.");
27810
28087
  }
27811
28088
  currentConfig = config3;
28089
+ setLocales(config3?.locales ?? DEFAULT_LOCALES2);
27812
28090
  if (env.NODE_ENV === "development" && env.FRPC_SERVER && env.FRPC_SUBDOMAIN && !env.BACKEND_URL) {
27813
28091
  process.env["BACKEND_URL"] = getTunnelUrl(env.FRPC_SUBDOMAIN, env.FRPC_SERVER);
27814
28092
  }
@@ -27833,13 +28111,17 @@ async function start(config3) {
27833
28111
  await initTelemetry2();
27834
28112
  await runMigrationsAndSeeds(config3);
27835
28113
  const effectiveCorsOrigins = buildEffectiveCorsOrigins(env.CORS_ORIGIN, config3?.spas);
28114
+ const httpServer = http.createServer();
27836
28115
  const app = await createApp({
27837
28116
  beforeRoutes: config3?.beforeRoutes,
27838
28117
  afterRoutes: config3?.afterRoutes,
27839
- spas: config3?.spas
28118
+ spas: config3?.spas,
28119
+ httpServer
27840
28120
  });
28121
+ httpServer.on("request", app);
27841
28122
  return new Promise((resolve2) => {
27842
- server = app.listen(resolved.port, resolved.host, async () => {
28123
+ server = httpServer;
28124
+ httpServer.listen(resolved.port, resolved.host, async () => {
27843
28125
  const timeoutMs = parseInt(process.env["REQUEST_TIMEOUT_MS"] || "30000", 10);
27844
28126
  if (timeoutMs > 0) {
27845
28127
  server.setTimeout(timeoutMs);
@@ -27875,12 +28157,10 @@ async function start(config3) {
27875
28157
  });
27876
28158
  }
27877
28159
  const baseUrl = env.BACKEND_URL || `http://localhost:${actualPort}`;
27878
- logger.info({ libPath: getLibPath(), projectPath: getProjectPath() }, "Paths");
27879
- logger.info(`API: ${baseUrl}/api/v1`);
27880
- if (resolved.ui.enabled) {
27881
- logger.info(`UI: ${baseUrl}`);
27882
- }
27883
- logger.info({ port: actualPort, mode: resolved.nodeEnv }, "Server started");
28160
+ logger.debug({ libPath: getLibPath(), projectPath: getProjectPath() }, "Paths");
28161
+ const urls = { api: `${baseUrl}/api/v1` };
28162
+ if (resolved.ui.enabled) urls["ui"] = baseUrl;
28163
+ logger.info({ port: actualPort, mode: resolved.nodeEnv, ...urls }, "Server started");
27884
28164
  nexusEvents.emitEvent("server.started", { port: actualPort, host: resolved.host });
27885
28165
  if (config3?.onReady) {
27886
28166
  try {
@@ -27906,7 +28186,8 @@ async function stop() {
27906
28186
  await resetSharedAdapters();
27907
28187
  resetConfigCache();
27908
28188
  clearCustomCaslRules();
27909
- resetServeSPAEndpoints();
28189
+ clearSeedPermissions();
28190
+ await resetServeSPA();
27910
28191
  return;
27911
28192
  }
27912
28193
  if (currentConfig?.beforeClose) {
@@ -27940,7 +28221,8 @@ async function stop() {
27940
28221
  await resetSharedAdapters();
27941
28222
  resetConfigCache();
27942
28223
  clearCustomCaslRules();
27943
- resetServeSPAEndpoints();
28224
+ clearSeedPermissions();
28225
+ await resetServeSPA();
27944
28226
  currentConfig = void 0;
27945
28227
  server = null;
27946
28228
  nexusEvents.emitEvent("server.stopped");
@@ -28000,7 +28282,7 @@ var init_server = __esm({
28000
28282
  init_cors();
28001
28283
  init_logger();
28002
28284
  init_error_middleware();
28003
- init_net();
28285
+ init_port_check();
28004
28286
  init_engine();
28005
28287
  init_context();
28006
28288
  init_db();