Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tweak(adminPanel): RN-1273: Map overlay relations by code #5646

Merged
merged 44 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
6e41add
`child_code` in `MapOverlayGroupRelations.find`
jaskfla May 16, 2024
7dcb145
use `generateInstance`
jaskfla May 16, 2024
bebda01
Merge branch 'dev' into rn-1273-overlay-relations-by-code
jaskfla May 17, 2024
e544e17
comment new methods
jaskfla May 17, 2024
e83030b
reorder a few symbols
jaskfla May 17, 2024
aebf87e
temp: clearer console log
jaskfla May 17, 2024
a3e699f
Merge branch 'dev' into rn-1273-overlay-relations-by-code
jaskfla May 19, 2024
bbc137b
use TSDoc
jaskfla May 19, 2024
b9af765
avoid arrow notation
jaskfla May 19, 2024
641cfa0
consistent param names
jaskfla May 19, 2024
2f9f176
remove console logs
jaskfla May 19, 2024
ccb7850
private remark comment
jaskfla May 19, 2024
395eed3
clearer comment
jaskfla May 19, 2024
6074dad
Merge branch 'dev' into rn-1273-overlay-relations-by-code
jaskfla May 22, 2024
077929f
format JSDoc
jaskfla May 22, 2024
b65b533
update error log
jaskfla May 22, 2024
b895ce2
document `mapOverlayGroupRelations` endpoints
jaskfla May 22, 2024
3454288
use `static joins`
jaskfla May 22, 2024
7f8a589
fix invalid criteria
jaskfla May 23, 2024
506ca1a
restore criteria, fix invalid options?
jaskfla May 23, 2024
b7c1261
Switch to using 'customColumnSelector' for child_code field
rohan-bes May 23, 2024
a77f4ee
remove overriding `find` and `generateInstance`
jaskfla May 23, 2024
fdcd731
Merge branch 'dev' into rn-1273-overlay-relations-by-code
jaskfla May 24, 2024
02b448a
Merge branch 'dev' into rn-1273-overlay-relations-by-code
jaskfla May 26, 2024
5641660
Merge branch 'dev' into rn-1273-overlay-relations-by-code
jaskfla May 26, 2024
0afce39
use `processColumnSelector`
jaskfla May 26, 2024
022dd41
Merge branch 'dev' into rn-1273-overlay-relations-by-code
jaskfla May 26, 2024
bd42c93
adjust inline comment
jaskfla May 27, 2024
8f4fc41
clarifying JSDoc
jaskfla May 27, 2024
de9be76
update comment, use `for`…`of`
jaskfla May 27, 2024
9d42431
Merge branch 'dev' into rn-1273-overlay-relations-by-code
jaskfla May 27, 2024
a7fd2cb
handling for `COALESCE`
jaskfla May 29, 2024
a4ac96e
Merge branch 'dev' into rn-1273-overlay-relations-by-code
jaskfla May 29, 2024
efc435f
fix import
jaskfla May 30, 2024
1ee9789
update Admin Panel columns, edit/create configs
jaskfla May 30, 2024
8ee3ac6
remove console log
jaskfla May 30, 2024
7f4f4e0
Merge branch 'dev' into rn-1273-overlay-relations-by-code
jaskfla May 31, 2024
b1999f2
fix cascading test failures
jaskfla May 31, 2024
2e84b4c
Merge branch 'dev' into rn-1273-overlay-relations-by-code
jaskfla Jun 6, 2024
7256ef1
Merge branch 'dev' into rn-1273-overlay-relations-by-code
jaskfla Jun 13, 2024
8920597
RN-1273: Ensure DatabaseModel find methods use fully qualified field …
rohan-bes Jun 14, 2024
1fb45fe
comment unhandled edge case
jaskfla Jun 17, 2024
74d75dd
Merge branch 'dev' into rn-1273-overlay-relations-by-code
jaskfla Jun 19, 2024
63ae3a3
beefier TSDoc
jaskfla Jun 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,64 +7,92 @@ const RESOURCE_NAME = { singular: 'map overlay group relation' };

