@terminalfour/terminalfour-js 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/cjs/content-cache.d.ts +38 -0
  2. package/dist/cjs/content-cache.js +42 -0
  3. package/dist/cjs/element-resolver.d.ts +16 -0
  4. package/dist/cjs/element-resolver.js +77 -1
  5. package/dist/cjs/models/content-item.js +13 -1
  6. package/dist/cjs/resources/content-resource.d.ts +7 -11
  7. package/dist/cjs/resources/content-resource.js +36 -79
  8. package/dist/cjs/resources/content-type-resource.d.ts +8 -8
  9. package/dist/cjs/resources/content-type-resource.js +25 -6
  10. package/dist/cjs/resources/list-resource.d.ts +2 -6
  11. package/dist/cjs/resources/list-resource.js +8 -6
  12. package/dist/cjs/resources/navigation-resource.d.ts +17 -0
  13. package/dist/cjs/resources/navigation-resource.js +13 -2
  14. package/dist/cjs/resources/page-layout-resource.d.ts +11 -0
  15. package/dist/cjs/resources/page-layout-resource.js +12 -2
  16. package/dist/cjs/section-ref.d.ts +3 -1
  17. package/dist/cjs/section-ref.js +6 -5
  18. package/dist/cjs/t4-client.d.ts +1 -0
  19. package/dist/cjs/t4-client.js +6 -1
  20. package/dist/cjs/utils.d.ts +39 -0
  21. package/dist/cjs/utils.js +37 -0
  22. package/dist/esm/content-cache.d.ts +38 -0
  23. package/dist/esm/content-cache.js +38 -0
  24. package/dist/esm/element-resolver.d.ts +16 -0
  25. package/dist/esm/element-resolver.js +77 -1
  26. package/dist/esm/models/content-item.js +13 -1
  27. package/dist/esm/resources/content-resource.d.ts +7 -11
  28. package/dist/esm/resources/content-resource.js +37 -80
  29. package/dist/esm/resources/content-type-resource.d.ts +8 -8
  30. package/dist/esm/resources/content-type-resource.js +26 -7
  31. package/dist/esm/resources/list-resource.d.ts +2 -6
  32. package/dist/esm/resources/list-resource.js +9 -7
  33. package/dist/esm/resources/navigation-resource.d.ts +17 -0
  34. package/dist/esm/resources/navigation-resource.js +13 -2
  35. package/dist/esm/resources/page-layout-resource.d.ts +11 -0
  36. package/dist/esm/resources/page-layout-resource.js +13 -3
  37. package/dist/esm/section-ref.d.ts +3 -1
  38. package/dist/esm/section-ref.js +6 -5
  39. package/dist/esm/t4-client.d.ts +1 -0
  40. package/dist/esm/t4-client.js +6 -1
  41. package/dist/esm/utils.d.ts +39 -0
  42. package/dist/esm/utils.js +32 -0
  43. package/docs/content-types.md +20 -3
  44. package/docs/content.md +13 -0
  45. package/docs/error-handling.md +3 -1
  46. package/docs/getting-started.md +4 -0
  47. package/docs/lists.md +2 -2
  48. package/docs/navigation.md +30 -1
  49. package/docs/page-layouts.md +26 -0
  50. package/package.json +1 -1
@@ -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.
@@ -68,6 +68,45 @@ 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;
71
110
  /** Accepted file input: a file path, URL, Blob, ReadableStream, or { file, filename } object */
72
111
  export type FileInput = string | Blob | NodeJS.ReadableStream | {
73
112
  file: string | Blob | NodeJS.ReadableStream;
package/dist/esm/utils.js CHANGED
@@ -170,6 +170,38 @@ 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
+ }
173
205
  /**
174
206
  * Resolves a file input (path, URL, Blob, or ReadableStream) to a Blob.
175
207
  */
@@ -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` | Primary group |
40
- | `sharedGroups` | Shared groups |
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:
@@ -167,6 +167,8 @@ await t4.contentTypes.update(44, {
167
167
  });
168
168
  ```
169
169
 
170
+ > **Warning:** Removing a field deletes that element's content from every content item using this content type, and it cannot be recovered. Only remove a field when you're certain the data is no longer needed. To change a field's length, description, or name, use `updateFields` rather than removing and re-adding.
171
+
170
172
  Change properties of existing fields with `updateFields`. Fields are matched by `name`, and only the properties you supply are changed — anything you omit is left as-is. This is how you increase an element's length (`maxSize`):
