@take-out/cli 0.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (331) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +274 -0
  3. package/cli.mjs +3 -0
  4. package/dist/cjs/cli.cjs +71 -0
  5. package/dist/cjs/cli.js +70 -0
  6. package/dist/cjs/cli.js.map +6 -0
  7. package/dist/cjs/cli.native.js +79 -0
  8. package/dist/cjs/cli.native.js.map +6 -0
  9. package/dist/cjs/commands/changed.cjs +212 -0
  10. package/dist/cjs/commands/changed.js +214 -0
  11. package/dist/cjs/commands/changed.js.map +6 -0
  12. package/dist/cjs/commands/changed.native.js +289 -0
  13. package/dist/cjs/commands/changed.native.js.map +6 -0
  14. package/dist/cjs/commands/docs.cjs +388 -0
  15. package/dist/cjs/commands/docs.js +313 -0
  16. package/dist/cjs/commands/docs.js.map +6 -0
  17. package/dist/cjs/commands/docs.native.js +476 -0
  18. package/dist/cjs/commands/docs.native.js.map +6 -0
  19. package/dist/cjs/commands/env-setup.cjs +90 -0
  20. package/dist/cjs/commands/env-setup.js +78 -0
  21. package/dist/cjs/commands/env-setup.js.map +6 -0
  22. package/dist/cjs/commands/env-setup.native.js +85 -0
  23. package/dist/cjs/commands/env-setup.native.js.map +6 -0
  24. package/dist/cjs/commands/onboard.cjs +479 -0
  25. package/dist/cjs/commands/onboard.js +631 -0
  26. package/dist/cjs/commands/onboard.js.map +6 -0
  27. package/dist/cjs/commands/onboard.native.js +608 -0
  28. package/dist/cjs/commands/onboard.native.js.map +6 -0
  29. package/dist/cjs/commands/run.cjs +148 -0
  30. package/dist/cjs/commands/run.js +116 -0
  31. package/dist/cjs/commands/run.js.map +6 -0
  32. package/dist/cjs/commands/run.native.js +140 -0
  33. package/dist/cjs/commands/run.native.js.map +6 -0
  34. package/dist/cjs/commands/script.cjs +379 -0
  35. package/dist/cjs/commands/script.js +339 -0
  36. package/dist/cjs/commands/script.js.map +6 -0
  37. package/dist/cjs/commands/script.native.js +449 -0
  38. package/dist/cjs/commands/script.native.js.map +6 -0
  39. package/dist/cjs/commands/sync.cjs +190 -0
  40. package/dist/cjs/commands/sync.js +168 -0
  41. package/dist/cjs/commands/sync.js.map +6 -0
  42. package/dist/cjs/commands/sync.native.js +211 -0
  43. package/dist/cjs/commands/sync.native.js.map +6 -0
  44. package/dist/cjs/constants/ascii.cjs +36 -0
  45. package/dist/cjs/constants/ascii.js +30 -0
  46. package/dist/cjs/constants/ascii.js.map +6 -0
  47. package/dist/cjs/constants/ascii.native.js +36 -0
  48. package/dist/cjs/constants/ascii.native.js.map +6 -0
  49. package/dist/cjs/index.cjs +64 -0
  50. package/dist/cjs/index.js +55 -0
  51. package/dist/cjs/index.js.map +6 -0
  52. package/dist/cjs/index.native.js +94 -0
  53. package/dist/cjs/index.native.js.map +6 -0
  54. package/dist/cjs/types.cjs +16 -0
  55. package/dist/cjs/types.js +14 -0
  56. package/dist/cjs/types.js.map +6 -0
  57. package/dist/cjs/types.native.js +15 -0
  58. package/dist/cjs/types.native.js.map +6 -0
  59. package/dist/cjs/utils/env-categories.cjs +272 -0
  60. package/dist/cjs/utils/env-categories.js +296 -0
  61. package/dist/cjs/utils/env-categories.js.map +6 -0
  62. package/dist/cjs/utils/env-categories.native.js +317 -0
  63. package/dist/cjs/utils/env-categories.native.js.map +6 -0
  64. package/dist/cjs/utils/env-setup.cjs +181 -0
  65. package/dist/cjs/utils/env-setup.js +190 -0
  66. package/dist/cjs/utils/env-setup.js.map +6 -0
  67. package/dist/cjs/utils/env-setup.native.js +264 -0
  68. package/dist/cjs/utils/env-setup.native.js.map +6 -0
  69. package/dist/cjs/utils/env.cjs +118 -0
  70. package/dist/cjs/utils/env.js +97 -0
  71. package/dist/cjs/utils/env.js.map +6 -0
  72. package/dist/cjs/utils/env.native.js +128 -0
  73. package/dist/cjs/utils/env.native.js.map +6 -0
  74. package/dist/cjs/utils/files.cjs +215 -0
  75. package/dist/cjs/utils/files.js +164 -0
  76. package/dist/cjs/utils/files.js.map +6 -0
  77. package/dist/cjs/utils/files.native.js +266 -0
  78. package/dist/cjs/utils/files.native.js.map +6 -0
  79. package/dist/cjs/utils/parallel-runner.cjs +99 -0
  80. package/dist/cjs/utils/parallel-runner.js +84 -0
  81. package/dist/cjs/utils/parallel-runner.js.map +6 -0
  82. package/dist/cjs/utils/parallel-runner.native.js +123 -0
  83. package/dist/cjs/utils/parallel-runner.native.js.map +6 -0
  84. package/dist/cjs/utils/ports.cjs +101 -0
  85. package/dist/cjs/utils/ports.js +81 -0
  86. package/dist/cjs/utils/ports.js.map +6 -0
  87. package/dist/cjs/utils/ports.native.js +130 -0
  88. package/dist/cjs/utils/ports.native.js.map +6 -0
  89. package/dist/cjs/utils/prerequisites.cjs +119 -0
  90. package/dist/cjs/utils/prerequisites.js +107 -0
  91. package/dist/cjs/utils/prerequisites.js.map +6 -0
  92. package/dist/cjs/utils/prerequisites.native.js +127 -0
  93. package/dist/cjs/utils/prerequisites.native.js.map +6 -0
  94. package/dist/cjs/utils/prompts.cjs +161 -0
  95. package/dist/cjs/utils/prompts.js +162 -0
  96. package/dist/cjs/utils/prompts.js.map +6 -0
  97. package/dist/cjs/utils/prompts.native.js +179 -0
  98. package/dist/cjs/utils/prompts.native.js.map +6 -0
  99. package/dist/cjs/utils/script-listing.cjs +113 -0
  100. package/dist/cjs/utils/script-listing.js +108 -0
  101. package/dist/cjs/utils/script-listing.js.map +6 -0
  102. package/dist/cjs/utils/script-listing.native.js +174 -0
  103. package/dist/cjs/utils/script-listing.native.js.map +6 -0
  104. package/dist/cjs/utils/sync.cjs +85 -0
  105. package/dist/cjs/utils/sync.js +70 -0
  106. package/dist/cjs/utils/sync.js.map +6 -0
  107. package/dist/cjs/utils/sync.native.js +84 -0
  108. package/dist/cjs/utils/sync.native.js.map +6 -0
  109. package/dist/cjs/utils/welcome.cjs +50 -0
  110. package/dist/cjs/utils/welcome.js +42 -0
  111. package/dist/cjs/utils/welcome.js.map +6 -0
  112. package/dist/cjs/utils/welcome.native.js +47 -0
  113. package/dist/cjs/utils/welcome.native.js.map +6 -0
  114. package/dist/esm/cli.js +79 -0
  115. package/dist/esm/cli.js.map +6 -0
  116. package/dist/esm/cli.mjs +71 -0
  117. package/dist/esm/cli.mjs.map +1 -0
  118. package/dist/esm/cli.native.js +69 -0
  119. package/dist/esm/cli.native.js.map +1 -0
  120. package/dist/esm/commands/changed.js +194 -0
  121. package/dist/esm/commands/changed.js.map +6 -0
  122. package/dist/esm/commands/changed.mjs +178 -0
  123. package/dist/esm/commands/changed.mjs.map +1 -0
  124. package/dist/esm/commands/changed.native.js +273 -0
  125. package/dist/esm/commands/changed.native.js.map +1 -0
  126. package/dist/esm/commands/docs.js +306 -0
  127. package/dist/esm/commands/docs.js.map +6 -0
  128. package/dist/esm/commands/docs.mjs +353 -0
  129. package/dist/esm/commands/docs.mjs.map +1 -0
  130. package/dist/esm/commands/docs.native.js +516 -0
  131. package/dist/esm/commands/docs.native.js.map +1 -0
  132. package/dist/esm/commands/env-setup.js +56 -0
  133. package/dist/esm/commands/env-setup.js.map +6 -0
  134. package/dist/esm/commands/env-setup.mjs +56 -0
  135. package/dist/esm/commands/env-setup.mjs.map +1 -0
  136. package/dist/esm/commands/env-setup.native.js +59 -0
  137. package/dist/esm/commands/env-setup.native.js.map +1 -0
  138. package/dist/esm/commands/onboard.js +645 -0
  139. package/dist/esm/commands/onboard.js.map +6 -0
  140. package/dist/esm/commands/onboard.mjs +445 -0
  141. package/dist/esm/commands/onboard.mjs.map +1 -0
  142. package/dist/esm/commands/onboard.native.js +584 -0
  143. package/dist/esm/commands/onboard.native.js.map +1 -0
  144. package/dist/esm/commands/run.js +95 -0
  145. package/dist/esm/commands/run.js.map +6 -0
  146. package/dist/esm/commands/run.mjs +114 -0
  147. package/dist/esm/commands/run.mjs.map +1 -0
  148. package/dist/esm/commands/run.native.js +133 -0
  149. package/dist/esm/commands/run.native.js.map +1 -0
  150. package/dist/esm/commands/script.js +338 -0
  151. package/dist/esm/commands/script.js.map +6 -0
  152. package/dist/esm/commands/script.mjs +336 -0
  153. package/dist/esm/commands/script.mjs.map +1 -0
  154. package/dist/esm/commands/script.native.js +445 -0
  155. package/dist/esm/commands/script.native.js.map +1 -0
  156. package/dist/esm/commands/sync.js +158 -0
  157. package/dist/esm/commands/sync.js.map +6 -0
  158. package/dist/esm/commands/sync.mjs +155 -0
  159. package/dist/esm/commands/sync.mjs.map +1 -0
  160. package/dist/esm/commands/sync.native.js +173 -0
  161. package/dist/esm/commands/sync.native.js.map +1 -0
  162. package/dist/esm/constants/ascii.js +14 -0
  163. package/dist/esm/constants/ascii.js.map +6 -0
  164. package/dist/esm/constants/ascii.mjs +12 -0
  165. package/dist/esm/constants/ascii.mjs.map +1 -0
  166. package/dist/esm/constants/ascii.native.js +12 -0
  167. package/dist/esm/constants/ascii.native.js.map +1 -0
  168. package/dist/esm/index.js +83 -0
  169. package/dist/esm/index.js.map +6 -0
  170. package/dist/esm/index.mjs +7 -0
  171. package/dist/esm/index.mjs.map +1 -0
  172. package/dist/esm/index.native.js +7 -0
  173. package/dist/esm/index.native.js.map +1 -0
  174. package/dist/esm/types.js +1 -0
  175. package/dist/esm/types.js.map +6 -0
  176. package/dist/esm/types.mjs +2 -0
  177. package/dist/esm/types.mjs.map +1 -0
  178. package/dist/esm/types.native.js +2 -0
  179. package/dist/esm/types.native.js.map +1 -0
  180. package/dist/esm/utils/env-categories.js +272 -0
  181. package/dist/esm/utils/env-categories.js.map +6 -0
  182. package/dist/esm/utils/env-categories.mjs +233 -0
  183. package/dist/esm/utils/env-categories.mjs.map +1 -0
  184. package/dist/esm/utils/env-categories.native.js +246 -0
  185. package/dist/esm/utils/env-categories.native.js.map +1 -0
  186. package/dist/esm/utils/env-setup.js +173 -0
  187. package/dist/esm/utils/env-setup.js.map +6 -0
  188. package/dist/esm/utils/env-setup.mjs +146 -0
  189. package/dist/esm/utils/env-setup.mjs.map +1 -0
  190. package/dist/esm/utils/env-setup.native.js +243 -0
  191. package/dist/esm/utils/env-setup.native.js.map +1 -0
  192. package/dist/esm/utils/env.js +83 -0
  193. package/dist/esm/utils/env.js.map +6 -0
  194. package/dist/esm/utils/env.mjs +90 -0
  195. package/dist/esm/utils/env.mjs.map +1 -0
  196. package/dist/esm/utils/env.native.js +99 -0
  197. package/dist/esm/utils/env.native.js.map +1 -0
  198. package/dist/esm/utils/files.js +150 -0
  199. package/dist/esm/utils/files.js.map +6 -0
  200. package/dist/esm/utils/files.mjs +187 -0
  201. package/dist/esm/utils/files.mjs.map +1 -0
  202. package/dist/esm/utils/files.native.js +247 -0
  203. package/dist/esm/utils/files.native.js.map +1 -0
  204. package/dist/esm/utils/parallel-runner.js +69 -0
  205. package/dist/esm/utils/parallel-runner.js.map +6 -0
  206. package/dist/esm/utils/parallel-runner.mjs +76 -0
  207. package/dist/esm/utils/parallel-runner.mjs.map +1 -0
  208. package/dist/esm/utils/parallel-runner.native.js +109 -0
  209. package/dist/esm/utils/parallel-runner.native.js.map +1 -0
  210. package/dist/esm/utils/ports.js +65 -0
  211. package/dist/esm/utils/ports.js.map +6 -0
  212. package/dist/esm/utils/ports.mjs +74 -0
  213. package/dist/esm/utils/ports.mjs.map +1 -0
  214. package/dist/esm/utils/ports.native.js +93 -0
  215. package/dist/esm/utils/ports.native.js.map +1 -0
  216. package/dist/esm/utils/prerequisites.js +91 -0
  217. package/dist/esm/utils/prerequisites.js.map +6 -0
  218. package/dist/esm/utils/prerequisites.mjs +91 -0
  219. package/dist/esm/utils/prerequisites.mjs.map +1 -0
  220. package/dist/esm/utils/prerequisites.native.js +97 -0
  221. package/dist/esm/utils/prerequisites.native.js.map +1 -0
  222. package/dist/esm/utils/prompts.js +139 -0
  223. package/dist/esm/utils/prompts.js.map +6 -0
  224. package/dist/esm/utils/prompts.mjs +112 -0
  225. package/dist/esm/utils/prompts.mjs.map +1 -0
  226. package/dist/esm/utils/prompts.native.js +115 -0
  227. package/dist/esm/utils/prompts.native.js.map +1 -0
  228. package/dist/esm/utils/script-listing.js +91 -0
  229. package/dist/esm/utils/script-listing.js.map +6 -0
  230. package/dist/esm/utils/script-listing.mjs +76 -0
  231. package/dist/esm/utils/script-listing.mjs.map +1 -0
  232. package/dist/esm/utils/script-listing.native.js +151 -0
  233. package/dist/esm/utils/script-listing.native.js.map +1 -0
  234. package/dist/esm/utils/sync.js +50 -0
  235. package/dist/esm/utils/sync.js.map +6 -0
  236. package/dist/esm/utils/sync.mjs +48 -0
  237. package/dist/esm/utils/sync.mjs.map +1 -0
  238. package/dist/esm/utils/sync.native.js +53 -0
  239. package/dist/esm/utils/sync.native.js.map +1 -0
  240. package/dist/esm/utils/welcome.js +21 -0
  241. package/dist/esm/utils/welcome.js.map +6 -0
  242. package/dist/esm/utils/welcome.mjs +15 -0
  243. package/dist/esm/utils/welcome.mjs.map +1 -0
  244. package/dist/esm/utils/welcome.native.js +18 -0
  245. package/dist/esm/utils/welcome.native.js.map +1 -0
  246. package/docs/aggregates.md +579 -0
  247. package/docs/cloudflare-dev-tunnel.md +41 -0
  248. package/docs/database.md +203 -0
  249. package/docs/docs.md +8 -0
  250. package/docs/emitters.md +562 -0
  251. package/docs/hot-updater.md +223 -0
  252. package/docs/native-hot-update.md +252 -0
  253. package/docs/one-components.md +234 -0
  254. package/docs/one-hooks.md +570 -0
  255. package/docs/one-routes.md +660 -0
  256. package/docs/package-json.md +115 -0
  257. package/docs/react-native-navigation-flow.md +184 -0
  258. package/docs/scripts.md +147 -0
  259. package/docs/sync-prompt.md +208 -0
  260. package/docs/tamagui.md +478 -0
  261. package/docs/testing-integration.md +564 -0
  262. package/docs/triggers.md +450 -0
  263. package/docs/zero.md +719 -0
  264. package/package.json +76 -0
  265. package/scripts/seed.ts +209 -0
  266. package/src/cli.ts +147 -0
  267. package/src/commands/changed.ts +313 -0
  268. package/src/commands/docs.ts +582 -0
  269. package/src/commands/env-setup.ts +69 -0
  270. package/src/commands/onboard.ts +1391 -0
  271. package/src/commands/run.ts +173 -0
  272. package/src/commands/script.ts +587 -0
  273. package/src/commands/sync.ts +305 -0
  274. package/src/constants/ascii.ts +17 -0
  275. package/src/index.ts +63 -0
  276. package/src/types.ts +59 -0
  277. package/src/utils/env-categories.ts +245 -0
  278. package/src/utils/env-setup.ts +338 -0
  279. package/src/utils/env.ts +127 -0
  280. package/src/utils/files.ts +302 -0
  281. package/src/utils/parallel-runner.ts +129 -0
  282. package/src/utils/ports.ts +77 -0
  283. package/src/utils/prerequisites.ts +137 -0
  284. package/src/utils/prompts.ts +197 -0
  285. package/src/utils/script-listing.ts +214 -0
  286. package/src/utils/sync.ts +101 -0
  287. package/src/withOpSqliteStatic.cjs +51 -0
  288. package/types/cli.d.ts +7 -0
  289. package/types/cli.d.ts.map +1 -0
  290. package/types/commands/changed.d.ts +14 -0
  291. package/types/commands/changed.d.ts.map +1 -0
  292. package/types/commands/docs.d.ts +5 -0
  293. package/types/commands/docs.d.ts.map +1 -0
  294. package/types/commands/env-setup.d.ts +25 -0
  295. package/types/commands/env-setup.d.ts.map +1 -0
  296. package/types/commands/onboard.d.ts +16 -0
  297. package/types/commands/onboard.d.ts.map +1 -0
  298. package/types/commands/run.d.ts +8 -0
  299. package/types/commands/run.d.ts.map +1 -0
  300. package/types/commands/script.d.ts +28 -0
  301. package/types/commands/script.d.ts.map +1 -0
  302. package/types/commands/sync.d.ts +5 -0
  303. package/types/commands/sync.d.ts.map +1 -0
  304. package/types/constants/ascii.d.ts +6 -0
  305. package/types/constants/ascii.d.ts.map +1 -0
  306. package/types/index.d.ts +12 -0
  307. package/types/index.d.ts.map +1 -0
  308. package/types/types.d.ts +54 -0
  309. package/types/types.d.ts.map +1 -0
  310. package/types/utils/env-categories.d.ts +8 -0
  311. package/types/utils/env-categories.d.ts.map +1 -0
  312. package/types/utils/env-setup.d.ts +10 -0
  313. package/types/utils/env-setup.d.ts.map +1 -0
  314. package/types/utils/env.d.ts +19 -0
  315. package/types/utils/env.d.ts.map +1 -0
  316. package/types/utils/files.d.ts +47 -0
  317. package/types/utils/files.d.ts.map +1 -0
  318. package/types/utils/parallel-runner.d.ts +15 -0
  319. package/types/utils/parallel-runner.d.ts.map +1 -0
  320. package/types/utils/ports.d.ts +16 -0
  321. package/types/utils/ports.d.ts.map +1 -0
  322. package/types/utils/prerequisites.d.ts +11 -0
  323. package/types/utils/prerequisites.d.ts.map +1 -0
  324. package/types/utils/prompts.d.ts +30 -0
  325. package/types/utils/prompts.d.ts.map +1 -0
  326. package/types/utils/script-listing.d.ts +7 -0
  327. package/types/utils/script-listing.d.ts.map +1 -0
  328. package/types/utils/sync.d.ts +16 -0
  329. package/types/utils/sync.d.ts.map +1 -0
  330. package/types/utils/welcome.d.ts +6 -0
  331. package/types/utils/welcome.d.ts.map +1 -0
