@gzl10/nexus-backend 0.13.0 → 0.13.1

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/cli.js CHANGED
@@ -6403,7 +6403,9 @@ function getAuthConfig() {
6403
6403
  // convert to ms
6404
6404
  cookieDomain: authEnv.AUTH_COOKIE_DOMAIN,
6405
6405
  challengeThreshold: authEnv.AUTH_CHALLENGE_THRESHOLD,
6406
- skipRegisterOtp: authEnv.AUTH_SKIP_REGISTER_OTP
6406
+ skipRegisterOtp: authEnv.AUTH_SKIP_REGISTER_OTP,
6407
+ disableRegistration: authEnv.AUTH_DISABLE_REGISTRATION,
6408
+ disableAutoCreate: authEnv.AUTH_DISABLE_AUTO_CREATE
6407
6409
  };
6408
6410
  }
6409
6411
  var authEnvSchema, _authEnv;
@@ -6424,7 +6426,11 @@ var init_auth_config = __esm({
6424
6426
  // Challenge threshold: failed attempts before requiring OTP (default: 2)
6425
6427
  AUTH_CHALLENGE_THRESHOLD: z5.coerce.number().default(2),
6426
6428
  // Skip OTP verification for registration (DEVELOPMENT ONLY - rejected in production)
6427
- AUTH_SKIP_REGISTER_OTP: z5.coerce.boolean().default(false)
6429
+ AUTH_SKIP_REGISTER_OTP: z5.coerce.boolean().default(false),
6430
+ // Disable self-registration via POST /auth/register (default: false)
6431
+ AUTH_DISABLE_REGISTRATION: z5.coerce.boolean().default(false),
6432
+ // Disable auto-creation of users on first OIDC login (default: false)
6433
+ AUTH_DISABLE_AUTO_CREATE: z5.coerce.boolean().default(false)
6428
6434
  });
6429
6435
  }
6430
6436
  });
@@ -6544,6 +6550,7 @@ var providersAction;
6544
6550
  var init_providers_action = __esm({
6545
6551
  "src/modules/auth/actions/providers.action.ts"() {
6546
6552
  "use strict";
6553
+ init_auth_config();
6547
6554
  providersAction = {
6548
6555
  type: "action",
6549
6556
  key: "providers",
@@ -6563,7 +6570,12 @@ var init_providers_action = __esm({
6563
6570
  }
6564
6571
  })
6565
6572
  );
6566
- return results.filter((info) => info !== null);
6573
+ const providers = results.filter((info) => info !== null);
6574
+ const config3 = getAuthConfig();
6575
+ return {
6576
+ providers,
6577
+ registrationEnabled: !config3.disableRegistration
6578
+ };
6567
6579
  }
6568
6580
  };
6569
6581
  }
