@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 +245 -82
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +173 -60
- package/dist/index.js.map +1 -1
- package/dist/main.js +173 -60
- package/dist/main.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
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:
|
|
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
|
|
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(
|
|
7435
|
-
const roleNames = await usersService.getRoleNames(
|
|
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:
|
|
7438
|
-
email:
|
|
7454
|
+
userId: validated.user.id,
|
|
7455
|
+
email: validated.user.email,
|
|
7439
7456
|
roleIds,
|
|
7440
7457
|
roleNames
|
|
7441
7458
|
});
|
|
7442
|
-
await db2
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
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(
|
|
7452
|
-
events.emitEvent("auth.refresh", { userId:
|
|
7470
|
+
const ability = await defineAbilityFor2(validated.user, roleNames);
|
|
7471
|
+
events.emitEvent("auth.refresh", { userId: validated.user.id });
|
|
7453
7472
|
await persistAuditEvent("refresh", {
|
|
7454
|
-
userId:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
17999
|
-
if (
|
|
18000
|
-
|
|
18001
|
-
|
|
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
|
-
|
|
18005
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
18076
|
-
|
|
18077
|
-
|
|
18078
|
-
|
|
18079
|
-
|
|
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
|
-
|
|
18083
|
-
|
|
18084
|
-
|
|
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
|
-
|
|
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
|
|
18296
|
-
lines.push(` table.dropColumn('${
|
|
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
|
|
18320
|
-
|
|
18321
|
-
lines.push(` //
|
|
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
|
|
20386
|
-
|
|
20387
|
-
|
|
20388
|
-
|
|
20389
|
-
|
|
20390
|
-
|
|
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(
|
|
20555
|
+
const mod = await import(pathToFileURL3(distEntry).href);
|
|
20393
20556
|
const manifest = extractPluginManifest(mod);
|
|
20394
20557
|
if (manifest) {
|
|
20395
20558
|
if (!manifest.migrationsDir) {
|