Compare commits
151
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
75a161fe35 | ||
|
|
eb1ed107ba | ||
|
|
13e2e340ac | ||
|
|
0af62da16b | ||
|
|
9b323cbcc8 | ||
|
|
18a0e2d75c | ||
|
|
87c89a9968 | ||
|
|
e237749abd | ||
|
|
994a36d039 | ||
|
|
c989817373 | ||
|
|
e3fb7340aa | ||
|
|
e4ef2832cd | ||
|
|
881602cf1c | ||
|
|
4c01f171ae | ||
|
|
d9817fcb07 | ||
|
|
a784af3e47 | ||
|
|
c268925330 | ||
|
|
378e8e0330 | ||
|
|
8b3fbb672b | ||
|
|
2d5a86774e | ||
|
|
0865104a77 | ||
|
|
1914b37ee4 | ||
|
|
58ef0abb99 | ||
|
|
13ba680a73 | ||
|
|
f40c6da93b | ||
|
|
fc2e81e89a | ||
|
|
293dd1a140 | ||
|
|
b7fda31678 | ||
|
|
94a04f0dc2 | ||
|
|
2338f85dcf | ||
|
|
fbaccc8e5f | ||
|
|
750db38d38 | ||
|
|
8829e1ca3f | ||
|
|
ab703a4346 | ||
|
|
8d5a7b4e10 | ||
|
|
6ba3049314 | ||
|
|
abe92cd693 | ||
|
|
6c6cf0433a | ||
|
|
211b958c58 | ||
|
|
01a0d43943 | ||
|
|
3133eb5aeb | ||
|
|
76b68bebd7 | ||
|
|
c20e723e34 | ||
|
|
7b7478c0a9 | ||
|
+3 |
ebeb008f9b | ||
|
|
a90d765fca | ||
|
|
af51df2b4d | ||
|
|
076e93ca56 | ||
|
|
47f45409d2 | ||
|
|
759d5b8663 | ||
|
|
c554396572 | ||
|
|
fafd3fb181 | ||
|
|
eb05d547de | ||
|
|
1fa8a5ef25 | ||
|
|
5a350efb40 | ||
|
|
242f86891a | ||
|
|
ce6969d058 | ||
|
|
26f8246b91 | ||
|
|
4baa9b6bc1 | ||
|
|
02c86d0268 | ||
|
|
49f99d69a8 | ||
|
|
659d9cf767 | ||
|
|
4d662d31a1 | ||
|
|
6fe6540bc6 | ||
|
|
ee1904bb2c | ||
|
|
aaa59f1455 | ||
|
|
e29b7e83b8 | ||
|
|
6af473a486 | ||
|
|
bf8f966811 | ||
|
|
c64b663514 | ||
|
|
29314cde75 | ||
|
|
3ea73e75d6 | ||
|
|
d18e23378b | ||
|
|
736f1f594c | ||
|
|
09b1643478 | ||
|
|
86f6e8bac3 | ||
|
|
9c65d02e84 | ||
|
|
be9ab02a7e | ||
|
|
cfefba6d4d | ||
|
|
815e13303b | ||
|
|
7c8dbb3ea3 | ||
|
|
cabd459682 | ||
|
|
33aedbae20 | ||
|
|
d3fbc73a39 | ||
|
|
cfda4757e4 | ||
|
|
895ed48bd6 | ||
|
|
eda328d196 | ||
|
|
1c501d1501 | ||
|
|
08a57f32c5 | ||
|
|
b5a26bc227 | ||
|
|
737b019199 | ||
|
|
b41031626f | ||
|
|
a1fc4bccb9 | ||
|
|
d764b6a32d | ||
|
|
87cb8b170a | ||
|
|
badd0de33c | ||
|
|
124fd8ebca | ||
|
|
5d0dc3f546 | ||
|
|
e4baf15b53 | ||
|
|
a1bd8b6259 | ||
|
|
7987c9f45d | ||
|
|
781d0d92df | ||
|
|
b71d8baccc | ||
|
|
a71d949a0c | ||
|
|
e55cee443b | ||
|
|
c3634b3aba | ||
|
|
cd576f5378 | ||
|
|
2965450929 | ||
|
|
db9b9c45dc | ||
|
|
37f500526c | ||
|
|
006aecb066 | ||
|
|
0829929e98 | ||
|
|
a61148cb51 | ||
|
|
593c48c8b4 | ||
|
|
fb364971fa | ||
|
|
a2205d497d | ||
|
|
cf4e7a5d2c | ||
|
|
0e2ee3adb2 | ||
|
|
dbe8a85cf5 | ||
|
|
84210dc1b4 | ||
|
|
4061bfb3fe |
No files matched your search
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@calcom/atoms": minor
|
||||
---
|
||||
|
||||
fix: Availability atom handleFormSubmit callbacks not triggering
|
||||
+5
-1
@@ -1 +1,5 @@
|
||||
module.exports = require("./packages/config/eslint-preset");
|
||||
// This configuration only applies to the package manager root.
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
extends: ["./packages/config/eslint-preset.js"],
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
name: Atoms E2E Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'packages/platform/atoms/**'
|
||||
- 'packages/platform/examples/base/**'
|
||||
- 'apps/api/v2/**'
|
||||
- '.github/workflows/atoms-e2e.yml'
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8096
|
||||
ATOMS_E2E_OAUTH_CLIENT_ID: ${{ secrets.ATOMS_E2E_OAUTH_CLIENT_ID }}
|
||||
ATOMS_E2E_OAUTH_CLIENT_SECRET: ${{ secrets.ATOMS_E2E_OAUTH_CLIENT_SECRET }}
|
||||
ATOMS_E2E_API_URL: ${{ secrets.ATOMS_E2E_API_URL }}
|
||||
ATOMS_E2E_ORG_ID: ${{ secrets.ATOMS_E2E_ORG_ID }}
|
||||
ATOMS_E2E_OAUTH_CLIENT_ID_BOOKER_EMBED: ${{ secrets.ATOMS_E2E_OAUTH_CLIENT_ID_BOOKER_EMBED }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
|
||||
jobs:
|
||||
atoms-e2e:
|
||||
timeout-minutes: 15
|
||||
name: Atoms E2E Tests
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/yarn-playwright-install
|
||||
- name: Run Atoms E2E Tests
|
||||
working-directory: packages/platform/examples/base
|
||||
run: yarn test:e2e
|
||||
- name: Upload Test Results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: atoms-e2e-test-results
|
||||
path: test-results
|
||||
retention-days: 7
|
||||
- name: Upload Playwright Report
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: atoms-e2e-playwright-report
|
||||
path: playwright-report
|
||||
retention-days: 7
|
||||
@@ -22,6 +22,7 @@ env:
|
||||
STRIPE_API_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
||||
SLOTS_CACHE_TTL: ${{ secrets.CI_SLOTS_CACHE_TTL }}
|
||||
jobs:
|
||||
e2e:
|
||||
timeout-minutes: 20
|
||||
|
||||
Vendored
+2
-2
@@ -724,7 +724,7 @@ ${we.map(pe=>`Not found: ${lo(pe)}
|
||||
To understand the dependency tree requiring vulnerable packages, check the raw report with the \`--json\` flag or use \`yarn why <package>\` to get more information as to who depends on them.
|
||||
`,examples:[["Checks for known security issues with the installed packages. The output is a list of known issues.","yarn npm audit"],["Audit dependencies in all workspaces","yarn npm audit --all"],["Limit auditing to `dependencies` (excludes `devDependencies`)","yarn npm audit --environment production"],["Show audit report as valid JSON","yarn npm audit --json"],["Audit all direct and transitive dependencies","yarn npm audit --recursive"],["Output moderate (or more severe) vulnerabilities","yarn npm audit --severity moderate"],["Exclude certain packages","yarn npm audit --exclude package1 --exclude package2"],["Ignore specific advisories","yarn npm audit --ignore 1234567 --ignore 7654321"]]});var TM=Pe(Xr()),OM=J("util"),Yu=class extends De{constructor(){super(...arguments);this.fields=z.String("-f,--fields",{description:"A comma-separated list of manifest fields that should be displayed"});this.json=z.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.packages=z.Rest()}async execute(){let t=await ye.find(this.context.cwd,this.context.plugins),{project:i}=await je.find(t,this.context.cwd),n=typeof this.fields<"u"?new Set(["name",...this.fields.split(/\s*,\s*/)]):null,s=[],o=!1,a=await Ge.start({configuration:t,includeFooter:!1,json:this.json,stdout:this.context.stdout},async l=>{for(let c of this.packages){let u;if(c==="."){let L=i.topLevelWorkspace;if(!L.manifest.name)throw new be(`Missing ${ee.pretty(t,"name",ee.Type.CODE)} field in ${K.fromPortablePath(x.join(L.cwd,xt.manifest))}`);u=P.makeDescriptor(L.manifest.name,"unknown")}else u=P.parseDescriptor(c);let g=Mt.getIdentUrl(u),f=MM(await Mt.get(g,{configuration:t,ident:u,jsonResponse:!0,customErrorMessage:Mt.customPackageError})),h=Object.keys(f.versions).sort(TM.default.compareLoose),C=f["dist-tags"].latest||h[h.length-1],y=vt.validRange(u.range);if(y){let L=TM.default.maxSatisfying(h,y);L!==null?C=L:(l.reportWarning(0,`Unmet range ${P.prettyRange(t,u.range)}; falling back to the latest version`),o=!0)}else Object.prototype.hasOwnProperty.call(f["dist-tags"],u.range)?C=f["dist-tags"][u.range]:u.range!=="unknown"&&(l.reportWarning(0,`Unknown tag ${P.prettyRange(t,u.range)}; falling back to the latest version`),o=!0);let B=f.versions[C],v={...f,...B,version:C,versions:h},D;if(n!==null){D={};for(let L of n){let H=v[L];if(typeof H<"u")D[L]=H;else{l.reportWarning(1,`The ${ee.pretty(t,L,ee.Type.CODE)} field doesn't exist inside ${P.prettyIdent(t,u)}'s information`),o=!0;continue}}}else this.json||(delete v.dist,delete v.readme,delete v.users),D=v;l.reportJson(D),this.json||s.push(D)}});OM.inspect.styles.name="cyan";for(let l of s)(l!==s[0]||o)&&this.context.stdout.write(`
|
||||
`),this.context.stdout.write(`${(0,OM.inspect)(l,{depth:1/0,colors:!0,compact:!1})}
|
||||
`);return a.exitCode()}};Yu.paths=[["npm","info"]],Yu.usage=ve.Usage({category:"Npm-related commands",description:"show information about a package",details:"\n This command fetches information about a package from the npm registry and prints it in a tree format.\n\n The package does not have to be installed locally, but needs to have been published (in particular, local changes will be ignored even for workspaces).\n\n Append `@<range>` to the package argument to provide information specific to the latest version that satisfies the range or to the corresponding tagged version. If the range is invalid or if there is no version satisfying the range, the command will print a warning and fall back to the latest version.\n\n If the `-f,--fields` option is set, it's a comma-separated list of fields which will be used to only display part of the package information.\n\n By default, this command won't return the `dist`, `readme`, and `users` fields, since they are often very long. To explicitly request those fields, explicitly list them with the `--fields` flag or request the output in JSON mode.\n ",examples:[["Show all available information about react (except the `dist`, `readme`, and `users` fields)","yarn npm info react"],["Show all available information about react as valid JSON (including the `dist`, `readme`, and `users` fields)","yarn npm info react --json"],["Show all available information about react@16.12.0","yarn npm info react@16.12.0"],["Show all available information about react@next","yarn npm info react@next"],["Show the description of react","yarn npm info react --fields description"],["Show all available versions of react","yarn npm info react --fields versions"],["Show the readme of react","yarn npm info react --fields readme"],["Show a few fields of react","yarn npm info react --fields homepage,repository"]]});function MM(r){if(Array.isArray(r)){let e=[];for(let t of r)t=MM(t),t&&e.push(t);return e}else if(typeof r=="object"&&r!==null){let e={};for(let t of Object.keys(r)){if(t.startsWith("_"))continue;let i=MM(r[t]);i&&(e[t]=i)}return e}else return r||null}var sue=Pe(Pm()),ju=class extends De{constructor(){super(...arguments);this.scope=z.String("-s,--scope",{description:"Login to the registry configured for a given scope"});this.publish=z.Boolean("--publish",!1,{description:"Login to the publish registry"})}async execute(){let t=await ye.find(this.context.cwd,this.context.plugins),i=await Qb({configuration:t,cwd:this.context.cwd,publish:this.publish,scope:this.scope});return(await Ge.start({configuration:t,stdout:this.context.stdout,includeFooter:!1},async s=>{let o=await zVe({configuration:t,registry:i,report:s,stdin:this.context.stdin,stdout:this.context.stdout}),a=`/-/user/org.couchdb.user:${encodeURIComponent(o.name)}`,l=await Mt.put(a,o,{attemptedAs:o.name,configuration:t,registry:i,jsonResponse:!0,authType:Mt.AuthType.NO_AUTH});return await WVe(i,l.token,{configuration:t,scope:this.scope}),s.reportInfo(0,"Successfully logged in")})).exitCode()}};ju.paths=[["npm","login"]],ju.usage=ve.Usage({category:"Npm-related commands",description:"store new login info to access the npm registry",details:"\n This command will ask you for your username, password, and 2FA One-Time-Password (when it applies). It will then modify your local configuration (in your home folder, never in the project itself) to reference the new tokens thus generated.\n\n Adding the `-s,--scope` flag will cause the authentication to be done against whatever registry is configured for the associated scope (see also `npmScopes`).\n\n Adding the `--publish` flag will cause the authentication to be done against the registry used when publishing the package (see also `publishConfig.registry` and `npmPublishRegistry`).\n ",examples:[["Login to the default registry","yarn npm login"],["Login to the registry linked to the @my-scope registry","yarn npm login --scope my-scope"],["Login to the publish registry for the current package","yarn npm login --publish"]]});async function Qb({scope:r,publish:e,configuration:t,cwd:i}){return r&&e?or.getScopeRegistry(r,{configuration:t,type:or.RegistryType.PUBLISH_REGISTRY}):r?or.getScopeRegistry(r,{configuration:t}):e?or.getPublishRegistry((await Ph(t,i)).manifest,{configuration:t}):or.getDefaultRegistry({configuration:t})}async function WVe(r,e,{configuration:t,scope:i}){let n=o=>a=>{let l=Ie.isIndexableObject(a)?a:{},c=l[o],u=Ie.isIndexableObject(c)?c:{};return{...l,[o]:{...u,npmAuthToken:e}}},s=i?{npmScopes:n(i)}:{npmRegistries:n(r)};return await ye.updateHomeConfiguration(s)}async function zVe({configuration:r,registry:e,report:t,stdin:i,stdout:n}){t.reportInfo(0,`Logging in to ${ee.pretty(r,e,ee.Type.URL)}`);let s=!1;if(e.match(/^https:\/\/npm\.pkg\.github\.com(\/|$)/)&&(t.reportInfo(0,"You seem to be using the GitHub Package Registry. Tokens must be generated with the 'repo', 'write:packages', and 'read:packages' permissions."),s=!0),t.reporLine truncated
|
||||
`);return a.exitCode()}};Yu.paths=[["npm","info"]],Yu.usage=ve.Usage({category:"Npm-related commands",description:"show information about a package",details:"\n This command fetches information about a package from the npm registry and prints it in a tree format.\n\n The package does not have to be installed locally, but needs to have been published (in particular, local changes will be ignored even for workspaces).\n\n Append `@<range>` to the package argument to provide information specific to the latest version that satisfies the range or to the corresponding tagged version. If the range is invalid or if there is no version satisfying the range, the command will print a warning and fall back to the latest version.\n\n If the `-f,--fields` option is set, it's a comma-separated list of fields which will be used to only display part of the package information.\n\n By default, this command won't return the `dist`, `readme`, and `users` fields, since they are often very long. To explicitly request those fields, explicitly list them with the `--fields` flag or request the output in JSON mode.\n ",examples:[["Show all available information about react (except the `dist`, `readme`, and `users` fields)","yarn npm info react"],["Show all available information about react as valid JSON (including the `dist`, `readme`, and `users` fields)","yarn npm info react --json"],["Show all available information about react@16.12.0","yarn npm info react@16.12.0"],["Show all available information about react@next","yarn npm info react@next"],["Show the description of react","yarn npm info react --fields description"],["Show all available versions of react","yarn npm info react --fields versions"],["Show the readme of react","yarn npm info react --fields readme"],["Show a few fields of react","yarn npm info react --fields homepage,repository"]]});function MM(r){if(Array.isArray(r)){let e=[];for(let t of r)t=MM(t),t&&e.push(t);return e}else if(typeof r=="object"&&r!==null){let e={};for(let t of Object.keys(r)){if(t.startsWith("_"))continue;let i=MM(r[t]);i&&(e[t]=i)}return e}else return r||null}var sue=Pe(Pm()),ju=class extends De{constructor(){super(...arguments);this.scope=z.String("-s,--filter",{description:"Login to the registry configured for a given scope"});this.publish=z.Boolean("--publish",!1,{description:"Login to the publish registry"})}async execute(){let t=await ye.find(this.context.cwd,this.context.plugins),i=await Qb({configuration:t,cwd:this.context.cwd,publish:this.publish,scope:this.scope});return(await Ge.start({configuration:t,stdout:this.context.stdout,includeFooter:!1},async s=>{let o=await zVe({configuration:t,registry:i,report:s,stdin:this.context.stdin,stdout:this.context.stdout}),a=`/-/user/org.couchdb.user:${encodeURIComponent(o.name)}`,l=await Mt.put(a,o,{attemptedAs:o.name,configuration:t,registry:i,jsonResponse:!0,authType:Mt.AuthType.NO_AUTH});return await WVe(i,l.token,{configuration:t,scope:this.scope}),s.reportInfo(0,"Successfully logged in")})).exitCode()}};ju.paths=[["npm","login"]],ju.usage=ve.Usage({category:"Npm-related commands",description:"store new login info to access the npm registry",details:"\n This command will ask you for your username, password, and 2FA One-Time-Password (when it applies). It will then modify your local configuration (in your home folder, never in the project itself) to reference the new tokens thus generated.\n\n Adding the `-s,--filter` flag will cause the authentication to be done against whatever registry is configured for the associated scope (see also `npmScopes`).\n\n Adding the `--publish` flag will cause the authentication to be done against the registry used when publishing the package (see also `publishConfig.registry` and `npmPublishRegistry`).\n ",examples:[["Login to the default registry","yarn npm login"],["Login to the registry linked to the @my-scope registry","yarn npm login --filter my-scope"],["Login to the publish registry for the current package","yarn npm login --publish"]]});async function Qb({scope:r,publish:e,configuration:t,cwd:i}){return r&&e?or.getScopeRegistry(r,{configuration:t,type:or.RegistryType.PUBLISH_REGISTRY}):r?or.getScopeRegistry(r,{configuration:t}):e?or.getPublishRegistry((await Ph(t,i)).manifest,{configuration:t}):or.getDefaultRegistry({configuration:t})}async function WVe(r,e,{configuration:t,scope:i}){let n=o=>a=>{let l=Ie.isIndexableObject(a)?a:{},c=l[o],u=Ie.isIndexableObject(c)?c:{};return{...l,[o]:{...u,npmAuthToken:e}}},s=i?{npmScopes:n(i)}:{npmRegistries:n(r)};return await ye.updateHomeConfiguration(s)}async function zVe({configuration:r,registry:e,report:t,stdin:i,stdout:n}){t.reportInfo(0,`Logging in to ${ee.pretty(r,e,ee.Type.URL)}`);let s=!1;if(e.match(/^https:\/\/npm\.pkg\.github\.com(\/|$)/)&&(t.reportInfo(0,"You seem to be using the GitHub Package Registry. Tokens must be generated with the 'repo', 'write:packages', and 'read:packages' permissions."),s=!0),t.reLine truncated
|
||||
This command will list all tags of a package from the npm registry.
|
||||
|
||||
If the package is not specified, Yarn will default to the current workspace.
|
||||
@@ -732,7 +732,7 @@ ${we.map(pe=>`Not found: ${lo(pe)}
|
||||
This command will add a tag to the npm registry for a specific version of a package. If the tag already exists, it will be overwritten.
|
||||
`,examples:[["Add a `beta` tag for version `2.3.4-beta.4` of package `my-pkg`","yarn npm tag add my-pkg@2.3.4-beta.4 beta"]]});var Vu=class extends De{constructor(){super(...arguments);this.package=z.String();this.tag=z.String()}async execute(){if(this.tag==="latest")throw new be("The 'latest' tag cannot be removed.");let t=await ye.find(this.context.cwd,this.context.plugins),{project:i,workspace:n}=await je.find(t,this.context.cwd);if(!n)throw new ct(i.cwd,this.context.cwd);let s=P.parseIdent(this.package),o=or.getPublishRegistry(n.manifest,{configuration:t}),a=ee.pretty(t,this.tag,ee.Type.CODE),l=ee.pretty(t,s,ee.Type.IDENT),c=await IE(s,t);if(!Object.prototype.hasOwnProperty.call(c,this.tag))throw new be(`${a} is not a tag of package ${l}`);return(await Ge.start({configuration:t,stdout:this.context.stdout},async g=>{let f=`/-/package${Mt.getIdentUrl(s)}/dist-tags/${encodeURIComponent(this.tag)}`;await Mt.del(f,{configuration:t,registry:o,ident:s,jsonResponse:!0}),g.reportInfo(0,`Tag ${a} removed from package ${l}`)})).exitCode()}};Vu.paths=[["npm","tag","remove"]],Vu.usage=ve.Usage({category:"Npm-related commands",description:"remove a tag from a package",details:`
|
||||
This command will remove a tag from a package from the npm registry.
|
||||
`,examples:[["Remove the `beta` tag from package `my-pkg`","yarn npm tag remove my-pkg beta"]]});var Xu=class extends De{constructor(){super(...arguments);this.scope=z.String("-s,--scope",{description:"Print username for the registry configured for a given scope"});this.publish=z.Boolean("--publish",!1,{description:"Print username for the publish registry"})}async execute(){let t=await ye.find(this.context.cwd,this.context.plugins),i;return this.scope&&this.publish?i=or.getScopeRegistry(this.scope,{configuration:t,type:or.RegistryType.PUBLISH_REGISTRY}):this.scope?i=or.getScopeRegistry(this.scope,{configuration:t}):this.publish?i=or.getPublishRegistry((await Ph(t,this.context.cwd)).manifest,{configuration:t}):i=or.getDefaultRegistry({configuration:t}),(await Ge.start({configuration:t,stdout:this.context.stdout},async s=>{var a,l;let o;try{o=await Mt.get("/-/whoami",{configuration:t,registry:i,authType:Mt.AuthType.ALWAYS_AUTH,jsonResponse:!0,ident:this.scope?P.makeIdent(this.scope,""):void 0})}catch(c){if(((a=c.response)==null?void 0:a.statusCode)===401||((l=c.response)==null?void 0:l.statusCode)===403){s.reportError(41,"Authentication failed - your credentials may have expired");return}else throw c}s.reportInfo(0,o.username)})).exitCode()}};Xu.paths=[["npm","whoami"]],Xu.usage=ve.Usage({category:"Npm-related commands",description:"display the name of the authenticated user",details:"\n Print the username associated with the current authentication settings to the standard output.\n\n When using `-s,--scope`, the username printed will be the one that matches the authentication settings of the registry associated with the given scope (those settings can be overriden using the `npmRegistries` map, and the registry associated with the scope is configured via the `npmScopes` map).\n\n When using `--publish`, the registry we'll select will by default be the one used when publishing packages (`publishConfig.registry` or `npmPublishRegistry` if available, otherwise we'll fallback to the regular `npmRegistryServer`).\n ",examples:[["Print username for the default registry","yarn npm whoami"],["Print username for the registry on a given scope","yarn npm whoami --scope company"]]});var _Ve={configuration:{npmPublishAccess:{description:"Default access of the published packages",type:"STRING",default:null},npmAuditExcludePackages:{description:"Array of glob patterns of packages to exclude from npm audit",type:"STRING",default:[],isArray:!0},npmAuditIgnoreAdvisories:{description:"Array of glob patterns of advisory IDs to exclude from npm audit",type:"STRING",default:[],isArray:!0}},commands:[Gu,Yu,ju,qu,Ju,zu,Wu,Vu,Xu]},ZVe=_Ve;var zM={};ut(zM,{default:()=>h6e,patchUtils:()=>_u});var _u={};ut(_u,{applyPatchFile:()=>Sb,diffFolders:()=>JM,ensureUnpatchedDescriptor:()=>HM,extractPackageToDisk:()=>qM,extractPatchFlags:()=>hue,isParentRequired:()=>jM,loadPatchFiles:()=>QE,makeDescriptor:()=>GM,makeLocator:()=>YM,makePatchHash:()=>WM,parseDescriptor:()=>wE,parseLocator:()=>BE,parsePatchFile:()=>yE});var $Ve=/^@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@.*/;function Ap(r){return x.relative(Oe.root,x.resolve(Oe.root,K.toPortablePath(r)))}function e6e(r){let e=r.trim().match($Ve);if(!e)throw new Error(`Bad header line: '${r}'`);return{original:{start:Math.max(Number(e[1]),1),length:Number(e[3]||1)},patched:{start:Math.max(Number(e[4]),1),length:Number(e[6]||1)}}}var t6e=420,r6e=493;var Aue=()=>({semverExclusivity:null,diffLineFromPath:null,diffLineToPath:null,oldMode:null,newMode:null,deletedFileMode:null,newFileMode:null,renameFrom:null,renameTo:null,beforeHash:null,afterHash:null,fromPath:null,toPath:null,hunks:null}),i6e=r=>({header:e6e(r),parts:[]}),n6e={["@"]:"header",["-"]:"deletion",["+"]:"insertion",[" "]:"context",["\\"]:"pragma",undefined:"context"};function s6e(r){let e=[],t=Aue(),i="parsing header",n=null,s=null;function o(){n&&(s&&(n.parts.push(s),s=null),t.hunks.push(n),n=null)}function a(){o(),e.push(t),t=Aue()}for(let l=0;l<r.length;l++){let c=r[l];if(i==="parsing header")if(c.startsWith("@@"))i="parsing hunks",t.hunks=[],l-=1;else if(c.startsWith("diff --git ")){t&&t.diffLineFromPath&&a();let u=c.match(/^diff --git a\/(.*?) b\/(.*?)\s*$/);if(!u)throw new Error(`Bad diff line: ${c}`);t.diffLineFromPath=u[1],t.diffLineToPath=u[2]}else if(c.startsWith("old mode "))t.oldMode=c.slice(9).trim();else if(c.startsWith("new mode "))t.newMode=c.slice(9).trim();else if(c.startsWith("deleted file mode "))t.deletedFileMode=c.slice(18).trim();else if(c.startsWith("new file mode "))t.newFileMode=c.slice(14).trim();else if(c.startsWith("rename from "))t.renameFrom=c.slice(12).trim();else if(c.startsWith("rename to "))t.renameTo=c.slice(10).trim();else if(c.startsWith("index ")){let u=c.match(/(\w+)\.\.(\w+)/);if(!u)continue;t.beforeHash=u[1],t.afterHash=u[2]}else c.startsWith("semver exclusivity ")?t.semverExclusivity=c.slice(19).trim():c.startsWith("--- ")?t.fromPath=c.slice(6).trim():c.startsWith("+++ ")&&(t.toP
Line truncated
|
||||
`,examples:[["Remove the `beta` tag from package `my-pkg`","yarn npm tag remove my-pkg beta"]]});var Xu=class extends De{constructor(){super(...arguments);this.scope=z.String("-s,--filter",{description:"Print username for the registry configured for a given scope"});this.publish=z.Boolean("--publish",!1,{description:"Print username for the publish registry"})}async execute(){let t=await ye.find(this.context.cwd,this.context.plugins),i;return this.scope&&this.publish?i=or.getScopeRegistry(this.scope,{configuration:t,type:or.RegistryType.PUBLISH_REGISTRY}):this.scope?i=or.getScopeRegistry(this.scope,{configuration:t}):this.publish?i=or.getPublishRegistry((await Ph(t,this.context.cwd)).manifest,{configuration:t}):i=or.getDefaultRegistry({configuration:t}),(await Ge.start({configuration:t,stdout:this.context.stdout},async s=>{var a,l;let o;try{o=await Mt.get("/-/whoami",{configuration:t,registry:i,authType:Mt.AuthType.ALWAYS_AUTH,jsonResponse:!0,ident:this.scope?P.makeIdent(this.scope,""):void 0})}catch(c){if(((a=c.response)==null?void 0:a.statusCode)===401||((l=c.response)==null?void 0:l.statusCode)===403){s.reportError(41,"Authentication failed - your credentials may have expired");return}else throw c}s.reportInfo(0,o.username)})).exitCode()}};Xu.paths=[["npm","whoami"]],Xu.usage=ve.Usage({category:"Npm-related commands",description:"display the name of the authenticated user",details:"\n Print the username associated with the current authentication settings to the standard output.\n\n When using `-s,--filter`, the username printed will be the one that matches the authentication settings of the registry associated with the given scope (those settings can be overriden using the `npmRegistries` map, and the registry associated with the scope is configured via the `npmScopes` map).\n\n When using `--publish`, the registry we'll select will by default be the one used when publishing packages (`publishConfig.registry` or `npmPublishRegistry` if available, otherwise we'll fallback to the regular `npmRegistryServer`).\n ",examples:[["Print username for the default registry","yarn npm whoami"],["Print username for the registry on a given scope","yarn npm whoami --filter company"]]});var _Ve={configuration:{npmPublishAccess:{description:"Default access of the published packages",type:"STRING",default:null},npmAuditExcludePackages:{description:"Array of glob patterns of packages to exclude from npm audit",type:"STRING",default:[],isArray:!0},npmAuditIgnoreAdvisories:{description:"Array of glob patterns of advisory IDs to exclude from npm audit",type:"STRING",default:[],isArray:!0}},commands:[Gu,Yu,ju,qu,Ju,zu,Wu,Vu,Xu]},ZVe=_Ve;var zM={};ut(zM,{default:()=>h6e,patchUtils:()=>_u});var _u={};ut(_u,{applyPatchFile:()=>Sb,diffFolders:()=>JM,ensureUnpatchedDescriptor:()=>HM,extractPackageToDisk:()=>qM,extractPatchFlags:()=>hue,isParentRequired:()=>jM,loadPatchFiles:()=>QE,makeDescriptor:()=>GM,makeLocator:()=>YM,makePatchHash:()=>WM,parseDescriptor:()=>wE,parseLocator:()=>BE,parsePatchFile:()=>yE});var $Ve=/^@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@.*/;function Ap(r){return x.relative(Oe.root,x.resolve(Oe.root,K.toPortablePath(r)))}function e6e(r){let e=r.trim().match($Ve);if(!e)throw new Error(`Bad header line: '${r}'`);return{original:{start:Math.max(Number(e[1]),1),length:Number(e[3]||1)},patched:{start:Math.max(Number(e[4]),1),length:Number(e[6]||1)}}}var t6e=420,r6e=493;var Aue=()=>({semverExclusivity:null,diffLineFromPath:null,diffLineToPath:null,oldMode:null,newMode:null,deletedFileMode:null,newFileMode:null,renameFrom:null,renameTo:null,beforeHash:null,afterHash:null,fromPath:null,toPath:null,hunks:null}),i6e=r=>({header:e6e(r),parts:[]}),n6e={["@"]:"header",["-"]:"deletion",["+"]:"insertion",[" "]:"context",["\\"]:"pragma",undefined:"context"};function s6e(r){let e=[],t=Aue(),i="parsing header",n=null,s=null;function o(){n&&(s&&(n.parts.push(s),s=null),t.hunks.push(n),n=null)}function a(){o(),e.push(t),t=Aue()}for(let l=0;l<r.length;l++){let c=r[l];if(i==="parsing header")if(c.startsWith("@@"))i="parsing hunks",t.hunks=[],l-=1;else if(c.startsWith("diff --git ")){t&&t.diffLineFromPath&&a();let u=c.match(/^diff --git a\/(.*?) b\/(.*?)\s*$/);if(!u)throw new Error(`Bad diff line: ${c}`);t.diffLineFromPath=u[1],t.diffLineToPath=u[2]}else if(c.startsWith("old mode "))t.oldMode=c.slice(9).trim();else if(c.startsWith("new mode "))t.newMode=c.slice(9).trim();else if(c.startsWith("deleted file mode "))t.deletedFileMode=c.slice(18).trim();else if(c.startsWith("new file mode "))t.newFileMode=c.slice(14).trim();else if(c.startsWith("rename from "))t.renameFrom=c.slice(12).trim();else if(c.startsWith("rename to "))t.renameTo=c.slice(10).trim();else if(c.startsWith("index ")){let u=c.match(/(\w+)\.\.(\w+)/);if(!u)continue;t.beforeHash=u[1],t.afterHash=u[2]}else c.startsWith("semver exclusivity ")?t.semverExclusivity=c.slice(19).trim():c.startsWith("--- ")?t.fromPath=c.slice(6).trim():c.startsWith("+++ ")&&(t.
Line truncated
|
||||
`)+(n.hunk.parts[0].noNewlineAtEndOfFile?"":`
|
||||
`):"";await e.mkdirpPromise(x.dirname(n.path),{chmod:493,utimes:[xr.SAFE_TIME,xr.SAFE_TIME]}),await e.writeFilePromise(n.path,s,{mode:n.mode}),await e.utimesPromise(n.path,xr.SAFE_TIME,xr.SAFE_TIME)}break;case"patch":await cp(e,n.path,async()=>{await c6e(n,{baseFs:e,dryRun:t})});break;case"mode change":{let o=(await e.statPromise(n.path)).mode;if(lue(n.newMode)!==lue(o))continue;await cp(e,n.path,async()=>{await e.chmodPromise(n.path,n.newMode)})}break;default:Ie.assertNever(n);break}}function lue(r){return(r&64)>0}function cue(r){return r.replace(/\s+$/,"")}function l6e(r,e){return cue(r)===cue(e)}async function c6e({hunks:r,path:e},{baseFs:t,dryRun:i=!1}){let n=await t.statSync(e).mode,o=(await t.readFileSync(e,"utf8")).split(/\n/),a=[],l=0,c=0;for(let g of r){let f=Math.max(c,g.header.patched.start+l),h=Math.max(0,f-c),p=Math.max(0,o.length-f-g.header.original.length),C=Math.max(h,p),y=0,B=0,v=null;for(;y<=C;){if(y<=h&&(B=f-y,v=uue(g,o,B),v!==null)){y=-y;break}if(y<=p&&(B=f+y,v=uue(g,o,B),v!==null))break;y+=1}if(v===null)throw new lp(r.indexOf(g),g);a.push(v),l+=y,c=B+g.header.original.length}if(i)return;let u=0;for(let g of a)for(let f of g)switch(f.type){case"splice":{let h=f.index+u;o.splice(h,f.numToDelete,...f.linesToInsert),u+=f.linesToInsert.length-f.numToDelete}break;case"pop":o.pop();break;case"push":o.push(f.line);break;default:Ie.assertNever(f);break}await t.writeFilePromise(e,o.join(`
|
||||
`),{mode:n})}function uue(r,e,t){let i=[];for(let n of r.parts)switch(n.type){case"context":case"deletion":{for(let s of n.lines){let o=e[t];if(o==null||!l6e(o,s))return null;t+=1}n.type==="deletion"&&(i.push({type:"splice",index:t-n.lines.length,numToDelete:n.lines.length,linesToInsert:[]}),n.noNewlineAtEndOfFile&&i.push({type:"push",line:""}))}break;case"insertion":i.push({type:"splice",index:t,numToDelete:0,linesToInsert:n.lines}),n.noNewlineAtEndOfFile&&i.push({type:"pop"});break;default:Ie.assertNever(n.type);break}return i}var g6e=/^builtin<([^>]+)>$/;function UM(r,e){let{source:t,selector:i,params:n}=P.parseRange(r);if(t===null)throw new Error("Patch locators must explicitly define their source");let s=i?i.split(/&/).map(c=>K.toPortablePath(c)):[],o=n&&typeof n.locator=="string"?P.parseLocator(n.locator):null,a=n&&typeof n.version=="string"?n.version:null,l=e(t);return{parentLocator:o,sourceItem:l,patchPaths:s,sourceVersion:a}}function wE(r){let{sourceItem:e,...t}=UM(r.range,P.parseDescriptor);return{...t,sourceDescriptor:e}}function BE(r){let{sourceItem:e,...t}=UM(r.reference,P.parseLocator);return{...t,sourceLocator:e}}function HM(r){if(!r.range.startsWith("patch:"))return r;let{sourceItem:e}=UM(r.range,P.parseDescriptor);return e}function gue({parentLocator:r,sourceItem:e,patchPaths:t,sourceVersion:i,patchHash:n},s){let o=r!==null?{locator:P.stringifyLocator(r)}:{},a=typeof i<"u"?{version:i}:{},l=typeof n<"u"?{hash:n}:{};return P.makeRange({protocol:"patch:",source:s(e),selector:t.join("&"),params:{...a,...l,...o}})}function GM(r,{parentLocator:e,sourceDescriptor:t,patchPaths:i}){return P.makeDescriptor(r,gue({parentLocator:e,sourceItem:t,patchPaths:i},P.stringifyDescriptor))}function YM(r,{parentLocator:e,sourcePackage:t,patchPaths:i,patchHash:n}){return P.makeLocator(r,gue({parentLocator:e,sourceItem:t,sourceVersion:t.version,patchPaths:i,patchHash:n},P.stringifyLocator))}function fue({onAbsolute:r,onRelative:e,onBuiltin:t},i){i.startsWith("~")&&(i=i.slice(1));let s=i.match(g6e);return s!==null?t(s[1]):x.isAbsolute(i)?r(i):e(i)}function hue(r){let e=r.startsWith("~");return e&&(r=r.slice(1)),{optional:e}}function jM(r){return fue({onAbsolute:()=>!1,onRelative:()=>!0,onBuiltin:()=>!1},r)}async function QE(r,e,t){let i=r!==null?await t.fetcher.fetch(r,t):null,n=i&&i.localPath?{packageFs:new jt(Oe.root),prefixPath:x.relative(Oe.root,i.localPath)}:i;i&&i!==n&&i.releaseFs&&i.releaseFs();let s=await Ie.releaseAfterUseAsync(async()=>await Promise.all(e.map(async o=>{let a=hue(o),l=await fue({onAbsolute:async()=>await M.readFilePromise(o,"utf8"),onRelative:async()=>{if(n===null)throw new Error("Assertion failed: The parent locator should have been fetched");return await n.packageFs.readFilePromise(x.join(n.prefixPath,o),"utf8")},onBuiltin:async c=>await t.project.configuration.firstHook(u=>u.getBuiltinPatch,t.project,c)},o);return{...a,source:l}})));for(let o of s)typeof o.source=="string"&&(o.source=o.source.replace(/\r\n?/g,`
|
||||
|
||||
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
+3
@@ -0,0 +1,3 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@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/prisma"
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- "@calcom/prisma"
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
undecided:
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
Vendored
Whitespace-only changes.
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- "@calcom/prisma"
|
||||
@@ -199,7 +199,7 @@ There's some settings that you'll need to setup.
|
||||
Under Vercel > Your API Project > 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`
|
||||
|
||||
In Git > Ignored Build Step
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"@sentry/nextjs": "^9.15.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"memory-cache": "^0.2.0",
|
||||
"next": "^15.3.0",
|
||||
"next": "^15.4.5",
|
||||
"next-api-middleware": "^1.0.1",
|
||||
"next-axiom": "^0.17.0",
|
||||
"next-swagger-doc": "^0.3.6",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getUserAvailability } from "@calcom/lib/getUserAvailability";
|
||||
import { getUserAvailabilityService } from "@calcom/lib/di/containers/GetUserAvailability";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import prisma from "@calcom/prisma";
|
||||
@@ -191,8 +191,9 @@ const availabilitySchema = z
|
||||
async function handler(req: NextApiRequest) {
|
||||
const { isSystemWideAdmin, userId: reqUserId } = req;
|
||||
const { username, userId, eventTypeId, dateTo, dateFrom, teamId } = availabilitySchema.parse(req.query);
|
||||
const userAvailabilityService = getUserAvailabilityService();
|
||||
if (!teamId)
|
||||
return getUserAvailability({
|
||||
return userAvailabilityService.getUserAvailability({
|
||||
username,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
@@ -230,7 +231,7 @@ async function handler(req: NextApiRequest) {
|
||||
const availabilities = members.map(async (user) => {
|
||||
return {
|
||||
userId: user.id,
|
||||
availability: await getUserAvailability({
|
||||
availability: await userAvailabilityService.getUserAvailability({
|
||||
userId: user.id,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { isSupportedTimeZone } from "@calcom/lib/dayjs";
|
||||
import { getAvailableSlotsService } from "@calcom/lib/di/containers/available-slots";
|
||||
import { getAvailableSlotsService } from "@calcom/lib/di/containers/AvailableSlots";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import { createContext } from "@calcom/trpc/server/createContext";
|
||||
|
||||
@@ -76,11 +76,13 @@ vi.mock("@calcom/features/watchlist/operations/check-if-users-are-blocked.contro
|
||||
checkIfUsersAreBlocked: vi.fn().mockResolvedValue(false),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/lib/bookings/findQualifiedHostsWithDelegationCredentials", () => ({
|
||||
findQualifiedHostsWithDelegationCredentials: vi.fn().mockResolvedValue({
|
||||
qualifiedRRHosts: [],
|
||||
allFallbackRRHosts: [],
|
||||
fixedHosts: [],
|
||||
vi.mock("@calcom/lib/di/containers/QualifiedHosts", () => ({
|
||||
getQualifiedHostsService: vi.fn().mockReturnValue({
|
||||
findQualifiedHostsWithDelegationCredentials: vi.fn().mockResolvedValue({
|
||||
qualifiedRRHosts: [],
|
||||
allFallbackRRHosts: [],
|
||||
fixedHosts: [],
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
|
||||
|
||||
@@ -42,15 +42,17 @@ $ yarn prisma generate
|
||||
|
||||
Copy `.env.example` to `.env` and fill values.
|
||||
|
||||
## Add license Key to deployments table in DB
|
||||
## Add license Key to Deployment table in DB
|
||||
|
||||
id, logo theme licenseKey agreedLicenseAt
|
||||
1, null, null, 'c4234812-12ab-42s6-a1e3-55bedd4a5bb7', '2023-05-15 21:39:47.611'
|
||||
id, logo, theme, licenseKey, agreedLicenseAt:-
|
||||
1, null, null, '00000000-0000-0000-0000-000000000000', '2023-05-15 21:39:47.611'
|
||||
|
||||
Replace with your actual license key.
|
||||
|
||||
your CALCOM_LICENSE_KEY env var need to contain the same value
|
||||
|
||||
.env
|
||||
CALCOM_LICENSE_KEY=c4234812-12ab-42s6-a1e3-55bedd4a5bb
|
||||
CALCOM_LICENSE_KEY=00000000-0000-0000-0000-000000000000
|
||||
|
||||
## Running the app
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.276",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.318",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
"@calcom/prisma": "*",
|
||||
@@ -98,6 +98,8 @@
|
||||
"@types/luxon": "^3.3.7",
|
||||
"@types/passport-jwt": "^3.0.13",
|
||||
"@types/supertest": "^2.0.12",
|
||||
"@typescript-eslint/eslint-plugin": "^6",
|
||||
"@typescript-eslint/parser": "^6",
|
||||
"jest": "^29.7.0",
|
||||
"jest-date-mock": "^1.0.10",
|
||||
"node-mocks-http": "^1.16.2",
|
||||
|
||||
+233
-3
@@ -15,11 +15,11 @@ import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
||||
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 { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.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 { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
EventTypeOutput_2024_06_14,
|
||||
GetBookingOutput_2024_08_13,
|
||||
GetBookingsOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
@@ -50,9 +51,9 @@ describe("Managed user bookings 2024-08-13", () => {
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let schedulesRepositoryFixture: SchedulesRepositoryFixture;
|
||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
|
||||
const platformAdminEmail = `managed-users-bookings-2024-08-13-admin-${randomString()}@api.com`;
|
||||
let platformAdmin: User;
|
||||
@@ -67,6 +68,10 @@ describe("Managed user bookings 2024-08-13", () => {
|
||||
let thirdManagedUser: CreateManagedUserData;
|
||||
|
||||
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 secondManagedUserBookingsCount = 0;
|
||||
@@ -85,9 +90,9 @@ describe("Managed user bookings 2024-08-13", () => {
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
schedulesRepositoryFixture = new SchedulesRepositoryFixture(moduleRef);
|
||||
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
|
||||
platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail });
|
||||
|
||||
@@ -95,6 +100,13 @@ describe("Managed user bookings 2024-08-13", () => {
|
||||
name: `oauth-client-users-organization-${randomString()}`,
|
||||
isPlatform: true,
|
||||
isOrganization: true,
|
||||
platformBilling: {
|
||||
create: {
|
||||
customerId: "cus_999",
|
||||
plan: "SCALE",
|
||||
subscriptionId: "sub_999",
|
||||
},
|
||||
},
|
||||
});
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
@@ -243,6 +255,59 @@ describe("Managed user bookings 2024-08-13", () => {
|
||||
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", () => {
|
||||
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 = {
|
||||
@@ -543,10 +608,175 @@ 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);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.delete(firstManagedUser.user.id);
|
||||
await userRepositoryFixture.delete(secondManagedUser.user.id);
|
||||
await userRepositoryFixture.delete(thirdManagedUser.user.id);
|
||||
await userRepositoryFixture.delete(orgAdminManagedUser.user.id);
|
||||
await userRepositoryFixture.delete(platformAdmin.id);
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
|
||||
@@ -702,6 +702,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
beforeUpdatedDate: queryParams.beforeUpdatedAt,
|
||||
afterCreatedDate: queryParams.afterCreatedAt,
|
||||
beforeCreatedDate: queryParams.beforeCreatedAt,
|
||||
bookingUid: queryParams.bookingUid,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -104,10 +104,11 @@ export class OutputBookingsService_2024_08_13 {
|
||||
const rescheduledToInfo = databaseBooking.rescheduled
|
||||
? await this.getRescheduledToInfo(databaseBooking.uid)
|
||||
: undefined;
|
||||
|
||||
const rescheduledToUid = rescheduledToInfo?.uid;
|
||||
const rescheduledByEmail = databaseBooking.rescheduled ? rescheduledToInfo?.rescheduledBy : databaseBooking.rescheduledBy;
|
||||
|
||||
const rescheduledToUid = rescheduledToInfo?.uid;
|
||||
const rescheduledByEmail = databaseBooking.rescheduled
|
||||
? rescheduledToInfo?.rescheduledBy
|
||||
: databaseBooking.rescheduledBy;
|
||||
|
||||
const booking = {
|
||||
id: databaseBooking.id,
|
||||
@@ -157,13 +158,12 @@ export class OutputBookingsService_2024_08_13 {
|
||||
|
||||
async getRescheduledToInfo(bookingUid: string): Promise<{ uid?: string; rescheduledBy?: string | null }> {
|
||||
const rescheduledTo = await this.bookingsRepository.getByFromReschedule(bookingUid);
|
||||
return {
|
||||
uid: rescheduledTo?.uid,
|
||||
rescheduledBy: rescheduledTo?.rescheduledBy
|
||||
return {
|
||||
uid: rescheduledTo?.uid,
|
||||
rescheduledBy: rescheduledTo?.rescheduledBy,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
getUserDefinedMetadata(databaseMetadata: DatabaseMetadata) {
|
||||
if (databaseMetadata === null) return {};
|
||||
|
||||
@@ -279,9 +279,11 @@ export class OutputBookingsService_2024_08_13 {
|
||||
const rescheduledToInfo = databaseBooking.rescheduled
|
||||
? await this.getRescheduledToInfo(databaseBooking.uid)
|
||||
: undefined;
|
||||
|
||||
|
||||
const rescheduledToUid = rescheduledToInfo?.uid;
|
||||
const rescheduledByEmail = databaseBooking.rescheduled ? rescheduledToInfo?.rescheduledBy : databaseBooking.rescheduledBy;
|
||||
const rescheduledByEmail = databaseBooking.rescheduled
|
||||
? rescheduledToInfo?.rescheduledBy
|
||||
: databaseBooking.rescheduledBy;
|
||||
|
||||
const booking = {
|
||||
id: databaseBooking.id,
|
||||
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import * as request from "supertest";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { CreatePrivateLinkInput } from "@calcom/platform-types";
|
||||
|
||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
|
||||
describe("Event Types Private Links Endpoints", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let oAuthClient: any;
|
||||
let organization: any;
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let user: any;
|
||||
let eventType: any;
|
||||
|
||||
const userEmail = `private-links-user-${randomString()}@api.com`;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||
imports: [AppModule, UsersModule, EventTypesModule_2024_06_14, TokensModule],
|
||||
})
|
||||
)
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
|
||||
organization = await teamRepositoryFixture.create({
|
||||
name: `private-links-organization-${randomString()}`,
|
||||
slug: `private-links-org-slug-${randomString()}`,
|
||||
});
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
name: `private-links-user-${randomString()}`,
|
||||
username: `private-links-user-${randomString()}`,
|
||||
});
|
||||
|
||||
// create an event type owned by user
|
||||
eventType = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: `private-links-event-type-${randomString()}`,
|
||||
slug: `private-links-event-type-${randomString()}`,
|
||||
length: 30,
|
||||
locations: [],
|
||||
},
|
||||
user.id
|
||||
);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["redirect-uri"],
|
||||
permissions: 32,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
it("POST /v2/event-types/:eventTypeId/private-links - create private link", async () => {
|
||||
const body: CreatePrivateLinkInput = {
|
||||
expiresAt: undefined,
|
||||
maxUsageCount: 5,
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/api/v2/event-types/${eventType.id}/private-links`)
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.send(body)
|
||||
.expect(201);
|
||||
|
||||
expect(response.body.status).toBe(SUCCESS_STATUS);
|
||||
expect(response.body.data.linkId).toBeDefined();
|
||||
expect(response.body.data.maxUsageCount).toBe(5);
|
||||
expect(response.body.data.usageCount).toBeDefined();
|
||||
});
|
||||
|
||||
it("GET /v2/event-types/:eventTypeId/private-links - list private links", async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types/${eventType.id}/private-links`)
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.status).toBe(SUCCESS_STATUS);
|
||||
expect(Array.isArray(response.body.data)).toBe(true);
|
||||
expect(response.body.data.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("PATCH /v2/event-types/:eventTypeId/private-links/:linkId - update private link", async () => {
|
||||
// create a link first
|
||||
const createResp = await request(app.getHttpServer())
|
||||
.post(`/api/v2/event-types/${eventType.id}/private-links`)
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.send({ maxUsageCount: 3 })
|
||||
.expect(201);
|
||||
|
||||
const linkId = createResp.body.data.linkId;
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.patch(`/api/v2/event-types/${eventType.id}/private-links/${linkId}`)
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.send({ maxUsageCount: 10 })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.status).toBe(SUCCESS_STATUS);
|
||||
expect(response.body.data.maxUsageCount).toBe(10);
|
||||
});
|
||||
|
||||
it("DELETE /v2/event-types/:eventTypeId/private-links/:linkId - delete private link", async () => {
|
||||
// create a link to delete
|
||||
const createResp = await request(app.getHttpServer())
|
||||
.post(`/api/v2/event-types/${eventType.id}/private-links`)
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.send({ maxUsageCount: 2 })
|
||||
.expect(201);
|
||||
|
||||
const linkId = createResp.body.data.linkId;
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.delete(`/api/v2/event-types/${eventType.id}/private-links/${linkId}`)
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.status).toBe(SUCCESS_STATUS);
|
||||
expect(response.body.data.linkId).toBe(linkId);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// cleanup created entities
|
||||
try {
|
||||
if (eventType?.id) {
|
||||
const repo = new EventTypesRepositoryFixture((app as any).select(AppModule));
|
||||
await repo.delete(eventType.id);
|
||||
}
|
||||
} catch {}
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { EventTypeOwnershipGuard } from "@/modules/event-types/guards/event-type-ownership.guard";
|
||||
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common";
|
||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import {
|
||||
CreatePrivateLinkInput,
|
||||
CreatePrivateLinkOutput,
|
||||
DeletePrivateLinkOutput,
|
||||
GetPrivateLinksOutput,
|
||||
UpdatePrivateLinkOutput,
|
||||
UpdatePrivateLinkBody,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
import { PrivateLinksService } from "../services/private-links.service";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/event-types/:eventTypeId/private-links",
|
||||
})
|
||||
@UseGuards(PermissionsGuard)
|
||||
@DocsTags("Event Types Private Links")
|
||||
export class EventTypesPrivateLinksController {
|
||||
constructor(private readonly privateLinksService: PrivateLinksService) {}
|
||||
|
||||
@Post("/")
|
||||
@Permissions([EVENT_TYPE_WRITE])
|
||||
@UseGuards(ApiAuthGuard, EventTypeOwnershipGuard)
|
||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@ApiOperation({ summary: "Create a private link for an event type" })
|
||||
async createPrivateLink(
|
||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||
@Body() body: CreatePrivateLinkInput,
|
||||
@GetUser("id") userId: number
|
||||
): Promise<CreatePrivateLinkOutput> {
|
||||
const privateLink = await this.privateLinksService.createPrivateLink(eventTypeId, userId, body);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: privateLink,
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/")
|
||||
@Permissions([EVENT_TYPE_READ])
|
||||
@UseGuards(ApiAuthGuard, EventTypeOwnershipGuard)
|
||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@ApiOperation({ summary: "Get all private links for an event type" })
|
||||
async getPrivateLinks(
|
||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number
|
||||
): Promise<GetPrivateLinksOutput> {
|
||||
const privateLinks = await this.privateLinksService.getPrivateLinks(eventTypeId);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: privateLinks,
|
||||
};
|
||||
}
|
||||
|
||||
@Patch("/:linkId")
|
||||
@Permissions([EVENT_TYPE_WRITE])
|
||||
@UseGuards(ApiAuthGuard, EventTypeOwnershipGuard)
|
||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@ApiOperation({ summary: "Update a private link for an event type" })
|
||||
async updatePrivateLink(
|
||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||
@Param("linkId") linkId: string,
|
||||
@Body() body: UpdatePrivateLinkBody
|
||||
): Promise<UpdatePrivateLinkOutput> {
|
||||
const updateInput = { ...body, linkId };
|
||||
const privateLink = await this.privateLinksService.updatePrivateLink(eventTypeId, updateInput);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: privateLink,
|
||||
};
|
||||
}
|
||||
|
||||
@Delete("/:linkId")
|
||||
@Permissions([EVENT_TYPE_WRITE])
|
||||
@UseGuards(ApiAuthGuard, EventTypeOwnershipGuard)
|
||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@ApiOperation({ summary: "Delete a private link for an event type" })
|
||||
async deletePrivateLink(
|
||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||
@Param("linkId") linkId: string
|
||||
): Promise<DeletePrivateLinkOutput> {
|
||||
await this.privateLinksService.deletePrivateLink(eventTypeId, linkId);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: {
|
||||
linkId,
|
||||
message: "Private link deleted successfully",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import {
|
||||
OPTIONAL_API_KEY_HEADER,
|
||||
OPTIONAL_X_CAL_CLIENT_ID_HEADER,
|
||||
OPTIONAL_X_CAL_SECRET_KEY_HEADER,
|
||||
} from "@/lib/docs/headers";
|
||||
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
||||
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
|
||||
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
|
||||
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common";
|
||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import {
|
||||
CreatePrivateLinkInput,
|
||||
CreatePrivateLinkOutput,
|
||||
DeletePrivateLinkOutput,
|
||||
GetPrivateLinksOutput,
|
||||
UpdatePrivateLinkInput,
|
||||
UpdatePrivateLinkOutput,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
import { PrivateLinksService } from "../services/private-links.service";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/organizations/:orgId/teams/:teamId/event-types/:eventTypeId/private-links",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@DocsTags("Orgs / Teams / Event Types / Private Links")
|
||||
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
|
||||
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
|
||||
@ApiHeader(OPTIONAL_API_KEY_HEADER)
|
||||
export class OrganizationsEventTypesPrivateLinksController {
|
||||
constructor(
|
||||
private readonly privateLinksService: PrivateLinksService,
|
||||
private readonly teamsEventTypesService: TeamsEventTypesService
|
||||
) {}
|
||||
|
||||
@Post("/")
|
||||
@Roles("TEAM_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||
@ApiOperation({ summary: "Create a private link for a team event type" })
|
||||
async createPrivateLink(
|
||||
@Param("teamId", ParseIntPipe) teamId: number,
|
||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||
@Body() body: CreatePrivateLinkInput
|
||||
): Promise<CreatePrivateLinkOutput> {
|
||||
await this.teamsEventTypesService.validateEventTypeExists(teamId, eventTypeId);
|
||||
// Use teamId as the seed for link generation in org/team context
|
||||
const privateLink = await this.privateLinksService.createPrivateLink(eventTypeId, teamId, body);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: privateLink,
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/")
|
||||
@Roles("TEAM_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||
@ApiOperation({ summary: "Get all private links for a team event type" })
|
||||
async getPrivateLinks(
|
||||
@Param("teamId", ParseIntPipe) teamId: number,
|
||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number
|
||||
): Promise<GetPrivateLinksOutput> {
|
||||
await this.teamsEventTypesService.validateEventTypeExists(teamId, eventTypeId);
|
||||
const privateLinks = await this.privateLinksService.getPrivateLinks(eventTypeId);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: privateLinks,
|
||||
};
|
||||
}
|
||||
|
||||
@Patch("/:linkId")
|
||||
@Roles("TEAM_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||
@ApiOperation({ summary: "Update a private link for a team event type" })
|
||||
async updatePrivateLink(
|
||||
@Param("teamId", ParseIntPipe) teamId: number,
|
||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||
@Param("linkId") linkId: string,
|
||||
@Body() body: Omit<UpdatePrivateLinkInput, "linkId">
|
||||
): Promise<UpdatePrivateLinkOutput> {
|
||||
await this.teamsEventTypesService.validateEventTypeExists(teamId, eventTypeId);
|
||||
const updateInput: UpdatePrivateLinkInput = { ...body, linkId };
|
||||
const privateLink = await this.privateLinksService.updatePrivateLink(eventTypeId, updateInput);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: privateLink,
|
||||
};
|
||||
}
|
||||
|
||||
@Delete("/:linkId")
|
||||
@Roles("TEAM_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||
@ApiOperation({ summary: "Delete a private link for a team event type" })
|
||||
async deletePrivateLink(
|
||||
@Param("teamId", ParseIntPipe) teamId: number,
|
||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||
@Param("linkId") linkId: string
|
||||
): Promise<DeletePrivateLinkOutput> {
|
||||
await this.teamsEventTypesService.validateEventTypeExists(teamId, eventTypeId);
|
||||
await this.privateLinksService.deletePrivateLink(eventTypeId, linkId);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: {
|
||||
linkId,
|
||||
message: "Private link deleted successfully",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
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 { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
import { EventTypesPrivateLinksController } from "./controllers/event-types-private-links.controller";
|
||||
import { PrivateLinksRepository } from "./private-links.repository";
|
||||
import { PrivateLinksInputService } from "./services/private-links-input.service";
|
||||
import { PrivateLinksOutputService } from "./services/private-links-output.service";
|
||||
import { PrivateLinksService } from "./services/private-links.service";
|
||||
|
||||
@Module({
|
||||
imports: [TokensModule, PrismaModule, EventTypesModule_2024_06_14],
|
||||
controllers: [EventTypesPrivateLinksController],
|
||||
providers: [
|
||||
PrivateLinksService,
|
||||
PrivateLinksInputService,
|
||||
PrivateLinksOutputService,
|
||||
PrivateLinksRepository,
|
||||
EventTypeOwnershipGuard,
|
||||
],
|
||||
exports: [PrivateLinksService],
|
||||
})
|
||||
export class EventTypesPrivateLinksModule {}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class PrivateLinksRepository {
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
async listByEventTypeId(eventTypeId: number) {
|
||||
return this.dbRead.prisma.hashedLink.findMany({
|
||||
where: { eventTypeId },
|
||||
select: { link: true, expiresAt: true, maxUsageCount: true, usageCount: true },
|
||||
});
|
||||
}
|
||||
|
||||
async findWithEventTypeDetails(linkId: string) {
|
||||
return this.dbRead.prisma.hashedLink.findUnique({
|
||||
where: { link: linkId },
|
||||
select: {
|
||||
link: true,
|
||||
expiresAt: true,
|
||||
maxUsageCount: true,
|
||||
usageCount: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async create(
|
||||
eventTypeId: number,
|
||||
link: { link: string; expiresAt: Date | null; maxUsageCount?: number | null }
|
||||
) {
|
||||
return this.dbWrite.prisma.hashedLink.create({
|
||||
data: {
|
||||
eventTypeId,
|
||||
link: link.link,
|
||||
expiresAt: link.expiresAt,
|
||||
...(typeof link.maxUsageCount === "number" ? { maxUsageCount: link.maxUsageCount } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async update(
|
||||
eventTypeId: number,
|
||||
link: { link: string; expiresAt: Date | null; maxUsageCount?: number | null }
|
||||
) {
|
||||
return this.dbWrite.prisma.hashedLink.updateMany({
|
||||
where: { eventTypeId, link: link.link },
|
||||
data: {
|
||||
expiresAt: link.expiresAt,
|
||||
...(typeof link.maxUsageCount === "number" ? { maxUsageCount: link.maxUsageCount } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async delete(eventTypeId: number, linkId: string) {
|
||||
return this.dbWrite.prisma.hashedLink.deleteMany({ where: { eventTypeId, link: linkId } });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { CreatePrivateLinkInput, UpdatePrivateLinkInput } from "@calcom/platform-types";
|
||||
|
||||
@Injectable()
|
||||
export class PrivateLinksInputService {
|
||||
constructor() {}
|
||||
|
||||
transformCreateInput(input: CreatePrivateLinkInput): CreatePrivateLinkInput {
|
||||
const hasExpires = input.expiresAt !== undefined && input.expiresAt !== null;
|
||||
const hasMaxCount = typeof input.maxUsageCount === "number";
|
||||
|
||||
if (!hasExpires && !hasMaxCount) {
|
||||
throw new Error("Either expiresAt or maxUsageCount must be provided");
|
||||
}
|
||||
|
||||
if (hasExpires && hasMaxCount) {
|
||||
throw new Error("Provide only one of expiresAt or maxUsageCount");
|
||||
}
|
||||
|
||||
return {
|
||||
expiresAt: input.expiresAt,
|
||||
maxUsageCount: input.maxUsageCount ?? (hasMaxCount ? input.maxUsageCount : undefined),
|
||||
};
|
||||
}
|
||||
|
||||
transformUpdateInput(input: UpdatePrivateLinkInput): UpdatePrivateLinkInput {
|
||||
return {
|
||||
linkId: input.linkId,
|
||||
expiresAt: input.expiresAt,
|
||||
maxUsageCount: input.maxUsageCount,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { plainToClass } from "class-transformer";
|
||||
|
||||
import {
|
||||
PrivateLinkOutput,
|
||||
TimeBasedPrivateLinkOutput,
|
||||
UsageBasedPrivateLinkOutput,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
export type PrivateLinkData = {
|
||||
id: string;
|
||||
eventTypeId: number;
|
||||
isExpired: boolean;
|
||||
bookingUrl: string;
|
||||
expiresAt?: Date | null;
|
||||
maxUsageCount?: number | null;
|
||||
usageCount?: number;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class PrivateLinksOutputService {
|
||||
constructor() {}
|
||||
|
||||
transformToOutput(data: PrivateLinkData): PrivateLinkOutput {
|
||||
const baseData = {
|
||||
linkId: data.id.toString(),
|
||||
eventTypeId: data.eventTypeId,
|
||||
isExpired: data.isExpired,
|
||||
bookingUrl: data.bookingUrl,
|
||||
};
|
||||
|
||||
if (data.expiresAt !== null && data.expiresAt !== undefined) {
|
||||
return plainToClass(TimeBasedPrivateLinkOutput, { ...baseData, expiresAt: data.expiresAt });
|
||||
}
|
||||
|
||||
return plainToClass(UsageBasedPrivateLinkOutput, {
|
||||
...baseData,
|
||||
maxUsageCount: data.maxUsageCount || 0,
|
||||
usageCount: data.usageCount || 0,
|
||||
});
|
||||
}
|
||||
|
||||
transformArrayToOutput(data: PrivateLinkData[]): PrivateLinkOutput[] {
|
||||
return data.map((item) => this.transformToOutput(item));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { PrivateLinksRepository } from "@/ee/event-types-private-links/private-links.repository";
|
||||
import { PrivateLinksInputService } from "@/ee/event-types-private-links/services/private-links-input.service";
|
||||
import {
|
||||
PrivateLinksOutputService,
|
||||
type PrivateLinkData,
|
||||
} from "@/ee/event-types-private-links/services/private-links-output.service";
|
||||
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
|
||||
|
||||
import { generateHashedLink, isLinkExpired } from "@calcom/platform-libraries/private-links";
|
||||
import { CreatePrivateLinkInput, PrivateLinkOutput, UpdatePrivateLinkInput } from "@calcom/platform-types";
|
||||
|
||||
@Injectable()
|
||||
export class PrivateLinksService {
|
||||
constructor(
|
||||
private readonly inputService: PrivateLinksInputService,
|
||||
private readonly outputService: PrivateLinksOutputService,
|
||||
private readonly repo: PrivateLinksRepository
|
||||
) {}
|
||||
|
||||
async createPrivateLink(
|
||||
eventTypeId: number,
|
||||
userId: number,
|
||||
input: CreatePrivateLinkInput
|
||||
): Promise<PrivateLinkOutput> {
|
||||
try {
|
||||
const transformedInput = this.inputService.transformCreateInput(input);
|
||||
const created = await this.repo.create(eventTypeId, {
|
||||
link: generateHashedLink(userId),
|
||||
expiresAt: transformedInput.expiresAt ?? null,
|
||||
maxUsageCount: transformedInput.maxUsageCount ?? null,
|
||||
});
|
||||
const mapped: PrivateLinkData = {
|
||||
id: created.link,
|
||||
eventTypeId,
|
||||
isExpired: isLinkExpired(created as any),
|
||||
bookingUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL || "https://cal.com"}/d/${created.link}`,
|
||||
expiresAt: created.expiresAt ?? null,
|
||||
maxUsageCount: (created as any).maxUsageCount ?? null,
|
||||
usageCount: (created as any).usageCount ?? 0,
|
||||
};
|
||||
return this.outputService.transformToOutput(mapped);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new BadRequestException(error.message);
|
||||
}
|
||||
throw new BadRequestException("Failed to create private link");
|
||||
}
|
||||
}
|
||||
|
||||
async getPrivateLinks(eventTypeId: number): Promise<PrivateLinkOutput[]> {
|
||||
try {
|
||||
const links = await this.repo.listByEventTypeId(eventTypeId);
|
||||
const mapped: PrivateLinkData[] = links.map((l) => ({
|
||||
id: l.link,
|
||||
eventTypeId,
|
||||
isExpired: isLinkExpired(l as any),
|
||||
bookingUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL || "https://cal.com"}/d/${l.link}`,
|
||||
expiresAt: l.expiresAt ?? null,
|
||||
maxUsageCount: l.maxUsageCount ?? null,
|
||||
usageCount: l.usageCount ?? 0,
|
||||
}));
|
||||
return this.outputService.transformArrayToOutput(mapped);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new BadRequestException(error.message);
|
||||
}
|
||||
throw new BadRequestException("Failed to get private links");
|
||||
}
|
||||
}
|
||||
|
||||
async updatePrivateLink(eventTypeId: number, input: UpdatePrivateLinkInput): Promise<PrivateLinkOutput> {
|
||||
try {
|
||||
const transformedInput = this.inputService.transformUpdateInput(input);
|
||||
const updatedResult = await this.repo.update(eventTypeId, {
|
||||
link: transformedInput.linkId,
|
||||
expiresAt: transformedInput.expiresAt ?? null,
|
||||
maxUsageCount: transformedInput.maxUsageCount ?? null,
|
||||
});
|
||||
if (!updatedResult || (updatedResult as any).count === 0) {
|
||||
throw new NotFoundException("Updated link not found");
|
||||
}
|
||||
const updated = await this.repo.findWithEventTypeDetails(transformedInput.linkId);
|
||||
if (!updated) throw new NotFoundException("Updated link not found");
|
||||
const mapped: PrivateLinkData = {
|
||||
id: updated.link,
|
||||
eventTypeId,
|
||||
isExpired: isLinkExpired(updated as any),
|
||||
bookingUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL || "https://cal.com"}/d/${updated.link}`,
|
||||
expiresAt: updated.expiresAt ?? null,
|
||||
maxUsageCount: updated.maxUsageCount ?? null,
|
||||
usageCount: updated.usageCount ?? 0,
|
||||
};
|
||||
return this.outputService.transformToOutput(mapped);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes("not found")) {
|
||||
throw new NotFoundException(error.message);
|
||||
}
|
||||
throw new BadRequestException(error.message);
|
||||
}
|
||||
throw new BadRequestException("Failed to update private link");
|
||||
}
|
||||
}
|
||||
|
||||
async deletePrivateLink(eventTypeId: number, linkId: string): Promise<void> {
|
||||
try {
|
||||
const { count } = await this.repo.delete(eventTypeId, linkId);
|
||||
if (count === 0) {
|
||||
throw new NotFoundException("Deleted link not found");
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes("not found")) {
|
||||
throw new NotFoundException(error.message);
|
||||
}
|
||||
throw new BadRequestException(error.message);
|
||||
}
|
||||
throw new BadRequestException("Failed to delete private link");
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -290,7 +290,10 @@ describe("Event types Endpoints", () => {
|
||||
expect(responseBookingFields).toBeDefined();
|
||||
// note(Lauris): response bookingFields are already existing default bookingFields + the new one
|
||||
const responseBookingField = responseBookingFields.find((field) => field.name === bookingFieldName);
|
||||
expect(responseBookingField).toEqual(bookingFields[0]);
|
||||
const fields = responseBookingField
|
||||
//@ts-ignore
|
||||
delete fields.labelAsSafeHtml
|
||||
expect(fields).toEqual(bookingFields[0]);
|
||||
eventType.bookingFields = responseBookingFields;
|
||||
});
|
||||
});
|
||||
|
||||
+45
@@ -2147,6 +2147,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 () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
|
||||
+3
@@ -89,6 +89,7 @@ type Input = Pick<
|
||||
| "hideCalendarEventDetails"
|
||||
| "hideOrganizerEmail"
|
||||
| "calVideoSettings"
|
||||
| "hidden"
|
||||
>;
|
||||
|
||||
@Injectable()
|
||||
@@ -127,6 +128,7 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
hideCalendarEventDetails,
|
||||
hideOrganizerEmail,
|
||||
calVideoSettings,
|
||||
hidden,
|
||||
} = databaseEventType;
|
||||
|
||||
const locations = this.transformLocations(databaseEventType.locations);
|
||||
@@ -203,6 +205,7 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
hideCalendarEventDetails,
|
||||
hideOrganizerEmail,
|
||||
calVideoSettings,
|
||||
hidden,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BookingsModule_2024_04_15 } from "@/ee/bookings/2024-04-15/bookings.module";
|
||||
import { BookingsModule_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.module";
|
||||
import { CalendarsModule } from "@/ee/calendars/calendars.module";
|
||||
import { EventTypesPrivateLinksModule } from "@/ee/event-types-private-links/event-types-private-links.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 { GcalModule } from "@/ee/gcal/gcal.module";
|
||||
@@ -35,6 +36,7 @@ import { Module } from "@nestjs/common";
|
||||
SlotsModule_2024_09_04,
|
||||
TeamsModule,
|
||||
RoutingFormsModule,
|
||||
EventTypesPrivateLinksModule,
|
||||
],
|
||||
})
|
||||
export class PlatformEndpointsModule implements NestModule {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
|
||||
import { PrismaEventTypeRepository } from "@/lib/repositories/prisma-event-type.repository";
|
||||
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
|
||||
import { PrismaMembershipRepository } from "@/lib/repositories/prisma-membership.repository";
|
||||
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
|
||||
import { PrismaRoutingFormResponseRepository } from "@/lib/repositories/prisma-routing-form-response.repository";
|
||||
import { PrismaScheduleRepository } from "@/lib/repositories/prisma-schedule.repository";
|
||||
@@ -8,9 +9,15 @@ import { PrismaSelectedSlotRepository } from "@/lib/repositories/prisma-selected
|
||||
import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository";
|
||||
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
|
||||
import { AvailableSlotsService } from "@/lib/services/available-slots.service";
|
||||
import { BusyTimesService } from "@/lib/services/busy-times.service";
|
||||
import { CacheService } from "@/lib/services/cache.service";
|
||||
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
||||
import { FilterHostsService } from "@/lib/services/filter-hosts.service";
|
||||
import { NoSlotsNotificationService } from "@/lib/services/no-slots-notification.service";
|
||||
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
|
||||
import { UserAvailabilityService } from "@/lib/services/user-availability.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
@@ -24,10 +31,17 @@ import { Module } from "@nestjs/common";
|
||||
PrismaEventTypeRepository,
|
||||
PrismaRoutingFormResponseRepository,
|
||||
PrismaTeamRepository,
|
||||
RedisService,
|
||||
PrismaFeaturesRepository,
|
||||
PrismaMembershipRepository,
|
||||
CheckBookingLimitsService,
|
||||
CacheService,
|
||||
AvailableSlotsService,
|
||||
UserAvailabilityService,
|
||||
BusyTimesService,
|
||||
FilterHostsService,
|
||||
QualifiedHostsService,
|
||||
NoSlotsNotificationService,
|
||||
],
|
||||
exports: [AvailableSlotsService],
|
||||
})
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { PrismaMembershipRepository as PrismaMembershipRepositoryLib } from "@calcom/platform-libraries/repositories";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@Injectable()
|
||||
export class PrismaMembershipRepository extends PrismaMembershipRepositoryLib {
|
||||
constructor(private readonly dbWrite: PrismaWriteService) {
|
||||
super(dbWrite.prisma as unknown as PrismaClient);
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,18 @@ import { PrismaScheduleRepository } from "@/lib/repositories/prisma-schedule.rep
|
||||
import { PrismaSelectedSlotRepository } from "@/lib/repositories/prisma-selected-slot.repository";
|
||||
import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository";
|
||||
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
|
||||
import { BusyTimesService } from "@/lib/services/busy-times.service";
|
||||
import { CacheService } from "@/lib/services/cache.service";
|
||||
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
||||
import { NoSlotsNotificationService } from "@/lib/services/no-slots-notification.service";
|
||||
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { AvailableSlotsService as BaseAvailableSlotsService } from "@calcom/platform-libraries/slots";
|
||||
|
||||
import { UserAvailabilityService } from "./user-availability.service";
|
||||
|
||||
@Injectable()
|
||||
export class AvailableSlotsService extends BaseAvailableSlotsService {
|
||||
constructor(
|
||||
@@ -24,7 +30,14 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
|
||||
selectedSlotRepository: PrismaSelectedSlotRepository,
|
||||
eventTypeRepository: PrismaEventTypeRepository,
|
||||
userRepository: PrismaUserRepository,
|
||||
featuresRepository: PrismaFeaturesRepository
|
||||
redisService: RedisService,
|
||||
featuresRepository: PrismaFeaturesRepository,
|
||||
qualifiedHostsService: QualifiedHostsService,
|
||||
checkBookingLimitsService: CheckBookingLimitsService,
|
||||
cacheService: CacheService,
|
||||
userAvailabilityService: UserAvailabilityService,
|
||||
busyTimesService: BusyTimesService,
|
||||
noSlotsNotificationService: NoSlotsNotificationService
|
||||
) {
|
||||
super({
|
||||
oooRepo: oooRepoDependency,
|
||||
@@ -35,8 +48,14 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
|
||||
selectedSlotRepo: selectedSlotRepository,
|
||||
eventTypeRepo: eventTypeRepository,
|
||||
userRepo: userRepository,
|
||||
checkBookingLimitsService: new CheckBookingLimitsService(bookingRepository) as any,
|
||||
cacheService: new CacheService(featuresRepository),
|
||||
redisClient: redisService,
|
||||
checkBookingLimitsService,
|
||||
cacheService,
|
||||
userAvailabilityService,
|
||||
busyTimesService,
|
||||
qualifiedHostsService,
|
||||
featuresRepo: featuresRepository,
|
||||
noSlotsNotificationService,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { BusyTimesService as BaseBusyTimesService } from "@calcom/platform-libraries/slots";
|
||||
|
||||
@Injectable()
|
||||
export class BusyTimesService extends BaseBusyTimesService {
|
||||
constructor(bookingRepository: PrismaBookingRepository) {
|
||||
super({
|
||||
bookingRepo: bookingRepository,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { FilterHostsService as BaseFilterHostsService } from "@calcom/platform-libraries/slots";
|
||||
|
||||
@Injectable()
|
||||
export class FilterHostsService extends BaseFilterHostsService {
|
||||
constructor(bookingRepository: PrismaBookingRepository) {
|
||||
super({
|
||||
bookingRepo: bookingRepository,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { PrismaMembershipRepository } from "@/lib/repositories/prisma-membership.repository";
|
||||
import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { NoSlotsNotificationService as BaseNoSlotsNotificationService } from "@calcom/platform-libraries/slots";
|
||||
|
||||
@Injectable()
|
||||
export class NoSlotsNotificationService extends BaseNoSlotsNotificationService {
|
||||
constructor(
|
||||
teamRepository: PrismaTeamRepository,
|
||||
membershipRepository: PrismaMembershipRepository,
|
||||
redisService: RedisService
|
||||
) {
|
||||
super({
|
||||
teamRepo: teamRepository,
|
||||
membershipRepo: membershipRepository,
|
||||
redisClient: redisService,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
|
||||
import { FilterHostsService } from "@/lib/services/filter-hosts.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { QualifiedHostsService as BaseQualifiedHostsService } from "@calcom/platform-libraries/slots";
|
||||
|
||||
@Injectable()
|
||||
export class QualifiedHostsService extends BaseQualifiedHostsService {
|
||||
constructor(bookingRepository: PrismaBookingRepository, filterHostsService: FilterHostsService) {
|
||||
super({
|
||||
bookingRepo: bookingRepository,
|
||||
filterHostsService,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
|
||||
import { PrismaEventTypeRepository } from "@/lib/repositories/prisma-event-type.repository";
|
||||
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { UserAvailabilityService as BaseUserAvailabilityService } from "@calcom/platform-libraries/schedules";
|
||||
|
||||
@Injectable()
|
||||
export class UserAvailabilityService extends BaseUserAvailabilityService {
|
||||
constructor(
|
||||
oooRepoDependency: PrismaOOORepository,
|
||||
bookingRepository: PrismaBookingRepository,
|
||||
eventTypeRepository: PrismaEventTypeRepository,
|
||||
redisService: RedisService
|
||||
) {
|
||||
super({
|
||||
oooRepo: oooRepoDependency,
|
||||
bookingRepo: bookingRepository,
|
||||
eventTypeRepo: eventTypeRepository,
|
||||
redisClient: redisService,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -403,6 +403,10 @@ export class BillingService implements OnModuleDestroy {
|
||||
fromReschedule?: string | null;
|
||||
}
|
||||
) {
|
||||
|
||||
if (this.configService.get("e2e")) {
|
||||
return true;
|
||||
}
|
||||
const { uid, startTime, fromReschedule } = booking;
|
||||
|
||||
const delay = startTime.getTime() - Date.now();
|
||||
@@ -427,6 +431,9 @@ export class BillingService implements OnModuleDestroy {
|
||||
* Removing an attendee from a booking does not cancel the usage increment job.
|
||||
*/
|
||||
async cancelUsageByBookingUid(bookingUid: string) {
|
||||
if (this.configService.get("e2e")) {
|
||||
return true;
|
||||
}
|
||||
const job = await this.billingQueue.getJob(`increment-${bookingUid}`);
|
||||
if (job) {
|
||||
await job.remove();
|
||||
@@ -458,6 +465,7 @@ export class BillingService implements OnModuleDestroy {
|
||||
|
||||
async onModuleDestroy() {
|
||||
try {
|
||||
|
||||
await this.billingQueue.close();
|
||||
} catch (err) {
|
||||
this.logger.error(err);
|
||||
|
||||
+52
-2
@@ -2,10 +2,21 @@ import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { GetUnifiedCalendarEventOutput } from "@/modules/cal-unified-calendars/outputs/get-unified-calendar-event";
|
||||
import { UpdateUnifiedCalendarEventInput } from "@/modules/cal-unified-calendars/inputs/update-unified-calendar-event.input";
|
||||
import { GetUnifiedCalendarEventOutput } from "@/modules/cal-unified-calendars/outputs/get-unified-calendar-event.output";
|
||||
import { GoogleCalendarEventOutputPipe } from "@/modules/cal-unified-calendars/pipes/get-calendar-event-details-output-pipe";
|
||||
import { GoogleCalendarService } from "@/modules/cal-unified-calendars/services/google-calendar.service";
|
||||
import { Controller, Get, Param, UseGuards, HttpCode, HttpStatus, BadRequestException } from "@nestjs/common";
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
UseGuards,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
BadRequestException,
|
||||
Patch,
|
||||
Body,
|
||||
} from "@nestjs/common";
|
||||
import { ApiTags as DocsTags, ApiParam, ApiHeader, ApiOperation } from "@nestjs/swagger";
|
||||
|
||||
import { GOOGLE_CALENDAR, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
@@ -30,6 +41,7 @@ export class CalUnifiedCalendarsController {
|
||||
type: String,
|
||||
})
|
||||
@Get("/:calendar/event/:eventUid")
|
||||
@Get("/:calendar/events/:eventUid")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(ApiAuthGuard, PermissionsGuard)
|
||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@@ -54,4 +66,42 @@ export class CalUnifiedCalendarsController {
|
||||
data: transformedEvent,
|
||||
};
|
||||
}
|
||||
|
||||
@ApiParam({
|
||||
name: "calendar",
|
||||
enum: [GOOGLE_CALENDAR],
|
||||
type: String,
|
||||
})
|
||||
@ApiParam({
|
||||
name: "eventUid",
|
||||
description:
|
||||
"The Google Calendar event ID. You can retrieve this by getting booking references from the following endpoints:\n\n- For team events: https://cal.com/docs/api-reference/v2/orgs-teams-bookings/get-booking-references-for-a-booking\n\n- For user events: https://cal.com/docs/api-reference/v2/bookings/get-booking-references-for-a-booking",
|
||||
type: String,
|
||||
})
|
||||
@Patch("/:calendar/events/:eventUid")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(ApiAuthGuard, PermissionsGuard)
|
||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@ApiOperation({
|
||||
summary: "Update meeting details in calendar",
|
||||
description: "Updates event information in the specified calendar provider",
|
||||
})
|
||||
async updateCalendarEvent(
|
||||
@Param("calendar") calendar: string,
|
||||
@Param("eventUid") eventUid: string,
|
||||
@Body() updateData: UpdateUnifiedCalendarEventInput
|
||||
): Promise<GetUnifiedCalendarEventOutput> {
|
||||
if (calendar !== GOOGLE_CALENDAR) {
|
||||
throw new BadRequestException("Event updates are currently only available for Google Calendar");
|
||||
}
|
||||
|
||||
const updatedEvent = await this.googleCalendarService.updateEventDetails(eventUid, updateData);
|
||||
|
||||
const transformedEvent = new GoogleCalendarEventOutputPipe().transform(updatedEvent);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: transformedEvent,
|
||||
};
|
||||
}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsISO8601, IsOptional, IsString, ValidateNested, IsEnum, IsArray } from "class-validator";
|
||||
|
||||
import { CalendarEventStatus, CalendarEventResponseStatus } from "../outputs/get-unified-calendar-event.output";
|
||||
|
||||
export class UpdateCalendarEventAttendee {
|
||||
@IsString()
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
description: "Email address of the attendee",
|
||||
})
|
||||
email!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
description: "Display name of the attendee",
|
||||
})
|
||||
name?: string;
|
||||
|
||||
@IsEnum(CalendarEventResponseStatus)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
enum: CalendarEventResponseStatus,
|
||||
enumName: "CalendarEventResponseStatus",
|
||||
nullable: true,
|
||||
description: "Response status of the attendee",
|
||||
})
|
||||
responseStatus?: CalendarEventResponseStatus | null;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
nullable: true,
|
||||
description: "Indicates if this attendee is the current user",
|
||||
})
|
||||
self?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
nullable: true,
|
||||
description: "Indicates if this attendee's attendance is optional",
|
||||
})
|
||||
optional?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
nullable: true,
|
||||
description: "Indicates if this attendee is the host",
|
||||
})
|
||||
host?: boolean;
|
||||
|
||||
}
|
||||
|
||||
export class UpdateDateTimeWithZone {
|
||||
@IsISO8601()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: "string", format: "date-time" })
|
||||
time?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
timeZone?: string;
|
||||
}
|
||||
|
||||
export class UpdateUnifiedCalendarEventInput {
|
||||
@ValidateNested()
|
||||
@Type(() => UpdateDateTimeWithZone)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
type: "object",
|
||||
properties: {
|
||||
time: { type: "string", format: "date-time" },
|
||||
timeZone: { type: "string" },
|
||||
},
|
||||
description: "Start date and time of the calendar event with timezone information",
|
||||
})
|
||||
start?: UpdateDateTimeWithZone;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => UpdateDateTimeWithZone)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
type: "object",
|
||||
properties: {
|
||||
time: { type: "string", format: "date-time" },
|
||||
timeZone: { type: "string" },
|
||||
},
|
||||
description: "End date and time of the calendar event with timezone information",
|
||||
})
|
||||
end?: UpdateDateTimeWithZone;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
description: "Title of the calendar event",
|
||||
})
|
||||
title?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
nullable: true,
|
||||
description: "Detailed description of the calendar event",
|
||||
})
|
||||
description?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => UpdateCalendarEventAttendee)
|
||||
@ApiPropertyOptional({
|
||||
type: [UpdateCalendarEventAttendee],
|
||||
nullable: true,
|
||||
description:
|
||||
"List of attendees. CAUTION: You must pass the entire array with all updated values. Any attendees not included in this array will be removed from the event.",
|
||||
})
|
||||
attendees?: UpdateCalendarEventAttendee[];
|
||||
|
||||
@IsEnum(CalendarEventStatus)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
enum: CalendarEventStatus,
|
||||
enumName: "CalendarEventStatus",
|
||||
nullable: true,
|
||||
description: "Status of the event (accepted, pending, declined, cancelled)",
|
||||
example: CalendarEventStatus.ACCEPTED,
|
||||
})
|
||||
status?: CalendarEventStatus | null;
|
||||
}
|
||||
+34
@@ -225,6 +225,22 @@ export class CalendarEventHost {
|
||||
responseStatus!: CalendarEventResponseStatus | null;
|
||||
}
|
||||
|
||||
export class calendarEventOwner {
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
description: "Email address of the event host",
|
||||
})
|
||||
email!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
nullable: true,
|
||||
description: "Display name of the event host",
|
||||
})
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export class CalendarEventAttendee {
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
@@ -264,6 +280,14 @@ export class CalendarEventAttendee {
|
||||
description: "Indicates if this attendee's attendance is optional",
|
||||
})
|
||||
optional?: boolean;
|
||||
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
nullable: true,
|
||||
description: "Indicates if this attendee is the host",
|
||||
})
|
||||
host?: boolean;
|
||||
}
|
||||
|
||||
export class DateTimeWithZone {
|
||||
@@ -384,6 +408,16 @@ export class UnifiedCalendarEventOutput {
|
||||
})
|
||||
hosts?: CalendarEventHost[];
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => calendarEventOwner)
|
||||
@ApiPropertyOptional({
|
||||
type: calendarEventOwner,
|
||||
nullable: true,
|
||||
description: "The calendar account that owns this event. This is the primary calendar where the event is stored and cannot be modified without appropriate permissions. Changing this would require moving the event to a different calendar",
|
||||
})
|
||||
calendarEventOwner?: calendarEventOwner;
|
||||
|
||||
@IsEnum(CALENDARS)
|
||||
@ApiProperty({
|
||||
enum: CALENDARS,
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
import { GoogleCalendarEventResponse } from "../get-calendar-event-details-output-pipe";
|
||||
|
||||
export const createGoogleCalendarEventFixture = (
|
||||
overrides: Partial<GoogleCalendarEventResponse> = {}
|
||||
): GoogleCalendarEventResponse => {
|
||||
const baseEvent: GoogleCalendarEventResponse = {
|
||||
kind: "calendar#event",
|
||||
etag: "test-etag",
|
||||
id: "test-event-id",
|
||||
status: "confirmed",
|
||||
htmlLink: "https://calendar.google.com/event",
|
||||
created: "2024-01-01T00:00:00Z",
|
||||
updated: "2024-01-01T00:00:00Z",
|
||||
summary: "Test Meeting",
|
||||
description: "Test description",
|
||||
creator: {
|
||||
email: "creator@example.com",
|
||||
displayName: "Creator Name",
|
||||
},
|
||||
organizer: {
|
||||
email: "organizer@example.com",
|
||||
displayName: "Organizer Name",
|
||||
},
|
||||
start: {
|
||||
dateTime: "2024-01-15T10:00:00Z",
|
||||
timeZone: "America/New_York",
|
||||
},
|
||||
end: {
|
||||
dateTime: "2024-01-15T11:00:00Z",
|
||||
timeZone: "America/New_York",
|
||||
},
|
||||
iCalUID: "test-ical-uid",
|
||||
sequence: 0,
|
||||
attendees: [
|
||||
{
|
||||
email: "attendee@example.com",
|
||||
displayName: "Attendee Name",
|
||||
responseStatus: "accepted",
|
||||
organizer: false,
|
||||
},
|
||||
{
|
||||
email: "organizer@example.com",
|
||||
displayName: "Organizer Name",
|
||||
responseStatus: "accepted",
|
||||
organizer: true,
|
||||
},
|
||||
],
|
||||
conferenceData: {
|
||||
conferenceId: "abc-def-ghi",
|
||||
entryPoints: [
|
||||
{
|
||||
entryPointType: "video",
|
||||
uri: "https://meet.google.com/abc-def-ghi",
|
||||
label: "meet.google.com/abc-def-ghi",
|
||||
},
|
||||
],
|
||||
conferenceSolution: {
|
||||
key: {
|
||||
type: "hangoutsMeet",
|
||||
},
|
||||
name: "Google Meet",
|
||||
iconUri:
|
||||
"https://fonts.gstatic.com/s/i/productlogos/meet_2020q4/v6/web-512dp/logo_meet_2020q4_color_2x_web_512dp.png",
|
||||
},
|
||||
},
|
||||
hangoutLink: "https://meet.google.com/abc-def-ghi",
|
||||
};
|
||||
|
||||
return { ...baseEvent, ...overrides };
|
||||
};
|
||||
|
||||
export const googleEventWithConferenceData = createGoogleCalendarEventFixture({
|
||||
conferenceData: {
|
||||
conferenceId: "test-conference-id",
|
||||
entryPoints: [
|
||||
{
|
||||
entryPointType: "video",
|
||||
uri: "https://meet.google.com/abc-def-ghi",
|
||||
label: "Google Meet",
|
||||
pin: "123456",
|
||||
regionCode: "US",
|
||||
},
|
||||
{
|
||||
entryPointType: "phone",
|
||||
uri: "tel:+1-555-123-4567",
|
||||
label: "Phone",
|
||||
pin: "789012",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
export const googleEventWithLocationOnly = createGoogleCalendarEventFixture({
|
||||
location: "123 Main St, City, State",
|
||||
conferenceData: undefined,
|
||||
hangoutLink: undefined,
|
||||
});
|
||||
|
||||
export const googleEventWithHangoutLink = createGoogleCalendarEventFixture({
|
||||
hangoutLink: "https://hangouts.google.com/call/abc123",
|
||||
conferenceData: undefined,
|
||||
location: undefined,
|
||||
});
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
import {
|
||||
CalendarEventStatus,
|
||||
CalendarEventResponseStatus,
|
||||
} from "@/modules/cal-unified-calendars/outputs/get-unified-calendar-event.output";
|
||||
|
||||
import {
|
||||
createGoogleCalendarEventFixture,
|
||||
googleEventWithConferenceData,
|
||||
googleEventWithLocationOnly,
|
||||
googleEventWithHangoutLink,
|
||||
} from "./__fixtures__/google-calendar-event.fixture";
|
||||
import {
|
||||
GoogleCalendarEventOutputPipe,
|
||||
GoogleCalendarEventResponse,
|
||||
} from "./get-calendar-event-details-output-pipe";
|
||||
|
||||
describe("GoogleCalendarEventOutputPipe", () => {
|
||||
let pipe: GoogleCalendarEventOutputPipe;
|
||||
let sharedGoogleEvent: GoogleCalendarEventResponse;
|
||||
|
||||
beforeAll(() => {
|
||||
sharedGoogleEvent = createGoogleCalendarEventFixture();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
pipe = new GoogleCalendarEventOutputPipe();
|
||||
});
|
||||
|
||||
describe("transform", () => {
|
||||
it("should transform Google Calendar event to unified format", () => {
|
||||
const result = pipe.transform(sharedGoogleEvent);
|
||||
|
||||
expect(result.id).toBe("test-event-id");
|
||||
expect(result.title).toBe("Test Meeting");
|
||||
expect(result.description).toBe("Test description");
|
||||
expect(result.source).toBe("google");
|
||||
expect(result.status).toBe(CalendarEventStatus.ACCEPTED);
|
||||
expect(result.start.time).toBe("2024-01-15T10:00:00Z");
|
||||
expect(result.start.timeZone).toBe("America/New_York");
|
||||
expect(result.end.time).toBe("2024-01-15T11:00:00Z");
|
||||
expect(result.end.timeZone).toBe("America/New_York");
|
||||
expect(result.attendees).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("transformDateTimeWithZone", () => {
|
||||
it("should transform Google date time to unified format", () => {
|
||||
const googleDateTime = {
|
||||
dateTime: "2024-01-15T10:00:00Z",
|
||||
timeZone: "America/New_York",
|
||||
};
|
||||
|
||||
const result = pipe["transformDateTimeWithZone"](googleDateTime);
|
||||
|
||||
expect(result.time).toBe("2024-01-15T10:00:00Z");
|
||||
expect(result.timeZone).toBe("America/New_York");
|
||||
});
|
||||
});
|
||||
|
||||
describe("transformAttendeeResponseStatus", () => {
|
||||
it("should transform accepted status", () => {
|
||||
const result = pipe["transformAttendeeResponseStatus"]("accepted");
|
||||
expect(result).toBe(CalendarEventResponseStatus.ACCEPTED);
|
||||
});
|
||||
|
||||
it("should transform tentative status", () => {
|
||||
const result = pipe["transformAttendeeResponseStatus"]("tentative");
|
||||
expect(result).toBe(CalendarEventResponseStatus.PENDING);
|
||||
});
|
||||
|
||||
it("should transform declined status", () => {
|
||||
const result = pipe["transformAttendeeResponseStatus"]("declined");
|
||||
expect(result).toBe(CalendarEventResponseStatus.DECLINED);
|
||||
});
|
||||
|
||||
it("should transform needsaction status", () => {
|
||||
const result = pipe["transformAttendeeResponseStatus"]("needsaction");
|
||||
expect(result).toBe(CalendarEventResponseStatus.NEEDS_ACTION);
|
||||
});
|
||||
|
||||
it("should handle case insensitive status", () => {
|
||||
const result = pipe["transformAttendeeResponseStatus"]("ACCEPTED");
|
||||
expect(result).toBe(CalendarEventResponseStatus.ACCEPTED);
|
||||
});
|
||||
|
||||
it("should handle unknown status", () => {
|
||||
const result = pipe["transformAttendeeResponseStatus"]("unknown");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should handle null status", () => {
|
||||
const result = pipe["transformAttendeeResponseStatus"](undefined);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("transformEventStatus", () => {
|
||||
it("should transform confirmed status", () => {
|
||||
const result = pipe["transformEventStatus"]("confirmed");
|
||||
expect(result).toBe(CalendarEventStatus.ACCEPTED);
|
||||
});
|
||||
|
||||
it("should transform tentative status", () => {
|
||||
const result = pipe["transformEventStatus"]("tentative");
|
||||
expect(result).toBe(CalendarEventStatus.PENDING);
|
||||
});
|
||||
|
||||
it("should transform cancelled status", () => {
|
||||
const result = pipe["transformEventStatus"]("cancelled");
|
||||
expect(result).toBe(CalendarEventStatus.CANCELLED);
|
||||
});
|
||||
|
||||
it("should handle case insensitive status", () => {
|
||||
const result = pipe["transformEventStatus"]("CONFIRMED");
|
||||
expect(result).toBe(CalendarEventStatus.ACCEPTED);
|
||||
});
|
||||
|
||||
it("should handle unknown status", () => {
|
||||
const result = pipe["transformEventStatus"]("unknown");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should handle null status", () => {
|
||||
const result = pipe["transformEventStatus"](undefined);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("transformOrganizer", () => {
|
||||
it("should return calendarEventOwner organizer exist", () => {
|
||||
const eventWithoutOrganizerAttendees = createGoogleCalendarEventFixture({
|
||||
organizer: { email: "organizer@example.com" },
|
||||
attendees: [
|
||||
{
|
||||
email: "attendee@example.com",
|
||||
displayName: "Regular Attendee",
|
||||
responseStatus: "accepted",
|
||||
organizer: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = pipe.transform(eventWithoutOrganizerAttendees);
|
||||
|
||||
expect(result?.calendarEventOwner?.email).toBe("organizer@example.com");
|
||||
});
|
||||
});
|
||||
|
||||
describe("transformLocations", () => {
|
||||
it("should transform conference data entry points", () => {
|
||||
const result = pipe["transformLocations"](googleEventWithConferenceData);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toEqual({
|
||||
type: "video",
|
||||
url: "https://meet.google.com/abc-def-ghi",
|
||||
label: "Google Meet",
|
||||
pin: "123456",
|
||||
regionCode: "US",
|
||||
});
|
||||
expect(result[1]).toEqual({
|
||||
type: "phone",
|
||||
url: "tel:+1-555-123-4567",
|
||||
label: "Phone",
|
||||
pin: "789012",
|
||||
regionCode: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("should fallback to location field", () => {
|
||||
const result = pipe["transformLocations"](googleEventWithLocationOnly);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({
|
||||
type: "video",
|
||||
url: "123 Main St, City, State",
|
||||
});
|
||||
});
|
||||
|
||||
it("should fallback to hangout link", () => {
|
||||
const result = pipe["transformLocations"](googleEventWithHangoutLink);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({
|
||||
type: "video",
|
||||
url: "https://hangouts.google.com/call/abc123",
|
||||
});
|
||||
});
|
||||
|
||||
it("should return empty array when no location data", () => {
|
||||
const eventWithoutLocation = createGoogleCalendarEventFixture({
|
||||
location: undefined,
|
||||
conferenceData: undefined,
|
||||
hangoutLink: undefined,
|
||||
});
|
||||
|
||||
const result = pipe["transformLocations"](eventWithoutLocation);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
+7
-6
@@ -3,7 +3,7 @@ import {
|
||||
CalendarEventStatus,
|
||||
DateTimeWithZone,
|
||||
UnifiedCalendarEventOutput,
|
||||
} from "@/modules/cal-unified-calendars/outputs/get-unified-calendar-event";
|
||||
} from "@/modules/cal-unified-calendars/outputs/get-unified-calendar-event.output";
|
||||
import { PipeTransform, Injectable } from "@nestjs/common";
|
||||
|
||||
export interface GoogleCalendarEventResponse {
|
||||
@@ -94,14 +94,12 @@ export class GoogleCalendarEventOutputPipe
|
||||
calendarEvent.locations = this.transformLocations(googleEvent);
|
||||
|
||||
if (googleEvent.attendees && googleEvent.attendees.length > 0) {
|
||||
calendarEvent.attendees = googleEvent.attendees
|
||||
.filter((attendee) => !attendee.organizer)
|
||||
.map((attendee) => {
|
||||
calendarEvent.attendees = googleEvent.attendees.map((attendee) => {
|
||||
return {
|
||||
email: attendee.email,
|
||||
name: attendee.displayName,
|
||||
responseStatus: this.transformAttendeeResponseStatus(attendee.responseStatus),
|
||||
organizer: attendee.organizer,
|
||||
host: attendee.organizer,
|
||||
self: attendee.self,
|
||||
optional: attendee.optional,
|
||||
};
|
||||
@@ -110,7 +108,8 @@ export class GoogleCalendarEventOutputPipe
|
||||
|
||||
calendarEvent.status = this.transformEventStatus(googleEvent.status);
|
||||
|
||||
calendarEvent.hosts = this.transformHosts(googleEvent);
|
||||
calendarEvent.calendarEventOwner = googleEvent.organizer
|
||||
calendarEvent.hosts = this.transformHosts(googleEvent)
|
||||
|
||||
calendarEvent.source = "google";
|
||||
|
||||
@@ -196,6 +195,8 @@ export class GoogleCalendarEventOutputPipe
|
||||
regionCode: entryPoint.regionCode,
|
||||
};
|
||||
});
|
||||
} else if (googleEvent.location) {
|
||||
return [{ type: "video", url: googleEvent.location }];
|
||||
} else if (googleEvent.hangoutLink) {
|
||||
return [{ type: "video", url: googleEvent.hangoutLink }];
|
||||
}
|
||||
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import { GoogleCalendarEventResponse } from "@/modules/cal-unified-calendars/pipes/get-calendar-event-details-output-pipe";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import {
|
||||
UpdateUnifiedCalendarEventInput,
|
||||
UpdateDateTimeWithZone,
|
||||
} from "../inputs/update-unified-calendar-event.input";
|
||||
import { CalendarEventResponseStatus, CalendarEventStatus } from "../outputs/get-unified-calendar-event.output";
|
||||
|
||||
// Common interfaces for Google Calendar types
|
||||
interface GoogleCalendarDateTime {
|
||||
dateTime: string;
|
||||
timeZone: string;
|
||||
}
|
||||
|
||||
interface GoogleCalendarAttendee {
|
||||
email: string;
|
||||
displayName?: string;
|
||||
responseStatus: string;
|
||||
organizer?: boolean;
|
||||
}
|
||||
|
||||
interface GoogleCalendarEventInputTransform {
|
||||
transform(
|
||||
updateData: UpdateUnifiedCalendarEventInput,
|
||||
fetchedEvent?: GoogleCalendarEventResponse | null
|
||||
): any;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class GoogleCalendarEventInputPipe implements GoogleCalendarEventInputTransform {
|
||||
transform(
|
||||
updateData: UpdateUnifiedCalendarEventInput,
|
||||
): any {
|
||||
const updatePayload: any = {};
|
||||
|
||||
if (updateData.title !== undefined) {
|
||||
updatePayload.summary = updateData.title;
|
||||
}
|
||||
|
||||
if (updateData.description !== undefined) {
|
||||
updatePayload.description = updateData.description;
|
||||
}
|
||||
|
||||
if (updateData.start) {
|
||||
updatePayload.start = this.transformDateTimeWithZone(updateData.start);
|
||||
}
|
||||
|
||||
if (updateData.end) {
|
||||
updatePayload.end = this.transformDateTimeWithZone(updateData.end);
|
||||
}
|
||||
|
||||
if (updateData.attendees !== undefined) {
|
||||
updatePayload.attendees = this.transformAttendees(
|
||||
updateData.attendees,
|
||||
);
|
||||
}
|
||||
|
||||
if (updateData.status !== undefined) {
|
||||
updatePayload.status = this.transformEventStatus(updateData.status);
|
||||
}
|
||||
|
||||
return updatePayload;
|
||||
}
|
||||
|
||||
private transformDateTimeWithZone(dateTime: UpdateDateTimeWithZone): GoogleCalendarDateTime {
|
||||
return {
|
||||
dateTime: dateTime.time || "",
|
||||
timeZone: dateTime.timeZone || "",
|
||||
};
|
||||
}
|
||||
|
||||
private transformAttendees(
|
||||
inputAttendees: NonNullable<UpdateUnifiedCalendarEventInput["attendees"]>,
|
||||
): GoogleCalendarAttendee[] {
|
||||
return inputAttendees.map((attendee) => {
|
||||
return {
|
||||
email: attendee.email,
|
||||
displayName: attendee.name,
|
||||
responseStatus: this.transformResponseStatus(attendee.responseStatus),
|
||||
organizer: attendee.host,
|
||||
self: attendee.self,
|
||||
optional: attendee.optional,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private transformResponseStatus(responseStatus?: CalendarEventResponseStatus | null): string {
|
||||
if (!responseStatus) return "needsAction";
|
||||
|
||||
switch (responseStatus) {
|
||||
case CalendarEventResponseStatus.ACCEPTED:
|
||||
return "accepted";
|
||||
case CalendarEventResponseStatus.PENDING:
|
||||
return "tentative";
|
||||
case CalendarEventResponseStatus.DECLINED:
|
||||
return "declined";
|
||||
case CalendarEventResponseStatus.NEEDS_ACTION:
|
||||
return "needsAction";
|
||||
default:
|
||||
return "needsAction";
|
||||
}
|
||||
}
|
||||
|
||||
private transformEventStatus(status?: CalendarEventStatus | null): string {
|
||||
if (!status) return "confirmed";
|
||||
|
||||
switch (status) {
|
||||
case CalendarEventStatus.ACCEPTED:
|
||||
return "confirmed";
|
||||
case CalendarEventStatus.PENDING:
|
||||
return "tentative";
|
||||
case CalendarEventStatus.CANCELLED:
|
||||
return "cancelled";
|
||||
case CalendarEventStatus.DECLINED:
|
||||
return "cancelled";
|
||||
default:
|
||||
return "confirmed";
|
||||
}
|
||||
}
|
||||
}
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
import {
|
||||
UpdateUnifiedCalendarEventInput,
|
||||
UpdateDateTimeWithZone,
|
||||
} from "../inputs/update-unified-calendar-event.input";
|
||||
import { CalendarEventResponseStatus, CalendarEventStatus } from "../outputs/get-unified-calendar-event.output";
|
||||
import { GoogleCalendarEventResponse } from "./get-calendar-event-details-output-pipe";
|
||||
import { GoogleCalendarEventInputPipe } from "./google-calendar-event-input-pipe";
|
||||
|
||||
describe("GoogleCalendarEventInputPipe", () => {
|
||||
let pipe: GoogleCalendarEventInputPipe;
|
||||
let sharedGoogleEvent: GoogleCalendarEventResponse;
|
||||
|
||||
beforeAll(() => {
|
||||
sharedGoogleEvent = {
|
||||
kind: "calendar#event",
|
||||
etag: "test-etag",
|
||||
id: "test-event-id",
|
||||
status: "confirmed",
|
||||
htmlLink: "https://calendar.google.com/event",
|
||||
created: "2024-01-01T00:00:00Z",
|
||||
updated: "2024-01-01T00:00:00Z",
|
||||
summary: "Test Meeting",
|
||||
description: "Test description",
|
||||
start: {
|
||||
dateTime: "2024-01-15T10:00:00Z",
|
||||
timeZone: "America/New_York",
|
||||
},
|
||||
end: {
|
||||
dateTime: "2024-01-15T11:00:00Z",
|
||||
timeZone: "America/New_York",
|
||||
},
|
||||
iCalUID: "test-ical-uid",
|
||||
sequence: 0,
|
||||
attendees: [
|
||||
{
|
||||
email: "attendee1@example.com",
|
||||
displayName: "Attendee One",
|
||||
responseStatus: "accepted",
|
||||
organizer: false,
|
||||
},
|
||||
{
|
||||
email: "attendee2@example.com",
|
||||
displayName: "Attendee Two",
|
||||
responseStatus: "needsAction",
|
||||
organizer: false,
|
||||
},
|
||||
{
|
||||
email: "organizer@example.com",
|
||||
displayName: "Organizer",
|
||||
responseStatus: "accepted",
|
||||
organizer: true,
|
||||
},
|
||||
],
|
||||
creator: {
|
||||
email: "organizer@example.com",
|
||||
displayName: "Organizer",
|
||||
},
|
||||
organizer: {
|
||||
email: "organizer@example.com",
|
||||
displayName: "Organizer",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
pipe = new GoogleCalendarEventInputPipe();
|
||||
});
|
||||
|
||||
describe("transform", () => {
|
||||
it("should transform basic event fields", () => {
|
||||
const input: UpdateUnifiedCalendarEventInput = {
|
||||
title: "Updated Meeting",
|
||||
description: "Updated description",
|
||||
start: {
|
||||
time: "2024-01-15T10:00:00Z",
|
||||
timeZone: "America/New_York",
|
||||
},
|
||||
end: {
|
||||
time: "2024-01-15T11:00:00Z",
|
||||
timeZone: "America/New_York",
|
||||
},
|
||||
status: CalendarEventStatus.ACCEPTED,
|
||||
};
|
||||
|
||||
const expectedOutput = {
|
||||
summary: "Updated Meeting",
|
||||
description: "Updated description",
|
||||
start: {
|
||||
dateTime: "2024-01-15T10:00:00Z",
|
||||
timeZone: "America/New_York",
|
||||
},
|
||||
end: {
|
||||
dateTime: "2024-01-15T11:00:00Z",
|
||||
timeZone: "America/New_York",
|
||||
},
|
||||
status: "confirmed",
|
||||
};
|
||||
|
||||
const result = pipe.transform(input);
|
||||
expect(result).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it("should handle partial updates", () => {
|
||||
const input: UpdateUnifiedCalendarEventInput = {
|
||||
title: "Only Title Update",
|
||||
};
|
||||
|
||||
const expectedOutput = {
|
||||
summary: "Only Title Update",
|
||||
};
|
||||
|
||||
const result = pipe.transform(input);
|
||||
expect(result).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it("should handle null description", () => {
|
||||
const input: UpdateUnifiedCalendarEventInput = {
|
||||
description: null,
|
||||
};
|
||||
|
||||
const expectedOutput = {
|
||||
description: null,
|
||||
};
|
||||
|
||||
const result = pipe.transform(input);
|
||||
expect(result).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe("transformDateTimeWithZone", () => {
|
||||
it("should transform date time with timezone", () => {
|
||||
const input: UpdateDateTimeWithZone = {
|
||||
time: "2024-01-15T10:00:00Z",
|
||||
timeZone: "America/New_York",
|
||||
};
|
||||
|
||||
const expectedOutput = {
|
||||
dateTime: "2024-01-15T10:00:00Z",
|
||||
timeZone: "America/New_York",
|
||||
};
|
||||
|
||||
const result = pipe["transformDateTimeWithZone"](input);
|
||||
expect(result).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it("should handle empty time and timezone", () => {
|
||||
const input: UpdateDateTimeWithZone = {};
|
||||
|
||||
const expectedOutput = {
|
||||
dateTime: "",
|
||||
timeZone: "",
|
||||
};
|
||||
|
||||
const result = pipe["transformDateTimeWithZone"](input);
|
||||
expect(result).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe("transformResponseStatus", () => {
|
||||
it("should transform ACCEPTED status", () => {
|
||||
const result = pipe["transformResponseStatus"](CalendarEventResponseStatus.ACCEPTED);
|
||||
expect(result).toBe("accepted");
|
||||
});
|
||||
|
||||
it("should transform PENDING status", () => {
|
||||
const result = pipe["transformResponseStatus"](CalendarEventResponseStatus.PENDING);
|
||||
expect(result).toBe("tentative");
|
||||
});
|
||||
|
||||
it("should transform DECLINED status", () => {
|
||||
const result = pipe["transformResponseStatus"](CalendarEventResponseStatus.DECLINED);
|
||||
expect(result).toBe("declined");
|
||||
});
|
||||
|
||||
it("should transform NEEDS_ACTION status", () => {
|
||||
const result = pipe["transformResponseStatus"](CalendarEventResponseStatus.NEEDS_ACTION);
|
||||
expect(result).toBe("needsAction");
|
||||
});
|
||||
|
||||
it("should handle null status", () => {
|
||||
const result = pipe["transformResponseStatus"](null);
|
||||
expect(result).toBe("needsAction");
|
||||
});
|
||||
|
||||
it("should handle undefined status", () => {
|
||||
const result = pipe["transformResponseStatus"](undefined);
|
||||
expect(result).toBe("needsAction");
|
||||
});
|
||||
});
|
||||
|
||||
describe("transformEventStatus", () => {
|
||||
it("should transform ACCEPTED status", () => {
|
||||
const result = pipe["transformEventStatus"](CalendarEventStatus.ACCEPTED);
|
||||
expect(result).toBe("confirmed");
|
||||
});
|
||||
|
||||
it("should transform PENDING status", () => {
|
||||
const result = pipe["transformEventStatus"](CalendarEventStatus.PENDING);
|
||||
expect(result).toBe("tentative");
|
||||
});
|
||||
|
||||
it("should transform CANCELLED status", () => {
|
||||
const result = pipe["transformEventStatus"](CalendarEventStatus.CANCELLED);
|
||||
expect(result).toBe("cancelled");
|
||||
});
|
||||
|
||||
it("should transform DECLINED status", () => {
|
||||
const result = pipe["transformEventStatus"](CalendarEventStatus.DECLINED);
|
||||
expect(result).toBe("cancelled");
|
||||
});
|
||||
|
||||
it("should handle null status", () => {
|
||||
const result = pipe["transformEventStatus"](null);
|
||||
expect(result).toBe("confirmed");
|
||||
});
|
||||
|
||||
it("should handle undefined status", () => {
|
||||
const result = pipe["transformEventStatus"](undefined);
|
||||
expect(result).toBe("confirmed");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,8 @@ import { JWT } from "googleapis-common";
|
||||
|
||||
import { DelegationCredentialRepository, OAuth2UniversalSchema } from "@calcom/platform-libraries/app-store";
|
||||
|
||||
import { UnifiedCalendarEventOutput } from "../outputs/get-unified-calendar-event";
|
||||
import { UpdateUnifiedCalendarEventInput } from "../inputs/update-unified-calendar-event.input";
|
||||
import { GoogleCalendarEventInputPipe } from "../pipes/google-calendar-event-input-pipe";
|
||||
|
||||
@Injectable()
|
||||
export class GoogleCalendarService {
|
||||
@@ -53,6 +54,44 @@ export class GoogleCalendarService {
|
||||
}
|
||||
}
|
||||
|
||||
async updateEventDetails(
|
||||
eventUid: string,
|
||||
updateData: UpdateUnifiedCalendarEventInput
|
||||
): Promise<GoogleCalendarEventResponse> {
|
||||
const bookingReference =
|
||||
await this.bookingReferencesRepository.getBookingReferencesIncludeSensitiveCredentials(eventUid);
|
||||
|
||||
if (!bookingReference) {
|
||||
throw new NotFoundException("Booking reference not found");
|
||||
}
|
||||
|
||||
const ownerUserEmail = bookingReference?.booking?.user?.email;
|
||||
|
||||
const calendar = await this.getAuthorizedCalendarInstance(
|
||||
ownerUserEmail,
|
||||
bookingReference.credential?.key,
|
||||
bookingReference.delegationCredential
|
||||
);
|
||||
|
||||
|
||||
const updatePayload = new GoogleCalendarEventInputPipe().transform(updateData);
|
||||
|
||||
try {
|
||||
const event = await calendar.events.patch({
|
||||
calendarId: bookingReference?.externalCalendarId ?? "primary",
|
||||
eventId: bookingReference?.uid,
|
||||
requestBody: updatePayload,
|
||||
});
|
||||
|
||||
if (!event.data) {
|
||||
throw new NotFoundException("Failed to update meeting");
|
||||
}
|
||||
return event.data as GoogleCalendarEventResponse;
|
||||
} catch (error) {
|
||||
throw new NotFoundException("Failed to update meeting details");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an authorized Google Calendar instance
|
||||
* Tries delegation credentials first, falls back to direct OAuth
|
||||
|
||||
@@ -11,6 +11,7 @@ import { OrganizationsTeamsBookingsModule } from "@/modules/organizations/teams/
|
||||
import { OrganizationsUsersBookingsModule } from "@/modules/organizations/users/bookings/organizations-users-bookings.module";
|
||||
import { RouterModule } from "@/modules/router/router.module";
|
||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||
import { TeamsSchedulesModule } from "@/modules/teams/schedules/teams-schedules.module";
|
||||
import { TimezoneModule } from "@/modules/timezones/timezones.module";
|
||||
import { VerifiedResourcesModule } from "@/modules/verified-resources/verified-resources.module";
|
||||
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
|
||||
@@ -38,6 +39,7 @@ import { WebhooksModule } from "./webhooks/webhooks.module";
|
||||
OrganizationsRoutingFormsModule,
|
||||
VerifiedResourcesModule,
|
||||
RouterModule,
|
||||
TeamsSchedulesModule,
|
||||
],
|
||||
})
|
||||
export class EndpointsModule implements NestModule {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service";
|
||||
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import {
|
||||
BadRequestException,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
@Injectable()
|
||||
export class EventTypeOwnershipGuard implements CanActivate {
|
||||
constructor(private readonly eventTypesService: EventTypesService_2024_06_14) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const user = request.user as ApiAuthGuardUser | undefined;
|
||||
const eventTypeIdParam = request.params?.eventTypeId;
|
||||
|
||||
if (!user) {
|
||||
throw new ForbiddenException("EventTypeOwnershipGuard - No user associated with the request.");
|
||||
}
|
||||
|
||||
if (!eventTypeIdParam) {
|
||||
throw new BadRequestException("Missing eventTypeId param.");
|
||||
}
|
||||
|
||||
const eventTypeId = Number(eventTypeIdParam);
|
||||
if (!Number.isInteger(eventTypeId) || eventTypeId <= 0) {
|
||||
throw new BadRequestException("Invalid eventTypeId param.");
|
||||
}
|
||||
const eventType = await this.eventTypesService.getUserEventType(user.id, eventTypeId);
|
||||
if (!eventType) {
|
||||
// Mirrors EventTypesService behavior: NotFound when not owned or not present
|
||||
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+551
@@ -0,0 +1,551 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { OrganizationsTeamsBookingsModule } from "@/modules/organizations/teams/bookings/organizations-teams-bookings.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { User } from "@prisma/client";
|
||||
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 { HostsRepositoryFixture } from "test/fixtures/repository/hosts.repository.fixture";
|
||||
import { ManagedOrganizationsRepositoryFixture } from "test/fixtures/repository/managed-organizations.repository.fixture";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { randomString } from "@calcom/lib/random";
|
||||
import {
|
||||
CAL_API_VERSION_HEADER,
|
||||
SUCCESS_STATUS,
|
||||
VERSION_2024_08_13,
|
||||
X_CAL_CLIENT_ID,
|
||||
X_CAL_SECRET_KEY,
|
||||
} from "@calcom/platform-constants";
|
||||
import {
|
||||
BookingOutput_2024_08_13,
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
GetBookingsOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
||||
|
||||
describe("Organizations Bookings Endpoints 2024-08-13", () => {
|
||||
describe("Manager and managed organizations bookings", () => {
|
||||
let app: INestApplication;
|
||||
let managerOrganization: Team;
|
||||
let managedOrganization: Team;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
let schedulesService: SchedulesService_2024_04_15;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
let hostsRepositoryFixture: HostsRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||
let managedOrganizationsRepositoryFixture: ManagedOrganizationsRepositoryFixture;
|
||||
|
||||
const managerOrgUserEmail = "manager-org-user-1-bookings@api.com";
|
||||
const managerOrgUserEmail2 = "manager-org-user-2-bookings@api.com";
|
||||
const managedOrgUserEmail = "managed-org-user-1-bookings@api.com";
|
||||
const nonOrgUserEmail1 = "non-org-user-1-bookings@api.com";
|
||||
let managerOrgUser1: User;
|
||||
let managerOrgUser2: User;
|
||||
let managedOrgUser: User;
|
||||
let nonOrgUser1: User;
|
||||
let managerOrgTeam1: Team;
|
||||
let managedOrgTeam1: Team;
|
||||
|
||||
let managerOrgEventTypeId: number;
|
||||
let managedOrgEventTypeId: number;
|
||||
let nonOrgEventTypeId: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
managerOrgUserEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, OrganizationsTeamsBookingsModule],
|
||||
})
|
||||
)
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.overrideGuard(PlatformPlanGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
managedOrganizationsRepositoryFixture = new ManagedOrganizationsRepositoryFixture(moduleRef);
|
||||
|
||||
await setupManagerOrganization();
|
||||
await setupManagedOrganization();
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
async function setupManagerOrganization() {
|
||||
managerOrganization = await organizationsRepositoryFixture.create({
|
||||
name: "organization bookings",
|
||||
isPlatform: true,
|
||||
});
|
||||
managerOrgTeam1 = await teamRepositoryFixture.create({
|
||||
name: "team orgs booking 1",
|
||||
isOrganization: false,
|
||||
parent: { connect: { id: managerOrganization.id } },
|
||||
});
|
||||
oAuthClient = await createOAuthClient(managerOrganization.id);
|
||||
|
||||
nonOrgUser1 = await userRepositoryFixture.create({
|
||||
email: nonOrgUserEmail1,
|
||||
locale: "it",
|
||||
name: "NonOrgUser1Bookings",
|
||||
platformOAuthClients: {
|
||||
connect: {
|
||||
id: oAuthClient.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
managerOrgUser1 = await userRepositoryFixture.create({
|
||||
email: managerOrgUserEmail,
|
||||
locale: "it",
|
||||
name: "orgUser1Bookings",
|
||||
platformOAuthClients: {
|
||||
connect: {
|
||||
id: oAuthClient.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
managerOrgUser2 = await userRepositoryFixture.create({
|
||||
email: managerOrgUserEmail2,
|
||||
locale: "it",
|
||||
name: "orgUser2Bookings",
|
||||
platformOAuthClients: {
|
||||
connect: {
|
||||
id: oAuthClient.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: "working time",
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(managerOrgUser1.id, userSchedule);
|
||||
await schedulesService.createUserSchedule(nonOrgUser1.id, userSchedule);
|
||||
await schedulesService.createUserSchedule(managerOrgUser2.id, userSchedule);
|
||||
|
||||
const orgEventType = await eventTypesRepositoryFixture.createTeamEventType({
|
||||
schedulingType: "COLLECTIVE",
|
||||
team: {
|
||||
connect: { id: managerOrgTeam1.id },
|
||||
},
|
||||
title: "Collective Event Type",
|
||||
slug: "manager-org-bookings-collective-event-type",
|
||||
length: 60,
|
||||
assignAllTeamMembers: true,
|
||||
bookingFields: [],
|
||||
locations: [],
|
||||
});
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${managerOrgUser1.id}`,
|
||||
username: managerOrgUserEmail,
|
||||
organization: {
|
||||
connect: {
|
||||
id: managerOrganization.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: managerOrgUser1.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${managerOrgUser2.id}`,
|
||||
username: managerOrgUserEmail2,
|
||||
organization: {
|
||||
connect: {
|
||||
id: managerOrganization.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: managerOrgUser2.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "OWNER",
|
||||
user: { connect: { id: managerOrgUser1.id } },
|
||||
team: { connect: { id: managerOrganization.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: managerOrgUser2.id } },
|
||||
team: { connect: { id: managerOrganization.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
const nonOrgEventType = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: "Non Org Event Type",
|
||||
slug: "non-org-event-type",
|
||||
length: 60,
|
||||
bookingFields: [],
|
||||
locations: [],
|
||||
},
|
||||
nonOrgUser1.id
|
||||
);
|
||||
|
||||
managerOrgEventTypeId = orgEventType.id;
|
||||
nonOrgEventTypeId = nonOrgEventType.id;
|
||||
|
||||
await hostsRepositoryFixture.create({
|
||||
isFixed: true,
|
||||
user: {
|
||||
connect: {
|
||||
id: managerOrgUser2.id,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id: managerOrgEventTypeId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await bookingsRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
id: managerOrgUser2.id,
|
||||
},
|
||||
},
|
||||
startTime: new Date(Date.UTC(2030, 0, 9, 13, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2030, 0, 9, 14, 0, 0)),
|
||||
title: "Manager Org Collective Booking",
|
||||
uid: `manager-org-collective-${randomString()}`,
|
||||
eventType: {
|
||||
connect: {
|
||||
id: managerOrgEventTypeId,
|
||||
},
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
responses: {
|
||||
name: "alice",
|
||||
email: "alice@gmail.com",
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: "alice@gmail.com",
|
||||
name: "alice",
|
||||
locale: "es",
|
||||
timeZone: "Europe/Madrid",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await bookingsRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
id: nonOrgUser1.id,
|
||||
},
|
||||
},
|
||||
startTime: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2030, 0, 8, 14, 0, 0)),
|
||||
title: "Non-Org Booking 1",
|
||||
uid: `non-org-booking-1-${randomString()}`,
|
||||
eventType: {
|
||||
connect: {
|
||||
id: nonOrgEventTypeId,
|
||||
},
|
||||
},
|
||||
location: "https://meet.google.com/jkl-mno-pqr",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
responses: {
|
||||
name: managerOrgUser2.name ?? "",
|
||||
email: managerOrgUserEmail2,
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: managerOrgUserEmail2,
|
||||
name: managerOrgUser2.name ?? "",
|
||||
locale: "en",
|
||||
timeZone: managerOrgUser2.timeZone ?? "Europe/Madrid",
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function setupManagedOrganization() {
|
||||
managedOrganization = await organizationsRepositoryFixture.create({
|
||||
name: "organization bookings",
|
||||
isPlatform: true,
|
||||
});
|
||||
await managedOrganizationsRepositoryFixture.createManagedOrganization(
|
||||
managerOrganization.id,
|
||||
managedOrganization.id
|
||||
);
|
||||
|
||||
managedOrgTeam1 = await teamRepositoryFixture.create({
|
||||
name: "managed org team 1",
|
||||
isOrganization: false,
|
||||
parent: { connect: { id: managedOrganization.id } },
|
||||
});
|
||||
|
||||
managedOrgUser = await userRepositoryFixture.create({
|
||||
email: managedOrgUserEmail,
|
||||
locale: "it",
|
||||
name: "ManagedOrgUser1Bookings",
|
||||
platformOAuthClients: {
|
||||
connect: {
|
||||
id: oAuthClient.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${managedOrgUser.id}`,
|
||||
username: managedOrgUserEmail,
|
||||
organization: {
|
||||
connect: {
|
||||
id: managedOrganization.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: managedOrgUser.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${managerOrgUser1.id}`,
|
||||
username: managerOrgUserEmail,
|
||||
organization: {
|
||||
connect: {
|
||||
id: managedOrganization.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: managerOrgUser1.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: managedOrgUser.id } },
|
||||
team: { connect: { id: managedOrganization.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "OWNER",
|
||||
user: { connect: { id: managerOrgUser1.id } },
|
||||
team: { connect: { id: managedOrganization.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: "working time",
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(managedOrgUser.id, userSchedule);
|
||||
|
||||
const managedOrgEventType = await eventTypesRepositoryFixture.createTeamEventType({
|
||||
schedulingType: "COLLECTIVE",
|
||||
team: {
|
||||
connect: { id: managedOrgTeam1.id },
|
||||
},
|
||||
title: "Collective Event Type",
|
||||
slug: "managed-org-bookings-collective-event-type",
|
||||
length: 60,
|
||||
assignAllTeamMembers: true,
|
||||
bookingFields: [],
|
||||
locations: [],
|
||||
});
|
||||
|
||||
managedOrgEventTypeId = managedOrgEventType.id;
|
||||
|
||||
await hostsRepositoryFixture.create({
|
||||
isFixed: true,
|
||||
user: {
|
||||
connect: {
|
||||
id: managedOrgUser.id,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id: managedOrgEventType.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await bookingsRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
id: managedOrgUser.id,
|
||||
},
|
||||
},
|
||||
startTime: new Date(Date.UTC(2030, 0, 11, 13, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2030, 0, 11, 14, 0, 0)),
|
||||
title: "Managed Org Booking",
|
||||
uid: `managed-org-booking-${randomString()}`,
|
||||
eventType: {
|
||||
connect: {
|
||||
id: managedOrgEventTypeId,
|
||||
},
|
||||
},
|
||||
location: "https://meet.google.com/ghi-jkl-mno",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
responses: {
|
||||
name: "charlie",
|
||||
email: "charlie@gmail.com",
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: "charlie@gmail.com",
|
||||
name: "charlie",
|
||||
locale: "en",
|
||||
timeZone: "Europe/Madrid",
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("get manager organization bookings", () => {
|
||||
it("should get bookings by organizationId for manager organization", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${managerOrganization.id}/bookings`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
expect(data.map((booking) => booking.eventTypeId).sort()).toEqual(
|
||||
[managerOrgEventTypeId, nonOrgEventTypeId].sort()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should get bookings by organizationId and userId", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${managerOrganization.id}/bookings?userIds=${managerOrgUser2.id}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
expect(data.map((booking) => booking.eventTypeId).sort()).toEqual(
|
||||
[managerOrgEventTypeId, nonOrgEventTypeId].sort()
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("get managed organization bookings", () => {
|
||||
it("should get bookings by organizationId for managed organization", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${managedOrganization.id}/bookings`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(1);
|
||||
expect(data[0].eventTypeId).toEqual(managedOrgEventTypeId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["http://localhost:5555"],
|
||||
permissions: 32,
|
||||
areCalendarEventsEnabled: false,
|
||||
areEmailsEnabled: false,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(managerOrganization.id);
|
||||
await userRepositoryFixture.deleteByEmail(managerOrgUser1.email);
|
||||
await userRepositoryFixture.deleteByEmail(nonOrgUser1.email);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(managerOrgUser1.id, managerOrgUser1.email);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(nonOrgUser1.id, nonOrgUser1.email);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
+128
-1
@@ -66,6 +66,8 @@ describe("Organizations Bookings Endpoints 2024-08-13", () => {
|
||||
let orgEventTypeId2: number;
|
||||
let nonOrgEventTypeId: number;
|
||||
|
||||
let collectiveOrgBookingUid: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
orgUserEmail2,
|
||||
@@ -91,12 +93,17 @@ describe("Organizations Bookings Endpoints 2024-08-13", () => {
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
|
||||
organization = await organizationsRepositoryFixture.create({ name: "organization bookings" });
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
team1 = await teamRepositoryFixture.create({
|
||||
name: "team orgs booking 1",
|
||||
isOrganization: false,
|
||||
parent: { connect: { id: organization.id } },
|
||||
createdByOAuthClient: {
|
||||
connect: {
|
||||
id: oAuthClient.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
nonOrgUser1 = await userRepositoryFixture.create({
|
||||
email: nonOrgUserEmail1,
|
||||
@@ -315,6 +322,7 @@ describe("Organizations Bookings Endpoints 2024-08-13", () => {
|
||||
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
collectiveOrgBookingUid = data.uid;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts.length).toEqual(1);
|
||||
@@ -669,6 +677,125 @@ describe("Organizations Bookings Endpoints 2024-08-13", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("get by bookingUid param", () => {
|
||||
it("should get a specific booking by bookingUid query param", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${organization.id}/bookings?bookingUid=${collectiveOrgBookingUid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(1);
|
||||
expect(responseDataIsBooking(data[0])).toBe(true);
|
||||
|
||||
if (responseDataIsBooking(data[0])) {
|
||||
const booking: BookingOutput_2024_08_13 = data[0];
|
||||
expect(booking.uid).toEqual(collectiveOrgBookingUid);
|
||||
expect(booking.eventTypeId).toEqual(orgEventTypeId);
|
||||
expect(booking.status).toEqual("accepted");
|
||||
expect(booking.attendees.length).toEqual(2);
|
||||
expect(booking.attendees[0].name).toEqual("alice");
|
||||
expect(booking.attendees[0].email).toEqual("alice@gmail.com");
|
||||
} else {
|
||||
throw new Error("Expected single booking but received different response type");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should return empty array for non-existent booking UID query param", async () => {
|
||||
const nonExistentUid = "non-existent-booking-uid";
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${organization.id}/bookings?bookingUid=${nonExistentUid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("should return empty array for booking UID not belonging to the organization", async () => {
|
||||
const regularUser = await userRepositoryFixture.create({
|
||||
email: `org-bookings-regular-user-${Math.floor(Math.random() * 10000)}@api.com`,
|
||||
name: "Regular User",
|
||||
});
|
||||
|
||||
const regularUserEventType = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: `regular-user-event-type-${Math.floor(Math.random() * 10000)}`,
|
||||
slug: `regular-user-event-type-${Math.floor(Math.random() * 10000)}`,
|
||||
length: 60,
|
||||
bookingFields: [],
|
||||
locations: [],
|
||||
},
|
||||
regularUser.id
|
||||
);
|
||||
|
||||
const regularUserBooking = await bookingsRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
id: regularUser.id,
|
||||
},
|
||||
},
|
||||
startTime: new Date(Date.UTC(2030, 0, 15, 13, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2030, 0, 15, 14, 0, 0)),
|
||||
title: "Regular user booking",
|
||||
uid: `regular-user-booking-${Math.floor(Math.random() * 10000)}`,
|
||||
eventType: {
|
||||
connect: {
|
||||
id: regularUserEventType.id,
|
||||
},
|
||||
},
|
||||
location: "https://meet.google.com/regular-user",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
status: "ACCEPTED",
|
||||
responses: {
|
||||
name: "Regular Attendee",
|
||||
email: "regular@example.com",
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: "regular@example.com",
|
||||
name: "Regular Attendee",
|
||||
locale: "en",
|
||||
timeZone: "Europe/Rome",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${organization.id}/bookings?bookingUid=${regularUserBooking.uid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.length).toEqual(0);
|
||||
|
||||
await userRepositoryFixture.delete(regularUser.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import * as request from "supertest";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { CreatePrivateLinkInput } from "@calcom/platform-types";
|
||||
|
||||
describe("Organizations / Teams / Event Types / Private Links Endpoints", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let orgFixture: OrganizationRepositoryFixture;
|
||||
let teamFixture: TeamRepositoryFixture;
|
||||
let userFixture: UserRepositoryFixture;
|
||||
let eventTypesFixture: EventTypesRepositoryFixture;
|
||||
|
||||
let org: any;
|
||||
let team: any;
|
||||
let user: any;
|
||||
let eventType: any;
|
||||
|
||||
const userEmail = `org-private-links-user-${randomString()}@api.com`;
|
||||
|
||||
beforeAll(async () => {
|
||||
const testingModuleBuilder = withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||
imports: [AppModule, UsersModule, TokensModule],
|
||||
})
|
||||
)
|
||||
// Bypass org admin plan and admin API checks and roles in this e2e
|
||||
.overrideGuard(PlatformPlanGuard)
|
||||
.useValue({ canActivate: () => true })
|
||||
.overrideGuard(IsAdminAPIEnabledGuard)
|
||||
.useValue({ canActivate: () => true })
|
||||
.overrideGuard(RolesGuard)
|
||||
.useValue({ canActivate: () => true })
|
||||
// Keep IsOrgGuard and IsTeamInOrg to validate org/team path integrity
|
||||
.overrideGuard(ApiAuthGuard)
|
||||
.useValue({ canActivate: () => true });
|
||||
|
||||
const moduleRef = await testingModuleBuilder.compile();
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
orgFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
teamFixture = new TeamRepositoryFixture(moduleRef);
|
||||
userFixture = new UserRepositoryFixture(moduleRef);
|
||||
eventTypesFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
|
||||
user = await userFixture.create({
|
||||
email: userEmail,
|
||||
username: `org-private-links-user-${randomString()}`,
|
||||
name: "Test User",
|
||||
});
|
||||
|
||||
org = await orgFixture.create({
|
||||
name: `org-private-links-org-${randomString()}`,
|
||||
slug: `org-private-links-org-${randomString()}`,
|
||||
isOrganization: true,
|
||||
});
|
||||
|
||||
team = await teamFixture.create({
|
||||
name: `org-private-links-team-${randomString()}`,
|
||||
isOrganization: false,
|
||||
parent: { connect: { id: org.id } },
|
||||
});
|
||||
|
||||
// Create a team-owned event type
|
||||
eventType = await eventTypesFixture.createTeamEventType({
|
||||
title: `org-private-links-event-type-${randomString()}`,
|
||||
slug: `org-private-links-event-type-${randomString()}`,
|
||||
length: 30,
|
||||
locations: [],
|
||||
team: { connect: { id: team.id } },
|
||||
});
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it("POST /v2/organizations/:orgId/teams/:teamId/event-types/:eventTypeId/private-links - create", async () => {
|
||||
const body: CreatePrivateLinkInput = { maxUsageCount: 5 };
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}/private-links`)
|
||||
.set("Authorization", "Bearer test")
|
||||
.send(body)
|
||||
.expect(201);
|
||||
|
||||
expect(response.body.status).toBe(SUCCESS_STATUS);
|
||||
expect(response.body.data.linkId).toBeDefined();
|
||||
expect(response.body.data.maxUsageCount).toBe(5);
|
||||
expect(response.body.data.usageCount).toBeDefined();
|
||||
});
|
||||
|
||||
it("GET /v2/organizations/:orgId/teams/:teamId/event-types/:eventTypeId/private-links - list", async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}/private-links`)
|
||||
.set("Authorization", "Bearer test")
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.status).toBe(SUCCESS_STATUS);
|
||||
expect(Array.isArray(response.body.data)).toBe(true);
|
||||
});
|
||||
|
||||
it("PATCH /v2/organizations/:orgId/teams/:teamId/event-types/:eventTypeId/private-links/:linkId - update", async () => {
|
||||
// create first
|
||||
const createResp = await request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}/private-links`)
|
||||
.set("Authorization", "Bearer test")
|
||||
.send({ maxUsageCount: 3 })
|
||||
.expect(201);
|
||||
|
||||
const linkId = createResp.body.data.linkId as string;
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.patch(
|
||||
`/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}/private-links/${linkId}`
|
||||
)
|
||||
.set("Authorization", "Bearer test")
|
||||
.send({ maxUsageCount: 10 })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.status).toBe(SUCCESS_STATUS);
|
||||
expect(response.body.data.maxUsageCount).toBe(10);
|
||||
});
|
||||
|
||||
it("DELETE /v2/organizations/:orgId/teams/:teamId/event-types/:eventTypeId/private-links/:linkId - delete", async () => {
|
||||
// create first
|
||||
const createResp = await request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}/private-links`)
|
||||
.set("Authorization", "Bearer test")
|
||||
.send({ maxUsageCount: 2 })
|
||||
.expect(201);
|
||||
|
||||
const linkId = createResp.body.data.linkId as string;
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.delete(
|
||||
`/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}/private-links/${linkId}`
|
||||
)
|
||||
.set("Authorization", "Bearer test")
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.status).toBe(SUCCESS_STATUS);
|
||||
expect(response.body.data.linkId).toBe(linkId);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
try {
|
||||
if (eventType?.id) {
|
||||
await eventTypesFixture.delete(eventType.id);
|
||||
}
|
||||
if (team?.id) {
|
||||
await teamFixture.delete(team.id);
|
||||
}
|
||||
if (org?.id) {
|
||||
await orgFixture.delete(org.id);
|
||||
}
|
||||
if (user?.email) {
|
||||
await userFixture.deleteByEmail(user.email);
|
||||
}
|
||||
} catch {}
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
@@ -74,6 +74,7 @@ type Input = Pick<
|
||||
| "hideOrganizerEmail"
|
||||
| "team"
|
||||
| "calVideoSettings"
|
||||
| "hidden"
|
||||
>;
|
||||
|
||||
@Injectable()
|
||||
|
||||
+17
-1
@@ -2,6 +2,8 @@ import { CreateOrgMembershipDto } from "@/modules/organizations/memberships/inpu
|
||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { TeamService } from "@calcom/platform-libraries";
|
||||
|
||||
import { UpdateOrgMembershipDto } from "../inputs/update-organization-membership.input";
|
||||
import { OrganizationsMembershipOutputService } from "./organizations-membership-output.service";
|
||||
|
||||
@@ -62,10 +64,24 @@ export class OrganizationsMembershipService {
|
||||
}
|
||||
|
||||
async deleteOrgMembership(organizationId: number, membershipId: number) {
|
||||
const membership = await this.organizationsMembershipRepository.deleteOrgMembership(
|
||||
// Get the membership first to get the userId
|
||||
const membership = await this.organizationsMembershipRepository.findOrgMembership(
|
||||
organizationId,
|
||||
membershipId
|
||||
);
|
||||
|
||||
if (!membership) {
|
||||
throw new NotFoundException(
|
||||
`Membership with id ${membershipId} within organization id ${organizationId} not found`
|
||||
);
|
||||
}
|
||||
|
||||
await TeamService.removeMembers({
|
||||
teamIds: [organizationId],
|
||||
userIds: [membership.userId],
|
||||
isOrg: true,
|
||||
});
|
||||
|
||||
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { OrganizationsEventTypesPrivateLinksController } from "@/ee/event-types-private-links/controllers/organizations-event-types-private-links.controller";
|
||||
import { EventTypesPrivateLinksModule } from "@/ee/event-types-private-links/event-types-private-links.module";
|
||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||
import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module";
|
||||
import { InputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/input-schedules.service";
|
||||
import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service";
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.repository";
|
||||
import { ConferencingService } from "@/modules/conferencing/services/conferencing.service";
|
||||
@@ -18,7 +22,6 @@ import { OrganizationAttributesService } from "@/modules/organizations/attribute
|
||||
import { OrganizationAttributeOptionRepository } from "@/modules/organizations/attributes/options/organization-attribute-options.repository";
|
||||
import { OrganizationsAttributesOptionsController } from "@/modules/organizations/attributes/options/organizations-attributes-options.controller";
|
||||
import { OrganizationAttributeOptionService } from "@/modules/organizations/attributes/options/services/organization-attributes-option.service";
|
||||
import { OrganizationsConferencingController } from "@/modules/organizations/conferencing/organizations-conferencing.controller";
|
||||
import { OrganizationsConferencingModule } from "@/modules/organizations/conferencing/organizations-conferencing.module";
|
||||
import { OrganizationsConferencingService } from "@/modules/organizations/conferencing/services/organizations-conferencing.service";
|
||||
import { OrganizationsDelegationCredentialModule } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.module";
|
||||
@@ -36,7 +39,6 @@ import { OrganizationsMembershipOutputService } from "@/modules/organizations/me
|
||||
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
||||
import { OrganizationsOrganizationsModule } from "@/modules/organizations/organizations/organizations-organizations.module";
|
||||
import { OrganizationsSchedulesController } from "@/modules/organizations/schedules/organizations-schedules.controller";
|
||||
import { OrganizationSchedulesRepository } from "@/modules/organizations/schedules/organizations-schedules.repository";
|
||||
import { OrganizationsSchedulesService } from "@/modules/organizations/schedules/services/organizations-schedules.service";
|
||||
import { OrganizationsStripeModule } from "@/modules/organizations/stripe/organizations-stripe.module";
|
||||
import { OrganizationsStripeService } from "@/modules/organizations/stripe/services/organizations-stripe.service";
|
||||
@@ -63,6 +65,7 @@ import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
|
||||
import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service";
|
||||
import { TeamsModule } from "@/modules/teams/teams/teams.module";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
@@ -91,6 +94,7 @@ import { Module } from "@nestjs/common";
|
||||
OrganizationsStripeModule,
|
||||
OrganizationsTeamsRoutingFormsModule,
|
||||
OrganizationsConferencingModule,
|
||||
EventTypesPrivateLinksModule,
|
||||
],
|
||||
providers: [
|
||||
OrganizationsRepository,
|
||||
@@ -99,7 +103,6 @@ import { Module } from "@nestjs/common";
|
||||
OrganizationsTeamsService,
|
||||
MembershipsRepository,
|
||||
OrganizationsSchedulesService,
|
||||
OrganizationSchedulesRepository,
|
||||
OrganizationsUsersRepository,
|
||||
OrganizationsUsersService,
|
||||
EmailService,
|
||||
@@ -141,6 +144,9 @@ import { Module } from "@nestjs/common";
|
||||
TeamWorkflowsService,
|
||||
WorkflowsInputService,
|
||||
WorkflowsOutputService,
|
||||
TeamsSchedulesService,
|
||||
SchedulesService_2024_06_11,
|
||||
InputSchedulesService_2024_06_11,
|
||||
],
|
||||
exports: [
|
||||
OrganizationsService,
|
||||
@@ -177,6 +183,7 @@ import { Module } from "@nestjs/common";
|
||||
OrganizationsTeamsSchedulesController,
|
||||
OrganizationsUsersOOOController,
|
||||
OrganizationTeamWorkflowsController,
|
||||
OrganizationsEventTypesPrivateLinksController,
|
||||
],
|
||||
})
|
||||
export class OrganizationsModule {}
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
||||
import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service";
|
||||
import { OrganizationSchedulesRepository } from "@/modules/organizations/schedules/organizations-schedules.repository";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@@ -8,8 +8,8 @@ import { ScheduleOutput_2024_06_11 } from "@calcom/platform-types";
|
||||
@Injectable()
|
||||
export class OrganizationsSchedulesService {
|
||||
constructor(
|
||||
private readonly organizationSchedulesService: OrganizationSchedulesRepository,
|
||||
private readonly outputSchedulesService: OutputSchedulesService_2024_06_11,
|
||||
private readonly schedulesRepository: SchedulesRepository_2024_06_11,
|
||||
private readonly usersRepository: UsersRepository
|
||||
) {}
|
||||
|
||||
@@ -17,7 +17,7 @@ export class OrganizationsSchedulesService {
|
||||
const users = await this.usersRepository.getOrganizationUsers(organizationId);
|
||||
const usersIds = users.map((user) => user.id);
|
||||
|
||||
const schedules = await this.organizationSchedulesService.getSchedulesByUserIds(usersIds, skip, take);
|
||||
const schedules = await this.schedulesRepository.getSchedulesByUserIds(usersIds, skip, take);
|
||||
|
||||
const responseSchedules: ScheduleOutput_2024_06_11[] = [];
|
||||
|
||||
|
||||
-4
@@ -20,7 +20,6 @@ import {
|
||||
OrgTeamMembershipOutputResponseDto,
|
||||
} from "@/modules/organizations/teams/memberships/outputs/organization-teams-memberships.output";
|
||||
import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/teams/memberships/services/organizations-teams-memberships.service";
|
||||
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
|
||||
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
|
||||
import {
|
||||
Controller,
|
||||
@@ -59,7 +58,6 @@ export class OrganizationsTeamsMembershipsController {
|
||||
|
||||
constructor(
|
||||
private organizationsTeamsMembershipsService: OrganizationsTeamsMembershipsService,
|
||||
private teamsEventTypesService: TeamsEventTypesService,
|
||||
private readonly organizationsRepository: OrganizationsRepository
|
||||
) {}
|
||||
|
||||
@@ -127,8 +125,6 @@ export class OrganizationsTeamsMembershipsController {
|
||||
membershipId
|
||||
);
|
||||
|
||||
await this.teamsEventTypesService.deleteUserTeamEventTypesAndHosts(membership.userId, teamId);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
||||
|
||||
+13
-1
@@ -3,6 +3,8 @@ import { UpdateOrgTeamMembershipDto } from "@/modules/organizations/teams/member
|
||||
import { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { TeamService } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationsTeamsMembershipsService {
|
||||
constructor(
|
||||
@@ -58,11 +60,21 @@ export class OrganizationsTeamsMembershipsService {
|
||||
}
|
||||
|
||||
async deleteOrgTeamMembership(organizationId: number, teamId: number, membershipId: number) {
|
||||
const teamMembership = await this.organizationsTeamsMembershipsRepository.deleteOrgTeamMembershipById(
|
||||
// First get the membership to get the userId
|
||||
const teamMembership = await this.organizationsTeamsMembershipsRepository.findOrgTeamMembership(
|
||||
organizationId,
|
||||
teamId,
|
||||
membershipId
|
||||
);
|
||||
|
||||
if (!teamMembership) {
|
||||
throw new NotFoundException(
|
||||
`Membership with id ${membershipId} not found in team ${teamId} of organization ${organizationId}`
|
||||
);
|
||||
}
|
||||
|
||||
await TeamService.removeMembers({ teamIds: [teamId], userIds: [teamMembership.userId], isOrg: false });
|
||||
|
||||
return teamMembership;
|
||||
}
|
||||
}
|
||||
+28
-3
@@ -14,11 +14,12 @@ import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
|
||||
import { IsUserInOrgTeam } from "@/modules/auth/guards/users/is-user-in-org-team.guard";
|
||||
import { Controller, UseGuards, Get, Param, ParseIntPipe } from "@nestjs/common";
|
||||
import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service";
|
||||
import { Controller, UseGuards, Get, Param, ParseIntPipe, Query } from "@nestjs/common";
|
||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
|
||||
import { GetSchedulesOutput_2024_06_11, SkipTakePagination } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/organizations/:orgId/teams/:teamId",
|
||||
@@ -29,7 +30,31 @@ import { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
|
||||
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
|
||||
@ApiHeader(OPTIONAL_API_KEY_HEADER)
|
||||
export class OrganizationsTeamsSchedulesController {
|
||||
constructor(private schedulesService: SchedulesService_2024_06_11) {}
|
||||
constructor(
|
||||
private schedulesService: SchedulesService_2024_06_11,
|
||||
|
||||
private teamsSchedulesService: TeamsSchedulesService
|
||||
) {}
|
||||
|
||||
@UseGuards(IsTeamInOrg)
|
||||
@Roles("TEAM_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@Get("/schedules")
|
||||
@DocsTags("Orgs / Teams / Schedules")
|
||||
@ApiOperation({ summary: "Get all team member schedules" })
|
||||
async getTeamSchedules(
|
||||
@Param("teamId", ParseIntPipe) teamId: number,
|
||||
@Query() queryParams: SkipTakePagination
|
||||
): Promise<GetSchedulesOutput_2024_06_11> {
|
||||
const { skip, take } = queryParams;
|
||||
|
||||
const schedules = await this.teamsSchedulesService.getTeamSchedules(teamId, skip, take);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: schedules,
|
||||
};
|
||||
}
|
||||
|
||||
@Roles("TEAM_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
|
||||
+39
@@ -18,6 +18,7 @@ import { randomString } from "test/utils/randomString";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
|
||||
import { ApiSuccessResponse, ScheduleOutput_2024_06_11 } from "@calcom/platform-types";
|
||||
import { Team, Schedule } from "@calcom/prisma/client";
|
||||
|
||||
@@ -36,6 +37,8 @@ describe("Organizations Teams Schedules Endpoints", () => {
|
||||
let org: Team;
|
||||
let orgTeam: Team;
|
||||
let nonOrgTeam: Team;
|
||||
|
||||
let userSchedule: Schedule;
|
||||
let user2Schedule: Schedule;
|
||||
|
||||
const userEmail = `organizations-teams-schedules-admin-${randomString()}@api.com`;
|
||||
@@ -73,6 +76,16 @@ describe("Organizations Teams Schedules Endpoints", () => {
|
||||
username: userEmail2,
|
||||
});
|
||||
|
||||
userSchedule = await scheduleRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
name: `organizations-teams-schedules-user-schedule-${randomString()}`,
|
||||
timeZone: "America/New_York",
|
||||
});
|
||||
|
||||
user2Schedule = await scheduleRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
@@ -214,6 +227,32 @@ describe("Organizations Teams Schedules Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should get all the schedules of members in a team", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/schedules`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: GetSchedulesOutput_2024_06_11 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(Array.isArray(responseBody.data)).toBe(true);
|
||||
expect(responseBody.data.length).toEqual(2);
|
||||
|
||||
const userOneSchedule = responseBody.data.find((schedule) => schedule.id === userSchedule.id);
|
||||
const userTwoSchedule = responseBody.data.find((schedule) => schedule.id === user2Schedule.id);
|
||||
|
||||
expect(userOneSchedule).toBeDefined();
|
||||
expect(userTwoSchedule).toBeDefined();
|
||||
|
||||
expect(userOneSchedule?.id).toEqual(userSchedule.id);
|
||||
expect(userOneSchedule?.name).toEqual(userSchedule.name);
|
||||
expect(userOneSchedule?.timeZone).toEqual(userSchedule.timeZone);
|
||||
|
||||
expect(userTwoSchedule?.id).toEqual(user2Schedule.id);
|
||||
expect(userTwoSchedule?.name).toEqual(user2Schedule.name);
|
||||
expect(userOneSchedule?.timeZone).toEqual(user2Schedule.timeZone);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await userRepositoryFixture.deleteByEmail(userToInviteViaApi.email);
|
||||
|
||||
+21
-1
@@ -12,7 +12,12 @@ import {
|
||||
EMAIL,
|
||||
WorkflowEmailAttendeeStepDto,
|
||||
} from "@/modules/workflows/inputs/workflow-step.input";
|
||||
import { BEFORE_EVENT, DAY } from "@/modules/workflows/inputs/workflow-trigger.input";
|
||||
import {
|
||||
BEFORE_EVENT,
|
||||
DAY,
|
||||
OnAfterEventTriggerDto,
|
||||
OnBeforeEventTriggerDto,
|
||||
} from "@/modules/workflows/inputs/workflow-trigger.input";
|
||||
// Adjust path if needed
|
||||
import { GetWorkflowOutput, GetWorkflowsOutput } from "@/modules/workflows/outputs/workflow.output";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
@@ -255,6 +260,10 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
|
||||
"+37255555555"
|
||||
);
|
||||
expect(responseBody.data.steps.find((step) => step.stepNumber === 3)?.email).toEqual(authEmail);
|
||||
const trigger = sampleCreateWorkflowDto.trigger as OnBeforeEventTriggerDto;
|
||||
expect(responseBody.data.trigger?.offset?.value).toEqual(trigger.offset.value);
|
||||
expect(responseBody.data.trigger?.offset?.unit).toEqual(trigger.offset.unit);
|
||||
|
||||
createdWorkflowId = responseBody.data.id;
|
||||
createdWorkflow = responseBody.data;
|
||||
});
|
||||
@@ -330,6 +339,13 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
|
||||
expect(step1).toBeDefined();
|
||||
const partialUpdateDto: Partial<CreateWorkflowDto> = {
|
||||
name: updatedName,
|
||||
trigger: {
|
||||
type: "afterEvent",
|
||||
offset: {
|
||||
unit: "minute",
|
||||
value: 10,
|
||||
},
|
||||
},
|
||||
steps: step1 ? [{ ...step1, sender: "updatedSender" } as WorkflowEmailAttendeeStepDto] : [],
|
||||
};
|
||||
expect(createdWorkflowId).toBeDefined();
|
||||
@@ -348,6 +364,10 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
|
||||
step1 && expect(responseBody.data.steps[0].id).toEqual(step1.id);
|
||||
expect(responseBody.data.steps[0].sender).toEqual("updatedSender");
|
||||
expect(responseBody.data.steps[1]?.id).toBeUndefined();
|
||||
const trigger = partialUpdateDto.trigger as OnAfterEventTriggerDto;
|
||||
expect(responseBody.data.trigger?.type).toEqual(trigger.type);
|
||||
expect(responseBody.data.trigger?.offset?.value).toEqual(trigger.offset.value);
|
||||
expect(responseBody.data.trigger?.offset?.unit).toEqual(trigger.offset.unit);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -7,12 +7,32 @@ import { Redis } from "ioredis";
|
||||
export class RedisService implements OnModuleDestroy {
|
||||
public redis: Redis;
|
||||
private readonly logger = new Logger("RedisService");
|
||||
private isReady = false; // Track connection status
|
||||
|
||||
constructor(readonly configService: ConfigService<AppConfig>) {
|
||||
const dbUrl = configService.get<string>("db.redisUrl", { infer: true });
|
||||
if (!dbUrl) throw new Error("Misconfigured Redis, halting.");
|
||||
|
||||
this.redis = new Redis(dbUrl);
|
||||
|
||||
this.redis.on("error", (err) => {
|
||||
this.logger.error(`IoRedis connection error: ${err.message}`);
|
||||
this.isReady = false;
|
||||
});
|
||||
|
||||
this.redis.on("connect", () => {
|
||||
this.logger.log("IoRedis connected!");
|
||||
this.isReady = true;
|
||||
});
|
||||
|
||||
this.redis.on("reconnecting", (delay: string) => {
|
||||
this.logger.warn(`IoRedis reconnecting... next retry in ${delay}ms`);
|
||||
});
|
||||
|
||||
this.redis.on("end", () => {
|
||||
this.logger.warn("IoRedis connection ended.");
|
||||
this.isReady = false;
|
||||
});
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
@@ -22,4 +42,99 @@ export class RedisService implements OnModuleDestroy {
|
||||
this.logger.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async get<TData>(key: string): Promise<TData | null> {
|
||||
let data = null;
|
||||
if (!this.isReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
data = await this.redis.get(key);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) this.logger.error(`IoRedis get failed: ${err.message}`);
|
||||
}
|
||||
|
||||
if (data === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(data) as TData;
|
||||
} catch (e) {
|
||||
return data as TData;
|
||||
}
|
||||
}
|
||||
|
||||
async del(key: string): Promise<number> {
|
||||
if (!this.isReady) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
return this.redis.del(key);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) this.logger.error(`IoRedis del failed: ${err.message}`);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
async set<TData>(key: string, value: TData, opts?: { ttl?: number }): Promise<"OK" | TData | null> {
|
||||
if (!this.isReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const stringifiedValue = typeof value === "object" ? JSON.stringify(value) : String(value);
|
||||
if (opts?.ttl) {
|
||||
await this.redis.set(key, stringifiedValue, "PX", opts.ttl);
|
||||
} else {
|
||||
await this.redis.set(key, stringifiedValue);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof Error) this.logger.error(`IoRedis set failed: ${err.message}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return "OK";
|
||||
}
|
||||
|
||||
async expire(key: string, seconds: number): Promise<0 | 1> {
|
||||
if (!this.isReady) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
return this.redis.expire(key, seconds) as Promise<0 | 1>;
|
||||
} catch (err) {
|
||||
if (err instanceof Error) this.logger.error(`IoRedis expire failed: ${err.message}`);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
async lrange<TResult = string>(key: string, start: number, end: number): Promise<TResult[]> {
|
||||
if (!this.isReady) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const results = await this.redis.lrange(key, start, end);
|
||||
return results.map((item) => JSON.parse(item) as TResult);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) this.logger.error(`IoRedis lrange failed: ${err.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async lpush<TData>(key: string, ...elements: TData[]): Promise<number> {
|
||||
if (!this.isReady) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
const stringifiedElements = elements.map((element) =>
|
||||
typeof element === "object" ? JSON.stringify(element) : String(element)
|
||||
);
|
||||
return this.redis.lpush(key, ...stringifiedElements);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) this.logger.error(`IoRedis lpush failed: ${err.message}`);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,17 +156,4 @@ export class TeamsEventTypesService {
|
||||
|
||||
return this.eventTypesRepository.deleteEventType(eventTypeId);
|
||||
}
|
||||
|
||||
async deleteUserTeamEventTypesAndHosts(userId: number, teamId: number) {
|
||||
try {
|
||||
await this.teamsEventTypesRepository.deleteUserManagedTeamEventTypes(userId, teamId);
|
||||
await this.teamsEventTypesRepository.removeUserFromTeamEventTypesHosts(userId, teamId);
|
||||
} catch (err) {
|
||||
this.logger.error("Could not remove user from all team event-types.", {
|
||||
error: err,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-8
@@ -3,7 +3,6 @@ import { API_KEY_HEADER } from "@/lib/docs/headers";
|
||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
|
||||
import { CreateTeamMembershipInput } from "@/modules/teams/memberships/inputs/create-team-membership.input";
|
||||
import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/update-team-membership.input";
|
||||
import { CreateTeamMembershipOutput } from "@/modules/teams/memberships/outputs/create-team-membership.output";
|
||||
@@ -45,10 +44,7 @@ import { SkipTakePagination } from "@calcom/platform-types";
|
||||
export class TeamsMembershipsController {
|
||||
private logger = new Logger("TeamsMembershipsController");
|
||||
|
||||
constructor(
|
||||
private teamsMembershipsService: TeamsMembershipsService,
|
||||
private teamsEventTypesService: TeamsEventTypesService
|
||||
) {}
|
||||
constructor(private teamsMembershipsService: TeamsMembershipsService) {}
|
||||
|
||||
@Roles("TEAM_ADMIN")
|
||||
@Post("/")
|
||||
@@ -127,7 +123,6 @@ export class TeamsMembershipsController {
|
||||
membershipId,
|
||||
body
|
||||
);
|
||||
|
||||
if (!currentMembership.accepted && updatedMembership.accepted) {
|
||||
try {
|
||||
await updateNewTeamMemberEventTypes(updatedMembership.userId, teamId);
|
||||
@@ -151,8 +146,6 @@ export class TeamsMembershipsController {
|
||||
): Promise<DeleteTeamMembershipOutput> {
|
||||
const membership = await this.teamsMembershipsService.deleteTeamMembership(teamId, membershipId);
|
||||
|
||||
await this.teamsEventTypesService.deleteUserTeamEventTypesAndHosts(membership.userId, teamId);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
||||
|
||||
@@ -3,6 +3,8 @@ import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/up
|
||||
import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { TeamService } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class TeamsMembershipsService {
|
||||
constructor(private readonly teamsMembershipsRepository: TeamsMembershipsRepository) {}
|
||||
@@ -41,10 +43,15 @@ export class TeamsMembershipsService {
|
||||
}
|
||||
|
||||
async deleteTeamMembership(teamId: number, membershipId: number) {
|
||||
const teamMembership = await this.teamsMembershipsRepository.deleteTeamMembershipById(
|
||||
teamId,
|
||||
membershipId
|
||||
);
|
||||
// First get the membership to get the userId
|
||||
const teamMembership = await this.teamsMembershipsRepository.findTeamMembership(teamId, membershipId);
|
||||
|
||||
if (!teamMembership) {
|
||||
throw new NotFoundException(`Membership with id ${membershipId} not found in team ${teamId}`);
|
||||
}
|
||||
|
||||
await TeamService.removeMembers({ teamIds: [teamId], userIds: [teamMembership.userId], isOrg: false });
|
||||
|
||||
return teamMembership;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service";
|
||||
import { Controller, UseGuards, Get, Param, ParseIntPipe, Query } from "@nestjs/common";
|
||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { GetSchedulesOutput_2024_06_11, SkipTakePagination } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/teams/:teamId/schedules",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(ApiAuthGuard, RolesGuard)
|
||||
@DocsTags("Teams / Schedules")
|
||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
export class TeamsSchedulesController {
|
||||
constructor(private teamsSchedulesService: TeamsSchedulesService) {}
|
||||
|
||||
@Roles("TEAM_ADMIN")
|
||||
@Get("/")
|
||||
@ApiOperation({
|
||||
summary: "Get all team member schedules",
|
||||
})
|
||||
async getTeamSchedules(
|
||||
@Param("teamId", ParseIntPipe) teamId: number,
|
||||
@Query() queryParams: SkipTakePagination
|
||||
): Promise<GetSchedulesOutput_2024_06_11> {
|
||||
const { skip, take } = queryParams;
|
||||
|
||||
const schedules = await this.teamsSchedulesService.getTeamSchedules(teamId, skip, take);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: schedules,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
|
||||
import { Team, Schedule } from "@calcom/prisma/client";
|
||||
|
||||
describe("Teams Schedules Endpoints", () => {
|
||||
describe("User Authentication - User is Team Admin", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
|
||||
let teamsRepositoryFixture: TeamRepositoryFixture;
|
||||
let scheduleRepositoryFixture: SchedulesRepositoryFixture;
|
||||
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
|
||||
let nonOrgTeam: Team;
|
||||
|
||||
let userSchedule: Schedule;
|
||||
let user2Schedule: Schedule;
|
||||
|
||||
const userEmail = `teams-schedules-admin-${randomString()}@api.com`;
|
||||
const userEmail2 = `teams-schedules-member-${randomString()}@api.com`;
|
||||
|
||||
let user: User;
|
||||
let user2: User;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||
})
|
||||
).compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
|
||||
scheduleRepositoryFixture = new SchedulesRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
username: userEmail,
|
||||
});
|
||||
user2 = await userRepositoryFixture.create({
|
||||
email: userEmail2,
|
||||
username: userEmail2,
|
||||
});
|
||||
|
||||
userSchedule = await scheduleRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
name: `teams-schedules-user-schedule-${randomString()}`,
|
||||
timeZone: "America/New_York",
|
||||
});
|
||||
|
||||
user2Schedule = await scheduleRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
id: user2.id,
|
||||
},
|
||||
},
|
||||
name: `teams-schedules-user2-schedule-${randomString()}`,
|
||||
timeZone: "America/New_York",
|
||||
});
|
||||
|
||||
nonOrgTeam = await teamsRepositoryFixture.create({
|
||||
name: `teams-schedules-non-org-team-${randomString()}`,
|
||||
isOrganization: false,
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "ADMIN",
|
||||
user: { connect: { id: user.id } },
|
||||
team: { connect: { id: nonOrgTeam.id } },
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: user2.id } },
|
||||
team: { connect: { id: nonOrgTeam.id } },
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it("should get all the schedules of members in a team", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/teams/${nonOrgTeam.id}/schedules`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: GetSchedulesOutput_2024_06_11 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(Array.isArray(responseBody.data)).toBe(true);
|
||||
expect(responseBody.data.length).toEqual(2);
|
||||
|
||||
const userOneSchedule = responseBody.data.find((schedule) => schedule.id === userSchedule.id);
|
||||
const userTwoSchedule = responseBody.data.find((schedule) => schedule.id === user2Schedule.id);
|
||||
|
||||
expect(userOneSchedule).toBeDefined();
|
||||
expect(userTwoSchedule).toBeDefined();
|
||||
|
||||
expect(userOneSchedule?.id).toEqual(userSchedule.id);
|
||||
expect(userOneSchedule?.name).toEqual(userSchedule.name);
|
||||
expect(userOneSchedule?.timeZone).toEqual(userSchedule.timeZone);
|
||||
|
||||
expect(userTwoSchedule?.id).toEqual(user2Schedule.id);
|
||||
expect(userTwoSchedule?.name).toEqual(user2Schedule.name);
|
||||
expect(userOneSchedule?.timeZone).toEqual(user2Schedule.timeZone);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await userRepositoryFixture.deleteByEmail(user2.email);
|
||||
await teamsRepositoryFixture.delete(nonOrgTeam.id);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
||||
import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service";
|
||||
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { ScheduleOutput_2024_06_11 } from "@calcom/platform-types";
|
||||
|
||||
@Injectable()
|
||||
export class TeamsSchedulesService {
|
||||
constructor(
|
||||
private readonly teamsRepository: TeamsRepository,
|
||||
private readonly schedulesRepository: SchedulesRepository_2024_06_11,
|
||||
private readonly outputSchedulesService: OutputSchedulesService_2024_06_11
|
||||
) {}
|
||||
|
||||
async getTeamSchedules(teamId: number, skip = 0, take = 250) {
|
||||
const userIds = await this.teamsRepository.getTeamUsersIds(teamId);
|
||||
const schedules = await this.schedulesRepository.getSchedulesByUserIds(userIds, skip, take);
|
||||
|
||||
const responseSchedules: ScheduleOutput_2024_06_11[] = [];
|
||||
|
||||
for (const schedule of schedules) {
|
||||
responseSchedules.push(await this.outputSchedulesService.getResponseSchedule(schedule));
|
||||
}
|
||||
|
||||
return responseSchedules;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
||||
import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service";
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { OrganizationSchedulesRepository } from "@/modules/organizations/schedules/organizations-schedules.repository";
|
||||
import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||
import { TeamsMembershipsService } from "@/modules/teams/memberships/services/teams-memberships.service";
|
||||
import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository";
|
||||
import { TeamsSchedulesController } from "@/modules/teams/schedules/controllers/teams-schedules.controller";
|
||||
import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service";
|
||||
import { TeamsController } from "@/modules/teams/teams/controllers/teams.controller";
|
||||
import { TeamsService } from "@/modules/teams/teams/services/teams.service";
|
||||
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, MembershipsModule, RedisModule],
|
||||
providers: [
|
||||
TeamsRepository,
|
||||
TeamsService,
|
||||
TeamsMembershipsRepository,
|
||||
TeamsMembershipsService,
|
||||
OutputSchedulesService_2024_06_11,
|
||||
OrganizationSchedulesRepository,
|
||||
StripeService,
|
||||
UsersRepository,
|
||||
AppsRepository,
|
||||
CredentialsRepository,
|
||||
OrganizationsRepository,
|
||||
TeamsSchedulesService,
|
||||
OrganizationsTeamsRepository,
|
||||
SchedulesRepository_2024_06_11,
|
||||
],
|
||||
controllers: [TeamsController, TeamsSchedulesController],
|
||||
exports: [TeamsRepository],
|
||||
})
|
||||
export class TeamsSchedulesModule {}
|
||||
@@ -10,6 +10,7 @@ export const SMS_ATTENDEE = "sms_attendee";
|
||||
export const SMS_NUMBER = "sms_number";
|
||||
export const WHATSAPP_ATTENDEE = "whatsapp_attendee";
|
||||
export const WHATSAPP_NUMBER = "whatsapp_number";
|
||||
export const CAL_AI_PHONE_CALL = "cal_ai_phone_call";
|
||||
|
||||
export const STEP_ACTIONS = [
|
||||
EMAIL_HOST,
|
||||
@@ -19,6 +20,7 @@ export const STEP_ACTIONS = [
|
||||
SMS_NUMBER,
|
||||
WHATSAPP_ATTENDEE,
|
||||
WHATSAPP_NUMBER,
|
||||
CAL_AI_PHONE_CALL,
|
||||
] as const;
|
||||
|
||||
export const STEP_ACTIONS_TO_ENUM = {
|
||||
@@ -29,6 +31,7 @@ export const STEP_ACTIONS_TO_ENUM = {
|
||||
[WHATSAPP_ATTENDEE]: WorkflowActions.WHATSAPP_ATTENDEE,
|
||||
[WHATSAPP_NUMBER]: WorkflowActions.WHATSAPP_NUMBER,
|
||||
[SMS_NUMBER]: WorkflowActions.SMS_NUMBER,
|
||||
[CAL_AI_PHONE_CALL]: WorkflowActions.CAL_AI_PHONE_CALL,
|
||||
} as const;
|
||||
|
||||
export const ENUM_TO_STEP_ACTIONS = {
|
||||
@@ -39,6 +42,7 @@ export const ENUM_TO_STEP_ACTIONS = {
|
||||
[WorkflowActions.WHATSAPP_ATTENDEE]: WHATSAPP_ATTENDEE,
|
||||
[WorkflowActions.WHATSAPP_NUMBER]: WHATSAPP_NUMBER,
|
||||
[WorkflowActions.SMS_NUMBER]: SMS_NUMBER,
|
||||
[WorkflowActions.CAL_AI_PHONE_CALL]: CAL_AI_PHONE_CALL,
|
||||
} as const;
|
||||
|
||||
export type StepAction = (typeof STEP_ACTIONS)[number];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { TimeUnit, WorkflowTriggerEvents } from "@prisma/client";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsNumber, ValidateNested } from "class-validator";
|
||||
import { IsIn, IsNumber, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
export const BEFORE_EVENT = "beforeEvent";
|
||||
export const EVENT_CANCELLED = "eventCancelled";
|
||||
@@ -68,6 +68,8 @@ export class WorkflowTriggerOffsetDto {
|
||||
value!: number;
|
||||
|
||||
@ApiProperty({ description: "Unit for the offset time", example: HOUR })
|
||||
@IsString()
|
||||
@IsIn(TIME_UNITS)
|
||||
unit!: TimeUnitType;
|
||||
}
|
||||
|
||||
@@ -75,6 +77,8 @@ export class BaseWorkflowTriggerDto {
|
||||
@ApiProperty({
|
||||
description: "Trigger type for the workflow",
|
||||
})
|
||||
@IsString()
|
||||
@IsIn([WORKFLOW_TRIGGER_TYPES])
|
||||
type!: WorkflowTriggerType;
|
||||
}
|
||||
|
||||
@@ -82,6 +86,8 @@ export class OnCreationTriggerDto {
|
||||
@ApiProperty({
|
||||
description: "Trigger type for the workflow",
|
||||
})
|
||||
@IsString()
|
||||
@IsIn([NEW_EVENT])
|
||||
type: typeof NEW_EVENT = NEW_EVENT;
|
||||
}
|
||||
|
||||
@@ -89,12 +95,16 @@ export class OnRescheduleTriggerDto {
|
||||
@ApiProperty({
|
||||
description: "Trigger type for the workflow",
|
||||
})
|
||||
@IsString()
|
||||
@IsIn([RESCHEDULE_EVENT])
|
||||
type: typeof RESCHEDULE_EVENT = RESCHEDULE_EVENT;
|
||||
}
|
||||
export class OnCancelTriggerDto {
|
||||
@ApiProperty({
|
||||
description: "Trigger type for the workflow",
|
||||
})
|
||||
@IsString()
|
||||
@IsIn([EVENT_CANCELLED])
|
||||
type: typeof EVENT_CANCELLED = EVENT_CANCELLED;
|
||||
}
|
||||
|
||||
@@ -113,6 +123,8 @@ export class OnBeforeEventTriggerDto extends TriggerOffsetDTO {
|
||||
description: "Trigger type for the workflow",
|
||||
example: BEFORE_EVENT,
|
||||
})
|
||||
@IsString()
|
||||
@IsIn([BEFORE_EVENT])
|
||||
type: typeof BEFORE_EVENT = BEFORE_EVENT;
|
||||
}
|
||||
|
||||
@@ -121,6 +133,8 @@ export class OnAfterEventTriggerDto extends TriggerOffsetDTO {
|
||||
description: "Trigger type for the workflow",
|
||||
example: AFTER_EVENT,
|
||||
})
|
||||
@IsString()
|
||||
@IsIn([AFTER_EVENT])
|
||||
type: typeof AFTER_EVENT = AFTER_EVENT;
|
||||
}
|
||||
|
||||
@@ -129,6 +143,8 @@ export class OnAfterCalVideoGuestsNoShowTriggerDto extends TriggerOffsetDTO {
|
||||
description: "Trigger type for the workflow",
|
||||
example: AFTER_GUESTS_CAL_VIDEO_NO_SHOW,
|
||||
})
|
||||
@IsString()
|
||||
@IsIn([AFTER_GUESTS_CAL_VIDEO_NO_SHOW])
|
||||
type: typeof AFTER_GUESTS_CAL_VIDEO_NO_SHOW = AFTER_GUESTS_CAL_VIDEO_NO_SHOW;
|
||||
}
|
||||
|
||||
@@ -137,5 +153,7 @@ export class OnAfterCalVideoHostsNoShowTriggerDto extends TriggerOffsetDTO {
|
||||
description: "Trigger type for the workflow",
|
||||
example: AFTER_HOSTS_CAL_VIDEO_NO_SHOW,
|
||||
})
|
||||
@IsString()
|
||||
@IsIn([AFTER_HOSTS_CAL_VIDEO_NO_SHOW])
|
||||
type: typeof AFTER_HOSTS_CAL_VIDEO_NO_SHOW = AFTER_HOSTS_CAL_VIDEO_NO_SHOW;
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -59,4 +59,8 @@ export class EventTypesRepositoryFixture {
|
||||
async delete(eventTypeId: EventType["id"]) {
|
||||
return this.prismaWriteClient.eventType.delete({ where: { id: eventTypeId } });
|
||||
}
|
||||
|
||||
async findById(eventTypeId: EventType["id"]) {
|
||||
return this.prismaReadClient.eventType.findUnique({ where: { id: eventTypeId } });
|
||||
}
|
||||
}
|
||||
+9
@@ -34,4 +34,13 @@ export class ManagedOrganizationsRepositoryFixture {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async createManagedOrganization(managerOrganizationId: number, managedOrganizationId: number) {
|
||||
return this.prismaWriteClient.managedOrganization.create({
|
||||
data: {
|
||||
managerOrganizationId,
|
||||
managedOrganizationId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -35,4 +35,8 @@ export class MembershipRepositoryFixture {
|
||||
await this.prismaWriteClient.user.update({ where: { id: user.id }, data: { organizationId: org.id } });
|
||||
return membership;
|
||||
}
|
||||
|
||||
async findById(membershipId: Membership["id"]) {
|
||||
return this.prismaReadClient.membership.findUnique({ where: { id: membershipId } });
|
||||
}
|
||||
}
|
||||
@@ -36,4 +36,5 @@ process.env = {
|
||||
CALENDSO_ENCRYPTION_KEY: "22gfxhWUlcKliUeXcu8xNah2+HP/29ZX",
|
||||
INTEGRATION_TEST_MODE: "true",
|
||||
e2e: "true",
|
||||
SLOTS_CACHE_TTL: "1"
|
||||
};
|
||||
@@ -27,7 +27,8 @@
|
||||
"@calcom/platform-libraries/workflows": ["../../../packages/platform/libraries/workflows.ts"],
|
||||
"@calcom/platform-libraries/conferencing": ["../../../packages/platform/libraries/conferencing.ts"],
|
||||
"@calcom/platform-libraries/repositories": ["../../../packages/platform/libraries/repositories.ts"],
|
||||
"@calcom/platform-libraries/bookings": ["../../../packages/platform/libraries/bookings.ts"]
|
||||
"@calcom/platform-libraries/bookings": ["../../../packages/platform/libraries/bookings.ts"],
|
||||
"@calcom/platform-libraries/private-links": ["../../../packages/platform/libraries/private-links.ts"]
|
||||
},
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
@@ -16,7 +16,7 @@ import slugify from "@calcom/lib/slugify";
|
||||
import { BookingStatus, RedirectType } from "@calcom/prisma/enums";
|
||||
|
||||
import { buildLegacyCtx, buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect";
|
||||
import { handleOrgRedirect } from "@lib/handleOrgRedirect";
|
||||
|
||||
import CachedClientView, { type TeamBookingPageProps } from "~/team/type-view-cached";
|
||||
|
||||
@@ -124,19 +124,17 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
|
||||
const CachedTeamBooker = async ({ params, searchParams }: PageProps) => {
|
||||
const { currentOrgDomain, isValidOrgDomain, teamSlug, meetingSlug } = await _getOrgContext(await params);
|
||||
const isOrgContext = currentOrgDomain && isValidOrgDomain;
|
||||
const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||
|
||||
// Handle org redirects for non-org contexts
|
||||
if (!isOrgContext) {
|
||||
const redirectResult = await getTemporaryOrgRedirect({
|
||||
slugs: teamSlug,
|
||||
redirectType: RedirectType.Team,
|
||||
eventTypeSlug: meetingSlug,
|
||||
currentQuery: legacyCtx.query,
|
||||
});
|
||||
if (redirectResult) return redirect(redirectResult.redirect.destination);
|
||||
}
|
||||
// Handle org redirects
|
||||
const redirectResult = await handleOrgRedirect({
|
||||
slugs: [teamSlug],
|
||||
redirectType: RedirectType.Team,
|
||||
eventTypeSlug: meetingSlug,
|
||||
context: legacyCtx,
|
||||
currentOrgDomain: isValidOrgDomain ? currentOrgDomain : null,
|
||||
});
|
||||
if (redirectResult) return redirect(redirectResult.redirect.destination);
|
||||
|
||||
const teamData = await getCachedTeamData(teamSlug, currentOrgDomain);
|
||||
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { createRouterCaller } from "app/_trpc/context";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { AppCategories } from "@calcom/prisma/enums";
|
||||
import { appsRouter } from "@calcom/trpc/server/routers/viewer/apps/_router";
|
||||
import { calendarsRouter } from "@calcom/trpc/server/routers/viewer/calendars/_router";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
import InstalledApps from "~/apps/installed/[category]/installed-category-view";
|
||||
|
||||
const querySchema = z.object({
|
||||
@@ -31,16 +35,23 @@ const InstalledAppsWrapper = async ({ params }: PageProps) => {
|
||||
redirect("/apps/installed/calendar");
|
||||
}
|
||||
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
if (!session?.user?.id) {
|
||||
return redirect("/auth/login");
|
||||
}
|
||||
|
||||
const [calendarsCaller, appsCaller] = await Promise.all([
|
||||
createRouterCaller(calendarsRouter),
|
||||
createRouterCaller(appsRouter),
|
||||
]);
|
||||
|
||||
const connectedCalendars = await calendarsCaller.connectedCalendars();
|
||||
const installedCalendars = await appsCaller.integrations({
|
||||
variant: "calendar",
|
||||
onlyInstalled: true,
|
||||
});
|
||||
const [connectedCalendars, installedCalendars] = await Promise.all([
|
||||
calendarsCaller.connectedCalendars(),
|
||||
appsCaller.integrations({
|
||||
variant: "calendar",
|
||||
onlyInstalled: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
return (
|
||||
<InstalledApps
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { createRouterCaller } from "app/_trpc/context";
|
||||
import type { PageProps as ServerPageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { meRouter } from "@calcom/trpc/server/routers/viewer/me/_router";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/getting-started/[[...step]]/getServerSideProps";
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
import type { PageProps as ClientPageProps } from "~/getting-started/[[...step]]/onboarding-view";
|
||||
import Page from "~/getting-started/[[...step]]/onboarding-view";
|
||||
|
||||
export const generateMetadata = async ({ params }: ServerPageProps) => {
|
||||
@@ -23,13 +26,28 @@ export const generateMetadata = async ({ params }: ServerPageProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<ClientPageProps>(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
|
||||
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
|
||||
const props = await getData(context);
|
||||
return <Page {...props} />;
|
||||
if (!session?.user?.id) {
|
||||
return redirect("/auth/login");
|
||||
}
|
||||
|
||||
const userRepo = new UserRepository(prisma);
|
||||
const meCaller = await createRouterCaller(meRouter);
|
||||
|
||||
const [userTeams, user] = await Promise.all([
|
||||
userRepo.findUserTeams({
|
||||
id: session.user.id,
|
||||
}),
|
||||
meCaller.get(),
|
||||
]);
|
||||
|
||||
if (!userTeams || !user) {
|
||||
return redirect("/auth/login");
|
||||
}
|
||||
|
||||
return <Page user={user} hasPendingInvites={!!userTeams.teams.find((team) => team.accepted === false)} />;
|
||||
};
|
||||
|
||||
export default ServerPage;
|
||||
+4
-1
@@ -5,6 +5,7 @@ import { unstable_cache } from "next/cache";
|
||||
import { RoleManagementFactory } from "@calcom/features/pbac/services/role-management.factory";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { PrismaAttributeRepository } from "@calcom/lib/server/repository/PrismaAttributeRepository";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { viewerTeamsRouter } from "@calcom/trpc/server/routers/viewer/teams/_router";
|
||||
|
||||
import { TeamMembersView } from "~/teams/team-members-view";
|
||||
@@ -36,8 +37,10 @@ const getCachedTeamRoles = unstable_cache(
|
||||
const getCachedTeamAttributes = unstable_cache(
|
||||
async (organizationId?: number) => {
|
||||
if (!organizationId) return [];
|
||||
const attributeRepo = new PrismaAttributeRepository(prisma);
|
||||
|
||||
try {
|
||||
return await PrismaAttributeRepository.findAllByOrgIdWithOptions({ orgId: organizationId });
|
||||
return await attributeRepo.findAllByOrgIdWithOptions({ orgId: organizationId });
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
|
||||
+4
-1
@@ -4,6 +4,7 @@ import { unstable_cache } from "next/cache";
|
||||
|
||||
import { RoleManagementFactory } from "@calcom/features/pbac/services/role-management.factory";
|
||||
import { PrismaAttributeRepository } from "@calcom/lib/server/repository/PrismaAttributeRepository";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { viewerOrganizationsRouter } from "@calcom/trpc/server/routers/viewer/organizations/_router";
|
||||
|
||||
import { MembersView } from "~/members/members-view";
|
||||
@@ -19,7 +20,9 @@ export const generateMetadata = async () =>
|
||||
|
||||
const getCachedAttributes = unstable_cache(
|
||||
async (orgId: number) => {
|
||||
return await PrismaAttributeRepository.findAllByOrgIdWithOptions({ orgId });
|
||||
const attributeRepo = new PrismaAttributeRepository(prisma);
|
||||
|
||||
return await attributeRepo.findAllByOrgIdWithOptions({ orgId });
|
||||
},
|
||||
undefined,
|
||||
{ revalidate: 3600, tags: ["viewer.attributes.list"] } // Cache for 1 hour
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
||||
|
||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/about-view";
|
||||
|
||||
@@ -17,10 +11,7 @@ export const generateMetadata = async () =>
|
||||
"/settings/organizations/new/about"
|
||||
);
|
||||
|
||||
const getData = withAppDirSsr(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
||||
const ServerPage = async () => {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<LegacyPage />
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
||||
|
||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/add-teams-view";
|
||||
|
||||
@@ -17,10 +11,7 @@ export const generateMetadata = async () =>
|
||||
"/settings/organizations/new/add-teams"
|
||||
);
|
||||
|
||||
const getData = withAppDirSsr(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
||||
const ServerPage = async () => {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<LegacyPage />
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies } from "next/headers";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
||||
|
||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/onboarding-handover";
|
||||
|
||||
@@ -18,10 +11,7 @@ export const generateMetadata = async () =>
|
||||
"/settings/organizations/new/handover"
|
||||
);
|
||||
|
||||
const getData = withAppDirSsr(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
||||
const ServerPage = async () => {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<LegacyPage />
|
||||
|
||||
+6
-12
@@ -1,21 +1,15 @@
|
||||
import type { GetServerSidePropsResult } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
export const getServerSideProps = async (): Promise<GetServerSidePropsResult<{ isOrg: boolean }>> => {
|
||||
export default async function Layout({ children }: { children: React.ReactNode }) {
|
||||
const featuresRepository = new FeaturesRepository(prisma);
|
||||
const organizations = await featuresRepository.checkIfFeatureIsEnabledGlobally("organizations");
|
||||
// Check if organizations are enabled
|
||||
|
||||
if (!organizations) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
isOrg: true,
|
||||
},
|
||||
};
|
||||
};
|
||||
return children;
|
||||
}
|
||||
+1
-10
@@ -1,10 +1,4 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
||||
|
||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/onboard-members-view";
|
||||
|
||||
@@ -17,10 +11,7 @@ export const generateMetadata = async () =>
|
||||
"/settings/organizations/new/onboard-members"
|
||||
);
|
||||
|
||||
const getData = withAppDirSsr(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
||||
const ServerPage = async () => {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<LegacyPage />
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
||||
|
||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/create-new-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
@@ -19,14 +13,7 @@ export const generateMetadata = async () =>
|
||||
"/settings/organizations/new"
|
||||
);
|
||||
|
||||
type Props = {
|
||||
isOrg: boolean;
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<Props>(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
||||
const ServerPage = async () => {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<LicenseRequired>
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
||||
|
||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/payment-status-view";
|
||||
|
||||
@@ -17,10 +11,7 @@ export const generateMetadata = async () =>
|
||||
"/settings/organizations/new/status"
|
||||
);
|
||||
|
||||
const getData = withAppDirSsr(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
||||
const ServerPage = async () => {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<LegacyPage />
|
||||
|
||||
Loaded 100 of 615 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user