neonctl 2.23.1 → 2.24.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.
@@ -16,28 +16,28 @@ import { formatNumericLocale } from './units.js';
16
16
  // East-Asian wide / fullwidth ranges from Unicode Standard Annex #11.
17
17
  // Trimmed to the ranges psql's `ucs_wcwidth` considers width 2.
18
18
  const WIDE_RANGES = [
19
- [0x1100, 0x115f],
20
- [0x2329, 0x232a],
21
- [0x2e80, 0x303e],
22
- [0x3041, 0x33ff],
23
- [0x3400, 0x4dbf],
24
- [0x4e00, 0x9fff],
25
- [0xa000, 0xa4cf],
26
- [0xac00, 0xd7a3],
27
- [0xf900, 0xfaff],
28
- [0xfe10, 0xfe19],
29
- [0xfe30, 0xfe6f],
30
- [0xff00, 0xff60],
31
- [0xffe0, 0xffe6],
32
- [0x1f300, 0x1f64f],
33
- [0x1f900, 0x1f9ff],
34
- [0x20000, 0x2fffd],
19
+ [0x1100, 0x115f], // Hangul Jamo
20
+ [0x2329, 0x232a], // Angle brackets
21
+ [0x2e80, 0x303e], // CJK Radicals, Kangxi, ...
22
+ [0x3041, 0x33ff], // Hiragana, Katakana, Bopomofo, etc.
23
+ [0x3400, 0x4dbf], // CJK Unified Ideographs Extension A
24
+ [0x4e00, 0x9fff], // CJK Unified Ideographs
25
+ [0xa000, 0xa4cf], // Yi Syllables
26
+ [0xac00, 0xd7a3], // Hangul Syllables
27
+ [0xf900, 0xfaff], // CJK Compatibility Ideographs
28
+ [0xfe10, 0xfe19], // Vertical forms
29
+ [0xfe30, 0xfe6f], // CJK Compatibility Forms, Small Form Variants
30
+ [0xff00, 0xff60], // Fullwidth Forms
31
+ [0xffe0, 0xffe6], // Fullwidth signs
32
+ [0x1f300, 0x1f64f], // Misc Symbols & Pictographs, Emoticons
33
+ [0x1f900, 0x1f9ff], // Supplemental Symbols & Pictographs
34
+ [0x20000, 0x2fffd], // CJK Extension B..F
35
35
  [0x30000, 0x3fffd], // CJK Extension G
36
36
  ];
37
37
  // Combining marks: width 0. From Unicode general categories Mn/Me/Cf
38
38
  // and the zero-width controls upstream treats as width 0.
