Compare commits

..

10 Commits

Author SHA1 Message Date
SauravDhakal
68135ae022 fix: Org slight 2026-04-02 21:51:46 +05:45
SauravDhakal
349196b801 feat: Added methods for organization 2026-03-11 21:47:35 +05:45
SauravDhakal
6fc494687a feat: Simple nodemailer integration 2026-03-07 07:38:50 +05:45
SauravDhakal
496d689ec1 feat: User operations on join org 2026-03-04 22:26:20 +05:45
sauravdhakal12
024702dd26 wip: added cache 2026-02-27 21:26:36 +05:45
sauravdhakal12
90b0192cd2 feat: Organization operations like invite and accept 2026-02-22 17:27:37 +05:45
sauravdhakal12
afed1731d2 feat: Organization services 2026-02-22 15:47:45 +05:45
sauravdhakal12
f4c9174752 feat: Basic setup with auth 2026-02-21 17:21:48 +05:45
sauravdhakal12
f6bce78aee feat: User services 2026-02-20 15:53:45 +05:45
sauravdhakal12
9561693cb4 feat: als and prisma 2026-02-10 17:19:53 +05:45
113 changed files with 12972 additions and 53 deletions

2
.gitignore vendored
View File

@@ -54,3 +54,5 @@ pids
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
/generated/prisma

389
CODE_REVIEW.md Normal file
View 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
View 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">
&copy; 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
View File

@@ -0,0 +1,9 @@
import { welcomeToApp } from "./auth"
const EmailTemplates = {
welcomeToApp,
}
export default EmailTemplates

View 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();
// }
// }

View 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
}
}

View 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,
});
}
}

View File

@@ -0,0 +1 @@
export * from './custom-exceptions';

1
common/http/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from './response';

25
common/http/response.ts Normal file
View 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;
}

View File

View 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
View 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__';

View 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';
},
},
});
};
}

View File

@@ -0,0 +1 @@
export * from './at-least-one-field';

View File

@@ -0,0 +1,6 @@
import { IsUUID } from 'class-validator';
export class UUIDQueryDTO {
@IsUUID()
id: string;
}

View 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);
}
}

View File

@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { RequestContextService } from './request-context.service';
@Module({
providers: [RequestContextService],
exports: [RequestContextService],
})
export class RequestContextModule {}

View 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)
}
}

View 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
View 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
View 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
}
)
}
}

View 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:

View File

@@ -17,12 +17,28 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"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": {
"@keyv/redis": "^5.1.6",
"@nestjs/cache-manager": "^3.1.0",
"@nestjs/common": "^11.0.1",
"@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^11.0.2",
"@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",
"rxjs": "^7.8.1"
},
@@ -30,13 +46,17 @@
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/config": "^4.0.3",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@swc/cli": "^0.6.0",
"@swc/core": "^1.10.7",
"@types/bcrypt": "^6.0.0",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/node": "^22.10.7",
"@types/nodemailer": "^7.0.11",
"@types/pg": "^8.16.0",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
@@ -44,6 +64,7 @@
"globals": "^16.0.0",
"jest": "^29.7.0",
"prettier": "^3.4.2",
"prisma": "^7.3.0",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",

1269
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

14
prisma.config.ts Normal file
View 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'],
},
});

View 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

View 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

View 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>
}

View 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]

View 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
}

File diff suppressed because it is too large Load Diff

View 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]

View 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'

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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");

View File

@@ -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';

View File

@@ -0,0 +1 @@
-- This is an empty migration.

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "organization_join_request" ADD COLUMN "role" "ORG_ROLE" NOT NULL DEFAULT 'member';

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "organization_join_request" ADD COLUMN "requestMessage" TEXT;

View File

@@ -0,0 +1 @@
-- This is an empty migration.

View File

@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "ORGANIZATION_JOIN_REQUEST" ADD VALUE 'CANCELLED';

View File

@@ -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");

View 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"

View 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
}

View 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
}

View 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")
}

View 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
View 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
View 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 Architects 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.

View File

@@ -1,7 +1,9 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { Public } from './auth/decorators';
@Controller()
@Controller('')
@Public(true)
export class AppController {
constructor(private readonly appService: AppService) {}

View File

@@ -1,10 +1,57 @@
import { Module } from '@nestjs/common';
import { Logger, MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { AppController } from './app.controller';
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({
imports: [],
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
UserModule,
AuthModule,
RequestContextModule,
PrismaModule,
OrganizationModule,
OrganizationMembershipModule,
AuthorizationModule,
CacheModule,
MailModule
],
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');
}
}

View 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();
});
});

View 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
View 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 { }

View 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
View 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 };
}
}

View 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)

View File

@@ -0,0 +1,3 @@
export * from './public.decorator';
export * from './role.decorator';
export * from './authorization.decorator';

View 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);

View 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
View File

@@ -0,0 +1,3 @@
export * from './register-user.dto';
export * from './login-user.dto';
export * from './login-response.dto';

View 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;
}
}

View 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;
}

View 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;
}

View 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;
}
}

View 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
View File

@@ -0,0 +1 @@
export * from "./authorization.guard"

View 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
View File

@@ -0,0 +1,3 @@
export * from './jwt';
export * from './role';
export * from './token';

12
src/auth/types/jwt.ts Normal file
View 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
View File

@@ -0,0 +1 @@
export type UserRoleType = 'user' | 'admin';

11
src/auth/types/token.ts Normal file
View File

@@ -0,0 +1,11 @@
export interface TokenInputType {
userId: string;
email: string;
role: string;
}
export interface AccessTokenPayloadType extends TokenInputType {}
export interface RefreshTokenPayloadType {
userId: string;
}

View 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 {}

View 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();
});
});

View 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,
},
},
});
}
}

View 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
View 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
View 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
View 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);
}
}

View File

@@ -1,8 +1,53 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ConfigService } from '@nestjs/config';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { Logger } from '@nestjs/common';
async function bootstrap() {
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();

View File

@@ -0,0 +1 @@
export * from "./type"

View File

@@ -0,0 +1,4 @@
export enum USER_ORG_ACCEPT_REJECT_ACTION {
ACCEPT = 'ACCEPT',
REJECT = 'REJECT'
}

View 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"

View 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;
}

View 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;
}

View 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
}

View File

@@ -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
}

View File

@@ -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();
});
});

View File

@@ -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
)
}
}

View File

@@ -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 {}

View File

@@ -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();
});
});

View 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;
}
}

View File

@@ -0,0 +1,2 @@
export * from './organization.dto';
export * from './organization-response.dto';

View 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