@terminalfour/terminalfour-js 1.0.0-rc.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.
Files changed (253) hide show
  1. package/LICENSE.md +106 -0
  2. package/README.md +169 -0
  3. package/dist/cjs/element-resolver.d.ts +115 -0
  4. package/dist/cjs/element-resolver.d.ts.map +1 -0
  5. package/dist/cjs/element-resolver.js +391 -0
  6. package/dist/cjs/element-resolver.js.map +1 -0
  7. package/dist/cjs/errors.d.ts +21 -0
  8. package/dist/cjs/errors.d.ts.map +1 -0
  9. package/dist/cjs/errors.js +21 -0
  10. package/dist/cjs/errors.js.map +1 -0
  11. package/dist/cjs/handlebars.d.ts +123 -0
  12. package/dist/cjs/handlebars.d.ts.map +1 -0
  13. package/dist/cjs/handlebars.js +306 -0
  14. package/dist/cjs/handlebars.js.map +1 -0
  15. package/dist/cjs/http-client.d.ts +21 -0
  16. package/dist/cjs/http-client.d.ts.map +1 -0
  17. package/dist/cjs/http-client.js +126 -0
  18. package/dist/cjs/http-client.js.map +1 -0
  19. package/dist/cjs/index.d.ts +37 -0
  20. package/dist/cjs/index.d.ts.map +1 -0
  21. package/dist/cjs/index.js +55 -0
  22. package/dist/cjs/index.js.map +1 -0
  23. package/dist/cjs/media-category-ref.d.ts +78 -0
  24. package/dist/cjs/media-category-ref.d.ts.map +1 -0
  25. package/dist/cjs/media-category-ref.js +184 -0
  26. package/dist/cjs/media-category-ref.js.map +1 -0
  27. package/dist/cjs/media-library.d.ts +30 -0
  28. package/dist/cjs/media-library.d.ts.map +1 -0
  29. package/dist/cjs/media-library.js +77 -0
  30. package/dist/cjs/media-library.js.map +1 -0
  31. package/dist/cjs/models/content-item.d.ts +80 -0
  32. package/dist/cjs/models/content-item.d.ts.map +1 -0
  33. package/dist/cjs/models/content-item.js +682 -0
  34. package/dist/cjs/models/content-item.js.map +1 -0
  35. package/dist/cjs/models/media-category-item.d.ts +29 -0
  36. package/dist/cjs/models/media-category-item.d.ts.map +1 -0
  37. package/dist/cjs/models/media-category-item.js +33 -0
  38. package/dist/cjs/models/media-category-item.js.map +1 -0
  39. package/dist/cjs/models/media-item.d.ts +74 -0
  40. package/dist/cjs/models/media-item.d.ts.map +1 -0
  41. package/dist/cjs/models/media-item.js +188 -0
  42. package/dist/cjs/models/media-item.js.map +1 -0
  43. package/dist/cjs/models/section-item.d.ts +58 -0
  44. package/dist/cjs/models/section-item.d.ts.map +1 -0
  45. package/dist/cjs/models/section-item.js +166 -0
  46. package/dist/cjs/models/section-item.js.map +1 -0
  47. package/dist/cjs/package.json +1 -0
  48. package/dist/cjs/resources/channel-resource.d.ts +112 -0
  49. package/dist/cjs/resources/channel-resource.d.ts.map +1 -0
  50. package/dist/cjs/resources/channel-resource.js +107 -0
  51. package/dist/cjs/resources/channel-resource.js.map +1 -0
  52. package/dist/cjs/resources/content-resource.d.ts +50 -0
  53. package/dist/cjs/resources/content-resource.d.ts.map +1 -0
  54. package/dist/cjs/resources/content-resource.js +286 -0
  55. package/dist/cjs/resources/content-resource.js.map +1 -0
  56. package/dist/cjs/resources/content-type-resource.d.ts +283 -0
  57. package/dist/cjs/resources/content-type-resource.d.ts.map +1 -0
  58. package/dist/cjs/resources/content-type-resource.js +970 -0
  59. package/dist/cjs/resources/content-type-resource.js.map +1 -0
  60. package/dist/cjs/resources/group-resource.d.ts +96 -0
  61. package/dist/cjs/resources/group-resource.d.ts.map +1 -0
  62. package/dist/cjs/resources/group-resource.js +213 -0
  63. package/dist/cjs/resources/group-resource.js.map +1 -0
  64. package/dist/cjs/resources/list-resource.d.ts +111 -0
  65. package/dist/cjs/resources/list-resource.d.ts.map +1 -0
  66. package/dist/cjs/resources/list-resource.js +179 -0
  67. package/dist/cjs/resources/list-resource.js.map +1 -0
  68. package/dist/cjs/resources/media-resource.d.ts +69 -0
  69. package/dist/cjs/resources/media-resource.d.ts.map +1 -0
  70. package/dist/cjs/resources/media-resource.js +210 -0
  71. package/dist/cjs/resources/media-resource.js.map +1 -0
  72. package/dist/cjs/resources/media-type-resource.d.ts +70 -0
  73. package/dist/cjs/resources/media-type-resource.d.ts.map +1 -0
  74. package/dist/cjs/resources/media-type-resource.js +195 -0
  75. package/dist/cjs/resources/media-type-resource.js.map +1 -0
  76. package/dist/cjs/resources/navigation-resource.d.ts +664 -0
  77. package/dist/cjs/resources/navigation-resource.d.ts.map +1 -0
  78. package/dist/cjs/resources/navigation-resource.js +2349 -0
  79. package/dist/cjs/resources/navigation-resource.js.map +1 -0
  80. package/dist/cjs/resources/page-layout-resource.d.ts +83 -0
  81. package/dist/cjs/resources/page-layout-resource.d.ts.map +1 -0
  82. package/dist/cjs/resources/page-layout-resource.js +214 -0
  83. package/dist/cjs/resources/page-layout-resource.js.map +1 -0
  84. package/dist/cjs/resources/user-resource.d.ts +126 -0
  85. package/dist/cjs/resources/user-resource.d.ts.map +1 -0
  86. package/dist/cjs/resources/user-resource.js +317 -0
  87. package/dist/cjs/resources/user-resource.js.map +1 -0
  88. package/dist/cjs/section-ref.d.ts +185 -0
  89. package/dist/cjs/section-ref.d.ts.map +1 -0
  90. package/dist/cjs/section-ref.js +813 -0
  91. package/dist/cjs/section-ref.js.map +1 -0
  92. package/dist/cjs/site-structure.d.ts +18 -0
  93. package/dist/cjs/site-structure.d.ts.map +1 -0
  94. package/dist/cjs/site-structure.js +57 -0
  95. package/dist/cjs/site-structure.js.map +1 -0
  96. package/dist/cjs/t4-client.d.ts +121 -0
  97. package/dist/cjs/t4-client.d.ts.map +1 -0
  98. package/dist/cjs/t4-client.js +190 -0
  99. package/dist/cjs/t4-client.js.map +1 -0
  100. package/dist/cjs/type-registry.d.ts +31 -0
  101. package/dist/cjs/type-registry.d.ts.map +1 -0
  102. package/dist/cjs/type-registry.js +76 -0
  103. package/dist/cjs/type-registry.js.map +1 -0
  104. package/dist/cjs/types.d.ts +211 -0
  105. package/dist/cjs/types.d.ts.map +1 -0
  106. package/dist/cjs/types.js +3 -0
  107. package/dist/cjs/types.js.map +1 -0
  108. package/dist/cjs/utils.d.ts +147 -0
  109. package/dist/cjs/utils.d.ts.map +1 -0
  110. package/dist/cjs/utils.js +408 -0
  111. package/dist/cjs/utils.js.map +1 -0
  112. package/dist/esm/element-resolver.d.ts +115 -0
  113. package/dist/esm/element-resolver.d.ts.map +1 -0
  114. package/dist/esm/element-resolver.js +387 -0
  115. package/dist/esm/element-resolver.js.map +1 -0
  116. package/dist/esm/errors.d.ts +21 -0
  117. package/dist/esm/errors.d.ts.map +1 -0
  118. package/dist/esm/errors.js +17 -0
  119. package/dist/esm/errors.js.map +1 -0
  120. package/dist/esm/handlebars.d.ts +123 -0
  121. package/dist/esm/handlebars.d.ts.map +1 -0
  122. package/dist/esm/handlebars.js +300 -0
  123. package/dist/esm/handlebars.js.map +1 -0
  124. package/dist/esm/http-client.d.ts +21 -0
  125. package/dist/esm/http-client.d.ts.map +1 -0
  126. package/dist/esm/http-client.js +122 -0
  127. package/dist/esm/http-client.js.map +1 -0
  128. package/dist/esm/index.d.ts +37 -0
  129. package/dist/esm/index.d.ts.map +1 -0
  130. package/dist/esm/index.js +26 -0
  131. package/dist/esm/index.js.map +1 -0
  132. package/dist/esm/media-category-ref.d.ts +78 -0
  133. package/dist/esm/media-category-ref.d.ts.map +1 -0
  134. package/dist/esm/media-category-ref.js +180 -0
  135. package/dist/esm/media-category-ref.js.map +1 -0
  136. package/dist/esm/media-library.d.ts +30 -0
  137. package/dist/esm/media-library.d.ts.map +1 -0
  138. package/dist/esm/media-library.js +73 -0
  139. package/dist/esm/media-library.js.map +1 -0
  140. package/dist/esm/models/content-item.d.ts +80 -0
  141. package/dist/esm/models/content-item.d.ts.map +1 -0
  142. package/dist/esm/models/content-item.js +676 -0
  143. package/dist/esm/models/content-item.js.map +1 -0
  144. package/dist/esm/models/media-category-item.d.ts +29 -0
  145. package/dist/esm/models/media-category-item.d.ts.map +1 -0
  146. package/dist/esm/models/media-category-item.js +29 -0
  147. package/dist/esm/models/media-category-item.js.map +1 -0
  148. package/dist/esm/models/media-item.d.ts +74 -0
  149. package/dist/esm/models/media-item.d.ts.map +1 -0
  150. package/dist/esm/models/media-item.js +184 -0
  151. package/dist/esm/models/media-item.js.map +1 -0
  152. package/dist/esm/models/section-item.d.ts +58 -0
  153. package/dist/esm/models/section-item.d.ts.map +1 -0
  154. package/dist/esm/models/section-item.js +162 -0
  155. package/dist/esm/models/section-item.js.map +1 -0
  156. package/dist/esm/resources/channel-resource.d.ts +112 -0
  157. package/dist/esm/resources/channel-resource.d.ts.map +1 -0
  158. package/dist/esm/resources/channel-resource.js +102 -0
  159. package/dist/esm/resources/channel-resource.js.map +1 -0
  160. package/dist/esm/resources/content-resource.d.ts +50 -0
  161. package/dist/esm/resources/content-resource.d.ts.map +1 -0
  162. package/dist/esm/resources/content-resource.js +282 -0
  163. package/dist/esm/resources/content-resource.js.map +1 -0
  164. package/dist/esm/resources/content-type-resource.d.ts +283 -0
  165. package/dist/esm/resources/content-type-resource.d.ts.map +1 -0
  166. package/dist/esm/resources/content-type-resource.js +964 -0
  167. package/dist/esm/resources/content-type-resource.js.map +1 -0
  168. package/dist/esm/resources/group-resource.d.ts +96 -0
  169. package/dist/esm/resources/group-resource.d.ts.map +1 -0
  170. package/dist/esm/resources/group-resource.js +208 -0
  171. package/dist/esm/resources/group-resource.js.map +1 -0
  172. package/dist/esm/resources/list-resource.d.ts +111 -0
  173. package/dist/esm/resources/list-resource.d.ts.map +1 -0
  174. package/dist/esm/resources/list-resource.js +174 -0
  175. package/dist/esm/resources/list-resource.js.map +1 -0
  176. package/dist/esm/resources/media-resource.d.ts +69 -0
  177. package/dist/esm/resources/media-resource.d.ts.map +1 -0
  178. package/dist/esm/resources/media-resource.js +206 -0
  179. package/dist/esm/resources/media-resource.js.map +1 -0
  180. package/dist/esm/resources/media-type-resource.d.ts +70 -0
  181. package/dist/esm/resources/media-type-resource.d.ts.map +1 -0
  182. package/dist/esm/resources/media-type-resource.js +190 -0
  183. package/dist/esm/resources/media-type-resource.js.map +1 -0
  184. package/dist/esm/resources/navigation-resource.d.ts +664 -0
  185. package/dist/esm/resources/navigation-resource.d.ts.map +1 -0
  186. package/dist/esm/resources/navigation-resource.js +2344 -0
  187. package/dist/esm/resources/navigation-resource.js.map +1 -0
  188. package/dist/esm/resources/page-layout-resource.d.ts +83 -0
  189. package/dist/esm/resources/page-layout-resource.d.ts.map +1 -0
  190. package/dist/esm/resources/page-layout-resource.js +209 -0
  191. package/dist/esm/resources/page-layout-resource.js.map +1 -0
  192. package/dist/esm/resources/user-resource.d.ts +126 -0
  193. package/dist/esm/resources/user-resource.d.ts.map +1 -0
  194. package/dist/esm/resources/user-resource.js +312 -0
  195. package/dist/esm/resources/user-resource.js.map +1 -0
  196. package/dist/esm/section-ref.d.ts +185 -0
  197. package/dist/esm/section-ref.d.ts.map +1 -0
  198. package/dist/esm/section-ref.js +808 -0
  199. package/dist/esm/section-ref.js.map +1 -0
  200. package/dist/esm/site-structure.d.ts +18 -0
  201. package/dist/esm/site-structure.d.ts.map +1 -0
  202. package/dist/esm/site-structure.js +53 -0
  203. package/dist/esm/site-structure.js.map +1 -0
  204. package/dist/esm/t4-client.d.ts +121 -0
  205. package/dist/esm/t4-client.d.ts.map +1 -0
  206. package/dist/esm/t4-client.js +186 -0
  207. package/dist/esm/t4-client.js.map +1 -0
  208. package/dist/esm/type-registry.d.ts +31 -0
  209. package/dist/esm/type-registry.d.ts.map +1 -0
  210. package/dist/esm/type-registry.js +72 -0
  211. package/dist/esm/type-registry.js.map +1 -0
  212. package/dist/esm/types.d.ts +211 -0
  213. package/dist/esm/types.d.ts.map +1 -0
  214. package/dist/esm/types.js +2 -0
  215. package/dist/esm/types.js.map +1 -0
  216. package/dist/esm/utils.d.ts +147 -0
  217. package/dist/esm/utils.d.ts.map +1 -0
  218. package/dist/esm/utils.js +355 -0
  219. package/dist/esm/utils.js.map +1 -0
  220. package/docs/channels.md +62 -0
  221. package/docs/content-types.md +313 -0
  222. package/docs/content.md +199 -0
  223. package/docs/error-handling.md +86 -0
  224. package/docs/getting-started.md +146 -0
  225. package/docs/groups-and-users.md +167 -0
  226. package/docs/handlebars.md +145 -0
  227. package/docs/lists.md +98 -0
  228. package/docs/media-types.md +111 -0
  229. package/docs/media.md +169 -0
  230. package/docs/navigation/a-to-z.md +62 -0
  231. package/docs/navigation/breadcrumbs.md +67 -0
  232. package/docs/navigation/css-selector.md +53 -0
  233. package/docs/navigation/generate-file.md +48 -0
  234. package/docs/navigation/keyword-search.md +134 -0
  235. package/docs/navigation/language-switcher.md +37 -0
  236. package/docs/navigation/link-menu.md +105 -0
  237. package/docs/navigation/pagination.md +80 -0
  238. package/docs/navigation/previous-next-fulltext.md +43 -0
  239. package/docs/navigation/publish-to-one-file.md +93 -0
  240. package/docs/navigation/related-content.md +79 -0
  241. package/docs/navigation/related-section-branch.md +31 -0
  242. package/docs/navigation/return-to-index.md +35 -0
  243. package/docs/navigation/section-details.md +51 -0
  244. package/docs/navigation/section-iterator.md +33 -0
  245. package/docs/navigation/section-meta-info.md +39 -0
  246. package/docs/navigation/site-map.md +58 -0
  247. package/docs/navigation/top-content.md +80 -0
  248. package/docs/navigation/top-stories.md +47 -0
  249. package/docs/navigation.md +134 -0
  250. package/docs/page-layouts.md +71 -0
  251. package/docs/sections.md +224 -0
  252. package/docs/typescript.md +124 -0
  253. package/package.json +64 -0
