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.
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 +2 -3
@@ -1,338 +1,338 @@
1
- import { Field, baseFieldConfigSchema } from "./field.js";
2
- import { entityKind } from "../helpers.js";
3
- import * as z from 'zod';
4
- import { customAlphabet } from 'nanoid';
5
- import sharp from 'sharp';
6
- import path from 'path';
7
- import { db } from "../../db/client.js";
8
- import { EditorPhotosTable } from "../../db/schema.js";
9
- import fs from 'fs';
10
- import { and, eq } from 'drizzle-orm';
11
- import { getCMSConfig } from "../config.js";
12
- import { sanitizeRichText } from "../security/dom.js";
13
- const positiveInt = z.number().int().positive();
14
- const nonNegativeInt = z.number().int().nonnegative();
15
- const allowImageUploadsSchema = z.strictObject({
16
- /**
17
- * The public URL prefix for inline photos.
18
- * This url should be handled by the website app, not the NextJS-CMS app
19
- * This is the URL that will be used to access the photos publicly on the website
20
- * The name of the photo will be appended to this URL
21
- */
22
- publicURLPrefix: z.url().describe('The public URL prefix for inline photos'),
23
- size: z
24
- .strictObject({
25
- width: positiveInt,
26
- height: positiveInt,
27
- crop: z.boolean().default(false),
28
- })
29
- .optional(),
30
- maxFileSize: z
31
- .strictObject({
32
- size: positiveInt,
33
- unit: z.enum(['kb', 'mb']),
34
- })
35
- .optional(),
36
- handleMethod: z.enum(['base64', 'tempSave']).optional(),
37
- /**
38
- * Omit the extension of the image when saving it to disk
39
- */
40
- omitExtension: z.boolean().optional(),
41
- format: z.enum(['webp', 'jpg', 'jpeg', 'png']).optional(),
42
- });
43
- const baseRichTextExtraConfigSchema = z
44
- .strictObject({
45
- placeholder: z.string().optional(),
46
- minLength: nonNegativeInt.optional(),
47
- maxLength: positiveInt.optional(),
48
- /**
49
- * Whether to sanitize the value before saving it to the database.
50
- * If true, the value will be sanitized using DOMPurify (removes scripts, keeps HTML).
51
- * If false, the value will be saved as is (raw input).
52
- * @default true
53
- */
54
- sanitize: z.boolean().optional().describe('Sanitize the value before saving'),
55
- })
56
- .superRefine((value, ctx) => {
57
- if (value.minLength !== undefined && value.maxLength !== undefined && value.minLength > value.maxLength) {
58
- ctx.addIssue({
59
- code: 'custom',
60
- path: ['maxLength'],
61
- message: 'maxLength must be greater than or equal to minLength',
62
- });
63
- }
64
- });
65
- const richTextFieldWithMediaConfigSchema = baseRichTextExtraConfigSchema.safeExtend({
66
- allowMedia: z.literal(true),
67
- allowImageUploads: allowImageUploadsSchema.optional(),
68
- });
69
- const richTextFieldWithoutMediaConfigSchema = baseRichTextExtraConfigSchema.safeExtend({
70
- allowMedia: z.literal(false).optional(),
71
- });
72
- const configSchema = z.discriminatedUnion('allowMedia', [
73
- richTextFieldWithMediaConfigSchema,
74
- richTextFieldWithoutMediaConfigSchema,
75
- ]);
76
- export class RichTextField extends Field {
77
- static [entityKind] = 'RichTextField';
78
- maxLength;
79
- minLength;
80
- placeholder;
81
- allowMedia;
82
- allowImageUploads;
83
- sanitize;
84
- _inlinePhotos = [];
85
- uploadsFolder = getCMSConfig().files.upload.uploadPath;
86
- constructor(config) {
87
- super(config, 'rich_text');
88
- this.maxLength = config.maxLength;
89
- this.minLength = config.minLength;
90
- this.placeholder = config.placeholder;
91
- this.sanitize = config.sanitize ?? true;
92
- if (config.allowMedia && config.allowImageUploads?.publicURLPrefix) {
93
- this.allowMedia = true;
94
- this.allowImageUploads = {
95
- publicURLPrefix: config.allowImageUploads.publicURLPrefix,
96
- size: config.allowImageUploads.size ?? { width: 400, height: 400, crop: false },
97
- maxFileSize: config.allowImageUploads.maxFileSize ?? { size: 1, unit: 'mb' },
98
- handleMethod: config.allowImageUploads.handleMethod ?? 'base64',
99
- omitExtension: config.allowImageUploads.omitExtension ?? true,
100
- format: config.allowImageUploads.format ?? 'webp',
101
- };
102
- }
103
- else {
104
- this.allowMedia = false;
105
- this.allowImageUploads = false;
106
- }
107
- }
108
- /**
109
- * Get the value of the field
110
- */
111
- getValue() {
112
- return this.value;
113
- }
114
- exportForClient() {
115
- return {
116
- ...super.exportForClient(),
117
- maxLength: this.maxLength,
118
- minLength: this.minLength,
119
- placeholder: this.placeholder,
120
- allowMedia: this.allowMedia,
121
- allowImageUploads: this.allowImageUploads,
122
- sanitize: this.sanitize,
123
- };
124
- }
125
- /**
126
- * Sanitize the value
127
- */
128
- sanitizeValue() {
129
- /**
130
- * Check if the value is not undefined
131
- */
132
- if (this.value !== undefined && this.sanitize) {
133
- /**
134
- * Sanitize the value
135
- */
136
- this.value = sanitizeRichText(this.value);
137
- }
138
- }
139
- checkRequired() {
140
- /**
141
- * Check if the field is required
142
- */
143
- if (this.required) {
144
- if (!this.value || this.value.trim().length === 0) {
145
- throw new Error(`Field ${this.label} is required`);
146
- }
147
- }
148
- }
149
- /**
150
- * Extract inline images from the value
151
- */
152
- extractInlineImages() {
153
- if (!this.value)
154
- return;
155
- if (!this.allowImageUploads)
156
- return;
157
- const regex = /<img.*?src="(data:image\/.*?;base64,.*?)".*?>/g;
158
- const matches = this.value.matchAll(regex);
159
- for (const match of matches) {
160
- const base64String = match[1];
161
- const extension = base64String.split(';')[0].split('/')[1];
162
- const randomString = customAlphabet('1234567890abcdef', 21)();
163
- const name = `${randomString}${this.allowImageUploads.omitExtension ? '' : `.${extension}`}`;
164
- this._inlinePhotos.push({
165
- base64String,
166
- extension,
167
- name: name,
168
- });
169
- /**
170
- * Replace the inline image src with the name of the image
171
- */
172
- this.value = this.value.replace(base64String, `${this.allowImageUploads?.publicURLPrefix}/${name}`);
173
- }
174
- }
175
- async writeInlineImages(sectionName, itemId) {
176
- if (!this.allowImageUploads)
177
- return;
178
- /**
179
- * If .photos, and 'sectionName' folders don't exist, create them
180
- */
181
- const photosFolder = path.join(this.uploadsFolder, '.photos', sectionName);
182
- if (!fs.existsSync(photosFolder)) {
183
- fs.mkdirSync(photosFolder, { recursive: true });
184
- }
185
- for (const photo of this._inlinePhotos) {
186
- const str = photo.base64String.split(',')[1];
187
- if (!str)
188
- continue;
189
- /**
190
- * Use sharp to write the image to disk
191
- */
192
- const buffer = Buffer.from(str, 'base64');
193
- sharp.cache({ files: 0 });
194
- sharp.cache(false);
195
- const image = sharp(buffer);
196
- await image
197
- .toFormat(this.allowImageUploads.format ?? 'webp')
198
- .toFile(path.join(this.uploadsFolder, '.photos', sectionName, photo.name));
199
- /**
200
- * Insert the image into the database
201
- */
202
- await db.insert(EditorPhotosTable).values({
203
- name: photo.name,
204
- itemId: itemId,
205
- section: sectionName,
206
- field: this.name,
207
- linked: true,
208
- });
209
- /**
210
- * Destroy the image to free up memory
211
- */
212
- image.destroy();
213
- }
214
- }
215
- /**
216
- * Prepare the field for submission
217
- */
218
- async prepareForSubmission() {
219
- /**
220
- * Sanitize the value
221
- */
222
- this.sanitizeValue();
223
- /**
224
- * Check minimum length
225
- */
226
- if (this.minLength) {
227
- if (this.minLength > this.value.length) {
228
- throw new Error(`Field ${this.label} must be at least ${this.minLength} characters long`);
229
- }
230
- }
231
- /**
232
- * Check maximum length
233
- */
234
- if (this.maxLength) {
235
- if (this.maxLength < this.value.length) {
236
- throw new Error(`Field ${this.label} must be at most ${this.maxLength} characters long`);
237
- }
238
- }
239
- this.extractInlineImages();
240
- }
241
- async postSubmit({ sectionName, itemId }) {
242
- if (!this._inlinePhotos)
243
- return;
244
- /**
245
- * First, check if there are any inline images saved in the `editor_photos` table (edit operation)
246
- * If there are, check if they are still present in the rich text value
247
- * If they are not present (admin deleted them), delete them from the database and disk
248
- */
249
- await this.checkPreviousInlineImages(sectionName, itemId);
250
- await this.writeInlineImages(sectionName, itemId);
251
- }
252
- async checkPreviousInlineImages(sectionName, itemId) {
253
- if (!this.allowImageUploads)
254
- return;
255
- /**
256
- * Get all the photos saved in the database for this field
257
- */
258
- const tablePhotos = await db
259
- .select()
260
- .from(EditorPhotosTable)
261
- .where(and(eq(EditorPhotosTable.section, sectionName), eq(EditorPhotosTable.itemId, itemId), eq(EditorPhotosTable.field, this.name)));
262
- if (tablePhotos.length === 0)
263
- return;
264
- /**
265
- * Extract the photos from the value (only photos that has the publicURLPrefix)
266
- */
267
- const regex = new RegExp(`${this.allowImageUploads.publicURLPrefix}/(.*?)`, 'g');
268
- const matches = this.value.matchAll(regex);
269
- const photosInValue = [];
270
- for (const match of matches) {
271
- photosInValue.push(match[1]);
272
- }
273
- /**
274
- * Check if the photos in the database are still present in the value
275
- */
276
- for (const tablePhoto of tablePhotos) {
277
- if (!photosInValue.includes(tablePhoto.name)) {
278
- /**
279
- * Delete the photo from the database
280
- */
281
- await db
282
- .delete(EditorPhotosTable)
283
- .where(and(eq(EditorPhotosTable.section, sectionName), eq(EditorPhotosTable.itemId, itemId), eq(EditorPhotosTable.field, this.name), eq(EditorPhotosTable.name, tablePhoto.name)));
284
- /**
285
- * Delete the photo from disk
286
- */
287
- try {
288
- await fs.promises.unlink(path.join(this.uploadsFolder, '.photos', sectionName, tablePhoto.name));
289
- }
290
- catch (error) {
291
- console.log(`${this.label}: Error deleting file from disk`);
292
- }
293
- }
294
- }
295
- }
296
- }
297
- const richTextFieldWithMediaOptionsSchema = z.strictObject({
298
- ...baseFieldConfigSchema.shape,
299
- ...richTextFieldWithMediaConfigSchema.shape,
300
- });
301
- const richTextFieldWithoutMediaOptionsSchema = z.strictObject({
302
- ...baseFieldConfigSchema.shape,
303
- ...richTextFieldWithoutMediaConfigSchema.shape,
304
- });
305
- const optionsSchema = z.discriminatedUnion('allowMedia', [
306
- richTextFieldWithMediaOptionsSchema,
307
- richTextFieldWithoutMediaOptionsSchema,
308
- ]);
309
- const richTextFieldConfigSchema = z.intersection(optionsSchema, z.strictObject({
310
- type: z.literal('rich_text').describe('The type of the field'),
311
- build: z
312
- .function()
313
- .output(z.instanceof(RichTextField))
314
- .describe('Build a RichTextField instance from this config'),
315
- }));
316
- /**
317
- * Helper function to create a rich text field configuration
318
- * Returns a config object with a build() method that can be serialized and used anywhere
319
- * @param field
320
- */
321
- export function richTextField(field) {
322
- /**
323
- * Validate the field config
324
- */
325
- const result = optionsSchema.safeParse(field);
326
- if (!result.success) {
327
- throw new Error(`[Field: ${field.name}]: ${z.prettifyError(result.error)}`);
328
- }
329
- const config = {
330
- ...field,
331
- type: 'rich_text',
332
- build() {
333
- // Use the original field config directly (it doesn't have build() method)
334
- return new RichTextField(field);
335
- },
336
- };
337
- return config;
338
- }
1
+ import { Field, baseFieldConfigSchema } from './field.js';
2
+ import { entityKind } from '../helpers/index.js';
3
+ import * as z from 'zod';
4
+ import { customAlphabet } from 'nanoid';
5
+ import sharp from 'sharp';
6
+ import path from 'path';
7
+ import { db } from '../../db/client.js';
8
+ import { EditorPhotosTable } from '../../db/schema.js';
9
+ import fs from 'fs';
10
+ import { and, eq } from 'drizzle-orm';
11
+ import { getCMSConfig } from '../config/index.js';
12
+ import { sanitizeRichText } from '../security/dom.js';
13
+ const positiveInt = z.number().int().positive();
14
+ const nonNegativeInt = z.number().int().nonnegative();
15
+ const allowImageUploadsSchema = z.strictObject({
16
+ /**
17
+ * The public URL prefix for inline photos.
18
+ * This url should be handled by the website app, not the NextJS-CMS app
19
+ * This is the URL that will be used to access the photos publicly on the website
20
+ * The name of the photo will be appended to this URL
21
+ */
22
+ publicURLPrefix: z.url().describe('The public URL prefix for inline photos'),
23
+ size: z
24
+ .strictObject({
25
+ width: positiveInt,
26
+ height: positiveInt,
27
+ crop: z.boolean().default(false),
28
+ })
29
+ .optional(),
30
+ maxFileSize: z
31
+ .strictObject({
32
+ size: positiveInt,
33
+ unit: z.enum(['kb', 'mb']),
34
+ })
35
+ .optional(),
36
+ handleMethod: z.enum(['base64', 'tempSave']).optional(),
37
+ /**
38
+ * Omit the extension of the image when saving it to disk
39
+ */
40
+ omitExtension: z.boolean().optional(),
41
+ format: z.enum(['webp', 'jpg', 'jpeg', 'png']).optional(),
42
+ });
43
+ const baseRichTextExtraConfigSchema = z
44
+ .strictObject({
45
+ placeholder: z.string().optional(),
46
+ minLength: nonNegativeInt.optional(),
47
+ maxLength: positiveInt.optional(),
48
+ /**
49
+ * Whether to sanitize the value before saving it to the database.
50
+ * If true, the value will be sanitized using DOMPurify (removes scripts, keeps HTML).
51
+ * If false, the value will be saved as is (raw input).
52
+ * @default true
53
+ */
54
+ sanitize: z.boolean().optional().describe('Sanitize the value before saving'),
55
+ })
56
+ .superRefine((value, ctx) => {
57
+ if (value.minLength !== undefined && value.maxLength !== undefined && value.minLength > value.maxLength) {
58
+ ctx.addIssue({
59
+ code: 'custom',
60
+ path: ['maxLength'],
61
+ message: 'maxLength must be greater than or equal to minLength',
62
+ });
63
+ }
64
+ });
65
+ const richTextFieldWithMediaConfigSchema = baseRichTextExtraConfigSchema.safeExtend({
66
+ allowMedia: z.literal(true),
67
+ allowImageUploads: allowImageUploadsSchema.optional(),
68
+ });
69
+ const richTextFieldWithoutMediaConfigSchema = baseRichTextExtraConfigSchema.safeExtend({
70
+ allowMedia: z.literal(false).optional(),
71
+ });
72
+ const configSchema = z.discriminatedUnion('allowMedia', [
73
+ richTextFieldWithMediaConfigSchema,
74
+ richTextFieldWithoutMediaConfigSchema,
75
+ ]);
76
+ export class RichTextField extends Field {
77
+ static [entityKind] = 'RichTextField';
78
+ maxLength;
79
+ minLength;
80
+ placeholder;
81
+ allowMedia;
82
+ allowImageUploads;
83
+ sanitize;
84
+ _inlinePhotos = [];
85
+ uploadsFolder = getCMSConfig().files.upload.uploadPath;
86
+ constructor(config) {
87
+ super(config, 'rich_text');
88
+ this.maxLength = config.maxLength;
89
+ this.minLength = config.minLength;
90
+ this.placeholder = config.placeholder;
91
+ this.sanitize = config.sanitize ?? true;
92
+ if (config.allowMedia && config.allowImageUploads?.publicURLPrefix) {
93
+ this.allowMedia = true;
94
+ this.allowImageUploads = {
95
+ publicURLPrefix: config.allowImageUploads.publicURLPrefix,
96
+ size: config.allowImageUploads.size ?? { width: 400, height: 400, crop: false },
97
+ maxFileSize: config.allowImageUploads.maxFileSize ?? { size: 1, unit: 'mb' },
98
+ handleMethod: config.allowImageUploads.handleMethod ?? 'base64',
99
+ omitExtension: config.allowImageUploads.omitExtension ?? true,
100
+ format: config.allowImageUploads.format ?? 'webp',
101
+ };
102
+ }
103
+ else {
104
+ this.allowMedia = false;
105
+ this.allowImageUploads = false;
106
+ }
107
+ }
108
+ /**
109
+ * Get the value of the field
110
+ */
111
+ getValue() {
112
+ return this.value;
113
+ }
114
+ exportForClient() {
115
+ return {
116
+ ...super.exportForClient(),
117
+ maxLength: this.maxLength,
118
+ minLength: this.minLength,
119
+ placeholder: this.placeholder,
120
+ allowMedia: this.allowMedia,
121
+ allowImageUploads: this.allowImageUploads,
122
+ sanitize: this.sanitize,
123
+ };
124
+ }
125
+ /**
126
+ * Sanitize the value
127
+ */
128
+ sanitizeValue() {
129
+ /**
130
+ * Check if the value is not undefined
131
+ */
132
+ if (this.value !== undefined && this.sanitize) {
133
+ /**
134
+ * Sanitize the value
135
+ */
136
+ this.value = sanitizeRichText(this.value);
137
+ }
138
+ }
139
+ checkRequired() {
140
+ /**
141
+ * Check if the field is required
142
+ */
143
+ if (this.required) {
144
+ if (!this.value || this.value.trim().length === 0) {
145
+ throw new Error(`Field ${this.label} is required`);
146
+ }
147
+ }
148
+ }
149
+ /**
150
+ * Extract inline images from the value
151
+ */
152
+ extractInlineImages() {
153
+ if (!this.value)
154
+ return;
155
+ if (!this.allowImageUploads)
156
+ return;
157
+ const regex = /<img.*?src="(data:image\/.*?;base64,.*?)".*?>/g;
158
+ const matches = this.value.matchAll(regex);
159
+ for (const match of matches) {
160
+ const base64String = match[1];
161
+ const extension = base64String.split(';')[0].split('/')[1];
162
+ const randomString = customAlphabet('1234567890abcdef', 21)();
163
+ const name = `${randomString}${this.allowImageUploads.omitExtension ? '' : `.${extension}`}`;
164
+ this._inlinePhotos.push({
165
+ base64String,
166
+ extension,
167
+ name: name,
168
+ });
169
+ /**
170
+ * Replace the inline image src with the name of the image
171
+ */
172
+ this.value = this.value.replace(base64String, `${this.allowImageUploads?.publicURLPrefix}/${name}`);
173
+ }
174
+ }
175
+ async writeInlineImages(sectionName, itemId) {
176
+ if (!this.allowImageUploads)
177
+ return;
178
+ /**
179
+ * If .photos, and 'sectionName' folders don't exist, create them
180
+ */
181
+ const photosFolder = path.join(this.uploadsFolder, '.photos', sectionName);
182
+ if (!fs.existsSync(photosFolder)) {
183
+ fs.mkdirSync(photosFolder, { recursive: true });
184
+ }
185
+ for (const photo of this._inlinePhotos) {
186
+ const str = photo.base64String.split(',')[1];
187
+ if (!str)
188
+ continue;
189
+ /**
190
+ * Use sharp to write the image to disk
191
+ */
192
+ const buffer = Buffer.from(str, 'base64');
193
+ sharp.cache({ files: 0 });
194
+ sharp.cache(false);
195
+ const image = sharp(buffer);
196
+ await image
197
+ .toFormat(this.allowImageUploads.format ?? 'webp')
198
+ .toFile(path.join(this.uploadsFolder, '.photos', sectionName, photo.name));
199
+ /**
200
+ * Insert the image into the database
201
+ */
202
+ await db.insert(EditorPhotosTable).values({
203
+ name: photo.name,
204
+ itemId: itemId,
205
+ section: sectionName,
206
+ field: this.name,
207
+ linked: true,
208
+ });
209
+ /**
210
+ * Destroy the image to free up memory
211
+ */
212
+ image.destroy();
213
+ }
214
+ }
215
+ /**
216
+ * Prepare the field for submission
217
+ */
218
+ async prepareForSubmission() {
219
+ /**
220
+ * Sanitize the value
221
+ */
222
+ this.sanitizeValue();
223
+ /**
224
+ * Check minimum length
225
+ */
226
+ if (this.minLength) {
227
+ if (this.minLength > this.value.length) {
228
+ throw new Error(`Field ${this.label} must be at least ${this.minLength} characters long`);
229
+ }
230
+ }
231
+ /**
232
+ * Check maximum length
233
+ */
234
+ if (this.maxLength) {
235
+ if (this.maxLength < this.value.length) {
236
+ throw new Error(`Field ${this.label} must be at most ${this.maxLength} characters long`);
237
+ }
238
+ }
239
+ this.extractInlineImages();
240
+ }
241
+ async postSubmit({ sectionName, itemId }) {
242
+ if (!this._inlinePhotos)
243
+ return;
244
+ /**
245
+ * First, check if there are any inline images saved in the `editor_photos` table (edit operation)
246
+ * If there are, check if they are still present in the rich text value
247
+ * If they are not present (admin deleted them), delete them from the database and disk
248
+ */
249
+ await this.checkPreviousInlineImages(sectionName, itemId);
250
+ await this.writeInlineImages(sectionName, itemId);
251
+ }
252
+ async checkPreviousInlineImages(sectionName, itemId) {
253
+ if (!this.allowImageUploads)
254
+ return;
255
+ /**
256
+ * Get all the photos saved in the database for this field
257
+ */
258
+ const tablePhotos = await db
259
+ .select()
260
+ .from(EditorPhotosTable)
261
+ .where(and(eq(EditorPhotosTable.section, sectionName), eq(EditorPhotosTable.itemId, itemId), eq(EditorPhotosTable.field, this.name)));
262
+ if (tablePhotos.length === 0)
263
+ return;
264
+ /**
265
+ * Extract the photos from the value (only photos that has the publicURLPrefix)
266
+ */
267
+ const regex = new RegExp(`${this.allowImageUploads.publicURLPrefix}/(.*?)`, 'g');
268
+ const matches = this.value.matchAll(regex);
269
+ const photosInValue = [];
270
+ for (const match of matches) {
271
+ photosInValue.push(match[1]);
272
+ }
273
+ /**
274
+ * Check if the photos in the database are still present in the value
275
+ */
276
+ for (const tablePhoto of tablePhotos) {
277
+ if (!photosInValue.includes(tablePhoto.name)) {
278
+ /**
279
+ * Delete the photo from the database
280
+ */
281
+ await db
282
+ .delete(EditorPhotosTable)
283
+ .where(and(eq(EditorPhotosTable.section, sectionName), eq(EditorPhotosTable.itemId, itemId), eq(EditorPhotosTable.field, this.name), eq(EditorPhotosTable.name, tablePhoto.name)));
284
+ /**
285
+ * Delete the photo from disk
286
+ */
287
+ try {
288
+ await fs.promises.unlink(path.join(this.uploadsFolder, '.photos', sectionName, tablePhoto.name));
289
+ }
290
+ catch (error) {
291
+ console.log(`${this.label}: Error deleting file from disk`);
292
+ }
293
+ }
294
+ }
295
+ }
296
+ }
297
+ const richTextFieldWithMediaOptionsSchema = z.strictObject({
298
+ ...baseFieldConfigSchema.shape,
299
+ ...richTextFieldWithMediaConfigSchema.shape,
300
+ });
301
+ const richTextFieldWithoutMediaOptionsSchema = z.strictObject({
302
+ ...baseFieldConfigSchema.shape,
303
+ ...richTextFieldWithoutMediaConfigSchema.shape,
304
+ });
305
+ const optionsSchema = z.discriminatedUnion('allowMedia', [
306
+ richTextFieldWithMediaOptionsSchema,
307
+ richTextFieldWithoutMediaOptionsSchema,
308
+ ]);
309
+ const richTextFieldConfigSchema = z.intersection(optionsSchema, z.strictObject({
310
+ type: z.literal('rich_text').describe('The type of the field'),
311
+ build: z
312
+ .function()
313
+ .output(z.instanceof(RichTextField))
314
+ .describe('Build a RichTextField instance from this config'),
315
+ }));
316
+ /**
317
+ * Helper function to create a rich text field configuration
318
+ * Returns a config object with a build() method that can be serialized and used anywhere
319
+ * @param field
320
+ */
321
+ export function richTextField(field) {
322
+ /**
323
+ * Validate the field config
324
+ */
325
+ const result = optionsSchema.safeParse(field);
326
+ if (!result.success) {
327
+ throw new Error(`[Field: ${field.name}]: ${z.prettifyError(result.error)}`);
328
+ }
329
+ const config = {
330
+ ...field,
331
+ type: 'rich_text',
332
+ build() {
333
+ // Use the original field config directly (it doesn't have build() method)
334
+ return new RichTextField(field);
335
+ },
336
+ };
337
+ return config;
338
+ }