@gzl10/nexus-backend 0.16.8 → 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.16.8",
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,22 +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-links": "workspace:*",
136
- "@gzl10/nexus-plugin-notifications": "workspace:*",
137
- "@gzl10/nexus-plugin-oidc-server": "workspace:^",
138
- "@gzl10/nexus-plugin-plane": "workspace:^",
139
- "@gzl10/nexus-plugin-prisma": "workspace:^",
140
- "@gzl10/nexus-plugin-remote": "workspace:*",
141
- "@gzl10/nexus-plugin-schedules": "workspace:*",
142
- "@gzl10/nexus-plugin-scim": "workspace:^",
143
- "@gzl10/nexus-plugin-tags": "workspace:*",
144
- "@gzl10/nexus-plugin-webhooks": "workspace:*",
145
129
  "@types/bcryptjs": "^2.4.0",
146
130
  "@types/compression": "^1.8.1",
147
131
  "@types/cookie-parser": "^1.4.10",
@@ -154,7 +138,16 @@ var init_package = __esm({
154
138
  "pino-pretty": "^13.1.3",
155
139
  "socket.io-client": "^4.8.3",
156
140
  supertest: "^7.2.2",
157
- 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
+ }
158
151
  },
159
152
  publishConfig: {
160
153
  access: "public",
@@ -1055,15 +1048,17 @@ async function isWorkspacePackage(name) {
1055
1048
  }
1056
1049
  }
1057
1050
  async function installPlugin(name, opts) {
1051
+ const isWorkspace = await isWorkspacePackage(name);
1058
1052
  let pkg2;
1059
1053
  if (opts?.version) {
1060
1054
  pkg2 = `${name}@${opts.version}`;
1061
- } else if (await isWorkspacePackage(name)) {
1055
+ } else if (isWorkspace) {
1062
1056
  pkg2 = `${name}@workspace:*`;
1063
1057
  } else {
1064
1058
  pkg2 = name;
1065
1059
  }
1066
- await execAsync(`pnpm add -D ${pkg2}`);
1060
+ const flag = isWorkspace ? "-D " : "";
1061
+ await execAsync(`pnpm add ${flag}${pkg2}`);
1067
1062
  const filePath = getPluginsFilePath(opts?.projectPath);
1068
1063
  const plugins = readPluginsFile(filePath);
1069
1064
  plugins[name] = { enabled: true };
@@ -1154,7 +1149,7 @@ var init_table_prefix = __esm({
1154
1149
  }
1155
1150
  });
1156
1151
 
