@buildinternet/releases-api-types 0.21.0 → 0.23.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 +141 -8
- package/src/schemas/collections.ts +93 -13
- package/src/schemas/feedback.ts +56 -0
- package/src/schemas/lookups.ts +2 -0
- package/src/schemas/orgs.ts +38 -6
- package/src/schemas/products.ts +73 -2
- package/src/schemas/related.ts +4 -0
- package/src/schemas/releases.ts +18 -1
- package/src/schemas/resolve.ts +15 -0
- package/src/schemas/search.ts +13 -7
- package/src/schemas/shared.ts +11 -0
- package/src/schemas/sitemap.ts +6 -0
- package/src/schemas/sources.ts +21 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buildinternet/releases-api-types",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.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",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"README.md"
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@buildinternet/releases-core": "^0.
|
|
20
|
+
"@buildinternet/releases-core": "^0.22.0",
|
|
21
21
|
"zod": "^4.4.3"
|
|
22
22
|
},
|
|
23
23
|
"publishConfig": {
|
package/src/api-types.ts
CHANGED
|
@@ -10,6 +10,8 @@ export type {
|
|
|
10
10
|
SourceDiscovery,
|
|
11
11
|
SourceFetchPriority,
|
|
12
12
|
} from "@buildinternet/releases-core/source-enums";
|
|
13
|
+
// Re-export above doesn't create local bindings; import for use in interfaces below.
|
|
14
|
+
import type { SourceType, SourceFetchPriority } from "@buildinternet/releases-core/source-enums";
|
|
13
15
|
import type {
|
|
14
16
|
MediaItemSchema,
|
|
15
17
|
PaginationSchema,
|
|
@@ -95,6 +97,7 @@ import type {
|
|
|
95
97
|
} from "./schemas/sources.js";
|
|
96
98
|
import type {
|
|
97
99
|
ProductRowSchema,
|
|
100
|
+
ProductCreateResponseSchema,
|
|
98
101
|
ProductListItemSchema,
|
|
99
102
|
ProductListResponseSchema,
|
|
100
103
|
ProductDetailSourceSchema,
|
|
@@ -109,9 +112,12 @@ import type {
|
|
|
109
112
|
ProductTagsListResponseSchema,
|
|
110
113
|
ProductTagsBodySchema,
|
|
111
114
|
ProductTagsMutationResponseSchema,
|
|
115
|
+
ProductActivityResponseSchema,
|
|
116
|
+
ProductHeatmapResponseSchema,
|
|
112
117
|
} from "./schemas/products.js";
|
|
113
118
|
import type {
|
|
114
119
|
ReleaseLatestSourceSchema,
|
|
120
|
+
ReleaseLatestProductSchema,
|
|
115
121
|
ReleaseLatestItemSchema,
|
|
116
122
|
ReleaseLatestResponseSchema,
|
|
117
123
|
ReleaseCoverageRowSchema,
|
|
@@ -171,6 +177,14 @@ import type {
|
|
|
171
177
|
StatsRecentActivitySchema,
|
|
172
178
|
StatsResponseSchema,
|
|
173
179
|
} from "./schemas/stats.js";
|
|
180
|
+
import type {
|
|
181
|
+
FeedbackTypeSchema,
|
|
182
|
+
FeedbackStatusSchema,
|
|
183
|
+
FeedbackItemSchema,
|
|
184
|
+
FeedbackListResponseSchema,
|
|
185
|
+
FeedbackUpdateBodySchema,
|
|
186
|
+
FeedbackDeleteResponseSchema,
|
|
187
|
+
} from "./schemas/feedback.js";
|
|
174
188
|
import type { SitemapSourceSchema, SitemapPayloadSchema } from "./schemas/sitemap.js";
|
|
175
189
|
import type {
|
|
176
190
|
RelatedScopeSchema,
|
|
@@ -187,6 +201,9 @@ import type {
|
|
|
187
201
|
} from "./schemas/related.js";
|
|
188
202
|
import type {
|
|
189
203
|
CollectionMemberOrgSchema,
|
|
204
|
+
ProductParentOrgSchema,
|
|
205
|
+
CollectionMemberProductSchema,
|
|
206
|
+
CollectionMemberSchema,
|
|
190
207
|
CollectionListItemSchema,
|
|
191
208
|
CollectionListResponseSchema,
|
|
192
209
|
CollectionDetailSchema,
|
|
@@ -304,6 +321,7 @@ export {
|
|
|
304
321
|
} from "./schemas/sources.js";
|
|
305
322
|
export {
|
|
306
323
|
ProductRowSchema,
|
|
324
|
+
ProductCreateResponseSchema,
|
|
307
325
|
ProductListItemSchema,
|
|
308
326
|
ProductListResponseSchema,
|
|
309
327
|
ProductDetailSourceSchema,
|
|
@@ -318,6 +336,8 @@ export {
|
|
|
318
336
|
ProductTagsListResponseSchema,
|
|
319
337
|
ProductTagsBodySchema,
|
|
320
338
|
ProductTagsMutationResponseSchema,
|
|
339
|
+
ProductActivityResponseSchema,
|
|
340
|
+
ProductHeatmapResponseSchema,
|
|
321
341
|
} from "./schemas/products.js";
|
|
322
342
|
export {
|
|
323
343
|
LookupStatusSchema,
|
|
@@ -347,6 +367,7 @@ export {
|
|
|
347
367
|
} from "./schemas/taxonomy.js";
|
|
348
368
|
export {
|
|
349
369
|
ReleaseLatestSourceSchema,
|
|
370
|
+
ReleaseLatestProductSchema,
|
|
350
371
|
ReleaseLatestItemSchema,
|
|
351
372
|
ReleaseLatestResponseSchema,
|
|
352
373
|
ReleaseCoverageRowSchema,
|
|
@@ -381,6 +402,14 @@ export {
|
|
|
381
402
|
StatsRecentActivitySchema,
|
|
382
403
|
StatsResponseSchema,
|
|
383
404
|
} from "./schemas/stats.js";
|
|
405
|
+
export {
|
|
406
|
+
FeedbackTypeSchema,
|
|
407
|
+
FeedbackStatusSchema,
|
|
408
|
+
FeedbackItemSchema,
|
|
409
|
+
FeedbackListResponseSchema,
|
|
410
|
+
FeedbackUpdateBodySchema,
|
|
411
|
+
FeedbackDeleteResponseSchema,
|
|
412
|
+
} from "./schemas/feedback.js";
|
|
384
413
|
export { SitemapSourceSchema, SitemapPayloadSchema } from "./schemas/sitemap.js";
|
|
385
414
|
export {
|
|
386
415
|
RelatedScopeSchema,
|
|
@@ -397,6 +426,9 @@ export {
|
|
|
397
426
|
} from "./schemas/related.js";
|
|
398
427
|
export {
|
|
399
428
|
CollectionMemberOrgSchema,
|
|
429
|
+
ProductParentOrgSchema,
|
|
430
|
+
CollectionMemberProductSchema,
|
|
431
|
+
CollectionMemberSchema,
|
|
400
432
|
CollectionListItemSchema,
|
|
401
433
|
CollectionListResponseSchema,
|
|
402
434
|
CollectionDetailSchema,
|
|
@@ -426,6 +458,8 @@ export {
|
|
|
426
458
|
UpdatePlaybookNotesBodySchema,
|
|
427
459
|
UpdatePlaybookNotesResponseSchema,
|
|
428
460
|
} from "./schemas/overviews.js";
|
|
461
|
+
export { ResolveResponseSchema } from "./schemas/resolve.js";
|
|
462
|
+
export type { ResolveResponse } from "./schemas/resolve.js";
|
|
429
463
|
|
|
430
464
|
// ── Media ──
|
|
431
465
|
|
|
@@ -566,6 +600,61 @@ export interface OrgsRollupResponse extends ListResponse<OrgsRollupRow> {
|
|
|
566
600
|
};
|
|
567
601
|
}
|
|
568
602
|
|
|
603
|
+
// ── Admin telemetry: stuck sources ──
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* A source whose recent fetch history is all errors with no reachability — a
|
|
607
|
+
* candidate for pausing. "Stuck" means: within the last `window` non-`dry_run`
|
|
608
|
+
* fetch attempts, every one was an `error` (zero `success`/`no_change`) and
|
|
609
|
+
* there were at least `minAttempts` of them.
|
|
610
|
+
*
|
|
611
|
+
* This keys off the `fetch_log` error streak rather than
|
|
612
|
+
* `sources.consecutive_errors`, because the scrape/agent fetch path never bumps
|
|
613
|
+
* that column (a source can fail for days with `consecutive_errors = 0`).
|
|
614
|
+
*/
|
|
615
|
+
export interface StuckSource {
|
|
616
|
+
sourceId: string;
|
|
617
|
+
sourceSlug: string;
|
|
618
|
+
name: string;
|
|
619
|
+
type: SourceType;
|
|
620
|
+
url: string;
|
|
621
|
+
kind: string | null;
|
|
622
|
+
orgSlug: string | null;
|
|
623
|
+
orgName: string | null;
|
|
624
|
+
/** Current fetch tier — `paused` rows only appear when `includePaused`. */
|
|
625
|
+
fetchPriority: SourceFetchPriority;
|
|
626
|
+
/** True when this is the org's primary changelog (Firebase's was). */
|
|
627
|
+
isPrimary: boolean;
|
|
628
|
+
isHidden: boolean;
|
|
629
|
+
/** Non-`dry_run` attempts examined in the window (all errors when stuck). */
|
|
630
|
+
recentAttempts: number;
|
|
631
|
+
/** Errors among the examined attempts (equals `recentAttempts` when stuck). */
|
|
632
|
+
recentErrors: number;
|
|
633
|
+
/** Most-recent fetch attempt timestamp (ISO), or null. */
|
|
634
|
+
lastAttemptAt: string | null;
|
|
635
|
+
/** Most-recent error message. */
|
|
636
|
+
lastError: string | null;
|
|
637
|
+
/** Most-recent error category, when the fetcher classified it. */
|
|
638
|
+
lastErrorCategory: string | null;
|
|
639
|
+
/** Last time the source was reachable (`success`/`no_change`), ISO; null = never. */
|
|
640
|
+
lastSuccessAt: string | null;
|
|
641
|
+
/** `sources.last_fetched_at` — null when the source has never fetched. */
|
|
642
|
+
lastFetchedAt: string | null;
|
|
643
|
+
/** When the source row was created (ISO) — proxy for how long it's failed. */
|
|
644
|
+
sourceCreatedAt: string | null;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
export interface StuckSourcesResponse extends ListResponse<StuckSource> {
|
|
648
|
+
meta: {
|
|
649
|
+
/** Recent non-`dry_run` attempts examined per source. */
|
|
650
|
+
window: number;
|
|
651
|
+
/** Minimum attempts in the window required to flag a source. */
|
|
652
|
+
minAttempts: number;
|
|
653
|
+
/** Whether already-paused sources were included. */
|
|
654
|
+
includePaused: boolean;
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
569
658
|
// ── Releases ──
|
|
570
659
|
|
|
571
660
|
/**
|
|
@@ -607,6 +696,8 @@ export interface ReleaseDetail {
|
|
|
607
696
|
type?: ReleaseType;
|
|
608
697
|
/** Per-category item counts from the AI release-content pass. See {@link ReleaseComposition}. */
|
|
609
698
|
composition?: ReleaseComposition | null;
|
|
699
|
+
/** App Store platform + icon, present only when `sourceType === "appstore"`. */
|
|
700
|
+
appStore?: { platform: "ios" | "macos"; iconUrl: string | null } | null;
|
|
610
701
|
}
|
|
611
702
|
|
|
612
703
|
export type ReleaseCoverageRow = z.infer<typeof ReleaseCoverageRowSchema>;
|
|
@@ -616,6 +707,7 @@ export type LinkReleaseCoverageResponse = z.infer<typeof LinkReleaseCoverageResp
|
|
|
616
707
|
export type UnlinkReleaseCoverageResponse = z.infer<typeof UnlinkReleaseCoverageResponseSchema>;
|
|
617
708
|
|
|
618
709
|
export type ReleaseLatestSource = z.infer<typeof ReleaseLatestSourceSchema>;
|
|
710
|
+
export type ReleaseLatestProduct = z.infer<typeof ReleaseLatestProductSchema>;
|
|
619
711
|
export type ReleaseLatestItem = z.infer<typeof ReleaseLatestItemSchema>;
|
|
620
712
|
export type ReleaseLatestResponse = z.infer<typeof ReleaseLatestResponseSchema>;
|
|
621
713
|
|
|
@@ -644,17 +736,22 @@ export type SearchSourceHit = z.infer<typeof SearchSourceHitSchema>;
|
|
|
644
736
|
export interface RawSourceHit extends SearchSourceHit {
|
|
645
737
|
productName?: string;
|
|
646
738
|
productCategory?: string;
|
|
739
|
+
/** Entity taxonomy kind from the `sources.kind` column. */
|
|
740
|
+
entityKind?: string | null;
|
|
647
741
|
}
|
|
648
742
|
|
|
649
743
|
/**
|
|
650
744
|
* Fold source hits into the catalog list. Sources under a matched product
|
|
651
|
-
* are dropped (product wins); orphan sources become `
|
|
745
|
+
* are dropped (product wins); orphan sources become `entryType: "source"`.
|
|
652
746
|
*/
|
|
653
747
|
export function foldSourcesIntoCatalog(
|
|
654
748
|
existingProducts: SearchCatalogHit[],
|
|
655
749
|
rawSources: RawSourceHit[],
|
|
656
750
|
): SearchCatalogHit[] {
|
|
657
|
-
const result: SearchCatalogHit[] = existingProducts.map((p) => ({
|
|
751
|
+
const result: SearchCatalogHit[] = existingProducts.map((p) => ({
|
|
752
|
+
...p,
|
|
753
|
+
entryType: "product" as const,
|
|
754
|
+
}));
|
|
658
755
|
const seen = new Set(result.map((p) => p.slug));
|
|
659
756
|
for (const s of rawSources) {
|
|
660
757
|
if (s.productSlug) {
|
|
@@ -665,7 +762,8 @@ export function foldSourcesIntoCatalog(
|
|
|
665
762
|
orgSlug: s.orgSlug,
|
|
666
763
|
orgName: s.orgName,
|
|
667
764
|
category: s.productCategory ?? null,
|
|
668
|
-
|
|
765
|
+
entryType: "product",
|
|
766
|
+
kind: undefined,
|
|
669
767
|
});
|
|
670
768
|
seen.add(s.productSlug);
|
|
671
769
|
} else {
|
|
@@ -675,7 +773,8 @@ export function foldSourcesIntoCatalog(
|
|
|
675
773
|
orgSlug: s.orgSlug,
|
|
676
774
|
orgName: s.orgName,
|
|
677
775
|
category: null,
|
|
678
|
-
|
|
776
|
+
entryType: "source",
|
|
777
|
+
kind: (s.entityKind as SearchCatalogHit["kind"]) ?? undefined,
|
|
679
778
|
sourceSlug: s.slug,
|
|
680
779
|
sourceType: s.type,
|
|
681
780
|
});
|
|
@@ -854,7 +953,7 @@ export type SourceActivity = SourceActivityResponse;
|
|
|
854
953
|
|
|
855
954
|
// ── Org Activity (Zod-derived aliases) ──
|
|
856
955
|
// The old hand-written OrgActivitySource / OrgActivity / OrgSparklines /
|
|
857
|
-
// OrgHeatmap / OrgReleaseItem
|
|
956
|
+
// OrgHeatmap / OrgReleaseItem interfaces have been
|
|
858
957
|
// replaced by Zod-derived types in the Organizations section above. Keeping
|
|
859
958
|
// the interface names as deprecated aliases so callsites don't need an
|
|
860
959
|
// immediate churn.
|
|
@@ -868,9 +967,6 @@ export type OrgSparklines = OrgSparklinesResponse;
|
|
|
868
967
|
/** @deprecated Use OrgHeatmapResponse */
|
|
869
968
|
export type OrgHeatmap = OrgHeatmapResponse;
|
|
870
969
|
|
|
871
|
-
/** @deprecated Use OrgReleasesFeedResponse */
|
|
872
|
-
export type OrgReleasesResponse = OrgReleasesFeedResponse;
|
|
873
|
-
|
|
874
970
|
// ── Org Sparklines (per-source/product breakdown) ──
|
|
875
971
|
// (covered above)
|
|
876
972
|
|
|
@@ -898,6 +994,7 @@ export interface SourceReleasesResponse {
|
|
|
898
994
|
// `Category` lives in @buildinternet/releases-core/categories — import it from
|
|
899
995
|
// there. CategorySchema is re-exported here for OpenAPI / Zod consumers only.
|
|
900
996
|
export type ProductRow = z.infer<typeof ProductRowSchema>;
|
|
997
|
+
export type ProductCreateResponse = z.infer<typeof ProductCreateResponseSchema>;
|
|
901
998
|
export type ProductListItem = z.infer<typeof ProductListItemSchema>;
|
|
902
999
|
export type ProductListResponse = z.infer<typeof ProductListResponseSchema>;
|
|
903
1000
|
export type ProductDetailSource = z.infer<typeof ProductDetailSourceSchema>;
|
|
@@ -912,6 +1009,8 @@ export type ProductDeleteResponse = z.infer<typeof ProductDeleteResponseSchema>;
|
|
|
912
1009
|
export type ProductTagsListResponse = z.infer<typeof ProductTagsListResponseSchema>;
|
|
913
1010
|
export type ProductTagsBody = z.infer<typeof ProductTagsBodySchema>;
|
|
914
1011
|
export type ProductTagsMutationResponse = z.infer<typeof ProductTagsMutationResponseSchema>;
|
|
1012
|
+
export type ProductActivityResponse = z.infer<typeof ProductActivityResponseSchema>;
|
|
1013
|
+
export type ProductHeatmapResponse = z.infer<typeof ProductHeatmapResponseSchema>;
|
|
915
1014
|
|
|
916
1015
|
// ── Taxonomy (categories + tags) ──
|
|
917
1016
|
|
|
@@ -940,6 +1039,9 @@ export type CategoryReleaseItem = CollectionReleaseItem;
|
|
|
940
1039
|
// /collections/frontier-ai-labs). Independent of the fixed `category` taxonomy.
|
|
941
1040
|
|
|
942
1041
|
export type CollectionMemberOrg = z.infer<typeof CollectionMemberOrgSchema>;
|
|
1042
|
+
export type ProductParentOrg = z.infer<typeof ProductParentOrgSchema>;
|
|
1043
|
+
export type CollectionMemberProduct = z.infer<typeof CollectionMemberProductSchema>;
|
|
1044
|
+
export type CollectionMember = z.infer<typeof CollectionMemberSchema>;
|
|
943
1045
|
export type CollectionListItem = z.infer<typeof CollectionListItemSchema>;
|
|
944
1046
|
export type CollectionListResponse = z.infer<typeof CollectionListResponseSchema>;
|
|
945
1047
|
export type CollectionDetail = z.infer<typeof CollectionDetailSchema>;
|
|
@@ -1007,6 +1109,12 @@ export interface LatestRelease {
|
|
|
1007
1109
|
*/
|
|
1008
1110
|
contentChars?: number | null;
|
|
1009
1111
|
contentTokens?: number | null;
|
|
1112
|
+
/**
|
|
1113
|
+
* Owning product, when the release's source is grouped under a product.
|
|
1114
|
+
* `null` / `undefined` when the source has no `product_id`. Additive — older
|
|
1115
|
+
* API responses omit this field; treat `undefined` as `null`. #1217.
|
|
1116
|
+
*/
|
|
1117
|
+
product?: { slug: string; name: string } | null;
|
|
1010
1118
|
}
|
|
1011
1119
|
|
|
1012
1120
|
// ── Stats ──
|
|
@@ -1027,6 +1135,14 @@ export type StatsResponse = z.infer<typeof StatsResponseSchema>;
|
|
|
1027
1135
|
*/
|
|
1028
1136
|
export type StatsSummary = Omit<StatsResponse, "orgs" | "sources" | "releases" | "products">;
|
|
1029
1137
|
|
|
1138
|
+
// ── Feedback ──
|
|
1139
|
+
export type FeedbackType = z.infer<typeof FeedbackTypeSchema>;
|
|
1140
|
+
export type FeedbackStatus = z.infer<typeof FeedbackStatusSchema>;
|
|
1141
|
+
export type FeedbackItem = z.infer<typeof FeedbackItemSchema>;
|
|
1142
|
+
export type FeedbackListResponse = z.infer<typeof FeedbackListResponseSchema>;
|
|
1143
|
+
export type FeedbackUpdateBody = z.infer<typeof FeedbackUpdateBodySchema>;
|
|
1144
|
+
export type FeedbackDeleteResponse = z.infer<typeof FeedbackDeleteResponseSchema>;
|
|
1145
|
+
|
|
1030
1146
|
// ── Fetch log ──
|
|
1031
1147
|
|
|
1032
1148
|
export interface FetchLogEntry {
|
|
@@ -1229,3 +1345,20 @@ export interface EvaluationResult {
|
|
|
1229
1345
|
provider?: string;
|
|
1230
1346
|
notes?: string;
|
|
1231
1347
|
}
|
|
1348
|
+
|
|
1349
|
+
/**
|
|
1350
|
+
* Identity returned by `GET /v1/tokens/me` — the caller introspecting its own
|
|
1351
|
+
* credential. `kind: "root"` is the static break-glass key (synthetic identity,
|
|
1352
|
+
* no DB row); `kind: "token"` is a DB-backed `relk_` token.
|
|
1353
|
+
*/
|
|
1354
|
+
export interface TokenIdentity {
|
|
1355
|
+
kind: "root" | "token";
|
|
1356
|
+
/** Display label; "root" for the static key, "local-dev" when no secret is bound. */
|
|
1357
|
+
name: string;
|
|
1358
|
+
/** e.g. ["read","write"] or ["*"] for root. */
|
|
1359
|
+
scopes: string[];
|
|
1360
|
+
principalType: "internal" | "agent" | "user";
|
|
1361
|
+
principalId?: string | null;
|
|
1362
|
+
expiresAt?: string | null;
|
|
1363
|
+
lastUsedAt?: string | null;
|
|
1364
|
+
}
|
|
@@ -17,30 +17,81 @@ export const CollectionMemberOrgSchema = z.object({
|
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* Org context attached to a product member so the chip can render the parent
|
|
21
|
+
* org's avatar / handle. Subset of `CollectionMemberOrg` — description is
|
|
22
|
+
* intentionally omitted because the byline comes from the product itself.
|
|
23
|
+
*/
|
|
24
|
+
export const ProductParentOrgSchema = z.object({
|
|
25
|
+
slug: z.string(),
|
|
26
|
+
name: z.string(),
|
|
27
|
+
domain: z.string().nullable(),
|
|
28
|
+
avatarUrl: z.string().nullable(),
|
|
29
|
+
githubHandle: z.string().nullable(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* One product as it appears inside a collection. Carries the parent org block
|
|
34
|
+
* so the chip can render the org's avatar (products don't have their own).
|
|
35
|
+
*/
|
|
36
|
+
export const CollectionMemberProductSchema = z.object({
|
|
37
|
+
slug: z.string(),
|
|
38
|
+
name: z.string(),
|
|
39
|
+
description: z.string().nullable(),
|
|
40
|
+
org: ProductParentOrgSchema,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Mixed-kind member entry. `kind` discriminates: `org` carries the org block at
|
|
45
|
+
* the top level; `product` carries the product fields plus a `org` parent block.
|
|
46
|
+
* Used by `GET /v1/collections/:slug` (`members` field) and the new
|
|
47
|
+
* `previewMembers` on `GET /v1/collections`.
|
|
48
|
+
*/
|
|
49
|
+
export const CollectionMemberSchema = z.discriminatedUnion("kind", [
|
|
50
|
+
CollectionMemberOrgSchema.extend({ kind: z.literal("org") }),
|
|
51
|
+
CollectionMemberProductSchema.extend({ kind: z.literal("product") }),
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Row on `GET /v1/collections`. `previewMembers` (the new mixed-kind preview)
|
|
56
|
+
* is the first few visible members so the list page can render inline avatars
|
|
57
|
+
* without a second round trip. `previewOrgs` is the legacy org-only preview,
|
|
58
|
+
* retained for back-compat — clients should migrate to `previewMembers`.
|
|
23
59
|
*/
|
|
24
60
|
export const CollectionListItemSchema = z.object({
|
|
25
61
|
slug: z.string(),
|
|
26
62
|
name: z.string(),
|
|
27
63
|
description: z.string().nullable(),
|
|
28
64
|
memberCount: z.number().int().min(0),
|
|
29
|
-
|
|
65
|
+
/** Whether the collection is promoted on the homepage. Filterable via `?featured=1`. */
|
|
66
|
+
isFeatured: z.boolean(),
|
|
67
|
+
/** Mixed-kind preview (org + product). Cap matches `previewOrgs`. */
|
|
68
|
+
previewMembers: z.array(CollectionMemberSchema).optional(),
|
|
69
|
+
/** Legacy org-only preview. Populated with the org-kind subset of `previewMembers`. */
|
|
70
|
+
previewOrgs: z.array(CollectionMemberOrgSchema).optional(),
|
|
30
71
|
});
|
|
31
72
|
|
|
32
73
|
export const CollectionListResponseSchema = z.array(CollectionListItemSchema);
|
|
33
74
|
|
|
34
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* Returned by `GET /v1/collections/:slug`.
|
|
77
|
+
*
|
|
78
|
+
* `members` is the mixed-kind canonical list. `orgs` is the legacy org-only
|
|
79
|
+
* field, populated with the org-kind subset of `members` for back-compat —
|
|
80
|
+
* new clients should consume `members`.
|
|
81
|
+
*/
|
|
35
82
|
export const CollectionDetailSchema = z.object({
|
|
36
83
|
slug: z.string(),
|
|
37
84
|
name: z.string(),
|
|
38
85
|
description: z.string().nullable(),
|
|
86
|
+
/** Whether the collection is promoted on the homepage. */
|
|
87
|
+
isFeatured: z.boolean(),
|
|
88
|
+
members: z.array(CollectionMemberSchema),
|
|
89
|
+
/** @deprecated Use `members`. Org-only subset, kept for back-compat. */
|
|
39
90
|
orgs: z.array(CollectionMemberOrgSchema),
|
|
40
91
|
});
|
|
41
92
|
|
|
42
93
|
/**
|
|
43
|
-
* Cross-
|
|
94
|
+
* Cross-member release feed row on `GET /v1/collections/:slug/releases`. Same
|
|
44
95
|
* shape as `OrgReleaseItem` plus the origin `org` block (and optional
|
|
45
96
|
* `product`) so the web's release card can render it identically. `source.type`
|
|
46
97
|
* stays as `z.string()` because the SQL passes the column through unvalidated —
|
|
@@ -79,6 +130,7 @@ export const CollectionRowSchema = z.object({
|
|
|
79
130
|
slug: z.string(),
|
|
80
131
|
name: z.string(),
|
|
81
132
|
description: z.string().nullable(),
|
|
133
|
+
isFeatured: z.boolean(),
|
|
82
134
|
createdAt: z.string(),
|
|
83
135
|
updatedAt: z.string(),
|
|
84
136
|
});
|
|
@@ -99,15 +151,26 @@ export const UpdateCollectionRequestSchema = z.object({
|
|
|
99
151
|
slug: z.string().optional(),
|
|
100
152
|
name: z.string().min(1).max(200).optional(),
|
|
101
153
|
description: z.string().max(2000).nullable().optional(),
|
|
154
|
+
/** Promote / demote the collection on the homepage. */
|
|
155
|
+
isFeatured: z.boolean().optional(),
|
|
102
156
|
});
|
|
103
157
|
|
|
104
158
|
/**
|
|
105
|
-
* One member entry accepted by the member-write endpoints.
|
|
106
|
-
*
|
|
159
|
+
* One member entry accepted by the member-write endpoints.
|
|
160
|
+
*
|
|
161
|
+
* Exactly one of `orgId` / `orgSlug` / `productId` / `productSlug` must be
|
|
162
|
+
* present. When both an id and a slug are supplied for the same kind, the id
|
|
163
|
+
* wins. Mixing kinds (e.g. `orgId` + `productSlug`) is rejected by the handler.
|
|
164
|
+
*
|
|
165
|
+
* Product slug is resolved within an org — supply either `productId`
|
|
166
|
+
* (`prod_…`, globally unique) or pair `productSlug` with `orgSlug`/`orgId`.
|
|
167
|
+
* `productSlug` alone is rejected.
|
|
107
168
|
*/
|
|
108
169
|
export const CollectionMemberInputSchema = z.object({
|
|
109
170
|
orgId: z.string().optional(),
|
|
110
171
|
orgSlug: z.string().optional(),
|
|
172
|
+
productId: z.string().optional(),
|
|
173
|
+
productSlug: z.string().optional(),
|
|
111
174
|
/** Authoring position (default 0). For PUT, omit to use the array index. */
|
|
112
175
|
position: z.number().int().optional(),
|
|
113
176
|
});
|
|
@@ -115,14 +178,25 @@ export const CollectionMemberInputSchema = z.object({
|
|
|
115
178
|
/** Body for `POST /v1/collections/:slug/members`. */
|
|
116
179
|
export const AddCollectionMemberRequestSchema = CollectionMemberInputSchema;
|
|
117
180
|
|
|
118
|
-
/**
|
|
181
|
+
/**
|
|
182
|
+
* Body for `PUT /v1/collections/:slug/members`. Replaces the full membership
|
|
183
|
+
* atomically. The field is still named `orgs` for back-compat — it accepts
|
|
184
|
+
* any `CollectionMemberInput`, org or product.
|
|
185
|
+
*/
|
|
119
186
|
export const ReplaceCollectionMembersRequestSchema = z.object({
|
|
120
187
|
orgs: z.array(CollectionMemberInputSchema),
|
|
121
188
|
});
|
|
122
189
|
|
|
123
|
-
/**
|
|
190
|
+
/**
|
|
191
|
+
* Resolved member returned by `PUT /v1/collections/:slug/members` and `POST
|
|
192
|
+
* /v1/collections/:slug/members`. `kind` discriminates which id field is
|
|
193
|
+
* populated. `orgId` is also set on legacy callers that only used org
|
|
194
|
+
* membership — for clean dispatch, dispatch on `kind`.
|
|
195
|
+
*/
|
|
124
196
|
export const ResolvedCollectionMemberSchema = z.object({
|
|
125
|
-
|
|
197
|
+
kind: z.enum(["org", "product"]),
|
|
198
|
+
orgId: z.string().optional(),
|
|
199
|
+
productId: z.string().optional(),
|
|
126
200
|
position: z.number().int(),
|
|
127
201
|
});
|
|
128
202
|
|
|
@@ -132,9 +206,15 @@ export const ReplaceCollectionMembersResponseSchema = z.object({
|
|
|
132
206
|
members: z.array(ResolvedCollectionMemberSchema),
|
|
133
207
|
});
|
|
134
208
|
|
|
135
|
-
/**
|
|
209
|
+
/**
|
|
210
|
+
* Response from `POST /v1/collections/:slug/members`. `orgId` stays populated
|
|
211
|
+
* for org-kind members so existing CLI callers don't break; `kind` +
|
|
212
|
+
* `productId` are the canonical fields for new code.
|
|
213
|
+
*/
|
|
136
214
|
export const AddCollectionMemberResponseSchema = z.object({
|
|
137
215
|
collectionSlug: z.string(),
|
|
138
|
-
|
|
216
|
+
kind: z.enum(["org", "product"]),
|
|
217
|
+
orgId: z.string().optional(),
|
|
218
|
+
productId: z.string().optional(),
|
|
139
219
|
position: z.number().int(),
|
|
140
220
|
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { FEEDBACK_TYPES, FEEDBACK_STATUSES } from "@buildinternet/releases-core/schema";
|
|
3
|
+
|
|
4
|
+
/** Feedback category — mirrors `FEEDBACK_TYPES` in `@buildinternet/releases-core/schema`. */
|
|
5
|
+
export const FeedbackTypeSchema = z.enum(FEEDBACK_TYPES);
|
|
6
|
+
|
|
7
|
+
/** Triage lifecycle — mirrors `FEEDBACK_STATUSES` in `@buildinternet/releases-core/schema`. */
|
|
8
|
+
export const FeedbackStatusSchema = z.enum(FEEDBACK_STATUSES);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* One submitted feedback row, as returned by `GET /v1/admin/feedback` and by
|
|
12
|
+
* the triage write-path (`PATCH /v1/feedback/:id` echoes the updated row).
|
|
13
|
+
* `archived` soft-removes the row from the default admin read view and is
|
|
14
|
+
* orthogonal to triage `status` — a row can be `closed` and visible, or `new`
|
|
15
|
+
* and archived. `createdAt` is epoch milliseconds.
|
|
16
|
+
*/
|
|
17
|
+
export const FeedbackItemSchema = z.object({
|
|
18
|
+
id: z.string(),
|
|
19
|
+
createdAt: z.number(),
|
|
20
|
+
message: z.string(),
|
|
21
|
+
contact: z.string().nullable(),
|
|
22
|
+
type: FeedbackTypeSchema,
|
|
23
|
+
status: FeedbackStatusSchema,
|
|
24
|
+
archived: z.boolean(),
|
|
25
|
+
cliVersion: z.string().nullable(),
|
|
26
|
+
clientKind: z.string(),
|
|
27
|
+
anonId: z.string().nullable(),
|
|
28
|
+
os: z.string().nullable(),
|
|
29
|
+
arch: z.string().nullable(),
|
|
30
|
+
runtime: z.string().nullable(),
|
|
31
|
+
surface: z.string(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* `GET /v1/admin/feedback` — cursor-paginated, newest-first. Archived rows are
|
|
36
|
+
* hidden unless the request passes `?includeArchived=true`.
|
|
37
|
+
*/
|
|
38
|
+
export const FeedbackListResponseSchema = z.object({
|
|
39
|
+
items: z.array(FeedbackItemSchema),
|
|
40
|
+
nextCursor: z.string().nullable(),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* `PATCH /v1/feedback/:id` body — partial triage update. At least one field
|
|
45
|
+
* must be present; the response echoes the updated `FeedbackItem`.
|
|
46
|
+
*/
|
|
47
|
+
export const FeedbackUpdateBodySchema = z.object({
|
|
48
|
+
status: FeedbackStatusSchema.optional(),
|
|
49
|
+
archived: z.boolean().optional(),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/** `DELETE /v1/feedback/:id` response — hard delete of a single row. */
|
|
53
|
+
export const FeedbackDeleteResponseSchema = z.object({
|
|
54
|
+
deleted: z.literal(true),
|
|
55
|
+
id: z.string(),
|
|
56
|
+
});
|
package/src/schemas/lookups.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { KIND_VALUES } from "@buildinternet/releases-core/kinds";
|
|
2
3
|
import { CategorySchema } from "./shared.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -114,6 +115,7 @@ export const DomainLookupProductSchema = z.object({
|
|
|
114
115
|
// path resolves through `resolveCategoryInput` before persisting — unlike
|
|
115
116
|
// the search catalog category, which passes the column through raw.
|
|
116
117
|
category: CategorySchema.nullable(),
|
|
118
|
+
kind: z.enum(KIND_VALUES).nullable().optional(),
|
|
117
119
|
});
|
|
118
120
|
|
|
119
121
|
export const DomainLookupResponseSchema = z.object({
|
package/src/schemas/orgs.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { KIND_VALUES } from "@buildinternet/releases-core/kinds";
|
|
2
3
|
import {
|
|
4
|
+
AppStoreSourceInfoSchema,
|
|
3
5
|
CategorySchema,
|
|
4
6
|
ListResponseSchema,
|
|
5
7
|
OverviewPageItemSchema,
|
|
@@ -102,6 +104,10 @@ export const UpdateOrgBodySchema = z.object({
|
|
|
102
104
|
aliases: z.array(z.string()).optional(),
|
|
103
105
|
/** Admin-only: pause/unpause all ingest for this org without touching sources (#1057). */
|
|
104
106
|
fetchPaused: z.boolean().optional(),
|
|
107
|
+
/** Admin-only: hide the org from the homepage ticker + /v1/orgs directory. Stays reachable via detail, search, sitemap. */
|
|
108
|
+
isHidden: z.boolean().optional(),
|
|
109
|
+
/** Admin-only: opt the org into automatic AI content — org overviews AND per-release summaries (single backend flag `auto_generate_content`). */
|
|
110
|
+
autoGenerateContent: z.boolean().optional(),
|
|
105
111
|
});
|
|
106
112
|
|
|
107
113
|
// Org detail's products query selects a strict subset of `ProductListItem` —
|
|
@@ -114,6 +120,9 @@ const OrgDetailProductSchema = ProductListItemSchema.pick({
|
|
|
114
120
|
url: true,
|
|
115
121
|
description: true,
|
|
116
122
|
sourceCount: true,
|
|
123
|
+
kind: true,
|
|
124
|
+
}).extend({
|
|
125
|
+
releaseCount: z.number().int().min(0),
|
|
117
126
|
});
|
|
118
127
|
|
|
119
128
|
const OrgDetailPlaybookSchema = z.object({
|
|
@@ -125,7 +134,8 @@ const OrgDetailPlaybookSchema = z.object({
|
|
|
125
134
|
// ── Org catalog (/orgs/:slug/catalog) ──
|
|
126
135
|
|
|
127
136
|
const OrgCatalogProductItemSchema = z.object({
|
|
128
|
-
|
|
137
|
+
entryType: z.literal("product"),
|
|
138
|
+
kind: z.enum(KIND_VALUES).nullable().optional(),
|
|
129
139
|
id: z.string(),
|
|
130
140
|
slug: z.string(),
|
|
131
141
|
name: z.string(),
|
|
@@ -135,7 +145,8 @@ const OrgCatalogProductItemSchema = z.object({
|
|
|
135
145
|
});
|
|
136
146
|
|
|
137
147
|
const OrgCatalogSourceItemSchema = z.object({
|
|
138
|
-
|
|
148
|
+
entryType: z.literal("source"),
|
|
149
|
+
kind: z.enum(KIND_VALUES).nullable().optional(),
|
|
139
150
|
id: z.string(),
|
|
140
151
|
slug: z.string(),
|
|
141
152
|
name: z.string(),
|
|
@@ -146,10 +157,12 @@ const OrgCatalogSourceItemSchema = z.object({
|
|
|
146
157
|
|
|
147
158
|
/**
|
|
148
159
|
* Combined product + source catalog for `GET /v1/orgs/:slug/catalog`.
|
|
149
|
-
* `
|
|
150
|
-
* `
|
|
160
|
+
* `entryType` discriminates between the two row shapes (`"product"` vs
|
|
161
|
+
* `"source"`). `kind` carries the entity taxonomy (platform/sdk/mobile/…).
|
|
162
|
+
* Products carry `category` and `description`; sources carry `type` and
|
|
163
|
+
* `productId`.
|
|
151
164
|
*/
|
|
152
|
-
export const OrgCatalogItemSchema = z.discriminatedUnion("
|
|
165
|
+
export const OrgCatalogItemSchema = z.discriminatedUnion("entryType", [
|
|
153
166
|
OrgCatalogProductItemSchema,
|
|
154
167
|
OrgCatalogSourceItemSchema,
|
|
155
168
|
]);
|
|
@@ -323,7 +336,18 @@ export const OrgSparklinesResponseSchema = z.object({
|
|
|
323
336
|
* with a `source` block identifying the originating source within the org.
|
|
324
337
|
*/
|
|
325
338
|
export const OrgReleaseItemSchema = ReleaseItemSchema.extend({
|
|
326
|
-
source: z.object({
|
|
339
|
+
source: z.object({
|
|
340
|
+
slug: z.string(),
|
|
341
|
+
name: z.string(),
|
|
342
|
+
type: z.string(),
|
|
343
|
+
appStore: AppStoreSourceInfoSchema.optional(),
|
|
344
|
+
}),
|
|
345
|
+
/**
|
|
346
|
+
* Owning product, when the release's source is grouped under a product.
|
|
347
|
+
* `null` when the source has no `product_id`. Additive — older API responses
|
|
348
|
+
* omit this field; treat `undefined` as `null`. #1217.
|
|
349
|
+
*/
|
|
350
|
+
product: z.object({ slug: z.string(), name: z.string() }).nullable().optional(),
|
|
327
351
|
});
|
|
328
352
|
|
|
329
353
|
/** Cursor pagination shape for the org releases feed. */
|
|
@@ -389,6 +413,14 @@ export const OrgDetailSchema = z.object({
|
|
|
389
413
|
description: z.string().nullable().optional(),
|
|
390
414
|
category: CategorySchema.nullable().optional(),
|
|
391
415
|
avatarUrl: z.string().nullable(),
|
|
416
|
+
/** Admin display flag: true when the org is hidden from listings (homepage + /v1/orgs). Optional on the wire for older workers mid-deploy. */
|
|
417
|
+
isHidden: z.boolean().optional(),
|
|
418
|
+
/** Admin display flag: org is opted into automatic overviews + per-release summaries. Optional on the wire for older workers mid-deploy. */
|
|
419
|
+
autoGenerateContent: z.boolean().optional(),
|
|
420
|
+
/** Admin display flag: all ingest paused for this org. */
|
|
421
|
+
fetchPaused: z.boolean().optional(),
|
|
422
|
+
/** How the org row was created. `on_demand` orgs are excluded from overview generation regardless of `autoGenerateContent`. */
|
|
423
|
+
discovery: z.enum(["curated", "agent", "on_demand"]).optional(),
|
|
392
424
|
tags: z.array(z.string()).optional(),
|
|
393
425
|
sourceCount: z.number().int().min(0),
|
|
394
426
|
releaseCount: z.number().int().min(0),
|
package/src/schemas/products.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { KIND_VALUES } from "@buildinternet/releases-core/kinds";
|
|
3
|
+
import { CategorySchema, ListResponseSchema, ReleaseItemSchema } from "./shared.js";
|
|
4
|
+
import { SourceTypeSchema, SourceFeedPaginationSchema } from "./sources.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Raw `products` table row, returned by `POST /v1/products` and
|
|
@@ -19,11 +20,20 @@ export const ProductRowSchema = z.object({
|
|
|
19
20
|
url: z.string().nullable(),
|
|
20
21
|
description: z.string().nullable(),
|
|
21
22
|
category: CategorySchema.nullable(),
|
|
23
|
+
kind: z.enum(KIND_VALUES).nullable().optional(),
|
|
24
|
+
avatarUrl: z.string().nullable().optional(),
|
|
22
25
|
createdAt: z.string(),
|
|
23
26
|
embeddedAt: z.string().nullable(),
|
|
24
27
|
deletedAt: z.string().nullable(),
|
|
25
28
|
});
|
|
26
29
|
|
|
30
|
+
/** 201 body of POST /v1/products — ProductRow plus an optional non-blocking
|
|
31
|
+
* warning emitted when the new slug shadows an existing same-org source (#1190). */
|
|
32
|
+
export const ProductCreateResponseSchema = ProductRowSchema.extend({
|
|
33
|
+
warning: z.string().optional(),
|
|
34
|
+
});
|
|
35
|
+
export type ProductCreateResponse = z.infer<typeof ProductCreateResponseSchema>;
|
|
36
|
+
|
|
27
37
|
/**
|
|
28
38
|
* Per-product row returned by `GET /v1/products`. Adds `sourceCount` to
|
|
29
39
|
* the row shape but omits the internal `embeddedAt` / `deletedAt` columns
|
|
@@ -37,6 +47,7 @@ export const ProductListItemSchema = z.object({
|
|
|
37
47
|
url: z.string().nullable(),
|
|
38
48
|
description: z.string().nullable(),
|
|
39
49
|
category: CategorySchema.nullable(),
|
|
50
|
+
kind: z.enum(KIND_VALUES).nullable().optional(),
|
|
40
51
|
createdAt: z.string(),
|
|
41
52
|
sourceCount: z.number().int().min(0),
|
|
42
53
|
});
|
|
@@ -54,16 +65,27 @@ export const ProductDetailSourceSchema = z.object({
|
|
|
54
65
|
name: z.string(),
|
|
55
66
|
type: SourceTypeSchema,
|
|
56
67
|
url: z.string(),
|
|
68
|
+
metadata: z.string().nullable().optional(),
|
|
69
|
+
kind: z.enum(KIND_VALUES).nullable().optional(),
|
|
57
70
|
});
|
|
58
71
|
|
|
59
72
|
/**
|
|
60
73
|
* Returned by `GET /v1/products/:identifier` (and the org-scoped twin).
|
|
61
74
|
* Spreads the raw product row and adds `sources`, `tags`, `aliases`.
|
|
75
|
+
*
|
|
76
|
+
* `releases` and `pagination` are populated only by the web format adapter
|
|
77
|
+
* (the `.json` / `.md` product route — see PR #1207), NOT by the bare product
|
|
78
|
+
* detail endpoint. They are optional so bare endpoint consumers see a valid
|
|
79
|
+
* `ProductDetail` without the embedded feed.
|
|
62
80
|
*/
|
|
63
81
|
export const ProductDetailSchema = ProductRowSchema.extend({
|
|
64
82
|
sources: z.array(ProductDetailSourceSchema),
|
|
65
83
|
tags: z.array(z.string()),
|
|
66
84
|
aliases: z.array(z.string()),
|
|
85
|
+
/** Embedded release feed — set by the web format adapter (PR #1207), absent on bare product detail. */
|
|
86
|
+
releases: z.array(ReleaseItemSchema).optional(),
|
|
87
|
+
/** Cursor pagination for the embedded release feed — set by the web format adapter (PR #1207), absent on bare product detail. */
|
|
88
|
+
pagination: SourceFeedPaginationSchema.optional(),
|
|
67
89
|
});
|
|
68
90
|
|
|
69
91
|
/**
|
|
@@ -83,6 +105,7 @@ export const CreateProductBodySchema = z.object({
|
|
|
83
105
|
description: z.string().optional(),
|
|
84
106
|
category: z.string().min(1).optional(),
|
|
85
107
|
tags: z.array(z.string()).optional(),
|
|
108
|
+
kind: z.enum(KIND_VALUES).nullable().optional(),
|
|
86
109
|
});
|
|
87
110
|
|
|
88
111
|
/**
|
|
@@ -97,6 +120,8 @@ export const UpdateProductBodySchema = z.object({
|
|
|
97
120
|
category: z.string().min(1).nullable().optional(),
|
|
98
121
|
tags: z.array(z.string()).optional(),
|
|
99
122
|
aliases: z.array(z.string()).optional(),
|
|
123
|
+
kind: z.enum(KIND_VALUES).nullable().optional(),
|
|
124
|
+
avatarUrl: z.string().nullable().optional(),
|
|
100
125
|
});
|
|
101
126
|
|
|
102
127
|
/**
|
|
@@ -172,3 +197,49 @@ export const ProductTagsBodySchema = z.object({
|
|
|
172
197
|
export const ProductTagsMutationResponseSchema = z.object({
|
|
173
198
|
ok: z.literal(true),
|
|
174
199
|
});
|
|
200
|
+
|
|
201
|
+
// ── Product activity (/products/:slug/activity, /orgs/:orgSlug/products/:productSlug/activity) ──
|
|
202
|
+
|
|
203
|
+
const ProductActivityWeeklyBucketSchema = z.object({
|
|
204
|
+
weekStart: z.string(),
|
|
205
|
+
count: z.number().int().min(0),
|
|
206
|
+
earliestVersion: z.string().nullable(),
|
|
207
|
+
latestVersion: z.string().nullable(),
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const ProductActivitySourceSchema = z.object({
|
|
211
|
+
slug: z.string(),
|
|
212
|
+
name: z.string(),
|
|
213
|
+
releaseCount: z.number().int().min(0),
|
|
214
|
+
avgReleasesPerWeek: z.number(),
|
|
215
|
+
earliestVersion: z.string().nullable(),
|
|
216
|
+
latestVersion: z.string().nullable(),
|
|
217
|
+
latestDate: z.string().nullable(),
|
|
218
|
+
weeklyBuckets: z.array(ProductActivityWeeklyBucketSchema),
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Response shape for `GET /v1/products/:slug/activity` and the org-scoped twin.
|
|
223
|
+
* Returns per-source weekly release buckets plus an aggregate rollup — used for
|
|
224
|
+
* timeline visualization on the product detail page.
|
|
225
|
+
*/
|
|
226
|
+
export const ProductActivityResponseSchema = z.object({
|
|
227
|
+
product: z.object({ slug: z.string(), name: z.string() }),
|
|
228
|
+
range: z.object({ from: z.string(), to: z.string() }),
|
|
229
|
+
sources: z.array(ProductActivitySourceSchema),
|
|
230
|
+
aggregateWeekly: z.array(z.object({ weekStart: z.string(), count: z.number().int().min(0) })),
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// ── Product heatmap (/products/:slug/heatmap, /orgs/:orgSlug/products/:productSlug/heatmap) ──
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Response shape for `GET /v1/products/:slug/heatmap` and the org-scoped twin.
|
|
237
|
+
* Returns daily release counts for the trailing 365 days — used for
|
|
238
|
+
* contribution-graph visualization on the product detail page.
|
|
239
|
+
*/
|
|
240
|
+
export const ProductHeatmapResponseSchema = z.object({
|
|
241
|
+
product: z.object({ slug: z.string(), name: z.string() }),
|
|
242
|
+
range: z.object({ from: z.string(), to: z.string() }),
|
|
243
|
+
dailyCounts: z.array(z.object({ date: z.string(), count: z.number().int().min(0) })),
|
|
244
|
+
total: z.number().int().min(0),
|
|
245
|
+
});
|
package/src/schemas/related.ts
CHANGED
|
@@ -17,8 +17,12 @@ export const RelatedReleaseSourceSchema = z.object({
|
|
|
17
17
|
id: z.string(),
|
|
18
18
|
slug: z.string(),
|
|
19
19
|
name: z.string(),
|
|
20
|
+
/** Parent product name, when the source belongs to one — preferred over the
|
|
21
|
+
* bare source/feed name for display. Null for sources with no product. */
|
|
22
|
+
productName: z.string().nullable(),
|
|
20
23
|
orgSlug: z.string().nullable(),
|
|
21
24
|
orgName: z.string().nullable(),
|
|
25
|
+
orgAvatarUrl: z.string().nullable(),
|
|
22
26
|
});
|
|
23
27
|
|
|
24
28
|
/**
|
package/src/schemas/releases.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { SOURCE_TYPES } from "@buildinternet/releases-core/source-enums";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
AppStoreSourceInfoSchema,
|
|
5
|
+
MediaItemSchema,
|
|
6
|
+
ReleaseCompositionSchema,
|
|
7
|
+
ReleaseTypeSchema,
|
|
8
|
+
} from "./shared.js";
|
|
4
9
|
|
|
5
10
|
// ReleaseCompositionSchema lives in ./shared.js (single source of truth) — both
|
|
6
11
|
// this file and ReleaseItemSchema reference it from there.
|
|
@@ -22,6 +27,11 @@ export const ReleaseLatestSourceSchema = z.object({
|
|
|
22
27
|
orgSlug: z.string().nullable(),
|
|
23
28
|
});
|
|
24
29
|
|
|
30
|
+
export const ReleaseLatestProductSchema = z.object({
|
|
31
|
+
slug: z.string(),
|
|
32
|
+
name: z.string(),
|
|
33
|
+
});
|
|
34
|
+
|
|
25
35
|
export const ReleaseLatestItemSchema = z.object({
|
|
26
36
|
id: z.string(),
|
|
27
37
|
version: z.string().nullable(),
|
|
@@ -34,6 +44,12 @@ export const ReleaseLatestItemSchema = z.object({
|
|
|
34
44
|
url: z.string().nullable(),
|
|
35
45
|
media: z.array(MediaItemSchema),
|
|
36
46
|
source: ReleaseLatestSourceSchema,
|
|
47
|
+
/**
|
|
48
|
+
* Owning product, when the release's source is grouped under a product.
|
|
49
|
+
* `null` when the source has no `product_id`. Additive — older API responses
|
|
50
|
+
* omit this field; treat `undefined` as `null`. #1217.
|
|
51
|
+
*/
|
|
52
|
+
product: ReleaseLatestProductSchema.nullable().optional(),
|
|
37
53
|
coverageCount: z.number().int().min(0).optional(),
|
|
38
54
|
// Cached release-body size hint — see {@link ReleaseItemSchema} for the
|
|
39
55
|
// same fields on the org / collection feeds. #958.
|
|
@@ -160,6 +176,7 @@ export const ReleaseDetailResponseSchema = z.object({
|
|
|
160
176
|
sourceType: z.enum(SOURCE_TYPES).nullable(),
|
|
161
177
|
org: ReleaseDetailOrgSchema.nullable(),
|
|
162
178
|
composition: ReleaseCompositionSchema.nullable(),
|
|
179
|
+
appStore: AppStoreSourceInfoSchema.nullable().optional(),
|
|
163
180
|
});
|
|
164
181
|
|
|
165
182
|
/**
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ProductDetailSchema } from "./products.js";
|
|
3
|
+
import { SourceDetailSchema } from "./sources.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Response of `GET /v1/orgs/:org/resolve/:slug` (#1190). Product-first: when a
|
|
7
|
+
* product and a source share a slug in an org, the product variant is returned.
|
|
8
|
+
* A 404 (ErrorResponse) is returned when neither matches.
|
|
9
|
+
*/
|
|
10
|
+
export const ResolveResponseSchema = z.discriminatedUnion("kind", [
|
|
11
|
+
z.object({ kind: z.literal("product"), product: ProductDetailSchema }),
|
|
12
|
+
z.object({ kind: z.literal("source"), source: SourceDetailSchema }),
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
export type ResolveResponse = z.infer<typeof ResolveResponseSchema>;
|
package/src/schemas/search.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { KIND_VALUES } from "@buildinternet/releases-core/kinds";
|
|
2
3
|
import { MediaItemSchema, ReleaseTypeSchema } from "./shared.js";
|
|
3
4
|
import { SourceTypeSchema } from "./sources.js";
|
|
4
5
|
import { LookupStatusSchema } from "./lookups.js";
|
|
@@ -24,10 +25,10 @@ export const SearchOrgHitSchema = z.object({
|
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Unified catalog entry — either a product row or a standalone source
|
|
27
|
-
* presented as product-shaped. `
|
|
28
|
-
*
|
|
29
|
-
* `type`) because
|
|
30
|
-
* `type: github|scrape|feed|agent` on the wire.
|
|
28
|
+
* presented as product-shaped. `entryType` discriminates between the two
|
|
29
|
+
* row shapes (`"product"` vs `"source"`). `kind` carries the entity
|
|
30
|
+
* taxonomy (platform/sdk/mobile/…). `entryType` (not `type`) because
|
|
31
|
+
* source rows already carry `type: github|scrape|feed|agent` on the wire.
|
|
31
32
|
*/
|
|
32
33
|
export const SearchCatalogHitSchema = z.object({
|
|
33
34
|
slug: z.string(),
|
|
@@ -41,7 +42,8 @@ export const SearchCatalogHitSchema = z.object({
|
|
|
41
42
|
// that haven't been migrated yet. Tighten once #689 (category overlay
|
|
42
43
|
// backfill) lands across the prod data set.
|
|
43
44
|
category: z.string().nullable(),
|
|
44
|
-
|
|
45
|
+
entryType: z.enum(["product", "source"]),
|
|
46
|
+
kind: z.enum(KIND_VALUES).nullable().optional(),
|
|
45
47
|
sourceSlug: z.string().optional(),
|
|
46
48
|
sourceType: SourceTypeSchema.optional(),
|
|
47
49
|
});
|
|
@@ -66,6 +68,9 @@ export const SearchReleaseHitSchema = z.object({
|
|
|
66
68
|
sourceType: z.string().optional(),
|
|
67
69
|
orgSlug: z.string().nullable(),
|
|
68
70
|
orgName: z.string().nullable().optional(),
|
|
71
|
+
/** Owning product slug — present when the source belongs to a product. Lets
|
|
72
|
+
* the web byline link to the product page instead of the source. */
|
|
73
|
+
productSlug: z.string().nullable().optional(),
|
|
69
74
|
version: z.string().nullable(),
|
|
70
75
|
title: z.string(),
|
|
71
76
|
summary: z.string(),
|
|
@@ -111,8 +116,9 @@ export const SearchCollectionHitSchema = z.object({
|
|
|
111
116
|
/**
|
|
112
117
|
* Heading-aware CHANGELOG.md slice returned by hybrid / semantic search.
|
|
113
118
|
* Clients can deep-link to
|
|
114
|
-
*
|
|
115
|
-
* surrounding file content
|
|
119
|
+
* `/<orgSlug>/<sourceSlug>/changelog?offset=<offset>#chunk` to read the
|
|
120
|
+
* surrounding file content (the web `chunkDeepLink` builder; falls back to
|
|
121
|
+
* the `/source/<sourceSlug>` redirect shim when `orgSlug` is null).
|
|
116
122
|
*/
|
|
117
123
|
export const SearchChunkHitSchema = z.object({
|
|
118
124
|
sourceSlug: z.string(),
|
package/src/schemas/shared.ts
CHANGED
|
@@ -11,6 +11,17 @@ export const MediaItemSchema = z.object({
|
|
|
11
11
|
r2Url: z.string().optional(),
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* App Store platform + icon for a `type: "appstore"` source. Threaded onto
|
|
16
|
+
* read surfaces so the UI can render the compact app-update row (app icon +
|
|
17
|
+
* "Available for iOS/macOS") instead of the standard version/notes/thumbnail
|
|
18
|
+
* layout. Sourced from `source.metadata.appStore` server-side. #mobile-appstore-feed-row
|
|
19
|
+
*/
|
|
20
|
+
export const AppStoreSourceInfoSchema = z.object({
|
|
21
|
+
platform: z.enum(["ios", "macos"]),
|
|
22
|
+
iconUrl: z.string().nullable(),
|
|
23
|
+
});
|
|
24
|
+
|
|
14
25
|
export const PaginationSchema = z.object({
|
|
15
26
|
page: z.number().int().min(1),
|
|
16
27
|
pageSize: z.number().int().min(1),
|
package/src/schemas/sitemap.ts
CHANGED
|
@@ -6,6 +6,12 @@ import { z } from "zod";
|
|
|
6
6
|
* fields, but the current handler always populates them (#875).
|
|
7
7
|
*/
|
|
8
8
|
export const SitemapSourceSchema = z.object({
|
|
9
|
+
/**
|
|
10
|
+
* Source id. `.optional()` for backwards compatibility — added #1190 so the
|
|
11
|
+
* web can route shadowed sources to `/sources/:id`; older/cached responses
|
|
12
|
+
* may omit it and the web degrades to the bare URL.
|
|
13
|
+
*/
|
|
14
|
+
id: z.string().optional(),
|
|
9
15
|
orgSlug: z.string(),
|
|
10
16
|
slug: z.string(),
|
|
11
17
|
latestDate: z.string().nullable(),
|
package/src/schemas/sources.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
SOURCE_DISCOVERY,
|
|
5
5
|
SOURCE_FETCH_PRIORITIES,
|
|
6
6
|
} from "@buildinternet/releases-core/source-enums";
|
|
7
|
+
import { KIND_VALUES } from "@buildinternet/releases-core/kinds";
|
|
7
8
|
import { ListResponseSchema, ReleaseItemSchema, ReleaseSummaryItemSchema } from "./shared.js";
|
|
8
9
|
|
|
9
10
|
export const SourceTypeSchema = z.enum(SOURCE_TYPES);
|
|
@@ -48,6 +49,7 @@ export const SourceListItemSchema = z.object({
|
|
|
48
49
|
metadata: z.string().nullable().optional(),
|
|
49
50
|
productName: z.string().nullable().optional(),
|
|
50
51
|
productSlug: z.string().nullable().optional(),
|
|
52
|
+
kind: z.enum(KIND_VALUES).nullable().optional(),
|
|
51
53
|
});
|
|
52
54
|
|
|
53
55
|
/**
|
|
@@ -81,6 +83,7 @@ export const SourceWithOrgSchema = z.object({
|
|
|
81
83
|
nextFetchAfter: z.string().nullable(),
|
|
82
84
|
medianGapDays: z.number().nullable(),
|
|
83
85
|
lastRetieredAt: z.string().nullable(),
|
|
86
|
+
kind: z.enum(KIND_VALUES).nullable().optional(),
|
|
84
87
|
});
|
|
85
88
|
|
|
86
89
|
export const SourceListResponseSchema = ListResponseSchema(SourceWithOrgSchema);
|
|
@@ -157,6 +160,7 @@ export const SourceDetailSchema = z.object({
|
|
|
157
160
|
releases: z.array(ReleaseItemSchema),
|
|
158
161
|
pagination: SourceFeedPaginationSchema,
|
|
159
162
|
summaries: SourceDetailSummariesSchema,
|
|
163
|
+
kind: z.enum(KIND_VALUES).nullable().optional(),
|
|
160
164
|
});
|
|
161
165
|
|
|
162
166
|
/**
|
|
@@ -187,6 +191,7 @@ export const SourceMutationResponseSchema = z.looseObject({
|
|
|
187
191
|
isHidden: z.boolean().nullable().optional(),
|
|
188
192
|
discovery: SourceDiscoverySchema.optional(),
|
|
189
193
|
createdAt: z.string().optional(),
|
|
194
|
+
kind: z.enum(KIND_VALUES).nullable().optional(),
|
|
190
195
|
});
|
|
191
196
|
|
|
192
197
|
/** Body accepted by `PATCH /v1/sources/:slug`. */
|
|
@@ -208,6 +213,7 @@ export const SourcePatchInputSchema = z.object({
|
|
|
208
213
|
isHidden: z.boolean().optional(),
|
|
209
214
|
changeDetectedAt: z.string().nullable().optional(),
|
|
210
215
|
lastPolledAt: z.string().nullable().optional(),
|
|
216
|
+
kind: z.enum(KIND_VALUES).nullable().optional(),
|
|
211
217
|
});
|
|
212
218
|
|
|
213
219
|
/** Body accepted by `POST /v1/sources`. */
|
|
@@ -228,6 +234,7 @@ export const CreateSourceBodySchema = z.object({
|
|
|
228
234
|
productSlug: z.string().optional(),
|
|
229
235
|
metadata: z.string().optional(),
|
|
230
236
|
isPrimary: z.boolean().optional(),
|
|
237
|
+
kind: z.enum(KIND_VALUES).nullable().optional(),
|
|
231
238
|
});
|
|
232
239
|
|
|
233
240
|
export const ChangelogFileSummarySchema = z.object({
|
|
@@ -452,12 +459,25 @@ export const SourceMetadataResponseSchema = z.object({
|
|
|
452
459
|
|
|
453
460
|
// ── Changelog probe (POST /sources/:slug/changelog/probe) ──
|
|
454
461
|
|
|
462
|
+
/**
|
|
463
|
+
* A discovered CHANGELOG path returned by the probe. Mirrors
|
|
464
|
+
* `DiscoveredChangelogPath` from `@releases/adapters/github-discovery`:
|
|
465
|
+
* `origin` tags where the path came from (repo root, a workspace package, or
|
|
466
|
+
* an explicit `metadata.changelogPaths` override) and `exists` is true when a
|
|
467
|
+
* directory listing confirmed the file on HEAD.
|
|
468
|
+
*/
|
|
469
|
+
export const DiscoveredChangelogPathSchema = z.object({
|
|
470
|
+
path: z.string(),
|
|
471
|
+
origin: z.enum(["root", "workspace", "override"]),
|
|
472
|
+
exists: z.boolean(),
|
|
473
|
+
});
|
|
474
|
+
|
|
455
475
|
/** Response for `POST /v1/sources/:slug/changelog/probe`. */
|
|
456
476
|
export const ChangelogProbeResponseSchema = z.object({
|
|
457
477
|
sourceId: z.string(),
|
|
458
478
|
sourceSlug: z.string(),
|
|
459
479
|
url: z.string(),
|
|
460
|
-
paths: z.array(
|
|
480
|
+
paths: z.array(DiscoveredChangelogPathSchema),
|
|
461
481
|
});
|
|
462
482
|
|
|
463
483
|
// ── Delete source (DELETE /sources/:slug) ──
|