@terminalfour/terminalfour-js 1.0.3 → 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/content-cache.d.ts +38 -0
- package/dist/cjs/content-cache.js +42 -0
- package/dist/cjs/element-resolver.d.ts +16 -0
- package/dist/cjs/element-resolver.js +77 -1
- package/dist/cjs/handlebars.js +1 -0
- package/dist/cjs/models/content-item.js +14 -1
- 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.d.ts +7 -11
- package/dist/cjs/resources/content-resource.js +38 -79
- package/dist/cjs/resources/content-type-resource.d.ts +2 -6
- package/dist/cjs/resources/content-type-resource.js +20 -6
- package/dist/cjs/resources/group-resource.js +1 -0
- package/dist/cjs/resources/list-resource.d.ts +2 -6
- package/dist/cjs/resources/list-resource.js +9 -6
- 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.d.ts +17 -0
- package/dist/cjs/resources/navigation-resource.js +14 -2
- package/dist/cjs/resources/page-layout-resource.d.ts +11 -0
- package/dist/cjs/resources/page-layout-resource.js +13 -2
- package/dist/cjs/section-ref.d.ts +3 -1
- package/dist/cjs/section-ref.js +11 -5
- package/dist/cjs/t4-client.d.ts +1 -0
- package/dist/cjs/t4-client.js +6 -1
- package/dist/cjs/utils.d.ts +56 -0
- package/dist/cjs/utils.js +66 -0
- package/dist/esm/content-cache.d.ts +38 -0
- package/dist/esm/content-cache.js +38 -0
- package/dist/esm/element-resolver.d.ts +16 -0
- package/dist/esm/element-resolver.js +77 -1
- package/dist/esm/handlebars.js +2 -1
- package/dist/esm/models/content-item.js +15 -2
- 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.d.ts +7 -11
- package/dist/esm/resources/content-resource.js +39 -80
- package/dist/esm/resources/content-type-resource.d.ts +2 -6
- package/dist/esm/resources/content-type-resource.js +21 -7
- package/dist/esm/resources/group-resource.js +2 -1
- package/dist/esm/resources/list-resource.d.ts +2 -6
- package/dist/esm/resources/list-resource.js +10 -7
- 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.d.ts +17 -0
- package/dist/esm/resources/navigation-resource.js +14 -2
- package/dist/esm/resources/page-layout-resource.d.ts +11 -0
- package/dist/esm/resources/page-layout-resource.js +14 -3
- package/dist/esm/section-ref.d.ts +3 -1
- package/dist/esm/section-ref.js +12 -6
- package/dist/esm/t4-client.d.ts +1 -0
- package/dist/esm/t4-client.js +6 -1
- package/dist/esm/utils.d.ts +56 -0
- package/dist/esm/utils.js +59 -0
- package/docs/content-types.md +3 -3
- package/docs/content.md +13 -0
- package/docs/error-handling.md +29 -1
- package/docs/getting-started.md +4 -0
- package/docs/lists.md +2 -2
- package/docs/navigation.md +30 -1
- package/docs/page-layouts.md +26 -0
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { HttpClient } from '../http-client.js';
|
|
2
|
+
import { RawPrimaryGroup } from '../utils.js';
|
|
2
3
|
/** SDK-friendly navigation type codes (consistent kebab-case) */
|
|
3
4
|
export type NavigationType = 'a-to-z' | 'breadcrumbs' | 'css-selector' | 'generate-file' | 'keyword-search' | 'language-switcher' | 'link-menu' | 'pagination' | 'previous-next-fulltext' | 'publish-to-one-file' | 'related-content' | 'related-section-branch' | 'return-to-index' | 'section-details' | 'section-iterator' | 'section-meta-info' | 'site-map' | 'top-content' | 'top-stories';
|
|
4
5
|
/** Human-readable type name mapping */
|
|
@@ -23,6 +24,10 @@ interface RawNavigationDetail {
|
|
|
23
24
|
isPreviewModeEnabled: boolean;
|
|
24
25
|
isCachingEnabled: boolean;
|
|
25
26
|
date?: string;
|
|
27
|
+
primaryGroup?: RawPrimaryGroup;
|
|
28
|
+
sharedGroups?: Array<{
|
|
29
|
+
id: number;
|
|
30
|
+
}>;
|
|
26
31
|
properties: Record<string, {
|
|
27
32
|
value?: string;
|
|
28
33
|
attribute: string;
|
|
@@ -42,6 +47,10 @@ export declare class NavigationObject {
|
|
|
42
47
|
enabled: boolean;
|
|
43
48
|
cachingEnabled: boolean;
|
|
44
49
|
previewEnabled: boolean;
|
|
50
|
+
/** Owning group ID. 0 = no primary group (Global). */
|
|
51
|
+
primaryGroup: number;
|
|
52
|
+
/** Group IDs this navigation object is shared with. */
|
|
53
|
+
sharedGroups: number[];
|
|
45
54
|
properties: Record<string, unknown>;
|
|
46
55
|
private readonly _httpClient;
|
|
47
56
|
private _rawData;
|
|
@@ -525,6 +534,10 @@ export interface CreateNavigationData {
|
|
|
525
534
|
description?: string;
|
|
526
535
|
enabled?: boolean;
|
|
527
536
|
previewEnabled?: boolean;
|
|
537
|
+
/** Owning group ID. 0 = no primary group (Global). Defaults to 0. */
|
|
538
|
+
primaryGroup?: number;
|
|
539
|
+
/** Group IDs to share this navigation object with. Defaults to none. */
|
|
540
|
+
sharedGroups?: number[];
|
|
528
541
|
properties?: A2ZProperties | BreadcrumbsProperties | CssSelectorProperties | GenerateFileProperties | LanguageSwitcherProperties | PaginationProperties | PreviousNextProperties | SectionIteratorProperties | RelatedSectionBranchProperties | ReturnToIndexProperties | SectionMetaInfoProperties | TopStoriesProperties | SiteMapProperties | SectionDetailsProperties | RelatedContentProperties | LinkMenuProperties | PublishToOneFileProperties | TopContentProperties | KeywordSearchProperties | Record<string, unknown>;
|
|
529
542
|
}
|
|
530
543
|
/**
|
|
@@ -538,6 +551,10 @@ export interface UpdateNavigationData {
|
|
|
538
551
|
enabled?: boolean;
|
|
539
552
|
previewEnabled?: boolean;
|
|
540
553
|
cachingEnabled?: boolean;
|
|
554
|
+
/** Owning group ID. 0 = no primary group (Global). */
|
|
555
|
+
primaryGroup?: number;
|
|
556
|
+
/** Group IDs to share this navigation object with. */
|
|
557
|
+
sharedGroups?: number[];
|
|
541
558
|
properties?: A2ZProperties | BreadcrumbsProperties | CssSelectorProperties | GenerateFileProperties | LanguageSwitcherProperties | PaginationProperties | PreviousNextProperties | SectionIteratorProperties | RelatedSectionBranchProperties | ReturnToIndexProperties | SectionMetaInfoProperties | TopStoriesProperties | SiteMapProperties | SectionDetailsProperties | RelatedContentProperties | LinkMenuProperties | PublishToOneFileProperties | TopContentProperties | KeywordSearchProperties | Record<string, unknown>;
|
|
542
559
|
}
|
|
543
560
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readPrimaryGroup, readSharedGroups, writePrimaryGroup, writeSharedGroups, assertGroupsValid, assertRequired } from '../utils.js';
|
|
1
2
|
/** Maps SDK type codes to API type codes */
|
|
2
3
|
const SDK_TO_API = {
|
|
3
4
|
'a-to-z': 'a2z',
|
|
@@ -985,6 +986,8 @@ export class NavigationObject {
|
|
|
985
986
|
this.enabled = raw.isEnabled;
|
|
986
987
|
this.cachingEnabled = raw.isCachingEnabled;
|
|
987
988
|
this.previewEnabled = raw.isPreviewModeEnabled;
|
|
989
|
+
this.primaryGroup = readPrimaryGroup(raw.primaryGroup);
|
|
990
|
+
this.sharedGroups = readSharedGroups(raw.sharedGroups);
|
|
988
991
|
// Convert properties to camelCase keys with string values
|
|
989
992
|
const originalKeys = [];
|
|
990
993
|
const rawCamelProps = {};
|
|
@@ -1000,6 +1003,8 @@ export class NavigationObject {
|
|
|
1000
1003
|
}
|
|
1001
1004
|
/** Persists current property values to the server via PUT. */
|
|
1002
1005
|
async save() {
|
|
1006
|
+
assertRequired(this.name, 'Navigation object name');
|
|
1007
|
+
assertGroupsValid(this.primaryGroup, this.sharedGroups);
|
|
1003
1008
|
// Apply type-aware write transformation (coerce back to strings, derive hidden fields)
|
|
1004
1009
|
const camelStringProps = transformPropertiesWrite(this.type, this.properties);
|
|
1005
1010
|
// Rebuild properties in API format
|
|
@@ -1019,6 +1024,8 @@ export class NavigationObject {
|
|
|
1019
1024
|
isEnabled: this.enabled,
|
|
1020
1025
|
isCachingEnabled: this.cachingEnabled,
|
|
1021
1026
|
isPreviewModeEnabled: this.previewEnabled,
|
|
1027
|
+
primaryGroup: writePrimaryGroup(this.primaryGroup),
|
|
1028
|
+
sharedGroups: writeSharedGroups(this.sharedGroups),
|
|
1022
1029
|
properties: apiProperties,
|
|
1023
1030
|
};
|
|
1024
1031
|
await this._httpClient.request({
|
|
@@ -1235,6 +1242,7 @@ export class NavigationResource {
|
|
|
1235
1242
|
throw new Error('Navigation object type is required');
|
|
1236
1243
|
if (!NAVIGATION_TYPE_NAMES[data.type])
|
|
1237
1244
|
throw new Error(`Unknown navigation type "${data.type}"`);
|
|
1245
|
+
assertGroupsValid(data.primaryGroup ?? 0, data.sharedGroups);
|
|
1238
1246
|
const apiType = SDK_TO_API[data.type];
|
|
1239
1247
|
const properties = await this.buildProperties(data.type, (data.properties ?? {}));
|
|
1240
1248
|
const body = {
|
|
@@ -1244,8 +1252,8 @@ export class NavigationResource {
|
|
|
1244
1252
|
name: data.name,
|
|
1245
1253
|
description: data.description ?? '',
|
|
1246
1254
|
navigationType: apiType,
|
|
1247
|
-
sharedGroups:
|
|
1248
|
-
primaryGroup:
|
|
1255
|
+
sharedGroups: writeSharedGroups(data.sharedGroups),
|
|
1256
|
+
primaryGroup: writePrimaryGroup(data.primaryGroup ?? 0),
|
|
1249
1257
|
properties,
|
|
1250
1258
|
};
|
|
1251
1259
|
// CSS Selector quirk: requires "section-name": "on" at the top level
|
|
@@ -1289,6 +1297,10 @@ export class NavigationResource {
|
|
|
1289
1297
|
nav.previewEnabled = data.previewEnabled;
|
|
1290
1298
|
if (data.cachingEnabled !== undefined)
|
|
1291
1299
|
nav.cachingEnabled = data.cachingEnabled;
|
|
1300
|
+
if (data.primaryGroup !== undefined)
|
|
1301
|
+
nav.primaryGroup = data.primaryGroup;
|
|
1302
|
+
if (data.sharedGroups !== undefined)
|
|
1303
|
+
nav.sharedGroups = data.sharedGroups;
|
|
1292
1304
|
// Merge properties rather than replace — callers pass only what changes
|
|
1293
1305
|
if (data.properties !== undefined) {
|
|
1294
1306
|
nav.properties = {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { HttpClient } from '../http-client.js';
|
|
2
|
+
import { RawPrimaryGroup } from '../utils.js';
|
|
2
3
|
/** Raw page layout detail from GET /pageLayout/{id} */
|
|
3
4
|
interface RawPageLayoutDetail {
|
|
4
5
|
id: number;
|
|
@@ -10,6 +11,10 @@ interface RawPageLayoutDetail {
|
|
|
10
11
|
fileExtension?: string;
|
|
11
12
|
syntaxType?: number;
|
|
12
13
|
layoutProcessor?: number;
|
|
14
|
+
primaryGroup?: RawPrimaryGroup;
|
|
15
|
+
sharedGroups?: Array<{
|
|
16
|
+
id: number;
|
|
17
|
+
}>;
|
|
13
18
|
[key: string]: unknown;
|
|
14
19
|
}
|
|
15
20
|
/** A page layout summary returned from list() */
|
|
@@ -28,6 +33,10 @@ export declare class PageLayout {
|
|
|
28
33
|
fileExtension: string;
|
|
29
34
|
syntax: string;
|
|
30
35
|
processor: string;
|
|
36
|
+
/** Owning group ID. 0 = no primary group (Global). */
|
|
37
|
+
primaryGroup: number;
|
|
38
|
+
/** Group IDs this page layout is shared with. */
|
|
39
|
+
sharedGroups: number[];
|
|
31
40
|
private readonly _httpClient;
|
|
32
41
|
private _rawData;
|
|
33
42
|
private _syntaxMap;
|
|
@@ -63,6 +72,8 @@ export declare class PageLayoutResource {
|
|
|
63
72
|
fileExtension?: string;
|
|
64
73
|
syntax?: string;
|
|
65
74
|
processor?: string;
|
|
75
|
+
primaryGroup?: number;
|
|
76
|
+
sharedGroups?: number[];
|
|
66
77
|
}): Promise<PageLayout>;
|
|
67
78
|
/** Creates a new page layout. */
|
|
68
79
|
create(data: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { decodeHtmlEntities, DEFAULT_CACHE_TTL, getCacheEpoch } 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',
|
|
@@ -19,6 +19,8 @@ export class PageLayout {
|
|
|
19
19
|
const procName = processorMap.get(raw.layoutProcessor ?? 0) ?? '';
|
|
20
20
|
const procEntry = Object.entries(PAGE_PROCESSOR_MAP).find(([, apiName]) => apiName === procName);
|
|
21
21
|
this.processor = procEntry ? procEntry[0] : procName || `unknown (${raw.layoutProcessor})`;
|
|
22
|
+
this.primaryGroup = readPrimaryGroup(raw.primaryGroup);
|
|
23
|
+
this.sharedGroups = readSharedGroups(raw.sharedGroups);
|
|
22
24
|
Object.defineProperty(this, '_httpClient', { value: httpClient, enumerable: false });
|
|
23
25
|
Object.defineProperty(this, '_rawData', { value: raw, enumerable: false, writable: true });
|
|
24
26
|
Object.defineProperty(this, '_syntaxMap', { value: syntaxMap, enumerable: false });
|
|
@@ -26,6 +28,8 @@ export class PageLayout {
|
|
|
26
28
|
}
|
|
27
29
|
/** Persists current property values to the server via PUT. */
|
|
28
30
|
async save() {
|
|
31
|
+
assertRequired(this.name, 'Page layout name');
|
|
32
|
+
assertGroupsValid(this.primaryGroup, this.sharedGroups);
|
|
29
33
|
// Resolve syntax name to ID
|
|
30
34
|
let syntaxId = this._rawData.syntaxType;
|
|
31
35
|
if (this._syntaxMap) {
|
|
@@ -50,6 +54,8 @@ export class PageLayout {
|
|
|
50
54
|
fileExtension: this.fileExtension,
|
|
51
55
|
syntaxType: String(syntaxId),
|
|
52
56
|
layoutProcessor: String(processorId),
|
|
57
|
+
primaryGroup: writePrimaryGroup(this.primaryGroup),
|
|
58
|
+
sharedGroups: writeSharedGroups(this.sharedGroups),
|
|
53
59
|
};
|
|
54
60
|
await this._httpClient.request({
|
|
55
61
|
method: 'PUT',
|
|
@@ -132,6 +138,10 @@ export class PageLayoutResource {
|
|
|
132
138
|
layout.syntax = data.syntax;
|
|
133
139
|
if (data.processor !== undefined)
|
|
134
140
|
layout.processor = data.processor;
|
|
141
|
+
if (data.primaryGroup !== undefined)
|
|
142
|
+
layout.primaryGroup = data.primaryGroup;
|
|
143
|
+
if (data.sharedGroups !== undefined)
|
|
144
|
+
layout.sharedGroups = data.sharedGroups;
|
|
135
145
|
await layout.save();
|
|
136
146
|
return layout;
|
|
137
147
|
}
|
|
@@ -139,6 +149,7 @@ export class PageLayoutResource {
|
|
|
139
149
|
async create(data) {
|
|
140
150
|
if (!data.name?.trim())
|
|
141
151
|
throw new Error('Page layout name is required');
|
|
152
|
+
assertGroupsValid(data.primaryGroup ?? 0, data.sharedGroups);
|
|
142
153
|
const [syntaxMap, processorMap] = await Promise.all([
|
|
143
154
|
this.getSyntaxMap(),
|
|
144
155
|
this.getProcessorMap(),
|
|
@@ -192,8 +203,8 @@ export class PageLayoutResource {
|
|
|
192
203
|
fileExtension: extensionValue,
|
|
193
204
|
syntaxType: syntaxId,
|
|
194
205
|
layoutProcessor: processorId,
|
|
195
|
-
sharedGroups: (data.sharedGroups
|
|
196
|
-
primaryGroup:
|
|
206
|
+
sharedGroups: writeSharedGroups(data.sharedGroups),
|
|
207
|
+
primaryGroup: writePrimaryGroup(data.primaryGroup ?? 0),
|
|
197
208
|
},
|
|
198
209
|
});
|
|
199
210
|
return new PageLayout(raw, this.httpClient, syntaxMap, processorMap);
|
|
@@ -3,6 +3,7 @@ import { LanguageOption, SectionChannel, Owner, AddSectionData } from './types.j
|
|
|
3
3
|
import { ContentResource } from './resources/content-resource.js';
|
|
4
4
|
import { SectionItem } from './models/section-item.js';
|
|
5
5
|
import { MediaCreateFn } from './element-resolver.js';
|
|
6
|
+
import { ContentCache } from './content-cache.js';
|
|
6
7
|
/** A node in the section hierarchy tree */
|
|
7
8
|
export interface SectionTreeNode {
|
|
8
9
|
id: number;
|
|
@@ -23,7 +24,8 @@ export declare class SectionRef {
|
|
|
23
24
|
private readonly sectionId;
|
|
24
25
|
private readonly defaultLanguage;
|
|
25
26
|
private readonly mediaCreateFn;
|
|
26
|
-
|
|
27
|
+
private readonly cache;
|
|
28
|
+
constructor(httpClient: HttpClient, sectionId: number, defaultLanguage: string, mediaCreateFn?: MediaCreateFn | null, cache?: ContentCache);
|
|
27
29
|
/** Returns a mutable section object. Modify properties and call save() to persist. */
|
|
28
30
|
get(options?: LanguageOption): Promise<SectionItem>;
|
|
29
31
|
/** Returns the list of channels associated with this section. */
|
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 */
|
|
@@ -53,12 +53,13 @@ async function resolveMetaDataTypeId(httpClient, sectionMetaDataType) {
|
|
|
53
53
|
* child section creation, and deletion — all from a single entry point.
|
|
54
54
|
*/
|
|
55
55
|
export class SectionRef {
|
|
56
|
-
constructor(httpClient, sectionId, defaultLanguage, mediaCreateFn) {
|
|
56
|
+
constructor(httpClient, sectionId, defaultLanguage, mediaCreateFn, cache) {
|
|
57
57
|
this.httpClient = httpClient;
|
|
58
58
|
this.sectionId = sectionId;
|
|
59
59
|
this.defaultLanguage = defaultLanguage;
|
|
60
60
|
this.mediaCreateFn = mediaCreateFn ?? null;
|
|
61
|
-
this.
|
|
61
|
+
this.cache = cache;
|
|
62
|
+
this.content = new ContentResource(httpClient, sectionId, defaultLanguage, this.mediaCreateFn, this.cache);
|
|
62
63
|
}
|
|
63
64
|
// ── Section metadata ──
|
|
64
65
|
/** Returns a mutable section object. Modify properties and call save() to persist. */
|
|
@@ -73,7 +74,7 @@ export class SectionRef {
|
|
|
73
74
|
const meta = raw.metaData;
|
|
74
75
|
if (meta?.enabled && meta.id) {
|
|
75
76
|
try {
|
|
76
|
-
const metaContent = new ContentResource(this.httpClient, this.sectionId, this.defaultLanguage, this.mediaCreateFn);
|
|
77
|
+
const metaContent = new ContentResource(this.httpClient, this.sectionId, this.defaultLanguage, this.mediaCreateFn, this.cache);
|
|
77
78
|
const item = await metaContent.get(meta.id, { language });
|
|
78
79
|
customFields = stripNameField(item.fields ?? null);
|
|
79
80
|
}
|
|
@@ -563,6 +564,7 @@ export class SectionRef {
|
|
|
563
564
|
* If `data.customFields` is provided, creates and saves section metadata content.
|
|
564
565
|
*/
|
|
565
566
|
async addSection(data, options) {
|
|
567
|
+
assertRequired(data.name, 'Section name');
|
|
566
568
|
const language = resolveLanguage(options?.language, this.defaultLanguage);
|
|
567
569
|
// Fetch this section's details to inherit config
|
|
568
570
|
const parentSection = await this.httpClient.request({
|
|
@@ -641,7 +643,7 @@ export class SectionRef {
|
|
|
641
643
|
if (metaContentTypeId) {
|
|
642
644
|
// Use a ContentResource on the new section to get full element resolution
|
|
643
645
|
// (list values, SS links, file uploads, etc.)
|
|
644
|
-
const metadataContentResource = new ContentResource(this.httpClient, newSectionId, this.defaultLanguage, this.mediaCreateFn);
|
|
646
|
+
const metadataContentResource = new ContentResource(this.httpClient, newSectionId, this.defaultLanguage, this.mediaCreateFn, this.cache);
|
|
645
647
|
// Create the metadata content item (ContentResource.create handles the full body)
|
|
646
648
|
const metaItem = await metadataContentResource.create({
|
|
647
649
|
type: metaContentTypeId,
|
|
@@ -707,6 +709,10 @@ export class SectionRef {
|
|
|
707
709
|
* Returns the updated SectionItem.
|
|
708
710
|
*/
|
|
709
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');
|
|
710
716
|
const language = resolveLanguage(options?.language, this.defaultLanguage);
|
|
711
717
|
const section = await this.httpClient.request({
|
|
712
718
|
method: 'GET',
|
|
@@ -730,7 +736,7 @@ export class SectionRef {
|
|
|
730
736
|
if (!metaDataTypeId) {
|
|
731
737
|
throw new Error('Cannot update customFields: no Section Meta Data content type is configured on this T4 instance');
|
|
732
738
|
}
|
|
733
|
-
const contentResource = new ContentResource(this.httpClient, this.sectionId, this.defaultLanguage, this.mediaCreateFn);
|
|
739
|
+
const contentResource = new ContentResource(this.httpClient, this.sectionId, this.defaultLanguage, this.mediaCreateFn, this.cache);
|
|
734
740
|
const metaContentId = meta?.id ?? 0;
|
|
735
741
|
if (metaContentId > 0) {
|
|
736
742
|
await contentResource.update(metaContentId, { fields: data.customFields });
|
package/dist/esm/t4-client.d.ts
CHANGED
|
@@ -35,6 +35,7 @@ export declare class T4Client {
|
|
|
35
35
|
private readonly httpClient;
|
|
36
36
|
private readonly defaultLanguage;
|
|
37
37
|
private readonly mediaCreateFn;
|
|
38
|
+
private readonly contentCache;
|
|
38
39
|
constructor(config: T4ClientConfig);
|
|
39
40
|
/**
|
|
40
41
|
* Returns a section reference scoped to the given section ID.
|
package/dist/esm/t4-client.js
CHANGED
|
@@ -13,6 +13,7 @@ import { PageLayoutResource } from './resources/page-layout-resource.js';
|
|
|
13
13
|
import { MediaTypeResource } from './resources/media-type-resource.js';
|
|
14
14
|
import { NavigationResource } from './resources/navigation-resource.js';
|
|
15
15
|
import { Handlebars } from './handlebars.js';
|
|
16
|
+
import { ContentCache } from './content-cache.js';
|
|
16
17
|
import { invalidateAllCaches, normaliseBaseUrl, assertNotBrowser } from './utils.js';
|
|
17
18
|
/**
|
|
18
19
|
* Main entry point for the T4 SDK.
|
|
@@ -54,12 +55,16 @@ export class T4Client {
|
|
|
54
55
|
});
|
|
55
56
|
return item.id;
|
|
56
57
|
};
|
|
58
|
+
// Shared content caches (element TypeRegistry, content type templates).
|
|
59
|
+
// Threaded into every SectionRef/ContentResource so hierarchy traversal
|
|
60
|
+
// doesn't re-fetch instance-wide data on each t4.section(id) call.
|
|
61
|
+
this.contentCache = new ContentCache(this.httpClient, this.defaultLanguage, this.mediaCreateFn);
|
|
57
62
|
}
|
|
58
63
|
/**
|
|
59
64
|
* Returns a section reference scoped to the given section ID.
|
|
60
65
|
*/
|
|
61
66
|
section(id) {
|
|
62
|
-
return new SectionRef(this.httpClient, id, this.defaultLanguage, this.mediaCreateFn);
|
|
67
|
+
return new SectionRef(this.httpClient, id, this.defaultLanguage, this.mediaCreateFn, this.contentCache);
|
|
63
68
|
}
|
|
64
69
|
/**
|
|
65
70
|
* Returns a media category reference scoped to the given category ID.
|
package/dist/esm/utils.d.ts
CHANGED
|
@@ -68,6 +68,62 @@ export declare function flattenGroups(groups: Array<{
|
|
|
68
68
|
name: string;
|
|
69
69
|
groupChildren?: unknown[];
|
|
70
70
|
}>, map?: Map<number, string>): Map<number, string>;
|
|
71
|
+
/**
|
|
72
|
+
* Group/visibility mapping helpers.
|
|
73
|
+
*
|
|
74
|
+
* The T4 API represents an asset's owning group as `primaryGroup` (an object,
|
|
75
|
+
* where the id may be direct or nested under `group`) and its shared groups as
|
|
76
|
+
* `sharedGroups` (an array of `{ id }`). The SDK exposes these as a plain
|
|
77
|
+
* `primaryGroup: number` (0 = none) and `sharedGroups: number[]`. These helpers
|
|
78
|
+
* centralise the read/write mapping so content types, lists, page layouts, and
|
|
79
|
+
* navigation objects all handle groups identically.
|
|
80
|
+
*/
|
|
81
|
+
/** Raw shape of the API `primaryGroup` field. */
|
|
82
|
+
export interface RawPrimaryGroup {
|
|
83
|
+
id: number | null;
|
|
84
|
+
group?: {
|
|
85
|
+
id: number;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** Reads the API `primaryGroup` object into a plain group id (0 = none). */
|
|
89
|
+
export declare function readPrimaryGroup(raw?: RawPrimaryGroup): number;
|
|
90
|
+
/** Reads the API `sharedGroups` array into a plain array of group ids. */
|
|
91
|
+
export declare function readSharedGroups(raw?: Array<{
|
|
92
|
+
id: number;
|
|
93
|
+
}>): number[];
|
|
94
|
+
/** Writes a plain group id back to the API `primaryGroup` shape (0/falsy = none). */
|
|
95
|
+
export declare function writePrimaryGroup(id: number): {
|
|
96
|
+
id: number | null;
|
|
97
|
+
};
|
|
98
|
+
/** Writes a plain array of group ids back to the API `sharedGroups` shape. */
|
|
99
|
+
export declare function writeSharedGroups(ids?: number[]): Array<{
|
|
100
|
+
id: number;
|
|
101
|
+
}>;
|
|
102
|
+
/**
|
|
103
|
+
* Validates that group/visibility values are acceptable to the T4 API before a
|
|
104
|
+
* write. The API returns an opaque 500 when `sharedGroups` contains the same id
|
|
105
|
+
* as `primaryGroup` (a group cannot be both the owner and a shared group), so we
|
|
106
|
+
* catch it here with a clear message. A `primaryGroup` of 0 means "no owning
|
|
107
|
+
* group" and is never a conflict.
|
|
108
|
+
*/
|
|
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;
|
|
71
127
|
/** Accepted file input: a file path, URL, Blob, ReadableStream, or { file, filename } object */
|
|
72
128
|
export type FileInput = string | Blob | NodeJS.ReadableStream | {
|
|
73
129
|
file: string | Blob | NodeJS.ReadableStream;
|
package/dist/esm/utils.js
CHANGED
|
@@ -170,6 +170,65 @@ export function flattenGroups(groups, map = new Map()) {
|
|
|
170
170
|
}
|
|
171
171
|
return map;
|
|
172
172
|
}
|
|
173
|
+
/** Reads the API `primaryGroup` object into a plain group id (0 = none). */
|
|
174
|
+
export function readPrimaryGroup(raw) {
|
|
175
|
+
return raw?.group?.id ?? raw?.id ?? 0;
|
|
176
|
+
}
|
|
177
|
+
/** Reads the API `sharedGroups` array into a plain array of group ids. */
|
|
178
|
+
export function readSharedGroups(raw) {
|
|
179
|
+
return (raw ?? []).map((g) => g.id);
|
|
180
|
+
}
|
|
181
|
+
/** Writes a plain group id back to the API `primaryGroup` shape (0/falsy = none). */
|
|
182
|
+
export function writePrimaryGroup(id) {
|
|
183
|
+
return { id: id || null };
|
|
184
|
+
}
|
|
185
|
+
/** Writes a plain array of group ids back to the API `sharedGroups` shape. */
|
|
186
|
+
export function writeSharedGroups(ids) {
|
|
187
|
+
return (ids ?? []).map((id) => ({ id }));
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Validates that group/visibility values are acceptable to the T4 API before a
|
|
191
|
+
* write. The API returns an opaque 500 when `sharedGroups` contains the same id
|
|
192
|
+
* as `primaryGroup` (a group cannot be both the owner and a shared group), so we
|
|
193
|
+
* catch it here with a clear message. A `primaryGroup` of 0 means "no owning
|
|
194
|
+
* group" and is never a conflict.
|
|
195
|
+
*/
|
|
196
|
+
export function assertGroupsValid(primaryGroup, sharedGroups) {
|
|
197
|
+
if (!primaryGroup)
|
|
198
|
+
return;
|
|
199
|
+
if ((sharedGroups ?? []).includes(primaryGroup)) {
|
|
200
|
+
throw new Error(`sharedGroups cannot contain the primaryGroup id (${primaryGroup}). ` +
|
|
201
|
+
'A group cannot be both the primary (owning) group and a shared group. ' +
|
|
202
|
+
'Remove it from sharedGroups or choose a different primaryGroup.');
|
|
203
|
+
}
|
|
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
|
+
}
|
|
173
232
|
/**
|
|
174
233
|
* Resolves a file input (path, URL, Blob, or ReadableStream) to a Blob.
|
|
175
234
|
*/
|
package/docs/content-types.md
CHANGED
|
@@ -36,8 +36,8 @@ The returned `ContentType` includes these main properties:
|
|
|
36
36
|
| `minUserLevel` | `'contributor'` |
|
|
37
37
|
| `workflow` | Assigned workflow |
|
|
38
38
|
| `directEdit` | Direct edit setting |
|
|
39
|
-
| `primaryGroup` |
|
|
40
|
-
| `sharedGroups` | Shared
|
|
39
|
+
| `primaryGroup` | Owning group ID (`0` = none) |
|
|
40
|
+
| `sharedGroups` | Shared group IDs. Cannot contain `primaryGroup`. |
|
|
41
41
|
| `fields` | Field definitions keyed by name |
|
|
42
42
|
|
|
43
43
|
Each entry in `news.fields` can include:
|
|
@@ -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/content.md
CHANGED
|
@@ -113,6 +113,19 @@ await article.save();
|
|
|
113
113
|
|
|
114
114
|
`save()` defaults the status to `'pending'` to match T4's approval workflow. Set `item.status = 'approved'` before saving if the item must remain approved.
|
|
115
115
|
|
|
116
|
+
`save()` resolves every field type the same way `content.create()` and `content.update()` do — including Repeater fields. A repeater is read back as an array of `{ name, fields }` items (see [Values returned on read](#values-returned-on-read)), and you assign the same friendly shape back:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
const deck = await t4.section(482).content.get(9200);
|
|
120
|
+
deck.fields['Slides'] = [
|
|
121
|
+
{ name: 'Slide 1', fields: { Heading: 'Welcome' } },
|
|
122
|
+
{ name: 'Slide 2', fields: { Heading: 'Agenda' } },
|
|
123
|
+
];
|
|
124
|
+
await deck.save();
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Each repeater item's sub-fields are resolved into the API's element format automatically, so the mutable `get()` → modify → `save()` path and `content.update()` produce identical results.
|
|
128
|
+
|
|
116
129
|
## Approve, duplicate, move, or remove
|
|
117
130
|
|
|
118
131
|
### Approve one item
|
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:
|
|
@@ -73,7 +99,9 @@ Graceful degradation paths, including failed media lookups and group name resolu
|
|
|
73
99
|
|
|
74
100
|
## Clear stale cache data
|
|
75
101
|
|
|
76
|
-
|
|
102
|
+
Content type writes through the SDK — `contentTypes.create()`, `contentTypes.update()`, `contentTypes.delete()`, and `ContentType.save()` (including `addField`/`removeField`) — clear the SDK's caches automatically, so a content type edit is reflected on the next read without any extra step.
|
|
103
|
+
|
|
104
|
+
You still need to clear the cache manually when configuration changes **outside** this SDK instance — for example content type, list, or other changes made in the T4 UI or by another process while your client is running:
|
|
77
105
|
|
|
78
106
|
```typescript
|
|
79
107
|
t4.clearCache();
|
package/docs/getting-started.md
CHANGED
|
@@ -141,6 +141,10 @@ The SDK caches content type templates, list values, element type definitions, an
|
|
|
141
141
|
t4.clearCache();
|
|
142
142
|
```
|
|
143
143
|
|
|
144
|
+
These caches are shared across the whole client, so reuse a single `T4Client` instance for the life of your task. Traversing the hierarchy — for example fetching and updating content across many sections with `t4.section(id)` — reuses cached element types and content type definitions instead of re-fetching them for each section. Creating a new `T4Client` per section would defeat this, so avoid that in loops.
|
|
145
|
+
|
|
146
|
+
Editing a content type through this client (`contentTypes.update()`, `ContentType.save()`, and so on) clears these caches for you, so your changes show up on the next read. Call `clearCache()` manually only for changes made outside this client instance.
|
|
147
|
+
|
|
144
148
|
---
|
|
145
149
|
|
|
146
150
|
**Next:** [Sections](./sections.md)
|
package/docs/lists.md
CHANGED
|
@@ -17,8 +17,8 @@ const list = await t4.lists.get(71);
|
|
|
17
17
|
| `description` | List description |
|
|
18
18
|
| `isForcedLanguage` | `false` |
|
|
19
19
|
| `isDefaultLanguage` | `false` |
|
|
20
|
-
| `primaryGroup` | `0` |
|
|
21
|
-
| `sharedGroups` | `
|
|
20
|
+
| `primaryGroup` | Owning group ID (`0` = none) |
|
|
21
|
+
| `sharedGroups` | Shared group IDs. Cannot contain `primaryGroup`. |
|
|
22
22
|
| `items` | Item definitions keyed by name |
|
|
23
23
|
|
|
24
24
|
Items are keyed by name:
|
package/docs/navigation.md
CHANGED
|
@@ -34,6 +34,8 @@ console.log(navigation.type); // 'a-to-z'
|
|
|
34
34
|
console.log(navigation.enabled); // true
|
|
35
35
|
console.log(navigation.cachingEnabled); // false
|
|
36
36
|
console.log(navigation.previewEnabled); // true
|
|
37
|
+
console.log(navigation.primaryGroup); // 1 (owning group ID; 0 = Global)
|
|
38
|
+
console.log(navigation.sharedGroups); // [34, 40]
|
|
37
39
|
console.log(navigation.properties); // properties vary by type
|
|
38
40
|
```
|
|
39
41
|
|
|
@@ -46,6 +48,8 @@ Type-specific properties use JavaScript booleans, numbers, and arrays. The SDK o
|
|
|
46
48
|
| `enabled` | yes | Whether the navigation is active |
|
|
47
49
|
| `cachingEnabled` | yes | Whether output caching is enabled |
|
|
48
50
|
| `previewEnabled` | yes | Whether preview mode is enabled |
|
|
51
|
+
| `primaryGroup` | yes | Owning group ID. `0` means no primary group (Global). |
|
|
52
|
+
| `sharedGroups` | yes | Array of group IDs the object is shared with |
|
|
49
53
|
| `properties` | yes | Type-specific configuration |
|
|
50
54
|
|
|
51
55
|
## Create a navigation object
|
|
@@ -63,7 +67,16 @@ await t4.navigation.create({
|
|
|
63
67
|
});
|
|
64
68
|
```
|
|
65
69
|
|
|
66
|
-
Only `type` and `name` are required. Properties have default values.
|
|
70
|
+
Only `type` and `name` are required. Properties have default values. `create()` also accepts optional `primaryGroup` and `sharedGroups`:
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
await t4.navigation.create({
|
|
74
|
+
type: 'breadcrumbs',
|
|
75
|
+
name: 'Main Breadcrumbs',
|
|
76
|
+
primaryGroup: 35, // optional; owning group ID
|
|
77
|
+
sharedGroups: [34, 40], // optional; group IDs to share with
|
|
78
|
+
});
|
|
79
|
+
```
|
|
67
80
|
|
|
68
81
|
## Update a navigation object
|
|
69
82
|
|
|
@@ -99,6 +112,22 @@ navigation.properties.beforeHtml = '<div>';
|
|
|
99
112
|
await navigation.save();
|
|
100
113
|
```
|
|
101
114
|
|
|
115
|
+
## Group and visibility
|
|
116
|
+
|
|
117
|
+
`primaryGroup` is the object's owning group and `sharedGroups` are the groups it is shared with. Both are read on `get()` and written on `save()`, `update()`, and `create()`. Set `primaryGroup` to `0` to remove the owning group (Global):
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
const navigation = await t4.navigation.get(181);
|
|
121
|
+
navigation.primaryGroup = 0; // remove from its group (Global)
|
|
122
|
+
navigation.sharedGroups = [];
|
|
123
|
+
await navigation.save();
|
|
124
|
+
|
|
125
|
+
// Or through update()
|
|
126
|
+
await t4.navigation.update(181, { primaryGroup: 35, sharedGroups: [34] });
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`sharedGroups` cannot contain the `primaryGroup` id: a group cannot be both the owning group and a shared group. The SDK throws a clear error before sending the request (Terminalfour otherwise returns an opaque 500).
|
|
130
|
+
|
|
102
131
|
## Delete a navigation object
|
|
103
132
|
|
|
104
133
|
```typescript
|