@@ -0,0 +1,808 @@
1
+ import { resolveLanguage, mapStatus, flattenGroups, STATUS_CODES, AUTH_LEVEL_MAP, debugWarn, DEFAULT_CACHE_TTL, getCacheEpoch } from './utils.js';
2
+ import { ContentResource } from './resources/content-resource.js';
3
+ import { SectionItem } from './models/section-item.js';
4
+ /** Per-client cache for meta tag definitions, keyed by HttpClient instance */
5
+ const metaTagCacheMap = new WeakMap();
6
+ /** Clears the meta tag cache for a specific HttpClient instance */
7
+ export function clearMetaTagCache(httpClient) {
8
+ metaTagCacheMap.delete(httpClient);
9
+ }
10
+ /**
11
+ * Strips the internal `Name` element from resolved metadata content fields.
12
+ * The Section Meta Data content type always has a Name element that mirrors the
13
+ * section name, but it isn't a user-facing custom field — `section.name` already
14
+ * exposes it. Returns `null` when the input is `null`.
15
+ */
16
+ function stripNameField(fields) {
17
+ if (fields === null)
18
+ return null;
19
+ const result = {};
20
+ for (const [key, value] of Object.entries(fields)) {
21
+ if (key.toLowerCase() === 'name')
22
+ continue;
23
+ result[key] = value;
24
+ }
25
+ return result;
26
+ }
27
+ /**
28
+ * Resolves the Section Meta Data content type ID.
29
+ *
30
+ * Prefers the section's own `metaData.type`, but falls back to the instance-level
31
+ * configuration (`GET /config/hierarchy.metaDataContentType`) when the section has
32
+ * none. This covers sections created before a metadata content type existed, or
33
+ * created via the API without one, while a metadata content type is now configured
34
+ * on the instance. Returns `0` when no metadata content type is configured anywhere.
35
+ */
36
+ async function resolveMetaDataTypeId(httpClient, sectionMetaDataType) {
37
+ if (sectionMetaDataType > 0)
38
+ return sectionMetaDataType;
39
+ try {
40
+ const config = await httpClient.request({
41
+ method: 'GET',
42
+ path: '/config/hierarchy.metaDataContentType',
43
+ });
44
+ return parseInt(config.value, 10) || 0;
45
+ }
46
+ catch {
47
+ return 0;
48
+ }
49
+ }
50
+ /**
51
+ * A section reference scoped to a specific section ID.
52
+ * Provides content operations, section metadata, hierarchy navigation,
53
+ * child section creation, and deletion — all from a single entry point.
54
+ */
55
+ export class SectionRef {
56
+ constructor(httpClient, sectionId, defaultLanguage, mediaCreateFn) {
57
+ this.httpClient = httpClient;
58
+ this.sectionId = sectionId;
59
+ this.defaultLanguage = defaultLanguage;
60
+ this.mediaCreateFn = mediaCreateFn ?? null;
61
+ this.content = new ContentResource(httpClient, sectionId, defaultLanguage, this.mediaCreateFn);
62
+ }
63
+ // ── Section metadata ──
64
+ /** Returns a mutable section object. Modify properties and call save() to persist. */
65
+ async get(options) {
66
+ const language = resolveLanguage(options?.language, this.defaultLanguage);
67
+ const raw = await this.httpClient.request({
68
+ method: 'GET',
69
+ path: `/hierarchy/${this.sectionId}/${language}`,
70
+ });
71
+ // Fetch custom fields from metadata content if configured
72
+ let customFields = null;
73
+ const meta = raw.metaData;
74
+ if (meta?.enabled && meta.id) {
75
+ try {
76
+ const metaContent = new ContentResource(this.httpClient, this.sectionId, this.defaultLanguage, this.mediaCreateFn);
77
+ const item = await metaContent.get(meta.id, { language });
78
+ customFields = stripNameField(item.fields ?? null);
79
+ }
80
+ catch {
81
+ // Metadata content may not exist yet — that's fine
82
+ }
83
+ }
84
+ return new SectionItem(raw, this.httpClient, language, customFields, this.mediaCreateFn);
85
+ }
86
+ /** Returns the list of channels associated with this section. */
87
+ async channels() {
88
+ return this.httpClient.request({
89
+ method: 'GET',
90
+ path: `/section/${this.sectionId}/channels`,
91
+ });
92
+ }
93
+ /**
94
+ * Returns the page layouts for this section, one entry per channel.
95
+ *
96
+ * Each entry has two fields:
97
+ * - `pageLayout` — the layout applied to *this* section for the channel.
98
+ * Either directly set (`inherited: false`) or inherited from a parent (`inherited: true`).
99
+ * `null` if no layout is set or inherited.
100
+ * - `childPageLayout` — the layout that descendant sections will inherit
101
+ * when they don't have their own explicit layout. `null` if not set.
102
+ *
103
+ * Data sources in the API response:
104
+ * - `channels[].pageLayout` — directly assigned layout on this section (non-zero = direct)
105
+ * - `inheritedPageLayouts[channelId]` — layout inherited from a parent (used when no direct assignment)
106
+ * - `channels[].inheritedPageLayout` — the "apply to descendants" layout
107
+ */
108
+ async pageLayouts(options) {
109
+ const language = resolveLanguage(options?.language, this.defaultLanguage);
110
+ // Fetch section details and channel names in parallel
111
+ const [section, channelList] = await Promise.all([
112
+ this.httpClient.request({
113
+ method: 'GET',
114
+ path: `/hierarchy/${this.sectionId}/${language}`,
115
+ }),
116
+ this.httpClient.request({
117
+ method: 'GET',
118
+ path: `/section/${this.sectionId}/channels`,
119
+ }),
120
+ ]);
121
+ // channels[]: direct layout + descendant layout per channel
122
+ const channelEntries = section.channels ?? [];
123
+ const directMap = new Map(channelEntries.map((ch) => [ch.id, ch.pageLayout]));
124
+ const childMap = new Map(channelEntries.map((ch) => [ch.id, ch.inheritedPageLayout]));
125
+ // inheritedPageLayouts: layout inherited from parent, keyed by string channel ID
126
+ const parentInheritedMap = new Map(Object.entries(section.inheritedPageLayouts ?? {}).map(([k, v]) => [Number(k), v]));
127
+ // Union of all channel IDs from both sources
128
+ const allChannelIds = new Set([...directMap.keys(), ...parentInheritedMap.keys()]);
129
+ if (allChannelIds.size === 0)
130
+ return [];
131
+ // Build a channel name lookup
132
+ const channelNameMap = new Map(channelList.map((ch) => [ch.id, ch.name]));
133
+ // Collect all unique non-zero layout IDs to resolve names in one request
134
+ const layoutIds = new Set();
135
+ for (const id of allChannelIds) {
136
+ const direct = directMap.get(id);
137
+ const parentInherited = parentInheritedMap.get(id);
138
+ const child = childMap.get(id);
139
+ if (direct)
140
+ layoutIds.add(direct);
141
+ if (parentInherited)
142
+ layoutIds.add(parentInherited);
143
+ if (child)
144
+ layoutIds.add(child);
145
+ }
146
+ // Fetch all page layouts in one request and build a lookup map
147
+ let layoutNameMap = new Map();
148
+ if (layoutIds.size > 0) {
149
+ const allLayouts = await this.httpClient.request({
150
+ method: 'GET',
151
+ path: '/pageLayout',
152
+ });
153
+ layoutNameMap = new Map(allLayouts.map((l) => [l.id, l.name]));
154
+ }
155
+ const resolveName = (id) => layoutNameMap.get(id) ?? `Page Layout ${id}`;
156
+ return Array.from(allChannelIds).map((channelId) => {
157
+ const directId = directMap.get(channelId) ?? 0;
158
+ const parentInheritedId = parentInheritedMap.get(channelId) ?? 0;
159
+ const childId = childMap.get(channelId) ?? 0;
160
+ // pageLayout: direct if set, otherwise inherited from parent
161
+ let pageLayout = null;
162
+ if (directId) {
163
+ pageLayout = { id: directId, name: resolveName(directId), inherited: false };
164
+ }
165
+ else if (parentInheritedId) {
166
+ pageLayout = { id: parentInheritedId, name: resolveName(parentInheritedId), inherited: true };
167
+ }
168
+ return {
169
+ channel: {
170
+ id: channelId,
171
+ name: channelNameMap.get(channelId) ?? `Channel ${channelId}`,
172
+ },
173
+ pageLayout,
174
+ childPageLayout: childId ? { id: childId, name: resolveName(childId) } : null,
175
+ };
176
+ });
177
+ }
178
+ /**
179
+ * Updates page layout assignments on this section. Additive — only the channels
180
+ * you pass are changed; others keep their existing values.
181
+ *
182
+ * Each entry requires a `channelId` and at least one of:
183
+ * - `pageLayout` — the layout to apply directly to this section for the channel
184
+ * - `childPageLayout` — the layout that descendant sections will inherit
185
+ *
186
+ * Pass `null` for either to clear that assignment (sets to 0).
187
+ */
188
+ async setPageLayouts(layouts, options) {
189
+ if (!layouts.length) {
190
+ throw new Error('setPageLayouts requires at least one channel entry');
191
+ }
192
+ for (const entry of layouts) {
193
+ if (entry.pageLayout === undefined && entry.childPageLayout === undefined) {
194
+ throw new Error(`setPageLayouts: channel ${entry.channelId} requires at least one of pageLayout or childPageLayout`);
195
+ }
196
+ }
197
+ const language = resolveLanguage(options?.language, this.defaultLanguage);
198
+ const section = await this.httpClient.request({
199
+ method: 'GET',
200
+ path: `/hierarchy/${this.sectionId}/${language}`,
201
+ });
202
+ // Build a mutable map of existing channel entries
203
+ const channelMap = new Map((section.channels ?? []).map((ch) => [ch.id, { ...ch }]));
204
+ // Merge in the user's changes
205
+ for (const entry of layouts) {
206
+ const existing = channelMap.get(entry.channelId);
207
+ if (existing) {
208
+ // Update existing channel entry
209
+ if (entry.pageLayout !== undefined) {
210
+ existing.pageLayout = entry.pageLayout ?? 0;
211
+ }
212
+ if (entry.childPageLayout !== undefined) {
213
+ existing.inheritedPageLayout = entry.childPageLayout ?? 0;
214
+ }
215
+ }
216
+ else {
217
+ // Add new channel entry
218
+ channelMap.set(entry.channelId, {
219
+ id: entry.channelId,
220
+ pageLayout: entry.pageLayout ?? 0,
221
+ inheritedPageLayout: entry.childPageLayout ?? 0,
222
+ });
223
+ }
224
+ }
225
+ await this.httpClient.request({
226
+ method: 'PUT',
227
+ path: `/hierarchy/${this.sectionId}/${language}`,
228
+ body: {
229
+ ...section,
230
+ channels: Array.from(channelMap.values()),
231
+ },
232
+ });
233
+ }
234
+ /** Returns the owner information for this section, including user details. */
235
+ async owner(options) {
236
+ const language = resolveLanguage(options?.language, this.defaultLanguage);
237
+ const ownerDto = await this.httpClient.request({
238
+ method: 'GET',
239
+ path: `/section/${this.sectionId}/${language}/owner`,
240
+ });
241
+ // Fetch user details to get the name
242
+ const user = await this.httpClient.request({
243
+ method: 'GET',
244
+ path: `/user/${ownerDto.id}`,
245
+ });
246
+ return {
247
+ id: ownerDto.id,
248
+ type: AUTH_LEVEL_MAP[user.authLevel ?? 2] ?? `unknown (${user.authLevel})`,
249
+ username: user.username,
250
+ firstName: user.firstName,
251
+ lastName: user.lastName,
252
+ emailAddress: user.emailAddress,
253
+ };
254
+ }
255
+ /**
256
+ * Returns the section's meta data values with friendly tag names.
257
+ * Maps meta tag IDs to their names (e.g. id 19 → "og:title").
258
+ */
259
+ async metaDatas(options) {
260
+ const language = resolveLanguage(options?.language, this.defaultLanguage);
261
+ // Fetch section details and meta tag definitions in parallel
262
+ const [section, metaTags] = await Promise.all([
263
+ this.httpClient.request({
264
+ method: 'GET',
265
+ path: `/hierarchy/${this.sectionId}/${language}`,
266
+ }),
267
+ this.getMetaTagDefinitions(),
268
+ ]);
269
+ const result = {};
270
+ for (const meta of section.metaDatas ?? []) {
271
+ const tag = metaTags.find((t) => t.id === meta.id);
272
+ const name = tag?.name ?? String(meta.id);
273
+ result[name] = meta.value;
274
+ }
275
+ return result;
276
+ }
277
+ /** Fetches and caches meta tag definitions from GET /meta */
278
+ async getMetaTagDefinitions() {
279
+ const cached = metaTagCacheMap.get(this.httpClient);
280
+ if (cached && Date.now() < cached.expiresAt && cached.epoch >= getCacheEpoch())
281
+ return cached.data;
282
+ const tags = await this.httpClient.request({
283
+ method: 'GET',
284
+ path: '/meta',
285
+ });
286
+ metaTagCacheMap.set(this.httpClient, { data: tags, expiresAt: Date.now() + DEFAULT_CACHE_TTL, epoch: getCacheEpoch() });
287
+ return tags;
288
+ }
289
+ /**
290
+ * Updates meta data values on this section.
291
+ * Only updates the tags you pass — existing values are preserved.
292
+ * Uses friendly tag names (e.g. "og:title", "description").
293
+ */
294
+ async setMetaDatas(values, options) {
295
+ const language = resolveLanguage(options?.language, this.defaultLanguage);
296
+ // Fetch current section and meta tag definitions in parallel
297
+ const [section, metaTags] = await Promise.all([
298
+ this.httpClient.request({
299
+ method: 'GET',
300
+ path: `/hierarchy/${this.sectionId}/${language}`,
301
+ }),
302
+ this.getMetaTagDefinitions(),
303
+ ]);
304
+ // Start with existing metaDatas
305
+ const existingMetas = [...(section.metaDatas ?? [])];
306
+ // Merge in the new values
307
+ for (const [tagName, value] of Object.entries(values)) {
308
+ const tagDef = metaTags.find((t) => t.name.toLowerCase() === tagName.toLowerCase());
309
+ if (!tagDef) {
310
+ const validNames = metaTags.map((t) => `"${t.name}"`).join(', ');
311
+ throw new Error(`Invalid meta tag "${tagName}". Valid tags are: ${validNames}`);
312
+ }
313
+ // Update existing or add new
314
+ const existing = existingMetas.find((m) => m.id === tagDef.id);
315
+ if (existing) {
316
+ existing.value = value;
317
+ }
318
+ else {
319
+ existingMetas.push({ id: tagDef.id, value, lang: language });
320
+ }
321
+ }
322
+ // PUT the full section body with updated metaDatas
323
+ await this.httpClient.request({
324
+ method: 'PUT',
325
+ path: `/hierarchy/${this.sectionId}/${language}`,
326
+ body: {
327
+ ...section,
328
+ metaDatas: existingMetas,
329
+ },
330
+ });
331
+ }
332
+ // ── Hierarchy operations ──
333
+ /**
334
+ * Returns the section hierarchy tree starting from this section.
335
+ */
336
+ async tree(options) {
337
+ const language = resolveLanguage(options?.language, this.defaultLanguage);
338
+ const response = await this.httpClient.request({
339
+ method: 'POST',
340
+ path: '/hierarchy/section',
341
+ body: {
342
+ read: {
343
+ section: { id: this.sectionId, language },
344
+ recursionDepth: 1,
345
+ activeNode: this.sectionId,
346
+ explode: true,
347
+ showContentInfo: false,
348
+ showWidget: false,
349
+ openNodes: [this.sectionId],
350
+ showFullTree: true,
351
+ showAllSections: true,
352
+ restrictedToPermitedSections: false,
353
+ },
354
+ },
355
+ });
356
+ const root = response[0];
357
+ if (!root) {
358
+ return { id: this.sectionId, name: '' };
359
+ }
360
+ return this.mapTreeNode(root);
361
+ }
362
+ /** Recursively maps API tree nodes to clean SectionTreeNode objects */
363
+ mapTreeNode(node) {
364
+ const children = (node.subsections ?? []).map((child) => this.mapTreeNode(child));
365
+ const result = {
366
+ id: node.id,
367
+ name: node.name,
368
+ status: mapStatus(node.status),
369
+ };
370
+ if (children.length > 0) {
371
+ result.children = children;
372
+ }
373
+ return result;
374
+ }
375
+ /** Lists direct child sections (one level below). */
376
+ async subsections(options) {
377
+ const language = resolveLanguage(options?.language, this.defaultLanguage);
378
+ const response = await this.httpClient.request({
379
+ method: 'GET',
380
+ path: `/hierarchy/${this.sectionId}/${language}/subsections?showAll=false&removeNonTranslated=false`,
381
+ });
382
+ return (response.children ?? []).map((child) => ({
383
+ id: child.id,
384
+ name: child.name,
385
+ lastModified: child.lastModified ? new Date(child.lastModified) : null,
386
+ }));
387
+ }
388
+ /** Returns users and groups with edit rights on this section, including inherited rights. */
389
+ async editRights(options) {
390
+ const language = resolveLanguage(options?.language, this.defaultLanguage);
391
+ const section = await this.httpClient.request({
392
+ method: 'GET',
393
+ path: `/hierarchy/${this.sectionId}/${language}`,
394
+ });
395
+ const directUserIds = section.userIDs ?? [];
396
+ const inheritedUserIds = section.inheritedUserIDs ?? [];
397
+ const directGroupIds = section.groupIDs ?? [];
398
+ const inheritedGroupIds = section.inheritedGroupIDs ?? [];
399
+ // Resolve users
400
+ const allUserIds = [
401
+ ...directUserIds.map((id) => ({ id, inherited: false })),
402
+ ...inheritedUserIds.map((id) => ({ id, inherited: true })),
403
+ ];
404
+ const users = await Promise.all(allUserIds.map(async ({ id, inherited }) => {
405
+ try {
406
+ const user = await this.httpClient.request({
407
+ method: 'GET',
408
+ path: `/user/${id}`,
409
+ });
410
+ return { id, username: user.username, firstName: user.firstName, lastName: user.lastName, emailAddress: user.emailAddress, inherited };
411
+ }
412
+ catch (error) {
413
+ debugWarn(`Failed to resolve user ${id} in editRights`, error);
414
+ return { id, username: `User ${id}`, firstName: '', lastName: '', emailAddress: '', inherited };
415
+ }
416
+ }));
417
+ // Resolve groups
418
+ let groupMap;
419
+ try {
420
+ const groups = await this.httpClient.request({
421
+ method: 'GET',
422
+ path: '/group/topLevelGroups',
423
+ });
424
+ groupMap = flattenGroups(groups);
425
+ }
426
+ catch (error) {
427
+ debugWarn('Failed to resolve group names in editRights', error);
428
+ groupMap = new Map();
429
+ }
430
+ const groups = [
431
+ ...directGroupIds.map((id) => ({ id, name: groupMap.get(id) ?? `Group ${id}`, inherited: false })),
432
+ ...inheritedGroupIds.map((id) => ({ id, name: groupMap.get(id) ?? `Group ${id}`, inherited: true })),
433
+ ];
434
+ return { users, groups };
435
+ }
436
+ /** Sets direct (non-inherited) edit rights on this section. At least one of users or groups is required. */
437
+ async setEditRights(data, options) {
438
+ if (data.users === undefined && data.groups === undefined) {
439
+ throw new Error('setEditRights requires at least one of users or groups');
440
+ }
441
+ const language = resolveLanguage(options?.language, this.defaultLanguage);
442
+ const section = await this.httpClient.request({
443
+ method: 'GET',
444
+ path: `/hierarchy/${this.sectionId}/${language}`,
445
+ });
446
+ // Merge new IDs into existing (deduplicated)
447
+ const mergedUsers = data.users
448
+ ? [...new Set([...(section.userIDs ?? []), ...data.users])]
449
+ : (section.userIDs ?? []);
450
+ const mergedGroups = data.groups
451
+ ? [...new Set([...(section.groupIDs ?? []), ...data.groups])]
452
+ : (section.groupIDs ?? []);
453
+ const updated = {
454
+ ...section,
455
+ userIDs: mergedUsers,
456
+ groupIDs: mergedGroups,
457
+ };
458
+ await this.httpClient.request({
459
+ method: 'PUT',
460
+ path: `/hierarchy/${this.sectionId}/${language}`,
461
+ body: updated,
462
+ });
463
+ }
464
+ /** Removes users and/or groups from direct edit rights on this section. */
465
+ async removeEditRights(data, options) {
466
+ if (data.users === undefined && data.groups === undefined) {
467
+ throw new Error('removeEditRights requires at least one of users or groups');
468
+ }
469
+ const language = resolveLanguage(options?.language, this.defaultLanguage);
470
+ const section = await this.httpClient.request({
471
+ method: 'GET',
472
+ path: `/hierarchy/${this.sectionId}/${language}`,
473
+ });
474
+ const removeUsers = new Set(data.users ?? []);
475
+ const removeGroups = new Set(data.groups ?? []);
476
+ const updated = {
477
+ ...section,
478
+ userIDs: (section.userIDs ?? []).filter((id) => !removeUsers.has(id)),
479
+ groupIDs: (section.groupIDs ?? []).filter((id) => !removeGroups.has(id)),
480
+ };
481
+ await this.httpClient.request({
482
+ method: 'PUT',
483
+ path: `/hierarchy/${this.sectionId}/${language}`,
484
+ body: updated,
485
+ });
486
+ }
487
+ /** Lists content types enabled in this section, with name, description, and scope. */
488
+ async contentTypes(options) {
489
+ const language = resolveLanguage(options?.language, this.defaultLanguage);
490
+ const section = await this.httpClient.request({
491
+ method: 'GET',
492
+ path: `/hierarchy/${this.sectionId}/${language}`,
493
+ });
494
+ const scopes = section.contentTypeScopes ?? [];
495
+ if (scopes.length === 0)
496
+ return [];
497
+ // Fetch all content types in one request and build a lookup map
498
+ const allTypes = await this.httpClient.request({
499
+ method: 'GET',
500
+ path: '/contenttype',
501
+ });
502
+ const typeMap = new Map(allTypes.map((t) => [t.id, t]));
503
+ return scopes.map((s) => {
504
+ const ct = typeMap.get(s.id);
505
+ return {
506
+ id: s.id,
507
+ name: ct?.name ?? `Content Type ${s.id}`,
508
+ description: ct?.description ?? '',
509
+ scope: s.scope === 1 ? 'branch' : 'section',
510
+ };
511
+ });
512
+ }
513
+ /** Adds or updates content types on this section. Merges with existing content type scopes. */
514
+ async setContentTypes(types, options) {
515
+ if (!types.length) {
516
+ throw new Error('setContentTypes requires at least one content type');
517
+ }
518
+ const language = resolveLanguage(options?.language, this.defaultLanguage);
519
+ const section = await this.httpClient.request({
520
+ method: 'GET',
521
+ path: `/hierarchy/${this.sectionId}/${language}`,
522
+ });
523
+ const existing = section.contentTypeScopes ?? [];
524
+ // Build a map of existing scopes, then merge in the new ones
525
+ const scopeMap = new Map(existing.map((s) => [s.id, s]));
526
+ for (const t of types) {
527
+ scopeMap.set(t.id, { id: t.id, scope: t.scope === 'branch' ? 1 : 0, inherited: false });
528
+ }
529
+ const updated = {
530
+ ...section,
531
+ contentTypeScopes: Array.from(scopeMap.values()),
532
+ };
533
+ await this.httpClient.request({
534
+ method: 'PUT',
535
+ path: `/hierarchy/${this.sectionId}/${language}`,
536
+ body: updated,
537
+ });
538
+ }
539
+ /** Removes content types from this section by ID. */
540
+ async removeContentTypes(ids, options) {
541
+ if (!ids.length) {
542
+ throw new Error('removeContentTypes requires at least one content type ID');
543
+ }
544
+ const language = resolveLanguage(options?.language, this.defaultLanguage);
545
+ const section = await this.httpClient.request({
546
+ method: 'GET',
547
+ path: `/hierarchy/${this.sectionId}/${language}`,
548
+ });
549
+ const removeSet = new Set(ids);
550
+ const updated = {
551
+ ...section,
552
+ contentTypeScopes: (section.contentTypeScopes ?? []).filter((s) => !removeSet.has(s.id)),
553
+ };
554
+ await this.httpClient.request({
555
+ method: 'PUT',
556
+ path: `/hierarchy/${this.sectionId}/${language}`,
557
+ body: updated,
558
+ });
559
+ }
560
+ /**
561
+ * Creates a child section under this section.
562
+ * Automatically inherits channels, page layouts, and type IDs from this section.
563
+ * If `data.customFields` is provided, creates and saves section metadata content.
564
+ */
565
+ async addSection(data, options) {
566
+ const language = resolveLanguage(options?.language, this.defaultLanguage);
567
+ // Fetch this section's details to inherit config
568
+ const parentSection = await this.httpClient.request({
569
+ method: 'GET',
570
+ path: `/hierarchy/${this.sectionId}/${language}`,
571
+ });
572
+ const accessControlType = parentSection.accessControl?.type ?? 0;
573
+ const metaDataTypeId = parentSection.metaData?.type ?? 0;
574
+ const channels = (parentSection.channels ?? []).map((ch) => ({
575
+ id: ch.id,
576
+ pageLayout: 0,
577
+ inheritedPageLayout: 0,
578
+ }));
579
+ // Step 1: Create the section
580
+ const raw = await this.httpClient.request({
581
+ method: 'POST',
582
+ path: `/hierarchy/${language}`,
583
+ body: {
584
+ parent: String(this.sectionId),
585
+ name: data.name,
586
+ status: String(STATUS_CODES[data.status ?? 'approved'] ?? 0),
587
+ workflow: '0',
588
+ path: '',
589
+ 'output-uri': '',
590
+ 'access-key': '',
591
+ seo: '',
592
+ 'file-name': '',
593
+ show: data.show ?? true,
594
+ eForm: false,
595
+ archive: false,
596
+ printSequence: 0,
597
+ contentSortMethod: 0,
598
+ sectionSortMethod: 0,
599
+ link: false,
600
+ channels,
601
+ userIDs: [],
602
+ inheritedUserIDs: [],
603
+ groupIDs: [],
604
+ inheritedGroupIDs: [],
605
+ viewUserIDs: [],
606
+ viewGroupIDs: [],
607
+ contentTypeScopes: [],
608
+ metaDatas: [],
609
+ excludedMirrorSections: [],
610
+ accessControl: parentSection.accessControl ?? {
611
+ id: 0, type: accessControlType, enabled: false, active: false,
612
+ },
613
+ metaData: parentSection.metaData ?? {
614
+ id: 0, type: metaDataTypeId, enabled: true, active: true,
615
+ },
616
+ pathMembers: [],
617
+ accessControlType,
618
+ metaDataType: metaDataTypeId,
619
+ inheritedPageLayouts: parentSection.inheritedPageLayouts ?? {},
620
+ enableOutputUri: true,
621
+ enableOutputFilename: true,
622
+ enablePathAsOutputUri: true,
623
+ inheritedLinkSection: false,
624
+ accessControlEnabled: true,
625
+ accessControlInherited: false,
626
+ },
627
+ });
628
+ const newSectionId = raw.id;
629
+ // Steps 2 & 3: Create the section metadata content instance.
630
+ // Every section gets a metadata content item when the instance has a Section
631
+ // Meta Data content type configured, matching T4's own section-create flow.
632
+ // The item's Name element is set to the section name; other elements are
633
+ // blank unless customFields are provided.
634
+ //
635
+ // Prefer the parent's metaData.type, but fall back to the instance-level
636
+ // configuration so a child still gets metadata content when the parent has
637
+ // none but the instance does.
638
+ let customFields = null;
639
+ const hasCustomFields = data.customFields && Object.keys(data.customFields).length > 0;
640
+ const metaContentTypeId = await resolveMetaDataTypeId(this.httpClient, metaDataTypeId);
641
+ if (metaContentTypeId) {
642
+ // Use a ContentResource on the new section to get full element resolution
643
+ // (list values, SS links, file uploads, etc.)
644
+ const metadataContentResource = new ContentResource(this.httpClient, newSectionId, this.defaultLanguage, this.mediaCreateFn);
645
+ // Create the metadata content item (ContentResource.create handles the full body)
646
+ const metaItem = await metadataContentResource.create({
647
+ type: metaContentTypeId,
648
+ name: data.name,
649
+ fields: data.customFields ?? {},
650
+ status: 'pending',
651
+ });
652
+ customFields = stripNameField(metaItem.fields ?? null);
653
+ }
654
+ else if (hasCustomFields) {
655
+ throw new Error('Cannot set customFields: no Section Meta Data content type is configured on this T4 instance');
656
+ }
657
+ // Return the created child as a mutable SectionItem
658
+ const childRaw = await this.httpClient.request({
659
+ method: 'GET',
660
+ path: `/hierarchy/${newSectionId}/${language}`,
661
+ });
662
+ return new SectionItem(childRaw, this.httpClient, language, customFields, this.mediaCreateFn);
663
+ }
664
+ /** Deletes (deactivates) this section by setting its status to inactive. */
665
+ async delete(options) {
666
+ await this.update({ status: 'inactive' }, options);
667
+ }
668
+ /**
669
+ * Permanently removes this section. The section must be inactive first.
670
+ * Call delete() before purge() if the section is still active.
671
+ */
672
+ async purge(options) {
673
+ const language = resolveLanguage(options?.language, this.defaultLanguage);
674
+ // Check the section is inactive before purging
675
+ const section = await this.httpClient.request({
676
+ method: 'GET',
677
+ path: `/hierarchy/${this.sectionId}/${language}`,
678
+ });
679
+ const status = Number(section.status);
680
+ if (status !== 2) {
681
+ const statusName = { 0: 'approved', 1: 'pending' }[status] ?? `unknown (${status})`;
682
+ throw new Error(`Cannot purge section ${this.sectionId} — it is currently "${statusName}". ` +
683
+ `Only inactive sections can be purged. Call delete() first to deactivate it.`);
684
+ }
685
+ await this.httpClient.request({
686
+ method: 'POST',
687
+ path: '/hierarchy/purge',
688
+ body: {
689
+ languageCode: language,
690
+ contentIds: [String(this.sectionId)],
691
+ },
692
+ });
693
+ }
694
+ /**
695
+ * Moves this section under a new parent section.
696
+ */
697
+ async move(newParentId) {
698
+ await this.httpClient.request({
699
+ method: 'MOVE',
700
+ path: `/hierarchy/${this.sectionId}/${newParentId}`,
701
+ body: {},
702
+ });
703
+ }
704
+ /**
705
+ * Updates section properties. Fetches the current section, merges your
706
+ * changes, and PUTs the full body back. Only pass the fields you want to change.
707
+ * Returns the updated SectionItem.
708
+ */
709
+ async update(data, options) {
710
+ const language = resolveLanguage(options?.language, this.defaultLanguage);
711
+ const section = await this.httpClient.request({
712
+ method: 'GET',
713
+ path: `/hierarchy/${this.sectionId}/${language}`,
714
+ });
715
+ const updated = {
716
+ ...section,
717
+ ...(data.name !== undefined && { name: data.name }),
718
+ ...(data.show !== undefined && { show: data.show }),
719
+ ...(data.status !== undefined && { status: String(STATUS_CODES[data.status] ?? section.status) }),
720
+ };
721
+ await this.httpClient.request({
722
+ method: 'PUT',
723
+ path: `/hierarchy/${this.sectionId}/${language}`,
724
+ body: updated,
725
+ });
726
+ // Update custom fields if provided
727
+ if (data.customFields !== undefined) {
728
+ const meta = section.metaData;
729
+ const metaDataTypeId = await resolveMetaDataTypeId(this.httpClient, meta?.type ?? 0);
730
+ if (!metaDataTypeId) {
731
+ throw new Error('Cannot update customFields: no Section Meta Data content type is configured on this T4 instance');
732
+ }
733
+ const contentResource = new ContentResource(this.httpClient, this.sectionId, this.defaultLanguage, this.mediaCreateFn);
734
+ const metaContentId = meta?.id ?? 0;
735
+ if (metaContentId > 0) {
736
+ await contentResource.update(metaContentId, { fields: data.customFields });
737
+ }
738
+ else {
739
+ // Create the metadata content. The Name element mirrors the section name.
740
+ await contentResource.create({
741
+ type: metaDataTypeId,
742
+ name: section.name,
743
+ fields: data.customFields,
744
+ status: 'pending',
745
+ });
746
+ }
747
+ }
748
+ return this.get(options);
749
+ }
750
+ // ── Publishing ──
751
+ /**
752
+ * Publishes this section (or its entire branch) to a channel.
753
+ *
754
+ * If the section belongs to a single channel, `channelId` is optional — the SDK
755
+ * auto-selects it. If the section belongs to multiple channels, `channelId` is
756
+ * required and the error message lists the valid options.
757
+ *
758
+ * Pass `{ branch: true }` to publish the entire branch below this section.
759
+ */
760
+ async publish(options) {
761
+ const language = resolveLanguage(options?.language, this.defaultLanguage);
762
+ const branch = options?.branch ?? false;
763
+ // Fetch publishable channels for this section
764
+ const publishableChannels = await this.httpClient.request({
765
+ method: 'GET',
766
+ path: `/channel/publishables/${this.sectionId}/publish`,
767
+ });
768
+ if (publishableChannels.length === 0) {
769
+ throw new Error(`Cannot publish section ${this.sectionId} — it has no publishable channels.`);
770
+ }
771
+ let channelId;
772
+ if (options?.channelId !== undefined) {
773
+ // Validate the provided channel ID
774
+ const valid = publishableChannels.find((ch) => ch.id === options.channelId);
775
+ if (!valid) {
776
+ const validList = publishableChannels.map((ch) => `${ch.name} (${ch.id})`).join(', ');
777
+ throw new Error(`Channel ${options.channelId} is not a publishable channel for section ${this.sectionId}. ` +
778
+ `Valid channels: ${validList}`);
779
+ }
780
+ channelId = options.channelId;
781
+ }
782
+ else if (publishableChannels.length === 1) {
783
+ // Auto-select the only channel
784
+ channelId = publishableChannels[0].id;
785
+ }
786
+ else {
787
+ const validList = publishableChannels.map((ch) => `${ch.name} (${ch.id})`).join(', ');
788
+ throw new Error(`Section ${this.sectionId} belongs to multiple channels. ` +
789
+ `Pass a channelId to specify which channel to publish. ` +
790
+ `Available channels: ${validList}`);
791
+ }
792
+ await this.httpClient.request({
793
+ method: 'POST',
794
+ path: '/task/repository',
795
+ body: {
796
+ taskType: 'channelPublish',
797
+ channel: channelId,
798
+ sections: [this.sectionId],
799
+ branch,
800
+ publishCompleteChannel: false,
801
+ publishOptions: { publishArchiveSections: true },
802
+ taskLevel: branch ? 'branch' : 'section',
803
+ selectedLanguage: language,
804
+ },
805
+ });
806
+ }
807
+ }
808
+ //# sourceMappingURL=section-ref.js.map