171
173
 
172
174
  ```typescript
@@ -178,7 +180,20 @@ await t4.contentTypes.update(44, {
178
180
  });
179
181
  ```
180
182
 
181
- `updateFields` can change `maxSize`, `description`, `required`, and `shown`. It throws if a named field does not exist on the content type, and (like the other field operations) nothing is sent to T4 when it throws. To change a field's *type* or *name*, remove it and add a new one instead.
183
+ Rename an existing element with `newName`. The field is matched by its current `name`, and you can rename and change other properties in the same entry:
184
+
185
+ ```typescript
186
+ await t4.contentTypes.update(44, {
187
+ updateFields: [
188
+ { name: 'Title', newName: 'Headline' },
189
+ { name: 'Summary', newName: 'Teaser', maxSize: 300 },
190
+ ],
191
+ });
192
+ ```
193
+
194
+ After a rename, the returned content type is addressable by the new name (`ct.fields['Headline']`); the old key no longer exists.
195
+
196
+ `updateFields` can change `maxSize`, `description`, `required`, `shown`, and `newName` (rename). It throws if a named field does not exist on the content type, and (like the other field operations) nothing is sent to T4 when it throws. Renaming an element on a system content type is blocked, except on the Section Meta Data and Extended User types (the same rule that applies to renames through the mutable pattern). It's currently not possible to change a field's *type*.
182
197
 
183
198
  `updateFields`, `removeFields`, and `addFields` can be combined in a single call; updates are applied first, then removals, then additions.
184
199
 
@@ -240,6 +255,8 @@ contentType.removeField('Old Field');
240
255
  await contentType.save();
241
256
  ```
242
257
 
258
+ > **Warning:** `removeField()` deletes the element's content from every content item using this content type once you `save()`, and it cannot be recovered. Prefer `updateFields` (or mutating the field directly) to change a field's length, description, or name.
259
+
243
260
  Delete a content type through the resource:
244
261
 
245
262
  ```typescript
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
@@ -73,7 +73,9 @@ Graceful degradation paths, including failed media lookups and group name resolu
73
73
 
74
74
  ## Clear stale cache data
75
75
 
76
- After changing content types, lists, or other configuration, invalidate every SDK cache:
76
+ 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.
77
+
78
+ 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
79
 
78
80
  ```typescript
79
81
  t4.clearCache();
@@ -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:
@@ -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
@@ -22,6 +22,8 @@ A full page layout includes:
22
22
  | `fileExtension` | Output file extension |
23
23
  | `syntax` | Syntax such as `'HTML/XML'` |
24
24
  | `processor` | Processor such as `'handlebars'` |
25
+ | `primaryGroup` | Owning group ID. `0` means no primary group (Global). |
26
+ | `sharedGroups` | Array of group IDs the layout is shared with |
25
27
 
26
28
  ## Create a page layout
27
29
 
@@ -34,11 +36,35 @@ await t4.pageLayouts.create({
34
36
  syntax: 'HTML/XML', // optional
35
37
  processor: 'handlebars', // optional; default: 'handlebars'
36
38
  fileExtension: 'html', // optional
39
+ primaryGroup: 35, // optional; owning group ID
40
+ sharedGroups: [34, 40], // optional; group IDs to share with
37
41
  });
38
42
  ```
39
43
 
40
44
  Processor options are `'handlebars'`, `'t4-tags'`, and `'programmable-layouts'`. The default is `'handlebars'`.
41
45
 
46
+ ## Group and visibility
47
+
48
+ `primaryGroup` is the layout'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):
49
+
50
+ ```typescript
51
+ const layout = await t4.pageLayouts.get(5);
52
+ console.log(layout.primaryGroup); // 35
53
+ console.log(layout.sharedGroups); // [34, 40]
54
+
55
+ layout.primaryGroup = 0; // remove from its group (Global)
56
+ layout.sharedGroups = [];
57
+ await layout.save();
58
+ ```
59
+
60
+ Or through `update()`:
61
+
62
+ ```typescript
63
+ await t4.pageLayouts.update(5, { primaryGroup: 35, sharedGroups: [34] });
64
+ ```
65
+
66
+ `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).
67
+
42
68
  ## Update a page layout
43
69
 
44
70
  ### Direct update
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terminalfour/terminalfour-js",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "JavaScript / TypeScript SDK for the Terminalfour Web Services REST API",
5
5
  "keywords": [
6
6
  "terminalfour",