@@ -6666,6 +6678,7 @@ var registerAction;
6666
6678
  var init_register_action = __esm({
6667
6679
  "src/modules/auth/actions/register.action.ts"() {
6668
6680
  "use strict";
6681
+ init_auth_config();
6669
6682
  init_helpers2();
6670
6683
  registerAction = {
6671
6684
  type: "action",
@@ -6685,6 +6698,9 @@ var init_register_action = __esm({
6685
6698
  deviceName: { input: "text", label: { en: "Device Name", es: "Nombre del dispositivo" }, validation: { max: 100 } }
6686
6699
  },
6687
6700
  handler: async (ctx, input, req, res) => {
6701
+ if (getAuthConfig().disableRegistration) {
6702
+ throw new ctx.core.errors.ForbiddenError("AUTH_REGISTRATION_DISABLED", "Registration is disabled");
6703
+ }
6688
6704
  const body = input;
6689
6705
  const authService = ctx.services.get("auth");
6690
6706
  const result = await authService.register(body, getRequestInfo(req, { deviceId: body.deviceId, deviceName: body.deviceName }));
@@ -7229,7 +7245,7 @@ function createAuthService(ctx) {
7229
7245
  const { errors, abilities, crypto: crypto2 } = ctx.core;
7230
7246
  const { generateId: generateId4 } = ctx.core;
7231
7247
  const { nowTimestamp: nowTimestamp2 } = ctx.db;
7232
- const { verifyPassword: verifyPassword2, DUMMY_HASH: _DUMMY_HASH } = crypto2;
7248
+ const { verifyPassword: verifyPassword2, DUMMY_HASH: DUMMY_HASH2 } = crypto2;
7233
7249
  const events = ctx.core.events;
7234
7250
  const { defineAbilityFor: defineAbilityFor2, packRules: packRules2 } = abilities;
7235
7251
  const ErrorCodes2 = errors.codes;
@@ -7291,6 +7307,7 @@ function createAuthService(ctx) {
7291
7307
  const ipKey = `login_attempts:ip:${requestInfo?.ip ?? "unknown"}`;
7292
7308
  const user = await db2(USERS3).where({ email }).first();
7293
7309
  if (!user) {
7310
+ await verifyPassword2(password, DUMMY_HASH2);
7294
7311
  events.emitEvent("auth.failed", { email, reason: "user_not_found" });
7295
7312
  await persistAuditEvent("failed", { email, requestInfo, details: { reason: "user_not_found" } });
7296
7313
  throw new errors.UnauthorizedError(ErrorCodes2["AUTH_INVALID_CREDENTIALS"], "Invalid credentials");
@@ -7411,7 +7428,7 @@ function createAuthService(ctx) {
7411
7428
  };
7412
7429
  },
7413
7430
  async refresh(refreshToken, requestInfo) {
7414
- const result = await db2.transaction(async (trx) => {
7431
+ const validated = await db2.transaction(async (trx) => {
7415
7432
  let query = trx(REFRESH_TOKENS).where({ token: refreshToken });
7416
7433
  const client = db2.client.config.client;
7417
7434
  if (!client.includes("sqlite")) {
@@ -7431,27 +7448,29 @@ function createAuthService(ctx) {
7431
7448
  }
7432
7449
  return { user, storedToken };
7433
7450
  });
7434
- const roleIds = await usersService.getRoleIds(result.user.id);
7435
- const roleNames = await usersService.getRoleNames(result.user.id);
7451
+ const roleIds = await usersService.getRoleIds(validated.user.id);
7452
+ const roleNames = await usersService.getRoleNames(validated.user.id);
7436
7453
  const tokens = generateTokenPair({
7437
- userId: result.user.id,
7438
- email: result.user.email,
7454
+ userId: validated.user.id,
7455
+ email: validated.user.email,
7439
7456
  roleIds,
7440
7457
  roleNames
7441
7458
  });
7442
- await db2(REFRESH_TOKENS).where({ id: result.storedToken.id }).delete();
7443
- await db2(REFRESH_TOKENS).insert({
7444
- id: generateId4(),
7445
- token: tokens.refreshToken,
7446
- user_id: result.user.id,
7447
- expires_at: getRefreshTokenExpiration(),
7448
- device_id: result.storedToken.device_id ?? null,
7449
- device_name: result.storedToken.device_name ?? null
7459
+ await db2.transaction(async (trx) => {
7460
+ await trx(REFRESH_TOKENS).where({ id: validated.storedToken.id }).delete();
7461
+ await trx(REFRESH_TOKENS).insert({
7462
+ id: generateId4(),
7463
+ token: tokens.refreshToken,
7464
+ user_id: validated.user.id,
7465
+ expires_at: getRefreshTokenExpiration(),
7466
+ device_id: validated.storedToken.device_id ?? null,
7467
+ device_name: validated.storedToken.device_name ?? null
7468
+ });
7450
7469
  });
7451
- const ability = await defineAbilityFor2(result.user, roleNames);
7452
- events.emitEvent("auth.refresh", { userId: result.user.id });
7470
+ const ability = await defineAbilityFor2(validated.user, roleNames);
7471
+ events.emitEvent("auth.refresh", { userId: validated.user.id });
7453
7472
  await persistAuditEvent("refresh", {
7454
- userId: result.user.id,
7473
+ userId: validated.user.id,
7455
7474
  requestInfo
7456
7475
  });
7457
7476
  return {
@@ -7691,6 +7710,9 @@ function createAuthService(ctx) {
7691
7710
  },
7692
7711
  /** Create a new user without password (for auth plugins using external providers) */
7693
7712
  async createUser(data) {
7713
+ if (getAuthConfig().disableAutoCreate) {
7714
+ throw new errors.ForbiddenError("AUTH_AUTO_CREATE_DISABLED", "Auto-creation of users is disabled");
7715
+ }
7694
7716
  const user = await usersService.create({
7695
7717
  email: data.email,
7696
7718
  password: null,
@@ -9140,6 +9162,7 @@ var init_notifications = __esm({
9140
9162
  });
9141
9163
 
9142
9164
  // src/modules/schedules/schedules.executor.ts
9165
+ import { resolve as dnsResolve } from "dns/promises";
9143
9166
  function registerFunction(key, fn) {
9144
9167
  functionRegistry.set(key, fn);
9145
9168
  }
@@ -9149,6 +9172,62 @@ function unregisterFunction(key) {
9149
9172
  function listRegisteredFunctions() {
9150
9173
  return Array.from(functionRegistry.keys());
9151
9174
  }
9175
+ function assertSafeInternalPath(path3) {
9176
+ if (!path3.startsWith("/")) {
9177
+ throw new Error(`Invalid internal path: must start with /`);
9178
+ }
9179
+ if (path3.includes("..") || path3.includes("//")) {
9180
+ throw new Error(`Invalid internal path: path traversal detected`);
9181
+ }
9182
+ if (!SAFE_PATH_RE.test(path3)) {
9183
+ throw new Error(`Invalid internal path: contains unsafe characters`);
9184
+ }
9185
+ }
9186
+ function isPrivateIP(ip) {
9187
+ if (/^127\./.test(ip)) return true;
9188
+ if (/^10\./.test(ip)) return true;
9189
+ if (/^172\.(1[6-9]|2\d|3[01])\./.test(ip)) return true;
9190
+ if (/^192\.168\./.test(ip)) return true;
9191
+ if (/^169\.254\./.test(ip)) return true;
9192
+ if (/^0\./.test(ip)) return true;
9193
+ if (ip === "255.255.255.255") return true;
9194
+ if (ip === "::1" || ip === "::") return true;
9195
+ if (/^fe80:/i.test(ip)) return true;
9196
+ if (/^f[cd]/i.test(ip)) return true;
9197
+ return false;
9198
+ }
9199
+ async function assertSafeExternalUrl(urlStr) {
9200
+ let parsed;
9201
+ try {
9202
+ parsed = new URL(urlStr);
9203
+ } catch {
9204
+ throw new Error(`Invalid URL: ${urlStr}`);
9205
+ }
9206
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
9207
+ throw new Error(`Invalid URL protocol: ${parsed.protocol} (only http/https allowed)`);
9208
+ }
9209
+ const hostname2 = parsed.hostname;
9210
+ if (hostname2 === "169.254.169.254" || hostname2 === "metadata.google.internal") {
9211
+ throw new Error("Access to cloud metadata endpoints is blocked");
9212
+ }
9213
+ const lowerHost = hostname2.toLowerCase();
9214
+ if (lowerHost === "localhost" || lowerHost === "ip6-localhost" || lowerHost === "ip6-loopback") {
9215
+ throw new Error("Access to localhost is blocked");
9216
+ }
9217
+ if (isPrivateIP(hostname2)) {
9218
+ throw new Error(`Access to private IP ${hostname2} is blocked`);
9219
+ }
9220
+ try {
9221
+ const addresses = await dnsResolve(hostname2);
9222
+ for (const addr of addresses) {
9223
+ if (isPrivateIP(addr)) {
9224
+ throw new Error(`URL resolves to private IP ${addr} \u2014 blocked to prevent SSRF`);
9225
+ }
9226
+ }
9227
+ } catch (err) {
9228
+ if (err instanceof Error && err.message.includes("blocked")) throw err;
9229
+ }
9230
+ }
9152
9231
  async function executeServiceAction(ctx, config3) {
9153
9232
  const { service, action, input, recordId } = config3;
9154
9233
  const targetService = ctx.services.get(service);
@@ -9160,15 +9239,21 @@ async function executeServiceAction(ctx, config3) {
9160
9239
  }
9161
9240
  async function executeHttpInternal(_ctx, config3) {
9162
9241
  const { method, path: path3, body, headers } = config3;
9242
+ assertSafeInternalPath(path3);
9163
9243
  const port = process.env["PORT"] ?? 3e3;
9164
9244
  const baseUrl = `http://127.0.0.1:${port}`;
9165
9245
  const url = `${baseUrl}${path3}`;
9246
+ const reqHeaders = {
9247
+ "Content-Type": "application/json",
9248
+ ...headers
9249
+ };
9250
+ const internalToken = process.env["SCHEDULE_INTERNAL_TOKEN"];
9251
+ if (internalToken && !reqHeaders["Authorization"]) {
9252
+ reqHeaders["Authorization"] = `Bearer ${internalToken}`;
9253
+ }
9166
9254
  const response = await fetch(url, {
9167
9255
  method,
9168
- headers: {
9169
- "Content-Type": "application/json",
9170
- ...headers
9171
- },
9256
+ headers: reqHeaders,
9172
9257
  body: body ? JSON.stringify(body) : void 0
9173
9258
  });
9174
9259
  if (!response.ok) {
@@ -9183,6 +9268,7 @@ async function executeHttpInternal(_ctx, config3) {
9183
9268
  }
9184
9269
  async function executeHttpExternal(config3) {
9185
9270
  const { method, url, body, headers, timeout = 3e4 } = config3;
9271
+ await assertSafeExternalUrl(url);
9186
9272
  const controller = new AbortController();
9187
9273
  const timeoutId = setTimeout(() => controller.abort(), timeout);
9188
9274
  try {
@@ -9249,11 +9335,12 @@ async function executeTarget(ctx, schedule) {
9249
9335
  };
9250
9336
  }
9251
9337
  }
9252
- var functionRegistry;
9338
+ var functionRegistry, SAFE_PATH_RE;
9253
9339
  var init_schedules_executor = __esm({
9254
9340
  "src/modules/schedules/schedules.executor.ts"() {
9255
9341
  "use strict";
9256
9342
  functionRegistry = /* @__PURE__ */ new Map();
9343
+ SAFE_PATH_RE = /^\/[a-zA-Z0-9/_\-.~%]+$/;
9257
9344
  }
9258
9345
  });
9259
9346
 
@@ -11675,6 +11762,11 @@ var init_filter_helpers = __esm({
11675
11762
  });
11676
11763
 
11677
11764
  // src/modules/charts/charts.service.ts
11765
+ function assertSafeIdentifier(name, label) {
11766
+ if (!SAFE_IDENTIFIER_RE.test(name)) {
11767
+ throw new Error(`Invalid ${label}: "${name}" is not a valid column name`);
11768
+ }
11769
+ }
11678
11770
  function createChartService(ctx) {
11679
11771
  const { db: db2 } = ctx;
11680
11772
  async function getChartData(request) {
@@ -11688,18 +11780,24 @@ function createChartService(ctx) {
11688
11780
  limit = 100,
11689
11781
  sort = "asc"
11690
11782
  } = request;
11783
+ if (!VALID_AGGREGATIONS.has(aggregation)) {
11784
+ throw new Error(`Invalid aggregation: "${aggregation}"`);
11785
+ }
11786
+ assertSafeIdentifier(xAxis, "xAxis");
11787
+ assertSafeIdentifier(yAxis, "yAxis");
11788
+ if (groupBy) assertSafeIdentifier(groupBy, "groupBy");
11789
+ const aggFn = AGGREGATION_SQL[aggregation];
11691
11790
  const knex2 = db2.knex;
11692
11791
  let query = knex2(entity);
11693
11792
  if (Object.keys(filters).length > 0) {
11694
11793
  const { applyFilters: applyFilters2 } = await Promise.resolve().then(() => (init_filter_helpers(), filter_helpers_exports));
11695
11794
  query = applyFilters2(query, filters);
11696
11795
  }
11796
+ const clampedLimit = Math.min(Math.max(limit, 1), 5e3);
11697
11797
  if (groupBy) {
11698
- const aggFn = getAggregationFunction(aggregation);
11699
- query = query.select(xAxis).select(groupBy).select(knex2.raw(`${aggFn}(${yAxis}) as value`)).groupBy(xAxis, groupBy).orderBy(xAxis, sort).limit(limit);
11798
+ query = query.select(xAxis, groupBy).select(knex2.raw(`${aggFn}(??) as ??`, [yAxis, "value"])).groupBy(xAxis, groupBy).orderBy(xAxis, sort).limit(clampedLimit);
11700
11799
  } else {
11701
- const aggFn = getAggregationFunction(aggregation);
11702
- query = query.select(xAxis).select(knex2.raw(`${aggFn}(${yAxis}) as value`)).groupBy(xAxis).orderBy(xAxis, sort).limit(limit);
11800
+ query = query.select(xAxis).select(knex2.raw(`${aggFn}(??) as ??`, [yAxis, "value"])).groupBy(xAxis).orderBy(xAxis, sort).limit(clampedLimit);
11703
11801
  }
11704
11802
  const rows = await query;
11705
11803
  const data = rows.map((row) => ({
@@ -11716,7 +11814,11 @@ function createChartService(ctx) {
11716
11814
  };
11717
11815
  }
11718
11816
  async function getRawData(entity, fields, filters = {}, limit = 1e3) {
11719
- let query = db2.knex(entity).select(fields).limit(limit);
11817
+ for (const field of fields) {
11818
+ assertSafeIdentifier(field, "field");
11819
+ }
11820
+ const knex2 = db2.knex;
11821
+ let query = knex2(entity).select(fields).limit(Math.min(Math.max(limit, 1), 5e3));
11720
11822
  if (Object.keys(filters).length > 0) {
11721
11823
  const { applyFilters: applyFilters2 } = await Promise.resolve().then(() => (init_filter_helpers(), filter_helpers_exports));
11722
11824
  query = applyFilters2(query, filters);
@@ -11728,25 +11830,19 @@ function createChartService(ctx) {
11728
11830
  getRawData
11729
11831
  };
11730
11832
  }
11731
- function getAggregationFunction(type2) {
11732
- switch (type2) {
11733
- case "sum":
11734
- return "SUM";
11735
- case "avg":
11736
- return "AVG";
11737
- case "count":
11738
- return "COUNT";
11739
- case "min":
11740
- return "MIN";
11741
- case "max":
11742
- return "MAX";
11743
- default:
11744
- return "SUM";
11745
- }
11746
- }
11833
+ var VALID_AGGREGATIONS, AGGREGATION_SQL, SAFE_IDENTIFIER_RE;
11747
11834
  var init_charts_service = __esm({
11748
11835
  "src/modules/charts/charts.service.ts"() {
11749
11836
  "use strict";
11837
+ VALID_AGGREGATIONS = /* @__PURE__ */ new Set(["sum", "avg", "count", "min", "max"]);
11838
+ AGGREGATION_SQL = {
11839
+ sum: "SUM",
11840
+ avg: "AVG",
11841
+ count: "COUNT",
11842
+ min: "MIN",
11843
+ max: "MAX"
11844
+ };
11845
+ SAFE_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]{0,63}$/;
11750
11846
  }
11751
11847
  });
11752
11848
 
@@ -16609,6 +16705,8 @@ var init_error_codes = __esm({
16609
16705
  AUTH_SESSION_NOT_FOUND: "AUTH_SESSION_NOT_FOUND",
16610
16706
  AUTH_SESSION_SELF_REVOKE: "AUTH_SESSION_SELF_REVOKE",
16611
16707
  AUTH_VERIFICATION_CODE_INVALID: "AUTH_VERIFICATION_CODE_INVALID",
16708
+ AUTH_REGISTRATION_DISABLED: "AUTH_REGISTRATION_DISABLED",
16709
+ AUTH_AUTO_CREATE_DISABLED: "AUTH_AUTO_CREATE_DISABLED",
16612
16710
  // User
16613
16711
  USER_NOT_FOUND: "USER_NOT_FOUND",
16614
16712
  USER_EMAIL_EXISTS: "USER_EMAIL_EXISTS",
@@ -17917,7 +18015,8 @@ async function generateMigrationForSource(name, scope = "all", targetDir, prefix
17917
18015
  return "";
17918
18016
  }
17919
18017
  const currentSchema = await readDatabaseSchema(knex2);
17920
- const changes = computeSchemaDiff(allEntities, currentSchema);
18018
+ const ownedTables = collectOwnedTables(allEntities);
18019
+ const changes = computeSchemaDiff(allEntities, currentSchema, { ownedTables });
17921
18020
  if (isEmptyDiff(changes)) {
17922
18021
  logger.info("No schema changes detected");
17923
18022
  console.log("\n\u2705 No schema changes detected\n");
@@ -17989,24 +18088,25 @@ async function devMigration(name, scope) {
17989
18088
  if (effectiveScope !== "project") {
17990
18089
  logger.info({ scope: effectiveScope }, "Auto-detected plugin scope");
17991
18090
  }
17992
- const filepath = await generateMigrationForSource(migrationName, effectiveScope);
17993
18091
  const { runMigrations: runMigrations2, loadAllMigrationFiles: loadAllMigrationFiles2 } = await Promise.resolve().then(() => (init_migration_runner(), migration_runner_exports));
17994
18092
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_connection(), connection_exports));
17995
18093
  const db2 = getDb2();
17996
18094
  const migrationFiles = await loadAllMigrationFiles2();
17997
18095
  const executed = await db2("_nexus_migrations").where({ status: "completed" }).select("name").then((rows) => new Set(rows.map((r) => r.name)));
17998
- const pending = migrationFiles.filter((m) => !executed.has(m.name));
17999
- if (pending.length > 0) {
18000
- if (!filepath) {
18001
- console.log(`
18002
- No schema changes for scope "${effectiveScope}", but ${pending.length} pending migration(s) found.
18096
+ const pendingBefore = migrationFiles.filter((m) => !executed.has(m.name));
18097
+ if (pendingBefore.length > 0) {
18098
+ console.log(`
18099
+ Applying ${pendingBefore.length} pending migration(s)...
18003
18100
  `);
18004
- } else {
18005
- console.log("Applying migration...\n");
18006
- }
18101
+ await runMigrations2();
18102
+ console.log("\u2705 Pending migrations applied\n");
18103
+ }
18104
+ const filepath = await generateMigrationForSource(migrationName, effectiveScope);
18105
+ if (filepath) {
18106
+ console.log("Applying new migration...\n");
18007
18107
  await runMigrations2();
18008
18108
  console.log("\n\u2705 Migration applied successfully\n");
18009
- } else if (!filepath) {
18109
+ } else if (pendingBefore.length === 0) {
18010
18110
  logger.info({ scope: effectiveScope }, "No changes detected and no pending migrations");
18011
18111
  }
18012
18112
  }
@@ -18017,7 +18117,8 @@ async function detectSchemaDrift(knexInstance) {
18017
18117
  return null;
18018
18118
  }
18019
18119
  const currentSchema = await readDatabaseSchema(knex2);
18020
- const diff = computeSchemaDiff(entities, currentSchema);
18120
+ const ownedTables = collectOwnedTables(entities);
18121
+ const diff = computeSchemaDiff(entities, currentSchema, { ownedTables });
18021
18122
  return isEmptyDiff(diff) ? null : diff;
18022
18123
  }
18023
18124
  function getAllPersistentEntities() {
@@ -18051,13 +18152,24 @@ function extractEntitiesFromModules(modules) {
18051
18152
  }
18052
18153
  return entities;
18053
18154
  }
18054
- function computeSchemaDiff(entities, currentSchema) {
18155
+ function collectOwnedTables(entities) {
18156
+ const tables = /* @__PURE__ */ new Set();
18157
+ for (const entity of entities) {
18158
+ const tableName = entity.table;
18159
+ if (tableName) tables.add(tableName);
18160
+ if (entity.type === "dag") {
18161
+ const parentsTable = entity.parentsTable ?? `${tableName}_parents`;
18162
+ tables.add(parentsTable);
18163
+ }
18164
+ }
18165
+ return tables;
18166
+ }
18167
+ function computeSchemaDiff(entities, currentSchema, options) {
18055
18168
  const changes = {
18056
18169
  newTables: [],
18057
18170
  droppedTables: [],
18058
18171
  alteredTables: []
18059
18172
  };
18060
- const _entityMap = new Map(entities.map((e) => [e.table, e]));
18061
18173
  for (const entity of entities) {
18062
18174
  const tableName = entity.table;
18063
18175
  const currentTable = currentSchema.tables.get(tableName);
@@ -18072,16 +18184,18 @@ function computeSchemaDiff(entities, currentSchema) {
18072
18184
  }
18073
18185
  }
18074
18186
  }
18075
- const entityTables = new Set(entities.map((e) => e.table));
18076
- for (const entity of entities) {
18077
- if (entity.type === "dag") {
18078
- const parentsTable = entity.parentsTable ?? `${entity.table}_parents`;
18079
- entityTables.add(parentsTable);
18187
+ if (options?.ownedTables && options.ownedTables.size > 0) {
18188
+ const entityTables = new Set(entities.map((e) => e.table));
18189
+ for (const entity of entities) {
18190
+ if (entity.type === "dag") {
18191
+ const parentsTable = entity.parentsTable ?? `${entity.table}_parents`;
18192
+ entityTables.add(parentsTable);
18193
+ }
18080
18194
  }
18081
- }
18082
- for (const [tableName] of currentSchema.tables) {
18083
- if (!entityTables.has(tableName) && !isSystemTable(tableName)) {
18084
- changes.droppedTables.push(tableName);
18195
+ for (const tableName of options.ownedTables) {
18196
+ if (!entityTables.has(tableName) && currentSchema.tables.has(tableName) && !isSystemTable(tableName)) {
18197
+ changes.droppedTables.push(tableName);
18198
+ }
18085
18199
  }
18086
18200
  }
18087
18201
  return changes;
@@ -18149,7 +18263,8 @@ function compareTable(entity, currentTable) {
18149
18263
  const systemFields = /* @__PURE__ */ new Set(["id", "created_at", "updated_at", "created_by", "updated_by", "deleted_at", "parent_id"]);
18150
18264
  for (const columnName of currentColumnNames) {
18151
18265
  if (!expectedColumns.has(columnName) && !systemFields.has(columnName)) {
18152
- result.droppedColumns.push(columnName);
18266
+ const col = currentTable.columns.get(columnName);
18267
+ result.droppedColumns.push({ name: columnName, type: col.type });
18153
18268
  }
18154
18269
  }
18155
18270
  return result;
@@ -18178,6 +18293,39 @@ function normalizeColumnType(type2) {
18178
18293
  };
18179
18294
  return typeMap[normalized] || normalized;
18180
18295
  }
18296
+ function mapDbTypeToKnex(dbType, colName) {
18297
+ const name = colName ? `'${colName}'` : `'column'`;
18298
+ const upper = dbType.replace(/\(.*\)/, "").trim().toUpperCase();
18299
+ const sizeMatch = dbType.match(/\((\d+)\)/);
18300
+ const size = sizeMatch ? sizeMatch[1] : null;
18301
+ const mapping = {
18302
+ "VARCHAR": size ? `table.string(${name}, ${size}).nullable()` : `table.string(${name}).nullable()`,
18303
+ "CHARACTER VARYING": size ? `table.string(${name}, ${size}).nullable()` : `table.string(${name}).nullable()`,
18304
+ "CHAR": size ? `table.string(${name}, ${size}).nullable()` : `table.string(${name}).nullable()`,
18305
+ "TEXT": `table.text(${name}).nullable()`,
18306
+ "INT": `table.integer(${name}).nullable()`,
18307
+ "INTEGER": `table.integer(${name}).nullable()`,
18308
+ "BIGINT": `table.bigInteger(${name}).nullable()`,
18309
+ "FLOAT": `table.float(${name}).nullable()`,
18310
+ "DOUBLE": `table.float(${name}).nullable()`,
18311
+ "DOUBLE PRECISION": `table.float(${name}).nullable()`,
18312
+ "DECIMAL": `table.decimal(${name}).nullable()`,
18313
+ "NUMERIC": `table.decimal(${name}).nullable()`,
18314
+ "BOOLEAN": `table.boolean(${name}).nullable()`,
18315
+ "BOOL": `table.boolean(${name}).nullable()`,
18316
+ "TINYINT": `table.boolean(${name}).nullable()`,
18317
+ "TIMESTAMP": `table.timestamp(${name}).nullable()`,
18318
+ "DATETIME": `table.timestamp(${name}).nullable()`,
18319
+ "TIMESTAMP WITHOUT TIME ZONE": `table.timestamp(${name}).nullable()`,
18320
+ "TIMESTAMP WITH TIME ZONE": `table.timestamp(${name}).nullable()`,
18321
+ "DATE": `table.date(${name}).nullable()`,
18322
+ "TIME": `table.time(${name}).nullable()`,
18323
+ "JSON": `table.json(${name}).nullable()`,
18324
+ "JSONB": `table.jsonb(${name}).nullable()`,
18325
+ "UUID": `table.uuid(${name}).nullable()`
18326
+ };
18327
+ return { code: mapping[upper] || `table.specificType(${name}, '${dbType}').nullable()` };
18328
+ }
18181
18329
  function isEmptyDiff(diff) {
18182
18330
  return diff.newTables.length === 0 && diff.droppedTables.length === 0 && diff.alteredTables.length === 0;
18183
18331
  }
@@ -18292,8 +18440,8 @@ function generateUpCode(changes) {
18292
18440
  lines.push(` // Modify ${col.name}: ${col.oldType} \u2192 ${col.newType}`);
18293
18441
  lines.push(` table.dropColumn('${col.name}')`);
18294
18442
  }
18295
- for (const colName of alteration.droppedColumns) {
18296
- lines.push(` table.dropColumn('${colName}')`);
18443
+ for (const col of alteration.droppedColumns) {
18444
+ lines.push(` table.dropColumn('${col.name}')`);
18297
18445
  }
18298
18446
  lines.push(` })`);
18299
18447
  lines.push("");
@@ -18316,9 +18464,9 @@ function generateDownCode(changes) {
18316
18464
  const hasChanges = alteration.newColumns.length > 0 || alteration.droppedColumns.length > 0 || alteration.modifiedColumns.length > 0;
18317
18465
  if (!hasChanges) continue;
18318
18466
  lines.push(` await knex.schema.alterTable('${alteration.table}', (table) => {`);
18319
- for (const colName of alteration.droppedColumns) {
18320
- lines.push(` // NOTE: Cannot recreate dropped column '${colName}' without type definition`);
18321
- lines.push(` // table.specificType('${colName}', 'TYPE')`);
18467
+ for (const col of alteration.droppedColumns) {
18468
+ const knexType = mapDbTypeToKnex(col.type, col.name);
18469
+ lines.push(` ${knexType.code} // was: ${col.type}`);
18322
18470
  }
18323
18471
  for (const col of alteration.modifiedColumns.slice().reverse()) {
18324
18472
  lines.push(` // Revert ${col.name}: ${col.newType} \u2192 ${col.oldType}`);
@@ -20382,14 +20530,29 @@ async function loadSelfPlugin() {
20382
20530
  const pkg3 = JSON.parse(readFileSync7(pkgPath, "utf-8"));
20383
20531
  const pkgName = pkg3?.name;
20384
20532
  if (!pkgName || !/nexus-plugin-/.test(pkgName)) return;
20385
- const candidates = [
20386
- join13(projectPath2, "dist", "index.js"),
20387
- join13(projectPath2, "src", "index.ts")
20388
- ];
20389
- for (const candidate of candidates) {
20390
- if (!existsSync10(candidate)) continue;
20533
+ const srcEntry = join13(projectPath2, "src", "index.ts");
20534
+ const distEntry = join13(projectPath2, "dist", "index.js");
20535
+ if (existsSync10(srcEntry)) {
20536
+ try {
20537
+ const { tsImport } = await import("tsx/esm/api");
20538
+ const mod = await tsImport(
20539
+ pathToFileURL3(srcEntry).href,
20540
+ import.meta.url
20541
+ );
20542
+ const manifest = extractPluginManifest(mod);
20543
+ if (manifest) {
20544
+ if (!manifest.migrationsDir) {
20545
+ manifest.migrationsDir = join13(projectPath2, "migrations");
20546
+ }
20547
+ registerPlugin(manifest);
20548
+ return;
20549
+ }
20550
+ } catch {
20551
+ }
20552
+ }
20553
+ if (existsSync10(distEntry)) {
20391
20554
  try {
20392
- const mod = await import(pathToFileURL3(candidate).href);
20555
+ const mod = await import(pathToFileURL3(distEntry).href);
20393
20556
  const manifest = extractPluginManifest(mod);
20394
20557
  if (manifest) {
20395
20558
  if (!manifest.migrationsDir) {