@flatfile/listener
Advanced tools
Comparing version
# @flatfile/listener | ||
## 0.1.0 | ||
### Minor Changes | ||
- 4d88069: Adds a major improvement to event filtering, allowing you to grep the key path and defaulting to matching any key in the object | ||
## 0.0.10 | ||
@@ -4,0 +10,0 @@ |
@@ -518,2 +518,3 @@ var __create = Object.create; | ||
// src/events/glob.match.ts | ||
import flat from "flat"; | ||
function glob(val, filter) { | ||
@@ -525,21 +526,37 @@ if (!val || typeof val !== "string") { | ||
} | ||
function objectMatches(object, filter) { | ||
function objectMatches(object, filterObject) { | ||
const cleanFilter = !filterObject || typeof filterObject !== "object" ? { "**": filterObject } : filterObject; | ||
if (typeof object !== "object") { | ||
throw new Error("You cannot filter a non-object"); | ||
} | ||
let denied = false; | ||
for (const x in filter) { | ||
const prop = object[x]; | ||
if (!object?.[x]) { | ||
return false; | ||
} | ||
if (typeof prop === "string" || Array.isArray(prop) && typeof prop[0] === "string") { | ||
if (Array.isArray(prop)) { | ||
denied || (denied = !prop.some((p) => { | ||
return glob(p, filter[x]); | ||
})); | ||
} else { | ||
denied || (denied = !glob(prop, filter[x])); | ||
} | ||
} | ||
const filter = flat(cleanFilter, { safe: true }); | ||
const flattened = flat(object, { safe: true }); | ||
for (const keyPattern in filter) { | ||
const keys = filterKeys(flattened, keyPattern); | ||
const valuePattern = Array.isArray(filter[keyPattern]) ? filter[keyPattern] : [filter[keyPattern]]; | ||
denied || (denied = !keys.some((key) => { | ||
const value = flattened[key]; | ||
return valuePattern.some((match) => globOrMatch(value, match)); | ||
})); | ||
} | ||
return !denied; | ||
} | ||
function filterKeys(object, glob2) { | ||
glob2 = glob2.includes("*") || glob2.includes(".") ? glob2 : `**.${glob2}`; | ||
const matcher = index_es_default(glob2, "."); | ||
return Object.keys(object).filter((key) => matcher(key)); | ||
} | ||
function globOrMatch(val, filter) { | ||
if (val === void 0 || val === null) { | ||
return filter === null; | ||
} | ||
if (Array.isArray(val)) { | ||
return val.some((v) => globOrMatch(v, filter)); | ||
} | ||
if (typeof filter === "string") { | ||
return glob(val.toString(), filter); | ||
} | ||
return val === filter; | ||
} | ||
@@ -546,0 +563,0 @@ // src/events/event.handler.ts |
{ | ||
"name": "@flatfile/listener", | ||
"version": "0.0.10", | ||
"version": "0.1.0", | ||
"description": "A PubSub Listener for configuring and using Flatfile", | ||
@@ -20,2 +20,3 @@ "main": "dist/index.js", | ||
"@flatfile/ts-config-platform-sdk": "*", | ||
"@types/flat": "^5.0.2", | ||
"@types/jest": "^29.5.1", | ||
@@ -31,4 +32,5 @@ "eslint": "^8.19.0", | ||
"@flatfile/api": "^0.0.19", | ||
"flat": "^5.0.2", | ||
"node-fetch": "^2.6.7" | ||
} | ||
} |
import { objectMatches } from './glob.match' | ||
describe('objectMatches', () => { | ||
test('matches a primitive anywhere in the object', () => { | ||
expect(objectMatches({ foo: 'bar' }, 'bar')).toBe(true) | ||
expect(objectMatches({ foo: 11 }, 11)).toBe(true) | ||
expect(objectMatches({ foo: 11 }, 13)).toBe(false) | ||
expect(objectMatches({ foo: 11, bar: 13 }, 13)).toBe(true) | ||
expect(objectMatches({ foo: { bar: 'blue' } }, 'blue')).toBe(true) | ||
expect(objectMatches({ foo: ['bar', 'baz'] }, 'baz')).toBe(true) | ||
expect(objectMatches({ foo: ['bar', 'baz'] }, 'qux')).toBe(false) | ||
}) | ||
test('matches an object', () => { | ||
@@ -77,2 +87,146 @@ expect(objectMatches({ foo: 'bar' }, { foo: 'bar' })).toBe(true) | ||
}) | ||
test('allows user to provide a nested match object as well', () => { | ||
expect( | ||
objectMatches({ one: { two: 'bar' } }, { one: { two: 'bar' } }) | ||
).toBe(true) | ||
}) | ||
test('matches a nested key', () => { | ||
expect(objectMatches({ one: { two: 'bar' } }, { 'one.two': 'bar' })).toBe( | ||
true | ||
) | ||
}) | ||
test('does not use end with logic when a nested pattern is provided', () => { | ||
expect( | ||
objectMatches({ zero: { one: { two: 'bar' } } }, { 'one.two': 'bar' }) | ||
).toBe(false) | ||
}) | ||
test('does not use end with logic when a glob pattern is provided', () => { | ||
expect( | ||
objectMatches({ zero: { one: { two: 'bar' } } }, { '*two': 'bar' }) | ||
).toBe(false) | ||
}) | ||
test('matches a glob pattern', () => { | ||
expect(objectMatches({ one: { two: 'bar' } }, { '*.two': 'bar' })).toBe( | ||
true | ||
) | ||
}) | ||
test('defaults to an `endsWith` pattern', () => { | ||
expect(objectMatches({ one: { two: 'bar' } }, { two: 'bar' })).toBe(true) | ||
}) | ||
test('false if non-existent key', () => { | ||
expect(objectMatches({ one: { two: 'bar' } }, { three: 'bar' })).toBe(false) | ||
}) | ||
test('matches a deep glob pattern', () => { | ||
expect( | ||
objectMatches({ one: { two: { three: 'bar' } } }, { '**.three': 'bar' }) | ||
).toBe(true) | ||
expect( | ||
objectMatches({ one: { two: { three: 'bar' } } }, { '*.three': 'bar' }) | ||
).toBe(false) | ||
}) | ||
describe('additional edge cases', () => { | ||
// Empty Objects | ||
test('handles empty objects', () => { | ||
expect(objectMatches({}, {})).toBe(true) | ||
expect(objectMatches({ foo: 'bar' }, {})).toBe(true) | ||
expect(objectMatches({}, { foo: 'bar' })).toBe(false) | ||
}) | ||
// Non-Object Inputs | ||
test('handles non-object inputs', () => { | ||
// @ts-ignore | ||
expect(() => objectMatches('foo', { foo: 'bar' })).toThrow() | ||
// @ts-ignore | ||
expect(() => objectMatches(123, { foo: 'bar' })).toThrow() | ||
// @ts-ignore | ||
expect(() => objectMatches(null, { foo: 'bar' })).toThrow() | ||
}) | ||
// Special Characters in Keys | ||
test('handles special characters in keys', () => { | ||
expect(objectMatches({ 'foo*': 'bar' }, { 'foo*': 'bar' })).toBe(true) | ||
expect(objectMatches({ 'foo.bar': 'baz' }, { 'foo.bar': 'baz' })).toBe( | ||
true | ||
) | ||
expect(objectMatches({ foo$bar: 'baz' }, { foo$bar: 'baz' })).toBe(true) | ||
}) | ||
// Matching Null Values | ||
test('matches null values', () => { | ||
expect(objectMatches({ foo: null }, { foo: null })).toBe(true) | ||
expect(objectMatches({ foo: 'bar' }, { foo: null })).toBe(false) | ||
}) | ||
// Matching Undefined Values | ||
test('matches undefined values', () => { | ||
expect(objectMatches({ foo: undefined }, { foo: null })).toBe(true) | ||
// @ts-ignore | ||
expect(objectMatches({ foo: 'bar' }, { foo: undefined })).toBe(false) | ||
}) | ||
// Nested Array Matches | ||
// test('handles nested array matches', () => { | ||
// expect( | ||
// objectMatches({ foo: [{ bar: 'baz' }] }, { foo: [{ bar: 'baz' }] }) | ||
// ).toBe(true) | ||
// expect( | ||
// objectMatches({ foo: [{ bar: 'qux' }] }, { foo: [{ bar: 'baz' }] }) | ||
// ).toBe(false) | ||
// }) | ||
// Nested Wildcard Matches | ||
test('handles nested wildcard matches', () => { | ||
expect( | ||
objectMatches({ foo: { bar: 'baz' } }, { foo: { bar: '*' } }) | ||
).toBe(true) | ||
expect( | ||
objectMatches({ foo: { bar: 'qux' } }, { foo: { bar: 'baz*' } }) | ||
).toBe(false) | ||
}) | ||
// Nested Glob Pattern Matches | ||
test('handles nested glob pattern matches', () => { | ||
expect(objectMatches({ foo: { bar: 'baz' } }, { 'foo.*': 'baz' })).toBe( | ||
true | ||
) | ||
expect(objectMatches({ foo: { bar: 'qux' } }, { 'foo.*': 'baz' })).toBe( | ||
false | ||
) | ||
}) | ||
// Type Coercion | ||
test('it attempts to perform type coercion when a string match is provided', () => { | ||
expect(objectMatches({ foo: 1 }, { foo: '1' })).toBe(true) | ||
}) | ||
// Type Coercion | ||
test('it will not coerce types if a non-string matcher is provided', () => { | ||
expect(objectMatches({ foo: '1' }, { foo: 1 })).toBe(false) | ||
}) | ||
// Case Sensitivity | ||
test('is case sensitive', () => { | ||
expect(objectMatches({ foo: 'Bar' }, { foo: 'bar' })).toBe(false) | ||
expect(objectMatches({ foo: 'bar' }, { foo: 'Bar' })).toBe(false) | ||
}) | ||
// Order of Keys | ||
test('does not care about order of keys', () => { | ||
expect( | ||
objectMatches({ foo: 'bar', baz: 'qux' }, { baz: 'qux', foo: 'bar' }) | ||
).toBe(true) | ||
}) | ||
}) | ||
}) |
import { Arrayable } from './event.handler' | ||
import wildMatch from 'wildcard-match' | ||
import flat from 'flat' | ||
@@ -21,30 +22,80 @@ /** | ||
* @param object | ||
* @param filter | ||
* @param filterObject | ||
*/ | ||
export function objectMatches( | ||
object: Record<string, any>, | ||
filter: Record<string, Arrayable<string>> | ||
filterObject: JSONPrimitive | FilterObj | ||
): boolean { | ||
const cleanFilter: FilterObj = | ||
!filterObject || typeof filterObject !== 'object' | ||
? { '**': filterObject } | ||
: filterObject | ||
if (typeof object !== 'object') { | ||
throw new Error('You cannot filter a non-object') | ||
} | ||
let denied = false | ||
for (const x in filter) { | ||
const prop: Arrayable<string> = object[x] | ||
const filter: FilterObj = flat(cleanFilter, { safe: true }) | ||
const flattened = flat(object, { safe: true }) as Record< | ||
string, | ||
JSONPrimitive | ||
> | ||
if (!object?.[x]) { | ||
return false | ||
} | ||
// all filters MUST resolve true | ||
for (const keyPattern in filter) { | ||
const keys = filterKeys(flattened, keyPattern) | ||
if ( | ||
typeof prop === 'string' || | ||
(Array.isArray(prop) && typeof prop[0] === 'string') | ||
) { | ||
if (Array.isArray(prop)) { | ||
denied ||= !prop.some((p) => { | ||
return glob(p, filter[x]) | ||
}) | ||
} else { | ||
denied ||= !glob(prop, filter[x]) | ||
} | ||
} | ||
const valuePattern = ( | ||
Array.isArray(filter[keyPattern]) | ||
? filter[keyPattern] | ||
: [filter[keyPattern]] | ||
) as JSONPrimitive[] | ||
// only one filter must match | ||
denied ||= !keys.some((key) => { | ||
const value: JSONPrimitive = flattened[key] | ||
return valuePattern.some((match) => globOrMatch(value, match)) | ||
}) | ||
} | ||
return !denied | ||
} | ||
/** | ||
* Glob keys of an object and return the narrowed set | ||
* | ||
* @param object | ||
* @param glob | ||
*/ | ||
function filterKeys<T extends Record<string, any>>( | ||
object: Record<string, any>, | ||
glob: string | ||
): Array<keyof T> { | ||
glob = glob.includes('*') || glob.includes('.') ? glob : `**.${glob}` | ||
const matcher = wildMatch(glob, '.') | ||
return Object.keys(object).filter((key) => matcher(key)) | ||
} | ||
function globOrMatch( | ||
val: Arrayable<JSONPrimitive>, | ||
filter: JSONPrimitive | ||
): boolean { | ||
if (val === undefined || val === null) { | ||
return filter === null | ||
} | ||
if (Array.isArray(val)) { | ||
return val.some((v) => globOrMatch(v, filter)) | ||
} | ||
if (typeof filter === 'string') { | ||
return glob(val.toString(), filter) | ||
} | ||
// otherwise do a simple comparison | ||
return val === filter | ||
} | ||
type JSONPrimitive = string | number | boolean | null | ||
type FilterObj = Record< | ||
string, | ||
Arrayable<JSONPrimitive> | Record<string, Arrayable<JSONPrimitive>> | ||
> |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
99982
9.11%3112
7.16%1
-50%3
50%11
10%+ Added
+ Added