nextjs-cms 0.9.21 → 0.9.23

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 (190) hide show
  1. package/README.md +65 -13
  2. package/dist/api/actions/files.d.ts +30 -0
  3. package/dist/api/actions/files.d.ts.map +1 -0
  4. package/dist/api/actions/files.js +234 -0
  5. package/dist/api/actions/index.d.ts +4 -0
  6. package/dist/api/actions/index.d.ts.map +1 -0
  7. package/dist/api/actions/index.js +3 -0
  8. package/dist/api/actions/pages.d.ts +297 -0
  9. package/dist/api/actions/pages.d.ts.map +1 -0
  10. package/dist/api/actions/pages.js +1215 -0
  11. package/dist/api/actions/privileges.d.ts +25 -0
  12. package/dist/api/actions/privileges.d.ts.map +1 -0
  13. package/dist/api/actions/privileges.js +98 -0
  14. package/dist/api/client/index.d.ts +4 -0
  15. package/dist/api/client/index.d.ts.map +1 -0
  16. package/dist/api/client/index.js +3 -0
  17. package/dist/api/client.d.ts +30 -0
  18. package/dist/api/client.d.ts.map +1 -0
  19. package/dist/api/client.js +82 -0
  20. package/dist/api/index.d.ts +1 -938
  21. package/dist/api/index.d.ts.map +1 -1
  22. package/dist/api/index.js +0 -13
  23. package/dist/api/lib/serverActions.d.ts +3 -3
  24. package/dist/api/plugin/index.d.ts +7 -0
  25. package/dist/api/plugin/index.d.ts.map +1 -0
  26. package/dist/api/plugin/index.js +5 -0
  27. package/dist/api/root.d.ts +18 -1844
  28. package/dist/api/root.d.ts.map +1 -1
  29. package/dist/api/root.js +18 -83
  30. package/dist/api/routers/navigation.d.ts +3 -3
  31. package/dist/api/server/index.d.ts +8 -0
  32. package/dist/api/server/index.d.ts.map +1 -0
  33. package/dist/api/server/index.js +3 -0
  34. package/dist/api/server.d.ts +2748 -0
  35. package/dist/api/server.d.ts.map +1 -0
  36. package/dist/api/server.js +100 -0
  37. package/dist/api/trpc/client.d.ts +19 -3
  38. package/dist/api/trpc/client.d.ts.map +1 -1
  39. package/dist/api/trpc/client.js +55 -1
  40. package/dist/api/trpc/query-client.d.ts +3 -1
  41. package/dist/api/trpc/query-client.d.ts.map +1 -1
  42. package/dist/api/trpc/query-client.js +25 -20
  43. package/dist/api/trpc/root.d.ts +906 -0
  44. package/dist/api/trpc/root.d.ts.map +1 -0
  45. package/dist/api/trpc/root.js +47 -0
  46. package/dist/api/trpc/routers/accountSettings.d.ts +66 -0
  47. package/dist/api/trpc/routers/accountSettings.d.ts.map +1 -0
  48. package/dist/api/trpc/routers/accountSettings.js +200 -0
  49. package/dist/api/trpc/routers/admins.d.ts +112 -0
  50. package/dist/api/trpc/routers/admins.d.ts.map +1 -0
  51. package/dist/api/trpc/routers/admins.js +331 -0
  52. package/dist/api/trpc/routers/auth.d.ts +54 -0
  53. package/dist/api/trpc/routers/auth.d.ts.map +1 -0
  54. package/dist/api/trpc/routers/auth.js +50 -0
  55. package/dist/api/trpc/routers/categorySection.d.ts +105 -0
  56. package/dist/api/trpc/routers/categorySection.d.ts.map +1 -0
  57. package/dist/api/trpc/routers/categorySection.js +49 -0
  58. package/dist/api/trpc/routers/config.d.ts +48 -0
  59. package/dist/api/trpc/routers/config.d.ts.map +1 -0
  60. package/dist/api/trpc/routers/config.js +18 -0
  61. package/dist/api/trpc/routers/cpanel.d.ts +82 -0
  62. package/dist/api/trpc/routers/cpanel.d.ts.map +1 -0
  63. package/dist/api/trpc/routers/cpanel.js +216 -0
  64. package/dist/api/trpc/routers/fields.d.ts +35 -0
  65. package/dist/api/trpc/routers/fields.d.ts.map +1 -0
  66. package/dist/api/trpc/routers/fields.js +81 -0
  67. package/dist/api/trpc/routers/files.d.ts +34 -0
  68. package/dist/api/trpc/routers/files.d.ts.map +1 -0
  69. package/dist/api/trpc/routers/files.js +14 -0
  70. package/dist/api/trpc/routers/gallery.d.ts +35 -0
  71. package/dist/api/trpc/routers/gallery.d.ts.map +1 -0
  72. package/dist/api/trpc/routers/gallery.js +92 -0
  73. package/dist/api/trpc/routers/hasItemsSection.d.ts +194 -0
  74. package/dist/api/trpc/routers/hasItemsSection.d.ts.map +1 -0
  75. package/dist/api/trpc/routers/hasItemsSection.js +86 -0
  76. package/dist/api/trpc/routers/logs.d.ts +59 -0
  77. package/dist/api/trpc/routers/logs.d.ts.map +1 -0
  78. package/dist/api/trpc/routers/logs.js +79 -0
  79. package/dist/api/trpc/routers/navigation.d.ts +65 -0
  80. package/dist/api/trpc/routers/navigation.d.ts.map +1 -0
  81. package/dist/api/trpc/routers/navigation.js +11 -0
  82. package/dist/api/trpc/routers/simpleSection.d.ts +93 -0
  83. package/dist/api/trpc/routers/simpleSection.d.ts.map +1 -0
  84. package/dist/api/trpc/routers/simpleSection.js +54 -0
  85. package/dist/api/trpc/server.d.ts +2789 -5
  86. package/dist/api/trpc/server.d.ts.map +1 -1
  87. package/dist/api/trpc/server.js +91 -52
  88. package/dist/api/trpc/trpc.d.ts +111 -0
  89. package/dist/api/trpc/trpc.d.ts.map +1 -0
  90. package/dist/api/trpc/trpc.js +99 -0
  91. package/dist/api/trpc/utils/async-caller-proxy.d.ts +2 -0
  92. package/dist/api/trpc/utils/async-caller-proxy.d.ts.map +1 -0
  93. package/dist/api/trpc/utils/async-caller-proxy.js +38 -0
  94. package/dist/api/trpc/utils/refresh-token-link.d.ts +6 -0
  95. package/dist/api/trpc/utils/refresh-token-link.d.ts.map +1 -0
  96. package/dist/api/trpc/utils/refresh-token-link.js +81 -0
  97. package/dist/api/trpc/utils/router-types.d.ts +7 -0
  98. package/dist/api/trpc/utils/router-types.d.ts.map +1 -0
  99. package/dist/api/trpc/utils/router-types.js +0 -0
  100. package/dist/api/use-axios-private.d.ts +6 -0
  101. package/dist/api/use-axios-private.d.ts.map +1 -0
  102. package/dist/api/use-axios-private.js +57 -0
  103. package/dist/api/utils/async-caller-proxy.d.ts +2 -0
  104. package/dist/api/utils/async-caller-proxy.d.ts.map +1 -0
  105. package/dist/api/utils/async-caller-proxy.js +36 -0
  106. package/dist/api/utils/lazy-caller-proxy.d.ts +2 -0
  107. package/dist/api/utils/lazy-caller-proxy.d.ts.map +1 -0
  108. package/dist/api/utils/lazy-caller-proxy.js +36 -0
  109. package/dist/api/utils/router-types.d.ts +7 -0
  110. package/dist/api/utils/router-types.d.ts.map +1 -0
  111. package/dist/api/utils/router-types.js +0 -0
  112. package/dist/auth/hooks/index.d.ts +1 -2
  113. package/dist/auth/hooks/index.d.ts.map +1 -1
  114. package/dist/auth/hooks/index.js +1 -2
  115. package/dist/auth/react.d.ts +1 -2
  116. package/dist/auth/react.d.ts.map +1 -1
  117. package/dist/auth/react.js +1 -2
  118. package/dist/auth/trpc.d.ts +1 -1
  119. package/dist/auth/trpc.d.ts.map +1 -1
  120. package/dist/auth/trpc.js +0 -1
  121. package/dist/cli/lib/fix-master-admin.d.ts.map +1 -1
  122. package/dist/cli/lib/fix-master-admin.js +12 -25
  123. package/dist/cli/lib/update-sections.d.ts.map +1 -1
  124. package/dist/cli/lib/update-sections.js +90 -46
  125. package/dist/core/config/config-loader.d.ts +23 -7
  126. package/dist/core/config/config-loader.d.ts.map +1 -1
  127. package/dist/core/config/config-loader.js +26 -9
  128. package/dist/core/db/table-checker/MysqlTable.d.ts.map +1 -1
  129. package/dist/core/db/table-checker/MysqlTable.js +3 -1
  130. package/dist/core/fields/date-range.d.ts +4 -4
  131. package/dist/core/fields/select.d.ts +1 -1
  132. package/dist/core/sections/category.d.ts +8 -8
  133. package/dist/core/sections/hasItems.d.ts +8 -8
  134. package/dist/core/sections/section.d.ts +5 -5
  135. package/dist/core/sections/simple.d.ts +4 -4
  136. package/dist/core/types/index.d.ts +17 -0
  137. package/dist/core/types/index.d.ts.map +1 -1
  138. package/dist/index.d.ts +0 -1
  139. package/dist/index.d.ts.map +1 -1
  140. package/dist/index.js +0 -1
  141. package/dist/plugins/client.d.ts +19 -0
  142. package/dist/plugins/client.d.ts.map +1 -0
  143. package/dist/plugins/client.js +24 -0
  144. package/dist/plugins/define.d.ts +4 -0
  145. package/dist/plugins/define.d.ts.map +1 -0
  146. package/dist/plugins/define.js +3 -0
  147. package/dist/plugins/derive.d.ts +32 -0
  148. package/dist/plugins/derive.d.ts.map +1 -0
  149. package/dist/plugins/derive.js +77 -0
  150. package/dist/plugins/loader.d.ts +51 -7
  151. package/dist/plugins/loader.d.ts.map +1 -1
  152. package/dist/plugins/loader.js +111 -51
  153. package/dist/plugins/manifest.d.ts +28 -0
  154. package/dist/plugins/manifest.d.ts.map +1 -0
  155. package/dist/plugins/manifest.js +83 -0
  156. package/dist/plugins/prefetch.d.ts +16 -0
  157. package/dist/plugins/prefetch.d.ts.map +1 -0
  158. package/dist/plugins/prefetch.js +40 -0
  159. package/dist/plugins/registry.d.ts +22 -0
  160. package/dist/plugins/registry.d.ts.map +1 -0
  161. package/dist/plugins/registry.js +25 -0
  162. package/dist/plugins/server.d.ts +2 -0
  163. package/dist/plugins/server.d.ts.map +1 -1
  164. package/dist/plugins/server.js +2 -0
  165. package/dist/plugins/types.d.ts +9 -0
  166. package/dist/plugins/types.d.ts.map +1 -0
  167. package/dist/plugins/types.js +0 -0
  168. package/dist/translations/base/en.d.ts +5 -0
  169. package/dist/translations/base/en.d.ts.map +1 -1
  170. package/dist/translations/base/en.js +5 -0
  171. package/dist/translations/client.d.ts +64 -4
  172. package/dist/translations/client.d.ts.map +1 -1
  173. package/dist/translations/server.d.ts +64 -4
  174. package/dist/translations/server.d.ts.map +1 -1
  175. package/dist/utils/console-log.d.ts +18 -0
  176. package/dist/utils/console-log.d.ts.map +1 -0
  177. package/dist/utils/console-log.js +28 -0
  178. package/dist/utils/index.d.ts +1 -0
  179. package/dist/utils/index.d.ts.map +1 -1
  180. package/dist/utils/index.js +1 -0
  181. package/dist/utils/log.d.ts +18 -0
  182. package/dist/utils/log.d.ts.map +1 -0
  183. package/dist/utils/log.js +28 -0
  184. package/dist/validators/index.d.ts +1 -0
  185. package/dist/validators/index.d.ts.map +1 -1
  186. package/dist/validators/index.js +1 -0
  187. package/dist/validators/tags.d.ts +4 -0
  188. package/dist/validators/tags.d.ts.map +1 -0
  189. package/dist/validators/tags.js +8 -0
  190. package/package.json +36 -18
