@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 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"]}
@@ -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 };
@@ -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
+ }