@kazurolabs/psql-explorer 0.0.0-stage → 0.3.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.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +505 -2
  3. package/RELEASES.md +87 -0
  4. package/dist/app.js +39 -0
  5. package/dist/cli.js +307 -0
  6. package/dist/commands/configgen.js +128 -0
  7. package/dist/commands/explore.js +388 -0
  8. package/dist/commands/terminal-prompt.js +68 -0
  9. package/dist/config/load.js +324 -0
  10. package/dist/config/resolve-environment.js +112 -0
  11. package/dist/config/resolvers/env.js +12 -0
  12. package/dist/config/resolvers/file.js +24 -0
  13. package/dist/config/resolvers/groups.js +1 -0
  14. package/dist/config/resolvers/index.js +49 -0
  15. package/dist/config/resolvers/literal.js +7 -0
  16. package/dist/config/resolvers/prompt.js +159 -0
  17. package/dist/config/resolvers/stored.js +13 -0
  18. package/dist/config/resolvers/types.js +18 -0
  19. package/dist/config/resolvers/vault.js +283 -0
  20. package/dist/config/schema.js +24 -0
  21. package/dist/config/source.js +105 -0
  22. package/dist/db/connect.js +55 -0
  23. package/dist/db/identifiers.js +6 -0
  24. package/dist/db/paths.js +14 -0
  25. package/dist/db/store.js +323 -0
  26. package/dist/db/tester.js +190 -0
  27. package/dist/db/validate.js +99 -0
  28. package/dist/environments.js +140 -0
  29. package/dist/explorer/alter.js +493 -0
  30. package/dist/explorer/catalog.js +414 -0
  31. package/dist/explorer/connect-entry.js +42 -0
  32. package/dist/explorer/cypher-parse.js +478 -0
  33. package/dist/explorer/cypher.js +963 -0
  34. package/dist/explorer/ddl.js +502 -0
  35. package/dist/explorer/dependents.js +222 -0
  36. package/dist/explorer/design-load.js +254 -0
  37. package/dist/explorer/design.js +45 -0
  38. package/dist/explorer/edit.js +108 -0
  39. package/dist/explorer/explain.js +37 -0
  40. package/dist/explorer/filter.js +121 -0
  41. package/dist/explorer/format.js +134 -0
  42. package/dist/explorer/graph.js +679 -0
  43. package/dist/explorer/gucs.js +31 -0
  44. package/dist/explorer/profile.js +102 -0
  45. package/dist/explorer/references.js +95 -0
  46. package/dist/explorer/rows.js +64 -0
  47. package/dist/explorer/schema-diff.js +360 -0
  48. package/dist/explorer/search.js +309 -0
  49. package/dist/explorer/security.js +152 -0
  50. package/dist/explorer/seed.js +1376 -0
  51. package/dist/explorer/session.js +122 -0
  52. package/dist/explorer/stats.js +135 -0
  53. package/dist/explorer/timeout.js +13 -0
  54. package/dist/prompts.js +52 -0
  55. package/dist/screens/environments.js +229 -0
  56. package/dist/screens/explorer.js +145 -0
  57. package/dist/screens/header.js +8 -0
  58. package/dist/screens/index.js +13 -0
  59. package/dist/screens/new-connection.js +178 -0
  60. package/dist/screens/panes/common.js +35 -0
  61. package/dist/screens/panes/history.js +36 -0
  62. package/dist/screens/panes/info.js +61 -0
  63. package/dist/screens/panes/query.js +113 -0
  64. package/dist/screens/panes/table.js +178 -0
  65. package/dist/screens/panes/tables.js +142 -0
  66. package/dist/screens/prompt-modal.js +18 -0
  67. package/dist/screens/record-view.js +42 -0
  68. package/dist/screens/result-grid.js +126 -0
  69. package/dist/screens/scroll.js +12 -0
  70. package/dist/screens/splash.js +78 -0
  71. package/dist/screens/terminal.js +25 -0
  72. package/dist/screens/types.js +1 -0
  73. package/dist/services.js +1 -0
  74. package/dist/startup.js +84 -0
  75. package/dist/web/server.js +988 -0
  76. package/package.json +69 -5
  77. package/web/dist/assets/anton-latin-400-normal-AUNGEG_V.woff +0 -0
  78. package/web/dist/assets/anton-latin-400-normal-Byf51wtH.woff2 +0 -0
  79. package/web/dist/assets/anton-latin-ext-400-normal-BMODBQc6.woff +0 -0
  80. package/web/dist/assets/anton-latin-ext-400-normal-SyiqE2Jt.woff2 +0 -0
  81. package/web/dist/assets/anton-vietnamese-400-normal-2FfR1wHA.woff +0 -0
  82. package/web/dist/assets/anton-vietnamese-400-normal-CkBxLiRJ.woff2 +0 -0
  83. package/web/dist/assets/antonio-latin-400-normal-B7iMKlzw.woff +0 -0
  84. package/web/dist/assets/antonio-latin-400-normal-CZjzwNAN.woff2 +0 -0
  85. package/web/dist/assets/antonio-latin-700-normal-BY3mCh47.woff +0 -0
  86. package/web/dist/assets/antonio-latin-700-normal-DSH-lEDG.woff2 +0 -0
  87. package/web/dist/assets/antonio-latin-ext-400-normal-nE82dizb.woff2 +0 -0
  88. package/web/dist/assets/antonio-latin-ext-400-normal-qA9Jb92z.woff +0 -0
  89. package/web/dist/assets/antonio-latin-ext-700-normal-BdSkr58W.woff +0 -0
  90. package/web/dist/assets/antonio-latin-ext-700-normal-D-dvgqjr.woff2 +0 -0
  91. package/web/dist/assets/button-ClA70PLR.js +1 -0
  92. package/web/dist/assets/code-xml-CXkpHeU9.js +1 -0
  93. package/web/dist/assets/compare-view-DKl5l_Jw.js +2 -0
  94. package/web/dist/assets/copy-CCXzeW-s.js +1 -0
  95. package/web/dist/assets/cypher-view-Pd0WFH8F.js +13 -0
  96. package/web/dist/assets/design-view-CIHSmH4u.js +9 -0
  97. package/web/dist/assets/erd-view-CV7mFGAN.js +1 -0
  98. package/web/dist/assets/format-sql-BLj5_RDZ.js +5 -0
  99. package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  100. package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  101. package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  102. package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  103. package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  104. package/web/dist/assets/graph-force-yBLyErpS.js +1 -0
  105. package/web/dist/assets/graph-link-OCo_s0zO.js +1 -0
  106. package/web/dist/assets/graph-view-sM1OlByf.js +3 -0
  107. package/web/dist/assets/index-CDcnUacl.js +10 -0
  108. package/web/dist/assets/index-OzJqnJWG.css +2 -0
  109. package/web/dist/assets/key-round-Cv_7QeED.js +1 -0
  110. package/web/dist/assets/loader-circle-BK_GMdvN.js +1 -0
  111. package/web/dist/assets/orbitron-latin-700-normal-4jsRXGGJ.woff2 +0 -0
  112. package/web/dist/assets/orbitron-latin-700-normal-DmYOh-W9.woff +0 -0
  113. package/web/dist/assets/orbitron-latin-900-normal-BbJatbhp.woff +0 -0
  114. package/web/dist/assets/orbitron-latin-900-normal-DrIi7unX.woff2 +0 -0
  115. package/web/dist/assets/query-builder-view-ChNFb51g.js +4 -0
  116. package/web/dist/assets/rotate-ccw-CdCaf7Q6.js +1 -0
  117. package/web/dist/assets/route-DO6IZ7of.js +15 -0
  118. package/web/dist/assets/seed-view-DFNB8WHE.js +7 -0
  119. package/web/dist/assets/share-tech-mono-latin-400-normal-15MgdvEM.woff +0 -0
  120. package/web/dist/assets/share-tech-mono-latin-400-normal-CCqdbj5z.woff2 +0 -0
  121. package/web/dist/assets/shield-alert-CMS-3j4U.js +1 -0
  122. package/web/dist/assets/sliders-horizontal-Dg51yc-C.js +1 -0
  123. package/web/dist/assets/sql-tabs-B4jt6bQj.js +2 -0
  124. package/web/dist/assets/table-DfMz2f8M.js +1 -0
  125. package/web/dist/assets/triangle-alert-COGnHCD1.js +1 -0
  126. package/web/dist/assets/zone-D9dXk8tn.js +5 -0
  127. package/web/dist/index.html +26 -0
