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,338 +1,338 @@
|
|
|
1
|
-
import { Field, baseFieldConfigSchema } from
|
|
2
|
-
import { entityKind } from
|
|
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
|
|
8
|
-
import { EditorPhotosTable } from
|
|
9
|
-
import fs from 'fs';
|
|
10
|
-
import { and, eq } from 'drizzle-orm';
|
|
11
|
-
import { getCMSConfig } from
|
|
12
|
-
import { sanitizeRichText } from
|
|
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
|
+
}
|