nextjs-cms 0.5.9 → 0.5.11

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 (258) hide show
  1. package/dist/api/axios/axiosInstance.d.ts +1 -1
  2. package/dist/api/axios/axiosInstance.js +8 -8
  3. package/dist/api/index.d.ts +855 -855
  4. package/dist/api/index.d.ts.map +1 -1
  5. package/dist/api/index.js +12 -12
  6. package/dist/api/lib/serverActions.d.ts +239 -239
  7. package/dist/api/lib/serverActions.d.ts.map +1 -1
  8. package/dist/api/lib/serverActions.js +834 -834
  9. package/dist/api/root.d.ts +828 -828
  10. package/dist/api/root.js +30 -30
  11. package/dist/api/routers/accountSettings.d.ts +60 -60
  12. package/dist/api/routers/accountSettings.js +108 -108
  13. package/dist/api/routers/admins.d.ts +105 -105
  14. package/dist/api/routers/admins.js +219 -219
  15. package/dist/api/routers/auth.d.ts +47 -47
  16. package/dist/api/routers/auth.js +25 -25
  17. package/dist/api/routers/categorySection.d.ts +103 -103
  18. package/dist/api/routers/categorySection.js +38 -38
  19. package/dist/api/routers/cmsSettings.d.ts +48 -48
  20. package/dist/api/routers/cmsSettings.js +51 -51
  21. package/dist/api/routers/cpanel.d.ts +83 -83
  22. package/dist/api/routers/cpanel.js +216 -216
  23. package/dist/api/routers/files.d.ts +47 -47
  24. package/dist/api/routers/files.js +23 -23
  25. package/dist/api/routers/gallery.d.ts +35 -35
  26. package/dist/api/routers/gallery.js +62 -62
  27. package/dist/api/routers/googleAnalytics.d.ts +30 -30
  28. package/dist/api/routers/googleAnalytics.js +7 -7
  29. package/dist/api/routers/hasItemsSection.d.ts +139 -139
  30. package/dist/api/routers/hasItemsSection.js +34 -34
  31. package/dist/api/routers/navigation.d.ts +51 -51
  32. package/dist/api/routers/navigation.js +11 -11
  33. package/dist/api/routers/simpleSection.d.ts +57 -57
  34. package/dist/api/routers/simpleSection.js +12 -12
  35. package/dist/api/trpc.d.ts +106 -106
  36. package/dist/api/trpc.js +72 -72
  37. package/dist/auth/axios/axiosInstance.d.ts +1 -1
  38. package/dist/auth/axios/axiosInstance.js +8 -8
  39. package/dist/auth/csrf.d.ts +29 -29
  40. package/dist/auth/csrf.js +76 -76
  41. package/dist/auth/hooks/index.d.ts +3 -3
  42. package/dist/auth/hooks/index.d.ts.map +1 -1
  43. package/dist/auth/hooks/index.js +3 -3
  44. package/dist/auth/hooks/useAxiosPrivate.d.ts +4 -4
  45. package/dist/auth/hooks/useAxiosPrivate.js +74 -74
  46. package/dist/auth/hooks/useRefreshToken.d.ts +6 -6
  47. package/dist/auth/hooks/useRefreshToken.js +79 -79
  48. package/dist/auth/index.d.ts +22 -22
  49. package/dist/auth/index.js +44 -44
  50. package/dist/auth/jwt.d.ts +5 -5
  51. package/dist/auth/jwt.js +25 -25
  52. package/dist/auth/lib/actions.d.ts +32 -32
  53. package/dist/auth/lib/actions.d.ts.map +1 -1
  54. package/dist/auth/lib/actions.js +209 -209
  55. package/dist/auth/lib/client.d.ts +3 -3
  56. package/dist/auth/lib/client.js +46 -46
  57. package/dist/auth/lib/index.d.ts +2 -2
  58. package/dist/auth/lib/index.d.ts.map +1 -1
  59. package/dist/auth/lib/index.js +2 -2
  60. package/dist/auth/react.d.ts +105 -105
  61. package/dist/auth/react.d.ts.map +1 -1
  62. package/dist/auth/react.js +347 -347
  63. package/dist/auth/trpc.d.ts +5 -5
  64. package/dist/auth/trpc.d.ts.map +1 -1
  65. package/dist/auth/trpc.js +81 -81
  66. package/dist/core/config/config-loader.d.ts +91 -91
  67. package/dist/core/config/config-loader.js +230 -230
  68. package/dist/core/config/index.d.ts +2 -2
  69. package/dist/core/config/index.d.ts.map +1 -1
  70. package/dist/core/config/index.js +1 -1
  71. package/dist/core/config/loader.d.ts +1 -1
  72. package/dist/core/config/loader.js +42 -42
  73. package/dist/core/db/index.d.ts +1 -1
  74. package/dist/core/db/index.d.ts.map +1 -1
  75. package/dist/core/db/index.js +1 -1
  76. package/dist/core/db/table-checker/DbTable.d.ts +5 -5
  77. package/dist/core/db/table-checker/DbTable.js +5 -5
  78. package/dist/core/db/table-checker/MysqlTable.d.ts +33 -33
  79. package/dist/core/db/table-checker/MysqlTable.d.ts.map +1 -1
  80. package/dist/core/db/table-checker/MysqlTable.js +94 -94
  81. package/dist/core/db/table-checker/index.d.ts +1 -1
  82. package/dist/core/db/table-checker/index.d.ts.map +1 -1
  83. package/dist/core/db/table-checker/index.js +1 -1
  84. package/dist/core/factories/FieldFactory.d.ts +123 -123
  85. package/dist/core/factories/FieldFactory.d.ts.map +1 -1
  86. package/dist/core/factories/FieldFactory.js +411 -411
  87. package/dist/core/factories/SectionFactory.d.ts +109 -109
  88. package/dist/core/factories/SectionFactory.d.ts.map +1 -1
  89. package/dist/core/factories/SectionFactory.js +415 -415
  90. package/dist/core/factories/index.d.ts +2 -2
  91. package/dist/core/factories/index.d.ts.map +1 -1
  92. package/dist/core/factories/index.js +2 -2
  93. package/dist/core/fields/checkbox.d.ts +62 -62
  94. package/dist/core/fields/checkbox.d.ts.map +1 -1
  95. package/dist/core/fields/checkbox.js +62 -62
  96. package/dist/core/fields/color.d.ts +83 -83
  97. package/dist/core/fields/color.d.ts.map +1 -1
  98. package/dist/core/fields/color.js +91 -91
  99. package/dist/core/fields/date.d.ts +99 -99
  100. package/dist/core/fields/date.d.ts.map +1 -1
  101. package/dist/core/fields/date.js +108 -108
  102. package/dist/core/fields/document.d.ts +179 -179
  103. package/dist/core/fields/document.d.ts.map +1 -1
  104. package/dist/core/fields/document.js +277 -277
  105. package/dist/core/fields/field-group.d.ts +17 -17
  106. package/dist/core/fields/field-group.d.ts.map +1 -1
  107. package/dist/core/fields/field-group.js +6 -6
  108. package/dist/core/fields/field.d.ts +125 -125
  109. package/dist/core/fields/field.d.ts.map +1 -1
  110. package/dist/core/fields/field.js +148 -148
  111. package/dist/core/fields/fileField.d.ts +14 -14
  112. package/dist/core/fields/fileField.d.ts.map +1 -1
  113. package/dist/core/fields/fileField.js +5 -5
  114. package/dist/core/fields/index.d.ts +64 -64
  115. package/dist/core/fields/index.d.ts.map +1 -1
  116. package/dist/core/fields/index.js +18 -18
  117. package/dist/core/fields/map.d.ts +166 -166
  118. package/dist/core/fields/map.d.ts.map +1 -1
  119. package/dist/core/fields/map.js +152 -152
  120. package/dist/core/fields/number.d.ts +185 -185
  121. package/dist/core/fields/number.d.ts.map +1 -1
  122. package/dist/core/fields/number.js +241 -241
  123. package/dist/core/fields/password.d.ts +108 -108
  124. package/dist/core/fields/password.d.ts.map +1 -1
  125. package/dist/core/fields/password.js +133 -133
  126. package/dist/core/fields/photo.d.ts +288 -288
  127. package/dist/core/fields/photo.d.ts.map +1 -1
  128. package/dist/core/fields/photo.js +410 -410
  129. package/dist/core/fields/richText.d.ts +294 -294
  130. package/dist/core/fields/richText.d.ts.map +1 -1
  131. package/dist/core/fields/richText.js +338 -338
  132. package/dist/core/fields/select.d.ts +365 -365
  133. package/dist/core/fields/select.d.ts.map +1 -1
  134. package/dist/core/fields/select.js +499 -499
  135. package/dist/core/fields/selectMultiple.d.ts +235 -235
  136. package/dist/core/fields/selectMultiple.d.ts.map +1 -1
  137. package/dist/core/fields/selectMultiple.js +417 -417
  138. package/dist/core/fields/tags.d.ts +130 -130
  139. package/dist/core/fields/tags.d.ts.map +1 -1
  140. package/dist/core/fields/tags.js +105 -105
  141. package/dist/core/fields/text.d.ts +135 -135
  142. package/dist/core/fields/text.d.ts.map +1 -1
  143. package/dist/core/fields/text.js +157 -157
  144. package/dist/core/fields/textArea.d.ts +106 -106
  145. package/dist/core/fields/textArea.d.ts.map +1 -1
  146. package/dist/core/fields/textArea.js +126 -126
  147. package/dist/core/fields/video.d.ts +147 -147
  148. package/dist/core/fields/video.d.ts.map +1 -1
  149. package/dist/core/fields/video.js +248 -248
  150. package/dist/core/helpers/entity.d.ts +7 -7
  151. package/dist/core/helpers/entity.js +27 -27
  152. package/dist/core/helpers/index.d.ts +4 -4
  153. package/dist/core/helpers/index.d.ts.map +1 -1
  154. package/dist/core/helpers/index.js +3 -3
  155. package/dist/core/index.d.ts +7 -7
  156. package/dist/core/index.d.ts.map +1 -1
  157. package/dist/core/index.js +7 -7
  158. package/dist/core/sections/category.d.ts +282 -282
  159. package/dist/core/sections/category.d.ts.map +1 -1
  160. package/dist/core/sections/category.js +147 -147
  161. package/dist/core/sections/hasItems.d.ts +631 -631
  162. package/dist/core/sections/hasItems.d.ts.map +1 -1
  163. package/dist/core/sections/hasItems.js +144 -144
  164. package/dist/core/sections/index.d.ts +4 -4
  165. package/dist/core/sections/index.d.ts.map +1 -1
  166. package/dist/core/sections/index.js +4 -4
  167. package/dist/core/sections/section.d.ts +225 -225
  168. package/dist/core/sections/section.d.ts.map +1 -1
  169. package/dist/core/sections/section.js +341 -341
  170. package/dist/core/sections/simple.d.ts +98 -98
  171. package/dist/core/sections/simple.d.ts.map +1 -1
  172. package/dist/core/sections/simple.js +95 -95
  173. package/dist/core/security/dom.d.ts +10 -10
  174. package/dist/core/security/dom.js +92 -92
  175. package/dist/core/submit/ItemEditSubmit.d.ts +75 -75
  176. package/dist/core/submit/ItemEditSubmit.js +186 -186
  177. package/dist/core/submit/NewItemSubmit.d.ts +13 -13
  178. package/dist/core/submit/NewItemSubmit.js +93 -93
  179. package/dist/core/submit/SimpleSectionSubmit.d.ts +12 -12
  180. package/dist/core/submit/SimpleSectionSubmit.js +93 -93
  181. package/dist/core/submit/index.d.ts +4 -4
  182. package/dist/core/submit/index.js +4 -4
  183. package/dist/core/submit/submit.d.ts +115 -115
  184. package/dist/core/submit/submit.js +479 -479
  185. package/dist/core/types/index.d.ts +279 -279
  186. package/dist/core/types/index.d.ts.map +1 -1
  187. package/dist/core/types/index.js +1 -1
  188. package/dist/db/client.d.ts +8 -8
  189. package/dist/db/client.d.ts.map +1 -1
  190. package/dist/db/client.js +19 -19
  191. package/dist/db/config.d.ts +5 -5
  192. package/dist/db/config.js +22 -22
  193. package/dist/db/drizzle.config.d.ts +5 -5
  194. package/dist/db/drizzle.config.js +18 -18
  195. package/dist/db/index.d.ts +2 -2
  196. package/dist/db/index.js +3 -3
  197. package/dist/db/schema.d.ts +638 -638
  198. package/dist/db/schema.js +73 -73
  199. package/dist/index.d.ts +7 -7
  200. package/dist/index.d.ts.map +1 -1
  201. package/dist/index.js +7 -7
  202. package/dist/translations/index.d.ts +2 -2
  203. package/dist/translations/index.js +15 -15
  204. package/dist/utils/CpanelApi.d.ts +24 -24
  205. package/dist/utils/CpanelApi.js +64 -64
  206. package/dist/utils/constants.d.ts +13 -13
  207. package/dist/utils/constants.js +61 -61
  208. package/dist/utils/index.d.ts +4 -4
  209. package/dist/utils/index.d.ts.map +1 -1
  210. package/dist/utils/index.js +4 -4
  211. package/dist/utils/utils.d.ts +59 -59
  212. package/dist/utils/utils.js +132 -132
  213. package/dist/validators/checkbox.d.ts +3 -3
  214. package/dist/validators/checkbox.d.ts.map +1 -1
  215. package/dist/validators/checkbox.js +12 -12
  216. package/dist/validators/color.d.ts +3 -3
  217. package/dist/validators/color.d.ts.map +1 -1
  218. package/dist/validators/color.js +7 -7
  219. package/dist/validators/date.d.ts +3 -3
  220. package/dist/validators/date.d.ts.map +1 -1
  221. package/dist/validators/date.js +5 -5
  222. package/dist/validators/document.d.ts +3 -3
  223. package/dist/validators/document.d.ts.map +1 -1
  224. package/dist/validators/document.js +57 -57
  225. package/dist/validators/index.d.ts +14 -14
  226. package/dist/validators/index.d.ts.map +1 -1
  227. package/dist/validators/index.js +14 -14
  228. package/dist/validators/map.d.ts +3 -3
  229. package/dist/validators/map.d.ts.map +1 -1
  230. package/dist/validators/map.js +5 -5
  231. package/dist/validators/number.d.ts +3 -3
  232. package/dist/validators/number.d.ts.map +1 -1
  233. package/dist/validators/number.js +20 -20
  234. package/dist/validators/password.d.ts +3 -3
  235. package/dist/validators/password.d.ts.map +1 -1
  236. package/dist/validators/password.js +11 -11
  237. package/dist/validators/photo.d.ts +3 -3
  238. package/dist/validators/photo.d.ts.map +1 -1
  239. package/dist/validators/photo.js +100 -100
  240. package/dist/validators/richText.d.ts +3 -3
  241. package/dist/validators/richText.d.ts.map +1 -1
  242. package/dist/validators/richText.js +8 -8
  243. package/dist/validators/select-multiple.d.ts +9 -9
  244. package/dist/validators/select-multiple.d.ts.map +1 -1
  245. package/dist/validators/select-multiple.js +20 -20
  246. package/dist/validators/select.d.ts +3 -3
  247. package/dist/validators/select.d.ts.map +1 -1
  248. package/dist/validators/select.js +5 -5
  249. package/dist/validators/text.d.ts +3 -3
  250. package/dist/validators/text.d.ts.map +1 -1
  251. package/dist/validators/text.js +7 -7
  252. package/dist/validators/textarea.d.ts +3 -3
  253. package/dist/validators/textarea.d.ts.map +1 -1
  254. package/dist/validators/textarea.js +7 -7
  255. package/dist/validators/video.d.ts +3 -3
  256. package/dist/validators/video.d.ts.map +1 -1
  257. package/dist/validators/video.js +57 -57
  258. package/package.json +4 -5
