@decoupla/sdk 0.1.0 → 0.1.2

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 CHANGED
@@ -1,4 +1,4 @@
1
- # Decoupla.js
1
+ # @decoupla/sdk
2
2
 
3
3
  A **type-safe TypeScript client** for the Decoupla headless CMS with integrated CLI tools for schema management and synchronization.
4
4
 
@@ -13,47 +13,33 @@ A **type-safe TypeScript client** for the Decoupla headless CMS with integrated
13
13
 
14
14
  ### 1. Installation
15
15
 
16
- Install via npm (recommended) or Bun. The package name is taken from the npm package metadata.
16
+ Install the package from npm (recommended) or with Bun/Yarn. The package name is
17
+ `@decoupla/sdk` and includes both the TypeScript client and the CLI binary.
17
18
 
18
19
  ```bash
19
20
  # npm
21
+ npm install @decoupla/sdk
20
22
 
21
- const Author = defineContentType({
22
- name: 'author',
23
- displayName: 'Author',
24
- description: 'Blog post authors',
25
- fields: {
26
- Name: { type: 'string', required: true, isLabel: true },
27
- Email: { type: 'string', required: false },
28
- Bio: { type: 'text', required: false },
29
- },
30
- });
23
+ # yarn
24
+ yarn add @decoupla/sdk
31
25
 
32
- const BlogPost = defineContentType({
33
- name: 'blog_post',
34
- displayName: 'Blog Post',
35
- description: 'Published blog posts',
36
- fields: {
37
- Title: { type: 'string', required: true, isLabel: true },
38
- Content: { type: 'text', required: true },
39
- Author: {
40
- type: 'reference',
41
- required: false,
42
- references: [Author],
43
- },
44
- IsPublished: { type: 'boolean', required: false },
45
- ViewCount: { type: 'int', required: false },
46
- },
47
- });
48
-
49
- // Export your configuration
50
- export default defineConfig({
51
- workspace: process.env.DECOUPLA_WORKSPACE!,
52
- apiToken: process.env.DECOUPLA_API_TOKEN!,
53
- contentTypes: [Author, BlogPost],
54
- });
26
+ # bun
27
+ bun add @decoupla/sdk
55
28
  ```
56
29
 
