Initial build of api aware middleware

This commit is contained in:
2025-05-27 16:37:30 -05:00
parent 23e9ac5b61
commit afd8084d45
25 changed files with 2080 additions and 36 deletions

View File

@ -54,6 +54,28 @@
<version>${revision}</version>
</dependency>
<!-- qqq modules for tests only -->
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-middleware-javalin</artifactId>
<version>${revision}</version>
<classifier>tests</classifier>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-module-rdbms</artifactId>
<version>${revision}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.220</version>
<scope>test</scope>
</dependency>
<!-- 3rd party deps specifically for this module -->
<dependency>
<groupId>com.konghq</groupId>
@ -106,4 +128,4 @@
</plugins>
</build>
</project>
</project>

View File

@ -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);

View File

@ -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<GetTableApiFieldsInput, GetTableApiFieldsOutput>
{
private static final QLogger LOG = QLogger.getLogger(GetTableApiFieldsAction.class);
private static Map<ApiNameVersionAndTableName, List<QFieldMetaData>> fieldListCache = new HashMap<>();
private static Map<ApiNameVersionAndTableName, Map<String, QFieldMetaData>> fieldMapCache = new HashMap<>();
@ -141,13 +147,16 @@ public class GetTableApiFieldsAction extends AbstractQActionFunction<GetTableApi
QTableMetaData table = QContext.getQInstance().getTable(input.getTableName());
if(table == null)
{
throw (new QException("Unrecognized table name: " + input.getTableName()));
throw (new QNotFoundException("Unrecognized table name: " + input.getTableName()));
}
// todo - verify the table is in this version?
APIVersion version = new APIVersion(input.getVersion());
// todo - validate the version?
APIVersionRange tableApiVersionRange = getApiVersionRange(input.getApiName(), table);
if(!tableApiVersionRange.includes(version))
{
throw (new QNotFoundException("Table [" + input.getTableName() + "] was not found in this version of this api."));
}
///////////////////////////////////////////////////////
// get fields on the table which are in this version //
@ -180,6 +189,42 @@ public class GetTableApiFieldsAction extends AbstractQActionFunction<GetTableApi
/*******************************************************************************
**
*******************************************************************************/
private APIVersionRange getApiVersionRange(String apiName, QTableMetaData table) throws QNotFoundException
{
ApiTableMetaDataContainer apiTableMetaDataContainer = ApiTableMetaDataContainer.of(table);
if(apiTableMetaDataContainer == null)
{
LOG.debug("Returning not found because table doesn't have an apiTableMetaDataContainer", logPair("tableName", table.getName()));
throw (new QNotFoundException("Table [" + table.getName() + "] was not found in this api."));
}
ApiTableMetaData apiTableMetaData = apiTableMetaDataContainer.getApis().get(apiName);
if(apiTableMetaData == null)
{
LOG.debug("Returning not found because api isn't present in table's apiTableMetaDataContainer", logPair("apiName", apiName), logPair("tableName", table.getName()));
throw (new QNotFoundException("Table [" + table.getName() + "] was not found in this api."));
}
if(apiTableMetaData.getInitialVersion() != null)
{
if(apiTableMetaData.getFinalVersion() != null)
{
return (APIVersionRange.betweenAndIncluding(apiTableMetaData.getInitialVersion(), apiTableMetaData.getFinalVersion()));
}
else
{
return (APIVersionRange.afterAndIncluding(apiTableMetaData.getInitialVersion()));
}
}
return (APIVersionRange.none());
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -29,6 +29,9 @@ import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.api.actions.output.ApiOutputMapWrapper;
import com.kingsrook.qqq.api.actions.output.ApiOutputQRecordWrapper;
import com.kingsrook.qqq.api.actions.output.ApiOutputRecordWrapperInterface;
import com.kingsrook.qqq.api.javalin.QBadRequestException;
import com.kingsrook.qqq.api.model.APIVersion;
import com.kingsrook.qqq.api.model.APIVersionRange;
@ -84,6 +87,46 @@ public class QRecordApiAdapter
** ApiFieldCustomValueMapperBulkSupportInterface's in the bulky way.
*******************************************************************************/
public static ArrayList<Map<String, Serializable>> qRecordsToApiMapList(List<QRecord> records, String tableName, String apiName, String apiVersion) throws QException
{
Map<String, ApiFieldCustomValueMapper> fieldValueMappers = getFieldValueMappers(records, tableName, apiName, apiVersion);
ArrayList<Map<String, Serializable>> 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<QRecord> qRecordsToApiVersionedQRecordList(List<QRecord> records, String tableName, String apiName, String apiVersion) throws QException
{
Map<String, ApiFieldCustomValueMapper> fieldValueMappers = getFieldValueMappers(records, tableName, apiName, apiVersion);
List<QRecord> 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<String, ApiFieldCustomValueMapper> getFieldValueMappers(List<QRecord> records, String tableName, String apiName, String apiVersion) throws QException
{
Map<String, ApiFieldCustomValueMapper> 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<Map<String, Serializable>> 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<String, Serializable> qRecordToApiMap(QRecord record, String tableName, String apiName, String apiVersion, Map<String, ApiFieldCustomValueMapper> fieldValueMappers) throws QException
private static <C, O extends ApiOutputRecordWrapperInterface<C, O>> O qRecordToApiMap(QRecord record, String tableName, String apiName, String apiVersion, Map<String, ApiFieldCustomValueMapper> fieldValueMappers, O output) throws QException
{
if(record == null)
{
return (null);
}
List<QFieldMetaData> tableApiFields = GetTableApiFieldsAction.getTableApiFieldList(new GetTableApiFieldsAction.ApiNameVersionAndTableName(apiName, apiVersion, tableName));
LinkedHashMap<String, Serializable> outputRecord = new LinkedHashMap<>();
List<QFieldMetaData> 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<Map<String, Serializable>> associationList = new ArrayList<>();
outputRecord.put(association.getName(), associationList);
ArrayList<O> associationList = new ArrayList<>();
for(QRecord associatedRecord : CollectionUtils.nonNullList(CollectionUtils.nonNullMap(record.getAssociatedRecords()).get(association.getName())))
{
associationList.add(qRecordToApiMap(associatedRecord, association.getAssociatedTableName(), apiName, apiVersion));
ApiOutputRecordWrapperInterface<C, O> 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);
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Map<String, Serializable>, ApiOutputMapWrapper>
{
private Map<String, Serializable> apiMap;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ApiOutputMapWrapper(Map<String, Serializable> apiMap)
{
this.apiMap = apiMap;
}
/***************************************************************************
**
***************************************************************************/
@Override
public void putValue(String key, Serializable value)
{
apiMap.put(key, value);
}
/***************************************************************************
**
***************************************************************************/
@Override
public void putAssociation(String key, List<ApiOutputMapWrapper> values)
{
ArrayList<Map<String, Serializable>> 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<String, Serializable> getContents()
{
return this.apiMap;
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<QRecord, ApiOutputQRecordWrapper>
{
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<ApiOutputQRecordWrapper> values)
{
List<QRecord> 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;
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<C, A extends ApiOutputRecordWrapperInterface<C, A>>
{
/***************************************************************************
** 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<A> values);
/***************************************************************************
** create a new "sibling" object to this - e.g., a wrapper around a new
** instance of the contents object
***************************************************************************/
ApiOutputRecordWrapperInterface<C, A> 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;
}
}

View File

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

View File

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

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String, QFieldMetaData> 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<QFrontendExposedJoin> 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<String, QFieldMetaData> 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<String, QFieldMetaData> getFieldsForApiVersion(String tableName) throws QException
{
GetTableApiFieldsInput getTableApiFieldsInput = new GetTableApiFieldsInput()
.withApiName(getApiName())
.withVersion(getApiVersion())
.withTableName(tableName);
GetTableApiFieldsOutput tableApiFieldsOutput = new GetTableApiFieldsAction().execute(getTableApiFieldsInput);
List<QFieldMetaData> fields = tableApiFieldsOutput.getFields();
Map<String, QFieldMetaData> fieldMap = CollectionUtils.listToMap(fields, f -> f.getName());
return fieldMap;
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String> 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<String, QFieldMetaData> 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<QRecord> versionedRecords = QRecordApiAdapter.qRecordsToApiVersionedQRecordList(queryOutput.getRecords(), table.getName(), getApiName(), getApiVersion());
QValueFormatter.setDisplayValuesInRecordsIncludingPossibleValueTranslations(table, versionedRecords);
output.setRecords(versionedRecords);
}
/***************************************************************************
**
***************************************************************************/
private static void manageOrderByFields(QQueryFilter filter, Map<String, QFieldMetaData> tableApiFields, List<String> 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);
}
}
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String, QFieldMetaData> tableApiFields, List<String> 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<String> 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)));
}
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String, ApiNameAndVersions> apiNameAndVersionsByPath = new HashMap<>();
private List<AbstractEndpointSpec<?, ?, ?>> specs;
/***************************************************************************
**
***************************************************************************/
private record ApiNameAndVersions(String apiName, Set<String> 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<AbstractEndpointSpec<?, ?, ?>> defineEndpointSpecs()
{
List<AbstractEndpointSpec<?, ?, ?>> specs = new ArrayList<>(super.getEndpointSpecs());
ListIterator<AbstractEndpointSpec<?, ?, ?>> 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<AbstractEndpointSpec<?, ?, ?>> 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<String> 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.");
}
}

View File

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

View File

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

View File

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

View File

@ -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);
}
}
/*******************************************************************************
**

View File

@ -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;

View File

@ -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))
);
}
}

View File

@ -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<? extends QFieldMetaData> 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<? extends QFieldMetaData> 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);
}
}

View File

@ -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<String, Serializable> 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)));
}
}
}
}

View File

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

View File

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

View File

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

View File

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