@@ -0,0 +1,1215 @@
1
+ import 'server-only';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { and, eq, sql } from 'drizzle-orm';
5
+ import { getCMSConfig } from '../../core/config/index.js';
6
+ import { MysqlTableChecker } from '../../core/db/index.js';
7
+ import { FieldFactory, SectionFactory } from '../../core/factories/index.js';
8
+ import { SelectField } from '../../core/fields/index.js';
9
+ import { resolveLocale } from '../../core/localization/index.js';
10
+ import { db } from '../../db/client.js';
11
+ import { EditorPhotosTable } from '../../db/schema.js';
12
+ import { recordLog } from '../../logging/index.js';
13
+ import { getPluginNavigation } from '../../plugins/loader.js';
14
+ import getString from '../../translations/index.js';
15
+ import { resolveLanguage, resolveMultilingualString } from '../../translations/language-utils.js';
16
+ import { getAdminPrivileges } from './privileges.js';
17
+ async function deleteGalleryFiles({ uploadsFolder, sectionName, photos, }) {
18
+ for (const photo of photos) {
19
+ try {
20
+ await fs.promises.unlink(path.join(uploadsFolder, '.photos', sectionName, photo));
21
+ await fs.promises.unlink(path.join(uploadsFolder, '.thumbs', sectionName, photo));
22
+ }
23
+ catch (error) {
24
+ console.error('Error deleting gallery photo', error);
25
+ }
26
+ }
27
+ }
28
+ async function deleteLocalizedGalleryRows({ section, gallery, sectionItemId, locale, uploadsFolder, }) {
29
+ if (!gallery.localized)
30
+ return;
31
+ const { tableName, referenceIdentifierField, photoNameField } = gallery.db;
32
+ const columns = await MysqlTableChecker.getColumns(tableName);
33
+ if (!columns.includes(referenceIdentifierField) ||
34
+ !columns.includes(photoNameField) ||
35
+ !columns.includes('locale')) {
36
+ return;
37
+ }
38
+ const [rows] = await db.execute(sql `SELECT \`${sql.raw(photoNameField)}\` AS photo FROM \`${sql.raw(tableName)}\` WHERE \`${sql.raw(referenceIdentifierField)}\` = ${sectionItemId} AND \`locale\` = ${locale}`);
39
+ const photos = (rows ?? []).map((row) => row.photo).filter(Boolean);
40
+ await deleteGalleryFiles({ uploadsFolder, sectionName: section.name, photos });
41
+ await db.execute(sql `DELETE FROM \`${sql.raw(tableName)}\` WHERE \`${sql.raw(referenceIdentifierField)}\` = ${sectionItemId} AND \`locale\` = ${locale}`);
42
+ }
43
+ function sectionHasLocalizedContent(section) {
44
+ return section.hasLocalizedContent ?? section.hasLocalizedFields ?? false;
45
+ }
46
+ export const createSimpleSectionPage = async (session, sectionName, locale) => {
47
+ try {
48
+ const cmsConfig = await getCMSConfig();
49
+ const localeResult = resolveLocale({
50
+ localization: cmsConfig.localization,
51
+ locale,
52
+ });
53
+ if (locale && !localeResult.resolvedLocale) {
54
+ if (localeResult.localizationEnabled === false) {
55
+ return {
56
+ error: {
57
+ message: getString('localizationNotEnabledForSection', session.user.language),
58
+ },
59
+ };
60
+ }
61
+ return {
62
+ error: {
63
+ message: getString('invalidLocale', session.user.language, {
64
+ locale,
65
+ locales: localeResult.availableLocales.map((l) => l.code).join(', '),
66
+ }),
67
+ },
68
+ };
69
+ }
70
+ /**
71
+ * Let's fetch the section information
72
+ */
73
+ const fieldsFactory = new FieldFactory({
74
+ type: 'edit',
75
+ sectionName,
76
+ session: session,
77
+ itemId: 1, // itemId is always 1 for simple sections
78
+ });
79
+ await fieldsFactory.initialize();
80
+ if (fieldsFactory.error) {
81
+ return {
82
+ error: {
83
+ message: fieldsFactory.errorMessage,
84
+ },
85
+ };
86
+ }
87
+ await fieldsFactory.generateFields();
88
+ const sectionInfo = fieldsFactory.sectionInfo;
89
+ const gallery = await sectionInfo.getGallery();
90
+ // Get the locale for resolving localized titles
91
+ const uiLanguage = resolveLanguage(session.user.language, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
92
+ const resolvedTitle = resolveMultilingualString(sectionInfo.title, uiLanguage, cmsConfig.i18n.fallbackLanguage);
93
+ /**
94
+ * Fetch localization metadata for sections with localized fields
95
+ */
96
+ let localizationData = null;
97
+ if (localeResult.localizationEnabled) {
98
+ let existingTranslations = [];
99
+ const hasLocalizedContent = sectionHasLocalizedContent(sectionInfo);
100
+ if (hasLocalizedContent) {
101
+ const localesTableName = sectionInfo.localesTableName;
102
+ const localeColumns = await MysqlTableChecker.getColumns(localesTableName);
103
+ const localeTableExists = localeColumns.length > 0;
104
+ if (localeTableExists) {
105
+ // Simple sections always have itemId = 1
106
+ const sectionItemId = 1;
107
+ const [rows] = await db.execute(sql `SELECT locale FROM \`${sql.raw(localesTableName)}\` WHERE parent_id = ${sectionItemId}`);
108
+ existingTranslations = rows.map((r) => r.locale);
109
+ // Override localized field values with locale-specific data
110
+ if (locale && localeResult.resolvedLocale && localeResult.isDefault === false) {
111
+ const [localeRows] = await db.execute(sql `SELECT * FROM \`${sql.raw(localesTableName)}\` WHERE parent_id = ${sectionItemId} AND locale = ${localeResult.resolvedLocale.code} LIMIT 1`);
112
+ const localeRow = localeRows[0] ?? null;
113
+ for (const field of sectionInfo.fields) {
114
+ if (!field.localized)
115
+ continue;
116
+ // For select/tags with destinationDb, fetch locale-scoped junction values
117
+ const f = field;
118
+ if (f.destinationDb && (field.type === 'select' || field.type === 'select_multiple')) {
119
+ if (f.db) {
120
+ const [_rows] = await db.execute(sql `SELECT * FROM \`${sql.raw(f.destinationDb.table)}\` a JOIN \`${sql.raw(f.db.table)}\` b ON a.\`${sql.raw(f.destinationDb.selectIdentifier)}\` = b.\`${sql.raw(f.db.identifier)}\` WHERE a.\`${sql.raw(f.destinationDb.itemIdentifier)}\` = ${sectionItemId} AND a.\`locale\` = ${localeResult.resolvedLocale.code}`);
121
+ const values = Array.isArray(_rows)
122
+ ? _rows.map((row) => ({
123
+ value: row[f.destinationDb.selectIdentifier],
124
+ label: row[f.db.label],
125
+ }))
126
+ : [];
127
+ field.setValue(values);
128
+ }
129
+ else {
130
+ const [_rows] = await db.execute(sql `SELECT * FROM \`${sql.raw(f.destinationDb.table)}\` WHERE \`${sql.raw(f.destinationDb.itemIdentifier)}\` = ${sectionItemId} AND \`locale\` = ${localeResult.resolvedLocale.code}`);
131
+ const values = Array.isArray(_rows)
132
+ ? _rows.map((row) => ({
133
+ value: row[f.destinationDb.selectIdentifier],
134
+ label: f.options?.find((opt) => opt.value?.toString() ===
135
+ row[f.destinationDb.selectIdentifier]?.toString())?.label ?? '',
136
+ }))
137
+ : [];
138
+ field.setValue(values);
139
+ }
140
+ }
141
+ else if (f.destinationDb && field.type === 'tags') {
142
+ const [_rows] = await db.execute(sql `SELECT \`${sql.raw(f.destinationDb.selectIdentifier)}\` FROM \`${sql.raw(f.destinationDb.table)}\` WHERE \`${sql.raw(f.destinationDb.itemIdentifier)}\` = ${sectionItemId} AND \`locale\` = ${localeResult.resolvedLocale.code}`);
143
+ const tags = Array.isArray(_rows)
144
+ ? _rows.map((row) => row[f.destinationDb.selectIdentifier])
145
+ : [];
146
+ field.setValue(tags.join(','));
147
+ }
148
+ else if (field.type === 'date_range' && typeof f.setRangeValues === 'function') {
149
+ f.setRangeValues(localeRow ? (localeRow[f.startName] ?? null) : null, localeRow ? (localeRow[f.endName] ?? null) : null);
150
+ }
151
+ else {
152
+ // Simple field: override from locale row
153
+ field.setValue(localeRow ? (localeRow[field.name] ?? null) : null);
154
+ }
155
+ }
156
+ }
157
+ }
158
+ }
159
+ // Show the locale switcher when multiple locales are configured AND either:
160
+ // - the section has localized fields (production case), or
161
+ // - the app is in development mode (so devs can preview the switcher
162
+ // while wiring up localization on a section).
163
+ // The developer note is rendered in dev-mode only, when the switcher is
164
+ // visible but the section itself has no localized content yet.
165
+ const localeSwitcherEnabled = localeResult.availableLocales.length > 1 &&
166
+ (hasLocalizedContent === true || process.env.NODE_ENV === 'development');
167
+ const developerNoteEnabled = localeSwitcherEnabled && hasLocalizedContent === false;
168
+ localizationData = {
169
+ defaultLocale: localeResult.defaultLocale,
170
+ currentLocale: localeResult.resolvedLocale ?? localeResult.defaultLocale,
171
+ existingTranslations,
172
+ locales: localeResult.availableLocales,
173
+ localeSwitcherEnabled,
174
+ developerNoteEnabled,
175
+ };
176
+ }
177
+ return {
178
+ section: {
179
+ name: sectionInfo.name,
180
+ title: resolvedTitle,
181
+ gallery: gallery,
182
+ variants: sectionInfo.variants,
183
+ configFile: sectionInfo.configFile,
184
+ },
185
+ inputGroups: await fieldsFactory.getGroupedFields(),
186
+ localization: localizationData,
187
+ };
188
+ }
189
+ catch (err) {
190
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
191
+ console.error('Error creating section page', err);
192
+ return {
193
+ error: {
194
+ message: `Failed to create section page: ${errorMessage}`,
195
+ },
196
+ };
197
+ }
198
+ };
199
+ export const deleteSectionItem = async (session, sectionName, sectionItemId, recursive, requestMetadata) => {
200
+ /**
201
+ * Convert the section item id to string
202
+ */
203
+ sectionItemId = sectionItemId.toString();
204
+ try {
205
+ const _s = (await SectionFactory.getSectionForAdmin({
206
+ name: sectionName,
207
+ admin: {
208
+ id: session.user.id,
209
+ requiredRole: 'D',
210
+ },
211
+ }));
212
+ const section = _s?.build();
213
+ if (!section) {
214
+ return {
215
+ error: {
216
+ message: getString('sectionNotFound', session.user.language),
217
+ },
218
+ };
219
+ }
220
+ const tableName = section.db.table;
221
+ const identifierFieldName = section.db.identifier.name;
222
+ const columns = await MysqlTableChecker.getColumns(tableName);
223
+ if (!columns.includes(identifierFieldName)) {
224
+ return {
225
+ error: {
226
+ message: getString('sectionTableIdentifierNotFound', session.user.language),
227
+ },
228
+ };
229
+ }
230
+ /**
231
+ * Get the section item from the table
232
+ */
233
+ const [_v, _f] = await db.execute(sql `select * from ${sql.raw(tableName)} where ${sql.raw(identifierFieldName)} = ${sectionItemId} limit 1`);
234
+ // @ts-ignore
235
+ // Bug: this is a bug in drizzle-orm/mysql2
236
+ const sectionItemRow = _v[0];
237
+ /**
238
+ * Run beforeDelete hook before the actual deletion
239
+ */
240
+ if (section.hooks?.beforeDelete) {
241
+ const hook = section.hooks.beforeDelete;
242
+ const handler = typeof hook === 'function' ? hook : hook.handler;
243
+ await handler({
244
+ itemId: sectionItemId,
245
+ values: sectionItemRow ?? {},
246
+ section: section,
247
+ });
248
+ }
249
+ /**
250
+ * Cascade delete translations from the locales table.
251
+ * Use deleteLocaleTranslation per locale to ensure localized files are cleaned up from disk.
252
+ */
253
+ const cmsConfig = await getCMSConfig();
254
+ if (cmsConfig.localization?.enabled && sectionHasLocalizedContent(section)) {
255
+ const localesTableName = section.localesTableName;
256
+ const localeColumns = await MysqlTableChecker.getColumns(localesTableName);
257
+ if (localeColumns.length > 0) {
258
+ const [localeRows] = await db.execute(sql `SELECT locale FROM \`${sql.raw(localesTableName)}\` WHERE parent_id = ${sectionItemId}`);
259
+ const locales = localeRows.map((r) => r.locale);
260
+ for (const locale of locales) {
261
+ const result = await deleteLocaleTranslation(session, sectionName, sectionItemId, locale);
262
+ if (result && typeof result === 'object' && 'error' in result) {
263
+ return result;
264
+ }
265
+ }
266
+ }
267
+ }
268
+ /**
269
+ * Grab the file fields to delete the files
270
+ */
271
+ const fileFieldConfigs = section.fieldConfigs.filter((fieldConfig) => fieldConfig.type === 'document' || fieldConfig.type === 'photo' || fieldConfig.type === 'video');
272
+ const uploadsFolder = cmsConfig.media.upload.path;
273
+ if (sectionItemRow) {
274
+ for (const field of fileFieldConfigs) {
275
+ // @ts-ignore
276
+ const value = sectionItemRow[field.name];
277
+ if (value) {
278
+ if (field.type === 'document') {
279
+ try {
280
+ await fs.promises.unlink(path.join(uploadsFolder, '.documents', section.name, value));
281
+ }
282
+ catch (error) {
283
+ // Log but continue - file may not exist or already deleted
284
+ console.error('Error deleting document', error);
285
+ }
286
+ }
287
+ else if (field.type === 'video') {
288
+ try {
289
+ await fs.promises.unlink(path.join(uploadsFolder, '.videos', section.name, value));
290
+ }
291
+ catch (error) {
292
+ // Log but continue - file may not exist or already deleted
293
+ console.error('Error deleting video', error);
294
+ }
295
+ }
296
+ else {
297
+ try {
298
+ await fs.promises.unlink(path.join(uploadsFolder, '.photos', section.name, value));
299
+ await fs.promises.unlink(path.join(uploadsFolder, '.thumbs', section.name, value));
300
+ }
301
+ catch (error) {
302
+ // Log but continue - file may not exist or already deleted
303
+ console.error('Error deleting photo', error);
304
+ }
305
+ }
306
+ }
307
+ }
308
+ }
309
+ /**
310
+ * Delete the rows from the destination table if they exist
311
+ */
312
+ const fieldsWithDestinationDbTable = section.fieldConfigs.filter((fieldConfig) => ['select_multiple', 'select', 'tags'].includes(fieldConfig.type));
313
+ for (const fieldConfig of fieldsWithDestinationDbTable) {
314
+ const field = fieldConfig.build();
315
+ if (field.destinationDb) {
316
+ const columns = await MysqlTableChecker.getColumns(field.destinationDb.table);
317
+ if (columns.includes(field.destinationDb.selectIdentifier) &&
318
+ columns.includes(field.destinationDb.itemIdentifier)) {
319
+ await db.execute(sql `delete from ${sql.raw(field.destinationDb.table)} where ${sql.raw(field.destinationDb.itemIdentifier)} = ${sectionItemId}`);
320
+ }
321
+ }
322
+ }
323
+ /**
324
+ * Also delete the gallery photos if they exist
325
+ */
326
+ const gallery = await section.getGallery();
327
+ if (gallery) {
328
+ const { tableName, referenceIdentifierField, photoNameField, metaField } = gallery.db;
329
+ const columns = await MysqlTableChecker.getColumns(tableName);
330
+ if (columns.includes(photoNameField) &&
331
+ columns.includes(referenceIdentifierField) &&
332
+ columns.includes(metaField)) {
333
+ /**
334
+ * First, get the gallery photos
335
+ */
336
+ const [_v, _f] = await db.execute(sql `select * from \`${sql.raw(tableName)}\` where \`${sql.raw(referenceIdentifierField)}\` = ${sectionItemId}`);
337
+ // @ts-ignore
338
+ // Bug: this is a bug in drizzle-orm/mysql2
339
+ const galleryPhotos = _v;
340
+ /**
341
+ * Delete the photos from disk
342
+ */
343
+ for (const photo of galleryPhotos) {
344
+ try {
345
+ await fs.promises.unlink(path.join(uploadsFolder, '.photos', section.name, photo[photoNameField]));
346
+ await fs.promises.unlink(path.join(uploadsFolder, '.thumbs', section.name, photo[photoNameField]));
347
+ }
348
+ catch (error) {
349
+ // Log but continue - file may not exist or already deleted
350
+ console.error('Error deleting photo', error);
351
+ }
352
+ }
353
+ /**
354
+ * Delete the photos from the table
355
+ */
356
+ await db.execute(sql `DELETE FROM ${sql.raw(tableName)} WHERE ${sql.raw(referenceIdentifierField)} = ${sectionItemId}`);
357
+ }
358
+ }
359
+ /**
360
+ * Check if there are photos in the editor_photos table
361
+ */
362
+ const editorPhotos = await db
363
+ .select()
364
+ .from(EditorPhotosTable)
365
+ .where(and(eq(EditorPhotosTable.section, sectionName), eq(EditorPhotosTable.itemId, sectionItemId)));
366
+ if (editorPhotos.length > 0) {
367
+ /**
368
+ * Delete the photos from disk
369
+ */
370
+ for (const photo of editorPhotos) {
371
+ try {
372
+ await fs.promises.unlink(path.join(uploadsFolder, '.photos', photo.section, photo.name));
373
+ }
374
+ catch (error) {
375
+ // Log but continue - file may not exist or already deleted
376
+ console.error('Error deleting photo', error);
377
+ }
378
+ }
379
+ /**
380
+ * Delete the photos from the editor_photos table
381
+ */
382
+ await db
383
+ .delete(EditorPhotosTable)
384
+ .where(and(eq(EditorPhotosTable.section, sectionName), eq(EditorPhotosTable.itemId, sectionItemId)));
385
+ }
386
+ /**
387
+ * Delete the row from the table
388
+ */
389
+ await db.execute(sql `delete from \`${sql.raw(tableName)}\` where \`${sql.raw(identifierFieldName)}\` = ${sectionItemId}`);
390
+ /**
391
+ * Run afterDelete hook after the deletion
392
+ */
393
+ if (section.hooks?.afterDelete) {
394
+ const hook = section.hooks.afterDelete;
395
+ const handler = typeof hook === 'function' ? hook : hook.handler;
396
+ try {
397
+ await handler({
398
+ itemId: sectionItemId,
399
+ values: sectionItemRow ?? {},
400
+ section: section,
401
+ });
402
+ }
403
+ catch (error) {
404
+ console.error('afterDelete hook failed:', error);
405
+ }
406
+ }
407
+ const headingFieldName = section.headingField.name;
408
+ const entityLabel = headingFieldName && sectionItemRow
409
+ ? // @ts-ignore
410
+ String(sectionItemRow[headingFieldName] ?? '')
411
+ : null;
412
+ await recordLog({
413
+ eventType: 'section.item.delete',
414
+ actorId: session.user.id,
415
+ actorUsername: session.user.name,
416
+ entityType: 'section_item',
417
+ entityId: sectionItemId,
418
+ entityLabel: entityLabel?.trim() ? entityLabel : null,
419
+ sectionName: section.name,
420
+ metadata: {
421
+ sectionType: section.type,
422
+ recursive: Boolean(recursive),
423
+ },
424
+ requestMetadata,
425
+ });
426
+ return true;
427
+ }
428
+ catch (err) {
429
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
430
+ console.error('Error deleting section item', err);
431
+ return {
432
+ error: {
433
+ message: `Failed to delete section item: ${errorMessage}`,
434
+ },
435
+ };
436
+ }
437
+ };
438
+ export const deleteLocaleTranslation = async (session, sectionName, sectionItemId, locale) => {
439
+ sectionItemId = sectionItemId.toString();
440
+ try {
441
+ const _s = (await SectionFactory.getSectionForAdmin({
442
+ name: sectionName,
443
+ admin: {
444
+ id: session.user.id,
445
+ requiredRole: 'D',
446
+ },
447
+ }));
448
+ const section = _s?.build();
449
+ if (!section) {
450
+ return {
451
+ error: {
452
+ message: getString('sectionNotFound', session.user.language),
453
+ },
454
+ };
455
+ }
456
+ const cmsConfig = await getCMSConfig();
457
+ if (!cmsConfig.localization?.enabled || !sectionHasLocalizedContent(section)) {
458
+ return {
459
+ error: {
460
+ message: getString('localizationNotEnabledForSection', session.user.language),
461
+ },
462
+ };
463
+ }
464
+ if (locale === cmsConfig.localization.defaultLocale) {
465
+ return {
466
+ error: {
467
+ message: getString('cannotDeleteBaseLocaleTranslation', session.user.language),
468
+ },
469
+ };
470
+ }
471
+ const localesTableName = section.localesTableName;
472
+ const localeColumns = await MysqlTableChecker.getColumns(localesTableName);
473
+ if (localeColumns.length === 0) {
474
+ return {
475
+ error: {
476
+ message: getString('localesTableDoesNotExist', session.user.language),
477
+ },
478
+ };
479
+ }
480
+ // Fetch the locale row before deleting so we can clean up files
481
+ const [localeRows] = await db.execute(sql `SELECT * FROM \`${sql.raw(localesTableName)}\` WHERE parent_id = ${sectionItemId} AND locale = ${locale} LIMIT 1`);
482
+ const localeRow = localeRows[0] ?? null;
483
+ const beforeDeleteHook = section.hooks?.beforeDelete;
484
+ if (beforeDeleteHook && typeof beforeDeleteHook === 'object' && beforeDeleteHook.runForLocales) {
485
+ await beforeDeleteHook.handler({
486
+ itemId: sectionItemId,
487
+ values: localeRow ?? {},
488
+ section: section,
489
+ locale,
490
+ });
491
+ }
492
+ // Delete files for localized photo/document/video fields
493
+ if (localeRow) {
494
+ const uploadsFolder = cmsConfig.media.upload.path;
495
+ const localizedFileFields = section.fieldConfigs.filter((f) => f.localized === true && (f.type === 'photo' || f.type === 'document' || f.type === 'video'));
496
+ for (const field of localizedFileFields) {
497
+ const value = localeRow[field.name];
498
+ if (!value)
499
+ continue;
500
+ if (field.type === 'document') {
501
+ try {
502
+ await fs.promises.unlink(path.join(uploadsFolder, '.documents', section.name, value));
503
+ }
504
+ catch (error) {
505
+ console.error('Error deleting document', error);
506
+ }
507
+ }
508
+ else if (field.type === 'video') {
509
+ try {
510
+ await fs.promises.unlink(path.join(uploadsFolder, '.videos', section.name, value));
511
+ }
512
+ catch (error) {
513
+ console.error('Error deleting video', error);
514
+ }
515
+ }
516
+ else {
517
+ // photo
518
+ try {
519
+ await fs.promises.unlink(path.join(uploadsFolder, '.photos', section.name, value));
520
+ await fs.promises.unlink(path.join(uploadsFolder, '.thumbs', section.name, value));
521
+ }
522
+ catch (error) {
523
+ console.error('Error deleting photo', error);
524
+ }
525
+ }
526
+ }
527
+ }
528
+ const gallery = await section.getGallery();
529
+ if (gallery?.localized) {
530
+ await deleteLocalizedGalleryRows({
531
+ section,
532
+ gallery,
533
+ sectionItemId,
534
+ locale,
535
+ uploadsFolder: cmsConfig.media.upload.path,
536
+ });
537
+ }
538
+ // Delete locale-scoped editor photos for localized rich_text fields.
539
+ // Uses raw SQL because the `locale` column only exists on the DB table when
540
+ // localization is enabled; the Drizzle schema no longer declares it.
541
+ const [editorPhotoRows] = await db.execute(sql `SELECT \`photo\` as \`name\` FROM \`editor_photos\` WHERE \`section\` = ${sectionName} AND \`item_id\` = ${sectionItemId.toString()} AND \`locale\` = ${locale}`);
542
+ const editorPhotos = editorPhotoRows ?? [];
543
+ if (editorPhotos.length > 0) {
544
+ const uploadsFolder = cmsConfig.media.upload.path;
545
+ for (const photo of editorPhotos) {
546
+ try {
547
+ await fs.promises.unlink(path.join(uploadsFolder, '.photos', section.name, photo.name));
548
+ }
549
+ catch (error) {
550
+ console.error('Error deleting editor photo', error);
551
+ }
552
+ }
553
+ await db.execute(sql `DELETE FROM \`editor_photos\` WHERE \`section\` = ${sectionName} AND \`item_id\` = ${sectionItemId.toString()} AND \`locale\` = ${locale}`);
554
+ }
555
+ // Delete locale-scoped junction table rows for localized select/tags fields
556
+ const localizedJunctionFields = section.fieldConfigs.filter((f) => f.localized === true &&
557
+ f.destinationDb &&
558
+ (f.type === 'select' || f.type === 'select_multiple' || f.type === 'tags'));
559
+ for (const field of localizedJunctionFields) {
560
+ const destDb = field.destinationDb;
561
+ if (!destDb)
562
+ continue;
563
+ try {
564
+ await db.execute(sql `DELETE FROM \`${sql.raw(destDb.table)}\` WHERE \`${sql.raw(destDb.itemIdentifier)}\` = ${sectionItemId} AND \`locale\` = ${locale}`);
565
+ }
566
+ catch (error) {
567
+ console.error(`Error deleting junction table rows for ${field.name}`, error);
568
+ }
569
+ }
570
+ await db.execute(sql `DELETE FROM \`${sql.raw(localesTableName)}\` WHERE parent_id = ${sectionItemId} AND locale = ${locale}`);
571
+ const afterDeleteHook = section.hooks?.afterDelete;
572
+ if (afterDeleteHook && typeof afterDeleteHook === 'object' && afterDeleteHook.runForLocales) {
573
+ try {
574
+ await afterDeleteHook.handler({
575
+ itemId: sectionItemId,
576
+ values: localeRow ?? {},
577
+ section: section,
578
+ locale,
579
+ });
580
+ }
581
+ catch (e) {
582
+ console.error('afterDelete hook failed:', e);
583
+ }
584
+ }
585
+ await recordLog({
586
+ eventType: 'section.item.locale.delete',
587
+ actorId: session.user.id,
588
+ actorUsername: session.user.name,
589
+ entityType: 'section_item_locale',
590
+ entityId: sectionItemId,
591
+ entityLabel: locale,
592
+ sectionName: section.name,
593
+ metadata: {
594
+ locale,
595
+ },
596
+ });
597
+ return true;
598
+ }
599
+ catch (err) {
600
+ const errorMessage = err instanceof Error ? err.message : getString('unknownErrorOccurred', session.user.language);
601
+ console.error('Error deleting locale translation', err);
602
+ return {
603
+ error: {
604
+ message: getString('deleteLocaleTranslationFailed', session.user.language, { detail: errorMessage }),
605
+ },
606
+ };
607
+ }
608
+ };
609
+ export const createEditPage = async (session, sectionName, sectionItemId, locale) => {
610
+ /**
611
+ * Generate the fields for the edit page
612
+ */
613
+ const fieldsFactory = new FieldFactory({
614
+ type: 'edit',
615
+ sectionName,
616
+ session,
617
+ itemId: sectionItemId,
618
+ });
619
+ try {
620
+ const cmsConfig = await getCMSConfig();
621
+ const localeResult = resolveLocale({
622
+ localization: cmsConfig.localization,
623
+ locale,
624
+ });
625
+ if (locale && !localeResult.resolvedLocale) {
626
+ if (localeResult.localizationEnabled === false) {
627
+ return {
628
+ error: {
629
+ message: getString('localizationNotEnabledForSection', session.user.language),
630
+ },
631
+ };
632
+ }
633
+ return {
634
+ error: {
635
+ message: getString('invalidLocale', session.user.language, {
636
+ locale,
637
+ locales: localeResult.availableLocales.map((l) => l.code).join(', '),
638
+ }),
639
+ },
640
+ };
641
+ }
642
+ await fieldsFactory.initialize();
643
+ await fieldsFactory.generateFields();
644
+ if (fieldsFactory.error) {
645
+ return {
646
+ error: {
647
+ message: fieldsFactory.errorMessage,
648
+ },
649
+ };
650
+ }
651
+ /**
652
+ * Let's check for variants
653
+ */
654
+ /*const variants = section[0].variants
655
+ const sectionVariants: { name: string }[] = []
656
+
657
+ if (variants && variants.trim() !== '') {
658
+ /!**
659
+ * Convert to JSON
660
+ *!/
661
+ const variantsJson = JSON.parse(variants)
662
+
663
+ /!**
664
+ * Loop through the variants
665
+ *!/
666
+
667
+ variantsJson.forEach((variant: any) => {
668
+ const variantName = variant.name
669
+ const variantInfo = variant.info
670
+ })
671
+ }*/
672
+ /**
673
+ * Get the gallery photos
674
+ * TODO: This is a temp implementation, will be removed once converting the gallery into a field
675
+ */
676
+ let galleryItems = [];
677
+ const gallery = await fieldsFactory.sectionInfo?.getGallery();
678
+ if (gallery) {
679
+ const { tableName, referenceIdentifierField, photoNameField, metaField } = gallery.db;
680
+ const columns = await MysqlTableChecker.getColumns(tableName);
681
+ if (columns.includes(photoNameField) &&
682
+ columns.includes(referenceIdentifierField) &&
683
+ columns.includes(metaField)) {
684
+ const galleryIsLocalized = gallery.localized === true && localeResult.localizationEnabled;
685
+ const currentLocale = localeResult.resolvedLocale?.code;
686
+ const localeFilter = galleryIsLocalized && currentLocale && columns.includes('locale')
687
+ ? sql ` AND \`locale\` = ${currentLocale}`
688
+ : sql ``;
689
+ const [items] = await db.execute(sql `select * from \`${sql.raw(tableName)}\` where \`${sql.raw(referenceIdentifierField)}\` = ${sectionItemId}${localeFilter}`);
690
+ const galleryPhotos = items;
691
+ galleryPhotos?.map((item) => {
692
+ galleryItems.push({
693
+ referenceId: item[referenceIdentifierField],
694
+ photo: item[photoNameField],
695
+ meta: item[metaField],
696
+ locale: item.locale,
697
+ });
698
+ });
699
+ }
700
+ }
701
+ const sectionInfo = fieldsFactory.sectionInfo;
702
+ // Resolve localized titles using the user's language
703
+ const uiLanguage = resolveLanguage(session.user.language, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
704
+ const resolvedTitle = {
705
+ section: resolveMultilingualString(sectionInfo.title.section, uiLanguage, cmsConfig.i18n.fallbackLanguage),
706
+ singular: resolveMultilingualString(sectionInfo.title.singular, uiLanguage, cmsConfig.i18n.fallbackLanguage),
707
+ plural: resolveMultilingualString(sectionInfo.title.plural, uiLanguage, cmsConfig.i18n.fallbackLanguage),
708
+ };
709
+ /**
710
+ * Fetch localization metadata for sections with localized fields
711
+ */
712
+ let localizationData = null;
713
+ if (localeResult.localizationEnabled) {
714
+ let existingTranslations = [];
715
+ const hasLocalizedContent = sectionHasLocalizedContent(sectionInfo);
716
+ if (hasLocalizedContent) {
717
+ const localesTableName = sectionInfo.localesTableName;
718
+ const localeColumns = await MysqlTableChecker.getColumns(localesTableName);
719
+ const localeTableExists = localeColumns.length > 0;
720
+ if (localeTableExists) {
721
+ const [rows] = await db.execute(sql `SELECT locale FROM \`${sql.raw(localesTableName)}\` WHERE parent_id = ${sectionItemId}`);
722
+ existingTranslations = rows.map((r) => r.locale);
723
+ // Override localized field values with locale-specific data
724
+ if (locale && localeResult.resolvedLocale && localeResult.isDefault === false) {
725
+ const [localeRows] = await db.execute(sql `SELECT * FROM \`${sql.raw(localesTableName)}\` WHERE parent_id = ${sectionItemId} AND locale = ${localeResult.resolvedLocale.code} LIMIT 1`);
726
+ const localeRow = localeRows[0] ?? null;
727
+ for (const field of sectionInfo.fields) {
728
+ if (!field.localized)
729
+ continue;
730
+ // For select/tags with destinationDb, fetch locale-scoped junction values
731
+ const f = field;
732
+ if (f.destinationDb && (field.type === 'select' || field.type === 'select_multiple')) {
733
+ if (f.db) {
734
+ const [_rows] = await db.execute(sql `SELECT * FROM \`${sql.raw(f.destinationDb.table)}\` a JOIN \`${sql.raw(f.db.table)}\` b ON a.\`${sql.raw(f.destinationDb.selectIdentifier)}\` = b.\`${sql.raw(f.db.identifier)}\` WHERE a.\`${sql.raw(f.destinationDb.itemIdentifier)}\` = ${sectionItemId} AND a.\`locale\` = ${localeResult.resolvedLocale.code}`);
735
+ const values = Array.isArray(_rows)
736
+ ? _rows.map((row) => ({
737
+ value: row[f.destinationDb.selectIdentifier],
738
+ label: row[f.db.label],
739
+ }))
740
+ : [];
741
+ field.setValue(values);
742
+ }
743
+ else {
744
+ const [_rows] = await db.execute(sql `SELECT * FROM \`${sql.raw(f.destinationDb.table)}\` WHERE \`${sql.raw(f.destinationDb.itemIdentifier)}\` = ${sectionItemId} AND \`locale\` = ${localeResult.resolvedLocale.code}`);
745
+ const values = Array.isArray(_rows)
746
+ ? _rows.map((row) => ({
747
+ value: row[f.destinationDb.selectIdentifier],
748
+ label: f.options?.find((opt) => opt.value?.toString() ===
749
+ row[f.destinationDb.selectIdentifier]?.toString())?.label ?? '',
750
+ }))
751
+ : [];
752
+ field.setValue(values);
753
+ }
754
+ }
755
+ else if (f.destinationDb && field.type === 'tags') {
756
+ const [_rows] = await db.execute(sql `SELECT \`${sql.raw(f.destinationDb.selectIdentifier)}\` FROM \`${sql.raw(f.destinationDb.table)}\` WHERE \`${sql.raw(f.destinationDb.itemIdentifier)}\` = ${sectionItemId} AND \`locale\` = ${localeResult.resolvedLocale.code}`);
757
+ const tags = Array.isArray(_rows)
758
+ ? _rows.map((row) => row[f.destinationDb.selectIdentifier])
759
+ : [];
760
+ field.setValue(tags.join(','));
761
+ }
762
+ else if (field.type === 'date_range' && typeof f.setRangeValues === 'function') {
763
+ f.setRangeValues(localeRow ? (localeRow[f.startName] ?? null) : null, localeRow ? (localeRow[f.endName] ?? null) : null);
764
+ }
765
+ else {
766
+ // Simple field: override from locale row
767
+ field.setValue(localeRow ? (localeRow[field.name] ?? null) : null);
768
+ }
769
+ }
770
+ }
771
+ }
772
+ }
773
+ // Show the locale switcher when multiple locales are configured AND either:
774
+ // - the section has localized fields (production case), or
775
+ // - the app is in development mode (so devs can preview the switcher
776
+ // while wiring up localization on a section).
777
+ // The developer note is rendered in dev-mode only, when the switcher is
778
+ // visible but the section itself has no localized content yet.
779
+ const localeSwitcherEnabled = localeResult.availableLocales.length > 1 &&
780
+ (hasLocalizedContent === true || process.env.NODE_ENV === 'development');
781
+ const developerNoteEnabled = localeSwitcherEnabled && hasLocalizedContent === false;
782
+ localizationData = {
783
+ defaultLocale: localeResult.defaultLocale,
784
+ currentLocale: localeResult.resolvedLocale ?? localeResult.defaultLocale,
785
+ existingTranslations,
786
+ locales: localeResult.availableLocales,
787
+ localeSwitcherEnabled,
788
+ developerNoteEnabled,
789
+ };
790
+ }
791
+ return {
792
+ section: {
793
+ name: sectionInfo.name,
794
+ title: resolvedTitle,
795
+ gallery: gallery,
796
+ variants: sectionInfo.variants,
797
+ configFile: sectionInfo.configFile,
798
+ },
799
+ inputGroups: await fieldsFactory.getGroupedFields(),
800
+ gallery: galleryItems,
801
+ localization: localizationData,
802
+ };
803
+ }
804
+ catch (error) {
805
+ const errorMessage = error?.errorMessage || (error instanceof Error ? error.message : 'Unknown error occurred');
806
+ console.error('Error creating edit page', error);
807
+ return {
808
+ error: {
809
+ message: errorMessage,
810
+ },
811
+ };
812
+ }
813
+ };
814
+ export const createNewPage = async (session, sectionName) => {
815
+ try {
816
+ const fieldsFactory = new FieldFactory({
817
+ type: 'new',
818
+ sectionName,
819
+ session,
820
+ });
821
+ await fieldsFactory.initialize();
822
+ if (fieldsFactory.error) {
823
+ return {
824
+ error: {
825
+ message: fieldsFactory.errorMessage,
826
+ },
827
+ };
828
+ }
829
+ try {
830
+ await fieldsFactory.generateFields();
831
+ }
832
+ catch (err) {
833
+ // console.error('Error generating fields', err)
834
+ return {
835
+ error: {
836
+ message: err.message,
837
+ },
838
+ };
839
+ }
840
+ /**
841
+ * Let's check for variants
842
+ */
843
+ /*const variants = section[0].variants
844
+ const sectionVariants: { name: string }[] = []
845
+
846
+ if (variants && variants.trim() !== '') {
847
+ /!**
848
+ * Convert to JSON
849
+ *!/
850
+ const variantsJson = JSON.parse(variants)
851
+
852
+ /!**
853
+ * Loop through the variants
854
+ *!/
855
+
856
+ variantsJson.forEach((variant: any) => {
857
+ const variantName = variant.name
858
+ const variantInfo = variant.info
859
+ })
860
+ }*/
861
+ const sectionInfo = fieldsFactory.sectionInfo;
862
+ const gallery = await sectionInfo.getGallery();
863
+ // Get the locale for resolving localized titles
864
+ const cmsConfig = await getCMSConfig();
865
+ const language = resolveLanguage(session.user.language, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
866
+ const resolvedTitle = {
867
+ section: resolveMultilingualString(sectionInfo.title.section, language, cmsConfig.i18n.fallbackLanguage),
868
+ singular: resolveMultilingualString(sectionInfo.title.singular, language, cmsConfig.i18n.fallbackLanguage),
869
+ plural: resolveMultilingualString(sectionInfo.title.plural, language, cmsConfig.i18n.fallbackLanguage),
870
+ };
871
+ const defaultLocale = cmsConfig.localization?.enabled
872
+ ? (cmsConfig.localization.locales.find((l) => l.code === cmsConfig.localization.defaultLocale) ?? null)
873
+ : null;
874
+ return {
875
+ section: {
876
+ name: sectionInfo.name,
877
+ title: resolvedTitle,
878
+ gallery: gallery,
879
+ variants: sectionInfo.variants,
880
+ configFile: sectionInfo.configFile,
881
+ },
882
+ inputGroups: await fieldsFactory.getGroupedFields(),
883
+ defaultLocale,
884
+ };
885
+ }
886
+ catch (err) {
887
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
888
+ console.error('Error creating new page', err);
889
+ return {
890
+ error: {
891
+ message: errorMessage,
892
+ },
893
+ };
894
+ }
895
+ };
896
+ export const getCategorySectionChildren = async ({ session, id, sectionName, level, }) => {
897
+ /**
898
+ * First, level up to get the children (next level)
899
+ */
900
+ level++;
901
+ // Let's fetch the section items and admin privileges for the section
902
+ const _s = (await SectionFactory.getSectionForAdmin({
903
+ name: sectionName,
904
+ admin: {
905
+ id: session.user.id,
906
+ },
907
+ }));
908
+ const section = _s?.build();
909
+ if (!section) {
910
+ return {
911
+ error: {
912
+ message: getString('sectionNotFound', session.user.language),
913
+ },
914
+ };
915
+ }
916
+ /**
917
+ * Check if new level is allowed in the category section depth
918
+ */
919
+ if (level > (section.depth ?? 1)) {
920
+ /**
921
+ * This is the last level, return an empty array
922
+ */
923
+ return {
924
+ options: null,
925
+ parentId: id,
926
+ level: level,
927
+ };
928
+ }
929
+ /**
930
+ * Let's get the options for the select input
931
+ */
932
+ const selectStatement = sql `select * from \`${sql.raw(section.db.table)}\` WHERE parent_id = ${id} AND level = ${level} ORDER BY \`${sql.raw(section.db.orderByField?.name ? section.db.orderByField?.name : section.db.identifier.name)}\` DESC`;
933
+ /**
934
+ * Get the options from the table
935
+ */
936
+ const selectOptionsRows = await db.execute(selectStatement);
937
+ const rows = selectOptionsRows[0];
938
+ return {
939
+ options: rows.map((row) => {
940
+ return {
941
+ value: row[section.db.identifier.name],
942
+ label: row[section.headingField.name],
943
+ };
944
+ }),
945
+ parentId: id,
946
+ level: level++,
947
+ };
948
+ };
949
+ export const getCategorySection = async (session, sectionName) => {
950
+ // Let's fetch the section items and admin privileges for the section
951
+ const section = (await SectionFactory.getSectionForAdmin({
952
+ name: sectionName,
953
+ admin: {
954
+ id: session.user.id,
955
+ },
956
+ }));
957
+ if (!section) {
958
+ return {
959
+ error: {
960
+ message: getString('sectionNotFound', session.user.language),
961
+ },
962
+ };
963
+ }
964
+ const tableName = section.db.table;
965
+ /**
966
+ * Create a select field config for the category section
967
+ */
968
+ const s = new SelectField({
969
+ name: section.name,
970
+ label: section.title.section,
971
+ required: false,
972
+ order: 1,
973
+ section: section,
974
+ destinationDb: undefined,
975
+ });
976
+ /**
977
+ * Build the select field to get the options
978
+ * from database and do all the necessary checks
979
+ */
980
+ await s.build();
981
+ /**
982
+ * Set the value to undefined
983
+ */
984
+ s.setValue(undefined);
985
+ // Get the locale for resolving localized titles (label must be a string for React)
986
+ const cmsConfig = await getCMSConfig();
987
+ const language = resolveLanguage(session.user.language, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
988
+ const resolvedTitle = {
989
+ section: resolveMultilingualString(section.title.section, language, cmsConfig.i18n.fallbackLanguage),
990
+ singular: resolveMultilingualString(section.title.singular, language, cmsConfig.i18n.fallbackLanguage),
991
+ plural: resolveMultilingualString(section.title.plural, language, cmsConfig.i18n.fallbackLanguage),
992
+ };
993
+ const categorySectionSelect = {
994
+ options: s.options,
995
+ required: s.required,
996
+ name: s.name,
997
+ label: resolvedTitle.section,
998
+ value: s.value,
999
+ parentId: undefined,
1000
+ level: 1,
1001
+ depth: section.depth,
1002
+ sectionName: section.name,
1003
+ allowRecursiveDelete: section.allowRecursiveDelete,
1004
+ };
1005
+ return {
1006
+ section: {
1007
+ tableName: tableName,
1008
+ sectionName: section.name,
1009
+ title: resolvedTitle,
1010
+ },
1011
+ data: categorySectionSelect,
1012
+ // publisher: privileges.publisher,
1013
+ };
1014
+ };
1015
+ export const getBrowsePage = async (session, sectionName, page = 1, q) => {
1016
+ // Let's fetch the section items and admin privileges for the section
1017
+ const _s = (await SectionFactory.getSectionForAdmin({
1018
+ name: sectionName,
1019
+ admin: {
1020
+ id: session.user.id,
1021
+ },
1022
+ }));
1023
+ const section = _s?.build();
1024
+ if (!section) {
1025
+ return {
1026
+ error: {
1027
+ message: getString('sectionNotFound', session.user.language),
1028
+ },
1029
+ };
1030
+ }
1031
+ const limit = 12;
1032
+ const offset = (page - 1) * limit;
1033
+ const tableName = section.db.table;
1034
+ const orderByFieldName = section.db.orderByField?.name || 'created_at';
1035
+ const sqlChunks = [];
1036
+ const totalRowsSqlChunks = [];
1037
+ const sectionSearch = section.search?.searchFields ? section.search?.searchFields.length > 0 : false;
1038
+ // Check if we need to JOIN _locales for search
1039
+ const cmsConfig = await getCMSConfig();
1040
+ const hasLocalization = !!cmsConfig.localization?.enabled;
1041
+ const hasLocalizedSearchFields = hasLocalization && q && q.trim().length > 0 && section.search?.searchFields?.some((f) => f.localized);
1042
+ const localesTable = hasLocalizedSearchFields ? section.localesTableName : null;
1043
+ if (localesTable) {
1044
+ // Use DISTINCT to avoid duplicate rows from the LEFT JOIN
1045
+ sqlChunks.push(sql `select distinct ${sql.raw(`\`${tableName}\``)}.* from ${sql.raw(`\`${tableName}\``)} left join ${sql.raw(`\`${localesTable}\``)} on ${sql.raw(`\`${localesTable}\``)}.parent_id = ${sql.raw(`\`${tableName}\``)}.${sql.raw(`\`${section.db.identifier.name}\``)}`);
1046
+ totalRowsSqlChunks.push(sql `select count(distinct ${sql.raw(`\`${tableName}\``)}.${sql.raw(`\`${section.db.identifier.name}\``)} ) as total from ${sql.raw(`\`${tableName}\``)} left join ${sql.raw(`\`${localesTable}\``)} on ${sql.raw(`\`${localesTable}\``)}.parent_id = ${sql.raw(`\`${tableName}\``)}.${sql.raw(`\`${section.db.identifier.name}\``)}`);
1047
+ }
1048
+ else {
1049
+ sqlChunks.push(sql `select * from ${sql.raw(`\`${tableName}\``)}`);
1050
+ totalRowsSqlChunks.push(sql `select count(*) as total from ${sql.raw(`\`${tableName}\``)}`);
1051
+ }
1052
+ if (q && q.trim().length > 0) {
1053
+ if (section.search?.searchFields.length) {
1054
+ const whereParts = [];
1055
+ for (const field of section.search.searchFields) {
1056
+ // Search in main table
1057
+ whereParts.push(sql `${sql.raw(`\`${tableName}\``)}.${sql.raw(`\`${field.name}\``)} LIKE CONCAT('%',${sql `${q.trim()}`},'%')`);
1058
+ // Also search in _locales table for localized fields
1059
+ if (localesTable && field.localized) {
1060
+ whereParts.push(sql `${sql.raw(`\`${localesTable}\``)}.${sql.raw(`\`${field.name}\``)} LIKE CONCAT('%',${sql `${q.trim()}`},'%')`);
1061
+ }
1062
+ }
1063
+ sqlChunks.push(sql `where`);
1064
+ sqlChunks.push(sql.join(whereParts, sql ` or `));
1065
+ totalRowsSqlChunks.push(sql `where`);
1066
+ totalRowsSqlChunks.push(sql.join(whereParts, sql ` or `));
1067
+ }
1068
+ }
1069
+ sqlChunks.push(sql `ORDER BY ${sql.raw(`\`${tableName}\``)}.${sql.raw(`\`${orderByFieldName}\``)} DESC LIMIT ${limit} OFFSET ${offset}`);
1070
+ const finalSql = sql.join(sqlChunks, sql.raw(' '));
1071
+ const totalRowsSql = sql.join(totalRowsSqlChunks, sql.raw(' '));
1072
+ // Now, let's get the section items from the table
1073
+ const sectionItems = await db.execute(finalSql);
1074
+ const totalCountResult = await db.execute(totalRowsSql);
1075
+ const totalCountRows = totalCountResult[0];
1076
+ const rows = sectionItems[0];
1077
+ // Resolve localized section title for the browse page header
1078
+ const language = resolveLanguage(session.user.language, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
1079
+ const resolvedTitle = resolveMultilingualString(section.title.section, language, cmsConfig.i18n.fallbackLanguage);
1080
+ return {
1081
+ section: {
1082
+ // tableName: tableName,
1083
+ // headingField: section.headingField.name,
1084
+ // identifierField: section.db.identifier.name,
1085
+ // coverPhotoField: section.coverPhotoField?.name,
1086
+ hasSearch: sectionSearch,
1087
+ name: section.name,
1088
+ title: resolvedTitle,
1089
+ },
1090
+ items: rows.map((row) => {
1091
+ // Custom browse page implementation: if browse.fields is configured,
1092
+ // return the specified fields plus default fields (id, headingTitle, coverPhoto, createdAt, createdBy, permission).
1093
+ // NOTE: This is for custom browse page implementations only.
1094
+ // The default CMS browse page component uses the fallback fields below.
1095
+ if (section.browse?.fields && section.browse.fields.length > 0) {
1096
+ const item = {
1097
+ // Always include default fields
1098
+ id: row[section.db.identifier.name],
1099
+ headingTitle: row[section.headingField.name],
1100
+ coverPhoto: section.coverPhotoField ? row[section.coverPhotoField?.name] : null,
1101
+ createdAt: row['created_at'],
1102
+ createdBy: row['created_by'],
1103
+ permission: row.permission,
1104
+ };
1105
+ // Add configured browse fields
1106
+ for (const field of section.browse.fields) {
1107
+ item[field.name] = row[field.name] ?? null;
1108
+ }
1109
+ return item;
1110
+ }
1111
+ // Default browse page fields (used by the standard CMS browse page)
1112
+ return {
1113
+ id: row[section.db.identifier.name],
1114
+ headingTitle: row[section.headingField.name],
1115
+ coverPhoto: section.coverPhotoField ? row[section.coverPhotoField?.name] : null,
1116
+ createdAt: row['created_at'],
1117
+ createdBy: row['created_by'],
1118
+ permission: row.permission,
1119
+ };
1120
+ }),
1121
+ totalCount: totalCountRows[0].total,
1122
+ };
1123
+ };
1124
+ export const getSidebar = async (session) => {
1125
+ // Let's get simple, has_items and categorized sections from the sections table
1126
+ const { simple, has_items, category, fixed } = await SectionFactory.getSectionsForAdmin({
1127
+ admin: { id: session.user.id },
1128
+ });
1129
+ const pluginNav = await getPluginNavigation();
1130
+ const privilegeSet = await getAdminPrivileges(session.user.id);
1131
+ // Get config and check for dashboard override
1132
+ const cmsConfig = await getCMSConfig();
1133
+ const dashboardOverridePath = cmsConfig.dashboard?.override;
1134
+ // Get the locale for resolving localized titles
1135
+ const language = resolveLanguage(session.user.language, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
1136
+ // Let's loop through the sections and add path, icon and title to each section item
1137
+ const simpleSectionItems = simple.map((section) => {
1138
+ return {
1139
+ title: resolveMultilingualString(section.title, language, cmsConfig.i18n.fallbackLanguage),
1140
+ path: `/section/${section.name}`,
1141
+ icon: section.icon,
1142
+ };
1143
+ });
1144
+ const hasItemsSectionItems = has_items.map((section) => {
1145
+ return {
1146
+ title: resolveMultilingualString(section.title.section, language, cmsConfig.i18n.fallbackLanguage),
1147
+ path: `/${section.name}`,
1148
+ icon: section.icon,
1149
+ };
1150
+ });
1151
+ const categorySectionsItems = category.map((section) => {
1152
+ return {
1153
+ title: resolveMultilingualString(section.title.section, language, cmsConfig.i18n.fallbackLanguage),
1154
+ path: `/categorized/${section.name}`,
1155
+ icon: section.icon,
1156
+ };
1157
+ });
1158
+ const pluginSections = [];
1159
+ for (const item of pluginNav) {
1160
+ const allowed = privilegeSet.has(item.pluginName);
1161
+ if (item.kind === 'leaf') {
1162
+ // Allowed if privileged, OR this leaf is the dashboard override (becomes /dashboard).
1163
+ if (item.path === dashboardOverridePath)
1164
+ continue;
1165
+ if (!allowed)
1166
+ continue;
1167
+ pluginSections.push({
1168
+ kind: 'leaf',
1169
+ title: resolveMultilingualString(item.title, language, cmsConfig.i18n.fallbackLanguage),
1170
+ path: item.path,
1171
+ icon: item.icon ?? '',
1172
+ });
1173
+ continue;
1174
+ }
1175
+ if (!allowed)
1176
+ continue;
1177
+ const children = item.children
1178
+ .filter((c) => c.path !== dashboardOverridePath)
1179
+ .map((c) => ({
1180
+ title: resolveMultilingualString(c.title, language, cmsConfig.i18n.fallbackLanguage),
1181
+ path: c.path,
1182
+ icon: c.icon ?? '',
1183
+ }));
1184
+ if (children.length === 0)
1185
+ continue;
1186
+ pluginSections.push({
1187
+ kind: 'group',
1188
+ title: resolveMultilingualString(item.title, language, cmsConfig.i18n.fallbackLanguage),
1189
+ icon: item.icon ?? '',
1190
+ children,
1191
+ });
1192
+ }
1193
+ const fixedSections = [
1194
+ /**
1195
+ * Add the dashboard section (points to override if configured)
1196
+ */
1197
+ {
1198
+ title: getString('dashboard', session.user.language),
1199
+ path: dashboardOverridePath ?? '/dashboard',
1200
+ icon: 'home',
1201
+ },
1202
+ ...fixed.map((section) => ({
1203
+ title: getString(section.name, session.user.language),
1204
+ path: `/${section.name}`,
1205
+ icon: section.icon,
1206
+ })),
1207
+ ];
1208
+ return {
1209
+ fixed_sections: fixedSections,
1210
+ plugin_sections: pluginSections,
1211
+ cat_sections: categorySectionsItems,
1212
+ has_items_sections: hasItemsSectionItems,
1213
+ simple_sections: simpleSectionItems,
1214
+ };
1215
+ };