30
+ Notes:
31
+ - The package exports TypeScript types; if you're using TypeScript the types are included.
32
+ - The CLI (`decoupla`) is available via `npx decoupla ...` (npm) or `bunx decoupla ...` (Bun).
33
+
34
+ After installing, continue to define your content types and configuration (see the
35
+ "Content Type Definition" section below.
36
+
37
+ ### 2. Define Content Types
38
+
39
+ Define your content types with `defineContentType` (see the "Content Type Definition" section).
40
+ Keep your content type definitions exported from a central module (for example `src/content-types.ts`) so
41
+ both your application code and `decoupla.config.ts` can import the same typed definitions.
42
+
57
43
  ### 3. Sync Your Schema
58
44
 
59
45
  Run the CLI to sync your local definitions with the backend. You can use `npx` (or `bunx` if you're on Bun) to run the installed binary:
@@ -91,17 +77,30 @@ const posts = await client.getEntries(BlogPost, {
91
77
  limit: 10,
92
78
  });
93
79
 
94
- // Create an entry
95
- const newPost = await client.createEntry(BlogPost, {
80
+ // NOTE:
81
+ // The `published` option passed to `createEntry`/`updateEntry` is a server-side
82
+ // command that tells the backend whether the entry should be published immediately.
83
+ // Create an entry as draft (unpublished)
84
+
85
+ const newPostMeta = await client.createEntry(BlogPost, {
96
86
  Title: 'My First Post',
97
87
  Content: 'Hello, World!',
98
- IsPublished: false,
99
- });
88
+ }, false);
100
89
 
101
- // Update an entry
102
- await client.updateEntry(BlogPost, newPost.id, {
103
- IsPublished: true,
104
- });
90
+ // Create an entry and request preloaded relations in the response
91
+ const newPostWithPreload = await client.createEntry(BlogPost, {
92
+ Title: 'Post with Preload',
93
+ Content: 'This post requests preload',
94
+ Author: 'author-id-123',
95
+ }, { published: true, preload: ['Author'] });
96
+
97
+ // Note: when you pass `preload` the client will return the full normalized entry
98
+ // inside a `{ data: ... }` payload so you can access `newPostWithPreload.data.author` directly.
99
+
100
+ // Update an entry — provide an options object for publishing and preload behavior
101
+ const updateResult = await client.updateEntry(BlogPost, newPostMeta.id, {
102
+ Title: 'My First Post (published)'
103
+ }, { published: true, preload: ['Author'] });
105
104
  ```
106
105
 
107
106
  ---
@@ -134,6 +133,43 @@ const MyType = defineContentType({
134
133
  Note: `name` is the machine-facing slug used to identify the content type in the API and CLI. It will be normalized to snake_case by the config loader (e.g. "BlogPost" -> "blog_post", "My Type" -> "my_type"). `displayName` is optional and intended as a human-readable label shown in UIs and logs.
135
134
  ```
136
135
 
136
+ **Important:** We strongly recommend using a `decoupla.config.ts` file to store your `workspace`, `apiToken`, and `contentTypes`.
137
+ The CLI reads this file when you run `decoupla sync`, and keeping a central config ensures the CLI and your application share the same type definitions and settings.
138
+
139
+ ### decoupla.config.ts and contentTypes
140
+
141
+ Your project should export a configuration file (usually `decoupla.config.ts`) that the CLI reads. The important piece is the `contentTypes` array — this is where you list the content type definitions created with `defineContentType`.
142
+
143
+ Example `decoupla.config.ts`:
144
+
145
+ ```typescript
146
+ import { defineConfig } from 'decoupla.js';
147
+ import { Author, BlogPost, Category } from './content-types'; // your defineContentType exports
148
+
149
+ export default defineConfig({
150
+ workspace: process.env.DECOUPLA_WORKSPACE!,
151
+ apiToken: process.env.DECOUPLA_API_TOKEN!,
152
+ contentTypes: [Author, BlogPost, Category],
153
+ });
154
+ ```
155
+
156
+ Notes:
157
+ - The CLI (`decoupla sync`) reads the exported `contentTypes` array and uses it to compare and synchronize your local schema with the remote workspace.
158
+ - Each item in `contentTypes` should be the result of `defineContentType(...)` (the library stores enough metadata to produce API requests and type-safe helpers).
159
+ - Keep the file next to your content-type definitions (for example `src/content-types.ts`) and export each content type so both the CLI and your application code can import the same definitions.
160
+
161
+ Using content type definitions in your application code:
162
+
163
+ ```typescript
164
+ import { createClient } from '@decoupla/sdk';
165
+ import { BlogPost } from './content-types'; // same defineContentType exported above
166
+
167
+ const client = createClient({ workspace: '...', apiToken: '...' });
168
+
169
+ const post = await client.getEntry(BlogPost, 'post-id', { preload: ['Author'] });
170
+ ```
171
+
172
+
137
173
  ### Field Types
138
174
 
139
175
  Decoupla supports the following field types:
@@ -199,7 +235,6 @@ Decoupla supports the following field types:
199
235
  required: true, // Field must be provided
200
236
  isLabel: true, // Use as display name
201
237
  options: ['active', 'inactive'], // Restrict values (string/string[] only)
202
- settings: { /* custom */ }, // Future: localization, validations, etc.
203
238
  }
204
239
  ```
205
240
 
@@ -268,15 +303,17 @@ const client = createClient({
268
303
 
269
304
  ```typescript
270
305
  {
271
- getEntry: (contentTypeDef, entryId, options?) => Promise
272
- getEntries: (contentTypeDef, options?) => Promise
273
- createEntry: (contentTypeDef, fieldValues, published?) => Promise
274
- updateEntry: (contentTypeDef, entryId, fieldValues, published?) => Promise
275
- upload: (file, filename?) => Promise
276
- inspect: () => Promise
277
- sync: (contentTypes) => Promise
278
- syncWithFields: (contentTypes, options?) => Promise
279
- deleteContentType: (contentTypeName) => Promise
306
+ getEntry: (contentTypeDef, entryId, options?) => Promise<{ data: Entry }>
307
+ getEntries: (contentTypeDef, options?) => Promise<{ data: Entry[] }>
308
+ // createEntry/updateEntry accept an options object { published?: boolean; preload?: PreloadSpec }.
309
+ // When `preload` is provided the client returns the full normalized entry in `{ data: ... }`.
310
+ createEntry: (contentTypeDef, fieldValues, publishedOrOptions?) => Promise<NormalizedEntryMetadata | { data: Entry }>
311
+ updateEntry: (contentTypeDef, entryId, fieldValues, publishedOrOptions?) => Promise<NormalizedEntryMetadata | { data: Entry }>
312
+ upload: (file, filename?) => Promise<UploadResult>
313
+ inspect: () => Promise<InspectResponse>
314
+ sync: (contentTypes) => Promise<SyncResult>
315
+ syncWithFields: (contentTypes, options?) => Promise<SyncResult>
316
+ deleteContentType: (contentTypeName) => Promise<void>
280
317
  }
281
318
  ```
282
319
 
@@ -304,6 +341,21 @@ console.log(post.data.title); // Type-safe field access
304
341
  }
305
342
  ```
306
343
 
344
+ Note: you can now explicitly select which dataset you want to read using the `contentView` option.
345
+ Acceptable values are `'live'` (published content) and `'preview'` (draft/unpublished data). The default is `'live'`.
346
+
347
+ The client maps `contentView` to the server `api_type` parameter. Use `contentView: 'preview'` to
348
+ request draft/unpublished data and `contentView: 'live'` for published data.
349
+
350
+ Example:
351
+
352
+ ```typescript
353
+ const postPreview = await client.getEntry(BlogPost, 'post-id-123', {
354
+ contentView: 'preview',
355
+ preload: [['Child', ['Child']]]
356
+ });
357
+ ```
358
+
307
359
  Preload supports a nested-array grammar for multi-level reference preloads. Example: to preload a reference field `Child` and then preload its `Child` field, use:
308
360
 
309
361
  ```typescript
@@ -411,14 +463,24 @@ const results = await client.getEntries(BlogPost, {
411
463
  Create a new entry:
412
464
 
413
465
  ```typescript
414
- const entry = await client.createEntry(BlogPost, {
466
+ // Boolean `published` positional arg
467
+ const meta = await client.createEntry(BlogPost, {
415
468
  Title: 'My First Post',
416
469
  Content: 'Hello, World!',
417
- IsPublished: false,
418
- });
470
+ }, false);
471
+
472
+ // Or use the options object to pass `published` and `preload`.
473
+ // When `preload` is provided the client returns the full normalized entry
474
+ // inside `{ data: ... }` so you can read relations directly.
475
+ const created = await client.createEntry(BlogPost, {
476
+ Title: 'Post with relations',
477
+ Content: 'Post body',
478
+ Author: 'author-id-123',
479
+ }, { published: true, preload: ['Author'] });
419
480
 
420
- console.log(entry.id); // Entry ID
421
- console.log(entry.createdAt); // Timestamp
481
+ // Access metadata or full data depending on call:
482
+ console.log(meta.id); // Entry metadata (no preload)
483
+ console.log(created.data.author); // Preloaded relation (if requested)
422
484
  ```
423
485
 
424
486
  **Returns:**
@@ -440,10 +502,16 @@ console.log(entry.createdAt); // Timestamp
440
502
  Update an existing entry:
441
503
 
442
504
  ```typescript
505
+ // Backwards-compatible boolean `published` positional arg
506
+ const updatedMeta = await client.updateEntry(BlogPost, 'post-id-123', {
507
+ ViewCount: 150,
508
+ }, false);
509
+
510
+ // Or use options object and request preload in the response
443
511
  const updated = await client.updateEntry(BlogPost, 'post-id-123', {
444
512
  ViewCount: 150,
445
- IsPublished: true,
446
- });
513
+ }, { published: true, preload: ['Author'] });
514
+ // If preload was requested updated will be { data: { ...full entry... } }
447
515
  ```
448
516
 
449
517
  ### `upload(file, filename?)`
@@ -803,15 +871,3 @@ TypeScript enforces correct filter operations per field type:
803
871
  { Title: { eq: 'hello' } }
804
872
  { ViewCount: { gte: 100 } }
805
873
  ```
