Skip to content

Commit

Permalink
Introduce resultTransformers.eager and resultTransformers.mapped (#…
Browse files Browse the repository at this point in the history
…1202)

**⚠️ This API is released as preview.**

This method are aliases to `resultTransformers.eagerResultTransformer` and `resultTransformers.mappedResultTransformers`.

**⚠️ This API is released as preview.**
  • Loading branch information
bigmontz committed Jun 27, 2024
1 parent 50aa6aa commit 91e38e3
Show file tree
Hide file tree
Showing 3 changed files with 293 additions and 81 deletions.
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

0 comments on commit 91e38e3

Please sign in to comment.