add exposed joins to frontend metadata; checkpoing on validation & enrichment of eposed joins

This commit is contained in:
2023-04-24 12:11:46 -05:00
parent d086284de7
commit 2495989584
13 changed files with 833 additions and 300 deletions

View File

@ -1,58 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.metadata;
import java.util.Set;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import org.junit.jupiter.api.Test;
/*******************************************************************************
**
*******************************************************************************/
public class JoinGraphTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void test()
{
JoinGraph joinGraph = new JoinGraph(QContext.getQInstance());
for(QTableMetaData table : QContext.getQInstance().getTables().values())
{
Set<String> joins = joinGraph.getJoins(table.getName());
if(joins.isEmpty())
{
System.out.println(table.getName() + " has no joins");
}
else
{
System.out.println(table.getName() + " joins: " + joins);
}
}
}
}

View File

@ -32,7 +32,11 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
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;
@ -41,6 +45,7 @@ import org.junit.jupiter.api.Test;
import static com.kingsrook.qqq.backend.core.utils.TestUtils.APP_NAME_GREETINGS;
import static com.kingsrook.qqq.backend.core.utils.TestUtils.APP_NAME_MISCELLANEOUS;
import static com.kingsrook.qqq.backend.core.utils.TestUtils.APP_NAME_PEOPLE;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
@ -304,4 +309,131 @@ class QInstanceEnricherTest extends BaseTest
}
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testExposedJoinPaths()
{
//////////////////////////
// no join path => fail //
//////////////////////////
{
QInstance qInstance = TestUtils.defineInstance();
qInstance.addTable(newTable("A", "id").withExposedJoin(new ExposedJoin().withJoinTable("B")));
qInstance.addTable(newTable("B", "id", "aId"));
assertThatThrownBy(() -> new QInstanceEnricher(qInstance).enrich())
.rootCause()
.hasMessageContaining("Could not infer a joinPath for table [A], exposedJoin to [B]")
.hasMessageContaining("No join connections between these tables exist in this instance.");
}
/////////////////////////////////
// multiple join paths => fail //
/////////////////////////////////
{
QInstance qInstance = TestUtils.defineInstance();
qInstance.addTable(newTable("A", "id").withExposedJoin(new ExposedJoin().withJoinTable("B")));
qInstance.addTable(newTable("B", "id", "aId1", "aId2"));
qInstance.addJoin(new QJoinMetaData().withLeftTable("A").withRightTable("B").withName("AB1").withJoinOn(new JoinOn("id", "aId1")).withType(JoinType.ONE_TO_ONE));
qInstance.addJoin(new QJoinMetaData().withLeftTable("A").withRightTable("B").withName("AB2").withJoinOn(new JoinOn("id", "aId2")).withType(JoinType.ONE_TO_ONE));
assertThatThrownBy(() -> new QInstanceEnricher(qInstance).enrich())
.rootCause()
.hasMessageContaining("Could not infer a joinPath for table [A], exposedJoin to [B]")
.hasMessageContaining("2 join connections exist")
.hasMessageContaining("\nAB1\n")
.hasMessageContaining("\nAB2.");
////////////////////////////////////////////
// but if you specify a path, you're good //
////////////////////////////////////////////
qInstance.getTable("A").getExposedJoins().get(0).setJoinPath(List.of("AB2"));
new QInstanceEnricher(qInstance).enrich();
assertEquals("B", qInstance.getTable("A").getExposedJoins().get(0).getLabel());
}
/////////////////////////////////
// multiple join paths => fail //
/////////////////////////////////
{
QInstance qInstance = TestUtils.defineInstance();
qInstance.addTable(newTable("A", "id").withExposedJoin(new ExposedJoin().withJoinTable("C")));
qInstance.addTable(newTable("B", "id", "aId"));
qInstance.addTable(newTable("C", "id", "bId", "aId"));
qInstance.addJoin(new QJoinMetaData().withLeftTable("A").withRightTable("B").withName("AB").withJoinOn(new JoinOn("id", "aId")).withType(JoinType.ONE_TO_ONE));
qInstance.addJoin(new QJoinMetaData().withLeftTable("B").withRightTable("C").withName("BC").withJoinOn(new JoinOn("id", "bId")).withType(JoinType.ONE_TO_ONE));
qInstance.addJoin(new QJoinMetaData().withLeftTable("A").withRightTable("C").withName("AC").withJoinOn(new JoinOn("id", "aId")).withType(JoinType.ONE_TO_ONE));
assertThatThrownBy(() -> new QInstanceEnricher(qInstance).enrich())
.rootCause()
.hasMessageContaining("Could not infer a joinPath for table [A], exposedJoin to [C]")
.hasMessageContaining("2 join connections exist")
.hasMessageContaining("\nAB, BC\n")
.hasMessageContaining("\nAC.");
////////////////////////////////////////////
// but if you specify a path, you're good //
////////////////////////////////////////////
qInstance.getTable("A").getExposedJoins().get(0).setJoinPath(List.of("AB", "BC"));
new QInstanceEnricher(qInstance).enrich();
assertEquals("C", qInstance.getTable("A").getExposedJoins().get(0).getLabel());
}
//////////////////////////////////////////////////////////////////////////////////////
// even if you specify a bogus path, Enricher doesn't care - see validator to care. //
//////////////////////////////////////////////////////////////////////////////////////
{
QInstance qInstance = TestUtils.defineInstance();
qInstance.addTable(newTable("A", "id").withExposedJoin(new ExposedJoin().withJoinTable("B").withJoinPath(List.of("not-a-join"))));
qInstance.addTable(newTable("B", "id", "aId"));
new QInstanceEnricher(qInstance).enrich();
}
////////////////////////////////////
// one join path => great success //
////////////////////////////////////
{
QInstance qInstance = TestUtils.defineInstance();
qInstance.addTable(newTable("A", "id")
.withExposedJoin(new ExposedJoin().withJoinTable("B"))
.withExposedJoin(new ExposedJoin().withJoinTable("C")));
qInstance.addTable(newTable("B", "id", "aId"));
qInstance.addTable(newTable("C", "id", "bId"));
qInstance.addJoin(new QJoinMetaData().withLeftTable("A").withRightTable("B").withName("AB").withJoinOn(new JoinOn("id", "aId")).withType(JoinType.ONE_TO_ONE));
qInstance.addJoin(new QJoinMetaData().withLeftTable("B").withRightTable("C").withName("BC").withJoinOn(new JoinOn("id", "bId")).withType(JoinType.ONE_TO_ONE));
new QInstanceEnricher(qInstance).enrich();
ExposedJoin exposedJoinAB = qInstance.getTable("A").getExposedJoins().get(0);
assertEquals("B", exposedJoinAB.getLabel());
assertEquals(List.of("AB"), exposedJoinAB.getJoinPath());
ExposedJoin exposedJoinAC = qInstance.getTable("A").getExposedJoins().get(1);
assertEquals("C", exposedJoinAC.getLabel());
assertEquals(List.of("AB", "BC"), exposedJoinAC.getJoinPath());
}
}
/*******************************************************************************
**
*******************************************************************************/
private QTableMetaData newTable(String tableName, String... fieldNames)
{
QTableMetaData tableMetaData = new QTableMetaData()
.withName(tableName)
.withBackendName(TestUtils.DEFAULT_BACKEND_NAME)
.withPrimaryKeyField(fieldNames[0]);
for(String fieldName : fieldNames)
{
tableMetaData.addField(new QFieldMetaData(fieldName, QFieldType.INTEGER));
}
return (tableMetaData);
}
}

