diff --git a/test/mocha_integration_cassandra.test.js b/test/mocha_integration_cassandra.test.js index 51078b55..7ef409c4 100644 --- a/test/mocha_integration_cassandra.test.js +++ b/test/mocha_integration_cassandra.test.js @@ -668,7 +668,23 @@ describe("Cassandra Local", function () { }); }); - it("17. Delete the associations", function () { + it("17. Read one instant and search on associated incident primary key", function () { + let res = itHelpers.request_graph_ql_post( + `{ + readOneInstant(instant_id: "instant_1") { + instant_id + incident(search: {field: incident_id value:"incident_7" operator:eq}) { + incident_id + } + } + }` + ); + let resBody = JSON.parse(res.body.toString("utf8")); + expect(res.statusCode).to.equal(200); + expect(resBody).to.deep.equal({"data":{"readOneInstant":{"instant_id":"instant_1","incident":{"incident_id":"incident_7"}}}}) + }); + + it("18. Delete the associations", function () { let res = itHelpers.request_graph_ql_post( `{instantsConnection(pagination:{first:20}, search:{field: incident_assoc_id, operator: eq, value:"incident_7"}) {edges {node{ instant_id}}}}` ); @@ -694,7 +710,7 @@ describe("Cassandra Local", function () { } }); - it("18. Get the table template", function () { + it("19. Get the table template", function () { let res = itHelpers.request_graph_ql_post(`{csvTableTemplateIncident}`); let resBody = JSON.parse(res.body.toString("utf8")); expect(res.statusCode).to.equal(200); @@ -708,7 +724,7 @@ describe("Cassandra Local", function () { }); }); - it("19. Associate cassandra to sql model", function () { + it("20. Associate cassandra to sql model", function () { // create sql-capital let res = itHelpers.request_graph_ql_post( `mutation { addCapital(capital_id: "cass_assoc_capital_1", name: "London") {capital_id}}` @@ -1273,7 +1289,95 @@ describe("cassandra Foreign-key arrays", function () { }); }); - it("03. Update record and remove one association - cassandra", function () { + it("03. Query rivers and filter associated cities on city_id existent in the fkarray: simple search - cassandra", function () { + // Operator: eq + let res = itHelpers.request_graph_ql_post( + `{ + riversConnection(pagination:{first:2}) { + rivers{ + river_id + citiesConnection( + pagination: {first: 2} + search: {field: city_id, value: "cassandra_city_1", operator: eq} + ){ + edges { + node { + city_id + } + } + } + } + } + } + ` + ); + expect(res.statusCode).to.equal(200); + let resBody = JSON.parse(res.body.toString("utf8")); + expect(resBody.data).to.deep.equal({"riversConnection":{"rivers":[{"river_id":"fkA_river_1","citiesConnection":{"edges":[{"node":{"city_id":"cassandra_city_1"}}]}}]}}); + + + }); + + it("04. Query rivers and filter associated cities on city_id existent in the fkarray: complex search - cassandra", function(){ + //Operator: in + let res = itHelpers.request_graph_ql_post( + `{ + riversConnection(pagination:{first:2}) { + rivers{ + river_id + citiesConnection( + pagination: {first: 2} + search: { operator: and, search:[ + {field: city_id, value: "cassandra_city_2", operator: eq}, + {field: name, value: "duesseldorf", operator: eq} + ]} + ){ + edges { + node { + city_id + } + } + } + } + } + } + ` + ); + expect(res.statusCode).to.equal(200); + let resBody = JSON.parse(res.body.toString("utf8")); + + expect(resBody.data).to.deep.equal({"riversConnection":{"rivers":[{"river_id":"fkA_river_1","citiesConnection":{"edges":[{"node":{"city_id":"cassandra_city_2"}}]}}]}}); + }); + + it("05. Query rivers and filter associated cities on city_id existent in the fkarray: IN search - cassandra", function(){ + //Operator: in + let res = itHelpers.request_graph_ql_post( + `{ + riversConnection(pagination:{first:2}) { + rivers{ + river_id + citiesConnection( + pagination: {first: 2} + search: {field: city_id, value: "cassandra_city_2,cassandra_city_1,city_non_existent", operator: in, valueType:Array} + ){ + edges { + node { + city_id + } + } + } + } + } + } + ` + ); + expect(res.statusCode).to.equal(200); + let resBody = JSON.parse(res.body.toString("utf8")); + + expect(resBody.data).to.deep.equal({"riversConnection":{"rivers":[{"river_id":"fkA_river_1","citiesConnection":{"edges":[{"node":{"city_id":"cassandra_city_1"}},{"node":{"city_id":"cassandra_city_2"}}]}}]}}) + }); + + it("06. Update record and remove one association - cassandra", function () { let res = itHelpers.request_graph_ql_post( 'mutation{updateCity(city_id:"cassandra_city_1" removeRivers:["fkA_river_1"]){city_id river_ids}}' ); @@ -1295,7 +1399,7 @@ describe("cassandra Foreign-key arrays", function () { }); }); - it("04. Update record and add one association - cassandra", function () { + it("07. Update record and add one association - cassandra", function () { let res = itHelpers.request_graph_ql_post( 'mutation{updateRiver(river_id:"fkA_river_1" addCities:["cassandra_city_1"]){river_id city_ids}}' ); @@ -1330,7 +1434,7 @@ describe("cassandra Foreign-key arrays", function () { }); }); - it("05. Update record and remove all association - cassandra", function () { + it("08. Update record and remove all association - cassandra", function () { let res = itHelpers.request_graph_ql_post( 'mutation{updateRiver(river_id:"fkA_river_1" removeCities:["cassandra_city_1","cassandra_city_2"]){river_id city_ids}}' ); diff --git a/test/mocha_unit.test.js b/test/mocha_unit.test.js index c9bd4299..6633c54f 100644 --- a/test/mocha_unit.test.js +++ b/test/mocha_unit.test.js @@ -2722,6 +2722,12 @@ describe("Cassandra storagetype", function () { testCompare(generated_resolver, data_test.cassandra_resolver_Count); }); + it("targetStorageType cassandra fieldResolver Workaround - citiesConnection", async function () { + let opts = funks.getOptions(models_cassandra.river); + let generated_resolver = await funks.generateJs("create-resolvers", opts); + testCompare(generated_resolver, data_test.river_many_to_many_cassandra_fieldResolver_Connection); + }); + it("cassandra models - constructor", async function () { let opts = funks.getOptions(models_cassandra.city); let generated_model = await funks.generateJs( diff --git a/test/unit_test_misc/data_models_cassandra.js b/test/unit_test_misc/data_models_cassandra.js index 7f81f515..20473e74 100644 --- a/test/unit_test_misc/data_models_cassandra.js +++ b/test/unit_test_misc/data_models_cassandra.js @@ -27,6 +27,39 @@ module.exports.city = { "internalId": "city_id" } +module.exports.river = { + "model": "river", + "storageType": "SQL", + "attributes": { + "name": "String", + "length": "Int", + "river_id": "String", + + "city_ids": "[String]" + }, + "associations": { + "countries": { + "type": "many_to_many", + "implementation": "sql_cross_table", + "target": "country", + "sourceKey": "river_id", + "targetKey": "country_id", + "keysIn": "country_to_river", + "targetStorageType": "sql" + }, + "cities": { + "type": "many_to_many", + "implementation": "foreignkeys", + "target": "city", + "targetStorageType": "cassandra", + "sourceKey": "city_ids", + "targetKey": "river_ids", + "keysIn": "river" + } + }, + "internalId": "river_id" +} + module.exports.incident = { "model": "Incident", "storageType": "cassandra", diff --git a/test/unit_test_misc/test-describe/cassandra-storagetype.js b/test/unit_test_misc/test-describe/cassandra-storagetype.js index e79108bb..3fa43571 100644 --- a/test/unit_test_misc/test-describe/cassandra-storagetype.js +++ b/test/unit_test_misc/test-describe/cassandra-storagetype.js @@ -161,6 +161,41 @@ countCities: async function({ } `; +module.exports.river_many_to_many_cassandra_fieldResolver_Connection = ` +river.prototype.citiesConnection = function({ + search, + order, + pagination +}, context) { + //return an empty response if the foreignKey Array is empty, no need to query the database + if (!Array.isArray(this.city_ids) || this.city_ids.length === 0) { + return { + edges: [], + cities: [], + pageInfo: { + startCursor: null, + endCursor: null, + hasPreviousPage: false, + hasNextPage: false + } + }; + } + const hasIdSearch = helper.parseFieldResolverSearchArgForCassandra(search, this.city_ids, models.city.idAttribute()); + let nsearch = hasIdSearch ? search : helper.addSearchField({ + "search": search, + "field": models.city.idAttribute(), + "value": this.city_ids.join(','), + "valueType": "Array", + "operator": "in" + }); + return resolvers.citiesConnection({ + search: nsearch, + order: order, + pagination: pagination + }, context); +} +`; + module.exports.cassandra_model_constructor = ` constructor(input) { for (let key of Object.keys(input)) { diff --git a/views/create-migrations-cassandra.ejs b/views/create-migrations-cassandra.ejs index 366f2855..625fd2a1 100644 --- a/views/create-migrations-cassandra.ejs +++ b/views/create-migrations-cassandra.ejs @@ -29,7 +29,7 @@ module.exports = { await cassandraClient.execute(createString); let indexCreationPromises = indexCreationStrings.map(async i => - await cassandraClient.execute('CREATE INDEX IF NOT EXISTS <%-namePl-%>_' + i + '_index ON <%-namePl-%> (' + i + ');')); + await cassandraClient.execute('CREATE INDEX IF NOT EXISTS <%-namePl-%>_' + i + '_index ON "<%-namePl-%>" ("' + i + '");')); await Promise.allSettled(indexCreationPromises); @@ -46,7 +46,7 @@ module.exports = { // get the default cassandra client const connectionInstances = await getConnectionInstances(); const cassandraClient = connectionInstances.get("default-cassandra").connection; - await cassandraClient.execute('DROP TABLE IF EXISTS <%-namePl-%>'); + await cassandraClient.execute('DROP TABLE IF EXISTS "<%-namePl-%>"'); } }; diff --git a/views/create-resolvers.ejs b/views/create-resolvers.ejs index f8b9b3e5..fb84b11f 100644 --- a/views/create-resolvers.ejs +++ b/views/create-resolvers.ejs @@ -154,8 +154,17 @@ const associationArgsDef = { if (search === undefined || search === null) { return resolvers.readOne<%=associations_one[i].target_cp%>({[models.<%=associations_one[i].target_lc-%>.idAttribute()]: this.<%=associations_one[i].targetKey%>},context) } else { + <%# WORKAROUND FOR Cassandra targetStorageType: + In case of an association to a model within cassandra we need to do intersections + of the search parameters with the foreignkey array if the search is on the idAttribute + and with operator "eq" / "in", since cassandra doesn't support multiple restricions + with an "eq" / "in" on the primary key field. %> + <%if(associations_one[i].targetStorageType === 'cassandra'){%> + //WORKAROUND for cassandra targetStorageType. Mainpulate search to intersect Equal searches on the primaryKey + const hasIdSearch = helper.parseFieldResolverSearchArgForCassandra(search, this.<%=associations_one[i].targetKey%>, models.<%=associations_one[i].target_lc-%>.idAttribute()); + <%}-%> //build new search filter - let nsearch = helper.addSearchField({ + let nsearch = <%if(associations_one[i].targetStorageType === 'cassandra'){%>hasIdSearch ? search : <%}-%>helper.addSearchField({ "search": search, "field": models.<%=associations_one[i].target_lc-%>.idAttribute(), "value": this.<%= associations_one[i].targetKey -%>, @@ -260,13 +269,23 @@ const associationArgsDef = { if (!Array.isArray(this.<%=associations_temp[i].sourceKey%>) || this.<%=associations_temp[i].sourceKey%>.length === 0 ) { return 0; } - let nsearch = helper.addSearchField({ + <%# WORKAROUND FOR Cassandra targetStorageType: + In case of an association to a model within cassandra we need to do intersections + of the search parameters with the foreignkey array if the search is on the idAttribute + and with operator "eq" / "in", since cassandra doesn't support multiple restricions + with an "eq" / "in" on the primary key field. %> + <%if(associations_temp[i].targetStorageType === 'cassandra'){%> + //WORKAROUND for cassandra targetStorageType. Mainpulate search to intersect Equal searches on the primaryKey + const hasIdSearch = helper.parseFieldResolverSearchArgForCassandra(search, this.<%=associations_temp[i].sourceKey%>, models.<%=associations_temp[i].target_lc-%>.idAttribute()); + <%}-%> + let nsearch = <%if(associations_temp[i].targetStorageType === 'cassandra'){%>hasIdSearch ? search : <%}-%>helper.addSearchField({ "search": search, "field": models.<%=associations_temp[i].target_lc-%>.idAttribute(), "value": this.<%=associations_temp[i].sourceKey%>.join(','), "valueType": "Array", "operator": "in" }); + <%}else{-%> //build new search filter let nsearch = helper.addSearchField({ @@ -293,26 +312,34 @@ const associationArgsDef = { <%- nameLc -%>.prototype.<%=associations_temp[i].name%>Connection = function({search,order,pagination}, context){ <%if(associations_temp[i].assocThroughArray){%> - //return an empty response if the foreignKey Array is empty, no need to query the database - if (!Array.isArray(this.<%=associations_temp[i].sourceKey%>) || this.<%=associations_temp[i].sourceKey%>.length === 0 ) { - return { - edges: [], - <%=associations_temp[i].target_lc_pl%>: [], - pageInfo: { - startCursor: null, - endCursor: null, - hasPreviousPage: false, - hasNextPage: false - } - }; - } - let nsearch = helper.addSearchField({ - "search": search, - "field": models.<%=associations_temp[i].target_lc-%>.idAttribute(), - "value": this.<%=associations_temp[i].sourceKey%>.join(','), - "valueType": "Array", - "operator": "in" - }); + //return an empty response if the foreignKey Array is empty, no need to query the database + if (!Array.isArray(this.<%=associations_temp[i].sourceKey%>) || this.<%=associations_temp[i].sourceKey%>.length === 0 ) { + return { + edges: [], + <%=associations_temp[i].target_lc_pl%>: [], + pageInfo: { + startCursor: null, + endCursor: null, + hasPreviousPage: false, + hasNextPage: false + } + }; + } + <%# WORKAROUND FOR Cassandra targetStorageType: + In case of an association to a model within cassandra we need to do intersections + of the search parameters with the foreignkey array if the search is on the idAttribute + and with operator "eq" / "in", since cassandra doesn't support multiple restricions + with an "eq" / "in" on the primary key field. %> + <%if(associations_temp[i].targetStorageType === 'cassandra'){%> + const hasIdSearch = helper.parseFieldResolverSearchArgForCassandra(search, this.<%=associations_temp[i].sourceKey%>, models.<%=associations_temp[i].target_lc-%>.idAttribute()); + <%}-%> + let nsearch = <%if(associations_temp[i].targetStorageType === 'cassandra'){%>hasIdSearch ? search : <%}-%>helper.addSearchField({ + "search": search, + "field": models.<%=associations_temp[i].target_lc-%>.idAttribute(), + "value": this.<%=associations_temp[i].sourceKey%>.join(','), + "valueType": "Array", + "operator": "in" + }); <%}else{-%> //build new search filter diff --git a/views/includes/create-adapter-fields-mutations.ejs b/views/includes/create-adapter-fields-mutations.ejs index 42d7d290..b44edb26 100644 --- a/views/includes/create-adapter-fields-mutations.ejs +++ b/views/includes/create-adapter-fields-mutations.ejs @@ -95,7 +95,7 @@ <%# /** - * check the type of adapter and handle cassandra-adapter + * check the type of adapter and handle neo4j-adapter */ -%> <%if(storageType === 'neo4j-adapter'){-%> @@ -132,7 +132,7 @@ } } <%}-%> - <%# /*** End of the the cassandra-adapter case */ -%> + <%# /*** End of the the neo4j-adapter case */ -%> <%# /** @@ -294,6 +294,29 @@ <%}-%> <%# /** End of the zendro-webservice-adapter case and the ddm-adapter case */ -%> + <%# + /** + * check the type of adapter and handle cassandra-adapter + */ + -%> + <%if(storageType === 'cassandra-adapter'){-%> + static async <%- op -%>_<%-associationsArguments["to_many"][i].sourceKey-%>(<%- idAttribute-%>, <%-associationsArguments["to_many"][i].sourceKey-%>, benignErrorReporter, handle_inverse = true) { + //handle inverse association + if(handle_inverse){ + let promises = []; + <%-associationsArguments["to_many"][i].sourceKey-%>.forEach( idx =>{ + promises.push(models.<%-associationsArguments["to_many"][i].target_lc-%>.<%- op %>_<%-associationsArguments["to_many"][i].targetKey%>(idx, [`${<%- idAttribute-%>}`], benignErrorReporter, false) ); + }); + await Promise.all(promises); + } + + let mutation = `UPDATE "<%- namePl -%>" SET <%-associationsArguments["to_many"][i].sourceKey-%> = <%-associationsArguments["to_many"][i].sourceKey-%> <% if(op == 'remove'){-%>-<% }else{ %>+<%}-%> ? WHERE <%- idAttribute-%> = ?` + await this.storageHandler.execute(mutation, [<%-associationsArguments["to_many"][i].sourceKey-%>, <%- idAttribute-%>],{ + prepare: true + }); + } + <%}-%> + <%# /*** End of the the cassandra-adapter case */ -%> <%} -%> <%}-%> \ No newline at end of file