• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / storage / 30647710496

31 Jul 2026 04:35PM UTC coverage: 80.583% (+0.2%) from 80.403%
30647710496

Pull #1270

github

web-flow
Merge 5281385e4 into 4ae100842
Pull Request #1270: fix(storage): document 200 responses for vector bucket CRUD endpoints

5683 of 7596 branches covered (74.82%)

Branch coverage included in aggregate %.

71 of 73 new or added lines in 3 files covered. (97.26%)

95 existing lines in 5 files now uncovered.

10851 of 12922 relevant lines covered (83.97%)

414.17 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.28
/src/http/routes/openapi-transform.ts
1
import { SwaggerTransformObject } from '@fastify/swagger'
2
import { FastifySchema, RouteOptions } from 'fastify'
3

4
/**
5
 * @fastify/swagger names every de-duplicated component schema `def-0`, `def-1`, ... by
6
 * default, even for schemas registered with a meaningful `$id` (bucketSchema, errorSchema).
7
 * Use the `$id` as the component name instead, falling back to the default `def-N` for
8
 * anonymous schemas so unrelated inline schemas don't collide.
9
 */
10
export function nameSchemaByDollarId(
11
  json: { $id?: string },
12
  _baseUri: unknown,
13
  _fragment: unknown,
14
  i: number
15
) {
16
  return json.$id || `def-${i}`
15!
17
}
18

19
const WILDCARD_PARAM = '*'
21✔
20
const WILDCARD_DOC_NAME = 'wildcard'
21✔
21

22
/**
23
 * Fastify's catch-all route segment is `*`, and its request params are keyed by the
24
 * literal `*` character (`request.params['*']`). @fastify/swagger mirrors that straight
25
 * into the OpenAPI doc as a path template `{*}` with a parameter named `*`, which isn't a
26
 * legal parameter/identifier name for any code generator. Rewrite it to a readable name for
27
 * docs only - the raw url string returned here still goes through Fastify's own `:name`
28
 * path-param formatting, and `route.schema` (the live validation schema) is never mutated.
29
 */
30
function renameWildcardParam(
31
  schema: FastifySchema,
32
  url: string
33
): { schema: FastifySchema; url: string } {
34
  if (!url.split('/').includes(WILDCARD_PARAM)) {
177✔
35
    return { schema, url }
99✔
36
  }
37

38
  const renamedUrl = url
78✔
39
    .split('/')
40
    .map((segment) => (segment === WILDCARD_PARAM ? `:${WILDCARD_DOC_NAME}` : segment))
386✔
41
    .join('/')
42

43
  const params = schema.params as
78✔
44
    | { properties?: Record<string, unknown>; required?: string[] }
45
    | undefined
46
  if (!params?.properties?.[WILDCARD_PARAM]) {
78✔
47
    return { schema, url: renamedUrl }
26✔
48
  }
49

50
  const { [WILDCARD_PARAM]: wildcardProperty, ...otherProperties } = params.properties
52✔
51
  const renamedParams = {
52✔
52
    ...params,
53
    properties: { ...otherProperties, [WILDCARD_DOC_NAME]: wildcardProperty },
54
    required: params.required?.map((name) => (name === WILDCARD_PARAM ? WILDCARD_DOC_NAME : name)),
104✔
55
  }
56

57
  return { schema: { ...schema, params: renamedParams }, url: renamedUrl }
177✔
58
}
59

60
/**
61
 * The S3-compatible surface dispatches ~18 real commands (PutObject, ListObjects,
62
 * CreateMultipartUpload, ...) from a handful of generic Fastify routes based on query
63
 * string/header matching done entirely inside the internal `s3/router.ts` Router - see
64
 * `s3/index.ts`. Fastify (and therefore @fastify/swagger) only ever sees the outer
65
 * catch-all route with no request/response schema, since OpenAPI has no way to express
66
 * "N distinct operations, same path and method, picked by a query parameter". Documenting
67
 * that catch-all as-is would give SDK generators a single method with an untyped body and
68
 * an untyped response for a request that's actually the whole S3 API - worse than nothing.
69
 * Hide it instead; the real per-command schemas remain the source of truth in s3/commands/*.
70
 */
