neon 4.9.0 → 4.10.1

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/README.md CHANGED
@@ -871,7 +871,7 @@ All sub-commands honor the [global options](#global-options), including `--outpu
871
871
 
872
872
  ## Database diagnostics (`inspect`)
873
873
 
874
- `neon inspect db stalled-queries` takes a read-only snapshot of active queries that have run for more than 30 seconds and groups parallel workers with their leader. Table output shows duration, wait event, blocking pids, role, query group, and query. `--output json` adds timestamps, query IDs, pids, database, and the rest of the row. A blocking pid can belong to an idle-in-transaction backend this command does not list; `neon inspect db locks` shows lock holders.
874
+ `neon inspect db stalled-queries` takes a read-only snapshot of active queries that have run for more than 30 seconds and groups parallel workers with their leader. Oldest group first. Table output shows duration, wait event, blocking pids, role, query group, and query. `--output json` adds timestamps, query IDs, pids, database, and the rest of the row. A blocking pid can belong to an idle-in-transaction backend this command does not list; `neon inspect db locks` shows lock holders.
875
875
 
876
876
  ```bash
877
877
  neon inspect db stalled-queries
@@ -1108,6 +1108,20 @@ When both are only environment variables the key wins, which keeps a CI pipeline
1108
1108
 
1109
1109
  `neon init` forwards `--profile` and `--config-dir` to the commands it runs. An explicit `--api-key` is passed to those children through `NEON_API_KEY`, not argv.
1110
1110
 
1111
+ ## API passthrough (`api`)
1112
+
1113
+ `neon api` sends an authenticated request to any Neon API route. `neon api --list` catalogs the routes. `neon api <path> --describe` prints the OpenAPI request shape for one operation so you can fill `-F` and `-Q` without guessing. Body field names are dotted to match `-F`.
1114
+
1115
+ ```bash
1116
+ neon api --list
1117
+ neon api /projects --describe
1118
+ neon api /projects -X POST --describe -o json
1119
+ neon api /projects/{project_id}/branches -X POST --describe
1120
+ neon api /projects/foo-bar-123/branches -X POST -F branch.name=dev
1121
+ ```
1122
+
1123
+ `--describe` does not send the selected API request. It uses the same CLI authentication as `--list`. `-X` defaults to GET; if that method is missing, the error names the methods that exist.
1124
+
1111
1125
  ## API keys (`api-keys`)
1112
1126
 
1113
1127
  ```bash
@@ -1182,6 +1196,7 @@ Id Name Project Created At Last Used At Last
1182
1196
  | claim (`claimable`) | `create`, `status`, `accept`, `list`, `delete` | Manage claimable projects |
1183
1197
  | profile | `list`, `create`, `rotate-key`, `remove` | Manage named sets of credentials |
1184
1198
  | api-keys | `list`, `create`, `revoke` | Manage API keys |
1199
+ | api | | Call any Neon API route |
1185
1200
  | [projects](https://neon.com/docs/reference/cli-projects) | `list`, `create`, `update`, `delete`, `get` | Manage projects |
1186
1201
  | [ip-allow](https://neon.com/docs/reference/cli-ip-allow) | `list`, `add`, `remove`, `reset` | Manage IP Allow |
1187
1202
  | [me](https://neon.com/docs/reference/cli-me) | | Show current user |
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
- import { getEndpoints, loadSpec } from "../utils/openapi.js";
2
+ import { describeOperation, getEndpoints, loadSpec } from "../utils/openapi.js";
3
3
  import { writer } from "../writer.js";
4
4
  import { readFileSync } from "node:fs";
5
5
  import { resolve } from "node:path";
@@ -141,6 +141,86 @@ async function listEndpoints(args) {
141
141
  emptyMessage: "No endpoints found in the spec."
142
142
  });
143
143
  }
144
+ function isListing(args) {
145
+ return args.list || args.path === "list" || args.path === "ls";
146
+ }
147
+ function describeType(field) {
148
+ const enumText = (values) => Array.isArray(values) && values.length > 0 ? ` (${values.map(String).join(", ")})` : "";
149
+ if (field.type === "array" && field.items) {
150
+ const props = field.items.properties;
151
+ if (props && props.length > 0) return `array (${props.map((prop) => `${prop.name}${enumText(prop.enum)}`).join(", ")})`;
152
+ return `array of ${field.items.type}${enumText(field.items.enum)}`;
153
+ }
154
+ return `${field.type}${enumText(field.enum)}`;
155
+ }
156
+ async function describeRoute(args) {
157
+ const path = args.path;
158
+ if (!path) throw new Error("Missing API path. Usage: neon api <path> --describe (e.g. neon api /projects --describe). Run `neon api --list` to see available routes.");
159
+ if (!path.startsWith("/")) throw new Error(`Invalid path "${path}". API paths must start with "/". Run \`neon api --list\` to see available routes.`);
160
+ if ([
161
+ ...toStrings(args.field),
162
+ ...toStrings(args.rawField),
163
+ ...toStrings(args.query),
164
+ ...toStrings(args.header)
165
+ ].length > 0 || args.data !== void 0 || args.include) throw new Error("--describe prints the field list; it does not send a request. Drop -F, -f, -d, -Q, -H, and -i.");
166
+ const spec = await loadSpec({
167
+ configDir: args.configDir,
168
+ specUrl: args.specUrl,
169
+ refresh: args.refresh
170
+ });
171
+ if (!spec) throw new Error(`Could not load the Neon OpenAPI spec from ${args.specUrl}. Check your network connection or pass --spec-url.`);
172
+ const method = String(args.method ?? "GET").toUpperCase();
173
+ assertMethod(method);
174
+ const description = describeOperation(spec, path, method);
175
+ const endpoint = {
176
+ method: description.method,
177
+ path: description.path,
178
+ summary: description.summary,
179
+ operationId: description.operationId,
180
+ bodyRequired: description.bodyRequired,
181
+ ...description.contentType !== "" && description.contentType !== "application/json" ? { contentType: description.contentType } : {}
182
+ };
183
+ let title = description.summary ? `${description.method} ${description.path} - ${description.summary}` : `${description.method} ${description.path}`;
184
+ if (description.bodyRequired) title = `${title} (body required)`;
185
+ if (description.contentType !== "" && description.contentType !== "application/json") title = `${title} (${description.contentType})`;
186
+ if (args.output === "json" || args.output === "yaml") {
187
+ writer(args).write(endpoint, {
188
+ fields: [
189
+ "method",
190
+ "path",
191
+ "summary",
192
+ "operationId",
193
+ "bodyRequired"
194
+ ],
195
+ title: "Endpoint"
196
+ }).end(description.fields, {
197
+ fields: [
198
+ "in",
199
+ "name",
200
+ "required",
201
+ "type",
202
+ "description"
203
+ ],
204
+ title: "Parameters"
205
+ });
206
+ return;
207
+ }
208
+ writer(args).end(description.fields, {
209
+ fields: [
210
+ "in",
211
+ "name",
212
+ "required",
213
+ "type",
214
+ "description"
215
+ ],
216
+ title,
217
+ emptyMessage: "No path, query, or body fields in the spec.",
218
+ renderColumns: {
219
+ required: (field) => field.required ? "required" : "optional",
220
+ type: (field) => describeType(field)
221
+ }
222
+ });
223
+ }
144
224
  async function runRequest(args) {
145
225
  const path = args.path;
146
226
  if (!path) throw new Error("Missing API path. Usage: neon api <path> (e.g. neon api /projects). Run `neon api --list` to see available routes.");
@@ -224,24 +304,34 @@ const builder = (argv) => argv.usage("$0 api <path> [options]").positional("path
224
304
  default: false,
225
305
  describe: "List available API endpoints from the OpenAPI spec."
226
306
  },
307
+ describe: {
308
+ type: "boolean",
309
+ default: false,
310
+ describe: "Print path, query, and body fields from the OpenAPI spec without calling the API. Body names are dotted for -F."
311
+ },
227
312
  refresh: {
228
313
  type: "boolean",
229
314
  default: false,
230
- describe: "Refresh the cached OpenAPI spec (used with --list)."
315
+ describe: "Refresh the cached OpenAPI spec (used with --list and --describe)."
231
316
  },
232
317
  "spec-url": {
233
318
  type: "string",
234
319
  default: process.env.NEON_API_SPEC_URL ?? "https://neon.com/api_spec/release/v2.json",
235
320
  hidden: true,
236
- describe: "OpenAPI spec URL used by --list."
321
+ describe: "OpenAPI spec URL used by --list and --describe."
237
322
  }
238
- }).example("$0 api /projects", "List your projects").example("$0 api /projects/{id}/branches -X POST -F branch.name=dev", "Create a branch").example("$0 api --list", "List every available API route");
323
+ }).example("$0 api /projects", "List your projects").example("$0 api /projects/{id}/branches -X POST -F branch.name=dev", "Create a branch").example("$0 api --list", "List every available API route").example("$0 api /projects --describe", "Show GET /projects query parameters").example("$0 api /projects -X POST --describe", "Show the create-project body fields");
239
324
  const handler = async (args) => {
240
325
  const apiArgs = args;
241
- if (apiArgs.list || apiArgs.path === "list" || apiArgs.path === "ls") {
326
+ if (isListing(apiArgs) && apiArgs.describe) throw new Error("Pass either --list or --describe, not both.");
327
+ if (isListing(apiArgs)) {
242
328
  await listEndpoints(apiArgs);
243
329
  return;
244
330
  }
331
+ if (apiArgs.describe) {
332
+ await describeRoute(apiArgs);
333
+ return;
334
+ }
245
335
  await runRequest(apiArgs);
246
336
  };
247
337
  //#endregion
@@ -112,7 +112,7 @@ const INSPECT_QUERIES = {
112
112
  `
113
113
  },
114
114
  "stalled-queries": {
115
- describe: "Active queries running longer than 30 seconds with parallel workers, waits, and blockers (compute-wide)",
115
+ describe: "Active queries running longer than 30 seconds with parallel workers, waits, and blockers, oldest group first (compute-wide)",
116
116
  scope: "compute",
117
117
  fields: [
118
118
  "duration",
@@ -132,9 +132,12 @@ const INSPECT_QUERIES = {
132
132
  AND pid <> pg_backend_pid()
133
133
  ),
134
134
  stalled_groups AS (
135
- SELECT DISTINCT COALESCE(leader_pid, pid) AS query_group
135
+ SELECT
136
+ COALESCE(leader_pid, pid) AS query_group,
137
+ min(query_start) AS group_start
136
138
  FROM activity
137
139
  WHERE query_start <= statement_timestamp() - interval '30 seconds'
140
+ GROUP BY COALESCE(leader_pid, pid)
138
141
  )
139
142
  SELECT
140
143
  statement_timestamp() AS observed_at,
@@ -159,7 +162,7 @@ const INSPECT_QUERIES = {
159
162
  FROM activity a
160
163
  JOIN stalled_groups g
161
164
  ON g.query_group = COALESCE(a.leader_pid, a.pid)
162
- ORDER BY g.query_group, a.leader_pid NULLS FIRST, a.pid;
165
+ ORDER BY g.group_start, g.query_group, a.leader_pid NULLS FIRST, a.pid;
163
166
  `
164
167
  },
165
168
  locks: {
@@ -16,6 +16,13 @@ const HTTP_METHODS = /* @__PURE__ */ new Set([
16
16
  "head",
17
17
  "options"
18
18
  ]);
19
+ const DESCRIBE_METHODS = [
20
+ "GET",
21
+ "POST",
22
+ "PUT",
23
+ "PATCH",
24
+ "DELETE"
25
+ ];
19
26
  async function fetchSpec(url) {
20
27
  const controller = new AbortController();
21
28
  const timer = setTimeout(() => {
@@ -98,5 +105,356 @@ function getEndpoints(spec) {
98
105
  endpoints.sort((a, b) => a.path === b.path ? a.method.localeCompare(b.method) : a.path.localeCompare(b.path));
99
106
  return endpoints;
100
107
  }
108
+ function isRecord(value) {
109
+ return typeof value === "object" && value !== null && !Array.isArray(value);
110
+ }
111
+ function asStringArray(value) {
112
+ if (!Array.isArray(value)) return [];
113
+ return value.filter((item) => typeof item === "string");
114
+ }
115
+ function stringDesc(value) {
116
+ if (!isRecord(value) || typeof value.description !== "string") return "";
117
+ return value.description.trim();
118
+ }
119
+ function specRecord(spec) {
120
+ return spec;
121
+ }
122
+ function resolveRef(spec, ref) {
123
+ if (!ref.startsWith("#/")) throw new Error(`Unsupported $ref "${ref}". Only document-local refs are resolved.`);
124
+ let node = specRecord(spec);
125
+ for (const part of ref.slice(2).split("/")) {
126
+ const key = part.replace(/~1/g, "/").replace(/~0/g, "~");
127
+ if (!isRecord(node) || !(key in node)) throw new Error(`Unresolved $ref ${ref}.`);
128
+ node = node[key];
129
+ }
130
+ return node;
131
+ }
132
+ function schemaType(schema) {
133
+ const t = schema.type;
134
+ if (typeof t === "string") return t;
135
+ if (Array.isArray(t)) {
136
+ const first = t.find((item) => typeof item === "string");
137
+ if (first) return first;
138
+ }
139
+ if (schema.items !== void 0) return "array";
140
+ if (isRecord(schema.properties) || schema.additionalProperties !== void 0) return "object";
141
+ if (Array.isArray(schema.enum)) return "string";
142
+ return "object";
143
+ }
144
+ function optionalEnum(schema) {
145
+ return Array.isArray(schema.enum) ? { enum: schema.enum } : {};
146
+ }
147
+ function optionalNullable(schema) {
148
+ return schema.nullable === true ? { nullable: true } : {};
149
+ }
150
+ function mergeSchema(base, overlay) {
151
+ const properties = {
152
+ ...isRecord(base.properties) ? base.properties : {},
153
+ ...isRecord(overlay.properties) ? overlay.properties : {}
154
+ };
155
+ const required = [.../* @__PURE__ */ new Set([...asStringArray(base.required), ...asStringArray(overlay.required)])];
156
+ const description = stringDesc(overlay) || stringDesc(base);
157
+ return {
158
+ ...base,
159
+ ...overlay,
160
+ properties,
161
+ required,
162
+ ...description ? { description } : {}
163
+ };
164
+ }
165
+ function resolveSchema(spec, schema, stack) {
166
+ if (!isRecord(schema)) return {};
167
+ if (typeof schema.$ref === "string") {
168
+ const ref = schema.$ref;
169
+ const { $ref: _ref, ...siblings } = schema;
170
+ if (stack.includes(ref)) return {
171
+ type: "object",
172
+ ...siblings
173
+ };
174
+ return mergeSchema(resolveSchema(spec, resolveRef(spec, ref), [...stack, ref]), siblings);
175
+ }
176
+ if (Array.isArray(schema.allOf)) {
177
+ const { allOf, ...rest } = schema;
178
+ let merged = {
179
+ type: "object",
180
+ properties: {},
181
+ required: []
182
+ };
183
+ for (const part of allOf) merged = mergeSchema(merged, resolveSchema(spec, part, stack));
184
+ return mergeSchema(merged, rest);
185
+ }
186
+ return schema;
187
+ }
188
+ function fieldFromSchema(location, name, required, schema) {
189
+ return {
190
+ in: location,
191
+ name,
192
+ required,
193
+ type: schemaType(schema),
194
+ description: stringDesc(schema),
195
+ ...optionalEnum(schema),
196
+ ...optionalNullable(schema)
197
+ };
198
+ }
199
+ function pushedRefs(schema, stack) {
200
+ if (!isRecord(schema)) return stack;
201
+ let next = stack;
202
+ if (typeof schema.$ref === "string" && !next.includes(schema.$ref)) next = [...next, schema.$ref];
203
+ if (Array.isArray(schema.allOf)) for (const part of schema.allOf) next = pushedRefs(part, next);
204
+ return next;
205
+ }
206
+ function arrayField(spec, name, required, schema, stack) {
207
+ const items = isRecord(schema.items) ? resolveSchema(spec, schema.items, stack) : {};
208
+ const itemType = schemaType(items);
209
+ const properties = isRecord(items.properties) ? items.properties : void 0;
210
+ const requiredItems = new Set(asStringArray(items.required));
211
+ return {
212
+ in: "body",
213
+ name,
214
+ required,
215
+ type: "array",
216
+ description: stringDesc(schema),
217
+ items: {
218
+ type: itemType,
219
+ ...optionalEnum(items),
220
+ ...optionalNullable(items),
221
+ ...properties ? { properties: Object.entries(properties).map(([propName, propSchema]) => {
222
+ const resolved = resolveSchema(spec, propSchema, stack);
223
+ return {
224
+ name: propName,
225
+ type: schemaType(resolved),
226
+ required: requiredItems.has(propName),
227
+ description: stringDesc(resolved),
228
+ ...optionalEnum(resolved),
229
+ ...optionalNullable(resolved)
230
+ };
231
+ }) } : {}
232
+ }
233
+ };
234
+ }
235
+ function unionMembers(schema) {
236
+ if (Array.isArray(schema.oneOf)) return schema.oneOf;
237
+ if (Array.isArray(schema.anyOf)) return schema.anyOf;
238
+ return [];
239
+ }
240
+ function discriminatorField(schema, prefix) {
241
+ if (!isRecord(schema.discriminator)) return null;
242
+ const propertyName = schema.discriminator.propertyName;
243
+ if (typeof propertyName !== "string") return null;
244
+ const mapping = schema.discriminator.mapping;
245
+ const values = isRecord(mapping) ? Object.keys(mapping) : [];
246
+ return {
247
+ in: "body",
248
+ name: prefix ? `${prefix}.${propertyName}` : propertyName,
249
+ required: true,
250
+ type: "string",
251
+ description: "",
252
+ ...values.length > 0 ? { enum: values } : {}
253
+ };
254
+ }
255
+ function flattenUnion(spec, schema, prefix, stack) {
256
+ const members = unionMembers(schema);
257
+ const byName = /* @__PURE__ */ new Map();
258
+ for (const member of members) {
259
+ const seen = /* @__PURE__ */ new Set();
260
+ for (const field of flattenBody(spec, member, prefix, stack)) {
261
+ if (seen.has(field.name)) continue;
262
+ seen.add(field.name);
263
+ const copies = byName.get(field.name) ?? [];
264
+ copies.push(field);
265
+ byName.set(field.name, copies);
266
+ }
267
+ }
268
+ const fields = [];
269
+ for (const copies of byName.values()) fields.push({
270
+ ...copies[0],
271
+ required: copies.length === members.length && copies.every((field) => field.required)
272
+ });
273
+ const discriminator = discriminatorField(schema, prefix);
274
+ if (discriminator && !byName.has(discriminator.name)) fields.unshift(discriminator);
275
+ else if (discriminator) {
276
+ const existing = fields.find((field) => field.name === discriminator.name);
277
+ if (existing) {
278
+ existing.required = true;
279
+ if (!existing.enum && discriminator.enum) existing.enum = discriminator.enum;
280
+ }
281
+ }
282
+ return fields;
283
+ }
284
+ function flattenBody(spec, schema, prefix, stack = []) {
285
+ const nextStack = pushedRefs(schema, stack);
286
+ const resolved = resolveSchema(spec, schema, stack);
287
+ if (unionMembers(resolved).length > 0) return flattenUnion(spec, resolved, prefix, nextStack);
288
+ const properties = isRecord(resolved.properties) ? resolved.properties : void 0;
289
+ if (!properties) {
290
+ if (prefix === "") return [];
291
+ if (schemaType(resolved) === "array") return [arrayField(spec, prefix, false, resolved, nextStack)];
292
+ return [fieldFromSchema("body", prefix, false, resolved)];
293
+ }
294
+ const requiredSet = new Set(asStringArray(resolved.required));
295
+ const fields = [];
296
+ for (const [key, prop] of Object.entries(properties)) {
297
+ const name = prefix ? `${prefix}.${key}` : key;
298
+ const required = requiredSet.has(key);
299
+ const resolvedProp = resolveSchema(spec, prop, nextStack);
300
+ const type = schemaType(resolvedProp);
301
+ if (type === "object" && isRecord(resolvedProp.properties)) {
302
+ fields.push(...flattenBody(spec, prop, name, nextStack));
303
+ continue;
304
+ }
305
+ if (type === "array") {
306
+ fields.push(arrayField(spec, name, required, resolvedProp, nextStack));
307
+ continue;
308
+ }
309
+ fields.push(fieldFromSchema("body", name, required, resolvedProp));
310
+ }
311
+ return fields;
312
+ }
313
+ function resolveParameter(spec, parameter) {
314
+ if (!isRecord(parameter)) return null;
315
+ if (typeof parameter.$ref === "string") {
316
+ const resolved = resolveRef(spec, parameter.$ref);
317
+ if (!isRecord(resolved)) throw new Error(`Unresolved $ref ${parameter.$ref}.`);
318
+ const { $ref: _ref, ...siblings } = parameter;
319
+ const description = stringDesc(siblings) || stringDesc(resolved);
320
+ return {
321
+ ...resolved,
322
+ ...siblings,
323
+ ...description ? { description } : {}
324
+ };
325
+ }
326
+ return parameter;
327
+ }
328
+ function paramToField(spec, param) {
329
+ const location = param.in;
330
+ if (location !== "path" && location !== "query" && location !== "header") return null;
331
+ if (typeof param.name !== "string") return null;
332
+ const schema = isRecord(param.schema) ? resolveSchema(spec, param.schema, []) : {};
333
+ const required = location === "path" ? param.required !== false : param.required === true;
334
+ const type = schemaType(schema);
335
+ const itemSchema = type === "array" && isRecord(schema.items) ? resolveSchema(spec, schema.items, []) : null;
336
+ return {
337
+ in: location,
338
+ name: param.name,
339
+ required,
340
+ type,
341
+ description: stringDesc(param) || stringDesc(schema),
342
+ ...optionalEnum(schema),
343
+ ...optionalNullable(schema),
344
+ ...itemSchema ? { items: {
345
+ type: schemaType(itemSchema),
346
+ ...optionalEnum(itemSchema),
347
+ ...optionalNullable(itemSchema)
348
+ } } : {}
349
+ };
350
+ }
351
+ function pathParamNames(template) {
352
+ return [...template.matchAll(/\{([^}]+)\}/g)].map((match) => match[1]);
353
+ }
354
+ function pathMatchesTemplate(path, template) {
355
+ const pathParts = path.split("/");
356
+ const templateParts = template.split("/");
357
+ if (pathParts.length !== templateParts.length) return false;
358
+ return templateParts.every((part, i) => {
359
+ if (part.startsWith("{") && part.endsWith("}")) return pathParts[i] !== "";
360
+ return part === pathParts[i];
361
+ });
362
+ }
363
+ function matchPath(spec, requestPath) {
364
+ const path = requestPath.split("?")[0] ?? requestPath;
365
+ const paths = spec.paths ?? {};
366
+ const exact = paths[path];
367
+ if (isRecord(exact)) return {
368
+ template: path,
369
+ pathItem: exact
370
+ };
371
+ const matches = Object.entries(paths).filter(([template, item]) => isRecord(item) && pathMatchesTemplate(path, template));
372
+ if (matches.length === 0) throw new Error(`No route matches "${path}". Run \`neon api --list\` to see available routes.`);
373
+ matches.sort((a, b) => {
374
+ const staticA = a[0].split("/").filter((p) => !p.startsWith("{")).length;
375
+ return b[0].split("/").filter((p) => !p.startsWith("{")).length - staticA;
376
+ });
377
+ const [template, pathItem] = matches[0];
378
+ if (!isRecord(pathItem)) throw new Error(`No route matches "${path}". Run \`neon api --list\` to see available routes.`);
379
+ return {
380
+ template,
381
+ pathItem
382
+ };
383
+ }
384
+ function availableMethods(pathItem) {
385
+ return DESCRIBE_METHODS.filter((method) => isRecord(pathItem[method.toLowerCase()]));
386
+ }
387
+ function collectParameters(spec, pathItem, operation, template) {
388
+ const raw = [...Array.isArray(pathItem.parameters) ? pathItem.parameters : [], ...Array.isArray(operation.parameters) ? operation.parameters : []];
389
+ const byKey = /* @__PURE__ */ new Map();
390
+ for (const entry of raw) {
391
+ const resolved = resolveParameter(spec, entry);
392
+ if (!resolved) continue;
393
+ const field = paramToField(spec, resolved);
394
+ if (!field) continue;
395
+ byKey.set(`${field.in}:${field.name}`, field);
396
+ }
397
+ const pathFields = pathParamNames(template).map((name) => {
398
+ const existing = byKey.get(`path:${name}`);
399
+ if (existing) return existing;
400
+ return {
401
+ in: "path",
402
+ name,
403
+ required: true,
404
+ type: "string",
405
+ description: ""
406
+ };
407
+ });
408
+ const query = [...byKey.values()].filter((field) => field.in === "query");
409
+ const header = [...byKey.values()].filter((field) => field.in === "header");
410
+ return [
411
+ ...pathFields,
412
+ ...query,
413
+ ...header
414
+ ];
415
+ }
416
+ function jsonBodySchema(spec, operation) {
417
+ let requestBody = operation.requestBody;
418
+ if (isRecord(requestBody) && typeof requestBody.$ref === "string") requestBody = resolveRef(spec, requestBody.$ref);
419
+ if (!isRecord(requestBody)) return null;
420
+ const content = requestBody.content;
421
+ if (!isRecord(content)) return null;
422
+ const json = content["application/json"];
423
+ let contentType = "application/json";
424
+ let selected = json;
425
+ if (!isRecord(json)) {
426
+ const entry = Object.entries(content).find(([, value]) => isRecord(value));
427
+ if (!entry) return null;
428
+ contentType = entry[0];
429
+ selected = entry[1];
430
+ }
431
+ if (!isRecord(selected) || selected.schema === void 0) return null;
432
+ return {
433
+ schema: selected.schema,
434
+ required: requestBody.required === true,
435
+ contentType
436
+ };
437
+ }
438
+ function describeOperation(spec, path, method) {
439
+ const methodUpper = method.toUpperCase();
440
+ const { template, pathItem } = matchPath(spec, path);
441
+ const available = availableMethods(pathItem);
442
+ const operation = pathItem[methodUpper.toLowerCase()];
443
+ if (!isRecord(operation)) {
444
+ const hint = available.length > 0 ? ` Available: ${available.join(", ")}. Pass -X ${available[0]}.` : " Run `neon api --list` to see available routes.";
445
+ throw new Error(`No ${methodUpper} ${template} in the spec.${hint}`);
446
+ }
447
+ const body = jsonBodySchema(spec, operation);
448
+ const fields = [...collectParameters(spec, pathItem, operation, template), ...body ? flattenBody(spec, body.schema, "") : []];
449
+ return {
450
+ method: methodUpper,
451
+ path: template,
452
+ summary: typeof operation.summary === "string" ? operation.summary : "",
453
+ operationId: typeof operation.operationId === "string" ? operation.operationId : "",
454
+ bodyRequired: body?.required === true,
455
+ contentType: body?.contentType ?? "",
456
+ fields
457
+ };
458
+ }
101
459
  //#endregion
102
- export { DEFAULT_SPEC_URL, getEndpoints, loadSpec };
460
+ export { DEFAULT_SPEC_URL, describeOperation, getEndpoints, loadSpec };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neon",
3
- "version": "4.9.0",
3
+ "version": "4.10.1",
4
4
  "description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
5
5
  "keywords": [
6
6
  "neon",
@@ -63,9 +63,9 @@
63
63
  "yaml": "^2.9.0",
64
64
  "yargs": "17.7.2",
65
65
  "yoctocolors": "^2.1.2",
66
- "@neon/config-runtime": "1.0.5",
66
+ "@neon/config": "1.0.5",
67
67
  "@neon/sdk": "3.0.0",
68
- "@neon/config": "1.0.5"
68
+ "@neon/config-runtime": "1.0.5"
69
69
  },
70
70
  "optionalDependencies": {
71
71
  "@napi-rs/keyring": "1.3.0",
@@ -96,9 +96,9 @@
96
96
  "tsx": "4.22.3",
97
97
  "typescript": "^5.9.0",
98
98
  "vitest": "^3.0.9",
99
+ "@neon-internals/cli-core": "0.0.0",
99
100
  "@neon-internals/env-core": "0.0.5",
100
- "@neon/e2e-harness": "0.0.0",
101
- "@neon-internals/cli-core": "0.0.0"
101
+ "@neon/e2e-harness": "0.0.0"
102
102
  },
103
103
  "publishConfig": {
104
104
  "access": "public",