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,499 +1,499 @@
|
|
|
1
|
-
import { Field, baseFieldConfigSchema } from
|
|
2
|
-
import { entityKind } from
|
|
3
|
-
import { MysqlTableChecker } from
|
|
4
|
-
import { db } from
|
|
5
|
-
import { sql } from 'drizzle-orm';
|
|
6
|
-
import chalk from 'chalk';
|
|
7
|
-
import * as z from 'zod';
|
|
8
|
-
export const selectOptionSchema = z.strictObject({
|
|
9
|
-
value: z.union([z.string(), z.number()]).describe('Stored value for this option'),
|
|
10
|
-
label: z.string().describe('Human-readable label for the option'),
|
|
11
|
-
});
|
|
12
|
-
export const selectFieldDestinationDbSchema = z.strictObject({
|
|
13
|
-
table: z.string().min(1).describe('Destination table where select values will be stored'),
|
|
14
|
-
/**
|
|
15
|
-
* The identifier of the item that has this select field as a field
|
|
16
|
-
*/
|
|
17
|
-
itemIdentifier: z.string().min(1).describe('Destination item identifier column'),
|
|
18
|
-
/**
|
|
19
|
-
* The identifier of the value of the select option that is coming from the select options
|
|
20
|
-
*/
|
|
21
|
-
selectIdentifier: z.string().min(1).describe('Destination select identifier column'),
|
|
22
|
-
});
|
|
23
|
-
const selectFieldSharedExtrasSchema = z.strictObject({
|
|
24
|
-
/**
|
|
25
|
-
* A destination table where the select values will be saved
|
|
26
|
-
* When this is set, the value of the field will only be saved in the destination table as one row per each value
|
|
27
|
-
*/
|
|
28
|
-
destinationDb: selectFieldDestinationDbSchema.optional(),
|
|
29
|
-
checkValueExists: z.boolean().optional(),
|
|
30
|
-
});
|
|
31
|
-
const selectFieldDbConfigSchema = selectFieldSharedExtrasSchema.extend({
|
|
32
|
-
options: z.never().optional(),
|
|
33
|
-
db: z
|
|
34
|
-
.strictObject({
|
|
35
|
-
table: z.string().min(1).describe('Database table to read select options from'),
|
|
36
|
-
identifier: z.string().min(1).describe('Identifier column for the option'),
|
|
37
|
-
label: z.string().min(1).describe('Label column for the option'),
|
|
38
|
-
orderBy: z.string().optional().describe('Optional order by column'),
|
|
39
|
-
})
|
|
40
|
-
.describe('Database configuration for select options'),
|
|
41
|
-
section: z.never().optional(),
|
|
42
|
-
});
|
|
43
|
-
const selectFieldSectionConfigSchema = selectFieldSharedExtrasSchema.extend({
|
|
44
|
-
options: z.array(selectOptionSchema).optional(),
|
|
45
|
-
db: z.never().optional(),
|
|
46
|
-
section: z.custom().describe('Section to derive select options from'),
|
|
47
|
-
});
|
|
48
|
-
const selectFieldStaticConfigSchema = selectFieldSharedExtrasSchema.extend({
|
|
49
|
-
options: z.array(selectOptionSchema).min(1).describe('List of static select options'),
|
|
50
|
-
db: z.never().optional(),
|
|
51
|
-
section: z.never().optional(),
|
|
52
|
-
});
|
|
53
|
-
export const selectFieldSpecificConfigSchema = z.union([
|
|
54
|
-
selectFieldDbConfigSchema,
|
|
55
|
-
selectFieldSectionConfigSchema,
|
|
56
|
-
selectFieldStaticConfigSchema,
|
|
57
|
-
]);
|
|
58
|
-
export class SelectField extends Field {
|
|
59
|
-
static [entityKind] = 'SelectField';
|
|
60
|
-
checkValueExists;
|
|
61
|
-
optionsType;
|
|
62
|
-
db;
|
|
63
|
-
destinationDb = undefined;
|
|
64
|
-
value = undefined;
|
|
65
|
-
// private _values: string[] | number[] | undefined
|
|
66
|
-
section;
|
|
67
|
-
_itemIdentifier;
|
|
68
|
-
options;
|
|
69
|
-
_allOptions;
|
|
70
|
-
constructor(config) {
|
|
71
|
-
super(config, 'select');
|
|
72
|
-
this.checkValueExists = config.checkValueExists ?? false;
|
|
73
|
-
if (config.section) {
|
|
74
|
-
this.destinationDb = config.destinationDb;
|
|
75
|
-
this.section = config.section;
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* If options are static, set them
|
|
79
|
-
*/
|
|
80
|
-
if (config.options && config.options?.length > 0) {
|
|
81
|
-
this.options = config.options;
|
|
82
|
-
this.optionsType = 'static';
|
|
83
|
-
this.db = null;
|
|
84
|
-
}
|
|
85
|
-
else if (config.db) {
|
|
86
|
-
/**
|
|
87
|
-
* Else, check if the db config is set
|
|
88
|
-
*/
|
|
89
|
-
this.db = config.db;
|
|
90
|
-
this.optionsType = 'db';
|
|
91
|
-
}
|
|
92
|
-
else if (config.section) {
|
|
93
|
-
/**
|
|
94
|
-
* Else, check if the section config is set
|
|
95
|
-
*/
|
|
96
|
-
/**
|
|
97
|
-
* Runtime validation: Ensure the section is a HasItemsSection or CategorySection
|
|
98
|
-
* TypeScript ensures type safety at compile time, but runtime data (from JSON/APIs)
|
|
99
|
-
* can violate types, so we validate here for safety.
|
|
100
|
-
*/
|
|
101
|
-
const sectionType = config.section.type;
|
|
102
|
-
if (sectionType !== 'has_items' && sectionType !== 'category') {
|
|
103
|
-
const message = `[SelectMultipleField: ${this.label}]: Section must be a HasItemsSection or CategorySection, but got type: ${sectionType ?? 'undefined'}`;
|
|
104
|
-
console.error(chalk.red.bold(message));
|
|
105
|
-
console.error(chalk.yellow('Please make sure the section is a HasItemsSection or CategorySection'));
|
|
106
|
-
throw new Error(message);
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* Build the section and set the options type to section
|
|
110
|
-
*/
|
|
111
|
-
const section = config.section.build();
|
|
112
|
-
this.optionsType = 'section';
|
|
113
|
-
this.db = {
|
|
114
|
-
table: section.db.table,
|
|
115
|
-
identifier: section.db.identifier.name,
|
|
116
|
-
label: section.headingField.name,
|
|
117
|
-
orderBy: section.db.orderByField ? section.db.orderByField.name : section.db.identifier.name,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
throw new Error('Select field requires either db, section or an options array.');
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* @param value The value of a single select field,
|
|
126
|
-
* or values array of multiple select fields if the select is selecting from
|
|
127
|
-
* a category section with depth higher than 1.
|
|
128
|
-
*/
|
|
129
|
-
setValue(value) {
|
|
130
|
-
/**
|
|
131
|
-
* Set the value
|
|
132
|
-
*/
|
|
133
|
-
if (value) {
|
|
134
|
-
/**
|
|
135
|
-
* Value can be a string of comma separated values if destinationDb is not set
|
|
136
|
-
*/
|
|
137
|
-
if (typeof value === 'string') {
|
|
138
|
-
/**
|
|
139
|
-
* Split the values by comma and set the value as an array of objects
|
|
140
|
-
*/
|
|
141
|
-
const values = value.toString().split(',');
|
|
142
|
-
values.map((value) => {
|
|
143
|
-
if (this.checkValueExists) {
|
|
144
|
-
/**
|
|
145
|
-
* Check if the value exists in the options and return the value and text
|
|
146
|
-
*/
|
|
147
|
-
const option = this.options?.find((option) => option.value.toString() === value.toString());
|
|
148
|
-
if (option) {
|
|
149
|
-
if (!this.value) {
|
|
150
|
-
this.value = [];
|
|
151
|
-
}
|
|
152
|
-
this.value.push(option);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
if (!this.value) {
|
|
157
|
-
this.value = [];
|
|
158
|
-
}
|
|
159
|
-
this.value.push({
|
|
160
|
-
value: value,
|
|
161
|
-
label: '', // TODO: Fix this
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
else {
|
|
167
|
-
/**
|
|
168
|
-
* Value is an array of objects
|
|
169
|
-
*/
|
|
170
|
-
this.value = value;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
else {
|
|
174
|
-
this.value = undefined;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
hasSqlNameAndValue() {
|
|
178
|
-
return this.destinationDb === undefined;
|
|
179
|
-
}
|
|
180
|
-
hasDepth() {
|
|
181
|
-
return this.section !== undefined && this.section.type === 'category' && (this.section.depth ?? 1) > 1;
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Get the value of the field
|
|
185
|
-
*/
|
|
186
|
-
getValue() {
|
|
187
|
-
/**
|
|
188
|
-
* Value is always a string | number
|
|
189
|
-
*/
|
|
190
|
-
return this.value;
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Get the value of the field when submitting to the database
|
|
194
|
-
*/
|
|
195
|
-
getSubmitValue() {
|
|
196
|
-
if (!this.hasSqlNameAndValue())
|
|
197
|
-
return;
|
|
198
|
-
/**
|
|
199
|
-
* Set the value as a comma separated string of the values
|
|
200
|
-
*/
|
|
201
|
-
return this.value?.map((item) => item.value).join(',');
|
|
202
|
-
}
|
|
203
|
-
exportForClient() {
|
|
204
|
-
return {
|
|
205
|
-
...super.exportForClient(),
|
|
206
|
-
options: this.options,
|
|
207
|
-
section: this.section
|
|
208
|
-
? {
|
|
209
|
-
name: this.section.name,
|
|
210
|
-
depth: this.section.type === 'category' ? this.section.depth : 1,
|
|
211
|
-
}
|
|
212
|
-
: undefined,
|
|
213
|
-
/**
|
|
214
|
-
* Add the values with the level if present
|
|
215
|
-
*/
|
|
216
|
-
value: this.value?.map((value, index) => ({ ...value, level: index + 1 })),
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
async postSubmit(itemId) {
|
|
220
|
-
if (!this.destinationDb)
|
|
221
|
-
return;
|
|
222
|
-
/**
|
|
223
|
-
* First, check if the select is selecting from a category section with depth higher than 1
|
|
224
|
-
*/
|
|
225
|
-
// if (!this.hasDepth()) return
|
|
226
|
-
this._itemIdentifier = itemId;
|
|
227
|
-
try {
|
|
228
|
-
/**
|
|
229
|
-
* Delete the existing values
|
|
230
|
-
*/
|
|
231
|
-
await db.execute(sql `DELETE FROM \`${sql.raw(this.destinationDb.table)}\` WHERE \`${sql.raw(this.destinationDb.itemIdentifier)}\` = ${this._itemIdentifier}`);
|
|
232
|
-
/**
|
|
233
|
-
* Prepare the sql statement
|
|
234
|
-
*/
|
|
235
|
-
const sqlChunks = [];
|
|
236
|
-
sqlChunks.push(sql `INSERT INTO \`${sql.raw(this.destinationDb.table)}\` (\`${sql.raw(this.destinationDb.selectIdentifier)}\`, \`${sql.raw(this.destinationDb.itemIdentifier)}\`) VALUES `);
|
|
237
|
-
/**
|
|
238
|
-
* Loop through the values and add them to the sql statement values
|
|
239
|
-
*/
|
|
240
|
-
if (this.value && this.value.length > 0) {
|
|
241
|
-
const count = this.value.length;
|
|
242
|
-
this.value.forEach((value, index) => {
|
|
243
|
-
sqlChunks.push(sql `(${value.value}, ${this._itemIdentifier})`);
|
|
244
|
-
if (index < count - 1) {
|
|
245
|
-
/**
|
|
246
|
-
* Add a comma to separate the values in the sql statement
|
|
247
|
-
*/
|
|
248
|
-
sqlChunks.push(sql `, `);
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* Join the sql chunks
|
|
254
|
-
*/
|
|
255
|
-
const finalSql = sql.join(sqlChunks);
|
|
256
|
-
/**
|
|
257
|
-
* Execute the sql statement
|
|
258
|
-
*/
|
|
259
|
-
await db.execute(finalSql);
|
|
260
|
-
}
|
|
261
|
-
catch (error) {
|
|
262
|
-
console.error(error);
|
|
263
|
-
throw new Error(`${this.label}: Error saving values to the destination table`);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
async postSubmitRollback() {
|
|
267
|
-
if (!this.destinationDb)
|
|
268
|
-
return;
|
|
269
|
-
/**
|
|
270
|
-
* First, check if the select is selecting from a category section with depth higher than 1
|
|
271
|
-
*/
|
|
272
|
-
// if (!this.hasDepth()) return
|
|
273
|
-
if (!this._itemIdentifier) {
|
|
274
|
-
throw new Error(`${this.label}: Item identifier is not set. Make sure to set the item identifier by calling postSubmit() before calling postSubmitRollback()`);
|
|
275
|
-
}
|
|
276
|
-
try {
|
|
277
|
-
await db.execute(sql `DELETE FROM \`${sql.raw(this.destinationDb.table)}\` WHERE \`${sql.raw(this.destinationDb.itemIdentifier)}\` = ${this._itemIdentifier}`);
|
|
278
|
-
}
|
|
279
|
-
catch (error) {
|
|
280
|
-
throw new Error(`${this.label}: Error deleting values from the destination table`);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
/**
|
|
284
|
-
* Check the destination db config is set
|
|
285
|
-
* @private
|
|
286
|
-
*/
|
|
287
|
-
checkDestinationDBConfig() {
|
|
288
|
-
if (this.destinationDb?.table.trim().length === 0 ||
|
|
289
|
-
this.destinationDb?.selectIdentifier.trim().length === 0 ||
|
|
290
|
-
this.destinationDb?.itemIdentifier.trim().length === 0) {
|
|
291
|
-
throw new Error(`Field ${this.label}: Destination table, select identifier and item identifier are required`);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
/**
|
|
295
|
-
* Check if destination table and columns exist
|
|
296
|
-
* @private
|
|
297
|
-
*/
|
|
298
|
-
async checkDestinationDBTableAndColumns() {
|
|
299
|
-
if (!this.destinationDb) {
|
|
300
|
-
throw new Error(`Field ${this.label}: Destination table, select identifier and item identifier are required`);
|
|
301
|
-
}
|
|
302
|
-
const columns = await MysqlTableChecker.getColumns(this.destinationDb.table);
|
|
303
|
-
if (!columns.includes(this.destinationDb.selectIdentifier) ||
|
|
304
|
-
!columns.includes(this.destinationDb.itemIdentifier)) {
|
|
305
|
-
throw new Error(`Field ${this.label}: Table ${this.destinationDb.table} and/or columns: ${this.destinationDb.selectIdentifier}, ${this.destinationDb.itemIdentifier} do not exist in the database!`);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
/**
|
|
309
|
-
* Build the field to be used in a form
|
|
310
|
-
*/
|
|
311
|
-
async build() {
|
|
312
|
-
if (this.destinationDb) {
|
|
313
|
-
this.checkDestinationDBConfig();
|
|
314
|
-
await this.checkDestinationDBTableAndColumns();
|
|
315
|
-
}
|
|
316
|
-
/**
|
|
317
|
-
* If options are already set, return
|
|
318
|
-
*/
|
|
319
|
-
if (this.options && this.optionsType === 'static')
|
|
320
|
-
return;
|
|
321
|
-
/**
|
|
322
|
-
* Initialize the select db and options
|
|
323
|
-
*/
|
|
324
|
-
this.checkDBConfig();
|
|
325
|
-
await this.checkDBTableAndColumns();
|
|
326
|
-
/**
|
|
327
|
-
* Fetch the options
|
|
328
|
-
*/
|
|
329
|
-
await this.fetchOptions();
|
|
330
|
-
}
|
|
331
|
-
async fetchOptions() {
|
|
332
|
-
/**
|
|
333
|
-
* Let's get the options for the select input
|
|
334
|
-
*/
|
|
335
|
-
const sqlChunks = [];
|
|
336
|
-
// Initialize the select statement
|
|
337
|
-
sqlChunks.push(sql `select * from \`${sql.raw(this.db.table)}\``);
|
|
338
|
-
/**
|
|
339
|
-
* Check if the select is from a category section with depth higher than 1
|
|
340
|
-
*/
|
|
341
|
-
if (this.hasDepth()) {
|
|
342
|
-
/**
|
|
343
|
-
* Add the where clause to the select statement
|
|
344
|
-
*/
|
|
345
|
-
sqlChunks.push(sql ` WHERE parent_id IS NULL AND level = 1`);
|
|
346
|
-
}
|
|
347
|
-
/**
|
|
348
|
-
* Add order by clause
|
|
349
|
-
*/
|
|
350
|
-
sqlChunks.push(sql ` ORDER BY \`${sql.raw(this.db.orderBy ?? this.db.identifier)}\` DESC`);
|
|
351
|
-
/**
|
|
352
|
-
* Get the options from the table
|
|
353
|
-
*/
|
|
354
|
-
const selectOptionsRows = await db.execute(sql.join(sqlChunks));
|
|
355
|
-
const rows = selectOptionsRows[0];
|
|
356
|
-
const inputSelectTableIdentifierField = this.db.identifier;
|
|
357
|
-
const inputSelectTableHeadingField = this.db.label;
|
|
358
|
-
/**
|
|
359
|
-
* Set the options
|
|
360
|
-
*/
|
|
361
|
-
this.options = rows.map((row) => ({
|
|
362
|
-
value: row[inputSelectTableIdentifierField],
|
|
363
|
-
label: row[inputSelectTableHeadingField],
|
|
364
|
-
}));
|
|
365
|
-
this._allOptions = this.options;
|
|
366
|
-
if (this.hasDepth()) {
|
|
367
|
-
/**
|
|
368
|
-
* Get all the options
|
|
369
|
-
*/
|
|
370
|
-
const selectOptionsRows1 = await db.execute(sql `select * from \`${sql.raw(this.db.table)}\` WHERE parent_id IS NOT NULL AND level > 1`);
|
|
371
|
-
const rows1 = selectOptionsRows1[0];
|
|
372
|
-
const tempRows = rows1.map((row) => ({
|
|
373
|
-
value: row[inputSelectTableIdentifierField],
|
|
374
|
-
label: row[inputSelectTableHeadingField],
|
|
375
|
-
}));
|
|
376
|
-
this._allOptions = this._allOptions.concat(tempRows);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
/**
|
|
380
|
-
* Check if table and columns exist
|
|
381
|
-
* @private
|
|
382
|
-
*/
|
|
383
|
-
async checkDBTableAndColumns() {
|
|
384
|
-
const columns = await MysqlTableChecker.getColumns(this.db.table);
|
|
385
|
-
if (!columns.includes(this.db.identifier) || !columns.includes(this.db.label)) {
|
|
386
|
-
throw new Error(`Field ${this.label}: Table ${this.db.table} and/or columns: ${this.db.identifier}, ${this.db.label} do not exist in the database!`);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
/**
|
|
390
|
-
* Check the db config is set
|
|
391
|
-
* @private
|
|
392
|
-
*/
|
|
393
|
-
checkDBConfig() {
|
|
394
|
-
if (this.db.table.trim().length === 0 ||
|
|
395
|
-
this.db.identifier.trim().length === 0 ||
|
|
396
|
-
this.db.label.trim().length === 0) {
|
|
397
|
-
throw new Error('Table, identifier and label are required');
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
checkRequired() {
|
|
401
|
-
/**
|
|
402
|
-
* Check if the field is required
|
|
403
|
-
*/
|
|
404
|
-
if (this.required) {
|
|
405
|
-
if (!this.value || this.value.toString().trim().length === 0) {
|
|
406
|
-
throw new Error(`Field ${this.label} is required`);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
/**
|
|
411
|
-
* Prepare the field for submission
|
|
412
|
-
*/
|
|
413
|
-
async prepareForSubmission() {
|
|
414
|
-
if (this.optionsType !== 'static') {
|
|
415
|
-
this.checkDBConfig();
|
|
416
|
-
await this.checkDBTableAndColumns();
|
|
417
|
-
/**
|
|
418
|
-
* Fetch the options
|
|
419
|
-
*/
|
|
420
|
-
await this.fetchOptions();
|
|
421
|
-
}
|
|
422
|
-
if (this.destinationDb) {
|
|
423
|
-
this.checkDestinationDBConfig();
|
|
424
|
-
await this.checkDestinationDBTableAndColumns();
|
|
425
|
-
}
|
|
426
|
-
/**
|
|
427
|
-
* If checkValueExists is set to true, check if the value exists in the options.
|
|
428
|
-
* This is important to prevent submitting a value that does not exist in the table. (e.g. tampering with the form)
|
|
429
|
-
* Default is false.
|
|
430
|
-
*/
|
|
431
|
-
if (this.checkValueExists && this.value && this.value.length > 0) {
|
|
432
|
-
/**
|
|
433
|
-
* Check if the values exist in the table
|
|
434
|
-
* This is now safe because we have verified the table and columns exist in the database
|
|
435
|
-
*/
|
|
436
|
-
this.value.forEach((value) => {
|
|
437
|
-
if (!this._allOptions?.find((option) => option.value.toString() === value.value.toString())) {
|
|
438
|
-
throw new Error(`Field ${this.label}: Option ${value.label} does not exist in the options`);
|
|
439
|
-
}
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
const selectFieldDbOptionsSchema = z.strictObject({
|
|
445
|
-
...baseFieldConfigSchema.shape,
|
|
446
|
-
...selectFieldDbConfigSchema.shape,
|
|
447
|
-
});
|
|
448
|
-
const selectFieldSectionOptionsSchema = z.strictObject({
|
|
449
|
-
...baseFieldConfigSchema.shape,
|
|
450
|
-
...selectFieldSectionConfigSchema.shape,
|
|
451
|
-
});
|
|
452
|
-
const selectFieldStaticOptionsSchema = z.strictObject({
|
|
453
|
-
...baseFieldConfigSchema.shape,
|
|
454
|
-
...selectFieldStaticConfigSchema.shape,
|
|
455
|
-
});
|
|
456
|
-
export const selectFieldOptionsSchema = z.union([
|
|
457
|
-
selectFieldDbOptionsSchema,
|
|
458
|
-
selectFieldSectionOptionsSchema,
|
|
459
|
-
selectFieldStaticOptionsSchema,
|
|
460
|
-
]);
|
|
461
|
-
const selectFieldConfigSchema = z.intersection(selectFieldOptionsSchema, z.strictObject({
|
|
462
|
-
type: z.literal('select').describe('The type of the field'),
|
|
463
|
-
optionsType: z.enum(['static', 'db', 'section']).describe('Source of select options'),
|
|
464
|
-
build: z.function().output(z.instanceof(SelectField)).describe('Build a SelectField instance from this config'),
|
|
465
|
-
}));
|
|
466
|
-
/**
|
|
467
|
-
* Helper function to create a select field configuration
|
|
468
|
-
* Returns a config object with a build() method that can be serialized and used anywhere.
|
|
469
|
-
*/
|
|
470
|
-
export function selectField(field) {
|
|
471
|
-
const result = selectFieldOptionsSchema.safeParse(field);
|
|
472
|
-
if (!result.success) {
|
|
473
|
-
throw new Error(`[Field: ${field.name}]: ${z.prettifyError(result.error)}`);
|
|
474
|
-
}
|
|
475
|
-
const parsedField = result.data;
|
|
476
|
-
let optionsType;
|
|
477
|
-
if (parsedField.options && parsedField.options.length > 0) {
|
|
478
|
-
optionsType = 'static';
|
|
479
|
-
}
|
|
480
|
-
else if (parsedField.db) {
|
|
481
|
-
optionsType = 'db';
|
|
482
|
-
}
|
|
483
|
-
else if (parsedField.section) {
|
|
484
|
-
optionsType = 'section';
|
|
485
|
-
}
|
|
486
|
-
else {
|
|
487
|
-
throw new Error(`[Field: ${parsedField.name}]: You have to assign options for the select field, you can assign one of the following sources: [db, section, options]`);
|
|
488
|
-
}
|
|
489
|
-
const config = {
|
|
490
|
-
...parsedField,
|
|
491
|
-
type: 'select',
|
|
492
|
-
optionsType,
|
|
493
|
-
build() {
|
|
494
|
-
// Use the original field config directly (it doesn't have build() method)
|
|
495
|
-
return new SelectField(parsedField);
|
|
496
|
-
},
|
|
497
|
-
};
|
|
498
|
-
return config;
|
|
499
|
-
}
|
|
1
|
+
import { Field, baseFieldConfigSchema } from './field.js';
|
|
2
|
+
import { entityKind } from '../helpers/index.js';
|
|
3
|
+
import { MysqlTableChecker } from '../db/index.js';
|
|
4
|
+
import { db } from '../../db/client.js';
|
|
5
|
+
import { sql } from 'drizzle-orm';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import * as z from 'zod';
|
|
8
|
+
export const selectOptionSchema = z.strictObject({
|
|
9
|
+
value: z.union([z.string(), z.number()]).describe('Stored value for this option'),
|
|
10
|
+
label: z.string().describe('Human-readable label for the option'),
|
|
11
|
+
});
|
|
12
|
+
export const selectFieldDestinationDbSchema = z.strictObject({
|
|
13
|
+
table: z.string().min(1).describe('Destination table where select values will be stored'),
|
|
14
|
+
/**
|
|
15
|
+
* The identifier of the item that has this select field as a field
|
|
16
|
+
*/
|
|
17
|
+
itemIdentifier: z.string().min(1).describe('Destination item identifier column'),
|
|
18
|
+
/**
|
|
19
|
+
* The identifier of the value of the select option that is coming from the select options
|
|
20
|
+
*/
|
|
21
|
+
selectIdentifier: z.string().min(1).describe('Destination select identifier column'),
|
|
22
|
+
});
|
|
23
|
+
const selectFieldSharedExtrasSchema = z.strictObject({
|
|
24
|
+
/**
|
|
25
|
+
* A destination table where the select values will be saved
|
|
26
|
+
* When this is set, the value of the field will only be saved in the destination table as one row per each value
|
|
27
|
+
*/
|
|
28
|
+
destinationDb: selectFieldDestinationDbSchema.optional(),
|
|
29
|
+
checkValueExists: z.boolean().optional(),
|
|
30
|
+
});
|
|
31
|
+
const selectFieldDbConfigSchema = selectFieldSharedExtrasSchema.extend({
|
|
32
|
+
options: z.never().optional(),
|
|
33
|
+
db: z
|
|
34
|
+
.strictObject({
|
|
35
|
+
table: z.string().min(1).describe('Database table to read select options from'),
|
|
36
|
+
identifier: z.string().min(1).describe('Identifier column for the option'),
|
|
37
|
+
label: z.string().min(1).describe('Label column for the option'),
|
|
38
|
+
orderBy: z.string().optional().describe('Optional order by column'),
|
|
39
|
+
})
|
|
40
|
+
.describe('Database configuration for select options'),
|
|
41
|
+
section: z.never().optional(),
|
|
42
|
+
});
|
|
43
|
+
const selectFieldSectionConfigSchema = selectFieldSharedExtrasSchema.extend({
|
|
44
|
+
options: z.array(selectOptionSchema).optional(),
|
|
45
|
+
db: z.never().optional(),
|
|
46
|
+
section: z.custom().describe('Section to derive select options from'),
|
|
47
|
+
});
|
|
48
|
+
const selectFieldStaticConfigSchema = selectFieldSharedExtrasSchema.extend({
|
|
49
|
+
options: z.array(selectOptionSchema).min(1).describe('List of static select options'),
|
|
50
|
+
db: z.never().optional(),
|
|
51
|
+
section: z.never().optional(),
|
|
52
|
+
});
|
|
53
|
+
export const selectFieldSpecificConfigSchema = z.union([
|
|
54
|
+
selectFieldDbConfigSchema,
|
|
55
|
+
selectFieldSectionConfigSchema,
|
|
56
|
+
selectFieldStaticConfigSchema,
|
|
57
|
+
]);
|
|
58
|
+
export class SelectField extends Field {
|
|
59
|
+
static [entityKind] = 'SelectField';
|
|
60
|
+
checkValueExists;
|
|
61
|
+
optionsType;
|
|
62
|
+
db;
|
|
63
|
+
destinationDb = undefined;
|
|
64
|
+
value = undefined;
|
|
65
|
+
// private _values: string[] | number[] | undefined
|
|
66
|
+
section;
|
|
67
|
+
_itemIdentifier;
|
|
68
|
+
options;
|
|
69
|
+
_allOptions;
|
|
70
|
+
constructor(config) {
|
|
71
|
+
super(config, 'select');
|
|
72
|
+
this.checkValueExists = config.checkValueExists ?? false;
|
|
73
|
+
if (config.section) {
|
|
74
|
+
this.destinationDb = config.destinationDb;
|
|
75
|
+
this.section = config.section;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* If options are static, set them
|
|
79
|
+
*/
|
|
80
|
+
if (config.options && config.options?.length > 0) {
|
|
81
|
+
this.options = config.options;
|
|
82
|
+
this.optionsType = 'static';
|
|
83
|
+
this.db = null;
|
|
84
|
+
}
|
|
85
|
+
else if (config.db) {
|
|
86
|
+
/**
|
|
87
|
+
* Else, check if the db config is set
|
|
88
|
+
*/
|
|
89
|
+
this.db = config.db;
|
|
90
|
+
this.optionsType = 'db';
|
|
91
|
+
}
|
|
92
|
+
else if (config.section) {
|
|
93
|
+
/**
|
|
94
|
+
* Else, check if the section config is set
|
|
95
|
+
*/
|
|
96
|
+
/**
|
|
97
|
+
* Runtime validation: Ensure the section is a HasItemsSection or CategorySection
|
|
98
|
+
* TypeScript ensures type safety at compile time, but runtime data (from JSON/APIs)
|
|
99
|
+
* can violate types, so we validate here for safety.
|
|
100
|
+
*/
|
|
101
|
+
const sectionType = config.section.type;
|
|
102
|
+
if (sectionType !== 'has_items' && sectionType !== 'category') {
|
|
103
|
+
const message = `[SelectMultipleField: ${this.label}]: Section must be a HasItemsSection or CategorySection, but got type: ${sectionType ?? 'undefined'}`;
|
|
104
|
+
console.error(chalk.red.bold(message));
|
|
105
|
+
console.error(chalk.yellow('Please make sure the section is a HasItemsSection or CategorySection'));
|
|
106
|
+
throw new Error(message);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Build the section and set the options type to section
|
|
110
|
+
*/
|
|
111
|
+
const section = config.section.build();
|
|
112
|
+
this.optionsType = 'section';
|
|
113
|
+
this.db = {
|
|
114
|
+
table: section.db.table,
|
|
115
|
+
identifier: section.db.identifier.name,
|
|
116
|
+
label: section.headingField.name,
|
|
117
|
+
orderBy: section.db.orderByField ? section.db.orderByField.name : section.db.identifier.name,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
throw new Error('Select field requires either db, section or an options array.');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* @param value The value of a single select field,
|
|
126
|
+
* or values array of multiple select fields if the select is selecting from
|
|
127
|
+
* a category section with depth higher than 1.
|
|
128
|
+
*/
|
|
129
|
+
setValue(value) {
|
|
130
|
+
/**
|
|
131
|
+
* Set the value
|
|
132
|
+
*/
|
|
133
|
+
if (value) {
|
|
134
|
+
/**
|
|
135
|
+
* Value can be a string of comma separated values if destinationDb is not set
|
|
136
|
+
*/
|
|
137
|
+
if (typeof value === 'string') {
|
|
138
|
+
/**
|
|
139
|
+
* Split the values by comma and set the value as an array of objects
|
|
140
|
+
*/
|
|
141
|
+
const values = value.toString().split(',');
|
|
142
|
+
values.map((value) => {
|
|
143
|
+
if (this.checkValueExists) {
|
|
144
|
+
/**
|
|
145
|
+
* Check if the value exists in the options and return the value and text
|
|
146
|
+
*/
|
|
147
|
+
const option = this.options?.find((option) => option.value.toString() === value.toString());
|
|
148
|
+
if (option) {
|
|
149
|
+
if (!this.value) {
|
|
150
|
+
this.value = [];
|
|
151
|
+
}
|
|
152
|
+
this.value.push(option);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
if (!this.value) {
|
|
157
|
+
this.value = [];
|
|
158
|
+
}
|
|
159
|
+
this.value.push({
|
|
160
|
+
value: value,
|
|
161
|
+
label: '', // TODO: Fix this
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
/**
|
|
168
|
+
* Value is an array of objects
|
|
169
|
+
*/
|
|
170
|
+
this.value = value;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
this.value = undefined;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
hasSqlNameAndValue() {
|
|
178
|
+
return this.destinationDb === undefined;
|
|
179
|
+
}
|
|
180
|
+
hasDepth() {
|
|
181
|
+
return this.section !== undefined && this.section.type === 'category' && (this.section.depth ?? 1) > 1;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Get the value of the field
|
|
185
|
+
*/
|
|
186
|
+
getValue() {
|
|
187
|
+
/**
|
|
188
|
+
* Value is always a string | number
|
|
189
|
+
*/
|
|
190
|
+
return this.value;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Get the value of the field when submitting to the database
|
|
194
|
+
*/
|
|
195
|
+
getSubmitValue() {
|
|
196
|
+
if (!this.hasSqlNameAndValue())
|
|
197
|
+
return;
|
|
198
|
+
/**
|
|
199
|
+
* Set the value as a comma separated string of the values
|
|
200
|
+
*/
|
|
201
|
+
return this.value?.map((item) => item.value).join(',');
|
|
202
|
+
}
|
|
203
|
+
exportForClient() {
|
|
204
|
+
return {
|
|
205
|
+
...super.exportForClient(),
|
|
206
|
+
options: this.options,
|
|
207
|
+
section: this.section
|
|
208
|
+
? {
|
|
209
|
+
name: this.section.name,
|
|
210
|
+
depth: this.section.type === 'category' ? this.section.depth : 1,
|
|
211
|
+
}
|
|
212
|
+
: undefined,
|
|
213
|
+
/**
|
|
214
|
+
* Add the values with the level if present
|
|
215
|
+
*/
|
|
216
|
+
value: this.value?.map((value, index) => ({ ...value, level: index + 1 })),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
async postSubmit(itemId) {
|
|
220
|
+
if (!this.destinationDb)
|
|
221
|
+
return;
|
|
222
|
+
/**
|
|
223
|
+
* First, check if the select is selecting from a category section with depth higher than 1
|
|
224
|
+
*/
|
|
225
|
+
// if (!this.hasDepth()) return
|
|
226
|
+
this._itemIdentifier = itemId;
|
|
227
|
+
try {
|
|
228
|
+
/**
|
|
229
|
+
* Delete the existing values
|
|
230
|
+
*/
|
|
231
|
+
await db.execute(sql `DELETE FROM \`${sql.raw(this.destinationDb.table)}\` WHERE \`${sql.raw(this.destinationDb.itemIdentifier)}\` = ${this._itemIdentifier}`);
|
|
232
|
+
/**
|
|
233
|
+
* Prepare the sql statement
|
|
234
|
+
*/
|
|
235
|
+
const sqlChunks = [];
|
|
236
|
+
sqlChunks.push(sql `INSERT INTO \`${sql.raw(this.destinationDb.table)}\` (\`${sql.raw(this.destinationDb.selectIdentifier)}\`, \`${sql.raw(this.destinationDb.itemIdentifier)}\`) VALUES `);
|
|
237
|
+
/**
|
|
238
|
+
* Loop through the values and add them to the sql statement values
|
|
239
|
+
*/
|
|
240
|
+
if (this.value && this.value.length > 0) {
|
|
241
|
+
const count = this.value.length;
|
|
242
|
+
this.value.forEach((value, index) => {
|
|
243
|
+
sqlChunks.push(sql `(${value.value}, ${this._itemIdentifier})`);
|
|
244
|
+
if (index < count - 1) {
|
|
245
|
+
/**
|
|
246
|
+
* Add a comma to separate the values in the sql statement
|
|
247
|
+
*/
|
|
248
|
+
sqlChunks.push(sql `, `);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Join the sql chunks
|
|
254
|
+
*/
|
|
255
|
+
const finalSql = sql.join(sqlChunks);
|
|
256
|
+
/**
|
|
257
|
+
* Execute the sql statement
|
|
258
|
+
*/
|
|
259
|
+
await db.execute(finalSql);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
console.error(error);
|
|
263
|
+
throw new Error(`${this.label}: Error saving values to the destination table`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async postSubmitRollback() {
|
|
267
|
+
if (!this.destinationDb)
|
|
268
|
+
return;
|
|
269
|
+
/**
|
|
270
|
+
* First, check if the select is selecting from a category section with depth higher than 1
|
|
271
|
+
*/
|
|
272
|
+
// if (!this.hasDepth()) return
|
|
273
|
+
if (!this._itemIdentifier) {
|
|
274
|
+
throw new Error(`${this.label}: Item identifier is not set. Make sure to set the item identifier by calling postSubmit() before calling postSubmitRollback()`);
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
await db.execute(sql `DELETE FROM \`${sql.raw(this.destinationDb.table)}\` WHERE \`${sql.raw(this.destinationDb.itemIdentifier)}\` = ${this._itemIdentifier}`);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
throw new Error(`${this.label}: Error deleting values from the destination table`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Check the destination db config is set
|
|
285
|
+
* @private
|
|
286
|
+
*/
|
|
287
|
+
checkDestinationDBConfig() {
|
|
288
|
+
if (this.destinationDb?.table.trim().length === 0 ||
|
|
289
|
+
this.destinationDb?.selectIdentifier.trim().length === 0 ||
|
|
290
|
+
this.destinationDb?.itemIdentifier.trim().length === 0) {
|
|
291
|
+
throw new Error(`Field ${this.label}: Destination table, select identifier and item identifier are required`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Check if destination table and columns exist
|
|
296
|
+
* @private
|
|
297
|
+
*/
|
|
298
|
+
async checkDestinationDBTableAndColumns() {
|
|
299
|
+
if (!this.destinationDb) {
|
|
300
|
+
throw new Error(`Field ${this.label}: Destination table, select identifier and item identifier are required`);
|
|
301
|
+
}
|
|
302
|
+
const columns = await MysqlTableChecker.getColumns(this.destinationDb.table);
|
|
303
|
+
if (!columns.includes(this.destinationDb.selectIdentifier) ||
|
|
304
|
+
!columns.includes(this.destinationDb.itemIdentifier)) {
|
|
305
|
+
throw new Error(`Field ${this.label}: Table ${this.destinationDb.table} and/or columns: ${this.destinationDb.selectIdentifier}, ${this.destinationDb.itemIdentifier} do not exist in the database!`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Build the field to be used in a form
|
|
310
|
+
*/
|
|
311
|
+
async build() {
|
|
312
|
+
if (this.destinationDb) {
|
|
313
|
+
this.checkDestinationDBConfig();
|
|
314
|
+
await this.checkDestinationDBTableAndColumns();
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* If options are already set, return
|
|
318
|
+
*/
|
|
319
|
+
if (this.options && this.optionsType === 'static')
|
|
320
|
+
return;
|
|
321
|
+
/**
|
|
322
|
+
* Initialize the select db and options
|
|
323
|
+
*/
|
|
324
|
+
this.checkDBConfig();
|
|
325
|
+
await this.checkDBTableAndColumns();
|
|
326
|
+
/**
|
|
327
|
+
* Fetch the options
|
|
328
|
+
*/
|
|
329
|
+
await this.fetchOptions();
|
|
330
|
+
}
|
|
331
|
+
async fetchOptions() {
|
|
332
|
+
/**
|
|
333
|
+
* Let's get the options for the select input
|
|
334
|
+
*/
|
|
335
|
+
const sqlChunks = [];
|
|
336
|
+
// Initialize the select statement
|
|
337
|
+
sqlChunks.push(sql `select * from \`${sql.raw(this.db.table)}\``);
|
|
338
|
+
/**
|
|
339
|
+
* Check if the select is from a category section with depth higher than 1
|
|
340
|
+
*/
|
|
341
|
+
if (this.hasDepth()) {
|
|
342
|
+
/**
|
|
343
|
+
* Add the where clause to the select statement
|
|
344
|
+
*/
|
|
345
|
+
sqlChunks.push(sql ` WHERE parent_id IS NULL AND level = 1`);
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Add order by clause
|
|
349
|
+
*/
|
|
350
|
+
sqlChunks.push(sql ` ORDER BY \`${sql.raw(this.db.orderBy ?? this.db.identifier)}\` DESC`);
|
|
351
|
+
/**
|
|
352
|
+
* Get the options from the table
|
|
353
|
+
*/
|
|
354
|
+
const selectOptionsRows = await db.execute(sql.join(sqlChunks));
|
|
355
|
+
const rows = selectOptionsRows[0];
|
|
356
|
+
const inputSelectTableIdentifierField = this.db.identifier;
|
|
357
|
+
const inputSelectTableHeadingField = this.db.label;
|
|
358
|
+
/**
|
|
359
|
+
* Set the options
|
|
360
|
+
*/
|
|
361
|
+
this.options = rows.map((row) => ({
|
|
362
|
+
value: row[inputSelectTableIdentifierField],
|
|
363
|
+
label: row[inputSelectTableHeadingField],
|
|
364
|
+
}));
|
|
365
|
+
this._allOptions = this.options;
|
|
366
|
+
if (this.hasDepth()) {
|
|
367
|
+
/**
|
|
368
|
+
* Get all the options
|
|
369
|
+
*/
|
|
370
|
+
const selectOptionsRows1 = await db.execute(sql `select * from \`${sql.raw(this.db.table)}\` WHERE parent_id IS NOT NULL AND level > 1`);
|
|
371
|
+
const rows1 = selectOptionsRows1[0];
|
|
372
|
+
const tempRows = rows1.map((row) => ({
|
|
373
|
+
value: row[inputSelectTableIdentifierField],
|
|
374
|
+
label: row[inputSelectTableHeadingField],
|
|
375
|
+
}));
|
|
376
|
+
this._allOptions = this._allOptions.concat(tempRows);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Check if table and columns exist
|
|
381
|
+
* @private
|
|
382
|
+
*/
|
|
383
|
+
async checkDBTableAndColumns() {
|
|
384
|
+
const columns = await MysqlTableChecker.getColumns(this.db.table);
|
|
385
|
+
if (!columns.includes(this.db.identifier) || !columns.includes(this.db.label)) {
|
|
386
|
+
throw new Error(`Field ${this.label}: Table ${this.db.table} and/or columns: ${this.db.identifier}, ${this.db.label} do not exist in the database!`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Check the db config is set
|
|
391
|
+
* @private
|
|
392
|
+
*/
|
|
393
|
+
checkDBConfig() {
|
|
394
|
+
if (this.db.table.trim().length === 0 ||
|
|
395
|
+
this.db.identifier.trim().length === 0 ||
|
|
396
|
+
this.db.label.trim().length === 0) {
|
|
397
|
+
throw new Error('Table, identifier and label are required');
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
checkRequired() {
|
|
401
|
+
/**
|
|
402
|
+
* Check if the field is required
|
|
403
|
+
*/
|
|
404
|
+
if (this.required) {
|
|
405
|
+
if (!this.value || this.value.toString().trim().length === 0) {
|
|
406
|
+
throw new Error(`Field ${this.label} is required`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Prepare the field for submission
|
|
412
|
+
*/
|
|
413
|
+
async prepareForSubmission() {
|
|
414
|
+
if (this.optionsType !== 'static') {
|
|
415
|
+
this.checkDBConfig();
|
|
416
|
+
await this.checkDBTableAndColumns();
|
|
417
|
+
/**
|
|
418
|
+
* Fetch the options
|
|
419
|
+
*/
|
|
420
|
+
await this.fetchOptions();
|
|
421
|
+
}
|
|
422
|
+
if (this.destinationDb) {
|
|
423
|
+
this.checkDestinationDBConfig();
|
|
424
|
+
await this.checkDestinationDBTableAndColumns();
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* If checkValueExists is set to true, check if the value exists in the options.
|
|
428
|
+
* This is important to prevent submitting a value that does not exist in the table. (e.g. tampering with the form)
|
|
429
|
+
* Default is false.
|
|
430
|
+
*/
|
|
431
|
+
if (this.checkValueExists && this.value && this.value.length > 0) {
|
|
432
|
+
/**
|
|
433
|
+
* Check if the values exist in the table
|
|
434
|
+
* This is now safe because we have verified the table and columns exist in the database
|
|
435
|
+
*/
|
|
436
|
+
this.value.forEach((value) => {
|
|
437
|
+
if (!this._allOptions?.find((option) => option.value.toString() === value.value.toString())) {
|
|
438
|
+
throw new Error(`Field ${this.label}: Option ${value.label} does not exist in the options`);
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
const selectFieldDbOptionsSchema = z.strictObject({
|
|
445
|
+
...baseFieldConfigSchema.shape,
|
|
446
|
+
...selectFieldDbConfigSchema.shape,
|
|
447
|
+
});
|
|
448
|
+
const selectFieldSectionOptionsSchema = z.strictObject({
|
|
449
|
+
...baseFieldConfigSchema.shape,
|
|
450
|
+
...selectFieldSectionConfigSchema.shape,
|
|
451
|
+
});
|
|
452
|
+
const selectFieldStaticOptionsSchema = z.strictObject({
|
|
453
|
+
...baseFieldConfigSchema.shape,
|
|
454
|
+
...selectFieldStaticConfigSchema.shape,
|
|
455
|
+
});
|
|
456
|
+
export const selectFieldOptionsSchema = z.union([
|
|
457
|
+
selectFieldDbOptionsSchema,
|
|
458
|
+
selectFieldSectionOptionsSchema,
|
|
459
|
+
selectFieldStaticOptionsSchema,
|
|
460
|
+
]);
|
|
461
|
+
const selectFieldConfigSchema = z.intersection(selectFieldOptionsSchema, z.strictObject({
|
|
462
|
+
type: z.literal('select').describe('The type of the field'),
|
|
463
|
+
optionsType: z.enum(['static', 'db', 'section']).describe('Source of select options'),
|
|
464
|
+
build: z.function().output(z.instanceof(SelectField)).describe('Build a SelectField instance from this config'),
|
|
465
|
+
}));
|
|
466
|
+
/**
|
|
467
|
+
* Helper function to create a select field configuration
|
|
468
|
+
* Returns a config object with a build() method that can be serialized and used anywhere.
|
|
469
|
+
*/
|
|
470
|
+
export function selectField(field) {
|
|
471
|
+
const result = selectFieldOptionsSchema.safeParse(field);
|
|
472
|
+
if (!result.success) {
|
|
473
|
+
throw new Error(`[Field: ${field.name}]: ${z.prettifyError(result.error)}`);
|
|
474
|
+
}
|
|
475
|
+
const parsedField = result.data;
|
|
476
|
+
let optionsType;
|
|
477
|
+
if (parsedField.options && parsedField.options.length > 0) {
|
|
478
|
+
optionsType = 'static';
|
|
479
|
+
}
|
|
480
|
+
else if (parsedField.db) {
|
|
481
|
+
optionsType = 'db';
|
|
482
|
+
}
|
|
483
|
+
else if (parsedField.section) {
|
|
484
|
+
optionsType = 'section';
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
throw new Error(`[Field: ${parsedField.name}]: You have to assign options for the select field, you can assign one of the following sources: [db, section, options]`);
|
|
488
|
+
}
|
|
489
|
+
const config = {
|
|
490
|
+
...parsedField,
|
|
491
|
+
type: 'select',
|
|
492
|
+
optionsType,
|
|
493
|
+
build() {
|
|
494
|
+
// Use the original field config directly (it doesn't have build() method)
|
|
495
|
+
return new SelectField(parsedField);
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
return config;
|
|
499
|
+
}
|