-
Notifications
You must be signed in to change notification settings - Fork 65
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
8f74a8b
commit e3b0dd2
Showing
11 changed files
with
298 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
CREATE TABLE address_books ( | ||
id SERIAL PRIMARY KEY, | ||
data BYTEA NOT NULL, | ||
key BYTEA NOT NULL, | ||
iv BYTEA NOT NULL, | ||
account_id INTEGER NOT NULL, | ||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), | ||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), | ||
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE, | ||
CONSTRAINT unique_account UNIQUE (account_id) | ||
); | ||
|
||
CREATE INDEX idx_address_books_account_id ON address_books(account_id); | ||
|
||
CREATE OR REPLACE TRIGGER update_address_books_updated_at | ||
BEFORE UPDATE ON address_books | ||
FOR EACH ROW | ||
EXECUTE FUNCTION update_updated_at_column(); |
18 changes: 18 additions & 0 deletions
18
src/datasources/accounts/address-books/address-books.datasource.module.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { AddressBooksDatasource } from '@/datasources/accounts/address-books/address-books.datasource'; | ||
import { AddressBookDbMapper } from '@/datasources/accounts/address-books/entities/address-book.db.mapper'; | ||
import { PostgresDatabaseModule } from '@/datasources/db/v1/postgres-database.module'; | ||
import { IAddressBooksDataSource } from '@/domain/interfaces/address-books.datasource.interface'; | ||
import { Module } from '@nestjs/common'; | ||
|
||
@Module({ | ||
imports: [PostgresDatabaseModule], | ||
providers: [ | ||
AddressBookDbMapper, | ||
{ | ||
provide: IAddressBooksDataSource, | ||
useClass: AddressBooksDatasource, | ||
}, | ||
], | ||
exports: [IAddressBooksDataSource], | ||
}) | ||
export class AddressBooksDatasourceModule {} |
95 changes: 95 additions & 0 deletions
95
src/datasources/accounts/address-books/address-books.datasource.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import { TestDbFactory } from '@/__tests__/db.factory'; | ||
import type { IConfigurationService } from '@/config/configuration.service.interface'; | ||
import { AddressBooksDatasource } from '@/datasources/accounts/address-books/address-books.datasource'; | ||
import { AddressBookDbMapper } from '@/datasources/accounts/address-books/entities/address-book.db.mapper'; | ||
import type { EncryptionApiManager } from '@/datasources/accounts/encryption/encryption-api.manager'; | ||
import { LocalEncryptionApiService } from '@/datasources/accounts/encryption/local-encryption-api.service'; | ||
import { FakeCacheService } from '@/datasources/cache/__tests__/fake.cache.service'; | ||
import { CachedQueryResolver } from '@/datasources/db/v1/cached-query-resolver'; | ||
import { PostgresDatabaseMigrator } from '@/datasources/db/v1/postgres-database.migrator'; | ||
import { createAccountDtoBuilder } from '@/domain/accounts/entities/__tests__/create-account.dto.builder'; | ||
import type { Account } from '@/domain/accounts/entities/account.entity'; | ||
import type { ILoggingService } from '@/logging/logging.interface'; | ||
import { faker } from '@faker-js/faker/.'; | ||
import type postgres from 'postgres'; | ||
import { getAddress } from 'viem'; | ||
|
||
const mockLoggingService = { | ||
debug: jest.fn(), | ||
error: jest.fn(), | ||
info: jest.fn(), | ||
warn: jest.fn(), | ||
} as jest.MockedObjectDeep<ILoggingService>; | ||
|
||
const mockConfigurationService = jest.mocked({ | ||
getOrThrow: jest.fn(), | ||
} as jest.MockedObjectDeep<IConfigurationService>); | ||
|
||
const mockEncryptionApiManager = jest.mocked({ | ||
getApi: jest.fn(), | ||
} as jest.MockedObjectDeep<EncryptionApiManager>); | ||
|
||
describe('AddressBooksDataSource', () => { | ||
let fakeCacheService: FakeCacheService; | ||
let sql: postgres.Sql; | ||
let migrator: PostgresDatabaseMigrator; | ||
let target: AddressBooksDatasource; | ||
const testDbFactory = new TestDbFactory(); | ||
|
||
beforeAll(async () => { | ||
fakeCacheService = new FakeCacheService(); | ||
sql = await testDbFactory.createTestDatabase(faker.string.uuid()); | ||
migrator = new PostgresDatabaseMigrator(sql); | ||
await migrator.migrate(); | ||
mockConfigurationService.getOrThrow.mockImplementation((key) => { | ||
if (key === 'expirationTimeInSeconds.default') return faker.number.int(); | ||
if (key === 'application.isProduction') return false; | ||
if (key === 'accounts.encryption.local.algorithm') return 'aes-256-cbc'; | ||
if (key === 'accounts.encryption.local.key') return 'a'.repeat(64); | ||
if (key === 'accounts.encryption.local.iv') return 'b'.repeat(32); | ||
}); | ||
mockEncryptionApiManager.getApi.mockResolvedValue( | ||
new LocalEncryptionApiService(mockConfigurationService), | ||
); | ||
|
||
target = new AddressBooksDatasource( | ||
sql, | ||
new CachedQueryResolver(mockLoggingService, fakeCacheService), | ||
mockEncryptionApiManager, | ||
mockConfigurationService, | ||
new AddressBookDbMapper(), | ||
); | ||
}); | ||
|
||
beforeEach(async () => { | ||
await sql`TRUNCATE TABLE accounts, account_data_settings, address_books CASCADE`; | ||
fakeCacheService.clear(); | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
afterAll(async () => { | ||
await testDbFactory.destroyTestDatabase(sql); | ||
}); | ||
|
||
describe('createAddressBookItem', () => { | ||
it('should create a new address book item', async () => { | ||
const createAccountDto = createAccountDtoBuilder().build(); | ||
const [account] = await sql< | ||
Account[] | ||
>`INSERT INTO accounts (address, name, name_hash) VALUES (${createAccountDto.address}, ${createAccountDto.name}, ${faker.string.alphanumeric(32)}) RETURNING *`; | ||
const addressBookItem = await target.createAddressBookItem({ | ||
account, | ||
createAddressBookItemDto: { | ||
// TODO: builder | ||
name: faker.string.alphanumeric(), | ||
address: getAddress(faker.finance.ethereumAddress()), | ||
}, | ||
}); | ||
expect(addressBookItem).toMatchObject({ | ||
id: expect.any(Number), | ||
data: expect.any(Buffer), // TODO: this should be decrypted | ||
accountId: account.id, | ||
}); // TODO: this should be an item, not the whole address book | ||
}); | ||
}); | ||
}); |
69 changes: 69 additions & 0 deletions
69
src/datasources/accounts/address-books/address-books.datasource.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { IConfigurationService } from '@/config/configuration.service.interface'; | ||
import { AddressBookDbMapper } from '@/datasources/accounts/address-books/entities/address-book.db.mapper'; | ||
import { AddressBook as DbAddressBook } from '@/datasources/accounts/address-books/entities/address-book.entity'; | ||
import { CacheRouter } from '@/datasources/cache/cache.router'; | ||
import { CachedQueryResolver } from '@/datasources/db/v1/cached-query-resolver'; | ||
import { ICachedQueryResolver } from '@/datasources/db/v1/cached-query-resolver.interface'; | ||
import { AddressBook } from '@/domain/accounts/address-books/entities/address-book.entity'; | ||
import { CreateAddressBookItemDto } from '@/domain/accounts/address-books/entities/create-address-book-item.dto.entity'; | ||
import { Account } from '@/domain/accounts/entities/account.entity'; | ||
import { IEncryptionApiManager } from '@/domain/interfaces/encryption-api.manager.interface'; | ||
import { Inject, Injectable } from '@nestjs/common'; | ||
import postgres from 'postgres'; | ||
|
||
@Injectable() | ||
export class AddressBooksDatasource { | ||
private readonly defaultExpirationTimeInSeconds: number; | ||
|
||
constructor( | ||
// @Inject(CacheService) private readonly cacheService: ICacheService, | ||
@Inject('DB_INSTANCE') private readonly sql: postgres.Sql, | ||
@Inject(ICachedQueryResolver) | ||
private readonly cachedQueryResolver: CachedQueryResolver, | ||
@Inject(IEncryptionApiManager) | ||
private readonly encryptionApiManager: IEncryptionApiManager, | ||
// @Inject(LoggingService) private readonly loggingService: ILoggingService, | ||
@Inject(IConfigurationService) | ||
private readonly configurationService: IConfigurationService, | ||
private readonly addressBookMapper: AddressBookDbMapper, | ||
) { | ||
this.defaultExpirationTimeInSeconds = | ||
this.configurationService.getOrThrow<number>( | ||
'expirationTimeInSeconds.default', | ||
); | ||
} | ||
|
||
async createAddressBookItem(args: { | ||
account: Account; | ||
createAddressBookItemDto: CreateAddressBookItemDto; | ||
}): Promise<AddressBook> { | ||
// TODO: return AddressBookItem | ||
const cacheDir = CacheRouter.getAddressBookCacheDir(args.account.id); | ||
const [dbAddressBook] = await this.cachedQueryResolver.get<DbAddressBook[]>( | ||
{ | ||
cacheDir, | ||
query: this.sql<DbAddressBook[]>` | ||
SELECT * FROM address_books WHERE account_id = ${args.account.id} | ||
`, | ||
ttl: this.defaultExpirationTimeInSeconds, | ||
}, | ||
); | ||
if (!dbAddressBook) { | ||
return this.createAddressBook({ account: args.account }); // TODO: return AddressBookItem | ||
} | ||
return this.addressBookMapper.map(dbAddressBook); | ||
} | ||
|
||
private async createAddressBook(args: { | ||
account: Account; | ||
}): Promise<AddressBook> { | ||
const encryptionApi = await this.encryptionApiManager.getApi(); | ||
const encryptedBlob = await encryptionApi.encryptBlob([]); | ||
const [dbAddressBook] = await this.sql<DbAddressBook[]>` | ||
INSERT INTO address_books (data, key, iv, account_id) | ||
VALUES (${encryptedBlob.encryptedData}, ${encryptedBlob.encryptedDataKey}, ${encryptedBlob.iv}, ${args.account.id}) | ||
RETURNING *; | ||
`; | ||
return this.addressBookMapper.map(dbAddressBook); | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
src/datasources/accounts/address-books/entities/address-book.db.mapper.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { AddressBook as DbAddressBook } from '@/datasources/accounts/address-books/entities/address-book.entity'; | ||
import { convertToDate } from '@/datasources/common/utils'; | ||
import { AddressBook } from '@/domain/accounts/address-books/entities/address-book.entity'; | ||
import { Injectable } from '@nestjs/common'; | ||
|
||
@Injectable() | ||
export class AddressBookDbMapper { | ||
map(addressBook: DbAddressBook): AddressBook { | ||
return { | ||
id: addressBook.id, | ||
data: addressBook.data, | ||
accountId: addressBook.account_id, | ||
created_at: convertToDate(addressBook.created_at), | ||
updated_at: convertToDate(addressBook.updated_at), | ||
}; | ||
} | ||
} |
27 changes: 27 additions & 0 deletions
27
src/datasources/accounts/address-books/entities/address-book.entity.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
export class AddressBook { | ||
id: number; | ||
data: object; | ||
key: string; | ||
iv: string; | ||
account_id: number; | ||
created_at: Date; | ||
updated_at: Date; | ||
|
||
constructor( | ||
id: number, | ||
data: object, | ||
key: string, | ||
iv: string, | ||
account_id: number, | ||
created_at: Date, | ||
updated_at: Date, | ||
) { | ||
this.id = id; | ||
this.data = data; | ||
this.key = key; | ||
this.iv = iv; | ||
this.account_id = account_id; | ||
this.created_at = created_at; | ||
this.updated_at = updated_at; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
10 changes: 10 additions & 0 deletions
10
src/domain/accounts/address-books/entities/address-book.entity.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { RowSchema } from '@/datasources/db/v1/entities/row.entity'; | ||
import { AccountSchema } from '@/domain/accounts/entities/account.entity'; | ||
import { z } from 'zod'; | ||
|
||
export type AddressBook = z.infer<typeof AddressBookSchema>; | ||
|
||
export const AddressBookSchema = RowSchema.extend({ | ||
data: z.object({}), | ||
accountId: AccountSchema.shape.id, | ||
}); |
20 changes: 20 additions & 0 deletions
20
src/domain/accounts/address-books/entities/create-address-book-item.dto.entity.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { AccountNameSchema } from '@/domain/accounts/entities/schemas/account-name.schema'; | ||
import { AddressSchema } from '@/validation/entities/schemas/address.schema'; | ||
import { z } from 'zod'; | ||
|
||
export const CreateAddressBookItemDtoSchema = z.object({ | ||
name: AccountNameSchema, | ||
address: AddressSchema, | ||
}); | ||
|
||
export class CreateAddressBookItemDto | ||
implements z.infer<typeof CreateAddressBookItemDtoSchema> | ||
{ | ||
name: string; | ||
address: `0x${string}`; | ||
|
||
constructor(props: CreateAddressBookItemDto) { | ||
this.name = props.name; | ||
this.address = props.address; | ||
} | ||
} |
7 changes: 7 additions & 0 deletions
7
src/domain/accounts/address-books/errors/address-book-not-found.error.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { HttpException, HttpStatus } from '@nestjs/common'; | ||
|
||
export class AddressBookNotFoundError extends HttpException { | ||
constructor() { | ||
super(`Address Book not found`, HttpStatus.NOT_FOUND); | ||
} | ||
} |
12 changes: 12 additions & 0 deletions
12
src/domain/interfaces/address-books.datasource.interface.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import type { AddressBook } from '@/domain/accounts/address-books/entities/address-book.entity'; | ||
import type { CreateAddressBookItemDto } from '@/domain/accounts/address-books/entities/create-address-book-item.dto.entity'; | ||
import type { Account } from '@/domain/accounts/entities/account.entity'; | ||
|
||
export const IAddressBooksDataSource = Symbol('IAddressBooksDataSource'); | ||
|
||
export interface IAddressBooksDataSource { | ||
createAddressBookItem(args: { | ||
account: Account; | ||
createAddressBookItemDto: CreateAddressBookItemDto; | ||
}): Promise<AddressBook>; | ||
} |