feat: User operations on join org

This commit is contained in:
SauravDhakal
2026-03-04 22:26:20 +05:45
parent 024702dd26
commit 496d689ec1
22 changed files with 911 additions and 127 deletions

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

View File

@@ -6,7 +6,7 @@ import {
} from '@nestjs/common';
import { DataResponse, MessageResponse } from 'common/http';
import { Observable } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { map } from 'rxjs/operators';
@Injectable()
export class ResponseInterceptor<T> implements NestInterceptor<T, any> {

View File

@@ -12,7 +12,8 @@
export const ORGANIZATION_JOIN_REQUEST = {
PENDING: 'PENDING',
ACCEPTED: 'ACCEPTED',
REJECTED: 'REJECTED'
REJECTED: 'REJECTED',
CANCELLED: 'CANCELLED'
} as const
export type ORGANIZATION_JOIN_REQUEST = (typeof ORGANIZATION_JOIN_REQUEST)[keyof typeof ORGANIZATION_JOIN_REQUEST]

View File

@@ -20,7 +20,7 @@ const config: runtime.GetPrismaClientConfig = {
"clientVersion": "7.3.0",
"engineVersion": "9d6ad21cbbceab97458517b147a6a09ff43aa735",
"activeProvider": "postgresql",
"inlineSchema": "model OrganizationJoinRequest {\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\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 @@map(\"organization_join_request\")\n}\n\nenum ORGANIZATION_JOIN_REQUEST {\n PENDING\n ACCEPTED\n REJECTED\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",
"inlineSchema": "model OrganizationJoinRequest {\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 @@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": {},
@@ -28,7 +28,7 @@ const config: runtime.GetPrismaClientConfig = {
}
}
config.runtimeDataModel = JSON.parse("{\"models\":{\"OrganizationJoinRequest\":{\"fields\":[{\"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\":\"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\":{}}")
config.runtimeDataModel = JSON.parse("{\"models\":{\"OrganizationJoinRequest\":{\"fields\":[{\"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')

View File

@@ -750,7 +750,8 @@ export const OrganizationJoinRequestScalarFieldEnum = {
requestedOn: 'requestedOn',
role: 'role',
updatedAt: 'updatedAt',
rejectReason: 'rejectReason'
rejectReason: 'rejectReason',
requestMessage: 'requestMessage'
} as const
export type OrganizationJoinRequestScalarFieldEnum = (typeof OrganizationJoinRequestScalarFieldEnum)[keyof typeof OrganizationJoinRequestScalarFieldEnum]

View File

@@ -81,7 +81,8 @@ export const OrganizationJoinRequestScalarFieldEnum = {
requestedOn: 'requestedOn',
role: 'role',
updatedAt: 'updatedAt',
rejectReason: 'rejectReason'
rejectReason: 'rejectReason',
requestMessage: 'requestMessage'
} as const
export type OrganizationJoinRequestScalarFieldEnum = (typeof OrganizationJoinRequestScalarFieldEnum)[keyof typeof OrganizationJoinRequestScalarFieldEnum]

View File

@@ -33,6 +33,7 @@ export type OrganizationJoinRequestMinAggregateOutputType = {
role: $Enums.ORG_ROLE | null
updatedAt: Date | null
rejectReason: string | null
requestMessage: string | null
}
export type OrganizationJoinRequestMaxAggregateOutputType = {
@@ -44,6 +45,7 @@ export type OrganizationJoinRequestMaxAggregateOutputType = {
role: $Enums.ORG_ROLE | null
updatedAt: Date | null
rejectReason: string | null
requestMessage: string | null
}
export type OrganizationJoinRequestCountAggregateOutputType = {
@@ -55,6 +57,7 @@ export type OrganizationJoinRequestCountAggregateOutputType = {
role: number
updatedAt: number
rejectReason: number
requestMessage: number
_all: number
}
@@ -68,6 +71,7 @@ export type OrganizationJoinRequestMinAggregateInputType = {
role?: true
updatedAt?: true
rejectReason?: true
requestMessage?: true
}
export type OrganizationJoinRequestMaxAggregateInputType = {
@@ -79,6 +83,7 @@ export type OrganizationJoinRequestMaxAggregateInputType = {
role?: true
updatedAt?: true
rejectReason?: true
requestMessage?: true
}
export type OrganizationJoinRequestCountAggregateInputType = {
@@ -90,6 +95,7 @@ export type OrganizationJoinRequestCountAggregateInputType = {
role?: true
updatedAt?: true
rejectReason?: true
requestMessage?: true
_all?: true
}
@@ -174,6 +180,7 @@ export type OrganizationJoinRequestGroupByOutputType = {
role: $Enums.ORG_ROLE
updatedAt: Date
rejectReason: string | null
requestMessage: string | null
_count: OrganizationJoinRequestCountAggregateOutputType | null
_min: OrganizationJoinRequestMinAggregateOutputType | null
_max: OrganizationJoinRequestMaxAggregateOutputType | null
@@ -206,6 +213,7 @@ export type OrganizationJoinRequestWhereInput = {
role?: Prisma.EnumORG_ROLEFilter<"OrganizationJoinRequest"> | $Enums.ORG_ROLE
updatedAt?: Prisma.DateTimeFilter<"OrganizationJoinRequest"> | Date | string
rejectReason?: Prisma.StringNullableFilter<"OrganizationJoinRequest"> | string | null
requestMessage?: Prisma.StringNullableFilter<"OrganizationJoinRequest"> | string | null
organization?: Prisma.XOR<Prisma.OrganizationScalarRelationFilter, Prisma.OrganizationWhereInput>
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
}
@@ -219,6 +227,7 @@ export type OrganizationJoinRequestOrderByWithRelationInput = {
role?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
rejectReason?: Prisma.SortOrderInput | Prisma.SortOrder
requestMessage?: Prisma.SortOrderInput | Prisma.SortOrder
organization?: Prisma.OrganizationOrderByWithRelationInput
user?: Prisma.UserOrderByWithRelationInput
}
@@ -236,6 +245,7 @@ export type OrganizationJoinRequestWhereUniqueInput = Prisma.AtLeast<{
role?: Prisma.EnumORG_ROLEFilter<"OrganizationJoinRequest"> | $Enums.ORG_ROLE
updatedAt?: Prisma.DateTimeFilter<"OrganizationJoinRequest"> | Date | string
rejectReason?: Prisma.StringNullableFilter<"OrganizationJoinRequest"> | string | null
requestMessage?: Prisma.StringNullableFilter<"OrganizationJoinRequest"> | string | null
organization?: Prisma.XOR<Prisma.OrganizationScalarRelationFilter, Prisma.OrganizationWhereInput>
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
}, "userId_orgId">
@@ -249,6 +259,7 @@ export type OrganizationJoinRequestOrderByWithAggregationInput = {
role?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
rejectReason?: Prisma.SortOrderInput | Prisma.SortOrder
requestMessage?: Prisma.SortOrderInput | Prisma.SortOrder
_count?: Prisma.OrganizationJoinRequestCountOrderByAggregateInput
_max?: Prisma.OrganizationJoinRequestMaxOrderByAggregateInput
_min?: Prisma.OrganizationJoinRequestMinOrderByAggregateInput
@@ -266,6 +277,7 @@ export type OrganizationJoinRequestScalarWhereWithAggregatesInput = {
role?: Prisma.EnumORG_ROLEWithAggregatesFilter<"OrganizationJoinRequest"> | $Enums.ORG_ROLE
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"OrganizationJoinRequest"> | Date | string
rejectReason?: Prisma.StringNullableWithAggregatesFilter<"OrganizationJoinRequest"> | string | null
requestMessage?: Prisma.StringNullableWithAggregatesFilter<"OrganizationJoinRequest"> | string | null
}
export type OrganizationJoinRequestCreateInput = {
@@ -275,6 +287,7 @@ export type OrganizationJoinRequestCreateInput = {
role?: $Enums.ORG_ROLE
updatedAt?: Date | string
rejectReason?: string | null
requestMessage?: string | null
organization: Prisma.OrganizationCreateNestedOneWithoutRequestingMembersInput
user: Prisma.UserCreateNestedOneWithoutOrganizationsRequestedInput
}
@@ -288,6 +301,7 @@ export type OrganizationJoinRequestUncheckedCreateInput = {
role?: $Enums.ORG_ROLE
updatedAt?: Date | string
rejectReason?: string | null
requestMessage?: string | null
}
export type OrganizationJoinRequestUpdateInput = {
@@ -297,6 +311,7 @@ export type OrganizationJoinRequestUpdateInput = {
role?: Prisma.EnumORG_ROLEFieldUpdateOperationsInput | $Enums.ORG_ROLE
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
rejectReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
requestMessage?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
organization?: Prisma.OrganizationUpdateOneRequiredWithoutRequestingMembersNestedInput
user?: Prisma.UserUpdateOneRequiredWithoutOrganizationsRequestedNestedInput
}
@@ -310,6 +325,7 @@ export type OrganizationJoinRequestUncheckedUpdateInput = {
role?: Prisma.EnumORG_ROLEFieldUpdateOperationsInput | $Enums.ORG_ROLE
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
rejectReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
requestMessage?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type OrganizationJoinRequestCreateManyInput = {
@@ -321,6 +337,7 @@ export type OrganizationJoinRequestCreateManyInput = {
role?: $Enums.ORG_ROLE
updatedAt?: Date | string
rejectReason?: string | null
requestMessage?: string | null
}
export type OrganizationJoinRequestUpdateManyMutationInput = {
@@ -330,6 +347,7 @@ export type OrganizationJoinRequestUpdateManyMutationInput = {
role?: Prisma.EnumORG_ROLEFieldUpdateOperationsInput | $Enums.ORG_ROLE
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
rejectReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
requestMessage?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type OrganizationJoinRequestUncheckedUpdateManyInput = {
@@ -341,6 +359,7 @@ export type OrganizationJoinRequestUncheckedUpdateManyInput = {
role?: Prisma.EnumORG_ROLEFieldUpdateOperationsInput | $Enums.ORG_ROLE
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
rejectReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
requestMessage?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type OrganizationJoinRequestUserIdOrgIdCompoundUniqueInput = {
@@ -357,6 +376,7 @@ export type OrganizationJoinRequestCountOrderByAggregateInput = {
role?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
rejectReason?: Prisma.SortOrder
requestMessage?: Prisma.SortOrder
}
export type OrganizationJoinRequestMaxOrderByAggregateInput = {
@@ -368,6 +388,7 @@ export type OrganizationJoinRequestMaxOrderByAggregateInput = {
role?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
rejectReason?: Prisma.SortOrder
requestMessage?: Prisma.SortOrder
}
export type OrganizationJoinRequestMinOrderByAggregateInput = {
@@ -379,6 +400,7 @@ export type OrganizationJoinRequestMinOrderByAggregateInput = {
role?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
rejectReason?: Prisma.SortOrder
requestMessage?: Prisma.SortOrder
}
export type OrganizationJoinRequestListRelationFilter = {
@@ -506,6 +528,7 @@ export type OrganizationJoinRequestCreateWithoutOrganizationInput = {
role?: $Enums.ORG_ROLE
updatedAt?: Date | string
rejectReason?: string | null
requestMessage?: string | null
user: Prisma.UserCreateNestedOneWithoutOrganizationsRequestedInput
}
@@ -517,6 +540,7 @@ export type OrganizationJoinRequestUncheckedCreateWithoutOrganizationInput = {
role?: $Enums.ORG_ROLE
updatedAt?: Date | string
rejectReason?: string | null
requestMessage?: string | null
}
export type OrganizationJoinRequestCreateOrConnectWithoutOrganizationInput = {
@@ -557,6 +581,7 @@ export type OrganizationJoinRequestScalarWhereInput = {
role?: Prisma.EnumORG_ROLEFilter<"OrganizationJoinRequest"> | $Enums.ORG_ROLE
updatedAt?: Prisma.DateTimeFilter<"OrganizationJoinRequest"> | Date | string
rejectReason?: Prisma.StringNullableFilter<"OrganizationJoinRequest"> | string | null
requestMessage?: Prisma.StringNullableFilter<"OrganizationJoinRequest"> | string | null
}
export type OrganizationJoinRequestCreateWithoutUserInput = {
@@ -566,6 +591,7 @@ export type OrganizationJoinRequestCreateWithoutUserInput = {
role?: $Enums.ORG_ROLE
updatedAt?: Date | string
rejectReason?: string | null
requestMessage?: string | null
organization: Prisma.OrganizationCreateNestedOneWithoutRequestingMembersInput
}
@@ -577,6 +603,7 @@ export type OrganizationJoinRequestUncheckedCreateWithoutUserInput = {
role?: $Enums.ORG_ROLE
updatedAt?: Date | string
rejectReason?: string | null
requestMessage?: string | null
}
export type OrganizationJoinRequestCreateOrConnectWithoutUserInput = {
@@ -613,6 +640,7 @@ export type OrganizationJoinRequestCreateManyOrganizationInput = {
role?: $Enums.ORG_ROLE
updatedAt?: Date | string
rejectReason?: string | null
requestMessage?: string | null
}
export type OrganizationJoinRequestUpdateWithoutOrganizationInput = {
@@ -622,6 +650,7 @@ export type OrganizationJoinRequestUpdateWithoutOrganizationInput = {
role?: Prisma.EnumORG_ROLEFieldUpdateOperationsInput | $Enums.ORG_ROLE
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
rejectReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
requestMessage?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
user?: Prisma.UserUpdateOneRequiredWithoutOrganizationsRequestedNestedInput
}
@@ -633,6 +662,7 @@ export type OrganizationJoinRequestUncheckedUpdateWithoutOrganizationInput = {
role?: Prisma.EnumORG_ROLEFieldUpdateOperationsInput | $Enums.ORG_ROLE
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
rejectReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
requestMessage?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type OrganizationJoinRequestUncheckedUpdateManyWithoutOrganizationInput = {
@@ -643,6 +673,7 @@ export type OrganizationJoinRequestUncheckedUpdateManyWithoutOrganizationInput =
role?: Prisma.EnumORG_ROLEFieldUpdateOperationsInput | $Enums.ORG_ROLE
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
rejectReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
requestMessage?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type OrganizationJoinRequestCreateManyUserInput = {
@@ -653,6 +684,7 @@ export type OrganizationJoinRequestCreateManyUserInput = {
role?: $Enums.ORG_ROLE
updatedAt?: Date | string
rejectReason?: string | null
requestMessage?: string | null
}
export type OrganizationJoinRequestUpdateWithoutUserInput = {
@@ -662,6 +694,7 @@ export type OrganizationJoinRequestUpdateWithoutUserInput = {
role?: Prisma.EnumORG_ROLEFieldUpdateOperationsInput | $Enums.ORG_ROLE
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
rejectReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
requestMessage?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
organization?: Prisma.OrganizationUpdateOneRequiredWithoutRequestingMembersNestedInput
}
@@ -673,6 +706,7 @@ export type OrganizationJoinRequestUncheckedUpdateWithoutUserInput = {
role?: Prisma.EnumORG_ROLEFieldUpdateOperationsInput | $Enums.ORG_ROLE
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
rejectReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
requestMessage?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type OrganizationJoinRequestUncheckedUpdateManyWithoutUserInput = {
@@ -683,6 +717,7 @@ export type OrganizationJoinRequestUncheckedUpdateManyWithoutUserInput = {
role?: Prisma.EnumORG_ROLEFieldUpdateOperationsInput | $Enums.ORG_ROLE
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
rejectReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
requestMessage?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
@@ -696,6 +731,7 @@ export type OrganizationJoinRequestSelect<ExtArgs extends runtime.Types.Extensio
role?: boolean
updatedAt?: boolean
rejectReason?: boolean
requestMessage?: boolean
organization?: boolean | Prisma.OrganizationDefaultArgs<ExtArgs>
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
}, ExtArgs["result"]["organizationJoinRequest"]>
@@ -709,6 +745,7 @@ export type OrganizationJoinRequestSelectCreateManyAndReturn<ExtArgs extends run
role?: boolean
updatedAt?: boolean
rejectReason?: boolean
requestMessage?: boolean
organization?: boolean | Prisma.OrganizationDefaultArgs<ExtArgs>
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
}, ExtArgs["result"]["organizationJoinRequest"]>
@@ -722,6 +759,7 @@ export type OrganizationJoinRequestSelectUpdateManyAndReturn<ExtArgs extends run
role?: boolean
updatedAt?: boolean
rejectReason?: boolean
requestMessage?: boolean
organization?: boolean | Prisma.OrganizationDefaultArgs<ExtArgs>
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
}, ExtArgs["result"]["organizationJoinRequest"]>
@@ -735,9 +773,10 @@ export type OrganizationJoinRequestSelectScalar = {
role?: boolean
updatedAt?: boolean
rejectReason?: boolean
requestMessage?: boolean
}
export type OrganizationJoinRequestOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"userId" | "orgId" | "status" | "requestType" | "requestedOn" | "role" | "updatedAt" | "rejectReason", ExtArgs["result"]["organizationJoinRequest"]>
export type OrganizationJoinRequestOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"userId" | "orgId" | "status" | "requestType" | "requestedOn" | "role" | "updatedAt" | "rejectReason" | "requestMessage", ExtArgs["result"]["organizationJoinRequest"]>
export type OrganizationJoinRequestInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
organization?: boolean | Prisma.OrganizationDefaultArgs<ExtArgs>
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
@@ -766,6 +805,7 @@ export type $OrganizationJoinRequestPayload<ExtArgs extends runtime.Types.Extens
role: $Enums.ORG_ROLE
updatedAt: Date
rejectReason: string | null
requestMessage: string | null
}, ExtArgs["result"]["organizationJoinRequest"]>
composites: {}
}
@@ -1199,6 +1239,7 @@ export interface OrganizationJoinRequestFieldRefs {
readonly role: Prisma.FieldRef<"OrganizationJoinRequest", 'ORG_ROLE'>
readonly updatedAt: Prisma.FieldRef<"OrganizationJoinRequest", 'DateTime'>
readonly rejectReason: Prisma.FieldRef<"OrganizationJoinRequest", 'String'>
readonly requestMessage: Prisma.FieldRef<"OrganizationJoinRequest", 'String'>
}

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

@@ -1,12 +1,13 @@
model OrganizationJoinRequest {
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?
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)
@@ -19,6 +20,7 @@ enum ORGANIZATION_JOIN_REQUEST {
PENDING
ACCEPTED
REJECTED
CANCELLED
}
enum ORGANIZATION_JOIN_REQUEST_TYPE {

View File

@@ -44,12 +44,14 @@ export class CacheService {
// Fallback to DB
const fresh = await factory();
// Try setting cache only if Redis available
if (this.redisAvailable) {
try {
await this.cache.set(key, fresh, ttl);
} catch {
this.redisAvailable = false;
if (fresh) {
// Try setting cache only if Redis available
if (this.redisAvailable) {
try {
await this.cache.set(key, fresh, ttl);
} catch {
this.redisAvailable = false;
}
}
}

View File

@@ -2,13 +2,14 @@ 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);
const swaggerConfig = new DocumentBuilder()
.setTitle('Kaa Khane')
.setDescription(`API Documentation for Kaa Khane`)
.setTitle('MultiTenant Saas')
.setDescription(`API Documentation for a simple MultiTenant Saas Application`)
.setVersion('0.0.1')
.addGlobalResponse(
{
@@ -46,5 +47,7 @@ async function bootstrap() {
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

@@ -1 +1,3 @@
export * from './invite-to-org.dto';
export * from './join-request.dto'
export * from "./user-invitation-action.dto"

View File

@@ -12,15 +12,6 @@ export class InviteUserToOrganizationRequestDTO {
@IsNotEmpty()
invitedUserEmail: string;
@ApiProperty({
description: 'Organization id',
example: 'eeec2c79-766a-4174-8004-2e57642095fe',
type: 'string',
})
@IsUUID()
@IsNotEmpty()
orgId: string;
@ApiProperty({
description: 'Role to assign',
example: ORG_ROLE.member,

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,24 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsEnum, IsNotEmpty, IsOptional, IsString, IsUUID } 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

@@ -1,62 +1,145 @@
import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common';
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 } from '@nestjs/swagger';
import { InviteUserToOrganizationRequestDTO } from './dto';
import { ApiBearerAuth, ApiOperation, ApiParam, ApiQuery } from '@nestjs/swagger';
import { InviteUserToOrganizationRequestDTO, JoinRequestToOrganizationRequestDTO, UserOrganizationInvitationActionRequestDTO } from './dto';
import { ORGANIZATION_JOIN_REQUEST_TYPE } from 'prisma/generated/prisma/enums';
@Controller('organization-membership')
/* 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,
) {}
) { }
@Post('/invite')
async inviteUserToOrg(@Body() body: InviteUserToOrganizationRequestDTO) {
const user = this.requestContext.user;
return await this.orgMemService.inviteUserToOrg(user.userId, body);
}
@Post('/request')
async requestToJoinOrg() {}
// TODO: Move invite to org. section and join to user. Also option to cancel invitation and join req.
/* *
* USER OPERATIONS
* */
@Patch('user/accept-invitation')
acceptInvitation() {}
@Patch('user/reject-invitation')
rejectInvitation() {}
@Get('user/invitations')
async getUserInvitations() {
@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.getUserInvitations(user.userId);
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')
async cancelRequestToJoinOrg(@Param('orgId', new ParseUUIDPipe()) orgId: string) {
const user = this.requestContext.user;
return await this.orgMemService.userCancelOrgJoinRequest(user.userId, orgId)
}
@ApiOperation({ summary: 'Accept or reject an invitation from an organization' })
@ApiParam({
name: 'orgId',
type: String,
})
@Patch('organization/:orgId/invitation')
async acceptOrRejectInvitation(
@Param('orgId', new ParseUUIDPipe()) orgId: string,
@Body() body: UserOrganizationInvitationActionRequestDTO
) {
const user = this.requestContext.user;
return await this.orgMemService.userOrganiaztionRequestAction(
user.userId,
orgId,
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)
}
@Get('user/organizations')
async getUserOrganizations() {}
/* *
* ORGANIZATION OPERATIONS
* */
@Get('organization/:id/members')
async getOrganizationMemebers(@Param('id') orgId: string) {
return await this.orgMemService.getMemebersOfOrganization(orgId);
}
@Get('organization/:id/invitations')
async getOrganizationInvitations(@Param('id') orgId: string) {
return await this.orgMemService.getMemebersOfOrganization(orgId);
}
@Patch('organization/:id/accept-request')
acceptJoinRequest() {}
@Patch('organization/:id/reject-request')
rejectJoinRequest() {}
// @ApiOperation({ summary: 'Invite user to organization' })
// @ApiParam({
// name: 'orgId',
// type: String,
// })
// @Post('organization/:orgId/invitation')
// async inviteUserToOrg(
// @Param('orgId', new ParseUUIDPipe()) orgId: string,
// @Body() body: InviteUserToOrganizationRequestDTO
// ) {
// const user = this.requestContext.user;
// return await this.orgMemService.inviteUserToOrg(
// user.userId,
// orgId,
// body
// );
// }
// @Get('organization/:id/members')
// async getOrganizationMemebers(@Param('id') orgId: string) {
// return await this.orgMemService.getMemebersOfOrganization(orgId);
// }
//
// @Get('organization/:id/invitations')
// async getOrganizationInvitations(@Param('id') orgId: string) {
// return await this.orgMemService.getOrganizationRequestList(orgId);
// }
//
// @Patch('organization/:id/accept-request')
// acceptJoinRequest() { }
//
// @Patch('organization/:id/reject-request')
// rejectJoinRequest() { }
}

View File

@@ -6,7 +6,7 @@ import {
} from '@nestjs/common';
import { OrganizationService } from 'src/organization/organization.service';
import { UserService } from 'src/user/user.service';
import { InviteUserToOrganizationRequestDTO } from './dto';
import { InviteUserToOrganizationRequestDTO, UserOrganizationInvitationActionRequestDTO } from './dto';
import { PrismaService } from 'src/prisma/prisma.service';
import {
ORGANIZATION_JOIN_REQUEST,
@@ -15,6 +15,8 @@ import {
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 {
@@ -23,14 +25,15 @@ export class OrganizationMembershipService {
private readonly orgService: OrganizationService,
private readonly prisma: PrismaService,
private readonly authorization: AuthorizationService,
) {}
) { }
async inviteUserToOrg(
userId: string,
orgId: string,
dto: InviteUserToOrganizationRequestDTO,
) {
const { invitedUserEmail, ...rest } = dto;
const [orgExists, invitedUser] = await Promise.all([
this.orgService.organizationExists(dto.orgId),
this.orgService.findById(orgId),
this.userService.findByEmail(invitedUserEmail),
]);
@@ -41,7 +44,7 @@ export class OrganizationMembershipService {
await this.prisma.organizationUserJoinTable.findUnique({
where: {
userId_orgId: {
orgId: dto.orgId,
orgId: orgId,
userId: invitedUser.id,
},
},
@@ -52,7 +55,7 @@ export class OrganizationMembershipService {
const canInviteUser = await this.authorization.canPerformOperation(
userId,
dto.orgId,
orgId,
USER_ORGANIZATION_OPERATIONS.INVITE_USERS,
);
if (!canInviteUser) throw new ForbiddenException('Insufficient Permission');
@@ -62,6 +65,7 @@ export class OrganizationMembershipService {
data: {
...rest,
userId: invitedUser.id,
orgId,
requestType: ORGANIZATION_JOIN_REQUEST_TYPE.INVITED,
},
});
@@ -77,63 +81,227 @@ export class OrganizationMembershipService {
}
}
requestToJoin() {}
// 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,
},
},
}),
/* *
* USER OPERATIONS
* */
async usersRequestToJoin(
userId: string,
orgId: string,
dto: JoinRequestToOrganizationRequestDTO
) {
const [
orgExists,
invitationAlreadySent,
userAlreadyPartOf
] = await Promise.all([
this.orgService.findById(orgId),
this.prisma.organizationJoinRequest.findUnique({
where: {
userId_orgId: {
orgId,
userId,
orgId: orgId
},
status: ORGANIZATION_JOIN_REQUEST.PENDING,
status: ORGANIZATION_JOIN_REQUEST.PENDING
},
select: { orgId: true }
}),
]);
this.prisma.organizationUserJoinTable.findUnique({
where: {
userId_orgId: {
orgId: orgId,
userId
}
},
select: { userId: true }
})
])
if (userAlreadyPart)
throw new BadRequestException('User already part of this organization');
if (!isUserInvited)
throw new BadRequestException(
'User has no invitations from this organization',
);
if (!orgExists)
throw new NotFoundException("Organization")
if (invitationAlreadySent)
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.$transaction(async (tx) => {
await tx.organizationJoinRequest.update({
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
) {
try {
await this.prisma.organizationJoinRequest.update({
where: {
userId_orgId: {
userId,
orgId,
orgId
},
},
data: {
status: ORGANIZATION_JOIN_REQUEST.ACCEPTED,
},
});
return await tx.organizationUserJoinTable.create({
data: {
orgId,
userId,
},
});
});
status: ORGANIZATION_JOIN_REQUEST.CANCELLED,
}
})
}
catch (err) {
throw new NotFoundException("Join 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
}
})
}
async userOrganiaztionRequestAction(
userId: string,
orgId: string,
dto: UserOrganizationInvitationActionRequestDTO
) {
const [orgExists, hasUserSendRequest] = await Promise.all([
this.orgService.findById(orgId),
this.prisma.organizationJoinRequest.findUnique({
where: {
userId_orgId: {
userId,
orgId: orgId
},
status: ORGANIZATION_JOIN_REQUEST.PENDING,
requestType: ORGANIZATION_JOIN_REQUEST_TYPE.REQUESTED
}
})
])
if (!orgExists)
throw new NotFoundException("Organization")
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: {
userId_orgId: {
userId,
orgId: orgId,
}
},
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) {
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,
@@ -153,8 +321,58 @@ export class OrganizationMembershipService {
});
}
async getOrganizationsOfUser(userId: string) {
return await this.prisma.organizationJoinRequest.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 getOrganizationRequestList(
orgId: string,
requestType: ORGANIZATION_JOIN_REQUEST_TYPE = ORGANIZATION_JOIN_REQUEST_TYPE.REQUESTED,
status: ORGANIZATION_JOIN_REQUEST = ORGANIZATION_JOIN_REQUEST.PENDING,
) {
// TODO: Check can perform
return await this.prisma.organizationJoinRequest.findMany({
where: {
orgId: orgId,
status: status,
requestType: requestType,
},
include: {
user: {
select: { email: true },
},
},
});
}
async getMemebersOfOrganization(orgId: string) {
const orgExists = await this.orgService.organizationExists(orgId);
const orgExists = await this.orgService.findById(orgId);
if (!orgExists) throw new NotFoundException('Organization');
return await this.prisma.organizationUserJoinTable.findMany({

View File

@@ -21,7 +21,7 @@ export class OrganizationService {
// private readonly reqContext: RequestContextService,
private readonly authorization: AuthorizationService,
private readonly cacheService: CacheService,
) {}
) { }
async createNewOrganization(
userId: string,
dto: CreateNewOrganizationRequestDTO,
@@ -60,7 +60,7 @@ export class OrganizationService {
orgId: string,
dto: UpdateOrganizationRequestDTO,
) {
const orgExists = await this.organizationExists(orgId);
const orgExists = await this.findById(orgId);
if (!orgExists) throw new NotFoundException('Organization');
const canUserUpdateOrganization =
@@ -83,7 +83,7 @@ export class OrganizationService {
// TODO: Either empty or choose someone to be owner
async deleteAnOrganization(userId: string, orgId: string) {
const orgExists = await this.organizationExists(orgId);
const orgExists = await this.findById(orgId);
if (!orgExists) throw new NotFoundException('Organization');
const canUserDeleteOrganization =
@@ -118,13 +118,16 @@ export class OrganizationService {
});
}
async organizationExists(orgId: string) {
return await this.prisma.organization.findUnique({
where: { id: orgId },
select: { id: true },
});
}
// async organizationExists(orgId: string) {
// return await this.prisma.organization.findUnique({
// where: { id: orgId },
// select: { id: true },
// });
// }
/*
* Its neat, it caches info plus only selects id so faster DB search as well
* */
async findById(orgId: string) {
const organization = await this.cacheService.getOrSet(
orgId,