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 +16 -1
- package/dist/commands/api.js +95 -5
- package/dist/utils/inspect_queries.js +6 -3
- package/dist/utils/openapi.js +359 -1
- package/package.json +5 -5
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 |
|
package/dist/commands/api.js
CHANGED
|
@@ -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
|
|
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
|
|
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: {
|
package/dist/utils/openapi.js
CHANGED
|
@@ -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.
|
|
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
|
|
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",
|