@terminalfour/terminalfour-js 1.1.0 → 1.1.1
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/dist/cjs/handlebars.js +1 -0
- package/dist/cjs/models/content-item.js +1 -0
- package/dist/cjs/models/media-category-item.js +2 -0
- package/dist/cjs/models/media-item.js +1 -0
- package/dist/cjs/models/section-item.js +1 -0
- package/dist/cjs/resources/content-resource.js +2 -0
- package/dist/cjs/resources/content-type-resource.js +3 -0
- package/dist/cjs/resources/group-resource.js +1 -0
- package/dist/cjs/resources/list-resource.js +1 -0
- package/dist/cjs/resources/media-resource.js +9 -2
- package/dist/cjs/resources/media-type-resource.js +1 -0
- package/dist/cjs/resources/navigation-resource.js +1 -0
- package/dist/cjs/resources/page-layout-resource.js +1 -0
- package/dist/cjs/section-ref.js +5 -0
- package/dist/cjs/utils.d.ts +17 -0
- package/dist/cjs/utils.js +29 -0
- package/dist/esm/handlebars.js +2 -1
- package/dist/esm/models/content-item.js +2 -1
- package/dist/esm/models/media-category-item.js +2 -0
- package/dist/esm/models/media-item.js +2 -1
- package/dist/esm/models/section-item.js +2 -1
- package/dist/esm/resources/content-resource.js +3 -1
- package/dist/esm/resources/content-type-resource.js +4 -1
- package/dist/esm/resources/group-resource.js +2 -1
- package/dist/esm/resources/list-resource.js +2 -1
- package/dist/esm/resources/media-resource.js +10 -3
- package/dist/esm/resources/media-type-resource.js +2 -1
- package/dist/esm/resources/navigation-resource.js +2 -1
- package/dist/esm/resources/page-layout-resource.js +2 -1
- package/dist/esm/section-ref.js +6 -1
- package/dist/esm/utils.d.ts +17 -0
- package/dist/esm/utils.js +27 -0
- package/docs/content-types.md +1 -1
- package/docs/error-handling.md +26 -0
- package/package.json +1 -1
package/dist/cjs/handlebars.js
CHANGED
|
@@ -25,6 +25,7 @@ class HandlebarsItem {
|
|
|
25
25
|
* Always saves with approved status.
|
|
26
26
|
*/
|
|
27
27
|
async save() {
|
|
28
|
+
(0, utils_js_1.assertRequired)(this.name, 'Name');
|
|
28
29
|
// Update elements with current name and code
|
|
29
30
|
const elements = { ...this._rawDTO.elements };
|
|
30
31
|
for (const key of Object.keys(elements)) {
|
|
@@ -502,6 +502,7 @@ class ContentItem {
|
|
|
502
502
|
* save and approve in one step.
|
|
503
503
|
*/
|
|
504
504
|
async save() {
|
|
505
|
+
(0, utils_js_1.assertRequired)(this.name, 'Content name');
|
|
505
506
|
// Start from the original raw elements (correct API format)
|
|
506
507
|
const rawElements = { ...this._rawDTO.elements };
|
|
507
508
|
// Re-resolve dirty fields through the ElementResolver so friendly values
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MediaCategoryItem = void 0;
|
|
4
|
+
const utils_js_1 = require("../utils.js");
|
|
4
5
|
/**
|
|
5
6
|
* A mutable media category object. Modify properties and call save() to persist.
|
|
6
7
|
*/
|
|
@@ -17,6 +18,7 @@ class MediaCategoryItem {
|
|
|
17
18
|
}
|
|
18
19
|
/** Persists current property values to the server via PUT. */
|
|
19
20
|
async save() {
|
|
21
|
+
(0, utils_js_1.assertRequired)(this.name, 'Media category name');
|
|
20
22
|
const updated = {
|
|
21
23
|
...this._rawData,
|
|
22
24
|
name: this.name,
|
|
@@ -80,6 +80,7 @@ class MediaItem {
|
|
|
80
80
|
* If `content` has changed on non-binary media, the version is bumped.
|
|
81
81
|
*/
|
|
82
82
|
async save() {
|
|
83
|
+
(0, utils_js_1.assertRequired)(this.name, 'Media name');
|
|
83
84
|
const categoryId = this.categories[0];
|
|
84
85
|
if (!categoryId) {
|
|
85
86
|
throw new Error('Cannot save media item — no category assigned.');
|
|
@@ -34,6 +34,7 @@ class SectionItem {
|
|
|
34
34
|
}
|
|
35
35
|
/** Persists current property values to the server via PUT. */
|
|
36
36
|
async save() {
|
|
37
|
+
(0, utils_js_1.assertRequired)(this.name, 'Section name');
|
|
37
38
|
const statusCode = utils_js_1.STATUS_CODES[this.status] ?? Number(this._rawData.status) ?? 0;
|
|
38
39
|
const updated = {
|
|
39
40
|
...this._rawData,
|
|
@@ -100,6 +100,7 @@ class ContentResource {
|
|
|
100
100
|
}
|
|
101
101
|
/** Creates a new content item in this section. */
|
|
102
102
|
async create(data, options) {
|
|
103
|
+
(0, utils_js_1.assertRequired)(data.name, 'Content name');
|
|
103
104
|
const language = (0, utils_js_1.resolveLanguage)(options?.language, this.defaultLanguage);
|
|
104
105
|
const template = await this.getTemplate(data.type);
|
|
105
106
|
const contentId = -Math.floor(Math.random() * 1000000);
|
|
@@ -135,6 +136,7 @@ class ContentResource {
|
|
|
135
136
|
}
|
|
136
137
|
/** Updates an existing content item's fields. */
|
|
137
138
|
async update(id, data, options) {
|
|
139
|
+
(0, utils_js_1.assertNotEmptyIfPresent)(data.name, 'Content name');
|
|
138
140
|
const language = (0, utils_js_1.resolveLanguage)(options?.language, this.defaultLanguage);
|
|
139
141
|
// 1. Fetch the existing content (full body needed for the POST)
|
|
140
142
|
const existing = await this.httpClient.request({
|
|
@@ -249,6 +249,7 @@ class ContentType {
|
|
|
249
249
|
return new Layout(raw, httpClient, contentTypeId, layoutElements, fetchLayouts, resolveSyntaxId, resolveProcessorId, resolveExtension);
|
|
250
250
|
},
|
|
251
251
|
update: async (layoutName, updateData) => {
|
|
252
|
+
(0, utils_js_1.assertNotEmptyIfPresent)(updateData.name, 'Layout name');
|
|
252
253
|
const all = await fetchLayouts();
|
|
253
254
|
const match = all.find((l) => l.name === layoutName);
|
|
254
255
|
if (!match)
|
|
@@ -289,6 +290,7 @@ class ContentType {
|
|
|
289
290
|
return new Layout(response, httpClient, contentTypeId, layoutElements, fetchLayouts, resolveSyntaxId, resolveProcessorId, resolveExtension);
|
|
290
291
|
},
|
|
291
292
|
create: async (createData) => {
|
|
293
|
+
(0, utils_js_1.assertRequired)(createData.name, 'Layout name');
|
|
292
294
|
// Check name uniqueness
|
|
293
295
|
const existing = await fetchLayouts();
|
|
294
296
|
if (existing.some((l) => l.name === createData.name)) {
|
|
@@ -590,6 +592,7 @@ class ContentType {
|
|
|
590
592
|
}
|
|
591
593
|
/** Persists current property values to the server via PUT. */
|
|
592
594
|
async save() {
|
|
595
|
+
(0, utils_js_1.assertRequired)(this.name, 'Content type name');
|
|
593
596
|
(0, utils_js_1.assertGroupsValid)(this.primaryGroup, this.sharedGroups);
|
|
594
597
|
const authLevel = String(utils_js_1.AUTH_LEVEL_REVERSE[this.minUserLevel] ?? this._rawData.minAuthLevel ?? 2);
|
|
595
598
|
// Sync field changes back to raw contentTypeElements
|
|
@@ -43,6 +43,7 @@ class Group {
|
|
|
43
43
|
}
|
|
44
44
|
/** Persists current property values to the server via PUT. */
|
|
45
45
|
async save() {
|
|
46
|
+
(0, utils_js_1.assertRequired)(this.name, 'Group name');
|
|
46
47
|
// Start from existing raw members
|
|
47
48
|
let rawMembers = [...(this._rawData.members ?? [])];
|
|
48
49
|
// Remove members
|
|
@@ -56,6 +56,7 @@ class List {
|
|
|
56
56
|
}
|
|
57
57
|
/** Persists current property values to the server via PUT. */
|
|
58
58
|
async save() {
|
|
59
|
+
(0, utils_js_2.assertRequired)(this.name, 'List name');
|
|
59
60
|
if (this.isForcedLanguage && this.isDefaultLanguage) {
|
|
60
61
|
throw new Error('isForcedLanguage and isDefaultLanguage cannot both be true');
|
|
61
62
|
}
|
|
@@ -53,10 +53,17 @@ class MediaResource {
|
|
|
53
53
|
* Returns the new media item.
|
|
54
54
|
*/
|
|
55
55
|
async create(data) {
|
|
56
|
-
|
|
57
|
-
throw new Error('Media name is required');
|
|
56
|
+
(0, utils_js_1.assertRequired)(data.name, 'Media name');
|
|
58
57
|
if (!data.category)
|
|
59
58
|
throw new Error('Media category is required');
|
|
59
|
+
// A media item is meaningless without its file/binary. Guard the common
|
|
60
|
+
// empty cases: missing, or an empty { file } wrapper.
|
|
61
|
+
const fileValue = data.file && typeof data.file === 'object' && !(data.file instanceof Blob) && 'file' in data.file
|
|
62
|
+
? data.file.file
|
|
63
|
+
: data.file;
|
|
64
|
+
if (fileValue === undefined || fileValue === null || fileValue === '') {
|
|
65
|
+
throw new Error('Media file is required');
|
|
66
|
+
}
|
|
60
67
|
const language = data.language ?? 'smxx';
|
|
61
68
|
const filename = (0, utils_js_1.deriveFilename)(data.file);
|
|
62
69
|
const ext = getExtension(filename);
|
|
@@ -68,6 +68,7 @@ class MediaType {
|
|
|
68
68
|
}
|
|
69
69
|
/** Persists current property values to the server via PUT. */
|
|
70
70
|
async save() {
|
|
71
|
+
(0, utils_js_1.assertRequired)(this.name, 'Media type name');
|
|
71
72
|
// Sync defaultLayout into the layouts array
|
|
72
73
|
if (this.defaultLayout) {
|
|
73
74
|
for (const layout of this.layouts) {
|
|
@@ -1006,6 +1006,7 @@ class NavigationObject {
|
|
|
1006
1006
|
}
|
|
1007
1007
|
/** Persists current property values to the server via PUT. */
|
|
1008
1008
|
async save() {
|
|
1009
|
+
(0, utils_js_1.assertRequired)(this.name, 'Navigation object name');
|
|
1009
1010
|
(0, utils_js_1.assertGroupsValid)(this.primaryGroup, this.sharedGroups);
|
|
1010
1011
|
// Apply type-aware write transformation (coerce back to strings, derive hidden fields)
|
|
1011
1012
|
const camelStringProps = transformPropertiesWrite(this.type, this.properties);
|
|
@@ -31,6 +31,7 @@ class PageLayout {
|
|
|
31
31
|
}
|
|
32
32
|
/** Persists current property values to the server via PUT. */
|
|
33
33
|
async save() {
|
|
34
|
+
(0, utils_js_1.assertRequired)(this.name, 'Page layout name');
|
|
34
35
|
(0, utils_js_1.assertGroupsValid)(this.primaryGroup, this.sharedGroups);
|
|
35
36
|
// Resolve syntax name to ID
|
|
36
37
|
let syntaxId = this._rawData.syntaxType;
|
package/dist/cjs/section-ref.js
CHANGED
|
@@ -568,6 +568,7 @@ class SectionRef {
|
|
|
568
568
|
* If `data.customFields` is provided, creates and saves section metadata content.
|
|
569
569
|
*/
|
|
570
570
|
async addSection(data, options) {
|
|
571
|
+
(0, utils_js_1.assertRequired)(data.name, 'Section name');
|
|
571
572
|
const language = (0, utils_js_1.resolveLanguage)(options?.language, this.defaultLanguage);
|
|
572
573
|
// Fetch this section's details to inherit config
|
|
573
574
|
const parentSection = await this.httpClient.request({
|
|
@@ -712,6 +713,10 @@ class SectionRef {
|
|
|
712
713
|
* Returns the updated SectionItem.
|
|
713
714
|
*/
|
|
714
715
|
async update(data, options) {
|
|
716
|
+
// This path PUTs directly (it does not go through SectionItem.save()), so
|
|
717
|
+
// guard the name here. Present-only: omitting name is valid (e.g. delete()
|
|
718
|
+
// calls update({ status: 'inactive' })), but setting it blank is not.
|
|
719
|
+
(0, utils_js_1.assertNotEmptyIfPresent)(data.name, 'Section name');
|
|
715
720
|
const language = (0, utils_js_1.resolveLanguage)(options?.language, this.defaultLanguage);
|
|
716
721
|
const section = await this.httpClient.request({
|
|
717
722
|
method: 'GET',
|
package/dist/cjs/utils.d.ts
CHANGED
|
@@ -107,6 +107,23 @@ export declare function writeSharedGroups(ids?: number[]): Array<{
|
|
|
107
107
|
* group" and is never a conflict.
|
|
108
108
|
*/
|
|
109
109
|
export declare function assertGroupsValid(primaryGroup: number, sharedGroups?: number[]): void;
|
|
110
|
+
/**
|
|
111
|
+
* Asserts that a required string value is present and not blank.
|
|
112
|
+
*
|
|
113
|
+
* Use on create paths where the field must always be provided. Throws
|
|
114
|
+
* `"<Label> is required"` when the value is missing, empty, or whitespace-only.
|
|
115
|
+
* Prevents opaque API 500s from malformed requests missing a required field.
|
|
116
|
+
*/
|
|
117
|
+
export declare function assertRequired(value: string | null | undefined, label: string): void;
|
|
118
|
+
/**
|
|
119
|
+
* Asserts that a value, *if provided*, is not blank.
|
|
120
|
+
*
|
|
121
|
+
* Use on update paths where a field is optional (omitting it leaves the
|
|
122
|
+
* current value unchanged) but explicitly setting it to an empty or
|
|
123
|
+
* whitespace-only string is invalid. `undefined` passes (means "no change");
|
|
124
|
+
* `''` or whitespace throws `"<Label> cannot be empty"`.
|
|
125
|
+
*/
|
|
126
|
+
export declare function assertNotEmptyIfPresent(value: string | null | undefined, label: string): void;
|
|
110
127
|
/** Accepted file input: a file path, URL, Blob, ReadableStream, or { file, filename } object */
|
|
111
128
|
export type FileInput = string | Blob | NodeJS.ReadableStream | {
|
|
112
129
|
file: string | Blob | NodeJS.ReadableStream;
|
package/dist/cjs/utils.js
CHANGED
|
@@ -47,6 +47,8 @@ exports.readSharedGroups = readSharedGroups;
|
|
|
47
47
|
exports.writePrimaryGroup = writePrimaryGroup;
|
|
48
48
|
exports.writeSharedGroups = writeSharedGroups;
|
|
49
49
|
exports.assertGroupsValid = assertGroupsValid;
|
|
50
|
+
exports.assertRequired = assertRequired;
|
|
51
|
+
exports.assertNotEmptyIfPresent = assertNotEmptyIfPresent;
|
|
50
52
|
exports.resolveFileToBlob = resolveFileToBlob;
|
|
51
53
|
exports.deriveFilename = deriveFilename;
|
|
52
54
|
exports.debugWarn = debugWarn;
|
|
@@ -258,6 +260,33 @@ function assertGroupsValid(primaryGroup, sharedGroups) {
|
|
|
258
260
|
'Remove it from sharedGroups or choose a different primaryGroup.');
|
|
259
261
|
}
|
|
260
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* Asserts that a required string value is present and not blank.
|
|
265
|
+
*
|
|
266
|
+
* Use on create paths where the field must always be provided. Throws
|
|
267
|
+
* `"<Label> is required"` when the value is missing, empty, or whitespace-only.
|
|
268
|
+
* Prevents opaque API 500s from malformed requests missing a required field.
|
|
269
|
+
*/
|
|
270
|
+
function assertRequired(value, label) {
|
|
271
|
+
if (!value || !value.trim()) {
|
|
272
|
+
throw new Error(`${label} is required`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Asserts that a value, *if provided*, is not blank.
|
|
277
|
+
*
|
|
278
|
+
* Use on update paths where a field is optional (omitting it leaves the
|
|
279
|
+
* current value unchanged) but explicitly setting it to an empty or
|
|
280
|
+
* whitespace-only string is invalid. `undefined` passes (means "no change");
|
|
281
|
+
* `''` or whitespace throws `"<Label> cannot be empty"`.
|
|
282
|
+
*/
|
|
283
|
+
function assertNotEmptyIfPresent(value, label) {
|
|
284
|
+
if (value === undefined || value === null)
|
|
285
|
+
return;
|
|
286
|
+
if (!value.trim()) {
|
|
287
|
+
throw new Error(`${label} cannot be empty`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
261
290
|
/**
|
|
262
291
|
* Resolves a file input (path, URL, Blob, or ReadableStream) to a Blob.
|
|
263
292
|
*/
|
package/dist/esm/handlebars.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_CACHE_TTL, getCacheEpoch } from './utils.js';
|
|
1
|
+
import { DEFAULT_CACHE_TTL, getCacheEpoch, assertRequired } from './utils.js';
|
|
2
2
|
/**
|
|
3
3
|
* A mutable Handlebars item (helper or partial).
|
|
4
4
|
* Modify `name` and/or `code`, then call `save()` to persist.
|
|
@@ -22,6 +22,7 @@ export class HandlebarsItem {
|
|
|
22
22
|
* Always saves with approved status.
|
|
23
23
|
*/
|
|
24
24
|
async save() {
|
|
25
|
+
assertRequired(this.name, 'Name');
|
|
25
26
|
// Update elements with current name and code
|
|
26
27
|
const elements = { ...this._rawDTO.elements };
|
|
27
28
|
for (const key of Object.keys(elements)) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatFileSize, parseElementKey, mapStatus, flattenGroups, STATUS_CODES, AUTH_LEVEL_MAP, debugWarn, DEFAULT_CACHE_TTL, getCacheEpoch } from '../utils.js';
|
|
1
|
+
import { formatFileSize, parseElementKey, mapStatus, flattenGroups, STATUS_CODES, AUTH_LEVEL_MAP, debugWarn, DEFAULT_CACHE_TTL, getCacheEpoch, assertRequired } from '../utils.js';
|
|
2
2
|
/** Symbol used to restrict _init() access to the factory function in this module */
|
|
3
3
|
const INIT = Symbol('ContentItem.init');
|
|
4
4
|
/**
|
|
@@ -497,6 +497,7 @@ export class ContentItem {
|
|
|
497
497
|
* save and approve in one step.
|
|
498
498
|
*/
|
|
499
499
|
async save() {
|
|
500
|
+
assertRequired(this.name, 'Content name');
|
|
500
501
|
// Start from the original raw elements (correct API format)
|
|
501
502
|
const rawElements = { ...this._rawDTO.elements };
|
|
502
503
|
// Re-resolve dirty fields through the ElementResolver so friendly values
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertRequired } from '../utils.js';
|
|
1
2
|
/**
|
|
2
3
|
* A mutable media category object. Modify properties and call save() to persist.
|
|
3
4
|
*/
|
|
@@ -14,6 +15,7 @@ export class MediaCategoryItem {
|
|
|
14
15
|
}
|
|
15
16
|
/** Persists current property values to the server via PUT. */
|
|
16
17
|
async save() {
|
|
18
|
+
assertRequired(this.name, 'Media category name');
|
|
17
19
|
const updated = {
|
|
18
20
|
...this._rawData,
|
|
19
21
|
name: this.name,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatFileSize, parseElementKey, STATUS_MAP, resolveFileToBlob, deriveFilename } from '../utils.js';
|
|
1
|
+
import { formatFileSize, parseElementKey, STATUS_MAP, resolveFileToBlob, deriveFilename, assertRequired } from '../utils.js';
|
|
2
2
|
/** Extension → syntax type mapping for non-binary media */
|
|
3
3
|
const EXTENSION_SYNTAX_MAP = {
|
|
4
4
|
js: 1,
|
|
@@ -77,6 +77,7 @@ export class MediaItem {
|
|
|
77
77
|
* If `content` has changed on non-binary media, the version is bumped.
|
|
78
78
|
*/
|
|
79
79
|
async save() {
|
|
80
|
+
assertRequired(this.name, 'Media name');
|
|
80
81
|
const categoryId = this.categories[0];
|
|
81
82
|
if (!categoryId) {
|
|
82
83
|
throw new Error('Cannot save media item — no category assigned.');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mapStatus, STATUS_CODES } from '../utils.js';
|
|
1
|
+
import { mapStatus, STATUS_CODES, assertRequired } from '../utils.js';
|
|
2
2
|
import { ContentResource } from '../resources/content-resource.js';
|
|
3
3
|
/**
|
|
4
4
|
* A mutable section object. Modify properties and call save() to persist.
|
|
@@ -31,6 +31,7 @@ export class SectionItem {
|
|
|
31
31
|
}
|
|
32
32
|
/** Persists current property values to the server via PUT. */
|
|
33
33
|
async save() {
|
|
34
|
+
assertRequired(this.name, 'Section name');
|
|
34
35
|
const statusCode = STATUS_CODES[this.status] ?? Number(this._rawData.status) ?? 0;
|
|
35
36
|
const updated = {
|
|
36
37
|
...this._rawData,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveLanguage, toTimestamp, STATUS_CODES } from '../utils.js';
|
|
1
|
+
import { resolveLanguage, toTimestamp, STATUS_CODES, assertRequired, assertNotEmptyIfPresent } from '../utils.js';
|
|
2
2
|
import { createContentItem } from '../models/content-item.js';
|
|
3
3
|
import { ContentCache } from '../content-cache.js';
|
|
4
4
|
/**
|
|
@@ -97,6 +97,7 @@ export class ContentResource {
|
|
|
97
97
|
}
|
|
98
98
|
/** Creates a new content item in this section. */
|
|
99
99
|
async create(data, options) {
|
|
100
|
+
assertRequired(data.name, 'Content name');
|
|
100
101
|
const language = resolveLanguage(options?.language, this.defaultLanguage);
|
|
101
102
|
const template = await this.getTemplate(data.type);
|
|
102
103
|
const contentId = -Math.floor(Math.random() * 1000000);
|
|
@@ -132,6 +133,7 @@ export class ContentResource {
|
|
|
132
133
|
}
|
|
133
134
|
/** Updates an existing content item's fields. */
|
|
134
135
|
async update(id, data, options) {
|
|
136
|
+
assertNotEmptyIfPresent(data.name, 'Content name');
|
|
135
137
|
const language = resolveLanguage(options?.language, this.defaultLanguage);
|
|
136
138
|
// 1. Fetch the existing content (full body needed for the POST)
|
|
137
139
|
const existing = await this.httpClient.request({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { decodeHtmlEntities, AUTH_LEVEL_MAP, AUTH_LEVEL_REVERSE, debugWarn, DEFAULT_CACHE_TTL, getCacheEpoch, invalidateAllCaches, readPrimaryGroup, readSharedGroups, writePrimaryGroup, writeSharedGroups, assertGroupsValid } from '../utils.js';
|
|
1
|
+
import { decodeHtmlEntities, AUTH_LEVEL_MAP, AUTH_LEVEL_REVERSE, debugWarn, DEFAULT_CACHE_TTL, getCacheEpoch, invalidateAllCaches, readPrimaryGroup, readSharedGroups, writePrimaryGroup, writeSharedGroups, assertGroupsValid, assertRequired, assertNotEmptyIfPresent } from '../utils.js';
|
|
2
2
|
/** The content type category ID that marks a system content type. */
|
|
3
3
|
const SYSTEM_CONTENT_TYPE = 30;
|
|
4
4
|
function resolveTypeName(type, typeMap) {
|
|
@@ -245,6 +245,7 @@ export class ContentType {
|
|
|
245
245
|
return new Layout(raw, httpClient, contentTypeId, layoutElements, fetchLayouts, resolveSyntaxId, resolveProcessorId, resolveExtension);
|
|
246
246
|
},
|
|
247
247
|
update: async (layoutName, updateData) => {
|
|
248
|
+
assertNotEmptyIfPresent(updateData.name, 'Layout name');
|
|
248
249
|
const all = await fetchLayouts();
|
|
249
250
|
const match = all.find((l) => l.name === layoutName);
|
|
250
251
|
if (!match)
|
|
@@ -285,6 +286,7 @@ export class ContentType {
|
|
|
285
286
|
return new Layout(response, httpClient, contentTypeId, layoutElements, fetchLayouts, resolveSyntaxId, resolveProcessorId, resolveExtension);
|
|
286
287
|
},
|
|
287
288
|
create: async (createData) => {
|
|
289
|
+
assertRequired(createData.name, 'Layout name');
|
|
288
290
|
// Check name uniqueness
|
|
289
291
|
const existing = await fetchLayouts();
|
|
290
292
|
if (existing.some((l) => l.name === createData.name)) {
|
|
@@ -586,6 +588,7 @@ export class ContentType {
|
|
|
586
588
|
}
|
|
587
589
|
/** Persists current property values to the server via PUT. */
|
|
588
590
|
async save() {
|
|
591
|
+
assertRequired(this.name, 'Content type name');
|
|
589
592
|
assertGroupsValid(this.primaryGroup, this.sharedGroups);
|
|
590
593
|
const authLevel = String(AUTH_LEVEL_REVERSE[this.minUserLevel] ?? this._rawData.minAuthLevel ?? 2);
|
|
591
594
|
// Sync field changes back to raw contentTypeElements
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AUTH_LEVEL_MAP } from '../utils.js';
|
|
1
|
+
import { AUTH_LEVEL_MAP, assertRequired } from '../utils.js';
|
|
2
2
|
/** A mutable group object. Modify properties and call save() to persist. */
|
|
3
3
|
export class Group {
|
|
4
4
|
constructor(raw, httpClient) {
|
|
@@ -40,6 +40,7 @@ export class Group {
|
|
|
40
40
|
}
|
|
41
41
|
/** Persists current property values to the server via PUT. */
|
|
42
42
|
async save() {
|
|
43
|
+
assertRequired(this.name, 'Group name');
|
|
43
44
|
// Start from existing raw members
|
|
44
45
|
let rawMembers = [...(this._rawData.members ?? [])];
|
|
45
46
|
// Remove members
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveLanguage } from '../utils.js';
|
|
2
|
-
import { decodeHtmlEntities, readPrimaryGroup, readSharedGroups, writePrimaryGroup, writeSharedGroups, assertGroupsValid } from '../utils.js';
|
|
2
|
+
import { decodeHtmlEntities, readPrimaryGroup, readSharedGroups, writePrimaryGroup, writeSharedGroups, assertGroupsValid, assertRequired } from '../utils.js';
|
|
3
3
|
/** A mutable list object. Modify properties and call save() to persist. */
|
|
4
4
|
export class List {
|
|
5
5
|
constructor(raw, httpClient, language) {
|
|
@@ -53,6 +53,7 @@ export class List {
|
|
|
53
53
|
}
|
|
54
54
|
/** Persists current property values to the server via PUT. */
|
|
55
55
|
async save() {
|
|
56
|
+
assertRequired(this.name, 'List name');
|
|
56
57
|
if (this.isForcedLanguage && this.isDefaultLanguage) {
|
|
57
58
|
throw new Error('isForcedLanguage and isDefaultLanguage cannot both be true');
|
|
58
59
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MediaItem } from '../models/media-item.js';
|
|
2
|
-
import { resolveLanguage, resolveFileToBlob, deriveFilename, DEFAULT_CACHE_TTL, getCacheEpoch } from '../utils.js';
|
|
2
|
+
import { resolveLanguage, resolveFileToBlob, deriveFilename, DEFAULT_CACHE_TTL, getCacheEpoch, assertRequired } from '../utils.js';
|
|
3
3
|
/** Extension → syntax type mapping for non-binary media */
|
|
4
4
|
const EXTENSION_SYNTAX_MAP = {
|
|
5
5
|
js: 1,
|
|
@@ -50,10 +50,17 @@ export class MediaResource {
|
|
|
50
50
|
* Returns the new media item.
|
|
51
51
|
*/
|
|
52
52
|
async create(data) {
|
|
53
|
-
|
|
54
|
-
throw new Error('Media name is required');
|
|
53
|
+
assertRequired(data.name, 'Media name');
|
|
55
54
|
if (!data.category)
|
|
56
55
|
throw new Error('Media category is required');
|
|
56
|
+
// A media item is meaningless without its file/binary. Guard the common
|
|
57
|
+
// empty cases: missing, or an empty { file } wrapper.
|
|
58
|
+
const fileValue = data.file && typeof data.file === 'object' && !(data.file instanceof Blob) && 'file' in data.file
|
|
59
|
+
? data.file.file
|
|
60
|
+
: data.file;
|
|
61
|
+
if (fileValue === undefined || fileValue === null || fileValue === '') {
|
|
62
|
+
throw new Error('Media file is required');
|
|
63
|
+
}
|
|
57
64
|
const language = data.language ?? 'smxx';
|
|
58
65
|
const filename = deriveFilename(data.file);
|
|
59
66
|
const ext = getExtension(filename);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatFileSize, parseFileSize } from '../utils.js';
|
|
1
|
+
import { formatFileSize, parseFileSize, assertRequired } from '../utils.js';
|
|
2
2
|
function mapFromDetail(raw) {
|
|
3
3
|
const layouts = (raw.formatters ?? []).map((f) => ({
|
|
4
4
|
name: f.mediaLayout,
|
|
@@ -65,6 +65,7 @@ export class MediaType {
|
|
|
65
65
|
}
|
|
66
66
|
/** Persists current property values to the server via PUT. */
|
|
67
67
|
async save() {
|
|
68
|
+
assertRequired(this.name, 'Media type name');
|
|
68
69
|
// Sync defaultLayout into the layouts array
|
|
69
70
|
if (this.defaultLayout) {
|
|
70
71
|
for (const layout of this.layouts) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readPrimaryGroup, readSharedGroups, writePrimaryGroup, writeSharedGroups, assertGroupsValid } from '../utils.js';
|
|
1
|
+
import { readPrimaryGroup, readSharedGroups, writePrimaryGroup, writeSharedGroups, assertGroupsValid, assertRequired } from '../utils.js';
|
|
2
2
|
/** Maps SDK type codes to API type codes */
|
|
3
3
|
const SDK_TO_API = {
|
|
4
4
|
'a-to-z': 'a2z',
|
|
@@ -1003,6 +1003,7 @@ export class NavigationObject {
|
|
|
1003
1003
|
}
|
|
1004
1004
|
/** Persists current property values to the server via PUT. */
|
|
1005
1005
|
async save() {
|
|
1006
|
+
assertRequired(this.name, 'Navigation object name');
|
|
1006
1007
|
assertGroupsValid(this.primaryGroup, this.sharedGroups);
|
|
1007
1008
|
// Apply type-aware write transformation (coerce back to strings, derive hidden fields)
|
|
1008
1009
|
const camelStringProps = transformPropertiesWrite(this.type, this.properties);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { decodeHtmlEntities, DEFAULT_CACHE_TTL, getCacheEpoch, readPrimaryGroup, readSharedGroups, writePrimaryGroup, writeSharedGroups, assertGroupsValid } from '../utils.js';
|
|
1
|
+
import { decodeHtmlEntities, DEFAULT_CACHE_TTL, getCacheEpoch, readPrimaryGroup, readSharedGroups, writePrimaryGroup, writeSharedGroups, assertGroupsValid, assertRequired } from '../utils.js';
|
|
2
2
|
/** Friendly processor keys mapped to API names for page layouts */
|
|
3
3
|
const PAGE_PROCESSOR_MAP = {
|
|
4
4
|
't4-tags': 'T4 Tag Page',
|
|
@@ -28,6 +28,7 @@ export class PageLayout {
|
|
|
28
28
|
}
|
|
29
29
|
/** Persists current property values to the server via PUT. */
|
|
30
30
|
async save() {
|
|
31
|
+
assertRequired(this.name, 'Page layout name');
|
|
31
32
|
assertGroupsValid(this.primaryGroup, this.sharedGroups);
|
|
32
33
|
// Resolve syntax name to ID
|
|
33
34
|
let syntaxId = this._rawData.syntaxType;
|
package/dist/esm/section-ref.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveLanguage, mapStatus, flattenGroups, STATUS_CODES, AUTH_LEVEL_MAP, debugWarn, DEFAULT_CACHE_TTL, getCacheEpoch } from './utils.js';
|
|
1
|
+
import { resolveLanguage, mapStatus, flattenGroups, STATUS_CODES, AUTH_LEVEL_MAP, debugWarn, DEFAULT_CACHE_TTL, getCacheEpoch, assertRequired, assertNotEmptyIfPresent } from './utils.js';
|
|
2
2
|
import { ContentResource } from './resources/content-resource.js';
|
|
3
3
|
import { SectionItem } from './models/section-item.js';
|
|
4
4
|
/** Per-client cache for meta tag definitions, keyed by HttpClient instance */
|
|
@@ -564,6 +564,7 @@ export class SectionRef {
|
|
|
564
564
|
* If `data.customFields` is provided, creates and saves section metadata content.
|
|
565
565
|
*/
|
|
566
566
|
async addSection(data, options) {
|
|
567
|
+
assertRequired(data.name, 'Section name');
|
|
567
568
|
const language = resolveLanguage(options?.language, this.defaultLanguage);
|
|
568
569
|
// Fetch this section's details to inherit config
|
|
569
570
|
const parentSection = await this.httpClient.request({
|
|
@@ -708,6 +709,10 @@ export class SectionRef {
|
|
|
708
709
|
* Returns the updated SectionItem.
|
|
709
710
|
*/
|
|
710
711
|
async update(data, options) {
|
|
712
|
+
// This path PUTs directly (it does not go through SectionItem.save()), so
|
|
713
|
+
// guard the name here. Present-only: omitting name is valid (e.g. delete()
|
|
714
|
+
// calls update({ status: 'inactive' })), but setting it blank is not.
|
|
715
|
+
assertNotEmptyIfPresent(data.name, 'Section name');
|
|
711
716
|
const language = resolveLanguage(options?.language, this.defaultLanguage);
|
|
712
717
|
const section = await this.httpClient.request({
|
|
713
718
|
method: 'GET',
|
package/dist/esm/utils.d.ts
CHANGED
|
@@ -107,6 +107,23 @@ export declare function writeSharedGroups(ids?: number[]): Array<{
|
|
|
107
107
|
* group" and is never a conflict.
|
|
108
108
|
*/
|
|
109
109
|
export declare function assertGroupsValid(primaryGroup: number, sharedGroups?: number[]): void;
|
|
110
|
+
/**
|
|
111
|
+
* Asserts that a required string value is present and not blank.
|
|
112
|
+
*
|
|
113
|
+
* Use on create paths where the field must always be provided. Throws
|
|
114
|
+
* `"<Label> is required"` when the value is missing, empty, or whitespace-only.
|
|
115
|
+
* Prevents opaque API 500s from malformed requests missing a required field.
|
|
116
|
+
*/
|
|
117
|
+
export declare function assertRequired(value: string | null | undefined, label: string): void;
|
|
118
|
+
/**
|
|
119
|
+
* Asserts that a value, *if provided*, is not blank.
|
|
120
|
+
*
|
|
121
|
+
* Use on update paths where a field is optional (omitting it leaves the
|
|
122
|
+
* current value unchanged) but explicitly setting it to an empty or
|
|
123
|
+
* whitespace-only string is invalid. `undefined` passes (means "no change");
|
|
124
|
+
* `''` or whitespace throws `"<Label> cannot be empty"`.
|
|
125
|
+
*/
|
|
126
|
+
export declare function assertNotEmptyIfPresent(value: string | null | undefined, label: string): void;
|
|
110
127
|
/** Accepted file input: a file path, URL, Blob, ReadableStream, or { file, filename } object */
|
|
111
128
|
export type FileInput = string | Blob | NodeJS.ReadableStream | {
|
|
112
129
|
file: string | Blob | NodeJS.ReadableStream;
|
package/dist/esm/utils.js
CHANGED
|
@@ -202,6 +202,33 @@ export function assertGroupsValid(primaryGroup, sharedGroups) {
|
|
|
202
202
|
'Remove it from sharedGroups or choose a different primaryGroup.');
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Asserts that a required string value is present and not blank.
|
|
207
|
+
*
|
|
208
|
+
* Use on create paths where the field must always be provided. Throws
|
|
209
|
+
* `"<Label> is required"` when the value is missing, empty, or whitespace-only.
|
|
210
|
+
* Prevents opaque API 500s from malformed requests missing a required field.
|
|
211
|
+
*/
|
|
212
|
+
export function assertRequired(value, label) {
|
|
213
|
+
if (!value || !value.trim()) {
|
|
214
|
+
throw new Error(`${label} is required`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Asserts that a value, *if provided*, is not blank.
|
|
219
|
+
*
|
|
220
|
+
* Use on update paths where a field is optional (omitting it leaves the
|
|
221
|
+
* current value unchanged) but explicitly setting it to an empty or
|
|
222
|
+
* whitespace-only string is invalid. `undefined` passes (means "no change");
|
|
223
|
+
* `''` or whitespace throws `"<Label> cannot be empty"`.
|
|
224
|
+
*/
|
|
225
|
+
export function assertNotEmptyIfPresent(value, label) {
|
|
226
|
+
if (value === undefined || value === null)
|
|
227
|
+
return;
|
|
228
|
+
if (!value.trim()) {
|
|
229
|
+
throw new Error(`${label} cannot be empty`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
205
232
|
/**
|
|
206
233
|
* Resolves a file input (path, URL, Blob, or ReadableStream) to a Blob.
|
|
207
234
|
*/
|
package/docs/content-types.md
CHANGED
|
@@ -328,7 +328,7 @@ await contentType.layouts.create({
|
|
|
328
328
|
});
|
|
329
329
|
```
|
|
330
330
|
|
|
331
|
-
Processor options are `'handlebars'`, `'t4-tags'`, and `'programmable-layouts'`. The default is `'handlebars'`. The SDK enforces unique layout names.
|
|
331
|
+
Processor options are `'handlebars'`, `'t4-tags'`, and `'programmable-layouts'`. The default is `'handlebars'`. The SDK enforces unique layout names. `name` is required (throws `Layout name is required` if empty); `code` is optional and may be empty. Renaming a layout to an empty string on update is rejected.
|
|
332
332
|
|
|
333
333
|
### Direct update
|
|
334
334
|
|
package/docs/error-handling.md
CHANGED
|
@@ -61,6 +61,32 @@ The final error can result from a call such as:
|
|
|
61
61
|
await t4.users.create({ username: '', ... });
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
### Required fields
|
|
65
|
+
|
|
66
|
+
Creating an asset without its required `name` (or other required input) throws before any request is sent, so you get a clear message instead of an opaque API error:
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
Error: Section name is required
|
|
70
|
+
Error: Content name is required
|
|
71
|
+
Error: Layout name is required
|
|
72
|
+
Error: Media name is required
|
|
73
|
+
Error: Media file is required
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The same protection applies on update: you cannot blank out a name that is already set. Omitting `name` leaves it unchanged, but setting it to an empty or whitespace-only string is rejected — through both the resource `update()` method and the mutable `get()` → modify → `save()` pattern:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
const section = await t4.section(482).get();
|
|
80
|
+
section.name = '';
|
|
81
|
+
await section.save();
|
|
82
|
+
// Error: Section name is required
|
|
83
|
+
|
|
84
|
+
await t4.section(482).update({ name: ' ' });
|
|
85
|
+
// Error: Section name cannot be empty
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
This covers content types, content items, sections, lists, groups, page layouts, media, media types, media categories, navigation objects, and Handlebars helpers/partials.
|
|
89
|
+
|
|
64
90
|
## Enable debug logging
|
|
65
91
|
|
|
66
92
|
Set `T4_DEBUG=1` to print HTTP requests and internal warnings:
|