71
function isS3ProtocolCatchAll(schema: FastifySchema | undefined, route: RouteOptions): boolean {
72
  const operation = (route.config as { operation?: string } | undefined)?.operation
209✔
73
  return (schema?.tags?.includes('s3') ?? false) && !operation
209✔
74
}
75

76
/**
77
 * Derives a stable, unique operationId from the route's `config.operation`
78
 * (see ROUTE_OPERATIONS in ./operations.ts), e.g. `storage.object.get_public` -> `objectGetPublic`.
79
 * Routes without a `config.operation` (protocol-level catch-alls like /s3 and /upload/resumable)
80
 * are left without an operationId.
81
 */
82
function operationToId(operation: string): string {
83
  const parts = operation.split('.').filter((part) => part !== 'storage')
452✔
84
  return parts
141✔
85
    .map((part) =>
86
      part
323✔
87
        .split('_')
88
        .filter(Boolean)
89
        .map((word, i) => (i === 0 ? word : word[0].toUpperCase() + word.slice(1)))
421✔
90
        .join('')
91
    )
92
    .map((part, i) => (i === 0 ? part : part[0].toUpperCase() + part.slice(1)))
323✔
93
    .join('')
94
}
95

96
const NON_STANDARD_ERROR_SHAPE_PATH_PREFIXES = ['/iceberg', '/upload/resumable']
21✔
97

98
/**
99
 * Every route can end up hitting setErrorHandler and getting back a {statusCode, error,
100
 * message, code} body - default the doc to that shape for any otherwise-undocumented 4xx.
101
 * Doc-only on purpose: several handlers reply with an ad-hoc, partial error body directly
102
 * (`reply.status(400).send({message: '...'})`, bypassing the formatter entirely), and an
103
 * earlier version of this defaulted via a real onRoute hook that made Fastify enforce
104
 * errorSchema's `required` fields during response *serialization* - which threw on exactly
105
 * those ad-hoc replies (fast-json-stringify errors on a missing required property rather
106
 * than dropping it). A transform can't affect request handling, so it can't cause that.
107
 * Skipped entirely for the iceberg and tus (/upload/resumable) subtrees: iceberg's
108
 * `setErrorHandler` formatter (src/http/routes/iceberg/index.ts) returns
109
 * `{ error: { message, type, code } }`, and tus's `onResponseError`
110
 * (src/http/routes/tus/lifecycle.ts) writes a plain-text body straight to the raw
111
 * `http.ServerResponse` via `@tus/server`, bypassing Fastify's reply/serialization
112
 * entirely - neither matches errorSchema's flat `{statusCode, error, message, code}`, so
113
 * defaulting to errorSchema there would document a shape those routes never actually send.
114
 * Detected by path prefix rather than `schema.tags`/`config.operation`, since some iceberg
115
 * routes (src/http/routes/iceberg/bucket.ts) reuse the same tag/operation constants as the
116
 * unrelated storage-bucket routes. Leaves these routes' 4xx responses undocumented for now -
117
 * real documentation needs its own schema, tracked as follow-up work alongside
118
 * error-handler.ts's formatter-doc pairing.
119
 */
120
function defaultErrorResponse(schema: FastifySchema | undefined, url: string): FastifySchema {
121
  if (NON_STANDARD_ERROR_SHAPE_PATH_PREFIXES.some((prefix) => url.startsWith(prefix))) {
353✔
122
    return schema ?? {}
33!
123
  }
124

125
  const response = schema?.response as Record<string, unknown> | undefined
144✔
126
  if (schema && response && Object.keys(response).some((status) => /^4xx$/i.test(status))) {
177✔
127
    return schema
103✔
128
  }
129

130
  return {
41✔
131
    ...schema,
132
    response: {
133
      ...(response ? undefined : { 200: { description: 'Default Response' } }),
41!
134
      '4xx': { description: 'Error response', $ref: 'errorSchema#' },
135
      ...response,
136
    },
137
  }
138
}
139