@@ -1,834 +1,834 @@
1
- // import 'server-only'
2
- import { db } from "../../db/client.js";
3
- import { AdminPrivilegesTable, AdminsTable, EditorPhotosTable } from "../../db/schema.js";
4
- import { and, eq, sql } from 'drizzle-orm';
5
- import { TRPCError } from '@trpc/server';
6
- import { FieldFactory, SectionFactory } from "../../core/factories.js";
7
- import { SelectField } from "../../core/fields.js";
8
- import { MysqlTableChecker } from "../../core/db.js";
9
- import fs from 'fs';
10
- import path from 'path';
11
- import { sanitizeFileName, sanitizeFolderOrFileName } from "../../utils.js";
12
- import sharp from 'sharp';
13
- import { readChunk } from 'read-chunk';
14
- import { fileTypeFromBuffer } from 'file-type';
15
- import { getCMSConfig } from "../../core/config.js";
16
- import through2 from 'through2';
17
- const uploadsFolder = getCMSConfig().files.upload.uploadPath;
18
- export const getDocument = async (session, input) => {
19
- const { name, sectionName, fieldName } = input;
20
- // Sanitize the inputs
21
- const sanitizedFolder = sanitizeFolderOrFileName(sectionName);
22
- const sanitizedName = sanitizeFileName(name);
23
- /**
24
- * Check the section and the field name, and get the allowed extensions,
25
- * while also checking if the user has access to the section
26
- */
27
- const section = await SectionFactory.getSectionForAdmin({
28
- name: sanitizedFolder,
29
- admin: { id: session.user.id },
30
- });
31
- /**
32
- * If the check fails, throw an error
33
- */
34
- if (!section?.name) {
35
- throw new TRPCError({
36
- code: 'BAD_REQUEST',
37
- message: 'Invalid file path',
38
- });
39
- }
40
- const fieldConfig = section.fields.find((field) => field.name === fieldName);
41
- const field = fieldConfig.build();
42
- /**
43
- * If field is not found, throw an error
44
- */
45
- if (!field || !field.name || !field.extensions || field.extensions.length === 0) {
46
- throw new TRPCError({
47
- code: 'BAD_REQUEST',
48
- message: 'Invalid request',
49
- });
50
- }
51
- /**
52
- * Split the allowed extensions into an array
53
- */
54
- const documentAllowedExtensions = field.extensions;
55
- const dir = '.documents';
56
- const pathToFile = path.join(uploadsFolder, dir, sanitizedFolder, sanitizedName);
57
- /**
58
- * First, check if the file exists
59
- */
60
- if (!fs.existsSync(pathToFile)) {
61
- throw new TRPCError({
62
- code: 'BAD_REQUEST',
63
- message: 'File not found',
64
- });
65
- }
66
- /**
67
- * Read the first 4100 bytes of the file
68
- */
69
- const chunkBuffer = await readChunk(pathToFile, { length: 4100 });
70
- /**
71
- * Get the file type from the buffer
72
- */
73
- const fileType = await fileTypeFromBuffer(chunkBuffer);
74
- /**
75
- * If the file type is invalid, return an error
76
- */
77
- if (!fileType) {
78
- throw new TRPCError({
79
- code: 'BAD_REQUEST',
80
- message: 'Invalid file type',
81
- });
82
- }
83
- /**
84
- * Check if the file type is allowed
85
- */
86
- if (!documentAllowedExtensions.includes(fileType.ext)) {
87
- throw new TRPCError({
88
- code: 'BAD_REQUEST',
89
- message: 'Invalid file type',
90
- });
91
- }
92
- /**
93
- * Convert the image to webp format and return a buffer
94
- */
95
- const buffer = fs.readFileSync(pathToFile);
96
- /**
97
- * Create a base64 string from the buffer
98
- */
99
- const base64 = buffer.toString('base64');
100
- /**
101
- * Return the base64 string with the mime type
102
- */
103
- return {
104
- base64: `data:${fileType.mime};base64,${base64}`,
105
- mimeType: fileType.mime,
106
- };
107
- };
108
- /**
109
- * Helper function with proper cleanup for converting Node.js stream to Web ReadableStream
110
- * @param nodeStream
111
- * @param sharpInstance
112
- */
113
- function nodeStreamToWebStream(nodeStream, sharpInstance) {
114
- return new ReadableStream({
115
- start(controller) {
116
- nodeStream.on('data', (chunk) => {
117
- controller.enqueue(new Uint8Array(chunk));
118
- });
119
- nodeStream.on('end', () => controller.close());
120
- nodeStream.on('error', (error) => controller.error(error));
121
- },
122
- cancel() {
123
- // Proper cleanup sequence
124
- sharpInstance.destroy();
125
- nodeStream.removeAllListeners();
126
- if ('destroy' in nodeStream && typeof nodeStream.destroy === 'function') {
127
- nodeStream.destroy();
128
- }
129
- },
130
- });
131
- }
132
- export const getPhoto = async (input) => {
133
- const { name, folder, isThumb } = input;
134
- // Sanitize the inputs
135
- const sanitizedFolder = sanitizeFolderOrFileName(folder);
136
- const sanitizedName = sanitizeFileName(name);
137
- /**
138
- * Check the folder name matches a section name in the database
139
- * Notice: Maybe we don't need this check because making an sql call for each image is maybe expensive
140
- */
141
- /*const sectionQuery = await db
142
- .select()
143
- .from(SectionsTable)
144
- .where(eq(SectionsTable.sectionName, sanitizedFolder))
145
- .limit(1)
146
-
147
- if (sectionQuery.length === 0) {
148
- throw new Error('Invalid folder name')
149
- }*/
150
- const dir = isThumb ? '.thumbs' : '.photos';
151
- const pathToFile = path.join(uploadsFolder, dir, sanitizedFolder, sanitizedName);
152
- /**
153
- * Disable caching for the image to avoid unlink issues when removing the image
154
- */
155
- sharp.cache({ files: 0 });
156
- sharp.cache(false);
157
- /**
158
- * Convert the image to webp format and return a buffer
159
- */
160
- const sharpInstance = sharp(pathToFile);
161
- let webpBuffer = await sharpInstance // Load the image
162
- .toFormat('webp') // Re-encode the image to webp
163
- .withExif({}) // Strip the exif data
164
- .toBuffer(); // Return a buffer
165
- /**
166
- * Destroy the sharp instance to free it from memory
167
- */
168
- const base64String = webpBuffer.toString('base64');
169
- sharpInstance.destroy();
170
- /**
171
- * Return the base64 string with the mime type
172
- */
173
- return `data:image/webp;base64,${base64String}`;
174
- };
175
- export async function streamPhoto(input) {
176
- const { name, folder, isThumb } = input;
177
- // Sanitize the inputs
178
- const sanitizedFolder = sanitizeFolderOrFileName(folder);
179
- const sanitizedName = sanitizeFileName(name);
180
- const dir = isThumb ? '.thumbs' : '.photos';
181
- const pathToFile = path.join(uploadsFolder, dir, sanitizedFolder, sanitizedName);
182
- /**
183
- * Disable caching for the image to avoid unlink issues when removing the image
184
- */
185
- sharp.cache({ files: 0 });
186
- sharp.cache(false);
187
- // Process all images through Sharp
188
- const processedImage = sharp(pathToFile).toFormat('webp').withExif({});
189
- /**
190
- * Convert Node.js stream to Web ReadableStream
191
- * Also, add through2 for proper streaming
192
- */
193
- const nodeStream = processedImage.pipe(through2());
194
- return nodeStreamToWebStream(nodeStream, processedImage);
195
- }
196
- export const getVideo = async (input) => {
197
- throw new Error('Please use the /api/video route');
198
- };
199
- export const getAllPrivileges = async () => {
200
- const sections = await SectionFactory.getSections();
201
- // First, let's assign the static privileges
202
- const privilegesList = [];
203
- // Now, let's add the rest of the privileges to the list
204
- sections.forEach((section, index) => {
205
- privilegesList.push({
206
- title: typeof section.title === 'string' ? section.title : section.title.section,
207
- order: section.order || index,
208
- sectionName: section.name,
209
- sectionType: section.type,
210
- });
211
- });
212
- privilegesList.push({ title: 'Admins', order: 0, sectionName: 'admins', sectionType: 'static' }, { title: 'Emails', order: 1, sectionName: 'emails', sectionType: 'static' }, { title: 'Log', order: 2, sectionName: 'log', sectionType: 'static' });
213
- return privilegesList;
214
- };
215
- export const getAdminsList = async () => {
216
- const admins = await db
217
- .select({
218
- id: AdminsTable.id,
219
- username: AdminsTable.user,
220
- avatar: AdminsTable.coverphoto,
221
- })
222
- .from(AdminsTable);
223
- const roles = await db.select().from(AdminPrivilegesTable);
224
- /**
225
- * Assign the roles to the admins
226
- */
227
- const adminsWithRoles = [];
228
- admins.forEach((admin) => {
229
- const adminRoles = roles.filter((role) => role.adminId === admin.id);
230
- adminsWithRoles.push({
231
- ...admin,
232
- roles: adminRoles,
233
- });
234
- });
235
- return adminsWithRoles;
236
- };
237
- export const createSimpleSectionPage = async (session, sectionName) => {
238
- try {
239
- /**
240
- * Let's fetch the section information
241
- */
242
- const fieldsFactory = new FieldFactory({
243
- type: 'edit',
244
- sectionName,
245
- session: session,
246
- itemId: 1, // itemId is always 1 for simple sections
247
- });
248
- await fieldsFactory.initialize();
249
- if (fieldsFactory.error) {
250
- return {
251
- error: {
252
- message: fieldsFactory.errorMessage,
253
- },
254
- };
255
- }
256
- await fieldsFactory.generateFields();
257
- /**
258
- * Let's check for variants
259
- */
260
- /*const variants = section[0].variants
261
- const sectionVariants: { name: string }[] = []
262
-
263
- if (variants && variants.trim() !== '') {
264
- /!**
265
- * Convert to JSON
266
- *!/
267
- const variantsJson = JSON.parse(variants)
268
-
269
- /!**
270
- * Loop through the variants
271
- *!/
272
-
273
- variantsJson.forEach((variant: any) => {
274
- const variantName = variant.name
275
- const variantInfo = variant.info
276
- })
277
- }*/
278
- return {
279
- section: fieldsFactory.sectionInfo,
280
- inputGroups: fieldsFactory.getGroupedFields(),
281
- };
282
- }
283
- catch (err) {
284
- const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
285
- console.error('Error creating section page', err);
286
- return {
287
- error: {
288
- message: `Failed to create section page: ${errorMessage}`,
289
- },
290
- };
291
- }
292
- };
293
- export const deleteSectionItem = async (session, sectionName, sectionItemId, recursive) => {
294
- /**
295
- * Convert the section item id to string
296
- */
297
- sectionItemId = sectionItemId.toString();
298
- try {
299
- const _s = await SectionFactory.getSectionForAdmin({
300
- name: sectionName,
301
- admin: {
302
- id: session.user.id,
303
- },
304
- });
305
- const section = _s?.build();
306
- if (!section) {
307
- return {
308
- error: {
309
- message: 'Section not found or you do not have access to it',
310
- },
311
- };
312
- }
313
- const tableName = section.db.table;
314
- const identifierFieldName = section.db.identifier.name;
315
- const columns = await MysqlTableChecker.getColumns(tableName);
316
- if (!columns.includes(identifierFieldName)) {
317
- return {
318
- error: {
319
- message: 'Section table identifier field not found',
320
- },
321
- };
322
- }
323
- /**
324
- * Get the section item from the table
325
- */
326
- const [_v, _f] = await db.execute(sql `select * from ${sql.raw(tableName)} where ${sql.raw(identifierFieldName)} = ${sectionItemId} limit 1`);
327
- // @ts-ignore
328
- // Bug: this is a bug in drizzle-orm/mysql2
329
- const sectionItemRow = _v[0];
330
- /**
331
- * Delete the row from the table
332
- */
333
- await db.execute(sql `delete from \`${sql.raw(tableName)}\` where \`${sql.raw(identifierFieldName)}\` = ${sectionItemId}`);
334
- /**
335
- * Grab the file fields to delete the files
336
- */
337
- const fileFieldConfigs = section.fieldConfigs.filter((fieldConfig) => fieldConfig.type === 'document' || fieldConfig.type === 'photo' || fieldConfig.type === 'video');
338
- for (const field of fileFieldConfigs) {
339
- // @ts-ignore
340
- const value = sectionItemRow[field.name];
341
- if (value) {
342
- if (field.type === 'document') {
343
- await fs.promises.unlink(path.join(uploadsFolder, '.documents', section.name, value));
344
- }
345
- else {
346
- try {
347
- await fs.promises.unlink(path.join(uploadsFolder, '.photos', section.name, value));
348
- await fs.promises.unlink(path.join(uploadsFolder, '.thumbs', section.name, value));
349
- }
350
- catch (error) {
351
- // Log but continue - file may not exist or already deleted
352
- console.error('Error deleting photo', error);
353
- }
354
- }
355
- }
356
- }
357
- /**
358
- * Grab the selectMultiple fields and delete the rows from the destination table
359
- */
360
- const selectMultipleFields = section.fieldConfigs.filter((fieldConfig) => fieldConfig.type === 'select_multiple');
361
- for (const fieldConfig of selectMultipleFields) {
362
- const field = fieldConfig.build();
363
- if (field.destinationDb) {
364
- const columns = await MysqlTableChecker.getColumns(field.destinationDb.table);
365
- if (columns.includes(field.destinationDb.selectIdentifier) &&
366
- columns.includes(field.destinationDb.itemIdentifier)) {
367
- await db.execute(sql `delete from ${sql.raw(field.destinationDb.table)} where ${sql.raw(field.destinationDb.itemIdentifier)} = ${sectionItemId}`);
368
- }
369
- }
370
- }
371
- /**
372
- * Also delete the gallery photos if they exist
373
- */
374
- if (section.gallery) {
375
- const { tableName, referenceIdentifierField, photoNameField, metaField } = section.gallery.db;
376
- const columns = await MysqlTableChecker.getColumns(tableName);
377
- if (columns.includes(photoNameField) &&
378
- columns.includes(referenceIdentifierField) &&
379
- columns.includes(metaField)) {
380
- /**
381
- * First, get the gallery photos
382
- */
383
- const [_v, _f] = await db.execute(sql `select * from \`${sql.raw(tableName)}\` where \`${sql.raw(referenceIdentifierField)}\` = ${sectionItemId}`);
384
- // @ts-ignore
385
- // Bug: this is a bug in drizzle-orm/mysql2
386
- const galleryPhotos = _v;
387
- /**
388
- * Delete the photos from disk
389
- */
390
- for (const photo of galleryPhotos) {
391
- try {
392
- await fs.promises.unlink(path.join(uploadsFolder, '.photos', section.name, photo[photoNameField]));
393
- await fs.promises.unlink(path.join(uploadsFolder, '.thumbs', section.name, photo[photoNameField]));
394
- }
395
- catch (error) {
396
- // Log but continue - file may not exist or already deleted
397
- console.error('Error deleting photo', error);
398
- }
399
- }
400
- /**
401
- * Delete the photos from the table
402
- */
403
- await db.execute(sql `DELETE FROM ${sql.raw(tableName)} WHERE ${sql.raw(referenceIdentifierField)} = ${sectionItemId}`);
404
- }
405
- }
406
- /**
407
- * Check if there are photos in the editor_photos table
408
- */
409
- const editorPhotos = await db
410
- .select()
411
- .from(EditorPhotosTable)
412
- .where(and(eq(EditorPhotosTable.section, sectionName), eq(EditorPhotosTable.itemId, sectionItemId)));
413
- if (editorPhotos.length > 0) {
414
- /**
415
- * Delete the photos from disk
416
- */
417
- for (const photo of editorPhotos) {
418
- try {
419
- await fs.promises.unlink(path.join(uploadsFolder, '.photos', photo.section, photo.name));
420
- }
421
- catch (error) {
422
- // Log but continue - file may not exist or already deleted
423
- console.error('Error deleting photo', error);
424
- }
425
- }
426
- /**
427
- * Delete the photos from the editor_photos table
428
- */
429
- await db
430
- .delete(EditorPhotosTable)
431
- .where(and(eq(EditorPhotosTable.section, sectionName), eq(EditorPhotosTable.itemId, sectionItemId)));
432
- }
433
- return true;
434
- }
435
- catch (err) {
436
- const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
437
- console.error('Error deleting section item', err);
438
- return {
439
- error: {
440
- message: `Failed to delete section item: ${errorMessage}`,
441
- },
442
- };
443
- }
444
- };
445
- export const createEditPage = async (session, sectionName, sectionItemId) => {
446
- /**
447
- * Generate the fields for the edit page
448
- */
449
- const fieldsFactory = new FieldFactory({
450
- type: 'edit',
451
- sectionName,
452
- session,
453
- itemId: sectionItemId,
454
- });
455
- try {
456
- await fieldsFactory.initialize();
457
- await fieldsFactory.generateFields();
458
- if (fieldsFactory.error) {
459
- return {
460
- error: {
461
- message: fieldsFactory.errorMessage,
462
- },
463
- };
464
- }
465
- /**
466
- * Let's check for variants
467
- */
468
- /*const variants = section[0].variants
469
- const sectionVariants: { name: string }[] = []
470
-
471
- if (variants && variants.trim() !== '') {
472
- /!**
473
- * Convert to JSON
474
- *!/
475
- const variantsJson = JSON.parse(variants)
476
-
477
- /!**
478
- * Loop through the variants
479
- *!/
480
-
481
- variantsJson.forEach((variant: any) => {
482
- const variantName = variant.name
483
- const variantInfo = variant.info
484
- })
485
- }*/
486
- /**
487
- * Get the gallery photos
488
- * TODO: This is a temp implementation, will be removed once converting the gallery into a field
489
- */
490
- let galleryItems = [];
491
- if (fieldsFactory.sectionInfo?.gallery) {
492
- const { tableName, referenceIdentifierField, photoNameField, metaField } = fieldsFactory.sectionInfo.gallery.db;
493
- const columns = await MysqlTableChecker.getColumns(tableName);
494
- if (columns.includes(photoNameField) &&
495
- columns.includes(referenceIdentifierField) &&
496
- columns.includes(metaField)) {
497
- const [items, fields] = await db.execute(sql `select * from \`${sql.raw(tableName)}\` where \`${sql.raw(referenceIdentifierField)}\` = ${sectionItemId}`);
498
- const galleryPhotos = items;
499
- galleryPhotos?.map((item) => {
500
- galleryItems.push({
501
- referenceId: item[referenceIdentifierField],
502
- photo: item[photoNameField],
503
- meta: item[metaField],
504
- });
505
- });
506
- }
507
- }
508
- return {
509
- section: fieldsFactory.sectionInfo,
510
- inputGroups: fieldsFactory.getGroupedFields(),
511
- gallery: galleryItems,
512
- };
513
- }
514
- catch (error) {
515
- const errorMessage = error?.errorMessage || (error instanceof Error ? error.message : 'Unknown error occurred');
516
- console.error('Error creating edit page', error);
517
- throw new TRPCError({
518
- code: 'BAD_REQUEST',
519
- message: errorMessage,
520
- });
521
- }
522
- };
523
- export const createNewPage = async (session, sectionName) => {
524
- try {
525
- const fieldsFactory = new FieldFactory({
526
- type: 'new',
527
- sectionName,
528
- session,
529
- });
530
- await fieldsFactory.initialize();
531
- if (fieldsFactory.error) {
532
- return {
533
- error: {
534
- message: fieldsFactory.errorMessage,
535
- },
536
- };
537
- }
538
- try {
539
- await fieldsFactory.generateFields();
540
- }
541
- catch (err) {
542
- // console.error('Error generating fields', err)
543
- return {
544
- error: {
545
- message: err.message,
546
- },
547
- };
548
- }
549
- /**
550
- * Let's check for variants
551
- */
552
- /*const variants = section[0].variants
553
- const sectionVariants: { name: string }[] = []
554
-
555
- if (variants && variants.trim() !== '') {
556
- /!**
557
- * Convert to JSON
558
- *!/
559
- const variantsJson = JSON.parse(variants)
560
-
561
- /!**
562
- * Loop through the variants
563
- *!/
564
-
565
- variantsJson.forEach((variant: any) => {
566
- const variantName = variant.name
567
- const variantInfo = variant.info
568
- })
569
- }*/
570
- return {
571
- section: fieldsFactory.sectionInfo,
572
- inputGroups: fieldsFactory.getGroupedFields(),
573
- };
574
- }
575
- catch (err) {
576
- const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
577
- console.error('Error creating new page', err);
578
- return {
579
- error: {
580
- message: `Failed to create new page: ${errorMessage}`,
581
- },
582
- };
583
- }
584
- };
585
- export const getCategorySectionChildren = async ({ session, id, sectionName, level, }) => {
586
- /**
587
- * First, level up to get the children (next level)
588
- */
589
- level++;
590
- // Let's fetch the section items and admin privileges for the section
591
- const _s = (await SectionFactory.getSectionForAdmin({
592
- name: sectionName,
593
- admin: {
594
- id: session.user.id,
595
- },
596
- }));
597
- const section = _s?.build();
598
- if (!section) {
599
- return {
600
- error: {
601
- message: 'Section not found or you do not have access to it',
602
- },
603
- };
604
- }
605
- /**
606
- * Check if new level is allowed in the category section depth
607
- */
608
- if (level > (section.depth ?? 1)) {
609
- /**
610
- * This is the last level, return an empty array
611
- */
612
- return {
613
- options: null,
614
- parentId: id,
615
- level: level,
616
- };
617
- }
618
- /**
619
- * Let's get the options for the select input
620
- */
621
- 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`;
622
- /**
623
- * Get the options from the table
624
- */
625
- const selectOptionsRows = await db.execute(selectStatement);
626
- const rows = selectOptionsRows[0];
627
- return {
628
- options: rows.map((row) => {
629
- return {
630
- value: row[section.db.identifier.name],
631
- label: row[section.headingField.name],
632
- };
633
- }),
634
- parentId: id,
635
- level: level++,
636
- };
637
- };
638
- export const getCategorySection = async (session, sectionName) => {
639
- // Let's fetch the section items and admin privileges for the section
640
- const section = (await SectionFactory.getSectionForAdmin({
641
- name: sectionName,
642
- admin: {
643
- id: session.user.id,
644
- },
645
- }));
646
- if (!section) {
647
- throw new TRPCError({
648
- code: 'NOT_FOUND',
649
- message: 'Section not found or you do not have access to it',
650
- });
651
- }
652
- const tableName = section.db.table;
653
- /**
654
- * Create a select field config for the category section
655
- */
656
- const s = new SelectField({
657
- name: section.name,
658
- label: section.title.section,
659
- required: false,
660
- order: 1,
661
- section: section,
662
- destinationDb: undefined,
663
- });
664
- /**
665
- * Build the select field to get the options
666
- * from database and do all the necessary checks
667
- */
668
- await s.build();
669
- /**
670
- * Set the value to undefined
671
- */
672
- s.setValue(undefined);
673
- const categorySectionSelect = {
674
- options: s.options,
675
- required: s.required,
676
- name: s.name,
677
- label: s.label,
678
- value: s.value,
679
- parentId: undefined,
680
- level: 1,
681
- depth: section.depth,
682
- sectionName: section.name,
683
- allowRecursiveDelete: section.allowRecursiveDelete,
684
- };
685
- return {
686
- section: {
687
- tableName: tableName,
688
- sectionName: section.name,
689
- title: section.title,
690
- },
691
- data: categorySectionSelect,
692
- // publisher: privileges.publisher,
693
- };
694
- };
695
- export const getBrowsePage = async (session, sectionName, page = 1, q) => {
696
- // Let's fetch the section items and admin privileges for the section
697
- const _s = (await SectionFactory.getSectionForAdmin({
698
- name: sectionName,
699
- admin: {
700
- id: session.user.id,
701
- },
702
- }));
703
- const section = _s?.build();
704
- if (!section) {
705
- return {
706
- error: {
707
- message: 'Section not found or you do not have access to it',
708
- },
709
- };
710
- }
711
- const limit = 12;
712
- const offset = (page - 1) * limit;
713
- const tableName = section.db.table;
714
- const orderByFieldName = section.db.orderByField?.name || 'created_at';
715
- const sqlChunks = [];
716
- const totalRowsSqlChunks = [];
717
- const sectionSearch = section.search?.searchFields ? section.search?.searchFields.length > 0 : false;
718
- sqlChunks.push(sql `select * from ${sql.raw(tableName)}`);
719
- totalRowsSqlChunks.push(sql `select count(*) as total from ${sql.raw(tableName)}`);
720
- if (q && q.trim().length > 0) {
721
- if (section.search?.searchFields.length) {
722
- let i = 1;
723
- sqlChunks.push(sql `where`);
724
- totalRowsSqlChunks.push(sql `where`);
725
- for (const field of section.search?.searchFields) {
726
- // sqlChunks.push(sql`${sql.raw(field.name)} like '%${q}%'`)
727
- sqlChunks.push(sql `${sql.raw(field.name)} LIKE CONCAT('%',${sql `${q.trim()}`},'%')`);
728
- totalRowsSqlChunks.push(sql `${sql.raw(field.name)} LIKE CONCAT('%',${sql `${q.trim()}`},'%')`);
729
- if (i === section.search?.searchFields.length)
730
- continue;
731
- sqlChunks.push(sql `or`);
732
- totalRowsSqlChunks.push(sql `or`);
733
- i++;
734
- }
735
- }
736
- }
737
- sqlChunks.push(sql `ORDER BY \`${sql.raw(orderByFieldName)}\` DESC LIMIT ${limit} OFFSET ${offset}`);
738
- const finalSql = sql.join(sqlChunks, sql.raw(' '));
739
- const totalRowsSql = sql.join(totalRowsSqlChunks, sql.raw(' '));
740
- // Now, let's get the section items from the table
741
- const sectionItems = await db.execute(finalSql);
742
- const totalCountResult = await db.execute(totalRowsSql);
743
- const totalCountRows = totalCountResult[0];
744
- const rows = sectionItems[0];
745
- return {
746
- section: {
747
- // tableName: tableName,
748
- // headingField: section.headingField.name,
749
- // identifierField: section.db.identifier.name,
750
- // coverPhotoField: section.coverPhotoField?.name,
751
- hasSearch: sectionSearch,
752
- name: section.name,
753
- title: section.title.section,
754
- },
755
- items: rows.map((row) => {
756
- return {
757
- id: row[section.db.identifier.name],
758
- headingTitle: row[section.headingField.name],
759
- coverPhoto: section.coverPhotoField ? row[section.coverPhotoField?.name] : null,
760
- createdAt: row['created_at'],
761
- createdBy: row['created_by'],
762
- permission: row.permission,
763
- };
764
- }),
765
- totalCount: totalCountRows[0].total,
766
- };
767
- };
768
- export const getSidebar = async (session) => {
769
- // Let's get simple, has_items and categorized sections from the sections table
770
- const { simple, has_items, category, fixed } = await SectionFactory.getSectionsForAdmin({
771
- admin: { id: session.user.id },
772
- });
773
- // Let's loop through the sections and add path, icon and title to each section item
774
- const simpleSectionItems = simple.map((section) => {
775
- return {
776
- title: section.title,
777
- path: `/section/${section.name}`,
778
- icon: section.icon,
779
- };
780
- });
781
- const hasItemsSectionItems = has_items.map((section) => {
782
- return {
783
- title: section.title.section,
784
- path: `/${section.name}`,
785
- icon: section.icon,
786
- };
787
- });
788
- const categorySectionsItems = category.map((section) => {
789
- return {
790
- title: section.title.section,
791
- path: `/categorized/${section.name}`,
792
- icon: section.icon,
793
- };
794
- });
795
- return {
796
- fixed_sections: [
797
- {
798
- title: 'Dashboard',
799
- path: '/dashboard',
800
- icon: 'home',
801
- },
802
- ...fixed.map((section) => {
803
- return {
804
- title: section,
805
- path: `/${section}`,
806
- icon: 'home',
807
- };
808
- }),
809
- ],
810
- cat_sections: categorySectionsItems,
811
- has_items_sections: hasItemsSectionItems,
812
- simple_sections: simpleSectionItems,
813
- };
814
- };
815
- /**
816
- * Return a stream from the disk
817
- * @param {string} path - The location of the file
818
- * @param options
819
- * @returns {ReadableStream} A readable stream of the file
820
- */
821
- export const streamFile = async (path, options) => {
822
- const stream = fs.createReadStream(path, options);
823
- return new ReadableStream({
824
- start(controller) {
825
- // @ts-ignore
826
- stream.on('data', (chunk) => controller.enqueue(new Uint8Array(chunk)));
827
- stream.on('end', () => controller.close());
828
- stream.on('error', (error) => controller.error(error));
829
- },
830
- cancel() {
831
- stream.destroy();
832
- },
833
- });
834
- };
1
+ // import 'server-only'
2
+ import { db } from '../../db/client.js';
3
+ import { AdminPrivilegesTable, AdminsTable, EditorPhotosTable } from '../../db/schema.js';
4
+ import { and, eq, sql } from 'drizzle-orm';
5
+ import { TRPCError } from '@trpc/server';
6
+ import { FieldFactory, SectionFactory } from '../../core/factories/index.js';
7
+ import { SelectField } from '../../core/fields/index.js';
8
+ import { MysqlTableChecker } from '../../core/db/index.js';
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+ import { sanitizeFileName, sanitizeFolderOrFileName } from '../../utils/index.js';
12
+ import sharp from 'sharp';
13
+ import { readChunk } from 'read-chunk';
14
+ import { fileTypeFromBuffer } from 'file-type';
15
+ import { getCMSConfig } from '../../core/config/index.js';
16
+ import through2 from 'through2';
17
+ const uploadsFolder = getCMSConfig().files.upload.uploadPath;
18
+ export const getDocument = async (session, input) => {
19
+ const { name, sectionName, fieldName } = input;
20
+ // Sanitize the inputs
21
+ const sanitizedFolder = sanitizeFolderOrFileName(sectionName);
22
+ const sanitizedName = sanitizeFileName(name);
23
+ /**
24
+ * Check the section and the field name, and get the allowed extensions,
25
+ * while also checking if the user has access to the section
26
+ */
27
+ const section = await SectionFactory.getSectionForAdmin({
28
+ name: sanitizedFolder,
29
+ admin: { id: session.user.id },
30
+ });
31
+ /**
32
+ * If the check fails, throw an error
33
+ */
34
+ if (!section?.name) {
35
+ throw new TRPCError({
36
+ code: 'BAD_REQUEST',
37
+ message: 'Invalid file path',
38
+ });
39
+ }
40
+ const fieldConfig = section.fields.find((field) => field.name === fieldName);
41
+ const field = fieldConfig.build();
42
+ /**
43
+ * If field is not found, throw an error
44
+ */
45
+ if (!field || !field.name || !field.extensions || field.extensions.length === 0) {
46
+ throw new TRPCError({
47
+ code: 'BAD_REQUEST',
48
+ message: 'Invalid request',
49
+ });
50
+ }
51
+ /**
52
+ * Split the allowed extensions into an array
53
+ */
54
+ const documentAllowedExtensions = field.extensions;
55
+ const dir = '.documents';
56
+ const pathToFile = path.join(uploadsFolder, dir, sanitizedFolder, sanitizedName);
57
+ /**
58
+ * First, check if the file exists
59
+ */
60
+ if (!fs.existsSync(pathToFile)) {
61
+ throw new TRPCError({
62
+ code: 'BAD_REQUEST',
63
+ message: 'File not found',
64
+ });
65
+ }
66
+ /**
67
+ * Read the first 4100 bytes of the file
68
+ */
69
+ const chunkBuffer = await readChunk(pathToFile, { length: 4100 });
70
+ /**
71
+ * Get the file type from the buffer
72
+ */
73
+ const fileType = await fileTypeFromBuffer(chunkBuffer);
74
+ /**
75
+ * If the file type is invalid, return an error
76
+ */
77
+ if (!fileType) {
78
+ throw new TRPCError({
79
+ code: 'BAD_REQUEST',
80
+ message: 'Invalid file type',
81
+ });
82
+ }
83
+ /**
84
+ * Check if the file type is allowed
85
+ */
86
+ if (!documentAllowedExtensions.includes(fileType.ext)) {
87
+ throw new TRPCError({
88
+ code: 'BAD_REQUEST',
89
+ message: 'Invalid file type',
90
+ });
91
+ }
92
+ /**
93
+ * Convert the image to webp format and return a buffer
94
+ */
95
+ const buffer = fs.readFileSync(pathToFile);
96
+ /**
97
+ * Create a base64 string from the buffer
98
+ */
99
+ const base64 = buffer.toString('base64');
100
+ /**
101
+ * Return the base64 string with the mime type
102
+ */
103
+ return {
104
+ base64: `data:${fileType.mime};base64,${base64}`,
105
+ mimeType: fileType.mime,
106
+ };
107
+ };
108
+ /**
109
+ * Helper function with proper cleanup for converting Node.js stream to Web ReadableStream
110
+ * @param nodeStream
111
+ * @param sharpInstance
112
+ */
113
+ function nodeStreamToWebStream(nodeStream, sharpInstance) {
114
+ return new ReadableStream({
115
+ start(controller) {
116
+ nodeStream.on('data', (chunk) => {
117
+ controller.enqueue(new Uint8Array(chunk));
118
+ });
119
+ nodeStream.on('end', () => controller.close());
120
+ nodeStream.on('error', (error) => controller.error(error));
121
+ },
122
+ cancel() {
123
+ // Proper cleanup sequence
124
+ sharpInstance.destroy();
125
+ nodeStream.removeAllListeners();
126
+ if ('destroy' in nodeStream && typeof nodeStream.destroy === 'function') {
127
+ nodeStream.destroy();
128
+ }
129
+ },
130
+ });
131
+ }
132
+ export const getPhoto = async (input) => {
133
+ const { name, folder, isThumb } = input;
134
+ // Sanitize the inputs
135
+ const sanitizedFolder = sanitizeFolderOrFileName(folder);
136
+ const sanitizedName = sanitizeFileName(name);
137
+ /**
138
+ * Check the folder name matches a section name in the database
139
+ * Notice: Maybe we don't need this check because making an sql call for each image is maybe expensive
140
+ */
141
+ /*const sectionQuery = await db
142
+ .select()
143
+ .from(SectionsTable)
144
+ .where(eq(SectionsTable.sectionName, sanitizedFolder))
145
+ .limit(1)
146
+
147
+ if (sectionQuery.length === 0) {
148
+ throw new Error('Invalid folder name')
149
+ }*/
150
+ const dir = isThumb ? '.thumbs' : '.photos';
151
+ const pathToFile = path.join(uploadsFolder, dir, sanitizedFolder, sanitizedName);
152
+ /**
153
+ * Disable caching for the image to avoid unlink issues when removing the image
154
+ */
155
+ sharp.cache({ files: 0 });
156
+ sharp.cache(false);
157
+ /**
158
+ * Convert the image to webp format and return a buffer
159
+ */
160
+ const sharpInstance = sharp(pathToFile);
161
+ let webpBuffer = await sharpInstance // Load the image
162
+ .toFormat('webp') // Re-encode the image to webp
163
+ .withExif({}) // Strip the exif data
164
+ .toBuffer(); // Return a buffer
165
+ /**
166
+ * Destroy the sharp instance to free it from memory
167
+ */
168
+ const base64String = webpBuffer.toString('base64');
169
+ sharpInstance.destroy();
170
+ /**
171
+ * Return the base64 string with the mime type
172
+ */
173
+ return `data:image/webp;base64,${base64String}`;
174
+ };
175
+ export async function streamPhoto(input) {
176
+ const { name, folder, isThumb } = input;
177
+ // Sanitize the inputs
178
+ const sanitizedFolder = sanitizeFolderOrFileName(folder);
179
+ const sanitizedName = sanitizeFileName(name);
180
+ const dir = isThumb ? '.thumbs' : '.photos';
181
+ const pathToFile = path.join(uploadsFolder, dir, sanitizedFolder, sanitizedName);
182
+ /**
183
+ * Disable caching for the image to avoid unlink issues when removing the image
184
+ */
185
+ sharp.cache({ files: 0 });
186
+ sharp.cache(false);
187
+ // Process all images through Sharp
188
+ const processedImage = sharp(pathToFile).toFormat('webp').withExif({});
189
+ /**
190
+ * Convert Node.js stream to Web ReadableStream
191
+ * Also, add through2 for proper streaming
192
+ */
193
+ const nodeStream = processedImage.pipe(through2());
194
+ return nodeStreamToWebStream(nodeStream, processedImage);
195
+ }
196
+ export const getVideo = async (input) => {
197
+ throw new Error('Please use the /api/video route');
198
+ };
199
+ export const getAllPrivileges = async () => {
200
+ const sections = await SectionFactory.getSections();
201
+ // First, let's assign the static privileges
202
+ const privilegesList = [];
203
+ // Now, let's add the rest of the privileges to the list
204
+ sections.forEach((section, index) => {
205
+ privilegesList.push({
206
+ title: typeof section.title === 'string' ? section.title : section.title.section,
207
+ order: section.order || index,
208
+ sectionName: section.name,
209
+ sectionType: section.type,
210
+ });
211
+ });
212
+ privilegesList.push({ title: 'Admins', order: 0, sectionName: 'admins', sectionType: 'static' }, { title: 'Emails', order: 1, sectionName: 'emails', sectionType: 'static' }, { title: 'Log', order: 2, sectionName: 'log', sectionType: 'static' });
213
+ return privilegesList;
214
+ };
215
+ export const getAdminsList = async () => {
216
+ const admins = await db
217
+ .select({
218
+ id: AdminsTable.id,
219
+ username: AdminsTable.user,
220
+ avatar: AdminsTable.coverphoto,
221
+ })
222
+ .from(AdminsTable);
223
+ const roles = await db.select().from(AdminPrivilegesTable);
224
+ /**
225
+ * Assign the roles to the admins
226
+ */
227
+ const adminsWithRoles = [];
228
+ admins.forEach((admin) => {
229
+ const adminRoles = roles.filter((role) => role.adminId === admin.id);
230
+ adminsWithRoles.push({
231
+ ...admin,
232
+ roles: adminRoles,
233
+ });
234
+ });
235
+ return adminsWithRoles;
236
+ };
237
+ export const createSimpleSectionPage = async (session, sectionName) => {
238
+ try {
239
+ /**
240
+ * Let's fetch the section information
241
+ */
242
+ const fieldsFactory = new FieldFactory({
243
+ type: 'edit',
244
+ sectionName,
245
+ session: session,
246
+ itemId: 1, // itemId is always 1 for simple sections
247
+ });
248
+ await fieldsFactory.initialize();
249
+ if (fieldsFactory.error) {
250
+ return {
251
+ error: {
252
+ message: fieldsFactory.errorMessage,
253
+ },
254
+ };
255
+ }
256
+ await fieldsFactory.generateFields();
257
+ /**
258
+ * Let's check for variants
259
+ */
260
+ /*const variants = section[0].variants
261
+ const sectionVariants: { name: string }[] = []
262
+
263
+ if (variants && variants.trim() !== '') {
264
+ /!**
265
+ * Convert to JSON
266
+ *!/
267
+ const variantsJson = JSON.parse(variants)
268
+
269
+ /!**
270
+ * Loop through the variants
271
+ *!/
272
+
273
+ variantsJson.forEach((variant: any) => {
274
+ const variantName = variant.name
275
+ const variantInfo = variant.info
276
+ })
277
+ }*/
278
+ return {
279
+ section: fieldsFactory.sectionInfo,
280
+ inputGroups: fieldsFactory.getGroupedFields(),
281
+ };
282
+ }
283
+ catch (err) {
284
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
285
+ console.error('Error creating section page', err);
286
+ return {
287
+ error: {
288
+ message: `Failed to create section page: ${errorMessage}`,
289
+ },
290
+ };
291
+ }
292
+ };
293
+ export const deleteSectionItem = async (session, sectionName, sectionItemId, recursive) => {
294
+ /**
295
+ * Convert the section item id to string
296
+ */
297
+ sectionItemId = sectionItemId.toString();
298
+ try {
299
+ const _s = await SectionFactory.getSectionForAdmin({
300
+ name: sectionName,
301
+ admin: {
302
+ id: session.user.id,
303
+ },
304
+ });
305
+ const section = _s?.build();
306
+ if (!section) {
307
+ return {
308
+ error: {
309
+ message: 'Section not found or you do not have access to it',
310
+ },
311
+ };
312
+ }
313
+ const tableName = section.db.table;
314
+ const identifierFieldName = section.db.identifier.name;
315
+ const columns = await MysqlTableChecker.getColumns(tableName);
316
+ if (!columns.includes(identifierFieldName)) {
317
+ return {
318
+ error: {
319
+ message: 'Section table identifier field not found',
320
+ },
321
+ };
322
+ }
323
+ /**
324
+ * Get the section item from the table
325
+ */
326
+ const [_v, _f] = await db.execute(sql `select * from ${sql.raw(tableName)} where ${sql.raw(identifierFieldName)} = ${sectionItemId} limit 1`);
327
+ // @ts-ignore
328
+ // Bug: this is a bug in drizzle-orm/mysql2
329
+ const sectionItemRow = _v[0];
330
+ /**
331
+ * Delete the row from the table
332
+ */
333
+ await db.execute(sql `delete from \`${sql.raw(tableName)}\` where \`${sql.raw(identifierFieldName)}\` = ${sectionItemId}`);
334
+ /**
335
+ * Grab the file fields to delete the files
336
+ */
337
+ const fileFieldConfigs = section.fieldConfigs.filter((fieldConfig) => fieldConfig.type === 'document' || fieldConfig.type === 'photo' || fieldConfig.type === 'video');
338
+ for (const field of fileFieldConfigs) {
339
+ // @ts-ignore
340
+ const value = sectionItemRow[field.name];
341
+ if (value) {
342
+ if (field.type === 'document') {
343
+ await fs.promises.unlink(path.join(uploadsFolder, '.documents', section.name, value));
344
+ }
345
+ else {
346
+ try {
347
+ await fs.promises.unlink(path.join(uploadsFolder, '.photos', section.name, value));
348
+ await fs.promises.unlink(path.join(uploadsFolder, '.thumbs', section.name, value));
349
+ }
350
+ catch (error) {
351
+ // Log but continue - file may not exist or already deleted
352
+ console.error('Error deleting photo', error);
353
+ }
354
+ }
355
+ }
356
+ }
357
+ /**
358
+ * Grab the selectMultiple fields and delete the rows from the destination table
359
+ */
360
+ const selectMultipleFields = section.fieldConfigs.filter((fieldConfig) => fieldConfig.type === 'select_multiple');
361
+ for (const fieldConfig of selectMultipleFields) {
362
+ const field = fieldConfig.build();
363
+ if (field.destinationDb) {
364
+ const columns = await MysqlTableChecker.getColumns(field.destinationDb.table);
365
+ if (columns.includes(field.destinationDb.selectIdentifier) &&
366
+ columns.includes(field.destinationDb.itemIdentifier)) {
367
+ await db.execute(sql `delete from ${sql.raw(field.destinationDb.table)} where ${sql.raw(field.destinationDb.itemIdentifier)} = ${sectionItemId}`);
368
+ }
369
+ }
370
+ }
371
+ /**
372
+ * Also delete the gallery photos if they exist
373
+ */
374
+ if (section.gallery) {
375
+ const { tableName, referenceIdentifierField, photoNameField, metaField } = section.gallery.db;
376
+ const columns = await MysqlTableChecker.getColumns(tableName);
377
+ if (columns.includes(photoNameField) &&
378
+ columns.includes(referenceIdentifierField) &&
379
+ columns.includes(metaField)) {
380
+ /**
381
+ * First, get the gallery photos
382
+ */
383
+ const [_v, _f] = await db.execute(sql `select * from \`${sql.raw(tableName)}\` where \`${sql.raw(referenceIdentifierField)}\` = ${sectionItemId}`);
384
+ // @ts-ignore
385
+ // Bug: this is a bug in drizzle-orm/mysql2
386
+ const galleryPhotos = _v;
387
+ /**
388
+ * Delete the photos from disk
389
+ */
390
+ for (const photo of galleryPhotos) {
391
+ try {
392
+ await fs.promises.unlink(path.join(uploadsFolder, '.photos', section.name, photo[photoNameField]));
393
+ await fs.promises.unlink(path.join(uploadsFolder, '.thumbs', section.name, photo[photoNameField]));
394
+ }
395
+ catch (error) {
396
+ // Log but continue - file may not exist or already deleted
397
+ console.error('Error deleting photo', error);
398
+ }
399
+ }
400
+ /**
401
+ * Delete the photos from the table
402
+ */
403
+ await db.execute(sql `DELETE FROM ${sql.raw(tableName)} WHERE ${sql.raw(referenceIdentifierField)} = ${sectionItemId}`);
404
+ }
405
+ }
406
+ /**
407
+ * Check if there are photos in the editor_photos table
408
+ */
409
+ const editorPhotos = await db
410
+ .select()
411
+ .from(EditorPhotosTable)
412
+ .where(and(eq(EditorPhotosTable.section, sectionName), eq(EditorPhotosTable.itemId, sectionItemId)));
413
+ if (editorPhotos.length > 0) {
414
+ /**
415
+ * Delete the photos from disk
416
+ */
417
+ for (const photo of editorPhotos) {
418
+ try {
419
+ await fs.promises.unlink(path.join(uploadsFolder, '.photos', photo.section, photo.name));
420
+ }
421
+ catch (error) {
422
+ // Log but continue - file may not exist or already deleted
423
+ console.error('Error deleting photo', error);
424
+ }
425
+ }
426
+ /**
427
+ * Delete the photos from the editor_photos table
428
+ */
429
+ await db
430
+ .delete(EditorPhotosTable)
431
+ .where(and(eq(EditorPhotosTable.section, sectionName), eq(EditorPhotosTable.itemId, sectionItemId)));
432
+ }
433
+ return true;
434
+ }
435
+ catch (err) {
436
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
437
+ console.error('Error deleting section item', err);
438
+ return {
439
+ error: {
440
+ message: `Failed to delete section item: ${errorMessage}`,
441
+ },
442
+ };
443
+ }
444
+ };
445
+ export const createEditPage = async (session, sectionName, sectionItemId) => {
446
+ /**
447
+ * Generate the fields for the edit page
448
+ */
449
+ const fieldsFactory = new FieldFactory({
450
+ type: 'edit',
451
+ sectionName,
452
+ session,
453
+ itemId: sectionItemId,
454
+ });
455
+ try {
456
+ await fieldsFactory.initialize();
457
+ await fieldsFactory.generateFields();
458
+ if (fieldsFactory.error) {
459
+ return {
460
+ error: {
461
+ message: fieldsFactory.errorMessage,
462
+ },
463
+ };
464
+ }
465
+ /**
466
+ * Let's check for variants
467
+ */
468
+ /*const variants = section[0].variants
469
+ const sectionVariants: { name: string }[] = []
470
+
471
+ if (variants && variants.trim() !== '') {
472
+ /!**
473
+ * Convert to JSON
474
+ *!/
475
+ const variantsJson = JSON.parse(variants)
476
+
477
+ /!**
478
+ * Loop through the variants
479
+ *!/
480
+
481
+ variantsJson.forEach((variant: any) => {
482
+ const variantName = variant.name
483
+ const variantInfo = variant.info
484
+ })
485
+ }*/
486
+ /**
487
+ * Get the gallery photos
488
+ * TODO: This is a temp implementation, will be removed once converting the gallery into a field
489
+ */
490
+ let galleryItems = [];
491
+ if (fieldsFactory.sectionInfo?.gallery) {
492
+ const { tableName, referenceIdentifierField, photoNameField, metaField } = fieldsFactory.sectionInfo.gallery.db;
493
+ const columns = await MysqlTableChecker.getColumns(tableName);
494
+ if (columns.includes(photoNameField) &&
495
+ columns.includes(referenceIdentifierField) &&
496
+ columns.includes(metaField)) {
497
+ const [items, fields] = await db.execute(sql `select * from \`${sql.raw(tableName)}\` where \`${sql.raw(referenceIdentifierField)}\` = ${sectionItemId}`);
498
+ const galleryPhotos = items;
499
+ galleryPhotos?.map((item) => {
500
+ galleryItems.push({
501
+ referenceId: item[referenceIdentifierField],
502
+ photo: item[photoNameField],
503
+ meta: item[metaField],
504
+ });
505
+ });
506
+ }
507
+ }
508
+ return {
509
+ section: fieldsFactory.sectionInfo,
510
+ inputGroups: fieldsFactory.getGroupedFields(),
511
+ gallery: galleryItems,
512
+ };
513
+ }
514
+ catch (error) {
515
+ const errorMessage = error?.errorMessage || (error instanceof Error ? error.message : 'Unknown error occurred');
516
+ console.error('Error creating edit page', error);
517
+ throw new TRPCError({
518
+ code: 'BAD_REQUEST',
519
+ message: errorMessage,
520
+ });
521
+ }
522
+ };
523
+ export const createNewPage = async (session, sectionName) => {
524
+ try {
525
+ const fieldsFactory = new FieldFactory({
526
+ type: 'new',
527
+ sectionName,
528
+ session,
529
+ });
530
+ await fieldsFactory.initialize();
531
+ if (fieldsFactory.error) {
532
+ return {
533
+ error: {
534
+ message: fieldsFactory.errorMessage,
535
+ },
536
+ };
537
+ }
538
+ try {
539
+ await fieldsFactory.generateFields();
540
+ }
541
+ catch (err) {
542
+ // console.error('Error generating fields', err)
543
+ return {
544
+ error: {
545
+ message: err.message,
546
+ },
547
+ };
548
+ }
549
+ /**
550
+ * Let's check for variants
551
+ */
552
+ /*const variants = section[0].variants
553
+ const sectionVariants: { name: string }[] = []
554
+
555
+ if (variants && variants.trim() !== '') {
556
+ /!**
557
+ * Convert to JSON
558
+ *!/
559
+ const variantsJson = JSON.parse(variants)
560
+
561
+ /!**
562
+ * Loop through the variants
563
+ *!/
564
+
565
+ variantsJson.forEach((variant: any) => {
566
+ const variantName = variant.name
567
+ const variantInfo = variant.info
568
+ })
569
+ }*/
570
+ return {
571
+ section: fieldsFactory.sectionInfo,
572
+ inputGroups: fieldsFactory.getGroupedFields(),
573
+ };
574
+ }
575
+ catch (err) {
576
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
577
+ console.error('Error creating new page', err);
578
+ return {
579
+ error: {
580
+ message: `Failed to create new page: ${errorMessage}`,
581
+ },
582
+ };
583
+ }
584
+ };
585
+ export const getCategorySectionChildren = async ({ session, id, sectionName, level, }) => {
586
+ /**
587
+ * First, level up to get the children (next level)
588
+ */
589
+ level++;
590
+ // Let's fetch the section items and admin privileges for the section
591
+ const _s = (await SectionFactory.getSectionForAdmin({
592
+ name: sectionName,
593
+ admin: {
594
+ id: session.user.id,
595
+ },
596
+ }));
597
+ const section = _s?.build();
598
+ if (!section) {
599
+ return {
600
+ error: {
601
+ message: 'Section not found or you do not have access to it',
602
+ },
603
+ };
604
+ }
605
+ /**
606
+ * Check if new level is allowed in the category section depth
607
+ */
608
+ if (level > (section.depth ?? 1)) {
609
+ /**
610
+ * This is the last level, return an empty array
611
+ */
612
+ return {
613
+ options: null,
614
+ parentId: id,
615
+ level: level,
616
+ };
617
+ }
618
+ /**
619
+ * Let's get the options for the select input
620
+ */
621
+ 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`;
622
+ /**
623
+ * Get the options from the table
624
+ */
625
+ const selectOptionsRows = await db.execute(selectStatement);
626
+ const rows = selectOptionsRows[0];
627
+ return {
628
+ options: rows.map((row) => {
629
+ return {
630
+ value: row[section.db.identifier.name],
631
+ label: row[section.headingField.name],
632
+ };
633
+ }),
634
+ parentId: id,
635
+ level: level++,
636
+ };
637
+ };
638
+ export const getCategorySection = async (session, sectionName) => {
639
+ // Let's fetch the section items and admin privileges for the section
640
+ const section = (await SectionFactory.getSectionForAdmin({
641
+ name: sectionName,
642
+ admin: {
643
+ id: session.user.id,
644
+ },
645
+ }));
646
+ if (!section) {
647
+ throw new TRPCError({
648
+ code: 'NOT_FOUND',
649
+ message: 'Section not found or you do not have access to it',
650
+ });
651
+ }
652
+ const tableName = section.db.table;
653
+ /**
654
+ * Create a select field config for the category section
655
+ */
656
+ const s = new SelectField({
657
+ name: section.name,
658
+ label: section.title.section,
659
+ required: false,
660
+ order: 1,
661
+ section: section,
662
+ destinationDb: undefined,
663
+ });
664
+ /**
665
+ * Build the select field to get the options
666
+ * from database and do all the necessary checks
667
+ */
668
+ await s.build();
669
+ /**
670
+ * Set the value to undefined
671
+ */
672
+ s.setValue(undefined);
673
+ const categorySectionSelect = {
674
+ options: s.options,
675
+ required: s.required,
676
+ name: s.name,
677
+ label: s.label,
678
+ value: s.value,
679
+ parentId: undefined,
680
+ level: 1,
681
+ depth: section.depth,
682
+ sectionName: section.name,
683
+ allowRecursiveDelete: section.allowRecursiveDelete,
684
+ };
685
+ return {
686
+ section: {
687
+ tableName: tableName,
688
+ sectionName: section.name,
689
+ title: section.title,
690
+ },
691
+ data: categorySectionSelect,
692
+ // publisher: privileges.publisher,
693
+ };
694
+ };
695
+ export const getBrowsePage = async (session, sectionName, page = 1, q) => {
696
+ // Let's fetch the section items and admin privileges for the section
697
+ const _s = (await SectionFactory.getSectionForAdmin({
698
+ name: sectionName,
699
+ admin: {
700
+ id: session.user.id,
701
+ },
702
+ }));
703
+ const section = _s?.build();
704
+ if (!section) {
705
+ return {
706
+ error: {
707
+ message: 'Section not found or you do not have access to it',
708
+ },
709
+ };
710
+ }
711
+ const limit = 12;
712
+ const offset = (page - 1) * limit;
713
+ const tableName = section.db.table;
714
+ const orderByFieldName = section.db.orderByField?.name || 'created_at';
715
+ const sqlChunks = [];
716
+ const totalRowsSqlChunks = [];
717
+ const sectionSearch = section.search?.searchFields ? section.search?.searchFields.length > 0 : false;
718
+ sqlChunks.push(sql `select * from ${sql.raw(tableName)}`);
719
+ totalRowsSqlChunks.push(sql `select count(*) as total from ${sql.raw(tableName)}`);
720
+ if (q && q.trim().length > 0) {
721
+ if (section.search?.searchFields.length) {
722
+ let i = 1;
723
+ sqlChunks.push(sql `where`);
724
+ totalRowsSqlChunks.push(sql `where`);
725
+ for (const field of section.search?.searchFields) {
726
+ // sqlChunks.push(sql`${sql.raw(field.name)} like '%${q}%'`)
727
+ sqlChunks.push(sql `${sql.raw(field.name)} LIKE CONCAT('%',${sql `${q.trim()}`},'%')`);
728
+ totalRowsSqlChunks.push(sql `${sql.raw(field.name)} LIKE CONCAT('%',${sql `${q.trim()}`},'%')`);
729
+ if (i === section.search?.searchFields.length)
730
+ continue;
731
+ sqlChunks.push(sql `or`);
732
+ totalRowsSqlChunks.push(sql `or`);
733
+ i++;
734
+ }
735
+ }
736
+ }
737
+ sqlChunks.push(sql `ORDER BY \`${sql.raw(orderByFieldName)}\` DESC LIMIT ${limit} OFFSET ${offset}`);
738
+ const finalSql = sql.join(sqlChunks, sql.raw(' '));
739
+ const totalRowsSql = sql.join(totalRowsSqlChunks, sql.raw(' '));
740
+ // Now, let's get the section items from the table
741
+ const sectionItems = await db.execute(finalSql);
742
+ const totalCountResult = await db.execute(totalRowsSql);
743
+ const totalCountRows = totalCountResult[0];
744
+ const rows = sectionItems[0];
745
+ return {
746
+ section: {
747
+ // tableName: tableName,
748
+ // headingField: section.headingField.name,
749
+ // identifierField: section.db.identifier.name,
750
+ // coverPhotoField: section.coverPhotoField?.name,
751
+ hasSearch: sectionSearch,
752
+ name: section.name,
753
+ title: section.title.section,
754
+ },
755
+ items: rows.map((row) => {
756
+ return {
757
+ id: row[section.db.identifier.name],
758
+ headingTitle: row[section.headingField.name],
759
+ coverPhoto: section.coverPhotoField ? row[section.coverPhotoField?.name] : null,
760
+ createdAt: row['created_at'],
761
+ createdBy: row['created_by'],
762
+ permission: row.permission,
763
+ };
764
+ }),
765
+ totalCount: totalCountRows[0].total,
766
+ };
767
+ };
768
+ export const getSidebar = async (session) => {
769
+ // Let's get simple, has_items and categorized sections from the sections table
770
+ const { simple, has_items, category, fixed } = await SectionFactory.getSectionsForAdmin({
771
+ admin: { id: session.user.id },
772
+ });
773
+ // Let's loop through the sections and add path, icon and title to each section item
774
+ const simpleSectionItems = simple.map((section) => {
775
+ return {
776
+ title: section.title,
777
+ path: `/section/${section.name}`,
778
+ icon: section.icon,
779
+ };
780
+ });
781
+ const hasItemsSectionItems = has_items.map((section) => {
782
+ return {
783
+ title: section.title.section,
784
+ path: `/${section.name}`,
785
+ icon: section.icon,
786
+ };
787
+ });
788
+ const categorySectionsItems = category.map((section) => {
789
+ return {
790
+ title: section.title.section,
791
+ path: `/categorized/${section.name}`,
792
+ icon: section.icon,
793
+ };
794
+ });
795
+ return {
796
+ fixed_sections: [
797
+ {
798
+ title: 'Dashboard',
799
+ path: '/dashboard',
800
+ icon: 'home',
801
+ },
802
+ ...fixed.map((section) => {
803
+ return {
804
+ title: section,
805
+ path: `/${section}`,
806
+ icon: 'home',
807
+ };
808
+ }),
809
+ ],
810
+ cat_sections: categorySectionsItems,
811
+ has_items_sections: hasItemsSectionItems,
812
+ simple_sections: simpleSectionItems,
813
+ };
814
+ };
815
+ /**
816
+ * Return a stream from the disk
817
+ * @param {string} path - The location of the file
818
+ * @param options
819
+ * @returns {ReadableStream} A readable stream of the file
820
+ */
821
+ export const streamFile = async (path, options) => {
822
+ const stream = fs.createReadStream(path, options);
823
+ return new ReadableStream({
824
+ start(controller) {
825
+ // @ts-ignore
826
+ stream.on('data', (chunk) => controller.enqueue(new Uint8Array(chunk)));
827
+ stream.on('end', () => controller.close());
828
+ stream.on('error', (error) => controller.error(error));
829
+ },
830
+ cancel() {
831
+ stream.destroy();
832
+ },
833
+ });
834
+ };