@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 +128 -72
- package/dist/cli/index.cjs +246 -24
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +246 -24
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +246 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +30 -3
- package/dist/index.js +246 -24
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
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
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
//
|
|
95
|
-
|
|
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
|
-
|
|
99
|
-
});
|
|
88
|
+
}, false);
|
|
100
89
|
|
|
101
|
-
//
|
|
102
|
-
await client.
|
|
103
|
-
|
|
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
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
466
|
+
// Boolean `published` positional arg
|
|
467
|
+
const meta = await client.createEntry(BlogPost, {
|
|
415
468
|
Title: 'My First Post',
|
|
416
469
|
Content: 'Hello, World!',
|
|
417
|
-
|
|
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
|
-
|
|
421
|
-
console.log(
|
|
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
|
-
|
|
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
|
package/dist/cli/index.cjs
CHANGED
|
@@ -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
|
|
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(
|
|
8511
|
+
debug("[getEntry] raw response:", JSON.stringify(resp, null, 2));
|
|
8500
8512
|
} catch (e) {
|
|
8501
8513
|
}
|
|
8502
|
-
|
|
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,
|
|
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 = (
|
|
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,
|
|
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
|
-
|
|
9227
|
-
|
|
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 }),
|