neonctl 2.37.1 → 2.38.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.
Files changed (159) hide show
  1. package/README.md +15 -700
  2. package/bin/cli.js +3 -0
  3. package/package.json +8 -75
  4. package/dist/analytics.js +0 -168
  5. package/dist/api.js +0 -720
  6. package/dist/auth.js +0 -125
  7. package/dist/callback.html +0 -60
  8. package/dist/cli.js +0 -9
  9. package/dist/commands/api.js +0 -278
  10. package/dist/commands/auth.js +0 -214
  11. package/dist/commands/bootstrap.js +0 -481
  12. package/dist/commands/branches.js +0 -488
  13. package/dist/commands/bucket.js +0 -549
  14. package/dist/commands/checkout.js +0 -321
  15. package/dist/commands/config.js +0 -626
  16. package/dist/commands/connection_string.js +0 -172
  17. package/dist/commands/data_api.js +0 -284
  18. package/dist/commands/databases.js +0 -82
  19. package/dist/commands/deploy.js +0 -26
  20. package/dist/commands/dev.js +0 -698
  21. package/dist/commands/diff.js +0 -222
  22. package/dist/commands/env.js +0 -189
  23. package/dist/commands/functions.js +0 -373
  24. package/dist/commands/index.js +0 -62
  25. package/dist/commands/init.js +0 -73
  26. package/dist/commands/inspect.js +0 -65
  27. package/dist/commands/ip_allow.js +0 -137
  28. package/dist/commands/link.js +0 -1121
  29. package/dist/commands/neon_auth.js +0 -1028
  30. package/dist/commands/operations.js +0 -28
  31. package/dist/commands/orgs.js +0 -24
  32. package/dist/commands/projects.js +0 -413
  33. package/dist/commands/psql.js +0 -62
  34. package/dist/commands/roles.js +0 -65
  35. package/dist/commands/schema_diff.js +0 -151
  36. package/dist/commands/set_context.js +0 -29
  37. package/dist/commands/snapshots.js +0 -455
  38. package/dist/commands/status.js +0 -40
  39. package/dist/commands/user.js +0 -15
  40. package/dist/commands/vpc_endpoints.js +0 -134
  41. package/dist/config.js +0 -11
  42. package/dist/config_format.js +0 -72
  43. package/dist/context.js +0 -231
  44. package/dist/current_branch_fast_path.js +0 -55
  45. package/dist/dev/env.js +0 -244
  46. package/dist/dev/functions.js +0 -70
  47. package/dist/dev/inputs.js +0 -63
  48. package/dist/dev/runtime.js +0 -146
  49. package/dist/env.js +0 -36
  50. package/dist/env_file.js +0 -159
  51. package/dist/errors.js +0 -80
  52. package/dist/functions_api.js +0 -48
  53. package/dist/help.js +0 -146
  54. package/dist/index.js +0 -240
  55. package/dist/log.js +0 -18
  56. package/dist/parameters.gen.js +0 -480
  57. package/dist/pkg.js +0 -25
  58. package/dist/psql/cli.js +0 -53
  59. package/dist/psql/command/cmd_cond.js +0 -437
  60. package/dist/psql/command/cmd_connect.js +0 -820
  61. package/dist/psql/command/cmd_copy.js +0 -1035
  62. package/dist/psql/command/cmd_describe.js +0 -1815
  63. package/dist/psql/command/cmd_format.js +0 -948
  64. package/dist/psql/command/cmd_io.js +0 -2193
  65. package/dist/psql/command/cmd_lo.js +0 -393
  66. package/dist/psql/command/cmd_meta.js +0 -969
  67. package/dist/psql/command/cmd_misc.js +0 -187
  68. package/dist/psql/command/cmd_pipeline.js +0 -1148
  69. package/dist/psql/command/cmd_restrict.js +0 -171
  70. package/dist/psql/command/cmd_show.js +0 -766
  71. package/dist/psql/command/dispatch.js +0 -343
  72. package/dist/psql/command/inputQueue.js +0 -42
  73. package/dist/psql/command/shared.js +0 -71
  74. package/dist/psql/complete/filenames.js +0 -139
  75. package/dist/psql/complete/index.js +0 -104
  76. package/dist/psql/complete/matcher.js +0 -315
  77. package/dist/psql/complete/psqlVars.js +0 -249
  78. package/dist/psql/complete/queries.js +0 -493
  79. package/dist/psql/complete/rules.js +0 -2424
  80. package/dist/psql/core/common.js +0 -1253
  81. package/dist/psql/core/help.js +0 -576
  82. package/dist/psql/core/mainloop.js +0 -1360
  83. package/dist/psql/core/prompt.js +0 -439
  84. package/dist/psql/core/settings.js +0 -686
  85. package/dist/psql/core/sqlHelp.js +0 -1066
  86. package/dist/psql/core/startup.js +0 -846
  87. package/dist/psql/core/syncVars.js +0 -116
  88. package/dist/psql/core/variables.js +0 -287
  89. package/dist/psql/describe/formatters.js +0 -1330
  90. package/dist/psql/describe/processNamePattern.js +0 -270
  91. package/dist/psql/describe/queries.js +0 -2452
  92. package/dist/psql/describe/versionGate.js +0 -44
  93. package/dist/psql/index.js +0 -2030
  94. package/dist/psql/io/history.js +0 -299
  95. package/dist/psql/io/input.js +0 -120
  96. package/dist/psql/io/lineEditor/buffer.js +0 -325
  97. package/dist/psql/io/lineEditor/complete.js +0 -227
  98. package/dist/psql/io/lineEditor/filename.js +0 -159
  99. package/dist/psql/io/lineEditor/index.js +0 -893
  100. package/dist/psql/io/lineEditor/keymap.js +0 -745
  101. package/dist/psql/io/lineEditor/vt100.js +0 -363
  102. package/dist/psql/io/pgpass.js +0 -202
  103. package/dist/psql/io/pgservice.js +0 -194
  104. package/dist/psql/io/psqlrc.js +0 -422
  105. package/dist/psql/print/aligned.js +0 -1748
  106. package/dist/psql/print/asciidoc.js +0 -230
  107. package/dist/psql/print/crosstab.js +0 -463
  108. package/dist/psql/print/csv.js +0 -76
  109. package/dist/psql/print/html.js +0 -240
  110. package/dist/psql/print/json.js +0 -96
  111. package/dist/psql/print/latex.js +0 -379
  112. package/dist/psql/print/pager.js +0 -267
  113. package/dist/psql/print/troff.js +0 -240
  114. package/dist/psql/print/unaligned.js +0 -99
  115. package/dist/psql/print/units.js +0 -188
  116. package/dist/psql/scanner/slash.js +0 -515
  117. package/dist/psql/scanner/sql.js +0 -914
  118. package/dist/psql/scanner/stringutils.js +0 -394
  119. package/dist/psql/types/backslash.js +0 -1
  120. package/dist/psql/types/connection.js +0 -1
  121. package/dist/psql/types/index.js +0 -7
  122. package/dist/psql/types/printer.js +0 -1
  123. package/dist/psql/types/repl.js +0 -1
  124. package/dist/psql/types/scanner.js +0 -24
  125. package/dist/psql/types/settings.js +0 -1
  126. package/dist/psql/types/variables.js +0 -1
  127. package/dist/psql/wire/connection.js +0 -2857
  128. package/dist/psql/wire/copy.js +0 -108
  129. package/dist/psql/wire/notify.js +0 -59
  130. package/dist/psql/wire/pipeline.js +0 -521
  131. package/dist/psql/wire/protocol.js +0 -466
  132. package/dist/psql/wire/sasl.js +0 -294
  133. package/dist/psql/wire/tls.js +0 -602
  134. package/dist/storage_api.js +0 -147
  135. package/dist/test_utils/fixtures.js +0 -121
  136. package/dist/test_utils/oauth_server.js +0 -9
  137. package/dist/types.js +0 -1
  138. package/dist/utils/ai_gateway_notice.js +0 -180
  139. package/dist/utils/api_enums.js +0 -33
  140. package/dist/utils/auth.js +0 -5
  141. package/dist/utils/branch_notice.js +0 -22
  142. package/dist/utils/branch_picker.js +0 -103
  143. package/dist/utils/compute_units.js +0 -28
  144. package/dist/utils/config_diff.js +0 -185
  145. package/dist/utils/enrichers.js +0 -161
  146. package/dist/utils/esbuild.js +0 -158
  147. package/dist/utils/formats.js +0 -18
  148. package/dist/utils/git_diff.js +0 -90
  149. package/dist/utils/inspect_db.js +0 -153
  150. package/dist/utils/inspect_queries.js +0 -372
  151. package/dist/utils/middlewares.js +0 -20
  152. package/dist/utils/openapi.js +0 -114
  153. package/dist/utils/package_manager.js +0 -68
  154. package/dist/utils/point_in_time.js +0 -56
  155. package/dist/utils/psql.js +0 -120
  156. package/dist/utils/string.js +0 -5
  157. package/dist/utils/ui.js +0 -59
  158. package/dist/utils/zip.js +0 -4
  159. package/dist/writer.js +0 -97
