Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
4cb810e
fix bugs
chris-bes Oct 2, 2025
486c294
fixed
chris-bes Oct 2, 2025
7b161c0
fix
chris-bes Oct 2, 2025
2fa2bc4
fix
chris-bes Oct 2, 2025
f233006
fix more
chris-bes Oct 2, 2025
bdab5cf
fix yarn.lock
chris-bes Oct 2, 2025
c2fa961
fixed
chris-bes Oct 2, 2025
1f01d5f
fixed type
chris-bes Oct 2, 2025
4558f04
fixed test
chris-bes Oct 3, 2025
22cb462
fix test
chris-bes Oct 3, 2025
fb13d03
fixed undefined import
chris-bes Oct 3, 2025
d862469
fix mocked `/getUesr` response
jaskfla Oct 3, 2025
ad87c49
fixed test
chris-bes Oct 3, 2025
8cbd16e
Merge branch 'rn-1545-fix-bugs' of https://github.com/beyondessential…
chris-bes Oct 3, 2025
66358a4
fixed test
chris-bes Oct 3, 2025
450230a
pass models into assertCanSubmit and assertCanImport of SurveyResponse
chris-bes Oct 3, 2025
69fb2f9
Merge branch 'rn-1545-entity-performance' into rn-1545-fix-bugs
chris-bes Oct 3, 2025
3b82aea
removed unused
chris-bes Oct 3, 2025
a3c4ebf
Merge branch 'rn-1545-remove-user-id' into rn-1545-fix-bugs
chris-bes Oct 3, 2025
48c5859
Merge branch 'rn-1545-remove-user-id' into rn-1545-fix-bugs
chris-bes Oct 3, 2025
59f92b4
fixed missing import
chris-bes Oct 3, 2025
8971226
fix permission test
chris-bes Oct 3, 2025
34875ca
fixed undefined
chris-bes Oct 4, 2025
c3cf790
Merge branch 'rn-1545-allow-switching-project' into rn-1545-fix-bugs
chris-bes Oct 5, 2025
872a75e
removed performance
chris-bes Oct 5, 2025
6eac371
Merge branch 'rn-1545-allow-switching-project' into rn-1545-fix-bugs
chris-bes Oct 5, 2025
46f4730
fixed more
chris-bes Oct 5, 2025
503ed8e
fix more bugs
chris-bes Oct 5, 2025
85b4bb3
Merge branch 'rn-1545-allow-switching-project' into rn-1545-fix-bugs
chris-bes Oct 5, 2025
2615578
fixed test
chris-bes Oct 6, 2025
806fd1c
Merge remote-tracking branch 'origin/rn-1545-allow-switching-project'…
jaskfla Oct 8, 2025
73159a3
Merge remote-tracking branch 'origin/rn-1545-allow-switching-project'…
jaskfla Oct 8, 2025
9d83e78
dedupe import
jaskfla Oct 8, 2025
2f714d9
dedupe another import
jaskfla Oct 8, 2025
a3b392f
Merge remote-tracking branch 'origin/rn-1545-allow-switching-project'…
jaskfla Oct 8, 2025
c7dc6af
Merge branch 'rn-1545-allow-switching-project' into rn-1545-fix-bugs
jaskfla Oct 9, 2025
1f6763a
Merge branch 'rn-1545-allow-switching-project' into rn-1545-fix-bugs
jaskfla Oct 9, 2025
50c8e2c
Merge branch 'rn-1545-allow-switching-project' into rn-1545-fix-bugs
jaskfla Oct 10, 2025
8648fbf
Merge branch 'rn-1545-allow-switching-project' into rn-1545-fix-bugs
jaskfla Oct 10, 2025
3aa3cd0
Merge branch 'rn-1545-allow-switching-project' into rn-1545-fix-bugs
jaskfla Oct 12, 2025
d8601d4
Merge branch 'rn-1545-allow-switching-project' into rn-1545-fix-bugs
jaskfla Oct 13, 2025
e9c040f
Merge branch 'rn-1545-allow-switching-project' into rn-1545-fix-bugs
jaskfla Oct 13, 2025
9f48e0d
Merge branch 'rn-1545-allow-switching-project' into rn-1545-fix-bugs
jaskfla Oct 14, 2025
42f8597
Merge branch 'rn-1545-allow-switching-project' into rn-1545-fix-bugs
jaskfla Oct 15, 2025
78aeadb
Merge branch 'rn-1545-allow-switching-project' into rn-1545-fix-bugs
jaskfla Oct 15, 2025
a7ac4c9
Merge branch 'rn-1545-allow-switching-project' into rn-1545-fix-bugs
jaskfla Oct 19, 2025
759be74
addressed reviews
chris-bes Oct 21, 2025
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
2 changes: 1 addition & 1 deletion packages/api-client/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ const productionSubdomains = [
'psss',
'psss-api',
'report-api',
'sync-api',
'entity-api',
'data-table-api',
'sync-api',
'www',
'api', // this must go last in the array, otherwise it will be detected before e.g. admin-api
];
Expand Down
14 changes: 7 additions & 7 deletions packages/central-server/src/permissions/assertions.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import { TUPAIA_ADMIN_PANEL_PERMISSION_GROUP } from '@tupaia/constants';
import { ensure } from '@tupaia/tsutils';
import { PermissionsError } from '@tupaia/utils';
import { TUPAIA_ADMIN_PANEL_PERMISSION_GROUP } from './constants';

