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/dist/index.d.cts CHANGED
@@ -155,7 +155,7 @@ type TSortEntry = {
155
155
  */
156
156
  type TFilterConfig = {
157
157
  [key: string]: {
158
- type: 'regex' | 'exact';
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, unknown>;
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: 'regex' | 'exact';
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, unknown>;
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 === "regex") {
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 === "exact") {
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, filter, filterCustom, defaultSort } = props;
102
- const finalAdapter = typeof adapter === "function" ? adapter : mongooseAdapter(adapter, {
103
- filter,
104
- filterCustom,
105
- defaultSort
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().max(100, "Maximum limit is 100").optional(),
239
- query: z.record(z.string(), z.unknown()).optional(),
240
- filter: z.record(z.string(), z.unknown()).optional(),
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(["regex", "exact"]),
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.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: [
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
  {