Last active
November 17, 2020 14:06
-
-
Save gugadev/ff22246f449c8d51219cf2c430f93891 to your computer and use it in GitHub Desktop.
Fluid - An small utility to do basic querying on arrays of objects
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
interface User { | |
name: string; | |
age: number; | |
birthDate: Date; | |
admin: boolean; | |
foo?: { | |
bar: string; | |
} | |
} | |
const DateComparator = (a: Date, b: Date): boolean => { | |
console.log(a, b) | |
return ( | |
a.getFullYear() === b.getFullYear() && | |
a.getMonth() + 1 === b.getMonth() + 1 && | |
a.getDate() === b.getDate() | |
) | |
} | |
const data: User[] = [ | |
{ | |
name: "Kai Garzaki", | |
age: 17, | |
birthDate: new Date("2019-03-15"), | |
admin: false, | |
foo: { | |
bar: "baz" | |
} | |
}, | |
{ | |
name: "Koda Garzaki", | |
age: 44, | |
birthDate: new Date("2012-10-15"), | |
admin: false, | |
}, | |
{ | |
name: "Francisco Sagasti", | |
age: 23, | |
birthDate: new Date("1958-10-20"), | |
admin: true, | |
}, | |
{ | |
name: "Gus Garzaki", | |
age: 27, | |
birthDate: new Date("1993-09-23"), | |
admin: true, | |
}, | |
] | |
const foundByName = fluid<User>(data).where("name").eq<string>("Francisco Sagasti", { ignoreCase: true }).pick(); | |
const foundByBirthDate = fluid<User>(data).where("birthDate").eq<Date>(new Date("1993-09-23"), { comparator: DateComparator }).pick(); | |
const foundByNameStarts = fluid<User>(data).where("name").like("%k", { ignoreCase: true }).pick() | |
const foundByAdmin = fluid<User>(data).where("admin").eq<boolean>(false).pick(); | |
const foundAgeGt25 = fluid<User>(data).where("age").gt(25).pick() | |
const foundAgeLt25 = fluid<User>(data).where("age").lt(25).pick() | |
const countNoAdmins = fluid<User>(data).where("admin").eq<boolean>(false).count() | |
const count = fluid<User>(data).count() | |
const first = fluid<User>(data).first() | |
const last = fluid<User>(data).last() | |
const foundByInnerField = fluid<User>(data).where("foo.bar").eq<string>("baz", { ignoreCase: true }).pick() | |
console.clear() | |
assert.equal(foundByName.length, 1) | |
assert.equal(foundByBirthDate.length, 1) | |
assert.equal(foundByNameStarts.length, 2) | |
assert.equal(foundByAdmin.length, 2) | |
assert.equal(foundAgeGt25.length, 3) | |
assert.equal(foundAgeLt25.length, 1) | |
assert.equal(countNoAdmins, 2) | |
assert.equal(count, 4) | |
assert.deepEqual(first, data[0]) | |
assert.deepEqual(last, data[data.length - 1]) | |
assert.equal(foundByInnerField.length, 1) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
interface EqualityOptions<K> { | |
ignoreCase?: boolean | |
comparator?: (fieldValue: K, cmpValue: K) => boolean | |
} | |
interface FluidApi<T> { | |
eq: <K>(value: K, eqOptions?: EqualityOptions<K>) => FluidApi<T> | |
like: (value: string, options?: EqualityOptions<string>) => FluidApi<T> | |
first: () => T | undefined | |
last: () => T | undefined | |
take: (howMany: number) => T[] | |
at: (index: number) => T | undefined | |
lt: (value: number, inclusive?: boolean) => FluidApi<T> | |
gt: (value: number, inclusive?: boolean) => FluidApi<T> | |
pick: () => T[] | |
count: () => number | |
} | |
interface Fluid<T> { | |
where: (field: string) => FluidApi<T> | |
count: () => number | |
first: () => T | undefined | |
last: () => T | undefined | |
at: (index: number) => T | undefined | |
take: (howMany: number) => T[] | |
pick: () => T[] | |
} | |
interface Ops<T> { | |
field: string | |
result: T[] | |
} | |
function fluid<T>(source: T[]): Fluid<T> { | |
const api: FluidApi<T> = { | |
eq, | |
like, | |
pick, | |
first, | |
last, | |
take, | |
lt, | |
gt, | |
count, | |
at, | |
} | |
const ops: Ops<T> = { | |
field: "", | |
result: source.map(o => ({ ...o })) | |
} | |
/* UTILITARIES */ | |
/* thanks to Felix Kling - https://stackoverflow.com/a/6491615/10670707 */ | |
function getField<K>(obj: T, prop: string): K | undefined { | |
if (prop.includes(".")) { | |
const parts: string[] = prop.split('.'); | |
const last = parts.pop(); | |
const len = parts.length; | |
let i = 1; | |
let current = parts[0]; | |
while((obj = (obj as any)[current]) && i < len) { | |
current = parts[i]; | |
i++; | |
} | |
if(obj) { | |
return (obj as any)[last as unknown as string] as K; | |
} | |
return undefined | |
} | |
return (obj as any)[ops.field] as K | |
} | |
/* GET DATA FNS */ | |
function pick(): T[] { | |
return ops.result | |
} | |
function at(index: number): T | undefined { | |
return ops.result[index] | |
} | |
function count(): number { | |
return ops.result.length | |
} | |
function first(): T | undefined { | |
return ops.result[0] | |
} | |
function last(): T | undefined { | |
return ops.result[count() - 1] | |
} | |
function take(howMany: number): T[] { | |
const toReturn: T[] = [] | |
for (let i = 0; i < howMany; i++) { | |
toReturn.push(ops.result[i]) | |
} | |
return toReturn | |
} | |
/* QUERY FNS */ | |
function where(field: string): FluidApi<T> { | |
ops.field = field | |
return api | |
} | |
function eq<K>(value: K, eqOptions?: EqualityOptions<K>): FluidApi<T> { | |
const { ignoreCase, comparator } = eqOptions ?? {} | |
if (comparator) { | |
ops.result = source.filter(i => { | |
const fieldValue = getField<K>(i, ops.field) | |
if (fieldValue === undefined) { | |
return false | |
} | |
return comparator(fieldValue, value); | |
}) | |
} else { | |
ops.result = source.filter(i => { | |
const fieldValue = getField<K>(i, ops.field) | |
if (fieldValue === undefined) { | |
return false | |
} | |
if (ignoreCase) { | |
return (fieldValue as unknown as string ?? "").toLowerCase() === (value as unknown as string).toLowerCase() | |
} | |
return fieldValue === value | |
}) | |
} | |
return api | |
} | |
function gt(value: number, inclusive?: boolean): FluidApi<T> { | |
ops.result = source.filter(i => { | |
const fieldValue = getField<number>(i, ops.field) | |
if (fieldValue === undefined) { | |
return false | |
} | |
return inclusive ? fieldValue >= value : fieldValue > value | |
}) | |
return api | |
} | |
function lt(value: number, inclusive?: boolean): FluidApi<T> { | |
ops.result = source.filter(i => { | |
const fieldValue = getField<number>(i, ops.field) | |
if (fieldValue === undefined) { | |
return false | |
} | |
return inclusive ? fieldValue <= value : fieldValue < value | |
}) | |
return api | |
} | |
function like(value: string, options?: EqualityOptions<string>): FluidApi<T> { | |
ops.result = source.filter(i => { | |
const fieldValue = getField<string>(i, ops.field) | |
if (fieldValue === undefined) { | |
return false | |
} | |
if (value.startsWith('%')) { | |
const searchTerm = value.substring(1) | |
if (options?.ignoreCase) { | |
return fieldValue.toLocaleLowerCase().startsWith(searchTerm.toLocaleLowerCase()) | |
} | |
return fieldValue.startsWith(searchTerm) | |
} | |
if (value.endsWith("%")) { | |
const searchTerm = value.substring(0, value.length - 1) | |
if (options?.ignoreCase) { | |
return fieldValue.toLocaleLowerCase().endsWith(searchTerm.toLocaleLowerCase()) | |
} | |
return fieldValue.endsWith(searchTerm) | |
} | |
if (value.startsWith("%") && value.endsWith("%")) { | |
const searchTerm = value.substring(1, value.length - 1) | |
if (options?.ignoreCase) { | |
return fieldValue.toLocaleLowerCase().includes(searchTerm.toLocaleLowerCase()) | |
} | |
return fieldValue.includes(searchTerm) | |
} | |
return false | |
}) | |
return api | |
} | |
return { | |
where, | |
count, | |
pick, | |
take, | |
first, | |
last, | |
at | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment