nextjs-cms 0.5.9 → 0.5.10
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.
- package/dist/api/axios/axiosInstance.d.ts +1 -1
- package/dist/api/axios/axiosInstance.js +8 -8
- package/dist/api/index.d.ts +855 -855
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +12 -12
- package/dist/api/lib/serverActions.d.ts +239 -239
- package/dist/api/lib/serverActions.d.ts.map +1 -1
- package/dist/api/lib/serverActions.js +834 -834
- package/dist/api/root.d.ts +828 -828
- package/dist/api/root.js +30 -30
- package/dist/api/routers/accountSettings.d.ts +60 -60
- package/dist/api/routers/accountSettings.js +108 -108
- package/dist/api/routers/admins.d.ts +105 -105
- package/dist/api/routers/admins.js +219 -219
- package/dist/api/routers/auth.d.ts +47 -47
- package/dist/api/routers/auth.js +25 -25
- package/dist/api/routers/categorySection.d.ts +103 -103
- package/dist/api/routers/categorySection.js +38 -38
- package/dist/api/routers/cmsSettings.d.ts +48 -48
- package/dist/api/routers/cmsSettings.js +51 -51
- package/dist/api/routers/cpanel.d.ts +83 -83
- package/dist/api/routers/cpanel.js +216 -216
- package/dist/api/routers/files.d.ts +47 -47
- package/dist/api/routers/files.js +23 -23
- package/dist/api/routers/gallery.d.ts +35 -35
- package/dist/api/routers/gallery.js +62 -62
- package/dist/api/routers/googleAnalytics.d.ts +30 -30
- package/dist/api/routers/googleAnalytics.js +7 -7
- package/dist/api/routers/hasItemsSection.d.ts +139 -139
- package/dist/api/routers/hasItemsSection.js +34 -34
- package/dist/api/routers/navigation.d.ts +51 -51
- package/dist/api/routers/navigation.js +11 -11
- package/dist/api/routers/simpleSection.d.ts +57 -57
- package/dist/api/routers/simpleSection.js +12 -12
- package/dist/api/trpc.d.ts +106 -106
- package/dist/api/trpc.js +72 -72
- package/dist/auth/axios/axiosInstance.d.ts +1 -1
- package/dist/auth/axios/axiosInstance.js +8 -8
- package/dist/auth/csrf.d.ts +29 -29
- package/dist/auth/csrf.js +76 -76
- package/dist/auth/hooks/index.d.ts +3 -3
- package/dist/auth/hooks/index.d.ts.map +1 -1
- package/dist/auth/hooks/index.js +3 -3
- package/dist/auth/hooks/useAxiosPrivate.d.ts +4 -4
- package/dist/auth/hooks/useAxiosPrivate.js +74 -74
- package/dist/auth/hooks/useRefreshToken.d.ts +6 -6
- package/dist/auth/hooks/useRefreshToken.js +79 -79
- package/dist/auth/index.d.ts +22 -22
- package/dist/auth/index.js +44 -44
- package/dist/auth/jwt.d.ts +5 -5
- package/dist/auth/jwt.js +25 -25
- package/dist/auth/lib/actions.d.ts +32 -32
- package/dist/auth/lib/actions.d.ts.map +1 -1
- package/dist/auth/lib/actions.js +209 -209
- package/dist/auth/lib/client.d.ts +3 -3
- package/dist/auth/lib/client.js +46 -46
- package/dist/auth/lib/index.d.ts +2 -2
- package/dist/auth/lib/index.d.ts.map +1 -1
- package/dist/auth/lib/index.js +2 -2
- package/dist/auth/react.d.ts +105 -105
- package/dist/auth/react.d.ts.map +1 -1
- package/dist/auth/react.js +347 -347
- package/dist/auth/trpc.d.ts +5 -5
- package/dist/auth/trpc.d.ts.map +1 -1
- package/dist/auth/trpc.js +81 -81
- package/dist/core/config/config-loader.d.ts +91 -91
- package/dist/core/config/config-loader.js +230 -230
- package/dist/core/config/index.d.ts +2 -2
- package/dist/core/config/index.d.ts.map +1 -1
- package/dist/core/config/index.js +1 -1
- package/dist/core/config/loader.d.ts +1 -1
- package/dist/core/config/loader.js +42 -42
- package/dist/core/db/index.d.ts +1 -1
- package/dist/core/db/index.d.ts.map +1 -1
- package/dist/core/db/index.js +1 -1
- package/dist/core/db/table-checker/DbTable.d.ts +5 -5
- package/dist/core/db/table-checker/DbTable.js +5 -5
- package/dist/core/db/table-checker/MysqlTable.d.ts +33 -33
- package/dist/core/db/table-checker/MysqlTable.d.ts.map +1 -1
- package/dist/core/db/table-checker/MysqlTable.js +94 -94
- package/dist/core/db/table-checker/index.d.ts +1 -1
- package/dist/core/db/table-checker/index.d.ts.map +1 -1
- package/dist/core/db/table-checker/index.js +1 -1
- package/dist/core/factories/FieldFactory.d.ts +123 -123
- package/dist/core/factories/FieldFactory.d.ts.map +1 -1
- package/dist/core/factories/FieldFactory.js +411 -411
- package/dist/core/factories/SectionFactory.d.ts +109 -109
- package/dist/core/factories/SectionFactory.d.ts.map +1 -1
- package/dist/core/factories/SectionFactory.js +415 -415
- package/dist/core/factories/index.d.ts +2 -2
- package/dist/core/factories/index.d.ts.map +1 -1
- package/dist/core/factories/index.js +2 -2
- package/dist/core/fields/checkbox.d.ts +62 -62
- package/dist/core/fields/checkbox.d.ts.map +1 -1
- package/dist/core/fields/checkbox.js +62 -62
- package/dist/core/fields/color.d.ts +83 -83
- package/dist/core/fields/color.d.ts.map +1 -1
- package/dist/core/fields/color.js +91 -91
- package/dist/core/fields/date.d.ts +99 -99
- package/dist/core/fields/date.d.ts.map +1 -1
- package/dist/core/fields/date.js +108 -108
- package/dist/core/fields/document.d.ts +179 -179
- package/dist/core/fields/document.d.ts.map +1 -1
- package/dist/core/fields/document.js +277 -277
- package/dist/core/fields/field-group.d.ts +17 -17
- package/dist/core/fields/field-group.d.ts.map +1 -1
- package/dist/core/fields/field-group.js +6 -6
- package/dist/core/fields/field.d.ts +125 -125
- package/dist/core/fields/field.d.ts.map +1 -1
- package/dist/core/fields/field.js +148 -148
- package/dist/core/fields/fileField.d.ts +14 -14
- package/dist/core/fields/fileField.d.ts.map +1 -1
- package/dist/core/fields/fileField.js +5 -5
- package/dist/core/fields/index.d.ts +64 -64
- package/dist/core/fields/index.d.ts.map +1 -1
- package/dist/core/fields/index.js +18 -18
- package/dist/core/fields/map.d.ts +166 -166
- package/dist/core/fields/map.d.ts.map +1 -1
- package/dist/core/fields/map.js +152 -152
- package/dist/core/fields/number.d.ts +185 -185
- package/dist/core/fields/number.d.ts.map +1 -1
- package/dist/core/fields/number.js +241 -241
- package/dist/core/fields/password.d.ts +108 -108
- package/dist/core/fields/password.d.ts.map +1 -1
- package/dist/core/fields/password.js +133 -133
- package/dist/core/fields/photo.d.ts +288 -288
- package/dist/core/fields/photo.d.ts.map +1 -1
- package/dist/core/fields/photo.js +410 -410
- package/dist/core/fields/richText.d.ts +294 -294
- package/dist/core/fields/richText.d.ts.map +1 -1
- package/dist/core/fields/richText.js +338 -338
- package/dist/core/fields/select.d.ts +365 -365
- package/dist/core/fields/select.d.ts.map +1 -1
- package/dist/core/fields/select.js +499 -499
- package/dist/core/fields/selectMultiple.d.ts +235 -235
- package/dist/core/fields/selectMultiple.d.ts.map +1 -1
- package/dist/core/fields/selectMultiple.js +417 -417
- package/dist/core/fields/tags.d.ts +130 -130
- package/dist/core/fields/tags.d.ts.map +1 -1
- package/dist/core/fields/tags.js +105 -105
- package/dist/core/fields/text.d.ts +135 -135
- package/dist/core/fields/text.d.ts.map +1 -1
- package/dist/core/fields/text.js +157 -157
- package/dist/core/fields/textArea.d.ts +106 -106
- package/dist/core/fields/textArea.d.ts.map +1 -1
- package/dist/core/fields/textArea.js +126 -126
- package/dist/core/fields/video.d.ts +147 -147
- package/dist/core/fields/video.d.ts.map +1 -1
- package/dist/core/fields/video.js +248 -248
- package/dist/core/helpers/entity.d.ts +7 -7
- package/dist/core/helpers/entity.js +27 -27
- package/dist/core/helpers/index.d.ts +4 -4
- package/dist/core/helpers/index.d.ts.map +1 -1
- package/dist/core/helpers/index.js +3 -3
- package/dist/core/index.d.ts +7 -7
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +7 -7
- package/dist/core/sections/category.d.ts +282 -282
- package/dist/core/sections/category.d.ts.map +1 -1
- package/dist/core/sections/category.js +147 -147
- package/dist/core/sections/hasItems.d.ts +631 -631
- package/dist/core/sections/hasItems.d.ts.map +1 -1
- package/dist/core/sections/hasItems.js +144 -144
- package/dist/core/sections/index.d.ts +4 -4
- package/dist/core/sections/index.d.ts.map +1 -1
- package/dist/core/sections/index.js +4 -4
- package/dist/core/sections/section.d.ts +225 -225
- package/dist/core/sections/section.d.ts.map +1 -1
- package/dist/core/sections/section.js +341 -341
- package/dist/core/sections/simple.d.ts +98 -98
- package/dist/core/sections/simple.d.ts.map +1 -1
- package/dist/core/sections/simple.js +95 -95
- package/dist/core/security/dom.d.ts +10 -10
- package/dist/core/security/dom.js +92 -92
- package/dist/core/submit/ItemEditSubmit.d.ts +75 -75
- package/dist/core/submit/ItemEditSubmit.js +186 -186
- package/dist/core/submit/NewItemSubmit.d.ts +13 -13
- package/dist/core/submit/NewItemSubmit.js +93 -93
- package/dist/core/submit/SimpleSectionSubmit.d.ts +12 -12
- package/dist/core/submit/SimpleSectionSubmit.js +93 -93
- package/dist/core/submit/index.d.ts +4 -4
- package/dist/core/submit/index.js +4 -4
- package/dist/core/submit/submit.d.ts +115 -115
- package/dist/core/submit/submit.js +479 -479
- package/dist/core/types/index.d.ts +279 -279
- package/dist/core/types/index.d.ts.map +1 -1
- package/dist/core/types/index.js +1 -1
- package/dist/db/client.d.ts +8 -8
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +19 -19
- package/dist/db/config.d.ts +5 -5
- package/dist/db/config.js +22 -22
- package/dist/db/drizzle.config.d.ts +5 -5
- package/dist/db/drizzle.config.js +18 -18
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.js +3 -3
- package/dist/db/schema.d.ts +638 -638
- package/dist/db/schema.js +73 -73
- package/dist/index.d.ts +7 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -7
- package/dist/translations/index.d.ts +2 -2
- package/dist/translations/index.js +15 -15
- package/dist/utils/CpanelApi.d.ts +24 -24
- package/dist/utils/CpanelApi.js +64 -64
- package/dist/utils/constants.d.ts +13 -13
- package/dist/utils/constants.js +61 -61
- package/dist/utils/index.d.ts +4 -4
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +4 -4
- package/dist/utils/utils.d.ts +59 -59
- package/dist/utils/utils.js +132 -132
- package/dist/validators/checkbox.d.ts +3 -3
- package/dist/validators/checkbox.d.ts.map +1 -1
- package/dist/validators/checkbox.js +12 -12
- package/dist/validators/color.d.ts +3 -3
- package/dist/validators/color.d.ts.map +1 -1
- package/dist/validators/color.js +7 -7
- package/dist/validators/date.d.ts +3 -3
- package/dist/validators/date.d.ts.map +1 -1
- package/dist/validators/date.js +5 -5
- package/dist/validators/document.d.ts +3 -3
- package/dist/validators/document.d.ts.map +1 -1
- package/dist/validators/document.js +57 -57
- package/dist/validators/index.d.ts +14 -14
- package/dist/validators/index.d.ts.map +1 -1
- package/dist/validators/index.js +14 -14
- package/dist/validators/map.d.ts +3 -3
- package/dist/validators/map.d.ts.map +1 -1
- package/dist/validators/map.js +5 -5
- package/dist/validators/number.d.ts +3 -3
- package/dist/validators/number.d.ts.map +1 -1
- package/dist/validators/number.js +20 -20
- package/dist/validators/password.d.ts +3 -3
- package/dist/validators/password.d.ts.map +1 -1
- package/dist/validators/password.js +11 -11
- package/dist/validators/photo.d.ts +3 -3
- package/dist/validators/photo.d.ts.map +1 -1
- package/dist/validators/photo.js +100 -100
- package/dist/validators/richText.d.ts +3 -3
- package/dist/validators/richText.d.ts.map +1 -1
- package/dist/validators/richText.js +8 -8
- package/dist/validators/select-multiple.d.ts +9 -9
- package/dist/validators/select-multiple.d.ts.map +1 -1
- package/dist/validators/select-multiple.js +20 -20
- package/dist/validators/select.d.ts +3 -3
- package/dist/validators/select.d.ts.map +1 -1
- package/dist/validators/select.js +5 -5
- package/dist/validators/text.d.ts +3 -3
- package/dist/validators/text.d.ts.map +1 -1
- package/dist/validators/text.js +7 -7
- package/dist/validators/textarea.d.ts +3 -3
- package/dist/validators/textarea.d.ts.map +1 -1
- package/dist/validators/textarea.js +7 -7
- package/dist/validators/video.d.ts +3 -3
- package/dist/validators/video.d.ts.map +1 -1
- package/dist/validators/video.js +57 -57
- package/package.json +2 -3
|
@@ -1,834 +1,834 @@
|
|
|
1
|
-
// import 'server-only'
|
|
2
|
-
import { db } from
|
|
3
|
-
import { AdminPrivilegesTable, AdminsTable, EditorPhotosTable } from
|
|
4
|
-
import { and, eq, sql } from 'drizzle-orm';
|
|
5
|
-
import { TRPCError } from '@trpc/server';
|
|
6
|
-
import { FieldFactory, SectionFactory } from
|
|
7
|
-
import { SelectField } from
|
|
8
|
-
import { MysqlTableChecker } from
|
|
9
|
-
import fs from 'fs';
|
|
10
|
-
import path from 'path';
|
|
11
|
-
import { sanitizeFileName, sanitizeFolderOrFileName } from
|
|
12
|
-
import sharp from 'sharp';
|
|
13
|
-
import { readChunk } from 'read-chunk';
|
|
14
|
-
import { fileTypeFromBuffer } from 'file-type';
|
|
15
|
-
import { getCMSConfig } from
|
|
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
|
+
};
|