@buildinternet/releases-api-types 0.6.0 → 0.8.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 +5 -1
- package/src/api-types.ts +253 -303
- package/src/schemas/orgs.ts +91 -0
- package/src/schemas/products.ts +132 -0
- package/src/schemas/shared.ts +80 -0
- package/src/schemas/sources.ts +267 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buildinternet/releases-api-types",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Wire protocol — request/response types for the Releases registry API. Shared by web, workers, and clients.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
"src",
|
|
17
17
|
"README.md"
|
|
18
18
|
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@buildinternet/releases-core": "workspace:*",
|
|
21
|
+
"zod": "^4.3.6"
|
|
22
|
+
},
|
|
19
23
|
"publishConfig": {
|
|
20
24
|
"access": "public"
|
|
21
25
|
}
|
package/src/api-types.ts
CHANGED
|
@@ -1,44 +1,132 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared API response types.
|
|
3
|
-
*
|
|
4
|
-
* This is the single source of truth for all API response shapes.
|
|
5
|
-
* Consumed by: web frontend, CLI client, and (optionally) API worker routes.
|
|
2
|
+
* Shared API response types — single source of truth for the wire protocol.
|
|
3
|
+
* Consumed by: web frontend, MCP worker, OSS CLI, and the API worker.
|
|
6
4
|
*/
|
|
7
5
|
|
|
6
|
+
import type { z } from "zod";
|
|
7
|
+
|
|
8
|
+
export type {
|
|
9
|
+
SourceType,
|
|
10
|
+
SourceDiscovery,
|
|
11
|
+
SourceFetchPriority,
|
|
12
|
+
} from "@buildinternet/releases-core/source-enums";
|
|
13
|
+
import type {
|
|
14
|
+
MediaItemSchema,
|
|
15
|
+
PaginationSchema,
|
|
16
|
+
StatsSchema,
|
|
17
|
+
ErrorResponseSchema,
|
|
18
|
+
ReleaseTypeSchema,
|
|
19
|
+
ReleaseItemSchema,
|
|
20
|
+
ReleaseSummaryItemSchema,
|
|
21
|
+
OverviewPageItemSchema,
|
|
22
|
+
} from "./schemas/shared.js";
|
|
23
|
+
import type {
|
|
24
|
+
OrgListItemSchema,
|
|
25
|
+
OrgListResponseSchema,
|
|
26
|
+
OrgAccountItemSchema,
|
|
27
|
+
OrgAccountsResponseSchema,
|
|
28
|
+
OrgTagsResponseSchema,
|
|
29
|
+
CreateOrgBodySchema,
|
|
30
|
+
UpdateOrgBodySchema,
|
|
31
|
+
OrgDetailSchema,
|
|
32
|
+
} from "./schemas/orgs.js";
|
|
33
|
+
import type {
|
|
34
|
+
SourceListItemSchema,
|
|
35
|
+
SourceWithOrgSchema,
|
|
36
|
+
SourceListResponseSchema,
|
|
37
|
+
SourceListResultSchema,
|
|
38
|
+
SourceDetailSchema,
|
|
39
|
+
SourceMutationResponseSchema,
|
|
40
|
+
SourceOrgRefSchema,
|
|
41
|
+
SourcePatchInputSchema,
|
|
42
|
+
CreateSourceBodySchema,
|
|
43
|
+
ChangelogFileSummarySchema,
|
|
44
|
+
SourceChangelogResponseSchema,
|
|
45
|
+
} from "./schemas/sources.js";
|
|
46
|
+
import type {
|
|
47
|
+
ProductRowSchema,
|
|
48
|
+
ProductListItemSchema,
|
|
49
|
+
ProductListResponseSchema,
|
|
50
|
+
ProductDetailSourceSchema,
|
|
51
|
+
ProductDetailSchema,
|
|
52
|
+
CreateProductBodySchema,
|
|
53
|
+
UpdateProductBodySchema,
|
|
54
|
+
AdoptProductBodySchema,
|
|
55
|
+
ProductAdoptResultSchema,
|
|
56
|
+
ProductAdoptDryRunSchema,
|
|
57
|
+
ProductAdoptResponseSchema,
|
|
58
|
+
ProductDeleteResponseSchema,
|
|
59
|
+
} from "./schemas/products.js";
|
|
60
|
+
|
|
61
|
+
export {
|
|
62
|
+
MediaItemSchema,
|
|
63
|
+
PaginationSchema,
|
|
64
|
+
ListResponseSchema,
|
|
65
|
+
StatsSchema,
|
|
66
|
+
ErrorResponseSchema,
|
|
67
|
+
ReleaseTypeSchema,
|
|
68
|
+
ReleaseItemSchema,
|
|
69
|
+
ReleaseSummaryItemSchema,
|
|
70
|
+
OverviewPageItemSchema,
|
|
71
|
+
CategorySchema,
|
|
72
|
+
} from "./schemas/shared.js";
|
|
73
|
+
export {
|
|
74
|
+
OrgListItemSchema,
|
|
75
|
+
OrgListResponseSchema,
|
|
76
|
+
OrgAccountItemSchema,
|
|
77
|
+
OrgAccountsResponseSchema,
|
|
78
|
+
OrgTagsResponseSchema,
|
|
79
|
+
CreateOrgBodySchema,
|
|
80
|
+
UpdateOrgBodySchema,
|
|
81
|
+
OrgDetailSchema,
|
|
82
|
+
} from "./schemas/orgs.js";
|
|
83
|
+
export {
|
|
84
|
+
SourceListItemSchema,
|
|
85
|
+
SourceWithOrgSchema,
|
|
86
|
+
SourceListResponseSchema,
|
|
87
|
+
SourceListResultSchema,
|
|
88
|
+
SourceDetailSchema,
|
|
89
|
+
SourceMutationResponseSchema,
|
|
90
|
+
SourceOrgRefSchema,
|
|
91
|
+
SourcePatchInputSchema,
|
|
92
|
+
CreateSourceBodySchema,
|
|
93
|
+
ChangelogFileSummarySchema,
|
|
94
|
+
SourceChangelogResponseSchema,
|
|
95
|
+
} from "./schemas/sources.js";
|
|
96
|
+
export {
|
|
97
|
+
ProductRowSchema,
|
|
98
|
+
ProductListItemSchema,
|
|
99
|
+
ProductListResponseSchema,
|
|
100
|
+
ProductDetailSourceSchema,
|
|
101
|
+
ProductDetailSchema,
|
|
102
|
+
CreateProductBodySchema,
|
|
103
|
+
UpdateProductBodySchema,
|
|
104
|
+
AdoptProductBodySchema,
|
|
105
|
+
ProductAdoptResultSchema,
|
|
106
|
+
ProductAdoptDryRunSchema,
|
|
107
|
+
ProductAdoptResponseSchema,
|
|
108
|
+
ProductDeleteResponseSchema,
|
|
109
|
+
} from "./schemas/products.js";
|
|
110
|
+
|
|
8
111
|
// ── Media ──
|
|
9
112
|
|
|
10
|
-
export
|
|
11
|
-
type: "image" | "video" | "gif";
|
|
12
|
-
url: string;
|
|
13
|
-
alt?: string;
|
|
14
|
-
r2Url?: string;
|
|
15
|
-
}
|
|
113
|
+
export type MediaItem = z.infer<typeof MediaItemSchema>;
|
|
16
114
|
|
|
17
115
|
// ── Stats ──
|
|
18
116
|
|
|
19
|
-
export
|
|
20
|
-
orgs: number;
|
|
21
|
-
sources: number;
|
|
22
|
-
releases: number;
|
|
23
|
-
products: number;
|
|
24
|
-
}
|
|
117
|
+
export type Stats = z.infer<typeof StatsSchema>;
|
|
25
118
|
|
|
26
119
|
// ── Pagination ──
|
|
27
120
|
|
|
28
|
-
export
|
|
29
|
-
page: number;
|
|
30
|
-
pageSize: number;
|
|
31
|
-
returned: number;
|
|
32
|
-
totalItems?: number;
|
|
33
|
-
totalPages?: number;
|
|
34
|
-
hasMore: boolean;
|
|
35
|
-
}
|
|
121
|
+
export type Pagination = z.infer<typeof PaginationSchema>;
|
|
36
122
|
|
|
37
123
|
export interface ListResponse<T> {
|
|
38
124
|
items: T[];
|
|
39
125
|
pagination: Pagination;
|
|
40
126
|
}
|
|
41
127
|
|
|
128
|
+
export type ErrorResponse = z.infer<typeof ErrorResponseSchema>;
|
|
129
|
+
|
|
42
130
|
// ── Sitemap (bulk URL emission) ──
|
|
43
131
|
|
|
44
132
|
export interface SitemapPayload {
|
|
@@ -49,225 +137,54 @@ export interface SitemapPayload {
|
|
|
49
137
|
|
|
50
138
|
// ── Organizations ──
|
|
51
139
|
|
|
52
|
-
export
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
releaseCount: number;
|
|
60
|
-
recentReleaseCount: number;
|
|
61
|
-
lastActivity: string | null;
|
|
62
|
-
topProducts: string[];
|
|
63
|
-
sparkline: number[];
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export type OrgListResponse = ListResponse<OrgListItem>;
|
|
140
|
+
export type OrgListItem = z.infer<typeof OrgListItemSchema>;
|
|
141
|
+
export type OrgListResponse = z.infer<typeof OrgListResponseSchema>;
|
|
142
|
+
export type OrgAccountItem = z.infer<typeof OrgAccountItemSchema>;
|
|
143
|
+
export type OrgAccountsResponse = z.infer<typeof OrgAccountsResponseSchema>;
|
|
144
|
+
export type OrgTagsResponse = z.infer<typeof OrgTagsResponseSchema>;
|
|
145
|
+
export type CreateOrgBody = z.infer<typeof CreateOrgBodySchema>;
|
|
146
|
+
export type UpdateOrgBody = z.infer<typeof UpdateOrgBodySchema>;
|
|
67
147
|
|
|
68
|
-
export
|
|
69
|
-
platform: string;
|
|
70
|
-
handle: string;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export type OrgAccountsResponse = ListResponse<OrgAccountItem>;
|
|
74
|
-
export type OrgTagsResponse = ListResponse<string>;
|
|
75
|
-
|
|
76
|
-
export interface OrgDetail {
|
|
77
|
-
id?: string;
|
|
78
|
-
slug: string;
|
|
79
|
-
name: string;
|
|
80
|
-
domain: string | null;
|
|
81
|
-
description?: string | null;
|
|
82
|
-
category?: string | null;
|
|
83
|
-
avatarUrl: string | null;
|
|
84
|
-
tags?: string[];
|
|
85
|
-
sourceCount: number;
|
|
86
|
-
releaseCount: number;
|
|
87
|
-
releasesLast30Days: number;
|
|
88
|
-
avgReleasesPerWeek: number;
|
|
89
|
-
lastFetchedAt: string | null;
|
|
90
|
-
lastPolledAt: string | null;
|
|
91
|
-
trackingSince: string;
|
|
92
|
-
aliases?: string[];
|
|
93
|
-
accounts: OrgAccountItem[];
|
|
94
|
-
products: Array<{
|
|
95
|
-
id: string;
|
|
96
|
-
slug: string;
|
|
97
|
-
name: string;
|
|
98
|
-
url: string | null;
|
|
99
|
-
description: string | null;
|
|
100
|
-
sourceCount: number;
|
|
101
|
-
}>;
|
|
102
|
-
sources: SourceListItem[];
|
|
103
|
-
overview?: OverviewPageItem | null;
|
|
104
|
-
playbook?: { scope: "playbook"; content: string; updatedAt: string } | null;
|
|
105
|
-
}
|
|
148
|
+
export type OrgDetail = z.infer<typeof OrgDetailSchema>;
|
|
106
149
|
|
|
107
150
|
// ── Sources ──
|
|
108
151
|
|
|
109
|
-
export
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
*/
|
|
146
|
-
export interface SourceWithOrg {
|
|
147
|
-
id: string;
|
|
148
|
-
name: string;
|
|
149
|
-
slug: string;
|
|
150
|
-
type: string;
|
|
151
|
-
url: string;
|
|
152
|
-
orgName: string | null;
|
|
153
|
-
orgSlug: string | null;
|
|
154
|
-
productName: string | null;
|
|
155
|
-
productSlug: string | null;
|
|
156
|
-
isPrimary: boolean;
|
|
157
|
-
isHidden: boolean | null;
|
|
158
|
-
discovery?: "curated" | "agent" | "on_demand";
|
|
159
|
-
metadata: string | null;
|
|
160
|
-
releaseCount: number;
|
|
161
|
-
latestVersion: string | null;
|
|
162
|
-
latestDate: string | null;
|
|
163
|
-
lastFetchedAt: string | null;
|
|
164
|
-
lastPolledAt: string | null;
|
|
165
|
-
fetchPriority: string | null;
|
|
166
|
-
changeDetectedAt: string | null;
|
|
167
|
-
consecutiveNoChange: number | null;
|
|
168
|
-
consecutiveErrors: number | null;
|
|
169
|
-
nextFetchAfter: string | null;
|
|
170
|
-
medianGapDays: number | null;
|
|
171
|
-
lastRetieredAt: string | null;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/** Fields accepted by PATCH /v1/sources/:slug. */
|
|
175
|
-
export interface SourcePatchInput {
|
|
176
|
-
name?: string;
|
|
177
|
-
url?: string;
|
|
178
|
-
type?: string;
|
|
179
|
-
slug?: string;
|
|
180
|
-
metadata?: string;
|
|
181
|
-
orgId?: string | null;
|
|
182
|
-
productId?: string | null;
|
|
183
|
-
lastFetchedAt?: string | null;
|
|
184
|
-
lastContentHash?: string | null;
|
|
185
|
-
fetchPriority?: string;
|
|
186
|
-
consecutiveNoChange?: number;
|
|
187
|
-
consecutiveErrors?: number;
|
|
188
|
-
nextFetchAfter?: string | null;
|
|
189
|
-
isPrimary?: boolean;
|
|
190
|
-
isHidden?: boolean;
|
|
191
|
-
changeDetectedAt?: string | null;
|
|
192
|
-
lastPolledAt?: string | null;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/** Lightweight summary of a changelog file — used for the file index. */
|
|
196
|
-
export interface ChangelogFileSummary {
|
|
197
|
-
path: string;
|
|
198
|
-
filename: string;
|
|
199
|
-
url: string;
|
|
200
|
-
bytes: number;
|
|
201
|
-
fetchedAt: string;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export interface SourceChangelogResponse {
|
|
205
|
-
path: string;
|
|
206
|
-
filename: string;
|
|
207
|
-
url: string;
|
|
208
|
-
rawUrl: string;
|
|
209
|
-
content: string;
|
|
210
|
-
bytes: number;
|
|
211
|
-
fetchedAt: string;
|
|
212
|
-
/** Character offset of the first character in `content` within the full file. */
|
|
213
|
-
offset: number;
|
|
214
|
-
/** The limit (in chars) that was applied to produce this slice. */
|
|
215
|
-
limit: number;
|
|
216
|
-
/** Next offset to request for the next slice, or null if `content` is the tail. */
|
|
217
|
-
nextOffset: number | null;
|
|
218
|
-
/** Total length of the full file in characters. */
|
|
219
|
-
totalChars: number;
|
|
220
|
-
/** Requested token budget when in token mode (cl100k_base). */
|
|
221
|
-
tokens?: number;
|
|
222
|
-
/** Encoded token count of the returned `content`. Set in token mode. */
|
|
223
|
-
sliceTokens?: number;
|
|
224
|
-
/** Full-file token count (cl100k_base). Always populated. */
|
|
225
|
-
totalTokens: number;
|
|
226
|
-
/** True when the upstream file exceeded the 1MB cap and content was sliced. */
|
|
227
|
-
truncated: boolean;
|
|
228
|
-
/** Byte offset where the file was truncated, or null when not truncated. */
|
|
229
|
-
truncatedAt: number | null;
|
|
230
|
-
/**
|
|
231
|
-
* Index of every changelog file tracked for this source (root plus any
|
|
232
|
-
* discovered per-package files). Always present even for single-file
|
|
233
|
-
* sources so clients can lazily render a file picker.
|
|
234
|
-
*/
|
|
235
|
-
files: ChangelogFileSummary[];
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
export interface SourceDetail {
|
|
239
|
-
slug: string;
|
|
240
|
-
name: string;
|
|
241
|
-
type: string;
|
|
242
|
-
url: string;
|
|
243
|
-
/**
|
|
244
|
-
* Hidden sources are reachable by direct URL but excluded from listings,
|
|
245
|
-
* sitemap, and AI features. Set on on-demand lookups and admin-suppressed
|
|
246
|
-
* rows; absent on canonical curated/agent sources.
|
|
247
|
-
*/
|
|
248
|
-
isHidden?: boolean;
|
|
249
|
-
/**
|
|
250
|
-
* Pairs with `isHidden` to distinguish admin-suppressed rows (`curated` /
|
|
251
|
-
* `agent` + hidden) from rows materialized by `/v1/lookups` (`on_demand`).
|
|
252
|
-
* Optional for graceful degradation against older API responses.
|
|
253
|
-
*/
|
|
254
|
-
discovery?: "curated" | "agent" | "on_demand";
|
|
255
|
-
changelogUrl?: string | null;
|
|
256
|
-
hasChangelogFile?: boolean;
|
|
257
|
-
org: { slug: string; name: string } | null;
|
|
258
|
-
releaseCount: number;
|
|
259
|
-
releasesLast30Days: number;
|
|
260
|
-
avgReleasesPerWeek: number;
|
|
261
|
-
latestVersion: string | null;
|
|
262
|
-
latestDate: string | null;
|
|
263
|
-
lastFetchedAt: string | null;
|
|
264
|
-
lastPolledAt: string | null;
|
|
265
|
-
trackingSince: string;
|
|
266
|
-
releases: ReleaseItem[];
|
|
267
|
-
pagination: { page: number; pageSize: number; totalPages: number; totalItems: number };
|
|
268
|
-
summaries: {
|
|
269
|
-
rolling: ReleaseSummaryItem | null;
|
|
270
|
-
monthly: ReleaseSummaryItem[];
|
|
152
|
+
export type SourceListItem = z.infer<typeof SourceListItemSchema>;
|
|
153
|
+
export type SourceWithOrg = z.infer<typeof SourceWithOrgSchema>;
|
|
154
|
+
export type SourceListResponse = z.infer<typeof SourceListResponseSchema>;
|
|
155
|
+
export type SourceListResult = z.infer<typeof SourceListResultSchema>;
|
|
156
|
+
export type SourcePatchInput = z.infer<typeof SourcePatchInputSchema>;
|
|
157
|
+
export type CreateSourceBody = z.infer<typeof CreateSourceBodySchema>;
|
|
158
|
+
export type ChangelogFileSummary = z.infer<typeof ChangelogFileSummarySchema>;
|
|
159
|
+
export type SourceChangelogResponse = z.infer<typeof SourceChangelogResponseSchema>;
|
|
160
|
+
export type SourceDetail = z.infer<typeof SourceDetailSchema>;
|
|
161
|
+
export type SourceMutationResponse = z.infer<typeof SourceMutationResponseSchema>;
|
|
162
|
+
export type SourceOrgRef = z.infer<typeof SourceOrgRefSchema>;
|
|
163
|
+
|
|
164
|
+
// ── Admin telemetry: orgs rollup ──
|
|
165
|
+
|
|
166
|
+
export interface OrgsRollupRow {
|
|
167
|
+
/** Org slug, or "—" for sources without an org. */
|
|
168
|
+
orgSlug: string;
|
|
169
|
+
sourceCount: number;
|
|
170
|
+
/** Sources with no release on file or `latestDate` older than `staleDays`. */
|
|
171
|
+
staleCount: number;
|
|
172
|
+
/** Most-recent release across all of the org's sources, or null. */
|
|
173
|
+
mostRecentRelease: string | null;
|
|
174
|
+
mostRecentAgeDays: number | null;
|
|
175
|
+
/** True iff every source in the org is stale (and the org has at least one source). */
|
|
176
|
+
allStale: boolean;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface OrgsRollupResponse extends ListResponse<OrgsRollupRow> {
|
|
180
|
+
meta: {
|
|
181
|
+
/** Stale cutoff used server-side, in days. */
|
|
182
|
+
staleDays: number;
|
|
183
|
+
totalOrgs: number;
|
|
184
|
+
/** Orgs where every source is stale. */
|
|
185
|
+
dormantOrgs: number;
|
|
186
|
+
/** Orgs with at least one stale source. */
|
|
187
|
+
anyStaleOrgs: number;
|
|
271
188
|
};
|
|
272
189
|
}
|
|
273
190
|
|
|
@@ -278,20 +195,9 @@ export interface SourceDetail {
|
|
|
278
195
|
* Optional on the wire so older API responses (mid-deploy or pinned old workers)
|
|
279
196
|
* degrade gracefully — consumers that see `undefined` should treat it as `"feature"`.
|
|
280
197
|
*/
|
|
281
|
-
export type ReleaseType =
|
|
198
|
+
export type ReleaseType = z.infer<typeof ReleaseTypeSchema>;
|
|
282
199
|
|
|
283
|
-
export
|
|
284
|
-
id?: string;
|
|
285
|
-
version: string | null;
|
|
286
|
-
title: string;
|
|
287
|
-
summary: string;
|
|
288
|
-
content?: string;
|
|
289
|
-
publishedAt: string | null;
|
|
290
|
-
url: string | null;
|
|
291
|
-
media?: MediaItem[];
|
|
292
|
-
/** Release type. See {@link ReleaseType}. */
|
|
293
|
-
type?: ReleaseType;
|
|
294
|
-
}
|
|
200
|
+
export type ReleaseItem = z.infer<typeof ReleaseItemSchema>;
|
|
295
201
|
|
|
296
202
|
export interface ReleaseDetail {
|
|
297
203
|
id: string;
|
|
@@ -325,14 +231,7 @@ export type ReleaseCoverageResponse =
|
|
|
325
231
|
| { role: "coverage"; canonical: ReleaseCoverageRow; covers: [] }
|
|
326
232
|
| { role: "canonical"; canonical: null; covers: ReleaseCoverageRow[] };
|
|
327
233
|
|
|
328
|
-
export
|
|
329
|
-
year?: number | null;
|
|
330
|
-
month?: number | null;
|
|
331
|
-
windowDays?: number | null;
|
|
332
|
-
summary: string;
|
|
333
|
-
releaseCount: number;
|
|
334
|
-
generatedAt: string;
|
|
335
|
-
}
|
|
234
|
+
export type ReleaseSummaryItem = z.infer<typeof ReleaseSummaryItemSchema>;
|
|
336
235
|
|
|
337
236
|
// ── Search ──
|
|
338
237
|
|
|
@@ -470,6 +369,50 @@ export interface SearchChunkHit {
|
|
|
470
369
|
score: number;
|
|
471
370
|
}
|
|
472
371
|
|
|
372
|
+
// ── Lookup by domain (entity resolution) ──
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Returned by GET /v1/lookups/by-domain. Pure resolution: the server
|
|
376
|
+
* normalizes the input domain, exact-matches against
|
|
377
|
+
* `organizations.domain` (primary) and `domain_aliases.domain` (alias for
|
|
378
|
+
* either an org or a product), and returns whatever it finds. Unknown
|
|
379
|
+
* domains 404 — there is no on-demand probing for domains, unlike the
|
|
380
|
+
* GitHub coordinate path.
|
|
381
|
+
*/
|
|
382
|
+
export interface DomainLookupResponse {
|
|
383
|
+
/** Normalized domain that was matched (lowercased, no scheme/path/www). */
|
|
384
|
+
domain: string;
|
|
385
|
+
/**
|
|
386
|
+
* Owning organization, if one exists. `matchedVia` distinguishes a hit
|
|
387
|
+
* on `organizations.domain` (`"primary"`) from an alias hit
|
|
388
|
+
* (`"alias"`) so the UI can render the difference.
|
|
389
|
+
*/
|
|
390
|
+
org: {
|
|
391
|
+
id: string;
|
|
392
|
+
slug: string;
|
|
393
|
+
name: string;
|
|
394
|
+
domain: string | null;
|
|
395
|
+
description: string | null;
|
|
396
|
+
category: string | null;
|
|
397
|
+
avatarUrl: string | null;
|
|
398
|
+
matchedVia: "primary" | "alias";
|
|
399
|
+
} | null;
|
|
400
|
+
/**
|
|
401
|
+
* Products whose alias points at this domain. Multiple are possible
|
|
402
|
+
* when an org owns several products that all set the same alias —
|
|
403
|
+
* unusual, but allowed.
|
|
404
|
+
*/
|
|
405
|
+
products: Array<{
|
|
406
|
+
id: string;
|
|
407
|
+
slug: string;
|
|
408
|
+
name: string;
|
|
409
|
+
orgId: string;
|
|
410
|
+
orgSlug: string;
|
|
411
|
+
orgName: string;
|
|
412
|
+
category: string | null;
|
|
413
|
+
}>;
|
|
414
|
+
}
|
|
415
|
+
|
|
473
416
|
// ── Lookup (on-demand GitHub index) ──
|
|
474
417
|
|
|
475
418
|
export type LookupStatus = "indexed" | "existing" | "empty" | "not_found" | "deferred";
|
|
@@ -508,6 +451,21 @@ export interface LookupResultPayload {
|
|
|
508
451
|
|
|
509
452
|
export interface UnifiedSearchResponse {
|
|
510
453
|
query: string;
|
|
454
|
+
/**
|
|
455
|
+
* Normalized form of `?domain=` when the caller passed it. The server
|
|
456
|
+
* does the normalization once and echoes the canonical form back so
|
|
457
|
+
* clients don't have to re-normalize for analytics or display. Absent
|
|
458
|
+
* when no domain filter was applied.
|
|
459
|
+
*/
|
|
460
|
+
domain?: string;
|
|
461
|
+
/**
|
|
462
|
+
* Outcome of the `?domain=` resolution step. `"matched"` means the
|
|
463
|
+
* domain resolved to a known org and results below are scoped to it.
|
|
464
|
+
* `"not_found"` means the domain didn't match anything; in that case
|
|
465
|
+
* orgs/catalog/releases will all be empty arrays — the caller can
|
|
466
|
+
* distinguish "no hits in scope" from "scope didn't exist."
|
|
467
|
+
*/
|
|
468
|
+
domainStatus?: "matched" | "not_found";
|
|
511
469
|
orgs: SearchOrgHit[];
|
|
512
470
|
/** Products and standalone sources folded into a single list. */
|
|
513
471
|
catalog: SearchCatalogHit[];
|
|
@@ -532,16 +490,7 @@ export interface UnifiedSearchResponse {
|
|
|
532
490
|
|
|
533
491
|
// ── Overview Pages ──
|
|
534
492
|
|
|
535
|
-
export
|
|
536
|
-
scope: "org" | "product";
|
|
537
|
-
orgSlug?: string | null;
|
|
538
|
-
productSlug?: string | null;
|
|
539
|
-
content: string;
|
|
540
|
-
releaseCount: number;
|
|
541
|
-
lastContributingReleaseAt: string | null;
|
|
542
|
-
generatedAt: string;
|
|
543
|
-
updatedAt: string;
|
|
544
|
-
}
|
|
493
|
+
export type OverviewPageItem = z.infer<typeof OverviewPageItemSchema>;
|
|
545
494
|
|
|
546
495
|
/** @deprecated Use OverviewPageItem */
|
|
547
496
|
export type KnowledgePageItem = OverviewPageItem;
|
|
@@ -673,39 +622,20 @@ export interface OrgReleasesResponse {
|
|
|
673
622
|
|
|
674
623
|
// ── Products ──
|
|
675
624
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
export type
|
|
689
|
-
|
|
690
|
-
export interface ProductDetail {
|
|
691
|
-
id: string;
|
|
692
|
-
name: string;
|
|
693
|
-
slug: string;
|
|
694
|
-
orgId: string;
|
|
695
|
-
url: string | null;
|
|
696
|
-
description: string | null;
|
|
697
|
-
category: string | null;
|
|
698
|
-
createdAt: string;
|
|
699
|
-
sources: Array<{ id: string; slug: string; name: string; type: string; url: string }>;
|
|
700
|
-
tags: string[];
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
export interface ProductAdoptResult {
|
|
704
|
-
product: ProductDetail;
|
|
705
|
-
sourcesMoved: number;
|
|
706
|
-
accountsMoved: number;
|
|
707
|
-
sourceOrgDeleted: string;
|
|
708
|
-
}
|
|
625
|
+
// `Category` lives in @buildinternet/releases-core/categories — import it from
|
|
626
|
+
// there. CategorySchema is re-exported here for OpenAPI / Zod consumers only.
|
|
627
|
+
export type ProductRow = z.infer<typeof ProductRowSchema>;
|
|
628
|
+
export type ProductListItem = z.infer<typeof ProductListItemSchema>;
|
|
629
|
+
export type ProductListResponse = z.infer<typeof ProductListResponseSchema>;
|
|
630
|
+
export type ProductDetailSource = z.infer<typeof ProductDetailSourceSchema>;
|
|
631
|
+
export type ProductDetail = z.infer<typeof ProductDetailSchema>;
|
|
632
|
+
export type CreateProductBody = z.infer<typeof CreateProductBodySchema>;
|
|
633
|
+
export type UpdateProductBody = z.infer<typeof UpdateProductBodySchema>;
|
|
634
|
+
export type AdoptProductBody = z.infer<typeof AdoptProductBodySchema>;
|
|
635
|
+
export type ProductAdoptResult = z.infer<typeof ProductAdoptResultSchema>;
|
|
636
|
+
export type ProductAdoptDryRun = z.infer<typeof ProductAdoptDryRunSchema>;
|
|
637
|
+
export type ProductAdoptResponse = z.infer<typeof ProductAdoptResponseSchema>;
|
|
638
|
+
export type ProductDeleteResponse = z.infer<typeof ProductDeleteResponseSchema>;
|
|
709
639
|
|
|
710
640
|
// ── Taxonomy (categories + tags) ──
|
|
711
641
|
|
|
@@ -939,3 +869,23 @@ export interface EmbedStatusResponse {
|
|
|
939
869
|
};
|
|
940
870
|
chunks: { total: number; embedded: number; unembedded: number };
|
|
941
871
|
}
|
|
872
|
+
|
|
873
|
+
// ── Evaluate (URL recommendation) ──
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Returned verbatim by GET /v1/evaluate. Lives in the public api-types package
|
|
877
|
+
* so external consumers (CLI, third-party clients) can import the shape
|
|
878
|
+
* without depending on worker-internal AI helpers.
|
|
879
|
+
*/
|
|
880
|
+
export interface EvaluationResult {
|
|
881
|
+
recommendedMethod: "feed" | "github" | "markdown" | "scrape" | "crawl";
|
|
882
|
+
recommendedUrl: string;
|
|
883
|
+
feedUrl?: string;
|
|
884
|
+
feedType?: "rss" | "atom" | "jsonfeed";
|
|
885
|
+
githubRepo?: string;
|
|
886
|
+
pageStructure: "single-page" | "index" | "unknown";
|
|
887
|
+
alternatives: Array<{ url: string; method: string; note: string }>;
|
|
888
|
+
confidence: "high" | "medium" | "low";
|
|
889
|
+
provider?: string;
|
|
890
|
+
notes?: string;
|
|
891
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CategorySchema, ListResponseSchema, OverviewPageItemSchema } from "./shared.js";
|
|
3
|
+
import { SourceListItemSchema } from "./sources.js";
|
|
4
|
+
import { ProductListItemSchema } from "./products.js";
|
|
5
|
+
|
|
6
|
+
export const OrgListItemSchema = z.object({
|
|
7
|
+
id: z.string(),
|
|
8
|
+
slug: z.string(),
|
|
9
|
+
name: z.string(),
|
|
10
|
+
domain: z.string().nullable(),
|
|
11
|
+
description: z.string().nullable(),
|
|
12
|
+
category: CategorySchema.nullable(),
|
|
13
|
+
avatarUrl: z.string().nullable(),
|
|
14
|
+
sourceCount: z.number().int().min(0),
|
|
15
|
+
releaseCount: z.number().int().min(0),
|
|
16
|
+
recentReleaseCount: z.number().int().min(0),
|
|
17
|
+
lastActivity: z.string().nullable(),
|
|
18
|
+
topProducts: z.array(z.string()),
|
|
19
|
+
sparkline: z.array(z.number().int().min(0)).length(30),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const OrgListResponseSchema = ListResponseSchema(OrgListItemSchema);
|
|
23
|
+
|
|
24
|
+
export const OrgAccountItemSchema = z.object({
|
|
25
|
+
platform: z.string(),
|
|
26
|
+
handle: z.string(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const OrgAccountsResponseSchema = ListResponseSchema(OrgAccountItemSchema);
|
|
30
|
+
export const OrgTagsResponseSchema = ListResponseSchema(z.string());
|
|
31
|
+
|
|
32
|
+
export const CreateOrgBodySchema = z.object({
|
|
33
|
+
name: z.string().min(1),
|
|
34
|
+
slug: z.string().optional(),
|
|
35
|
+
domain: z.string().optional(),
|
|
36
|
+
description: z.string().optional(),
|
|
37
|
+
category: CategorySchema.optional(),
|
|
38
|
+
tags: z.array(z.string()).optional(),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const UpdateOrgBodySchema = z.object({
|
|
42
|
+
name: z.string().optional(),
|
|
43
|
+
slug: z.string().optional(),
|
|
44
|
+
domain: z.string().nullable().optional(),
|
|
45
|
+
description: z.string().nullable().optional(),
|
|
46
|
+
category: CategorySchema.nullable().optional(),
|
|
47
|
+
tags: z.array(z.string()).optional(),
|
|
48
|
+
aliases: z.array(z.string()).optional(),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Org detail's products query selects a strict subset of `ProductListItem` —
|
|
52
|
+
// no category/orgId/createdAt because the parent already names the org and
|
|
53
|
+
// the org detail UI doesn't surface those columns.
|
|
54
|
+
const OrgDetailProductSchema = ProductListItemSchema.pick({
|
|
55
|
+
id: true,
|
|
56
|
+
slug: true,
|
|
57
|
+
name: true,
|
|
58
|
+
url: true,
|
|
59
|
+
description: true,
|
|
60
|
+
sourceCount: true,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const OrgDetailPlaybookSchema = z.object({
|
|
64
|
+
scope: z.literal("playbook"),
|
|
65
|
+
content: z.string(),
|
|
66
|
+
updatedAt: z.string(),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
export const OrgDetailSchema = z.object({
|
|
70
|
+
id: z.string().optional(),
|
|
71
|
+
slug: z.string(),
|
|
72
|
+
name: z.string(),
|
|
73
|
+
domain: z.string().nullable(),
|
|
74
|
+
description: z.string().nullable().optional(),
|
|
75
|
+
category: CategorySchema.nullable().optional(),
|
|
76
|
+
avatarUrl: z.string().nullable(),
|
|
77
|
+
tags: z.array(z.string()).optional(),
|
|
78
|
+
sourceCount: z.number().int().min(0),
|
|
79
|
+
releaseCount: z.number().int().min(0),
|
|
80
|
+
releasesLast30Days: z.number().int().min(0),
|
|
81
|
+
avgReleasesPerWeek: z.number(),
|
|
82
|
+
lastFetchedAt: z.string().nullable(),
|
|
83
|
+
lastPolledAt: z.string().nullable(),
|
|
84
|
+
trackingSince: z.string(),
|
|
85
|
+
aliases: z.array(z.string()).optional(),
|
|
86
|
+
accounts: z.array(OrgAccountItemSchema),
|
|
87
|
+
products: z.array(OrgDetailProductSchema),
|
|
88
|
+
sources: z.array(SourceListItemSchema),
|
|
89
|
+
overview: OverviewPageItemSchema.nullable().optional(),
|
|
90
|
+
playbook: OrgDetailPlaybookSchema.nullable().optional(),
|
|
91
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CategorySchema, ListResponseSchema } from "./shared.js";
|
|
3
|
+
import { SourceTypeSchema } from "./sources.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Raw `products` table row, returned by `POST /v1/products` and
|
|
7
|
+
* `PATCH /v1/products/:slug`. The OSS CLI types these responses against
|
|
8
|
+
* `Product` from `@buildinternet/releases-core/schema` (the drizzle row
|
|
9
|
+
* type), so `embeddedAt` and `deletedAt` stay on the wire even though
|
|
10
|
+
* they're internal columns. `deletedAt` is always `null` on these paths
|
|
11
|
+
* (live rows only); `embeddedAt` reflects when the product was last
|
|
12
|
+
* indexed for semantic search.
|
|
13
|
+
*/
|
|
14
|
+
export const ProductRowSchema = z.object({
|
|
15
|
+
id: z.string(),
|
|
16
|
+
name: z.string(),
|
|
17
|
+
slug: z.string(),
|
|
18
|
+
orgId: z.string(),
|
|
19
|
+
url: z.string().nullable(),
|
|
20
|
+
description: z.string().nullable(),
|
|
21
|
+
category: CategorySchema.nullable(),
|
|
22
|
+
createdAt: z.string(),
|
|
23
|
+
embeddedAt: z.string().nullable(),
|
|
24
|
+
deletedAt: z.string().nullable(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Per-product row returned by `GET /v1/products`. Adds `sourceCount` to
|
|
29
|
+
* the row shape but omits the internal `embeddedAt` / `deletedAt` columns
|
|
30
|
+
* (the list handler explicitly selects only the user-facing fields).
|
|
31
|
+
*/
|
|
32
|
+
export const ProductListItemSchema = z.object({
|
|
33
|
+
id: z.string(),
|
|
34
|
+
name: z.string(),
|
|
35
|
+
slug: z.string(),
|
|
36
|
+
orgId: z.string(),
|
|
37
|
+
url: z.string().nullable(),
|
|
38
|
+
description: z.string().nullable(),
|
|
39
|
+
category: CategorySchema.nullable(),
|
|
40
|
+
createdAt: z.string(),
|
|
41
|
+
sourceCount: z.number().int().min(0),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export const ProductListResponseSchema = ListResponseSchema(ProductListItemSchema);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Embedded source row returned in `ProductDetail.sources`. The detail
|
|
48
|
+
* handler explicitly selects only this small subset; reuses the shared
|
|
49
|
+
* `SourceTypeSchema` enum so source types stay in one place.
|
|
50
|
+
*/
|
|
51
|
+
export const ProductDetailSourceSchema = z.object({
|
|
52
|
+
id: z.string(),
|
|
53
|
+
slug: z.string(),
|
|
54
|
+
name: z.string(),
|
|
55
|
+
type: SourceTypeSchema,
|
|
56
|
+
url: z.string(),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Returned by `GET /v1/products/:identifier` (and the org-scoped twin).
|
|
61
|
+
* Spreads the raw product row and adds `sources`, `tags`, `aliases`.
|
|
62
|
+
*/
|
|
63
|
+
export const ProductDetailSchema = ProductRowSchema.extend({
|
|
64
|
+
sources: z.array(ProductDetailSourceSchema),
|
|
65
|
+
tags: z.array(z.string()),
|
|
66
|
+
aliases: z.array(z.string()),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/** Body accepted by `POST /v1/products`. */
|
|
70
|
+
export const CreateProductBodySchema = z.object({
|
|
71
|
+
name: z.string().min(1),
|
|
72
|
+
orgId: z.string().optional(),
|
|
73
|
+
orgSlug: z.string().optional(),
|
|
74
|
+
slug: z.string().optional(),
|
|
75
|
+
url: z.string().optional(),
|
|
76
|
+
description: z.string().optional(),
|
|
77
|
+
category: CategorySchema.optional(),
|
|
78
|
+
tags: z.array(z.string()).optional(),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/** Body accepted by `PATCH /v1/products/:slug`. */
|
|
82
|
+
export const UpdateProductBodySchema = z.object({
|
|
83
|
+
name: z.string().optional(),
|
|
84
|
+
url: z.string().nullable().optional(),
|
|
85
|
+
description: z.string().nullable().optional(),
|
|
86
|
+
category: CategorySchema.nullable().optional(),
|
|
87
|
+
tags: z.array(z.string()).optional(),
|
|
88
|
+
aliases: z.array(z.string()).optional(),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
/** Body accepted by `POST /v1/products/adopt`. */
|
|
92
|
+
export const AdoptProductBodySchema = z.object({
|
|
93
|
+
sourceOrgSlug: z.string().min(1),
|
|
94
|
+
targetOrgSlug: z.string().min(1),
|
|
95
|
+
slug: z.string().optional(),
|
|
96
|
+
url: z.string().optional(),
|
|
97
|
+
dryRun: z.boolean().optional(),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
/** Live (non-dryRun) result from `POST /v1/products/adopt`. */
|
|
101
|
+
export const ProductAdoptResultSchema = z.object({
|
|
102
|
+
product: ProductRowSchema,
|
|
103
|
+
sourcesMoved: z.number().int().min(0),
|
|
104
|
+
accountsMoved: z.number().int().min(0),
|
|
105
|
+
sourceOrgDeleted: z.string(),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
/** Dry-run preview from `POST /v1/products/adopt` with `dryRun: true`. */
|
|
109
|
+
export const ProductAdoptDryRunSchema = z.object({
|
|
110
|
+
dryRun: z.literal(true),
|
|
111
|
+
product: z.object({
|
|
112
|
+
name: z.string(),
|
|
113
|
+
slug: z.string(),
|
|
114
|
+
url: z.string().nullable(),
|
|
115
|
+
orgSlug: z.string(),
|
|
116
|
+
}),
|
|
117
|
+
sourcesToMove: z.array(z.string()),
|
|
118
|
+
sourceOrgToDelete: z.string(),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
/** Response shape returned by `POST /v1/products/adopt` (union of live + dry-run). */
|
|
122
|
+
export const ProductAdoptResponseSchema = z.union([
|
|
123
|
+
ProductAdoptResultSchema,
|
|
124
|
+
ProductAdoptDryRunSchema,
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
/** Response shape returned by `DELETE /v1/products/:identifier`. */
|
|
128
|
+
export const ProductDeleteResponseSchema = z.object({
|
|
129
|
+
deleted: z.literal(true),
|
|
130
|
+
hard: z.literal(true).optional(),
|
|
131
|
+
deletedAt: z.string().optional(),
|
|
132
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { RELEASE_TYPES } from "@buildinternet/releases-core/schema";
|
|
3
|
+
import { CATEGORIES } from "@buildinternet/releases-core/categories";
|
|
4
|
+
|
|
5
|
+
export const CategorySchema = z.enum(CATEGORIES);
|
|
6
|
+
|
|
7
|
+
export const MediaItemSchema = z.object({
|
|
8
|
+
type: z.enum(["image", "video", "gif"]),
|
|
9
|
+
url: z.string(),
|
|
10
|
+
alt: z.string().optional(),
|
|
11
|
+
r2Url: z.string().optional(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const PaginationSchema = z.object({
|
|
15
|
+
page: z.number().int().min(1),
|
|
16
|
+
pageSize: z.number().int().min(1),
|
|
17
|
+
returned: z.number().int().min(0),
|
|
18
|
+
totalItems: z.number().int().min(0).optional(),
|
|
19
|
+
totalPages: z.number().int().min(0).optional(),
|
|
20
|
+
hasMore: z.boolean(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const ListResponseSchema = <T extends z.ZodTypeAny>(item: T) =>
|
|
24
|
+
z.object({
|
|
25
|
+
items: z.array(item),
|
|
26
|
+
pagination: PaginationSchema,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const ErrorResponseSchema = z.object({
|
|
30
|
+
error: z.string(),
|
|
31
|
+
message: z.string(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const StatsSchema = z.object({
|
|
35
|
+
orgs: z.number().int().min(0),
|
|
36
|
+
sources: z.number().int().min(0),
|
|
37
|
+
releases: z.number().int().min(0),
|
|
38
|
+
products: z.number().int().min(0),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const ReleaseTypeSchema = z.enum(RELEASE_TYPES);
|
|
42
|
+
|
|
43
|
+
export const ReleaseItemSchema = z.object({
|
|
44
|
+
id: z.string().optional(),
|
|
45
|
+
version: z.string().nullable(),
|
|
46
|
+
title: z.string(),
|
|
47
|
+
summary: z.string(),
|
|
48
|
+
content: z.string().optional(),
|
|
49
|
+
publishedAt: z.string().nullable(),
|
|
50
|
+
url: z.string().nullable(),
|
|
51
|
+
media: z.array(MediaItemSchema).optional(),
|
|
52
|
+
// `.optional()` — older API responses (mid-deploy or pinned old workers)
|
|
53
|
+
// may omit `type`; consumers should treat `undefined` as `"feature"`.
|
|
54
|
+
type: ReleaseTypeSchema.optional(),
|
|
55
|
+
// Pre-release flag (alpha/beta/rc/preview/nightly). Computed at ingest —
|
|
56
|
+
// GitHub uses the API's authoritative `prerelease` field; other adapters
|
|
57
|
+
// fall back to a SemVer-prerelease regex. `.optional()` for the same
|
|
58
|
+
// older-response degrade-gracefully reason as `type`.
|
|
59
|
+
prerelease: z.boolean().optional(),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export const ReleaseSummaryItemSchema = z.object({
|
|
63
|
+
year: z.number().int().nullable().optional(),
|
|
64
|
+
month: z.number().int().nullable().optional(),
|
|
65
|
+
windowDays: z.number().int().nullable().optional(),
|
|
66
|
+
summary: z.string(),
|
|
67
|
+
releaseCount: z.number().int().min(0),
|
|
68
|
+
generatedAt: z.string(),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
export const OverviewPageItemSchema = z.object({
|
|
72
|
+
scope: z.enum(["org", "product"]),
|
|
73
|
+
orgSlug: z.string().nullable().optional(),
|
|
74
|
+
productSlug: z.string().nullable().optional(),
|
|
75
|
+
content: z.string(),
|
|
76
|
+
releaseCount: z.number().int().min(0),
|
|
77
|
+
lastContributingReleaseAt: z.string().nullable(),
|
|
78
|
+
generatedAt: z.string(),
|
|
79
|
+
updatedAt: z.string(),
|
|
80
|
+
});
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
SOURCE_TYPES,
|
|
4
|
+
SOURCE_DISCOVERY,
|
|
5
|
+
SOURCE_FETCH_PRIORITIES,
|
|
6
|
+
} from "@buildinternet/releases-core/source-enums";
|
|
7
|
+
import {
|
|
8
|
+
ListResponseSchema,
|
|
9
|
+
PaginationSchema,
|
|
10
|
+
ReleaseItemSchema,
|
|
11
|
+
ReleaseSummaryItemSchema,
|
|
12
|
+
} from "./shared.js";
|
|
13
|
+
|
|
14
|
+
export const SourceTypeSchema = z.enum(SOURCE_TYPES);
|
|
15
|
+
export const SourceDiscoverySchema = z.enum(SOURCE_DISCOVERY);
|
|
16
|
+
export const SourceFetchPrioritySchema = z.enum(SOURCE_FETCH_PRIORITIES);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Per-source row used in list/embedded contexts (e.g. nested inside OrgDetail).
|
|
20
|
+
* Most fields are optional — embedded shapes (org detail) only carry the
|
|
21
|
+
* subset needed for that view.
|
|
22
|
+
*/
|
|
23
|
+
export const SourceListItemSchema = z.object({
|
|
24
|
+
id: z.string().optional(),
|
|
25
|
+
slug: z.string(),
|
|
26
|
+
name: z.string(),
|
|
27
|
+
type: SourceTypeSchema,
|
|
28
|
+
url: z.string().optional(),
|
|
29
|
+
// `orgSlug` is populated on the standalone `/v1/sources` list path; the
|
|
30
|
+
// `OrgDetail.sources` embedded path omits it (the parent already names the org).
|
|
31
|
+
orgSlug: z.string().nullable().optional(),
|
|
32
|
+
releaseCount: z.number().int().min(0),
|
|
33
|
+
latestVersion: z.string().nullable(),
|
|
34
|
+
latestDate: z.string().nullable(),
|
|
35
|
+
latestAddedAt: z.string().nullable().optional(),
|
|
36
|
+
isPrimary: z.boolean().optional(),
|
|
37
|
+
isHidden: z.boolean().optional(),
|
|
38
|
+
/**
|
|
39
|
+
* How the row was created. Optional on the wire so older API responses
|
|
40
|
+
* (mid-deploy or pinned old workers) degrade gracefully — consumers that
|
|
41
|
+
* see `undefined` should treat it as `"curated"`.
|
|
42
|
+
*/
|
|
43
|
+
discovery: SourceDiscoverySchema.optional(),
|
|
44
|
+
fetchPriority: SourceFetchPrioritySchema.nullable().optional(),
|
|
45
|
+
lastFetchedAt: z.string().nullable().optional(),
|
|
46
|
+
lastPolledAt: z.string().nullable().optional(),
|
|
47
|
+
changeDetectedAt: z.string().nullable().optional(),
|
|
48
|
+
consecutiveNoChange: z.number().int().nullable().optional(),
|
|
49
|
+
consecutiveErrors: z.number().int().nullable().optional(),
|
|
50
|
+
nextFetchAfter: z.string().nullable().optional(),
|
|
51
|
+
medianGapDays: z.number().nullable().optional(),
|
|
52
|
+
lastRetieredAt: z.string().nullable().optional(),
|
|
53
|
+
metadata: z.string().nullable().optional(),
|
|
54
|
+
productName: z.string().nullable().optional(),
|
|
55
|
+
productSlug: z.string().nullable().optional(),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Canonical row returned by `GET /v1/sources` (list). Superset of
|
|
60
|
+
* `SourceListItem` with required `id` / `orgName` / `orgSlug` fields used by
|
|
61
|
+
* the CLI `list` command.
|
|
62
|
+
*/
|
|
63
|
+
export const SourceWithOrgSchema = z.object({
|
|
64
|
+
id: z.string(),
|
|
65
|
+
name: z.string(),
|
|
66
|
+
slug: z.string(),
|
|
67
|
+
type: SourceTypeSchema,
|
|
68
|
+
url: z.string(),
|
|
69
|
+
orgName: z.string().nullable(),
|
|
70
|
+
orgSlug: z.string().nullable(),
|
|
71
|
+
productName: z.string().nullable(),
|
|
72
|
+
productSlug: z.string().nullable(),
|
|
73
|
+
isPrimary: z.boolean(),
|
|
74
|
+
isHidden: z.boolean().nullable(),
|
|
75
|
+
discovery: SourceDiscoverySchema.optional(),
|
|
76
|
+
metadata: z.string().nullable(),
|
|
77
|
+
releaseCount: z.number().int().min(0),
|
|
78
|
+
latestVersion: z.string().nullable(),
|
|
79
|
+
latestDate: z.string().nullable(),
|
|
80
|
+
lastFetchedAt: z.string().nullable(),
|
|
81
|
+
lastPolledAt: z.string().nullable(),
|
|
82
|
+
fetchPriority: SourceFetchPrioritySchema.nullable(),
|
|
83
|
+
changeDetectedAt: z.string().nullable(),
|
|
84
|
+
consecutiveNoChange: z.number().int().nullable(),
|
|
85
|
+
consecutiveErrors: z.number().int().nullable(),
|
|
86
|
+
nextFetchAfter: z.string().nullable(),
|
|
87
|
+
medianGapDays: z.number().nullable(),
|
|
88
|
+
lastRetieredAt: z.string().nullable(),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
export const SourceListResponseSchema = ListResponseSchema(SourceWithOrgSchema);
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* `GET /v1/sources` returns a bare array by default and an envelope when
|
|
95
|
+
* `?envelope=true` is passed. The OpenAPI spec advertises the union so
|
|
96
|
+
* either client expectation is documented.
|
|
97
|
+
*/
|
|
98
|
+
export const SourceListResultSchema = z.union([
|
|
99
|
+
z.array(SourceWithOrgSchema),
|
|
100
|
+
SourceListResponseSchema,
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
const SourceDetailSummariesSchema = z.object({
|
|
104
|
+
rolling: ReleaseSummaryItemSchema.nullable(),
|
|
105
|
+
monthly: z.array(ReleaseSummaryItemSchema),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Resolved attribution block returned alongside a source row. `id` is
|
|
110
|
+
* additive vs the legacy `{ slug, name }` shape — older clients that ignored
|
|
111
|
+
* it still parse cleanly. `productId` / `productSlug` are sibling top-level
|
|
112
|
+
* fields on the source response (kept flat to mirror `orgId`), not nested
|
|
113
|
+
* inside this block.
|
|
114
|
+
*/
|
|
115
|
+
export const SourceOrgRefSchema = z.object({
|
|
116
|
+
id: z.string(),
|
|
117
|
+
slug: z.string(),
|
|
118
|
+
name: z.string(),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
export const SourceDetailSchema = z.object({
|
|
122
|
+
id: z.string(),
|
|
123
|
+
slug: z.string(),
|
|
124
|
+
name: z.string(),
|
|
125
|
+
type: SourceTypeSchema,
|
|
126
|
+
url: z.string(),
|
|
127
|
+
orgId: z.string().nullable(),
|
|
128
|
+
productId: z.string().nullable(),
|
|
129
|
+
productSlug: z.string().nullable(),
|
|
130
|
+
// Hidden sources are reachable by direct URL but excluded from listings,
|
|
131
|
+
// sitemap, and AI features. On-demand lookups and admin-suppressed rows
|
|
132
|
+
// set this true; canonical curated/agent sources leave it false.
|
|
133
|
+
isHidden: z.boolean(),
|
|
134
|
+
// Pairs with `isHidden` to distinguish admin-suppressed rows (`curated` /
|
|
135
|
+
// `agent` + hidden) from rows materialized by `/v1/lookups` (`on_demand`).
|
|
136
|
+
// Optional for graceful degradation against older API responses.
|
|
137
|
+
discovery: SourceDiscoverySchema.optional(),
|
|
138
|
+
isPrimary: z.boolean(),
|
|
139
|
+
metadata: z.string(),
|
|
140
|
+
changelogUrl: z.string().nullable().optional(),
|
|
141
|
+
hasChangelogFile: z.boolean().optional(),
|
|
142
|
+
org: SourceOrgRefSchema.nullable(),
|
|
143
|
+
releaseCount: z.number().int().min(0),
|
|
144
|
+
releasesLast30Days: z.number().int().min(0),
|
|
145
|
+
avgReleasesPerWeek: z.number(),
|
|
146
|
+
latestVersion: z.string().nullable(),
|
|
147
|
+
latestDate: z.string().nullable(),
|
|
148
|
+
lastFetchedAt: z.string().nullable(),
|
|
149
|
+
lastPolledAt: z.string().nullable(),
|
|
150
|
+
trackingSince: z.string(),
|
|
151
|
+
releases: z.array(ReleaseItemSchema),
|
|
152
|
+
pagination: PaginationSchema,
|
|
153
|
+
summaries: SourceDetailSummariesSchema,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Response returned by `POST /v1/sources` and `PATCH /v1/sources/:slug`.
|
|
158
|
+
*
|
|
159
|
+
* Pre-#794 these endpoints returned the bare drizzle row, leaving callers to
|
|
160
|
+
* round-trip a separate `GET` to confirm the resolved org/product attribution.
|
|
161
|
+
* Now they return the same source row plus a resolved `org { id, slug, name }`
|
|
162
|
+
* block and `productSlug`, so an agent can answer "did the write take?" from
|
|
163
|
+
* the response alone.
|
|
164
|
+
*
|
|
165
|
+
* Wire fields are loosely-typed (`z.unknown()`) for the timestamps/counters
|
|
166
|
+
* that aren't load-bearing for callers — the row is large and we don't want
|
|
167
|
+
* to hand-mirror every column. Stable, agent-relevant fields are typed.
|
|
168
|
+
*/
|
|
169
|
+
export const SourceMutationResponseSchema = z.looseObject({
|
|
170
|
+
id: z.string(),
|
|
171
|
+
slug: z.string(),
|
|
172
|
+
name: z.string(),
|
|
173
|
+
type: SourceTypeSchema,
|
|
174
|
+
url: z.string(),
|
|
175
|
+
orgId: z.string().nullable(),
|
|
176
|
+
productId: z.string().nullable(),
|
|
177
|
+
productSlug: z.string().nullable(),
|
|
178
|
+
org: SourceOrgRefSchema.nullable(),
|
|
179
|
+
metadata: z.string().nullable(),
|
|
180
|
+
isPrimary: z.boolean().nullable().optional(),
|
|
181
|
+
isHidden: z.boolean().nullable().optional(),
|
|
182
|
+
discovery: SourceDiscoverySchema.optional(),
|
|
183
|
+
createdAt: z.string().optional(),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
/** Body accepted by `PATCH /v1/sources/:slug`. */
|
|
187
|
+
export const SourcePatchInputSchema = z.object({
|
|
188
|
+
name: z.string().optional(),
|
|
189
|
+
url: z.string().optional(),
|
|
190
|
+
type: SourceTypeSchema.optional(),
|
|
191
|
+
slug: z.string().optional(),
|
|
192
|
+
metadata: z.string().optional(),
|
|
193
|
+
orgId: z.string().nullable().optional(),
|
|
194
|
+
productId: z.string().nullable().optional(),
|
|
195
|
+
lastFetchedAt: z.string().nullable().optional(),
|
|
196
|
+
lastContentHash: z.string().nullable().optional(),
|
|
197
|
+
fetchPriority: SourceFetchPrioritySchema.optional(),
|
|
198
|
+
consecutiveNoChange: z.number().int().optional(),
|
|
199
|
+
consecutiveErrors: z.number().int().optional(),
|
|
200
|
+
nextFetchAfter: z.string().nullable().optional(),
|
|
201
|
+
isPrimary: z.boolean().optional(),
|
|
202
|
+
isHidden: z.boolean().optional(),
|
|
203
|
+
changeDetectedAt: z.string().nullable().optional(),
|
|
204
|
+
lastPolledAt: z.string().nullable().optional(),
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
/** Body accepted by `POST /v1/sources`. */
|
|
208
|
+
export const CreateSourceBodySchema = z.object({
|
|
209
|
+
name: z.string().min(1),
|
|
210
|
+
url: z.string().min(1),
|
|
211
|
+
type: SourceTypeSchema.optional(),
|
|
212
|
+
slug: z.string().optional(),
|
|
213
|
+
orgId: z.string().optional(),
|
|
214
|
+
orgSlug: z.string().optional(),
|
|
215
|
+
/**
|
|
216
|
+
* Either accepted; `productId` wins when both are supplied. `productSlug`
|
|
217
|
+
* resolves within the requested org (slug uniqueness is per-org). The
|
|
218
|
+
* legacy `productId` ingestion path is preserved for callers that already
|
|
219
|
+
* resolve products client-side; new agent flows should pass `productSlug`.
|
|
220
|
+
*/
|
|
221
|
+
productId: z.string().optional(),
|
|
222
|
+
productSlug: z.string().optional(),
|
|
223
|
+
metadata: z.string().optional(),
|
|
224
|
+
isPrimary: z.boolean().optional(),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
export const ChangelogFileSummarySchema = z.object({
|
|
228
|
+
path: z.string(),
|
|
229
|
+
filename: z.string(),
|
|
230
|
+
url: z.string(),
|
|
231
|
+
bytes: z.number().int().min(0),
|
|
232
|
+
fetchedAt: z.string(),
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
export const SourceChangelogResponseSchema = z.object({
|
|
236
|
+
path: z.string(),
|
|
237
|
+
filename: z.string(),
|
|
238
|
+
url: z.string(),
|
|
239
|
+
rawUrl: z.string(),
|
|
240
|
+
content: z.string(),
|
|
241
|
+
bytes: z.number().int().min(0),
|
|
242
|
+
fetchedAt: z.string(),
|
|
243
|
+
/** Character offset of the first character in `content` within the full file. */
|
|
244
|
+
offset: z.number().int().min(0),
|
|
245
|
+
/** The limit (in chars) that was applied to produce this slice. */
|
|
246
|
+
limit: z.number().int().min(0),
|
|
247
|
+
/** Next offset to request for the next slice, or null if `content` is the tail. */
|
|
248
|
+
nextOffset: z.number().int().nullable(),
|
|
249
|
+
/** Total length of the full file in characters. */
|
|
250
|
+
totalChars: z.number().int().min(0),
|
|
251
|
+
/** Requested token budget when in token mode (cl100k_base). */
|
|
252
|
+
tokens: z.number().int().optional(),
|
|
253
|
+
/** Encoded token count of the returned `content`. Set in token mode. */
|
|
254
|
+
sliceTokens: z.number().int().optional(),
|
|
255
|
+
/** Full-file token count (cl100k_base). Always populated. */
|
|
256
|
+
totalTokens: z.number().int().min(0),
|
|
257
|
+
/** True when the upstream file exceeded the 1MB cap and content was sliced. */
|
|
258
|
+
truncated: z.boolean(),
|
|
259
|
+
/** Byte offset where the file was truncated, or null when not truncated. */
|
|
260
|
+
truncatedAt: z.number().int().nullable(),
|
|
261
|
+
/**
|
|
262
|
+
* Index of every changelog file tracked for this source (root plus any
|
|
263
|
+
* discovered per-package files). Always present even for single-file
|
|
264
|
+
* sources so clients can lazily render a file picker.
|
|
265
|
+
*/
|
|
266
|
+
files: z.array(ChangelogFileSummarySchema),
|
|
267
|
+
});
|