diff --git a/src/odata-to-abstract-sql.ts b/src/odata-to-abstract-sql.ts index 9988210..4faaa01 100644 --- a/src/odata-to-abstract-sql.ts +++ b/src/odata-to-abstract-sql.ts @@ -4,6 +4,8 @@ import stringHash = require('string-hash'); import { isAliasNode, isFromNode, + isSelectNode, + isSelectQueryNode, isTableNode, } from '@balena/abstract-sql-compiler'; import type { @@ -69,6 +71,7 @@ import type { IsNotDistinctFromNode, IsDistinctFromNode, UnknownTypeNodes, + FromTypeNode, } from '@balena/abstract-sql-compiler'; import type { ODataBinds, @@ -176,6 +179,54 @@ const containsQueryOption = (opts?: object): boolean => { return false; }; +const addNestedFieldSelect = ( + selectNode: SelectNode[1], + fromNode: FromNode[1], + fieldName: string, + fieldNameAlias: string, +) => { + let aliasName: string | undefined; + let tableOrSubqueryNode: FromTypeNode[keyof FromTypeNode]; + if (isAliasNode(fromNode)) { + tableOrSubqueryNode = fromNode[1]; + aliasName = fromNode[2]; + } else { + tableOrSubqueryNode = fromNode; + } + if (isTableNode(tableOrSubqueryNode)) { + selectNode.push([ + 'Alias', + ['ReferencedField', aliasName ?? tableOrSubqueryNode[1], fieldName], + fieldNameAlias, + ]); + return; + } + if (!isSelectQueryNode(tableOrSubqueryNode)) { + throw new Error( + `Adding a nested field select to a subquery containing a ${tableOrSubqueryNode[0]} is not supported`, + ); + } + if (aliasName == null) { + // This should never happen but we are checking it to make TS happy. + throw new Error('Found unaliased SelectQueryNode'); + } + const nestedSelectNode = tableOrSubqueryNode.find(isSelectNode); + if (nestedSelectNode == null) { + throw new Error(`Cannot find SelectNode in subquery`); + } + const nestedFromNode = tableOrSubqueryNode.find(isFromNode); + if (nestedFromNode == null) { + throw new Error(`Cannot find FromNode in subquery`); + } + addNestedFieldSelect( + nestedSelectNode[1], + nestedFromNode[1], + fieldName, + fieldNameAlias, + ); + selectNode.push(['ReferencedField', aliasName, fieldNameAlias]); +}; + class Query { public select: Array< | ReferencedFieldNode @@ -215,6 +266,14 @@ class Query { ); this.from.push(tableRef); } + addNestedFieldSelect(fieldName: string, fieldNameAlias: string): void { + if (this.from.length !== 1) { + throw new Error( + `Adding nested field SELECTs is only supported for queries with exactly 1 FROM clause. Found ${this.from.length}`, + ); + } + addNestedFieldSelect(this.select, this.from[0], fieldName, fieldNameAlias); + } compile(queryType: 'SelectQuery'): SelectQueryNode; compile(queryType: 'InsertQuery'): InsertQueryNode; compile(queryType: 'UpdateQuery'): UpdateQueryNode; @@ -717,8 +776,8 @@ export class OData2AbstractSQL { ) { // For update/delete statements we need to use a style query const subQuery = new Query(); - subQuery.select.push(referencedIdField); subQuery.fromResource(this, resource); + subQuery.addNestedFieldSelect(resource.idField, '$modifyid'); if (hasQueryOpts) { this.AddQueryOptions(resource, path, subQuery); } diff --git a/test/filterby.js b/test/filterby.js index 600f81e..47e55f1 100644 --- a/test/filterby.js +++ b/test/filterby.js @@ -478,7 +478,7 @@ run(function () { ['ReferencedField', 'pilot', 'id'], [ 'SelectQuery', - ['Select', [['ReferencedField', 'pilot', 'id']]], + ['Select', [['Alias', ['ReferencedField', 'pilot', 'id'], '$modifyid']]], ['From', ['Table', 'pilot']], [ 'From', @@ -542,7 +542,7 @@ run(function () { it('that inserts', () => { insertTest(result[1]); }); - return it('and updates', () => + it('and updates', () => { expect(result[2]) .to.be.a.query.that.updates.fields( 'created at', @@ -573,11 +573,12 @@ run(function () { 'Default', ) .from('pilot') - .where(updateWhere)); + .where(updateWhere); + }); }), ); - return test('/pilot?$filter=' + odata, 'DELETE', (result) => + test('/pilot?$filter=' + odata, 'DELETE', (result) => it('should delete from pilot where "' + odata + '"', () => { expect(result) .to.be.a.query.that.deletes.from('pilot') @@ -586,7 +587,10 @@ run(function () { ['ReferencedField', 'pilot', 'id'], [ 'SelectQuery', - ['Select', [['ReferencedField', 'pilot', 'id']]], + [ + 'Select', + [['Alias', ['ReferencedField', 'pilot', 'id'], '$modifyid']], + ], ['From', ['Table', 'pilot']], [ 'From', @@ -721,7 +725,10 @@ run([['Number', 1]], function () { ['ReferencedField', 'pilot', 'id'], [ 'SelectQuery', - ['Select', [['ReferencedField', 'pilot', 'id']]], + [ + 'Select', + [['Alias', ['ReferencedField', 'pilot', 'id'], '$modifyid']], + ], ['From', ['Table', 'pilot']], ['Where', abstractsql], ], @@ -744,14 +751,14 @@ run([['Number', 1]], function () { }), ); - return test('/pilot(1)?$filter=' + odata, 'PUT', { name }, (result) => + test('/pilot(1)?$filter=' + odata, 'PUT', { name }, (result) => describe('should upsert the pilot with id 1', function () { it('should be an upsert', () => expect(result).to.be.a.query.that.upserts); it('that inserts', () => { insertTest(result[1]); }); - return it('and updates', () => { + it('and updates', () => { expect(result[2]) .to.be.a.query.that.updates.fields( 'created at', @@ -1511,7 +1518,7 @@ test( ['ReferencedField', 'copilot', 'id'], [ 'SelectQuery', - ['Select', [['ReferencedField', 'copilot', 'id']]], + ['Select', [['ReferencedField', 'copilot', '$modifyid']]], [ 'From', [ @@ -1524,6 +1531,11 @@ test( ['Field', '*'], ['Alias', ['Boolean', false], 'is blocked'], ['Alias', ['Text', 'Junior'], 'rank'], + [ + 'Alias', + ['ReferencedField', 'copilot', 'id'], + '$modifyid', + ], ], ], ['From', ['Table', 'copilot']], @@ -1547,3 +1559,59 @@ test( ]); }), ); + +test( + `/copilot?$select=id,rank&$filter=rank eq 'major'`, + 'DELETE', + { assists__pilot: 1 }, + (result) => + it(`should DELETE copilot based on filtered computed field rank`, () => { + expect(result).to.be.a.query.to.deep.equal([ + 'DeleteQuery', + ['From', ['Table', 'copilot']], + [ + 'Where', + [ + 'In', + ['ReferencedField', 'copilot', 'id'], + [ + 'SelectQuery', + ['Select', [['ReferencedField', 'copilot', '$modifyid']]], + [ + 'From', + [ + 'Alias', + [ + 'SelectQuery', + [ + 'Select', + [ + ['Field', '*'], + ['Alias', ['Boolean', false], 'is blocked'], + ['Alias', ['Text', 'Junior'], 'rank'], + [ + 'Alias', + ['ReferencedField', 'copilot', 'id'], + '$modifyid', + ], + ], + ], + ['From', ['Table', 'copilot']], + ], + 'copilot', + ], + ], + [ + 'Where', + [ + 'IsNotDistinctFrom', + ['ReferencedField', 'copilot', 'rank'], + ['Bind', 0], + ], + ], + ], + ], + ], + ]); + }), +); diff --git a/test/paging.js b/test/paging.js index ccda0ec..ca91e4d 100644 --- a/test/paging.js +++ b/test/paging.js @@ -36,7 +36,10 @@ test('/pilot?$top=5&$skip=100', 'PATCH', { name }, (result) => ['ReferencedField', 'pilot', 'id'], [ 'SelectQuery', - ['Select', [['ReferencedField', 'pilot', 'id']]], + [ + 'Select', + [['Alias', ['ReferencedField', 'pilot', 'id'], '$modifyid']], + ], ['From', ['Table', 'pilot']], ['Limit', ['Number', 5]], ['Offset', ['Number', 100]], @@ -52,7 +55,10 @@ test('/pilot?$top=5&$skip=100', 'DELETE', (result) => ['ReferencedField', 'pilot', 'id'], [ 'SelectQuery', - ['Select', [['ReferencedField', 'pilot', 'id']]], + [ + 'Select', + [['Alias', ['ReferencedField', 'pilot', 'id'], '$modifyid']], + ], ['From', ['Table', 'pilot']], ['Limit', ['Number', 5]], ['Offset', ['Number', 100]],