neon 4.8.0 → 4.10.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/README.md CHANGED
@@ -133,6 +133,18 @@ automation:
133
133
  neon branches create --project-id <project-id> --name production --protected
134
134
  ```
135
135
 
136
+ Project and branch creation include connection credentials in their output by
137
+ default for backward compatibility. When the output may be logged or passed to
138
+ an agent, use `--no-secrets` to return only the created resource metadata:
139
+
140
+ ```bash
141
+ neon projects create --name my-project --output json --no-secrets
142
+ neon branches create --project-id <project-id> --name preview --output json --no-secrets
143
+ ```
144
+
145
+ The flag omits the complete `connection_uris` block rather than redacting one
146
+ field. Retrieve a connection string separately when it is actually needed.
147
+
136
148
  ### Enable logical replication
137
149
 
138
150
  Enable logical replication for every endpoint in an existing project:
@@ -1096,6 +1108,20 @@ When both are only environment variables the key wins, which keeps a CI pipeline
1096
1108
 
1097
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.
1098
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
+
1099
1125
  ## API keys (`api-keys`)
1100
1126
 
1101
1127
  ```bash
@@ -1170,6 +1196,7 @@ Id Name Project Created At Last Used At Last
1170
1196
  | claim (`claimable`) | `create`, `status`, `accept`, `list`, `delete` | Manage claimable projects |
1171
1197
  | profile | `list`, `create`, `rotate-key`, `remove` | Manage named sets of credentials |
1172
1198
  | api-keys | `list`, `create`, `revoke` | Manage API keys |
1199
+ | api | | Call any Neon API route |
1173
1200
  | [projects](https://neon.com/docs/reference/cli-projects) | `list`, `create`, `update`, `delete`, `get` | Manage projects |
1174
1201
  | [ip-allow](https://neon.com/docs/reference/cli-ip-allow) | `list`, `add`, `remove`, `reset` | Manage IP Allow |
1175
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
@@ -44,6 +44,11 @@ const builder = (argv) => argv.usage("$0 branches <sub-command> [options]").opti
44
44
  } }).middleware(fillSingleProject).middleware((args) => {
45
45
  args.branchId ??= args.id;
46
46
  }).command("list", "List branches", (yargs) => yargs, (args) => list(args)).command("create", "Create a branch", (yargs) => yargs.options({
47
+ secrets: {
48
+ describe: "Include connection credentials in command output. Use --no-secrets to omit them",
49
+ type: "boolean",
50
+ default: true
51
+ },
47
52
  name: branchCreateRequest["branch.name"],
48
53
  parent: {
49
54
  describe: "Parent branch name or id or timestamp or LSN. Defaults to the default branch",
@@ -220,21 +225,31 @@ const create = async (props) => {
220
225
  }));
221
226
  if (branches.find((b) => b.id === data.branch.parent_id)?.protected) log.warning("The parent branch is protected; a unique role password has been generated for the new branch.");
222
227
  const out = writer(props);
223
- out.write(data.branch, {
224
- fields: BRANCH_FIELDS,
225
- title: "branch",
226
- emptyMessage: "No branches have been found."
227
- });
228
- if (data.endpoints?.length > 0) out.write(data.endpoints, {
229
- fields: ["id", "created_at"],
230
- title: "endpoints",
231
- emptyMessage: "No endpoints have been found."
232
- });
233
- if (data.connection_uris?.length) out.write(data.connection_uris, {
234
- fields: ["connection_uri"],
235
- title: "connection_uris",
236
- emptyMessage: "No connection uris have been found"
228
+ const endpoints = data.endpoints ?? [];
229
+ const connectionUris = data.connection_uris ?? [];
230
+ const writeEndpoints = endpoints.length > 0;
231
+ const writeUris = props.secrets && connectionUris.length > 0;
232
+ if (!props.secrets && (props.output === "json" || props.output === "yaml") && !writeEndpoints) out.write({ branch: data.branch }, {
233
+ fields: ["branch"],
234
+ title: "branch"
237
235
  });
236
+ else {
237
+ out.write(data.branch, {
238
+ fields: BRANCH_FIELDS,
239
+ title: "branch",
240
+ emptyMessage: "No branches have been found."
241
+ });
242
+ if (writeEndpoints) out.write(endpoints, {
243
+ fields: ["id", "created_at"],
244
+ title: "endpoints",
245
+ emptyMessage: "No endpoints have been found."
246
+ });
247
+ if (writeUris) out.write(connectionUris, {
248
+ fields: ["connection_uri"],
249
+ title: "connection_uris",
250
+ emptyMessage: "No connection uris have been found"
251
+ });
252
+ }
238
253
  out.end();
239
254
  if (props.psql) {
240
255
  if (!data.connection_uris?.length) throw new Error(`Branch ${data.branch.id} doesn't have a connection uri`);
@@ -62,6 +62,11 @@ const builder = (argv) => {
62
62
  }), async (args) => {
63
63
  await handleMissingOrgId(args, list);
64
64
  }).command("create", "Create a project", (yargs) => yargs.options({
65
+ secrets: {
66
+ describe: "Include connection credentials in command output. Use --no-secrets to omit them",
67
+ type: "boolean",
68
+ default: true
69
+ },
65
70
  "block-public-connections": {
66
71
  describe: projectCreateRequest["project.settings.block_public_connections"].description,
67
72
  type: "boolean"
@@ -230,14 +235,20 @@ const create = async (props) => {
230
235
  const { data } = await props.apiClient.createProject({ project });
231
236
  if (props.setContext) updateContextFile(props.contextFile, { projectId: data.project.id });
232
237
  const out = writer(props);
233
- out.write(data.project, {
234
- fields: PROJECT_FIELDS,
238
+ if (!props.secrets && (props.output === "json" || props.output === "yaml")) out.write({ project: data.project }, {
239
+ fields: ["project"],
235
240
  title: "Project"
236
241
  });
237
- out.write(data.connection_uris, {
238
- fields: ["connection_uri"],
239
- title: "Connection URIs"
240
- });
242
+ else {
243
+ out.write(data.project, {
244
+ fields: PROJECT_FIELDS,
245
+ title: "Project"
246
+ });
247
+ if (props.secrets) out.write(data.connection_uris, {
248
+ fields: ["connection_uri"],
249
+ title: "Connection URIs"
250
+ });
251
+ }
241
252
  out.end();
242
253
  if (props.psql) {
243
254
  const connection_uri = data.connection_uris[0].connection_uri;
@@ -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.8.0",
3
+ "version": "4.10.0",
4
4
  "description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
5
5
  "keywords": [
6
6
  "neon",
@@ -64,8 +64,8 @@
64
64
  "yargs": "17.7.2",
65
65
  "yoctocolors": "^2.1.2",
66
66
  "@neon/config": "1.0.5",
67
- "@neon/config-runtime": "1.0.5",
68
- "@neon/sdk": "3.0.0"
67
+ "@neon/sdk": "3.0.0",
68
+ "@neon/config-runtime": "1.0.5"
69
69
  },
70
70
  "optionalDependencies": {
71
71
  "@napi-rs/keyring": "1.3.0",