@theholocron/holocron-plugin-neon 2.0.0-alpha.8 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ <!-- editorconfig-checker-disable-file -->
2
+
1
3
  # `@theholocron/holocron-plugin-neon`
2
4
 
3
5
  Neon plugin for [Holocron](../cli). Implements the `storage`
@@ -5,8 +7,10 @@ capability against [Neon's REST API](https://api-docs.neon.tech/reference/gettin
5
7
 
6
8
  ## Install
7
9
 
10
+ <!-- prettier-ignore -->
8
11
  ```bash
9
12
  pnpm add -D @theholocron/holocron-plugin-neon@alpha
13
+
10
14
  ```
11
15
 
12
16
  ## Auth
@@ -19,16 +23,18 @@ Token resolution order:
19
23
 
20
24
  ## Config
21
25
 
26
+ <!-- prettier-ignore -->
22
27
  ```jsonc
23
28
  {
24
- "providers": {
25
- "storage": ["neon", { "projectId": "ancient-resonance-…" }],
26
- },
29
+ "providers": {
30
+ "storage": ["neon", { "projectId": "ancient-resonance-…" }],
31
+ },
27
32
  }
33
+
28
34
  ```
29
35
 
30
36
  - `projectId` (required) — the Neon project id. The plugin binds to
31
- this project; every method operates within it.
37
+ this project; every method operates within it.
32
38
 
33
39
  ## What's implemented
34
40
 
package/dist/index.d.mts CHANGED
@@ -1,67 +1,19 @@
1
- import { ConnectionStringOptions, Storage, StorageBranch } from "@theholocron/cli";
1
+ import { AuthError, ConnectionStringOptions, ResolveTokenInput, Storage, StorageBranch } from "@theholocron/cli";
2
+ import { NeonClient, NeonClient as NeonClient$1, createNeonClient } from "@theholocron/neon-client";
2
3
 
3
4
  //#region src/auth.d.ts
4
- /**
5
- * Token resolution for the Neon plugin.
6
- *
7
- * Resolution order (matches the standard 4-step precedence set by
8
- * `.notes/tech-auth-bootstrap.spec.md`):
9
- * 1. explicit `cliToken` argument (from `--token` flag)
10
- * 2. HOLOCRON_NEON_API_KEY env var (preferred — explicit intent)
11
- * 3. NEON_API_KEY env var (the default Neon CLI reads)
12
- * 4. keyring (com.theholocron.cli / "neon")
13
- * 5. AuthError naming all four options + the bootstrap hint
14
- */
15
- declare class AuthError extends Error {
16
- name: string;
17
- }
18
- interface ResolveTokenInput {
19
- /** From `--token` CLI flag. */
20
- cliToken?: string;
21
- /** Env vars; passed in for testability. Defaults to `process.env`. */
22
- env?: NodeJS.ProcessEnv;
23
- /** Keyring lookup fn; passed in for testability. Defaults to `getToken(provider)`. */
24
- keyring?: (provider: string) => string | null;
25
- }
26
- declare function resolveToken(input?: ResolveTokenInput): string;
27
- //#endregion
28
- //#region src/rest.d.ts
29
- /**
30
- * Thin REST wrapper around console.neon.tech/api/v2.
31
- *
32
- * Same pattern as the GitHub + Vercel REST clients — bearer auth,
33
- * JSON-only bodies, transport-failure wrapping with status: 0 so the
34
- * orchestrator's soft-skip path sees a clear "Neon GET /path failed"
35
- * message instead of a generic `TypeError: fetch failed`.
36
- */
37
- interface RestClientOptions {
38
- token: string;
39
- fetch?: typeof fetch;
40
- baseUrl?: string;
41
- }
42
- interface RequestOptions {
43
- method?: "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
44
- body?: unknown;
45
- query?: Record<string, string>;
46
- }
47
- declare class NeonRestClient {
48
- private readonly token;
49
- private readonly fetchImpl;
50
- readonly baseUrl: string;
51
- constructor(opts: RestClientOptions);
52
- request<T>(path: string, opts?: RequestOptions): Promise<T>;
53
- }
5
+ declare const resolveToken: (input?: import("@theholocron/http-client").ResolveTokenInput) => string;
54
6
  //#endregion
55
7
  //#region src/capabilities/storage.d.ts
56
8
  interface StorageOptions {
57
9
  projectId: string;
58
10
  }
59
11
  declare class NeonStorage implements Storage {
60
- private readonly rest;
12
+ private readonly client;
13
+ private readonly opts;
61
14
  readonly key: "storage";
62
15
  readonly providerName = "neon";
63
- private readonly base;
64
- constructor(rest: NeonRestClient, opts: StorageOptions);
16
+ constructor(client: NeonClient$1, opts: StorageOptions);
65
17
  getConnectionString(scope: string, options?: ConnectionStringOptions): Promise<string>;
66
18
  listBranches(): Promise<StorageBranch[]>;
67
19
  createBranch(input: {
@@ -118,7 +70,7 @@ interface NeonPluginOptions extends ResolveTokenInput {
118
70
  }
119
71
  interface PluginContext {
120
72
  options: NeonPluginOptions;
121
- rest: NeonRestClient;
73
+ client: NeonClient;
122
74
  }
123
75
  declare function createContext(options: NeonPluginOptions): PluginContext;
124
76
  declare function storage(ctx: PluginContext): Storage;
@@ -136,4 +88,4 @@ declare function createPlugin(options: NeonPluginOptions): {
136
88
  */
137
89
  declare const AUTH_HINT: string;
138
90
  //#endregion
139
- export { AUTH_HINT, AuthError, NeonPluginOptions, NeonRestClient, NeonStorage, PluginContext, ResolveTokenInput, type VerifyTokenFailure, type VerifyTokenResult, type VerifyTokenSuccess, createContext, createPlugin, resolveToken, storage, verifyToken };
91
+ export { AUTH_HINT, AuthError, type NeonClient, NeonPluginOptions, NeonStorage, PluginContext, type ResolveTokenInput, type VerifyTokenFailure, type VerifyTokenResult, type VerifyTokenSuccess, createContext, createNeonClient, createPlugin, resolveToken, storage, verifyToken };
package/dist/index.mjs CHANGED
@@ -1,26 +1,12 @@
1
- import { ProviderApiError, getToken } from "@theholocron/cli";
1
+ import { AuthError, ProviderApiError, createResolveToken } from "@theholocron/cli";
2
+ import { createNeonClient } from "@theholocron/neon-client";
2
3
  //#region src/auth.ts
3
- /**
4
- * Token resolution for the Neon plugin.
5
- *
6
- * Resolution order (matches the standard 4-step precedence set by
7
- * `.notes/tech-auth-bootstrap.spec.md`):
8
- * 1. explicit `cliToken` argument (from `--token` flag)
9
- * 2. HOLOCRON_NEON_API_KEY env var (preferred — explicit intent)
10
- * 3. NEON_API_KEY env var (the default Neon CLI reads)
11
- * 4. keyring (com.theholocron.cli / "neon")
12
- * 5. AuthError naming all four options + the bootstrap hint
13
- */
14
- var AuthError = class extends Error {
15
- name = "AuthError";
16
- };
17
- function resolveToken(input = {}) {
18
- const env = input.env ?? process.env;
19
- const keyring = input.keyring ?? getToken;
20
- const token = input.cliToken || env["HOLOCRON_NEON_API_KEY"] || env["NEON_API_KEY"] || keyring("neon");
21
- if (!token) throw new AuthError("no Neon API key found. Pass --token <KEY>, set HOLOCRON_NEON_API_KEY / NEON_API_KEY, or run: holocron auth set neon <KEY>");
22
- return token;
23
- }
4
+ const resolveToken = createResolveToken({
5
+ envName: "HOLOCRON_NEON_API_KEY",
6
+ vendorEnvName: "NEON_API_KEY",
7
+ keyringService: "neon",
8
+ errorMessage: "no Neon API key found. Pass --token <KEY>, set HOLOCRON_NEON_API_KEY / NEON_API_KEY, or run: holocron auth set neon <KEY>"
9
+ });
24
10
  //#endregion
25
11
  //#region src/capabilities/storage.ts
26
12
  /**
@@ -42,55 +28,46 @@ function resolveToken(input = {}) {
42
28
  * via `projectId` in options.
43
29
  */
44
30
  var NeonStorage = class {
45
- rest;
31
+ client;
32
+ opts;
46
33
  key = "storage";
47
34
  providerName = "neon";
48
- base;
49
- constructor(rest, opts) {
50
- this.rest = rest;
35
+ constructor(client, opts) {
36
+ this.client = client;
37
+ this.opts = opts;
51
38
  if (!opts.projectId) throw new Error("NeonStorage requires `projectId` in options");
52
- this.base = `/projects/${opts.projectId}`;
53
39
  }
54
40
  async getConnectionString(scope, options = {}) {
55
41
  const db = await this.firstDatabase(scope);
56
- const params = {
42
+ const { uri } = await this.client.connection.uri(this.opts.projectId, {
57
43
  branch_id: scope,
58
44
  database_name: db.name,
59
45
  role_name: db.owner_name,
60
46
  pooled: options.pooled ? "true" : "false"
61
- };
62
- return (await this.rest.request(`${this.base}/connection_uri`, { query: params })).uri;
47
+ });
48
+ return uri;
63
49
  }
64
50
  async listBranches() {
65
- return (await this.rest.request(`${this.base}/branches`)).branches.map(mapBranch);
51
+ const { branches } = await this.client.branches.list(this.opts.projectId);
52
+ return branches.map(mapBranch);
66
53
  }
67
54
  async createBranch(input) {
68
- return mapBranch((await this.rest.request(`${this.base}/branches`, {
69
- method: "POST",
70
- body: {
71
- branch: {
72
- name: input.name,
73
- ...input.from ? { parent_id: input.from } : {}
74
- },
75
- endpoints: [{ type: "read_write" }]
76
- }
77
- })).branch);
55
+ const { branch } = await this.client.branches.create(this.opts.projectId, {
56
+ name: input.name,
57
+ ...input.from ? { parent_id: input.from } : {},
58
+ endpoints: [{ type: "read_write" }]
59
+ });
60
+ return mapBranch(branch);
78
61
  }
79
62
  async destroyBranch(branch) {
80
- await this.rest.request(`${this.base}/branches/${encodeURIComponent(branch)}`, { method: "DELETE" });
63
+ await this.client.branches.destroy(this.opts.projectId, branch);
81
64
  }
82
65
  async resetBranch(input) {
83
- await this.rest.request(`${this.base}/branches/${encodeURIComponent(input.branch)}/restore`, {
84
- method: "POST",
85
- body: { source_branch_id: input.from }
86
- });
66
+ await this.client.branches.restore(this.opts.projectId, input.branch, input.from);
87
67
  }
88
68
  async enableExtension(input) {
89
69
  const db = await this.firstDatabase(input.branch);
90
- await this.rest.request(`${this.base}/branches/${encodeURIComponent(input.branch)}/databases/${encodeURIComponent(db.name)}/run_sql`, {
91
- method: "POST",
92
- body: { query: `CREATE EXTENSION IF NOT EXISTS "${input.extension}"` }
93
- });
70
+ await this.client.databases.runSql(this.opts.projectId, input.branch, db.name, `CREATE EXTENSION IF NOT EXISTS "${input.extension}"`);
94
71
  }
95
72
  /**
96
73
  * The first database on a branch — the "default" for connection-string
@@ -98,7 +75,8 @@ var NeonStorage = class {
98
75
  * means it was created without an endpoint and needs initialization).
99
76
  */
100
77
  async firstDatabase(branchId) {
101
- const db = (await this.rest.request(`${this.base}/branches/${encodeURIComponent(branchId)}/databases`)).databases[0];
78
+ const { databases } = await this.client.databases.list(this.opts.projectId, branchId);
79
+ const db = databases[0];
102
80
  if (!db) throw new ProviderApiError(`Neon branch ${branchId} has no databases — initialize the branch first`, 404, void 0);
103
81
  return db;
104
82
  }
@@ -112,60 +90,6 @@ function mapBranch(raw) {
112
90
  };
113
91
  }
114
92
  //#endregion
115
- //#region src/rest.ts
116
- /**
117
- * Thin REST wrapper around console.neon.tech/api/v2.
118
- *
119
- * Same pattern as the GitHub + Vercel REST clients — bearer auth,
120
- * JSON-only bodies, transport-failure wrapping with status: 0 so the
121
- * orchestrator's soft-skip path sees a clear "Neon GET /path failed"
122
- * message instead of a generic `TypeError: fetch failed`.
123
- */
124
- var NeonRestClient = class {
125
- token;
126
- fetchImpl;
127
- baseUrl;
128
- constructor(opts) {
129
- this.token = opts.token;
130
- this.fetchImpl = opts.fetch ?? globalThis.fetch;
131
- let url = opts.baseUrl ?? "https://console.neon.tech/api/v2";
132
- while (url.endsWith("/")) url = url.slice(0, -1);
133
- this.baseUrl = url;
134
- }
135
- async request(path, opts = {}) {
136
- const url = new URL(`${this.baseUrl}${path.startsWith("/") ? path : "/" + path}`);
137
- for (const [k, v] of Object.entries(opts.query ?? {})) url.searchParams.set(k, v);
138
- const fullUrl = url.toString();
139
- const headers = {
140
- authorization: `Bearer ${this.token}`,
141
- accept: "application/json"
142
- };
143
- const init = {
144
- method: opts.method ?? "GET",
145
- headers
146
- };
147
- if (opts.body !== void 0) {
148
- headers["content-type"] = "application/json";
149
- init.body = JSON.stringify(opts.body);
150
- }
151
- let res;
152
- try {
153
- res = await this.fetchImpl(fullUrl, init);
154
- } catch (err) {
155
- const detail = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
156
- throw new ProviderApiError(`Neon ${init.method} ${path} failed: ${detail}`, 0, void 0);
157
- }
158
- if (!res.ok) {
159
- const body = await res.text().catch(() => "");
160
- throw new ProviderApiError(`Neon ${init.method} ${path} → ${res.status}`, res.status, body);
161
- }
162
- if (res.status === 204) return void 0;
163
- const text = await res.text();
164
- if (!text) return void 0;
165
- return JSON.parse(text);
166
- }
167
- };
168
- //#endregion
169
93
  //#region src/verify-token.ts
170
94
  /**
171
95
  * `verifyToken` — plugin-level export used by `holocron auth set` +
@@ -174,12 +98,13 @@ var NeonRestClient = class {
174
98
  * shape.
175
99
  */
176
100
  async function verifyToken(token, opts = {}) {
177
- const restOpts = { token };
178
- if (opts.baseUrl !== void 0) restOpts.baseUrl = opts.baseUrl;
179
- if (opts.fetch !== void 0) restOpts.fetch = opts.fetch;
180
- const rest = new NeonRestClient(restOpts);
101
+ const client = createNeonClient({
102
+ token,
103
+ baseUrl: opts.baseUrl,
104
+ fetch: opts.fetch
105
+ });
181
106
  try {
182
- const me = await rest.request("/users/me");
107
+ const me = await client.users.me();
183
108
  return {
184
109
  ok: true,
185
110
  subject: `user @ ${me?.email ?? me?.login ?? me?.name ?? me?.id ?? "unknown"}`
@@ -195,16 +120,17 @@ async function verifyToken(token, opts = {}) {
195
120
  //#region src/index.ts
196
121
  function createContext(options) {
197
122
  if (!options.projectId) throw new Error("@theholocron/holocron-plugin-neon requires `projectId` in options");
198
- const restOpts = { token: resolveToken(options) };
199
- if (options.baseUrl !== void 0) restOpts.baseUrl = options.baseUrl;
200
- if (options.fetch !== void 0) restOpts.fetch = options.fetch;
201
123
  return {
202
124
  options,
203
- rest: new NeonRestClient(restOpts)
125
+ client: createNeonClient({
126
+ token: resolveToken(options),
127
+ baseUrl: options.baseUrl,
128
+ fetch: options.fetch
129
+ })
204
130
  };
205
131
  }
206
132
  function storage(ctx) {
207
- return new NeonStorage(ctx.rest, { projectId: ctx.options.projectId });
133
+ return new NeonStorage(ctx.client, { projectId: ctx.options.projectId });
208
134
  }
209
135
  function createPlugin(options) {
210
136
  const ctx = createContext(options);
@@ -221,4 +147,4 @@ function createPlugin(options) {
221
147
  */
222
148
  const AUTH_HINT = "generate a Neon API key at https://console.neon.tech/app/settings/api-keys, then run: holocron auth set neon <KEY>";
223
149
  //#endregion
224
- export { AUTH_HINT, AuthError, NeonRestClient, NeonStorage, createContext, createPlugin, resolveToken, storage, verifyToken };
150
+ export { AUTH_HINT, AuthError, NeonStorage, createContext, createNeonClient, createPlugin, resolveToken, storage, verifyToken };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/holocron-plugin-neon",
3
- "version": "2.0.0-alpha.8",
3
+ "version": "2.0.0",
4
4
  "description": "Holocron plugin for Neon. Implements the storage capability against Neon's REST API — branch ops, connection strings, extensions.",
5
5
  "homepage": "https://github.com/theholocron/holocron/tree/main/packages/holocron-plugin-neon#readme",
6
6
  "bugs": "https://github.com/theholocron/holocron/issues",
@@ -21,19 +21,31 @@
21
21
  }
22
22
  },
23
23
  "peerDependencies": {
24
- "@theholocron/cli": "2.0.0-alpha.8"
24
+ "@theholocron/neon-client": "^1.1.0",
25
+ "@theholocron/cli": "2.0.0"
26
+ },
27
+ "peerDependenciesMeta": {
28
+ "@theholocron/neon-client": {
29
+ "optional": false
30
+ }
25
31
  },
26
32
  "devDependencies": {
27
- "@theholocron/tsconfig": "^4.1.0",
28
- "@tsconfig/node-lts": "^24.0.0",
29
- "@vitest/coverage-v8": "^3.2.6",
30
- "eslint": "^9.36.0",
31
- "globals": "^16.5.0",
32
- "typescript": "^5.9.3",
33
- "vitest": "^3.2.6",
33
+ "@theholocron/eslint-config": "^7.3.0",
34
+ "@theholocron/neon-client": "^1.1.0",
35
+ "@theholocron/tsconfig": "^7.3.0",
36
+ "@theholocron/tsdown-config": "^7.3.0",
37
+ "@theholocron/vitest-config": "^7.3.0",
38
+ "@types/node": "^26",
39
+ "@vitest/coverage-v8": "^4.1.10",
40
+ "@vitest/eslint-plugin": "^1.6.23",
41
+ "eslint": "^10.7.0",
42
+ "eslint-plugin-n": "^18.2.2",
43
+ "globals": "^17.7.0",
34
44
  "tsdown": "^0.22.3",
35
45
  "tsx": "^4.22.4",
36
- "@theholocron/cli": "2.0.0-alpha.8"
46
+ "typescript": "^5.9.3",
47
+ "vitest": "^4.1.10",
48
+ "@theholocron/cli": "2.0.0"
37
49
  },
38
50
  "publishConfig": {
39
51
  "access": "public"