@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,970 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ContentTypeResource = exports.ContentType = exports.Layout = void 0;
4
+ const utils_js_1 = require("../utils.js");
5
+ /** The content type category ID that marks a system content type. */
6
+ const SYSTEM_CONTENT_TYPE = 30;
7
+ function resolveTypeName(type, typeMap) {
8
+ if (type == null)
9
+ return '';
10
+ if (typeof type === 'number')
11
+ return typeMap.get(type) ?? `Type ${type}`;
12
+ if (typeof type === 'object' && type !== null && 'name' in type)
13
+ return type.name ?? '';
14
+ return '';
15
+ }
16
+ /**
17
+ * Maps a raw API content type response to a ContentTypeData object.
18
+ */
19
+ function mapContentType(raw, typeMap, editorMap) {
20
+ const fieldsRecord = {};
21
+ for (const el of raw.contentTypeElements ?? []) {
22
+ const friendlyName = (0, utils_js_1.decodeHtmlEntities)(el.alias || el.name);
23
+ const field = {
24
+ name: friendlyName,
25
+ description: (0, utils_js_1.decodeHtmlEntities)(el.description ?? ''),
26
+ type: resolveTypeName(el.type, typeMap),
27
+ required: el.compulsory ?? false,
28
+ maxSize: el.maxSize ?? 0,
29
+ listId: el.listId ?? 0,
30
+ listName: '',
31
+ shown: el.shown ?? true,
32
+ useAsFilename: raw.elementIdforFilename !== undefined && el.id === raw.elementIdforFilename,
33
+ };
34
+ if (el.contentTypeElementConfiguration) {
35
+ const cfg = el.contentTypeElementConfiguration;
36
+ const elTypeName = resolveTypeName(el.type, typeMap);
37
+ if (elTypeName === 'Repeater') {
38
+ field.config = {
39
+ contentTypeId: cfg.contentTypeId ?? 0,
40
+ contentTypeName: cfg.contentTypeDTO?.alias || cfg.contentTypeDTO?.name || '',
41
+ contentTypeDescription: cfg.contentTypeDTO?.description ?? '',
42
+ layout: cfg.layout ?? '',
43
+ minRepeats: cfg.minRepeats ?? 0,
44
+ maxRepeats: cfg.maxRepeats ?? 0,
45
+ };
46
+ }
47
+ else if (elTypeName === 'HTML' && cfg.editorID != null && editorMap) {
48
+ field.editor = editorMap.get(cfg.editorID) ?? `Editor ${cfg.editorID}`;
49
+ }
50
+ }
51
+ // Store element ID as non-enumerable for internal matching on save
52
+ Object.defineProperty(field, '_elementId', { value: el.id, enumerable: false });
53
+ fieldsRecord[friendlyName] = field;
54
+ }
55
+ return {
56
+ data: {
57
+ id: raw.id,
58
+ name: (0, utils_js_1.decodeHtmlEntities)(raw.alias || raw.name),
59
+ description: (0, utils_js_1.decodeHtmlEntities)(raw.description ?? ''),
60
+ minUserLevel: utils_js_1.AUTH_LEVEL_MAP[raw.minAuthLevel ?? 2] ?? `unknown (${raw.minAuthLevel})`,
61
+ workflow: raw.workflow ?? 0,
62
+ sharedGroups: (raw.sharedGroups ?? []).map((g) => g.id),
63
+ primaryGroup: raw.primaryGroup?.group?.id ?? raw.primaryGroup?.id ?? 0,
64
+ directEdit: raw.enableDirectEdit ?? true,
65
+ fields: fieldsRecord,
66
+ },
67
+ rawData: raw,
68
+ };
69
+ }
70
+ /** Friendly processor names mapped to API names */
71
+ const PROCESSOR_MAP = {
72
+ 't4-tags': 'T4 Tag Content',
73
+ 'handlebars': 'Handlebars Content',
74
+ 'programmable-layouts': 'Programmable Layout Content',
75
+ };
76
+ /** Extracts a value from raw elements by element name prefix (case-insensitive). */
77
+ function getElementValue(elements, name) {
78
+ const key = Object.keys(elements).find((k) => k.toLowerCase().startsWith(name.toLowerCase() + '#'));
79
+ return key ? String(elements[key] ?? '') : '';
80
+ }
81
+ /** A mutable content layout. Modify properties and call save() to persist. */
82
+ class Layout {
83
+ constructor(raw, httpClient, _contentTypeId, layoutElements, fetchLayouts, _resolveSyntaxId, _resolveProcessorId, _resolveExtension) {
84
+ this.name = raw.name;
85
+ this.code = getElementValue(raw.elements, 'formatcode');
86
+ this.lastModified = raw.lastModified ? new Date(raw.lastModified) : null;
87
+ Object.defineProperty(this, '_httpClient', { value: httpClient, enumerable: false });
88
+ Object.defineProperty(this, '_rawDTO', { value: raw, enumerable: false, writable: true });
89
+ Object.defineProperty(this, '_layoutElements', { value: layoutElements, enumerable: false });
90
+ Object.defineProperty(this, '_fetchLayouts', { value: fetchLayouts, enumerable: false });
91
+ }
92
+ /** Persists current property values to the server. */
93
+ async save() {
94
+ // If name changed, check uniqueness
95
+ if (this.name !== this._rawDTO.name) {
96
+ const existing = await this._fetchLayouts();
97
+ if (existing.some((l) => l.name === this.name && l.id !== this._rawDTO.id)) {
98
+ throw new Error(`Layout "${this.name}" already exists on this content type`);
99
+ }
100
+ }
101
+ const findEl = (n) => {
102
+ const el = this._layoutElements.find((e) => e.name.toLowerCase() === n.toLowerCase());
103
+ if (!el)
104
+ throw new Error(`Layout element "${n}" not found`);
105
+ return el;
106
+ };
107
+ const nameEl = findEl('name');
108
+ const codeEl = findEl('formatcode');
109
+ // Start from raw elements, overlay changed fields
110
+ const elements = { ...this._rawDTO.elements };
111
+ elements[layoutKey(nameEl)] = this.name;
112
+ elements[layoutKey(codeEl)] = this.code;
113
+ const response = await this._httpClient.request({
114
+ method: 'PUT',
115
+ path: `/layout/${this._rawDTO.id}/en`,
116
+ body: {
117
+ ...this._rawDTO,
118
+ name: this.name,
119
+ elements,
120
+ },
121
+ });
122
+ this._rawDTO = response;
123
+ this.name = response.name;
124
+ this.code = getElementValue(response.elements, 'formatcode');
125
+ this.lastModified = response.lastModified ? new Date(response.lastModified) : null;
126
+ }
127
+ }
128
+ exports.Layout = Layout;
129
+ /** Builds an element key from a layout element: Name#id:type */
130
+ function layoutKey(el) {
131
+ return `${el.name}#${el.id}:${el.type}`;
132
+ }
133
+ /** A content type with methods for related operations. */
134
+ class ContentType {
135
+ constructor(data, httpClient, rawData) {
136
+ this.id = data.id;
137
+ this.name = data.name;
138
+ this.description = data.description;
139
+ this.minUserLevel = data.minUserLevel;
140
+ this.workflow = data.workflow;
141
+ this.sharedGroups = data.sharedGroups;
142
+ this.primaryGroup = data.primaryGroup;
143
+ this.directEdit = data.directEdit;
144
+ this.fields = data.fields;
145
+ Object.defineProperty(this, '_httpClient', { value: httpClient, enumerable: false });
146
+ Object.defineProperty(this, '_rawData', { value: rawData ?? {}, enumerable: false, writable: true });
147
+ Object.defineProperty(this, '_removedElementIds', { value: new Set(), enumerable: false, writable: true });
148
+ Object.defineProperty(this, '_exemptSystemTypeIdsCache', { value: null, enumerable: false, writable: true });
149
+ const contentTypeId = this.id;
150
+ let layoutElementsCache = null;
151
+ let layoutContentTypeIdCache = null;
152
+ const fetchLayouts = async () => {
153
+ return httpClient.request({
154
+ method: 'GET',
155
+ path: `/layout/contenttype/${contentTypeId}/en`,
156
+ });
157
+ };
158
+ /** Resolves the layout content type ID by looking up "Content Layout" (type 30). */
159
+ const getLayoutContentTypeId = async () => {
160
+ if (layoutContentTypeIdCache !== null)
161
+ return layoutContentTypeIdCache;
162
+ const types = await httpClient.request({
163
+ method: 'GET',
164
+ path: '/contenttype/?excludeElements=true',
165
+ });
166
+ const match = types.find((t) => (t.name === 'Content Layout' || t.alias === 'Content Layout') && t.type === 30);
167
+ if (!match)
168
+ throw new Error('Could not find "Content Layout" system content type');
169
+ layoutContentTypeIdCache = match.id;
170
+ return match.id;
171
+ };
172
+ /** Fetches and caches the layout content type elements. */
173
+ const getLayoutElements = async () => {
174
+ if (layoutElementsCache)
175
+ return layoutElementsCache;
176
+ const ctId = await getLayoutContentTypeId();
177
+ const raw = await httpClient.request({
178
+ method: 'GET',
179
+ path: `/contenttype/${ctId}`,
180
+ });
181
+ layoutElementsCache = raw.contentTypeElements ?? [];
182
+ return layoutElementsCache;
183
+ };
184
+ /** Finds a layout element by name (case-insensitive). */
185
+ const findElement = (elements, name) => {
186
+ const el = elements.find((e) => e.name.toLowerCase() === name.toLowerCase());
187
+ if (!el)
188
+ throw new Error(`Layout element "${name}" not found in layout content type`);
189
+ return el;
190
+ };
191
+ const resolveSyntaxId = async (syntaxName) => {
192
+ const types = await httpClient.request({
193
+ method: 'GET',
194
+ path: '/syntaxType',
195
+ });
196
+ const match = types.find((t) => t.name.toLowerCase() === syntaxName.toLowerCase());
197
+ if (!match) {
198
+ const valid = types.map((t) => t.name).join(', ');
199
+ throw new Error(`Unknown syntax "${syntaxName}". Valid options: ${valid}`);
200
+ }
201
+ return String(match.id);
202
+ };
203
+ const resolveProcessorId = async (processorKey) => {
204
+ const apiName = PROCESSOR_MAP[processorKey];
205
+ if (!apiName) {
206
+ throw new Error(`Unknown processor "${processorKey}". Valid options: t4-tags, handlebars, programmable-layouts`);
207
+ }
208
+ const processors = await httpClient.request({
209
+ method: 'GET',
210
+ path: '/publishProcessor/20',
211
+ });
212
+ const match = processors.find((p) => p.name === apiName);
213
+ if (!match)
214
+ throw new Error(`Processor "${apiName}" not found on this T4 instance`);
215
+ return String(match.id);
216
+ };
217
+ const resolveExtension = async (ext) => {
218
+ const extensions = await httpClient.request({
219
+ method: 'GET',
220
+ path: '/fileExtension',
221
+ });
222
+ const match = extensions.find((e) => e.extension.toLowerCase() === ext.toLowerCase());
223
+ if (!match) {
224
+ const valid = extensions.map((e) => e.extension).join(', ');
225
+ throw new Error(`Unknown extension "${ext}". Valid options: ${valid}`);
226
+ }
227
+ return match.extension;
228
+ };
229
+ Object.defineProperty(this, 'layouts', {
230
+ value: {
231
+ list: async () => {
232
+ const raw = await fetchLayouts();
233
+ return raw.map((l) => ({
234
+ name: l.name,
235
+ lastModified: l.lastModified ? new Date(l.lastModified) : null,
236
+ }));
237
+ },
238
+ get: async (layoutName) => {
239
+ const all = await fetchLayouts();
240
+ const match = all.find((l) => l.name === layoutName);
241
+ if (!match)
242
+ throw new Error(`Layout "${layoutName}" not found on this content type`);
243
+ // Fetch full layout details
244
+ const raw = await httpClient.request({
245
+ method: 'GET',
246
+ path: `/layout/${match.id}/en`,
247
+ });
248
+ const layoutElements = await getLayoutElements();
249
+ return new Layout(raw, httpClient, contentTypeId, layoutElements, fetchLayouts, resolveSyntaxId, resolveProcessorId, resolveExtension);
250
+ },
251
+ update: async (layoutName, updateData) => {
252
+ const all = await fetchLayouts();
253
+ const match = all.find((l) => l.name === layoutName);
254
+ if (!match)
255
+ throw new Error(`Layout "${layoutName}" not found on this content type`);
256
+ // If renaming, check uniqueness
257
+ if (updateData.name && updateData.name !== layoutName) {
258
+ if (all.some((l) => l.name === updateData.name)) {
259
+ throw new Error(`Layout "${updateData.name}" already exists on this content type`);
260
+ }
261
+ }
262
+ // Fetch full layout
263
+ const raw = await httpClient.request({
264
+ method: 'GET',
265
+ path: `/layout/${match.id}/en`,
266
+ });
267
+ const layoutElements = await getLayoutElements();
268
+ const elements = { ...raw.elements };
269
+ const newName = updateData.name ?? raw.name;
270
+ elements[layoutKey(findElement(layoutElements, 'name'))] = newName;
271
+ if (updateData.code !== undefined) {
272
+ elements[layoutKey(findElement(layoutElements, 'formatcode'))] = updateData.code;
273
+ }
274
+ if (updateData.syntax !== undefined) {
275
+ elements[layoutKey(findElement(layoutElements, 'syntax'))] = await resolveSyntaxId(updateData.syntax);
276
+ }
277
+ if (updateData.processor !== undefined) {
278
+ elements[layoutKey(findElement(layoutElements, 'Format Processor'))] = await resolveProcessorId(updateData.processor);
279
+ }
280
+ if (updateData.extension !== undefined) {
281
+ const extValue = updateData.extension ? await resolveExtension(updateData.extension) : '';
282
+ elements[layoutKey(findElement(layoutElements, 'extension'))] = extValue;
283
+ }
284
+ const response = await httpClient.request({
285
+ method: 'PUT',
286
+ path: `/layout/${match.id}/en`,
287
+ body: { ...raw, name: newName, elements },
288
+ });
289
+ return new Layout(response, httpClient, contentTypeId, layoutElements, fetchLayouts, resolveSyntaxId, resolveProcessorId, resolveExtension);
290
+ },
291
+ create: async (createData) => {
292
+ // Check name uniqueness
293
+ const existing = await fetchLayouts();
294
+ if (existing.some((l) => l.name === createData.name)) {
295
+ throw new Error(`Layout "${createData.name}" already exists on this content type`);
296
+ }
297
+ // Fetch layout content type elements for dynamic key resolution
298
+ const layoutElements = await getLayoutElements();
299
+ const layoutCtId = await getLayoutContentTypeId();
300
+ const nameEl = findElement(layoutElements, 'name');
301
+ const codeEl = findElement(layoutElements, 'formatcode');
302
+ const extensionEl = findElement(layoutElements, 'extension');
303
+ const syntaxEl = findElement(layoutElements, 'syntax');
304
+ const processorEl = findElement(layoutElements, 'Format Processor');
305
+ const [syntaxId, processorId] = await Promise.all([
306
+ resolveSyntaxId(createData.syntax ?? 'HTML/XML'),
307
+ resolveProcessorId(createData.processor ?? 'handlebars'),
308
+ ]);
309
+ const extensionValue = createData.extension
310
+ ? await resolveExtension(createData.extension)
311
+ : '';
312
+ const response = await httpClient.request({
313
+ method: 'PUT',
314
+ path: `/layout/${contentTypeId}`,
315
+ body: {
316
+ id: 0,
317
+ contentTypeID: layoutCtId,
318
+ archiveSection: 0,
319
+ name: createData.name,
320
+ status: 0,
321
+ lastModifiedBy: 0,
322
+ channels: [],
323
+ editable: false,
324
+ expired: false,
325
+ canPublishNow: false,
326
+ canSaveAndApprove: false,
327
+ contentTypeAccess: 0,
328
+ elements: {
329
+ [layoutKey(nameEl)]: createData.name,
330
+ [layoutKey(extensionEl)]: extensionValue,
331
+ [layoutKey(syntaxEl)]: syntaxId,
332
+ [layoutKey(codeEl)]: createData.code,
333
+ [layoutKey(processorEl)]: processorId,
334
+ },
335
+ sectionIDs: [],
336
+ },
337
+ });
338
+ return new Layout(response, httpClient, contentTypeId, layoutElements, fetchLayouts, resolveSyntaxId, resolveProcessorId, resolveExtension);
339
+ },
340
+ delete: async (layoutName) => {
341
+ const all = await fetchLayouts();
342
+ const match = all.find((l) => l.name === layoutName);
343
+ if (!match)
344
+ throw new Error(`Layout "${layoutName}" not found on this content type`);
345
+ await httpClient.request({
346
+ method: 'DELETE',
347
+ path: `/layout/${match.id}/en`,
348
+ });
349
+ },
350
+ },
351
+ enumerable: false,
352
+ configurable: false,
353
+ });
354
+ }
355
+ /**
356
+ * Adds a new field (element) to this content type. Takes effect on save().
357
+ *
358
+ * The `type` must be a valid element type name (e.g. 'Plain Text', 'HTML', 'Select Box', 'Repeater').
359
+ * Use `t4.contentTypes.get(id)` to see existing field types for reference.
360
+ */
361
+ async addField(data) {
362
+ if (!data.name?.trim())
363
+ throw new Error('Field name is required');
364
+ if (!data.type?.trim())
365
+ throw new Error('Field type is required');
366
+ if (this.fields[data.name])
367
+ throw new Error(`Field "${data.name}" already exists on this content type`);
368
+ // Resolve type name to ID via GET /type/
369
+ const types = await this._httpClient.request({
370
+ method: 'GET',
371
+ path: '/type/',
372
+ });
373
+ const typeMatch = types.find((t) => t.name.toLowerCase() === data.type.toLowerCase());
374
+ if (!typeMatch) {
375
+ const valid = types.map((t) => t.name).join(', ');
376
+ throw new Error(`Unknown element type "${data.type}". Valid options: ${valid}`);
377
+ }
378
+ // Validate listId for list-based types
379
+ if (typeMatch.listType && !data.listId) {
380
+ throw new Error(`Field "${data.name}" is type "${data.type}" which requires a listId`);
381
+ }
382
+ // Validate repeater config
383
+ const isRepeater = typeMatch.name.toLowerCase() === 'repeater';
384
+ if (isRepeater && !data.repeater) {
385
+ throw new Error(`Field "${data.name}" is type "Repeater" which requires a repeater configuration (contentTypeId)`);
386
+ }
387
+ if (!isRepeater && data.repeater) {
388
+ throw new Error(`Field "${data.name}" is type "${data.type}" — repeater configuration is only valid for Repeater elements`);
389
+ }
390
+ // Validate editor option (only valid for HTML elements)
391
+ const isHtml = typeMatch.name === 'HTML';
392
+ let resolvedEditorId;
393
+ if (data.editor !== undefined) {
394
+ if (!isHtml) {
395
+ throw new Error(`Field "${data.name}" is type "${data.type}" — editor is only valid for HTML elements`);
396
+ }
397
+ const editors = await this._httpClient.request({
398
+ method: 'GET',
399
+ path: '/htmlEditor',
400
+ });
401
+ const match = editors.find((e) => e.name.toLowerCase() === data.editor.toLowerCase());
402
+ if (!match) {
403
+ const valid = editors.map((e) => `"${e.name}"`).join(', ');
404
+ throw new Error(`Unknown editor "${data.editor}". Valid options: ${valid}`);
405
+ }
406
+ resolvedEditorId = match.id;
407
+ }
408
+ // Generate a negative random ID for the new element (API convention for new items)
409
+ const newElementId = -Math.floor(Math.random() * 1000000);
410
+ // Determine the next sequence number
411
+ const existingElements = this._rawData.contentTypeElements ?? [];
412
+ const maxSequence = existingElements.reduce((max, el) => {
413
+ const seq = el.sequence;
414
+ return seq !== undefined && seq > max ? seq : max;
415
+ }, 0);
416
+ // Add to the raw elements array so save() picks it up.
417
+ // New elements use string type IDs and include contentTypeID (matching browser behaviour).
418
+ const newRawElement = {
419
+ id: newElementId,
420
+ name: data.name,
421
+ description: data.description ?? '',
422
+ type: String(typeMatch.id),
423
+ maxSize: data.maxSize ?? 80,
424
+ compulsory: data.required ?? false,
425
+ shown: data.shown ?? true,
426
+ contentTypeID: this.id,
427
+ sequence: maxSequence + 1,
428
+ };
429
+ // Add repeater configuration if applicable
430
+ if (isRepeater && data.repeater) {
431
+ const maxRepeats = data.repeater.maxRepeats ?? 100;
432
+ newRawElement.contentTypeElementConfiguration = {
433
+ contentTypeId: data.repeater.contentTypeId,
434
+ layout: data.repeater.layout ?? '',
435
+ minRepeats: data.repeater.minRepeats ?? 0,
436
+ maxRepeats: maxRepeats,
437
+ maxRepeatsLimit: maxRepeats,
438
+ };
439
+ }
440
+ else {
441
+ newRawElement.listId = data.listId ?? 0;
442
+ }
443
+ // Add HTML editor configuration if applicable
444
+ if (isHtml && resolvedEditorId !== undefined) {
445
+ newRawElement.contentTypeElementConfiguration = { editorID: resolvedEditorId };
446
+ }
447
+ existingElements.push(newRawElement);
448
+ // Add to the friendly fields record
449
+ const field = {
450
+ name: data.name,
451
+ description: data.description ?? '',
452
+ type: typeMatch.name,
453
+ required: data.required ?? false,
454
+ maxSize: data.maxSize ?? 80,
455
+ listId: data.listId ?? 0,
456
+ listName: '',
457
+ shown: data.shown ?? true,
458
+ useAsFilename: false,
459
+ };
460
+ if (isRepeater && data.repeater) {
461
+ field.config = {
462
+ contentTypeId: data.repeater.contentTypeId,
463
+ contentTypeName: '',
464
+ contentTypeDescription: '',
465
+ layout: data.repeater.layout ?? '',
466
+ minRepeats: data.repeater.minRepeats ?? 0,
467
+ maxRepeats: data.repeater.maxRepeats ?? 100,
468
+ };
469
+ }
470
+ if (isHtml && data.editor) {
471
+ field.editor = data.editor;
472
+ }
473
+ Object.defineProperty(field, '_elementId', { value: newElementId, enumerable: false });
474
+ this.fields[data.name] = field;
475
+ }
476
+ /** Removes a field (element) by name. Takes effect on save(). */
477
+ removeField(fieldName) {
478
+ const field = this.fields[fieldName];
479
+ if (!field)
480
+ throw new Error(`Field "${fieldName}" not found on this content type`);
481
+ const elementId = field._elementId;
482
+ if (elementId !== undefined) {
483
+ this._removedElementIds.add(elementId);
484
+ }
485
+ delete this.fields[fieldName];
486
+ }
487
+ /**
488
+ * Resolves the set of system content type IDs where removing and renaming
489
+ * elements is permitted: the Section Meta Data content type and the Extended
490
+ * User content type. Cached per instance; only called when a removal or rename
491
+ * is attempted on a system content type.
492
+ */
493
+ async _getExemptSystemTypeIds() {
494
+ if (this._exemptSystemTypeIdsCache)
495
+ return this._exemptSystemTypeIdsCache;
496
+ const ids = new Set();
497
+ // Section Meta Data content type
498
+ try {
499
+ const config = await this._httpClient.request({
500
+ method: 'GET',
501
+ path: '/config/hierarchy.metaDataContentType',
502
+ });
503
+ const sectionMetaId = parseInt(config.value, 10);
504
+ if (sectionMetaId > 0)
505
+ ids.add(sectionMetaId);
506
+ }
507
+ catch (error) {
508
+ (0, utils_js_1.debugWarn)('Failed to resolve Section Meta Data content type ID', error);
509
+ }
510
+ // Extended User content type
511
+ try {
512
+ const credentials = await this._httpClient.request({
513
+ method: 'GET',
514
+ path: '/userSearch/credentials',
515
+ });
516
+ const userExtId = credentials.userExtensibleObjectID;
517
+ if (typeof userExtId === 'number' && userExtId > 0)
518
+ ids.add(userExtId);
519
+ }
520
+ catch (error) {
521
+ (0, utils_js_1.debugWarn)('Failed to resolve Extended User content type ID', error);
522
+ }
523
+ this._exemptSystemTypeIdsCache = ids;
524
+ return ids;
525
+ }
526
+ /**
527
+ * Guards against removing or renaming elements on system content types.
528
+ *
529
+ * System content types (`type === 30`) may have elements added, and their
530
+ * descriptions and maxSize changed, but removing or renaming existing elements
531
+ * is unsafe. Two system content types are exempt: the Section Meta Data content
532
+ * type and the Extended User content type.
533
+ *
534
+ * Detects renames by comparing each field's current name against the name/alias
535
+ * of its matched raw element. Only performs network calls when a removal or
536
+ * rename is actually attempted on a system content type.
537
+ */
538
+ async _guardSystemContentTypeElementChanges(rawElements) {
539
+ if (this._rawData.type !== SYSTEM_CONTENT_TYPE)
540
+ return;
541
+ const removedNames = this._collectRemovedElementNames();
542
+ const renames = this._collectRenamedElements(rawElements);
543
+ if (removedNames.length === 0 && renames.length === 0)
544
+ return;
545
+ const exemptIds = await this._getExemptSystemTypeIds();
546
+ if (exemptIds.has(this.id))
547
+ return;
548
+ if (removedNames.length > 0) {
549
+ throw new Error(`Cannot remove element${removedNames.length > 1 ? 's' : ''} ${removedNames.map((n) => `"${n}"`).join(', ')} ` +
550
+ `from content type "${this.name}" because it is a system content type. ` +
551
+ 'Removing elements from system content types is not allowed. ' +
552
+ 'You can still add elements and change their description or maxSize.');
553
+ }
554
+ const { from, to } = renames[0];
555
+ throw new Error(`Cannot rename element "${from}" to "${to}" on content type "${this.name}" because it is a system content type. ` +
556
+ 'Renaming elements on system content types is not allowed. ' +
557
+ 'You can still add elements and change their description or maxSize.');
558
+ }
559
+ /** Returns the names of elements queued for removal, resolved from the raw data. */
560
+ _collectRemovedElementNames() {
561
+ if (this._removedElementIds.size === 0)
562
+ return [];
563
+ const names = [];
564
+ for (const el of this._rawData.contentTypeElements ?? []) {
565
+ if (el.id !== undefined && this._removedElementIds.has(el.id)) {
566
+ names.push((0, utils_js_1.decodeHtmlEntities)(el.alias || el.name));
567
+ }
568
+ }
569
+ return names;
570
+ }
571
+ /**
572
+ * Detects fields whose name differs from the current name/alias of their matched
573
+ * raw element (i.e. a rename). New fields (no matching raw element) are ignored.
574
+ */
575
+ _collectRenamedElements(rawElements) {
576
+ const renames = [];
577
+ for (const field of Object.values(this.fields)) {
578
+ const elementId = field._elementId;
579
+ if (elementId === undefined)
580
+ continue;
581
+ const rawEl = rawElements.find((el) => el.id === elementId);
582
+ if (!rawEl)
583
+ continue;
584
+ const currentName = (0, utils_js_1.decodeHtmlEntities)(rawEl.alias || rawEl.name);
585
+ if (currentName !== field.name) {
586
+ renames.push({ from: currentName, to: field.name });
587
+ }
588
+ }
589
+ return renames;
590
+ }
591
+ /** Persists current property values to the server via PUT. */
592
+ async save() {
593
+ const authLevel = String(utils_js_1.AUTH_LEVEL_REVERSE[this.minUserLevel] ?? this._rawData.minAuthLevel ?? 2);
594
+ // Sync field changes back to raw contentTypeElements
595
+ let rawElements = (this._rawData.contentTypeElements ?? []);
596
+ // Block element removal/renaming on system content types (except the two
597
+ // exempt types). Runs before any mutation so nothing is persisted on failure.
598
+ await this._guardSystemContentTypeElementChanges(rawElements);
599
+ // Remove elements that were deleted via removeField()
600
+ if (this._removedElementIds.size > 0) {
601
+ rawElements = rawElements.filter((el) => !this._removedElementIds.has(el.id));
602
+ }
603
+ const editorFieldsToSync = [];
604
+ for (const field of Object.values(this.fields)) {
605
+ const elementId = field._elementId;
606
+ const rawEl = elementId !== undefined
607
+ ? rawElements.find((el) => el.id === elementId)
608
+ : rawElements.find((el) => {
609
+ const elAlias = el.alias;
610
+ const elName = el.name;
611
+ return elAlias === field.name || elName === field.name;
612
+ });
613
+ if (rawEl) {
614
+ rawEl.alias = field.name;
615
+ rawEl.description = field.description;
616
+ rawEl.maxSize = field.maxSize;
617
+ rawEl.compulsory = field.required;
618
+ rawEl.shown = field.shown;
619
+ // Sync repeater config min/max if present (only for Repeater elements)
620
+ if (field.config && rawEl.contentTypeElementConfiguration && field.type === 'Repeater') {
621
+ const cfg = rawEl.contentTypeElementConfiguration;
622
+ cfg.minRepeats = field.config.minRepeats;
623
+ cfg.maxRepeats = field.config.maxRepeats;
624
+ }
625
+ // Sync HTML editor if changed
626
+ if (field.type === 'HTML' && field.editor !== undefined) {
627
+ if (field.editor === null) {
628
+ // Explicitly remove the editor configuration
629
+ delete rawEl.contentTypeElementConfiguration;
630
+ }
631
+ else {
632
+ editorFieldsToSync.push({ field, rawEl });
633
+ }
634
+ }
635
+ }
636
+ }
637
+ // Resolve HTML editor names to IDs for any fields that need syncing
638
+ if (editorFieldsToSync.length > 0) {
639
+ const editors = await this._httpClient.request({
640
+ method: 'GET',
641
+ path: '/htmlEditor',
642
+ });
643
+ for (const { field, rawEl } of editorFieldsToSync) {
644
+ const match = editors.find((e) => e.name.toLowerCase() === field.editor.toLowerCase());
645
+ if (!match) {
646
+ const valid = editors.map((e) => `"${e.name}"`).join(', ');
647
+ throw new Error(`Unknown editor "${field.editor}". Valid options: ${valid}`);
648
+ }
649
+ rawEl.contentTypeElementConfiguration = { editorID: match.id };
650
+ }
651
+ }
652
+ const updated = {
653
+ ...this._rawData,
654
+ alias: this.name,
655
+ name: this.name,
656
+ description: this.description,
657
+ minAuthLevel: authLevel,
658
+ workflow: String(this.workflow),
659
+ enableDirectEdit: this.directEdit,
660
+ sharedGroups: this.sharedGroups.map((id) => ({ id })),
661
+ primaryGroup: { id: this.primaryGroup || null },
662
+ contentTypeElements: rawElements,
663
+ };
664
+ // Resolve useAsFilename → elementIdforFilename
665
+ const filenameFields = Object.values(this.fields).filter((f) => f.useAsFilename);
666
+ if (filenameFields.length > 0) {
667
+ // Last one wins — use the last field marked as useAsFilename
668
+ const chosen = filenameFields[filenameFields.length - 1];
669
+ if (chosen.type !== 'Plain Text') {
670
+ throw new Error(`Only Plain Text elements can be used as filename. "${chosen.name}" is type "${chosen.type}"`);
671
+ }
672
+ const elementId = chosen._elementId;
673
+ if (elementId !== undefined) {
674
+ updated.elementIdforFilename = elementId;
675
+ }
676
+ // Clear all others
677
+ for (const f of Object.values(this.fields)) {
678
+ f.useAsFilename = f === chosen;
679
+ }
680
+ }
681
+ await this._httpClient.request({
682
+ method: 'PUT',
683
+ path: `/contenttype/${this.id}`,
684
+ body: updated,
685
+ });
686
+ // Update raw data for next save
687
+ this._rawData = updated;
688
+ this._removedElementIds.clear();
689
+ }
690
+ }
691
+ exports.ContentType = ContentType;
692
+ /**
693
+ * Resource for content type operations.
694
+ * Provides listing and retrieval of content type definitions.
695
+ */
696
+ class ContentTypeResource {
697
+ constructor(httpClient) {
698
+ this.typeMapPromise = null;
699
+ this.typeMapExpiresAt = 0;
700
+ this.typeMapEpoch = -1;
701
+ this.editorMapPromise = null;
702
+ this.editorMapExpiresAt = 0;
703
+ this.editorMapEpoch = -1;
704
+ this.httpClient = httpClient;
705
+ }
706
+ /** Fetches and caches element type ID → name map from GET /type/ */
707
+ getTypeMap() {
708
+ if (!this.typeMapPromise || Date.now() > this.typeMapExpiresAt || this.typeMapEpoch < (0, utils_js_1.getCacheEpoch)()) {
709
+ this.typeMapExpiresAt = Date.now() + utils_js_1.DEFAULT_CACHE_TTL;
710
+ this.typeMapEpoch = (0, utils_js_1.getCacheEpoch)();
711
+ this.typeMapPromise = this.httpClient.request({
712
+ method: 'GET',
713
+ path: '/type/',
714
+ }).then((types) => new Map(types.map((t) => [t.id, t.name])));
715
+ }
716
+ return this.typeMapPromise;
717
+ }
718
+ /** Fetches and caches HTML editor ID → name map from GET /htmlEditor */
719
+ getEditorMap() {
720
+ if (!this.editorMapPromise || Date.now() > this.editorMapExpiresAt || this.editorMapEpoch < (0, utils_js_1.getCacheEpoch)()) {
721
+ this.editorMapExpiresAt = Date.now() + utils_js_1.DEFAULT_CACHE_TTL;
722
+ this.editorMapEpoch = (0, utils_js_1.getCacheEpoch)();
723
+ this.editorMapPromise = this.httpClient.request({
724
+ method: 'GET',
725
+ path: '/htmlEditor',
726
+ }).then((editors) => new Map(editors.map((e) => [e.id, e.name])), () => new Map());
727
+ }
728
+ return this.editorMapPromise;
729
+ }
730
+ /** Resolves listName for all fields that have a non-zero listId. */
731
+ async resolveListNames(contentTypes) {
732
+ // Collect unique list IDs across all content types
733
+ const listIds = new Set();
734
+ for (const ct of contentTypes) {
735
+ for (const f of Object.values(ct.fields)) {
736
+ if (f.listId)
737
+ listIds.add(f.listId);
738
+ }
739
+ }
740
+ if (listIds.size === 0)
741
+ return;
742
+ // Fetch each list and build a name map
743
+ const listNameMap = new Map();
744
+ await Promise.all(Array.from(listIds).map(async (id) => {
745
+ try {
746
+ const list = await this.httpClient.request({
747
+ method: 'GET',
748
+ path: `/list/${id}/en?override=false`,
749
+ });
750
+ listNameMap.set(id, list.name ?? '');
751
+ }
752
+ catch (error) {
753
+ (0, utils_js_1.debugWarn)(`Failed to resolve list name for listId ${id}`, error);
754
+ }
755
+ }));
756
+ // Apply names to fields
757
+ for (const ct of contentTypes) {
758
+ for (const f of Object.values(ct.fields)) {
759
+ if (f.listId) {
760
+ f.listName = listNameMap.get(f.listId) ?? '';
761
+ }
762
+ }
763
+ }
764
+ }
765
+ /**
766
+ * Lists all content type definitions.
767
+ */
768
+ async list() {
769
+ const [raw, typeMap, editorMap] = await Promise.all([
770
+ this.httpClient.request({ method: 'GET', path: '/contenttype?excludeElements=true' }),
771
+ this.getTypeMap(),
772
+ this.getEditorMap(),
773
+ ]);
774
+ const results = raw.map((r) => mapContentType(r, typeMap, editorMap));
775
+ const dataOnly = results.map((r) => r.data);
776
+ await this.resolveListNames(dataOnly);
777
+ return results.map((r) => new ContentType(r.data, this.httpClient, r.rawData));
778
+ }
779
+ /**
780
+ * Retrieves a single content type definition by ID.
781
+ */
782
+ async get(id) {
783
+ const [raw, typeMap, editorMap] = await Promise.all([
784
+ this.httpClient.request({ method: 'GET', path: `/contenttype/${id}` }),
785
+ this.getTypeMap(),
786
+ this.getEditorMap(),
787
+ ]);
788
+ const result = mapContentType(raw, typeMap, editorMap);
789
+ await this.resolveListNames([result.data]);
790
+ return new ContentType(result.data, this.httpClient, result.rawData);
791
+ }
792
+ /**
793
+ * Updates a content type's properties (immutable pattern).
794
+ * Fetches existing, merges changes, PUTs full body.
795
+ */
796
+ async update(id, data) {
797
+ const ct = await this.get(id);
798
+ if (data.name !== undefined)
799
+ ct.name = data.name;
800
+ if (data.description !== undefined)
801
+ ct.description = data.description;
802
+ if (data.minUserLevel !== undefined)
803
+ ct.minUserLevel = data.minUserLevel;
804
+ if (data.workflow !== undefined)
805
+ ct.workflow = data.workflow;
806
+ if (data.directEdit !== undefined)
807
+ ct.directEdit = data.directEdit;
808
+ if (data.sharedGroups !== undefined)
809
+ ct.sharedGroups = data.sharedGroups;
810
+ if (data.primaryGroup !== undefined)
811
+ ct.primaryGroup = data.primaryGroup;
812
+ if (data.removeFields) {
813
+ for (const fieldName of data.removeFields) {
814
+ ct.removeField(fieldName);
815
+ }
816
+ }
817
+ if (data.addFields) {
818
+ for (const field of data.addFields) {
819
+ await ct.addField(field);
820
+ }
821
+ }
822
+ await ct.save();
823
+ return ct;
824
+ }
825
+ /** Deletes a content type by ID. */
826
+ async delete(id) {
827
+ await this.httpClient.request({
828
+ method: 'DELETE',
829
+ path: `/contenttype/${id}`,
830
+ });
831
+ }
832
+ /** Creates a new content type. */
833
+ async create(data) {
834
+ if (!data.name?.trim()) {
835
+ throw new Error('Content type name is required');
836
+ }
837
+ if (!data.elements?.length) {
838
+ throw new Error('Content type must have at least one element');
839
+ }
840
+ // Validate useAsFilename constraints
841
+ const filenameElements = data.elements.filter((el) => el.useAsFilename);
842
+ if (filenameElements.length > 1) {
843
+ throw new Error('Only one element can be used as filename');
844
+ }
845
+ if (filenameElements.length === 1 && filenameElements[0].type.toLowerCase() !== 'plain text') {
846
+ throw new Error(`Only Plain Text elements can be used as filename. "${filenameElements[0].name}" is type "${filenameElements[0].type}"`);
847
+ }
848
+ const typeMap = await this.getTypeMap();
849
+ const reverseTypeMap = new Map(Array.from(typeMap.entries()).map(([id, name]) => [name.toLowerCase(), id]));
850
+ // Fetch raw types to get listType flag for validation
851
+ const rawTypes = await this.httpClient.request({
852
+ method: 'GET',
853
+ path: '/type/',
854
+ });
855
+ const listTypeNames = new Set(rawTypes.filter((t) => t.listType).map((t) => t.name.toLowerCase()));
856
+ const plainTextId = reverseTypeMap.get('plain text') ?? 1;
857
+ // Build elements array — Name element is always first
858
+ const contentTypeElements = [
859
+ {
860
+ id: 1,
861
+ name: 'Name',
862
+ description: '',
863
+ maxSize: 80,
864
+ compulsory: true,
865
+ type: plainTextId,
866
+ listId: '',
867
+ alias: 'Name',
868
+ shown: true,
869
+ sequence: 1,
870
+ },
871
+ ];
872
+ // Default: Name element is used for filename
873
+ let elementIdForFilename = 1;
874
+ let editorMapForCreate = null;
875
+ for (let i = 0; i < data.elements.length; i++) {
876
+ const el = data.elements[i];
877
+ const typeId = reverseTypeMap.get(el.type.toLowerCase());
878
+ if (typeId === undefined) {
879
+ const valid = Array.from(typeMap.values()).join(', ');
880
+ throw new Error(`Unknown element type "${el.type}". Valid options: ${valid}`);
881
+ }
882
+ // Validate listId for list-based types
883
+ if (listTypeNames.has(el.type.toLowerCase()) && !el.listId) {
884
+ throw new Error(`Element "${el.name}" is type "${el.type}" which requires a listId`);
885
+ }
886
+ // Validate repeater config
887
+ const isRepeater = el.type.toLowerCase() === 'repeater';
888
+ if (isRepeater && !el.repeater) {
889
+ throw new Error(`Element "${el.name}" is type "Repeater" which requires a repeater configuration (contentTypeId)`);
890
+ }
891
+ if (!isRepeater && el.repeater) {
892
+ throw new Error(`Element "${el.name}" is type "${el.type}" — repeater configuration is only valid for Repeater elements`);
893
+ }
894
+ // Validate editor option (only valid for HTML elements)
895
+ const isHtml = el.type.toLowerCase() === 'html';
896
+ if (el.editor !== undefined && !isHtml) {
897
+ throw new Error(`Element "${el.name}" is type "${el.type}" — editor is only valid for HTML elements`);
898
+ }
899
+ const elId = -Math.floor(Math.random() * 1000000);
900
+ const elDef = {
901
+ id: elId,
902
+ name: el.name,
903
+ description: el.description ?? '',
904
+ maxSize: String(el.maxSize ?? 80),
905
+ compulsory: el.required ?? false,
906
+ type: String(typeId),
907
+ listId: el.listId ? String(el.listId) : '',
908
+ shown: el.shown ?? true,
909
+ sequence: i + 2,
910
+ };
911
+ // Add repeater configuration if applicable
912
+ if (isRepeater && el.repeater) {
913
+ const maxRepeats = el.repeater.maxRepeats ?? 100;
914
+ elDef.contentTypeElementConfiguration = {
915
+ contentTypeId: el.repeater.contentTypeId,
916
+ layout: el.repeater.layout ?? '',
917
+ minRepeats: el.repeater.minRepeats ?? 0,
918
+ maxRepeats: maxRepeats,
919
+ maxRepeatsLimit: maxRepeats,
920
+ };
921
+ }
922
+ // Add HTML editor configuration if applicable
923
+ if (isHtml && el.editor) {
924
+ if (!editorMapForCreate) {
925
+ const editors = await this.httpClient.request({
926
+ method: 'GET',
927
+ path: '/htmlEditor',
928
+ });
929
+ editorMapForCreate = new Map(editors.map((e) => [e.name.toLowerCase(), e]));
930
+ }
931
+ const match = editorMapForCreate.get(el.editor.toLowerCase());
932
+ if (!match) {
933
+ const valid = Array.from(editorMapForCreate.values()).map((e) => `"${e.name}"`).join(', ');
934
+ throw new Error(`Unknown editor "${el.editor}". Valid options: ${valid}`);
935
+ }
936
+ elDef.contentTypeElementConfiguration = { editorID: match.id };
937
+ }
938
+ contentTypeElements.push(elDef);
939
+ if (el.useAsFilename) {
940
+ elementIdForFilename = elId;
941
+ }
942
+ }
943
+ const raw = await this.httpClient.request({
944
+ method: 'POST',
945
+ path: '/contenttype',
946
+ body: {
947
+ name: data.name,
948
+ description: data.description ?? '',
949
+ type: 10,
950
+ editable: true,
951
+ fullAccess: true,
952
+ enableDirectEdit: data.directEdit ?? true,
953
+ minAuthLevel: String(utils_js_1.AUTH_LEVEL_REVERSE[data.minUserLevel ?? 'contributor'] ?? 2),
954
+ workflow: data.workflow !== undefined ? String(data.workflow) : '',
955
+ warningMessage: '',
956
+ elementIdforFilename: elementIdForFilename,
957
+ conditionals: [],
958
+ sharedGroups: (data.sharedGroups ?? []).map((id) => ({ id })),
959
+ primaryGroup: { id: data.primaryGroup ?? 0 },
960
+ contentTypeElements,
961
+ },
962
+ });
963
+ const editorMap = await this.getEditorMap();
964
+ const result = mapContentType(raw, typeMap, editorMap);
965
+ await this.resolveListNames([result.data]);
966
+ return new ContentType(result.data, this.httpClient, result.rawData);
967
+ }
968
+ }
969
+ exports.ContentTypeResource = ContentTypeResource;
970
+ //# sourceMappingURL=content-type-resource.js.map