📦 EqualifyEverything / equalify-api

📄 getResultsCsv.ts · 146 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146import { db, graphql } from '#src/utils';
import { gzipSync } from 'zlib';

export const getResultsCsv = async ({ request, reply }) => {
    await db.connect();
    const report = (await db.query(`SELECT "id", "name", "filters", "cache_date" FROM "reports" WHERE "id" = $1`, [request.query.reportId])).rows[0];
    const types = ['properties', 'urls', 'messages', 'nodes', 'tags', 'types', 'status'];
    const filters = Object.fromEntries(types.map(obj => [obj, []]));
    for (const type of types) {
        filters[type] = report.filters.filter(obj => obj.type === type).map(obj => obj.value)
    }

    // Check if there are any urls in our request
    const urls = (await db.query({
        text: `SELECT "id", "url" FROM "urls" WHERE "id" = ANY($1::uuid[]) OR "property_id" = ANY($2::uuid[])`,
        values: [filters.urls, filters.properties],
    })).rows;

    const query = {
        request,
        query: `query (
            $urlIds: [uuid!],
            ${filters.types.length > 0 ? '$typeIds: [String],' : ''}
            ${filters.messages.length > 0 ? '$messageIds: [uuid],' : ''}
            ${filters.tags.length > 0 ? '$tagIds: [uuid],' : ''}
            ${filters.status.length > 0 ? '$equalified: Boolean,' : ''}
        ) {
            nodes: enodes(where: {
                url_id: {_in: $urlIds},
                ${filters.status.length > 0 ? `equalified: {_eq: $equalified},` : ''}
                ${filters.types.length > 0 || filters.messages.length > 0 || filters.tags.length > 0 ? `message_nodes: {message: {` : ``}
                    ${filters.types.length > 0 ? `type: {_in: $typeIds},` : ``}
                    ${filters.messages.length > 0 ? `id: {_in: $messageIds},` : ``}
                    ${filters.tags.length > 0 ? `message_tags:{tag:{id: {_in: $tagIds}}},` : ``}
                ${filters.types.length > 0 || filters.messages.length > 0 || filters.tags.length > 0 ? `}}` : ``}
            }) {
                nodeId: id
                createdAt: created_at
                html
                targets
                relatedUrlId: url_id
                equalified
                enodeUpdates: enode_updates { createdAt: created_at equalified }
                messageNodes: message_nodes(where:{
                    ${filters.types.length > 0 || filters.messages.length > 0 || filters.tags.length > 0 ? `message: {` : ``}
                        ${filters.types.length > 0 ? `type: {_in: $typeIds},` : ``}
                        ${filters.messages.length > 0 ? `id: {_in: $messageIds},` : ``}
                        ${filters.tags.length > 0 ? `message_tags:{tag:{id: {_in: $tagIds}}},` : ``}
                    ${filters.types.length > 0 || filters.messages.length > 0 || filters.tags.length > 0 ? `}` : ``}
                }) {
                    id
                    node: enode {
                        equalified
                    }
                    message {
                        id
                        message
                        type
                        ${urls.length < 100 ? `
                            messageTags: message_tags(where:{
                                ${filters.tags.length > 0 ? `tag:{id: {_in: $tagIds}},` : ``}
                            }) {
                                tag {
                                    id
                                    tag
                                }
                            }
                        ` : ''}
                    }
                }
            }
        }`,
        variables: {
            urlIds: urls.map(obj => obj.id),
            ...filters.types.length > 0 && ({ typeIds: filters.types }),
            ...filters.messages.length > 0 && ({ messageIds: filters.messages }),
            ...filters.tags.length > 0 && ({ tagIds: filters.tags }),
            ...filters.status.length > 0 && ({ equalified: filters.status[0] === 'active' ? false : true }),
        },
    };
    console.log(JSON.stringify({ query }));
    const response = await graphql(query);
    const filteredNodes = response.nodes ?? [];

    // Get URL lookup map for easier reference
    const urlMap = urls.reduce((acc, url) => {
        acc[url.id] = url.url;
        return acc;
    }, {});

    // Create CSV header
    const csvHeader = [
        'Node ID',
        'URL',
        'HTML',
        'Targets',
        'Status',
        'Created At',
        'Messages'
    ].join(',');

    // Create CSV rows
    const csvRows = filteredNodes.slice(0, 10000).map(node => {
        // Escape fields that might contain commas or quotes
        const escapeField = (field) => {
            if (field === null || field === undefined) return '';
            const stringField = String(field);
            if (stringField.includes(',') || stringField.includes('"') || stringField.includes('\n')) {
                return `"${stringField.replace(/"/g, '""')}"`;
            }
            return stringField;
        };

        const nodeMessages = node.messageNodes.map(mn => 
            `${escapeField(mn.message.type)}: ${escapeField(mn.message.message)}`
        ).join(' | ');

        return [
            escapeField(node.nodeId),
            escapeField(urlMap[node.relatedUrlId] || ''),
            escapeField(node.html),
            escapeField(Array.isArray(node.targets) ? node.targets.join(', ') : node.targets),
            escapeField(node.equalified ? 'Equalified' : 'Active'),
            escapeField(node.createdAt),
            escapeField(nodeMessages)
        ].join(',');
    });

    // Combine header and rows
    const csvContent = [csvHeader, ...csvRows].join('\r\n');

    // Compress the CSV content
    const compressedBody = gzipSync(csvContent);
    
    // Cache the compressed CSV
    await db.clean();
    
    // Set appropriate headers for CSV download
    reply.headers({ 
        'content-encoding': 'gzip',
        'content-type': 'text/csv',
        'content-disposition': `attachment; filename="${report.name.replace(/[^a-z0-9]/gi, '_')}_report.csv"`
    });
    
    return reply.send(compressedBody);
}