@buildinternet/releases-api-types 0.15.0 → 0.17.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.
- package/package.json +2 -2
- package/src/api-types.ts +516 -516
- package/src/schemas/collections.ts +140 -0
- package/src/schemas/lookups.ts +123 -0
- package/src/schemas/orgs.ts +294 -4
- package/src/schemas/overviews.ts +138 -0
- package/src/schemas/products.ts +32 -4
- package/src/schemas/related.ts +107 -0
- package/src/schemas/releases.ts +249 -0
- package/src/schemas/search.ts +156 -0
- package/src/schemas/sitemap.ts +36 -0
- package/src/schemas/sources.ts +291 -0
- package/src/schemas/stats.ts +62 -0
- package/src/schemas/taxonomy.ts +110 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { MediaItemSchema, ReleaseTypeSchema } from "./shared.js";
|
|
3
|
+
import { SourceTypeSchema } from "./sources.js";
|
|
4
|
+
import { LookupStatusSchema } from "./lookups.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Org hit on the unified `/v1/search` response. The list is built either
|
|
8
|
+
* from `searchOrgs` LIKE matches or, when `?domain=` resolves, from a
|
|
9
|
+
* single-row "scoped org" projection — both produce the same wire shape.
|
|
10
|
+
*
|
|
11
|
+
* `category` is loose (`z.string().nullable()`) rather than the canonical
|
|
12
|
+
* `CategorySchema` enum because the scoped-org projection passes the raw
|
|
13
|
+
* DB column through without going through `resolveCategoryInput`; legacy
|
|
14
|
+
* rows may carry deprecated slugs that haven't been migrated yet. Tighten
|
|
15
|
+
* once #689 (category overlay backfill) lands across the prod data set.
|
|
16
|
+
*/
|
|
17
|
+
export const SearchOrgHitSchema = z.object({
|
|
18
|
+
slug: z.string(),
|
|
19
|
+
name: z.string(),
|
|
20
|
+
domain: z.string().nullable(),
|
|
21
|
+
avatarUrl: z.string().nullable(),
|
|
22
|
+
category: z.string().nullable(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Unified catalog entry — either a product row or a standalone source
|
|
27
|
+
* presented as product-shaped. `kind` routes clicks to the right URL but
|
|
28
|
+
* the two forms are otherwise interchangeable for display. `kind` (not
|
|
29
|
+
* `type`) because source rows already carry
|
|
30
|
+
* `type: github|scrape|feed|agent` on the wire.
|
|
31
|
+
*/
|
|
32
|
+
export const SearchCatalogHitSchema = z.object({
|
|
33
|
+
slug: z.string(),
|
|
34
|
+
name: z.string(),
|
|
35
|
+
orgSlug: z.string().nullable(),
|
|
36
|
+
orgName: z.string().nullable(),
|
|
37
|
+
// Loose `z.string().nullable()` rather than the canonical `CategorySchema`
|
|
38
|
+
// enum because `foldSourcesIntoCatalog` and the catalog-search handlers
|
|
39
|
+
// pass `s.productCategory` / `o.category` through without validating
|
|
40
|
+
// against `resolveCategoryInput`. Legacy rows may carry deprecated slugs
|
|
41
|
+
// that haven't been migrated yet. Tighten once #689 (category overlay
|
|
42
|
+
// backfill) lands across the prod data set.
|
|
43
|
+
category: z.string().nullable(),
|
|
44
|
+
kind: z.enum(["product", "source"]),
|
|
45
|
+
sourceSlug: z.string().optional(),
|
|
46
|
+
sourceType: SourceTypeSchema.optional(),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export const SearchSourceHitSchema = z.object({
|
|
50
|
+
slug: z.string(),
|
|
51
|
+
name: z.string(),
|
|
52
|
+
type: SourceTypeSchema,
|
|
53
|
+
orgSlug: z.string().nullable(),
|
|
54
|
+
orgName: z.string().nullable(),
|
|
55
|
+
productSlug: z.string().nullable(),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export const SearchReleaseHitSchema = z.object({
|
|
59
|
+
id: z.string(),
|
|
60
|
+
sourceSlug: z.string(),
|
|
61
|
+
sourceName: z.string(),
|
|
62
|
+
// Loose `z.string()` rather than `SourceTypeSchema` because the
|
|
63
|
+
// release-hit hydration path reads `sources.type` straight from the DB
|
|
64
|
+
// without re-validating; legacy rows occasionally drift from the
|
|
65
|
+
// canonical four-value set. Used by the web byline icon.
|
|
66
|
+
sourceType: z.string().optional(),
|
|
67
|
+
orgSlug: z.string().nullable(),
|
|
68
|
+
orgName: z.string().nullable().optional(),
|
|
69
|
+
version: z.string().nullable(),
|
|
70
|
+
title: z.string(),
|
|
71
|
+
summary: z.string(),
|
|
72
|
+
titleGenerated: z.string().nullable().optional(),
|
|
73
|
+
titleShort: z.string().nullable().optional(),
|
|
74
|
+
content: z.string().optional(),
|
|
75
|
+
media: z.array(MediaItemSchema).optional(),
|
|
76
|
+
publishedAt: z.string().nullable(),
|
|
77
|
+
// Hybrid fusion score; absent on pure-lexical results.
|
|
78
|
+
score: z.number().optional(),
|
|
79
|
+
type: ReleaseTypeSchema.optional(),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Heading-aware CHANGELOG.md slice returned by hybrid / semantic search.
|
|
84
|
+
* Clients can deep-link to
|
|
85
|
+
* `/source/<sourceSlug>?tab=changelog&offset=<offset>` to read the
|
|
86
|
+
* surrounding file content.
|
|
87
|
+
*/
|
|
88
|
+
export const SearchChunkHitSchema = z.object({
|
|
89
|
+
sourceSlug: z.string(),
|
|
90
|
+
sourceName: z.string(),
|
|
91
|
+
orgSlug: z.string().nullable(),
|
|
92
|
+
orgName: z.string().nullable().optional(),
|
|
93
|
+
filePath: z.string(),
|
|
94
|
+
offset: z.number().int().min(0),
|
|
95
|
+
length: z.number().int().min(0),
|
|
96
|
+
heading: z.string().nullable(),
|
|
97
|
+
snippet: z.string(),
|
|
98
|
+
score: z.number(),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Slim wire payload embedded in a search response when the query is a
|
|
103
|
+
* GitHub `org/repo` coordinate and no existing entity matched. Reuses
|
|
104
|
+
* `LookupStatusSchema` from `./lookups.js` rather than redefining — the
|
|
105
|
+
* thick `POST /v1/lookups` response and the slim search-embedded payload
|
|
106
|
+
* share the same status set.
|
|
107
|
+
*/
|
|
108
|
+
export const LookupResultPayloadSchema = z.object({
|
|
109
|
+
status: LookupStatusSchema,
|
|
110
|
+
source: z
|
|
111
|
+
.object({
|
|
112
|
+
id: z.string(),
|
|
113
|
+
slug: z.string(),
|
|
114
|
+
name: z.string(),
|
|
115
|
+
url: z.string(),
|
|
116
|
+
discovery: z.enum(["curated", "agent", "on_demand"]),
|
|
117
|
+
})
|
|
118
|
+
.optional(),
|
|
119
|
+
releases: z
|
|
120
|
+
.array(
|
|
121
|
+
z.object({
|
|
122
|
+
id: z.string(),
|
|
123
|
+
version: z.string().nullable(),
|
|
124
|
+
title: z.string(),
|
|
125
|
+
publishedAt: z.string().nullable(),
|
|
126
|
+
}),
|
|
127
|
+
)
|
|
128
|
+
.optional(),
|
|
129
|
+
// Unambiguous "did you mean" rail; null when the org segment matches
|
|
130
|
+
// multiple curated orgs or none.
|
|
131
|
+
relatedOrg: z
|
|
132
|
+
.object({
|
|
133
|
+
org: z.object({ id: z.string(), slug: z.string(), name: z.string() }),
|
|
134
|
+
sources: z.array(
|
|
135
|
+
z.object({ id: z.string(), slug: z.string(), name: z.string(), url: z.string() }),
|
|
136
|
+
),
|
|
137
|
+
})
|
|
138
|
+
.nullable(),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
export const UnifiedSearchResponseSchema = z.object({
|
|
142
|
+
query: z.string(),
|
|
143
|
+
// Normalized `?domain=` echo + resolution outcome. Both absent when no
|
|
144
|
+
// domain filter was applied.
|
|
145
|
+
domain: z.string().optional(),
|
|
146
|
+
domainStatus: z.enum(["matched", "not_found"]).optional(),
|
|
147
|
+
orgs: z.array(SearchOrgHitSchema),
|
|
148
|
+
catalog: z.array(SearchCatalogHitSchema),
|
|
149
|
+
sources: z.array(SearchSourceHitSchema),
|
|
150
|
+
releases: z.array(SearchReleaseHitSchema),
|
|
151
|
+
chunks: z.array(SearchChunkHitSchema).optional(),
|
|
152
|
+
mode: z.enum(["lexical", "semantic", "hybrid"]).optional(),
|
|
153
|
+
degraded: z.boolean().optional(),
|
|
154
|
+
degradedReason: z.string().optional(),
|
|
155
|
+
lookup: LookupResultPayloadSchema.nullable().optional(),
|
|
156
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Per-source row in `GET /v1/sitemap`. `hasChangelog` and `hasHighlights`
|
|
5
|
+
* are `.optional()` for backwards compatibility — older clients ignore the
|
|
6
|
+
* fields, but the current handler always populates them (#875).
|
|
7
|
+
*/
|
|
8
|
+
export const SitemapSourceSchema = z.object({
|
|
9
|
+
orgSlug: z.string(),
|
|
10
|
+
slug: z.string(),
|
|
11
|
+
latestDate: z.string().nullable(),
|
|
12
|
+
/**
|
|
13
|
+
* Whether this source has a stored GitHub CHANGELOG file. Used by the web
|
|
14
|
+
* sitemap to emit `/{org}/{src}/changelog` URLs only for sources where the
|
|
15
|
+
* route resolves.
|
|
16
|
+
*/
|
|
17
|
+
hasChangelog: z.boolean().optional(),
|
|
18
|
+
/**
|
|
19
|
+
* Whether this source has any rolling or monthly highlight summaries.
|
|
20
|
+
* Drives `/{org}/{src}/highlights` sitemap emission.
|
|
21
|
+
*/
|
|
22
|
+
hasHighlights: z.boolean().optional(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Bulk URL payload consumed by the web sitemap generator. Lists every
|
|
27
|
+
* orgs/sources/products/collections slug paired with the timestamp the web
|
|
28
|
+
* uses to drive `<lastmod>`. Soft-deleted / hidden / on_demand rows are
|
|
29
|
+
* excluded.
|
|
30
|
+
*/
|
|
31
|
+
export const SitemapPayloadSchema = z.object({
|
|
32
|
+
orgs: z.array(z.object({ slug: z.string(), lastActivity: z.string().nullable() })),
|
|
33
|
+
sources: z.array(SitemapSourceSchema),
|
|
34
|
+
products: z.array(z.object({ orgSlug: z.string(), slug: z.string() })),
|
|
35
|
+
collections: z.array(z.object({ slug: z.string(), updatedAt: z.string() })),
|
|
36
|
+
});
|
package/src/schemas/sources.ts
CHANGED
|
@@ -265,3 +265,294 @@ export const SourceChangelogResponseSchema = z.object({
|
|
|
265
265
|
*/
|
|
266
266
|
files: z.array(ChangelogFileSummarySchema),
|
|
267
267
|
});
|
|
268
|
+
|
|
269
|
+
// ── Source activity (/sources/:slug/activity, /orgs/:orgSlug/sources/:sourceSlug/activity) ──
|
|
270
|
+
|
|
271
|
+
const SourceActivityWeeklyBucketSchema = z.object({
|
|
272
|
+
weekStart: z.string(),
|
|
273
|
+
count: z.number().int().min(0),
|
|
274
|
+
earliestVersion: z.string().nullable(),
|
|
275
|
+
latestVersion: z.string().nullable(),
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Response shape for `GET /v1/sources/:slug/activity` and the org-scoped twin.
|
|
280
|
+
* Returns week-bucketed release counts for the full tracking lifetime of the source.
|
|
281
|
+
* Optional `?from=YYYY-MM-DD` and `?to=YYYY-MM-DD` narrow the range.
|
|
282
|
+
*/
|
|
283
|
+
export const SourceActivityResponseSchema = z.object({
|
|
284
|
+
source: z.object({
|
|
285
|
+
slug: z.string(),
|
|
286
|
+
name: z.string(),
|
|
287
|
+
orgSlug: z.string().nullable(),
|
|
288
|
+
orgName: z.string().nullable(),
|
|
289
|
+
}),
|
|
290
|
+
range: z.object({ from: z.string(), to: z.string() }),
|
|
291
|
+
weeklyBuckets: z.array(SourceActivityWeeklyBucketSchema),
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// ── Source heatmap (/sources/:slug/heatmap, /orgs/:orgSlug/sources/:sourceSlug/heatmap) ──
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Response shape for `GET /v1/sources/:slug/heatmap` and the org-scoped twin.
|
|
298
|
+
* Returns daily release counts for the trailing 365 days — contribution-graph
|
|
299
|
+
* visualization for the source detail page.
|
|
300
|
+
*/
|
|
301
|
+
export const SourceHeatmapResponseSchema = z.object({
|
|
302
|
+
source: z.object({ slug: z.string(), name: z.string() }),
|
|
303
|
+
range: z.object({ from: z.string(), to: z.string() }),
|
|
304
|
+
dailyCounts: z.array(z.object({ date: z.string(), count: z.number().int().min(0) })),
|
|
305
|
+
total: z.number().int().min(0),
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// ── Source known releases (/sources/:slug/known-releases, /orgs/:orgSlug/…) ──
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Single row returned inside `GET /v1/sources/:slug/known-releases`. Minimal
|
|
312
|
+
* identifier set used by the incremental parsing agent to skip already-known
|
|
313
|
+
* versions without fetching full content.
|
|
314
|
+
*/
|
|
315
|
+
export const SourceKnownReleaseItemSchema = z.object({
|
|
316
|
+
version: z.string().nullable(),
|
|
317
|
+
title: z.string(),
|
|
318
|
+
publishedAt: z.string().nullable(),
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Response shape for `GET /v1/sources/:slug/known-releases`. Returns the
|
|
323
|
+
* N most-recent non-suppressed releases (default N=10, max 500), ordered
|
|
324
|
+
* by `publishedAt` descending. Accepts `?limit=N`.
|
|
325
|
+
*/
|
|
326
|
+
export const SourceKnownReleasesResponseSchema = z.array(SourceKnownReleaseItemSchema);
|
|
327
|
+
|
|
328
|
+
// ── Source recent releases (/sources/:slug/recent-releases, /orgs/:orgSlug/…) ──
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Response shape for `GET /v1/sources/:slug/recent-releases`. Returns the
|
|
332
|
+
* full release rows (non-suppressed, published at or after `?cutoff=`) ordered
|
|
333
|
+
* by `publishedAt` descending. Used by the summarization agent to retrieve the
|
|
334
|
+
* release window it needs to summarize. `?cutoff=` is required (ISO-8601 date
|
|
335
|
+
* string); missing `?cutoff=` returns 400.
|
|
336
|
+
*/
|
|
337
|
+
export const SourceRecentReleasesResponseSchema = z.array(
|
|
338
|
+
z.looseObject({
|
|
339
|
+
id: z.string(),
|
|
340
|
+
sourceId: z.string(),
|
|
341
|
+
version: z.string().nullable(),
|
|
342
|
+
title: z.string(),
|
|
343
|
+
content: z.string(),
|
|
344
|
+
url: z.string().nullable(),
|
|
345
|
+
publishedAt: z.string().nullable(),
|
|
346
|
+
fetchedAt: z.string().optional(),
|
|
347
|
+
}),
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
// ── Source sessions (/sources/:slug/sessions) ──
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Response shape for `GET /v1/sources/:slug/sessions`. Returns the active
|
|
354
|
+
* discovery session (if any) touching this source. The session object is the
|
|
355
|
+
* live DO state blob from the `StatusHub` Durable Object. Returns `{ sessions:
|
|
356
|
+
* [] }` when no active session references this source.
|
|
357
|
+
*/
|
|
358
|
+
export const SourceSessionsResponseSchema = z.object({
|
|
359
|
+
sessions: z.array(z.record(z.string(), z.unknown())),
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// ── Source summaries (/sources/:slug/summaries) ──
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Full summary row as stored in `release_summaries`, returned by
|
|
366
|
+
* `GET /v1/sources/:slug/summaries`. Filter by `?type=rolling|monthly`,
|
|
367
|
+
* `?year=`, `?month=`.
|
|
368
|
+
*/
|
|
369
|
+
export const SourceSummaryRowSchema = z.object({
|
|
370
|
+
id: z.string().optional(),
|
|
371
|
+
sourceId: z.string(),
|
|
372
|
+
orgId: z.string().nullable(),
|
|
373
|
+
type: z.enum(["rolling", "monthly"]),
|
|
374
|
+
year: z.number().int().nullable(),
|
|
375
|
+
month: z.number().int().nullable(),
|
|
376
|
+
windowDays: z.number().int().nullable(),
|
|
377
|
+
summary: z.string(),
|
|
378
|
+
releaseCount: z.number().int().min(0),
|
|
379
|
+
generatedAt: z.string(),
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
export const SourceSummariesResponseSchema = z.array(SourceSummaryRowSchema);
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Body accepted by `POST /v1/sources/:slug/summaries`.
|
|
386
|
+
*
|
|
387
|
+
* `month` is calendar-bounded (1..12) and `windowDays` must be a positive
|
|
388
|
+
* integer when present — silently dropping wrong-typed values masks client
|
|
389
|
+
* bugs, so the validator rejects them explicitly. `year` is not bound-checked
|
|
390
|
+
* (any cutoff would be arbitrary).
|
|
391
|
+
*/
|
|
392
|
+
export const CreateSourceSummaryBodySchema = z.object({
|
|
393
|
+
type: z.enum(["rolling", "monthly"]),
|
|
394
|
+
year: z.number().int().nullable().optional(),
|
|
395
|
+
month: z.number().int().min(1).max(12).nullable().optional(),
|
|
396
|
+
windowDays: z.number().int().min(1).nullable().optional(),
|
|
397
|
+
summary: z.string().min(1),
|
|
398
|
+
releaseCount: z.number().int().min(0),
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
/** Response for `POST /v1/sources/:slug/summaries` (upsert). */
|
|
402
|
+
export const CreateSourceSummaryResponseSchema = z.object({
|
|
403
|
+
ok: z.literal(true),
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// ── Source fetch trigger (POST /sources/:slug/fetch) ──
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Response shape for `POST /v1/sources/:slug/fetch`. Two branches:
|
|
410
|
+
* - Feed/GitHub/scraped-with-feed sources: `{ fetched: true, releasesInserted, … }`
|
|
411
|
+
* - Scrape/agent sources (flagged for CLI pickup): `{ queued: true, type: "flagged" }`
|
|
412
|
+
*/
|
|
413
|
+
export const SourceFetchResponseSchema = z.union([
|
|
414
|
+
z.looseObject({ fetched: z.literal(true) }),
|
|
415
|
+
z.object({ queued: z.literal(true), type: z.string() }),
|
|
416
|
+
]);
|
|
417
|
+
|
|
418
|
+
// ── Source content-hash check (POST /sources/:slug/content-hash) ──
|
|
419
|
+
|
|
420
|
+
/** Response for `POST /v1/sources/:slug/content-hash`. */
|
|
421
|
+
export const SourceContentHashResponseSchema = z.union([
|
|
422
|
+
z.object({ unchanged: z.literal(true) }),
|
|
423
|
+
z.object({ unchanged: z.literal(false) }),
|
|
424
|
+
]);
|
|
425
|
+
|
|
426
|
+
/** Body for `POST /v1/sources/:slug/content-hash`. */
|
|
427
|
+
export const SourceContentHashBodySchema = z.object({
|
|
428
|
+
contentHash: z.string().min(1),
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// ── Changelog token backfill (PATCH /sources/:slug/changelog/tokens) ──
|
|
432
|
+
|
|
433
|
+
/** Response for `PATCH /v1/sources/:slug/changelog/tokens`. */
|
|
434
|
+
export const ChangelogTokensResponseSchema = z.object({
|
|
435
|
+
path: z.string(),
|
|
436
|
+
oldTokens: z.number().int().nullable(),
|
|
437
|
+
tokens: z.number().int().min(0),
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// ── Source metadata merge (PATCH /sources/:slug/metadata) ──
|
|
441
|
+
|
|
442
|
+
/** Response for `PATCH /v1/sources/:slug/metadata`. Echoes the merged metadata object. */
|
|
443
|
+
export const SourceMetadataResponseSchema = z.object({
|
|
444
|
+
metadata: z.record(z.string(), z.unknown()),
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// ── Changelog probe (POST /sources/:slug/changelog/probe) ──
|
|
448
|
+
|
|
449
|
+
/** Response for `POST /v1/sources/:slug/changelog/probe`. */
|
|
450
|
+
export const ChangelogProbeResponseSchema = z.object({
|
|
451
|
+
sourceId: z.string(),
|
|
452
|
+
sourceSlug: z.string(),
|
|
453
|
+
url: z.string(),
|
|
454
|
+
paths: z.array(z.string()),
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// ── Delete source (DELETE /sources/:slug) ──
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Response for `DELETE /v1/sources/:slug`. Two shapes:
|
|
461
|
+
* - Soft delete: `{ deleted: true, deletedAt: "<iso>" }`
|
|
462
|
+
* - Hard delete (`?hard=true`): `{ deleted: true, hard: true }`
|
|
463
|
+
*/
|
|
464
|
+
export const DeleteSourceResponseSchema = z.union([
|
|
465
|
+
z.object({ deleted: z.literal(true), deletedAt: z.string() }),
|
|
466
|
+
z.object({ deleted: z.literal(true), hard: z.literal(true) }),
|
|
467
|
+
]);
|
|
468
|
+
|
|
469
|
+
// ── Delete source releases (DELETE /sources/:slug/releases) ──
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Response for `DELETE /v1/sources/:slug/releases`. Two shapes:
|
|
473
|
+
* - Soft (`?hard` absent): `{ suppressed: N }` — releases hidden, not deleted
|
|
474
|
+
* - Hard (`?hard=true`): `{ deleted: N, hard: true }`
|
|
475
|
+
*/
|
|
476
|
+
export const DeleteSourceReleasesResponseSchema = z.union([
|
|
477
|
+
z.object({ suppressed: z.number().int().min(0) }),
|
|
478
|
+
z.object({ deleted: z.number().int().min(0), hard: z.literal(true) }),
|
|
479
|
+
]);
|
|
480
|
+
|
|
481
|
+
// ── Single release insert (POST /sources/:slug/releases) ──
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Response for `POST /v1/sources/:slug/releases`. Returns the inserted release
|
|
485
|
+
* row (201) or `{ skipped: true }` (200) on URL conflict.
|
|
486
|
+
*/
|
|
487
|
+
export const InsertReleaseResponseSchema = z.union([
|
|
488
|
+
z.looseObject({
|
|
489
|
+
id: z.string(),
|
|
490
|
+
sourceId: z.string(),
|
|
491
|
+
title: z.string(),
|
|
492
|
+
}),
|
|
493
|
+
z.object({ skipped: z.literal(true) }),
|
|
494
|
+
]);
|
|
495
|
+
|
|
496
|
+
// ── Batch release insert (POST /sources/:slug/releases/batch) ──
|
|
497
|
+
|
|
498
|
+
/** Response for `POST /v1/sources/:slug/releases/batch`. */
|
|
499
|
+
export const BatchReleasesResponseSchema = z.object({
|
|
500
|
+
inserted: z.number().int().min(0),
|
|
501
|
+
total: z.number().int().min(0),
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// ── Oversized changelog files (GET /sources/changelog-files/oversized) ──
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Row returned by `GET /v1/sources/changelog-files/oversized`. Each row is a
|
|
508
|
+
* changelog file whose content length exceeds `?minBytes=` (default 256 KB).
|
|
509
|
+
* Used by `scripts/backfill-changelog-tokens.ts` to find rows with estimated
|
|
510
|
+
* (not exact) token counts.
|
|
511
|
+
*/
|
|
512
|
+
export const OversizedChangelogFileRowSchema = z.object({
|
|
513
|
+
sourceId: z.string(),
|
|
514
|
+
sourceSlug: z.string(),
|
|
515
|
+
sourceName: z.string(),
|
|
516
|
+
orgSlug: z.string(),
|
|
517
|
+
path: z.string(),
|
|
518
|
+
filename: z.string(),
|
|
519
|
+
bytes: z.number().int().min(0),
|
|
520
|
+
tokens: z.number().int().nullable(),
|
|
521
|
+
fetchedAt: z.string(),
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
export const OversizedChangelogFilesResponseSchema = z.array(OversizedChangelogFileRowSchema);
|
|
525
|
+
|
|
526
|
+
// ── Fetchable sources (GET /sources/fetchable) ──
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Response for `GET /v1/sources/fetchable`. Returns raw source rows matching
|
|
530
|
+
* the requested fetch mode (`?mode=unfetched|stale|retry_errors|all`).
|
|
531
|
+
* Accepts `?staleHours=N` when `mode=stale`.
|
|
532
|
+
*/
|
|
533
|
+
export const FetchableSourcesResponseSchema = z.array(
|
|
534
|
+
z.looseObject({
|
|
535
|
+
id: z.string(),
|
|
536
|
+
slug: z.string(),
|
|
537
|
+
name: z.string(),
|
|
538
|
+
type: SourceTypeSchema,
|
|
539
|
+
url: z.string(),
|
|
540
|
+
metadata: z.string().nullable(),
|
|
541
|
+
}),
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
// ── Feed sources (GET /sources/feeds) ──
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Response for `GET /v1/sources/feeds`. Returns visible source rows where
|
|
548
|
+
* `metadata.feedUrl` is set and `fetchPriority != 'paused'`.
|
|
549
|
+
*/
|
|
550
|
+
export const FeedSourcesResponseSchema = FetchableSourcesResponseSchema;
|
|
551
|
+
|
|
552
|
+
// ── Changed sources (GET /sources/changes) ──
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Response for `GET /v1/sources/changes`. Returns visible source rows that
|
|
556
|
+
* have a non-null `changeDetectedAt` — the set flagged for CLI pickup.
|
|
557
|
+
*/
|
|
558
|
+
export const ChangedSourcesResponseSchema = FetchableSourcesResponseSchema;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { StatsSchema } from "./shared.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Per-source activity row in `GET /v1/stats`. `orgName` is nullable because
|
|
6
|
+
* the source-to-org JOIN is left-joined (legacy orphan sources still exist).
|
|
7
|
+
*/
|
|
8
|
+
export const StatsSourceActivitySchema = z.object({
|
|
9
|
+
sourceName: z.string(),
|
|
10
|
+
sourceSlug: z.string(),
|
|
11
|
+
sourceType: z.string(),
|
|
12
|
+
orgName: z.string().nullable(),
|
|
13
|
+
lastFetchedAt: z.string().nullable(),
|
|
14
|
+
totalReleases: z.number(),
|
|
15
|
+
recentReleases: z.number(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Recent fetch-log row, joined with source + org for display. `durationMs`
|
|
20
|
+
* and `error` are nullable on the underlying table; `orgName` is nullable
|
|
21
|
+
* for the same orphan-source reason as above.
|
|
22
|
+
*/
|
|
23
|
+
export const StatsRecentActivitySchema = z.object({
|
|
24
|
+
sourceName: z.string(),
|
|
25
|
+
sourceSlug: z.string(),
|
|
26
|
+
orgName: z.string().nullable(),
|
|
27
|
+
releasesFound: z.number(),
|
|
28
|
+
releasesInserted: z.number(),
|
|
29
|
+
totalReleases: z.number(),
|
|
30
|
+
status: z.string(),
|
|
31
|
+
durationMs: z.number().nullable(),
|
|
32
|
+
error: z.string().nullable(),
|
|
33
|
+
createdAt: z.string(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Response shape for `GET /v1/stats`. The handler returns a hybrid payload —
|
|
38
|
+
* the flat `Stats` counts (legacy `orgs/sources/releases/products` fields used
|
|
39
|
+
* by the web homepage banner) merged with the richer `StatsSummary` shape
|
|
40
|
+
* (period window + totals + sourceHealth + sourceActivity + recentActivity).
|
|
41
|
+
* Modelled with `z.object` over the union of both shapes; consumers may
|
|
42
|
+
* pick either subset.
|
|
43
|
+
*/
|
|
44
|
+
export const StatsResponseSchema = StatsSchema.extend({
|
|
45
|
+
period: z.object({
|
|
46
|
+
days: z.number().int().min(1),
|
|
47
|
+
cutoff: z.string(),
|
|
48
|
+
}),
|
|
49
|
+
totals: z.object({
|
|
50
|
+
organizations: z.number().int().min(0),
|
|
51
|
+
sources: z.number().int().min(0),
|
|
52
|
+
releases: z.number().int().min(0),
|
|
53
|
+
releasesInPeriod: z.number().int().min(0),
|
|
54
|
+
}),
|
|
55
|
+
sourceHealth: z.object({
|
|
56
|
+
upToDate: z.number().int().min(0),
|
|
57
|
+
stale: z.number().int().min(0),
|
|
58
|
+
neverFetched: z.number().int().min(0),
|
|
59
|
+
}),
|
|
60
|
+
sourceActivity: z.array(StatsSourceActivitySchema),
|
|
61
|
+
recentActivity: z.array(StatsRecentActivitySchema),
|
|
62
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CategorySchema } from "./shared.js";
|
|
3
|
+
import { CollectionReleaseItemSchema } from "./collections.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Org rollup row shared by category and tag detail responses. Slim by
|
|
7
|
+
* design — taxonomy pages link out to the full org detail page rather
|
|
8
|
+
* than embedding it.
|
|
9
|
+
*/
|
|
10
|
+
export const TaxonomyOrgSchema = z.object({
|
|
11
|
+
slug: z.string(),
|
|
12
|
+
name: z.string(),
|
|
13
|
+
domain: z.string().nullable(),
|
|
14
|
+
avatarUrl: z.string().nullable(),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
/** Product rollup row, paired with `TaxonomyOrg`. */
|
|
18
|
+
export const TaxonomyProductSchema = z.object({
|
|
19
|
+
slug: z.string(),
|
|
20
|
+
name: z.string(),
|
|
21
|
+
description: z.string().nullable(),
|
|
22
|
+
orgSlug: z.string(),
|
|
23
|
+
orgName: z.string(),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Per-category row on `GET /v1/categories`. Categories are a fixed
|
|
28
|
+
* taxonomy (`CATEGORIES` in `@buildinternet/releases-core/categories`),
|
|
29
|
+
* so the response always includes every slug, including ones with zero
|
|
30
|
+
* members. `name` / `description` / `aliases` come from the optional
|
|
31
|
+
* `categories` metadata overlay; `description` is null when no override
|
|
32
|
+
* has been set.
|
|
33
|
+
*/
|
|
34
|
+
export const CategoryListItemSchema = z.object({
|
|
35
|
+
slug: CategorySchema,
|
|
36
|
+
name: z.string(),
|
|
37
|
+
description: z.string().nullable(),
|
|
38
|
+
aliases: z.array(z.string()),
|
|
39
|
+
orgCount: z.number().int().min(0),
|
|
40
|
+
productCount: z.number().int().min(0),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const CategoryListResponseSchema = z.array(CategoryListItemSchema);
|
|
44
|
+
|
|
45
|
+
/** Returned by `GET /v1/categories/:slug` for the canonical slug. */
|
|
46
|
+
export const CategoryDetailSchema = z.object({
|
|
47
|
+
slug: CategorySchema,
|
|
48
|
+
name: z.string(),
|
|
49
|
+
description: z.string().nullable(),
|
|
50
|
+
aliases: z.array(z.string()),
|
|
51
|
+
orgs: z.array(TaxonomyOrgSchema),
|
|
52
|
+
products: z.array(TaxonomyProductSchema),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Body accepted by `PATCH /v1/categories/:slug`. All fields are optional,
|
|
57
|
+
* but the body must set at least one of them — an empty body is rejected.
|
|
58
|
+
* `name` and `description` accept `null` to clear the overlay value;
|
|
59
|
+
* `aliases` replaces the full set (pass `[]` to clear). The row is
|
|
60
|
+
* upserted — categories that have never been customized have no row.
|
|
61
|
+
*
|
|
62
|
+
* `name` is bounded at 200 chars and `description` at 2000. Alias element
|
|
63
|
+
* shape (kebab-case, not a canonical slug, not claimed by another row) is
|
|
64
|
+
* enforced in the handler after the strings are trimmed + lowercased, since
|
|
65
|
+
* those checks depend on runtime state and case-folding.
|
|
66
|
+
*/
|
|
67
|
+
export const UpdateCategoryRequestSchema = z
|
|
68
|
+
.object({
|
|
69
|
+
name: z.string().max(200).nullable().optional(),
|
|
70
|
+
description: z.string().max(2000).nullable().optional(),
|
|
71
|
+
aliases: z.array(z.string()).optional(),
|
|
72
|
+
})
|
|
73
|
+
.refine((b) => b.name !== undefined || b.description !== undefined || b.aliases !== undefined, {
|
|
74
|
+
message: "Body must set at least one of `name`, `description`, or `aliases`",
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/** Response from `PATCH /v1/categories/:slug` — the resolved overlay values. */
|
|
78
|
+
export const UpdateCategoryResponseSchema = z.object({
|
|
79
|
+
slug: CategorySchema,
|
|
80
|
+
name: z.string(),
|
|
81
|
+
description: z.string().nullable(),
|
|
82
|
+
aliases: z.array(z.string()),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Aggregated release row on `GET /v1/categories/:slug/releases`. Same wire
|
|
87
|
+
* shape as `CollectionReleaseItem` — both surfaces aggregate across orgs and
|
|
88
|
+
* use `formatAggregateReleaseRow` in `workers/api/src/utils.ts` for the
|
|
89
|
+
* formatting. Re-exported here so the two endpoints describe one schema
|
|
90
|
+
* instead of two drifting copies.
|
|
91
|
+
*/
|
|
92
|
+
export const CategoryReleaseItemSchema = CollectionReleaseItemSchema;
|
|
93
|
+
|
|
94
|
+
export const CategoryFeedPaginationSchema = z.object({
|
|
95
|
+
nextCursor: z.string().nullable(),
|
|
96
|
+
limit: z.number().int().min(1),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
export const CategoryReleasesResponseSchema = z.object({
|
|
100
|
+
releases: z.array(CategoryReleaseItemSchema),
|
|
101
|
+
pagination: CategoryFeedPaginationSchema,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
/** Returned by `GET /v1/tags/:slug`. */
|
|
105
|
+
export const TagDetailSchema = z.object({
|
|
106
|
+
slug: z.string(),
|
|
107
|
+
name: z.string(),
|
|
108
|
+
orgs: z.array(TaxonomyOrgSchema),
|
|
109
|
+
products: z.array(TaxonomyProductSchema),
|
|
110
|
+
});
|