@flatfile/listener
Advanced tools
Comparing version 0.0.10 to 0.1.0
# @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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
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
3112
2
3
11
+ Addedflat@^5.0.2
+ Addedflat@5.0.2(transitive)