@aphexcms/cms-core 0.1.16 → 0.2.1
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/client.d.ts.map +1 -1
- package/dist/api/client.js +7 -1
- package/dist/api/types.d.ts +2 -0
- package/dist/api/types.d.ts.map +1 -1
- package/dist/cli/generate-types.js +62 -17
- package/dist/cli/index.js +1 -1
- package/dist/client/index.d.ts +0 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +0 -1
- package/dist/components/AdminApp.svelte +278 -45
- package/dist/components/AdminApp.svelte.d.ts +2 -0
- package/dist/components/AdminApp.svelte.d.ts.map +1 -1
- package/dist/components/admin/DocumentEditor.svelte +60 -13
- package/dist/components/admin/DocumentEditor.svelte.d.ts.map +1 -1
- package/dist/components/admin/ObjectModal.svelte +15 -4
- package/dist/components/admin/ObjectModal.svelte.d.ts +1 -0
- package/dist/components/admin/ObjectModal.svelte.d.ts.map +1 -1
- package/dist/components/admin/SchemaField.svelte +64 -5
- package/dist/components/admin/SchemaField.svelte.d.ts +1 -0
- package/dist/components/admin/SchemaField.svelte.d.ts.map +1 -1
- package/dist/components/admin/fields/ArrayField.svelte +402 -111
- package/dist/components/admin/fields/ArrayField.svelte.d.ts +1 -0
- package/dist/components/admin/fields/ArrayField.svelte.d.ts.map +1 -1
- package/dist/components/admin/fields/DateField.svelte +145 -0
- package/dist/components/admin/fields/DateField.svelte.d.ts +14 -0
- package/dist/components/admin/fields/DateField.svelte.d.ts.map +1 -0
- package/dist/components/admin/fields/DateTimeField.svelte +225 -0
- package/dist/components/admin/fields/DateTimeField.svelte.d.ts +14 -0
- package/dist/components/admin/fields/DateTimeField.svelte.d.ts.map +1 -0
- package/dist/components/admin/fields/ImageField.svelte +221 -110
- package/dist/components/admin/fields/ImageField.svelte.d.ts +2 -0
- package/dist/components/admin/fields/ImageField.svelte.d.ts.map +1 -1
- package/dist/components/admin/fields/ReferenceField.svelte +1 -1
- package/dist/components/admin/fields/SlugField.svelte +21 -13
- package/dist/components/admin/fields/SlugField.svelte.d.ts +2 -2
- package/dist/components/admin/fields/SlugField.svelte.d.ts.map +1 -1
- package/dist/components/admin/fields/StringField.svelte +156 -12
- package/dist/components/admin/fields/StringField.svelte.d.ts +3 -2
- package/dist/components/admin/fields/StringField.svelte.d.ts.map +1 -1
- package/dist/components/admin/fields/URLField.svelte +41 -0
- package/dist/components/admin/fields/URLField.svelte.d.ts +14 -0
- package/dist/components/admin/fields/URLField.svelte.d.ts.map +1 -0
- package/dist/components/index.d.ts +0 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +0 -1
- package/dist/db/interfaces/asset.d.ts.map +1 -1
- package/dist/db/interfaces/document.d.ts +0 -2
- package/dist/db/interfaces/document.d.ts.map +1 -1
- package/dist/db/interfaces/index.d.ts +2 -1
- package/dist/db/interfaces/index.d.ts.map +1 -1
- package/dist/db/interfaces/user.d.ts +2 -0
- package/dist/db/interfaces/user.d.ts.map +1 -1
- package/dist/db/utils/reference-resolver.js +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +3 -0
- package/dist/field-validation/date-utils.d.ts +30 -0
- package/dist/field-validation/date-utils.d.ts.map +1 -0
- package/dist/field-validation/date-utils.js +147 -0
- package/dist/field-validation/rule.d.ts +4 -0
- package/dist/field-validation/rule.d.ts.map +1 -1
- package/dist/field-validation/rule.js +170 -4
- package/dist/field-validation/utils.d.ts +7 -3
- package/dist/field-validation/utils.d.ts.map +1 -1
- package/dist/field-validation/utils.js +130 -38
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +38 -21
- package/dist/lib/field-validation/date-utils.js +147 -0
- package/dist/lib/field-validation/rule.js +170 -4
- package/dist/lib/field-validation/utils.js +130 -38
- package/dist/local-api/collection-api.d.ts +16 -4
- package/dist/local-api/collection-api.d.ts.map +1 -1
- package/dist/local-api/collection-api.js +51 -17
- package/dist/local-api/index.d.ts +1 -1
- package/dist/local-api/index.d.ts.map +1 -1
- package/dist/routes/assets-cdn.d.ts.map +1 -1
- package/dist/routes/assets-cdn.js +14 -7
- package/dist/routes/assets.d.ts.map +1 -1
- package/dist/routes/assets.js +6 -1
- package/dist/routes/documents-by-id.d.ts.map +1 -1
- package/dist/routes/documents-by-id.js +18 -7
- package/dist/routes/documents-publish.js +2 -2
- package/dist/routes/documents-query.d.ts +3 -1
- package/dist/routes/documents-query.d.ts.map +1 -1
- package/dist/routes/documents-query.js +6 -2
- package/dist/routes/documents.d.ts.map +1 -1
- package/dist/routes/documents.js +20 -4
- package/dist/routes/index.d.ts +1 -0
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +2 -0
- package/dist/routes/user-preferences.d.ts +4 -0
- package/dist/routes/user-preferences.d.ts.map +1 -0
- package/dist/routes/user-preferences.js +77 -0
- package/dist/schema-utils/utils.d.ts +4 -0
- package/dist/schema-utils/utils.d.ts.map +1 -1
- package/dist/schema-utils/utils.js +23 -2
- package/dist/schema-utils/validator.d.ts +4 -0
- package/dist/schema-utils/validator.d.ts.map +1 -1
- package/dist/schema-utils/validator.js +120 -0
- package/dist/types/filters.d.ts +13 -0
- package/dist/types/filters.d.ts.map +1 -1
- package/dist/types/organization.d.ts +3 -0
- package/dist/types/organization.d.ts.map +1 -1
- package/dist/types/schemas.d.ts +67 -7
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/utils/default-orderings.d.ts +10 -0
- package/dist/utils/default-orderings.d.ts.map +1 -0
- package/dist/utils/default-orderings.js +63 -0
- package/dist/utils/field-defaults.d.ts +8 -0
- package/dist/utils/field-defaults.d.ts.map +1 -0
- package/dist/utils/field-defaults.js +20 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/initial-value-helpers.d.ts +50 -0
- package/dist/utils/initial-value-helpers.d.ts.map +1 -0
- package/dist/utils/initial-value-helpers.js +70 -0
- package/package.json +6 -4
- package/dist/components/admin/DocumentTypesList.svelte +0 -97
- package/dist/components/admin/DocumentTypesList.svelte.d.ts +0 -14
- package/dist/components/admin/DocumentTypesList.svelte.d.ts.map +0 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
2
|
+
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
|
3
|
+
import utc from 'dayjs/plugin/utc';
|
|
4
|
+
dayjs.extend(customParseFormat);
|
|
5
|
+
dayjs.extend(utc);
|
|
6
|
+
/**
|
|
7
|
+
* Convert a date value to user format for validation
|
|
8
|
+
* Handles both ISO format and user format inputs
|
|
9
|
+
*/
|
|
10
|
+
export function convertDateToUserFormat(value, userFormat) {
|
|
11
|
+
// Try parsing as ISO format first (what DateField stores)
|
|
12
|
+
// Use strict mode to reject invalid dates
|
|
13
|
+
const parsedISO = dayjs(value, 'YYYY-MM-DD', true);
|
|
14
|
+
if (parsedISO.isValid()) {
|
|
15
|
+
return parsedISO.format(userFormat);
|
|
16
|
+
}
|
|
17
|
+
// Try parsing as user format (what API might send)
|
|
18
|
+
// Use strict mode to reject invalid dates like Feb 31st
|
|
19
|
+
const parsedUser = dayjs(value, userFormat, true);
|
|
20
|
+
if (parsedUser.isValid()) {
|
|
21
|
+
return value; // Already in user format
|
|
22
|
+
}
|
|
23
|
+
// Invalid date - return as-is for validation to catch
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Convert a date value to ISO format for storage
|
|
28
|
+
* Returns ISO if already valid, or original value if invalid
|
|
29
|
+
*/
|
|
30
|
+
export function convertDateToISO(value, userFormat) {
|
|
31
|
+
// Try parsing as user format first (for API consumers)
|
|
32
|
+
// Use strict mode to reject invalid dates like Feb 31st
|
|
33
|
+
const parsedUser = dayjs(value, userFormat, true);
|
|
34
|
+
if (parsedUser.isValid()) {
|
|
35
|
+
return parsedUser.format('YYYY-MM-DD');
|
|
36
|
+
}
|
|
37
|
+
// Try parsing as ISO (for DateField component)
|
|
38
|
+
// Use strict mode
|
|
39
|
+
const parsedISO = dayjs(value, 'YYYY-MM-DD', true);
|
|
40
|
+
if (parsedISO.isValid()) {
|
|
41
|
+
return value; // Already ISO
|
|
42
|
+
}
|
|
43
|
+
// Invalid date - return as-is
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Convert a datetime value to user format for validation
|
|
48
|
+
* Handles both ISO datetime and user format inputs
|
|
49
|
+
*/
|
|
50
|
+
export function convertDateTimeToUserFormat(value, dateFormat, timeFormat = 'HH:mm') {
|
|
51
|
+
const userFormat = `${dateFormat} ${timeFormat}`;
|
|
52
|
+
// Try parsing as ISO datetime first (what DateTimeField stores)
|
|
53
|
+
// Use strict mode to reject invalid dates
|
|
54
|
+
const parsedISO = dayjs(value, 'YYYY-MM-DDTHH:mm:ss[Z]', true);
|
|
55
|
+
if (parsedISO.isValid()) {
|
|
56
|
+
return parsedISO.format(userFormat);
|
|
57
|
+
}
|
|
58
|
+
// Try parsing as user format (what API might send)
|
|
59
|
+
// Use strict mode to reject invalid dates like Feb 31st
|
|
60
|
+
const parsedUser = dayjs(value, userFormat, true);
|
|
61
|
+
if (parsedUser.isValid()) {
|
|
62
|
+
return value; // Already in user format
|
|
63
|
+
}
|
|
64
|
+
// Invalid datetime - return as-is for validation to catch
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Convert a datetime value to ISO UTC format for storage
|
|
69
|
+
* Returns ISO UTC if already valid, or original value if invalid
|
|
70
|
+
*/
|
|
71
|
+
export function convertDateTimeToISO(value, dateFormat, timeFormat = 'HH:mm') {
|
|
72
|
+
const userFormat = `${dateFormat} ${timeFormat}`;
|
|
73
|
+
console.log('[convertDateTimeToISO]', { value, userFormat });
|
|
74
|
+
// Try parsing as user format first (for API consumers)
|
|
75
|
+
// Use strict mode to reject invalid dates like Feb 31st
|
|
76
|
+
const parsedUser = dayjs(value, userFormat, true);
|
|
77
|
+
if (parsedUser.isValid()) {
|
|
78
|
+
const result = parsedUser.utc().format('YYYY-MM-DDTHH:mm:ss[Z]');
|
|
79
|
+
console.log('[convertDateTimeToISO] User format parse successful, converting to UTC:', result);
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
// Try parsing as ISO datetime (for DateTimeField component)
|
|
83
|
+
// Use strict mode with ISO format
|
|
84
|
+
const parsedISO = dayjs(value, 'YYYY-MM-DDTHH:mm:ss[Z]', true);
|
|
85
|
+
if (parsedISO.isValid()) {
|
|
86
|
+
const result = parsedISO.utc().format('YYYY-MM-DDTHH:mm:ss[Z]');
|
|
87
|
+
console.log('[convertDateTimeToISO] ISO parse successful:', result);
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
// Invalid datetime - return as-is
|
|
91
|
+
console.log('[convertDateTimeToISO] Invalid - returning as-is');
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Normalize date fields in data object
|
|
96
|
+
* Converts dates to ISO for storage and creates a parallel object with user-formatted dates for validation
|
|
97
|
+
*/
|
|
98
|
+
export function normalizeDateFields(data, schema) {
|
|
99
|
+
const normalizedData = { ...data };
|
|
100
|
+
const dataForValidation = { ...data };
|
|
101
|
+
console.log('[normalizeDateFields] Starting normalization...');
|
|
102
|
+
console.log('[normalizeDateFields] Input data:', data);
|
|
103
|
+
for (const field of schema.fields) {
|
|
104
|
+
if (field.type === 'date' && normalizedData[field.name]) {
|
|
105
|
+
const dateField = field;
|
|
106
|
+
const userFormat = dateField.options?.dateFormat || 'YYYY-MM-DD';
|
|
107
|
+
const dateValue = normalizedData[field.name];
|
|
108
|
+
console.log(`[normalizeDateFields] Processing DATE field "${field.name}"`, {
|
|
109
|
+
originalValue: dateValue,
|
|
110
|
+
userFormat
|
|
111
|
+
});
|
|
112
|
+
if (typeof dateValue === 'string') {
|
|
113
|
+
normalizedData[field.name] = convertDateToISO(dateValue, userFormat);
|
|
114
|
+
dataForValidation[field.name] = convertDateToUserFormat(dateValue, userFormat);
|
|
115
|
+
console.log(`[normalizeDateFields] Converted DATE field "${field.name}"`, {
|
|
116
|
+
normalizedValue: normalizedData[field.name],
|
|
117
|
+
validationValue: dataForValidation[field.name]
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else if (field.type === 'datetime' && normalizedData[field.name]) {
|
|
122
|
+
const dateTimeField = field;
|
|
123
|
+
const dateFormat = dateTimeField.options?.dateFormat || 'YYYY-MM-DD';
|
|
124
|
+
const timeFormat = dateTimeField.options?.timeFormat || 'HH:mm';
|
|
125
|
+
const dateTimeValue = normalizedData[field.name];
|
|
126
|
+
console.log(`[normalizeDateFields] Processing DATETIME field "${field.name}"`, {
|
|
127
|
+
originalValue: dateTimeValue,
|
|
128
|
+
dateFormat,
|
|
129
|
+
timeFormat,
|
|
130
|
+
combinedFormat: `${dateFormat} ${timeFormat}`
|
|
131
|
+
});
|
|
132
|
+
if (typeof dateTimeValue === 'string') {
|
|
133
|
+
normalizedData[field.name] = convertDateTimeToISO(dateTimeValue, dateFormat, timeFormat);
|
|
134
|
+
dataForValidation[field.name] = convertDateTimeToUserFormat(dateTimeValue, dateFormat, timeFormat);
|
|
135
|
+
console.log(`[normalizeDateFields] Converted DATETIME field "${field.name}"`, {
|
|
136
|
+
normalizedValue: normalizedData[field.name],
|
|
137
|
+
validationValue: dataForValidation[field.name]
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
console.log('[normalizeDateFields] Final result:', {
|
|
143
|
+
normalizedData,
|
|
144
|
+
dataForValidation
|
|
145
|
+
});
|
|
146
|
+
return { normalizedData, dataForValidation };
|
|
147
|
+
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// Sanity-style validation Rule implementation
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
|
4
|
+
// Enable strict parsing
|
|
5
|
+
dayjs.extend(customParseFormat);
|
|
1
6
|
export class Rule {
|
|
2
7
|
_required = false;
|
|
3
8
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -39,6 +44,11 @@ export class Rule {
|
|
|
39
44
|
newRule._rules.push({ type: 'length', constraint: len });
|
|
40
45
|
return newRule;
|
|
41
46
|
}
|
|
47
|
+
unique() {
|
|
48
|
+
const newRule = this.clone();
|
|
49
|
+
newRule._rules.push({ type: 'unique' });
|
|
50
|
+
return newRule;
|
|
51
|
+
}
|
|
42
52
|
email() {
|
|
43
53
|
const newRule = this.clone();
|
|
44
54
|
newRule._rules.push({ type: 'email' });
|
|
@@ -79,6 +89,17 @@ export class Rule {
|
|
|
79
89
|
newRule._rules.push({ type: 'lessThan', constraint: num });
|
|
80
90
|
return newRule;
|
|
81
91
|
}
|
|
92
|
+
date(format) {
|
|
93
|
+
const newRule = this.clone();
|
|
94
|
+
newRule._rules.push({ type: 'date', constraint: format || 'YYYY-MM-DD' });
|
|
95
|
+
return newRule;
|
|
96
|
+
}
|
|
97
|
+
datetime(dateFormat, timeFormat) {
|
|
98
|
+
const newRule = this.clone();
|
|
99
|
+
const fullFormat = `${dateFormat || 'YYYY-MM-DD'} ${timeFormat || 'HH:mm'}`;
|
|
100
|
+
newRule._rules.push({ type: 'datetime', constraint: fullFormat });
|
|
101
|
+
return newRule;
|
|
102
|
+
}
|
|
82
103
|
custom(fn) {
|
|
83
104
|
const newRule = this.clone();
|
|
84
105
|
newRule._rules.push({ type: 'custom', constraint: fn });
|
|
@@ -155,6 +176,9 @@ export class Rule {
|
|
|
155
176
|
if (typeof value === 'number' && value < rule.constraint) {
|
|
156
177
|
return `Must be at least ${rule.constraint}`;
|
|
157
178
|
}
|
|
179
|
+
if (Array.isArray(value) && value.length < rule.constraint) {
|
|
180
|
+
return `Must have at least ${rule.constraint} item${rule.constraint === 1 ? '' : 's'}`;
|
|
181
|
+
}
|
|
158
182
|
break;
|
|
159
183
|
case 'max':
|
|
160
184
|
if (typeof value === 'string' && value.length > rule.constraint) {
|
|
@@ -163,6 +187,31 @@ export class Rule {
|
|
|
163
187
|
if (typeof value === 'number' && value > rule.constraint) {
|
|
164
188
|
return `Must be at most ${rule.constraint}`;
|
|
165
189
|
}
|
|
190
|
+
if (Array.isArray(value) && value.length > rule.constraint) {
|
|
191
|
+
return `Must have at most ${rule.constraint} item${rule.constraint === 1 ? '' : 's'}`;
|
|
192
|
+
}
|
|
193
|
+
break;
|
|
194
|
+
case 'length':
|
|
195
|
+
if (Array.isArray(value) && value.length !== rule.constraint) {
|
|
196
|
+
return `Must have exactly ${rule.constraint} item${rule.constraint === 1 ? '' : 's'}`;
|
|
197
|
+
}
|
|
198
|
+
if (typeof value === 'string' && value.length !== rule.constraint) {
|
|
199
|
+
return `Must be exactly ${rule.constraint} characters`;
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
case 'unique':
|
|
203
|
+
if (Array.isArray(value)) {
|
|
204
|
+
const seen = new Set();
|
|
205
|
+
for (const item of value) {
|
|
206
|
+
// Deep comparison excluding _key property
|
|
207
|
+
const normalized = this.normalizeForComparison(item);
|
|
208
|
+
const serialized = JSON.stringify(normalized);
|
|
209
|
+
if (seen.has(serialized)) {
|
|
210
|
+
return 'All items must be unique';
|
|
211
|
+
}
|
|
212
|
+
seen.add(serialized);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
166
215
|
break;
|
|
167
216
|
case 'email':
|
|
168
217
|
if (typeof value === 'string' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
@@ -171,11 +220,57 @@ export class Rule {
|
|
|
171
220
|
break;
|
|
172
221
|
case 'uri':
|
|
173
222
|
if (typeof value === 'string') {
|
|
174
|
-
|
|
175
|
-
|
|
223
|
+
const opts = rule.constraint || {};
|
|
224
|
+
const schemes = opts.scheme || [/^https?$/];
|
|
225
|
+
const allowRelative = opts.allowRelative || false;
|
|
226
|
+
const relativeOnly = opts.relativeOnly || false;
|
|
227
|
+
// Check if URL is relative (doesn't have a scheme)
|
|
228
|
+
const hasScheme = /^[a-z][a-z0-9+.-]*:/i.test(value);
|
|
229
|
+
const isProtocolRelative = value.startsWith('//');
|
|
230
|
+
if (relativeOnly) {
|
|
231
|
+
// Only allow relative URLs
|
|
232
|
+
if (hasScheme || isProtocolRelative) {
|
|
233
|
+
return 'Must be a relative URL';
|
|
234
|
+
}
|
|
235
|
+
// Validate it's a reasonable path
|
|
236
|
+
if (!value.startsWith('/') &&
|
|
237
|
+
!value.startsWith('.') &&
|
|
238
|
+
!value.startsWith('#') &&
|
|
239
|
+
!value.startsWith('?')) {
|
|
240
|
+
return 'Must be a relative URL starting with /, ., #, or ?';
|
|
241
|
+
}
|
|
176
242
|
}
|
|
177
|
-
|
|
178
|
-
|
|
243
|
+
else if (!hasScheme && !isProtocolRelative) {
|
|
244
|
+
// It's a relative URL
|
|
245
|
+
if (!allowRelative) {
|
|
246
|
+
return 'Must be an absolute URL';
|
|
247
|
+
}
|
|
248
|
+
// Validate it's a reasonable relative path
|
|
249
|
+
if (!value.startsWith('/') &&
|
|
250
|
+
!value.startsWith('.') &&
|
|
251
|
+
!value.startsWith('#') &&
|
|
252
|
+
!value.startsWith('?')) {
|
|
253
|
+
return 'Must be a valid relative URL';
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
// It's an absolute URL, validate with URL constructor
|
|
258
|
+
try {
|
|
259
|
+
const url = new URL(value);
|
|
260
|
+
// Extract scheme without the trailing colon
|
|
261
|
+
const urlScheme = url.protocol.slice(0, -1);
|
|
262
|
+
// Check if scheme is allowed
|
|
263
|
+
const schemeMatches = schemes.some((s) => s instanceof RegExp ? s.test(urlScheme) : s === urlScheme);
|
|
264
|
+
if (!schemeMatches) {
|
|
265
|
+
const schemeList = schemes
|
|
266
|
+
.map((s) => (s instanceof RegExp ? s.toString() : s))
|
|
267
|
+
.join(', ');
|
|
268
|
+
return `URL scheme must be one of: ${schemeList}`;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return 'Must be a valid URL';
|
|
273
|
+
}
|
|
179
274
|
}
|
|
180
275
|
}
|
|
181
276
|
break;
|
|
@@ -199,6 +294,58 @@ export class Rule {
|
|
|
199
294
|
return 'Must be an integer';
|
|
200
295
|
}
|
|
201
296
|
break;
|
|
297
|
+
case 'date': {
|
|
298
|
+
if (typeof value === 'string') {
|
|
299
|
+
const format = rule.constraint || 'YYYY-MM-DD';
|
|
300
|
+
console.log('[Rule.validate] DATE validation', { value, format });
|
|
301
|
+
// Parse with strict mode
|
|
302
|
+
const parsed = dayjs(value, format, true);
|
|
303
|
+
console.log('[Rule.validate] DATE parsed', {
|
|
304
|
+
isValid: parsed.isValid(),
|
|
305
|
+
parsed: parsed.format()
|
|
306
|
+
});
|
|
307
|
+
if (!parsed.isValid()) {
|
|
308
|
+
console.log('[Rule.validate] DATE validation FAILED - invalid format');
|
|
309
|
+
return `Invalid date format. Expected: ${format}`;
|
|
310
|
+
}
|
|
311
|
+
// Verify the parsed date matches the input (catches invalid dates like 2025-02-31)
|
|
312
|
+
if (parsed.format(format) !== value) {
|
|
313
|
+
console.log('[Rule.validate] DATE validation FAILED - format mismatch', {
|
|
314
|
+
expected: value,
|
|
315
|
+
got: parsed.format(format)
|
|
316
|
+
});
|
|
317
|
+
return `Invalid date. Expected format: ${format}`;
|
|
318
|
+
}
|
|
319
|
+
console.log('[Rule.validate] DATE validation PASSED');
|
|
320
|
+
}
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
case 'datetime': {
|
|
324
|
+
if (typeof value === 'string') {
|
|
325
|
+
const format = rule.constraint || 'YYYY-MM-DD HH:mm';
|
|
326
|
+
console.log('[Rule.validate] DATETIME validation', { value, format });
|
|
327
|
+
// Parse with strict mode
|
|
328
|
+
const parsed = dayjs(value, format, true);
|
|
329
|
+
console.log('[Rule.validate] DATETIME parsed', {
|
|
330
|
+
isValid: parsed.isValid(),
|
|
331
|
+
parsed: parsed.format()
|
|
332
|
+
});
|
|
333
|
+
if (!parsed.isValid()) {
|
|
334
|
+
console.log('[Rule.validate] DATETIME validation FAILED - invalid format');
|
|
335
|
+
return `Invalid datetime format. Expected: ${format}`;
|
|
336
|
+
}
|
|
337
|
+
// Verify the parsed datetime matches the input (catches invalid dates like 2025-02-31 23:59)
|
|
338
|
+
if (parsed.format(format) !== value) {
|
|
339
|
+
console.log('[Rule.validate] DATETIME validation FAILED - format mismatch', {
|
|
340
|
+
expected: value,
|
|
341
|
+
got: parsed.format(format)
|
|
342
|
+
});
|
|
343
|
+
return `Invalid datetime. Expected format: ${format}`;
|
|
344
|
+
}
|
|
345
|
+
console.log('[Rule.validate] DATETIME validation PASSED');
|
|
346
|
+
}
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
202
349
|
case 'custom': {
|
|
203
350
|
const customResult = await rule.constraint(value, context);
|
|
204
351
|
if (customResult === false) {
|
|
@@ -218,4 +365,23 @@ export class Rule {
|
|
|
218
365
|
isRequired() {
|
|
219
366
|
return this._required;
|
|
220
367
|
}
|
|
368
|
+
// Helper method to normalize objects for comparison (exclude _key)
|
|
369
|
+
normalizeForComparison(value) {
|
|
370
|
+
if (value === null || value === undefined) {
|
|
371
|
+
return value;
|
|
372
|
+
}
|
|
373
|
+
if (Array.isArray(value)) {
|
|
374
|
+
return value.map((item) => this.normalizeForComparison(item));
|
|
375
|
+
}
|
|
376
|
+
if (typeof value === 'object') {
|
|
377
|
+
const normalized = {};
|
|
378
|
+
for (const [key, val] of Object.entries(value)) {
|
|
379
|
+
if (key !== '_key') {
|
|
380
|
+
normalized[key] = this.normalizeForComparison(val);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return normalized;
|
|
384
|
+
}
|
|
385
|
+
return value;
|
|
386
|
+
}
|
|
221
387
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Rule } from './rule.js';
|
|
2
|
+
import { normalizeDateFields } from './date-utils.js';
|
|
2
3
|
/**
|
|
3
4
|
* Check if a field is required based on its validation rules
|
|
4
5
|
*/
|
|
@@ -20,39 +21,106 @@ export function isFieldRequired(field) {
|
|
|
20
21
|
* Validate a field value against its validation rules
|
|
21
22
|
*/
|
|
22
23
|
export async function validateField(field, value, context = {}) {
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
console.log(`[validateField] Validating field "${field.name}"`, {
|
|
25
|
+
type: field.type,
|
|
26
|
+
value,
|
|
27
|
+
hasValidation: !!field.validation
|
|
28
|
+
});
|
|
29
|
+
const allErrors = [];
|
|
30
|
+
// Add automatic validation for date/datetime/url fields based on type
|
|
31
|
+
if (field.type === 'date') {
|
|
32
|
+
const dateField = field;
|
|
33
|
+
const dateFormat = dateField.options?.dateFormat || 'YYYY-MM-DD';
|
|
34
|
+
console.log(`[validateField] Adding automatic DATE validation for "${field.name}"`, {
|
|
35
|
+
dateFormat
|
|
36
|
+
});
|
|
37
|
+
const autoRule = new Rule().date(dateFormat);
|
|
38
|
+
const markers = await autoRule.validate(value, {
|
|
39
|
+
path: [field.name],
|
|
40
|
+
...context
|
|
41
|
+
});
|
|
42
|
+
allErrors.push(...markers.map((marker) => ({
|
|
43
|
+
level: marker.level,
|
|
44
|
+
message: marker.message
|
|
45
|
+
})));
|
|
25
46
|
}
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
47
|
+
else if (field.type === 'datetime') {
|
|
48
|
+
const dateTimeField = field;
|
|
49
|
+
const dateFormat = dateTimeField.options?.dateFormat || 'YYYY-MM-DD';
|
|
50
|
+
const timeFormat = dateTimeField.options?.timeFormat || 'HH:mm';
|
|
51
|
+
console.log(`[validateField] Adding automatic DATETIME validation for "${field.name}"`, {
|
|
52
|
+
dateFormat,
|
|
53
|
+
timeFormat
|
|
54
|
+
});
|
|
55
|
+
const autoRule = new Rule().datetime(dateFormat, timeFormat);
|
|
56
|
+
const markers = await autoRule.validate(value, {
|
|
57
|
+
path: [field.name],
|
|
58
|
+
...context
|
|
59
|
+
});
|
|
60
|
+
allErrors.push(...markers.map((marker) => ({
|
|
61
|
+
level: marker.level,
|
|
62
|
+
message: marker.message
|
|
63
|
+
})));
|
|
64
|
+
}
|
|
65
|
+
else if (field.type === 'url') {
|
|
66
|
+
// Only add automatic URL validation if there's no custom validation
|
|
67
|
+
// This allows custom validation to specify different options (scheme, allowRelative, relativeOnly)
|
|
68
|
+
if (!field.validation) {
|
|
69
|
+
console.log(`[validateField] Adding automatic URL validation for "${field.name}"`);
|
|
70
|
+
// Automatic URL validation - only validate if there's a value
|
|
71
|
+
if (value && value !== '') {
|
|
72
|
+
const autoRule = new Rule().uri();
|
|
73
|
+
const markers = await autoRule.validate(value, {
|
|
74
|
+
path: [field.name],
|
|
75
|
+
...context
|
|
76
|
+
});
|
|
77
|
+
allErrors.push(...markers.map((marker) => ({
|
|
78
|
+
level: marker.level,
|
|
79
|
+
message: marker.message
|
|
80
|
+
})));
|
|
36
81
|
}
|
|
37
|
-
const markers = await rule.validate(value, {
|
|
38
|
-
path: [field.name],
|
|
39
|
-
...context
|
|
40
|
-
});
|
|
41
|
-
allErrors.push(...markers.map((marker) => ({
|
|
42
|
-
level: marker.level,
|
|
43
|
-
message: marker.message
|
|
44
|
-
})));
|
|
45
82
|
}
|
|
46
|
-
|
|
47
|
-
|
|
83
|
+
else {
|
|
84
|
+
console.log(`[validateField] Skipping automatic URL validation for "${field.name}" (has custom validation)`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Run user-defined validation rules if present
|
|
88
|
+
if (!field.validation) {
|
|
89
|
+
console.log(`[validateField] No custom validation rules for "${field.name}"`);
|
|
48
90
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
91
|
+
else {
|
|
92
|
+
try {
|
|
93
|
+
const validationFunctions = Array.isArray(field.validation)
|
|
94
|
+
? field.validation
|
|
95
|
+
: [field.validation];
|
|
96
|
+
console.log(`[validateField] Field "${field.name}" has ${validationFunctions.length} custom validation function(s)`);
|
|
97
|
+
for (const validationFn of validationFunctions) {
|
|
98
|
+
const rule = validationFn(new Rule());
|
|
99
|
+
if (!(rule instanceof Rule)) {
|
|
100
|
+
console.error(`Validation function for field "${field.name}" did not return a Rule object. Make sure you are chaining validation methods and returning the result.`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const markers = await rule.validate(value, {
|
|
104
|
+
path: [field.name],
|
|
105
|
+
...context
|
|
106
|
+
});
|
|
107
|
+
allErrors.push(...markers.map((marker) => ({
|
|
108
|
+
level: marker.level,
|
|
109
|
+
message: marker.message
|
|
110
|
+
})));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
console.error(`[validateField] Validation error for "${field.name}":`, error);
|
|
115
|
+
allErrors.push({ level: 'error', message: 'Validation failed' });
|
|
116
|
+
}
|
|
55
117
|
}
|
|
118
|
+
const isValid = allErrors.filter((e) => e.level === 'error').length === 0;
|
|
119
|
+
console.log(`[validateField] Field "${field.name}" validation complete`, {
|
|
120
|
+
isValid,
|
|
121
|
+
errors: allErrors
|
|
122
|
+
});
|
|
123
|
+
return { isValid, errors: allErrors };
|
|
56
124
|
}
|
|
57
125
|
/**
|
|
58
126
|
* Get validation CSS classes for input styling
|
|
@@ -66,24 +134,43 @@ export function getValidationClasses(hasErrors) {
|
|
|
66
134
|
}
|
|
67
135
|
/**
|
|
68
136
|
* Validate an entire document's data against a schema
|
|
69
|
-
* This function
|
|
70
|
-
*
|
|
137
|
+
* This function:
|
|
138
|
+
* 1. Normalizes date fields (converts user format to ISO for storage)
|
|
139
|
+
* 2. Converts ISO dates to user format for validation
|
|
140
|
+
* 3. Validates all fields and returns errors
|
|
141
|
+
* 4. Returns normalized data (with ISO dates) for storage
|
|
71
142
|
*
|
|
72
143
|
* @param schema - The schema type containing field definitions
|
|
73
144
|
* @param data - The document data to validate
|
|
74
145
|
* @param context - Optional context to pass to field validators
|
|
75
|
-
* @returns Validation result with isValid flag and
|
|
146
|
+
* @returns Validation result with isValid flag, errors, and normalized data
|
|
76
147
|
*/
|
|
77
148
|
export async function validateDocumentData(schema, data, context = {}) {
|
|
149
|
+
console.log('[validateDocumentData] Starting validation', {
|
|
150
|
+
schemaName: schema.name,
|
|
151
|
+
data
|
|
152
|
+
});
|
|
78
153
|
const validationErrors = [];
|
|
79
|
-
//
|
|
154
|
+
// Normalize date fields: convert to ISO for storage, user format for validation
|
|
155
|
+
const { normalizedData, dataForValidation } = normalizeDateFields(data, schema);
|
|
156
|
+
console.log('[validateDocumentData] After normalization', {
|
|
157
|
+
normalizedData,
|
|
158
|
+
dataForValidation
|
|
159
|
+
});
|
|
160
|
+
// Validate each field using the user-formatted data
|
|
80
161
|
for (const field of schema.fields) {
|
|
81
|
-
const value =
|
|
82
|
-
|
|
162
|
+
const value = dataForValidation[field.name];
|
|
163
|
+
console.log(`[validateDocumentData] Validating field "${field.name}"`, {
|
|
164
|
+
type: field.type,
|
|
165
|
+
value
|
|
166
|
+
});
|
|
167
|
+
const result = await validateField(field, value, { ...context, ...dataForValidation });
|
|
168
|
+
console.log(`[validateDocumentData] Field "${field.name}" validation result`, {
|
|
169
|
+
isValid: result.isValid,
|
|
170
|
+
errors: result.errors
|
|
171
|
+
});
|
|
83
172
|
if (!result.isValid) {
|
|
84
|
-
const errorMessages = result.errors
|
|
85
|
-
.filter((e) => e.level === 'error')
|
|
86
|
-
.map((e) => e.message);
|
|
173
|
+
const errorMessages = result.errors.filter((e) => e.level === 'error').map((e) => e.message);
|
|
87
174
|
if (errorMessages.length > 0) {
|
|
88
175
|
validationErrors.push({
|
|
89
176
|
field: field.name,
|
|
@@ -92,8 +179,13 @@ export async function validateDocumentData(schema, data, context = {}) {
|
|
|
92
179
|
}
|
|
93
180
|
}
|
|
94
181
|
}
|
|
95
|
-
|
|
182
|
+
console.log('[validateDocumentData] Final result', {
|
|
96
183
|
isValid: validationErrors.length === 0,
|
|
97
184
|
errors: validationErrors
|
|
185
|
+
});
|
|
186
|
+
return {
|
|
187
|
+
isValid: validationErrors.length === 0,
|
|
188
|
+
errors: validationErrors,
|
|
189
|
+
normalizedData
|
|
98
190
|
};
|
|
99
191
|
}
|
|
@@ -4,6 +4,14 @@ import type { Document } from '../types/document.js';
|
|
|
4
4
|
import type { LocalAPIContext } from './types.js';
|
|
5
5
|
import type { SchemaType } from '../types/schemas.js';
|
|
6
6
|
import { PermissionChecker } from './permissions.js';
|
|
7
|
+
import { type DocumentValidationResult } from '../field-validation/utils.js';
|
|
8
|
+
/**
|
|
9
|
+
* Result from create/update operations that includes validation
|
|
10
|
+
*/
|
|
11
|
+
export interface DocumentResult<T> {
|
|
12
|
+
document: T;
|
|
13
|
+
validation: DocumentValidationResult;
|
|
14
|
+
}
|
|
7
15
|
/**
|
|
8
16
|
* Collection API - provides type-safe operations for a single collection
|
|
9
17
|
* Generic type T represents the document type for this collection
|
|
@@ -69,7 +77,7 @@ export declare class CollectionAPI<T = Document> {
|
|
|
69
77
|
*
|
|
70
78
|
* @example
|
|
71
79
|
* ```typescript
|
|
72
|
-
* const
|
|
80
|
+
* const result = await api.collections.pages.create(
|
|
73
81
|
* { organizationId: 'org_123', user },
|
|
74
82
|
* {
|
|
75
83
|
* title: 'New Page',
|
|
@@ -77,27 +85,31 @@ export declare class CollectionAPI<T = Document> {
|
|
|
77
85
|
* content: []
|
|
78
86
|
* }
|
|
79
87
|
* );
|
|
88
|
+
* // result.document - the created document
|
|
89
|
+
* // result.validation - validation results
|
|
80
90
|
* ```
|
|
81
91
|
*/
|
|
82
92
|
create(context: LocalAPIContext, data: Omit<T, 'id' | '_meta'>, options?: {
|
|
83
93
|
publish?: boolean;
|
|
84
|
-
}): Promise<T
|
|
94
|
+
}): Promise<DocumentResult<T>>;
|
|
85
95
|
/**
|
|
86
96
|
* Update an existing document
|
|
87
97
|
*
|
|
88
98
|
* @example
|
|
89
99
|
* ```typescript
|
|
90
|
-
* const
|
|
100
|
+
* const result = await api.collections.pages.update(
|
|
91
101
|
* { organizationId: 'org_123', user },
|
|
92
102
|
* 'doc_123',
|
|
93
103
|
* { title: 'Updated Title' },
|
|
94
104
|
* { publish: true }
|
|
95
105
|
* );
|
|
106
|
+
* // result.document - the updated document
|
|
107
|
+
* // result.validation - validation results
|
|
96
108
|
* ```
|
|
97
109
|
*/
|
|
98
110
|
update(context: LocalAPIContext, id: string, data: Partial<Omit<T, 'id' | '_meta'>>, options?: {
|
|
99
111
|
publish?: boolean;
|
|
100
|
-
}): Promise<T | null>;
|
|
112
|
+
}): Promise<DocumentResult<T> | null>;
|
|
101
113
|
/**
|
|
102
114
|
* Delete a document
|
|
103
115
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection-api.d.ts","sourceRoot":"","sources":["../../src/lib/local-api/collection-api.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"collection-api.d.ts","sourceRoot":"","sources":["../../src/lib/local-api/collection-api.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAwB,KAAK,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAEhG;;GAEG;AACH,MAAM,WAAW,cAAc,CAAC,CAAC;IAChC,QAAQ,EAAE,CAAC,CAAC;IACZ,UAAU,EAAE,wBAAwB,CAAC;CACrC;AA4BD;;;GAGG;AACH,qBAAa,aAAa,CAAC,CAAC,GAAG,QAAQ;IAErC,OAAO,CAAC,cAAc;IACtB,OAAO,CAAC,eAAe;IACvB,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,WAAW;gBAHX,cAAc,EAAE,MAAM,EACtB,eAAe,EAAE,eAAe,EAChC,OAAO,EAAE,UAAU,EACnB,WAAW,EAAE,iBAAiB;IAMvC;;OAEG;IACH,IAAI,MAAM,IAAI,UAAU,CAEvB;IAED;;;;;;;;;;;;;;;;;OAiBG;IACG,IAAI,CAAC,OAAO,EAAE,eAAe,EAAE,OAAO,GAAE,WAAW,CAAC,CAAC,CAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IAqB1F;;;;;;;;;;;OAWG;IACG,QAAQ,CACb,OAAO,EAAE,eAAe,EACxB,EAAE,EAAE,MAAM,EACV,OAAO,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,GAC/B,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAmBpB;;;;;;;;;;OAUG;IACG,KAAK,CACV,OAAO,EAAE,eAAe,EACxB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,KAAK,CAAA;KAAE,GACzC,OAAO,CAAC,MAAM,CAAC;IAWlB;;;;;;;;;;;;;;;;OAgBG;IACG,MAAM,CACX,OAAO,EAAE,eAAe,EACxB,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,IAAI,GAAG,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,GAC7B,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC;IA4C7B;;;;;;;;;;;;;;OAcG;IACG,MAAM,CACX,OAAO,EAAE,eAAe,EACxB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,GAAG,OAAO,CAAC,CAAC,EACtC,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,GAC7B,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAkEpC;;;;;;;;;;OAUG;IACG,MAAM,CAAC,OAAO,EAAE,eAAe,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAOpE;;;;;;;;;;OAUG;IACG,OAAO,CAAC,OAAO,EAAE,eAAe,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAgCtE;;;;;;;;;;OAUG;IACG,SAAS,CAAC,OAAO,EAAE,eAAe,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;CAUxE"}
|