@theholocron/netlify-client 1.36.0 → 1.38.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 +41 -12
- package/dist/index.d.mts +39 -7
- package/dist/index.mjs +169 -132
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -32,12 +32,17 @@ await netlify.sites.update("site-id", { custom_domain: "example.com" });
|
|
|
32
32
|
// Trigger a build from a git-linked site's default branch
|
|
33
33
|
const { deploy_id } = await netlify.sites.triggerBuild("site-id");
|
|
34
34
|
|
|
35
|
-
// Create a
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
// Create a deploy — static files (plain text) and/or functions (pre-built
|
|
36
|
+
// zips). This is the digest-based flow: real, invokable functions — not
|
|
37
|
+
// the single-whole-zip method, which only deploys static assets.
|
|
38
|
+
import { buildZip } from "@theholocron/netlify-client";
|
|
39
|
+
|
|
40
|
+
const functionZip = buildZip({
|
|
41
|
+
"webhook.mjs": "exports.handler = async () => ({ statusCode: 200, body: 'ok' });",
|
|
42
|
+
});
|
|
43
|
+
const deploy = await netlify.deploys.create("site-id", {
|
|
44
|
+
files: { "index.html": "<!doctype html><title>status</title>" },
|
|
45
|
+
functions: [{ name: "webhook", zip: functionZip }],
|
|
41
46
|
});
|
|
42
47
|
|
|
43
48
|
// Get a deploy by id
|
|
@@ -53,15 +58,39 @@ await netlify.env.set("account-id", "site-id", "API_URL", "production", "https:/
|
|
|
53
58
|
|
|
54
59
|
## Notes
|
|
55
60
|
|
|
56
|
-
- **
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
- **Functions need the digest flow — the single-zip method doesn't deploy
|
|
62
|
+
functions at all.** Confirmed live: a function file placed inside a
|
|
63
|
+
single whole-site zip (`POST .../deploys` with `Content-Type:
|
|
64
|
+
application/zip`) is served as an inert static asset, never wired up
|
|
65
|
+
as a Lambda. `deploys.create()`'s `functions` field uses the real flow
|
|
66
|
+
(SHA1 file manifest + SHA256 function manifest, then
|
|
67
|
+
`PUT .../functions/{name}?runtime=js`) — this is the only way to ship
|
|
68
|
+
an actually-invokable function through this API.
|
|
69
|
+
- **A function's zip entry file basename must exactly match the function
|
|
70
|
+
name.** Confirmed live: naming the function `webhook` but the zip's
|
|
71
|
+
entry file `handler.mjs` fails at invoke time with
|
|
72
|
+
`Runtime.ImportModuleError: Cannot find module 'webhook'`. No nested
|
|
73
|
+
paths either (unlike Vercel's `api/webhook.mjs`) — the file needs to
|
|
74
|
+
sit at the zip root as `<name>.mjs` (or `.js`).
|
|
75
|
+
- **`runtime: "js"` expects a classic CommonJS/Lambda handler, not the
|
|
76
|
+
modern `(Request) => Response` style.** Confirmed live: `export default
|
|
77
|
+
(req) => new Response(...)` deploys without error but fails at invoke
|
|
78
|
+
time with `Runtime.HandlerNotFound: <name>.handler is undefined or not
|
|
79
|
+
exported`. Use `exports.handler = async (event, context) => ({
|
|
80
|
+
statusCode, headers, body })` (or the ESM equivalent, `export const
|
|
81
|
+
handler = ...`) instead.
|
|
82
|
+
- **No build step on a function upload.** Netlify runs no `npm install`
|
|
83
|
+
or configured build command for a raw API-uploaded function, unlike a
|
|
84
|
+
Git-linked deploy (or `netlify deploy` run locally via the CLI). If the
|
|
85
|
+
function imports anything beyond Node builtins, its `zip` needs a real
|
|
86
|
+
`node_modules` included — resolve dependencies into the zip yourself
|
|
87
|
+
before calling `create()`.
|
|
61
88
|
- **Sites are account-slug-scoped to create, account-id-scoped for env
|
|
62
89
|
vars.** Netlify's own API is inconsistent here — `POST /{account_slug}/sites`
|
|
63
90
|
but `GET/POST/PUT /accounts/{account_id}/env`. Not something this client
|
|
64
|
-
can paper over; pass whichever one each method asks for.
|
|
91
|
+
can paper over; pass whichever one each method asks for. The slug is
|
|
92
|
+
also mutable (an account/team can rename it in settings) — don't treat
|
|
93
|
+
a previously-observed slug as permanent.
|
|
65
94
|
- **`state` on a deploy is a free-form string**, not a published enum.
|
|
66
95
|
Known values seen in practice: `new`, `building`, `uploading`,
|
|
67
96
|
`uploaded`, `preparing`, `prepared`, `processing`, `ready`, `error`,
|
package/dist/index.d.mts
CHANGED
|
@@ -52,7 +52,36 @@ interface NetlifyDeploy {
|
|
|
52
52
|
error_message?: string | null;
|
|
53
53
|
created_at: string;
|
|
54
54
|
}
|
|
55
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* One function to deploy. `zip` is a pre-built archive — the function's
|
|
57
|
+
* own code plus, if it imports anything beyond Node builtins, a real
|
|
58
|
+
* `node_modules` (Netlify's raw deploy API runs no build/install step,
|
|
59
|
+
* unlike a Git-linked deploy — `src/zip.ts`'s `buildZip()` builds this
|
|
60
|
+
* from `{ path: content }` entries, text or binary).
|
|
61
|
+
*
|
|
62
|
+
* Netlify's classic function runtime (`runtime: "js"`) expects a
|
|
63
|
+
* CommonJS-style `exports.handler` or ESM `export const handler` entry
|
|
64
|
+
* point — confirmed live: a V2-style `export default (req) => Response`
|
|
65
|
+
* handler deploys without error but fails at invoke time with
|
|
66
|
+
* `Runtime.HandlerNotFound`. The entry file's basename (minus extension)
|
|
67
|
+
* inside the zip should match `name`.
|
|
68
|
+
*/
|
|
69
|
+
interface NetlifyDeployFunctionSpec {
|
|
70
|
+
/** Becomes invokable at `/.netlify/functions/<name>`. */
|
|
71
|
+
name: string;
|
|
72
|
+
/** Lambda runtime: `"js"` (Node.js, zipped) or `"go"` (zipped binary). */
|
|
73
|
+
runtime?: "js" | "go";
|
|
74
|
+
zip: Uint8Array;
|
|
75
|
+
}
|
|
76
|
+
interface NetlifyCreateDeployInput {
|
|
77
|
+
/** Static files, plain text — served as-is, no build step. */
|
|
78
|
+
files?: Record<string, string>;
|
|
79
|
+
/** Functions to deploy alongside (or instead of) static files. */
|
|
80
|
+
functions?: NetlifyDeployFunctionSpec[];
|
|
81
|
+
/** Process normally but don't make this the published deploy. */
|
|
82
|
+
draft?: boolean;
|
|
83
|
+
}
|
|
84
|
+
/** Raw transport details `create()` needs beyond the generic `RestClient` — file/function uploads send a binary body, which `RestClient.request()` can't carry (always JSON-encodes a given body). */
|
|
56
85
|
interface NetlifyRawTransport {
|
|
57
86
|
token: string;
|
|
58
87
|
baseUrl?: string;
|
|
@@ -95,8 +124,13 @@ interface NetlifyUser {
|
|
|
95
124
|
* real timestamp would only make otherwise-identical deploys look
|
|
96
125
|
* different in diffs/caches for no benefit.
|
|
97
126
|
*/
|
|
98
|
-
/**
|
|
99
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Builds a STORED-method ZIP archive from `{ path: content }` entries.
|
|
129
|
+
* `content` may be a utf8 string (text files) or raw bytes (binary —
|
|
130
|
+
* native addons, wasm, anything under a real `node_modules` tree a
|
|
131
|
+
* function's own dependencies need).
|
|
132
|
+
*/
|
|
133
|
+
declare function buildZip(files: Record<string, string | Uint8Array>): Uint8Array;
|
|
100
134
|
//#endregion
|
|
101
135
|
//#region ../../../../../src/index.d.ts
|
|
102
136
|
declare function createNetlifyClient(opts: NetlifyClientOptions): {
|
|
@@ -111,9 +145,7 @@ declare function createNetlifyClient(opts: NetlifyClientOptions): {
|
|
|
111
145
|
};
|
|
112
146
|
deploys: {
|
|
113
147
|
get: (deployId: string) => Promise<NetlifyDeploy>;
|
|
114
|
-
|
|
115
|
-
draft?: boolean;
|
|
116
|
-
}): Promise<NetlifyDeploy>;
|
|
148
|
+
create(siteId: string, input?: NetlifyCreateDeployInput): Promise<NetlifyDeploy>;
|
|
117
149
|
};
|
|
118
150
|
env: {
|
|
119
151
|
list: (accountId: string, siteId: string) => Promise<NetlifyEnvVar[]>;
|
|
@@ -125,4 +157,4 @@ declare function createNetlifyClient(opts: NetlifyClientOptions): {
|
|
|
125
157
|
};
|
|
126
158
|
type NetlifyClient = ReturnType<typeof createNetlifyClient>;
|
|
127
159
|
//#endregion
|
|
128
|
-
export { NetlifyClient, type NetlifyClientOptions, type NetlifyCreateSiteInput, type NetlifyDeploy, type NetlifyEnvContext, type NetlifyEnvVar, type NetlifyEnvVarValue, type NetlifyRawTransport, type NetlifySite, type NetlifyUpdateSiteInput, type NetlifyUser, buildZip, createNetlifyClient };
|
|
160
|
+
export { NetlifyClient, type NetlifyClientOptions, type NetlifyCreateDeployInput, type NetlifyCreateSiteInput, type NetlifyDeploy, type NetlifyDeployFunctionSpec, type NetlifyEnvContext, type NetlifyEnvVar, type NetlifyEnvVarValue, type NetlifyRawTransport, type NetlifySite, type NetlifyUpdateSiteInput, type NetlifyUser, buildZip, createNetlifyClient };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,166 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { ProviderApiError, createRestClient } from "@theholocron/http-client";
|
|
3
|
+
//#region ../../../../../src/deploys/deploys.ts
|
|
4
|
+
function sha1Hex(bytes) {
|
|
5
|
+
return createHash("sha1").update(bytes).digest("hex");
|
|
6
|
+
}
|
|
7
|
+
function sha256Hex(bytes) {
|
|
8
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
9
|
+
}
|
|
10
|
+
function deploys(rest, raw) {
|
|
11
|
+
async function uploadRaw(path, body) {
|
|
12
|
+
const fetchImpl = raw.fetch ?? globalThis.fetch;
|
|
13
|
+
let baseUrl = raw.baseUrl ?? "https://api.netlify.com/api/v1";
|
|
14
|
+
while (baseUrl.endsWith("/")) baseUrl = baseUrl.slice(0, -1);
|
|
15
|
+
const tag = `Netlify PUT ${path}`;
|
|
16
|
+
let res;
|
|
17
|
+
try {
|
|
18
|
+
res = await fetchImpl(`${baseUrl}${path}`, {
|
|
19
|
+
method: "PUT",
|
|
20
|
+
headers: {
|
|
21
|
+
authorization: `Bearer ${raw.token}`,
|
|
22
|
+
accept: "application/json",
|
|
23
|
+
"content-type": "application/octet-stream"
|
|
24
|
+
},
|
|
25
|
+
body
|
|
26
|
+
});
|
|
27
|
+
} catch (err) {
|
|
28
|
+
const detail = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
|
29
|
+
throw new ProviderApiError(`${tag} failed: ${detail}`, 0, void 0);
|
|
30
|
+
}
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
const resBody = await res.text().catch(() => "");
|
|
33
|
+
throw new ProviderApiError(`${tag} → ${res.status}`, res.status, resBody);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
get: (deployId) => rest.request(`/deploys/${encodeURIComponent(deployId)}`),
|
|
38
|
+
/**
|
|
39
|
+
* The digest-based deploy flow: send a manifest of file/function
|
|
40
|
+
* hashes, then upload only the content Netlify doesn't already have
|
|
41
|
+
* cached (`required`/`required_functions` in the response — in
|
|
42
|
+
* practice, for a fresh deploy that's everything). This is the
|
|
43
|
+
* *only* flow that deploys real, invokable functions — the
|
|
44
|
+
* single-request whole-zip method (`POST .../deploys` with
|
|
45
|
+
* `Content-Type: application/zip`) only deploys static assets; a
|
|
46
|
+
* function file placed in that zip is served as an inert static
|
|
47
|
+
* file, never wired up as a Lambda (confirmed live).
|
|
48
|
+
*/
|
|
49
|
+
async create(siteId, input = {}) {
|
|
50
|
+
const files = input.files ?? {};
|
|
51
|
+
const functions = input.functions ?? [];
|
|
52
|
+
const fileHashes = Object.fromEntries(Object.entries(files).map(([path, content]) => [path, sha1Hex(new TextEncoder().encode(content))]));
|
|
53
|
+
const functionHashes = Object.fromEntries(functions.map((f) => [f.name, sha256Hex(f.zip)]));
|
|
54
|
+
const deploy = await rest.request(`/sites/${encodeURIComponent(siteId)}/deploys`, {
|
|
55
|
+
method: "POST",
|
|
56
|
+
body: {
|
|
57
|
+
files: fileHashes,
|
|
58
|
+
...functions.length > 0 ? { functions: functionHashes } : {},
|
|
59
|
+
...input.draft ? { draft: true } : {}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
const requiredFiles = new Set(deploy.required ?? []);
|
|
63
|
+
for (const [path, hash] of Object.entries(fileHashes)) {
|
|
64
|
+
if (!requiredFiles.has(hash)) continue;
|
|
65
|
+
await uploadRaw(`/deploys/${deploy.id}/files/${path.replace(/^\//, "")}`, new TextEncoder().encode(files[path]));
|
|
66
|
+
}
|
|
67
|
+
const requiredFunctions = new Set(deploy.required_functions ?? []);
|
|
68
|
+
for (const fn of functions) {
|
|
69
|
+
if (!requiredFunctions.has(functionHashes[fn.name])) continue;
|
|
70
|
+
await uploadRaw(`/deploys/${deploy.id}/functions/${encodeURIComponent(fn.name)}?runtime=${fn.runtime ?? "js"}`, fn.zip);
|
|
71
|
+
}
|
|
72
|
+
return deploy;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region ../../../../../src/env/env.ts
|
|
78
|
+
function env(rest) {
|
|
79
|
+
const list = (accountId, siteId) => rest.request(`/accounts/${encodeURIComponent(accountId)}/env`, { query: { site_id: siteId } });
|
|
80
|
+
return {
|
|
81
|
+
list,
|
|
82
|
+
/**
|
|
83
|
+
* Upsert one value for one context on one key. Netlify's env API has
|
|
84
|
+
* no single "upsert a value" endpoint — `PUT /env/{key}` replaces the
|
|
85
|
+
* entire `values` array, and there's no documented "get one key"
|
|
86
|
+
* endpoint either (only list + PUT/DELETE by key) — so a genuine
|
|
87
|
+
* upsert needs the key's current full value list first.
|
|
88
|
+
*/
|
|
89
|
+
async set(accountId, siteId, key, context, value) {
|
|
90
|
+
const query = { site_id: siteId };
|
|
91
|
+
const existing = (await list(accountId, siteId)).find((v) => v.key === key);
|
|
92
|
+
if (!existing) {
|
|
93
|
+
await rest.request(`/accounts/${encodeURIComponent(accountId)}/env`, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
query,
|
|
96
|
+
body: {
|
|
97
|
+
key,
|
|
98
|
+
scopes: ["functions", "runtime"],
|
|
99
|
+
values: [{
|
|
100
|
+
value,
|
|
101
|
+
context
|
|
102
|
+
}]
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const values = existing.values.filter((v) => v.context !== context);
|
|
108
|
+
values.push({
|
|
109
|
+
value,
|
|
110
|
+
context
|
|
111
|
+
});
|
|
112
|
+
await rest.request(`/accounts/${encodeURIComponent(accountId)}/env/${encodeURIComponent(key)}`, {
|
|
113
|
+
method: "PUT",
|
|
114
|
+
query,
|
|
115
|
+
body: {
|
|
116
|
+
key,
|
|
117
|
+
scopes: existing.scopes ?? ["functions", "runtime"],
|
|
118
|
+
values
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
//#endregion
|
|
125
|
+
//#region ../../../../../src/sites/sites.ts
|
|
126
|
+
function sites(rest) {
|
|
127
|
+
return {
|
|
128
|
+
list: () => rest.request("/sites"),
|
|
129
|
+
get: (siteId) => rest.request(`/sites/${encodeURIComponent(siteId)}`),
|
|
130
|
+
/** Scoped to `/{account_slug}/sites` — Netlify has no account-id-based create path. */
|
|
131
|
+
create: (accountSlug, input) => rest.request(`/${encodeURIComponent(accountSlug)}/sites`, {
|
|
132
|
+
method: "POST",
|
|
133
|
+
body: input
|
|
134
|
+
}),
|
|
135
|
+
update: (siteId, input) => rest.request(`/sites/${encodeURIComponent(siteId)}`, {
|
|
136
|
+
method: "PATCH",
|
|
137
|
+
body: input
|
|
138
|
+
}),
|
|
139
|
+
/** Triggers a new build from the latest commit on a git-linked site's default branch. */
|
|
140
|
+
triggerBuild: (siteId) => rest.request(`/sites/${encodeURIComponent(siteId)}/builds`, {
|
|
141
|
+
method: "POST",
|
|
142
|
+
body: { clear_cache: false }
|
|
143
|
+
})
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region ../../../../../src/user/user.ts
|
|
148
|
+
function user(rest) {
|
|
149
|
+
return { get: () => rest.request("/user") };
|
|
150
|
+
}
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region ../../../../../src/utils.ts
|
|
153
|
+
function createNetlifyRestClient(opts) {
|
|
154
|
+
return createRestClient({
|
|
155
|
+
baseUrl: opts.baseUrl ?? "https://api.netlify.com/api/v1",
|
|
156
|
+
token: opts.token,
|
|
157
|
+
vendor: "Netlify",
|
|
158
|
+
fetch: opts.fetch,
|
|
159
|
+
logger: opts.logger,
|
|
160
|
+
errors: opts.errors
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
//#endregion
|
|
2
164
|
//#region ../../../../../src/zip.ts
|
|
3
165
|
/**
|
|
4
166
|
* Minimal ZIP archive builder — STORED (uncompressed) entries only, no
|
|
@@ -42,11 +204,16 @@ function crc32(data) {
|
|
|
42
204
|
for (const byte of data) crc = CRC_TABLE[(crc ^ byte) & 255] ^ crc >>> 8;
|
|
43
205
|
return (crc ^ 4294967295) >>> 0;
|
|
44
206
|
}
|
|
45
|
-
/**
|
|
207
|
+
/**
|
|
208
|
+
* Builds a STORED-method ZIP archive from `{ path: content }` entries.
|
|
209
|
+
* `content` may be a utf8 string (text files) or raw bytes (binary —
|
|
210
|
+
* native addons, wasm, anything under a real `node_modules` tree a
|
|
211
|
+
* function's own dependencies need).
|
|
212
|
+
*/
|
|
46
213
|
function buildZip(files) {
|
|
47
214
|
const entries = Object.entries(files).map(([path, content]) => ({
|
|
48
215
|
path: path.startsWith("/") ? path.slice(1) : path,
|
|
49
|
-
content: new TextEncoder().encode(content)
|
|
216
|
+
content: typeof content === "string" ? new TextEncoder().encode(content) : content
|
|
50
217
|
}));
|
|
51
218
|
const localParts = [];
|
|
52
219
|
const centralParts = [];
|
|
@@ -113,136 +280,6 @@ function buildZip(files) {
|
|
|
113
280
|
return total;
|
|
114
281
|
}
|
|
115
282
|
//#endregion
|
|
116
|
-
//#region ../../../../../src/deploys/deploys.ts
|
|
117
|
-
function deploys(rest, raw) {
|
|
118
|
-
return {
|
|
119
|
-
get: (deployId) => rest.request(`/deploys/${encodeURIComponent(deployId)}`),
|
|
120
|
-
/**
|
|
121
|
-
* Zips `files` (plain text) and uploads as a one-shot deploy via
|
|
122
|
-
* `POST /sites/{id}/deploys` with `Content-Type: application/zip`.
|
|
123
|
-
* Netlify runs no build step for a raw zip upload (unlike a
|
|
124
|
-
* Git-linked deploy) — the caller is responsible for having already
|
|
125
|
-
* resolved dependencies into the file set.
|
|
126
|
-
*/
|
|
127
|
-
async createFromZip(siteId, files, opts = {}) {
|
|
128
|
-
const fetchImpl = raw.fetch ?? globalThis.fetch;
|
|
129
|
-
let baseUrl = raw.baseUrl ?? "https://api.netlify.com/api/v1";
|
|
130
|
-
while (baseUrl.endsWith("/")) baseUrl = baseUrl.slice(0, -1);
|
|
131
|
-
const url = new URL(`${baseUrl}/sites/${encodeURIComponent(siteId)}/deploys`);
|
|
132
|
-
if (opts.draft) url.searchParams.set("draft", "true");
|
|
133
|
-
const zip = buildZip(files);
|
|
134
|
-
const tag = `Netlify POST /sites/${siteId}/deploys`;
|
|
135
|
-
let res;
|
|
136
|
-
try {
|
|
137
|
-
res = await fetchImpl(url.toString(), {
|
|
138
|
-
method: "POST",
|
|
139
|
-
headers: {
|
|
140
|
-
authorization: `Bearer ${raw.token}`,
|
|
141
|
-
accept: "application/json",
|
|
142
|
-
"content-type": "application/zip"
|
|
143
|
-
},
|
|
144
|
-
body: zip
|
|
145
|
-
});
|
|
146
|
-
} catch (err) {
|
|
147
|
-
const detail = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
|
148
|
-
throw new ProviderApiError(`${tag} failed: ${detail}`, 0, void 0);
|
|
149
|
-
}
|
|
150
|
-
if (!res.ok) {
|
|
151
|
-
const body = await res.text().catch(() => "");
|
|
152
|
-
throw new ProviderApiError(`${tag} → ${res.status}`, res.status, body);
|
|
153
|
-
}
|
|
154
|
-
return await res.json();
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
//#endregion
|
|
159
|
-
//#region ../../../../../src/env/env.ts
|
|
160
|
-
function env(rest) {
|
|
161
|
-
const list = (accountId, siteId) => rest.request(`/accounts/${encodeURIComponent(accountId)}/env`, { query: { site_id: siteId } });
|
|
162
|
-
return {
|
|
163
|
-
list,
|
|
164
|
-
/**
|
|
165
|
-
* Upsert one value for one context on one key. Netlify's env API has
|
|
166
|
-
* no single "upsert a value" endpoint — `PUT /env/{key}` replaces the
|
|
167
|
-
* entire `values` array, and there's no documented "get one key"
|
|
168
|
-
* endpoint either (only list + PUT/DELETE by key) — so a genuine
|
|
169
|
-
* upsert needs the key's current full value list first.
|
|
170
|
-
*/
|
|
171
|
-
async set(accountId, siteId, key, context, value) {
|
|
172
|
-
const query = { site_id: siteId };
|
|
173
|
-
const existing = (await list(accountId, siteId)).find((v) => v.key === key);
|
|
174
|
-
if (!existing) {
|
|
175
|
-
await rest.request(`/accounts/${encodeURIComponent(accountId)}/env`, {
|
|
176
|
-
method: "POST",
|
|
177
|
-
query,
|
|
178
|
-
body: {
|
|
179
|
-
key,
|
|
180
|
-
scopes: ["functions", "runtime"],
|
|
181
|
-
values: [{
|
|
182
|
-
value,
|
|
183
|
-
context
|
|
184
|
-
}]
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
const values = existing.values.filter((v) => v.context !== context);
|
|
190
|
-
values.push({
|
|
191
|
-
value,
|
|
192
|
-
context
|
|
193
|
-
});
|
|
194
|
-
await rest.request(`/accounts/${encodeURIComponent(accountId)}/env/${encodeURIComponent(key)}`, {
|
|
195
|
-
method: "PUT",
|
|
196
|
-
query,
|
|
197
|
-
body: {
|
|
198
|
-
key,
|
|
199
|
-
scopes: existing.scopes ?? ["functions", "runtime"],
|
|
200
|
-
values
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
//#endregion
|
|
207
|
-
//#region ../../../../../src/sites/sites.ts
|
|
208
|
-
function sites(rest) {
|
|
209
|
-
return {
|
|
210
|
-
list: () => rest.request("/sites"),
|
|
211
|
-
get: (siteId) => rest.request(`/sites/${encodeURIComponent(siteId)}`),
|
|
212
|
-
/** Scoped to `/{account_slug}/sites` — Netlify has no account-id-based create path. */
|
|
213
|
-
create: (accountSlug, input) => rest.request(`/${encodeURIComponent(accountSlug)}/sites`, {
|
|
214
|
-
method: "POST",
|
|
215
|
-
body: input
|
|
216
|
-
}),
|
|
217
|
-
update: (siteId, input) => rest.request(`/sites/${encodeURIComponent(siteId)}`, {
|
|
218
|
-
method: "PATCH",
|
|
219
|
-
body: input
|
|
220
|
-
}),
|
|
221
|
-
/** Triggers a new build from the latest commit on a git-linked site's default branch. */
|
|
222
|
-
triggerBuild: (siteId) => rest.request(`/sites/${encodeURIComponent(siteId)}/builds`, {
|
|
223
|
-
method: "POST",
|
|
224
|
-
body: { clear_cache: false }
|
|
225
|
-
})
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
//#endregion
|
|
229
|
-
//#region ../../../../../src/user/user.ts
|
|
230
|
-
function user(rest) {
|
|
231
|
-
return { get: () => rest.request("/user") };
|
|
232
|
-
}
|
|
233
|
-
//#endregion
|
|
234
|
-
//#region ../../../../../src/utils.ts
|
|
235
|
-
function createNetlifyRestClient(opts) {
|
|
236
|
-
return createRestClient({
|
|
237
|
-
baseUrl: opts.baseUrl ?? "https://api.netlify.com/api/v1",
|
|
238
|
-
token: opts.token,
|
|
239
|
-
vendor: "Netlify",
|
|
240
|
-
fetch: opts.fetch,
|
|
241
|
-
logger: opts.logger,
|
|
242
|
-
errors: opts.errors
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
//#endregion
|
|
246
283
|
//#region ../../../../../src/index.ts
|
|
247
284
|
function createNetlifyClient(opts) {
|
|
248
285
|
const rest = createNetlifyRestClient(opts);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theholocron/netlify-client",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.38.0",
|
|
4
4
|
"description": "A TypeScript client for the Netlify API",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"netlify",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@theholocron/observability": "^0.3.0",
|
|
36
|
-
"@theholocron/http-client": "^1.
|
|
36
|
+
"@theholocron/http-client": "^1.38.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@theholocron/cli": "5.0.0-alpha.79",
|