Katakanlah saya memiliki aplikasi yang memungkinkan pengguna membuat, menetapkan, dan mengedit tugas. Aplikasi ini dibangun untuk mendukung banyak akun pelanggan. Aplikasi ini memiliki berbagai peran, namun kami akan fokus pada tiga peran dan izinnya relevan dengan tindakan dan sumber daya tertentu (memperbarui teks pada tugas) - admin (memiliki izin untuk memperbarui teks pada semua tugas di semua akun), admin akun (memiliki izin untuk memperbarui teks pada tugas apa pun yang termasuk dalam akun mereka, namun mereka dapat tergabung dalam beberapa akun) dan pengguna akun (memiliki izin untuk memperbarui teks pada tugas hanya jika tugas tersebut diberikan kepada mereka).
Contohnya agak dibuat-buat dan nama perannya agak terlalu umum, tapi bersabarlah.
Tujuannya di sini adalah untuk mencoba menemukan cara yang tepat untuk memisahkan peran dan izin, namun tampaknya peran tersebut pasti terkait dengan izin (lihat kode di bawah).
Mungkin izinnya seharusnya task:updateText
, tapi lalu bagaimana cara memeriksa perannya? Apakah saya akan memindahkan blok switch (actor.type)
saya dalam model domain ke layanan domain dan memeriksa di sana apakah pengguna dikaitkan dengan admin, admin akun, atau pengguna akun pada akun tersebut? Data dapat di-cache, namun admin akun (dan kemungkinan pengguna lain) dapat dikaitkan dengan beberapa akun yang berarti pramuat data ini mungkin memerlukan terlalu banyak data dalam konteksnya dan dapat menimbulkan masalah karena data ini diteruskan antar layanan.
Pemeriksaan kepemilikan/penugasan dilakukan sebagai bagian dari domain karena bergantung pada status model saat ini. Tidak dibahas di sini, namun mekanisme pembuatan versi sederhana digunakan untuk memastikan model tidak berubah antara waktu pengambilan dan saat pembaruan telah diterapkan. Tampaknya kebijakan setidaknya dapat membuat logika ini lebih bersih, tetapi jika saya memindahkan logika ini ke dalam kebijakan, saya tidak yakin bagaimana saya akan terus menjaminnya kecuali kebijakan dan metode layanan memiliki cara untuk menjamin keduanya berbagi versi yang sama. sumber daya.
Apa pilihan saya di sini? Bimbingan apa pun akan sangat dihargai.
class TaskApplicationService {
constructor(private taskRepository: TaskRepository) { }
async updateText({ taskId, text, accountId, context }: { taskId: string, text: string, accountId?: string, context: Context }) {
let actor: Actor;
const userId = context.user.id;
// permissions follow pattern resource:action:qualifier
if (await hasPermission('task:updateText:all')) {
actor = await anAdmin({ userId });
} else if (await hasPermission('task:updateText:account')) {
actor = await anAccountAdmin({ accountId, userId });
} else if (await hasPermission('task.updateText:assigned')) {
actor = await anAccountUser({ accountId, userId });
} else {
throw new Error('not authorized');
}
const task = await this.taskRepository.findOne({ taskId });
task.updateText({ text, actor });
await this.taskRepository.save(task);
// return TaskMapper.toDto(task);
}
}
class TaskDomainModel {
private props: {
text: string,
accountId: string,
assignedAccountUserId: string;
};
get text(): string {
return this.props.text;
}
updateText({ text, actor }: { text: string, actor: Actor }) {
switch (actor.type) {
case ActorType.ADMIN:
break;
case ActorType.ACCOUNT_ADMIN:
assert(this.props.accountId === actor.tenantId);
break;
case ActorType.ACCOUNT_USER:
assert(this.props.accountId === actor.tenantId);
assert(this.props.assignedAccountUserId === actor.tenantUserId);
break;
default:
// note assertions and throwing errors are here for brevity,
// but normally would use something similar to this:
// https://khalilstemmler.com/articles/enterprise-typescript-nodejs/handling-errors-result-class/
throw new Error('unknown actor type');
}
this.props.text = text;
}
}
// supporting cast
interface User {
id: string;
}
interface Context {
user: User;
}
enum ActorType {
ADMIN,
ACCOUNT_ADMIN,
ACCOUNT_USER
}
interface Admin {
type: ActorType.ADMIN,
userId: string
}
interface AccountAdmin {
type: ActorType.ACCOUNT_ADMIN,
tenantId: string,
userId: string
}
interface AccountUser {
type: ActorType.ACCOUNT_USER,
tenantUserId: string,
tenantId: string,
userId: string
}
async function anAdmin({ userId }: { userId: string }): Promise<Admin> {
// gets an admin
}
async function anAccountAdmin({ accountId, userId }: { accountId: string, userId: string }): Promise<AccountAdmin> {
// gets an account admin
}
async function anAccountUser({ accountId, userId }: { accountId: string, userId: string }): Promise<AccountUser> {
// gets an account user
}
async function hasPermission(permission: string) {
// checks permissions in cache or calls to external service
}
type Actor = Admin | AccountAdmin | AccountUser;
interface TaskRepository {
findOne({ taskId }: { taskId: string }): Promise<TaskModel>;
save(task: TaskModel): Promise<TaskModel>;
}