nextjs-cms 0.9.36 → 0.9.37

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 (113) hide show
  1. package/dist/api/actions/pages.d.ts +3 -3
  2. package/dist/api/trpc/root.d.ts +3 -3
  3. package/dist/api/trpc/routers/navigation.d.ts +3 -3
  4. package/dist/api/trpc/server.d.ts +9 -9
  5. package/dist/cli/lib/update-sections.d.ts.map +1 -1
  6. package/dist/cli/lib/update-sections.js +43 -10
  7. package/dist/core/config/config-loader.d.ts +2 -2
  8. package/dist/core/fields/date-range.d.ts +4 -4
  9. package/dist/core/fields/slug.d.ts +55 -3
  10. package/dist/core/fields/slug.d.ts.map +1 -1
  11. package/dist/core/fields/slug.js +56 -2
  12. package/dist/core/sections/category.d.ts +4 -4
  13. package/dist/core/sections/hasItems.d.ts +4 -4
  14. package/dist/core/sections/section.d.ts +3 -3
  15. package/dist/core/sections/simple.d.ts +4 -4
  16. package/dist/translations/base/en.d.ts +3 -0
  17. package/dist/translations/base/en.d.ts.map +1 -1
  18. package/dist/translations/base/en.js +3 -0
  19. package/dist/translations/client.d.ts +40 -4
  20. package/dist/translations/client.d.ts.map +1 -1
  21. package/dist/translations/server.d.ts +40 -4
  22. package/dist/translations/server.d.ts.map +1 -1
  23. package/package.json +6 -3
  24. package/dist/api/axios/axiosInstance.d.ts +0 -2
  25. package/dist/api/axios/axiosInstance.d.ts.map +0 -1
  26. package/dist/api/axios/axiosInstance.js +0 -8
  27. package/dist/api/client.d.ts +0 -30
  28. package/dist/api/client.d.ts.map +0 -1
  29. package/dist/api/client.js +0 -82
  30. package/dist/api/index.d.ts +0 -2
  31. package/dist/api/index.d.ts.map +0 -1
  32. package/dist/api/index.js +0 -0
  33. package/dist/api/lib/serverActions.d.ts +0 -338
  34. package/dist/api/lib/serverActions.d.ts.map +0 -1
  35. package/dist/api/lib/serverActions.js +0 -1517
  36. package/dist/api/root.d.ts +0 -19
  37. package/dist/api/root.d.ts.map +0 -1
  38. package/dist/api/root.js +0 -50
  39. package/dist/api/routers/accountSettings.d.ts +0 -66
  40. package/dist/api/routers/accountSettings.d.ts.map +0 -1
  41. package/dist/api/routers/accountSettings.js +0 -202
  42. package/dist/api/routers/admins.d.ts +0 -112
  43. package/dist/api/routers/admins.d.ts.map +0 -1
  44. package/dist/api/routers/admins.js +0 -323
  45. package/dist/api/routers/auth.d.ts +0 -54
  46. package/dist/api/routers/auth.d.ts.map +0 -1
  47. package/dist/api/routers/auth.js +0 -50
  48. package/dist/api/routers/categorySection.d.ts +0 -105
  49. package/dist/api/routers/categorySection.d.ts.map +0 -1
  50. package/dist/api/routers/categorySection.js +0 -49
  51. package/dist/api/routers/config.d.ts +0 -48
  52. package/dist/api/routers/config.d.ts.map +0 -1
  53. package/dist/api/routers/config.js +0 -18
  54. package/dist/api/routers/cpanel.d.ts +0 -82
  55. package/dist/api/routers/cpanel.d.ts.map +0 -1
  56. package/dist/api/routers/cpanel.js +0 -216
  57. package/dist/api/routers/fields.d.ts +0 -35
  58. package/dist/api/routers/fields.d.ts.map +0 -1
  59. package/dist/api/routers/fields.js +0 -81
  60. package/dist/api/routers/files.d.ts +0 -34
  61. package/dist/api/routers/files.d.ts.map +0 -1
  62. package/dist/api/routers/files.js +0 -14
  63. package/dist/api/routers/gallery.d.ts +0 -35
  64. package/dist/api/routers/gallery.d.ts.map +0 -1
  65. package/dist/api/routers/gallery.js +0 -92
  66. package/dist/api/routers/hasItemsSection.d.ts +0 -194
  67. package/dist/api/routers/hasItemsSection.d.ts.map +0 -1
  68. package/dist/api/routers/hasItemsSection.js +0 -86
  69. package/dist/api/routers/logs.d.ts +0 -59
  70. package/dist/api/routers/logs.d.ts.map +0 -1
  71. package/dist/api/routers/logs.js +0 -76
  72. package/dist/api/routers/navigation.d.ts +0 -50
  73. package/dist/api/routers/navigation.d.ts.map +0 -1
  74. package/dist/api/routers/navigation.js +0 -11
  75. package/dist/api/routers/simpleSection.d.ts +0 -93
  76. package/dist/api/routers/simpleSection.d.ts.map +0 -1
  77. package/dist/api/routers/simpleSection.js +0 -54
  78. package/dist/api/server.d.ts +0 -2748
  79. package/dist/api/server.d.ts.map +0 -1
  80. package/dist/api/server.js +0 -100
  81. package/dist/api/trpc/error-logging.d.ts +0 -14
  82. package/dist/api/trpc/error-logging.d.ts.map +0 -1
  83. package/dist/api/trpc/error-logging.js +0 -75
  84. package/dist/api/trpc.d.ts +0 -111
  85. package/dist/api/trpc.d.ts.map +0 -1
  86. package/dist/api/trpc.js +0 -99
  87. package/dist/api/utils/async-caller-proxy.d.ts +0 -2
  88. package/dist/api/utils/async-caller-proxy.d.ts.map +0 -1
  89. package/dist/api/utils/async-caller-proxy.js +0 -36
  90. package/dist/api/utils/lazy-caller-proxy.d.ts +0 -2
  91. package/dist/api/utils/lazy-caller-proxy.d.ts.map +0 -1
  92. package/dist/api/utils/lazy-caller-proxy.js +0 -36
  93. package/dist/api/utils/router-types.d.ts +0 -7
  94. package/dist/api/utils/router-types.d.ts.map +0 -1
  95. package/dist/api/utils/router-types.js +0 -0
  96. package/dist/auth/axios/axiosInstance.d.ts +0 -2
  97. package/dist/auth/axios/axiosInstance.d.ts.map +0 -1
  98. package/dist/auth/axios/axiosInstance.js +0 -8
  99. package/dist/auth/hooks/useAxiosPrivate.d.ts +0 -5
  100. package/dist/auth/hooks/useAxiosPrivate.d.ts.map +0 -1
  101. package/dist/auth/hooks/useAxiosPrivate.js +0 -74
  102. package/dist/auth/trpc.d.ts +0 -6
  103. package/dist/auth/trpc.d.ts.map +0 -1
  104. package/dist/auth/trpc.js +0 -81
  105. package/dist/plugins/manifest.d.ts +0 -28
  106. package/dist/plugins/manifest.d.ts.map +0 -1
  107. package/dist/plugins/manifest.js +0 -83
  108. package/dist/plugins/registry.d.ts +0 -22
  109. package/dist/plugins/registry.d.ts.map +0 -1
  110. package/dist/plugins/registry.js +0 -25
  111. package/dist/utils/log.d.ts +0 -18
  112. package/dist/utils/log.d.ts.map +0 -1
  113. package/dist/utils/log.js +0 -28
