From 6a14a23964ba61f9a88aaa1145fbda963636af0d Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Tue, 8 Sep 2026 05:54:56 -0600 Subject: [PATCH 1/3] Use the shared LDK helper for lookup normalization Replaces the copied normalization loop in six ehr_compliancedb trigger scripts. Field lists and error text are unchanged, so behavior is identical; the matching LDK change must be deployed first or the require() fails to compile. --- .../queries/ehr_compliancedb/completiondates.js | 15 ++------------- .../employeerequirementexemptions.js | 15 ++------------- .../queries/ehr_compliancedb/employees.js | 15 ++------------- .../queries/ehr_compliancedb/requirements.js | 15 ++------------- .../ehr_compliancedb/requirementspercategory.js | 15 ++------------- .../ehr_compliancedb/requirementsperemployee.js | 15 ++------------- 6 files changed, 12 insertions(+), 78 deletions(-) diff --git a/EHR_ComplianceDB/resources/queries/ehr_compliancedb/completiondates.js b/EHR_ComplianceDB/resources/queries/ehr_compliancedb/completiondates.js index 5dcf17811..41d824e0a 100644 --- a/EHR_ComplianceDB/resources/queries/ehr_compliancedb/completiondates.js +++ b/EHR_ComplianceDB/resources/queries/ehr_compliancedb/completiondates.js @@ -6,6 +6,7 @@ var console = require("console"); var LABKEY = require("labkey"); +var ldkUtils = require("ldk/Utils").LDK.Server.Utils; var helper = org.labkey.ldk.query.LookupValidationHelper.create(LABKEY.Security.currentContainer.id, LABKEY.Security.currentUser.id, 'ehr_compliancedb', 'completiondates'); @@ -27,17 +28,5 @@ function beforeUpdate(row, oldRow, errors){ } function beforeUpsert(row, errors){ - var lookupFields = ['employeeid', 'requirementname']; - for (var i=0;i Date: Wed, 26 Aug 2026 05:30:47 -0600 Subject: [PATCH 2/3] Normalize employeeid, unit, and category on write in compliance tables employeeperunit was the only table in ehr_compliancedb with no trigger script, so its employeeid, unit, and category values were never canonicalized against their lookups - the reason the ONPRC report procedures had to wrap every unit and category comparison in lower(). Its schema metadata already declares all three columns as lookups. sopdates had no trigger script either. sopid is deliberately left out of its lookup fields: the fk to sops is commented out in ehr_compliancedb.xml, and getLookupValue returns null for a column with no fk, which would reject every row. The lower() removal these pair with has already merged (onprcEHRModules PR 1859), so those reports are exposed to case drift until this deploys. Existing rows carrying off-lookup values need a backfill first, since beforeUpdate revalidates the merged old row and will otherwise block edits that work today. --- .../ehr_compliancedb/employeeperunit.js | 29 ++++++++++++++++++ .../queries/ehr_compliancedb/sopdates.js | 30 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 EHR_ComplianceDB/resources/queries/ehr_compliancedb/employeeperunit.js create mode 100644 EHR_ComplianceDB/resources/queries/ehr_compliancedb/sopdates.js diff --git a/EHR_ComplianceDB/resources/queries/ehr_compliancedb/employeeperunit.js b/EHR_ComplianceDB/resources/queries/ehr_compliancedb/employeeperunit.js new file mode 100644 index 000000000..e5ecdd530 --- /dev/null +++ b/EHR_ComplianceDB/resources/queries/ehr_compliancedb/employeeperunit.js @@ -0,0 +1,29 @@ +/* +* Copyright (c) 2026 LabKey Corporation +* +* Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 +*/ + +var LABKEY = require("labkey"); +var ldkUtils = require("ldk/Utils").LDK.Server.Utils; + +var helper = org.labkey.ldk.query.LookupValidationHelper.create(LABKEY.Security.currentContainer.id, LABKEY.Security.currentUser.id, 'ehr_compliancedb', 'employeeperunit'); + +function beforeInsert(row, errors){ + beforeUpsert(row, errors); +} + +function beforeUpdate(row, oldRow, errors){ + //NOTE: this is designed to merge the old row into the new one. + for (var prop in oldRow){ + if(!row.hasOwnProperty(prop) && LABKEY.ExtAdapter.isDefined(oldRow[prop])){ + row[prop] = oldRow[prop]; + } + } + + beforeUpsert(row, errors); +} + +function beforeUpsert(row, errors){ + ldkUtils.normalizeLookupFields(helper, row, errors, ['employeeid', 'unit', 'category']); +} diff --git a/EHR_ComplianceDB/resources/queries/ehr_compliancedb/sopdates.js b/EHR_ComplianceDB/resources/queries/ehr_compliancedb/sopdates.js new file mode 100644 index 000000000..c611c1660 --- /dev/null +++ b/EHR_ComplianceDB/resources/queries/ehr_compliancedb/sopdates.js @@ -0,0 +1,30 @@ +/* +* Copyright (c) 2026 LabKey Corporation +* +* Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 +*/ + +var LABKEY = require("labkey"); +var ldkUtils = require("ldk/Utils").LDK.Server.Utils; + +var helper = org.labkey.ldk.query.LookupValidationHelper.create(LABKEY.Security.currentContainer.id, LABKEY.Security.currentUser.id, 'ehr_compliancedb', 'sopdates'); + +function beforeInsert(row, errors){ + beforeUpsert(row, errors); +} + +function beforeUpdate(row, oldRow, errors){ + //NOTE: this is designed to merge the old row into the new one. + for (var prop in oldRow){ + if(!row.hasOwnProperty(prop) && LABKEY.ExtAdapter.isDefined(oldRow[prop])){ + row[prop] = oldRow[prop]; + } + } + + beforeUpsert(row, errors); +} + +function beforeUpsert(row, errors){ + //sopid is omitted deliberately: its fk to sops is commented out in ehr_compliancedb.xml, so getLookupValue would return null and reject every row + ldkUtils.normalizeLookupFields(helper, row, errors, ['employeeid']); +} From 5a6a84bfc69ab953a7ae1822616063481405ea6c Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Tue, 8 Sep 2026 10:45:18 -0600 Subject: [PATCH 3/3] Cascade compliance lookup renames into employeeperunit Adds the matching delete guards, so renaming or deleting an employee, unit, or category can no longer strand employeeperunit rows that its new trigger would then reject. --- .../resources/queries/ehr_compliancedb/employeecategory.js | 5 +++++ .../resources/queries/ehr_compliancedb/employees.js | 3 ++- .../resources/queries/ehr_compliancedb/unit_names.js | 3 ++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/EHR_ComplianceDB/resources/queries/ehr_compliancedb/employeecategory.js b/EHR_ComplianceDB/resources/queries/ehr_compliancedb/employeecategory.js index 80e5e380c..9e941a582 100644 --- a/EHR_ComplianceDB/resources/queries/ehr_compliancedb/employeecategory.js +++ b/EHR_ComplianceDB/resources/queries/ehr_compliancedb/employeecategory.js @@ -16,6 +16,7 @@ function afterUpdate(row, oldRow, errors){ helper.cascadeUpdate('ehr_compliancedb', 'employees', 'category', row[fieldName], oldRow[fieldName]); helper.cascadeUpdate('ehr_compliancedb', 'requirementspercategory', 'category', row[fieldName], oldRow[fieldName]); helper.cascadeUpdate('ehr_compliancedb', 'sopbycategory', 'category', row[fieldName], oldRow[fieldName]); + helper.cascadeUpdate('ehr_compliancedb', 'employeeperunit', 'category', row[fieldName], oldRow[fieldName]); } } } @@ -35,6 +36,10 @@ function beforeDelete(row, errors){ if (helper.verifyNotUsed('ehr_compliancedb', 'sopbycategory', 'category', row[fieldName], 'employeecategory')){ addError(errors, fieldName, 'Cannot delete row with value: ' + row[fieldName] + ' because it is referenced by the sopbycategory table'); } + + if (helper.verifyNotUsed('ehr_compliancedb', 'employeeperunit', 'category', row[fieldName], 'employeecategory')){ + addError(errors, fieldName, 'Cannot delete row with value: ' + row[fieldName] + ' because it is referenced by the employeeperunit table'); + } } } diff --git a/EHR_ComplianceDB/resources/queries/ehr_compliancedb/employees.js b/EHR_ComplianceDB/resources/queries/ehr_compliancedb/employees.js index 7ebcd23ff..1a276343a 100644 --- a/EHR_ComplianceDB/resources/queries/ehr_compliancedb/employees.js +++ b/EHR_ComplianceDB/resources/queries/ehr_compliancedb/employees.js @@ -37,6 +37,7 @@ function afterUpdate(row, oldRow, errors){ fieldName = fields[i]; if (row[fieldName] && oldRow[fieldName] && row[fieldName] != oldRow[fieldName]){ helper.cascadeUpdate('ehr_compliancedb', 'employeerequirementexemptions', 'employeeid', row[fieldName], oldRow[fieldName]); + helper.cascadeUpdate('ehr_compliancedb', 'employeeperunit', 'employeeid', row[fieldName], oldRow[fieldName]); helper.cascadeUpdate('ehr_compliancedb', 'requirementsperemployee', 'employeeid', row[fieldName], oldRow[fieldName]); helper.cascadeUpdate('ehr_compliancedb', 'sopdates', 'employeeid', row[fieldName], oldRow[fieldName]); helper.cascadeUpdate('ehr_compliancedb', 'completiondates', 'employeeid', row[fieldName], oldRow[fieldName]); @@ -46,7 +47,7 @@ function afterUpdate(row, oldRow, errors){ } function beforeDelete(row, errors){ - var queries = ['employeerequirementexemptions', 'requirementsperemployee', 'sopdates', 'completiondates'], query; + var queries = ['employeerequirementexemptions', 'employeeperunit', 'requirementsperemployee', 'sopdates', 'completiondates'], query; var fields = ['employeeid'], fieldName; for (var j=0;j