Skip to content

Commit 09c508a

Browse files
Vojtěch Václav Portešvojtechportes
authored andcommitted
feat: Added parsers and formatters
- Added support for SQL, Mongo, AQL, JSONata, JsonLogic, CEL, Elasticsearch, SpEL, Prisma, OData, RSQL, Dynamo and Django - Updated Readme - Updated package.json
1 parent f530baa commit 09c508a

166 files changed

Lines changed: 14100 additions & 9 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 442 additions & 0 deletions
Large diffs are not rendered by default.

example/src/main.tsx

Lines changed: 203 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
colors,
88
type DenormalizedQuery,
99
} from '../../src';
10+
import { formatQuery } from '../../src/formatQuery';
1011
import { Brand } from './components/brand';
1112
import { Checkbox } from './components/checkbox';
1213
import { Sidepanel } from './components/sidepanel';
@@ -61,6 +62,32 @@ const Code = styled.pre`
6162
overflow: auto;
6263
`;
6364

65+
const OutputTabs = styled.div`
66+
display: inline-flex;
67+
gap: 0.25rem;
68+
padding: 0.25rem;
69+
background: ${colors.grey['100']};
70+
border: 1px solid ${colors.grey['300']};
71+
border-radius: 0.5rem;
72+
`;
73+
74+
const OutputTab = styled.button<{ $active: boolean }>`
75+
padding: 0.45rem 0.75rem;
76+
font-size: 0.8rem;
77+
color: ${({ $active }) =>
78+
$active ? colors.white : colors.grey['700']};
79+
background: ${({ $active }) =>
80+
$active ? colors.primary.default : 'transparent'};
81+
border: 0;
82+
border-radius: 0.35rem;
83+
cursor: pointer;
84+
85+
&:hover {
86+
background: ${({ $active }) =>
87+
$active ? colors.primary.dark : colors.grey['200']};
88+
}
89+
`;
90+
6491
const initialQueryTree: DenormalizedQuery = [
6592
{
6693
type: 'GROUP',
@@ -254,13 +281,74 @@ const fields: IBuilderFieldProps[] = [
254281
const App: React.FC = () => {
255282
const [output, setOutput] =
256283
React.useState<DenormalizedQuery>(initialQueryTree);
284+
const [outputFormat, setOutputFormat] = React.useState<
285+
| 'native'
286+
| 'sql'
287+
| 'mongo'
288+
| 'aql'
289+
| 'jsonata'
290+
| 'jsonlogic'
291+
| 'cel'
292+
| 'elasticsearch'
293+
| 'spel'
294+
| 'prisma'
295+
| 'odata'
296+
| 'rsql'
297+
| 'dynamo'
298+
| 'django'
299+
>('native');
257300
const [readOnly, setReadOnly] = React.useState(false);
258301
const [draggable, setDraggable] = React.useState(false);
259302
const [singleRootGroup, setSingleRootGroup] = React.useState(true);
260303
const [showValidationErrors, setShowValidationErrors] = React.useState(true);
261304
const [theme, setTheme] = React.useState<IThemeProviderProps>({
262305
colors,
263306
});
307+
const nativeOutput = JSON.stringify(output, null, 2);
308+
const sqlOutput = formatQuery(output, 'SQL', {
309+
fields,
310+
wrapWhereClause: true,
311+
});
312+
const mongoOutput = formatQuery(output, 'Mongo', {
313+
fields,
314+
});
315+
const aqlOutput = formatQuery(output, 'AQL', {
316+
fields,
317+
variableName: 'doc',
318+
});
319+
const jsonataOutput = formatQuery(output, 'JSONata', {
320+
fields,
321+
});
322+
const jsonLogicOutput = formatQuery(output, 'JsonLogic', {
323+
fields,
324+
});
325+
const celOutput = formatQuery(output, 'CEL', {
326+
fields,
327+
});
328+
const elasticsearchOutput = formatQuery(output, 'Elasticsearch', {
329+
fields,
330+
wrapQueryClause: true,
331+
});
332+
const spelOutput = formatQuery(output, 'SpEL', {
333+
fields,
334+
});
335+
const prismaOutput = formatQuery(output, 'Prisma', {
336+
fields,
337+
wrapWhereClause: true,
338+
});
339+
const odataOutput = formatQuery(output, 'OData', {
340+
fields,
341+
wrapFilterClause: true,
342+
});
343+
const rsqlOutput = formatQuery(output, 'RSQL', {
344+
fields,
345+
});
346+
const dynamoOutput = formatQuery(output, 'Dynamo', {
347+
fields,
348+
});
349+
const djangoOutput = formatQuery(output, 'Django', {
350+
fields,
351+
});
264352

265353
return (
266354
<Page>
@@ -338,7 +426,121 @@ const App: React.FC = () => {
338426
</ThemeProvider>
339427

340428
<h3>Output</h3>
341-
<Code>{JSON.stringify(output, null, 2)}</Code>
429+
<OutputTabs>
430+
<OutputTab
431+
$active={outputFormat === 'native'}
432+
onClick={() => setOutputFormat('native')}
433+
>
434+
Native
435+
</OutputTab>
436+
<OutputTab
437+
$active={outputFormat === 'sql'}
438+
onClick={() => setOutputFormat('sql')}
439+
>
440+
SQL
441+
</OutputTab>
442+
<OutputTab
443+
$active={outputFormat === 'mongo'}
444+
onClick={() => setOutputFormat('mongo')}
445+
>
446+
Mongo
447+
</OutputTab>
448+
<OutputTab
449+
$active={outputFormat === 'aql'}
450+
onClick={() => setOutputFormat('aql')}
451+
>
452+
AQL
453+
</OutputTab>
454+
<OutputTab
455+
$active={outputFormat === 'jsonata'}
456+
onClick={() => setOutputFormat('jsonata')}
457+
>
458+
JSONata
459+
</OutputTab>
460+
<OutputTab
461+
$active={outputFormat === 'jsonlogic'}
462+
onClick={() => setOutputFormat('jsonlogic')}
463+
>
464+
JsonLogic
465+
</OutputTab>
466+
<OutputTab
467+
$active={outputFormat === 'cel'}
468+
onClick={() => setOutputFormat('cel')}
469+
>
470+
CEL
471+
</OutputTab>
472+
<OutputTab
473+
$active={outputFormat === 'elasticsearch'}
474+
onClick={() => setOutputFormat('elasticsearch')}
475+
>
476+
Elasticsearch
477+
</OutputTab>
478+
<OutputTab
479+
$active={outputFormat === 'spel'}
480+
onClick={() => setOutputFormat('spel')}
481+
>
482+
SpEL
483+
</OutputTab>
484+
<OutputTab
485+
$active={outputFormat === 'prisma'}
486+
onClick={() => setOutputFormat('prisma')}
487+
>
488+
Prisma
489+
</OutputTab>
490+
<OutputTab
491+
$active={outputFormat === 'odata'}
492+
onClick={() => setOutputFormat('odata')}
493+
>
494+
OData
495+
</OutputTab>
496+
<OutputTab
497+
$active={outputFormat === 'rsql'}
498+
onClick={() => setOutputFormat('rsql')}
499+
>
500+
RSQL
501+
</OutputTab>
502+
<OutputTab
503+
$active={outputFormat === 'dynamo'}
504+
onClick={() => setOutputFormat('dynamo')}
505+
>
506+
Dynamo
507+
</OutputTab>
508+
<OutputTab
509+
$active={outputFormat === 'django'}
510+
onClick={() => setOutputFormat('django')}
511+
>
512+
Django
513+
</OutputTab>
514+
</OutputTabs>
515+
<Code>
516+
{outputFormat === 'native'
517+
? nativeOutput
518+
: outputFormat === 'sql'
519+
? sqlOutput
520+
: outputFormat === 'mongo'
521+
? mongoOutput
522+
: outputFormat === 'aql'
523+
? aqlOutput
524+
: outputFormat === 'jsonata'
525+
? jsonataOutput
526+
: outputFormat === 'jsonlogic'
527+
? jsonLogicOutput
528+
: outputFormat === 'cel'
529+
? celOutput
530+
: outputFormat === 'elasticsearch'
531+
? elasticsearchOutput
532+
: outputFormat === 'spel'
533+
? spelOutput
534+
: outputFormat === 'prisma'
535+
? prismaOutput
536+
: outputFormat === 'odata'
537+
? odataOutput
538+
: outputFormat === 'rsql'
539+
? rsqlOutput
540+
: outputFormat === 'dynamo'
541+
? dynamoOutput
542+
: djangoOutput}
543+
</Code>
342544
</Main>
343545
</Layout>
344546
</Page>

package.json

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,21 @@
1111
"query-builder",
1212
"react-query-builder",
1313
"typescript",
14-
"configurable"
14+
"configurable",
15+
"query parser",
16+
"query formatter",
17+
"sql",
18+
"mongodb",
19+
"aql",
20+
"jsonata",
21+
"jsonlogic",
22+
"cel",
23+
"elasticsearch",
24+
"prisma",
25+
"odata",
26+
"rsql",
27+
"dynamodb",
28+
"django"
1529
],
1630
"author": {
1731
"email": "vojtech.v.portes@gmail.com",
@@ -35,6 +49,16 @@
3549
"types": "./dist/index.d.ts",
3650
"import": "./dist/index.mjs",
3751
"require": "./dist/index.cjs"
52+
},
53+
"./parseQuery": {
54+
"types": "./dist/parseQuery.d.ts",
55+
"import": "./dist/parseQuery.mjs",
56+
"require": "./dist/parseQuery.cjs"
57+
},
58+
"./formatQuery": {
59+
"types": "./dist/formatQuery.d.ts",
60+
"import": "./dist/formatQuery.mjs",
61+
"require": "./dist/formatQuery.cjs"
3862
}
3963
},
4064
"publishConfig": {

scripts/copy-dts.mjs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
11
/* global console */
22

3-
import { copyFile, access } from 'node:fs/promises';
3+
import { copyFile, readdir } from 'node:fs/promises';
44
import path from 'node:path';
55
import { fileURLToPath } from 'node:url';
66

77
const __filename = fileURLToPath(import.meta.url);
88
const __dirname = path.dirname(__filename);
99
const distDir = path.resolve(__dirname, '..', 'dist');
10-
const sourcePath = path.join(distDir, 'index.d.mts');
11-
const targetPath = path.join(distDir, 'index.d.ts');
1210

1311
try {
14-
await access(sourcePath);
15-
await copyFile(sourcePath, targetPath);
12+
const files = await readdir(distDir);
13+
const declarationFiles = files.filter(fileName => fileName.endsWith('.d.mts'));
14+
15+
await Promise.all(
16+
declarationFiles.map(fileName =>
17+
copyFile(
18+
path.join(distDir, fileName),
19+
path.join(distDir, fileName.replace(/\.d\.mts$/, '.d.ts'))
20+
)
21+
)
22+
);
1623
} catch (error) {
17-
console.error('Unable to create dist/index.d.ts from dist/index.d.mts');
24+
console.error('Unable to create .d.ts files from .d.mts build artifacts');
1825
throw error;
1926
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { DenormalizedQuery } from '../utils/query-tree';
2+
import { formatQuery } from './index';
3+
4+
describe('formatQuery AQL', () => {
5+
it('formats root-level rules with FILTER wrapping', () => {
6+
const query: DenormalizedQuery = [
7+
{ field: 'price', operator: 'LARGER', value: 10 },
8+
{ field: 'active', operator: 'EQUAL', value: true },
9+
];
10+
11+
expect(
12+
formatQuery(query, 'AQL', {
13+
rootlessCombinator: 'OR',
14+
variableName: 'item',
15+
})
16+
).toEqual('FILTER (item.price > 10 OR item.active == true)');
17+
});
18+
19+
it('formats groups without modifiers using the configured combinator', () => {
20+
const query: DenormalizedQuery = [
21+
{
22+
type: 'GROUP',
23+
children: [
24+
{ field: 'first_name', operator: 'EQUAL', value: 'Alice' },
25+
{ field: 'last_name', operator: 'EQUAL', value: 'Smith' },
26+
],
27+
},
28+
];
29+
30+
expect(
31+
formatQuery(query, 'AQL', {
32+
modifierlessGroupCombinator: 'OR',
33+
wrapFilterClause: false,
34+
})
35+
).toEqual('(doc.first_name == "Alice" OR doc.last_name == "Smith")');
36+
});
37+
});
38+
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { DenormalizedQuery } from '../utils/query-tree';
2+
import { formatQuery } from './index';
3+
4+
describe('formatQuery CEL', () => {
5+
it('formats root-level rules with the configured combinator', () => {
6+
const query: DenormalizedQuery = [
7+
{ field: 'price', operator: 'LARGER', value: 10 },
8+
{ field: 'active', operator: 'EQUAL', value: true },
9+
];
10+
11+
expect(formatQuery(query, 'CEL', { rootlessCombinator: 'OR' })).toEqual(
12+
'(price > 10 || active == true)'
13+
);
14+
});
15+
16+
it('formats string, range, and membership operators', () => {
17+
const query: DenormalizedQuery = [
18+
{
19+
type: 'GROUP',
20+
value: 'AND',
21+
isNegated: false,
22+
children: [
23+
{ field: 'name', operator: 'STARTS_WITH', value: 'Stev' },
24+
{ field: 'age', operator: 'BETWEEN', value: [18, 30] },
25+
{ field: 'segments', operator: 'ALL_IN', value: ['b2b', 'priority'] },
26+
],
27+
},
28+
];
29+
30+
expect(formatQuery(query, 'CEL')).toEqual(
31+
'(name.startsWith("Stev") && (age >= 18 && age <= 30) && ["b2b", "priority"].all(item, item in segments))'
32+
);
33+
});
34+
});

0 commit comments

Comments
 (0)