Skip to content

Commit 990bf15

Browse files
committed
refactor: simplify account management by removing address-related methods
- Removed addAddress, removeAddress, and setPrimaryAddress methods from the AccountProvider interface and StalwartAccountProvider implementation to streamline account management. - Updated AccountService to handle address management directly, ensuring proper deletion and linking of addresses during account operations. - Adjusted related unit tests to reflect the changes in address handling and account deletion processes.
1 parent d686303 commit 990bf15

12 files changed

Lines changed: 96 additions & 171 deletions
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use strict';
2+
3+
/** @type {import('sequelize-cli').Migration} */
4+
module.exports = {
5+
async up(queryInterface, Sequelize) {
6+
await queryInterface.addColumn('mail_provider_accounts', 'deleted_at', {
7+
type: Sequelize.DATE,
8+
allowNull: true,
9+
defaultValue: null,
10+
});
11+
},
12+
13+
async down(queryInterface) {
14+
await queryInterface.removeColumn('mail_provider_accounts', 'deleted_at');
15+
},
16+
};

src/modules/account/account-provider.port.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,4 @@ export abstract class AccountProvider {
44
abstract createAccount(params: CreateAccountParams): Promise<void>;
55
abstract deleteAccount(name: string): Promise<void>;
66
abstract getAccount(name: string): Promise<AccountInfo | null>;
7-
abstract addAddress(name: string, address: string): Promise<void>;
8-
abstract removeAddress(name: string, address: string): Promise<void>;
9-
abstract setPrimaryAddress(
10-
currentName: string,
11-
newPrimaryAddress: string,
12-
): Promise<void>;
137
}

src/modules/account/account.service.spec.ts

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -63,16 +63,24 @@ describe('AccountService', () => {
6363
});
6464