39
39
  const ZERO_RANGES = [
40
- [0x0300, 0x036f],
40
+ [0x0300, 0x036f], // Combining Diacritical Marks
41
41
  [0x0483, 0x0489],
42
42
  [0x0591, 0x05bd],
43
43
  [0x05bf, 0x05bf],
@@ -190,7 +190,7 @@ const ZERO_RANGES = [
190
190
  [0x1cf8, 0x1cf9],
191
191
  [0x1dc0, 0x1df9],
192
192
  [0x1dfb, 0x1dff],
193
- [0x200b, 0x200f],
193
+ [0x200b, 0x200f], // zero-width space, ZWNJ, ZWJ, LRM, RLM
194
194
  [0x202a, 0x202e],
195
195
  [0x2060, 0x206f],
196
196
  [0x20d0, 0x20f0],
@@ -233,9 +233,9 @@ const ZERO_RANGES = [
233
233
  [0xabe8, 0xabe8],
234
234
  [0xabed, 0xabed],
235
235
  [0xfb1e, 0xfb1e],
236
- [0xfe00, 0xfe0f],
237
- [0xfe20, 0xfe2f],
238
- [0xfeff, 0xfeff],
236
+ [0xfe00, 0xfe0f], // variation selectors
237
+ [0xfe20, 0xfe2f], // combining half-marks
238
+ [0xfeff, 0xfeff], // BOM
239
239
  [0xfff9, 0xfffb],
240
240
  [0x101fd, 0x101fd],
241
241
  [0x102e0, 0x102e0],
@@ -342,15 +342,15 @@ export const padToWidth = (text, width, alignment) => {
342
342
  // divergences — we previously right-aligned interval & pg_lsn and omitted
343
343
  // xid & cid).
344
344
  const RIGHT_ALIGNED_OIDS = new Set([
345
- 20,
346
- 21,
347
- 23,
348
- 26,
349
- 28,
350
- 29,
351
- 700,
352
- 701,
353
- 790,
345
+ 20, // int8
346
+ 21, // int2
347
+ 23, // int4
348
+ 26, // oid
349
+ 28, // xid
350
+ 29, // cid
351
+ 700, // float4
352
+ 701, // float8
353
+ 790, // money (cash)
354
354
  1700, // numeric
355
355
  ]);
356
356
  const isRightAlignedField = (oid) => RIGHT_ALIGNED_OIDS.has(oid);
@@ -22,11 +22,11 @@
22
22
  // PostgreSQL type OIDs for the numeric family that map cleanly to
23
23
  // JSON numbers. NUMERIC is included but guarded by isFinite().
24
24
  const NUMERIC_TYPE_OIDS = new Set([
25
- 21,
26
- 23,
27
- 20,
28
- 700,
29
- 701,
25
+ 21, // INT2
26
+ 23, // INT4
27
+ 20, // INT8
28
+ 700, // FLOAT4
29
+ 701, // FLOAT8
30
30
  1700, // NUMERIC
31
31
  ]);
32
32
  export const jsonPrinter = {
@@ -142,10 +142,10 @@ const cloneState = (s) => ({
142
142
  // blocks, dollar-quoted bodies). Identifier letters are stored lower-cased.
143
143
  // ---------------------------------------------------------------------------
144
144
  const KEYWORD_PREFIX_LETTERS = new Set([
145
- 'c',
146
- 'f',
147
- 'p',
148
- 'o',
145
+ 'c', // create
146
+ 'f', // function
147
+ 'p', // procedure
148
+ 'o', // or
149
149
  'r', // replace
150
150
  ]);
151
151
  const PREFIX_MATCHES_CREATE_FN_OR_PROC = (letters) => {
package/storage_api.js ADDED
@@ -0,0 +1,114 @@
1
+ // Typed client helpers for the branch object-storage (bucket/object) API.
2
+ //
3
+ // These endpoints are part of the Neon object-storage surface (the "Buckets"
4
+ // tag in the public API). They are not yet exposed as typed methods on the
5
+ // published `@neondatabase/api-client` package, so the request/response types
6
+ // and the thin call helpers live here. They are implemented on top of the
7
+ // api-client's public `request()` method, which means they reuse the exact
8
+ // same authentication, base URL, headers and retry behaviour as every other
9
+ // neonctl command. When the generated client gains these methods, the call
10
+ // sites in `src/commands/bucket.ts` can switch over with no behavioural
11
+ // change.
12
+ const bucketsPath = (projectId, branchId) => `/projects/${encodeURIComponent(projectId)}/branches/${encodeURIComponent(branchId)}/buckets`;
13
+ const bucketPath = (projectId, branchId, bucketName) => `${bucketsPath(projectId, branchId)}/${encodeURIComponent(bucketName)}`;
14
+ /**
15
+ * Create a bucket on a branch.
16
+ *
17
+ * @request POST /projects/{project_id}/branches/{branch_id}/buckets
18
+ */
19
+ export const createProjectBranchBucket = (apiClient, { projectId, branchId, name, accessLevel, }) => {
20
+ const body = { name };
21
+ // Omit access_level entirely so the server default (`private`) applies.
22
+ if (accessLevel !== undefined) {
23
+ body.access_level = accessLevel;
24
+ }
25
+ return apiClient.request({
26
+ path: bucketsPath(projectId, branchId),
27
+ method: 'POST',
28
+ body,
29
+ format: 'json',
30
+ secure: true,
31
+ });
32
+ };
33
+ /**
34
+ * List the buckets on a branch.
35
+ *
36
+ * @request GET /projects/{project_id}/branches/{branch_id}/buckets
37
+ */
38
+ export const listProjectBranchBuckets = (apiClient, { projectId, branchId }) => apiClient.request({
39
+ path: bucketsPath(projectId, branchId),
40
+ method: 'GET',
41
+ format: 'json',
42
+ secure: true,
43
+ });
44
+ /**
45
+ * Delete a bucket from a branch.
46
+ *
47
+ * @request DELETE /projects/{project_id}/branches/{branch_id}/buckets/{bucket_name}
48
+ */
49
+ export const deleteProjectBranchBucket = (apiClient, { projectId, branchId, bucketName, }) => apiClient.request({
50
+ path: bucketPath(projectId, branchId, bucketName),
51
+ method: 'DELETE',
52
+ secure: true,
53
+ });
54
+ /**
55
+ * List objects (and collapsed folders) in a bucket on a branch.
56
+ *
57
+ * @request GET /projects/{project_id}/branches/{branch_id}/buckets/{bucket_name}/objects
58
+ */
59
+ export const listProjectBranchBucketObjects = (apiClient, { projectId, branchId, bucketName, ...query }) => apiClient.request({
60
+ path: `${bucketPath(projectId, branchId, bucketName)}/objects`,
61
+ method: 'GET',
62
+ query,
63
+ format: 'json',
64
+ secure: true,
65
+ });
66
+ /**
67
+ * Download an object's raw bytes from a bucket on a branch.
68
+ *
69
+ * The server returns the body as `application/octet-stream` with a
70
+ * `Content-Disposition: attachment` header; the helper requests the body as a
71
+ * stream (`responseType: 'stream'`), so `.data` is a Node `Readable` the caller
72
+ * can pipe straight to disk without buffering the whole object in memory. The
73
+ * response headers are returned alongside so the caller can derive a filename
74
+ * from `Content-Disposition`.
75
+ *
76
+ * The object key may contain `/`; it is percent-encoded into a single path
77
+ * segment so nested keys are routed to the `{object_key}` parameter.
78
+ *
79
+ * @request GET /projects/{project_id}/branches/{branch_id}/buckets/{bucket_name}/objects/{object_key}/download
80
+ */
81
+ export const getProjectBranchBucketObject = (apiClient, { projectId, branchId, bucketName, objectKey, }) => apiClient.request({
82
+ path: `${bucketPath(projectId, branchId, bucketName)}/objects/${encodeURIComponent(objectKey)}/download`,
83
+ method: 'GET',
84
+ format: 'stream',
85
+ secure: true,
86
+ });
87
+ /**
88
+ * Delete an object from a bucket on a branch.
89
+ *
90
+ * The object key may contain `/`; it is percent-encoded into a single path
91
+ * segment so nested keys are routed to the `{object_key}` parameter.
92
+ *
93
+ * @request DELETE /projects/{project_id}/branches/{branch_id}/buckets/{bucket_name}/objects/{object_key}
94
+ */
95
+ export const deleteProjectBranchBucketObject = (apiClient, { projectId, branchId, bucketName, objectKey, }) => apiClient.request({
96
+ path: `${bucketPath(projectId, branchId, bucketName)}/objects/${encodeURIComponent(objectKey)}`,
97
+ method: 'DELETE',
98
+ secure: true,
99
+ });
100
+ /**
101
+ * Delete every object under a key prefix (folder) in a bucket on a branch.
102
+ *
103
+ * `prefix` must be non-empty and end with `/`; every object on this branch
104
+ * whose key starts with the prefix is soft-deleted in a single call.
105
+ *
106
+ * @request DELETE /projects/{project_id}/branches/{branch_id}/buckets/{bucket_name}/objects-by-prefix
107
+ */
108
+ export const deleteProjectBranchBucketObjectsByPrefix = (apiClient, { projectId, branchId, bucketName, prefix, }) => apiClient.request({
109
+ path: `${bucketPath(projectId, branchId, bucketName)}/objects-by-prefix`,
110
+ method: 'DELETE',
111
+ query: { prefix },
112
+ format: 'json',
113
+ secure: true,
114
+ });
@@ -0,0 +1,88 @@
1
+ import { EndpointType } from '@neondatabase/api-client';
2
+ import prompts from 'prompts';
3
+ import { retryOnLock } from '../api.js';
4
+ import { log } from '../log.js';
5
+ import { isCi } from '../env.js';
6
+ /** Sentinel `value` for the "create a new branch" choice (no branch id can collide). */
7
+ const CREATE_BRANCH_CHOICE = Symbol('create-branch');
8
+ /**
9
+ * Prompt the user to pick a branch from `branches`, with a "+ Create a new branch…" option
10
+ * pinned to the top (mirroring the project/org pickers). The default selection is the
11
+ * project's default branch (the create option sits at index 0, so the default index is
12
+ * offset by one).
13
+ *
14
+ * Throws `opts.nonInteractiveMessage` when there is no TTY (or in CI): the caller knows the
15
+ * right guidance for its command, so the message is supplied rather than hard-coded here.
16
+ */
17
+ export const pickBranchInteractively = async (branches, opts) => {
18
+ if (isCi() || !process.stdout.isTTY) {
19
+ throw new Error(opts.nonInteractiveMessage);
20
+ }
21
+ const defaultBranchIndex = branches.findIndex((b) => b.default);
22
+ const initial = defaultBranchIndex >= 0 ? defaultBranchIndex + 1 : 0;
23
+ const { choice } = await prompts({
24
+ type: 'select',
25
+ name: 'choice',
26
+ message: opts.message,
27
+ choices: [
28
+ { title: '+ Create a new branch…', value: CREATE_BRANCH_CHOICE },
29
+ ...branches.map((b) => ({
30
+ title: `${b.default ? '✱ ' : ''}${b.name} (${b.id})`,
31
+ value: b.id,
32
+ })),
33
+ ],
34
+ initial,
35
+ });
36
+ if (choice === undefined) {
37
+ throw new Error('Aborted: no branch selected.');
38
+ }
39
+ if (choice === CREATE_BRANCH_CHOICE) {
40
+ return { kind: 'create', name: await promptNewBranchName(branches) };
41
+ }
42
+ return { kind: 'existing', branchId: choice };
43
+ };
44
+ /**
45
+ * Prompt for a new branch name, rejecting empty input and names already taken on the
46
+ * project (so we never silently select a different, pre-existing branch).
47
+ */
48
+ export const promptNewBranchName = async (branches) => {
49
+ const existing = new Set(branches.map((b) => b.name));
50
+ const { name } = await prompts({
51
+ type: 'text',
52
+ name: 'name',
53
+ message: 'New branch name:',
54
+ validate: (value) => {
55
+ const trimmed = value.trim();
56
+ if (trimmed === '')
57
+ return 'Branch name cannot be empty.';
58
+ if (existing.has(trimmed))
59
+ return `A branch named "${trimmed}" already exists.`;
60
+ return true;
61
+ },
62
+ });
63
+ const trimmed = typeof name === 'string' ? name.trim() : '';
64
+ if (trimmed === '') {
65
+ throw new Error('Aborted: no branch name provided.');
66
+ }
67
+ return trimmed;
68
+ };
69
+ /**
70
+ * Create a branch with the same defaults as `neonctl branch create --name <name>`:
71
+ * branched from the project's default branch with a read-write compute endpoint. Returns
72
+ * the new branch id.
73
+ */
74
+ export const createBranch = async (apiClient, projectId, name, branches) => {
75
+ const defaultBranch = branches.find((b) => b.default);
76
+ if (!defaultBranch) {
77
+ throw new Error('No default branch found');
78
+ }
79
+ const { data } = await retryOnLock(() => apiClient.createProjectBranch(projectId, {
80
+ branch: { name, parent_id: defaultBranch.id },
81
+ endpoints: [{ type: EndpointType.ReadWrite }],
82
+ }));
83
+ if (defaultBranch.protected) {
84
+ log.warning('The parent branch is protected; a unique role password has been generated for the new branch.');
85
+ }
86
+ log.info('Created branch %s (%s).', data.branch.name, data.branch.id);
87
+ return data.branch.id;
88
+ };
package/utils/esbuild.js CHANGED
@@ -48,7 +48,10 @@ const bundleViaModule = async (source, loadEsbuild) => {
48
48
  .build({
49
49
  entryPoints: [source],
50
50
  bundle: true,
51
- outfile: 'out.js',
51
+ // Emit `index.mjs` (not `out.js`): the Functions runtime imports the archive's entry
52
+ // by the conventional `index.{js,mjs}` name, and `.mjs` makes Node treat the ESM
53
+ // output as a module without needing a `package.json` type marker alongside it.
54
+ outfile: 'index.mjs',
52
55
  write: false,
53
56
  sourcemap: true,
54
57
  minify: true,
@@ -61,7 +64,7 @@ const bundleViaModule = async (source, loadEsbuild) => {
61
64
  throw new Error(`Failed to bundle function from ${source}. ${message(err)}`.trim());
62
65
  });
63
66
  const files = result.outputFiles ?? [];
64
- // write:false with one entry always yields out.js + out.js.map; an empty set
67
+ // write:false with one entry always yields index.mjs + index.mjs.map; an empty set
65
68
  // means the API contract changed under us — fail loud rather than ship an
66
69
  // empty archive.
67
70
  if (files.length === 0) {
@@ -105,7 +108,7 @@ const runEsbuild = (bin, args) => new Promise((resolve, reject) => {
105
108
  const bundleViaBinary = async (source) => {
106
109
  const bin = resolveEsbuild();
107
110
  const outDir = mkdtempSync(join(tmpdir(), 'neon-fn-bundle-'));
108
- const outfile = join(outDir, 'out.js');
111
+ const outfile = join(outDir, 'index.mjs');
109
112
  try {
110
113
  const { code, stderr } = await runEsbuild(bin, [
111
114
  source,
@@ -122,8 +125,8 @@ const bundleViaBinary = async (source) => {
122
125
  throw new Error(`Failed to bundle function from ${source}. ${stderr.trim()}`.trim());
123
126
  }
124
127
  return {
125
- 'out.js': new Uint8Array(readFileSync(outfile)),
126
- 'out.js.map': new Uint8Array(readFileSync(`${outfile}.map`)),
128
+ 'index.mjs': new Uint8Array(readFileSync(outfile)),
129
+ 'index.mjs.map': new Uint8Array(readFileSync(`${outfile}.map`)),
127
130
  };
128
131
  }
129
132
  finally {
package/utils/zip.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import { zipSync } from 'fflate';
2
- // Zip the esbuild output (out.js + out.js.map) into the archive the Functions
2
+ // Zip the esbuild output (index.mjs + index.mjs.map) into the archive the Functions
3
3
  // deploy endpoint expects. Compression level 6 matches the previous bundler.
4
4
  export const zipBundle = (entries) => zipSync(entries, { level: 6 });