chore(api): temporary boot-time data diagnostics in the container log
Prints aggregate counts only, prefixed [DIAG], right after migrations: customers by kind/active/archived/step, what the omde list can show and how many rows are sales-expert trading accounts (created vs adopted), experts' profile/trading state, referral counts, the Admin table and the last applied migrations. Read-only, no mobile/name/id is printed, each query isolated so a failure is reported without stopping boot. Disable with DIAG_LOG=false; remove once the empty-omde-list investigation is closed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ee43d664d5
commit
ab39f6abed
@@ -163921,6 +163921,102 @@ var require_app_module = __commonJS({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// dist/prisma/diagnostics.js
|
||||||
|
var require_diagnostics = __commonJS({
|
||||||
|
"dist/prisma/diagnostics.js"(exports2) {
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
||||||
|
exports2.logDataDiagnostics = logDataDiagnostics;
|
||||||
|
var common_12 = require_common();
|
||||||
|
var QUERIES = [
|
||||||
|
{
|
||||||
|
label: "migrations: last 5 applied",
|
||||||
|
sql: `SELECT name, applied_at FROM "_goldbank_migrations" ORDER BY applied_at DESC, name DESC LIMIT 5`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "customers by kind / active / archived / step",
|
||||||
|
sql: `SELECT kind, "isActive" AS active, "isArchived" AS archived, "verificationStep" AS step, count(*)::int AS n
|
||||||
|
FROM "Customer" GROUP BY 1,2,3,4 ORDER BY n DESC`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'omde list ("all" scope) \u2014 what the panel can show',
|
||||||
|
sql: `SELECT
|
||||||
|
count(*)::int AS app_shopkeeper_not_archived,
|
||||||
|
count(*) FILTER (WHERE p.id IS NOT NULL)::int AS of_which_expert_trading_accounts,
|
||||||
|
count(*) FILTER (WHERE p.id IS NULL)::int AS listed_in_omde
|
||||||
|
FROM "Customer" c
|
||||||
|
LEFT JOIN "SalesExpertProfile" p ON p."tradingCustomerId" = c.id
|
||||||
|
WHERE c.kind = 'APP_SHOPKEEPER' AND c."isArchived" = false`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "expert trading accounts: created for the expert vs adopted existing customer",
|
||||||
|
sql: `SELECT
|
||||||
|
count(*)::int AS trading_accounts,
|
||||||
|
count(*) FILTER (WHERE c."createdAt" < a."createdAt")::int AS customer_older_than_expert,
|
||||||
|
count(*) FILTER (WHERE c."createdAt" >= a."createdAt")::int AS customer_newer_than_expert,
|
||||||
|
count(*) FILTER (WHERE c.mobile = a.mobile)::int AS same_mobile,
|
||||||
|
count(*) FILTER (WHERE c."referredByAgentId" IS NOT NULL)::int AS has_referrer,
|
||||||
|
count(*) FILTER (WHERE EXISTS (SELECT 1 FROM "Order" o WHERE o."customerId" = c.id))::int AS has_orders,
|
||||||
|
count(*) FILTER (WHERE c."firstName" IS DISTINCT FROM a."firstName"
|
||||||
|
OR c."lastName" IS DISTINCT FROM a."lastName")::int AS name_differs_from_expert
|
||||||
|
FROM "SalesExpertProfile" p
|
||||||
|
JOIN "Customer" c ON c.id = p."tradingCustomerId"
|
||||||
|
JOIN "AdminUser" a ON a.id = p."adminUserId"`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "AdminUser by role / active (experts + pre-split admin copies)",
|
||||||
|
sql: `SELECT role, "isActive" AS active, count(*)::int AS n FROM "AdminUser" GROUP BY 1,2 ORDER BY n DESC`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "experts: profile / trading account",
|
||||||
|
sql: `SELECT
|
||||||
|
count(*)::int AS experts,
|
||||||
|
count(*) FILTER (WHERE p.id IS NULL)::int AS no_profile,
|
||||||
|
count(*) FILTER (WHERE p."tradingCustomerId" IS NOT NULL)::int AS with_trading_account,
|
||||||
|
count(*) FILTER (WHERE EXISTS (SELECT 1 FROM "Customer" c WHERE c.mobile = a.mobile))::int AS mobile_also_a_customer
|
||||||
|
FROM "AdminUser" a
|
||||||
|
LEFT JOIN "SalesExpertProfile" p ON p."adminUserId" = a.id
|
||||||
|
WHERE a.role = 'SALES_EXPERT'`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "experts: when created (by day, last 10 days with rows)",
|
||||||
|
sql: `SELECT date_trunc('day', "createdAt")::date AS day, count(*)::int AS n
|
||||||
|
FROM "AdminUser" WHERE role = 'SALES_EXPERT' GROUP BY 1 ORDER BY 1 DESC LIMIT 10`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "customers: when created (by day, last 10 days with rows)",
|
||||||
|
sql: `SELECT date_trunc('day', "createdAt")::date AS day, count(*)::int AS n
|
||||||
|
FROM "Customer" GROUP BY 1 ORDER BY 1 DESC LIMIT 10`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "customers with a referring expert",
|
||||||
|
sql: `SELECT count(*) FILTER (WHERE "referredByAgentId" IS NOT NULL)::int AS referred,
|
||||||
|
count(*)::int AS all_customers
|
||||||
|
FROM "Customer"`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Admin table",
|
||||||
|
sql: `SELECT role, "isActive" AS active, count(*)::int AS n FROM "Admin" GROUP BY 1,2 ORDER BY n DESC`
|
||||||
|
}
|
||||||
|
];
|
||||||
|
async function logDataDiagnostics(prisma) {
|
||||||
|
if (process.env.DIAG_LOG === "false")
|
||||||
|
return;
|
||||||
|
const logger = new common_12.Logger("DIAG");
|
||||||
|
logger.log("[DIAG] ===== data diagnostics start (counts only, read-only) =====");
|
||||||
|
for (const q of QUERIES) {
|
||||||
|
try {
|
||||||
|
const rows = await prisma.$queryRawUnsafe(q.sql);
|
||||||
|
logger.log(`[DIAG] ${q.label}: ${JSON.stringify(rows, (_k, v) => typeof v === "bigint" ? Number(v) : v)}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`[DIAG] ${q.label}: FAILED \u2014 ${error.message.split("\n").pop()}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.log("[DIAG] ===== data diagnostics end =====");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// dist/prisma/migrate.js
|
// dist/prisma/migrate.js
|
||||||
var require_migrate = __commonJS({
|
var require_migrate = __commonJS({
|
||||||
"dist/prisma/migrate.js"(exports2) {
|
"dist/prisma/migrate.js"(exports2) {
|
||||||
@@ -164099,6 +164195,7 @@ var require_bootstrap = __commonJS({
|
|||||||
var util_1 = require("util");
|
var util_1 = require("util");
|
||||||
var common_12 = require_common();
|
var common_12 = require_common();
|
||||||
var client_1 = require("@prisma/client");
|
var client_1 = require("@prisma/client");
|
||||||
|
var diagnostics_1 = require_diagnostics();
|
||||||
var migrate_1 = require_migrate();
|
var migrate_1 = require_migrate();
|
||||||
var execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
var execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
||||||
function resolveSeedScript() {
|
function resolveSeedScript() {
|
||||||
@@ -164121,6 +164218,7 @@ var require_bootstrap = __commonJS({
|
|||||||
} else {
|
} else {
|
||||||
logger.log("No pending migrations");
|
logger.log("No pending migrations");
|
||||||
}
|
}
|
||||||
|
await (0, diagnostics_1.logDataDiagnostics)(prisma);
|
||||||
if (process.env.RUN_SEED === "false")
|
if (process.env.RUN_SEED === "false")
|
||||||
return;
|
return;
|
||||||
const seedScript = resolveSeedScript();
|
const seedScript = resolveSeedScript();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { join } from 'path';
|
|||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import { logDataDiagnostics } from './diagnostics';
|
||||||
import { runMigrations, runOnce } from './migrate';
|
import { runMigrations, runOnce } from './migrate';
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
@@ -40,6 +41,9 @@ export async function prepareDatabase(): Promise<void> {
|
|||||||
logger.log('No pending migrations');
|
logger.log('No pending migrations');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TEMPORARY — see diagnostics.ts. Never throws.
|
||||||
|
await logDataDiagnostics(prisma);
|
||||||
|
|
||||||
if (process.env.RUN_SEED === 'false') return;
|
if (process.env.RUN_SEED === 'false') return;
|
||||||
|
|
||||||
const seedScript = resolveSeedScript();
|
const seedScript = resolveSeedScript();
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { Logger } from '@nestjs/common';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TEMPORARY boot-time data diagnostics — prints aggregate COUNTS only to the
|
||||||
|
* container log, prefixed "[DIAG]", so the production state behind two open
|
||||||
|
* issues can be read from Dokploy's log without database access:
|
||||||
|
*
|
||||||
|
* 1. «لیست مشتریان عمده» is empty after the last deploy;
|
||||||
|
* 2. a sales expert's app data vs the panel.
|
||||||
|
*
|
||||||
|
* Read-only (SELECT only). No mobile, name or id is printed. Every query is
|
||||||
|
* isolated, so one failing query (e.g. a missing table) is reported and the
|
||||||
|
* rest still run; nothing here can stop the API from booting.
|
||||||
|
*
|
||||||
|
* Disable with DIAG_LOG=false. Remove this file and its call in bootstrap.ts
|
||||||
|
* once the issues are closed.
|
||||||
|
*/
|
||||||
|
const QUERIES: { label: string; sql: string }[] = [
|
||||||
|
{
|
||||||
|
label: 'migrations: last 5 applied',
|
||||||
|
sql: `SELECT name, applied_at FROM "_goldbank_migrations" ORDER BY applied_at DESC, name DESC LIMIT 5`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'customers by kind / active / archived / step',
|
||||||
|
sql: `SELECT kind, "isActive" AS active, "isArchived" AS archived, "verificationStep" AS step, count(*)::int AS n
|
||||||
|
FROM "Customer" GROUP BY 1,2,3,4 ORDER BY n DESC`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'omde list ("all" scope) — what the panel can show',
|
||||||
|
sql: `SELECT
|
||||||
|
count(*)::int AS app_shopkeeper_not_archived,
|
||||||
|
count(*) FILTER (WHERE p.id IS NOT NULL)::int AS of_which_expert_trading_accounts,
|
||||||
|
count(*) FILTER (WHERE p.id IS NULL)::int AS listed_in_omde
|
||||||
|
FROM "Customer" c
|
||||||
|
LEFT JOIN "SalesExpertProfile" p ON p."tradingCustomerId" = c.id
|
||||||
|
WHERE c.kind = 'APP_SHOPKEEPER' AND c."isArchived" = false`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'expert trading accounts: created for the expert vs adopted existing customer',
|
||||||
|
sql: `SELECT
|
||||||
|
count(*)::int AS trading_accounts,
|
||||||
|
count(*) FILTER (WHERE c."createdAt" < a."createdAt")::int AS customer_older_than_expert,
|
||||||
|
count(*) FILTER (WHERE c."createdAt" >= a."createdAt")::int AS customer_newer_than_expert,
|
||||||
|
count(*) FILTER (WHERE c.mobile = a.mobile)::int AS same_mobile,
|
||||||
|
count(*) FILTER (WHERE c."referredByAgentId" IS NOT NULL)::int AS has_referrer,
|
||||||
|
count(*) FILTER (WHERE EXISTS (SELECT 1 FROM "Order" o WHERE o."customerId" = c.id))::int AS has_orders,
|
||||||
|
count(*) FILTER (WHERE c."firstName" IS DISTINCT FROM a."firstName"
|
||||||
|
OR c."lastName" IS DISTINCT FROM a."lastName")::int AS name_differs_from_expert
|
||||||
|
FROM "SalesExpertProfile" p
|
||||||
|
JOIN "Customer" c ON c.id = p."tradingCustomerId"
|
||||||
|
JOIN "AdminUser" a ON a.id = p."adminUserId"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'AdminUser by role / active (experts + pre-split admin copies)',
|
||||||
|
sql: `SELECT role, "isActive" AS active, count(*)::int AS n FROM "AdminUser" GROUP BY 1,2 ORDER BY n DESC`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'experts: profile / trading account',
|
||||||
|
sql: `SELECT
|
||||||
|
count(*)::int AS experts,
|
||||||
|
count(*) FILTER (WHERE p.id IS NULL)::int AS no_profile,
|
||||||
|
count(*) FILTER (WHERE p."tradingCustomerId" IS NOT NULL)::int AS with_trading_account,
|
||||||
|
count(*) FILTER (WHERE EXISTS (SELECT 1 FROM "Customer" c WHERE c.mobile = a.mobile))::int AS mobile_also_a_customer
|
||||||
|
FROM "AdminUser" a
|
||||||
|
LEFT JOIN "SalesExpertProfile" p ON p."adminUserId" = a.id
|
||||||
|
WHERE a.role = 'SALES_EXPERT'`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'experts: when created (by day, last 10 days with rows)',
|
||||||
|
sql: `SELECT date_trunc('day', "createdAt")::date AS day, count(*)::int AS n
|
||||||
|
FROM "AdminUser" WHERE role = 'SALES_EXPERT' GROUP BY 1 ORDER BY 1 DESC LIMIT 10`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'customers: when created (by day, last 10 days with rows)',
|
||||||
|
sql: `SELECT date_trunc('day', "createdAt")::date AS day, count(*)::int AS n
|
||||||
|
FROM "Customer" GROUP BY 1 ORDER BY 1 DESC LIMIT 10`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'customers with a referring expert',
|
||||||
|
sql: `SELECT count(*) FILTER (WHERE "referredByAgentId" IS NOT NULL)::int AS referred,
|
||||||
|
count(*)::int AS all_customers
|
||||||
|
FROM "Customer"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Admin table',
|
||||||
|
sql: `SELECT role, "isActive" AS active, count(*)::int AS n FROM "Admin" GROUP BY 1,2 ORDER BY n DESC`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export async function logDataDiagnostics(prisma: PrismaClient): Promise<void> {
|
||||||
|
if (process.env.DIAG_LOG === 'false') return;
|
||||||
|
const logger = new Logger('DIAG');
|
||||||
|
|
||||||
|
logger.log('[DIAG] ===== data diagnostics start (counts only, read-only) =====');
|
||||||
|
for (const q of QUERIES) {
|
||||||
|
try {
|
||||||
|
const rows = await prisma.$queryRawUnsafe<Record<string, unknown>[]>(q.sql);
|
||||||
|
logger.log(`[DIAG] ${q.label}: ${JSON.stringify(rows, (_k, v) => (typeof v === 'bigint' ? Number(v) : v))}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`[DIAG] ${q.label}: FAILED — ${(error as Error).message.split('\n').pop()}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.log('[DIAG] ===== data diagnostics end =====');
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user