@paragraphcms/client 2.1.0 → 2.2.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 +3 -1
- package/dist/client.d.ts +1 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +86 -132
- package/dist/rate-limiter.d.ts +1 -4
- package/dist/rate-limiter.d.ts.map +1 -1
- package/dist/rate-limiter.js +8 -24
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -10,7 +10,9 @@ Replace this block with the README image.
|
|
|
10
10
|
</p>
|
|
11
11
|
-->
|
|
12
12
|
|
|
13
|
-
`@paragraphcms/client` is a small, typed SDK
|
|
13
|
+
`@paragraphcms/client` is a small, typed SDK for the Paragraph CMS API. It runs in Node.js 18+ and other server-side runtimes that expose `fetch`.
|
|
14
|
+
|
|
15
|
+
Internally, the client uses `ky` for HTTP transport and retries, plus `Bottleneck` for per-instance rate limiting.
|
|
14
16
|
|
|
15
17
|
## Install
|
|
16
18
|
|
package/dist/client.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import type { AiContentResult, AiMetaDescriptionResult, AiMetaNameResult, ApiInfo, ClientOptions, Collection, CollectionListQuery, CollectionMutationResult, CreateCollectionRequest, CreateDataModelRequest, CreateLabelRequest, CreateLocaleRequest, CreatePageRequest, CreatePageTranslationRequest, CreateStatusRequest, DataModel, DataModelListQuery, DataModelMutationResult, DeleteByCodeResult, DeleteResult, GenerateContentRequest, GenerateMetaRequest, GetPageQuery, Label, LabelMutationResult, LabelListQuery, ListResponse, Locale, LocaleMutationResult, Media, MediaDeleteResult, MediaDetail, MediaListQuery, MediaMutationResult, MediaUploadResult, Member, MemberListQuery, Page, PageListQuery, PageSummary, PageMutationResult, PageRestoreResult, PermanentDeleteResult, ReorderLabelsRequest, ReorderResult, ReorderStatusesRequest, RequestOptions, Status, StatusListQuery, StatusMutationResult, UpdateCollectionRequest, UpdateDataModelRequest, UpdateLabelRequest, UpdateMediaRequest, UpdatePageRequest, UpdateStatusRequest, UploadMediaRequest } from "./types.js";
|
|
2
2
|
export declare class Client {
|
|
3
3
|
private readonly apiKey;
|
|
4
|
-
private readonly
|
|
4
|
+
private readonly http;
|
|
5
5
|
private readonly defaultHeaders;
|
|
6
6
|
private readonly timeoutMs?;
|
|
7
7
|
private readonly maxRateLimitRetries;
|
|
8
|
-
private readonly limiter;
|
|
9
8
|
readonly pages: {
|
|
10
9
|
list: (query?: PageListQuery, options?: RequestOptions) => Promise<ListResponse<PageSummary>>;
|
|
11
10
|
create: (body?: CreatePageRequest, options?: RequestOptions) => Promise<PageMutationResult>;
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
CHANGED
|
@@ -1,15 +1,33 @@
|
|
|
1
|
+
import ky, { HTTPError, TimeoutError, } from "ky";
|
|
1
2
|
import { ParagraphApiError, ParagraphClientError, } from "./errors.js";
|
|
2
3
|
import { RequestRateLimiter } from "./rate-limiter.js";
|
|
3
4
|
const API_BASE_URL = "https://api.paragraphcms.com/v1";
|
|
4
5
|
const DEFAULT_REQUESTS_PER_SECOND = 5;
|
|
5
|
-
const DEFAULT_RATE_LIMIT_RETRIES =
|
|
6
|
-
const DEFAULT_RATE_LIMIT_RETRY_DELAY_MS = 1000;
|
|
6
|
+
const DEFAULT_RATE_LIMIT_RETRIES = 3;
|
|
7
7
|
const LOOKUP_PAGE_SIZE = 100;
|
|
8
8
|
const PRESERVED_TRANSFORM_KEYS = new Set([
|
|
9
9
|
"content",
|
|
10
10
|
"editorNode",
|
|
11
11
|
"fields",
|
|
12
12
|
]);
|
|
13
|
+
const RETRYABLE_METHODS = [
|
|
14
|
+
"get",
|
|
15
|
+
"post",
|
|
16
|
+
"patch",
|
|
17
|
+
"delete",
|
|
18
|
+
];
|
|
19
|
+
const SAFE_TRANSIENT_RETRY_METHODS = new Set([
|
|
20
|
+
"GET",
|
|
21
|
+
"DELETE",
|
|
22
|
+
]);
|
|
23
|
+
const RETRYABLE_STATUS_CODES = [
|
|
24
|
+
429,
|
|
25
|
+
500,
|
|
26
|
+
502,
|
|
27
|
+
503,
|
|
28
|
+
504,
|
|
29
|
+
];
|
|
30
|
+
const RETRY_AFTER_STATUS_CODES = [429, 503];
|
|
13
31
|
function resolveFetchImplementation(customFetch) {
|
|
14
32
|
const fetchImpl = customFetch ?? globalThis.fetch;
|
|
15
33
|
if (typeof fetchImpl !== "function") {
|
|
@@ -111,81 +129,20 @@ function createRequestDescriptor(method, url) {
|
|
|
111
129
|
url: url.toString(),
|
|
112
130
|
};
|
|
113
131
|
}
|
|
114
|
-
function
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
didTimeout: () => false,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
const controller = new AbortController();
|
|
123
|
-
let timedOut = false;
|
|
124
|
-
let timeoutId;
|
|
125
|
-
const onAbort = () => {
|
|
126
|
-
controller.abort(signal?.reason);
|
|
127
|
-
};
|
|
128
|
-
if (signal) {
|
|
129
|
-
if (signal.aborted) {
|
|
130
|
-
controller.abort(signal.reason);
|
|
131
|
-
}
|
|
132
|
-
else {
|
|
133
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
if (timeoutMs && timeoutMs > 0) {
|
|
137
|
-
timeoutId = setTimeout(() => {
|
|
138
|
-
timedOut = true;
|
|
139
|
-
controller.abort(new Error(`Request timed out after ${timeoutMs}ms.`));
|
|
140
|
-
}, timeoutMs);
|
|
141
|
-
}
|
|
142
|
-
return {
|
|
143
|
-
signal: controller.signal,
|
|
144
|
-
cleanup: () => {
|
|
145
|
-
if (timeoutId !== undefined) {
|
|
146
|
-
clearTimeout(timeoutId);
|
|
147
|
-
}
|
|
148
|
-
if (signal) {
|
|
149
|
-
signal.removeEventListener("abort", onAbort);
|
|
150
|
-
}
|
|
151
|
-
},
|
|
152
|
-
didTimeout: () => timedOut,
|
|
153
|
-
};
|
|
132
|
+
function toKyInput(url) {
|
|
133
|
+
const path = url.pathname
|
|
134
|
+
.replace(/^\/v1/, "")
|
|
135
|
+
.replace(/^\/+/, "");
|
|
136
|
+
return `${path}${url.search}`;
|
|
154
137
|
}
|
|
155
|
-
function
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
return new Promise((resolve, reject) => {
|
|
160
|
-
let timeoutId;
|
|
161
|
-
const cleanup = () => {
|
|
162
|
-
if (timeoutId !== undefined) {
|
|
163
|
-
clearTimeout(timeoutId);
|
|
164
|
-
}
|
|
165
|
-
if (signal) {
|
|
166
|
-
signal.removeEventListener("abort", onAbort);
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
const onAbort = () => {
|
|
170
|
-
cleanup();
|
|
171
|
-
reject(signal?.reason ??
|
|
172
|
-
new DOMException("The operation was aborted.", "AbortError"));
|
|
173
|
-
};
|
|
174
|
-
if (signal) {
|
|
175
|
-
if (signal.aborted) {
|
|
176
|
-
onAbort();
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
180
|
-
}
|
|
181
|
-
timeoutId = setTimeout(() => {
|
|
182
|
-
cleanup();
|
|
183
|
-
resolve();
|
|
184
|
-
}, delayMs);
|
|
185
|
-
});
|
|
138
|
+
function canRetryTransientRequest(method) {
|
|
139
|
+
return (typeof method === "string" &&
|
|
140
|
+
SAFE_TRANSIENT_RETRY_METHODS.has(method.toUpperCase()));
|
|
186
141
|
}
|
|
187
142
|
function isAbortError(error) {
|
|
188
|
-
return (error instanceof DOMException &&
|
|
143
|
+
return ((error instanceof DOMException &&
|
|
144
|
+
error.name === "AbortError") ||
|
|
145
|
+
(error instanceof Error && error.name === "AbortError"));
|
|
189
146
|
}
|
|
190
147
|
function resolveMaxRateLimitRetries(value, fallback = DEFAULT_RATE_LIMIT_RETRIES) {
|
|
191
148
|
const resolved = value ?? fallback;
|
|
@@ -195,23 +152,30 @@ function resolveMaxRateLimitRetries(value, fallback = DEFAULT_RATE_LIMIT_RETRIES
|
|
|
195
152
|
}
|
|
196
153
|
return resolved;
|
|
197
154
|
}
|
|
198
|
-
function
|
|
199
|
-
|
|
200
|
-
return undefined;
|
|
201
|
-
}
|
|
202
|
-
const seconds = Number(headerValue);
|
|
203
|
-
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
204
|
-
return Math.ceil(seconds * 1000);
|
|
205
|
-
}
|
|
206
|
-
const retryAt = Date.parse(headerValue);
|
|
207
|
-
if (Number.isNaN(retryAt)) {
|
|
208
|
-
return undefined;
|
|
209
|
-
}
|
|
210
|
-
return Math.max(0, retryAt - Date.now());
|
|
155
|
+
function resolveTotalTimeout(timeoutMs) {
|
|
156
|
+
return timeoutMs && timeoutMs > 0 ? timeoutMs : false;
|
|
211
157
|
}
|
|
212
|
-
function
|
|
213
|
-
return
|
|
214
|
-
|
|
158
|
+
function createRetryOptions(limit) {
|
|
159
|
+
return {
|
|
160
|
+
limit,
|
|
161
|
+
methods: [...RETRYABLE_METHODS],
|
|
162
|
+
statusCodes: [...RETRYABLE_STATUS_CODES],
|
|
163
|
+
afterStatusCodes: [...RETRY_AFTER_STATUS_CODES],
|
|
164
|
+
retryOnTimeout: false,
|
|
165
|
+
shouldRetry: ({ error }) => {
|
|
166
|
+
if (error instanceof HTTPError) {
|
|
167
|
+
if (error.response.status === 429) {
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
return canRetryTransientRequest(error.request.method)
|
|
171
|
+
? undefined
|
|
172
|
+
: false;
|
|
173
|
+
}
|
|
174
|
+
return canRetryTransientRequest(error.request?.method)
|
|
175
|
+
? undefined
|
|
176
|
+
: false;
|
|
177
|
+
},
|
|
178
|
+
};
|
|
215
179
|
}
|
|
216
180
|
function toBlobPart(file) {
|
|
217
181
|
if (typeof File !== "undefined" && file instanceof File) {
|
|
@@ -300,11 +264,10 @@ function createUploadFormData(input) {
|
|
|
300
264
|
}
|
|
301
265
|
export class Client {
|
|
302
266
|
apiKey;
|
|
303
|
-
|
|
267
|
+
http;
|
|
304
268
|
defaultHeaders;
|
|
305
269
|
timeoutMs;
|
|
306
270
|
maxRateLimitRetries;
|
|
307
|
-
limiter;
|
|
308
271
|
pages = {
|
|
309
272
|
list: (query, options) => this.listPages(query, options),
|
|
310
273
|
create: (body = {}, options) => this.requestData("POST", "/pages", {
|
|
@@ -524,11 +487,16 @@ export class Client {
|
|
|
524
487
|
throw new ParagraphClientError("`apiKey` is required.");
|
|
525
488
|
}
|
|
526
489
|
this.apiKey = options.apiKey.trim();
|
|
527
|
-
this.fetchImpl = resolveFetchImplementation(options.fetch);
|
|
528
490
|
this.defaultHeaders = new Headers(options.headers);
|
|
529
491
|
this.timeoutMs = options.timeoutMs;
|
|
530
492
|
this.maxRateLimitRetries = resolveMaxRateLimitRetries(options.maxRateLimitRetries);
|
|
531
|
-
|
|
493
|
+
const fetchImpl = resolveFetchImplementation(options.fetch);
|
|
494
|
+
const limiter = new RequestRateLimiter(options.maxRequestsPerSecond ?? DEFAULT_REQUESTS_PER_SECOND);
|
|
495
|
+
this.http = ky.create({
|
|
496
|
+
fetch: (input, init) => limiter.schedule(() => fetchImpl(input, init)),
|
|
497
|
+
prefix: API_BASE_URL,
|
|
498
|
+
timeout: false,
|
|
499
|
+
});
|
|
532
500
|
}
|
|
533
501
|
getInfo(options) {
|
|
534
502
|
return this.requestData("GET", "", { options });
|
|
@@ -651,6 +619,7 @@ export class Client {
|
|
|
651
619
|
}
|
|
652
620
|
requestJson(method, path, config) {
|
|
653
621
|
const url = buildUrl(path, config?.query);
|
|
622
|
+
const kyInput = toKyInput(url);
|
|
654
623
|
const request = createRequestDescriptor(method, url);
|
|
655
624
|
const headers = new Headers(this.defaultHeaders);
|
|
656
625
|
const timeoutMs = config?.options?.timeoutMs ?? this.timeoutMs;
|
|
@@ -678,27 +647,28 @@ export class Client {
|
|
|
678
647
|
if (config?.formData) {
|
|
679
648
|
headers.delete("content-type");
|
|
680
649
|
}
|
|
681
|
-
const requestSignal = createRequestSignal(config?.options?.signal, timeoutMs);
|
|
682
650
|
return (async () => {
|
|
683
|
-
let retryCount = 0;
|
|
684
651
|
try {
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
652
|
+
const response = await this.http(kyInput, {
|
|
653
|
+
method,
|
|
654
|
+
headers,
|
|
655
|
+
body,
|
|
656
|
+
signal: config?.options?.signal,
|
|
657
|
+
timeout: false,
|
|
658
|
+
totalTimeout: resolveTotalTimeout(timeoutMs),
|
|
659
|
+
retry: createRetryOptions(maxRateLimitRetries),
|
|
660
|
+
});
|
|
661
|
+
const payload = await parseResponse(response);
|
|
662
|
+
return toSdkPayload(payload);
|
|
663
|
+
}
|
|
664
|
+
catch (error) {
|
|
665
|
+
if (error instanceof HTTPError) {
|
|
666
|
+
const responseHeaders = new Headers(error.response.headers);
|
|
667
|
+
const payload = error.data;
|
|
668
|
+
throw isApiErrorPayload(payload)
|
|
699
669
|
? new ParagraphApiError({
|
|
700
670
|
body: toSdkPayload(payload),
|
|
701
|
-
status: response.status,
|
|
671
|
+
status: error.response.status,
|
|
702
672
|
code: payload.error.code,
|
|
703
673
|
message: payload.error.message,
|
|
704
674
|
details: toSdkPayload(payload.error.details),
|
|
@@ -706,32 +676,19 @@ export class Client {
|
|
|
706
676
|
request,
|
|
707
677
|
})
|
|
708
678
|
: new ParagraphApiError({
|
|
709
|
-
status: response.status,
|
|
710
|
-
code: response.status === 401
|
|
679
|
+
status: error.response.status,
|
|
680
|
+
code: error.response.status === 401
|
|
711
681
|
? "unauthorized"
|
|
712
682
|
: "requestFailed",
|
|
713
683
|
message: typeof payload === "string" && payload.length > 0
|
|
714
684
|
? payload
|
|
715
|
-
: response.statusText ||
|
|
685
|
+
: error.response.statusText ||
|
|
686
|
+
"Request failed.",
|
|
716
687
|
headers: responseHeaders,
|
|
717
688
|
request,
|
|
718
689
|
});
|
|
719
|
-
if (response.status === 429 &&
|
|
720
|
-
retryCount < maxRateLimitRetries) {
|
|
721
|
-
const retryDelayMs = resolveRateLimitRetryDelayMs(responseHeaders, retryCount);
|
|
722
|
-
retryCount += 1;
|
|
723
|
-
await waitForDelay(retryDelayMs, requestSignal.signal);
|
|
724
|
-
continue;
|
|
725
|
-
}
|
|
726
|
-
throw apiError;
|
|
727
690
|
}
|
|
728
|
-
|
|
729
|
-
catch (error) {
|
|
730
|
-
if (error instanceof ParagraphApiError ||
|
|
731
|
-
error instanceof ParagraphClientError) {
|
|
732
|
-
throw error;
|
|
733
|
-
}
|
|
734
|
-
if (requestSignal.didTimeout()) {
|
|
691
|
+
if (error instanceof TimeoutError) {
|
|
735
692
|
throw new ParagraphClientError(`Request timed out after ${timeoutMs}ms.`, {
|
|
736
693
|
request,
|
|
737
694
|
cause: error,
|
|
@@ -746,9 +703,6 @@ export class Client {
|
|
|
746
703
|
cause: error,
|
|
747
704
|
});
|
|
748
705
|
}
|
|
749
|
-
finally {
|
|
750
|
-
requestSignal.cleanup();
|
|
751
|
-
}
|
|
752
706
|
})();
|
|
753
707
|
}
|
|
754
708
|
}
|
package/dist/rate-limiter.d.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
export declare class RequestRateLimiter {
|
|
2
|
-
private readonly
|
|
3
|
-
private nextAvailableAt;
|
|
4
|
-
private scheduling;
|
|
2
|
+
private readonly limiter;
|
|
5
3
|
constructor(requestsPerSecond: number);
|
|
6
4
|
schedule<T>(task: () => Promise<T>): Promise<T>;
|
|
7
|
-
private reserveSlot;
|
|
8
5
|
}
|
|
9
6
|
//# sourceMappingURL=rate-limiter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../src/rate-limiter.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../src/rate-limiter.ts"],"names":[],"mappings":"AAKA,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAa;gBAEzB,iBAAiB,EAAE,MAAM;IAgBrC,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC;CAGnC"}
|
package/dist/rate-limiter.js
CHANGED
|
@@ -1,35 +1,19 @@
|
|
|
1
|
+
import Bottleneck from "bottleneck";
|
|
1
2
|
import { ParagraphClientError } from "./errors.js";
|
|
2
|
-
|
|
3
|
-
return new Promise((resolve) => {
|
|
4
|
-
setTimeout(resolve, ms);
|
|
5
|
-
});
|
|
6
|
-
}
|
|
3
|
+
const DEFAULT_MAX_CONCURRENT_REQUESTS = 3;
|
|
7
4
|
export class RequestRateLimiter {
|
|
8
|
-
|
|
9
|
-
nextAvailableAt = 0;
|
|
10
|
-
scheduling = Promise.resolve();
|
|
5
|
+
limiter;
|
|
11
6
|
constructor(requestsPerSecond) {
|
|
12
7
|
if (!Number.isFinite(requestsPerSecond) ||
|
|
13
8
|
requestsPerSecond <= 0) {
|
|
14
9
|
throw new ParagraphClientError("`maxRequestsPerSecond` must be a positive number.");
|
|
15
10
|
}
|
|
16
|
-
this.
|
|
11
|
+
this.limiter = new Bottleneck({
|
|
12
|
+
minTime: Math.ceil(1000 / requestsPerSecond),
|
|
13
|
+
maxConcurrent: DEFAULT_MAX_CONCURRENT_REQUESTS,
|
|
14
|
+
});
|
|
17
15
|
}
|
|
18
16
|
schedule(task) {
|
|
19
|
-
|
|
20
|
-
return slot.then(task);
|
|
21
|
-
}
|
|
22
|
-
reserveSlot() {
|
|
23
|
-
const slot = this.scheduling.then(async () => {
|
|
24
|
-
const now = Date.now();
|
|
25
|
-
const scheduledAt = Math.max(now, this.nextAvailableAt);
|
|
26
|
-
const delayMs = scheduledAt - now;
|
|
27
|
-
this.nextAvailableAt = scheduledAt + this.minIntervalMs;
|
|
28
|
-
if (delayMs > 0) {
|
|
29
|
-
await sleep(delayMs);
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
this.scheduling = slot.catch(() => { });
|
|
33
|
-
return slot;
|
|
17
|
+
return this.limiter.schedule(task);
|
|
34
18
|
}
|
|
35
19
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paragraphcms/client",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Official TypeScript client for the Paragraph CMS API.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Paragraph CMS",
|
|
@@ -41,5 +41,9 @@
|
|
|
41
41
|
],
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"typescript": "^5.9.3"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"bottleneck": "^2.19.5",
|
|
47
|
+
"ky": "^2.0.2"
|
|
44
48
|
}
|
|
45
49
|
}
|