Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Try it out on the online playground: <https://jsonquerylang.org>

## Features

- Small: just `3.7 kB` when minified and gzipped! The JSON query engine without parse/stringify is only `1.7 kB`.
- Small: just `3.9 kB` when minified and gzipped! The JSON query engine without parse/stringify is only `1.9 kB`.
- Feature rich (50+ powerful functions and operators)
- Easy to interoperate with thanks to the intermediate JSON format.
- Expressive
Expand Down
10 changes: 0 additions & 10 deletions src/compile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,16 +109,6 @@ describe('error handling', () => {
])
})

test('should do nothing when sorting objects without a getter', () => {
const data = [{ a: 1 }, { c: 3 }, { b: 2 }]
expect(go(data, ['sort'])).toEqual(data)
})

test('should not crash when sorting a list with nested arrays', () => {
expect(go([[3], [7], [4]], ['sort'])).toEqual([[3], [4], [7]])
expect(go([[], [], []], ['sort'])).toEqual([[], [], []])
})

test('should throw an error when calculating the sum of an empty array', () => {
expect(() => go([], ['sum'])).toThrow('Reduce of empty array with no initial value')
})
Expand Down
55 changes: 42 additions & 13 deletions src/functions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { compile } from './compile'
import { isArray } from './is'
import { isArray, isEqual } from './is'
import type {
Entry,
FunctionBuilder,
Expand Down Expand Up @@ -27,6 +27,20 @@ export function buildFunction(fn: (...args: unknown[]) => unknown): FunctionBuil
}
}

const gt = (a: unknown, b: unknown) => {
if (
(typeof a === 'number' && typeof b === 'number') ||
(typeof a === 'string' && typeof b === 'string')
) {
return a > b
}

throw new TypeError('Two numbers or two strings expected')
}
const gte = (a: unknown, b: unknown) => isEqual(a, b) || gt(a, b)
const lt = (a: unknown, b: unknown) => gt(b, a)
const lte = (a: unknown, b: unknown) => gte(b, a)

export const functions: FunctionBuildersMap = {
pipe: (...entries: JSONQuery[]) => {
const _entries = entries.map((entry) => compile(entry))
Expand Down Expand Up @@ -130,7 +144,7 @@ export const functions: FunctionBuildersMap = {
function compare(itemA: unknown, itemB: unknown) {
const a = getter(itemA)
const b = getter(itemB)
return a > b ? sign : a < b ? -sign : 0
return gt(a, b) ? sign : lt(a, b) ? -sign : 0
}

return (data: T[]) => data.slice().sort(compare)
Expand Down Expand Up @@ -216,7 +230,17 @@ export const functions: FunctionBuildersMap = {

uniq:
() =>
<T>(data: T[]) => [...new Set(data)],
<T>(data: T[]) => {
const res: T[] = []

for (const item of data) {
if (!res.find((resItem) => isEqual(resItem, item))) {
res.push(item)
}
}

return res
},

uniqBy:
<T>(path: JSONQueryProperty) =>
Expand Down Expand Up @@ -268,11 +292,16 @@ export const functions: FunctionBuildersMap = {

return (data: unknown) => (truthy(_condition(data)) ? _valueIfTrue(data) : _valueIfFalse(data))
},
in: (path: string, values: JSONQuery) => {
const getter = compile(path)
const _values = compile(values)
in: (value: JSONQuery, values: JSONQuery) => {
const getValue = compile(value)
const getValues = compile(values)

return (data: unknown) => (_values(data) as string[]).includes(getter(data) as string)
return (data: unknown) => {
const _value = getValue(data)
const _values = getValues(data) as unknown[]

return !!_values.find((item) => isEqual(item, _value))
}
},
'not in': (path: string, values: JSONQuery) => {
const _in = functions.in(path, values)
Expand All @@ -286,12 +315,12 @@ export const functions: FunctionBuildersMap = {
return (data: unknown) => regex.test(getter(data) as string)
},

eq: buildFunction((a, b) => a === b),
gt: buildFunction((a, b) => a > b),
gte: buildFunction((a, b) => a >= b),
lt: buildFunction((a, b) => a < b),
lte: buildFunction((a, b) => a <= b),
ne: buildFunction((a, b) => a !== b),
eq: buildFunction(isEqual),
gt: buildFunction(gt),
gte: buildFunction(gte),
lt: buildFunction(lt),
lte: buildFunction(lte),
ne: buildFunction((a, b) => !isEqual(a, b)),

add: buildFunction((a: number, b: number) => a + b),
subtract: buildFunction((a: number, b: number) => a - b),
Expand Down
15 changes: 15 additions & 0 deletions src/is.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,18 @@ export const isObject = (value: unknown): value is object =>
value && typeof value === 'object' && !isArray(value)

export const isString = (value: unknown): value is string => typeof value === 'string'

// source: https://stackoverflow.com/a/77278013/1262753
export const isEqual = <T>(a: T, b: T): boolean => {
if (a === b) {
return true
}

const bothObject = a && b && typeof a === 'object' && typeof b === 'object'

return (
bothObject &&
Object.keys(a).length === Object.keys(b).length &&
Object.entries(a).every(([k, v]) => isEqual(v, b[k as keyof T]))
)
}
Loading