@@ -0,0 +1,579 @@
1
+ ---
2
+ name: aggregates
3
+ description: Database aggregates guide using PostgreSQL triggers and stats tables. INVOKE WHEN: commentCount, followerCount, likesCount, reactionCount, replyCount, derived columns, computed values, denormalized data, INSERT ON CONFLICT, UPSERT pattern, maintaining counts, syncing counts, aggregate tables, stats tables.
4
+ ---
5
+
6
+ # Aggregates in your data
7
+
8
+ Zero doesn't support aggregates, and running them on demand is often slow
9
+ anyway. We use Postgres triggers and tables to handle things like counting
10
+ replies or reactions. Note that we don't use materialized views since Zero
11
+ doesn't support them.
12
+
13
+ While these patterns seem complex, LLMs handle generating and maintaining them
14
+ well. In a future iteration we want to standardize how to define them and
15
+ automate migrations.
16
+
17
+ This guide explains how to implement efficient aggregate statistics using
18
+ PostgreSQL triggers, based on the privateChatsStats pattern used in this
19
+ codebase.
20
+
21
+ ## Overview
22
+
23
+ Non-materialized aggregate triggers maintain summary statistics in real-time by:
24
+
25
+ 1. Creating a dedicated stats table to store aggregated data
26
+ 2. Using triggers to incrementally update stats when source data changes
27
+ 3. Avoiding expensive full-table recalculations on every query
28
+
29
+ Use this approach when:
30
+
31
+ - You need real-time statistics that are always up-to-date
32
+ - The source data changes frequently but not massively
33
+ - You want to avoid the complexity and storage overhead of materialized views
34
+ - You need granular control over the aggregation logic
35
+
36
+ ## Step 1: Design Your Stats Table
37
+
38
+ First, identify what statistics you need to track. Consider:
39
+
40
+ - Primary keys that uniquely identify each aggregate row
41
+ - Foreign keys to maintain referential integrity
42
+ - Aggregate columns (counts, sums, averages, etc.)
43
+ - Time-based aggregates (weekly, monthly, yearly counts)
44
+ - Metadata (lastUpdatedAt, score calculations, etc.)
45
+
46
+ ### Example: privateChatsStats
47
+
48
+ ```sql
49
+ CREATE TABLE "privateChatsStats" (
50
+ "serverId" varchar NOT NULL REFERENCES "server"(id) ON DELETE CASCADE,
51
+ "userServerId" varchar NOT NULL REFERENCES "server"(id) ON DELETE CASCADE,
52
+ "userAId" varchar NOT NULL REFERENCES "userPublic"(id) ON DELETE CASCADE,
53
+ "userBId" varchar NOT NULL REFERENCES "userPublic"(id) ON DELETE CASCADE,
54
+ "score" integer NOT NULL DEFAULT 0,
55
+ "lastMessageAt" timestamp NOT NULL,
56
+ "messageCountWeek" integer NOT NULL DEFAULT 0,
57
+ "messageCountMonth" integer NOT NULL DEFAULT 0,
58
+ "messageCountYear" integer NOT NULL DEFAULT 0,
59
+ "lastRefreshedAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
60
+ PRIMARY KEY ("serverId", "userServerId")
61
+ );
62
+
63
+ CREATE INDEX "idx_privateChatsStats_server"
64
+ ON "privateChatsStats" ("serverId", "score");
65
+ CREATE INDEX "idx_privateChatsStats_userA"
66
+ ON "privateChatsStats" ("serverId", "userAId", "score");
67
+ CREATE INDEX "idx_privateChatsStats_userB"
68
+ ON "privateChatsStats" ("serverId", "userBId", "score");
69
+ ```
70
+
71
+ ## Step 2: Initial Population
72
+
73
+ Populate the stats table with existing data. This is a one-time operation that
74
+ calculates all historical statistics.
75
+
76
+ Key patterns:
77
+
78
+ - Use CTEs (WITH clauses) to structure complex queries
79
+ - Apply business logic for scoring/ranking
80
+ - Handle edge cases (NULL values, missing relationships)
81
+
82
+ ```sql
83
+ INSERT INTO "privateChatsStats"
84
+ WITH private_chat_stats AS (
85
+ SELECT DISTINCT
86
+ sm1."serverId" as parent_server_id,
87
+ pcs.id as user_server_id,
88
+ LEAST(pcs."userId", pcs."friendId") as user_a_id,
89
+ GREATEST(pcs."userId", pcs."friendId") as user_b_id,
90
+ COUNT(DISTINCT m.id) as total_messages,
91
+ MAX(m."createdAt") as last_message_at,
92
+ COUNT(DISTINCT CASE
93
+ WHEN m."createdAt" >= CURRENT_TIMESTAMP - INTERVAL '7 days'
94
+ THEN m.id
95
+ END) as message_count_week,
96
+ COUNT(DISTINCT CASE
97
+ WHEN m."createdAt" >= CURRENT_TIMESTAMP - INTERVAL '30 days'
98
+ THEN m.id
99
+ END) as message_count_month,
100
+ COUNT(DISTINCT CASE
101
+ WHEN m."createdAt" >= CURRENT_TIMESTAMP - INTERVAL '365 days'
102
+ THEN m.id
103
+ END) as message_count_year
104
+ FROM server pcs
105
+ INNER JOIN "serverMember" sm1 ON sm1."userId" = pcs."userId"
106
+ INNER JOIN "serverMember" sm2 ON sm2."userId" = pcs."friendId"
107
+ AND sm2."serverId" = sm1."serverId"
108
+ LEFT JOIN channel c ON c."serverId" = pcs.id
109
+ LEFT JOIN message m ON m."channelId" = c.id
110
+ AND m.deleted = false
111
+ AND m.type != 'draft'
112
+ WHERE pcs."isPrivateChat" = true
113
+ AND pcs."userId" IS NOT NULL
114
+ AND pcs."friendId" IS NOT NULL
115
+ AND sm1."serverId" != pcs.id
116
+ GROUP BY sm1."serverId", pcs.id, pcs."userId", pcs."friendId"
117
+ )
118
+ SELECT
119
+ parent_server_id as "serverId",
120
+ user_server_id as "userServerId",
121
+ user_a_id as "userAId",
122
+ user_b_id as "userBId",
123
+ CASE
124
+ WHEN message_count_week > 0
125
+ THEN message_count_week * 10 + message_count_month
126
+ ELSE message_count_month * 2 + message_count_year
127
+ END::integer as score,
128
+ COALESCE(last_message_at, CURRENT_TIMESTAMP) as "lastMessageAt",
129
+ message_count_week::integer as "messageCountWeek",
130
+ message_count_month::integer as "messageCountMonth",
131
+ message_count_year::integer as "messageCountYear",
132
+ CURRENT_TIMESTAMP as "lastRefreshedAt"
133
+ FROM private_chat_stats
134
+ WHERE parent_server_id IS NOT NULL;
135
+ ```
136
+
137
+ ## Step 3: Create the Trigger Function
138
+
139
+ The trigger function contains the logic for incrementally updating statistics.
140
+
141
+ Critical considerations:
142
+
143
+ 1. Performance: Minimize database operations
144
+ 2. Correctness: Handle all edge cases
145
+ 3. Idempotency: Updates should be consistent regardless of order
146
+ 4. Atomicity: Use proper transaction handling
147
+
148
+ ```sql
149
+ CREATE OR REPLACE FUNCTION update_private_chats_stats_on_message()
150
+ RETURNS TRIGGER AS $$
151
+ DECLARE
152
+ private_server RECORD;
153
+ parent_server_id varchar;
154
+ new_week_count integer;
155
+ new_month_count integer;
156
+ new_year_count integer;
157
+ BEGIN
158
+ IF NEW.type = 'draft' OR NEW.deleted = true THEN
159
+ RETURN NEW;
160
+ END IF;
161
+
162
+ SELECT s.* INTO private_server
163
+ FROM server s
164
+ INNER JOIN channel c ON c."serverId" = s.id
165
+ WHERE c.id = NEW."channelId"
166
+ AND s."isPrivateChat" = true
167
+ AND s."userId" IS NOT NULL
168
+ AND s."friendId" IS NOT NULL;
169
+
170
+ IF private_server IS NULL THEN
171
+ RETURN NEW;
172
+ END IF;
173
+
174
+ FOR parent_server_id IN
175
+ SELECT DISTINCT sm1."serverId"
176
+ FROM "serverMember" sm1
177
+ INNER JOIN "serverMember" sm2 ON sm2."serverId" = sm1."serverId"
178
+ WHERE sm1."userId" = private_server."userId"
179
+ AND sm2."userId" = private_server."friendId"
180
+ AND sm1."serverId" != private_server.id
181
+ LOOP
182
+ SELECT
183
+ COALESCE("messageCountWeek", 0) + CASE
184
+ WHEN NEW."createdAt" >= CURRENT_TIMESTAMP - INTERVAL '7 days' THEN 1
185
+ ELSE 0
186
+ END,
187
+ COALESCE("messageCountMonth", 0) + CASE
188
+ WHEN NEW."createdAt" >= CURRENT_TIMESTAMP - INTERVAL '30 days' THEN 1
189
+ ELSE 0
190
+ END,
191
+ COALESCE("messageCountYear", 0) + CASE
192
+ WHEN NEW."createdAt" >= CURRENT_TIMESTAMP - INTERVAL '365 days' THEN 1
193
+ ELSE 0
194
+ END
195
+ INTO new_week_count, new_month_count, new_year_count
196
+ FROM "privateChatsStats"
197
+ WHERE "serverId" = parent_server_id
198
+ AND "userServerId" = private_server.id;
199
+
200
+ IF new_week_count IS NULL THEN
201
+ new_week_count := CASE
202
+ WHEN NEW."createdAt" >= CURRENT_TIMESTAMP - INTERVAL '7 days'
203
+ THEN 1 ELSE 0 END;
204
+ new_month_count := CASE
205
+ WHEN NEW."createdAt" >= CURRENT_TIMESTAMP - INTERVAL '30 days'
206
+ THEN 1 ELSE 0 END;
207
+ new_year_count := CASE
208
+ WHEN NEW."createdAt" >= CURRENT_TIMESTAMP - INTERVAL '365 days'
209
+ THEN 1 ELSE 0 END;
210
+ END IF;
211
+
212
+ INSERT INTO "privateChatsStats" (
213
+ "serverId", "userServerId", "userAId", "userBId",
214
+ score, "lastMessageAt", "messageCountWeek",
215
+ "messageCountMonth", "messageCountYear", "lastRefreshedAt"
216
+ )
217
+ VALUES (
218
+ parent_server_id,
219
+ private_server.id,
220
+ LEAST(private_server."userId", private_server."friendId"),
221
+ GREATEST(private_server."userId", private_server."friendId"),
222
+ CASE
223
+ WHEN new_week_count > 0
224
+ THEN new_week_count * 10 + new_month_count
225
+ ELSE new_month_count * 2 + new_year_count
226
+ END,
227
+ NEW."createdAt",
228
+ new_week_count,
229
+ new_month_count,
230
+ new_year_count,
231
+ CURRENT_TIMESTAMP
232
+ )
233
+ ON CONFLICT ("serverId", "userServerId") DO UPDATE SET
234
+ "lastMessageAt" = GREATEST("privateChatsStats"."lastMessageAt", NEW."createdAt"),
235
+ "messageCountWeek" = new_week_count,
236
+ "messageCountMonth" = new_month_count,
237
+ "messageCountYear" = new_year_count,
238
+ score = CASE
239
+ WHEN new_week_count > 0
240
+ THEN new_week_count * 10 + new_month_count
241
+ ELSE new_month_count * 2 + new_year_count
242
+ END,
243
+ "lastRefreshedAt" = CURRENT_TIMESTAMP;
244
+ END LOOP;
245
+
246
+ RETURN NEW;
247
+ END;
248
+ $$ LANGUAGE plpgsql;
249
+ ```
250
+
251
+ ## Step 4: Create the Trigger
252
+
253
+ ```sql
254
+ CREATE TRIGGER update_private_chats_stats_on_message_trigger
255
+ AFTER INSERT ON message
256
+ FOR EACH ROW
257
+ EXECUTE FUNCTION update_private_chats_stats_on_message();
258
+ ```
259
+
260
+ ## Step 5: Maintenance Functions
261
+
262
+ Create utility functions for maintenance tasks:
263
+
264
+ - Full refresh when data gets out of sync
265
+ - Cleanup of stale statistics
266
+ - Periodic recalculation of time-based aggregates
267
+
268
+ ### Full Refresh Function
269
+
270
+ ```sql
271
+ CREATE OR REPLACE FUNCTION "refreshPrivateChatsStats"()
272
+ RETURNS void AS $$
273
+ BEGIN
274
+ TRUNCATE TABLE "privateChatsStats";
275
+ INSERT INTO "privateChatsStats"
276
+ WITH private_chat_stats AS (
277
+ -- same CTE as initial population
278
+ )
279
+ SELECT
280
+ -- same selection as initial population
281
+ FROM private_chat_stats
282
+ WHERE parent_server_id IS NOT NULL;
283
+ END;
284
+ $$ LANGUAGE plpgsql;
285
+ ```
286
+
287
+ ### Periodic Update Function
288
+
289
+ ```sql
290
+ CREATE OR REPLACE FUNCTION "updateTimeBasedAggregates"()
291
+ RETURNS void AS $$
292
+ BEGIN
293
+ UPDATE "privateChatsStats" pcs
294
+ SET
295
+ "messageCountWeek" = (
296
+ SELECT COUNT(*)
297
+ FROM message m
298
+ INNER JOIN channel c ON c.id = m."channelId"
299
+ WHERE c."serverId" = pcs."userServerId"
300
+ AND m.type != 'draft'
301
+ AND m.deleted = false
302
+ AND m."createdAt" >= CURRENT_TIMESTAMP - INTERVAL '7 days'
303
+ ),
304
+ "messageCountMonth" = (
305
+ SELECT COUNT(*)
306
+ FROM message m
307
+ INNER JOIN channel c ON c.id = m."channelId"
308
+ WHERE c."serverId" = pcs."userServerId"
309
+ AND m.type != 'draft'
310
+ AND m.deleted = false
311
+ AND m."createdAt" >= CURRENT_TIMESTAMP - INTERVAL '30 days'
312
+ ),
313
+ "messageCountYear" = (
314
+ SELECT COUNT(*)
315
+ FROM message m
316
+ INNER JOIN channel c ON c.id = m."channelId"
317
+ WHERE c."serverId" = pcs."userServerId"
318
+ AND m.type != 'draft'
319
+ AND m.deleted = false
320
+ AND m."createdAt" >= CURRENT_TIMESTAMP - INTERVAL '365 days'
321
+ ),
322
+ score = CASE
323
+ WHEN "messageCountWeek" > 0
324
+ THEN "messageCountWeek" * 10 + "messageCountMonth"
325
+ ELSE "messageCountMonth" * 2 + "messageCountYear"
326
+ END,
327
+ "lastRefreshedAt" = CURRENT_TIMESTAMP
328
+ WHERE "lastRefreshedAt" < CURRENT_TIMESTAMP - INTERVAL '1 day';
329
+ END;
330
+ $$ LANGUAGE plpgsql;
331
+ ```
332
+
333
+ ## LLM Prompting Guide
334
+
335
+ When asking an LLM to implement aggregate triggers, use this template:
336
+
337
+ ```markdown
338
+ I need to implement a non-materialized aggregate trigger system in PostgreSQL.
339
+
340
+ Context:
341
+
342
+ - Source table: [describe your source table and its purpose]
343
+ - Aggregation needs: [what statistics do you need to track]
344
+ - Update frequency: [how often does source data change]
345
+ - Query patterns: [how will the aggregated data be queried]
346
+
347
+ Requirements:
348
+
349
+ 1. Create a stats table that stores aggregated data efficiently
350
+ 2. Implement triggers that incrementally update statistics when source data
351
+ changes
352
+ 3. Avoid full table scans on every update - use incremental calculations
353
+ 4. Handle edge cases: NULL values, deletions, updates to existing records
354
+ 5. Include proper indexes for query performance
355
+ 6. Provide a full refresh function for maintenance
356
+
357
+ Specific statistics needed:
358
+
359
+ - [List each aggregate: counts, sums, averages, time-based aggregates]
360
+ - [Scoring/ranking algorithms if applicable]
361
+ - [Relationships between entities]
362
+
363
+ Performance constraints:
364
+
365
+ - Trigger execution should be < [X]ms
366
+ - Stats queries should return in < [Y]ms
367
+ - Support [Z] concurrent updates
368
+
369
+ Please provide:
370
+
371
+ 1. CREATE TABLE statement for the stats table with appropriate indexes
372
+ 2. Initial population query using CTEs
373
+ 3. Trigger function with incremental update logic
374
+ 4. CREATE TRIGGER statement
375
+ 5. Maintenance functions (full refresh, cleanup)
376
+ 6. Migration script structure (up/down functions)
377
+
378
+ Follow these patterns:
379
+
380
+ - Use LEAST/GREATEST for consistent ordering of paired values
381
+ - Use ON CONFLICT ... DO UPDATE for upserts
382
+ - Calculate time-based aggregates using CASE statements
383
+ - Return NEW from trigger functions for AFTER triggers
384
+ - Include proper error handling and NULL checks
385
+ ```
386
+
387
+ ## Best Practices
388
+
389
+ ### Performance
390
+
391
+ - Use early exits in trigger functions to skip irrelevant records
392
+ - Batch updates when possible using `INSERT ... ON CONFLICT`
393
+ - Create indexes on foreign keys and commonly queried columns
394
+ - Consider partitioning stats tables for very large datasets
395
+ - Use `EXPLAIN ANALYZE` to optimize trigger performance
396
+
397
+ ### Correctness
398
+
399
+ - Always normalize data order (e.g., `LEAST/GREATEST` for user pairs)
400
+ - Handle NULL values explicitly in all calculations
401
+ - Use transactions for complex multi-step updates
402
+ - Test edge cases: first record, deletions, updates
403
+ - Implement idempotent operations where possible
404
+
405
+ ### Maintenance
406
+
407
+ - Create a full refresh function for recovering from corruption
408
+ - Implement periodic cleanup for time-based aggregates
409
+ - Monitor trigger execution time and table bloat
410
+ - Document the scoring/ranking algorithms clearly
411
+ - Version your trigger functions with migration scripts
412
+
413
+ ### Testing
414
+
415
+ - Write integration tests that verify trigger behavior
416
+ - Test concurrent updates to ensure data consistency
417
+ - Verify that stats remain accurate after bulk operations
418
+ - Test the full refresh function against production-like data
419
+ - Benchmark trigger performance under load
420
+
421
+ ## Troubleshooting
422
+
423
+ ### Stats are out of sync
424
+
425
+ - Diagnosis: Run a query comparing aggregated stats with actual counts
426
+ - Solution: Execute the full refresh function, investigate trigger failures
427
+ - Prevention: Add monitoring queries, implement periodic validation
428
+
429
+ ### Trigger is too slow
430
+
431
+ - Diagnosis: Use `EXPLAIN ANALYZE` on the trigger function queries
432
+ - Solution: Add missing indexes, simplify calculations, consider async
433
+ processing
434
+ - Prevention: Profile trigger performance before production deployment
435
+
436
+ ### Time-based aggregates are incorrect
437
+
438
+ - Diagnosis: Check timezone handling, verify INTERVAL calculations
439
+ - Solution: Run the updateTimeBasedAggregates function, fix timezone issues
440
+ - Prevention: Use consistent timezone handling (preferably UTC)
441
+
442
+ ### Deadlocks during concurrent updates
443
+
444
+ - Diagnosis: Check `pg_stat_activity` and `deadlock_timeout` settings
445
+ - Solution: Reorder operations to acquire locks consistently
446
+ - Prevention: Use advisory locks or queue-based processing for high contention
447
+
448
+ ### Stats table is growing too large
449
+
450
+ - Diagnosis: Check for orphaned records, analyze table bloat
451
+ - Solution: Implement cleanup for deleted entities, `VACUUM` regularly
452
+ - Prevention: Add `ON DELETE CASCADE`, implement retention policies
453
+
454
+ ## Migration Structure
455
+
456
+ ```typescript
457
+ import type { PoolClient } from 'pg'
458
+
459
+ export async function up(client: PoolClient) {
460
+ await client.query(`...`) // 1. create the stats table
461
+ await client.query(`...`) // 2. create indexes
462
+ await client.query(`...`) // 3. populate initial data
463
+ await client.query(`...`) // 4. create trigger function
464
+ await client.query(`...`) // 5. create trigger
465
+ await client.query(`...`) // 6. create maintenance functions
466
+ }
467
+ ```
468
+
469
+ ## Advanced Patterns
470
+
471
+ ### Multi-Table Aggregates
472
+
473
+ ```sql
474
+ -- aggregate data from multiple related tables
475
+ -- use JOINs carefully to avoid cartesian products
476
+ -- consider using LATERAL joins for correlated subqueries
477
+ ```
478
+
479
+ ### Conditional Aggregates
480
+
481
+ ```sql
482
+ -- use FILTER clauses in PostgreSQL 9.4+
483
+ COUNT(*) FILTER (WHERE condition) as conditional_count
484
+
485
+ -- or use CASE statements for older versions
486
+ COUNT(CASE WHEN condition THEN 1 END) as conditional_count
487
+ ```
488
+
489
+ ### Window Functions
490
+
491
+ ```sql
492
+ -- use window functions for ranking within groups
493
+ ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY score DESC) as rank
494
+
495
+ -- calculate running totals
496
+ SUM(amount) OVER (ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
497
+ ```
498
+
499
+ ### Recursive Aggregates
500
+
501
+ ```sql
502
+ -- use recursive CTEs for hierarchical data
503
+ WITH RECURSIVE hierarchy AS (
504
+ SELECT id, parent_id, value FROM nodes WHERE parent_id IS NULL
505
+ UNION ALL
506
+ SELECT n.id, n.parent_id, n.value + h.value
507
+ FROM nodes n
508
+ INNER JOIN hierarchy h ON n.parent_id = h.id
509
+ )
510
+ SELECT * FROM hierarchy;
511
+ ```
512
+
513
+ ## Complete Example: Channel User Statistics
514
+
515
+ Here's a simplified example showing all pieces working together:
516
+
517
+ ### Stats Table
518
+
519
+ ```sql
520
+ CREATE TABLE channel_user_stats (
521
+ channel_id UUID REFERENCES channels(id) ON DELETE CASCADE,
522
+ user_id UUID REFERENCES users(id) ON DELETE CASCADE,
523
+ message_count INTEGER NOT NULL DEFAULT 0,
524
+ last_message_at TIMESTAMP,
525
+ first_message_at TIMESTAMP,
526
+ PRIMARY KEY (channel_id, user_id)
527
+ );
528
+
529
+ CREATE INDEX idx_channel_user_stats_user
530
+ ON channel_user_stats(user_id, message_count DESC);
531
+ ```
532
+
533
+ ### Trigger Function
534
+
535
+ ```sql
536
+ CREATE OR REPLACE FUNCTION update_channel_user_stats()
537
+ RETURNS TRIGGER AS $$
538
+ BEGIN
539
+ IF TG_OP = 'INSERT' THEN
540
+ INSERT INTO channel_user_stats (
541
+ channel_id, user_id, message_count,
542
+ last_message_at, first_message_at
543
+ )
544
+ VALUES (
545
+ NEW.channel_id, NEW.user_id, 1,
546
+ NEW.created_at, NEW.created_at
547
+ )
548
+ ON CONFLICT (channel_id, user_id) DO UPDATE SET
549
+ message_count = channel_user_stats.message_count + 1,
550
+ last_message_at = NEW.created_at,
551
+ first_message_at = COALESCE(
552
+ channel_user_stats.first_message_at,
553
+ NEW.created_at
554
+ );
555
+ ELSIF TG_OP = 'DELETE' THEN
556
+ UPDATE channel_user_stats
557
+ SET message_count = GREATEST(0, message_count - 1)
558
+ WHERE channel_id = OLD.channel_id AND user_id = OLD.user_id;
559
+ END IF;
560
+ RETURN NULL;
561
+ END;
562
+ $$ LANGUAGE plpgsql;
563
+ ```
564
+
565
+ ### Trigger
566
+
567
+ ```sql
568
+ CREATE TRIGGER trigger_update_channel_user_stats
569
+ AFTER INSERT OR DELETE ON messages
570
+ FOR EACH ROW
571
+ EXECUTE FUNCTION update_channel_user_stats();
572
+ ```
573
+
574
+ ## Conclusion
575
+
576
+ Non-materialized aggregate triggers provide a powerful pattern for maintaining
577
+ real-time statistics in PostgreSQL. By following this guide and the
578
+ privateChatsStats implementation pattern, you can build efficient, maintainable
579
+ aggregate systems that scale with your application's needs.
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: cloudflare-dev-tunnel
3
+ description: Cloudflare dev tunnel guide for exposing local development servers publicly. INVOKE WHEN: dev tunnel, cloudflare tunnel, cfargotunnel, dev:tunnel, local tunnel, testing webhooks, webhook testing, share local server, expose localhost, ngrok alternative.
4
+ ---
5
+
6
+ # Development Tunnel
7
+
8
+ The dev tunnel feature provides a stable public URL for your local development
9
+ server, perfect for testing webhooks and sharing your dev environment with team
10
+ members.
11
+
12
+ ## Features
13
+
14
+ Each developer gets their own persistent tunnel URL using Cloudflare Tunnel
15
+ (cloudflared) installed via npm. Single command to start. Webhooks automatically
16
+ use the tunnel URL when available.
17
+
18
+ ## Setup
19
+
20
+ Run `bun install`, then run `bun dev:tunnel` once to set up cloudflare. After
21
+ that you can just run your dev server as normal with `bun dev`.
22
+
23
+ ## How it Works
24
+
25
+ The tunnel creates a stable subdomain on `cfargotunnel.com`. Your tunnel URL is
26
+ saved and reused across sessions. Webhooks automatically detect and use the
27
+ tunnel URL. The tunnel persists until you stop it with Ctrl+C.
28
+
29
+ ## Usage with Webhooks
30
+
31
+ When the tunnel is running, webhook URLs will automatically use your public
32
+ tunnel URL instead of localhost.
33
+
34
+ Without tunnel: `http://localhost:8081/api/webhook/...` With tunnel:
35
+ `https://your-tunnel-id.cfargotunnel.com/api/webhook/...`
36
+
37
+ ## Troubleshooting
38
+
39
+ If you get permission errors, the script will try to install cloudflared
40
+ automatically. Your tunnel configuration is stored in `~/.onechat-tunnel/`. To
41
+ reset your tunnel, delete `~/.onechat-tunnel/tunnel-id.txt`.