Reputation: 2688
I'm building an API and would like to isolate the business logic into simple POTO "services" that deal with a single business concern.
The below examples deal with the concept of a user
, which can have an account
, which can have notes
attached.
First a naive example:
class AccountService {
addNote(userId:string, accountId:string, note:string) {
NoteEntity.create({userId, accountId, note})
}
}
This works, but presents a security problem: What if accountId
is not owned by userId
? So this method would allow the creation of a note with invalid ownership, violating my intention to contain business logic in the service layer.
So I add validation:
class AccountService {
addNote(userId:string, accountId:string, note:string) {
validateUserAndAccount(userId, accountId) // <- throws if IDs are not associated
NoteEntity.create({userId, accountId, note})
}
}
Now the method is secure. But that validateUserAndAccount
function call will have to be copy-pasted across all methods, and probably also to other services that also deal with the userId
and accountId
concerns. It'll probably also be quite easy to forget to add that line because validation is not part of what the method really is about.
Can you suggest code-patterns that could extract that kind of validation behaviour, so it sits separate from the method's implementation? I'd like the validation to live in another layer and be composed or mixed into the service layer.
I did an experiment with decorators:
class AccountService {
@validateUserAndAccount() // <- using Reflection and Regex-parsing it validates the signature and does runtime validation of the values
addNote(userId:string, accountId:string, note:string) {
NoteEntity.create({userId, accountId, note})
}
}
This works fairly well, but testing becomes a pain because it's complex to mock out the validation:
const testSvc = new AccountService().addNote("1", "2", "foo")
// Fails until I go create a set of correctly configured entities,
// and allow database connection in the test.
// What was supposed to be a simple POTO service now has hidden database complexities :(
So I think I'm looking for a pattern that preserves the separation between validation and each method's implementation, but also maintains the simplicity of services being "just POTOs" without making testing them super complex.. Any ideas?
Upvotes: 1
Views: 137
Reputation: 33091
maybe Your problem related to structural nature of TS type system.
Please try next approach:
type Nominal<T> = string & { __brand: T };
type UserID = Nominal<'UserID'>;
type AccountId= Nominal<'UserAccountID'>;
type User = {
userId:UserID;
accountId: AccountId;
}
class AccountService {
addNote(userId:UserID, accountId:AccountId) {
// some code
}
}
const service = new AccountService();
const makeUserId = () => '123' as UserID;
const makeAccountId = () => '321' as AccountId;
const handleNote = service.addNote(makeUserId(), makeAccountId())
Now, if You will try to call addNote
method with just simple strings, You will end up with compile error:
service.addNote('some string', 'another string') // compile Error
More insformation You can find here Please let me know if You have any other questions
Upvotes: 2