Skip to content

Commit 5513b36

Browse files
fix: unselect orgs under a row lock
1 parent 72a13ad commit 5513b36

4 files changed

Lines changed: 32 additions & 24 deletions

File tree

‎src/extensions/endpoints/sync-github-data/test/index.test.ts‎

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ describe('/sync-github-data endpoint', () => {
1616
const itemsCreateOne = sinon.stub();
1717
const itemsUpdateOne = sinon.stub();
1818
const deleteMany = sinon.stub();
19+
const userRow = sinon.stub();
20+
const trx = sinon.stub().returns({ where: sinon.stub().returnsThis(), forUpdate: sinon.stub().returnsThis(), first: userRow });
1921
const itemsServiceStub = sinon.stub().returns({
2022
readOne,
2123
readByQuery,
@@ -46,7 +48,7 @@ describe('/sync-github-data endpoint', () => {
4648
NotificationsService: notificationsServiceStub,
4749
},
4850
database: {
49-
transaction: async (callback: (trx: unknown) => unknown) => callback({}),
51+
transaction: async (callback: (trx: unknown) => unknown) => callback(trx),
5052
},
5153
getSchema: sinon.stub().resolves({}),
5254
} as unknown as EndpointExtensionContext;
@@ -85,6 +87,7 @@ describe('/sync-github-data endpoint', () => {
8587
readByQuery.resolves([]);
8688
itemsCreateOne.resolves('created-id');
8789
deleteMany.resolves();
90+
userRow.resolves({ selected_orgs: null });
8891

8992
readOne.resolves({
9093
id: 'directus-id',
@@ -282,14 +285,7 @@ describe('/sync-github-data endpoint', () => {
282285
});
283286

284287
it('should drop an org the user left from their selected list', async () => {
285-
readOne.resolves({
286-
id: 'directus-id',
287-
external_identifier: '123456',
288-
github_username: 'new-username',
289-
github_organizations: [ 'jsdelivr' ],
290-
github_oauth_token: 'user-github-token',
291-
selected_orgs: [ 'org-1', 'org-2' ],
292-
});
288+
userRow.resolves({ selected_orgs: JSON.stringify([ 'org-1', 'org-2' ]) });
293289

294290
seedDirectus({
295291
orgs: [],

‎src/extensions/lib/src/sync-orgs.ts‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import type { GithubOrganization } from './github-api-client.js';
66
type User = {
77
id: string;
88
external_identifier: string | null;
9-
selected_orgs?: string[];
109
};
1110

1211
type Org = {
@@ -124,18 +123,21 @@ const removeMemberships = async (user: User, githubOrgs: GithubOrganization[], m
124123
await Promise.all([
125124
// The member's org tokens and approvals are removed by a database trigger.
126125
membersService.deleteMany(removed.map(membership => membership.id)),
127-
unselectOrgs(user, removed.map(membership => membership.org.id), context),
126+
unselectOrgs(user.id, removed.map(membership => membership.org.id), context),
128127
]);
129128
};
130129

131-
export const unselectOrgs = async (user: Pick<User, 'id' | 'selected_orgs'>, removedOrgIds: string[], { services, getSchema }: ApiExtensionContext) => {
132-
const selectedOrgs = user.selected_orgs ?? [];
133-
const remaining = selectedOrgs.filter(orgId => !removedOrgIds.includes(orgId));
130+
export const unselectOrgs = async (userId: string, removedOrgIds: string[], { database, services, getSchema }: ApiExtensionContext) => {
131+
await database.transaction(async (trx) => {
132+
const user = await trx('directus_users').where({ id: userId }).forUpdate().first('selected_orgs');
133+
const selectedOrgs: string[] = JSON.parse(user?.selected_orgs || '[]');
134+
const remaining = selectedOrgs.filter(orgId => !removedOrgIds.includes(orgId));
134135

135-
if (remaining.length === selectedOrgs.length) {
136-
return;
137-
}
136+
if (remaining.length === selectedOrgs.length) {
137+
return;
138+
}
138139

139-
const usersService = new services.UsersService({ schema: await getSchema() });
140-
await usersService.updateOne(user.id, { selected_orgs: remaining }, { emitEvents: false });
140+
const usersService = new services.UsersService({ schema: await getSchema(), knex: trx });
141+
await usersService.updateOne(userId, { selected_orgs: remaining }, { emitEvents: false });
142+
});
141143
};

‎src/extensions/operations/check-members-cron-handler/src/repositories/directus.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { Org } from '../types.js';
44

55
type RemovedMembership = {
66
org: string;
7-
user: { id: string; selected_orgs?: string[] };
7+
user: string;
88
};
99

1010
type Row = {
@@ -57,7 +57,7 @@ export const removeMemberships = async (membershipIds: string[], context: Operat
5757
const { services, getSchema } = context;
5858
const { ItemsService } = services;
5959
const membersService = new ItemsService('gp_org_members', { schema: await getSchema() });
60-
const removed = await membersService.readMany(membershipIds, { fields: [ 'org', 'user.id', 'user.selected_orgs' ] }) as RemovedMembership[];
60+
const removed = await membersService.readMany(membershipIds, { fields: [ 'org', 'user' ] }) as RemovedMembership[];
6161

6262
await membersService.deleteMany(membershipIds);
6363
// The members' org tokens and app approvals are removed by a database trigger.

‎src/extensions/operations/check-members-cron-handler/test/api.test.ts‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ describe('Check org members CRON handler', () => {
3232
query.select = sinon.stub().callsFake((arg: unknown) => typeof arg === 'string' ? query : Promise.resolve(rows));
3333

3434
const database = sinon.stub().returns(query) as any;
35+
const userRow = sinon.stub();
36+
const userQuery = { where: sinon.stub().returnsThis(), forUpdate: sinon.stub().returnsThis(), first: userRow };
37+
const trx = sinon.stub().returns(userQuery);
38+
database.transaction = (callback: (trx: unknown) => unknown) => callback(trx);
3539

3640
const deleteMany = sinon.stub().resolves([]);
3741
const readMany = sinon.stub().resolves([]);
@@ -92,6 +96,7 @@ describe('Check org members CRON handler', () => {
9296
sinon.resetHistory();
9397
deleteMany.resolves([]);
9498
readMany.resolves([]);
99+
userRow.resolves({ selected_orgs: null });
95100
rows = [];
96101
});
97102

@@ -156,19 +161,24 @@ describe('Check org members CRON handler', () => {
156161
rows = [ member(1), member(2) ];
157162
nockSelfCheck('token-1', 200);
158163
nockGraphql({ 1: [ org.orgGithubId ], 2: [ '555' ] });
159-
readMany.resolves([{ org: org.orgId, user: { id: 'user-2', selected_orgs: [ org.orgId, 'other-org' ] } }]);
164+
readMany.resolves([{ org: org.orgId, user: 'user-2' }]);
165+
userRow.resolves({ selected_orgs: JSON.stringify([ org.orgId, 'other-org' ]) });
160166

161167
await operationApi.handler({}, context as any);
162168

163-
expect(readMany.args[0]).to.deep.equal([ [ 'membership-2' ], { fields: [ 'org', 'user.id', 'user.selected_orgs' ] }]);
169+
expect(readMany.args[0]).to.deep.equal([ [ 'membership-2' ], { fields: [ 'org', 'user' ] }]);
170+
expect(userQuery.where.args[0]).to.deep.equal([{ id: 'user-2' }]);
171+
expect(userQuery.forUpdate.callCount).to.equal(1);
172+
expect(services.UsersService.args[0]![0].knex).to.equal(trx);
164173
expect(updateOne.args[0]).to.deep.equal([ 'user-2', { selected_orgs: [ 'other-org' ] }, { emitEvents: false }]);
165174
});
166175

167176
it('should not touch the selected orgs of a member who did not have the org selected', async () => {
168177
rows = [ member(1), member(2) ];
169178
nockSelfCheck('token-1', 200);
170179
nockGraphql({ 1: [ org.orgGithubId ], 2: [ '555' ] });
171-
readMany.resolves([{ org: org.orgId, user: { id: 'user-2', selected_orgs: [ 'other-org' ] } }]);
180+
readMany.resolves([{ org: org.orgId, user: 'user-2' }]);
181+
userRow.resolves({ selected_orgs: JSON.stringify([ 'other-org' ]) });
172182

173183
await operationApi.handler({}, context as any);
174184

0 commit comments

Comments
 (0)