export const RELATION_ENDPOINT = 'mapOverlayGroupRelations';

const FIELDS = [
{
Header: 'Map overlay group code',
source: 'map_overlay_group.code',

editConfig: {
optionsEndpoint: 'mapOverlayGroups',
optionLabelKey: 'map_overlay_group.code',
optionValueKey: 'map_overlay_group.id',
sourceKey: 'map_overlay_group_id',
},
const mapOverlayGroupCode = {
Header: 'Map overlay group code',
source: 'map_overlay_group.code',
editConfig: {
optionsEndpoint: 'mapOverlayGroups',
optionLabelKey: 'map_overlay_group.code',
optionValueKey: 'map_overlay_group.id',
sourceKey: 'map_overlay_group_id',
},
{
Header: 'Child ID',
source: 'child_id',
};

editConfig: {
optionsEndpoint: 'mapOverlays',
optionLabelKey: 'mapOverlay.id',
optionValueKey: 'mapOverlay.id',
canCreateNewOptions: true,
sourceKey: 'child_id',
},
const childType = {
Header: 'Child type',
width: 160,
source: 'child_type',
editConfig: {
options: [
{
label: 'Map overlay',
value: 'mapOverlay',
},
{
label: 'Map overlay group',
value: 'mapOverlayGroup',
},
],
},
{
Header: 'Child type',
width: 160,
source: 'child_type',
};

editConfig: {
options: [
{
label: 'Map overlay',
value: 'mapOverlay',
},
{
label: 'Map overlay group',
value: 'mapOverlayGroup',
},
],
},
const childCode = {
Header: 'Child Code',
source: 'child_code',
};

const childMapOverlayCode = {
Header: 'Child map overlay code',
id: 'child_map_overlay_code',
source: 'child_code',
editConfig: {
optionsEndpoint: 'mapOverlays',
optionLabelKey: 'mapOverlay.code',
optionValueKey: 'mapOverlay.id',
sourceKey: 'child_id',
visibilityCriteria: { child_type: 'mapOverlay' },
},
{
Header: 'Sort order',
source: 'sort_order',
};

const childMapOverlayGroupCode = {
Header: 'Child map overlay group code',
id: 'child_map_overlay_group_code',
source: 'child_code',
editConfig: {
optionsEndpoint: 'mapOverlayGroups',
optionLabelKey: 'mapOverlayGroups.code',
optionValueKey: 'mapOverlayGroups.id',
sourceKey: 'child_id',
visibilityCriteria: { child_type: 'mapOverlayGroup' },
},
};

const sortOrder = {
Header: 'Sort order',
source: 'sort_order',
};

const EDIT_FIELDS = [
mapOverlayGroupCode,
childType,
childMapOverlayCode,
childMapOverlayGroupCode,
sortOrder,
];
jaskfla marked this conversation as resolved.
Show resolved Hide resolved

const COLUMNS = [
...FIELDS,
mapOverlayGroupCode,
childType,
childCode,
sortOrder,
{
Header: 'Edit',
type: 'edit',
source: 'id',
actionConfig: {
title: `Edit ${RESOURCE_NAME.singular}`,
editEndpoint: 'mapOverlayGroupRelations',
fields: FIELDS,
fields: EDIT_FIELDS,
},
},
{
Expand All @@ -79,7 +107,7 @@ const COLUMNS = [
const CREATE_CONFIG = {
actionConfig: {
editEndpoint: RELATION_ENDPOINT,
fields: FIELDS,
fields: EDIT_FIELDS,
},
};

Expand Down
18 changes: 18 additions & 0 deletions packages/central-server/examples.http
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,24 @@ GET {{baseUrl}}/export/optionSet/5c625e01f013d60db42a5763 HTTP/1.1
Authorization: {{user-authorization}}
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet

### Get all map overlay groups

GET {{baseUrl}}/mapOverlayGroups
content-type: {{contentType}}
Authorization: {{user-authorization}}

### Get all map overlay group relations

GET {{baseUrl}}/mapOverlayGroupRelations
content-type: {{contentType}}
Authorization: {{user-authorization}}

### Get map overlay group relation by ID

GET {{baseUrl}}/mapOverlayGroupRelations/5f2c7ddb61f76a513a0000bf
content-type: {{contentType}}
Authorization: {{user-authorization}}

### Get survey responses

GET {{baseUrl}}/surveyResponses?pageSize=5&columns=["entity.name", "country.name"] HTTP/2.0
Expand Down
10 changes: 5 additions & 5 deletions packages/central-server/src/apiV2/GETHandler/GETHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { respond } from '@tupaia/utils';
import { CRUDHandler } from '../CRUDHandler';
import {
getQueryOptionsForColumns,
fullyQualifyColumnSelector,
processColumns,
processColumnSelector,
processColumnSelectorKeys,
generateLinkHeader,
} from './helpers';
Expand Down Expand Up @@ -78,14 +78,14 @@ export class GETHandler extends CRUDHandler {
// add any user requested sorting to the start of the sort clause
if (sortString) {
const sortKeys = JSON.parse(sortString);
const fullyQualifiedSortKeys = sortKeys.map(sortKey =>
fullyQualifyColumnSelector(this.models, sortKey, this.recordType),
const processedSortKeys = sortKeys.map(sortKey =>
processColumnSelector(this.models, sortKey, this.recordType),
);
// if 'distinct', we can't order by any columns that aren't included in the distinct selection
if (distinct) {
dbQueryOptions.sort = fullyQualifiedSortKeys;
dbQueryOptions.sort = processedSortKeys;
} else {
dbQueryOptions.sort.unshift(...fullyQualifiedSortKeys);
dbQueryOptions.sort.unshift(...processedSortKeys);
jaskfla marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down
7 changes: 5 additions & 2 deletions packages/central-server/src/apiV2/GETHandler/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ export const generateLinkHeader = (resource, pageString, lastPage, originalQuery
return formatLinkHeader(linkHeader);
};

export const fullyQualifyColumnSelector = (models, unprocessedColumnSelector, baseRecordType) => {
/**
* Used only by {@link processColumnSelector}, so not exported.
*/
const fullyQualifyColumnSelector = (models, unprocessedColumnSelector, baseRecordType) => {
const [resource, column] = unprocessedColumnSelector.includes('.')
? unprocessedColumnSelector.split('.')
: [baseRecordType, unprocessedColumnSelector];
Expand All @@ -55,7 +58,7 @@ export const fullyQualifyColumnSelector = (models, unprocessedColumnSelector, ba
export const processColumnSelectorKeys = (models, object, recordType) => {
const processedObject = {};
Object.entries(object).forEach(([columnSelector, value]) => {
processedObject[fullyQualifyColumnSelector(models, columnSelector, recordType)] = value;
processedObject[processColumnSelector(models, columnSelector, recordType)] = value;
});
return processedObject;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,6 @@ export class GETMapOverlayGroupRelations extends GETHandler {
nearTableKey: 'map_overlay_group_relation.map_overlay_group_id',
farTableKey: 'map_overlay_group.id',
},
map_overlay: {
nearTableKey: 'map_overlay_group_relation.child_id',
farTableKey: 'map_overlay.id',
},
};

async findSingleRecord(mapOverlayGroupRelationId, options) {
Expand All @@ -54,6 +50,17 @@ export class GETMapOverlayGroupRelations extends GETHandler {
return mapOverlayGroupRelation;
}

async getDbQueryOptions() {
const { multiJoin, sort, ...restOfOptions } = await super.getDbQueryOptions();
return {
...restOfOptions,
// Strip table prefix from `child_code` as it’s a `customColumn`
sort: sort.map(s => s.replace('map_overlay_group_relation.child_code', 'child_code')),
// Appending the multi-join from the Record class so that we can fetch the `child_code`
multiJoin: multiJoin.concat(this.models.mapOverlayGroupRelation.DatabaseRecordClass.joins),
};
}

async getPermissionsFilter(criteria, options) {
const dbConditions = await createMapOverlayGroupRelationDBFilter(
this.accessPolicy,
Expand Down
53 changes: 49 additions & 4 deletions packages/database/src/DatabaseModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
import { DatabaseError, reduceToDictionary } from '@tupaia/utils';
import { runDatabaseFunctionInBatches } from './utilities/runDatabaseFunctionInBatches';
import { QUERY_CONJUNCTIONS } from './TupaiaDatabase';

export class DatabaseModel {
otherModels = {};
Expand Down Expand Up @@ -100,16 +101,26 @@ export class DatabaseModel {
// A helper for the 'xById' methods, which disambiguates the id field to ensure joins are handled
getIdClause(id) {
return {
[`${this.databaseRecord}.id`]: id,
[this.fullyQualifyColumn('id')]: id,
};
}

// A helper function to ensure that we're using fully qualified column names to avoid ambiguous references when joins are being used
fullyQualifyColumn(column) {
if (column.includes('.')) {
// Already fully qualified
return column;
}

return `${this.databaseRecord}.${column}`;
}

async getColumnsForQuery() {
// Alias field names to the table to prevent errors when joining other tables
// with same column names.
const fieldNames = await this.fetchFieldNames();
return fieldNames.map(fieldName => {
const qualifiedName = `${this.databaseRecord}.${fieldName}`;
const qualifiedName = this.fullyQualifyColumn(fieldName);
const customSelector = this.customColumnSelectors && this.customColumnSelectors[fieldName];
if (customSelector) {
return { [fieldName]: customSelector(qualifiedName) };
Expand All @@ -136,6 +147,30 @@ export class DatabaseModel {
return { ...options, ...customQueryOptions };
}

async getDbConditions(dbConditions = {}) {
const fieldNames = await this.fetchFieldNames();
const fullyQualifiedConditions = {};

const whereClauses = Object.entries(dbConditions);
for (let i = 0; i < whereClauses.length; i++) {
const [field, value] = whereClauses[i];
if (field === QUERY_CONJUNCTIONS.AND || field === QUERY_CONJUNCTIONS.OR) {
// Recursively proccess AND and OR conditions
fullyQualifiedConditions[field] = await this.getDbConditions(value);
} else if (field === QUERY_CONJUNCTIONS.RAW) {
// Don't touch RAW conditions
fullyQualifiedConditions[field] = value;
} else {
const fullyQualifiedField = fieldNames.includes(field)
? this.fullyQualifyColumn(field)
: field;
fullyQualifiedConditions[fullyQualifiedField] = value;
}
}

return fullyQualifiedConditions;
}

/**
* @param {...any} args
* @returns {Promise<number>} Count of records matching args
Expand Down Expand Up @@ -171,7 +206,12 @@ export class DatabaseModel {

async findOne(dbConditions, customQueryOptions = {}) {
const queryOptions = await this.getQueryOptions(customQueryOptions);
const result = await this.database.findOne(this.databaseRecord, dbConditions, queryOptions);
const processedDbConditions = await this.getDbConditions(dbConditions);
const result = await this.database.findOne(
this.databaseRecord,
processedDbConditions,
queryOptions,
);
if (!result) return null;
return this.generateInstance(result);
}
Expand All @@ -184,7 +224,12 @@ export class DatabaseModel {
*/
async find(dbConditions, customQueryOptions = {}) {
const queryOptions = await this.getQueryOptions(customQueryOptions);
const dbResults = await this.database.find(this.databaseRecord, dbConditions, queryOptions);
const processedDbConditions = await this.getDbConditions(dbConditions);
const dbResults = await this.database.find(
this.databaseRecord,
processedDbConditions,
queryOptions,
);
return Promise.all(dbResults.map(result => this.generateInstance(result)));
}

Expand Down
Loading