@doccov/api 0.4.0 → 0.6.0

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.
@@ -1,3 +1,4 @@
1
+ import { getPlanLimits, type Plan } from '@doccov/db';
1
2
  import { Hono } from 'hono';
2
3
  import { nanoid } from 'nanoid';
3
4
  import { auth } from '../auth/config';
@@ -29,33 +30,50 @@ coverageRoute.get('/projects/:projectId/history', async (c) => {
29
30
  const { projectId } = c.req.param();
30
31
  const { range = '30d', limit = '50' } = c.req.query();
31
32
 
32
- // Verify user has access to project
33
- const project = await db
33
+ // Verify user has access to project and get org plan
34
+ const projectWithOrg = await db
34
35
  .selectFrom('projects')
35
36
  .innerJoin('org_members', 'org_members.orgId', 'projects.orgId')
37
+ .innerJoin('organizations', 'organizations.id', 'projects.orgId')
36
38
  .where('projects.id', '=', projectId)
37
39
  .where('org_members.userId', '=', session.user.id)
38
- .select(['projects.id', 'projects.name'])
40
+ .select(['projects.id', 'projects.name', 'organizations.plan'])
39
41
  .executeTakeFirst();
40
42
 
41
- if (!project) {
43
+ if (!projectWithOrg) {
42
44
  return c.json({ error: 'Project not found' }, 404);
43
45
  }
44
46
 
45
- // Calculate date filter based on range
47
+ // Check plan limits for trends access
48
+ const planLimits = getPlanLimits(projectWithOrg.plan as Plan);
49
+ if (planLimits.historyDays === 0) {
50
+ return c.json(
51
+ {
52
+ error: 'Coverage trends require Team plan or higher',
53
+ upgrade: 'https://doccov.com/pricing',
54
+ },
55
+ 403,
56
+ );
57
+ }
58
+
59
+ // Calculate date filter based on range (capped by plan limit)
46
60
  let dateFilter: Date | null = null;
47
61
  const now = new Date();
48
- switch (range) {
49
- case '7d':
50
- dateFilter = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
51
- break;
52
- case '30d':
53
- dateFilter = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
54
- break;
55
- case '90d':
56
- dateFilter = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000);
57
- break;
58
- // 'all' and 'versions' - no date filter
62
+ const maxDays = planLimits.historyDays;
63
+
64
+ // Map range to days, capped by plan limit
65
+ const rangeDays: Record<string, number> = {
66
+ '7d': Math.min(7, maxDays),
67
+ '30d': Math.min(30, maxDays),
68
+ '90d': Math.min(90, maxDays),
69
+ };
70
+
71
+ if (range in rangeDays) {
72
+ const days = rangeDays[range];
73
+ dateFilter = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
74
+ } else if (range === 'all' || range === 'versions') {
75
+ // Still cap by plan limit
76
+ dateFilter = new Date(now.getTime() - maxDays * 24 * 60 * 60 * 1000);
59
77
  }
60
78
 
61
79
  let query = db
@@ -74,13 +92,9 @@ coverageRoute.get('/projects/:projectId/history', async (c) => {
74
92
  'version',
75
93
  'branch',
76
94
  'commitSha',
77
- 'coveragePercent',
78
- 'documentedCount',
79
- 'totalCount',
80
- 'descriptionCount',
81
- 'paramsCount',
82
- 'returnsCount',
83
- 'examplesCount',
95
+ 'coverageScore',
96
+ 'documentedExports',
97
+ 'totalExports',
84
98
  'driftCount',
85
99
  'source',
86
100
  'createdAt',
@@ -111,13 +125,9 @@ coverageRoute.post('/projects/:projectId/snapshots', async (c) => {
111
125
  version?: string;
112
126
  branch?: string;
113
127
  commitSha?: string;
114
- coveragePercent: number;
115
- documentedCount: number;
116
- totalCount: number;
117
- descriptionCount?: number;
118
- paramsCount?: number;
119
- returnsCount?: number;
120
- examplesCount?: number;
128
+ coverageScore: number;
129
+ documentedExports: number;
130
+ totalExports: number;
121
131
  driftCount?: number;
122
132
  source?: 'ci' | 'manual' | 'scheduled';
123
133
  }>();
@@ -144,13 +154,9 @@ coverageRoute.post('/projects/:projectId/snapshots', async (c) => {
144
154
  version: body.version || null,
145
155
  branch: body.branch || null,
146
156
  commitSha: body.commitSha || null,
147
- coveragePercent: body.coveragePercent,
148
- documentedCount: body.documentedCount,
149
- totalCount: body.totalCount,
150
- descriptionCount: body.descriptionCount || null,
151
- paramsCount: body.paramsCount || null,
152
- returnsCount: body.returnsCount || null,
153
- examplesCount: body.examplesCount || null,
157
+ coverageScore: body.coverageScore,
158
+ documentedExports: body.documentedExports,
159
+ totalExports: body.totalExports,
154
160
  driftCount: body.driftCount || 0,
155
161
  source: body.source || 'manual',
156
162
  })
@@ -161,7 +167,7 @@ coverageRoute.post('/projects/:projectId/snapshots', async (c) => {
161
167
  await db
162
168
  .updateTable('projects')
163
169
  .set({
164
- coverageScore: body.coveragePercent,
170
+ coverageScore: body.coverageScore,
165
171
  driftCount: body.driftCount || 0,
166
172
  lastAnalyzedAt: new Date(),
167
173
  })
@@ -174,9 +180,9 @@ coverageRoute.post('/projects/:projectId/snapshots', async (c) => {
174
180
  // Helper: Generate insights from coverage data
175
181
  interface Snapshot {
176
182
  version: string | null;
177
- coveragePercent: number;
178
- documentedCount: number;
179
- totalCount: number;
183
+ coverageScore: number;
184
+ documentedExports: number;
185
+ totalExports: number;
180
186
  driftCount: number;
181
187
  }
182
188
 
@@ -192,7 +198,7 @@ function generateInsights(snapshots: Snapshot[]): Insight[] {
192
198
 
193
199
  const first = snapshots[0];
194
200
  const last = snapshots[snapshots.length - 1];
195
- const diff = last.coveragePercent - first.coveragePercent;
201
+ const diff = last.coverageScore - first.coverageScore;
196
202
 
197
203
  // Overall improvement/regression
198
204
  if (diff > 0) {
@@ -210,8 +216,8 @@ function generateInsights(snapshots: Snapshot[]): Insight[] {
210
216
  }
211
217
 
212
218
  // Predict time to 100%
213
- if (diff > 0 && last.coveragePercent < 100) {
214
- const remaining = 100 - last.coveragePercent;
219
+ if (diff > 0 && last.coverageScore < 100) {
220
+ const remaining = 100 - last.coverageScore;
215
221
  const avgGainPerSnapshot = diff / (snapshots.length - 1);
216
222
  if (avgGainPerSnapshot > 0) {
217
223
  const snapshotsToComplete = Math.ceil(remaining / avgGainPerSnapshot);
@@ -227,8 +233,7 @@ function generateInsights(snapshots: Snapshot[]): Insight[] {
227
233
  const milestones = [50, 75, 90, 100];
228
234
  for (const milestone of milestones) {
229
235
  const crossedAt = snapshots.findIndex(
230
- (s, i) =>
231
- i > 0 && s.coveragePercent >= milestone && snapshots[i - 1].coveragePercent < milestone,
236
+ (s, i) => i > 0 && s.coverageScore >= milestone && snapshots[i - 1].coverageScore < milestone,
232
237
  );
233
238
  if (crossedAt > 0) {
234
239
  insights.push({
@@ -253,7 +258,7 @@ function detectRegression(
253
258
  for (let i = 1; i < recent.length; i++) {
254
259
  const prev = recent[i - 1];
255
260
  const curr = recent[i];
256
- const drop = prev.coveragePercent - curr.coveragePercent;
261
+ const drop = prev.coverageScore - curr.coverageScore;
257
262
 
258
263
  if (drop >= 3) {
259
264
  // 3% or more drop
@@ -261,7 +266,7 @@ function detectRegression(
261
266
  fromVersion: prev.version || `v${i}`,
262
267
  toVersion: curr.version || `v${i + 1}`,
263
268
  coverageDrop: Math.round(drop),
264
- exportsLost: prev.documentedCount - curr.documentedCount,
269
+ exportsLost: prev.documentedExports - curr.documentedExports,
265
270
  };
266
271
  }
267
272
  }