@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/main.js CHANGED
@@ -106,7 +106,7 @@ var init_package = __esm({
106
106
  "package.json"() {
107
107
  package_default = {
108
108
  name: "@gzl10/nexus-backend",
109
- version: "0.17.0",
109
+ version: "0.18.0",
110
110
  description: "Backend as a Service (BaaS) with Express 5, Knex and CASL",
111
111
  type: "module",
112
112
  main: "./dist/index.js",
@@ -162,7 +162,7 @@ var init_package = __esm({
162
162
  "jwt"
163
163
  ],
164
164
  scripts: {
165
- dev: "tsx watch src/main.ts",
165
+ dev: "node --watch-path=./src --import tsx/esm src/main.ts",
166
166
  build: "tsup",
167
167
  start: "node dist/main.js",
168
168
  nexus: "tsx src/cli.ts",
@@ -218,26 +218,6 @@ var init_package = __esm({
218
218
  zod: "^3.24.0"
219
219
  },
220
220
  devDependencies: {
221
- "@gzl10/nexus-plugin-ai": "workspace:^",
222
- "@gzl10/nexus-plugin-auth-providers": "workspace:^",
223
- "@gzl10/nexus-plugin-charts": "workspace:^",
224
- "@gzl10/nexus-plugin-cms": "workspace:^",
225
- "@gzl10/nexus-plugin-compliance": "workspace:^",
226
- "@gzl10/nexus-plugin-docker": "workspace:^",
227
- "@gzl10/nexus-plugin-feeds": "workspace:^",
228
- "@gzl10/nexus-plugin-importer": "workspace:^",
229
- "@gzl10/nexus-plugin-links": "workspace:*",
230
- "@gzl10/nexus-plugin-notifications": "workspace:*",
231
- "@gzl10/nexus-plugin-oidc-server": "workspace:^",
232
- "@gzl10/nexus-plugin-plane": "workspace:^",
233
- "@gzl10/nexus-plugin-prisma": "workspace:^",
234
- "@gzl10/nexus-plugin-remote": "workspace:*",
235
- "@gzl10/nexus-plugin-schedules": "workspace:*",
236
- "@gzl10/nexus-plugin-scim": "workspace:^",
237
- "@gzl10/nexus-plugin-scraper": "workspace:^",
238
- "@gzl10/nexus-plugin-secrets": "workspace:^",
239
- "@gzl10/nexus-plugin-tags": "workspace:*",
240
- "@gzl10/nexus-plugin-webhooks": "workspace:*",
241
221
  "@types/bcryptjs": "^2.4.0",
242
222
  "@types/compression": "^1.8.1",
243
223
  "@types/cookie-parser": "^1.4.10",
@@ -250,7 +230,16 @@ var init_package = __esm({
250
230
  "pino-pretty": "^13.1.3",
251
231
  "socket.io-client": "^4.8.3",
252
232
  supertest: "^7.2.2",
253
- tsx: "^4.21.0"
233
+ tsx: "^4.21.0",
234
+ vite: "^8.0.3"
235
+ },
236
+ peerDependencies: {
237
+ vite: ">=6.0.0"
238
+ },
239
+ peerDependenciesMeta: {
240
+ vite: {
241
+ optional: true
242
+ }
254
243
  },
255
244
  publishConfig: {
256
245
  access: "public",
@@ -1252,7 +1241,7 @@ var init_table_prefix = __esm({
1252
1241
  }
1253
1242
  });
1254
1243
 
1255
- // src/engine/store.ts
1244
+ // src/engine/module-store.ts
1256
1245
  function resetStore() {
1257
1246
  moduleStore.modules.length = 0;
1258
1247
  moduleStore.plugins.clear();
@@ -1263,8 +1252,8 @@ function resetStore() {
1263
1252
  moduleStore.tableToSubject.clear();
1264
1253
  }
1265
1254
  var moduleStore;
1266
- var init_store = __esm({
1267
- "src/engine/store.ts"() {
1255
+ var init_module_store = __esm({
1256
+ "src/engine/module-store.ts"() {
1268
1257
  "use strict";
1269
1258
  moduleStore = {
1270
1259
  /** Registered modules with source metadata */
@@ -1317,7 +1306,7 @@ var init_id = __esm({
1317
1306
  }
1318
1307
  });
1319
1308
 
1320
- // src/engine/extractors.ts
1309
+ // src/engine/definition-extractors.ts
1321
1310
  function getTableAndSubject(def) {
1322
1311
  const caslSubject = def.casl?.subject;
1323
1312
  if (!TYPES_WITH_TABLE.has(def.type)) {
@@ -1385,8 +1374,8 @@ function validateModuleDependencies(modules) {
1385
1374
  }
1386
1375
  }
1387
1376
  var TYPES_WITH_TABLE;
1388
- var init_extractors = __esm({
1389
- "src/engine/extractors.ts"() {
1377
+ var init_definition_extractors = __esm({
1378
+ "src/engine/definition-extractors.ts"() {
1390
1379
  "use strict";
1391
1380
  TYPES_WITH_TABLE = /* @__PURE__ */ new Set(["collection", "reference", "event", "config", "temp", "view", void 0]);
1392
1381
  }
@@ -1506,9 +1495,9 @@ var init_registry = __esm({
1506
1495
  "use strict";
1507
1496
  init_plugin_ops();
1508
1497
  init_table_prefix();
1509
- init_store();
1498
+ init_module_store();
1510
1499
  init_id();
1511
- init_extractors();
1500
+ init_definition_extractors();
1512
1501
  }
1513
1502
  });
1514
1503
 
@@ -1560,7 +1549,28 @@ var init_paths = __esm({
1560
1549
  }
1561
1550
  });
1562
1551
 
1563
- // src/engine/queries.ts
1552
+ // src/engine/module-queries.ts
1553
+ var module_queries_exports = {};
1554
+ __export(module_queries_exports, {
1555
+ getCoreManifest: () => getCoreManifest,
1556
+ getCoreModules: () => getCoreModules,
1557
+ getModule: () => getModule,
1558
+ getModules: () => getModules,
1559
+ getOrderedModules: () => getOrderedModules,
1560
+ getOrderedModulesInternal: () => getOrderedModulesInternal,
1561
+ getPlugin: () => getPlugin,
1562
+ getPluginByCode: () => getPluginByCode,
1563
+ getPlugins: () => getPlugins,
1564
+ getRegisteredSubjects: () => getRegisteredSubjects,
1565
+ getSubjectForTable: () => getSubjectForTable,
1566
+ getUserManifest: () => getUserManifest,
1567
+ getUserModules: () => getUserModules,
1568
+ hasModule: () => hasModule,
1569
+ hasPlugin: () => hasPlugin,
1570
+ hasPluginByCode: () => hasPluginByCode,
1571
+ hasUserApp: () => hasUserApp,
1572
+ isValidSubject: () => isValidSubject
1573
+ });
1564
1574
  import { join as join4 } from "path";
1565
1575
  import { readFileSync as readFileSync3 } from "fs";
1566
1576
  function readPackageJson(dir) {
@@ -1594,12 +1604,22 @@ function getOrderedModulesInternal() {
1594
1604
  moduleStore.modules.forEach(visit);
1595
1605
  return sorted;
1596
1606
  }
1607
+ function getModule(name) {
1608
+ const mod = moduleStore.modules.find((m) => m.name === name);
1609
+ return mod ? toModuleManifest(mod) : void 0;
1610
+ }
1611
+ function getPlugin(name) {
1612
+ return moduleStore.plugins.get(name);
1613
+ }
1597
1614
  function getPlugins() {
1598
1615
  return [...moduleStore.plugins.values()];
1599
1616
  }
1600
1617
  function getRegisteredSubjects() {
1601
1618
  return [...moduleStore.subjects];
1602
1619
  }
1620
+ function isValidSubject(subject2) {
1621
+ return moduleStore.subjects.has(subject2);
1622
+ }
1603
1623
  function getSubjectForTable(table) {
1604
1624
  return moduleStore.tableToSubject.get(table);
1605
1625
  }
@@ -1646,10 +1666,10 @@ function getPluginByCode(code) {
1646
1666
  function hasPluginByCode(code) {
1647
1667
  return moduleStore.pluginsByCode.has(code);
1648
1668
  }
1649
- var init_queries = __esm({
1650
- "src/engine/queries.ts"() {
1669
+ var init_module_queries = __esm({
1670
+ "src/engine/module-queries.ts"() {
1651
1671
  "use strict";
1652
- init_store();
1672
+ init_module_store();
1653
1673
  init_paths();
1654
1674
  }
1655
1675
  });
@@ -2133,12 +2153,16 @@ var init_definitions = __esm({
2133
2153
  labelPlural: { en: "Masters", es: "Maestros" },
2134
2154
  labelField: "label",
2135
2155
  timestamps: true,
2156
+ availableDisplayModes: ["board"],
2157
+ groupBy: "type",
2158
+ groupableFields: ["type"],
2159
+ //columnDragFields: ['is_active'],
2136
2160
  fields: {
2137
2161
  id: useTextField2({
2138
2162
  label: { en: "ID", es: "ID" },
2139
2163
  required: true,
2140
2164
  size: 100,
2141
- hint: { en: "Format: type:code (e.g. countries:ES)", es: "Formato: type:code (ej. countries:ES)" },
2165
+ hidden: true,
2142
2166
  meta: { sortable: true }
2143
2167
  }),
2144
2168
  type: useTextField2({
@@ -2159,9 +2183,20 @@ var init_definitions = __esm({
2159
2183
  is_active: isActiveField,
2160
2184
  metadata: useJsonField({
2161
2185
  label: { en: "Metadata", es: "Metadatos" },
2162
- hint: { en: "Type-specific fields (symbol, flag, etc.)", es: "Campos espec\xEDficos del tipo" }
2186
+ hint: {
2187
+ en: "Type-specific fields (symbol, flag, etc.)",
2188
+ es: "Campos espec\xEDficos del tipo"
2189
+ }
2163
2190
  })
2164
2191
  },
2192
+ hooks: () => ({
2193
+ beforeCreate: async (data) => {
2194
+ if (data["type"] && data["code"] && !data["id"]) {
2195
+ data["id"] = `${data["type"]}:${data["code"]}`;
2196
+ }
2197
+ return data;
2198
+ }
2199
+ }),
2165
2200
  defaultSort: { field: "order", order: "asc" },
2166
2201
  indexes: [{ columns: ["type", "code"], unique: true }],
2167
2202
  casl: { subject: "Master", permissions: masterCaslPermissions }
@@ -2170,6 +2205,10 @@ var init_definitions = __esm({
2170
2205
  });
2171
2206
 
2172
2207
  // src/modules/masters/registry.ts
2208
+ var registry_exports = {};
2209
+ __export(registry_exports, {
2210
+ createMasterRegistry: () => createMasterRegistry
2211
+ });
2173
2212
  function createMasterRegistry() {
2174
2213
  const registrations = [];
2175
2214
  return {
@@ -8848,15 +8887,18 @@ function toEntityDefinitionDTO(def, _engine, moduleName) {
8848
8887
  const meta = field["meta"];
8849
8888
  return meta?.["sortable"] === true && !field["hidden"];
8850
8889
  });
8890
+ const explicitGroupable = def["groupableFields"];
8851
8891
  const groupableInputTypes = ["select", "switch", "checkbox", "radio", "tags"];
8852
- const groupableFields = fieldEntries.filter(([, f]) => {
8892
+ const autoGroupableFields = fieldEntries.filter(([, f]) => {
8853
8893
  const field = f;
8854
8894
  const inputType = field["input"];
8855
8895
  return groupableInputTypes.includes(inputType ?? "") && !field["hidden"];
8856
8896
  });
8897
+ const resolvedGroupableFields = explicitGroupable ?? (autoGroupableFields.length > 0 ? autoGroupableFields.map(([name]) => name) : void 0);
8857
8898
  const entityType = def["type"] ?? "collection";
8858
8899
  const explicitDisplayMode = def["displayMode"];
8859
- const defaultDisplayMode = explicitDisplayMode ?? (["tree", "dag"].includes(entityType) ? "tree" : "table");
8900
+ const explicitAvailableModes = def["availableDisplayModes"];
8901
+ const defaultDisplayMode = explicitDisplayMode ?? (explicitAvailableModes?.length === 1 ? explicitAvailableModes[0] : void 0) ?? (["tree", "dag"].includes(entityType) ? "tree" : "table");
8860
8902
  const entityIdent = def["table"] ?? def["key"];
8861
8903
  return {
8862
8904
  id: def._id,
@@ -8876,9 +8918,11 @@ function toEntityDefinitionDTO(def, _engine, moduleName) {
8876
8918
  displayMode: explicitDisplayMode,
8877
8919
  defaultDisplayMode,
8878
8920
  availableDisplayModes: (() => {
8921
+ const explicit = def["availableDisplayModes"];
8922
+ if (explicit?.length) return explicit;
8879
8923
  const modes = ["table", "list", "masonry"];
8880
8924
  if (["tree", "dag"].includes(entityType)) modes.push("tree");
8881
- if (groupableFields.length > 0) modes.push("board");
8925
+ if ((resolvedGroupableFields?.length ?? 0) > 0) modes.push("board");
8882
8926
  if (def["calendarFrom"]) modes.push("calendar");
8883
8927
  return modes;
8884
8928
  })(),
@@ -8888,10 +8932,8 @@ function toEntityDefinitionDTO(def, _engine, moduleName) {
8888
8932
  value: name,
8889
8933
  label: f["label"]
8890
8934
  })),
8891
- groupableFields: groupableFields.length > 0 ? groupableFields.map(([name, f]) => ({
8892
- value: name,
8893
- label: f["label"]
8894
- })) : void 0,
8935
+ groupableFields: resolvedGroupableFields,
8936
+ columnDragFields: def["columnDragFields"],
8895
8937
  groupBy: def["groupBy"],
8896
8938
  subgroupBy: def["subgroupBy"],
8897
8939
  calendarFrom: def["calendarFrom"],
@@ -8931,21 +8973,6 @@ function toModuleDTO(mod, ctx) {
8931
8973
  hasInit: !!mod.init
8932
8974
  };
8933
8975
  }
8934
- function getOrderedModulesViaContext(ctx) {
8935
- return ctx.engine.getModules();
8936
- }
8937
- async function runModuleSeedViaContext(mod, ctx) {
8938
- if (!mod.seed) {
8939
- return false;
8940
- }
8941
- try {
8942
- await mod.seed(ctx);
8943
- return true;
8944
- } catch (err) {
8945
- ctx.core.logger.error({ module: mod.name, err }, "Seed failed");
8946
- return false;
8947
- }
8948
- }
8949
8976
  var init_system_helpers = __esm({
8950
8977
  "src/modules/system/system.helpers.ts"() {
8951
8978
  "use strict";
@@ -9180,7 +9207,8 @@ function createSystemController(ctx) {
9180
9207
  const plugins = engine.getPlugins();
9181
9208
  const body = {
9182
9209
  version: manifest.version,
9183
- plugins: plugins.map((p) => p.code)
9210
+ plugins: plugins.map((p) => p.code),
9211
+ locales: ctx.locales
9184
9212
  };
9185
9213
  res.json(body);
9186
9214
  }
@@ -10067,7 +10095,6 @@ var SYSTEM_TABLES, factoryResetAction;
10067
10095
  var init_factory_reset_action = __esm({
10068
10096
  "src/modules/system/actions/factory-reset.action.ts"() {
10069
10097
  "use strict";
10070
- init_system_helpers();
10071
10098
  SYSTEM_TABLES = /* @__PURE__ */ new Set([
10072
10099
  "_nexus_migrations",
10073
10100
  "_nexus_migration_lock",
@@ -10110,8 +10137,8 @@ var init_factory_reset_action = __esm({
10110
10137
  source: "core:system",
10111
10138
  action: "factory_reset",
10112
10139
  actorId: authReq.user?.id,
10113
- ip: req.ip,
10114
- userAgent: req.headers["user-agent"]
10140
+ ip: req?.ip,
10141
+ userAgent: req?.headers["user-agent"]
10115
10142
  });
10116
10143
  await new Promise((resolve2) => setImmediate(resolve2));
10117
10144
  const allTables = await getAllTables(knex3);
@@ -10149,11 +10176,11 @@ var init_factory_reset_action = __esm({
10149
10176
  } catch {
10150
10177
  }
10151
10178
  ctx.core.logger.info({ tables: dataTables.length }, "All data tables cleared");
10152
- const modules = getOrderedModulesViaContext(ctx);
10179
+ const modules = ctx.engine.getModules();
10153
10180
  let modulesSeeded = 0;
10154
10181
  for (const mod of modules) {
10155
10182
  try {
10156
- const seeded = await runModuleSeedViaContext(mod, ctx);
10183
+ const seeded = await ctx.db.seedModule(mod);
10157
10184
  if (seeded) modulesSeeded++;
10158
10185
  } catch (err) {
10159
10186
  ctx.core.logger.error({ module: mod.name, err }, "Seed failed during factory reset");
@@ -10207,8 +10234,8 @@ var init_restart_server_action = __esm({
10207
10234
  source: "core:system",
10208
10235
  action: "server_restart",
10209
10236
  actorId: authReq.user?.id,
10210
- ip: req.ip,
10211
- userAgent: req.headers["user-agent"]
10237
+ ip: req?.ip,
10238
+ userAgent: req?.headers["user-agent"]
10212
10239
  });
10213
10240
  setTimeout(() => process.exit(0), 500);
10214
10241
  return { success: true, message: "Server is restarting..." };
@@ -11830,7 +11857,7 @@ function createUploadMiddleware(ctx, options) {
11830
11857
  const rateLimit2 = ctx.core.middleware.rateLimit({
11831
11858
  windowMs: 60 * 1e3,
11832
11859
  max: 20,
11833
- message: "Demasiados uploads, intenta en 1 minuto"
11860
+ message: "Too many uploads, try again in 1 minute"
11834
11861
  });
11835
11862
  const upload = multer({
11836
11863
  storage: multer.memoryStorage(),
@@ -12077,7 +12104,7 @@ function createStorageRoutes(ctx) {
12077
12104
  }
12078
12105
  res.status(201).json(results);
12079
12106
  };
12080
- const uploadRateLimit = ctx.core.middleware.rateLimit({ windowMs: 60 * 1e3, max: 20, message: "Demasiados uploads, intenta en 1 minuto" });
12107
+ const uploadRateLimit = ctx.core.middleware.rateLimit({ windowMs: 60 * 1e3, max: 20, message: "Too many uploads, try again in 1 minute" });
12081
12108
  if (auth) {
12082
12109
  router.post("/upload/multiple", uploadRateLimit, auth, upload.array("files", 10), uploadMultiple);
12083
12110
  } else {
@@ -12375,7 +12402,7 @@ var init_users_entity = __esm({
12375
12402
  ],
12376
12403
  nullable: true,
12377
12404
  meta: { sortable: true },
12378
- defaultValue: "es"
12405
+ defaultValue: "en"
12379
12406
  }),
12380
12407
  timezone: useSelectField9({
12381
12408
  label: { en: "Timezone", es: "Zona horaria" },
@@ -12453,7 +12480,7 @@ var init_users_entity = __esm({
12453
12480
  middleware: (ctx) => ctx.core.middleware.rateLimit({
12454
12481
  windowMs: 15 * 60 * 1e3,
12455
12482
  max: 5,
12456
- message: "Demasiados intentos, intenta en 15 minutos"
12483
+ message: "Too many attempts, try again in 15 minutes"
12457
12484
  }),
12458
12485
  handler: async (ctx, input) => {
12459
12486
  const {
@@ -16635,7 +16662,7 @@ var init_plugins_entity = __esm({
16635
16662
  label: "Plugins",
16636
16663
  icon: "mdi:puzzle",
16637
16664
  labelField: "code",
16638
- routePrefix: "/plugins",
16665
+ routePrefix: "/",
16639
16666
  defaultSort: { field: "name", order: "asc" },
16640
16667
  fields: {
16641
16668
  name: useTextField12({
@@ -17048,12 +17075,12 @@ var init_loader = __esm({
17048
17075
  "src/engine/loader.ts"() {
17049
17076
  "use strict";
17050
17077
  init_registry();
17051
- init_extractors();
17078
+ init_definition_extractors();
17052
17079
  init_modules();
17053
17080
  }
17054
17081
  });
17055
17082
 
17056
- // src/engine/subjectExtractor.ts
17083
+ // src/engine/subject-extractor.ts
17057
17084
  function getModuleSubjects(mod) {
17058
17085
  const subjects = /* @__PURE__ */ new Set();
17059
17086
  for (const def of mod.definitions ?? []) {
@@ -17062,10 +17089,10 @@ function getModuleSubjects(mod) {
17062
17089
  }
17063
17090
  return [...subjects];
17064
17091
  }
17065
- var init_subjectExtractor = __esm({
17066
- "src/engine/subjectExtractor.ts"() {
17092
+ var init_subject_extractor = __esm({
17093
+ "src/engine/subject-extractor.ts"() {
17067
17094
  "use strict";
17068
- init_extractors();
17095
+ init_definition_extractors();
17069
17096
  }
17070
17097
  });
17071
17098
 
@@ -17143,7 +17170,7 @@ function initSocketIO(httpServer, options) {
17143
17170
  maxHttpBufferSize: 1e6
17144
17171
  // 1MB - match Express json body limit
17145
17172
  });
17146
- logger.info({ maxHttpBufferSize: 1e6, cors: corsOrigin }, "Socket.IO initialized");
17173
+ logger.info({ cors: corsOrigin }, "Socket.IO initialized");
17147
17174
  io.use((socket, next) => {
17148
17175
  const token = socket.handshake.auth?.["token"] || socket.handshake.query?.["token"];
17149
17176
  if (token && typeof token === "string" && jwtSecret) {
@@ -17166,7 +17193,6 @@ function initSocketIO(httpServer, options) {
17166
17193
  logger.warn({ code: err.code, message: err.message }, "Socket.IO connection error");
17167
17194
  });
17168
17195
  io.on("connection", handleConnection);
17169
- logger.info("Socket.IO initialized");
17170
17196
  nexusEvents.emitEvent("socket.initialized");
17171
17197
  return io;
17172
17198
  }
@@ -17747,6 +17773,12 @@ var init_app_error = __esm({
17747
17773
 
17748
17774
  // src/core/abilities/ability.factory.ts
17749
17775
  import { AbilityBuilder, createMongoAbility } from "@casl/ability";
17776
+ function setSeedPermissions(perms) {
17777
+ seedPermissions = perms;
17778
+ }
17779
+ function clearSeedPermissions() {
17780
+ seedPermissions = null;
17781
+ }
17750
17782
  function setCustomCaslRules(fn) {
17751
17783
  customCaslRules = fn;
17752
17784
  }
@@ -17804,16 +17836,28 @@ async function defineAbilityFor(user, roleNames) {
17804
17836
  if (customCaslRules) {
17805
17837
  await customCaslRules(user, { can, cannot });
17806
17838
  }
17839
+ if (seedPermissions && !isSuperuser) {
17840
+ for (const roleName of roleNames) {
17841
+ const rolePerms = seedPermissions.get(roleName);
17842
+ if (!rolePerms) continue;
17843
+ for (const [subject2, actions] of rolePerms) {
17844
+ for (const action of actions) {
17845
+ can(action, subject2);
17846
+ }
17847
+ }
17848
+ }
17849
+ }
17807
17850
  return build();
17808
17851
  }
17809
17852
  function packRules(ability) {
17810
17853
  return ability.rules;
17811
17854
  }
17812
- var customCaslRules, entityDefinitionsRegistry, SUPERUSER_ROLES;
17855
+ var customCaslRules, seedPermissions, entityDefinitionsRegistry, SUPERUSER_ROLES;
17813
17856
  var init_ability_factory = __esm({
17814
17857
  "src/core/abilities/ability.factory.ts"() {
17815
17858
  "use strict";
17816
17859
  init_logger();
17860
+ seedPermissions = null;
17817
17861
  entityDefinitionsRegistry = null;
17818
17862
  SUPERUSER_ROLES = ["ADMIN", "OWNER"];
17819
17863
  }
@@ -18366,7 +18410,7 @@ var init_sequence = __esm({
18366
18410
  }
18367
18411
  });
18368
18412
 
18369
- // src/core/utils/error-handler.ts
18413
+ // src/core/utils/safe-json.ts
18370
18414
  function safeJsonParse(logger2, jsonString, fallback, context) {
18371
18415
  try {
18372
18416
  return JSON.parse(jsonString);
@@ -18375,8 +18419,8 @@ function safeJsonParse(logger2, jsonString, fallback, context) {
18375
18419
  return fallback;
18376
18420
  }
18377
18421
  }
18378
- var init_error_handler = __esm({
18379
- "src/core/utils/error-handler.ts"() {
18422
+ var init_safe_json = __esm({
18423
+ "src/core/utils/safe-json.ts"() {
18380
18424
  "use strict";
18381
18425
  }
18382
18426
  });
@@ -18385,14 +18429,15 @@ var init_error_handler = __esm({
18385
18429
  import express from "express";
18386
18430
  import { resolve, join as join9 } from "path";
18387
18431
  import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
18388
- function createServeSPA(app) {
18389
- return (endpoint, distPath, options = {}) => {
18432
+ function createServeSPA(app, httpServer) {
18433
+ return async (endpoint, distPath, options = {}) => {
18390
18434
  const {
18391
18435
  maxAge = "1d",
18392
18436
  etag = true,
18393
18437
  immutable = false,
18394
18438
  index = "index.html",
18395
- absolute = false
18439
+ absolute = false,
18440
+ viteSrc
18396
18441
  } = options;
18397
18442
  if (endpoint === "/api" || endpoint.startsWith("/api/")) {
18398
18443
  logger.error(`Cannot mount SPA on ${endpoint} - reserved for API routes`);
@@ -18403,58 +18448,117 @@ function createServeSPA(app) {
18403
18448
  return;
18404
18449
  }
18405
18450
  registeredEndpoints.add(endpoint);
18406
- let resolvedPath;
18407
- if (absolute) {
18408
- resolvedPath = distPath;
18409
- } else {
18410
- const projectPath2 = resolve(getProjectPath(), distPath);
18411
- if (existsSync9(projectPath2)) {
18412
- resolvedPath = projectPath2;
18451
+ if (env.NODE_ENV === "development" && viteSrc) {
18452
+ const srcPath = resolve(getProjectPath(), viteSrc);
18453
+ if (!existsSync9(srcPath)) {
18454
+ logger.warn({ endpoint, viteSrc, resolved: srcPath }, "Vite source not found \u2014 falling back to static");
18413
18455
  } else {
18414
- resolvedPath = resolve(getLibPath(), distPath);
18456
+ const mounted = await mountViteDevMiddleware(app, endpoint, srcPath, httpServer);
18457
+ if (mounted) return;
18415
18458
  }
18416
18459
  }
18417
- if (!existsSync9(resolvedPath)) {
18418
- logger.warn({ endpoint, distPath, hint: "Build the frontend first" }, `SPA directory not found: ${resolvedPath}`);
18419
- return;
18460
+ mountStaticSPA(app, endpoint, distPath, { maxAge, etag, immutable, index, absolute });
18461
+ };
18462
+ }
18463
+ async function mountViteDevMiddleware(app, endpoint, srcPath, httpServer) {
18464
+ try {
18465
+ const vite = await import("vite");
18466
+ const apiUrl = env.BACKEND_URL ? `${env.BACKEND_URL}/api/v1` : "/api/v1";
18467
+ const server2 = await vite.createServer({
18468
+ root: srcPath,
18469
+ server: {
18470
+ middlewareMode: true,
18471
+ allowedHosts: true,
18472
+ hmr: httpServer ? { server: httpServer } : true
18473
+ },
18474
+ plugins: [{
18475
+ name: "nexus-config-inject",
18476
+ transformIndexHtml(html) {
18477
+ const config3 = JSON.stringify({ apiUrl });
18478
+ return html.replace("</head>", `<script>window.__NEXUS__=${config3}</script>
18479
+ </head>`);
18480
+ }
18481
+ }],
18482
+ appType: "spa",
18483
+ clearScreen: false
18484
+ });
18485
+ viteServers.push(server2);
18486
+ if (endpoint === "/") {
18487
+ app.use(server2.middlewares);
18488
+ } else {
18489
+ app.use(endpoint, server2.middlewares);
18420
18490
  }
18421
- const indexPath = join9(resolvedPath, index);
18422
- if (!existsSync9(indexPath)) {
18423
- logger.warn({ endpoint, index }, `Index file not found: ${indexPath}`);
18424
- }
18425
- app.use(endpoint, express.static(resolvedPath, { maxAge, etag, immutable }));
18426
- let injectedHtml = "";
18427
- if (existsSync9(indexPath)) {
18428
- const rawHtml = readFileSync5(indexPath, "utf-8");
18429
- const apiUrl = env.BACKEND_URL ? `${env.BACKEND_URL}/api/v1` : "/api/v1";
18430
- const nexusConfig = JSON.stringify({ apiUrl });
18431
- injectedHtml = rawHtml.replace(
18432
- "</head>",
18433
- `<script>window.__NEXUS__=${nexusConfig}</script>
18434
- </head>`
18435
- );
18491
+ logger.info({ path: srcPath }, `Vite dev server mounted at ${endpoint} (HMR enabled)`);
18492
+ return true;
18493
+ } catch (err) {
18494
+ if (err.code === "ERR_MODULE_NOT_FOUND" || err.code === "MODULE_NOT_FOUND") {
18495
+ logger.warn(`vite not installed \u2014 falling back to static serving for ${endpoint}`);
18496
+ return false;
18436
18497
  }
18437
- const fallbackHandler = (_req, res) => {
18438
- if (!injectedHtml) {
18439
- res.status(404).send("index.html not found");
18440
- return;
18441
- }
18442
- res.set("Cache-Control", "no-cache, no-store, must-revalidate");
18443
- res.type("html").send(injectedHtml);
18444
- };
18445
- if (endpoint === "/") {
18446
- app.get("{*splat}", fallbackHandler);
18498
+ logger.error({ err }, `Failed to mount Vite dev server at ${endpoint}`);
18499
+ return false;
18500
+ }
18501
+ }
18502
+ function mountStaticSPA(app, endpoint, distPath, options) {
18503
+ const { maxAge, etag, immutable, index, absolute } = options;
18504
+ let resolvedPath;
18505
+ if (absolute) {
18506
+ resolvedPath = distPath;
18507
+ } else {
18508
+ const projectPath2 = resolve(getProjectPath(), distPath);
18509
+ if (existsSync9(projectPath2)) {
18510
+ resolvedPath = projectPath2;
18447
18511
  } else {
18448
- app.get(endpoint, fallbackHandler);
18449
- app.get(`${endpoint}/{*splat}`, fallbackHandler);
18512
+ resolvedPath = resolve(getLibPath(), distPath);
18513
+ }
18514
+ }
18515
+ if (!existsSync9(resolvedPath)) {
18516
+ logger.warn({ endpoint, distPath, hint: "Build the frontend first" }, `SPA directory not found: ${resolvedPath}`);
18517
+ return;
18518
+ }
18519
+ const indexPath = join9(resolvedPath, index);
18520
+ if (!existsSync9(indexPath)) {
18521
+ logger.warn({ endpoint, index }, `Index file not found: ${indexPath}`);
18522
+ }
18523
+ app.use(endpoint, express.static(resolvedPath, { maxAge, etag, immutable }));
18524
+ let injectedHtml = "";
18525
+ if (existsSync9(indexPath)) {
18526
+ const rawHtml = readFileSync5(indexPath, "utf-8");
18527
+ const apiUrl = env.BACKEND_URL ? `${env.BACKEND_URL}/api/v1` : "/api/v1";
18528
+ const nexusConfig = JSON.stringify({ apiUrl });
18529
+ injectedHtml = rawHtml.replace(
18530
+ "</head>",
18531
+ `<script>window.__NEXUS__=${nexusConfig}</script>
18532
+ </head>`
18533
+ );
18534
+ }
18535
+ const fallbackHandler = (_req, res) => {
18536
+ if (!injectedHtml) {
18537
+ res.status(404).send("index.html not found");
18538
+ return;
18450
18539
  }
18451
- logger.info({ path: resolvedPath }, `SPA mounted at ${endpoint}`);
18540
+ res.set("Cache-Control", "no-cache, no-store, must-revalidate");
18541
+ res.type("html").send(injectedHtml);
18452
18542
  };
18543
+ if (endpoint === "/") {
18544
+ app.get("{*splat}", fallbackHandler);
18545
+ } else {
18546
+ app.get(endpoint, fallbackHandler);
18547
+ app.get(`${endpoint}/{*splat}`, fallbackHandler);
18548
+ }
18549
+ logger.info({ path: resolvedPath }, `SPA mounted at ${endpoint}`);
18453
18550
  }
18454
- function resetServeSPAEndpoints() {
18551
+ async function resetServeSPA() {
18552
+ for (const server2 of viteServers) {
18553
+ try {
18554
+ await server2.close();
18555
+ } catch {
18556
+ }
18557
+ }
18558
+ viteServers.length = 0;
18455
18559
  registeredEndpoints.clear();
18456
18560
  }
18457
- var registeredEndpoints;
18561
+ var registeredEndpoints, viteServers;
18458
18562
  var init_spa_handler = __esm({
18459
18563
  "src/core/spa-handler.ts"() {
18460
18564
  "use strict";
@@ -18462,6 +18566,7 @@ var init_spa_handler = __esm({
18462
18566
  init_logger();
18463
18567
  init_env();
18464
18568
  registeredEndpoints = /* @__PURE__ */ new Set();
18569
+ viteServers = [];
18465
18570
  }
18466
18571
  });
18467
18572
 
@@ -19032,7 +19137,7 @@ var init_core = __esm({
19032
19137
  init_id();
19033
19138
  init_sequence();
19034
19139
  init_paths();
19035
- init_error_handler();
19140
+ init_safe_json();
19036
19141
  init_spa_handler();
19037
19142
  init_cache();
19038
19143
  init_jwt();
@@ -19747,7 +19852,9 @@ var init_base_service = __esm({
19747
19852
  const countResult = await qb.clone().count("* as count").first();
19748
19853
  const total = Number(countResult?.count ?? 0);
19749
19854
  qb = this.applySortingWithDefaults(qb, query);
19750
- qb = qb.limit(limit).offset(offset);
19855
+ if (limit > 0) {
19856
+ qb = qb.limit(limit).offset(offset);
19857
+ }
19751
19858
  const rawItems = await qb;
19752
19859
  const items = this.parseJsonFieldsFromArray(rawItems);
19753
19860
  const processedItems = await this.afterFindAll(items);
@@ -19832,7 +19939,7 @@ var init_base_service = __esm({
19832
19939
  });
19833
19940
  }
19834
19941
  const total = result.length;
19835
- const paginatedItems = result.slice(offset, offset + limit);
19942
+ const paginatedItems = limit === 0 ? result : result.slice(offset, offset + limit);
19836
19943
  return this.buildPaginatedResult(paginatedItems, total, page, limit);
19837
19944
  }
19838
19945
  /**
@@ -20087,14 +20194,14 @@ var init_base_service = __esm({
20087
20194
  * Build paginated result from items and count
20088
20195
  */
20089
20196
  buildPaginatedResult(items, total, page, limit) {
20090
- const totalPages = Math.ceil(total / limit);
20197
+ const totalPages = limit === 0 ? 1 : Math.ceil(total / limit);
20091
20198
  return {
20092
20199
  items,
20093
20200
  total,
20094
- page,
20095
- limit,
20201
+ page: limit === 0 ? 1 : page,
20202
+ limit: limit === 0 ? total : limit,
20096
20203
  totalPages,
20097
- hasNext: page < totalPages
20204
+ hasNext: limit === 0 ? false : page < totalPages
20098
20205
  };
20099
20206
  }
20100
20207
  /**
@@ -20103,8 +20210,8 @@ var init_base_service = __esm({
20103
20210
  getPagination(query) {
20104
20211
  const maxLimit = query?.maxLimit ?? 100;
20105
20212
  const page = Math.max(1, query?.page ?? 1);
20106
- const limit = Math.min(maxLimit, Math.max(1, query?.limit ?? 20));
20107
- const offset = (page - 1) * limit;
20213
+ const limit = query?.limit === 0 ? 0 : Math.min(maxLimit, Math.max(1, query?.limit ?? 20));
20214
+ const offset = limit === 0 ? 0 : (page - 1) * limit;
20108
20215
  return { page, limit, offset };
20109
20216
  }
20110
20217
  /**
@@ -21751,7 +21858,8 @@ function createEntityController(service, definition, ctx) {
21751
21858
  async list(req, res) {
21752
21859
  checkPermission(req, "read");
21753
21860
  const page = Math.max(1, parseInt(req.query["page"]) || 1);
21754
- const limit = Math.min(100, Math.max(1, parseInt(req.query["limit"]) || 20));
21861
+ const rawLimit = parseInt(req.query["limit"]);
21862
+ const limit = rawLimit === 0 ? 0 : Math.min(100, Math.max(1, rawLimit || 20));
21755
21863
  let filters;
21756
21864
  if (req.query["filters"]) {
21757
21865
  filters = parseFilters(req.query["filters"], ctx.core.errors);
@@ -23217,17 +23325,18 @@ async function createModuleRouters(ctx, definitions, modulePrefix) {
23217
23325
  const runtime = await createEntityRuntimeAsync(ctx, definition);
23218
23326
  const route = inferEntityRoutePath(definition);
23219
23327
  const entityLabel = resolveLocalized7(definition.label, "en");
23220
- if (modulePrefix && route === modulePrefix) {
23328
+ const isExposed = !("expose" in definition && definition.expose === false);
23329
+ if (isExposed && modulePrefix && route === modulePrefix) {
23221
23330
  ctx.core.logger.warn(
23222
23331
  `Entity "${entityLabel}" inferred route "${route}" duplicates module prefix \u2014 add routePrefix to the entity definition to fix`
23223
23332
  );
23224
23333
  }
23225
- if (routeMap.has(route)) {
23334
+ if (isExposed && routeMap.has(route)) {
23226
23335
  ctx.core.logger.warn(
23227
23336
  `Entity "${entityLabel}" route "${route}" collides with "${routeMap.get(route)}" in the same module`
23228
23337
  );
23229
23338
  }
23230
- routeMap.set(route, entityLabel);
23339
+ if (isExposed) routeMap.set(route, entityLabel);
23231
23340
  router.use(route, runtime.router);
23232
23341
  const key = getServiceKey(definition);
23233
23342
  if (ctx.services.has(key)) {
@@ -23443,7 +23552,7 @@ var init_runtime = __esm({
23443
23552
  }
23444
23553
  });
23445
23554
 
23446
- // src/db/module-runner.ts
23555
+ // src/db/seed-runner.ts
23447
23556
  import { existsSync as existsSync10 } from "fs";
23448
23557
  import { join as join10 } from "path";
23449
23558
  import { pathToFileURL } from "url";
@@ -23511,8 +23620,8 @@ function hasSeedData(seed5) {
23511
23620
  if (!Array.isArray(seed5) && "source" in seed5 && seed5.source === "url") return true;
23512
23621
  return Array.isArray(seed5) && seed5.length > 0;
23513
23622
  }
23514
- var init_module_runner = __esm({
23515
- "src/db/module-runner.ts"() {
23623
+ var init_seed_runner = __esm({
23624
+ "src/db/seed-runner.ts"() {
23516
23625
  "use strict";
23517
23626
  init_runtime();
23518
23627
  init_paths();
@@ -23615,7 +23724,7 @@ var init_migration_sources = __esm({
23615
23724
  init_shared();
23616
23725
  init_paths();
23617
23726
  init_plugin_ops();
23618
- init_store();
23727
+ init_module_store();
23619
23728
  }
23620
23729
  });
23621
23730
 
@@ -23948,7 +24057,7 @@ async function runMigrations(knexInstance, sources) {
23948
24057
  const executedMigrations = await knex3("_nexus_migrations").where({ status: "completed" }).select("name").then((rows) => new Set(rows.map((r) => r.name)));
23949
24058
  const pendingMigrations = migrationFiles.filter((m) => !executedMigrations.has(m.name));
23950
24059
  if (pendingMigrations.length === 0) {
23951
- logger.info("No pending migrations");
24060
+ logger.debug("No pending migrations");
23952
24061
  return;
23953
24062
  }
23954
24063
  const batch = await getNextBatch(knex3);
@@ -24348,6 +24457,27 @@ var init_migration_helpers = __esm({
24348
24457
  import path2 from "path";
24349
24458
  import fs2 from "fs/promises";
24350
24459
  import { readFileSync as readFileSync6, mkdirSync as mkdirSync5, realpathSync } from "fs";
24460
+ function getColumnIndexBytes(field) {
24461
+ if (!field?.db) return 255 * MYSQL_BYTES_PER_CHAR;
24462
+ const size = field.db.size ?? 255;
24463
+ if (field.db.type === "text") return 0;
24464
+ if (field.db.type === "string") return size * MYSQL_BYTES_PER_CHAR;
24465
+ if (field.db.type === "integer") return 4;
24466
+ if (field.db.type === "boolean") return 1;
24467
+ if (field.db.type === "datetime" || field.db.type === "date") return 8;
24468
+ if (field.db.type === "uuid") return 16;
24469
+ return size * MYSQL_BYTES_PER_CHAR;
24470
+ }
24471
+ function warnIfIndexExceedsMySQLLimit(table, columns, unique, fields) {
24472
+ if (!fields) return;
24473
+ const totalBytes = columns.reduce((sum, col) => sum + getColumnIndexBytes(fields[col]), 0);
24474
+ if (totalBytes > MYSQL_MAX_INDEX_BYTES) {
24475
+ const indexType = unique ? "UNIQUE" : "INDEX";
24476
+ logger.warn(
24477
+ `${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.`
24478
+ );
24479
+ }
24480
+ }
24351
24481
  async function detectSchemaDrift(knexInstance) {
24352
24482
  const knex3 = knexInstance ?? getDb();
24353
24483
  const entities = getAllPersistentEntities();
@@ -24455,6 +24585,7 @@ function computeSchemaDiff(entities, currentSchema, options) {
24455
24585
  for (const idx of entityIndexes) {
24456
24586
  const key = normalizeKey(idx.columns, !!idx.unique);
24457
24587
  if (!currentKeys.has(key)) {
24588
+ warnIfIndexExceedsMySQLLimit(tableName, idx.columns, !!idx.unique, entity.fields);
24458
24589
  diff.newIndexes.push({ columns: idx.columns, unique: !!idx.unique });
24459
24590
  }
24460
24591
  }
@@ -24626,7 +24757,7 @@ function formatDriftMessage(drift) {
24626
24757
  lines.push('Run "pnpm migrate:dev" to generate and apply migrations.');
24627
24758
  return lines.join("\n");
24628
24759
  }
24629
- var PERSISTENT_TYPES;
24760
+ var MYSQL_MAX_INDEX_BYTES, MYSQL_BYTES_PER_CHAR, PERSISTENT_TYPES;
24630
24761
  var init_migration_generator = __esm({
24631
24762
  "src/db/migration-generator.ts"() {
24632
24763
  "use strict";
@@ -24635,9 +24766,11 @@ var init_migration_generator = __esm({
24635
24766
  init_paths();
24636
24767
  init_schema_reader();
24637
24768
  init_engine();
24638
- init_queries();
24769
+ init_module_queries();
24639
24770
  init_migration_helpers();
24640
- init_store();
24771
+ init_module_store();
24772
+ MYSQL_MAX_INDEX_BYTES = 3072;
24773
+ MYSQL_BYTES_PER_CHAR = 4;
24641
24774
  PERSISTENT_TYPES = /* @__PURE__ */ new Set([
24642
24775
  "collection",
24643
24776
  "tree",
@@ -26211,7 +26344,7 @@ var init_db = __esm({
26211
26344
  init_schema_helpers();
26212
26345
  init_sql_utils();
26213
26346
  init_sqlite_compat();
26214
- init_module_runner();
26347
+ init_seed_runner();
26215
26348
  init_ensure_system_tables();
26216
26349
  init_migration_sources();
26217
26350
  init_migration_runner();
@@ -26475,8 +26608,11 @@ var init_events_api = __esm({
26475
26608
 
26476
26609
  // src/engine/context.ts
26477
26610
  import { ForbiddenError as CASLForbiddenError3, subject } from "@casl/ability";
26478
- import { DEFAULT_TENANT_ID as DEFAULT_TENANT_ID2 } from "@gzl10/nexus-sdk";
26611
+ import { DEFAULT_TENANT_ID as DEFAULT_TENANT_ID2, DEFAULT_LOCALES } from "@gzl10/nexus-sdk";
26479
26612
  import { Redis as Redis2 } from "ioredis";
26613
+ function setLocales(locales) {
26614
+ platformLocales = locales;
26615
+ }
26480
26616
  function getSharedCacheManager() {
26481
26617
  if (!sharedCacheManager) {
26482
26618
  const redisUrl = env.REDIS_URL;
@@ -26552,7 +26688,7 @@ function createModuleContext() {
26552
26688
  const defaultAdapter = createKnexAdapter(knex3);
26553
26689
  const defaultSchemaAdapter = createKnexSchemaAdapter(knex3);
26554
26690
  adaptersRegistry["temp"] = { data: getSharedTempAdapter() };
26555
- logger.debug(env.REDIS_URL ? "Temp adapter: Redis (shared)" : "Temp adapter: InMemory (shared)");
26691
+ logger.trace(env.REDIS_URL ? "Temp adapter: Redis (shared)" : "Temp adapter: InMemory (shared)");
26556
26692
  const middleware = {
26557
26693
  validate,
26558
26694
  rateLimit: createRateLimit,
@@ -26660,7 +26796,9 @@ function createModuleContext() {
26660
26796
  throw new Error(`Knex connection for adapter "${adapter}" not found. Available: ${Object.keys(knexConnections).join(", ")}`);
26661
26797
  }
26662
26798
  return conn;
26663
- }
26799
+ },
26800
+ // Placeholder — bound after ctx construction (needs full ModuleContext)
26801
+ seedModule: null
26664
26802
  };
26665
26803
  const configContext = {
26666
26804
  env,
@@ -26753,7 +26891,8 @@ function createModuleContext() {
26753
26891
  adapters: adaptersContext,
26754
26892
  // Root-level shortcuts for frequently used utilities
26755
26893
  events: createEventsApi(nexusEvents, logger),
26756
- createRouter: () => createRouter()
26894
+ createRouter: () => createRouter(),
26895
+ locales: platformLocales
26757
26896
  };
26758
26897
  servicesRegistry["cacheManager"] = getSharedCacheManager();
26759
26898
  ctx.runtime = {
@@ -26762,9 +26901,10 @@ function createModuleContext() {
26762
26901
  createEntityController: (service, def) => createEntityController(service, def, ctx),
26763
26902
  createEntityRouter: (controller, def) => createEntityRouter(controller, def, ctx)
26764
26903
  };
26904
+ ctx.db.seedModule = (mod) => runModuleSeed(mod, ctx);
26765
26905
  return ctx;
26766
26906
  }
26767
- var sharedCacheManager, sharedTempAdapter;
26907
+ var platformLocales, sharedCacheManager, sharedTempAdapter;
26768
26908
  var init_context = __esm({
26769
26909
  "src/engine/context.ts"() {
26770
26910
  "use strict";
@@ -26777,7 +26917,9 @@ var init_context = __esm({
26777
26917
  init_plugin_ops();
26778
26918
  init_load_config();
26779
26919
  init_events_api();
26920
+ init_seed_runner();
26780
26921
  init_cache_manager();
26922
+ platformLocales = DEFAULT_LOCALES;
26781
26923
  sharedCacheManager = null;
26782
26924
  sharedTempAdapter = null;
26783
26925
  }
@@ -26788,11 +26930,11 @@ var init_engine = __esm({
26788
26930
  "src/engine/index.ts"() {
26789
26931
  "use strict";
26790
26932
  init_registry();
26791
- init_queries();
26933
+ init_module_queries();
26792
26934
  init_loader();
26793
- init_store();
26794
- init_subjectExtractor();
26795
- init_extractors();
26935
+ init_module_store();
26936
+ init_subject_extractor();
26937
+ init_definition_extractors();
26796
26938
  init_context();
26797
26939
  }
26798
26940
  });
@@ -27328,7 +27470,7 @@ async function createApp(options = {}) {
27328
27470
  // Only accept arrays and objects
27329
27471
  }));
27330
27472
  app.use(cookieParser());
27331
- const serveSPA = createServeSPA(app);
27473
+ const serveSPA = createServeSPA(app, options.httpServer);
27332
27474
  if (options.beforeRoutes) {
27333
27475
  const result = options.beforeRoutes(app, serveSPA);
27334
27476
  if (result instanceof Promise) {
@@ -27422,11 +27564,11 @@ async function createApp(options = {}) {
27422
27564
  });
27423
27565
  const sortedSpas = [...servedSpas].sort((a, b) => b.endpoint.length - a.endpoint.length);
27424
27566
  for (const spa of sortedSpas) {
27425
- serveSPA(spa.endpoint, spa.path, spa);
27567
+ await serveSPA(spa.endpoint, spa.path, { ...spa, viteSrc: spa.viteSrc });
27426
27568
  }
27427
27569
  const { ui } = getConfig();
27428
27570
  if (ui.enabled) {
27429
- serveSPA(ui.base, ui.path);
27571
+ await serveSPA(ui.base, ui.path, { viteSrc: "../ui" });
27430
27572
  }
27431
27573
  app.use(errorMiddleware);
27432
27574
  return app;
@@ -27452,58 +27594,32 @@ var init_app = __esm({
27452
27594
  }
27453
27595
  });
27454
27596
 
27455
- // src/core/utils/net.ts
27597
+ // src/core/utils/port-check.ts
27456
27598
  import net from "net";
27457
27599
  import { execSync } from "child_process";
27458
27600
  function findProcessOnPort(port) {
27459
27601
  try {
27460
27602
  const output = execSync(`lsof -ti :${port} 2>/dev/null`, { encoding: "utf-8" });
27461
27603
  const pid = parseInt(output.trim().split("\n")[0] ?? "", 10);
27462
- 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 };
27463
27611
  } catch {
27464
27612
  return null;
27465
27613
  }
27466
27614
  }
27467
- function isSameProcessGroup(pid) {
27468
- if (pid === process.pid || pid === process.ppid) return true;
27469
- try {
27470
- const ourPgid = execSync(`ps -o pgid= -p ${process.pid} 2>/dev/null`, { encoding: "utf-8" }).trim();
27471
- const targetPgid = execSync(`ps -o pgid= -p ${pid} 2>/dev/null`, { encoding: "utf-8" }).trim();
27472
- return ourPgid === targetPgid;
27473
- } catch {
27474
- return false;
27475
- }
27476
- }
27477
- function killProcessOnPort(port) {
27478
- const pid = findProcessOnPort(port);
27479
- if (!pid) return false;
27480
- if (isSameProcessGroup(pid)) {
27481
- logger.warn({ port, pid }, `Skipping same process group (PID ${pid}) on port ${port}`);
27482
- return false;
27483
- }
27484
- try {
27485
- process.kill(pid, "SIGTERM");
27486
- logger.warn({ port, pid }, `Killed process ${pid} on port ${port}`);
27487
- return true;
27488
- } catch {
27489
- return false;
27490
- }
27491
- }
27492
27615
  async function checkPortAvailable(port, host = "0.0.0.0") {
27493
27616
  return new Promise((resolve2, reject) => {
27494
27617
  const server2 = net.createServer();
27495
27618
  server2.once("error", (err) => {
27496
27619
  if (err.code === "EADDRINUSE") {
27497
- if (process.env["NODE_ENV"] !== "production") {
27498
- if (killProcessOnPort(port)) {
27499
- setTimeout(() => {
27500
- checkPortAvailable(port, host).then(resolve2).catch(reject);
27501
- }, 500);
27502
- return;
27503
- }
27504
- }
27505
- logger.error({ port }, `Port ${port} is already in use`);
27506
- 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));
27507
27623
  } else {
27508
27624
  reject(err);
27509
27625
  }
@@ -27514,10 +27630,9 @@ async function checkPortAvailable(port, host = "0.0.0.0") {
27514
27630
  server2.listen(port, host);
27515
27631
  });
27516
27632
  }
27517
- var init_net = __esm({
27518
- "src/core/utils/net.ts"() {
27633
+ var init_port_check = __esm({
27634
+ "src/core/utils/port-check.ts"() {
27519
27635
  "use strict";
27520
- init_logger();
27521
27636
  }
27522
27637
  });
27523
27638
 
@@ -27612,6 +27727,150 @@ var init_tunnel = __esm({
27612
27727
  }
27613
27728
  });
27614
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
+
27615
27874
  // src/core/server.ts
27616
27875
  var server_exports = {};
27617
27876
  __export(server_exports, {
@@ -27620,7 +27879,9 @@ __export(server_exports, {
27620
27879
  start: () => start,
27621
27880
  stop: () => stop
27622
27881
  });
27882
+ import http from "http";
27623
27883
  import { entityRoom as entityRoom6 } from "@gzl10/nexus-sdk";
27884
+ import { DEFAULT_LOCALES as DEFAULT_LOCALES2 } from "@gzl10/nexus-sdk";
27624
27885
  async function runMigrationsAndSeeds(config3) {
27625
27886
  initLoggerService(getLoggerConfig());
27626
27887
  setLoggerInstance(getPinoLogger());
@@ -27673,7 +27934,7 @@ async function runMigrationsAndSeeds(config3) {
27673
27934
  const sources = buildMigrationSources();
27674
27935
  const migrationFiles = await loadAllMigrationFiles(sources);
27675
27936
  if (migrationFiles.length > 0) {
27676
- logger.info({ count: migrationFiles.length }, "Deploying pending migrations...");
27937
+ logger.info({ sources: sources.length, files: migrationFiles.length }, "Running migration deploy...");
27677
27938
  try {
27678
27939
  await runMigrations(void 0, sources);
27679
27940
  } catch (err) {
@@ -27682,7 +27943,7 @@ async function runMigrationsAndSeeds(config3) {
27682
27943
  throw err;
27683
27944
  }
27684
27945
  }
27685
- logger.info("Checking schema drift...");
27946
+ logger.debug("Checking schema drift...");
27686
27947
  const drift = await detectSchemaDrift();
27687
27948
  if (drift && (drift.newTables.length > 0 || drift.alteredTables.length > 0)) {
27688
27949
  const message = formatDriftMessage(drift);
@@ -27729,7 +27990,7 @@ ${dirs}`);
27729
27990
  }
27730
27991
  const allDefinitions = modules.flatMap((m) => m.definitions ?? []);
27731
27992
  await createMemoryTables(allDefinitions);
27732
- logger.info("Running seeds...");
27993
+ logger.debug("Running seeds...");
27733
27994
  for (const mod of modules) {
27734
27995
  try {
27735
27996
  await runModuleSeed(mod, ctx);
@@ -27737,12 +27998,39 @@ ${dirs}`);
27737
27998
  logger.error({ module: mod.name, err }, "Seed failed - continuing with next module");
27738
27999
  }
27739
28000
  }
28001
+ if (config3?.onSeed) {
28002
+ const { getPlugins: getPlugins2 } = await Promise.resolve().then(() => (init_module_queries(), module_queries_exports));
28003
+ const { createMasterRegistry: createMasterRegistry2 } = await Promise.resolve().then(() => (init_registry2(), registry_exports));
28004
+ const masterRegistry = ctx.services.has("masters") ? ctx.services.get("masters") : createMasterRegistry2();
28005
+ const pluginPrefixes = /* @__PURE__ */ new Map();
28006
+ for (const plugin of getPlugins2()) {
28007
+ pluginPrefixes.set(plugin.code, `${plugin.code}_`);
28008
+ const shortName = plugin.name.replace(/^@[^/]+\/nexus-plugin-/, "");
28009
+ if (shortName !== plugin.code) {
28010
+ pluginPrefixes.set(shortName, `${plugin.code}_`);
28011
+ }
28012
+ }
28013
+ const { createSeedContext: createSeedContext2 } = await Promise.resolve().then(() => (init_seed_context(), seed_context_exports));
28014
+ const { ctx: seedCtx, flushPermissions } = createSeedContext2({
28015
+ knex: ctx.db.knex,
28016
+ generateId: ctx.core.generateId,
28017
+ hashPassword: ctx.core.crypto.hashPassword,
28018
+ masterRegistry,
28019
+ pluginPrefixes,
28020
+ logger: ctx.core.logger,
28021
+ onPermissionsCollected: (perms) => setSeedPermissions(perms)
28022
+ });
28023
+ await config3.onSeed(seedCtx);
28024
+ await masterRegistry.seed(ctx);
28025
+ flushPermissions();
28026
+ }
27740
28027
  }
27741
28028
  async function start(config3) {
27742
28029
  if (server) {
27743
28030
  throw new Error("Server already running. Call stop() first.");
27744
28031
  }
27745
28032
  currentConfig = config3;
28033
+ setLocales(config3?.locales ?? DEFAULT_LOCALES2);
27746
28034
  if (env.NODE_ENV === "development" && env.FRPC_SERVER && env.FRPC_SUBDOMAIN && !env.BACKEND_URL) {
27747
28035
  process.env["BACKEND_URL"] = getTunnelUrl(env.FRPC_SUBDOMAIN, env.FRPC_SERVER);
27748
28036
  }
@@ -27767,13 +28055,17 @@ async function start(config3) {
27767
28055
  await initTelemetry2();
27768
28056
  await runMigrationsAndSeeds(config3);
27769
28057
  const effectiveCorsOrigins = buildEffectiveCorsOrigins(env.CORS_ORIGIN, config3?.spas);
28058
+ const httpServer = http.createServer();
27770
28059
  const app = await createApp({
27771
28060
  beforeRoutes: config3?.beforeRoutes,
27772
28061
  afterRoutes: config3?.afterRoutes,
27773
- spas: config3?.spas
28062
+ spas: config3?.spas,
28063
+ httpServer
27774
28064
  });
28065
+ httpServer.on("request", app);
27775
28066
  return new Promise((resolve2) => {
27776
- server = app.listen(resolved.port, resolved.host, async () => {
28067
+ server = httpServer;
28068
+ httpServer.listen(resolved.port, resolved.host, async () => {
27777
28069
  const timeoutMs = parseInt(process.env["REQUEST_TIMEOUT_MS"] || "30000", 10);
27778
28070
  if (timeoutMs > 0) {
27779
28071
  server.setTimeout(timeoutMs);
@@ -27809,12 +28101,10 @@ async function start(config3) {
27809
28101
  });
27810
28102
  }
27811
28103
  const baseUrl = env.BACKEND_URL || `http://localhost:${actualPort}`;
27812
- logger.info({ libPath: getLibPath(), projectPath: getProjectPath() }, "Paths");
27813
- logger.info(`API: ${baseUrl}/api/v1`);
27814
- if (resolved.ui.enabled) {
27815
- logger.info(`UI: ${baseUrl}`);
27816
- }
27817
- logger.info({ port: actualPort, mode: resolved.nodeEnv }, "Server started");
28104
+ logger.debug({ libPath: getLibPath(), projectPath: getProjectPath() }, "Paths");
28105
+ const urls = { api: `${baseUrl}/api/v1` };
28106
+ if (resolved.ui.enabled) urls["ui"] = baseUrl;
28107
+ logger.info({ port: actualPort, mode: resolved.nodeEnv, ...urls }, "Server started");
27818
28108
  nexusEvents.emitEvent("server.started", { port: actualPort, host: resolved.host });
27819
28109
  if (config3?.onReady) {
27820
28110
  try {
@@ -27840,7 +28130,8 @@ async function stop() {
27840
28130
  await resetSharedAdapters();
27841
28131
  resetConfigCache();
27842
28132
  clearCustomCaslRules();
27843
- resetServeSPAEndpoints();
28133
+ clearSeedPermissions();
28134
+ await resetServeSPA();
27844
28135
  return;
27845
28136
  }
27846
28137
  if (currentConfig?.beforeClose) {
@@ -27874,7 +28165,8 @@ async function stop() {
27874
28165
  await resetSharedAdapters();
27875
28166
  resetConfigCache();
27876
28167
  clearCustomCaslRules();
27877
- resetServeSPAEndpoints();
28168
+ clearSeedPermissions();
28169
+ await resetServeSPA();
27878
28170
  currentConfig = void 0;
27879
28171
  server = null;
27880
28172
  nexusEvents.emitEvent("server.stopped");
@@ -27934,7 +28226,7 @@ var init_server = __esm({
27934
28226
  init_cors();
27935
28227
  init_logger();
27936
28228
  init_error_middleware();
27937
- init_net();
28229
+ init_port_check();
27938
28230
  init_engine();
27939
28231
  init_context();
27940
28232
  init_db();