From d4186287ceacd24052949ce7ec7007b6e605f5d9 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 11 Aug 2022 17:01:35 -0500 Subject: [PATCH 01/19] Add Memory backend module --- .../backend/QBackendModuleDispatcher.java | 1 + .../memory/MemoryBackendModule.java | 116 +++++++++ .../memory/MemoryCountAction.java | 54 ++++ .../memory/MemoryDeleteAction.java | 55 ++++ .../memory/MemoryInsertAction.java | 55 ++++ .../memory/MemoryQueryAction.java | 55 ++++ .../memory/MemoryRecordStore.java | 238 ++++++++++++++++++ .../memory/MemoryUpdateAction.java | 55 ++++ .../memory/MemoryBackendModuleTest.java | 185 ++++++++++++++ .../qqq/backend/core/utils/TestUtils.java | 41 ++- .../filesystem/sync/FilesystemSyncStep.java | 2 +- 11 files changed, 853 insertions(+), 4 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModule.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryCountAction.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryDeleteAction.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryInsertAction.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryQueryAction.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryUpdateAction.java create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleDispatcher.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleDispatcher.java index c57afd5b..19161379 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleDispatcher.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleDispatcher.java @@ -72,6 +72,7 @@ public class QBackendModuleDispatcher // todo - let modules somehow "export" their types here? // e.g., backend-core shouldn't need to "know" about the modules. "com.kingsrook.qqq.backend.core.modules.backend.implementations.mock.MockBackendModule", + "com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule", "com.kingsrook.qqq.backend.module.rdbms.RDBMSBackendModule", "com.kingsrook.qqq.backend.module.filesystem.local.FilesystemBackendModule", "com.kingsrook.qqq.backend.module.filesystem.s3.S3BackendModule" diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModule.java new file mode 100644 index 00000000..84852cb9 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModule.java @@ -0,0 +1,116 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory; + + +import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface; +import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface; +import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface; +import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface; +import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface; +import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; +import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; + + +/******************************************************************************* + ** A simple (probably only valid for testing?) implementation of the QModuleInterface, + ** that just stores its records in-memory. + ** + *******************************************************************************/ +public class MemoryBackendModule implements QBackendModuleInterface +{ + /******************************************************************************* + ** Method where a backend module must be able to provide its type (name). + *******************************************************************************/ + @Override + public String getBackendType() + { + return ("memory"); + } + + + + /******************************************************************************* + ** Method to identify the class used for backend meta data for this module. + *******************************************************************************/ + @Override + public Class getBackendMetaDataClass() + { + return (QBackendMetaData.class); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public CountInterface getCountInterface() + { + return new MemoryCountAction(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public QueryInterface getQueryInterface() + { + return new MemoryQueryAction(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public InsertInterface getInsertInterface() + { + return (new MemoryInsertAction()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public UpdateInterface getUpdateInterface() + { + return (new MemoryUpdateAction()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public DeleteInterface getDeleteInterface() + { + return (new MemoryDeleteAction()); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryCountAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryCountAction.java new file mode 100644 index 00000000..4f5e0d67 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryCountAction.java @@ -0,0 +1,54 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory; + + +import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; + + +/******************************************************************************* + ** In-memory version of count action. + ** + *******************************************************************************/ +public class MemoryCountAction implements CountInterface +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public CountOutput execute(CountInput countInput) throws QException + { + try + { + CountOutput countOutput = new CountOutput(); + countOutput.setCount(MemoryRecordStore.getInstance().count(countInput)); + return (countOutput); + } + catch(Exception e) + { + throw new QException("Error executing count", e); + } + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryDeleteAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryDeleteAction.java new file mode 100644 index 00000000..50c5e239 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryDeleteAction.java @@ -0,0 +1,55 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory; + + +import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput; + + +/******************************************************************************* + ** In-memory version of delete action. + ** + *******************************************************************************/ +public class MemoryDeleteAction implements DeleteInterface +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public DeleteOutput execute(DeleteInput deleteInput) throws QException + { + try + { + DeleteOutput deleteOutput = new DeleteOutput(); + deleteOutput.setDeletedRecordCount(MemoryRecordStore.getInstance().delete(deleteInput)); + return (deleteOutput); + } + catch(Exception e) + { + throw new QException("Error executing delete: " + e.getMessage(), e); + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryInsertAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryInsertAction.java new file mode 100644 index 00000000..01839359 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryInsertAction.java @@ -0,0 +1,55 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory; + + +import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; + + +/******************************************************************************* + ** In-memory version of insert action. + ** + *******************************************************************************/ +public class MemoryInsertAction implements InsertInterface +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public InsertOutput execute(InsertInput insertInput) throws QException + { + try + { + InsertOutput insertOutput = new InsertOutput(); + insertOutput.setRecords(MemoryRecordStore.getInstance().insert(insertInput, true)); + return (insertOutput); + } + catch(Exception e) + { + throw new QException("Error executing insert: " + e.getMessage(), e); + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryQueryAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryQueryAction.java new file mode 100644 index 00000000..cd0e8bf7 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryQueryAction.java @@ -0,0 +1,55 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory; + + +import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; + + +/******************************************************************************* + ** In-memory version of query action. + ** + *******************************************************************************/ +public class MemoryQueryAction implements QueryInterface +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public QueryOutput execute(QueryInput queryInput) throws QException + { + try + { + QueryOutput queryOutput = new QueryOutput(queryInput); + queryOutput.addRecords(MemoryRecordStore.getInstance().query(queryInput)); + return (queryOutput); + } + catch(Exception e) + { + throw new QException("Error executing query", e); + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java new file mode 100644 index 00000000..8fc7a02b --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java @@ -0,0 +1,238 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; +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.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; + + +/******************************************************************************* + ** Storage provider for the MemoryBackendModule + *******************************************************************************/ +public class MemoryRecordStore +{ + private static MemoryRecordStore instance; + + private Map> data; + private Map nextSerials; + + + + /******************************************************************************* + ** private singleton constructor + *******************************************************************************/ + private MemoryRecordStore() + { + data = new HashMap<>(); + nextSerials = new HashMap<>(); + } + + + + /******************************************************************************* + ** Forget all data in the memory store... + *******************************************************************************/ + public void reset() + { + data.clear(); + nextSerials.clear(); + } + + + + /******************************************************************************* + ** singleton accessor + *******************************************************************************/ + public static MemoryRecordStore getInstance() + { + if(instance == null) + { + instance = new MemoryRecordStore(); + } + return (instance); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private Map getTableData(QTableMetaData table) + { + if(!data.containsKey(table.getName())) + { + data.put(table.getName(), new HashMap<>()); + } + return (data.get(table.getName())); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public List query(QueryInput input) + { + Map tableData = getTableData(input.getTable()); + List records = new ArrayList<>(tableData.values()); + // todo - filtering + return (records); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Integer count(CountInput input) + { + Map tableData = getTableData(input.getTable()); + List records = new ArrayList<>(tableData.values()); + // todo - filtering + return (records.size()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public List insert(InsertInput input, boolean returnInsertedRecords) + { + if(input.getRecords() == null) + { + return (new ArrayList<>()); + } + + QTableMetaData table = input.getTable(); + Map tableData = getTableData(table); + Integer nextSerial = nextSerials.get(table.getName()); + if(nextSerial == null) + { + nextSerial = 1; + while(tableData.containsKey(nextSerial)) + { + nextSerial++; + } + } + + List outputRecords = new ArrayList<>(); + QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField()); + for(QRecord record : input.getRecords()) + { + if(record.getValue(primaryKeyField.getName()) == null && primaryKeyField.getType().equals(QFieldType.INTEGER)) + { + record.setValue(primaryKeyField.getName(), nextSerial++); + } + + tableData.put(record.getValue(primaryKeyField.getName()), record); + if(returnInsertedRecords) + { + outputRecords.add(record); + } + } + + return (outputRecords); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public List update(UpdateInput input, boolean returnUpdatedRecords) + { + if(input.getRecords() == null) + { + return (new ArrayList<>()); + } + + QTableMetaData table = input.getTable(); + Map tableData = getTableData(table); + + List outputRecords = new ArrayList<>(); + QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField()); + for(QRecord record : input.getRecords()) + { + Serializable primaryKeyValue = record.getValue(primaryKeyField.getName()); + if(tableData.containsKey(primaryKeyValue)) + { + QRecord recordToUpdate = tableData.get(primaryKeyValue); + for(Map.Entry valueEntry : record.getValues().entrySet()) + { + recordToUpdate.setValue(valueEntry.getKey(), valueEntry.getValue()); + } + + if(returnUpdatedRecords) + { + outputRecords.add(record); + } + } + else + { + outputRecords.add(record); + } + } + + return (outputRecords); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public int delete(DeleteInput input) + { + if(input.getPrimaryKeys() == null) + { + return (0); + } + + QTableMetaData table = input.getTable(); + Map tableData = getTableData(table); + int rowsDeleted = 0; + for(Serializable primaryKeyValue : input.getPrimaryKeys()) + { + if(tableData.containsKey(primaryKeyValue)) + { + tableData.remove(primaryKeyValue); + rowsDeleted++; + } + } + + return (rowsDeleted); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryUpdateAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryUpdateAction.java new file mode 100644 index 00000000..97793ce6 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryUpdateAction.java @@ -0,0 +1,55 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory; + + +import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput; + + +/******************************************************************************* + ** In-memory version of update action. + ** + *******************************************************************************/ +public class MemoryUpdateAction implements UpdateInterface +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public UpdateOutput execute(UpdateInput updateInput) throws QException + { + try + { + UpdateOutput updateOutput = new UpdateOutput(); + updateOutput.setRecords(MemoryRecordStore.getInstance().update(updateInput, true)); + return (updateOutput); + } + catch(Exception e) + { + throw new QException("Error executing update: " + e.getMessage(), e); + } + } + +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java new file mode 100644 index 00000000..f07e1b9a --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java @@ -0,0 +1,185 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.tables.CountAction; +import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; +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.actions.tables.update.UpdateInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/******************************************************************************* + ** Unit test for MemoryBackendModule + *******************************************************************************/ +class MemoryBackendModuleTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @AfterEach + void afterEach() + { + MemoryRecordStore.getInstance().reset(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFullCRUD() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_SHAPE); + QSession session = new QSession(); + + ///////////////////////// + // do an initial count // + ///////////////////////// + CountInput countInput = new CountInput(qInstance); + countInput.setSession(session); + countInput.setTableName(table.getName()); + assertEquals(0, new CountAction().execute(countInput).getCount()); + + ////////////////// + // do an insert // + ////////////////// + InsertInput insertInput = new InsertInput(qInstance); + insertInput.setSession(session); + insertInput.setTableName(table.getName()); + insertInput.setRecords(List.of( + new QRecord() + .withTableName(table.getName()) + .withValue("name", "My Triangle") + .withValue("type", "triangle") + .withValue("noOfSides", 3) + .withValue("isPolygon", true), + new QRecord() + .withTableName(table.getName()) + .withValue("name", "Your Square") + .withValue("type", "square") + .withValue("noOfSides", 4) + .withValue("isPolygon", true), + new QRecord() + .withTableName(table.getName()) + .withValue("name", "Some Circle") + .withValue("type", "circle") + .withValue("noOfSides", null) + .withValue("isPolygon", false) + )); + InsertOutput insertOutput = new InsertAction().execute(insertInput); + assertEquals(insertOutput.getRecords().size(), 3); + assertTrue(insertOutput.getRecords().stream().allMatch(r -> r.getValue("id") != null)); + assertTrue(insertOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(1))); + assertTrue(insertOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(2))); + assertTrue(insertOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(3))); + + //////////////// + // do a query // + //////////////// + QueryInput queryInput = new QueryInput(qInstance); + queryInput.setSession(session); + queryInput.setTableName(table.getName()); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(queryOutput.getRecords().size(), 3); + assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValue("id") != null)); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(1))); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(2))); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(3))); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueString("name").equals("My Triangle"))); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueString("name").equals("Your Square"))); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueString("name").equals("Some Circle"))); + + assertEquals(3, new CountAction().execute(countInput).getCount()); + + ////////////////// + // do an update // + ////////////////// + UpdateInput updateInput = new UpdateInput(qInstance); + updateInput.setSession(session); + updateInput.setTableName(table.getName()); + updateInput.setRecords(List.of( + new QRecord() + .withTableName(table.getName()) + .withValue("id", 1) + .withValue("name", "Not My Triangle any more"), + new QRecord() + .withTableName(table.getName()) + .withValue("id", 3) + .withValue("type", "ellipse") + )); + UpdateOutput updateOutput = new UpdateAction().execute(updateInput); + assertEquals(updateOutput.getRecords().size(), 2); + + queryOutput = new QueryAction().execute(queryInput); + assertEquals(queryOutput.getRecords().size(), 3); + assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueString("name").equals("My Triangle"))); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueString("name").equals("Not My Triangle any more"))); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueString("type").equals("ellipse"))); + assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueString("type").equals("circle"))); + + assertEquals(3, new CountAction().execute(countInput).getCount()); + + ///////////////// + // do a delete // + ///////////////// + DeleteInput deleteInput = new DeleteInput(qInstance); + deleteInput.setSession(session); + deleteInput.setTableName(table.getName()); + deleteInput.setPrimaryKeys(List.of(1, 2)); + DeleteOutput deleteOutput = new DeleteAction().execute(deleteInput); + assertEquals(deleteOutput.getDeletedRecordCount(), 2); + + assertEquals(1, new CountAction().execute(countInput).getCount()); + + queryOutput = new QueryAction().execute(queryInput); + assertEquals(queryOutput.getRecords().size(), 1); + assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueInteger("id").equals(1))); + assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueInteger("id").equals(2))); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(3))); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java index a0711f87..aa537948 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java @@ -26,7 +26,6 @@ import java.util.List; import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.AddAge; import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.GetAgeStatistics; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; -import com.kingsrook.qqq.backend.core.adapters.QInstanceAdapter; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; @@ -52,6 +51,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.modules.authentication.MockAuthenticationModule; import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; +import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule; import com.kingsrook.qqq.backend.core.modules.backend.implementations.mock.MockBackendModule; import com.kingsrook.qqq.backend.core.processes.implementations.etl.basic.BasicETLProcess; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamed.StreamedETLProcess; @@ -65,12 +65,14 @@ import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackend public class TestUtils { public static final String DEFAULT_BACKEND_NAME = "default"; + public static final String MEMORY_BACKEND_NAME = "memory"; public static final String APP_NAME_GREETINGS = "greetingsApp"; public static final String APP_NAME_PEOPLE = "peopleApp"; public static final String APP_NAME_MISCELLANEOUS = "miscellaneous"; public static final String TABLE_NAME_PERSON = "person"; + public static final String TABLE_NAME_SHAPE = "shape"; public static final String PROCESS_NAME_GREET_PEOPLE = "greet"; public static final String PROCESS_NAME_GREET_PEOPLE_INTERACTIVE = "greetInteractive"; @@ -89,10 +91,12 @@ public class TestUtils QInstance qInstance = new QInstance(); qInstance.setAuthentication(defineAuthentication()); qInstance.addBackend(defineBackend()); + qInstance.addBackend(defineMemoryBackend()); qInstance.addTable(defineTablePerson()); qInstance.addTable(definePersonFileTable()); qInstance.addTable(defineTableIdAndNameOnly()); + qInstance.addTable(defineTableShape()); qInstance.addPossibleValueSource(defineStatesPossibleValueSource()); @@ -104,8 +108,6 @@ public class TestUtils defineApps(qInstance); - System.out.println(new QInstanceAdapter().qInstanceToJson(qInstance)); - return (qInstance); } @@ -174,6 +176,18 @@ public class TestUtils + /******************************************************************************* + ** Define the in-memory backend used in standard tests + *******************************************************************************/ + public static QBackendMetaData defineMemoryBackend() + { + return new QBackendMetaData() + .withName(MEMORY_BACKEND_NAME) + .withBackendType(MemoryBackendModule.class); + } + + + /******************************************************************************* ** Define the 'person' table used in standard tests. *******************************************************************************/ @@ -196,6 +210,27 @@ public class TestUtils + /******************************************************************************* + ** Define the 'shape' table used in standard tests. + *******************************************************************************/ + public static QTableMetaData defineTableShape() + { + return new QTableMetaData() + .withName(TABLE_NAME_SHAPE) + .withBackendName(MEMORY_BACKEND_NAME) + .withPrimaryKeyField("id") + .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) + .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false)) + .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false)) + .withField(new QFieldMetaData("name", QFieldType.STRING)) + .withField(new QFieldMetaData("type", QFieldType.STRING)) // todo PVS + .withField(new QFieldMetaData("noOfSides", QFieldType.INTEGER)) + .withField(new QFieldMetaData("isPolygon", QFieldType.BOOLEAN)) // mmm, should be derived from type, no? + ; + } + + + /******************************************************************************* ** Define a 2nd version of the 'person' table for this test (pretend it's backed by a file) *******************************************************************************/ diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/processes/implementations/filesystem/sync/FilesystemSyncStep.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/processes/implementations/filesystem/sync/FilesystemSyncStep.java index 1ea36990..8464bb22 100644 --- a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/processes/implementations/filesystem/sync/FilesystemSyncStep.java +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/processes/implementations/filesystem/sync/FilesystemSyncStep.java @@ -91,7 +91,7 @@ public class FilesystemSyncStep implements BackendStep String sourceFileName = sourceEntry.getKey(); if(!archiveFiles.contains(sourceFileName)) { - LOG.info("Syncing file [" + sourceFileName + "] to [" + archiveTable + "] and [" + processingTable + "]"); + LOG.info("Syncing file [" + sourceFileName + "] to [" + archiveTable.getName() + "] and [" + processingTable.getName() + "]"); InputStream inputStream = sourceActionBase.readFile(sourceEntry.getValue()); byte[] bytes = inputStream.readAllBytes(); From 965bc5bf295887b1f38f715d26ea09260ab5e79e Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Fri, 12 Aug 2022 11:40:18 -0500 Subject: [PATCH 02/19] added getValueLocalTime --- .../qqq/backend/core/model/data/QRecord.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java index 3c30366f..bf51c6fb 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.model.data; import java.io.Serializable; import java.math.BigDecimal; import java.time.LocalDate; +import java.time.LocalTime; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -328,6 +329,16 @@ public class QRecord implements Serializable + /******************************************************************************* + ** + *******************************************************************************/ + public LocalTime getValueLocalTime(String fieldName) + { + return ((LocalTime) ValueUtils.getValueAsLocalTime(values.get(fieldName))); + } + + + /******************************************************************************* ** *******************************************************************************/ From 52121cc4f327bc9b41b54524ad4f650c298b99b5 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 12 Aug 2022 11:39:39 -0500 Subject: [PATCH 03/19] Adding POST_QUERY_RECORD customizer; formalizing customizers a bit more --- .../actions/customizers/CustomizerLoader.java | 96 +++++++++++++ .../core/actions/customizers/Customizers.java | 32 +++++ .../QInstanceValidationException.java | 2 +- .../actions/tables/query/QueryOutput.java | 31 +++++ .../model/metadata/tables/QTableMetaData.java | 7 +- .../memory/MemoryBackendModuleTest.java | 126 ++++++++++++++---- .../FilesystemBackendModuleInterface.java | 2 +- .../actions/AbstractBaseFilesystemAction.java | 13 +- .../base/actions/FilesystemCustomizers.java | 35 +++++ .../local/FilesystemBackendModule.java | 12 ++ .../local/actions/FilesystemCountAction.java | 55 ++++++++ .../actions/FilesystemQueryActionTest.java | 12 +- 12 files changed, 378 insertions(+), 45 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/CustomizerLoader.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/Customizers.java create mode 100644 qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/FilesystemCustomizers.java create mode 100644 qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemCountAction.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/CustomizerLoader.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/CustomizerLoader.java new file mode 100644 index 00000000..d0ef11d8 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/CustomizerLoader.java @@ -0,0 +1,96 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.customizers; + + +import java.util.Optional; +import java.util.function.Function; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + + +/******************************************************************************* + ** Utility to load code for running QQQ customizers. + *******************************************************************************/ +public class CustomizerLoader +{ + private static final Logger LOG = LogManager.getLogger(CustomizerLoader.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Function getTableCustomizerFunction(QTableMetaData table, String customizerName) + { + Optional codeReference = table.getCustomizer(customizerName); + if(codeReference.isPresent()) + { + return (CustomizerLoader.getFunction(codeReference.get())); + } + + return null; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("unchecked") + public static Function getFunction(QCodeReference codeReference) + { + if(codeReference == null) + { + return (null); + } + + if(!codeReference.getCodeType().equals(QCodeType.JAVA)) + { + /////////////////////////////////////////////////////////////////////////////////////// + // todo - 1) support more languages, 2) wrap them w/ java Functions here, 3) profit! // + /////////////////////////////////////////////////////////////////////////////////////// + throw (new IllegalArgumentException("Only JAVA customizers are supported at this time.")); + } + + try + { + Class customizerClass = Class.forName(codeReference.getName()); + return ((Function) customizerClass.getConstructor().newInstance()); + } + catch(Exception e) + { + LOG.error("Error initializing customizer: " + codeReference); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////// + // return null here - under the assumption that during normal run-time operations, we'll never hit here // + // as we'll want to validate all functions in the instance validator at startup time (and IT will throw // + // if it finds an invalid code reference // + ////////////////////////////////////////////////////////////////////////////////////////////////////////// + return (null); + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/Customizers.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/Customizers.java new file mode 100644 index 00000000..4da39af3 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/Customizers.java @@ -0,0 +1,32 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.customizers; + + +/******************************************************************************* + ** Standard place where the names of QQQ Customization points are defined. + *******************************************************************************/ +public interface Customizers +{ + String POST_QUERY_RECORD = "postQueryRecord"; + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QInstanceValidationException.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QInstanceValidationException.java index 311ec0a8..fa090ca1 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QInstanceValidationException.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QInstanceValidationException.java @@ -57,7 +57,7 @@ public class QInstanceValidationException extends QException { super( (reasons != null && reasons.size() > 0) - ? "Instance validation failed for the following reasons: " + StringUtils.joinWithCommasAndAnd(reasons) + ? "Instance validation failed for the following reasons:\n - " + StringUtils.join("\n - ", reasons) : "Validation failed, but no reasons were provided"); if(reasons != null && reasons.size() > 0) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutput.java index a9e19342..96340a69 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutput.java @@ -24,8 +24,13 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query; import java.io.Serializable; import java.util.List; +import java.util.function.Function; +import com.kingsrook.qqq.backend.core.actions.customizers.CustomizerLoader; +import com.kingsrook.qqq.backend.core.actions.customizers.Customizers; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; /******************************************************************************* @@ -34,8 +39,12 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord; *******************************************************************************/ public class QueryOutput extends AbstractActionOutput implements Serializable { + private static final Logger LOG = LogManager.getLogger(QueryOutput.class); + private QueryOutputStorageInterface storage; + private Function postQueryRecordCustomizer; + /******************************************************************************* @@ -52,6 +61,8 @@ public class QueryOutput extends AbstractActionOutput implements Serializable { storage = new QueryOutputList(); } + + postQueryRecordCustomizer = (Function) CustomizerLoader.getTableCustomizerFunction(queryInput.getTable(), Customizers.POST_QUERY_RECORD); } @@ -65,16 +76,36 @@ public class QueryOutput extends AbstractActionOutput implements Serializable *******************************************************************************/ public void addRecord(QRecord record) { + record = runPostQueryRecordCustomizer(record); storage.addRecord(record); } + /******************************************************************************* + ** + *******************************************************************************/ + public QRecord runPostQueryRecordCustomizer(QRecord record) + { + if(this.postQueryRecordCustomizer != null) + { + record = this.postQueryRecordCustomizer.apply(record); + } + return record; + } + + + /******************************************************************************* ** add a list of records to this output *******************************************************************************/ public void addRecords(List records) { + if(this.postQueryRecordCustomizer != null) + { + records.replaceAll(t -> this.postQueryRecordCustomizer.apply(t)); + } + storage.addRecords(records); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java index 0fec3d48..2f920886 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java @@ -408,12 +408,7 @@ public class QTableMetaData implements QAppChildMetaData, Serializable } QCodeReference function = customizers.get(customizerName); - if(function == null) - { - throw (new IllegalArgumentException("Customizer [" + customizerName + "] was not found in table [" + name + "].")); - } - - return (Optional.of(function)); + return (Optional.ofNullable(function)); } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java index f07e1b9a..ec1b2673 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java @@ -23,6 +23,8 @@ package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory; import java.util.List; +import java.util.function.Function; +import com.kingsrook.qqq.backend.core.actions.customizers.Customizers; import com.kingsrook.qqq.backend.core.actions.tables.CountAction; import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; @@ -40,6 +42,8 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.utils.TestUtils; @@ -90,28 +94,9 @@ class MemoryBackendModuleTest InsertInput insertInput = new InsertInput(qInstance); insertInput.setSession(session); insertInput.setTableName(table.getName()); - insertInput.setRecords(List.of( - new QRecord() - .withTableName(table.getName()) - .withValue("name", "My Triangle") - .withValue("type", "triangle") - .withValue("noOfSides", 3) - .withValue("isPolygon", true), - new QRecord() - .withTableName(table.getName()) - .withValue("name", "Your Square") - .withValue("type", "square") - .withValue("noOfSides", 4) - .withValue("isPolygon", true), - new QRecord() - .withTableName(table.getName()) - .withValue("name", "Some Circle") - .withValue("type", "circle") - .withValue("noOfSides", null) - .withValue("isPolygon", false) - )); + insertInput.setRecords(getTestRecords(table)); InsertOutput insertOutput = new InsertAction().execute(insertInput); - assertEquals(insertOutput.getRecords().size(), 3); + assertEquals(3, insertOutput.getRecords().size()); assertTrue(insertOutput.getRecords().stream().allMatch(r -> r.getValue("id") != null)); assertTrue(insertOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(1))); assertTrue(insertOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(2))); @@ -124,7 +109,7 @@ class MemoryBackendModuleTest queryInput.setSession(session); queryInput.setTableName(table.getName()); QueryOutput queryOutput = new QueryAction().execute(queryInput); - assertEquals(queryOutput.getRecords().size(), 3); + assertEquals(3, queryOutput.getRecords().size()); assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValue("id") != null)); assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(1))); assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(2))); @@ -152,10 +137,10 @@ class MemoryBackendModuleTest .withValue("type", "ellipse") )); UpdateOutput updateOutput = new UpdateAction().execute(updateInput); - assertEquals(updateOutput.getRecords().size(), 2); + assertEquals(2, updateOutput.getRecords().size()); queryOutput = new QueryAction().execute(queryInput); - assertEquals(queryOutput.getRecords().size(), 3); + assertEquals(3, queryOutput.getRecords().size()); assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueString("name").equals("My Triangle"))); assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueString("name").equals("Not My Triangle any more"))); assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueString("type").equals("ellipse"))); @@ -171,15 +156,104 @@ class MemoryBackendModuleTest deleteInput.setTableName(table.getName()); deleteInput.setPrimaryKeys(List.of(1, 2)); DeleteOutput deleteOutput = new DeleteAction().execute(deleteInput); - assertEquals(deleteOutput.getDeletedRecordCount(), 2); + assertEquals(2, deleteOutput.getDeletedRecordCount()); assertEquals(1, new CountAction().execute(countInput).getCount()); queryOutput = new QueryAction().execute(queryInput); - assertEquals(queryOutput.getRecords().size(), 1); + assertEquals(1, queryOutput.getRecords().size()); assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueInteger("id").equals(1))); assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueInteger("id").equals(2))); assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(3))); } + + + /******************************************************************************* + ** + *******************************************************************************/ + private List getTestRecords(QTableMetaData table) + { + return List.of( + new QRecord() + .withTableName(table.getName()) + .withValue("name", "My Triangle") + .withValue("type", "triangle") + .withValue("noOfSides", 3) + .withValue("isPolygon", true), + new QRecord() + .withTableName(table.getName()) + .withValue("name", "Your Square") + .withValue("type", "square") + .withValue("noOfSides", 4) + .withValue("isPolygon", true), + new QRecord() + .withTableName(table.getName()) + .withValue("name", "Some Circle") + .withValue("type", "circle") + .withValue("noOfSides", null) + .withValue("isPolygon", false) + ); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testCustomizer() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_SHAPE); + QSession session = new QSession(); + + /////////////////////////////////// + // add a customizer to the table // + /////////////////////////////////// + table.withCustomizer(Customizers.POST_QUERY_RECORD, new QCodeReference(ShapeTestCustomizer.class, QCodeUsage.CUSTOMIZER)); + + ////////////////// + // do an insert // + ////////////////// + InsertInput insertInput = new InsertInput(qInstance); + insertInput.setSession(session); + insertInput.setTableName(table.getName()); + insertInput.setRecords(getTestRecords(table)); + new InsertAction().execute(insertInput); + + /////////////////////////////////////////////////////// + // do a query - assert that the customizer did stuff // + /////////////////////////////////////////////////////// + ShapeTestCustomizer.executionCount = 0; + QueryInput queryInput = new QueryInput(qInstance); + queryInput.setSession(session); + queryInput.setTableName(table.getName()); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(3, queryOutput.getRecords().size()); + assertEquals(3, ShapeTestCustomizer.executionCount); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(1) && r.getValueInteger("tenTimesId").equals(10))); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(2) && r.getValueInteger("tenTimesId").equals(20))); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(3) && r.getValueInteger("tenTimesId").equals(30))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class ShapeTestCustomizer implements Function + { + static int executionCount = 0; + + + + @Override + public QRecord apply(QRecord record) + { + executionCount++; + record.setValue("tenTimesId", record.getValueInteger("id") * 10); + return (record); + } + } } \ No newline at end of file diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/FilesystemBackendModuleInterface.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/FilesystemBackendModuleInterface.java index 23c38c46..816caa06 100644 --- a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/FilesystemBackendModuleInterface.java +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/FilesystemBackendModuleInterface.java @@ -31,11 +31,11 @@ import com.kingsrook.qqq.backend.module.filesystem.base.actions.AbstractBaseFile *******************************************************************************/ public interface FilesystemBackendModuleInterface { - String CUSTOMIZER_FILE_POST_FILE_READ = "postFileRead"; /******************************************************************************* ** For filesystem backends, get the module-specific action base-class, that helps ** with functions like listing and deleting files. *******************************************************************************/ AbstractBaseFilesystemAction getActionBase(); + } diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/AbstractBaseFilesystemAction.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/AbstractBaseFilesystemAction.java index 02b26622..5f846dea 100644 --- a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/AbstractBaseFilesystemAction.java +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/AbstractBaseFilesystemAction.java @@ -40,7 +40,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableBackendDetails; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.StringUtils; -import com.kingsrook.qqq.backend.module.filesystem.base.FilesystemBackendModuleInterface; import com.kingsrook.qqq.backend.module.filesystem.base.FilesystemRecordBackendDetailFields; import com.kingsrook.qqq.backend.module.filesystem.base.model.metadata.AbstractFilesystemBackendMetaData; import com.kingsrook.qqq.backend.module.filesystem.base.model.metadata.AbstractFilesystemTableBackendDetails; @@ -203,7 +202,15 @@ public abstract class AbstractBaseFilesystemAction if(queryInput.getRecordPipe() != null) { - new CsvToQRecordAdapter().buildRecordsFromCsv(queryInput.getRecordPipe(), fileContents, table, null, (record -> addBackendDetailsToRecord(record, file))); + new CsvToQRecordAdapter().buildRecordsFromCsv(queryInput.getRecordPipe(), fileContents, table, null, (record -> + { + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // since the CSV adapter is the one responsible for putting records into the pipe (rather than the queryOutput), // + // we must do some of QueryOutput's normal job here - and run the runPostQueryRecordCustomizer // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + addBackendDetailsToRecord(record, file); + queryOutput.runPostQueryRecordCustomizer(record); + })); } else { @@ -281,7 +288,7 @@ public abstract class AbstractBaseFilesystemAction *******************************************************************************/ private String customizeFileContentsAfterReading(QTableMetaData table, String fileContents) throws QException { - Optional optionalCustomizer = table.getCustomizer(FilesystemBackendModuleInterface.CUSTOMIZER_FILE_POST_FILE_READ); + Optional optionalCustomizer = table.getCustomizer(FilesystemCustomizers.POST_READ_FILE); if(optionalCustomizer.isEmpty()) { return (fileContents); diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/FilesystemCustomizers.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/FilesystemCustomizers.java new file mode 100644 index 00000000..8e2416e0 --- /dev/null +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/FilesystemCustomizers.java @@ -0,0 +1,35 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.filesystem.base.actions; + + +import com.kingsrook.qqq.backend.core.actions.customizers.Customizers; + + +/******************************************************************************* + ** Standard place where the names of QQQ Customization points for filesystem-based + ** backends are defined. + *******************************************************************************/ +public interface FilesystemCustomizers extends Customizers +{ + String POST_READ_FILE = "postReadFile"; +} diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/FilesystemBackendModule.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/FilesystemBackendModule.java index 2bc14244..493a08ac 100644 --- a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/FilesystemBackendModule.java +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/FilesystemBackendModule.java @@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.module.filesystem.local; import java.io.File; +import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface; import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface; import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface; import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface; @@ -33,6 +34,7 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; import com.kingsrook.qqq.backend.module.filesystem.base.FilesystemBackendModuleInterface; import com.kingsrook.qqq.backend.module.filesystem.base.actions.AbstractBaseFilesystemAction; import com.kingsrook.qqq.backend.module.filesystem.local.actions.AbstractFilesystemAction; +import com.kingsrook.qqq.backend.module.filesystem.local.actions.FilesystemCountAction; import com.kingsrook.qqq.backend.module.filesystem.local.actions.FilesystemDeleteAction; import com.kingsrook.qqq.backend.module.filesystem.local.actions.FilesystemInsertAction; import com.kingsrook.qqq.backend.module.filesystem.local.actions.FilesystemQueryAction; @@ -107,6 +109,16 @@ public class FilesystemBackendModule implements QBackendModuleInterface, Filesys } + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public CountInterface getCountInterface() + { + return new FilesystemCountAction(); + } + + /******************************************************************************* ** diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemCountAction.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemCountAction.java new file mode 100644 index 00000000..586eb9f8 --- /dev/null +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemCountAction.java @@ -0,0 +1,55 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.filesystem.local.actions; + + +import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class FilesystemCountAction extends AbstractFilesystemAction implements CountInterface +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public CountOutput execute(CountInput countInput) throws QException + { + QueryInput queryInput = new QueryInput(countInput.getInstance()); + queryInput.setSession(countInput.getSession()); + queryInput.setTableName(countInput.getTableName()); + queryInput.setFilter(countInput.getFilter()); + QueryOutput queryOutput = executeQuery(queryInput); + + CountOutput countOutput = new CountOutput(); + countOutput.setCount(queryOutput.getRecords().size()); + return (countOutput); + } + +} diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemQueryActionTest.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemQueryActionTest.java index 90771e43..be40e86a 100644 --- a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemQueryActionTest.java +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemQueryActionTest.java @@ -26,14 +26,13 @@ import java.util.function.Function; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; -import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; -import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; -import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.module.filesystem.TestUtils; -import com.kingsrook.qqq.backend.module.filesystem.base.FilesystemBackendModuleInterface; import com.kingsrook.qqq.backend.module.filesystem.base.FilesystemRecordBackendDetailFields; +import com.kingsrook.qqq.backend.module.filesystem.base.actions.FilesystemCustomizers; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -72,10 +71,7 @@ public class FilesystemQueryActionTest extends FilesystemActionTest QInstance instance = TestUtils.defineInstance(); QTableMetaData table = instance.getTable(TestUtils.TABLE_NAME_PERSON_LOCAL_FS_JSON); - table.withCustomizer(FilesystemBackendModuleInterface.CUSTOMIZER_FILE_POST_FILE_READ, new QCodeReference() - .withName(ValueUpshifter.class.getName()) - .withCodeType(QCodeType.JAVA) - .withCodeUsage(QCodeUsage.CUSTOMIZER)); + table.withCustomizer(FilesystemCustomizers.POST_READ_FILE, new QCodeReference(ValueUpshifter.class, QCodeUsage.CUSTOMIZER)); queryInput.setInstance(instance); queryInput.setTableName(TestUtils.defineLocalFilesystemJSONPersonTable().getName()); From 83c1bd802870f66f385aba5bc7ae617582799d16 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 12 Aug 2022 18:55:58 -0500 Subject: [PATCH 04/19] Adding check for 95% of classes being covered by junits (and supporting test coverage); Update filesystem s3 tests to reuse localstack docker container --- pom.xml | 6 ++ .../actions/AbstractBaseFilesystemAction.java | 20 +++++ .../local/actions/FilesystemCountAction.java | 12 +-- .../filesystem/s3/actions/S3CountAction.java | 45 ++++++++++ .../FilesystemModuleJunitExtension.java | 30 +++++++ .../local/actions/FilesystemActionTest.java | 19 ++-- .../actions/FilesystemCountActionTest.java | 89 +++++++++++++++++++ .../actions/FilesystemDeleteActionTest.java | 47 ++++++++++ .../actions/FilesystemInsertActionTest.java | 47 ++++++++++ .../actions/FilesystemUpdateActionTest.java | 47 ++++++++++ .../module/filesystem/s3/BaseS3Test.java | 4 +- .../s3/actions/S3CountActionTest.java | 66 ++++++++++++++ .../s3/actions/S3DeleteActionTest.java | 48 ++++++++++ .../s3/actions/S3InsertActionTest.java | 48 ++++++++++ .../s3/actions/S3UpdateActionTest.java | 48 ++++++++++ .../module/rdbms/jdbc/QueryManager.java | 7 +- .../module/rdbms/jdbc/QueryManagerTest.java | 21 +++++ .../com/kingsrook/sampleapp/SampleCli.java | 18 +++- .../kingsrook/sampleapp/SampleCliTest.java | 46 ++++++++++ .../sampleapp/SampleJavalinServerTest.java | 4 +- 20 files changed, 639 insertions(+), 33 deletions(-) create mode 100644 qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3CountAction.java create mode 100644 qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/FilesystemModuleJunitExtension.java create mode 100644 qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemCountActionTest.java create mode 100644 qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemDeleteActionTest.java create mode 100644 qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemInsertActionTest.java create mode 100644 qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemUpdateActionTest.java create mode 100644 qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3CountActionTest.java create mode 100644 qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3DeleteActionTest.java create mode 100644 qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3InsertActionTest.java create mode 100644 qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3UpdateActionTest.java create mode 100644 qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleCliTest.java diff --git a/pom.xml b/pom.xml index 098a13fe..f1436792 100644 --- a/pom.xml +++ b/pom.xml @@ -48,6 +48,7 @@ true true 0.80 + 0.95 @@ -211,6 +212,11 @@ COVEREDRATIO ${coverage.instructionCoveredRatioMinimum} + + CLASS + COVEREDRATIO + ${coverage.classCoveredRatioMinimum} + diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/AbstractBaseFilesystemAction.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/AbstractBaseFilesystemAction.java index 5f846dea..61730733 100644 --- a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/AbstractBaseFilesystemAction.java +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/AbstractBaseFilesystemAction.java @@ -31,6 +31,8 @@ import java.util.function.Function; import com.kingsrook.qqq.backend.core.adapters.CsvToQRecordAdapter; import com.kingsrook.qqq.backend.core.adapters.JsonToQRecordAdapter; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; 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; @@ -250,6 +252,24 @@ public abstract class AbstractBaseFilesystemAction + /******************************************************************************* + ** + *******************************************************************************/ + public CountOutput executeCount(CountInput countInput) throws QException + { + QueryInput queryInput = new QueryInput(countInput.getInstance()); + queryInput.setSession(countInput.getSession()); + queryInput.setTableName(countInput.getTableName()); + queryInput.setFilter(countInput.getFilter()); + QueryOutput queryOutput = executeQuery(queryInput); + + CountOutput countOutput = new CountOutput(); + countOutput.setCount(queryOutput.getRecords().size()); + return (countOutput); + } + + + /******************************************************************************* ** Add backend details to records about the file that they are in. *******************************************************************************/ diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemCountAction.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemCountAction.java index 586eb9f8..d93c7da8 100644 --- a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemCountAction.java +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemCountAction.java @@ -26,8 +26,6 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; /******************************************************************************* @@ -41,15 +39,7 @@ public class FilesystemCountAction extends AbstractFilesystemAction implements C *******************************************************************************/ public CountOutput execute(CountInput countInput) throws QException { - QueryInput queryInput = new QueryInput(countInput.getInstance()); - queryInput.setSession(countInput.getSession()); - queryInput.setTableName(countInput.getTableName()); - queryInput.setFilter(countInput.getFilter()); - QueryOutput queryOutput = executeQuery(queryInput); - - CountOutput countOutput = new CountOutput(); - countOutput.setCount(queryOutput.getRecords().size()); - return (countOutput); + return (executeCount(countInput)); } } diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3CountAction.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3CountAction.java new file mode 100644 index 00000000..71854aa9 --- /dev/null +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3CountAction.java @@ -0,0 +1,45 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.filesystem.s3.actions; + + +import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class S3CountAction extends AbstractS3Action implements CountInterface +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public CountOutput execute(CountInput countInput) throws QException + { + return (executeCount(countInput)); + } + +} diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/FilesystemModuleJunitExtension.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/FilesystemModuleJunitExtension.java new file mode 100644 index 00000000..e9bea180 --- /dev/null +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/FilesystemModuleJunitExtension.java @@ -0,0 +1,30 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.filesystem; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class FilesystemModuleJunitExtension // implements Extension, BeforeAllCallback, AfterAllCallback +{ +} diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemActionTest.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemActionTest.java index 94cbfbf7..8cf24310 100644 --- a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemActionTest.java +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemActionTest.java @@ -73,11 +73,14 @@ public class FilesystemActionTest TestUtils.increaseTestInstanceCounter(); FilesystemBackendMetaData filesystemBackendMetaData = TestUtils.defineLocalFilesystemBackend(); - File baseDirectory = new File(filesystemBackendMetaData.getBasePath()); - boolean mkdirsResult = baseDirectory.mkdirs(); - if(!mkdirsResult) + File baseDirectory = new File(filesystemBackendMetaData.getBasePath()); + if(!baseDirectory.exists()) { - fail("Failed to make directories at [" + baseDirectory + "] for filesystem backend module"); + boolean mkdirsResult = baseDirectory.mkdirs(); + if(!mkdirsResult) + { + fail("Failed to make directories at [" + baseDirectory + "] for filesystem backend module"); + } } writePersonJSONFiles(baseDirectory); @@ -92,9 +95,9 @@ public class FilesystemActionTest private void writePersonJSONFiles(File baseDirectory) throws IOException { String fullPath = baseDirectory.getAbsolutePath(); - if (TestUtils.defineLocalFilesystemJSONPersonTable().getBackendDetails() instanceof FilesystemTableBackendDetails details) + if(TestUtils.defineLocalFilesystemJSONPersonTable().getBackendDetails() instanceof FilesystemTableBackendDetails details) { - if (StringUtils.hasContent(details.getBasePath())) + if(StringUtils.hasContent(details.getBasePath())) { fullPath += File.separatorChar + details.getBasePath(); } @@ -125,9 +128,9 @@ public class FilesystemActionTest private void writePersonCSVFiles(File baseDirectory) throws IOException { String fullPath = baseDirectory.getAbsolutePath(); - if (TestUtils.defineLocalFilesystemCSVPersonTable().getBackendDetails() instanceof FilesystemTableBackendDetails details) + if(TestUtils.defineLocalFilesystemCSVPersonTable().getBackendDetails() instanceof FilesystemTableBackendDetails details) { - if (StringUtils.hasContent(details.getBasePath())) + if(StringUtils.hasContent(details.getBasePath())) { fullPath += File.separatorChar + details.getBasePath(); } diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemCountActionTest.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemCountActionTest.java new file mode 100644 index 00000000..2c488900 --- /dev/null +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemCountActionTest.java @@ -0,0 +1,89 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.filesystem.local.actions; + + +import java.util.function.Function; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.module.filesystem.TestUtils; +import com.kingsrook.qqq.backend.module.filesystem.base.actions.FilesystemCustomizers; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class FilesystemCountActionTest extends FilesystemActionTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testCount1() throws QException + { + CountInput countInput = new CountInput(); + countInput.setInstance(TestUtils.defineInstance()); + countInput.setTableName(TestUtils.defineLocalFilesystemJSONPersonTable().getName()); + CountOutput countOutput = new FilesystemCountAction().execute(countInput); + Assertions.assertEquals(3, countOutput.getCount(), "Unfiltered count should find all rows"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testCountWithFileCustomizer() throws QException + { + CountInput countInput = new CountInput(); + QInstance instance = TestUtils.defineInstance(); + + QTableMetaData table = instance.getTable(TestUtils.TABLE_NAME_PERSON_LOCAL_FS_JSON); + table.withCustomizer(FilesystemCustomizers.POST_READ_FILE, new QCodeReference(ValueUpshifter.class, QCodeUsage.CUSTOMIZER)); + + countInput.setInstance(instance); + countInput.setTableName(TestUtils.defineLocalFilesystemJSONPersonTable().getName()); + CountOutput countOutput = new FilesystemCountAction().execute(countInput); + Assertions.assertEquals(3, countOutput.getCount(), "Unfiltered count should find all rows"); + } + + + + public static class ValueUpshifter implements Function + { + @Override + public String apply(String s) + { + return (s.replaceAll("kingsrook.com", "KINGSROOK.COM")); + } + } + +} \ No newline at end of file diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemDeleteActionTest.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemDeleteActionTest.java new file mode 100644 index 00000000..e43a667e --- /dev/null +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemDeleteActionTest.java @@ -0,0 +1,47 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.filesystem.local.actions; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; +import org.apache.commons.lang.NotImplementedException; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class FilesystemDeleteActionTest extends FilesystemActionTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void test() throws QException + { + assertThrows(NotImplementedException.class, () -> new FilesystemDeleteAction().execute(new DeleteInput())); + } + +} \ No newline at end of file diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemInsertActionTest.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemInsertActionTest.java new file mode 100644 index 00000000..92d38604 --- /dev/null +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemInsertActionTest.java @@ -0,0 +1,47 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.filesystem.local.actions; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import org.apache.commons.lang.NotImplementedException; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class FilesystemInsertActionTest extends FilesystemActionTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void test() throws QException + { + assertThrows(NotImplementedException.class, () -> new FilesystemInsertAction().execute(new InsertInput())); + } + +} \ No newline at end of file diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemUpdateActionTest.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemUpdateActionTest.java new file mode 100644 index 00000000..25d39070 --- /dev/null +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemUpdateActionTest.java @@ -0,0 +1,47 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.filesystem.local.actions; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; +import org.apache.commons.lang.NotImplementedException; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class FilesystemUpdateActionTest extends FilesystemActionTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void test() throws QException + { + assertThrows(NotImplementedException.class, () -> new FilesystemUpdateAction().execute(new UpdateInput())); + } + +} \ No newline at end of file diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/BaseS3Test.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/BaseS3Test.java index 4a9c979b..413f7ae8 100644 --- a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/BaseS3Test.java +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/BaseS3Test.java @@ -31,8 +31,6 @@ import com.amazonaws.services.s3.model.S3ObjectSummary; import com.kingsrook.qqq.backend.module.filesystem.s3.utils.S3Utils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.condition.DisabledOnOs; -import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.extension.ExtendWith; @@ -40,7 +38,7 @@ import org.junit.jupiter.api.extension.ExtendWith; ** Base class for tests that want to be able to work with localstack s3. *******************************************************************************/ @ExtendWith(LocalstackDockerExtension.class) -@LocalstackDockerProperties(services = { ServiceName.S3 }, portEdge = "2960", portElasticSearch = "2961") +@LocalstackDockerProperties(useSingleDockerContainer = true, services = { ServiceName.S3 }, portEdge = "2960", portElasticSearch = "2961") public class BaseS3Test { public static final String BUCKET_NAME = "localstack-test-bucket"; diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3CountActionTest.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3CountActionTest.java new file mode 100644 index 00000000..b2538f43 --- /dev/null +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3CountActionTest.java @@ -0,0 +1,66 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.filesystem.s3.actions; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; +import com.kingsrook.qqq.backend.module.filesystem.TestUtils; +import com.kingsrook.qqq.backend.module.filesystem.s3.BaseS3Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class S3CountActionTest extends BaseS3Test +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testCount1() throws QException + { + CountInput countInput = initCountRequest(); + S3CountAction s3CountAction = new S3CountAction(); + s3CountAction.setS3Utils(getS3Utils()); + CountOutput countOutput = s3CountAction.execute(countInput); + Assertions.assertEquals(5, countOutput.getCount(), "Expected # of rows from unfiltered count"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private CountInput initCountRequest() throws QException + { + CountInput countInput = new CountInput(); + countInput.setInstance(TestUtils.defineInstance()); + countInput.setTableName(TestUtils.defineS3CSVPersonTable().getName()); + return countInput; + } + +} \ No newline at end of file diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3DeleteActionTest.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3DeleteActionTest.java new file mode 100644 index 00000000..6b1ba2fa --- /dev/null +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3DeleteActionTest.java @@ -0,0 +1,48 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.filesystem.s3.actions; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; +import com.kingsrook.qqq.backend.module.filesystem.s3.BaseS3Test; +import org.apache.commons.lang.NotImplementedException; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class S3DeleteActionTest extends BaseS3Test +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void test() throws QException + { + assertThrows(NotImplementedException.class, () -> new S3DeleteAction().execute(new DeleteInput())); + } + +} \ No newline at end of file diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3InsertActionTest.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3InsertActionTest.java new file mode 100644 index 00000000..94e774f4 --- /dev/null +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3InsertActionTest.java @@ -0,0 +1,48 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.filesystem.s3.actions; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.module.filesystem.s3.BaseS3Test; +import org.apache.commons.lang.NotImplementedException; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class S3InsertActionTest extends BaseS3Test +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void test() throws QException + { + assertThrows(NotImplementedException.class, () -> new S3InsertAction().execute(new InsertInput())); + } + +} \ No newline at end of file diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3UpdateActionTest.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3UpdateActionTest.java new file mode 100644 index 00000000..c2e0bcae --- /dev/null +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3UpdateActionTest.java @@ -0,0 +1,48 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.filesystem.s3.actions; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; +import com.kingsrook.qqq.backend.module.filesystem.s3.BaseS3Test; +import org.apache.commons.lang.NotImplementedException; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class S3UpdateActionTest extends BaseS3Test +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void test() throws QException + { + assertThrows(NotImplementedException.class, () -> new S3UpdateAction().execute(new UpdateInput())); + } + +} \ No newline at end of file diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java index b5572324..0a90182f 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java @@ -29,6 +29,7 @@ import java.sql.Connection; import java.sql.Date; import java.sql.PreparedStatement; import java.sql.ResultSet; +import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Statement; import java.sql.Timestamp; @@ -277,8 +278,6 @@ public class QueryManager *******************************************************************************/ public static SimpleEntity executeStatementForSimpleEntity(Connection connection, String sql, Object... params) throws SQLException { - throw (new NotImplementedException()); - /* PreparedStatement statement = prepareStatementAndBindParams(connection, sql, params); statement.execute(); ResultSet resultSet = statement.getResultSet(); @@ -290,7 +289,6 @@ public class QueryManager { return (null); } - */ } @@ -355,8 +353,6 @@ public class QueryManager *******************************************************************************/ public static SimpleEntity buildSimpleEntity(ResultSet resultSet) throws SQLException { - throw (new NotImplementedException()); - /* SimpleEntity row = new SimpleEntity(); ResultSetMetaData metaData = resultSet.getMetaData(); @@ -365,7 +361,6 @@ public class QueryManager row.put(metaData.getColumnName(i), getObject(resultSet, i)); } return row; - */ } diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManagerTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManagerTest.java index ea5e99cc..b7be5476 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManagerTest.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManagerTest.java @@ -360,4 +360,25 @@ class QueryManagerTest assertEquals(null, QueryManager.executeStatementForSingleValue(connection, Integer.class, "SELECT int_col FROM test_table WHERE int_col IS NULL")); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQueryForSimpleEntity() throws SQLException + { + Connection connection = getConnection(); + QueryManager.executeUpdate(connection, """ + INSERT INTO test_table + ( int_col, datetime_col, char_col, date_col, time_col ) + VALUES + ( 47, '2022-08-10 19:22:08', 'Q', '2022-08-10', '19:22:08') + """); + SimpleEntity simpleEntity = QueryManager.executeStatementForSimpleEntity(connection, "SELECT * FROM test_table"); + assertNotNull(simpleEntity); + assertEquals(47, simpleEntity.get("INT_COL")); + assertEquals("Q", simpleEntity.get("CHAR_COL")); + } + } \ No newline at end of file diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleCli.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleCli.java index b8f0d4ab..d524c6cd 100644 --- a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleCli.java +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleCli.java @@ -22,6 +22,7 @@ package com.kingsrook.sampleapp; +import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.frontend.picocli.QPicoCliImplementation; @@ -48,9 +49,7 @@ public class SampleCli { try { - QInstance qInstance = SampleMetaDataProvider.defineInstance(); - QPicoCliImplementation qPicoCliImplementation = new QPicoCliImplementation(qInstance); - int exitCode = qPicoCliImplementation.runCli("my-sample-cli", args); + int exitCode = runForExitCode(args); System.exit(exitCode); } catch(Exception e) @@ -60,4 +59,17 @@ public class SampleCli } } + + + /******************************************************************************* + ** + *******************************************************************************/ + int runForExitCode(String[] args) throws QException + { + QInstance qInstance = SampleMetaDataProvider.defineInstance(); + QPicoCliImplementation qPicoCliImplementation = new QPicoCliImplementation(qInstance); + int exitCode = qPicoCliImplementation.runCli("my-sample-cli", args); + return exitCode; + } + } diff --git a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleCliTest.java b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleCliTest.java new file mode 100644 index 00000000..ab4546cc --- /dev/null +++ b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleCliTest.java @@ -0,0 +1,46 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.sampleapp; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/******************************************************************************* + ** Unit test for SampleCli + *******************************************************************************/ +class SampleCliTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException + { + int exitCode = new SampleCli().runForExitCode(new String[] { "--meta-data" }); + assertEquals(0, exitCode); + } + +} \ No newline at end of file diff --git a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleJavalinServerTest.java b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleJavalinServerTest.java index 06bcd1e7..0a861b18 100644 --- a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleJavalinServerTest.java +++ b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleJavalinServerTest.java @@ -2,14 +2,14 @@ package com.kingsrook.sampleapp; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; /******************************************************************************* - ** Unit test for com.kingsrook.sampleapp.SampleJavalinServer + ** Unit test for SampleJavalinServer *******************************************************************************/ class SampleJavalinServerTest { + /******************************************************************************* ** *******************************************************************************/ From a840bd1d508a9b3ddc589acdeeade8ab9e40d892 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 15 Aug 2022 10:55:23 -0500 Subject: [PATCH 05/19] Adding status updates to ETL Load; Add YYYYmmDD as localDate format --- .../etl/basic/BasicETLLoadAsUpdateFunction.java | 2 ++ .../implementations/etl/basic/BasicETLLoadFunction.java | 2 ++ .../java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java | 3 ++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/basic/BasicETLLoadAsUpdateFunction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/basic/BasicETLLoadAsUpdateFunction.java index e6a8290f..0219acf6 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/basic/BasicETLLoadAsUpdateFunction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/basic/BasicETLLoadAsUpdateFunction.java @@ -82,6 +82,8 @@ public class BasicETLLoadAsUpdateFunction implements BackendStep for(List page : CollectionUtils.getPages(inputRecords, pageSize)) { LOG.info("Updating a page of [" + page.size() + "] records. Progress: " + recordsUpdated + " loaded out of " + inputRecords.size() + " total"); + runBackendStepInput.getAsyncJobCallback().updateStatus("Updating records", recordsUpdated, inputRecords.size()); + UpdateInput updateInput = new UpdateInput(runBackendStepInput.getInstance()); updateInput.setSession(runBackendStepInput.getSession()); updateInput.setTableName(table); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/basic/BasicETLLoadFunction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/basic/BasicETLLoadFunction.java index f8e782f2..d0a6c77b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/basic/BasicETLLoadFunction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/basic/BasicETLLoadFunction.java @@ -86,6 +86,8 @@ public class BasicETLLoadFunction implements BackendStep for(List page : CollectionUtils.getPages(inputRecords, pageSize)) { LOG.info("Inserting a page of [" + page.size() + "] records. Progress: " + recordsInserted + " loaded out of " + inputRecords.size() + " total"); + runBackendStepInput.getAsyncJobCallback().updateStatus("Inserting records", recordsInserted, inputRecords.size()); + InsertInput insertInput = new InsertInput(runBackendStepInput.getInstance()); insertInput.setSession(runBackendStepInput.getSession()); insertInput.setTableName(table); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java index 08ecf054..0dbe02b3 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java @@ -44,6 +44,7 @@ public class ValueUtils { private static final DateTimeFormatter dateTimeFormatter_yyyyMMddWithDashes = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private static final DateTimeFormatter dateTimeFormatter_MdyyyyWithSlashes = DateTimeFormatter.ofPattern("M/d/yyyy"); + private static final DateTimeFormatter dateTimeFormatter_yyyyMMdd = DateTimeFormatter.ofPattern("yyyyMMdd"); @@ -262,7 +263,7 @@ public class ValueUtils private static LocalDate tryLocalDateParsers(String s) { DateTimeParseException lastException = null; - for(DateTimeFormatter dateTimeFormatter : List.of(dateTimeFormatter_yyyyMMddWithDashes, dateTimeFormatter_MdyyyyWithSlashes)) + for(DateTimeFormatter dateTimeFormatter : List.of(dateTimeFormatter_yyyyMMddWithDashes, dateTimeFormatter_MdyyyyWithSlashes, dateTimeFormatter_yyyyMMdd )) { try { From 5735bdf9d7cbf4abde8bb2445c190497ea72ffc5 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 15 Aug 2022 10:55:39 -0500 Subject: [PATCH 06/19] Update to 0.3.1-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f368b909..ec6dbc2f 100644 --- a/pom.xml +++ b/pom.xml @@ -38,7 +38,7 @@ - 0.3.0 + 0.3.1-SNAPSHOT UTF-8 UTF-8 From a0cfd5a97eb42959a93621a91498909c5e621664 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 15 Aug 2022 10:58:10 -0500 Subject: [PATCH 07/19] Checkstyle fix --- .../java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java index 0dbe02b3..9850719b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java @@ -44,7 +44,7 @@ public class ValueUtils { private static final DateTimeFormatter dateTimeFormatter_yyyyMMddWithDashes = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private static final DateTimeFormatter dateTimeFormatter_MdyyyyWithSlashes = DateTimeFormatter.ofPattern("M/d/yyyy"); - private static final DateTimeFormatter dateTimeFormatter_yyyyMMdd = DateTimeFormatter.ofPattern("yyyyMMdd"); + private static final DateTimeFormatter dateTimeFormatter_yyyyMMdd = DateTimeFormatter.ofPattern("yyyyMMdd"); @@ -263,7 +263,7 @@ public class ValueUtils private static LocalDate tryLocalDateParsers(String s) { DateTimeParseException lastException = null; - for(DateTimeFormatter dateTimeFormatter : List.of(dateTimeFormatter_yyyyMMddWithDashes, dateTimeFormatter_MdyyyyWithSlashes, dateTimeFormatter_yyyyMMdd )) + for(DateTimeFormatter dateTimeFormatter : List.of(dateTimeFormatter_yyyyMMddWithDashes, dateTimeFormatter_MdyyyyWithSlashes, dateTimeFormatter_yyyyMMdd)) { try { From 9bf898af7a74806b9bf63b7c7ff7dd2ccc7a7c54 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 17 Aug 2022 11:34:00 -0500 Subject: [PATCH 08/19] Update to handle BOM char and index-out-of-bounds condition --- .../core/adapters/CsvToQRecordAdapter.java | 11 +++- .../adapters/CsvToQRecordAdapterTest.java | 57 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapter.java index 3d5493a3..98f85478 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapter.java @@ -95,6 +95,15 @@ public class CsvToQRecordAdapter throw (new IllegalArgumentException("Empty csv value was provided.")); } + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // once, from a DOS csv file (that had come from Excel), we had a "" character (FEFF, Byte-order marker) at the start of a // + // CSV, which caused our first header to not match... So, let us strip away any FEFF or FFFE's at the start of CSV strings. // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(csv.length() > 1 && (csv.charAt(0) == 0xfeff || csv.charAt(0) == 0xfffe)) + { + csv = csv.substring(1); + } + try { /////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -118,7 +127,7 @@ public class CsvToQRecordAdapter // put values from the CSV record into a map of header -> value // ////////////////////////////////////////////////////////////////// Map csvValues = new HashMap<>(); - for(int i = 0; i < headers.size(); i++) + for(int i = 0; i < headers.size() && i < csvRecord.size(); i++) { csvValues.put(headers.get(i), csvRecord.get(i)); } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java index 2593b3e2..a81e5b53 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java @@ -31,6 +31,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -281,4 +282,60 @@ class CsvToQRecordAdapterTest // todo - this is what the method header comment means when it says we don't handle all cases well... // Assertions.assertEquals(List.of("A", "B", "C", "C 2", "C 3"), csvToQRecordAdapter.makeHeadersUnique(List.of("A", "B", "C 2", "C", "C 3"))); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testByteOrderMarker() + { + CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter(); + List records = csvToQRecordAdapter.buildRecordsFromCsv(""" + id,firstName + 1,John""", TestUtils.defineTablePerson(), null); + + assertEquals(1, records.get(0).getValueInteger("id")); + assertEquals("John", records.get(0).getValueString("firstName")); + } + + + + /******************************************************************************* + ** Fix an IndexOutOfBounds that we used to throw. + *******************************************************************************/ + @Test + void testTooFewBodyColumns() + { + CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter(); + List records = csvToQRecordAdapter.buildRecordsFromCsv(""" + id,firstName,lastName + 1,John""", TestUtils.defineTablePerson(), null); + + assertEquals(1, records.get(0).getValueInteger("id")); + assertEquals("John", records.get(0).getValueString("firstName")); + assertNull(records.get(0).getValueString("lastName")); + } + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testTooFewColumnsIndexMapping() + { + int index = 1; + QIndexBasedFieldMapping mapping = new QIndexBasedFieldMapping() + .withMapping("id", index++) + .withMapping("firstName", index++) + .withMapping("lastName", index++); + + CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter(); + List records = csvToQRecordAdapter.buildRecordsFromCsv("1,John", TestUtils.defineTablePerson(), mapping); + + assertEquals(1, records.get(0).getValueInteger("id")); + assertEquals("John", records.get(0).getValueString("firstName")); + assertNull(records.get(0).getValueString("lastName")); + } + } From 5e703ad060bd91beaa9bc101b1a5a3cb9cdac507 Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Thu, 18 Aug 2022 17:30:04 -0500 Subject: [PATCH 09/19] added liquibase to sample project --- qqq-sample-project/pom.xml | 18 ++++ .../sampleapp/SampleMetaDataProvider.java | 1 + .../main/resources/liquibase/changelog.xml | 13 +++ .../liquibase/changesets/initial.xml | 100 ++++++++++++++++++ .../resources/liquibase/liquibase.properties | 6 ++ 5 files changed, 138 insertions(+) create mode 100644 qqq-sample-project/src/main/resources/liquibase/changelog.xml create mode 100644 qqq-sample-project/src/main/resources/liquibase/changesets/initial.xml create mode 100644 qqq-sample-project/src/main/resources/liquibase/liquibase.properties diff --git a/qqq-sample-project/pom.xml b/qqq-sample-project/pom.xml index 917a1c4f..23f84047 100644 --- a/qqq-sample-project/pom.xml +++ b/qqq-sample-project/pom.xml @@ -98,6 +98,11 @@ assertj-core test + + org.liquibase + liquibase-core + 4.10.0 + @@ -116,6 +121,19 @@ + + + org.liquibase + liquibase-maven-plugin + 4.10.0 + + /src/main/resources/liquibase/liquibase.properties + ${env.LB_DB_URL} + ${env.LB_DB_USERNAME} + ${env.LB_DB_PASSWORD} + ${env.LB_CONTEXTS} + + diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java index 2c9bb956..9be5c4d6 100644 --- a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java @@ -261,6 +261,7 @@ public class SampleMetaDataProvider .withField(new QFieldMetaData("lastName", QFieldType.STRING).withBackendName("last_name").withIsRequired(true)) .withField(new QFieldMetaData("birthDate", QFieldType.DATE).withBackendName("birth_date")) .withField(new QFieldMetaData("email", QFieldType.STRING)) + .withField(new QFieldMetaData("isEmployed", QFieldType.BOOLEAN).withBackendName("is_employed")) .withField(new QFieldMetaData("annualSalary", QFieldType.DECIMAL).withBackendName("annual_salary").withDisplayFormat(DisplayFormat.CURRENCY)) .withField(new QFieldMetaData("daysWorked", QFieldType.INTEGER).withBackendName("days_worked").withDisplayFormat(DisplayFormat.COMMAS)) diff --git a/qqq-sample-project/src/main/resources/liquibase/changelog.xml b/qqq-sample-project/src/main/resources/liquibase/changelog.xml new file mode 100644 index 00000000..9ceff1dd --- /dev/null +++ b/qqq-sample-project/src/main/resources/liquibase/changelog.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/qqq-sample-project/src/main/resources/liquibase/changesets/initial.xml b/qqq-sample-project/src/main/resources/liquibase/changesets/initial.xml new file mode 100644 index 00000000..4beba756 --- /dev/null +++ b/qqq-sample-project/src/main/resources/liquibase/changesets/initial.xml @@ -0,0 +1,100 @@ + + + + + + DROP TABLE IF EXISTS person; + CREATE TABLE person + ( + id INT AUTO_INCREMENT primary key , + create_date TIMESTAMP DEFAULT now(), + modify_date TIMESTAMP DEFAULT now(), + + first_name VARCHAR(80) NOT NULL, + last_name VARCHAR(80) NOT NULL, + birth_date DATE, + email VARCHAR(250) NOT NULL, + is_employed BOOLEAN, + annual_salary DECIMAL(12,2), + days_worked INTEGER + ); + INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (1, 'Darin', 'Kelkhoff', '1980-05-31', 'darin.kelkhoff@gmail.com', 1, 25000, 27); + INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (2, 'James', 'Maes', '1980-05-15', 'jmaes@mmltholdings.com', 1, 26000, 124); + INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (3, 'Tim', 'Chamberlain', '1976-05-28', 'tchamberlain@mmltholdings.com', 0, null, 0); + INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (4, 'Tyler', 'Samples', NULL, 'tsamples@mmltholdings.com', 1, 30000, 99); + INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (5, 'Garret', 'Richardson', '1981-01-01', 'grichardson@mmltholdings.com', 1, 1000000, 232); + + + DROP TABLE IF EXISTS carrier; + CREATE TABLE carrier + ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(80) NOT NULL, + company_code VARCHAR(80) NOT NULL, + service_level VARCHAR(80) NOT NULL + ); + INSERT INTO carrier (id, name, company_code, service_level) VALUES (1, 'UPS Ground', 'UPS', 'G'); + INSERT INTO carrier (id, name, company_code, service_level) VALUES (2, 'UPS 2Day', 'UPS', '2'); + INSERT INTO carrier (id, name, company_code, service_level) VALUES (3, 'UPS International', 'UPS', 'I'); + INSERT INTO carrier (id, name, company_code, service_level) VALUES (4, 'Fedex Ground', 'FEDEX', 'G'); + INSERT INTO carrier (id, name, company_code, service_level) VALUES (5, 'Fedex Next Day', 'UPS', '1'); + INSERT INTO carrier (id, name, company_code, service_level) VALUES (6, 'Will Call', 'WILL_CALL', 'W'); + INSERT INTO carrier (id, name, company_code, service_level) VALUES (7, 'USPS Priority', 'USPS', '1'); + INSERT INTO carrier (id, name, company_code, service_level) VALUES (8, 'USPS Super Slow', 'USPS', '4'); + INSERT INTO carrier (id, name, company_code, service_level) VALUES (9, 'USPS Super Fast', 'USPS', '0'); + INSERT INTO carrier (id, name, company_code, service_level) VALUES (10, 'DHL International', 'DHL', 'I'); + INSERT INTO carrier (id, name, company_code, service_level) VALUES (11, 'GSO', 'GSO', 'G'); + + + DROP TABLE IF EXISTS child_table; + CREATE TABLE child_table + ( + id INT AUTO_INCREMENT primary key, + name VARCHAR(80) NOT NULL + ); + INSERT INTO child_table (id, name) VALUES (1, 'Timmy'); + INSERT INTO child_table (id, name) VALUES (2, 'Jimmy'); + INSERT INTO child_table (id, name) VALUES (3, 'Johnny'); + INSERT INTO child_table (id, name) VALUES (4, 'Gracie'); + INSERT INTO child_table (id, name) VALUES (5, 'Suzie'); + + + DROP TABLE IF EXISTS parent_table; + CREATE TABLE parent_table + ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(80) NOT NULL, + child_id INT, + foreign key (child_id) references child_table(id) + ); + INSERT INTO parent_table (id, name, child_id) VALUES (1, 'Tim''s Dad', 1); + INSERT INTO parent_table (id, name, child_id) VALUES (2, 'Tim''s Mom', 1); + INSERT INTO parent_table (id, name, child_id) VALUES (3, 'Childless Man', null); + INSERT INTO parent_table (id, name, child_id) VALUES (4, 'Childless Woman', null); + INSERT INTO parent_table (id, name, child_id) VALUES (5, 'Johny''s Single Dad', 3); + + + DROP TABLE IF EXISTS city; + CREATE TABLE city + ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(80) NOT NULL, + state VARCHAR(2) NOT NULL + ); + INSERT INTO city (id, name, state) VALUES (1, 'Decatur', 'IL'); + INSERT INTO city (id, name, state) VALUES (2, 'Chester', 'IL'); + INSERT INTO city (id, name, state) VALUES (3, 'St. Louis', 'MO'); + INSERT INTO city (id, name, state) VALUES (4, 'Baltimore', 'MD'); + INSERT INTO city (id, name, state) VALUES (5, 'New York', 'NY'); + + + + + diff --git a/qqq-sample-project/src/main/resources/liquibase/liquibase.properties b/qqq-sample-project/src/main/resources/liquibase/liquibase.properties new file mode 100644 index 00000000..160d8648 --- /dev/null +++ b/qqq-sample-project/src/main/resources/liquibase/liquibase.properties @@ -0,0 +1,6 @@ +#liquibase.properties +classpath: /src/main/resources/liquibase/lib/mysql-connector-java-8.0.29.jar +driver: com.mysql.cj.jdbc.Driver +changeLogFile:/src/main/resources/liquibase/changelog.xml +logLevel: INFO +liquibase.hub.mode=off \ No newline at end of file From 45d785f1a5c245ef7b4c942fa0ba5eb80c6d3a73 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 18 Aug 2022 19:15:24 -0500 Subject: [PATCH 10/19] Initial passable version of possible values --- .../processes/RunBackendStepAction.java | 2 +- .../core/actions/tables/QueryAction.java | 17 +- .../values/QCustomPossibleValueProvider.java | 43 +++ .../values/QPossibleValueTranslator.java | 359 ++++++++++++++++++ .../core/actions/values/QValueFormatter.java | 38 +- .../core/adapters/CsvToQRecordAdapter.java | 11 +- .../core/instances/QInstanceEnricher.java | 12 +- .../core/instances/QInstanceValidator.java | 75 +++- .../actions/tables/query/QueryInput.java | 45 +++ .../qqq/backend/core/model/data/QField.java | 5 + .../core/model/metadata/QInstance.java | 14 +- .../model/metadata/code/QCodeReference.java | 5 + .../core/model/metadata/code/QCodeUsage.java | 3 +- .../model/metadata/fields/QFieldMetaData.java | 7 + .../frontend/QFrontendFieldMetaData.java | 34 +- .../possiblevalues/PossibleValueEnum.java | 39 ++ .../possiblevalues/QPossibleValue.java | 78 ++++ .../possiblevalues/QPossibleValueSource.java | 321 +++++++++++++++- .../model/metadata/tables/QTableMetaData.java | 13 + .../Auth0AuthenticationModule.java | 5 + .../memory/MemoryRecordStore.java | 111 +++++- .../core/actions/tables/QueryActionTest.java | 31 +- .../values/QPossibleValueTranslatorTest.java | 241 ++++++++++++ .../actions/values/QValueFormatterTest.java | 71 ++-- .../adapters/CsvToQRecordAdapterTest.java | 57 +++ .../core/instances/QInstanceEnricherTest.java | 42 +- .../instances/QInstanceValidatorTest.java | 121 +++++- .../memory/MemoryBackendModuleTest.java | 6 +- .../qqq/backend/core/utils/TestUtils.java | 68 +++- .../src/test/resources/personQInstance.json | 4 +- .../personQInstanceIncludingBackend.json | 4 +- .../rdbms/actions/RDBMSQueryAction.java | 4 + .../rdbms/actions/RDBMSQueryActionTest.java | 4 +- .../javalin/QJavalinImplementation.java | 74 +++- .../picocli/QPicoCliImplementation.java | 9 + .../sampleapp/SampleMetaDataProvider.java | 4 +- .../test/resources/prime-test-database.sql | 11 +- 37 files changed, 1871 insertions(+), 117 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QCustomPossibleValueProvider.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/PossibleValueEnum.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValue.java create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslatorTest.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java index 7daf8de9..1d242057 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java @@ -215,7 +215,7 @@ public class RunBackendStepAction Object codeObject = codeClass.getConstructor().newInstance(); if(!(codeObject instanceof BackendStep backendStepCodeObject)) { - throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of FunctionBody")); + throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of BackendStep")); } backendStepCodeObject.run(runBackendStepInput, runBackendStepOutput); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java index eec134f0..83ec1dd1 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java @@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.actions.tables; import com.kingsrook.qqq.backend.core.actions.ActionHelper; +import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator; import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; @@ -45,14 +46,24 @@ public class QueryAction ActionHelper.validateSession(queryInput); QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); - QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(queryInput.getBackend()); + QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(queryInput.getBackend()); // todo pre-customization - just get to modify the request? QueryOutput queryOutput = qModule.getQueryInterface().execute(queryInput); // todo post-customization - can do whatever w/ the result if you want - if (queryInput.getRecordPipe() == null) + if(queryInput.getRecordPipe() == null) { - QValueFormatter.setDisplayValuesInRecords(queryInput.getTable(), queryOutput.getRecords()); + if(queryInput.getShouldGenerateDisplayValues()) + { + QValueFormatter qValueFormatter = new QValueFormatter(); + qValueFormatter.setDisplayValuesInRecords(queryInput.getTable(), queryOutput.getRecords()); + } + + if(queryInput.getShouldTranslatePossibleValues()) + { + QPossibleValueTranslator qPossibleValueTranslator = new QPossibleValueTranslator(queryInput.getInstance(), queryInput.getSession()); + qPossibleValueTranslator.translatePossibleValuesInRecords(queryInput.getTable(), queryOutput.getRecords()); + } } return queryOutput; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QCustomPossibleValueProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QCustomPossibleValueProvider.java new file mode 100644 index 00000000..5f2cdc17 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QCustomPossibleValueProvider.java @@ -0,0 +1,43 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.values; + + +import java.io.Serializable; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue; + + +/******************************************************************************* + ** Interface to be implemented by user-defined code that serves as the backing + ** for a CUSTOM type possibleValueSource + *******************************************************************************/ +public interface QCustomPossibleValueProvider +{ + + /******************************************************************************* + ** + *******************************************************************************/ + QPossibleValue getPossibleValue(Serializable idValue); + + // todo - get/search list of possible values + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java new file mode 100644 index 00000000..603951ee --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java @@ -0,0 +1,359 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.values; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.ListingHash; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + + +/******************************************************************************* + ** Class responsible for looking up possible-values for fields/records and + ** make them into display values. + *******************************************************************************/ +public class QPossibleValueTranslator +{ + private static final Logger LOG = LogManager.getLogger(QPossibleValueTranslator.class); + + private final QInstance qInstance; + private final QSession session; + + // top-level keys are pvsNames (not table names) + // 2nd-level keys are pkey values from the PVS table + private Map> possibleValueCache; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QPossibleValueTranslator(QInstance qInstance, QSession session) + { + this.qInstance = qInstance; + this.session = session; + + this.possibleValueCache = new HashMap<>(); + } + + + + /******************************************************************************* + ** For a list of records, translate their possible values (populating their display values) + *******************************************************************************/ + public void translatePossibleValuesInRecords(QTableMetaData table, List records) + { + if(records == null) + { + return; + } + + primePvsCache(table, records); + + for(QRecord record : records) + { + for(QFieldMetaData field : table.getFields().values()) + { + if(field.getPossibleValueSourceName() != null) + { + record.setDisplayValue(field.getName(), translatePossibleValue(field, record.getValue(field.getName()))); + } + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + String translatePossibleValue(QFieldMetaData field, Serializable value) + { + QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName()); + if(possibleValueSource == null) + { + LOG.error("Missing possible value source named [" + field.getPossibleValueSourceName() + "] when formatting value for field [" + field.getName() + "]"); + return (null); + } + + // todo - memoize!!! + // todo - bulk!!! + + String resultValue = null; + if(possibleValueSource.getType().equals(QPossibleValueSourceType.ENUM)) + { + resultValue = translatePossibleValueEnum(value, possibleValueSource); + } + else if(possibleValueSource.getType().equals(QPossibleValueSourceType.TABLE)) + { + resultValue = translatePossibleValueTable(field, value, possibleValueSource); + } + else if(possibleValueSource.getType().equals(QPossibleValueSourceType.CUSTOM)) + { + resultValue = translatePossibleValueCustom(field, value, possibleValueSource); + } + else + { + LOG.error("Unrecognized possibleValueSourceType [" + possibleValueSource.getType() + "] in PVS named [" + possibleValueSource.getName() + "] on field [" + field.getName() + "]"); + } + + if(resultValue == null) + { + resultValue = getDefaultForPossibleValue(possibleValueSource, value); + } + + return (resultValue); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String translatePossibleValueCustom(QFieldMetaData field, Serializable value, QPossibleValueSource possibleValueSource) + { + try + { + Class codeClass = Class.forName(possibleValueSource.getCustomCodeReference().getName()); + Object codeObject = codeClass.getConstructor().newInstance(); + if(!(codeObject instanceof QCustomPossibleValueProvider customPossibleValueProvider)) + { + throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of QCustomPossibleValueProvider")); + } + + return (formatPossibleValue(possibleValueSource, customPossibleValueProvider.getPossibleValue(value))); + } + catch(Exception e) + { + LOG.warn("Error sending [" + value + "] for field [" + field + "] through custom code for PVS [" + field.getPossibleValueSourceName() + "]", e); + } + + return (null); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String translatePossibleValueTable(QFieldMetaData field, Serializable value, QPossibleValueSource possibleValueSource) + { + ///////////////////////////////// + // null input gets null output // + ///////////////////////////////// + if(value == null) + { + return (null); + } + + ////////////////////////////////////////////////////////////// + // look for cached value - if it's missing, call the primer // + ////////////////////////////////////////////////////////////// + possibleValueCache.putIfAbsent(possibleValueSource.getName(), new HashMap<>()); + Map cacheForPvs = possibleValueCache.get(possibleValueSource.getName()); + if(!cacheForPvs.containsKey(value)) + { + primePvsCache(possibleValueSource.getTableName(), List.of(possibleValueSource), List.of(value)); + } + + return (cacheForPvs.get(value)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String formatPossibleValue(QPossibleValueSource possibleValueSource, QPossibleValue possibleValue) + { + return (doFormatPossibleValue(possibleValueSource.getValueFormat(), possibleValueSource.getValueFields(), possibleValue.getId(), possibleValue.getLabel())); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String getDefaultForPossibleValue(QPossibleValueSource possibleValueSource, Serializable value) + { + if(possibleValueSource.getValueFormatIfNotFound() == null) + { + return (null); + } + + return (doFormatPossibleValue(possibleValueSource.getValueFormatIfNotFound(), possibleValueSource.getValueFieldsIfNotFound(), value, null)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("checkstyle:Indentation") + private String doFormatPossibleValue(String formatString, List valueFields, Object id, String label) + { + List values = new ArrayList<>(); + if(valueFields != null) + { + for(String valueField : valueFields) + { + Object value = switch(valueField) + { + case "id" -> id; + case "label" -> label; + default -> throw new IllegalArgumentException("Unexpected value field: " + valueField); + }; + values.add(Objects.requireNonNullElse(value, "")); + } + } + + return (formatString.formatted(values.toArray())); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String translatePossibleValueEnum(Serializable value, QPossibleValueSource possibleValueSource) + { + for(QPossibleValue possibleValue : possibleValueSource.getEnumValues()) + { + if(possibleValue.getId().equals(value)) + { + return (formatPossibleValue(possibleValueSource, possibleValue)); + } + } + + return (null); + } + + + + /******************************************************************************* + ** prime the cache (e.g., by doing bulk-queries) for table-based PVS's + *******************************************************************************/ + void primePvsCache(QTableMetaData table, List records) + { + ListingHash fieldsByPvsTable = new ListingHash<>(); + ListingHash pvsesByTable = new ListingHash<>(); + for(QFieldMetaData field : table.getFields().values()) + { + QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName()); + if(possibleValueSource != null && possibleValueSource.getType().equals(QPossibleValueSourceType.TABLE)) + { + fieldsByPvsTable.add(possibleValueSource.getTableName(), field); + pvsesByTable.add(possibleValueSource.getTableName(), possibleValueSource); + } + } + + for(String tableName : fieldsByPvsTable.keySet()) + { + Set values = new HashSet<>(); + for(QRecord record : records) + { + for(QFieldMetaData field : fieldsByPvsTable.get(tableName)) + { + values.add(record.getValue(field.getName())); + } + } + + primePvsCache(tableName, pvsesByTable.get(tableName), values); + } + } + + + + /******************************************************************************* + ** For a given table, and a list of pkey-values in that table, AND a list of + ** possible value sources based on that table (maybe usually 1, but could be more, + ** e.g., if they had different formatting, or different filters (todo, would that work?) + ** - query for the values in the table, and populate the possibleValueCache. + *******************************************************************************/ + private void primePvsCache(String tableName, List possibleValueSources, Collection values) + { + for(QPossibleValueSource possibleValueSource : possibleValueSources) + { + possibleValueCache.putIfAbsent(possibleValueSource.getName(), new HashMap<>()); + } + + try + { + String primaryKeyField = qInstance.getTable(tableName).getPrimaryKeyField(); + + for(List page : CollectionUtils.getPages(values, 1000)) + { + QueryInput queryInput = new QueryInput(qInstance); + queryInput.setSession(session); + queryInput.setTableName(tableName); + queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(primaryKeyField, QCriteriaOperator.IN, page))); + + ///////////////////////////////////////////////////////////////////////////////////////// + // this is needed to get record labels, which are what we use here... unclear if best! // + ///////////////////////////////////////////////////////////////////////////////////////// + queryInput.setShouldGenerateDisplayValues(true); + + QueryOutput queryOutput = new QueryAction().execute(queryInput); + + for(QRecord record : queryOutput.getRecords()) + { + Serializable pkeyValue = record.getValue(primaryKeyField); + for(QPossibleValueSource possibleValueSource : possibleValueSources) + { + QPossibleValue possibleValue = new QPossibleValue<>(pkeyValue, record.getRecordLabel()); + possibleValueCache.get(possibleValueSource.getName()).put(pkeyValue, formatPossibleValue(possibleValueSource, possibleValue)); + } + } + } + } + catch(Exception e) + { + LOG.warn("Error looking up possible values for table [" + tableName + "]", e); + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java index 4c20ae7f..27ccde27 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java @@ -25,8 +25,10 @@ package com.kingsrook.qqq.backend.core.actions.values; import java.io.Serializable; import java.util.List; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; import org.apache.logging.log4j.LogManager; @@ -34,7 +36,8 @@ import org.apache.logging.log4j.Logger; /******************************************************************************* - ** Utility to apply display formats to values for fields + ** Utility to apply display formats to values for records and fields. + ** Note that this includes handling PossibleValues. *******************************************************************************/ public class QValueFormatter { @@ -45,7 +48,16 @@ public class QValueFormatter /******************************************************************************* ** *******************************************************************************/ - public static String formatValue(QFieldMetaData field, Serializable value) + public QValueFormatter() + { + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public String formatValue(QFieldMetaData field, Serializable value) { ////////////////////////////////// // null values get null results // @@ -55,6 +67,16 @@ public class QValueFormatter return (null); } + // todo - is this appropriate, with this class and possibleValueTransaltor being decoupled - to still do standard formatting here? + // alternatively, shold we return null here? + // /////////////////////////////////////////////// + // // if the field has a possible value, use it // + // /////////////////////////////////////////////// + // if(field.getPossibleValueSourceName() != null) + // { + // return (this.possibleValueTranslator.translatePossibleValue(field, value)); + // } + //////////////////////////////////////////////////////// // if the field has a display format, try to apply it // //////////////////////////////////////////////////////// @@ -68,6 +90,7 @@ public class QValueFormatter { try { + // todo - revisit if we actually want this - or - if you should get an error if you mis-configure your table this way (ideally during validation!) if(e.getMessage().equals("f != java.lang.Integer")) { return formatValue(field, ValueUtils.getValueAsBigDecimal(value)); @@ -99,7 +122,7 @@ public class QValueFormatter /******************************************************************************* ** Make a string from a table's recordLabelFormat and fields, for a given record. *******************************************************************************/ - public static String formatRecordLabel(QTableMetaData table, QRecord record) + public String formatRecordLabel(QTableMetaData table, QRecord record) { if(!StringUtils.hasContent(table.getRecordLabelFormat())) { @@ -128,7 +151,7 @@ public class QValueFormatter /******************************************************************************* ** Deal with non-happy-path cases for making a record label. *******************************************************************************/ - private static String formatRecordLabelExceptionalCases(QTableMetaData table, QRecord record) + private String formatRecordLabelExceptionalCases(QTableMetaData table, QRecord record) { /////////////////////////////////////////////////////////////////////////////////////// // if there's no record label format, then just return the primary key display value // @@ -156,7 +179,7 @@ public class QValueFormatter /******************************************************************************* ** For a list of records, set their recordLabels and display values *******************************************************************************/ - public static void setDisplayValuesInRecords(QTableMetaData table, List records) + public void setDisplayValuesInRecords(QTableMetaData table, List records) { if(records == null) { @@ -167,12 +190,13 @@ public class QValueFormatter { for(QFieldMetaData field : table.getFields().values()) { - String formattedValue = QValueFormatter.formatValue(field, record.getValue(field.getName())); + String formattedValue = formatValue(field, record.getValue(field.getName())); record.setDisplayValue(field.getName(), formattedValue); } - record.setRecordLabel(QValueFormatter.formatRecordLabel(table, record)); + record.setRecordLabel(formatRecordLabel(table, record)); } } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapter.java index 3d5493a3..98f85478 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapter.java @@ -95,6 +95,15 @@ public class CsvToQRecordAdapter throw (new IllegalArgumentException("Empty csv value was provided.")); } + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // once, from a DOS csv file (that had come from Excel), we had a "" character (FEFF, Byte-order marker) at the start of a // + // CSV, which caused our first header to not match... So, let us strip away any FEFF or FFFE's at the start of CSV strings. // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(csv.length() > 1 && (csv.charAt(0) == 0xfeff || csv.charAt(0) == 0xfffe)) + { + csv = csv.substring(1); + } + try { /////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -118,7 +127,7 @@ public class CsvToQRecordAdapter // put values from the CSV record into a map of header -> value // ////////////////////////////////////////////////////////////////// Map csvValues = new HashMap<>(); - for(int i = 0; i < headers.size(); i++) + for(int i = 0; i < headers.size() && i < csvRecord.size(); i++) { csvValues.put(headers.get(i), csvRecord.get(i)); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java index 6104f9db..119daa34 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java @@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.instances; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Locale; @@ -128,6 +129,11 @@ public class QInstanceEnricher { generateTableFieldSections(table); } + + if(CollectionUtils.nullSafeHasContents(table.getRecordLabelFields()) && !StringUtils.hasContent(table.getRecordLabelFormat())) + { + table.setRecordLabelFormat(String.join(" ", Collections.nCopies(table.getRecordLabelFields().size(), "%s"))); + } } @@ -211,7 +217,7 @@ public class QInstanceEnricher /******************************************************************************* ** *******************************************************************************/ - private String nameToLabel(String name) + static String nameToLabel(String name) { if(!StringUtils.hasContent(name)) { @@ -223,7 +229,7 @@ public class QInstanceEnricher return (name.substring(0, 1).toUpperCase(Locale.ROOT)); } - return (name.substring(0, 1).toUpperCase(Locale.ROOT) + name.substring(1).replaceAll("([A-Z])", " $1")); + return (name.substring(0, 1).toUpperCase(Locale.ROOT) + name.substring(1).replaceAll("([A-Z0-9]+)", " $1").replaceAll("([0-9])([A-Za-z])", "$1 $2")); } @@ -579,7 +585,7 @@ public class QInstanceEnricher { for(String fieldName : table.getRecordLabelFields()) { - if(!usedFieldNames.contains(fieldName)) + if(!usedFieldNames.contains(fieldName) && table.getFields().containsKey(fieldName)) { identitySection.getFieldNames().add(fieldName); usedFieldNames.add(fieldName); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java index 9ae87598..66959a31 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java @@ -29,6 +29,7 @@ import java.util.Objects; import java.util.Set; import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; @@ -88,6 +89,7 @@ public class QInstanceValidator validateTables(qInstance, errors); validateProcesses(qInstance, errors); validateApps(qInstance, errors); + validatePossibleValueSources(qInstance, errors); } catch(Exception e) { @@ -167,8 +169,8 @@ public class QInstanceValidator ////////////////////////////////////////// // validate field sections in the table // ////////////////////////////////////////// - Set fieldNamesInSections = new HashSet<>(); - QFieldSection tier1Section = null; + Set fieldNamesInSections = new HashSet<>(); + QFieldSection tier1Section = null; if(table.getSections() != null) { for(QFieldSection section : table.getSections()) @@ -190,6 +192,16 @@ public class QInstanceValidator } } + /////////////////////////////// + // validate the record label // + /////////////////////////////// + if(table.getRecordLabelFields() != null) + { + for(String recordLabelField : table.getRecordLabelFields()) + { + assertCondition(errors, table.getFields().containsKey(recordLabelField), "Table " + tableName + " record label field " + recordLabelField + " is not a field on this table."); + } + } }); } } @@ -225,7 +237,7 @@ public class QInstanceValidator *******************************************************************************/ private void validateProcesses(QInstance qInstance, List errors) { - if(!CollectionUtils.nullSafeIsEmpty(qInstance.getProcesses())) + if(CollectionUtils.nullSafeHasContents(qInstance.getProcesses())) { qInstance.getProcesses().forEach((processName, process) -> { @@ -264,7 +276,7 @@ public class QInstanceValidator *******************************************************************************/ private void validateApps(QInstance qInstance, List errors) { - if(!CollectionUtils.nullSafeIsEmpty(qInstance.getApps())) + if(CollectionUtils.nullSafeHasContents(qInstance.getApps())) { qInstance.getApps().forEach((appName, app) -> { @@ -291,6 +303,61 @@ public class QInstanceValidator + /******************************************************************************* + ** + *******************************************************************************/ + private void validatePossibleValueSources(QInstance qInstance, List errors) + { + if(CollectionUtils.nullSafeHasContents(qInstance.getPossibleValueSources())) + { + qInstance.getPossibleValueSources().forEach((pvsName, possibleValueSource) -> + { + assertCondition(errors, Objects.equals(pvsName, possibleValueSource.getName()), "Inconsistent naming for possibleValueSource: " + pvsName + "/" + possibleValueSource.getName() + "."); + assertCondition(errors, possibleValueSource.getIdType() != null, "Missing an idType for possibleValueSource: " + pvsName); + if(assertCondition(errors, possibleValueSource.getType() != null, "Missing type for possibleValueSource: " + pvsName)) + { + //////////////////////////////////////////////////////////////////////////////////////////////// + // assert about fields that should and should not be set, based on possible value source type // + // do additional type-specific validations as well // + //////////////////////////////////////////////////////////////////////////////////////////////// + switch(possibleValueSource.getType()) + { + case ENUM -> + { + assertCondition(errors, !StringUtils.hasContent(possibleValueSource.getTableName()), "enum-type possibleValueSource " + pvsName + " should not have a tableName."); + assertCondition(errors, possibleValueSource.getCustomCodeReference() == null, "enum-type possibleValueSource " + pvsName + " should not have a customCodeReference."); + + assertCondition(errors, CollectionUtils.nullSafeHasContents(possibleValueSource.getEnumValues()), "enum-type possibleValueSource " + pvsName + " is missing enum values"); + } + case TABLE -> + { + assertCondition(errors, CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "table-type possibleValueSource " + pvsName + " should not have enum values."); + assertCondition(errors, possibleValueSource.getCustomCodeReference() == null, "table-type possibleValueSource " + pvsName + " should not have a customCodeReference."); + + if(assertCondition(errors, StringUtils.hasContent(possibleValueSource.getTableName()), "table-type possibleValueSource " + pvsName + " is missing a tableName.")) + { + assertCondition(errors, qInstance.getTable(possibleValueSource.getTableName()) != null, "Unrecognized table " + possibleValueSource.getTableName() + " for possibleValueSource " + pvsName + "."); + } + } + case CUSTOM -> + { + assertCondition(errors, CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "custom-type possibleValueSource " + pvsName + " should not have enum values."); + assertCondition(errors, !StringUtils.hasContent(possibleValueSource.getTableName()), "custom-type possibleValueSource " + pvsName + " should not have a tableName."); + + if(assertCondition(errors, possibleValueSource.getCustomCodeReference() != null, "custom-type possibleValueSource " + pvsName + " is missing a customCodeReference.")) + { + assertCondition(errors, QCodeUsage.POSSIBLE_VALUE_PROVIDER.equals(possibleValueSource.getCustomCodeReference().getCodeUsage()), "customCodeReference for possibleValueSource " + pvsName + " is not a possibleValueProvider."); + } + } + default -> errors.add("Unexpected possibleValueSource type: " + possibleValueSource.getType()); + } + } + }); + } + } + + + /******************************************************************************* ** Check if an app's child list can recursively be traversed without finding a ** duplicate, which would indicate a cycle (e.g., an error) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java index daaa5ab7..94c2a7ae 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java @@ -40,6 +40,8 @@ public class QueryInput extends AbstractTableActionInput private RecordPipe recordPipe; + private boolean shouldTranslatePossibleValues = false; + private boolean shouldGenerateDisplayValues = false; /******************************************************************************* @@ -158,4 +160,47 @@ public class QueryInput extends AbstractTableActionInput this.recordPipe = recordPipe; } + + + /******************************************************************************* + ** Getter for shouldTranslatePossibleValues + ** + *******************************************************************************/ + public boolean getShouldTranslatePossibleValues() + { + return shouldTranslatePossibleValues; + } + + + + /******************************************************************************* + ** Setter for shouldTranslatePossibleValues + ** + *******************************************************************************/ + public void setShouldTranslatePossibleValues(boolean shouldTranslatePossibleValues) + { + this.shouldTranslatePossibleValues = shouldTranslatePossibleValues; + } + + + + /******************************************************************************* + ** Getter for shouldGenerateDisplayValues + ** + *******************************************************************************/ + public boolean getShouldGenerateDisplayValues() + { + return shouldGenerateDisplayValues; + } + + + + /******************************************************************************* + ** Setter for shouldGenerateDisplayValues + ** + *******************************************************************************/ + public void setShouldGenerateDisplayValues(boolean shouldGenerateDisplayValues) + { + this.shouldGenerateDisplayValues = shouldGenerateDisplayValues; + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QField.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QField.java index 6fd3eeb6..1988bd6a 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QField.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QField.java @@ -62,6 +62,11 @@ public @interface QField *******************************************************************************/ String displayFormat() default ""; + /******************************************************************************* + ** + *******************************************************************************/ + String possibleValueSourceName() default ""; + ////////////////////////////////////////////////////////////////////////////////////////// // new attributes here likely need implementation in QFieldMetaData.constructFromGetter // ////////////////////////////////////////////////////////////////////////////////////////// diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java index 6f4c5230..3712d077 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java @@ -54,10 +54,10 @@ public class QInstance //////////////////////////////////////////////////////////////////////////////////////////// // Important to use LinkedHashmap here, to preserve the order in which entries are added. // //////////////////////////////////////////////////////////////////////////////////////////// - private Map tables = new LinkedHashMap<>(); - private Map> possibleValueSources = new LinkedHashMap<>(); - private Map processes = new LinkedHashMap<>(); - private Map apps = new LinkedHashMap<>(); + private Map tables = new LinkedHashMap<>(); + private Map possibleValueSources = new LinkedHashMap<>(); + private Map processes = new LinkedHashMap<>(); + private Map apps = new LinkedHashMap<>(); // todo - lock down the object (no more changes allowed) after it's been validated? @@ -190,7 +190,7 @@ public class QInstance /******************************************************************************* ** *******************************************************************************/ - public void addPossibleValueSource(QPossibleValueSource possibleValueSource) + public void addPossibleValueSource(QPossibleValueSource possibleValueSource) { this.addPossibleValueSource(possibleValueSource.getName(), possibleValueSource); } @@ -353,7 +353,7 @@ public class QInstance ** Getter for possibleValueSources ** *******************************************************************************/ - public Map> getPossibleValueSources() + public Map getPossibleValueSources() { return possibleValueSources; } @@ -364,7 +364,7 @@ public class QInstance ** Setter for possibleValueSources ** *******************************************************************************/ - public void setPossibleValueSources(Map> possibleValueSources) + public void setPossibleValueSources(Map possibleValueSources) { this.possibleValueSources = possibleValueSources; } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java index c1093ca9..5a82f004 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java @@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.code; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider; /******************************************************************************* @@ -70,6 +71,10 @@ public class QCodeReference { this.codeUsage = QCodeUsage.BACKEND_STEP; } + else if(QCustomPossibleValueProvider.class.isAssignableFrom(javaClass)) + { + this.codeUsage = QCodeUsage.POSSIBLE_VALUE_PROVIDER; + } else { throw (new IllegalStateException("Unable to infer code usage type for class: " + javaClass.getName())); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeUsage.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeUsage.java index 9b459af1..925c1f55 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeUsage.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeUsage.java @@ -29,5 +29,6 @@ package com.kingsrook.qqq.backend.core.model.metadata.code; public enum QCodeUsage { BACKEND_STEP, // a backend-step in a process - CUSTOMIZER // a function to customize part of a QQQ table's behavior + CUSTOMIZER, // a function to customize part of a QQQ table's behavior + POSSIBLE_VALUE_PROVIDER // code that drives a custom possibleValueSource } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java index 8967c579..a21d7cce 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java @@ -133,6 +133,11 @@ public class QFieldMetaData { setDisplayFormat(fieldAnnotation.displayFormat()); } + + if(StringUtils.hasContent(fieldAnnotation.possibleValueSourceName())) + { + setPossibleValueSourceName(fieldAnnotation.possibleValueSourceName()); + } } } catch(QException qe) @@ -406,6 +411,7 @@ public class QFieldMetaData } + /******************************************************************************* ** Getter for displayFormat ** @@ -427,6 +433,7 @@ public class QFieldMetaData } + /******************************************************************************* ** Fluent setter for displayFormat ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java index f57a05ff..4dbbcfc6 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java @@ -36,11 +36,13 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; @JsonInclude(Include.NON_NULL) public class QFrontendFieldMetaData { - private String name; - private String label; + private String name; + private String label; private QFieldType type; - private boolean isRequired; - private boolean isEditable; + private boolean isRequired; + private boolean isEditable; + private String possibleValueSourceName; + private String displayFormat; ////////////////////////////////////////////////////////////////////////////////// // do not add setters. take values from the source-object in the constructor!! // @@ -58,6 +60,8 @@ public class QFrontendFieldMetaData this.type = fieldMetaData.getType(); this.isRequired = fieldMetaData.getIsRequired(); this.isEditable = fieldMetaData.getIsEditable(); + this.possibleValueSourceName = fieldMetaData.getPossibleValueSourceName(); + this.displayFormat = fieldMetaData.getDisplayFormat(); } @@ -115,4 +119,26 @@ public class QFrontendFieldMetaData return isEditable; } + + + /******************************************************************************* + ** Getter for displayFormat + ** + *******************************************************************************/ + public String getDisplayFormat() + { + return displayFormat; + } + + + + /******************************************************************************* + ** Getter for possibleValueSourceName + ** + *******************************************************************************/ + public String getPossibleValueSourceName() + { + return possibleValueSourceName; + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/PossibleValueEnum.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/PossibleValueEnum.java new file mode 100644 index 00000000..24018b3a --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/PossibleValueEnum.java @@ -0,0 +1,39 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues; + + +/******************************************************************************* + ** + *******************************************************************************/ +public interface PossibleValueEnum +{ + /******************************************************************************* + ** + *******************************************************************************/ + T getPossibleValueId(); + + /******************************************************************************* + ** + *******************************************************************************/ + String getPossibleValueLabel(); +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValue.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValue.java new file mode 100644 index 00000000..9bdae034 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValue.java @@ -0,0 +1,78 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues; + + +/******************************************************************************* + ** An actual possible value - an id and label. + ** + *******************************************************************************/ +public class QPossibleValue +{ + private final T id; + private final String label; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("unchecked") + public QPossibleValue(String value) + { + this.id = (T) value; + this.label = value; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QPossibleValue(T id, String label) + { + this.id = id; + this.label = label; + } + + + + /******************************************************************************* + ** Getter for id + ** + *******************************************************************************/ + public T getId() + { + return id; + } + + + + /******************************************************************************* + ** Getter for label + ** + *******************************************************************************/ + public String getLabel() + { + return label; + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java index 0cb0a322..735a22e4 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java @@ -24,19 +24,62 @@ package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues; import java.util.ArrayList; import java.util.List; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; /******************************************************************************* ** Meta-data to represent a single field in a table. ** *******************************************************************************/ -public class QPossibleValueSource +public class QPossibleValueSource { - private String name; + private String name; private QPossibleValueSourceType type; + private QFieldType idType = QFieldType.INTEGER; - // should these be in sub-types?? - private List enumValues; + private String valueFormat = ValueFormat.DEFAULT; + private List valueFields = ValueFields.DEFAULT; + private String valueFormatIfNotFound = null; + private List valueFieldsIfNotFound = null; + + + + public interface ValueFormat + { + String DEFAULT = "%s"; + String LABEL_ONLY = "%s"; + String LABEL_PARENS_ID = "%s (%s)"; + String ID_COLON_LABEL = "%s: %s"; + } + + + + public interface ValueFields + { + List DEFAULT = List.of("label"); + List LABEL_ONLY = List.of("label"); + List LABEL_PARENS_ID = List.of("label", "id"); + List ID_COLON_LABEL = List.of("id", "label"); + } + + // todo - optimization hints, such as "table is static, fully cache" or "table is small, so we can pull the whole thing into memory?" + + ////////////////////// + // for type = TABLE // + ////////////////////// + private String tableName; + // todo - override labelFormat & labelFields? + + ///////////////////// + // for type = ENUM // + ///////////////////// + private List> enumValues; + + /////////////////////// + // for type = CUSTOM // + /////////////////////// + private QCodeReference customCodeReference; @@ -72,7 +115,7 @@ public class QPossibleValueSource /******************************************************************************* ** *******************************************************************************/ - public QPossibleValueSource withName(String name) + public QPossibleValueSource withName(String name) { this.name = name; return (this); @@ -103,7 +146,7 @@ public class QPossibleValueSource /******************************************************************************* ** *******************************************************************************/ - public QPossibleValueSource withType(QPossibleValueSourceType type) + public QPossibleValueSource withType(QPossibleValueSourceType type) { this.type = type; return (this); @@ -111,11 +154,215 @@ public class QPossibleValueSource + /******************************************************************************* + ** Getter for idType + ** + *******************************************************************************/ + public QFieldType getIdType() + { + return idType; + } + + + + /******************************************************************************* + ** Setter for idType + ** + *******************************************************************************/ + public void setIdType(QFieldType idType) + { + this.idType = idType; + } + + + + /******************************************************************************* + ** Fluent setter for idType + ** + *******************************************************************************/ + public QPossibleValueSource withIdType(QFieldType idType) + { + this.idType = idType; + return (this); + } + + + + /******************************************************************************* + ** Getter for valueFormat + ** + *******************************************************************************/ + public String getValueFormat() + { + return valueFormat; + } + + + + /******************************************************************************* + ** Setter for valueFormat + ** + *******************************************************************************/ + public void setValueFormat(String valueFormat) + { + this.valueFormat = valueFormat; + } + + + + /******************************************************************************* + ** Fluent setter for valueFormat + ** + *******************************************************************************/ + public QPossibleValueSource withValueFormat(String valueFormat) + { + this.valueFormat = valueFormat; + return (this); + } + + + + /******************************************************************************* + ** Getter for valueFields + ** + *******************************************************************************/ + public List getValueFields() + { + return valueFields; + } + + + + /******************************************************************************* + ** Setter for valueFields + ** + *******************************************************************************/ + public void setValueFields(List valueFields) + { + this.valueFields = valueFields; + } + + + + /******************************************************************************* + ** Fluent setter for valueFields + ** + *******************************************************************************/ + public QPossibleValueSource withValueFields(List valueFields) + { + this.valueFields = valueFields; + return (this); + } + + + + /******************************************************************************* + ** Getter for valueFormatIfNotFound + ** + *******************************************************************************/ + public String getValueFormatIfNotFound() + { + return valueFormatIfNotFound; + } + + + + /******************************************************************************* + ** Setter for valueFormatIfNotFound + ** + *******************************************************************************/ + public void setValueFormatIfNotFound(String valueFormatIfNotFound) + { + this.valueFormatIfNotFound = valueFormatIfNotFound; + } + + + + /******************************************************************************* + ** Fluent setter for valueFormatIfNotFound + ** + *******************************************************************************/ + public QPossibleValueSource withValueFormatIfNotFound(String valueFormatIfNotFound) + { + this.valueFormatIfNotFound = valueFormatIfNotFound; + return (this); + } + + + + /******************************************************************************* + ** Getter for valueFieldsIfNotFound + ** + *******************************************************************************/ + public List getValueFieldsIfNotFound() + { + return valueFieldsIfNotFound; + } + + + + /******************************************************************************* + ** Setter for valueFieldsIfNotFound + ** + *******************************************************************************/ + public void setValueFieldsIfNotFound(List valueFieldsIfNotFound) + { + this.valueFieldsIfNotFound = valueFieldsIfNotFound; + } + + + + /******************************************************************************* + ** Fluent setter for valueFieldsIfNotFound + ** + *******************************************************************************/ + public QPossibleValueSource withValueFieldsIfNotFound(List valueFieldsIfNotFound) + { + this.valueFieldsIfNotFound = valueFieldsIfNotFound; + return (this); + } + + + + /******************************************************************************* + ** Getter for tableName + ** + *******************************************************************************/ + public String getTableName() + { + return tableName; + } + + + + /******************************************************************************* + ** Setter for tableName + ** + *******************************************************************************/ + public void setTableName(String tableName) + { + this.tableName = tableName; + } + + + + /******************************************************************************* + ** Fluent setter for tableName + ** + *******************************************************************************/ + public QPossibleValueSource withTableName(String tableName) + { + this.tableName = tableName; + return (this); + } + + + /******************************************************************************* ** Getter for enumValues ** *******************************************************************************/ - public List getEnumValues() + public List> getEnumValues() { return enumValues; } @@ -126,7 +373,7 @@ public class QPossibleValueSource ** Setter for enumValues ** *******************************************************************************/ - public void setEnumValues(List enumValues) + public void setEnumValues(List> enumValues) { this.enumValues = enumValues; } @@ -137,7 +384,7 @@ public class QPossibleValueSource ** Fluent setter for enumValues ** *******************************************************************************/ - public QPossibleValueSource withEnumValues(List enumValues) + public QPossibleValueSource withEnumValues(List> enumValues) { this.enumValues = enumValues; return this; @@ -146,16 +393,64 @@ public class QPossibleValueSource /******************************************************************************* - ** Fluent adder for enumValues ** *******************************************************************************/ - public QPossibleValueSource addEnumValue(T enumValue) + public void addEnumValue(QPossibleValue possibleValue) { if(this.enumValues == null) { this.enumValues = new ArrayList<>(); } - this.enumValues.add(enumValue); - return this; + this.enumValues.add(possibleValue); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public > QPossibleValueSource withValuesFromEnum(T[] values) + { + for(T t : values) + { + addEnumValue(new QPossibleValue<>(t.getPossibleValueId(), t.getPossibleValueLabel())); + } + + return (this); + } + + + + /******************************************************************************* + ** Getter for customCodeReference + ** + *******************************************************************************/ + public QCodeReference getCustomCodeReference() + { + return customCodeReference; + } + + + + /******************************************************************************* + ** Setter for customCodeReference + ** + *******************************************************************************/ + public void setCustomCodeReference(QCodeReference customCodeReference) + { + this.customCodeReference = customCodeReference; + } + + + + /******************************************************************************* + ** Fluent setter for customCodeReference + ** + *******************************************************************************/ + public QPossibleValueSource withCustomCodeReference(QCodeReference customCodeReference) + { + this.customCodeReference = customCodeReference; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java index 2f920886..ebb3e36a 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java @@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.tables; import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -587,6 +588,18 @@ public class QTableMetaData implements QAppChildMetaData, Serializable + /******************************************************************************* + ** Fluent setter for recordLabelFields + ** + *******************************************************************************/ + public QTableMetaData withRecordLabelFields(String... recordLabelFields) + { + this.recordLabelFields = Arrays.asList(recordLabelFields); + return (this); + } + + + /******************************************************************************* ** Getter for sections ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java index 435a13ce..6b53dd6b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java @@ -167,6 +167,11 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface return (false); } + if(session.getIdReference() == null) + { + return (false); + } + StateProviderInterface spi = getStateProvider(); Auth0StateKey key = new Auth0StateKey(session.getIdReference()); Optional lastTimeCheckedOptional = spi.get(Instant.class, key); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java index 8fc7a02b..f195edcf 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java @@ -24,18 +24,21 @@ package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory; import java.io.Serializable; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; 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.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import org.apache.commons.lang.NotImplementedException; /******************************************************************************* @@ -48,6 +51,12 @@ public class MemoryRecordStore private Map> data; private Map nextSerials; + private static boolean collectStatistics = false; + + private static final Map statistics = Collections.synchronizedMap(new HashMap<>()); + + public static final String STAT_QUERIES_RAN = "queriesRan"; + /******************************************************************************* @@ -105,9 +114,56 @@ public class MemoryRecordStore *******************************************************************************/ public List query(QueryInput input) { + incrementStatistic(STAT_QUERIES_RAN); + Map tableData = getTableData(input.getTable()); - List records = new ArrayList<>(tableData.values()); - // todo - filtering + List records = new ArrayList<>(); + + for(QRecord qRecord : tableData.values()) + { + boolean recordMatches = true; + if(input.getFilter() != null && input.getFilter().getCriteria() != null) + { + for(QFilterCriteria criterion : input.getFilter().getCriteria()) + { + String fieldName = criterion.getFieldName(); + Serializable value = qRecord.getValue(fieldName); + switch(criterion.getOperator()) + { + case EQUALS: + { + if(!value.equals(criterion.getValues().get(0))) + { + recordMatches = false; + } + break; + } + case IN: + { + if(!criterion.getValues().contains(value)) + { + recordMatches = false; + } + break; + } + default: + { + throw new NotImplementedException("Operator [" + criterion.getOperator() + "] is not yet implemented in the Memory backend."); + } + } + if(!recordMatches) + { + break; + } + } + } + + if(recordMatches) + { + records.add(qRecord); + } + } + return (records); } @@ -120,7 +176,7 @@ public class MemoryRecordStore { Map tableData = getTableData(input.getTable()); List records = new ArrayList<>(tableData.values()); - // todo - filtering + // todo - filtering (call query) return (records.size()); } @@ -235,4 +291,53 @@ public class MemoryRecordStore return (rowsDeleted); } + + + + /******************************************************************************* + ** Setter for collectStatistics + ** + *******************************************************************************/ + public static void setCollectStatistics(boolean collectStatistics) + { + MemoryRecordStore.collectStatistics = collectStatistics; + } + + + + /******************************************************************************* + ** Increment a statistic + ** + *******************************************************************************/ + public static void incrementStatistic(String statName) + { + if(collectStatistics) + { + statistics.putIfAbsent(statName, 0); + statistics.put(statName, statistics.get(statName) + 1); + } + } + + + + /******************************************************************************* + ** clear the map of statistics + ** + *******************************************************************************/ + public static void resetStatistics() + { + statistics.clear(); + } + + + + /******************************************************************************* + ** Getter for statistics + ** + *******************************************************************************/ + public static Map getStatistics() + { + return statistics; + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java index 326e0742..bdb3b21b 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java @@ -47,18 +47,33 @@ class QueryActionTest @Test public void test() throws QException { - QueryInput request = new QueryInput(TestUtils.defineInstance()); - request.setSession(TestUtils.getMockSession()); - request.setTableName("person"); - QueryOutput result = new QueryAction().execute(request); - assertNotNull(result); + QueryInput queryInput = new QueryInput(TestUtils.defineInstance()); + queryInput.setSession(TestUtils.getMockSession()); + queryInput.setTableName("person"); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertNotNull(queryOutput); - assertThat(result.getRecords()).isNotEmpty(); - for(QRecord record : result.getRecords()) + assertThat(queryOutput.getRecords()).isNotEmpty(); + for(QRecord record : queryOutput.getRecords()) { assertThat(record.getValues()).isNotEmpty(); - assertThat(record.getDisplayValues()).isNotEmpty(); assertThat(record.getErrors()).isEmpty(); + + /////////////////////////////////////////////////////////////// + // this SHOULD be empty, based on the default for the should // + /////////////////////////////////////////////////////////////// + assertThat(record.getDisplayValues()).isEmpty(); + } + + //////////////////////////////////// + // now flip that field and re-run // + //////////////////////////////////// + queryInput.setShouldGenerateDisplayValues(true); + assertThat(queryOutput.getRecords()).isNotEmpty(); + queryOutput = new QueryAction().execute(queryInput); + for(QRecord record : queryOutput.getRecords()) + { + assertThat(record.getDisplayValues()).isNotEmpty(); } } } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslatorTest.java new file mode 100644 index 00000000..2d4dbbf6 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslatorTest.java @@ -0,0 +1,241 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.values; + + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class QPossibleValueTranslatorTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPossibleValueEnum() + { + QInstance qInstance = TestUtils.defineInstance(); + QPossibleValueTranslator possibleValueTranslator = new QPossibleValueTranslator(qInstance, new QSession()); + QFieldMetaData stateField = qInstance.getTable("person").getField("homeStateId"); + QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(stateField.getPossibleValueSourceName()); + + ////////////////////////////////////////////////////////////////////////// + // assert the default formatting for a not-found value is a null string // + ////////////////////////////////////////////////////////////////////////// + assertNull(possibleValueTranslator.translatePossibleValue(stateField, null)); + assertNull(possibleValueTranslator.translatePossibleValue(stateField, -1)); + + ////////////////////////////////////////////////////////////////////// + // let the not-found value be a simple string (no formatted values) // + ////////////////////////////////////////////////////////////////////// + possibleValueSource.setValueFormatIfNotFound("?"); + assertEquals("?", possibleValueTranslator.translatePossibleValue(stateField, null)); + assertEquals("?", possibleValueTranslator.translatePossibleValue(stateField, -1)); + + ///////////////////////////////////////////////////////////// + // let the not-found value be a string w/ formatted values // + ///////////////////////////////////////////////////////////// + possibleValueSource.setValueFormatIfNotFound("? (%s)"); + possibleValueSource.setValueFieldsIfNotFound(List.of("id")); + assertEquals("? ()", possibleValueTranslator.translatePossibleValue(stateField, null)); + assertEquals("? (-1)", possibleValueTranslator.translatePossibleValue(stateField, -1)); + + ///////////////////////////////////////////////////// + // assert the default formatting is just the label // + ///////////////////////////////////////////////////// + assertEquals("MO", possibleValueTranslator.translatePossibleValue(stateField, 2)); + assertEquals("IL", possibleValueTranslator.translatePossibleValue(stateField, 1)); + + ///////////////////////////////////////////////////////////////// + // assert the LABEL_ONLY format (when called out specifically) // + ///////////////////////////////////////////////////////////////// + possibleValueSource.setValueFormat(QPossibleValueSource.ValueFormat.LABEL_ONLY); + possibleValueSource.setValueFields(QPossibleValueSource.ValueFields.LABEL_ONLY); + assertEquals("IL", possibleValueTranslator.translatePossibleValue(stateField, 1)); + + /////////////////////////////////////// + // assert the LABEL_PARAMS_ID format // + /////////////////////////////////////// + possibleValueSource.setValueFormat(QPossibleValueSource.ValueFormat.LABEL_PARENS_ID); + possibleValueSource.setValueFields(QPossibleValueSource.ValueFields.LABEL_PARENS_ID); + assertEquals("IL (1)", possibleValueTranslator.translatePossibleValue(stateField, 1)); + + ////////////////////////////////////// + // assert the ID_COLON_LABEL format // + ////////////////////////////////////// + possibleValueSource.setValueFormat(QPossibleValueSource.ValueFormat.ID_COLON_LABEL); + possibleValueSource.setValueFields(QPossibleValueSource.ValueFields.ID_COLON_LABEL); + assertEquals("1: IL", possibleValueTranslator.translatePossibleValue(stateField, 1)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPossibleValueTable() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QPossibleValueTranslator possibleValueTranslator = new QPossibleValueTranslator(qInstance, new QSession()); + QTableMetaData shapeTable = qInstance.getTable(TestUtils.TABLE_NAME_SHAPE); + QFieldMetaData shapeField = qInstance.getTable(TestUtils.TABLE_NAME_PERSON).getField("favoriteShapeId"); + QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(shapeField.getPossibleValueSourceName()); + + List shapeRecords = List.of( + new QRecord().withTableName(shapeTable.getName()).withValue("id", 1).withValue("name", "Triangle"), + new QRecord().withTableName(shapeTable.getName()).withValue("id", 2).withValue("name", "Square"), + new QRecord().withTableName(shapeTable.getName()).withValue("id", 3).withValue("name", "Circle")); + + InsertInput insertInput = new InsertInput(qInstance); + insertInput.setSession(new QSession()); + insertInput.setTableName(shapeTable.getName()); + insertInput.setRecords(shapeRecords); + new InsertAction().execute(insertInput); + + ////////////////////////////////////////////////////////////////////////// + // assert the default formatting for a not-found value is a null string // + ////////////////////////////////////////////////////////////////////////// + assertNull(possibleValueTranslator.translatePossibleValue(shapeField, null)); + assertNull(possibleValueTranslator.translatePossibleValue(shapeField, -1)); + + ////////////////////////////////////////////////////////////////////// + // let the not-found value be a simple string (no formatted values) // + ////////////////////////////////////////////////////////////////////// + possibleValueSource.setValueFormatIfNotFound("?"); + assertEquals("?", possibleValueTranslator.translatePossibleValue(shapeField, null)); + assertEquals("?", possibleValueTranslator.translatePossibleValue(shapeField, -1)); + + ///////////////////////////////////////////////////// + // assert the default formatting is just the label // + ///////////////////////////////////////////////////// + assertEquals("Square", possibleValueTranslator.translatePossibleValue(shapeField, 2)); + assertEquals("Triangle", possibleValueTranslator.translatePossibleValue(shapeField, 1)); + + /////////////////////////////////////// + // assert the LABEL_PARAMS_ID format // + /////////////////////////////////////// + possibleValueSource.setValueFormat(QPossibleValueSource.ValueFormat.LABEL_PARENS_ID); + possibleValueSource.setValueFields(QPossibleValueSource.ValueFields.LABEL_PARENS_ID); + assertEquals("Circle (3)", possibleValueTranslator.translatePossibleValue(shapeField, 3)); + + /////////////////////////////////////////////////////////// + // assert that we don't re-run queries for cached values // + /////////////////////////////////////////////////////////// + possibleValueTranslator = new QPossibleValueTranslator(qInstance, new QSession()); + MemoryRecordStore.setCollectStatistics(true); + possibleValueTranslator.translatePossibleValue(shapeField, 1); + possibleValueTranslator.translatePossibleValue(shapeField, 2); + assertEquals(2, MemoryRecordStore.getStatistics().get(MemoryRecordStore.STAT_QUERIES_RAN), "Should have ran 2 queries so far"); + possibleValueTranslator.translatePossibleValue(shapeField, 2); + possibleValueTranslator.translatePossibleValue(shapeField, 3); + possibleValueTranslator.translatePossibleValue(shapeField, 3); + assertEquals(3, MemoryRecordStore.getStatistics().get(MemoryRecordStore.STAT_QUERIES_RAN), "Should have ran 3 queries in total"); + + /////////////////////////////////////////////////////////////// + // assert that if we prime the cache, we can do just 1 query // + /////////////////////////////////////////////////////////////// + possibleValueTranslator = new QPossibleValueTranslator(qInstance, new QSession()); + List personRecords = List.of( + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 1), + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 1), + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 2), + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 2), + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 3) + ); + QTableMetaData personTable = qInstance.getTable(TestUtils.TABLE_NAME_PERSON); + MemoryRecordStore.resetStatistics(); + possibleValueTranslator.primePvsCache(personTable, personRecords); + assertEquals(1, MemoryRecordStore.getStatistics().get(MemoryRecordStore.STAT_QUERIES_RAN), "Should only run 1 query"); + possibleValueTranslator.translatePossibleValue(shapeField, 1); + possibleValueTranslator.translatePossibleValue(shapeField, 2); + assertEquals(1, MemoryRecordStore.getStatistics().get(MemoryRecordStore.STAT_QUERIES_RAN), "Should only run 1 query"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSetDisplayValuesInRecords() + { + QTableMetaData table = new QTableMetaData() + .withRecordLabelFormat("%s %s") + .withRecordLabelFields("firstName", "lastName") + .withField(new QFieldMetaData("firstName", QFieldType.STRING)) + .withField(new QFieldMetaData("lastName", QFieldType.STRING)) + .withField(new QFieldMetaData("price", QFieldType.DECIMAL).withDisplayFormat(DisplayFormat.CURRENCY)) + .withField(new QFieldMetaData("homeStateId", QFieldType.INTEGER).withPossibleValueSourceName(TestUtils.POSSIBLE_VALUE_SOURCE_STATE)); + + ///////////////////////////////////////////////////////////////// + // first, make sure it doesn't crash with null or empty inputs // + ///////////////////////////////////////////////////////////////// + QPossibleValueTranslator possibleValueTranslator = new QPossibleValueTranslator(TestUtils.defineInstance(), new QSession()); + possibleValueTranslator.translatePossibleValuesInRecords(table, null); + possibleValueTranslator.translatePossibleValuesInRecords(table, Collections.emptyList()); + + List records = List.of( + new QRecord() + .withValue("firstName", "Tim") + .withValue("lastName", "Chamberlain") + .withValue("price", new BigDecimal("3.50")) + .withValue("homeStateId", 1), + new QRecord() + .withValue("firstName", "Tyler") + .withValue("lastName", "Samples") + .withValue("price", new BigDecimal("174999.99")) + .withValue("homeStateId", 2) + ); + + possibleValueTranslator.translatePossibleValuesInRecords(table, records); + + assertNull(records.get(0).getRecordLabel()); // regular display stuff NOT done by PVS translator + assertNull(records.get(0).getDisplayValue("price")); + + assertEquals("IL", records.get(0).getDisplayValue("homeStateId")); + assertEquals("MO", records.get(1).getDisplayValue("homeStateId")); + } + +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatterTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatterTest.java index 4be9cf52..27428209 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatterTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatterTest.java @@ -30,6 +30,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; @@ -47,24 +48,26 @@ class QValueFormatterTest @Test void testFormatValue() { - assertNull(QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), null)); + QValueFormatter qValueFormatter = new QValueFormatter(); - assertEquals("1", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), 1)); - assertEquals("1,000", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), 1000)); - assertEquals("1000", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(null), 1000)); - assertEquals("$1,000.00", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.CURRENCY), 1000)); - assertEquals("1,000.00", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.DECIMAL2_COMMAS), 1000)); - assertEquals("1000.00", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.DECIMAL2), 1000)); + assertNull(qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), null)); - assertEquals("1", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), new BigDecimal("1"))); - assertEquals("1,000", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), new BigDecimal("1000"))); - assertEquals("1000", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.STRING), new BigDecimal("1000"))); - assertEquals("1000", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.STRING), 1000)); + assertEquals("1", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), 1)); + assertEquals("1,000", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), 1000)); + assertEquals("1000", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(null), 1000)); + assertEquals("$1,000.00", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.CURRENCY), 1000)); + assertEquals("1,000.00", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.DECIMAL2_COMMAS), 1000)); + assertEquals("1000.00", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.DECIMAL2), 1000)); + + assertEquals("1", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), new BigDecimal("1"))); + assertEquals("1,000", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), new BigDecimal("1000"))); + assertEquals("1000", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.STRING), new BigDecimal("1000"))); + assertEquals("1000", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.STRING), 1000)); ////////////////////////////////////////////////// // this one flows through the exceptional cases // ////////////////////////////////////////////////// - assertEquals("1000.01", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), new BigDecimal("1000.01"))); + assertEquals("1000.01", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), new BigDecimal("1000.01"))); } @@ -75,40 +78,42 @@ class QValueFormatterTest @Test void testFormatRecordLabel() { - QTableMetaData table = new QTableMetaData().withRecordLabelFormat("%s %s").withRecordLabelFields(List.of("firstName", "lastName")); - assertEquals("Darin Kelkhoff", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("firstName", "Darin").withValue("lastName", "Kelkhoff"))); - assertEquals("Darin ", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("firstName", "Darin"))); - assertEquals("Darin ", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("firstName", "Darin").withValue("lastName", null))); + QValueFormatter qValueFormatter = new QValueFormatter(); - table = new QTableMetaData().withRecordLabelFormat("%s " + DisplayFormat.CURRENCY).withRecordLabelFields(List.of("firstName", "price")); - assertEquals("Darin $10,000.00", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("firstName", "Darin").withValue("price", new BigDecimal(10000)))); + QTableMetaData table = new QTableMetaData().withRecordLabelFormat("%s %s").withRecordLabelFields(List.of("firstName", "lastName")); + assertEquals("Darin Kelkhoff", qValueFormatter.formatRecordLabel(table, new QRecord().withValue("firstName", "Darin").withValue("lastName", "Kelkhoff"))); + assertEquals("Darin ", qValueFormatter.formatRecordLabel(table, new QRecord().withValue("firstName", "Darin"))); + assertEquals("Darin ", qValueFormatter.formatRecordLabel(table, new QRecord().withValue("firstName", "Darin").withValue("lastName", null))); + + table = new QTableMetaData().withRecordLabelFormat("%s " + DisplayFormat.CURRENCY).withRecordLabelFields("firstName", "price"); + assertEquals("Darin $10,000.00", qValueFormatter.formatRecordLabel(table, new QRecord().withValue("firstName", "Darin").withValue("price", new BigDecimal(10000)))); table = new QTableMetaData().withRecordLabelFormat(DisplayFormat.DEFAULT).withRecordLabelFields(List.of("id")); - assertEquals("123456", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("id", "123456"))); + assertEquals("123456", qValueFormatter.formatRecordLabel(table, new QRecord().withValue("id", "123456"))); /////////////////////////////////////////////////////// // exceptional flow: no recordLabelFormat specified // /////////////////////////////////////////////////////// table = new QTableMetaData().withPrimaryKeyField("id"); - assertEquals("42", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("id", 42))); + assertEquals("42", qValueFormatter.formatRecordLabel(table, new QRecord().withValue("id", 42))); ///////////////////////////////////////////////// // exceptional flow: no fields for the format // ///////////////////////////////////////////////// table = new QTableMetaData().withRecordLabelFormat("%s %s").withPrimaryKeyField("id"); - assertEquals("128", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("id", 128))); + assertEquals("128", qValueFormatter.formatRecordLabel(table, new QRecord().withValue("id", 128))); ///////////////////////////////////////////////////////// // exceptional flow: not enough fields for the format // ///////////////////////////////////////////////////////// - table = new QTableMetaData().withRecordLabelFormat("%s %s").withRecordLabelFields(List.of("a")).withPrimaryKeyField("id"); - assertEquals("256", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("a", 47).withValue("id", 256))); + table = new QTableMetaData().withRecordLabelFormat("%s %s").withRecordLabelFields("a").withPrimaryKeyField("id"); + assertEquals("256", qValueFormatter.formatRecordLabel(table, new QRecord().withValue("a", 47).withValue("id", 256))); ////////////////////////////////////////////////////////////////////////////////////////////////////////// // exceptional flow (kinda): too many fields for the format (just get the ones that are in the format) // ////////////////////////////////////////////////////////////////////////////////////////////////////////// table = new QTableMetaData().withRecordLabelFormat("%s %s").withRecordLabelFields(List.of("a", "b", "c")).withPrimaryKeyField("id"); - assertEquals("47 48", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("a", 47).withValue("b", 48).withValue("c", 49).withValue("id", 256))); + assertEquals("47 48", qValueFormatter.formatRecordLabel(table, new QRecord().withValue("a", 47).withValue("b", 48).withValue("c", 49).withValue("id", 256))); } @@ -121,40 +126,46 @@ class QValueFormatterTest { QTableMetaData table = new QTableMetaData() .withRecordLabelFormat("%s %s") - .withRecordLabelFields(List.of("firstName", "lastName")) + .withRecordLabelFields("firstName", "lastName") .withField(new QFieldMetaData("firstName", QFieldType.STRING)) .withField(new QFieldMetaData("lastName", QFieldType.STRING)) .withField(new QFieldMetaData("price", QFieldType.DECIMAL).withDisplayFormat(DisplayFormat.CURRENCY)) - .withField(new QFieldMetaData("quantity", QFieldType.INTEGER).withDisplayFormat(DisplayFormat.COMMAS)); + .withField(new QFieldMetaData("quantity", QFieldType.INTEGER).withDisplayFormat(DisplayFormat.COMMAS)) + .withField(new QFieldMetaData("homeStateId", QFieldType.INTEGER).withPossibleValueSourceName(TestUtils.POSSIBLE_VALUE_SOURCE_STATE)); ///////////////////////////////////////////////////////////////// // first, make sure it doesn't crash with null or empty inputs // ///////////////////////////////////////////////////////////////// - QValueFormatter.setDisplayValuesInRecords(table, null); - QValueFormatter.setDisplayValuesInRecords(table, Collections.emptyList()); + QValueFormatter qValueFormatter = new QValueFormatter(); + qValueFormatter.setDisplayValuesInRecords(table, null); + qValueFormatter.setDisplayValuesInRecords(table, Collections.emptyList()); List records = List.of( new QRecord() .withValue("firstName", "Tim") .withValue("lastName", "Chamberlain") .withValue("price", new BigDecimal("3.50")) - .withValue("quantity", 1701), + .withValue("quantity", 1701) + .withValue("homeStateId", 1), new QRecord() .withValue("firstName", "Tyler") .withValue("lastName", "Samples") .withValue("price", new BigDecimal("174999.99")) .withValue("quantity", 47) + .withValue("homeStateId", 2) ); - QValueFormatter.setDisplayValuesInRecords(table, records); + qValueFormatter.setDisplayValuesInRecords(table, records); assertEquals("Tim Chamberlain", records.get(0).getRecordLabel()); assertEquals("$3.50", records.get(0).getDisplayValue("price")); assertEquals("1,701", records.get(0).getDisplayValue("quantity")); + assertEquals("1", records.get(0).getDisplayValue("homeStateId")); // PVS NOT translated by this class. assertEquals("Tyler Samples", records.get(1).getRecordLabel()); assertEquals("$174,999.99", records.get(1).getDisplayValue("price")); assertEquals("47", records.get(1).getDisplayValue("quantity")); + assertEquals("2", records.get(1).getDisplayValue("homeStateId")); // PVS NOT translated by this class. } } \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java index 2593b3e2..a81e5b53 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java @@ -31,6 +31,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -281,4 +282,60 @@ class CsvToQRecordAdapterTest // todo - this is what the method header comment means when it says we don't handle all cases well... // Assertions.assertEquals(List.of("A", "B", "C", "C 2", "C 3"), csvToQRecordAdapter.makeHeadersUnique(List.of("A", "B", "C 2", "C", "C 3"))); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testByteOrderMarker() + { + CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter(); + List records = csvToQRecordAdapter.buildRecordsFromCsv(""" + id,firstName + 1,John""", TestUtils.defineTablePerson(), null); + + assertEquals(1, records.get(0).getValueInteger("id")); + assertEquals("John", records.get(0).getValueString("firstName")); + } + + + + /******************************************************************************* + ** Fix an IndexOutOfBounds that we used to throw. + *******************************************************************************/ + @Test + void testTooFewBodyColumns() + { + CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter(); + List records = csvToQRecordAdapter.buildRecordsFromCsv(""" + id,firstName,lastName + 1,John""", TestUtils.defineTablePerson(), null); + + assertEquals(1, records.get(0).getValueInteger("id")); + assertEquals("John", records.get(0).getValueString("firstName")); + assertNull(records.get(0).getValueString("lastName")); + } + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testTooFewColumnsIndexMapping() + { + int index = 1; + QIndexBasedFieldMapping mapping = new QIndexBasedFieldMapping() + .withMapping("id", index++) + .withMapping("firstName", index++) + .withMapping("lastName", index++); + + CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter(); + List records = csvToQRecordAdapter.buildRecordsFromCsv("1,John", TestUtils.defineTablePerson(), mapping); + + assertEquals(1, records.get(0).getValueInteger("id")); + assertEquals("John", records.get(0).getValueString("firstName")); + assertNull(records.get(0).getValueString("lastName")); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java index f9017298..2404c915 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java @@ -22,10 +22,10 @@ package com.kingsrook.qqq.backend.core.instances; +import java.util.ArrayList; import java.util.Collections; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; 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.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.TestUtils; @@ -130,6 +130,20 @@ class QInstanceEnricherTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testNameToLabel() + { + assertEquals("Address 2", QInstanceEnricher.nameToLabel("address2")); + assertEquals("Field 20", QInstanceEnricher.nameToLabel("field20")); + assertEquals("Something USA", QInstanceEnricher.nameToLabel("somethingUSA")); + assertEquals("Number 1 Dad", QInstanceEnricher.nameToLabel("number1Dad")); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -146,4 +160,28 @@ class QInstanceEnricherTest assertEquals("tla_and_another_tla", QInstanceEnricher.inferBackendName("TLAAndAnotherTLA")); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testInferredRecordLabelFormat() + { + QInstance qInstance = TestUtils.defineInstance(); + QTableMetaData table = qInstance.getTable("person").withRecordLabelFormat(null).withRecordLabelFields(new ArrayList<>()); + new QInstanceEnricher().enrich(qInstance); + assertNull(table.getRecordLabelFormat()); + + qInstance = TestUtils.defineInstance(); + table = qInstance.getTable("person").withRecordLabelFormat(null).withRecordLabelFields("firstName"); + new QInstanceEnricher().enrich(qInstance); + assertEquals("%s", table.getRecordLabelFormat()); + + qInstance = TestUtils.defineInstance(); + table = qInstance.getTable("person").withRecordLabelFormat(null).withRecordLabelFields("firstName", "lastName"); + new QInstanceEnricher().enrich(qInstance); + assertEquals("%s %s", table.getRecordLabelFormat()); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java index a5cefc2c..d4c21bd8 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java @@ -22,16 +22,20 @@ package com.kingsrook.qqq.backend.core.instances; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.function.Consumer; import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; @@ -110,7 +114,7 @@ class QInstanceValidatorTest @Test public void test_validateNullTables() { - assertValidationFailureReasons((qInstance) -> + assertValidationFailureReasonsAllowingExtraReasons((qInstance) -> { qInstance.setTables(null); qInstance.setProcesses(null); @@ -127,7 +131,7 @@ class QInstanceValidatorTest @Test public void test_validateEmptyTables() { - assertValidationFailureReasons((qInstance) -> + assertValidationFailureReasonsAllowingExtraReasons((qInstance) -> { qInstance.setTables(new HashMap<>()); qInstance.setProcesses(new HashMap<>()); @@ -150,10 +154,13 @@ class QInstanceValidatorTest qInstance.getTable("person").setName("notPerson"); qInstance.getBackend("default").setName("notDefault"); qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).setName("notGreetPeople"); + qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_STATE).setName("notStates"); }, "Inconsistent naming for table", "Inconsistent naming for backend", - "Inconsistent naming for process"); + "Inconsistent naming for process", + "Inconsistent naming for possibleValueSource" + ); } @@ -184,6 +191,19 @@ class QInstanceValidatorTest + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void test_validateTableBadRecordFormatField() + { + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withRecordLabelFields("notAField"), + "not a field"); + } + + + /******************************************************************************* ** Test that if a process specifies a table that doesn't exist, that it fails. ** @@ -252,7 +272,7 @@ class QInstanceValidatorTest @Test public void test_validateFieldWithMissingPossibleValueSource() { - assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").getField("homeState").setPossibleValueSourceName("not a real possible value source"), + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").getField("homeStateId").setPossibleValueSourceName("not a real possible value source"), "Unrecognized possibleValueSourceName"); } @@ -319,6 +339,7 @@ class QInstanceValidatorTest } + /******************************************************************************* ** *******************************************************************************/ @@ -376,6 +397,7 @@ class QInstanceValidatorTest } + /******************************************************************************* ** *******************************************************************************/ @@ -391,6 +413,7 @@ class QInstanceValidatorTest } + /******************************************************************************* ** *******************************************************************************/ @@ -408,6 +431,96 @@ class QInstanceValidatorTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPossibleValueSourceMissingType() + { + assertValidationFailureReasons((qInstance) -> qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_STATE).setType(null), + "Missing type for possibleValueSource"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPossibleValueSourceMissingIdType() + { + assertValidationFailureReasons((qInstance) -> qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_STATE).setIdType(null), + "Missing an idType for possibleValueSource"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPossibleValueSourceMisConfiguredEnum() + { + assertValidationFailureReasons((qInstance) -> { + QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_STATE); + possibleValueSource.setTableName("person"); + possibleValueSource.setCustomCodeReference(new QCodeReference()); + possibleValueSource.setEnumValues(null); + }, + "should not have a tableName", + "should not have a customCodeReference", + "is missing enum values"); + + assertValidationFailureReasons((qInstance) -> qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_STATE).setEnumValues(new ArrayList<>()), + "is missing enum values"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPossibleValueSourceMisConfiguredTable() + { + assertValidationFailureReasons((qInstance) -> { + QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_SHAPE); + possibleValueSource.setTableName(null); + possibleValueSource.setCustomCodeReference(new QCodeReference()); + possibleValueSource.setEnumValues(List.of(new QPossibleValue<>("test"))); + }, + "should not have enum values", + "should not have a customCodeReference", + "is missing a tableName"); + + assertValidationFailureReasons((qInstance) -> qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_SHAPE).setTableName("Not a table"), + "Unrecognized table"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPossibleValueSourceMisConfiguredCustom() + { + assertValidationFailureReasons((qInstance) -> { + QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_CUSTOM); + possibleValueSource.setTableName("person"); + possibleValueSource.setCustomCodeReference(null); + possibleValueSource.setEnumValues(List.of(new QPossibleValue<>("test"))); + }, + "should not have enum values", + "should not have a tableName", + "is missing a customCodeReference"); + + assertValidationFailureReasons((qInstance) -> qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_CUSTOM).setCustomCodeReference(new QCodeReference()), + "not a possibleValueProvider"); + } + + + /******************************************************************************* ** Run a little setup code on a qInstance; then validate it, and assert that it ** failed validation with reasons that match the supplied vararg-reasons (but allow diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java index ec1b2673..97ab5288 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java @@ -48,6 +48,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -62,8 +63,9 @@ class MemoryBackendModuleTest /******************************************************************************* ** *******************************************************************************/ + @BeforeEach @AfterEach - void afterEach() + void beforeAndAfter() { MemoryRecordStore.getInstance().reset(); } @@ -120,6 +122,8 @@ class MemoryBackendModuleTest assertEquals(3, new CountAction().execute(countInput).getCount()); + // todo - filters in query + ////////////////// // do an update // ////////////////// diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java index aa537948..915498d7 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java @@ -22,10 +22,12 @@ package com.kingsrook.qqq.backend.core.utils; +import java.io.Serializable; import java.util.List; import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.AddAge; import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.GetAgeStatistics; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; @@ -39,6 +41,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; @@ -80,6 +83,10 @@ public class TestUtils public static final String TABLE_NAME_PERSON_FILE = "personFile"; public static final String TABLE_NAME_ID_AND_NAME_ONLY = "idAndNameOnly"; + public static final String POSSIBLE_VALUE_SOURCE_STATE = "state"; // enum-type + public static final String POSSIBLE_VALUE_SOURCE_SHAPE = "shape"; // table-type + public static final String POSSIBLE_VALUE_SOURCE_CUSTOM = "custom"; // custom-type + /******************************************************************************* @@ -99,6 +106,8 @@ public class TestUtils qInstance.addTable(defineTableShape()); qInstance.addPossibleValueSource(defineStatesPossibleValueSource()); + qInstance.addPossibleValueSource(defineShapePossibleValueSource()); + qInstance.addPossibleValueSource(defineCustomPossibleValueSource()); qInstance.addProcess(defineProcessGreetPeople()); qInstance.addProcess(defineProcessGreetPeopleInteractive()); @@ -141,12 +150,40 @@ public class TestUtils ** Define the "states" possible value source used in standard tests ** *******************************************************************************/ - private static QPossibleValueSource defineStatesPossibleValueSource() + private static QPossibleValueSource defineStatesPossibleValueSource() { - return new QPossibleValueSource() - .withName("state") + return new QPossibleValueSource() + .withName(POSSIBLE_VALUE_SOURCE_STATE) .withType(QPossibleValueSourceType.ENUM) - .withEnumValues(List.of("IL", "MO")); + .withEnumValues(List.of(new QPossibleValue<>(1, "IL"), new QPossibleValue<>(2, "MO"))); + } + + + + /******************************************************************************* + ** Define the "shape" possible value source used in standard tests + ** + *******************************************************************************/ + private static QPossibleValueSource defineShapePossibleValueSource() + { + return new QPossibleValueSource() + .withName(POSSIBLE_VALUE_SOURCE_SHAPE) + .withType(QPossibleValueSourceType.TABLE) + .withTableName(TABLE_NAME_SHAPE); + } + + + + /******************************************************************************* + ** Define the "custom" possible value source used in standard tests + ** + *******************************************************************************/ + private static QPossibleValueSource defineCustomPossibleValueSource() + { + return new QPossibleValueSource() + .withName(POSSIBLE_VALUE_SOURCE_CUSTOM) + .withType(QPossibleValueSourceType.CUSTOM) + .withCustomCodeReference(new QCodeReference(CustomPossibleValueSource.class)); } @@ -205,7 +242,10 @@ public class TestUtils .withField(new QFieldMetaData("lastName", QFieldType.STRING)) .withField(new QFieldMetaData("birthDate", QFieldType.DATE)) .withField(new QFieldMetaData("email", QFieldType.STRING)) - .withField(new QFieldMetaData("homeState", QFieldType.STRING).withPossibleValueSourceName("state")); + .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)) + ; } @@ -219,6 +259,7 @@ public class TestUtils .withName(TABLE_NAME_SHAPE) .withBackendName(MEMORY_BACKEND_NAME) .withPrimaryKeyField("id") + .withRecordLabelFields("name") .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false)) .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false)) @@ -452,4 +493,21 @@ public class TestUtils """); } + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class CustomPossibleValueSource implements QCustomPossibleValueProvider + { + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public QPossibleValue getPossibleValue(Serializable idValue) + { + return (new QPossibleValue<>(idValue, "Custom[" + idValue + "]")); + } + } } diff --git a/qqq-backend-core/src/test/resources/personQInstance.json b/qqq-backend-core/src/test/resources/personQInstance.json index 9b1d333b..a0d2f454 100644 --- a/qqq-backend-core/src/test/resources/personQInstance.json +++ b/qqq-backend-core/src/test/resources/personQInstance.json @@ -60,8 +60,8 @@ "type": "STRING", "possibleValueSourceName": null }, - "homeState": { - "name": "homeState", + "homeStateId": { + "name": "homeStateId", "label": null, "backendName": null, "type": "STRING", diff --git a/qqq-backend-core/src/test/resources/personQInstanceIncludingBackend.json b/qqq-backend-core/src/test/resources/personQInstanceIncludingBackend.json index 5502e715..bda0d747 100644 --- a/qqq-backend-core/src/test/resources/personQInstanceIncludingBackend.json +++ b/qqq-backend-core/src/test/resources/personQInstanceIncludingBackend.json @@ -27,8 +27,8 @@ "type": "DATE_TIME", "possibleValueSourceName": null }, - "homeState": { - "name": "homeState", + "homeStateId": { + "name": "homeStateId", "backendName": null, "label": null, "type": "STRING", diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java index 02718222..566a220e 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java @@ -200,6 +200,10 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf { return (QueryManager.getLocalDateTime(resultSet, i)); } + case BOOLEAN: + { + return (QueryManager.getBoolean(resultSet, i)); + } default: { throw new IllegalStateException("Unexpected field type: " + qFieldMetaData.getType()); diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java index 3e4c37ba..d440dcc7 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java @@ -428,12 +428,14 @@ public class RDBMSQueryActionTest extends RDBMSActionTest /******************************************************************************* ** This doesn't really test any RDBMS code, but is a checkpoint that the core - ** module is populating displayValues when it performs the system-level query action. + ** module is populating displayValues when it performs the system-level query action + ** (if so requested by input field). *******************************************************************************/ @Test public void testThatDisplayValuesGetSetGoingThroughQueryAction() throws QException { QueryInput queryInput = initQueryRequest(); + queryInput.setShouldGenerateDisplayValues(true); QueryOutput queryOutput = new QueryAction().execute(queryInput); Assertions.assertEquals(5, queryOutput.getRecords().size(), "Unfiltered query should find all rows"); diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index 3e1fcf77..72e0ee79 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -34,6 +34,7 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.function.Supplier; import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager; import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction; import com.kingsrook.qqq.backend.core.actions.metadata.ProcessMetaDataAction; @@ -109,11 +110,16 @@ public class QJavalinImplementation { private static final Logger LOG = LogManager.getLogger(QJavalinImplementation.class); - private static final int SESSION_COOKIE_AGE = 60 * 60 * 24; + private static final int SESSION_COOKIE_AGE = 60 * 60 * 24; private static final String SESSION_ID_COOKIE_NAME = "sessionId"; static QInstance qInstance; + private static Supplier qInstanceHotSwapSupplier; + private static long lastQInstanceHotSwapMillis; + + private static final long MILLIS_BETWEEN_HOT_SWAPS = 2500; + private static int DEFAULT_PORT = 8001; private static Javalin service; @@ -166,6 +172,44 @@ public class QJavalinImplementation // todo base path from arg? - and then potentially multiple instances too (chosen based on the root path??) service = Javalin.create().start(port); service.routes(getRoutes()); + service.before(QJavalinImplementation::hotSwapQInstance); + } + + + + /******************************************************************************* + ** If there's a qInstanceHotSwapSupplier, and its been a little while, replace + ** the qInstance with a new one from the supplier. Meant to be used while doing + ** development. + *******************************************************************************/ + public static void hotSwapQInstance(Context context) + { + if(qInstanceHotSwapSupplier != null) + { + long now = System.currentTimeMillis(); + if(now - lastQInstanceHotSwapMillis < MILLIS_BETWEEN_HOT_SWAPS) + { + return; + } + + lastQInstanceHotSwapMillis = now; + + try + { + QInstance newQInstance = qInstanceHotSwapSupplier.get(); + new QInstanceValidator().validate(newQInstance); + QJavalinImplementation.qInstance = newQInstance; + LOG.info("Swapped qInstance"); + } + catch(QInstanceValidationException e) + { + LOG.warn(e.getMessage()); + } + catch(Exception e) + { + LOG.error("Error swapping QInstance", e); + } + } } @@ -249,7 +293,7 @@ public class QJavalinImplementation static void setupSession(Context context, AbstractActionInput input) throws QModuleDispatchException { QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher(); - QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(input.getAuthenticationMetaData()); + QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(input.getAuthenticationMetaData()); try { @@ -266,7 +310,7 @@ public class QJavalinImplementation else { String authorizationHeaderValue = context.header("Authorization"); - if (authorizationHeaderValue != null) + if(authorizationHeaderValue != null) { String bearerPrefix = "Bearer "; if(authorizationHeaderValue.startsWith(bearerPrefix)) @@ -309,7 +353,7 @@ public class QJavalinImplementation { try { - String table = context.pathParam("table"); + String table = context.pathParam("table"); List primaryKeys = new ArrayList<>(); primaryKeys.add(context.pathParam("primaryKey")); @@ -338,9 +382,9 @@ public class QJavalinImplementation { try { - String table = context.pathParam("table"); + String table = context.pathParam("table"); List recordList = new ArrayList<>(); - QRecord record = new QRecord(); + QRecord record = new QRecord(); record.setTableName(table); recordList.add(record); @@ -382,9 +426,9 @@ public class QJavalinImplementation { try { - String table = context.pathParam("table"); + String table = context.pathParam("table"); List recordList = new ArrayList<>(); - QRecord record = new QRecord(); + QRecord record = new QRecord(); record.setTableName(table); recordList.add(record); @@ -429,6 +473,8 @@ public class QJavalinImplementation setupSession(context, queryInput); queryInput.setTableName(tableName); + queryInput.setShouldGenerateDisplayValues(true); + queryInput.setShouldTranslatePossibleValues(true); // todo - validate that the primary key is of the proper type (e.g,. not a string for an id field) // and throw a 400-series error (tell the user bad-request), rather than, we're doing a 500 (server error) @@ -524,6 +570,8 @@ public class QJavalinImplementation QueryInput queryInput = new QueryInput(qInstance); setupSession(context, queryInput); queryInput.setTableName(context.pathParam("table")); + queryInput.setShouldGenerateDisplayValues(true); + queryInput.setShouldTranslatePossibleValues(true); queryInput.setSkip(integerQueryParam(context, "skip")); queryInput.setLimit(integerQueryParam(context, "limit")); @@ -836,4 +884,14 @@ public class QJavalinImplementation return (null); } + + + /******************************************************************************* + ** Setter for qInstanceHotSwapSupplier + *******************************************************************************/ + public static void setQInstanceHotSwapSupplier(Supplier qInstanceHotSwapSupplier) + { + QJavalinImplementation.qInstanceHotSwapSupplier = qInstanceHotSwapSupplier; + } + } diff --git a/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java b/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java index e83fe394..a59cada8 100644 --- a/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java +++ b/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java @@ -536,6 +536,11 @@ public class QPicoCliImplementation queryInput.setSession(session); queryInput.setTableName(tableName); queryInput.setSkip(subParseResult.matchedOptionValue("skip", null)); + + // todo - think about these (e.g., based on user's requested output format? + // queryInput.setShouldGenerateDisplayValues(true); + // queryInput.setShouldTranslatePossibleValues(true); + String primaryKeyValue = subParseResult.matchedPositionalValue(0, null); if(primaryKeyValue == null) @@ -581,6 +586,10 @@ public class QPicoCliImplementation queryInput.setLimit(subParseResult.matchedOptionValue("limit", null)); queryInput.setFilter(generateQueryFilter(subParseResult)); + // todo - think about these (e.g., based on user's requested output format? + // queryInput.setShouldGenerateDisplayValues(true); + // queryInput.setShouldTranslatePossibleValues(true); + QueryAction queryAction = new QueryAction(); QueryOutput queryOutput = queryAction.execute(queryInput); commandLine.getOut().println(JsonUtils.toPrettyJson(queryOutput)); diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java index 9be5c4d6..7c0e5c00 100644 --- a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java @@ -253,7 +253,7 @@ public class SampleMetaDataProvider .withBackendName(RDBMS_BACKEND_NAME) .withPrimaryKeyField("id") .withRecordLabelFormat("%s %s") - .withRecordLabelFields(List.of("firstName", "lastName")) + .withRecordLabelFields("firstName", "lastName") .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withBackendName("create_date").withIsEditable(false)) .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withBackendName("modify_date").withIsEditable(false)) @@ -267,7 +267,7 @@ public class SampleMetaDataProvider .withSection(new QFieldSection("identity", "Identity", new QIcon("badge"), Tier.T1, List.of("id", "firstName", "lastName"))) .withSection(new QFieldSection("basicInfo", "Basic Info", new QIcon("dataset"), Tier.T2, List.of("email", "birthDate"))) - .withSection(new QFieldSection("employmentInfo", "Employment Info", new QIcon("work"), Tier.T2, List.of("annualSalary", "daysWorked"))) + .withSection(new QFieldSection("employmentInfo", "Employment Info", new QIcon("work"), Tier.T2, List.of("isEmployed", "annualSalary", "daysWorked"))) .withSection(new QFieldSection("dates", "Dates", new QIcon("calendar_month"), Tier.T3, List.of("createDate", "modifyDate"))); QInstanceEnricher.setInferredFieldBackendNames(qTableMetaData); diff --git a/qqq-sample-project/src/test/resources/prime-test-database.sql b/qqq-sample-project/src/test/resources/prime-test-database.sql index ef295c31..10185156 100644 --- a/qqq-sample-project/src/test/resources/prime-test-database.sql +++ b/qqq-sample-project/src/test/resources/prime-test-database.sql @@ -31,15 +31,16 @@ CREATE TABLE person birth_date DATE, email VARCHAR(250) NOT NULL, + is_employed BOOLEAN, annual_salary DECIMAL(12, 2), days_worked INTEGER ); -INSERT INTO person (id, first_name, last_name, birth_date, email, annual_salary, days_worked) VALUES (1, 'Darin', 'Kelkhoff', '1980-05-31', 'darin.kelkhoff@gmail.com', 75003.50, 1001); -INSERT INTO person (id, first_name, last_name, birth_date, email, annual_salary, days_worked) VALUES (2, 'James', 'Maes', '1980-05-15', 'jmaes@mmltholdings.com', 150000, 10100); -INSERT INTO person (id, first_name, last_name, birth_date, email, annual_salary, days_worked) VALUES (3, 'Tim', 'Chamberlain', '1976-05-28', 'tchamberlain@mmltholdings.com', 300000, 100100); -INSERT INTO person (id, first_name, last_name, birth_date, email, annual_salary, days_worked) VALUES (4, 'Tyler', 'Samples', NULL, 'tsamples@mmltholdings.com', 950000, 75); -INSERT INTO person (id, first_name, last_name, birth_date, email, annual_salary, days_worked) VALUES (5, 'Garret', 'Richardson', '1981-01-01', 'grichardson@mmltholdings.com', 1500000, 1); +INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (1, 'Darin', 'Kelkhoff', '1980-05-31', 'darin.kelkhoff@gmail.com', 1, 75003.50, 1001); +INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (2, 'James', 'Maes', '1980-05-15', 'jmaes@mmltholdings.com', 1, 150000, 10100); +INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (3, 'Tim', 'Chamberlain', '1976-05-28', 'tchamberlain@mmltholdings.com', 1, 300000, 100100); +INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (4, 'Tyler', 'Samples', NULL, 'tsamples@mmltholdings.com', 1, 950000, 75); +INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (5, 'Garret', 'Richardson', '1981-01-01', 'grichardson@mmltholdings.com', 0, 1500000, 1); DROP TABLE IF EXISTS carrier; CREATE TABLE carrier From 6d733018782e1c140127fd53ecf5127af4736b46 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 19 Aug 2022 08:20:34 -0500 Subject: [PATCH 11/19] Add -n to xpath for reporting jacoco coverage --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f1436792..ea1003a4 100644 --- a/pom.xml +++ b/pom.xml @@ -261,7 +261,7 @@ echo "------------------------------------------------------------" which xpath > /dev/null 2>&1 if [ "$?" == "0" ]; then echo "Element\nInstructions Missed\nInstruction Coverage\nBranches Missed\nBranch Coverage\nComplexity Missed\nComplexity Hit\nLines Missed\nLines Hit\nMethods Missed\nMethods Hit\nClasses Missed\nClasses Hit\n" > /tmp/$$.headers - xpath -q -e '/html/body/table/tfoot/tr[1]/td/text()' target/site/jacoco/index.html > /tmp/$$.values + xpath -n -q -e '/html/body/table/tfoot/tr[1]/td/text()' target/site/jacoco/index.html > /tmp/$$.values paste /tmp/$$.headers /tmp/$$.values | tail +2 | awk -v FS='\t' '{printf("%-20s %s\n",$1,$2)}' rm /tmp/$$.headers /tmp/$$.values else From e1efd952af1109d050c72608c5429cc14428196b Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Fri, 19 Aug 2022 09:53:02 -0500 Subject: [PATCH 12/19] test broken build --- .../qqq/backend/core/actions/metadata/MetaDataAction.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java index ce21b93f..da137485 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java @@ -56,7 +56,7 @@ public class MetaDataAction // todo pre-customization - just get to modify the request? MetaDataOutput metaDataOutput = new MetaDataOutput(); - Map treeNodes = new LinkedHashMap<>(); + Map treeNodes = new LinkedHashMap<>()); ///////////////////////////////////// // map tables to frontend metadata // From c7e4fe8d56360c4602acb1677b3a7592ac96fca1 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 19 Aug 2022 10:10:08 -0500 Subject: [PATCH 13/19] Fixed syntax from last commit --- .../qqq/backend/core/actions/metadata/MetaDataAction.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java index da137485..ce21b93f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java @@ -56,7 +56,7 @@ public class MetaDataAction // todo pre-customization - just get to modify the request? MetaDataOutput metaDataOutput = new MetaDataOutput(); - Map treeNodes = new LinkedHashMap<>()); + Map treeNodes = new LinkedHashMap<>(); ///////////////////////////////////// // map tables to frontend metadata // From 99f724e2c26bb1052807acce84c17d9e04ef1877 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 22 Aug 2022 08:27:33 -0500 Subject: [PATCH 14/19] Renamed --- .../customizers/{CustomizerLoader.java => QCodeLoader.java} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/{CustomizerLoader.java => QCodeLoader.java} (95%) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/CustomizerLoader.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/QCodeLoader.java similarity index 95% rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/CustomizerLoader.java rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/QCodeLoader.java index d0ef11d8..8e7c0dfc 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/CustomizerLoader.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/QCodeLoader.java @@ -34,9 +34,9 @@ import org.apache.logging.log4j.Logger; /******************************************************************************* ** Utility to load code for running QQQ customizers. *******************************************************************************/ -public class CustomizerLoader +public class QCodeLoader { - private static final Logger LOG = LogManager.getLogger(CustomizerLoader.class); + private static final Logger LOG = LogManager.getLogger(QCodeLoader.class); @@ -48,7 +48,7 @@ public class CustomizerLoader Optional codeReference = table.getCustomizer(customizerName); if(codeReference.isPresent()) { - return (CustomizerLoader.getFunction(codeReference.get())); + return (QCodeLoader.getFunction(codeReference.get())); } return null; From e4dc0155ef8f26959e396b6965db32f1df8ed818 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 22 Aug 2022 10:20:41 -0500 Subject: [PATCH 15/19] Renamed CustomizerLoader to QCodeLoader --- .../backend/core/model/actions/tables/query/QueryOutput.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutput.java index 96340a69..9412e0d0 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutput.java @@ -25,8 +25,8 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query; import java.io.Serializable; import java.util.List; import java.util.function.Function; -import com.kingsrook.qqq.backend.core.actions.customizers.CustomizerLoader; import com.kingsrook.qqq.backend.core.actions.customizers.Customizers; +import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import org.apache.logging.log4j.LogManager; @@ -62,7 +62,7 @@ public class QueryOutput extends AbstractActionOutput implements Serializable storage = new QueryOutputList(); } - postQueryRecordCustomizer = (Function) CustomizerLoader.getTableCustomizerFunction(queryInput.getTable(), Customizers.POST_QUERY_RECORD); + postQueryRecordCustomizer = (Function) QCodeLoader.getTableCustomizerFunction(queryInput.getTable(), Customizers.POST_QUERY_RECORD); } From ed6d9f4ceee49ff4a3d379f0e0d03983ffb5dbb6 Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Mon, 22 Aug 2022 11:18:19 -0500 Subject: [PATCH 16/19] sprint-9: removed all uses of junit.framework.* classes and replaced with jupiters --- .../java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java | 2 +- .../test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java | 2 +- .../java/com/kingsrook/qqq/frontend/picocli/TestUtils.java | 2 +- .../com/kingsrook/sampleapp/SampleMetaDataProviderTest.java | 3 +-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java index 18b38403..ce9b3ae5 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java @@ -37,7 +37,7 @@ import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSTableBackendDetails; import org.apache.commons.io.IOUtils; -import static junit.framework.Assert.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotNull; /******************************************************************************* diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java index 818c2b53..10b8cc02 100644 --- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java @@ -50,7 +50,7 @@ import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; import org.apache.commons.io.IOUtils; -import static junit.framework.Assert.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotNull; /******************************************************************************* diff --git a/qqq-middleware-picocli/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java b/qqq-middleware-picocli/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java index 434b9d39..fb2aced0 100644 --- a/qqq-middleware-picocli/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java +++ b/qqq-middleware-picocli/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java @@ -44,7 +44,7 @@ import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; import org.apache.commons.io.IOUtils; -import static junit.framework.Assert.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotNull; /******************************************************************************* diff --git a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleMetaDataProviderTest.java b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleMetaDataProviderTest.java index 6192bb38..08b5c6ea 100644 --- a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleMetaDataProviderTest.java +++ b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleMetaDataProviderTest.java @@ -43,7 +43,6 @@ import com.kingsrook.qqq.backend.module.filesystem.local.actions.FilesystemQuery import com.kingsrook.qqq.backend.module.filesystem.local.model.metadata.FilesystemTableBackendDetails; import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; -import junit.framework.Assert; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.Assertions; @@ -84,7 +83,7 @@ class SampleMetaDataProviderTest try(Connection connection = connectionManager.getConnection(SampleMetaDataProvider.defineRdbmsBackend())) { InputStream primeTestDatabaseSqlStream = SampleMetaDataProviderTest.class.getResourceAsStream("/" + sqlFileName); - Assert.assertNotNull(primeTestDatabaseSqlStream); + assertNotNull(primeTestDatabaseSqlStream); List lines = (List) IOUtils.readLines(primeTestDatabaseSqlStream); lines = lines.stream().filter(line -> !line.startsWith("-- ")).toList(); String joinedSQL = String.join("\n", lines); From 3410c76c8129413128578fc6269d1977193e7111 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 22 Aug 2022 17:03:47 -0500 Subject: [PATCH 17/19] Feedback from code reviews --- .../core/actions/customizers/Customizers.java | 32 --- .../core/actions/customizers/QCodeLoader.java | 36 ++- .../actions/customizers/TableCustomizer.java | 61 +++++ .../actions/customizers/TableCustomizers.java | 85 +++++++ .../core/actions/reporting/RecordPipe.java | 40 ++- .../core/actions/tables/QueryAction.java | 61 ++++- .../values/QPossibleValueTranslator.java | 68 +++--- .../core/actions/values/QValueFormatter.java | 24 +- .../core/instances/QInstanceEnricher.java | 16 +- .../core/instances/QInstanceValidator.java | 227 +++++++++++++++++- .../actions/tables/query/QueryOutput.java | 31 --- .../qqq/backend/core/model/data/QRecord.java | 2 +- .../PVSValueFormatAndFields.java | 55 +++++ .../possiblevalues/PossibleValueEnum.java | 1 + .../possiblevalues/QPossibleValueSource.java | 89 +++---- .../model/metadata/tables/QTableMetaData.java | 22 ++ .../memory/MemoryRecordStore.java | 47 ++-- .../qqq/backend/core/utils/ValueUtils.java | 2 +- .../values/QPossibleValueTranslatorTest.java | 150 ++++++++++-- .../adapters/CsvToQRecordAdapterTest.java | 5 + .../core/instances/QInstanceEnricherTest.java | 1 + .../instances/QInstanceValidatorTest.java | 186 ++++++++++++-- .../memory/MemoryBackendModuleTest.java | 79 +++++- .../actions/AbstractBaseFilesystemAction.java | 10 +- .../base/actions/FilesystemCustomizers.java | 35 --- .../actions/FilesystemTableCustomizers.java | 72 ++++++ .../filesystem/sync/FilesystemSyncStep.java | 2 +- .../FilesystemModuleJunitExtension.java | 30 --- .../actions/FilesystemCountActionTest.java | 39 +-- .../actions/FilesystemQueryActionTest.java | 4 +- .../javalin/QJavalinImplementation.java | 6 + .../com/kingsrook/sampleapp/SampleCli.java | 27 +-- .../sampleapp/SampleJavalinServer.java | 19 +- .../kingsrook/sampleapp/SampleCliTest.java | 16 +- 34 files changed, 1190 insertions(+), 390 deletions(-) delete mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/Customizers.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/TableCustomizer.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/TableCustomizers.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/PVSValueFormatAndFields.java delete mode 100644 qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/FilesystemCustomizers.java create mode 100644 qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/FilesystemTableCustomizers.java delete mode 100644 qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/FilesystemModuleJunitExtension.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/Customizers.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/Customizers.java deleted file mode 100644 index 4da39af3..00000000 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/Customizers.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2022. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.kingsrook.qqq.backend.core.actions.customizers; - - -/******************************************************************************* - ** Standard place where the names of QQQ Customization points are defined. - *******************************************************************************/ -public interface Customizers -{ - String POST_QUERY_RECORD = "postQueryRecord"; - -} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/QCodeLoader.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/QCodeLoader.java index 8e7c0dfc..f1774928 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/QCodeLoader.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/QCodeLoader.java @@ -24,8 +24,11 @@ package com.kingsrook.qqq.backend.core.actions.customizers; import java.util.Optional; import java.util.function.Function; +import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider; +import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -43,15 +46,14 @@ public class QCodeLoader /******************************************************************************* ** *******************************************************************************/ - public static Function getTableCustomizerFunction(QTableMetaData table, String customizerName) + public static Optional> getTableCustomizerFunction(QTableMetaData table, String customizerName) { Optional codeReference = table.getCustomizer(customizerName); if(codeReference.isPresent()) { - return (QCodeLoader.getFunction(codeReference.get())); + return (Optional.ofNullable(QCodeLoader.getFunction(codeReference.get()))); } - - return null; + return (Optional.empty()); } @@ -93,4 +95,30 @@ public class QCodeLoader } } + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QCustomPossibleValueProvider getCustomPossibleValueProvider(QPossibleValueSource possibleValueSource) throws QException + { + try + { + Class codeClass = Class.forName(possibleValueSource.getCustomCodeReference().getName()); + Object codeObject = codeClass.getConstructor().newInstance(); + if(!(codeObject instanceof QCustomPossibleValueProvider customPossibleValueProvider)) + { + throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of QCustomPossibleValueProvider")); + } + return (customPossibleValueProvider); + } + catch(QException qe) + { + throw (qe); + } + catch(Exception e) + { + throw (new QException("Error getting custom possible value provider for PVS [" + possibleValueSource.getName() + "]", e)); + } + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/TableCustomizer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/TableCustomizer.java new file mode 100644 index 00000000..9367a0c1 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/TableCustomizer.java @@ -0,0 +1,61 @@ +package com.kingsrook.qqq.backend.core.actions.customizers; + + +import java.util.function.Consumer; + + +/******************************************************************************* + ** Object used by TableCustomizers enum (and similar enums in backend modules) + ** to assist with definition and validation of Customizers applied to tables. + *******************************************************************************/ +public class TableCustomizer +{ + private final String role; + private final Class expectedType; + private final Consumer validationFunction; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public TableCustomizer(String role, Class expectedType, Consumer validationFunction) + { + this.role = role; + this.expectedType = expectedType; + this.validationFunction = validationFunction; + } + + + + /******************************************************************************* + ** Getter for role + ** + *******************************************************************************/ + public String getRole() + { + return role; + } + + + + /******************************************************************************* + ** Getter for expectedType + ** + *******************************************************************************/ + public Class getExpectedType() + { + return expectedType; + } + + + + /******************************************************************************* + ** Getter for validationFunction + ** + *******************************************************************************/ + public Consumer getValidationFunction() + { + return validationFunction; + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/TableCustomizers.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/TableCustomizers.java new file mode 100644 index 00000000..e5899fad --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/TableCustomizers.java @@ -0,0 +1,85 @@ +package com.kingsrook.qqq.backend.core.actions.customizers; + + +import java.util.function.Function; +import com.kingsrook.qqq.backend.core.model.data.QRecord; + + +/******************************************************************************* + ** Enum definition of possible table customizers - "roles" for custom code that + ** can be applied to tables. + ** + ** Works with TableCustomizer (singular version of this name) objects, during + ** instance validation, to provide validation of the referenced code (and to + ** make such validation from sub-backend-modules possible in the future). + ** + ** The idea of the 3rd argument here is to provide a way that we can enforce + ** the type-parameters for the custom code. E.g., if it's a Function - how + ** can we check at run-time that the type-params are correct? We couldn't find + ** how to do this "reflectively", so we can instead try to run the custom code, + ** passing it objects of the type that this customizer expects, and a validation + ** error will raise upon ClassCastException... This maybe could improve! + *******************************************************************************/ +public enum TableCustomizers +{ + POST_QUERY_RECORD(new TableCustomizer("postQueryRecord", Function.class, ((Object x) -> + { + Function function = (Function) x; + QRecord output = function.apply(new QRecord()); + }))); + + + private final TableCustomizer tableCustomizer; + + + + /******************************************************************************* + ** + *******************************************************************************/ + TableCustomizers(TableCustomizer tableCustomizer) + { + this.tableCustomizer = tableCustomizer; + } + + + + /******************************************************************************* + ** Get the TableCustomer for a given role (e.g., the role used in meta-data, not + ** the enum-constant name). + *******************************************************************************/ + public static TableCustomizers forRole(String name) + { + for(TableCustomizers value : values()) + { + if(value.tableCustomizer.getRole().equals(name)) + { + return (value); + } + } + + return (null); + } + + + + /******************************************************************************* + ** Getter for tableCustomizer + ** + *******************************************************************************/ + public TableCustomizer getTableCustomizer() + { + return tableCustomizer; + } + + + + /******************************************************************************* + ** get the role from the tableCustomizer + ** + *******************************************************************************/ + public String getRole() + { + return (tableCustomizer.getRole()); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java index 3703a841..c41e7f08 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.utils.SleepUtils; import org.apache.logging.log4j.LogManager; @@ -42,12 +43,31 @@ public class RecordPipe private ArrayBlockingQueue queue = new ArrayBlockingQueue<>(1_000); + private Consumer> postRecordActions = null; + + /******************************************************************************* ** Add a record to the pipe ** Returns true iff the record fit in the pipe; false if the pipe is currently full. *******************************************************************************/ public void addRecord(QRecord record) + { + if(postRecordActions != null) + { + postRecordActions.accept(List.of(record)); + } + + doAddRecord(record); + } + + + + /******************************************************************************* + ** Private internal version of add record - assumes the postRecordActions have + ** already ran. + *******************************************************************************/ + private void doAddRecord(QRecord record) { boolean offerResult = queue.offer(record); @@ -66,7 +86,15 @@ public class RecordPipe *******************************************************************************/ public void addRecords(List records) { - records.forEach(this::addRecord); + if(postRecordActions != null) + { + postRecordActions.accept(records); + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // make sure to go to the private version of doAddRecord - to avoid re-running the post-actions // + ////////////////////////////////////////////////////////////////////////////////////////////////// + records.forEach(this::doAddRecord); } @@ -101,4 +129,14 @@ public class RecordPipe return (queue.size()); } + + + /******************************************************************************* + ** + *******************************************************************************/ + public void setPostRecordActions(Consumer> postRecordActions) + { + this.postRecordActions = postRecordActions; + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java index 83ec1dd1..d492dd97 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java @@ -22,12 +22,18 @@ package com.kingsrook.qqq.backend.core.actions.tables; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; import com.kingsrook.qqq.backend.core.actions.ActionHelper; +import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; +import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator; import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; @@ -38,6 +44,14 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; *******************************************************************************/ public class QueryAction { + private Optional> postQueryRecordCustomizer; + + private QueryInput queryInput; + private QValueFormatter qValueFormatter; + private QPossibleValueTranslator qPossibleValueTranslator; + + + /******************************************************************************* ** *******************************************************************************/ @@ -45,6 +59,14 @@ public class QueryAction { ActionHelper.validateSession(queryInput); + postQueryRecordCustomizer = QCodeLoader.getTableCustomizerFunction(queryInput.getTable(), TableCustomizers.POST_QUERY_RECORD.getRole()); + this.queryInput = queryInput; + + if(queryInput.getRecordPipe() != null) + { + queryInput.getRecordPipe().setPostRecordActions(this::postRecordActions); + } + QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(queryInput.getBackend()); // todo pre-customization - just get to modify the request? @@ -53,20 +75,37 @@ public class QueryAction if(queryInput.getRecordPipe() == null) { - if(queryInput.getShouldGenerateDisplayValues()) - { - QValueFormatter qValueFormatter = new QValueFormatter(); - qValueFormatter.setDisplayValuesInRecords(queryInput.getTable(), queryOutput.getRecords()); - } - - if(queryInput.getShouldTranslatePossibleValues()) - { - QPossibleValueTranslator qPossibleValueTranslator = new QPossibleValueTranslator(queryInput.getInstance(), queryInput.getSession()); - qPossibleValueTranslator.translatePossibleValuesInRecords(queryInput.getTable(), queryOutput.getRecords()); - } + postRecordActions(queryOutput.getRecords()); } return queryOutput; } + + + /******************************************************************************* + ** + *******************************************************************************/ + public void postRecordActions(List records) + { + this.postQueryRecordCustomizer.ifPresent(qRecordQRecordFunction -> records.replaceAll(qRecordQRecordFunction::apply)); + + if(queryInput.getShouldGenerateDisplayValues()) + { + if(qValueFormatter == null) + { + qValueFormatter = new QValueFormatter(); + } + qValueFormatter.setDisplayValuesInRecords(queryInput.getTable(), records); + } + + if(queryInput.getShouldTranslatePossibleValues()) + { + if(qPossibleValueTranslator == null) + { + qPossibleValueTranslator = new QPossibleValueTranslator(queryInput.getInstance(), queryInput.getSession()); + } + qPossibleValueTranslator.translatePossibleValuesInRecords(queryInput.getTable(), records); + } + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java index 603951ee..5c50cf2c 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java @@ -31,8 +31,8 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; -import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; @@ -63,8 +63,10 @@ public class QPossibleValueTranslator private final QInstance qInstance; private final QSession session; - // top-level keys are pvsNames (not table names) - // 2nd-level keys are pkey values from the PVS table + /////////////////////////////////////////////////////// + // top-level keys are pvsNames (not table names) // + // 2nd-level keys are pkey values from the PVS table // + /////////////////////////////////////////////////////// private Map> possibleValueCache; @@ -120,9 +122,6 @@ public class QPossibleValueTranslator return (null); } - // todo - memoize!!! - // todo - bulk!!! - String resultValue = null; if(possibleValueSource.getType().equals(QPossibleValueSourceType.ENUM)) { @@ -154,22 +153,14 @@ public class QPossibleValueTranslator /******************************************************************************* ** *******************************************************************************/ - private String translatePossibleValueCustom(QFieldMetaData field, Serializable value, QPossibleValueSource possibleValueSource) + private String translatePossibleValueEnum(Serializable value, QPossibleValueSource possibleValueSource) { - try + for(QPossibleValue possibleValue : possibleValueSource.getEnumValues()) { - Class codeClass = Class.forName(possibleValueSource.getCustomCodeReference().getName()); - Object codeObject = codeClass.getConstructor().newInstance(); - if(!(codeObject instanceof QCustomPossibleValueProvider customPossibleValueProvider)) + if(possibleValue.getId().equals(value)) { - throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of QCustomPossibleValueProvider")); + return (formatPossibleValue(possibleValueSource, possibleValue)); } - - return (formatPossibleValue(possibleValueSource, customPossibleValueProvider.getPossibleValue(value))); - } - catch(Exception e) - { - LOG.warn("Error sending [" + value + "] for field [" + field + "] through custom code for PVS [" + field.getPossibleValueSourceName() + "]", e); } return (null); @@ -205,6 +196,26 @@ public class QPossibleValueTranslator + /******************************************************************************* + ** + *******************************************************************************/ + private String translatePossibleValueCustom(QFieldMetaData field, Serializable value, QPossibleValueSource possibleValueSource) + { + try + { + QCustomPossibleValueProvider customPossibleValueProvider = QCodeLoader.getCustomPossibleValueProvider(possibleValueSource); + return (formatPossibleValue(possibleValueSource, customPossibleValueProvider.getPossibleValue(value))); + } + catch(Exception e) + { + LOG.warn("Error sending [" + value + "] for field [" + field + "] through custom code for PVS [" + field.getPossibleValueSourceName() + "]", e); + } + + return (null); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -256,26 +267,11 @@ public class QPossibleValueTranslator - /******************************************************************************* - ** - *******************************************************************************/ - private String translatePossibleValueEnum(Serializable value, QPossibleValueSource possibleValueSource) - { - for(QPossibleValue possibleValue : possibleValueSource.getEnumValues()) - { - if(possibleValue.getId().equals(value)) - { - return (formatPossibleValue(possibleValueSource, possibleValue)); - } - } - - return (null); - } - - - /******************************************************************************* ** prime the cache (e.g., by doing bulk-queries) for table-based PVS's + ** + ** @param table the table that the records are from + ** @param records the records that have the possible value id's (e.g., foreign keys) *******************************************************************************/ void primePvsCache(QTableMetaData table, List records) { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java index 27ccde27..5ac811a9 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java @@ -25,10 +25,8 @@ package com.kingsrook.qqq.backend.core.actions.values; import java.io.Serializable; import java.util.List; import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; -import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; import org.apache.logging.log4j.LogManager; @@ -37,7 +35,7 @@ import org.apache.logging.log4j.Logger; /******************************************************************************* ** Utility to apply display formats to values for records and fields. - ** Note that this includes handling PossibleValues. + ** *******************************************************************************/ public class QValueFormatter { @@ -45,15 +43,6 @@ public class QValueFormatter - /******************************************************************************* - ** - *******************************************************************************/ - public QValueFormatter() - { - } - - - /******************************************************************************* ** *******************************************************************************/ @@ -67,16 +56,6 @@ public class QValueFormatter return (null); } - // todo - is this appropriate, with this class and possibleValueTransaltor being decoupled - to still do standard formatting here? - // alternatively, shold we return null here? - // /////////////////////////////////////////////// - // // if the field has a possible value, use it // - // /////////////////////////////////////////////// - // if(field.getPossibleValueSourceName() != null) - // { - // return (this.possibleValueTranslator.translatePossibleValue(field, value)); - // } - //////////////////////////////////////////////////////// // if the field has a display format, try to apply it // //////////////////////////////////////////////////////// @@ -198,5 +177,4 @@ public class QValueFormatter } } - } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java index 119daa34..19beae30 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java @@ -229,7 +229,21 @@ public class QInstanceEnricher return (name.substring(0, 1).toUpperCase(Locale.ROOT)); } - return (name.substring(0, 1).toUpperCase(Locale.ROOT) + name.substring(1).replaceAll("([A-Z0-9]+)", " $1").replaceAll("([0-9])([A-Za-z])", "$1 $2")); + String suffix = name.substring(1) + + ////////////////////////////////////////////////////////////////////// + // Put a space before capital letters or numbers embedded in a name // + // e.g., omethingElse -> omething Else; umber1 -> umber 1 // + ////////////////////////////////////////////////////////////////////// + .replaceAll("([A-Z0-9]+)", " $1") + + //////////////////////////////////////////////////////////////// + // put a space between numbers and words that come after them // + // e.g., umber1dad -> number 1 dad // + //////////////////////////////////////////////////////////////// + .replaceAll("([0-9])([A-Za-z])", "$1 $2"); + + return (name.substring(0, 1).toUpperCase(Locale.ROOT) + suffix); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java index 66959a31..4b7c4c2b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java @@ -22,13 +22,20 @@ package com.kingsrook.qqq.backend.core.instances; +import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.Consumer; +import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; +import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider; import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; @@ -38,6 +45,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; /******************************************************************************* @@ -52,6 +61,11 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils; *******************************************************************************/ public class QInstanceValidator { + private static final Logger LOG = LogManager.getLogger(QInstanceValidator.class); + + private boolean printWarnings = false; + + /******************************************************************************* ** @@ -202,12 +216,130 @@ public class QInstanceValidator assertCondition(errors, table.getFields().containsKey(recordLabelField), "Table " + tableName + " record label field " + recordLabelField + " is not a field on this table."); } } + + if(table.getCustomizers() != null) + { + for(Map.Entry entry : table.getCustomizers().entrySet()) + { + validateTableCustomizer(errors, tableName, entry.getKey(), entry.getValue()); + } + } }); } } + /******************************************************************************* + ** + *******************************************************************************/ + private void validateTableCustomizer(List errors, String tableName, String customizerName, QCodeReference codeReference) + { + String prefix = "Table " + tableName + ", customizer " + customizerName + ": "; + + if(!preAssertionsForCodeReference(errors, codeReference, prefix)) + { + return; + } + + ////////////////////////////////////////////////////////////////////////////// + // make sure (at this time) that it's a java type, then do some java checks // + ////////////////////////////////////////////////////////////////////////////// + if(assertCondition(errors, codeReference.getCodeType().equals(QCodeType.JAVA), prefix + "Only JAVA customizers are supported at this time.")) + { + /////////////////////////////////////// + // make sure the class can be loaded // + /////////////////////////////////////// + Class customizerClass = getClassForCodeReference(errors, codeReference, prefix); + if(customizerClass != null) + { + ////////////////////////////////////////////////// + // make sure the customizer can be instantiated // + ////////////////////////////////////////////////// + Object customizerInstance = getInstanceOfCodeReference(errors, prefix, customizerClass); + + TableCustomizers tableCustomizer = TableCustomizers.forRole(customizerName); + if(tableCustomizer == null) + { + //////////////////////////////////////////////////////////////////////////////////////////////////// + // todo - in the future, load customizers from backend-modules (e.g., FilesystemTableCustomizers) // + //////////////////////////////////////////////////////////////////////////////////////////////////// + warn(prefix + "Unrecognized table customizer name (at least at backend-core level)"); + } + else + { + //////////////////////////////////////////////////////////////////////// + // make sure the customizer instance can be cast to the expected type // + //////////////////////////////////////////////////////////////////////// + if(customizerInstance != null && tableCustomizer.getTableCustomizer().getExpectedType() != null) + { + Object castedObject = getCastedObject(errors, prefix, tableCustomizer.getTableCustomizer().getExpectedType(), customizerInstance); + + Consumer validationFunction = tableCustomizer.getTableCustomizer().getValidationFunction(); + if(castedObject != null && validationFunction != null) + { + try + { + validationFunction.accept(castedObject); + } + catch(ClassCastException e) + { + errors.add(prefix + "Error validating customizer type parameters: " + e.getMessage()); + } + catch(Exception e) + { + /////////////////////////////////////////////////////////////////////////////////////////////////////////// + // mmm, calling customizers w/ random data is expected to often throw, so, this check is iffy at best... // + // if we run into more trouble here, we might consider disabling the whole "validation function" check. // + /////////////////////////////////////////////////////////////////////////////////////////////////////////// + } + } + } + } + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private T getCastedObject(List errors, String prefix, Class expectedType, Object customizerInstance) + { + T castedObject = null; + try + { + castedObject = expectedType.cast(customizerInstance); + } + catch(ClassCastException e) + { + errors.add(prefix + "CodeReference could not be casted to the expected type: " + expectedType); + } + return castedObject; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private Object getInstanceOfCodeReference(List errors, String prefix, Class customizerClass) + { + Object customizerInstance = null; + try + { + customizerInstance = customizerClass.getConstructor().newInstance(); + } + catch(InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e) + { + errors.add(prefix + "Instance of CodeReference could not be created: " + e); + } + return customizerInstance; + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -313,7 +445,6 @@ public class QInstanceValidator qInstance.getPossibleValueSources().forEach((pvsName, possibleValueSource) -> { assertCondition(errors, Objects.equals(pvsName, possibleValueSource.getName()), "Inconsistent naming for possibleValueSource: " + pvsName + "/" + possibleValueSource.getName() + "."); - assertCondition(errors, possibleValueSource.getIdType() != null, "Missing an idType for possibleValueSource: " + pvsName); if(assertCondition(errors, possibleValueSource.getType() != null, "Missing type for possibleValueSource: " + pvsName)) { //////////////////////////////////////////////////////////////////////////////////////////////// @@ -347,6 +478,7 @@ public class QInstanceValidator if(assertCondition(errors, possibleValueSource.getCustomCodeReference() != null, "custom-type possibleValueSource " + pvsName + " is missing a customCodeReference.")) { assertCondition(errors, QCodeUsage.POSSIBLE_VALUE_PROVIDER.equals(possibleValueSource.getCustomCodeReference().getCodeUsage()), "customCodeReference for possibleValueSource " + pvsName + " is not a possibleValueProvider."); + validateCustomPossibleValueSourceCode(errors, pvsName, possibleValueSource.getCustomCodeReference()); } } default -> errors.add("Unexpected possibleValueSource type: " + possibleValueSource.getType()); @@ -358,6 +490,87 @@ public class QInstanceValidator + /******************************************************************************* + ** + *******************************************************************************/ + private void validateCustomPossibleValueSourceCode(List errors, String pvsName, QCodeReference codeReference) + { + String prefix = "PossibleValueSource " + pvsName + " custom code reference: "; + + if(!preAssertionsForCodeReference(errors, codeReference, prefix)) + { + return; + } + + ////////////////////////////////////////////////////////////////////////////// + // make sure (at this time) that it's a java type, then do some java checks // + ////////////////////////////////////////////////////////////////////////////// + if(assertCondition(errors, codeReference.getCodeType().equals(QCodeType.JAVA), prefix + "Only JAVA customizers are supported at this time.")) + { + /////////////////////////////////////// + // make sure the class can be loaded // + /////////////////////////////////////// + Class customizerClass = getClassForCodeReference(errors, codeReference, prefix); + if(customizerClass != null) + { + ////////////////////////////////////////////////// + // make sure the customizer can be instantiated // + ////////////////////////////////////////////////// + Object customizerInstance = getInstanceOfCodeReference(errors, prefix, customizerClass); + + //////////////////////////////////////////////////////////////////////// + // make sure the customizer instance can be cast to the expected type // + //////////////////////////////////////////////////////////////////////// + if(customizerInstance != null) + { + getCastedObject(errors, prefix, QCustomPossibleValueProvider.class, customizerInstance); + } + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private Class getClassForCodeReference(List errors, QCodeReference codeReference, String prefix) + { + Class customizerClass = null; + try + { + customizerClass = Class.forName(codeReference.getName()); + } + catch(ClassNotFoundException e) + { + errors.add(prefix + "Class for CodeReference could not be found."); + } + return customizerClass; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private boolean preAssertionsForCodeReference(List errors, QCodeReference codeReference, String prefix) + { + boolean okay = true; + if(!assertCondition(errors, StringUtils.hasContent(codeReference.getName()), prefix + " is missing a code reference name")) + { + okay = false; + } + + if(!assertCondition(errors, codeReference.getCodeType() != null, prefix + " is missing a code type")) + { + okay = false; + } + + return (okay); + } + + + /******************************************************************************* ** Check if an app's child list can recursively be traversed without finding a ** duplicate, which would indicate a cycle (e.g., an error) @@ -410,4 +623,16 @@ public class QInstanceValidator return (condition); } + + + /******************************************************************************* + ** + *******************************************************************************/ + private void warn(String message) + { + if(printWarnings) + { + LOG.info("Validation warning: " + message); + } + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutput.java index 9412e0d0..a9e19342 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutput.java @@ -24,13 +24,8 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query; import java.io.Serializable; import java.util.List; -import java.util.function.Function; -import com.kingsrook.qqq.backend.core.actions.customizers.Customizers; -import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; /******************************************************************************* @@ -39,12 +34,8 @@ import org.apache.logging.log4j.Logger; *******************************************************************************/ public class QueryOutput extends AbstractActionOutput implements Serializable { - private static final Logger LOG = LogManager.getLogger(QueryOutput.class); - private QueryOutputStorageInterface storage; - private Function postQueryRecordCustomizer; - /******************************************************************************* @@ -61,8 +52,6 @@ public class QueryOutput extends AbstractActionOutput implements Serializable { storage = new QueryOutputList(); } - - postQueryRecordCustomizer = (Function) QCodeLoader.getTableCustomizerFunction(queryInput.getTable(), Customizers.POST_QUERY_RECORD); } @@ -76,36 +65,16 @@ public class QueryOutput extends AbstractActionOutput implements Serializable *******************************************************************************/ public void addRecord(QRecord record) { - record = runPostQueryRecordCustomizer(record); storage.addRecord(record); } - /******************************************************************************* - ** - *******************************************************************************/ - public QRecord runPostQueryRecordCustomizer(QRecord record) - { - if(this.postQueryRecordCustomizer != null) - { - record = this.postQueryRecordCustomizer.apply(record); - } - return record; - } - - - /******************************************************************************* ** add a list of records to this output *******************************************************************************/ public void addRecords(List records) { - if(this.postQueryRecordCustomizer != null) - { - records.replaceAll(t -> this.postQueryRecordCustomizer.apply(t)); - } - storage.addRecords(records); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java index bf51c6fb..77cdd6e8 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java @@ -334,7 +334,7 @@ public class QRecord implements Serializable *******************************************************************************/ public LocalTime getValueLocalTime(String fieldName) { - return ((LocalTime) ValueUtils.getValueAsLocalTime(values.get(fieldName))); + return (ValueUtils.getValueAsLocalTime(values.get(fieldName))); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/PVSValueFormatAndFields.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/PVSValueFormatAndFields.java new file mode 100644 index 00000000..f8ea0eb9 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/PVSValueFormatAndFields.java @@ -0,0 +1,55 @@ +package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues; + + +import java.util.List; + + +/******************************************************************************* + ** Define some standard ways to format the value portion of a PossibleValueSource. + ** + ** Can be passed to short-cut {set,with}ValueFormatAndFields methods in QPossibleValueSource + ** class, or the format & field properties can be extracted and passed to regular field-level setters. + *******************************************************************************/ +public enum PVSValueFormatAndFields +{ + LABEL_ONLY("%s", "label"), + LABEL_PARENS_ID("%s (%s)", "label", "id"), + ID_COLON_LABEL("%s: %s", "id", "label"); + + + private final String format; + private final List fields; + + + + /******************************************************************************* + ** + *******************************************************************************/ + PVSValueFormatAndFields(String format, String... fields) + { + this.format = format; + this.fields = List.of(fields); + } + + + + /******************************************************************************* + ** Getter for format + ** + *******************************************************************************/ + public String getFormat() + { + return format; + } + + + + /******************************************************************************* + ** Getter for fields + ** + *******************************************************************************/ + public List getFields() + { + return fields; + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/PossibleValueEnum.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/PossibleValueEnum.java index 24018b3a..fc61b2df 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/PossibleValueEnum.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/PossibleValueEnum.java @@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues; /******************************************************************************* + ** Interface to be implemented by enums which can be used as a PossibleValueSource. ** *******************************************************************************/ public interface PossibleValueEnum diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java index 735a22e4..4b15b193 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java @@ -25,44 +25,24 @@ package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues; import java.util.ArrayList; import java.util.List; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; /******************************************************************************* - ** Meta-data to represent a single field in a table. + ** Meta-data to represent a "Possible value" - e.g., a translation of a foreign + ** key and/or a limited set of "possible values" for a field (e.g., from a foreign + ** table or an enum). ** *******************************************************************************/ public class QPossibleValueSource { private String name; private QPossibleValueSourceType type; - private QFieldType idType = QFieldType.INTEGER; - private String valueFormat = ValueFormat.DEFAULT; - private List valueFields = ValueFields.DEFAULT; + private String valueFormat = PVSValueFormatAndFields.LABEL_ONLY.getFormat(); + private List valueFields = PVSValueFormatAndFields.LABEL_ONLY.getFields(); private String valueFormatIfNotFound = null; private List valueFieldsIfNotFound = null; - - - public interface ValueFormat - { - String DEFAULT = "%s"; - String LABEL_ONLY = "%s"; - String LABEL_PARENS_ID = "%s (%s)"; - String ID_COLON_LABEL = "%s: %s"; - } - - - - public interface ValueFields - { - List DEFAULT = List.of("label"); - List LABEL_ONLY = List.of("label"); - List LABEL_PARENS_ID = List.of("label", "id"); - List ID_COLON_LABEL = List.of("id", "label"); - } - // todo - optimization hints, such as "table is static, fully cache" or "table is small, so we can pull the whole thing into memory?" ////////////////////// @@ -154,40 +134,6 @@ public class QPossibleValueSource - /******************************************************************************* - ** Getter for idType - ** - *******************************************************************************/ - public QFieldType getIdType() - { - return idType; - } - - - - /******************************************************************************* - ** Setter for idType - ** - *******************************************************************************/ - public void setIdType(QFieldType idType) - { - this.idType = idType; - } - - - - /******************************************************************************* - ** Fluent setter for idType - ** - *******************************************************************************/ - public QPossibleValueSource withIdType(QFieldType idType) - { - this.idType = idType; - return (this); - } - - - /******************************************************************************* ** Getter for valueFormat ** @@ -407,6 +353,9 @@ public class QPossibleValueSource /******************************************************************************* + ** This is the easiest way to add the values from an enum to a PossibleValueSource. + ** Make sure the enum implements PossibleValueEnum - then call as: + ** myPossibleValueSource.withValuesFromEnum(MyEnum.values())); ** *******************************************************************************/ public > QPossibleValueSource withValuesFromEnum(T[] values) @@ -453,4 +402,26 @@ public class QPossibleValueSource return (this); } + + + /******************************************************************************* + ** + *******************************************************************************/ + public void setValueFormatAndFields(PVSValueFormatAndFields valueFormatAndFields) + { + this.valueFormat = valueFormatAndFields.getFormat(); + this.valueFields = valueFormatAndFields.getFields(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QPossibleValueSource withValueFormatAndFields(PVSValueFormatAndFields valueFormatAndFields) + { + setValueFormatAndFields(valueFormatAndFields); + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java index ebb3e36a..0d1cbcef 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java @@ -30,6 +30,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizer; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; import com.kingsrook.qqq.backend.core.model.data.QRecordEntityField; @@ -85,6 +86,17 @@ public class QTableMetaData implements QAppChildMetaData, Serializable + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public String toString() + { + return ("QTableMetaData[" + name + "]"); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -451,6 +463,16 @@ public class QTableMetaData implements QAppChildMetaData, Serializable + /******************************************************************************* + ** + *******************************************************************************/ + public QTableMetaData withCustomizer(TableCustomizer tableCustomizer, QCodeReference customizer) + { + return (withCustomizer(tableCustomizer.getRole(), customizer)); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java index f195edcf..90eedd28 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java @@ -174,10 +174,13 @@ public class MemoryRecordStore *******************************************************************************/ public Integer count(CountInput input) { - Map tableData = getTableData(input.getTable()); - List records = new ArrayList<>(tableData.values()); - // todo - filtering (call query) - return (records.size()); + QueryInput queryInput = new QueryInput(input.getInstance()); + queryInput.setSession(input.getSession()); + queryInput.setTableName(input.getTableName()); + queryInput.setFilter(input.getFilter()); + List queryResult = query(queryInput); + + return (queryResult.size()); } @@ -192,27 +195,43 @@ public class MemoryRecordStore return (new ArrayList<>()); } - QTableMetaData table = input.getTable(); - Map tableData = getTableData(table); - Integer nextSerial = nextSerials.get(table.getName()); + QTableMetaData table = input.getTable(); + Map tableData = getTableData(table); + + //////////////////////////////////////// + // grab the next unique serial to use // + //////////////////////////////////////// + Integer nextSerial = nextSerials.get(table.getName()); if(nextSerial == null) { nextSerial = 1; - while(tableData.containsKey(nextSerial)) - { - nextSerial++; - } + } + + while(tableData.containsKey(nextSerial)) + { + nextSerial++; } List outputRecords = new ArrayList<>(); QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField()); for(QRecord record : input.getRecords()) { + ///////////////////////////////////////////////// + // set the next serial in the record if needed // + ///////////////////////////////////////////////// if(record.getValue(primaryKeyField.getName()) == null && primaryKeyField.getType().equals(QFieldType.INTEGER)) { record.setValue(primaryKeyField.getName(), nextSerial++); } + /////////////////////////////////////////////////////////////////////////////////////////////////// + // make sure that if the user supplied a serial, greater than the one we had, that we skip ahead // + /////////////////////////////////////////////////////////////////////////////////////////////////// + if(primaryKeyField.getType().equals(QFieldType.INTEGER) && record.getValueInteger(primaryKeyField.getName()) > nextSerial) + { + nextSerial = record.getValueInteger(primaryKeyField.getName()) + 1; + } + tableData.put(record.getValue(primaryKeyField.getName()), record); if(returnInsertedRecords) { @@ -220,6 +239,8 @@ public class MemoryRecordStore } } + nextSerials.put(table.getName(), nextSerial); + return (outputRecords); } @@ -256,10 +277,6 @@ public class MemoryRecordStore outputRecords.add(record); } } - else - { - outputRecords.add(record); - } } return (outputRecords); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java index 9850719b..4d422746 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java @@ -423,7 +423,7 @@ public class ValueUtils /******************************************************************************* ** *******************************************************************************/ - public static Object getValueAsLocalTime(Serializable value) + public static LocalTime getValueAsLocalTime(Serializable value) { try { diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslatorTest.java index 2d4dbbf6..75d03a38 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslatorTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslatorTest.java @@ -30,25 +30,39 @@ import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueFormatAndFields; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore; import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; /******************************************************************************* - ** + ** Unit test for QPossibleValueTranslator *******************************************************************************/ public class QPossibleValueTranslatorTest { + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + void beforeEach() + { + MemoryRecordStore.getInstance().reset(); + MemoryRecordStore.resetStatistics(); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -90,22 +104,20 @@ public class QPossibleValueTranslatorTest ///////////////////////////////////////////////////////////////// // assert the LABEL_ONLY format (when called out specifically) // ///////////////////////////////////////////////////////////////// - possibleValueSource.setValueFormat(QPossibleValueSource.ValueFormat.LABEL_ONLY); - possibleValueSource.setValueFields(QPossibleValueSource.ValueFields.LABEL_ONLY); + possibleValueSource.setValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY); assertEquals("IL", possibleValueTranslator.translatePossibleValue(stateField, 1)); /////////////////////////////////////// // assert the LABEL_PARAMS_ID format // /////////////////////////////////////// - possibleValueSource.setValueFormat(QPossibleValueSource.ValueFormat.LABEL_PARENS_ID); - possibleValueSource.setValueFields(QPossibleValueSource.ValueFields.LABEL_PARENS_ID); + possibleValueSource.setValueFormatAndFields(PVSValueFormatAndFields.LABEL_PARENS_ID); assertEquals("IL (1)", possibleValueTranslator.translatePossibleValue(stateField, 1)); ////////////////////////////////////// // assert the ID_COLON_LABEL format // ////////////////////////////////////// - possibleValueSource.setValueFormat(QPossibleValueSource.ValueFormat.ID_COLON_LABEL); - possibleValueSource.setValueFields(QPossibleValueSource.ValueFields.ID_COLON_LABEL); + possibleValueSource.setValueFormat(PVSValueFormatAndFields.ID_COLON_LABEL.getFormat()); + possibleValueSource.setValueFields(PVSValueFormatAndFields.ID_COLON_LABEL.getFields()); assertEquals("1: IL", possibleValueTranslator.translatePossibleValue(stateField, 1)); } @@ -156,8 +168,7 @@ public class QPossibleValueTranslatorTest /////////////////////////////////////// // assert the LABEL_PARAMS_ID format // /////////////////////////////////////// - possibleValueSource.setValueFormat(QPossibleValueSource.ValueFormat.LABEL_PARENS_ID); - possibleValueSource.setValueFields(QPossibleValueSource.ValueFields.LABEL_PARENS_ID); + possibleValueSource.setValueFormatAndFields(PVSValueFormatAndFields.LABEL_PARENS_ID); assertEquals("Circle (3)", possibleValueTranslator.translatePossibleValue(shapeField, 3)); /////////////////////////////////////////////////////////// @@ -195,19 +206,124 @@ public class QPossibleValueTranslatorTest + /******************************************************************************* + ** Make sure that if we have 2 different PVS's pointed at the same 1 table, + ** that we avoid re-doing queries, and that we actually get different (formatted) values. + *******************************************************************************/ + @Test + void testPossibleValueTableMultiplePvsForATable() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QTableMetaData shapeTable = qInstance.getTable(TestUtils.TABLE_NAME_SHAPE); + QTableMetaData personTable = qInstance.getTable(TestUtils.TABLE_NAME_PERSON); + + //////////////////////////////////////////////////////////////////// + // define a second version of the Shape PVS, with a unique format // + //////////////////////////////////////////////////////////////////// + qInstance.addPossibleValueSource(new QPossibleValueSource() + .withName("shapeV2") + .withType(QPossibleValueSourceType.TABLE) + .withTableName(TestUtils.TABLE_NAME_SHAPE) + .withValueFormat("%d: %s") + .withValueFields(List.of("id", "label")) + ); + + ////////////////////////////////////////////////////// + // use that PVS in a new column on the person table // + ////////////////////////////////////////////////////// + personTable.addField(new QFieldMetaData("currentShapeId", QFieldType.INTEGER) + .withPossibleValueSourceName("shapeV2") + ); + + /////////////////////////////// + // insert the list of shapes // + /////////////////////////////// + List shapeRecords = List.of( + new QRecord().withTableName(shapeTable.getName()).withValue("id", 1).withValue("name", "Triangle"), + new QRecord().withTableName(shapeTable.getName()).withValue("id", 2).withValue("name", "Square"), + new QRecord().withTableName(shapeTable.getName()).withValue("id", 3).withValue("name", "Circle")); + InsertInput insertInput = new InsertInput(qInstance); + insertInput.setSession(new QSession()); + insertInput.setTableName(shapeTable.getName()); + insertInput.setRecords(shapeRecords); + new InsertAction().execute(insertInput); + + /////////////////////////////////////////////////////// + // define a list of persons pointing at those shapes // + /////////////////////////////////////////////////////// + List personRecords = List.of( + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 1).withValue("currentShapeId", 2), + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 1).withValue("currentShapeId", 3), + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 2).withValue("currentShapeId", 3), + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 2).withValue("currentShapeId", 3) + ); + + ///////////////////////// + // translate the PVS's // + ///////////////////////// + MemoryRecordStore.setCollectStatistics(true); + new QPossibleValueTranslator(qInstance, new QSession()).translatePossibleValuesInRecords(personTable, personRecords); + + ///////////////////////////////// + // assert only 1 query was ran // + ///////////////////////////////// + assertEquals(1, MemoryRecordStore.getStatistics().get(MemoryRecordStore.STAT_QUERIES_RAN), "Should only run 1 query"); + + //////////////////////////////////////// + // assert expected values and formats // + //////////////////////////////////////// + assertEquals("Triangle", personRecords.get(0).getDisplayValue("favoriteShapeId")); + assertEquals("2: Square", personRecords.get(0).getDisplayValue("currentShapeId")); + assertEquals("Triangle", personRecords.get(1).getDisplayValue("favoriteShapeId")); + assertEquals("3: Circle", personRecords.get(1).getDisplayValue("currentShapeId")); + assertEquals("Square", personRecords.get(2).getDisplayValue("favoriteShapeId")); + assertEquals("3: Circle", personRecords.get(2).getDisplayValue("currentShapeId")); + } + + + + /******************************************************************************* + ** Make sure that if we have 2 different PVS's pointed at the same 1 table, + ** that we avoid re-doing queries, and that we actually get different (formatted) values. + *******************************************************************************/ + @Test + void testCustomPossibleValue() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QTableMetaData personTable = qInstance.getTable(TestUtils.TABLE_NAME_PERSON); + String fieldName = "customValue"; + + ////////////////////////////////////////////////////////////// + // define a list of persons with values in the custom field // + ////////////////////////////////////////////////////////////// + List personRecords = List.of( + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue(fieldName, 1), + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue(fieldName, 2), + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue(fieldName, "Buckle my shoe") + ); + + ///////////////////////// + // translate the PVS's // + ///////////////////////// + new QPossibleValueTranslator(qInstance, new QSession()).translatePossibleValuesInRecords(personTable, personRecords); + + //////////////////////////////////////// + // assert expected values and formats // + //////////////////////////////////////// + assertEquals("Custom[1]", personRecords.get(0).getDisplayValue(fieldName)); + assertEquals("Custom[2]", personRecords.get(1).getDisplayValue(fieldName)); + assertEquals("Custom[Buckle my shoe]", personRecords.get(2).getDisplayValue(fieldName)); + } + + + /******************************************************************************* ** *******************************************************************************/ @Test void testSetDisplayValuesInRecords() { - QTableMetaData table = new QTableMetaData() - .withRecordLabelFormat("%s %s") - .withRecordLabelFields("firstName", "lastName") - .withField(new QFieldMetaData("firstName", QFieldType.STRING)) - .withField(new QFieldMetaData("lastName", QFieldType.STRING)) - .withField(new QFieldMetaData("price", QFieldType.DECIMAL).withDisplayFormat(DisplayFormat.CURRENCY)) - .withField(new QFieldMetaData("homeStateId", QFieldType.INTEGER).withPossibleValueSourceName(TestUtils.POSSIBLE_VALUE_SOURCE_STATE)); + QTableMetaData table = TestUtils.defineTablePerson(); ///////////////////////////////////////////////////////////////// // first, make sure it doesn't crash with null or empty inputs // diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java index a81e5b53..7dff6bad 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java @@ -292,6 +292,11 @@ class CsvToQRecordAdapterTest void testByteOrderMarker() { CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter(); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////// + // note - there's a zero-width non-breaking-space character (0xFEFF or some-such) // + // at the start of this string!! You may not be able to see it, depending on where you view this file. // + ////////////////////////////////////////////////////////////////////////////////////////////////////////// List records = csvToQRecordAdapter.buildRecordsFromCsv(""" id,firstName 1,John""", TestUtils.defineTablePerson(), null); diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java index 2404c915..c86e9fca 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java @@ -140,6 +140,7 @@ class QInstanceEnricherTest assertEquals("Field 20", QInstanceEnricher.nameToLabel("field20")); assertEquals("Something USA", QInstanceEnricher.nameToLabel("somethingUSA")); assertEquals("Number 1 Dad", QInstanceEnricher.nameToLabel("number1Dad")); + assertEquals("Number 417 Dad", QInstanceEnricher.nameToLabel("number417Dad")); } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java index d4c21bd8..ee2464a9 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java @@ -27,9 +27,14 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.function.Consumer; +import java.util.function.Function; +import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; +import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; @@ -114,12 +119,13 @@ class QInstanceValidatorTest @Test public void test_validateNullTables() { - assertValidationFailureReasonsAllowingExtraReasons((qInstance) -> + assertValidationFailureReasons((qInstance) -> { qInstance.setTables(null); qInstance.setProcesses(null); }, - "At least 1 table must be defined"); + "At least 1 table must be defined", + "Unrecognized table shape for possibleValueSource shape"); } @@ -131,12 +137,13 @@ class QInstanceValidatorTest @Test public void test_validateEmptyTables() { - assertValidationFailureReasonsAllowingExtraReasons((qInstance) -> + assertValidationFailureReasons((qInstance) -> { qInstance.setTables(new HashMap<>()); qInstance.setProcesses(new HashMap<>()); }, - "At least 1 table must be defined"); + "At least 1 table must be defined", + "Unrecognized table shape for possibleValueSource shape"); } @@ -191,7 +198,6 @@ class QInstanceValidatorTest - /******************************************************************************* ** *******************************************************************************/ @@ -265,6 +271,138 @@ class QInstanceValidatorTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTableCustomizers() + { + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference()), + "missing a code reference name", "missing a code type"); + + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(null, QCodeType.JAVA, null)), + "missing a code reference name"); + + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference("", QCodeType.JAVA, null)), + "missing a code reference name"); + + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference("Test", null, null)), + "missing a code type"); + + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference("Test", QCodeType.JAVA, QCodeUsage.CUSTOMIZER)), + "Class for CodeReference could not be found"); + + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerWithNoVoidConstructor.class, QCodeUsage.CUSTOMIZER)), + "Instance of CodeReference could not be created"); + + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerThatIsNotAFunction.class, QCodeUsage.CUSTOMIZER)), + "CodeReference could not be casted"); + + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerFunctionWithIncorrectTypeParameters.class, QCodeUsage.CUSTOMIZER)), + "Error validating customizer type parameters"); + + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerFunctionWithIncorrectTypeParameter1.class, QCodeUsage.CUSTOMIZER)), + "Error validating customizer type parameters"); + + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerFunctionWithIncorrectTypeParameter2.class, QCodeUsage.CUSTOMIZER)), + "Error validating customizer type parameters"); + + assertValidationSuccess((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerValid.class, QCodeUsage.CUSTOMIZER))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class CustomizerWithNoVoidConstructor + { + public CustomizerWithNoVoidConstructor(boolean b) + { + + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class CustomizerWithOnlyPrivateConstructor + { + private CustomizerWithOnlyPrivateConstructor() + { + + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class CustomizerThatIsNotAFunction + { + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class CustomizerFunctionWithIncorrectTypeParameters implements Function + { + @Override + public String apply(String s) + { + return null; + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class CustomizerFunctionWithIncorrectTypeParameter1 implements Function + { + @Override + public QRecord apply(String s) + { + return null; + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class CustomizerFunctionWithIncorrectTypeParameter2 implements Function + { + @Override + public String apply(QRecord s) + { + return "Test"; + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class CustomizerValid implements Function + { + @Override + public QRecord apply(QRecord record) + { + return null; + } + } + + + /******************************************************************************* ** Test that if a field specifies a backend that doesn't exist, that it fails. ** @@ -443,18 +581,6 @@ class QInstanceValidatorTest - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void testPossibleValueSourceMissingIdType() - { - assertValidationFailureReasons((qInstance) -> qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_STATE).setIdType(null), - "Missing an idType for possibleValueSource"); - } - - - /******************************************************************************* ** *******************************************************************************/ @@ -516,7 +642,9 @@ class QInstanceValidatorTest "is missing a customCodeReference"); assertValidationFailureReasons((qInstance) -> qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_CUSTOM).setCustomCodeReference(new QCodeReference()), - "not a possibleValueProvider"); + "not a possibleValueProvider", + "missing a code reference name", + "missing a code type"); } @@ -561,7 +689,8 @@ class QInstanceValidatorTest { if(!allowExtraReasons) { - assertEquals(reasons.length, e.getReasons().size(), "Expected number of validation failure reasons\nExpected: " + String.join(",", reasons) + "\nActual: " + e.getReasons()); + int noOfReasons = e.getReasons() == null ? 0 : e.getReasons().size(); + assertEquals(reasons.length, noOfReasons, "Expected number of validation failure reasons.\nExpected reasons: " + String.join(",", reasons) + "\nActual reasons: " + e.getReasons()); } for(String reason : reasons) @@ -573,6 +702,25 @@ class QInstanceValidatorTest + /******************************************************************************* + ** Assert that an instance is valid! + *******************************************************************************/ + private void assertValidationSuccess(Consumer setup) + { + try + { + QInstance qInstance = TestUtils.defineInstance(); + setup.accept(qInstance); + new QInstanceValidator().validate(qInstance); + } + catch(QInstanceValidationException e) + { + fail("Expected no validation errors, but received: " + e.getMessage()); + } + } + + + /******************************************************************************* ** utility method for asserting that a specific reason string is found within ** the list of reasons in the QInstanceValidationException. diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java index 97ab5288..9bc5081f 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java @@ -24,7 +24,7 @@ package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory; import java.util.List; import java.util.function.Function; -import com.kingsrook.qqq.backend.core.actions.customizers.Customizers; +import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; import com.kingsrook.qqq.backend.core.actions.tables.CountAction; import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; @@ -36,6 +36,9 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; @@ -68,6 +71,7 @@ class MemoryBackendModuleTest void beforeAndAfter() { MemoryRecordStore.getInstance().reset(); + MemoryRecordStore.resetStatistics(); } @@ -122,8 +126,6 @@ class MemoryBackendModuleTest assertEquals(3, new CountAction().execute(countInput).getCount()); - // todo - filters in query - ////////////////// // do an update // ////////////////// @@ -152,6 +154,24 @@ class MemoryBackendModuleTest assertEquals(3, new CountAction().execute(countInput).getCount()); + ///////////////////////// + // do a filtered query // + ///////////////////////// + queryInput = new QueryInput(qInstance); + queryInput.setSession(session); + queryInput.setTableName(table.getName()); + queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria("id", QCriteriaOperator.IN, List.of(1, 3)))); + queryOutput = new QueryAction().execute(queryInput); + assertEquals(2, queryOutput.getRecords().size()); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(1))); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(3))); + + ///////////////////////// + // do a filtered count // + ///////////////////////// + countInput.setFilter(queryInput.getFilter()); + assertEquals(2, new CountAction().execute(countInput).getCount()); + ///////////////// // do a delete // ///////////////// @@ -173,6 +193,57 @@ class MemoryBackendModuleTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSerials() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_SHAPE); + QSession session = new QSession(); + + ////////////////// + // do an insert // + ////////////////// + InsertInput insertInput = new InsertInput(qInstance); + insertInput.setSession(session); + insertInput.setTableName(table.getName()); + insertInput.setRecords(List.of(new QRecord().withTableName(table.getName()).withValue("name", "Shape 1"))); + new InsertAction().execute(insertInput); + + insertInput.setRecords(List.of(new QRecord().withTableName(table.getName()).withValue("name", "Shape 2"))); + new InsertAction().execute(insertInput); + + insertInput.setRecords(List.of(new QRecord().withTableName(table.getName()).withValue("name", "Shape 3"))); + new InsertAction().execute(insertInput); + + QueryInput queryInput = new QueryInput(qInstance); + queryInput.setSession(new QSession()); + queryInput.setTableName(table.getName()); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(1))); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(2))); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(3))); + + insertInput.setRecords(List.of(new QRecord().withTableName(table.getName()).withValue("id", 4).withValue("name", "Shape 4"))); + new InsertAction().execute(insertInput); + queryOutput = new QueryAction().execute(queryInput); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(4))); + + insertInput.setRecords(List.of(new QRecord().withTableName(table.getName()).withValue("id", 6).withValue("name", "Shape 6"))); + new InsertAction().execute(insertInput); + queryOutput = new QueryAction().execute(queryInput); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(6))); + + insertInput.setRecords(List.of(new QRecord().withTableName(table.getName()).withValue("name", "Shape 7"))); + new InsertAction().execute(insertInput); + queryOutput = new QueryAction().execute(queryInput); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(7))); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -215,7 +286,7 @@ class MemoryBackendModuleTest /////////////////////////////////// // add a customizer to the table // /////////////////////////////////// - table.withCustomizer(Customizers.POST_QUERY_RECORD, new QCodeReference(ShapeTestCustomizer.class, QCodeUsage.CUSTOMIZER)); + table.withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(ShapeTestCustomizer.class, QCodeUsage.CUSTOMIZER)); ////////////////// // do an insert // diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/AbstractBaseFilesystemAction.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/AbstractBaseFilesystemAction.java index 61730733..3da02b62 100644 --- a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/AbstractBaseFilesystemAction.java +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/AbstractBaseFilesystemAction.java @@ -206,12 +206,10 @@ public abstract class AbstractBaseFilesystemAction { new CsvToQRecordAdapter().buildRecordsFromCsv(queryInput.getRecordPipe(), fileContents, table, null, (record -> { - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // since the CSV adapter is the one responsible for putting records into the pipe (rather than the queryOutput), // - // we must do some of QueryOutput's normal job here - and run the runPostQueryRecordCustomizer // - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////////// + // Before the records go into the pipe, make sure their backend details are added to them // + //////////////////////////////////////////////////////////////////////////////////////////// addBackendDetailsToRecord(record, file); - queryOutput.runPostQueryRecordCustomizer(record); })); } else @@ -308,7 +306,7 @@ public abstract class AbstractBaseFilesystemAction *******************************************************************************/ private String customizeFileContentsAfterReading(QTableMetaData table, String fileContents) throws QException { - Optional optionalCustomizer = table.getCustomizer(FilesystemCustomizers.POST_READ_FILE); + Optional optionalCustomizer = table.getCustomizer(FilesystemTableCustomizers.POST_READ_FILE.getRole()); if(optionalCustomizer.isEmpty()) { return (fileContents); diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/FilesystemCustomizers.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/FilesystemCustomizers.java deleted file mode 100644 index 8e2416e0..00000000 --- a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/FilesystemCustomizers.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2022. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.kingsrook.qqq.backend.module.filesystem.base.actions; - - -import com.kingsrook.qqq.backend.core.actions.customizers.Customizers; - - -/******************************************************************************* - ** Standard place where the names of QQQ Customization points for filesystem-based - ** backends are defined. - *******************************************************************************/ -public interface FilesystemCustomizers extends Customizers -{ - String POST_READ_FILE = "postReadFile"; -} diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/FilesystemTableCustomizers.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/FilesystemTableCustomizers.java new file mode 100644 index 00000000..06157ceb --- /dev/null +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/FilesystemTableCustomizers.java @@ -0,0 +1,72 @@ +package com.kingsrook.qqq.backend.module.filesystem.base.actions; + + +import java.util.function.Function; +import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizer; + + +/******************************************************************************* + ** + *******************************************************************************/ +public enum FilesystemTableCustomizers +{ + POST_READ_FILE(new TableCustomizer("postReadFile", Function.class, ((Object x) -> + { + Function function = (Function) x; + String output = function.apply(new String()); + }))); + + private final TableCustomizer tableCustomizer; + + + + /******************************************************************************* + ** + *******************************************************************************/ + FilesystemTableCustomizers(TableCustomizer tableCustomizer) + { + this.tableCustomizer = tableCustomizer; + } + + + + /******************************************************************************* + ** Get the FilesystemTableCustomer for a given role (e.g., the role used in meta-data, not + ** the enum-constant name). + *******************************************************************************/ + public static FilesystemTableCustomizers forRole(String name) + { + for(FilesystemTableCustomizers value : values()) + { + if(value.tableCustomizer.getRole().equals(name)) + { + return (value); + } + } + + return (null); + } + + + + /******************************************************************************* + ** Getter for tableCustomizer + ** + *******************************************************************************/ + public TableCustomizer getTableCustomizer() + { + return tableCustomizer; + } + + + + /******************************************************************************* + ** get the role from the tableCustomizer + ** + *******************************************************************************/ + public String getRole() + { + return (tableCustomizer.getRole()); + } + +} diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/processes/implementations/filesystem/sync/FilesystemSyncStep.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/processes/implementations/filesystem/sync/FilesystemSyncStep.java index 8464bb22..1ea36990 100644 --- a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/processes/implementations/filesystem/sync/FilesystemSyncStep.java +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/processes/implementations/filesystem/sync/FilesystemSyncStep.java @@ -91,7 +91,7 @@ public class FilesystemSyncStep implements BackendStep String sourceFileName = sourceEntry.getKey(); if(!archiveFiles.contains(sourceFileName)) { - LOG.info("Syncing file [" + sourceFileName + "] to [" + archiveTable.getName() + "] and [" + processingTable.getName() + "]"); + LOG.info("Syncing file [" + sourceFileName + "] to [" + archiveTable + "] and [" + processingTable + "]"); InputStream inputStream = sourceActionBase.readFile(sourceEntry.getValue()); byte[] bytes = inputStream.readAllBytes(); diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/FilesystemModuleJunitExtension.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/FilesystemModuleJunitExtension.java deleted file mode 100644 index e9bea180..00000000 --- a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/FilesystemModuleJunitExtension.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2022. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.kingsrook.qqq.backend.module.filesystem; - - -/******************************************************************************* - ** - *******************************************************************************/ -public class FilesystemModuleJunitExtension // implements Extension, BeforeAllCallback, AfterAllCallback -{ -} diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemCountActionTest.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemCountActionTest.java index 2c488900..46cfabe2 100644 --- a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemCountActionTest.java +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemCountActionTest.java @@ -22,22 +22,16 @@ package com.kingsrook.qqq.backend.module.filesystem.local.actions; -import java.util.function.Function; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; -import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; -import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; -import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.module.filesystem.TestUtils; -import com.kingsrook.qqq.backend.module.filesystem.base.actions.FilesystemCustomizers; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; /******************************************************************************* - ** + ** Unit test for FilesystemCountAction *******************************************************************************/ public class FilesystemCountActionTest extends FilesystemActionTest { @@ -55,35 +49,4 @@ public class FilesystemCountActionTest extends FilesystemActionTest Assertions.assertEquals(3, countOutput.getCount(), "Unfiltered count should find all rows"); } - - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - public void testCountWithFileCustomizer() throws QException - { - CountInput countInput = new CountInput(); - QInstance instance = TestUtils.defineInstance(); - - QTableMetaData table = instance.getTable(TestUtils.TABLE_NAME_PERSON_LOCAL_FS_JSON); - table.withCustomizer(FilesystemCustomizers.POST_READ_FILE, new QCodeReference(ValueUpshifter.class, QCodeUsage.CUSTOMIZER)); - - countInput.setInstance(instance); - countInput.setTableName(TestUtils.defineLocalFilesystemJSONPersonTable().getName()); - CountOutput countOutput = new FilesystemCountAction().execute(countInput); - Assertions.assertEquals(3, countOutput.getCount(), "Unfiltered count should find all rows"); - } - - - - public static class ValueUpshifter implements Function - { - @Override - public String apply(String s) - { - return (s.replaceAll("kingsrook.com", "KINGSROOK.COM")); - } - } - } \ No newline at end of file diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemQueryActionTest.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemQueryActionTest.java index be40e86a..f6646085 100644 --- a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemQueryActionTest.java +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemQueryActionTest.java @@ -32,7 +32,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.module.filesystem.TestUtils; import com.kingsrook.qqq.backend.module.filesystem.base.FilesystemRecordBackendDetailFields; -import com.kingsrook.qqq.backend.module.filesystem.base.actions.FilesystemCustomizers; +import com.kingsrook.qqq.backend.module.filesystem.base.actions.FilesystemTableCustomizers; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -71,7 +71,7 @@ public class FilesystemQueryActionTest extends FilesystemActionTest QInstance instance = TestUtils.defineInstance(); QTableMetaData table = instance.getTable(TestUtils.TABLE_NAME_PERSON_LOCAL_FS_JSON); - table.withCustomizer(FilesystemCustomizers.POST_READ_FILE, new QCodeReference(ValueUpshifter.class, QCodeUsage.CUSTOMIZER)); + table.withCustomizer(FilesystemTableCustomizers.POST_READ_FILE.getRole(), new QCodeReference(ValueUpshifter.class, QCodeUsage.CUSTOMIZER)); queryInput.setInstance(instance); queryInput.setTableName(TestUtils.defineLocalFilesystemJSONPersonTable().getName()); diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index 72e0ee79..85b72727 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -197,6 +197,12 @@ public class QJavalinImplementation try { QInstance newQInstance = qInstanceHotSwapSupplier.get(); + if(newQInstance == null) + { + LOG.warn("Got a null qInstance from hotSwapSupplier. Not hot-swapping."); + return; + } + new QInstanceValidator().validate(newQInstance); QJavalinImplementation.qInstance = newQInstance; LOG.info("Swapped qInstance"); diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleCli.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleCli.java index d524c6cd..78884a30 100644 --- a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleCli.java +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleCli.java @@ -22,7 +22,6 @@ package com.kingsrook.sampleapp; -import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.frontend.picocli.QPicoCliImplementation; @@ -37,7 +36,8 @@ public class SampleCli *******************************************************************************/ public static void main(String[] args) { - new SampleCli().run(args); + int exitCode = new SampleCli().run(args); + System.exit(exitCode); } @@ -45,31 +45,20 @@ public class SampleCli /******************************************************************************* ** *******************************************************************************/ - private void run(String[] args) + int run(String[] args) { try { - int exitCode = runForExitCode(args); - System.exit(exitCode); + QInstance qInstance = SampleMetaDataProvider.defineInstance(); + QPicoCliImplementation qPicoCliImplementation = new QPicoCliImplementation(qInstance); + + return (qPicoCliImplementation.runCli("my-sample-cli", args)); } catch(Exception e) { e.printStackTrace(); - System.exit(-1); + return (-1); } } - - - /******************************************************************************* - ** - *******************************************************************************/ - int runForExitCode(String[] args) throws QException - { - QInstance qInstance = SampleMetaDataProvider.defineInstance(); - QPicoCliImplementation qPicoCliImplementation = new QPicoCliImplementation(qInstance); - int exitCode = qPicoCliImplementation.runCli("my-sample-cli", args); - return exitCode; - } - } diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java index aa5b049b..e88ec6c5 100644 --- a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java @@ -22,7 +22,6 @@ package com.kingsrook.sampleapp; -import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.javalin.QJavalinImplementation; import io.javalin.Javalin; @@ -71,6 +70,24 @@ public class SampleJavalinServer config.enableCorsForAllOrigins(); }).start(PORT); javalinService.routes(qJavalinImplementation.getRoutes()); + + ///////////////////////////////////////////////////////////////// + // set the server to hot-swap the q instance before all routes // + ///////////////////////////////////////////////////////////////// + QJavalinImplementation.setQInstanceHotSwapSupplier(() -> + { + try + { + return (SampleMetaDataProvider.defineInstance()); + } + catch(Exception e) + { + LOG.warn("Error hot-swapping meta data", e); + return (null); + } + }); + javalinService.before(QJavalinImplementation::hotSwapQInstance); + javalinService.after(ctx -> ctx.res.setHeader("Access-Control-Allow-Origin", "http://localhost:3000")); } diff --git a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleCliTest.java b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleCliTest.java index ab4546cc..be51bead 100644 --- a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleCliTest.java +++ b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleCliTest.java @@ -25,6 +25,7 @@ package com.kingsrook.sampleapp; import com.kingsrook.qqq.backend.core.exceptions.QException; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; /******************************************************************************* @@ -37,10 +38,21 @@ class SampleCliTest ** *******************************************************************************/ @Test - void test() throws QException + void testExitSuccess() throws QException { - int exitCode = new SampleCli().runForExitCode(new String[] { "--meta-data" }); + int exitCode = new SampleCli().run(new String[] { "--meta-data" }); assertEquals(0, exitCode); } + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testNotExitSuccess() throws QException + { + int exitCode = new SampleCli().run(new String[] { "asdfasdf" }); + assertNotEquals(0, exitCode); + } + } \ No newline at end of file From b9d498b57e464fad72e06dc423f0fbee7d4a5be0 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 23 Aug 2022 09:57:06 -0500 Subject: [PATCH 18/19] Fix to pass mutable list into postRecordActions --- .../core/actions/reporting/RecordPipe.java | 17 ++++++++++++++--- .../core/actions/tables/QueryAction.java | 9 +++++++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java index c41e7f08..2ed70d09 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java @@ -45,17 +45,28 @@ public class RecordPipe private Consumer> postRecordActions = null; + ///////////////////////////////////// + // See usage below for explanation // + ///////////////////////////////////// + private List singleRecordListForPostRecordActions = new ArrayList<>(); /******************************************************************************* - ** Add a record to the pipe - ** Returns true iff the record fit in the pipe; false if the pipe is currently full. + ** Add a record to the pipe. Will block if the pipe is full. *******************************************************************************/ public void addRecord(QRecord record) { if(postRecordActions != null) { - postRecordActions.accept(List.of(record)); + //////////////////////////////////////////////////////////////////////////////////// + // the initial use-case of this method is to call QueryAction.postRecordActions // + // that method requires that the list param be modifiable. Originally we used // + // List.of here - but that is immutable, so, instead use this single-record-list // + // (which we'll create as a field in this class, to avoid always re-constructing) // + //////////////////////////////////////////////////////////////////////////////////// + singleRecordListForPostRecordActions.add(record); + postRecordActions.accept(singleRecordListForPostRecordActions); + record = singleRecordListForPostRecordActions.remove(0); } doAddRecord(record); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java index d492dd97..55eb17c9 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java @@ -84,11 +84,16 @@ public class QueryAction /******************************************************************************* - ** + ** Run the necessary actions on a list of records (which must be a mutable list - e.g., + ** not one created via List.of()). This may include setting display values, + ** translating possible values, and running post-record customizations. *******************************************************************************/ public void postRecordActions(List records) { - this.postQueryRecordCustomizer.ifPresent(qRecordQRecordFunction -> records.replaceAll(qRecordQRecordFunction::apply)); + if(this.postQueryRecordCustomizer.isPresent()) + { + records.replaceAll(t -> postQueryRecordCustomizer.get().apply(t)); + } if(queryInput.getShouldGenerateDisplayValues()) { From ffec68b3ef35c45c81a693ec2d3e05dac0e5e18b Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 23 Aug 2022 11:15:43 -0500 Subject: [PATCH 19/19] Improving query test coverage --- .../core/actions/tables/QueryActionTest.java | 48 +++++++++++++++++++ .../values/QPossibleValueTranslatorTest.java | 26 +--------- .../qqq/backend/core/utils/TestUtils.java | 21 ++++++++ 3 files changed, 71 insertions(+), 24 deletions(-) diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java index bdb3b21b..a936e6b4 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java @@ -22,6 +22,8 @@ package com.kingsrook.qqq.backend.core.actions.tables; +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; @@ -76,4 +78,50 @@ class QueryActionTest assertThat(record.getDisplayValues()).isNotEmpty(); } } + + + + /******************************************************************************* + ** Test running with a recordPipe - using the shape table, which uses the memory + ** backend, which is known to do an addAll to the query output. + ** + *******************************************************************************/ + @Test + public void testRecordPipeShapeTable() throws QException + { + TestUtils.insertDefaultShapes(TestUtils.defineInstance()); + + RecordPipe pipe = new RecordPipe(); + QueryInput queryInput = new QueryInput(TestUtils.defineInstance()); + queryInput.setSession(TestUtils.getMockSession()); + queryInput.setTableName(TestUtils.TABLE_NAME_SHAPE); + queryInput.setRecordPipe(pipe); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertNotNull(queryOutput); + + List records = pipe.consumeAvailableRecords(); + assertThat(records).isNotEmpty(); + } + + + /******************************************************************************* + ** Test running with a recordPipe - using the person table, which uses the mock + ** backend, which is known to do a single-add (not addAll) to the query output. + ** + *******************************************************************************/ + @Test + public void testRecordPipePersonTable() throws QException + { + RecordPipe pipe = new RecordPipe(); + QueryInput queryInput = new QueryInput(TestUtils.defineInstance()); + queryInput.setSession(TestUtils.getMockSession()); + queryInput.setTableName(TestUtils.TABLE_NAME_PERSON); + queryInput.setRecordPipe(pipe); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertNotNull(queryOutput); + + List records = pipe.consumeAvailableRecords(); + assertThat(records).isNotEmpty(); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslatorTest.java index 75d03a38..fd79e473 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslatorTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslatorTest.java @@ -25,9 +25,7 @@ package com.kingsrook.qqq.backend.core.actions.values; import java.math.BigDecimal; import java.util.Collections; import java.util.List; -import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; @@ -135,16 +133,7 @@ public class QPossibleValueTranslatorTest QFieldMetaData shapeField = qInstance.getTable(TestUtils.TABLE_NAME_PERSON).getField("favoriteShapeId"); QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(shapeField.getPossibleValueSourceName()); - List shapeRecords = List.of( - new QRecord().withTableName(shapeTable.getName()).withValue("id", 1).withValue("name", "Triangle"), - new QRecord().withTableName(shapeTable.getName()).withValue("id", 2).withValue("name", "Square"), - new QRecord().withTableName(shapeTable.getName()).withValue("id", 3).withValue("name", "Circle")); - - InsertInput insertInput = new InsertInput(qInstance); - insertInput.setSession(new QSession()); - insertInput.setTableName(shapeTable.getName()); - insertInput.setRecords(shapeRecords); - new InsertAction().execute(insertInput); + TestUtils.insertDefaultShapes(qInstance); ////////////////////////////////////////////////////////////////////////// // assert the default formatting for a not-found value is a null string // @@ -235,18 +224,7 @@ public class QPossibleValueTranslatorTest .withPossibleValueSourceName("shapeV2") ); - /////////////////////////////// - // insert the list of shapes // - /////////////////////////////// - List shapeRecords = List.of( - new QRecord().withTableName(shapeTable.getName()).withValue("id", 1).withValue("name", "Triangle"), - new QRecord().withTableName(shapeTable.getName()).withValue("id", 2).withValue("name", "Square"), - new QRecord().withTableName(shapeTable.getName()).withValue("id", 3).withValue("name", "Circle")); - InsertInput insertInput = new InsertInput(qInstance); - insertInput.setSession(new QSession()); - insertInput.setTableName(shapeTable.getName()); - insertInput.setRecords(shapeRecords); - new InsertAction().execute(insertInput); + TestUtils.insertDefaultShapes(qInstance); /////////////////////////////////////////////////////// // define a list of persons pointing at those shapes // diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java index 915498d7..9058bc4a 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java @@ -26,9 +26,11 @@ import java.io.Serializable; import java.util.List; import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.AddAge; import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.GetAgeStatistics; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; @@ -447,6 +449,25 @@ public class TestUtils + /******************************************************************************* + ** + *******************************************************************************/ + public static void insertDefaultShapes(QInstance qInstance) throws QException + { + List shapeRecords = List.of( + new QRecord().withTableName(TABLE_NAME_SHAPE).withValue("id", 1).withValue("name", "Triangle"), + new QRecord().withTableName(TABLE_NAME_SHAPE).withValue("id", 2).withValue("name", "Square"), + new QRecord().withTableName(TABLE_NAME_SHAPE).withValue("id", 3).withValue("name", "Circle")); + + InsertInput insertInput = new InsertInput(qInstance); + insertInput.setSession(new QSession()); + insertInput.setTableName(TABLE_NAME_SHAPE); + insertInput.setRecords(shapeRecords); + new InsertAction().execute(insertInput); + } + + + /******************************************************************************* ** *******************************************************************************/