From d0233e839b2bf00043d1e345df5e685c3560f99b Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 11 Jan 2024 10:28:59 -0600 Subject: [PATCH] CE-781 initial set of tets for mongodb --- .../actions/AbstractMongoDBAction.java | 131 ++++++++++++++++-- .../mongodb/actions/MongoDBInsertAction.java | 120 ---------------- .../qqq/backend/module/mongodb/BaseTest.java | 69 +++++++++ .../qqq/backend/module/mongodb/TestUtils.java | 22 +-- .../actions/MongoDBAggregateActionTest.java | 93 +++++++++++++ .../actions/MongoDBCountActionTest.java | 80 +++++++++++ .../actions/MongoDBDeleteActionTest.java | 102 ++++++++++++++ .../actions/MongoDBInsertActionTest.java | 101 ++++++++++++++ .../actions/MongoDBQueryActionTest.java | 119 ++++++++++++++++ .../actions/MongoDBUpdateActionTest.java | 19 +-- 10 files changed, 704 insertions(+), 152 deletions(-) create mode 100644 qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBAggregateActionTest.java create mode 100644 qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBCountActionTest.java create mode 100644 qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBDeleteActionTest.java create mode 100644 qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBInsertActionTest.java create mode 100644 qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBQueryActionTest.java rename qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/QActionInterface.java => qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBUpdateActionTest.java (75%) diff --git a/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/AbstractMongoDBAction.java b/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/AbstractMongoDBAction.java index f51704bc..76c99546 100644 --- a/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/AbstractMongoDBAction.java +++ b/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/AbstractMongoDBAction.java @@ -25,10 +25,12 @@ package com.kingsrook.qqq.backend.module.mongodb.actions; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.ListIterator; import java.util.Map; +import java.util.Set; import java.util.regex.Pattern; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.context.QContext; @@ -86,7 +88,8 @@ public class AbstractMongoDBAction return (new MongoClientContainer(mongoDBTransaction.getMongoClient(), mongoDBTransaction.getClientSession(), false)); } - ConnectionString connectionString = new ConnectionString("mongodb://" + backend.getHost() + ":" + backend.getPort() + "/"); + String suffix = StringUtils.hasContent(backend.getUrlSuffix()) ? "?" + backend.getUrlSuffix() : ""; + ConnectionString connectionString = new ConnectionString("mongodb://" + backend.getHost() + ":" + backend.getPort() + "/" + suffix); MongoCredential credential = MongoCredential.createCredential(backend.getUsername(), backend.getAuthSourceDatabase(), backend.getPassword().toCharArray()); @@ -165,19 +168,66 @@ public class AbstractMongoDBAction QRecord record = new QRecord(); record.setTableName(table.getName()); - /////////////////////////////////////////////////////////////////////////// - // todo - this - or iterate over the values in the document?? // - // seems like, maybe, this is an attribute in the table-backend-details? // - /////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////////////////////// + // first iterate over the table's fields, looking for them (at their backend name (path, // + // if it has dots) inside the document note that we'll remove values from the document // + // as we go - then after this loop, will handle all remaining values as unstructured fields // + ////////////////////////////////////////////////////////////////////////////////////////////// Map values = record.getValues(); for(QFieldMetaData field : table.getFields().values()) { + String fieldName = field.getName(); String fieldBackendName = getFieldBackendName(field); - Object value = document.get(fieldBackendName); - String fieldName = field.getName(); - setValue(values, fieldName, value); + if(fieldBackendName.contains(".")) + { + ///////////////////////////////////////////////////////////// + // process backend-names with dots as hierarchical objects // + ///////////////////////////////////////////////////////////// + String[] parts = fieldBackendName.split("\\."); + Document tmpDocument = document; + for(int i = 0; i < parts.length - 1; i++) + { + if(!tmpDocument.containsKey(parts[i])) + { + /////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if we can't find the sub-document, break, and we won't have a value for this field (do we want null?) // + /////////////////////////////////////////////////////////////////////////////////////////////////////////// + setValue(values, fieldName, null); + break; + } + else + { + if(tmpDocument.get(parts[i]) instanceof Document subDocument) + { + tmpDocument = subDocument; + } + else + { + LOG.warn("Unexpected - In table [" + table.getName() + "] found a non-document at sub-key [" + parts[i] + "] for field [" + field.getName() + "]"); + } + } + } + + Object value = tmpDocument.remove(parts[parts.length - 1]); + setValue(values, fieldName, value); + } + else + { + Object value = document.remove(fieldBackendName); + setValue(values, fieldName, value); + } } + + ////////////////////////////////////////////////////////////// + // handle remaining values in the document as un-structured // + ////////////////////////////////////////////////////////////// + for(String subFieldName : document.keySet()) + { + Object subValue = document.get(subFieldName); + setValue(values, subFieldName, subValue); + } + return (record); } @@ -227,17 +277,23 @@ public class AbstractMongoDBAction /******************************************************************************* ** Convert a QRecord to a mongodb document. *******************************************************************************/ - protected Document recordToDocument(QTableMetaData table, QRecord record) + protected Document recordToDocument(QTableMetaData table, QRecord record) throws QException { Document document = new Document(); - /////////////////////////////////////////////////////////////////////////// - // todo - this - or iterate over the values in the record?? // - // seems like, maybe, this is an attribute in the table-backend-details? // - /////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////////////// + // first iterate over fields defined in the table - put them in the document for mongo first. // + // track the names that we've processed in a set. then later we'll go over all values in the // + // record and send them all to mongo (skipping ones we knew about from the table definition) // + //////////////////////////////////////////////////////////////////////////////////////////////// + Set processedFields = new HashSet<>(); + for(QFieldMetaData field : table.getFields().values()) { - if(field.getName().equals(table.getPrimaryKeyField()) && record.getValue(field.getName()) == null) + Serializable value = record.getValue(field.getName()); + processedFields.add(field.getName()); + + if(field.getName().equals(table.getPrimaryKeyField()) && value == null) { //////////////////////////////////// // let mongodb client generate id // @@ -246,8 +302,53 @@ public class AbstractMongoDBAction } String fieldBackendName = getFieldBackendName(field); - document.append(fieldBackendName, record.getValue(field.getName())); + if(fieldBackendName.contains(".")) + { + ///////////////////////////////////////////////////////////// + // process backend-names with dots as hierarchical objects // + ///////////////////////////////////////////////////////////// + String[] parts = fieldBackendName.split("\\."); + Document tmpDocument = document; + for(int i = 0; i < parts.length - 1; i++) + { + if(!tmpDocument.containsKey(parts[i])) + { + Document subDocument = new Document(); + tmpDocument.put(parts[i], subDocument); + tmpDocument = subDocument; + } + else + { + if(tmpDocument.get(parts[i]) instanceof Document subDocument) + { + tmpDocument = subDocument; + } + else + { + throw (new QException("Fields in table [" + table.getName() + "] specify both a sub-object and a field at the key: " + parts[i])); + } + } + } + tmpDocument.append(parts[parts.length - 1], value); + } + else + { + document.append(fieldBackendName, value); + } } + + ///////////////////////// + // do remaining values // + ///////////////////////// + // for(Map.Entry entry : clone.getValues().entrySet()) + for(Map.Entry entry : record.getValues().entrySet()) + { + if(!processedFields.contains(entry.getKey())) + { + document.append(entry.getKey(), entry.getValue()); + } + } + return (document); } diff --git a/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBInsertAction.java b/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBInsertAction.java index fe89411f..e385f570 100644 --- a/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBInsertAction.java +++ b/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBInsertAction.java @@ -141,126 +141,6 @@ public class MongoDBInsertAction extends AbstractMongoDBAction implements Insert } return (rs); - - /* - try - { - List insertableFields = table.getFields().values().stream() - .filter(field -> !field.getName().equals("id")) // todo - intent here is to avoid non-insertable fields. - .toList(); - - String columns = insertableFields.stream() - .map(f -> "`" + getColumnName(f) + "`") - .collect(Collectors.joining(", ")); - String questionMarks = insertableFields.stream() - .map(x -> "?") - .collect(Collectors.joining(", ")); - - List outputRecords = new ArrayList<>(); - rs.setRecords(outputRecords); - - Connection connection; - boolean needToCloseConnection = false; - if(insertInput.getTransaction() != null && insertInput.getTransaction() instanceof RDBMSTransaction rdbmsTransaction) - { - connection = rdbmsTransaction.getConnection(); - } - else - { - connection = getConnection(insertInput); - needToCloseConnection = true; - } - - try - { - for(List page : CollectionUtils.getPages(insertInput.getRecords(), QueryManager.PAGE_SIZE)) - { - String tableName = escapeIdentifier(getTableName(table)); - StringBuilder sql = new StringBuilder("INSERT INTO ").append(tableName).append("(").append(columns).append(") VALUES"); - List params = new ArrayList<>(); - int recordIndex = 0; - - ////////////////////////////////////////////////////// - // for each record in the page: // - // - if it has errors, skip it // - // - else add a "(?,?,...,?)," clause to the INSERT // - // - then add all fields into the params list // - ////////////////////////////////////////////////////// - for(QRecord record : page) - { - if(CollectionUtils.nullSafeHasContents(record.getErrors())) - { - continue; - } - - if(recordIndex++ > 0) - { - sql.append(","); - } - sql.append("(").append(questionMarks).append(")"); - - for(QFieldMetaData field : insertableFields) - { - Serializable value = record.getValue(field.getName()); - value = scrubValue(field, value); - params.add(value); - } - } - - //////////////////////////////////////////////////////////////////////////////////////// - // if all records had errors, copy them to the output, and continue w/o running query // - //////////////////////////////////////////////////////////////////////////////////////// - if(recordIndex == 0) - { - for(QRecord record : page) - { - QRecord outputRecord = new QRecord(record); - outputRecords.add(outputRecord); - } - continue; - } - - Long mark = System.currentTimeMillis(); - - /////////////////////////////////////////////////////////// - // execute the insert, then foreach record in the input, // - // add it to the output, and set its generated id too. // - /////////////////////////////////////////////////////////// - // todo sql customization - can edit sql and/or param list - // todo - non-serial-id style tables - // todo - other generated values, e.g., createDate... maybe need to re-select? - List idList = QueryManager.executeInsertForGeneratedIds(connection, sql.toString(), params); - int index = 0; - for(QRecord record : page) - { - QRecord outputRecord = new QRecord(record); - outputRecords.add(outputRecord); - - if(CollectionUtils.nullSafeIsEmpty(record.getErrors())) - { - Integer id = idList.get(index++); - outputRecord.setValue(table.getPrimaryKeyField(), id); - } - } - - logSQL(sql, params, mark); - } - } - finally - { - if(needToCloseConnection) - { - connection.close(); - } - } - - return rs; - } - catch(Exception e) - { - throw new QException("Error executing insert: " + e.getMessage(), e); - } - */ } } diff --git a/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/BaseTest.java b/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/BaseTest.java index 38488a60..2cea4bf6 100644 --- a/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/BaseTest.java +++ b/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/BaseTest.java @@ -26,8 +26,17 @@ import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.module.mongodb.actions.AbstractMongoDBAction; +import com.kingsrook.qqq.backend.module.mongodb.actions.MongoClientContainer; +import com.kingsrook.qqq.backend.module.mongodb.model.metadata.MongoDBBackendMetaData; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoDatabase; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.utility.DockerImageName; /******************************************************************************* @@ -37,6 +46,27 @@ public class BaseTest { private static final QLogger LOG = QLogger.getLogger(BaseTest.class); + private static GenericContainer mongoDBContainer; + + private static final String MONGO_IMAGE = "mongo:4.2.0-bionic"; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeAll + static void beforeAll() + { + mongoDBContainer = new GenericContainer<>(DockerImageName.parse(MONGO_IMAGE)) + .withEnv("MONGO_INITDB_ROOT_USERNAME", TestUtils.MONGO_USERNAME) + .withEnv("MONGO_INITDB_ROOT_PASSWORD", TestUtils.MONGO_PASSWORD) + .withEnv("MONGO_INITDB_DATABASE", TestUtils.MONGO_DATABASE) + .withExposedPorts(TestUtils.MONGO_PORT); + + mongoDBContainer.start(); + } + /******************************************************************************* @@ -46,6 +76,13 @@ public class BaseTest void baseBeforeEach() { QContext.init(TestUtils.defineInstance(), new QSession()); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // host could(?) be different, and mapped port will be, so set them in backend meta-data based on our running container // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + MongoDBBackendMetaData backend = (MongoDBBackendMetaData) QContext.getQInstance().getBackend(TestUtils.DEFAULT_BACKEND_NAME); + backend.setHost(mongoDBContainer.getHost()); + backend.setPort(mongoDBContainer.getMappedPort(TestUtils.MONGO_PORT)); } @@ -56,11 +93,43 @@ public class BaseTest @AfterEach void baseAfterEach() { + /////////////////////////////////////// + // clear test database between tests // + /////////////////////////////////////// + MongoClient mongoClient = getMongoClient(); + MongoDatabase database = mongoClient.getDatabase(TestUtils.MONGO_DATABASE); + database.drop(); + QContext.clear(); } + /******************************************************************************* + ** + *******************************************************************************/ + protected static MongoClient getMongoClient() + { + MongoDBBackendMetaData backend = (MongoDBBackendMetaData) QContext.getQInstance().getBackend(TestUtils.DEFAULT_BACKEND_NAME); + MongoClientContainer mongoClientContainer = new AbstractMongoDBAction().openClient(backend, null); + MongoClient mongoClient = mongoClientContainer.getMongoClient(); + return mongoClient; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @AfterAll + static void afterAll() + { + // this.mongoDbReplicaSet.close(); + mongoDBContainer.close(); + } + + + /******************************************************************************* ** if needed, re-initialize the QInstance in context. *******************************************************************************/ diff --git a/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/TestUtils.java b/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/TestUtils.java index 7f188131..82e61ef1 100644 --- a/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/TestUtils.java +++ b/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/TestUtils.java @@ -43,6 +43,13 @@ public class TestUtils public static final String SECURITY_KEY_STORE_ALL_ACCESS = "storeAllAccess"; + public static final String MONGO_USERNAME = "mongoUser"; + public static final String MONGO_PASSWORD = "password"; + public static final Integer MONGO_PORT = 27017; + public static final String MONGO_DATABASE = "testDatabase"; + + public static final String TEST_COLLECTION = "testTable"; + /******************************************************************************* @@ -105,12 +112,11 @@ public class TestUtils return (new MongoDBBackendMetaData() .withName(DEFAULT_BACKEND_NAME) .withHost("localhost") - .withPort(27017) - .withUsername("ctliveuser") - .withPassword("uoaKOIjfk23h8lozK983L") + .withPort(TestUtils.MONGO_PORT) + .withUsername(TestUtils.MONGO_USERNAME) + .withPassword(TestUtils.MONGO_PASSWORD) .withAuthSourceDatabase("admin") - .withDatabaseName("testDatabase") - /*.withUrlSuffix("?tls=true&tlsCAFile=global-bundle.pem&retryWrites=false")*/); + .withDatabaseName(TestUtils.MONGO_DATABASE)); } @@ -128,8 +134,8 @@ public class TestUtils .withBackendName(DEFAULT_BACKEND_NAME) .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.STRING).withBackendName("_id")) - .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME)) - .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME)) + .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withBackendName("metaData.createDate")) + .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withBackendName("metaData.modifyDate")) .withField(new QFieldMetaData("firstName", QFieldType.STRING)) .withField(new QFieldMetaData("lastName", QFieldType.STRING)) .withField(new QFieldMetaData("birthDate", QFieldType.DATE)) @@ -139,7 +145,7 @@ public class TestUtils .withField(new QFieldMetaData("daysWorked", QFieldType.INTEGER)) .withField(new QFieldMetaData("homeTown", QFieldType.STRING)) .withBackendDetails(new MongoDBTableBackendDetails() - .withTableName("testTable")); + .withTableName(TEST_COLLECTION)); } } diff --git a/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBAggregateActionTest.java b/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBAggregateActionTest.java new file mode 100644 index 00000000..a2d98d06 --- /dev/null +++ b/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBAggregateActionTest.java @@ -0,0 +1,93 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.mongodb.actions; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.tables.AggregateAction; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.Aggregate; +import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOperator; +import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.GroupBy; +import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.QFilterOrderByAggregate; +import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.QFilterOrderByGroupBy; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.module.mongodb.BaseTest; +import com.kingsrook.qqq.backend.module.mongodb.TestUtils; +import org.junit.jupiter.api.Test; + + +/******************************************************************************* + ** Unit test for MongoDBQueryAction + *******************************************************************************/ +class MongoDBAggregateActionTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException + { + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(TestUtils.TABLE_NAME_PERSON); + insertInput.setRecords(List.of( + new QRecord().withValue("firstName", "Darin").withValue("lastName", "Kelkhoff").withValue("isEmployed", true).withValue("annualSalary", 1), + new QRecord().withValue("firstName", "Linda").withValue("lastName", "Kelkhoff").withValue("isEmployed", true).withValue("annualSalary", 5), + new QRecord().withValue("firstName", "Tim").withValue("lastName", "Chamberlain").withValue("isEmployed", true).withValue("annualSalary", 3), + new QRecord().withValue("firstName", "James").withValue("lastName", "Maes").withValue("isEmployed", true).withValue("annualSalary", 5), + new QRecord().withValue("firstName", "J.D.").withValue("lastName", "Maes").withValue("isEmployed", false).withValue("annualSalary", 0) + )); + new InsertAction().execute(insertInput); + + { + AggregateInput aggregateInput = new AggregateInput(); + aggregateInput.setTableName(TestUtils.TABLE_NAME_PERSON); + aggregateInput.setFilter(new QQueryFilter() + .withOrderBy(new QFilterOrderByAggregate(new Aggregate("annualSalary", AggregateOperator.MAX)).withIsAscending(false)) + .withOrderBy(new QFilterOrderByGroupBy(new GroupBy(QFieldType.STRING, "lastName"))) + ); + aggregateInput.withAggregate(new Aggregate("id", AggregateOperator.COUNT)); + aggregateInput.withAggregate(new Aggregate("annualSalary", AggregateOperator.SUM)); + aggregateInput.withAggregate(new Aggregate("annualSalary", AggregateOperator.MAX)); + aggregateInput.withGroupBy(new GroupBy(QFieldType.STRING, "lastName")); + aggregateInput.withGroupBy(new GroupBy(QFieldType.BOOLEAN, "isEmployed")); + AggregateOutput aggregateOutput = new AggregateAction().execute(aggregateInput); + // todo - actual assertions + } + { + AggregateInput aggregateInput = new AggregateInput(); + aggregateInput.setTableName(TestUtils.TABLE_NAME_PERSON); + aggregateInput.withAggregate(new Aggregate("id", AggregateOperator.COUNT)); + aggregateInput.withAggregate(new Aggregate("annualSalary", AggregateOperator.AVG)); + AggregateOutput aggregateOutput = new AggregateAction().execute(aggregateInput); + // todo - actual assertions + } + } + +} \ No newline at end of file diff --git a/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBCountActionTest.java b/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBCountActionTest.java new file mode 100644 index 00000000..bc8ca654 --- /dev/null +++ b/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBCountActionTest.java @@ -0,0 +1,80 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.mongodb.actions; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.tables.CountAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.module.mongodb.BaseTest; +import com.kingsrook.qqq.backend.module.mongodb.TestUtils; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import org.bson.Document; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/******************************************************************************* + ** Unit test for MongoDBQueryAction + *******************************************************************************/ +class MongoDBCountActionTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException + { + //////////////////////////////////////// + // directly insert some mongo records // + //////////////////////////////////////// + MongoDatabase database = getMongoClient().getDatabase(TestUtils.MONGO_DATABASE); + MongoCollection collection = database.getCollection(TestUtils.TEST_COLLECTION); + collection.insertMany(List.of( + Document.parse(""" + {"firstName": "Darin", "lastName": "Kelkhoff"}"""), + Document.parse(""" + {"firstName": "Tylers", "lastName": "Sample"}"""), + Document.parse(""" + {"firstName": "Tylers", "lastName": "Simple"}"""), + Document.parse(""" + {"firstName": "Thom", "lastName": "Chutterloin"}""") + )); + + CountInput countInput = new CountInput(); + countInput.setTableName(TestUtils.TABLE_NAME_PERSON); + assertEquals(4, new CountAction().execute(countInput).getCount()); + + countInput.setFilter(new QQueryFilter(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, "Tylers"))); + assertEquals(2, new CountAction().execute(countInput).getCount()); + + countInput.setFilter(new QQueryFilter(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, "assdf"))); + assertEquals(0, new CountAction().execute(countInput).getCount()); + } + +} \ No newline at end of file diff --git a/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBDeleteActionTest.java b/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBDeleteActionTest.java new file mode 100644 index 00000000..8c751a1b --- /dev/null +++ b/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBDeleteActionTest.java @@ -0,0 +1,102 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.mongodb.actions; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; +import com.kingsrook.qqq.backend.module.mongodb.BaseTest; +import com.kingsrook.qqq.backend.module.mongodb.TestUtils; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import org.bson.Document; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/******************************************************************************* + ** Unit test for MongoDBQueryAction + *******************************************************************************/ +class MongoDBDeleteActionTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException + { + //////////////////////////////////////// + // directly insert some mongo records // + //////////////////////////////////////// + MongoDatabase database = getMongoClient().getDatabase(TestUtils.MONGO_DATABASE); + MongoCollection collection = database.getCollection(TestUtils.TEST_COLLECTION); + collection.insertMany(List.of( + Document.parse(""" + {"firstName": "Darin", "lastName": "Kelkhoff"}"""), + Document.parse(""" + {"firstName": "Tylers", "lastName": "Sample"}"""), + Document.parse(""" + {"firstName": "Tylers", "lastName": "Simple"}"""), + Document.parse(""" + {"firstName": "Thom", "lastName": "Chutterloin"}""") + )); + assertEquals(4, collection.countDocuments()); + + ////////////////////////////////////////// + // do a delete by id (look it up first) // + ////////////////////////////////////////// + { + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(TestUtils.TABLE_NAME_PERSON); + queryInput.setFilter(new QQueryFilter(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, "Darin"))); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + String id0 = queryOutput.getRecords().get(0).getValueString("id"); + + DeleteInput deleteInput = new DeleteInput(); + deleteInput.setTableName(TestUtils.TABLE_NAME_PERSON); + deleteInput.setPrimaryKeys(List.of(id0)); + assertEquals(1, new DeleteAction().execute(deleteInput).getDeletedRecordCount()); + } + assertEquals(3, collection.countDocuments()); + + /////////////////////////// + // do a delete by filter // + /////////////////////////// + { + DeleteInput deleteInput = new DeleteInput(); + deleteInput.setTableName(TestUtils.TABLE_NAME_PERSON); + deleteInput.setQueryFilter(new QQueryFilter(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, "Tylers"))); + assertEquals(2, new DeleteAction().execute(deleteInput).getDeletedRecordCount()); + } + assertEquals(1, collection.countDocuments()); + } + +} \ No newline at end of file diff --git a/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBInsertActionTest.java b/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBInsertActionTest.java new file mode 100644 index 00000000..06e8c3cc --- /dev/null +++ b/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBInsertActionTest.java @@ -0,0 +1,101 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.mongodb.actions; + + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.module.mongodb.BaseTest; +import com.kingsrook.qqq.backend.module.mongodb.TestUtils; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import org.bson.Document; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + + +/******************************************************************************* + ** Unit test for MongoDBQueryAction + *******************************************************************************/ +class MongoDBInsertActionTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException + { + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(TestUtils.TABLE_NAME_PERSON); + insertInput.setRecords(List.of( + new QRecord().withValue("firstName", "Darin") + .withValue("unmappedField", 1701) + .withValue("unmappedList", new ArrayList<>(List.of("A", "B", "C"))) + .withValue("unmappedObject", new HashMap<>(Map.of("A", 1, "C", true))), + new QRecord().withValue("firstName", "Tim"), + new QRecord().withValue("firstName", "Tyler") + )); + InsertOutput insertOutput = new InsertAction().execute(insertInput); + + ///////////////////////////////////////// + // make sure id got put on all records // + ///////////////////////////////////////// + for(QRecord record : insertOutput.getRecords()) + { + assertNotNull(record.getValueString("id")); + } + + /////////////////////////////////////////////////// + // directly query mongo for the inserted records // + /////////////////////////////////////////////////// + MongoDatabase database = getMongoClient().getDatabase(TestUtils.MONGO_DATABASE); + MongoCollection collection = database.getCollection(TestUtils.TEST_COLLECTION); + assertEquals(3, collection.countDocuments()); + for(Document document : collection.find()) + { + ///////////////////////////////////////////////////////////// + // make sure values got set - including some nested values // + ///////////////////////////////////////////////////////////// + assertNotNull(document.get("firstName")); + assertNotNull(document.get("metaData")); + assertThat(document.get("metaData")).isInstanceOf(Document.class); + assertNotNull(((Document) document.get("metaData")).get("createDate")); + } + + Document document = collection.find(new Document("firstName", "Darin")).first(); + assertNotNull(document); + assertEquals(1701, document.get("unmappedField")); + assertEquals(List.of("A", "B", "C"), document.get("unmappedList")); + assertEquals(Map.of("A", 1, "C", true), document.get("unmappedObject")); + } + +} \ No newline at end of file diff --git a/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBQueryActionTest.java b/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBQueryActionTest.java new file mode 100644 index 00000000..2bb6035c --- /dev/null +++ b/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBQueryActionTest.java @@ -0,0 +1,119 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.mongodb.actions; + + +import java.time.Instant; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.module.mongodb.BaseTest; +import com.kingsrook.qqq.backend.module.mongodb.TestUtils; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import org.bson.Document; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/******************************************************************************* + ** Unit test for MongoDBQueryAction + *******************************************************************************/ +class MongoDBQueryActionTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + void beforeEach() + { + + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException + { + //////////////////////////////////////// + // directly insert some mongo records // + //////////////////////////////////////// + MongoDatabase database = getMongoClient().getDatabase(TestUtils.MONGO_DATABASE); + MongoCollection collection = database.getCollection(TestUtils.TEST_COLLECTION); + collection.insertMany(List.of( + Document.parse(""" + { "metaData": {"createDate": "2023-01-09T01:01:01.123Z", "modifyDate": "2023-01-09T02:02:02.123Z", "oops": "All Crunchberries"}, + "firstName": "Darin", + "lastName": "Kelkhoff", + "unmappedField": 1701, + "unmappedList": [1,2,3], + "unmappedObject": { + "A": "B", + "One": 2, + "subSub": { + "so": true + } + } + }"""), + Document.parse(""" + {"metaData": {"createDate": "2023-01-09T03:03:03.123Z", "modifyDate": "2023-01-09T04:04:04.123Z"}, "firstName": "Tylers", "lastName": "Sample"}""") + )); + + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(TestUtils.TABLE_NAME_PERSON); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + + assertEquals(2, queryOutput.getRecords().size()); + + QRecord record = queryOutput.getRecords().get(0); + assertEquals(Instant.parse("2023-01-09T01:01:01.123Z"), record.getValueInstant("createDate")); + assertEquals(Instant.parse("2023-01-09T02:02:02.123Z"), record.getValueInstant("modifyDate")); + assertThat(record.getValue("id")).isInstanceOf(String.class); + assertEquals("Darin", record.getValueString("firstName")); + assertEquals("Kelkhoff", record.getValueString("lastName")); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // test that un-mapped (or un-structured) fields come through, with their shape as they exist in the mongo record // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + assertEquals(1701, record.getValueInteger("unmappedField")); + assertEquals(List.of(1, 2, 3), record.getValue("unmappedList")); + assertEquals(Map.of("A", "B", "One", 2, "subSub", Map.of("so", true)), record.getValue("unmappedObject")); + assertEquals(Map.of("oops", "All Crunchberries"), record.getValue("metaData")); + + record = queryOutput.getRecords().get(1); + assertEquals(Instant.parse("2023-01-09T03:03:03.123Z"), record.getValueInstant("createDate")); + assertEquals(Instant.parse("2023-01-09T04:04:04.123Z"), record.getValueInstant("modifyDate")); + assertEquals("Tylers", record.getValueString("firstName")); + assertEquals("Sample", record.getValueString("lastName")); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/QActionInterface.java b/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBUpdateActionTest.java similarity index 75% rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/QActionInterface.java rename to qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBUpdateActionTest.java index 88bc436a..6412cd4a 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/QActionInterface.java +++ b/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBUpdateActionTest.java @@ -1,6 +1,6 @@ /* * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2022. Kingsrook, LLC + * Copyright (C) 2021-2024. Kingsrook, LLC * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States * contact@kingsrook.com * https://github.com/Kingsrook/ @@ -19,26 +19,27 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.backend.core.actions.interfaces; +package com.kingsrook.qqq.backend.module.mongodb.actions; -import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; +import com.kingsrook.qqq.backend.module.mongodb.BaseTest; +import org.junit.jupiter.api.Test; /******************************************************************************* - ** + ** Unit test for MongoDBUpdateAction *******************************************************************************/ -public interface QActionInterface +class MongoDBUpdateActionTest extends BaseTest { /******************************************************************************* ** *******************************************************************************/ - default QBackendTransaction openTransaction(AbstractTableActionInput input) throws QException + @Test + void test() throws QException { - return (new QBackendTransaction()); + // todo - test!! } -} +} \ No newline at end of file