@nestledjs/data-browser 0.1.13 → 0.1.16
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 +0 -2
- package/index.d.ts +1 -0
- package/index.js +2 -0
- package/lib/hooks/useAdminList.js +2 -0
- package/lib/hooks/useRelationData.js +0 -3
- package/lib/layouts/AdminDataLayout.js +1 -1
- package/lib/pages/AdminDataCreatePage.js +41 -44
- package/lib/pages/AdminDataEditPage.js +101 -121
- package/lib/pages/AdminDataIndexPage.js +1 -1
- package/lib/pages/AdminDataListPage.js +115 -124
- package/lib/types/index.d.ts +3 -0
- package/lib/utils/get-plural-names.d.ts +21 -0
- package/lib/utils/get-plural-names.js +41 -0
- package/lib/utils/graphql-utils.d.ts +1 -1
- package/lib/utils/graphql-utils.js +18 -20
- package/lib/utils/secure-storage.js +0 -13
- package/package.json +2 -3
package/lib/types/index.d.ts
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pluralizes a word using the pluralize library, with special handling for words
|
|
3
|
+
* where the singular and plural forms are the same (like "data", "sheep").
|
|
4
|
+
* In those cases, appends "List" to make it clear it's a collection.
|
|
5
|
+
* Also includes overrides for known uncountable nouns that the pluralize library
|
|
6
|
+
* incorrectly pluralizes.
|
|
7
|
+
*
|
|
8
|
+
* @param name - The word to pluralize
|
|
9
|
+
* @returns The pluralized form, or the word + "List" if singular equals plural or is uncountable
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* getPluralName('user') // 'users'
|
|
14
|
+
* getPluralName('category') // 'categories'
|
|
15
|
+
* getPluralName('data') // 'dataList' (because 'data' plural is also 'data')
|
|
16
|
+
* getPluralName('sheep') // 'sheepList' (because 'sheep' plural is also 'sheep')
|
|
17
|
+
* getPluralName('luggage') // 'luggageList' (override: pluralize incorrectly returns 'luggages')
|
|
18
|
+
* getPluralName('furniture') // 'furnitureList' (override: pluralize incorrectly returns 'furnitures')
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare function getPluralName(name: string): string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import pluralize from "pluralize";
|
|
2
|
+
const UNCOUNTABLE_OVERRIDES = /* @__PURE__ */ new Set([
|
|
3
|
+
"advice",
|
|
4
|
+
"anger",
|
|
5
|
+
"art",
|
|
6
|
+
"beauty",
|
|
7
|
+
"courage",
|
|
8
|
+
"evidence",
|
|
9
|
+
"feedback",
|
|
10
|
+
"furniture",
|
|
11
|
+
"happiness",
|
|
12
|
+
"hardware",
|
|
13
|
+
"homework",
|
|
14
|
+
"housework",
|
|
15
|
+
"knowledge",
|
|
16
|
+
"love",
|
|
17
|
+
"luggage",
|
|
18
|
+
"music",
|
|
19
|
+
"news",
|
|
20
|
+
"progress",
|
|
21
|
+
"research",
|
|
22
|
+
"software",
|
|
23
|
+
"traffic",
|
|
24
|
+
"weather",
|
|
25
|
+
"wisdom"
|
|
26
|
+
]);
|
|
27
|
+
function getPluralName(name) {
|
|
28
|
+
if (!name || typeof name !== "string") {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`getPluralName: name must be a non-empty string, received type: ${typeof name}, value: ${String(name)}`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (UNCOUNTABLE_OVERRIDES.has(name.toLowerCase())) {
|
|
34
|
+
return name + "List";
|
|
35
|
+
}
|
|
36
|
+
const plural = pluralize(name);
|
|
37
|
+
return plural === name ? name + "List" : plural;
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
getPluralName
|
|
41
|
+
};
|
|
@@ -17,7 +17,7 @@ export declare function getMutationName(model: DatabaseModel, operation: 'create
|
|
|
17
17
|
/**
|
|
18
18
|
* Build form fields for a model and operation
|
|
19
19
|
*/
|
|
20
|
-
export declare function buildFormFields(sdk: any, model: DatabaseModel, operation: 'create' | 'update', currentItem?: any, isSubmitting?: boolean, basePath?: string): FormField[];
|
|
20
|
+
export declare function buildFormFields(sdk: any, model: DatabaseModel, operation: 'create' | 'update', currentItem?: any, isSubmitting?: boolean, basePath?: string, databaseModels?: DatabaseModel[]): FormField[];
|
|
21
21
|
/**
|
|
22
22
|
* Clean form input data for GraphQL mutations
|
|
23
23
|
* Removes Apollo metadata and system fields
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Link } from "react-router";
|
|
3
3
|
import { FormFieldClass } from "@nestledjs/forms";
|
|
4
|
-
import { getPluralName } from "@nestledjs/helpers";
|
|
5
4
|
import { RelationFieldWrapper } from "../components/RelationFieldWrapper.js";
|
|
5
|
+
import { getPluralName } from "./get-plural-names.js";
|
|
6
6
|
function getEnumValues(sdk, enumType) {
|
|
7
7
|
try {
|
|
8
8
|
const enumObject = sdk[enumType];
|
|
@@ -16,7 +16,6 @@ function getEnumValues(sdk, enumType) {
|
|
|
16
16
|
}
|
|
17
17
|
return values;
|
|
18
18
|
} catch (error) {
|
|
19
|
-
console.warn(`Failed to get enum values for type ${enumType}:`, error);
|
|
20
19
|
return null;
|
|
21
20
|
}
|
|
22
21
|
}
|
|
@@ -53,15 +52,6 @@ function getAdminDocuments(sdk, model) {
|
|
|
53
52
|
if (!documents.update) missingDocuments.push(updateDocumentName);
|
|
54
53
|
if (!documents.delete) missingDocuments.push(deleteDocumentName);
|
|
55
54
|
if (missingDocuments.length > 0) {
|
|
56
|
-
console.error(`[GraphQL Documents] Missing documents for model "${model.name}":`, {
|
|
57
|
-
model: model.name,
|
|
58
|
-
normalizedModelName,
|
|
59
|
-
normalizedPluralName,
|
|
60
|
-
missingDocuments,
|
|
61
|
-
availableDocuments: Object.keys(sdk).filter(
|
|
62
|
-
(key) => key.includes("Admin") && key.includes("Document")
|
|
63
|
-
)
|
|
64
|
-
});
|
|
65
55
|
throw new Error(
|
|
66
56
|
`Missing GraphQL documents for model "${model.name}": ${missingDocuments.join(", ")}. Please ensure the API server is running and the GraphQL schema is up to date.`
|
|
67
57
|
);
|
|
@@ -88,7 +78,19 @@ function toLowerCamelCase(str) {
|
|
|
88
78
|
if (!str) return "";
|
|
89
79
|
return str.charAt(0).toLowerCase() + str.slice(1);
|
|
90
80
|
}
|
|
91
|
-
function
|
|
81
|
+
function findForeignKeyFieldName(relatedModel, currentModelName, relationName) {
|
|
82
|
+
var _a;
|
|
83
|
+
if (!relatedModel) return null;
|
|
84
|
+
const foreignKeyField = relatedModel.fields.find((f) => {
|
|
85
|
+
if (f.type !== currentModelName) return false;
|
|
86
|
+
if (!f.relationName) return false;
|
|
87
|
+
if (f.isList) return false;
|
|
88
|
+
if (relationName && f.relationName !== relationName) return false;
|
|
89
|
+
return true;
|
|
90
|
+
});
|
|
91
|
+
return ((_a = foreignKeyField == null ? void 0 : foreignKeyField.relationFromFields) == null ? void 0 : _a[0]) || null;
|
|
92
|
+
}
|
|
93
|
+
function buildFormFields(sdk, model, operation, currentItem, isSubmitting, basePath = "/admin/data", databaseModels) {
|
|
92
94
|
const editableFields = model.fields.filter((field) => {
|
|
93
95
|
if (operation === "create" && field.isId) return false;
|
|
94
96
|
if (field.isReadOnly || field.isGenerated) return false;
|
|
@@ -113,7 +115,6 @@ function buildFormFields(sdk, model, operation, currentItem, isSubmitting, baseP
|
|
|
113
115
|
initialValue = isoString.substring(0, 16);
|
|
114
116
|
}
|
|
115
117
|
} catch (e) {
|
|
116
|
-
console.warn(`Failed to convert date value for field ${field.name}:`, e);
|
|
117
118
|
initialValue = "";
|
|
118
119
|
}
|
|
119
120
|
}
|
|
@@ -261,9 +262,6 @@ function buildFormFields(sdk, model, operation, currentItem, isSubmitting, baseP
|
|
|
261
262
|
});
|
|
262
263
|
break;
|
|
263
264
|
} else {
|
|
264
|
-
console.warn(
|
|
265
|
-
`GraphQL document ${adminDocumentName} or ${regularDocumentName} not found for relation ${field.type}. Using text input instead.`
|
|
266
|
-
);
|
|
267
265
|
formField = FormFieldClass.text(relationFieldName, {
|
|
268
266
|
label: `${label} ID`,
|
|
269
267
|
required: options.required,
|
|
@@ -291,16 +289,17 @@ function buildFormFields(sdk, model, operation, currentItem, isSubmitting, baseP
|
|
|
291
289
|
formFields.push(formField);
|
|
292
290
|
});
|
|
293
291
|
listRelationFields.forEach((field) => {
|
|
294
|
-
var _a
|
|
292
|
+
var _a;
|
|
295
293
|
const label = field.name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (str) => str.toUpperCase());
|
|
296
294
|
if (operation === "update" && currentItem) {
|
|
297
295
|
const pluralModelName = getPluralName(field.type);
|
|
298
296
|
const relatedModelKebab = toKebabCase(pluralModelName);
|
|
299
297
|
const displayName = field.type.replace(/([a-z])([A-Z])/g, "$1 $2");
|
|
300
298
|
const pluralDisplayName = getPluralName(displayName);
|
|
301
|
-
const
|
|
299
|
+
const relatedModel = databaseModels == null ? void 0 : databaseModels.find((m) => m.name === field.type);
|
|
300
|
+
const foreignKeyFieldName = findForeignKeyFieldName(relatedModel, model.name, field.relationName) || `${toLowerCamelCase(model.name)}Id`;
|
|
302
301
|
const filterUrl = `${basePath}/${relatedModelKebab}?${foreignKeyFieldName}=${currentItem.id}`;
|
|
303
|
-
const countData = (
|
|
302
|
+
const countData = (_a = currentItem._count) == null ? void 0 : _a[field.name];
|
|
304
303
|
const countText = countData !== void 0 ? ` (${countData})` : "";
|
|
305
304
|
const formField = FormFieldClass.content(field.name, {
|
|
306
305
|
content: React.createElement("div", { className: "py-2" }, [
|
|
@@ -384,7 +383,6 @@ function convertStringValue(value, field) {
|
|
|
384
383
|
const date = new Date(value);
|
|
385
384
|
return date.toISOString();
|
|
386
385
|
} catch (e) {
|
|
387
|
-
console.warn(`Failed to convert datetime-local value: ${value}`);
|
|
388
386
|
return value;
|
|
389
387
|
}
|
|
390
388
|
}
|
|
@@ -47,19 +47,16 @@ const SecureAdminLocalStorage = {
|
|
|
47
47
|
return { version: ADMIN_CONFIG_VERSION, models: {} };
|
|
48
48
|
}
|
|
49
49
|
if (stored.length > MAX_CONFIG_SIZE) {
|
|
50
|
-
console.warn("[AdminLocalStorage] Config exceeds size limit, resetting");
|
|
51
50
|
localStorage.removeItem(ADMIN_CONFIG_KEY);
|
|
52
51
|
return { version: ADMIN_CONFIG_VERSION, models: {} };
|
|
53
52
|
}
|
|
54
53
|
const parsed = JSON.parse(stored);
|
|
55
54
|
if (!validateAdminConfig(parsed)) {
|
|
56
|
-
console.warn("[AdminLocalStorage] Invalid config detected, resetting");
|
|
57
55
|
localStorage.removeItem(ADMIN_CONFIG_KEY);
|
|
58
56
|
return { version: ADMIN_CONFIG_VERSION, models: {} };
|
|
59
57
|
}
|
|
60
58
|
return parsed;
|
|
61
59
|
} catch (error) {
|
|
62
|
-
console.warn("[AdminLocalStorage] Failed to load config:", error);
|
|
63
60
|
try {
|
|
64
61
|
localStorage.removeItem(ADMIN_CONFIG_KEY);
|
|
65
62
|
} catch {
|
|
@@ -71,18 +68,15 @@ const SecureAdminLocalStorage = {
|
|
|
71
68
|
setConfig: (config) => {
|
|
72
69
|
try {
|
|
73
70
|
if (!validateAdminConfig(config)) {
|
|
74
|
-
console.warn("[AdminLocalStorage] Invalid config provided");
|
|
75
71
|
return false;
|
|
76
72
|
}
|
|
77
73
|
const serialized = JSON.stringify(config);
|
|
78
74
|
if (serialized.length > MAX_CONFIG_SIZE) {
|
|
79
|
-
console.warn("[AdminLocalStorage] Config too large to store");
|
|
80
75
|
return false;
|
|
81
76
|
}
|
|
82
77
|
localStorage.setItem(ADMIN_CONFIG_KEY, serialized);
|
|
83
78
|
return true;
|
|
84
79
|
} catch (error) {
|
|
85
|
-
console.warn("[AdminLocalStorage] Failed to save config:", error);
|
|
86
80
|
return false;
|
|
87
81
|
}
|
|
88
82
|
},
|
|
@@ -154,12 +148,10 @@ const SecureAdminLocalStorage = {
|
|
|
154
148
|
try {
|
|
155
149
|
const config = SecureAdminLocalStorage.getConfig();
|
|
156
150
|
if (!validateAdminConfig(config)) {
|
|
157
|
-
console.warn("[AdminLocalStorage] Cannot export invalid config");
|
|
158
151
|
return null;
|
|
159
152
|
}
|
|
160
153
|
return JSON.stringify(config, null, 2);
|
|
161
154
|
} catch (error) {
|
|
162
|
-
console.warn("[AdminLocalStorage] Failed to export config:", error);
|
|
163
155
|
return null;
|
|
164
156
|
}
|
|
165
157
|
},
|
|
@@ -168,12 +160,10 @@ const SecureAdminLocalStorage = {
|
|
|
168
160
|
try {
|
|
169
161
|
const sanitizedJson = sanitizeString(configJson);
|
|
170
162
|
if (!sanitizedJson || sanitizedJson.length > MAX_CONFIG_SIZE) {
|
|
171
|
-
console.warn("[AdminLocalStorage] Invalid or oversized config JSON");
|
|
172
163
|
return false;
|
|
173
164
|
}
|
|
174
165
|
const config = JSON.parse(sanitizedJson);
|
|
175
166
|
if (!validateAdminConfig(config)) {
|
|
176
|
-
console.warn("[AdminLocalStorage] Failed config validation during import");
|
|
177
167
|
return false;
|
|
178
168
|
}
|
|
179
169
|
const serialized = JSON.stringify(config);
|
|
@@ -186,13 +176,11 @@ const SecureAdminLocalStorage = {
|
|
|
186
176
|
];
|
|
187
177
|
for (const pattern of suspiciousPatterns) {
|
|
188
178
|
if (pattern.test(serialized)) {
|
|
189
|
-
console.warn("[AdminLocalStorage] Suspicious content detected in config");
|
|
190
179
|
return false;
|
|
191
180
|
}
|
|
192
181
|
}
|
|
193
182
|
return SecureAdminLocalStorage.setConfig(config);
|
|
194
183
|
} catch (error) {
|
|
195
|
-
console.warn("[AdminLocalStorage] Failed to import config:", error);
|
|
196
184
|
return false;
|
|
197
185
|
}
|
|
198
186
|
},
|
|
@@ -202,7 +190,6 @@ const SecureAdminLocalStorage = {
|
|
|
202
190
|
localStorage.removeItem(ADMIN_CONFIG_KEY);
|
|
203
191
|
return true;
|
|
204
192
|
} catch (error) {
|
|
205
|
-
console.warn("[AdminLocalStorage] Failed to clear config:", error);
|
|
206
193
|
return false;
|
|
207
194
|
}
|
|
208
195
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nestledjs/data-browser",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
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",
|
|
@@ -38,8 +38,7 @@
|
|
|
38
38
|
"@apollo/client": "^4.0.0",
|
|
39
39
|
"@heroicons/react": "^2.0.0",
|
|
40
40
|
"@nestledjs/forms": "^0.5.0",
|
|
41
|
-
"@nestledjs/
|
|
42
|
-
"@nestledjs/shared-components": "^0.1.12",
|
|
41
|
+
"@nestledjs/shared-components": "^0.1.16",
|
|
43
42
|
"react": "^19.0.0",
|
|
44
43
|
"react-router": "^7.0.0"
|
|
45
44
|
},
|