search for standard sites pub-search.waow.tech
search zig blog atproto
at multi-platform-schema 173 lines 5.5 kB view raw
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}