@fabriciogferreira/schema-to-query-string 0.1.1 → 0.1.2
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/package.json
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabriciogferreira/schema-to-query-string",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"types": "dist/index.d.ts",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Fabrício G. Ferreira <fabriciof481@gmail.com>",
|
|
7
|
+
"description": "Function for convert schema to Spatie Laravel Query Builder",
|
|
9
8
|
"files": [
|
|
10
|
-
"
|
|
9
|
+
"src"
|
|
11
10
|
],
|
|
11
|
+
"keywords": [
|
|
12
|
+
"conversion",
|
|
13
|
+
"schema",
|
|
14
|
+
"query string"
|
|
15
|
+
],
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/fabriciogferreira/schema-to-query-string"
|
|
19
|
+
},
|
|
12
20
|
"scripts": {
|
|
13
21
|
"build": "tsc",
|
|
14
22
|
"test": "bun test",
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { schemaToQueryString } from './schema-to-query-string';
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
//* Libraries imports
|
|
2
|
+
import { ZodArray, ZodNullable, ZodObject } from "zod/v4";
|
|
3
|
+
|
|
4
|
+
export const schemaToQueryString = (
|
|
5
|
+
schema: ZodObject,
|
|
6
|
+
rootResource: string,
|
|
7
|
+
includeKey: string = "include",
|
|
8
|
+
fieldsKey: string = "fields"
|
|
9
|
+
) => {
|
|
10
|
+
const fields: Record<string, string[]> = {};
|
|
11
|
+
const includes = new Set<string>();
|
|
12
|
+
|
|
13
|
+
fields[rootResource] = [];
|
|
14
|
+
|
|
15
|
+
const walk = (
|
|
16
|
+
currentSchema: ZodObject,
|
|
17
|
+
resourcePath: string | null
|
|
18
|
+
) => {
|
|
19
|
+
const currentFieldsKey = resourcePath ?? rootResource;
|
|
20
|
+
|
|
21
|
+
if (!fields[currentFieldsKey]) {
|
|
22
|
+
fields[currentFieldsKey] = [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for (const key in currentSchema.shape) {
|
|
26
|
+
let rawField = currentSchema.shape[key];
|
|
27
|
+
|
|
28
|
+
if (rawField instanceof ZodNullable) {
|
|
29
|
+
rawField = rawField.unwrap()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (rawField instanceof ZodObject || rawField instanceof ZodArray) {
|
|
33
|
+
let nextPath = resourcePath
|
|
34
|
+
? `${resourcePath}.${key}`
|
|
35
|
+
: key;
|
|
36
|
+
|
|
37
|
+
includes.add(nextPath);
|
|
38
|
+
|
|
39
|
+
// ARRAY
|
|
40
|
+
if (rawField instanceof ZodArray) {
|
|
41
|
+
rawField = rawField.unwrap()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// OBJETO
|
|
45
|
+
if (rawField instanceof ZodObject) {
|
|
46
|
+
walk(rawField, nextPath);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// CAMPO SIMPLES
|
|
53
|
+
fields[currentFieldsKey].push(key);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
walk(schema, null);
|
|
58
|
+
|
|
59
|
+
const queryParts: string[] = [];
|
|
60
|
+
|
|
61
|
+
for (const resource in fields) {
|
|
62
|
+
if (fields[resource].length > 0) {
|
|
63
|
+
queryParts.push(
|
|
64
|
+
`${fieldsKey}[${resource}]=${fields[resource].join(",")}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (includes.size) {
|
|
70
|
+
queryParts.push(
|
|
71
|
+
`${includeKey}=${Array.from(includes).join(",")}`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return "?" + queryParts.join("&");
|
|
76
|
+
};
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
//* Libraries imports
|
|
2
|
-
import { it, expect } from "bun:test";
|
|
3
|
-
import z from "zod/v4";
|
|
4
|
-
import fastCartesian from "fast-cartesian";
|
|
5
|
-
import { PowerSet } from 'js-combinatorics';
|
|
6
|
-
//* Utils imports
|
|
7
|
-
import { schemaToQueryString } from "../src";
|
|
8
|
-
const partialObjects = [
|
|
9
|
-
[
|
|
10
|
-
{
|
|
11
|
-
rootOne: z.string(),
|
|
12
|
-
},
|
|
13
|
-
"fields[root]=rootOne"
|
|
14
|
-
],
|
|
15
|
-
[
|
|
16
|
-
{
|
|
17
|
-
rootTwo: z.string(),
|
|
18
|
-
},
|
|
19
|
-
"fields[root]=rootTwo"
|
|
20
|
-
],
|
|
21
|
-
[
|
|
22
|
-
{
|
|
23
|
-
rootObject: z.object({
|
|
24
|
-
subOne: z.number(),
|
|
25
|
-
}),
|
|
26
|
-
},
|
|
27
|
-
"fields[rootObject]=subOne&include=rootObject"
|
|
28
|
-
],
|
|
29
|
-
[
|
|
30
|
-
{
|
|
31
|
-
rootObject: z.object({
|
|
32
|
-
subTwo: z.number(),
|
|
33
|
-
}),
|
|
34
|
-
},
|
|
35
|
-
"fields[rootObject]=subTwo&include=rootObject"
|
|
36
|
-
],
|
|
37
|
-
[
|
|
38
|
-
{
|
|
39
|
-
rootNullableObject: z.object({
|
|
40
|
-
subOne: z.number(),
|
|
41
|
-
}).nullable(),
|
|
42
|
-
},
|
|
43
|
-
"fields[rootNullableObject]=subOne&include=rootNullableObject"
|
|
44
|
-
],
|
|
45
|
-
[
|
|
46
|
-
{
|
|
47
|
-
rootNullableObject: z.object({
|
|
48
|
-
subTwo: z.number(),
|
|
49
|
-
}).nullable(),
|
|
50
|
-
},
|
|
51
|
-
"fields[rootNullableObject]=subTwo&include=rootNullableObject"
|
|
52
|
-
],
|
|
53
|
-
[
|
|
54
|
-
{
|
|
55
|
-
rootArrayObject: z.object({
|
|
56
|
-
subOne: z.number(),
|
|
57
|
-
}).array(),
|
|
58
|
-
},
|
|
59
|
-
"fields[rootArrayObject]=subOne&include=rootArrayObject"
|
|
60
|
-
],
|
|
61
|
-
[
|
|
62
|
-
{
|
|
63
|
-
rootArrayObject: z.object({
|
|
64
|
-
subTwo: z.number(),
|
|
65
|
-
}).array(),
|
|
66
|
-
},
|
|
67
|
-
"fields[rootArrayObject]=subTwo&include=rootArrayObject"
|
|
68
|
-
],
|
|
69
|
-
];
|
|
70
|
-
function unwrapObject(schema) {
|
|
71
|
-
if (schema instanceof z.ZodObject) {
|
|
72
|
-
return schema;
|
|
73
|
-
}
|
|
74
|
-
if (schema instanceof z.ZodNullable) {
|
|
75
|
-
const inner = schema.unwrap();
|
|
76
|
-
return inner instanceof z.ZodObject ? inner : null;
|
|
77
|
-
}
|
|
78
|
-
if (schema instanceof z.ZodArray) {
|
|
79
|
-
const inner = schema.unwrap();
|
|
80
|
-
return inner instanceof z.ZodObject ? inner : null;
|
|
81
|
-
}
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
function isNullable(schema) {
|
|
85
|
-
return schema instanceof z.ZodNullable;
|
|
86
|
-
}
|
|
87
|
-
function deepExtendZod(base, extension) {
|
|
88
|
-
const newShape = { ...base.shape };
|
|
89
|
-
for (const key in extension) {
|
|
90
|
-
const baseField = newShape[key];
|
|
91
|
-
const extField = extension[key];
|
|
92
|
-
const baseObj = baseField ? unwrapObject(baseField) : null;
|
|
93
|
-
const extObj = unwrapObject(extField);
|
|
94
|
-
if (baseObj && extObj) {
|
|
95
|
-
const merged = deepExtendZod(baseObj, extObj.shape);
|
|
96
|
-
const shouldBeNullable = isNullable(baseField) || isNullable(extField);
|
|
97
|
-
newShape[key] = shouldBeNullable
|
|
98
|
-
? merged.nullable()
|
|
99
|
-
: merged;
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
newShape[key] = extField;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return z.object(newShape);
|
|
106
|
-
}
|
|
107
|
-
const schemaCombinations = [...new PowerSet(partialObjects)];
|
|
108
|
-
const schemaCases = schemaCombinations.map((objects) => {
|
|
109
|
-
let schema = z.object({});
|
|
110
|
-
let fields = {};
|
|
111
|
-
let includes = new Set([]);
|
|
112
|
-
let expectedQueryString = '';
|
|
113
|
-
objects.forEach(([object, queryString]) => {
|
|
114
|
-
//@ts-expect-error
|
|
115
|
-
schema = deepExtendZod(schema, object);
|
|
116
|
-
const params = queryString.split("&");
|
|
117
|
-
params.forEach((param) => {
|
|
118
|
-
const [key, values] = param.split("=");
|
|
119
|
-
if (key.startsWith("include")) {
|
|
120
|
-
values.split(',').map(value => includes.add(value));
|
|
121
|
-
}
|
|
122
|
-
if (key.startsWith("fields")) {
|
|
123
|
-
if (!(key in fields))
|
|
124
|
-
fields[key] = new Set([]);
|
|
125
|
-
values.split(',').map(value => fields[key].add(value));
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
let includeValues = Array.from(includes).join(',');
|
|
130
|
-
let includeQueryString = includeValues ? `include=${Array.from(includes).join(',')}` : '';
|
|
131
|
-
let filtersQueryString = Object.entries(fields).map(([key, values]) => `${key}=${Array.from(values).join(',')}`).join('&');
|
|
132
|
-
expectedQueryString = '?' + [filtersQueryString, includeQueryString].filter(Boolean).join('&');
|
|
133
|
-
return [schema, expectedQueryString];
|
|
134
|
-
});
|
|
135
|
-
const objectNameCases = fastCartesian([
|
|
136
|
-
// ["fields"],
|
|
137
|
-
["fields", "alternativeKeyForFields"],
|
|
138
|
-
// [undefined, "", "fields", "alternativeKeyForFields"],
|
|
139
|
-
// ["include"],
|
|
140
|
-
["include", "alternativeKeyForInclude"],
|
|
141
|
-
// [undefined, "", "include", "alternativeKeyForInclude"],
|
|
142
|
-
]);
|
|
143
|
-
const combinations = fastCartesian([
|
|
144
|
-
objectNameCases,
|
|
145
|
-
schemaCases
|
|
146
|
-
]);
|
|
147
|
-
const cases = combinations.map(([config, [schema, expectedQueryString]]) => {
|
|
148
|
-
const fieldKey = config[0] === "" ? "vazio" : config[0] === undefined ? "undefined" : config[0];
|
|
149
|
-
const includeKey = config[1] === "" ? "vazio" : config[1] === undefined ? "undefined" : config[1];
|
|
150
|
-
return [`${fieldKey},${includeKey}`, expectedQueryString, fieldKey, includeKey, schema];
|
|
151
|
-
});
|
|
152
|
-
it.each(cases)("when config is %s and should %s", (_, expected, fieldKey, includeKey, schema) => {
|
|
153
|
-
expected = expected.replace("include", includeKey);
|
|
154
|
-
expected = expected.replaceAll("fields", fieldKey);
|
|
155
|
-
const received = schemaToQueryString(schema, "root", includeKey, fieldKey);
|
|
156
|
-
// console.log("======================================================================================================")
|
|
157
|
-
// console.log(received, expected)
|
|
158
|
-
expect(received).toBe(expected);
|
|
159
|
-
});
|
|
File without changes
|
|
File without changes
|