@@ -1,153 +0,0 @@
1
- import { parseConnectionUri } from "../psql/index.js";
2
- import { PgConnection } from "../psql/wire/connection.js";
3
- import { EndpointType } from "./api_enums.js";
4
- import { branchIdFromProps } from "./enrichers.js";
5
- import { parsePITBranch } from "./point_in_time.js";
6
- export const SSL_MODES = [
7
- "require",
8
- "verify-ca",
9
- "verify-full",
10
- "omit",
11
- ];
12
- /**
13
- * Resolve a branch's live Postgres connection details via the Neon API
14
- * (endpoint → role → password → database → URL, honoring point-in-time,
15
- * pooling, prisma tuning, and SSL mode). Mirrors how `connection-string`
16
- * resolves a branch so `inspect db` connects the same way.
17
- */
18
- export const resolveConnectionUri = async (props) => {
19
- const projectId = props.projectId;
20
- const parsedPIT = props.branch
21
- ? parsePITBranch(props.branch)
22
- : { tag: "head", branch: "" };
23
- if (props.branch) {
24
- props.branch = parsedPIT.branch;
25
- }
26
- const branchId = await branchIdFromProps(props);
27
- const { data: { endpoints }, } = await props.apiClient.listProjectBranchEndpoints(projectId, branchId);
28
- const matchEndpointType = props.endpointType ?? EndpointType.ReadWrite;
29
- let endpoint = endpoints.find((e) => e.type === matchEndpointType);
30
- if (!endpoint && props.endpointType == null) {
31
- endpoint = endpoints[0];
32
- }
33
- if (!endpoint) {
34
- throw new Error(`No ${props.endpointType ?? ""} endpoint found for the branch: ${branchId}`);
35
- }
36
- const role = props.roleName ||
37
- (await props.apiClient
38
- .listProjectBranchRoles(projectId, branchId)
39
- .then(({ data }) => {
40
- if (data.roles.length === 0) {
41
- throw new Error(`No roles found for the branch: ${branchId}`);
42
- }
43
- if (data.roles.length === 1) {
44
- return data.roles[0].name;
45
- }
46
- throw new Error(`Multiple roles found for the branch, please provide one with the --role-name option: ${data.roles
47
- .map((r) => r.name)
48
- .join(", ")}`);
49
- }));
50
- const { data: { databases: branchDatabases }, } = await props.apiClient.listProjectBranchDatabases(projectId, branchId);
51
- const database = props.databaseName ||
52
- (() => {
53
- if (branchDatabases.length === 0) {
54
- throw new Error(`No databases found for the branch: ${branchId}`);
55
- }
56
- if (branchDatabases.length === 1) {
57
- return branchDatabases[0].name;
58
- }
59
- throw new Error(`Multiple databases found for the branch, please provide one with the --database-name option: ${branchDatabases
60
- .map((d) => d.name)
61
- .join(", ")}`);
62
- })();
63
- if (!branchDatabases.find((d) => d.name === database)) {
64
- throw new Error(`Database not found: ${database}`);
65
- }
66
- const { data: { password }, } = await props.apiClient.getProjectBranchRolePassword(props.projectId, endpoint.branch_id, role);
67
- let host = props.pooled
68
- ? endpoint.host.replace(endpoint.id, `${endpoint.id}-pooler`)
69
- : endpoint.host;
70
- if (parsedPIT.tag !== "head") {
71
- host = endpoint.host.replace(endpoint.id, endpoint.branch_id);
72
- }
73
- const connectionString = new URL(`postgresql://${host}`);
74
- connectionString.pathname = database;
75
- connectionString.username = role;
76
- connectionString.password = password;
77
- if (props.prisma) {
78
- connectionString.searchParams.set("connect_timeout", "30");
79
- if (props.pooled) {
80
- connectionString.searchParams.set("pool_timeout", "30");
81
- connectionString.searchParams.set("pgbouncer", "true");
82
- }
83
- }
84
- const ssl = props.ssl ?? "require";
85
- if (ssl !== "omit") {
86
- connectionString.searchParams.set("sslmode", ssl);
87
- connectionString.searchParams.set("channel_binding", "require");
88
- }
89
- if (parsedPIT.tag === "lsn") {
90
- connectionString.searchParams.set("options", `neon_lsn:${parsedPIT.lsn}`);
91
- }
92
- else if (parsedPIT.tag === "timestamp") {
93
- connectionString.searchParams.set("options", `neon_timestamp:${parsedPIT.timestamp}`);
94
- }
95
- return {
96
- connectionUri: connectionString.toString(),
97
- host,
98
- role,
99
- password,
100
- database,
101
- options: connectionString.searchParams.toString(),
102
- };
103
- };
104
- const rowsFrom = (result) => {
105
- const names = result.fields.map((f) => f.name);
106
- return result.rows.map((row) => {
107
- const obj = {};
108
- names.forEach((name, i) => {
109
- obj[name] = row[i];
110
- });
111
- return obj;
112
- });
113
- };
114
- /**
115
- * Connect to `connectionUri` with the embedded wire client, run a single
116
- * read-only `sql`, and return the rows as objects keyed by column name (so the
117
- * shared `writer` can render them by field). The connection is always closed,
118
- * even on error.
119
- */
120
- export const runInspectQuery = async (connectionUri, sql, options = {}) => {
121
- const opts = parseConnectionUri(connectionUri);
122
- let connection;
123
- try {
124
- connection = await PgConnection.connect(opts);
125
- }
126
- catch (err) {
127
- // Give the failure a Postgres-specific message. The raw socket error
128
- // (e.g. ECONNREFUSED) would otherwise be mislabeled by the top-level
129
- // `isNetworkError` handler as a "Could not reach the Neon API" hint —
130
- // misleading for `inspect db`, which talks to Postgres, not the API.
131
- // We intentionally do not chain `cause` so that classifier can't match
132
- // the socket `code`; the underlying reason is preserved in the message.
133
- const reason = err instanceof Error ? err.message : String(err);
134
- throw new Error(`Could not connect to Postgres at ${opts.host}:${opts.port}: ${reason}`);
135
- }
136
- try {
137
- const ext = options.requiresExtension;
138
- if (ext) {
139
- // `ext` is an internal constant from INSPECT_QUERIES, never user input,
140
- // so interpolating it into the literal is safe.
141
- const check = await connection.query(`SELECT 1 FROM pg_extension WHERE extname = '${ext}';`);
142
- if (check.rows.length === 0) {
143
- throw new Error(`This query needs the "${ext}" extension, which is not installed. ` +
144
- `Enable it with: CREATE EXTENSION ${ext};`);
145
- }
146
- }
147
- const result = await connection.query(sql);
148
- return rowsFrom(result);
149
- }
150
- finally {
151
- await connection.close();
152
- }
153
- };
@@ -1,372 +0,0 @@
1
- /**
2
- * Diagnostic SQL for `neon inspect db`. Each entry is a read-only query against
3
- * Postgres' own statistics/catalog views plus the output columns (in display
4
- * order) the table/json/yaml formatter should show.
5
- *
6
- * Most queries need no extra extension. The few that do (currently `outliers`
7
- * and `calls`, which read `pg_stat_statements`) declare it via
8
- * `requiresExtension`; the runner checks for it and fails with an actionable
9
- * hint when it's missing.
10
- */
11
- export const INSPECT_QUERIES = {
12
- "table-sizes": {
13
- describe: "Size of each table (including TOAST), largest first (pg_table_size)",
14
- fields: ["schema", "name", "size"],
15
- emptyMessage: "No user tables found.",
16
- sql: /* sql */ `
17
- SELECT
18
- n.nspname AS schema,
19
- c.relname AS name,
20
- pg_size_pretty(pg_table_size(c.oid)) AS size
21
- FROM pg_class c
22
- LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
23
- WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
24
- AND n.nspname !~ '^pg_toast'
25
- AND c.relkind = 'r'
26
- ORDER BY pg_table_size(c.oid) DESC;
27
- `,
28
- },
29
- "index-sizes": {
30
- describe: "Size of each index, largest first (pg_relation_size)",
31
- fields: ["schema", "name", "size"],
32
- emptyMessage: "No indexes found.",
33
- sql: /* sql */ `
34
- SELECT
35
- n.nspname AS schema,
36
- c.relname AS name,
37
- pg_size_pretty(pg_relation_size(c.oid)) AS size
38
- FROM pg_class c
39
- LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
40
- WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
41
- AND n.nspname !~ '^pg_toast'
42
- AND c.relkind = 'i'
43
- ORDER BY pg_relation_size(c.oid) DESC;
44
- `,
45
- },
46
- "unused-indexes": {
47
- describe: "Non-unique indexes with few scans — candidates for removal (pg_stat_user_indexes)",
48
- fields: ["table", "index", "index_size", "index_scans"],
49
- emptyMessage: "No unused indexes detected.",
50
- sql: /* sql */ `
51
- SELECT
52
- s.schemaname || '.' || s.relname AS "table",
53
- s.indexrelname AS index,
54
- pg_size_pretty(pg_relation_size(s.indexrelid)) AS index_size,
55
- s.idx_scan AS index_scans
56
- FROM pg_stat_user_indexes s
57
- JOIN pg_index i ON i.indexrelid = s.indexrelid
58
- WHERE NOT i.indisunique
59
- AND NOT i.indisprimary
60
- AND s.idx_scan < 50
61
- ORDER BY pg_relation_size(s.indexrelid) DESC, s.idx_scan ASC;
62
- `,
63
- },
64
- "seq-scans": {
65
- describe: "Number of sequential scans recorded against each table (pg_stat_user_tables)",
66
- fields: ["schema", "name", "count"],
67
- emptyMessage: "No user tables found.",
68
- sql: /* sql */ `
69
- SELECT
70
- schemaname AS schema,
71
- relname AS name,
72
- seq_scan AS count
73
- FROM pg_stat_user_tables
74
- ORDER BY seq_scan DESC;
75
- `,
76
- },
77
- "long-running-queries": {
78
- describe: "Queries running longer than 5 minutes (pg_stat_activity)",
79
- fields: ["pid", "duration", "state", "query"],
80
- emptyMessage: "No long-running queries.",
81
- sql: /* sql */ `
82
- SELECT
83
- pid,
84
- (now() - query_start)::text AS duration,
85
- state,
86
- query
87
- FROM pg_stat_activity
88
- WHERE state <> 'idle'
89
- AND query NOT ILIKE '%pg_stat_activity%'
90
- AND now() - query_start > interval '5 minutes'
91
- ORDER BY now() - query_start DESC;
92
- `,
93
- },
94
- locks: {
95
- describe: "Locks held with the acquiring query and its age (pg_locks + pg_stat_activity)",
96
- fields: [
97
- "pid",
98
- "relname",
99
- "mode",
100
- "locktype",
101
- "granted",
102
- "age",
103
- "query",
104
- ],
105
- emptyMessage: "No locks held.",
106
- sql: /* sql */ `
107
- SELECT
108
- a.pid,
109
- c.relname,
110
- l.mode,
111
- l.locktype,
112
- l.granted,
113
- (now() - a.query_start)::text AS age,
114
- a.query
115
- FROM pg_locks l
116
- JOIN pg_stat_activity a ON a.pid = l.pid
117
- LEFT JOIN pg_class c ON c.oid = l.relation
118
- WHERE a.query <> '<insufficient privilege>'
119
- AND l.pid <> pg_backend_pid()
120
- ORDER BY a.query_start;
121
- `,
122
- },
123
- outliers: {
124
- describe: "Queries taking the most cumulative execution time (needs pg_stat_statements)",
125
- fields: ["total_exec_time", "prop_exec_time", "ncalls", "query"],
126
- emptyMessage: "No statements recorded yet.",
127
- requiresExtension: "pg_stat_statements",
128
- sql: /* sql */ `
129
- SELECT
130
- (interval '1 millisecond' * total_exec_time)::text AS total_exec_time,
131
- to_char(
132
- (total_exec_time / nullif(sum(total_exec_time) OVER (), 0)) * 100,
133
- 'FM990D0'
134
- ) || '%' AS prop_exec_time,
135
- calls AS ncalls,
136
- query
137
- FROM pg_stat_statements s
138
- JOIN pg_database d ON d.oid = s.dbid
139
- WHERE d.datname = current_database()
140
- ORDER BY total_exec_time DESC
141
- LIMIT 25;
142
- `,
143
- },
144
- calls: {
145
- describe: "Most frequently called queries (needs pg_stat_statements)",
146
- fields: ["ncalls", "total_exec_time", "prop_exec_time", "query"],
147
- emptyMessage: "No statements recorded yet.",
148
- requiresExtension: "pg_stat_statements",
149
- sql: /* sql */ `
150
- SELECT
151
- calls AS ncalls,
152
- (interval '1 millisecond' * total_exec_time)::text AS total_exec_time,
153
- to_char(
154
- (calls / nullif(sum(calls) OVER (), 0)::float) * 100,
155
- 'FM990D0'
156
- ) || '%' AS prop_exec_time,
157
- query
158
- FROM pg_stat_statements s
159
- JOIN pg_database d ON d.oid = s.dbid
160
- WHERE d.datname = current_database()
161
- ORDER BY calls DESC
162
- LIMIT 25;
163
- `,
164
- },
165
- "lfc-hit-rate": {
166
- describe: "Local File Cache hit rate (needs neon extension)",
167
- fields: ["name", "ratio"],
168
- emptyMessage: "No LFC stats available.",
169
- requiresExtension: "neon",
170
- // Read the hit/miss counters from neon_get_lfc_stats() rather than the
171
- // neon_lfc_stats / neon_stat_file_cache views: those views are owned by
172
- // cloud_admin and are not always readable by end-user roles, whereas the
173
- // function is EXECUTE-able by the default role.
174
- sql: /* sql */ `
175
- WITH lfc AS (
176
- SELECT lfc_key AS k, lfc_value AS v
177
- FROM neon_get_lfc_stats() t(lfc_key text, lfc_value bigint)
178
- )
179
- SELECT
180
- 'lfc hit rate' AS name,
181
- max(v) FILTER (WHERE k = 'file_cache_hits')::float
182
- / nullif(
183
- max(v) FILTER (WHERE k = 'file_cache_hits')
184
- + max(v) FILTER (WHERE k = 'file_cache_misses'),
185
- 0
186
- ) AS ratio
187
- FROM lfc;
188
- `,
189
- },
190
- "working-set": {
191
- describe: "Estimated working set vs LFC size (needs neon extension)",
192
- fields: ["window", "working_set", "lfc_size", "exceeds_lfc"],
193
- emptyMessage: "No working-set estimate available.",
194
- requiresExtension: "neon",
195
- sql: /* sql */ `
196
- SELECT
197
- w.window,
198
- pg_size_pretty(w.working_set_bytes) AS working_set,
199
- pg_size_pretty(pg_size_bytes(current_setting('neon.file_cache_size_limit'))) AS lfc_size,
200
- CASE
201
- WHEN w.working_set_bytes
202
- > pg_size_bytes(current_setting('neon.file_cache_size_limit'))
203
- THEN 'yes' ELSE 'no'
204
- END AS exceeds_lfc
205
- FROM (
206
- SELECT
207
- x AS window,
208
- approximate_working_set_size_seconds(
209
- extract('epoch' from x::interval)::int
210
- )::bigint * 8192 AS working_set_bytes
211
- FROM (VALUES ('1m'), ('5m'), ('15m'), ('1h')) AS t(x)
212
- ) w
213
- ORDER BY working_set_bytes;
214
- `,
215
- },
216
- "vacuum-stats": {
217
- describe: "Autovacuum status per table: last (auto)vacuum, dead tuples, threshold",
218
- fields: [
219
- "schema",
220
- "table",
221
- "last_vacuum",
222
- "last_autovacuum",
223
- "rowcount",
224
- "dead_rowcount",
225
- "expect_autovacuum",
226
- ],
227
- emptyMessage: "No user tables found.",
228
- sql: /* sql */ `
229
- SELECT
230
- n.nspname AS schema,
231
- c.relname AS "table",
232
- to_char(psut.last_vacuum, 'YYYY-MM-DD HH24:MI') AS last_vacuum,
233
- to_char(psut.last_autovacuum, 'YYYY-MM-DD HH24:MI') AS last_autovacuum,
234
- c.reltuples::bigint AS rowcount,
235
- psut.n_dead_tup AS dead_rowcount,
236
- CASE
237
- WHEN current_setting('autovacuum_vacuum_threshold')::bigint
238
- + current_setting('autovacuum_vacuum_scale_factor')::numeric
239
- * c.reltuples
240
- < psut.n_dead_tup
241
- THEN 'yes'
242
- ELSE 'no'
243
- END AS expect_autovacuum
244
- FROM pg_stat_user_tables psut
245
- JOIN pg_class c ON psut.relid = c.oid
246
- JOIN pg_namespace n ON c.relnamespace = n.oid
247
- ORDER BY psut.n_dead_tup DESC;
248
- `,
249
- },
250
- bloat: {
251
- describe: "Estimated table/index bloat (statistical estimate, no extension needed)",
252
- fields: ["type", "schema", "object_name", "bloat", "waste"],
253
- emptyMessage: "No bloat estimate available.",
254
- sql: /* sql */ `
255
- WITH constants AS (
256
- SELECT current_setting('block_size')::numeric AS bs, 23 AS hdr, 8 AS ma
257
- ),
258
- bloat_info AS (
259
- SELECT
260
- ma, bs, schemaname, tablename,
261
- (datawidth + (hdr + ma - (CASE WHEN hdr % ma = 0 THEN ma ELSE hdr % ma END)))::numeric AS datahdr,
262
- (maxfracsum * (nullhdr + ma - (CASE WHEN nullhdr % ma = 0 THEN ma ELSE nullhdr % ma END))) AS nullhdr2
263
- FROM (
264
- SELECT
265
- schemaname, tablename, hdr, ma, bs,
266
- SUM((1 - null_frac) * avg_width) AS datawidth,
267
- MAX(null_frac) AS maxfracsum,
268
- hdr + (
269
- SELECT 1 + count(*) / 8
270
- FROM pg_stats s2
271
- WHERE null_frac <> 0
272
- AND s2.schemaname = s.schemaname
273
- AND s2.tablename = s.tablename
274
- ) AS nullhdr
275
- FROM pg_stats s, constants
276
- GROUP BY 1, 2, 3, 4, 5
277
- ) AS foo
278
- ),
279
- table_bloat AS (
280
- SELECT
281
- schemaname, tablename, cc.relpages, bs,
282
- CEIL((cc.reltuples * ((datahdr + ma
283
- - (CASE WHEN datahdr % ma = 0 THEN ma ELSE datahdr % ma END))
284
- + nullhdr2 + 4)) / (bs - 20::float)) AS otta
285
- FROM bloat_info
286
- JOIN pg_class cc ON cc.relname = bloat_info.tablename
287
- JOIN pg_namespace nn ON cc.relnamespace = nn.oid
288
- AND nn.nspname = bloat_info.schemaname
289
- AND nn.nspname NOT IN ('information_schema', 'pg_catalog')
290
- WHERE cc.relkind = 'r'
291
- )
292
- SELECT
293
- 'table' AS type,
294
- schemaname AS schema,
295
- tablename AS object_name,
296
- round(CASE WHEN otta = 0 THEN 0.0 ELSE relpages::numeric / otta::numeric END, 1) AS bloat,
297
- pg_size_pretty(
298
- (CASE WHEN relpages < otta THEN 0 ELSE (bs * (relpages - otta))::bigint END)
299
- ) AS waste
300
- FROM table_bloat
301
- ORDER BY (CASE WHEN relpages < otta THEN 0 ELSE bs * (relpages - otta) END) DESC
302
- LIMIT 25;
303
- `,
304
- },
305
- "replication-slots": {
306
- describe: "Replication slots: kind, status, client, restart/confirmed-flush LSNs, and lag (pg_replication_slots + pg_stat_replication)",
307
- fields: [
308
- "slot_name",
309
- "slot_type",
310
- "slot_kind",
311
- "status",
312
- "client_addr",
313
- "restart_lsn",
314
- "confirmed_flush_lsn",
315
- "replication_lag",
316
- ],
317
- emptyMessage: "No replication slots found.",
318
- sql: /* sql */ `
319
- SELECT
320
- s.slot_name,
321
- s.slot_type,
322
- CASE
323
- WHEN s.slot_type = 'physical' THEN 'physical'
324
- WHEN s.slot_name LIKE 'pg_%_sync_%' THEN 'initial-sync (tablesync)'
325
- WHEN s.slot_type = 'logical' THEN 'CDC (apply)'
326
- ELSE 'other'
327
- END AS slot_kind,
328
- CASE
329
- WHEN r.state IS NOT NULL THEN r.state
330
- WHEN s.active THEN 'active (no walsender)'
331
- ELSE 'inactive'
332
- END AS status,
333
- r.client_addr,
334
- s.restart_lsn,
335
- s.confirmed_flush_lsn,
336
- pg_size_pretty(
337
- pg_wal_lsn_diff(
338
- CASE
339
- WHEN pg_is_in_recovery() THEN pg_last_wal_receive_lsn()
340
- ELSE pg_current_wal_lsn()
341
- END,
342
- COALESCE(s.confirmed_flush_lsn, s.restart_lsn)
343
- )
344
- ) AS replication_lag
345
- FROM pg_replication_slots s
346
- LEFT JOIN pg_stat_replication r ON r.pid = s.active_pid
347
- ORDER BY s.slot_name;
348
- `,
349
- },
350
- subscriptions: {
351
- describe: "Per-table logical replication progress on this subscriber (pg_subscription_rel)",
352
- fields: ["subscription", "table_name", "status", "lsn"],
353
- emptyMessage: "No subscriptions found on this database.",
354
- sql: /* sql */ `
355
- SELECT
356
- sub.subname AS subscription,
357
- sr.srrelid::regclass::text AS table_name,
358
- CASE sr.srsubstate
359
- WHEN 'i' THEN 'initialize'
360
- WHEN 'd' THEN 'data being copied'
361
- WHEN 'f' THEN 'finished table copy'
362
- WHEN 's' THEN 'synchronized'
363
- WHEN 'r' THEN 'ready'
364
- ELSE sr.srsubstate::text
365
- END AS status,
366
- sr.srsublsn AS lsn
367
- FROM pg_subscription_rel sr
368
- JOIN pg_subscription sub ON sub.oid = sr.srsubid
369
- ORDER BY sub.subname, table_name;
370
- `,
371
- },
372
- };
@@ -1,20 +0,0 @@
1
- /**
2
- * This middleware is needed to fill in the args for nested objects,
3
- * so that required arguments would work
4
- * otherwise yargs just throws an error
5
- */
6
- export const fillInArgs = (args, currentArgs = args, acc = []) => {
7
- Object.entries(currentArgs).forEach(([k, v]) => {
8
- if (k === "_" || k === "--") {
9
- return;
10
- }
11
- // check if the value is an Object
12
- if (typeof v === "object" && v !== null) {
13
- fillInArgs(args, v, [...acc, k]);
14
- }
15
- else if (acc.length > 0) {
16
- // if it's not an object, and we have a path, fill it in
17
- args[acc.join(".") + "." + k] = v;
18
- }
19
- });
20
- };
@@ -1,114 +0,0 @@
1
- // Lightweight loader for the Neon OpenAPI spec, used by `neon api --list` to
2
- // enumerate the available routes. The `neon api <path>` request path does NOT
3
- // depend on this module — it is a pure passthrough — so a stale or unreachable
4
- // spec never blocks a real API call. Listing degrades gracefully: fresh cache →
5
- // live fetch → stale cache → clear error.
6
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
- import { dirname, join } from "node:path";
8
- import { log } from "../log.js";
9
- /** Public URL of the released Neon OpenAPI v2 spec. */
10
- export const DEFAULT_SPEC_URL = "https://neon.com/api_spec/release/v2.json";
11
- const CACHE_FILE = "openapi-spec.json";
12
- const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
13
- const FETCH_TIMEOUT_MS = 10000;
14
- const HTTP_METHODS = new Set([
15
- "get",
16
- "post",
17
- "put",
18
- "patch",
19
- "delete",
20
- "head",
21
- "options",
22
- ]);
23
- async function fetchSpec(url) {
24
- const controller = new AbortController();
25
- const timer = setTimeout(() => {
26
- controller.abort();
27
- }, FETCH_TIMEOUT_MS);
28
- try {
29
- const res = await fetch(url, {
30
- signal: controller.signal,
31
- headers: { Accept: "application/json" },
32
- });
33
- if (!res.ok) {
34
- throw new Error(`Failed to fetch OpenAPI spec (${res.status} ${res.statusText})`);
35
- }
36
- return (await res.json());
37
- }
38
- finally {
39
- clearTimeout(timer);
40
- }
41
- }
42
- function readCache(cachePath) {
43
- try {
44
- return JSON.parse(readFileSync(cachePath, "utf8"));
45
- }
46
- catch {
47
- return null;
48
- }
49
- }
50
- function writeCache(cachePath, data) {
51
- try {
52
- mkdirSync(dirname(cachePath), { recursive: true });
53
- writeFileSync(cachePath, JSON.stringify(data));
54
- }
55
- catch (err) {
56
- log.debug("Failed to cache OpenAPI spec: %s", err);
57
- }
58
- }
59
- /**
60
- * Load the Neon OpenAPI spec, preferring a fresh on-disk cache, then a live
61
- * fetch (which refreshes the cache), then a stale cache as a last resort.
62
- * Returns `null` when no spec can be obtained.
63
- */
64
- export async function loadSpec(opts) {
65
- const { configDir, specUrl, refresh } = opts;
66
- const cachePath = join(configDir, CACHE_FILE);
67
- if (!refresh) {
68
- const cached = readCache(cachePath);
69
- if (cached &&
70
- cached.specUrl === specUrl &&
71
- Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
72
- log.debug("Using cached OpenAPI spec from %s", cachePath);
73
- return cached.spec;
74
- }
75
- }
76
- try {
77
- log.debug("Fetching OpenAPI spec from %s", specUrl);
78
- const spec = await fetchSpec(specUrl);
79
- writeCache(cachePath, { fetchedAt: Date.now(), specUrl, spec });
80
- return spec;
81
- }
82
- catch (err) {
83
- log.debug("Failed to fetch OpenAPI spec: %s", err);
84
- const stale = readCache(cachePath);
85
- if (stale && stale.specUrl === specUrl) {
86
- log.debug("Falling back to stale cached OpenAPI spec");
87
- return stale.spec;
88
- }
89
- return null;
90
- }
91
- }
92
- /** Flatten a spec into a sorted list of routes (by path, then method). */
93
- export function getEndpoints(spec) {
94
- const endpoints = [];
95
- for (const [path, item] of Object.entries(spec.paths ?? {})) {
96
- for (const [method, op] of Object.entries(item)) {
97
- if (!HTTP_METHODS.has(method.toLowerCase())) {
98
- continue;
99
- }
100
- const operation = (op ?? {});
101
- endpoints.push({
102
- method: method.toUpperCase(),
103
- path,
104
- summary: operation.summary,
105
- operationId: operation.operationId,
106
- tags: operation.tags ?? [],
107
- });
108
- }
109
- }
110
- endpoints.sort((a, b) => a.path === b.path
111
- ? a.method.localeCompare(b.method)
112
- : a.path.localeCompare(b.path));
113
- return endpoints;
114
- }