Compare commits
299
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76bd90b228 | ||
|
|
d908403a99 | ||
|
|
0fe775962f | ||
|
|
412a6992c7 | ||
|
|
cfd1992733 | ||
|
|
e86d6169ba | ||
|
|
a379bd2b52 | ||
|
|
1137047606 | ||
|
|
ad4cf0cc3a | ||
|
|
e7d2e0dc2f | ||
|
|
aa51218428 | ||
|
|
38bc5fb71e | ||
|
|
411516ddb9 | ||
|
|
c6c17ac388 | ||
|
|
6838133865 | ||
|
|
4b2bb2271a | ||
|
|
20b7ac8c0d | ||
|
|
ca7c432728 | ||
|
|
3b966191e3 | ||
|
|
a7889984a0 | ||
|
|
3c72d7aaa0 | ||
|
|
ece0030978 | ||
|
|
a26a769119 | ||
|
|
3313de1eec | ||
|
|
7a6e16631b | ||
|
|
ce4d71ba2e | ||
|
|
300cd31efb | ||
|
|
f5bb734441 | ||
|
|
e33560ef82 | ||
|
|
84f34b2244 | ||
|
|
086bae03d3 | ||
|
|
4a0a429774 | ||
|
|
c4142ab4e7 | ||
|
|
95f8046db9 | ||
|
|
d8a87e9f04 | ||
|
|
86dcd6f0ff | ||
|
|
411a14dd3d | ||
|
|
03bc8db64a | ||
|
|
fe7fd8bc79 | ||
|
|
92eb41c117 | ||
|
|
a1fca56aac | ||
|
|
f3630399ee | ||
|
|
5375c656af | ||
|
|
1792d49634 | ||
|
|
de98578fdd | ||
|
|
c0a59a2837 | ||
|
|
29201006d5 | ||
|
|
7a1c780088 | ||
|
|
550f34c128 | ||
|
|
d6b17b76db | ||
|
|
c42e803cfa | ||
|
|
0233c3a11a | ||
|
|
71a2b6956f | ||
|
|
8282557ebe | ||
|
|
083a162d0e | ||
|
|
60487ea02e | ||
|
|
60a8841439 | ||
|
|
184d1eb751 | ||
|
|
9fea30a44d | ||
|
|
04efcbcdb6 | ||
|
|
2b29b5607d | ||
|
|
20f19c2509 | ||
|
|
5d0e66763e | ||
|
|
bcfbc24e93 | ||
|
|
37d805b007 | ||
|
|
6a246e6c57 | ||
|
|
3a0229f60d | ||
|
|
0bf95ec568 | ||
|
|
6f05142447 | ||
|
|
e02108d535 | ||
|
|
b7e668217d | ||
|
|
472ffef5cf | ||
|
|
5acdb208f5 | ||
|
|
7bf5d9cf64 | ||
|
|
34a80180ef | ||
|
|
3bbba9c6cb | ||
|
|
b052c2e029 | ||
|
|
4eb884e2a6 | ||
|
|
3f8ce8ff50 | ||
|
|
67a2371c05 | ||
|
|
80250d32bd | ||
|
|
0fadbe1ce9 | ||
|
|
9c3ee262b4 | ||
|
|
b62eba8128 | ||
|
|
ef114d7727 | ||
|
|
be7d127c9e | ||
|
|
1209150785 | ||
|
|
31940b919e | ||
|
|
c3c990db43 | ||
|
|
84a742336c | ||
|
|
ada8ffd3b9 | ||
|
|
2277d90326 | ||
|
|
cbc27a78ee | ||
|
|
a2189643bd | ||
|
|
a6101b3da9 | ||
|
|
216fb2141b | ||
|
|
afb54ca048 | ||
|
|
f4b23b3a03 | ||
|
|
d6cd593b95 | ||
|
|
2d9c1b3fd9 | ||
|
|
f2f10a06d9 | ||
|
|
e1def884f3 | ||
|
|
fd61c00ad1 | ||
|
|
d8f422f98d | ||
|
|
7f200df1ac | ||
|
|
9b38ed65c0 | ||
|
|
25c20e5093 | ||
|
|
ec8a441649 | ||
|
|
04a91dc71a | ||
|
|
2023926a3a | ||
|
|
75465ac4bf | ||
|
|
2dbd0b3832 | ||
|
|
3b7d97b194 | ||
|
|
a1fab0e744 | ||
|
|
87331f0a3a | ||
|
|
6cd4f1fcaa | ||
|
|
6b84dc09a7 | ||
|
|
d125064430 | ||
|
|
39490b6883 | ||
|
|
e45f9c5a13 | ||
|
|
9ceb5ea3c5 | ||
|
|
16bd52148e | ||
|
|
9a2705c84c | ||
|
|
6c069a4afe | ||
|
|
42cbc46609 | ||
|
|
096026b92a | ||
|
|
060eafac60 | ||
|
|
f6f36750ec | ||
|
|
2e9aa01771 | ||
|
|
40d70cd1d4 | ||
|
|
17ef0dc12b | ||
|
|
e530ad5412 | ||
|
|
882eb85a33 | ||
|
|
6b0726bcde | ||
|
|
712467a7f4 | ||
|
|
0a9e732ea4 | ||
|
|
b101157d67 | ||
|
|
f859c289aa | ||
|
|
1009d2b4bc | ||
|
|
1739114490 | ||
|
|
9724bc07a6 | ||
|
|
24ff3622fd | ||
|
|
f1d1674693 | ||
|
|
c87a88b244 | ||
|
|
8bdcf4ec3b | ||
|
|
5f0188b036 | ||
|
|
bf3037d57e | ||
|
|
5ebd33576a | ||
|
|
4fb3639f71 | ||
|
|
d44a41c587 | ||
|
|
4960c4b97c | ||
|
|
5c51b0632f | ||
|
|
73b0693632 | ||
|
|
0bcb81bedd | ||
|
|
bfa23ddb80 | ||
|
|
28f473da14 | ||
|
|
1fd1202373 | ||
|
|
8845e736e5 | ||
|
|
dd16c2b99d | ||
|
|
e7779bba0a | ||
|
|
701c834115 | ||
|
|
18423e5805 | ||
|
|
b332d9897d | ||
|
|
191db5104a | ||
|
|
bfacd8a780 | ||
|
|
d4bff9d6b1 | ||
|
|
6defa1cd7d | ||
|
|
5b2b5fd967 | ||
|
|
6932498d48 | ||
|
|
e07f800417 | ||
|
|
464e5f1496 | ||
|
|
724e3f2755 | ||
|
|
59c76141ce | ||
|
|
a494428334 | ||
|
|
7145a2d640 | ||
|
|
baaa04151d | ||
|
|
381125becd | ||
|
|
091327afee | ||
|
|
f1d7d7f02a | ||
|
|
84bd4d3d38 | ||
|
|
0367cf8e41 | ||
|
|
afea724e4d | ||
|
|
a4915184cd | ||
|
|
b7a7797293 | ||
|
|
d0e59f1824 | ||
|
|
67befea66e | ||
|
|
f18ebc4d67 | ||
|
|
5324832f13 | ||
|
|
2d9162a4dd | ||
|
|
d9443162ef | ||
|
|
e8dbab99eb | ||
|
|
cdcc04c8ff | ||
|
|
740a1c991e | ||
|
|
70cfe8d693 | ||
|
|
520e711ca6 | ||
|
|
6d1fe32a39 | ||
|
|
1cc548284d | ||
|
|
d49843de0b | ||
|
|
42efd95476 | ||
|
|
2ec68ef1aa | ||
|
|
6e09489685 | ||
|
|
af07c411ee | ||
|
|
a22ca4758c | ||
|
|
bb3ba88f18 | ||
|
|
ce646c590f | ||
|
|
ba8cca55e8 | ||
|
|
ae738fae35 | ||
|
|
2922c8fe77 | ||
|
|
4599740637 | ||
|
|
8d17ca536e | ||
|
|
3fa7b30485 | ||
|
|
d9adb6c1fc | ||
|
|
f4944a05c9 | ||
|
|
6ba49a73df | ||
|
|
babeb1f31d | ||
|
|
13cbb59e5e | ||
|
|
8d2a072645 | ||
|
|
b2244503b1 | ||
|
|
9d67e1c473 | ||
|
|
489d56dad1 | ||
|
|
532c2f57a1 | ||
|
|
efca90507b | ||
|
|
28ff809739 | ||
|
|
8e73421094 | ||
|
|
5bab2af500 | ||
|
|
9b7c6585d8 | ||
|
|
58930c4c5e | ||
|
|
9412226a76 | ||
|
|
df12581879 | ||
|
|
0ec1638ca7 | ||
|
|
2516e18ebb | ||
|
|
d2f8076c70 | ||
|
|
cbfb11015e | ||
|
|
2d90d11e9e | ||
|
|
7ae0d6b7cc | ||
|
|
e0dc2ba03a | ||
|
|
aea6e105e6 | ||
|
|
a082cf73a0 | ||
|
|
61268f36b9 | ||
|
|
99cd0c5c41 | ||
|
|
2266301469 | ||
|
|
30b0012e6a | ||
|
|
77d1958c42 | ||
|
|
88f347f18c | ||
|
|
09cf8885cf | ||
|
|
f3d4eab951 | ||
|
|
866b6cc233 | ||
|
|
cafecad8ae | ||
|
|
0ea673e507 | ||
|
|
cf367c9340 | ||
|
|
d72e15649f | ||
|
|
89a3b77a1d | ||
|
|
38f6f71ae0 | ||
|
|
40caa82e55 | ||
|
|
b9aee3b18c | ||
|
|
3f8d2f2b38 | ||
|
|
c3731d9f17 | ||
|
|
6e4b2d8886 | ||
|
|
cf13c06c57 | ||
|
|
e309e72be3 | ||
|
|
b4d804de7c | ||
|
|
f501ef3269 | ||
|
|
79f1074f93 | ||
|
|
48aa6bf489 | ||
|
|
8ed2aacd9e | ||
|
|
dfc5d914fc | ||
|
|
0a11c45065 | ||
|
|
4ae5de5a3a | ||
|
|
e6959f3c7c | ||
|
|
67af8b4973 | ||
|
|
7794f2596e | ||
|
|
167cf6e352 | ||
|
|
3d778ba481 | ||
|
|
b8f0000b59 | ||
|
|
618ef63017 | ||
|
|
ae8e633716 | ||
|
|
4a23832388 | ||
|
|
02e486d757 | ||
|
|
af6e24f433 | ||
|
|
f9e24f51a7 | ||
|
|
9e9f815809 | ||
|
|
a91561f3c2 | ||
|
|
ff79064ca7 | ||
|
|
1a573e1619 | ||
|
|
511be7386c | ||
|
|
2a0991c259 | ||
|
|
2e454c9723 | ||
|
|
7e6d94c8e0 | ||
|
|
e18b790ccd | ||
|
|
0274417ad1 | ||
|
|
a8a3a93ace | ||
|
|
88bbab216e | ||
|
|
f74434a282 | ||
|
|
ed6f9d8252 | ||
|
|
a19ccab16a | ||
|
|
77369a13f1 | ||
|
|
8231356d2a | ||
|
|
469a942b5a | ||
|
|
0afe6cf5d1 |
@@ -0,0 +1,82 @@
|
|||||||
|
# Cal.com Development Guide for AI Agents
|
||||||
|
|
||||||
|
This directory contains comprehensive documentation for AI agents working on the Cal.com codebase.
|
||||||
|
|
||||||
|
## Quick Navigation
|
||||||
|
|
||||||
|
- **[Commands](commands.md)** - Build, test, and development commands
|
||||||
|
- **[Knowledge Base](knowledge-base.md)** - Knowledge base & best practices
|
||||||
|
- **[Architecture Overview](#architecture-overview)** - System structure and patterns
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
Cal.com is a monorepo using Yarn workspaces and Turbo for build orchestration. The main application is in `apps/web/` with shared packages in `packages/`.
|
||||||
|
|
||||||
|
### Key Directories
|
||||||
|
|
||||||
|
- `apps/web/` - Main Next.js application
|
||||||
|
- `packages/prisma/` - Database schema and migrations
|
||||||
|
- `packages/trpc/` - API layer using tRPC
|
||||||
|
- `packages/ui/` - Shared UI components
|
||||||
|
- `packages/features/` - Feature-specific code
|
||||||
|
- `packages/app-store/` - Third-party app integrations
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
### Database Layer
|
||||||
|
|
||||||
|
- **Prisma ORM** with PostgreSQL
|
||||||
|
- Schema in `packages/prisma/schema.prisma`
|
||||||
|
- Always use `select` instead of `include` for better performance
|
||||||
|
- Never expose `credential.key` field in API responses
|
||||||
|
|
||||||
|
### API Layer
|
||||||
|
|
||||||
|
- **tRPC** for type-safe APIs
|
||||||
|
- Routers in `packages/trpc/server/routers/`
|
||||||
|
- Authentication handled via NextAuth.js
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
- **Next.js 13+** with App Router in some areas
|
||||||
|
- **React 18** with TypeScript
|
||||||
|
- **Tailwind CSS** for styling
|
||||||
|
- Internationalization with `next-i18next`
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
- Use early returns to reduce nesting
|
||||||
|
- Throw descriptive errors with proper error codes
|
||||||
|
- Prefer composition over prop drilling
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- Avoid O(n²) logic in backend code
|
||||||
|
- Minimize Day.js usage in performance-critical paths
|
||||||
|
- Use `select` queries to only fetch needed data
|
||||||
|
- Consider using `.utc()` for Day.js operations
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Never commit secrets or API keys
|
||||||
|
- Always validate input data
|
||||||
|
- Use proper authentication checks
|
||||||
|
- Never expose sensitive credential fields
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
- **Unit tests** with Vitest
|
||||||
|
- **Integration tests** for complex workflows
|
||||||
|
- **E2E tests** with Playwright
|
||||||
|
- Test files use `.test.ts` or `.spec.ts` extensions
|
||||||
|
|
||||||
|
## Pull Request Guidelines
|
||||||
|
|
||||||
|
For large PRs (>500 lines or >10 files):
|
||||||
|
|
||||||
|
- Split by feature boundaries
|
||||||
|
- Separate database migrations, backend logic, frontend components
|
||||||
|
- Create dependency chains that can be merged sequentially
|
||||||
|
- Pattern: Database → Backend → Frontend → Tests
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Coding Standards & Best Practices
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Import Guidelines
|
||||||
|
|
||||||
|
### Type Imports
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Good - Use type imports for TypeScript types
|
||||||
|
import type { User } from "@prisma/client";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
|
// ❌ Bad - Regular import for types
|
||||||
|
import { User } from "@prisma/client";
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Code Structure
|
||||||
|
|
||||||
|
### Early Returns
|
||||||
|
|
||||||
|
- Prefer early returns to reduce nesting: `if (!booking) return null;`
|
||||||
|
|
||||||
|
### Composition Over Prop Drilling
|
||||||
|
|
||||||
|
- Use React children and context instead of passing props through multiple components
|
||||||
|
|
||||||
|
### Security Rules
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ NEVER expose credential keys
|
||||||
|
const user = await prisma.user.findFirst({
|
||||||
|
select: {
|
||||||
|
credentials: {
|
||||||
|
select: {
|
||||||
|
key: true, // ❌ NEVER do this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ Good - Never select credential.key field
|
||||||
|
const user = await prisma.user.findFirst({
|
||||||
|
select: {
|
||||||
|
credentials: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
type: true,
|
||||||
|
// key field is excluded for security
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# Build, Test & Development Commands
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
- `yarn dev` - Start development server for web app
|
||||||
|
- `yarn dev:all` - Start web, website, and console apps
|
||||||
|
- `yarn dev:api` - Start web app with API proxy and API
|
||||||
|
- `yarn dev:console` - Start web app with console
|
||||||
|
- `yarn dx` - Start development with database setup
|
||||||
|
|
||||||
|
## Build Commands
|
||||||
|
|
||||||
|
- `yarn build` - Build all packages and apps
|
||||||
|
- `yarn build:ai` - Build AI package specifically
|
||||||
|
- `yarn clean` - Remove build artifacts (node_modules, .next, .turbo, dist)
|
||||||
|
|
||||||
|
## Lint & Type Check
|
||||||
|
|
||||||
|
- `yarn lint` - Run ESLint on codebase
|
||||||
|
- `yarn lint:fix` - Run ESLint and fix issues
|
||||||
|
- `yarn lint:report` - Generate lint report
|
||||||
|
- `yarn type-check` - Run TypeScript type checking
|
||||||
|
- `yarn format` - Format code with Prettier
|
||||||
|
|
||||||
|
## Testing Commands
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
|
||||||
|
- `yarn test` - Run unit tests (vitest)
|
||||||
|
- `yarn test <filename>` - Run tests for specific file
|
||||||
|
- `yarn test <filename> -- --integrationTestsOnly` - Run integration tests for specific file
|
||||||
|
- `yarn test <filename> -t "<testName>" -- --integrationTestsOnly` - Run specific test by name
|
||||||
|
- `yarn tdd` - Run tests in watch mode
|
||||||
|
- `yarn test:ui` - Run tests with UI interface
|
||||||
|
|
||||||
|
### End-to-End Tests
|
||||||
|
|
||||||
|
- `yarn e2e` - Run end-to-end tests (Playwright)
|
||||||
|
- `yarn e2e <filename>` - Run E2E tests for specific file
|
||||||
|
- `yarn e2e <filename> --grep "<testName>"` - Run specific E2E test by name
|
||||||
|
- `yarn e2e:app-store` - Run app store E2E tests
|
||||||
|
- `yarn e2e:embed` - Run embed E2E tests
|
||||||
|
- `yarn test-e2e` - Run database seed + E2E tests
|
||||||
|
|
||||||
|
## Database Commands
|
||||||
|
|
||||||
|
- `yarn prisma` - Run Prisma CLI commands
|
||||||
|
- `yarn db-seed` - Seed database with test data
|
||||||
|
- `yarn db-deploy` - Deploy database migrations
|
||||||
|
- `yarn db-studio` - Open Prisma Studio
|
||||||
|
- `psql "postgresql://postgres:@localhost:5432/calendso"` - Connect to local PostgreSQL database
|
||||||
|
|
||||||
|
## App Store Commands
|
||||||
|
|
||||||
|
- `yarn create-app` - Create new app store integration
|
||||||
|
- `yarn edit-app` - Edit existing app
|
||||||
|
- `yarn delete-app` - Delete app
|
||||||
|
- `yarn app-store:build` - Build app store
|
||||||
|
- `yarn app-store:watch` - Watch app store for changes
|
||||||
|
|
||||||
|
## Useful Development Patterns
|
||||||
|
|
||||||
|
### Running Single Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Unit test specific file
|
||||||
|
yarn vitest run packages/lib/some-file.test.ts
|
||||||
|
|
||||||
|
# Integration test specific file
|
||||||
|
yarn test routing-form-response-denormalized.integration-test.ts -- --integrationTestsOnly
|
||||||
|
|
||||||
|
# E2E test specific file
|
||||||
|
yarn e2e tests/booking-flow.e2e.ts
|
||||||
|
|
||||||
|
# Run specific test by name
|
||||||
|
yarn e2e tests/booking-flow.e2e.ts --grep "should create booking"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Setup
|
||||||
|
|
||||||
|
- Copy `.env.example` to `.env` and configure
|
||||||
|
- Copy `.env.appStore.example` to `.env.appStore` for app store development
|
||||||
|
- Run `yarn dx` for initial development setup with database
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
# Knowledge Base - Product & Domain-Specific Information
|
||||||
|
|
||||||
|
## Repo note for calcom/cal.com
|
||||||
|
|
||||||
|
The whole thing is a monorepo. You need to be working in the apps/web folder.
|
||||||
|
|
||||||
|
Linting and Formatting
|
||||||
|
- Run lint with report generation: `yarn lint:report`
|
||||||
|
- Run type checking: `yarn type-check:ci --force`
|
||||||
|
- Run auto-fix: `yarn lint -- --fix`
|
||||||
|
|
||||||
|
Development
|
||||||
|
- Install dependencies: `yarn`
|
||||||
|
- Set up environment:
|
||||||
|
- Copy `.env.example` to `.env`
|
||||||
|
- Generate keys with `openssl rand -base64 32` for both `NEXTAUTH_SECRET` and `CALENDSO_ENCRYPTION_KEY`
|
||||||
|
- Configure Postgres database URL in `.env`
|
||||||
|
- Set DATABASE_DIRECT_URL to the same value as DATABASE_URL
|
||||||
|
|
||||||
|
Database Setup
|
||||||
|
- Development: `yarn workspace @calcom/prisma db-migrate`
|
||||||
|
- Production: `yarn workspace @calcom/prisma db-deploy`
|
||||||
|
|
||||||
|
When setting up local development database, it'll create a bunch of users for you. The passwords are the same as the username. e.g. 'free:free' and 'pro:pro'
|
||||||
|
|
||||||
|
PR Requirements
|
||||||
|
- PR title must follow Conventional Commits specification
|
||||||
|
- For most PRs, you only need to run linting and type checking
|
||||||
|
- E2E tests will only run if PR has "ready-for-e2e" label
|
||||||
|
|
||||||
|
Logging
|
||||||
|
- Control logging verbosity by setting `NEXT_PUBLIC_LOGGER_LEVEL` in .env:
|
||||||
|
- 0: silly
|
||||||
|
- 1: trace
|
||||||
|
- 2: debug
|
||||||
|
- 3: info
|
||||||
|
- 4: warn
|
||||||
|
- 5: error
|
||||||
|
- 6: fatal
|
||||||
|
|
||||||
|
## When addressing issues in the Cal.com repository
|
||||||
|
|
||||||
|
When working on the Cal.com repository, prioritize fixing type issues before addressing failing tests. Running `yarn type-check:ci --force` to identify and fix TypeScript errors should be done first, as these errors are often the root cause of test failures. Only after resolving type issues should you move on to fixing failing tests with `TZ=UTC yarn test`.
|
||||||
|
|
||||||
|
## When creating pull requests
|
||||||
|
|
||||||
|
Create pull requests in draft mode by default, so that actual human can mark it as ready for review only when it is.
|
||||||
|
|
||||||
|
## When developing Playwright tests in the Cal.com repository
|
||||||
|
|
||||||
|
Always ensure Playwright tests pass locally before pushing code. The user requires fast local e2e feedback loops instead of relying on CI, which is too slow for development iteration. Never push test code until those tests are passing locally first.
|
||||||
|
|
||||||
|
## When fixing failing tests in the Cal.com repository
|
||||||
|
|
||||||
|
When fixing failing tests in the Cal.com repository, take an incremental approach by addressing one file at a time rather than attempting to fix all issues simultaneously. This methodical approach makes it easier to identify and resolve specific issues without getting overwhelmed by the complexity of multiple failing tests across different files. Focus on getting each file's tests passing completely before moving on to the next file.
|
||||||
|
|
||||||
|
To identify and fix issues in the Cal.com codebase:
|
||||||
|
|
||||||
|
1. Run `yarn type-check:ci --force` to identify TypeScript type errors and get fresh results always, bypassing any caching issues
|
||||||
|
2. Run `yarn test` to identify failing unit tests
|
||||||
|
3. Address both type errors and failing tests before considering the task complete
|
||||||
|
4. Type errors often need to be fixed first as they may be causing the test failures
|
||||||
|
|
||||||
|
## When implementing mock services in the Cal.com test files
|
||||||
|
|
||||||
|
When mocking calendar services in Cal.com test files, implement the Calendar interface rather than adding individual properties from each specific calendar service type (like FeishuCalendarService). Since all calendar services implement the Calendar interface and are stored in a map, the mock service should also implement this interface to ensure type compatibility. This approach is more maintainable than trying to add all properties from each specific calendar service implementation. For complex mocks that cause type compatibility issues with deep mocks, consider using simpler fake implementations that directly implement the required interfaces instead of trying to match all properties of the original service classes. When needed, you can modify other mock files to support your implementation rather than trying to force compatibility with existing mocks.
|
||||||
|
|
||||||
|
## When implementing mocks for Cal.com app-store integrations
|
||||||
|
|
||||||
|
When mocking app-store resources in Cal.com tests, prefer implementing simpler mock designs that directly implement the required interfaces rather than trying to match complex deep mock structures created with mockDeep. This approach is more maintainable and helps resolve type compatibility issues. The user encourages creative solutions and refactoring to better designs when the standard mocking approach causes persistent type errors.
|
||||||
|
|
||||||
|
## When implementing workflow triggers in the Cal.com repository
|
||||||
|
|
||||||
|
To trigger workflows in Cal.com, use the `scheduleWorkflowReminders` function. This is the standard approach used throughout the codebase for triggering any workflow. Before implementing new workflow triggers, examine existing implementations in the codebase to understand the pattern. The function filters workflows by trigger type and processes each workflow step. Key locations where this is used include booking handlers, confirmation processes, and other booking-related events.
|
||||||
|
|
||||||
|
## When making changes to Prisma schema in Cal.com
|
||||||
|
|
||||||
|
After making changes to the Prisma schema in Cal.com and creating migrations, you need to run `yarn prisma generate` to update the TypeScript types. This is especially important when switching Node.js versions, after adding new fields to models, or after pulling changes that include Prisma schema updates, as it ensures the TypeScript compiler recognizes the updated schema structure. If you encounter errors related to missing enum values (like CreationSource.WEBAPP), running `yarn prisma generate` will typically resolve these issues by regenerating the TypeScript types from the schema.
|
||||||
|
|
||||||
|
If you encounter enum generator errors during the Prisma generate step (like "Cannot find module './enum-generator.ts'"), run `yarn install` first before trying to generate. This ensures all dependencies are properly installed before the generation process.
|
||||||
|
|
||||||
|
Whenever you change the schema.prisma file, remember to always consolidate migrations by squashing them as declared in the Prisma docs: https://www.prisma.io/docs/orm/prisma-migrate/workflows/squashing-migrations. This helps maintain a clean migration history and prevents accumulation of multiple migration files.
|
||||||
|
|
||||||
|
## When reviewing PRs in the Cal.com repository
|
||||||
|
|
||||||
|
When asked to review a PR, focus on providing a clear summary of what the PR is doing and its core functionality. Avoid getting sidetracked by CI failures, testing issues, or technical implementation details unless specifically requested. The user prefers concise, focused reviews that prioritize understanding the main purpose and changes of the PR.
|
||||||
|
|
||||||
|
## When running Playwright tests in the Cal.com repository
|
||||||
|
|
||||||
|
Use the command format `PLAYWRIGHT_HEADLESS=1 yarn e2e [test-file.e2e.ts]` to run Playwright tests instead of the standard `yarn playwright test` command. This format includes the proper timezone setting, virtual display server, and uses the repository's e2e runner.
|
||||||
|
|
||||||
|
## When running tests in the Cal.com repository
|
||||||
|
|
||||||
|
When running tests in the Cal.com repository, use the TZ=UTC environment variable (e.g., `TZ=UTC yarn test`) to ensure consistent timezone handling. This prevents timezone-related test failures that might occur when tests are run in different environments or by different developers with varying local timezone settings.
|
||||||
|
|
||||||
|
## When working on calendar cache features in the Cal.com codebase
|
||||||
|
|
||||||
|
The calendar cache system follows specific patterns in `packages/features/calendar-cache-sql`. When implementing provider-specific calendar cache services (like for Outlook/Office365), the provider-specific code should be placed in the corresponding provider directory (e.g., `packages/app-store/office365calendar`).
|
||||||
|
|
||||||
|
## When working on PRs in the Cal.com repository
|
||||||
|
|
||||||
|
When making changes to the Cal.com codebase, always run type checks locally using `yarn type-check:ci` before concluding that CI failures are unrelated to your changes. Even if errors appear in files you haven't directly modified, your changes might still be causing type issues through dependencies or type inference. Compare type check results between the main branch and your feature branch to confirm whether you've introduced new type errors.
|
||||||
|
|
||||||
|
## When working on type issues in the Cal.com repository
|
||||||
|
|
||||||
|
Type casting with "as any" is strictly forbidden in the Cal.com codebase. When encountering Prisma type incompatibilities or other TypeScript type issues, proper type-safe solutions must be used instead, such as Prisma extensions system, type parameter constraints, repository pattern isolation, explicit type definitions, and extension composition patterns that are already established in the codebase.
|
||||||
|
|
||||||
|
## When working on UI layout changes in the Cal.com web application
|
||||||
|
|
||||||
|
The event types page UI components are located in `apps/web/modules/event-types/views/event-types-listing-view.tsx`. This file contains the layout implementation for the search bar and team tabs components on the event types listing page.
|
||||||
|
|
||||||
|
Changes to shared UI patterns (like tab layouts and button alignments) need to be checked across multiple views to maintain consistency:
|
||||||
|
- Event types page layout: `apps/web/modules/event-types/views/event-types-listing-view.tsx`
|
||||||
|
- Bookings page layout: `apps/web/modules/bookings/views/bookings-listing-view.tsx`
|
||||||
|
- Common elements like tabs, search bars, and filter buttons should maintain consistent alignment across views
|
||||||
|
|
||||||
|
## When working on workflow triggers or similar enum-based features in the Cal.com codebase
|
||||||
|
|
||||||
|
To add new workflow triggers:
|
||||||
|
|
||||||
|
1. Check packages/prisma/schema.prisma for existing webhooks and workflow trigger enums as reference
|
||||||
|
2. Add the same enums to workflows (only when asked by user specifically, or else focus on users requirement)
|
||||||
|
3. Add enums to packages/features/ee/workflows/lib/constants.ts for UI display
|
||||||
|
4. Add translations to en/locale.json using the format {enum}_trigger (all lowercase). Webhook triggers serve as the reference implementation pattern for workflow triggers.
|
||||||
|
|
||||||
|
## When working with app-store integrations in Cal.com
|
||||||
|
|
||||||
|
The Cal.com repository uses generated files (*.generated.ts) for app-store integrations. These files are created by the app-store-cli tool. When making structural changes to how integrations are imported or used, you need to update CLI code that generates these files. Typically manual changes to the *.generated.ts files are not made. That was only for PR 19771 for a proof-of-concept. Recent changes have moved from dynamic imports to static map-based imports for better performance. When working with browser components in the app-store, static imports should be used rather than dynamic imports (using Next.js' dynamic function) to maintain consistency with the performance improvements.
|
||||||
|
|
||||||
|
When modifying the app-store-cli build.ts file, you must ensure it correctly handles all types of generated files:
|
||||||
|
1. Regular service files (calendar.services.generated.ts, crm.services.generated.ts, etc.) need default imports
|
||||||
|
2. Browser component files (apps.browser-addon.generated.tsx, etc.) may require dynamic imports with Next.js
|
||||||
|
3. After making changes to build.ts, always verify all generated files have the correct imports by checking each file type
|
||||||
|
|
||||||
|
The lazyImport parameter in getExportedObject() determines whether to use dynamic imports (for browser components) or static imports (for server-side services).
|
||||||
|
|
||||||
|
## When working with branches in the Cal.com repository
|
||||||
|
|
||||||
|
When asked to move changes to a different branch in the Cal.com repository, use git commands to commit existing changes to the specified branch rather than redoing the work. This is more efficient and prevents duplication of effort. The user prefers direct branch operations over reimplementing the same changes multiple times.
|
||||||
|
|
||||||
|
## When working with calendar events in the Cal.com codebase
|
||||||
|
|
||||||
|
Cal.com events in Google Calendar can be identified by checking if the iCalUID ends with "@Cal.com" (e.g., "2GBXSdEixretciJfKVmYN8@Cal.com"). This identifier is used to distinguish Cal.com bookings from other calendar events for data storage and privacy purposes.
|
||||||
|
|
||||||
|
## When working with CI/CD in the Cal.com repository
|
||||||
|
|
||||||
|
When reviewing CI check failures in Cal.com:
|
||||||
|
1. E2E tests can be flaky and may fail intermittently
|
||||||
|
2. Focus only on CI failures that are directly related to your code changes
|
||||||
|
3. Infrastructure-related failures (like dependency installation issues) can be disregarded if all code-specific checks (type checking, linting, unit tests) are passing
|
||||||
|
|
||||||
|
## When working with database models in Cal.com
|
||||||
|
|
||||||
|
Database models in Cal.com are defined in `packages/prisma/schema.prisma`. When adding new fields to models:
|
||||||
|
|
||||||
|
1. For timestamp fields like `createdAt` and `updatedAt`:
|
||||||
|
- Do not set default values if you want existing records to have null values
|
||||||
|
- Only new records should get timestamps automatically
|
||||||
|
- For `updatedAt` fields, ensure they're updated when records are modified
|
||||||
|
|
||||||
|
2. Create a migration using `npx prisma migrate dev --name migration_name` to update the database schema
|
||||||
|
|
||||||
|
3. When implementing cache-related features that require timestamp tracking, always update the database schema first before modifying application code that references those fields. The schema changes must be completed and migrated before the application can successfully query or use the new fields.
|
||||||
|
|
||||||
|
## When working with generated files in the Cal.com repository
|
||||||
|
|
||||||
|
When fixing imports in Cal.com's generated files (like packages/app-store/apps.browser-*.generated.tsx), always check the actual exports in the source files first. For EventTypeAppCardInterface components, they likely use named exports rather than default exports, requiring imports like `import * as ComponentName from "./path"` instead of `import ComponentName from "./path"`. This verification step is crucial when working with browser-addon, browser-appsettings, browser-eventtypesettings, and browser-install generated files. For these files, if you're seeing import errors, check whether the components are exported as default exports in their source files and adjust your import statements accordingly.
|
||||||
|
|
||||||
|
## When working with git and CI systems
|
||||||
|
|
||||||
|
Always push committed changes to the remote repository before waiting for or checking CI status. Waiting for CI checks on unpushed local commits is backwards - the CI runs on the remote repository state, not local commits. The proper sequence is: commit locally, run local checks, push to remote, then monitor CI status.
|
||||||
|
|
||||||
|
## When working with imports/exports in the Cal.com codebase
|
||||||
|
|
||||||
|
When working with imports in the Cal.com codebase, particularly in app-store integrations, pay attention to whether modules use named exports or default exports. Many services like VideoApiAdapter, CalendarService, and PaymentService are exported as named exports, but the actual export name may differ from the generic service type (e.g., `export class AppleCalendarService` instead of `export class CalendarService`). When importing these services, verify the actual export name in the source file and use the appropriate named import syntax (e.g., `import { AppleCalendarService } from "./applecalendar/lib/CalendarService"` or with renaming `import { AppleCalendarService as ApplecalendarCalendarService } from "./applecalendar/lib/CalendarService"`). Always check the actual export pattern and name in the source file to determine the correct import syntax.
|
||||||
|
|
||||||
|
## When working with tRPC components in the Cal.com repository
|
||||||
|
|
||||||
|
When making changes that affect tRPC components or after pulling updates that modify tRPC-related files, you need to follow a specific build order:
|
||||||
|
1. First run `yarn prisma generate` to ensure all database types are up-to-date
|
||||||
|
2. Then run `cd packages/trpc && yarn build` to rebuild the tRPC package
|
||||||
|
This sequence ensures that type definitions are properly generated before building the tRPC components that depend on them, preventing type errors during compilation.
|
||||||
|
|
||||||
|
## When working with workflows or webhooks in the Cal.com repository
|
||||||
|
|
||||||
|
Workflows and webhooks are two completely separate features in Cal.com with different implementations and file structures. The workflow constants file is located at `packages/features/ee/workflows/lib/constants.ts`, not in the webhooks directory. When working on workflow triggers, do not reference or use webhook trigger implementations - they are distinct systems that should not be confused or mixed.
|
||||||
|
|
||||||
|
## When adding new feature flags to the Cal.com repository
|
||||||
|
|
||||||
|
To seed new feature flags in Cal.com, create a Prisma migration using the command `yarn prisma migrate dev --create-only --name seed_[feature_name]_feature`. The migration file should be placed in `packages/prisma/migrations/` with a timestamp prefix format like `20250724210733_seed_calendar_cache_sql_features/migration.sql`. Follow the pattern from existing feature seeding migrations like `packages/prisma/migrations/20241216000000_add_calendar_cache_serve/migration.sql` for the SQL structure. The migration should INSERT the new features into the `Feature` table with appropriate type (like `OPERATIONAL`) and default `enabled` status for manual team enablement.
|
||||||
|
|
||||||
|
## When adding new UI elements or text strings to Cal.com
|
||||||
|
|
||||||
|
All UI strings in Cal.com must be properly translated using the i18n system. This includes:
|
||||||
|
- Labels for new UI elements (like dropdown labels, settings headers)
|
||||||
|
- Option values that are displayed to users
|
||||||
|
- Any text that appears in the interface
|
||||||
|
|
||||||
|
Even if some related strings are already translated (like "Bookings" and "Insights"), new strings must be explicitly added to the translation system.
|
||||||
|
|
||||||
|
## When creating a pull request
|
||||||
|
|
||||||
|
### Title
|
||||||
|
|
||||||
|
- Use conventional commits: `feat:`, `fix:`, `refactor:`
|
||||||
|
- Be specific: `fix: handle timezone edge case in booking creation`
|
||||||
|
- Not generic: `fix: booking bug`
|
||||||
|
|
||||||
|
### Size Limits
|
||||||
|
|
||||||
|
- **Large PRs** (>500 lines or >10 files) are not recommended.
|
||||||
|
- Guide the user how to split large PRs into smaller ones.
|
||||||
|
|
||||||
|
## When handling errors
|
||||||
|
|
||||||
|
### Descriptive Errors
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Good - Descriptive error with context
|
||||||
|
throw new Error(`Unable to create booking: User ${userId} has no available time slots for ${date}`);
|
||||||
|
|
||||||
|
// ❌ Bad - Generic error
|
||||||
|
throw new Error("Booking failed");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Good - Use proper error classes
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Invalid booking time slot",
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Performance Guidelines
|
||||||
|
|
||||||
|
- Aim for O(n) or O(n log n) complexity, avoid O(n²)
|
||||||
|
- Use database-level filtering instead of JavaScript filtering
|
||||||
|
- Consider pagination for large datasets
|
||||||
|
- Use database transactions for related operations
|
||||||
|
|
||||||
|
## When querying database
|
||||||
|
|
||||||
|
Prefer Select over Include:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Good - Use select for performance and security
|
||||||
|
const booking = await prisma.booking.findFirst({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ❌ Bad - Include fetches all fields
|
||||||
|
const booking = await prisma.booking.findFirst({
|
||||||
|
include: {
|
||||||
|
user: true, // This gets ALL user fields
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Avoid barrel imports
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ Bad - Avoid importing from index.ts barrel files
|
||||||
|
import { BookingService, UserService } from "./services";
|
||||||
|
|
||||||
|
// ✅ Good - Import directly from source files
|
||||||
|
import { BookingService } from "./services/BookingService";
|
||||||
|
import { UserService } from "./services/UserService";
|
||||||
|
|
||||||
|
|
||||||
|
// ❌ Bad
|
||||||
|
import { Button } from "@calcom/ui";
|
||||||
|
|
||||||
|
// ✅ Good - Import directly from source files
|
||||||
|
import { Button } from "@calcom/ui/components/button";
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Naming Conventions
|
||||||
|
|
||||||
|
### Repository Files
|
||||||
|
|
||||||
|
- **Must** include `Repository` suffix, PascalCase matching class: `PrismaBookingRepository.ts`
|
||||||
|
|
||||||
|
### Service Files
|
||||||
|
|
||||||
|
- **Must** include `Service` suffix, PascalCase matching class, avoid generic names: `MembershipService.ts`
|
||||||
|
|
||||||
|
### General Files
|
||||||
|
|
||||||
|
- **Components**: PascalCase (e.g., `BookingForm.tsx`)
|
||||||
|
- **Utilities**: kebab-case (e.g., `date-utils.ts`)
|
||||||
|
- **Types**: PascalCase with `.types.ts` suffix (e.g., `Booking.types.ts`)
|
||||||
|
- **Tests**: Same as source file + `.test.ts` or `.spec.ts`
|
||||||
|
- **Avoid**: Dot-suffixes like `.service.ts`, `.repository.ts` (except for tests, types, specs)
|
||||||
|
|
||||||
|
## When using Day.js
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ⚠️ Slow in performance-critical code (loops)
|
||||||
|
dates.map((date) => dayjs(date).add(1, "day").format());
|
||||||
|
|
||||||
|
// ✅ Better - Use .utc() for performance
|
||||||
|
dates.map((date) => dayjs.utc(date).add(1, "day").format());
|
||||||
|
|
||||||
|
// ✅ Best - Use native Date when possible
|
||||||
|
dates.map((date) => new Date(date.valueOf() + 24 * 60 * 60 * 1000));
|
||||||
|
```
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
language: en-US
|
||||||
|
tone_instructions: ''
|
||||||
|
early_access: false
|
||||||
|
enable_free_tier: true
|
||||||
|
reviews:
|
||||||
|
profile: chill
|
||||||
|
request_changes_workflow: false
|
||||||
|
high_level_summary: false
|
||||||
|
high_level_summary_placeholder: '@coderabbitai summary'
|
||||||
|
high_level_summary_in_walkthrough: false
|
||||||
|
auto_title_placeholder: '@coderabbitai'
|
||||||
|
auto_title_instructions: ''
|
||||||
|
review_status: true
|
||||||
|
commit_status: true
|
||||||
|
fail_commit_status: false
|
||||||
|
collapse_walkthrough: false
|
||||||
|
changed_files_summary: false
|
||||||
|
sequence_diagrams: false
|
||||||
|
estimate_code_review_effort: false
|
||||||
|
assess_linked_issues: true
|
||||||
|
related_issues: false
|
||||||
|
related_prs: true
|
||||||
|
suggested_labels: false
|
||||||
|
auto_apply_labels: false
|
||||||
|
suggested_reviewers: false
|
||||||
|
auto_assign_reviewers: false
|
||||||
|
poem: false
|
||||||
|
labeling_instructions: []
|
||||||
|
path_filters:
|
||||||
|
- '!**/node_modules/**'
|
||||||
|
- '!**/dist/**'
|
||||||
|
- '!**/build/**'
|
||||||
|
- '!**/.next/**'
|
||||||
|
path_instructions:
|
||||||
|
- path: "**/*.{ts,tsx,js,jsx}"
|
||||||
|
instructions: "Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module."
|
||||||
|
abort_on_close: true
|
||||||
|
disable_cache: false
|
||||||
|
auto_review:
|
||||||
|
enabled: true
|
||||||
|
auto_incremental_review: true
|
||||||
|
ignore_title_keywords: []
|
||||||
|
labels: []
|
||||||
|
drafts: false
|
||||||
|
base_branches: []
|
||||||
|
finishing_touches:
|
||||||
|
docstrings:
|
||||||
|
enabled: true
|
||||||
|
unit_tests:
|
||||||
|
enabled: true
|
||||||
|
pre_merge_checks:
|
||||||
|
docstrings:
|
||||||
|
mode: warning
|
||||||
|
threshold: 80
|
||||||
|
title:
|
||||||
|
mode: warning
|
||||||
|
requirements: ''
|
||||||
|
description:
|
||||||
|
mode: warning
|
||||||
|
issue_assessment:
|
||||||
|
mode: warning
|
||||||
|
tools:
|
||||||
|
ast-grep:
|
||||||
|
rule_dirs: []
|
||||||
|
util_dirs: []
|
||||||
|
essential_rules: true
|
||||||
|
packages: []
|
||||||
|
shellcheck:
|
||||||
|
enabled: true
|
||||||
|
ruff:
|
||||||
|
enabled: true
|
||||||
|
markdownlint:
|
||||||
|
enabled: true
|
||||||
|
github-checks:
|
||||||
|
enabled: true
|
||||||
|
timeout_ms: 180000
|
||||||
|
languagetool:
|
||||||
|
enabled: true
|
||||||
|
enabled_rules: []
|
||||||
|
disabled_rules: []
|
||||||
|
enabled_categories: []
|
||||||
|
disabled_categories: []
|
||||||
|
enabled_only: false
|
||||||
|
level: default
|
||||||
|
biome:
|
||||||
|
enabled: true
|
||||||
|
hadolint:
|
||||||
|
enabled: true
|
||||||
|
swiftlint:
|
||||||
|
enabled: true
|
||||||
|
phpstan:
|
||||||
|
enabled: true
|
||||||
|
level: default
|
||||||
|
phpmd:
|
||||||
|
enabled: true
|
||||||
|
phpcs:
|
||||||
|
enabled: true
|
||||||
|
golangci-lint:
|
||||||
|
enabled: true
|
||||||
|
yamllint:
|
||||||
|
enabled: true
|
||||||
|
gitleaks:
|
||||||
|
enabled: true
|
||||||
|
checkov:
|
||||||
|
enabled: true
|
||||||
|
detekt:
|
||||||
|
enabled: true
|
||||||
|
eslint:
|
||||||
|
enabled: true
|
||||||
|
flake8:
|
||||||
|
enabled: true
|
||||||
|
rubocop:
|
||||||
|
enabled: true
|
||||||
|
buf:
|
||||||
|
enabled: true
|
||||||
|
regal:
|
||||||
|
enabled: true
|
||||||
|
actionlint:
|
||||||
|
enabled: true
|
||||||
|
pmd:
|
||||||
|
enabled: true
|
||||||
|
cppcheck:
|
||||||
|
enabled: true
|
||||||
|
semgrep:
|
||||||
|
enabled: true
|
||||||
|
circleci:
|
||||||
|
enabled: true
|
||||||
|
clippy:
|
||||||
|
enabled: true
|
||||||
|
sqlfluff:
|
||||||
|
enabled: true
|
||||||
|
prismaLint:
|
||||||
|
enabled: true
|
||||||
|
pylint:
|
||||||
|
enabled: true
|
||||||
|
oxc:
|
||||||
|
enabled: true
|
||||||
|
shopifyThemeCheck:
|
||||||
|
enabled: true
|
||||||
|
luacheck:
|
||||||
|
enabled: true
|
||||||
|
brakeman:
|
||||||
|
enabled: true
|
||||||
|
dotenvLint:
|
||||||
|
enabled: true
|
||||||
|
htmlhint:
|
||||||
|
enabled: true
|
||||||
|
checkmake:
|
||||||
|
enabled: true
|
||||||
|
osvScanner:
|
||||||
|
enabled: true
|
||||||
|
chat:
|
||||||
|
art: true
|
||||||
|
auto_reply: true
|
||||||
|
integrations:
|
||||||
|
jira:
|
||||||
|
usage: auto
|
||||||
|
linear:
|
||||||
|
usage: auto
|
||||||
|
knowledge_base:
|
||||||
|
opt_out: false
|
||||||
|
web_search:
|
||||||
|
enabled: true
|
||||||
|
code_guidelines:
|
||||||
|
enabled: true
|
||||||
|
filePatterns: []
|
||||||
|
learnings:
|
||||||
|
scope: auto
|
||||||
|
issues:
|
||||||
|
scope: auto
|
||||||
|
jira:
|
||||||
|
usage: auto
|
||||||
|
project_keys: []
|
||||||
|
linear:
|
||||||
|
usage: auto
|
||||||
|
team_keys: []
|
||||||
|
pull_requests:
|
||||||
|
scope: auto
|
||||||
|
mcp:
|
||||||
|
usage: auto
|
||||||
|
disabled_servers: []
|
||||||
|
code_generation:
|
||||||
|
docstrings:
|
||||||
|
language: en-US
|
||||||
|
path_instructions: []
|
||||||
|
unit_tests:
|
||||||
|
path_instructions: []
|
||||||
+22
-9
@@ -103,19 +103,14 @@ NEXT_PUBLIC_INTERCOM_APP_ID=
|
|||||||
# Secret to enable Intercom Identity Verification
|
# Secret to enable Intercom Identity Verification
|
||||||
INTERCOM_SECRET=
|
INTERCOM_SECRET=
|
||||||
|
|
||||||
|
# Intercom api token for free users
|
||||||
|
INTERCOM_API_TOKEN=
|
||||||
|
|
||||||
# Posthog Config
|
# Posthog Config
|
||||||
NEXT_PUBLIC_POSTHOG_KEY=
|
NEXT_PUBLIC_POSTHOG_KEY=
|
||||||
|
|
||||||
NEXT_PUBLIC_POSTHOG_HOST=
|
NEXT_PUBLIC_POSTHOG_HOST=
|
||||||
|
|
||||||
# plain.com config
|
|
||||||
|
|
||||||
PLAIN_API_KEY=
|
|
||||||
PLAIN_API_URL=https://api.plain.com/v1
|
|
||||||
PLAIN_HMAC_SECRET_KEY=
|
|
||||||
NEXT_PUBLIC_PLAIN_CHAT_ID=
|
|
||||||
PLAIN_CHAT_HMAC_SECRET_KEY=
|
|
||||||
NEXT_PUBLIC_PLAIN_CHAT_EXCLUDED_PATHS=
|
|
||||||
|
|
||||||
|
|
||||||
# Dub Config
|
# Dub Config
|
||||||
@@ -383,9 +378,27 @@ TASKER_ENABLE_EMAILS=0
|
|||||||
# Ratelimiting via unkey
|
# Ratelimiting via unkey
|
||||||
UNKEY_ROOT_KEY=
|
UNKEY_ROOT_KEY=
|
||||||
|
|
||||||
# Used for Cal.ai Enterprise Voice AI Agents
|
# Used for Cal.ai Voice AI Agents
|
||||||
# https://retellai.com
|
# https://retellai.com
|
||||||
RETELL_AI_KEY=
|
RETELL_AI_KEY=
|
||||||
|
# Local testing overrides for Retell AI
|
||||||
|
RETELL_AI_TEST_MODE=false
|
||||||
|
# JSON mapping of local to production event type IDs (e.g. {"50": 747280} without any string)
|
||||||
|
RETELL_AI_TEST_EVENT_TYPE_MAP=
|
||||||
|
RETELL_AI_TEST_CAL_API_KEY=
|
||||||
|
|
||||||
|
# Used for buying phone number for cal ai voice agent
|
||||||
|
STRIPE_PHONE_NUMBER_MONTHLY_PRICE_ID=
|
||||||
|
|
||||||
|
|
||||||
|
CAL_AI_CALL_RATE_PER_MINUTE=0.29
|
||||||
|
|
||||||
|
|
||||||
|
STRIPE_WEBHOOK_SECRET_BILLING=
|
||||||
|
|
||||||
|
|
||||||
|
# Price for buying a phone number for cal.ai voice agent (Default is 5)
|
||||||
|
NEXT_PUBLIC_CAL_AI_PHONE_NUMBER_MONTHLY_PRICE=
|
||||||
|
|
||||||
# Used for the huddle01 integration
|
# Used for the huddle01 integration
|
||||||
HUDDLE01_API_TOKEN=
|
HUDDLE01_API_TOKEN=
|
||||||
|
|||||||
@@ -2,4 +2,34 @@
|
|||||||
/** @type {import("eslint").Linter.Config} */
|
/** @type {import("eslint").Linter.Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
extends: ["./packages/config/eslint-preset.js"],
|
extends: ["./packages/config/eslint-preset.js"],
|
||||||
|
plugins: ["import"],
|
||||||
|
rules: {
|
||||||
|
"import/no-cycle": ["warn", { maxDepth: Infinity }],
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ["packages/lib/**/*.{ts,tsx,js,jsx}", "packages/prisma/**/*.{ts,tsx,js,jsx}"],
|
||||||
|
rules: {
|
||||||
|
"no-restricted-imports": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
paths: ["@calcom/app-store"],
|
||||||
|
patterns: ["@calcom/app-store/*"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["packages/app-store/**/*.{ts,tsx,js,jsx}"],
|
||||||
|
rules: {
|
||||||
|
"no-restricted-imports": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
paths: ["@calcom/trpc"],
|
||||||
|
patterns: ["@calcom/trpc/*"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
+7
-2
@@ -2,7 +2,12 @@
|
|||||||
/apps/api/**/* @calcom/Platform @calcom/Foundation
|
/apps/api/**/* @calcom/Platform @calcom/Foundation
|
||||||
/apps/ui-playground/**/* @calcom/Consumer @calcom/Foundation
|
/apps/ui-playground/**/* @calcom/Consumer @calcom/Foundation
|
||||||
/apps/web/**/layout.tsx @calcom/Consumer @calcom/Foundation
|
/apps/web/**/layout.tsx @calcom/Consumer @calcom/Foundation
|
||||||
/apps/web/lib/**/* @calcom/Foundation
|
/apps/web/lib/csp.ts @calcom/Foundation
|
||||||
|
/apps/web/lib/buildNonce.ts @calcom/Foundation
|
||||||
|
/apps/web/lib/daily-webhook/**/* @calcom/Foundation
|
||||||
|
/apps/web/lib/core/**/* @calcom/Foundation
|
||||||
|
/apps/web/lib/booking.ts @calcom/Foundation
|
||||||
|
/apps/web/lib/handleOrgRedirect.ts @calcom/Foundation
|
||||||
/apps/web/middleware.ts @calcom/Foundation
|
/apps/web/middleware.ts @calcom/Foundation
|
||||||
/deploy/**/* @calcom/Foundation
|
/deploy/**/* @calcom/Foundation
|
||||||
/infra/**/* @calcom/Foundation
|
/infra/**/* @calcom/Foundation
|
||||||
@@ -25,7 +30,7 @@
|
|||||||
/packages/lib/slots.ts @calcom/Foundation
|
/packages/lib/slots.ts @calcom/Foundation
|
||||||
/packages/platform/atoms/**/*WebWrapper.tsx @calcom/Consumer
|
/packages/platform/atoms/**/*WebWrapper.tsx @calcom/Consumer
|
||||||
/packages/platform/**/* @calcom/Platform @calcom/Foundation
|
/packages/platform/**/* @calcom/Platform @calcom/Foundation
|
||||||
/packages/prisma/**/* @calcom/DBA
|
/packages/prisma/**/* @calcom/Foundation
|
||||||
/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts @calcom/Foundation
|
/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts @calcom/Foundation
|
||||||
/packages/trpc/server/routers/viewer/bookings/get.handler.ts @calcom/Foundation
|
/packages/trpc/server/routers/viewer/bookings/get.handler.ts @calcom/Foundation
|
||||||
/packages/trpc/server/routers/viewer/slots/**/* @calcom/Foundation
|
/packages/trpc/server/routers/viewer/slots/**/* @calcom/Foundation
|
||||||
|
|||||||
@@ -74,8 +74,14 @@ jobs:
|
|||||||
uses: ./.github/workflows/e2e-embed-react.yml
|
uses: ./.github/workflows/e2e-embed-react.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
|
e2e-atoms:
|
||||||
|
name: Tests
|
||||||
|
needs: [lint, build, build-atoms, build-api-v2]
|
||||||
|
uses: ./.github/workflows/e2e-atoms.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
required:
|
required:
|
||||||
needs: [lint, type-check, unit-test, integration-test, build, build-api-v1, build-api-v2, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
needs: [lint, type-check, unit-test, integration-test, build, build-api-v1, build-api-v2, e2e, e2e-embed, e2e-embed-react, e2e-app-store, e2e-atoms]
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
working-directory: docs
|
working-directory: docs
|
||||||
run: |
|
run: |
|
||||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||||
npm install -g mintlify@4.0.436
|
npm install -g mintlify@4.2.87
|
||||||
mintlify dev &
|
mintlify dev &
|
||||||
sleep 5 # Let it run for 5 seconds
|
sleep 5 # Let it run for 5 seconds
|
||||||
kill $!
|
kill $!
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
name: Atoms E2E Tests
|
name: E2E Atoms
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
workflow_call:
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- 'packages/platform/atoms/**'
|
|
||||||
- 'packages/platform/examples/base/**'
|
|
||||||
- 'apps/api/v2/**'
|
|
||||||
- '.github/workflows/atoms-e2e.yml'
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
actions: write
|
actions: write
|
||||||
contents: read
|
contents: read
|
||||||
@@ -25,9 +17,9 @@ env:
|
|||||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
atoms-e2e:
|
e2e-atoms:
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
name: Atoms E2E Tests
|
name: E2E Atoms
|
||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- uses: docker/login-action@v3
|
- uses: docker/login-action@v3
|
||||||
@@ -38,20 +30,20 @@ jobs:
|
|||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
- uses: ./.github/actions/yarn-playwright-install
|
- uses: ./.github/actions/yarn-playwright-install
|
||||||
- name: Run Atoms E2E Tests
|
- name: Run E2E Atoms Tests
|
||||||
working-directory: packages/platform/examples/base
|
working-directory: packages/platform/examples/base
|
||||||
run: yarn test:e2e
|
run: yarn test:e2e
|
||||||
- name: Upload Test Results
|
- name: Upload Test Results
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: atoms-e2e-test-results
|
name: e2e-atoms-test-results
|
||||||
path: test-results
|
path: test-results
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
- name: Upload Playwright Report
|
- name: Upload Playwright Report
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: atoms-e2e-playwright-report
|
name: e2e-atoms-playwright-report
|
||||||
path: playwright-report
|
path: playwright-report
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.EQUITY_BEE_TEAM_LABELER_ACTION_TOKEN }}
|
repo-token: ${{ secrets.EQUITY_BEE_TEAM_LABELER_ACTION_TOKEN }}
|
||||||
organization-name: calcom
|
organization-name: calcom
|
||||||
ignore-labels: "admin, app-store, ai, authentication, automated-testing, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, dba, devops, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier"
|
ignore-labels: "admin, app-store, ai, authentication, automated-testing, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, dba, devops, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, self-hosted, teams, webhooks, workflows, zapier"
|
||||||
apply-labels-from-issue:
|
apply-labels-from-issue:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
name: Performance Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [created]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
base_url:
|
||||||
|
description: 'Base URL for performance tests'
|
||||||
|
required: false
|
||||||
|
default: 'https://cal.dev'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
performance-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
BASE_URL: ${{ github.event.inputs.base_url || 'https://cal.dev' }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup K6
|
||||||
|
uses: grafana/setup-k6-action@v1
|
||||||
|
with:
|
||||||
|
k6-version: '1.0.0'
|
||||||
|
|
||||||
|
- name: Run Smoke Tests
|
||||||
|
uses: grafana/run-k6-action@v1
|
||||||
|
env:
|
||||||
|
K6_CLOUD_TOKEN: ${{ secrets.K6_CLOUD_TOKEN }}
|
||||||
|
K6_CLOUD_PROJECT_ID: ${{ secrets.K6_CLOUD_PROJECT_ID }}
|
||||||
|
with:
|
||||||
|
path: tests/performance/smoke/*.js
|
||||||
|
cloud-run-locally: true
|
||||||
|
fail-fast: true
|
||||||
|
|
||||||
|
- name: Run Load Tests
|
||||||
|
uses: grafana/run-k6-action@v1
|
||||||
|
env:
|
||||||
|
K6_CLOUD_TOKEN: ${{ secrets.K6_CLOUD_TOKEN }}
|
||||||
|
K6_CLOUD_PROJECT_ID: ${{ secrets.K6_CLOUD_PROJECT_ID }}
|
||||||
|
with:
|
||||||
|
path: tests/performance/load/*.js
|
||||||
|
cloud-run-locally: true
|
||||||
|
fail-fast: true
|
||||||
|
|
||||||
|
- name: Run Stress Tests
|
||||||
|
uses: grafana/run-k6-action@v1
|
||||||
|
env:
|
||||||
|
K6_CLOUD_TOKEN: ${{ secrets.K6_CLOUD_TOKEN }}
|
||||||
|
K6_CLOUD_PROJECT_ID: ${{ secrets.K6_CLOUD_PROJECT_ID }}
|
||||||
|
with:
|
||||||
|
path: tests/performance/stress/*.js
|
||||||
|
cloud-run-locally: true
|
||||||
|
fail-fast: true
|
||||||
|
|
||||||
|
- name: Run Spike Tests
|
||||||
|
uses: grafana/run-k6-action@v1
|
||||||
|
env:
|
||||||
|
K6_CLOUD_TOKEN: ${{ secrets.K6_CLOUD_TOKEN }}
|
||||||
|
K6_CLOUD_PROJECT_ID: ${{ secrets.K6_CLOUD_PROJECT_ID }}
|
||||||
|
with:
|
||||||
|
path: tests/performance/spike/*.js
|
||||||
|
cloud-run-locally: true
|
||||||
|
fail-fast: true
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
filters: |
|
filters: |
|
||||||
has-files-requiring-all-checks:
|
has-files-requiring-all-checks:
|
||||||
- "!(**.md|.github/CODEOWNERS|docs/**|help/**|apps/web/public/static/locales/**/common.json)"
|
- "!(**.md|**.mdx|.github/CODEOWNERS|docs/**|help/**|apps/web/public/static/locales/**/common.json|i18n.lock)"
|
||||||
- name: Get Latest Commit SHA
|
- name: Get Latest Commit SHA
|
||||||
id: get_sha
|
id: get_sha
|
||||||
run: |
|
run: |
|
||||||
@@ -195,6 +195,13 @@ jobs:
|
|||||||
uses: ./.github/workflows/e2e-embed-react.yml
|
uses: ./.github/workflows/e2e-embed-react.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
|
e2e-atoms:
|
||||||
|
name: Tests
|
||||||
|
needs: [changes, check-label, build, build-atoms, build-api-v2]
|
||||||
|
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
|
uses: ./.github/workflows/e2e-atoms.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
analyze:
|
analyze:
|
||||||
name: Analyze Build
|
name: Analyze Build
|
||||||
needs: [build]
|
needs: [build]
|
||||||
@@ -204,7 +211,7 @@ jobs:
|
|||||||
merge-reports:
|
merge-reports:
|
||||||
name: Merge reports
|
name: Merge reports
|
||||||
if: ${{ !cancelled() && needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
if: ${{ !cancelled() && needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
needs: [changes, check-label, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
needs: [changes, check-label, e2e, e2e-embed, e2e-embed-react, e2e-app-store, e2e-atoms]
|
||||||
uses: ./.github/workflows/merge-reports.yml
|
uses: ./.github/workflows/merge-reports.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
@@ -249,6 +256,7 @@ jobs:
|
|||||||
e2e-embed,
|
e2e-embed,
|
||||||
e2e-embed-react,
|
e2e-embed-react,
|
||||||
e2e-app-store,
|
e2e-app-store,
|
||||||
|
e2e-atoms,
|
||||||
]
|
]
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||||
|
|||||||
@@ -97,3 +97,7 @@ apps/auth
|
|||||||
!.yarn/sdks
|
!.yarn/sdks
|
||||||
!.yarn/versions
|
!.yarn/versions
|
||||||
i18n.cache
|
i18n.cache
|
||||||
|
packages/**/.yarn/ci-cache/
|
||||||
|
|
||||||
|
# AI
|
||||||
|
.claude
|
||||||
|
|||||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
Vendored
Vendored
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
Vendored
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
|
- "@calcom/prisma"
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Cal.com Development Guide for AI Agents
|
||||||
|
|
||||||
|
**📖 Complete documentation is in the [.agents/](.agents/) directory.**
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### Essential Commands
|
||||||
|
|
||||||
|
- `yarn dev` - Start development server
|
||||||
|
- `yarn build` - Build all packages
|
||||||
|
- `yarn lint:fix` - Lint and fix code
|
||||||
|
- `yarn type-check` - TypeScript checking
|
||||||
|
- `yarn test <filename> -- --integrationTestsOnly` - Run integration tests
|
||||||
|
- `yarn e2e <filename> --grep "<testName>"` - Run specific E2E test
|
||||||
|
|
||||||
|
## 📚 Detailed Documentation
|
||||||
|
|
||||||
|
- **[.agents/README.md](.agents/README.md)** - Complete development guide
|
||||||
|
- **[.agents/commands.md](.agents/commands.md)** - All build, test & dev commands
|
||||||
|
- **[.agents/knowledge-base.md](.agents/knowledge-base.md)** - Knowledge base & best practices
|
||||||
@@ -199,7 +199,7 @@ There's some settings that you'll need to setup.
|
|||||||
Under Vercel > Your API Project > Settings
|
Under Vercel > Your API Project > Settings
|
||||||
|
|
||||||
In General > Build & Development Settings
|
In General > Build & Development Settings
|
||||||
BUILD COMMAND: `yarn turbo run build --scope=@calcom/api --include-dependencies --no-deps`
|
BUILD COMMAND: `yarn turbo run build --filter=@calcom/api --no-deps`
|
||||||
OUTPUT DIRECTORY: `apps/api/.next`
|
OUTPUT DIRECTORY: `apps/api/.next`
|
||||||
|
|
||||||
In Git > Ignored Build Step
|
In Git > Ignored Build Step
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export const bookingCancelSchema = z.object({
|
|||||||
id: z.number(),
|
id: z.number(),
|
||||||
allRemainingBookings: z.boolean().optional(),
|
allRemainingBookings: z.boolean().optional(),
|
||||||
cancelSubsequentBookings: z.boolean().optional(),
|
cancelSubsequentBookings: z.boolean().optional(),
|
||||||
cancellationReason: z.string().optional().default("Not Provided"),
|
cancellationReason: z.string().min(1).optional(),
|
||||||
seatReferenceUid: z.string().optional(),
|
seatReferenceUid: z.string().optional(),
|
||||||
cancelledBy: z.string().email({ message: "Invalid email" }).optional(),
|
cancelledBy: z.string().email({ message: "Invalid email" }).optional(),
|
||||||
internalNote: z
|
internalNote: z
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import type { NextApiRequest } from "next";
|
|||||||
import { OAuth2UniversalSchema } from "@calcom/app-store/_utils/oauth/universalSchema";
|
import { OAuth2UniversalSchema } from "@calcom/app-store/_utils/oauth/universalSchema";
|
||||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||||
import prisma from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
|
import type { Prisma } from "@calcom/prisma/client";
|
||||||
|
|
||||||
import { schemaCredentialPatchParams, schemaCredentialPatchBody } from "~/lib/validations/credential-sync";
|
import { schemaCredentialPatchParams, schemaCredentialPatchBody } from "~/lib/validations/credential-sync";
|
||||||
|
|
||||||
@@ -71,7 +72,7 @@ async function handler(req: NextApiRequest) {
|
|||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
key,
|
key: key as unknown as Prisma.InputJsonValue,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
|||||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||||
import prisma from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
|
import type { Prisma } from "@calcom/prisma/client";
|
||||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||||
|
|
||||||
import { schemaCredentialPostBody, schemaCredentialPostParams } from "~/lib/validations/credential-sync";
|
import { schemaCredentialPostBody, schemaCredentialPostParams } from "~/lib/validations/credential-sync";
|
||||||
@@ -92,7 +93,7 @@ async function handler(req: NextApiRequest) {
|
|||||||
data: {
|
data: {
|
||||||
userId,
|
userId,
|
||||||
appId: appSlug,
|
appId: appSlug,
|
||||||
key,
|
key: key as unknown as Prisma.InputJsonValue,
|
||||||
type: appMetadata.type,
|
type: appMetadata.type,
|
||||||
},
|
},
|
||||||
select: credentialForCalendarServiceSelect,
|
select: credentialForCalendarServiceSelect,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { NextApiRequest } from "next";
|
|||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { MembershipRole } from "@calcom/prisma/client";
|
import { MembershipRole } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
import { schemaEventTypeCreateBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
import { schemaEventTypeCreateBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
||||||
import { canUserAccessTeamWithRole } from "~/pages/api/teams/[teamId]/_auth-middleware";
|
import { canUserAccessTeamWithRole } from "~/pages/api/teams/[teamId]/_auth-middleware";
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ describe("POST /api/attendees", () => {
|
|||||||
|
|
||||||
const userBooking = { id: 1 };
|
const userBooking = { id: 1 };
|
||||||
|
|
||||||
prismaMock.booking.findFirst.mockResolvedValue(userBooking);
|
prismaMock.booking.findFirst.mockResolvedValue(userBooking as any);
|
||||||
|
|
||||||
const attendeeData = {
|
const attendeeData = {
|
||||||
id: 1,
|
id: 1,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ beforeEach(() => {
|
|||||||
id: userId,
|
id: userId,
|
||||||
email: "test@example.com",
|
email: "test@example.com",
|
||||||
name: "Test User",
|
name: "Test User",
|
||||||
});
|
} as any);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ beforeEach(() => {
|
|||||||
id: userId,
|
id: userId,
|
||||||
email: "test@example.com",
|
email: "test@example.com",
|
||||||
name: "Test User",
|
name: "Test User",
|
||||||
});
|
} as any);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@@ -30,12 +30,13 @@ beforeEach(() => {
|
|||||||
id: userId,
|
id: userId,
|
||||||
email: "test@example.com",
|
email: "test@example.com",
|
||||||
name: "Test User",
|
name: "Test User",
|
||||||
});
|
} as any);
|
||||||
(getAccessibleUsers as any).mockResolvedValue([userId]);
|
(getAccessibleUsers as any).mockResolvedValue([userId]);
|
||||||
(retrieveOrgScopedAccessibleUsers as any).mockResolvedValue([userId]);
|
(retrieveOrgScopedAccessibleUsers as any).mockResolvedValue([userId]);
|
||||||
|
|
||||||
prismaMock.membership.findMany.mockResolvedValue([
|
prismaMock.membership.findMany.mockResolvedValue([
|
||||||
{
|
{
|
||||||
|
// @ts-expect-error Will be fixed by Prisma 6.7.0 upgrade mock changes - which uses vitest-mock-extended
|
||||||
team: {
|
team: {
|
||||||
id: 1,
|
id: 1,
|
||||||
isOrganization: true,
|
isOrganization: true,
|
||||||
@@ -48,7 +49,7 @@ beforeEach(() => {
|
|||||||
id: userId,
|
id: userId,
|
||||||
email: "test@example.com",
|
email: "test@example.com",
|
||||||
},
|
},
|
||||||
]);
|
] as any);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import { getEventTypesFromDB } from "@calcom/features/bookings/lib/handleNewBook
|
|||||||
import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
|
import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
|
||||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||||
import { buildBooking, buildEventType, buildWebhook, buildUser } from "@calcom/lib/test/builder";
|
import { buildBooking, buildEventType, buildWebhook, buildUser } from "@calcom/lib/test/builder";
|
||||||
import prisma from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
import type { Booking } from "@calcom/prisma/client";
|
import type { Booking } from "@calcom/prisma/client";
|
||||||
import { CreationSource } from "@calcom/prisma/enums";
|
import { CreationSource, BookingStatus } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
import handler from "../../../pages/api/bookings/_post";
|
import handler from "../../../pages/api/bookings/_post";
|
||||||
|
|
||||||
@@ -303,6 +303,7 @@ describe("POST /api/bookings", () => {
|
|||||||
|
|
||||||
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
||||||
...buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } }),
|
...buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } }),
|
||||||
|
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
|
||||||
profile: { organizationId: null },
|
profile: { organizationId: null },
|
||||||
hosts: [],
|
hosts: [],
|
||||||
users: [buildUser()],
|
users: [buildUser()],
|
||||||
@@ -340,6 +341,7 @@ describe("POST /api/bookings", () => {
|
|||||||
|
|
||||||
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
||||||
...buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } }),
|
...buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } }),
|
||||||
|
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
|
||||||
profile: { organizationId: null },
|
profile: { organizationId: null },
|
||||||
hosts: [],
|
hosts: [],
|
||||||
users: [buildUser()],
|
users: [buildUser()],
|
||||||
@@ -376,6 +378,7 @@ describe("POST /api/bookings", () => {
|
|||||||
|
|
||||||
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
||||||
...buildEventType({ profileId: null }),
|
...buildEventType({ profileId: null }),
|
||||||
|
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
|
||||||
profile: { organizationId: null },
|
profile: { organizationId: null },
|
||||||
hosts: [],
|
hosts: [],
|
||||||
users: [buildUser()],
|
users: [buildUser()],
|
||||||
@@ -417,6 +420,7 @@ describe("POST /api/bookings", () => {
|
|||||||
|
|
||||||
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
||||||
...buildEventType({ profileId: null, length: 15 }),
|
...buildEventType({ profileId: null, length: 15 }),
|
||||||
|
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
|
||||||
profile: { organizationId: null },
|
profile: { organizationId: null },
|
||||||
hosts: [],
|
hosts: [],
|
||||||
users: [buildUser()],
|
users: [buildUser()],
|
||||||
@@ -468,7 +472,7 @@ describe("POST /api/bookings", () => {
|
|||||||
|
|
||||||
prismaMock.booking.findUnique.mockResolvedValue({
|
prismaMock.booking.findUnique.mockResolvedValue({
|
||||||
...originalBooking,
|
...originalBooking,
|
||||||
status: "cancelled",
|
status: "CANCELLED",
|
||||||
});
|
});
|
||||||
|
|
||||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
@@ -492,6 +496,7 @@ describe("POST /api/bookings", () => {
|
|||||||
|
|
||||||
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
||||||
...buildEventType({ profileId: null, length: 15 }),
|
...buildEventType({ profileId: null, length: 15 }),
|
||||||
|
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
|
||||||
profile: { organizationId: null },
|
profile: { organizationId: null },
|
||||||
hosts: [],
|
hosts: [],
|
||||||
users: [buildUser()],
|
users: [buildUser()],
|
||||||
@@ -532,7 +537,7 @@ describe("POST /api/bookings", () => {
|
|||||||
const previousBooking = await prisma.booking.findUnique({
|
const previousBooking = await prisma.booking.findUnique({
|
||||||
where: { uid: "original-booking-uid" },
|
where: { uid: "original-booking-uid" },
|
||||||
});
|
});
|
||||||
expect(previousBooking?.status).toBe("cancelled");
|
expect(previousBooking?.status).toBe(BookingStatus.CANCELLED);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Creates source as api_v1", async () => {
|
test("Creates source as api_v1", async () => {
|
||||||
@@ -556,6 +561,7 @@ describe("POST /api/bookings", () => {
|
|||||||
|
|
||||||
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
||||||
...buildEventType({ profileId: null, length: 15 }),
|
...buildEventType({ profileId: null, length: 15 }),
|
||||||
|
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
|
||||||
profile: { organizationId: null },
|
profile: { organizationId: null },
|
||||||
hosts: [],
|
hosts: [],
|
||||||
users: [buildUser()],
|
users: [buildUser()],
|
||||||
@@ -627,6 +633,7 @@ describe("POST /api/bookings", () => {
|
|||||||
|
|
||||||
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
||||||
...buildEventType({ profileId: null, length: 15 }),
|
...buildEventType({ profileId: null, length: 15 }),
|
||||||
|
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
|
||||||
profile: { organizationId: null },
|
profile: { organizationId: null },
|
||||||
hosts: [],
|
hosts: [],
|
||||||
users: [buildUser()],
|
users: [buildUser()],
|
||||||
@@ -684,6 +691,7 @@ describe("POST /api/bookings", () => {
|
|||||||
|
|
||||||
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
||||||
...buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } }),
|
...buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } }),
|
||||||
|
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
|
||||||
profile: { organizationId: null },
|
profile: { organizationId: null },
|
||||||
hosts: [],
|
hosts: [],
|
||||||
users: [buildUser()],
|
users: [buildUser()],
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ describe("DELETE /api/event-types/[id]", () => {
|
|||||||
// Mocking team.findUnique
|
// Mocking team.findUnique
|
||||||
prismaMock.team.findUnique.mockResolvedValue({
|
prismaMock.team.findUnique.mockResolvedValue({
|
||||||
id: teamId,
|
id: teamId,
|
||||||
|
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
|
||||||
members: [
|
members: [
|
||||||
{ userId: memberUser, role: MembershipRole.MEMBER, teamId: teamId },
|
{ userId: memberUser, role: MembershipRole.MEMBER, teamId: teamId },
|
||||||
{ userId: adminUser, role: MembershipRole.ADMIN, teamId: teamId },
|
{ userId: adminUser, role: MembershipRole.ADMIN, teamId: teamId },
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ describe("GET /api/event-types/[id]", () => {
|
|||||||
|
|
||||||
prismaMock.team.findFirst.mockResolvedValue({
|
prismaMock.team.findFirst.mockResolvedValue({
|
||||||
id: teamId,
|
id: teamId,
|
||||||
|
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
|
||||||
members: [
|
members: [
|
||||||
{
|
{
|
||||||
userId,
|
userId,
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"functions": {
|
|
||||||
"pages/api/slots/*.ts": {
|
|
||||||
"memory": 512
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
"@axiomhq/winston": "^1.2.0",
|
"@axiomhq/winston": "^1.2.0",
|
||||||
"@calcom/platform-constants": "*",
|
"@calcom/platform-constants": "*",
|
||||||
"@calcom/platform-enums": "*",
|
"@calcom/platform-enums": "*",
|
||||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.312",
|
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.349",
|
||||||
"@calcom/platform-types": "*",
|
"@calcom/platform-types": "*",
|
||||||
"@calcom/platform-utils": "*",
|
"@calcom/platform-utils": "*",
|
||||||
"@calcom/prisma": "*",
|
"@calcom/prisma": "*",
|
||||||
@@ -55,9 +55,9 @@
|
|||||||
"@nestjs/platform-express": "^10.0.0",
|
"@nestjs/platform-express": "^10.0.0",
|
||||||
"@nestjs/swagger": "^7.3.0",
|
"@nestjs/swagger": "^7.3.0",
|
||||||
"@nestjs/throttler": "6.2.1",
|
"@nestjs/throttler": "6.2.1",
|
||||||
"@sentry/nestjs": "^8.37.1",
|
"@sentry/nestjs": "^9.15.0",
|
||||||
"@sentry/node": "^8.8.0",
|
"@sentry/node": "^9.15.0",
|
||||||
"@sentry/profiling-node": "^8.37.1",
|
"@sentry/profiling-node": "^9.15.0",
|
||||||
"@snyk/protect": "latest",
|
"@snyk/protect": "latest",
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
"bull": "^4.12.4",
|
"bull": "^4.12.4",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { TestingModule } from "@nestjs/testing";
|
|||||||
import { Test } from "@nestjs/testing";
|
import { Test } from "@nestjs/testing";
|
||||||
import * as request from "supertest";
|
import * as request from "supertest";
|
||||||
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
||||||
|
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||||
@@ -33,6 +34,7 @@ describe("AppController", () => {
|
|||||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||||
|
let membershipRepositoryFixture: MembershipRepositoryFixture;
|
||||||
|
|
||||||
let apiKeyString: string;
|
let apiKeyString: string;
|
||||||
|
|
||||||
@@ -108,6 +110,14 @@ describe("AppController", () => {
|
|||||||
organization: { connect: { id: organization.id } },
|
organization: { connect: { id: organization.id } },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||||
|
await membershipRepositoryFixture.create({
|
||||||
|
user: { connect: { id: user.id } },
|
||||||
|
team: { connect: { id: organization.id } },
|
||||||
|
role: "OWNER",
|
||||||
|
accepted: true,
|
||||||
|
});
|
||||||
|
|
||||||
app = moduleRef.createNestApplication();
|
app = moduleRef.createNestApplication();
|
||||||
await app.init();
|
await app.init();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { BookingsController_2024_04_15 } from "@/ee/bookings/2024-04-15/controllers/bookings.controller";
|
import { BookingsController_2024_04_15 } from "@/ee/bookings/2024-04-15/controllers/bookings.controller";
|
||||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||||
|
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
|
||||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||||
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
|
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
|
||||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||||
@@ -41,6 +42,7 @@ import { Module } from "@nestjs/common";
|
|||||||
OAuthClientUsersService,
|
OAuthClientUsersService,
|
||||||
PlatformBookingsService,
|
PlatformBookingsService,
|
||||||
CalendarsService,
|
CalendarsService,
|
||||||
|
CalendarsCacheService,
|
||||||
CredentialsRepository,
|
CredentialsRepository,
|
||||||
AppsRepository,
|
AppsRepository,
|
||||||
CalendarsRepository,
|
CalendarsRepository,
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ import {
|
|||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { ApiQuery, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
import { ApiQuery, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
||||||
import { CreationSource } from "@prisma/client";
|
|
||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
import { NextApiRequest } from "next/types";
|
import { NextApiRequest } from "next/types";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
@@ -58,6 +57,7 @@ import {
|
|||||||
getBookingForReschedule,
|
getBookingForReschedule,
|
||||||
ErrorCode,
|
ErrorCode,
|
||||||
} from "@calcom/platform-libraries";
|
} from "@calcom/platform-libraries";
|
||||||
|
import { CreationSource } from "@calcom/platform-libraries";
|
||||||
import {
|
import {
|
||||||
GetBookingsInput_2024_04_15,
|
GetBookingsInput_2024_04_15,
|
||||||
CancelBookingInput_2024_04_15,
|
CancelBookingInput_2024_04_15,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { OutputBookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08
|
|||||||
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
|
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
|
||||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||||
|
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
|
||||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||||
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
|
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
|
||||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||||
@@ -22,6 +23,7 @@ import { BookingSeatModule } from "@/modules/booking-seat/booking-seat.module";
|
|||||||
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
||||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
import { KyselyModule } from "@/modules/kysely/kysely.module";
|
import { KyselyModule } from "@/modules/kysely/kysely.module";
|
||||||
|
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||||
@@ -53,6 +55,7 @@ import { Module } from "@nestjs/common";
|
|||||||
StripeModule,
|
StripeModule,
|
||||||
TeamsModule,
|
TeamsModule,
|
||||||
TeamsEventTypesModule,
|
TeamsEventTypesModule,
|
||||||
|
MembershipsModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
TokensRepository,
|
TokensRepository,
|
||||||
@@ -69,6 +72,7 @@ import { Module } from "@nestjs/common";
|
|||||||
ApiKeysRepository,
|
ApiKeysRepository,
|
||||||
PlatformBookingsService,
|
PlatformBookingsService,
|
||||||
CalendarsService,
|
CalendarsService,
|
||||||
|
CalendarsCacheService,
|
||||||
CredentialsRepository,
|
CredentialsRepository,
|
||||||
AppsRepository,
|
AppsRepository,
|
||||||
CalendarsRepository,
|
CalendarsRepository,
|
||||||
|
|||||||
@@ -13,10 +13,14 @@ import { CalVideoService } from "@/ee/bookings/2024-08-13/services/cal-video.ser
|
|||||||
import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions";
|
import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions";
|
||||||
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
||||||
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||||
|
import {
|
||||||
|
AuthOptionalUser,
|
||||||
|
GetOptionalUser,
|
||||||
|
} from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
|
||||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
|
||||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
|
import { OptionalApiAuthGuard } from "@/modules/auth/guards/optional-api-auth/optional-api-auth.guard";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
import { UsersService } from "@/modules/users/services/users.service";
|
import { UsersService } from "@/modules/users/services/users.service";
|
||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
@@ -96,6 +100,7 @@ export class BookingsController_2024_08_13 {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post("/")
|
@Post("/")
|
||||||
|
@UseGuards(OptionalApiAuthGuard)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Create a booking",
|
summary: "Create a booking",
|
||||||
description: `
|
description: `
|
||||||
@@ -138,9 +143,10 @@ export class BookingsController_2024_08_13 {
|
|||||||
async createBooking(
|
async createBooking(
|
||||||
@Body(new CreateBookingInputPipe())
|
@Body(new CreateBookingInputPipe())
|
||||||
body: CreateBookingInput,
|
body: CreateBookingInput,
|
||||||
@Req() request: Request
|
@Req() request: Request,
|
||||||
|
@GetOptionalUser() user: AuthOptionalUser
|
||||||
): Promise<CreateBookingOutput_2024_08_13> {
|
): Promise<CreateBookingOutput_2024_08_13> {
|
||||||
const booking = await this.bookingsService.createBooking(request, body);
|
const booking = await this.bookingsService.createBooking(request, body, user);
|
||||||
|
|
||||||
if (Array.isArray(booking)) {
|
if (Array.isArray(booking)) {
|
||||||
await this.bookingsService.billBookings(booking);
|
await this.bookingsService.billBookings(booking);
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
slug: seatedEventTypeSlug,
|
slug: seatedEventTypeSlug,
|
||||||
length: 60,
|
length: 60,
|
||||||
seatsPerTimeSlot: 3,
|
seatsPerTimeSlot: 3,
|
||||||
|
seatsShowAttendees: true,
|
||||||
},
|
},
|
||||||
user.id
|
user.id
|
||||||
);
|
);
|
||||||
|
|||||||
+270
-724
File diff suppressed because it is too large
Load Diff
+306
-4
@@ -3,6 +3,7 @@ import { AppModule } from "@/app.module";
|
|||||||
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||||
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||||
import { Locales } from "@/lib/enums/locales";
|
import { Locales } from "@/lib/enums/locales";
|
||||||
|
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||||
import {
|
import {
|
||||||
CreateManagedUserData,
|
CreateManagedUserData,
|
||||||
CreateManagedUserOutput,
|
CreateManagedUserOutput,
|
||||||
@@ -15,11 +16,11 @@ import { NestExpressApplication } from "@nestjs/platform-express";
|
|||||||
import { Test } from "@nestjs/testing";
|
import { Test } from "@nestjs/testing";
|
||||||
import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
||||||
import * as request from "supertest";
|
import * as request from "supertest";
|
||||||
|
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||||
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
|
|
||||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
import { randomString } from "test/utils/randomString";
|
import { randomString } from "test/utils/randomString";
|
||||||
@@ -36,6 +37,7 @@ import {
|
|||||||
CreateBookingInput_2024_08_13,
|
CreateBookingInput_2024_08_13,
|
||||||
CreateEventTypeInput_2024_06_14,
|
CreateEventTypeInput_2024_06_14,
|
||||||
EventTypeOutput_2024_06_14,
|
EventTypeOutput_2024_06_14,
|
||||||
|
GetBookingOutput_2024_08_13,
|
||||||
GetBookingsOutput_2024_08_13,
|
GetBookingsOutput_2024_08_13,
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
|
|
||||||
@@ -50,9 +52,9 @@ describe("Managed user bookings 2024-08-13", () => {
|
|||||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||||
let schedulesRepositoryFixture: SchedulesRepositoryFixture;
|
|
||||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||||
|
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||||
|
|
||||||
const platformAdminEmail = `managed-users-bookings-2024-08-13-admin-${randomString()}@api.com`;
|
const platformAdminEmail = `managed-users-bookings-2024-08-13-admin-${randomString()}@api.com`;
|
||||||
let platformAdmin: User;
|
let platformAdmin: User;
|
||||||
@@ -67,6 +69,10 @@ describe("Managed user bookings 2024-08-13", () => {
|
|||||||
let thirdManagedUser: CreateManagedUserData;
|
let thirdManagedUser: CreateManagedUserData;
|
||||||
|
|
||||||
let firstManagedUserEventTypeId: number;
|
let firstManagedUserEventTypeId: number;
|
||||||
|
let eventTypeRequiresConfirmationId: number;
|
||||||
|
|
||||||
|
const orgAdminManagedUserEmail = `managed-user-bookings-2024-08-13-org-admin-${randomString()}@api.com`;
|
||||||
|
let orgAdminManagedUser: CreateManagedUserData;
|
||||||
|
|
||||||
let firstManagedUserBookingsCount = 0;
|
let firstManagedUserBookingsCount = 0;
|
||||||
let secondManagedUserBookingsCount = 0;
|
let secondManagedUserBookingsCount = 0;
|
||||||
@@ -75,7 +81,7 @@ describe("Managed user bookings 2024-08-13", () => {
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await Test.createTestingModule({
|
const moduleRef = await Test.createTestingModule({
|
||||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||||
imports: [AppModule, UsersModule],
|
imports: [AppModule, UsersModule, MembershipsModule],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
app = moduleRef.createNestApplication();
|
app = moduleRef.createNestApplication();
|
||||||
@@ -85,9 +91,9 @@ describe("Managed user bookings 2024-08-13", () => {
|
|||||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||||
schedulesRepositoryFixture = new SchedulesRepositoryFixture(moduleRef);
|
|
||||||
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||||
|
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail });
|
platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail });
|
||||||
|
|
||||||
@@ -95,6 +101,13 @@ describe("Managed user bookings 2024-08-13", () => {
|
|||||||
name: `oauth-client-users-organization-${randomString()}`,
|
name: `oauth-client-users-organization-${randomString()}`,
|
||||||
isPlatform: true,
|
isPlatform: true,
|
||||||
isOrganization: true,
|
isOrganization: true,
|
||||||
|
platformBilling: {
|
||||||
|
create: {
|
||||||
|
customerId: "cus_999",
|
||||||
|
plan: "SCALE",
|
||||||
|
subscriptionId: "sub_999",
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
oAuthClient = await createOAuthClient(organization.id);
|
oAuthClient = await createOAuthClient(organization.id);
|
||||||
|
|
||||||
@@ -243,6 +256,59 @@ describe("Managed user bookings 2024-08-13", () => {
|
|||||||
thirdManagedUser = responseBody.data;
|
thirdManagedUser = responseBody.data;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it(`should create org admin managed user`, async () => {
|
||||||
|
const requestBody: CreateManagedUserInput = {
|
||||||
|
email: orgAdminManagedUserEmail,
|
||||||
|
timeZone: managedUsersTimeZone,
|
||||||
|
weekStart: "Monday",
|
||||||
|
timeFormat: 24,
|
||||||
|
locale: Locales.FR,
|
||||||
|
name: "Org Admin Smith",
|
||||||
|
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
|
||||||
|
.set("x-cal-secret-key", oAuthClient.secret)
|
||||||
|
.send(requestBody)
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const responseBody: CreateManagedUserOutput = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseBody.data.user.email).toEqual(
|
||||||
|
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email)
|
||||||
|
);
|
||||||
|
expect(responseBody.data.accessToken).toBeDefined();
|
||||||
|
expect(responseBody.data.refreshToken).toBeDefined();
|
||||||
|
|
||||||
|
orgAdminManagedUser = responseBody.data;
|
||||||
|
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post(`/v2/organizations/${organization.id}/memberships`)
|
||||||
|
.set("x-cal-client-id", oAuthClient.id)
|
||||||
|
.set("x-cal-secret-key", oAuthClient.secret)
|
||||||
|
.send({
|
||||||
|
userId: orgAdminManagedUser.user.id,
|
||||||
|
role: "ADMIN",
|
||||||
|
accepted: true,
|
||||||
|
})
|
||||||
|
.expect(201);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create an event type requiring confirmation for first managed user", async () => {
|
||||||
|
const eventTypeRequiresConfirmation = await eventTypesRepositoryFixture.create(
|
||||||
|
{
|
||||||
|
title: `managed-user-bookings-event-type-requires-confirmation-${randomString()}`,
|
||||||
|
slug: `managed-user-bookings-event-type-requires-confirmation-${randomString()}`,
|
||||||
|
length: 60,
|
||||||
|
requiresConfirmation: true,
|
||||||
|
},
|
||||||
|
firstManagedUser.user.id
|
||||||
|
);
|
||||||
|
eventTypeRequiresConfirmationId = eventTypeRequiresConfirmation.id;
|
||||||
|
});
|
||||||
|
|
||||||
describe("bookings using original emails", () => {
|
describe("bookings using original emails", () => {
|
||||||
it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => {
|
it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => {
|
||||||
const body: CreateBookingInput_2024_08_13 = {
|
const body: CreateBookingInput_2024_08_13 = {
|
||||||
@@ -543,10 +609,246 @@ describe("Managed user bookings 2024-08-13", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("booking confirmation by org admin", () => {
|
||||||
|
it("should allow org admin managed user to confirm booking using access token", async () => {
|
||||||
|
const bookingRequiringConfirmation = await bookingsRepositoryFixture.create({
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: firstManagedUser.user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
startTime: new Date(Date.UTC(2030, 0, 9, 13, 0, 0)),
|
||||||
|
endTime: new Date(Date.UTC(2030, 0, 9, 14, 0, 0)),
|
||||||
|
title: "Booking requiring confirmation",
|
||||||
|
uid: `booking-requiring-confirmation-${randomString()}`,
|
||||||
|
eventType: {
|
||||||
|
connect: {
|
||||||
|
id: eventTypeRequiresConfirmationId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
location: "https://meet.google.com/abc-def-ghi",
|
||||||
|
customInputs: {},
|
||||||
|
metadata: {},
|
||||||
|
status: "PENDING",
|
||||||
|
responses: {
|
||||||
|
name: secondManagedUser.user.name,
|
||||||
|
email: secondManagedUserEmail,
|
||||||
|
},
|
||||||
|
attendees: {
|
||||||
|
create: {
|
||||||
|
email: secondManagedUserEmail,
|
||||||
|
name: secondManagedUser.user.name!,
|
||||||
|
locale: secondManagedUser.user.locale,
|
||||||
|
timeZone: secondManagedUser.user.timeZone,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings/${bookingRequiringConfirmation.uid}/confirm`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
|
||||||
|
const bookingData = responseBody.data as BookingOutput_2024_08_13;
|
||||||
|
expect(bookingData.status).toEqual("accepted");
|
||||||
|
expect(bookingData.uid).toEqual(bookingRequiringConfirmation.uid);
|
||||||
|
|
||||||
|
const confirmedBooking = await bookingsRepositoryFixture.getByUid(bookingRequiringConfirmation.uid);
|
||||||
|
expect(confirmedBooking?.status).toEqual("ACCEPTED");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow org admin managed user to reject booking using access token", async () => {
|
||||||
|
const bookingRequiringConfirmation = await bookingsRepositoryFixture.create({
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: firstManagedUser.user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
startTime: new Date(Date.UTC(2030, 0, 9, 15, 0, 0)),
|
||||||
|
endTime: new Date(Date.UTC(2030, 0, 9, 16, 0, 0)),
|
||||||
|
title: "Booking requiring rejection",
|
||||||
|
uid: `booking-requiring-rejection-${randomString()}`,
|
||||||
|
eventType: {
|
||||||
|
connect: {
|
||||||
|
id: eventTypeRequiresConfirmationId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
location: "https://meet.google.com/abc-def-ghi",
|
||||||
|
customInputs: {},
|
||||||
|
metadata: {},
|
||||||
|
status: "PENDING",
|
||||||
|
responses: {
|
||||||
|
name: secondManagedUser.user.name,
|
||||||
|
email: secondManagedUserEmail,
|
||||||
|
},
|
||||||
|
attendees: {
|
||||||
|
create: {
|
||||||
|
email: secondManagedUserEmail,
|
||||||
|
name: secondManagedUser.user.name!,
|
||||||
|
locale: secondManagedUser.user.locale,
|
||||||
|
timeZone: secondManagedUser.user.timeZone,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings/${bookingRequiringConfirmation.uid}/decline`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
|
||||||
|
const bookingData = responseBody.data as BookingOutput_2024_08_13;
|
||||||
|
expect(bookingData.status).toEqual("rejected");
|
||||||
|
expect(bookingData.uid).toEqual(bookingRequiringConfirmation.uid);
|
||||||
|
|
||||||
|
const rejectedBooking = await bookingsRepositoryFixture.getByUid(bookingRequiringConfirmation.uid);
|
||||||
|
expect(rejectedBooking?.status).toEqual("REJECTED");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return unauthorized when org admin tries to confirm regular user's booking", async () => {
|
||||||
|
const regularUser = await userRepositoryFixture.create({
|
||||||
|
email: `managed-user-bookings-2024-08-13-regular-user-${randomString()}@api.com`,
|
||||||
|
name: "Regular User",
|
||||||
|
});
|
||||||
|
|
||||||
|
const regularUserEventType = await eventTypesRepositoryFixture.create(
|
||||||
|
{
|
||||||
|
title: `regular-user-event-type-requires-confirmation-${randomString()}`,
|
||||||
|
slug: `regular-user-event-type-requires-confirmation-${randomString()}`,
|
||||||
|
length: 60,
|
||||||
|
requiresConfirmation: true,
|
||||||
|
},
|
||||||
|
regularUser.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const regularUserBooking = await bookingsRepositoryFixture.create({
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: regularUser.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
startTime: new Date(Date.UTC(2030, 0, 9, 17, 0, 0)),
|
||||||
|
endTime: new Date(Date.UTC(2030, 0, 9, 18, 0, 0)),
|
||||||
|
title: "Regular user booking requiring confirmation",
|
||||||
|
uid: `regular-user-booking-${randomString()}`,
|
||||||
|
eventType: {
|
||||||
|
connect: {
|
||||||
|
id: regularUserEventType.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
location: "https://meet.google.com/abc-def-ghi",
|
||||||
|
customInputs: {},
|
||||||
|
metadata: {},
|
||||||
|
status: "PENDING",
|
||||||
|
responses: {
|
||||||
|
name: "External Attendee",
|
||||||
|
email: "external@example.com",
|
||||||
|
},
|
||||||
|
attendees: {
|
||||||
|
create: {
|
||||||
|
email: "external@example.com",
|
||||||
|
name: "External Attendee",
|
||||||
|
locale: "en",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings/${regularUserBooking.uid}/confirm`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`)
|
||||||
|
.expect(401);
|
||||||
|
|
||||||
|
await userRepositoryFixture.delete(regularUser.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("event type booking requires authentication", () => {
|
||||||
|
let eventTypeRequiringAuthenticationId: number;
|
||||||
|
|
||||||
|
let body: CreateBookingInput_2024_08_13;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const eventTypeRequiringAuthentication = await eventTypesRepositoryFixture.create(
|
||||||
|
{
|
||||||
|
title: `event-type-requiring-authentication-${randomString()}`,
|
||||||
|
slug: `event-type-requiring-authentication-${randomString()}`,
|
||||||
|
length: 60,
|
||||||
|
requiresConfirmation: true,
|
||||||
|
bookingRequiresAuthentication: true,
|
||||||
|
},
|
||||||
|
secondManagedUser.user.id
|
||||||
|
);
|
||||||
|
eventTypeRequiringAuthenticationId = eventTypeRequiringAuthentication.id;
|
||||||
|
|
||||||
|
body = {
|
||||||
|
start: new Date(Date.UTC(2030, 0, 9, 15, 0, 0)).toISOString(),
|
||||||
|
eventTypeId: eventTypeRequiringAuthenticationId,
|
||||||
|
attendee: {
|
||||||
|
email: "external@example.com",
|
||||||
|
name: "External Attendee",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can't be booked without credentials", async () => {
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings`)
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.expect(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can't be booked with managed user credentials who is not admin and not event type owner", async () => {
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings`)
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
|
||||||
|
.expect(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can be booked with managed user credentials who is event type owner", async () => {
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings`)
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const bookingId = response.body.data.id;
|
||||||
|
await bookingsRepositoryFixture.deleteById(bookingId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can be booked with managed user credentials who is admin", async () => {
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings`)
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`)
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const bookingId = response.body.data.id;
|
||||||
|
await bookingsRepositoryFixture.deleteById(bookingId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await userRepositoryFixture.delete(firstManagedUser.user.id);
|
await userRepositoryFixture.delete(firstManagedUser.user.id);
|
||||||
await userRepositoryFixture.delete(secondManagedUser.user.id);
|
await userRepositoryFixture.delete(secondManagedUser.user.id);
|
||||||
await userRepositoryFixture.delete(thirdManagedUser.user.id);
|
await userRepositoryFixture.delete(thirdManagedUser.user.id);
|
||||||
|
await userRepositoryFixture.delete(orgAdminManagedUser.user.id);
|
||||||
await userRepositoryFixture.delete(platformAdmin.id);
|
await userRepositoryFixture.delete(platformAdmin.id);
|
||||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||||
await teamRepositoryFixture.delete(organization.id);
|
await teamRepositoryFixture.delete(organization.id);
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repo
|
|||||||
import { BookingSeatRepositoryFixture } from "test/fixtures/repository/booking-seat.repository.fixture";
|
import { BookingSeatRepositoryFixture } from "test/fixtures/repository/booking-seat.repository.fixture";
|
||||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
|
||||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
import { randomString } from "test/utils/randomString";
|
import { randomString } from "test/utils/randomString";
|
||||||
@@ -27,7 +26,6 @@ import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@cal
|
|||||||
import {
|
import {
|
||||||
CancelBookingInput_2024_08_13,
|
CancelBookingInput_2024_08_13,
|
||||||
CancelSeatedBookingInput_2024_08_13,
|
CancelSeatedBookingInput_2024_08_13,
|
||||||
CreateRecurringSeatedBookingOutput_2024_08_13,
|
|
||||||
CreateSeatedBookingOutput_2024_08_13,
|
CreateSeatedBookingOutput_2024_08_13,
|
||||||
GetBookingOutput_2024_08_13,
|
GetBookingOutput_2024_08_13,
|
||||||
GetBookingsOutput_2024_08_13,
|
GetBookingsOutput_2024_08_13,
|
||||||
@@ -35,11 +33,9 @@ import {
|
|||||||
RescheduleSeatedBookingInput_2024_08_13,
|
RescheduleSeatedBookingInput_2024_08_13,
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
import { CreateBookingInput_2024_08_13 } from "@calcom/platform-types";
|
import { CreateBookingInput_2024_08_13 } from "@calcom/platform-types";
|
||||||
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
import { Team } from "@calcom/prisma/client";
|
||||||
|
|
||||||
describe("Bookings Endpoints 2024-08-13", () => {
|
describe("Bookings Endpoints 2024-08-13", () => {
|
||||||
const googleMeetUrl = "https://meet.google.com/abc-def-ghi";
|
|
||||||
|
|
||||||
describe("Seated bookings", () => {
|
describe("Seated bookings", () => {
|
||||||
let app: INestApplication;
|
let app: INestApplication;
|
||||||
let organization: Team;
|
let organization: Team;
|
||||||
@@ -48,7 +44,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||||
let schedulesService: SchedulesService_2024_04_15;
|
let schedulesService: SchedulesService_2024_04_15;
|
||||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
|
||||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||||
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||||
let bookingSeatRepositoryFixture: BookingSeatRepositoryFixture;
|
let bookingSeatRepositoryFixture: BookingSeatRepositoryFixture;
|
||||||
@@ -58,9 +53,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
let apiKeyString: string;
|
let apiKeyString: string;
|
||||||
|
|
||||||
let seatedEventTypeId: number;
|
let seatedEventTypeId: number;
|
||||||
const maxRecurrenceCount = 3;
|
let seatedEventTypeIdAttendeesDisabledId: number;
|
||||||
|
|
||||||
const seatedEventSlug = `seated-bookings-event-type-${randomString()}`;
|
const seatedEventSlug = `seated-bookings-event-type-${randomString()}`;
|
||||||
|
const seatedEventSlugAttendeesDisabled = `seated-bookings-event-type-attendees-disabled-${randomString()}`;
|
||||||
|
|
||||||
let createdSeatedBooking: CreateSeatedBookingOutput_2024_08_13;
|
let createdSeatedBooking: CreateSeatedBookingOutput_2024_08_13;
|
||||||
let createdSeatedBooking2: CreateSeatedBookingOutput_2024_08_13;
|
let createdSeatedBooking2: CreateSeatedBookingOutput_2024_08_13;
|
||||||
@@ -83,7 +79,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
|
||||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||||
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
|
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
|
||||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||||
@@ -128,6 +123,28 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
);
|
);
|
||||||
seatedEventTypeId = seatedEvent.id;
|
seatedEventTypeId = seatedEvent.id;
|
||||||
|
|
||||||
|
const seatedEventAttendeesDisabled = await eventTypesRepositoryFixture.create(
|
||||||
|
{
|
||||||
|
title: `seated-bookings-2024-08-13-event-type-attendees-disabled-${randomString()}`,
|
||||||
|
slug: seatedEventSlugAttendeesDisabled,
|
||||||
|
length: 60,
|
||||||
|
seatsPerTimeSlot: 5,
|
||||||
|
seatsShowAttendees: false,
|
||||||
|
seatsShowAvailabilityCount: false,
|
||||||
|
locations: [{ type: "inPerson", address: "via 10, rome, italy" }],
|
||||||
|
metadata: {
|
||||||
|
disableStandardEmails: {
|
||||||
|
all: {
|
||||||
|
attendee: true,
|
||||||
|
host: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user.id
|
||||||
|
);
|
||||||
|
seatedEventTypeIdAttendeesDisabledId = seatedEventAttendeesDisabled.id;
|
||||||
|
|
||||||
app = moduleRef.createNestApplication();
|
app = moduleRef.createNestApplication();
|
||||||
bootstrap(app as NestExpressApplication);
|
bootstrap(app as NestExpressApplication);
|
||||||
|
|
||||||
@@ -190,6 +207,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
absent: false,
|
absent: false,
|
||||||
seatUid,
|
seatUid,
|
||||||
bookingFieldsResponses: {
|
bookingFieldsResponses: {
|
||||||
|
guests: [],
|
||||||
name: body.attendee.name,
|
name: body.attendee.name,
|
||||||
...body.bookingFieldsResponses,
|
...body.bookingFieldsResponses,
|
||||||
},
|
},
|
||||||
@@ -264,6 +282,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
absent: false,
|
absent: false,
|
||||||
seatUid: createdSeatedBooking.seatUid,
|
seatUid: createdSeatedBooking.seatUid,
|
||||||
bookingFieldsResponses: {
|
bookingFieldsResponses: {
|
||||||
|
guests: [],
|
||||||
name: createdSeatedBooking.attendees[0].name,
|
name: createdSeatedBooking.attendees[0].name,
|
||||||
...createdSeatedBooking.attendees[0].bookingFieldsResponses,
|
...createdSeatedBooking.attendees[0].bookingFieldsResponses,
|
||||||
},
|
},
|
||||||
@@ -278,6 +297,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
absent: false,
|
absent: false,
|
||||||
seatUid,
|
seatUid,
|
||||||
bookingFieldsResponses: {
|
bookingFieldsResponses: {
|
||||||
|
guests: [],
|
||||||
name: body.attendee.name,
|
name: body.attendee.name,
|
||||||
...body.bookingFieldsResponses,
|
...body.bookingFieldsResponses,
|
||||||
},
|
},
|
||||||
@@ -418,6 +438,63 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should book an event type with attendees disabled", async () => {
|
||||||
|
const body: CreateBookingInput_2024_08_13 = {
|
||||||
|
start: new Date(Date.UTC(2030, 0, 9, 14, 0, 0)).toISOString(),
|
||||||
|
eventTypeId: seatedEventTypeIdAttendeesDisabledId,
|
||||||
|
attendee: {
|
||||||
|
name: nameAttendeeOne,
|
||||||
|
email: emailAttendeeOne,
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
language: "it",
|
||||||
|
},
|
||||||
|
bookingFieldsResponses: {
|
||||||
|
codingLanguage: "TypeScript",
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
userId: "100",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post("/v2/bookings")
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.expect(201)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseDataIsCreateSeatedBooking(responseBody.data)).toBe(true);
|
||||||
|
|
||||||
|
if (responseDataIsCreateSeatedBooking(responseBody.data)) {
|
||||||
|
const data: CreateSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||||
|
expect(data.seatUid).toBeDefined();
|
||||||
|
expect(data.id).toBeDefined();
|
||||||
|
expect(data.uid).toBeDefined();
|
||||||
|
expect(data.hosts[0].id).toEqual(user.id);
|
||||||
|
expect(data.status).toEqual("accepted");
|
||||||
|
expect(data.start).toEqual(body.start);
|
||||||
|
expect(data.end).toEqual(
|
||||||
|
DateTime.fromISO(body.start, { zone: "utc" }).plus({ hours: 1 }).toISO()
|
||||||
|
);
|
||||||
|
expect(data.duration).toEqual(60);
|
||||||
|
expect(data.eventTypeId).toEqual(seatedEventTypeIdAttendeesDisabledId);
|
||||||
|
expect(data.eventType).toEqual({
|
||||||
|
id: seatedEventTypeIdAttendeesDisabledId,
|
||||||
|
slug: seatedEventSlugAttendeesDisabled,
|
||||||
|
});
|
||||||
|
expect(data.attendees.length).toEqual(0);
|
||||||
|
expect(data.location).toBeDefined();
|
||||||
|
expect(data.absentHost).toEqual(false);
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"Invalid response data - expected seated booking but received non array response"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("cancel seated booking", () => {
|
describe("cancel seated booking", () => {
|
||||||
describe("cancel seated booking as attendee", () => {
|
describe("cancel seated booking as attendee", () => {
|
||||||
it("should cancel seated booking", async () => {
|
it("should cancel seated booking", async () => {
|
||||||
@@ -517,6 +594,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
absent: false,
|
absent: false,
|
||||||
seatUid,
|
seatUid,
|
||||||
bookingFieldsResponses: {
|
bookingFieldsResponses: {
|
||||||
|
guests: [],
|
||||||
name: body.attendee.name,
|
name: body.attendee.name,
|
||||||
...body.bookingFieldsResponses,
|
...body.bookingFieldsResponses,
|
||||||
},
|
},
|
||||||
|
|||||||
+76
-1
@@ -21,7 +21,12 @@ import { randomString } from "test/utils/randomString";
|
|||||||
import { withApiAuth } from "test/utils/withApiAuth";
|
import { withApiAuth } from "test/utils/withApiAuth";
|
||||||
|
|
||||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||||
import { CreateBookingInput_2024_08_13, BookingOutput_2024_08_13 } from "@calcom/platform-types";
|
import {
|
||||||
|
CreateBookingInput_2024_08_13,
|
||||||
|
BookingOutput_2024_08_13,
|
||||||
|
CreateRecurringBookingInput_2024_08_13,
|
||||||
|
RecurringBookingOutput_2024_08_13,
|
||||||
|
} from "@calcom/platform-types";
|
||||||
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
||||||
|
|
||||||
describe("Bookings Endpoints 2024-08-13", () => {
|
describe("Bookings Endpoints 2024-08-13", () => {
|
||||||
@@ -44,6 +49,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
const VARIABLE_LENGTH_EVENT_TYPE_SLUG = `variable-length-bookings-2024-08-13-event-type-${randomString()}`;
|
const VARIABLE_LENGTH_EVENT_TYPE_SLUG = `variable-length-bookings-2024-08-13-event-type-${randomString()}`;
|
||||||
let normalEventType: EventType;
|
let normalEventType: EventType;
|
||||||
const NORMAL_EVENT_TYPE_SLUG = `variable-length-bookings-2024-08-13-event-type-${randomString()}`;
|
const NORMAL_EVENT_TYPE_SLUG = `variable-length-bookings-2024-08-13-event-type-${randomString()}`;
|
||||||
|
let variableLengthRecurringEventType: EventType;
|
||||||
|
const VARIABLE_LENGTH_RECURRING_EVENT_TYPE_SLUG = `variable-length-recurring-bookings-2024-08-13-event-type-${randomString()}`;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await withApiAuth(
|
const moduleRef = await withApiAuth(
|
||||||
@@ -105,6 +112,17 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
user.id
|
user.id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
variableLengthRecurringEventType = await eventTypesRepositoryFixture.create(
|
||||||
|
{
|
||||||
|
title: `variable-length-recurring-bookings-2024-08-13-event-type-${randomString()}`,
|
||||||
|
slug: VARIABLE_LENGTH_RECURRING_EVENT_TYPE_SLUG,
|
||||||
|
length: 15,
|
||||||
|
metadata: { multipleDuration: [15, 30, 60] },
|
||||||
|
recurringEvent: { freq: 2, count: 3, interval: 1 }, // Weekly, 3 times, every 1 week
|
||||||
|
},
|
||||||
|
user.id
|
||||||
|
);
|
||||||
|
|
||||||
app = moduleRef.createNestApplication();
|
app = moduleRef.createNestApplication();
|
||||||
bootstrap(app as NestExpressApplication);
|
bootstrap(app as NestExpressApplication);
|
||||||
|
|
||||||
@@ -246,6 +264,63 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("create recurring bookings", () => {
|
||||||
|
it("should create recurring bookings with custom lengthInMinutes", async () => {
|
||||||
|
const lengthInMinutes = 30;
|
||||||
|
const body: CreateRecurringBookingInput_2024_08_13 = {
|
||||||
|
start: new Date(Date.UTC(2030, 1, 1, 14, 0, 0)).toISOString(),
|
||||||
|
eventTypeId: variableLengthRecurringEventType.id,
|
||||||
|
lengthInMinutes,
|
||||||
|
recurrenceCount: 2,
|
||||||
|
attendee: {
|
||||||
|
name: "Mr Recurring",
|
||||||
|
email: "mr_recurring@gmail.com",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
language: "it",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post("/v2/bookings")
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.expect(201)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: { status: string; data: RecurringBookingOutput_2024_08_13[] } = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(Array.isArray(responseBody.data)).toBe(true);
|
||||||
|
expect(responseBody.data).toHaveLength(2);
|
||||||
|
|
||||||
|
const firstBooking = responseBody.data[0];
|
||||||
|
const secondBooking = responseBody.data[1];
|
||||||
|
expect(firstBooking.duration).toEqual(lengthInMinutes);
|
||||||
|
expect(secondBooking.duration).toEqual(lengthInMinutes);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject recurring booking with invalid lengthInMinutes", async () => {
|
||||||
|
const body: CreateRecurringBookingInput_2024_08_13 = {
|
||||||
|
start: new Date(Date.UTC(2030, 3, 1, 10, 0, 0)).toISOString(),
|
||||||
|
eventTypeId: variableLengthRecurringEventType.id,
|
||||||
|
lengthInMinutes: 45, // Not in allowed [15, 30, 60]
|
||||||
|
recurrenceCount: 2,
|
||||||
|
attendee: {
|
||||||
|
name: "Mr Invalid",
|
||||||
|
email: "mr_invalid@gmail.com",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
language: "it",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post("/v2/bookings")
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.expect(400);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||||
await teamRepositoryFixture.delete(organization.id);
|
await teamRepositoryFixture.delete(organization.id);
|
||||||
|
|||||||
@@ -6,9 +6,12 @@ import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/servi
|
|||||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||||
import { getPagination } from "@/lib/pagination/pagination";
|
import { getPagination } from "@/lib/pagination/pagination";
|
||||||
|
import { AuthOptionalUser } from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
|
||||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||||
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
||||||
import { KyselyReadService } from "@/modules/kysely/kysely-read.service";
|
import { KyselyReadService } from "@/modules/kysely/kysely-read.service";
|
||||||
|
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||||
|
import { MembershipsService } from "@/modules/memberships/services/memberships.service";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||||
@@ -18,7 +21,14 @@ import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-eve
|
|||||||
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
||||||
import { UsersService } from "@/modules/users/services/users.service";
|
import { UsersService } from "@/modules/users/services/users.service";
|
||||||
import { UsersRepository, UserWithProfile } from "@/modules/users/users.repository";
|
import { UsersRepository, UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import { ConflictException, Injectable, Logger, NotFoundException } from "@nestjs/common";
|
import {
|
||||||
|
ConflictException,
|
||||||
|
ForbiddenException,
|
||||||
|
Injectable,
|
||||||
|
Logger,
|
||||||
|
NotFoundException,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from "@nestjs/common";
|
||||||
import { BadRequestException } from "@nestjs/common";
|
import { BadRequestException } from "@nestjs/common";
|
||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
@@ -95,10 +105,12 @@ export class BookingsService_2024_08_13 {
|
|||||||
private readonly organizationsRepository: OrganizationsRepository,
|
private readonly organizationsRepository: OrganizationsRepository,
|
||||||
private readonly teamsRepository: TeamsRepository,
|
private readonly teamsRepository: TeamsRepository,
|
||||||
private readonly teamsEventTypesRepository: TeamsEventTypesRepository,
|
private readonly teamsEventTypesRepository: TeamsEventTypesRepository,
|
||||||
|
private readonly membershipsRepository: MembershipsRepository,
|
||||||
|
private readonly membershipsService: MembershipsService,
|
||||||
private readonly errorsBookingsService: ErrorsBookingsService_2024_08_13
|
private readonly errorsBookingsService: ErrorsBookingsService_2024_08_13
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async createBooking(request: Request, body: CreateBookingInput) {
|
async createBooking(request: Request, body: CreateBookingInput, authUser: AuthOptionalUser) {
|
||||||
let bookingTeamEventType = false;
|
let bookingTeamEventType = false;
|
||||||
try {
|
try {
|
||||||
const eventType = await this.getBookedEventType(body);
|
const eventType = await this.getBookedEventType(body);
|
||||||
@@ -108,6 +120,7 @@ export class BookingsService_2024_08_13 {
|
|||||||
if (!eventType) {
|
if (!eventType) {
|
||||||
this.errorsBookingsService.handleEventTypeToBeBookedNotFound(body);
|
this.errorsBookingsService.handleEventTypeToBeBookedNotFound(body);
|
||||||
}
|
}
|
||||||
|
await this.checkBookingRequiresAuthenticationSetting(eventType, authUser);
|
||||||
|
|
||||||
if (eventType.schedulingType === "MANAGED") {
|
if (eventType.schedulingType === "MANAGED") {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
@@ -142,6 +155,56 @@ export class BookingsService_2024_08_13 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async checkBookingRequiresAuthenticationSetting(
|
||||||
|
eventType: EventTypeWithOwnerAndTeam,
|
||||||
|
authUser: AuthOptionalUser
|
||||||
|
) {
|
||||||
|
if (!eventType.bookingRequiresAuthentication) return true;
|
||||||
|
if (!authUser) {
|
||||||
|
throw new UnauthorizedException(
|
||||||
|
"checkBookingRequiresAuthentication - request must be authenticated by passing credentials belonging to event type owner, host or team or org admin or owner."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const authUserId = authUser.id;
|
||||||
|
const authUserRole = authUser.role;
|
||||||
|
const eventTypeId = eventType.id;
|
||||||
|
const teamId = eventType.teamId;
|
||||||
|
const bookingUserId = eventType.owner?.id || null;
|
||||||
|
|
||||||
|
if (authUserRole === "ADMIN") return;
|
||||||
|
|
||||||
|
if (bookingUserId === authUserId) return;
|
||||||
|
|
||||||
|
if (eventTypeId) {
|
||||||
|
const [isUserHost, isUserAssigned] = await Promise.all([
|
||||||
|
this.eventTypesRepository.isUserHostOfEventType(authUserId, eventTypeId),
|
||||||
|
this.eventTypesRepository.isUserAssignedToEventType(authUserId, eventTypeId),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (isUserHost || isUserAssigned) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (teamId) {
|
||||||
|
const membership = await this.membershipsRepository.getUserAdminOrOwnerTeamMembership(
|
||||||
|
authUserId,
|
||||||
|
teamId
|
||||||
|
);
|
||||||
|
if (membership) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
bookingUserId &&
|
||||||
|
(await this.membershipsService.isUserOrgAdminOrOwnerOfAnotherUser(authUserId, bookingUserId))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"checkBookingRequiresAuthentication - user is not authorized to access this event type. User has to be either event type owner, host, team admin or owner or org admin or owner."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async getBookedEventType(body: CreateBookingInput) {
|
async getBookedEventType(body: CreateBookingInput) {
|
||||||
if (body.eventTypeId) {
|
if (body.eventTypeId) {
|
||||||
return await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(body.eventTypeId);
|
return await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(body.eventTypeId);
|
||||||
@@ -513,10 +576,13 @@ export class BookingsService_2024_08_13 {
|
|||||||
return this.outputService.getOutputRecurringBooking(booking);
|
return this.outputService.getOutputRecurringBooking(booking);
|
||||||
}
|
}
|
||||||
if (isRecurring && isSeated) {
|
if (isRecurring && isSeated) {
|
||||||
return this.outputService.getOutputRecurringSeatedBooking(booking);
|
return this.outputService.getOutputRecurringSeatedBooking(
|
||||||
|
booking,
|
||||||
|
!!booking.eventType?.seatsShowAttendees
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (isSeated) {
|
if (isSeated) {
|
||||||
return this.outputService.getOutputSeatedBooking(booking);
|
return this.outputService.getOutputSeatedBooking(booking, !!booking.eventType?.seatsShowAttendees);
|
||||||
}
|
}
|
||||||
return this.outputService.getOutputBooking(booking);
|
return this.outputService.getOutputBooking(booking);
|
||||||
}
|
}
|
||||||
@@ -528,7 +594,10 @@ export class BookingsService_2024_08_13 {
|
|||||||
const ids = recurringBooking.map((booking) => booking.id);
|
const ids = recurringBooking.map((booking) => booking.id);
|
||||||
const isRecurringSeated = !!recurringBooking[0].eventType?.seatsPerTimeSlot;
|
const isRecurringSeated = !!recurringBooking[0].eventType?.seatsPerTimeSlot;
|
||||||
if (isRecurringSeated) {
|
if (isRecurringSeated) {
|
||||||
return this.outputService.getOutputRecurringSeatedBookings(ids);
|
return this.outputService.getOutputRecurringSeatedBookings(
|
||||||
|
ids,
|
||||||
|
!!recurringBooking[0].eventType?.seatsShowAttendees
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.outputService.getOutputRecurringBookings(ids);
|
return this.outputService.getOutputRecurringBookings(ids);
|
||||||
@@ -594,9 +663,19 @@ export class BookingsService_2024_08_13 {
|
|||||||
if (isRecurring && !isSeated) {
|
if (isRecurring && !isSeated) {
|
||||||
formattedBookings.push(this.outputService.getOutputRecurringBooking(formatted));
|
formattedBookings.push(this.outputService.getOutputRecurringBooking(formatted));
|
||||||
} else if (isRecurring && isSeated) {
|
} else if (isRecurring && isSeated) {
|
||||||
formattedBookings.push(this.outputService.getOutputRecurringSeatedBooking(formatted));
|
formattedBookings.push(
|
||||||
|
this.outputService.getOutputRecurringSeatedBooking(
|
||||||
|
formatted,
|
||||||
|
!!formatted.eventType?.seatsShowAttendees
|
||||||
|
)
|
||||||
|
);
|
||||||
} else if (isSeated) {
|
} else if (isSeated) {
|
||||||
formattedBookings.push(await this.outputService.getOutputSeatedBooking(formatted));
|
formattedBookings.push(
|
||||||
|
await this.outputService.getOutputSeatedBooking(
|
||||||
|
formatted,
|
||||||
|
!!formatted.eventType?.seatsShowAttendees
|
||||||
|
)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
formattedBookings.push(await this.outputService.getOutputBooking(formatted));
|
formattedBookings.push(await this.outputService.getOutputBooking(formatted));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import { UsersRepository } from "@/modules/users/users.repository";
|
|||||||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||||
import { Logger } from "@nestjs/common";
|
import { Logger } from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { CreationSource } from "@prisma/client";
|
|
||||||
import { isURL, isPhoneNumber } from "class-validator";
|
import { isURL, isPhoneNumber } from "class-validator";
|
||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
@@ -30,6 +29,7 @@ import { NextApiRequest } from "next/types";
|
|||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { CreationSource } from "@calcom/platform-libraries";
|
||||||
import { EventTypeMetaDataSchema } from "@calcom/platform-libraries/event-types";
|
import { EventTypeMetaDataSchema } from "@calcom/platform-libraries/event-types";
|
||||||
import {
|
import {
|
||||||
CancelBookingInput,
|
CancelBookingInput,
|
||||||
@@ -420,6 +420,9 @@ export class InputBookingsService_2024_08_13 {
|
|||||||
throw new NotFoundException(`Event type with id=${inputBooking.eventTypeId} is not a recurring event`);
|
throw new NotFoundException(`Event type with id=${inputBooking.eventTypeId} is not a recurring event`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.validateBookingLengthInMinutes(inputBooking, eventType);
|
||||||
|
const lengthInMinutes = inputBooking.lengthInMinutes ?? eventType.length;
|
||||||
|
|
||||||
const occurrence = recurringEventSchema.parse(eventType.recurringEvent);
|
const occurrence = recurringEventSchema.parse(eventType.recurringEvent);
|
||||||
const repeatsEvery = occurrence.interval;
|
const repeatsEvery = occurrence.interval;
|
||||||
|
|
||||||
@@ -456,7 +459,7 @@ export class InputBookingsService_2024_08_13 {
|
|||||||
const location = inputLocation ? this.transformLocation(inputLocation) : undefined;
|
const location = inputLocation ? this.transformLocation(inputLocation) : undefined;
|
||||||
|
|
||||||
for (let i = 0; i < repeatsTimes; i++) {
|
for (let i = 0; i < repeatsTimes; i++) {
|
||||||
const endTime = startTime.plus({ minutes: eventType.length });
|
const endTime = startTime.plus({ minutes: lengthInMinutes });
|
||||||
|
|
||||||
events.push({
|
events.push({
|
||||||
start: startTime.toISO(),
|
start: startTime.toISO(),
|
||||||
@@ -702,6 +705,7 @@ export class InputBookingsService_2024_08_13 {
|
|||||||
beforeUpdatedDate: queryParams.beforeUpdatedAt,
|
beforeUpdatedDate: queryParams.beforeUpdatedAt,
|
||||||
afterCreatedDate: queryParams.afterCreatedAt,
|
afterCreatedDate: queryParams.afterCreatedAt,
|
||||||
beforeCreatedDate: queryParams.beforeCreatedAt,
|
beforeCreatedDate: queryParams.beforeCreatedAt,
|
||||||
|
bookingUid: queryParams.bookingUid,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ type DatabaseBooking = Booking & {
|
|||||||
eventType: {
|
eventType: {
|
||||||
id: number;
|
id: number;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
seatsShowAttendees?: boolean | null;
|
||||||
} | null;
|
} | null;
|
||||||
attendees: {
|
attendees: {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -167,6 +168,7 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
getUserDefinedMetadata(databaseMetadata: DatabaseMetadata) {
|
getUserDefinedMetadata(databaseMetadata: DatabaseMetadata) {
|
||||||
if (databaseMetadata === null) return {};
|
if (databaseMetadata === null) return {};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { videoCallUrl, ...userDefinedMetadata } = databaseMetadata;
|
const { videoCallUrl, ...userDefinedMetadata } = databaseMetadata;
|
||||||
|
|
||||||
return userDefinedMetadata;
|
return userDefinedMetadata;
|
||||||
@@ -266,11 +268,14 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
databaseBooking: DatabaseBooking,
|
databaseBooking: DatabaseBooking,
|
||||||
seatUid: string
|
seatUid: string
|
||||||
): Promise<CreateSeatedBookingOutput_2024_08_13> {
|
): Promise<CreateSeatedBookingOutput_2024_08_13> {
|
||||||
const getSeatedBookingOutput = await this.getOutputSeatedBooking(databaseBooking);
|
const getSeatedBookingOutput = await this.getOutputSeatedBooking(
|
||||||
|
databaseBooking,
|
||||||
|
!!databaseBooking.eventType?.seatsShowAttendees
|
||||||
|
);
|
||||||
return { ...getSeatedBookingOutput, seatUid };
|
return { ...getSeatedBookingOutput, seatUid };
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOutputSeatedBooking(databaseBooking: DatabaseBooking) {
|
async getOutputSeatedBooking(databaseBooking: DatabaseBooking, showAttendees: boolean) {
|
||||||
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
||||||
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
||||||
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
||||||
@@ -317,7 +322,8 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
const parsed = plainToClass(GetSeatedBookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
const parsed = plainToClass(GetSeatedBookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
||||||
|
|
||||||
// note(Lauris): I don't know why plainToClass erases booking.attendees[n].responses so attaching manually
|
// note(Lauris): I don't know why plainToClass erases booking.attendees[n].responses so attaching manually
|
||||||
parsed.attendees = databaseBooking.attendees.map((attendee) => {
|
parsed.attendees = showAttendees
|
||||||
|
? databaseBooking.attendees.map((attendee) => {
|
||||||
const { responses } = safeParse(
|
const { responses } = safeParse(
|
||||||
seatedBookingDataSchema,
|
seatedBookingDataSchema,
|
||||||
attendee.bookingSeat?.data,
|
attendee.bookingSeat?.data,
|
||||||
@@ -346,12 +352,13 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
delete attendeeParsed.bookingFieldsResponses.email;
|
delete attendeeParsed.bookingFieldsResponses.email;
|
||||||
|
|
||||||
return attendeeParsed;
|
return attendeeParsed;
|
||||||
});
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOutputRecurringSeatedBookings(bookingsIds: number[]) {
|
async getOutputRecurringSeatedBookings(bookingsIds: number[], showAttendees: boolean) {
|
||||||
const transformed = [];
|
const transformed = [];
|
||||||
|
|
||||||
for (const bookingId of bookingsIds) {
|
for (const bookingId of bookingsIds) {
|
||||||
@@ -361,7 +368,7 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
throw new Error(`Booking with id=${bookingId} was not found in the database`);
|
throw new Error(`Booking with id=${bookingId} was not found in the database`);
|
||||||
}
|
}
|
||||||
|
|
||||||
transformed.push(this.getOutputRecurringSeatedBooking(databaseBooking));
|
transformed.push(this.getOutputRecurringSeatedBooking(databaseBooking, showAttendees));
|
||||||
}
|
}
|
||||||
|
|
||||||
return transformed.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
|
return transformed.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
|
||||||
@@ -386,11 +393,14 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
databaseBooking: DatabaseBooking,
|
databaseBooking: DatabaseBooking,
|
||||||
seatUid: string
|
seatUid: string
|
||||||
): CreateRecurringSeatedBookingOutput_2024_08_13 {
|
): CreateRecurringSeatedBookingOutput_2024_08_13 {
|
||||||
const getRecurringSeatedBookingOutput = this.getOutputRecurringSeatedBooking(databaseBooking);
|
const getRecurringSeatedBookingOutput = this.getOutputRecurringSeatedBooking(
|
||||||
|
databaseBooking,
|
||||||
|
!!databaseBooking.eventType?.seatsShowAttendees
|
||||||
|
);
|
||||||
return { ...getRecurringSeatedBookingOutput, seatUid };
|
return { ...getRecurringSeatedBookingOutput, seatUid };
|
||||||
}
|
}
|
||||||
|
|
||||||
getOutputRecurringSeatedBooking(databaseBooking: DatabaseBooking) {
|
getOutputRecurringSeatedBooking(databaseBooking: DatabaseBooking, showAttendees: boolean) {
|
||||||
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
||||||
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
||||||
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
||||||
@@ -430,7 +440,8 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// note(Lauris): I don't know why plainToClass erases booking.attendees[n].responses so attaching manually
|
// note(Lauris): I don't know why plainToClass erases booking.attendees[n].responses so attaching manually
|
||||||
parsed.attendees = databaseBooking.attendees.map((attendee) => {
|
parsed.attendees = showAttendees
|
||||||
|
? databaseBooking.attendees.map((attendee) => {
|
||||||
const { responses } = safeParse(
|
const { responses } = safeParse(
|
||||||
seatedBookingDataSchema,
|
seatedBookingDataSchema,
|
||||||
attendee.bookingSeat?.data,
|
attendee.bookingSeat?.data,
|
||||||
@@ -458,7 +469,8 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
// note(Lauris): as of now email is not returned for privacy
|
// note(Lauris): as of now email is not returned for privacy
|
||||||
delete attendeeParsed.bookingFieldsResponses.email;
|
delete attendeeParsed.bookingFieldsResponses.email;
|
||||||
return attendeeParsed;
|
return attendeeParsed;
|
||||||
});
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings
|
|||||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||||
import { CalendarsController } from "@/ee/calendars/controllers/calendars.controller";
|
import { CalendarsController } from "@/ee/calendars/controllers/calendars.controller";
|
||||||
import { AppleCalendarService } from "@/ee/calendars/services/apple-calendar.service";
|
import { AppleCalendarService } from "@/ee/calendars/services/apple-calendar.service";
|
||||||
|
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
|
||||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||||
import { GoogleCalendarService } from "@/ee/calendars/services/gcal.service";
|
import { GoogleCalendarService } from "@/ee/calendars/services/gcal.service";
|
||||||
import { IcsFeedService } from "@/ee/calendars/services/ics-feed.service";
|
import { IcsFeedService } from "@/ee/calendars/services/ics-feed.service";
|
||||||
@@ -10,16 +11,18 @@ import { OutlookService } from "@/ee/calendars/services/outlook.service";
|
|||||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { RedisModule } from "@/modules/redis/redis.module";
|
||||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
import { UsersModule } from "@/modules/users/users.module";
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, UsersModule, TokensModule],
|
imports: [PrismaModule, UsersModule, TokensModule, RedisModule],
|
||||||
providers: [
|
providers: [
|
||||||
CredentialsRepository,
|
CredentialsRepository,
|
||||||
CalendarsService,
|
CalendarsService,
|
||||||
|
CalendarsCacheService,
|
||||||
OutlookService,
|
OutlookService,
|
||||||
GoogleCalendarService,
|
GoogleCalendarService,
|
||||||
AppleCalendarService,
|
AppleCalendarService,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
DeletedCalendarCredentialsOutputDto,
|
DeletedCalendarCredentialsOutputDto,
|
||||||
} from "@/ee/calendars/outputs/delete-calendar-credentials.output";
|
} from "@/ee/calendars/outputs/delete-calendar-credentials.output";
|
||||||
import { AppleCalendarService } from "@/ee/calendars/services/apple-calendar.service";
|
import { AppleCalendarService } from "@/ee/calendars/services/apple-calendar.service";
|
||||||
|
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
|
||||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||||
import { GoogleCalendarService } from "@/ee/calendars/services/gcal.service";
|
import { GoogleCalendarService } from "@/ee/calendars/services/gcal.service";
|
||||||
import { IcsFeedService } from "@/ee/calendars/services/ics-feed.service";
|
import { IcsFeedService } from "@/ee/calendars/services/ics-feed.service";
|
||||||
@@ -79,6 +80,7 @@ const calendarStateSchema = z.object({
|
|||||||
export class CalendarsController {
|
export class CalendarsController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly calendarsService: CalendarsService,
|
private readonly calendarsService: CalendarsService,
|
||||||
|
private readonly calendarsCacheService: CalendarsCacheService,
|
||||||
private readonly outlookService: OutlookService,
|
private readonly outlookService: OutlookService,
|
||||||
private readonly googleCalendarService: GoogleCalendarService,
|
private readonly googleCalendarService: GoogleCalendarService,
|
||||||
private readonly appleCalendarService: AppleCalendarService,
|
private readonly appleCalendarService: AppleCalendarService,
|
||||||
@@ -307,6 +309,8 @@ export class CalendarsController {
|
|||||||
credentialId
|
credentialId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.calendarsCacheService.deleteConnectedAndDestinationCalendarsCache(user.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: plainToClass(
|
data: plainToClass(
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { RedisService } from "@/modules/redis/redis.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import type { getConnectedDestinationCalendarsAndEnsureDefaultsInDb } from "@calcom/platform-libraries";
|
||||||
|
|
||||||
|
export const REDIS_CALENDARS_CACHE_KEY = (userId: number) => `apiv2:user:${userId}:calendars`;
|
||||||
|
export const CALENDARS_CACHE_TTL_MS = 10_000;
|
||||||
|
|
||||||
|
type ConnectedDestinationCalendars = Awaited<
|
||||||
|
ReturnType<typeof getConnectedDestinationCalendarsAndEnsureDefaultsInDb>
|
||||||
|
>;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CalendarsCacheService {
|
||||||
|
constructor(private readonly redisService: RedisService) {}
|
||||||
|
|
||||||
|
async deleteConnectedAndDestinationCalendarsCache(userId: number) {
|
||||||
|
await this.redisService.del(REDIS_CALENDARS_CACHE_KEY(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getConnectedAndDestinationCalendarsCache(userId: number) {
|
||||||
|
const cachedResult = await this.redisService.get<ConnectedDestinationCalendars>(
|
||||||
|
REDIS_CALENDARS_CACHE_KEY(userId)
|
||||||
|
);
|
||||||
|
return cachedResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setConnectedAndDestinationCalendarsCache(
|
||||||
|
userId: number,
|
||||||
|
calendars: ConnectedDestinationCalendars
|
||||||
|
): Promise<void> {
|
||||||
|
await this.redisService.set<ConnectedDestinationCalendars>(REDIS_CALENDARS_CACHE_KEY(userId), calendars, {
|
||||||
|
ttl: CALENDARS_CACHE_TTL_MS,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||||
|
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
|
||||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||||
import {
|
import {
|
||||||
CredentialsRepository,
|
CredentialsRepository,
|
||||||
@@ -37,7 +38,8 @@ export class CalendarsService {
|
|||||||
private readonly appsRepository: AppsRepository,
|
private readonly appsRepository: AppsRepository,
|
||||||
private readonly calendarsRepository: CalendarsRepository,
|
private readonly calendarsRepository: CalendarsRepository,
|
||||||
private readonly dbWrite: PrismaWriteService,
|
private readonly dbWrite: PrismaWriteService,
|
||||||
private readonly selectedCalendarsRepository: SelectedCalendarsRepository
|
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
|
||||||
|
private readonly calendarsCacheService: CalendarsCacheService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private buildNonDelegationCredentials<TCredential>(credentials: TCredential[]) {
|
private buildNonDelegationCredentials<TCredential>(credentials: TCredential[]) {
|
||||||
@@ -52,11 +54,17 @@ export class CalendarsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getCalendars(userId: number) {
|
async getCalendars(userId: number) {
|
||||||
|
const cachedResult = await this.calendarsCacheService.getConnectedAndDestinationCalendarsCache(userId);
|
||||||
|
|
||||||
|
if (cachedResult) {
|
||||||
|
return cachedResult;
|
||||||
|
}
|
||||||
|
|
||||||
const userWithCalendars = await this.usersRepository.findByIdWithCalendars(userId);
|
const userWithCalendars = await this.usersRepository.findByIdWithCalendars(userId);
|
||||||
if (!userWithCalendars) {
|
if (!userWithCalendars) {
|
||||||
throw new NotFoundException("User not found");
|
throw new NotFoundException("User not found");
|
||||||
}
|
}
|
||||||
return getConnectedDestinationCalendarsAndEnsureDefaultsInDb({
|
const result = await getConnectedDestinationCalendarsAndEnsureDefaultsInDb({
|
||||||
user: {
|
user: {
|
||||||
...userWithCalendars,
|
...userWithCalendars,
|
||||||
allSelectedCalendars: userWithCalendars.selectedCalendars,
|
allSelectedCalendars: userWithCalendars.selectedCalendars,
|
||||||
@@ -68,6 +76,10 @@ export class CalendarsService {
|
|||||||
eventTypeId: null,
|
eventTypeId: null,
|
||||||
prisma: this.dbWrite.prisma as unknown as PrismaClient,
|
prisma: this.dbWrite.prisma as unknown as PrismaClient,
|
||||||
});
|
});
|
||||||
|
console.log("saving cache", JSON.stringify(result));
|
||||||
|
await this.calendarsCacheService.setConnectedAndDestinationCalendarsCache(userId, result);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getBusyTimes(
|
async getBusyTimes(
|
||||||
@@ -187,6 +199,8 @@ export class CalendarsService {
|
|||||||
userId,
|
userId,
|
||||||
calendarType
|
calendarType
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await this.calendarsCacheService.deleteConnectedAndDestinationCalendarsCache(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkCalendarCredentialValidity(userId: number, credentialId: number, type: string) {
|
async checkCalendarCredentialValidity(userId: number, credentialId: number, type: string) {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { ICSFeedCalendarApp } from "@/ee/calendars/calendars.interface";
|
import { ICSFeedCalendarApp } from "@/ee/calendars/calendars.interface";
|
||||||
import { CreateIcsFeedOutputResponseDto } from "@/ee/calendars/input/create-ics.output";
|
import { CreateIcsFeedOutputResponseDto } from "@/ee/calendars/input/create-ics.output";
|
||||||
|
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
|
||||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
|
import { RedisService } from "@/modules/redis/redis.service";
|
||||||
import { BadRequestException, UnauthorizedException, Logger } from "@nestjs/common";
|
import { BadRequestException, UnauthorizedException, Logger } from "@nestjs/common";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
@@ -13,7 +15,9 @@ import { IcsFeedCalendarService } from "@calcom/platform-libraries/app-store";
|
|||||||
export class IcsFeedService implements ICSFeedCalendarApp {
|
export class IcsFeedService implements ICSFeedCalendarApp {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly calendarsService: CalendarsService,
|
private readonly calendarsService: CalendarsService,
|
||||||
private readonly credentialRepository: CredentialsRepository
|
private readonly calendarsCacheService: CalendarsCacheService,
|
||||||
|
private readonly credentialRepository: CredentialsRepository,
|
||||||
|
private readonly redisService: RedisService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private logger = new Logger("IcsFeedService");
|
private logger = new Logger("IcsFeedService");
|
||||||
@@ -58,6 +62,9 @@ export class IcsFeedService implements ICSFeedCalendarApp {
|
|||||||
data.key,
|
data.key,
|
||||||
userId
|
userId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await this.calendarsCacheService.deleteConnectedAndDestinationCalendarsCache(userId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||||
import { EventTypeOwnershipGuard } from "@/modules/event-types/guards/event-type-ownership.guard";
|
import { EventTypeOwnershipGuard } from "@/modules/event-types/guards/event-type-ownership.guard";
|
||||||
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
|||||||
+263
-39
@@ -12,6 +12,7 @@ import { NestExpressApplication } from "@nestjs/platform-express";
|
|||||||
import { Test } from "@nestjs/testing";
|
import { Test } from "@nestjs/testing";
|
||||||
import { PlatformOAuthClient, Team, User, Schedule, EventType } from "@prisma/client";
|
import { PlatformOAuthClient, Team, User, Schedule, EventType } from "@prisma/client";
|
||||||
import * as request from "supertest";
|
import * as request from "supertest";
|
||||||
|
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
||||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||||
@@ -30,6 +31,7 @@ import {
|
|||||||
NoticeThresholdUnitEnum,
|
NoticeThresholdUnitEnum,
|
||||||
FrequencyInput,
|
FrequencyInput,
|
||||||
} from "@calcom/platform-enums";
|
} from "@calcom/platform-enums";
|
||||||
|
import { SchedulingType } from "@calcom/platform-libraries";
|
||||||
import {
|
import {
|
||||||
ApiSuccessResponse,
|
ApiSuccessResponse,
|
||||||
CreateEventTypeInput_2024_06_14,
|
CreateEventTypeInput_2024_06_14,
|
||||||
@@ -39,7 +41,6 @@ import {
|
|||||||
NotesDefaultFieldInput_2024_06_14,
|
NotesDefaultFieldInput_2024_06_14,
|
||||||
UpdateEventTypeInput_2024_06_14,
|
UpdateEventTypeInput_2024_06_14,
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
import { SchedulingType } from "@calcom/prisma/enums";
|
|
||||||
|
|
||||||
const orderBySlug = (a: { slug: string }, b: { slug: string }) => {
|
const orderBySlug = (a: { slug: string }, b: { slug: string }) => {
|
||||||
if (a.slug < b.slug) return -1;
|
if (a.slug < b.slug) return -1;
|
||||||
@@ -88,11 +89,16 @@ describe("Event types Endpoints", () => {
|
|||||||
let schedulesRepostoryFixture: SchedulesRepositoryFixture;
|
let schedulesRepostoryFixture: SchedulesRepositoryFixture;
|
||||||
let profileRepositoryFixture: ProfileRepositoryFixture;
|
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||||
|
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||||
|
let apiKeyString: string;
|
||||||
|
let apiKeyOrgUser: string;
|
||||||
|
|
||||||
const userEmail = `event-types-2024-06-14-user-${randomString()}@api.com`;
|
const userEmail = `event-types-2024-06-14-user-${randomString()}@api.com`;
|
||||||
const falseTestEmail = `event-types-2024-06-14-false-user-${randomString()}@api.com`;
|
const falseTestEmail = `event-types-2024-06-14-false-user-${randomString()}@api.com`;
|
||||||
const name = `event-types-2024-06-14-user-${randomString()}`;
|
const name = `event-types-2024-06-14-user-${randomString()}`;
|
||||||
const username = name;
|
const username = name;
|
||||||
let eventType: EventTypeOutput_2024_06_14;
|
let eventType: EventTypeOutput_2024_06_14;
|
||||||
|
let hiddenEventType: EventTypeOutput_2024_06_14;
|
||||||
let user: User;
|
let user: User;
|
||||||
let orgUser: User;
|
let orgUser: User;
|
||||||
let falseTestUser: User;
|
let falseTestUser: User;
|
||||||
@@ -165,13 +171,10 @@ describe("Event types Endpoints", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await withApiAuth(
|
const moduleRef = await Test.createTestingModule({
|
||||||
userEmail,
|
|
||||||
Test.createTestingModule({
|
|
||||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||||
imports: [AppModule, UsersModule, EventTypesModule_2024_06_14, TokensModule],
|
imports: [AppModule, UsersModule, EventTypesModule_2024_06_14, TokensModule],
|
||||||
})
|
})
|
||||||
)
|
|
||||||
.overrideGuard(PermissionsGuard)
|
.overrideGuard(PermissionsGuard)
|
||||||
.useValue({
|
.useValue({
|
||||||
canActivate: () => true,
|
canActivate: () => true,
|
||||||
@@ -198,12 +201,20 @@ describe("Event types Endpoints", () => {
|
|||||||
name,
|
name,
|
||||||
username,
|
username,
|
||||||
});
|
});
|
||||||
|
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
|
||||||
|
const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null);
|
||||||
|
apiKeyString = `cal_test_${keyString}`;
|
||||||
|
|
||||||
orgUser = await userRepositoryFixture.create({
|
orgUser = await userRepositoryFixture.create({
|
||||||
email: `event-types-2024-06-14-org-user-${randomString()}@example.com`,
|
email: `event-types-2024-06-14-org-user-${randomString()}@example.com`,
|
||||||
name: `event-types-2024-06-14-org-user-${randomString()}`,
|
name: `event-types-2024-06-14-org-user-${randomString()}`,
|
||||||
username: `event-types-2024-06-14-org-user-${randomString()}`,
|
username: `event-types-2024-06-14-org-user-${randomString()}`,
|
||||||
});
|
});
|
||||||
|
const { keyString: orgUserApiKeyString } = await apiKeysRepositoryFixture.createApiKey(
|
||||||
|
orgUser.id,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
apiKeyOrgUser = `cal_test_${orgUserApiKeyString}`;
|
||||||
|
|
||||||
profileRepositoryFixture.create({
|
profileRepositoryFixture.create({
|
||||||
uid: `usr-${orgUser.id}`,
|
uid: `usr-${orgUser.id}`,
|
||||||
@@ -334,6 +345,7 @@ describe("Event types Endpoints", () => {
|
|||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.post("/api/v2/event-types")
|
.post("/api/v2/event-types")
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
.send(body)
|
.send(body)
|
||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
@@ -370,6 +382,7 @@ describe("Event types Endpoints", () => {
|
|||||||
const response = await request(app.getHttpServer())
|
const response = await request(app.getHttpServer())
|
||||||
.post("/api/v2/event-types")
|
.post("/api/v2/event-types")
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
.send(body)
|
.send(body)
|
||||||
.expect(400);
|
.expect(400);
|
||||||
|
|
||||||
@@ -395,6 +408,7 @@ describe("Event types Endpoints", () => {
|
|||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.post("/api/v2/event-types")
|
.post("/api/v2/event-types")
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
.send(body)
|
.send(body)
|
||||||
.expect(400);
|
.expect(400);
|
||||||
});
|
});
|
||||||
@@ -498,11 +512,13 @@ describe("Event types Endpoints", () => {
|
|||||||
lightThemeHex: "#fafafa",
|
lightThemeHex: "#fafafa",
|
||||||
},
|
},
|
||||||
customName: `{Event type title} between {Organiser} and {Scheduler}`,
|
customName: `{Event type title} between {Organiser} and {Scheduler}`,
|
||||||
|
bookingRequiresAuthentication: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.post("/api/v2/event-types")
|
.post("/api/v2/event-types")
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
.send(body)
|
.send(body)
|
||||||
.expect(201)
|
.expect(201)
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
@@ -570,25 +586,57 @@ describe("Event types Endpoints", () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
expect(createdEventType.bookingFields).toEqual(expectedBookingFields);
|
expect(createdEventType.bookingFields).toEqual(expectedBookingFields);
|
||||||
|
expect(createdEventType.bookingRequiresAuthentication).toEqual(true);
|
||||||
eventType = responseBody.data;
|
eventType = responseBody.data;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should create a hidden event type", async () => {
|
||||||
|
const body: CreateEventTypeInput_2024_06_14 = {
|
||||||
|
title: "Coding class hidden",
|
||||||
|
slug: "coding-class-hidden",
|
||||||
|
description: "Let's learn how to code like a pro.",
|
||||||
|
lengthInMinutes: 60,
|
||||||
|
locations: [
|
||||||
|
{
|
||||||
|
type: "integration",
|
||||||
|
integration: "cal-video",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
hidden: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post("/api/v2/event-types")
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
|
.send(body)
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||||
|
const createdEventType = responseBody.data;
|
||||||
|
expect(createdEventType).toHaveProperty("id");
|
||||||
|
expect(createdEventType.title).toEqual(body.title);
|
||||||
|
expect(createdEventType.hidden).toEqual(body.hidden);
|
||||||
|
expect(createdEventType.ownerId).toEqual(user.id);
|
||||||
|
hiddenEventType = responseBody.data;
|
||||||
|
});
|
||||||
|
|
||||||
it(`/GET/event-types by username`, async () => {
|
it(`/GET/event-types by username`, async () => {
|
||||||
const response = await request(app.getHttpServer())
|
const response = await request(app.getHttpServer())
|
||||||
.get(`/api/v2/event-types?username=${username}`)
|
.get(`/api/v2/event-types?username=${user.username}`)
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
.set("Authorization", `Bearer whatever`)
|
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||||
|
|
||||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseBody.data).toBeDefined();
|
expect(responseBody.data).toBeDefined();
|
||||||
expect(responseBody.data?.length).toEqual(1);
|
expect(responseBody.data?.length).toEqual(2);
|
||||||
|
|
||||||
const fetchedEventType = responseBody.data?.[0];
|
const fetchedEventType = responseBody.data?.find((et) => et.id === eventType.id);
|
||||||
|
const fetchedHiddenEventType = responseBody.data?.find((et) => et.id === hiddenEventType.id);
|
||||||
|
|
||||||
expect(fetchedEventType?.id).toEqual(eventType.id);
|
expect(fetchedEventType?.id).toEqual(eventType.id);
|
||||||
expect(fetchedEventType?.title).toEqual(eventType.title);
|
expect(fetchedEventType?.title).toEqual(eventType.title);
|
||||||
@@ -600,33 +648,149 @@ describe("Event types Endpoints", () => {
|
|||||||
eventType.bookingFields.sort(orderBySlug).filter((f) => ("hidden" in f ? !f?.hidden : true))
|
eventType.bookingFields.sort(orderBySlug).filter((f) => ("hidden" in f ? !f?.hidden : true))
|
||||||
);
|
);
|
||||||
expect(fetchedEventType?.ownerId).toEqual(user.id);
|
expect(fetchedEventType?.ownerId).toEqual(user.id);
|
||||||
expect(fetchedEventType.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
|
expect(fetchedEventType?.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
|
||||||
expect(fetchedEventType.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
|
expect(fetchedEventType?.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
|
||||||
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
|
expect(fetchedEventType?.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
|
||||||
expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart);
|
expect(fetchedEventType?.offsetStart).toEqual(eventType.offsetStart);
|
||||||
expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow);
|
expect(fetchedEventType?.bookingWindow).toEqual(eventType.bookingWindow);
|
||||||
expect(fetchedEventType.bookerLayouts).toEqual(eventType.bookerLayouts);
|
expect(fetchedEventType?.bookerLayouts).toEqual(eventType.bookerLayouts);
|
||||||
expect(fetchedEventType.confirmationPolicy).toEqual(eventType.confirmationPolicy);
|
expect(fetchedEventType?.confirmationPolicy).toEqual(eventType.confirmationPolicy);
|
||||||
expect(fetchedEventType.recurrence).toEqual(eventType.recurrence);
|
expect(fetchedEventType?.recurrence).toEqual(eventType.recurrence);
|
||||||
expect(fetchedEventType.customName).toEqual(eventType.customName);
|
expect(fetchedEventType?.customName).toEqual(eventType.customName);
|
||||||
expect(fetchedEventType.requiresBookerEmailVerification).toEqual(
|
expect(fetchedEventType?.requiresBookerEmailVerification).toEqual(
|
||||||
eventType.requiresBookerEmailVerification
|
eventType.requiresBookerEmailVerification
|
||||||
);
|
);
|
||||||
expect(fetchedEventType.hideCalendarNotes).toEqual(eventType.hideCalendarNotes);
|
expect(fetchedEventType?.hideCalendarNotes).toEqual(eventType.hideCalendarNotes);
|
||||||
expect(fetchedEventType.hideCalendarEventDetails).toEqual(eventType.hideCalendarEventDetails);
|
expect(fetchedEventType?.hideCalendarEventDetails).toEqual(eventType.hideCalendarEventDetails);
|
||||||
expect(fetchedEventType.hideOrganizerEmail).toEqual(eventType.hideOrganizerEmail);
|
expect(fetchedEventType?.hideOrganizerEmail).toEqual(eventType.hideOrganizerEmail);
|
||||||
expect(fetchedEventType.lockTimeZoneToggleOnBookingPage).toEqual(
|
expect(fetchedEventType?.lockTimeZoneToggleOnBookingPage).toEqual(
|
||||||
eventType.lockTimeZoneToggleOnBookingPage
|
eventType.lockTimeZoneToggleOnBookingPage
|
||||||
);
|
);
|
||||||
expect(fetchedEventType.color).toEqual(eventType.color);
|
expect(fetchedEventType?.color).toEqual(eventType.color);
|
||||||
|
expect(fetchedEventType?.hidden).toEqual(false);
|
||||||
|
|
||||||
|
expect(fetchedHiddenEventType?.id).toEqual(hiddenEventType.id);
|
||||||
|
expect(fetchedHiddenEventType?.hidden).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/event-types by username should not return hidden event type if auth of non event type owner provided`, async () => {
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.get(`/api/v2/event-types?username=${user.username}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.set("Authorization", `Bearer ${apiKeyOrgUser}`)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||||
|
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseBody.data?.length).toEqual(1);
|
||||||
|
|
||||||
|
const fetchedEventType = responseBody.data?.find((et) => et.id === eventType.id);
|
||||||
|
|
||||||
|
expect(fetchedEventType?.id).toEqual(eventType.id);
|
||||||
|
expect(fetchedEventType?.ownerId).toEqual(user.id);
|
||||||
|
expect(fetchedEventType?.hidden).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/event-types by username should not return hidden event type if no auth provided`, async () => {
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.get(`/api/v2/event-types?username=${user.username}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||||
|
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseBody.data?.length).toEqual(1);
|
||||||
|
|
||||||
|
const fetchedEventType = responseBody.data?.find((et) => et.id === eventType.id);
|
||||||
|
|
||||||
|
expect(fetchedEventType?.id).toEqual(eventType.id);
|
||||||
|
expect(fetchedEventType?.ownerId).toEqual(user.id);
|
||||||
|
expect(fetchedEventType?.hidden).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/event-types by username and eventSlug`, async () => {
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.get(`/api/v2/event-types?username=${user.username}&eventSlug=${eventType.slug}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||||
|
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseBody.data?.length).toEqual(1);
|
||||||
|
|
||||||
|
const fetchedEventType = responseBody.data?.find((et) => et.id === eventType.id);
|
||||||
|
|
||||||
|
expect(fetchedEventType?.id).toEqual(eventType.id);
|
||||||
|
expect(fetchedEventType?.title).toEqual(eventType.title);
|
||||||
|
expect(fetchedEventType?.description).toEqual(eventType.description);
|
||||||
|
expect(fetchedEventType?.lengthInMinutes).toEqual(eventType.lengthInMinutes);
|
||||||
|
expect(fetchedEventType?.lengthInMinutesOptions).toEqual(eventType.lengthInMinutesOptions);
|
||||||
|
expect(fetchedEventType?.locations).toEqual(eventType.locations);
|
||||||
|
expect(fetchedEventType?.bookingFields.sort(orderBySlug)).toEqual(
|
||||||
|
eventType.bookingFields.sort(orderBySlug).filter((f) => ("hidden" in f ? !f?.hidden : true))
|
||||||
|
);
|
||||||
|
expect(fetchedEventType?.ownerId).toEqual(user.id);
|
||||||
|
expect(fetchedEventType?.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
|
||||||
|
expect(fetchedEventType?.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
|
||||||
|
expect(fetchedEventType?.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
|
||||||
|
expect(fetchedEventType?.offsetStart).toEqual(eventType.offsetStart);
|
||||||
|
expect(fetchedEventType?.bookingWindow).toEqual(eventType.bookingWindow);
|
||||||
|
expect(fetchedEventType?.bookerLayouts).toEqual(eventType.bookerLayouts);
|
||||||
|
expect(fetchedEventType?.confirmationPolicy).toEqual(eventType.confirmationPolicy);
|
||||||
|
expect(fetchedEventType?.recurrence).toEqual(eventType.recurrence);
|
||||||
|
expect(fetchedEventType?.customName).toEqual(eventType.customName);
|
||||||
|
expect(fetchedEventType?.requiresBookerEmailVerification).toEqual(
|
||||||
|
eventType.requiresBookerEmailVerification
|
||||||
|
);
|
||||||
|
expect(fetchedEventType?.hideCalendarNotes).toEqual(eventType.hideCalendarNotes);
|
||||||
|
expect(fetchedEventType?.hideCalendarEventDetails).toEqual(eventType.hideCalendarEventDetails);
|
||||||
|
expect(fetchedEventType?.hideOrganizerEmail).toEqual(eventType.hideOrganizerEmail);
|
||||||
|
expect(fetchedEventType?.lockTimeZoneToggleOnBookingPage).toEqual(
|
||||||
|
eventType.lockTimeZoneToggleOnBookingPage
|
||||||
|
);
|
||||||
|
expect(fetchedEventType?.color).toEqual(eventType.color);
|
||||||
|
expect(fetchedEventType?.hidden).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/event-types by username and eventSlug should not return hidden event type if auth of non event type owner provided`, async () => {
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.get(`/api/v2/event-types?username=${user.username}&eventSlug=${hiddenEventType.slug}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.set("Authorization", `Bearer ${apiKeyOrgUser}`)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||||
|
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseBody.data?.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/event-types by username and eventSlug should not return hidden event type if no auth provided`, async () => {
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.get(`/api/v2/event-types?username=${user.username}&eventSlug=${hiddenEventType.slug}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||||
|
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseBody.data?.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`/GET/event-types by username and orgSlug`, async () => {
|
it(`/GET/event-types by username and orgSlug`, async () => {
|
||||||
const response = await request(app.getHttpServer())
|
const response = await request(app.getHttpServer())
|
||||||
.get(`/api/v2/event-types?username=${orgUser.username}&orgSlug=${organization.slug}`)
|
.get(`/api/v2/event-types?username=${orgUser.username}&orgSlug=${organization.slug}`)
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
.set("Authorization", `Bearer whatever`)
|
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||||
@@ -645,8 +809,7 @@ describe("Event types Endpoints", () => {
|
|||||||
`/api/v2/event-types?username=${orgUser.username}&orgSlug=${organization.slug}&eventSlug=${orgUserEventType1.slug}`
|
`/api/v2/event-types?username=${orgUser.username}&orgSlug=${organization.slug}&eventSlug=${orgUserEventType1.slug}`
|
||||||
)
|
)
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
.set("Authorization", `Bearer whatever`)
|
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||||
@@ -661,8 +824,7 @@ describe("Event types Endpoints", () => {
|
|||||||
const response = await request(app.getHttpServer())
|
const response = await request(app.getHttpServer())
|
||||||
.get(`/api/v2/event-types?username=${orgUser.username}&orgId=${organization.id}`)
|
.get(`/api/v2/event-types?username=${orgUser.username}&orgId=${organization.id}`)
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
.set("Authorization", `Bearer whatever`)
|
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||||
@@ -703,6 +865,7 @@ describe("Event types Endpoints", () => {
|
|||||||
await request(app.getHttpServer())
|
await request(app.getHttpServer())
|
||||||
.post("/api/v2/event-types")
|
.post("/api/v2/event-types")
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
.send(body)
|
.send(body)
|
||||||
.expect(400);
|
.expect(400);
|
||||||
});
|
});
|
||||||
@@ -730,6 +893,7 @@ describe("Event types Endpoints", () => {
|
|||||||
const createResponse = await request(app.getHttpServer())
|
const createResponse = await request(app.getHttpServer())
|
||||||
.post("/api/v2/event-types")
|
.post("/api/v2/event-types")
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
.send(body)
|
.send(body)
|
||||||
.expect(201);
|
.expect(201);
|
||||||
|
|
||||||
@@ -746,6 +910,7 @@ describe("Event types Endpoints", () => {
|
|||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.patch(`/api/v2/event-types/${createdEventType.id}`)
|
.patch(`/api/v2/event-types/${createdEventType.id}`)
|
||||||
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
.send({
|
.send({
|
||||||
seats: {
|
seats: {
|
||||||
@@ -777,6 +942,7 @@ describe("Event types Endpoints", () => {
|
|||||||
|
|
||||||
await request(app.getHttpServer())
|
await request(app.getHttpServer())
|
||||||
.post("/api/v2/event-types")
|
.post("/api/v2/event-types")
|
||||||
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
.send(body)
|
.send(body)
|
||||||
.expect(400);
|
.expect(400);
|
||||||
@@ -797,6 +963,7 @@ describe("Event types Endpoints", () => {
|
|||||||
|
|
||||||
const createResponse = await request(app.getHttpServer())
|
const createResponse = await request(app.getHttpServer())
|
||||||
.post("/api/v2/event-types")
|
.post("/api/v2/event-types")
|
||||||
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
.send(body)
|
.send(body)
|
||||||
.expect(201);
|
.expect(201);
|
||||||
@@ -815,6 +982,7 @@ describe("Event types Endpoints", () => {
|
|||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.patch(`/api/v2/event-types/${createdEventType.id}`)
|
.patch(`/api/v2/event-types/${createdEventType.id}`)
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
.send({
|
.send({
|
||||||
seats: {
|
seats: {
|
||||||
seatsPerTimeSlot: 4,
|
seatsPerTimeSlot: 4,
|
||||||
@@ -842,6 +1010,7 @@ describe("Event types Endpoints", () => {
|
|||||||
const createResponse = await request(app.getHttpServer())
|
const createResponse = await request(app.getHttpServer())
|
||||||
.post("/api/v2/event-types")
|
.post("/api/v2/event-types")
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
.send(body)
|
.send(body)
|
||||||
.expect(201);
|
.expect(201);
|
||||||
|
|
||||||
@@ -858,6 +1027,7 @@ describe("Event types Endpoints", () => {
|
|||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.patch(`/api/v2/event-types/${createdEventType.id}`)
|
.patch(`/api/v2/event-types/${createdEventType.id}`)
|
||||||
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
.send({
|
.send({
|
||||||
locations: [
|
locations: [
|
||||||
@@ -895,6 +1065,7 @@ describe("Event types Endpoints", () => {
|
|||||||
|
|
||||||
await request(app.getHttpServer())
|
await request(app.getHttpServer())
|
||||||
.post("/api/v2/event-types")
|
.post("/api/v2/event-types")
|
||||||
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
.send(body)
|
.send(body)
|
||||||
.expect(400);
|
.expect(400);
|
||||||
@@ -916,6 +1087,7 @@ describe("Event types Endpoints", () => {
|
|||||||
|
|
||||||
const createResponse = await request(app.getHttpServer())
|
const createResponse = await request(app.getHttpServer())
|
||||||
.post("/api/v2/event-types")
|
.post("/api/v2/event-types")
|
||||||
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
.send(body)
|
.send(body)
|
||||||
.expect(201);
|
.expect(201);
|
||||||
@@ -933,6 +1105,7 @@ describe("Event types Endpoints", () => {
|
|||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.patch(`/api/v2/event-types/${createdEventType.id}`)
|
.patch(`/api/v2/event-types/${createdEventType.id}`)
|
||||||
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
.send({
|
.send({
|
||||||
confirmationPolicy: {
|
confirmationPolicy: {
|
||||||
@@ -1026,11 +1199,13 @@ describe("Event types Endpoints", () => {
|
|||||||
lightThemeHex: "#fafafa",
|
lightThemeHex: "#fafafa",
|
||||||
},
|
},
|
||||||
customName: `{Event type title} betweennnnnnnnnnn {Organiser} and {Scheduler}`,
|
customName: `{Event type title} betweennnnnnnnnnn {Organiser} and {Scheduler}`,
|
||||||
|
bookingRequiresAuthentication: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.patch(`/api/v2/event-types/${eventType.id}`)
|
.patch(`/api/v2/event-types/${eventType.id}`)
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
.send(body)
|
.send(body)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
@@ -1120,6 +1295,8 @@ describe("Event types Endpoints", () => {
|
|||||||
eventType.color = updatedEventType.color;
|
eventType.color = updatedEventType.color;
|
||||||
eventType.bookingFields = updatedEventType.bookingFields;
|
eventType.bookingFields = updatedEventType.bookingFields;
|
||||||
eventType.calVideoSettings = updatedEventType.calVideoSettings;
|
eventType.calVideoSettings = updatedEventType.calVideoSettings;
|
||||||
|
|
||||||
|
expect(updatedEventType.bookingRequiresAuthentication).toEqual(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1130,6 +1307,7 @@ describe("Event types Endpoints", () => {
|
|||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.patch(`/api/v2/event-types/${eventType.id}`)
|
.patch(`/api/v2/event-types/${eventType.id}`)
|
||||||
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
.send(body)
|
.send(body)
|
||||||
.expect(404);
|
.expect(404);
|
||||||
@@ -1147,6 +1325,7 @@ describe("Event types Endpoints", () => {
|
|||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.patch(`/api/v2/event-types/${eventType.id}`)
|
.patch(`/api/v2/event-types/${eventType.id}`)
|
||||||
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
.send(body)
|
.send(body)
|
||||||
.expect(400);
|
.expect(400);
|
||||||
@@ -1156,8 +1335,7 @@ describe("Event types Endpoints", () => {
|
|||||||
const response = await request(app.getHttpServer())
|
const response = await request(app.getHttpServer())
|
||||||
.get(`/api/v2/event-types/${eventType.id}`)
|
.get(`/api/v2/event-types/${eventType.id}`)
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
.set("Authorization", `Bearer whatever`)
|
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||||
@@ -1198,8 +1376,7 @@ describe("Event types Endpoints", () => {
|
|||||||
const response = await request(app.getHttpServer())
|
const response = await request(app.getHttpServer())
|
||||||
.get(`/api/v2/event-types?username=${username}&eventSlug=${eventType.slug}`)
|
.get(`/api/v2/event-types?username=${username}&eventSlug=${eventType.slug}`)
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
.set("Authorization", `Bearer whatever`)
|
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||||
@@ -1240,13 +1417,16 @@ describe("Event types Endpoints", () => {
|
|||||||
await request(app.getHttpServer())
|
await request(app.getHttpServer())
|
||||||
.get(`/api/v2/event-types/1000`)
|
.get(`/api/v2/event-types/1000`)
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
.set("Authorization", `Bearer whatever`)
|
|
||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should delete event type", async () => {
|
it("should delete event type", async () => {
|
||||||
return request(app.getHttpServer()).delete(`/api/v2/event-types/${eventType.id}`).expect(200);
|
return request(app.getHttpServer())
|
||||||
|
.delete(`/api/v2/event-types/${eventType.id}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
|
.expect(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@@ -2028,7 +2208,6 @@ describe("Event types Endpoints", () => {
|
|||||||
const username = name;
|
const username = name;
|
||||||
let user: User;
|
let user: User;
|
||||||
let legacyEventTypeId1: number;
|
let legacyEventTypeId1: number;
|
||||||
let legacyEventTypeId2: number;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await withApiAuth(
|
const moduleRef = await withApiAuth(
|
||||||
@@ -2147,6 +2326,51 @@ describe("Event types Endpoints", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("EventType Hidden Property", () => {
|
||||||
|
let createdEventTypeId: number;
|
||||||
|
|
||||||
|
it("should create an event type with hidden=true", async () => {
|
||||||
|
const createPayload = {
|
||||||
|
title: "Hidden Event",
|
||||||
|
slug: "hidden-event",
|
||||||
|
lengthInMinutes: 30,
|
||||||
|
hidden: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post("/api/v2/event-types")
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.send(createPayload)
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||||
|
const createdEventType = responseBody.data;
|
||||||
|
expect(createdEventType).toHaveProperty("id");
|
||||||
|
expect(createdEventType.title).toEqual(createPayload.title);
|
||||||
|
expect(createdEventType.slug).toEqual(createPayload.slug);
|
||||||
|
expect(createdEventType.lengthInMinutes).toEqual(createPayload.lengthInMinutes);
|
||||||
|
expect(createdEventType.hidden).toBe(true);
|
||||||
|
|
||||||
|
createdEventTypeId = createdEventType.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update the hidden property to false", async () => {
|
||||||
|
const updatePayload = {
|
||||||
|
hidden: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.patch(`/api/v2/event-types/${createdEventTypeId}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.send(updatePayload)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||||
|
const updatedEventType = responseBody.data;
|
||||||
|
+expect(updatedEventType.hidden).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||||
await teamRepositoryFixture.delete(organization.id);
|
await teamRepositoryFixture.delete(organization.id);
|
||||||
|
|||||||
+30
-26
@@ -6,11 +6,22 @@ import { UpdateEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2
|
|||||||
import { EventTypeResponseTransformPipe } from "@/ee/event-types/event-types_2024_06_14/pipes/event-type-response.transformer";
|
import { EventTypeResponseTransformPipe } from "@/ee/event-types/event-types_2024_06_14/pipes/event-type-response.transformer";
|
||||||
import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service";
|
import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service";
|
||||||
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
|
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
|
||||||
|
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
|
||||||
import { VERSION_2024_06_14_VALUE } from "@/lib/api-versions";
|
import { VERSION_2024_06_14_VALUE } from "@/lib/api-versions";
|
||||||
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
import {
|
||||||
|
API_KEY_OR_ACCESS_TOKEN_HEADER,
|
||||||
|
OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER,
|
||||||
|
OPTIONAL_X_CAL_CLIENT_ID_HEADER,
|
||||||
|
OPTIONAL_X_CAL_SECRET_KEY_HEADER,
|
||||||
|
} from "@/lib/docs/headers";
|
||||||
|
import {
|
||||||
|
AuthOptionalUser,
|
||||||
|
GetOptionalUser,
|
||||||
|
} from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
|
||||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
|
import { OptionalApiAuthGuard } from "@/modules/auth/guards/optional-api-auth/optional-api-auth.guard";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import {
|
import {
|
||||||
@@ -40,7 +51,6 @@ import {
|
|||||||
UpdateEventTypeInput_2024_06_14,
|
UpdateEventTypeInput_2024_06_14,
|
||||||
GetEventTypesQuery_2024_06_14,
|
GetEventTypesQuery_2024_06_14,
|
||||||
CreateEventTypeInput_2024_06_14,
|
CreateEventTypeInput_2024_06_14,
|
||||||
EventTypeOutput_2024_06_14,
|
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
@@ -62,7 +72,8 @@ export class EventTypesController_2024_06_14 {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly eventTypesService: EventTypesService_2024_06_14,
|
private readonly eventTypesService: EventTypesService_2024_06_14,
|
||||||
private readonly inputEventTypesService: InputEventTypesService_2024_06_14,
|
private readonly inputEventTypesService: InputEventTypesService_2024_06_14,
|
||||||
private readonly eventTypeResponseTransformPipe: EventTypeResponseTransformPipe
|
private readonly eventTypeResponseTransformPipe: EventTypeResponseTransformPipe,
|
||||||
|
private readonly outputEventTypesService: OutputEventTypesService_2024_06_14
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post("/")
|
@Post("/")
|
||||||
@@ -109,30 +120,23 @@ export class EventTypesController_2024_06_14 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get("/")
|
@Get("/")
|
||||||
@ApiOperation({ summary: "Get all event types" })
|
@ApiOperation({
|
||||||
async getEventTypes(
|
summary: "Get all event types",
|
||||||
@Query() queryParams: GetEventTypesQuery_2024_06_14
|
description:
|
||||||
): Promise<GetEventTypesOutput_2024_06_14> {
|
"Hidden event types are returned only if authentication is provided and it belongs to the event type owner.",
|
||||||
const eventTypes = await this.eventTypesService.getEventTypes(queryParams);
|
|
||||||
if (!eventTypes || eventTypes.length === 0) {
|
|
||||||
throw new NotFoundException(`Event types not found`);
|
|
||||||
}
|
|
||||||
const eventTypesFormatted = this.eventTypeResponseTransformPipe.transform(eventTypes);
|
|
||||||
const eventTypesWithoutHiddenFields = eventTypesFormatted.map((eventType) => {
|
|
||||||
return {
|
|
||||||
...eventType,
|
|
||||||
bookingFields: Array.isArray(eventType?.bookingFields)
|
|
||||||
? eventType?.bookingFields
|
|
||||||
.map((field) => {
|
|
||||||
if ("hidden" in field) {
|
|
||||||
return field.hidden !== true ? field : null;
|
|
||||||
}
|
|
||||||
return field;
|
|
||||||
})
|
})
|
||||||
.filter((f) => f)
|
@UseGuards(OptionalApiAuthGuard)
|
||||||
: [],
|
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
|
||||||
};
|
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
|
||||||
}) as EventTypeOutput_2024_06_14[];
|
@ApiHeader(OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
async getEventTypes(
|
||||||
|
@Query() queryParams: GetEventTypesQuery_2024_06_14,
|
||||||
|
@GetOptionalUser() authUser: AuthOptionalUser
|
||||||
|
): Promise<GetEventTypesOutput_2024_06_14> {
|
||||||
|
const eventTypes = await this.eventTypesService.getEventTypes(queryParams, authUser);
|
||||||
|
const eventTypesFormatted = this.eventTypeResponseTransformPipe.transform(eventTypes);
|
||||||
|
const eventTypesWithoutHiddenFields =
|
||||||
|
this.outputEventTypesService.getResponseEventTypesWithoutHiddenFields(eventTypesFormatted);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||||
|
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
|
||||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||||
import { EventTypesController_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/controllers/event-types.controller";
|
import { EventTypesController_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/controllers/event-types.controller";
|
||||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||||
@@ -11,6 +12,7 @@ import { AppsRepository } from "@/modules/apps/apps.repository";
|
|||||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { RedisModule } from "@/modules/redis/redis.module";
|
||||||
import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module";
|
import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module";
|
||||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
import { UsersService } from "@/modules/users/services/users.service";
|
import { UsersService } from "@/modules/users/services/users.service";
|
||||||
@@ -18,7 +20,7 @@ import { UsersRepository } from "@/modules/users/users.repository";
|
|||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, MembershipsModule, TokensModule, SelectedCalendarsModule],
|
imports: [PrismaModule, MembershipsModule, TokensModule, SelectedCalendarsModule, RedisModule],
|
||||||
providers: [
|
providers: [
|
||||||
EventTypesRepository_2024_06_14,
|
EventTypesRepository_2024_06_14,
|
||||||
EventTypesService_2024_06_14,
|
EventTypesService_2024_06_14,
|
||||||
@@ -29,6 +31,7 @@ import { Module } from "@nestjs/common";
|
|||||||
SchedulesRepository_2024_06_11,
|
SchedulesRepository_2024_06_11,
|
||||||
EventTypeResponseTransformPipe,
|
EventTypeResponseTransformPipe,
|
||||||
CalendarsService,
|
CalendarsService,
|
||||||
|
CalendarsCacheService,
|
||||||
CredentialsRepository,
|
CredentialsRepository,
|
||||||
AppsRepository,
|
AppsRepository,
|
||||||
CalendarsRepository,
|
CalendarsRepository,
|
||||||
|
|||||||
@@ -75,6 +75,16 @@ export class EventTypesRepository_2024_06_14 {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUserEventTypesPublic(userId: number) {
|
||||||
|
return this.dbRead.prisma.eventType.findMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
hidden: false,
|
||||||
|
},
|
||||||
|
include: { users: true, schedule: true, destinationCalendar: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async getEventTypeById(eventTypeId: number) {
|
async getEventTypeById(eventTypeId: number) {
|
||||||
return this.dbRead.prisma.eventType.findUnique({
|
return this.dbRead.prisma.eventType.findUnique({
|
||||||
where: { id: eventTypeId },
|
where: { id: eventTypeId },
|
||||||
@@ -132,4 +142,26 @@ export class EventTypesRepository_2024_06_14 {
|
|||||||
async deleteEventType(eventTypeId: number) {
|
async deleteEventType(eventTypeId: number) {
|
||||||
return this.dbWrite.prisma.eventType.delete({ where: { id: eventTypeId } });
|
return this.dbWrite.prisma.eventType.delete({ where: { id: eventTypeId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async isUserHostOfEventType(userId: number, eventTypeId: number) {
|
||||||
|
const eventType = await this.dbRead.prisma.eventType.findFirst({
|
||||||
|
where: {
|
||||||
|
id: eventTypeId,
|
||||||
|
hosts: { some: { userId: userId } },
|
||||||
|
},
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
return !!eventType;
|
||||||
|
}
|
||||||
|
|
||||||
|
async isUserAssignedToEventType(userId: number, eventTypeId: number) {
|
||||||
|
const eventType = await this.dbRead.prisma.eventType.findFirst({
|
||||||
|
where: {
|
||||||
|
id: eventTypeId,
|
||||||
|
users: { some: { id: userId } },
|
||||||
|
},
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
return !!eventType;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-13
@@ -3,6 +3,7 @@ import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_20
|
|||||||
import { InputEventTransformed_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/transformed";
|
import { InputEventTransformed_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/transformed";
|
||||||
import { SystemField, CustomField } from "@/ee/event-types/event-types_2024_06_14/transformers";
|
import { SystemField, CustomField } from "@/ee/event-types/event-types_2024_06_14/transformers";
|
||||||
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
||||||
|
import { AuthOptionalUser } from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
|
||||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||||
@@ -38,6 +39,7 @@ export class EventTypesService_2024_06_14 {
|
|||||||
}
|
}
|
||||||
await this.checkCanCreateEventType(user.id, body);
|
await this.checkCanCreateEventType(user.id, body);
|
||||||
const eventTypeUser = await this.getUserToCreateEvent(user);
|
const eventTypeUser = await this.getUserToCreateEvent(user);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { destinationCalendar: _destinationCalendar, ...rest } = body;
|
const { destinationCalendar: _destinationCalendar, ...rest } = body;
|
||||||
|
|
||||||
const { eventType: eventTypeCreated } = await createEventType({
|
const { eventType: eventTypeCreated } = await createEventType({
|
||||||
@@ -93,34 +95,47 @@ export class EventTypesService_2024_06_14 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEventTypeByUsernameAndSlug(
|
async getEventTypeByUsernameAndSlug(params: {
|
||||||
username: string,
|
username: string;
|
||||||
eventTypeSlug: string,
|
eventTypeSlug: string;
|
||||||
orgSlug?: string,
|
orgSlug?: string;
|
||||||
orgId?: number
|
orgId?: number;
|
||||||
) {
|
authUser?: AuthOptionalUser;
|
||||||
const user = await this.usersRepository.findByUsername(username, orgSlug, orgId);
|
}) {
|
||||||
|
const user = await this.usersRepository.findByUsername(params.username, params.orgSlug, params.orgId);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventType = await this.eventTypesRepository.getUserEventTypeBySlug(user.id, eventTypeSlug);
|
const eventType = await this.eventTypesRepository.getUserEventTypeBySlug(user.id, params.eventTypeSlug);
|
||||||
|
|
||||||
if (!eventType) {
|
if (!eventType) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (eventType.hidden && params.authUser?.id !== user.id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ownerId: user.id,
|
ownerId: user.id,
|
||||||
...eventType,
|
...eventType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEventTypesByUsername(username: string, orgSlug?: string, orgId?: number) {
|
async getEventTypesByUsername(params: {
|
||||||
const user = await this.usersRepository.findByUsername(username, orgSlug, orgId);
|
username: string;
|
||||||
|
orgSlug?: string;
|
||||||
|
orgId?: number;
|
||||||
|
authUser?: AuthOptionalUser;
|
||||||
|
}) {
|
||||||
|
const user = await this.usersRepository.findByUsername(params.username, params.orgSlug, params.orgId);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
if (params.authUser?.id !== user.id) {
|
||||||
|
return await this.getUserEventTypesPublic(user.id);
|
||||||
|
}
|
||||||
return await this.getUserEventTypes(user.id);
|
return await this.getUserEventTypes(user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,6 +187,14 @@ export class EventTypesService_2024_06_14 {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUserEventTypesPublic(userId: number) {
|
||||||
|
const eventTypes = await this.eventTypesRepository.getUserEventTypesPublic(userId);
|
||||||
|
|
||||||
|
return eventTypes.map((eventType) => {
|
||||||
|
return { ownerId: userId, ...eventType };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async getEventTypesPublicByUsername(username: string): Promise<EventTypesPublic> {
|
async getEventTypesPublicByUsername(username: string): Promise<EventTypesPublic> {
|
||||||
const user = await this.usersRepository.findByUsername(username);
|
const user = await this.usersRepository.findByUsername(username);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@@ -181,15 +204,26 @@ export class EventTypesService_2024_06_14 {
|
|||||||
return await getEventTypesPublic(user.id);
|
return await getEventTypesPublic(user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEventTypes(queryParams: GetEventTypesQuery_2024_06_14) {
|
async getEventTypes(queryParams: GetEventTypesQuery_2024_06_14, authUser?: AuthOptionalUser) {
|
||||||
const { username, eventSlug, usernames, orgSlug, orgId } = queryParams;
|
const { username, eventSlug, usernames, orgSlug, orgId } = queryParams;
|
||||||
if (username && eventSlug) {
|
if (username && eventSlug) {
|
||||||
const eventType = await this.getEventTypeByUsernameAndSlug(username, eventSlug, orgSlug, orgId);
|
const eventType = await this.getEventTypeByUsernameAndSlug({
|
||||||
|
username,
|
||||||
|
eventTypeSlug: eventSlug,
|
||||||
|
orgSlug,
|
||||||
|
orgId,
|
||||||
|
authUser,
|
||||||
|
});
|
||||||
return eventType ? [eventType] : [];
|
return eventType ? [eventType] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (username) {
|
if (username) {
|
||||||
return await this.getEventTypesByUsername(username, orgSlug, orgId);
|
return await this.getEventTypesByUsername({
|
||||||
|
username,
|
||||||
|
orgSlug,
|
||||||
|
orgId,
|
||||||
|
authUser,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (usernames) {
|
if (usernames) {
|
||||||
|
|||||||
+30
-1
@@ -31,6 +31,7 @@ import {
|
|||||||
EventTypeOutput_2024_06_14,
|
EventTypeOutput_2024_06_14,
|
||||||
OutputUnknownLocation_2024_06_14,
|
OutputUnknownLocation_2024_06_14,
|
||||||
OutputUnknownBookingField_2024_06_14,
|
OutputUnknownBookingField_2024_06_14,
|
||||||
|
OutputBookingField_2024_06_14,
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
|
|
||||||
type EventTypeRelations = {
|
type EventTypeRelations = {
|
||||||
@@ -89,6 +90,8 @@ type Input = Pick<
|
|||||||
| "hideCalendarEventDetails"
|
| "hideCalendarEventDetails"
|
||||||
| "hideOrganizerEmail"
|
| "hideOrganizerEmail"
|
||||||
| "calVideoSettings"
|
| "calVideoSettings"
|
||||||
|
| "hidden"
|
||||||
|
| "bookingRequiresAuthentication"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -127,6 +130,8 @@ export class OutputEventTypesService_2024_06_14 {
|
|||||||
hideCalendarEventDetails,
|
hideCalendarEventDetails,
|
||||||
hideOrganizerEmail,
|
hideOrganizerEmail,
|
||||||
calVideoSettings,
|
calVideoSettings,
|
||||||
|
hidden,
|
||||||
|
bookingRequiresAuthentication,
|
||||||
} = databaseEventType;
|
} = databaseEventType;
|
||||||
|
|
||||||
const locations = this.transformLocations(databaseEventType.locations);
|
const locations = this.transformLocations(databaseEventType.locations);
|
||||||
@@ -203,6 +208,8 @@ export class OutputEventTypesService_2024_06_14 {
|
|||||||
hideCalendarEventDetails,
|
hideCalendarEventDetails,
|
||||||
hideOrganizerEmail,
|
hideOrganizerEmail,
|
||||||
calVideoSettings,
|
calVideoSettings,
|
||||||
|
hidden,
|
||||||
|
bookingRequiresAuthentication,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +254,7 @@ export class OutputEventTypesService_2024_06_14 {
|
|||||||
type: "unknown",
|
type: "unknown",
|
||||||
slug: "unknown",
|
slug: "unknown",
|
||||||
bookingField: JSON.stringify(bookingField),
|
bookingField: JSON.stringify(bookingField),
|
||||||
});
|
} satisfies OutputUnknownBookingField_2024_06_14);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,4 +345,26 @@ export class OutputEventTypesService_2024_06_14 {
|
|||||||
seatsShowAvailabilityCount: !!seatsShowAvailabilityCount,
|
seatsShowAvailabilityCount: !!seatsShowAvailabilityCount,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getResponseEventTypesWithoutHiddenFields(
|
||||||
|
eventTypes: EventTypeOutput_2024_06_14[]
|
||||||
|
): EventTypeOutput_2024_06_14[] {
|
||||||
|
return eventTypes.map((eventType) => this.getResponseEventTypeWithoutHiddenFields(eventType));
|
||||||
|
}
|
||||||
|
|
||||||
|
getResponseEventTypeWithoutHiddenFields(eventType: EventTypeOutput_2024_06_14): EventTypeOutput_2024_06_14 {
|
||||||
|
if (!Array.isArray(eventType?.bookingFields) || eventType.bookingFields.length === 0) return eventType;
|
||||||
|
|
||||||
|
const visibleBookingFields: OutputBookingField_2024_06_14[] = [];
|
||||||
|
for (const bookingField of eventType.bookingFields) {
|
||||||
|
if ("hidden" in bookingField && bookingField.hidden === true) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
visibleBookingFields.push(bookingField);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...eventType,
|
||||||
|
bookingFields: visibleBookingFields,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -90,7 +90,7 @@ function getBaseProperties(field: InputBookingField): CustomField | SystemField
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (fieldIsCustomSystemName(field)) {
|
if (fieldIsCustomSystemName(field)) {
|
||||||
const systemName = { ...systemBeforeFieldName };
|
const systemName = structuredClone(systemBeforeFieldName);
|
||||||
if (systemName.variantsConfig?.variants?.fullName?.fields?.[0]) {
|
if (systemName.variantsConfig?.variants?.fullName?.fields?.[0]) {
|
||||||
systemName.variantsConfig.variants.fullName.fields[0].label = field.label;
|
systemName.variantsConfig.variants.fullName.fields[0].label = field.label;
|
||||||
}
|
}
|
||||||
@@ -110,7 +110,7 @@ function getBaseProperties(field: InputBookingField): CustomField | SystemField
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (fieldIsCustomSystemNameSplit(field)) {
|
if (fieldIsCustomSystemNameSplit(field)) {
|
||||||
const systemNameSplit = { ...systemBeforeFieldNameSplit };
|
const systemNameSplit = structuredClone(systemBeforeFieldNameSplit);
|
||||||
|
|
||||||
const firstNameField = systemNameSplit.variantsConfig?.variants?.firstAndLastName?.fields?.find(
|
const firstNameField = systemNameSplit.variantsConfig?.variants?.firstAndLastName?.fields?.find(
|
||||||
(field) => field.name === "firstName"
|
(field) => field.name === "firstName"
|
||||||
|
|||||||
+1
@@ -520,6 +520,7 @@ export const systemBeforeFieldName: NameSystemField = {
|
|||||||
name: "fullName",
|
name: "fullName",
|
||||||
type: "text",
|
type: "text",
|
||||||
required: true,
|
required: true,
|
||||||
|
label: "your_name",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -292,7 +292,7 @@ describe("transformBookingFieldsInternalToApi", () => {
|
|||||||
isDefault: true,
|
isDefault: true,
|
||||||
required: true,
|
required: true,
|
||||||
disableOnPrefill: false,
|
disableOnPrefill: false,
|
||||||
label: undefined,
|
label: "your_name",
|
||||||
placeholder: undefined,
|
placeholder: undefined,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||||
|
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
|
||||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||||
import { GcalController } from "@/ee/gcal/gcal.controller";
|
import { GcalController } from "@/ee/gcal/gcal.controller";
|
||||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||||
@@ -6,6 +7,7 @@ import { GCalService } from "@/modules/apps/services/gcal.service";
|
|||||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { RedisModule } from "@/modules/redis/redis.module";
|
||||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
import { UsersRepository } from "@/modules/users/users.repository";
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
@@ -13,7 +15,7 @@ import { Module } from "@nestjs/common";
|
|||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, TokensModule, OAuthClientModule],
|
imports: [PrismaModule, TokensModule, OAuthClientModule, RedisModule],
|
||||||
providers: [
|
providers: [
|
||||||
AppsRepository,
|
AppsRepository,
|
||||||
ConfigService,
|
ConfigService,
|
||||||
@@ -21,6 +23,7 @@ import { ConfigService } from "@nestjs/config";
|
|||||||
SelectedCalendarsRepository,
|
SelectedCalendarsRepository,
|
||||||
GCalService,
|
GCalService,
|
||||||
CalendarsService,
|
CalendarsService,
|
||||||
|
CalendarsCacheService,
|
||||||
UsersRepository,
|
UsersRepository,
|
||||||
CalendarsRepository,
|
CalendarsRepository,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -229,4 +229,19 @@ export class SchedulesRepository_2024_06_11 {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getSchedulesByUserIds(userIds: number[], skip: number, take: number) {
|
||||||
|
return this.dbRead.prisma.schedule.findMany({
|
||||||
|
where: {
|
||||||
|
userId: {
|
||||||
|
in: userIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
availability: true,
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,7 @@ import { extractUserContext } from "@/lib/extract-user-context";
|
|||||||
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
||||||
import type { ArgumentsHost, ExceptionFilter } from "@nestjs/common";
|
import type { ArgumentsHost, ExceptionFilter } from "@nestjs/common";
|
||||||
import { Catch, HttpStatus, Logger } from "@nestjs/common";
|
import { Catch, HttpStatus, Logger } from "@nestjs/common";
|
||||||
import {
|
import { Prisma } from "@prisma/client";
|
||||||
PrismaClientInitializationError,
|
|
||||||
PrismaClientKnownRequestError,
|
|
||||||
PrismaClientRustPanicError,
|
|
||||||
PrismaClientUnknownRequestError,
|
|
||||||
PrismaClientValidationError,
|
|
||||||
} from "@prisma/client/runtime/library";
|
|
||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -21,18 +15,18 @@ import {
|
|||||||
import { Response } from "@calcom/platform-types";
|
import { Response } from "@calcom/platform-types";
|
||||||
|
|
||||||
type PrismaError =
|
type PrismaError =
|
||||||
| PrismaClientInitializationError
|
| Prisma.PrismaClientInitializationError
|
||||||
| PrismaClientKnownRequestError
|
| Prisma.PrismaClientKnownRequestError
|
||||||
| PrismaClientRustPanicError
|
| Prisma.PrismaClientRustPanicError
|
||||||
| PrismaClientUnknownRequestError
|
| Prisma.PrismaClientUnknownRequestError
|
||||||
| PrismaClientValidationError;
|
| Prisma.PrismaClientValidationError;
|
||||||
|
|
||||||
@Catch(
|
@Catch(
|
||||||
PrismaClientInitializationError,
|
Prisma.PrismaClientInitializationError,
|
||||||
PrismaClientKnownRequestError,
|
Prisma.PrismaClientKnownRequestError,
|
||||||
PrismaClientRustPanicError,
|
Prisma.PrismaClientRustPanicError,
|
||||||
PrismaClientUnknownRequestError,
|
Prisma.PrismaClientUnknownRequestError,
|
||||||
PrismaClientValidationError
|
Prisma.PrismaClientValidationError
|
||||||
)
|
)
|
||||||
export class PrismaExceptionFilter implements ExceptionFilter {
|
export class PrismaExceptionFilter implements ExceptionFilter {
|
||||||
private readonly logger = new Logger("PrismaExceptionFilter");
|
private readonly logger = new Logger("PrismaExceptionFilter");
|
||||||
@@ -58,7 +52,7 @@ export class PrismaExceptionFilter implements ExceptionFilter {
|
|||||||
let message = "There was an error, please try again later.";
|
let message = "There was an error, please try again later.";
|
||||||
let statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
|
let statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||||
let errorCode = INTERNAL_SERVER_ERROR;
|
let errorCode = INTERNAL_SERVER_ERROR;
|
||||||
if (error instanceof PrismaClientKnownRequestError) {
|
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
switch (error.code) {
|
switch (error.code) {
|
||||||
case "P2002": // Unique constraint failed
|
case "P2002": // Unique constraint failed
|
||||||
errorCode = CONFLICT;
|
errorCode = CONFLICT;
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { PrismaAttributeRepository as PrismaAttributeRepositoryLib } from "@calcom/platform-libraries/repositories";
|
||||||
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaAttributeRepository extends PrismaAttributeRepositoryLib {
|
||||||
|
constructor(private readonly dbWrite: PrismaWriteService) {
|
||||||
|
super(dbWrite.prisma as unknown as PrismaClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { PrismaHostRepository as PrismaHostRepositoryLib } from "@calcom/platform-libraries/repositories";
|
||||||
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaHostRepository extends PrismaHostRepositoryLib {
|
||||||
|
constructor(private readonly dbWrite: PrismaWriteService) {
|
||||||
|
super(dbWrite.prisma as unknown as PrismaClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MembershipRole } from "@prisma/client";
|
import { MembershipRole } from "@calcom/platform-libraries";
|
||||||
|
|
||||||
export const SYSTEM_ADMIN_ROLE = "SYSADMIN";
|
export const SYSTEM_ADMIN_ROLE = "SYSADMIN";
|
||||||
export const ORG_ROLES = [
|
export const ORG_ROLES = [
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { PrismaAttributeRepository } from "@/lib/repositories/prisma-attribute.repository";
|
||||||
|
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
|
||||||
|
import { PrismaHostRepository } from "@/lib/repositories/prisma-host.repository";
|
||||||
|
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
|
||||||
|
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { LuckyUserService as BaseLuckyUserService } from "@calcom/platform-libraries/bookings";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LuckyUserService extends BaseLuckyUserService {
|
||||||
|
constructor(
|
||||||
|
bookingRepository: PrismaBookingRepository,
|
||||||
|
hostRepository: PrismaHostRepository,
|
||||||
|
oooRepository: PrismaOOORepository,
|
||||||
|
userRepository: PrismaUserRepository,
|
||||||
|
attributeRepository: PrismaAttributeRepository
|
||||||
|
) {
|
||||||
|
super({
|
||||||
|
bookingRepository: bookingRepository,
|
||||||
|
hostRepository: hostRepository,
|
||||||
|
oooRepository: oooRepository,
|
||||||
|
userRepository: userRepository,
|
||||||
|
attributeRepository: attributeRepository,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,10 +4,12 @@ import { AtomsConferencingAppsController } from "@/modules/atoms/controllers/ato
|
|||||||
import { AtomsController } from "@/modules/atoms/controllers/atoms.controller";
|
import { AtomsController } from "@/modules/atoms/controllers/atoms.controller";
|
||||||
import { AtomsEventTypesController } from "@/modules/atoms/controllers/atoms.event-types.controller";
|
import { AtomsEventTypesController } from "@/modules/atoms/controllers/atoms.event-types.controller";
|
||||||
import { AtomsSchedulesController } from "@/modules/atoms/controllers/atoms.schedules.controller";
|
import { AtomsSchedulesController } from "@/modules/atoms/controllers/atoms.schedules.controller";
|
||||||
|
import { AtomsVerificationController } from "@/modules/atoms/controllers/atoms.verification.controller";
|
||||||
import { AttributesAtomsService } from "@/modules/atoms/services/attributes-atom.service";
|
import { AttributesAtomsService } from "@/modules/atoms/services/attributes-atom.service";
|
||||||
import { ConferencingAtomsService } from "@/modules/atoms/services/conferencing-atom.service";
|
import { ConferencingAtomsService } from "@/modules/atoms/services/conferencing-atom.service";
|
||||||
import { EventTypesAtomService } from "@/modules/atoms/services/event-types-atom.service";
|
import { EventTypesAtomService } from "@/modules/atoms/services/event-types-atom.service";
|
||||||
import { SchedulesAtomsService } from "@/modules/atoms/services/schedules-atom.service";
|
import { SchedulesAtomsService } from "@/modules/atoms/services/schedules-atom.service";
|
||||||
|
import { VerificationAtomsService } from "@/modules/atoms/services/verification-atom.service";
|
||||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||||
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
||||||
@@ -15,6 +17,7 @@ import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/inde
|
|||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { RedisService } from "@/modules/redis/redis.service";
|
import { RedisService } from "@/modules/redis/redis.service";
|
||||||
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
|
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
|
||||||
|
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
||||||
import { UsersService } from "@/modules/users/services/users.service";
|
import { UsersService } from "@/modules/users/services/users.service";
|
||||||
import { UsersRepository } from "@/modules/users/users.repository";
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
@@ -32,7 +35,9 @@ import { Module } from "@nestjs/common";
|
|||||||
AtomsRepository,
|
AtomsRepository,
|
||||||
UsersService,
|
UsersService,
|
||||||
SchedulesAtomsService,
|
SchedulesAtomsService,
|
||||||
|
VerificationAtomsService,
|
||||||
RedisService,
|
RedisService,
|
||||||
|
TeamsRepository,
|
||||||
],
|
],
|
||||||
exports: [EventTypesAtomService],
|
exports: [EventTypesAtomService],
|
||||||
controllers: [
|
controllers: [
|
||||||
@@ -40,6 +45,7 @@ import { Module } from "@nestjs/common";
|
|||||||
AtomsEventTypesController,
|
AtomsEventTypesController,
|
||||||
AtomsConferencingAppsController,
|
AtomsConferencingAppsController,
|
||||||
AtomsSchedulesController,
|
AtomsSchedulesController,
|
||||||
|
AtomsVerificationController,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AtomsModule {}
|
export class AtomsModule {}
|
||||||
|
|||||||
@@ -64,4 +64,47 @@ export class AtomsRepository {
|
|||||||
|
|
||||||
return userTeams;
|
return userTeams;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getSecondaryEmails(userId: number) {
|
||||||
|
return await this.dbRead.prisma.secondaryEmail.findMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
emailVerified: {
|
||||||
|
not: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getExistingSecondaryEmailByUserAndEmail(userId: number, email: string) {
|
||||||
|
const existingSecondaryEmailRecord = await this.dbRead.prisma.secondaryEmail.findUnique({
|
||||||
|
where: {
|
||||||
|
userId_email: { userId, email },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return existingSecondaryEmailRecord?.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getExistingSecondaryEmail(email: string) {
|
||||||
|
const existingSecondaryEmailRecord = await this.dbRead.prisma.secondaryEmail.findUnique({
|
||||||
|
where: {
|
||||||
|
email,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return existingSecondaryEmailRecord?.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
async addSecondaryEmail(userId: number, email: string) {
|
||||||
|
const existingSecondaryEmailRecord = await this.dbWrite.prisma.secondaryEmail.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
email,
|
||||||
|
emailVerified: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return existingSecondaryEmailRecord?.email;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
|
import { Throttle } from "@/lib/endpoint-throttler-decorator";
|
||||||
|
import { AddVerifiedEmailInput } from "@/modules/atoms/inputs/add-verified-email.input";
|
||||||
|
import { CheckEmailVerificationRequiredParams } from "@/modules/atoms/inputs/check-email-verification-required-params";
|
||||||
|
import { GetVerifiedEmailsParams } from "@/modules/atoms/inputs/get-verified-emails-params";
|
||||||
|
import { SendVerificationEmailInput } from "@/modules/atoms/inputs/send-verification-email.input";
|
||||||
|
import { VerifyEmailCodeInput } from "@/modules/atoms/inputs/verify-email-code.input";
|
||||||
|
import { GetVerifiedEmailsOutput } from "@/modules/atoms/outputs/get-verified-emails-output";
|
||||||
|
import { SendVerificationEmailOutput } from "@/modules/atoms/outputs/send-verification-email.output";
|
||||||
|
import { VerifyEmailCodeOutput } from "@/modules/atoms/outputs/verify-email-code.output";
|
||||||
|
import { VerificationAtomsService } from "@/modules/atoms/services/verification-atom.service";
|
||||||
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
UseGuards,
|
||||||
|
Version,
|
||||||
|
VERSION_NEUTRAL,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
Get,
|
||||||
|
Query,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { ApiTags as DocsTags, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { ApiResponse } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@Controller({
|
||||||
|
path: "/v2/atoms",
|
||||||
|
version: API_VERSIONS_VALUES,
|
||||||
|
})
|
||||||
|
@DocsTags("Atoms - verification endpoints for atoms")
|
||||||
|
@DocsExcludeController(true)
|
||||||
|
export class AtomsVerificationController {
|
||||||
|
constructor(private readonly verificationService: VerificationAtomsService) {}
|
||||||
|
|
||||||
|
@Post("/verification/email/send-code")
|
||||||
|
@Version(VERSION_NEUTRAL)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Throttle({ limit: 3, ttl: 60000, blockDuration: 60000, name: "atoms_verification_email_send_code" })
|
||||||
|
async sendEmailVerificationCode(
|
||||||
|
@Body() body: SendVerificationEmailInput
|
||||||
|
): Promise<SendVerificationEmailOutput> {
|
||||||
|
const result = await this.verificationService.sendEmailVerificationCode({
|
||||||
|
email: body.email,
|
||||||
|
username: body.username,
|
||||||
|
language: body.language,
|
||||||
|
isVerifyingEmail: body.isVerifyingEmail,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: { sent: result.ok && !result.skipped },
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/verification/email/check")
|
||||||
|
@Version(VERSION_NEUTRAL)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async checkEmailVerificationRequired(
|
||||||
|
@Query() query: CheckEmailVerificationRequiredParams
|
||||||
|
): Promise<ApiResponse<boolean>> {
|
||||||
|
const required = await this.verificationService.checkEmailVerificationRequired({
|
||||||
|
email: query.email,
|
||||||
|
userSessionEmail: query.userSessionEmail,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: required,
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("/verification/email/verify-code")
|
||||||
|
@Version(VERSION_NEUTRAL)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async verifyEmailCode(@Body() body: VerifyEmailCodeInput): Promise<VerifyEmailCodeOutput> {
|
||||||
|
const verified = await this.verificationService.verifyEmailCodeUnAuthenticated({
|
||||||
|
email: body.email,
|
||||||
|
code: body.code,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: { verified },
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("/verification/email/verify-code-authenticated")
|
||||||
|
@Version(VERSION_NEUTRAL)
|
||||||
|
@UseGuards(ApiAuthGuard)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async verifyEmailCodeAuthenticated(
|
||||||
|
@Body() body: VerifyEmailCodeInput,
|
||||||
|
@GetUser() user: UserWithProfile
|
||||||
|
): Promise<VerifyEmailCodeOutput> {
|
||||||
|
const verified = await this.verificationService.verifyEmailCodeAuthenticated(user, {
|
||||||
|
email: body.email,
|
||||||
|
code: body.code,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: { verified },
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/emails/verified-emails")
|
||||||
|
@Version(VERSION_NEUTRAL)
|
||||||
|
@UseGuards(ApiAuthGuard)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getVerifiedEmails(
|
||||||
|
@Query() query: GetVerifiedEmailsParams,
|
||||||
|
@GetUser() user: UserWithProfile
|
||||||
|
): Promise<GetVerifiedEmailsOutput> {
|
||||||
|
const verifiedEmails = await this.verificationService.getVerifiedEmails({
|
||||||
|
userId: user.id,
|
||||||
|
userEmail: user.email,
|
||||||
|
teamId: query.teamId ? Number(query.teamId) : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: verifiedEmails,
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("/emails/verified-emails")
|
||||||
|
@Version(VERSION_NEUTRAL)
|
||||||
|
@UseGuards(ApiAuthGuard)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async addVerifiedEmails(
|
||||||
|
@Body() body: AddVerifiedEmailInput,
|
||||||
|
@GetUser() user: UserWithProfile
|
||||||
|
): Promise<ApiResponse<{ emailVerified: boolean }>> {
|
||||||
|
const emailVerified = await this.verificationService.addVerifiedEmail({
|
||||||
|
userId: user.id,
|
||||||
|
existingPrimaryEmail: user.email,
|
||||||
|
email: body.email,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: { emailVerified },
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { IsEmail } from "class-validator";
|
||||||
|
|
||||||
|
export class AddVerifiedEmailInput {
|
||||||
|
@ApiProperty({ example: "user@example.com" })
|
||||||
|
@IsEmail()
|
||||||
|
email!: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { IsEmail, IsOptional, IsString } from "class-validator";
|
||||||
|
|
||||||
|
export class CheckEmailVerificationRequiredParams {
|
||||||
|
@ApiProperty({ example: "user@example.com" })
|
||||||
|
@IsEmail()
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: "user@example.com" })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
userSessionEmail?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { IsNumber, IsOptional, IsString, IsEmail } from "class-validator";
|
||||||
|
|
||||||
|
export class GetVerifiedEmailsParams {
|
||||||
|
@ApiPropertyOptional({ example: "12345" })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
teamId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GetVerifiedEmailsInput {
|
||||||
|
@ApiProperty()
|
||||||
|
@IsNumber()
|
||||||
|
userId!: number;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsEmail()
|
||||||
|
@IsString()
|
||||||
|
userEmail!: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
teamId?: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { IsEmail, IsOptional, IsString, IsBoolean } from "class-validator";
|
||||||
|
|
||||||
|
export class SendVerificationEmailInput {
|
||||||
|
@ApiProperty({ example: "user@example.com" })
|
||||||
|
@IsEmail()
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: "johndoe" })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
username?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: "en" })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
language?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isVerifyingEmail?: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { IsEmail, IsString } from "class-validator";
|
||||||
|
|
||||||
|
export class VerifyEmailCodeInput {
|
||||||
|
@ApiProperty({ example: "user@example.com" })
|
||||||
|
@IsEmail()
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: "123456" })
|
||||||
|
@IsString()
|
||||||
|
code!: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Expose } from "class-transformer";
|
||||||
|
import { IsString, IsArray } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class GetVerifiedEmailsOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
readonly status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
@ApiProperty({ type: [String] })
|
||||||
|
readonly data!: string[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Expose, Type } from "class-transformer";
|
||||||
|
import { IsString, ValidateNested, IsBoolean } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class SendVerificationEmailData {
|
||||||
|
@ApiProperty({ example: true })
|
||||||
|
@IsBoolean()
|
||||||
|
@Expose()
|
||||||
|
readonly sent!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SendVerificationEmailOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
readonly status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => SendVerificationEmailData)
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty({ type: SendVerificationEmailData })
|
||||||
|
readonly data!: SendVerificationEmailData;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Expose, Type } from "class-transformer";
|
||||||
|
import { IsString, ValidateNested, IsBoolean } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class VerifyEmailCodeData {
|
||||||
|
@ApiProperty({ example: true })
|
||||||
|
@IsBoolean()
|
||||||
|
@Expose()
|
||||||
|
readonly verified!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VerifyEmailCodeOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
readonly status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => VerifyEmailCodeData)
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty({ type: VerifyEmailCodeData })
|
||||||
|
readonly data!: VerifyEmailCodeData;
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import { AtomsRepository } from "@/modules/atoms/atoms.repository";
|
||||||
|
import { CheckEmailVerificationRequiredParams } from "@/modules/atoms/inputs/check-email-verification-required-params";
|
||||||
|
import { GetVerifiedEmailsInput } from "@/modules/atoms/inputs/get-verified-emails-params";
|
||||||
|
import { SendVerificationEmailInput } from "@/modules/atoms/inputs/send-verification-email.input";
|
||||||
|
import { VerifyEmailCodeInput } from "@/modules/atoms/inputs/verify-email-code.input";
|
||||||
|
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
||||||
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
|
import { Injectable, BadRequestException, UnauthorizedException } from "@nestjs/common";
|
||||||
|
|
||||||
|
import {
|
||||||
|
verifyCodeUnAuthenticated,
|
||||||
|
verifyCodeAuthenticated,
|
||||||
|
sendEmailVerificationByCode,
|
||||||
|
checkEmailVerificationRequired,
|
||||||
|
} from "@calcom/platform-libraries";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class VerificationAtomsService {
|
||||||
|
constructor(
|
||||||
|
private readonly atomsRepository: AtomsRepository,
|
||||||
|
private readonly teamsRepository: TeamsRepository
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async checkEmailVerificationRequired(input: CheckEmailVerificationRequiredParams) {
|
||||||
|
return await checkEmailVerificationRequired(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyEmailCodeUnAuthenticated(input: VerifyEmailCodeInput) {
|
||||||
|
try {
|
||||||
|
return await verifyCodeUnAuthenticated(input);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
if (error.message === "invalid_code") {
|
||||||
|
throw new BadRequestException("Invalid verification code");
|
||||||
|
}
|
||||||
|
if (error.message === "BAD_REQUEST") {
|
||||||
|
throw new BadRequestException("Email and code are required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new BadRequestException("Verification failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyEmailCodeAuthenticated(user: UserWithProfile, input: VerifyEmailCodeInput) {
|
||||||
|
try {
|
||||||
|
return await verifyCodeAuthenticated({
|
||||||
|
user,
|
||||||
|
email: input.email,
|
||||||
|
code: input.code,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
if (error.message === "invalid_code") {
|
||||||
|
throw new BadRequestException("Invalid verification code");
|
||||||
|
}
|
||||||
|
if (error.message === "BAD_REQUEST") {
|
||||||
|
throw new BadRequestException("Email, code, and user ID are required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new UnauthorizedException("Verification failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendEmailVerificationCode(input: SendVerificationEmailInput) {
|
||||||
|
return await sendEmailVerificationByCode({
|
||||||
|
email: input.email,
|
||||||
|
username: input.username,
|
||||||
|
language: input.language,
|
||||||
|
isVerifyingEmail: input.isVerifyingEmail,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getVerifiedEmails(input: GetVerifiedEmailsInput): Promise<string[]> {
|
||||||
|
const { userId, userEmail, teamId } = input;
|
||||||
|
const userEmailWithoutOauthClientId = this.removeClientIdFromEmail(userEmail);
|
||||||
|
|
||||||
|
if (teamId) {
|
||||||
|
const verifiedEmails: string[] = [];
|
||||||
|
const teamMembers = await this.teamsRepository.getTeamMemberEmails(teamId);
|
||||||
|
|
||||||
|
if (teamMembers.length === 0) {
|
||||||
|
return verifiedEmails;
|
||||||
|
}
|
||||||
|
|
||||||
|
teamMembers.forEach((member) => {
|
||||||
|
const memberEmailWithoutOauthClientId = this.removeClientIdFromEmail(member.email);
|
||||||
|
|
||||||
|
verifiedEmails.push(memberEmailWithoutOauthClientId);
|
||||||
|
member.secondaryEmails.forEach((secondaryEmail) => {
|
||||||
|
verifiedEmails.push(this.removeClientIdFromEmail(secondaryEmail.email));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return verifiedEmails;
|
||||||
|
}
|
||||||
|
|
||||||
|
let verifiedEmails = [userEmailWithoutOauthClientId];
|
||||||
|
|
||||||
|
const secondaryEmails = await this.atomsRepository.getSecondaryEmails(userId);
|
||||||
|
verifiedEmails = verifiedEmails.concat(
|
||||||
|
secondaryEmails.map((secondaryEmail) => this.removeClientIdFromEmail(secondaryEmail.email))
|
||||||
|
);
|
||||||
|
|
||||||
|
return verifiedEmails;
|
||||||
|
}
|
||||||
|
|
||||||
|
async addVerifiedEmail({
|
||||||
|
userId,
|
||||||
|
existingPrimaryEmail,
|
||||||
|
email,
|
||||||
|
}: {
|
||||||
|
userId: number;
|
||||||
|
existingPrimaryEmail: string;
|
||||||
|
email: string;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
const existingSecondaryEmail = await this.atomsRepository.getExistingSecondaryEmailByUserAndEmail(
|
||||||
|
userId,
|
||||||
|
email
|
||||||
|
);
|
||||||
|
const alreadyExistingEmail = await this.atomsRepository.getExistingSecondaryEmail(email);
|
||||||
|
|
||||||
|
if (alreadyExistingEmail) {
|
||||||
|
throw new BadRequestException("Email already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingPrimaryEmail === email || existingSecondaryEmail === email) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.atomsRepository.addSecondaryEmail(userId, email);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeClientIdFromEmail(email: string): string {
|
||||||
|
const [localPart, domain] = email.split("@");
|
||||||
|
const localPartSegments = localPart.split("+");
|
||||||
|
|
||||||
|
localPartSegments.pop();
|
||||||
|
|
||||||
|
const baseEmail = localPartSegments.join("+");
|
||||||
|
const normalizedEmail = `${baseEmail}@${domain}`;
|
||||||
|
|
||||||
|
return normalizedEmail;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ import { NextAuthStrategy } from "@/modules/auth/strategies/next-auth/next-auth.
|
|||||||
import { DeploymentsModule } from "@/modules/deployments/deployments.module";
|
import { DeploymentsModule } from "@/modules/deployments/deployments.module";
|
||||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||||
import { ProfilesModule } from "@/modules/profiles/profiles.module";
|
|
||||||
import { RedisModule } from "@/modules/redis/redis.module";
|
import { RedisModule } from "@/modules/redis/redis.module";
|
||||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
import { UsersModule } from "@/modules/users/users.module";
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
@@ -22,7 +21,6 @@ import { PassportModule } from "@nestjs/passport";
|
|||||||
MembershipsModule,
|
MembershipsModule,
|
||||||
TokensModule,
|
TokensModule,
|
||||||
DeploymentsModule,
|
DeploymentsModule,
|
||||||
ProfilesModule,
|
|
||||||
],
|
],
|
||||||
providers: [NextAuthGuard, NextAuthStrategy, ApiAuthGuard, ApiAuthStrategy, OAuthFlowService],
|
providers: [NextAuthGuard, NextAuthStrategy, ApiAuthGuard, ApiAuthStrategy, OAuthFlowService],
|
||||||
exports: [NextAuthGuard, ApiAuthGuard],
|
exports: [NextAuthGuard, ApiAuthGuard],
|
||||||
|
|||||||
+2
@@ -2,6 +2,8 @@ import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.st
|
|||||||
import { ExecutionContext } from "@nestjs/common";
|
import { ExecutionContext } from "@nestjs/common";
|
||||||
import { createParamDecorator } from "@nestjs/common";
|
import { createParamDecorator } from "@nestjs/common";
|
||||||
|
|
||||||
|
export type AuthOptionalUser = ApiAuthGuardUser | null;
|
||||||
|
|
||||||
export const GetOptionalUser = createParamDecorator<
|
export const GetOptionalUser = createParamDecorator<
|
||||||
keyof ApiAuthGuardUser | (keyof ApiAuthGuardUser)[],
|
keyof ApiAuthGuardUser | (keyof ApiAuthGuardUser)[],
|
||||||
ExecutionContext
|
ExecutionContext
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Reflector } from "@nestjs/core";
|
import { Reflector } from "@nestjs/core";
|
||||||
import { MembershipRole } from "@prisma/client";
|
|
||||||
|
import { MembershipRole } from "@calcom/platform-libraries";
|
||||||
|
|
||||||
export const MembershipRoles = Reflector.createDecorator<MembershipRole[]>();
|
export const MembershipRoles = Reflector.createDecorator<MembershipRole[]>();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { UserWithProfile } from "@/modules/users/users.repository";
|
|||||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||||
import { Reflector } from "@nestjs/core";
|
import { Reflector } from "@nestjs/core";
|
||||||
|
|
||||||
import { MembershipRole } from "@calcom/prisma/enums";
|
import { MembershipRole } from "@calcom/platform-libraries";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OrganizationRolesGuard implements CanActivate {
|
export class OrganizationRolesGuard implements CanActivate {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
|||||||
import { DeploymentsRepository } from "@/modules/deployments/deployments.repository";
|
import { DeploymentsRepository } from "@/modules/deployments/deployments.repository";
|
||||||
import { DeploymentsService } from "@/modules/deployments/deployments.service";
|
import { DeploymentsService } from "@/modules/deployments/deployments.service";
|
||||||
import { JwtService } from "@/modules/jwt/jwt.service";
|
import { JwtService } from "@/modules/jwt/jwt.service";
|
||||||
|
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
@@ -20,6 +21,7 @@ import { Test, TestingModule } from "@nestjs/testing";
|
|||||||
import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
||||||
import { createRequest } from "node-mocks-http";
|
import { createRequest } from "node-mocks-http";
|
||||||
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
||||||
|
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||||
@@ -45,6 +47,7 @@ describe("ApiAuthStrategy", () => {
|
|||||||
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||||
let oAuthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
let oAuthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||||
|
let membershipRepositoryFixture: MembershipRepositoryFixture;
|
||||||
|
|
||||||
const validApiKeyEmail = `api-auth-api-key-user-${randomString()}@api.com`;
|
const validApiKeyEmail = `api-auth-api-key-user-${randomString()}@api.com`;
|
||||||
const validAccessTokenEmail = `api-auth-access-token-user-${randomString()}@api.com`;
|
const validAccessTokenEmail = `api-auth-access-token-user-${randomString()}@api.com`;
|
||||||
@@ -65,6 +68,7 @@ describe("ApiAuthStrategy", () => {
|
|||||||
}),
|
}),
|
||||||
ProfilesModule,
|
ProfilesModule,
|
||||||
TokensModule,
|
TokensModule,
|
||||||
|
MembershipsModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
MockedRedisService,
|
MockedRedisService,
|
||||||
@@ -91,6 +95,7 @@ describe("ApiAuthStrategy", () => {
|
|||||||
teamRepositoryFixture = new TeamRepositoryFixture(module);
|
teamRepositoryFixture = new TeamRepositoryFixture(module);
|
||||||
oAuthClientRepositoryFixture = new OAuthClientRepositoryFixture(module);
|
oAuthClientRepositoryFixture = new OAuthClientRepositoryFixture(module);
|
||||||
profilesRepositoryFixture = new ProfileRepositoryFixture(module);
|
profilesRepositoryFixture = new ProfileRepositoryFixture(module);
|
||||||
|
membershipRepositoryFixture = new MembershipRepositoryFixture(module);
|
||||||
organization = await teamRepositoryFixture.create({ name: `api-auth-organization-1-${randomString()}` });
|
organization = await teamRepositoryFixture.create({ name: `api-auth-organization-1-${randomString()}` });
|
||||||
organizationTwo = await teamRepositoryFixture.create({
|
organizationTwo = await teamRepositoryFixture.create({
|
||||||
name: `api-auth-organization-2-${randomString()}`,
|
name: `api-auth-organization-2-${randomString()}`,
|
||||||
@@ -121,6 +126,20 @@ describe("ApiAuthStrategy", () => {
|
|||||||
organization: { connect: { id: organizationTwo.id } },
|
organization: { connect: { id: organizationTwo.id } },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await membershipRepositoryFixture.create({
|
||||||
|
user: { connect: { id: validOAuthUser.id } },
|
||||||
|
team: { connect: { id: organization.id } },
|
||||||
|
role: "OWNER",
|
||||||
|
accepted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await membershipRepositoryFixture.create({
|
||||||
|
user: { connect: { id: validOAuthUser.id } },
|
||||||
|
team: { connect: { id: organizationTwo.id } },
|
||||||
|
role: "OWNER",
|
||||||
|
accepted: true,
|
||||||
|
});
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
logo: "logo-url",
|
logo: "logo-url",
|
||||||
name: "name",
|
name: "name",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user