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

merge latest dev before testing #5511

Merged
merged 1 commit into from
Mar 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 0 additions & 1 deletion packages/admin-panel-server/src/@types/express/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
*/
import { AccessPolicy } from '@tupaia/access-policy';
import { TupaiaApiClient } from '@tupaia/api-client';

import { AdminPanelSessionType } from '../../models';

declare global {
Expand Down
6 changes: 6 additions & 0 deletions packages/central-server/examples.http
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,9 @@ Authorization: {{user-authorization}}
GET http://{{host}}/{{version}}/surveyResponses?pageSize=5&columns=["entity.name", "country.name"] HTTP/2.0
content-type: {{contentType}}
Authorization: {{user-authorization}}


### Get socialFeed
GET http://{{host}}/{{version}}/socialFeed?appVersion=1.14.140&page=0&earliestCreationDate=2024-02-19T21:45:16.391Z HTTP/2.0
content-type: {{contentType}}
Authorization: {{user-authorization}}
21 changes: 9 additions & 12 deletions packages/central-server/src/apiV2/meditrakApp/getSocialFeed.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const INTERNAL_EMAIL = ['@beyondessential.com.au', '@bes.au'];

// TODO: Remove as part of RN-502
export const getSocialFeed = async (req, res) => {
const { query, models } = req;
const { query, models, accessPolicy } = req;
const {
countryId,
earliestCreationDate = 0,
Expand All @@ -52,24 +52,21 @@ export const getSocialFeed = async (req, res) => {
};
}

// Fetch an extra record on page 0 to check to see if the page range exceeded the toDate.
const limit = numberPerPage + 1;

if (earliestCreationDate) {
conditions.creation_date = {
comparator: '>',
comparisonValue: new Date(earliestCreationDate),
};
}

const feedItems = await models.feedItem.find(conditions, {
limit,
offset: pageNumber * numberPerPage,
sort: ['creation_date DESC'],
});

const hasMorePages = feedItems.length > numberPerPage;
const items = await Promise.all(feedItems.slice(0, numberPerPage - 1).map(f => f.getData()));
const { items, hasMorePages } = await models.feedItem.findByAccessPolicy(
accessPolicy,
conditions,
{
pageLimit: numberPerPage,
page,
},
);

await intersperseDynamicFeedItems(items, countryId, pageNumber, models);

Expand Down
4 changes: 2 additions & 2 deletions packages/central-server/src/database/models/Survey.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
* Copyright (c) 2017 Beyond Essential Systems Pty Ltd
*/

import { MaterializedViewLogDatabaseModel, SurveyType as BaseSurveyType } from '@tupaia/database';
import { SurveyModel as BaseSurveyModel, SurveyType as BaseSurveyType } from '@tupaia/database';

class SurveyType extends BaseSurveyType {
static meditrakConfig = {
minAppVersion: '0.0.1',
};
}

export class SurveyModel extends MaterializedViewLogDatabaseModel {
export class SurveyModel extends BaseSurveyModel {
notifiers = [onChangeUpdateDataGroup];

get DatabaseTypeClass() {
Expand Down
24 changes: 7 additions & 17 deletions packages/central-server/src/social/feedScraper.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,28 +43,18 @@ const addLatestSurveyFeedItems = async models => {
? latestSurveyFeedItem.creation_date
: minimumDateOfSurveyToAddToFeed;

const publicPermissionGroup = await models.permissionGroup.findOne({ name: 'Public' });
const publicLevelSurveys = await models.survey.find({
permission_group_id: publicPermissionGroup.id,
});
const newSurveys = await getLatestSurveyResponses(models, lastSurveyFeedItemDate, {
survey_id: publicLevelSurveys.map(survey => survey.id),
});
const newSurveyResponses = await getLatestSurveyResponses(models, lastSurveyFeedItemDate);

// Use async for loop instead of Promise.all because this process doesn't block
// user events and should pace itself to avoid overloading the database.
for (let i = 0; i < newSurveys.length; i++) {
for (const surveyResponse of newSurveyResponses) {
try {
const surveyResponse = newSurveys[i];
const { id: surveyResponseId, end_time: endTime } = surveyResponse;

const { name: surveyName } = await surveyResponse.survey();
const {
facilityCode,
facilityName,
geographicalAreaId,
regionName,
} = await getSurveyResponseFacilityData(models, surveyResponse);
const { name: surveyName, permission_group_id: permissionGroupId } =
await surveyResponse.survey();
const { facilityCode, facilityName, geographicalAreaId, regionName } =
await getSurveyResponseFacilityData(models, surveyResponse);
const {
id: countryId,
name: countryName,
Expand Down Expand Up @@ -93,7 +83,7 @@ const addLatestSurveyFeedItems = async models => {
user_id: userId,
country_id: countryId,
geographical_area_id: geographicalAreaId,
permission_group_id: publicPermissionGroup.id,
permission_group_id: permissionGroupId,
creation_date: endTime,
template_variables: {
authorName,
Expand Down
1 change: 1 addition & 0 deletions packages/database/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"check-test-database-exists": "DB_NAME=tupaia_test scripts/checkTestDatabaseExists.sh"
},
"dependencies": {
"@tupaia/access-policy": "workspace:*",
"@tupaia/auth": "workspace:*",
"@tupaia/tsutils": "workspace:*",
"@tupaia/utils": "workspace:*",
Expand Down
97 changes: 94 additions & 3 deletions packages/database/src/modelClasses/FeedItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
*/

import moment from 'moment';

import { AccessPolicy } from '@tupaia/access-policy';
import { FeedItemTypes } from '@tupaia/types';
import { reduceToDictionary } from '@tupaia/utils';
import { DatabaseModel } from '../DatabaseModel';
import { DatabaseType } from '../DatabaseType';
import { TYPES } from '../types';

export const FEED_ITEM_TYPES = ['SurveyResponse', 'markdown'];
import { QUERY_CONJUNCTIONS } from '../TupaiaDatabase';

export class FeedItemType extends DatabaseType {
static databaseType = TYPES.FEED_ITEM;
Expand All @@ -27,4 +28,94 @@ export class FeedItemModel extends DatabaseModel {
get DatabaseTypeClass() {
return FeedItemType;
}

async createAccessPolicyQueryClause(accessPolicy) {
const countryIdsByPermissionGroup = await this.getCountryIdsByPermissionGroup(accessPolicy);
const params = Object.entries(countryIdsByPermissionGroup).flat().flat(); // e.g. ['Public', 'id1', 'id2', 'Admin', 'id3']

return {
sql: `((${Object.entries(countryIdsByPermissionGroup)
.map(([_, countryIds]) => {
return `
(
feed_item.permission_group_id = ? AND
feed_item.country_id IN (${countryIds.map(_ => `?`).join(',')})
)
`;
})
// add the markdown type to the query here so that it always gets wrapped in brackets with the permissions query in the final query, regardless of what other custom conditions are added
.join(' OR ')}) OR feed_item.type = '${FeedItemTypes.Markdown}')`,
parameters: params,
};
}

async getCountryIdsByPermissionGroup(accessPolicy) {
const permissionGroupNames = accessPolicy.getPermissionGroups();

const countries = await this.otherModels.country.find({});

const permissionGroups = await this.otherModels.permissionGroup.find({
name: permissionGroupNames,
});

const countryIdByCode = reduceToDictionary(countries, 'code', 'id');

const permissionGroupIdByName = reduceToDictionary(permissionGroups, 'name', 'id');
return permissionGroupNames.reduce((result, permissionGroupName) => {
const countryCodes = accessPolicy.getEntitiesAllowed(permissionGroupName);
const permissionGroupId = permissionGroupIdByName[permissionGroupName];
const countryIds = countryCodes.map(code => countryIdByCode[code]);
return {
...result,
[permissionGroupId]: countryIds,
};
}, {});
}

/**
*
* @param {AccessPolicy} accessPolicy
* @param {object} customDbConditions
* @param {object} dbOptions
* @param {string[]} [dbOptions.sort]
* @param {number} [dbOptions.pageLimit]
* @param {number} [dbOptions.page]
* @param {string} [dbOptions.joinWith]
* @param {string[]} [dbOptions.joinCondition]
* @param {string} [dbOptions.joinType]
* @returns
*/
async findByAccessPolicy(accessPolicy, customDbConditions = {}, dbOptions = {}) {
const { sort = ['creation_date DESC'], pageLimit = 20, page = 0, ...options } = dbOptions;

// get an extra item to see if there are more pages of results
const limit = pageLimit + 1;
const offset = page * pageLimit;

const permissionsClause = await this.createAccessPolicyQueryClause(accessPolicy);

const feedItems = await this.find(
{
...customDbConditions,
// also limit to the user's country-level permissions, because in some cases we filter the surveys by projectId
[QUERY_CONJUNCTIONS.RAW]: permissionsClause,
},
{
sort,
limit,
offset,
columns: [`${TYPES.FEED_ITEM}.*`],
...options,
},
);

const items = await Promise.all(feedItems.slice(0, pageLimit).map(item => item.getData()));

const hasMorePages = feedItems.length > pageLimit;

return {
hasMorePages,
items,
};
}
}
66 changes: 66 additions & 0 deletions packages/database/src/modelClasses/Survey.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
* Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd
*/

import { reduceToDictionary } from '@tupaia/utils';
import { AccessPolicy } from '@tupaia/access-policy';
import { MaterializedViewLogDatabaseModel } from '../analytics';
import { DatabaseType } from '../DatabaseType';
import { QUERY_CONJUNCTIONS } from '../TupaiaDatabase';
import { TYPES } from '../types';
import { SqlQuery } from '../SqlQuery';

export class SurveyType extends DatabaseType {
static databaseType = TYPES.SURVEY;
Expand Down Expand Up @@ -124,4 +128,66 @@ export class SurveyModel extends MaterializedViewLogDatabaseModel {
get DatabaseTypeClass() {
return SurveyType;
}

async createAccessPolicyQueryClause(accessPolicy) {
const countryIdsByPermissionGroup = await this.getCountryIdsByPermissionGroup(accessPolicy);
const params = Object.entries(countryIdsByPermissionGroup).flat().flat(); // e.g. ['Public', 'id1', 'id2', 'Admin', 'id3']

return {
sql: `(${Object.entries(countryIdsByPermissionGroup)
.map(([_, countryIds]) => {
return `
(
permission_group_id = ? AND
${SqlQuery.array(countryIds, 'TEXT')} && country_ids
)
`;
})
.join(' OR ')})`,
parameters: params,
};
}

async getCountryIdsByPermissionGroup(accessPolicy) {
const permissionGroupNames = accessPolicy.getPermissionGroups();

const countries = await this.otherModels.country.find({});

const permissionGroups = await this.otherModels.permissionGroup.find({
name: permissionGroupNames,
});

const countryIdByCode = reduceToDictionary(countries, 'code', 'id');

const permissionGroupIdByName = reduceToDictionary(permissionGroups, 'name', 'id');
return permissionGroupNames.reduce((result, permissionGroupName) => {
const countryCodes = accessPolicy.getEntitiesAllowed(permissionGroupName);
const permissionGroupId = permissionGroupIdByName[permissionGroupName];
const countryIds = countryCodes.map(code => countryIdByCode[code]);
return {
...result,
[permissionGroupId]: countryIds,
};
}, {});
}

/**
*
* @param {AccessPolicy} accessPolicy
* @param {*} dbConditions
* @param {*} customQueryOptions
* @returns
*/
async findByAccessPolicy(accessPolicy, dbConditions = {}, customQueryOptions) {
const queryClause = await this.createAccessPolicyQueryClause(accessPolicy);

const queryConditions = {
[QUERY_CONJUNCTIONS.RAW]: queryClause,
...dbConditions,
};

const surveys = await this.find(queryConditions, customQueryOptions);

return surveys;
}
}
2 changes: 1 addition & 1 deletion packages/datatrak-web-server/examples.http
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ content-type: {{contentType}}


### Get Activity feed
GET {{host}}/activityFeed HTTP/1.1
GET {{host}}/activityFeed?projectId=64df18f22ecffb2ac901a2b3 HTTP/1.1
content-type: {{contentType}}


Expand Down
5 changes: 1 addition & 4 deletions packages/datatrak-web-server/src/app/createApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,8 @@ const {
const authHandlerProvider = (req: Request) => new SessionSwitchingAuthHandler(req);

export async function createApp() {
const builder = new OrchestratorApiBuilder(new TupaiaDatabase(), 'datatrak-web-server', {
attachModels: true,
})
const builder = new OrchestratorApiBuilder(new TupaiaDatabase(), 'datatrak-web-server')
.useSessionModel(DataTrakSessionModel)

.useAttachSession(attachSessionIfAvailable)
.use('*', attachAccessPolicy)
.attachApiClientToContext(authHandlerProvider)
Expand Down
Loading
Loading