ag-grid-url-sync 0.3.0 → 0.5.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 +144 -14
- package/dist/core/ag-grid-url-sync.d.ts +13 -0
- package/dist/core/ag-grid-url-sync.js +10 -55
- package/dist/core/ag-grid-url-sync.js.map +1 -1
- package/dist/core/grid-integration.js +5 -67
- package/dist/core/grid-integration.js.map +1 -1
- package/dist/core/index.js +0 -5
- package/dist/core/index.js.map +1 -1
- package/dist/core/individual-params.d.ts +19 -0
- package/dist/core/individual-params.js +260 -0
- package/dist/core/individual-params.js.map +1 -0
- package/dist/core/serialization/formats.d.ts +8 -9
- package/dist/core/serialization/formats.js +63 -65
- package/dist/core/serialization/formats.js.map +1 -1
- package/dist/core/serialization/grouped.d.ts +10 -5
- package/dist/core/serialization/grouped.js +4 -51
- package/dist/core/serialization/grouped.js.map +1 -1
- package/dist/core/serialization/index.js +0 -4
- package/dist/core/serialization/index.js.map +1 -1
- package/dist/core/types.d.ts +42 -9
- package/dist/core/types.js +0 -118
- package/dist/core/types.js.map +1 -1
- package/dist/core/url-generator.d.ts +10 -7
- package/dist/core/url-generator.js +25 -165
- package/dist/core/url-generator.js.map +1 -1
- package/dist/core/url-parser.d.ts +19 -7
- package/dist/core/url-parser.js +28 -294
- package/dist/core/url-parser.js.map +1 -1
- package/dist/core/validation.d.ts +6 -6
- package/dist/core/validation.js +1 -78
- package/dist/core/validation.js.map +1 -1
- package/dist/core/view-storage.d.ts +63 -0
- package/dist/core/view-storage.js +112 -0
- package/dist/core/view-storage.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +0 -6
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +1 -0
- package/dist/react/types.d.ts +103 -1
- package/dist/react/use-ag-grid-url-sync.js +406 -46
- package/dist/react/use-ag-grid-url-sync.js.map +1 -1
- package/package.json +98 -31
|
@@ -1,178 +1,38 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { INTERNAL_TO_URL_OPERATION_MAP } from './types.js';
|
|
1
|
+
import { serializeFilters } from './individual-params.js';
|
|
3
2
|
import { serializeGrouped } from './serialization/grouped.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
};
|
|
9
|
-
/**
|
|
10
|
-
* Serializes a single column filter to URL parameter
|
|
11
|
-
*/
|
|
12
|
-
function serializeColumnFilter(columnName, filter, paramPrefix) {
|
|
13
|
-
const urlOperation = INTERNAL_TO_URL_OPERATION_MAP[filter.type];
|
|
14
|
-
if (!urlOperation)
|
|
15
|
-
return '';
|
|
16
|
-
const paramName = `${paramPrefix}${columnName}_${urlOperation}`;
|
|
17
|
-
// Handle date range operations
|
|
18
|
-
if (filter.type === 'dateRange' && filter.filterType === FILTER_TYPES.DATE) {
|
|
19
|
-
// Type guard: ensure this is a DateColumnFilter with dateRange operation
|
|
20
|
-
if ('filterTo' in filter && filter.filterTo !== undefined) {
|
|
21
|
-
// Don't encode the comma for range values - handle this directly
|
|
22
|
-
return `${paramName}=${filter.filter},${filter.filterTo}`;
|
|
23
|
-
}
|
|
24
|
-
return '';
|
|
25
|
-
}
|
|
26
|
-
// Handle number range operations
|
|
27
|
-
if (filter.type === 'inRange' && filter.filterType === FILTER_TYPES.NUMBER) {
|
|
28
|
-
// Type guard: ensure this is a NumberColumnFilter with inRange operation
|
|
29
|
-
if ('filterTo' in filter && filter.filterTo !== undefined) {
|
|
30
|
-
// Don't encode the comma for range values - handle this directly
|
|
31
|
-
return `${paramName}=${filter.filter},${filter.filterTo}`;
|
|
32
|
-
}
|
|
33
|
-
return '';
|
|
34
|
-
}
|
|
35
|
-
// Handle blank operations (shared across text, number, and date)
|
|
36
|
-
if (filter.type === 'blank' || filter.type === 'notBlank') {
|
|
37
|
-
return `${paramName}=true`;
|
|
38
|
-
}
|
|
39
|
-
// Handle date operations (ISO date format preservation)
|
|
40
|
-
if (filter.filterType === FILTER_TYPES.DATE) {
|
|
41
|
-
return `${paramName}=${filter.filter}`; // ISO dates don't need URL encoding
|
|
42
|
-
}
|
|
43
|
-
// Handle number operations
|
|
44
|
-
if (filter.filterType === FILTER_TYPES.NUMBER) {
|
|
45
|
-
return `${paramName}=${filter.filter}`;
|
|
46
|
-
}
|
|
47
|
-
// Handle text operations (existing logic)
|
|
48
|
-
if (filter.filterType === FILTER_TYPES.TEXT && filter.filter) {
|
|
49
|
-
// Note: Validation should be done at the serializeFilters level, not here
|
|
50
|
-
return `${paramName}=${encodeURIComponent(filter.filter.toString())}`;
|
|
51
|
-
}
|
|
52
|
-
return '';
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Helper function to append a serialized parameter to URLSearchParams
|
|
56
|
-
*/
|
|
57
|
-
function appendSerializedParam(serialized, params, isRangeOperation = false) {
|
|
58
|
-
const [paramName, paramValue] = serialized.split('=');
|
|
59
|
-
if (paramName && paramValue) {
|
|
60
|
-
if (isRangeOperation) {
|
|
61
|
-
// For range operations (which contain commas), don't decode
|
|
62
|
-
params.append(paramName, paramValue);
|
|
63
|
-
}
|
|
64
|
-
else {
|
|
65
|
-
// For regular operations, decode special characters for text filters
|
|
66
|
-
params.append(paramName, decodeURIComponent(paramValue));
|
|
67
|
-
}
|
|
68
|
-
}
|
|
3
|
+
export { serializeFilters };
|
|
4
|
+
function isFilterParam(key, config) {
|
|
5
|
+
return (key === config.groupedParam ||
|
|
6
|
+
(Boolean(config.paramPrefix) && key.startsWith(config.paramPrefix)));
|
|
69
7
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
*/
|
|
73
|
-
export function serializeFilters(filterState, config) {
|
|
74
|
-
const params = new URLSearchParams();
|
|
75
|
-
for (const [column, filter] of Object.entries(filterState)) {
|
|
76
|
-
// Support text, number, and date filters
|
|
77
|
-
if (filter.filterType !== FILTER_TYPES.TEXT &&
|
|
78
|
-
filter.filterType !== FILTER_TYPES.NUMBER &&
|
|
79
|
-
filter.filterType !== FILTER_TYPES.DATE)
|
|
80
|
-
continue;
|
|
81
|
-
// Apply validation for text filters before serialization
|
|
82
|
-
if (filter.filterType === FILTER_TYPES.TEXT) {
|
|
83
|
-
const validatedValue = validateFilterValue(filter.filter, config, filter.type);
|
|
84
|
-
const updatedFilter = { ...filter, filter: validatedValue };
|
|
85
|
-
const serialized = serializeColumnFilter(column, updatedFilter, config.paramPrefix);
|
|
86
|
-
if (serialized) {
|
|
87
|
-
appendSerializedParam(serialized, params, false); // Text filters are not range operations
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
// Number and date filters don't need additional validation as they're already validated
|
|
92
|
-
const serialized = serializeColumnFilter(column, filter, config.paramPrefix);
|
|
93
|
-
if (serialized) {
|
|
94
|
-
// Check if this is a range operation
|
|
95
|
-
const isRangeOp = filter.type === 'inRange' || filter.type === 'dateRange';
|
|
96
|
-
appendSerializedParam(serialized, params, isRangeOp);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
return params;
|
|
8
|
+
function getHostParams(url, config) {
|
|
9
|
+
return [...url.searchParams].filter(([key]) => !isFilterParam(key, config));
|
|
101
10
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
for (const [key, value] of url.searchParams.entries()) {
|
|
108
|
-
if ((excludeParam && key === excludeParam) ||
|
|
109
|
-
(config.paramPrefix && key.startsWith(config.paramPrefix))) {
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
params.set(key, value);
|
|
11
|
+
function toUrlString(url, params) {
|
|
12
|
+
const queryString = params.toString();
|
|
13
|
+
if (queryString) {
|
|
14
|
+
url.search = queryString;
|
|
15
|
+
return url.toString();
|
|
113
16
|
}
|
|
114
|
-
|
|
17
|
+
url.search = '';
|
|
18
|
+
const result = url.toString();
|
|
19
|
+
return result === `${url.origin}/` ? result.slice(0, -1) : result;
|
|
115
20
|
}
|
|
116
|
-
/**
|
|
117
|
-
* Generates a URL with the current filter state
|
|
118
|
-
*/
|
|
119
21
|
export function generateUrl(baseUrl, filterState, config) {
|
|
120
22
|
const url = new URL(baseUrl);
|
|
121
|
-
|
|
23
|
+
const hostParams = getHostParams(url, config);
|
|
122
24
|
if (config.serialization === 'grouped') {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
// Use shared helper to preserve non-filter parameters
|
|
128
|
-
const preservedParams = getPreservedParams(url, config);
|
|
129
|
-
for (const [key, value] of preservedParams.entries()) {
|
|
130
|
-
filterParams.append(key, value);
|
|
131
|
-
}
|
|
132
|
-
const queryString = filterParams.toString();
|
|
133
|
-
// If no filters, return the base URL without query string or trailing slash
|
|
134
|
-
if (!queryString) {
|
|
135
|
-
let result = url.toString().replace(/\?$/, '');
|
|
136
|
-
// Remove trailing slash for root paths when no query parameters
|
|
137
|
-
if (result.endsWith('/') && result === url.origin + '/') {
|
|
138
|
-
result = result.slice(0, -1);
|
|
25
|
+
const params = new URLSearchParams(hostParams);
|
|
26
|
+
const groupedResult = serializeGrouped(filterState, config);
|
|
27
|
+
if (groupedResult.value) {
|
|
28
|
+
params.append(groupedResult.paramName, groupedResult.value);
|
|
139
29
|
}
|
|
140
|
-
return
|
|
30
|
+
return toUrlString(url, params);
|
|
141
31
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Generates a URL with grouped serialization
|
|
147
|
-
*/
|
|
148
|
-
function generateGroupedUrl(baseUrl, filterState, config) {
|
|
149
|
-
const url = new URL(baseUrl);
|
|
150
|
-
// Serialize filters using grouped format
|
|
151
|
-
const groupedResult = serializeGrouped(filterState, config);
|
|
152
|
-
// Use shared helper to preserve non-filter parameters (excluding grouped param)
|
|
153
|
-
const preservedParams = getPreservedParams(url, config, config.groupedParam);
|
|
154
|
-
url.search = '';
|
|
155
|
-
for (const [key, value] of preservedParams.entries()) {
|
|
156
|
-
url.searchParams.set(key, value);
|
|
157
|
-
}
|
|
158
|
-
// Add the grouped parameter if there are filters
|
|
159
|
-
if (groupedResult.value) {
|
|
160
|
-
url.searchParams.set(groupedResult.paramName, groupedResult.value);
|
|
161
|
-
}
|
|
162
|
-
// If no filters, ensure the grouped param is removed
|
|
163
|
-
if (!groupedResult.value && url.searchParams.has(config.groupedParam)) {
|
|
164
|
-
url.searchParams.delete(config.groupedParam);
|
|
165
|
-
}
|
|
166
|
-
// Clean up empty query string
|
|
167
|
-
const queryString = url.searchParams.toString();
|
|
168
|
-
if (!queryString) {
|
|
169
|
-
let result = url.toString().replace(/\?$/, '');
|
|
170
|
-
// Remove trailing slash for root paths when no query parameters
|
|
171
|
-
if (result.endsWith('/') && result === url.origin + '/') {
|
|
172
|
-
result = result.slice(0, -1);
|
|
173
|
-
}
|
|
174
|
-
return result;
|
|
32
|
+
const params = serializeFilters(filterState, config);
|
|
33
|
+
for (const [key, value] of hostParams) {
|
|
34
|
+
params.append(key, value);
|
|
175
35
|
}
|
|
176
|
-
return url
|
|
36
|
+
return toUrlString(url, params);
|
|
177
37
|
}
|
|
178
38
|
//# sourceMappingURL=url-generator.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"url-generator.js","sourceRoot":"","sources":["../../src/core/url-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"url-generator.js","sourceRoot":"","sources":["../../src/core/url-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AACzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAE7D,OAAO,EAAE,gBAAgB,EAAE,CAAA;AAQ3B,SAAS,aAAa,CAAC,GAAW,EAAE,MAAmB;IACrD,OAAO,CACL,GAAG,KAAK,MAAM,CAAC,YAAY;QAC3B,CAAC,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CACpE,CAAA;AACH,CAAC;AAMD,SAAS,aAAa,CAAC,GAAQ,EAAE,MAAmB;IAClD,OAAO,CAAC,GAAG,GAAG,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,aAAa,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAA;AAC7E,CAAC;AAMD,SAAS,WAAW,CAAC,GAAQ,EAAE,MAAuB;IACpD,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAA;IACrC,IAAI,WAAW,EAAE,CAAC;QAChB,GAAG,CAAC,MAAM,GAAG,WAAW,CAAA;QACxB,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAA;IACvB,CAAC;IAED,GAAG,CAAC,MAAM,GAAG,EAAE,CAAA;IACf,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAA;IAC7B,OAAO,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAA;AACnE,CAAC;AAUD,MAAM,UAAU,WAAW,CACzB,OAAe,EACf,WAAwB,EACxB,MAAmB;IAEnB,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAA;IAC5B,MAAM,UAAU,GAAG,aAAa,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;IAE7C,IAAI,MAAM,CAAC,aAAa,KAAK,SAAS,EAAE,CAAC;QAEvC,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC,UAAU,CAAC,CAAA;QAC9C,MAAM,aAAa,GAAG,gBAAgB,CAAC,WAAW,EAAE,MAAM,CAAC,CAAA;QAC3D,IAAI,aAAa,CAAC,KAAK,EAAE,CAAC;YACxB,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,SAAS,EAAE,aAAa,CAAC,KAAK,CAAC,CAAA;QAC7D,CAAC;QACD,OAAO,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;IACjC,CAAC;IAGD,MAAM,MAAM,GAAG,gBAAgB,CAAC,WAAW,EAAE,MAAM,CAAC,CAAA;IACpD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,UAAU,EAAE,CAAC;QACtC,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;IAC3B,CAAC;IACD,OAAO,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;AACjC,CAAC"}
|
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
import type { FilterState,
|
|
1
|
+
import type { FilterState, ParseConfig } from './types.js';
|
|
2
|
+
import { parseFilterParam } from './individual-params.js';
|
|
3
|
+
export { parseFilterParam };
|
|
2
4
|
/**
|
|
3
|
-
*
|
|
5
|
+
* Converts URL search params into a filter state object.
|
|
6
|
+
* This function does not require a GridApi instance: leave `config.gridApi` out
|
|
7
|
+
* (or set it to `null`) when you only need to parse filters without a grid.
|
|
8
|
+
*
|
|
9
|
+
* Accepts an absolute URL, a relative path, a query string or a raw query
|
|
10
|
+
* string (see {@link toUrl}). A param that is not valid is skipped and reported
|
|
11
|
+
* through `onParseError`.
|
|
12
|
+
*
|
|
13
|
+
* If the URL has a non-empty `config.groupedParam`, it is decoded with
|
|
14
|
+
* `config.format` and the prefixed params are not read. This works in both
|
|
15
|
+
* serialization modes, so a grouped link opens in an app in individual mode
|
|
16
|
+
* when the param name and format match.
|
|
17
|
+
*
|
|
18
|
+
* @throws InvalidURLError if the input is an absolute URL that is not valid,
|
|
19
|
+
* or a host with no scheme
|
|
4
20
|
*/
|
|
5
|
-
export declare function
|
|
6
|
-
/**
|
|
7
|
-
* Converts URL search params into a filter state object
|
|
8
|
-
*/
|
|
9
|
-
export declare function parseUrlFilters(url: string, config: InternalConfig): FilterState;
|
|
21
|
+
export declare function parseUrlFilters(url: string, config: ParseConfig): FilterState;
|
package/dist/core/url-parser.js
CHANGED
|
@@ -1,305 +1,39 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
'greaterThanOrEqual',
|
|
13
|
-
'inRange'
|
|
14
|
-
].includes(operation);
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Detects if an operation is date-specific
|
|
18
|
-
*/
|
|
19
|
-
function isDateOperation(operation) {
|
|
20
|
-
return [
|
|
21
|
-
'dateBefore',
|
|
22
|
-
'dateBeforeOrEqual',
|
|
23
|
-
'dateAfter',
|
|
24
|
-
'dateAfterOrEqual',
|
|
25
|
-
'dateRange'
|
|
26
|
-
].includes(operation);
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Detects if an operation works across multiple filter types (shared operations)
|
|
30
|
-
*/
|
|
31
|
-
function isSharedOperation(operation) {
|
|
32
|
-
return ['eq', 'notEqual', 'blank', 'notBlank'].includes(operation);
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Attempts to parse a value as a date and returns the filter type
|
|
36
|
-
* This is used for shared operations to determine if they should be treated as date filters
|
|
37
|
-
*/
|
|
38
|
-
function tryParseAsDate(value) {
|
|
39
|
-
try {
|
|
40
|
-
validateAndParseDate(value);
|
|
41
|
-
return 'date';
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
return null;
|
|
1
|
+
import { InvalidURLError } from './types.js';
|
|
2
|
+
import { parseFilterParam, parseIndividualParams } from './individual-params.js';
|
|
3
|
+
import { deserializeGrouped } from './serialization/grouped.js';
|
|
4
|
+
export { parseFilterParam };
|
|
5
|
+
const PARSE_BASE = 'http://localhost';
|
|
6
|
+
const HAS_SCHEME = /^[a-z][a-z\d+.-]*:/i;
|
|
7
|
+
const SCHEMELESS_HOST = /^[\p{L}\p{N}-]+(?:(?:\.[\p{L}\p{N}-]+)+(?::\d+)?|:\d+)(?=[/?#]|$)/u;
|
|
8
|
+
function toUrl(input) {
|
|
9
|
+
if (SCHEMELESS_HOST.test(input)) {
|
|
10
|
+
throw new InvalidURLError(`Failed to parse URL: "${input}" has a host but no scheme. ` +
|
|
11
|
+
'Add a scheme (https://...) or start a relative path with "/".');
|
|
45
12
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
* Priority: Date > Number > Text (if the value can be parsed as date/number, prefer that type)
|
|
50
|
-
*/
|
|
51
|
-
function detectFilterTypeForSharedOperation(operation, value) {
|
|
52
|
-
// Blank operations default to text (most common case)
|
|
53
|
-
if (operation === 'blank' || operation === 'notBlank') {
|
|
54
|
-
return 'text';
|
|
55
|
-
}
|
|
56
|
-
// For eq/notEqual operations, try to detect the most specific type
|
|
57
|
-
if (operation === 'eq' || operation === 'notEqual') {
|
|
58
|
-
// Try date first (most specific format)
|
|
59
|
-
if (tryParseAsDate(value) === 'date') {
|
|
60
|
-
return 'date';
|
|
61
|
-
}
|
|
62
|
-
// Try number second
|
|
63
|
-
try {
|
|
64
|
-
const numValue = parseFloat(value);
|
|
65
|
-
if (!isNaN(numValue) && Number.isFinite(numValue)) {
|
|
66
|
-
const validation = validateNumberFilter(numValue);
|
|
67
|
-
if (validation.valid) {
|
|
68
|
-
return 'number';
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
// Fall through to text
|
|
74
|
-
}
|
|
75
|
-
// Default to text
|
|
76
|
-
return 'text';
|
|
77
|
-
}
|
|
78
|
-
return 'text';
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Extracts column name and operation from a filter parameter
|
|
82
|
-
*/
|
|
83
|
-
function extractColumnAndOperation(param, expectedPrefix) {
|
|
84
|
-
const ensureTrailingUnderscore = (prefix) => prefix.endsWith('_') ? prefix : prefix + '_';
|
|
85
|
-
const sanitizedPrefix = expectedPrefix
|
|
86
|
-
? ensureTrailingUnderscore(expectedPrefix)
|
|
87
|
-
: ensureTrailingUnderscore(DEFAULT_CONFIG.paramPrefix);
|
|
88
|
-
const prefixToCheck = sanitizedPrefix.slice(0, -1);
|
|
89
|
-
if (!param.startsWith(prefixToCheck + '_')) {
|
|
90
|
-
throw new InvalidFilterError(`Invalid filter prefix in parameter: ${param}`);
|
|
91
|
-
}
|
|
92
|
-
// Remove prefix and find the last underscore to separate column from operation
|
|
93
|
-
const withoutPrefix = param.substring(prefixToCheck.length + 1);
|
|
94
|
-
const lastUnderscoreIndex = withoutPrefix.lastIndexOf('_');
|
|
95
|
-
if (lastUnderscoreIndex === -1) {
|
|
96
|
-
throw new InvalidFilterError(`Invalid filter parameter format: ${param}`);
|
|
97
|
-
}
|
|
98
|
-
const column = withoutPrefix.substring(0, lastUnderscoreIndex);
|
|
99
|
-
const operation = withoutPrefix.substring(lastUnderscoreIndex + 1);
|
|
100
|
-
if (!column.trim()) {
|
|
101
|
-
throw new InvalidFilterError(`Empty column name in parameter: ${param}`);
|
|
102
|
-
}
|
|
103
|
-
return { column, operation };
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Parses a URL parameter into a column filter
|
|
107
|
-
*/
|
|
108
|
-
export function parseFilterParam(param, value, expectedPrefix) {
|
|
109
|
-
const { operation } = extractColumnAndOperation(param, expectedPrefix);
|
|
110
|
-
const filterOp = OPERATION_MAP[operation];
|
|
111
|
-
if (!filterOp) {
|
|
112
|
-
throw new InvalidFilterError(`Unsupported filter operation: ${operation}`);
|
|
113
|
-
}
|
|
114
|
-
// Handle date range operations
|
|
115
|
-
if (filterOp === 'dateRange') {
|
|
116
|
-
try {
|
|
117
|
-
const [startDate, endDate] = validateAndParseDateRange(value);
|
|
118
|
-
return {
|
|
119
|
-
filterType: 'date',
|
|
120
|
-
type: filterOp,
|
|
121
|
-
filter: startDate,
|
|
122
|
-
filterTo: endDate
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
catch (error) {
|
|
126
|
-
throw new InvalidFilterError(error instanceof Error ? error.message : 'Invalid date range');
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
// Handle number range operations
|
|
130
|
-
if (filterOp === 'inRange') {
|
|
131
|
-
const rangeParts = value.split(',');
|
|
132
|
-
if (rangeParts.length !== 2) {
|
|
133
|
-
throw new InvalidFilterError('Range operation requires exactly two values separated by comma');
|
|
134
|
-
}
|
|
135
|
-
const min = validateAndParseNumber(rangeParts[0] || '');
|
|
136
|
-
const max = validateAndParseNumber(rangeParts[1] || '');
|
|
137
|
-
const rangeValidation = validateNumberRange(min, max);
|
|
138
|
-
if (!rangeValidation.valid) {
|
|
139
|
-
throw new InvalidFilterError(rangeValidation.error || 'Invalid range values');
|
|
140
|
-
}
|
|
141
|
-
return {
|
|
142
|
-
filterType: 'number',
|
|
143
|
-
type: filterOp,
|
|
144
|
-
filter: min,
|
|
145
|
-
filterTo: max
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
// Handle date-specific operations
|
|
149
|
-
if (isDateOperation(filterOp)) {
|
|
150
|
-
try {
|
|
151
|
-
const dateValue = validateAndParseDate(value);
|
|
152
|
-
const validation = validateDateFilter(dateValue, filterOp);
|
|
153
|
-
if (!validation.valid) {
|
|
154
|
-
throw new InvalidFilterError(validation.error || 'Invalid date filter');
|
|
155
|
-
}
|
|
156
|
-
return {
|
|
157
|
-
filterType: 'date',
|
|
158
|
-
type: filterOp,
|
|
159
|
-
filter: dateValue
|
|
160
|
-
};
|
|
13
|
+
try {
|
|
14
|
+
if (HAS_SCHEME.test(input)) {
|
|
15
|
+
return new URL(input);
|
|
161
16
|
}
|
|
162
|
-
|
|
163
|
-
|
|
17
|
+
if (/^[/?#]/.test(input)) {
|
|
18
|
+
return new URL(input, PARSE_BASE);
|
|
164
19
|
}
|
|
20
|
+
const url = new URL(PARSE_BASE);
|
|
21
|
+
url.search = input;
|
|
22
|
+
return url;
|
|
165
23
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const numValue = validateAndParseNumber(value);
|
|
169
|
-
return {
|
|
170
|
-
filterType: 'number',
|
|
171
|
-
type: filterOp,
|
|
172
|
-
filter: numValue
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
// Handle shared operations (eq, notEqual, blank, notBlank)
|
|
176
|
-
if (isSharedOperation(filterOp)) {
|
|
177
|
-
const detectedType = detectFilterTypeForSharedOperation(filterOp, value);
|
|
178
|
-
if (detectedType === 'date') {
|
|
179
|
-
try {
|
|
180
|
-
const dateValue = validateAndParseDate(value);
|
|
181
|
-
return {
|
|
182
|
-
filterType: 'date',
|
|
183
|
-
type: filterOp,
|
|
184
|
-
filter: dateValue
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
catch {
|
|
188
|
-
// If date parsing fails, fall back to text
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
if (detectedType === 'number') {
|
|
192
|
-
try {
|
|
193
|
-
const numValue = parseFloat(value);
|
|
194
|
-
if (!isNaN(numValue) && Number.isFinite(numValue)) {
|
|
195
|
-
const validation = validateNumberFilter(numValue);
|
|
196
|
-
if (validation.valid) {
|
|
197
|
-
return {
|
|
198
|
-
filterType: 'number',
|
|
199
|
-
type: filterOp,
|
|
200
|
-
filter: numValue
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
catch {
|
|
206
|
-
// Fall through to text handling
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
// Handle as text filter (default for shared operations)
|
|
210
|
-
return {
|
|
211
|
-
filterType: 'text',
|
|
212
|
-
type: filterOp,
|
|
213
|
-
filter: filterOp === 'blank' || filterOp === 'notBlank' ? '' : value || ''
|
|
214
|
-
};
|
|
24
|
+
catch (error) {
|
|
25
|
+
throw new InvalidURLError(`Failed to parse URL: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
|
|
215
26
|
}
|
|
216
|
-
// Handle text-specific operations (existing logic)
|
|
217
|
-
return {
|
|
218
|
-
filterType: 'text',
|
|
219
|
-
type: filterOp,
|
|
220
|
-
filter: value || ''
|
|
221
|
-
};
|
|
222
27
|
}
|
|
223
|
-
/**
|
|
224
|
-
* Converts URL search params into a filter state object
|
|
225
|
-
*/
|
|
226
28
|
export function parseUrlFilters(url, config) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (!url || url.trim() === '') {
|
|
230
|
-
return {};
|
|
231
|
-
}
|
|
232
|
-
// Check for obviously malformed URLs that should throw errors
|
|
233
|
-
if (url === 'not-a-url' ||
|
|
234
|
-
url.startsWith('://') ||
|
|
235
|
-
(!url.includes('://') &&
|
|
236
|
-
!url.startsWith('/') &&
|
|
237
|
-
!url.startsWith('?') &&
|
|
238
|
-
url.includes('.'))) {
|
|
239
|
-
throw new Error('Invalid URL format');
|
|
240
|
-
}
|
|
241
|
-
// If URL starts with '?' treat as query string only
|
|
242
|
-
let urlToProcess = url;
|
|
243
|
-
if (url.startsWith('?')) {
|
|
244
|
-
urlToProcess = `http://example.com${url}`;
|
|
245
|
-
}
|
|
246
|
-
else if (!url.includes('://') && !url.startsWith('/')) {
|
|
247
|
-
// For other cases without protocol, try to handle as query string
|
|
248
|
-
urlToProcess = `http://example.com?${url}`;
|
|
249
|
-
}
|
|
250
|
-
else {
|
|
251
|
-
urlToProcess = url;
|
|
252
|
-
}
|
|
253
|
-
// First, try to detect grouped serialization
|
|
254
|
-
const DEFAULT_GROUPED_PARAMS = ['grid_filters', 'filters'];
|
|
255
|
-
const groupedDetection = detectGroupedSerialization(urlToProcess, [config.groupedParam, ...DEFAULT_GROUPED_PARAMS] // Common parameter names to check
|
|
256
|
-
);
|
|
257
|
-
if (groupedDetection.isGrouped &&
|
|
258
|
-
groupedDetection.value &&
|
|
259
|
-
groupedDetection.format) {
|
|
260
|
-
// Handle grouped serialization
|
|
261
|
-
return deserializeGrouped(groupedDetection.value, groupedDetection.format, config);
|
|
262
|
-
}
|
|
263
|
-
// Handle individual serialization (existing logic)
|
|
264
|
-
const urlObj = new URL(urlToProcess);
|
|
265
|
-
const filterState = {};
|
|
266
|
-
for (const [param, value] of urlObj.searchParams.entries()) {
|
|
267
|
-
if (!param.startsWith(config.paramPrefix))
|
|
268
|
-
continue;
|
|
269
|
-
try {
|
|
270
|
-
// Extract column name and parse the filter parameter
|
|
271
|
-
const { column: columnName } = extractColumnAndOperation(param, config.paramPrefix);
|
|
272
|
-
const filterParam = parseFilterParam(param, value, config.paramPrefix);
|
|
273
|
-
// Apply validation based on filter type
|
|
274
|
-
if (filterParam.filterType === 'text') {
|
|
275
|
-
const validatedValue = validateFilterValue(value, config, filterParam.type);
|
|
276
|
-
filterState[columnName] = {
|
|
277
|
-
...filterParam,
|
|
278
|
-
filter: validatedValue
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
else if (filterParam.filterType === 'date') {
|
|
282
|
-
// Date filters are already validated in parseFilterParam
|
|
283
|
-
filterState[columnName] = filterParam;
|
|
284
|
-
}
|
|
285
|
-
else {
|
|
286
|
-
// Number filters are already validated in parseFilterParam
|
|
287
|
-
filterState[columnName] = filterParam;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
catch (error) {
|
|
291
|
-
// Log parsing errors through the configured error handler
|
|
292
|
-
if (config.onParseError) {
|
|
293
|
-
config.onParseError(error);
|
|
294
|
-
}
|
|
295
|
-
// Continue processing other parameters instead of failing completely
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
return filterState;
|
|
29
|
+
if (!url || url.trim() === '') {
|
|
30
|
+
return {};
|
|
300
31
|
}
|
|
301
|
-
|
|
302
|
-
|
|
32
|
+
const urlObj = toUrl(url.trim());
|
|
33
|
+
const groupedValue = urlObj.searchParams.get(config.groupedParam);
|
|
34
|
+
if (groupedValue) {
|
|
35
|
+
return deserializeGrouped(groupedValue, config.format, config);
|
|
303
36
|
}
|
|
37
|
+
return parseIndividualParams(urlObj.searchParams, config);
|
|
304
38
|
}
|
|
305
39
|
//# sourceMappingURL=url-parser.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"url-parser.js","sourceRoot":"","sources":["../../src/core/url-parser.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"url-parser.js","sourceRoot":"","sources":["../../src/core/url-parser.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AAChF,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAA;AAE/D,OAAO,EAAE,gBAAgB,EAAE,CAAA;AAM3B,MAAM,UAAU,GAAG,kBAAkB,CAAA;AAGrC,MAAM,UAAU,GAAG,qBAAqB,CAAA;AAcxC,MAAM,eAAe,GACnB,oEAAoE,CAAA;AAqBtE,SAAS,KAAK,CAAC,KAAa;IAC1B,IAAI,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,eAAe,CACvB,yBAAyB,KAAK,8BAA8B;YAC1D,+DAA+D,CAClE,CAAA;IACH,CAAC;IAED,IAAI,CAAC;QACH,IAAI,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YAC3B,OAAO,IAAI,GAAG,CAAC,KAAK,CAAC,CAAA;QACvB,CAAC;QACD,IAAI,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YACzB,OAAO,IAAI,GAAG,CAAC,KAAK,EAAE,UAAU,CAAC,CAAA;QACnC,CAAC;QAGD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAA;QAC/B,GAAG,CAAC,MAAM,GAAG,KAAK,CAAA;QAClB,OAAO,GAAG,CAAA;IACZ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,eAAe,CACvB,wBAAwB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,EAAE,EAClF,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAC3C,CAAA;IACH,CAAC;AACH,CAAC;AAmBD,MAAM,UAAU,eAAe,CAAC,GAAW,EAAE,MAAmB;IAE9D,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAC9B,OAAO,EAAE,CAAA;IACX,CAAC;IAED,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAA;IAMhC,MAAM,YAAY,GAAG,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,YAAY,CAAC,CAAA;IACjE,IAAI,YAAY,EAAE,CAAC;QACjB,OAAO,kBAAkB,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChE,CAAC;IAGD,OAAO,qBAAqB,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAA;AAC3D,CAAC"}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ParseConfig, FilterOperation, DateFilterOperation, ColumnFilter } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Default configuration values
|
|
4
4
|
*/
|
|
5
5
|
export declare const DEFAULT_CONFIG: {
|
|
6
|
-
readonly paramPrefix:
|
|
6
|
+
readonly paramPrefix: 'f_';
|
|
7
7
|
readonly maxValueLength: 200;
|
|
8
|
-
readonly serialization:
|
|
9
|
-
readonly groupedParam:
|
|
10
|
-
readonly format:
|
|
8
|
+
readonly serialization: 'individual';
|
|
9
|
+
readonly groupedParam: 'grid_filters';
|
|
10
|
+
readonly format: 'querystring';
|
|
11
11
|
readonly onParseError: () => void;
|
|
12
12
|
};
|
|
13
13
|
/**
|
|
@@ -28,7 +28,7 @@ export type ValidationResult = {
|
|
|
28
28
|
* @returns The validated filter value (empty string for blank operations)
|
|
29
29
|
* @throws InvalidFilterError if value exceeds maximum length
|
|
30
30
|
*/
|
|
31
|
-
export declare function validateFilterValue(value: string, config:
|
|
31
|
+
export declare function validateFilterValue(value: string, config: ParseConfig, operation?: FilterOperation): string;
|
|
32
32
|
/**
|
|
33
33
|
* Validates a number filter value
|
|
34
34
|
*
|