Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -641,13 +641,13 @@ jobs:
run: npm ci --legacy-peer-deps || npm install --legacy-peer-deps

- name: TypeScript check
run: npx tsc --noEmit || echo "WARN: Mobile TS check had issues (non-blocking)"
run: 'npx tsc --noEmit || echo "WARN: Mobile TS check had issues (non-blocking)"'

- name: Run mobile tests
run: npx jest --passWithNoTests || echo "WARN: Mobile tests had issues (non-blocking)"
run: 'npx jest --passWithNoTests || echo "WARN: Mobile tests had issues (non-blocking)"'

- name: Expo doctor
run: npx expo-doctor || echo "WARN: expo-doctor found issues (non-blocking)"
run: 'npx expo-doctor || echo "WARN: expo-doctor found issues (non-blocking)"'

- name: Build validation
run: |
Expand Down Expand Up @@ -723,7 +723,7 @@ jobs:
branch-protection:
name: Branch Protection Gate
runs-on: ubuntu-latest
needs: [lint, test, build, security, microservice-tests, production-audit, smoke-tests]
needs: [lint, test, build, security, microservice-tests, production-readiness, smoke-tests]
if: github.event_name == 'pull_request'
steps:
- name: All required checks passed
Expand Down
8 changes: 4 additions & 4 deletions server/__tests__/drizzle-enhancements.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,10 +504,10 @@ describe("QueryPerformanceAnalyzer", () => {

describe("DatabaseHealthMonitor", () => {
it("should report connected status on successful check", async () => {
mockExecute.mockResolvedValueOnce([{ "?column?": 1 }]); // SELECT 1
mockExecute.mockResolvedValueOnce([{ active: 5, max_conn: 100 }]);
mockExecute.mockResolvedValueOnce([{ cache_hit_ratio: 98.5 }]);
mockExecute.mockResolvedValueOnce([{ deadlocks: 0, slow_queries: 0 }]);
mockExecute.mockResolvedValueOnce({ rows: [{ "?column?": 1 }] }); // SELECT 1
mockExecute.mockResolvedValueOnce({ rows: [{ active: 5, max_conn: 100 }] });
mockExecute.mockResolvedValueOnce({ rows: [{ cache_hit_ratio: 98.5 }] });
mockExecute.mockResolvedValueOnce({ rows: [{ deadlocks: 0, slow_queries: 0 }] });

const monitor = new DatabaseHealthMonitor(mockDb, 999999);
const status = await monitor.check();
Expand Down
32 changes: 16 additions & 16 deletions server/db-enhancements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ export class DrizzleQueryCache {
await this.redis.expire(`${this.tagPrefix}${tag}`, ttl * 2);
}
} catch (err) {
logger.warn({ err }, "DrizzleQueryCache.set failed");
logger.warn("DrizzleQueryCache.set failed", { err });
}
}

Expand All @@ -169,7 +169,7 @@ export class DrizzleQueryCache {
}
await this.redis.del(tagKey);
} catch (err) {
logger.warn({ err }, "DrizzleQueryCache.invalidateByTag failed");
logger.warn("DrizzleQueryCache.invalidateByTag failed", { err });
}
}

Expand All @@ -181,7 +181,7 @@ export class DrizzleQueryCache {
await Promise.all(keys.map((k: string) => this.redis.del(k)));
}
} catch (err) {
logger.warn({ err }, "DrizzleQueryCache.invalidateByPattern failed");
logger.warn("DrizzleQueryCache.invalidateByPattern failed", { err });
}
}

