feat: User operations on join org
This commit is contained in:
389
CODE_REVIEW.md
Normal file
389
CODE_REVIEW.md
Normal file
@@ -0,0 +1,389 @@
|
||||
# Code Review — Kaa Khane (Multi-Tenant SaaS Backend)
|
||||
|
||||
## What This Is
|
||||
|
||||
A NestJS backend for a multi-tenant SaaS platform. It models the core of any team-based product: users belong to organizations, organizations have roles (`owner`, `admin`, `member`), and membership is managed through a request/invitation flow. Stack: NestJS + Prisma + PostgreSQL + Redis (via Keyv). This is a learning project but the ambition is real — you've touched AsyncLocalStorage, operation-based authorization, cache resilience, and transactional writes. That's not beginner stuff.
|
||||
|
||||
---
|
||||
|
||||
## What You're Doing Well
|
||||
|
||||
**1. AsyncLocalStorage for request context**
|
||||
This is a legitimately good pattern. Instead of threading `req.user` through every function signature, you store it in ALS and services pull it from context. This is how production NestJS apps at scale handle per-request state. Most people learning NestJS never get here.
|
||||
|
||||
**2. Cache-aside with graceful degradation**
|
||||
`CacheService.getOrSet()` is well thought out. It tracks `redisAvailable`, silently falls back to DB, and doesn't let a Redis outage crash the app. The pattern is correct. The placement of `console.warn` in a service is slightly off (more on that below) but the intent is solid.
|
||||
|
||||
**3. Parallel DB checks with `Promise.all`**
|
||||
You're doing this in multiple places — running independent queries concurrently instead of awaiting them in sequence. This is the right instinct and most junior devs miss it.
|
||||
|
||||
**4. Operation-based authorization**
|
||||
`AuthorizationService.canPerformOperation()` with a switch statement and named operations (`INVITE_USERS`, `DELETE_ORGANIZATION`) is cleaner than scattering raw role checks everywhere. It's a solid foundation.
|
||||
|
||||
**5. Transactions where they matter**
|
||||
`createNewOrganization` and `userOrganiaztionRequestAction` use `$transaction` to ensure atomicity. You understand when to use them.
|
||||
|
||||
**6. DTO validation layer**
|
||||
Using `class-validator` + `@nestjs/swagger` together is standard and correct. `@AtLeastOneField()` is a good custom validator — you're not reaching for a library when a simple decorator does the job.
|
||||
|
||||
**7. Global `AuthGuard` with `@Public()` opt-out**
|
||||
Secure by default. Making all routes protected and requiring explicit `@Public()` to open them is the right model. A lot of apps do it backwards.
|
||||
|
||||
---
|
||||
|
||||
## Issues and What to Improve
|
||||
|
||||
Organized by severity: `[critical]`, `[high]`, `[medium]`, `[low]`.
|
||||
|
||||
---
|
||||
|
||||
### 1. Hardcoded JWT secret `[critical]`
|
||||
|
||||
**File:** `src/auth/auth.service.ts:60`, `src/auth/guards/auth.guard.ts:38`
|
||||
|
||||
```ts
|
||||
secret: 'demo';
|
||||
```
|
||||
|
||||
You know this is wrong (you left a TODO). But let me explain _why_ it's critical: if this ever reaches a staging environment and someone rotates the secret, every user is logged out. Worse, the secret is in version control. Fix this before you add anything else.
|
||||
|
||||
**Fix:**
|
||||
|
||||
```ts
|
||||
// in JwtModule registration (auth.module.ts)
|
||||
JwtModule.registerAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
secret: config.getOrThrow<string>('JWT_SECRET'),
|
||||
signOptions: { expiresIn: '15m' },
|
||||
}),
|
||||
}),
|
||||
```
|
||||
|
||||
Then remove the `secret` override from every `signAsync` / `verifyAsync` call. The module-level secret is used automatically.
|
||||
|
||||
---
|
||||
|
||||
### 2. `userOrganiaztionRequestAction` uses `this.prisma` inside a `$transaction` `[critical]`
|
||||
|
||||
**File:** `src/organization-membership/organization-membership.service.ts:226`
|
||||
|
||||
```ts
|
||||
return await this.prisma.$transaction(async (tx) => {
|
||||
await tx.organizationJoinRequest.update(...) // ✅ uses tx
|
||||
|
||||
if (userAction === ORGANIZATION_JOIN_REQUEST.ACCEPTED)
|
||||
await this.prisma.organizationUserJoinTable.create(...) // ❌ uses this.prisma — NOT the tx
|
||||
})
|
||||
```
|
||||
|
||||
The `create` inside the transaction is using `this.prisma` (the global client) instead of `tx` (the transaction client). If the second write fails, the first write is NOT rolled back. The whole point of wrapping in `$transaction` is broken here.
|
||||
|
||||
**Fix:**
|
||||
|
||||
```ts
|
||||
await tx.organizationUserJoinTable.create({ ... })
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Two flows conflated into one method — and the semantics are wrong `[high]`
|
||||
|
||||
**File:** `src/organization-membership/organization-membership.service.ts:180`
|
||||
|
||||
`userOrganiaztionRequestAction` is currently called by the user (the invitee) to accept or reject an invitation. But based on your plan, you also want an org admin to accept/reject a join request. These are fundamentally different actors:
|
||||
|
||||
| Flow | Actor | What they act on |
|
||||
| -------------------------- | ---------------------- | ---------------------------------- |
|
||||
| Accept/reject invitation | The invited **user** | Their own pending `INVITED` record |
|
||||
| Accept/reject join request | An **org admin/owner** | Someone else's `REQUESTED` record |
|
||||
|
||||
Right now the method doesn't distinguish by `requestType`. An org admin could use it to act on _their own_ data when they should be acting on _another user's_ data. They need to be two methods:
|
||||
|
||||
- `userRespondToInvitation(userId, dto)` — user accepts/rejects where `requestType = INVITED`
|
||||
- `adminRespondToJoinRequest(adminUserId, targetUserId, orgId, action)` — admin acts on `requestType = REQUESTED`, with a `canPerformOperation` check first
|
||||
|
||||
This is not a minor naming issue. It's a modeling issue. The current code would let a user "accept" their own join request.
|
||||
|
||||
---
|
||||
|
||||
### 4. `organizationExists` vs `findById` — two methods doing nearly the same thing `[high]`
|
||||
|
||||
**File:** `src/organization/organization.service.ts:121`
|
||||
|
||||
```ts
|
||||
async organizationExists(orgId: string) {
|
||||
return await this.prisma.organization.findUnique({
|
||||
where: { id: orgId },
|
||||
select: { id: true }, // minimal select — just checking existence
|
||||
});
|
||||
}
|
||||
|
||||
async findById(orgId: string) {
|
||||
return await this.cacheService.getOrSet(orgId, async () =>
|
||||
await this.prisma.organization.findUnique({ where: { id: orgId } }),
|
||||
);
|
||||
// returns full org object, goes through cache
|
||||
}
|
||||
```
|
||||
|
||||
These have different purposes (one is a lightweight existence check, one returns full data with caching) but the distinction is opaque from the call site. `OrganizationMembershipService` calls both, inconsistently.
|
||||
|
||||
More importantly: `organizationExists` bypasses the cache entirely. If you call `findById` first (which caches), then call `organizationExists`, you've now made an extra DB round-trip for no reason.
|
||||
|
||||
**What to do:**
|
||||
|
||||
- Consolidate. Use `findById` (cached) everywhere.
|
||||
- If you need a boolean check, add a thin wrapper: `async assertOrganizationExists(orgId)` that calls `findById` and throws `NotFoundException` if null. Then services call that one line instead of the two-line check-then-throw pattern you repeat everywhere.
|
||||
- Delete `organizationExists` or make it call `findById` internally.
|
||||
|
||||
---
|
||||
|
||||
### 5. Repeated guard pattern — check existence, throw — everywhere `[high]`
|
||||
|
||||
You do this in almost every method:
|
||||
|
||||
```ts
|
||||
const orgExists = await this.orgService.findById(dto.orgId);
|
||||
if (!orgExists) throw new NotFoundException('Organization');
|
||||
```
|
||||
|
||||
And separately:
|
||||
|
||||
```ts
|
||||
const canUserDeleteOrganization = await this.authorization.canPerformOperation(...);
|
||||
if (!canUserDeleteOrganization) throw new ForbiddenException('Not enough permission');
|
||||
```
|
||||
|
||||
This is the "guard clause" pattern and the repetition itself isn't wrong — guard clauses are good. What's wrong is that the _authorization check_ is manually wired in every service method instead of being declarative. This will become painful as you add more operations.
|
||||
|
||||
**Better pattern — make `canPerformOperation` throw directly:**
|
||||
|
||||
```ts
|
||||
// authorization.service.ts
|
||||
async assertCanPerformOperation(
|
||||
userId: string,
|
||||
orgId: string,
|
||||
operation: USER_ORGANIZATION_OPERATIONS,
|
||||
): Promise<void> {
|
||||
const allowed = await this.canPerformOperation(userId, orgId, operation);
|
||||
if (!allowed) throw new ForbiddenException('Insufficient permissions');
|
||||
}
|
||||
```
|
||||
|
||||
Now the call site is one line with no `if`:
|
||||
|
||||
```ts
|
||||
await this.authorization.assertCanPerformOperation(userId, orgId, INVITE_USERS);
|
||||
```
|
||||
|
||||
Clean. Unambiguous. And you can add logging, metrics, or audit trail in one place.
|
||||
|
||||
---
|
||||
|
||||
### 6. `isAdmin()` is dead code `[medium]`
|
||||
|
||||
**File:** `src/authorization/authorization.service.ts:49`
|
||||
|
||||
`isAdmin()` is defined but never called. Either use it in `canInvite` (which currently re-implements the same logic inline) or delete it.
|
||||
|
||||
```ts
|
||||
// current canInvite re-implements admin check inline:
|
||||
return (
|
||||
isUserPartOfOrganization.role === ORG_ROLE.admin ||
|
||||
isUserPartOfOrganization.role === ORG_ROLE.owner
|
||||
);
|
||||
|
||||
// isAdmin() does the same thing but only for admin role.
|
||||
// Clean it up or wire it in.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. `AuthorizationService` hits the DB every time — no caching `[medium]`
|
||||
|
||||
**File:** `src/authorization/authorization.service.ts:61`
|
||||
|
||||
Every call to `canPerformOperation` does a `findUnique` on `OrganizationUserJoinTable`. In `inviteUserToOrg` alone, you've already called `orgService.findById` (which hits cache), but then `canPerformOperation` hits DB cold. Membership data is relatively stable — it changes when someone joins, leaves, or is promoted.
|
||||
|
||||
**Fix:** Cache the membership record in `CacheService` using a compound key like `membership:{userId}:{orgId}`. Invalidate it when a member joins, leaves, or their role changes.
|
||||
|
||||
```ts
|
||||
const key = `membership:${userId}:${orgId}`;
|
||||
return this.cacheService.getOrSet(key, () =>
|
||||
this.prisma.organizationUserJoinTable.findUnique(...)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8. `deleteAnOrganization` has broken cascade logic `[medium]`
|
||||
|
||||
**File:** `src/organization/organization.service.ts:99`
|
||||
|
||||
```ts
|
||||
await tx.organization.delete({ where: { id: orgId } });
|
||||
|
||||
await tx.organizationUserJoinTable.delete({
|
||||
where: { userId_orgId: { userId, orgId } }, // only deletes the OWNER's row
|
||||
});
|
||||
```
|
||||
|
||||
This only deletes the owner's row from the join table. All other members' rows are left orphaned (or fail with a FK constraint — depending on your Prisma schema cascade setting). Also, `OrganizationJoinRequest` records for this org are not cleaned up.
|
||||
|
||||
You should either:
|
||||
|
||||
- Set `onDelete: Cascade` in the Prisma schema on those relations (so deleting the org auto-cleans everything), or
|
||||
- Manually `deleteMany` all join rows and requests in the transaction before deleting the org
|
||||
|
||||
Option 1 (schema cascade) is cleaner for this case.
|
||||
|
||||
---
|
||||
|
||||
### 9. `console.log` / `console.warn` in production code `[medium]`
|
||||
|
||||
**Files:** `src/cache/cache.service.ts:13,18`, `src/cache/cache.service.ts:62`, `src/prisma/prisma.service.ts:40`
|
||||
|
||||
```ts
|
||||
console.log(a); // in deleteKey — what is `a`?
|
||||
console.warn('Redis disconnected');
|
||||
```
|
||||
|
||||
Use NestJS's built-in `Logger`:
|
||||
|
||||
```ts
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
private readonly logger = new Logger(CacheService.name);
|
||||
|
||||
this.logger.warn('Redis disconnected');
|
||||
this.logger.error('Prisma connection failed', err.stack);
|
||||
```
|
||||
|
||||
The NestJS logger respects log levels, outputs structured logs, and can be replaced with a production logger (Pino, Winston) without touching service code.
|
||||
|
||||
---
|
||||
|
||||
### 10. Controller stubs are wired wrong — copy-paste bug `[medium]`
|
||||
|
||||
**File:** `src/organization-membership/organization-membership.controller.ts:54`
|
||||
|
||||
```ts
|
||||
@Get('organization/:id/invitations')
|
||||
async getOrganizationInvitations(@Param('id') orgId: string) {
|
||||
return await this.orgMemService.getMemebersOfOrganization(orgId); // ❌ wrong method
|
||||
}
|
||||
```
|
||||
|
||||
`getOrganizationInvitations` is calling `getMemebersOfOrganization`. Both endpoints return the same data. This is a copy-paste leftover.
|
||||
|
||||
---
|
||||
|
||||
### 11. `userOrganizationJoinRequestList` does manual enum validation `[low]`
|
||||
|
||||
**File:** `src/organization-membership/organization-membership.service.ts:135`
|
||||
|
||||
```ts
|
||||
async userOrganizationJoinRequestList(userId: string, requestType: string) {
|
||||
const joinReqType: ORGANIZATION_JOIN_REQUEST_TYPE | undefined =
|
||||
ORGANIZATION_JOIN_REQUEST_TYPE[requestType];
|
||||
if (!joinReqType) throw new BadRequestException('Invalid request type');
|
||||
```
|
||||
|
||||
This manual enum validation in the service layer exists because `requestType` comes in as a raw `string`. The fix is to validate it in the DTO/query params using `@IsEnum(ORGANIZATION_JOIN_REQUEST_TYPE)`. Then the service never receives an invalid value — NestJS's validation pipe rejects it before it gets there. The service layer should not be doing input validation.
|
||||
|
||||
---
|
||||
|
||||
### 12. `BaseException` exists but is never used `[low]`
|
||||
|
||||
**File:** `common/exceptions/custom-exceptions.ts`
|
||||
|
||||
You built a `BaseException` class with `code`, `message`, and `HttpStatus`. Nothing uses it. Either start using it for domain-specific errors (e.g., `OrganizationNotFoundError extends BaseException`) or delete it. Dead abstractions add noise.
|
||||
|
||||
---
|
||||
|
||||
### 13. Typos in method and variable names `[low]`
|
||||
|
||||
- `userOrganiaztionRequestAction` → `userOrganizationRequestAction`
|
||||
- `getMemebersOfOrganization` → `getMembersOfOrganization`
|
||||
|
||||
These propagate to controller method names, controller routes, and service interface. Fix them now while the surface area is small.
|
||||
|
||||
---
|
||||
|
||||
## General Guidelines to Internalize
|
||||
|
||||
### On DRY vs. "right abstraction"
|
||||
|
||||
DRY (Don't Repeat Yourself) is often misapplied. The goal isn't zero duplication — it's avoiding duplication of _knowledge_ (the same decision made in two places). Two methods that both check `if (!org) throw NotFoundException` are DRY violations only if they're checking the same _thing_. The right fix is `assertOrganizationExists()` — a single function that owns the "org must exist" rule. The callers delegate to it.
|
||||
|
||||
### On service layer responsibilities
|
||||
|
||||
A service method should do one thing: execute a business operation. Input validation belongs in DTOs + pipes. Authorization belongs in guards or a dedicated authorization service (you're halfway there). Existence checks belong in helper methods on the service. What remains in the method body should read like a business narrative: "check user is allowed → do the thing → return result."
|
||||
|
||||
### On the transaction client (`this.prisma.client`)
|
||||
|
||||
You built the ALS-based `prisma.client` getter for shared transactions across services. It's a good idea — but it only works if services call `this.prisma.client.xxx` instead of `this.prisma.xxx`. Right now no service uses it. Either commit to the pattern (use `client` everywhere) or remove the getter to avoid confusion. Half-implemented patterns are worse than no pattern — they create false confidence.
|
||||
|
||||
### On authorization: where you are vs. where to go
|
||||
|
||||
**Where you are:** operation-based (`canPerformOperation` switch). This is fine for 3-5 operations. As the app grows, the switch becomes unmaintainable and you end up with logic spread across the switch, `isOwner`, `canInvite`, etc.
|
||||
|
||||
**Where to go next:** a role-permission matrix.
|
||||
|
||||
```ts
|
||||
const PERMISSIONS: Record<ORG_ROLE, USER_ORGANIZATION_OPERATIONS[]> = {
|
||||
[ORG_ROLE.owner]: [
|
||||
UPDATE_ORGANIZATION,
|
||||
DELETE_ORGANIZATION,
|
||||
INVITE_USERS,
|
||||
REMOVE_MEMBERS,
|
||||
],
|
||||
[ORG_ROLE.admin]: [INVITE_USERS, REMOVE_MEMBERS],
|
||||
[ORG_ROLE.member]: [],
|
||||
};
|
||||
```
|
||||
|
||||
Then `canPerformOperation` becomes:
|
||||
|
||||
```ts
|
||||
async canPerformOperation(userId, orgId, operation) {
|
||||
const membership = await this.getMembership(userId, orgId);
|
||||
if (!membership) return false;
|
||||
return PERMISSIONS[membership.role].includes(operation);
|
||||
}
|
||||
```
|
||||
|
||||
Adding a new operation is one line in the matrix. No new DB queries, no new switch case.
|
||||
|
||||
**Beyond that:** [CASL](https://casl.js.org/) is the standard Node.js authorization library for more complex scenarios (attribute-level permissions, "can user edit _this specific_ resource"). You don't need it now but it's worth knowing it exists.
|
||||
|
||||
### On caching: what to cache, what to invalidate
|
||||
|
||||
Cache data that is: (a) read frequently, (b) expensive to recompute, (c) stable. Organization data and membership data fit all three. The rule is simple: **every write must invalidate or update the relevant cache key**. Right now you invalidate on org delete but not on org update. When you cache membership, you must invalidate on role change, member removal, and member addition.
|
||||
|
||||
Use structured cache keys: `org:{orgId}`, `membership:{userId}:{orgId}`. Bare IDs as keys (like you're doing now with just `orgId`) work but collide if you ever cache different types under the same store.
|
||||
|
||||
### On the two-flow membership design (your plan)
|
||||
|
||||
Your instinct to split into two separate flows is correct. Model it explicitly:
|
||||
|
||||
- `POST /organization-membership/invite` — org admin invites a user (creates `INVITED` record)
|
||||
- `POST /organization-membership/request` — user requests to join (creates `REQUESTED` record)
|
||||
- `PATCH /organization-membership/user/respond` — **user** accepts/rejects their own `INVITED` record
|
||||
- `PATCH /organization-membership/organization/:id/respond` — **admin** accepts/rejects a `REQUESTED` record
|
||||
|
||||
The actor and the target record type are different in each case. Wire your service methods to match this — each method should assert `requestType` before acting.
|
||||
|
||||
### On what to build next (priority order)
|
||||
|
||||
1. Fix the `$transaction` bug (item 2 above) — data integrity issue
|
||||
2. Split the two membership flows into two methods
|
||||
3. Move JWT secret to `ConfigService`
|
||||
4. Add `assertOrganizationExists()` / `assertCanPerformOperation()` helpers — this cleans up every service method
|
||||
5. Fix the cascade on org delete
|
||||
6. Wire up the remaining controller stubs
|
||||
7. Add pagination to `getOrganizations()`
|
||||
8. Replace `console.log` with `Logger`
|
||||
9. Cache membership in `AuthorizationService`
|
||||
10. Delete dead code (`isAdmin`, `BaseException`, commented-out `acceptInvitation`)
|
||||
@@ -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> {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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'>
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "organization_join_request" ADD COLUMN "requestMessage" TEXT;
|
||||
@@ -0,0 +1 @@
|
||||
-- This is an empty migration.
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "ORGANIZATION_JOIN_REQUEST" ADD VALUE 'CANCELLED';
|
||||
@@ -7,6 +7,7 @@ model OrganizationJoinRequest {
|
||||
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 {
|
||||
|
||||
2
src/cache/cache.service.ts
vendored
2
src/cache/cache.service.ts
vendored
@@ -44,6 +44,7 @@ export class CacheService {
|
||||
// Fallback to DB
|
||||
const fresh = await factory();
|
||||
|
||||
if (fresh) {
|
||||
// Try setting cache only if Redis available
|
||||
if (this.redisAvailable) {
|
||||
try {
|
||||
@@ -52,6 +53,7 @@ export class CacheService {
|
||||
this.redisAvailable = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fresh;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
1
src/organization-membership/constants/index.ts
Normal file
1
src/organization-membership/constants/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./type"
|
||||
4
src/organization-membership/constants/type.ts
Normal file
4
src/organization-membership/constants/type.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export enum USER_ORG_ACCEPT_REJECT_ACTION {
|
||||
ACCEPT = 'ACCEPT',
|
||||
REJECT = 'REJECT'
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
export * from './invite-to-org.dto';
|
||||
export * from './join-request.dto'
|
||||
export * from "./user-invitation-action.dto"
|
||||
|
||||
@@ -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,
|
||||
|
||||
13
src/organization-membership/dto/join-request.dto.ts
Normal file
13
src/organization-membership/dto/join-request.dto.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsNotEmpty, IsOptional, IsString, IsUUID } from "class-validator";
|
||||
|
||||
export class JoinRequestToOrganizationRequestDTO {
|
||||
@ApiPropertyOptional({
|
||||
description: 'Message along with the request',
|
||||
example: 'I would like to join',
|
||||
type: 'string',
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
requestMessage?: string;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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() { }
|
||||
}
|
||||
|
||||
@@ -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,62 +81,226 @@ 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
|
||||
},
|
||||
select: { orgId: true }
|
||||
}),
|
||||
this.prisma.organizationUserJoinTable.findUnique({
|
||||
where: {
|
||||
userId_orgId: {
|
||||
orgId: orgId,
|
||||
userId
|
||||
}
|
||||
},
|
||||
select: { userId: true }
|
||||
})
|
||||
])
|
||||
|
||||
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.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
|
||||
},
|
||||
},
|
||||
data: {
|
||||
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 (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 (!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: orgId,
|
||||
}
|
||||
},
|
||||
data: {
|
||||
status: ORGANIZATION_JOIN_REQUEST.ACCEPTED,
|
||||
},
|
||||
status: userAction,
|
||||
...(
|
||||
userAction === ORGANIZATION_JOIN_REQUEST.REJECTED && dto.message
|
||||
? { rejectReason: dto.message }
|
||||
: {}
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
return await tx.organizationUserJoinTable.create({
|
||||
if (userAction === ORGANIZATION_JOIN_REQUEST.ACCEPTED)
|
||||
await tx.organizationUserJoinTable.create({
|
||||
data: {
|
||||
orgId,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
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,
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user