@@ -1,1517 +0,0 @@
1
- // import 'server-only'
2
- import fs from 'fs';
3
- import path from 'path';
4
- import { Readable } from 'stream';
5
- import { TRPCError } from '@trpc/server';
6
- import { and, eq, sql } from 'drizzle-orm';
7
- import { fileTypeFromBuffer } from 'file-type';
8
- import { readChunk } from 'read-chunk';
9
- import sharp from 'sharp';
10
- import through2 from 'through2';
11
- import { getCMSConfig } from '../../core/config/index.js';
12
- import { MysqlTableChecker } from '../../core/db/index.js';
13
- import { FieldFactory, SectionFactory } from '../../core/factories/index.js';
14
- import { SelectField } from '../../core/fields/index.js';
15
- import { resolveLocale } from '../../core/localization/index.js';
16
- import { db } from '../../db/client.js';
17
- import { AdminPrivilegesTable, AdminsTable, EditorPhotosTable } from '../../db/schema.js';
18
- import { recordLog } from '../../logging/index.js';
19
- import { getPluginRoutes } from '../../plugins/loader.js';
20
- import getString from '../../translations/index.js';
21
- import { resolveLanguage, resolveMultilingualString } from '../../translations/language-utils.js';
22
- import { sanitizeFileName, sanitizeFolderOrFileName } from '../../utils/index.js';
23
- export const isAccessAllowed = async ({ sectionName, role, userId, }) => {
24
- /**
25
- * Check admin privileges
26
- */
27
- const _res = await db
28
- .select()
29
- .from(AdminPrivilegesTable)
30
- .where(and(eq(AdminPrivilegesTable.adminId, userId), eq(AdminPrivilegesTable.sectionName, sectionName)))
31
- .limit(1);
32
- const allowed = _res[0];
33
- if (!allowed)
34
- return false;
35
- if (role) {
36
- if (!allowed.operations.includes(role))
37
- return false;
38
- }
39
- return true;
40
- };
41
- async function deleteGalleryFiles({ uploadsFolder, sectionName, photos, }) {
42
- for (const photo of photos) {
43
- try {
44
- await fs.promises.unlink(path.join(uploadsFolder, '.photos', sectionName, photo));
45
- await fs.promises.unlink(path.join(uploadsFolder, '.thumbs', sectionName, photo));
46
- }
47
- catch (error) {
48
- console.error('Error deleting gallery photo', error);
49
- }
50
- }
51
- }
52
- async function deleteLocalizedGalleryRows({ section, gallery, sectionItemId, locale, uploadsFolder, }) {
53
- if (!gallery.localized)
54
- return;
55
- const { tableName, referenceIdentifierField, photoNameField } = gallery.db;
56
- const columns = await MysqlTableChecker.getColumns(tableName);
57
- if (!columns.includes(referenceIdentifierField) ||
58
- !columns.includes(photoNameField) ||
59
- !columns.includes('locale')) {
60
- return;
61
- }
62
- const [rows] = await db.execute(sql `SELECT \`${sql.raw(photoNameField)}\` AS photo FROM \`${sql.raw(tableName)}\` WHERE \`${sql.raw(referenceIdentifierField)}\` = ${sectionItemId} AND \`locale\` = ${locale}`);
63
- const photos = (rows ?? []).map((row) => row.photo).filter(Boolean);
64
- await deleteGalleryFiles({ uploadsFolder, sectionName: section.name, photos });
65
- await db.execute(sql `DELETE FROM \`${sql.raw(tableName)}\` WHERE \`${sql.raw(referenceIdentifierField)}\` = ${sectionItemId} AND \`locale\` = ${locale}`);
66
- }
67
- function sectionHasLocalizedContent(section) {
68
- return section.hasLocalizedContent ?? section.hasLocalizedFields ?? false;
69
- }
70
- export const getDocument = async (session, input) => {
71
- const { name, sectionName, fieldName } = input;
72
- // Sanitize the inputs
73
- const sanitizedFolder = sanitizeFolderOrFileName(sectionName);
74
- const sanitizedName = sanitizeFileName(name);
75
- /**
76
- * Check the section and the field name, and get the allowed extensions,
77
- * while also checking if the user has access to the section
78
- */
79
- const section = await SectionFactory.getSectionForAdmin({
80
- name: sanitizedFolder,
81
- admin: { id: session.user.id },
82
- });
83
- /**
84
- * If the check fails, throw an error
85
- */
86
- if (!section?.name) {
87
- throw new TRPCError({
88
- code: 'BAD_REQUEST',
89
- message: getString('invalidFilePath', session.user.language),
90
- });
91
- }
92
- const fieldConfig = section.fields.find((field) => field.name === fieldName);
93
- if (!fieldConfig || typeof fieldConfig.build !== 'function') {
94
- throw new TRPCError({
95
- code: 'BAD_REQUEST',
96
- message: getString('invalidRequest', session.user.language),
97
- });
98
- }
99
- const field = fieldConfig.build();
100
- /**
101
- * If field is not found, throw an error
102
- */
103
- if (!field || !field.name || !field.extensions || field.extensions.length === 0) {
104
- throw new TRPCError({
105
- code: 'BAD_REQUEST',
106
- message: getString('invalidRequest', session.user.language),
107
- });
108
- }
109
- /**
110
- * Split the allowed extensions into an array
111
- */
112
- const uploadsFolder = (await getCMSConfig()).media.upload.path;
113
- const documentAllowedExtensions = field.extensions;
114
- const dir = '.documents';
115
- const pathToFile = path.join(uploadsFolder, dir, sanitizedFolder, sanitizedName);
116
- /**
117
- * First, check if the file exists
118
- */
119
- if (!fs.existsSync(pathToFile)) {
120
- throw new TRPCError({
121
- code: 'BAD_REQUEST',
122
- message: getString('fileNotFound', session.user.language),
123
- });
124
- }
125
- /**
126
- * Read the first 4100 bytes of the file
127
- */
128
- const chunkBuffer = await readChunk(pathToFile, { length: 4100 });
129
- /**
130
- * Get the file type from the buffer
131
- */
132
- const fileType = await fileTypeFromBuffer(chunkBuffer);
133
- /**
134
- * If the file type is invalid, return an error
135
- */
136
- if (!fileType) {
137
- throw new TRPCError({
138
- code: 'BAD_REQUEST',
139
- message: getString('invalidFileType', session.user.language),
140
- });
141
- }
142
- /**
143
- * Check if the file type is allowed
144
- */
145
- if (!documentAllowedExtensions.includes(fileType.ext)) {
146
- throw new TRPCError({
147
- code: 'BAD_REQUEST',
148
- message: getString('invalidFileType', session.user.language),
149
- });
150
- }
151
- /**
152
- * Convert the image to webp format and return a buffer
153
- */
154
- const buffer = fs.readFileSync(pathToFile);
155
- /**
156
- * Create a base64 string from the buffer
157
- */
158
- const base64 = buffer.toString('base64');
159
- /**
160
- * Return the base64 string with the mime type
161
- */
162
- return {
163
- base64: `data:${fileType.mime};base64,${base64}`,
164
- mimeType: fileType.mime,
165
- };
166
- };
167
- /**
168
- * Helper function with proper cleanup for converting Node.js stream to Web ReadableStream
169
- * Uses pull()-based reading for proper backpressure support.
170
- * @param nodeStream
171
- * @param sharpInstance
172
- */
173
- function nodeStreamToWebStream(nodeStream, sharpInstance) {
174
- return new ReadableStream({
175
- start() {
176
- // Start paused — let pull() drive reading
177
- if ('pause' in nodeStream && typeof nodeStream.pause === 'function') {
178
- nodeStream.pause();
179
- }
180
- },
181
- pull(controller) {
182
- return new Promise((resolve, reject) => {
183
- const onData = (chunk) => {
184
- controller.enqueue(new Uint8Array(chunk));
185
- if ('pause' in nodeStream && typeof nodeStream.pause === 'function') {
186
- nodeStream.pause();
187
- }
188
- nodeStream.removeListener('data', onData);
189
- nodeStream.removeListener('error', onError);
190
- resolve();
191
- };
192
- const onError = (err) => {
193
- controller.error(err);
194
- nodeStream.removeListener('data', onData);
195
- reject(err);
196
- };
197
- nodeStream.on('data', onData);
198
- nodeStream.on('error', onError);
199
- nodeStream.once('end', () => {
200
- controller.close();
201
- resolve();
202
- });
203
- if ('resume' in nodeStream && typeof nodeStream.resume === 'function') {
204
- nodeStream.resume();
205
- }
206
- });
207
- },
208
- cancel() {
209
- // Proper cleanup sequence
210
- sharpInstance.destroy();
211
- nodeStream.removeAllListeners();
212
- if ('destroy' in nodeStream && typeof nodeStream.destroy === 'function') {
213
- nodeStream.destroy();
214
- }
215
- },
216
- });
217
- }
218
- export const getPhoto = async (input) => {
219
- const { name, folder, isThumb } = input;
220
- // Sanitize the inputs
221
- const sanitizedFolder = sanitizeFolderOrFileName(folder);
222
- const sanitizedName = sanitizeFileName(name);
223
- /**
224
- * Check the folder name matches a section name in the database
225
- * Notice: Maybe we don't need this check because making an sql call for each image is maybe expensive
226
- */
227
- /*const sectionQuery = await db
228
- .select()
229
- .from(SectionsTable)
230
- .where(eq(SectionsTable.sectionName, sanitizedFolder))
231
- .limit(1)
232
-
233
- if (sectionQuery.length === 0) {
234
- throw new Error('Invalid folder name')
235
- }*/
236
- const uploadsFolder = (await getCMSConfig()).media.upload.path;
237
- const dir = isThumb ? '.thumbs' : '.photos';
238
- const pathToFile = path.join(uploadsFolder, dir, sanitizedFolder, sanitizedName);
239
- /**
240
- * Disable caching for the image to avoid unlink issues when removing the image
241
- */
242
- sharp.cache({ files: 0 });
243
- sharp.cache(false);
244
- /**
245
- * Convert the image to webp format and return a buffer
246
- */
247
- const sharpInstance = sharp(pathToFile);
248
- let webpBuffer = await sharpInstance // Load the image
249
- .toFormat('webp') // Re-encode the image to webp
250
- .withExif({}) // Strip the exif data
251
- .toBuffer(); // Return a buffer
252
- /**
253
- * Destroy the sharp instance to free it from memory
254
- */
255
- const base64String = webpBuffer.toString('base64');
256
- sharpInstance.destroy();
257
- /**
258
- * Return the base64 string with the mime type
259
- */
260
- return `data:image/webp;base64,${base64String}`;
261
- };
262
- export async function streamPhoto(input) {
263
- const { name, folder, isThumb } = input;
264
- // Sanitize the inputs
265
- const sanitizedFolder = sanitizeFolderOrFileName(folder);
266
- const sanitizedName = sanitizeFileName(name);
267
- const dir = isThumb ? '.thumbs' : '.photos';
268
- const uploadsFolder = (await getCMSConfig()).media.upload.path;
269
- const pathToFile = path.join(uploadsFolder, dir, sanitizedFolder, sanitizedName);
270
- /**
271
- * Disable caching for the image to avoid unlink issues when removing the image
272
- */
273
- sharp.cache({ files: 0 });
274
- sharp.cache(false);
275
- // Process all images through Sharp
276
- const processedImage = sharp(pathToFile).toFormat('webp').withExif({});
277
- /**
278
- * Convert Node.js stream to Web ReadableStream
279
- * Also, add through2 for proper streaming
280
- */
281
- const nodeStream = processedImage.pipe(through2());
282
- return nodeStreamToWebStream(nodeStream, processedImage);
283
- }
284
- export const getVideo = async (session, input) => {
285
- throw new Error(getString('useVideoApiRoute', session.user.language));
286
- };
287
- export const getAdminPrivileges = async (adminId) => {
288
- const rows = await db
289
- .select({ sectionName: AdminPrivilegesTable.sectionName })
290
- .from(AdminPrivilegesTable)
291
- .where(eq(AdminPrivilegesTable.adminId, adminId));
292
- return new Set(rows.map((row) => row.sectionName));
293
- };
294
- export const getAllPrivileges = async (session) => {
295
- const [sections, pluginRoutes] = await Promise.all([SectionFactory.getSections(), getPluginRoutes()]);
296
- // Get the locale for resolving localized titles
297
- const cmsConfig = await getCMSConfig();
298
- const language = resolveLanguage(session.user.language, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
299
- // First, let's assign the static privileges
300
- const privilegesList = [];
301
- // Now, let's add the rest of the privileges to the list
302
- sections.forEach((section, index) => {
303
- let title;
304
- if (section.type === 'has_items' || section.type === 'category') {
305
- title = resolveMultilingualString(section.title.section, language, cmsConfig.i18n.fallbackLanguage);
306
- }
307
- else {
308
- title = resolveMultilingualString(section.title, language, cmsConfig.i18n.fallbackLanguage);
309
- }
310
- privilegesList.push({
311
- title,
312
- order: section.order || index,
313
- sectionName: section.name,
314
- sectionType: section.type,
315
- });
316
- });
317
- const pluginPrivileges = new Map();
318
- pluginRoutes.forEach((route) => {
319
- if (pluginPrivileges.has(route.pluginName))
320
- return;
321
- pluginPrivileges.set(route.pluginName, {
322
- title: resolveMultilingualString(route.title, language, cmsConfig.i18n.fallbackLanguage),
323
- });
324
- });
325
- Array.from(pluginPrivileges.entries()).forEach(([pluginName, meta], index) => {
326
- privilegesList.push({
327
- title: meta.title,
328
- order: sections.length + index,
329
- sectionName: pluginName,
330
- sectionType: 'plugin',
331
- });
332
- });
333
- privilegesList.push({ title: getString('admins', language), order: 0, sectionName: 'admins', sectionType: 'static' }, { title: getString('log', language), order: 2, sectionName: 'log', sectionType: 'static' });
334
- return privilegesList;
335
- };
336
- export const getAdminsList = async () => {
337
- const admins = await db
338
- .select({
339
- id: AdminsTable.id,
340
- username: AdminsTable.user,
341
- avatar: AdminsTable.coverphoto,
342
- })
343
- .from(AdminsTable);
344
- const roles = await db.select().from(AdminPrivilegesTable);
345
- /**
346
- * Assign the roles to the admins
347
- */
348
- const adminsWithRoles = [];
349
- admins.forEach((admin) => {
350
- const adminRoles = roles.filter((role) => role.adminId === admin.id);
351
- adminsWithRoles.push({
352
- ...admin,
353
- roles: adminRoles,
354
- });
355
- });
356
- return adminsWithRoles;
357
- };
358
- export const createSimpleSectionPage = async (session, sectionName, locale) => {
359
- try {
360
- const cmsConfig = await getCMSConfig();
361
- const localeResult = resolveLocale({
362
- localization: cmsConfig.localization,
363
- locale,
364
- });
365
- if (locale && !localeResult.resolvedLocale) {
366
- if (localeResult.localizationEnabled === false) {
367
- return {
368
- error: {
369
- message: getString('localizationNotEnabledForSection', session.user.language),
370
- },
371
- };
372
- }
373
- return {
374
- error: {
375
- message: getString('invalidLocale', session.user.language, {
376
- locale,
377
- locales: localeResult.availableLocales.map((l) => l.code).join(', '),
378
- }),
379
- },
380
- };
381
- }
382
- /**
383
- * Let's fetch the section information
384
- */
385
- const fieldsFactory = new FieldFactory({
386
- type: 'edit',
387
- sectionName,
388
- session: session,
389
- itemId: 1, // itemId is always 1 for simple sections
390
- });
391
- await fieldsFactory.initialize();
392
- if (fieldsFactory.error) {
393
- return {
394
- error: {
395
- message: fieldsFactory.errorMessage,
396
- },
397
- };
398
- }
399
- await fieldsFactory.generateFields();
400
- const sectionInfo = fieldsFactory.sectionInfo;
401
- const gallery = await sectionInfo.getGallery();
402
- // Get the locale for resolving localized titles
403
- const uiLanguage = resolveLanguage(session.user.language, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
404
- const resolvedTitle = resolveMultilingualString(sectionInfo.title, uiLanguage, cmsConfig.i18n.fallbackLanguage);
405
- /**
406
- * Fetch localization metadata for sections with localized fields
407
- */
408
- let localizationData = null;
409
- if (localeResult.localizationEnabled) {
410
- let existingTranslations = [];
411
- const hasLocalizedContent = sectionHasLocalizedContent(sectionInfo);
412
- if (hasLocalizedContent) {
413
- const localesTableName = sectionInfo.localesTableName;
414
- const localeColumns = await MysqlTableChecker.getColumns(localesTableName);
415
- const localeTableExists = localeColumns.length > 0;
416
- if (localeTableExists) {
417
- // Simple sections always have itemId = 1
418
- const sectionItemId = 1;
419
- const [rows] = await db.execute(sql `SELECT locale FROM \`${sql.raw(localesTableName)}\` WHERE parent_id = ${sectionItemId}`);
420
- existingTranslations = rows.map((r) => r.locale);
421
- // Override localized field values with locale-specific data
422
- if (locale && localeResult.resolvedLocale && localeResult.isDefault === false) {
423
- const [localeRows] = await db.execute(sql `SELECT * FROM \`${sql.raw(localesTableName)}\` WHERE parent_id = ${sectionItemId} AND locale = ${localeResult.resolvedLocale.code} LIMIT 1`);
424
- const localeRow = localeRows[0] ?? null;
425
- for (const field of sectionInfo.fields) {
426
- if (!field.localized)
427
- continue;
428
- // For select/tags with destinationDb, fetch locale-scoped junction values
429
- const f = field;
430
- if (f.destinationDb && (field.type === 'select' || field.type === 'select_multiple')) {
431
- if (f.db) {
432
- 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}`);
433
- const values = Array.isArray(_rows)
434
- ? _rows.map((row) => ({
435
- value: row[f.destinationDb.selectIdentifier],
436
- label: row[f.db.label],
437
- }))
438
- : [];
439
- field.setValue(values);
440
- }
441
- else {
442
- 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}`);
443
- const values = Array.isArray(_rows)
444
- ? _rows.map((row) => ({
445
- value: row[f.destinationDb.selectIdentifier],
446
- label: f.options?.find((opt) => opt.value?.toString() ===
447
- row[f.destinationDb.selectIdentifier]?.toString())?.label ?? '',
448
- }))
449
- : [];
450
- field.setValue(values);
451
- }
452
- }
453
- else if (f.destinationDb && field.type === 'tags') {
454
- 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}`);
455
- const tags = Array.isArray(_rows)
456
- ? _rows.map((row) => row[f.destinationDb.selectIdentifier])
457
- : [];
458
- field.setValue(tags.join(','));
459
- }
460
- else if (field.type === 'date_range' && typeof f.setRangeValues === 'function') {
461
- f.setRangeValues(localeRow ? (localeRow[f.startName] ?? null) : null, localeRow ? (localeRow[f.endName] ?? null) : null);
462
- }
463
- else {
464
- // Simple field: override from locale row
465
- field.setValue(localeRow ? (localeRow[field.name] ?? null) : null);
466
- }
467
- }
468
- }
469
- }
470
- }
471
- // Show the locale switcher when multiple locales are configured AND either:
472
- // - the section has localized fields (production case), or
473
- // - the app is in development mode (so devs can preview the switcher
474
- // while wiring up localization on a section).
475
- // The developer note is rendered in dev-mode only, when the switcher is
476
- // visible but the section itself has no localized content yet.
477
- const localeSwitcherEnabled = localeResult.availableLocales.length > 1 &&
478
- (hasLocalizedContent === true || process.env.NODE_ENV === 'development');
479
- const developerNoteEnabled = localeSwitcherEnabled && hasLocalizedContent === false;
480
- localizationData = {
481
- defaultLocale: localeResult.defaultLocale,
482
- currentLocale: localeResult.resolvedLocale ?? localeResult.defaultLocale,
483
- existingTranslations,
484
- locales: localeResult.availableLocales,
485
- localeSwitcherEnabled,
486
- developerNoteEnabled,
487
- };
488
- }
489
- return {
490
- section: {
491
- name: sectionInfo.name,
492
- title: resolvedTitle,
493
- gallery: gallery,
494
- variants: sectionInfo.variants,
495
- configFile: sectionInfo.configFile,
496
- },
497
- inputGroups: await fieldsFactory.getGroupedFields(),
498
- localization: localizationData,
499
- };
500
- }
501
- catch (err) {
502
- const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
503
- console.error('Error creating section page', err);
504
- return {
505
- error: {
506
- message: `Failed to create section page: ${errorMessage}`,
507
- },
508
- };
509
- }
510
- };
511
- export const deleteSectionItem = async (session, sectionName, sectionItemId, recursive, requestMetadata) => {
512
- /**
513
- * Convert the section item id to string
514
- */
515
- sectionItemId = sectionItemId.toString();
516
- try {
517
- const _s = (await SectionFactory.getSectionForAdmin({
518
- name: sectionName,
519
- admin: {
520
- id: session.user.id,
521
- requiredRole: 'D',
522
- },
523
- }));
524
- const section = _s?.build();
525
- if (!section) {
526
- return {
527
- error: {
528
- message: getString('sectionNotFound', session.user.language),
529
- },
530
- };
531
- }
532
- const tableName = section.db.table;
533
- const identifierFieldName = section.db.identifier.name;
534
- const columns = await MysqlTableChecker.getColumns(tableName);
535
- if (!columns.includes(identifierFieldName)) {
536
- return {
537
- error: {
538
- message: getString('sectionTableIdentifierNotFound', session.user.language),
539
- },
540
- };
541
- }
542
- /**
543
- * Get the section item from the table
544
- */
545
- const [_v, _f] = await db.execute(sql `select * from ${sql.raw(tableName)} where ${sql.raw(identifierFieldName)} = ${sectionItemId} limit 1`);
546
- // @ts-ignore
547
- // Bug: this is a bug in drizzle-orm/mysql2
548
- const sectionItemRow = _v[0];
549
- /**
550
- * Run beforeDelete hook before the actual deletion
551
- */
552
- if (section.hooks?.beforeDelete) {
553
- const hook = section.hooks.beforeDelete;
554
- const handler = typeof hook === 'function' ? hook : hook.handler;
555
- await handler({
556
- itemId: sectionItemId,
557
- values: sectionItemRow ?? {},
558
- section: section,
559
- });
560
- }
561
- /**
562
- * Cascade delete translations from the locales table.
563
- * Use deleteLocaleTranslation per locale to ensure localized files are cleaned up from disk.
564
- */
565
- const cmsConfig = await getCMSConfig();
566
- if (cmsConfig.localization?.enabled && sectionHasLocalizedContent(section)) {
567
- const localesTableName = section.localesTableName;
568
- const localeColumns = await MysqlTableChecker.getColumns(localesTableName);
569
- if (localeColumns.length > 0) {
570
- const [localeRows] = await db.execute(sql `SELECT locale FROM \`${sql.raw(localesTableName)}\` WHERE parent_id = ${sectionItemId}`);
571
- const locales = localeRows.map((r) => r.locale);
572
- for (const locale of locales) {
573
- const result = await deleteLocaleTranslation(session, sectionName, sectionItemId, locale);
574
- if (result && typeof result === 'object' && 'error' in result) {
575
- return result;
576
- }
577
- }
578
- }
579
- }
580
- /**
581
- * Grab the file fields to delete the files
582
- */
583
- const fileFieldConfigs = section.fieldConfigs.filter((fieldConfig) => fieldConfig.type === 'document' || fieldConfig.type === 'photo' || fieldConfig.type === 'video');
584
- const uploadsFolder = cmsConfig.media.upload.path;
585
- if (sectionItemRow) {
586
- for (const field of fileFieldConfigs) {
587
- // @ts-ignore
588
- const value = sectionItemRow[field.name];
589
- if (value) {
590
- if (field.type === 'document') {
591
- try {
592
- await fs.promises.unlink(path.join(uploadsFolder, '.documents', section.name, value));
593
- }
594
- catch (error) {
595
- // Log but continue - file may not exist or already deleted
596
- console.error('Error deleting document', error);
597
- }
598
- }
599
- else if (field.type === 'video') {
600
- try {
601
- await fs.promises.unlink(path.join(uploadsFolder, '.videos', section.name, value));
602
- }
603
- catch (error) {
604
- // Log but continue - file may not exist or already deleted
605
- console.error('Error deleting video', error);
606
- }
607
- }
608
- else {
609
- try {
610
- await fs.promises.unlink(path.join(uploadsFolder, '.photos', section.name, value));
611
- await fs.promises.unlink(path.join(uploadsFolder, '.thumbs', section.name, value));
612
- }
613
- catch (error) {
614
- // Log but continue - file may not exist or already deleted
615
- console.error('Error deleting photo', error);
616
- }
617
- }
618
- }
619
- }
620
- }
621
- /**
622
- * Delete the rows from the destination table if they exist
623
- */
624
- const fieldsWithDestinationDbTable = section.fieldConfigs.filter((fieldConfig) => ['select_multiple', 'select', 'tags'].includes(fieldConfig.type));
625
- for (const fieldConfig of fieldsWithDestinationDbTable) {
626
- const field = fieldConfig.build();
627
- if (field.destinationDb) {
628
- const columns = await MysqlTableChecker.getColumns(field.destinationDb.table);
629
- if (columns.includes(field.destinationDb.selectIdentifier) &&
630
- columns.includes(field.destinationDb.itemIdentifier)) {
631
- await db.execute(sql `delete from ${sql.raw(field.destinationDb.table)} where ${sql.raw(field.destinationDb.itemIdentifier)} = ${sectionItemId}`);
632
- }
633
- }
634
- }
635
- /**
636
- * Also delete the gallery photos if they exist
637
- */
638
- const gallery = await section.getGallery();
639
- if (gallery) {
640
- const { tableName, referenceIdentifierField, photoNameField, metaField } = gallery.db;
641
- const columns = await MysqlTableChecker.getColumns(tableName);
642
- if (columns.includes(photoNameField) &&
643
- columns.includes(referenceIdentifierField) &&
644
- columns.includes(metaField)) {
645
- /**
646
- * First, get the gallery photos
647
- */
648
- const [_v, _f] = await db.execute(sql `select * from \`${sql.raw(tableName)}\` where \`${sql.raw(referenceIdentifierField)}\` = ${sectionItemId}`);
649
- // @ts-ignore
650
- // Bug: this is a bug in drizzle-orm/mysql2
651
- const galleryPhotos = _v;
652
- /**
653
- * Delete the photos from disk
654
- */
655
- for (const photo of galleryPhotos) {
656
- try {
657
- await fs.promises.unlink(path.join(uploadsFolder, '.photos', section.name, photo[photoNameField]));
658
- await fs.promises.unlink(path.join(uploadsFolder, '.thumbs', section.name, photo[photoNameField]));
659
- }
660
- catch (error) {
661
- // Log but continue - file may not exist or already deleted
662
- console.error('Error deleting photo', error);
663
- }
664
- }
665
- /**
666
- * Delete the photos from the table
667
- */
668
- await db.execute(sql `DELETE FROM ${sql.raw(tableName)} WHERE ${sql.raw(referenceIdentifierField)} = ${sectionItemId}`);
669
- }
670
- }
671
- /**
672
- * Check if there are photos in the editor_photos table
673
- */
674
- const editorPhotos = await db
675
- .select()
676
- .from(EditorPhotosTable)
677
- .where(and(eq(EditorPhotosTable.section, sectionName), eq(EditorPhotosTable.itemId, sectionItemId)));
678
- if (editorPhotos.length > 0) {
679
- /**
680
- * Delete the photos from disk
681
- */
682
- for (const photo of editorPhotos) {
683
- try {
684
- await fs.promises.unlink(path.join(uploadsFolder, '.photos', photo.section, photo.name));
685
- }
686
- catch (error) {
687
- // Log but continue - file may not exist or already deleted
688
- console.error('Error deleting photo', error);
689
- }
690
- }
691
- /**
692
- * Delete the photos from the editor_photos table
693
- */
694
- await db
695
- .delete(EditorPhotosTable)
696
- .where(and(eq(EditorPhotosTable.section, sectionName), eq(EditorPhotosTable.itemId, sectionItemId)));
697
- }
698
- /**
699
- * Delete the row from the table
700
- */
701
- await db.execute(sql `delete from \`${sql.raw(tableName)}\` where \`${sql.raw(identifierFieldName)}\` = ${sectionItemId}`);
702
- /**
703
- * Run afterDelete hook after the deletion
704
- */
705
- if (section.hooks?.afterDelete) {
706
- const hook = section.hooks.afterDelete;
707
- const handler = typeof hook === 'function' ? hook : hook.handler;
708
- try {
709
- await handler({
710
- itemId: sectionItemId,
711
- values: sectionItemRow ?? {},
712
- section: section,
713
- });
714
- }
715
- catch (error) {
716
- console.error('afterDelete hook failed:', error);
717
- }
718
- }
719
- const headingFieldName = section.headingField.name;
720
- const entityLabel = headingFieldName && sectionItemRow
721
- ? // @ts-ignore
722
- String(sectionItemRow[headingFieldName] ?? '')
723
- : null;
724
- await recordLog({
725
- eventType: 'section.item.delete',
726
- actorId: session.user.id,
727
- actorUsername: session.user.name,
728
- entityType: 'section_item',
729
- entityId: sectionItemId,
730
- entityLabel: entityLabel?.trim() ? entityLabel : null,
731
- sectionName: section.name,
732
- metadata: {
733
- sectionType: section.type,
734
- recursive: Boolean(recursive),
735
- },
736
- requestMetadata,
737
- });
738
- return true;
739
- }
740
- catch (err) {
741
- const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
742
- console.error('Error deleting section item', err);
743
- return {
744
- error: {
745
- message: `Failed to delete section item: ${errorMessage}`,
746
- },
747
- };
748
- }
749
- };
750
- export const deleteLocaleTranslation = async (session, sectionName, sectionItemId, locale) => {
751
- sectionItemId = sectionItemId.toString();
752
- try {
753
- const _s = (await SectionFactory.getSectionForAdmin({
754
- name: sectionName,
755
- admin: {
756
- id: session.user.id,
757
- requiredRole: 'D',
758
- },
759
- }));
760
- const section = _s?.build();
761
- if (!section) {
762
- return {
763
- error: {
764
- message: getString('sectionNotFound', session.user.language),
765
- },
766
- };
767
- }
768
- const cmsConfig = await getCMSConfig();
769
- if (!cmsConfig.localization?.enabled || !sectionHasLocalizedContent(section)) {
770
- return {
771
- error: {
772
- message: getString('localizationNotEnabledForSection', session.user.language),
773
- },
774
- };
775
- }
776
- if (locale === cmsConfig.localization.defaultLocale) {
777
- return {
778
- error: {
779
- message: getString('cannotDeleteBaseLocaleTranslation', session.user.language),
780
- },
781
- };
782
- }
783
- const localesTableName = section.localesTableName;
784
- const localeColumns = await MysqlTableChecker.getColumns(localesTableName);
785
- if (localeColumns.length === 0) {
786
- return {
787
- error: {
788
- message: getString('localesTableDoesNotExist', session.user.language),
789
- },
790
- };
791
- }
792
- // Fetch the locale row before deleting so we can clean up files
793
- const [localeRows] = await db.execute(sql `SELECT * FROM \`${sql.raw(localesTableName)}\` WHERE parent_id = ${sectionItemId} AND locale = ${locale} LIMIT 1`);
794
- const localeRow = localeRows[0] ?? null;
795
- const beforeDeleteHook = section.hooks?.beforeDelete;
796
- if (beforeDeleteHook && typeof beforeDeleteHook === 'object' && beforeDeleteHook.runForLocales) {
797
- await beforeDeleteHook.handler({
798
- itemId: sectionItemId,
799
- values: localeRow ?? {},
800
- section: section,
801
- locale,
802
- });
803
- }
804
- // Delete files for localized photo/document/video fields
805
- if (localeRow) {
806
- const uploadsFolder = cmsConfig.media.upload.path;
807
- const localizedFileFields = section.fieldConfigs.filter((f) => f.localized === true && (f.type === 'photo' || f.type === 'document' || f.type === 'video'));
808
- for (const field of localizedFileFields) {
809
- const value = localeRow[field.name];
810
- if (!value)
811
- continue;
812
- if (field.type === 'document') {
813
- try {
814
- await fs.promises.unlink(path.join(uploadsFolder, '.documents', section.name, value));
815
- }
816
- catch (error) {
817
- console.error('Error deleting document', error);
818
- }
819
- }
820
- else if (field.type === 'video') {
821
- try {
822
- await fs.promises.unlink(path.join(uploadsFolder, '.videos', section.name, value));
823
- }
824
- catch (error) {
825
- console.error('Error deleting video', error);
826
- }
827
- }
828
- else {
829
- // photo
830
- try {
831
- await fs.promises.unlink(path.join(uploadsFolder, '.photos', section.name, value));
832
- await fs.promises.unlink(path.join(uploadsFolder, '.thumbs', section.name, value));
833
- }
834
- catch (error) {
835
- console.error('Error deleting photo', error);
836
- }
837
- }
838
- }
839
- }
840
- const gallery = await section.getGallery();
841
- if (gallery?.localized) {
842
- await deleteLocalizedGalleryRows({
843
- section,
844
- gallery,
845
- sectionItemId,
846
- locale,
847
- uploadsFolder: cmsConfig.media.upload.path,
848
- });
849
- }
850
- // Delete locale-scoped editor photos for localized rich_text fields.
851
- // Uses raw SQL because the `locale` column only exists on the DB table when
852
- // localization is enabled; the Drizzle schema no longer declares it.
853
- const [editorPhotoRows] = await db.execute(sql `SELECT \`photo\` as \`name\` FROM \`editor_photos\` WHERE \`section\` = ${sectionName} AND \`item_id\` = ${sectionItemId.toString()} AND \`locale\` = ${locale}`);
854
- const editorPhotos = editorPhotoRows ?? [];
855
- if (editorPhotos.length > 0) {
856
- const uploadsFolder = cmsConfig.media.upload.path;
857
- for (const photo of editorPhotos) {
858
- try {
859
- await fs.promises.unlink(path.join(uploadsFolder, '.photos', section.name, photo.name));
860
- }
861
- catch (error) {
862
- console.error('Error deleting editor photo', error);
863
- }
864
- }
865
- await db.execute(sql `DELETE FROM \`editor_photos\` WHERE \`section\` = ${sectionName} AND \`item_id\` = ${sectionItemId.toString()} AND \`locale\` = ${locale}`);
866
- }
867
- // Delete locale-scoped junction table rows for localized select/tags fields
868
- const localizedJunctionFields = section.fieldConfigs.filter((f) => f.localized === true &&
869
- f.destinationDb &&
870
- (f.type === 'select' || f.type === 'select_multiple' || f.type === 'tags'));
871
- for (const field of localizedJunctionFields) {
872
- const destDb = field.destinationDb;
873
- if (!destDb)
874
- continue;
875
- try {
876
- await db.execute(sql `DELETE FROM \`${sql.raw(destDb.table)}\` WHERE \`${sql.raw(destDb.itemIdentifier)}\` = ${sectionItemId} AND \`locale\` = ${locale}`);
877
- }
878
- catch (error) {
879
- console.error(`Error deleting junction table rows for ${field.name}`, error);
880
- }
881
- }
882
- await db.execute(sql `DELETE FROM \`${sql.raw(localesTableName)}\` WHERE parent_id = ${sectionItemId} AND locale = ${locale}`);
883
- const afterDeleteHook = section.hooks?.afterDelete;
884
- if (afterDeleteHook && typeof afterDeleteHook === 'object' && afterDeleteHook.runForLocales) {
885
- try {
886
- await afterDeleteHook.handler({
887
- itemId: sectionItemId,
888
- values: localeRow ?? {},
889
- section: section,
890
- locale,
891
- });
892
- }
893
- catch (e) {
894
- console.error('afterDelete hook failed:', e);
895
- }
896
- }
897
- await recordLog({
898
- eventType: 'section.item.locale.delete',
899
- actorId: session.user.id,
900
- actorUsername: session.user.name,
901
- entityType: 'section_item_locale',
902
- entityId: sectionItemId,
903
- entityLabel: locale,
904
- sectionName: section.name,
905
- metadata: {
906
- locale,
907
- },
908
- });
909
- return true;
910
- }
911
- catch (err) {
912
- const errorMessage = err instanceof Error ? err.message : getString('unknownErrorOccurred', session.user.language);
913
- console.error('Error deleting locale translation', err);
914
- return {
915
- error: {
916
- message: getString('deleteLocaleTranslationFailed', session.user.language, { detail: errorMessage }),
917
- },
918
- };
919
- }
920
- };
921
- export const createEditPage = async (session, sectionName, sectionItemId, locale) => {
922
- /**
923
- * Generate the fields for the edit page
924
- */
925
- const fieldsFactory = new FieldFactory({
926
- type: 'edit',
927
- sectionName,
928
- session,
929
- itemId: sectionItemId,
930
- });
931
- try {
932
- const cmsConfig = await getCMSConfig();
933
- const localeResult = resolveLocale({
934
- localization: cmsConfig.localization,
935
- locale,
936
- });
937
- if (locale && !localeResult.resolvedLocale) {
938
- if (localeResult.localizationEnabled === false) {
939
- return {
940
- error: {
941
- message: getString('localizationNotEnabledForSection', session.user.language),
942
- },
943
- };
944
- }
945
- return {
946
- error: {
947
- message: getString('invalidLocale', session.user.language, {
948
- locale,
949
- locales: localeResult.availableLocales.map((l) => l.code).join(', '),
950
- }),
951
- },
952
- };
953
- }
954
- await fieldsFactory.initialize();
955
- await fieldsFactory.generateFields();
956
- if (fieldsFactory.error) {
957
- return {
958
- error: {
959
- message: fieldsFactory.errorMessage,
960
- },
961
- };
962
- }
963
- /**
964
- * Let's check for variants
965
- */
966
- /*const variants = section[0].variants
967
- const sectionVariants: { name: string }[] = []
968
-
969
- if (variants && variants.trim() !== '') {
970
- /!**
971
- * Convert to JSON
972
- *!/
973
- const variantsJson = JSON.parse(variants)
974
-
975
- /!**
976
- * Loop through the variants
977
- *!/
978
-
979
- variantsJson.forEach((variant: any) => {
980
- const variantName = variant.name
981
- const variantInfo = variant.info
982
- })
983
- }*/
984
- /**
985
- * Get the gallery photos
986
- * TODO: This is a temp implementation, will be removed once converting the gallery into a field
987
- */
988
- let galleryItems = [];
989
- const gallery = await fieldsFactory.sectionInfo?.getGallery();
990
- if (gallery) {
991
- const { tableName, referenceIdentifierField, photoNameField, metaField } = gallery.db;
992
- const columns = await MysqlTableChecker.getColumns(tableName);
993
- if (columns.includes(photoNameField) &&
994
- columns.includes(referenceIdentifierField) &&
995
- columns.includes(metaField)) {
996
- const galleryIsLocalized = gallery.localized === true && localeResult.localizationEnabled;
997
- const currentLocale = localeResult.resolvedLocale?.code;
998
- const localeFilter = galleryIsLocalized && currentLocale && columns.includes('locale')
999
- ? sql ` AND \`locale\` = ${currentLocale}`
1000
- : sql ``;
1001
- const [items] = await db.execute(sql `select * from \`${sql.raw(tableName)}\` where \`${sql.raw(referenceIdentifierField)}\` = ${sectionItemId}${localeFilter}`);
1002
- const galleryPhotos = items;
1003
- galleryPhotos?.map((item) => {
1004
- galleryItems.push({
1005
- referenceId: item[referenceIdentifierField],
1006
- photo: item[photoNameField],
1007
- meta: item[metaField],
1008
- locale: item.locale,
1009
- });
1010
- });
1011
- }
1012
- }
1013
- const sectionInfo = fieldsFactory.sectionInfo;
1014
- // Resolve localized titles using the user's language
1015
- const uiLanguage = resolveLanguage(session.user.language, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
1016
- const resolvedTitle = {
1017
- section: resolveMultilingualString(sectionInfo.title.section, uiLanguage, cmsConfig.i18n.fallbackLanguage),
1018
- singular: resolveMultilingualString(sectionInfo.title.singular, uiLanguage, cmsConfig.i18n.fallbackLanguage),
1019
- plural: resolveMultilingualString(sectionInfo.title.plural, uiLanguage, cmsConfig.i18n.fallbackLanguage),
1020
- };
1021
- /**
1022
- * Fetch localization metadata for sections with localized fields
1023
- */
1024
- let localizationData = null;
1025
- if (localeResult.localizationEnabled) {
1026
- let existingTranslations = [];
1027
- const hasLocalizedContent = sectionHasLocalizedContent(sectionInfo);
1028
- if (hasLocalizedContent) {
1029
- const localesTableName = sectionInfo.localesTableName;
1030
- const localeColumns = await MysqlTableChecker.getColumns(localesTableName);
1031
- const localeTableExists = localeColumns.length > 0;
1032
- if (localeTableExists) {
1033
- const [rows] = await db.execute(sql `SELECT locale FROM \`${sql.raw(localesTableName)}\` WHERE parent_id = ${sectionItemId}`);
1034
- existingTranslations = rows.map((r) => r.locale);
1035
- // Override localized field values with locale-specific data
1036
- if (locale && localeResult.resolvedLocale && localeResult.isDefault === false) {
1037
- const [localeRows] = await db.execute(sql `SELECT * FROM \`${sql.raw(localesTableName)}\` WHERE parent_id = ${sectionItemId} AND locale = ${localeResult.resolvedLocale.code} LIMIT 1`);
1038
- const localeRow = localeRows[0] ?? null;
1039
- for (const field of sectionInfo.fields) {
1040
- if (!field.localized)
1041
- continue;
1042
- // For select/tags with destinationDb, fetch locale-scoped junction values
1043
- const f = field;
1044
- if (f.destinationDb && (field.type === 'select' || field.type === 'select_multiple')) {
1045
- if (f.db) {
1046
- 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}`);
1047
- const values = Array.isArray(_rows)
1048
- ? _rows.map((row) => ({
1049
- value: row[f.destinationDb.selectIdentifier],
1050
- label: row[f.db.label],
1051
- }))
1052
- : [];
1053
- field.setValue(values);
1054
- }
1055
- else {
1056
- 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}`);
1057
- const values = Array.isArray(_rows)
1058
- ? _rows.map((row) => ({
1059
- value: row[f.destinationDb.selectIdentifier],
1060
- label: f.options?.find((opt) => opt.value?.toString() ===
1061
- row[f.destinationDb.selectIdentifier]?.toString())?.label ?? '',
1062
- }))
1063
- : [];
1064
- field.setValue(values);
1065
- }
1066
- }
1067
- else if (f.destinationDb && field.type === 'tags') {
1068
- 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}`);
1069
- const tags = Array.isArray(_rows)
1070
- ? _rows.map((row) => row[f.destinationDb.selectIdentifier])
1071
- : [];
1072
- field.setValue(tags.join(','));
1073
- }
1074
- else if (field.type === 'date_range' && typeof f.setRangeValues === 'function') {
1075
- f.setRangeValues(localeRow ? (localeRow[f.startName] ?? null) : null, localeRow ? (localeRow[f.endName] ?? null) : null);
1076
- }
1077
- else {
1078
- // Simple field: override from locale row
1079
- field.setValue(localeRow ? (localeRow[field.name] ?? null) : null);
1080
- }
1081
- }
1082
- }
1083
- }
1084
- }
1085
- // Show the locale switcher when multiple locales are configured AND either:
1086
- // - the section has localized fields (production case), or
1087
- // - the app is in development mode (so devs can preview the switcher
1088
- // while wiring up localization on a section).
1089
- // The developer note is rendered in dev-mode only, when the switcher is
1090
- // visible but the section itself has no localized content yet.
1091
- const localeSwitcherEnabled = localeResult.availableLocales.length > 1 &&
1092
- (hasLocalizedContent === true || process.env.NODE_ENV === 'development');
1093
- const developerNoteEnabled = localeSwitcherEnabled && hasLocalizedContent === false;
1094
- localizationData = {
1095
- defaultLocale: localeResult.defaultLocale,
1096
- currentLocale: localeResult.resolvedLocale ?? localeResult.defaultLocale,
1097
- existingTranslations,
1098
- locales: localeResult.availableLocales,
1099
- localeSwitcherEnabled,
1100
- developerNoteEnabled,
1101
- };
1102
- }
1103
- return {
1104
- section: {
1105
- name: sectionInfo.name,
1106
- title: resolvedTitle,
1107
- gallery: gallery,
1108
- variants: sectionInfo.variants,
1109
- configFile: sectionInfo.configFile,
1110
- },
1111
- inputGroups: await fieldsFactory.getGroupedFields(),
1112
- gallery: galleryItems,
1113
- localization: localizationData,
1114
- };
1115
- }
1116
- catch (error) {
1117
- const errorMessage = error?.errorMessage || (error instanceof Error ? error.message : 'Unknown error occurred');
1118
- console.error('Error creating edit page', error);
1119
- return {
1120
- error: {
1121
- message: errorMessage,
1122
- },
1123
- };
1124
- }
1125
- };
1126
- export const createNewPage = async (session, sectionName) => {
1127
- try {
1128
- const fieldsFactory = new FieldFactory({
1129
- type: 'new',
1130
- sectionName,
1131
- session,
1132
- });
1133
- await fieldsFactory.initialize();
1134
- if (fieldsFactory.error) {
1135
- return {
1136
- error: {
1137
- message: fieldsFactory.errorMessage,
1138
- },
1139
- };
1140
- }
1141
- try {
1142
- await fieldsFactory.generateFields();
1143
- }
1144
- catch (err) {
1145
- // console.error('Error generating fields', err)
1146
- return {
1147
- error: {
1148
- message: err.message,
1149
- },
1150
- };
1151
- }
1152
- /**
1153
- * Let's check for variants
1154
- */
1155
- /*const variants = section[0].variants
1156
- const sectionVariants: { name: string }[] = []
1157
-
1158
- if (variants && variants.trim() !== '') {
1159
- /!**
1160
- * Convert to JSON
1161
- *!/
1162
- const variantsJson = JSON.parse(variants)
1163
-
1164
- /!**
1165
- * Loop through the variants
1166
- *!/
1167
-
1168
- variantsJson.forEach((variant: any) => {
1169
- const variantName = variant.name
1170
- const variantInfo = variant.info
1171
- })
1172
- }*/
1173
- const sectionInfo = fieldsFactory.sectionInfo;
1174
- const gallery = await sectionInfo.getGallery();
1175
- // Get the locale for resolving localized titles
1176
- const cmsConfig = await getCMSConfig();
1177
- const language = resolveLanguage(session.user.language, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
1178
- const resolvedTitle = {
1179
- section: resolveMultilingualString(sectionInfo.title.section, language, cmsConfig.i18n.fallbackLanguage),
1180
- singular: resolveMultilingualString(sectionInfo.title.singular, language, cmsConfig.i18n.fallbackLanguage),
1181
- plural: resolveMultilingualString(sectionInfo.title.plural, language, cmsConfig.i18n.fallbackLanguage),
1182
- };
1183
- const defaultLocale = cmsConfig.localization?.enabled
1184
- ? (cmsConfig.localization.locales.find((l) => l.code === cmsConfig.localization.defaultLocale) ?? null)
1185
- : null;
1186
- return {
1187
- section: {
1188
- name: sectionInfo.name,
1189
- title: resolvedTitle,
1190
- gallery: gallery,
1191
- variants: sectionInfo.variants,
1192
- configFile: sectionInfo.configFile,
1193
- },
1194
- inputGroups: await fieldsFactory.getGroupedFields(),
1195
- defaultLocale,
1196
- };
1197
- }
1198
- catch (err) {
1199
- const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
1200
- console.error('Error creating new page', err);
1201
- return {
1202
- error: {
1203
- message: errorMessage,
1204
- },
1205
- };
1206
- }
1207
- };
1208
- export const getCategorySectionChildren = async ({ session, id, sectionName, level, }) => {
1209
- /**
1210
- * First, level up to get the children (next level)
1211
- */
1212
- level++;
1213
- // Let's fetch the section items and admin privileges for the section
1214
- const _s = (await SectionFactory.getSectionForAdmin({
1215
- name: sectionName,
1216
- admin: {
1217
- id: session.user.id,
1218
- },
1219
- }));
1220
- const section = _s?.build();
1221
- if (!section) {
1222
- return {
1223
- error: {
1224
- message: getString('sectionNotFound', session.user.language),
1225
- },
1226
- };
1227
- }
1228
- /**
1229
- * Check if new level is allowed in the category section depth
1230
- */
1231
- if (level > (section.depth ?? 1)) {
1232
- /**
1233
- * This is the last level, return an empty array
1234
- */
1235
- return {
1236
- options: null,
1237
- parentId: id,
1238
- level: level,
1239
- };
1240
- }
1241
- /**
1242
- * Let's get the options for the select input
1243
- */
1244
- 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`;
1245
- /**
1246
- * Get the options from the table
1247
- */
1248
- const selectOptionsRows = await db.execute(selectStatement);
1249
- const rows = selectOptionsRows[0];
1250
- return {
1251
- options: rows.map((row) => {
1252
- return {
1253
- value: row[section.db.identifier.name],
1254
- label: row[section.headingField.name],
1255
- };
1256
- }),
1257
- parentId: id,
1258
- level: level++,
1259
- };
1260
- };
1261
- export const getCategorySection = async (session, sectionName) => {
1262
- // Let's fetch the section items and admin privileges for the section
1263
- const section = (await SectionFactory.getSectionForAdmin({
1264
- name: sectionName,
1265
- admin: {
1266
- id: session.user.id,
1267
- },
1268
- }));
1269
- if (!section) {
1270
- return {
1271
- error: {
1272
- message: getString('sectionNotFound', session.user.language),
1273
- },
1274
- };
1275
- }
1276
- const tableName = section.db.table;
1277
- /**
1278
- * Create a select field config for the category section
1279
- */
1280
- const s = new SelectField({
1281
- name: section.name,
1282
- label: section.title.section,
1283
- required: false,
1284
- order: 1,
1285
- section: section,
1286
- destinationDb: undefined,
1287
- });
1288
- /**
1289
- * Build the select field to get the options
1290
- * from database and do all the necessary checks
1291
- */
1292
- await s.build();
1293
- /**
1294
- * Set the value to undefined
1295
- */
1296
- s.setValue(undefined);
1297
- // Get the locale for resolving localized titles (label must be a string for React)
1298
- const cmsConfig = await getCMSConfig();
1299
- const language = resolveLanguage(session.user.language, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
1300
- const resolvedTitle = {
1301
- section: resolveMultilingualString(section.title.section, language, cmsConfig.i18n.fallbackLanguage),
1302
- singular: resolveMultilingualString(section.title.singular, language, cmsConfig.i18n.fallbackLanguage),
1303
- plural: resolveMultilingualString(section.title.plural, language, cmsConfig.i18n.fallbackLanguage),
1304
- };
1305
- const categorySectionSelect = {
1306
- options: s.options,
1307
- required: s.required,
1308
- name: s.name,
1309
- label: resolvedTitle.section,
1310
- value: s.value,
1311
- parentId: undefined,
1312
- level: 1,
1313
- depth: section.depth,
1314
- sectionName: section.name,
1315
- allowRecursiveDelete: section.allowRecursiveDelete,
1316
- };
1317
- return {
1318
- section: {
1319
- tableName: tableName,
1320
- sectionName: section.name,
1321
- title: resolvedTitle,
1322
- },
1323
- data: categorySectionSelect,
1324
- // publisher: privileges.publisher,
1325
- };
1326
- };
1327
- export const getBrowsePage = async (session, sectionName, page = 1, q) => {
1328
- // Let's fetch the section items and admin privileges for the section
1329
- const _s = (await SectionFactory.getSectionForAdmin({
1330
- name: sectionName,
1331
- admin: {
1332
- id: session.user.id,
1333
- },
1334
- }));
1335
- const section = _s?.build();
1336
- if (!section) {
1337
- return {
1338
- error: {
1339
- message: getString('sectionNotFound', session.user.language),
1340
- },
1341
- };
1342
- }
1343
- const limit = 12;
1344
- const offset = (page - 1) * limit;
1345
- const tableName = section.db.table;
1346
- const orderByFieldName = section.db.orderByField?.name || 'created_at';
1347
- const sqlChunks = [];
1348
- const totalRowsSqlChunks = [];
1349
- const sectionSearch = section.search?.searchFields ? section.search?.searchFields.length > 0 : false;
1350
- // Check if we need to JOIN _locales for search
1351
- const cmsConfig = await getCMSConfig();
1352
- const hasLocalization = !!cmsConfig.localization?.enabled;
1353
- const hasLocalizedSearchFields = hasLocalization && q && q.trim().length > 0 && section.search?.searchFields?.some((f) => f.localized);
1354
- const localesTable = hasLocalizedSearchFields ? section.localesTableName : null;
1355
- if (localesTable) {
1356
- // Use DISTINCT to avoid duplicate rows from the LEFT JOIN
1357
- 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}\``)}`);
1358
- 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}\``)}`);
1359
- }
1360
- else {
1361
- sqlChunks.push(sql `select * from ${sql.raw(`\`${tableName}\``)}`);
1362
- totalRowsSqlChunks.push(sql `select count(*) as total from ${sql.raw(`\`${tableName}\``)}`);
1363
- }
1364
- if (q && q.trim().length > 0) {
1365
- if (section.search?.searchFields.length) {
1366
- const whereParts = [];
1367
- for (const field of section.search.searchFields) {
1368
- // Search in main table
1369
- whereParts.push(sql `${sql.raw(`\`${tableName}\``)}.${sql.raw(`\`${field.name}\``)} LIKE CONCAT('%',${sql `${q.trim()}`},'%')`);
1370
- // Also search in _locales table for localized fields
1371
- if (localesTable && field.localized) {
1372
- whereParts.push(sql `${sql.raw(`\`${localesTable}\``)}.${sql.raw(`\`${field.name}\``)} LIKE CONCAT('%',${sql `${q.trim()}`},'%')`);
1373
- }
1374
- }
1375
- sqlChunks.push(sql `where`);
1376
- sqlChunks.push(sql.join(whereParts, sql ` or `));
1377
- totalRowsSqlChunks.push(sql `where`);
1378
- totalRowsSqlChunks.push(sql.join(whereParts, sql ` or `));
1379
- }
1380
- }
1381
- sqlChunks.push(sql `ORDER BY ${sql.raw(`\`${tableName}\``)}.${sql.raw(`\`${orderByFieldName}\``)} DESC LIMIT ${limit} OFFSET ${offset}`);
1382
- const finalSql = sql.join(sqlChunks, sql.raw(' '));
1383
- const totalRowsSql = sql.join(totalRowsSqlChunks, sql.raw(' '));
1384
- // Now, let's get the section items from the table
1385
- const sectionItems = await db.execute(finalSql);
1386
- const totalCountResult = await db.execute(totalRowsSql);
1387
- const totalCountRows = totalCountResult[0];
1388
- const rows = sectionItems[0];
1389
- // Resolve localized section title for the browse page header
1390
- const language = resolveLanguage(session.user.language, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
1391
- const resolvedTitle = resolveMultilingualString(section.title.section, language, cmsConfig.i18n.fallbackLanguage);
1392
- return {
1393
- section: {
1394
- // tableName: tableName,
1395
- // headingField: section.headingField.name,
1396
- // identifierField: section.db.identifier.name,
1397
- // coverPhotoField: section.coverPhotoField?.name,
1398
- hasSearch: sectionSearch,
1399
- name: section.name,
1400
- title: resolvedTitle,
1401
- },
1402
- items: rows.map((row) => {
1403
- // Custom browse page implementation: if browse.fields is configured,
1404
- // return the specified fields plus default fields (id, headingTitle, coverPhoto, createdAt, createdBy, permission).
1405
- // NOTE: This is for custom browse page implementations only.
1406
- // The default CMS browse page component uses the fallback fields below.
1407
- if (section.browse?.fields && section.browse.fields.length > 0) {
1408
- const item = {
1409
- // Always include default fields
1410
- id: row[section.db.identifier.name],
1411
- headingTitle: row[section.headingField.name],
1412
- coverPhoto: section.coverPhotoField ? row[section.coverPhotoField?.name] : null,
1413
- createdAt: row['created_at'],
1414
- createdBy: row['created_by'],
1415
- permission: row.permission,
1416
- };
1417
- // Add configured browse fields
1418
- for (const field of section.browse.fields) {
1419
- item[field.name] = row[field.name] ?? null;
1420
- }
1421
- return item;
1422
- }
1423
- // Default browse page fields (used by the standard CMS browse page)
1424
- return {
1425
- id: row[section.db.identifier.name],
1426
- headingTitle: row[section.headingField.name],
1427
- coverPhoto: section.coverPhotoField ? row[section.coverPhotoField?.name] : null,
1428
- createdAt: row['created_at'],
1429
- createdBy: row['created_by'],
1430
- permission: row.permission,
1431
- };
1432
- }),
1433
- totalCount: totalCountRows[0].total,
1434
- };
1435
- };
1436
- export const getSidebar = async (session) => {
1437
- // Let's get simple, has_items and categorized sections from the sections table
1438
- const { simple, has_items, category, fixed } = await SectionFactory.getSectionsForAdmin({
1439
- admin: { id: session.user.id },
1440
- });
1441
- const pluginRoutes = await getPluginRoutes();
1442
- const privilegeSet = await getAdminPrivileges(session.user.id);
1443
- // Get config and check for dashboard override
1444
- const cmsConfig = await getCMSConfig();
1445
- const dashboardOverridePath = cmsConfig.dashboard?.override;
1446
- // Include dashboard override plugin even without explicit privilege
1447
- const allowedPluginRoutes = pluginRoutes.filter(({ pluginName, path }) => privilegeSet.has(pluginName) || path === dashboardOverridePath);
1448
- // Get the locale for resolving localized titles
1449
- const language = resolveLanguage(session.user.language, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
1450
- // Let's loop through the sections and add path, icon and title to each section item
1451
- const simpleSectionItems = simple.map((section) => {
1452
- return {
1453
- title: resolveMultilingualString(section.title, language, cmsConfig.i18n.fallbackLanguage),
1454
- path: `/section/${section.name}`,
1455
- icon: section.icon,
1456
- };
1457
- });
1458
- const hasItemsSectionItems = has_items.map((section) => {
1459
- return {
1460
- title: resolveMultilingualString(section.title.section, language, cmsConfig.i18n.fallbackLanguage),
1461
- path: `/${section.name}`,
1462
- icon: section.icon,
1463
- };
1464
- });
1465
- const categorySectionsItems = category.map((section) => {
1466
- return {
1467
- title: resolveMultilingualString(section.title.section, language, cmsConfig.i18n.fallbackLanguage),
1468
- path: `/categorized/${section.name}`,
1469
- icon: section.icon,
1470
- };
1471
- });
1472
- // Filter out dashboard override from plugin sections (it becomes the dashboard entry)
1473
- const pluginSections = allowedPluginRoutes
1474
- .filter((route) => route.path !== dashboardOverridePath)
1475
- .map((route) => ({
1476
- title: resolveMultilingualString(route.title, language, cmsConfig.i18n.fallbackLanguage),
1477
- path: route.path,
1478
- icon: route.icon ?? '',
1479
- }));
1480
- const fixedSections = [
1481
- /**
1482
- * Add the dashboard section (points to override if configured)
1483
- */
1484
- {
1485
- title: getString('dashboard', session.user.language),
1486
- path: dashboardOverridePath ?? '/dashboard',
1487
- icon: 'home',
1488
- },
1489
- /**
1490
- * Add the plugin sections (excluding dashboard override)
1491
- */
1492
- ...pluginSections,
1493
- ...fixed.map((section) => ({
1494
- title: getString(section.name, session.user.language),
1495
- path: `/${section.name}`,
1496
- icon: section.icon,
1497
- })),
1498
- ];
1499
- return {
1500
- fixed_sections: fixedSections,
1501
- cat_sections: categorySectionsItems,
1502
- has_items_sections: hasItemsSectionItems,
1503
- simple_sections: simpleSectionItems,
1504
- };
1505
- };
1506
- /**
1507
- * Return a stream from the disk with proper backpressure handling.
1508
- * Uses Node.js built-in Readable.toWeb() which correctly pauses/resumes
1509
- * the underlying fs stream based on consumer demand.
1510
- * @param {string} path - The location of the file
1511
- * @param options
1512
- * @returns {ReadableStream} A readable stream of the file
1513
- */
1514
- export const streamFile = async (path, options) => {
1515
- const stream = fs.createReadStream(path, options);
1516
- return Readable.toWeb(stream);
1517
- };