Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce resultTransformers.eager and resultTransformers.mapped #1202

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 138 additions & 35 deletions packages/core/src/result-transformers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,36 @@ class ResultTransformers {
* const { keys, records, summary } = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'})
*
* @returns {ResultTransformer<EagerResult<Entries>>} The result transformer
* @alias {@link ResultTransformers#eager}
*/
eagerResultTransformer<Entries extends RecordShape = RecordShape>(): ResultTransformer<EagerResult<Entries>> {
return createEagerResultFromResult
}

/**
* Creates a {@link ResultTransformer} which transforms {@link Result} to {@link EagerResult}
* by consuming the whole stream.
*
* This is the default implementation used in {@link Driver#executeQuery} and a alias to
* {@link resultTransformers.eagerResultTransformer}
*
* @example
* // This:
* const { keys, records, summary } = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, {
* resultTransformer: neo4j.resultTransformers.eager()
* })
* // is equivalent to:
* const { keys, records, summary } = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'})
*
* @returns {ResultTransformer<EagerResult<Entries>>} The result transformer
* @experimental this is a preview
* @since 5.22.0
* @alias {@link ResultTransformers#eagerResultTransformer}
*/
eager<Entries extends RecordShape = RecordShape>(): ResultTransformer<EagerResult<Entries>> {
return createEagerResultFromResult
}

/**
* Creates a {@link ResultTransformer} which maps the {@link Record} in the result and collects it
* along with the {@link ResultSummary} and {@link Result#keys}.
Expand Down Expand Up @@ -122,41 +147,81 @@ class ResultTransformers {
mappedResultTransformer <
R = Record, T = { records: R[], keys: string[], summary: ResultSummary }
>(config: { map?: (rec: Record) => R | undefined, collect?: (records: R[], summary: ResultSummary, keys: string[]) => T }): ResultTransformer<T> {
if (config == null || (config.collect == null && config.map == null)) {
throw newError('Requires a map or/and a collect functions.')
}
return async (result: Result) => {
return await new Promise((resolve, reject) => {
const state: { keys: string[], records: R[] } = { records: [], keys: [] }

result.subscribe({
onKeys (keys: string[]) {
state.keys = keys
},
onNext (record: Record) {
if (config.map != null) {
const mappedRecord = config.map(record)
if (mappedRecord !== undefined) {
state.records.push(mappedRecord)
}
} else {
state.records.push(record as unknown as R)
}
},
onCompleted (summary: ResultSummary) {
if (config.collect != null) {
resolve(config.collect(state.records, summary, state.keys))
} else {
const obj = { records: state.records, summary, keys: state.keys }
resolve(obj as unknown as T)
}
},
onError (error: Error) {
reject(error)
}
})
})
}
return createMappedResultTransformer(config)
}

/**
* Creates a {@link ResultTransformer} which maps the {@link Record} in the result and collects it
* along with the {@link ResultSummary} and {@link Result#keys}.
*
* NOTE: The config object requires map or/and collect to be valid.
*
* This method is a alias to {@link ResultTransformers#mappedResultTransformer}
*
*
* @example
* // Mapping the records
* const { keys, records, summary } = await driver.executeQuery('MATCH (p:Person{ age: $age }) RETURN p.name as name', { age: 25 }, {
* resultTransformer: neo4j.resultTransformers.mapped({
* map(record) {
* return record.get('name')
* }
* })
* })
*
* records.forEach(name => console.log(`${name} has 25`))
*
* @example
* // Mapping records and collect result
* const names = await driver.executeQuery('MATCH (p:Person{ age: $age }) RETURN p.name as name', { age: 25 }, {
* resultTransformer: neo4j.resultTransformers.mapped({
* map(record) {
* return record.get('name')
* },
* collect(records, summary, keys) {
* return records
* }
* })
* })
*
* names.forEach(name => console.log(`${name} has 25`))
*
* @example
* // The transformer can be defined one and used everywhere
* const getRecordsAsObjects = neo4j.resultTransformers.mapped({
* map(record) {
* return record.toObject()
* },
* collect(objects) {
* return objects
* }
* })
*
* // The usage in a driver.executeQuery
* const objects = await driver.executeQuery('MATCH (p:Person{ age: $age }) RETURN p.name as name', { age: 25 }, {
* resultTransformer: getRecordsAsObjects
* })
* objects.forEach(object => console.log(`${object.name} has 25`))
*
*
* // The usage in session.executeRead
* const objects = await session.executeRead(tx => getRecordsAsObjects(tx.run('MATCH (p:Person{ age: $age }) RETURN p.name as name')))
* objects.forEach(object => console.log(`${object.name} has 25`))
*
* @param {object} config The result transformer configuration
* @param {function(record:Record):R} [config.map=function(record) { return record }] Method called for mapping each record
* @param {function(records:R[], summary:ResultSummary, keys:string[]):T} [config.collect=function(records, summary, keys) { return { records, summary, keys }}] Method called for mapping
* the result data to the transformer output.
* @returns {ResultTransformer<T>} The result transformer
* @experimental This is a preview feature
* @alias {@link ResultTransformers#mappedResultTransformer}
* @since 5.22.0
* @see {@link Driver#executeQuery}
*/
mapped <
R = Record, T = { records: R[], keys: string[], summary: ResultSummary }
>(config: { map?: (rec: Record) => R | undefined, collect?: (records: R[], summary: ResultSummary, keys: string[]) => T }): ResultTransformer<T> {
return createMappedResultTransformer(config)
}

/**
Expand Down Expand Up @@ -222,6 +287,44 @@ async function createEagerResultFromResult<Entries extends RecordShape> (result:
return new EagerResult<Entries>(keys, records, summary)
}

function createMappedResultTransformer<R = Record, T = { records: R[], keys: string[], summary: ResultSummary }> (config: { map?: (rec: Record) => R | undefined, collect?: (records: R[], summary: ResultSummary, keys: string[]) => T }): ResultTransformer<T> {
if (config == null || (config.collect == null && config.map == null)) {
throw newError('Requires a map or/and a collect functions.')
}
return async (result: Result) => {
return await new Promise((resolve, reject) => {
const state: { keys: string[], records: R[] } = { records: [], keys: [] }

result.subscribe({
onKeys (keys: string[]) {
state.keys = keys
},
onNext (record: Record) {
if (config.map != null) {
const mappedRecord = config.map(record)
if (mappedRecord !== undefined) {
state.records.push(mappedRecord)
}
} else {
state.records.push(record as unknown as R)
}
},
onCompleted (summary: ResultSummary) {
if (config.collect != null) {
resolve(config.collect(state.records, summary, state.keys))
} else {
const obj = { records: state.records, summary, keys: state.keys }
resolve(obj as unknown as T)
}
},
onError (error: Error) {
reject(error)
}
})
})
}
}

async function first<Entries extends RecordShape> (result: Result): Promise<Record<Entries> | undefined> {
// The async iterator is not used in the for await fashion
// because the transpiler is generating a code which
Expand Down
28 changes: 17 additions & 11 deletions packages/core/test/result-transformers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ import resultTransformers from '../src/result-transformers'
import ResultStreamObserverMock from './utils/result-stream-observer.mock'

describe('resultTransformers', () => {
describe('.eagerResultTransformer()', () => {
describe.each([
['.eagerResultTransformer()', resultTransformers.eagerResultTransformer],
['.eager()', resultTransformers.eager]
])('%s', (_, transformerFactory) => {
describe('with a valid result', () => {
it('it should return an EagerResult', async () => {
const resultStreamObserverMock = new ResultStreamObserverMock()
Expand All @@ -36,7 +39,7 @@ describe('resultTransformers', () => {
resultStreamObserverMock.onNext(rawRecord2)
resultStreamObserverMock.onCompleted(meta)

const eagerResult: EagerResult = await resultTransformers.eagerResultTransformer()(result)
const eagerResult: EagerResult = await transformerFactory()(result)

expect(eagerResult.keys).toEqual(keys)
expect(eagerResult.records).toEqual([
Expand Down Expand Up @@ -66,7 +69,7 @@ describe('resultTransformers', () => {
resultStreamObserverMock.onNext(rawRecord1)
resultStreamObserverMock.onNext(rawRecord2)
resultStreamObserverMock.onCompleted(meta)
const eagerResult: EagerResult<Car> = await resultTransformers.eagerResultTransformer<Car>()(result)
const eagerResult: EagerResult<Car> = await transformerFactory<Car>()(result)

expect(eagerResult.keys).toEqual(keys)
expect(eagerResult.records).toEqual([
Expand All @@ -92,12 +95,15 @@ describe('resultTransformers', () => {
const expectedError = newError('expected error')
const result = new Result(Promise.reject(expectedError), 'query')

await expect(resultTransformers.eagerResultTransformer()(result)).rejects.toThrow(expectedError)
await expect(transformerFactory()(result)).rejects.toThrow(expectedError)
})
})
})

describe('.mappedResultTransformer', () => {
describe.each([
['.mappedResultTransformer', resultTransformers.mappedResultTransformer],
['.mapped', resultTransformers.mapped]
])('%s', (_, transformerFactory) => {
describe('with a valid result', () => {
it('should map and collect the result', async () => {
const {
Expand All @@ -116,7 +122,7 @@ describe('resultTransformers', () => {
ks: keys
}))

const transform = resultTransformers.mappedResultTransformer({ map, collect })
const transform = transformerFactory({ map, collect })

const { as, db, ks }: { as: number[], db: string | undefined | null, ks: string[] } = await transform(result)

Expand Down Expand Up @@ -146,7 +152,7 @@ describe('resultTransformers', () => {

const map = jest.fn((record) => record.get('a') as number)

const transform = resultTransformers.mappedResultTransformer({ map })
const transform = transformerFactory({ map })

const { records: as, summary, keys: receivedKeys }: { records: number[], summary: ResultSummary, keys: string[] } = await transform(result)

Expand Down Expand Up @@ -177,7 +183,7 @@ describe('resultTransformers', () => {
ks: keys
}))

const transform = resultTransformers.mappedResultTransformer({ collect })
const transform = transformerFactory({ collect })

const { recordsFetched, db, ks }: { recordsFetched: number, db: string | undefined | null, ks: string[] } = await transform(result)

Expand All @@ -204,7 +210,7 @@ describe('resultTransformers', () => {
return record.get('a') as number
})

const transform = resultTransformers.mappedResultTransformer({ map })
const transform = transformerFactory({ map })

const { records: as }: { records: number[] } = await transform(result)

Expand All @@ -224,7 +230,7 @@ describe('resultTransformers', () => {
{ Collect: () => {} }
])('should throw if miss-configured [config=%o]', (config) => {
// @ts-expect-error
expect(() => resultTransformers.mappedResultTransformer(config))
expect(() => transformerFactory(config))
.toThrow(newError('Requires a map or/and a collect functions.'))
})

Expand Down Expand Up @@ -259,7 +265,7 @@ describe('resultTransformers', () => {
it('should propagate the exception', async () => {
const expectedError = newError('expected error')
const result = new Result(Promise.reject(expectedError), 'query')
const transformer = resultTransformers.mappedResultTransformer({
const transformer = transformerFactory({
collect: (records) => records
})

Expand Down
Loading