Compare commits
1 Commits
main
...
app-syntro
| Author | SHA1 | Date | |
|---|---|---|---|
| c8bf3a227b |
1
apps/syntropy/domain/README.md
Normal file
1
apps/syntropy/domain/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
# @syntropy/domain
|
||||||
10
apps/syntropy/domain/deno.json
Normal file
10
apps/syntropy/domain/deno.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "@syntropy/domain",
|
||||||
|
"exports": {
|
||||||
|
".": "./index.ts",
|
||||||
|
"./use-cases": "./use-cases.ts"
|
||||||
|
},
|
||||||
|
"imports": {
|
||||||
|
"@fabric/core": "jsr:@fabric/core"
|
||||||
|
}
|
||||||
|
}
|
||||||
0
apps/syntropy/domain/events/index.ts
Normal file
0
apps/syntropy/domain/events/index.ts
Normal file
5
apps/syntropy/domain/index.ts
Normal file
5
apps/syntropy/domain/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from "./events/index.ts";
|
||||||
|
export * from "./models/index.ts";
|
||||||
|
export * from "./security/index.ts";
|
||||||
|
export * from "./services/index.ts";
|
||||||
|
export * from "./use-cases/index.ts";
|
||||||
4
apps/syntropy/domain/models/index.ts
Normal file
4
apps/syntropy/domain/models/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import type { ProjectModel } from "./project.ts";
|
||||||
|
import type { UserModel } from "./user.ts";
|
||||||
|
|
||||||
|
export type DomainModels = UserModel | ProjectModel;
|
||||||
11
apps/syntropy/domain/models/project.ts
Normal file
11
apps/syntropy/domain/models/project.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Field, Model, type ModelToType } from "@fabric/domain";
|
||||||
|
|
||||||
|
export const ProjectModel = Model.aggregateFrom("projects", {
|
||||||
|
name: Field.string(),
|
||||||
|
description: Field.string(),
|
||||||
|
userId: Field.reference({
|
||||||
|
targetModel: "users",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
export type ProjectModel = typeof ProjectModel;
|
||||||
|
export type Project = ModelToType<ProjectModel>;
|
||||||
18
apps/syntropy/domain/models/user.ts
Normal file
18
apps/syntropy/domain/models/user.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Field, Model, type ModelToType } from "@fabric/domain";
|
||||||
|
import type { ReadStateStore } from "../services/state-store.ts";
|
||||||
|
|
||||||
|
export const UserModel = Model.aggregateFrom("users", {
|
||||||
|
email: Field.string(),
|
||||||
|
hashedPassword: Field.string(),
|
||||||
|
role: Field.string(),
|
||||||
|
});
|
||||||
|
export type UserModel = typeof UserModel;
|
||||||
|
export type User = ModelToType<UserModel>;
|
||||||
|
|
||||||
|
export function findUserByEmail(stateStore: ReadStateStore, email: string) {
|
||||||
|
return stateStore.from("users")
|
||||||
|
.where({
|
||||||
|
email,
|
||||||
|
})
|
||||||
|
.selectOneOrFail();
|
||||||
|
}
|
||||||
3
apps/syntropy/domain/security/index.ts
Normal file
3
apps/syntropy/domain/security/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./permission.ts";
|
||||||
|
export * from "./policy.ts";
|
||||||
|
export * from "./users.ts";
|
||||||
10
apps/syntropy/domain/security/permission.ts
Normal file
10
apps/syntropy/domain/security/permission.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { EnumToType } from "@fabric/core";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A permission is a string that represents a something that a user is allowed to do in the system. It should be in the form of: `ACTION_ENTITY`.
|
||||||
|
* - `ACTION`: The domain action that the user can perform on the domain object. This is a domain verb in the imperative mood. i.e. "CREATE", "EDIT", "VIEW", "FIX", "RELEASE", etc.
|
||||||
|
* - `ENTITY`: The domain object that the user can perform the action on. This is a domain noun in the singular form.
|
||||||
|
*/
|
||||||
|
export const Permission = {} as const;
|
||||||
|
|
||||||
|
export type Permission = EnumToType<typeof Permission>;
|
||||||
5
apps/syntropy/domain/security/policy.ts
Normal file
5
apps/syntropy/domain/security/policy.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { Policy } from "@fabric/domain";
|
||||||
|
import { Permission } from "./permission.ts";
|
||||||
|
import { UserType } from "./users.ts";
|
||||||
|
|
||||||
|
export const policy = {} as const satisfies Policy<UserType, Permission>;
|
||||||
11
apps/syntropy/domain/security/users.ts
Normal file
11
apps/syntropy/domain/security/users.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { EnumToType } from "@fabric/core";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A User Type is a string that represents a user type.
|
||||||
|
* It should be in uppercase and singular form.
|
||||||
|
*/
|
||||||
|
export const UserType = {
|
||||||
|
ADMIN: "ADMIN",
|
||||||
|
// SPECIAL_USER: "SPECIAL_USER",
|
||||||
|
};
|
||||||
|
export type UserType = EnumToType<typeof UserType>;
|
||||||
6
apps/syntropy/domain/services/auth-service.ts
Normal file
6
apps/syntropy/domain/services/auth-service.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import type { User } from "../models/user.ts";
|
||||||
|
|
||||||
|
export interface AuthService {
|
||||||
|
generateAccessToken(user: User): string;
|
||||||
|
generateRefreshToken(user: User): string;
|
||||||
|
}
|
||||||
28
apps/syntropy/domain/services/crypto-service.ts
Normal file
28
apps/syntropy/domain/services/crypto-service.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { type AsyncResult, TaggedError } from "@fabric/core";
|
||||||
|
|
||||||
|
export interface CryptoService {
|
||||||
|
hashPassword(password: string): AsyncResult<string, InvalidPrivateKeyError>;
|
||||||
|
verifyPassword(
|
||||||
|
password: string,
|
||||||
|
hash: string,
|
||||||
|
): AsyncResult<void, InvalidPasswordError | InvalidPrivateKeyError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InvalidPasswordError extends TaggedError<"InvalidPasswordError"> {
|
||||||
|
constructor() {
|
||||||
|
super(
|
||||||
|
"InvalidPasswordError",
|
||||||
|
"The password is invalid or was not provided.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InvalidPrivateKeyError
|
||||||
|
extends TaggedError<"InvalidPrivateKeyError"> {
|
||||||
|
constructor() {
|
||||||
|
super(
|
||||||
|
"InvalidPrivateKeyError",
|
||||||
|
"The private key is invalid or was not provided.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
0
apps/syntropy/domain/services/index.ts
Normal file
0
apps/syntropy/domain/services/index.ts
Normal file
4
apps/syntropy/domain/services/state-store.ts
Normal file
4
apps/syntropy/domain/services/state-store.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import type { ReadonlyStateStore } from "@fabric/domain";
|
||||||
|
import type { DomainModels } from "../models/index.ts";
|
||||||
|
|
||||||
|
export type ReadStateStore = ReadonlyStateStore<DomainModels>;
|
||||||
5
apps/syntropy/domain/use-cases.ts
Normal file
5
apps/syntropy/domain/use-cases.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { Query } from "@fabric/domain";
|
||||||
|
|
||||||
|
export const UseCases = [] as const satisfies Query[];
|
||||||
|
|
||||||
|
export type UseCases = typeof UseCases;
|
||||||
55
apps/syntropy/domain/use-cases/auth/login.ts
Normal file
55
apps/syntropy/domain/use-cases/auth/login.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { TaggedError } from "@fabric/core";
|
||||||
|
import { Field, Model, type ModelToType, type Query } from "@fabric/domain";
|
||||||
|
import type { AuthService } from "../../services/auth-service.ts";
|
||||||
|
import type { CryptoService } from "../../services/crypto-service.ts";
|
||||||
|
import type { ReadStateStore } from "../../services/state-store.ts";
|
||||||
|
|
||||||
|
export interface LoginDependencies {
|
||||||
|
state: ReadStateStore;
|
||||||
|
crypto: CryptoService;
|
||||||
|
auth: AuthService;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LoginRequestModel = Model.from("LoginRequestModel", {
|
||||||
|
email: Field.email(),
|
||||||
|
password: Field.string(),
|
||||||
|
rememberMe: Field.boolean({ isOptional: true }),
|
||||||
|
});
|
||||||
|
export type LoginRequestModel = ModelToType<typeof LoginRequestModel>;
|
||||||
|
|
||||||
|
export const LoginResponseModel = Model.from("LoginResponseModel", {
|
||||||
|
accessToken: Field.string(),
|
||||||
|
refreshToken: Field.string(),
|
||||||
|
});
|
||||||
|
export type LoginResponseModel = ModelToType<typeof LoginResponseModel>;
|
||||||
|
|
||||||
|
export type LoginErrors = InvalidCredentialsError;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "login",
|
||||||
|
isAuthRequired: false,
|
||||||
|
useCase: ({ state, crypto, auth }, { email, password }) =>
|
||||||
|
state.from("users")
|
||||||
|
.where({
|
||||||
|
email,
|
||||||
|
})
|
||||||
|
.selectOneOrFail()
|
||||||
|
.assert((user) => crypto.verifyPassword(password, user.hashedPassword))
|
||||||
|
.errorMap(() => new InvalidCredentialsError())
|
||||||
|
.map((user) => ({
|
||||||
|
accessToken: auth.generateAccessToken(user),
|
||||||
|
refreshToken: auth.generateRefreshToken(user),
|
||||||
|
})),
|
||||||
|
} as const satisfies Query<
|
||||||
|
LoginDependencies,
|
||||||
|
LoginRequestModel,
|
||||||
|
LoginResponseModel,
|
||||||
|
LoginErrors
|
||||||
|
>;
|
||||||
|
|
||||||
|
export class InvalidCredentialsError
|
||||||
|
extends TaggedError<"InvalidCredentialsError"> {
|
||||||
|
constructor() {
|
||||||
|
super("InvalidCredentialsError");
|
||||||
|
}
|
||||||
|
}
|
||||||
0
apps/syntropy/domain/use-cases/index.ts
Normal file
0
apps/syntropy/domain/use-cases/index.ts
Normal file
74
apps/syntropy/domain/use-cases/projects/create.ts
Normal file
74
apps/syntropy/domain/use-cases/projects/create.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { TaggedError, type UUID } from "@fabric/core";
|
||||||
|
import {
|
||||||
|
type Command,
|
||||||
|
type DomainEvent,
|
||||||
|
Field,
|
||||||
|
Model,
|
||||||
|
type ModelToType,
|
||||||
|
type UUIDGenerator,
|
||||||
|
} from "@fabric/domain";
|
||||||
|
import type { ReadStateStore } from "../../services/state-store.ts";
|
||||||
|
|
||||||
|
export interface CreateProjectDependencies {
|
||||||
|
state: ReadStateStore;
|
||||||
|
uuid: UUIDGenerator;
|
||||||
|
currentUserId: UUID;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreateProjectRequestModel = Model.from(
|
||||||
|
"CreateProjectRequestModel",
|
||||||
|
{
|
||||||
|
name: Field.string(),
|
||||||
|
description: Field.string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
export type CreateProjectRequestModel = ModelToType<
|
||||||
|
typeof CreateProjectRequestModel
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type ProjectCreatedEvent = DomainEvent<"ProjectCreated", {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
userId: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type CreateProjectErrors = ProjectNameInUseError;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "createProject",
|
||||||
|
isAuthRequired: true,
|
||||||
|
useCase: ({ state, uuid, currentUserId }, { name, description }) =>
|
||||||
|
state.from("projects")
|
||||||
|
.where({ name })
|
||||||
|
.assertNone()
|
||||||
|
.errorMap(() => new ProjectNameInUseError())
|
||||||
|
.map(() => {
|
||||||
|
const newEventId = uuid.generate();
|
||||||
|
const newProjectId = uuid.generate();
|
||||||
|
|
||||||
|
return {
|
||||||
|
_tag: "ProjectCreated",
|
||||||
|
id: newEventId,
|
||||||
|
streamId: newProjectId,
|
||||||
|
payload: {
|
||||||
|
id: newProjectId,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
userId: currentUserId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
} as const satisfies Command<
|
||||||
|
CreateProjectDependencies,
|
||||||
|
CreateProjectRequestModel,
|
||||||
|
ProjectCreatedEvent,
|
||||||
|
CreateProjectErrors
|
||||||
|
>;
|
||||||
|
|
||||||
|
export class ProjectNameInUseError
|
||||||
|
extends TaggedError<"ProjectNameInUseError"> {
|
||||||
|
constructor() {
|
||||||
|
super("ProjectNameInUseError");
|
||||||
|
}
|
||||||
|
}
|
||||||
52
deno.lock
52
deno.lock
@ -125,13 +125,61 @@
|
|||||||
"integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA=="
|
"integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA=="
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"redirects": {
|
||||||
|
"https://deno.land/std/testing/asserts.ts": "https://deno.land/std@0.224.0/testing/asserts.ts"
|
||||||
|
},
|
||||||
|
"remote": {
|
||||||
|
"https://deno.land/std@0.224.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_almost_equals.ts": "9e416114322012c9a21fa68e187637ce2d7df25bcbdbfd957cd639e65d3cf293",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_array_includes.ts": "14c5094471bc8e4a7895fc6aa5a184300d8a1879606574cb1cd715ef36a4a3c7",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_equals.ts": "3bbca947d85b9d374a108687b1a8ba3785a7850436b5a8930d81f34a32cb8c74",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_exists.ts": "43420cf7f956748ae6ed1230646567b3593cb7a36c5a5327269279c870c5ddfd",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_false.ts": "3e9be8e33275db00d952e9acb0cd29481a44fa0a4af6d37239ff58d79e8edeff",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_greater.ts": "5e57b201fd51b64ced36c828e3dfd773412c1a6120c1a5a99066c9b261974e46",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_greater_or_equal.ts": "9870030f997a08361b6f63400273c2fb1856f5db86c0c3852aab2a002e425c5b",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_instance_of.ts": "e22343c1fdcacfaea8f37784ad782683ec1cf599ae9b1b618954e9c22f376f2c",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_is_error.ts": "f856b3bc978a7aa6a601f3fec6603491ab6255118afa6baa84b04426dd3cc491",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_less.ts": "60b61e13a1982865a72726a5fa86c24fad7eb27c3c08b13883fb68882b307f68",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_less_or_equal.ts": "d2c84e17faba4afe085e6c9123a63395accf4f9e00150db899c46e67420e0ec3",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_match.ts": "ace1710dd3b2811c391946954234b5da910c5665aed817943d086d4d4871a8b7",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_not_equals.ts": "78d45dd46133d76ce624b2c6c09392f6110f0df9b73f911d20208a68dee2ef29",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_not_instance_of.ts": "3434a669b4d20cdcc5359779301a0588f941ffdc2ad68803c31eabdb4890cf7a",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_not_match.ts": "df30417240aa2d35b1ea44df7e541991348a063d9ee823430e0b58079a72242a",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_not_strict_equals.ts": "37f73880bd672709373d6dc2c5f148691119bed161f3020fff3548a0496f71b8",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_object_match.ts": "411450fd194fdaabc0089ae68f916b545a49d7b7e6d0026e84a54c9e7eed2693",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_rejects.ts": "4bee1d6d565a5b623146a14668da8f9eb1f026a4f338bbf92b37e43e0aa53c31",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_strict_equals.ts": "b4f45f0fd2e54d9029171876bd0b42dd9ed0efd8f853ab92a3f50127acfa54f5",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_string_includes.ts": "496b9ecad84deab72c8718735373feb6cdaa071eb91a98206f6f3cb4285e71b8",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_throws.ts": "c6508b2879d465898dab2798009299867e67c570d7d34c90a2d235e4553906eb",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917",
|
||||||
|
"https://deno.land/std@0.224.0/assert/equal.ts": "bddf07bb5fc718e10bb72d5dc2c36c1ce5a8bdd3b647069b6319e07af181ac47",
|
||||||
|
"https://deno.land/std@0.224.0/assert/fail.ts": "0eba674ffb47dff083f02ced76d5130460bff1a9a68c6514ebe0cdea4abadb68",
|
||||||
|
"https://deno.land/std@0.224.0/assert/mod.ts": "48b8cb8a619ea0b7958ad7ee9376500fe902284bb36f0e32c598c3dc34cbd6f3",
|
||||||
|
"https://deno.land/std@0.224.0/assert/unimplemented.ts": "8c55a5793e9147b4f1ef68cd66496b7d5ba7a9e7ca30c6da070c1a58da723d73",
|
||||||
|
"https://deno.land/std@0.224.0/assert/unreachable.ts": "5ae3dbf63ef988615b93eb08d395dda771c96546565f9e521ed86f6510c29e19",
|
||||||
|
"https://deno.land/std@0.224.0/fmt/colors.ts": "508563c0659dd7198ba4bbf87e97f654af3c34eb56ba790260f252ad8012e1c5",
|
||||||
|
"https://deno.land/std@0.224.0/internal/diff.ts": "6234a4b493ebe65dc67a18a0eb97ef683626a1166a1906232ce186ae9f65f4e6",
|
||||||
|
"https://deno.land/std@0.224.0/internal/format.ts": "0a98ee226fd3d43450245b1844b47003419d34d210fa989900861c79820d21c2",
|
||||||
|
"https://deno.land/std@0.224.0/internal/mod.ts": "534125398c8e7426183e12dc255bb635d94e06d0f93c60a297723abe69d3b22e",
|
||||||
|
"https://deno.land/std@0.224.0/testing/asserts.ts": "d0cdbabadc49cc4247a50732ee0df1403fdcd0f95360294ad448ae8c240f3f5c"
|
||||||
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"members": {
|
"members": {
|
||||||
|
"apps/syntropy/domain": {
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@fabric/core@*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"packages/fabric/core": {
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@quentinadam/decimal@~0.1.6"
|
||||||
|
]
|
||||||
|
},
|
||||||
"packages/fabric/domain": {
|
"packages/fabric/domain": {
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@fabric/core@*",
|
"jsr:@fabric/core@*",
|
||||||
"jsr:@fabric/validations@*",
|
"jsr:@fabric/validations@*"
|
||||||
"jsr:@quentinadam/decimal@~0.1.6"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"packages/fabric/sqlite-store": {
|
"packages/fabric/sqlite-store": {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user