Skip to content

feat: allow inviting Data Access Users to Proposal with email#1615

Open
shivoomiess wants to merge 23 commits into
developfrom
SWAP-5408-invite-DAU
Open

feat: allow inviting Data Access Users to Proposal with email#1615
shivoomiess wants to merge 23 commits into
developfrom
SWAP-5408-invite-DAU

Conversation

@shivoomiess

@shivoomiess shivoomiess commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Description

This PR introduces the capability to invite data access users to a proposal by creating a new data access claim.

Motivation and Context

This change is required to allow data access users to be linked directly to proposals, enabling them to view and manipulate data related to the proposal they are invited to. This enhances collaboration and data management in the project.

Changes

  1. A new table 'data_access_claims' is created in the database to store the data access user claims.
  2. The 'DataAccessClaimDataSource' token is added to various configuration files to enable the data access claim functionality across different environments.
  3. The 'DataAccessClaimDataSource' is mapped to 'PostgresDataAccessClaimDataSource' in the dependency configuration files to tie the functionality with the PostgreSQL database.
  4. GraphQL queries and mutations have been added on the frontend and backend for getting / setting DAU email invites.
  5. The read/write path for creating invites has been mirrored from the existing code for Co Proposer Invites and corresponding mutations have been added.
  6. On the frontend Proposal Summary page, we now see invited emails inline for both Co-Proposers and Data Access Users instead of a separate row for only co-proposers.

Visual Changes

The invite pop-ups have been updated to look clean and show the type of proposal invite (Co-Proposer / Data Access).

Previous

Screenshot 2026-07-06 at 15 48 27

Current

Screenshot 2026-07-06 at 16 02 22

How Has This Been Tested?

Adding and Deleting Invited Users

  1. Changes are saved to the DB
Screen.Recording.2026-07-02.at.19.51.10.mov
Screen.Recording.2026-07-02.at.19.51.42.mov
  1. Emails are sent correctly
Screen.Recording.2026-07-02.at.19.53.53.mov

Invite Popups for New Sign-Ups

Screen.Recording.2026-07-06.at.16.15.20.mov

Fixes Jira Issue

https://jira.ess.eu//browse/SWAP-5408

Depends On

Tests included/Docs Updated?

  • I have added tests to cover my changes.
  • All relevant doc has been updated

@shivoomiess
shivoomiess requested a review from a team as a code owner June 30, 2026 14:37
@shivoomiess
shivoomiess requested review from simonfernandes and removed request for a team June 30, 2026 14:37
@shivoomiess shivoomiess changed the title feat: Invite Data Access Users to Proposal feat: allow inviting Data Access Users to Proposal with email Jun 30, 2026

@yoganandaness yoganandaness left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@shivoomiess Kindly attach relevant video or screenshot

Comment thread apps/backend/src/datasources/postgres/records.ts Outdated
Comment thread apps/backend/src/datasources/postgres/DataAccessUsersDataSource.ts Outdated
Comment thread apps/backend/src/datasources/postgres/DataAccessUsersDataSource.ts Outdated
Comment thread apps/backend/src/datasources/DataAccessUsersDataSource.ts

@yoganandaness yoganandaness left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

  1. kindly test all pieces of change
  2. Attach video or pic if possible
  3. Pls have clean diff

Comment thread apps/backend/src/eventHandlers/logging.ts
Comment thread apps/backend/src/eventHandlers/email/essEmailHandler.ts
Comment thread apps/backend/src/mutations/InviteMutations.ts

@jekabs-karklins jekabs-karklins left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Over all looks very good :raisedhands:
As this is a bigger PR I think you will get a bit more comments :).

I have checked the PR and left some comments, and Happy to chat if any help or clarification needed.

Comment thread apps/backend/src/queries/InviteQueries.ts Outdated
Comment thread apps/backend/src/eventHandlers/email/essEmailHandler.ts Outdated
Comment thread apps/backend/src/mutations/InviteMutations.ts Outdated

@shivoomiess shivoomiess left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

incorporated feedback

Comment on lines +86 to 94
try {
await this.processAcceptedRoleClaims(agent.id, invite);
await this.processAcceptedCoProposerClaims(agent.id, invite);
await this.processAcceptedDataAccessClaims(agent.id, invite);
await this.processAcceptedVisitRegistrationClaims(agent.id, invite);
} catch (error) {
logger.logException('Error during claim processing', error);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

breaking: the problem is that catch will capture the error, and flow will continue, and it will mark invite as claimed. We need to re-throw or simply remove the try...catch block all together.
The any exception occurring are handled globally under apps/backend/src/middlewares/graphql.ts see errorLoggingPlugin

Comment on lines +93 to +116
async addDataAccessUser(
proposalPk: number,
userId: number
): Promise<undefined | Rejection> {
try {
await database.transaction(async (trx) => {
const insertData = {
proposal_pk: proposalPk,
user_id: userId,
};

await database('data_access_user_has_proposal')
.insert(insertData)
.onConflict(['proposal_pk', 'user_id'])
.ignore();
});
} catch (error) {
return new Rejection('Failed to add data access user', {
proposalPk,
userId,
error: error instanceof Error ? error.message : String(error),
});
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion: there is no need for transaction here as we only do one database mutation

Suggested change
async addDataAccessUser(
proposalPk: number,
userId: number
): Promise<undefined | Rejection> {
try {
await database.transaction(async (trx) => {
const insertData = {
proposal_pk: proposalPk,
user_id: userId,
};
await database('data_access_user_has_proposal')
.insert(insertData)
.onConflict(['proposal_pk', 'user_id'])
.ignore();
});
} catch (error) {
return new Rejection('Failed to add data access user', {
proposalPk,
userId,
error: error instanceof Error ? error.message : String(error),
});
}
}
async addDataAccessUser(proposalPk: number, userId: number): Promise<undefined | Rejection> {
try {
await database('data_access_user_has_proposal')
.insert({ proposal_pk: proposalPk, user_id: userId })
.onConflict(['proposal_pk', 'user_id'])
.ignore();
} catch (error) {
return new Rejection('Failed to update data access users', {
proposalPk,
userIds,
error: error instanceof Error ? error.message : String(error),
});
}
}

event,
});

return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

breaking: should be continue instead of the return, so that in case the user is missing for one invite we do not skip the rest of the invites. I can see this bug originates by taking the code example from above. Could you also address the issue in the example code?

Comment on lines +205 to +207
if (filter.isExpired) {
query.where('expires_at', '<', new Date());
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

breaking: this currently checks if isExpired is a truthy value then apply the filter.
In VisitMutations we actually use this param to filter out expired invites by specifying { isExpired: false }, which looks right from outside but as this is implemented will skip the check alltogether, meaning you can accept expired invites.
Again I see this originating from the example code, the fix would look something like this

Suggested change
if (filter.isExpired) {
query.where('expires_at', '<', new Date());
}
if (filter.isExpired !== undefined) {
if (filter.isExpired) {
query.where('expires_at', '<', new Date());
} else {
query.where((qb) =>
qb.whereNull('expires_at').orWhere('expires_at', '>=', new Date())
);
}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants