@studiolayer/client 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/README.md +155 -0
- package/dist/index.cjs +286 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +230 -0
- package/dist/index.d.ts +230 -0
- package/dist/index.js +279 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# @studiolayer/client
|
|
2
|
+
|
|
3
|
+
Typed client for the StudioLayer **content API**. Read and write a project's
|
|
4
|
+
node datasets from any website or app: headless-CMS style, and beyond.
|
|
5
|
+
|
|
6
|
+
Your site talks to project **nodes** over a small, key-authenticated REST
|
|
7
|
+
surface (`/api/content/*`). A **project API key** (`slk_...`) grants read and/or
|
|
8
|
+
write access to specific nodes and datasets. This package wraps that surface in
|
|
9
|
+
a fully typed client with built-in read caching.
|
|
10
|
+
|
|
11
|
+
- Zero dependencies, works in Node 18+ and the browser (the API sends permissive CORS).
|
|
12
|
+
- Dual ESM / CommonJS build, full TypeScript types.
|
|
13
|
+
- Read caching with TTL, automatic invalidation on writes, and a `clearCache()` method.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @studiolayer/client
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { createClient } from '@studiolayer/client'
|
|
25
|
+
|
|
26
|
+
const studio = createClient({
|
|
27
|
+
apiKey: process.env.STUDIOLAYER_API_KEY!, // slk_...
|
|
28
|
+
baseUrl: 'https://studio.example.com', // your StudioLayer server
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// Discover what this key can reach.
|
|
32
|
+
const { nodes } = await studio.schema()
|
|
33
|
+
|
|
34
|
+
// List records from a dataset.
|
|
35
|
+
const posts = await studio.listRecords('blog', 'posts')
|
|
36
|
+
|
|
37
|
+
// Or use the fluent, typed handle.
|
|
38
|
+
interface Post { title: string, slug: string, body: string, published: boolean }
|
|
39
|
+
const blog = studio.dataset<Post>('blog', 'posts')
|
|
40
|
+
|
|
41
|
+
const all = await blog.list()
|
|
42
|
+
const one = await blog.get('rec_abc123')
|
|
43
|
+
const created = await blog.create({ title: 'Hello', slug: 'hello', body: '...', published: true })
|
|
44
|
+
const updated = await blog.update('rec_abc123', { published: false })
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Each record is `{ uid, data, createdAt, updatedAt }`, where `data` is keyed by
|
|
48
|
+
field slug with references (files, referenced records) already inflated.
|
|
49
|
+
|
|
50
|
+
## Authentication & scopes
|
|
51
|
+
|
|
52
|
+
Pass the project API key as `apiKey`. A key's reach is the union of its per-node
|
|
53
|
+
scopes (read and/or write, optionally narrowed to specific datasets). Calls
|
|
54
|
+
outside the key's scope throw a `StudioLayerError` with `status` 403; an
|
|
55
|
+
unknown node/dataset/record throws 404.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { StudioLayerError } from '@studiolayer/client'
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
await studio.getRecord('blog', 'posts', 'nope')
|
|
62
|
+
} catch (err) {
|
|
63
|
+
if (err instanceof StudioLayerError && err.isNotFound) {
|
|
64
|
+
// handle missing record
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Introspection
|
|
70
|
+
|
|
71
|
+
`schema()` returns every node the key can reach, the datasets within, each
|
|
72
|
+
dataset's field schema, and `canRead` / `canWrite` flags so a site can adapt to
|
|
73
|
+
its access without hardcoding slugs.
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
for (const node of await studio.nodes()) {
|
|
77
|
+
for (const ds of node.datasets) {
|
|
78
|
+
console.log(node.slug, ds.slug, ds.canWrite ? '(writable)' : '(read-only)')
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Saved queries
|
|
84
|
+
|
|
85
|
+
Run a server-defined query (`queries.<slug>`) and get its result. The `shape`
|
|
86
|
+
tells you whether `value` is a `collection`, a `single` record, or a scalar
|
|
87
|
+
`value`.
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
const result = await studio.query('featured-posts')
|
|
91
|
+
if (result.shape === 'collection') {
|
|
92
|
+
// result.value is an array of records
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Caching
|
|
97
|
+
|
|
98
|
+
Reads (`schema`, `listRecords`, `getRecord`, `query`) are cached in memory.
|
|
99
|
+
Writes (`createRecord`, `updateRecord`) automatically invalidate the affected
|
|
100
|
+
dataset's cached reads (and all query results, since a query may aggregate it).
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
const studio = createClient({
|
|
104
|
+
apiKey, baseUrl,
|
|
105
|
+
cache: { ttl: 30_000, maxEntries: 1000 }, // defaults: ttl 60s, maxEntries 500
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// Disable entirely:
|
|
109
|
+
createClient({ apiKey, baseUrl, cache: false })
|
|
110
|
+
|
|
111
|
+
// Bypass the cache for a single call:
|
|
112
|
+
await studio.listRecords('blog', 'posts', { cache: false })
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Clearing the cache
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
studio.clearCache() // everything
|
|
119
|
+
studio.clearCache('blog') // one node
|
|
120
|
+
studio.clearCache({ node: 'blog', dataset: 'posts' }) // one dataset
|
|
121
|
+
studio.dataset('blog', 'posts').clearCache() // same, fluent
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Bring your own store (share a cache across instances, or persist it) by
|
|
125
|
+
implementing `CacheStore` and passing it as `cache.store`.
|
|
126
|
+
|
|
127
|
+
## API
|
|
128
|
+
|
|
129
|
+
| Method | Description |
|
|
130
|
+
| --- | --- |
|
|
131
|
+
| `schema(opts?)` | Full reachable content shape (`{ nodes }`). |
|
|
132
|
+
| `nodes(opts?)` | Shortcut for `schema().nodes`. |
|
|
133
|
+
| `listRecords<T>(node, dataset, opts?)` | All records in a dataset. |
|
|
134
|
+
| `getRecord<T>(node, dataset, uid, opts?)` | One record by uid. |
|
|
135
|
+
| `createRecord<T>(node, dataset, data)` | Create a record (needs write scope). |
|
|
136
|
+
| `updateRecord<T>(node, dataset, uid, data)` | Merge-patch a record (needs write scope). |
|
|
137
|
+
| `query<V>(slug, opts?)` | Run a saved query. |
|
|
138
|
+
| `dataset<T>(node, dataset)` | Fluent handle: `.list() .get() .create() .update() .clearCache()`. |
|
|
139
|
+
| `clearCache(scope?)` | Clear all, one node, or one dataset. |
|
|
140
|
+
|
|
141
|
+
`opts` is `{ cache?: boolean }`. All read methods accept a row type parameter
|
|
142
|
+
`<T>` for `record.data`.
|
|
143
|
+
|
|
144
|
+
## Node < 18
|
|
145
|
+
|
|
146
|
+
Global `fetch` is required. On older runtimes, pass one:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import fetch from 'node-fetch'
|
|
150
|
+
createClient({ apiKey, baseUrl, fetch: fetch as unknown as typeof globalThis.fetch })
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/cache.ts
|
|
4
|
+
var MemoryCacheStore = class {
|
|
5
|
+
constructor(maxEntries = 500) {
|
|
6
|
+
this.maxEntries = maxEntries;
|
|
7
|
+
this.map = /* @__PURE__ */ new Map();
|
|
8
|
+
}
|
|
9
|
+
get(key) {
|
|
10
|
+
return this.map.get(key);
|
|
11
|
+
}
|
|
12
|
+
set(key, entry) {
|
|
13
|
+
this.map.delete(key);
|
|
14
|
+
this.map.set(key, entry);
|
|
15
|
+
while (this.map.size > this.maxEntries) {
|
|
16
|
+
const oldest = this.map.keys().next().value;
|
|
17
|
+
if (oldest === void 0) break;
|
|
18
|
+
this.map.delete(oldest);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
delete(key) {
|
|
22
|
+
this.map.delete(key);
|
|
23
|
+
}
|
|
24
|
+
keys() {
|
|
25
|
+
return this.map.keys();
|
|
26
|
+
}
|
|
27
|
+
clear() {
|
|
28
|
+
this.map.clear();
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
var ContentCache = class {
|
|
32
|
+
constructor(opts = {}) {
|
|
33
|
+
this.enabled = opts.enabled ?? true;
|
|
34
|
+
this.ttl = opts.ttl ?? 6e4;
|
|
35
|
+
this.store = opts.store ?? new MemoryCacheStore(opts.maxEntries ?? 500);
|
|
36
|
+
}
|
|
37
|
+
/** Return a fresh cached value for `key`, or `undefined` on miss/expiry. */
|
|
38
|
+
get(key, now) {
|
|
39
|
+
if (!this.enabled) return void 0;
|
|
40
|
+
const entry = this.store.get(key);
|
|
41
|
+
if (!entry) return void 0;
|
|
42
|
+
if (entry.expiresAt !== 0 && entry.expiresAt <= now) {
|
|
43
|
+
this.store.delete(key);
|
|
44
|
+
return void 0;
|
|
45
|
+
}
|
|
46
|
+
return entry.value;
|
|
47
|
+
}
|
|
48
|
+
set(key, value, now) {
|
|
49
|
+
if (!this.enabled) return;
|
|
50
|
+
this.store.set(key, { value, expiresAt: this.ttl === 0 ? 0 : now + this.ttl });
|
|
51
|
+
}
|
|
52
|
+
/** Drop one exact key. */
|
|
53
|
+
delete(key) {
|
|
54
|
+
this.store.delete(key);
|
|
55
|
+
}
|
|
56
|
+
/** Drop every key that starts with `prefix`. */
|
|
57
|
+
deletePrefix(prefix) {
|
|
58
|
+
for (const key of [...this.store.keys()]) {
|
|
59
|
+
if (key.startsWith(prefix)) this.store.delete(key);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
clear() {
|
|
63
|
+
this.store.clear();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// src/errors.ts
|
|
68
|
+
var StudioLayerError = class _StudioLayerError extends Error {
|
|
69
|
+
constructor(message, status, body) {
|
|
70
|
+
super(message);
|
|
71
|
+
this.name = "StudioLayerError";
|
|
72
|
+
this.status = status;
|
|
73
|
+
this.body = body;
|
|
74
|
+
Object.setPrototypeOf(this, _StudioLayerError.prototype);
|
|
75
|
+
}
|
|
76
|
+
get isUnauthorized() {
|
|
77
|
+
return this.status === 401;
|
|
78
|
+
}
|
|
79
|
+
get isForbidden() {
|
|
80
|
+
return this.status === 403;
|
|
81
|
+
}
|
|
82
|
+
get isNotFound() {
|
|
83
|
+
return this.status === 404;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// src/client.ts
|
|
88
|
+
var StudioLayerClient = class {
|
|
89
|
+
constructor(options) {
|
|
90
|
+
if (!options.apiKey) throw new Error("StudioLayerClient: `apiKey` is required");
|
|
91
|
+
if (!options.baseUrl) throw new Error("StudioLayerClient: `baseUrl` is required");
|
|
92
|
+
this.apiKey = options.apiKey;
|
|
93
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
94
|
+
const resolvedFetch = options.fetch ?? globalThis.fetch;
|
|
95
|
+
if (!resolvedFetch) {
|
|
96
|
+
throw new Error("StudioLayerClient: no `fetch` available; pass one via options.fetch (Node < 18)");
|
|
97
|
+
}
|
|
98
|
+
this.fetchImpl = resolvedFetch.bind(globalThis);
|
|
99
|
+
this.headers = options.headers ?? {};
|
|
100
|
+
const cacheOpt = options.cache;
|
|
101
|
+
this.cache = new ContentCache(
|
|
102
|
+
cacheOpt === false ? { enabled: false } : cacheOpt === true || cacheOpt === void 0 ? {} : cacheOpt
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
// ── Introspection ─────────────────────────────────────────────────────────
|
|
106
|
+
/** The full content shape this key can reach: nodes, datasets, field schemas. */
|
|
107
|
+
async schema(opts) {
|
|
108
|
+
return this.cachedGet("schema", "/schema", opts);
|
|
109
|
+
}
|
|
110
|
+
/** Shorthand for `schema()` then `.nodes`. */
|
|
111
|
+
async nodes(opts) {
|
|
112
|
+
return (await this.schema(opts)).nodes;
|
|
113
|
+
}
|
|
114
|
+
// ── Records ───────────────────────────────────────────────────────────────
|
|
115
|
+
/** List every record in a dataset (references inflated). Requires read scope. */
|
|
116
|
+
async listRecords(nodeSlug, datasetSlug, opts) {
|
|
117
|
+
const res = await this.cachedGet(
|
|
118
|
+
recordsKey(nodeSlug, datasetSlug),
|
|
119
|
+
datasetPath(nodeSlug, datasetSlug),
|
|
120
|
+
opts
|
|
121
|
+
);
|
|
122
|
+
return res.records;
|
|
123
|
+
}
|
|
124
|
+
/** Read one record by uid. Requires read scope. Throws 404 if it does not exist. */
|
|
125
|
+
async getRecord(nodeSlug, datasetSlug, recordUid, opts) {
|
|
126
|
+
const res = await this.cachedGet(
|
|
127
|
+
recordKey(nodeSlug, datasetSlug, recordUid),
|
|
128
|
+
`${datasetPath(nodeSlug, datasetSlug)}/${encodeURIComponent(recordUid)}`,
|
|
129
|
+
opts
|
|
130
|
+
);
|
|
131
|
+
return res.record;
|
|
132
|
+
}
|
|
133
|
+
/** Create a record. Requires write scope. Invalidates cached reads of the dataset. */
|
|
134
|
+
async createRecord(nodeSlug, datasetSlug, data) {
|
|
135
|
+
const res = await this.request(
|
|
136
|
+
"POST",
|
|
137
|
+
datasetPath(nodeSlug, datasetSlug),
|
|
138
|
+
{ data }
|
|
139
|
+
);
|
|
140
|
+
this.invalidateDataset(nodeSlug, datasetSlug);
|
|
141
|
+
return res.record;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Partially update a record. The patch is merged onto the stored data (omitted
|
|
145
|
+
* top-level fields are preserved). Requires write scope. Invalidates cached
|
|
146
|
+
* reads of the dataset and of this record.
|
|
147
|
+
*/
|
|
148
|
+
async updateRecord(nodeSlug, datasetSlug, recordUid, data) {
|
|
149
|
+
const res = await this.request(
|
|
150
|
+
"PATCH",
|
|
151
|
+
`${datasetPath(nodeSlug, datasetSlug)}/${encodeURIComponent(recordUid)}`,
|
|
152
|
+
{ data }
|
|
153
|
+
);
|
|
154
|
+
this.invalidateDataset(nodeSlug, datasetSlug);
|
|
155
|
+
this.cache.delete(recordKey(nodeSlug, datasetSlug, recordUid));
|
|
156
|
+
return res.record;
|
|
157
|
+
}
|
|
158
|
+
// -- Queries --
|
|
159
|
+
/** Run a saved query (`queries.<slug>`) and return its result. Requires read scope. */
|
|
160
|
+
async query(querySlug, opts) {
|
|
161
|
+
return this.cachedGet(
|
|
162
|
+
`query:${querySlug}`,
|
|
163
|
+
`/queries/${encodeURIComponent(querySlug)}`,
|
|
164
|
+
opts
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
// ── Fluent dataset handle ───────────────────────────────────────────────────
|
|
168
|
+
/**
|
|
169
|
+
* A fluent, type-parameterised handle to one dataset:
|
|
170
|
+
* `client.dataset<Post>('blog', 'posts').list()`.
|
|
171
|
+
*/
|
|
172
|
+
dataset(nodeSlug, datasetSlug) {
|
|
173
|
+
return new DatasetHandle(this, nodeSlug, datasetSlug);
|
|
174
|
+
}
|
|
175
|
+
// ── Cache control ───────────────────────────────────────────────────────────
|
|
176
|
+
/**
|
|
177
|
+
* Clear cached reads. With no argument, clears everything. Pass a scope to
|
|
178
|
+
* clear just part of it: a node slug, or `{ node, dataset }` for one dataset.
|
|
179
|
+
*/
|
|
180
|
+
clearCache(scope) {
|
|
181
|
+
if (scope === void 0) {
|
|
182
|
+
this.cache.clear();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (typeof scope === "string") {
|
|
186
|
+
this.cache.deletePrefix(`records:${scope}/`);
|
|
187
|
+
this.cache.deletePrefix(`record:${scope}/`);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (scope.dataset) {
|
|
191
|
+
this.invalidateDataset(scope.node, scope.dataset);
|
|
192
|
+
} else {
|
|
193
|
+
this.cache.deletePrefix(`records:${scope.node}/`);
|
|
194
|
+
this.cache.deletePrefix(`record:${scope.node}/`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/** Drop cached record reads for a dataset, plus all query results (which may aggregate it). */
|
|
198
|
+
invalidateDataset(nodeSlug, datasetSlug) {
|
|
199
|
+
this.cache.delete(recordsKey(nodeSlug, datasetSlug));
|
|
200
|
+
this.cache.deletePrefix(`${recordKey(nodeSlug, datasetSlug, "")}`);
|
|
201
|
+
this.cache.deletePrefix("query:");
|
|
202
|
+
}
|
|
203
|
+
// ── Transport ───────────────────────────────────────────────────────────────
|
|
204
|
+
async cachedGet(cacheKey, path, opts) {
|
|
205
|
+
const useCache = opts?.cache !== false;
|
|
206
|
+
const now = Date.now();
|
|
207
|
+
if (useCache) {
|
|
208
|
+
const hit = this.cache.get(cacheKey, now);
|
|
209
|
+
if (hit !== void 0) return hit;
|
|
210
|
+
}
|
|
211
|
+
const value = await this.request("GET", path);
|
|
212
|
+
if (useCache) this.cache.set(cacheKey, value, now);
|
|
213
|
+
return value;
|
|
214
|
+
}
|
|
215
|
+
async request(method, path, body) {
|
|
216
|
+
const res = await this.fetchImpl(`${this.baseUrl}/api/content${path}`, {
|
|
217
|
+
method,
|
|
218
|
+
headers: {
|
|
219
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
220
|
+
...body !== void 0 ? { "Content-Type": "application/json" } : {},
|
|
221
|
+
...this.headers
|
|
222
|
+
},
|
|
223
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
224
|
+
});
|
|
225
|
+
if (!res.ok) {
|
|
226
|
+
let message = res.statusText || `Request failed with status ${res.status}`;
|
|
227
|
+
let parsed;
|
|
228
|
+
try {
|
|
229
|
+
parsed = await res.json();
|
|
230
|
+
const m = parsed;
|
|
231
|
+
if (typeof m?.message === "string") message = m.message;
|
|
232
|
+
else if (typeof m?.statusMessage === "string") message = m.statusMessage;
|
|
233
|
+
} catch {
|
|
234
|
+
}
|
|
235
|
+
throw new StudioLayerError(message, res.status, parsed);
|
|
236
|
+
}
|
|
237
|
+
if (res.status === 204) return void 0;
|
|
238
|
+
return res.json();
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
var DatasetHandle = class {
|
|
242
|
+
constructor(client, nodeSlug, datasetSlug) {
|
|
243
|
+
this.client = client;
|
|
244
|
+
this.nodeSlug = nodeSlug;
|
|
245
|
+
this.datasetSlug = datasetSlug;
|
|
246
|
+
}
|
|
247
|
+
list(opts) {
|
|
248
|
+
return this.client.listRecords(this.nodeSlug, this.datasetSlug, opts);
|
|
249
|
+
}
|
|
250
|
+
get(recordUid, opts) {
|
|
251
|
+
return this.client.getRecord(this.nodeSlug, this.datasetSlug, recordUid, opts);
|
|
252
|
+
}
|
|
253
|
+
create(data) {
|
|
254
|
+
return this.client.createRecord(this.nodeSlug, this.datasetSlug, data);
|
|
255
|
+
}
|
|
256
|
+
update(recordUid, data) {
|
|
257
|
+
return this.client.updateRecord(this.nodeSlug, this.datasetSlug, recordUid, data);
|
|
258
|
+
}
|
|
259
|
+
/** Clear cached reads for this dataset. */
|
|
260
|
+
clearCache() {
|
|
261
|
+
this.client.clearCache({ node: this.nodeSlug, dataset: this.datasetSlug });
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
function datasetPath(nodeSlug, datasetSlug) {
|
|
265
|
+
return `/nodes/${encodeURIComponent(nodeSlug)}/datasets/${encodeURIComponent(datasetSlug)}/records`;
|
|
266
|
+
}
|
|
267
|
+
function recordsKey(nodeSlug, datasetSlug) {
|
|
268
|
+
return `records:${nodeSlug}/${datasetSlug}`;
|
|
269
|
+
}
|
|
270
|
+
function recordKey(nodeSlug, datasetSlug, recordUid) {
|
|
271
|
+
return `record:${nodeSlug}/${datasetSlug}/${recordUid}`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/index.ts
|
|
275
|
+
function createClient(options) {
|
|
276
|
+
return new StudioLayerClient(options);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
exports.ContentCache = ContentCache;
|
|
280
|
+
exports.DatasetHandle = DatasetHandle;
|
|
281
|
+
exports.MemoryCacheStore = MemoryCacheStore;
|
|
282
|
+
exports.StudioLayerClient = StudioLayerClient;
|
|
283
|
+
exports.StudioLayerError = StudioLayerError;
|
|
284
|
+
exports.createClient = createClient;
|
|
285
|
+
//# sourceMappingURL=index.cjs.map
|
|
286
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cache.ts","../src/errors.ts","../src/client.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Read caching for the content client. GET responses (schema, record lists,\n * single records, queries) are cached by request key; writes invalidate the\n * affected keys automatically. Swap in a custom `CacheStore` to share a cache\n * across client instances or to back it with something persistent.\n */\n\nexport interface CacheEntry {\n value: unknown\n /** Epoch ms when the entry expires. `0` means it never expires. */\n expiresAt: number\n}\n\n/** Pluggable backing store. The default is an in-memory, insertion-ordered map. */\nexport interface CacheStore {\n get(key: string): CacheEntry | undefined\n set(key: string, entry: CacheEntry): void\n delete(key: string): void\n /** Iterate current keys (used for prefix invalidation). */\n keys(): Iterable<string>\n clear(): void\n}\n\nexport interface CacheOptions {\n /** Master switch. Default `true`. */\n enabled?: boolean\n /** Time-to-live for cached reads, in ms. Default `60000`. `0` = never expires. */\n ttl?: number\n /** Soft cap on entries; oldest are evicted first. Default `500`. */\n maxEntries?: number\n /** Custom backing store. Defaults to an in-memory store. */\n store?: CacheStore\n}\n\n/** In-memory store with insertion-order (FIFO) eviction once `maxEntries` is hit. */\nexport class MemoryCacheStore implements CacheStore {\n private map = new Map<string, CacheEntry>()\n\n constructor(private maxEntries = 500) {}\n\n get(key: string): CacheEntry | undefined {\n return this.map.get(key)\n }\n\n set(key: string, entry: CacheEntry): void {\n // Re-insert so recently written keys are considered newest.\n this.map.delete(key)\n this.map.set(key, entry)\n while (this.map.size > this.maxEntries) {\n const oldest = this.map.keys().next().value\n if (oldest === undefined) break\n this.map.delete(oldest)\n }\n }\n\n delete(key: string): void {\n this.map.delete(key)\n }\n\n keys(): Iterable<string> {\n return this.map.keys()\n }\n\n clear(): void {\n this.map.clear()\n }\n}\n\n/**\n * Thin cache facade the client talks to. Owns TTL logic and prefix-based\n * invalidation so the client only deals in cache keys.\n */\nexport class ContentCache {\n readonly enabled: boolean\n private ttl: number\n private store: CacheStore\n\n constructor(opts: CacheOptions = {}) {\n this.enabled = opts.enabled ?? true\n this.ttl = opts.ttl ?? 60_000\n this.store = opts.store ?? new MemoryCacheStore(opts.maxEntries ?? 500)\n }\n\n /** Return a fresh cached value for `key`, or `undefined` on miss/expiry. */\n get<T>(key: string, now: number): T | undefined {\n if (!this.enabled) return undefined\n const entry = this.store.get(key)\n if (!entry) return undefined\n if (entry.expiresAt !== 0 && entry.expiresAt <= now) {\n this.store.delete(key)\n return undefined\n }\n return entry.value as T\n }\n\n set(key: string, value: unknown, now: number): void {\n if (!this.enabled) return\n this.store.set(key, { value, expiresAt: this.ttl === 0 ? 0 : now + this.ttl })\n }\n\n /** Drop one exact key. */\n delete(key: string): void {\n this.store.delete(key)\n }\n\n /** Drop every key that starts with `prefix`. */\n deletePrefix(prefix: string): void {\n for (const key of [...this.store.keys()]) {\n if (key.startsWith(prefix)) this.store.delete(key)\n }\n }\n\n clear(): void {\n this.store.clear()\n }\n}\n","/**\n * Error thrown for any non-2xx response from the content API. `status` carries\n * the HTTP status code so callers can branch (401 bad key, 403 out of scope,\n * 404 missing node/dataset/record, 422 query failed).\n */\nexport class StudioLayerError extends Error {\n readonly status: number\n /** The raw parsed response body, when the server returned one. */\n readonly body: unknown\n\n constructor(message: string, status: number, body?: unknown) {\n super(message)\n this.name = 'StudioLayerError'\n this.status = status\n this.body = body\n // Restore prototype chain for `instanceof` when transpiled to ES5.\n Object.setPrototypeOf(this, StudioLayerError.prototype)\n }\n\n get isUnauthorized(): boolean {\n return this.status === 401\n }\n\n get isForbidden(): boolean {\n return this.status === 403\n }\n\n get isNotFound(): boolean {\n return this.status === 404\n }\n}\n","import { ContentCache, type CacheOptions } from './cache'\nimport { StudioLayerError } from './errors'\nimport type { ContentRecord, ContentSchema, QueryResult, SchemaNode } from './types'\n\nexport interface StudioLayerClientOptions {\n /**\n * Project API key, `slk_...`. Mint one in the project's settings under\n * \"API keys\". Its reach is the union of its per-node read/write scopes.\n */\n apiKey: string\n /**\n * Base URL of your StudioLayer server, without a trailing `/api/content`\n * (that path is appended automatically), e.g. `https://studio.example.com`.\n */\n baseUrl: string\n /** Read caching. Pass `false` to disable, or an options object to tune it. */\n cache?: boolean | CacheOptions\n /** Custom `fetch` implementation. Defaults to the global `fetch`. */\n fetch?: typeof fetch\n /** Extra headers merged into every request. */\n headers?: Record<string, string>\n}\n\n/** Per-call read options. */\nexport interface ReadOptions {\n /** Set `false` to bypass the cache for this call and always hit the network. */\n cache?: boolean\n}\n\ntype Method = 'GET' | 'POST' | 'PATCH'\n\n/**\n * Typed client for the StudioLayer content API: read and write a project's node\n * datasets from any website or app. Reads are cached (see the `cache` option);\n * writes invalidate the affected cache entries automatically.\n *\n * ```ts\n * const studio = new StudioLayerClient({ apiKey: 'slk_...', baseUrl: 'https://studio.example.com' })\n * const posts = await studio.dataset<Post>('blog', 'posts').list()\n * ```\n */\nexport class StudioLayerClient {\n private readonly apiKey: string\n private readonly baseUrl: string\n private readonly fetchImpl: typeof fetch\n private readonly headers: Record<string, string>\n private readonly cache: ContentCache\n\n constructor(options: StudioLayerClientOptions) {\n if (!options.apiKey) throw new Error('StudioLayerClient: `apiKey` is required')\n if (!options.baseUrl) throw new Error('StudioLayerClient: `baseUrl` is required')\n\n this.apiKey = options.apiKey\n this.baseUrl = options.baseUrl.replace(/\\/+$/, '')\n const resolvedFetch = options.fetch ?? globalThis.fetch\n if (!resolvedFetch) {\n throw new Error('StudioLayerClient: no `fetch` available; pass one via options.fetch (Node < 18)')\n }\n this.fetchImpl = resolvedFetch.bind(globalThis)\n this.headers = options.headers ?? {}\n\n const cacheOpt = options.cache\n this.cache = new ContentCache(\n cacheOpt === false ? { enabled: false }\n : cacheOpt === true || cacheOpt === undefined ? {}\n : cacheOpt,\n )\n }\n\n // ── Introspection ─────────────────────────────────────────────────────────\n\n /** The full content shape this key can reach: nodes, datasets, field schemas. */\n async schema(opts?: ReadOptions): Promise<ContentSchema> {\n return this.cachedGet<ContentSchema>('schema', '/schema', opts)\n }\n\n /** Shorthand for `schema()` then `.nodes`. */\n async nodes(opts?: ReadOptions): Promise<SchemaNode[]> {\n return (await this.schema(opts)).nodes\n }\n\n // ── Records ───────────────────────────────────────────────────────────────\n\n /** List every record in a dataset (references inflated). Requires read scope. */\n async listRecords<T = Record<string, unknown>>(\n nodeSlug: string,\n datasetSlug: string,\n opts?: ReadOptions,\n ): Promise<ContentRecord<T>[]> {\n const res = await this.cachedGet<{ records: ContentRecord<T>[] }>(\n recordsKey(nodeSlug, datasetSlug),\n datasetPath(nodeSlug, datasetSlug),\n opts,\n )\n return res.records\n }\n\n /** Read one record by uid. Requires read scope. Throws 404 if it does not exist. */\n async getRecord<T = Record<string, unknown>>(\n nodeSlug: string,\n datasetSlug: string,\n recordUid: string,\n opts?: ReadOptions,\n ): Promise<ContentRecord<T>> {\n const res = await this.cachedGet<{ record: ContentRecord<T> }>(\n recordKey(nodeSlug, datasetSlug, recordUid),\n `${datasetPath(nodeSlug, datasetSlug)}/${encodeURIComponent(recordUid)}`,\n opts,\n )\n return res.record\n }\n\n /** Create a record. Requires write scope. Invalidates cached reads of the dataset. */\n async createRecord<T = Record<string, unknown>>(\n nodeSlug: string,\n datasetSlug: string,\n data: Record<string, unknown>,\n ): Promise<ContentRecord<T>> {\n const res = await this.request<{ record: ContentRecord<T> }>(\n 'POST',\n datasetPath(nodeSlug, datasetSlug),\n { data },\n )\n this.invalidateDataset(nodeSlug, datasetSlug)\n return res.record\n }\n\n /**\n * Partially update a record. The patch is merged onto the stored data (omitted\n * top-level fields are preserved). Requires write scope. Invalidates cached\n * reads of the dataset and of this record.\n */\n async updateRecord<T = Record<string, unknown>>(\n nodeSlug: string,\n datasetSlug: string,\n recordUid: string,\n data: Record<string, unknown>,\n ): Promise<ContentRecord<T>> {\n const res = await this.request<{ record: ContentRecord<T> }>(\n 'PATCH',\n `${datasetPath(nodeSlug, datasetSlug)}/${encodeURIComponent(recordUid)}`,\n { data },\n )\n this.invalidateDataset(nodeSlug, datasetSlug)\n this.cache.delete(recordKey(nodeSlug, datasetSlug, recordUid))\n return res.record\n }\n\n // -- Queries --\n\n /** Run a saved query (`queries.<slug>`) and return its result. Requires read scope. */\n async query<V = unknown>(querySlug: string, opts?: ReadOptions): Promise<QueryResult<V>> {\n return this.cachedGet<QueryResult<V>>(\n `query:${querySlug}`,\n `/queries/${encodeURIComponent(querySlug)}`,\n opts,\n )\n }\n\n // ── Fluent dataset handle ───────────────────────────────────────────────────\n\n /**\n * A fluent, type-parameterised handle to one dataset:\n * `client.dataset<Post>('blog', 'posts').list()`.\n */\n dataset<T = Record<string, unknown>>(nodeSlug: string, datasetSlug: string): DatasetHandle<T> {\n return new DatasetHandle<T>(this, nodeSlug, datasetSlug)\n }\n\n // ── Cache control ───────────────────────────────────────────────────────────\n\n /**\n * Clear cached reads. With no argument, clears everything. Pass a scope to\n * clear just part of it: a node slug, or `{ node, dataset }` for one dataset.\n */\n clearCache(scope?: string | { node: string, dataset?: string }): void {\n if (scope === undefined) {\n this.cache.clear()\n return\n }\n if (typeof scope === 'string') {\n this.cache.deletePrefix(`records:${scope}/`)\n this.cache.deletePrefix(`record:${scope}/`)\n return\n }\n if (scope.dataset) {\n this.invalidateDataset(scope.node, scope.dataset)\n } else {\n this.cache.deletePrefix(`records:${scope.node}/`)\n this.cache.deletePrefix(`record:${scope.node}/`)\n }\n }\n\n /** Drop cached record reads for a dataset, plus all query results (which may aggregate it). */\n private invalidateDataset(nodeSlug: string, datasetSlug: string): void {\n this.cache.delete(recordsKey(nodeSlug, datasetSlug))\n this.cache.deletePrefix(`${recordKey(nodeSlug, datasetSlug, '')}`)\n this.cache.deletePrefix('query:')\n }\n\n // ── Transport ───────────────────────────────────────────────────────────────\n\n private async cachedGet<T>(cacheKey: string, path: string, opts?: ReadOptions): Promise<T> {\n const useCache = opts?.cache !== false\n const now = Date.now()\n if (useCache) {\n const hit = this.cache.get<T>(cacheKey, now)\n if (hit !== undefined) return hit\n }\n const value = await this.request<T>('GET', path)\n if (useCache) this.cache.set(cacheKey, value, now)\n return value\n }\n\n private async request<T>(method: Method, path: string, body?: unknown): Promise<T> {\n const res = await this.fetchImpl(`${this.baseUrl}/api/content${path}`, {\n method,\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),\n ...this.headers,\n },\n body: body !== undefined ? JSON.stringify(body) : undefined,\n })\n\n if (!res.ok) {\n let message = res.statusText || `Request failed with status ${res.status}`\n let parsed: unknown\n try {\n parsed = await res.json()\n const m = parsed as { message?: unknown, statusMessage?: unknown }\n if (typeof m?.message === 'string') message = m.message\n else if (typeof m?.statusMessage === 'string') message = m.statusMessage\n } catch {\n /* non-JSON error body; keep the status text */\n }\n throw new StudioLayerError(message, res.status, parsed)\n }\n\n if (res.status === 204) return undefined as T\n return res.json() as Promise<T>\n }\n}\n\n/** Fluent, dataset-scoped view returned by `client.dataset()`. */\nexport class DatasetHandle<T = Record<string, unknown>> {\n constructor(\n private readonly client: StudioLayerClient,\n private readonly nodeSlug: string,\n private readonly datasetSlug: string,\n ) {}\n\n list(opts?: ReadOptions): Promise<ContentRecord<T>[]> {\n return this.client.listRecords<T>(this.nodeSlug, this.datasetSlug, opts)\n }\n\n get(recordUid: string, opts?: ReadOptions): Promise<ContentRecord<T>> {\n return this.client.getRecord<T>(this.nodeSlug, this.datasetSlug, recordUid, opts)\n }\n\n create(data: Record<string, unknown>): Promise<ContentRecord<T>> {\n return this.client.createRecord<T>(this.nodeSlug, this.datasetSlug, data)\n }\n\n update(recordUid: string, data: Record<string, unknown>): Promise<ContentRecord<T>> {\n return this.client.updateRecord<T>(this.nodeSlug, this.datasetSlug, recordUid, data)\n }\n\n /** Clear cached reads for this dataset. */\n clearCache(): void {\n this.client.clearCache({ node: this.nodeSlug, dataset: this.datasetSlug })\n }\n}\n\n// ── Cache-key + path helpers ──────────────────────────────────────────────────\n\nfunction datasetPath(nodeSlug: string, datasetSlug: string): string {\n return `/nodes/${encodeURIComponent(nodeSlug)}/datasets/${encodeURIComponent(datasetSlug)}/records`\n}\n\nfunction recordsKey(nodeSlug: string, datasetSlug: string): string {\n return `records:${nodeSlug}/${datasetSlug}`\n}\n\nfunction recordKey(nodeSlug: string, datasetSlug: string, recordUid: string): string {\n return `record:${nodeSlug}/${datasetSlug}/${recordUid}`\n}\n","export { StudioLayerClient, DatasetHandle } from './client'\nexport type { StudioLayerClientOptions, ReadOptions } from './client'\nexport { StudioLayerError } from './errors'\nexport { ContentCache, MemoryCacheStore } from './cache'\nexport type { CacheOptions, CacheStore, CacheEntry } from './cache'\nexport type {\n ContentSchema,\n SchemaNode,\n SchemaDataset,\n SchemaField,\n FieldKind,\n FieldType,\n ContentRecord,\n QueryResult,\n QueryShape,\n} from './types'\n\nimport { StudioLayerClient, type StudioLayerClientOptions } from './client'\n\n/** Convenience factory: `createClient({ apiKey, baseUrl })`. */\nexport function createClient(options: StudioLayerClientOptions): StudioLayerClient {\n return new StudioLayerClient(options)\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read caching for the content client. GET responses (schema, record lists,
|
|
3
|
+
* single records, queries) are cached by request key; writes invalidate the
|
|
4
|
+
* affected keys automatically. Swap in a custom `CacheStore` to share a cache
|
|
5
|
+
* across client instances or to back it with something persistent.
|
|
6
|
+
*/
|
|
7
|
+
interface CacheEntry {
|
|
8
|
+
value: unknown;
|
|
9
|
+
/** Epoch ms when the entry expires. `0` means it never expires. */
|
|
10
|
+
expiresAt: number;
|
|
11
|
+
}
|
|
12
|
+
/** Pluggable backing store. The default is an in-memory, insertion-ordered map. */
|
|
13
|
+
interface CacheStore {
|
|
14
|
+
get(key: string): CacheEntry | undefined;
|
|
15
|
+
set(key: string, entry: CacheEntry): void;
|
|
16
|
+
delete(key: string): void;
|
|
17
|
+
/** Iterate current keys (used for prefix invalidation). */
|
|
18
|
+
keys(): Iterable<string>;
|
|
19
|
+
clear(): void;
|
|
20
|
+
}
|
|
21
|
+
interface CacheOptions {
|
|
22
|
+
/** Master switch. Default `true`. */
|
|
23
|
+
enabled?: boolean;
|
|
24
|
+
/** Time-to-live for cached reads, in ms. Default `60000`. `0` = never expires. */
|
|
25
|
+
ttl?: number;
|
|
26
|
+
/** Soft cap on entries; oldest are evicted first. Default `500`. */
|
|
27
|
+
maxEntries?: number;
|
|
28
|
+
/** Custom backing store. Defaults to an in-memory store. */
|
|
29
|
+
store?: CacheStore;
|
|
30
|
+
}
|
|
31
|
+
/** In-memory store with insertion-order (FIFO) eviction once `maxEntries` is hit. */
|
|
32
|
+
declare class MemoryCacheStore implements CacheStore {
|
|
33
|
+
private maxEntries;
|
|
34
|
+
private map;
|
|
35
|
+
constructor(maxEntries?: number);
|
|
36
|
+
get(key: string): CacheEntry | undefined;
|
|
37
|
+
set(key: string, entry: CacheEntry): void;
|
|
38
|
+
delete(key: string): void;
|
|
39
|
+
keys(): Iterable<string>;
|
|
40
|
+
clear(): void;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Thin cache facade the client talks to. Owns TTL logic and prefix-based
|
|
44
|
+
* invalidation so the client only deals in cache keys.
|
|
45
|
+
*/
|
|
46
|
+
declare class ContentCache {
|
|
47
|
+
readonly enabled: boolean;
|
|
48
|
+
private ttl;
|
|
49
|
+
private store;
|
|
50
|
+
constructor(opts?: CacheOptions);
|
|
51
|
+
/** Return a fresh cached value for `key`, or `undefined` on miss/expiry. */
|
|
52
|
+
get<T>(key: string, now: number): T | undefined;
|
|
53
|
+
set(key: string, value: unknown, now: number): void;
|
|
54
|
+
/** Drop one exact key. */
|
|
55
|
+
delete(key: string): void;
|
|
56
|
+
/** Drop every key that starts with `prefix`. */
|
|
57
|
+
deletePrefix(prefix: string): void;
|
|
58
|
+
clear(): void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Wire types for the StudioLayer content API. These mirror the server DTOs
|
|
63
|
+
* (`server/utils/content/*`, `server/api/content/*`) one-to-one. Slugs are used
|
|
64
|
+
* on the wire; internal database ids never leak.
|
|
65
|
+
*/
|
|
66
|
+
/** Top-level shape category of a dataset field. */
|
|
67
|
+
type FieldKind = 'primitive' | 'object' | 'collection' | 'reference';
|
|
68
|
+
/** The primitive field types the platform ships. `reference` and `''` also occur. */
|
|
69
|
+
type FieldType = 'text' | 'textarea' | 'number' | 'boolean' | 'color' | 'date' | 'email' | 'file' | 'image' | 'identifier' | 'json' | 'select' | 'url' | 'reference' | '';
|
|
70
|
+
/** One field in a dataset's schema, as returned by `GET /schema`. */
|
|
71
|
+
interface SchemaField {
|
|
72
|
+
slug: string;
|
|
73
|
+
label: string;
|
|
74
|
+
kind: FieldKind;
|
|
75
|
+
type: FieldType;
|
|
76
|
+
required: boolean;
|
|
77
|
+
description: string | null;
|
|
78
|
+
}
|
|
79
|
+
/** A dataset reachable by the key, with its field schema and this key's access. */
|
|
80
|
+
interface SchemaDataset {
|
|
81
|
+
slug: string;
|
|
82
|
+
name: string;
|
|
83
|
+
description: string | null;
|
|
84
|
+
type: string;
|
|
85
|
+
/** Whether the key may read records from this dataset. */
|
|
86
|
+
canRead: boolean;
|
|
87
|
+
/** Whether the key may create/update records in this dataset. */
|
|
88
|
+
canWrite: boolean;
|
|
89
|
+
fields: SchemaField[];
|
|
90
|
+
}
|
|
91
|
+
/** A node reachable by the key, with the datasets it exposes. */
|
|
92
|
+
interface SchemaNode {
|
|
93
|
+
slug: string;
|
|
94
|
+
name: string;
|
|
95
|
+
description: string | null;
|
|
96
|
+
datasets: SchemaDataset[];
|
|
97
|
+
}
|
|
98
|
+
/** Response of `GET /schema`: the full content shape this key can reach. */
|
|
99
|
+
interface ContentSchema {
|
|
100
|
+
nodes: SchemaNode[];
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* A single content record. `data` holds the field values keyed by field slug,
|
|
104
|
+
* with references already inflated (file URLs, referenced-record snapshots).
|
|
105
|
+
* Parameterise it with your own row type for full type safety.
|
|
106
|
+
*/
|
|
107
|
+
interface ContentRecord<T = Record<string, unknown>> {
|
|
108
|
+
uid: string;
|
|
109
|
+
data: T;
|
|
110
|
+
createdAt: string;
|
|
111
|
+
updatedAt: string;
|
|
112
|
+
}
|
|
113
|
+
/** Result shape of a saved query, mirroring the query engine. */
|
|
114
|
+
type QueryShape = 'collection' | 'single' | 'value';
|
|
115
|
+
/** Response of `GET /queries/:slug`. `value` type depends on `shape`. */
|
|
116
|
+
interface QueryResult<V = unknown> {
|
|
117
|
+
query: string;
|
|
118
|
+
shape: QueryShape;
|
|
119
|
+
value: V;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface StudioLayerClientOptions {
|
|
123
|
+
/**
|
|
124
|
+
* Project API key, `slk_...`. Mint one in the project's settings under
|
|
125
|
+
* "API keys". Its reach is the union of its per-node read/write scopes.
|
|
126
|
+
*/
|
|
127
|
+
apiKey: string;
|
|
128
|
+
/**
|
|
129
|
+
* Base URL of your StudioLayer server, without a trailing `/api/content`
|
|
130
|
+
* (that path is appended automatically), e.g. `https://studio.example.com`.
|
|
131
|
+
*/
|
|
132
|
+
baseUrl: string;
|
|
133
|
+
/** Read caching. Pass `false` to disable, or an options object to tune it. */
|
|
134
|
+
cache?: boolean | CacheOptions;
|
|
135
|
+
/** Custom `fetch` implementation. Defaults to the global `fetch`. */
|
|
136
|
+
fetch?: typeof fetch;
|
|
137
|
+
/** Extra headers merged into every request. */
|
|
138
|
+
headers?: Record<string, string>;
|
|
139
|
+
}
|
|
140
|
+
/** Per-call read options. */
|
|
141
|
+
interface ReadOptions {
|
|
142
|
+
/** Set `false` to bypass the cache for this call and always hit the network. */
|
|
143
|
+
cache?: boolean;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Typed client for the StudioLayer content API: read and write a project's node
|
|
147
|
+
* datasets from any website or app. Reads are cached (see the `cache` option);
|
|
148
|
+
* writes invalidate the affected cache entries automatically.
|
|
149
|
+
*
|
|
150
|
+
* ```ts
|
|
151
|
+
* const studio = new StudioLayerClient({ apiKey: 'slk_...', baseUrl: 'https://studio.example.com' })
|
|
152
|
+
* const posts = await studio.dataset<Post>('blog', 'posts').list()
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
declare class StudioLayerClient {
|
|
156
|
+
private readonly apiKey;
|
|
157
|
+
private readonly baseUrl;
|
|
158
|
+
private readonly fetchImpl;
|
|
159
|
+
private readonly headers;
|
|
160
|
+
private readonly cache;
|
|
161
|
+
constructor(options: StudioLayerClientOptions);
|
|
162
|
+
/** The full content shape this key can reach: nodes, datasets, field schemas. */
|
|
163
|
+
schema(opts?: ReadOptions): Promise<ContentSchema>;
|
|
164
|
+
/** Shorthand for `schema()` then `.nodes`. */
|
|
165
|
+
nodes(opts?: ReadOptions): Promise<SchemaNode[]>;
|
|
166
|
+
/** List every record in a dataset (references inflated). Requires read scope. */
|
|
167
|
+
listRecords<T = Record<string, unknown>>(nodeSlug: string, datasetSlug: string, opts?: ReadOptions): Promise<ContentRecord<T>[]>;
|
|
168
|
+
/** Read one record by uid. Requires read scope. Throws 404 if it does not exist. */
|
|
169
|
+
getRecord<T = Record<string, unknown>>(nodeSlug: string, datasetSlug: string, recordUid: string, opts?: ReadOptions): Promise<ContentRecord<T>>;
|
|
170
|
+
/** Create a record. Requires write scope. Invalidates cached reads of the dataset. */
|
|
171
|
+
createRecord<T = Record<string, unknown>>(nodeSlug: string, datasetSlug: string, data: Record<string, unknown>): Promise<ContentRecord<T>>;
|
|
172
|
+
/**
|
|
173
|
+
* Partially update a record. The patch is merged onto the stored data (omitted
|
|
174
|
+
* top-level fields are preserved). Requires write scope. Invalidates cached
|
|
175
|
+
* reads of the dataset and of this record.
|
|
176
|
+
*/
|
|
177
|
+
updateRecord<T = Record<string, unknown>>(nodeSlug: string, datasetSlug: string, recordUid: string, data: Record<string, unknown>): Promise<ContentRecord<T>>;
|
|
178
|
+
/** Run a saved query (`queries.<slug>`) and return its result. Requires read scope. */
|
|
179
|
+
query<V = unknown>(querySlug: string, opts?: ReadOptions): Promise<QueryResult<V>>;
|
|
180
|
+
/**
|
|
181
|
+
* A fluent, type-parameterised handle to one dataset:
|
|
182
|
+
* `client.dataset<Post>('blog', 'posts').list()`.
|
|
183
|
+
*/
|
|
184
|
+
dataset<T = Record<string, unknown>>(nodeSlug: string, datasetSlug: string): DatasetHandle<T>;
|
|
185
|
+
/**
|
|
186
|
+
* Clear cached reads. With no argument, clears everything. Pass a scope to
|
|
187
|
+
* clear just part of it: a node slug, or `{ node, dataset }` for one dataset.
|
|
188
|
+
*/
|
|
189
|
+
clearCache(scope?: string | {
|
|
190
|
+
node: string;
|
|
191
|
+
dataset?: string;
|
|
192
|
+
}): void;
|
|
193
|
+
/** Drop cached record reads for a dataset, plus all query results (which may aggregate it). */
|
|
194
|
+
private invalidateDataset;
|
|
195
|
+
private cachedGet;
|
|
196
|
+
private request;
|
|
197
|
+
}
|
|
198
|
+
/** Fluent, dataset-scoped view returned by `client.dataset()`. */
|
|
199
|
+
declare class DatasetHandle<T = Record<string, unknown>> {
|
|
200
|
+
private readonly client;
|
|
201
|
+
private readonly nodeSlug;
|
|
202
|
+
private readonly datasetSlug;
|
|
203
|
+
constructor(client: StudioLayerClient, nodeSlug: string, datasetSlug: string);
|
|
204
|
+
list(opts?: ReadOptions): Promise<ContentRecord<T>[]>;
|
|
205
|
+
get(recordUid: string, opts?: ReadOptions): Promise<ContentRecord<T>>;
|
|
206
|
+
create(data: Record<string, unknown>): Promise<ContentRecord<T>>;
|
|
207
|
+
update(recordUid: string, data: Record<string, unknown>): Promise<ContentRecord<T>>;
|
|
208
|
+
/** Clear cached reads for this dataset. */
|
|
209
|
+
clearCache(): void;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Error thrown for any non-2xx response from the content API. `status` carries
|
|
214
|
+
* the HTTP status code so callers can branch (401 bad key, 403 out of scope,
|
|
215
|
+
* 404 missing node/dataset/record, 422 query failed).
|
|
216
|
+
*/
|
|
217
|
+
declare class StudioLayerError extends Error {
|
|
218
|
+
readonly status: number;
|
|
219
|
+
/** The raw parsed response body, when the server returned one. */
|
|
220
|
+
readonly body: unknown;
|
|
221
|
+
constructor(message: string, status: number, body?: unknown);
|
|
222
|
+
get isUnauthorized(): boolean;
|
|
223
|
+
get isForbidden(): boolean;
|
|
224
|
+
get isNotFound(): boolean;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Convenience factory: `createClient({ apiKey, baseUrl })`. */
|
|
228
|
+
declare function createClient(options: StudioLayerClientOptions): StudioLayerClient;
|
|
229
|
+
|
|
230
|
+
export { type CacheEntry, type CacheOptions, type CacheStore, ContentCache, type ContentRecord, type ContentSchema, DatasetHandle, type FieldKind, type FieldType, MemoryCacheStore, type QueryResult, type QueryShape, type ReadOptions, type SchemaDataset, type SchemaField, type SchemaNode, StudioLayerClient, type StudioLayerClientOptions, StudioLayerError, createClient };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read caching for the content client. GET responses (schema, record lists,
|
|
3
|
+
* single records, queries) are cached by request key; writes invalidate the
|
|
4
|
+
* affected keys automatically. Swap in a custom `CacheStore` to share a cache
|
|
5
|
+
* across client instances or to back it with something persistent.
|
|
6
|
+
*/
|
|
7
|
+
interface CacheEntry {
|
|
8
|
+
value: unknown;
|
|
9
|
+
/** Epoch ms when the entry expires. `0` means it never expires. */
|
|
10
|
+
expiresAt: number;
|
|
11
|
+
}
|
|
12
|
+
/** Pluggable backing store. The default is an in-memory, insertion-ordered map. */
|
|
13
|
+
interface CacheStore {
|
|
14
|
+
get(key: string): CacheEntry | undefined;
|
|
15
|
+
set(key: string, entry: CacheEntry): void;
|
|
16
|
+
delete(key: string): void;
|
|
17
|
+
/** Iterate current keys (used for prefix invalidation). */
|
|
18
|
+
keys(): Iterable<string>;
|
|
19
|
+
clear(): void;
|
|
20
|
+
}
|
|
21
|
+
interface CacheOptions {
|
|
22
|
+
/** Master switch. Default `true`. */
|
|
23
|
+
enabled?: boolean;
|
|
24
|
+
/** Time-to-live for cached reads, in ms. Default `60000`. `0` = never expires. */
|
|
25
|
+
ttl?: number;
|
|
26
|
+
/** Soft cap on entries; oldest are evicted first. Default `500`. */
|
|
27
|
+
maxEntries?: number;
|
|
28
|
+
/** Custom backing store. Defaults to an in-memory store. */
|
|
29
|
+
store?: CacheStore;
|
|
30
|
+
}
|
|
31
|
+
/** In-memory store with insertion-order (FIFO) eviction once `maxEntries` is hit. */
|
|
32
|
+
declare class MemoryCacheStore implements CacheStore {
|
|
33
|
+
private maxEntries;
|
|
34
|
+
private map;
|
|
35
|
+
constructor(maxEntries?: number);
|
|
36
|
+
get(key: string): CacheEntry | undefined;
|
|
37
|
+
set(key: string, entry: CacheEntry): void;
|
|
38
|
+
delete(key: string): void;
|
|
39
|
+
keys(): Iterable<string>;
|
|
40
|
+
clear(): void;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Thin cache facade the client talks to. Owns TTL logic and prefix-based
|
|
44
|
+
* invalidation so the client only deals in cache keys.
|
|
45
|
+
*/
|
|
46
|
+
declare class ContentCache {
|
|
47
|
+
readonly enabled: boolean;
|
|
48
|
+
private ttl;
|
|
49
|
+
private store;
|
|
50
|
+
constructor(opts?: CacheOptions);
|
|
51
|
+
/** Return a fresh cached value for `key`, or `undefined` on miss/expiry. */
|
|
52
|
+
get<T>(key: string, now: number): T | undefined;
|
|
53
|
+
set(key: string, value: unknown, now: number): void;
|
|
54
|
+
/** Drop one exact key. */
|
|
55
|
+
delete(key: string): void;
|
|
56
|
+
/** Drop every key that starts with `prefix`. */
|
|
57
|
+
deletePrefix(prefix: string): void;
|
|
58
|
+
clear(): void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Wire types for the StudioLayer content API. These mirror the server DTOs
|
|
63
|
+
* (`server/utils/content/*`, `server/api/content/*`) one-to-one. Slugs are used
|
|
64
|
+
* on the wire; internal database ids never leak.
|
|
65
|
+
*/
|
|
66
|
+
/** Top-level shape category of a dataset field. */
|
|
67
|
+
type FieldKind = 'primitive' | 'object' | 'collection' | 'reference';
|
|
68
|
+
/** The primitive field types the platform ships. `reference` and `''` also occur. */
|
|
69
|
+
type FieldType = 'text' | 'textarea' | 'number' | 'boolean' | 'color' | 'date' | 'email' | 'file' | 'image' | 'identifier' | 'json' | 'select' | 'url' | 'reference' | '';
|
|
70
|
+
/** One field in a dataset's schema, as returned by `GET /schema`. */
|
|
71
|
+
interface SchemaField {
|
|
72
|
+
slug: string;
|
|
73
|
+
label: string;
|
|
74
|
+
kind: FieldKind;
|
|
75
|
+
type: FieldType;
|
|
76
|
+
required: boolean;
|
|
77
|
+
description: string | null;
|
|
78
|
+
}
|
|
79
|
+
/** A dataset reachable by the key, with its field schema and this key's access. */
|
|
80
|
+
interface SchemaDataset {
|
|
81
|
+
slug: string;
|
|
82
|
+
name: string;
|
|
83
|
+
description: string | null;
|
|
84
|
+
type: string;
|
|
85
|
+
/** Whether the key may read records from this dataset. */
|
|
86
|
+
canRead: boolean;
|
|
87
|
+
/** Whether the key may create/update records in this dataset. */
|
|
88
|
+
canWrite: boolean;
|
|
89
|
+
fields: SchemaField[];
|
|
90
|
+
}
|
|
91
|
+
/** A node reachable by the key, with the datasets it exposes. */
|
|
92
|
+
interface SchemaNode {
|
|
93
|
+
slug: string;
|
|
94
|
+
name: string;
|
|
95
|
+
description: string | null;
|
|
96
|
+
datasets: SchemaDataset[];
|
|
97
|
+
}
|
|
98
|
+
/** Response of `GET /schema`: the full content shape this key can reach. */
|
|
99
|
+
interface ContentSchema {
|
|
100
|
+
nodes: SchemaNode[];
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* A single content record. `data` holds the field values keyed by field slug,
|
|
104
|
+
* with references already inflated (file URLs, referenced-record snapshots).
|
|
105
|
+
* Parameterise it with your own row type for full type safety.
|
|
106
|
+
*/
|
|
107
|
+
interface ContentRecord<T = Record<string, unknown>> {
|
|
108
|
+
uid: string;
|
|
109
|
+
data: T;
|
|
110
|
+
createdAt: string;
|
|
111
|
+
updatedAt: string;
|
|
112
|
+
}
|
|
113
|
+
/** Result shape of a saved query, mirroring the query engine. */
|
|
114
|
+
type QueryShape = 'collection' | 'single' | 'value';
|
|
115
|
+
/** Response of `GET /queries/:slug`. `value` type depends on `shape`. */
|
|
116
|
+
interface QueryResult<V = unknown> {
|
|
117
|
+
query: string;
|
|
118
|
+
shape: QueryShape;
|
|
119
|
+
value: V;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface StudioLayerClientOptions {
|
|
123
|
+
/**
|
|
124
|
+
* Project API key, `slk_...`. Mint one in the project's settings under
|
|
125
|
+
* "API keys". Its reach is the union of its per-node read/write scopes.
|
|
126
|
+
*/
|
|
127
|
+
apiKey: string;
|
|
128
|
+
/**
|
|
129
|
+
* Base URL of your StudioLayer server, without a trailing `/api/content`
|
|
130
|
+
* (that path is appended automatically), e.g. `https://studio.example.com`.
|
|
131
|
+
*/
|
|
132
|
+
baseUrl: string;
|
|
133
|
+
/** Read caching. Pass `false` to disable, or an options object to tune it. */
|
|
134
|
+
cache?: boolean | CacheOptions;
|
|
135
|
+
/** Custom `fetch` implementation. Defaults to the global `fetch`. */
|
|
136
|
+
fetch?: typeof fetch;
|
|
137
|
+
/** Extra headers merged into every request. */
|
|
138
|
+
headers?: Record<string, string>;
|
|
139
|
+
}
|
|
140
|
+
/** Per-call read options. */
|
|
141
|
+
interface ReadOptions {
|
|
142
|
+
/** Set `false` to bypass the cache for this call and always hit the network. */
|
|
143
|
+
cache?: boolean;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Typed client for the StudioLayer content API: read and write a project's node
|
|
147
|
+
* datasets from any website or app. Reads are cached (see the `cache` option);
|
|
148
|
+
* writes invalidate the affected cache entries automatically.
|
|
149
|
+
*
|
|
150
|
+
* ```ts
|
|
151
|
+
* const studio = new StudioLayerClient({ apiKey: 'slk_...', baseUrl: 'https://studio.example.com' })
|
|
152
|
+
* const posts = await studio.dataset<Post>('blog', 'posts').list()
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
declare class StudioLayerClient {
|
|
156
|
+
private readonly apiKey;
|
|
157
|
+
private readonly baseUrl;
|
|
158
|
+
private readonly fetchImpl;
|
|
159
|
+
private readonly headers;
|
|
160
|
+
private readonly cache;
|
|
161
|
+
constructor(options: StudioLayerClientOptions);
|
|
162
|
+
/** The full content shape this key can reach: nodes, datasets, field schemas. */
|
|
163
|
+
schema(opts?: ReadOptions): Promise<ContentSchema>;
|
|
164
|
+
/** Shorthand for `schema()` then `.nodes`. */
|
|
165
|
+
nodes(opts?: ReadOptions): Promise<SchemaNode[]>;
|
|
166
|
+
/** List every record in a dataset (references inflated). Requires read scope. */
|
|
167
|
+
listRecords<T = Record<string, unknown>>(nodeSlug: string, datasetSlug: string, opts?: ReadOptions): Promise<ContentRecord<T>[]>;
|
|
168
|
+
/** Read one record by uid. Requires read scope. Throws 404 if it does not exist. */
|
|
169
|
+
getRecord<T = Record<string, unknown>>(nodeSlug: string, datasetSlug: string, recordUid: string, opts?: ReadOptions): Promise<ContentRecord<T>>;
|
|
170
|
+
/** Create a record. Requires write scope. Invalidates cached reads of the dataset. */
|
|
171
|
+
createRecord<T = Record<string, unknown>>(nodeSlug: string, datasetSlug: string, data: Record<string, unknown>): Promise<ContentRecord<T>>;
|
|
172
|
+
/**
|
|
173
|
+
* Partially update a record. The patch is merged onto the stored data (omitted
|
|
174
|
+
* top-level fields are preserved). Requires write scope. Invalidates cached
|
|
175
|
+
* reads of the dataset and of this record.
|
|
176
|
+
*/
|
|
177
|
+
updateRecord<T = Record<string, unknown>>(nodeSlug: string, datasetSlug: string, recordUid: string, data: Record<string, unknown>): Promise<ContentRecord<T>>;
|
|
178
|
+
/** Run a saved query (`queries.<slug>`) and return its result. Requires read scope. */
|
|
179
|
+
query<V = unknown>(querySlug: string, opts?: ReadOptions): Promise<QueryResult<V>>;
|
|
180
|
+
/**
|
|
181
|
+
* A fluent, type-parameterised handle to one dataset:
|
|
182
|
+
* `client.dataset<Post>('blog', 'posts').list()`.
|
|
183
|
+
*/
|
|
184
|
+
dataset<T = Record<string, unknown>>(nodeSlug: string, datasetSlug: string): DatasetHandle<T>;
|
|
185
|
+
/**
|
|
186
|
+
* Clear cached reads. With no argument, clears everything. Pass a scope to
|
|
187
|
+
* clear just part of it: a node slug, or `{ node, dataset }` for one dataset.
|
|
188
|
+
*/
|
|
189
|
+
clearCache(scope?: string | {
|
|
190
|
+
node: string;
|
|
191
|
+
dataset?: string;
|
|
192
|
+
}): void;
|
|
193
|
+
/** Drop cached record reads for a dataset, plus all query results (which may aggregate it). */
|
|
194
|
+
private invalidateDataset;
|
|
195
|
+
private cachedGet;
|
|
196
|
+
private request;
|
|
197
|
+
}
|
|
198
|
+
/** Fluent, dataset-scoped view returned by `client.dataset()`. */
|
|
199
|
+
declare class DatasetHandle<T = Record<string, unknown>> {
|
|
200
|
+
private readonly client;
|
|
201
|
+
private readonly nodeSlug;
|
|
202
|
+
private readonly datasetSlug;
|
|
203
|
+
constructor(client: StudioLayerClient, nodeSlug: string, datasetSlug: string);
|
|
204
|
+
list(opts?: ReadOptions): Promise<ContentRecord<T>[]>;
|
|
205
|
+
get(recordUid: string, opts?: ReadOptions): Promise<ContentRecord<T>>;
|
|
206
|
+
create(data: Record<string, unknown>): Promise<ContentRecord<T>>;
|
|
207
|
+
update(recordUid: string, data: Record<string, unknown>): Promise<ContentRecord<T>>;
|
|
208
|
+
/** Clear cached reads for this dataset. */
|
|
209
|
+
clearCache(): void;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Error thrown for any non-2xx response from the content API. `status` carries
|
|
214
|
+
* the HTTP status code so callers can branch (401 bad key, 403 out of scope,
|
|
215
|
+
* 404 missing node/dataset/record, 422 query failed).
|
|
216
|
+
*/
|
|
217
|
+
declare class StudioLayerError extends Error {
|
|
218
|
+
readonly status: number;
|
|
219
|
+
/** The raw parsed response body, when the server returned one. */
|
|
220
|
+
readonly body: unknown;
|
|
221
|
+
constructor(message: string, status: number, body?: unknown);
|
|
222
|
+
get isUnauthorized(): boolean;
|
|
223
|
+
get isForbidden(): boolean;
|
|
224
|
+
get isNotFound(): boolean;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Convenience factory: `createClient({ apiKey, baseUrl })`. */
|
|
228
|
+
declare function createClient(options: StudioLayerClientOptions): StudioLayerClient;
|
|
229
|
+
|
|
230
|
+
export { type CacheEntry, type CacheOptions, type CacheStore, ContentCache, type ContentRecord, type ContentSchema, DatasetHandle, type FieldKind, type FieldType, MemoryCacheStore, type QueryResult, type QueryShape, type ReadOptions, type SchemaDataset, type SchemaField, type SchemaNode, StudioLayerClient, type StudioLayerClientOptions, StudioLayerError, createClient };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// src/cache.ts
|
|
2
|
+
var MemoryCacheStore = class {
|
|
3
|
+
constructor(maxEntries = 500) {
|
|
4
|
+
this.maxEntries = maxEntries;
|
|
5
|
+
this.map = /* @__PURE__ */ new Map();
|
|
6
|
+
}
|
|
7
|
+
get(key) {
|
|
8
|
+
return this.map.get(key);
|
|
9
|
+
}
|
|
10
|
+
set(key, entry) {
|
|
11
|
+
this.map.delete(key);
|
|
12
|
+
this.map.set(key, entry);
|
|
13
|
+
while (this.map.size > this.maxEntries) {
|
|
14
|
+
const oldest = this.map.keys().next().value;
|
|
15
|
+
if (oldest === void 0) break;
|
|
16
|
+
this.map.delete(oldest);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
delete(key) {
|
|
20
|
+
this.map.delete(key);
|
|
21
|
+
}
|
|
22
|
+
keys() {
|
|
23
|
+
return this.map.keys();
|
|
24
|
+
}
|
|
25
|
+
clear() {
|
|
26
|
+
this.map.clear();
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var ContentCache = class {
|
|
30
|
+
constructor(opts = {}) {
|
|
31
|
+
this.enabled = opts.enabled ?? true;
|
|
32
|
+
this.ttl = opts.ttl ?? 6e4;
|
|
33
|
+
this.store = opts.store ?? new MemoryCacheStore(opts.maxEntries ?? 500);
|
|
34
|
+
}
|
|
35
|
+
/** Return a fresh cached value for `key`, or `undefined` on miss/expiry. */
|
|
36
|
+
get(key, now) {
|
|
37
|
+
if (!this.enabled) return void 0;
|
|
38
|
+
const entry = this.store.get(key);
|
|
39
|
+
if (!entry) return void 0;
|
|
40
|
+
if (entry.expiresAt !== 0 && entry.expiresAt <= now) {
|
|
41
|
+
this.store.delete(key);
|
|
42
|
+
return void 0;
|
|
43
|
+
}
|
|
44
|
+
return entry.value;
|
|
45
|
+
}
|
|
46
|
+
set(key, value, now) {
|
|
47
|
+
if (!this.enabled) return;
|
|
48
|
+
this.store.set(key, { value, expiresAt: this.ttl === 0 ? 0 : now + this.ttl });
|
|
49
|
+
}
|
|
50
|
+
/** Drop one exact key. */
|
|
51
|
+
delete(key) {
|
|
52
|
+
this.store.delete(key);
|
|
53
|
+
}
|
|
54
|
+
/** Drop every key that starts with `prefix`. */
|
|
55
|
+
deletePrefix(prefix) {
|
|
56
|
+
for (const key of [...this.store.keys()]) {
|
|
57
|
+
if (key.startsWith(prefix)) this.store.delete(key);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
clear() {
|
|
61
|
+
this.store.clear();
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// src/errors.ts
|
|
66
|
+
var StudioLayerError = class _StudioLayerError extends Error {
|
|
67
|
+
constructor(message, status, body) {
|
|
68
|
+
super(message);
|
|
69
|
+
this.name = "StudioLayerError";
|
|
70
|
+
this.status = status;
|
|
71
|
+
this.body = body;
|
|
72
|
+
Object.setPrototypeOf(this, _StudioLayerError.prototype);
|
|
73
|
+
}
|
|
74
|
+
get isUnauthorized() {
|
|
75
|
+
return this.status === 401;
|
|
76
|
+
}
|
|
77
|
+
get isForbidden() {
|
|
78
|
+
return this.status === 403;
|
|
79
|
+
}
|
|
80
|
+
get isNotFound() {
|
|
81
|
+
return this.status === 404;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// src/client.ts
|
|
86
|
+
var StudioLayerClient = class {
|
|
87
|
+
constructor(options) {
|
|
88
|
+
if (!options.apiKey) throw new Error("StudioLayerClient: `apiKey` is required");
|
|
89
|
+
if (!options.baseUrl) throw new Error("StudioLayerClient: `baseUrl` is required");
|
|
90
|
+
this.apiKey = options.apiKey;
|
|
91
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
92
|
+
const resolvedFetch = options.fetch ?? globalThis.fetch;
|
|
93
|
+
if (!resolvedFetch) {
|
|
94
|
+
throw new Error("StudioLayerClient: no `fetch` available; pass one via options.fetch (Node < 18)");
|
|
95
|
+
}
|
|
96
|
+
this.fetchImpl = resolvedFetch.bind(globalThis);
|
|
97
|
+
this.headers = options.headers ?? {};
|
|
98
|
+
const cacheOpt = options.cache;
|
|
99
|
+
this.cache = new ContentCache(
|
|
100
|
+
cacheOpt === false ? { enabled: false } : cacheOpt === true || cacheOpt === void 0 ? {} : cacheOpt
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
// ── Introspection ─────────────────────────────────────────────────────────
|
|
104
|
+
/** The full content shape this key can reach: nodes, datasets, field schemas. */
|
|
105
|
+
async schema(opts) {
|
|
106
|
+
return this.cachedGet("schema", "/schema", opts);
|
|
107
|
+
}
|
|
108
|
+
/** Shorthand for `schema()` then `.nodes`. */
|
|
109
|
+
async nodes(opts) {
|
|
110
|
+
return (await this.schema(opts)).nodes;
|
|
111
|
+
}
|
|
112
|
+
// ── Records ───────────────────────────────────────────────────────────────
|
|
113
|
+
/** List every record in a dataset (references inflated). Requires read scope. */
|
|
114
|
+
async listRecords(nodeSlug, datasetSlug, opts) {
|
|
115
|
+
const res = await this.cachedGet(
|
|
116
|
+
recordsKey(nodeSlug, datasetSlug),
|
|
117
|
+
datasetPath(nodeSlug, datasetSlug),
|
|
118
|
+
opts
|
|
119
|
+
);
|
|
120
|
+
return res.records;
|
|
121
|
+
}
|
|
122
|
+
/** Read one record by uid. Requires read scope. Throws 404 if it does not exist. */
|
|
123
|
+
async getRecord(nodeSlug, datasetSlug, recordUid, opts) {
|
|
124
|
+
const res = await this.cachedGet(
|
|
125
|
+
recordKey(nodeSlug, datasetSlug, recordUid),
|
|
126
|
+
`${datasetPath(nodeSlug, datasetSlug)}/${encodeURIComponent(recordUid)}`,
|
|
127
|
+
opts
|
|
128
|
+
);
|
|
129
|
+
return res.record;
|
|
130
|
+
}
|
|
131
|
+
/** Create a record. Requires write scope. Invalidates cached reads of the dataset. */
|
|
132
|
+
async createRecord(nodeSlug, datasetSlug, data) {
|
|
133
|
+
const res = await this.request(
|
|
134
|
+
"POST",
|
|
135
|
+
datasetPath(nodeSlug, datasetSlug),
|
|
136
|
+
{ data }
|
|
137
|
+
);
|
|
138
|
+
this.invalidateDataset(nodeSlug, datasetSlug);
|
|
139
|
+
return res.record;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Partially update a record. The patch is merged onto the stored data (omitted
|
|
143
|
+
* top-level fields are preserved). Requires write scope. Invalidates cached
|
|
144
|
+
* reads of the dataset and of this record.
|
|
145
|
+
*/
|
|
146
|
+
async updateRecord(nodeSlug, datasetSlug, recordUid, data) {
|
|
147
|
+
const res = await this.request(
|
|
148
|
+
"PATCH",
|
|
149
|
+
`${datasetPath(nodeSlug, datasetSlug)}/${encodeURIComponent(recordUid)}`,
|
|
150
|
+
{ data }
|
|
151
|
+
);
|
|
152
|
+
this.invalidateDataset(nodeSlug, datasetSlug);
|
|
153
|
+
this.cache.delete(recordKey(nodeSlug, datasetSlug, recordUid));
|
|
154
|
+
return res.record;
|
|
155
|
+
}
|
|
156
|
+
// -- Queries --
|
|
157
|
+
/** Run a saved query (`queries.<slug>`) and return its result. Requires read scope. */
|
|
158
|
+
async query(querySlug, opts) {
|
|
159
|
+
return this.cachedGet(
|
|
160
|
+
`query:${querySlug}`,
|
|
161
|
+
`/queries/${encodeURIComponent(querySlug)}`,
|
|
162
|
+
opts
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
// ── Fluent dataset handle ───────────────────────────────────────────────────
|
|
166
|
+
/**
|
|
167
|
+
* A fluent, type-parameterised handle to one dataset:
|
|
168
|
+
* `client.dataset<Post>('blog', 'posts').list()`.
|
|
169
|
+
*/
|
|
170
|
+
dataset(nodeSlug, datasetSlug) {
|
|
171
|
+
return new DatasetHandle(this, nodeSlug, datasetSlug);
|
|
172
|
+
}
|
|
173
|
+
// ── Cache control ───────────────────────────────────────────────────────────
|
|
174
|
+
/**
|
|
175
|
+
* Clear cached reads. With no argument, clears everything. Pass a scope to
|
|
176
|
+
* clear just part of it: a node slug, or `{ node, dataset }` for one dataset.
|
|
177
|
+
*/
|
|
178
|
+
clearCache(scope) {
|
|
179
|
+
if (scope === void 0) {
|
|
180
|
+
this.cache.clear();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (typeof scope === "string") {
|
|
184
|
+
this.cache.deletePrefix(`records:${scope}/`);
|
|
185
|
+
this.cache.deletePrefix(`record:${scope}/`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (scope.dataset) {
|
|
189
|
+
this.invalidateDataset(scope.node, scope.dataset);
|
|
190
|
+
} else {
|
|
191
|
+
this.cache.deletePrefix(`records:${scope.node}/`);
|
|
192
|
+
this.cache.deletePrefix(`record:${scope.node}/`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/** Drop cached record reads for a dataset, plus all query results (which may aggregate it). */
|
|
196
|
+
invalidateDataset(nodeSlug, datasetSlug) {
|
|
197
|
+
this.cache.delete(recordsKey(nodeSlug, datasetSlug));
|
|
198
|
+
this.cache.deletePrefix(`${recordKey(nodeSlug, datasetSlug, "")}`);
|
|
199
|
+
this.cache.deletePrefix("query:");
|
|
200
|
+
}
|
|
201
|
+
// ── Transport ───────────────────────────────────────────────────────────────
|
|
202
|
+
async cachedGet(cacheKey, path, opts) {
|
|
203
|
+
const useCache = opts?.cache !== false;
|
|
204
|
+
const now = Date.now();
|
|
205
|
+
if (useCache) {
|
|
206
|
+
const hit = this.cache.get(cacheKey, now);
|
|
207
|
+
if (hit !== void 0) return hit;
|
|
208
|
+
}
|
|
209
|
+
const value = await this.request("GET", path);
|
|
210
|
+
if (useCache) this.cache.set(cacheKey, value, now);
|
|
211
|
+
return value;
|
|
212
|
+
}
|
|
213
|
+
async request(method, path, body) {
|
|
214
|
+
const res = await this.fetchImpl(`${this.baseUrl}/api/content${path}`, {
|
|
215
|
+
method,
|
|
216
|
+
headers: {
|
|
217
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
218
|
+
...body !== void 0 ? { "Content-Type": "application/json" } : {},
|
|
219
|
+
...this.headers
|
|
220
|
+
},
|
|
221
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
222
|
+
});
|
|
223
|
+
if (!res.ok) {
|
|
224
|
+
let message = res.statusText || `Request failed with status ${res.status}`;
|
|
225
|
+
let parsed;
|
|
226
|
+
try {
|
|
227
|
+
parsed = await res.json();
|
|
228
|
+
const m = parsed;
|
|
229
|
+
if (typeof m?.message === "string") message = m.message;
|
|
230
|
+
else if (typeof m?.statusMessage === "string") message = m.statusMessage;
|
|
231
|
+
} catch {
|
|
232
|
+
}
|
|
233
|
+
throw new StudioLayerError(message, res.status, parsed);
|
|
234
|
+
}
|
|
235
|
+
if (res.status === 204) return void 0;
|
|
236
|
+
return res.json();
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
var DatasetHandle = class {
|
|
240
|
+
constructor(client, nodeSlug, datasetSlug) {
|
|
241
|
+
this.client = client;
|
|
242
|
+
this.nodeSlug = nodeSlug;
|
|
243
|
+
this.datasetSlug = datasetSlug;
|
|
244
|
+
}
|
|
245
|
+
list(opts) {
|
|
246
|
+
return this.client.listRecords(this.nodeSlug, this.datasetSlug, opts);
|
|
247
|
+
}
|
|
248
|
+
get(recordUid, opts) {
|
|
249
|
+
return this.client.getRecord(this.nodeSlug, this.datasetSlug, recordUid, opts);
|
|
250
|
+
}
|
|
251
|
+
create(data) {
|
|
252
|
+
return this.client.createRecord(this.nodeSlug, this.datasetSlug, data);
|
|
253
|
+
}
|
|
254
|
+
update(recordUid, data) {
|
|
255
|
+
return this.client.updateRecord(this.nodeSlug, this.datasetSlug, recordUid, data);
|
|
256
|
+
}
|
|
257
|
+
/** Clear cached reads for this dataset. */
|
|
258
|
+
clearCache() {
|
|
259
|
+
this.client.clearCache({ node: this.nodeSlug, dataset: this.datasetSlug });
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
function datasetPath(nodeSlug, datasetSlug) {
|
|
263
|
+
return `/nodes/${encodeURIComponent(nodeSlug)}/datasets/${encodeURIComponent(datasetSlug)}/records`;
|
|
264
|
+
}
|
|
265
|
+
function recordsKey(nodeSlug, datasetSlug) {
|
|
266
|
+
return `records:${nodeSlug}/${datasetSlug}`;
|
|
267
|
+
}
|
|
268
|
+
function recordKey(nodeSlug, datasetSlug, recordUid) {
|
|
269
|
+
return `record:${nodeSlug}/${datasetSlug}/${recordUid}`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/index.ts
|
|
273
|
+
function createClient(options) {
|
|
274
|
+
return new StudioLayerClient(options);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export { ContentCache, DatasetHandle, MemoryCacheStore, StudioLayerClient, StudioLayerError, createClient };
|
|
278
|
+
//# sourceMappingURL=index.js.map
|
|
279
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cache.ts","../src/errors.ts","../src/client.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Read caching for the content client. GET responses (schema, record lists,\n * single records, queries) are cached by request key; writes invalidate the\n * affected keys automatically. Swap in a custom `CacheStore` to share a cache\n * across client instances or to back it with something persistent.\n */\n\nexport interface CacheEntry {\n value: unknown\n /** Epoch ms when the entry expires. `0` means it never expires. */\n expiresAt: number\n}\n\n/** Pluggable backing store. The default is an in-memory, insertion-ordered map. */\nexport interface CacheStore {\n get(key: string): CacheEntry | undefined\n set(key: string, entry: CacheEntry): void\n delete(key: string): void\n /** Iterate current keys (used for prefix invalidation). */\n keys(): Iterable<string>\n clear(): void\n}\n\nexport interface CacheOptions {\n /** Master switch. Default `true`. */\n enabled?: boolean\n /** Time-to-live for cached reads, in ms. Default `60000`. `0` = never expires. */\n ttl?: number\n /** Soft cap on entries; oldest are evicted first. Default `500`. */\n maxEntries?: number\n /** Custom backing store. Defaults to an in-memory store. */\n store?: CacheStore\n}\n\n/** In-memory store with insertion-order (FIFO) eviction once `maxEntries` is hit. */\nexport class MemoryCacheStore implements CacheStore {\n private map = new Map<string, CacheEntry>()\n\n constructor(private maxEntries = 500) {}\n\n get(key: string): CacheEntry | undefined {\n return this.map.get(key)\n }\n\n set(key: string, entry: CacheEntry): void {\n // Re-insert so recently written keys are considered newest.\n this.map.delete(key)\n this.map.set(key, entry)\n while (this.map.size > this.maxEntries) {\n const oldest = this.map.keys().next().value\n if (oldest === undefined) break\n this.map.delete(oldest)\n }\n }\n\n delete(key: string): void {\n this.map.delete(key)\n }\n\n keys(): Iterable<string> {\n return this.map.keys()\n }\n\n clear(): void {\n this.map.clear()\n }\n}\n\n/**\n * Thin cache facade the client talks to. Owns TTL logic and prefix-based\n * invalidation so the client only deals in cache keys.\n */\nexport class ContentCache {\n readonly enabled: boolean\n private ttl: number\n private store: CacheStore\n\n constructor(opts: CacheOptions = {}) {\n this.enabled = opts.enabled ?? true\n this.ttl = opts.ttl ?? 60_000\n this.store = opts.store ?? new MemoryCacheStore(opts.maxEntries ?? 500)\n }\n\n /** Return a fresh cached value for `key`, or `undefined` on miss/expiry. */\n get<T>(key: string, now: number): T | undefined {\n if (!this.enabled) return undefined\n const entry = this.store.get(key)\n if (!entry) return undefined\n if (entry.expiresAt !== 0 && entry.expiresAt <= now) {\n this.store.delete(key)\n return undefined\n }\n return entry.value as T\n }\n\n set(key: string, value: unknown, now: number): void {\n if (!this.enabled) return\n this.store.set(key, { value, expiresAt: this.ttl === 0 ? 0 : now + this.ttl })\n }\n\n /** Drop one exact key. */\n delete(key: string): void {\n this.store.delete(key)\n }\n\n /** Drop every key that starts with `prefix`. */\n deletePrefix(prefix: string): void {\n for (const key of [...this.store.keys()]) {\n if (key.startsWith(prefix)) this.store.delete(key)\n }\n }\n\n clear(): void {\n this.store.clear()\n }\n}\n","/**\n * Error thrown for any non-2xx response from the content API. `status` carries\n * the HTTP status code so callers can branch (401 bad key, 403 out of scope,\n * 404 missing node/dataset/record, 422 query failed).\n */\nexport class StudioLayerError extends Error {\n readonly status: number\n /** The raw parsed response body, when the server returned one. */\n readonly body: unknown\n\n constructor(message: string, status: number, body?: unknown) {\n super(message)\n this.name = 'StudioLayerError'\n this.status = status\n this.body = body\n // Restore prototype chain for `instanceof` when transpiled to ES5.\n Object.setPrototypeOf(this, StudioLayerError.prototype)\n }\n\n get isUnauthorized(): boolean {\n return this.status === 401\n }\n\n get isForbidden(): boolean {\n return this.status === 403\n }\n\n get isNotFound(): boolean {\n return this.status === 404\n }\n}\n","import { ContentCache, type CacheOptions } from './cache'\nimport { StudioLayerError } from './errors'\nimport type { ContentRecord, ContentSchema, QueryResult, SchemaNode } from './types'\n\nexport interface StudioLayerClientOptions {\n /**\n * Project API key, `slk_...`. Mint one in the project's settings under\n * \"API keys\". Its reach is the union of its per-node read/write scopes.\n */\n apiKey: string\n /**\n * Base URL of your StudioLayer server, without a trailing `/api/content`\n * (that path is appended automatically), e.g. `https://studio.example.com`.\n */\n baseUrl: string\n /** Read caching. Pass `false` to disable, or an options object to tune it. */\n cache?: boolean | CacheOptions\n /** Custom `fetch` implementation. Defaults to the global `fetch`. */\n fetch?: typeof fetch\n /** Extra headers merged into every request. */\n headers?: Record<string, string>\n}\n\n/** Per-call read options. */\nexport interface ReadOptions {\n /** Set `false` to bypass the cache for this call and always hit the network. */\n cache?: boolean\n}\n\ntype Method = 'GET' | 'POST' | 'PATCH'\n\n/**\n * Typed client for the StudioLayer content API: read and write a project's node\n * datasets from any website or app. Reads are cached (see the `cache` option);\n * writes invalidate the affected cache entries automatically.\n *\n * ```ts\n * const studio = new StudioLayerClient({ apiKey: 'slk_...', baseUrl: 'https://studio.example.com' })\n * const posts = await studio.dataset<Post>('blog', 'posts').list()\n * ```\n */\nexport class StudioLayerClient {\n private readonly apiKey: string\n private readonly baseUrl: string\n private readonly fetchImpl: typeof fetch\n private readonly headers: Record<string, string>\n private readonly cache: ContentCache\n\n constructor(options: StudioLayerClientOptions) {\n if (!options.apiKey) throw new Error('StudioLayerClient: `apiKey` is required')\n if (!options.baseUrl) throw new Error('StudioLayerClient: `baseUrl` is required')\n\n this.apiKey = options.apiKey\n this.baseUrl = options.baseUrl.replace(/\\/+$/, '')\n const resolvedFetch = options.fetch ?? globalThis.fetch\n if (!resolvedFetch) {\n throw new Error('StudioLayerClient: no `fetch` available; pass one via options.fetch (Node < 18)')\n }\n this.fetchImpl = resolvedFetch.bind(globalThis)\n this.headers = options.headers ?? {}\n\n const cacheOpt = options.cache\n this.cache = new ContentCache(\n cacheOpt === false ? { enabled: false }\n : cacheOpt === true || cacheOpt === undefined ? {}\n : cacheOpt,\n )\n }\n\n // ── Introspection ─────────────────────────────────────────────────────────\n\n /** The full content shape this key can reach: nodes, datasets, field schemas. */\n async schema(opts?: ReadOptions): Promise<ContentSchema> {\n return this.cachedGet<ContentSchema>('schema', '/schema', opts)\n }\n\n /** Shorthand for `schema()` then `.nodes`. */\n async nodes(opts?: ReadOptions): Promise<SchemaNode[]> {\n return (await this.schema(opts)).nodes\n }\n\n // ── Records ───────────────────────────────────────────────────────────────\n\n /** List every record in a dataset (references inflated). Requires read scope. */\n async listRecords<T = Record<string, unknown>>(\n nodeSlug: string,\n datasetSlug: string,\n opts?: ReadOptions,\n ): Promise<ContentRecord<T>[]> {\n const res = await this.cachedGet<{ records: ContentRecord<T>[] }>(\n recordsKey(nodeSlug, datasetSlug),\n datasetPath(nodeSlug, datasetSlug),\n opts,\n )\n return res.records\n }\n\n /** Read one record by uid. Requires read scope. Throws 404 if it does not exist. */\n async getRecord<T = Record<string, unknown>>(\n nodeSlug: string,\n datasetSlug: string,\n recordUid: string,\n opts?: ReadOptions,\n ): Promise<ContentRecord<T>> {\n const res = await this.cachedGet<{ record: ContentRecord<T> }>(\n recordKey(nodeSlug, datasetSlug, recordUid),\n `${datasetPath(nodeSlug, datasetSlug)}/${encodeURIComponent(recordUid)}`,\n opts,\n )\n return res.record\n }\n\n /** Create a record. Requires write scope. Invalidates cached reads of the dataset. */\n async createRecord<T = Record<string, unknown>>(\n nodeSlug: string,\n datasetSlug: string,\n data: Record<string, unknown>,\n ): Promise<ContentRecord<T>> {\n const res = await this.request<{ record: ContentRecord<T> }>(\n 'POST',\n datasetPath(nodeSlug, datasetSlug),\n { data },\n )\n this.invalidateDataset(nodeSlug, datasetSlug)\n return res.record\n }\n\n /**\n * Partially update a record. The patch is merged onto the stored data (omitted\n * top-level fields are preserved). Requires write scope. Invalidates cached\n * reads of the dataset and of this record.\n */\n async updateRecord<T = Record<string, unknown>>(\n nodeSlug: string,\n datasetSlug: string,\n recordUid: string,\n data: Record<string, unknown>,\n ): Promise<ContentRecord<T>> {\n const res = await this.request<{ record: ContentRecord<T> }>(\n 'PATCH',\n `${datasetPath(nodeSlug, datasetSlug)}/${encodeURIComponent(recordUid)}`,\n { data },\n )\n this.invalidateDataset(nodeSlug, datasetSlug)\n this.cache.delete(recordKey(nodeSlug, datasetSlug, recordUid))\n return res.record\n }\n\n // -- Queries --\n\n /** Run a saved query (`queries.<slug>`) and return its result. Requires read scope. */\n async query<V = unknown>(querySlug: string, opts?: ReadOptions): Promise<QueryResult<V>> {\n return this.cachedGet<QueryResult<V>>(\n `query:${querySlug}`,\n `/queries/${encodeURIComponent(querySlug)}`,\n opts,\n )\n }\n\n // ── Fluent dataset handle ───────────────────────────────────────────────────\n\n /**\n * A fluent, type-parameterised handle to one dataset:\n * `client.dataset<Post>('blog', 'posts').list()`.\n */\n dataset<T = Record<string, unknown>>(nodeSlug: string, datasetSlug: string): DatasetHandle<T> {\n return new DatasetHandle<T>(this, nodeSlug, datasetSlug)\n }\n\n // ── Cache control ───────────────────────────────────────────────────────────\n\n /**\n * Clear cached reads. With no argument, clears everything. Pass a scope to\n * clear just part of it: a node slug, or `{ node, dataset }` for one dataset.\n */\n clearCache(scope?: string | { node: string, dataset?: string }): void {\n if (scope === undefined) {\n this.cache.clear()\n return\n }\n if (typeof scope === 'string') {\n this.cache.deletePrefix(`records:${scope}/`)\n this.cache.deletePrefix(`record:${scope}/`)\n return\n }\n if (scope.dataset) {\n this.invalidateDataset(scope.node, scope.dataset)\n } else {\n this.cache.deletePrefix(`records:${scope.node}/`)\n this.cache.deletePrefix(`record:${scope.node}/`)\n }\n }\n\n /** Drop cached record reads for a dataset, plus all query results (which may aggregate it). */\n private invalidateDataset(nodeSlug: string, datasetSlug: string): void {\n this.cache.delete(recordsKey(nodeSlug, datasetSlug))\n this.cache.deletePrefix(`${recordKey(nodeSlug, datasetSlug, '')}`)\n this.cache.deletePrefix('query:')\n }\n\n // ── Transport ───────────────────────────────────────────────────────────────\n\n private async cachedGet<T>(cacheKey: string, path: string, opts?: ReadOptions): Promise<T> {\n const useCache = opts?.cache !== false\n const now = Date.now()\n if (useCache) {\n const hit = this.cache.get<T>(cacheKey, now)\n if (hit !== undefined) return hit\n }\n const value = await this.request<T>('GET', path)\n if (useCache) this.cache.set(cacheKey, value, now)\n return value\n }\n\n private async request<T>(method: Method, path: string, body?: unknown): Promise<T> {\n const res = await this.fetchImpl(`${this.baseUrl}/api/content${path}`, {\n method,\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),\n ...this.headers,\n },\n body: body !== undefined ? JSON.stringify(body) : undefined,\n })\n\n if (!res.ok) {\n let message = res.statusText || `Request failed with status ${res.status}`\n let parsed: unknown\n try {\n parsed = await res.json()\n const m = parsed as { message?: unknown, statusMessage?: unknown }\n if (typeof m?.message === 'string') message = m.message\n else if (typeof m?.statusMessage === 'string') message = m.statusMessage\n } catch {\n /* non-JSON error body; keep the status text */\n }\n throw new StudioLayerError(message, res.status, parsed)\n }\n\n if (res.status === 204) return undefined as T\n return res.json() as Promise<T>\n }\n}\n\n/** Fluent, dataset-scoped view returned by `client.dataset()`. */\nexport class DatasetHandle<T = Record<string, unknown>> {\n constructor(\n private readonly client: StudioLayerClient,\n private readonly nodeSlug: string,\n private readonly datasetSlug: string,\n ) {}\n\n list(opts?: ReadOptions): Promise<ContentRecord<T>[]> {\n return this.client.listRecords<T>(this.nodeSlug, this.datasetSlug, opts)\n }\n\n get(recordUid: string, opts?: ReadOptions): Promise<ContentRecord<T>> {\n return this.client.getRecord<T>(this.nodeSlug, this.datasetSlug, recordUid, opts)\n }\n\n create(data: Record<string, unknown>): Promise<ContentRecord<T>> {\n return this.client.createRecord<T>(this.nodeSlug, this.datasetSlug, data)\n }\n\n update(recordUid: string, data: Record<string, unknown>): Promise<ContentRecord<T>> {\n return this.client.updateRecord<T>(this.nodeSlug, this.datasetSlug, recordUid, data)\n }\n\n /** Clear cached reads for this dataset. */\n clearCache(): void {\n this.client.clearCache({ node: this.nodeSlug, dataset: this.datasetSlug })\n }\n}\n\n// ── Cache-key + path helpers ──────────────────────────────────────────────────\n\nfunction datasetPath(nodeSlug: string, datasetSlug: string): string {\n return `/nodes/${encodeURIComponent(nodeSlug)}/datasets/${encodeURIComponent(datasetSlug)}/records`\n}\n\nfunction recordsKey(nodeSlug: string, datasetSlug: string): string {\n return `records:${nodeSlug}/${datasetSlug}`\n}\n\nfunction recordKey(nodeSlug: string, datasetSlug: string, recordUid: string): string {\n return `record:${nodeSlug}/${datasetSlug}/${recordUid}`\n}\n","export { StudioLayerClient, DatasetHandle } from './client'\nexport type { StudioLayerClientOptions, ReadOptions } from './client'\nexport { StudioLayerError } from './errors'\nexport { ContentCache, MemoryCacheStore } from './cache'\nexport type { CacheOptions, CacheStore, CacheEntry } from './cache'\nexport type {\n ContentSchema,\n SchemaNode,\n SchemaDataset,\n SchemaField,\n FieldKind,\n FieldType,\n ContentRecord,\n QueryResult,\n QueryShape,\n} from './types'\n\nimport { StudioLayerClient, type StudioLayerClientOptions } from './client'\n\n/** Convenience factory: `createClient({ apiKey, baseUrl })`. */\nexport function createClient(options: StudioLayerClientOptions): StudioLayerClient {\n return new StudioLayerClient(options)\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@studiolayer/client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Typed client for the StudioLayer content API. Read and write project node datasets from any website or app, headless-CMS style and beyond.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.cjs",
|
|
10
|
+
"module": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"require": "./dist/index.cjs"
|
|
17
|
+
},
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"dev": "tsup --watch",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"prepublishOnly": "npm run build"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"studiolayer",
|
|
33
|
+
"headless-cms",
|
|
34
|
+
"content-api",
|
|
35
|
+
"cms",
|
|
36
|
+
"datasets",
|
|
37
|
+
"client",
|
|
38
|
+
"sdk"
|
|
39
|
+
],
|
|
40
|
+
"author": "StudioLayer",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"tsup": "^8.3.5",
|
|
47
|
+
"typescript": "^5.7.2"
|
|
48
|
+
}
|
|
49
|
+
}
|