diff --git a/packages/repository/src/__tests__/unit/repositories/legacy-juggler-bridge.unit.ts b/packages/repository/src/__tests__/unit/repositories/legacy-juggler-bridge.unit.ts index 9b4c2664d536..e332ed9d3807 100644 --- a/packages/repository/src/__tests__/unit/repositories/legacy-juggler-bridge.unit.ts +++ b/packages/repository/src/__tests__/unit/repositories/legacy-juggler-bridge.unit.ts @@ -686,21 +686,52 @@ describe('DefaultCrudRepository', () => { expect(ok).to.be.true(); }); - it('implements Repository.execute()', async () => { - // Dummy implementation for execute() in datasource - ds.execute = (...args: unknown[]) => { - return Promise.resolve(args); - }; - const repo = new DefaultCrudRepository(Note, ds); - const result = await repo.execute('query', ['arg']); - expect(result).to.deepEqual(['query', ['arg'], undefined]); - }); + describe('Repository.execute()', () => { + beforeEach(() => { + // Dummy implementation for execute() in datasource + ds.execute = (...args: unknown[]) => { + return Promise.resolve(args); + }; + }); - it(`throws error when execute() not implemented by ds connector`, async () => { - const repo = new DefaultCrudRepository(Note, ds); - await expect(repo.execute('query', [])).to.be.rejectedWith( - 'execute() must be implemented by the connector', - ); + it('implements SQL variant', async () => { + const repo = new DefaultCrudRepository(Note, ds); + const result = await repo.execute('query', ['arg']); + expect(result).to.deepEqual(['query', ['arg']]); + }); + + it('implements MongoDB variant', async () => { + const repo = new DefaultCrudRepository(Note, ds); + const result = await repo.execute('MyCollection', 'aggregate', [ + {$unwind: '$data'}, + {$out: 'tempData'}, + ]); + expect(result).to.deepEqual([ + 'MyCollection', + 'aggregate', + [{$unwind: '$data'}, {$out: 'tempData'}], + ]); + }); + + it('implements a generic variant', async () => { + const repo = new DefaultCrudRepository(Note, ds); + const command = { + query: 'MATCH (u:User {email: {email}}) RETURN u', + params: { + email: 'alice@example.com', + }, + }; + const result = await repo.execute(command); + expect(result).to.deepEqual([command]); + }); + + it(`throws error when execute() not implemented by ds connector`, async () => { + delete ds.execute; + const repo = new DefaultCrudRepository(Note, ds); + await expect(repo.execute('query', [])).to.be.rejectedWith( + 'execute() must be implemented by the connector', + ); + }); }); it('has the property inclusionResolvers', () => { diff --git a/packages/repository/src/repositories/legacy-juggler-bridge.ts b/packages/repository/src/repositories/legacy-juggler-bridge.ts index 0afbb4e7146a..96a2e925d188 100644 --- a/packages/repository/src/repositories/legacy-juggler-bridge.ts +++ b/packages/repository/src/repositories/legacy-juggler-bridge.ts @@ -573,12 +573,95 @@ export class DefaultCrudRepository< return ensurePromise(this.modelClass.exists(id, options)); } - async execute( + /** + * Execute a SQL command. + * + * **WARNING:** In general, it is always better to perform database actions + * through repository methods. Directly executing SQL may lead to unexpected + * results, corrupted data, security vulnerabilities and other issues. + * + * @example + * + * ```ts + * // MySQL + * const result = await repo.execute( + * 'SELECT * FROM Products WHERE size > ?', + * [42] + * ); + * + * // PostgreSQL + * const result = await repo.execute( + * 'SELECT * FROM Products WHERE size > $1', + * [42] + * ); + * ``` + * + * @param command A parameterized SQL command or query. + * Check your database documentation for information on which characters to + * use as parameter placeholders. + * @param parameters List of parameter values to use. + * @param options Additional options, for example `transaction`. + * @returns A promise which resolves to the command output as returned by the + * database driver. The output type (data structure) is database specific and + * often depends on the command executed. + */ + execute( command: Command, parameters: NamedParameters | PositionalParameters, options?: Options, - ): Promise { - return ensurePromise(this.dataSource.execute(command, parameters, options)); + ): Promise; + + /** + * Execute a MongoDB command. + * + * **WARNING:** In general, it is always better to perform database actions + * through repository methods. Directly executing MongoDB commands may lead + * to unexpected results and other issues. + * + * @example + * + * ```ts + * const result = await repo.execute('MyCollection', 'aggregate', [ + * {$lookup: { + * // ... + * }}, + * {$unwind: '$data'}, + * {$out: 'tempData'} + * ]); + * ``` + * + * @param collectionName The name of the collection to execute the command on. + * @param command The command name. See + * [Collection API docs](http://mongodb.github.io/node-mongodb-native/3.6/api/Collection.html) + * for the list of commands supported by the MongoDB client. + * @param parameters Command parameters (arguments), as described in MongoDB API + * docs for individual collection methods. + * @returns A promise which resolves to the command output as returned by the + * database driver. + */ + execute( + collectionName: string, + command: string, + ...parameters: PositionalParameters + ): Promise; + + /** + * Execute a raw database command using a connector that's not described + * by LoopBack's `execute` API yet. + * + * **WARNING:** In general, it is always better to perform database actions + * through repository methods. Directly executing database commands may lead + * to unexpected results and other issues. + * + * @param args Command and parameters, please consult your connector's + * documentation to learn about supported commands and their parameters. + * @returns A promise which resolves to the command output as returned by the + * database driver. + */ + execute(...args: PositionalParameters): Promise; + + async execute(...args: PositionalParameters): Promise { + return ensurePromise(this.dataSource.execute(...args)); } protected toEntity(model: juggler.PersistedModel): R {