140
/**
141
 * OpenAPI requires operationId to be unique across the whole document. A route can set
142
 * the standard `schema.operationId` to pin its id explicitly (takes precedence over the
143
 * derived `config.operation` id) - do this for any route whose id must stay stable
144
 * regardless of where it's registered, since generated SDK method names key off of it.
145
 * `exposeHeadRoutes` auto-derives a HEAD operation from every GET route re-using the same
146
 * `config.operation` - give that specific, deterministic case a `Head` suffix. Any other
147
 * collision (two distinct routes resolving to the same id) means the route needs its own
148
 * `schema.operationId` - several pre-existing route families (tus, object) already reuse
149
 * the same ROUTE_OPERATIONS constant across multiple registrations (e.g. POST / and
150
 * POST /*), so this can't hard-fail doc generation for the whole app over a pre-existing
151
 * duplicate it doesn't own. Warn and leave the colliding route without an operationId
152
 * instead - no worse than before this transform existed, and each occurrence is a route
153
 * family that should get its own `schema.operationId` in a follow-up.
154
 * Returns a fresh transform bound to its own dedup state, so main/admin specs don't
155
 * leak collisions into each other when generated in the same process (see export-docs.ts).
156
 */
157
export function createOpenApiTransform() {
158
  const seenIds = new Map<string, string>()
12✔
159

160
  return function transformOpenApiSchema({
12✔
161
    schema,
162
    url,
163
    route,
164
  }: {
165
    schema: FastifySchema
166
    url: string
167
    route: RouteOptions
168
  }): { schema: FastifySchema; url: string } {
169
    if (isS3ProtocolCatchAll(schema, route)) {
209✔
170
      return { schema: { ...schema, hide: true }, url }
32✔
171
    }
172

173
    ;({ schema, url } = renameWildcardParam(schema, url))
177✔
174
    schema = defaultErrorResponse(schema, url)
177✔
175

176
    const baseId = route.config?.operation && operationToId(route.config.operation)
177✔
177

178
    if (!baseId || (schema as { operationId?: string }).operationId) {
209✔
179
      return { schema, url }
37✔
180
    }
181

182
    const methods = Array.isArray(route.method) ? route.method : [route.method]
140!
183
    const isAutoHeadRoute = methods.length === 1 && methods[0] === 'HEAD'
209✔
184
    const operationId = isAutoHeadRoute ? `${baseId}Head` : baseId
209✔
185
    const location = `${methods.join(',')} ${url}`
209✔
186

187
    const firstSeenAt = seenIds.get(operationId)
209✔
188
    if (firstSeenAt) {
209✔
189
      console.warn(
23✔
190
        `[openapi] Duplicate operationId "${operationId}" for ${location} - already used by ` +
191
          `${firstSeenAt}. Leaving it undocumented. Give this route (or its ROUTE_OPERATIONS ` +
192
          `entry) a distinct schema.operationId to fix.`
193
      )
194
      return { schema, url }
23✔
195
    }
196
    seenIds.set(operationId, location)
117✔
197

198
    return {
117✔
199
      schema: { ...schema, operationId },
200
      url,
201
    }
202
  }
203
}
204

205
/**
206
 * A handful of paths are reachable both with and without a trailing slash - either because
207
 * Fastify's own `exposeHeadRoutes` derives a HEAD route from the un-prefixed path (e.g.
208
 * /health vs /health/) or because a route is explicitly registered both ways (the S3
209
 * protocol surface). Both forms genuinely work at runtime, but documenting them as two
210
 * unrelated paths just doubles the number of operations a generated SDK has to deal with
211
 * for the same endpoint. Keep the slash-less form and fold the other one's methods into it.
212
 */
213
export const dedupeTrailingSlashPaths: SwaggerTransformObject = (documentObject) => {
21✔
214
  if (!('openapiObject' in documentObject)) {
2!
NEW
215
    return documentObject.swaggerObject
×
216
  }
217

218
  const { openapiObject } = documentObject
2✔
219
  const paths = openapiObject.paths as Record<string, Record<string, unknown>> | undefined
2✔
220
  if (!paths) {
2!
NEW
221
    return openapiObject
×
222
  }
223

224
  for (const url of Object.keys(paths)) {
2✔
225
    if (url === '/' || !url.endsWith('/')) {
75✔
226
      continue
65✔
227
    }
228

229
    const canonicalUrl = url.slice(0, -1)
10✔
230
    const canonicalPathItem = paths[canonicalUrl]
10✔
231
    if (!canonicalPathItem) {
10✔
232
      continue
6✔
233
    }
234

235
    for (const [method, operation] of Object.entries(paths[url])) {
4✔
236
      canonicalPathItem[method] ??= operation
10✔
237
    }
238
    delete paths[url]
4✔
239
  }
240

241
  return openapiObject
2✔
242
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc