@buildinternet/releases-api-types 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/api-types.ts +516 -516
- package/src/schemas/collections.ts +140 -0
- package/src/schemas/lookups.ts +123 -0
- package/src/schemas/orgs.ts +294 -4
- package/src/schemas/overviews.ts +138 -0
- package/src/schemas/products.ts +32 -4
- package/src/schemas/related.ts +107 -0
- package/src/schemas/releases.ts +249 -0
- package/src/schemas/search.ts +156 -0
- package/src/schemas/sitemap.ts +36 -0
- package/src/schemas/sources.ts +291 -0
- package/src/schemas/stats.ts +62 -0
- package/src/schemas/taxonomy.ts +110 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { OverviewPageItemSchema, OverviewCitationSchema } from "./shared.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Response shape returned by `GET /v1/orgs/:slug/overview` — the org-scoped
|
|
6
|
+
* knowledge page with inline citations, or `null` when no overview has been
|
|
7
|
+
* generated yet.
|
|
8
|
+
*/
|
|
9
|
+
export const OrgOverviewResponseSchema = OverviewPageItemSchema.extend({
|
|
10
|
+
citations: z.array(OverviewCitationSchema).optional(),
|
|
11
|
+
}).nullable();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Incoming citation row attached to `POST /v1/orgs/:slug/overview`. Slimmer
|
|
15
|
+
* than the read-side `OverviewCitationSchema` because the wire shape doesn't
|
|
16
|
+
* carry `releaseId` — that's resolved server-side from `sourceUrl`. Spans
|
|
17
|
+
* must be in-range (`endIndex > startIndex`); the cross-field
|
|
18
|
+
* `endIndex <= content.length` check stays in the handler.
|
|
19
|
+
*/
|
|
20
|
+
export const IncomingOverviewCitationSchema = z
|
|
21
|
+
.object({
|
|
22
|
+
startIndex: z.number().int().min(0),
|
|
23
|
+
endIndex: z.number().int().min(1),
|
|
24
|
+
sourceUrl: z.string().min(1),
|
|
25
|
+
title: z.string().nullable().optional(),
|
|
26
|
+
citedText: z.string().min(1),
|
|
27
|
+
})
|
|
28
|
+
.refine((c) => c.endIndex > c.startIndex, {
|
|
29
|
+
message: "endIndex must be > startIndex",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Body accepted by `POST /v1/orgs/:slug/overview` — the agent-authored
|
|
34
|
+
* markdown content, the release count it was derived from, the timestamp of
|
|
35
|
+
* the most-recent contributing release, and optional inline citations.
|
|
36
|
+
*
|
|
37
|
+
* `content` is non-empty. `releaseCount` is a finite non-negative integer.
|
|
38
|
+
* `citations` is an array of `IncomingOverviewCitation`; the handler still
|
|
39
|
+
* cross-checks `endIndex <= content.length` since that bound is content-aware.
|
|
40
|
+
*/
|
|
41
|
+
export const RegenerateOverviewBodySchema = z.object({
|
|
42
|
+
content: z.string().min(1),
|
|
43
|
+
releaseCount: z.number().int().min(0),
|
|
44
|
+
lastContributingReleaseAt: z.string().nullable().optional(),
|
|
45
|
+
citations: z.array(IncomingOverviewCitationSchema).optional(),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/** Response returned by `POST /v1/orgs/:slug/overview` on success. */
|
|
49
|
+
export const RegenerateOverviewResponseSchema = z.object({
|
|
50
|
+
ok: z.literal(true),
|
|
51
|
+
citations: z.number().int().min(0),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Response shape returned by `GET /v1/products/:slug/overview` — the
|
|
56
|
+
* product-scoped knowledge page, or `null` when no overview exists.
|
|
57
|
+
* Does not include inline citations (products overview does not store them).
|
|
58
|
+
*/
|
|
59
|
+
export const ProductOverviewResponseSchema = OverviewPageItemSchema.nullable();
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Pre-flight payload returned by `GET /v1/orgs/:slug/overview/inputs?check=true`.
|
|
63
|
+
* Lets an orchestrator decide whether to dispatch a per-org sub-agent without
|
|
64
|
+
* paying for the full release-content hydration.
|
|
65
|
+
*/
|
|
66
|
+
export const OverviewInputsCheckResponseSchema = z.object({
|
|
67
|
+
orgSlug: z.string(),
|
|
68
|
+
selected: z.number().int().min(0),
|
|
69
|
+
totalAvailable: z.number().int().min(0),
|
|
70
|
+
hasExistingContent: z.boolean(),
|
|
71
|
+
wouldRegenerate: z.boolean(),
|
|
72
|
+
windowDays: z.number().int().min(1),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Full inputs payload returned by `GET /v1/orgs/:slug/overview/inputs` (without
|
|
77
|
+
* `?check=true`). Contains org metadata, active sources, the existing overview
|
|
78
|
+
* content if any, and the selected hydrated releases ready for the AI model.
|
|
79
|
+
*
|
|
80
|
+
* Typed loosely because the release shape includes hydrated media URLs that
|
|
81
|
+
* differ slightly from `ReleaseItem` — this avoids tight coupling to internal
|
|
82
|
+
* fields while still surfacing the discriminating top-level structure.
|
|
83
|
+
*/
|
|
84
|
+
export const OverviewInputsFullResponseSchema = z.object({
|
|
85
|
+
org: z.object({
|
|
86
|
+
id: z.string(),
|
|
87
|
+
slug: z.string(),
|
|
88
|
+
name: z.string(),
|
|
89
|
+
description: z.string().nullable(),
|
|
90
|
+
discovery: z.string(),
|
|
91
|
+
}),
|
|
92
|
+
sources: z.array(
|
|
93
|
+
z.object({
|
|
94
|
+
id: z.string(),
|
|
95
|
+
slug: z.string(),
|
|
96
|
+
name: z.string(),
|
|
97
|
+
type: z.string(),
|
|
98
|
+
}),
|
|
99
|
+
),
|
|
100
|
+
existingContent: z.string().nullable(),
|
|
101
|
+
selected: z.array(z.unknown()),
|
|
102
|
+
totalAvailable: z.number().int().min(0),
|
|
103
|
+
windowDays: z.number().int().min(1),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Union of the two shapes `GET /v1/orgs/:slug/overview/inputs` can return.
|
|
108
|
+
* The pre-flight (`?check=true`) variant skips content hydration; the full
|
|
109
|
+
* variant carries org metadata + sources + selected releases. Single response
|
|
110
|
+
* entry — Zod union so OpenAPI emits one schema with the variants inlined.
|
|
111
|
+
*/
|
|
112
|
+
export const OverviewInputsResponseSchema = z.union([
|
|
113
|
+
OverviewInputsCheckResponseSchema,
|
|
114
|
+
OverviewInputsFullResponseSchema,
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Response returned by `GET /v1/orgs/:slug/playbook` — the playbook knowledge
|
|
119
|
+
* page (scope `"playbook"`), or `null` when no playbook exists for the org.
|
|
120
|
+
*
|
|
121
|
+
* The playbook `content` field holds a deterministic markdown header generated
|
|
122
|
+
* by `generatePlaybookHeader()` from `@releases/ai-internal/playbook`.
|
|
123
|
+
*/
|
|
124
|
+
export const PlaybookResponseSchema = OverviewPageItemSchema.extend({
|
|
125
|
+
scope: z.literal("playbook"),
|
|
126
|
+
notes: z.string().nullable().optional(),
|
|
127
|
+
}).nullable();
|
|
128
|
+
|
|
129
|
+
/** Body accepted by `PATCH /v1/orgs/:slug/playbook/notes`. */
|
|
130
|
+
export const UpdatePlaybookNotesBodySchema = z.object({
|
|
131
|
+
notes: z.string(),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
/** Response returned by `PATCH /v1/orgs/:slug/playbook/notes` on success. */
|
|
135
|
+
export const UpdatePlaybookNotesResponseSchema = z.object({
|
|
136
|
+
ok: z.literal(true),
|
|
137
|
+
notes: z.string().nullable(),
|
|
138
|
+
});
|
package/src/schemas/products.ts
CHANGED
|
@@ -66,7 +66,14 @@ export const ProductDetailSchema = ProductRowSchema.extend({
|
|
|
66
66
|
aliases: z.array(z.string()),
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Body accepted by `POST /v1/products`. `category` is a non-empty string at
|
|
71
|
+
* the wire boundary so callers can pass either a canonical slug or one of
|
|
72
|
+
* its configured aliases (resolved server-side via `resolveCategoryInput`).
|
|
73
|
+
* Responses always carry the canonical slug. Empty strings are rejected so
|
|
74
|
+
* the truthy-guard around `resolveCategoryInput` in the handler can't be
|
|
75
|
+
* bypassed into persisting an invalid `""` value.
|
|
76
|
+
*/
|
|
70
77
|
export const CreateProductBodySchema = z.object({
|
|
71
78
|
name: z.string().min(1),
|
|
72
79
|
orgId: z.string().optional(),
|
|
@@ -74,16 +81,20 @@ export const CreateProductBodySchema = z.object({
|
|
|
74
81
|
slug: z.string().optional(),
|
|
75
82
|
url: z.string().optional(),
|
|
76
83
|
description: z.string().optional(),
|
|
77
|
-
category:
|
|
84
|
+
category: z.string().min(1).optional(),
|
|
78
85
|
tags: z.array(z.string()).optional(),
|
|
79
86
|
});
|
|
80
87
|
|
|
81
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* Body accepted by `PATCH /v1/products/:slug`. Same alias-allowed string rule
|
|
90
|
+
* as create, plus `null` to explicitly clear the overlay. Empty strings stay
|
|
91
|
+
* rejected so callers must pick between a slug/alias or `null`.
|
|
92
|
+
*/
|
|
82
93
|
export const UpdateProductBodySchema = z.object({
|
|
83
94
|
name: z.string().optional(),
|
|
84
95
|
url: z.string().nullable().optional(),
|
|
85
96
|
description: z.string().nullable().optional(),
|
|
86
|
-
category:
|
|
97
|
+
category: z.string().min(1).nullable().optional(),
|
|
87
98
|
tags: z.array(z.string()).optional(),
|
|
88
99
|
aliases: z.array(z.string()).optional(),
|
|
89
100
|
});
|
|
@@ -144,3 +155,20 @@ export const ProductDeleteResponseSchema = z.object({
|
|
|
144
155
|
hard: z.literal(true).optional(),
|
|
145
156
|
deletedAt: z.string().optional(),
|
|
146
157
|
});
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Tag list returned by `GET /v1/products/:identifier/tags`. Tag names are
|
|
161
|
+
* sorted alphabetically; the empty array is returned when the product has
|
|
162
|
+
* no tags rather than 404.
|
|
163
|
+
*/
|
|
164
|
+
export const ProductTagsListResponseSchema = z.array(z.string());
|
|
165
|
+
|
|
166
|
+
/** Body accepted by `PUT` and `DELETE` `/v1/products/:identifier/tags`. */
|
|
167
|
+
export const ProductTagsBodySchema = z.object({
|
|
168
|
+
tags: z.array(z.string()),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
/** Response shape returned by `PUT` and `DELETE` `/v1/products/:identifier/tags`. */
|
|
172
|
+
export const ProductTagsMutationResponseSchema = z.object({
|
|
173
|
+
ok: z.literal(true),
|
|
174
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Anchor scope for related-* lookups. `org` filters the Vectorize query by
|
|
5
|
+
* the anchor's org id; `global` (default) runs unscoped.
|
|
6
|
+
*/
|
|
7
|
+
export const RelatedScopeSchema = z.enum(["org", "global"]);
|
|
8
|
+
|
|
9
|
+
/** Thumbnail picked from the release's first image-like media entry. */
|
|
10
|
+
export const RelatedReleaseThumbnailSchema = z.object({
|
|
11
|
+
url: z.string(),
|
|
12
|
+
alt: z.string().optional(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/** Owning source rollup attached to each related-release row. */
|
|
16
|
+
export const RelatedReleaseSourceSchema = z.object({
|
|
17
|
+
id: z.string(),
|
|
18
|
+
slug: z.string(),
|
|
19
|
+
name: z.string(),
|
|
20
|
+
orgSlug: z.string().nullable(),
|
|
21
|
+
orgName: z.string().nullable(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* One neighbor returned by `GET /v1/related/releases`. `score` is the
|
|
26
|
+
* Vectorize cosine similarity from the underlying query.
|
|
27
|
+
*/
|
|
28
|
+
export const RelatedReleaseItemSchema = z.object({
|
|
29
|
+
id: z.string(),
|
|
30
|
+
title: z.string(),
|
|
31
|
+
version: z.string().nullable(),
|
|
32
|
+
url: z.string().nullable(),
|
|
33
|
+
publishedAt: z.string().nullable(),
|
|
34
|
+
summary: z.string(),
|
|
35
|
+
titleGenerated: z.string().nullable(),
|
|
36
|
+
titleShort: z.string().nullable(),
|
|
37
|
+
thumbnail: RelatedReleaseThumbnailSchema.nullable(),
|
|
38
|
+
score: z.number(),
|
|
39
|
+
source: RelatedReleaseSourceSchema,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* One neighbor returned by `GET /v1/related/sources`. Carries enough
|
|
44
|
+
* source + org + recent-release stats for the rail card to render without
|
|
45
|
+
* a follow-up request.
|
|
46
|
+
*/
|
|
47
|
+
export const RelatedSourceItemSchema = z.object({
|
|
48
|
+
id: z.string(),
|
|
49
|
+
slug: z.string(),
|
|
50
|
+
name: z.string(),
|
|
51
|
+
type: z.string(),
|
|
52
|
+
url: z.string().nullable(),
|
|
53
|
+
score: z.number(),
|
|
54
|
+
orgSlug: z.string().nullable(),
|
|
55
|
+
orgName: z.string().nullable(),
|
|
56
|
+
orgAvatarUrl: z.string().nullable(),
|
|
57
|
+
releaseCount: z.number().int().min(0),
|
|
58
|
+
latestDate: z.string().nullable(),
|
|
59
|
+
latestTitle: z.string().nullable(),
|
|
60
|
+
latestVersion: z.string().nullable(),
|
|
61
|
+
/** Total releases published in the last 30 days (includes the latest). */
|
|
62
|
+
recentCount: z.number().int().min(0),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Successful response from `GET /v1/related/releases`. The handler returns
|
|
67
|
+
* `{scope, items}` when Vectorize is reachable and the anchor has a vector.
|
|
68
|
+
*/
|
|
69
|
+
export const RelatedReleasesOkResponseSchema = z.object({
|
|
70
|
+
scope: RelatedScopeSchema,
|
|
71
|
+
items: z.array(RelatedReleaseItemSchema),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Degraded response from `GET /v1/related/releases`. Returned (HTTP 200) when
|
|
76
|
+
* Vectorize bindings are missing, the anchor hasn't been embedded yet, or a
|
|
77
|
+
* downstream call errored. Callers render nothing in this case rather than
|
|
78
|
+
* surfacing a hard failure. `degradedReason` is human-readable, not stable.
|
|
79
|
+
*/
|
|
80
|
+
export const RelatedReleasesDegradedResponseSchema = z.object({
|
|
81
|
+
degraded: z.literal(true),
|
|
82
|
+
degradedReason: z.string(),
|
|
83
|
+
items: z.array(RelatedReleaseItemSchema),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
export const RelatedReleasesResponseSchema = z.union([
|
|
87
|
+
RelatedReleasesOkResponseSchema,
|
|
88
|
+
RelatedReleasesDegradedResponseSchema,
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
/** Successful response from `GET /v1/related/sources`. */
|
|
92
|
+
export const RelatedSourcesOkResponseSchema = z.object({
|
|
93
|
+
scope: RelatedScopeSchema,
|
|
94
|
+
items: z.array(RelatedSourceItemSchema),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
/** Degraded response from `GET /v1/related/sources`. Same semantics as the releases variant. */
|
|
98
|
+
export const RelatedSourcesDegradedResponseSchema = z.object({
|
|
99
|
+
degraded: z.literal(true),
|
|
100
|
+
degradedReason: z.string(),
|
|
101
|
+
items: z.array(RelatedSourceItemSchema),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
export const RelatedSourcesResponseSchema = z.union([
|
|
105
|
+
RelatedSourcesOkResponseSchema,
|
|
106
|
+
RelatedSourcesDegradedResponseSchema,
|
|
107
|
+
]);
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { SOURCE_TYPES } from "@buildinternet/releases-core/source-enums";
|
|
3
|
+
import { MediaItemSchema, ReleaseTypeSchema } from "./shared.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Per-row shape returned by `GET /v1/releases/latest`. Distinct from the
|
|
7
|
+
* shared `ReleaseItemSchema` because the latest-feed handler joins the
|
|
8
|
+
* source row in and exposes it as a nested `source` object instead of
|
|
9
|
+
* leaving the caller to do a second lookup.
|
|
10
|
+
*
|
|
11
|
+
* `type` is required here — the latest handler reads it directly from
|
|
12
|
+
* `releases.type` (NOT NULL with DB-default `'feature'`), so mid-deploy
|
|
13
|
+
* degrade-gracefully nullability doesn't apply on this path.
|
|
14
|
+
*/
|
|
15
|
+
export const ReleaseLatestSourceSchema = z.object({
|
|
16
|
+
slug: z.string(),
|
|
17
|
+
name: z.string(),
|
|
18
|
+
type: z.enum(SOURCE_TYPES),
|
|
19
|
+
orgSlug: z.string().nullable(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const ReleaseLatestItemSchema = z.object({
|
|
23
|
+
id: z.string(),
|
|
24
|
+
version: z.string().nullable(),
|
|
25
|
+
type: ReleaseTypeSchema,
|
|
26
|
+
title: z.string(),
|
|
27
|
+
summary: z.string().nullable(),
|
|
28
|
+
titleGenerated: z.string().nullable(),
|
|
29
|
+
titleShort: z.string().nullable(),
|
|
30
|
+
publishedAt: z.string().nullable(),
|
|
31
|
+
url: z.string().nullable(),
|
|
32
|
+
media: z.array(MediaItemSchema),
|
|
33
|
+
source: ReleaseLatestSourceSchema,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const ReleaseLatestResponseSchema = z.object({
|
|
37
|
+
releases: z.array(ReleaseLatestItemSchema),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Single row of the `release_coverage` join table. Drives the
|
|
42
|
+
* GET /releases/:id/coverage response.
|
|
43
|
+
*/
|
|
44
|
+
export const ReleaseCoverageRowSchema = z.object({
|
|
45
|
+
coverageId: z.string(),
|
|
46
|
+
canonicalId: z.string(),
|
|
47
|
+
reason: z.string().nullable(),
|
|
48
|
+
decidedBy: z.string(),
|
|
49
|
+
decidedAt: z.string(),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Discriminated by `role`:
|
|
54
|
+
* - `standalone` — release is neither canonical nor coverage of anything.
|
|
55
|
+
* - `coverage` — release is coverage rolling up to `canonical`.
|
|
56
|
+
* - `canonical` — release is the canonical row; `covers` enumerates rollups.
|
|
57
|
+
*/
|
|
58
|
+
export const ReleaseCoverageResponseSchema = z.discriminatedUnion("role", [
|
|
59
|
+
z.object({
|
|
60
|
+
role: z.literal("standalone"),
|
|
61
|
+
canonical: z.null(),
|
|
62
|
+
covers: z.array(z.never()).length(0),
|
|
63
|
+
}),
|
|
64
|
+
z.object({
|
|
65
|
+
role: z.literal("coverage"),
|
|
66
|
+
canonical: ReleaseCoverageRowSchema,
|
|
67
|
+
covers: z.array(z.never()).length(0),
|
|
68
|
+
}),
|
|
69
|
+
z.object({
|
|
70
|
+
role: z.literal("canonical"),
|
|
71
|
+
canonical: z.null(),
|
|
72
|
+
covers: z.array(ReleaseCoverageRowSchema),
|
|
73
|
+
}),
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Body accepted by `POST /v1/releases/:id/coverage`. `decidedBy` MUST be
|
|
78
|
+
* prefixed with `human:` or `agent:` so the audit trail records who linked
|
|
79
|
+
* the rows; the handler enforces this with a regex.
|
|
80
|
+
*/
|
|
81
|
+
export const LinkReleaseCoverageBodySchema = z.object({
|
|
82
|
+
coverageIds: z.array(z.string()).min(1),
|
|
83
|
+
reason: z.string().nullable().optional(),
|
|
84
|
+
decidedBy: z.string().regex(/^(human:|agent:)/),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
export const LinkReleaseCoverageResponseSchema = z.object({
|
|
88
|
+
canonicalId: z.string(),
|
|
89
|
+
coverageIds: z.array(z.string()),
|
|
90
|
+
linked: z.number().int().min(0),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Response for `DELETE /v1/releases/:id/coverage`. Idempotent — the handler
|
|
95
|
+
* returns `{ unlinked: false }` when the release isn't in a cluster, so the
|
|
96
|
+
* remote client can skip a brittle error-message sniff.
|
|
97
|
+
*/
|
|
98
|
+
export const UnlinkReleaseCoverageResponseSchema = z.object({
|
|
99
|
+
unlinked: z.boolean(),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Row returned by `GET /v1/releases?hasMedia=true`. Internal/admin escape
|
|
104
|
+
* hatch consumed by the `media backfill` CLI command.
|
|
105
|
+
*/
|
|
106
|
+
export const ReleaseWithMediaRowSchema = z.object({
|
|
107
|
+
id: z.string(),
|
|
108
|
+
sourceId: z.string(),
|
|
109
|
+
media: z.string().nullable(),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
export const ReleasesWithMediaResponseSchema = z.array(ReleaseWithMediaRowSchema);
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Nested org context joined into the `GET /v1/releases/:id` response.
|
|
116
|
+
* Both fields are nullable — releases whose source has no org attached
|
|
117
|
+
* return `null` for this object entirely.
|
|
118
|
+
*/
|
|
119
|
+
export const ReleaseDetailOrgSchema = z.object({
|
|
120
|
+
slug: z.string(),
|
|
121
|
+
name: z.string(),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Full release detail returned by `GET /v1/releases/:id`. Joins source
|
|
126
|
+
* name/slug/type and a nullable org object; media is parsed from the raw
|
|
127
|
+
* JSON stored in D1 and typed URLs are rewritten to the CDN origin. The
|
|
128
|
+
* route only surfaces non-suppressed, non-coverage rows (filtered via the
|
|
129
|
+
* `releases_visible` view).
|
|
130
|
+
*/
|
|
131
|
+
export const ReleaseDetailResponseSchema = z.object({
|
|
132
|
+
id: z.string(),
|
|
133
|
+
sourceId: z.string(),
|
|
134
|
+
version: z.string().nullable(),
|
|
135
|
+
type: ReleaseTypeSchema,
|
|
136
|
+
title: z.string(),
|
|
137
|
+
content: z.string(),
|
|
138
|
+
summary: z.string().nullable(),
|
|
139
|
+
titleGenerated: z.string().nullable(),
|
|
140
|
+
titleShort: z.string().nullable(),
|
|
141
|
+
url: z.string().nullable(),
|
|
142
|
+
contentHash: z.string().nullable(),
|
|
143
|
+
media: z.array(MediaItemSchema),
|
|
144
|
+
publishedAt: z.string().nullable(),
|
|
145
|
+
suppressed: z.boolean().nullable(),
|
|
146
|
+
suppressedReason: z.string().nullable(),
|
|
147
|
+
prerelease: z.boolean().nullable(),
|
|
148
|
+
fetchedAt: z.string().nullable(),
|
|
149
|
+
sourceName: z.string().nullable(),
|
|
150
|
+
sourceSlug: z.string().nullable(),
|
|
151
|
+
sourceType: z.enum(SOURCE_TYPES).nullable(),
|
|
152
|
+
org: ReleaseDetailOrgSchema.nullable(),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Body accepted by `PATCH /v1/releases/:id`. All fields are optional;
|
|
157
|
+
* omitted fields are not updated. The nullable string columns (`version`,
|
|
158
|
+
* `url`, `publishedAt`, `contentHash`) accept a string or `null`. AI-generated
|
|
159
|
+
* fields (`summary`, `titleGenerated`, `titleShort`) accept `null` to
|
|
160
|
+
* explicitly clear the stored value. Non-whitelisted fields are rejected
|
|
161
|
+
* via `.strict()` — the handler also re-runs the field-set check after
|
|
162
|
+
* sanitization so an empty update payload still 400s.
|
|
163
|
+
*/
|
|
164
|
+
export const UpdateReleaseBodySchema = z
|
|
165
|
+
.object({
|
|
166
|
+
title: z.string().optional(),
|
|
167
|
+
content: z.string().optional(),
|
|
168
|
+
version: z.string().nullable().optional(),
|
|
169
|
+
url: z.string().nullable().optional(),
|
|
170
|
+
publishedAt: z.string().nullable().optional(),
|
|
171
|
+
contentHash: z.string().nullable().optional(),
|
|
172
|
+
summary: z.string().nullable().optional(),
|
|
173
|
+
titleGenerated: z.string().nullable().optional(),
|
|
174
|
+
titleShort: z.string().nullable().optional(),
|
|
175
|
+
})
|
|
176
|
+
.strict();
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Response returned by `PATCH /v1/releases/:id`. The handler returns the raw
|
|
180
|
+
* Drizzle row from the `releases` table — distinct from the augmented
|
|
181
|
+
* `ReleaseDetailResponseSchema` shape served by `GET /v1/releases/:id`, which
|
|
182
|
+
* adds joined source/org metadata and parses `media` from raw JSON. Clients
|
|
183
|
+
* that need the augmented shape after an edit should re-fetch via GET.
|
|
184
|
+
*/
|
|
185
|
+
export const ReleasePatchResponseSchema = z.object({
|
|
186
|
+
id: z.string(),
|
|
187
|
+
sourceId: z.string(),
|
|
188
|
+
version: z.string().nullable(),
|
|
189
|
+
type: ReleaseTypeSchema,
|
|
190
|
+
title: z.string(),
|
|
191
|
+
content: z.string(),
|
|
192
|
+
summary: z.string().nullable(),
|
|
193
|
+
titleGenerated: z.string().nullable(),
|
|
194
|
+
titleShort: z.string().nullable(),
|
|
195
|
+
url: z.string().nullable(),
|
|
196
|
+
contentHash: z.string().nullable(),
|
|
197
|
+
metadata: z.string().nullable(),
|
|
198
|
+
// Raw JSON string of the media array — not parsed at this endpoint.
|
|
199
|
+
media: z.string().nullable(),
|
|
200
|
+
publishedAt: z.string().nullable(),
|
|
201
|
+
prerelease: z.boolean().nullable(),
|
|
202
|
+
suppressed: z.boolean().nullable(),
|
|
203
|
+
suppressedReason: z.string().nullable(),
|
|
204
|
+
fetchedAt: z.string().nullable(),
|
|
205
|
+
embeddedAt: z.string().nullable(),
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
/** Response returned by `DELETE /v1/releases/:id`. */
|
|
209
|
+
export const ReleaseDeleteResponseSchema = z.object({
|
|
210
|
+
deleted: z.literal(true),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
/** Response returned by `POST /v1/releases/:id/suppress`. */
|
|
214
|
+
export const ReleaseSuppressResponseSchema = z.object({
|
|
215
|
+
suppressed: z.literal(true),
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
/** Response returned by `POST /v1/releases/:id/unsuppress`. */
|
|
219
|
+
export const ReleaseUnsuppressResponseSchema = z.object({
|
|
220
|
+
unsuppressed: z.literal(true),
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Body accepted by `POST /v1/releases/:id/suppress`. The `reason` field
|
|
225
|
+
* is optional; when omitted the stored `suppressed_reason` is set to null.
|
|
226
|
+
*/
|
|
227
|
+
export const ReleaseSuppressBodySchema = z.object({
|
|
228
|
+
reason: z.string().optional(),
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* WebSocket message schema for the `GET /v1/releases/stream` endpoint.
|
|
233
|
+
* Each message is a JSON object with one of the following shapes:
|
|
234
|
+
* - `{ type: "ready", seq: number }` — sent on connect to give the caller
|
|
235
|
+
* the current head sequence number for later resume.
|
|
236
|
+
* - `{ type: "release.created", seq: number, release: object }` — a new
|
|
237
|
+
* release was indexed; `release` carries the full release row.
|
|
238
|
+
* - `{ type: "snapshot_gap" }` — the requested `since` sequence fell
|
|
239
|
+
* behind the oldest buffered event; the client must REST-backfill.
|
|
240
|
+
*/
|
|
241
|
+
export const ReleaseStreamMessageSchema = z.discriminatedUnion("type", [
|
|
242
|
+
z.object({ type: z.literal("ready"), seq: z.number().int() }),
|
|
243
|
+
z.object({
|
|
244
|
+
type: z.literal("release.created"),
|
|
245
|
+
seq: z.number().int(),
|
|
246
|
+
release: z.record(z.string(), z.unknown()),
|
|
247
|
+
}),
|
|
248
|
+
z.object({ type: z.literal("snapshot_gap") }),
|
|
249
|
+
]);
|