View File

@ -50,6 +50,9 @@ 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.fields.ValueTooLongBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
@ -65,6 +68,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
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;
@ -1671,6 +1675,83 @@ class QInstanceValidatorTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testExposedJoinPaths()
{
assertValidationFailureReasons(qInstance -> qInstance.addTable(newTable("A", "id").withExposedJoin(new ExposedJoin())),
"Table A has an exposedJoin that is missing a joinTable name",
"Table A exposedJoin [missingJoinTableName] is missing a label");
assertValidationFailureReasons(qInstance -> qInstance.addTable(newTable("A", "id").withExposedJoin(new ExposedJoin().withJoinTable("B"))),
"Table A exposedJoin B is referencing an unrecognized table",
"Table A exposedJoin B is missing a label");
assertValidationFailureReasons(qInstance ->
{
qInstance.addTable(newTable("A", "id").withExposedJoin(new ExposedJoin().withJoinTable("B").withLabel("B").withJoinPath(List.of("notAJoin"))));
qInstance.addTable(newTable("B", "id", "aId"));
qInstance.addJoin(new QJoinMetaData().withLeftTable("A").withRightTable("B").withName("AB").withType(JoinType.ONE_TO_ONE).withJoinOn(new JoinOn("id", "aId")));
},
"does not match a valid join connection in the instance");
assertValidationFailureReasons(qInstance ->
{
qInstance.addTable(newTable("A", "id")
.withExposedJoin(new ExposedJoin().withJoinTable("B").withLabel("foo").withJoinPath(List.of("AB")))
.withExposedJoin(new ExposedJoin().withJoinTable("C").withLabel("foo").withJoinPath(List.of("AC")))
);
qInstance.addTable(newTable("B", "id", "aId"));
qInstance.addTable(newTable("C", "id", "aId"));
qInstance.addJoin(new QJoinMetaData().withLeftTable("A").withRightTable("B").withName("AB").withType(JoinType.ONE_TO_ONE).withJoinOn(new JoinOn("id", "aId")));
qInstance.addJoin(new QJoinMetaData().withLeftTable("A").withRightTable("C").withName("AC").withType(JoinType.ONE_TO_ONE).withJoinOn(new JoinOn("id", "aId")));
},
"more than one join labeled: foo");
assertValidationFailureReasons(qInstance ->
{
qInstance.addTable(newTable("A", "id")
.withExposedJoin(new ExposedJoin().withJoinTable("B").withLabel("B1").withJoinPath(List.of("AB")))
.withExposedJoin(new ExposedJoin().withJoinTable("B").withLabel("B2").withJoinPath(List.of("AB")))
);
qInstance.addTable(newTable("B", "id", "aId"));
qInstance.addJoin(new QJoinMetaData().withLeftTable("A").withRightTable("B").withName("AB").withType(JoinType.ONE_TO_ONE).withJoinOn(new JoinOn("id", "aId")));
},
"than one join with the joinPath: [AB]");
assertValidationSuccess(qInstance ->
{
qInstance.addTable(newTable("A", "id").withExposedJoin(new ExposedJoin().withJoinTable("B").withLabel("B").withJoinPath(List.of("AB"))));
qInstance.addTable(newTable("B", "id", "aId"));
qInstance.addJoin(new QJoinMetaData().withLeftTable("A").withRightTable("B").withName("AB").withType(JoinType.ONE_TO_ONE).withJoinOn(new JoinOn("id", "aId")));
});
}
/*******************************************************************************
**
*******************************************************************************/
private QTableMetaData newTable(String tableName, String... fieldNames)
{
QTableMetaData tableMetaData = new QTableMetaData()
.withName(tableName)
.withBackendName(TestUtils.DEFAULT_BACKEND_NAME)
.withPrimaryKeyField(fieldNames[0]);
for(String fieldName : fieldNames)
{
tableMetaData.addField(new QFieldMetaData(fieldName, QFieldType.INTEGER));
}
return (tableMetaData);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -0,0 +1,137 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.tables;
import java.util.List;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** Unit test for ExposedJoin
*******************************************************************************/
class ExposedJoinTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void testIsManyOneToOne()
{
QInstance qInstance = QContext.getQInstance();
qInstance.addTable(new QTableMetaData().withName("A").withExposedJoin(new ExposedJoin().withJoinTable("B").withJoinPath(List.of("AB"))));
qInstance.addTable(new QTableMetaData().withName("B").withExposedJoin(new ExposedJoin().withJoinTable("A").withJoinPath(List.of("AB"))));
qInstance.addJoin(new QJoinMetaData().withName("AB").withLeftTable("A").withRightTable("B").withType(JoinType.ONE_TO_ONE));
assertFalse(qInstance.getTable("A").getExposedJoins().get(0).getIsMany());
assertFalse(qInstance.getTable("B").getExposedJoins().get(0).getIsMany());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testIsManyOneToMany()
{
QInstance qInstance = QContext.getQInstance();
qInstance.addTable(new QTableMetaData().withName("A").withExposedJoin(new ExposedJoin().withJoinTable("B").withJoinPath(List.of("AB"))));
qInstance.addTable(new QTableMetaData().withName("B").withExposedJoin(new ExposedJoin().withJoinTable("A").withJoinPath(List.of("AB"))));
qInstance.addJoin(new QJoinMetaData().withName("AB").withLeftTable("A").withRightTable("B").withType(JoinType.ONE_TO_MANY));
assertTrue(qInstance.getTable("A").getExposedJoins().get(0).getIsMany());
assertFalse(qInstance.getTable("B").getExposedJoins().get(0).getIsMany());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testIsManyManyToOne()
{
QInstance qInstance = QContext.getQInstance();
qInstance.addTable(new QTableMetaData().withName("A").withExposedJoin(new ExposedJoin().withJoinTable("B").withJoinPath(List.of("AB"))));
qInstance.addTable(new QTableMetaData().withName("B").withExposedJoin(new ExposedJoin().withJoinTable("A").withJoinPath(List.of("AB"))));
qInstance.addJoin(new QJoinMetaData().withName("AB").withLeftTable("A").withRightTable("B").withType(JoinType.MANY_TO_ONE));
assertFalse(qInstance.getTable("A").getExposedJoins().get(0).getIsMany());
assertTrue(qInstance.getTable("B").getExposedJoins().get(0).getIsMany());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testIsManyOneToOneThroughChain()
{
QInstance qInstance = QContext.getQInstance();
qInstance.addTable(new QTableMetaData().withName("A").withExposedJoin(new ExposedJoin().withJoinTable("G").withJoinPath(List.of("AB", "BC", "CD", "DE", "EF", "FG"))));
qInstance.addTable(new QTableMetaData().withName("B").withExposedJoin(new ExposedJoin().withJoinTable("G").withJoinPath(List.of("BC", "CD", "DE", "EF", "FG"))));
qInstance.addJoin(new QJoinMetaData().withName("AB").withLeftTable("A").withRightTable("B").withType(JoinType.ONE_TO_ONE));
qInstance.addJoin(new QJoinMetaData().withName("BC").withLeftTable("B").withRightTable("C").withType(JoinType.ONE_TO_ONE));
qInstance.addJoin(new QJoinMetaData().withName("CD").withLeftTable("C").withRightTable("D").withType(JoinType.ONE_TO_ONE));
qInstance.addJoin(new QJoinMetaData().withName("DE").withLeftTable("D").withRightTable("E").withType(JoinType.ONE_TO_ONE));
qInstance.addJoin(new QJoinMetaData().withName("EF").withLeftTable("E").withRightTable("F").withType(JoinType.ONE_TO_ONE));
qInstance.addJoin(new QJoinMetaData().withName("FG").withLeftTable("F").withRightTable("G").withType(JoinType.ONE_TO_ONE));
assertFalse(qInstance.getTable("A").getExposedJoins().get(0).getIsMany());
assertFalse(qInstance.getTable("B").getExposedJoins().get(0).getIsMany());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testIsManyOneToManyThroughChain()
{
QInstance qInstance = QContext.getQInstance();
qInstance.addTable(new QTableMetaData().withName("A").withExposedJoin(new ExposedJoin().withJoinTable("G").withJoinPath(List.of("AB", "BC", "CD", "DE", "EF", "FG"))));
qInstance.addTable(new QTableMetaData().withName("B").withExposedJoin(new ExposedJoin().withJoinTable("E").withJoinPath(List.of("BC", "CD", "DE"))));
qInstance.addTable(new QTableMetaData().withName("F").withExposedJoin(new ExposedJoin().withJoinTable("C").withJoinPath(List.of("FG", "EF", "DE", "CD"))));
qInstance.addJoin(new QJoinMetaData().withName("AB").withLeftTable("A").withRightTable("B").withType(JoinType.ONE_TO_ONE));
qInstance.addJoin(new QJoinMetaData().withName("BC").withLeftTable("B").withRightTable("C").withType(JoinType.ONE_TO_ONE));
qInstance.addJoin(new QJoinMetaData().withName("CD").withLeftTable("C").withRightTable("D").withType(JoinType.ONE_TO_ONE));
qInstance.addJoin(new QJoinMetaData().withName("DE").withLeftTable("D").withRightTable("E").withType(JoinType.ONE_TO_ONE));
qInstance.addJoin(new QJoinMetaData().withName("EF").withLeftTable("E").withRightTable("F").withType(JoinType.ONE_TO_MANY));
qInstance.addJoin(new QJoinMetaData().withName("FG").withLeftTable("F").withRightTable("G").withType(JoinType.ONE_TO_ONE));
assertTrue(qInstance.getTable("A").getExposedJoins().get(0).getIsMany());
assertFalse(qInstance.getTable("B").getExposedJoins().get(0).getIsMany());
assertFalse(qInstance.getTable("F").getExposedJoins().get(0).getIsMany());
}
}