next-data-kit 1.0.0 → 2.0.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 +60 -3
- package/dist/client/components/data-kit-table.d.ts.map +1 -1
- package/dist/client/components/data-kit-table.js +1 -1
- package/dist/client/components/data-kit-table.js.map +1 -1
- package/dist/client/hooks/useDataKit.js +1 -1
- package/dist/client/hooks/useDataKit.js.map +1 -1
- package/dist/index.cjs +75 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -2
- package/dist/index.d.ts +6 -2
- package/dist/index.js +75 -41
- package/dist/index.js.map +1 -1
- package/dist/{next-data-kit-DmZ7pNHV.d.cts → next-data-kit-DBl9PPWh.d.cts} +2 -2
- package/dist/{next-data-kit-DmZ7pNHV.d.ts → next-data-kit-DBl9PPWh.d.ts} +2 -2
- package/dist/server.cjs +70 -39
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +20 -13
- package/dist/server.d.ts +20 -13
- package/dist/server.js +70 -39
- package/dist/server.js.map +1 -1
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.d.cts +2 -2
- package/dist/types/next-data-kit.d.ts +2 -2
- package/dist/types/next-data-kit.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -155,7 +155,7 @@ type TSortEntry = {
|
|
|
155
155
|
*/
|
|
156
156
|
type TFilterConfig = {
|
|
157
157
|
[key: string]: {
|
|
158
|
-
type: '
|
|
158
|
+
type: 'REGEX' | 'EXACT';
|
|
159
159
|
field?: string;
|
|
160
160
|
};
|
|
161
161
|
};
|
|
@@ -182,7 +182,7 @@ type TDataKitInput<T = unknown> = {
|
|
|
182
182
|
limit?: number;
|
|
183
183
|
sort?: TSortOptions<T>;
|
|
184
184
|
sorts?: TSortEntry[];
|
|
185
|
-
query?: Record<string,
|
|
185
|
+
query?: Record<string, string | number | boolean>;
|
|
186
186
|
filter?: Record<string, unknown>;
|
|
187
187
|
filterConfig?: TFilterConfig;
|
|
188
188
|
filterCustom?: TFilterCustomConfig<T>;
|
|
@@ -493,6 +493,9 @@ type TDataKitServerActionOptions<T, R> = {
|
|
|
493
493
|
filter?: (filterInput?: Record<string, unknown>) => TMongoFilterQuery<T>;
|
|
494
494
|
filterCustom?: TFilterCustomConfigWithFilter<T, TMongoFilterQuery<T>>;
|
|
495
495
|
defaultSort?: TSortOptions<T>;
|
|
496
|
+
maxLimit?: number;
|
|
497
|
+
filterAllowed?: string[];
|
|
498
|
+
queryAllowed?: string[];
|
|
496
499
|
};
|
|
497
500
|
declare const dataKitServerAction: <T, R>(props: Readonly<TDataKitServerActionOptions<T, R>>) => Promise<TDataKitResult<R>>;
|
|
498
501
|
|
|
@@ -506,6 +509,7 @@ declare const mongooseAdapter: <M extends TMongoModel<unknown, object>, DocType
|
|
|
506
509
|
filter?: (filterInput?: Record<string, unknown>) => TMongoFilterQuery<DocType>;
|
|
507
510
|
filterCustom?: TFilterCustomConfigWithFilter<DocType, TMongoFilterQuery<DocType>>;
|
|
508
511
|
defaultSort?: TSortOptions<DocType>;
|
|
512
|
+
[key: string]: any;
|
|
509
513
|
}>) => TDataKitAdapter<DocType>;
|
|
510
514
|
|
|
511
515
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -155,7 +155,7 @@ type TSortEntry = {
|
|
|
155
155
|
*/
|
|
156
156
|
type TFilterConfig = {
|
|
157
157
|
[key: string]: {
|
|
158
|
-
type: '
|
|
158
|
+
type: 'REGEX' | 'EXACT';
|
|
159
159
|
field?: string;
|
|
160
160
|
};
|
|
161
161
|
};
|
|
@@ -182,7 +182,7 @@ type TDataKitInput<T = unknown> = {
|
|
|
182
182
|
limit?: number;
|
|
183
183
|
sort?: TSortOptions<T>;
|
|
184
184
|
sorts?: TSortEntry[];
|
|
185
|
-
query?: Record<string,
|
|
185
|
+
query?: Record<string, string | number | boolean>;
|
|
186
186
|
filter?: Record<string, unknown>;
|
|
187
187
|
filterConfig?: TFilterConfig;
|
|
188
188
|
filterCustom?: TFilterCustomConfig<T>;
|
|
@@ -493,6 +493,9 @@ type TDataKitServerActionOptions<T, R> = {
|
|
|
493
493
|
filter?: (filterInput?: Record<string, unknown>) => TMongoFilterQuery<T>;
|
|
494
494
|
filterCustom?: TFilterCustomConfigWithFilter<T, TMongoFilterQuery<T>>;
|
|
495
495
|
defaultSort?: TSortOptions<T>;
|
|
496
|
+
maxLimit?: number;
|
|
497
|
+
filterAllowed?: string[];
|
|
498
|
+
queryAllowed?: string[];
|
|
496
499
|
};
|
|
497
500
|
declare const dataKitServerAction: <T, R>(props: Readonly<TDataKitServerActionOptions<T, R>>) => Promise<TDataKitResult<R>>;
|
|
498
501
|
|
|
@@ -506,6 +509,7 @@ declare const mongooseAdapter: <M extends TMongoModel<unknown, object>, DocType
|
|
|
506
509
|
filter?: (filterInput?: Record<string, unknown>) => TMongoFilterQuery<DocType>;
|
|
507
510
|
filterCustom?: TFilterCustomConfigWithFilter<DocType, TMongoFilterQuery<DocType>>;
|
|
508
511
|
defaultSort?: TSortOptions<DocType>;
|
|
512
|
+
[key: string]: any;
|
|
509
513
|
}>) => TDataKitAdapter<DocType>;
|
|
510
514
|
|
|
511
515
|
/**
|
package/dist/index.js
CHANGED
|
@@ -21,8 +21,30 @@ var calculatePagination = (page, limit, total) => ({
|
|
|
21
21
|
hasPrevPage: page > 1
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
+
// src/server/utils.ts
|
|
25
|
+
var escapeRegex = (str) => {
|
|
26
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
27
|
+
};
|
|
28
|
+
var createSearchFilter = (fields) => {
|
|
29
|
+
return (value) => {
|
|
30
|
+
if (!value || typeof value !== "string") {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
const escapedValue = escapeRegex(value);
|
|
34
|
+
return {
|
|
35
|
+
$or: fields.map((field) => ({
|
|
36
|
+
[field]: { $regex: escapedValue, $options: "i" }
|
|
37
|
+
}))
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
24
42
|
// src/server/adapters/mongoose.ts
|
|
25
43
|
var isProvided = (value) => value !== void 0 && value !== null && value !== "";
|
|
44
|
+
var isSafeKey = (key) => {
|
|
45
|
+
const unsafeKeys = ["__proto__", "constructor", "prototype"];
|
|
46
|
+
return !unsafeKeys.includes(key);
|
|
47
|
+
};
|
|
26
48
|
var mongooseAdapter = (model, options = {}) => {
|
|
27
49
|
const { filter: customFilterFn, filterCustom, defaultSort = { _id: -1 } } = options;
|
|
28
50
|
return async ({ filter, sorts, limit, skip, input }) => {
|
|
@@ -42,7 +64,7 @@ var mongooseAdapter = (model, options = {}) => {
|
|
|
42
64
|
let filterQuery = {};
|
|
43
65
|
if (input.query) {
|
|
44
66
|
Object.entries(input.query).forEach(([key, value]) => {
|
|
45
|
-
if (isProvided(value)) {
|
|
67
|
+
if (isProvided(value) && isSafeKey(key)) {
|
|
46
68
|
filterQuery[key] = value;
|
|
47
69
|
}
|
|
48
70
|
});
|
|
@@ -54,28 +76,28 @@ var mongooseAdapter = (model, options = {}) => {
|
|
|
54
76
|
if (filter && !customFilterFn) {
|
|
55
77
|
if (input.filterConfig) {
|
|
56
78
|
Object.entries(filter).forEach(([key, value]) => {
|
|
57
|
-
if (isProvided(value) && input.filterConfig?.[key]) {
|
|
79
|
+
if (isProvided(value) && isSafeKey(key) && input.filterConfig?.[key]) {
|
|
58
80
|
const config = input.filterConfig[key];
|
|
59
81
|
const fieldName = config?.field ?? key;
|
|
60
|
-
if (config?.type === "
|
|
82
|
+
if (config?.type === "REGEX") {
|
|
61
83
|
filterQuery[fieldName] = {
|
|
62
|
-
$regex: value,
|
|
84
|
+
$regex: escapeRegex(String(value)),
|
|
63
85
|
$options: "i"
|
|
64
86
|
};
|
|
65
|
-
} else if (config?.type === "
|
|
87
|
+
} else if (config?.type === "EXACT") {
|
|
66
88
|
filterQuery[fieldName] = value;
|
|
67
89
|
}
|
|
68
90
|
}
|
|
69
91
|
});
|
|
70
92
|
} else {
|
|
71
93
|
Object.entries(filter).forEach(([key, value]) => {
|
|
72
|
-
if (isProvided(value)) {
|
|
94
|
+
if (isProvided(value) && isSafeKey(key)) {
|
|
73
95
|
if (typeof value === "string") {
|
|
74
96
|
filterQuery[key] = {
|
|
75
|
-
$regex: value,
|
|
97
|
+
$regex: escapeRegex(value),
|
|
76
98
|
$options: "i"
|
|
77
99
|
};
|
|
78
|
-
} else {
|
|
100
|
+
} else if (typeof value === "number" || typeof value === "boolean") {
|
|
79
101
|
filterQuery[key] = value;
|
|
80
102
|
}
|
|
81
103
|
}
|
|
@@ -84,7 +106,7 @@ var mongooseAdapter = (model, options = {}) => {
|
|
|
84
106
|
}
|
|
85
107
|
if (filterCustom && filter) {
|
|
86
108
|
Object.entries(filter).forEach(([key, value]) => {
|
|
87
|
-
if (isProvided(value) && filterCustom[key]) {
|
|
109
|
+
if (isProvided(value) && isSafeKey(key) && filterCustom[key]) {
|
|
88
110
|
const customFilter = filterCustom[key](value);
|
|
89
111
|
filterQuery = { ...filterQuery, ...customFilter };
|
|
90
112
|
}
|
|
@@ -98,18 +120,44 @@ var mongooseAdapter = (model, options = {}) => {
|
|
|
98
120
|
|
|
99
121
|
// src/server/action.ts
|
|
100
122
|
var dataKitServerAction = async (props) => {
|
|
101
|
-
const { input, adapter, item,
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
123
|
+
const { input, adapter, item, maxLimit = 100, filterAllowed, queryAllowed } = props;
|
|
124
|
+
if (input.query) {
|
|
125
|
+
const safeQuery = {};
|
|
126
|
+
Object.keys(input.query).forEach((key) => {
|
|
127
|
+
if (queryAllowed && !queryAllowed.includes(key)) {
|
|
128
|
+
throw new Error(`[Security] Query field '${key}' is not allowed.`);
|
|
129
|
+
}
|
|
130
|
+
const val = input.query[key];
|
|
131
|
+
if (val !== null && typeof val === "object") {
|
|
132
|
+
throw new Error(`[Security] Query value for '${key}' must be a primitive.`);
|
|
133
|
+
}
|
|
134
|
+
if (val !== void 0) {
|
|
135
|
+
safeQuery[key] = val;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
input.query = safeQuery;
|
|
139
|
+
}
|
|
140
|
+
if (input.filter) {
|
|
141
|
+
const safeFilter = {};
|
|
142
|
+
Object.keys(input.filter).forEach((key) => {
|
|
143
|
+
if (filterAllowed && !filterAllowed.includes(key)) {
|
|
144
|
+
throw new Error(`[Security] Filter field '${key}' is not allowed.`);
|
|
145
|
+
}
|
|
146
|
+
const val = input.filter[key];
|
|
147
|
+
if (val !== null && typeof val === "object") {
|
|
148
|
+
throw new Error(`[Security] Filter value for '${key}' must be a primitive.`);
|
|
149
|
+
}
|
|
150
|
+
safeFilter[key] = val;
|
|
151
|
+
});
|
|
152
|
+
input.filter = safeFilter;
|
|
153
|
+
}
|
|
154
|
+
const finalAdapter = typeof adapter === "function" ? adapter : mongooseAdapter(adapter, props);
|
|
107
155
|
switch (input.action ?? "FETCH") {
|
|
108
156
|
case "FETCH": {
|
|
109
157
|
if (!input.limit || !input.page) {
|
|
110
158
|
throw new Error("Invalid input: missing limit or page");
|
|
111
159
|
}
|
|
112
|
-
const limit = input.limit;
|
|
160
|
+
const limit = Math.min(input.limit, maxLimit);
|
|
113
161
|
const skip = limit * (input.page - 1);
|
|
114
162
|
const { items, total } = await finalAdapter({
|
|
115
163
|
filter: input.filter ?? {},
|
|
@@ -215,39 +263,22 @@ var adapterMemory = (dataset, options = {}) => {
|
|
|
215
263
|
return { items, total };
|
|
216
264
|
};
|
|
217
265
|
};
|
|
218
|
-
|
|
219
|
-
// src/server/utils.ts
|
|
220
|
-
var escapeRegex = (str) => {
|
|
221
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
222
|
-
};
|
|
223
|
-
var createSearchFilter = (fields) => {
|
|
224
|
-
return (value) => {
|
|
225
|
-
if (!value || typeof value !== "string") {
|
|
226
|
-
return {};
|
|
227
|
-
}
|
|
228
|
-
const escapedValue = escapeRegex(value);
|
|
229
|
-
return {
|
|
230
|
-
$or: fields.map((field) => ({
|
|
231
|
-
[field]: { $regex: escapedValue, $options: "i" }
|
|
232
|
-
}))
|
|
233
|
-
};
|
|
234
|
-
};
|
|
235
|
-
};
|
|
236
266
|
z.object({
|
|
237
267
|
page: z.number().int().positive().optional(),
|
|
238
|
-
limit: z.number().int().positive().
|
|
239
|
-
query: z.record(z.string(), z.
|
|
240
|
-
filter: z.record(z.string(), z.
|
|
268
|
+
limit: z.number().int().positive().optional(),
|
|
269
|
+
query: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
|
|
270
|
+
filter: z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null()])).optional(),
|
|
241
271
|
filterConfig: z.record(z.string(), z.object({
|
|
242
|
-
type: z.enum(["
|
|
272
|
+
type: z.enum(["REGEX", "EXACT"]),
|
|
243
273
|
field: z.string().optional()
|
|
244
274
|
})).optional(),
|
|
245
275
|
sorts: z.array(z.object({
|
|
246
276
|
path: z.string().max(100),
|
|
247
277
|
// Limit path length to prevent abuse
|
|
248
278
|
value: z.literal(-1).or(z.literal(1))
|
|
249
|
-
})).max(5).optional()
|
|
279
|
+
})).max(5).optional(),
|
|
250
280
|
// Limit to 5 sort fields
|
|
281
|
+
sort: z.record(z.string(), z.literal(1).or(z.literal(-1))).optional()
|
|
251
282
|
});
|
|
252
283
|
|
|
253
284
|
// src/client/utils/cn.ts
|
|
@@ -1278,7 +1309,7 @@ var DataKitRoot = (props) => {
|
|
|
1278
1309
|
/* @__PURE__ */ jsx(DropdownMenuContent, { align: "start", container: overlayContainer, children: Object.entries(selectable.actions).map(([key, action2]) => /* @__PURE__ */ jsx(DropdownMenuItem, { disabled: !!actionLoading, onSelect: () => handleSelectionAction(key), children: actionLoading === key ? "Working\u2026" : action2.name }, key)) })
|
|
1279
1310
|
] })
|
|
1280
1311
|
] }) }),
|
|
1281
|
-
columns.map((col, idx) => /* @__PURE__ */ jsx(React2__default.Fragment, { children: col.sortable ? /* @__PURE__ */ jsx(TableHead, { children: /* @__PURE__ */ jsxs(
|
|
1312
|
+
columns.map((col, idx) => /* @__PURE__ */ jsx(React2__default.Fragment, { children: col.sortable ? /* @__PURE__ */ jsx(TableHead, { ...React2__default.isValidElement(col.head) ? col.head.props : {}, children: /* @__PURE__ */ jsxs(
|
|
1282
1313
|
Button,
|
|
1283
1314
|
{
|
|
1284
1315
|
variant: "ghost",
|
|
@@ -1293,7 +1324,10 @@ var DataKitRoot = (props) => {
|
|
|
1293
1324
|
}
|
|
1294
1325
|
) }) : col.head }, idx))
|
|
1295
1326
|
] }) }),
|
|
1296
|
-
/* @__PURE__ */ jsx(TableBody, { children: dataKit.state.isLoading ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, { colSpan, className: "h-24 text-center", children: /* @__PURE__ */ jsx(Loader2, { className: "mx-auto size-5 animate-spin" }) }) }) : dataKit.
|
|
1327
|
+
/* @__PURE__ */ jsx(TableBody, { children: dataKit.state.isLoading ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, { colSpan, className: "h-24 text-center", children: /* @__PURE__ */ jsx(Loader2, { className: "mx-auto size-5 animate-spin" }) }) }) : dataKit.state.error ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsxs(TableCell, { colSpan, className: "h-24 text-center text-red-500", children: [
|
|
1328
|
+
"Error: ",
|
|
1329
|
+
dataKit.state.error.message
|
|
1330
|
+
] }) }) : dataKit.items.length === 0 ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, { colSpan, className: "h-24 text-center text-muted-foreground", children: "No results found." }) }) : dataKit.items.map((item, idx) => /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
1297
1331
|
selectable?.enabled && /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(
|
|
1298
1332
|
Checkbox,
|
|
1299
1333
|
{
|