From 019497a08df0882dfd9022a313c58c6baece398b Mon Sep 17 00:00:00 2001 From: ToastedToast Date: Mon, 2 Sep 2024 21:26:12 +0800 Subject: [PATCH] feat(api): add analytics --- api/package.json | 3 ++- api/src/analytics.ts | 24 ++++++++++++++++++++++++ api/src/index.ts | 3 +++ api/src/utils/db/postpush.ts | 17 +++++++++++++++++ api/src/utils/db/schema.ts | 13 ++++++++++++- bun.lockb | Bin 262288 -> 263312 bytes docker-compose.yml | 2 +- 7 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 api/src/analytics.ts create mode 100644 api/src/utils/db/postpush.ts diff --git a/api/package.json b/api/package.json index 9501e37..b399f4b 100644 --- a/api/package.json +++ b/api/package.json @@ -6,7 +6,7 @@ "dev": "BUN_CONFIG_NO_CLEAR_TERMINAL_ON_RELOAD=1 bun with-env bun --watch src/index.ts", "start": "bun with-env bun src/index.ts", "typecheck": "tsc --noEmit", - "db:push": "bun with-env drizzle-kit push", + "db:push": "bun with-env drizzle-kit push && bun with-env bun src/utils/db/postpush.ts", "db:studio": "bun with-env drizzle-kit studio", "with-env": "dotenv -e ../.env --" }, @@ -14,6 +14,7 @@ "@hono/trpc-server": "^0.3.2", "@hono/zod-openapi": "^0.15.1", "@trpc/server": "^10.45.2", + "cron": "^3.1.7", "drizzle-orm": "^0.32.1", "hono": "^4.5.2", "postgres": "^3.4.4", diff --git a/api/src/analytics.ts b/api/src/analytics.ts new file mode 100644 index 0000000..2a348ca --- /dev/null +++ b/api/src/analytics.ts @@ -0,0 +1,24 @@ +import { CronJob } from "cron"; +import { db } from "./utils/db"; +import { analytics } from "./utils/db/schema"; + +export async function createAnalyticsCronJob() { + new CronJob( + "* * * * *", + async () => { + const currentDate = new Date(); + const channels = await db.query.channels.findMany(); + await db.insert(analytics).values( + channels.map((channel) => ({ + timestamp: currentDate, + guildId: channel.guildId, + channelId: channel.id, + count: channel.count, + })) + ); + }, + null, + true, + "Africa/Abidjan" + ); +} diff --git a/api/src/index.ts b/api/src/index.ts index 7402579..25d0c83 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -4,9 +4,12 @@ import { Hono } from "hono"; import { appRouter } from "./router"; import { createContext } from "./utils/trpc"; import { restRouter } from "./rest"; +import { createAnalyticsCronJob } from "./analytics"; const app = new Hono(); +createAnalyticsCronJob(); + app.get("/", (c) => { return c.json({ message: "Hello World", diff --git a/api/src/utils/db/postpush.ts b/api/src/utils/db/postpush.ts new file mode 100644 index 0000000..fd83140 --- /dev/null +++ b/api/src/utils/db/postpush.ts @@ -0,0 +1,17 @@ +import { sql } from "drizzle-orm"; +import { db } from "."; + +async function main() { + try { + await db.execute( + sql`SELECT create_hypertable('analytics', by_range('timestamp'))` + ); + } catch (err) { + console.log( + "An error occured while creating hypertable. Hypertable probably already exists." + ); + } + process.exit(0); +} + +main(); diff --git a/api/src/utils/db/schema.ts b/api/src/utils/db/schema.ts index 626d05e..eca016b 100644 --- a/api/src/utils/db/schema.ts +++ b/api/src/utils/db/schema.ts @@ -1,5 +1,5 @@ import { relations } from "drizzle-orm"; -import { integer, pgTable, text } from "drizzle-orm/pg-core"; +import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; export const guilds = pgTable("guilds", { id: text("id").primaryKey(), @@ -25,3 +25,14 @@ export const channelRelations = relations(channels, ({ one }) => ({ references: [guilds.id], }), })); + +export const analytics = pgTable("analytics", { + timestamp: timestamp("timestamp", { withTimezone: true }).primaryKey(), + guildId: text("guild_id") + .notNull() + .references(() => guilds.id), + channelId: text("channel_id") + .notNull() + .references(() => channels.id), + count: integer("count").notNull().default(0), +}); diff --git a/bun.lockb b/bun.lockb index 7be2d6db378f336a8076710f817c438ea7e114af..e70f0a24833f6686d0d0b079567d140705d9a7a6 100755 GIT binary patch delta 6680 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 IYSfSZ4UmfM0{{R3 delta 7108 zcmeI1d3;pW702&;ZxS+K5{5kj1(X2UCYg!J5E2p+*}@(`*<_U{K>|b;2}lxLp)Mc7 z6Aq%KprwQ*ff&LNNPq-TkTK#?SL&yz2!RxhwiIlE{?42GX8e3gKepBOkLvqy_@4Va z=ia&ZzRTm?`&O@Nxq3y*)%J+2h&LZQzqaSrW);z)n;(7Oll%hx_IG8rHoNCu{4D!K z_-P?5g0BU|jMu6(F)98Cq#3Oqs;ZP7tC0cGiRmumvtk8p zH{9@QeO|~RQ=8=TO5hDO^1gwWX?Q7zP497^SMT%M*P793K5v`P``YIv*9kGmXzvl9 zcf#kjJYq)2!5d)Yz3B7K_`DuRWz_lrybO$2=#CPXwG<_uDol*SrCvX(Z?8@b7>v6< zQZKDe4j_2L4exWG=XllB3Vhy4ctef6cCYcUxb9S+SK{-Ud|sd9X5OPd&*Ssjo-m_R z;0-d`+v4*YeO~EN})GX@`8C<)o=)`n+dNPkra4zP&Eh+6oKPd;6OzUDi=3 z^~Q8qbVszy`VmU~QBw4#NSE~&ZwN8On5w2&mvt6OJdLWJhthVGI8)O(b_u08qh4NA z@1S}!geC)Kpp>G=L?^2o>nkFU-D*q1zb3IbK!`x_VNd~=gBn-_@^v#R`l9!`6ms-2 znZ@jg@~=S)_%f)1TO_xF+2Thp@xM7vhId3eiww?-4I+<1wrXewsIhe zau|K6mXS6CNRcQ zIpZ#}9sus9>5Oqy#TZYvYG4B8FeXwhV-khc0QXQHV=|p!OrdTEfvJ?wm_~0grc=Zr zUkYF{4dN#*Pmwt%i<@#4*su)f4E*5^gW9l{|>@A zWqPaT^XKq;BF-=JsMkC-rMpoO;`$Bm#l=Wu@GBMeAI23)E9R-K`$kEb-_G8bp=hb` z+xG`ji;)_?kbNXIJH5qC#L5uA(A_Uz*y5xX1kE8eht#aloKkaAxA|xyUWRzLF_wFY z>kg^i0{nk-J=7_aYOC?*L+Pbix4Dsta4ke5$U1}XrdO~U$QVT;l2Xn!5 zcc;{Dh0mKFzWPgzH%>Ru+veZ^sda+i89&dHHV_(aVTkCA!bUl)Oe#m8`VNu#T@ad$ z5RZJQH(zvx-yRitC?lZpWOsvgklI929z+w9WD~a|#QQGp;yuuKd(|EGitO%#GA|rj zpwt#n7B{g_hI%3tiC%N>7fG!b{MXSl?)+k@MZk|h9{2ho+RRNXk)bF^QRrY-FkfoX z@EuZHDzzAB@lsnxP29x82;q;2g}G#?K&E3)EIL8s@fS+X0Y4tS;H!wz7odq1G87Lf z0U;jiBT~BqzDuS*3Jrfm0<5>x9;ae%;t84VLWt*puP3F3{k^yo>HNzprIrLg9U3>k ziq4>kR=+gU^St$EE~D86Y7dHDsE(z_7OEvl{C0a5_63ah(mP-~VWlwMgKvWI&i*0T z5?DTLDQuZLcafT=M(`f(JgfnB0d^7A2>T;!JdAgx^{_YT#6#+sw!C6@!FW5*2LuJM zLO1SNN7c&PJ6R#bz;bnJbKbGYU_wmkVnFJY4anA zpELXza>Dq5!OxgPSOVp*RY%(Tz~$!yKR0+ajG_we$4P zhs}lM!uUb_0BjztNXK8eUX7s{>(m}tTEhM0I(1mvK(9`Y2DP=jafceW#GTk(TVqi| z=~^!>410w35!wp%^gh}Up-oY9s_5GYtxqTOyf&xvki?1){+gjGQ{wFjcJ#Eqibh3h z9TLs6?2xUVEm`#Y8OU%V15406%^q>^m9)i$*CUL&@ph*W6ZX?a)K$0ar+tx{6R-Fe zBegJf%K-|E(wvFrp>a-CO3598xnEe6IJ={_-5Dp$W94(}7Jj`s?eYPCq_~>qMxi_A zL3H9U&)&^z_AK>hc+aXuQC!A`sqVl~)gL)tP1UH|$voX&Y1uhzs;$owf5xS1`U)8d z=6U$>W}|4}`P@2xhIwM%;oEN=qhC+V_D3S9Z!{*jFP*V#Z32y9WKkX@HM@qMj>hkr zr}BDo&yb%Fisnpv>Bf7-Rq6gn2u+I72BiM^sP~ZDJoNtLlS9AS;jBqA z5|iu>r?}~@G9rc5^a*}G(flG%d$zcwb^Aeo@z=fStupFXR#UVc6Jvg6cxhX~>`Ou6 zZ~Nu7%Ks1uh4^=MH0`KubYDhzu? z+@&@Y8mk3#GC!EiX!kd3XqPsZ{mJwenx9yXUoPtM@Ne$Y{TVmCc}7q6)=&Z0 zH9y+~x9{Juv25{c{<=54c}CqUHRM5EZ0XO&YQvRg6d$L>C$7W+E&jtUdiKpNV9aeN>K>fu39=$jb?f0@B20vYybcN diff --git a/docker-compose.yml b/docker-compose.yml index 2866967..a351807 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: db: - image: postgres:16 + image: timescale/timescaledb-ha:pg16 restart: always environment: POSTGRES_PASSWORD: ${DATABASE_PASSWORD}