@script-it/json-views-core 0.1.0-beta.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/LICENSE +21 -0
- package/README.md +174 -0
- package/THIRD_PARTY_NOTICES.txt +4917 -0
- package/dist/annotation-capabilities.d.ts +17 -0
- package/dist/annotation-capabilities.js +21 -0
- package/dist/atomic-value.d.ts +3 -0
- package/dist/atomic-value.js +6 -0
- package/dist/date-type.d.ts +48 -0
- package/dist/date-type.js +244 -0
- package/dist/diagnostics.d.ts +8 -0
- package/dist/diagnostics.js +252 -0
- package/dist/document-session.d.ts +52 -0
- package/dist/document-session.js +164 -0
- package/dist/formats/conversion.d.ts +17 -0
- package/dist/formats/conversion.js +75 -0
- package/dist/formats/csv-format.d.ts +31 -0
- package/dist/formats/csv-format.js +269 -0
- package/dist/formats/document-format.d.ts +35 -0
- package/dist/formats/document-format.js +1 -0
- package/dist/formats/json-format.d.ts +12 -0
- package/dist/formats/json-format.js +109 -0
- package/dist/html-view-bindings.d.ts +4 -0
- package/dist/html-view-bindings.js +32 -0
- package/dist/html-view-limits.d.ts +1 -0
- package/dist/html-view-limits.js +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/dist/inference-fields.d.ts +5 -0
- package/dist/inference-fields.js +23 -0
- package/dist/inference.d.ts +26 -0
- package/dist/inference.js +304 -0
- package/dist/json-limits.d.ts +8 -0
- package/dist/json-limits.js +25 -0
- package/dist/json-path.d.ts +14 -0
- package/dist/json-path.js +84 -0
- package/dist/json-source-patcher.d.ts +97 -0
- package/dist/json-source-patcher.js +778 -0
- package/dist/metadata.d.ts +137 -0
- package/dist/metadata.js +952 -0
- package/dist/safe-pattern.d.ts +5 -0
- package/dist/safe-pattern.js +20 -0
- package/dist/schema-validation.d.ts +3 -0
- package/dist/schema-validation.js +4 -0
- package/dist/source-default.d.ts +2 -0
- package/dist/source-default.js +39 -0
- package/dist/table-suitability.d.ts +5 -0
- package/dist/table-suitability.js +55 -0
- package/dist/type-registry.d.ts +57 -0
- package/dist/type-registry.js +238 -0
- package/dist/value-comparison.d.ts +18 -0
- package/dist/value-comparison.js +133 -0
- package/dist/view-model.d.ts +18 -0
- package/dist/view-model.js +134 -0
- package/package.json +52 -0
- package/schema/jsonviews.schema.json +102 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Shared vocabulary for validation and just-in-time diagnostic help. */
|
|
2
|
+
export declare const COMMON_DESCRIPTOR_PROPERTIES: readonly ["type", "title", "description", "required", "pattern"];
|
|
3
|
+
export declare const BUILT_IN_DESCRIPTOR_PROPERTIES: Readonly<Record<string, readonly string[]>>;
|
|
4
|
+
export declare const ANNOTATION_PROPERTIES: readonly ["version", "schema", "views"];
|
|
5
|
+
export declare const VIEW_PROPERTIES: readonly ["id", "name", "path", "display", "html", "css", "groupBy", "groupOrder", "orderPath", "columns", "filter", "sort"];
|
|
6
|
+
export declare const COMMON_FILTER_OPERATORS: readonly ["eq", "neq", "in", "notIn", "gt", "gte", "lt", "lte", "contains", "notContains", "isEmpty", "isNotEmpty"];
|
|
7
|
+
export interface JsonViewDiagnosticHelp {
|
|
8
|
+
/** A concrete manual repair, never an automatically applied data change. */
|
|
9
|
+
fix: string;
|
|
10
|
+
expected: string;
|
|
11
|
+
/** Bounded preview; use the source and paths for the complete original value. */
|
|
12
|
+
received?: string;
|
|
13
|
+
allowedValues?: readonly string[];
|
|
14
|
+
/** Illustrative JSON values or fragments, not patches to apply blindly. */
|
|
15
|
+
examples?: readonly unknown[];
|
|
16
|
+
capabilities?: readonly string[];
|
|
17
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Shared vocabulary for validation and just-in-time diagnostic help. */
|
|
2
|
+
export const COMMON_DESCRIPTOR_PROPERTIES = ['type', 'title', 'description', 'required', 'pattern'];
|
|
3
|
+
export const BUILT_IN_DESCRIPTOR_PROPERTIES = {
|
|
4
|
+
text: ['placeholder', 'multiline'],
|
|
5
|
+
markdown: ['placeholder'],
|
|
6
|
+
html: ['placeholder'],
|
|
7
|
+
number: ['minimum', 'maximum', 'step'],
|
|
8
|
+
checkbox: [],
|
|
9
|
+
select: ['options', 'optionColors'],
|
|
10
|
+
'multi-select': ['options', 'optionColors'],
|
|
11
|
+
date: ['minimum', 'maximum', 'defaultIncludeTime', 'placeholder'],
|
|
12
|
+
url: [],
|
|
13
|
+
email: [],
|
|
14
|
+
body: ['placeholder'],
|
|
15
|
+
};
|
|
16
|
+
export const ANNOTATION_PROPERTIES = ['version', 'schema', 'views'];
|
|
17
|
+
export const VIEW_PROPERTIES = ['id', 'name', 'path', 'display', 'html', 'css', 'groupBy', 'groupOrder', 'orderPath', 'columns', 'filter', 'sort'];
|
|
18
|
+
export const COMMON_FILTER_OPERATORS = [
|
|
19
|
+
'eq', 'neq', 'in', 'notIn', 'gt', 'gte', 'lt', 'lte',
|
|
20
|
+
'contains', 'notContains', 'isEmpty', 'isNotEmpty',
|
|
21
|
+
];
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export type DateValue = string | null;
|
|
2
|
+
export type ParsedDateKind = 'empty' | 'date' | 'datetime' | 'floating-datetime' | 'invalid';
|
|
3
|
+
export interface ParsedDateResult {
|
|
4
|
+
kind: ParsedDateKind;
|
|
5
|
+
source: unknown;
|
|
6
|
+
calendarDate?: string;
|
|
7
|
+
year?: number;
|
|
8
|
+
month?: number;
|
|
9
|
+
day?: number;
|
|
10
|
+
hour?: number;
|
|
11
|
+
minute?: number;
|
|
12
|
+
second?: number;
|
|
13
|
+
fractionalSeconds?: string;
|
|
14
|
+
timezone?: string;
|
|
15
|
+
offsetMinutes?: number;
|
|
16
|
+
epochSeconds?: number;
|
|
17
|
+
error?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface DateTypeDescriptor {
|
|
20
|
+
type: 'date';
|
|
21
|
+
title?: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
required?: boolean;
|
|
24
|
+
minimum?: DateValue;
|
|
25
|
+
maximum?: DateValue;
|
|
26
|
+
defaultIncludeTime?: boolean;
|
|
27
|
+
placeholder?: string;
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
}
|
|
30
|
+
export interface DateEditorValue {
|
|
31
|
+
date: string;
|
|
32
|
+
includeTime: boolean;
|
|
33
|
+
time?: string;
|
|
34
|
+
seconds?: number;
|
|
35
|
+
fractionalSeconds?: string;
|
|
36
|
+
timezone?: string;
|
|
37
|
+
}
|
|
38
|
+
export declare const DATE_FILTER_OPERATORS: readonly ["eq", "neq", "gt", "gte", "lt", "lte", "isEmpty", "isNotEmpty"];
|
|
39
|
+
export declare function parseFixedOffset(value: string): number | undefined;
|
|
40
|
+
export declare function formatFixedOffset(offsetMinutes: number): string;
|
|
41
|
+
export declare function parseDateValue(value: unknown): ParsedDateResult;
|
|
42
|
+
export declare function compareDateValues(left: unknown, right: unknown, direction?: 'asc' | 'desc'): number;
|
|
43
|
+
export declare function matchesDateFilter(value: unknown, operator: string, expected: unknown): boolean | undefined;
|
|
44
|
+
export declare function validateDateValue(value: unknown, descriptor: DateTypeDescriptor): string | undefined;
|
|
45
|
+
export declare function dateValueWarnings(_value: unknown): string[];
|
|
46
|
+
export declare function serializeDateEditorValue(value: DateEditorValue): string;
|
|
47
|
+
export declare function convertDateTimeToOffset(value: string, timezone: string): string;
|
|
48
|
+
export declare function localOffsetForDate(date: string, time?: string): string;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
export const DATE_FILTER_OPERATORS = [
|
|
2
|
+
'eq',
|
|
3
|
+
'neq',
|
|
4
|
+
'gt',
|
|
5
|
+
'gte',
|
|
6
|
+
'lt',
|
|
7
|
+
'lte',
|
|
8
|
+
'isEmpty',
|
|
9
|
+
'isNotEmpty',
|
|
10
|
+
];
|
|
11
|
+
const DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
12
|
+
const DATETIME_PATTERN = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/;
|
|
13
|
+
function leapYear(year) {
|
|
14
|
+
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
15
|
+
}
|
|
16
|
+
function daysInMonth(year, month) {
|
|
17
|
+
if (month === 2)
|
|
18
|
+
return leapYear(year) ? 29 : 28;
|
|
19
|
+
return [4, 6, 9, 11].includes(month) ? 30 : 31;
|
|
20
|
+
}
|
|
21
|
+
function validCalendarDate(year, month, day) {
|
|
22
|
+
return month >= 1 && month <= 12 && day >= 1 && day <= daysInMonth(year, month);
|
|
23
|
+
}
|
|
24
|
+
export function parseFixedOffset(value) {
|
|
25
|
+
if (value === 'Z')
|
|
26
|
+
return 0;
|
|
27
|
+
const match = /^([+-])(\d{2}):(\d{2})$/.exec(value);
|
|
28
|
+
if (!match)
|
|
29
|
+
return undefined;
|
|
30
|
+
const hours = Number(match[2]);
|
|
31
|
+
const minutes = Number(match[3]);
|
|
32
|
+
if (hours > 14 || minutes > 59 || (hours === 14 && minutes !== 0))
|
|
33
|
+
return undefined;
|
|
34
|
+
const total = hours * 60 + minutes;
|
|
35
|
+
return match[1] === '-' ? -total : total;
|
|
36
|
+
}
|
|
37
|
+
export function formatFixedOffset(offsetMinutes) {
|
|
38
|
+
if (!Number.isInteger(offsetMinutes) || Math.abs(offsetMinutes) > 14 * 60) {
|
|
39
|
+
throw new TypeError('Timezone offset must be a whole number of minutes between -14:00 and +14:00');
|
|
40
|
+
}
|
|
41
|
+
if (offsetMinutes === 0)
|
|
42
|
+
return 'Z';
|
|
43
|
+
const sign = offsetMinutes < 0 ? '-' : '+';
|
|
44
|
+
const absolute = Math.abs(offsetMinutes);
|
|
45
|
+
return `${sign}${String(Math.floor(absolute / 60)).padStart(2, '0')}:${String(absolute % 60).padStart(2, '0')}`;
|
|
46
|
+
}
|
|
47
|
+
function epochSecondsFor(year, month, day, hour, minute, second, offsetMinutes) {
|
|
48
|
+
const instant = new Date(0);
|
|
49
|
+
instant.setUTCFullYear(year, month - 1, day);
|
|
50
|
+
instant.setUTCHours(hour, minute, second, 0);
|
|
51
|
+
return Math.floor(instant.getTime() / 1000) - offsetMinutes * 60;
|
|
52
|
+
}
|
|
53
|
+
export function parseDateValue(value) {
|
|
54
|
+
if (value === null || value === undefined)
|
|
55
|
+
return { kind: 'empty', source: value };
|
|
56
|
+
if (typeof value !== 'string' || value === '') {
|
|
57
|
+
return { kind: 'invalid', source: value, error: 'Enter a valid date or date and time' };
|
|
58
|
+
}
|
|
59
|
+
const date = DATE_PATTERN.exec(value);
|
|
60
|
+
if (date) {
|
|
61
|
+
const [, yearText, monthText, dayText] = date;
|
|
62
|
+
const year = Number(yearText);
|
|
63
|
+
const month = Number(monthText);
|
|
64
|
+
const day = Number(dayText);
|
|
65
|
+
if (!validCalendarDate(year, month, day)) {
|
|
66
|
+
return { kind: 'invalid', source: value, error: 'Enter a real calendar date' };
|
|
67
|
+
}
|
|
68
|
+
return { kind: 'date', source: value, calendarDate: value, year, month, day };
|
|
69
|
+
}
|
|
70
|
+
const datetime = DATETIME_PATTERN.exec(value);
|
|
71
|
+
if (!datetime)
|
|
72
|
+
return { kind: 'invalid', source: value, error: 'Use YYYY-MM-DD or an RFC 3339 date and time' };
|
|
73
|
+
const [, yearText, monthText, dayText, hourText, minuteText, secondText, fractionText, timezone] = datetime;
|
|
74
|
+
const year = Number(yearText);
|
|
75
|
+
const month = Number(monthText);
|
|
76
|
+
const day = Number(dayText);
|
|
77
|
+
const hour = Number(hourText);
|
|
78
|
+
const minute = Number(minuteText);
|
|
79
|
+
const second = Number(secondText);
|
|
80
|
+
if (!validCalendarDate(year, month, day) || hour > 23 || minute > 59 || second > 59) {
|
|
81
|
+
return { kind: 'invalid', source: value, error: 'Enter a real calendar date and valid time' };
|
|
82
|
+
}
|
|
83
|
+
const calendarDate = `${yearText}-${monthText}-${dayText}`;
|
|
84
|
+
const common = {
|
|
85
|
+
source: value,
|
|
86
|
+
calendarDate,
|
|
87
|
+
year,
|
|
88
|
+
month,
|
|
89
|
+
day,
|
|
90
|
+
hour,
|
|
91
|
+
minute,
|
|
92
|
+
second,
|
|
93
|
+
fractionalSeconds: fractionText?.slice(1) ?? '',
|
|
94
|
+
};
|
|
95
|
+
if (!timezone) {
|
|
96
|
+
return { ...common, kind: 'floating-datetime' };
|
|
97
|
+
}
|
|
98
|
+
const offsetMinutes = parseFixedOffset(timezone);
|
|
99
|
+
if (offsetMinutes === undefined) {
|
|
100
|
+
return { kind: 'invalid', source: value, error: 'Enter a valid timezone offset' };
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
...common,
|
|
104
|
+
kind: 'datetime',
|
|
105
|
+
timezone,
|
|
106
|
+
offsetMinutes,
|
|
107
|
+
epochSeconds: epochSecondsFor(year, month, day, hour, minute, second, offsetMinutes),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function compareFractions(left = '', right = '') {
|
|
111
|
+
const width = Math.max(left.length, right.length);
|
|
112
|
+
const normalizedLeft = left.padEnd(width, '0');
|
|
113
|
+
const normalizedRight = right.padEnd(width, '0');
|
|
114
|
+
return normalizedLeft < normalizedRight ? -1 : normalizedLeft > normalizedRight ? 1 : 0;
|
|
115
|
+
}
|
|
116
|
+
function validForOrdering(parsed) {
|
|
117
|
+
return parsed.kind === 'date' || parsed.kind === 'datetime' || parsed.kind === 'floating-datetime';
|
|
118
|
+
}
|
|
119
|
+
/** Calendar dates and floating times use their written fields on a UTC reference axis.
|
|
120
|
+
* Zoned times use their actual instant. Kind breaks midnight ties consistently.
|
|
121
|
+
*/
|
|
122
|
+
function ascendingDateComparison(left, right) {
|
|
123
|
+
const seconds = (value) => value.epochSeconds ?? epochSecondsFor(value.year ?? 0, value.month ?? 1, value.day ?? 1, value.hour ?? 0, value.minute ?? 0, value.second ?? 0, 0);
|
|
124
|
+
const rank = (value) => value.kind === 'date' ? 0 : value.kind === 'floating-datetime' ? 1 : 2;
|
|
125
|
+
return seconds(left) - seconds(right)
|
|
126
|
+
|| compareFractions(left.fractionalSeconds, right.fractionalSeconds)
|
|
127
|
+
|| rank(left) - rank(right);
|
|
128
|
+
}
|
|
129
|
+
export function compareDateValues(left, right, direction = 'asc') {
|
|
130
|
+
const parsedLeft = parseDateValue(left);
|
|
131
|
+
const parsedRight = parseDateValue(right);
|
|
132
|
+
const leftValid = validForOrdering(parsedLeft);
|
|
133
|
+
const rightValid = validForOrdering(parsedRight);
|
|
134
|
+
if (!leftValid || !rightValid) {
|
|
135
|
+
if (leftValid)
|
|
136
|
+
return -1;
|
|
137
|
+
if (rightValid)
|
|
138
|
+
return 1;
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
const result = ascendingDateComparison(parsedLeft, parsedRight);
|
|
142
|
+
return result === 0 ? 0 : direction === 'desc' ? -result : result;
|
|
143
|
+
}
|
|
144
|
+
export function matchesDateFilter(value, operator, expected) {
|
|
145
|
+
if (!DATE_FILTER_OPERATORS.includes(operator))
|
|
146
|
+
return undefined;
|
|
147
|
+
const parsed = parseDateValue(value);
|
|
148
|
+
const empty = parsed.kind === 'empty';
|
|
149
|
+
if (operator === 'isEmpty')
|
|
150
|
+
return empty;
|
|
151
|
+
if (operator === 'isNotEmpty')
|
|
152
|
+
return !empty;
|
|
153
|
+
if (!validForOrdering(parsed))
|
|
154
|
+
return false;
|
|
155
|
+
const filter = parseDateValue(expected);
|
|
156
|
+
if (!validForOrdering(filter))
|
|
157
|
+
return false;
|
|
158
|
+
// A calendar-day predicate compares written days for every relational operator.
|
|
159
|
+
const order = filter.kind === 'date'
|
|
160
|
+
? String(parsed.calendarDate).localeCompare(String(filter.calendarDate))
|
|
161
|
+
: compareDateValues(value, expected);
|
|
162
|
+
if (operator === 'eq')
|
|
163
|
+
return order === 0;
|
|
164
|
+
if (operator === 'neq')
|
|
165
|
+
return order !== 0;
|
|
166
|
+
if (operator === 'gt')
|
|
167
|
+
return order > 0;
|
|
168
|
+
if (operator === 'gte')
|
|
169
|
+
return order >= 0;
|
|
170
|
+
if (operator === 'lt')
|
|
171
|
+
return order < 0;
|
|
172
|
+
return order <= 0;
|
|
173
|
+
}
|
|
174
|
+
function validateBound(value, bound, edge) {
|
|
175
|
+
if (typeof bound !== 'string')
|
|
176
|
+
return undefined;
|
|
177
|
+
const parsedBound = parseDateValue(bound);
|
|
178
|
+
if (!validForOrdering(parsedBound))
|
|
179
|
+
return undefined;
|
|
180
|
+
const order = parsedBound.kind === 'date'
|
|
181
|
+
? String(value.calendarDate).localeCompare(parsedBound.calendarDate ?? '')
|
|
182
|
+
: compareDateValues(value.source, bound);
|
|
183
|
+
if (edge === 'minimum' && order < 0)
|
|
184
|
+
return `Date must be on or after ${bound}`;
|
|
185
|
+
if (edge === 'maximum' && order > 0)
|
|
186
|
+
return `Date must be on or before ${bound}`;
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
export function validateDateValue(value, descriptor) {
|
|
190
|
+
if (value === null || value === undefined)
|
|
191
|
+
return descriptor.required ? 'A value is required' : undefined;
|
|
192
|
+
const parsed = parseDateValue(value);
|
|
193
|
+
if (!validForOrdering(parsed))
|
|
194
|
+
return parsed.error ?? 'Enter a valid date or date and time';
|
|
195
|
+
return validateBound(parsed, descriptor.minimum, 'minimum')
|
|
196
|
+
?? validateBound(parsed, descriptor.maximum, 'maximum');
|
|
197
|
+
}
|
|
198
|
+
export function dateValueWarnings(_value) {
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
export function serializeDateEditorValue(value) {
|
|
202
|
+
const parsedDate = parseDateValue(value.date);
|
|
203
|
+
if (parsedDate.kind !== 'date')
|
|
204
|
+
throw new TypeError('Select a valid calendar date');
|
|
205
|
+
if (!value.includeTime)
|
|
206
|
+
return value.date;
|
|
207
|
+
const time = /^(\d{2}):(\d{2})$/.exec(value.time ?? '');
|
|
208
|
+
if (!time || Number(time[1]) > 23 || Number(time[2]) > 59)
|
|
209
|
+
throw new TypeError('Enter a valid time');
|
|
210
|
+
if (value.timezone && parseFixedOffset(value.timezone) === undefined)
|
|
211
|
+
throw new TypeError('Select a valid timezone');
|
|
212
|
+
const seconds = value.seconds ?? 0;
|
|
213
|
+
if (!Number.isInteger(seconds) || seconds < 0 || seconds > 59)
|
|
214
|
+
throw new TypeError('Enter valid seconds');
|
|
215
|
+
const fraction = value.fractionalSeconds
|
|
216
|
+
? `.${value.fractionalSeconds}`
|
|
217
|
+
: '';
|
|
218
|
+
if (fraction && !/^\.\d+$/.test(fraction))
|
|
219
|
+
throw new TypeError('Enter valid fractional seconds');
|
|
220
|
+
return `${value.date}T${value.time}:${String(seconds).padStart(2, '0')}${fraction}${value.timezone ?? ''}`;
|
|
221
|
+
}
|
|
222
|
+
export function convertDateTimeToOffset(value, timezone) {
|
|
223
|
+
const parsed = parseDateValue(value);
|
|
224
|
+
if (parsed.kind !== 'datetime' || parsed.epochSeconds === undefined)
|
|
225
|
+
throw new TypeError('A timezone-aware date and time is required');
|
|
226
|
+
const offsetMinutes = parseFixedOffset(timezone);
|
|
227
|
+
if (offsetMinutes === undefined)
|
|
228
|
+
throw new TypeError('Select a valid timezone');
|
|
229
|
+
const written = new Date((parsed.epochSeconds + offsetMinutes * 60) * 1000);
|
|
230
|
+
const date = `${String(written.getUTCFullYear()).padStart(4, '0')}-${String(written.getUTCMonth() + 1).padStart(2, '0')}-${String(written.getUTCDate()).padStart(2, '0')}`;
|
|
231
|
+
const time = `${String(written.getUTCHours()).padStart(2, '0')}:${String(written.getUTCMinutes()).padStart(2, '0')}`;
|
|
232
|
+
return serializeDateEditorValue({
|
|
233
|
+
date,
|
|
234
|
+
includeTime: true,
|
|
235
|
+
time,
|
|
236
|
+
seconds: written.getUTCSeconds(),
|
|
237
|
+
fractionalSeconds: parsed.fractionalSeconds,
|
|
238
|
+
timezone,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
export function localOffsetForDate(date, time = '12:00') {
|
|
242
|
+
const local = new Date(`${date}T${time}:00`);
|
|
243
|
+
return formatFixedOffset(-local.getTimezoneOffset());
|
|
244
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CompiledJsonViewSchema, JsonViewMetadataDiagnostic } from './metadata.js';
|
|
2
|
+
import { type JsonViewTypeRegistry } from './type-registry.js';
|
|
3
|
+
import { type JsonViewDiagnosticHelp } from './annotation-capabilities.js';
|
|
4
|
+
export type { JsonViewDiagnosticHelp } from './annotation-capabilities.js';
|
|
5
|
+
/** Do not echo entire records or long strings into every diagnostic. */
|
|
6
|
+
export declare function diagnosticValuePreview(value: unknown, depth?: number): string;
|
|
7
|
+
export declare function explainMetadataDiagnostics(diagnostics: readonly JsonViewMetadataDiagnostic[], root: unknown, metadata: unknown, registry: JsonViewTypeRegistry, metadataSource: 'embedded' | 'external' | 'inferred', schema?: readonly CompiledJsonViewSchema[]): JsonViewMetadataDiagnostic[];
|
|
8
|
+
export declare function sourceDiagnosticHelp(code: string): JsonViewDiagnosticHelp;
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { MAX_JSON_DEPTH } from './json-limits.js';
|
|
2
|
+
import { describeValuePath, getValueAtPath, VALUE_PATH_MISSING } from './json-path.js';
|
|
3
|
+
import { JSON_VIEW_OPTION_COLORS } from './type-registry.js';
|
|
4
|
+
import { ANNOTATION_PROPERTIES, COMMON_FILTER_OPERATORS, VIEW_PROPERTIES } from './annotation-capabilities.js';
|
|
5
|
+
/** Do not echo entire records or long strings into every diagnostic. */
|
|
6
|
+
export function diagnosticValuePreview(value, depth = 0) {
|
|
7
|
+
if (value === VALUE_PATH_MISSING || value === undefined)
|
|
8
|
+
return 'missing';
|
|
9
|
+
if (value === null)
|
|
10
|
+
return 'null';
|
|
11
|
+
if (typeof value === 'string')
|
|
12
|
+
return JSON.stringify(value.length > 160 ? `${value.slice(0, 157)}…` : value);
|
|
13
|
+
if (Array.isArray(value))
|
|
14
|
+
return depth > 0 ? `array (${value.length} items)`
|
|
15
|
+
: `[${value.slice(0, 3).map((item) => diagnosticValuePreview(item, depth + 1)).join(', ')}${value.length > 3 ? ', …' : ''}]`;
|
|
16
|
+
if (typeof value === 'object') {
|
|
17
|
+
const keys = Object.keys(value);
|
|
18
|
+
return `object {${keys.slice(0, 5).map((key) => diagnosticValuePreview(key)).join(', ')}${keys.length > 5 ? ', …' : ''}}`;
|
|
19
|
+
}
|
|
20
|
+
return String(value);
|
|
21
|
+
}
|
|
22
|
+
function record(value) {
|
|
23
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
24
|
+
}
|
|
25
|
+
function propertyHelp(property, type) {
|
|
26
|
+
switch (property) {
|
|
27
|
+
case 'title':
|
|
28
|
+
case 'description':
|
|
29
|
+
return { fix: `Set ${property} to a JSON string or remove it.`, expected: 'string', examples: ['Customer name'], capabilities: [property === 'title' ? 'Descriptor title labels a field. The first configured view column identifies records.' : 'Description documents the field; it does not validate or transform its value.'] };
|
|
30
|
+
case 'required':
|
|
31
|
+
case 'nullable':
|
|
32
|
+
return { fix: 'Use required: true to reject empty values; omit required or set it to false for optional fields. Remove nullable.', expected: 'boolean required; no nullable property', examples: [{ type, required: true }], capabilities: ['Optional fields allow missing values and null. Empty strings are optional except for date, where an empty string is invalid.', 'required does not enforce a nonempty collection; multi-select accepts [] even when required is true.'] };
|
|
33
|
+
case 'minimum':
|
|
34
|
+
case 'maximum':
|
|
35
|
+
return type === 'date'
|
|
36
|
+
? { fix: `Set ${property} to a valid date string; remove it or use null for no bound.`, expected: 'YYYY-MM-DD or a timestamp with seconds', examples: ['2026-01-01', '2026-01-01T09:30:00Z'], capabilities: ['Date-only bounds compare the written calendar day; timestamp bounds compare the reference timeline. minimum must not be after maximum when they have comparable kinds.'] }
|
|
37
|
+
: { fix: `Set ${property} to an unquoted finite number or remove it.`, expected: 'finite JSON number', examples: [0, 100], capabilities: ['Numeric bounds are inclusive. Built-in bounds apply to number and date, not text length or collection size.'] };
|
|
38
|
+
case 'defaultIncludeTime':
|
|
39
|
+
return { fix: 'Set defaultIncludeTime to true or false, or remove it.', expected: 'boolean', examples: [true], capabilities: ['For date editors only: chooses whether a new empty date starts with time enabled. It does not require times, add a default value, or convert existing dates.'] };
|
|
40
|
+
case 'pattern':
|
|
41
|
+
return { fix: 'Use an RE2 pattern string of at most 4,096 UTF-16 code units. Replace lookarounds and backreferences with supported syntax.', expected: 'RE2 pattern string', examples: ['^[A-Z][0-9]+$'], capabilities: ['Searches strings for a match; add ^ and $ to constrain the whole string.', 'This is a JSON string, so backslashes must be JSON-escaped. Patterns do not validate array elements or non-string values.'] };
|
|
42
|
+
case 'options':
|
|
43
|
+
return { fix: 'Set options to an array of strings on a select or multi-select descriptor.', expected: 'string[]', examples: [['Todo', 'Done']], capabilities: ['Choices are open: options are suggestions, and observed values are appended. Options do not enforce a closed enum.', 'select stores one string; multi-select stores an array of strings. Use a custom validator for a closed enum.'] };
|
|
44
|
+
case 'optionColors':
|
|
45
|
+
return { fix: 'Use an object mapping option strings to built-in palette names on select or multi-select.', expected: 'Record<option, palette color>', allowedValues: JSON_VIEW_OPTION_COLORS, examples: [{ Todo: 'blue', Done: 'green' }], capabilities: ['Keys are option values, not array indices. Hex colors, CSS color names outside the palette, and label/value option objects are unsupported.'] };
|
|
46
|
+
case 'placeholder':
|
|
47
|
+
return { fix: 'Set placeholder to a string or remove it.', expected: 'string', examples: ['Enter a name'], capabilities: ['Built-in text, Markdown, and date editors use placeholder. It is an input hint, not a default data value.'] };
|
|
48
|
+
case 'multiline':
|
|
49
|
+
return { fix: 'Set multiline to true or false on a text descriptor.', expected: 'boolean', examples: [{ type: 'text', multiline: true }], capabilities: ['Changes the text editor to multiple lines. Long text can use a multiline editor and full-width record presentation.'] };
|
|
50
|
+
case 'step':
|
|
51
|
+
return { fix: 'Set step to a positive finite number on a number descriptor.', expected: 'number greater than 0', examples: [0.5, 1], capabilities: ['An editor increment only; step does not enforce integer values or divisibility. Use a custom validator for those constraints.'] };
|
|
52
|
+
default: return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const VALUE_SHAPES = {
|
|
56
|
+
text: { expected: 'JSON string', examples: ['Example'] },
|
|
57
|
+
markdown: { expected: 'JSON string containing Markdown', examples: ['## Notes\nRecord details'], capabilities: ['Renders Markdown and supported HTML; editing preserves source text. Multiple properties may use Markdown.'] },
|
|
58
|
+
body: { expected: 'JSON string containing the record body', examples: ['## Notes\nRecord details'], capabilities: ['Legacy body annotations are normalized to Markdown.'] },
|
|
59
|
+
number: { expected: 'finite JSON number, not a quoted number', examples: [0, 1, 0.5] },
|
|
60
|
+
checkbox: { expected: 'JSON boolean true or false, not a string or 0/1', examples: [true, false] },
|
|
61
|
+
select: { expected: 'one JSON string', examples: ['Todo'], capabilities: ['options are suggestions, not a closed enum; new strings are valid.'] },
|
|
62
|
+
'multi-select': { expected: 'JSON array of strings', examples: [[], ['Todo']], capabilities: ['options are suggestions, not a closed enum. [] is valid even with required: true.'] },
|
|
63
|
+
date: { expected: 'real calendar date YYYY-MM-DD, or YYYY-MM-DDTHH:mm:ss with optional fractional seconds and optional Z/±HH:mm', examples: ['2026-01-01', '2026-01-01T09:30:00Z'], capabilities: ['Seconds are required in timestamps. Empty strings, Unix numbers, impossible dates, leap seconds, and 24:00:00 are invalid.', 'An absent offset is a floating time, not the machine timezone. Add Z or an explicit offset only if the intended timezone is known.'] },
|
|
64
|
+
url: { expected: 'absolute http:// or https:// URL string', examples: ['https://example.com'] },
|
|
65
|
+
email: { expected: 'email string containing a local part, @, and a domain with a dot', examples: ['name@example.com'] },
|
|
66
|
+
};
|
|
67
|
+
function valueHelp(item, descriptor, registry, entry) {
|
|
68
|
+
const type = descriptor.type;
|
|
69
|
+
if (/^(Validation failed:|Validator must return|Warning provider failed:|Warning provider must return)/.test(item.message)) {
|
|
70
|
+
const hook = item.code === 'typed-value-warning' ? 'warnings' : 'validate';
|
|
71
|
+
return {
|
|
72
|
+
fix: `Correct the ${hook} hook in the host registration for ${JSON.stringify(type)}: ${item.message} Changing the data may not fix a failing hook.`,
|
|
73
|
+
expected: hook === 'warnings' ? 'warnings returns an array of strings without throwing' : 'validate returns a diagnostic string or undefined without throwing',
|
|
74
|
+
capabilities: ['Custom hooks are host application code; annotations cannot repair their implementation.'],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const shape = (registry.get(type)?.descriptorProperties && Object.hasOwn(VALUE_SHAPES, type) ? VALUE_SHAPES[type] : undefined) ?? { expected: `a value accepted by registered type ${JSON.stringify(type)}`, examples: [] };
|
|
78
|
+
const capabilities = [...(shape.capabilities ?? [])];
|
|
79
|
+
if (descriptor.required)
|
|
80
|
+
capabilities.push('required: true rejects missing and null values, and empty strings. Populate the data; change required only if the field is intended to be optional.');
|
|
81
|
+
else
|
|
82
|
+
capabilities.push(`The field is optional: missing and null are allowed${type === 'date' ? '; use null instead of an empty string' : ', as is an empty string'}.`);
|
|
83
|
+
for (const key of ['minimum', 'maximum', 'pattern']) {
|
|
84
|
+
if (descriptor[key] !== undefined)
|
|
85
|
+
capabilities.push(`Active ${key}: ${diagnosticValuePreview(descriptor[key])}.`);
|
|
86
|
+
}
|
|
87
|
+
if (descriptor.pattern !== undefined)
|
|
88
|
+
capabilities.push('pattern searches strings; ^ and $ anchor the whole string. It does not test individual array elements.');
|
|
89
|
+
if (item.code === 'required-value-missing' && entry && (item.sourcePath?.length ?? 0) < entry.path.segments.length) {
|
|
90
|
+
return {
|
|
91
|
+
fix: `Restore the missing object or collection ancestors at ${describeValuePath(item.sourcePath ?? [])}, then populate the required fields matching ${JSON.stringify(entry.declaration)}. Do not replace the container with a leaf value.`,
|
|
92
|
+
expected: `object/collection ancestors leading to ${shape.expected} at ${entry.declaration}`,
|
|
93
|
+
capabilities: [...capabilities, 'This sourcePath is the unresolved ancestor, not the leaf. An existing empty collection has no required child instances; required does not enforce collection size.'],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const candidates = [descriptor.minimum, descriptor.maximum, ...(descriptor.options?.slice(0, 2) ?? []), ...shape.examples]
|
|
97
|
+
.filter((value) => value !== undefined && value !== null);
|
|
98
|
+
const examples = candidates.filter((value) => !registry.validate(value, descriptor)).slice(0, 3);
|
|
99
|
+
return {
|
|
100
|
+
fix: item.code === 'typed-value-warning'
|
|
101
|
+
? `Review ${describeValuePath(item.sourcePath ?? [])}: ${item.message} Preserve the intended value when resolving this warning.`
|
|
102
|
+
: `Set ${describeValuePath(item.sourcePath ?? [])} to ${shape.expected}. If the annotation describes the wrong data, correct ${describeValuePath(item.metadataPath)} instead.`,
|
|
103
|
+
expected: `${shape.expected}, satisfying the active descriptor constraints`,
|
|
104
|
+
...(examples.length ? { examples } : {}), capabilities,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function helpFor(item, metadata, registry, descriptors) {
|
|
108
|
+
const raw = (path) => getValueAtPath(metadata, path.slice(1));
|
|
109
|
+
const declaration = record(raw(item.metadataPath.slice(0, 3)));
|
|
110
|
+
const descriptor = declaration;
|
|
111
|
+
const property = String(item.metadataPath.at(-1));
|
|
112
|
+
const view = record(raw(item.metadataPath.slice(0, 3)));
|
|
113
|
+
const viewPath = typeof view.path === 'string' ? view.path : '$.rows';
|
|
114
|
+
const fieldPath = `${viewPath}[*].field`;
|
|
115
|
+
if (item.scope === 'value')
|
|
116
|
+
return valueHelp(item, descriptors.get(item.declaration ?? '')?.descriptor ?? descriptor, registry, descriptors.get(item.declaration ?? ''));
|
|
117
|
+
if (item.code === 'unknown-schema-type' || item.code === 'invalid-schema-type')
|
|
118
|
+
return {
|
|
119
|
+
fix: 'Set type to a name registered in this host. Register a custom type and its editor in the host before using a new name.',
|
|
120
|
+
expected: 'nonempty registered type name', allowedValues: registry.names(), examples: [{ type: 'text' }],
|
|
121
|
+
capabilities: ['JSON Views types describe editors and validation. JSON Schema names such as string, boolean, integer, array, and object are not built-in type names. Use text, checkbox, number, or multi-select as appropriate.'],
|
|
122
|
+
};
|
|
123
|
+
if (item.code === 'unknown-schema-property' || item.code === 'inapplicable-schema-property') {
|
|
124
|
+
const supported = registry.get(descriptor.type)?.descriptorProperties ?? [];
|
|
125
|
+
const detail = propertyHelp(property, descriptor.type);
|
|
126
|
+
return { fix: `Remove or correct ${JSON.stringify(property)}; it has no built-in effect for ${JSON.stringify(descriptor.type)}. Keep it only if a host extension consumes it.`, expected: `supported properties for ${descriptor.type}`, allowedValues: supported, capabilities: detail ? [detail.fix, ...(detail.capabilities ?? [])] : ['Unknown descriptor fields are preserved for host extensions. JSON Schema keywords such as enum, default, format, minLength, items, and properties do not implement validation here.'] };
|
|
127
|
+
}
|
|
128
|
+
if (item.code === 'unknown-annotation-property' || item.code === 'unknown-view-property') {
|
|
129
|
+
const parent = item.metadataPath.at(-2);
|
|
130
|
+
const ancestor = item.metadataPath.at(-3);
|
|
131
|
+
const allowed = item.scope === 'metadata' ? ANNOTATION_PROPERTIES
|
|
132
|
+
: parent === 'filter' ? ['match', 'rules']
|
|
133
|
+
: ancestor === 'rules' ? ['path', 'operator', 'value']
|
|
134
|
+
: ancestor === 'columns' ? ['label', 'path']
|
|
135
|
+
: ancestor === 'sort' ? ['path', 'direction'] : VIEW_PROPERTIES;
|
|
136
|
+
return { fix: `Remove or rename ${JSON.stringify(property)} to a supported property. Keep it only if a host extension consumes it.`, expected: 'a supported property at this location', allowedValues: allowed, capabilities: ['Unknown fields are retained in source but ignored by the built-in compiler.'] };
|
|
137
|
+
}
|
|
138
|
+
if (item.scope === 'schema' && item.metadataPath.length >= 4) {
|
|
139
|
+
const detail = propertyHelp(item.code === 'invalid-option-color' ? 'optionColors' : property, descriptor.type);
|
|
140
|
+
if (detail)
|
|
141
|
+
return detail;
|
|
142
|
+
}
|
|
143
|
+
if (item.code.endsWith('-path'))
|
|
144
|
+
return {
|
|
145
|
+
fix: item.code === 'invalid-schema-path' ? 'Rename this schema map key to a supported absolute path.'
|
|
146
|
+
: item.code === 'invalid-view-path' || item.code === 'unresolved-view-path' ? 'Set view.path to an existing concrete location in the document; select the collection itself for a record view.'
|
|
147
|
+
: `Set this field path to ${fieldPath}, replacing field with an existing record property.`,
|
|
148
|
+
expected: item.code === 'invalid-schema-path' ? 'absolute JSONPath; [*] may match array items or dictionary values'
|
|
149
|
+
: item.code === 'invalid-view-path' || item.code === 'unresolved-view-path' ? 'absolute JSONPath with no wildcard'
|
|
150
|
+
: `the view path ${viewPath}, followed by exactly one [*], then a nonempty concrete field path`,
|
|
151
|
+
examples: item.code === 'invalid-schema-path' ? ['$.rows[*].name', "$['key with spaces']"]
|
|
152
|
+
: item.code === 'invalid-view-path' || item.code === 'unresolved-view-path' ? ['$', '$.rows'] : [fieldPath, `${viewPath}[*].profile.email`],
|
|
153
|
+
capabilities: ['All paths start at $. Supported segments: .key, [\'quoted key\'], [0], [*]. No @ relative paths, recursive descent, slices, unions, or filter expressions.', 'Record field paths must resolve on at least one current row; empty collections are allowed. Nested wildcards after the record selector are unsupported.'],
|
|
154
|
+
};
|
|
155
|
+
switch (item.code) {
|
|
156
|
+
case 'invalid-metadata':
|
|
157
|
+
case 'unsupported-metadata-version':
|
|
158
|
+
return { fix: item.code === 'unsupported-metadata-version' ? 'Use a host that supports this version, or deliberately translate the annotations to version 1. Do not merely relabel future metadata.' : 'Set annotations to an object containing numeric version: 1.', expected: '{ version: 1, schema?: object, views?: array }', examples: [{ version: 1 }], capabilities: ['schema maps absolute data paths to descriptors; it is not JSON Schema. Existing invalid annotations suppress inference.'] };
|
|
159
|
+
case 'invalid-schema-map':
|
|
160
|
+
case 'invalid-schema-descriptor':
|
|
161
|
+
return { fix: item.code === 'invalid-schema-map' ? 'Replace schema with an object whose keys are absolute data paths and values are descriptor objects.' : 'Replace this descriptor with an object containing type.', expected: item.code === 'invalid-schema-map' ? 'Record<absolute path, descriptor>' : '{ type: registered type, ...properties }', examples: item.code === 'invalid-schema-map' ? [{ '$.rows[*].name': { type: 'text' } }] : [{ type: 'text' }] };
|
|
162
|
+
case 'invalid-number-bounds':
|
|
163
|
+
case 'invalid-date-bounds':
|
|
164
|
+
return { fix: 'Correct minimum or maximum so minimum does not exceed maximum, or remove the unintended bound.', expected: 'ordered inclusive bounds', capabilities: ['Both conflicting bounds are omitted from the compiled descriptor until repaired.'] };
|
|
165
|
+
case 'invalid-type-descriptor':
|
|
166
|
+
if (/^Descriptor validator (failed:|must return)/.test(item.message))
|
|
167
|
+
return { fix: `Correct the validateDescriptor hook in the host registration for ${JSON.stringify(descriptor.type)}: ${item.message}`, expected: 'validateDescriptor returns a diagnostic string or undefined without throwing', capabilities: ['The descriptor is excluded while the host hook fails.'] };
|
|
168
|
+
return { fix: `Correct this ${JSON.stringify(descriptor.type)} descriptor according to its registered validator: ${item.message}`, expected: 'a descriptor accepted by the host type validator', capabilities: ['This descriptor is excluded until its custom validation succeeds. Extension-specific requirements come from the host registration.'] };
|
|
169
|
+
case 'schema-specificity-conflict':
|
|
170
|
+
return { fix: 'Remove the duplicate/conflicting declaration, make the descriptors identical, or make the intended override more specific with a concrete key/index.', expected: 'one unambiguous winning descriptor per data location', capabilities: ['More concrete segments win, then longer paths, then the earlier declaration. Equally specific conflicting declarations do not merge.'] };
|
|
171
|
+
case 'invalid-views':
|
|
172
|
+
case 'invalid-view':
|
|
173
|
+
return { fix: item.code === 'invalid-views' ? 'Set views to an array of view objects.' : 'Provide a view object with a nonempty name and a concrete absolute path.', expected: 'view object with name and path; optional id, display, columns, filter, sort, groupBy, groupOrder, orderPath', examples: item.code === 'invalid-views' ? [[{ name: 'Rows', path: '$.rows' }]] : [{ name: 'Rows', path: '$.rows' }] };
|
|
174
|
+
case 'invalid-view-id':
|
|
175
|
+
case 'duplicate-view-id':
|
|
176
|
+
return { fix: 'Use a unique nonempty string id, or omit id to let the compiler assign one.', expected: 'unique string id', capabilities: ['A missing id is generated. Duplicate ids receive a numeric suffix; references should use the resulting compiled id.'] };
|
|
177
|
+
case 'invalid-columns':
|
|
178
|
+
case 'invalid-column':
|
|
179
|
+
if (property === 'label')
|
|
180
|
+
return { fix: 'Set label to a nonempty display string; keep the field selector in path.', expected: 'nonempty string label', examples: ['Name'], capabilities: ['Column labels do not rename source properties and need not be unique.'] };
|
|
181
|
+
return { fix: 'Set columns to an array of objects, each with a nonempty label and an absolute record field path.', expected: '{ label: string, path: absolute field path }[]', examples: [[{ label: 'Name', path: `${viewPath}[*].name` }]], capabilities: ['Omit columns or use [] to infer columns. Labels may repeat; paths select data without renaming source properties.'] };
|
|
182
|
+
case 'invalid-filter':
|
|
183
|
+
if (property === 'match')
|
|
184
|
+
return { fix: 'Set match to all or any, or remove match to use all.', expected: '"all" or "any"', allowedValues: ['all', 'any'], capabilities: ['all requires every rule; any requires at least one. Empty rules leave all rows visible.'] };
|
|
185
|
+
if (property === 'rules')
|
|
186
|
+
return { fix: 'Set rules to an array of rule objects, or remove filter.', expected: '{ path, operator, value? }[]', examples: [[{ path: fieldPath, operator: 'isNotEmpty' }]], capabilities: ['An empty rules array leaves all rows visible.'] };
|
|
187
|
+
return { fix: 'Provide filter.rules as an array and set match to all or any, or omit match.', expected: '{ match?: "all" | "any", rules: rule[] }', examples: [{ match: 'all', rules: [{ path: fieldPath, operator: 'eq', value: 'Todo' }] }], capabilities: ['all requires every rule; any requires at least one. Empty rules leave all rows visible.'] };
|
|
188
|
+
case 'invalid-filter-rule':
|
|
189
|
+
case 'invalid-filter-operator':
|
|
190
|
+
return { fix: 'Set operator to one supported by every applicable field type. Custom operators require a registered type with a filterOperators allowlist and matchesFilter implementation.', expected: 'supported operator string', allowedValues: COMMON_FILTER_OPERATORS, capabilities: ['date only supports eq, neq, gt, gte, lt, lte, isEmpty, isNotEmpty.', 'All non-presence operators require value; in and notIn require an array.'] };
|
|
191
|
+
case 'invalid-filter-value': {
|
|
192
|
+
const rule = record(raw(item.metadataPath.slice(0, -1)));
|
|
193
|
+
const array = rule.operator === 'in' || rule.operator === 'notIn';
|
|
194
|
+
return { fix: `Set value to ${array ? 'an array of comparison values' : 'a comparison value matching the field type'}.`, expected: array ? 'JSON array' : 'JSON value appropriate for the operator and field type', examples: array ? [['Todo', 'Done']] : ['Todo'], capabilities: ['isEmpty and isNotEmpty do not use value. For date comparisons use a valid date or timestamp string; date-only operands compare the written calendar day.'] };
|
|
195
|
+
}
|
|
196
|
+
case 'unused-filter-value':
|
|
197
|
+
return { fix: 'Remove value from this presence rule.', expected: 'isEmpty/isNotEmpty with path and operator only', capabilities: ['Presence operators ignore comparison values. Common presence considers missing, null, empty strings, and empty arrays empty; date presence considers only missing and null empty.'] };
|
|
198
|
+
case 'invalid-sort':
|
|
199
|
+
case 'invalid-sort-entry':
|
|
200
|
+
if (property === 'direction')
|
|
201
|
+
return { fix: 'Set direction to asc or desc.', expected: '"asc" or "desc"', allowedValues: ['asc', 'desc'], capabilities: ['asc sorts ascending; desc sorts descending. Missing/null values remain last.'] };
|
|
202
|
+
return { fix: 'Set sort to an array with an absolute field path and direction asc or desc on every entry.', expected: '{ path: absolute field path, direction: "asc" | "desc" }[]', examples: [[{ path: fieldPath, direction: 'asc' }]], capabilities: ['Earlier entries have higher priority. Missing/null sort last in either direction, and source order breaks ties.'] };
|
|
203
|
+
case 'invalid-html':
|
|
204
|
+
return { fix: 'Set html to a non-empty HTML string with explicit closing tags for jv-field/jv-value.', expected: 'non-empty html string', examples: ['<h1><jv-field bind="$.name"></jv-field></h1>'] };
|
|
205
|
+
case 'invalid-html-css':
|
|
206
|
+
return { fix: 'Set css to a string or omit it.', expected: 'optional CSS string' };
|
|
207
|
+
case 'html-size-limit':
|
|
208
|
+
return { fix: 'Reduce this template string to at most 256 KiB UTF-8.', expected: 'HTML and CSS at most 256 KiB each' };
|
|
209
|
+
case 'incompatible-html':
|
|
210
|
+
return { fix: 'Use display: "html" for html/css and remove columns, filter, sort, groupBy, groupOrder and orderPath from HTML views.', expected: '{ name, path, display: "html", html, css? }' };
|
|
211
|
+
case 'removed-view-title':
|
|
212
|
+
return { fix: 'Remove title from the view and put the identifying field first in columns.', expected: 'columns: [{ label, path }, ...]', capabilities: ['The first configured column supplies table record links and Kanban headings. Schema title remains a field label.'] };
|
|
213
|
+
case 'invalid-display':
|
|
214
|
+
return { fix: 'Remove display for automatic rendering, or choose kanban for a grouped collection or html with embedded HTML/CSS.', expected: 'omitted display, "kanban", or "html"', allowedValues: ['kanban', 'html'], capabilities: ['table, list, grid, and adaptive are not valid explicit display values. Tables and other default layouts are chosen from the data shape when display is omitted.'] };
|
|
215
|
+
case 'invalid-group-order':
|
|
216
|
+
return { fix: 'Set groupOrder to an array of unique scalar lane values, or remove it to use schema and data order.', expected: 'array of strings, finite numbers, booleans, or null', examples: [['Todo', 'In progress', 'Done']], capabilities: ['Only Kanban views use groupOrder. Values not currently present remain available as empty lanes.'] };
|
|
217
|
+
case 'incompatible-view':
|
|
218
|
+
return { fix: 'Point path at an array of objects or a dictionary of object records, or remove collection-only columns, filter, sort, groupBy, groupOrder, and orderPath.', expected: 'record collection for collection field configuration', capabilities: ['Mixed arrays and arrays of primitives are not record collections. Raw data remains available when a view is excluded.'] };
|
|
219
|
+
case 'incompatible-kanban':
|
|
220
|
+
return { fix: `For a board, set display: "kanban", select a record collection, and set groupBy to ${viewPath}[*].status. Otherwise remove groupBy, groupOrder, and orderPath.`, expected: 'kanban display + record collection + groupBy field path', examples: [{ name: 'Board', path: '$.rows', display: 'kanban', groupBy: '$.rows[*].status' }], capabilities: ['groupBy selects lanes. groupOrder controls lane order. orderPath optionally controls card ordering and must be a different finite numeric field on every record.'] };
|
|
221
|
+
case 'conflicting-kanban-paths':
|
|
222
|
+
case 'invalid-order-value':
|
|
223
|
+
return { fix: item.code === 'conflicting-kanban-paths' ? 'Choose a separate numeric field for orderPath, or remove orderPath.' : `Populate ${describeValuePath(item.sourcePath ?? [])} and the corresponding order field on every record with finite numbers, or remove orderPath.`, expected: 'a finite numeric ordering field, distinct from groupBy, on every record', examples: [{ position: 1 }, { position: 2 }], capabilities: ['Numeric strings and missing ranks are invalid. Equal ranks retain stable source order.'] };
|
|
224
|
+
default:
|
|
225
|
+
return { fix: `Correct the value at ${describeValuePath(item.metadataPath)}: ${item.message}`, expected: item.message };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
export function explainMetadataDiagnostics(diagnostics, root, metadata, registry, metadataSource, schema = []) {
|
|
229
|
+
const descriptors = new Map(schema.map((entry) => [entry.declaration, entry]));
|
|
230
|
+
return diagnostics.map((item) => {
|
|
231
|
+
const target = (item.scope === 'value' || item.code === 'invalid-order-value') && item.sourcePath ? getValueAtPath(root, item.sourcePath)
|
|
232
|
+
: getValueAtPath(metadata, item.metadataPath.slice(1));
|
|
233
|
+
return { ...item, metadataSource, help: { ...helpFor(item, metadata, registry, descriptors), received: diagnosticValuePreview(target), ...item.help } };
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
export function sourceDiagnosticHelp(code) {
|
|
237
|
+
switch (code) {
|
|
238
|
+
case 'json-depth-limit': return { fix: 'Reduce nesting to use structured views. The original source remains available for editing and download.', expected: `at most ${MAX_JSON_DEPTH} nested object/array containers`, capabilities: ['This is a structured-view resource limit, not a JSON syntax error.'] };
|
|
239
|
+
case 'duplicate-key': return { fix: 'Edit the exact source to rename or remove duplicate properties after deciding which data to retain. Do not repair by parsing and reserializing the document.', expected: 'unique property names within each object', capabilities: ['Structured inspection shows only the last occurrence. Source preserves all occurrences; editing an ambiguous path or ancestor is restricted. start/end identify a duplicate source span.'] };
|
|
240
|
+
case 'unsafe-number': return { fix: 'Read and edit the exact numeric token in Source. If this is an identifier or exact decimal, consider a string and update its descriptor to text.', expected: 'an intentionally preserved exact token, or a value safe for JavaScript numeric editing', capabilities: ['Do not round-trip this token through JSON.parse/JSON.stringify or a parsed-value update. Use exact source text.', 'Unsafe integers, overflow, underflow, negative zero, and decimal precision changes restrict structured edits.'] };
|
|
241
|
+
case 'missing-header': return { fix: 'Add a nonempty name for every CSV header column.', expected: 'one header row with nonempty, unique column names', examples: ['name,status\nAda,Todo\n'] };
|
|
242
|
+
case 'duplicate-header': return { fix: 'Rename the repeated CSV header so every column has a unique name.', expected: 'unique CSV column names' };
|
|
243
|
+
case 'irregular-row': return { fix: 'Add missing cells or remove extra cells so this record has the same cell count as the header.', expected: 'one cell per header column', capabilities: ['Quote cells containing delimiters or line breaks; escape a quote inside a quoted cell as two quotes.'] };
|
|
244
|
+
case 'bare-cr': return { fix: 'Replace bare carriage-return line endings with LF or CRLF.', expected: 'LF or CRLF line endings' };
|
|
245
|
+
case 'unterminated-quote':
|
|
246
|
+
case 'invalid-quoted-cell':
|
|
247
|
+
case 'invalid-quote':
|
|
248
|
+
case 'invalid-csv':
|
|
249
|
+
return { fix: 'Repair CSV quoting at the reported source location: close quoted fields and double embedded quotes.', expected: 'CSV with balanced double quotes and consistent columns', examples: ['name,note\nAda,"Said ""hello"""\n'] };
|
|
250
|
+
default: return { fix: 'Repair the JSON syntax in the exact source, then validate again. Use double-quoted keys/strings; remove comments and trailing commas.', expected: 'one valid JSON value', examples: ['{"rows":[]}'], capabilities: ['Syntax errors prevent annotation and value validation. Once parsing succeeds, diagnostics reveal the next relevant issues.'] };
|
|
251
|
+
}
|
|
252
|
+
}
|