diff --git a/packages/core/src/result-transformers.ts b/packages/core/src/result-transformers.ts index 258fd79b4..1cf22d701 100644 --- a/packages/core/src/result-transformers.ts +++ b/packages/core/src/result-transformers.ts @@ -52,11 +52,36 @@ class ResultTransformers { * const { keys, records, summary } = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}) * * @returns {ResultTransformer>} The result transformer + * @alias {@link ResultTransformers#eager} */ eagerResultTransformer(): ResultTransformer> { 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>} The result transformer + * @experimental this is a preview + * @since 5.22.0 + * @alias {@link ResultTransformers#eagerResultTransformer} + */ + eager(): ResultTransformer> { + 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}. @@ -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 { - 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} 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 { + return createMappedResultTransformer(config) } /** @@ -222,6 +287,44 @@ async function createEagerResultFromResult (result: return new EagerResult(keys, records, summary) } +function createMappedResultTransformer (config: { map?: (rec: Record) => R | undefined, collect?: (records: R[], summary: ResultSummary, keys: string[]) => T }): ResultTransformer { + 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 (result: Result): Promise | undefined> { // The async iterator is not used in the for await fashion // because the transpiler is generating a code which diff --git a/packages/core/test/result-transformers.test.ts b/packages/core/test/result-transformers.test.ts index 6e66ad51c..49dadcd32 100644 --- a/packages/core/test/result-transformers.test.ts +++ b/packages/core/test/result-transformers.test.ts @@ -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() @@ -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([ @@ -66,7 +69,7 @@ describe('resultTransformers', () => { resultStreamObserverMock.onNext(rawRecord1) 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([ @@ -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 { @@ -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) @@ -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) @@ -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) @@ -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) @@ -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.')) }) @@ -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 }) diff --git a/packages/neo4j-driver-deno/lib/core/result-transformers.ts b/packages/neo4j-driver-deno/lib/core/result-transformers.ts index ed377690c..f48e160ce 100644 --- a/packages/neo4j-driver-deno/lib/core/result-transformers.ts +++ b/packages/neo4j-driver-deno/lib/core/result-transformers.ts @@ -52,11 +52,36 @@ class ResultTransformers { * const { keys, records, summary } = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}) * * @returns {ResultTransformer>} The result transformer + * @alias {@link ResultTransformers#eager} */ eagerResultTransformer(): ResultTransformer> { 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>} The result transformer + * @experimental this is a preview + * @since 5.22.0 + * @alias {@link ResultTransformers#eagerResultTransformer} + */ + eager(): ResultTransformer> { + 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}. @@ -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 { - 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} 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 { + return createMappedResultTransformer(config) } /** @@ -222,6 +287,44 @@ async function createEagerResultFromResult (result: return new EagerResult(keys, records, summary) } +function createMappedResultTransformer (config: { map?: (rec: Record) => R | undefined, collect?: (records: R[], summary: ResultSummary, keys: string[]) => T }): ResultTransformer { + 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 (result: Result): Promise | undefined> { // The async iterator is not used in the for await fashion // because the transpiler is generating a code which