Skip to content

Commit

Permalink
Support includePath and excludePath on project transformer
Browse files Browse the repository at this point in the history
  • Loading branch information
kjellmorten committed Jan 17, 2024
1 parent d2f61e5 commit 8930007
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 17 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1855,6 +1855,12 @@ props in `exclude`. Both `include` and `exclude` may be array of strings, and
they should not be used in combination. If both are provided, `include` will be
used.

You may also specify an `includePath` or `excludePath`. These are dot notation
paths to arrays of strings, and will be used instead of `include` or `exclude`.
If `include` or `exclude` are also provided, they will be used as default
values when the corresponding path yields no value. Note that "no value" here
means `undefined`, and we don't support custom nonvalues here yet.

When given an array of object, each object will be projected. When given
anything that is not an object, undefined will be returned.

Expand Down
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"devDependencies": {
"@integreat/ts-dev-setup": "^5.0.4",
"@types/deep-freeze": "^0.1.5",
"@types/sinon": "^17.0.2",
"@types/sinon": "^17.0.3",
"deep-freeze": "0.0.1",
"sinon": "^17.0.1"
}
Expand Down
110 changes: 110 additions & 0 deletions src/transformers/project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,116 @@ test('should use include when exclude is also specified', (t) => {
t.deepEqual(ret, expected)
})

test('should use keys from includePath', (t) => {
const data = {
id: 'ent1',
title: 'Entry 1',
$type: 'entry',
author: { id: 'johnf' },
meta: {
keys: ['id', 'title'],
},
}
const includePath = 'meta.keys'
const expected = { id: 'ent1', title: 'Entry 1' }

const ret = project({ includePath })(options)(data, state)

t.deepEqual(ret, expected)
})

test('should leave object untouched when no keys in includePath', (t) => {
const data = {
id: 'ent1',
title: 'Entry 1',
$type: 'entry',
author: { id: 'johnf' },
meta: {
// No keys
},
}
const includePath = 'meta.keys'
const expected = data

const ret = project({ includePath })(options)(data, state)

t.deepEqual(ret, expected)
})

test('should use include keys as fall-back for includePath', (t) => {
const data = {
id: 'ent1',
title: 'Entry 1',
$type: 'entry',
author: { id: 'johnf' },
meta: {
// No keys
},
}
const includePath = 'meta.keys'
const include = ['id', 'title']
const expected = { id: 'ent1', title: 'Entry 1' }

const ret = project({ includePath, include })(options)(data, state)

t.deepEqual(ret, expected)
})

test('should use keys from excludePath', (t) => {
const data = {
id: 'ent1',
title: 'Entry 1',
$type: 'entry',
author: { id: 'johnf' },
meta: {
keys: ['$type', 'title', 'meta'],
},
}
const excludePath = 'meta.keys'
const expected = { id: 'ent1', author: { id: 'johnf' } }

const ret = project({ excludePath })(options)(data, state)

t.deepEqual(ret, expected)
})

test('should leave object untouched when no keys in excludePath', (t) => {
const data = {
id: 'ent1',
title: 'Entry 1',
$type: 'entry',
author: { id: 'johnf' },
meta: {
// No keys
},
}
const excludePath = 'meta.keys'
const expected = data

const ret = project({ excludePath })(options)(data, state)

t.deepEqual(ret, expected)
})

test('should use exclude keys as fall-back for excludePath', (t) => {
const data = {
id: 'ent1',
title: 'Entry 1',
$type: 'entry',
author: { id: 'johnf' },
meta: {
// No keys
},
}
const excludePath = 'meta.keys'
const exclude = ['$type', 'title', 'meta']
const expected = { id: 'ent1', author: { id: 'johnf' } }

const ret = project({ excludePath, exclude })(options)(data, state)

t.deepEqual(ret, expected)
})

test('should return object as is when neither include nor exclude are defined', (t) => {
const data = {
id: 'ent1',
Expand Down
64 changes: 55 additions & 9 deletions src/transformers/project.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import mapAny from 'map-any'
import { pathGetter } from '../operations/getSet.js'
import { isObject, isString, isNonEmptyArray } from '../utils/is.js'
import type { Transformer, TransformerProps } from '../types.js'
import { ensureArray } from '../utils/array.js'
import type { Transformer, TransformerProps, State } from '../types.js'

export interface Props extends TransformerProps {
include?: string[]
includePath?: string
exclude?: string[]
excludePath?: string
}

const projectProps = (rawProps: string[], doInclude: boolean) => {
const projectProps = (rawProps: unknown[], doInclude: boolean) => {
const props = rawProps.filter(isString)
const filterFn = doInclude
? ([key]: [string, unknown]) => props.includes(key)
Expand All @@ -16,20 +20,62 @@ const projectProps = (rawProps: string[], doInclude: boolean) => {
Object.fromEntries(Object.entries(obj).filter(filterFn))
}

const transformer: Transformer<Props> = function bucket({ include, exclude }) {
const projectPropsFromPath = (
path: string,
rawProps: unknown[] | undefined,
doInclude: boolean,
) => {
const getFn = pathGetter(path)
return (obj: Record<string, unknown>, state: State) => {
let props = getFn(obj, state)
if (props === undefined) {
props = rawProps
}
if (!props) {
return obj
}
return projectProps(ensureArray(props), doInclude)(obj)
}
}

function prepareProjectFn(
include?: string[],
includePath?: string,
exclude?: string[],
excludePath?: string,
) {
if (typeof includePath === 'string') {
return projectPropsFromPath(includePath, include, true)
} else if (typeof excludePath === 'string') {
return projectPropsFromPath(excludePath, exclude, false)
} else if (isNonEmptyArray(include)) {
return projectProps(include, true)
} else if (isNonEmptyArray(exclude)) {
return projectProps(exclude, false)
} else {
return (obj: Record<string, unknown>) => obj
}
}

const transformer: Transformer<Props> = function bucket({
include,
includePath,
exclude,
excludePath,
}) {
// Pick the right project function
const projectFn = isNonEmptyArray(include)
? projectProps(include, true)
: isNonEmptyArray(exclude)
? projectProps(exclude, false)
: (obj: Record<string, unknown>) => obj
const projectFn = prepareProjectFn(include, includePath, exclude, excludePath)

// Return a transformer that will apply the project function to objects or
// arrays of objects. Any non-object will replaced by `undefined`.
return () => (data, state) =>
mapAny(
(data) =>
isObject(data) ? (state.rev ? data : projectFn(data)) : undefined,
isObject(data)
? state.rev
? data
: projectFn(data, state)
: undefined,
data,
)
}
Expand Down

0 comments on commit 8930007

Please sign in to comment.