Add coverage to data management tab

This commit is contained in:
Dries Augustyns
2025-12-03 08:21:54 +01:00
parent e506659736
commit de34b8c76b
2 changed files with 43 additions and 14 deletions
+38 -14
View File
@@ -383,18 +383,25 @@ export class ContactService {
*/ */
public static async getAvailableFields( public static async getAvailableFields(
projectId: string, projectId: string,
): Promise<Array<{field: string; type: 'string' | 'number' | 'boolean' | 'date'}>> { ): Promise<Array<{field: string; type: 'string' | 'number' | 'boolean' | 'date'; coverage: number}>> {
// Standard fields with known types // Get total contact count for coverage calculation
const totalContacts = await prisma.contact.count({
where: {projectId},
});
// Standard fields with known types (always 100% coverage)
const standardFields = [ const standardFields = [
{field: 'email', type: 'string' as const}, {field: 'email', type: 'string' as const, coverage: 100},
{field: 'subscribed', type: 'boolean' as const}, {field: 'subscribed', type: 'boolean' as const, coverage: 100},
{field: 'createdAt', type: 'date' as const}, {field: 'createdAt', type: 'date' as const, coverage: 100},
{field: 'updatedAt', type: 'date' as const}, {field: 'updatedAt', type: 'date' as const, coverage: 100},
]; ];
// Get custom fields from the data JSON column with type inference // Get custom fields from the data JSON column with type inference and coverage
// Use raw SQL to extract all keys and sample values from the JSON data column // Use raw SQL to extract all keys, sample values, and contact counts from the JSON data column
const result = await prisma.$queryRaw<Array<{key: string; sample_value: string; json_type: string}>>` const result = await prisma.$queryRaw<
Array<{key: string; sample_value: string; json_type: string; contact_count: bigint}>
>`
WITH field_keys AS ( WITH field_keys AS (
SELECT DISTINCT jsonb_object_keys(data) as key SELECT DISTINCT jsonb_object_keys(data) as key
FROM contacts FROM contacts
@@ -418,15 +425,27 @@ export class ContactService {
AND data->fk.key IS NOT NULL AND data->fk.key IS NOT NULL
LIMIT 1 LIMIT 1
) c ) c
),
field_counts AS (
SELECT
fk.key,
COUNT(*) as contact_count
FROM field_keys fk
JOIN contacts c ON c."projectId" = ${projectId}
AND c.data ? fk.key
AND c.data->fk.key IS NOT NULL
GROUP BY fk.key
) )
SELECT SELECT
key, fs.key,
sample_value, fs.sample_value,
json_type fs.json_type,
FROM field_samples fc.contact_count
FROM field_samples fs
JOIN field_counts fc ON fc.key = fs.key
`; `;
// Infer types from JSON types and sample values // Infer types from JSON types and sample values, calculate coverage
const customFields = result.map(row => { const customFields = result.map(row => {
let type: 'string' | 'number' | 'boolean' | 'date' = 'string'; let type: 'string' | 'number' | 'boolean' | 'date' = 'string';
@@ -443,9 +462,14 @@ export class ContactService {
} }
} }
// Calculate coverage percentage
const contactCount = Number(row.contact_count);
const coverage = totalContacts > 0 ? Math.round((contactCount / totalContacts) * 100) : 0;
return { return {
field: `data.${row.key}`, field: `data.${row.key}`,
type, type,
coverage,
}; };
}); });
@@ -30,6 +30,7 @@ import {network} from '../lib/network';
interface FieldData { interface FieldData {
field: string; field: string;
type: 'string' | 'number' | 'boolean' | 'date'; type: 'string' | 'number' | 'boolean' | 'date';
coverage: number;
} }
interface FieldUsage { interface FieldUsage {
@@ -158,6 +159,7 @@ export function DataManagementSettings() {
<TableRow> <TableRow>
<TableHead>Field Name</TableHead> <TableHead>Field Name</TableHead>
<TableHead>Type</TableHead> <TableHead>Type</TableHead>
<TableHead>Coverage</TableHead>
<TableHead className="text-right">Actions</TableHead> <TableHead className="text-right">Actions</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
@@ -170,6 +172,9 @@ export function DataManagementSettings() {
<TableCell> <TableCell>
<Badge variant="secondary">{field.type}</Badge> <Badge variant="secondary">{field.type}</Badge>
</TableCell> </TableCell>
<TableCell>
<span className="text-sm text-muted-foreground">{field.coverage}%</span>
</TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<Button <Button
variant="ghost" variant="ghost"