@nestledjs/data-browser 0.1.7 → 0.1.8
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/index.js +62 -62
- package/lib/components/filters/DateRangeFilter.js +79 -98
- package/lib/components/filters/NumberRangeFilter.js +92 -108
- package/lib/components/filters/RelationComponents.js +129 -247
- package/lib/components/filters/RelationFilterField.js +77 -107
- package/lib/components/shared/AdminBreadcrumbs.js +51 -92
- package/lib/components/shared/AdminErrorStates.js +153 -200
- package/lib/components/shared/AdminStatusDisplay.js +202 -192
- package/lib/context/AdminDataContext.js +25 -21
- package/lib/hooks/useAdminList.js +22 -22
- package/lib/hooks/useClickOutside.js +12 -9
- package/lib/hooks/useDebounce.js +11 -10
- package/lib/hooks/useRelationData.js +70 -47
- package/lib/layouts/AdminDataLayout.js +250 -300
- package/lib/pages/AdminDataCreatePage.js +322 -433
- package/lib/pages/AdminDataEditPage.js +664 -803
- package/lib/pages/AdminDataIndexPage.js +58 -86
- package/lib/pages/AdminDataListPage.js +751 -1164
- package/lib/types/index.js +5 -5
- package/lib/utils/graphql-utils.js +266 -177
- package/lib/utils/secure-storage.js +165 -96
- package/lib/utils/string-utils.js +31 -19
- package/package.json +2 -2
|
@@ -1,145 +1,214 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if (
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
const ADMIN_CONFIG_KEY = "mi-admin-config";
|
|
2
|
+
const ADMIN_CONFIG_VERSION = "1.0";
|
|
3
|
+
const MAX_CONFIG_SIZE = 5e4;
|
|
4
|
+
const sanitizeString = (value) => {
|
|
5
|
+
if (typeof value !== "string") return "";
|
|
6
|
+
return value.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "").replace(/javascript:/gi, "").replace(/on\w+\s*=/gi, "").trim().substring(0, 1e3);
|
|
7
|
+
};
|
|
8
|
+
const sanitizeArray = (value) => {
|
|
9
|
+
if (!Array.isArray(value)) return [];
|
|
10
|
+
return value.slice(0, 100).map((item) => sanitizeString(item)).filter((item) => item.length > 0);
|
|
11
|
+
};
|
|
12
|
+
const sanitizeSortPreference = (value) => {
|
|
13
|
+
if (!value || typeof value !== "object") return null;
|
|
14
|
+
const obj = value;
|
|
15
|
+
const orderBy = sanitizeString(obj.orderBy);
|
|
16
|
+
const orderDirection = sanitizeString(obj.orderDirection);
|
|
17
|
+
if (!["asc", "desc"].includes(orderDirection)) return null;
|
|
18
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(orderBy)) return null;
|
|
19
|
+
return { orderBy, orderDirection };
|
|
20
|
+
};
|
|
21
|
+
const validateAdminConfig = (config) => {
|
|
22
|
+
if (!config || typeof config !== "object") return false;
|
|
23
|
+
const configObj = config;
|
|
24
|
+
if (configObj.version !== ADMIN_CONFIG_VERSION) return false;
|
|
25
|
+
if (!configObj.models || typeof configObj.models !== "object") return false;
|
|
26
|
+
const models = configObj.models;
|
|
27
|
+
for (const [modelName, modelConfig] of Object.entries(models)) {
|
|
28
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(modelName)) return false;
|
|
29
|
+
if (modelConfig && typeof modelConfig === "object") {
|
|
30
|
+
const config2 = modelConfig;
|
|
31
|
+
if (config2.visibleColumns !== void 0 && !Array.isArray(config2.visibleColumns)) return false;
|
|
32
|
+
if (config2.sortPreference !== void 0) {
|
|
33
|
+
const sortPref = sanitizeSortPreference(config2.sortPreference);
|
|
34
|
+
if (config2.sortPreference !== null && sortPref === null) return false;
|
|
19
35
|
}
|
|
20
|
-
if (
|
|
36
|
+
if (config2.searchFields !== void 0 && !Array.isArray(config2.searchFields)) return false;
|
|
21
37
|
}
|
|
22
38
|
}
|
|
23
|
-
return
|
|
24
|
-
}
|
|
39
|
+
return true;
|
|
40
|
+
};
|
|
41
|
+
const SecureAdminLocalStorage = {
|
|
25
42
|
// Get the full admin config with validation
|
|
26
43
|
getConfig: () => {
|
|
27
44
|
try {
|
|
28
|
-
const
|
|
29
|
-
if (!
|
|
30
|
-
return { version:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
45
|
+
const stored = localStorage.getItem(ADMIN_CONFIG_KEY);
|
|
46
|
+
if (!stored) {
|
|
47
|
+
return { version: ADMIN_CONFIG_VERSION, models: {} };
|
|
48
|
+
}
|
|
49
|
+
if (stored.length > MAX_CONFIG_SIZE) {
|
|
50
|
+
console.warn("[AdminLocalStorage] Config exceeds size limit, resetting");
|
|
51
|
+
localStorage.removeItem(ADMIN_CONFIG_KEY);
|
|
52
|
+
return { version: ADMIN_CONFIG_VERSION, models: {} };
|
|
53
|
+
}
|
|
54
|
+
const parsed = JSON.parse(stored);
|
|
55
|
+
if (!validateAdminConfig(parsed)) {
|
|
56
|
+
console.warn("[AdminLocalStorage] Invalid config detected, resetting");
|
|
57
|
+
localStorage.removeItem(ADMIN_CONFIG_KEY);
|
|
58
|
+
return { version: ADMIN_CONFIG_VERSION, models: {} };
|
|
59
|
+
}
|
|
60
|
+
return parsed;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.warn("[AdminLocalStorage] Failed to load config:", error);
|
|
37
63
|
try {
|
|
38
|
-
localStorage.removeItem(
|
|
64
|
+
localStorage.removeItem(ADMIN_CONFIG_KEY);
|
|
39
65
|
} catch {
|
|
40
66
|
}
|
|
41
|
-
return { version:
|
|
67
|
+
return { version: ADMIN_CONFIG_VERSION, models: {} };
|
|
42
68
|
}
|
|
43
69
|
},
|
|
44
70
|
// Save the full admin config with validation
|
|
45
|
-
setConfig: (
|
|
71
|
+
setConfig: (config) => {
|
|
46
72
|
try {
|
|
47
|
-
if (!
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
73
|
+
if (!validateAdminConfig(config)) {
|
|
74
|
+
console.warn("[AdminLocalStorage] Invalid config provided");
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
const serialized = JSON.stringify(config);
|
|
78
|
+
if (serialized.length > MAX_CONFIG_SIZE) {
|
|
79
|
+
console.warn("[AdminLocalStorage] Config too large to store");
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
localStorage.setItem(ADMIN_CONFIG_KEY, serialized);
|
|
83
|
+
return true;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.warn("[AdminLocalStorage] Failed to save config:", error);
|
|
86
|
+
return false;
|
|
53
87
|
}
|
|
54
88
|
},
|
|
55
89
|
// Get visible columns for a specific model with sanitization
|
|
56
|
-
getColumnVisibility: (
|
|
57
|
-
var
|
|
58
|
-
const
|
|
59
|
-
if (!
|
|
60
|
-
const
|
|
61
|
-
|
|
90
|
+
getColumnVisibility: (modelName) => {
|
|
91
|
+
var _a;
|
|
92
|
+
const sanitizedModelName = sanitizeString(modelName);
|
|
93
|
+
if (!sanitizedModelName) return null;
|
|
94
|
+
const config = SecureAdminLocalStorage.getConfig();
|
|
95
|
+
const columns = (_a = config.models[sanitizedModelName]) == null ? void 0 : _a.visibleColumns;
|
|
96
|
+
return columns ? sanitizeArray(columns) : null;
|
|
62
97
|
},
|
|
63
98
|
// Set visible columns for a specific model with validation
|
|
64
|
-
setColumnVisibility: (
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
99
|
+
setColumnVisibility: (modelName, visibleColumns) => {
|
|
100
|
+
const sanitizedModelName = sanitizeString(modelName);
|
|
101
|
+
const sanitizedColumns = sanitizeArray(visibleColumns);
|
|
102
|
+
if (!sanitizedModelName) return false;
|
|
103
|
+
const config = SecureAdminLocalStorage.getConfig();
|
|
104
|
+
if (!config.models[sanitizedModelName]) {
|
|
105
|
+
config.models[sanitizedModelName] = {};
|
|
106
|
+
}
|
|
107
|
+
config.models[sanitizedModelName].visibleColumns = sanitizedColumns;
|
|
108
|
+
return SecureAdminLocalStorage.setConfig(config);
|
|
69
109
|
},
|
|
70
110
|
// Get sort preference for a specific model with validation
|
|
71
|
-
getSortPreference: (
|
|
72
|
-
var
|
|
73
|
-
const
|
|
74
|
-
if (!
|
|
75
|
-
const
|
|
76
|
-
|
|
111
|
+
getSortPreference: (modelName) => {
|
|
112
|
+
var _a;
|
|
113
|
+
const sanitizedModelName = sanitizeString(modelName);
|
|
114
|
+
if (!sanitizedModelName) return null;
|
|
115
|
+
const config = SecureAdminLocalStorage.getConfig();
|
|
116
|
+
const sortPref = (_a = config.models[sanitizedModelName]) == null ? void 0 : _a.sortPreference;
|
|
117
|
+
return sortPref ? sanitizeSortPreference(sortPref) : null;
|
|
77
118
|
},
|
|
78
119
|
// Set sort preference for a specific model with validation
|
|
79
|
-
setSortPreference: (
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
120
|
+
setSortPreference: (modelName, sortPreference) => {
|
|
121
|
+
const sanitizedModelName = sanitizeString(modelName);
|
|
122
|
+
const sanitizedSortPref = sanitizeSortPreference(sortPreference);
|
|
123
|
+
if (!sanitizedModelName || !sanitizedSortPref) return false;
|
|
124
|
+
const config = SecureAdminLocalStorage.getConfig();
|
|
125
|
+
if (!config.models[sanitizedModelName]) {
|
|
126
|
+
config.models[sanitizedModelName] = {};
|
|
127
|
+
}
|
|
128
|
+
config.models[sanitizedModelName].sortPreference = sanitizedSortPref;
|
|
129
|
+
return SecureAdminLocalStorage.setConfig(config);
|
|
84
130
|
},
|
|
85
131
|
// Get search fields for a specific model with sanitization
|
|
86
|
-
getSearchFields: (
|
|
87
|
-
var
|
|
88
|
-
const
|
|
89
|
-
if (!
|
|
90
|
-
const
|
|
91
|
-
|
|
132
|
+
getSearchFields: (modelName) => {
|
|
133
|
+
var _a;
|
|
134
|
+
const sanitizedModelName = sanitizeString(modelName);
|
|
135
|
+
if (!sanitizedModelName) return null;
|
|
136
|
+
const config = SecureAdminLocalStorage.getConfig();
|
|
137
|
+
const searchFields = (_a = config.models[sanitizedModelName]) == null ? void 0 : _a.searchFields;
|
|
138
|
+
return searchFields ? sanitizeArray(searchFields) : null;
|
|
92
139
|
},
|
|
93
140
|
// Set search fields for a specific model with validation
|
|
94
|
-
setSearchFields: (
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
141
|
+
setSearchFields: (modelName, searchFields) => {
|
|
142
|
+
const sanitizedModelName = sanitizeString(modelName);
|
|
143
|
+
const sanitizedFields = sanitizeArray(searchFields);
|
|
144
|
+
if (!sanitizedModelName) return false;
|
|
145
|
+
const config = SecureAdminLocalStorage.getConfig();
|
|
146
|
+
if (!config.models[sanitizedModelName]) {
|
|
147
|
+
config.models[sanitizedModelName] = {};
|
|
148
|
+
}
|
|
149
|
+
config.models[sanitizedModelName].searchFields = sanitizedFields;
|
|
150
|
+
return SecureAdminLocalStorage.setConfig(config);
|
|
99
151
|
},
|
|
100
152
|
// Export config as JSON string with validation
|
|
101
153
|
exportConfig: () => {
|
|
102
154
|
try {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
155
|
+
const config = SecureAdminLocalStorage.getConfig();
|
|
156
|
+
if (!validateAdminConfig(config)) {
|
|
157
|
+
console.warn("[AdminLocalStorage] Cannot export invalid config");
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
return JSON.stringify(config, null, 2);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.warn("[AdminLocalStorage] Failed to export config:", error);
|
|
163
|
+
return null;
|
|
107
164
|
}
|
|
108
165
|
},
|
|
109
166
|
// Import config from JSON string with comprehensive security validation
|
|
110
|
-
importConfig: (
|
|
167
|
+
importConfig: (configJson) => {
|
|
111
168
|
try {
|
|
112
|
-
const
|
|
113
|
-
if (!
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
169
|
+
const sanitizedJson = sanitizeString(configJson);
|
|
170
|
+
if (!sanitizedJson || sanitizedJson.length > MAX_CONFIG_SIZE) {
|
|
171
|
+
console.warn("[AdminLocalStorage] Invalid or oversized config JSON");
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
const config = JSON.parse(sanitizedJson);
|
|
175
|
+
if (!validateAdminConfig(config)) {
|
|
176
|
+
console.warn("[AdminLocalStorage] Failed config validation during import");
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
const serialized = JSON.stringify(config);
|
|
180
|
+
const suspiciousPatterns = [
|
|
119
181
|
/<script/i,
|
|
120
182
|
/javascript:/i,
|
|
121
183
|
/on\w+\s*=/i,
|
|
122
184
|
/eval\s*\(/i,
|
|
123
185
|
/function\s*\(/i
|
|
124
186
|
];
|
|
125
|
-
for (const
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
187
|
+
for (const pattern of suspiciousPatterns) {
|
|
188
|
+
if (pattern.test(serialized)) {
|
|
189
|
+
console.warn("[AdminLocalStorage] Suspicious content detected in config");
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return SecureAdminLocalStorage.setConfig(config);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.warn("[AdminLocalStorage] Failed to import config:", error);
|
|
196
|
+
return false;
|
|
131
197
|
}
|
|
132
198
|
},
|
|
133
199
|
// Clear all stored data (for security/privacy)
|
|
134
200
|
clearConfig: () => {
|
|
135
201
|
try {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
202
|
+
localStorage.removeItem(ADMIN_CONFIG_KEY);
|
|
203
|
+
return true;
|
|
204
|
+
} catch (error) {
|
|
205
|
+
console.warn("[AdminLocalStorage] Failed to clear config:", error);
|
|
206
|
+
return false;
|
|
139
207
|
}
|
|
140
208
|
}
|
|
141
|
-
}
|
|
209
|
+
};
|
|
210
|
+
const AdminLocalStorage = SecureAdminLocalStorage;
|
|
142
211
|
export {
|
|
143
|
-
|
|
144
|
-
|
|
212
|
+
AdminLocalStorage,
|
|
213
|
+
SecureAdminLocalStorage
|
|
145
214
|
};
|
|
@@ -1,27 +1,39 @@
|
|
|
1
|
-
function
|
|
2
|
-
return
|
|
1
|
+
function kebabCase(name) {
|
|
2
|
+
return name.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
|
|
3
3
|
}
|
|
4
|
-
function
|
|
5
|
-
return
|
|
4
|
+
function spacedWords(name) {
|
|
5
|
+
return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/([A-Z])([A-Z][a-z])/g, "$1 $2");
|
|
6
6
|
}
|
|
7
|
-
function
|
|
8
|
-
return
|
|
7
|
+
function formatFieldName(fieldName) {
|
|
8
|
+
return fieldName.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (str) => str.toUpperCase());
|
|
9
9
|
}
|
|
10
|
-
function
|
|
11
|
-
|
|
10
|
+
function normalizeModelNameForDocument(modelName) {
|
|
11
|
+
if (modelName === modelName.toUpperCase() && modelName.length > 1) {
|
|
12
|
+
return modelName.charAt(0).toUpperCase() + modelName.slice(1).toLowerCase();
|
|
13
|
+
}
|
|
14
|
+
return modelName;
|
|
12
15
|
}
|
|
13
|
-
function
|
|
14
|
-
|
|
16
|
+
function getItemDisplayName(item) {
|
|
17
|
+
if (item.name) return item.name;
|
|
18
|
+
if (item.title) return item.title;
|
|
19
|
+
if (item.firstName && item.lastName) return `${item.firstName} ${item.lastName}`;
|
|
20
|
+
if (item.firstName) return item.firstName;
|
|
21
|
+
if (item.email) return item.email;
|
|
22
|
+
return item.id;
|
|
15
23
|
}
|
|
16
|
-
function
|
|
17
|
-
const
|
|
18
|
-
|
|
24
|
+
function getSmartSearchFields(availableFields) {
|
|
25
|
+
const primaryFields = ["name", "title", "email", "firstName", "lastName", "subject"];
|
|
26
|
+
const primaryMatches = primaryFields.filter((field) => availableFields.includes(field));
|
|
27
|
+
if (primaryMatches.length >= 1) {
|
|
28
|
+
return primaryMatches.slice(0, 2);
|
|
29
|
+
}
|
|
30
|
+
return availableFields.slice(0, Math.min(2, availableFields.length));
|
|
19
31
|
}
|
|
20
32
|
export {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
33
|
+
formatFieldName,
|
|
34
|
+
getItemDisplayName,
|
|
35
|
+
getSmartSearchFields,
|
|
36
|
+
kebabCase,
|
|
37
|
+
normalizeModelNameForDocument,
|
|
38
|
+
spacedWords
|
|
27
39
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nestledjs/data-browser",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Universal admin data browser for Nestled framework projects with full CRUD operations",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"module": "./index.js",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"@heroicons/react": "^2.0.0",
|
|
40
40
|
"@nestledjs/forms": "^0.5.0",
|
|
41
41
|
"@nestledjs/helpers": "^0.1.0",
|
|
42
|
-
"@nestledjs/shared-components": "^0.1.
|
|
42
|
+
"@nestledjs/shared-components": "^0.1.8",
|
|
43
43
|
"react": "^19.0.0",
|
|
44
44
|
"react-router": "^7.0.0"
|
|
45
45
|
},
|