diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/DeleteSharedRecordProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/DeleteSharedRecordProcess.java new file mode 100644 index 00000000..ea73e9ff --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/DeleteSharedRecordProcess.java @@ -0,0 +1,130 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.processes.implementations.sharing; + + +import java.util.List; +import java.util.Objects; +import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +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.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.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.sharing.ShareableTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; + + +/******************************************************************************* + ** DeleteSharedRecord: {tableName; recordId; shareId;} + *******************************************************************************/ +public class DeleteSharedRecordProcess implements BackendStep, MetaDataProducerInterface +{ + public static final String NAME = "deleteSharedRecord"; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public QProcessMetaData produce(QInstance qInstance) throws QException + { + return new QProcessMetaData() + .withName(NAME) + .withIcon(new QIcon().withName("share")) + .withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED)) // todo confirm or protect + .withStepList(List.of( + new QBackendStepMetaData() + .withName("execute") + .withCode(new QCodeReference(getClass())) + .withInputData(new QFunctionInputMetaData() + .withField(new QFieldMetaData("tableName", QFieldType.STRING).withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME)) // todo - actually only a subset of this... + .withField(new QFieldMetaData("recordId", QFieldType.STRING)) + .withField(new QFieldMetaData("shareId", QFieldType.INTEGER)) + ) + )); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + String tableName = runBackendStepInput.getValueString("tableName"); + String recordIdString = runBackendStepInput.getValueString("recordId"); + Integer shareId = runBackendStepInput.getValueInteger("shareId"); + + Objects.requireNonNull(tableName, "Missing required input: tableName"); + Objects.requireNonNull(recordIdString, "Missing required input: recordId"); + Objects.requireNonNull(shareId, "Missing required input: shareId"); + + try + { + SharedRecordProcessUtils.AssetTableAndRecord assetTableAndRecord = SharedRecordProcessUtils.getAssetTableAndRecord(tableName, recordIdString); + + ShareableTableMetaData shareableTableMetaData = assetTableAndRecord.shareableTableMetaData(); + QRecord assetRecord = assetTableAndRecord.record(); + + SharedRecordProcessUtils.assertRecordOwnership(shareableTableMetaData, assetRecord, "delete shares of"); + + /////////////////// + // do the delete // + /////////////////// + DeleteOutput deleteOutput = new DeleteAction().execute(new DeleteInput(shareableTableMetaData.getSharedRecordTableName()).withPrimaryKeys(List.of(shareId))); + + ////////////////////// + // check for errors // + ////////////////////// + if(CollectionUtils.nullSafeHasContents(deleteOutput.getRecordsWithErrors())) + { + throw (new QException("Error deleting shared record: " + deleteOutput.getRecordsWithErrors().get(0).getErrors().get(0).getMessage())); + } + } + catch(QException qe) + { + throw (qe); + } + catch(Exception e) + { + throw (new QException("Error deleting shared record", e)); + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/EditSharedRecordProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/EditSharedRecordProcess.java new file mode 100644 index 00000000..406bea55 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/EditSharedRecordProcess.java @@ -0,0 +1,140 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.processes.implementations.sharing; + + +import java.util.List; +import java.util.Objects; +import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +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.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.sharing.ShareScopePossibleValueMetaDataProducer; +import com.kingsrook.qqq.backend.core.model.metadata.sharing.ShareableTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; + + +/******************************************************************************* + ** EditSharedRecord: {tableName; recordId; shareId; scopeId;} + *******************************************************************************/ +public class EditSharedRecordProcess implements BackendStep, MetaDataProducerInterface +{ + public static final String NAME = "editSharedRecord"; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public QProcessMetaData produce(QInstance qInstance) throws QException + { + return new QProcessMetaData() + .withName(NAME) + .withIcon(new QIcon().withName("share")) + .withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED)) // todo confirm or protect + .withStepList(List.of( + new QBackendStepMetaData() + .withName("execute") + .withCode(new QCodeReference(getClass())) + .withInputData(new QFunctionInputMetaData() + .withField(new QFieldMetaData("tableName", QFieldType.STRING).withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME)) // todo - actually only a subset of this... + .withField(new QFieldMetaData("recordId", QFieldType.STRING)) + .withField(new QFieldMetaData("scopeId", QFieldType.STRING).withPossibleValueSourceName(ShareScopePossibleValueMetaDataProducer.NAME)) + .withField(new QFieldMetaData("shareId", QFieldType.INTEGER)) + ) + )); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + String tableName = runBackendStepInput.getValueString("tableName"); + String recordIdString = runBackendStepInput.getValueString("recordId"); + String scopeId = runBackendStepInput.getValueString("scopeId"); + Integer shareId = runBackendStepInput.getValueInteger("shareId"); + + Objects.requireNonNull(tableName, "Missing required input: tableName"); + Objects.requireNonNull(recordIdString, "Missing required input: recordId"); + Objects.requireNonNull(scopeId, "Missing required input: scopeId"); + Objects.requireNonNull(shareId, "Missing required input: shareId"); + + try + { + SharedRecordProcessUtils.AssetTableAndRecord assetTableAndRecord = SharedRecordProcessUtils.getAssetTableAndRecord(tableName, recordIdString); + + ShareableTableMetaData shareableTableMetaData = assetTableAndRecord.shareableTableMetaData(); + QRecord assetRecord = assetTableAndRecord.record(); + QTableMetaData shareTable = QContext.getQInstance().getTable(shareableTableMetaData.getSharedRecordTableName()); + + SharedRecordProcessUtils.assertRecordOwnership(shareableTableMetaData, assetRecord, "edit shares of"); + ShareScope shareScope = SharedRecordProcessUtils.validateScopeId(scopeId); + + /////////////////// + // do the insert // + /////////////////// + UpdateOutput updateOutput = new UpdateAction().execute(new UpdateInput(shareableTableMetaData.getSharedRecordTableName()).withRecord(new QRecord() + .withValue(shareTable.getPrimaryKeyField(), shareId) + .withValue(shareableTableMetaData.getScopeFieldName(), shareScope.getPossibleValueId()))); + + ////////////////////// + // check for errors // + ////////////////////// + if(CollectionUtils.nullSafeHasContents(updateOutput.getRecords().get(0).getErrors())) + { + throw (new QException("Error editing shared record: " + updateOutput.getRecords().get(0).getErrors().get(0).getMessage())); + } + } + catch(QException qe) + { + throw (qe); + } + catch(Exception e) + { + throw (new QException("Error editing shared record", e)); + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/GetSharedRecordsProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/GetSharedRecordsProcess.java new file mode 100644 index 00000000..3b898f65 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/GetSharedRecordsProcess.java @@ -0,0 +1,243 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.processes.implementations.sharing; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +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.processes.BackendStep; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; +import com.kingsrook.qqq.backend.core.model.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.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.sharing.ShareableAudienceType; +import com.kingsrook.qqq.backend.core.model.metadata.sharing.ShareableTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.ListingHash; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; + + +/******************************************************************************* + ** GetSharedRecords: {tableName; recordId;} => [{id; audienceType; audienceId; audienceLabel; scopeId}] + *******************************************************************************/ +public class GetSharedRecordsProcess implements BackendStep, MetaDataProducerInterface +{ + public static final String NAME = "getSharedRecords"; + + private static final QLogger LOG = QLogger.getLogger(GetSharedRecordsProcess.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public QProcessMetaData produce(QInstance qInstance) throws QException + { + return new QProcessMetaData() + .withName(NAME) + .withIcon(new QIcon().withName("share")) + .withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED)) // todo confirm or protect + .withStepList(List.of( + new QBackendStepMetaData() + .withName("execute") + .withCode(new QCodeReference(getClass())) + .withInputData(new QFunctionInputMetaData() + .withField(new QFieldMetaData("tableName", QFieldType.STRING).withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME)) // todo - actually only a subset of this... + .withField(new QFieldMetaData("recordId", QFieldType.STRING)) + ) + )); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + String tableName = runBackendStepInput.getValueString("tableName"); + String recordIdString = runBackendStepInput.getValueString("recordId"); + + Objects.requireNonNull(tableName, "Missing required input: tableName"); + Objects.requireNonNull(recordIdString, "Missing required input: recordId"); + + try + { + SharedRecordProcessUtils.AssetTableAndRecord assetTableAndRecord = SharedRecordProcessUtils.getAssetTableAndRecord(tableName, recordIdString); + + ShareableTableMetaData shareableTableMetaData = assetTableAndRecord.shareableTableMetaData(); + QTableMetaData shareTable = QContext.getQInstance().getTable(shareableTableMetaData.getSharedRecordTableName()); + Serializable recordId = assetTableAndRecord.recordId(); + + ///////////////////////////////////// + // query for shares on this record // + ///////////////////////////////////// + QueryInput queryInput = new QueryInput(shareTable.getName()); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria(shareableTableMetaData.getAssetIdFieldName(), QCriteriaOperator.EQUALS, recordId)) + .withOrderBy(new QFilterOrderBy(shareTable.getPrimaryKeyField())) + ); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // iterate results, building QRecords to output - note - we'll need to collect ids, then look them up in audience-source tables // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ArrayList resultList = new ArrayList<>(); + ListingHash audienceIds = new ListingHash<>(); + for(QRecord record : queryOutput.getRecords()) + { + QRecord outputRecord = new QRecord(); + outputRecord.setValue("shareId", record.getValue(shareTable.getPrimaryKeyField())); + outputRecord.setValue("scopeId", record.getValue(shareableTableMetaData.getScopeFieldName())); + + boolean foundAudienceType = false; + for(ShareableAudienceType audienceType : shareableTableMetaData.getAudienceTypes().values()) + { + Serializable audienceId = record.getValueString(audienceType.getFieldName()); + if(audienceId != null) + { + outputRecord.setValue("audienceType", audienceType.getName()); + outputRecord.setValue("audienceId", audienceId); + audienceIds.add(audienceType.getName(), audienceId); + foundAudienceType = true; + break; + } + } + + if(!foundAudienceType) + { + LOG.warn("Failed to find what audience type to use for a shared record", + logPair("sharedTableName", shareTable.getName()), + logPair("id", record.getValue(shareTable.getPrimaryKeyField())), + logPair("recordId", record.getValue(shareableTableMetaData.getAssetIdFieldName()))); + continue; + } + + resultList.add(outputRecord); + } + + ///////////////////////////////// + // look up the audience labels // + ///////////////////////////////// + Map> audienceLabels = new HashMap<>(); + Set audienceTypesWithLabels = new HashSet<>(); + for(Map.Entry> entry : audienceIds.entrySet()) + { + String audienceType = entry.getKey(); + List ids = entry.getValue(); + if(CollectionUtils.nullSafeHasContents(ids)) + { + ShareableAudienceType shareableAudienceType = shareableTableMetaData.getAudienceTypes().get(audienceType); + if(StringUtils.hasContent(shareableAudienceType.getSourceTableName())) + { + audienceTypesWithLabels.add(audienceType); + + String keyField = shareableAudienceType.getSourceTableKeyFieldName(); + if(!StringUtils.hasContent(keyField)) + { + keyField = QContext.getQInstance().getTable(shareableAudienceType.getSourceTableName()).getPrimaryKeyField(); + } + + QueryInput audienceQueryInput = new QueryInput(shareableAudienceType.getSourceTableName()); + audienceQueryInput.setFilter(new QQueryFilter(new QFilterCriteria(keyField, QCriteriaOperator.IN, ids))); + audienceQueryInput.setShouldGenerateDisplayValues(true); // to get record labels + QueryOutput audienceQueryOutput = new QueryAction().execute(audienceQueryInput); + for(QRecord audienceRecord : audienceQueryOutput.getRecords()) + { + audienceLabels.computeIfAbsent(audienceType, k -> new HashMap<>()); + audienceLabels.get(audienceType).put(audienceRecord.getValue(keyField), audienceRecord.getRecordLabel()); + } + } + } + } + + //////////////////////////////////////////// + // put those labels on the output records // + //////////////////////////////////////////// + for(QRecord outputRecord : resultList) + { + String audienceType = outputRecord.getValueString("audienceType"); + Map typeLabels = audienceLabels.getOrDefault(audienceType, Collections.emptyMap()); + Serializable audienceId = outputRecord.getValue("audienceId"); + String label = typeLabels.get(audienceId); + if(StringUtils.hasContent(label)) + { + outputRecord.setValue("audienceLabel", label); + } + else + { + if(audienceTypesWithLabels.contains(audienceType)) + { + outputRecord.setValue("audienceLabel", "Unknown " + audienceType + " (id=" + audienceId + ")"); + } + else + { + outputRecord.setValue("audienceLabel", audienceType + " " + audienceId); + } + } + } + + runBackendStepOutput.addValue("resultList", resultList); + } + catch(QException qe) + { + throw (qe); + } + catch(Exception e) + { + throw (new QException("Error getting shared records.", e)); + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/InsertSharedRecordProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/InsertSharedRecordProcess.java new file mode 100644 index 00000000..4d449ede --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/InsertSharedRecordProcess.java @@ -0,0 +1,186 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.processes.implementations.sharing; + + +import java.io.Serializable; +import java.util.List; +import java.util.Objects; +import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.actions.tables.GetAction; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; +import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.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.QIcon; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.sharing.ShareScopePossibleValueMetaDataProducer; +import com.kingsrook.qqq.backend.core.model.metadata.sharing.ShareableAudienceType; +import com.kingsrook.qqq.backend.core.model.metadata.sharing.ShareableTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider; +import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage; +import com.kingsrook.qqq.backend.core.model.statusmessages.QErrorMessage; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; + + +/******************************************************************************* + ** InsertSharedRecord: {tableName; recordId; audienceType; audienceId; scopeId;} + *******************************************************************************/ +public class InsertSharedRecordProcess implements BackendStep, MetaDataProducerInterface +{ + public static final String NAME = "insertSharedRecord"; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public QProcessMetaData produce(QInstance qInstance) throws QException + { + return new QProcessMetaData() + .withName(NAME) + .withIcon(new QIcon().withName("share")) + .withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED)) // todo confirm or protect + .withStepList(List.of( + new QBackendStepMetaData() + .withName("execute") + .withCode(new QCodeReference(getClass())) + .withInputData(new QFunctionInputMetaData() + .withField(new QFieldMetaData("tableName", QFieldType.STRING).withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME)) // todo - actually only a subset of this... + .withField(new QFieldMetaData("recordId", QFieldType.STRING)) + .withField(new QFieldMetaData("audienceType", QFieldType.STRING)) // todo take a PVS name as param? + .withField(new QFieldMetaData("audienceId", QFieldType.STRING)) + .withField(new QFieldMetaData("scopeId", QFieldType.STRING).withPossibleValueSourceName(ShareScopePossibleValueMetaDataProducer.NAME)) + ) + )); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + String tableName = runBackendStepInput.getValueString("tableName"); + String recordIdString = runBackendStepInput.getValueString("recordId"); + String audienceType = runBackendStepInput.getValueString("audienceType"); + String audienceIdString = runBackendStepInput.getValueString("audienceId"); + String scopeId = runBackendStepInput.getValueString("scopeId"); + + Objects.requireNonNull(tableName, "Missing required input: tableName"); + Objects.requireNonNull(recordIdString, "Missing required input: recordId"); + Objects.requireNonNull(audienceType, "Missing required input: audienceType"); + Objects.requireNonNull(audienceIdString, "Missing required input: audienceId"); + Objects.requireNonNull(scopeId, "Missing required input: scopeId"); + + try + { + SharedRecordProcessUtils.AssetTableAndRecord assetTableAndRecord = SharedRecordProcessUtils.getAssetTableAndRecord(tableName, recordIdString); + + ShareableTableMetaData shareableTableMetaData = assetTableAndRecord.shareableTableMetaData(); + QRecord assetRecord = assetTableAndRecord.record(); + Serializable recordId = assetTableAndRecord.recordId(); + + SharedRecordProcessUtils.assertRecordOwnership(shareableTableMetaData, assetRecord, "share"); + + //////////////////////////////// + // validate the audience type // + //////////////////////////////// + ShareableAudienceType shareableAudienceType = shareableTableMetaData.getAudienceTypes().get(audienceType); + if(shareableAudienceType == null) + { + throw (new QException("[" + audienceType + "] is not a recognized audience type for sharing records from the " + tableName + " table. Allowed values are: " + shareableTableMetaData.getAudienceTypes().keySet())); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // if we know the audience source-table, then fetch & validate security-wise the audience id // + /////////////////////////////////////////////////////////////////////////////////////////////// + Serializable audienceId = audienceIdString; + if(StringUtils.hasContent(shareableAudienceType.getSourceTableName())) + { + QTableMetaData audienceTable = QContext.getQInstance().getTable(shareableAudienceType.getSourceTableName()); + audienceId = ValueUtils.getValueAsFieldType(audienceTable.getField(audienceTable.getPrimaryKeyField()).getType(), audienceIdString); + QRecord audienceRecord = new GetAction().executeForRecord(new GetInput(audienceTable.getName()).withPrimaryKey(audienceId)); + if(audienceRecord == null) + { + throw (new QException("A record could not be found for audience type " + audienceType + ", audience id: " + audienceIdString)); + } + } + + //////////////////////////////// + // validate input share scope // + //////////////////////////////// + ShareScope shareScope = SharedRecordProcessUtils.validateScopeId(scopeId); + + /////////////////// + // do the insert // + /////////////////// + InsertOutput insertOutput = new InsertAction().execute(new InsertInput(shareableTableMetaData.getSharedRecordTableName()).withRecord(new QRecord() + .withValue(shareableTableMetaData.getAssetIdFieldName(), recordId) + .withValue(shareableTableMetaData.getScopeFieldName(), shareScope.getPossibleValueId()) + .withValue(shareableAudienceType.getFieldName(), audienceId))); + + ////////////////////// + // check for errors // + ////////////////////// + if(CollectionUtils.nullSafeHasContents(insertOutput.getRecords().get(0).getErrors())) + { + QErrorMessage errorMessage = insertOutput.getRecords().get(0).getErrors().get(0); + if(errorMessage instanceof BadInputStatusMessage) + { + throw (new QUserFacingException(errorMessage.getMessage())); + } + throw (new QException("Error inserting shared record: " + errorMessage.getMessage())); + } + } + catch(QException qe) + { + throw (qe); + } + catch(Exception e) + { + throw (new QException("Error inserting shared record", e)); + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/ShareScope.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/ShareScope.java new file mode 100644 index 00000000..720402cc --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/ShareScope.java @@ -0,0 +1,81 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.processes.implementations.sharing; + + +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum; + + +/******************************************************************************* + ** for a shared record, what scope of access is given. + *******************************************************************************/ +public enum ShareScope implements PossibleValueEnum +{ + READ_ONLY("Read Only"), + READ_WRITE("Read and Edit"); + + + private final String label; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + ShareScope(String label) + { + this.label = label; + } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public String getPossibleValueId() + { + return name(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public String getPossibleValueLabel() + { + return label; + } + + + + /******************************************************************************* + ** Getter for label + ** + *******************************************************************************/ + public String getLabel() + { + return label; + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/SharedRecordProcessUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/SharedRecordProcessUtils.java new file mode 100644 index 00000000..6cd37a1e --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/SharedRecordProcessUtils.java @@ -0,0 +1,126 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.processes.implementations.sharing; + + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Objects; +import com.kingsrook.qqq.backend.core.actions.tables.GetAction; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.sharing.ShareableTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class SharedRecordProcessUtils +{ + /******************************************************************************* + ** + *******************************************************************************/ + record AssetTableAndRecord(QTableMetaData table, ShareableTableMetaData shareableTableMetaData, QRecord record, Serializable recordId) {} + + + + /******************************************************************************* + ** + *******************************************************************************/ + static AssetTableAndRecord getAssetTableAndRecord(String tableName, String recordIdString) throws QException + { + ////////////////////////////// + // validate the asset table // + ////////////////////////////// + QTableMetaData assetTable = QContext.getQInstance().getTable(tableName); + if(assetTable == null) + { + throw (new QException("The specified tableName, " + tableName + ", was not found.")); + } + + ShareableTableMetaData shareableTableMetaData = assetTable.getShareableTableMetaData(); + if(shareableTableMetaData == null) + { + throw (new QException("The specified tableName, " + tableName + ", is not shareable.")); + } + + ////////////////////////////// + // look up the asset record // + ////////////////////////////// + Serializable recordId = ValueUtils.getValueAsFieldType(assetTable.getField(assetTable.getPrimaryKeyField()).getType(), recordIdString); + QRecord assetRecord = new GetAction().executeForRecord(new GetInput(tableName).withPrimaryKey(recordId)); + if(assetRecord == null) + { + throw (new QException("A record could not be found in table, " + tableName + ", with primary key: " + recordIdString)); + } + + return new AssetTableAndRecord(assetTable, shareableTableMetaData, assetRecord, recordId); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + static void assertRecordOwnership(ShareableTableMetaData shareableTableMetaData, QRecord assetRecord, String verbClause) throws QException + { + /////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if the shareable meta-data says this-table's owner id, then validate that the current user own the record // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(StringUtils.hasContent(shareableTableMetaData.getThisTableOwnerIdFieldName())) + { + Serializable ownerId = assetRecord.getValue(shareableTableMetaData.getThisTableOwnerIdFieldName()); + if(!Objects.equals(ownerId, QContext.getQSession().getUser().getIdReference())) + { + throw (new QException("You are not the owner of this record, so you may not " + verbClause + " it.")); + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + static ShareScope validateScopeId(String scopeId) throws QException + { + //////////////////////////////// + // validate input share scope // + //////////////////////////////// + ShareScope shareScope = null; + try + { + shareScope = ShareScope.valueOf(scopeId); + return (shareScope); + } + catch(IllegalArgumentException e) + { + throw (new QException("[" + shareScope + "] is not a recognized value for shareScope. Allowed values are: " + Arrays.toString(ShareScope.values()))); + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/SharingMetaDataProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/SharingMetaDataProvider.java new file mode 100644 index 00000000..ffbb13f4 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/SharingMetaDataProvider.java @@ -0,0 +1,61 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.processes.implementations.sharing; + + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class SharingMetaDataProvider +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public void defineAll(QInstance instance, Consumer processEnricher) throws QException + { + List processes = new ArrayList<>(); + processes.add(new GetSharedRecordsProcess().produce(instance)); + processes.add(new InsertSharedRecordProcess().produce(instance)); + processes.add(new EditSharedRecordProcess().produce(instance)); + processes.add(new DeleteSharedRecordProcess().produce(instance)); + + for(QProcessMetaData process : processes) + { + if(processEnricher != null) + { + processEnricher.accept(process); + } + + instance.addProcess(process); + } + } + +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/DeleteSharedRecordProcessTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/DeleteSharedRecordProcessTest.java new file mode 100644 index 00000000..38a9e731 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/DeleteSharedRecordProcessTest.java @@ -0,0 +1,132 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.processes.implementations.sharing; + + +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.actions.tables.GetAction; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; +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.savedreports.ReportColumns; +import com.kingsrook.qqq.backend.core.model.savedreports.SavedReport; +import com.kingsrook.qqq.backend.core.model.savedreports.SavedReportsMetaDataProvider; +import com.kingsrook.qqq.backend.core.model.savedreports.SharedSavedReport; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertNull; + + +/******************************************************************************* + ** Unit test for DeleteSharedRecordProcess + *******************************************************************************/ +class DeleteSharedRecordProcessTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + void beforeEach() throws Exception + { + new SavedReportsMetaDataProvider().defineAll(QContext.getQInstance(), TestUtils.MEMORY_BACKEND_NAME, TestUtils.MEMORY_BACKEND_NAME, null); + + new InsertAction().execute(new InsertInput(SavedReport.TABLE_NAME).withRecordEntity(new SavedReport() + .withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withLabel("Test") + .withColumnsJson(JsonUtils.toJson(new ReportColumns().withColumn("id"))) + )); + + new InsertAction().execute(new InsertInput(SharedSavedReport.TABLE_NAME).withRecordEntity(new SharedSavedReport() + .withSavedReportId(1) + .withUserId(BaseTest.DEFAULT_USER_ID) + .withScope(ShareScope.READ_WRITE.getPossibleValueId()) + )); + + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFailCases() throws QException + { + RunBackendStepInput input = new RunBackendStepInput(); + RunBackendStepOutput output = new RunBackendStepOutput(); + DeleteSharedRecordProcess processStep = new DeleteSharedRecordProcess(); + + assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("Missing required input: tableName"); + input.addValue("tableName", SavedReport.TABLE_NAME); + assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("Missing required input: recordId"); + input.addValue("recordId", 1); + assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("Missing required input: shareId"); + input.addValue("shareId", 3); + + /////////////////////////////////////////////////// + // fail because the requested record isn't found // + /////////////////////////////////////////////////// + assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("Error deleting shared record: No record was found to delete"); + + /////////////////////////////////////////////////////////////////////////////////////////////////////// + // now fail because a different user (than the owner, who did the initial delete) is trying to share // + /////////////////////////////////////////////////////////////////////////////////////////////////////// + QContext.setQSession(newSession("not-" + DEFAULT_USER_ID)); + input.addValue("shareId", 1); + assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("not the owner of this record"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSuccess() throws QException + { + RunBackendStepInput input = new RunBackendStepInput(); + RunBackendStepOutput output = new RunBackendStepOutput(); + DeleteSharedRecordProcess processStep = new DeleteSharedRecordProcess(); + + input.addValue("tableName", SavedReport.TABLE_NAME); + input.addValue("recordId", 1); + input.addValue("shareId", 1); + + ////////////////////////////////////////// + // assert the shared record got deleted // + ////////////////////////////////////////// + processStep.run(input, output); + + QRecord sharedSavedReportRecord = new GetAction().executeForRecord(new GetInput(SharedSavedReport.TABLE_NAME).withPrimaryKey(1)); + assertNull(sharedSavedReportRecord); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/EditSharedRecordProcessTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/EditSharedRecordProcessTest.java new file mode 100644 index 00000000..1d55f4eb --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/EditSharedRecordProcessTest.java @@ -0,0 +1,144 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.processes.implementations.sharing; + + +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.actions.tables.GetAction; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; +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.savedreports.ReportColumns; +import com.kingsrook.qqq.backend.core.model.savedreports.SavedReport; +import com.kingsrook.qqq.backend.core.model.savedreports.SavedReportsMetaDataProvider; +import com.kingsrook.qqq.backend.core.model.savedreports.SharedSavedReport; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/******************************************************************************* + ** Unit test for EditSharedRecordProcess + *******************************************************************************/ +class EditSharedRecordProcessTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + void beforeEach() throws Exception + { + new SavedReportsMetaDataProvider().defineAll(QContext.getQInstance(), TestUtils.MEMORY_BACKEND_NAME, TestUtils.MEMORY_BACKEND_NAME, null); + + new InsertAction().execute(new InsertInput(SavedReport.TABLE_NAME).withRecordEntity(new SavedReport() + .withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withLabel("Test") + .withColumnsJson(JsonUtils.toJson(new ReportColumns().withColumn("id"))) + )); + + new InsertAction().execute(new InsertInput(SharedSavedReport.TABLE_NAME).withRecordEntity(new SharedSavedReport() + .withSavedReportId(1) + .withUserId(BaseTest.DEFAULT_USER_ID) + .withScope(ShareScope.READ_WRITE.getPossibleValueId()) + )); + + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFailCases() throws QException + { + RunBackendStepInput input = new RunBackendStepInput(); + RunBackendStepOutput output = new RunBackendStepOutput(); + EditSharedRecordProcess processStep = new EditSharedRecordProcess(); + + assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("Missing required input: tableName"); + input.addValue("tableName", SavedReport.TABLE_NAME); + assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("Missing required input: recordId"); + input.addValue("recordId", 1); + assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("Missing required input: scopeId"); + input.addValue("scopeId", ShareScope.READ_WRITE.getPossibleValueId()); + assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("Missing required input: shareId"); + input.addValue("shareId", 3); + + /////////////////////////////////////////////////// + // fail because the requested record isn't found // + /////////////////////////////////////////////////// + assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("Error editing shared record: No record was found to update"); + + /////////////////////////////////////////////////////////////////////////////////////////////////////// + // now fail because a different user (than the owner, who did the initial edit) is trying to share // + /////////////////////////////////////////////////////////////////////////////////////////////////////// + QContext.setQSession(newSession("not-" + DEFAULT_USER_ID)); + input.addValue("shareId", 1); + assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("not the owner of this record"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSuccess() throws QException + { + RunBackendStepInput input = new RunBackendStepInput(); + RunBackendStepOutput output = new RunBackendStepOutput(); + EditSharedRecordProcess processStep = new EditSharedRecordProcess(); + + /////////////////////////// + // assert original value // + /////////////////////////// + QRecord sharedSavedReportRecord = new GetAction().executeForRecord(new GetInput(SharedSavedReport.TABLE_NAME).withPrimaryKey(1)); + assertEquals(ShareScope.READ_WRITE.getPossibleValueId(), sharedSavedReportRecord.getValueString("scope")); + + input.addValue("tableName", SavedReport.TABLE_NAME); + input.addValue("recordId", 1); + input.addValue("shareId", 1); + input.addValue("scopeId", ShareScope.READ_ONLY.getPossibleValueId()); + + ///////////////////////////////////////// + // assert the shared record got edited // + ///////////////////////////////////////// + processStep.run(input, output); + + ////////////////////////// + // assert updated value // + ////////////////////////// + sharedSavedReportRecord = new GetAction().executeForRecord(new GetInput(SharedSavedReport.TABLE_NAME).withPrimaryKey(1)); + assertEquals(ShareScope.READ_ONLY.getPossibleValueId(), sharedSavedReportRecord.getValueString("scope")); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/GetSharedRecordsProcessTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/GetSharedRecordsProcessTest.java new file mode 100644 index 00000000..9a7fdb63 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/GetSharedRecordsProcessTest.java @@ -0,0 +1,98 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.processes.implementations.sharing; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +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.savedreports.ReportColumns; +import com.kingsrook.qqq.backend.core.model.savedreports.SavedReport; +import com.kingsrook.qqq.backend.core.model.savedreports.SavedReportsMetaDataProvider; +import com.kingsrook.qqq.backend.core.model.savedreports.SharedSavedReport; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; +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; + + +/******************************************************************************* + ** Unit test for GetSharedRecordsProcess + *******************************************************************************/ +class GetSharedRecordsProcessTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + void beforeEach() throws Exception + { + new SavedReportsMetaDataProvider().defineAll(QContext.getQInstance(), TestUtils.MEMORY_BACKEND_NAME, TestUtils.MEMORY_BACKEND_NAME, null); + + new InsertAction().execute(new InsertInput(SavedReport.TABLE_NAME).withRecordEntity(new SavedReport() + .withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withLabel("Test") + .withColumnsJson(JsonUtils.toJson(new ReportColumns().withColumn("id"))))); + + new InsertAction().execute(new InsertInput(SharedSavedReport.TABLE_NAME).withRecordEntity(new SharedSavedReport() + .withScope(ShareScope.READ_WRITE.getPossibleValueId()) + .withUserId("007") + .withSavedReportId(1) + )); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException + { + RunBackendStepInput input = new RunBackendStepInput(); + RunBackendStepOutput output = new RunBackendStepOutput(); + GetSharedRecordsProcess processStep = new GetSharedRecordsProcess(); + + input.addValue("tableName", SavedReport.TABLE_NAME); + input.addValue("recordId", 1); + processStep.run(input, output); + + List resultList = (List) output.getValue("resultList"); + assertEquals(1, resultList.size()); + + QRecord outputRecord = resultList.get(0); + assertEquals(1, outputRecord.getValueInteger("id")); + assertEquals(ShareScope.READ_WRITE.getPossibleValueId(), outputRecord.getValueString("scopeId")); + assertEquals("user", outputRecord.getValueString("audienceType")); + assertEquals("007", outputRecord.getValueString("audienceId")); + assertEquals("user 007", outputRecord.getValueString("audienceLabel")); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/InsertSharedRecordProcessTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/InsertSharedRecordProcessTest.java new file mode 100644 index 00000000..67120798 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/InsertSharedRecordProcessTest.java @@ -0,0 +1,146 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.processes.implementations.sharing; + + +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.actions.tables.GetAction; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; +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.savedreports.ReportColumns; +import com.kingsrook.qqq.backend.core.model.savedreports.SavedReport; +import com.kingsrook.qqq.backend.core.model.savedreports.SavedReportsMetaDataProvider; +import com.kingsrook.qqq.backend.core.model.savedreports.SharedSavedReport; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + + +/******************************************************************************* + ** Unit test for InsertSharedRecordProcess + *******************************************************************************/ +class InsertSharedRecordProcessTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + void beforeEach() throws Exception + { + new SavedReportsMetaDataProvider().defineAll(QContext.getQInstance(), TestUtils.MEMORY_BACKEND_NAME, TestUtils.MEMORY_BACKEND_NAME, null); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFailCases() throws QException + { + RunBackendStepInput input = new RunBackendStepInput(); + RunBackendStepOutput output = new RunBackendStepOutput(); + InsertSharedRecordProcess processStep = new InsertSharedRecordProcess(); + + assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("Missing required input: tableName"); + input.addValue("tableName", SavedReport.TABLE_NAME); + assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("Missing required input: recordId"); + input.addValue("recordId", 1); + assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("Missing required input: audienceType"); + input.addValue("audienceType", "user"); + assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("Missing required input: audienceId"); + input.addValue("audienceId", "darin@kingsrook.com"); + assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("Missing required input: scopeId"); + input.addValue("scopeId", ShareScope.READ_WRITE); + + ////////////////////////////// + // try a non-sharable table // + ////////////////////////////// + input.addValue("tableName", TestUtils.TABLE_NAME_PERSON_MEMORY); + assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("is not shareable"); + input.addValue("tableName", SavedReport.TABLE_NAME); + + /////////////////////////////////////////////////// + // fail because the requested record isn't found // + /////////////////////////////////////////////////// + assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("record could not be found in table, savedReport, with primary key: 1"); + new InsertAction().execute(new InsertInput(SavedReport.TABLE_NAME).withRecordEntity(new SavedReport() + .withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withLabel("Test") + .withColumnsJson(JsonUtils.toJson(new ReportColumns().withColumn("id"))) + )); + + /////////////////////////////////////////////////////////////////////////////////////////////////////// + // now fail because a different user (than the owner, who did the initial insert) is trying to share // + /////////////////////////////////////////////////////////////////////////////////////////////////////// + QContext.setQSession(newSession("not-" + DEFAULT_USER_ID)); + assertThatThrownBy(() -> processStep.run(input, output)).hasMessageContaining("not the owner of this record"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSuccess() throws QException + { + RunBackendStepInput input = new RunBackendStepInput(); + RunBackendStepOutput output = new RunBackendStepOutput(); + InsertSharedRecordProcess processStep = new InsertSharedRecordProcess(); + + input.addValue("tableName", SavedReport.TABLE_NAME); + input.addValue("recordId", 1); + input.addValue("audienceType", "user"); + input.addValue("audienceId", "darin@kingsrook.com"); + input.addValue("scopeId", ShareScope.READ_WRITE); + + new InsertAction().execute(new InsertInput(SavedReport.TABLE_NAME).withRecordEntity(new SavedReport() + .withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withLabel("Test") + .withColumnsJson(JsonUtils.toJson(new ReportColumns().withColumn("id"))) + )); + + //////////////////////////////////////// + // assert the shared record got built // + //////////////////////////////////////// + processStep.run(input, output); + + QRecord sharedSavedReportRecord = new GetAction().executeForRecord(new GetInput(SharedSavedReport.TABLE_NAME).withPrimaryKey(1)); + assertNotNull(sharedSavedReportRecord); + assertEquals(1, sharedSavedReportRecord.getValueInteger("savedReportId")); + assertEquals("darin@kingsrook.com", sharedSavedReportRecord.getValueString("userId")); + assertEquals(ShareScope.READ_WRITE.getPossibleValueId(), sharedSavedReportRecord.getValueString("scope")); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/SharingMetaDataProviderTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/SharingMetaDataProviderTest.java new file mode 100644 index 00000000..841b03dc --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/SharingMetaDataProviderTest.java @@ -0,0 +1,46 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.processes.implementations.sharing; + + +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import org.junit.jupiter.api.Test; + + +/******************************************************************************* + ** Unit test for SharingMetaDataProvider + *******************************************************************************/ +class SharingMetaDataProviderTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException + { + new SharingMetaDataProvider().defineAll(QContext.getQInstance(), null); + } + +} \ No newline at end of file