/* Re-export for backward compatibility. Prefer importing directly from @tupaia/access-policy. */
export {
allowNoPermissions,
assertAdminPanelAccess,
assertAllPermissions,
assertAnyPermissions,
assertBESAdminAccess,
assertPermissionGroupAccess,
assertPermissionGroupsAccess,
assertVizBuilderAccess,
hasBESAdminAccess,
hasVizBuilderAccess,
hasPermissionGroupAccess,
hasPermissionGroupsAccess,
hasSomePermissionGroupsAccess,
assertBESAdminAccess,
assertVizBuilderAccess,
hasTupaiaAdminPanelAccess,
hasTupaiaAdminPanelAccessToCountry,
assertAdminPanelAccess,
assertPermissionGroupAccess,
assertPermissionGroupsAccess,
hasVizBuilderAccess,
} from '@tupaia/access-policy';

/**
Expand Down
9 changes: 4 additions & 5 deletions packages/database/src/core/modelClasses/Entity.js
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,10 @@ export class EntityRecord extends DatabaseRecord {
export class EntityModel extends MaterializedViewLogDatabaseModel {
static syncDirection = SyncDirections.BIDIRECTIONAL;

get excludedFieldsFromSync() {
return ['point', 'bounds', 'region', 'parent_id'];
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Method Naming Violates JavaScript Conventions

The getter method name ExcludedFieldsFromSync uses PascalCase instead of camelCase, which violates JavaScript naming conventions. Based on the PR discussion, this should be excludedFieldsFromSync.

Fix in Cursor Fix in Web


get DatabaseRecordClass() {
return EntityRecord;
}
Expand Down Expand Up @@ -707,9 +711,4 @@ export class EntityModel extends MaterializedViewLogDatabaseModel {
groupBy: ['entity.id'],
};
}

sanitizeForClient = data => {
const { point, bounds, region, parent_id, ...sanitizedData } = data;
return sanitizedData;
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function getLeaderboardQuery(projectId = '') {
return `
SELECT r.user_id, user_account.first_name, user_account.last_name, r.coconuts, r.pigs
FROM (
SELECT user_id, COUNT(*)::INT as coconuts, FLOOR(COUNT(*) / 100)::INT AS pigs
SELECT user_id, COUNT(*)::INT AS coconuts, FLOOR(COUNT(*) / 100)::INT AS pigs
FROM survey_response
JOIN survey ON survey.id=survey_id
${projectId ? 'WHERE survey.project_id = ?' : ''}
Expand Down
5 changes: 2 additions & 3 deletions packages/database/src/core/sync/buildSyncLookupSelect.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export async function buildSyncLookupSelect(model, columns = {}) {
const attributes = Object.keys(await model.fetchSchema());
const { projectIds } = columns;
const table = model.databaseRecord;
const excludedFields = [...(model.excludedFieldsFromSync || []), ...COLUMNS_EXCLUDED_FROM_SYNC];

return `
SELECT
Expand All @@ -12,9 +13,7 @@ export async function buildSyncLookupSelect(model, columns = {}) {
COALESCE(:updatedAtSyncTick, ${table}.updated_at_sync_tick),
sync_device_tick.device_id,
json_build_object(
${attributes
.filter(a => !COLUMNS_EXCLUDED_FROM_SYNC.includes(a))
.map(a => `'${a}', ${table}.${a}`)}
${attributes.filter(a => !excludedFields.includes(a)).map(a => `'${a}', ${table}.${a}`)}
),
${projectIds || 'NULL'}
`;
Expand Down
70 changes: 67 additions & 3 deletions packages/datatrak-web/jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,77 @@ Object.defineProperty(window, 'matchMedia', {
})),
});

// TODO: Set up database for testing later
const mockModels = {
localSystemFact: {
get: jest.fn().mockResolvedValue('test-device-id'),
set: jest.fn().mockResolvedValue(undefined),
addProjectForSync: jest.fn(),
},
closeDatabaseConnections: jest.fn(),
};

jest.mock('@tupaia/database', () => ({
migrate: jest.fn(),
ModelRegistry: jest.fn().mockImplementation(() => ({})),
}));

jest.mock('./src/database/DatatrakDatabase', () => ({
DatatrakDatabase: jest.fn().mockImplementation(() => ({})),
jest.mock('./src/database/createDatabase', () => ({
createDatabase: jest.fn().mockImplementation(() => ({
models: {
localSystemFact: {
get: jest.fn().mockImplementation(arg => {
if (arg === 'deviceId') {
return 'test-device-id';
}
return undefined;
}),
addProjectForSync: jest.fn(),
},
},
})),
}));

jest.mock('./src/api/CurrentUserContext', () => {
const actual = jest.requireActual('./src/api/CurrentUserContext');

return {
...actual,
useCurrentUserContext: jest.fn(() => ({
...actual.useCurrentUserContext(), // Get the actual return value
accessPolicy: {}, // Override just this property
})),
};
});

// TODO: Set up database for testing later
jest.mock('./src/api/DatabaseContext', () => {
const React = require('react');

return {
DatabaseContext: React.createContext({
models: mockModels,
}),
DatabaseProvider: ({ children }) => children,
useDatabaseContext: () => ({
models: mockModels,
}),
};
});

jest.mock('./src/api/SyncContext', () => {
const React = require('react');

return {
SyncContext: React.createContext({
clientSyncManager: {
triggerSync: jest.fn(),
},
}),
SyncProvider: ({ children }) => children,
useSyncContext: () => ({
clientSyncManager: {
triggerSync: jest.fn(),
},
}),
};
});
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe('Login', () => {
renderPage('/login');
expect(await screen.findByRole('heading', { level: 2 })).toHaveTextContent('Log in');
await doLogin();
server.use(mockUserRequest({ email: '[email protected]' }));
server.use(mockUserRequest({ email: '[email protected]', accessPolicy: [] }));

expect(await screen.findByRole('heading', { level: 1 })).toHaveTextContent(/Select project/i);
});
Expand All @@ -49,7 +49,7 @@ describe('Login', () => {
renderPage('/login');
expect(await screen.findByRole('heading', { level: 2 })).toHaveTextContent('Log in');

server.use(mockUserRequest({ email: '[email protected]', projectId: 'foo' }));
server.use(mockUserRequest({ email: '[email protected]', projectId: 'foo', accessPolicy: [] }));
await doLogin();

await screen.findByText(/Select survey/i);
Expand All @@ -59,7 +59,7 @@ describe('Login', () => {
renderPage('/survey');
expect(await screen.findByRole('heading', { level: 2 })).toHaveTextContent('Log in');

server.use(mockUserRequest({ email: '[email protected]', projectId: 'foo' }));
server.use(mockUserRequest({ email: '[email protected]', projectId: 'foo', accessPolicy: [] }));
await doLogin();

expect(await screen.findByRole('heading', { level: 1 })).toHaveTextContent(/Select survey/i);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ import { handlers } from '../mocks/handlers';
const server = setupServer(
...handlers,
rest.get('*/v1/getUser', (_, res, ctx) => {
return res(ctx.status(200), ctx.json({ name: 'John Smith', email: '[email protected]' }));
return res(
ctx.status(200),
ctx.json({
name: 'John Smith',
email: '[email protected]',
id: '0'.repeat(24),
}),
);
}),
rest.get('*/v1/*', (_, res, ctx) => {
return res(ctx.status(200), ctx.json([]));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import { handlers } from '../../mocks/handlers';
const server = setupServer(
...handlers,
rest.get('*/v1/getUser', (_, res, ctx) => {
return res(ctx.status(200), ctx.json({ name: 'John Smith', email: '[email protected]' }));
return res(
ctx.status(200),
ctx.json({ name: 'John Smith', email: '[email protected]', id: '0'.repeat(24) }),
);
}),
);

Expand Down
6 changes: 6 additions & 0 deletions packages/datatrak-web/src/api/DatabaseContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,18 @@ export const DatabaseProvider = ({ children }: { children: Readonly<React.ReactN
const [models, setModels] = useState<DatatrakWebModelRegistry | null>(null);

useEffect(() => {
let modelsInstance: DatatrakWebModelRegistry | null = null;
const init = async () => {
const { models } = await createDatabase();
modelsInstance = models;
setModels(models);
};

init();

return () => {
modelsInstance?.closeDatabaseConnections();
};
}, []);

if (!models) {
Expand Down
3 changes: 2 additions & 1 deletion packages/datatrak-web/src/api/mutations/useEditUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,13 @@ export const useEditUser = (onSuccess?: () => void) => {
await put('me', { data: updates });
},
{
onSuccess: async (_, variables) => {
onSuccess: (_, variables) => {
queryClient.invalidateQueries(['getUser']);
// If the user changes their project, we need to invalidate the entity descendants query so that recent entities are updated if they change back to the previous project without refreshing the page
if (variables.projectId) {
queryClient.invalidateQueries(['entityDescendants']);
queryClient.invalidateQueries(['tasks']);

models.localSystemFact.addProjectForSync(variables.projectId);
}
onSuccess?.();
Expand Down
4 changes: 2 additions & 2 deletions packages/datatrak-web/src/api/queries/useTask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { getTask } from '../../database';
import { DatatrakWebModelRegistry } from '../../types';
import { get } from '../api';
import { useIsOfflineFirst } from '../offlineFirst';
import { ContextualQueryFunctionContext, useDatabaseQuery } from './useDatabaseQuery';
import { useDatabaseQuery } from './useDatabaseQuery';

export interface UseTaskLocalContext extends ContextualQueryFunctionContext {
export interface UseTaskLocalContext {
models: DatatrakWebModelRegistry;
taskId?: string;
}
Expand Down
13 changes: 12 additions & 1 deletion packages/datatrak-web/src/routes/PrivateRoute.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
import React, { ReactElement } from 'react';
import React, { ReactElement, useEffect } from 'react';
import { Navigate, Outlet, useLocation } from 'react-router-dom';

import { isFeatureEnabled } from '@tupaia/utils';

import { useCurrentUserContext } from '../api';
import { ADMIN_ONLY_ROUTES, ROUTES } from '../constants';
import { isWebApp } from '../utils';
import { useDatabaseContext } from '../hooks/database';

// Reusable wrapper to handle redirecting to login if user is not logged in and the route is private
export const PrivateRoute = ({ children }: { children?: ReactElement }): ReactElement => {
const { isLoggedIn, hasAdminPanelAccess, hideWelcomeScreen, ...user } = useCurrentUserContext();
const { pathname, search } = useLocation();
const { models } = useDatabaseContext();

useEffect(() => {
const addProjectForSync = async () => {
await models.localSystemFact.addProjectForSync(user.projectId);
};
if (isLoggedIn && user.projectId) {
addProjectForSync();
}
}, [models, isLoggedIn, user.projectId]);

if (!isLoggedIn) {
return (
Expand Down
2 changes: 1 addition & 1 deletion packages/datatrak-web/src/sync/ClientSyncManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ export class ClientSyncManager {
pullProgressCallback(records.length);
};

const batchSize = 200;
const batchSize = 10000;
await pullIncomingChanges(this.models, sessionId, batchSize, processStreamedDataFunction);

this.setProgress(this.progressMaxByStage[SYNC_STAGES.PERSIST - 1], 'Saving changes...');
Expand Down
12 changes: 12 additions & 0 deletions packages/server-utils/src/ScheduledTask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export class ScheduledTask {
*/
models: DatabaseInterface;

isRunning = false;

constructor(models: DatabaseInterface, name: string, schedule: string) {
if (!name) {
throw new Error(`ScheduledTask has no name`);
Expand All @@ -84,7 +86,16 @@ export class ScheduledTask {
async runTask() {
this.start = Date.now();

if (this.isRunning) {
const durationMs = Date.now() - this.start;
winston.info(`ScheduledTask: ${this.name}: Not running (previous task still running)`, {
durationMs,
});
return false;
}

try {
this.isRunning = true;
await this.models.wrapInTransaction(async (transactingModels: DatabaseInterface) => {
// Acquire a database advisory lock for the transaction
// Ensures no other server instance can execute its change handler at the same time
Expand All @@ -101,6 +112,7 @@ export class ScheduledTask {
return false;
} finally {
this.start = null;
this.isRunning = false;
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/superset-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"test": "yarn package:test"
},
"dependencies": {
"@tupaia/utils": "workspace:*",
"https-proxy-agent": "^5.0.1",
"node-fetch": "^1.7.3",
"winston": "^3.17.0"
Expand Down
Loading
Loading