@izak0s/spacebring-api 1.0.0 → 1.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 +33 -15
- package/dist/index.cjs +64 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8168 -14157
- package/dist/index.d.ts +8168 -14157
- package/dist/index.js +64 -36
- package/dist/index.js.map +1 -1
- package/package.json +11 -8
package/README.md
CHANGED
|
@@ -46,26 +46,38 @@ const sb = new Spacebring({
|
|
|
46
46
|
// fetch: customFetch, // inject your own fetch (tests, proxies)
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
await sb.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
async function main() {
|
|
50
|
+
// Single-property envelopes are unwrapped: entities and plain arrays come back directly
|
|
51
|
+
const invoice = await sb.billing.invoices.get(invoiceId);
|
|
52
|
+
const locations = await sb.locations.list();
|
|
53
|
+
await sb.visitors.visits.checkIn({ locationRef, visitRef });
|
|
54
|
+
|
|
55
|
+
// Paginated lists return the page envelope, so nextPageToken stays available
|
|
56
|
+
const { benefits, nextPageToken } = await sb.benefits.list({ locationRef });
|
|
57
|
+
|
|
58
|
+
// ...or let iterate() walk nextPageToken for you; breaking early stops fetching
|
|
59
|
+
for await (const booking of sb.resources.bookings.iterate({ locationRef })) {
|
|
60
|
+
console.log(booking.id);
|
|
61
|
+
}
|
|
56
62
|
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
// Endpoints returning multiple payloads keep the envelope
|
|
64
|
+
const { invoice: paid, payment } = await sb.billing.invoices.pay(invoiceId, {
|
|
65
|
+
paymentMethod: { type: "stripe" },
|
|
66
|
+
});
|
|
60
67
|
}
|
|
61
68
|
|
|
62
|
-
|
|
63
|
-
const { invoice: paid, payment } = await sb.billing.invoices.pay(invoiceId, {
|
|
64
|
-
paymentMethod: { type: "stripe" },
|
|
65
|
-
});
|
|
69
|
+
main().catch(console.error);
|
|
66
70
|
```
|
|
67
71
|
|
|
68
|
-
|
|
72
|
+
Entity types are exported by name — `import type { Booking, Invoice, Membership } from "@izak0s/spacebring-api"` — matching what the methods return (`get`/`create`/`update` resolve to the entity, `iterate()` yields it). Lower-level helpers too: `SpacebringConfig`, `SpacebringResources`, and the raw spec types `paths` / `components` / `operations`.
|
|
73
|
+
|
|
74
|
+
### Data formats
|
|
75
|
+
|
|
76
|
+
Values are passed through exactly as the API sends them — no runtime conversion:
|
|
77
|
+
|
|
78
|
+
- **Dates** (`createDate`, `startDate`, …) are ISO 8601 strings — wrap in `new Date(booking.startDate)` when you need a `Date`.
|
|
79
|
+
- **Money** (`amount`, `price`, …) arrives as decimal floats. Fine for display; for accounting arithmetic convert to integer cents first to avoid floating-point drift.
|
|
80
|
+
- **IDs** (`id`, `*Ref`) are UUID strings.
|
|
69
81
|
|
|
70
82
|
---
|
|
71
83
|
|
|
@@ -73,6 +85,8 @@ TypeScript helpers are exported too: `SpacebringConfig`, `SpacebringResources`,
|
|
|
73
85
|
|
|
74
86
|
HTTP Basic with your **Client ID** and **Client Secret** from **Spacebring → [Network] → Network Settings → Developers**. The client builds the `Authorization: Basic …` header for you. The API's OAuth2 flow is not currently supported.
|
|
75
87
|
|
|
88
|
+
For development without touching live data, Spacebring offers a [test environment](https://www.spacebring.com/docs/administration/test-environment) (Network settings → Billing add-on) with free sandbox API credentials that work with this client unchanged.
|
|
89
|
+
|
|
76
90
|
---
|
|
77
91
|
|
|
78
92
|
## Error handling
|
|
@@ -93,6 +107,10 @@ try {
|
|
|
93
107
|
|
|
94
108
|
Malformed successes are covered too: a 2xx with an empty or incomplete body throws a `SpacebringError` (never a bare `TypeError`), and `iterate()` throws instead of looping forever if the API repeats a page token.
|
|
95
109
|
|
|
110
|
+
### Rate limits
|
|
111
|
+
|
|
112
|
+
The API allows **10 requests per second**. Rate-limited requests (429) are retried automatically — up to 3 times, honoring `Retry-After` or backing off exponentially — so `iterate()` survives the limit out of the box. Tune or disable via `maxRetries` in the config (`maxRetries: 0` turns it off); a 429 that persists past the retries is thrown as a normal `SpacebringError`.
|
|
113
|
+
|
|
96
114
|
---
|
|
97
115
|
|
|
98
116
|
## Escape hatch
|
package/dist/index.cjs
CHANGED
|
@@ -40,24 +40,27 @@ function createClient(clientOptions) {
|
|
|
40
40
|
fetch: baseFetch = globalThis.fetch,
|
|
41
41
|
querySerializer: globalQuerySerializer,
|
|
42
42
|
bodySerializer: globalBodySerializer,
|
|
43
|
+
pathSerializer: globalPathSerializer,
|
|
43
44
|
headers: baseHeaders,
|
|
44
45
|
requestInitExt = void 0,
|
|
45
46
|
...baseOptions
|
|
46
47
|
} = { ...clientOptions };
|
|
47
48
|
requestInitExt = supportsRequestInitExt() ? requestInitExt : void 0;
|
|
48
49
|
baseUrl = removeTrailingSlash(baseUrl);
|
|
49
|
-
const
|
|
50
|
+
const globalMiddlewares = [];
|
|
50
51
|
async function coreFetch(schemaPath, fetchOptions) {
|
|
51
52
|
const {
|
|
52
53
|
baseUrl: localBaseUrl,
|
|
53
54
|
fetch = baseFetch,
|
|
54
|
-
Request = CustomRequest,
|
|
55
|
+
Request: Request2 = CustomRequest,
|
|
55
56
|
headers,
|
|
56
57
|
params = {},
|
|
57
58
|
parseAs = "json",
|
|
58
59
|
querySerializer: requestQuerySerializer,
|
|
59
60
|
bodySerializer = globalBodySerializer ?? defaultBodySerializer,
|
|
61
|
+
pathSerializer: requestPathSerializer,
|
|
60
62
|
body,
|
|
63
|
+
middleware: requestMiddlewares = [],
|
|
61
64
|
...init
|
|
62
65
|
} = fetchOptions || {};
|
|
63
66
|
let finalBaseUrl = baseUrl;
|
|
@@ -71,6 +74,7 @@ function createClient(clientOptions) {
|
|
|
71
74
|
...requestQuerySerializer
|
|
72
75
|
});
|
|
73
76
|
}
|
|
77
|
+
const pathSerializer = requestPathSerializer || globalPathSerializer || defaultPathSerializer;
|
|
74
78
|
const serializedBody = body === void 0 ? void 0 : bodySerializer(
|
|
75
79
|
body,
|
|
76
80
|
// Note: we declare mergeHeaders() both here and below because it’s a bit of a chicken-or-egg situation:
|
|
@@ -90,6 +94,7 @@ function createClient(clientOptions) {
|
|
|
90
94
|
headers,
|
|
91
95
|
params.header
|
|
92
96
|
);
|
|
97
|
+
const finalMiddlewares = [...globalMiddlewares, ...requestMiddlewares];
|
|
93
98
|
const requestInit = {
|
|
94
99
|
redirect: "follow",
|
|
95
100
|
...baseOptions,
|
|
@@ -99,8 +104,8 @@ function createClient(clientOptions) {
|
|
|
99
104
|
};
|
|
100
105
|
let id;
|
|
101
106
|
let options;
|
|
102
|
-
let request = new
|
|
103
|
-
createFinalURL(schemaPath, { baseUrl: finalBaseUrl, params, querySerializer }),
|
|
107
|
+
let request = new Request2(
|
|
108
|
+
createFinalURL(schemaPath, { baseUrl: finalBaseUrl, params, querySerializer, pathSerializer }),
|
|
104
109
|
requestInit
|
|
105
110
|
);
|
|
106
111
|
let response;
|
|
@@ -109,16 +114,17 @@ function createClient(clientOptions) {
|
|
|
109
114
|
request[key] = init[key];
|
|
110
115
|
}
|
|
111
116
|
}
|
|
112
|
-
if (
|
|
117
|
+
if (finalMiddlewares.length) {
|
|
113
118
|
id = randomID();
|
|
114
119
|
options = Object.freeze({
|
|
115
120
|
baseUrl: finalBaseUrl,
|
|
116
121
|
fetch,
|
|
117
122
|
parseAs,
|
|
118
123
|
querySerializer,
|
|
119
|
-
bodySerializer
|
|
124
|
+
bodySerializer,
|
|
125
|
+
pathSerializer
|
|
120
126
|
});
|
|
121
|
-
for (const m of
|
|
127
|
+
for (const m of finalMiddlewares) {
|
|
122
128
|
if (m && typeof m === "object" && typeof m.onRequest === "function") {
|
|
123
129
|
const result = await m.onRequest({
|
|
124
130
|
request,
|
|
@@ -128,7 +134,7 @@ function createClient(clientOptions) {
|
|
|
128
134
|
id
|
|
129
135
|
});
|
|
130
136
|
if (result) {
|
|
131
|
-
if (result instanceof
|
|
137
|
+
if (result instanceof Request2) {
|
|
132
138
|
request = result;
|
|
133
139
|
} else if (result instanceof Response) {
|
|
134
140
|
response = result;
|
|
@@ -145,9 +151,9 @@ function createClient(clientOptions) {
|
|
|
145
151
|
response = await fetch(request, requestInitExt);
|
|
146
152
|
} catch (error2) {
|
|
147
153
|
let errorAfterMiddleware = error2;
|
|
148
|
-
if (
|
|
149
|
-
for (let i =
|
|
150
|
-
const m =
|
|
154
|
+
if (finalMiddlewares.length) {
|
|
155
|
+
for (let i = finalMiddlewares.length - 1; i >= 0; i--) {
|
|
156
|
+
const m = finalMiddlewares[i];
|
|
151
157
|
if (m && typeof m === "object" && typeof m.onError === "function") {
|
|
152
158
|
const result = await m.onError({
|
|
153
159
|
request,
|
|
@@ -176,9 +182,9 @@ function createClient(clientOptions) {
|
|
|
176
182
|
throw errorAfterMiddleware;
|
|
177
183
|
}
|
|
178
184
|
}
|
|
179
|
-
if (
|
|
180
|
-
for (let i =
|
|
181
|
-
const m =
|
|
185
|
+
if (finalMiddlewares.length) {
|
|
186
|
+
for (let i = finalMiddlewares.length - 1; i >= 0; i--) {
|
|
187
|
+
const m = finalMiddlewares[i];
|
|
182
188
|
if (m && typeof m === "object" && typeof m.onResponse === "function") {
|
|
183
189
|
const result = await m.onResponse({
|
|
184
190
|
request,
|
|
@@ -198,14 +204,22 @@ function createClient(clientOptions) {
|
|
|
198
204
|
}
|
|
199
205
|
}
|
|
200
206
|
}
|
|
201
|
-
|
|
207
|
+
const contentLength = response.headers.get("Content-Length");
|
|
208
|
+
if (response.status === 204 || request.method === "HEAD" || contentLength === "0" && !response.headers.get("Transfer-Encoding")?.includes("chunked")) {
|
|
202
209
|
return response.ok ? { data: void 0, response } : { error: void 0, response };
|
|
203
210
|
}
|
|
204
211
|
if (response.ok) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
212
|
+
const getResponseData = async () => {
|
|
213
|
+
if (parseAs === "stream") {
|
|
214
|
+
return response.body;
|
|
215
|
+
}
|
|
216
|
+
if (parseAs === "json" && !contentLength) {
|
|
217
|
+
const raw = await response.text();
|
|
218
|
+
return raw ? JSON.parse(raw) : void 0;
|
|
219
|
+
}
|
|
220
|
+
return await response[parseAs]();
|
|
221
|
+
};
|
|
222
|
+
return { data: await getResponseData(), response };
|
|
209
223
|
}
|
|
210
224
|
let error = await response.text();
|
|
211
225
|
try {
|
|
@@ -259,15 +273,15 @@ function createClient(clientOptions) {
|
|
|
259
273
|
if (typeof m !== "object" || !("onRequest" in m || "onResponse" in m || "onError" in m)) {
|
|
260
274
|
throw new Error("Middleware must be an object with one of `onRequest()`, `onResponse() or `onError()`");
|
|
261
275
|
}
|
|
262
|
-
|
|
276
|
+
globalMiddlewares.push(m);
|
|
263
277
|
}
|
|
264
278
|
},
|
|
265
279
|
/** Unregister middleware */
|
|
266
280
|
eject(...middleware) {
|
|
267
281
|
for (const m of middleware) {
|
|
268
|
-
const i =
|
|
282
|
+
const i = globalMiddlewares.indexOf(m);
|
|
269
283
|
if (i !== -1) {
|
|
270
|
-
|
|
284
|
+
globalMiddlewares.splice(i, 1);
|
|
271
285
|
}
|
|
272
286
|
}
|
|
273
287
|
}
|
|
@@ -448,7 +462,7 @@ function defaultBodySerializer(body, headers) {
|
|
|
448
462
|
function createFinalURL(pathname, options) {
|
|
449
463
|
let finalURL = `${options.baseUrl}${pathname}`;
|
|
450
464
|
if (options.params?.path) {
|
|
451
|
-
finalURL =
|
|
465
|
+
finalURL = options.pathSerializer(finalURL, options.params.path);
|
|
452
466
|
}
|
|
453
467
|
let search = options.querySerializer(options.params.query ?? {});
|
|
454
468
|
if (search.startsWith("?")) {
|
|
@@ -1708,8 +1722,8 @@ function createSubscriptions(client, defaults) {
|
|
|
1708
1722
|
*
|
|
1709
1723
|
* Retrieve a certain subscription.
|
|
1710
1724
|
*/
|
|
1711
|
-
async get(
|
|
1712
|
-
return unwrapProp(await client.GET("/subscriptions/v1/{
|
|
1725
|
+
async get(subscriptionId) {
|
|
1726
|
+
return unwrapProp(await client.GET("/subscriptions/v1/{subscriptionId}", { params: { path: { subscriptionId } } }), "subscription");
|
|
1713
1727
|
},
|
|
1714
1728
|
/**
|
|
1715
1729
|
* Create a subscription
|
|
@@ -1724,28 +1738,28 @@ function createSubscriptions(client, defaults) {
|
|
|
1724
1738
|
*
|
|
1725
1739
|
* Update a certain subscription.
|
|
1726
1740
|
*/
|
|
1727
|
-
async update(
|
|
1728
|
-
return unwrap(await client.PATCH("/subscriptions/v1/{
|
|
1741
|
+
async update(subscriptionId, body) {
|
|
1742
|
+
return unwrap(await client.PATCH("/subscriptions/v1/{subscriptionId}", { params: { path: { subscriptionId } }, body }));
|
|
1729
1743
|
},
|
|
1730
1744
|
/**
|
|
1731
1745
|
* Delete a subscription
|
|
1732
1746
|
*
|
|
1733
1747
|
* Delete a certain subscription.
|
|
1734
1748
|
*/
|
|
1735
|
-
async delete(
|
|
1736
|
-
return unwrap(await client.DELETE("/subscriptions/v1/{
|
|
1749
|
+
async delete(subscriptionId) {
|
|
1750
|
+
return unwrap(await client.DELETE("/subscriptions/v1/{subscriptionId}", { params: { path: { subscriptionId } } }));
|
|
1737
1751
|
},
|
|
1738
1752
|
/** Create a subscription item */
|
|
1739
|
-
async createItem(
|
|
1740
|
-
return unwrapProp(await client.POST("/subscriptions/v1/{
|
|
1753
|
+
async createItem(subscriptionId, body) {
|
|
1754
|
+
return unwrapProp(await client.POST("/subscriptions/v1/{subscriptionId}/items", { params: { path: { subscriptionId } }, body }), "subscription");
|
|
1741
1755
|
},
|
|
1742
1756
|
/** Delete a subscription item */
|
|
1743
|
-
async deleteItem(
|
|
1744
|
-
return unwrap(await client.DELETE("/subscriptions/v1/{
|
|
1757
|
+
async deleteItem(subscriptionId, itemId) {
|
|
1758
|
+
return unwrap(await client.DELETE("/subscriptions/v1/{subscriptionId}/items/{itemId}", { params: { path: { subscriptionId, itemId } } }));
|
|
1745
1759
|
},
|
|
1746
1760
|
/** Update a subscription item */
|
|
1747
|
-
async updateItem(
|
|
1748
|
-
return unwrap(await client.PATCH("/subscriptions/v1/{
|
|
1761
|
+
async updateItem(subscriptionId, itemId, body) {
|
|
1762
|
+
return unwrap(await client.PATCH("/subscriptions/v1/{subscriptionId}/items/{itemId}", { params: { path: { subscriptionId, itemId } }, body }));
|
|
1749
1763
|
}
|
|
1750
1764
|
};
|
|
1751
1765
|
}
|
|
@@ -2075,11 +2089,25 @@ var Spacebring = class {
|
|
|
2075
2089
|
this.raw = createClient({
|
|
2076
2090
|
baseUrl: config.baseUrl ?? "https://api.spacebring.com",
|
|
2077
2091
|
headers,
|
|
2078
|
-
fetch: config.fetch
|
|
2092
|
+
fetch: withRetry(config.fetch ?? globalThis.fetch, config.maxRetries ?? 3)
|
|
2079
2093
|
});
|
|
2080
2094
|
Object.assign(this, createResources2(this.raw, { networkId: config.networkId }));
|
|
2081
2095
|
}
|
|
2082
2096
|
};
|
|
2097
|
+
function withRetry(fetchImpl, maxRetries) {
|
|
2098
|
+
if (maxRetries <= 0) return fetchImpl;
|
|
2099
|
+
return async (input, init) => {
|
|
2100
|
+
const request = input instanceof Request && init === void 0 ? input : new Request(input, init);
|
|
2101
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
2102
|
+
const response = await fetchImpl(request.clone());
|
|
2103
|
+
if (response.status !== 429 || attempt >= maxRetries) return response;
|
|
2104
|
+
const retryAfterHeader = response.headers.get("retry-after");
|
|
2105
|
+
const retryAfter = retryAfterHeader === null ? Number.NaN : Number(retryAfterHeader);
|
|
2106
|
+
const delayMs = Number.isFinite(retryAfter) && retryAfter >= 0 ? Math.min(retryAfter * 1e3, 6e4) : Math.min(250 * 2 ** attempt + Math.random() * 100, 5e3);
|
|
2107
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
2108
|
+
}
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2083
2111
|
function toBase64(value) {
|
|
2084
2112
|
if (typeof Buffer !== "undefined") {
|
|
2085
2113
|
return Buffer.from(value, "utf8").toString("base64");
|