CE-781 initial set of tets for mongodb

This commit is contained in:
2024-01-11 10:28:59 -06:00
parent e4d7797bbe
commit d0233e839b
10 changed files with 704 additions and 152 deletions

View File

@ -25,10 +25,12 @@ package com.kingsrook.qqq.backend.module.mongodb.actions;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.ListIterator; import java.util.ListIterator;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.context.QContext;
@ -86,7 +88,8 @@ public class AbstractMongoDBAction
return (new MongoClientContainer(mongoDBTransaction.getMongoClient(), mongoDBTransaction.getClientSession(), false)); 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()); MongoCredential credential = MongoCredential.createCredential(backend.getUsername(), backend.getAuthSourceDatabase(), backend.getPassword().toCharArray());
@ -165,19 +168,66 @@ public class AbstractMongoDBAction
QRecord record = new QRecord(); QRecord record = new QRecord();
record.setTableName(table.getName()); record.setTableName(table.getName());
/////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////
// todo - this - or iterate over the values in the document?? // // first iterate over the table's fields, looking for them (at their backend name (path, //
// seems like, maybe, this is an attribute in the table-backend-details? // // 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<String, Serializable> values = record.getValues(); Map<String, Serializable> values = record.getValues();
for(QFieldMetaData field : table.getFields().values()) for(QFieldMetaData field : table.getFields().values())
{ {
String fieldBackendName = getFieldBackendName(field);
Object value = document.get(fieldBackendName);
String fieldName = field.getName(); String fieldName = field.getName();
String fieldBackendName = getFieldBackendName(field);
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); 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); return (record);
} }
@ -227,17 +277,23 @@ public class AbstractMongoDBAction
/******************************************************************************* /*******************************************************************************
** Convert a QRecord to a mongodb document. ** 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(); Document document = new Document();
/////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////
// todo - this - or iterate over the values in the record?? // // first iterate over fields defined in the table - put them in the document for mongo first. //
// seems like, maybe, this is an attribute in the table-backend-details? // // 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<String> processedFields = new HashSet<>();
for(QFieldMetaData field : table.getFields().values()) 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 // // let mongodb client generate id //
@ -246,8 +302,53 @@ public class AbstractMongoDBAction
} }
String fieldBackendName = getFieldBackendName(field); 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<String, Serializable> entry : clone.getValues().entrySet())
for(Map.Entry<String, Serializable> entry : record.getValues().entrySet())
{
if(!processedFields.contains(entry.getKey()))
{
document.append(entry.getKey(), entry.getValue());
}
}
return (document); return (document);
} }

View File

@ -141,126 +141,6 @@ public class MongoDBInsertAction extends AbstractMongoDBAction implements Insert
} }
return (rs); return (rs);
/*
try
{
List<QFieldMetaData> 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<QRecord> 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<QRecord> 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<Object> 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<Integer> 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);
}
*/
} }
} }

View File

@ -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.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.session.QSession; 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.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach; 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 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() void baseBeforeEach()
{ {
QContext.init(TestUtils.defineInstance(), new QSession()); 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 @AfterEach
void baseAfterEach() void baseAfterEach()
{ {
///////////////////////////////////////
// clear test database between tests //
///////////////////////////////////////
MongoClient mongoClient = getMongoClient();
MongoDatabase database = mongoClient.getDatabase(TestUtils.MONGO_DATABASE);
database.drop();
QContext.clear(); 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. ** if needed, re-initialize the QInstance in context.
*******************************************************************************/ *******************************************************************************/

View File

@ -43,6 +43,13 @@ public class TestUtils
public static final String SECURITY_KEY_STORE_ALL_ACCESS = "storeAllAccess"; 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() return (new MongoDBBackendMetaData()
.withName(DEFAULT_BACKEND_NAME) .withName(DEFAULT_BACKEND_NAME)
.withHost("localhost") .withHost("localhost")
.withPort(27017) .withPort(TestUtils.MONGO_PORT)
.withUsername("ctliveuser") .withUsername(TestUtils.MONGO_USERNAME)
.withPassword("uoaKOIjfk23h8lozK983L") .withPassword(TestUtils.MONGO_PASSWORD)
.withAuthSourceDatabase("admin") .withAuthSourceDatabase("admin")
.withDatabaseName("testDatabase") .withDatabaseName(TestUtils.MONGO_DATABASE));
/*.withUrlSuffix("?tls=true&tlsCAFile=global-bundle.pem&retryWrites=false")*/);
} }
@ -128,8 +134,8 @@ public class TestUtils
.withBackendName(DEFAULT_BACKEND_NAME) .withBackendName(DEFAULT_BACKEND_NAME)
.withPrimaryKeyField("id") .withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.STRING).withBackendName("_id")) .withField(new QFieldMetaData("id", QFieldType.STRING).withBackendName("_id"))
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME)) .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withBackendName("metaData.createDate"))
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME)) .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withBackendName("metaData.modifyDate"))
.withField(new QFieldMetaData("firstName", QFieldType.STRING)) .withField(new QFieldMetaData("firstName", QFieldType.STRING))
.withField(new QFieldMetaData("lastName", QFieldType.STRING)) .withField(new QFieldMetaData("lastName", QFieldType.STRING))
.withField(new QFieldMetaData("birthDate", QFieldType.DATE)) .withField(new QFieldMetaData("birthDate", QFieldType.DATE))
@ -139,7 +145,7 @@ public class TestUtils
.withField(new QFieldMetaData("daysWorked", QFieldType.INTEGER)) .withField(new QFieldMetaData("daysWorked", QFieldType.INTEGER))
.withField(new QFieldMetaData("homeTown", QFieldType.STRING)) .withField(new QFieldMetaData("homeTown", QFieldType.STRING))
.withBackendDetails(new MongoDBTableBackendDetails() .withBackendDetails(new MongoDBTableBackendDetails()
.withTableName("testTable")); .withTableName(TEST_COLLECTION));
} }
} }

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Document> 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());
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Document> 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());
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Document> 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"));
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Document> 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"));
}
}

View File

@ -1,6 +1,6 @@
/* /*
* QQQ - Low-code Application Framework for Engineers. * 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 * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com * contact@kingsrook.com
* https://github.com/Kingsrook/ * https://github.com/Kingsrook/
@ -19,26 +19,27 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
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.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!!
} }
} }