cmskite 0.1.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/LICENSE +21 -0
- package/README.md +462 -0
- package/dist/analytics-B0zzt0nW.d.cts +91 -0
- package/dist/analytics-B0zzt0nW.d.ts +91 -0
- package/dist/browser.cjs +152 -0
- package/dist/browser.cjs.map +1 -0
- package/dist/browser.d.cts +30 -0
- package/dist/browser.d.ts +30 -0
- package/dist/browser.js +12 -0
- package/dist/browser.js.map +1 -0
- package/dist/chunk-DT3V5CH2.js +145 -0
- package/dist/chunk-DT3V5CH2.js.map +1 -0
- package/dist/chunk-QXWHDCMJ.js +143 -0
- package/dist/chunk-QXWHDCMJ.js.map +1 -0
- package/dist/index.cjs +348 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +278 -0
- package/dist/index.d.ts +278 -0
- package/dist/index.js +204 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +175 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +45 -0
- package/dist/react.d.ts +45 -0
- package/dist/react.js +32 -0
- package/dist/react.js.map +1 -0
- package/package.json +54 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/errors.ts
|
|
4
|
+
var CMSKiteError = class extends Error {
|
|
5
|
+
/** HTTP status, or 0 when no response arrived at all. */
|
|
6
|
+
status;
|
|
7
|
+
/** A stable machine-readable reason. Safe to branch on; see the constants. */
|
|
8
|
+
code;
|
|
9
|
+
/** Whatever the API attached. Field-level validation problems live here. */
|
|
10
|
+
details;
|
|
11
|
+
/** Quote this when asking us about a request. */
|
|
12
|
+
requestId;
|
|
13
|
+
constructor(status, code, message, details = {}, requestId = null) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "CMSKiteError";
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.code = code;
|
|
18
|
+
this.details = details;
|
|
19
|
+
this.requestId = requestId;
|
|
20
|
+
}
|
|
21
|
+
/** Nothing you send differently will change the answer. */
|
|
22
|
+
get isClientError() {
|
|
23
|
+
return this.status >= 400 && this.status < 500;
|
|
24
|
+
}
|
|
25
|
+
/** Worth trying again. The client already retried once. */
|
|
26
|
+
get isRetryable() {
|
|
27
|
+
return this.status === 0 || this.status === 429 || this.status >= 500;
|
|
28
|
+
}
|
|
29
|
+
get isNotFound() {
|
|
30
|
+
return this.status === 404;
|
|
31
|
+
}
|
|
32
|
+
/** The key is missing, wrong, or has been revoked. */
|
|
33
|
+
get isAuthError() {
|
|
34
|
+
return this.status === 401 || this.status === 403;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
var ErrorCode = {
|
|
38
|
+
NETWORK: "NETWORK",
|
|
39
|
+
TIMEOUT: "TIMEOUT",
|
|
40
|
+
BAD_RESPONSE: "BAD_RESPONSE",
|
|
41
|
+
UNAUTHENTICATED: "UNAUTHENTICATED",
|
|
42
|
+
FORBIDDEN: "FORBIDDEN",
|
|
43
|
+
NOT_FOUND: "NOT_FOUND",
|
|
44
|
+
RATE_LIMITED: "RATE_LIMITED",
|
|
45
|
+
INVALID_REQUEST: "INVALID_REQUEST"
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// src/transport.ts
|
|
49
|
+
var DEFAULT_BASE_URL = "https://api.cmskite.com";
|
|
50
|
+
var VERSION = "0.1.0";
|
|
51
|
+
async function request(config, method, path, init = {}) {
|
|
52
|
+
const url = buildUrl(config.baseUrl, path, init.query);
|
|
53
|
+
const timeoutMs = init.options?.timeoutMs ?? config.timeoutMs;
|
|
54
|
+
let lastError = null;
|
|
55
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
56
|
+
try {
|
|
57
|
+
return await once(config, method, url, timeoutMs, init);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const failure = error instanceof CMSKiteError ? error : toError(error);
|
|
60
|
+
if (init.options?.signal?.aborted) throw failure;
|
|
61
|
+
if (!failure.isRetryable || attempt === 1) throw failure;
|
|
62
|
+
lastError = failure;
|
|
63
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
throw lastError ?? new CMSKiteError(0, ErrorCode.NETWORK, "Request failed.");
|
|
67
|
+
}
|
|
68
|
+
async function once(config, method, url, timeoutMs, init) {
|
|
69
|
+
const timeout = new AbortController();
|
|
70
|
+
const timer = setTimeout(() => timeout.abort(), timeoutMs);
|
|
71
|
+
const signal = init.options?.signal ? AbortSignal.any([init.options.signal, timeout.signal]) : timeout.signal;
|
|
72
|
+
let response;
|
|
73
|
+
let text;
|
|
74
|
+
try {
|
|
75
|
+
response = await config.fetch(url, {
|
|
76
|
+
method,
|
|
77
|
+
signal,
|
|
78
|
+
headers: {
|
|
79
|
+
authorization: `Bearer ${config.apiKey}`,
|
|
80
|
+
accept: "application/json",
|
|
81
|
+
"x-cmskite-sdk": VERSION,
|
|
82
|
+
...init.body === void 0 ? {} : { "content-type": "application/json" },
|
|
83
|
+
...config.headers
|
|
84
|
+
},
|
|
85
|
+
...init.body === void 0 ? {} : { body: JSON.stringify(init.body) }
|
|
86
|
+
});
|
|
87
|
+
text = await response.text();
|
|
88
|
+
} catch (error) {
|
|
89
|
+
throw toError(error, timeout.signal.aborted, timeoutMs);
|
|
90
|
+
} finally {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
}
|
|
93
|
+
let payload;
|
|
94
|
+
try {
|
|
95
|
+
payload = text ? JSON.parse(text) : {};
|
|
96
|
+
} catch {
|
|
97
|
+
throw new CMSKiteError(
|
|
98
|
+
response.status,
|
|
99
|
+
ErrorCode.BAD_RESPONSE,
|
|
100
|
+
"CMSKite returned something this client could not read.",
|
|
101
|
+
{},
|
|
102
|
+
response.headers.get("x-request-id")
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (!response.ok || payload.success === false) {
|
|
106
|
+
throw new CMSKiteError(
|
|
107
|
+
response.status,
|
|
108
|
+
payload.error?.code ?? codeForStatus(response.status),
|
|
109
|
+
payload.error?.message ?? `CMSKite answered ${response.status}.`,
|
|
110
|
+
payload.error?.details ?? {},
|
|
111
|
+
payload.requestId ?? response.headers.get("x-request-id")
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const result = { data: payload.data };
|
|
115
|
+
if (payload.pagination) result.pagination = payload.pagination;
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
function toError(error, timedOut = false, timeoutMs = 0) {
|
|
119
|
+
if (error instanceof CMSKiteError) return error;
|
|
120
|
+
if (timedOut) {
|
|
121
|
+
return new CMSKiteError(0, ErrorCode.TIMEOUT, `CMSKite did not answer within ${timeoutMs}ms.`);
|
|
122
|
+
}
|
|
123
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
124
|
+
return new CMSKiteError(0, ErrorCode.NETWORK, "The request was cancelled.");
|
|
125
|
+
}
|
|
126
|
+
return new CMSKiteError(0, ErrorCode.NETWORK, "Could not reach CMSKite. Check the connection.");
|
|
127
|
+
}
|
|
128
|
+
function codeForStatus(status) {
|
|
129
|
+
if (status === 401) return ErrorCode.UNAUTHENTICATED;
|
|
130
|
+
if (status === 403) return ErrorCode.FORBIDDEN;
|
|
131
|
+
if (status === 404) return ErrorCode.NOT_FOUND;
|
|
132
|
+
if (status === 429) return ErrorCode.RATE_LIMITED;
|
|
133
|
+
if (status >= 400 && status < 500) return ErrorCode.INVALID_REQUEST;
|
|
134
|
+
return "INTERNAL_ERROR";
|
|
135
|
+
}
|
|
136
|
+
function buildUrl(baseUrl, path, query = {}) {
|
|
137
|
+
const url = new URL(path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
|
|
138
|
+
for (const [key, value] of Object.entries(query)) {
|
|
139
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
140
|
+
url.searchParams.set(key, String(value));
|
|
141
|
+
}
|
|
142
|
+
return url.toString();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/client.ts
|
|
146
|
+
var CMSKite = class {
|
|
147
|
+
posts;
|
|
148
|
+
categories;
|
|
149
|
+
tags;
|
|
150
|
+
authors;
|
|
151
|
+
/** @internal Not part of the public surface; the shape here may change. */
|
|
152
|
+
config;
|
|
153
|
+
constructor(options) {
|
|
154
|
+
if (!options?.apiKey) {
|
|
155
|
+
throw new CMSKiteError(
|
|
156
|
+
0,
|
|
157
|
+
ErrorCode.INVALID_REQUEST,
|
|
158
|
+
"createCMSKite needs an apiKey. Create one under Project \u2192 API keys."
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
this.config = {
|
|
162
|
+
apiKey: options.apiKey,
|
|
163
|
+
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
164
|
+
timeoutMs: options.timeoutMs ?? 1e4,
|
|
165
|
+
headers: options.headers ?? {},
|
|
166
|
+
// Bound, because an unbound `fetch` throws "Illegal invocation" in a
|
|
167
|
+
// browser -- a failure that only appears in the environment that matters
|
|
168
|
+
// most and reads as nothing to do with us.
|
|
169
|
+
fetch: (options.fetch ?? globalThis.fetch).bind(globalThis)
|
|
170
|
+
};
|
|
171
|
+
this.posts = new Posts(this.config);
|
|
172
|
+
this.categories = new Categories(this.config);
|
|
173
|
+
this.tags = new Tags(this.config);
|
|
174
|
+
this.authors = new Authors(this.config);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
function page(result) {
|
|
178
|
+
return {
|
|
179
|
+
items: result.data ?? [],
|
|
180
|
+
pagination: result.pagination ?? { hasNext: false, nextCursor: null, limit: 0 }
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
var Posts = class {
|
|
184
|
+
constructor(config) {
|
|
185
|
+
this.config = config;
|
|
186
|
+
}
|
|
187
|
+
config;
|
|
188
|
+
/** A page of posts. Published only, newest first, unless you say otherwise. */
|
|
189
|
+
async list(query = {}, options) {
|
|
190
|
+
const result = await request(this.config, "GET", "v1/blog/posts", {
|
|
191
|
+
query: { ...query, withTotal: query.withTotal ? "true" : void 0 },
|
|
192
|
+
...options ? { options } : {}
|
|
193
|
+
});
|
|
194
|
+
return page(result);
|
|
195
|
+
}
|
|
196
|
+
/** One post by its id. The body is always included. */
|
|
197
|
+
async get(id, options) {
|
|
198
|
+
const result = await request(this.config, "GET", `v1/blog/posts/${encodeURIComponent(id)}`, {
|
|
199
|
+
...options ? { options } : {}
|
|
200
|
+
});
|
|
201
|
+
return result.data;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* One post by its address.
|
|
205
|
+
*
|
|
206
|
+
* This is what a page at `/blog/[slug]` wants. An old slug still resolves,
|
|
207
|
+
* and the answer carries `slugRedirectedFrom` so the page can issue a 301
|
|
208
|
+
* rather than quietly serving two addresses for one post.
|
|
209
|
+
*/
|
|
210
|
+
async getBySlug(slug, options) {
|
|
211
|
+
const result = await request(
|
|
212
|
+
this.config,
|
|
213
|
+
"GET",
|
|
214
|
+
`v1/blog/posts/slug/${encodeURIComponent(slug)}`,
|
|
215
|
+
{ ...options ? { options } : {} }
|
|
216
|
+
);
|
|
217
|
+
return result.data;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* The same as `getBySlug`, but a missing post is `null` rather than a throw.
|
|
221
|
+
*
|
|
222
|
+
* For a page that renders its own not-found state, which is most of them.
|
|
223
|
+
*/
|
|
224
|
+
async findBySlug(slug, options) {
|
|
225
|
+
try {
|
|
226
|
+
return await this.getBySlug(slug, options);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (error instanceof CMSKiteError && error.isNotFound) return null;
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/** Posts like this one, chosen by shared tags and category. */
|
|
233
|
+
async related(id, query = {}, options) {
|
|
234
|
+
const result = await request(
|
|
235
|
+
this.config,
|
|
236
|
+
"GET",
|
|
237
|
+
`v1/blog/posts/${encodeURIComponent(id)}/related`,
|
|
238
|
+
{ query, ...options ? { options } : {} }
|
|
239
|
+
);
|
|
240
|
+
return page(result);
|
|
241
|
+
}
|
|
242
|
+
/** Full-text search across titles and bodies. */
|
|
243
|
+
async search(q, query = {}, options) {
|
|
244
|
+
const result = await request(this.config, "GET", "v1/blog/search", {
|
|
245
|
+
query: { q, ...query },
|
|
246
|
+
...options ? { options } : {}
|
|
247
|
+
});
|
|
248
|
+
return page(result);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Every post, a page at a time, without writing the cursor loop.
|
|
252
|
+
*
|
|
253
|
+
* An async iterator rather than an array, so a site with four thousand posts
|
|
254
|
+
* does not hold four thousand posts in memory to generate a sitemap.
|
|
255
|
+
*
|
|
256
|
+
* for await (const post of cms.posts.all()) { … }
|
|
257
|
+
*/
|
|
258
|
+
async *all(query = {}, options) {
|
|
259
|
+
let cursor;
|
|
260
|
+
do {
|
|
261
|
+
const result = await this.list(
|
|
262
|
+
{ ...query, ...cursor ? { cursor } : {} },
|
|
263
|
+
options
|
|
264
|
+
);
|
|
265
|
+
for (const item of result.items) yield item;
|
|
266
|
+
cursor = result.pagination.nextCursor ?? void 0;
|
|
267
|
+
} while (cursor);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
var Categories = class {
|
|
271
|
+
constructor(config) {
|
|
272
|
+
this.config = config;
|
|
273
|
+
}
|
|
274
|
+
config;
|
|
275
|
+
async list(query = {}, options) {
|
|
276
|
+
return page(
|
|
277
|
+
await request(this.config, "GET", "v1/blog/categories", {
|
|
278
|
+
query,
|
|
279
|
+
...options ? { options } : {}
|
|
280
|
+
})
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
async get(id, options) {
|
|
284
|
+
const result = await request(
|
|
285
|
+
this.config,
|
|
286
|
+
"GET",
|
|
287
|
+
`v1/blog/categories/${encodeURIComponent(id)}`,
|
|
288
|
+
{ ...options ? { options } : {} }
|
|
289
|
+
);
|
|
290
|
+
return result.data;
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
var Tags = class {
|
|
294
|
+
constructor(config) {
|
|
295
|
+
this.config = config;
|
|
296
|
+
}
|
|
297
|
+
config;
|
|
298
|
+
async list(query = {}, options) {
|
|
299
|
+
return page(
|
|
300
|
+
await request(this.config, "GET", "v1/blog/tags", {
|
|
301
|
+
query,
|
|
302
|
+
...options ? { options } : {}
|
|
303
|
+
})
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
async get(id, options) {
|
|
307
|
+
const result = await request(
|
|
308
|
+
this.config,
|
|
309
|
+
"GET",
|
|
310
|
+
`v1/blog/tags/${encodeURIComponent(id)}`,
|
|
311
|
+
{ ...options ? { options } : {} }
|
|
312
|
+
);
|
|
313
|
+
return result.data;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
var Authors = class {
|
|
317
|
+
constructor(config) {
|
|
318
|
+
this.config = config;
|
|
319
|
+
}
|
|
320
|
+
config;
|
|
321
|
+
async list(query = {}, options) {
|
|
322
|
+
return page(
|
|
323
|
+
await request(this.config, "GET", "v1/blog/authors", {
|
|
324
|
+
query,
|
|
325
|
+
...options ? { options } : {}
|
|
326
|
+
})
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
async get(id, options) {
|
|
330
|
+
const result = await request(
|
|
331
|
+
this.config,
|
|
332
|
+
"GET",
|
|
333
|
+
`v1/blog/authors/${encodeURIComponent(id)}`,
|
|
334
|
+
{ ...options ? { options } : {} }
|
|
335
|
+
);
|
|
336
|
+
return result.data;
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
function createCMSKite(options) {
|
|
340
|
+
return new CMSKite(options);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
exports.CMSKite = CMSKite;
|
|
344
|
+
exports.CMSKiteError = CMSKiteError;
|
|
345
|
+
exports.ErrorCode = ErrorCode;
|
|
346
|
+
exports.createCMSKite = createCMSKite;
|
|
347
|
+
//# sourceMappingURL=index.cjs.map
|
|
348
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/transport.ts","../src/client.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * One error type, so a caller writes one catch.\n *\n * Everything that can go wrong on the way to an answer arrives as a\n * `CMSKiteError`: a refusal from the API, a dropped connection, a timeout, a\n * response that was not JSON. Without that, a `catch` has to tell a `TypeError`\n * from a `SyntaxError` from whatever the API said, and the ones that forget\n * show somebody \"Failed to fetch\" -- a sentence about our code rather than\n * about their connection.\n */\nexport class CMSKiteError extends Error {\n /** HTTP status, or 0 when no response arrived at all. */\n readonly status: number\n /** A stable machine-readable reason. Safe to branch on; see the constants. */\n readonly code: string\n /** Whatever the API attached. Field-level validation problems live here. */\n readonly details: Record<string, unknown>\n /** Quote this when asking us about a request. */\n readonly requestId: string | null\n\n constructor(\n status: number,\n code: string,\n message: string,\n details: Record<string, unknown> = {},\n requestId: string | null = null,\n ) {\n super(message)\n this.name = 'CMSKiteError'\n this.status = status\n this.code = code\n this.details = details\n this.requestId = requestId\n }\n\n /** Nothing you send differently will change the answer. */\n get isClientError(): boolean {\n return this.status >= 400 && this.status < 500\n }\n\n /** Worth trying again. The client already retried once. */\n get isRetryable(): boolean {\n return this.status === 0 || this.status === 429 || this.status >= 500\n }\n\n get isNotFound(): boolean {\n return this.status === 404\n }\n\n /** The key is missing, wrong, or has been revoked. */\n get isAuthError(): boolean {\n return this.status === 401 || this.status === 403\n }\n}\n\n/**\n * The codes worth branching on, as values rather than as strings scattered\n * through a codebase.\n */\nexport const ErrorCode = {\n NETWORK: 'NETWORK',\n TIMEOUT: 'TIMEOUT',\n BAD_RESPONSE: 'BAD_RESPONSE',\n UNAUTHENTICATED: 'UNAUTHENTICATED',\n FORBIDDEN: 'FORBIDDEN',\n NOT_FOUND: 'NOT_FOUND',\n RATE_LIMITED: 'RATE_LIMITED',\n INVALID_REQUEST: 'INVALID_REQUEST',\n} as const\n\nexport type ErrorCodeValue = (typeof ErrorCode)[keyof typeof ErrorCode]\n","import { CMSKiteError, ErrorCode } from './errors.js'\nimport type { Page, RequestOptions } from './types.js'\n\nexport const DEFAULT_BASE_URL = 'https://api.cmskite.com'\n/**\n * What a caller may pass as a query.\n *\n * An interface with named optional fields -- which every query type here is --\n * does not satisfy `Record<string, unknown>`, because TypeScript will not give\n * one an implicit index signature. Widening to `object` and reading entries off\n * it is the honest way to accept both, and the values are stringified anyway.\n */\ntype QueryInput = Record<string, unknown> | object\n\n/** Sent so we can tell an SDK request from a hand-rolled one in support. */\nconst VERSION = '0.1.0'\n\nexport interface TransportConfig {\n apiKey: string\n baseUrl: string\n timeoutMs: number\n /** Extra headers on every request. For a proxy or a trace id. */\n headers: Record<string, string>\n fetch: typeof globalThis.fetch\n}\n\ninterface Envelope<T> {\n success?: boolean\n data?: T\n pagination?: Page<T>['pagination']\n error?: { code?: string; message?: string; details?: Record<string, unknown> }\n requestId?: string\n}\n\n/**\n * One request, and everything that can go wrong with it turned into one type.\n *\n * Retries exactly once, and only what a retry can fix: a dropped connection, a\n * timeout, a 429, a 5xx. A 404 is a settled answer and asking again only delays\n * showing somebody the truth. A 401 will not become a 200 by being repeated.\n */\nexport async function request<T>(\n config: TransportConfig,\n method: string,\n path: string,\n init: { query?: QueryInput; body?: unknown; options?: RequestOptions } = {},\n): Promise<{ data: T; pagination?: Page<T>['pagination'] }> {\n const url = buildUrl(config.baseUrl, path, init.query)\n const timeoutMs = init.options?.timeoutMs ?? config.timeoutMs\n\n let lastError: CMSKiteError | null = null\n for (let attempt = 0; attempt < 2; attempt++) {\n try {\n return await once<T>(config, method, url, timeoutMs, init)\n } catch (error) {\n const failure = error instanceof CMSKiteError ? error : toError(error)\n // A caller who aborted wants the abort, not a retry and not our wrapper.\n if (init.options?.signal?.aborted) throw failure\n if (!failure.isRetryable || attempt === 1) throw failure\n lastError = failure\n // One short pause. Anything longer is a decision the caller should make.\n await new Promise((resolve) => setTimeout(resolve, 250))\n }\n }\n\n throw lastError ?? new CMSKiteError(0, ErrorCode.NETWORK, 'Request failed.')\n}\n\nasync function once<T>(\n config: TransportConfig,\n method: string,\n url: string,\n timeoutMs: number,\n init: { body?: unknown; options?: RequestOptions },\n): Promise<{ data: T; pagination?: Page<T>['pagination'] }> {\n /**\n * The caller's signal and our timeout, combined.\n *\n * `AbortSignal.any` is the correct tool and exists everywhere this package\n * supports; the timeout controller is still created so that a timeout can be\n * told apart from a caller's abort in the catch below.\n */\n const timeout = new AbortController()\n const timer = setTimeout(() => timeout.abort(), timeoutMs)\n const signal = init.options?.signal\n ? AbortSignal.any([init.options.signal, timeout.signal])\n : timeout.signal\n\n let response: Response\n let text: string\n try {\n response = await config.fetch(url, {\n method,\n signal,\n headers: {\n authorization: `Bearer ${config.apiKey}`,\n accept: 'application/json',\n 'x-cmskite-sdk': VERSION,\n ...(init.body === undefined ? {} : { 'content-type': 'application/json' }),\n ...config.headers,\n },\n ...(init.body === undefined ? {} : { body: JSON.stringify(init.body) }),\n })\n text = await response.text()\n } catch (error) {\n throw toError(error, timeout.signal.aborted, timeoutMs)\n } finally {\n clearTimeout(timer)\n }\n\n let payload: Envelope<T>\n try {\n payload = text ? (JSON.parse(text) as Envelope<T>) : {}\n } catch {\n throw new CMSKiteError(\n response.status,\n ErrorCode.BAD_RESPONSE,\n 'CMSKite returned something this client could not read.',\n {},\n response.headers.get('x-request-id'),\n )\n }\n\n if (!response.ok || payload.success === false) {\n throw new CMSKiteError(\n response.status,\n payload.error?.code ?? codeForStatus(response.status),\n payload.error?.message ?? `CMSKite answered ${response.status}.`,\n payload.error?.details ?? {},\n payload.requestId ?? response.headers.get('x-request-id'),\n )\n }\n\n const result: { data: T; pagination?: Page<T>['pagination'] } = { data: payload.data as T }\n if (payload.pagination) result.pagination = payload.pagination\n return result\n}\n\nfunction toError(error: unknown, timedOut = false, timeoutMs = 0): CMSKiteError {\n if (error instanceof CMSKiteError) return error\n if (timedOut) {\n return new CMSKiteError(0, ErrorCode.TIMEOUT, `CMSKite did not answer within ${timeoutMs}ms.`)\n }\n if (error instanceof Error && error.name === 'AbortError') {\n return new CMSKiteError(0, ErrorCode.NETWORK, 'The request was cancelled.')\n }\n return new CMSKiteError(0, ErrorCode.NETWORK, 'Could not reach CMSKite. Check the connection.')\n}\n\nfunction codeForStatus(status: number): string {\n if (status === 401) return ErrorCode.UNAUTHENTICATED\n if (status === 403) return ErrorCode.FORBIDDEN\n if (status === 404) return ErrorCode.NOT_FOUND\n if (status === 429) return ErrorCode.RATE_LIMITED\n if (status >= 400 && status < 500) return ErrorCode.INVALID_REQUEST\n return 'INTERNAL_ERROR'\n}\n\n/**\n * Query building, in one place.\n *\n * `undefined`, `null` and the empty string are dropped rather than sent, so a\n * caller can pass an optional filter straight through without writing the same\n * three-line guard at every call site.\n */\nexport function buildUrl(baseUrl: string, path: string, query: QueryInput = {}): string {\n const url = new URL(path, baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`)\n for (const [key, value] of Object.entries(query as Record<string, unknown>)) {\n if (value === undefined || value === null || value === '') continue\n url.searchParams.set(key, String(value))\n }\n return url.toString()\n}\n","import { CMSKiteError, ErrorCode } from './errors.js'\nimport { DEFAULT_BASE_URL, request, type TransportConfig } from './transport.js'\nimport type {\n Author,\n Category,\n ListPostsQuery,\n ListQuery,\n Page,\n Post,\n RequestOptions,\n Tag,\n} from './types.js'\n\nexport interface CMSKiteOptions {\n /**\n * A CMSKite API key. Read-only, and scoped to one project.\n *\n * Safe in a browser bundle: the only scope a key can hold is `blog:read`, it\n * cannot write or delete anything, and a project can restrict which origins\n * may use it. See the security section of the README.\n */\n apiKey: string\n /** Only for a self-hosted deployment or a local API. */\n baseUrl?: string\n /** How long to wait for an answer. Default 10 seconds. */\n timeoutMs?: number\n /** Extra headers on every request, for a proxy or a trace id. */\n headers?: Record<string, string>\n /** Supply your own, for a test double or a fetch with retries built in. */\n fetch?: typeof globalThis.fetch\n}\n\n/**\n * The CMSKite client.\n *\n * Created once and reused. It holds no connection and no mutable state beyond\n * its configuration, so it is safe to build at module scope and share.\n *\n * const cms = createCMSKite({ apiKey: process.env.CMSKITE_API_KEY! })\n * const { items } = await cms.posts.list({ limit: 10 })\n *\n * Every method takes an optional `{ signal }`, so a request can be cancelled\n * when a component unmounts or a newer search supersedes an older one.\n */\nexport class CMSKite {\n readonly posts: Posts\n readonly categories: Categories\n readonly tags: Tags\n readonly authors: Authors\n /** @internal Not part of the public surface; the shape here may change. */\n private readonly config: TransportConfig\n\n constructor(options: CMSKiteOptions) {\n if (!options?.apiKey) {\n throw new CMSKiteError(\n 0,\n ErrorCode.INVALID_REQUEST,\n 'createCMSKite needs an apiKey. Create one under Project → API keys.',\n )\n }\n\n this.config = {\n apiKey: options.apiKey,\n baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,\n timeoutMs: options.timeoutMs ?? 10_000,\n headers: options.headers ?? {},\n // Bound, because an unbound `fetch` throws \"Illegal invocation\" in a\n // browser -- a failure that only appears in the environment that matters\n // most and reads as nothing to do with us.\n fetch: (options.fetch ?? globalThis.fetch).bind(globalThis),\n }\n\n this.posts = new Posts(this.config)\n this.categories = new Categories(this.config)\n this.tags = new Tags(this.config)\n this.authors = new Authors(this.config)\n }\n}\n\n/**\n * The shape a list endpoint answers with, normalised.\n *\n * The wire says `{ data, pagination }`. This says `{ items, pagination }`,\n * because a caller writing `posts.items.map(...)` should not have to know that\n * our envelope calls it `data`.\n */\nfunction page<T>(result: { data: T[]; pagination?: Page<T>['pagination'] }): Page<T> {\n return {\n items: result.data ?? [],\n pagination: result.pagination ?? { hasNext: false, nextCursor: null, limit: 0 },\n }\n}\n\nclass Posts {\n constructor(private readonly config: TransportConfig) {}\n\n /** A page of posts. Published only, newest first, unless you say otherwise. */\n async list(query: ListPostsQuery = {}, options?: RequestOptions): Promise<Page<Post>> {\n const result = await request<Post[]>(this.config, 'GET', 'v1/blog/posts', {\n query: { ...query, withTotal: query.withTotal ? 'true' : undefined },\n ...(options ? { options } : {}),\n })\n return page(result)\n }\n\n /** One post by its id. The body is always included. */\n async get(id: string, options?: RequestOptions): Promise<Post> {\n const result = await request<Post>(this.config, 'GET', `v1/blog/posts/${encodeURIComponent(id)}`, {\n ...(options ? { options } : {}),\n })\n return result.data\n }\n\n /**\n * One post by its address.\n *\n * This is what a page at `/blog/[slug]` wants. An old slug still resolves,\n * and the answer carries `slugRedirectedFrom` so the page can issue a 301\n * rather than quietly serving two addresses for one post.\n */\n async getBySlug(slug: string, options?: RequestOptions): Promise<Post> {\n const result = await request<Post>(\n this.config,\n 'GET',\n `v1/blog/posts/slug/${encodeURIComponent(slug)}`,\n { ...(options ? { options } : {}) },\n )\n return result.data\n }\n\n /**\n * The same as `getBySlug`, but a missing post is `null` rather than a throw.\n *\n * For a page that renders its own not-found state, which is most of them.\n */\n async findBySlug(slug: string, options?: RequestOptions): Promise<Post | null> {\n try {\n return await this.getBySlug(slug, options)\n } catch (error) {\n if (error instanceof CMSKiteError && error.isNotFound) return null\n throw error\n }\n }\n\n /** Posts like this one, chosen by shared tags and category. */\n async related(id: string, query: ListQuery = {}, options?: RequestOptions): Promise<Page<Post>> {\n const result = await request<Post[]>(\n this.config,\n 'GET',\n `v1/blog/posts/${encodeURIComponent(id)}/related`,\n { query, ...(options ? { options } : {}) },\n )\n return page(result)\n }\n\n /** Full-text search across titles and bodies. */\n async search(\n q: string,\n query: ListQuery = {},\n options?: RequestOptions,\n ): Promise<Page<Post>> {\n const result = await request<Post[]>(this.config, 'GET', 'v1/blog/search', {\n query: { q, ...query },\n ...(options ? { options } : {}),\n })\n return page(result)\n }\n\n /**\n * Every post, a page at a time, without writing the cursor loop.\n *\n * An async iterator rather than an array, so a site with four thousand posts\n * does not hold four thousand posts in memory to generate a sitemap.\n *\n * for await (const post of cms.posts.all()) { … }\n */\n async *all(\n query: Omit<ListPostsQuery, 'cursor'> = {},\n options?: RequestOptions,\n ): AsyncGenerator<Post, void, undefined> {\n let cursor: string | undefined\n do {\n const result: Page<Post> = await this.list(\n { ...query, ...(cursor ? { cursor } : {}) },\n options,\n )\n for (const item of result.items) yield item\n cursor = result.pagination.nextCursor ?? undefined\n } while (cursor)\n }\n}\n\nclass Categories {\n constructor(private readonly config: TransportConfig) {}\n\n async list(query: ListQuery = {}, options?: RequestOptions): Promise<Page<Category>> {\n return page(\n await request<Category[]>(this.config, 'GET', 'v1/blog/categories', {\n query,\n ...(options ? { options } : {}),\n }),\n )\n }\n\n async get(id: string, options?: RequestOptions): Promise<Category> {\n const result = await request<Category>(\n this.config,\n 'GET',\n `v1/blog/categories/${encodeURIComponent(id)}`,\n { ...(options ? { options } : {}) },\n )\n return result.data\n }\n}\n\nclass Tags {\n constructor(private readonly config: TransportConfig) {}\n\n async list(query: ListQuery = {}, options?: RequestOptions): Promise<Page<Tag>> {\n return page(\n await request<Tag[]>(this.config, 'GET', 'v1/blog/tags', {\n query,\n ...(options ? { options } : {}),\n }),\n )\n }\n\n async get(id: string, options?: RequestOptions): Promise<Tag> {\n const result = await request<Tag>(\n this.config,\n 'GET',\n `v1/blog/tags/${encodeURIComponent(id)}`,\n { ...(options ? { options } : {}) },\n )\n return result.data\n }\n}\n\nclass Authors {\n constructor(private readonly config: TransportConfig) {}\n\n async list(query: ListQuery = {}, options?: RequestOptions): Promise<Page<Author>> {\n return page(\n await request<Author[]>(this.config, 'GET', 'v1/blog/authors', {\n query,\n ...(options ? { options } : {}),\n }),\n )\n }\n\n async get(id: string, options?: RequestOptions): Promise<Author> {\n const result = await request<Author>(\n this.config,\n 'GET',\n `v1/blog/authors/${encodeURIComponent(id)}`,\n { ...(options ? { options } : {}) },\n )\n return result.data\n }\n}\n\n/** Creates a client. The function, rather than `new`, is the documented way in. */\nexport function createCMSKite(options: CMSKiteOptions): CMSKite {\n return new CMSKite(options)\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shapes CMSKite answers with.
|
|
3
|
+
*
|
|
4
|
+
* These mirror the API's own response schemas rather than its database, and
|
|
5
|
+
* that distinction is the SDK's main promise to a customer: a column added,
|
|
6
|
+
* renamed or moved behind the scenes is not their problem. What is documented
|
|
7
|
+
* here is what will keep working.
|
|
8
|
+
*
|
|
9
|
+
* Every interface is open to new optional fields. A minor release may add one;
|
|
10
|
+
* none will remove one or change the meaning of one.
|
|
11
|
+
*/
|
|
12
|
+
type BodyFormat = 'markdown' | 'html' | 'text';
|
|
13
|
+
type PostStatus = 'draft' | 'published' | 'scheduled';
|
|
14
|
+
interface Author {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
slug: string;
|
|
18
|
+
bio: string | null;
|
|
19
|
+
avatarUrl: string | null;
|
|
20
|
+
}
|
|
21
|
+
interface Category {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
slug: string;
|
|
25
|
+
/** `engineering/databases` — the full path, so two "Guides" can be told apart. */
|
|
26
|
+
path: string;
|
|
27
|
+
depth: number;
|
|
28
|
+
}
|
|
29
|
+
interface Tag {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
slug: string;
|
|
33
|
+
}
|
|
34
|
+
interface Media {
|
|
35
|
+
id: string;
|
|
36
|
+
url: string;
|
|
37
|
+
alt: string | null;
|
|
38
|
+
width: number | null;
|
|
39
|
+
height: number | null;
|
|
40
|
+
}
|
|
41
|
+
interface Seo {
|
|
42
|
+
title?: string | null;
|
|
43
|
+
description?: string | null;
|
|
44
|
+
canonicalUrl?: string | null;
|
|
45
|
+
ogImageUrl?: string | null;
|
|
46
|
+
noindex?: boolean;
|
|
47
|
+
}
|
|
48
|
+
interface Post {
|
|
49
|
+
id: string;
|
|
50
|
+
title: string;
|
|
51
|
+
slug: string;
|
|
52
|
+
excerpt: string | null;
|
|
53
|
+
/** Empty when `bodyOmitted` is true, which is how a list stays small. */
|
|
54
|
+
body: string;
|
|
55
|
+
/**
|
|
56
|
+
* Whether the body was left out of this response.
|
|
57
|
+
*
|
|
58
|
+
* List endpoints omit it: twenty posts with full bodies is a payload nobody
|
|
59
|
+
* asked for. Fetching one post always includes it.
|
|
60
|
+
*/
|
|
61
|
+
bodyOmitted: boolean;
|
|
62
|
+
bodyFormat: BodyFormat;
|
|
63
|
+
status: PostStatus;
|
|
64
|
+
author: Author | null;
|
|
65
|
+
category: Category | null;
|
|
66
|
+
tags: Tag[];
|
|
67
|
+
featuredMedia: Media | null;
|
|
68
|
+
publishedAt: string | null;
|
|
69
|
+
scheduledAt: string | null;
|
|
70
|
+
seo: Seo;
|
|
71
|
+
/** Addresses this post used to live at. Useful for issuing redirects. */
|
|
72
|
+
previousSlugs: string[];
|
|
73
|
+
/** Set when the post was reached by one of its old slugs. */
|
|
74
|
+
slugRedirectedFrom: string | null;
|
|
75
|
+
wordCount: number;
|
|
76
|
+
readingMinutes: number;
|
|
77
|
+
createdAt: string;
|
|
78
|
+
updatedAt: string;
|
|
79
|
+
}
|
|
80
|
+
interface Pagination {
|
|
81
|
+
hasNext: boolean;
|
|
82
|
+
/** Opaque. Pass it back as `cursor`; never parse it. */
|
|
83
|
+
nextCursor: string | null;
|
|
84
|
+
limit: number;
|
|
85
|
+
/** Only present when the request asked for it. Counting is not free. */
|
|
86
|
+
total?: number;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* A page of results.
|
|
90
|
+
*
|
|
91
|
+
* `items` rather than `data`, because `data` is what the wire calls it and the
|
|
92
|
+
* wire is an implementation detail. Iterating a page directly is the common
|
|
93
|
+
* case, so the array is the first thing you reach.
|
|
94
|
+
*/
|
|
95
|
+
interface Page<T> {
|
|
96
|
+
items: T[];
|
|
97
|
+
pagination: Pagination;
|
|
98
|
+
}
|
|
99
|
+
type PostSort = 'publishedAt' | '-publishedAt' | 'updatedAt' | '-updatedAt' | 'createdAt' | '-createdAt' | 'title' | '-title';
|
|
100
|
+
interface ListPostsQuery {
|
|
101
|
+
/** Default 20, maximum 100. */
|
|
102
|
+
limit?: number;
|
|
103
|
+
/** From a previous page's `pagination.nextCursor`. */
|
|
104
|
+
cursor?: string;
|
|
105
|
+
status?: PostStatus;
|
|
106
|
+
/** Category slug or id. */
|
|
107
|
+
category?: string;
|
|
108
|
+
/** Tag slug or id. */
|
|
109
|
+
tag?: string;
|
|
110
|
+
author?: string;
|
|
111
|
+
sort?: PostSort;
|
|
112
|
+
/** Free text across title and body. */
|
|
113
|
+
search?: string;
|
|
114
|
+
publishedAfter?: string;
|
|
115
|
+
publishedBefore?: string;
|
|
116
|
+
/** Ask for `pagination.total`. Costs a count; off by default. */
|
|
117
|
+
withTotal?: boolean;
|
|
118
|
+
}
|
|
119
|
+
interface ListQuery {
|
|
120
|
+
limit?: number;
|
|
121
|
+
cursor?: string;
|
|
122
|
+
}
|
|
123
|
+
interface RequestOptions {
|
|
124
|
+
/** Cancels the request. Every method takes one. */
|
|
125
|
+
signal?: AbortSignal;
|
|
126
|
+
/** Overrides the client's default for this call only. */
|
|
127
|
+
timeoutMs?: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface TransportConfig {
|
|
131
|
+
apiKey: string;
|
|
132
|
+
baseUrl: string;
|
|
133
|
+
timeoutMs: number;
|
|
134
|
+
/** Extra headers on every request. For a proxy or a trace id. */
|
|
135
|
+
headers: Record<string, string>;
|
|
136
|
+
fetch: typeof globalThis.fetch;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
interface CMSKiteOptions {
|
|
140
|
+
/**
|
|
141
|
+
* A CMSKite API key. Read-only, and scoped to one project.
|
|
142
|
+
*
|
|
143
|
+
* Safe in a browser bundle: the only scope a key can hold is `blog:read`, it
|
|
144
|
+
* cannot write or delete anything, and a project can restrict which origins
|
|
145
|
+
* may use it. See the security section of the README.
|
|
146
|
+
*/
|
|
147
|
+
apiKey: string;
|
|
148
|
+
/** Only for a self-hosted deployment or a local API. */
|
|
149
|
+
baseUrl?: string;
|
|
150
|
+
/** How long to wait for an answer. Default 10 seconds. */
|
|
151
|
+
timeoutMs?: number;
|
|
152
|
+
/** Extra headers on every request, for a proxy or a trace id. */
|
|
153
|
+
headers?: Record<string, string>;
|
|
154
|
+
/** Supply your own, for a test double or a fetch with retries built in. */
|
|
155
|
+
fetch?: typeof globalThis.fetch;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The CMSKite client.
|
|
159
|
+
*
|
|
160
|
+
* Created once and reused. It holds no connection and no mutable state beyond
|
|
161
|
+
* its configuration, so it is safe to build at module scope and share.
|
|
162
|
+
*
|
|
163
|
+
* const cms = createCMSKite({ apiKey: process.env.CMSKITE_API_KEY! })
|
|
164
|
+
* const { items } = await cms.posts.list({ limit: 10 })
|
|
165
|
+
*
|
|
166
|
+
* Every method takes an optional `{ signal }`, so a request can be cancelled
|
|
167
|
+
* when a component unmounts or a newer search supersedes an older one.
|
|
168
|
+
*/
|
|
169
|
+
declare class CMSKite {
|
|
170
|
+
readonly posts: Posts;
|
|
171
|
+
readonly categories: Categories;
|
|
172
|
+
readonly tags: Tags;
|
|
173
|
+
readonly authors: Authors;
|
|
174
|
+
/** @internal Not part of the public surface; the shape here may change. */
|
|
175
|
+
private readonly config;
|
|
176
|
+
constructor(options: CMSKiteOptions);
|
|
177
|
+
}
|
|
178
|
+
declare class Posts {
|
|
179
|
+
private readonly config;
|
|
180
|
+
constructor(config: TransportConfig);
|
|
181
|
+
/** A page of posts. Published only, newest first, unless you say otherwise. */
|
|
182
|
+
list(query?: ListPostsQuery, options?: RequestOptions): Promise<Page<Post>>;
|
|
183
|
+
/** One post by its id. The body is always included. */
|
|
184
|
+
get(id: string, options?: RequestOptions): Promise<Post>;
|
|
185
|
+
/**
|
|
186
|
+
* One post by its address.
|
|
187
|
+
*
|
|
188
|
+
* This is what a page at `/blog/[slug]` wants. An old slug still resolves,
|
|
189
|
+
* and the answer carries `slugRedirectedFrom` so the page can issue a 301
|
|
190
|
+
* rather than quietly serving two addresses for one post.
|
|
191
|
+
*/
|
|
192
|
+
getBySlug(slug: string, options?: RequestOptions): Promise<Post>;
|
|
193
|
+
/**
|
|
194
|
+
* The same as `getBySlug`, but a missing post is `null` rather than a throw.
|
|
195
|
+
*
|
|
196
|
+
* For a page that renders its own not-found state, which is most of them.
|
|
197
|
+
*/
|
|
198
|
+
findBySlug(slug: string, options?: RequestOptions): Promise<Post | null>;
|
|
199
|
+
/** Posts like this one, chosen by shared tags and category. */
|
|
200
|
+
related(id: string, query?: ListQuery, options?: RequestOptions): Promise<Page<Post>>;
|
|
201
|
+
/** Full-text search across titles and bodies. */
|
|
202
|
+
search(q: string, query?: ListQuery, options?: RequestOptions): Promise<Page<Post>>;
|
|
203
|
+
/**
|
|
204
|
+
* Every post, a page at a time, without writing the cursor loop.
|
|
205
|
+
*
|
|
206
|
+
* An async iterator rather than an array, so a site with four thousand posts
|
|
207
|
+
* does not hold four thousand posts in memory to generate a sitemap.
|
|
208
|
+
*
|
|
209
|
+
* for await (const post of cms.posts.all()) { … }
|
|
210
|
+
*/
|
|
211
|
+
all(query?: Omit<ListPostsQuery, 'cursor'>, options?: RequestOptions): AsyncGenerator<Post, void, undefined>;
|
|
212
|
+
}
|
|
213
|
+
declare class Categories {
|
|
214
|
+
private readonly config;
|
|
215
|
+
constructor(config: TransportConfig);
|
|
216
|
+
list(query?: ListQuery, options?: RequestOptions): Promise<Page<Category>>;
|
|
217
|
+
get(id: string, options?: RequestOptions): Promise<Category>;
|
|
218
|
+
}
|
|
219
|
+
declare class Tags {
|
|
220
|
+
private readonly config;
|
|
221
|
+
constructor(config: TransportConfig);
|
|
222
|
+
list(query?: ListQuery, options?: RequestOptions): Promise<Page<Tag>>;
|
|
223
|
+
get(id: string, options?: RequestOptions): Promise<Tag>;
|
|
224
|
+
}
|
|
225
|
+
declare class Authors {
|
|
226
|
+
private readonly config;
|
|
227
|
+
constructor(config: TransportConfig);
|
|
228
|
+
list(query?: ListQuery, options?: RequestOptions): Promise<Page<Author>>;
|
|
229
|
+
get(id: string, options?: RequestOptions): Promise<Author>;
|
|
230
|
+
}
|
|
231
|
+
/** Creates a client. The function, rather than `new`, is the documented way in. */
|
|
232
|
+
declare function createCMSKite(options: CMSKiteOptions): CMSKite;
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* One error type, so a caller writes one catch.
|
|
236
|
+
*
|
|
237
|
+
* Everything that can go wrong on the way to an answer arrives as a
|
|
238
|
+
* `CMSKiteError`: a refusal from the API, a dropped connection, a timeout, a
|
|
239
|
+
* response that was not JSON. Without that, a `catch` has to tell a `TypeError`
|
|
240
|
+
* from a `SyntaxError` from whatever the API said, and the ones that forget
|
|
241
|
+
* show somebody "Failed to fetch" -- a sentence about our code rather than
|
|
242
|
+
* about their connection.
|
|
243
|
+
*/
|
|
244
|
+
declare class CMSKiteError extends Error {
|
|
245
|
+
/** HTTP status, or 0 when no response arrived at all. */
|
|
246
|
+
readonly status: number;
|
|
247
|
+
/** A stable machine-readable reason. Safe to branch on; see the constants. */
|
|
248
|
+
readonly code: string;
|
|
249
|
+
/** Whatever the API attached. Field-level validation problems live here. */
|
|
250
|
+
readonly details: Record<string, unknown>;
|
|
251
|
+
/** Quote this when asking us about a request. */
|
|
252
|
+
readonly requestId: string | null;
|
|
253
|
+
constructor(status: number, code: string, message: string, details?: Record<string, unknown>, requestId?: string | null);
|
|
254
|
+
/** Nothing you send differently will change the answer. */
|
|
255
|
+
get isClientError(): boolean;
|
|
256
|
+
/** Worth trying again. The client already retried once. */
|
|
257
|
+
get isRetryable(): boolean;
|
|
258
|
+
get isNotFound(): boolean;
|
|
259
|
+
/** The key is missing, wrong, or has been revoked. */
|
|
260
|
+
get isAuthError(): boolean;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* The codes worth branching on, as values rather than as strings scattered
|
|
264
|
+
* through a codebase.
|
|
265
|
+
*/
|
|
266
|
+
declare const ErrorCode: {
|
|
267
|
+
readonly NETWORK: "NETWORK";
|
|
268
|
+
readonly TIMEOUT: "TIMEOUT";
|
|
269
|
+
readonly BAD_RESPONSE: "BAD_RESPONSE";
|
|
270
|
+
readonly UNAUTHENTICATED: "UNAUTHENTICATED";
|
|
271
|
+
readonly FORBIDDEN: "FORBIDDEN";
|
|
272
|
+
readonly NOT_FOUND: "NOT_FOUND";
|
|
273
|
+
readonly RATE_LIMITED: "RATE_LIMITED";
|
|
274
|
+
readonly INVALID_REQUEST: "INVALID_REQUEST";
|
|
275
|
+
};
|
|
276
|
+
type ErrorCodeValue = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
277
|
+
|
|
278
|
+
export { type Author, type BodyFormat, CMSKite, CMSKiteError, type CMSKiteOptions, type Category, ErrorCode, type ErrorCodeValue, type ListPostsQuery, type ListQuery, type Media, type Page, type Pagination, type Post, type PostSort, type PostStatus, type RequestOptions, type Seo, type Tag, createCMSKite };
|