From e162bbc127717867d3c003c8d460e7c819d9366a Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 14 Oct 2025 15:00:06 -0500 Subject: [PATCH] feat(model): add clone method to QTableMEtaData and all composed classes --- src/model/QPossibleValue.ts | 11 ++ src/model/metaData/FieldAdornment.ts | 21 +++ src/model/metaData/QExposedJoin.ts | 21 +++ src/model/metaData/QFieldMetaData.ts | 55 +++++++ src/model/metaData/QHelpContent.ts | 18 +++ src/model/metaData/QJoinMetaData.ts | 11 ++ src/model/metaData/QTableMetaData.ts | 73 +++++++++ src/model/metaData/QTableSection.ts | 21 +++ src/model/query/QFilterCriteria.ts | 14 ++ src/model/query/QFilterOrderBy.ts | 12 ++ src/model/query/QQueryFilter.ts | 43 +++++ tests/model/query/QTableMetaData.test.ts | 193 +++++++++++++++++++++++ 12 files changed, 493 insertions(+) create mode 100644 tests/model/query/QTableMetaData.test.ts diff --git a/src/model/QPossibleValue.ts b/src/model/QPossibleValue.ts index 00115c3..ee3549e 100644 --- a/src/model/QPossibleValue.ts +++ b/src/model/QPossibleValue.ts @@ -34,5 +34,16 @@ export class QPossibleValue this.id = object.id; this.label = object.label; } + + /*************************************************************************** + * + ***************************************************************************/ + public clone(): QPossibleValue + { + const clone = new QPossibleValue({ + ...this + }); + return (clone); + } } diff --git a/src/model/metaData/FieldAdornment.ts b/src/model/metaData/FieldAdornment.ts index 0dca7b7..e868935 100644 --- a/src/model/metaData/FieldAdornment.ts +++ b/src/model/metaData/FieldAdornment.ts @@ -53,4 +53,25 @@ export class FieldAdornment } return (null); } + + /*************************************************************************** + * + ***************************************************************************/ + public clone(): FieldAdornment + { + const valuesClone = this.values ? new Map() : undefined; + if(this.values && valuesClone) + { + this.values.forEach((value, key) => + { + valuesClone.set(key, value); + }); + } + + const clone = new FieldAdornment({ + ...this, + values: valuesClone + }); + return (clone); + } } diff --git a/src/model/metaData/QExposedJoin.ts b/src/model/metaData/QExposedJoin.ts index bab0030..4f31e1d 100644 --- a/src/model/metaData/QExposedJoin.ts +++ b/src/model/metaData/QExposedJoin.ts @@ -45,4 +45,25 @@ export class QExposedJoin this.joinPath.push(new QJoinMetaData(object.joinPath[i])); } } + + /*************************************************************************** + * + ***************************************************************************/ + public clone(): QExposedJoin + { + const joinTableClone = (this.joinTable as any).clone(); + + const joinPathClone: QJoinMetaData[] = []; + for (let joinMetaData of this.joinPath) + { + joinPathClone.push((joinMetaData as any).clone()); + } + + const clone = new QExposedJoin({ + ...this, + joinTable: joinTableClone, + joinPath: joinPathClone + }); + return (clone); + } } diff --git a/src/model/metaData/QFieldMetaData.ts b/src/model/metaData/QFieldMetaData.ts index 06a2476..cb11632 100644 --- a/src/model/metaData/QFieldMetaData.ts +++ b/src/model/metaData/QFieldMetaData.ts @@ -135,4 +135,59 @@ export class QFieldMetaData return (null); } + + /*************************************************************************** + * + ***************************************************************************/ + public clone(): QFieldMetaData + { + const inlinePossibleValueSourceClone: QPossibleValue[] | undefined = this.inlinePossibleValueSource ? [] : undefined; + if(this.inlinePossibleValueSource && inlinePossibleValueSourceClone) + { + for (let qPossibleValue of this.inlinePossibleValueSource) + { + inlinePossibleValueSourceClone.push((qPossibleValue as any).clone()); + } + } + + const adornmentsClone: FieldAdornment[] | undefined = this.adornments ? [] : undefined; + if(this.adornments && adornmentsClone) + { + for (let adornment of this.adornments) + { + adornmentsClone.push((adornment as any).clone()); + } + } + + const helpContentsClone: QHelpContent[] | undefined = this.helpContents ? [] : undefined; + if(this.helpContents && helpContentsClone) + { + for (let helpContent of this.helpContents) + { + helpContentsClone.push((helpContent as any).clone()); + } + } + + const behaviorsClone: any[] | undefined = this.behaviors ? [...this.behaviors] : undefined; + + const supplementalFieldMetaDataClone: Map = new Map(); + this.supplementalFieldMetaData.forEach((value, key) => + { + supplementalFieldMetaDataClone.set(key, value); + }); + + const possibleValueSourceFilterClone = this.possibleValueSourceFilter ? (this.possibleValueSourceFilter as any).clone() : undefined; + + const clone = new QFieldMetaData({ + ...this, + inlinePossibleValueSource: inlinePossibleValueSourceClone, + adornments: adornmentsClone, + helpContents: helpContentsClone, + behaviors: behaviorsClone, + supplementalFieldMetaData: supplementalFieldMetaDataClone, + possibleValueSourceFilter: possibleValueSourceFilterClone + }); + return (clone); + } + } diff --git a/src/model/metaData/QHelpContent.ts b/src/model/metaData/QHelpContent.ts index 02debe6..fd1b1c1 100644 --- a/src/model/metaData/QHelpContent.ts +++ b/src/model/metaData/QHelpContent.ts @@ -105,4 +105,22 @@ export class QHelpContent return (rs); } + /*************************************************************************** + * + ***************************************************************************/ + public clone(): QHelpContent + { + const rolesClone = new Set(); + this.roles.forEach((role) => + { + rolesClone.add(role); + }); + + const clone = new QHelpContent({ + ...this, + roles: rolesClone + }); + return (clone); + } + } diff --git a/src/model/metaData/QJoinMetaData.ts b/src/model/metaData/QJoinMetaData.ts index e1a050e..239b241 100644 --- a/src/model/metaData/QJoinMetaData.ts +++ b/src/model/metaData/QJoinMetaData.ts @@ -37,4 +37,15 @@ export class QJoinMetaData this.leftTable = object.leftTable; this.rightTable = object.rightTable; } + + /*************************************************************************** + * + ***************************************************************************/ + public clone(): QJoinMetaData + { + const clone = new QJoinMetaData({ + ...this + }); + return (clone); + } } diff --git a/src/model/metaData/QTableMetaData.ts b/src/model/metaData/QTableMetaData.ts index 6627df8..c3279b1 100644 --- a/src/model/metaData/QTableMetaData.ts +++ b/src/model/metaData/QTableMetaData.ts @@ -111,4 +111,77 @@ export class QTableMetaData this.helpContent = QHelpContent.buildMap(object.helpContents); } + + + /*************************************************************************** + * + ***************************************************************************/ + public clone(): QTableMetaData + { + const fieldsClone = this.fields ? new Map() : undefined; + if(this.fields && fieldsClone) + { + this.fields.forEach((field, name) => + { + fieldsClone.set(name, field.clone()); + }) + } + + const sectionsClone: QTableSection[] | undefined = this.sections ? [] : undefined; + if(this.sections && sectionsClone) + { + for (let section of this.sections) + { + sectionsClone.push((section as any).clone()); + } + } + + const exposedJoinsClone: QExposedJoin[] | undefined = this.exposedJoins ? [] : undefined; + if(this.exposedJoins && exposedJoinsClone) + { + for (let exposedJoin of this.exposedJoins) + { + exposedJoinsClone.push((exposedJoin as any).clone()); + } + } + + const capabilitiesClone = this.capabilities ? new Set() : undefined; + if(this.capabilities && capabilitiesClone) + { + this.capabilities.forEach((capability) => + { + capabilitiesClone.add(capability); + }); + } + + const helpContentsClone = this.helpContent ? new Map() : undefined; + if(this.helpContent && helpContentsClone) + { + this.helpContent.forEach((helpContentArray, name) => + { + if(helpContentArray) + { + const helpContentArrayClone: QHelpContent[] = []; + for (let qHelpContent of helpContentArray) + { + helpContentArrayClone.push((qHelpContent as any).clone()) + } + helpContentsClone.set(name, helpContentArrayClone); + } + }); + } + + const clone = new QTableMetaData({ + ...this, + fields: fieldsClone, + sections: sectionsClone, + exposedJoins: exposedJoinsClone, + capabilities: capabilitiesClone, + supplementalTableMetaData: new Map(this.supplementalTableMetaData), + helpContent: helpContentsClone + }); + return (clone); + } + + } diff --git a/src/model/metaData/QTableSection.ts b/src/model/metaData/QTableSection.ts index 12009f7..2ef07af 100644 --- a/src/model/metaData/QTableSection.ts +++ b/src/model/metaData/QTableSection.ts @@ -60,4 +60,25 @@ export class QTableSection this.helpContents = QHelpContent.buildArray(object.helpContents) } + /*************************************************************************** + * + ***************************************************************************/ + public clone(): QTableSection + { + const helpContentsClone: QHelpContent[] = (this.helpContents ? [] : undefined) as QHelpContent[]; + if(this.helpContents && helpContentsClone) + { + for (let helpContent of this.helpContents) + { + helpContentsClone.push((helpContent as any).clone()); + } + } + + const clone = new QTableSection({ + ...this, + helpContents: helpContentsClone + }); + return (clone); + } + } diff --git a/src/model/query/QFilterCriteria.ts b/src/model/query/QFilterCriteria.ts index 5c4a82d..39f5fb9 100644 --- a/src/model/query/QFilterCriteria.ts +++ b/src/model/query/QFilterCriteria.ts @@ -38,4 +38,18 @@ export class QFilterCriteria this.operator = operator; this.values = values; } + + /*************************************************************************** + * + ***************************************************************************/ + public clone(): QFilterCriteria + { + const clone = new QFilterCriteria( + this.fieldName, + this.operator, + [...this.values] + ); + clone.otherFieldName = this.otherFieldName; + return (clone); + } } diff --git a/src/model/query/QFilterOrderBy.ts b/src/model/query/QFilterOrderBy.ts index a319dda..1e1a9cd 100644 --- a/src/model/query/QFilterOrderBy.ts +++ b/src/model/query/QFilterOrderBy.ts @@ -33,4 +33,16 @@ export class QFilterOrderBy this.fieldName = fieldName; this.isAscending = isAscending; } + + /*************************************************************************** + * + ***************************************************************************/ + public clone(): QFilterOrderBy + { + const clone = new QFilterOrderBy( + this.fieldName, + this.isAscending + ); + return (clone); + } } diff --git a/src/model/query/QQueryFilter.ts b/src/model/query/QQueryFilter.ts index ec3f8df..3ebc749 100644 --- a/src/model/query/QQueryFilter.ts +++ b/src/model/query/QQueryFilter.ts @@ -107,4 +107,47 @@ export class QQueryFilter } this.subFilters.push(subFilter); } + + /*************************************************************************** + * + ***************************************************************************/ + public clone(): QQueryFilter + { + const criteriaClone: QFilterCriteria[] | undefined = this.criteria ? [] : undefined; + if(this.criteria && criteriaClone) + { + for (let criteria of this.criteria) + { + criteriaClone.push((criteria as any).clone()); + } + } + + const orderBysClone: QFilterOrderBy[] | undefined = this.orderBys ? [] : undefined; + if(this.orderBys && orderBysClone) + { + for (let orderBy of this.orderBys) + { + orderBysClone.push((orderBy as any).clone()); + } + } + + const subFiltersClone: QQueryFilter[] | undefined = this.subFilters ? [] : undefined; + if(this.subFilters && subFiltersClone) + { + for (let subFilter of this.subFilters) + { + subFiltersClone.push((subFilter as any).clone()); + } + } + + const clone = new QQueryFilter( + criteriaClone, + orderBysClone, + subFiltersClone, + this.booleanOperator, + this.skip, + this.limit + ); + return (clone); + } } diff --git a/tests/model/query/QTableMetaData.test.ts b/tests/model/query/QTableMetaData.test.ts new file mode 100644 index 0000000..b211058 --- /dev/null +++ b/tests/model/query/QTableMetaData.test.ts @@ -0,0 +1,193 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +import {QTableMetaData} from "../../../src/model/metaData/QTableMetaData"; + +describe("QTableMetaData tests", () => +{ + it("it should clone", () => + { + ///////////////////////////////////// + // build a table with lots of data // + ///////////////////////////////////// + const table = new QTableMetaData( + { + "name": "myTable", + "label": "My Table", + "isHidden": false, + "primaryKeyField": "id", + "icon": {"name": "link"}, + "fields": { + "id": { + "name": "id", + "label": "Id", + "type": "INTEGER", + "isRequired": false, + "isEditable": true, + "isHeavy": false, + "isHidden": false, + "gridColumns": -1, + "displayFormat": "%s" + }, + "name": { + "name": "name", + "label": "Name", + "type": "STRING", + "isRequired": false, + "isEditable": true, + "isHeavy": false, + "isHidden": false, + "gridColumns": -1, + "displayFormat": "%s", + "maxLength": 100 + }, + "clientId": { + "name": "clientId", + "label": "Client", + "type": "INTEGER", + "isRequired": false, + "isEditable": true, + "isHeavy": false, + "isHidden": false, + "gridColumns": -1, + "possibleValueSourceName": "client", + "displayFormat": "%s", + "adornments": [ + { + "type": "LINK", + "values": { + "toRecordFromTable": "client" + } + } + ] + }, + "apiKey": { + "name": "apiKey", + "label": "API Key", + "type": "PASSWORD", + "isRequired": false, + "isEditable": true, + "isHeavy": false, + "isHidden": false, + "gridColumns": -1, + "displayFormat": "%s", + "adornments": [ + { + "type": "REVEAL" + } + ] + }, + "type": { + "name": "type", + "label": "Type", + "type": "BOOLEAN", + "isRequired": false, + "isEditable": true, + "isHeavy": false, + "isHidden": false, + "gridColumns": -1, + "displayFormat": "%s", + "possibleValueSourceName": "type", + "supplementalFieldMetaData": { + "materialDashboard": { + "formAdjusterIdentifier": "table:myTable;field:type", + "onChangeFormAdjuster": { + "name": "com.qqq.MyTableFormAdjuster", + "codeType": "JAVA" + }, + "type": "materialDashboard" + } + } + }, + }, + "sections": [ + { + "name": "identity", + "label": "Identity", + "tier": "T1", + "fieldNames": ["id", "name"], + "icon": {"name": "badge"}, + "isHidden": false + }, + { + "name": "authentication", + "label": "Authentication", + "tier": "T2", + "fieldNames": ["apiKey",], + "icon": {"name": "edit_attributes"}, + "isHidden": false + }, + ], + "exposedJoins": [], + "supplementalTableMetaData": { + "materialDashboard": { + "onLoadFormAdjuster": { + "name": "com.qqq.MyTableFormAdjuster", + "codeType": "JAVA" + }, + "type": "materialDashboard" + } + }, + "capabilities": [ + "TABLE_COUNT", + "TABLE_GET", + "TABLE_QUERY", + "QUERY_STATS", + "TABLE_INSERT", + "TABLE_UPDATE", + "TABLE_DELETE", + "TABLE_EXPORT" + ], + "readPermission": true, + "insertPermission": true, + "editPermission": true, + "deletePermission": true, + "usesVariants": false, + "iconName": "link" + }); + + //////////////////////////////////////////////////////////////// + // clone it - make a few assertions about data being the same // + //////////////////////////////////////////////////////////////// + const cloneTable = table.clone(); + expect(cloneTable.name).toEqual(table.name); + + expect(cloneTable.sections?.length).toEqual(2); + expect(cloneTable.sections?.length).toEqual(table.sections?.length); + expect(cloneTable.sections?.[0].name).toEqual(table.sections?.[0].name); + + /////////////////////////////////////////////////////////////////////////////////////////// + // change some things in the clone - assert that they are then not equal to the original // + /////////////////////////////////////////////////////////////////////////////////////////// + cloneTable.name = "changed" + expect(cloneTable.name).not.toEqual(table.name); + if(cloneTable.sections) + { + cloneTable.sections[0].name = "changed" + cloneTable.sections[0].isHidden = true; + } + + expect(cloneTable.sections?.[0].name).not.toEqual(table.sections?.[0].name); + expect(cloneTable.sections?.[0].isHidden).not.toEqual(table.sections?.[0].isHidden); + }); + +}); \ No newline at end of file -- 2.54.0