Skip to content

Instantly share code, notes, and snippets.

@gugadev
Last active November 17, 2020 14:06
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gugadev/ff22246f449c8d51219cf2c430f93891 to your computer and use it in GitHub Desktop.
Save gugadev/ff22246f449c8d51219cf2c430f93891 to your computer and use it in GitHub Desktop.
Fluid - An small utility to do basic querying on arrays of objects
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)
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