@aginix/vulcan-data-provider 0.1.0 → 0.1.1

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.
@@ -1,6 +1,5 @@
1
- import { dataWithVirtualId, dataWithoutVirtualId, encodeId, getOrderBy, getPrimaryKey, getQuery, parseFilters, removePrimaryKey } from "./url_builder.js";
1
+ import { dataWithVirtualId, dataWithoutVirtualId, encodeId, getOrderBy, getPrimaryKey, getQuery, parseFilters, removePrimaryKey, stringifyQuery } from "./url_builder.js";
2
2
  import { HttpError } from "ra-core";
3
- import qs from "qs";
4
3
  import { dequal } from "dequal";
5
4
  //#region src/data_provider.ts
6
5
  /**
@@ -78,7 +77,7 @@ const unwrapItem = (json) => {
78
77
  */
79
78
  const vulcanDataProvider = (config) => {
80
79
  const { apiUrl, httpClient, defaultListOp = "eq", sortOrder, primaryKeys, pageParam = "page", perPageParam = "perPage" } = config;
81
- const stringify = (q) => qs.stringify(q);
80
+ const stringify = stringifyQuery;
82
81
  const sendList = async (url, options, primaryKey) => {
83
82
  const { json } = await runRequest(httpClient, url, options);
84
83
  const { rows, total } = unwrapList(json);
@@ -4,7 +4,7 @@ export type ParsedFilters = {
4
4
  /**
5
5
  * A flat record of query-string keys to either a single
6
6
  * `<op>.<value>` string (or `(child1,child2)` for `and`/`or`) or an array of
7
- * those strings when the same key needs to repeat — `qs.stringify` will
7
+ * those strings when the same key needs to repeat — `stringifyQuery` will
8
8
  * expand the array into `field=foo&field=bar`, which the server-side
9
9
  * `FilterParser` joins back into an AND.
10
10
  */
@@ -21,7 +21,7 @@ export type ParsedFilters = {
21
21
  * - `field@op: value` overrides the operator (e.g. `age@gt: 18`).
22
22
  * - Array values map to `field=in.(v1,v2,v3)`.
23
23
  * - `like` / `ilike` split the value on whitespace and emit one
24
- * `field=like.*token*` per token, which `qs.stringify` turns into repeated
24
+ * `field=like.*token*` per token, which `stringifyQuery` turns into repeated
25
25
  * keys (joined with AND server-side).
26
26
  * - `or` / `and` keys recurse and emit `or=(c1.<op>.v1,c2.<op>.v2)`.
27
27
  * - The `q` key is a free-form search term — it is forwarded verbatim
@@ -33,6 +33,25 @@ export type ParsedFilters = {
33
33
  export declare const parseFilters: (params: {
34
34
  filter?: Record<string, any>;
35
35
  }, defaultListOp?: VulcanOperator) => ParsedFilters;
36
+ /**
37
+ * Serialize a flat query record into a URL query string.
38
+ *
39
+ * Uses the platform-native `URLSearchParams` (present in every browser and in
40
+ * Node) rather than a userland query-string library, so nothing here drags a
41
+ * Node built-in into browser bundles. `qs` used to do that: its transitive
42
+ * `object-inspect` dependency imports `util.inspect` at module top level and
43
+ * relies on the legacy `package.json#browser` object mapping to stub it out —
44
+ * a mapping some bundlers (e.g. Vite 8's rolldown backend) don't honor, so the
45
+ * externalized stub throws on `.custom` access as soon as the data provider is
46
+ * imported. See issue #29.
47
+ *
48
+ * Array values repeat the bare key once per element (`field=a&field=b`), which
49
+ * matches `qs`'s `arrayFormat: 'repeat'` and is what the server-side
50
+ * `FilterParser` expects — it decodes each repeated value and AND-joins them.
51
+ * The repeat form is shorter than bracket-indexed keys, matches PostgREST URL
52
+ * conventions, and reads better in logs.
53
+ */
54
+ export declare const stringifyQuery: (query: Record<string, unknown>) => string;
36
55
  export declare const getPrimaryKey: (resource: string, primaryKeys?: Map<string, PrimaryKey>) => PrimaryKey;
37
56
  export declare const decodeId: (id: Identifier, primaryKey: PrimaryKey) => string[] | number[];
38
57
  export declare const encodeId: (data: Record<string, unknown>, primaryKey: PrimaryKey) => Identifier;
@@ -28,7 +28,7 @@ const isCompoundKey = (primaryKey) => primaryKey.length > 1;
28
28
  * - `field@op: value` overrides the operator (e.g. `age@gt: 18`).
29
29
  * - Array values map to `field=in.(v1,v2,v3)`.
30
30
  * - `like` / `ilike` split the value on whitespace and emit one
31
- * `field=like.*token*` per token, which `qs.stringify` turns into repeated
31
+ * `field=like.*token*` per token, which `stringifyQuery` turns into repeated
32
32
  * keys (joined with AND server-side).
33
33
  * - `or` / `and` keys recurse and emit `or=(c1.<op>.v1,c2.<op>.v2)`.
34
34
  * - The `q` key is a free-form search term — it is forwarded verbatim
@@ -98,6 +98,36 @@ const parseFilters = (params, defaultListOp = "eq") => {
98
98
  });
99
99
  return result;
100
100
  };
101
+ /**
102
+ * Serialize a flat query record into a URL query string.
103
+ *
104
+ * Uses the platform-native `URLSearchParams` (present in every browser and in
105
+ * Node) rather than a userland query-string library, so nothing here drags a
106
+ * Node built-in into browser bundles. `qs` used to do that: its transitive
107
+ * `object-inspect` dependency imports `util.inspect` at module top level and
108
+ * relies on the legacy `package.json#browser` object mapping to stub it out —
109
+ * a mapping some bundlers (e.g. Vite 8's rolldown backend) don't honor, so the
110
+ * externalized stub throws on `.custom` access as soon as the data provider is
111
+ * imported. See issue #29.
112
+ *
113
+ * Array values repeat the bare key once per element (`field=a&field=b`), which
114
+ * matches `qs`'s `arrayFormat: 'repeat'` and is what the server-side
115
+ * `FilterParser` expects — it decodes each repeated value and AND-joins them.
116
+ * The repeat form is shorter than bracket-indexed keys, matches PostgREST URL
117
+ * conventions, and reads better in logs.
118
+ */
119
+ const stringifyQuery = (query) => {
120
+ const params = new URLSearchParams();
121
+ for (const [key, value] of Object.entries(query)) {
122
+ if (value === void 0 || value === null) continue;
123
+ if (Array.isArray(value)) for (const item of value) {
124
+ if (item === void 0 || item === null) continue;
125
+ params.append(key, String(item));
126
+ }
127
+ else params.append(key, String(value));
128
+ }
129
+ return params.toString();
130
+ };
101
131
  const getPrimaryKey = (resource, primaryKeys) => {
102
132
  return primaryKeys?.get(resource) ?? ["id"];
103
133
  };
@@ -165,4 +195,4 @@ const getOrderBy = (field, order, primaryKey, sortOrder = "asc,desc") => {
165
195
  return `${field}.${direction}`;
166
196
  };
167
197
  //#endregion
168
- export { dataWithVirtualId, dataWithoutVirtualId, decodeId, encodeId, getOrderBy, getPrimaryKey, getQuery, parseFilters, removePrimaryKey };
198
+ export { dataWithVirtualId, dataWithoutVirtualId, decodeId, encodeId, getOrderBy, getPrimaryKey, getQuery, parseFilters, removePrimaryKey, stringifyQuery };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aginix/vulcan-data-provider",
3
3
  "description": "React Admin data provider for APIs built with @aginix/adonis-vulcan",
4
- "version": "0.1.0",
4
+ "version": "0.1.1",
5
5
  "engines": {
6
6
  "node": ">=24.0.0"
7
7
  },
@@ -54,8 +54,7 @@
54
54
  "author": "n3n",
55
55
  "license": "UNLICENSED",
56
56
  "dependencies": {
57
- "dequal": "^2.0.3",
58
- "qs": "^6.14.0"
57
+ "dequal": "^2.0.3"
59
58
  },
60
59
  "devDependencies": {
61
60
  "@adonisjs/eslint-config": "^3.0.0",
@@ -66,7 +65,6 @@
66
65
  "@poppinss/ts-exec": "^1.4.4",
67
66
  "@release-it/conventional-changelog": "^10.0.5",
68
67
  "@types/node": "^25.3.5",
69
- "@types/qs": "^6.9.18",
70
68
  "c8": "^11.0.0",
71
69
  "eslint": "^10.0.3",
72
70
  "prettier": "^3.8.1",
@@ -107,7 +105,6 @@
107
105
  "external": [
108
106
  "ra-core",
109
107
  "react-admin",
110
- "qs",
111
108
  "dequal"
112
109
  ]
113
110
  },