1157
- // src/engine/store.ts
1152
+ // src/engine/module-store.ts
1158
1153
  function resetStore() {
1159
1154
  moduleStore.modules.length = 0;
1160
1155
  moduleStore.plugins.clear();
@@ -1165,8 +1160,8 @@ function resetStore() {
1165
1160
  moduleStore.tableToSubject.clear();
1166
1161
  }
1167
1162
  var moduleStore;
1168
- var init_store = __esm({
1169
- "src/engine/store.ts"() {
1163
+ var init_module_store = __esm({
1164
+ "src/engine/module-store.ts"() {
1170
1165
  "use strict";
1171
1166
  moduleStore = {
1172
1167
  /** Registered modules with source metadata */
@@ -1219,7 +1214,7 @@ var init_id = __esm({
1219
1214
  }
1220
1215
  });
1221
1216
 
1222
- // src/engine/extractors.ts
1217
+ // src/engine/definition-extractors.ts
1223
1218
  function getTableAndSubject(def) {
1224
1219
  const caslSubject = def.casl?.subject;
1225
1220
  if (!TYPES_WITH_TABLE.has(def.type)) {
@@ -1287,8 +1282,8 @@ function validateModuleDependencies(modules) {
1287
1282
  }
1288
1283
  }
1289
1284
  var TYPES_WITH_TABLE;
1290
- var init_extractors = __esm({
1291
- "src/engine/extractors.ts"() {
1285
+ var init_definition_extractors = __esm({
1286
+ "src/engine/definition-extractors.ts"() {
1292
1287
  "use strict";
1293
1288
  TYPES_WITH_TABLE = /* @__PURE__ */ new Set(["collection", "reference", "event", "config", "temp", "view", void 0]);
1294
1289
  }
@@ -1408,9 +1403,9 @@ var init_registry = __esm({
1408
1403
  "use strict";
1409
1404
  init_plugin_ops();
1410
1405
  init_table_prefix();
1411
- init_store();
1406
+ init_module_store();
1412
1407
  init_id();
1413
- init_extractors();
1408
+ init_definition_extractors();
1414
1409
  }
1415
1410
  });
1416
1411
 
@@ -1473,7 +1468,28 @@ var init_paths = __esm({
1473
1468
  }
1474
1469
  });
1475
1470
 
1476
- // 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
+ });
1477
1493
  import { join as join4 } from "path";
1478
1494
  import { readFileSync as readFileSync3 } from "fs";
1479
1495
  function readPackageJson(dir) {
@@ -1569,10 +1585,10 @@ function getPluginByCode(code) {
1569
1585
  function hasPluginByCode(code) {
1570
1586
  return moduleStore.pluginsByCode.has(code);
1571
1587
  }
1572
- var init_queries = __esm({
1573
- "src/engine/queries.ts"() {
1588
+ var init_module_queries = __esm({
1589
+ "src/engine/module-queries.ts"() {
1574
1590
  "use strict";
1575
- init_store();
1591
+ init_module_store();
1576
1592
  init_paths();
1577
1593
  }
1578
1594
  });
@@ -2057,12 +2073,16 @@ var init_definitions = __esm({
2057
2073
  labelPlural: { en: "Masters", es: "Maestros" },
2058
2074
  labelField: "label",
2059
2075
  timestamps: true,
2076
+ availableDisplayModes: ["board"],
2077
+ groupBy: "type",
2078
+ groupableFields: ["type"],
2079
+ //columnDragFields: ['is_active'],
2060
2080
  fields: {
2061
2081
  id: useTextField2({
2062
2082
  label: { en: "ID", es: "ID" },
2063
2083
  required: true,
2064
2084
  size: 100,
2065
- hint: { en: "Format: type:code (e.g. countries:ES)", es: "Formato: type:code (ej. countries:ES)" },
2085
+ hidden: true,
2066
2086
  meta: { sortable: true }
2067
2087
  }),
2068
2088
  type: useTextField2({
@@ -2083,9 +2103,20 @@ var init_definitions = __esm({
2083
2103
  is_active: isActiveField,
2084
2104
  metadata: useJsonField({
2085
2105
  label: { en: "Metadata", es: "Metadatos" },
2086
- 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
+ }
2087
2110
  })
2088
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
+ }),
2089
2120
  defaultSort: { field: "order", order: "asc" },
2090
2121
  indexes: [{ columns: ["type", "code"], unique: true }],
2091
2122
  casl: { subject: "Master", permissions: masterCaslPermissions }
@@ -2094,6 +2125,10 @@ var init_definitions = __esm({
2094
2125
  });
2095
2126
 
2096
2127
  // src/modules/masters/registry.ts
2128
+ var registry_exports = {};
2129
+ __export(registry_exports, {
2130
+ createMasterRegistry: () => createMasterRegistry
2131
+ });
2097
2132
  function createMasterRegistry() {
2098
2133
  const registrations = [];
2099
2134
  return {
@@ -8772,15 +8807,18 @@ function toEntityDefinitionDTO(def, _engine, moduleName) {
8772
8807
  const meta = field["meta"];
8773
8808
  return meta?.["sortable"] === true && !field["hidden"];
8774
8809
  });
8810
+ const explicitGroupable = def["groupableFields"];
8775
8811
  const groupableInputTypes = ["select", "switch", "checkbox", "radio", "tags"];
8776
- const groupableFields = fieldEntries.filter(([, f]) => {
8812
+ const autoGroupableFields = fieldEntries.filter(([, f]) => {
8777
8813
  const field = f;
8778
8814
  const inputType = field["input"];
8779
8815
  return groupableInputTypes.includes(inputType ?? "") && !field["hidden"];
8780
8816
  });
8817
+ const resolvedGroupableFields = explicitGroupable ?? (autoGroupableFields.length > 0 ? autoGroupableFields.map(([name]) => name) : void 0);
8781
8818
  const entityType = def["type"] ?? "collection";
8782
8819
  const explicitDisplayMode = def["displayMode"];
8783
- 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");
8784
8822
  const entityIdent = def["table"] ?? def["key"];
8785
8823
  return {
8786
8824
  id: def._id,
@@ -8800,9 +8838,11 @@ function toEntityDefinitionDTO(def, _engine, moduleName) {
8800
8838
  displayMode: explicitDisplayMode,
8801
8839
  defaultDisplayMode,
8802
8840
  availableDisplayModes: (() => {
8841
+ const explicit = def["availableDisplayModes"];
8842
+ if (explicit?.length) return explicit;
8803
8843
  const modes = ["table", "list", "masonry"];
8804
8844
  if (["tree", "dag"].includes(entityType)) modes.push("tree");
8805
- if (groupableFields.length > 0) modes.push("board");
8845
+ if ((resolvedGroupableFields?.length ?? 0) > 0) modes.push("board");
8806
8846
  if (def["calendarFrom"]) modes.push("calendar");
8807
8847
  return modes;
8808
8848
  })(),
@@ -8812,10 +8852,8 @@ function toEntityDefinitionDTO(def, _engine, moduleName) {
8812
8852
  value: name,
8813
8853
  label: f["label"]
8814
8854
  })),
8815
- groupableFields: groupableFields.length > 0 ? groupableFields.map(([name, f]) => ({
8816
- value: name,
8817
- label: f["label"]
8818
- })) : void 0,
8855
+ groupableFields: resolvedGroupableFields,
8856
+ columnDragFields: def["columnDragFields"],
8819
8857
  groupBy: def["groupBy"],
8820
8858
  subgroupBy: def["subgroupBy"],
8821
8859
  calendarFrom: def["calendarFrom"],
@@ -8845,7 +8883,7 @@ function toModuleDTO(mod, ctx) {
8845
8883
  dependencies: mod.dependencies ?? [],
8846
8884
  routePrefix: mod.routePrefix ?? `/${mod.name}`,
8847
8885
  subjects: ctx.engine.getModuleSubjects(mod),
8848
- definitions: (mod.definitions ?? []).map(
8886
+ definitions: (mod.definitions ?? []).filter((def) => ("expose" in def ? def.expose : true) !== false).map(
8849
8887
  (def) => toEntityDefinitionDTO(def, ctx.engine, mod.name)
8850
8888
  ),
8851
8889
  actions: (mod.actions ?? []).length > 0 ? (mod.actions ?? []).filter((a) => !a.hidden).map((a) => toActionDTO(a)) : void 0,
@@ -8855,21 +8893,6 @@ function toModuleDTO(mod, ctx) {
8855
8893
  hasInit: !!mod.init
8856
8894
  };
8857
8895
  }
8858
- function getOrderedModulesViaContext(ctx) {
8859
- return ctx.engine.getModules();
8860
- }
8861
- async function runModuleSeedViaContext(mod, ctx) {
8862
- if (!mod.seed) {
8863
- return false;
8864
- }
8865
- try {
8866
- await mod.seed(ctx);
8867
- return true;
8868
- } catch (err) {
8869
- ctx.core.logger.error({ module: mod.name, err }, "Seed failed");
8870
- return false;
8871
- }
8872
- }
8873
8896
  var init_system_helpers = __esm({
8874
8897
  "src/modules/system/system.helpers.ts"() {
8875
8898
  "use strict";
@@ -9104,7 +9127,8 @@ function createSystemController(ctx) {
9104
9127
  const plugins = engine.getPlugins();
9105
9128
  const body = {
9106
9129
  version: manifest.version,
9107
- plugins: plugins.map((p) => p.code)
9130
+ plugins: plugins.map((p) => p.code),
9131
+ locales: ctx.locales
9108
9132
  };
9109
9133
  res.json(body);
9110
9134
  }
@@ -9991,7 +10015,6 @@ var SYSTEM_TABLES, factoryResetAction;
9991
10015
  var init_factory_reset_action = __esm({
9992
10016
  "src/modules/system/actions/factory-reset.action.ts"() {
9993
10017
  "use strict";
9994
- init_system_helpers();
9995
10018
  SYSTEM_TABLES = /* @__PURE__ */ new Set([
9996
10019
  "_nexus_migrations",
9997
10020
  "_nexus_migration_lock",
@@ -10034,8 +10057,8 @@ var init_factory_reset_action = __esm({
10034
10057
  source: "core:system",
10035
10058
  action: "factory_reset",
10036
10059
  actorId: authReq.user?.id,
10037
- ip: req.ip,
10038
- userAgent: req.headers["user-agent"]
10060
+ ip: req?.ip,
10061
+ userAgent: req?.headers["user-agent"]
10039
10062
  });
10040
10063
  await new Promise((resolve2) => setImmediate(resolve2));
10041
10064
  const allTables = await getAllTables(knex3);
@@ -10073,11 +10096,11 @@ var init_factory_reset_action = __esm({
10073
10096
  } catch {
10074
10097
  }
10075
10098
  ctx.core.logger.info({ tables: dataTables.length }, "All data tables cleared");
10076
- const modules = getOrderedModulesViaContext(ctx);
10099
+ const modules = ctx.engine.getModules();
10077
10100
  let modulesSeeded = 0;
10078
10101
  for (const mod of modules) {
10079
10102
  try {
10080
- const seeded = await runModuleSeedViaContext(mod, ctx);
10103
+ const seeded = await ctx.db.seedModule(mod);
10081
10104
  if (seeded) modulesSeeded++;
10082
10105
  } catch (err) {
10083
10106
  ctx.core.logger.error({ module: mod.name, err }, "Seed failed during factory reset");
@@ -10131,8 +10154,8 @@ var init_restart_server_action = __esm({
10131
10154
  source: "core:system",
10132
10155
  action: "server_restart",
10133
10156
  actorId: authReq.user?.id,
10134
- ip: req.ip,
10135
- userAgent: req.headers["user-agent"]
10157
+ ip: req?.ip,
10158
+ userAgent: req?.headers["user-agent"]
10136
10159
  });
10137
10160
  setTimeout(() => process.exit(0), 500);
10138
10161
  return { success: true, message: "Server is restarting..." };
@@ -11754,7 +11777,7 @@ function createUploadMiddleware(ctx, options) {
11754
11777
  const rateLimit2 = ctx.core.middleware.rateLimit({
11755
11778
  windowMs: 60 * 1e3,
11756
11779
  max: 20,
11757
- message: "Demasiados uploads, intenta en 1 minuto"
11780
+ message: "Too many uploads, try again in 1 minute"
11758
11781
  });
11759
11782
  const upload = multer({
11760
11783
  storage: multer.memoryStorage(),
@@ -12001,7 +12024,7 @@ function createStorageRoutes(ctx) {
12001
12024
  }
12002
12025
  res.status(201).json(results);
12003
12026
  };
12004
- 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" });
12005
12028
  if (auth) {
12006
12029
  router.post("/upload/multiple", uploadRateLimit, auth, upload.array("files", 10), uploadMultiple);
12007
12030
  } else {
@@ -12035,7 +12058,7 @@ function createStorageRoutes(ctx) {
12035
12058
  delete filesController.create;
12036
12059
  delete filesController.update;
12037
12060
  filesController.delete = async (req, res) => {
12038
- const id = req.params["id"];
12061
+ const id = String(req.params["id"] ?? "");
12039
12062
  if (!id) throw new ValidationError2("VALIDATION_ERROR");
12040
12063
  const storageService = getStorageService();
12041
12064
  const file = await storageService.getById(id);
@@ -12299,7 +12322,7 @@ var init_users_entity = __esm({
12299
12322
  ],
12300
12323
  nullable: true,
12301
12324
  meta: { sortable: true },
12302
- defaultValue: "es"
12325
+ defaultValue: "en"
12303
12326
  }),
12304
12327
  timezone: useSelectField9({
12305
12328
  label: { en: "Timezone", es: "Zona horaria" },
@@ -12377,7 +12400,7 @@ var init_users_entity = __esm({
12377
12400
  middleware: (ctx) => ctx.core.middleware.rateLimit({
12378
12401
  windowMs: 15 * 60 * 1e3,
12379
12402
  max: 5,
12380
- message: "Demasiados intentos, intenta en 15 minutos"
12403
+ message: "Too many attempts, try again in 15 minutes"
12381
12404
  }),
12382
12405
  handler: async (ctx, input) => {
12383
12406
  const {
@@ -12471,9 +12494,9 @@ var init_users_entity = __esm({
12471
12494
  labelField: "role_id",
12472
12495
  timestamps: true,
12473
12496
  order: 15,
12474
- routePrefix: "/user-roles",
12475
12497
  hidden: true,
12476
12498
  // Pivot table - managed via Users UI
12499
+ expose: false,
12477
12500
  fields: {
12478
12501
  id: useIdField4(),
12479
12502
  user_id: useSelectField9({
@@ -12526,7 +12549,7 @@ function createUsersRoutes(ctx) {
12526
12549
  if (originalUsersDelete) {
12527
12550
  usersController.delete = async (req, res) => {
12528
12551
  const authReq = req;
12529
- const id = req.params["id"] ?? "";
12552
+ const id = String(req.params["id"] ?? "");
12530
12553
  if (authReq.user?.id === id) {
12531
12554
  throw new ctx.core.errors.ForbiddenError("No puedes eliminarte a ti mismo");
12532
12555
  }
@@ -12601,7 +12624,7 @@ function createUsersRoutes(ctx) {
12601
12624
  "/:userId/roles",
12602
12625
  auth,
12603
12626
  async (req, res) => {
12604
- const roles = await usersService.getUserRoles(req.params["userId"]);
12627
+ const roles = await usersService.getUserRoles(String(req.params["userId"] ?? ""));
12605
12628
  res.json(roles);
12606
12629
  }
12607
12630
  );
@@ -12611,18 +12634,19 @@ function createUsersRoutes(ctx) {
12611
12634
  validate2({ body: z3.object({ roleId: z3.string().min(1) }) }),
12612
12635
  async (req, res) => {
12613
12636
  const { roleId } = req.body;
12614
- await usersService.assignRole(req.params["userId"], roleId);
12637
+ const userId = String(req.params["userId"] ?? "");
12638
+ await usersService.assignRole(userId, roleId);
12615
12639
  ctx.events.notify("audit.log", {
12616
12640
  source: "core:users",
12617
12641
  action: "role_assigned",
12618
12642
  actorId: req.user?.id,
12619
12643
  resourceType: "user",
12620
- resourceId: req.params["userId"],
12644
+ resourceId: userId,
12621
12645
  ip: req.ip,
12622
12646
  userAgent: req.headers["user-agent"],
12623
12647
  metadata: { roleId }
12624
12648
  });
12625
- const roles = await usersService.getUserRoles(req.params["userId"]);
12649
+ const roles = await usersService.getUserRoles(userId);
12626
12650
  res.json(roles);
12627
12651
  }
12628
12652
  );
@@ -12632,18 +12656,19 @@ function createUsersRoutes(ctx) {
12632
12656
  validate2({ body: z3.object({ roleIds: z3.array(z3.string()) }) }),
12633
12657
  async (req, res) => {
12634
12658
  const { roleIds } = req.body;
12635
- await usersService.setRoles(req.params["userId"], roleIds);
12659
+ const userId = String(req.params["userId"] ?? "");
12660
+ await usersService.setRoles(userId, roleIds);
12636
12661
  ctx.events.notify("audit.log", {
12637
12662
  source: "core:users",
12638
12663
  action: "roles_replaced",
12639
12664
  actorId: req.user?.id,
12640
12665
  resourceType: "user",
12641
- resourceId: req.params["userId"],
12666
+ resourceId: userId,
12642
12667
  ip: req.ip,
12643
12668
  userAgent: req.headers["user-agent"],
12644
12669
  metadata: { roleIds }
12645
12670
  });
12646
- const roles = await usersService.getUserRoles(req.params["userId"]);
12671
+ const roles = await usersService.getUserRoles(userId);
12647
12672
  res.json(roles);
12648
12673
  }
12649
12674
  );
@@ -12651,16 +12676,18 @@ function createUsersRoutes(ctx) {
12651
12676
  "/:userId/roles/:roleId",
12652
12677
  auth,
12653
12678
  async (req, res) => {
12654
- await usersService.removeRole(req.params["userId"], req.params["roleId"]);
12679
+ const userId = String(req.params["userId"] ?? "");
12680
+ const roleId = String(req.params["roleId"] ?? "");
12681
+ await usersService.removeRole(userId, roleId);
12655
12682
  ctx.events.notify("audit.log", {
12656
12683
  source: "core:users",
12657
12684
  action: "role_removed",
12658
12685
  actorId: req.user?.id,
12659
12686
  resourceType: "user",
12660
- resourceId: req.params["userId"],
12687
+ resourceId: userId,
12661
12688
  ip: req.ip,
12662
12689
  userAgent: req.headers["user-agent"],
12663
- metadata: { roleId: req.params["roleId"] }
12690
+ metadata: { roleId }
12664
12691
  });
12665
12692
  res.json({ success: true });
12666
12693
  }
@@ -13438,8 +13465,8 @@ var init_auth_entity = __esm({
13438
13465
  label: { en: "Refresh Token", es: "Token de refresco" },
13439
13466
  labelPlural: { en: "Refresh Tokens", es: "Tokens de refresco" },
13440
13467
  labelField: "id",
13441
- routePrefix: "/tokens",
13442
13468
  retention: { days: 7, expiresField: "expires_at" },
13469
+ expose: false,
13443
13470
  fields: {
13444
13471
  id: useIdField5(),
13445
13472
  token: useTextField9({
@@ -13504,8 +13531,8 @@ var init_auth_entity = __esm({
13504
13531
  labelField: "provider",
13505
13532
  timestamps: true,
13506
13533
  hidden: true,
13534
+ expose: false,
13507
13535
  order: 5,
13508
- routePrefix: "/identities",
13509
13536
  fields: {
13510
13537
  id: useIdField5(),
13511
13538
  user_id: useSelectField10({
@@ -16584,7 +16611,7 @@ var init_plugins_entity = __esm({
16584
16611
  label: "Plugins",
16585
16612
  icon: "mdi:puzzle",
16586
16613
  labelField: "code",
16587
- routePrefix: "/plugins",
16614
+ routePrefix: "/",
16588
16615
  defaultSort: { field: "name", order: "asc" },
16589
16616
  fields: {
16590
16617
  name: useTextField12({
@@ -16997,12 +17024,12 @@ var init_loader = __esm({
16997
17024
  "src/engine/loader.ts"() {
16998
17025
  "use strict";
16999
17026
  init_registry();
17000
- init_extractors();
17027
+ init_definition_extractors();
17001
17028
  init_modules();
17002
17029
  }
17003
17030
  });
17004
17031
 
17005
- // src/engine/subjectExtractor.ts
17032
+ // src/engine/subject-extractor.ts
17006
17033
  function getModuleSubjects(mod) {
17007
17034
  const subjects = /* @__PURE__ */ new Set();
17008
17035
  for (const def of mod.definitions ?? []) {
@@ -17011,10 +17038,10 @@ function getModuleSubjects(mod) {
17011
17038
  }
17012
17039
  return [...subjects];
17013
17040
  }
17014
- var init_subjectExtractor = __esm({
17015
- "src/engine/subjectExtractor.ts"() {
17041
+ var init_subject_extractor = __esm({
17042
+ "src/engine/subject-extractor.ts"() {
17016
17043
  "use strict";
17017
- init_extractors();
17044
+ init_definition_extractors();
17018
17045
  }
17019
17046
  });
17020
17047
 
@@ -17092,7 +17119,7 @@ function initSocketIO(httpServer, options) {
17092
17119
  maxHttpBufferSize: 1e6
17093
17120
  // 1MB - match Express json body limit
17094
17121
  });
17095
- logger.info({ maxHttpBufferSize: 1e6, cors: corsOrigin }, "Socket.IO initialized");
17122
+ logger.info({ cors: corsOrigin }, "Socket.IO initialized");
17096
17123
  io.use((socket, next) => {
17097
17124
  const token = socket.handshake.auth?.["token"] || socket.handshake.query?.["token"];
17098
17125
  if (token && typeof token === "string" && jwtSecret) {
@@ -17115,7 +17142,6 @@ function initSocketIO(httpServer, options) {
17115
17142
  logger.warn({ code: err.code, message: err.message }, "Socket.IO connection error");
17116
17143
  });
17117
17144
  io.on("connection", handleConnection);
17118
- logger.info("Socket.IO initialized");
17119
17145
  nexusEvents.emitEvent("socket.initialized");
17120
17146
  return io;
17121
17147
  }
@@ -17710,6 +17736,12 @@ var init_app_error = __esm({
17710
17736
 
17711
17737
  // src/core/abilities/ability.factory.ts
17712
17738
  import { AbilityBuilder, createMongoAbility } from "@casl/ability";
17739
+ function setSeedPermissions(perms) {
17740
+ seedPermissions = perms;
17741
+ }
17742
+ function clearSeedPermissions() {
17743
+ seedPermissions = null;
17744
+ }
17713
17745
  function setCustomCaslRules(fn) {
17714
17746
  customCaslRules = fn;
17715
17747
  }
@@ -17767,6 +17799,17 @@ async function defineAbilityFor(user, roleNames) {
17767
17799
  if (customCaslRules) {
17768
17800
  await customCaslRules(user, { can, cannot });
17769
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
+ }
17770
17813
  return build();
17771
17814
  }
17772
17815
  function packRules(ability) {
@@ -17775,11 +17818,12 @@ function packRules(ability) {
17775
17818
  function unpackRules(rules) {
17776
17819
  return createMongoAbility(rules);
17777
17820
  }
17778
- var customCaslRules, entityDefinitionsRegistry, SUPERUSER_ROLES;
17821
+ var customCaslRules, seedPermissions, entityDefinitionsRegistry, SUPERUSER_ROLES;
17779
17822
  var init_ability_factory = __esm({
17780
17823
  "src/core/abilities/ability.factory.ts"() {
17781
17824
  "use strict";
17782
17825
  init_logger();
17826
+ seedPermissions = null;
17783
17827
  entityDefinitionsRegistry = null;
17784
17828
  SUPERUSER_ROLES = ["ADMIN", "OWNER"];
17785
17829
  }
@@ -17863,7 +17907,7 @@ var init_validate_middleware = __esm({
17863
17907
  import { z as z8 } from "zod";
17864
17908
  function resolveConfig() {
17865
17909
  env = envSchema.parse(process.env);
17866
- process.env.TZ = env.TZ;
17910
+ process.env["TZ"] = env.TZ;
17867
17911
  resolvedConfig = {
17868
17912
  nodeEnv: env.NODE_ENV,
17869
17913
  port: env.PORT,
@@ -17918,7 +17962,7 @@ var init_env = __esm({
17918
17962
  FRPC_SUBDOMAIN: z8.string().optional()
17919
17963
  });
17920
17964
  env = envSchema.parse(process.env);
17921
- process.env.TZ = env.TZ;
17965
+ process.env["TZ"] = env.TZ;
17922
17966
  resolvedConfig = null;
17923
17967
  }
17924
17968
  });
@@ -18345,7 +18389,7 @@ var init_sequence = __esm({
18345
18389
  }
18346
18390
  });
18347
18391
 
18348
- // src/core/utils/error-handler.ts
18392
+ // src/core/utils/safe-json.ts
18349
18393
  function safeJsonParse(logger2, jsonString, fallback, context) {
18350
18394
  try {
18351
18395
  return JSON.parse(jsonString);
@@ -18354,8 +18398,8 @@ function safeJsonParse(logger2, jsonString, fallback, context) {
18354
18398
  return fallback;
18355
18399
  }
18356
18400
  }
18357
- var init_error_handler = __esm({
18358
- "src/core/utils/error-handler.ts"() {
18401
+ var init_safe_json = __esm({
18402
+ "src/core/utils/safe-json.ts"() {
18359
18403
  "use strict";
18360
18404
  }
18361
18405
  });
@@ -18364,14 +18408,15 @@ var init_error_handler = __esm({
18364
18408
  import express from "express";
18365
18409
  import { resolve, join as join9 } from "path";
18366
18410
  import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
18367
- function createServeSPA(app) {
18368
- return (endpoint, distPath, options = {}) => {
18411
+ function createServeSPA(app, httpServer) {
18412
+ return async (endpoint, distPath, options = {}) => {
18369
18413
  const {
18370
18414
  maxAge = "1d",
18371
18415
  etag = true,
18372
18416
  immutable = false,
18373
18417
  index = "index.html",
18374
- absolute = false
18418
+ absolute = false,
18419
+ viteSrc
18375
18420
  } = options;
18376
18421
  if (endpoint === "/api" || endpoint.startsWith("/api/")) {
18377
18422
  logger.error(`Cannot mount SPA on ${endpoint} - reserved for API routes`);
@@ -18382,58 +18427,117 @@ function createServeSPA(app) {
18382
18427
  return;
18383
18428
  }
18384
18429
  registeredEndpoints.add(endpoint);
18385
- let resolvedPath;
18386
- if (absolute) {
18387
- resolvedPath = distPath;
18388
- } else {
18389
- const projectPath2 = resolve(getProjectPath(), distPath);
18390
- if (existsSync9(projectPath2)) {
18391
- 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");
18392
18434
  } else {
18393
- resolvedPath = resolve(getLibPath(), distPath);
18435
+ const mounted = await mountViteDevMiddleware(app, endpoint, srcPath, httpServer);
18436
+ if (mounted) return;
18394
18437
  }
18395
18438
  }
18396
- if (!existsSync9(resolvedPath)) {
18397
- logger.warn({ endpoint, distPath, hint: "Build the frontend first" }, `SPA directory not found: ${resolvedPath}`);
18398
- 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);
18399
18469
  }
18400
- const indexPath = join9(resolvedPath, index);
18401
- if (!existsSync9(indexPath)) {
18402
- logger.warn({ endpoint, index }, `Index file not found: ${indexPath}`);
18403
- }
18404
- app.use(endpoint, express.static(resolvedPath, { maxAge, etag, immutable }));
18405
- let injectedHtml = "";
18406
- if (existsSync9(indexPath)) {
18407
- const rawHtml = readFileSync5(indexPath, "utf-8");
18408
- const apiUrl = env.BACKEND_URL ? `${env.BACKEND_URL}/api/v1` : "/api/v1";
18409
- const nexusConfig = JSON.stringify({ apiUrl });
18410
- injectedHtml = rawHtml.replace(
18411
- "</head>",
18412
- `<script>window.__NEXUS__=${nexusConfig}</script>
18413
- </head>`
18414
- );
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;
18415
18476
  }
18416
- const fallbackHandler = (_req, res) => {
18417
- if (!injectedHtml) {
18418
- res.status(404).send("index.html not found");
18419
- return;
18420
- }
18421
- res.set("Cache-Control", "no-cache, no-store, must-revalidate");
18422
- res.type("html").send(injectedHtml);
18423
- };
18424
- if (endpoint === "/") {
18425
- 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;
18426
18490
  } else {
18427
- app.get(endpoint, fallbackHandler);
18428
- 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;
18429
18518
  }
18430
- 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);
18431
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}`);
18432
18529
  }
18433
- 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;
18434
18538
  registeredEndpoints.clear();
18435
18539
  }
18436
- var registeredEndpoints;
18540
+ var registeredEndpoints, viteServers;
18437
18541
  var init_spa_handler = __esm({
18438
18542
  "src/core/spa-handler.ts"() {
18439
18543
  "use strict";
@@ -18441,6 +18545,7 @@ var init_spa_handler = __esm({
18441
18545
  init_logger();
18442
18546
  init_env();
18443
18547
  registeredEndpoints = /* @__PURE__ */ new Set();
18548
+ viteServers = [];
18444
18549
  }
18445
18550
  });
18446
18551
 
@@ -19011,7 +19116,7 @@ var init_core = __esm({
19011
19116
  init_id();
19012
19117
  init_sequence();
19013
19118
  init_paths();
19014
- init_error_handler();
19119
+ init_safe_json();
19015
19120
  init_spa_handler();
19016
19121
  init_cache();
19017
19122
  init_jwt();
@@ -19726,7 +19831,9 @@ var init_base_service = __esm({
19726
19831
  const countResult = await qb.clone().count("* as count").first();
19727
19832
  const total = Number(countResult?.count ?? 0);
19728
19833
  qb = this.applySortingWithDefaults(qb, query);
19729
- qb = qb.limit(limit).offset(offset);
19834
+ if (limit > 0) {
19835
+ qb = qb.limit(limit).offset(offset);
19836
+ }
19730
19837
  const rawItems = await qb;
19731
19838
  const items = this.parseJsonFieldsFromArray(rawItems);
19732
19839
  const processedItems = await this.afterFindAll(items);
@@ -19811,7 +19918,7 @@ var init_base_service = __esm({
19811
19918
  });
19812
19919
  }
19813
19920
  const total = result.length;
19814
- const paginatedItems = result.slice(offset, offset + limit);
19921
+ const paginatedItems = limit === 0 ? result : result.slice(offset, offset + limit);
19815
19922
  return this.buildPaginatedResult(paginatedItems, total, page, limit);
19816
19923
  }
19817
19924
  /**
@@ -20066,14 +20173,14 @@ var init_base_service = __esm({
20066
20173
  * Build paginated result from items and count
20067
20174
  */
20068
20175
  buildPaginatedResult(items, total, page, limit) {
20069
- const totalPages = Math.ceil(total / limit);
20176
+ const totalPages = limit === 0 ? 1 : Math.ceil(total / limit);
20070
20177
  return {
20071
20178
  items,
20072
20179
  total,
20073
- page,
20074
- limit,
20180
+ page: limit === 0 ? 1 : page,
20181
+ limit: limit === 0 ? total : limit,
20075
20182
  totalPages,
20076
- hasNext: page < totalPages
20183
+ hasNext: limit === 0 ? false : page < totalPages
20077
20184
  };
20078
20185
  }
20079
20186
  /**
@@ -20082,8 +20189,8 @@ var init_base_service = __esm({
20082
20189
  getPagination(query) {
20083
20190
  const maxLimit = query?.maxLimit ?? 100;
20084
20191
  const page = Math.max(1, query?.page ?? 1);
20085
- const limit = Math.min(maxLimit, Math.max(1, query?.limit ?? 20));
20086
- 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;
20087
20194
  return { page, limit, offset };
20088
20195
  }
20089
20196
  /**
@@ -21730,7 +21837,8 @@ function createEntityController(service, definition, ctx) {
21730
21837
  async list(req, res) {
21731
21838
  checkPermission(req, "read");
21732
21839
  const page = Math.max(1, parseInt(req.query["page"]) || 1);
21733
- 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));
21734
21842
  let filters;
21735
21843
  if (req.query["filters"]) {
21736
21844
  filters = parseFilters(req.query["filters"], ctx.core.errors);
@@ -21773,7 +21881,7 @@ function createEntityController(service, definition, ctx) {
21773
21881
  * Get single entity
21774
21882
  */
21775
21883
  async get(req, res) {
21776
- const id = req.params["id"] ?? "";
21884
+ const id = String(req.params["id"] ?? "");
21777
21885
  const entity = await service.findById(id);
21778
21886
  if (!entity) {
21779
21887
  throw new ctx.core.errors.NotFoundError(`${resolveLocalized6(definition.label, "en")} not found`);
@@ -21824,7 +21932,7 @@ function createEntityController(service, definition, ctx) {
21824
21932
  if (!service.update) {
21825
21933
  throw new ctx.core.errors.ForbiddenError(`${resolveLocalized6(definition.label, "en")} does not support update`);
21826
21934
  }
21827
- const id = req.params["id"] ?? "";
21935
+ const id = String(req.params["id"] ?? "");
21828
21936
  const existing = await service.findById(id);
21829
21937
  if (!existing) {
21830
21938
  throw new ctx.core.errors.NotFoundError(`${resolveLocalized6(definition.label, "en")} not found`);
@@ -21856,7 +21964,7 @@ function createEntityController(service, definition, ctx) {
21856
21964
  if (!service.delete) {
21857
21965
  throw new ctx.core.errors.ForbiddenError(`${resolveLocalized6(definition.label, "en")} does not support delete`);
21858
21966
  }
21859
- const id = req.params["id"] ?? "";
21967
+ const id = String(req.params["id"] ?? "");
21860
21968
  const existing = await service.findById(id);
21861
21969
  if (!existing) {
21862
21970
  throw new ctx.core.errors.NotFoundError(`${resolveLocalized6(definition.label, "en")} not found`);
@@ -22055,7 +22163,7 @@ function createActionHandler(action, definition, ctx, scope = "row") {
22055
22163
  const caslSubject = (action.casl && "subject" in action.casl ? action.casl.subject : void 0) ?? definition.casl?.subject ?? capitalizeFirst(entityName ?? "Entity");
22056
22164
  const hasCasl = !action.skipAuth && (!!definition.casl || !!action.casl);
22057
22165
  return async (req, res) => {
22058
- const recordId = scope === "row" ? req.params["id"] : void 0;
22166
+ const recordId = scope === "row" ? String(req.params["id"] ?? "") : void 0;
22059
22167
  const authReq = req;
22060
22168
  if (hasCasl && !authReq.ability) {
22061
22169
  ctx.core.logger.warn({ action: "execute", subject: caslSubject, actionKey: action.key, reqId: req.requestId, decision: "deny", reason: "unauthenticated" }, "authz:deny");
@@ -22129,6 +22237,8 @@ var init_entity_controller = __esm({
22129
22237
  // src/runtime/routes/entity.routes.ts
22130
22238
  function createEntityRouter(controller, definition, ctx, service) {
22131
22239
  const router = ctx.createRouter();
22240
+ const expose = "expose" in definition ? definition.expose : true;
22241
+ if (expose === false) return router;
22132
22242
  const type2 = definition.type ?? "collection";
22133
22243
  const isSingleton = (type2 === "single" || type2 === "config") && !("scopeField" in definition && definition.scopeField);
22134
22244
  const entityDef = definition;
@@ -22223,44 +22333,44 @@ function createEntityRouter(controller, definition, ctx, service) {
22223
22333
  res.json(await treeSvc.findRoots());
22224
22334
  }));
22225
22335
  router.post("/:id/move", ...entityMiddleware, ...authMiddleware, asyncHandler(async (req, res) => {
22226
- const id = req.params["id"] ?? "";
22336
+ const id = String(req.params["id"] ?? "");
22227
22337
  const { parentId } = req.body;
22228
22338
  res.json(await treeSvc.move(id, parentId ?? null));
22229
22339
  }));
22230
22340
  router.get("/:id/ancestors", ...entityMiddleware, ...authMiddleware, asyncHandler(async (req, res) => {
22231
- const id = req.params["id"] ?? "";
22341
+ const id = String(req.params["id"] ?? "");
22232
22342
  res.json(await treeSvc.getAncestors(id));
22233
22343
  }));
22234
22344
  router.get("/:id/descendants", ...entityMiddleware, ...authMiddleware, asyncHandler(async (req, res) => {
22235
- const id = req.params["id"] ?? "";
22345
+ const id = String(req.params["id"] ?? "");
22236
22346
  res.json(await treeSvc.getDescendants(id));
22237
22347
  }));
22238
22348
  router.get("/:id/children", ...entityMiddleware, ...authMiddleware, asyncHandler(async (req, res) => {
22239
- const id = req.params["id"] ?? "";
22349
+ const id = String(req.params["id"] ?? "");
22240
22350
  res.json(await treeSvc.findChildren(id));
22241
22351
  }));
22242
22352
  }
22243
22353
  if (type2 === "dag" && service) {
22244
22354
  const dagSvc = service;
22245
22355
  router.get("/:id/parents", ...entityMiddleware, ...authMiddleware, asyncHandler(async (req, res) => {
22246
- const id = req.params["id"] ?? "";
22356
+ const id = String(req.params["id"] ?? "");
22247
22357
  res.json(await dagSvc.getParents(id));
22248
22358
  }));
22249
22359
  router.put("/:id/parents", ...entityMiddleware, ...authMiddleware, asyncHandler(async (req, res) => {
22250
- const id = req.params["id"] ?? "";
22360
+ const id = String(req.params["id"] ?? "");
22251
22361
  const { parentIds } = req.body;
22252
22362
  await dagSvc.setParents(id, parentIds);
22253
22363
  res.status(204).send();
22254
22364
  }));
22255
22365
  router.post("/:id/parents", ...entityMiddleware, ...authMiddleware, asyncHandler(async (req, res) => {
22256
- const id = req.params["id"] ?? "";
22366
+ const id = String(req.params["id"] ?? "");
22257
22367
  const { parentId } = req.body;
22258
22368
  await dagSvc.addParent(id, parentId);
22259
22369
  res.status(204).send();
22260
22370
  }));
22261
22371
  router.delete("/:id/parents/:parentId", ...entityMiddleware, ...authMiddleware, asyncHandler(async (req, res) => {
22262
- const id = req.params["id"] ?? "";
22263
- const parentId = req.params["parentId"] ?? "";
22372
+ const id = String(req.params["id"] ?? "");
22373
+ const parentId = String(req.params["parentId"] ?? "");
22264
22374
  await dagSvc.removeParent(id, parentId);
22265
22375
  res.status(204).send();
22266
22376
  }));
@@ -23209,17 +23319,18 @@ async function createModuleRouters(ctx, definitions, modulePrefix) {
23209
23319
  const runtime = await createEntityRuntimeAsync(ctx, definition);
23210
23320
  const route = inferEntityRoutePath(definition);
23211
23321
  const entityLabel = resolveLocalized7(definition.label, "en");
23212
- if (modulePrefix && route === modulePrefix) {
23322
+ const isExposed = !("expose" in definition && definition.expose === false);
23323
+ if (isExposed && modulePrefix && route === modulePrefix) {
23213
23324
  ctx.core.logger.warn(
23214
23325
  `Entity "${entityLabel}" inferred route "${route}" duplicates module prefix \u2014 add routePrefix to the entity definition to fix`
23215
23326
  );
23216
23327
  }
23217
- if (routeMap.has(route)) {
23328
+ if (isExposed && routeMap.has(route)) {
23218
23329
  ctx.core.logger.warn(
23219
23330
  `Entity "${entityLabel}" route "${route}" collides with "${routeMap.get(route)}" in the same module`
23220
23331
  );
23221
23332
  }
23222
- routeMap.set(route, entityLabel);
23333
+ if (isExposed) routeMap.set(route, entityLabel);
23223
23334
  router.use(route, runtime.router);
23224
23335
  const key = getServiceKey(definition);
23225
23336
  if (ctx.services.has(key)) {
@@ -23435,7 +23546,7 @@ var init_runtime = __esm({
23435
23546
  }
23436
23547
  });
23437
23548
 
23438
- // src/db/module-runner.ts
23549
+ // src/db/seed-runner.ts
23439
23550
  import { existsSync as existsSync10 } from "fs";
23440
23551
  import { join as join10 } from "path";
23441
23552
  import { pathToFileURL } from "url";
@@ -23503,8 +23614,8 @@ function hasSeedData(seed5) {
23503
23614
  if (!Array.isArray(seed5) && "source" in seed5 && seed5.source === "url") return true;
23504
23615
  return Array.isArray(seed5) && seed5.length > 0;
23505
23616
  }
23506
- var init_module_runner = __esm({
23507
- "src/db/module-runner.ts"() {
23617
+ var init_seed_runner = __esm({
23618
+ "src/db/seed-runner.ts"() {
23508
23619
  "use strict";
23509
23620
  init_runtime();
23510
23621
  init_paths();
@@ -23607,7 +23718,7 @@ var init_migration_sources = __esm({
23607
23718
  init_shared();
23608
23719
  init_paths();
23609
23720
  init_plugin_ops();
23610
- init_store();
23721
+ init_module_store();
23611
23722
  }
23612
23723
  });
23613
23724
 
@@ -23940,7 +24051,7 @@ async function runMigrations(knexInstance, sources) {
23940
24051
  const executedMigrations = await knex3("_nexus_migrations").where({ status: "completed" }).select("name").then((rows) => new Set(rows.map((r) => r.name)));
23941
24052
  const pendingMigrations = migrationFiles.filter((m2) => !executedMigrations.has(m2.name));
23942
24053
  if (pendingMigrations.length === 0) {
23943
- logger.info("No pending migrations");
24054
+ logger.debug("No pending migrations");
23944
24055
  return;
23945
24056
  }
23946
24057
  const batch = await getNextBatch(knex3);
@@ -24340,6 +24451,27 @@ var init_migration_helpers = __esm({
24340
24451
  import path2 from "path";
24341
24452
  import fs2 from "fs/promises";
24342
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
+ }
24343
24475
  async function detectSchemaDrift(knexInstance) {
24344
24476
  const knex3 = knexInstance ?? getDb();
24345
24477
  const entities = getAllPersistentEntities();
@@ -24447,6 +24579,7 @@ function computeSchemaDiff(entities, currentSchema, options) {
24447
24579
  for (const idx of entityIndexes) {
24448
24580
  const key = normalizeKey(idx.columns, !!idx.unique);
24449
24581
  if (!currentKeys.has(key)) {
24582
+ warnIfIndexExceedsMySQLLimit(tableName, idx.columns, !!idx.unique, entity.fields);
24450
24583
  diff.newIndexes.push({ columns: idx.columns, unique: !!idx.unique });
24451
24584
  }
24452
24585
  }
@@ -24618,7 +24751,7 @@ function formatDriftMessage(drift) {
24618
24751
  lines.push('Run "pnpm migrate:dev" to generate and apply migrations.');
24619
24752
  return lines.join("\n");
24620
24753
  }
24621
- var PERSISTENT_TYPES;
24754
+ var MYSQL_MAX_INDEX_BYTES, MYSQL_BYTES_PER_CHAR, PERSISTENT_TYPES;
24622
24755
  var init_migration_generator = __esm({
24623
24756
  "src/db/migration-generator.ts"() {
24624
24757
  "use strict";
@@ -24627,9 +24760,11 @@ var init_migration_generator = __esm({
24627
24760
  init_paths();
24628
24761
  init_schema_reader();
24629
24762
  init_engine();
24630
- init_queries();
24763
+ init_module_queries();
24631
24764
  init_migration_helpers();
24632
- init_store();
24765
+ init_module_store();
24766
+ MYSQL_MAX_INDEX_BYTES = 3072;
24767
+ MYSQL_BYTES_PER_CHAR = 4;
24633
24768
  PERSISTENT_TYPES = /* @__PURE__ */ new Set([
24634
24769
  "collection",
24635
24770
  "tree",
@@ -24683,6 +24818,13 @@ async function runAllGeneratedMigrations(ctx, modules) {
24683
24818
  table.string("created_by", 26).nullable();
24684
24819
  table.string("updated_by", 26).nullable();
24685
24820
  }
24821
+ const hasSoftDelete = entityDef.softDelete;
24822
+ if (hasSoftDelete) {
24823
+ table.timestamp("deleted_at").nullable();
24824
+ }
24825
+ if (entityDef.type === "tree") {
24826
+ table.string("parent_id", 26).nullable().references(`${tableName}.id`).onDelete("SET NULL");
24827
+ }
24686
24828
  });
24687
24829
  const entityIndexes = entityDef.indexes;
24688
24830
  if (entityIndexes?.length) {
@@ -24799,7 +24941,16 @@ function addColumn(table, fieldName, field, db2) {
24799
24941
  column.unique();
24800
24942
  }
24801
24943
  if (dbConfig.default !== void 0) {
24802
- column.defaultTo(dbConfig.default);
24944
+ const isJsonColumn = dbConfig.type === "json" || dbConfig.type === "array";
24945
+ if (isJsonColumn && typeof dbConfig.default === "string") {
24946
+ try {
24947
+ column.defaultTo(JSON.parse(dbConfig.default));
24948
+ } catch {
24949
+ column.defaultTo(dbConfig.default);
24950
+ }
24951
+ } else {
24952
+ column.defaultTo(dbConfig.default);
24953
+ }
24803
24954
  } else if (dbConfig.defaultFn === "now") {
24804
24955
  column.defaultTo(db2.fn.now());
24805
24956
  }
@@ -26187,7 +26338,7 @@ var init_db = __esm({
26187
26338
  init_schema_helpers();
26188
26339
  init_sql_utils();
26189
26340
  init_sqlite_compat();
26190
- init_module_runner();
26341
+ init_seed_runner();
26191
26342
  init_ensure_system_tables();
26192
26343
  init_migration_sources();
26193
26344
  init_migration_runner();
@@ -26277,12 +26428,20 @@ async function discoverPlugins(projectPath2) {
26277
26428
  manifest.image = imagePath;
26278
26429
  }
26279
26430
  }
26431
+ if (!manifest.llms) {
26432
+ const llmsPath = join12(projectPath2, "node_modules", pkgName, "llms.txt");
26433
+ if (existsSync11(llmsPath)) {
26434
+ manifest.llms = readFileSync7(llmsPath, "utf-8");
26435
+ }
26436
+ }
26280
26437
  discovered.push(manifest);
26281
26438
  } else {
26282
26439
  logger.warn({ plugin: pkgName }, "Plugin discovery: no PluginManifest export, skipping");
26283
26440
  }
26284
26441
  } catch (err) {
26285
- logger.warn({ plugin: pkgName, err: err.message }, "Plugin discovery: failed to import");
26442
+ const errorMsg = err.message;
26443
+ logger.warn({ plugin: pkgName, err: errorMsg }, "Plugin discovery: failed to import");
26444
+ console.error(` \u26A0 Plugin '${pkgName}' failed to load: ${errorMsg}`);
26286
26445
  }
26287
26446
  }
26288
26447
  return discovered;
@@ -26350,7 +26509,9 @@ async function discoverModules(projectPath2) {
26350
26509
  discovered.push(...manifests);
26351
26510
  }
26352
26511
  } catch (err) {
26353
- logger.warn({ module: entry, err: err.message }, "Module discovery: failed to import");
26512
+ const errorMsg = err.message;
26513
+ logger.warn({ module: entry, err: errorMsg }, "Module discovery: failed to import");
26514
+ console.error(` \u26A0 Module '${entry}' failed to load: ${errorMsg}`);
26354
26515
  }
26355
26516
  }
26356
26517
  return discovered;
@@ -26441,8 +26602,11 @@ var init_events_api = __esm({
26441
26602
 
26442
26603
  // src/engine/context.ts
26443
26604
  import { ForbiddenError as CASLForbiddenError3, subject } from "@casl/ability";
26444
- 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";
26445
26606
  import { Redis as Redis2 } from "ioredis";
26607
+ function setLocales(locales) {
26608
+ platformLocales = locales;
26609
+ }
26446
26610
  function getSharedCacheManager() {
26447
26611
  if (!sharedCacheManager) {
26448
26612
  const redisUrl = env.REDIS_URL;
@@ -26518,7 +26682,7 @@ function createModuleContext() {
26518
26682
  const defaultAdapter = createKnexAdapter(knex3);
26519
26683
  const defaultSchemaAdapter = createKnexSchemaAdapter(knex3);
26520
26684
  adaptersRegistry["temp"] = { data: getSharedTempAdapter() };
26521
- 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)");
26522
26686
  const middleware = {
26523
26687
  validate,
26524
26688
  rateLimit: createRateLimit,
@@ -26626,7 +26790,9 @@ function createModuleContext() {
26626
26790
  throw new Error(`Knex connection for adapter "${adapter}" not found. Available: ${Object.keys(knexConnections).join(", ")}`);
26627
26791
  }
26628
26792
  return conn;
26629
- }
26793
+ },
26794
+ // Placeholder — bound after ctx construction (needs full ModuleContext)
26795
+ seedModule: null
26630
26796
  };
26631
26797
  const configContext = {
26632
26798
  env,
@@ -26719,7 +26885,8 @@ function createModuleContext() {
26719
26885
  adapters: adaptersContext,
26720
26886
  // Root-level shortcuts for frequently used utilities
26721
26887
  events: createEventsApi(nexusEvents, logger),
26722
- createRouter: () => createRouter()
26888
+ createRouter: () => createRouter(),
26889
+ locales: platformLocales
26723
26890
  };
26724
26891
  servicesRegistry["cacheManager"] = getSharedCacheManager();
26725
26892
  ctx.runtime = {
@@ -26728,9 +26895,10 @@ function createModuleContext() {
26728
26895
  createEntityController: (service, def) => createEntityController(service, def, ctx),
26729
26896
  createEntityRouter: (controller, def) => createEntityRouter(controller, def, ctx)
26730
26897
  };
26898
+ ctx.db.seedModule = (mod) => runModuleSeed(mod, ctx);
26731
26899
  return ctx;
26732
26900
  }
26733
- var sharedCacheManager, sharedTempAdapter;
26901
+ var platformLocales, sharedCacheManager, sharedTempAdapter;
26734
26902
  var init_context = __esm({
26735
26903
  "src/engine/context.ts"() {
26736
26904
  "use strict";
@@ -26743,7 +26911,9 @@ var init_context = __esm({
26743
26911
  init_plugin_ops();
26744
26912
  init_load_config();
26745
26913
  init_events_api();
26914
+ init_seed_runner();
26746
26915
  init_cache_manager();
26916
+ platformLocales = DEFAULT_LOCALES;
26747
26917
  sharedCacheManager = null;
26748
26918
  sharedTempAdapter = null;
26749
26919
  }
@@ -26754,11 +26924,11 @@ var init_engine = __esm({
26754
26924
  "src/engine/index.ts"() {
26755
26925
  "use strict";
26756
26926
  init_registry();
26757
- init_queries();
26927
+ init_module_queries();
26758
26928
  init_loader();
26759
- init_store();
26760
- init_subjectExtractor();
26761
- init_extractors();
26929
+ init_module_store();
26930
+ init_subject_extractor();
26931
+ init_definition_extractors();
26762
26932
  init_context();
26763
26933
  }
26764
26934
  });
@@ -27300,7 +27470,7 @@ async function createApp(options = {}) {
27300
27470
  // Only accept arrays and objects
27301
27471
  }));
27302
27472
  app.use(cookieParser());
27303
- const serveSPA = createServeSPA(app);
27473
+ const serveSPA = createServeSPA(app, options.httpServer);
27304
27474
  if (options.beforeRoutes) {
27305
27475
  const result = options.beforeRoutes(app, serveSPA);
27306
27476
  if (result instanceof Promise) {
@@ -27394,11 +27564,11 @@ async function createApp(options = {}) {
27394
27564
  });
27395
27565
  const sortedSpas = [...servedSpas].sort((a, b) => b.endpoint.length - a.endpoint.length);
27396
27566
  for (const spa of sortedSpas) {
27397
- serveSPA(spa.endpoint, spa.path, spa);
27567
+ await serveSPA(spa.endpoint, spa.path, { ...spa, viteSrc: spa.viteSrc });
27398
27568
  }
27399
27569
  const { ui } = getConfig();
27400
27570
  if (ui.enabled) {
27401
- serveSPA(ui.base, ui.path);
27571
+ await serveSPA(ui.base, ui.path, { viteSrc: "../ui" });
27402
27572
  }
27403
27573
  app.use(errorMiddleware);
27404
27574
  return app;
@@ -27424,58 +27594,32 @@ var init_app = __esm({
27424
27594
  }
27425
27595
  });
27426
27596
 
27427
- // src/core/utils/net.ts
27597
+ // src/core/utils/port-check.ts
27428
27598
  import net from "net";
27429
27599
  import { execSync } from "child_process";
27430
27600
  function findProcessOnPort(port) {
27431
27601
  try {
27432
27602
  const output = execSync(`lsof -ti :${port} 2>/dev/null`, { encoding: "utf-8" });
27433
27603
  const pid = parseInt(output.trim().split("\n")[0] ?? "", 10);
27434
- 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 };
27435
27611
  } catch {
27436
27612
  return null;
27437
27613
  }
27438
27614
  }
27439
- function isSameProcessGroup(pid) {
27440
- if (pid === process.pid || pid === process.ppid) return true;
27441
- try {
27442
- const ourPgid = execSync(`ps -o pgid= -p ${process.pid} 2>/dev/null`, { encoding: "utf-8" }).trim();
27443
- const targetPgid = execSync(`ps -o pgid= -p ${pid} 2>/dev/null`, { encoding: "utf-8" }).trim();
27444
- return ourPgid === targetPgid;
27445
- } catch {
27446
- return false;
27447
- }
27448
- }
27449
- function killProcessOnPort(port) {
27450
- const pid = findProcessOnPort(port);
27451
- if (!pid) return false;
27452
- if (isSameProcessGroup(pid)) {
27453
- logger.warn({ port, pid }, `Skipping same process group (PID ${pid}) on port ${port}`);
27454
- return false;
27455
- }
27456
- try {
27457
- process.kill(pid, "SIGTERM");
27458
- logger.warn({ port, pid }, `Killed process ${pid} on port ${port}`);
27459
- return true;
27460
- } catch {
27461
- return false;
27462
- }
27463
- }
27464
27615
  async function checkPortAvailable(port, host = "0.0.0.0") {
27465
27616
  return new Promise((resolve2, reject) => {
27466
27617
  const server2 = net.createServer();
27467
27618
  server2.once("error", (err) => {
27468
27619
  if (err.code === "EADDRINUSE") {
27469
- if (process.env["NODE_ENV"] !== "production") {
27470
- if (killProcessOnPort(port)) {
27471
- setTimeout(() => {
27472
- checkPortAvailable(port, host).then(resolve2).catch(reject);
27473
- }, 500);
27474
- return;
27475
- }
27476
- }
27477
- logger.error({ port }, `Port ${port} is already in use`);
27478
- 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));
27479
27623
  } else {
27480
27624
  reject(err);
27481
27625
  }
@@ -27486,10 +27630,9 @@ async function checkPortAvailable(port, host = "0.0.0.0") {
27486
27630
  server2.listen(port, host);
27487
27631
  });
27488
27632
  }
27489
- var init_net = __esm({
27490
- "src/core/utils/net.ts"() {
27633
+ var init_port_check = __esm({
27634
+ "src/core/utils/port-check.ts"() {
27491
27635
  "use strict";
27492
- init_logger();
27493
27636
  }
27494
27637
  });
27495
27638
 
@@ -27584,6 +27727,150 @@ var init_tunnel = __esm({
27584
27727
  }
27585
27728
  });
27586
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
+
27587
27874
  // src/instrumentation.ts
27588
27875
  var instrumentation_exports = {};
27589
27876
  __export(instrumentation_exports, {
@@ -27648,7 +27935,9 @@ var init_instrumentation = __esm({
27648
27935
  });
27649
27936
 
27650
27937
  // src/core/server.ts
27938
+ import http from "http";
27651
27939
  import { entityRoom as entityRoom6 } from "@gzl10/nexus-sdk";
27940
+ import { DEFAULT_LOCALES as DEFAULT_LOCALES2 } from "@gzl10/nexus-sdk";
27652
27941
  async function runMigrationsAndSeeds(config3) {
27653
27942
  initLoggerService(getLoggerConfig());
27654
27943
  setLoggerInstance(getPinoLogger());
@@ -27701,7 +27990,7 @@ async function runMigrationsAndSeeds(config3) {
27701
27990
  const sources = buildMigrationSources();
27702
27991
  const migrationFiles = await loadAllMigrationFiles(sources);
27703
27992
  if (migrationFiles.length > 0) {
27704
- logger.info({ count: migrationFiles.length }, "Deploying pending migrations...");
27993
+ logger.info({ sources: sources.length, files: migrationFiles.length }, "Running migration deploy...");
27705
27994
  try {
27706
27995
  await runMigrations(void 0, sources);
27707
27996
  } catch (err) {
@@ -27710,7 +27999,7 @@ async function runMigrationsAndSeeds(config3) {
27710
27999
  throw err;
27711
28000
  }
27712
28001
  }
27713
- logger.info("Checking schema drift...");
28002
+ logger.debug("Checking schema drift...");
27714
28003
  const drift = await detectSchemaDrift();
27715
28004
  if (drift && (drift.newTables.length > 0 || drift.alteredTables.length > 0)) {
27716
28005
  const message = formatDriftMessage(drift);
@@ -27757,7 +28046,7 @@ ${dirs}`);
27757
28046
  }
27758
28047
  const allDefinitions = modules.flatMap((m2) => m2.definitions ?? []);
27759
28048
  await createMemoryTables(allDefinitions);
27760
- logger.info("Running seeds...");
28049
+ logger.debug("Running seeds...");
27761
28050
  for (const mod of modules) {
27762
28051
  try {
27763
28052
  await runModuleSeed(mod, ctx);
@@ -27765,12 +28054,39 @@ ${dirs}`);
27765
28054
  logger.error({ module: mod.name, err }, "Seed failed - continuing with next module");
27766
28055
  }
27767
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
+ }
27768
28083
  }
27769
28084
  async function start(config3) {
27770
28085
  if (server) {
27771
28086
  throw new Error("Server already running. Call stop() first.");
27772
28087
  }
27773
28088
  currentConfig = config3;
28089
+ setLocales(config3?.locales ?? DEFAULT_LOCALES2);
27774
28090
  if (env.NODE_ENV === "development" && env.FRPC_SERVER && env.FRPC_SUBDOMAIN && !env.BACKEND_URL) {
27775
28091
  process.env["BACKEND_URL"] = getTunnelUrl(env.FRPC_SUBDOMAIN, env.FRPC_SERVER);
27776
28092
  }
@@ -27795,13 +28111,17 @@ async function start(config3) {
27795
28111
  await initTelemetry2();
27796
28112
  await runMigrationsAndSeeds(config3);
27797
28113
  const effectiveCorsOrigins = buildEffectiveCorsOrigins(env.CORS_ORIGIN, config3?.spas);
28114
+ const httpServer = http.createServer();
27798
28115
  const app = await createApp({
27799
28116
  beforeRoutes: config3?.beforeRoutes,
27800
28117
  afterRoutes: config3?.afterRoutes,
27801
- spas: config3?.spas
28118
+ spas: config3?.spas,
28119
+ httpServer
27802
28120
  });
28121
+ httpServer.on("request", app);
27803
28122
  return new Promise((resolve2) => {
27804
- server = app.listen(resolved.port, resolved.host, async () => {
28123
+ server = httpServer;
28124
+ httpServer.listen(resolved.port, resolved.host, async () => {
27805
28125
  const timeoutMs = parseInt(process.env["REQUEST_TIMEOUT_MS"] || "30000", 10);
27806
28126
  if (timeoutMs > 0) {
27807
28127
  server.setTimeout(timeoutMs);
@@ -27837,12 +28157,10 @@ async function start(config3) {
27837
28157
  });
27838
28158
  }
27839
28159
  const baseUrl = env.BACKEND_URL || `http://localhost:${actualPort}`;
27840
- logger.info({ libPath: getLibPath(), projectPath: getProjectPath() }, "Paths");
27841
- logger.info(`API: ${baseUrl}/api/v1`);
27842
- if (resolved.ui.enabled) {
27843
- logger.info(`UI: ${baseUrl}`);
27844
- }
27845
- 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");
27846
28164
  nexusEvents.emitEvent("server.started", { port: actualPort, host: resolved.host });
27847
28165
  if (config3?.onReady) {
27848
28166
  try {
@@ -27868,7 +28186,8 @@ async function stop() {
27868
28186
  await resetSharedAdapters();
27869
28187
  resetConfigCache();
27870
28188
  clearCustomCaslRules();
27871
- resetServeSPAEndpoints();
28189
+ clearSeedPermissions();
28190
+ await resetServeSPA();
27872
28191
  return;
27873
28192
  }
27874
28193
  if (currentConfig?.beforeClose) {
@@ -27902,7 +28221,8 @@ async function stop() {
27902
28221
  await resetSharedAdapters();
27903
28222
  resetConfigCache();
27904
28223
  clearCustomCaslRules();
27905
- resetServeSPAEndpoints();
28224
+ clearSeedPermissions();
28225
+ await resetServeSPA();
27906
28226
  currentConfig = void 0;
27907
28227
  server = null;
27908
28228
  nexusEvents.emitEvent("server.stopped");
@@ -27962,7 +28282,7 @@ var init_server = __esm({
27962
28282
  init_cors();
27963
28283
  init_logger();
27964
28284
  init_error_middleware();
27965
- init_net();
28285
+ init_port_check();
27966
28286
  init_engine();
27967
28287
  init_context();
27968
28288
  init_db();