@@ -0,0 +1,388 @@
1
+ // The commands that work without the TUI: list environments, look at what is in a database, page
2
+ // through a table and run SQL. Each prints its result on stdout (as a table, CSV or JSON) and
3
+ // returns the exit code; what went wrong goes to stderr.
4
+ import { addressOf, findEnvironment, listEnvironments, } from '../environments.js';
5
+ import { describeTable, listSchemas, listTables, parseTableRef, serverInfo, tableLabel, } from '../explorer/catalog.js';
6
+ import { commandSummary, formatBytes, formatDuration, formatEstimate, formatResult, outputFormats, } from '../explorer/format.js';
7
+ import { browseRows, DEFAULT_PAGE_SIZE } from '../explorer/rows.js';
8
+ import { compareSchemas } from '../explorer/schema-diff.js';
9
+ import { DEFAULT_MAX_ROWS, describeQueryError, } from '../explorer/session.js';
10
+ import { connectEntry } from '../explorer/connect-entry.js';
11
+ export const commandNames = [
12
+ 'envs',
13
+ 'schemas',
14
+ 'tables',
15
+ 'describe',
16
+ 'rows',
17
+ 'query',
18
+ 'info',
19
+ 'history',
20
+ 'diff',
21
+ ];
22
+ export const isCommand = (name) => commandNames.includes(name);
23
+ /** Thrown for a wrong command line: exit code 2. */
24
+ class UsageError extends Error {
25
+ }
26
+ const messageOf = (error) => (error instanceof Error ? error.message : String(error));
27
+ // A listing of the explorer's own (not a query's), where nothing is shown for a missing value.
28
+ const result = (columns, rows) => ({
29
+ command: 'SELECT',
30
+ columns: columns.map((name) => ({ name, type: 'text' })),
31
+ rows: rows.map((row) => row.map((value) => value ?? '')),
32
+ rowCount: rows.length,
33
+ truncated: false,
34
+ durationMs: 0,
35
+ });
36
+ const text = (value) => value === null || value === undefined ? null : String(value);
37
+ function formatOf(flags) {
38
+ if (flags.json)
39
+ return 'json';
40
+ const format = flags.format ?? 'table';
41
+ if (!outputFormats.includes(format)) {
42
+ throw new UsageError(`--format must be one of: ${outputFormats.join(', ')}`);
43
+ }
44
+ return format;
45
+ }
46
+ function environmentFor(options, env = options.flags.env) {
47
+ if (!env)
48
+ throw new UsageError(`${options.command} needs --env <name>`);
49
+ const entry = findEnvironment(options.services, env);
50
+ if (entry)
51
+ return entry;
52
+ const names = listEnvironments(options.services).map((candidate) => candidate.name);
53
+ throw new Error(`no environment named "${env}"${names.length > 0 ? `; there are: ${names.join(', ')}` : ''}`);
54
+ }
55
+ /** Looks up the environment's settings (Vault, prompts, ...) and connects. */
56
+ function connect(options, entry) {
57
+ const { timeout } = options.flags;
58
+ return connectEntry(entry, options.services, {
59
+ ...(timeout === undefined ? {} : { statementTimeoutMs: timeout === 0 ? null : timeout * 1000 }),
60
+ });
61
+ }
62
+ async function withSession(options, work) {
63
+ const entry = environmentFor(options);
64
+ const session = await connect(options, entry);
65
+ try {
66
+ return await work(session, entry);
67
+ }
68
+ finally {
69
+ await session.close();
70
+ }
71
+ }
72
+ function envs(options) {
73
+ const format = formatOf(options.flags);
74
+ const entries = listEnvironments(options.services);
75
+ if (format === 'json') {
76
+ options.out(`${JSON.stringify(entries.map((entry) => ({
77
+ name: entry.name,
78
+ source: entry.source,
79
+ tier: entry.tier,
80
+ protected: entry.protected,
81
+ readOnly: entry.readOnly,
82
+ address: addressOf(entry),
83
+ tls: entry.tlsMode,
84
+ })), null, 2)}\n`);
85
+ }
86
+ else {
87
+ options.out(formatResult(result(['name', 'source', 'tier', 'access', 'address'], entries.map((entry) => [
88
+ entry.name,
89
+ entry.source,
90
+ entry.tier,
91
+ entry.readOnly ? 'read only' : 'read write',
92
+ addressOf(entry),
93
+ ])), format));
94
+ }
95
+ const error = options.services.configError;
96
+ if (error)
97
+ options.err(`psql-explorer: ${error.message}\n`);
98
+ return 0;
99
+ }
100
+ const json = (options, value) => {
101
+ options.out(`${JSON.stringify(value, null, 2)}\n`);
102
+ };
103
+ async function schemas(options, session) {
104
+ const format = formatOf(options.flags);
105
+ const found = await listSchemas(session);
106
+ if (format === 'json')
107
+ json(options, found);
108
+ else {
109
+ options.out(formatResult(result(['name', 'owner', 'tables'], found.map((schema) => [schema.name, schema.owner, String(schema.tables)])), format));
110
+ }
111
+ return 0;
112
+ }
113
+ async function tables(options, session) {
114
+ const format = formatOf(options.flags);
115
+ const schema = options.flags.schema ?? session.params.schema;
116
+ const found = await listTables(session, schema);
117
+ if (format === 'json')
118
+ json(options, found);
119
+ else {
120
+ options.out(formatResult(result(['name', 'kind', 'rows', 'size', 'comment'], found.map((table) => [
121
+ table.name,
122
+ table.kind,
123
+ format === 'table' ? formatEstimate(table.estimatedRows) : text(table.estimatedRows),
124
+ format === 'table' ? formatBytes(table.totalBytes) : text(table.totalBytes),
125
+ table.comment,
126
+ ])), format));
127
+ }
128
+ if (found.length === 0)
129
+ options.err(`psql-explorer: schema ${schema} has no tables\n`);
130
+ return 0;
131
+ }
132
+ const tableArgument = (options, session) => {
133
+ const [name] = options.args;
134
+ if (!name)
135
+ throw new UsageError(`${options.command} needs a table, e.g. users or app.users`);
136
+ return parseTableRef(name, options.flags.schema ?? session.params.schema);
137
+ };
138
+ async function describe(options, session) {
139
+ const format = formatOf(options.flags);
140
+ const details = await describeTable(session, tableArgument(options, session));
141
+ if (format === 'json') {
142
+ json(options, details);
143
+ return 0;
144
+ }
145
+ const columns = result(['column', 'type', 'nullable', 'default', 'key', 'comment'], details.columns.map((column) => [
146
+ column.name,
147
+ column.type,
148
+ column.nullable ? 'yes' : 'no',
149
+ column.default,
150
+ column.primaryKey ? 'PK' : null,
151
+ column.comment,
152
+ ]));
153
+ const indexes = result(['index', 'definition', 'size'], details.indexes.map((index) => [index.name, index.definition, formatBytes(index.bytes)]));
154
+ const constraints = result(['constraint', 'kind', 'definition'], details.constraints.map((constraint) => [
155
+ constraint.name,
156
+ constraint.kind,
157
+ constraint.definition,
158
+ ]));
159
+ if (format === 'csv') {
160
+ options.out(formatResult(columns, 'csv'));
161
+ return 0;
162
+ }
163
+ options.out(`Table ${tableLabel(details.table)}\n\n${formatResult(columns, 'table')}`);
164
+ if (details.indexes.length > 0)
165
+ options.out(`\nIndexes\n${formatResult(indexes, 'table')}`);
166
+ if (details.constraints.length > 0) {
167
+ options.out(`\nConstraints\n${formatResult(constraints, 'table')}`);
168
+ }
169
+ return 0;
170
+ }
171
+ const orderOf = (flags) => flags.order
172
+ ? flags.order
173
+ .split(',')
174
+ .map((column) => ({ column: column.trim(), descending: flags.desc ?? false }))
175
+ : [];
176
+ async function rows(options, session) {
177
+ const format = formatOf(options.flags);
178
+ const table = tableArgument(options, session);
179
+ const limit = options.flags.limit ?? DEFAULT_PAGE_SIZE;
180
+ if (!Number.isInteger(limit) || limit < 1)
181
+ throw new UsageError('--limit must be at least 1');
182
+ const offset = options.flags.offset ?? 0;
183
+ if (!Number.isInteger(offset) || offset < 0)
184
+ throw new UsageError('--offset must be 0 or more');
185
+ const page = await browseRows(session, table, { limit, offset, orderBy: orderOf(options.flags) });
186
+ options.out(formatResult(page, format));
187
+ return 0;
188
+ }
189
+ async function sqlOf(options) {
190
+ const [given] = options.args;
191
+ if (given !== undefined && given !== '-')
192
+ return given;
193
+ if (given === undefined && options.stdinIsTTY) {
194
+ throw new UsageError('query needs SQL: query --env <name> "<sql>", or pipe it in');
195
+ }
196
+ return options.readStdin();
197
+ }
198
+ async function query(options, session, entry, sql) {
199
+ const format = formatOf(options.flags);
200
+ const maxRows = options.flags.maxRows ?? DEFAULT_MAX_ROWS;
201
+ const started = performance.now();
202
+ let ran;
203
+ try {
204
+ ran = await session.run(sql, [], { maxRows });
205
+ }
206
+ catch (error) {
207
+ const message = describeQueryError(error);
208
+ options.services.store.recordQuery({
209
+ envName: entry.name,
210
+ sql,
211
+ durationMs: performance.now() - started,
212
+ error: message,
213
+ });
214
+ options.err(`psql-explorer: ${message}\n`);
215
+ return 1;
216
+ }
217
+ options.services.store.recordQuery({
218
+ envName: entry.name,
219
+ sql,
220
+ durationMs: ran.durationMs,
221
+ rowCount: ran.rowCount ?? ran.rows.length,
222
+ });
223
+ if (ran.columns.length > 0)
224
+ options.out(formatResult(ran, format));
225
+ else if (format === 'json')
226
+ json(options, { command: ran.command, rowCount: ran.rowCount });
227
+ else if (format === 'table')
228
+ options.out(`${commandSummary(ran)}\n`);
229
+ if (ran.truncated) {
230
+ options.err(`psql-explorer: showing the first ${ran.rows.length} rows; raise --max-rows for more\n`);
231
+ }
232
+ return 0;
233
+ }
234
+ async function info(options, session, entry) {
235
+ const format = formatOf(options.flags);
236
+ const server = await serverInfo(session);
237
+ if (format === 'json') {
238
+ json(options, { environment: entry.name, ...server });
239
+ return 0;
240
+ }
241
+ options.out(formatResult(result(['setting', 'value'], [
242
+ ['environment', entry.name],
243
+ ['server', server.version],
244
+ ['database', server.database],
245
+ ['user', server.user],
246
+ ['size', formatBytes(server.databaseBytes)],
247
+ ['started', server.startedAt],
248
+ ['read only', server.readOnly ? 'yes' : 'no'],
249
+ ['time zone', server.timeZone],
250
+ ['search path', server.searchPath],
251
+ ['encoding', server.encoding],
252
+ ]), format));
253
+ return 0;
254
+ }
255
+ const indent = (text, prefix) => text
256
+ .split('\n')
257
+ .map((line) => `${prefix}${line}`)
258
+ .join('\n');
259
+ /** One object that differs, as text: its parts with each side's value, or a line by line diff. */
260
+ function describeChange(change, a, b) {
261
+ const name = change.kind === 'schema' || change.kind === 'extension'
262
+ ? change.name
263
+ : `${change.schema}.${change.name}`;
264
+ const status = change.status === 'only-a'
265
+ ? `only in ${a}`
266
+ : change.status === 'only-b'
267
+ ? `only in ${b}`
268
+ : 'differs';
269
+ const out = [`${change.kind} ${name}: ${status}`];
270
+ const width = Math.max(a.length, b.length) + 1;
271
+ for (const part of change.parts) {
272
+ const value = part.a ?? part.b ?? '';
273
+ const label = change.status === 'changed' && (part.a === null || part.b === null)
274
+ ? `${part.part}, only in ${part.a === null ? b : a}`
275
+ : part.part;
276
+ if (change.status !== 'changed' || part.a === null || part.b === null) {
277
+ out.push(value.includes('\n') ? ` ${label}:\n${indent(value, ' ')}` : ` ${label}: ${value}`);
278
+ }
279
+ else if (part.lines) {
280
+ out.push(` ${part.part} (- ${a}, + ${b})`);
281
+ for (const line of part.lines) {
282
+ out.push(` ${line.side === 'a' ? '-' : line.side === 'b' ? '+' : ' '} ${line.text}`);
283
+ }
284
+ }
285
+ else {
286
+ out.push(` ${part.part}`, ` ${`${a}:`.padEnd(width)} ${part.a}`, ` ${`${b}:`.padEnd(width)} ${part.b}`);
287
+ }
288
+ }
289
+ return out.join('\n');
290
+ }
291
+ async function diff(options) {
292
+ const format = formatOf(options.flags);
293
+ const entry = environmentFor(options);
294
+ if (!options.flags.with)
295
+ throw new UsageError('diff needs --with <name>, the environment to compare with');
296
+ const other = environmentFor(options, options.flags.with);
297
+ const { schema, withSchema } = options.flags;
298
+ const session = await connect(options, entry);
299
+ let found;
300
+ try {
301
+ const otherSession = await connect(options, other);
302
+ try {
303
+ found = await compareSchemas(session, otherSession, { schema, withSchema });
304
+ }
305
+ finally {
306
+ await otherSession.close();
307
+ }
308
+ }
309
+ finally {
310
+ await session.close();
311
+ }
312
+ if (format === 'json') {
313
+ json(options, { a: entry.name, b: other.name, ...found });
314
+ return 0;
315
+ }
316
+ if (format === 'csv') {
317
+ options.out(formatResult(result(['schema', 'kind', 'name', 'status', 'part', entry.name, other.name], found.changes.flatMap((change) => (change.parts.length > 0 ? change.parts : [{ part: null, a: null, b: null }]).map((part) => [
318
+ change.schema,
319
+ change.kind,
320
+ change.name,
321
+ change.status,
322
+ part.part,
323
+ part.a,
324
+ part.b,
325
+ ]))), 'csv'));
326
+ return 0;
327
+ }
328
+ for (const change of found.changes)
329
+ options.out(`${describeChange(change, entry.name, other.name)}\n\n`);
330
+ const differ = found.changes.length;
331
+ options.out(differ === 0
332
+ ? `${entry.name} and ${other.name} are the same (${found.same} objects).\n`
333
+ : `${differ} ${differ === 1 ? 'object differs' : 'objects differ'}, ${found.same} the same.\n`);
334
+ return 0;
335
+ }
336
+ function history(options) {
337
+ const format = formatOf(options.flags);
338
+ const entry = environmentFor(options);
339
+ const records = options.services.store.historyFor(entry.name, options.flags.limit ?? 50);
340
+ if (format === 'json')
341
+ json(options, records);
342
+ else {
343
+ options.out(formatResult(result(['at', 'duration', 'rows', 'sql', 'error'], records.map((record) => [
344
+ record.at.slice(0, 19).replace('T', ' '),
345
+ formatDuration(record.durationMs),
346
+ text(record.rowCount),
347
+ record.sql,
348
+ record.error,
349
+ ])), format));
350
+ }
351
+ return 0;
352
+ }
353
+ /** Runs one command; never throws. 0 done, 1 failed, 2 wrong command line. */
354
+ export async function runCommand(options) {
355
+ try {
356
+ switch (options.command) {
357
+ case 'envs':
358
+ return envs(options);
359
+ case 'history':
360
+ return history(options);
361
+ case 'schemas':
362
+ return await withSession(options, (session) => schemas(options, session));
363
+ case 'tables':
364
+ return await withSession(options, (session) => tables(options, session));
365
+ case 'describe':
366
+ return await withSession(options, (session) => describe(options, session));
367
+ case 'rows':
368
+ return await withSession(options, (session) => rows(options, session));
369
+ case 'query': {
370
+ // Read before connecting, so a wrong command line costs no login.
371
+ formatOf(options.flags);
372
+ const sql = (await sqlOf(options)).trim();
373
+ if (!sql)
374
+ throw new UsageError('the SQL is empty');
375
+ return await withSession(options, (session, entry) => query(options, session, entry, sql));
376
+ }
377
+ case 'info':
378
+ return await withSession(options, (session, entry) => info(options, session, entry));
379
+ case 'diff':
380
+ return await diff(options);
381
+ }
382
+ }
383
+ catch (error) {
384
+ const message = error instanceof UsageError ? error.message : describeQueryError(error);
385
+ options.err(`psql-explorer: ${message || messageOf(error)}\n`);
386
+ return error instanceof UsageError ? 2 : 1;
387
+ }
388
+ }
@@ -0,0 +1,68 @@
1
+ import { ResolveError } from '../config/resolvers/index.js';
2
+ /**
3
+ * Asks on the terminal, for commands run by a person: the question goes to stderr so stdout stays
4
+ * the command's output, and a secret is read without echo. One question at a time.
5
+ */
6
+ export function createTerminalPrompter(input = process.stdin, output = process.stderr) {
7
+ let queue = Promise.resolve();
8
+ const ask = ({ envName, field, masked, challenge, signal }) => new Promise((resolve, reject) => {
9
+ let answer = '';
10
+ const wasRaw = input.isRaw;
11
+ const finish = (error) => {
12
+ input.off('data', onData);
13
+ signal?.removeEventListener('abort', onAbort);
14
+ input.setRawMode?.(wasRaw);
15
+ input.pause();
16
+ output.write('\n');
17
+ if (error)
18
+ reject(error);
19
+ else
20
+ resolve(answer);
21
+ };
22
+ const onAbort = () => {
23
+ finish(new ResolveError(envName, field, 'cancelled'));
24
+ };
25
+ const onData = (chunk) => {
26
+ for (const char of chunk.toString('utf8')) {
27
+ if (char === '\r' || char === '\n') {
28
+ if (answer)
29
+ finish();
30
+ return;
31
+ }
32
+ if (char === '\u0003' || char === '\u001b') {
33
+ finish(new ResolveError(envName, field, 'cancelled'));
34
+ return;
35
+ }
36
+ if (char === '\u007f' || char === '\b') {
37
+ if (answer) {
38
+ answer = [...answer].slice(0, -1).join('');
39
+ output.write('\b \b');
40
+ }
41
+ continue;
42
+ }
43
+ if (char < ' ')
44
+ continue;
45
+ answer += char;
46
+ output.write(masked ? '*' : char);
47
+ }
48
+ };
49
+ if (signal?.aborted) {
50
+ reject(new ResolveError(envName, field, 'cancelled'));
51
+ return;
52
+ }
53
+ output.write(challenge === undefined
54
+ ? `${envName}.${field} (${masked ? 'secret' : 'value'}): `
55
+ : `${envName} is protected. Type "${challenge}" to continue: `);
56
+ input.setRawMode?.(true);
57
+ input.resume();
58
+ input.on('data', onData);
59
+ signal?.addEventListener('abort', onAbort);
60
+ });
61
+ return {
62
+ ask(request) {
63
+ const next = queue.then(() => ask(request), () => ask(request));
64
+ queue = next.catch(() => undefined);
65
+ return next;
66
+ },
67
+ };
68
+ }