@svadmin/lite 0.12.0 → 0.14.0
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/README.md +1 -1
- package/dist/components/LiteErrorSummary.svelte +34 -0
- package/dist/components/LiteErrorSummary.svelte.d.ts +14 -0
- package/dist/components/LiteFileUpload.svelte +53 -0
- package/dist/components/LiteFileUpload.svelte.d.ts +16 -0
- package/dist/components/LiteFilterBuilder.svelte +168 -155
- package/dist/components/LiteFilterBuilder.svelte.d.ts +0 -1
- package/dist/components/LiteGanttChart.svelte +43 -10
- package/dist/components/LiteGanttChart.svelte.d.ts +6 -0
- package/dist/components/LiteJsonSchemaForm.svelte +182 -72
- package/dist/components/LiteJsonSchemaForm.svelte.d.ts +3 -2
- package/dist/components/LiteKanbanBoard.svelte +56 -8
- package/dist/components/LiteKanbanBoard.svelte.d.ts +4 -0
- package/dist/components/LitePivotTable.svelte +88 -58
- package/dist/components/LitePivotTable.svelte.d.ts +13 -1
- package/dist/components/LiteSpreadsheetView.svelte +55 -9
- package/dist/components/LiteSpreadsheetView.svelte.d.ts +8 -0
- package/dist/filter-form.d.ts +3 -0
- package/dist/filter-form.js +34 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/dist/schema-form-data.d.ts +18 -0
- package/dist/schema-form-data.js +264 -0
- package/dist/server-adapter.js +170 -2
- package/package.json +2 -2
package/dist/server-adapter.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { definedOptions } from '@svadmin/core/options';
|
|
2
|
-
import {
|
|
2
|
+
import { FILTER_COLLECTION_LIMIT, isFilterCollectionValue, isCollectionFilterOperator, filterOperatorsForField } from '@svadmin/core/filter-values';
|
|
3
|
+
import { error, redirect, isRedirect } from '@sveltejs/kit';
|
|
3
4
|
import { resourceToTypeBoxSchema } from './schema-generator';
|
|
4
5
|
import { parseExplicitBoolean } from './value-normalization';
|
|
5
6
|
function listRequestState(url, resource) {
|
|
@@ -58,7 +59,174 @@ function listRequestFilters(resource, url) {
|
|
|
58
59
|
});
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
|
-
|
|
62
|
+
const structured = parseStructuredFilters(resource, url);
|
|
63
|
+
return structured.length > 0 ? structured : filters;
|
|
64
|
+
}
|
|
65
|
+
function invalidStructuredFilters() {
|
|
66
|
+
throw error(400, 'Invalid structured filter query.');
|
|
67
|
+
}
|
|
68
|
+
function parseStructuredFilters(resource, url) {
|
|
69
|
+
const structuredKeys = [...url.searchParams.keys()].filter(key => key.startsWith('filters['));
|
|
70
|
+
const entryPattern = /^filters\[([0-9]+(?:\.value\.[0-9]+)*)\]\[(field|operator|value|values\.(?:0|[1-9]\d*))\]$/;
|
|
71
|
+
if (structuredKeys.some(key => !entryPattern.test(key)))
|
|
72
|
+
invalidStructuredFilters();
|
|
73
|
+
const entries = [...url.searchParams.entries()]
|
|
74
|
+
.map(([key, value]) => {
|
|
75
|
+
const match = entryPattern.exec(key);
|
|
76
|
+
return match ? { path: match[1], property: match[2], value } : undefined;
|
|
77
|
+
})
|
|
78
|
+
.filter((entry) => entry !== undefined);
|
|
79
|
+
if (entries.length === 0)
|
|
80
|
+
return [];
|
|
81
|
+
if (entries.length > 300)
|
|
82
|
+
invalidStructuredFilters();
|
|
83
|
+
const roots = [];
|
|
84
|
+
const seen = new Set();
|
|
85
|
+
let textLength = 0;
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
textLength += entry.path.length + entry.property.length + entry.value.length;
|
|
88
|
+
if (textLength > 64_000 || entry.path.length > 400)
|
|
89
|
+
invalidStructuredFilters();
|
|
90
|
+
const entryKey = `${entry.path}:${entry.property}`;
|
|
91
|
+
if (seen.has(entryKey))
|
|
92
|
+
invalidStructuredFilters();
|
|
93
|
+
seen.add(entryKey);
|
|
94
|
+
const parts = entry.path.split('.value.');
|
|
95
|
+
if (parts.length > 33 || parts.some(part => !/^(0|[1-9]\d*)$/.test(part)))
|
|
96
|
+
invalidStructuredFilters();
|
|
97
|
+
const segments = parts.map(Number);
|
|
98
|
+
const rootIndex = segments.shift();
|
|
99
|
+
if (rootIndex === undefined || !Number.isSafeInteger(rootIndex) || rootIndex < 0 || rootIndex > 1000)
|
|
100
|
+
invalidStructuredFilters();
|
|
101
|
+
let node = roots[rootIndex];
|
|
102
|
+
if (!node) {
|
|
103
|
+
node = {};
|
|
104
|
+
roots[rootIndex] = node;
|
|
105
|
+
}
|
|
106
|
+
for (const childIndex of segments) {
|
|
107
|
+
if (!Number.isSafeInteger(childIndex) || childIndex < 0 || childIndex > 1000)
|
|
108
|
+
invalidStructuredFilters();
|
|
109
|
+
node.children ??= [];
|
|
110
|
+
node = node.children[childIndex] ??= {};
|
|
111
|
+
}
|
|
112
|
+
if (entry.property.startsWith('values.')) {
|
|
113
|
+
const index = Number(entry.property.slice('values.'.length));
|
|
114
|
+
if (!Number.isSafeInteger(index) || index >= FILTER_COLLECTION_LIMIT)
|
|
115
|
+
invalidStructuredFilters();
|
|
116
|
+
(node.values ??= [])[index] = entry.value;
|
|
117
|
+
}
|
|
118
|
+
else if (entry.property === 'field' || entry.property === 'operator' || entry.property === 'value') {
|
|
119
|
+
node[entry.property] = entry.value;
|
|
120
|
+
}
|
|
121
|
+
else
|
|
122
|
+
invalidStructuredFilters();
|
|
123
|
+
}
|
|
124
|
+
const validOperators = new Set([
|
|
125
|
+
'eq', 'ne', 'lt', 'gt', 'lte', 'gte', 'contains', 'ncontains',
|
|
126
|
+
'startswith', 'endswith', 'in', 'nin', 'null', 'nnull', 'between', 'nbetween',
|
|
127
|
+
]);
|
|
128
|
+
let visited = 0;
|
|
129
|
+
function compileList(nodes, depth) {
|
|
130
|
+
// 数组迭代方法会跳过空洞,显式逐索引拒绝稀疏路径。
|
|
131
|
+
const filters = [];
|
|
132
|
+
for (let index = 0; index < nodes.length; index++) {
|
|
133
|
+
const node = nodes[index];
|
|
134
|
+
if (!node)
|
|
135
|
+
invalidStructuredFilters();
|
|
136
|
+
filters.push(compile(node, depth));
|
|
137
|
+
}
|
|
138
|
+
return filters;
|
|
139
|
+
}
|
|
140
|
+
function compile(node, depth) {
|
|
141
|
+
if (++visited > 1000 || depth > 32)
|
|
142
|
+
invalidStructuredFilters();
|
|
143
|
+
const operator = node.operator;
|
|
144
|
+
if (operator === 'and' || operator === 'or') {
|
|
145
|
+
if (node.field !== undefined || node.value !== undefined || node.values !== undefined)
|
|
146
|
+
invalidStructuredFilters();
|
|
147
|
+
const children = node.children ?? [];
|
|
148
|
+
return { operator, value: compileList(children, depth + 1) };
|
|
149
|
+
}
|
|
150
|
+
if (node.children !== undefined || !node.field || !operator || !validOperators.has(operator)) {
|
|
151
|
+
invalidStructuredFilters();
|
|
152
|
+
}
|
|
153
|
+
const field = resource.fields.find(candidate => candidate.key === node.field);
|
|
154
|
+
if (!field || field.filterable === false)
|
|
155
|
+
invalidStructuredFilters();
|
|
156
|
+
if (isCollectionFilterOperator(operator)) {
|
|
157
|
+
if (node.value !== undefined || !node.values)
|
|
158
|
+
invalidStructuredFilters();
|
|
159
|
+
const values = [];
|
|
160
|
+
for (let index = 0; index < node.values.length; index++) {
|
|
161
|
+
const raw = node.values[index];
|
|
162
|
+
if (raw === undefined)
|
|
163
|
+
invalidStructuredFilters();
|
|
164
|
+
if (['number', 'currency', 'percent', 'rating', 'rate'].includes(field.type)) {
|
|
165
|
+
if (!/^-?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(raw))
|
|
166
|
+
invalidStructuredFilters();
|
|
167
|
+
values.push(Number(raw));
|
|
168
|
+
}
|
|
169
|
+
else if (field.type === 'boolean') {
|
|
170
|
+
if (raw !== 'true' && raw !== 'false')
|
|
171
|
+
invalidStructuredFilters();
|
|
172
|
+
values.push(raw === 'true');
|
|
173
|
+
}
|
|
174
|
+
else if (field.type === 'select') {
|
|
175
|
+
// 集合枚举使用 JSON 标量令牌,数值 2 与字符串 "2" 不再碰撞。
|
|
176
|
+
try {
|
|
177
|
+
values.push(JSON.parse(raw));
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
invalidStructuredFilters();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else
|
|
184
|
+
values.push(raw);
|
|
185
|
+
}
|
|
186
|
+
if (!isFilterCollectionValue(field, operator, values))
|
|
187
|
+
invalidStructuredFilters();
|
|
188
|
+
return { field: node.field, operator: operator, value: values };
|
|
189
|
+
}
|
|
190
|
+
if (node.values !== undefined || !filterOperatorsForField(field).includes(operator))
|
|
191
|
+
invalidStructuredFilters();
|
|
192
|
+
if (operator === 'null' || operator === 'nnull') {
|
|
193
|
+
if (node.value !== undefined)
|
|
194
|
+
invalidStructuredFilters();
|
|
195
|
+
return { field: node.field, operator, value: null };
|
|
196
|
+
}
|
|
197
|
+
if (node.value === undefined)
|
|
198
|
+
invalidStructuredFilters();
|
|
199
|
+
{
|
|
200
|
+
if ((field.type === 'boolean' && !['eq', 'ne'].includes(operator))
|
|
201
|
+
|| (['in', 'nin', 'between', 'nbetween'].includes(operator)))
|
|
202
|
+
invalidStructuredFilters();
|
|
203
|
+
const raw = node.value;
|
|
204
|
+
if (['number', 'currency', 'percent', 'rating', 'rate'].includes(field.type)) {
|
|
205
|
+
if (!['eq', 'ne', 'lt', 'gt', 'lte', 'gte'].includes(operator))
|
|
206
|
+
invalidStructuredFilters();
|
|
207
|
+
if (!/^-?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(raw))
|
|
208
|
+
invalidStructuredFilters();
|
|
209
|
+
const value = Number(raw);
|
|
210
|
+
if (!Number.isFinite(value))
|
|
211
|
+
invalidStructuredFilters();
|
|
212
|
+
return { field: node.field, operator: operator, value };
|
|
213
|
+
}
|
|
214
|
+
if (field.type === 'boolean') {
|
|
215
|
+
if (raw !== 'true' && raw !== 'false')
|
|
216
|
+
invalidStructuredFilters();
|
|
217
|
+
return { field: node.field, operator: operator, value: raw === 'true' };
|
|
218
|
+
}
|
|
219
|
+
if (field.type === 'select') {
|
|
220
|
+
const matches = field.options?.filter(option => String(option.value) === raw && !option.disabled) ?? [];
|
|
221
|
+
const match = matches[0];
|
|
222
|
+
if (!['eq', 'ne'].includes(operator) || matches.length !== 1 || !match)
|
|
223
|
+
return invalidStructuredFilters();
|
|
224
|
+
return { field: node.field, operator: operator, value: match.value };
|
|
225
|
+
}
|
|
226
|
+
return { field: node.field, operator: operator, value: raw };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return compileList(roots, 0);
|
|
62
230
|
}
|
|
63
231
|
/**
|
|
64
232
|
* Creates a SvelteKit `load` function that fetches a resource list
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/lite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "SSR-first lightweight admin UI for @svadmin with optional progressive enhancement",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"svelte": "^5.56.10",
|
|
52
|
-
"@svadmin/core": ">=0.34.2 <0.
|
|
52
|
+
"@svadmin/core": ">=0.34.2 <0.55.0",
|
|
53
53
|
"@sveltejs/kit": "^2.70.3"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|