Compare commits
10 Commits
65480c4f8c
...
68135ae022
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68135ae022 | ||
|
|
349196b801 | ||
|
|
6fc494687a | ||
|
|
496d689ec1 | ||
|
|
024702dd26 | ||
|
|
90b0192cd2 | ||
|
|
afed1731d2 | ||
|
|
f4c9174752 | ||
|
|
f6bce78aee | ||
|
|
9561693cb4 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -54,3 +54,5 @@ pids
|
|||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
/generated/prisma
|
||||||
|
|||||||
389
CODE_REVIEW.md
Normal file
389
CODE_REVIEW.md
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
# Code Review — Kaa Khane (Multi-Tenant SaaS Backend)
|
||||||
|
|
||||||
|
## What This Is
|
||||||
|
|
||||||
|
A NestJS backend for a multi-tenant SaaS platform. It models the core of any team-based product: users belong to organizations, organizations have roles (`owner`, `admin`, `member`), and membership is managed through a request/invitation flow. Stack: NestJS + Prisma + PostgreSQL + Redis (via Keyv). This is a learning project but the ambition is real — you've touched AsyncLocalStorage, operation-based authorization, cache resilience, and transactional writes. That's not beginner stuff.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What You're Doing Well
|
||||||
|
|
||||||
|
**1. AsyncLocalStorage for request context**
|
||||||
|
This is a legitimately good pattern. Instead of threading `req.user` through every function signature, you store it in ALS and services pull it from context. This is how production NestJS apps at scale handle per-request state. Most people learning NestJS never get here.
|
||||||
|
|
||||||
|
**2. Cache-aside with graceful degradation**
|
||||||
|
`CacheService.getOrSet()` is well thought out. It tracks `redisAvailable`, silently falls back to DB, and doesn't let a Redis outage crash the app. The pattern is correct. The placement of `console.warn` in a service is slightly off (more on that below) but the intent is solid.
|
||||||
|
|
||||||
|
**3. Parallel DB checks with `Promise.all`**
|
||||||
|
You're doing this in multiple places — running independent queries concurrently instead of awaiting them in sequence. This is the right instinct and most junior devs miss it.
|
||||||
|
|
||||||
|
**4. Operation-based authorization**
|
||||||
|
`AuthorizationService.canPerformOperation()` with a switch statement and named operations (`INVITE_USERS`, `DELETE_ORGANIZATION`) is cleaner than scattering raw role checks everywhere. It's a solid foundation.
|
||||||
|
|
||||||
|
**5. Transactions where they matter**
|
||||||
|
`createNewOrganization` and `userOrganiaztionRequestAction` use `$transaction` to ensure atomicity. You understand when to use them.
|
||||||
|
|
||||||
|
**6. DTO validation layer**
|
||||||
|
Using `class-validator` + `@nestjs/swagger` together is standard and correct. `@AtLeastOneField()` is a good custom validator — you're not reaching for a library when a simple decorator does the job.
|
||||||
|
|
||||||
|
**7. Global `AuthGuard` with `@Public()` opt-out**
|
||||||
|
Secure by default. Making all routes protected and requiring explicit `@Public()` to open them is the right model. A lot of apps do it backwards.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issues and What to Improve
|
||||||
|
|
||||||
|
Organized by severity: `[critical]`, `[high]`, `[medium]`, `[low]`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. Hardcoded JWT secret `[critical]`
|
||||||
|
|
||||||
|
**File:** `src/auth/auth.service.ts:60`, `src/auth/guards/auth.guard.ts:38`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
secret: 'demo';
|
||||||
|
```
|
||||||
|
|
||||||
|
You know this is wrong (you left a TODO). But let me explain _why_ it's critical: if this ever reaches a staging environment and someone rotates the secret, every user is logged out. Worse, the secret is in version control. Fix this before you add anything else.
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// in JwtModule registration (auth.module.ts)
|
||||||
|
JwtModule.registerAsync({
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory: (config: ConfigService) => ({
|
||||||
|
secret: config.getOrThrow<string>('JWT_SECRET'),
|
||||||
|
signOptions: { expiresIn: '15m' },
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
```
|
||||||
|
|
||||||
|
Then remove the `secret` override from every `signAsync` / `verifyAsync` call. The module-level secret is used automatically.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. `userOrganiaztionRequestAction` uses `this.prisma` inside a `$transaction` `[critical]`
|
||||||
|
|
||||||
|
**File:** `src/organization-membership/organization-membership.service.ts:226`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
return await this.prisma.$transaction(async (tx) => {
|
||||||
|
await tx.organizationJoinRequest.update(...) // ✅ uses tx
|
||||||
|
|
||||||
|
if (userAction === ORGANIZATION_JOIN_REQUEST.ACCEPTED)
|
||||||
|
await this.prisma.organizationUserJoinTable.create(...) // ❌ uses this.prisma — NOT the tx
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The `create` inside the transaction is using `this.prisma` (the global client) instead of `tx` (the transaction client). If the second write fails, the first write is NOT rolled back. The whole point of wrapping in `$transaction` is broken here.
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await tx.organizationUserJoinTable.create({ ... })
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Two flows conflated into one method — and the semantics are wrong `[high]`
|
||||||
|
|
||||||
|
**File:** `src/organization-membership/organization-membership.service.ts:180`
|
||||||
|
|
||||||
|
`userOrganiaztionRequestAction` is currently called by the user (the invitee) to accept or reject an invitation. But based on your plan, you also want an org admin to accept/reject a join request. These are fundamentally different actors:
|
||||||
|
|
||||||
|
| Flow | Actor | What they act on |
|
||||||
|
| -------------------------- | ---------------------- | ---------------------------------- |
|
||||||
|
| Accept/reject invitation | The invited **user** | Their own pending `INVITED` record |
|
||||||
|
| Accept/reject join request | An **org admin/owner** | Someone else's `REQUESTED` record |
|
||||||
|
|
||||||
|
Right now the method doesn't distinguish by `requestType`. An org admin could use it to act on _their own_ data when they should be acting on _another user's_ data. They need to be two methods:
|
||||||
|
|
||||||
|
- `userRespondToInvitation(userId, dto)` — user accepts/rejects where `requestType = INVITED`
|
||||||
|
- `adminRespondToJoinRequest(adminUserId, targetUserId, orgId, action)` — admin acts on `requestType = REQUESTED`, with a `canPerformOperation` check first
|
||||||
|
|
||||||
|
This is not a minor naming issue. It's a modeling issue. The current code would let a user "accept" their own join request.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. `organizationExists` vs `findById` — two methods doing nearly the same thing `[high]`
|
||||||
|
|
||||||
|
**File:** `src/organization/organization.service.ts:121`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
async organizationExists(orgId: string) {
|
||||||
|
return await this.prisma.organization.findUnique({
|
||||||
|
where: { id: orgId },
|
||||||
|
select: { id: true }, // minimal select — just checking existence
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(orgId: string) {
|
||||||
|
return await this.cacheService.getOrSet(orgId, async () =>
|
||||||
|
await this.prisma.organization.findUnique({ where: { id: orgId } }),
|
||||||
|
);
|
||||||
|
// returns full org object, goes through cache
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
These have different purposes (one is a lightweight existence check, one returns full data with caching) but the distinction is opaque from the call site. `OrganizationMembershipService` calls both, inconsistently.
|
||||||
|
|
||||||
|
More importantly: `organizationExists` bypasses the cache entirely. If you call `findById` first (which caches), then call `organizationExists`, you've now made an extra DB round-trip for no reason.
|
||||||
|
|
||||||
|
**What to do:**
|
||||||
|
|
||||||
|
- Consolidate. Use `findById` (cached) everywhere.
|
||||||
|
- If you need a boolean check, add a thin wrapper: `async assertOrganizationExists(orgId)` that calls `findById` and throws `NotFoundException` if null. Then services call that one line instead of the two-line check-then-throw pattern you repeat everywhere.
|
||||||
|
- Delete `organizationExists` or make it call `findById` internally.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Repeated guard pattern — check existence, throw — everywhere `[high]`
|
||||||
|
|
||||||
|
You do this in almost every method:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const orgExists = await this.orgService.findById(dto.orgId);
|
||||||
|
if (!orgExists) throw new NotFoundException('Organization');
|
||||||
|
```
|
||||||
|
|
||||||
|
And separately:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const canUserDeleteOrganization = await this.authorization.canPerformOperation(...);
|
||||||
|
if (!canUserDeleteOrganization) throw new ForbiddenException('Not enough permission');
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the "guard clause" pattern and the repetition itself isn't wrong — guard clauses are good. What's wrong is that the _authorization check_ is manually wired in every service method instead of being declarative. This will become painful as you add more operations.
|
||||||
|
|
||||||
|
**Better pattern — make `canPerformOperation` throw directly:**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// authorization.service.ts
|
||||||
|
async assertCanPerformOperation(
|
||||||
|
userId: string,
|
||||||
|
orgId: string,
|
||||||
|
operation: USER_ORGANIZATION_OPERATIONS,
|
||||||
|
): Promise<void> {
|
||||||
|
const allowed = await this.canPerformOperation(userId, orgId, operation);
|
||||||
|
if (!allowed) throw new ForbiddenException('Insufficient permissions');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now the call site is one line with no `if`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await this.authorization.assertCanPerformOperation(userId, orgId, INVITE_USERS);
|
||||||
|
```
|
||||||
|
|
||||||
|
Clean. Unambiguous. And you can add logging, metrics, or audit trail in one place.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. `isAdmin()` is dead code `[medium]`
|
||||||
|
|
||||||
|
**File:** `src/authorization/authorization.service.ts:49`
|
||||||
|
|
||||||
|
`isAdmin()` is defined but never called. Either use it in `canInvite` (which currently re-implements the same logic inline) or delete it.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// current canInvite re-implements admin check inline:
|
||||||
|
return (
|
||||||
|
isUserPartOfOrganization.role === ORG_ROLE.admin ||
|
||||||
|
isUserPartOfOrganization.role === ORG_ROLE.owner
|
||||||
|
);
|
||||||
|
|
||||||
|
// isAdmin() does the same thing but only for admin role.
|
||||||
|
// Clean it up or wire it in.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. `AuthorizationService` hits the DB every time — no caching `[medium]`
|
||||||
|
|
||||||
|
**File:** `src/authorization/authorization.service.ts:61`
|
||||||
|
|
||||||
|
Every call to `canPerformOperation` does a `findUnique` on `OrganizationUserJoinTable`. In `inviteUserToOrg` alone, you've already called `orgService.findById` (which hits cache), but then `canPerformOperation` hits DB cold. Membership data is relatively stable — it changes when someone joins, leaves, or is promoted.
|
||||||
|
|
||||||
|
**Fix:** Cache the membership record in `CacheService` using a compound key like `membership:{userId}:{orgId}`. Invalidate it when a member joins, leaves, or their role changes.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const key = `membership:${userId}:${orgId}`;
|
||||||
|
return this.cacheService.getOrSet(key, () =>
|
||||||
|
this.prisma.organizationUserJoinTable.findUnique(...)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. `deleteAnOrganization` has broken cascade logic `[medium]`
|
||||||
|
|
||||||
|
**File:** `src/organization/organization.service.ts:99`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await tx.organization.delete({ where: { id: orgId } });
|
||||||
|
|
||||||
|
await tx.organizationUserJoinTable.delete({
|
||||||
|
where: { userId_orgId: { userId, orgId } }, // only deletes the OWNER's row
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
This only deletes the owner's row from the join table. All other members' rows are left orphaned (or fail with a FK constraint — depending on your Prisma schema cascade setting). Also, `OrganizationJoinRequest` records for this org are not cleaned up.
|
||||||
|
|
||||||
|
You should either:
|
||||||
|
|
||||||
|
- Set `onDelete: Cascade` in the Prisma schema on those relations (so deleting the org auto-cleans everything), or
|
||||||
|
- Manually `deleteMany` all join rows and requests in the transaction before deleting the org
|
||||||
|
|
||||||
|
Option 1 (schema cascade) is cleaner for this case.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. `console.log` / `console.warn` in production code `[medium]`
|
||||||
|
|
||||||
|
**Files:** `src/cache/cache.service.ts:13,18`, `src/cache/cache.service.ts:62`, `src/prisma/prisma.service.ts:40`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
console.log(a); // in deleteKey — what is `a`?
|
||||||
|
console.warn('Redis disconnected');
|
||||||
|
```
|
||||||
|
|
||||||
|
Use NestJS's built-in `Logger`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
private readonly logger = new Logger(CacheService.name);
|
||||||
|
|
||||||
|
this.logger.warn('Redis disconnected');
|
||||||
|
this.logger.error('Prisma connection failed', err.stack);
|
||||||
|
```
|
||||||
|
|
||||||
|
The NestJS logger respects log levels, outputs structured logs, and can be replaced with a production logger (Pino, Winston) without touching service code.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10. Controller stubs are wired wrong — copy-paste bug `[medium]`
|
||||||
|
|
||||||
|
**File:** `src/organization-membership/organization-membership.controller.ts:54`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
@Get('organization/:id/invitations')
|
||||||
|
async getOrganizationInvitations(@Param('id') orgId: string) {
|
||||||
|
return await this.orgMemService.getMemebersOfOrganization(orgId); // ❌ wrong method
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`getOrganizationInvitations` is calling `getMemebersOfOrganization`. Both endpoints return the same data. This is a copy-paste leftover.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11. `userOrganizationJoinRequestList` does manual enum validation `[low]`
|
||||||
|
|
||||||
|
**File:** `src/organization-membership/organization-membership.service.ts:135`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
async userOrganizationJoinRequestList(userId: string, requestType: string) {
|
||||||
|
const joinReqType: ORGANIZATION_JOIN_REQUEST_TYPE | undefined =
|
||||||
|
ORGANIZATION_JOIN_REQUEST_TYPE[requestType];
|
||||||
|
if (!joinReqType) throw new BadRequestException('Invalid request type');
|
||||||
|
```
|
||||||
|
|
||||||
|
This manual enum validation in the service layer exists because `requestType` comes in as a raw `string`. The fix is to validate it in the DTO/query params using `@IsEnum(ORGANIZATION_JOIN_REQUEST_TYPE)`. Then the service never receives an invalid value — NestJS's validation pipe rejects it before it gets there. The service layer should not be doing input validation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 12. `BaseException` exists but is never used `[low]`
|
||||||
|
|
||||||
|
**File:** `common/exceptions/custom-exceptions.ts`
|
||||||
|
|
||||||
|
You built a `BaseException` class with `code`, `message`, and `HttpStatus`. Nothing uses it. Either start using it for domain-specific errors (e.g., `OrganizationNotFoundError extends BaseException`) or delete it. Dead abstractions add noise.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 13. Typos in method and variable names `[low]`
|
||||||
|
|
||||||
|
- `userOrganiaztionRequestAction` → `userOrganizationRequestAction`
|
||||||
|
- `getMemebersOfOrganization` → `getMembersOfOrganization`
|
||||||
|
|
||||||
|
These propagate to controller method names, controller routes, and service interface. Fix them now while the surface area is small.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## General Guidelines to Internalize
|
||||||
|
|
||||||
|
### On DRY vs. "right abstraction"
|
||||||
|
|
||||||
|
DRY (Don't Repeat Yourself) is often misapplied. The goal isn't zero duplication — it's avoiding duplication of _knowledge_ (the same decision made in two places). Two methods that both check `if (!org) throw NotFoundException` are DRY violations only if they're checking the same _thing_. The right fix is `assertOrganizationExists()` — a single function that owns the "org must exist" rule. The callers delegate to it.
|
||||||
|
|
||||||
|
### On service layer responsibilities
|
||||||
|
|
||||||
|
A service method should do one thing: execute a business operation. Input validation belongs in DTOs + pipes. Authorization belongs in guards or a dedicated authorization service (you're halfway there). Existence checks belong in helper methods on the service. What remains in the method body should read like a business narrative: "check user is allowed → do the thing → return result."
|
||||||
|
|
||||||
|
### On the transaction client (`this.prisma.client`)
|
||||||
|
|
||||||
|
You built the ALS-based `prisma.client` getter for shared transactions across services. It's a good idea — but it only works if services call `this.prisma.client.xxx` instead of `this.prisma.xxx`. Right now no service uses it. Either commit to the pattern (use `client` everywhere) or remove the getter to avoid confusion. Half-implemented patterns are worse than no pattern — they create false confidence.
|
||||||
|
|
||||||
|
### On authorization: where you are vs. where to go
|
||||||
|
|
||||||
|
**Where you are:** operation-based (`canPerformOperation` switch). This is fine for 3-5 operations. As the app grows, the switch becomes unmaintainable and you end up with logic spread across the switch, `isOwner`, `canInvite`, etc.
|
||||||
|
|
||||||
|
**Where to go next:** a role-permission matrix.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const PERMISSIONS: Record<ORG_ROLE, USER_ORGANIZATION_OPERATIONS[]> = {
|
||||||
|
[ORG_ROLE.owner]: [
|
||||||
|
UPDATE_ORGANIZATION,
|
||||||
|
DELETE_ORGANIZATION,
|
||||||
|
INVITE_USERS,
|
||||||
|
REMOVE_MEMBERS,
|
||||||
|
],
|
||||||
|
[ORG_ROLE.admin]: [INVITE_USERS, REMOVE_MEMBERS],
|
||||||
|
[ORG_ROLE.member]: [],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Then `canPerformOperation` becomes:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
async canPerformOperation(userId, orgId, operation) {
|
||||||
|
const membership = await this.getMembership(userId, orgId);
|
||||||
|
if (!membership) return false;
|
||||||
|
return PERMISSIONS[membership.role].includes(operation);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Adding a new operation is one line in the matrix. No new DB queries, no new switch case.
|
||||||
|
|
||||||
|
**Beyond that:** [CASL](https://casl.js.org/) is the standard Node.js authorization library for more complex scenarios (attribute-level permissions, "can user edit _this specific_ resource"). You don't need it now but it's worth knowing it exists.
|
||||||
|
|
||||||
|
### On caching: what to cache, what to invalidate
|
||||||
|
|
||||||
|
Cache data that is: (a) read frequently, (b) expensive to recompute, (c) stable. Organization data and membership data fit all three. The rule is simple: **every write must invalidate or update the relevant cache key**. Right now you invalidate on org delete but not on org update. When you cache membership, you must invalidate on role change, member removal, and member addition.
|
||||||
|
|
||||||
|
Use structured cache keys: `org:{orgId}`, `membership:{userId}:{orgId}`. Bare IDs as keys (like you're doing now with just `orgId`) work but collide if you ever cache different types under the same store.
|
||||||
|
|
||||||
|
### On the two-flow membership design (your plan)
|
||||||
|
|
||||||
|
Your instinct to split into two separate flows is correct. Model it explicitly:
|
||||||
|
|
||||||
|
- `POST /organization-membership/invite` — org admin invites a user (creates `INVITED` record)
|
||||||
|
- `POST /organization-membership/request` — user requests to join (creates `REQUESTED` record)
|
||||||
|
- `PATCH /organization-membership/user/respond` — **user** accepts/rejects their own `INVITED` record
|
||||||
|
- `PATCH /organization-membership/organization/:id/respond` — **admin** accepts/rejects a `REQUESTED` record
|
||||||
|
|
||||||
|
The actor and the target record type are different in each case. Wire your service methods to match this — each method should assert `requestType` before acting.
|
||||||
|
|
||||||
|
### On what to build next (priority order)
|
||||||
|
|
||||||
|
1. Fix the `$transaction` bug (item 2 above) — data integrity issue
|
||||||
|
2. Split the two membership flows into two methods
|
||||||
|
3. Move JWT secret to `ConfigService`
|
||||||
|
4. Add `assertOrganizationExists()` / `assertCanPerformOperation()` helpers — this cleans up every service method
|
||||||
|
5. Fix the cascade on org delete
|
||||||
|
6. Wire up the remaining controller stubs
|
||||||
|
7. Add pagination to `getOrganizations()`
|
||||||
|
8. Replace `console.log` with `Logger`
|
||||||
|
9. Cache membership in `AuthorizationService`
|
||||||
|
10. Delete dead code (`isAdmin`, `BaseException`, commented-out `acceptInvitation`)
|
||||||
45
common/emails/auth.ts
Normal file
45
common/emails/auth.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
export const welcomeToApp =
|
||||||
|
`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Welcome to Research Shock</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; line-height: 1.6; color: #333; margin: 0; padding: 0; background-color: #f4f7f9; }
|
||||||
|
.container { max-width: 600px; margin: 20px auto; background: #ffffff; padding: 40px; border-radius: 8px; box-shadow: 0 4px 10px rgba(0,0,0,0.05); }
|
||||||
|
.logo { font-size: 24px; font-weight: bold; color: #2563eb; margin-bottom: 20px; text-align: center; }
|
||||||
|
h1 { font-size: 22px; color: #111827; }
|
||||||
|
p { margin-bottom: 20px; color: #4b5563; }
|
||||||
|
.button { display: inline-block; padding: 12px 24px; background-color: #2563eb; color: #ffffff; text-decoration: none; border-radius: 5px; font-weight: 600; transition: background 0.3s ease; }
|
||||||
|
.footer { margin-top: 30px; font-size: 12px; color: #9ca3af; text-align: center; }
|
||||||
|
.divider { height: 1px; background: #e5e7eb; margin: 30px 0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="logo">MultiTenant SaaS</div>
|
||||||
|
<h1>Welcome to the team!</h1>
|
||||||
|
<p>Hi there,</p>
|
||||||
|
<p>Thanks for signing up for <strong>MultiTenant Saas</strong>. We're excited to help you around.</p>
|
||||||
|
<p>To get started, we recommend setting up your organization profile and inviting your first team member.</p>
|
||||||
|
|
||||||
|
<div style="text-align: center; margin: 35px 0;">
|
||||||
|
<a href="http://localhost:3000" class="button">Go to Dashboard</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>If you have any questions, just reply to this email. We're here to help!</p>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<p>Cheers,<br>Team</p>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
© 2026 App. All rights reserved.<br>
|
||||||
|
If you didn't create an account, you can safely ignore this email.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
9
common/emails/index.ts
Normal file
9
common/emails/index.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { welcomeToApp } from "./auth"
|
||||||
|
|
||||||
|
const EmailTemplates = {
|
||||||
|
welcomeToApp,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EmailTemplates
|
||||||
|
|
||||||
|
|
||||||
24
common/exceptions/custom-exceptions.ts
Normal file
24
common/exceptions/custom-exceptions.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { HttpException, HttpStatus } from '@nestjs/common';
|
||||||
|
|
||||||
|
// Base exception
|
||||||
|
export class BaseException extends HttpException {
|
||||||
|
protected constructor(
|
||||||
|
errorCode: string,
|
||||||
|
errorMessage: string,
|
||||||
|
status: HttpStatus
|
||||||
|
) {
|
||||||
|
super({ code: errorCode, message: errorMessage }, status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Organization Exceptions
|
||||||
|
* */
|
||||||
|
export class OrganizationNotFoundException extends BaseException {
|
||||||
|
|
||||||
|
}
|
||||||
|
// export class NotPartOfOrganizationException extends BaseException {
|
||||||
|
// constructor(code: string, message: string, status: HttpStatus) {
|
||||||
|
// super();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
17
common/exceptions/error_codes.ts
Normal file
17
common/exceptions/error_codes.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { HttpStatus } from "@nestjs/common";
|
||||||
|
|
||||||
|
interface AppError {
|
||||||
|
errorCode: string,
|
||||||
|
message: string,
|
||||||
|
status: HttpStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorsType = Record<string, AppError>
|
||||||
|
|
||||||
|
export const ORGANIZATION_ERRORS: ErrorsType = {
|
||||||
|
NOT_FOUND: {
|
||||||
|
errorCode: 'ORG_001',
|
||||||
|
message: 'Organization not found',
|
||||||
|
status: HttpStatus.NOT_FOUND
|
||||||
|
}
|
||||||
|
}
|
||||||
37
common/exceptions/exception-filter.ts
Normal file
37
common/exceptions/exception-filter.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import {
|
||||||
|
ExceptionFilter,
|
||||||
|
Catch,
|
||||||
|
ArgumentsHost,
|
||||||
|
HttpException,
|
||||||
|
Logger,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
|
@Catch(HttpException) // What exception to catch
|
||||||
|
export class HttpExceptionFilter implements ExceptionFilter<HttpException> {
|
||||||
|
constructor(private readonly logger: Logger) {}
|
||||||
|
catch(exception: HttpException, host: ArgumentsHost) {
|
||||||
|
const ctx = host.switchToHttp();
|
||||||
|
const request: Request = ctx.getRequest();
|
||||||
|
const response: Response = ctx.getResponse();
|
||||||
|
const status = exception.getStatus();
|
||||||
|
|
||||||
|
if (status >= 500) {
|
||||||
|
this.logger.warn({
|
||||||
|
method: request.method,
|
||||||
|
url: request.url,
|
||||||
|
message: exception.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 404) {
|
||||||
|
exception.message = `${exception.message} not found`;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.status(status).json({
|
||||||
|
success: false,
|
||||||
|
message: exception.message,
|
||||||
|
statusCode: status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
1
common/exceptions/index.ts
Normal file
1
common/exceptions/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './custom-exceptions';
|
||||||
1
common/http/index.ts
Normal file
1
common/http/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './response';
|
||||||
25
common/http/response.ts
Normal file
25
common/http/response.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
export class MessageResponse {
|
||||||
|
readonly success: boolean;
|
||||||
|
readonly message: string;
|
||||||
|
|
||||||
|
constructor(message?: string) {
|
||||||
|
this.success = true;
|
||||||
|
this.message = message ?? 'Success';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DataResponse<T> extends MessageResponse {
|
||||||
|
readonly data: T;
|
||||||
|
|
||||||
|
constructor(data: T, message?: string) {
|
||||||
|
super(message);
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skipped
|
||||||
|
export class GlobalErrorResponseDTO {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
statusCode: number;
|
||||||
|
}
|
||||||
0
common/interceptors/index.ts
Normal file
0
common/interceptors/index.ts
Normal file
72
common/interceptors/response.interceptor.ts
Normal file
72
common/interceptors/response.interceptor.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
NestInterceptor,
|
||||||
|
ExecutionContext,
|
||||||
|
CallHandler,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { DataResponse, MessageResponse } from 'common/http';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ResponseInterceptor<T> implements NestInterceptor<T, any> {
|
||||||
|
intercept(_: ExecutionContext, next: CallHandler): Observable<any> {
|
||||||
|
return next.handle().pipe(
|
||||||
|
map((data) => {
|
||||||
|
if (data instanceof MessageResponse) return data;
|
||||||
|
else if (data instanceof DataResponse) return data;
|
||||||
|
else if (typeof data === 'string') return new MessageResponse(data);
|
||||||
|
return new DataResponse(data);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* NOTE: How to access request
|
||||||
|
*
|
||||||
|
@Injectable()
|
||||||
|
export class ResponseInterceptor<T> implements NestInterceptor<T, any> {
|
||||||
|
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||||
|
const req = context.switchToHttp().getRequest();
|
||||||
|
req.userId = 'ram';
|
||||||
|
return next.handle();
|
||||||
|
// return next.handle().pipe(
|
||||||
|
// map((data) => {
|
||||||
|
// if (data instanceof MessageResponse) return data;
|
||||||
|
// else if (data instanceof DataResponse) return data;
|
||||||
|
// else if (typeof data === 'string') return new MessageResponse(data);
|
||||||
|
// return new DataResponse(data);
|
||||||
|
// }),
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
REQUEST
|
||||||
|
↓
|
||||||
|
Guards
|
||||||
|
↓
|
||||||
|
Interceptors (before)
|
||||||
|
↓
|
||||||
|
Pipes
|
||||||
|
↓
|
||||||
|
Controller method
|
||||||
|
↓
|
||||||
|
Interceptors (after) ← YOU ARE HERE
|
||||||
|
↓
|
||||||
|
Response
|
||||||
|
|
||||||
|
|
||||||
|
3️⃣ What is next.handle() really?
|
||||||
|
next.handle(): Observable<T>
|
||||||
|
|
||||||
|
|
||||||
|
This is:
|
||||||
|
|
||||||
|
An RxJS stream of whatever the controller returns
|
||||||
|
|
||||||
|
Not the request
|
||||||
|
|
||||||
|
Not the response object
|
||||||
|
|
||||||
|
Just the return value
|
||||||
|
* */
|
||||||
5
common/keys.ts
Normal file
5
common/keys.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export const PUBLIC_KEY = '__PUBLIC_KEY__';
|
||||||
|
export const ROLE_KEY = '__ROLE_KEY__';
|
||||||
|
export const ORG_ROLE_KEY = '__ORG_ROLE_KEY__'
|
||||||
|
export const ORG_ROLES_ALL_KEY = '__ORG_ROLE_ALL_KEY__';
|
||||||
|
export const CAN_PERFORM_KEY = '__CAN_PERFORM_KEY__';
|
||||||
26
common/validators/at-least-one-field.ts
Normal file
26
common/validators/at-least-one-field.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import {
|
||||||
|
registerDecorator,
|
||||||
|
ValidationArguments,
|
||||||
|
ValidationOptions,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
export function AtLeastOneField(validationOptions?: ValidationOptions) {
|
||||||
|
return function (constructor: Function) {
|
||||||
|
registerDecorator({
|
||||||
|
name: 'atLeastOneField',
|
||||||
|
target: constructor,
|
||||||
|
propertyName: undefined as any, // important for class-level
|
||||||
|
options: validationOptions,
|
||||||
|
validator: {
|
||||||
|
validate(_: any, args: ValidationArguments) {
|
||||||
|
const object = args.object as Record<string, any>;
|
||||||
|
|
||||||
|
return Object.values(object).some((value) => value !== undefined);
|
||||||
|
},
|
||||||
|
defaultMessage() {
|
||||||
|
return 'At least one field must be provided';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
1
common/validators/index.ts
Normal file
1
common/validators/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './at-least-one-field';
|
||||||
6
common/validators/query-uuid.dto.ts
Normal file
6
common/validators/query-uuid.dto.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { IsUUID } from 'class-validator';
|
||||||
|
|
||||||
|
export class UUIDQueryDTO {
|
||||||
|
@IsUUID()
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
19
core/als/request-context.middleware.ts
Normal file
19
core/als/request-context.middleware.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
|
import { RequestContextService } from './request-context.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RequestContextMiddleware implements NestMiddleware {
|
||||||
|
constructor(private readonly ctx: RequestContextService) {}
|
||||||
|
|
||||||
|
use(req: Request, _: Response, next: NextFunction) {
|
||||||
|
const context = {
|
||||||
|
requestId: randomUUID(),
|
||||||
|
correlationId: (req.headers['x-correlation-id'] as string) ?? undefined,
|
||||||
|
headers: req.headers as Record<string, string>,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ctx.run(context, next);
|
||||||
|
}
|
||||||
|
}
|
||||||
8
core/als/request-context.module.ts
Normal file
8
core/als/request-context.module.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { RequestContextService } from './request-context.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [RequestContextService],
|
||||||
|
exports: [RequestContextService],
|
||||||
|
})
|
||||||
|
export class RequestContextModule {}
|
||||||
58
core/als/request-context.service.ts
Normal file
58
core/als/request-context.service.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { AsyncLocalStorage } from 'async_hooks';
|
||||||
|
import { RequestContext } from './request-context.type';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
/**
|
||||||
|
* RequestContext holds per-request metadata including:
|
||||||
|
* - HTTP request
|
||||||
|
* - Auth payload (decoded JWT, optional)
|
||||||
|
* - Prisma transaction client (optional)
|
||||||
|
* - Correlation ID and other future cross-cutting concerns
|
||||||
|
*/
|
||||||
|
export class RequestContextService {
|
||||||
|
private readonly als = new AsyncLocalStorage<RequestContext>();
|
||||||
|
|
||||||
|
run(context: RequestContext, fn: () => void) {
|
||||||
|
this.als.run(context, fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(): RequestContext {
|
||||||
|
const store = this.als.getStore();
|
||||||
|
if (!store) {
|
||||||
|
throw new Error('RequestContext not initialized');
|
||||||
|
}
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
set<K extends keyof RequestContext>(key: K, value: RequestContext[K]) {
|
||||||
|
this.get()[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
get user() {
|
||||||
|
const user = this.get().user;
|
||||||
|
if (!user) throw new UnauthorizedException();
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
get tx() {
|
||||||
|
return this.get().tx;
|
||||||
|
}
|
||||||
|
|
||||||
|
set tx(tx) {
|
||||||
|
this.set('tx', tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
get isTransaction(): boolean {
|
||||||
|
return !!this.get().tx;
|
||||||
|
}
|
||||||
|
|
||||||
|
get orgId(): string {
|
||||||
|
return this.orgId
|
||||||
|
}
|
||||||
|
|
||||||
|
set orgId(id: string) {
|
||||||
|
this.set('orgId', id)
|
||||||
|
}
|
||||||
|
}
|
||||||
11
core/als/request-context.type.ts
Normal file
11
core/als/request-context.type.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Prisma } from 'prisma/generated/prisma/client';
|
||||||
|
import { JwtPayload } from 'src/auth/types';
|
||||||
|
|
||||||
|
export interface RequestContext {
|
||||||
|
requestId: string;
|
||||||
|
correlationId?: string;
|
||||||
|
headers: Record<string, string>;
|
||||||
|
user?: JwtPayload;
|
||||||
|
orgId?: string;
|
||||||
|
tx?: Prisma.TransactionClient;
|
||||||
|
}
|
||||||
8
core/mail/mail.module.ts
Normal file
8
core/mail/mail.module.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { MailService } from "./mail.service";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [MailService],
|
||||||
|
exports: [MailService]
|
||||||
|
})
|
||||||
|
export class MailModule { }
|
||||||
45
core/mail/mail.service.ts
Normal file
45
core/mail/mail.service.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
import * as nodemailer from "nodemailer";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MailService {
|
||||||
|
private transporter: nodemailer.Transporter;
|
||||||
|
private mailServiceAvailable = false;
|
||||||
|
|
||||||
|
constructor(private readonly configService: ConfigService) {
|
||||||
|
const mailId = this.configService.get<string | undefined>("MAIL_ID");
|
||||||
|
const mailPass = this.configService.get<string | undefined>("MAIL_PASS");
|
||||||
|
|
||||||
|
if (!mailId || !mailPass)
|
||||||
|
throw new Error("Make sure MAIL_ID and MAIL_PASS environment variables are set")
|
||||||
|
|
||||||
|
// Use secure in prod
|
||||||
|
// TODO: A table for failed emails to retry later(actually bullmq)
|
||||||
|
this.transporter = nodemailer.createTransport({
|
||||||
|
host: "smtp.gmail.com",
|
||||||
|
port: 587,
|
||||||
|
secure: false, // Use true for port 465, false for port 587
|
||||||
|
auth: {
|
||||||
|
user: mailId,
|
||||||
|
pass: mailPass,
|
||||||
|
},
|
||||||
|
from: mailId
|
||||||
|
});
|
||||||
|
|
||||||
|
this.mailServiceAvailable = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMail({ to, subject, body }: { to: string, subject: string, body: string }) {
|
||||||
|
if (!this.mailServiceAvailable)
|
||||||
|
throw new Error("Mail service not available")
|
||||||
|
|
||||||
|
this.transporter.sendMail(
|
||||||
|
{
|
||||||
|
to,
|
||||||
|
subject,
|
||||||
|
html: body
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
17
infra/db/docker-compose.yml
Normal file
17
infra/db/docker-compose.yml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:18
|
||||||
|
container_name: multi-tenant
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: admin
|
||||||
|
POSTGRES_DB: multi_tenant
|
||||||
|
ports:
|
||||||
|
- '5454:5432'
|
||||||
|
volumes:
|
||||||
|
- multiTenant:/var/lib/postgresql
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
multiTenant:
|
||||||
23
package.json
23
package.json
@@ -17,12 +17,28 @@
|
|||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:cov": "jest --coverage",
|
"test:cov": "jest --coverage",
|
||||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||||
|
"prisma:migrate": "prisma migrate dev --create-only",
|
||||||
|
"prisma:apply": "prisma migrate dev && prisma generate",
|
||||||
|
"prisma:deploy": "prisma migrate deploy",
|
||||||
|
"prisma:generate": "prisma generate"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@keyv/redis": "^5.1.6",
|
||||||
|
"@nestjs/cache-manager": "^3.1.0",
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
|
"@nestjs/jwt": "^11.0.2",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
|
"@nestjs/swagger": "^11.2.6",
|
||||||
|
"@prisma/adapter-pg": "^7.3.0",
|
||||||
|
"@prisma/client": "^7.3.0",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
|
"cache-manager": "^7.2.8",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.14.3",
|
||||||
|
"nodemailer": "^8.0.1",
|
||||||
|
"pg": "^8.18.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1"
|
||||||
},
|
},
|
||||||
@@ -30,13 +46,17 @@
|
|||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
"@eslint/js": "^9.18.0",
|
"@eslint/js": "^9.18.0",
|
||||||
"@nestjs/cli": "^11.0.0",
|
"@nestjs/cli": "^11.0.0",
|
||||||
|
"@nestjs/config": "^4.0.3",
|
||||||
"@nestjs/schematics": "^11.0.0",
|
"@nestjs/schematics": "^11.0.0",
|
||||||
"@nestjs/testing": "^11.0.1",
|
"@nestjs/testing": "^11.0.1",
|
||||||
"@swc/cli": "^0.6.0",
|
"@swc/cli": "^0.6.0",
|
||||||
"@swc/core": "^1.10.7",
|
"@swc/core": "^1.10.7",
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
|
"@types/nodemailer": "^7.0.11",
|
||||||
|
"@types/pg": "^8.16.0",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.18.0",
|
||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
@@ -44,6 +64,7 @@
|
|||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
|
"prisma": "^7.3.0",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"supertest": "^7.0.0",
|
"supertest": "^7.0.0",
|
||||||
"ts-jest": "^29.2.5",
|
"ts-jest": "^29.2.5",
|
||||||
|
|||||||
1269
pnpm-lock.yaml
generated
1269
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
14
prisma.config.ts
Normal file
14
prisma.config.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// This file was generated by Prisma, and assumes you have installed the following:
|
||||||
|
// npm install --save-dev prisma dotenv
|
||||||
|
import 'dotenv/config';
|
||||||
|
import { defineConfig } from 'prisma/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
schema: 'prisma/models',
|
||||||
|
migrations: {
|
||||||
|
path: 'prisma/migrations',
|
||||||
|
},
|
||||||
|
datasource: {
|
||||||
|
url: process.env['DATABASE_URL'],
|
||||||
|
},
|
||||||
|
});
|
||||||
39
prisma/generated/prisma/browser.ts
Normal file
39
prisma/generated/prisma/browser.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
|
/* eslint-disable */
|
||||||
|
// biome-ignore-all lint: generated file
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* This file should be your main import to use Prisma-related types and utilities in a browser.
|
||||||
|
* Use it to get access to models, enums, and input types.
|
||||||
|
*
|
||||||
|
* This file does not contain a `PrismaClient` class, nor several other helpers that are intended as server-side only.
|
||||||
|
* See `client.ts` for the standard, server-side entry point.
|
||||||
|
*
|
||||||
|
* 🟢 You can import this file directly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as Prisma from './internal/prismaNamespaceBrowser'
|
||||||
|
export { Prisma }
|
||||||
|
export * as $Enums from './enums'
|
||||||
|
export * from './enums';
|
||||||
|
/**
|
||||||
|
* Model OrganizationJoinRequest
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type OrganizationJoinRequest = Prisma.OrganizationJoinRequestModel
|
||||||
|
/**
|
||||||
|
* Model OrganizationUserJoinTable
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type OrganizationUserJoinTable = Prisma.OrganizationUserJoinTableModel
|
||||||
|
/**
|
||||||
|
* Model Organization
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Organization = Prisma.OrganizationModel
|
||||||
|
/**
|
||||||
|
* Model User
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type User = Prisma.UserModel
|
||||||
59
prisma/generated/prisma/client.ts
Normal file
59
prisma/generated/prisma/client.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
|
/* eslint-disable */
|
||||||
|
// biome-ignore-all lint: generated file
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* This file should be your main import to use Prisma. Through it you get access to all the models, enums, and input types.
|
||||||
|
* If you're looking for something you can import in the client-side of your application, please refer to the `browser.ts` file instead.
|
||||||
|
*
|
||||||
|
* 🟢 You can import this file directly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as process from 'node:process'
|
||||||
|
import * as path from 'node:path'
|
||||||
|
|
||||||
|
import * as runtime from "@prisma/client/runtime/client"
|
||||||
|
import * as $Enums from "./enums"
|
||||||
|
import * as $Class from "./internal/class"
|
||||||
|
import * as Prisma from "./internal/prismaNamespace"
|
||||||
|
|
||||||
|
export * as $Enums from './enums'
|
||||||
|
export * from "./enums"
|
||||||
|
/**
|
||||||
|
* ## Prisma Client
|
||||||
|
*
|
||||||
|
* Type-safe database client for TypeScript
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* const prisma = new PrismaClient()
|
||||||
|
* // Fetch zero or more OrganizationJoinRequests
|
||||||
|
* const organizationJoinRequests = await prisma.organizationJoinRequest.findMany()
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Read more in our [docs](https://pris.ly/d/client).
|
||||||
|
*/
|
||||||
|
export const PrismaClient = $Class.getPrismaClientClass()
|
||||||
|
export type PrismaClient<LogOpts extends Prisma.LogLevel = never, OmitOpts extends Prisma.PrismaClientOptions["omit"] = Prisma.PrismaClientOptions["omit"], ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>
|
||||||
|
export { Prisma }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model OrganizationJoinRequest
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type OrganizationJoinRequest = Prisma.OrganizationJoinRequestModel
|
||||||
|
/**
|
||||||
|
* Model OrganizationUserJoinTable
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type OrganizationUserJoinTable = Prisma.OrganizationUserJoinTableModel
|
||||||
|
/**
|
||||||
|
* Model Organization
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Organization = Prisma.OrganizationModel
|
||||||
|
/**
|
||||||
|
* Model User
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type User = Prisma.UserModel
|
||||||
434
prisma/generated/prisma/commonInputTypes.ts
Normal file
434
prisma/generated/prisma/commonInputTypes.ts
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
|
/* eslint-disable */
|
||||||
|
// biome-ignore-all lint: generated file
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* This file exports various common sort, input & filter types that are not directly linked to a particular model.
|
||||||
|
*
|
||||||
|
* 🟢 You can import this file directly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type * as runtime from "@prisma/client/runtime/client"
|
||||||
|
import * as $Enums from "./enums"
|
||||||
|
import type * as Prisma from "./internal/prismaNamespace"
|
||||||
|
|
||||||
|
|
||||||
|
export type StringFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
mode?: Prisma.QueryMode
|
||||||
|
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnumORGANIZATION_JOIN_REQUESTFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.ORGANIZATION_JOIN_REQUEST | Prisma.EnumORGANIZATION_JOIN_REQUESTFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.ORGANIZATION_JOIN_REQUEST[] | Prisma.ListEnumORGANIZATION_JOIN_REQUESTFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.ORGANIZATION_JOIN_REQUEST[] | Prisma.ListEnumORGANIZATION_JOIN_REQUESTFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumORGANIZATION_JOIN_REQUESTFilter<$PrismaModel> | $Enums.ORGANIZATION_JOIN_REQUEST
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnumORGANIZATION_JOIN_REQUEST_TYPEFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.ORGANIZATION_JOIN_REQUEST_TYPE | Prisma.EnumORGANIZATION_JOIN_REQUEST_TYPEFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.ORGANIZATION_JOIN_REQUEST_TYPE[] | Prisma.ListEnumORGANIZATION_JOIN_REQUEST_TYPEFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.ORGANIZATION_JOIN_REQUEST_TYPE[] | Prisma.ListEnumORGANIZATION_JOIN_REQUEST_TYPEFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumORGANIZATION_JOIN_REQUEST_TYPEFilter<$PrismaModel> | $Enums.ORGANIZATION_JOIN_REQUEST_TYPE
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DateTimeFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnumORG_ROLEFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.ORG_ROLE | Prisma.EnumORG_ROLEFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.ORG_ROLE[] | Prisma.ListEnumORG_ROLEFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.ORG_ROLE[] | Prisma.ListEnumORG_ROLEFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumORG_ROLEFilter<$PrismaModel> | $Enums.ORG_ROLE
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StringNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
mode?: Prisma.QueryMode
|
||||||
|
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SortOrderInput = {
|
||||||
|
sort: Prisma.SortOrder
|
||||||
|
nulls?: Prisma.NullsOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StringWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
mode?: Prisma.QueryMode
|
||||||
|
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedStringFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnumORGANIZATION_JOIN_REQUESTWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.ORGANIZATION_JOIN_REQUEST | Prisma.EnumORGANIZATION_JOIN_REQUESTFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.ORGANIZATION_JOIN_REQUEST[] | Prisma.ListEnumORGANIZATION_JOIN_REQUESTFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.ORGANIZATION_JOIN_REQUEST[] | Prisma.ListEnumORGANIZATION_JOIN_REQUESTFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumORGANIZATION_JOIN_REQUESTWithAggregatesFilter<$PrismaModel> | $Enums.ORGANIZATION_JOIN_REQUEST
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedEnumORGANIZATION_JOIN_REQUESTFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedEnumORGANIZATION_JOIN_REQUESTFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnumORGANIZATION_JOIN_REQUEST_TYPEWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.ORGANIZATION_JOIN_REQUEST_TYPE | Prisma.EnumORGANIZATION_JOIN_REQUEST_TYPEFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.ORGANIZATION_JOIN_REQUEST_TYPE[] | Prisma.ListEnumORGANIZATION_JOIN_REQUEST_TYPEFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.ORGANIZATION_JOIN_REQUEST_TYPE[] | Prisma.ListEnumORGANIZATION_JOIN_REQUEST_TYPEFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumORGANIZATION_JOIN_REQUEST_TYPEWithAggregatesFilter<$PrismaModel> | $Enums.ORGANIZATION_JOIN_REQUEST_TYPE
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedEnumORGANIZATION_JOIN_REQUEST_TYPEFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedEnumORGANIZATION_JOIN_REQUEST_TYPEFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnumORG_ROLEWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.ORG_ROLE | Prisma.EnumORG_ROLEFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.ORG_ROLE[] | Prisma.ListEnumORG_ROLEFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.ORG_ROLE[] | Prisma.ListEnumORG_ROLEFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumORG_ROLEWithAggregatesFilter<$PrismaModel> | $Enums.ORG_ROLE
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedEnumORG_ROLEFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedEnumORG_ROLEFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
mode?: Prisma.QueryMode
|
||||||
|
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnumUSER_ROLEFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.USER_ROLE | Prisma.EnumUSER_ROLEFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.USER_ROLE[] | Prisma.ListEnumUSER_ROLEFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.USER_ROLE[] | Prisma.ListEnumUSER_ROLEFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumUSER_ROLEFilter<$PrismaModel> | $Enums.USER_ROLE
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BoolNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel> | null
|
||||||
|
not?: Prisma.NestedBoolNullableFilter<$PrismaModel> | boolean | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DateTimeNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnumUSER_ROLEWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.USER_ROLE | Prisma.EnumUSER_ROLEFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.USER_ROLE[] | Prisma.ListEnumUSER_ROLEFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.USER_ROLE[] | Prisma.ListEnumUSER_ROLEFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumUSER_ROLEWithAggregatesFilter<$PrismaModel> | $Enums.USER_ROLE
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedEnumUSER_ROLEFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedEnumUSER_ROLEFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BoolNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel> | null
|
||||||
|
not?: Prisma.NestedBoolNullableWithAggregatesFilter<$PrismaModel> | boolean | null
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedBoolNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedBoolNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedStringFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedEnumORGANIZATION_JOIN_REQUESTFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.ORGANIZATION_JOIN_REQUEST | Prisma.EnumORGANIZATION_JOIN_REQUESTFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.ORGANIZATION_JOIN_REQUEST[] | Prisma.ListEnumORGANIZATION_JOIN_REQUESTFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.ORGANIZATION_JOIN_REQUEST[] | Prisma.ListEnumORGANIZATION_JOIN_REQUESTFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumORGANIZATION_JOIN_REQUESTFilter<$PrismaModel> | $Enums.ORGANIZATION_JOIN_REQUEST
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedEnumORGANIZATION_JOIN_REQUEST_TYPEFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.ORGANIZATION_JOIN_REQUEST_TYPE | Prisma.EnumORGANIZATION_JOIN_REQUEST_TYPEFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.ORGANIZATION_JOIN_REQUEST_TYPE[] | Prisma.ListEnumORGANIZATION_JOIN_REQUEST_TYPEFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.ORGANIZATION_JOIN_REQUEST_TYPE[] | Prisma.ListEnumORGANIZATION_JOIN_REQUEST_TYPEFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumORGANIZATION_JOIN_REQUEST_TYPEFilter<$PrismaModel> | $Enums.ORGANIZATION_JOIN_REQUEST_TYPE
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedDateTimeFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedEnumORG_ROLEFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.ORG_ROLE | Prisma.EnumORG_ROLEFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.ORG_ROLE[] | Prisma.ListEnumORG_ROLEFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.ORG_ROLE[] | Prisma.ListEnumORG_ROLEFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumORG_ROLEFilter<$PrismaModel> | $Enums.ORG_ROLE
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedStringNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedStringWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedStringFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedIntFilter<$PrismaModel = never> = {
|
||||||
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||||
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedIntFilter<$PrismaModel> | number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedEnumORGANIZATION_JOIN_REQUESTWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.ORGANIZATION_JOIN_REQUEST | Prisma.EnumORGANIZATION_JOIN_REQUESTFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.ORGANIZATION_JOIN_REQUEST[] | Prisma.ListEnumORGANIZATION_JOIN_REQUESTFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.ORGANIZATION_JOIN_REQUEST[] | Prisma.ListEnumORGANIZATION_JOIN_REQUESTFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumORGANIZATION_JOIN_REQUESTWithAggregatesFilter<$PrismaModel> | $Enums.ORGANIZATION_JOIN_REQUEST
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedEnumORGANIZATION_JOIN_REQUESTFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedEnumORGANIZATION_JOIN_REQUESTFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedEnumORGANIZATION_JOIN_REQUEST_TYPEWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.ORGANIZATION_JOIN_REQUEST_TYPE | Prisma.EnumORGANIZATION_JOIN_REQUEST_TYPEFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.ORGANIZATION_JOIN_REQUEST_TYPE[] | Prisma.ListEnumORGANIZATION_JOIN_REQUEST_TYPEFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.ORGANIZATION_JOIN_REQUEST_TYPE[] | Prisma.ListEnumORGANIZATION_JOIN_REQUEST_TYPEFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumORGANIZATION_JOIN_REQUEST_TYPEWithAggregatesFilter<$PrismaModel> | $Enums.ORGANIZATION_JOIN_REQUEST_TYPE
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedEnumORGANIZATION_JOIN_REQUEST_TYPEFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedEnumORGANIZATION_JOIN_REQUEST_TYPEFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedEnumORG_ROLEWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.ORG_ROLE | Prisma.EnumORG_ROLEFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.ORG_ROLE[] | Prisma.ListEnumORG_ROLEFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.ORG_ROLE[] | Prisma.ListEnumORG_ROLEFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumORG_ROLEWithAggregatesFilter<$PrismaModel> | $Enums.ORG_ROLE
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedEnumORG_ROLEFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedEnumORG_ROLEFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedIntNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedEnumUSER_ROLEFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.USER_ROLE | Prisma.EnumUSER_ROLEFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.USER_ROLE[] | Prisma.ListEnumUSER_ROLEFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.USER_ROLE[] | Prisma.ListEnumUSER_ROLEFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumUSER_ROLEFilter<$PrismaModel> | $Enums.USER_ROLE
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedBoolNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel> | null
|
||||||
|
not?: Prisma.NestedBoolNullableFilter<$PrismaModel> | boolean | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedEnumUSER_ROLEWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.USER_ROLE | Prisma.EnumUSER_ROLEFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.USER_ROLE[] | Prisma.ListEnumUSER_ROLEFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.USER_ROLE[] | Prisma.ListEnumUSER_ROLEFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumUSER_ROLEWithAggregatesFilter<$PrismaModel> | $Enums.USER_ROLE
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedEnumUSER_ROLEFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedEnumUSER_ROLEFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedBoolNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel> | null
|
||||||
|
not?: Prisma.NestedBoolNullableWithAggregatesFilter<$PrismaModel> | boolean | null
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedBoolNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedBoolNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
44
prisma/generated/prisma/enums.ts
Normal file
44
prisma/generated/prisma/enums.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
|
/* eslint-disable */
|
||||||
|
// biome-ignore-all lint: generated file
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* This file exports all enum related types from the schema.
|
||||||
|
*
|
||||||
|
* 🟢 You can import this file directly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const ORGANIZATION_JOIN_REQUEST = {
|
||||||
|
PENDING: 'PENDING',
|
||||||
|
ACCEPTED: 'ACCEPTED',
|
||||||
|
REJECTED: 'REJECTED',
|
||||||
|
CANCELLED: 'CANCELLED'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type ORGANIZATION_JOIN_REQUEST = (typeof ORGANIZATION_JOIN_REQUEST)[keyof typeof ORGANIZATION_JOIN_REQUEST]
|
||||||
|
|
||||||
|
|
||||||
|
export const ORGANIZATION_JOIN_REQUEST_TYPE = {
|
||||||
|
INVITED: 'INVITED',
|
||||||
|
REQUESTED: 'REQUESTED'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type ORGANIZATION_JOIN_REQUEST_TYPE = (typeof ORGANIZATION_JOIN_REQUEST_TYPE)[keyof typeof ORGANIZATION_JOIN_REQUEST_TYPE]
|
||||||
|
|
||||||
|
|
||||||
|
export const ORG_ROLE = {
|
||||||
|
owner: 'owner',
|
||||||
|
admin: 'admin',
|
||||||
|
member: 'member'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type ORG_ROLE = (typeof ORG_ROLE)[keyof typeof ORG_ROLE]
|
||||||
|
|
||||||
|
|
||||||
|
export const USER_ROLE = {
|
||||||
|
superadmin: 'superadmin',
|
||||||
|
user: 'user'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type USER_ROLE = (typeof USER_ROLE)[keyof typeof USER_ROLE]
|
||||||
222
prisma/generated/prisma/internal/class.ts
Normal file
222
prisma/generated/prisma/internal/class.ts
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
|
/* eslint-disable */
|
||||||
|
// biome-ignore-all lint: generated file
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* WARNING: This is an internal file that is subject to change!
|
||||||
|
*
|
||||||
|
* 🛑 Under no circumstances should you import this file directly! 🛑
|
||||||
|
*
|
||||||
|
* Please import the `PrismaClient` class from the `client.ts` file instead.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as runtime from "@prisma/client/runtime/client"
|
||||||
|
import type * as Prisma from "./prismaNamespace"
|
||||||
|
|
||||||
|
|
||||||
|
const config: runtime.GetPrismaClientConfig = {
|
||||||
|
"previewFeatures": [],
|
||||||
|
"clientVersion": "7.3.0",
|
||||||
|
"engineVersion": "9d6ad21cbbceab97458517b147a6a09ff43aa735",
|
||||||
|
"activeProvider": "postgresql",
|
||||||
|
"inlineSchema": "model OrganizationJoinRequest {\n id String @id @default(uuid())\n userId String\n orgId String\n status ORGANIZATION_JOIN_REQUEST @default(PENDING)\n requestType ORGANIZATION_JOIN_REQUEST_TYPE\n requestedOn DateTime @default(now())\n role ORG_ROLE @default(member)\n updatedAt DateTime @updatedAt\n rejectReason String?\n requestMessage String?\n\n organization Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n // @@unique([userId, orgId])\n @@index([userId, orgId])\n @@map(\"organization_join_request\")\n}\n\nenum ORGANIZATION_JOIN_REQUEST {\n PENDING\n ACCEPTED\n REJECTED\n CANCELLED\n}\n\nenum ORGANIZATION_JOIN_REQUEST_TYPE {\n INVITED\n REQUESTED\n}\n\nmodel OrganizationUserJoinTable {\n userId String\n orgId String\n role ORG_ROLE @default(member)\n joinedDate DateTime @default(now())\n\n organization Organization @relation(fields: [orgId], references: [id], onDelete: Restrict)\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@unique([userId, orgId])\n @@map(\"organization_user_join\")\n}\n\nenum ORG_ROLE {\n owner\n admin\n member\n}\n\nmodel Organization {\n id String @id @default(uuid())\n name String\n description String?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n members OrganizationUserJoinTable[]\n requestingMembers OrganizationJoinRequest[]\n\n @@map(\"organization\")\n}\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\n// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?\n// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"../generated/prisma\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n}\n\nmodel User {\n id String @id @default(uuid())\n firstName String\n middleName String?\n lastName String\n email String @unique\n password String\n role USER_ROLE @default(user)\n isVerified Boolean? @default(false) // TODO: Email using queue\n refreshToken String?\n profilePicture String?\n isDeleted Boolean? @default(false)\n deletedAt DateTime?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n organizations OrganizationUserJoinTable[]\n organizationsRequested OrganizationJoinRequest[]\n\n @@map(\"user\")\n}\n\nenum USER_ROLE {\n superadmin\n user\n}\n",
|
||||||
|
"runtimeDataModel": {
|
||||||
|
"models": {},
|
||||||
|
"enums": {},
|
||||||
|
"types": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.runtimeDataModel = JSON.parse("{\"models\":{\"OrganizationJoinRequest\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"orgId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"status\",\"kind\":\"enum\",\"type\":\"ORGANIZATION_JOIN_REQUEST\"},{\"name\":\"requestType\",\"kind\":\"enum\",\"type\":\"ORGANIZATION_JOIN_REQUEST_TYPE\"},{\"name\":\"requestedOn\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"role\",\"kind\":\"enum\",\"type\":\"ORG_ROLE\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"rejectReason\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"requestMessage\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"organization\",\"kind\":\"object\",\"type\":\"Organization\",\"relationName\":\"OrganizationToOrganizationJoinRequest\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"OrganizationJoinRequestToUser\"}],\"dbName\":\"organization_join_request\"},\"OrganizationUserJoinTable\":{\"fields\":[{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"orgId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"role\",\"kind\":\"enum\",\"type\":\"ORG_ROLE\"},{\"name\":\"joinedDate\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"organization\",\"kind\":\"object\",\"type\":\"Organization\",\"relationName\":\"OrganizationToOrganizationUserJoinTable\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"OrganizationUserJoinTableToUser\"}],\"dbName\":\"organization_user_join\"},\"Organization\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"members\",\"kind\":\"object\",\"type\":\"OrganizationUserJoinTable\",\"relationName\":\"OrganizationToOrganizationUserJoinTable\"},{\"name\":\"requestingMembers\",\"kind\":\"object\",\"type\":\"OrganizationJoinRequest\",\"relationName\":\"OrganizationToOrganizationJoinRequest\"}],\"dbName\":\"organization\"},\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"firstName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"middleName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"lastName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"password\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"role\",\"kind\":\"enum\",\"type\":\"USER_ROLE\"},{\"name\":\"isVerified\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"refreshToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"profilePicture\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"isDeleted\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"deletedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"organizations\",\"kind\":\"object\",\"type\":\"OrganizationUserJoinTable\",\"relationName\":\"OrganizationUserJoinTableToUser\"},{\"name\":\"organizationsRequested\",\"kind\":\"object\",\"type\":\"OrganizationJoinRequest\",\"relationName\":\"OrganizationJoinRequestToUser\"}],\"dbName\":\"user\"}},\"enums\":{},\"types\":{}}")
|
||||||
|
|
||||||
|
async function decodeBase64AsWasm(wasmBase64: string): Promise<WebAssembly.Module> {
|
||||||
|
const { Buffer } = await import('node:buffer')
|
||||||
|
const wasmArray = Buffer.from(wasmBase64, 'base64')
|
||||||
|
return new WebAssembly.Module(wasmArray)
|
||||||
|
}
|
||||||
|
|
||||||
|
config.compilerWasm = {
|
||||||
|
getRuntime: async () => await import("@prisma/client/runtime/query_compiler_fast_bg.postgresql.js"),
|
||||||
|
|
||||||
|
getQueryCompilerWasmModule: async () => {
|
||||||
|
const { wasm } = await import("@prisma/client/runtime/query_compiler_fast_bg.postgresql.wasm-base64.js")
|
||||||
|
return await decodeBase64AsWasm(wasm)
|
||||||
|
},
|
||||||
|
|
||||||
|
importName: "./query_compiler_fast_bg.js"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export type LogOptions<ClientOptions extends Prisma.PrismaClientOptions> =
|
||||||
|
'log' extends keyof ClientOptions ? ClientOptions['log'] extends Array<Prisma.LogLevel | Prisma.LogDefinition> ? Prisma.GetEvents<ClientOptions['log']> : never : never
|
||||||
|
|
||||||
|
export interface PrismaClientConstructor {
|
||||||
|
/**
|
||||||
|
* ## Prisma Client
|
||||||
|
*
|
||||||
|
* Type-safe database client for TypeScript
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* const prisma = new PrismaClient()
|
||||||
|
* // Fetch zero or more OrganizationJoinRequests
|
||||||
|
* const organizationJoinRequests = await prisma.organizationJoinRequest.findMany()
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Read more in our [docs](https://pris.ly/d/client).
|
||||||
|
*/
|
||||||
|
|
||||||
|
new <
|
||||||
|
Options extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions,
|
||||||
|
LogOpts extends LogOptions<Options> = LogOptions<Options>,
|
||||||
|
OmitOpts extends Prisma.PrismaClientOptions['omit'] = Options extends { omit: infer U } ? U : Prisma.PrismaClientOptions['omit'],
|
||||||
|
ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
|
||||||
|
>(options: Prisma.Subset<Options, Prisma.PrismaClientOptions> ): PrismaClient<LogOpts, OmitOpts, ExtArgs>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ## Prisma Client
|
||||||
|
*
|
||||||
|
* Type-safe database client for TypeScript
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* const prisma = new PrismaClient()
|
||||||
|
* // Fetch zero or more OrganizationJoinRequests
|
||||||
|
* const organizationJoinRequests = await prisma.organizationJoinRequest.findMany()
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Read more in our [docs](https://pris.ly/d/client).
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface PrismaClient<
|
||||||
|
in LogOpts extends Prisma.LogLevel = never,
|
||||||
|
in out OmitOpts extends Prisma.PrismaClientOptions['omit'] = undefined,
|
||||||
|
in out ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
|
||||||
|
> {
|
||||||
|
[K: symbol]: { types: Prisma.TypeMap<ExtArgs>['other'] }
|
||||||
|
|
||||||
|
$on<V extends LogOpts>(eventType: V, callback: (event: V extends 'query' ? Prisma.QueryEvent : Prisma.LogEvent) => void): PrismaClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect with the database
|
||||||
|
*/
|
||||||
|
$connect(): runtime.Types.Utils.JsPromise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect from the database
|
||||||
|
*/
|
||||||
|
$disconnect(): runtime.Types.Utils.JsPromise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a prepared raw query and returns the number of affected rows.
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* const result = await prisma.$executeRaw`UPDATE User SET cool = ${true} WHERE email = ${'user@email.com'};`
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Read more in our [docs](https://pris.ly/d/raw-queries).
|
||||||
|
*/
|
||||||
|
$executeRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<number>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a raw query and returns the number of affected rows.
|
||||||
|
* Susceptible to SQL injections, see documentation.
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* const result = await prisma.$executeRawUnsafe('UPDATE User SET cool = $1 WHERE email = $2 ;', true, 'user@email.com')
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Read more in our [docs](https://pris.ly/d/raw-queries).
|
||||||
|
*/
|
||||||
|
$executeRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<number>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a prepared raw query and returns the `SELECT` data.
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* const result = await prisma.$queryRaw`SELECT * FROM User WHERE id = ${1} OR email = ${'user@email.com'};`
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Read more in our [docs](https://pris.ly/d/raw-queries).
|
||||||
|
*/
|
||||||
|
$queryRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a raw query and returns the `SELECT` data.
|
||||||
|
* Susceptible to SQL injections, see documentation.
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* const result = await prisma.$queryRawUnsafe('SELECT * FROM User WHERE id = $1 OR email = $2;', 1, 'user@email.com')
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Read more in our [docs](https://pris.ly/d/raw-queries).
|
||||||
|
*/
|
||||||
|
$queryRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<T>;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows the running of a sequence of read/write operations that are guaranteed to either succeed or fail as a whole.
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* const [george, bob, alice] = await prisma.$transaction([
|
||||||
|
* prisma.user.create({ data: { name: 'George' } }),
|
||||||
|
* prisma.user.create({ data: { name: 'Bob' } }),
|
||||||
|
* prisma.user.create({ data: { name: 'Alice' } }),
|
||||||
|
* ])
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Read more in our [docs](https://www.prisma.io/docs/concepts/components/prisma-client/transactions).
|
||||||
|
*/
|
||||||
|
$transaction<P extends Prisma.PrismaPromise<any>[]>(arg: [...P], options?: { isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise<runtime.Types.Utils.UnwrapTuple<P>>
|
||||||
|
|
||||||
|
$transaction<R>(fn: (prisma: Omit<PrismaClient, runtime.ITXClientDenyList>) => runtime.Types.Utils.JsPromise<R>, options?: { maxWait?: number, timeout?: number, isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise<R>
|
||||||
|
|
||||||
|
$extends: runtime.Types.Extensions.ExtendsHook<"extends", Prisma.TypeMapCb<OmitOpts>, ExtArgs, runtime.Types.Utils.Call<Prisma.TypeMapCb<OmitOpts>, {
|
||||||
|
extArgs: ExtArgs
|
||||||
|
}>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `prisma.organizationJoinRequest`: Exposes CRUD operations for the **OrganizationJoinRequest** model.
|
||||||
|
* Example usage:
|
||||||
|
* ```ts
|
||||||
|
* // Fetch zero or more OrganizationJoinRequests
|
||||||
|
* const organizationJoinRequests = await prisma.organizationJoinRequest.findMany()
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
get organizationJoinRequest(): Prisma.OrganizationJoinRequestDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `prisma.organizationUserJoinTable`: Exposes CRUD operations for the **OrganizationUserJoinTable** model.
|
||||||
|
* Example usage:
|
||||||
|
* ```ts
|
||||||
|
* // Fetch zero or more OrganizationUserJoinTables
|
||||||
|
* const organizationUserJoinTables = await prisma.organizationUserJoinTable.findMany()
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
get organizationUserJoinTable(): Prisma.OrganizationUserJoinTableDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `prisma.organization`: Exposes CRUD operations for the **Organization** model.
|
||||||
|
* Example usage:
|
||||||
|
* ```ts
|
||||||
|
* // Fetch zero or more Organizations
|
||||||
|
* const organizations = await prisma.organization.findMany()
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
get organization(): Prisma.OrganizationDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `prisma.user`: Exposes CRUD operations for the **User** model.
|
||||||
|
* Example usage:
|
||||||
|
* ```ts
|
||||||
|
* // Fetch zero or more Users
|
||||||
|
* const users = await prisma.user.findMany()
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
get user(): Prisma.UserDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPrismaClientClass(): PrismaClientConstructor {
|
||||||
|
return runtime.getPrismaClient(config) as unknown as PrismaClientConstructor
|
||||||
|
}
|
||||||
1097
prisma/generated/prisma/internal/prismaNamespace.ts
Normal file
1097
prisma/generated/prisma/internal/prismaNamespace.ts
Normal file
File diff suppressed because it is too large
Load Diff
155
prisma/generated/prisma/internal/prismaNamespaceBrowser.ts
Normal file
155
prisma/generated/prisma/internal/prismaNamespaceBrowser.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
|
/* eslint-disable */
|
||||||
|
// biome-ignore-all lint: generated file
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* WARNING: This is an internal file that is subject to change!
|
||||||
|
*
|
||||||
|
* 🛑 Under no circumstances should you import this file directly! 🛑
|
||||||
|
*
|
||||||
|
* All exports from this file are wrapped under a `Prisma` namespace object in the browser.ts file.
|
||||||
|
* While this enables partial backward compatibility, it is not part of the stable public API.
|
||||||
|
*
|
||||||
|
* If you are looking for your Models, Enums, and Input Types, please import them from the respective
|
||||||
|
* model files in the `model` directory!
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as runtime from "@prisma/client/runtime/index-browser"
|
||||||
|
|
||||||
|
export type * from '../models'
|
||||||
|
export type * from './prismaNamespace'
|
||||||
|
|
||||||
|
export const Decimal = runtime.Decimal
|
||||||
|
|
||||||
|
|
||||||
|
export const NullTypes = {
|
||||||
|
DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull),
|
||||||
|
JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull),
|
||||||
|
AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull),
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
|
||||||
|
*
|
||||||
|
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||||
|
*/
|
||||||
|
export const DbNull = runtime.DbNull
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
|
||||||
|
*
|
||||||
|
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||||
|
*/
|
||||||
|
export const JsonNull = runtime.JsonNull
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
|
||||||
|
*
|
||||||
|
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||||
|
*/
|
||||||
|
export const AnyNull = runtime.AnyNull
|
||||||
|
|
||||||
|
|
||||||
|
export const ModelName = {
|
||||||
|
OrganizationJoinRequest: 'OrganizationJoinRequest',
|
||||||
|
OrganizationUserJoinTable: 'OrganizationUserJoinTable',
|
||||||
|
Organization: 'Organization',
|
||||||
|
User: 'User'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Enums
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const TransactionIsolationLevel = runtime.makeStrictEnum({
|
||||||
|
ReadUncommitted: 'ReadUncommitted',
|
||||||
|
ReadCommitted: 'ReadCommitted',
|
||||||
|
RepeatableRead: 'RepeatableRead',
|
||||||
|
Serializable: 'Serializable'
|
||||||
|
} as const)
|
||||||
|
|
||||||
|
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
|
||||||
|
|
||||||
|
|
||||||
|
export const OrganizationJoinRequestScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
userId: 'userId',
|
||||||
|
orgId: 'orgId',
|
||||||
|
status: 'status',
|
||||||
|
requestType: 'requestType',
|
||||||
|
requestedOn: 'requestedOn',
|
||||||
|
role: 'role',
|
||||||
|
updatedAt: 'updatedAt',
|
||||||
|
rejectReason: 'rejectReason',
|
||||||
|
requestMessage: 'requestMessage'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type OrganizationJoinRequestScalarFieldEnum = (typeof OrganizationJoinRequestScalarFieldEnum)[keyof typeof OrganizationJoinRequestScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const OrganizationUserJoinTableScalarFieldEnum = {
|
||||||
|
userId: 'userId',
|
||||||
|
orgId: 'orgId',
|
||||||
|
role: 'role',
|
||||||
|
joinedDate: 'joinedDate'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type OrganizationUserJoinTableScalarFieldEnum = (typeof OrganizationUserJoinTableScalarFieldEnum)[keyof typeof OrganizationUserJoinTableScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const OrganizationScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
name: 'name',
|
||||||
|
description: 'description',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type OrganizationScalarFieldEnum = (typeof OrganizationScalarFieldEnum)[keyof typeof OrganizationScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const UserScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
firstName: 'firstName',
|
||||||
|
middleName: 'middleName',
|
||||||
|
lastName: 'lastName',
|
||||||
|
email: 'email',
|
||||||
|
password: 'password',
|
||||||
|
role: 'role',
|
||||||
|
isVerified: 'isVerified',
|
||||||
|
refreshToken: 'refreshToken',
|
||||||
|
profilePicture: 'profilePicture',
|
||||||
|
isDeleted: 'isDeleted',
|
||||||
|
deletedAt: 'deletedAt',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const SortOrder = {
|
||||||
|
asc: 'asc',
|
||||||
|
desc: 'desc'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
|
||||||
|
|
||||||
|
|
||||||
|
export const QueryMode = {
|
||||||
|
default: 'default',
|
||||||
|
insensitive: 'insensitive'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode]
|
||||||
|
|
||||||
|
|
||||||
|
export const NullsOrder = {
|
||||||
|
first: 'first',
|
||||||
|
last: 'last'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
|
||||||
|
|
||||||
15
prisma/generated/prisma/models.ts
Normal file
15
prisma/generated/prisma/models.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
|
/* eslint-disable */
|
||||||
|
// biome-ignore-all lint: generated file
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* This is a barrel export file for all models and their related types.
|
||||||
|
*
|
||||||
|
* 🟢 You can import this file directly.
|
||||||
|
*/
|
||||||
|
export type * from './models/OrganizationJoinRequest'
|
||||||
|
export type * from './models/OrganizationUserJoinTable'
|
||||||
|
export type * from './models/Organization'
|
||||||
|
export type * from './models/User'
|
||||||
|
export type * from './commonInputTypes'
|
||||||
1436
prisma/generated/prisma/models/Organization.ts
Normal file
1436
prisma/generated/prisma/models/Organization.ts
Normal file
File diff suppressed because it is too large
Load Diff
1689
prisma/generated/prisma/models/OrganizationJoinRequest.ts
Normal file
1689
prisma/generated/prisma/models/OrganizationJoinRequest.ts
Normal file
File diff suppressed because it is too large
Load Diff
1425
prisma/generated/prisma/models/OrganizationUserJoinTable.ts
Normal file
1425
prisma/generated/prisma/models/OrganizationUserJoinTable.ts
Normal file
File diff suppressed because it is too large
Load Diff
1772
prisma/generated/prisma/models/User.ts
Normal file
1772
prisma/generated/prisma/models/User.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,65 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "ORGANIZATION_JOIN_REQUEST" AS ENUM ('PENDING', 'ACCEPTED', 'REJECTED');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "ORG_ROLE" AS ENUM ('admin', 'user');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "USER_ROLE" AS ENUM ('superadmin', 'ordinary');
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "organization_join_request" (
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"orgId" TEXT NOT NULL,
|
||||||
|
"status" "ORGANIZATION_JOIN_REQUEST" NOT NULL DEFAULT 'PENDING',
|
||||||
|
"requestedOn" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"rejectReason" TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "organization_user_join" (
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"orgId" TEXT NOT NULL,
|
||||||
|
"role" "ORG_ROLE" NOT NULL DEFAULT 'user',
|
||||||
|
"joinedDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "organization" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "organization_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "user" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"firstName" TEXT NOT NULL,
|
||||||
|
"middleName" TEXT,
|
||||||
|
"lastName" TEXT NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"password" TEXT NOT NULL,
|
||||||
|
"role" "USER_ROLE" NOT NULL DEFAULT 'ordinary',
|
||||||
|
"isVerified" BOOLEAN DEFAULT false,
|
||||||
|
"refreshToken" TEXT,
|
||||||
|
"profilePicture" TEXT,
|
||||||
|
"isDeleted" BOOLEAN DEFAULT false,
|
||||||
|
"deletedAt" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "user_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "organization_join_request_userId_orgId_key" ON "organization_join_request"("userId", "orgId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "organization_user_join_userId_orgId_key" ON "organization_user_join"("userId", "orgId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "user_email_key" ON "user"("email");
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- The values [user] on the enum `ORG_ROLE` will be removed. If these variants are still used in the database, this will fail.
|
||||||
|
- The values [ordinary] on the enum `USER_ROLE` will be removed. If these variants are still used in the database, this will fail.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- AlterEnum
|
||||||
|
BEGIN;
|
||||||
|
CREATE TYPE "ORG_ROLE_new" AS ENUM ('owner', 'admin', 'member');
|
||||||
|
ALTER TABLE "public"."organization_user_join" ALTER COLUMN "role" DROP DEFAULT;
|
||||||
|
ALTER TABLE "organization_user_join" ALTER COLUMN "role" TYPE "ORG_ROLE_new" USING ("role"::text::"ORG_ROLE_new");
|
||||||
|
ALTER TYPE "ORG_ROLE" RENAME TO "ORG_ROLE_old";
|
||||||
|
ALTER TYPE "ORG_ROLE_new" RENAME TO "ORG_ROLE";
|
||||||
|
DROP TYPE "public"."ORG_ROLE_old";
|
||||||
|
ALTER TABLE "organization_user_join" ALTER COLUMN "role" SET DEFAULT 'member';
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- AlterEnum
|
||||||
|
BEGIN;
|
||||||
|
CREATE TYPE "USER_ROLE_new" AS ENUM ('superadmin', 'user');
|
||||||
|
ALTER TABLE "public"."user" ALTER COLUMN "role" DROP DEFAULT;
|
||||||
|
ALTER TABLE "user" ALTER COLUMN "role" TYPE "USER_ROLE_new" USING ("role"::text::"USER_ROLE_new");
|
||||||
|
ALTER TYPE "USER_ROLE" RENAME TO "USER_ROLE_old";
|
||||||
|
ALTER TYPE "USER_ROLE_new" RENAME TO "USER_ROLE";
|
||||||
|
DROP TYPE "public"."USER_ROLE_old";
|
||||||
|
ALTER TABLE "user" ALTER COLUMN "role" SET DEFAULT 'user';
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "organization_user_join" ALTER COLUMN "role" SET DEFAULT 'member';
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "user" ALTER COLUMN "role" SET DEFAULT 'user';
|
||||||
1
prisma/migrations/20260221131903_new_enum/migration.sql
Normal file
1
prisma/migrations/20260221131903_new_enum/migration.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
-- This is an empty migration.
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Added the required column `requestType` to the `organization_join_request` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "ORGANIZATION_JOIN_REQUEST_TYPE" AS ENUM ('INVITED', 'REQUESTED');
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "organization_join_request" ADD COLUMN "requestType" "ORGANIZATION_JOIN_REQUEST_TYPE" NOT NULL;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Added the required column `updatedAt` to the `organization_join_request` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "organization_join_request" ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "organization_join_request" ADD CONSTRAINT "organization_join_request_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "organization_join_request" ADD CONSTRAINT "organization_join_request_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "organization_user_join" ADD CONSTRAINT "organization_user_join_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "organization_user_join" ADD CONSTRAINT "organization_user_join_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "organization_join_request" ADD COLUMN "role" "ORG_ROLE" NOT NULL DEFAULT 'member';
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "organization_join_request" ADD COLUMN "requestMessage" TEXT;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
-- This is an empty migration.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterEnum
|
||||||
|
ALTER TYPE "ORGANIZATION_JOIN_REQUEST" ADD VALUE 'CANCELLED';
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- The required column `id` was added to the `organization_join_request` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "organization_join_request_userId_orgId_key";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "organization_join_request" ADD COLUMN "id" TEXT NOT NULL,
|
||||||
|
ADD CONSTRAINT "organization_join_request_pkey" PRIMARY KEY ("id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "organization_join_request_userId_orgId_idx" ON "organization_join_request"("userId", "orgId");
|
||||||
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (e.g., Git)
|
||||||
|
provider = "postgresql"
|
||||||
31
prisma/models/organization-join-request.prisma
Normal file
31
prisma/models/organization-join-request.prisma
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
model OrganizationJoinRequest {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
userId String
|
||||||
|
orgId String
|
||||||
|
status ORGANIZATION_JOIN_REQUEST @default(PENDING)
|
||||||
|
requestType ORGANIZATION_JOIN_REQUEST_TYPE
|
||||||
|
requestedOn DateTime @default(now())
|
||||||
|
role ORG_ROLE @default(member)
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
rejectReason String?
|
||||||
|
requestMessage String?
|
||||||
|
|
||||||
|
organization Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
// @@unique([userId, orgId])
|
||||||
|
@@index([userId, orgId])
|
||||||
|
@@map("organization_join_request")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ORGANIZATION_JOIN_REQUEST {
|
||||||
|
PENDING
|
||||||
|
ACCEPTED
|
||||||
|
REJECTED
|
||||||
|
CANCELLED
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ORGANIZATION_JOIN_REQUEST_TYPE {
|
||||||
|
INVITED
|
||||||
|
REQUESTED
|
||||||
|
}
|
||||||
18
prisma/models/organization-user-join.prisma
Normal file
18
prisma/models/organization-user-join.prisma
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
model OrganizationUserJoinTable {
|
||||||
|
userId String
|
||||||
|
orgId String
|
||||||
|
role ORG_ROLE @default(member)
|
||||||
|
joinedDate DateTime @default(now())
|
||||||
|
|
||||||
|
organization Organization @relation(fields: [orgId], references: [id], onDelete: Restrict)
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([userId, orgId])
|
||||||
|
@@map("organization_user_join")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ORG_ROLE {
|
||||||
|
owner
|
||||||
|
admin
|
||||||
|
member
|
||||||
|
}
|
||||||
13
prisma/models/organization.prisma
Normal file
13
prisma/models/organization.prisma
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
model Organization {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
name String
|
||||||
|
description String?
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
members OrganizationUserJoinTable[]
|
||||||
|
requestingMembers OrganizationJoinRequest[]
|
||||||
|
|
||||||
|
@@map("organization")
|
||||||
|
}
|
||||||
14
prisma/models/schema.prisma
Normal file
14
prisma/models/schema.prisma
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// This is your Prisma schema file,
|
||||||
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||||
|
|
||||||
|
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
|
||||||
|
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client"
|
||||||
|
output = "../generated/prisma"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
}
|
||||||
27
prisma/models/user.prisma
Normal file
27
prisma/models/user.prisma
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
model User {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
firstName String
|
||||||
|
middleName String?
|
||||||
|
lastName String
|
||||||
|
email String @unique
|
||||||
|
password String
|
||||||
|
role USER_ROLE @default(user)
|
||||||
|
isVerified Boolean? @default(false) // TODO: Email using queue
|
||||||
|
refreshToken String?
|
||||||
|
profilePicture String?
|
||||||
|
isDeleted Boolean? @default(false)
|
||||||
|
deletedAt DateTime?
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
organizations OrganizationUserJoinTable[]
|
||||||
|
organizationsRequested OrganizationJoinRequest[]
|
||||||
|
|
||||||
|
@@map("user")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum USER_ROLE {
|
||||||
|
superadmin
|
||||||
|
user
|
||||||
|
}
|
||||||
104
roadmap.md
Normal file
104
roadmap.md
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
```
|
||||||
|
|
||||||
|
``` md
|
||||||
|
## Immediate
|
||||||
|
|
||||||
|
User registers: AuthService saves the user and emits a UserSignedUpEvent.
|
||||||
|
|
||||||
|
Listener catches it: The WelcomeEmailListener hears the event.
|
||||||
|
|
||||||
|
Queue handles it: The listener adds a job to BullMQ called send-welcome-email.
|
||||||
|
|
||||||
|
Worker processes it: A separate background worker picks up the job, tries to send the email, and automatically handles retries if it fails.
|
||||||
|
```
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
# 🏗️ SaaS Architect’s Roadmap: NestJS & DevOps
|
||||||
|
|
||||||
|
This document serves as a strategic guide for evolving from a developer to an architect. It balances the "What" (the technology) with the "How" (the engineering mindset).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Phase 1: The "What" — Advanced Backend Features
|
||||||
|
*Focus: Scaling complexity and ensuring production-grade reliability.*
|
||||||
|
|
||||||
|
### 1. Advanced Multi-Tenancy Architecture
|
||||||
|
* **The Goal:** Move beyond simple `organization_id` filters to true data isolation.
|
||||||
|
* **Concepts:** Shared Database/Separate Schema or **Row Level Security (RLS)**.
|
||||||
|
* **Challenge:** Implement a NestJS `Interceptor` or `Provider` that extracts a `tenant_id` from the request and automatically switches the database context/schema.
|
||||||
|
|
||||||
|
|
||||||
|
### 2. Background Tasks & Reliability
|
||||||
|
* **The Goal:** Decouple time-consuming tasks from the request/response cycle.
|
||||||
|
* **Tech Stack:** **BullMQ** + **Redis**.
|
||||||
|
* **Implementation:** Offload email invitations, image processing, and report generation to a separate "Worker" instance.
|
||||||
|
|
||||||
|
|
||||||
|
### 3. Real-Time Interactions
|
||||||
|
* **The Goal:** Create a reactive, living dashboard.
|
||||||
|
* **Tech Stack:** **Socket.io** (WebSockets).
|
||||||
|
* **Implementation:** Sync team membership changes or notification counts in real-time without requiring a page refresh.
|
||||||
|
|
||||||
|
### 4. Billing & Webhooks (The SaaS "S")
|
||||||
|
* **The Goal:** Turn a project into a business.
|
||||||
|
* **Tech Stack:** **Stripe API**.
|
||||||
|
* **Learning Point:** Handle **Idempotency**. Ensure that if Stripe sends a "Payment Success" webhook twice, you only upgrade the user's account once.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧠 Phase 2: The "How" — Architectural Thinking
|
||||||
|
*Focus: Designing systems that don't break under edge cases.*
|
||||||
|
|
||||||
|
### 1. State Machine Logic
|
||||||
|
Stop thinking in "CRUD" (Create, Read, Update, Delete) and start thinking in **Transitions**.
|
||||||
|
* **Entity Lifecycle:** An Invitation isn't just a row; it's a flow: `Pending` → `Accepted` | `Declined` | `Expired`.
|
||||||
|
* **Action:** Explicitly define what happens during every status change (e.g., "On Accept: Create User, Join Team, Clear Invite Cache").
|
||||||
|
|
||||||
|
|
||||||
|
### 2. Atomic Transactions
|
||||||
|
Never perform "partial" updates.
|
||||||
|
* **Strategy:** Use Database Transactions (`BEGIN...COMMIT`) for any operation touching multiple tables. If the user joins the team but the invitation update fails, the whole thing must roll back.
|
||||||
|
|
||||||
|
### 3. Event-Driven Side Effects
|
||||||
|
Don't bloat your services with 10 different tasks.
|
||||||
|
* **Pattern:** Use an **Event Emitter**.
|
||||||
|
* **Logic:** `UserService.create()` emits a `USER_CREATED` event. Separate listeners then handle the email, the analytics ping, and the Slack notification independently.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ☁️ Phase 3: The DevOps Pillar
|
||||||
|
*Focus: Automation, Observability, and "Infrastructure as Code".*
|
||||||
|
|
||||||
|
### 1. Observability (Stop Guessing)
|
||||||
|
* **Structured Logging:** Use **Pino** to output JSON logs that include `trace_id` and `tenant_id`.
|
||||||
|
* **Tracing:** Implement **OpenTelemetry** to visualize the lifecycle of a request across different services.
|
||||||
|
|
||||||
|
|
||||||
|
### 2. Infrastructure as Code (IaC)
|
||||||
|
* **The Goal:** Destroy and rebuild your entire cloud environment in minutes.
|
||||||
|
* **Tech Stack:** **Terraform**.
|
||||||
|
* **Action:** Codify your GCP/Azure VMs, VPC Firewalls, and Managed Databases so you never have to click buttons in a portal manually.
|
||||||
|
|
||||||
|
### 3. Hardened CI/CD
|
||||||
|
* **The Goal:** Catch errors before they hit production.
|
||||||
|
* **Action:** Add "Structure Verification" steps in your GitHub Actions to ensure the build folder matches what the server expects (e.g., checking for Next.js standalone chunks).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤖 The "Anti-GIGO" AI Workflow
|
||||||
|
*How to use AI as an architect, not a typist.*
|
||||||
|
|
||||||
|
1. **Sketch First:** Define your DB schema and logic flow on a whiteboard before opening the chat.
|
||||||
|
2. **Hunt Edge Cases:** Ask yourself: "What if the user is already in another org? What if the database is locked?"
|
||||||
|
3. **Prompt as an Architect:**
|
||||||
|
* *Bad:* "Make an invite system."
|
||||||
|
* *Good:* "Design a NestJS service for an Invitation State Machine. It must use a database transaction, handle BullMQ for email side-effects, and include a unique constraint on (email, organization_id)."
|
||||||
|
4. **Audit the Output:** Check for N+1 query problems, lack of input validation, and missing error handling.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Next Steps for You:**
|
||||||
|
* Start with the **Invitation State Machine**.
|
||||||
|
* Implement **BullMQ** for the invitation email.
|
||||||
|
* Write your first **Terraform** script to manage your GCP firewall.
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Controller, Get } from '@nestjs/common';
|
import { Controller, Get } from '@nestjs/common';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
|
import { Public } from './auth/decorators';
|
||||||
|
|
||||||
@Controller()
|
@Controller('')
|
||||||
|
@Public(true)
|
||||||
export class AppController {
|
export class AppController {
|
||||||
constructor(private readonly appService: AppService) {}
|
constructor(private readonly appService: AppService) {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,57 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Logger, MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
|
import { UserModule } from './user/user.module';
|
||||||
|
import { AuthModule } from './auth/auth.module';
|
||||||
|
import { RequestContextMiddleware } from 'core/als/request-context.middleware';
|
||||||
|
import { RequestContextModule } from 'core/als/request-context.module';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { PrismaModule } from './prisma/prisma.module';
|
||||||
|
import { APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core';
|
||||||
|
import { ResponseInterceptor } from 'common/interceptors/response.interceptor';
|
||||||
|
import { HttpExceptionFilter } from 'common/exceptions/exception-filter';
|
||||||
|
import { OrganizationModule } from './organization/organization.module';
|
||||||
|
import { OrganizationMembershipModule } from './organization-membership/organization-membership.module';
|
||||||
|
import { AuthorizationModule } from './authorization/authorization.module';
|
||||||
|
import { CacheModule } from './cache/cache.module';
|
||||||
|
import { MailModule } from 'core/mail/mail.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
}),
|
||||||
|
UserModule,
|
||||||
|
AuthModule,
|
||||||
|
RequestContextModule,
|
||||||
|
PrismaModule,
|
||||||
|
OrganizationModule,
|
||||||
|
OrganizationMembershipModule,
|
||||||
|
AuthorizationModule,
|
||||||
|
CacheModule,
|
||||||
|
MailModule
|
||||||
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [
|
||||||
|
AppService,
|
||||||
|
Logger,
|
||||||
|
{
|
||||||
|
provide: APP_INTERCEPTOR,
|
||||||
|
useClass: ResponseInterceptor,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: APP_FILTER,
|
||||||
|
useClass: HttpExceptionFilter,
|
||||||
|
},
|
||||||
|
// NOTE: Auto cache controller response
|
||||||
|
// {
|
||||||
|
// provide: APP_INTERCEPTOR,
|
||||||
|
// useClass: CacheInterceptor,
|
||||||
|
// },
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule implements NestModule {
|
||||||
|
configure(consumer: MiddlewareConsumer) {
|
||||||
|
consumer.apply(RequestContextMiddleware).forRoutes('*paths');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
18
src/auth/auth.controller.spec.ts
Normal file
18
src/auth/auth.controller.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { AuthController } from './auth.controller';
|
||||||
|
|
||||||
|
describe('AuthController', () => {
|
||||||
|
let controller: AuthController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [AuthController],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<AuthController>(AuthController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
57
src/auth/auth.controller.ts
Normal file
57
src/auth/auth.controller.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
Post,
|
||||||
|
Res,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { ApiOperation } from '@nestjs/swagger';
|
||||||
|
import {
|
||||||
|
LoginUserRequestDTO,
|
||||||
|
LoginUserResponseDTO,
|
||||||
|
RegisterUserRequestDTO,
|
||||||
|
} from './dto';
|
||||||
|
import { Response } from 'express';
|
||||||
|
import { DataResponse } from 'common/http';
|
||||||
|
import { Public } from './decorators';
|
||||||
|
|
||||||
|
@Controller('auth')
|
||||||
|
@Public()
|
||||||
|
export class AuthController {
|
||||||
|
constructor(private readonly authService: AuthService) {}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: 'User login' })
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Post('/login')
|
||||||
|
async login(
|
||||||
|
@Body() body: LoginUserRequestDTO,
|
||||||
|
@Res({ passthrough: true }) response: Response,
|
||||||
|
): Promise<DataResponse<LoginUserResponseDTO>> {
|
||||||
|
const { accessToken, refreshToken, user } =
|
||||||
|
await this.authService.login(body);
|
||||||
|
|
||||||
|
response.cookie('accessToken', accessToken);
|
||||||
|
|
||||||
|
return new DataResponse(
|
||||||
|
new LoginUserResponseDTO(user, accessToken, refreshToken),
|
||||||
|
'Login successfull',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: 'User register' })
|
||||||
|
@HttpCode(HttpStatus.CREATED)
|
||||||
|
@Post('/register')
|
||||||
|
async register(@Body() body: RegisterUserRequestDTO): Promise<string> {
|
||||||
|
await this.authService.register(body);
|
||||||
|
|
||||||
|
return 'Registered successfully. Login to continue.';
|
||||||
|
}
|
||||||
|
|
||||||
|
logout() {}
|
||||||
|
|
||||||
|
forgotPassword() {}
|
||||||
|
|
||||||
|
regenTokens() {}
|
||||||
|
}
|
||||||
28
src/auth/auth.module.ts
Normal file
28
src/auth/auth.module.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { AuthController } from './auth.controller';
|
||||||
|
import { APP_GUARD, Reflector } from '@nestjs/core';
|
||||||
|
import { AuthGuard } from './guards/auth.guard';
|
||||||
|
import { UserModule } from 'src/user/user.module';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
import { RequestContextModule } from 'core/als/request-context.module';
|
||||||
|
import { MailModule } from 'core/mail/mail.module';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [
|
||||||
|
AuthService,
|
||||||
|
{
|
||||||
|
provide: APP_GUARD,
|
||||||
|
useClass: AuthGuard,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
controllers: [AuthController],
|
||||||
|
imports: [
|
||||||
|
UserModule,
|
||||||
|
JwtModule,
|
||||||
|
RequestContextModule,
|
||||||
|
MailModule
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AuthModule { }
|
||||||
18
src/auth/auth.service.spec.ts
Normal file
18
src/auth/auth.service.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
|
||||||
|
describe('AuthService', () => {
|
||||||
|
let service: AuthService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [AuthService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<AuthService>(AuthService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
82
src/auth/auth.service.ts
Normal file
82
src/auth/auth.service.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { Public } from './decorators';
|
||||||
|
import { LoginUserRequestDTO, RegisterUserRequestDTO } from './dto';
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
import { UserService } from 'src/user/user.service';
|
||||||
|
import { TokenInputType } from './types';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import { MailService } from 'core/mail/mail.service';
|
||||||
|
import EmailTemplates from 'common/emails';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
@Public()
|
||||||
|
export class AuthService {
|
||||||
|
constructor(
|
||||||
|
private readonly userService: UserService,
|
||||||
|
private readonly jwtService: JwtService,
|
||||||
|
private readonly mailService: MailService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
async register(dto: RegisterUserRequestDTO) {
|
||||||
|
const hashedPassword = await bcrypt.hash(dto.password, 10);
|
||||||
|
await this.userService.createUserWithPassword({
|
||||||
|
...dto,
|
||||||
|
password: hashedPassword,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.mailService.sendMail({
|
||||||
|
to: dto.email,
|
||||||
|
subject: "Welcome onboard",
|
||||||
|
body: EmailTemplates.welcomeToApp
|
||||||
|
})
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(dto: LoginUserRequestDTO) {
|
||||||
|
const user = await this.userService.findUserForAuth(dto.email);
|
||||||
|
if (!user) throw new UnauthorizedException('Invalid credentials.');
|
||||||
|
|
||||||
|
const passwordMatch = await bcrypt.compare(dto.password, user.password);
|
||||||
|
if (!passwordMatch) throw new UnauthorizedException('Invalid credentials.');
|
||||||
|
|
||||||
|
const token = {
|
||||||
|
userId: user.id,
|
||||||
|
email: user.email,
|
||||||
|
role: user.role,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Store more info: orgId, orgRole, etc
|
||||||
|
const { accessToken, refreshToken } = await this.genSignedTokens(token);
|
||||||
|
const hashedRefreshToken = await bcrypt.hash(refreshToken, 10);
|
||||||
|
|
||||||
|
await this.userService.updateRefreshToken(user.id, hashedRefreshToken);
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
|
user,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
logout() { }
|
||||||
|
|
||||||
|
resetPassword() { }
|
||||||
|
|
||||||
|
// TODO: Use nest jwt
|
||||||
|
private async genSignedTokens(token: TokenInputType) {
|
||||||
|
const accessToken = await this.jwtService.signAsync(token, {
|
||||||
|
secret: 'demo',
|
||||||
|
});
|
||||||
|
|
||||||
|
const refreshToken = await this.jwtService.signAsync(
|
||||||
|
{
|
||||||
|
userId: token.userId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
secret: 'demo',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return { accessToken, refreshToken };
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/auth/decorators/authorization.decorator.ts
Normal file
16
src/auth/decorators/authorization.decorator.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { applyDecorators, SetMetadata, UseGuards } from "@nestjs/common";
|
||||||
|
import { CAN_PERFORM_KEY } from "common/keys";
|
||||||
|
import { ORG_ROLE } from "prisma/generated/prisma/enums";
|
||||||
|
import { AuthorizationGuard } from "../guards";
|
||||||
|
|
||||||
|
/*
|
||||||
|
*Is this user part of the organization (And optionally, has required role)
|
||||||
|
* */
|
||||||
|
|
||||||
|
export function Authorization(role?: ORG_ROLE[]) {
|
||||||
|
return applyDecorators(
|
||||||
|
SetMetadata(CAN_PERFORM_KEY, role),
|
||||||
|
UseGuards(AuthorizationGuard)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
//export const Authorization = (role?: ORG_ROLE[]) => SetMetadata(CAN_PERFORM_KEY, role)
|
||||||
3
src/auth/decorators/index.ts
Normal file
3
src/auth/decorators/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './public.decorator';
|
||||||
|
export * from './role.decorator';
|
||||||
|
export * from './authorization.decorator';
|
||||||
5
src/auth/decorators/public.decorator.ts
Normal file
5
src/auth/decorators/public.decorator.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { SetMetadata } from '@nestjs/common';
|
||||||
|
import { PUBLIC_KEY } from 'common/keys';
|
||||||
|
|
||||||
|
export const Public = (isPublic?: boolean) =>
|
||||||
|
SetMetadata(PUBLIC_KEY, isPublic ?? true);
|
||||||
6
src/auth/decorators/role.decorator.ts
Normal file
6
src/auth/decorators/role.decorator.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { SetMetadata } from '@nestjs/common';
|
||||||
|
import { ORG_ROLE_KEY, ROLE_KEY } from 'common/keys';
|
||||||
|
|
||||||
|
export const Roles = (role: string) => SetMetadata(ROLE_KEY, role);
|
||||||
|
|
||||||
|
export const OrgRole = (role: string) => SetMetadata(ORG_ROLE_KEY, role);
|
||||||
3
src/auth/dto/index.ts
Normal file
3
src/auth/dto/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './register-user.dto';
|
||||||
|
export * from './login-user.dto';
|
||||||
|
export * from './login-response.dto';
|
||||||
14
src/auth/dto/login-response.dto.ts
Normal file
14
src/auth/dto/login-response.dto.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { User } from 'prisma/generated/prisma/client';
|
||||||
|
import { UserDTO } from 'src/user/dtos';
|
||||||
|
|
||||||
|
export class LoginUserResponseDTO {
|
||||||
|
readonly accessToken: string;
|
||||||
|
readonly refreshToken: string;
|
||||||
|
readonly user: UserDTO;
|
||||||
|
|
||||||
|
constructor(user: User, accessToken: string, refreshToken: string) {
|
||||||
|
this.user = new UserDTO(user);
|
||||||
|
this.accessToken = accessToken;
|
||||||
|
this.refreshToken = refreshToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/auth/dto/login-user.dto.ts
Normal file
24
src/auth/dto/login-user.dto.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
|
||||||
|
|
||||||
|
export class LoginUserRequestDTO {
|
||||||
|
@ApiProperty({
|
||||||
|
description: "User's email",
|
||||||
|
example: 'user@example.com',
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
@IsEmail()
|
||||||
|
@IsNotEmpty()
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: "User's password",
|
||||||
|
example: '123456',
|
||||||
|
type: 'string',
|
||||||
|
minLength: 6,
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@MinLength(6)
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
57
src/auth/dto/register-user.dto.ts
Normal file
57
src/auth/dto/register-user.dto.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import {
|
||||||
|
IsEmail,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
MinLength,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
export class RegisterUserRequestDTO {
|
||||||
|
@ApiProperty({
|
||||||
|
description: "User's firstName",
|
||||||
|
example: 'John',
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
firstName: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "User's middleName",
|
||||||
|
example: 'Kumar',
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
middleName?: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: "User's lastName",
|
||||||
|
example: 'Doe',
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
lastName: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: "User's email",
|
||||||
|
example: 'user@example.com',
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
@IsEmail()
|
||||||
|
@IsNotEmpty()
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: "User's password",
|
||||||
|
example: '123456',
|
||||||
|
type: 'string',
|
||||||
|
minLength: 6,
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@MinLength(6)
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
56
src/auth/guards/auth.guard.ts
Normal file
56
src/auth/guards/auth.guard.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import {
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
Injectable,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { RequestContextService } from 'core/als/request-context.service';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import { JwtPayload } from '../types';
|
||||||
|
import { Request } from 'express';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { PUBLIC_KEY } from 'common/keys';
|
||||||
|
import { UserService } from 'src/user/user.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthGuard implements CanActivate {
|
||||||
|
constructor(
|
||||||
|
private readonly reflector: Reflector,
|
||||||
|
private readonly jwtService: JwtService,
|
||||||
|
private readonly requestContext: RequestContextService,
|
||||||
|
private readonly userService: UserService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext) {
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
const isPublicRoute = this.reflector.getAllAndOverride<boolean>(
|
||||||
|
PUBLIC_KEY,
|
||||||
|
[context.getHandler(), context.getClass()],
|
||||||
|
);
|
||||||
|
if (isPublicRoute) return true;
|
||||||
|
|
||||||
|
const token = this.extractTokenFromHeader(request);
|
||||||
|
if (!token) throw new UnauthorizedException();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload: JwtPayload = await this.jwtService.verifyAsync(token, {
|
||||||
|
secret: 'demo',
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Redis + Org too, blacklist token
|
||||||
|
const userExists = await this.userService.findById(payload.userId);
|
||||||
|
if (!userExists) throw new UnauthorizedException();
|
||||||
|
this.requestContext.set('user', payload);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractTokenFromHeader(request: Request): string | undefined {
|
||||||
|
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||||
|
return type === 'Bearer' ? token : undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
57
src/auth/guards/authorization.guard.ts
Normal file
57
src/auth/guards/authorization.guard.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
ForbiddenException,
|
||||||
|
Injectable,
|
||||||
|
UnauthorizedException
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { Reflector } from "@nestjs/core";
|
||||||
|
import { CAN_PERFORM_KEY } from "common/keys";
|
||||||
|
import { RequestContextService } from "core/als/request-context.service";
|
||||||
|
import { ORG_ROLE } from "prisma/generated/prisma/enums";
|
||||||
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthorizationGuard implements CanActivate {
|
||||||
|
constructor(
|
||||||
|
private readonly reqeustContext: RequestContextService,
|
||||||
|
private readonly reflector: Reflector,
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
) { };
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const requiredRole = this.reflector.getAllAndOverride<ORG_ROLE[] | undefined>(
|
||||||
|
CAN_PERFORM_KEY,
|
||||||
|
[context.getHandler(), context.getClass()]
|
||||||
|
)
|
||||||
|
|
||||||
|
const userId = this.reqeustContext.user.userId;
|
||||||
|
if (!userId)
|
||||||
|
throw new UnauthorizedException()
|
||||||
|
|
||||||
|
const request = context.switchToHttp().getRequest()
|
||||||
|
const orgId = request.params.orgId;
|
||||||
|
|
||||||
|
if (!orgId)
|
||||||
|
throw new BadRequestException()
|
||||||
|
|
||||||
|
const userIsPartOfOrg = await this.prisma.organizationUserJoinTable.findUnique({
|
||||||
|
where: {
|
||||||
|
userId_orgId: {
|
||||||
|
userId,
|
||||||
|
orgId
|
||||||
|
},
|
||||||
|
...(requiredRole ? { role: { in: requiredRole } } : {})
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
userId: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!userIsPartOfOrg)
|
||||||
|
throw new ForbiddenException()
|
||||||
|
|
||||||
|
this.reqeustContext.orgId = orgId;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/auth/guards/index.ts
Normal file
1
src/auth/guards/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./authorization.guard"
|
||||||
26
src/auth/guards/rbac.guard.ts
Normal file
26
src/auth/guards/rbac.guard.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import {
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { ROLE_KEY } from 'common/keys';
|
||||||
|
import { RequestContextService } from 'core/als/request-context.service';
|
||||||
|
|
||||||
|
export class RbacGuard implements CanActivate {
|
||||||
|
constructor(
|
||||||
|
private readonly reflector: Reflector,
|
||||||
|
private readonly requestContext: RequestContextService,
|
||||||
|
) { }
|
||||||
|
canActivate(context: ExecutionContext) {
|
||||||
|
const requiredRole = this.reflector.getAllAndOverride<string>(ROLE_KEY, [
|
||||||
|
context.getHandler(),
|
||||||
|
context.getClass(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const user = this.requestContext.user;
|
||||||
|
if (!user) throw new UnauthorizedException();
|
||||||
|
|
||||||
|
return user.role === requiredRole;
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/auth/types/index.ts
Normal file
3
src/auth/types/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './jwt';
|
||||||
|
export * from './role';
|
||||||
|
export * from './token';
|
||||||
12
src/auth/types/jwt.ts
Normal file
12
src/auth/types/jwt.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { ORG_ROLE, USER_ROLE } from 'prisma/generated/prisma/enums';
|
||||||
|
|
||||||
|
export interface JwtPayload {
|
||||||
|
iat?: number;
|
||||||
|
exp?: number;
|
||||||
|
orgId?: string;
|
||||||
|
orgRole?: ORG_ROLE;
|
||||||
|
userId: string;
|
||||||
|
email: string;
|
||||||
|
role: USER_ROLE;
|
||||||
|
permissions?: string[];
|
||||||
|
}
|
||||||
1
src/auth/types/role.ts
Normal file
1
src/auth/types/role.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export type UserRoleType = 'user' | 'admin';
|
||||||
11
src/auth/types/token.ts
Normal file
11
src/auth/types/token.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export interface TokenInputType {
|
||||||
|
userId: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccessTokenPayloadType extends TokenInputType {}
|
||||||
|
|
||||||
|
export interface RefreshTokenPayloadType {
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
10
src/authorization/authorization.module.ts
Normal file
10
src/authorization/authorization.module.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AuthorizationService } from './authorization.service';
|
||||||
|
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [AuthorizationService],
|
||||||
|
imports: [PrismaModule],
|
||||||
|
exports: [AuthorizationService],
|
||||||
|
})
|
||||||
|
export class AuthorizationModule {}
|
||||||
18
src/authorization/authorization.service.spec.ts
Normal file
18
src/authorization/authorization.service.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { AuthorizationService } from './authorization.service';
|
||||||
|
|
||||||
|
describe('AuthorizationService', () => {
|
||||||
|
let service: AuthorizationService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [AuthorizationService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<AuthorizationService>(AuthorizationService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
71
src/authorization/authorization.service.ts
Normal file
71
src/authorization/authorization.service.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
|
import { USER_ORGANIZATION_OPERATIONS } from './operations';
|
||||||
|
import { ORG_ROLE } from 'prisma/generated/prisma/enums';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthorizationService {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
// can perform operation
|
||||||
|
async canPerformOperation(
|
||||||
|
userId: string,
|
||||||
|
orgId: string,
|
||||||
|
operation: USER_ORGANIZATION_OPERATIONS,
|
||||||
|
) {
|
||||||
|
switch (operation) {
|
||||||
|
case USER_ORGANIZATION_OPERATIONS.DELETE_ORGANIZATION:
|
||||||
|
return await this.isOwner(userId, orgId);
|
||||||
|
case USER_ORGANIZATION_OPERATIONS.UPDATE_ORGANIZATION:
|
||||||
|
return await this.isOwner(userId, orgId);
|
||||||
|
case USER_ORGANIZATION_OPERATIONS.INVITE_USERS:
|
||||||
|
return await this.canInvite(userId, orgId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async isOwner(userId: string, orgId: string) {
|
||||||
|
const isUserPartOfOrganization = await this.isUserPartOfOrganization(
|
||||||
|
userId,
|
||||||
|
orgId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return isUserPartOfOrganization
|
||||||
|
? isUserPartOfOrganization.role === ORG_ROLE.owner
|
||||||
|
: !!isUserPartOfOrganization;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async canInvite(userId: string, orgId: string) {
|
||||||
|
const isUserPartOfOrganization = await this.isUserPartOfOrganization(
|
||||||
|
userId,
|
||||||
|
orgId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return isUserPartOfOrganization
|
||||||
|
? isUserPartOfOrganization.role === ORG_ROLE.admin ||
|
||||||
|
isUserPartOfOrganization.role === ORG_ROLE.owner
|
||||||
|
: !!isUserPartOfOrganization;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async isAdmin(userId: string, orgId: string) {
|
||||||
|
const isUserPartOfOrganization = await this.isUserPartOfOrganization(
|
||||||
|
userId,
|
||||||
|
orgId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return isUserPartOfOrganization
|
||||||
|
? isUserPartOfOrganization.role === ORG_ROLE.admin
|
||||||
|
: !!isUserPartOfOrganization;
|
||||||
|
}
|
||||||
|
|
||||||
|
// HELPER FUNCTION
|
||||||
|
private async isUserPartOfOrganization(userId: string, orgId: string) {
|
||||||
|
return await this.prisma.organizationUserJoinTable.findUnique({
|
||||||
|
where: {
|
||||||
|
userId_orgId: {
|
||||||
|
orgId,
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/authorization/operations.ts
Normal file
5
src/authorization/operations.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export enum USER_ORGANIZATION_OPERATIONS {
|
||||||
|
UPDATE_ORGANIZATION = 'update_organization',
|
||||||
|
DELETE_ORGANIZATION = 'delete_organization',
|
||||||
|
INVITE_USERS = 'invite_users',
|
||||||
|
}
|
||||||
32
src/cache/cache.module.ts
vendored
Normal file
32
src/cache/cache.module.ts
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { CacheModule as NestCacheManager } from '@nestjs/cache-manager';
|
||||||
|
import KeyvRedis from '@keyv/redis';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { CacheService } from './cache.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
NestCacheManager.registerAsync({
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory: async (configService: ConfigService) => {
|
||||||
|
const redisUrl = configService.get<string>('REDIS_URL');
|
||||||
|
const redisStore = new KeyvRedis(redisUrl, {
|
||||||
|
connectionTimeout: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
redisStore.on('error', (err) => {
|
||||||
|
console.error('Redis error:', err.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
stores: [redisStore],
|
||||||
|
ttl: 120 * 1000,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
isGlobal: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
providers: [CacheService],
|
||||||
|
exports: [CacheService],
|
||||||
|
})
|
||||||
|
export class CacheModule {}
|
||||||
18
src/cache/cache.service.spec.ts
vendored
Normal file
18
src/cache/cache.service.spec.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { CacheService } from './cache.service';
|
||||||
|
|
||||||
|
describe('CacheService', () => {
|
||||||
|
let service: CacheService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [CacheService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<CacheService>(CacheService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
65
src/cache/cache.service.ts
vendored
Normal file
65
src/cache/cache.service.ts
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CacheService {
|
||||||
|
private redisAvailable = true;
|
||||||
|
|
||||||
|
constructor(@Inject(CACHE_MANAGER) private cache: Cache) {
|
||||||
|
const store = this.cache.stores[0];
|
||||||
|
|
||||||
|
if (store?.on) {
|
||||||
|
store.on('end', () => {
|
||||||
|
this.redisAvailable = false;
|
||||||
|
console.warn('Redis disconnected');
|
||||||
|
});
|
||||||
|
|
||||||
|
store.on('ready', () => {
|
||||||
|
this.redisAvailable = true;
|
||||||
|
console.log('Redis ready');
|
||||||
|
});
|
||||||
|
|
||||||
|
store.on('error', () => {
|
||||||
|
this.redisAvailable = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrSet<T>(
|
||||||
|
key: string,
|
||||||
|
factory: () => Promise<T>,
|
||||||
|
ttl?: number,
|
||||||
|
): Promise<T> {
|
||||||
|
if (this.redisAvailable) {
|
||||||
|
try {
|
||||||
|
const cached = await this.cache.get<T>(key);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
this.redisAvailable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to DB
|
||||||
|
const fresh = await factory();
|
||||||
|
|
||||||
|
if (fresh) {
|
||||||
|
// Try setting cache only if Redis available
|
||||||
|
if (this.redisAvailable) {
|
||||||
|
try {
|
||||||
|
await this.cache.set(key, fresh, ttl);
|
||||||
|
} catch {
|
||||||
|
this.redisAvailable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fresh;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteKey(key: string) {
|
||||||
|
const a = await this.cache.del(key);
|
||||||
|
console.log(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src/main.ts
47
src/main.ts
@@ -1,8 +1,53 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||||
|
import { Logger } from '@nestjs/common';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
await app.listen(process.env.PORT ?? 3000);
|
|
||||||
|
const swaggerConfig = new DocumentBuilder()
|
||||||
|
.setTitle('MultiTenant Saas')
|
||||||
|
.setDescription(`API Documentation for a simple MultiTenant Saas Application`)
|
||||||
|
.setVersion('0.0.1')
|
||||||
|
.addGlobalResponse(
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
description: 'Internal Server Error',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
description: 'Unauthorized',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 403,
|
||||||
|
description: 'Forbidden',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.addBearerAuth(
|
||||||
|
{
|
||||||
|
type: 'http',
|
||||||
|
scheme: 'bearer',
|
||||||
|
bearerFormat: 'JWT',
|
||||||
|
},
|
||||||
|
'access-token',
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
const documentFactory = () =>
|
||||||
|
SwaggerModule.createDocument(app, swaggerConfig);
|
||||||
|
SwaggerModule.setup('/docs', app, documentFactory, {
|
||||||
|
swaggerOptions: {
|
||||||
|
persistAuthorization: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const config = app.get(ConfigService);
|
||||||
|
const port = config.get<number>('PORT') ?? 3000;
|
||||||
|
|
||||||
|
await app.listen(port);
|
||||||
|
|
||||||
|
Logger.log(`Listning on port ${port}`)
|
||||||
}
|
}
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|||||||
1
src/organization-membership/constants/index.ts
Normal file
1
src/organization-membership/constants/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./type"
|
||||||
4
src/organization-membership/constants/type.ts
Normal file
4
src/organization-membership/constants/type.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export enum USER_ORG_ACCEPT_REJECT_ACTION {
|
||||||
|
ACCEPT = 'ACCEPT',
|
||||||
|
REJECT = 'REJECT'
|
||||||
|
}
|
||||||
4
src/organization-membership/dto/index.ts
Normal file
4
src/organization-membership/dto/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './invite-to-org.dto';
|
||||||
|
export * from './join-request.dto'
|
||||||
|
export * from "./user-invitation-action.dto"
|
||||||
|
export * from "./org-request-action.dto"
|
||||||
34
src/organization-membership/dto/invite-to-org.dto.ts
Normal file
34
src/organization-membership/dto/invite-to-org.dto.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsEmail, IsEnum, IsNotEmpty, IsUUID } from 'class-validator';
|
||||||
|
import { ORG_ROLE } from 'prisma/generated/prisma/enums';
|
||||||
|
|
||||||
|
export class InviteUserToOrganizationRequestDTO {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Who to invite',
|
||||||
|
example: 'user1@example.com',
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
@IsEmail()
|
||||||
|
@IsNotEmpty()
|
||||||
|
invitedUserEmail: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Role to assign',
|
||||||
|
example: ORG_ROLE.member,
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
@IsEnum(ORG_ROLE)
|
||||||
|
@IsNotEmpty()
|
||||||
|
role: ORG_ROLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CancelInviteUserToOrganizationRequestDTO {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Who to cancel',
|
||||||
|
example: 'bb1c81da-ce8f-4231-aee8-2b976124a589',
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
@IsUUID()
|
||||||
|
@IsNotEmpty()
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
13
src/organization-membership/dto/join-request.dto.ts
Normal file
13
src/organization-membership/dto/join-request.dto.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { IsNotEmpty, IsOptional, IsString, IsUUID } from "class-validator";
|
||||||
|
|
||||||
|
export class JoinRequestToOrganizationRequestDTO {
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Message along with the request',
|
||||||
|
example: 'I would like to join',
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
requestMessage?: string;
|
||||||
|
}
|
||||||
23
src/organization-membership/dto/org-request-action.dto.ts
Normal file
23
src/organization-membership/dto/org-request-action.dto.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"
|
||||||
|
import { IsEnum, IsNotEmpty, IsOptional, IsString } from "class-validator"
|
||||||
|
import { USER_ORG_ACCEPT_REJECT_ACTION } from "../constants"
|
||||||
|
|
||||||
|
export class UserOrganizationRequestActionRequestDTO {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Action',
|
||||||
|
example: USER_ORG_ACCEPT_REJECT_ACTION.ACCEPT,
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
@IsEnum(USER_ORG_ACCEPT_REJECT_ACTION)
|
||||||
|
@IsNotEmpty()
|
||||||
|
action: USER_ORG_ACCEPT_REJECT_ACTION
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Message(reject reason)',
|
||||||
|
example: 'Bad sry or smth',
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { IsEnum, IsNotEmpty, IsOptional, IsString } from "class-validator";
|
||||||
|
import { USER_ORG_ACCEPT_REJECT_ACTION } from "../constants";
|
||||||
|
|
||||||
|
|
||||||
|
export class UserOrganizationInvitationActionRequestDTO {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Action',
|
||||||
|
example: USER_ORG_ACCEPT_REJECT_ACTION.ACCEPT,
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
@IsEnum(USER_ORG_ACCEPT_REJECT_ACTION)
|
||||||
|
@IsNotEmpty()
|
||||||
|
action: string
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Message(reject reason)',
|
||||||
|
example: 'Bad sry or smth',
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { OrganizationMembershipController } from './organization-membership.controller';
|
||||||
|
|
||||||
|
describe('OrganizationMembershipController', () => {
|
||||||
|
let controller: OrganizationMembershipController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [OrganizationMembershipController],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<OrganizationMembershipController>(OrganizationMembershipController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import { Body, Controller, Delete, Get, Param, ParseEnumPipe, ParseUUIDPipe, Patch, Post, Query } from '@nestjs/common';
|
||||||
|
import { OrganizationMembershipService } from './organization-membership.service';
|
||||||
|
import { RequestContextService } from 'core/als/request-context.service';
|
||||||
|
import { ApiBearerAuth, ApiOperation, ApiParam, ApiQuery } from '@nestjs/swagger';
|
||||||
|
import { CancelInviteUserToOrganizationRequestDTO, InviteUserToOrganizationRequestDTO, JoinRequestToOrganizationRequestDTO, UserOrganizationInvitationActionRequestDTO, UserOrganizationRequestActionRequestDTO } from './dto';
|
||||||
|
import { ORG_ROLE, ORGANIZATION_JOIN_REQUEST_TYPE } from 'prisma/generated/prisma/enums';
|
||||||
|
import { Authorization } from 'src/auth/decorators';
|
||||||
|
|
||||||
|
/* NOTE: Regarding endpoint path naming
|
||||||
|
* - Since we follow REST style, endpoint are resource based.
|
||||||
|
* - So insted of /organization/:orgId/invitation-action, we user ..../invitation/:invitationId
|
||||||
|
* (invitationId points to a resource)
|
||||||
|
* */
|
||||||
|
|
||||||
|
@Controller('membership')
|
||||||
|
@ApiBearerAuth('access-token')
|
||||||
|
export class OrganizationMembershipController {
|
||||||
|
constructor(
|
||||||
|
private readonly orgMemService: OrganizationMembershipService,
|
||||||
|
private readonly requestContext: RequestContextService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
|
||||||
|
/* *
|
||||||
|
* USER OPERATIONS
|
||||||
|
* */
|
||||||
|
@ApiOperation({ summary: 'Send request to join an organization' })
|
||||||
|
@ApiParam({
|
||||||
|
name: 'orgId',
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
@Post('organization/:orgId/join-request')
|
||||||
|
async requestToJoinOrg(
|
||||||
|
@Param('orgId', new ParseUUIDPipe()) orgId: string,
|
||||||
|
@Body() body: JoinRequestToOrganizationRequestDTO
|
||||||
|
) {
|
||||||
|
const user = this.requestContext.user;
|
||||||
|
return await this.orgMemService.usersRequestToJoin(user.userId, orgId, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: 'Cancel a sent invitation to join an organization' })
|
||||||
|
@ApiParam({
|
||||||
|
name: 'orgId',
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
@Delete('organization/:orgId/join-request/:id')
|
||||||
|
async cancelRequestToJoinOrg(
|
||||||
|
@Param('orgId', new ParseUUIDPipe()) orgId: string,
|
||||||
|
@Param('id', new ParseUUIDPipe()) joinReqId: string
|
||||||
|
) {
|
||||||
|
const user = this.requestContext.user;
|
||||||
|
return await this.orgMemService.userCancelOrgJoinRequest(
|
||||||
|
user.userId,
|
||||||
|
orgId,
|
||||||
|
joinReqId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: 'Accept or reject an invitation from an organization' })
|
||||||
|
@ApiParam({
|
||||||
|
name: 'orgId',
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
@Patch('organization/:orgId/invitation/:id')
|
||||||
|
async acceptOrRejectInvitation(
|
||||||
|
@Param('orgId', new ParseUUIDPipe()) orgId: string,
|
||||||
|
@Param('id', new ParseUUIDPipe()) invitationId: string,
|
||||||
|
@Body() body: UserOrganizationInvitationActionRequestDTO
|
||||||
|
) {
|
||||||
|
const user = this.requestContext.user;
|
||||||
|
return await this.orgMemService.userOrganiaztionRequestAction(
|
||||||
|
user.userId,
|
||||||
|
orgId,
|
||||||
|
invitationId,
|
||||||
|
body
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: 'List invitations recieved or join requests sent' })
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'requestType',
|
||||||
|
enum: ORGANIZATION_JOIN_REQUEST_TYPE,
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
@Get('me/invitations')
|
||||||
|
async getUserInvitations(
|
||||||
|
@Query(
|
||||||
|
'requestType',
|
||||||
|
new ParseEnumPipe(
|
||||||
|
ORGANIZATION_JOIN_REQUEST_TYPE, { optional: true }
|
||||||
|
)
|
||||||
|
) requestType?: ORGANIZATION_JOIN_REQUEST_TYPE
|
||||||
|
) {
|
||||||
|
const user = this.requestContext.user;
|
||||||
|
return await this.orgMemService.getUserInvitations(user.userId, requestType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: 'List organizations user is part of' })
|
||||||
|
@Get('me/organizations')
|
||||||
|
async getUserOrganizations() {
|
||||||
|
const user = this.requestContext.user;
|
||||||
|
return await this.orgMemService.getOrganizationsOfUser(user.userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: 'Leave an organization' })
|
||||||
|
@ApiParam({
|
||||||
|
name: 'orgId',
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
@Delete('organization/:orgId/member/me')
|
||||||
|
async leaveOrganization(
|
||||||
|
@Param('orgId', new ParseUUIDPipe()) orgId: string,
|
||||||
|
) {
|
||||||
|
const user = this.requestContext.user;
|
||||||
|
return await this.orgMemService.userLeaveAnOrganization(user.userId, orgId)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* *
|
||||||
|
* ORGANIZATION OPERATIONS
|
||||||
|
* */
|
||||||
|
|
||||||
|
@ApiOperation({ summary: 'Invite user to organization' })
|
||||||
|
@ApiParam({
|
||||||
|
name: 'orgId',
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
@Authorization([ORG_ROLE.admin, ORG_ROLE.owner])
|
||||||
|
@Post('organization/:orgId/invitation')
|
||||||
|
async inviteUserToOrg(
|
||||||
|
@Param('orgId', new ParseUUIDPipe()) orgId: string,
|
||||||
|
@Body() body: InviteUserToOrganizationRequestDTO
|
||||||
|
) {
|
||||||
|
return await this.orgMemService.inviteUserToOrg(
|
||||||
|
orgId,
|
||||||
|
body
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: 'Cancel a sent invitation to user' })
|
||||||
|
@ApiParam({
|
||||||
|
name: 'userId',
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
@Authorization([ORG_ROLE.admin, ORG_ROLE.owner])
|
||||||
|
@Delete('organization/:orgId/invitation/:id')
|
||||||
|
async cancelInvitationsToUser(
|
||||||
|
@Param('orgId', new ParseUUIDPipe()) orgId: string,
|
||||||
|
@Param('id', new ParseUUIDPipe()) invitationId: string,
|
||||||
|
@Body() body: CancelInviteUserToOrganizationRequestDTO
|
||||||
|
) {
|
||||||
|
const userId = body.userId;
|
||||||
|
return await this.orgMemService.orgCancelUserInviteRequest(userId, orgId, invitationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('organization/:orgId/members')
|
||||||
|
@Authorization()
|
||||||
|
async getOrganizationMemebers(@Param('orgId') orgId: string) {
|
||||||
|
const user = this.requestContext.user;
|
||||||
|
return await this.orgMemService.getMemebersOfOrganization(user.userId, orgId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('organization/:orgId/invitations')
|
||||||
|
@Authorization([ORG_ROLE.admin, ORG_ROLE.owner])
|
||||||
|
async getOrganizationInvitations(@Param('orgId') orgId: string) {
|
||||||
|
const user = this.requestContext.user;
|
||||||
|
return await this.orgMemService.getOrganizationRequestList(user.userId, orgId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('organization/:orgId/request/:id')
|
||||||
|
@Authorization([ORG_ROLE.admin, ORG_ROLE.owner])
|
||||||
|
async acceptOrRejectRequest(
|
||||||
|
@Param('orgId', new ParseUUIDPipe()) orgId: string,
|
||||||
|
@Param('id', new ParseUUIDPipe()) invitationId: string,
|
||||||
|
@Body() body: UserOrganizationRequestActionRequestDTO
|
||||||
|
) {
|
||||||
|
const user = this.requestContext.user;
|
||||||
|
return await this.orgMemService.organizationUserJoinRequestAction(
|
||||||
|
user.userId,
|
||||||
|
orgId,
|
||||||
|
invitationId,
|
||||||
|
body
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { OrganizationMembershipController } from './organization-membership.controller';
|
||||||
|
import { OrganizationMembershipService } from './organization-membership.service';
|
||||||
|
import { OrganizationModule } from 'src/organization/organization.module';
|
||||||
|
import { UserModule } from 'src/user/user.module';
|
||||||
|
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||||
|
import { AuthorizationModule } from 'src/authorization/authorization.module';
|
||||||
|
import { RequestContextModule } from 'core/als/request-context.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [OrganizationMembershipController],
|
||||||
|
providers: [OrganizationMembershipService],
|
||||||
|
imports: [
|
||||||
|
OrganizationModule,
|
||||||
|
UserModule,
|
||||||
|
PrismaModule,
|
||||||
|
AuthorizationModule,
|
||||||
|
RequestContextModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class OrganizationMembershipModule {}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { OrganizationMembershipService } from './organization-membership.service';
|
||||||
|
|
||||||
|
describe('OrganizationMembershipService', () => {
|
||||||
|
let service: OrganizationMembershipService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [OrganizationMembershipService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<OrganizationMembershipService>(OrganizationMembershipService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
459
src/organization-membership/organization-membership.service.ts
Normal file
459
src/organization-membership/organization-membership.service.ts
Normal file
@@ -0,0 +1,459 @@
|
|||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
ForbiddenException,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { OrganizationService } from 'src/organization/organization.service';
|
||||||
|
import { UserService } from 'src/user/user.service';
|
||||||
|
import { InviteUserToOrganizationRequestDTO, UserOrganizationInvitationActionRequestDTO, UserOrganizationRequestActionRequestDTO } from './dto';
|
||||||
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
|
import {
|
||||||
|
ORGANIZATION_JOIN_REQUEST,
|
||||||
|
ORGANIZATION_JOIN_REQUEST_TYPE,
|
||||||
|
} from 'prisma/generated/prisma/enums';
|
||||||
|
import { AuthorizationService } from 'src/authorization/authorization.service';
|
||||||
|
import { USER_ORGANIZATION_OPERATIONS } from 'src/authorization/operations';
|
||||||
|
import { Prisma } from 'prisma/generated/prisma/client';
|
||||||
|
import { JoinRequestToOrganizationRequestDTO } from './dto/join-request.dto';
|
||||||
|
import { USER_ORG_ACCEPT_REJECT_ACTION } from './constants';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OrganizationMembershipService {
|
||||||
|
constructor(
|
||||||
|
private readonly userService: UserService,
|
||||||
|
private readonly orgService: OrganizationService,
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly authorization: AuthorizationService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
/* *
|
||||||
|
* USER OPERATIONS
|
||||||
|
* */
|
||||||
|
async usersRequestToJoin(
|
||||||
|
userId: string,
|
||||||
|
orgId: string,
|
||||||
|
dto: JoinRequestToOrganizationRequestDTO
|
||||||
|
) {
|
||||||
|
const [
|
||||||
|
orgExists,
|
||||||
|
invitationAlreadySent,
|
||||||
|
userAlreadyPartOf
|
||||||
|
] = await Promise.all([
|
||||||
|
this.orgService.findById(orgId),
|
||||||
|
this.prisma.organizationJoinRequest.findFirst({
|
||||||
|
where: {
|
||||||
|
orgId,
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
requestedOn: "desc"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
this.prisma.organizationUserJoinTable.findFirst({
|
||||||
|
where: {
|
||||||
|
orgId,
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
select: { userId: true }
|
||||||
|
})
|
||||||
|
])
|
||||||
|
|
||||||
|
if (!orgExists)
|
||||||
|
throw new NotFoundException("Organization")
|
||||||
|
if (invitationAlreadySent?.status === "PENDING")
|
||||||
|
throw new BadRequestException("Invitation to join this organization already sent")
|
||||||
|
if (userAlreadyPartOf)
|
||||||
|
throw new BadRequestException("User already part of the organization")
|
||||||
|
|
||||||
|
return await this.prisma.organizationJoinRequest.create({
|
||||||
|
data: {
|
||||||
|
orgId: orgId,
|
||||||
|
userId,
|
||||||
|
requestType: ORGANIZATION_JOIN_REQUEST_TYPE.REQUESTED,
|
||||||
|
requestMessage: dto.requestMessage,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async userCancelOrgJoinRequest(
|
||||||
|
userId: string,
|
||||||
|
orgId: string,
|
||||||
|
id: string,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await this.prisma.organizationJoinRequest.update({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
userId,
|
||||||
|
orgId,
|
||||||
|
status: ORGANIZATION_JOIN_REQUEST.PENDING,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: ORGANIZATION_JOIN_REQUEST.CANCELLED,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
throw new NotFoundException("Request")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* List of organizations that:
|
||||||
|
* - user have requested to join
|
||||||
|
* - have send invitations to user
|
||||||
|
*
|
||||||
|
* filtered by requestType
|
||||||
|
* */
|
||||||
|
async userOrganizationJoinRequestList(
|
||||||
|
userId: string,
|
||||||
|
requestType: string
|
||||||
|
) {
|
||||||
|
const joinReqType: ORGANIZATION_JOIN_REQUEST_TYPE | undefined = ORGANIZATION_JOIN_REQUEST_TYPE[requestType];
|
||||||
|
if (!joinReqType)
|
||||||
|
throw new BadRequestException("Invalid request type")
|
||||||
|
|
||||||
|
return await this.prisma.organizationJoinRequest.findMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
requestType: joinReqType,
|
||||||
|
status: ORGANIZATION_JOIN_REQUEST.PENDING
|
||||||
|
},
|
||||||
|
include: { user: { select: { firstName: true, email: true } } }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async userOrganiaztionRequestAction(
|
||||||
|
userId: string,
|
||||||
|
orgId: string,
|
||||||
|
id: string,
|
||||||
|
dto: UserOrganizationInvitationActionRequestDTO
|
||||||
|
) {
|
||||||
|
const hasUserSendRequest = await this.prisma.organizationJoinRequest.findUnique({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
userId,
|
||||||
|
orgId,
|
||||||
|
status: ORGANIZATION_JOIN_REQUEST.PENDING,
|
||||||
|
requestType: ORGANIZATION_JOIN_REQUEST_TYPE.REQUESTED
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!hasUserSendRequest)
|
||||||
|
throw new BadRequestException("No pending join request")
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await this.prisma.$transaction(async (tx) => {
|
||||||
|
const userAction = dto.action === USER_ORG_ACCEPT_REJECT_ACTION.ACCEPT
|
||||||
|
? ORGANIZATION_JOIN_REQUEST.ACCEPTED
|
||||||
|
: ORGANIZATION_JOIN_REQUEST.REJECTED;
|
||||||
|
|
||||||
|
await tx.organizationJoinRequest.update({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
userId,
|
||||||
|
orgId,
|
||||||
|
status: ORGANIZATION_JOIN_REQUEST.PENDING,
|
||||||
|
requestType: ORGANIZATION_JOIN_REQUEST_TYPE.REQUESTED
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: userAction,
|
||||||
|
...(
|
||||||
|
userAction === ORGANIZATION_JOIN_REQUEST.REJECTED && dto.message
|
||||||
|
? { rejectReason: dto.message }
|
||||||
|
: {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userAction === ORGANIZATION_JOIN_REQUEST.ACCEPTED)
|
||||||
|
await tx.organizationUserJoinTable.create({
|
||||||
|
data: {
|
||||||
|
userId: userId,
|
||||||
|
orgId: orgId,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
if (err instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
|
if (err.code === 'P2002')
|
||||||
|
throw new BadRequestException('User already part of this organization.');
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: reject, rejectReason
|
||||||
|
//
|
||||||
|
// async acceptInvitation(userId: string, orgId: string) {
|
||||||
|
// const orgExists = await this.orgService.organizationExists(orgId);
|
||||||
|
// if (!orgExists) throw new NotFoundException('Organization');
|
||||||
|
//
|
||||||
|
// const [userAlreadyPart, isUserInvited] = await Promise.all([
|
||||||
|
// this.prisma.organizationUserJoinTable.findUnique({
|
||||||
|
// where: {
|
||||||
|
// userId_orgId: {
|
||||||
|
// orgId,
|
||||||
|
// userId,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// }),
|
||||||
|
//
|
||||||
|
// this.prisma.organizationJoinRequest.findUnique({
|
||||||
|
// where: {
|
||||||
|
// userId_orgId: {
|
||||||
|
// orgId,
|
||||||
|
// userId,
|
||||||
|
// },
|
||||||
|
// status: ORGANIZATION_JOIN_REQUEST.PENDING,
|
||||||
|
// },
|
||||||
|
// }),
|
||||||
|
// ]);
|
||||||
|
//
|
||||||
|
// if (userAlreadyPart)
|
||||||
|
// throw new BadRequestException('User already part of this organization');
|
||||||
|
// if (!isUserInvited)
|
||||||
|
// throw new BadRequestException(
|
||||||
|
// 'User has no invitations from this organization',
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// return await this.prisma.$transaction(async (tx) => {
|
||||||
|
// await tx.organizationJoinRequest.update({
|
||||||
|
// where: {
|
||||||
|
// userId_orgId: {
|
||||||
|
// userId,
|
||||||
|
// orgId,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// data: {
|
||||||
|
// status: ORGANIZATION_JOIN_REQUEST.ACCEPTED,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// return await tx.organizationUserJoinTable.create({
|
||||||
|
// data: {
|
||||||
|
// orgId,
|
||||||
|
// userId,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
async getUserInvitations(
|
||||||
|
userId: string,
|
||||||
|
requestType: ORGANIZATION_JOIN_REQUEST_TYPE = ORGANIZATION_JOIN_REQUEST_TYPE.INVITED,
|
||||||
|
status: ORGANIZATION_JOIN_REQUEST = ORGANIZATION_JOIN_REQUEST.PENDING,
|
||||||
|
) {
|
||||||
|
return await this.prisma.organizationJoinRequest.findMany({
|
||||||
|
where: {
|
||||||
|
userId: userId,
|
||||||
|
status: status,
|
||||||
|
requestType: requestType,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
organization: {
|
||||||
|
select: { name: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrganizationsOfUser(userId: string) {
|
||||||
|
return await this.prisma.organizationUserJoinTable.findMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
organization: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
description: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async userLeaveAnOrganization(userId: string, orgId: string) {
|
||||||
|
try {
|
||||||
|
await this.prisma.organizationUserJoinTable.delete({
|
||||||
|
where: {
|
||||||
|
userId_orgId: { userId, orgId }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
throw new NotFoundException("Membership not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async inviteUserToOrg(
|
||||||
|
orgId: string,
|
||||||
|
dto: InviteUserToOrganizationRequestDTO,
|
||||||
|
) {
|
||||||
|
const { invitedUserEmail, ...rest } = dto;
|
||||||
|
|
||||||
|
const invitedUser = await this.userService.findByEmail(invitedUserEmail);
|
||||||
|
if (!invitedUser) throw new NotFoundException('User');
|
||||||
|
|
||||||
|
const userAlreadyPart =
|
||||||
|
await this.prisma.organizationUserJoinTable.findUnique({
|
||||||
|
where: {
|
||||||
|
userId_orgId: {
|
||||||
|
orgId: orgId,
|
||||||
|
userId: invitedUser.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userAlreadyPart)
|
||||||
|
throw new BadRequestException('User already part of this organization');
|
||||||
|
|
||||||
|
// TODO: Test in Authorization and remove
|
||||||
|
// const canInviteUser = await this.authorization.canPerformOperation(
|
||||||
|
// userId,
|
||||||
|
// orgId,
|
||||||
|
// USER_ORGANIZATION_OPERATIONS.INVITE_USERS,
|
||||||
|
// );
|
||||||
|
// if (!canInviteUser) throw new ForbiddenException('Insufficient Permission');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const invitation = await this.prisma.organizationJoinRequest.create({
|
||||||
|
data: {
|
||||||
|
...rest,
|
||||||
|
userId: invitedUser.id,
|
||||||
|
orgId,
|
||||||
|
requestType: ORGANIZATION_JOIN_REQUEST_TYPE.INVITED,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return invitation;
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
|
if (err.code === 'P2002')
|
||||||
|
throw new BadRequestException('User invitation already sent.');
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async orgCancelUserInviteRequest(
|
||||||
|
userId: string,
|
||||||
|
orgId: string,
|
||||||
|
id: string,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
return await this.prisma.organizationJoinRequest.update({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
userId,
|
||||||
|
orgId,
|
||||||
|
requestType: ORGANIZATION_JOIN_REQUEST_TYPE.INVITED,
|
||||||
|
status: ORGANIZATION_JOIN_REQUEST.PENDING
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: ORGANIZATION_JOIN_REQUEST.CANCELLED
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
// TODO: Check error type and use it
|
||||||
|
throw new BadRequestException("Invitation not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async organizationUserJoinRequestAction(
|
||||||
|
userId: string,
|
||||||
|
orgId: string,
|
||||||
|
id: string,
|
||||||
|
dto: UserOrganizationRequestActionRequestDTO
|
||||||
|
) {
|
||||||
|
// NOTE: Experiment, don't know if its better
|
||||||
|
try {
|
||||||
|
return await this.prisma.$transaction(async (tx) => {
|
||||||
|
const updatedJoinReq = await tx.organizationJoinRequest.update({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
orgId,
|
||||||
|
status: ORGANIZATION_JOIN_REQUEST.PENDING,
|
||||||
|
requestType: ORGANIZATION_JOIN_REQUEST_TYPE.REQUESTED
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: dto.action === USER_ORG_ACCEPT_REJECT_ACTION.ACCEPT
|
||||||
|
? ORGANIZATION_JOIN_REQUEST.ACCEPTED
|
||||||
|
: ORGANIZATION_JOIN_REQUEST.REJECTED,
|
||||||
|
|
||||||
|
...((dto.action === USER_ORG_ACCEPT_REJECT_ACTION.REJECT && dto.message) ? {
|
||||||
|
rejectReason: dto.message
|
||||||
|
} : {})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(updatedJoinReq)
|
||||||
|
|
||||||
|
return await tx.organizationUserJoinTable.create({
|
||||||
|
data: {
|
||||||
|
userId: updatedJoinReq.userId,
|
||||||
|
orgId,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
throw new BadRequestException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrganizationRequestList(
|
||||||
|
userId: string,
|
||||||
|
orgId: string,
|
||||||
|
requestType: ORGANIZATION_JOIN_REQUEST_TYPE = ORGANIZATION_JOIN_REQUEST_TYPE.REQUESTED,
|
||||||
|
) {
|
||||||
|
// TODO: Check can perform
|
||||||
|
return await this.prisma.organizationJoinRequest.findMany({
|
||||||
|
where: {
|
||||||
|
orgId,
|
||||||
|
requestType: requestType
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async getMemebersOfOrganization(userId: string, orgId: string) {
|
||||||
|
const members = await this.prisma.organization.findFirst({
|
||||||
|
where: {
|
||||||
|
id: orgId,
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
userId: userId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
members: {
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
firstName: true,
|
||||||
|
email: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!members)
|
||||||
|
throw new NotFoundException("Organization")
|
||||||
|
|
||||||
|
return members;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/organization/dtos/index.ts
Normal file
2
src/organization/dtos/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './organization.dto';
|
||||||
|
export * from './organization-response.dto';
|
||||||
17
src/organization/dtos/organization-response.dto.ts
Normal file
17
src/organization/dtos/organization-response.dto.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Organization } from 'prisma/generated/prisma/client';
|
||||||
|
|
||||||
|
export class OrganizationDTO {
|
||||||
|
readonly id: string;
|
||||||
|
readonly name: string;
|
||||||
|
readonly description: string | null;
|
||||||
|
readonly createdAt: Date;
|
||||||
|
|
||||||
|
constructor(organization: Organization) {
|
||||||
|
this.id = organization.id;
|
||||||
|
this.name = organization.name;
|
||||||
|
this.description = organization.description;
|
||||||
|
this.createdAt = organization.createdAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreateNewOrganizationResponseDTO extends OrganizationDTO {}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user