From ba805a4c9279f5b9567e45bf664755daf799a8ad Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 27 Mar 2023 09:52:39 -0500 Subject: [PATCH] Initial support for associated records (implemented insert, delete). Include "api" on audit. --- .../core/actions/audits/DMLAuditAction.java | 10 +- .../core/actions/tables/DeleteAction.java | 47 ++++++ .../core/actions/tables/InsertAction.java | 49 +++++- .../qqq/backend/core/model/data/QRecord.java | 66 ++++++++ .../model/metadata/tables/Association.java | 128 ++++++++++++++ .../model/metadata/tables/QTableMetaData.java | 47 ++++++ .../core/actions/tables/DeleteActionTest.java | 144 ++++++++++++++++ .../core/actions/tables/InsertActionTest.java | 74 +++++++++ .../qqq/backend/core/utils/TestUtils.java | 89 +++++++++- .../actions/GenerateOpenApiSpecAction.java | 121 +++++++++----- .../qqq/api/actions/QRecordApiAdapter.java | 36 ++++ .../qqq/api/javalin/QJavalinApiHandler.java | 38 +++-- .../java/com/kingsrook/qqq/api/TestUtils.java | 156 +++++++++++++++++- .../api/javalin/QJavalinApiHandlerTest.java | 128 ++++++++++++-- .../javalin/QJavalinImplementation.java | 4 +- 15 files changed, 1052 insertions(+), 85 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/Association.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/DMLAuditAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/DMLAuditAction.java index e9d835be..cb255edd 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/DMLAuditAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/DMLAuditAction.java @@ -54,6 +54,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; @@ -106,6 +107,13 @@ public class DMLAuditAction extends AbstractQActionFunction recordListForAudit = getRecordListForAuditIfNeeded(deleteInput); DeleteOutput deleteOutput = deleteInterface.execute(deleteInput); + + manageAssociations(deleteInput); + // todo post-customization - can do whatever w/ the result if you want new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(deleteInput).withRecordList(recordListForAudit)); @@ -100,6 +107,46 @@ public class DeleteAction + /******************************************************************************* + ** + *******************************************************************************/ + private void manageAssociations(DeleteInput deleteInput) throws QException + { + QTableMetaData table = deleteInput.getTable(); + for(Association association : CollectionUtils.nonNullList(table.getAssociations())) + { + // e.g., order -> orderLine + QJoinMetaData join = QContext.getQInstance().getJoin(association.getJoinName()); // todo ... ever need to flip? + // just assume this, at least for now... if(BooleanUtils.isTrue(association.getDoInserts())) + + QQueryFilter filter = new QQueryFilter(); + + if(join.getJoinOns().size() == 1 && join.getJoinOns().get(0).getLeftField().equals(table.getPrimaryKeyField())) + { + filter.addCriteria(new QFilterCriteria(join.getJoinOns().get(0).getRightField(), QCriteriaOperator.IN, deleteInput.getPrimaryKeys())); + } + else + { + throw (new QException("Join of this type is not supported for an associated delete at this time...")); + } + + QTableMetaData associatedTable = QContext.getQInstance().getTable(association.getAssociatedTableName()); + + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(association.getAssociatedTableName()); + queryInput.setFilter(filter); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + List associatedKeys = queryOutput.getRecords().stream().map(r -> r.getValue(associatedTable.getPrimaryKeyField())).toList(); + + DeleteInput nextLevelDeleteInput = new DeleteInput(); + nextLevelDeleteInput.setTableName(association.getAssociatedTableName()); + nextLevelDeleteInput.setPrimaryKeys(associatedKeys); + DeleteOutput nextLevelDeleteOutput = new DeleteAction().execute(nextLevelDeleteInput); + } + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/InsertAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/InsertAction.java index 118bedd8..7d3d2d64 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/InsertAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/InsertAction.java @@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.actions.tables; import java.io.Serializable; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -40,12 +41,16 @@ import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; import com.kingsrook.qqq.backend.core.actions.tables.helpers.UniqueKeyHelper; import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier; +import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.audits.DMLAuditInput; 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.core.model.metadata.joins.JoinOn; +import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.Association; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher; @@ -75,15 +80,16 @@ public class InsertAction extends AbstractQActionFunction postInsertCustomizer = QCodeLoader.getTableCustomizer(AbstractPostInsertCustomizer.class, table, TableCustomizers.POST_INSERT_RECORD.getRole()); setAutomationStatusField(insertInput); - ValueBehaviorApplier.applyFieldBehaviors(insertInput.getInstance(), table, insertInput.getRecords()); - // todo - need to handle records with errors coming out of here... - QBackendModuleInterface qModule = getBackendModuleInterface(insertInput); // todo pre-customization - just get to modify the request? + ValueBehaviorApplier.applyFieldBehaviors(insertInput.getInstance(), table, insertInput.getRecords()); setErrorsIfUniqueKeyErrors(insertInput, table); InsertOutput insertOutput = qModule.getInsertInterface().execute(insertInput); + + manageAssociations(table, insertOutput.getRecords()); + // todo post-customization - can do whatever w/ the result if you want new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(insertInput).withRecordList(insertOutput.getRecords())); @@ -98,6 +104,43 @@ public class InsertAction extends AbstractQActionFunction insertedRecords) throws QException + { + for(Association association : CollectionUtils.nonNullList(table.getAssociations())) + { + // e.g., order -> orderLine + QJoinMetaData join = QContext.getQInstance().getJoin(association.getJoinName()); // todo ... ever need to flip? + // just assume this, at least for now... if(BooleanUtils.isTrue(association.getDoInserts())) + + List nextLevelInserts = new ArrayList<>(); + for(QRecord record : insertedRecords) + { + if(record.getAssociatedRecords() != null && record.getAssociatedRecords().containsKey(association.getName())) + { + for(QRecord associatedRecord : CollectionUtils.nonNullList(record.getAssociatedRecords().get(association.getName()))) + { + for(JoinOn joinOn : join.getJoinOns()) + { + associatedRecord.setValue(joinOn.getRightField(), record.getValue(joinOn.getLeftField())); + } + nextLevelInserts.add(associatedRecord); + } + } + } + + InsertInput nextLevelInsertInput = new InsertInput(); + nextLevelInsertInput.setTableName(association.getAssociatedTableName()); + nextLevelInsertInput.setRecords(nextLevelInserts); + InsertOutput nextLevelInsertOutput = new InsertAction().execute(nextLevelInsertInput); + } + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java index 49d310f8..8a2f0a91 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java @@ -28,6 +28,7 @@ import java.time.Instant; import java.time.LocalDate; import java.time.LocalTime; import java.util.ArrayList; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -65,6 +66,8 @@ public class QRecord implements Serializable private Map backendDetails = new LinkedHashMap<>(); private List errors = new ArrayList<>(); + private Map> associatedRecords = new HashMap<>(); + public static final String BACKEND_DETAILS_TYPE_JSON_SOURCE_OBJECT = "jsonSourceObject"; @@ -102,6 +105,7 @@ public class QRecord implements Serializable this.displayValues = doDeepCopy(record.displayValues); this.backendDetails = doDeepCopy(record.backendDetails); this.errors = doDeepCopy(record.errors); + this.associatedRecords = doDeepCopy(record.associatedRecords); } @@ -575,4 +579,66 @@ public class QRecord implements Serializable return (QRecordEntity.fromQRecord(c, this)); } + + + /******************************************************************************* + ** Getter for associatedRecords + *******************************************************************************/ + public Map> getAssociatedRecords() + { + return (this.associatedRecords); + } + + + + /******************************************************************************* + ** Setter for associatedRecords + *******************************************************************************/ + public void setAssociatedRecords(Map> associatedRecords) + { + this.associatedRecords = associatedRecords; + } + + + + /******************************************************************************* + ** Fluent setter for associatedRecords + *******************************************************************************/ + public QRecord withAssociatedRecords(Map> associatedRecords) + { + this.associatedRecords = associatedRecords; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for associatedRecords + *******************************************************************************/ + public QRecord withAssociatedRecords(String name, List associatedRecords) + { + if(this.associatedRecords == null) + { + this.associatedRecords = new HashMap<>(); + } + this.associatedRecords.put(name, associatedRecords); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for associatedRecord + *******************************************************************************/ + public QRecord withAssociatedRecord(String name, QRecord associatedRecord) + { + if(this.associatedRecords == null) + { + this.associatedRecords = new HashMap<>(); + } + this.associatedRecords.putIfAbsent(name, new ArrayList<>()); + this.associatedRecords.get(name).add(associatedRecord); + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/Association.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/Association.java new file mode 100644 index 00000000..5ccd956e --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/Association.java @@ -0,0 +1,128 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. 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.core.model.metadata.tables; + + +/******************************************************************************* + ** definition of a qqq table that is "associated" with another table, e.g., + ** managed along with it - such as child-records under a parent record. + *******************************************************************************/ +public class Association +{ + private String name; + private String associatedTableName; + private String joinName; + + + + /******************************************************************************* + ** Getter for name + *******************************************************************************/ + public String getName() + { + return (this.name); + } + + + + /******************************************************************************* + ** Setter for name + *******************************************************************************/ + public void setName(String name) + { + this.name = name; + } + + + + /******************************************************************************* + ** Fluent setter for name + *******************************************************************************/ + public Association withName(String name) + { + this.name = name; + return (this); + } + + + + /******************************************************************************* + ** Getter for associatedTableName + *******************************************************************************/ + public String getAssociatedTableName() + { + return (this.associatedTableName); + } + + + + /******************************************************************************* + ** Setter for associatedTableName + *******************************************************************************/ + public void setAssociatedTableName(String associatedTableName) + { + this.associatedTableName = associatedTableName; + } + + + + /******************************************************************************* + ** Fluent setter for associatedTableName + *******************************************************************************/ + public Association withAssociatedTableName(String associatedTableName) + { + this.associatedTableName = associatedTableName; + return (this); + } + + + + /******************************************************************************* + ** Getter for joinName + *******************************************************************************/ + public String getJoinName() + { + return (this.joinName); + } + + + + /******************************************************************************* + ** Setter for joinName + *******************************************************************************/ + public void setJoinName(String joinName) + { + this.joinName = joinName; + } + + + + /******************************************************************************* + ** Fluent setter for joinName + *******************************************************************************/ + public Association withJoinName(String joinName) + { + this.joinName = joinName; + return (this); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java index 73fd684f..a50e8c09 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java @@ -72,6 +72,7 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData private Map fields; private List uniqueKeys; + private List associations; private List recordSecurityLocks; private QPermissionRules permissionRules; @@ -1237,4 +1238,50 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData return (this); } + + + /******************************************************************************* + ** Getter for associations + *******************************************************************************/ + public List getAssociations() + { + return (this.associations); + } + + + + /******************************************************************************* + ** Setter for associations + *******************************************************************************/ + public void setAssociations(List associations) + { + this.associations = associations; + } + + + + /******************************************************************************* + ** Fluent setter for associations + *******************************************************************************/ + public QTableMetaData withAssociations(List associations) + { + this.associations = associations; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for associations + *******************************************************************************/ + public QTableMetaData withAssociation(Association association) + { + if(this.associations == null) + { + this.associations = new ArrayList<>(); + } + this.associations.add(association); + return (this); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteActionTest.java index 758471c1..4145d3bc 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteActionTest.java @@ -31,7 +31,10 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; 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.QFilterOrderBy; 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.core.model.audits.AuditsMetaDataProvider; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; @@ -149,4 +152,145 @@ class DeleteActionTest extends BaseTest assertTrue(audits.stream().allMatch(r -> r.getValueString("message").equals("Record was Deleted"))); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testAssociatedDeletes() throws QException + { + { + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(TestUtils.TABLE_NAME_ORDER); + insertInput.setRecords(List.of( + new QRecord().withValue("id", 1), + new QRecord().withValue("id", 2), + new QRecord().withValue("id", 3) + )); + new InsertAction().execute(insertInput); + } + + { + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(TestUtils.TABLE_NAME_ORDER_EXTRINSIC); + insertInput.setRecords(List.of( + new QRecord().withValue("id", 1).withValue("orderId", 1), + new QRecord().withValue("id", 2).withValue("orderId", 1), + new QRecord().withValue("id", 3).withValue("orderId", 1), + new QRecord().withValue("id", 4).withValue("orderId", 1), + new QRecord().withValue("id", 5).withValue("orderId", 3), + new QRecord().withValue("id", 6).withValue("orderId", 3), + new QRecord().withValue("id", 7).withValue("orderId", 3) + )); + new InsertAction().execute(insertInput); + } + + { + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM); + insertInput.setRecords(List.of( + new QRecord().withValue("id", 1).withValue("orderId", 1), + new QRecord().withValue("id", 2).withValue("orderId", 1), + new QRecord().withValue("id", 3).withValue("orderId", 2), + new QRecord().withValue("id", 4).withValue("orderId", 3), + new QRecord().withValue("id", 5).withValue("orderId", 3), + new QRecord().withValue("id", 6).withValue("orderId", 3) + )); + new InsertAction().execute(insertInput); + } + + { + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC); + insertInput.setRecords(List.of( + new QRecord().withValue("id", 1).withValue("lineItemId", 1), // orderId: 1 + new QRecord().withValue("id", 2).withValue("lineItemId", 1), // orderId: 1 + new QRecord().withValue("id", 3).withValue("lineItemId", 2), // orderId: 1 + new QRecord().withValue("id", 4).withValue("lineItemId", 2), // orderId: 1 + new QRecord().withValue("id", 5).withValue("lineItemId", 3), // orderId: 2 + new QRecord().withValue("id", 6).withValue("lineItemId", 3), // orderId: 2 + new QRecord().withValue("id", 7).withValue("lineItemId", 4), // orderId: 3 + new QRecord().withValue("id", 8).withValue("lineItemId", 5), // orderId: 3 + new QRecord().withValue("id", 9).withValue("lineItemId", 6) // orderId: 3 + )); + new InsertAction().execute(insertInput); + } + + ///////////////////////////////////////////////////////// + // assert about how many things we originally inserted // + ///////////////////////////////////////////////////////// + assertEquals(3, queryTable(TestUtils.TABLE_NAME_ORDER).size()); + assertEquals(7, queryTable(TestUtils.TABLE_NAME_ORDER_EXTRINSIC).size()); + assertEquals(6, queryTable(TestUtils.TABLE_NAME_LINE_ITEM).size()); + assertEquals(9, queryTable(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC).size()); + + //////////////////////////////// + // delete (cascading) order 1 // + //////////////////////////////// + DeleteInput deleteInput = new DeleteInput(); + deleteInput.setTableName(TestUtils.TABLE_NAME_ORDER); + deleteInput.setPrimaryKeys(List.of(1)); + new DeleteAction().execute(deleteInput); + + ////////////////////////////////////////////////// + // assert that the associated data were deleted // + ////////////////////////////////////////////////// + assertEquals(2, queryTable(TestUtils.TABLE_NAME_ORDER).size()); + assertEquals(3, queryTable(TestUtils.TABLE_NAME_ORDER_EXTRINSIC).size()); + assertEquals(4, queryTable(TestUtils.TABLE_NAME_LINE_ITEM).size()); + assertEquals(5, queryTable(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC).size()); + + //////////////////// + // delete order 2 // + //////////////////// + deleteInput.setPrimaryKeys(List.of(2)); + new DeleteAction().execute(deleteInput); + + ////////////////////////////////////////////////// + // assert that the associated data were deleted // + ////////////////////////////////////////////////// + assertEquals(1, queryTable(TestUtils.TABLE_NAME_ORDER).size()); + assertEquals(3, queryTable(TestUtils.TABLE_NAME_ORDER_EXTRINSIC).size()); + assertEquals(3, queryTable(TestUtils.TABLE_NAME_LINE_ITEM).size()); + assertEquals(3, queryTable(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC).size()); + + //////////////////// + // delete order 3 // + //////////////////// + deleteInput.setPrimaryKeys(List.of(3)); + new DeleteAction().execute(deleteInput); + + /////////////////////////////// + // everything is deleted now // + /////////////////////////////// + assertEquals(0, queryTable(TestUtils.TABLE_NAME_ORDER).size()); + assertEquals(0, queryTable(TestUtils.TABLE_NAME_ORDER_EXTRINSIC).size()); + assertEquals(0, queryTable(TestUtils.TABLE_NAME_LINE_ITEM).size()); + assertEquals(0, queryTable(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC).size()); + + //////////////////////////////////////////////// + // make sure no errors if we try more deletes // + //////////////////////////////////////////////// + deleteInput.setPrimaryKeys(List.of(3)); + new DeleteAction().execute(deleteInput); + + deleteInput.setPrimaryKeys(List.of(1, 2, 3, 4)); + new DeleteAction().execute(deleteInput); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static List queryTable(String tableName) throws QException + { + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(tableName); + queryInput.setFilter(new QQueryFilter().withOrderBy(new QFilterOrderBy("id"))); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + return (queryOutput.getRecords()); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/InsertActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/InsertActionTest.java index 958fdcb1..05127d15 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/InsertActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/InsertActionTest.java @@ -209,4 +209,78 @@ class InsertActionTest extends BaseTest assertTrue(CollectionUtils.nullSafeIsEmpty(insertOutput.getRecords().get(1).getErrors())); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testInsertAssociations() throws QException + { + QInstance qInstance = QContext.getQInstance(); + QContext.getQSession().withSecurityKeyValue("storeId", 1); + + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(TestUtils.TABLE_NAME_ORDER); + insertInput.setRecords(List.of( + new QRecord().withValue("storeId", 1).withValue("orderNo", "ORD123") + + .withAssociatedRecord("orderLine", new QRecord().withValue("sku", "BASIC1").withValue("quantity", 1) + .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "LINE-EXT-1.1").withValue("value", "LINE-VAL-1"))) + + .withAssociatedRecord("orderLine", new QRecord().withValue("sku", "BASIC2").withValue("quantity", 2) + .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "LINE-EXT-2.1").withValue("value", "LINE-VAL-2")) + .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "LINE-EXT-2.2").withValue("value", "LINE-VAL-3"))) + + .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "MY-FIELD-1").withValue("value", "MY-VALUE-1")) + .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "MY-FIELD-2").withValue("value", "MY-VALUE-2")) + .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "MY-FIELD-3").withValue("value", "MY-VALUE-3")), + + new QRecord().withValue("storeId", 1).withValue("orderNo", "ORD124") + .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "YOUR-FIELD-1").withValue("value", "YOUR-VALUE-1")) + )); + new InsertAction().execute(insertInput); + + List orders = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_ORDER); + assertEquals(2, orders.size()); + assertEquals(1, orders.get(0).getValueInteger("id")); + assertEquals(2, orders.get(1).getValueInteger("id")); + assertEquals("ORD123", orders.get(0).getValueString("orderNo")); + assertEquals("ORD124", orders.get(1).getValueString("orderNo")); + + List orderLines = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_LINE_ITEM); + assertEquals(2, orderLines.size()); + assertEquals(1, orderLines.get(0).getValueInteger("orderId")); + assertEquals(1, orderLines.get(1).getValueInteger("orderId")); + assertEquals("BASIC1", orderLines.get(0).getValueString("sku")); + assertEquals("BASIC2", orderLines.get(1).getValueString("sku")); + + List orderExtrinsics = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_ORDER_EXTRINSIC); + assertEquals(4, orderExtrinsics.size()); + assertEquals(1, orderExtrinsics.get(0).getValueInteger("orderId")); + assertEquals(1, orderExtrinsics.get(1).getValueInteger("orderId")); + assertEquals(1, orderExtrinsics.get(2).getValueInteger("orderId")); + assertEquals(2, orderExtrinsics.get(3).getValueInteger("orderId")); + assertEquals("MY-FIELD-1", orderExtrinsics.get(0).getValueString("key")); + assertEquals("MY-FIELD-2", orderExtrinsics.get(1).getValueString("key")); + assertEquals("MY-FIELD-3", orderExtrinsics.get(2).getValueString("key")); + assertEquals("YOUR-FIELD-1", orderExtrinsics.get(3).getValueString("key")); + assertEquals("MY-VALUE-1", orderExtrinsics.get(0).getValueString("value")); + assertEquals("MY-VALUE-2", orderExtrinsics.get(1).getValueString("value")); + assertEquals("MY-VALUE-3", orderExtrinsics.get(2).getValueString("value")); + assertEquals("YOUR-VALUE-1", orderExtrinsics.get(3).getValueString("value")); + + List lineItemExtrinsics = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC); + assertEquals(3, lineItemExtrinsics.size()); + assertEquals(1, lineItemExtrinsics.get(0).getValueInteger("lineItemId")); + assertEquals(2, lineItemExtrinsics.get(1).getValueInteger("lineItemId")); + assertEquals(2, lineItemExtrinsics.get(2).getValueInteger("lineItemId")); + assertEquals("LINE-EXT-1.1", lineItemExtrinsics.get(0).getValueString("key")); + assertEquals("LINE-EXT-2.1", lineItemExtrinsics.get(1).getValueString("key")); + assertEquals("LINE-EXT-2.2", lineItemExtrinsics.get(2).getValueString("key")); + assertEquals("LINE-VAL-1", lineItemExtrinsics.get(0).getValueString("value")); + assertEquals("LINE-VAL-2", lineItemExtrinsics.get(1).getValueString("value")); + assertEquals("LINE-VAL-3", lineItemExtrinsics.get(2).getValueString("value")); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java index 726cf2c9..cbeabf93 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java @@ -91,6 +91,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.reporting.ReportType; import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock; import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType; import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; +import com.kingsrook.qqq.backend.core.model.metadata.tables.Association; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTracking; @@ -126,10 +127,12 @@ public class TestUtils public static final String APP_NAME_PEOPLE = "peopleApp"; public static final String APP_NAME_MISCELLANEOUS = "miscellaneous"; - public static final String TABLE_NAME_PERSON = "person"; - public static final String TABLE_NAME_SHAPE = "shape"; - public static final String TABLE_NAME_ORDER = "order"; - public static final String TABLE_NAME_LINE_ITEM = "orderLine"; + public static final String TABLE_NAME_PERSON = "person"; + public static final String TABLE_NAME_SHAPE = "shape"; + public static final String TABLE_NAME_ORDER = "order"; + public static final String TABLE_NAME_LINE_ITEM = "orderLine"; + public static final String TABLE_NAME_LINE_ITEM_EXTRINSIC = "orderLineExtrinsic"; + public static final String TABLE_NAME_ORDER_EXTRINSIC = "orderExtrinsic"; public static final String PROCESS_NAME_GREET_PEOPLE = "greet"; public static final String PROCESS_NAME_GREET_PEOPLE_INTERACTIVE = "greetInteractive"; @@ -185,8 +188,12 @@ public class TestUtils qInstance.addTable(defineTableBasepull()); qInstance.addTable(defineTableOrder()); qInstance.addTable(defineTableLineItem()); + qInstance.addTable(defineTableLineItemExtrinsic()); + qInstance.addTable(defineTableOrderExtrinsic()); qInstance.addJoin(defineJoinOrderLineItem()); + qInstance.addJoin(defineJoinLineItemLineItemExtrinsic()); + qInstance.addJoin(defineJoinOrderOrderExtrinsic()); qInstance.addPossibleValueSource(defineAutomationStatusPossibleValueSource()); qInstance.addPossibleValueSource(defineStatesPossibleValueSource()); @@ -538,9 +545,12 @@ public class TestUtils .withRecordSecurityLock(new RecordSecurityLock() .withSecurityKeyType(SECURITY_KEY_TYPE_STORE) .withFieldName("storeId")) + .withAssociation(new Association().withName("orderLine").withAssociatedTableName(TABLE_NAME_LINE_ITEM).withJoinName("orderLineItem")) + .withAssociation(new Association().withName("extrinsics").withAssociatedTableName(TABLE_NAME_ORDER_EXTRINSIC).withJoinName("orderOrderExtrinsic")) .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false)) .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false)) + .withField(new QFieldMetaData("orderNo", QFieldType.STRING)) .withField(new QFieldMetaData("orderDate", QFieldType.DATE)) .withField(new QFieldMetaData("storeId", QFieldType.INTEGER)) .withField(new QFieldMetaData("total", QFieldType.DECIMAL).withDisplayFormat(DisplayFormat.CURRENCY).withFieldSecurityLock(new FieldSecurityLock() @@ -561,6 +571,7 @@ public class TestUtils .withName(TABLE_NAME_LINE_ITEM) .withBackendName(MEMORY_BACKEND_NAME) .withPrimaryKeyField("id") + .withAssociation(new Association().withName("extrinsics").withAssociatedTableName(TABLE_NAME_LINE_ITEM_EXTRINSIC).withJoinName("lineItemLineItemExtrinsic")) .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false)) .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false)) @@ -572,6 +583,44 @@ public class TestUtils + /******************************************************************************* + ** Define the lineItemExtrinsic table used in standard tests. + *******************************************************************************/ + public static QTableMetaData defineTableLineItemExtrinsic() + { + return new QTableMetaData() + .withName(TABLE_NAME_LINE_ITEM_EXTRINSIC) + .withBackendName(MEMORY_BACKEND_NAME) + .withPrimaryKeyField("id") + .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) + .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false)) + .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false)) + .withField(new QFieldMetaData("lineItemId", QFieldType.INTEGER)) + .withField(new QFieldMetaData("key", QFieldType.STRING)) + .withField(new QFieldMetaData("value", QFieldType.STRING)); + } + + + + /******************************************************************************* + ** Define the orderExtrinsic table used in standard tests. + *******************************************************************************/ + public static QTableMetaData defineTableOrderExtrinsic() + { + return new QTableMetaData() + .withName(TABLE_NAME_ORDER_EXTRINSIC) + .withBackendName(MEMORY_BACKEND_NAME) + .withPrimaryKeyField("id") + .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) + .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false)) + .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false)) + .withField(new QFieldMetaData("orderId", QFieldType.INTEGER)) + .withField(new QFieldMetaData("key", QFieldType.STRING)) + .withField(new QFieldMetaData("value", QFieldType.STRING)); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -588,6 +637,38 @@ public class TestUtils + /******************************************************************************* + ** + *******************************************************************************/ + public static QJoinMetaData defineJoinLineItemLineItemExtrinsic() + { + return new QJoinMetaData() + .withName("lineItemLineItemExtrinsic") + .withType(JoinType.ONE_TO_MANY) + .withLeftTable(TABLE_NAME_LINE_ITEM) + .withRightTable(TABLE_NAME_LINE_ITEM_EXTRINSIC) + .withJoinOn(new JoinOn("id", "lineItemId")) + .withOrderBy(new QFilterOrderBy("key")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QJoinMetaData defineJoinOrderOrderExtrinsic() + { + return new QJoinMetaData() + .withName("orderOrderExtrinsic") + .withType(JoinType.ONE_TO_MANY) + .withLeftTable(TABLE_NAME_ORDER) + .withRightTable(TABLE_NAME_ORDER_EXTRINSIC) + .withJoinOn(new JoinOn("id", "orderId")) + .withOrderBy(new QFilterOrderBy("key")); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java index 5ea168d6..45b54fcf 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import com.kingsrook.qqq.api.model.APIVersion; import com.kingsrook.qqq.api.model.APIVersionRange; import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecInput; @@ -66,8 +67,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; +import com.kingsrook.qqq.backend.core.model.metadata.tables.Association; import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.YamlUtils; @@ -204,7 +207,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction tableApiFields = new GetTableApiFieldsAction().execute(new GetTableApiFieldsInput().withTableName(tableName).withVersion(version)).getFields(); + /////////////////////////////// + // permissions for the table // + /////////////////////////////// String tableReadPermissionName = PermissionsHelper.getTablePermissionName(tableName, TablePermissionSubType.READ); if(StringUtils.hasContent(tableReadPermissionName)) { @@ -256,13 +262,15 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction tableFieldsWithoutPrimaryKey = new LinkedHashMap<>(); - componentSchemas.put(tableApiName + "WithoutPrimaryKey", new Schema() + Schema tableWithoutPrimaryKeySchema = new Schema() .withType("object") - .withProperties(tableFieldsWithoutPrimaryKey)); + .withProperties(tableFieldsWithoutPrimaryKey); + componentSchemas.put(tableApiName + "WithoutPrimaryKey", tableWithoutPrimaryKeySchema); for(QFieldMetaData field : tableApiFields) { @@ -271,46 +279,26 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction enumValues = new ArrayList<>(); - for(QPossibleValue enumValue : possibleValueSource.getEnumValues()) - { - enumValues.add(enumValue.getId() + "=" + enumValue.getLabel()); - } - fieldSchema.setEnumValues(enumValues); - } - else if(QPossibleValueSourceType.TABLE.equals(possibleValueSource.getType())) - { - QTableMetaData sourceTable = qInstance.getTable(possibleValueSource.getTableName()); - fieldSchema.setDescription(fieldSchema.getDescription() + " Values in this field come from the primary key of the " + sourceTable.getLabel() + " table"); - } - } - - tableFieldsWithoutPrimaryKey.put(apiFieldName, fieldSchema); + Schema fieldSchema = getFieldSchema(table, field); + tableFieldsWithoutPrimaryKey.put(ApiFieldMetaData.getEffectiveApiFieldName(field), fieldSchema); } + ////////////////////////////////// + // recursively add associations // + ////////////////////////////////// + addAssociations(table, tableWithoutPrimaryKeySchema); + + ///////////////////////////////////////////////// + // full version of table (w/o pkey + the pkey) // + ///////////////////////////////////////////////// componentSchemas.put(tableApiName, new Schema() .withType("object") .withAllOf(ListBuilder.of(new Schema().withRef("#/components/schemas/" + tableApiName + "WithoutPrimaryKey"))) - .withProperties(MapBuilder.of( - primaryKeyApiName, new Schema() - .withType(getFieldType(table.getField(primaryKeyName))) - .withFormat(getFieldFormat(table.getField(primaryKeyName))) - .withDescription(primaryKeyLabel + " for the " + tableLabel + ". Primary Key.") - )) - ); + .withProperties(MapBuilder.of(primaryKeyApiName, getFieldSchema(table, table.getField(primaryKeyName))))); + ////////////////////////////////////////////////////////////////////////////// + // table as a search result (the base search result, plus the table itself) // + ////////////////////////////////////////////////////////////////////////////// componentSchemas.put(tableApiName + "SearchResult", new Schema() .withType("object") .withAllOf(ListBuilder.of(new Schema().withRef("#/components/schemas/baseSearchResultFields"))) @@ -577,6 +565,59 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction enumValues = new ArrayList<>(); + for(QPossibleValue enumValue : possibleValueSource.getEnumValues()) + { + enumValues.add(enumValue.getId() + "=" + enumValue.getLabel()); + } + fieldSchema.setEnumValues(enumValues); + } + else if(QPossibleValueSourceType.TABLE.equals(possibleValueSource.getType())) + { + QTableMetaData sourceTable = QContext.getQInstance().getTable(possibleValueSource.getTableName()); + fieldSchema.setDescription(fieldSchema.getDescription() + " Values in this field come from the primary key of the " + sourceTable.getLabel() + " table"); + } + } + return fieldSchema; + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/QRecordApiAdapter.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/QRecordApiAdapter.java index 7862ed31..5eb59976 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/QRecordApiAdapter.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/QRecordApiAdapter.java @@ -32,11 +32,16 @@ import java.util.stream.Collectors; import com.kingsrook.qqq.api.javalin.QBadRequestException; import com.kingsrook.qqq.api.model.actions.GetTableApiFieldsInput; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; +import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.Association; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.Pair; import com.kingsrook.qqq.backend.core.utils.StringUtils; +import org.json.JSONArray; import org.json.JSONObject; @@ -94,6 +99,13 @@ public class QRecordApiAdapter List unrecognizedFieldNames = new ArrayList<>(); QRecord qRecord = new QRecord(); + Map associationMap = new HashMap<>(); + QTableMetaData table = QContext.getQInstance().getTable(tableName); + for(Association association : CollectionUtils.nonNullList(table.getAssociations())) + { + associationMap.put(association.getName(), association); + } + ////////////////////////////////////////// // iterate over keys in the json object // ////////////////////////////////////////// @@ -117,6 +129,30 @@ public class QRecordApiAdapter qRecord.setValue(field.getName(), value); } } + else if(associationMap.containsKey(jsonKey)) + { + Association association = associationMap.get(jsonKey); + Object value = jsonObject.get(jsonKey); + if(value instanceof JSONArray jsonArray) + { + for(Object subObject : jsonArray) + { + if(subObject instanceof JSONObject subJsonObject) + { + QRecord subRecord = apiJsonObjectToQRecord(subJsonObject, association.getAssociatedTableName(), apiVersion); + qRecord.withAssociatedRecord(association.getName(), subRecord); + } + else + { + throw (new QBadRequestException("Found a " + value.getClass().getSimpleName() + " in the array under key " + jsonKey + ", but a JSON object is required here.")); + } + } + } + else + { + throw (new QBadRequestException("Found a " + value.getClass().getSimpleName() + " at key " + jsonKey + ", but a JSON array is required here.")); + } + } else { /////////////////////////////////////////////////// diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java index 10612098..dba52b15 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java @@ -76,6 +76,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.ExceptionUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils; @@ -253,7 +254,7 @@ public class QJavalinApiHandler String version = context.pathParam("version"); GenerateOpenApiSpecInput input = new GenerateOpenApiSpecInput().withVersion(version); - if(context.pathParam("tableName") != null) + if(StringUtils.hasContent(context.pathParam("tableName"))) { input.setTableName(context.pathParam("tableName")); } @@ -273,9 +274,10 @@ public class QJavalinApiHandler /******************************************************************************* ** *******************************************************************************/ - public static void setupSession(Context context, AbstractActionInput input) throws QModuleDispatchException, QAuthenticationException + public static void setupSession(Context context, AbstractActionInput input, String version) throws QModuleDispatchException, QAuthenticationException { - QJavalinImplementation.setupSession(context, input); + QSession session = QJavalinImplementation.setupSession(context, input); + session.setValue("apiVersion", version); } @@ -296,8 +298,8 @@ public class QJavalinApiHandler GetInput getInput = new GetInput(); - setupSession(context, getInput); - QJavalinAccessLogger.logStart("get", logPair("table", tableName), logPair("primaryKey", primaryKey)); + setupSession(context, getInput, version); + QJavalinAccessLogger.logStart("apiGet", logPair("table", tableName), logPair("primaryKey", primaryKey)); getInput.setTableName(tableName); // i think not for api... getInput.setShouldGenerateDisplayValues(true); @@ -354,7 +356,7 @@ public class QJavalinApiHandler String tableName = table.getName(); QueryInput queryInput = new QueryInput(); - setupSession(context, queryInput); + setupSession(context, queryInput, version); QJavalinAccessLogger.logStart("apiQuery", logPair("table", tableName)); queryInput.setTableName(tableName); @@ -785,8 +787,8 @@ public class QJavalinApiHandler InsertInput insertInput = new InsertInput(); - setupSession(context, insertInput); - QJavalinAccessLogger.logStart("insert", logPair("table", tableName)); + setupSession(context, insertInput, version); + QJavalinAccessLogger.logStart("apiInsert", logPair("table", tableName)); insertInput.setTableName(tableName); @@ -852,8 +854,8 @@ public class QJavalinApiHandler InsertInput insertInput = new InsertInput(); - setupSession(context, insertInput); - QJavalinAccessLogger.logStart("bulkInsert", logPair("table", tableName)); + setupSession(context, insertInput, version); + QJavalinAccessLogger.logStart("apiBulkInsert", logPair("table", tableName)); insertInput.setTableName(tableName); @@ -958,8 +960,8 @@ public class QJavalinApiHandler UpdateInput updateInput = new UpdateInput(); - setupSession(context, updateInput); - QJavalinAccessLogger.logStart("bulkUpdate", logPair("table", tableName)); + setupSession(context, updateInput, version); + QJavalinAccessLogger.logStart("apiBulkUpdate", logPair("table", tableName)); updateInput.setTableName(tableName); @@ -1063,8 +1065,8 @@ public class QJavalinApiHandler DeleteInput deleteInput = new DeleteInput(); - setupSession(context, deleteInput); - QJavalinAccessLogger.logStart("bulkDelete", logPair("table", tableName)); + setupSession(context, deleteInput, version); + QJavalinAccessLogger.logStart("apiBulkDelete", logPair("table", tableName)); deleteInput.setTableName(tableName); @@ -1182,8 +1184,8 @@ public class QJavalinApiHandler UpdateInput updateInput = new UpdateInput(); - setupSession(context, updateInput); - QJavalinAccessLogger.logStart("update", logPair("table", tableName)); + setupSession(context, updateInput, version); + QJavalinAccessLogger.logStart("apiUpdate", logPair("table", tableName)); updateInput.setTableName(tableName); @@ -1268,8 +1270,8 @@ public class QJavalinApiHandler DeleteInput deleteInput = new DeleteInput(); - setupSession(context, deleteInput); - QJavalinAccessLogger.logStart("delete", logPair("table", tableName)); + setupSession(context, deleteInput, version); + QJavalinAccessLogger.logStart("apiDelete", logPair("table", tableName)); deleteInput.setTableName(tableName); deleteInput.setPrimaryKeys(List.of(primaryKey)); diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java index 6517d1c6..f85df985 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java @@ -27,6 +27,7 @@ import com.kingsrook.qqq.api.model.APIVersion; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; @@ -34,6 +35,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.authentication.Auth0Authent import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn; +import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType; +import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.Association; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule; @@ -45,10 +50,15 @@ import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.Mem public class TestUtils { public static final String MEMORY_BACKEND_NAME = "memory"; - public static final String TABLE_NAME_PERSON = "person"; - public static final String V2023_Q1 = "2023.Q1"; + public static final String TABLE_NAME_PERSON = "person"; + public static final String TABLE_NAME_ORDER = "order"; + public static final String TABLE_NAME_LINE_ITEM = "orderLine"; + public static final String TABLE_NAME_LINE_ITEM_EXTRINSIC = "orderLineExtrinsic"; + public static final String TABLE_NAME_ORDER_EXTRINSIC = "orderExtrinsic"; + public static final String V2022_Q4 = "2022.Q4"; + public static final String V2023_Q1 = "2023.Q1"; public static final String V2023_Q2 = "2023.Q2"; public static final String CURRENT_API_VERSION = V2023_Q1; @@ -64,6 +74,15 @@ public class TestUtils qInstance.addBackend(defineMemoryBackend()); qInstance.addTable(defineTablePerson()); + qInstance.addTable(defineTableOrder()); + qInstance.addTable(defineTableLineItem()); + qInstance.addTable(defineTableLineItemExtrinsic()); + qInstance.addTable(defineTableOrderExtrinsic()); + + qInstance.addJoin(defineJoinOrderLineItem()); + qInstance.addJoin(defineJoinLineItemLineItemExtrinsic()); + qInstance.addJoin(defineJoinOrderOrderExtrinsic()); + qInstance.setAuthentication(new Auth0AuthenticationMetaData().withType(QAuthenticationType.FULLY_ANONYMOUS).withName("anonymous")); qInstance.withMiddlewareMetaData(new ApiInstanceMetaData() @@ -139,4 +158,137 @@ public class TestUtils ; } + + + /******************************************************************************* + ** Define the order table used in standard tests. + *******************************************************************************/ + public static QTableMetaData defineTableOrder() + { + return new QTableMetaData() + .withName(TABLE_NAME_ORDER) + .withBackendName(MEMORY_BACKEND_NAME) + .withMiddlewareMetaData(new ApiTableMetaData().withInitialVersion(V2022_Q4)) + .withPrimaryKeyField("id") + .withAssociation(new Association().withName("orderLines").withAssociatedTableName(TABLE_NAME_LINE_ITEM).withJoinName("orderLineItem")) + .withAssociation(new Association().withName("extrinsics").withAssociatedTableName(TABLE_NAME_ORDER_EXTRINSIC).withJoinName("orderOrderExtrinsic")) + .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) + .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false)) + .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false)) + .withField(new QFieldMetaData("orderNo", QFieldType.STRING)) + .withField(new QFieldMetaData("orderDate", QFieldType.DATE)) + .withField(new QFieldMetaData("storeId", QFieldType.INTEGER)) + .withField(new QFieldMetaData("total", QFieldType.DECIMAL).withDisplayFormat(DisplayFormat.CURRENCY)); + } + + + + /******************************************************************************* + ** Define the lineItem table used in standard tests. + *******************************************************************************/ + public static QTableMetaData defineTableLineItem() + { + return new QTableMetaData() + .withName(TABLE_NAME_LINE_ITEM) + .withBackendName(MEMORY_BACKEND_NAME) + .withMiddlewareMetaData(new ApiTableMetaData().withInitialVersion(V2022_Q4)) + .withPrimaryKeyField("id") + .withAssociation(new Association().withName("extrinsics").withAssociatedTableName(TABLE_NAME_LINE_ITEM_EXTRINSIC).withJoinName("lineItemLineItemExtrinsic")) + .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) + .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false)) + .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false)) + .withField(new QFieldMetaData("orderId", QFieldType.INTEGER)) + .withField(new QFieldMetaData("lineNumber", QFieldType.STRING)) + .withField(new QFieldMetaData("sku", QFieldType.STRING)) + .withField(new QFieldMetaData("quantity", QFieldType.INTEGER)); + } + + + + /******************************************************************************* + ** Define the lineItemExtrinsic table used in standard tests. + *******************************************************************************/ + public static QTableMetaData defineTableLineItemExtrinsic() + { + return new QTableMetaData() + .withName(TABLE_NAME_LINE_ITEM_EXTRINSIC) + .withBackendName(MEMORY_BACKEND_NAME) + .withMiddlewareMetaData(new ApiTableMetaData().withInitialVersion(V2022_Q4)) + .withPrimaryKeyField("id") + .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) + .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false)) + .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false)) + .withField(new QFieldMetaData("lineItemId", QFieldType.INTEGER)) + .withField(new QFieldMetaData("key", QFieldType.STRING)) + .withField(new QFieldMetaData("value", QFieldType.STRING)); + } + + + + /******************************************************************************* + ** Define the orderExtrinsic table used in standard tests. + *******************************************************************************/ + public static QTableMetaData defineTableOrderExtrinsic() + { + return new QTableMetaData() + .withName(TABLE_NAME_ORDER_EXTRINSIC) + .withBackendName(MEMORY_BACKEND_NAME) + .withMiddlewareMetaData(new ApiTableMetaData().withInitialVersion(V2022_Q4)) + .withPrimaryKeyField("id") + .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) + .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false)) + .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false)) + .withField(new QFieldMetaData("orderId", QFieldType.INTEGER)) + .withField(new QFieldMetaData("key", QFieldType.STRING)) + .withField(new QFieldMetaData("value", QFieldType.STRING)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QJoinMetaData defineJoinOrderLineItem() + { + return new QJoinMetaData() + .withName("orderLineItem") + .withType(JoinType.ONE_TO_MANY) + .withLeftTable(TABLE_NAME_ORDER) + .withRightTable(TABLE_NAME_LINE_ITEM) + .withJoinOn(new JoinOn("id", "orderId")) + .withOrderBy(new QFilterOrderBy("lineNumber")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QJoinMetaData defineJoinLineItemLineItemExtrinsic() + { + return new QJoinMetaData() + .withName("lineItemLineItemExtrinsic") + .withType(JoinType.ONE_TO_MANY) + .withLeftTable(TABLE_NAME_LINE_ITEM) + .withRightTable(TABLE_NAME_LINE_ITEM_EXTRINSIC) + .withJoinOn(new JoinOn("id", "lineItemId")) + .withOrderBy(new QFilterOrderBy("key")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QJoinMetaData defineJoinOrderOrderExtrinsic() + { + return new QJoinMetaData() + .withName("orderOrderExtrinsic") + .withType(JoinType.ONE_TO_MANY) + .withLeftTable(TABLE_NAME_ORDER) + .withRightTable(TABLE_NAME_ORDER_EXTRINSIC) + .withJoinOn(new JoinOn("id", "orderId")) + .withOrderBy(new QFilterOrderBy("key")); + } + } diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java index ec3de2d3..1cde9d5a 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java @@ -37,6 +37,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; 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.QFilterOrderBy; 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; @@ -59,6 +60,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; /******************************************************************************* @@ -407,12 +409,55 @@ class QJavalinApiHandlerTest extends BaseTest JSONObject jsonObject = new JSONObject(response.getBody()); assertEquals(1, jsonObject.getInt("id")); - QRecord record = getPersonRecord(1); + QRecord record = getRecord(TestUtils.TABLE_NAME_PERSON, 1); assertEquals("Moe", record.getValueString("firstName")); } + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testInsert201WithAssociatedRecords() throws QException + { + HttpResponse response = Unirest.post(BASE_URL + "/api/" + VERSION + "/order/") + .body(""" + {"orderNo": "ORD123", "storeId": 47, "orderLines": + [ + {"lineNumber": 1, "sku": "BASIC1", "quantity": 17, "extrinsics": [{"key": "size", "value": "Large"}]}, + {"lineNumber": 2, "sku": "BASIC2", "quantity": 23} + ], "extrinsics": + [ + {"key": "storeName", "value": "My Shopify"}, + {"key": "shopifyOrderNo", "value": "#2820503"} + ] + } + """) + .asString(); + System.out.println(response.getBody()); + assertEquals(HttpStatus.CREATED_201, response.getStatus()); + JSONObject jsonObject = new JSONObject(response.getBody()); + assertEquals(1, jsonObject.getInt("id")); + + QRecord record = getRecord(TestUtils.TABLE_NAME_ORDER, 1); + assertEquals("ORD123", record.getValueString("orderNo")); + + List lines = queryTable(TestUtils.TABLE_NAME_LINE_ITEM); + assertEquals(2, lines.size()); + assertTrue(lines.stream().allMatch(r -> r.getValueInteger("orderId").equals(1))); + assertTrue(lines.stream().anyMatch(r -> r.getValueString("sku").equals("BASIC1"))); + assertTrue(lines.stream().anyMatch(r -> r.getValueString("sku").equals("BASIC2"))); + + List orderExtrinsics = queryTable(TestUtils.TABLE_NAME_ORDER_EXTRINSIC); + assertEquals(2, orderExtrinsics.size()); + assertTrue(orderExtrinsics.stream().allMatch(r -> r.getValueInteger("orderId").equals(1))); + assertTrue(orderExtrinsics.stream().anyMatch(r -> r.getValueString("key").equals("storeName") && r.getValueString("value").equals("My Shopify"))); + assertTrue(orderExtrinsics.stream().anyMatch(r -> r.getValueString("key").equals("shopifyOrderNo") && r.getValueString("value").equals("#2820503"))); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -462,7 +507,7 @@ class QJavalinApiHandlerTest extends BaseTest /////////////////////////////////// // assert it didn't get inserted // /////////////////////////////////// - QRecord personRecord = getPersonRecord(1); + QRecord personRecord = getRecord(TestUtils.TABLE_NAME_PERSON, 1); assertNull(personRecord); /////////////////////////////////////////// @@ -511,16 +556,16 @@ class QJavalinApiHandlerTest extends BaseTest assertEquals(HttpStatus.BAD_REQUEST_400, jsonArray.getJSONObject(3).getInt("statusCode")); assertEquals("Error inserting Person: Another record already exists with this Email", jsonArray.getJSONObject(3).getString("error")); - QRecord record = getPersonRecord(1); + QRecord record = getRecord(TestUtils.TABLE_NAME_PERSON, 1); assertEquals("Moe", record.getValueString("firstName")); - record = getPersonRecord(2); + record = getRecord(TestUtils.TABLE_NAME_PERSON, 2); assertEquals("Barney", record.getValueString("firstName")); - record = getPersonRecord(3); + record = getRecord(TestUtils.TABLE_NAME_PERSON, 3); assertEquals("CM", record.getValueString("firstName")); - record = getPersonRecord(4); + record = getRecord(TestUtils.TABLE_NAME_PERSON, 4); assertNull(record); } @@ -559,7 +604,7 @@ class QJavalinApiHandlerTest extends BaseTest ///////////////////////////////// // assert nothing got inserted // ///////////////////////////////// - QRecord personRecord = getPersonRecord(1); + QRecord personRecord = getRecord(TestUtils.TABLE_NAME_PERSON, 1); assertNull(personRecord); ////////////////////////////////////////// @@ -592,7 +637,7 @@ class QJavalinApiHandlerTest extends BaseTest assertEquals(HttpStatus.NO_CONTENT_204, response.getStatus()); assertFalse(StringUtils.hasContent(response.getBody())); - QRecord record = getPersonRecord(1); + QRecord record = getRecord(TestUtils.TABLE_NAME_PERSON, 1); assertEquals("Charles", record.getValueString("firstName")); } @@ -665,7 +710,7 @@ class QJavalinApiHandlerTest extends BaseTest /////////////////////////////////// // assert it didn't get updated. // /////////////////////////////////// - QRecord personRecord = getPersonRecord(1); + QRecord personRecord = getRecord(TestUtils.TABLE_NAME_PERSON, 1); assertEquals("Mo", personRecord.getValueString("firstName")); response = Unirest.patch(BASE_URL + "/api/" + VERSION + "/person/1") @@ -706,10 +751,10 @@ class QJavalinApiHandlerTest extends BaseTest assertEquals(HttpStatus.BAD_REQUEST_400, jsonArray.getJSONObject(2).getInt("statusCode")); assertEquals("Error updating Person: Missing value in primary key field", jsonArray.getJSONObject(2).getString("error")); - QRecord record = getPersonRecord(1); + QRecord record = getRecord(TestUtils.TABLE_NAME_PERSON, 1); assertEquals("homer@simpson.com", record.getValueString("email")); - record = getPersonRecord(2); + record = getRecord(TestUtils.TABLE_NAME_PERSON, 2); assertEquals("marge@simpson.com", record.getValueString("email")); QueryInput queryInput = new QueryInput(); @@ -754,7 +799,7 @@ class QJavalinApiHandlerTest extends BaseTest //////////////////////////////// // assert nothing got updated // //////////////////////////////// - QRecord personRecord = getPersonRecord(1); + QRecord personRecord = getRecord(TestUtils.TABLE_NAME_PERSON, 1); assertNull(personRecord); ////////////////////////////////////////// @@ -834,7 +879,7 @@ class QJavalinApiHandlerTest extends BaseTest //////////////////////////////// // assert nothing got deleted // //////////////////////////////// - QRecord personRecord = getPersonRecord(1); + QRecord personRecord = getRecord(TestUtils.TABLE_NAME_PERSON, 1); assertNull(personRecord); ////////////////////////////////////////// @@ -877,7 +922,7 @@ class QJavalinApiHandlerTest extends BaseTest assertEquals(HttpStatus.NO_CONTENT_204, response.getStatus()); assertFalse(StringUtils.hasContent(response.getBody())); - QRecord record = getPersonRecord(1); + QRecord record = getRecord(TestUtils.TABLE_NAME_PERSON, 1); assertNull(record); } @@ -895,6 +940,43 @@ class QJavalinApiHandlerTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testDeleteAssociations() throws QException + { + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(TestUtils.TABLE_NAME_ORDER); + insertInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("orderNo", "ORD123").withValue("storeId", 47) + .withAssociatedRecord("orderLines", new QRecord().withValue("lineNumber", 1).withValue("sku", "BASIC1").withValue("quantity", 42) + .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "Size").withValue("value", "Medium")) + .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "Discount").withValue("value", "3.50")) + .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "Color").withValue("value", "Red"))) + .withAssociatedRecord("orderLines", new QRecord().withValue("lineNumber", 2).withValue("sku", "BASIC2").withValue("quantity", 42) + .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "Size").withValue("value", "Medium"))) + .withAssociatedRecord("orderLines", new QRecord().withValue("lineNumber", 3).withValue("sku", "BASIC3").withValue("quantity", 42)) + .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "shopifyOrderNo").withValue("value", "#1032")) + )); + new InsertAction().execute(insertInput); + + assertEquals(1, queryTable(TestUtils.TABLE_NAME_ORDER).size()); + assertEquals(4, queryTable(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC).size()); + assertEquals(3, queryTable(TestUtils.TABLE_NAME_LINE_ITEM).size()); + assertEquals(1, queryTable(TestUtils.TABLE_NAME_ORDER_EXTRINSIC).size()); + + HttpResponse response = Unirest.delete(BASE_URL + "/api/" + VERSION + "/order/1").asString(); + assertEquals(HttpStatus.NO_CONTENT_204, response.getStatus()); + assertFalse(StringUtils.hasContent(response.getBody())); + + assertEquals(0, queryTable(TestUtils.TABLE_NAME_ORDER).size()); + assertEquals(0, queryTable(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC).size()); + assertEquals(0, queryTable(TestUtils.TABLE_NAME_LINE_ITEM).size()); + assertEquals(0, queryTable(TestUtils.TABLE_NAME_ORDER_EXTRINSIC).size()); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -919,10 +1001,10 @@ class QJavalinApiHandlerTest extends BaseTest /******************************************************************************* ** *******************************************************************************/ - private static QRecord getPersonRecord(Integer id) throws QException + private static QRecord getRecord(String tableName, Integer id) throws QException { GetInput getInput = new GetInput(); - getInput.setTableName(TestUtils.TABLE_NAME_PERSON); + getInput.setTableName(tableName); getInput.setPrimaryKey(id); GetOutput getOutput = new GetAction().execute(getInput); QRecord record = getOutput.getRecord(); @@ -931,6 +1013,20 @@ class QJavalinApiHandlerTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + private static List queryTable(String tableName) throws QException + { + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(tableName); + queryInput.setFilter(new QQueryFilter().withOrderBy(new QFilterOrderBy("id"))); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + return (queryOutput.getRecords()); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index 4adde09e..43b12f80 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -411,7 +411,7 @@ public class QJavalinImplementation /******************************************************************************* ** *******************************************************************************/ - public static void setupSession(Context context, AbstractActionInput input) throws QModuleDispatchException, QAuthenticationException + public static QSession setupSession(Context context, AbstractActionInput input) throws QModuleDispatchException, QAuthenticationException { QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher(); QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(qInstance.getAuthentication()); @@ -466,6 +466,8 @@ public class QJavalinImplementation setUserTimezoneOffsetMinutesInSession(context, session); setUserTimezoneInSession(context, session); + + return (session); } catch(QAuthenticationException qae) {