diff --git a/qqq-middleware-api/pom.xml b/qqq-middleware-api/pom.xml index 5a2d3c6a..b9572da8 100644 --- a/qqq-middleware-api/pom.xml +++ b/qqq-middleware-api/pom.xml @@ -54,6 +54,28 @@ ${revision} + + + com.kingsrook.qqq + qqq-middleware-javalin + ${revision} + tests + test-jar + test + + + com.kingsrook.qqq + qqq-backend-module-rdbms + ${revision} + test + + + com.h2database + h2 + 2.2.220 + test + + com.konghq @@ -106,4 +128,4 @@ - \ No newline at end of file + diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java index 2525263f..f92c8716 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java @@ -338,7 +338,7 @@ public class ApiImplementation else if(apiFieldMetaData.getCustomValueMapper() != null) { ApiFieldCustomValueMapper customValueMapper = QCodeLoader.getAdHoc(ApiFieldCustomValueMapper.class, apiFieldMetaData.getCustomValueMapper()); - customValueMapper.customizeFilterCriteria(queryInput, filter, criteria, name, apiFieldMetaData); + customValueMapper.customizeFilterCriteriaForQueryOrCount(queryInput, filter, criteria, name, apiFieldMetaData); } filter.addCriteria(criteria); @@ -389,8 +389,14 @@ public class ApiImplementation ///////////////////////////// if(includeCount) { + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // todo - at one time we wondered if we might need a call to customValueMapper.customizeFilterCriteriaForQueryOrCount // + // as the filter would have already gone through there, but not other attributes of the input, e.g, joins... // + // but, instead we're trying to just put the query joins in here FROM the query input... // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// CountInput countInput = new CountInput(); countInput.setTableName(tableName); + countInput.setQueryJoins(queryInput.getQueryJoins()); countInput.setFilter(filter); countInput.withQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND); CountOutput countOutput = new CountAction().execute(countInput); diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GetTableApiFieldsAction.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GetTableApiFieldsAction.java index 02c585bb..86332a51 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GetTableApiFieldsAction.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GetTableApiFieldsAction.java @@ -31,6 +31,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import com.kingsrook.qqq.api.model.APIVersion; +import com.kingsrook.qqq.api.model.APIVersionRange; import com.kingsrook.qqq.api.model.actions.GetTableApiFieldsInput; import com.kingsrook.qqq.api.model.actions.GetTableApiFieldsOutput; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; @@ -39,10 +40,13 @@ import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; +import com.kingsrook.qqq.backend.core.logging.QLogger; 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.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.ObjectUtils; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; /******************************************************************************* @@ -51,6 +55,8 @@ import com.kingsrook.qqq.backend.core.utils.ObjectUtils; *******************************************************************************/ public class GetTableApiFieldsAction extends AbstractQActionFunction { + private static final QLogger LOG = QLogger.getLogger(GetTableApiFieldsAction.class); + private static Map> fieldListCache = new HashMap<>(); private static Map> fieldMapCache = new HashMap<>(); @@ -141,13 +147,16 @@ public class GetTableApiFieldsAction extends AbstractQActionFunction> qRecordsToApiMapList(List records, String tableName, String apiName, String apiVersion) throws QException + { + Map fieldValueMappers = getFieldValueMappers(records, tableName, apiName, apiVersion); + + ArrayList> rs = new ArrayList<>(); + for(QRecord record : records) + { + ApiOutputMapWrapper apiOutputMap = qRecordToApiMap(record, tableName, apiName, apiVersion, fieldValueMappers, new ApiOutputMapWrapper(new LinkedHashMap<>())); + rs.add(apiOutputMap.getContents()); + } + + return (rs); + } + + + + /******************************************************************************* + ** version of the qRecordToApiMap that returns QRecords, not maps. + ** useful for cases where we're staying inside QQQ, but working with an api- + ** versioned application. + *******************************************************************************/ + public static List qRecordsToApiVersionedQRecordList(List records, String tableName, String apiName, String apiVersion) throws QException + { + Map fieldValueMappers = getFieldValueMappers(records, tableName, apiName, apiVersion); + + List rs = new ArrayList<>(); + for(QRecord record : records) + { + ApiOutputQRecordWrapper apiOutputQRecord = qRecordToApiMap(record, tableName, apiName, apiVersion, fieldValueMappers, new ApiOutputQRecordWrapper(new QRecord().withTableName(tableName))); + rs.add(apiOutputQRecord.getContents()); + } + + return (rs); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private static Map getFieldValueMappers(List records, String tableName, String apiName, String apiVersion) throws QException { Map fieldValueMappers = new HashMap<>(); @@ -91,8 +134,6 @@ public class QRecordApiAdapter for(QFieldMetaData field : tableApiFields) { ApiFieldMetaData apiFieldMetaData = ObjectUtils.tryAndRequireNonNullElse(() -> ApiFieldMetaDataContainer.of(field).getApiFieldMetaData(apiName), new ApiFieldMetaData()); - String apiFieldName = ApiFieldMetaData.getEffectiveApiFieldName(apiName, field); - if(apiFieldMetaData.getCustomValueMapper() != null) { if(!fieldValueMappers.containsKey(apiFieldMetaData.getCustomValueMapper().getName())) @@ -107,31 +148,24 @@ public class QRecordApiAdapter } } } - - ArrayList> rs = new ArrayList<>(); - for(QRecord record : records) - { - rs.add(QRecordApiAdapter.qRecordToApiMap(record, tableName, apiName, apiVersion, fieldValueMappers)); - } - - return (rs); + return fieldValueMappers; } /******************************************************************************* - ** private version of convert a QRecord to a map for the API - takes params to + ** private version of convert a QRecord to a map for the API (or, another + ** QRecord - whatever object is in the `O output` param). Takes params to ** support working in bulk w/ customizers much better. *******************************************************************************/ - private static Map qRecordToApiMap(QRecord record, String tableName, String apiName, String apiVersion, Map fieldValueMappers) throws QException + private static > O qRecordToApiMap(QRecord record, String tableName, String apiName, String apiVersion, Map fieldValueMappers, O output) throws QException { if(record == null) { return (null); } - List tableApiFields = GetTableApiFieldsAction.getTableApiFieldList(new GetTableApiFieldsAction.ApiNameVersionAndTableName(apiName, apiVersion, tableName)); - LinkedHashMap outputRecord = new LinkedHashMap<>(); + List tableApiFields = GetTableApiFieldsAction.getTableApiFieldList(new GetTableApiFieldsAction.ApiNameVersionAndTableName(apiName, apiVersion, tableName)); ///////////////////////////////////////// // iterate over the table's api fields // @@ -172,7 +206,7 @@ public class QRecordApiAdapter value = Base64.getEncoder().encodeToString(bytes); } - outputRecord.put(apiFieldName, value); + output.putValue(apiFieldName, value); } ////////////////////////////////////////////////////////////////////////////////////////////////// @@ -187,16 +221,18 @@ public class QRecordApiAdapter continue; } - ArrayList> associationList = new ArrayList<>(); - outputRecord.put(association.getName(), associationList); + ArrayList associationList = new ArrayList<>(); for(QRecord associatedRecord : CollectionUtils.nonNullList(CollectionUtils.nonNullMap(record.getAssociatedRecords()).get(association.getName()))) { - associationList.add(qRecordToApiMap(associatedRecord, association.getAssociatedTableName(), apiName, apiVersion)); + ApiOutputRecordWrapperInterface apiOutputAssociation = output.newSibling(associatedRecord.getTableName()); + associationList.add(qRecordToApiMap(associatedRecord, association.getAssociatedTableName(), apiName, apiVersion, fieldValueMappers, apiOutputAssociation.unwrap())); } + + output.putAssociation(association.getName(), associationList); } - return (outputRecord); + return (output); } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/output/ApiOutputMapWrapper.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/output/ApiOutputMapWrapper.java new file mode 100644 index 00000000..3262198d --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/output/ApiOutputMapWrapper.java @@ -0,0 +1,95 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.api.actions.output; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + + +/*************************************************************************** + ** implementation of ApiOutputRecordWrapperInterface that wraps a Map + ***************************************************************************/ +public class ApiOutputMapWrapper implements ApiOutputRecordWrapperInterface, ApiOutputMapWrapper> +{ + private Map apiMap; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ApiOutputMapWrapper(Map apiMap) + { + this.apiMap = apiMap; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void putValue(String key, Serializable value) + { + apiMap.put(key, value); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void putAssociation(String key, List values) + { + ArrayList> associatedMaps = new ArrayList<>(values.stream().map(oqr -> oqr.apiMap).toList()); + apiMap.put(key, associatedMaps); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public ApiOutputMapWrapper newSibling(String tableName) + { + return new ApiOutputMapWrapper(new LinkedHashMap<>()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public Map getContents() + { + return this.apiMap; + } + +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/output/ApiOutputQRecordWrapper.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/output/ApiOutputQRecordWrapper.java new file mode 100644 index 00000000..fd09af2a --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/output/ApiOutputQRecordWrapper.java @@ -0,0 +1,94 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.api.actions.output; + + +import java.io.Serializable; +import java.util.List; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; + + +/*************************************************************************** + ** implementation of ApiOutputRecordWrapperInterface that wraps a QRecord + ***************************************************************************/ +public class ApiOutputQRecordWrapper implements ApiOutputRecordWrapperInterface +{ + private QRecord record; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ApiOutputQRecordWrapper(QRecord record) + { + this.record = record; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void putValue(String key, Serializable value) + { + record.setValue(key, value); + record.setDisplayValue(key, ValueUtils.getValueAsString(value)); // todo is this useful? + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void putAssociation(String key, List values) + { + List records = values.stream().map(oqr -> oqr.record).toList(); + record.withAssociatedRecords(key, records); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public ApiOutputQRecordWrapper newSibling(String tableName) + { + return (new ApiOutputQRecordWrapper(new QRecord().withTableName(tableName))); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QRecord getContents() + { + return this.record; + } + +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/output/ApiOutputRecordWrapperInterface.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/output/ApiOutputRecordWrapperInterface.java new file mode 100644 index 00000000..9dc4a1ae --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/output/ApiOutputRecordWrapperInterface.java @@ -0,0 +1,73 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.api.actions.output; + + +import java.io.Serializable; +import java.util.List; + + +/*************************************************************************** + ** interface to define wrappers for either a Map of values (e.g., the + ** original/native return type for the API), or a QRecord. Built for use + ** by QRecordApiAdapter - not clear ever useful outside of there. + ** + ** Type params are: + ** C: the wrapped Contents + ** A: the child-type... e.g: + ** class Child implements ApiOutputRecordInterface(Something, Child) + ***************************************************************************/ +public interface ApiOutputRecordWrapperInterface> +{ + /*************************************************************************** + ** put a value in the wrapped object + ***************************************************************************/ + void putValue(String key, Serializable value); + + /*************************************************************************** + ** put associated wrapper-objects in the wrapped object + ***************************************************************************/ + void putAssociation(String key, List values); + + /*************************************************************************** + ** create a new "sibling" object to this - e.g., a wrapper around a new + ** instance of the contents object + ***************************************************************************/ + ApiOutputRecordWrapperInterface newSibling(String tableName); + + /*************************************************************************** + ** get the wrapped contents object + ***************************************************************************/ + C getContents(); + + /*************************************************************************** + ** return this, but as the `A` type... + ***************************************************************************/ + @SuppressWarnings("unchecked") + default A unwrap() + { + return (A) this; + } +} + + + diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/executors/ApiAwareExecutorInterface.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/executors/ApiAwareExecutorInterface.java new file mode 100644 index 00000000..b56e8a1a --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/executors/ApiAwareExecutorInterface.java @@ -0,0 +1,63 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.api.middleware.executors; + + +import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; +import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer; +import com.kingsrook.qqq.backend.core.context.QContext; + + +/******************************************************************************* + ** + *******************************************************************************/ +public interface ApiAwareExecutorInterface +{ + + /*************************************************************************** + ** + ***************************************************************************/ + default String getApiName() + { + return (QContext.getQSession().getValue("apiName")); + } + + + /*************************************************************************** + ** + ***************************************************************************/ + default String getApiVersion() + { + return (QContext.getQSession().getValue("apiVersion")); + } + + + /*************************************************************************** + ** + ***************************************************************************/ + default ApiInstanceMetaData getApiInstanceMetaData() + { + ApiInstanceMetaDataContainer apiInstanceMetaDataContainer = ApiInstanceMetaDataContainer.of(QContext.getQInstance()); + ApiInstanceMetaData apiInstanceMetaData = apiInstanceMetaDataContainer.getApis().get(getApiName()); + return apiInstanceMetaData; + } +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/executors/ApiAwareTableCountExecutor.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/executors/ApiAwareTableCountExecutor.java new file mode 100644 index 00000000..3c8c1346 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/executors/ApiAwareTableCountExecutor.java @@ -0,0 +1,104 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.api.middleware.executors; + + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import com.kingsrook.qqq.api.actions.ApiImplementation; +import com.kingsrook.qqq.api.actions.GetTableApiFieldsAction; +import com.kingsrook.qqq.api.model.metadata.ApiOperation; +import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper; +import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType; +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.QueryHint; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +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.utils.ValueUtils; +import com.kingsrook.qqq.middleware.javalin.executors.TableCountExecutor; +import com.kingsrook.qqq.middleware.javalin.executors.io.TableCountInput; +import com.kingsrook.qqq.middleware.javalin.executors.io.TableCountOutputInterface; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiAwareTableCountExecutor extends TableCountExecutor implements ApiAwareExecutorInterface +{ + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void execute(TableCountInput input, TableCountOutputInterface output) throws QException + { + List badRequestMessages = new ArrayList<>(); + + // todo - new operation? move all this to the api impl class?? + // todo table api name vs. internal name?? + String apiName = getApiName(); + String apiVersion = getApiVersion(); + QTableMetaData table = ApiImplementation.validateTableAndVersion(getApiInstanceMetaData(), apiVersion, input.getTableName(), ApiOperation.QUERY_BY_QUERY_STRING); + + CountInput countInput = new CountInput(); + countInput.setTableName(input.getTableName()); + + PermissionsHelper.checkTablePermissionThrowing(countInput, TablePermissionSubType.READ); + Map tableApiFields = GetTableApiFieldsAction.getTableApiFieldMap(new GetTableApiFieldsAction.ApiNameVersionAndTableName(apiName, apiVersion, table.getName())); + + countInput.setTimeoutSeconds(DEFAULT_QUERY_TIMEOUT_SECONDS); // todo param + countInput.withQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND); + countInput.setIncludeDistinctCount(input.getIncludeDistinct()); + + countInput.setQueryJoins(input.getJoins()); // todo - what are version implications here?? + + /////////////////////////////////////////////////////////////////////////// + // take care of managing criteria, which may not be in this version, etc // + /////////////////////////////////////////////////////////////////////////// + QQueryFilter filter = Objects.requireNonNullElseGet(input.getFilter(), () -> new QQueryFilter()); + QueryExecutorUtils.manageCriteriaFields(filter, tableApiFields, badRequestMessages, apiName, countInput); + + ////////////////////////////////////////// + // no more badRequest checks below here // + ////////////////////////////////////////// + QueryExecutorUtils.throwIfBadRequestMessages(badRequestMessages); + + // + CountAction countAction = new CountAction(); + countInput.setFilter(filter); + CountOutput countOutput = countAction.execute(countInput); + + // todo - removed field handling... + + // todo display values... + + output.setCount(ValueUtils.getValueAsLong(countOutput.getCount())); + output.setDistinctCount(ValueUtils.getValueAsLong(countOutput.getDistinctCount())); + } + +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/executors/ApiAwareTableMetaDataExecutor.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/executors/ApiAwareTableMetaDataExecutor.java new file mode 100644 index 00000000..6b8be56a --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/executors/ApiAwareTableMetaDataExecutor.java @@ -0,0 +1,150 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.api.middleware.executors; + + +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.api.actions.GetTableApiFieldsAction; +import com.kingsrook.qqq.api.model.actions.GetTableApiFieldsInput; +import com.kingsrook.qqq.api.model.actions.GetTableApiFieldsOutput; +import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper; +import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataOutput; +import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendExposedJoin; +import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.middleware.javalin.executors.TableMetaDataExecutor; +import com.kingsrook.qqq.middleware.javalin.executors.io.TableMetaDataInput; +import com.kingsrook.qqq.middleware.javalin.executors.io.TableMetaDataOutputInterface; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiAwareTableMetaDataExecutor extends TableMetaDataExecutor implements ApiAwareExecutorInterface +{ + private static final QLogger LOG = QLogger.getLogger(ApiAwareTableMetaDataExecutor.class); + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void execute(TableMetaDataInput input, TableMetaDataOutputInterface output) throws QException + { + QTableMetaData table = getQTableMetaData(input); + + Map fieldMap = getFieldsForApiVersion(input.getTableName()); + + com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput tableMetaDataInput = new com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput(); + tableMetaDataInput.setTableName(input.getTableName()); + PermissionsHelper.checkTablePermissionThrowing(tableMetaDataInput, TablePermissionSubType.READ); + + QBackendMetaData backendForTable = QContext.getQInstance().getBackendForTable(table.getName()); + TableMetaDataOutput tableMetaDataOutput = new TableMetaDataOutput(); + tableMetaDataOutput.setTable(new QFrontendTableMetaData(tableMetaDataInput, backendForTable, table, true, true, fieldMap)); + + adjustExposedJoinsForApi(tableMetaDataOutput); + + output.setTableMetaData(tableMetaDataOutput.getTable()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private void adjustExposedJoinsForApi(TableMetaDataOutput tableMetaDataOutput) throws QException + { + if(CollectionUtils.nullSafeIsEmpty(tableMetaDataOutput.getTable().getExposedJoins())) + { + return; + } + + Iterator iterator = tableMetaDataOutput.getTable().getExposedJoins().iterator(); + while(iterator.hasNext()) + { + QFrontendExposedJoin frontendExposedJoin = iterator.next(); + String tableName = frontendExposedJoin.getJoinTable().getName(); + + try + { + QTableMetaData joinTable = QContext.getQInstance().getTable(tableName); + QBackendMetaData backendForTable = QContext.getQInstance().getBackendForTable(tableName); + Map joinFieldMap = getFieldsForApiVersion(tableName); + + com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput tableMetaDataInput = new com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput(); + frontendExposedJoin.setJoinTable(new QFrontendTableMetaData(tableMetaDataInput, backendForTable, joinTable, true, true, joinFieldMap)); + } + catch(QNotFoundException e) + { + LOG.debug("Removing exposed-join table that isn't in api version", logPair("mainTable", tableMetaDataOutput.getTable().getName()), logPair("joinTable", tableName), logPair("apiName", getApiName()), logPair("apiVersion", getApiVersion())); + iterator.remove(); + } + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private QTableMetaData getQTableMetaData(TableMetaDataInput input) throws QNotFoundException + { + String tableName = input.getTableName(); + QTableMetaData table = QContext.getQInstance().getTable(tableName); + if(table == null) + { + throw (new QNotFoundException("Table [" + tableName + "] was not found.")); + } + return table; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private Map getFieldsForApiVersion(String tableName) throws QException + { + GetTableApiFieldsInput getTableApiFieldsInput = new GetTableApiFieldsInput() + .withApiName(getApiName()) + .withVersion(getApiVersion()) + .withTableName(tableName); + + GetTableApiFieldsOutput tableApiFieldsOutput = new GetTableApiFieldsAction().execute(getTableApiFieldsInput); + List fields = tableApiFieldsOutput.getFields(); + Map fieldMap = CollectionUtils.listToMap(fields, f -> f.getName()); + return fieldMap; + } +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/executors/ApiAwareTableQueryExecutor.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/executors/ApiAwareTableQueryExecutor.java new file mode 100644 index 00000000..b02473ee --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/executors/ApiAwareTableQueryExecutor.java @@ -0,0 +1,160 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.api.middleware.executors; + + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import com.kingsrook.qqq.api.actions.ApiImplementation; +import com.kingsrook.qqq.api.actions.GetTableApiFieldsAction; +import com.kingsrook.qqq.api.actions.QRecordApiAdapter; +import com.kingsrook.qqq.api.model.actions.ApiFieldCustomValueMapper; +import com.kingsrook.qqq.api.model.metadata.ApiOperation; +import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; +import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaDataContainer; +import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; +import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper; +import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint; +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.data.QRecord; +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.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.ObjectUtils; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.javalin.QJavalinMetaData; +import com.kingsrook.qqq.backend.javalin.QJavalinUtils; +import com.kingsrook.qqq.middleware.javalin.executors.TableQueryExecutor; +import com.kingsrook.qqq.middleware.javalin.executors.io.TableQueryInput; +import com.kingsrook.qqq.middleware.javalin.executors.io.TableQueryOutputInterface; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiAwareTableQueryExecutor extends TableQueryExecutor implements ApiAwareExecutorInterface +{ + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void execute(TableQueryInput input, TableQueryOutputInterface output) throws QException + { + List badRequestMessages = new ArrayList<>(); + + // todo - new operation? move all this to the api impl class?? + // todo table api name vs. internal name?? + String apiName = getApiName(); + String apiVersion = getApiVersion(); + QTableMetaData table = ApiImplementation.validateTableAndVersion(getApiInstanceMetaData(), apiVersion, input.getTableName(), ApiOperation.QUERY_BY_QUERY_STRING); + + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(input.getTableName()); + + PermissionsHelper.checkTablePermissionThrowing(queryInput, TablePermissionSubType.READ); + Map tableApiFields = GetTableApiFieldsAction.getTableApiFieldMap(new GetTableApiFieldsAction.ApiNameVersionAndTableName(apiName, apiVersion, table.getName())); + + queryInput.setIncludeAssociations(true); + // queryInput.setShouldFetchHeavyFields(true); // diffs from raw api + queryInput.setShouldGenerateDisplayValues(true); + queryInput.setShouldTranslatePossibleValues(true); + queryInput.setTimeoutSeconds(DEFAULT_QUERY_TIMEOUT_SECONDS); // todo param + queryInput.withQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND); + + queryInput.setQueryJoins(input.getJoins()); // todo - what are version implications here?? + + QQueryFilter filter = Objects.requireNonNullElseGet(input.getFilter(), () -> new QQueryFilter()); + queryInput.setFilter(filter); + + if(filter.getLimit() == null) + { + QJavalinUtils.handleQueryNullLimit(QJavalinMetaData.of(QContext.getQInstance()), queryInput, null); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // take care of managing order-by fields and criteria, which may not be in this version, etc // + /////////////////////////////////////////////////////////////////////////////////////////////// + manageOrderByFields(filter, tableApiFields, badRequestMessages, apiName, queryInput); + QueryExecutorUtils.manageCriteriaFields(filter, tableApiFields, badRequestMessages, apiName, queryInput); + + ////////////////////////////////////////// + // no more badRequest checks below here // + ////////////////////////////////////////// + QueryExecutorUtils.throwIfBadRequestMessages(badRequestMessages); + + /////////////////////// + // execute the query // + /////////////////////// + QueryAction queryAction = new QueryAction(); + QueryOutput queryOutput = queryAction.execute(queryInput); + + List versionedRecords = QRecordApiAdapter.qRecordsToApiVersionedQRecordList(queryOutput.getRecords(), table.getName(), getApiName(), getApiVersion()); + + QValueFormatter.setDisplayValuesInRecordsIncludingPossibleValueTranslations(table, versionedRecords); + + output.setRecords(versionedRecords); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private static void manageOrderByFields(QQueryFilter filter, Map tableApiFields, List badRequestMessages, String apiName, QueryInput queryInput) + { + for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(filter.getOrderBys())) + { + String apiFieldName = orderBy.getFieldName(); + QFieldMetaData field = tableApiFields.get(apiFieldName); + if(field == null) + { + badRequestMessages.add("Unrecognized orderBy field name: " + apiFieldName + "."); + } + else + { + ApiFieldMetaData apiFieldMetaData = ObjectUtils.tryAndRequireNonNullElse(() -> ApiFieldMetaDataContainer.of(field).getApiFieldMetaData(apiName), new ApiFieldMetaData()); + if(StringUtils.hasContent(apiFieldMetaData.getReplacedByFieldName())) + { + orderBy.setFieldName(apiFieldMetaData.getReplacedByFieldName()); + } + else if(apiFieldMetaData.getCustomValueMapper() != null) + { + ApiFieldCustomValueMapper customValueMapper = QCodeLoader.getAdHoc(ApiFieldCustomValueMapper.class, apiFieldMetaData.getCustomValueMapper()); + customValueMapper.customizeFilterOrderBy(queryInput, orderBy, apiFieldName, apiFieldMetaData); + } + } + + } + } + +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/executors/QueryExecutorUtils.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/executors/QueryExecutorUtils.java new file mode 100644 index 00000000..aa7e07b3 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/executors/QueryExecutorUtils.java @@ -0,0 +1,103 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.api.middleware.executors; + + +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.api.model.actions.ApiFieldCustomValueMapper; +import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; +import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaDataContainer; +import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; +import com.kingsrook.qqq.backend.core.exceptions.QBadRequestException; +import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrCountInputInterface; +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.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.ObjectUtils; +import com.kingsrook.qqq.backend.core.utils.StringUtils; + + +/******************************************************************************* + ** shared code for query & count executors + *******************************************************************************/ +public class QueryExecutorUtils +{ + + /*************************************************************************** + ** + ***************************************************************************/ + static void manageCriteriaFields(QQueryFilter filter, Map tableApiFields, List badRequestMessages, String apiName, QueryOrCountInputInterface input) + { + for(QFilterCriteria criteria : CollectionUtils.nonNullList(filter.getCriteria())) + { + String apiFieldName = criteria.getFieldName(); + QFieldMetaData field = tableApiFields.get(apiFieldName); + if(field == null) + { + badRequestMessages.add("Unrecognized criteria field name: " + apiFieldName + "."); + } + else + { + try + { + ApiFieldMetaData apiFieldMetaData = ObjectUtils.tryAndRequireNonNullElse(() -> ApiFieldMetaDataContainer.of(field).getApiFieldMetaData(apiName), new ApiFieldMetaData()); + if(StringUtils.hasContent(apiFieldMetaData.getReplacedByFieldName())) + { + criteria.setFieldName(apiFieldMetaData.getReplacedByFieldName()); + } + else if(apiFieldMetaData.getCustomValueMapper() != null) + { + ApiFieldCustomValueMapper customValueMapper = QCodeLoader.getAdHoc(ApiFieldCustomValueMapper.class, apiFieldMetaData.getCustomValueMapper()); + customValueMapper.customizeFilterCriteriaForQueryOrCount(input, filter, criteria, apiFieldName, apiFieldMetaData); + } + } + catch(Exception e) + { + badRequestMessages.add("Error processing criteria field " + apiFieldName + ": " + e.getMessage()); + } + } + } + } + + + + /*************************************************************************** + * + ***************************************************************************/ + static void throwIfBadRequestMessages(List badRequestMessages) throws QBadRequestException + { + if(!badRequestMessages.isEmpty()) + { + if(badRequestMessages.size() == 1) + { + throw (new QBadRequestException(badRequestMessages.get(0))); + } + else + { + throw (new QBadRequestException("Request failed with " + badRequestMessages.size() + " reasons: " + StringUtils.join("\n", badRequestMessages))); + } + } + } + +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/specs/v1/ApiAwareMiddlewareVersionV1.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/specs/v1/ApiAwareMiddlewareVersionV1.java new file mode 100644 index 00000000..319be83b --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/specs/v1/ApiAwareMiddlewareVersionV1.java @@ -0,0 +1,174 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.api.middleware.specs.v1; + + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import com.kingsrook.qqq.api.model.APIVersion; +import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; +import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec; +import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1; +import com.kingsrook.qqq.middleware.javalin.specs.v1.TableCountSpecV1; +import com.kingsrook.qqq.middleware.javalin.specs.v1.TableMetaDataSpecV1; +import com.kingsrook.qqq.middleware.javalin.specs.v1.TableQuerySpecV1; +import io.javalin.http.Context; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiAwareMiddlewareVersionV1 extends MiddlewareVersionV1 +{ + private Map apiNameAndVersionsByPath = new HashMap<>(); + + private List> specs; + + + + /*************************************************************************** + ** + ***************************************************************************/ + private record ApiNameAndVersions(String apiName, Set apiVersions) + { + + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ApiAwareMiddlewareVersionV1() + { + this.specs = defineEndpointSpecs(); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public void addVersion(String apiName, APIVersion apiVersion) + { + ApiInstanceMetaDataContainer apiInstanceMetaDataContainer = ApiInstanceMetaDataContainer.of(QContext.getQInstance()); + ApiInstanceMetaData apiInstanceMetaData = apiInstanceMetaDataContainer.getApis().get(apiName); + String apiPath = apiInstanceMetaData.getPath(); + + apiPath = Objects.requireNonNullElse(apiPath, "").replaceFirst("^/", "").replaceFirst("/$", ""); + String apiVersionString = apiVersion.toString().replaceFirst("^/", "").replaceFirst("/$", ""); + + ApiNameAndVersions apiNameAndVersions = apiNameAndVersionsByPath.computeIfAbsent(apiPath, (p) -> new ApiNameAndVersions(apiName, new HashSet<>())); + apiNameAndVersions.apiVersions().add(apiVersionString); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public List> defineEndpointSpecs() + { + List> specs = new ArrayList<>(super.getEndpointSpecs()); + + ListIterator> listIterator = specs.listIterator(); + while(listIterator.hasNext()) + { + AbstractEndpointSpec spec = listIterator.next(); + if(spec.getClass().equals(TableMetaDataSpecV1.class)) + { + listIterator.set(new ApiAwareTableMetaDataSpecV1()); + } + else if(spec.getClass().equals(TableQuerySpecV1.class)) + { + listIterator.set(new ApiAwareTableQuerySpecV1()); + } + else if(spec.getClass().equals(TableCountSpecV1.class)) + { + listIterator.set(new ApiAwareTableCountSpecV1()); + } + } + + return (specs); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public List> getEndpointSpecs() + { + return (specs); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public String getVersionBasePath() + { + // return ("/" + getVersion() + "/" + apiPath + "/" + apiVersion + "/"); + return ("/" + getVersion() + "/{applicationApiPath}/{applicationApiVersion}/"); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public void preExecute(Context context) throws QException + { + String apiPath = context.pathParam("applicationApiPath"); + String apiVersion = context.pathParam("applicationApiVersion"); + + ApiNameAndVersions apiNameAndVersions = apiNameAndVersionsByPath.get(apiPath); + if(apiNameAndVersions != null) + { + Set allowedVersions = apiNameAndVersions.apiVersions(); + if(allowedVersions.contains(apiVersion)) + { + QSession session = QContext.getQSession(); + session.setValue("apiName", apiNameAndVersions.apiName()); + session.setValue("apiVersion", apiVersion); + return; + } + } + + throw new QNotFoundException("No API exists at the requested path."); + } +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/specs/v1/ApiAwareTableCountSpecV1.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/specs/v1/ApiAwareTableCountSpecV1.java new file mode 100644 index 00000000..c4e654e7 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/specs/v1/ApiAwareTableCountSpecV1.java @@ -0,0 +1,44 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.api.middleware.specs.v1; + + +import com.kingsrook.qqq.api.middleware.executors.ApiAwareTableCountExecutor; +import com.kingsrook.qqq.middleware.javalin.executors.TableCountExecutor; +import com.kingsrook.qqq.middleware.javalin.specs.v1.TableCountSpecV1; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiAwareTableCountSpecV1 extends TableCountSpecV1 +{ + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public TableCountExecutor newExecutor() + { + return new ApiAwareTableCountExecutor(); + } + +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/specs/v1/ApiAwareTableMetaDataSpecV1.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/specs/v1/ApiAwareTableMetaDataSpecV1.java new file mode 100644 index 00000000..bf9bcce5 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/specs/v1/ApiAwareTableMetaDataSpecV1.java @@ -0,0 +1,44 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.api.middleware.specs.v1; + + +import com.kingsrook.qqq.api.middleware.executors.ApiAwareTableMetaDataExecutor; +import com.kingsrook.qqq.middleware.javalin.executors.TableMetaDataExecutor; +import com.kingsrook.qqq.middleware.javalin.specs.v1.TableMetaDataSpecV1; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiAwareTableMetaDataSpecV1 extends TableMetaDataSpecV1 +{ + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public TableMetaDataExecutor newExecutor() + { + return new ApiAwareTableMetaDataExecutor(); + } + +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/specs/v1/ApiAwareTableQuerySpecV1.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/specs/v1/ApiAwareTableQuerySpecV1.java new file mode 100644 index 00000000..a2f4cb4b --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/middleware/specs/v1/ApiAwareTableQuerySpecV1.java @@ -0,0 +1,44 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.api.middleware.specs.v1; + + +import com.kingsrook.qqq.api.middleware.executors.ApiAwareTableQueryExecutor; +import com.kingsrook.qqq.middleware.javalin.executors.TableQueryExecutor; +import com.kingsrook.qqq.middleware.javalin.specs.v1.TableQuerySpecV1; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiAwareTableQuerySpecV1 extends TableQuerySpecV1 +{ + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public TableQueryExecutor newExecutor() + { + return new ApiAwareTableQueryExecutor(); + } + +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/ApiFieldCustomValueMapper.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/ApiFieldCustomValueMapper.java index 36f27359..a69d5a3e 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/ApiFieldCustomValueMapper.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/ApiFieldCustomValueMapper.java @@ -24,6 +24,7 @@ package com.kingsrook.qqq.api.model.actions; import java.io.Serializable; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; +import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrCountInputInterface; 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; @@ -70,6 +71,7 @@ public abstract class ApiFieldCustomValueMapper /******************************************************************************* ** *******************************************************************************/ + @Deprecated(since = "0.26.0 changed QueryInput to QueryOrCountInputInterface") public void customizeFilterCriteria(QueryInput queryInput, QQueryFilter filter, QFilterCriteria criteria, String apiFieldName, ApiFieldMetaData apiFieldMetaData) { ///////////////////// @@ -78,6 +80,18 @@ public abstract class ApiFieldCustomValueMapper } + /******************************************************************************* + ** + *******************************************************************************/ + public void customizeFilterCriteriaForQueryOrCount(QueryOrCountInputInterface input, QQueryFilter filter, QFilterCriteria criteria, String apiFieldName, ApiFieldMetaData apiFieldMetaData) + { + if(input instanceof QueryInput queryInput) + { + customizeFilterCriteria(queryInput, filter, criteria, apiFieldName, apiFieldMetaData); + } + } + + /******************************************************************************* ** diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiTableMetaDataContainer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiTableMetaDataContainer.java index 5a62d1ab..8629b202 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiTableMetaDataContainer.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiTableMetaDataContainer.java @@ -31,6 +31,7 @@ import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer; import com.kingsrook.qqq.backend.core.context.CapturedContext; import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; @@ -216,6 +217,12 @@ public class ApiTableMetaDataContainer extends QSupplementalTableMetaData /////////////////////////////////////////////////////////////////////////////////////////////////// GetTableApiFieldsAction.getTableApiFieldMap(new GetTableApiFieldsAction.ApiNameVersionAndTableName(apiName, version.toString(), tableMetaData.getName())); } + catch(QNotFoundException qnfe) + { + ///////////////////////////// + // skip tables not in apis // + ///////////////////////////// + } catch(Exception e) { String message = "Error validating ApiTableMetaData for table: " + tableMetaData.getName() + ", api: " + apiName + ", version: " + version; 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 9c198ec0..d0133dc7 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 @@ -22,7 +22,11 @@ package com.kingsrook.qqq.api; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.Month; import java.util.List; +import java.util.Map; import java.util.function.Consumer; import com.kingsrook.qqq.api.implementations.savedreports.RenderSavedReportProcessApiMetaDataEnricher; import com.kingsrook.qqq.api.model.APIVersion; @@ -74,12 +78,14 @@ 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.TablesPossibleValueSourceMetaDataProvider; import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; +import com.kingsrook.qqq.backend.core.model.metadata.variants.BackendVariantsConfig; import com.kingsrook.qqq.backend.core.model.savedreports.SavedReport; import com.kingsrook.qqq.backend.core.model.savedreports.SavedReportsMetaDataProvider; import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage; import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage; import com.kingsrook.qqq.backend.core.model.statusmessages.SystemErrorStatusMessage; import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule; +import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryModuleBackendVariantSetting; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaUpdateStep; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess; @@ -101,12 +107,19 @@ public class TestUtils public static final String TABLE_NAME_LINE_ITEM_EXTRINSIC = "orderLineExtrinsic"; public static final String TABLE_NAME_ORDER_EXTRINSIC = "orderExtrinsic"; + public static final String MEMORY_BACKEND_WITH_VARIANTS_NAME = "memoryWithVariants"; + public static final String TABLE_NAME_MEMORY_VARIANT_OPTIONS = "memoryVariantOptions"; + public static final String TABLE_NAME_MEMORY_VARIANT_DATA = "memoryVariantData"; + public static final String PROCESS_NAME_GET_PERSON_INFO = "getPersonInfo"; public static final String PROCESS_NAME_TRANSFORM_PEOPLE = "transformPeople"; public static final String API_NAME = "test-api"; public static final String ALTERNATIVE_API_NAME = "person-api"; + public static final String API_PATH = "/api/"; + public static final String ALTERNATIVE_API_PATH = "/person-api/"; + public static final String V2022_Q4 = "2022.Q4"; public static final String V2023_Q1 = "2023.Q1"; public static final String V2023_Q2 = "2023.Q2"; @@ -139,12 +152,14 @@ public class TestUtils qInstance.setAuthentication(new Auth0AuthenticationMetaData().withType(QAuthenticationType.FULLY_ANONYMOUS).withName("anonymous")); + defineMemoryBackendVariantUseCases(qInstance); + addSavedReports(qInstance); qInstance.withSupplementalMetaData(new ApiInstanceMetaDataContainer() .withApiInstanceMetaData(new ApiInstanceMetaData() .withName(API_NAME) - .withPath("/api/") + .withPath(API_PATH) .withLabel("Test API") .withDescription("QQQ Test API") .withContactEmail("contact@kingsrook.com") @@ -154,7 +169,7 @@ public class TestUtils .withFutureVersions(List.of(new APIVersion(V2023_Q2)))) .withApiInstanceMetaData(new ApiInstanceMetaData() .withName(ALTERNATIVE_API_NAME) - .withPath("/person-api/") + .withPath(ALTERNATIVE_API_PATH) .withLabel("Person-Only API") .withDescription("QQQ Test API, that only has the Person table.") .withContactEmail("contact@kingsrook.com") @@ -331,6 +346,8 @@ public class TestUtils QTableMetaData table = new QTableMetaData() .withName(TABLE_NAME_PERSON) .withLabel("Person") + .withRecordLabelFormat("%s %s") + .withRecordLabelFields("firstName", "lastName") .withBackendName(MEMORY_BACKEND_NAME) .withPrimaryKeyField("id") .withUniqueKey(new UniqueKey("email")) @@ -342,6 +359,7 @@ public class TestUtils .withField(new QFieldMetaData("lastName", QFieldType.STRING)) .withField(new QFieldMetaData("birthDate", QFieldType.DATE)) .withField(new QFieldMetaData("email", QFieldType.STRING)) + .withField(new QFieldMetaData("bestFriendPersonId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_PERSON)) // .withField(new QFieldMetaData("homeStateId", QFieldType.INTEGER).withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_STATE)) // .withField(new QFieldMetaData("favoriteShapeId", QFieldType.INTEGER).withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_SHAPE)) // .withField(new QFieldMetaData("customValue", QFieldType.INTEGER).withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_CUSTOM)) @@ -551,6 +569,7 @@ public class TestUtils } + /******************************************************************************* ** *******************************************************************************/ @@ -579,6 +598,33 @@ public class TestUtils + /******************************************************************************* + ** + *******************************************************************************/ + public static void insertTim2Shoes() throws QException + { + new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_PERSON).withRecord(getTim2ShoesRecord())); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QRecord getTim2ShoesRecord() throws QException + { + return new QRecord() + .withValue("firstName", "Tim") + .withValue("noOfShoes", 2) + .withValue("birthDate", LocalDate.of(1980, Month.MAY, 31)) + .withValue("cost", new BigDecimal("3.50")) + .withValue("price", new BigDecimal("9.99")) + .withValue("photo", "ABCD".getBytes()) + .withValue("bestFriendPersonId", 1); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -683,4 +729,38 @@ public class TestUtils } } + + + /*************************************************************************** + ** + ***************************************************************************/ + private static void defineMemoryBackendVariantUseCases(QInstance qInstance) + { + qInstance.addBackend(new QBackendMetaData() + .withName(MEMORY_BACKEND_WITH_VARIANTS_NAME) + .withBackendType(MemoryBackendModule.class) + .withUsesVariants(true) + .withBackendVariantsConfig(new BackendVariantsConfig() + .withVariantTypeKey(TABLE_NAME_MEMORY_VARIANT_OPTIONS) + .withOptionsTableName(TABLE_NAME_MEMORY_VARIANT_OPTIONS) + .withBackendSettingSourceFieldNameMap(Map.of(MemoryModuleBackendVariantSetting.PRIMARY_KEY, "id")) + )); + + qInstance.addTable(new QTableMetaData() + .withName(TABLE_NAME_MEMORY_VARIANT_DATA) + .withBackendName(MEMORY_BACKEND_WITH_VARIANTS_NAME) + .withPrimaryKeyField("id") + .withField(new QFieldMetaData("id", QFieldType.INTEGER)) + .withField(new QFieldMetaData("name", QFieldType.STRING)) + ); + + qInstance.addTable(new QTableMetaData() + .withName(TABLE_NAME_MEMORY_VARIANT_OPTIONS) + .withBackendName(MEMORY_BACKEND_NAME) // note, the version without variants! + .withPrimaryKeyField("id") + .withField(new QFieldMetaData("id", QFieldType.INTEGER)) + .withField(new QFieldMetaData("name", QFieldType.STRING)) + ); + } + } diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/GetTableApiFieldsActionTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/GetTableApiFieldsActionTest.java index 6e7edda6..c1eac159 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/GetTableApiFieldsActionTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/GetTableApiFieldsActionTest.java @@ -35,12 +35,14 @@ import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher; 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 org.junit.jupiter.api.Test; import static com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType.STRING; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -60,7 +62,16 @@ class GetTableApiFieldsActionTest extends BaseTest *******************************************************************************/ private List getFields(String tableName, String version) throws QException { - return new GetTableApiFieldsAction().execute(new GetTableApiFieldsInput().withApiName(TestUtils.API_NAME).withTableName(tableName).withVersion(version)).getFields(); + return (getFields(TestUtils.API_NAME, tableName, version)); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + private List getFields(String apiName, String tableName, String version) throws QException + { + return new GetTableApiFieldsAction().execute(new GetTableApiFieldsInput().withApiName(apiName).withTableName(tableName).withVersion(version)).getFields(); } @@ -113,4 +124,47 @@ class GetTableApiFieldsActionTest extends BaseTest assertEquals(Set.of("a", "b", "d"), fieldListToNameSet.apply(getFields(TABLE_NAME, "3"))); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTablesNotFound() throws QException + { + String tableNameVersion2plus = "tableNameVersion2plus"; + QInstance qInstance = QContext.getQInstance(); + qInstance.addTable(new QTableMetaData() + .withName(tableNameVersion2plus) + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("2"))) + .withField(new QFieldMetaData("a", STRING))); + + String tableNameVersion2through4 = "tableNameVersion2through4"; + qInstance.addTable(new QTableMetaData() + .withName(tableNameVersion2through4) + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("2").withFinalVersion("4"))) + .withField(new QFieldMetaData("a", STRING))); + + String tableNameNoApis = "tableNameNoApis"; + qInstance.addTable(new QTableMetaData() + .withName(tableNameNoApis) + .withField(new QFieldMetaData("a", STRING))); + + new QInstanceEnricher(qInstance).enrich(); + + assertThatThrownBy(() -> getFields("no-such-table", "1")).isInstanceOf(QNotFoundException.class); + + assertThatThrownBy(() -> getFields(tableNameVersion2plus, "1")).isInstanceOf(QNotFoundException.class); + getFields(tableNameVersion2plus, "2"); + assertThatThrownBy(() -> getFields("noSuchApi", tableNameVersion2plus, "2")).isInstanceOf(QNotFoundException.class); + + assertThatThrownBy(() -> getFields(tableNameVersion2through4, "1")).isInstanceOf(QNotFoundException.class); + getFields(tableNameVersion2through4, "2"); + getFields(tableNameVersion2through4, "3"); + getFields(tableNameVersion2through4, "4"); + assertThatThrownBy(() -> getFields(tableNameVersion2through4, "5")).isInstanceOf(QNotFoundException.class); + + assertThatThrownBy(() -> getFields(tableNameNoApis, "1")).isInstanceOf(QNotFoundException.class); + } + } \ No newline at end of file diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/QRecordApiAdapterTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/QRecordApiAdapterTest.java index 24c6abc0..e2b9e09a 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/QRecordApiAdapterTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/QRecordApiAdapterTest.java @@ -25,7 +25,6 @@ package com.kingsrook.qqq.api.actions; import java.io.Serializable; import java.math.BigDecimal; import java.time.LocalDate; -import java.time.Month; import java.util.List; import java.util.Map; import com.kingsrook.qqq.api.BaseTest; @@ -58,13 +57,7 @@ class QRecordApiAdapterTest extends BaseTest ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // QRecord has values corresponding to what's defined in the QInstance (and the underlying backend system) // ///////////////////////////////////////////////////////////////////////////////////////////////////////////// - QRecord person = new QRecord() - .withValue("firstName", "Tim") - .withValue("noOfShoes", 2) - .withValue("birthDate", LocalDate.of(1980, Month.MAY, 31)) - .withValue("cost", new BigDecimal("3.50")) - .withValue("price", new BigDecimal("9.99")) - .withValue("photo", "ABCD".getBytes()); + QRecord person = TestUtils.getTim2ShoesRecord(); Map pastApiRecord = QRecordApiAdapter.qRecordToApiMap(person, TestUtils.TABLE_NAME_PERSON, TestUtils.API_NAME, TestUtils.V2022_Q4); assertEquals(2, pastApiRecord.get("shoeCount")); // old field name - not currently in the QTable, but we can still get its value! @@ -204,4 +197,60 @@ class QRecordApiAdapterTest extends BaseTest } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQRecordsToApiVersionedQRecordList() throws QException + { + QRecord person = TestUtils.getTim2ShoesRecord(); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// + // QRecord has values corresponding to what's defined in the QInstance (and the underlying backend system) // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// + QRecord pastQRecordRecord = QRecordApiAdapter.qRecordsToApiVersionedQRecordList(List.of(person), TestUtils.TABLE_NAME_PERSON, TestUtils.API_NAME, TestUtils.V2022_Q4).get(0); + assertEquals(2, pastQRecordRecord.getValueInteger("shoeCount")); // old field name - not currently in the QTable, but we can still get its value! + assertFalse(pastQRecordRecord.getValues().containsKey("noOfShoes")); // current field name - doesn't appear in old api-version + assertFalse(pastQRecordRecord.getValues().containsKey("cost")); // a current field name, but also not in this old api version + assertEquals("QUJDRA==", pastQRecordRecord.getValueString("photo")); // base64 version of "ABCD".getBytes() + + QRecord currentQRecord = QRecordApiAdapter.qRecordsToApiVersionedQRecordList(List.of(person), TestUtils.TABLE_NAME_PERSON, TestUtils.API_NAME, TestUtils.V2023_Q1).get(0); + assertFalse(currentQRecord.getValues().containsKey("shoeCount")); // old field name - not in this current api version + assertEquals(2, currentQRecord.getValueInteger("noOfShoes")); // current field name - value here as we expect + assertFalse(currentQRecord.getValues().containsKey("cost")); // future field name - not in the current api (we added the field during new dev, and didn't change the api) + + QRecord futureQRecord = QRecordApiAdapter.qRecordsToApiVersionedQRecordList(List.of(person), TestUtils.TABLE_NAME_PERSON, TestUtils.API_NAME, TestUtils.V2023_Q2).get(0); + assertFalse(futureQRecord.getValues().containsKey("shoeCount")); // old field name - also not in this future api version + assertEquals(2, futureQRecord.getValueInteger("noOfShoes")); // current field name - still here. + assertEquals(new BigDecimal("3.50"), futureQRecord.getValueBigDecimal("cost")); // future field name appears now that we've requested this future api version. + + for(QRecord specialRecord : List.of(pastQRecordRecord, currentQRecord, futureQRecord)) + { + assertEquals(LocalDate.parse("1980-05-31"), specialRecord.getValueLocalDate("birthDay")); // use the apiFieldName + assertFalse(specialRecord.getValues().containsKey("price")); // excluded field never appears + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // confirm that for the alternative api, we get a record that looks just like the input record (per its api meta data) // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + for(String version : List.of(TestUtils.V2022_Q4, TestUtils.V2023_Q1, TestUtils.V2023_Q2)) + { + QRecord alternativeQRecord = QRecordApiAdapter.qRecordsToApiVersionedQRecordList(List.of(person), TestUtils.TABLE_NAME_PERSON, TestUtils.ALTERNATIVE_API_NAME, version).get(0); + for(String key : person.getValues().keySet()) + { + if(key.equals("photo")) + { + //////////////////////////////////////////////////////////////////////////////////////// + // ok, well, skip the blob field (should be base64 version, and is covered elsewhere) // + //////////////////////////////////////////////////////////////////////////////////////// + continue; + } + + assertEquals(person.getValueString(key), ValueUtils.getValueAsString(alternativeQRecord.getValueString(key))); + } + } + } + } \ No newline at end of file diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/middleware/specs/ApiAwareSpecTestBase.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/middleware/specs/ApiAwareSpecTestBase.java new file mode 100644 index 00000000..c831c8fb --- /dev/null +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/middleware/specs/ApiAwareSpecTestBase.java @@ -0,0 +1,97 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.api.middleware.specs; + + +import com.kingsrook.qqq.api.TestUtils; +import com.kingsrook.qqq.api.middleware.specs.v1.ApiAwareMiddlewareVersionV1; +import com.kingsrook.qqq.api.model.APIVersion; +import com.kingsrook.qqq.backend.core.context.CapturedContext; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession; +import com.kingsrook.qqq.middleware.javalin.specs.AbstractMiddlewareVersion; +import com.kingsrook.qqq.middleware.javalin.specs.SpecTestBase; + + +/******************************************************************************* + ** + *******************************************************************************/ +public abstract class ApiAwareSpecTestBase extends SpecTestBase +{ + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + protected QInstance defineQInstance() throws QException + { + return (TestUtils.defineInstance()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + protected void primeTestData(QInstance qInstance) throws Exception + { + QContext.withTemporaryContext(new CapturedContext(qInstance, new QSystemUserSession()), () -> + { + TestUtils.insertSimpsons(); + TestUtils.insertTim2Shoes(); + }); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + protected AbstractMiddlewareVersion getMiddlewareVersion() + { + ApiAwareMiddlewareVersionV1 apiAwareMiddlewareVersionV1 = new ApiAwareMiddlewareVersionV1(); + + apiAwareMiddlewareVersionV1.addVersion(TestUtils.API_NAME, new APIVersion(TestUtils.V2022_Q4)); + apiAwareMiddlewareVersionV1.addVersion(TestUtils.API_NAME, new APIVersion(TestUtils.V2023_Q1)); + apiAwareMiddlewareVersionV1.addVersion(TestUtils.API_NAME, new APIVersion(TestUtils.V2023_Q2)); + apiAwareMiddlewareVersionV1.addVersion(TestUtils.ALTERNATIVE_API_NAME, new APIVersion(TestUtils.V2022_Q4)); + apiAwareMiddlewareVersionV1.addVersion(TestUtils.ALTERNATIVE_API_NAME, new APIVersion(TestUtils.V2023_Q1)); + apiAwareMiddlewareVersionV1.addVersion(TestUtils.ALTERNATIVE_API_NAME, new APIVersion(TestUtils.V2023_Q2)); + + return apiAwareMiddlewareVersionV1; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + protected String getBaseUrlAndPath(String apiPath, String version) + { + String path = "/qqq/" + getVersion() + "/" + apiPath + "/" + version; + return "http://localhost:" + PORT + path.replaceAll("/+", "/").replaceFirst("/$", ""); + } + +} diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/middleware/specs/v1/ApiAwareTableCountSpecV1Test.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/middleware/specs/v1/ApiAwareTableCountSpecV1Test.java new file mode 100644 index 00000000..b449f74c --- /dev/null +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/middleware/specs/v1/ApiAwareTableCountSpecV1Test.java @@ -0,0 +1,118 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.api.middleware.specs.v1; + + +import java.util.Map; +import com.kingsrook.qqq.api.TestUtils; +import com.kingsrook.qqq.api.middleware.specs.ApiAwareSpecTestBase; +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.utils.JsonUtils; +import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec; +import io.javalin.http.ContentType; +import kong.unirest.HttpResponse; +import kong.unirest.Unirest; +import org.json.JSONObject; +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 ApiAwareTableQuerySpecV1 + *******************************************************************************/ +class ApiAwareTableCountSpecV1Test extends ApiAwareSpecTestBase +{ + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + protected AbstractEndpointSpec getSpec() + { + return new ApiAwareTableCountSpecV1(); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + protected String getVersion() + { + return "v1"; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testBasicSuccess() + { + HttpResponse response = Unirest.post(getBaseUrlAndPath(TestUtils.API_PATH, TestUtils.V2023_Q1) + "/table/person/count") + .contentType(ContentType.APPLICATION_JSON.getMimeType()) + .body(JsonUtils.toJson(Map.of("filter", new QQueryFilter(new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, "Simpson"))))) + .asString(); + + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertThat(jsonObject.getInt("count")).isEqualTo(5); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQueryByOldFieldName() + { + ///////////////////////////////////////////////// + // old field name - make sure works in old api // + ///////////////////////////////////////////////// + HttpResponse response = Unirest.post(getBaseUrlAndPath(TestUtils.API_PATH, TestUtils.V2022_Q4) + "/table/person/count") + .contentType(ContentType.APPLICATION_JSON.getMimeType()) + .body(JsonUtils.toJson(Map.of("filter", new QQueryFilter(new QFilterCriteria("shoeCount", QCriteriaOperator.EQUALS, 2))))) + .asString(); + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertThat(jsonObject.getInt("count")).isGreaterThanOrEqualTo(1); + + ///////////////////////////////////////////////////// + // old field name - make sure fails in current api // + ///////////////////////////////////////////////////// + response = Unirest.post(getBaseUrlAndPath(TestUtils.API_PATH, TestUtils.V2023_Q1) + "/table/person/count") + .contentType(ContentType.APPLICATION_JSON.getMimeType()) + .body(JsonUtils.toJson(Map.of("filter", new QQueryFilter(new QFilterCriteria("shoeCount", QCriteriaOperator.EQUALS, 2))))) + .asString(); + assertEquals(400, response.getStatus()); + jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertEquals("Unrecognized criteria field name: shoeCount.", jsonObject.getString("error")); + } + +} \ No newline at end of file diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/middleware/specs/v1/ApiAwareTableMetaDataSpecV1Test.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/middleware/specs/v1/ApiAwareTableMetaDataSpecV1Test.java new file mode 100644 index 00000000..47ecba9e --- /dev/null +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/middleware/specs/v1/ApiAwareTableMetaDataSpecV1Test.java @@ -0,0 +1,95 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.api.middleware.specs.v1; + + +import com.kingsrook.qqq.api.TestUtils; +import com.kingsrook.qqq.api.middleware.specs.ApiAwareSpecTestBase; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec; +import kong.unirest.HttpResponse; +import kong.unirest.Unirest; +import org.json.JSONObject; +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.assertFalse; + + +/******************************************************************************* + ** + *******************************************************************************/ +class ApiAwareTableMetaDataSpecV1Test extends ApiAwareSpecTestBase +{ + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + protected AbstractEndpointSpec getSpec() + { + return new ApiAwareTableMetaDataSpecV1(); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + protected String getVersion() + { + return "v1"; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testBasicSuccess() + { + HttpResponse response = Unirest.get(getBaseUrlAndPath(TestUtils.API_PATH, TestUtils.V2023_Q1) + "/metaData/table/person").asString(); + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertThat(jsonObject.getJSONObject("fields").getJSONObject("noOfShoes").getString("label")).isEqualTo("No Of Shoes"); + assertFalse(jsonObject.getJSONObject("fields").has("shoeCount")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQueryOldVersion() + { + HttpResponse response = Unirest.get(getBaseUrlAndPath(TestUtils.API_PATH, TestUtils.V2022_Q4) + "/metaData/table/person").asString(); + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertFalse(jsonObject.getJSONObject("fields").has("noOfShoes")); + assertThat(jsonObject.getJSONObject("fields").getJSONObject("shoeCount").getString("label")).isEqualTo("Shoe Count"); + } + +} \ No newline at end of file diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/middleware/specs/v1/ApiAwareTableQuerySpecV1Test.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/middleware/specs/v1/ApiAwareTableQuerySpecV1Test.java new file mode 100644 index 00000000..b0a842e8 --- /dev/null +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/middleware/specs/v1/ApiAwareTableQuerySpecV1Test.java @@ -0,0 +1,273 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.api.middleware.specs.v1; + + +import java.util.Map; +import com.kingsrook.qqq.api.TestUtils; +import com.kingsrook.qqq.api.middleware.specs.ApiAwareSpecTestBase; +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.utils.JsonUtils; +import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec; +import io.javalin.http.ContentType; +import kong.unirest.HttpResponse; +import kong.unirest.Unirest; +import org.json.JSONArray; +import org.json.JSONObject; +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.assertFalse; + + +/******************************************************************************* + ** Unit test for ApiAwareTableQuerySpecV1 + *******************************************************************************/ +class ApiAwareTableQuerySpecV1Test extends ApiAwareSpecTestBase +{ + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + protected AbstractEndpointSpec getSpec() + { + return new ApiAwareTableQuerySpecV1(); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + protected String getVersion() + { + return "v1"; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testBasicSuccess() + { + HttpResponse response = Unirest.post(getBaseUrlAndPath(TestUtils.API_PATH, TestUtils.V2023_Q1) + "/table/person/query") + .contentType(ContentType.APPLICATION_JSON.getMimeType()) + .body(JsonUtils.toJson(Map.of("filter", new QQueryFilter(new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, "Simpson"))))) + .asString(); + + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + JSONArray records = jsonObject.getJSONArray("records"); + assertThat(records.length()).isGreaterThanOrEqualTo(1); + + JSONObject record = records.getJSONObject(0); + assertThat(record.getString("recordLabel")).contains("Simpson"); + assertThat(record.getString("tableName")).isEqualTo("person"); + assertThat(record.getJSONObject("values").getString("lastName")).isEqualTo("Simpson"); + assertThat(record.getJSONObject("displayValues").getString("lastName")).isEqualTo("Simpson"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testDisplayValues() + { + HttpResponse response = Unirest.post(getBaseUrlAndPath(TestUtils.API_PATH, TestUtils.V2023_Q1) + "/table/person/query") + .contentType(ContentType.APPLICATION_JSON.getMimeType()) + .body(JsonUtils.toJson(Map.of("filter", new QQueryFilter(new QFilterCriteria("bestFriendPersonId", QCriteriaOperator.IS_NOT_BLANK))))) + .asString(); + + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + JSONArray records = jsonObject.getJSONArray("records"); + assertThat(records.length()).isGreaterThanOrEqualTo(1); + + JSONObject record = records.getJSONObject(0); + assertThat(record.getString("tableName")).isEqualTo("person"); + assertThat(record.getJSONObject("values").getInt("bestFriendPersonId")).isEqualTo(1); + assertThat(record.getJSONObject("displayValues").getString("bestFriendPersonId")).isEqualTo("Homer Simpson"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testNoBody() + { + HttpResponse response = Unirest.post(getBaseUrlAndPath(TestUtils.API_PATH, TestUtils.V2023_Q1) + "/table/person/query") + .contentType(ContentType.APPLICATION_JSON.getMimeType()) + .asString(); + + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + JSONArray records = jsonObject.getJSONArray("records"); + assertThat(records.length()).isGreaterThanOrEqualTo(1); + } + + + + /******************************************************************************* + ** Note - same data cases as in QRecordApiAdapterTest + *******************************************************************************/ + @Test + void testVersions() + { + String requestBody = JsonUtils.toJson(Map.of("filter", new QQueryFilter(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, "Tim")))); + + ///////////////// + // old version // + ///////////////// + { + HttpResponse response = Unirest.post(getBaseUrlAndPath(TestUtils.API_PATH, TestUtils.V2022_Q4) + "/table/person/query") + .contentType(ContentType.APPLICATION_JSON.getMimeType()) + .body(requestBody) + .asString(); + + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + JSONArray records = jsonObject.getJSONArray("records"); + JSONObject record = records.getJSONObject(0); + assertThat(record.getJSONObject("values").getInt("shoeCount")).isEqualTo(2); + assertFalse(record.getJSONObject("values").has("noOfShoes")); + assertFalse(record.getJSONObject("values").has("cost")); + assertThat(record.getJSONObject("values").getString("photo")).isEqualTo("QUJDRA=="); + } + + ///////////////////// + // current version // + ///////////////////// + { + HttpResponse response = Unirest.post(getBaseUrlAndPath(TestUtils.API_PATH, TestUtils.V2023_Q1) + "/table/person/query") + .contentType(ContentType.APPLICATION_JSON.getMimeType()) + .body(requestBody) + .asString(); + + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + JSONArray records = jsonObject.getJSONArray("records"); + JSONObject record = records.getJSONObject(0); + assertFalse(record.getJSONObject("values").has("shoeCount")); + assertThat(record.getJSONObject("values").getInt("noOfShoes")).isEqualTo(2); + assertFalse(record.getJSONObject("values").has("cost")); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // future version ... not actually yet exposed, so, can't query on it // + // (unlike the QRecordApiAdapterTest that this is based on, that doesn't care about supported versions or not) // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /* + { + HttpResponse response = Unirest.post(getBaseUrlAndPath(TestUtils.API_PATH, TestUtils.V2023_Q2) + "/table/person/query") + .contentType(ContentType.APPLICATION_JSON.getMimeType()) + .body(requestBody) + .asString(); + + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + JSONArray records = jsonObject.getJSONArray("records"); + JSONObject record = records.getJSONObject(0); + assertFalse(record.getJSONObject("values").has("shoeCount")); + assertThat(record.getJSONObject("values").getInt("noOfShoes")).isEqualTo(2); + assertThat(record.getJSONObject("values").getString("cost")).isEqualTo("3.50"); + } + */ + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQueryByOldFieldName() + { + ///////////////////////////////////////////////// + // old field name - make sure works in old api // + ///////////////////////////////////////////////// + HttpResponse response = Unirest.post(getBaseUrlAndPath(TestUtils.API_PATH, TestUtils.V2022_Q4) + "/table/person/query") + .contentType(ContentType.APPLICATION_JSON.getMimeType()) + .body(JsonUtils.toJson(Map.of("filter", new QQueryFilter(new QFilterCriteria("shoeCount", QCriteriaOperator.EQUALS, 2))))) + .asString(); + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + JSONArray records = jsonObject.getJSONArray("records"); + assertThat(records.length()).isGreaterThanOrEqualTo(1); + + ///////////////////////////////////////////////////// + // old field name - make sure fails in current api // + ///////////////////////////////////////////////////// + response = Unirest.post(getBaseUrlAndPath(TestUtils.API_PATH, TestUtils.V2023_Q1) + "/table/person/query") + .contentType(ContentType.APPLICATION_JSON.getMimeType()) + .body(JsonUtils.toJson(Map.of("filter", new QQueryFilter(new QFilterCriteria("shoeCount", QCriteriaOperator.EQUALS, 2))))) + .asString(); + assertEquals(400, response.getStatus()); + jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertEquals("Unrecognized criteria field name: shoeCount.", jsonObject.getString("error")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testNotFoundCases() + { + HttpResponse response = Unirest.post(getBaseUrlAndPath(TestUtils.API_PATH, TestUtils.V2023_Q1 + "-no-such-version") + "/table/person/query") + .contentType(ContentType.APPLICATION_JSON.getMimeType()) + .asString(); + assertEquals(404, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertEquals("No API exists at the requested path.", jsonObject.getString("error")); + + response = Unirest.post(getBaseUrlAndPath("no-such-api", TestUtils.V2023_Q1) + "/table/person/query") + .contentType(ContentType.APPLICATION_JSON.getMimeType()) + .asString(); + assertEquals(404, response.getStatus()); + jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertEquals("No API exists at the requested path.", jsonObject.getString("error")); + + response = Unirest.post(getBaseUrlAndPath(TestUtils.API_PATH, TestUtils.V2023_Q1) + "/table/no-such-table/query") + .contentType(ContentType.APPLICATION_JSON.getMimeType()) + .asString(); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // todo - better as 403 (to make non-tables look like non-permissed tables, to avoid leaking that bit of data? // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + assertEquals(404, response.getStatus()); + jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertEquals("Could not find a table named no-such-table in this api.", jsonObject.getString("error")); + } + +} \ No newline at end of file