search for standard sites
pub-search.waow.tech
search
zig
blog
atproto
1const std = @import("std");
2const json = std.json;
3const Allocator = std.mem.Allocator;
4const db = @import("db/mod.zig");
5
6// JSON output types
7const TagJson = struct { tag: []const u8, count: i64 };
8const TimelineJson = struct { date: []const u8, count: i64 };
9const PubJson = struct { name: []const u8, basePath: []const u8, count: i64 };
10
11/// All data needed to render the dashboard
12pub const Data = struct {
13 started_at: i64,
14 searches: i64,
15 publications: i64,
16 articles: i64,
17 looseleafs: i64,
18 tags_json: []const u8,
19 timeline_json: []const u8,
20 top_pubs_json: []const u8,
21};
22
23// all dashboard queries batched into one request
24const STATS_SQL =
25 \\SELECT
26 \\ (SELECT COUNT(*) FROM documents) as docs,
27 \\ (SELECT COUNT(*) FROM publications) as pubs,
28 \\ (SELECT total_searches FROM stats WHERE id = 1) as searches,
29 \\ (SELECT total_errors FROM stats WHERE id = 1) as errors,
30 \\ (SELECT service_started_at FROM stats WHERE id = 1) as started_at
31;
32
33const DOC_TYPES_SQL =
34 \\SELECT
35 \\ SUM(CASE WHEN publication_uri != '' THEN 1 ELSE 0 END) as articles,
36 \\ SUM(CASE WHEN publication_uri = '' OR publication_uri IS NULL THEN 1 ELSE 0 END) as looseleafs
37 \\FROM documents
38;
39
40const TAGS_SQL =
41 \\SELECT tag, COUNT(*) as count
42 \\FROM document_tags
43 \\GROUP BY tag
44 \\ORDER BY count DESC
45 \\LIMIT 100
46;
47
48const TIMELINE_SQL =
49 \\SELECT DATE(created_at) as date, COUNT(*) as count
50 \\FROM documents
51 \\WHERE created_at IS NOT NULL AND created_at != ''
52 \\GROUP BY DATE(created_at)
53 \\ORDER BY date DESC
54 \\LIMIT 30
55;
56
57const TOP_PUBS_SQL =
58 \\SELECT p.name, p.base_path, COUNT(d.uri) as doc_count
59 \\FROM publications p
60 \\JOIN documents d ON d.publication_uri = p.uri
61 \\GROUP BY p.uri
62 \\ORDER BY doc_count DESC
63 \\LIMIT 8
64;
65
66pub fn fetch(alloc: Allocator) !Data {
67 const client = db.getClient() orelse return error.NotInitialized;
68
69 // batch all 5 queries into one HTTP request
70 var batch = client.queryBatch(&.{
71 .{ .sql = STATS_SQL },
72 .{ .sql = DOC_TYPES_SQL },
73 .{ .sql = TAGS_SQL },
74 .{ .sql = TIMELINE_SQL },
75 .{ .sql = TOP_PUBS_SQL },
76 }) catch return error.QueryFailed;
77 defer batch.deinit();
78
79 // extract stats (query 0)
80 const stats_row = batch.getFirst(0);
81 const started_at = if (stats_row) |r| r.int(4) else 0;
82 const searches = if (stats_row) |r| r.int(2) else 0;
83 const publications = if (stats_row) |r| r.int(1) else 0;
84
85 // extract doc types (query 1)
86 const doc_row = batch.getFirst(1);
87 const articles = if (doc_row) |r| r.int(0) else 0;
88 const looseleafs = if (doc_row) |r| r.int(1) else 0;
89
90 return .{
91 .started_at = started_at,
92 .searches = searches,
93 .publications = publications,
94 .articles = articles,
95 .looseleafs = looseleafs,
96 .tags_json = try formatTagsJson(alloc, batch.get(2)),
97 .timeline_json = try formatTimelineJson(alloc, batch.get(3)),
98 .top_pubs_json = try formatPubsJson(alloc, batch.get(4)),
99 };
100}
101
102fn formatTagsJson(alloc: Allocator, rows: []const db.Row) ![]const u8 {
103 var output: std.Io.Writer.Allocating = .init(alloc);
104 errdefer output.deinit();
105 var jw: json.Stringify = .{ .writer = &output.writer };
106 try jw.beginArray();
107 for (rows) |row| try jw.write(TagJson{ .tag = row.text(0), .count = row.int(1) });
108 try jw.endArray();
109 return try output.toOwnedSlice();
110}
111
112fn formatTimelineJson(alloc: Allocator, rows: []const db.Row) ![]const u8 {
113 var output: std.Io.Writer.Allocating = .init(alloc);
114 errdefer output.deinit();
115 var jw: json.Stringify = .{ .writer = &output.writer };
116 try jw.beginArray();
117 for (rows) |row| try jw.write(TimelineJson{ .date = row.text(0), .count = row.int(1) });
118 try jw.endArray();
119 return try output.toOwnedSlice();
120}
121
122fn formatPubsJson(alloc: Allocator, rows: []const db.Row) ![]const u8 {
123 var output: std.Io.Writer.Allocating = .init(alloc);
124 errdefer output.deinit();
125 var jw: json.Stringify = .{ .writer = &output.writer };
126 try jw.beginArray();
127 for (rows) |row| try jw.write(PubJson{ .name = row.text(0), .basePath = row.text(1), .count = row.int(2) });
128 try jw.endArray();
129 return try output.toOwnedSlice();
130}
131
132/// Generate dashboard data as JSON for API endpoint
133pub fn toJson(alloc: Allocator, data: Data) ![]const u8 {
134 var output: std.Io.Writer.Allocating = .init(alloc);
135 errdefer output.deinit();
136
137 var jw: json.Stringify = .{ .writer = &output.writer };
138 try jw.beginObject();
139
140 try jw.objectField("startedAt");
141 try jw.write(data.started_at);
142
143 try jw.objectField("searches");
144 try jw.write(data.searches);
145
146 try jw.objectField("publications");
147 try jw.write(data.publications);
148
149 try jw.objectField("articles");
150 try jw.write(data.articles);
151
152 try jw.objectField("looseleafs");
153 try jw.write(data.looseleafs);
154
155 // use beginWriteRaw/endWriteRaw for pre-formatted JSON arrays
156 try jw.objectField("tags");
157 try jw.beginWriteRaw();
158 try jw.writer.writeAll(data.tags_json);
159 jw.endWriteRaw();
160
161 try jw.objectField("timeline");
162 try jw.beginWriteRaw();
163 try jw.writer.writeAll(data.timeline_json);
164 jw.endWriteRaw();
165
166 try jw.objectField("topPubs");
167 try jw.beginWriteRaw();
168 try jw.writer.writeAll(data.top_pubs_json);
169 jw.endWriteRaw();
170
171 try jw.endObject();
172 return try output.toOwnedSlice();
173}