Skip to content
This repository was archived by the owner on Feb 8, 2025. It is now read-only.

Commit 4d80fbe

Browse files
committed
fix(api): linking user with contacts and duplicate risk labels
1 parent 9c26015 commit 4d80fbe

File tree

7 files changed

+51
-40
lines changed

7 files changed

+51
-40
lines changed

api/dbschema/default.esdl

+4-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,10 @@ module default {
9595
required proposal: Proposal {
9696
on target delete delete source;
9797
}
98-
required user: User { default := (<User>(global current_user).id); }
98+
required user: User {
99+
default := (<User>(global current_user).id);
100+
on target delete delete source;
101+
}
99102
required risk: ProposalRisk;
100103

101104
constraint exclusive on ((.proposal, .user));

api/dbschema/edgeql-js/__spec__.ts

+3-3
Large diffs are not rendered by default.

api/dbschema/edgeql-js/modules/default.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -245,9 +245,9 @@ const $CloudShare = $.makeType<$CloudShare>(_.spec, "6060e7a5-9eb1-11ee-a81d-3de
245245
const CloudShare: $.$expr_PathNode<$.TypeSet<$CloudShare, $.Cardinality.Many>, null> = _.syntax.$PathNode($.$toSet($CloudShare, $.Cardinality.Many), null);
246246

247247
export type $ContactλShape = $.typeutil.flatten<_std.$Object_8ce8c71ee4fa5f73840c22d7eaa58588λShape & {
248-
"user": $.LinkDesc<$User, $.Cardinality.One, {}, false, false, false, true>;
249248
"address": $.PropertyDesc<$UAddress, $.Cardinality.One, false, false, false, false>;
250249
"label": $.PropertyDesc<$Label, $.Cardinality.One, false, false, false, false>;
250+
"user": $.LinkDesc<$User, $.Cardinality.One, {}, false, false, false, true>;
251251
"<contact[is Approver]": $.LinkDesc<$Approver, $.Cardinality.Many, {}, false, false, false, false>;
252252
"<contact[is current_approver]": $.LinkDesc<$current_approver, $.Cardinality.Many, {}, false, false, false, false>;
253253
"<contacts[is User]": $.LinkDesc<$User, $.Cardinality.Many, {}, false, false, false, false>;
@@ -442,8 +442,8 @@ const PolicyState: $.$expr_PathNode<$.TypeSet<$PolicyState, $.Cardinality.Many>,
442442

443443
export type $ProposalRiskLabelλShape = $.typeutil.flatten<_std.$Object_8ce8c71ee4fa5f73840c22d7eaa58588λShape & {
444444
"proposal": $.LinkDesc<$Proposal, $.Cardinality.One, {}, false, false, false, false>;
445-
"user": $.LinkDesc<$User, $.Cardinality.One, {}, false, false, false, true>;
446445
"risk": $.PropertyDesc<$ProposalRisk, $.Cardinality.One, false, false, false, false>;
446+
"user": $.LinkDesc<$User, $.Cardinality.One, {}, false, false, false, true>;
447447
}>;
448448
type $ProposalRiskLabel = $.ObjectType<"default::ProposalRiskLabel", $ProposalRiskLabelλShape, null, [
449449
..._std.$Object_8ce8c71ee4fa5f73840c22d7eaa58588['__exclusives__'],
@@ -709,8 +709,8 @@ export type $UserλShape = $.typeutil.flatten<_std.$Object_8ce8c71ee4fa5f73840c2
709709
"<user[is Approver]": $.LinkDesc<$Approver, $.Cardinality.Many, {}, false, false, false, false>;
710710
"<user[is current_approver]": $.LinkDesc<$current_approver, $.Cardinality.Many, {}, false, false, false, false>;
711711
"<user[is Token]": $.LinkDesc<$Token, $.Cardinality.Many, {}, false, false, false, false>;
712-
"<user[is ProposalRiskLabel]": $.LinkDesc<$ProposalRiskLabel, $.Cardinality.Many, {}, false, false, false, false>;
713712
"<user[is Contact]": $.LinkDesc<$Contact, $.Cardinality.Many, {}, false, false, false, false>;
713+
"<user[is ProposalRiskLabel]": $.LinkDesc<$ProposalRiskLabel, $.Cardinality.Many, {}, false, false, false, false>;
714714
"<user": $.LinkDesc<$.ObjectType, $.Cardinality.Many, {}, false, false, false, false>;
715715
}>;
716716
type $User = $.ObjectType<"default::User", $UserλShape, null, [

api/dbschema/interfaces.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,9 @@ export interface CloudShare extends std.$Object {
122122
"share": string;
123123
}
124124
export interface Contact extends std.$Object {
125-
"user": User;
126125
"address": string;
127126
"label": string;
127+
"user": User;
128128
}
129129
export interface Contract extends std.$Object {
130130
"functions": Function[];
@@ -201,8 +201,8 @@ export interface PolicyState extends std.$Object {
201201
export type ProposalRisk = "Low" | "Medium" | "High";
202202
export interface ProposalRiskLabel extends std.$Object {
203203
"proposal": Proposal;
204-
"user": User;
205204
"risk": ProposalRisk;
205+
"user": User;
206206
}
207207
export interface Receipt extends std.$Object {
208208
"responses": string[];

api/dbschema/migrations/00003.edgeql

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
CREATE MIGRATION m1snled4gso2wkpig4ozz776xv3xrfsozyuwqznukjvip2nlvoycpq
2+
ONTO m1jyyy6uam4hnajxp6wy2oi2bhxxgier2ryh6jxlpfplk35qq4dmma
3+
{
4+
ALTER TYPE default::Contact {
5+
ALTER LINK user {
6+
ON TARGET DELETE DELETE SOURCE;
7+
};
8+
};
9+
ALTER TYPE default::ProposalRiskLabel {
10+
ALTER LINK user {
11+
ON TARGET DELETE DELETE SOURCE;
12+
};
13+
};
14+
};

api/dbschema/user.esdl

+4-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ module default {
5454
}
5555

5656
type Contact {
57-
required user: User { default := (<User>(global current_user).id); }
57+
required user: User {
58+
default := (<User>(global current_user).id);
59+
on target delete delete source;
60+
}
5861
required address: UAddress;
5962
required label: Label;
6063

api/src/features/users/users.service.ts

+21-30
Original file line numberDiff line numberDiff line change
@@ -66,53 +66,44 @@ export class UsersService {
6666
}
6767

6868
async link(token: string) {
69-
const [oldUser, secret] = token.split(':');
69+
const [oldId, secret] = token.split(':');
7070

71-
const expectedSecret = await this.redis.get(this.getLinkingTokenKey(oldUser));
71+
const expectedSecret = await this.redis.get(this.getLinkingTokenKey(oldId));
7272
if (secret !== expectedSecret)
7373
throw new UserInputError(`Invalid linking token; token may have expired (1h)`);
7474

75-
const newUser = await this.db.query(
76-
e.assert_exists(e.select(e.global.current_user, () => ({ id: true }))).id,
77-
);
78-
if (oldUser === newUser) return;
75+
const newId = await this.db.query(e.assert_exists(e.select(e.global.current_user.id)));
76+
if (oldId === newId) return;
7977

8078
const approvers = await this.db.DANGEROUS_superuserClient.transaction(async (db) => {
81-
const selectNewUser = e.select(e.User, () => ({ filter_single: { id: newUser } }));
79+
const oldUser = e.select(e.User, () => ({ filter_single: { id: oldId } }));
80+
const newUser = e.select(e.User, () => ({ filter_single: { id: newId } }));
8281

83-
// Pair with the user - merging their approvers into the current user
84-
const approvers = await e
82+
// Merge old user into new user (avoiding exclusivity constraint violations)
83+
const { approvers } = await e
8584
.select({
86-
approvers: e.select(
87-
e.update(e.Approver, (a) => ({
88-
filter: e.op(a.user.id, '=', e.cast(e.uuid, oldUser)),
89-
set: { user: selectNewUser },
90-
})),
91-
() => ({ address: true }),
92-
).address,
93-
oldUserRiskLabels: e.update(e.ProposalRiskLabel, (l) => ({
94-
filter: e.op(
95-
l.user,
96-
'=',
97-
e.select(e.User, () => ({ filter_single: { id: oldUser } })),
98-
),
99-
set: {
100-
user: selectNewUser,
101-
},
85+
approvers: e.update(oldUser.approvers, () => ({ set: { user: newUser } })).address,
86+
contacts: e.update(oldUser.contacts, (c) => ({
87+
filter: e.op(c.label, 'not in', newUser.contacts.label),
88+
set: { user: newUser },
89+
})),
90+
riskLabels: e.update(oldUser['<user[is ProposalRiskLabel]'], (l) => ({
91+
filter: e.op(l.proposal, 'not in', newUser['<user[is ProposalRiskLabel]'].proposal),
92+
set: { user: newUser },
10293
})),
10394
})
104-
.approvers.run(db);
95+
.run(db);
10596

10697
// Delete old user
107-
await e.delete(e.User, () => ({ filter_single: { id: oldUser } })).run(db);
98+
await e.delete(oldUser).run(db);
10899

109100
return approvers;
110101
});
111102

112103
// Remove approver -> user cache
113104
await this.accountsCache.removeApproverUserCache(...(approvers as Address[]));
114105
await Promise.all([
115-
this.pubsub.publish<UserSubscriptionPayload>(getUserTrigger(newUser), {}),
106+
this.pubsub.publish<UserSubscriptionPayload>(getUserTrigger(newId), {}),
116107
...approvers.map((approver) =>
117108
this.pubsub.publish<UserSubscriptionPayload>(
118109
getUserApproverTrigger(approver as Address),
@@ -122,10 +113,10 @@ export class UsersService {
122113
]);
123114

124115
// Remove user -> accounts cache for both old & new user
125-
await this.accountsCache.removeUserAccountsCache(oldUser, newUser);
116+
await this.accountsCache.removeUserAccountsCache(oldId, newId);
126117

127118
// Remove token
128-
await this.redis.del(this.getLinkingTokenKey(oldUser));
119+
await this.redis.del(this.getLinkingTokenKey(oldId));
129120
}
130121

131122
private getLinkingTokenKey(user: uuid) {

0 commit comments

Comments
 (0)