6565
describe('deleteAccount', () => {
66-
it('when account has a principal name, then deletes from provider and DB', async () => {
67-
const attrs = newMailAccountAttributes();
68-
const account = MailAccount.build(attrs);
66+
it('when account has addresses, then deletes all principals and account', async () => {
67+
const addr1 = newMailAddressAttributes({ isDefault: true });
68+
const addr2 = newMailAddressAttributes({ isDefault: false });
69+
const account = MailAccount.build(
70+
newMailAccountAttributes({ addresses: [addr1, addr2] }),
71+
);
6972
accounts.findByUserId.mockResolvedValue(account);
7073

71-
await service.deleteAccount(attrs.userId);
74+
await service.deleteAccount(account.userId);
7275

7376
expect(provider.deleteAccount).toHaveBeenCalledWith(
74-
account.providerAccountId,
77+
addr1.providerExternalId,
78+
);
79+
expect(provider.deleteAccount).toHaveBeenCalledWith(
80+
addr2.providerExternalId,
7581
);
82+
expect(addresses.deleteProviderLink).toHaveBeenCalledWith(addr1.id);
83+
expect(addresses.deleteProviderLink).toHaveBeenCalledWith(addr2.id);
7684
expect(accounts.delete).toHaveBeenCalledWith(account.id);
7785
});
7886

@@ -86,7 +94,7 @@ describe('AccountService', () => {
8694
});
8795

8896
describe('addAddress', () => {
89-
it('when all conditions met, then creates address and links provider', async () => {
97+
it('when all conditions met, then creates principal and links provider', async () => {
9098
const accountAttrs = newMailAccountAttributes();
9199
const account = MailAccount.build(accountAttrs);
92100
const domainAttrs = newMailDomainAttributes();
@@ -103,6 +111,8 @@ describe('AccountService', () => {
103111
accountAttrs.userId,
104112
newAddr,
105113
domainAttrs.domain,
114+
'password123',
115+
'Display Name',
106116
);
107117

108118
expect(addresses.create).toHaveBeenCalledWith({
@@ -111,14 +121,16 @@ describe('AccountService', () => {
111121
domainId: domain.id,
112122
isDefault: false,
113123
});
114-
expect(provider.addAddress).toHaveBeenCalledWith(
115-
account.providerAccountId,
116-
newAddr,
117-
);
124+
expect(provider.createAccount).toHaveBeenCalledWith({
125+
accountId: newAddressId,
126+
primaryAddress: newAddr,
127+
displayName: 'Display Name',
128+
password: 'password123',
129+
});
118130
expect(addresses.createProviderLink).toHaveBeenCalledWith({
119131
mailAddressId: newAddressId,
120132
provider: 'stalwart',
121-
externalId: account.providerAccountId,
133+
externalId: newAddr,
122134
});
123135
});
124136

@@ -130,7 +142,7 @@ describe('AccountService', () => {
130142
addresses.findByAddress.mockResolvedValue(null);
131143

132144
await expect(
133-
service.addAddress('unknown', 'a@b.com', 'b.com'),
145+
service.addAddress('unknown', 'a@b.com', 'b.com', 'pass'),
134146
).rejects.toThrow(NotFoundException);
135147
});
136148

@@ -141,7 +153,7 @@ describe('AccountService', () => {
141153
addresses.findByAddress.mockResolvedValue(null);
142154

143155
await expect(
144-
service.addAddress(account.userId, 'a@b.com', 'unknown.com'),
156+
service.addAddress(account.userId, 'a@b.com', 'unknown.com', 'pass'),
145157
).rejects.toThrow(NotFoundException);
146158
});
147159

@@ -155,7 +167,12 @@ describe('AccountService', () => {
155167
addresses.findByAddress.mockResolvedValue(existing);
156168

157169
await expect(
158-
service.addAddress(account.userId, existing.address, domain.domain),
170+
service.addAddress(
171+
account.userId,
172+
existing.address,
173+
domain.domain,
174+
'pass',
175+
),
159176
).rejects.toThrow(ConflictException);
160177
});
161178

@@ -168,10 +185,15 @@ describe('AccountService', () => {
168185
domains.findByDomain.mockResolvedValue(domain);
169186
addresses.findByAddress.mockResolvedValue(null);
170187
addresses.create.mockResolvedValue(newAddressId);
171-
provider.addAddress.mockRejectedValue(new Error('provider down'));
188+
provider.createAccount.mockRejectedValue(new Error('provider down'));
172189

173190
await expect(
174-
service.addAddress(account.userId, 'new@example.com', domain.domain),
191+
service.addAddress(
192+
account.userId,
193+
'new@example.com',
194+
domain.domain,
195+
'pass',
196+
),
175197
).rejects.toThrow('provider down');
176198

177199
expect(addresses.delete).toHaveBeenCalledWith(newAddressId);
@@ -180,7 +202,7 @@ describe('AccountService', () => {
180202
});
181203

182204
describe('removeAddress', () => {
183-
it('when address exists and is not default, then removes it', async () => {
205+
it('when address exists and is not default, then deletes principal and address', async () => {
184206
const nonDefaultAddr = newMailAddressAttributes({ isDefault: false });
185207
const account = MailAccount.build(
186208
newMailAccountAttributes({
@@ -194,9 +216,8 @@ describe('AccountService', () => {
194216

195217
await service.removeAddress(account.userId, nonDefaultAddr.address);
196218

197-
expect(provider.removeAddress).toHaveBeenCalledWith(
198-
account.providerAccountId,
199-
nonDefaultAddr.address,
219+
expect(provider.deleteAccount).toHaveBeenCalledWith(
220+
nonDefaultAddr.providerExternalId,
200221
);
201222
expect(addresses.deleteProviderLink).toHaveBeenCalledWith(
202223
nonDefaultAddr.id,

src/modules/account/account.service.ts

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,12 @@ export class AccountService {
2929
async deleteAccount(userId: string): Promise<void> {
3030
const account = await this.getAccountOrFail(userId);
3131

32-
if (account.providerAccountId) {
33-
await this.provider.deleteAccount(account.providerAccountId);
34-
}
32+
await Promise.all(
33+
account.addresses.map(async (a) => {
34+
await this.provider.deleteAccount(a.providerExternalId);
35+
await this.addresses.deleteProviderLink(a.id);
36+
}),
37+
);
3538

3639
await this.accounts.delete(account.id);
3740
this.logger.log(`Deleted account for user '${userId}'`);
@@ -41,6 +44,8 @@ export class AccountService {
4144
userId: string,
4245
address: string,
4346
domainName: string,
47+
password: string,
48+
displayName?: string,
4449
): Promise<void> {
4550
const [account, domain, existing] = await Promise.all([
4651
this.accounts.findByUserId(userId),
@@ -51,8 +56,6 @@ export class AccountService {
5156
if (!account) {
5257
throw new NotFoundException(`No mail account for user '${userId}'`);
5358
}
54-
const providerAccountId = this.requireProviderAccountId(account);
55-
5659
if (!domain) {
5760
throw new NotFoundException(`Domain '${domainName}' not found`);
5861
}
@@ -68,7 +71,12 @@ export class AccountService {
6871
});
6972

7073
try {
71-
await this.provider.addAddress(providerAccountId, address);
74+
await this.provider.createAccount({
75+
accountId: newAddressId,
76+
primaryAddress: address,
77+
displayName: displayName ?? '',
78+
password,
79+
});
7280
} catch (error) {
7381
await this.addresses.delete(newAddressId);
7482
throw error;
@@ -77,7 +85,7 @@ export class AccountService {
7785
await this.addresses.createProviderLink({
7886
mailAddressId: newAddressId,
7987
provider: 'stalwart',
80-
externalId: providerAccountId,
88+
externalId: address,
8189
});
8290

8391
this.logger.log(`Added address '${address}' to account '${userId}'`);
@@ -99,9 +107,7 @@ export class AccountService {
99107
);
100108
}
101109

102-
const providerAccountId = this.requireProviderAccountId(account);
103-
104-
await this.provider.removeAddress(providerAccountId, address);
110+
await this.provider.deleteAccount(addressRecord.providerExternalId);
105111
await Promise.all([
106112
this.addresses.deleteProviderLink(addressRecord.id),
107113
this.addresses.delete(addressRecord.id),
@@ -138,12 +144,4 @@ export class AccountService {
138144
}
139145
return account;
140146
}
141-
142-
private requireProviderAccountId(account: MailAccount): string {
143-
const id = account.providerAccountId;
144-
if (!id) {
145-
throw new UnprocessableEntityException('Account has no primary address');
146-
}
147-
return id;
148-
}
149147
}

src/modules/account/domain/mail-account.domain.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,4 @@ export class MailAccount {
3030
get defaultAddress(): MailAddress | undefined {
3131
return this.addresses.find((a) => a.isDefault);
3232
}
33-
34-
get providerAccountId(): string | undefined {
35-
return this.defaultAddress?.providerExternalId;
36-
}
3733
}

src/modules/account/domain/mail-address.domain.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export interface MailAddressAttributes {
44
address: string;
55
domainId: string;
66
isDefault: boolean;
7-
providerExternalId: string | null;
7+
providerExternalId: string;
88
createdAt: Date;
99
updatedAt: Date;
1010
}
@@ -15,7 +15,7 @@ export class MailAddress {
1515
readonly address!: string;
1616
readonly domainId!: string;
1717
readonly isDefault!: boolean;
18-
readonly providerExternalId!: string | null;
18+
readonly providerExternalId!: string;
1919
readonly createdAt!: Date;
2020
readonly updatedAt!: Date;
2121

src/modules/account/domain/mail-domain.domain.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
1+
export enum MailDomainStatus {
2+
Active = 'active',
3+
Inactive = 'inactive',
4+
}
5+
16
export interface MailDomainAttributes {
27
id: string;
38
domain: string;
4-
status: string;
9+
status: MailDomainStatus;
510
createdAt: Date;
611
updatedAt: Date;
712
}
813

914
export class MailDomain {
1015
readonly id!: string;
1116
readonly domain!: string;
12-
readonly status!: string;
17+
readonly status!: MailDomainStatus;
1318
readonly createdAt!: Date;
1419
readonly updatedAt!: Date;
1520

src/modules/account/models/mail-provider-account.model.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { MailAddressModel } from './mail-address.model.js';
1515
@Table({
1616
underscored: true,
1717
timestamps: true,
18+
paranoid: true,
1819
tableName: 'mail_provider_accounts',
1920
indexes: [{ unique: true, fields: ['provider', 'external_id'] }],
2021
})
@@ -38,6 +39,9 @@ export class MailProviderAccountModel extends Model {
3839
@Column(DataType.STRING(255))
3940
declare externalId: string;
4041

42+
@Column(DataType.DATE)
43+
declare deletedAt: Date | null;
44+
4145
@BelongsTo(() => MailAddressModel)
4246
declare address: MailAddressModel;
4347
}

src/modules/account/repositories/address.repository.ts

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,18 @@ import { MailProviderAccountModel } from '../models/mail-provider-account.model.
1111
export function toAddressAttributes(
1212
model: MailAddressModel,
1313
): MailAddressAttributes {
14+
const providerExternalId = model.providerAccount?.externalId;
15+
if (!providerExternalId) {
16+
throw new Error(`Address '${model.id}' has no provider link`);
17+
}
18+
1419
return {
1520
id: model.id,
1621
mailAccountId: model.mailAccountId,
1722
address: model.address,
1823
domainId: model.domainId,
1924
isDefault: model.isDefault,
20-
providerExternalId: model.providerAccount?.externalId ?? null,
25+
providerExternalId,
2126
createdAt: model.createdAt as Date,
2227
updatedAt: model.updatedAt as Date,
2328
};
@@ -67,9 +72,9 @@ export class AddressRepository {
6772
address: string;
6873
domainId: string;
6974
isDefault: boolean;
70-
}): Promise<MailAddress> {
75+
}): Promise<string> {
7176
const model = await this.addressModel.create(params);
72-
return MailAddress.build(toAddressAttributes(model));
77+
return model.id;
7378
}
7479

7580
async delete(id: string): Promise<void> {
@@ -94,15 +99,4 @@ export class AddressRepository {
9499
async deleteProviderLink(mailAddressId: string): Promise<void> {
95100
await this.providerAccountModel.destroy({ where: { mailAddressId } });
96101
}
97-
98-
async updateAllProviderExternalIds(
99-
mailAccountId: string,
100-
newExternalId: string,
101-
): Promise<void> {
102-
await this.sequelize.query(
103-
`UPDATE mail_provider_accounts SET external_id = :newExternalId
104-
WHERE mail_address_id IN (SELECT id FROM mail_addresses WHERE mail_account_id = :mailAccountId)`,
105-
{ replacements: { newExternalId, mailAccountId } },
106-
);
107-
}
108102
}

src/modules/account/repositories/domain.repository.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Injectable } from '@nestjs/common';
22
import { InjectModel } from '@nestjs/sequelize';
3-
import { MailDomain } from '../domain/mail-domain.domain.js';
3+
import { MailDomain, MailDomainStatus } from '../domain/mail-domain.domain.js';
44
import { MailDomainModel } from '../models/mail-domain.model.js';
55

66
@Injectable()
@@ -19,7 +19,7 @@ export class DomainRepository {
1919
return MailDomain.build({
2020
id: model.id,
2121
domain: model.domain,
22-
status: model.status,
22+
status: model.status as MailDomainStatus,
2323
createdAt: model.createdAt as Date,
2424
updatedAt: model.updatedAt as Date,
2525
});

0 commit comments

Comments
 (0)