806
-
807
- ---
808
-
809
- ## Contributing
810
-
811
- Contributions are welcome! Please check the [CONTRIBUTING.md](./CONTRIBUTING.md) file.
812
-
813
- ---
814
-
815
- ## License
816
-
817
- MIT © 2024 Decoupla
@@ -8161,6 +8161,10 @@ var preloadType = import_zod.z.lazy(
8161
8161
  var requestSchema = import_zod.z.object({
8162
8162
  type: import_zod.z.string().optional(),
8163
8163
  op_type: import_zod.z.enum(["get_entry", "get_entries", "inspect"]),
8164
+ // Which API surface to target when fetching entries. Defaults to 'live' in the client.
8165
+ api_type: import_zod.z.enum(["live", "preview"]).optional(),
8166
+ // entry_id is required for get_entry operations (top-level param)
8167
+ entry_id: import_zod.z.string().optional(),
8164
8168
  filters: import_zod.z.any().optional(),
8165
8169
  preload: preloadType.optional(),
8166
8170
  sort: import_zod.z.array(import_zod.z.tuple([import_zod.z.string(), import_zod.z.enum(["ASC", "DESC"])])).optional(),
@@ -8295,6 +8299,8 @@ function buildCreateFieldRequest(modelId, fieldName, fieldDef) {
8295
8299
  control,
8296
8300
  required: fieldDef.required ?? false,
8297
8301
  description: (_b = fieldDef.settings) == null ? void 0 : _b.description,
8302
+ is_label: fieldDef.isLabel ?? false,
8303
+ options: fieldDef.options,
8298
8304
  meta: Object.keys(meta).length > 0 ? meta : void 0
8299
8305
  };
8300
8306
  }
@@ -8312,6 +8318,12 @@ function buildUpdateFieldRequest(fieldId, changes) {
8312
8318
  if (changes.description !== void 0) {
8313
8319
  request.description = changes.description;
8314
8320
  }
8321
+ if (changes.isLabel !== void 0) {
8322
+ request.is_label = !!changes.isLabel;
8323
+ }
8324
+ if (changes.options !== void 0) {
8325
+ request.options = changes.options;
8326
+ }
8315
8327
  if (changes.references !== void 0) {
8316
8328
  request.meta = { ...changes.meta || {}, reference_types: changes.references };
8317
8329
  } else if (changes.meta !== void 0) {
@@ -8384,6 +8396,7 @@ var makeRequest = (options) => async (request) => {
8384
8396
  const { apiToken, workspace } = options;
8385
8397
  const {
8386
8398
  op_type,
8399
+ api_type,
8387
8400
  filters,
8388
8401
  type,
8389
8402
  preload,
@@ -8396,15 +8409,20 @@ var makeRequest = (options) => async (request) => {
8396
8409
  acc.push({ field, direction });
8397
8410
  return acc;
8398
8411
  }, void 0);
8412
+ const outgoingApiType = op_type === "inspect" ? void 0 : api_type ?? "live";
8399
8413
  const requestBody = {
8400
8414
  op_type,
8401
8415
  type,
8416
+ // pass through entry_id when present (used by get_entry)
8417
+ entry_id: request.entry_id,
8402
8418
  filters,
8403
8419
  preload,
8404
8420
  sort: sendSort,
8405
8421
  limit,
8406
8422
  offset
8407
8423
  };
8424
+ if (outgoingApiType)
8425
+ requestBody.api_type = outgoingApiType;
8408
8426
  const req = await fetch(`${DECOUPLA_API_URL_BASE}${workspace}`, {
8409
8427
  method: "POST",
8410
8428
  headers: {
@@ -8441,7 +8459,7 @@ ${errorDetails}`);
8441
8459
  return respData;
8442
8460
  };
8443
8461
  var getEntry = (request) => async (contentTypeDef, entryId, options) => {
8444
- var _a;
8462
+ var _a, _b;
8445
8463
  const normalizePreload = (p) => {
8446
8464
  if (!Array.isArray(p))
8447
8465
  return p;
@@ -8476,34 +8494,24 @@ var getEntry = (request) => async (contentTypeDef, entryId, options) => {
8476
8494
  }
8477
8495
  return out;
8478
8496
  };
8497
+ const chosenApiType = (options == null ? void 0 : options.contentView) ?? "live";
8479
8498
  const reqBody = {
8480
8499
  op_type: "get_entry",
8481
8500
  type: contentTypeDef.__definition.name,
8482
8501
  entry_id: entryId,
8483
- preload: normalizePreload((options == null ? void 0 : options.preload) || [])
8502
+ preload: normalizePreload((options == null ? void 0 : options.preload) || []),
8503
+ api_type: chosenApiType
8484
8504
  };
8485
8505
  try {
8486
8506
  debug("[getEntry] request body:", JSON.stringify(reqBody));
8487
8507
  } catch (e) {
8488
8508
  }
8489
- const resp = await fetch(`${DECOUPLA_API_URL_BASE}${process.env.DECOUPLA_WORKSPACE}`, {
8490
- method: "POST",
8491
- headers: {
8492
- "Content-Type": "application/json",
8493
- "Authorization": `Bearer ${process.env.DECOUPLA_API_TOKEN}`
8494
- },
8495
- body: JSON.stringify(reqBody)
8496
- });
8497
- const respData = await resp.json();
8509
+ const resp = await request(reqBody);
8498
8510
  try {
8499
- debug("[getEntry] raw response:", JSON.stringify(respData, null, 2));
8511
+ debug("[getEntry] raw response:", JSON.stringify(resp, null, 2));
8500
8512
  } catch (e) {
8501
8513
  }
8502
- if (respData.errors) {
8503
- console.error("Get Entry Error:", respData);
8504
- throw new Error(`API Error: ${respData.errors.map((e) => e.message).join(", ")}`);
8505
- }
8506
- const entry = (_a = respData.data) == null ? void 0 : _a.entry;
8514
+ const entry = ((_a = resp == null ? void 0 : resp.data) == null ? void 0 : _a.entry) || ((_b = resp == null ? void 0 : resp.data) == null ? void 0 : _b.node);
8507
8515
  const normalizedEntry = {};
8508
8516
  for (const [key, value] of Object.entries(entry || {})) {
8509
8517
  normalizedEntry[snakeToCamel(key)] = value;
@@ -8525,8 +8533,10 @@ var getEntries = (request) => async (contentTypeDef, options) => {
8525
8533
  limit,
8526
8534
  offset,
8527
8535
  preload = [],
8528
- sort = []
8536
+ sort = [],
8537
+ contentView: optContentView
8529
8538
  } = options;
8539
+ const api_type = optContentView ?? "live";
8530
8540
  const normalizePreload = (p) => {
8531
8541
  if (!Array.isArray(p))
8532
8542
  return p;
@@ -8651,6 +8661,7 @@ var getEntries = (request) => async (contentTypeDef, options) => {
8651
8661
  const reqBody = {
8652
8662
  type: contentTypeDef.__definition.name,
8653
8663
  op_type: "get_entries",
8664
+ api_type,
8654
8665
  limit,
8655
8666
  offset,
8656
8667
  filters: backendFilters,
@@ -9170,20 +9181,153 @@ var normalizeEntryMetadata = (entry) => ({
9170
9181
  createdAt: entry.created_at,
9171
9182
  updatedAt: entry.updated_at
9172
9183
  });
9173
- var createEntry = (options) => async (contentTypeDef, fieldValues, published = true) => {
9174
- var _a;
9184
+ var createEntry = (options) => async (contentTypeDef, fieldValues, optionsParam) => {
9185
+ var _a, _b, _c;
9175
9186
  const { apiToken, workspace } = options;
9187
+ const opts = typeof optionsParam === "boolean" ? { published: optionsParam } : optionsParam || {};
9188
+ const published = opts.published ?? true;
9176
9189
  const validation = validateFieldValues(fieldValues);
9177
9190
  if (!validation.valid) {
9178
9191
  throw new Error(validation.error || "Invalid field values");
9179
9192
  }
9180
9193
  const normalizedFieldValues = normalizeFieldValues(fieldValues);
9194
+ try {
9195
+ const fieldDefs = ((_a = contentTypeDef.__definition) == null ? void 0 : _a.fields) || {};
9196
+ const keyMap = /* @__PURE__ */ new Map();
9197
+ for (const defKey of Object.keys(fieldDefs)) {
9198
+ keyMap.set(defKey, defKey);
9199
+ try {
9200
+ keyMap.set(camelToSnake(defKey), defKey);
9201
+ } catch (e) {
9202
+ }
9203
+ try {
9204
+ keyMap.set(snakeToCamel(camelToSnake(defKey)), defKey);
9205
+ } catch (e) {
9206
+ }
9207
+ }
9208
+ for (const [k, v] of Object.entries(normalizedFieldValues)) {
9209
+ const defKey = keyMap.get(k) || void 0;
9210
+ if (!defKey)
9211
+ continue;
9212
+ const fdef = fieldDefs[defKey];
9213
+ if (!fdef)
9214
+ continue;
9215
+ if (fdef.type === "date") {
9216
+ const toDateOnly = (val) => {
9217
+ if (val instanceof Date) {
9218
+ const y = val.getFullYear();
9219
+ const m = String(val.getMonth() + 1).padStart(2, "0");
9220
+ const d = String(val.getDate()).padStart(2, "0");
9221
+ return `${y}-${m}-${d}`;
9222
+ }
9223
+ if (typeof val === "string") {
9224
+ if (/^\d{4}-\d{2}-\d{2}$/.test(val))
9225
+ return val;
9226
+ const parsed = new Date(val);
9227
+ if (!isNaN(parsed.getTime())) {
9228
+ const y = parsed.getFullYear();
9229
+ const m = String(parsed.getMonth() + 1).padStart(2, "0");
9230
+ const d = String(parsed.getDate()).padStart(2, "0");
9231
+ return `${y}-${m}-${d}`;
9232
+ }
9233
+ }
9234
+ return val;
9235
+ };
9236
+ normalizedFieldValues[k] = toDateOnly(v);
9237
+ }
9238
+ }
9239
+ } catch (e) {
9240
+ }
9241
+ try {
9242
+ const fieldDefs = ((_b = contentTypeDef.__definition) == null ? void 0 : _b.fields) || {};
9243
+ const keyMap = /* @__PURE__ */ new Map();
9244
+ for (const defKey of Object.keys(fieldDefs)) {
9245
+ keyMap.set(defKey, defKey);
9246
+ try {
9247
+ keyMap.set(camelToSnake(defKey), defKey);
9248
+ } catch (e) {
9249
+ }
9250
+ try {
9251
+ keyMap.set(snakeToCamel(camelToSnake(defKey)), defKey);
9252
+ } catch (e) {
9253
+ }
9254
+ }
9255
+ for (const [k, v] of Object.entries(normalizedFieldValues)) {
9256
+ const defKey = keyMap.get(k) || keyMap.get(k) || void 0;
9257
+ if (!defKey)
9258
+ continue;
9259
+ const fdef = fieldDefs[defKey];
9260
+ if (!fdef)
9261
+ continue;
9262
+ if (fdef.type === "date") {
9263
+ const toDateOnly = (val) => {
9264
+ if (val instanceof Date) {
9265
+ const y = val.getFullYear();
9266
+ const m = String(val.getMonth() + 1).padStart(2, "0");
9267
+ const d = String(val.getDate()).padStart(2, "0");
9268
+ return `${y}-${m}-${d}`;
9269
+ }
9270
+ if (typeof val === "string") {
9271
+ if (/^\d{4}-\d{2}-\d{2}$/.test(val))
9272
+ return val;
9273
+ const parsed = new Date(val);
9274
+ if (!isNaN(parsed.getTime())) {
9275
+ const y = parsed.getFullYear();
9276
+ const m = String(parsed.getMonth() + 1).padStart(2, "0");
9277
+ const d = String(parsed.getDate()).padStart(2, "0");
9278
+ return `${y}-${m}-${d}`;
9279
+ }
9280
+ }
9281
+ return val;
9282
+ };
9283
+ normalizedFieldValues[k] = toDateOnly(v);
9284
+ }
9285
+ }
9286
+ } catch (e) {
9287
+ }
9288
+ const normalizePreload = (p) => {
9289
+ if (!Array.isArray(p))
9290
+ return p;
9291
+ const out = [];
9292
+ for (const item of p) {
9293
+ if (typeof item === "string") {
9294
+ out.push(camelToSnake(item));
9295
+ continue;
9296
+ }
9297
+ if (!Array.isArray(item)) {
9298
+ out.push(item);
9299
+ continue;
9300
+ }
9301
+ const [key, inner] = item;
9302
+ if (inner == null) {
9303
+ out.push(camelToSnake(key));
9304
+ continue;
9305
+ }
9306
+ let innerArr;
9307
+ if (typeof inner === "string") {
9308
+ innerArr = [inner];
9309
+ } else if (Array.isArray(inner)) {
9310
+ if (inner.length === 2 && typeof inner[0] === "string" && (Array.isArray(inner[1]) || inner[1] == null)) {
9311
+ innerArr = [inner];
9312
+ } else {
9313
+ innerArr = inner;
9314
+ }
9315
+ } else {
9316
+ innerArr = [inner];
9317
+ }
9318
+ out.push([camelToSnake(key), normalizePreload(innerArr)]);
9319
+ }
9320
+ return out;
9321
+ };
9181
9322
  const requestBody = {
9182
9323
  op_type: "create_entry",
9183
9324
  type: contentTypeDef.__definition.name,
9184
9325
  field_values: normalizedFieldValues,
9185
9326
  published
9186
9327
  };
9328
+ if (opts.preload) {
9329
+ requestBody.preload = normalizePreload(opts.preload);
9330
+ }
9187
9331
  const response = await fetch(`${DECOUPLA_API_URL_BASE}${workspace}`, {
9188
9332
  method: "POST",
9189
9333
  headers: {
@@ -9199,15 +9343,24 @@ var createEntry = (options) => async (contentTypeDef, fieldValues, published = t
9199
9343
  throw new Error(`Failed to create entry: ${errorMessages}`);
9200
9344
  }
9201
9345
  const createResponse = respData;
9202
- const entry = (_a = createResponse.data) == null ? void 0 : _a.entry;
9346
+ const entry = (_c = createResponse.data) == null ? void 0 : _c.entry;
9203
9347
  if (!entry) {
9204
9348
  throw new Error(
9205
9349
  `Failed to create entry: Content type "${contentTypeDef.__definition.name}" returned null. This typically means the content type has no fields defined. Please add at least one field to the content type before creating entries.`
9206
9350
  );
9207
9351
  }
9352
+ if (opts.preload) {
9353
+ const normalized = { id: entry.id };
9354
+ for (const [key, value] of Object.entries(entry)) {
9355
+ if (key !== "id") {
9356
+ normalized[snakeToCamel(key)] = value;
9357
+ }
9358
+ }
9359
+ return { data: normalized };
9360
+ }
9208
9361
  return normalizeEntryMetadata(entry);
9209
9362
  };
9210
- var updateEntry = (options) => async (contentTypeDef, entryId, fieldValues, published) => {
9363
+ var updateEntry = (options) => async (contentTypeDef, entryId, fieldValues, optionsParam) => {
9211
9364
  var _a;
9212
9365
  const { apiToken, workspace } = options;
9213
9366
  if (!isValidUUID(entryId)) {
@@ -9223,8 +9376,46 @@ var updateEntry = (options) => async (contentTypeDef, entryId, fieldValues, publ
9223
9376
  entry_id: entryId,
9224
9377
  field_values: normalizedFieldValues
9225
9378
  };
9226
- if (published !== void 0) {
9227
- requestBody.published = published;
9379
+ const opts = typeof optionsParam === "boolean" ? { published: optionsParam } : optionsParam || {};
9380
+ if (opts.published !== void 0) {
9381
+ requestBody.published = opts.published;
9382
+ }
9383
+ const normalizePreload = (p) => {
9384
+ if (!Array.isArray(p))
9385
+ return p;
9386
+ const out = [];
9387
+ for (const item of p) {
9388
+ if (typeof item === "string") {
9389
+ out.push(camelToSnake(item));
9390
+ continue;
9391
+ }
9392
+ if (!Array.isArray(item)) {
9393
+ out.push(item);
9394
+ continue;
9395
+ }
9396
+ const [key, inner] = item;
9397
+ if (inner == null) {
9398
+ out.push(camelToSnake(key));
9399
+ continue;
9400
+ }
9401
+ let innerArr;
9402
+ if (typeof inner === "string") {
9403
+ innerArr = [inner];
9404
+ } else if (Array.isArray(inner)) {
9405
+ if (inner.length === 2 && typeof inner[0] === "string" && (Array.isArray(inner[1]) || inner[1] == null)) {
9406
+ innerArr = [inner];
9407
+ } else {
9408
+ innerArr = inner;
9409
+ }
9410
+ } else {
9411
+ innerArr = [inner];
9412
+ }
9413
+ out.push([camelToSnake(key), normalizePreload(innerArr)]);
9414
+ }
9415
+ return out;
9416
+ };
9417
+ if (opts.preload) {
9418
+ requestBody.preload = normalizePreload(opts.preload);
9228
9419
  }
9229
9420
  debug(`[DEBUG] Update Entry Request:`, JSON.stringify(requestBody, null, 2));
9230
9421
  const response = await fetch(`${DECOUPLA_API_URL_BASE}${workspace}`, {
@@ -9362,6 +9553,37 @@ var createClient = (config) => {
9362
9553
  getEntries: getEntries(request),
9363
9554
  // Note: inline preload literal inference is supported by getEntries overloads.
9364
9555
  inspect: inspect(request),
9556
+ /**
9557
+ * Validate whether the current token can read the requested content view.
9558
+ * Returns true when the view is accessible, false when an authorization error is returned.
9559
+ * This helper inspects the remote for a content type and issues a harmless get_entries
9560
+ * against that content type using the requested view; it treats a structured
9561
+ * `{ errors: [{ field: 'authorization', ... }] }` as a permission failure.
9562
+ */
9563
+ validateContentView: async (view) => {
9564
+ var _a, _b;
9565
+ try {
9566
+ const inspectResp = await inspect(request)();
9567
+ const firstCT = inspectResp.data.content_types && inspectResp.data.content_types[0];
9568
+ if (!firstCT)
9569
+ return true;
9570
+ const typeName = firstCT.slug || firstCT.id;
9571
+ const resp = await request({ op_type: "get_entries", type: typeName, limit: 1, api_type: view }).catch((err) => ({ __err: err }));
9572
+ if (resp == null ? void 0 : resp.__err) {
9573
+ throw resp.__err;
9574
+ }
9575
+ if (resp.errors || ((_a = resp.data) == null ? void 0 : _a.errors)) {
9576
+ const errors = resp.errors || ((_b = resp.data) == null ? void 0 : _b.errors) || [];
9577
+ return !errors.some((e) => e.field === "authorization");
9578
+ }
9579
+ return true;
9580
+ } catch (e) {
9581
+ if (e && typeof e === "object" && e.errors) {
9582
+ return !e.errors.some((er) => er.field === "authorization");
9583
+ }
9584
+ throw e;
9585
+ }
9586
+ },
9365
9587
  sync: sync(request),
9366
9588
  syncWithFields: syncWithFieldsBound,
9367
9589
  upload: upload({ apiToken, workspace }),