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

supabase / storage / 30345663115

28 Jul 2026 09:13AM UTC coverage: 80.391% (+0.08%) from 80.307%
30345663115

Pull #1270

github

web-flow
Merge 283c2fdcf into c57fbf24e
Pull Request #1270: fix(storage): document 200 responses for vector bucket CRUD endpoints

5660 of 7582 branches covered (74.65%)

Branch coverage included in aggregate %.

75 of 77 new or added lines in 4 files covered. (97.4%)

1 existing line in 1 file now uncovered.

10804 of 12898 relevant lines covered (83.76%)

413.49 hits per line

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

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

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

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

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

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

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

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

58
  return { schema: { ...schema, params: renamedParams }, url: renamedUrl }
81✔
59
}
60

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

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

97
/**
98
 * Every route can end up hitting setErrorHandler and getting back a {statusCode, error,
99
 * message, code} body (or, for a subtree with its own formatter, whatever that subtree
100
 * documents instead) - 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
 */
108
function defaultErrorResponse(schema: FastifySchema | undefined): FastifySchema {
109
  const response = schema?.response as Record<string, unknown> | undefined
81✔
110
  if (schema && response && Object.keys(response).some((status) => /^4xx$/i.test(status))) {
117✔
111
    return schema
43✔
112
  }
113

114
  return {
38✔
115
    ...schema,
116
    response: {
117
      ...(response ? undefined : { 200: { description: 'Default Response' } }),
38✔
118
      '4xx': { description: 'Error response', $ref: 'errorSchema#' },
119
      ...response,
120
    },
121
  }
122
}
123

124
const MULTIPART_UPLOAD_OPERATIONS = new Set<string>([
21✔
125
  ROUTE_OPERATIONS.CREATE_OBJECT,
126
  ROUTE_OPERATIONS.UPDATE_OBJECT,
127
  ROUTE_OPERATIONS.UPLOAD_SIGN_OBJECT,
128
])
129

130
/**
131
 * createObject/updateObject/uploadSignedObject never set `schema.body` - they read the raw
132
 * multipart stream directly via `uploadFromRequest` -> `fileUploadFromRequest`
133
 * (see src/storage/object.ts), without registering `@fastify/multipart`'s
134
 * `attachFieldsToBody`. That means `request.body` is always `undefined` on these routes, so a
135
 * real `schema.body` would make Fastify validate that `undefined` (substituted as `null`)
136
 * against a required-fields schema on every real upload and fail it with a 400 - a production
137
 * regression, not a docs improvement. Document the multipart shape here instead, transform-only,
138
 * where - like `defaultErrorResponse` above - it can never reach live request validation.
139
 */
140
function documentMultipartUploadBody(schema: FastifySchema, route: RouteOptions): FastifySchema {
141
  const operation = (route.config as { operation?: string } | undefined)?.operation
81✔
142
  if (!operation || !MULTIPART_UPLOAD_OPERATIONS.has(operation)) {
81✔
143
    return schema
75✔
144
  }
145

146
  return {
6✔
147
    ...schema,
148
    consumes: ['multipart/form-data'],
149
    body: {
150
      type: 'object',
151
      properties: {
152
        cacheControl: { type: 'string' },
153
        metadata: { type: 'string' },
154
        file: { type: 'string', contentEncoding: 'binary' },
155
      },
156
      required: ['cacheControl', 'file'],
157
    },
158
  }
159
}
160

161
/**
162
 * OpenAPI requires operationId to be unique across the whole document. `exposeHeadRoutes`
163
 * auto-derives a HEAD operation from every GET route re-using the same `config.operation`,
164
 * so the id needs a per-method suffix to stay unique when that happens.
165
 * Returns a fresh transform bound to its own dedup state, so main/admin specs don't
166
 * leak collisions into each other when generated in the same process (see export-docs.ts).
167
 */
168
export function createOpenApiTransform() {
169
  const seenIds = new Set<string>()
6✔
170

171
  return function transformOpenApiSchema({
6✔
172
    schema,
173
    url,
174
    route,
175
  }: {
176
    schema: FastifySchema
177
    url: string
178
    route: RouteOptions
179
  }): { schema: FastifySchema; url: string } {
180
    if (isS3ProtocolCatchAll(schema, route)) {
97✔
181
      return { schema: { ...schema, hide: true }, url }
16✔
182
    }
183

184
    ;({ schema, url } = renameWildcardParam(schema, url))
81✔
185
    schema = defaultErrorResponse(schema)
81✔
186
    schema = documentMultipartUploadBody(schema, route)
81✔
187

188
    const operation = (route.config as { operation?: string } | undefined)?.operation
81✔
189

190
    if (!operation || (schema as { operationId?: string }).operationId) {
97✔
191
      return { schema, url }
18✔
192
    }
193

194
    const baseId = operationToId(operation)
63✔
195
    let operationId = baseId
63✔
196
    if (seenIds.has(operationId)) {
63✔
197
      const method = Array.isArray(route.method) ? route.method[0] : route.method
19!
198
      operationId = baseId + method[0].toUpperCase() + method.slice(1).toLowerCase()
19✔
199
    }
200
    for (let suffix = 2; seenIds.has(operationId); suffix++) {
63✔
201
      operationId = baseId + suffix
2✔
202
    }
203
    seenIds.add(operationId)
63✔
204

205
    return {
63✔
206
      schema: { ...schema, operationId },
207
      url,
208
    }
209
  }
210
}
211

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

225
  const { openapiObject } = documentObject
1✔
226
  const paths = openapiObject.paths as Record<string, Record<string, unknown>> | undefined
1✔
227
  if (!paths) {
1!
NEW
228
    return openapiObject
×
229
  }
230

231
  for (const url of Object.keys(paths)) {
1✔
232
    if (url === '/' || !url.endsWith('/')) {
31✔
233
      continue
26✔
234
    }
235

236
    const canonicalUrl = url.slice(0, -1)
5✔
237
    const canonicalPathItem = paths[canonicalUrl]
5✔
238
    if (!canonicalPathItem) {
5✔
239
      continue
3✔
240
    }
241

242
    for (const [method, operation] of Object.entries(paths[url])) {
2✔
243
      canonicalPathItem[method] ??= operation
5✔
244
    }
245
    delete paths[url]
2✔
246
  }
247

248
  return openapiObject
1✔
249
}
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