Skip to content

Commit 617e313

Browse files
authored
Improve ExecuteSql endpoint wrapper (#1813)
1 parent afba04a commit 617e313

20 files changed

Lines changed: 545 additions & 679 deletions

File tree

packages/components/package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/components/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@labkey/components",
3-
"version": "6.51.0",
3+
"version": "6.52.0",
44
"description": "Components, models, actions, and utility functions for LabKey applications and pages",
55
"sideEffects": false,
66
"files": [
@@ -50,7 +50,7 @@
5050
"homepage": "https://github.com/LabKey/labkey-ui-components#readme",
5151
"dependencies": {
5252
"@hello-pangea/dnd": "18.0.1",
53-
"@labkey/api": "1.41.2",
53+
"@labkey/api": "1.42.0",
5454
"@testing-library/dom": "~10.4.0",
5555
"@testing-library/jest-dom": "~6.6.3",
5656
"@testing-library/react": "~16.3.0",

packages/components/releaseNotes/components.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# @labkey/components
22
Components, models, actions, and utility functions for LabKey applications and pages
33

4+
### version 6.52.0
5+
*Released*: 24 June 2025
6+
- Improve ExecuteSql endpoint wrapper. See #1813.
7+
48
### version 6.51.0
59
*Released*: 23 June 2025
610
- SampleAliquotViewSelector, GridAliquotViewSelector: Fix props types

packages/components/src/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ import {
245245
NOT_IN_EXP_DESCENDANTS_OF_FILTER_TYPE,
246246
registerFilterType,
247247
} from './internal/query/filter';
248+
import { executeSql } from './internal/query/executeSql';
248249
import { getSelectedRows, selectRows } from './internal/query/selectRows';
249250
import { flattenBrowseDataTreeResponse, loadReports } from './internal/query/reports';
250251
import {
@@ -1304,6 +1305,7 @@ export {
13041305
EntityMoveModal,
13051306
EntityParentType,
13061307
EntityTypeOption,
1308+
executeSql,
13071309
ExpandableContainer,
13081310
ExpandableFilterToggle,
13091311
EXPERIMENT_AUDIT_EVENT,
@@ -1967,6 +1969,13 @@ export type { UseTimeout } from './internal/hooks';
19671969
export type { ModalProps } from './internal/Modal';
19681970
export type { TriggerType } from './internal/OverlayTrigger';
19691971
export type { IImportData, ISelectRowsResult } from './internal/query/api';
1972+
export type {
1973+
ExecuteSqlOptions,
1974+
ExecuteSqlResponse,
1975+
ExecuteSqlResponseBase,
1976+
ExecuteSqlResponseWithoutSession,
1977+
ExecuteSqlResponseWithSession,
1978+
} from './internal/query/executeSql';
19701979
export type { Row, RowValue, SelectRowsOptions, SelectRowsResponse } from './internal/query/selectRows';
19711980
export type { IAttachment } from './internal/renderers/AttachmentCard';
19721981
export type { RequestHandler, RequestOptions } from './internal/request';

packages/components/src/internal/components/domainproperties/actions.ts

Lines changed: 49 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import { handleRequestFailure } from '../../request';
3838

3939
import { getExcludedDataTypeNames } from '../entities/actions';
4040

41-
import { getQueryDetails, selectRowsDeprecated } from '../../query/api';
41+
import { getQueryDetails } from '../../query/api';
4242

4343
import { selectRows } from '../../query/selectRows';
4444

@@ -104,6 +104,7 @@ import {
104104
} from './models';
105105
import { createFormInputId, createFormInputName, getIndexFromId, getNameFromId } from './utils';
106106
import { DomainPropertiesAPIWrapper } from './APIWrapper';
107+
import { executeSql } from '../../query/executeSql';
107108

108109
let sharedCache = Map<string, Promise<any>>();
109110

@@ -248,8 +249,8 @@ export function getRequiredParentTypes(
248249
success: response => {
249250
const domainDetails: DomainDetails = DomainDetails.create(Map(response));
250251
const importAliases: Record<string, IImportAlias> = domainDetails.options?.get('importAliases');
251-
const sampleTypes = [],
252-
dataClasses = [];
252+
const dataClasses = [],
253+
sampleTypes = [];
253254
if (importAliases && Object.keys(importAliases).length > 0) {
254255
Object.values(importAliases).forEach(alias => {
255256
if (alias.required) {
@@ -317,11 +318,11 @@ export function getExcludedSchemaQueryNames(schemaName: string, queryContainerPa
317318
switch (schemaName) {
318319
case 'assay':
319320
return getExcludedDataTypeNames(SCHEMAS.ASSAY_TABLES.ASSAY_LIST, 'AssayDesign', queryContainerPath);
320-
case 'samples':
321-
case 'exp.materials':
322-
return getExcludedDataTypeNames(SCHEMAS.EXP_TABLES.SAMPLE_SETS, 'SampleType', queryContainerPath);
323321
case 'exp.data':
324322
return getExcludedDataTypeNames(SCHEMAS.EXP_TABLES.DATA_CLASSES, 'DataClass', queryContainerPath);
323+
case 'exp.materials':
324+
case 'samples':
325+
return getExcludedDataTypeNames(SCHEMAS.EXP_TABLES.SAMPLE_SETS, 'SampleType', queryContainerPath);
325326
}
326327
return new Promise(resolve => {
327328
resolve([]);
@@ -423,21 +424,21 @@ export function getMaxPhiLevel(containerPath?: string): Promise<string> {
423424
export function getCastStatement(key: string, type: string): string {
424425
const quotedKey = key.replace(/"/g, '""'); // Issue 52608: escape double quotes in key
425426
switch (type) {
426-
case 'INTEGER':
427-
case 'SAMPLE':
428-
case 'USERS':
429-
return `CAST(1 AS INTEGER) AS "${quotedKey}"`;
430-
case 'DOUBLE':
431-
case 'DECIMAL (FLOATING POINT)':
432-
case 'VISITID':
433-
return `CAST(1.1 AS DOUBLE) AS "${quotedKey}"`;
434427
case 'BOOLEAN':
435428
return `CAST(TRUE AS BOOLEAN) AS "${quotedKey}"`;
429+
case 'DATE':
430+
return `CAST(CURDATE() AS DATE) AS "${quotedKey}"`;
436431
case 'DATETIME':
437432
case 'VISITDATE':
438433
return `CAST(CURDATE() AS TIMESTAMP) AS "${quotedKey}"`;
439-
case 'DATE':
440-
return `CAST(CURDATE() AS DATE) AS "${quotedKey}"`;
434+
case 'DECIMAL (FLOATING POINT)':
435+
case 'DOUBLE':
436+
case 'VISITID':
437+
return `CAST(1.1 AS DOUBLE) AS "${quotedKey}"`;
438+
case 'INTEGER':
439+
case 'SAMPLE':
440+
case 'USERS':
441+
return `CAST(1 AS INTEGER) AS "${quotedKey}"`;
441442
case 'TIME':
442443
return `CAST('13:00' AS TIME) AS "${quotedKey}"`;
443444
default:
@@ -461,14 +462,12 @@ export async function parseCalculatedColumn(
461462
expression +
462463
'\nFROM (SELECT ' +
463464
Object.keys(columnMap)
464-
.map(key => {
465-
return getCastStatement(key, columnMap[key]);
466-
})
465+
.map(key => getCastStatement(key, columnMap[key]))
467466
.join(',\n') +
468467
') AS x' +
469468
' WHERE 1=0';
470469

471-
await selectRowsDeprecated({
470+
await executeSql({
472471
schemaName: 'core',
473472
sql,
474473
includeTotalCount: false,
@@ -811,18 +810,9 @@ export function updateDomainField(domain: DomainDesign, change: IFieldChange): D
811810
let newField = isSelection ? field : (field.set('updatedField', true) as DomainField);
812811

813812
switch (type) {
814-
case DOMAIN_FIELD_TYPE:
815-
newField = updateDataType(newField, change.value);
816-
break;
817813
case DOMAIN_FIELD_LOOKUP_CONTAINER:
818814
newField = updateLookup(newField, change.value);
819815
break;
820-
case DOMAIN_FIELD_LOOKUP_SCHEMA:
821-
newField = updateLookup(newField, newField.lookupContainer, change.value);
822-
break;
823-
case DOMAIN_FIELD_SAMPLE_TYPE:
824-
newField = updateSampleField(newField, change.value);
825-
break;
826816
case DOMAIN_FIELD_LOOKUP_QUERY:
827817
const { queryName, rangeURI } = decodeLookup(change.value);
828818
newField = newField.merge({
@@ -833,15 +823,23 @@ export function updateDomainField(domain: DomainDesign, change: IFieldChange): D
833823
lookupIsValid: true,
834824
}) as DomainField;
835825
break;
826+
case DOMAIN_FIELD_LOOKUP_SCHEMA:
827+
newField = updateLookup(newField, newField.lookupContainer, change.value);
828+
break;
836829
case DOMAIN_FIELD_ONTOLOGY_PRINCIPAL_CONCEPT:
837830
const concept = change.value as ConceptModel;
838831
newField = newField.merge({ principalConceptCode: concept?.code }) as DomainField;
839832
break;
833+
case DOMAIN_FIELD_SAMPLE_TYPE:
834+
newField = updateSampleField(newField, change.value);
835+
break;
836+
case DOMAIN_FIELD_TYPE:
837+
newField = updateDataType(newField, change.value);
838+
break;
840839
case DOMAIN_FIELD_NAME:
841840
// Note: it's important to update filter criteria names, because if the field we're updating is new
842841
// we rely on the name when saving. For existing fields we can rely on propertyId.
843842
newField = updateFilterCriteriaNames(newField, change.value);
844-
// eslint-disable-next-line no-fallthrough -- Intentionally falling through here
845843
default:
846844
newField = newField.set(type, change.value) as DomainField;
847845
break;
@@ -1357,12 +1355,14 @@ export function getDomainNamePreviews(
13571355
});
13581356
}
13591357

1358+
type TextChoiceInUseValues = Record<string, { count: number; locked: boolean }>;
1359+
13601360
export async function getTextChoiceInUseValues(
13611361
field: DomainField,
13621362
schemaName: string,
13631363
queryName: string,
13641364
lockedSqlFragment: string
1365-
): Promise<Record<string, any>> {
1365+
): Promise<TextChoiceInUseValues> {
13661366
const containerFilter = Query.ContainerFilter.allInProjectPlusShared; // to account for a shared domain at project or /Shared
13671367
const fieldName = field.original?.name ?? field.name;
13681368

@@ -1376,7 +1376,7 @@ export async function getTextChoiceInUseValues(
13761376
schemaQuery: new SchemaQuery(schemaName, queryName),
13771377
});
13781378

1379-
const values = {};
1379+
const values: TextChoiceInUseValues = {};
13801380
result.rows.forEach(row => {
13811381
const value = row[fieldName]?.value;
13821382
if (isValidTextChoiceValue(value)) {
@@ -1391,33 +1391,25 @@ export async function getTextChoiceInUseValues(
13911391
return values;
13921392
}
13931393

1394-
return new Promise((resolve, reject) => {
1395-
Query.executeSql({
1396-
containerFilter,
1397-
schemaName,
1398-
sql: `SELECT "${fieldName}", ${lockedSqlFragment} AS IsLocked, COUNT(*) AS RowCount FROM "${queryName}" WHERE "${fieldName}" IS NOT NULL GROUP BY "${fieldName}"`,
1399-
success: response => {
1400-
const values = response.rows
1401-
.filter(row => isValidTextChoiceValue(row[fieldName]))
1402-
.reduce((prev, current) => {
1403-
prev[current[fieldName]] = {
1404-
count: current['RowCount'],
1405-
locked: current['IsLocked'] === 1,
1406-
};
1407-
return prev;
1408-
}, {});
1409-
1410-
resolve(values);
1411-
},
1412-
failure: error => {
1413-
console.error('Error fetching distinct values for the text field: ', error);
1414-
reject(error);
1415-
},
1416-
});
1394+
const response = await executeSql({
1395+
containerFilter,
1396+
schemaName,
1397+
sql: `SELECT "${fieldName}", ${lockedSqlFragment} AS IsLocked, COUNT(*) AS RowCount FROM "${queryName}" WHERE "${fieldName}" IS NOT NULL GROUP BY "${fieldName}"`,
14171398
});
1399+
1400+
return response.rows
1401+
.filter(row => isValidTextChoiceValue(row[fieldName].value))
1402+
.reduce<TextChoiceInUseValues>((prev, row) => {
1403+
const value = row[fieldName].value;
1404+
prev[value] = {
1405+
count: row.RowCount.value,
1406+
locked: row.IsLocked.value === 1,
1407+
};
1408+
return prev;
1409+
}, {});
14181410
}
14191411

1420-
export function getGenId(rowId: number, kindName: 'SampleSet' | 'DataClass', containerPath?: string): Promise<number> {
1412+
export function getGenId(rowId: number, kindName: 'DataClass' | 'SampleSet', containerPath?: string): Promise<number> {
14211413
return new Promise((resolve, reject) => {
14221414
Experiment.getEntitySequence({
14231415
containerPath,
@@ -1440,7 +1432,7 @@ export function getGenId(rowId: number, kindName: 'SampleSet' | 'DataClass', con
14401432

14411433
export function setGenId(
14421434
rowId: number,
1443-
kindName: 'SampleSet' | 'DataClass',
1435+
kindName: 'DataClass' | 'SampleSet',
14441436
genId: number,
14451437
containerPath?: string
14461438
): Promise<any> {

packages/components/src/internal/components/domainproperties/dataclasses/actions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,6 @@ function fetchDataClassProperties(rowId: number, containerPath?: string): Promis
103103
});
104104
}
105105

106-
export function deleteDataClass(rowId: number, containerPath?: string, auditUserComment?: string): Promise<any> {
106+
export function deleteDataClass(rowId: number, containerPath?: string, auditUserComment?: string): Promise<void> {
107107
return deleteEntityType('deleteDataClass', rowId, containerPath, auditUserComment);
108108
}

0 commit comments

Comments
 (0)