Expand Down Expand Up @@ -235,7 +235,7 @@ export class DrizzleBatchProcessor {
const result = await (q as any).execute();
totalInserted += chunk.length;
} catch (err) {
logger.error({ err, chunkSize: chunk.length }, "bulkInsert chunk failed");
logger.error("bulkInsert chunk failed", { err, chunkSize: chunk.length });
throw err;
}
}
Expand Down Expand Up @@ -345,7 +345,7 @@ export class DrizzleAuditTrail {
ON CONFLICT DO NOTHING
`);
} catch (err) {
logger.warn({ err, count: entries.length }, "AuditTrail flush failed");
logger.warn("AuditTrail flush failed", { err, count: entries.length });
}
}

Expand Down Expand Up @@ -754,7 +754,7 @@ export class DatabaseHealthMonitor {
const latencyMs = Date.now() - start;

// Connection stats
const [connStats] = await this.db.execute(sql`
const { rows: connRows } = await this.db.execute(sql`
SELECT
count(*) FILTER (WHERE state = 'active') AS active,
(SELECT setting::int FROM pg_settings WHERE name = 'max_connections') AS max_conn
Expand All @@ -763,7 +763,7 @@ export class DatabaseHealthMonitor {
`);

// Cache hit ratio
const [cacheStats] = await this.db.execute(sql`
const { rows: cacheRows } = await this.db.execute(sql`
SELECT
ROUND(
100.0 * sum(heap_blks_hit) / NULLIF(sum(heap_blks_hit) + sum(heap_blks_read), 0),
Expand All @@ -773,7 +773,7 @@ export class DatabaseHealthMonitor {
`);

// Deadlocks and slow queries
const [lockStats] = await this.db.execute(sql`
const { rows: lockRows } = await this.db.execute(sql`
SELECT
deadlocks,
(SELECT count(*) FROM pg_stat_activity WHERE state = 'active' AND query_start < NOW() - INTERVAL '5 seconds') AS slow_queries
Expand All @@ -784,11 +784,11 @@ export class DatabaseHealthMonitor {
const status: DbHealthStatus = {
connected: true,
latencyMs,
activeConnections: Number((connStats as any)?.active ?? 0),
maxConnections: Number((connStats as any)?.max_conn ?? 100),
cacheHitRatio: Number((cacheStats as any)?.cache_hit_ratio ?? 0),
deadlocks: Number((lockStats as any)?.deadlocks ?? 0),
slowQueries: Number((lockStats as any)?.slow_queries ?? 0),
activeConnections: Number((connRows[0] as any)?.active ?? 0),
maxConnections: Number((connRows[0] as any)?.max_conn ?? 100),
cacheHitRatio: Number((cacheRows[0] as any)?.cache_hit_ratio ?? 0),
deadlocks: Number((lockRows[0] as any)?.deadlocks ?? 0),
slowQueries: Number((lockRows[0] as any)?.slow_queries ?? 0),
timestamp: new Date(),
};

Expand All @@ -806,7 +806,7 @@ export class DatabaseHealthMonitor {
timestamp: new Date(),
};
this.lastStatus = status;
logger.error({ err }, "Database health check failed");
logger.error("Database health check failed", { err });
return status;
}
}
Expand Down Expand Up @@ -890,7 +890,7 @@ export class QueryPerformanceAnalyzer {
if (this.queryLog.length > 1000) this.queryLog.shift();

if (isSlowQuery) {
logger.warn({ queryName, durationMs, threshold: this.slowQueryThresholdMs }, "Slow query detected");
logger.warn("Slow query detected", { queryName, durationMs, threshold: this.slowQueryThresholdMs });
}

return { result, durationMs, isSlowQuery };
Expand All @@ -904,7 +904,7 @@ export class QueryPerformanceAnalyzer {
rawSql: string,
): Promise<string> {
const result = await db.execute(sql.raw(`EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) ${rawSql}`));
return (result as any[]).map((r) => Object.values(r)[0]).join("\n");
return (result.rows as any[]).map((r) => Object.values(r)[0]).join("\n");
}

getSlowQueries(limit = 10): typeof this.queryLog {
Expand Down
76 changes: 40 additions & 36 deletions server/db-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,9 @@ export const FarmerQueries = {
if (params.search) {
conditions.push(
or(
ilike(users.name, `%${params.search}%`),
ilike(users.firstName, `%${params.search}%`),
ilike(users.lastName, `%${params.search}%`),
ilike(users.email, `%${params.search}%`),
ilike(users.phone, `%${params.search}%`),
),
);
}
Expand Down Expand Up @@ -165,19 +165,20 @@ export const FarmerQueries = {

const farmIds = userFarms.map((f) => f.id);

const conditions = [inArray(harvests.farmId, farmIds)];
void farmIds;
const conditions = [eq(harvests.userId, userId)];
if (year) {
const startDate = new Date(year, 0, 1);
const endDate = new Date(year + 1, 0, 1);
conditions.push(gte(harvests.harvestDate, startDate.toISOString().split("T")[0]));
conditions.push(lte(harvests.harvestDate, endDate.toISOString().split("T")[0]));
conditions.push(gte(harvests.harvestDate, startDate));
conditions.push(lte(harvests.harvestDate, endDate));
}

const [stats] = await db
.select({
totalHarvests: count(),
totalYield: sum(harvests.quantity),
totalRevenue: sum(harvests.totalValue),
totalRevenue: sum(harvests.revenue),
})
.from(harvests)
.where(and(...conditions));
Expand Down Expand Up @@ -252,7 +253,9 @@ export const FinancialQueries = {
const limit = Math.min(params.limit ?? 20, 100);
const offset = (page - 1) * limit;

const conditions = [eq(paymentTransactions.userId, userId)];
const conditions = [
or(eq(paymentTransactions.senderId, userId), eq(paymentTransactions.receiverId, userId)),
];
if (params.type) conditions.push(eq(paymentTransactions.type, params.type));
if (params.status) conditions.push(eq(paymentTransactions.status, params.status));
if (params.fromDate) conditions.push(gte(paymentTransactions.createdAt, params.fromDate));
Expand Down Expand Up @@ -310,15 +313,13 @@ export const MarketQueries = {
priceDate: marketPrices.priceDate,
price: marketPrices.price,
market: marketPrices.market,
priceChange: marketPrices.priceChange,
priceChangePct: marketPrices.priceChangePct,
})
.from(marketPrices)
.where(
and(
eq(marketPrices.commodity, commodity),
eq(marketPrices.market, market),
gte(marketPrices.priceDate, fromDate.toISOString().split("T")[0]),
gte(marketPrices.priceDate, fromDate),
),
)
.orderBy(asc(marketPrices.priceDate));
Expand All @@ -345,7 +346,7 @@ export const MarketQueries = {
.where(
and(
eq(marketPrices.commodity, commodity),
gte(marketPrices.priceDate, fromDate.toISOString().split("T")[0]),
gte(marketPrices.priceDate, fromDate),
),
);

Expand Down Expand Up @@ -373,7 +374,7 @@ export const IoTQueries = {
if (!db) return [];

const deviceConditions = [eq(iotDevices.farmId, farmId)];
if (deviceType) deviceConditions.push(eq(iotDevices.deviceType, deviceType));
if (deviceType) deviceConditions.push(eq(iotDevices.type, deviceType));

const devices = await db
.select({ id: iotDevices.id })
Expand Down Expand Up @@ -414,10 +415,10 @@ export const IoTQueries = {

const [stats] = await db
.select({
avgSoilMoisture: avg(iotReadings.soilMoisture),
avgAirTemp: avg(iotReadings.airTemperature),
avgHumidity: avg(iotReadings.humidity),
totalRainfall: sum(iotReadings.rainfall),
avgSoilMoisture: sql<number>`AVG(CASE WHEN ${iotReadings.metric} = 'soil_moisture' THEN ${iotReadings.value}::numeric END)`,
avgAirTemp: sql<number>`AVG(CASE WHEN ${iotReadings.metric} = 'air_temperature' THEN ${iotReadings.value}::numeric END)`,
avgHumidity: sql<number>`AVG(CASE WHEN ${iotReadings.metric} = 'humidity' THEN ${iotReadings.value}::numeric END)`,
totalRainfall: sql<number>`SUM(CASE WHEN ${iotReadings.metric} = 'rainfall' THEN ${iotReadings.value}::numeric ELSE 0 END)`,
readingCount: count(),
})
.from(iotReadings)
Expand Down Expand Up @@ -487,7 +488,9 @@ export const InsuranceQueries = {
pendingClaims: count(sql`CASE WHEN ${insuranceClaims.status} = 'pending' THEN 1 END`),
approvedClaims: count(sql`CASE WHEN ${insuranceClaims.status} = 'approved' THEN 1 END`),
totalClaimAmount: sum(insuranceClaims.claimAmount),
totalApprovedAmount: sum(insuranceClaims.approvedAmount),
totalApprovedAmount: sum(
sql`CASE WHEN ${insuranceClaims.status} = 'approved' THEN ${insuranceClaims.claimAmount}::numeric ELSE 0 END`,
),
})
.from(insuranceClaims);

Expand Down Expand Up @@ -519,15 +522,14 @@ export const CarbonQueries = {
const [creditStats] = await db
.select({
totalAvailable: sum(
sql`CASE WHEN ${carbonCredits.status} = 'available' THEN ${carbonCredits.quantity}::numeric ELSE 0 END`,
sql`CASE WHEN ${carbonCredits.status} = 'available' THEN ${carbonCredits.tonnes}::numeric ELSE 0 END`,
),
totalSold: sum(
sql`CASE WHEN ${carbonCredits.status} = 'sold' THEN ${carbonCredits.quantity}::numeric ELSE 0 END`,
sql`CASE WHEN ${carbonCredits.status} = 'sold' THEN ${carbonCredits.tonnes}::numeric ELSE 0 END`,
),
totalRetired: sum(
sql`CASE WHEN ${carbonCredits.status} = 'retired' THEN ${carbonCredits.quantity}::numeric ELSE 0 END`,
sql`CASE WHEN ${carbonCredits.status} = 'retired' THEN ${carbonCredits.tonnes}::numeric ELSE 0 END`,
),
avgPrice: avg(carbonCredits.pricePerTon),
})
.from(carbonCredits)
.where(eq(carbonCredits.projectId, projectId));
Expand All @@ -538,7 +540,7 @@ export const CarbonQueries = {
available: Number(creditStats?.totalAvailable ?? 0),
sold: Number(creditStats?.totalSold ?? 0),
retired: Number(creditStats?.totalRetired ?? 0),
avgPricePerTon: Number(creditStats?.avgPrice ?? 0),
avgPricePerTon: Number(project.pricePerTonne ?? 0),
},
};
},
Expand Down Expand Up @@ -579,12 +581,11 @@ export const ExportQueries = {
commodity: exportShipments.commodity,
totalShipments: count(),
totalQuantity: sum(exportShipments.quantity),
totalValueUsd: sum(exportShipments.totalValueUsd),
avgQuantity: avg(exportShipments.quantity),
})
.from(exportShipments)
.groupBy(exportShipments.commodity)
.orderBy(desc(sum(exportShipments.totalValueUsd)));
.orderBy(desc(sum(exportShipments.quantity)));
},
};

Expand Down Expand Up @@ -612,8 +613,12 @@ export const PipelineQueries = {

const [aggMetrics] = await db
.select({
totalRecordsProcessed: sum(pipelineMetrics.recordsWritten),
avgThroughput: avg(pipelineMetrics.throughputRps),
totalRecordsProcessed: sum(
sql`CASE WHEN ${pipelineMetrics.metricName} = 'records_written' THEN ${pipelineMetrics.metricValue}::numeric ELSE 0 END`,
),
avgThroughput: avg(
sql`CASE WHEN ${pipelineMetrics.metricName} = 'throughput_rps' THEN ${pipelineMetrics.metricValue}::numeric END`,
),
totalRuns: count(),
})
.from(pipelineMetrics)
Expand Down Expand Up @@ -654,8 +659,8 @@ export const FederatedLearningQueries = {
const [stats] = await db
.select({
activeParticipants: count(sql`CASE WHEN ${federatedParticipants.status} = 'active' THEN 1 END`),
avgContribution: avg(federatedParticipants.contributionScore),
totalDataSize: sum(federatedParticipants.localDataSize),
avgContribution: avg(federatedParticipants.localAccuracy),
totalDataSize: sum(federatedParticipants.dataPoints),
})
.from(federatedParticipants)
.where(eq(federatedParticipants.modelId, modelId));
Expand Down Expand Up @@ -734,7 +739,6 @@ export const BatchOperations = {
.set({
memberCount: Number(memberCount?.count ?? 0),
totalSavings: String(txnStats?.totalSavings ?? "0"),
updatedAt: new Date(),
})
.where(eq(chamaGroups.id, chamaId));
},
Expand All @@ -754,23 +758,23 @@ export const SearchQueries = {

const [farmers, farmsResult, commodities] = await Promise.all([
db
.select({ id: users.id, name: users.name, email: users.email, role: users.role })
.select({ id: users.id, name: users.firstName, email: users.email, role: users.role })
.from(users)
.where(
or(
ilike(users.name, `%${query}%`),
ilike(users.firstName, `%${query}%`),
ilike(users.lastName, `%${query}%`),
ilike(users.email, `%${query}%`),
ilike(users.phone, `%${query}%`),
),
)
.limit(limit),

db
.select({ id: farms.id, name: farms.name, location: farms.location })
.select({ id: farms.id, name: farms.farmName, location: farms.location })
.from(farms)
.where(
or(
ilike(farms.name, `%${query}%`),
ilike(farms.farmName, `%${query}%`),
ilike(farms.location, `%${query}%`),
),
)
Expand Down Expand Up @@ -815,11 +819,11 @@ export const AnalyticsQueries = {
.from(chamaGroups)
.where(eq(chamaGroups.isActive, true)),
db
.select({ total: count(), totalSequestration: sum(carbonProjects.annualSequestration) })
.select({ total: count(), totalSequestration: sum(carbonProjects.annualCredits) })
.from(carbonProjects)
.where(eq(carbonProjects.status, "active")),
db
.select({ total: count(), totalValue: sum(exportShipments.totalValueUsd) })
.select({ total: count(), totalValue: sum(exportShipments.quantity) })
.from(exportShipments),
db
.select({ total: count(), active: count(sql`CASE WHEN ${pipelineJobs.status} = 'running' THEN 1 END`) })
Expand Down
Loading
Loading