From 3364573c7aa0ca7df482af2c32866ed3b5b6bdc8 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Tue, 26 Oct 2021 21:02:10 -0300 Subject: [PATCH 01/10] Add typings to RestApiClient module --- app/utils/client/lib/RestApiClient.d.ts | 43 +++++++++++++++++++ app/utils/client/lib/RestApiClient.js | 1 + .../contexts/ServerContext/ServerContext.ts | 6 +-- client/hooks/useEndpointAction.ts | 2 +- client/hooks/useEndpointActionExperimental.ts | 2 +- client/hooks/useEndpointData.ts | 4 +- client/lib/userData.ts | 34 +++++++++++++-- client/providers/ServerProvider.tsx | 2 +- client/startup/banners.ts | 17 ++++---- definition/Serialized.ts | 19 ++++---- 10 files changed, 100 insertions(+), 30 deletions(-) create mode 100644 app/utils/client/lib/RestApiClient.d.ts diff --git a/app/utils/client/lib/RestApiClient.d.ts b/app/utils/client/lib/RestApiClient.d.ts new file mode 100644 index 0000000000000..452fb2a9c13d7 --- /dev/null +++ b/app/utils/client/lib/RestApiClient.d.ts @@ -0,0 +1,43 @@ +import { Serialized } from '../../../../definition/Serialized'; + +export declare const APIClient: { + delete(endpoint: string, params?: Serialized

): Promise>; + get(endpoint: string, params?: Serialized

): Promise>; + post(endpoint: string, params?: Serialized

, body?: B): Promise>; + upload( + endpoint: string, + params?: Serialized

, + formData?: B, + xhrOptions?: { + progress: (amount: number) => void; + error: (ev: ProgressEvent) => void; + } + ): { promise: Promise> }; + getCredentials(): { + 'X-User-Id': string; + 'X-Auth-Token': string; + }; + _jqueryCall( + method?: string, + endpoint?: string, + params?: any, + body?: any, + headers?: Record, + dataType?: string + ): any; + v1: { + delete(endpoint: string, params?: Serialized

): Promise>; + get(endpoint: string, params?: Serialized

): Promise>; + post(endpoint: string, params?: Serialized

, body?: B): Promise>; + upload( + endpoint: string, + params?: Serialized

, + formData?: B, + xhrOptions?: { + progress: (amount: number) => void; + error: (ev: ProgressEvent) => void; + } + ): { promise: Promise> }; + }; +}; diff --git a/app/utils/client/lib/RestApiClient.js b/app/utils/client/lib/RestApiClient.js index 9d857af0d580d..bcec20f9df76c 100644 --- a/app/utils/client/lib/RestApiClient.js +++ b/app/utils/client/lib/RestApiClient.js @@ -1,5 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { Accounts } from 'meteor/accounts-base'; +import jQuery from 'jquery'; import { process2faReturn } from '../../../../client/lib/2fa/process2faReturn'; import { baseURI } from '../../../../client/lib/baseURI'; diff --git a/client/contexts/ServerContext/ServerContext.ts b/client/contexts/ServerContext/ServerContext.ts index 15b8c50f97f51..fe603a4fdc0d8 100644 --- a/client/contexts/ServerContext/ServerContext.ts +++ b/client/contexts/ServerContext/ServerContext.ts @@ -1,6 +1,6 @@ import { createContext, useCallback, useContext, useMemo } from 'react'; -import { IServerInfo } from '../../../definition/IServerInfo'; +import type { IServerInfo } from '../../../definition/IServerInfo'; import type { Serialized } from '../../../definition/Serialized'; import type { PathFor, Params, Return, Method } from '../../../definition/rest'; import { @@ -21,7 +21,7 @@ type ServerContextValue = { callEndpoint: >( method: M, path: P, - params: Params[0], + params: Serialized[0]>, ) => Promise>>; uploadToEndpoint: (endpoint: string, params: any, formData: any) => Promise; getStream: ( @@ -73,7 +73,7 @@ export const useMethod = ( export const useEndpoint = >( method: M, path: P, -): ((params: Params[0]) => Promise>>) => { +): ((params: Serialized[0]>) => Promise>>) => { const { callEndpoint } = useContext(ServerContext); return useCallback((params) => callEndpoint(method, path, params), [callEndpoint, path, method]); diff --git a/client/hooks/useEndpointAction.ts b/client/hooks/useEndpointAction.ts index 0deb6cdd7d7c4..f8710047b94a9 100644 --- a/client/hooks/useEndpointAction.ts +++ b/client/hooks/useEndpointAction.ts @@ -8,7 +8,7 @@ import { useToastMessageDispatch } from '../contexts/ToastMessagesContext'; export const useEndpointAction = >( method: M, path: P, - params: Params[0] = {}, + params: Serialized[0]> = {} as Serialized[0]>, successMessage?: string, ): ((extraParams?: Params[1]) => Promise>>) => { const sendData = useEndpoint(method, path); diff --git a/client/hooks/useEndpointActionExperimental.ts b/client/hooks/useEndpointActionExperimental.ts index 0e82f6d428306..62c8e2ef28bb0 100644 --- a/client/hooks/useEndpointActionExperimental.ts +++ b/client/hooks/useEndpointActionExperimental.ts @@ -9,7 +9,7 @@ export const useEndpointActionExperimental = [0]) => Promise>>) => { +): ((params: Serialized[0]>) => Promise>>) => { const sendData = useEndpoint(method, path); const dispatchToastMessage = useToastMessageDispatch(); diff --git a/client/hooks/useEndpointData.ts b/client/hooks/useEndpointData.ts index 1e8f5ebc880cd..b328ba49184e7 100644 --- a/client/hooks/useEndpointData.ts +++ b/client/hooks/useEndpointData.ts @@ -6,11 +6,9 @@ import { useEndpoint } from '../contexts/ServerContext'; import { useToastMessageDispatch } from '../contexts/ToastMessagesContext'; import { AsyncState, useAsyncState } from './useAsyncState'; -const defaultParams = {}; - export const useEndpointData =

>( endpoint: P, - params: Params<'GET', P>[0] = defaultParams as Params<'GET', P>[0], + params: Serialized[0]> = {} as Serialized[0]>, initialValue?: Serialized> | (() => Serialized>), ): AsyncState>> & { reload: () => void } => { const { resolve, reject, reset, ...state } = useAsyncState(initialValue); diff --git a/client/lib/userData.ts b/client/lib/userData.ts index 2b37d1957f5a1..76488ead80137 100644 --- a/client/lib/userData.ts +++ b/client/lib/userData.ts @@ -5,14 +5,39 @@ import { Users } from '../../app/models/client'; import { Notifications } from '../../app/notifications/client'; import { APIClient } from '../../app/utils/client'; import type { IUser, IUserDataEvent } from '../../definition/IUser'; +import { Serialized } from '../../definition/Serialized'; export const isSyncReady = new ReactiveVar(false); -type RawUserData = Omit & { - _updatedAt: string; -}; +type RawUserData = Serialized< + Pick< + IUser, + | '_id' + | 'type' + | 'name' + | 'username' + | 'emails' + | 'status' + | 'statusDefault' + | 'statusText' + | 'statusConnection' + | 'avatarOrigin' + | 'utcOffset' + | 'language' + | 'settings' + | 'roles' + | 'active' + | 'defaultRoom' + | 'customFields' + | 'statusLivechat' + | 'oauth' + | 'createdAt' + | '_updatedAt' + | 'avatarETag' + > +>; -const updateUser = (userData: IUser & { _updatedAt: Date }): void => { +const updateUser = (userData: IUser): void => { const user: IUser = Users.findOne({ _id: userData._id }); if (!user || !user._updatedAt || user._updatedAt.getTime() < userData._updatedAt.getTime()) { @@ -57,6 +82,7 @@ export const synchronizeUserData = async (uid: Meteor.User['_id']): Promise( const callEndpoint = >( method: M, path: P, - params: Params[0], + params: Serialized[0]>, ): Promise>> => { const api = path[0] === '/' ? APIClient : APIClient.v1; const endpointPath = path[0] === '/' ? path.slice(1) : path; diff --git a/client/startup/banners.ts b/client/startup/banners.ts index 57e0d9c7daa9e..64ba23041accc 100644 --- a/client/startup/banners.ts +++ b/client/startup/banners.ts @@ -4,14 +4,15 @@ import { Tracker } from 'meteor/tracker'; import { Notifications } from '../../app/notifications/client'; import { APIClient } from '../../app/utils/client'; import { IBanner, BannerPlatform } from '../../definition/IBanner'; +import { Serialized } from '../../definition/Serialized'; import * as banners from '../lib/banners'; const fetchInitialBanners = async (): Promise => { - const response = (await APIClient.get('v1/banners', { - platform: BannerPlatform.Web, - })) as { + const response: Serialized<{ banners: IBanner[]; - }; + }> = await APIClient.get('v1/banners', { + platform: BannerPlatform.Web, + }); for (const banner of response.banners) { banners.open({ @@ -22,11 +23,11 @@ const fetchInitialBanners = async (): Promise => { }; const handleBanner = async (event: { bannerId: string }): Promise => { - const response = (await APIClient.get(`v1/banners/${event.bannerId}`, { - platform: BannerPlatform.Web, - })) as { + const response: Serialized<{ banners: IBanner[]; - }; + }> = await APIClient.get(`v1/banners/${event.bannerId}`, { + platform: BannerPlatform.Web, + }); if (!response.banners.length) { return banners.closeById(event.bannerId); diff --git a/definition/Serialized.ts b/definition/Serialized.ts index 2871f401689e3..a6626dcdb9b84 100644 --- a/definition/Serialized.ts +++ b/definition/Serialized.ts @@ -1,9 +1,10 @@ -export type Serialized = T extends Date - ? (Exclude | string) - : T extends boolean | number | string | null | undefined - ? T - : T extends {} - ? { - [K in keyof T]: Serialized; - } - : null; +export type Serialized = + T extends Date + ? (Exclude | string) + : T extends boolean | number | string | null | undefined + ? T + : T extends {} + ? { + [K in keyof T]: Serialized; + } + : null; From 81812a830020ecb065415d8de60295da032ec9ea Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Wed, 17 Nov 2021 13:23:57 -0300 Subject: [PATCH 02/10] Overhaul api.d.ts and some endpoints --- app/api/server/api.d.ts | 203 +++++++++++------- app/api/server/v1/banners.ts | 8 +- app/api/server/v1/roles.ts | 6 +- app/api/server/v1/settings.ts | 4 +- app/api/server/v1/teams.ts | 18 +- .../contexts/ServerContext/ServerContext.ts | 22 +- client/hooks/useEndpointAction.ts | 10 +- client/hooks/useEndpointActionExperimental.ts | 13 +- .../rest/helpers/ReplacePlaceholders.ts | 8 + definition/rest/index.ts | 58 ++++- definition/rest/v1/banners.ts | 8 +- definition/rest/v1/omnichannel.ts | 2 - definition/utils.ts | 3 + .../rest/v1/omnichannel/businessHours.ts | 12 +- 14 files changed, 239 insertions(+), 136 deletions(-) create mode 100644 definition/rest/helpers/ReplacePlaceholders.ts diff --git a/app/api/server/api.d.ts b/app/api/server/api.d.ts index 94b44c774fe6f..4c1d395b9dd07 100644 --- a/app/api/server/api.d.ts +++ b/app/api/server/api.d.ts @@ -1,77 +1,44 @@ -import { Endpoints } from '../../../definition/rest'; -import { Awaited } from '../../../definition/utils'; -import { IUser } from '../../../definition/IUser'; +import type { JoinPathPattern, Method, MethodOf, OperationParams, OperationResult, PathPattern, UrlParams } from '../../../definition/rest'; +import type { IUser } from '../../../definition/IUser'; - -export type ChangeTypeOfKeys< - T extends object, - Keys extends keyof T, - NewType -> = { - [key in keyof T]: key extends Keys ? NewType : T[key] -} - -type This = { - getPaginationItems(): ({ - offset: number; - count: number; - }); - parseJsonQuery(): ({ - sort: Record; - fields: Record; - query: Record; - }); - readonly urlParams: Record; - getUserFromParams(): IUser; -} - -type ThisLoggedIn = { - readonly user: IUser; - readonly userId: string; -} - -type ThisLoggedOut = { - readonly user: null; - readonly userId: null; -} - -type EndpointWithExtraOptions any, A> = WrappedFunction | ({ action: WrappedFunction } & (A extends true ? { twoFactorRequired: boolean } : {})); - -export type Methods = { - [K in keyof T as `${Lowercase}`]: T[K] extends (...args: any) => any ? EndpointWithExtraOptions<(this: This & (A extends true ? ThisLoggedIn : ThisLoggedOut) & Params[0]>) => ReturnType, A> : never; +type SuccessResult = { + statusCode: 200; + body: + T extends object + ? { success: true } & T + : T; }; -type Params = K extends 'GET' ? { readonly queryParams: Partial

} : K extends 'POST' ? { readonly bodyParams: Partial

} : never; - -type SuccessResult = { - statusCode: 200; - success: true; -} & T extends (undefined) ? {} : { body: T } +type FailureResult = { + statusCode: 400; + body: + T extends object + ? { success: false } & T + : ({ + success: false; + error: T; + stack: TStack; + errorType: TErrorType; + details: TErrorDetails; + }) & ( + undefined extends TErrorType + ? {} + : { errorType: TErrorType } + ) & ( + undefined extends TErrorDetails + ? {} + : { details: TErrorDetails extends string ? unknown : TErrorDetails } + ); +}; -type UnauthorizedResult = { +type UnauthorizedResult = { statusCode: 403; body: { success: false; - error: string; + error: T | 'unauthorized'; }; } -type FailureResult = { - statusCode: 400; -} & FailureBody; - -type FailureBody = Exclude extends object ? { body: T & E } : { - body: E & { error: string } & E extends Error ? { details: string } : {} & ST extends undefined ? {} : { stack: string } & ET extends undefined ? {} : { errorType: string }; -} - -type Errors = FailureResult | FailureResult | FailureResult | UnauthorizedResult; - -type WrappedFunction any> = (this: ThisParameterType, ...args: Parameters) => ReturnTypes; - -type ReturnTypes any> = PromisedOrNot> | PromisedOrNot>; - -type PromisedOrNot = Promise | T; - type Options = { permissionsRequired?: string[]; twoFactorOptions?: unknown; @@ -79,32 +46,102 @@ type Options = { authRequired?: boolean; } -export type RestEndpoints

= Methods; - -type ToLowerCaseKeys = { - [K in keyof T as `${Lowercase}`]: T[K]; +type ActionThis = { + urlParams: UrlParams; + // TODO make it unsafe + readonly queryParams: TMethod extends 'GET' ? Partial> : Record; + // TODO make it unsafe + readonly bodyParams: TMethod extends 'GET' ? Record : Partial>; + requestParams(): OperationParams; + getPaginationItems(): { + readonly offset: number; + readonly count: number; + }; + parseJsonQuery(): { + sort: Record; + fields: Record; + query: Record; + }; + getUserFromParams(): IUser; +} & ( + TOptions extends { authRequired: true } + ? { + readonly user: IUser; + readonly userId: string; + } + : { + readonly user: null; + readonly userId: null; + } +); + +export type ResultFor< + TMethod extends Method, + TPathPattern extends PathPattern +> = SuccessResult> | FailureResult | UnauthorizedResult; + +type Action = + ((this: ActionThis) => Promise>) + | ((this: ActionThis) => ResultFor); + +type Operation = Action | { + action: Action; +} & ({ twoFactorRequired: boolean }); + +type Operations = { + [M in MethodOf as Lowercase]: Operation, TPathPattern, TOptions>; }; -type ToResultType = { - [K in keyof T]: T[K] extends (...args: any) => any ? Awaited> : never; -} -export type ResultTypeEndpoints

= ToResultType>; -declare class APIClass { - addRoute

(route: P, endpoints: RestEndpoints

): void; +declare class APIClass { + addRoute< + TSubPathPattern extends string + >(subpath: TSubPathPattern, operations: Operations>): void; + + // addRoute< + // TSubPathPattern extends string, + // TPathPattern extends JoinPathPattern + // >(subpaths: TSubPathPattern[], operations: Operations): void; + + addRoute< + TSubPathPattern extends string, + TOptions extends Options + >( + subpath: TSubPathPattern, + options: TOptions, + operations: Operations, TOptions> + ): void; + + // addRoute< + // TSubPathPattern extends string, + // TPathPattern extends JoinPathPattern, + // TOptions extends Options + // >( + // subpaths: TSubPathPattern[], + // options: TOptions, + // operations: Operations + // ): void; - addRoute

(route: P, options: O, endpoints: RestEndpoints): void; + success(result: T): SuccessResult; - unauthorized(msg?: string): UnauthorizedResult; + success(): SuccessResult; - failure(result: string, errorType?: ET, stack?: ST, error?: E): FailureResult; + failure< + T, + TErrorType extends string, + TStack extends string, + TErrorDetails + >( + result: T, + errorType?: TErrorType, + stack?: TStack, + error?: { details: TErrorDetails } + ): FailureResult; - failure(result: object): FailureResult; + failure(result: T): FailureResult; - failure(): FailureResult; + failure(): FailureResult; - success(): SuccessResult; - - success(result: T): SuccessResult; + unauthorized(msg?: T): UnauthorizedResult; defaultFieldsToExclude: { joinCode: 0; @@ -115,6 +152,6 @@ declare class APIClass { } export declare const API: { - v1: APIClass; + v1: APIClass<'/v1'>; default: APIClass; }; diff --git a/app/api/server/v1/banners.ts b/app/api/server/v1/banners.ts index 678d6e726cf24..8d1944933da8a 100644 --- a/app/api/server/v1/banners.ts +++ b/app/api/server/v1/banners.ts @@ -54,7 +54,7 @@ import { BannerPlatform } from '../../../../definition/IBanner'; API.v1.addRoute('banners.getNew', { authRequired: true }, { // deprecated async get() { check(this.queryParams, Match.ObjectIncluding({ - platform: String, + platform: Match.OneOf(...Object.values(BannerPlatform)), bid: Match.Maybe(String), })); @@ -67,7 +67,7 @@ API.v1.addRoute('banners.getNew', { authRequired: true }, { // deprecated throw new Meteor.Error('error-unknown-platform', 'Platform is unknown.'); } - const banners = await Banner.getBannersForUser(this.userId, platform, bannerId); + const banners = await Banner.getBannersForUser(this.userId, platform, bannerId ?? undefined); return API.v1.success({ banners }); }, @@ -126,7 +126,7 @@ API.v1.addRoute('banners/:id', { authRequired: true }, { // TODO: move to users/ })); check(this.queryParams, Match.ObjectIncluding({ - platform: String, + platform: Match.OneOf(...Object.values(BannerPlatform)), })); const { platform } = this.queryParams; @@ -186,7 +186,7 @@ API.v1.addRoute('banners/:id', { authRequired: true }, { // TODO: move to users/ API.v1.addRoute('banners', { authRequired: true }, { async get() { check(this.queryParams, Match.ObjectIncluding({ - platform: String, + platform: Match.OneOf(...Object.values(BannerPlatform)), })); const { platform } = this.queryParams; diff --git a/app/api/server/v1/roles.ts b/app/api/server/v1/roles.ts index 95c4b34a5d7f3..5de4348624eb1 100644 --- a/app/api/server/v1/roles.ts +++ b/app/api/server/v1/roles.ts @@ -20,7 +20,7 @@ API.v1.addRoute('roles.list', { authRequired: true }, { API.v1.addRoute('roles.sync', { authRequired: true }, { async get() { - const { updatedSince } = this.queryParams; + const { updatedSince } = this.queryParams as any; // TODO if (isNaN(Date.parse(updatedSince))) { throw new Meteor.Error('error-updatedSince-param-invalid', 'The "updatedSince" query parameter must be a valid date.'); @@ -56,7 +56,7 @@ API.v1.addRoute('roles.create', { authRequired: true }, { throw new Meteor.Error('error-duplicate-role-names-not-allowed', 'Role name already exists'); } - if (['Users', 'Subscriptions'].includes(roleData.scope) === false) { + if (['Users', 'Subscriptions'].includes(roleData.scope as string) === false) { // TODO roleData.scope = 'Users'; } const roleId = (await Roles.createWithRandomId(roleData.name, roleData.scope, roleData.description, false, roleData.mandatory2fa)).insertedId; @@ -239,7 +239,7 @@ API.v1.addRoute('roles.removeUserFromRole', { authRequired: true }, { const data = { roleName: bodyParams.roleName, username: bodyParams.username, - scope: this.bodyParams.scope, + scope: (bodyParams as any).scope, // TODO }; if (!await hasPermissionAsync(this.userId, 'access-permissions')) { diff --git a/app/api/server/v1/settings.ts b/app/api/server/v1/settings.ts index b9233b1e09d13..ca0a8ac5178da 100644 --- a/app/api/server/v1/settings.ts +++ b/app/api/server/v1/settings.ts @@ -4,7 +4,7 @@ import _ from 'underscore'; import { Settings } from '../../../models/server/raw'; import { hasPermission } from '../../../authorization/server'; -import { API, ResultTypeEndpoints } from '../api'; +import { API, ResultFor } from '../api'; import { SettingsEvents, settings } from '../../../settings/server'; import { setValue } from '../../../settings/server/raw'; import { ISetting, ISettingColor, isSettingAction, isSettingColor } from '../../../../definition/ISetting'; @@ -126,7 +126,7 @@ API.v1.addRoute('settings/:_id', { authRequired: true }, { }, post: { twoFactorRequired: true, - async action(): Promise['post']> { + async action(): Promise> { if (!hasPermission(this.userId, 'edit-privileged-setting')) { return API.v1.unauthorized(); } diff --git a/app/api/server/v1/teams.ts b/app/api/server/v1/teams.ts index dab3ac1e0a82f..2b8eab31d8035 100644 --- a/app/api/server/v1/teams.ts +++ b/app/api/server/v1/teams.ts @@ -59,7 +59,7 @@ API.v1.addRoute('teams.create', { authRequired: true }, { if (!hasPermission(this.userId, 'create-team')) { return API.v1.unauthorized(); } - const { name, type, members, room, owner } = this.bodyParams; + const { name, type, members, room, owner } = this.bodyParams as any; // TODO if (!name) { return API.v1.failure('Body param "name" is required'); @@ -122,7 +122,7 @@ API.v1.addRoute('teams.convertToChannel', { authRequired: true }, { API.v1.addRoute('teams.addRooms', { authRequired: true }, { async post() { - const { rooms, teamId, teamName } = this.bodyParams; + const { rooms, teamId, teamName } = this.bodyParams as any; // TODO if (!teamId && !teamName) { return API.v1.failure('missing-teamId-or-teamName'); @@ -148,7 +148,7 @@ API.v1.addRoute('teams.removeRoom', { authRequired: true }, { if (!isTeamsRemoveRoomProps(this.bodyParams)) { return API.v1.failure('body-params-invalid', isTeamsRemoveRoomProps.errors?.map((error) => error.message).join('\n ')); } - const { roomId, teamId, teamName } = this.bodyParams; + const { roomId, teamId, teamName } = this.bodyParams as any; // TODO const team = await (teamId ? Team.getOneById(teamId) : Team.getOneByName(teamName)); if (!team) { @@ -169,7 +169,7 @@ API.v1.addRoute('teams.removeRoom', { authRequired: true }, { API.v1.addRoute('teams.updateRoom', { authRequired: true }, { async post() { - const { roomId, isDefault } = this.bodyParams; + const { roomId, isDefault } = this.bodyParams as any; // TODO const team = await Team.getOneByRoomId(roomId); if (!team) { @@ -189,7 +189,7 @@ API.v1.addRoute('teams.updateRoom', { authRequired: true }, { API.v1.addRoute('teams.listRooms', { authRequired: true }, { async get() { - const { teamId, teamName, filter, type } = this.queryParams; + const { teamId, teamName, filter, type } = this.queryParams as any; // TODO const { offset, count } = this.getPaginationItems(); const team = await (teamId ? Team.getOneById(teamId) : Team.getOneByName(teamName)); @@ -225,7 +225,7 @@ API.v1.addRoute('teams.listRooms', { authRequired: true }, { API.v1.addRoute('teams.listRoomsOfUser', { authRequired: true }, { async get() { const { offset, count } = this.getPaginationItems(); - const { teamId, teamName, userId, canUserDelete = false } = this.queryParams; + const { teamId, teamName, userId, canUserDelete = false } = this.queryParams as any; // TODO if (!teamId && !teamName) { @@ -266,7 +266,7 @@ API.v1.addRoute('teams.members', { authRequired: true }, { username: Match.Maybe(String), name: Match.Maybe(String), })); - const { teamId, teamName, status, username, name } = this.queryParams; + const { teamId, teamName, status, username, name } = this.queryParams as any; // TODO if (!teamId && !teamName) { return API.v1.failure('missing-teamId-or-teamName'); @@ -431,7 +431,7 @@ API.v1.addRoute('teams.leave', { authRequired: true }, { API.v1.addRoute('teams.info', { authRequired: true }, { async get() { - const { teamId, teamName } = this.queryParams; + const { teamId, teamName } = this.queryParams as any; // TODO if (!teamId && !teamName) { return API.v1.failure('Provide either the "teamId" or "teamName"'); @@ -498,7 +498,7 @@ API.v1.addRoute('teams.delete', { authRequired: true }, { API.v1.addRoute('teams.autocomplete', { authRequired: true }, { async get() { - const { name } = this.queryParams; + const { name } = this.queryParams as any; // TODO const teams = await Team.autocomplete(this.userId, name); diff --git a/client/contexts/ServerContext/ServerContext.ts b/client/contexts/ServerContext/ServerContext.ts index fe603a4fdc0d8..26e9db4660d4b 100644 --- a/client/contexts/ServerContext/ServerContext.ts +++ b/client/contexts/ServerContext/ServerContext.ts @@ -2,7 +2,7 @@ import { createContext, useCallback, useContext, useMemo } from 'react'; import type { IServerInfo } from '../../../definition/IServerInfo'; import type { Serialized } from '../../../definition/Serialized'; -import type { PathFor, Params, Return, Method } from '../../../definition/rest'; +import type { Params, Return, Method, PathFor } from '../../../definition/rest'; import { ServerMethodFunction, ServerMethodName, @@ -18,11 +18,11 @@ type ServerContextValue = { methodName: MethodName, ...args: ServerMethodParameters ) => Promise>; - callEndpoint: >( - method: M, - path: P, - params: Serialized[0]>, - ) => Promise>>; + callEndpoint: >( + method: TMethod, + path: TPath, + params: Serialized[0]>, + ) => Promise>>; uploadToEndpoint: (endpoint: string, params: any, formData: any) => Promise; getStream: ( streamName: string, @@ -70,10 +70,12 @@ export const useMethod = ( ); }; -export const useEndpoint = >( - method: M, - path: P, -): ((params: Serialized[0]>) => Promise>>) => { +export const useEndpoint = >( + method: TMethod, + path: TPath, +): (( + params: Serialized[0]>, +) => Promise>>) => { const { callEndpoint } = useContext(ServerContext); return useCallback((params) => callEndpoint(method, path, params), [callEndpoint, path, method]); diff --git a/client/hooks/useEndpointAction.ts b/client/hooks/useEndpointAction.ts index f8710047b94a9..45de670e9dc51 100644 --- a/client/hooks/useEndpointAction.ts +++ b/client/hooks/useEndpointAction.ts @@ -5,12 +5,12 @@ import { Method, Params, PathFor, Return } from '../../definition/rest'; import { useEndpoint } from '../contexts/ServerContext'; import { useToastMessageDispatch } from '../contexts/ToastMessagesContext'; -export const useEndpointAction = >( - method: M, - path: P, - params: Serialized[0]> = {} as Serialized[0]>, +export const useEndpointAction = >( + method: TMethod, + path: TPath, + params: Serialized[0]> = {} as Serialized[0]>, successMessage?: string, -): ((extraParams?: Params[1]) => Promise>>) => { +): ((extraParams?: Params[1]) => Promise>>) => { const sendData = useEndpoint(method, path); const dispatchToastMessage = useToastMessageDispatch(); diff --git a/client/hooks/useEndpointActionExperimental.ts b/client/hooks/useEndpointActionExperimental.ts index 62c8e2ef28bb0..0469b1e40356a 100644 --- a/client/hooks/useEndpointActionExperimental.ts +++ b/client/hooks/useEndpointActionExperimental.ts @@ -5,11 +5,16 @@ import { Method, Params, PathFor, Return } from '../../definition/rest'; import { useEndpoint } from '../contexts/ServerContext'; import { useToastMessageDispatch } from '../contexts/ToastMessagesContext'; -export const useEndpointActionExperimental = >( - method: M, - path: P, +export const useEndpointActionExperimental = < + TMethod extends Method, + TPath extends PathFor, +>( + method: TMethod, + path: TPath, successMessage?: string, -): ((params: Serialized[0]>) => Promise>>) => { +): (( + params: Serialized[0]>, +) => Promise>>) => { const sendData = useEndpoint(method, path); const dispatchToastMessage = useToastMessageDispatch(); diff --git a/definition/rest/helpers/ReplacePlaceholders.ts b/definition/rest/helpers/ReplacePlaceholders.ts new file mode 100644 index 0000000000000..d8503b4b8471f --- /dev/null +++ b/definition/rest/helpers/ReplacePlaceholders.ts @@ -0,0 +1,8 @@ +export type ReplacePlaceholders = + string extends TPath + ? TPath + : TPath extends `${ infer Start }:${ infer _Param }/${ infer Rest }` + ? `${ Start }${ string }/${ ReplacePlaceholders }` + : TPath extends `${ infer Start }:${ infer _Param }` + ? `${ Start }${ string }` + : TPath; diff --git a/definition/rest/index.ts b/definition/rest/index.ts index b7bad4b5ffdae..758c0afd35a8a 100644 --- a/definition/rest/index.ts +++ b/definition/rest/index.ts @@ -1,7 +1,8 @@ import type { EnterpriseEndpoints } from '../../ee/definition/rest'; import type { ExtractKeys, ValueOf } from '../utils'; import type { AppsEndpoints } from './apps'; -import { BannersEndpoints } from './v1/banners'; +import type { ReplacePlaceholders } from './helpers/ReplacePlaceholders'; +import type { BannersEndpoints } from './v1/banners'; import type { ChannelsEndpoints } from './v1/channels'; import type { ChatEndpoints } from './v1/chat'; import type { CloudEndpoints } from './v1/cloud'; @@ -11,20 +12,21 @@ import type { DnsEndpoints } from './v1/dns'; import type { EmojiCustomEndpoints } from './v1/emojiCustom'; import type { GroupsEndpoints } from './v1/groups'; import type { ImEndpoints } from './v1/im'; -import { InstancesEndpoints } from './v1/instances'; +import type { InstancesEndpoints } from './v1/instances'; import type { LDAPEndpoints } from './v1/ldap'; import type { LicensesEndpoints } from './v1/licenses'; import type { MiscEndpoints } from './v1/misc'; import type { OmnichannelEndpoints } from './v1/omnichannel'; -import { PermissionsEndpoints } from './v1/permissions'; -import { RolesEndpoints } from './v1/roles'; +import type { PermissionsEndpoints } from './v1/permissions'; +import type { RolesEndpoints } from './v1/roles'; import type { RoomsEndpoints } from './v1/rooms'; -import { SettingsEndpoints } from './v1/settings'; +import type { SettingsEndpoints } from './v1/settings'; import type { StatisticsEndpoints } from './v1/statistics'; import type { TeamsEndpoints } from './v1/teams'; import type { UsersEndpoints } from './v1/users'; -type CommunityEndpoints = BannersEndpoints & ChatEndpoints & +type CommunityEndpoints = BannersEndpoints & +ChatEndpoints & ChannelsEndpoints & CloudEndpoints & CustomUserStatusEndpoints & @@ -53,17 +55,17 @@ type Endpoint = UnionizeEndpoints; type UnionizeEndpoints = ValueOf< { - [P in keyof EE]: UnionizeMethods; + [P in keyof EE as P extends string ? P : never]: UnionizeMethods

; } >; type ExtractOperations = ExtractKeys any>; -type UnionizeMethods = ValueOf< +type UnionizeMethods

= ValueOf< { [M in keyof OO as ExtractOperations]: ( method: M, - path: OO extends { path: string } ? OO['path'] : P, + path: ReplacePlaceholders

, ...params: Parameters any>> ) => ReturnType any>>; } @@ -95,3 +97,41 @@ export type Params> = ExtractParams< Parameters> >; export type Return> = ReturnType>; + + +export type PathPattern = keyof Endpoints; + +export type JoinPathPattern = Extract< +PathPattern, +`${ TBasePath }/${ TSubPathPattern }` | TSubPathPattern +>; + +type GetParams = TOperation extends (...args: any) => any + ? Parameters[0] + : never + +type GetResult = TOperation extends (...args: any) => any + ? ReturnType + : never + +export type OperationParams = + TMethod extends keyof Endpoints[TPathPattern] + ? GetParams + : never; + +export type OperationResult = + TMethod extends keyof Endpoints[TPathPattern] + ? GetResult + : never; + +export type UrlParams = string extends T + ? Record + : T extends `${ infer _Start }:${ infer Param }/${ infer Rest }` + ? { [k in Param | keyof UrlParams]: string } + : T extends `${ infer _Start }:${ infer Param }` + ? { [k in Param]: string } + : {}; + +export type MethodOf = TPathPattern extends any + ? keyof Endpoints[TPathPattern] + : never; diff --git a/definition/rest/v1/banners.ts b/definition/rest/v1/banners.ts index e448abb7b410f..7b667a4f26e9c 100644 --- a/definition/rest/v1/banners.ts +++ b/definition/rest/v1/banners.ts @@ -1,21 +1,21 @@ -import { IBanner } from '../../IBanner'; +import type { BannerPlatform, IBanner } from '../../IBanner'; export type BannersEndpoints = { /* @deprecated */ 'banners.getNew': { - GET: () => ({ + GET: (params: { platform: BannerPlatform; bid: IBanner['_id'] }) => ({ banners: IBanner[]; }); }; 'banners/:id': { - GET: (params: { platform: string }) => ({ + GET: (params: { platform: BannerPlatform }) => ({ banners: IBanner[]; }); }; 'banners': { - GET: () => ({ + GET: (params: { platform: BannerPlatform }) => ({ banners: IBanner[]; }); }; diff --git a/definition/rest/v1/omnichannel.ts b/definition/rest/v1/omnichannel.ts index 1598355cb5bac..21e4250ac9491 100644 --- a/definition/rest/v1/omnichannel.ts +++ b/definition/rest/v1/omnichannel.ts @@ -49,7 +49,6 @@ export type OmnichannelEndpoints = { }; }; 'livechat/department/:_id': { - path: `livechat/department/${ string }`; GET: () => { department: ILivechatDepartment; }; @@ -138,7 +137,6 @@ export type OmnichannelEndpoints = { DELETE: (params: { _id: IOmnichannelCannedResponse['_id'] }) => void; }; 'canned-responses/:_id': { - path: `canned-responses/${ string }`; GET: () => { cannedResponse: IOmnichannelCannedResponse; }; diff --git a/definition/utils.ts b/definition/utils.ts index 72339afa798a0..211bcd5234dea 100644 --- a/definition/utils.ts +++ b/definition/utils.ts @@ -9,3 +9,6 @@ export type UnionToIntersection = (T extends any ? (x: T) => void : never) ex : never; export type Awaited = T extends PromiseLike ? Awaited : T; + +// `T extends any` is a trick to apply a operator to each member of a union +export type KeyOfEach = T extends any ? keyof T : never; diff --git a/ee/definition/rest/v1/omnichannel/businessHours.ts b/ee/definition/rest/v1/omnichannel/businessHours.ts index 77b352c612599..1211ccdb9de05 100644 --- a/ee/definition/rest/v1/omnichannel/businessHours.ts +++ b/ee/definition/rest/v1/omnichannel/businessHours.ts @@ -2,6 +2,16 @@ import { ILivechatBusinessHour } from '../../../../../definition/ILivechatBusine export type OmnichannelBusinessHoursEndpoints = { 'livechat/business-hours.list': { - GET: () => ({ businessHours: ILivechatBusinessHour[] }); + GET: (params: { + name?: string; + offset: number; + count: number; + sort: Record; + }) => { + businessHours: ILivechatBusinessHour[]; + count: number; + offset: number; + total: number; + }; }; } From eed68f394d0f3f49750a2589e72b20b3649f2086 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Thu, 18 Nov 2021 01:28:25 -0300 Subject: [PATCH 03/10] Use assertions on `banners` endpoints --- app/api/server/v1/banners.ts | 31 ++---------------------------- tests/end-to-end/api/21-banners.js | 7 ------- 2 files changed, 2 insertions(+), 36 deletions(-) diff --git a/app/api/server/v1/banners.ts b/app/api/server/v1/banners.ts index 8d1944933da8a..790dbd138afaa 100644 --- a/app/api/server/v1/banners.ts +++ b/app/api/server/v1/banners.ts @@ -1,4 +1,3 @@ -import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { API } from '../api'; @@ -59,13 +58,6 @@ API.v1.addRoute('banners.getNew', { authRequired: true }, { // deprecated })); const { platform, bid: bannerId } = this.queryParams; - if (!platform) { - throw new Meteor.Error('error-missing-param', 'The required "platform" param is missing.'); - } - - if (!Object.values(BannerPlatform).includes(platform)) { - throw new Meteor.Error('error-unknown-platform', 'Platform is unknown.'); - } const banners = await Banner.getBannersForUser(this.userId, platform, bannerId ?? undefined); @@ -122,7 +114,7 @@ API.v1.addRoute('banners.getNew', { authRequired: true }, { // deprecated API.v1.addRoute('banners/:id', { authRequired: true }, { // TODO: move to users/:id/banners async get() { check(this.urlParams, Match.ObjectIncluding({ - id: String, + id: Match.Where((id: unknown): id is string => typeof id === 'string' && Boolean(id.trim())), })); check(this.queryParams, Match.ObjectIncluding({ @@ -130,15 +122,7 @@ API.v1.addRoute('banners/:id', { authRequired: true }, { // TODO: move to users/ })); const { platform } = this.queryParams; - - if (!platform) { - throw new Meteor.Error('error-missing-param', 'The required "platform" param is missing.'); - } - const { id } = this.urlParams; - if (!id) { - throw new Meteor.Error('error-missing-param', 'The required "id" param is missing.'); - } const banners = await Banner.getBannersForUser(this.userId, platform, id); @@ -190,13 +174,6 @@ API.v1.addRoute('banners', { authRequired: true }, { })); const { platform } = this.queryParams; - if (!platform) { - throw new Meteor.Error('error-missing-param', 'The required "platform" param is missing.'); - } - - if (!Object.values(BannerPlatform).includes(platform)) { - throw new Meteor.Error('error-unknown-platform', 'Platform is unknown.'); - } const banners = await Banner.getBannersForUser(this.userId, platform); @@ -240,15 +217,11 @@ API.v1.addRoute('banners', { authRequired: true }, { API.v1.addRoute('banners.dismiss', { authRequired: true }, { async post() { check(this.bodyParams, Match.ObjectIncluding({ - bannerId: String, + bannerId: Match.Where((id: unknown): id is string => typeof id === 'string' && Boolean(id.trim())), })); const { bannerId } = this.bodyParams; - if (!bannerId || !bannerId.trim()) { - throw new Meteor.Error('error-missing-param', 'The required "bannerId" param is missing.'); - } - await Banner.dismiss(this.userId, bannerId); return API.v1.success(); }, diff --git a/tests/end-to-end/api/21-banners.js b/tests/end-to-end/api/21-banners.js index 6d31095aced43..ede0cccc737a8 100644 --- a/tests/end-to-end/api/21-banners.js +++ b/tests/end-to-end/api/21-banners.js @@ -27,7 +27,6 @@ describe('banners', function() { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('error', 'Match error: Missing key \'platform\''); }) .end(done); }); @@ -41,8 +40,6 @@ describe('banners', function() { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('error', 'Platform is unknown. [error-unknown-platform]'); - expect(res.body).to.have.property('errorType', 'error-unknown-platform'); }) .end(done); }); @@ -56,8 +53,6 @@ describe('banners', function() { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('error', 'The required "platform" param is missing. [error-missing-param]'); - expect(res.body).to.have.property('errorType', 'error-missing-param'); }) .end(done); }); @@ -112,8 +107,6 @@ describe('banners', function() { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('error', 'The required "bannerId" param is missing. [error-missing-param]'); - expect(res.body).to.have.property('errorType', 'error-missing-param'); }) .end(done); }); From 5a13aeca8ea273ebb982b81d8b4b4cae8ecb7e59 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Thu, 18 Nov 2021 02:36:35 -0300 Subject: [PATCH 04/10] Improve types for `roles` endpoints --- app/api/server/api.d.ts | 26 +++++++-------- app/api/server/v1/roles.ts | 45 ++++++++++++-------------- definition/externals/meteor/check.d.ts | 7 ++++ definition/rest/v1/roles.ts | 5 +++ 4 files changed, 45 insertions(+), 38 deletions(-) create mode 100644 definition/externals/meteor/check.d.ts diff --git a/app/api/server/api.d.ts b/app/api/server/api.d.ts index 4c1d395b9dd07..1edac24cae4cb 100644 --- a/app/api/server/api.d.ts +++ b/app/api/server/api.d.ts @@ -97,10 +97,10 @@ declare class APIClass { TSubPathPattern extends string >(subpath: TSubPathPattern, operations: Operations>): void; - // addRoute< - // TSubPathPattern extends string, - // TPathPattern extends JoinPathPattern - // >(subpaths: TSubPathPattern[], operations: Operations): void; + addRoute< + TSubPathPattern extends string, + TPathPattern extends JoinPathPattern + >(subpaths: TSubPathPattern[], operations: Operations): void; addRoute< TSubPathPattern extends string, @@ -111,15 +111,15 @@ declare class APIClass { operations: Operations, TOptions> ): void; - // addRoute< - // TSubPathPattern extends string, - // TPathPattern extends JoinPathPattern, - // TOptions extends Options - // >( - // subpaths: TSubPathPattern[], - // options: TOptions, - // operations: Operations - // ): void; + addRoute< + TSubPathPattern extends string, + TPathPattern extends JoinPathPattern, + TOptions extends Options + >( + subpaths: TSubPathPattern[], + options: TOptions, + operations: Operations + ): void; success(result: T): SuccessResult; diff --git a/app/api/server/v1/roles.ts b/app/api/server/v1/roles.ts index 5de4348624eb1..ef92c3547c2d5 100644 --- a/app/api/server/v1/roles.ts +++ b/app/api/server/v1/roles.ts @@ -1,4 +1,5 @@ import { Meteor } from 'meteor/meteor'; +import { check, Match } from 'meteor/check'; import { Users } from '../../../models/server'; import { API } from '../api'; @@ -20,11 +21,11 @@ API.v1.addRoute('roles.list', { authRequired: true }, { API.v1.addRoute('roles.sync', { authRequired: true }, { async get() { - const { updatedSince } = this.queryParams as any; // TODO + check(this.queryParams, Match.ObjectIncluding({ + updatedSince: Match.Where((value: unknown): value is string => typeof value === 'string' && !Number.isNaN(Date.parse(value))), + })); - if (isNaN(Date.parse(updatedSince))) { - throw new Meteor.Error('error-updatedSince-param-invalid', 'The "updatedSince" query parameter must be a valid date.'); - } + const { updatedSince } = this.queryParams; return API.v1.success({ roles: { @@ -41,25 +42,23 @@ API.v1.addRoute('roles.create', { authRequired: true }, { throw new Meteor.Error('error-invalid-role-properties', 'The role properties are invalid.'); } - const roleData = { - name: this.bodyParams.name, - scope: this.bodyParams.scope, - description: this.bodyParams.description, - mandatory2fa: this.bodyParams.mandatory2fa, - }; + const { name, scope, description, mandatory2fa } = this.bodyParams; if (!await hasPermissionAsync(Meteor.userId(), 'access-permissions')) { throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed'); } - if (await Roles.findOneByIdOrName(roleData.name)) { + if (await Roles.findOneByIdOrName(name)) { throw new Meteor.Error('error-duplicate-role-names-not-allowed', 'Role name already exists'); } - if (['Users', 'Subscriptions'].includes(roleData.scope as string) === false) { // TODO - roleData.scope = 'Users'; - } - const roleId = (await Roles.createWithRandomId(roleData.name, roleData.scope, roleData.description, false, roleData.mandatory2fa)).insertedId; + const roleId = (await Roles.createWithRandomId( + name, + scope && ['Users', 'Subscriptions'].includes(scope) ? scope : 'Users', + description, + false, + mandatory2fa, + )).insertedId; if (settings.get('UI_DisplayRoles')) { api.broadcast('user.roleUpdate', { @@ -236,29 +235,25 @@ API.v1.addRoute('roles.removeUserFromRole', { authRequired: true }, { throw new Meteor.Error('error-invalid-role-properties', 'The role properties are invalid.'); } - const data = { - roleName: bodyParams.roleName, - username: bodyParams.username, - scope: (bodyParams as any).scope, // TODO - }; + const { roleName, username, scope } = bodyParams; if (!await hasPermissionAsync(this.userId, 'access-permissions')) { throw new Meteor.Error('error-not-allowed', 'Accessing permissions is not allowed'); } - const user = Users.findOneByUsername(data.username); + const user = Users.findOneByUsername(username); if (!user) { throw new Meteor.Error('error-invalid-user', 'There is no user with this username'); } - const role = await Roles.findOneByIdOrName(data.roleName); + const role = await Roles.findOneByIdOrName(roleName); if (!role) { throw new Meteor.Error('error-invalid-roleId', 'This role does not exist'); } - if (!await hasRoleAsync(user._id, role.name, data.scope)) { + if (!await hasRoleAsync(user._id, role.name, scope)) { throw new Meteor.Error('error-user-not-in-role', 'User is not in this role'); } @@ -269,7 +264,7 @@ API.v1.addRoute('roles.removeUserFromRole', { authRequired: true }, { } } - await Roles.removeUserRoles(user._id, [role.name], data.scope); + await Roles.removeUserRoles(user._id, [role.name], scope); if (settings.get('UI_DisplayRoles')) { api.broadcast('user.roleUpdate', { @@ -279,7 +274,7 @@ API.v1.addRoute('roles.removeUserFromRole', { authRequired: true }, { _id: user._id, username: user.username, }, - scope: data.scope, + scope, }); } diff --git a/definition/externals/meteor/check.d.ts b/definition/externals/meteor/check.d.ts new file mode 100644 index 0000000000000..401b56c831fd3 --- /dev/null +++ b/definition/externals/meteor/check.d.ts @@ -0,0 +1,7 @@ +import 'meteor/check'; + +declare module 'meteor/check' { + namespace Match { + function Where(condition: (val: T) => val is U): Matcher; + } +} diff --git a/definition/rest/v1/roles.ts b/definition/rest/v1/roles.ts index 844d125083b3d..bccc9a37e1d10 100644 --- a/definition/rest/v1/roles.ts +++ b/definition/rest/v1/roles.ts @@ -109,6 +109,7 @@ type RoleRemoveUserFromRoleProps = { username: string; roleName: string; roomId?: string; + scope?: string; } const roleRemoveUserFromRolePropsSchema: JSONSchemaType = { @@ -124,6 +125,10 @@ const roleRemoveUserFromRolePropsSchema: JSONSchemaType Date: Thu, 18 Nov 2021 20:14:45 -0300 Subject: [PATCH 05/10] Improve type guards for `teams` endpoints --- app/api/server/v1/teams.ts | 151 +++++++++++++++++++++--------- definition/rest/v1/teams/index.ts | 28 +++--- server/sdk/types/ITeamService.ts | 2 +- 3 files changed, 122 insertions(+), 59 deletions(-) diff --git a/app/api/server/v1/teams.ts b/app/api/server/v1/teams.ts index 2b8eab31d8035..97d39d19a2df3 100644 --- a/app/api/server/v1/teams.ts +++ b/app/api/server/v1/teams.ts @@ -18,6 +18,7 @@ import { isTeamsAddMembersProps } from '../../../../definition/rest/v1/teams/Tea import { isTeamsDeleteProps } from '../../../../definition/rest/v1/teams/TeamsDeleteProps'; import { isTeamsLeaveProps } from '../../../../definition/rest/v1/teams/TeamsLeaveProps'; import { isTeamsUpdateProps } from '../../../../definition/rest/v1/teams/TeamsUpdateProps'; +import { ITeam, TEAM_TYPE } from '../../../../definition/ITeam'; API.v1.addRoute('teams.list', { authRequired: true }, { async get() { @@ -59,11 +60,16 @@ API.v1.addRoute('teams.create', { authRequired: true }, { if (!hasPermission(this.userId, 'create-team')) { return API.v1.unauthorized(); } - const { name, type, members, room, owner } = this.bodyParams as any; // TODO - if (!name) { - return API.v1.failure('Body param "name" is required'); - } + check(this.bodyParams, Match.ObjectIncluding({ + name: String, + type: Match.OneOf(TEAM_TYPE.PRIVATE, TEAM_TYPE.PUBLIC), + members: Match.Maybe([String]), + room: Match.Any, + owner: Match.Maybe(String), + })); + + const { name, type, members, room, owner } = this.bodyParams; const team = await Team.create(this.userId, { team: { @@ -120,15 +126,34 @@ API.v1.addRoute('teams.convertToChannel', { authRequired: true }, { }, }); +const getTeamByIdOrName = async (params: { teamId: string } | { teamName: string }): Promise => { + if ('teamId' in params && params.teamId) { + return Team.getOneById(params.teamId); + } + + if ('teamName' in params && params.teamName) { + return Team.getOneByName(params.teamName); + } + + return null; +}; + API.v1.addRoute('teams.addRooms', { authRequired: true }, { async post() { - const { rooms, teamId, teamName } = this.bodyParams as any; // TODO - - if (!teamId && !teamName) { - return API.v1.failure('missing-teamId-or-teamName'); - } + check(this.bodyParams, Match.OneOf( + Match.ObjectIncluding({ + teamId: String, + }), + Match.ObjectIncluding({ + teamName: String, + }), + )); + + check(this.bodyParams, Match.ObjectIncluding({ + rooms: [String], + })); - const team = await (teamId ? Team.getOneById(teamId) : Team.getOneByName(teamName)); + const team = await getTeamByIdOrName(this.bodyParams); if (!team) { return API.v1.failure('team-does-not-exist'); } @@ -137,6 +162,8 @@ API.v1.addRoute('teams.addRooms', { authRequired: true }, { return API.v1.unauthorized('error-no-permission-team-channel'); } + const { rooms } = this.bodyParams; + const validRooms = await Team.addRooms(this.userId, rooms, team._id); return API.v1.success({ rooms: validRooms }); @@ -148,9 +175,8 @@ API.v1.addRoute('teams.removeRoom', { authRequired: true }, { if (!isTeamsRemoveRoomProps(this.bodyParams)) { return API.v1.failure('body-params-invalid', isTeamsRemoveRoomProps.errors?.map((error) => error.message).join('\n ')); } - const { roomId, teamId, teamName } = this.bodyParams as any; // TODO - const team = await (teamId ? Team.getOneById(teamId) : Team.getOneByName(teamName)); + const team = await getTeamByIdOrName(this.bodyParams); if (!team) { return API.v1.failure('team-does-not-exist'); } @@ -161,6 +187,8 @@ API.v1.addRoute('teams.removeRoom', { authRequired: true }, { const canRemoveAny = !!hasPermission(this.userId, 'view-all-team-channels', team.roomId); + const { roomId } = this.bodyParams; + const room = await Team.removeRoom(this.userId, roomId, team._id, canRemoveAny); return API.v1.success({ room }); @@ -169,7 +197,12 @@ API.v1.addRoute('teams.removeRoom', { authRequired: true }, { API.v1.addRoute('teams.updateRoom', { authRequired: true }, { async post() { - const { roomId, isDefault } = this.bodyParams as any; // TODO + check(this.bodyParams, Match.ObjectIncluding({ + roomId: String, + isDefault: Boolean, + })); + + const { roomId, isDefault } = this.bodyParams; const team = await Team.getOneByRoomId(roomId); if (!team) { @@ -189,15 +222,29 @@ API.v1.addRoute('teams.updateRoom', { authRequired: true }, { API.v1.addRoute('teams.listRooms', { authRequired: true }, { async get() { - const { teamId, teamName, filter, type } = this.queryParams as any; // TODO + check(this.queryParams, Match.OneOf( + Match.ObjectIncluding({ + teamId: String, + }), + Match.ObjectIncluding({ + teamName: String, + }), + )); + + check(this.queryParams, Match.ObjectIncluding({ + filter: Match.Maybe(String), + type: Match.Maybe(String), + })); + + const { filter, type } = this.queryParams; const { offset, count } = this.getPaginationItems(); - const team = await (teamId ? Team.getOneById(teamId) : Team.getOneByName(teamName)); + const team = await getTeamByIdOrName(this.queryParams); if (!team) { return API.v1.failure('team-does-not-exist'); } - const allowPrivateTeam = hasPermission(this.userId, 'view-all-teams', team.roomId); + const allowPrivateTeam: boolean = hasPermission(this.userId, 'view-all-teams', team.roomId); let getAllRooms = false; if (hasPermission(this.userId, 'view-all-team-channels', team.roomId)) { @@ -205,7 +252,7 @@ API.v1.addRoute('teams.listRooms', { authRequired: true }, { } const listFilter = { - name: filter, + name: filter ?? undefined, isDefault: type === 'autoJoin', getAllRooms, allowPrivateTeam, @@ -224,27 +271,36 @@ API.v1.addRoute('teams.listRooms', { authRequired: true }, { API.v1.addRoute('teams.listRoomsOfUser', { authRequired: true }, { async get() { - const { offset, count } = this.getPaginationItems(); - const { teamId, teamName, userId, canUserDelete = false } = this.queryParams as any; // TODO - + check(this.queryParams, Match.OneOf( + Match.ObjectIncluding({ + teamId: String, + }), + Match.ObjectIncluding({ + teamName: String, + }), + )); - if (!teamId && !teamName) { - return API.v1.failure('missing-teamId-or-teamName'); - } + check(this.queryParams, Match.ObjectIncluding({ + userId: String, + canUserDelete: Match.Maybe(Boolean), + })); - const team = await (teamId ? Team.getOneById(teamId) : Team.getOneByName(teamName!)); + const { offset, count } = this.getPaginationItems(); + const team = await getTeamByIdOrName(this.queryParams); if (!team) { return API.v1.failure('team-does-not-exist'); } const allowPrivateTeam = hasPermission(this.userId, 'view-all-teams', team.roomId); + const { userId, canUserDelete } = this.queryParams; + if (!(this.userId === userId || hasPermission(this.userId, 'view-all-team-channels', team.roomId))) { return API.v1.unauthorized(); } - const { records, total } = await Team.listRoomsOfUser(this.userId, team._id, userId, allowPrivateTeam, canUserDelete, { offset, count }); + const { records, total } = await Team.listRoomsOfUser(this.userId, team._id, userId, allowPrivateTeam, canUserDelete ?? false, { offset, count }); return API.v1.success({ rooms: records, @@ -259,23 +315,28 @@ API.v1.addRoute('teams.members', { authRequired: true }, { async get() { const { offset, count } = this.getPaginationItems(); + check(this.queryParams, Match.OneOf( + Match.ObjectIncluding({ + teamId: String, + }), + Match.ObjectIncluding({ + teamName: String, + }), + )); + check(this.queryParams, Match.ObjectIncluding({ - teamId: Match.Maybe(String), - teamName: Match.Maybe(String), status: Match.Maybe([String]), username: Match.Maybe(String), name: Match.Maybe(String), })); - const { teamId, teamName, status, username, name } = this.queryParams as any; // TODO - if (!teamId && !teamName) { - return API.v1.failure('missing-teamId-or-teamName'); - } + const { status, username, name } = this.queryParams; - const team = await (teamId ? Team.getOneById(teamId) : Team.getOneByName(teamName)); + const team = await getTeamByIdOrName(this.queryParams); if (!team) { return API.v1.failure('team-does-not-exist'); } + const canSeeAllMembers = hasPermission(this.userId, 'view-all-teams', team.roomId); const query = { @@ -431,16 +492,16 @@ API.v1.addRoute('teams.leave', { authRequired: true }, { API.v1.addRoute('teams.info', { authRequired: true }, { async get() { - const { teamId, teamName } = this.queryParams as any; // TODO - - if (!teamId && !teamName) { - return API.v1.failure('Provide either the "teamId" or "teamName"'); - } - - const teamInfo = await (teamId - ? Team.getInfoById(teamId) - : Team.getInfoByName(teamName)); - + check(this.queryParams, Match.OneOf( + Match.ObjectIncluding({ + teamId: String, + }), + Match.ObjectIncluding({ + teamName: String, + }), + )); + + const teamInfo = await getTeamByIdOrName(this.queryParams); if (!teamInfo) { return API.v1.failure('Team not found'); } @@ -498,7 +559,11 @@ API.v1.addRoute('teams.delete', { authRequired: true }, { API.v1.addRoute('teams.autocomplete', { authRequired: true }, { async get() { - const { name } = this.queryParams as any; // TODO + check(this.queryParams, Match.ObjectIncluding({ + name: String, + })); + + const { name } = this.queryParams; const teams = await Team.autocomplete(this.userId, name); diff --git a/definition/rest/v1/teams/index.ts b/definition/rest/v1/teams/index.ts index 72694da7ea0c2..1b981937f1686 100644 --- a/definition/rest/v1/teams/index.ts +++ b/definition/rest/v1/teams/index.ts @@ -1,19 +1,17 @@ - - import type { IRoom } from '../../../IRoom'; import type { ITeam } from '../../../ITeam'; import type { IUser } from '../../../IUser'; -import { PaginatedResult } from '../../helpers/PaginatedResult'; -import { PaginatedRequest } from '../../helpers/PaginatedRequest'; -import { ITeamAutocompleteResult, ITeamMemberInfo } from '../../../../server/sdk/types/ITeamService'; -import { TeamsRemoveRoomProps } from './TeamsRemoveRoomProps'; -import { TeamsConvertToChannelProps } from './TeamsConvertToChannelProps'; -import { TeamsUpdateMemberProps } from './TeamsUpdateMemberProps'; -import { TeamsAddMembersProps } from './TeamsAddMembersProps'; -import { TeamsRemoveMemberProps } from './TeamsRemoveMemberProps'; -import { TeamsDeleteProps } from './TeamsDeleteProps'; -import { TeamsLeaveProps } from './TeamsLeaveProps'; -import { TeamsUpdateProps } from './TeamsUpdateProps'; +import type { PaginatedResult } from '../../helpers/PaginatedResult'; +import type { PaginatedRequest } from '../../helpers/PaginatedRequest'; +import type { ITeamAutocompleteResult, ITeamMemberInfo } from '../../../../server/sdk/types/ITeamService'; +import type { TeamsRemoveRoomProps } from './TeamsRemoveRoomProps'; +import type { TeamsConvertToChannelProps } from './TeamsConvertToChannelProps'; +import type { TeamsUpdateMemberProps } from './TeamsUpdateMemberProps'; +import type { TeamsAddMembersProps } from './TeamsAddMembersProps'; +import type { TeamsRemoveMemberProps } from './TeamsRemoveMemberProps'; +import type { TeamsDeleteProps } from './TeamsDeleteProps'; +import type { TeamsLeaveProps } from './TeamsLeaveProps'; +import type { TeamsUpdateProps } from './TeamsUpdateProps'; type TeamProps = @@ -40,7 +38,7 @@ export type TeamsEndpoints = { 'teams.create': { POST: (params: { name: ITeam['name']; - type?: ITeam['type']; + type: ITeam['type']; members?: IUser['_id'][]; room: { id?: string; @@ -138,7 +136,7 @@ export type TeamsEndpoints = { }; 'teams.listRooms': { - GET: (params: PaginatedRequest & ({ teamId: string } | { teamId: string }) & { filter?: string; type?: string }) => PaginatedResult & { rooms: IRoom[] }; + GET: (params: PaginatedRequest & ({ teamId: string } | { teamName: string }) & { filter?: string; type?: string }) => PaginatedResult & { rooms: IRoom[] }; }; diff --git a/server/sdk/types/ITeamService.ts b/server/sdk/types/ITeamService.ts index 697a67e406418..1d464759397ab 100644 --- a/server/sdk/types/ITeamService.ts +++ b/server/sdk/types/ITeamService.ts @@ -41,7 +41,7 @@ export interface ITeamInfo extends ITeam { } export interface IListRoomsFilter { - name: string; + name?: string; isDefault: boolean; getAllRooms: boolean; allowPrivateTeam: boolean; From 0c3cde5cf078516d725549f0f608789546652b20 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Thu, 18 Nov 2021 20:34:08 -0300 Subject: [PATCH 06/10] Remove confusing type alias --- app/models/server/raw/Sessions.ts | 56 +++++++++++++++---------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/app/models/server/raw/Sessions.ts b/app/models/server/raw/Sessions.ts index d79fd32c89133..dbd6080f18b6f 100644 --- a/app/models/server/raw/Sessions.ts +++ b/app/models/server/raw/Sessions.ts @@ -1,14 +1,14 @@ import { AggregationCursor, BulkWriteOperation, BulkWriteOpResultObject, Collection, IndexSpecification, UpdateWriteOpResult, FilterQuery } from 'mongodb'; -import { ISession as T } from '../../../../definition/ISession'; +import { ISession } from '../../../../definition/ISession'; import { BaseRaw, ModelOptionalId } from './BaseRaw'; type DestructuredDate = {year: number; month: number; day: number}; type DestructuredDateWithType = {year: number; month: number; day: number; type?: 'month' | 'week'}; type DestructuredRange = {start: DestructuredDate; end: DestructuredDate}; -type FullReturn = { year: number; month: number; day: number; data: T[] }; +type FullReturn = { year: number; month: number; day: number; data: ISession[] }; -const matchBasedOnDate = (start: DestructuredDate, end: DestructuredDate): FilterQuery => { +const matchBasedOnDate = (start: DestructuredDate, end: DestructuredDate): FilterQuery => { if (start.year === end.year && start.month === end.month) { return { year: start.year, @@ -112,16 +112,16 @@ const getProjectionByFullDate = (): { day: string; month: string; year: string } }); export const aggregates = { - dailySessionsOfYesterday(collection: Collection, { year, month, day }: DestructuredDate): AggregationCursor & { + dailySessionsOfYesterday(collection: Collection, { year, month, day }: DestructuredDate): AggregationCursor & { time: number; sessions: number; - devices: T['device'][]; + devices: ISession['device'][]; _computedAt: string; }> { - return collection.aggregate & { + return collection.aggregate & { time: number; sessions: number; - devices: T['device'][]; + devices: ISession['device'][]; _computedAt: string; }>([{ $match: { @@ -211,7 +211,7 @@ export const aggregates = { }], { allowDiskUse: true }); }, - async getUniqueUsersOfYesterday(collection: Collection, { year, month, day }: DestructuredDate): Promise { + async getUniqueUsersOfYesterday(collection: Collection, { year, month, day }: DestructuredDate): Promise { return collection.aggregate([{ $match: { year, @@ -273,7 +273,7 @@ export const aggregates = { }]).toArray(); }, - async getUniqueUsersOfLastMonthOrWeek(collection: Collection, { year, month, day, type = 'month' }: DestructuredDateWithType): Promise { + async getUniqueUsersOfLastMonthOrWeek(collection: Collection, { year, month, day, type = 'month' }: DestructuredDateWithType): Promise { return collection.aggregate([{ $match: { type: 'user_daily', @@ -343,7 +343,7 @@ export const aggregates = { }], { allowDiskUse: true }).toArray(); }, - getMatchOfLastMonthOrWeek({ year, month, day, type = 'month' }: DestructuredDateWithType): FilterQuery { + getMatchOfLastMonthOrWeek({ year, month, day, type = 'month' }: DestructuredDateWithType): FilterQuery { let startOfPeriod; if (type === 'month') { @@ -418,7 +418,7 @@ export const aggregates = { }; }, - async getUniqueDevicesOfLastMonthOrWeek(collection: Collection, { year, month, day, type = 'month' }: DestructuredDateWithType): Promise { + async getUniqueDevicesOfLastMonthOrWeek(collection: Collection, { year, month, day, type = 'month' }: DestructuredDateWithType): Promise { return collection.aggregate([{ $match: { type: 'user_daily', @@ -456,7 +456,7 @@ export const aggregates = { }], { allowDiskUse: true }).toArray(); }, - getUniqueDevicesOfYesterday(collection: Collection, { year, month, day }: DestructuredDate): Promise { + getUniqueDevicesOfYesterday(collection: Collection, { year, month, day }: DestructuredDate): Promise { return collection.aggregate([{ $match: { year, @@ -496,7 +496,7 @@ export const aggregates = { }]).toArray(); }, - getUniqueOSOfLastMonthOrWeek(collection: Collection, { year, month, day, type = 'month' }: DestructuredDateWithType): Promise { + getUniqueOSOfLastMonthOrWeek(collection: Collection, { year, month, day, type = 'month' }: DestructuredDateWithType): Promise { return collection.aggregate([{ $match: { type: 'user_daily', @@ -535,7 +535,7 @@ export const aggregates = { }], { allowDiskUse: true }).toArray(); }, - getUniqueOSOfYesterday(collection: Collection, { year, month, day }: DestructuredDate): Promise { + getUniqueOSOfYesterday(collection: Collection, { year, month, day }: DestructuredDate): Promise { return collection.aggregate([{ $match: { year, @@ -577,7 +577,7 @@ export const aggregates = { }, }; -export class SessionsRaw extends BaseRaw { +export class SessionsRaw extends BaseRaw { protected indexes: IndexSpecification[] = [ { key: { instanceId: 1, sessionId: 1, year: 1, month: 1, day: 1 } }, { key: { instanceId: 1, sessionId: 1, userId: 1 } }, @@ -590,19 +590,19 @@ export class SessionsRaw extends BaseRaw { { key: { _computedAt: 1 }, expireAfterSeconds: 60 * 60 * 24 * 45 }, ] - private secondaryCollection: Collection; + private secondaryCollection: Collection; constructor( - public readonly col: Collection, - public readonly colSecondary: Collection, - public readonly trash?: Collection, + public readonly col: Collection, + public readonly colSecondary: Collection, + public readonly trash?: Collection, ) { super(col, trash); this.secondaryCollection = colSecondary; } - async getActiveUsersBetweenDates({ start, end }: DestructuredRange): Promise { + async getActiveUsersBetweenDates({ start, end }: DestructuredRange): Promise { return this.col.aggregate([ { $match: { @@ -618,7 +618,7 @@ export class SessionsRaw extends BaseRaw { ]).toArray(); } - async findLastLoginByIp(ip: string): Promise { + async findLastLoginByIp(ip: string): Promise { return this.findOne({ ip, }, { @@ -627,7 +627,7 @@ export class SessionsRaw extends BaseRaw { }); } - async getActiveUsersOfPeriodByDayBetweenDates({ start, end }: DestructuredRange): Promise { + async getActiveUsersOfPeriodByDayBetweenDates({ start, end }: DestructuredRange): Promise { return this.col.aggregate([ { $match: { @@ -675,7 +675,7 @@ export class SessionsRaw extends BaseRaw { ]).toArray(); } - async getBusiestTimeWithinHoursPeriod({ start, end, groupSize }: DestructuredRange & {groupSize: number}): Promise { + async getBusiestTimeWithinHoursPeriod({ start, end, groupSize }: DestructuredRange & {groupSize: number}): Promise { const match = { $match: { type: 'computed-session', @@ -709,7 +709,7 @@ export class SessionsRaw extends BaseRaw { return this.col.aggregate([match, rangeProject, unwind, groups.listGroup, groups.countGroup, presentationProject, sort]).toArray(); } - async getTotalOfSessionsByDayBetweenDates({ start, end }: DestructuredRange): Promise { + async getTotalOfSessionsByDayBetweenDates({ start, end }: DestructuredRange): Promise { return this.col.aggregate([ { $match: { @@ -739,7 +739,7 @@ export class SessionsRaw extends BaseRaw { ]).toArray(); } - async getTotalOfSessionByHourAndDayBetweenDates({ start, end }: DestructuredRange): Promise { + async getTotalOfSessionByHourAndDayBetweenDates({ start, end }: DestructuredRange): Promise { const match = { $match: { type: 'computed-session', @@ -922,7 +922,7 @@ export class SessionsRaw extends BaseRaw { }; } - async createOrUpdate(data: T): Promise { + async createOrUpdate(data: ISession): Promise { const { year, month, day, sessionId, instanceId } = data; if (!year || !month || !day || !sessionId || !instanceId) { @@ -992,12 +992,12 @@ export class SessionsRaw extends BaseRaw { return this.updateMany(query, update); } - async createBatch(sessions: ModelOptionalId[]): Promise { + async createBatch(sessions: ModelOptionalId[]): Promise { if (!sessions || sessions.length === 0) { return; } - const ops: BulkWriteOperation[] = []; + const ops: BulkWriteOperation[] = []; sessions.forEach((doc) => { const { year, month, day, sessionId, instanceId } = doc; delete doc._id; From 9152b65b689e9f58a6f9880d5ff66d511f07150c Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Fri, 19 Nov 2021 12:35:13 -0300 Subject: [PATCH 07/10] Simplify endpoints types --- app/api/server/v1/teams.ts | 2 +- .../contexts/ServerContext/ServerContext.ts | 16 +++-- client/hooks/useEndpointAction.ts | 14 +++- client/hooks/useEndpointActionExperimental.ts | 12 +++- client/hooks/useEndpointData.ts | 18 +++-- client/providers/ServerProvider.tsx | 18 +++-- definition/rest/index.ts | 71 ++++++++----------- 7 files changed, 85 insertions(+), 66 deletions(-) diff --git a/app/api/server/v1/teams.ts b/app/api/server/v1/teams.ts index 97d39d19a2df3..7cdb8120ecddc 100644 --- a/app/api/server/v1/teams.ts +++ b/app/api/server/v1/teams.ts @@ -65,7 +65,7 @@ API.v1.addRoute('teams.create', { authRequired: true }, { name: String, type: Match.OneOf(TEAM_TYPE.PRIVATE, TEAM_TYPE.PUBLIC), members: Match.Maybe([String]), - room: Match.Any, + room: Match.Maybe(Match.Any), owner: Match.Maybe(String), })); diff --git a/client/contexts/ServerContext/ServerContext.ts b/client/contexts/ServerContext/ServerContext.ts index 26e9db4660d4b..6547751986f3e 100644 --- a/client/contexts/ServerContext/ServerContext.ts +++ b/client/contexts/ServerContext/ServerContext.ts @@ -2,7 +2,13 @@ import { createContext, useCallback, useContext, useMemo } from 'react'; import type { IServerInfo } from '../../../definition/IServerInfo'; import type { Serialized } from '../../../definition/Serialized'; -import type { Params, Return, Method, PathFor } from '../../../definition/rest'; +import type { + Method, + PathFor, + OperationParams, + MatchPathPattern, + OperationResult, +} from '../../../definition/rest'; import { ServerMethodFunction, ServerMethodName, @@ -21,8 +27,8 @@ type ServerContextValue = { callEndpoint: >( method: TMethod, path: TPath, - params: Serialized[0]>, - ) => Promise>>; + params: Serialized>>, + ) => Promise>>>; uploadToEndpoint: (endpoint: string, params: any, formData: any) => Promise; getStream: ( streamName: string, @@ -74,8 +80,8 @@ export const useEndpoint = [0]>, -) => Promise>>) => { + params: Serialized>>, +) => Promise>>>) => { const { callEndpoint } = useContext(ServerContext); return useCallback((params) => callEndpoint(method, path, params), [callEndpoint, path, method]); diff --git a/client/hooks/useEndpointAction.ts b/client/hooks/useEndpointAction.ts index 45de670e9dc51..62990514185ca 100644 --- a/client/hooks/useEndpointAction.ts +++ b/client/hooks/useEndpointAction.ts @@ -1,16 +1,24 @@ import { useCallback } from 'react'; import { Serialized } from '../../definition/Serialized'; -import { Method, Params, PathFor, Return } from '../../definition/rest'; +import { + MatchPathPattern, + Method, + OperationParams, + OperationResult, + PathFor, +} from '../../definition/rest'; import { useEndpoint } from '../contexts/ServerContext'; import { useToastMessageDispatch } from '../contexts/ToastMessagesContext'; export const useEndpointAction = >( method: TMethod, path: TPath, - params: Serialized[0]> = {} as Serialized[0]>, + params: Serialized>> = {} as Serialized< + OperationParams> + >, successMessage?: string, -): ((extraParams?: Params[1]) => Promise>>) => { +): (() => Promise>>>) => { const sendData = useEndpoint(method, path); const dispatchToastMessage = useToastMessageDispatch(); diff --git a/client/hooks/useEndpointActionExperimental.ts b/client/hooks/useEndpointActionExperimental.ts index 0469b1e40356a..1b5420acae277 100644 --- a/client/hooks/useEndpointActionExperimental.ts +++ b/client/hooks/useEndpointActionExperimental.ts @@ -1,7 +1,13 @@ import { useCallback } from 'react'; import { Serialized } from '../../definition/Serialized'; -import { Method, Params, PathFor, Return } from '../../definition/rest'; +import { + MatchPathPattern, + Method, + OperationParams, + OperationResult, + PathFor, +} from '../../definition/rest'; import { useEndpoint } from '../contexts/ServerContext'; import { useToastMessageDispatch } from '../contexts/ToastMessagesContext'; @@ -13,8 +19,8 @@ export const useEndpointActionExperimental = < path: TPath, successMessage?: string, ): (( - params: Serialized[0]>, -) => Promise>>) => { + params: Serialized>>, +) => Promise>>>) => { const sendData = useEndpoint(method, path); const dispatchToastMessage = useToastMessageDispatch(); diff --git a/client/hooks/useEndpointData.ts b/client/hooks/useEndpointData.ts index b328ba49184e7..3c180b6457b72 100644 --- a/client/hooks/useEndpointData.ts +++ b/client/hooks/useEndpointData.ts @@ -1,16 +1,22 @@ import { useCallback, useEffect } from 'react'; import { Serialized } from '../../definition/Serialized'; -import { Params, PathFor, Return } from '../../definition/rest'; +import { MatchPathPattern, OperationParams, OperationResult, PathFor } from '../../definition/rest'; import { useEndpoint } from '../contexts/ServerContext'; import { useToastMessageDispatch } from '../contexts/ToastMessagesContext'; import { AsyncState, useAsyncState } from './useAsyncState'; -export const useEndpointData =

>( - endpoint: P, - params: Serialized[0]> = {} as Serialized[0]>, - initialValue?: Serialized> | (() => Serialized>), -): AsyncState>> & { reload: () => void } => { +export const useEndpointData = >( + endpoint: TPath, + params: Serialized>> = {} as Serialized< + OperationParams<'GET', MatchPathPattern> + >, + initialValue?: + | Serialized>> + | (() => Serialized>>), +): AsyncState>>> & { + reload: () => void; +} => { const { resolve, reject, reset, ...state } = useAsyncState(initialValue); const dispatchToastMessage = useToastMessageDispatch(); const getData = useEndpoint('GET', endpoint); diff --git a/client/providers/ServerProvider.tsx b/client/providers/ServerProvider.tsx index d98fa0d981400..8e4e0db6aa7ff 100644 --- a/client/providers/ServerProvider.tsx +++ b/client/providers/ServerProvider.tsx @@ -3,7 +3,13 @@ import React, { FC } from 'react'; import { Info as info, APIClient } from '../../app/utils/client'; import { Serialized } from '../../definition/Serialized'; -import { Method, Params, Return, PathFor } from '../../definition/rest'; +import { + Method, + PathFor, + MatchPathPattern, + OperationParams, + OperationResult, +} from '../../definition/rest'; import { ServerContext, ServerMethodName, @@ -28,11 +34,11 @@ const callMethod = ( }); }); -const callEndpoint = >( - method: M, - path: P, - params: Serialized[0]>, -): Promise>> => { +const callEndpoint = >( + method: TMethod, + path: TPath, + params: Serialized>>, +): Promise>>> => { const api = path[0] === '/' ? APIClient : APIClient.v1; const endpointPath = path[0] === '/' ? path.slice(1) : path; diff --git a/definition/rest/index.ts b/definition/rest/index.ts index 758c0afd35a8a..ea67a8a6f4501 100644 --- a/definition/rest/index.ts +++ b/definition/rest/index.ts @@ -1,5 +1,5 @@ import type { EnterpriseEndpoints } from '../../ee/definition/rest'; -import type { ExtractKeys, ValueOf } from '../utils'; +import type { KeyOfEach } from '../utils'; import type { AppsEndpoints } from './apps'; import type { ReplacePlaceholders } from './helpers/ReplacePlaceholders'; import type { BannersEndpoints } from './v1/banners'; @@ -49,57 +49,44 @@ MiscEndpoints & PermissionsEndpoints & InstancesEndpoints; -export type Endpoints = CommunityEndpoints & EnterpriseEndpoints; +type Endpoints = CommunityEndpoints & EnterpriseEndpoints; -type Endpoint = UnionizeEndpoints; +type OperationsByPathPattern = TPathPattern extends any + ? OperationsByPathPatternAndMethod + : never; -type UnionizeEndpoints = ValueOf< -{ - [P in keyof EE as P extends string ? P : never]: UnionizeMethods

; -} ->; +type OperationsByPathPatternAndMethod< + TPathPattern extends keyof Endpoints, + TMethod extends KeyOfEach = KeyOfEach +> = TMethod extends any + ? { + pathPattern: TPathPattern; + method: TMethod; + path: ReplacePlaceholders; + params: GetParams; + result: GetResult; + } + : never; -type ExtractOperations = ExtractKeys any>; +type Operations = OperationsByPathPattern; -type UnionizeMethods

= ValueOf< -{ - [M in keyof OO as ExtractOperations]: ( - method: M, - path: ReplacePlaceholders

, - ...params: Parameters any>> - ) => ReturnType any>>; -} ->; +export type PathPattern = Operations['pathPattern']; -export type Method = Parameters[0]; -export type Path = Parameters[1]; +export type Method = Operations['method']; -export type MethodFor

= P extends any - ? Parameters any>>[0] - : never; -export type PathFor = M extends any - ? Parameters any>>[1] - : never; +export type Path = Operations['path']; -type Operation> = M extends any - ? P extends any - ? Extract any> - : never +export type MethodFor = TPath extends any + ? Extract['method'] : never; -type ExtractParams = Q extends [any, any] - ? [undefined?] - : Q extends [any, any, any, ...any[]] - ? [Q[2]] - : never; - -export type Params> = ExtractParams< -Parameters> ->; -export type Return> = ReturnType>; - +export type PathFor = TMethod extends any + ? Extract['path'] + : never; -export type PathPattern = keyof Endpoints; +export type MatchPathPattern = TPath extends any + ? Extract['pathPattern'] + : never; export type JoinPathPattern = Extract< PathPattern, From 383d0e28fe61ce3a1eedfc82801e43faee393e58 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Fri, 19 Nov 2021 14:21:30 -0300 Subject: [PATCH 08/10] Overcome Serialized issue --- .../contexts/ServerContext/ServerContext.ts | 11 ++++++-- client/hooks/useEndpointData.ts | 10 +++++-- .../admin/settings/groups/LDAPGroupPage.tsx | 8 +++--- definition/rest/index.ts | 2 +- .../livechat-enterprise/server/api/units.ts | 8 +++--- .../rest/v1/omnichannel/businessUnits.ts | 28 +++++++++++++++---- 6 files changed, 46 insertions(+), 21 deletions(-) diff --git a/client/contexts/ServerContext/ServerContext.ts b/client/contexts/ServerContext/ServerContext.ts index 6547751986f3e..838500ef1864f 100644 --- a/client/contexts/ServerContext/ServerContext.ts +++ b/client/contexts/ServerContext/ServerContext.ts @@ -8,6 +8,7 @@ import type { OperationParams, MatchPathPattern, OperationResult, + PathPattern, } from '../../../definition/rest'; import { ServerMethodFunction, @@ -76,12 +77,16 @@ export const useMethod = ( ); }; +type EndpointFunction = ( + params: void extends OperationParams + ? void + : Serialized>, +) => Promise>>; + export const useEndpoint = >( method: TMethod, path: TPath, -): (( - params: Serialized>>, -) => Promise>>>) => { +): EndpointFunction> => { const { callEndpoint } = useContext(ServerContext); return useCallback((params) => callEndpoint(method, path, params), [callEndpoint, path, method]); diff --git a/client/hooks/useEndpointData.ts b/client/hooks/useEndpointData.ts index 3c180b6457b72..38469217d1723 100644 --- a/client/hooks/useEndpointData.ts +++ b/client/hooks/useEndpointData.ts @@ -8,9 +8,13 @@ import { AsyncState, useAsyncState } from './useAsyncState'; export const useEndpointData = >( endpoint: TPath, - params: Serialized>> = {} as Serialized< - OperationParams<'GET', MatchPathPattern> - >, + params: void extends OperationParams<'GET', MatchPathPattern> + ? void + : Serialized< + OperationParams<'GET', MatchPathPattern> + > = undefined as void extends OperationParams<'GET', MatchPathPattern> + ? void + : Serialized>>, initialValue?: | Serialized>> | (() => Serialized>>), diff --git a/client/views/admin/settings/groups/LDAPGroupPage.tsx b/client/views/admin/settings/groups/LDAPGroupPage.tsx index 94a4b59273a15..f466c56d3d17c 100644 --- a/client/views/admin/settings/groups/LDAPGroupPage.tsx +++ b/client/views/admin/settings/groups/LDAPGroupPage.tsx @@ -39,7 +39,7 @@ function LDAPGroupPage({ _id, ...group }: ISetting): JSX.Element { const handleTestConnectionButtonClick = async (): Promise => { try { - const { message } = await testConnection(undefined); + const { message } = await testConnection(); dispatchToastMessage({ type: 'success', message: t(message) }); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); @@ -48,12 +48,12 @@ function LDAPGroupPage({ _id, ...group }: ISetting): JSX.Element { const handleSyncNowButtonClick = async (): Promise => { try { - await testConnection(undefined); + await testConnection(); const confirmSync = async (): Promise => { closeModal(); try { - const { message } = await syncNow(undefined); + const { message } = await syncNow(); dispatchToastMessage({ type: 'success', message: t(message) }); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); @@ -79,7 +79,7 @@ function LDAPGroupPage({ _id, ...group }: ISetting): JSX.Element { const handleSearchTestButtonClick = async (): Promise => { try { - await testConnection(undefined); + await testConnection(); let username = ''; const handleChangeUsername = (event: FormEvent): void => { username = event.currentTarget.value; diff --git a/definition/rest/index.ts b/definition/rest/index.ts index ea67a8a6f4501..8a2f9e3105a70 100644 --- a/definition/rest/index.ts +++ b/definition/rest/index.ts @@ -94,7 +94,7 @@ PathPattern, >; type GetParams = TOperation extends (...args: any) => any - ? Parameters[0] + ? Parameters[0] extends void ? void : Parameters[0] : never type GetResult = TOperation extends (...args: any) => any diff --git a/ee/app/livechat-enterprise/server/api/units.ts b/ee/app/livechat-enterprise/server/api/units.ts index 9f6d94ee663c4..91d441a93c7dd 100644 --- a/ee/app/livechat-enterprise/server/api/units.ts +++ b/ee/app/livechat-enterprise/server/api/units.ts @@ -26,10 +26,10 @@ API.v1.addRoute('livechat/units.list', { authRequired: true }, { API.v1.addRoute('livechat/units.getOne', { authRequired: true }, { async get() { - const { id } = this.urlParams; + const { unitId } = this.queryParams; const { unit } = await findUnitById({ userId: this.userId, - unitId: id, + unitId, }) as { unit: IOmnichannelBusinessUnit }; return API.v1.success(deprecationWarning({ response: unit, endpoint: 'livechat/units.getOne' })); @@ -64,7 +64,7 @@ API.v1.addRoute('livechat/units', { authRequired: true, permissionsRequired: ['m }))); }, async post() { - const { unitData, unitMonitors, unitDepartments } = this.bodyParams?.(); + const { unitData, unitMonitors, unitDepartments } = this.bodyParams; return LivechatEnterprise.saveUnit(null, unitData, unitMonitors, unitDepartments); }, }); @@ -80,7 +80,7 @@ API.v1.addRoute('livechat/units/:id', { authRequired: true, permissionsRequired: return API.v1.success(unit); }, async post() { - const { unitData, unitMonitors, unitDepartments } = this.bodyParams?.(); + const { unitData, unitMonitors, unitDepartments } = this.bodyParams; const { id } = this.urlParams; return LivechatEnterprise.saveUnit(id, unitData, unitMonitors, unitDepartments); diff --git a/ee/definition/rest/v1/omnichannel/businessUnits.ts b/ee/definition/rest/v1/omnichannel/businessUnits.ts index 0d93cb56ebf81..0d9b83afe07d7 100644 --- a/ee/definition/rest/v1/omnichannel/businessUnits.ts +++ b/ee/definition/rest/v1/omnichannel/businessUnits.ts @@ -10,21 +10,37 @@ type WithPagination = { export type OmnichannelBusinessUnitsEndpoints = { 'livechat/units.list': { - GET: () => (WithPagination); + GET: (params: { + text: string; + }) => (WithPagination); }; 'livechat/units.getOne': { - GET: () => (IOmnichannelBusinessUnit); + GET: (params: { + unitId: string; + }) => (IOmnichannelBusinessUnit); }; 'livechat/unitMonitors.list': { - GET: () => ({ monitors: ILivechatMonitor[] }); + GET: (params: { + unitId: string; + }) => ({ monitors: ILivechatMonitor[] }); }; 'livechat/units': { - GET: () => (WithPagination); - POST: () => IOmnichannelBusinessUnit; + GET: (params: { + text: string; + }) => (WithPagination); + POST: (params: { + unitData: unknown; // TODO + unitMonitors: unknown; // TODO + unitDepartments: unknown; // TODO + }) => IOmnichannelBusinessUnit; }; 'livechat/units/:id': { GET: () => IOmnichannelBusinessUnit; - POST: () => IOmnichannelBusinessUnit; + POST: (params: { + unitData: unknown; // TODO + unitMonitors: unknown; // TODO + unitDepartments: unknown; // TODO + }) => IOmnichannelBusinessUnit; DELETE: () => number; }; } From 493bc156c2306a5a67e27f435799ff5b74285486 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 19 Nov 2021 15:55:49 -0300 Subject: [PATCH 09/10] Regression: Units endpoint to TS (#23757) --- definition/rest/v1/omnichannel.ts | 1 + .../server/api/lib/units.js | 53 ---------------- .../server/api/lib/units.ts | 60 +++++++++++++++++++ .../livechat-enterprise/server/api/units.ts | 31 ++++++---- ...itEditWithData.js => UnitEditWithData.tsx} | 13 +++- .../rest/v1/omnichannel/businessUnits.ts | 27 ++++----- 6 files changed, 103 insertions(+), 82 deletions(-) delete mode 100644 ee/app/livechat-enterprise/server/api/lib/units.js create mode 100644 ee/app/livechat-enterprise/server/api/lib/units.ts rename ee/client/omnichannel/units/{UnitEditWithData.js => UnitEditWithData.tsx} (86%) diff --git a/definition/rest/v1/omnichannel.ts b/definition/rest/v1/omnichannel.ts index 21e4250ac9491..f517fefa039b0 100644 --- a/definition/rest/v1/omnichannel.ts +++ b/definition/rest/v1/omnichannel.ts @@ -54,6 +54,7 @@ export type OmnichannelEndpoints = { }; }; 'livechat/departments.by-unit/': { + path: `livechat/departments.by-unit/${ string }`; GET: (params: { text: string; offset: number; count: number }) => { departments: ILivechatDepartment[]; total: number; diff --git a/ee/app/livechat-enterprise/server/api/lib/units.js b/ee/app/livechat-enterprise/server/api/lib/units.js deleted file mode 100644 index 6b0f8deedbc62..0000000000000 --- a/ee/app/livechat-enterprise/server/api/lib/units.js +++ /dev/null @@ -1,53 +0,0 @@ -import { escapeRegExp } from '@rocket.chat/string-helpers'; - -import { hasPermissionAsync } from '../../../../../../app/authorization/server/functions/hasPermission'; -import LivechatUnit from '../../../../models/server/models/LivechatUnit'; -import LivechatUnitMonitors from '../../../../models/server/models/LivechatUnitMonitors'; - -export async function findUnits({ userId, text, pagination: { offset, count, sort } }) { - if (!await hasPermissionAsync(userId, 'manage-livechat-units')) { - throw new Error('error-not-authorized'); - } - const filter = new RegExp(escapeRegExp(text), 'i'); - - const query = { ...text && { $or: [{ name: filter }] } }; - - const cursor = LivechatUnit.find(query, { - sort: sort || { name: 1 }, - skip: offset, - limit: count, - }); - - const total = cursor.count(); - - const units = cursor.fetch(); - - return { - units, - count: units.length, - offset, - total, - }; -} - -export async function findUnitMonitors({ userId, unitId }) { - if (!await hasPermissionAsync(userId, 'manage-livechat-monitors')) { - throw new Error('error-not-authorized'); - } - const monitors = LivechatUnitMonitors.find({ unitId }).fetch(); - - return { - monitors, - }; -} - -export async function findUnitById({ userId, unitId }) { - if (!await hasPermissionAsync(userId, 'manage-livechat-units')) { - throw new Error('error-not-authorized'); - } - const unit = LivechatUnit.findOneById(unitId); - - return { - unit, - }; -} diff --git a/ee/app/livechat-enterprise/server/api/lib/units.ts b/ee/app/livechat-enterprise/server/api/lib/units.ts new file mode 100644 index 0000000000000..ba37e5200e49e --- /dev/null +++ b/ee/app/livechat-enterprise/server/api/lib/units.ts @@ -0,0 +1,60 @@ +import { escapeRegExp } from '@rocket.chat/string-helpers'; + +import { hasPermissionAsync } from '../../../../../../app/authorization/server/functions/hasPermission'; +import LivechatUnit from '../../../../models/server/models/LivechatUnit'; +import LivechatUnitMonitors from '../../../../models/server/raw/LivechatUnitMonitors'; +import { IOmnichannelBusinessUnit } from '../../../../../../definition/IOmnichannelBusinessUnit'; +import { ILivechatMonitor } from '../../../../../../definition/ILivechatMonitor'; + +export async function findUnits({ userId, text, pagination: { offset, count, sort } }: { + userId: string; + text?: string; + pagination: { + offset: number; + count: number; + sort: Record; + }; +}): Promise<{ + units: IOmnichannelBusinessUnit[]; + count: number; + offset: number; + total: number; + }> { + if (!await hasPermissionAsync(userId, 'manage-livechat-units')) { + throw new Error('error-not-authorized'); + } + const filter = text && new RegExp(escapeRegExp(text), 'i'); + + const query = { ...text && { $or: [{ name: filter }] } }; + + const cursor = LivechatUnit.find(query, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + }); + + const total = cursor.count(); + + const units = cursor.fetch(); + + return { + units, + count: units.length, + offset, + total, + }; +} + +export async function findUnitMonitors({ userId, unitId }: { userId: string; unitId: string }): Promise { + if (!await hasPermissionAsync(userId, 'manage-livechat-monitors')) { + throw new Error('error-not-authorized'); + } + return LivechatUnitMonitors.find({ unitId }).toArray() as Promise; +} + +export async function findUnitById({ userId, unitId }: { userId: string; unitId: string }): Promise { + if (!await hasPermissionAsync(userId, 'manage-livechat-units')) { + throw new Error('error-not-authorized'); + } + return LivechatUnit.findOneById(unitId); +} diff --git a/ee/app/livechat-enterprise/server/api/units.ts b/ee/app/livechat-enterprise/server/api/units.ts index 91d441a93c7dd..9132aaa7cd9ef 100644 --- a/ee/app/livechat-enterprise/server/api/units.ts +++ b/ee/app/livechat-enterprise/server/api/units.ts @@ -2,7 +2,6 @@ import { API } from '../../../../../app/api/server'; import { deprecationWarning } from '../../../../../app/api/server/helpers/deprecationWarning'; import { findUnits, findUnitById, findUnitMonitors } from './lib/units'; import { LivechatEnterprise } from '../lib/LivechatEnterprise'; -import { IOmnichannelBusinessUnit } from '../../../../../definition/IOmnichannelBusinessUnit'; API.v1.addRoute('livechat/units.list', { authRequired: true }, { async get() { @@ -27,10 +26,15 @@ API.v1.addRoute('livechat/units.list', { authRequired: true }, { API.v1.addRoute('livechat/units.getOne', { authRequired: true }, { async get() { const { unitId } = this.queryParams; - const { unit } = await findUnitById({ + + if (!unitId) { + return API.v1.failure('Missing "unitId" query parameter'); + } + + const unit = await findUnitById({ userId: this.userId, unitId, - }) as { unit: IOmnichannelBusinessUnit }; + }); return API.v1.success(deprecationWarning({ response: unit, endpoint: 'livechat/units.getOne' })); }, @@ -40,10 +44,15 @@ API.v1.addRoute('livechat/unitMonitors.list', { authRequired: true }, { async get() { const { unitId } = this.queryParams; - return API.v1.success(await findUnitMonitors({ - userId: this.userId, - unitId, - })); + if (!unitId) { + return API.v1.failure('The "unitId" parameter is required'); + } + return API.v1.success({ + monitors: await findUnitMonitors({ + userId: this.userId, + unitId, + }), + }); }, }); @@ -53,7 +62,7 @@ API.v1.addRoute('livechat/units', { authRequired: true, permissionsRequired: ['m const { sort } = this.parseJsonQuery(); const { text } = this.queryParams; - return API.v1.success(Promise.await(findUnits({ + return API.v1.success(await findUnits({ userId: this.userId, text, pagination: { @@ -61,7 +70,7 @@ API.v1.addRoute('livechat/units', { authRequired: true, permissionsRequired: ['m count, sort, }, - }))); + })); }, async post() { const { unitData, unitMonitors, unitDepartments } = this.bodyParams; @@ -72,10 +81,10 @@ API.v1.addRoute('livechat/units', { authRequired: true, permissionsRequired: ['m API.v1.addRoute('livechat/units/:id', { authRequired: true, permissionsRequired: ['manage-livechat-units'] }, { async get() { const { id } = this.urlParams; - const { unit } = await findUnitById({ + const unit = await findUnitById({ userId: this.userId, unitId: id, - }) as { unit: IOmnichannelBusinessUnit }; + }); return API.v1.success(unit); }, diff --git a/ee/client/omnichannel/units/UnitEditWithData.js b/ee/client/omnichannel/units/UnitEditWithData.tsx similarity index 86% rename from ee/client/omnichannel/units/UnitEditWithData.js rename to ee/client/omnichannel/units/UnitEditWithData.tsx index 8057160888f2d..4c685f4d57f58 100644 --- a/ee/client/omnichannel/units/UnitEditWithData.js +++ b/ee/client/omnichannel/units/UnitEditWithData.tsx @@ -1,5 +1,5 @@ import { Callout } from '@rocket.chat/fuselage'; -import React, { useMemo } from 'react'; +import React, { useMemo, FC } from 'react'; import { FormSkeleton } from '../../../../client/components/Skeleton'; import { useTranslation } from '../../../../client/contexts/TranslationContext'; @@ -7,9 +7,15 @@ import { AsyncStatePhase } from '../../../../client/hooks/useAsyncState'; import { useEndpointData } from '../../../../client/hooks/useEndpointData'; import UnitEdit from './UnitEdit'; -function UnitEditWithData({ unitId, reload, title }) { +const UnitEditWithData: FC<{ + unitId: string; + title: string; + reload: () => void; +}> = function UnitEditWithData({ unitId, reload, title }) { const query = useMemo(() => ({ unitId }), [unitId]); + const { value: data, phase: state, error } = useEndpointData('livechat/units.getOne', query); + const { value: unitMonitors, phase: unitMonitorsState, @@ -44,8 +50,9 @@ function UnitEditWithData({ unitId, reload, title }) { unitMonitors={unitMonitors} unitDepartments={unitDepartments} reload={reload} + isNew={false} /> ); -} +}; export default UnitEditWithData; diff --git a/ee/definition/rest/v1/omnichannel/businessUnits.ts b/ee/definition/rest/v1/omnichannel/businessUnits.ts index 0d9b83afe07d7..9b18f13865cfe 100644 --- a/ee/definition/rest/v1/omnichannel/businessUnits.ts +++ b/ee/definition/rest/v1/omnichannel/businessUnits.ts @@ -1,18 +1,14 @@ import { IOmnichannelBusinessUnit } from '../../../../../definition/IOmnichannelBusinessUnit'; import { ILivechatMonitor } from '../../../../../definition/ILivechatMonitor'; - -type WithPagination = { - units: T; - count: number; - offset: number; - total: number; -} +import { PaginatedResult } from '../../../../../definition/rest/helpers/PaginatedResult'; export type OmnichannelBusinessUnitsEndpoints = { 'livechat/units.list': { GET: (params: { text: string; - }) => (WithPagination); + }) => (PaginatedResult & { + units: IOmnichannelBusinessUnit[]; + }); }; 'livechat/units.getOne': { GET: (params: { @@ -27,19 +23,20 @@ export type OmnichannelBusinessUnitsEndpoints = { 'livechat/units': { GET: (params: { text: string; - }) => (WithPagination); + }) => (PaginatedResult & + { units: IOmnichannelBusinessUnit[] }); POST: (params: { - unitData: unknown; // TODO - unitMonitors: unknown; // TODO - unitDepartments: unknown; // TODO + unitData: string; + unitMonitors: string; + unitDepartments: string; }) => IOmnichannelBusinessUnit; }; 'livechat/units/:id': { GET: () => IOmnichannelBusinessUnit; POST: (params: { - unitData: unknown; // TODO - unitMonitors: unknown; // TODO - unitDepartments: unknown; // TODO + unitData: string; + unitMonitors: string; + unitDepartments: string; }) => IOmnichannelBusinessUnit; DELETE: () => number; }; From 40df9bc57cc6335106d00135ffa553b35080f7cb Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Fri, 19 Nov 2021 16:27:17 -0300 Subject: [PATCH 10/10] Fix broken endpoint types --- client/views/hooks/useDepartmentsByUnitsList.ts | 4 +--- definition/rest/v1/omnichannel.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/client/views/hooks/useDepartmentsByUnitsList.ts b/client/views/hooks/useDepartmentsByUnitsList.ts index e2a1de9352a96..7211afe283fc9 100644 --- a/client/views/hooks/useDepartmentsByUnitsList.ts +++ b/client/views/hooks/useDepartmentsByUnitsList.ts @@ -21,9 +21,7 @@ export const useDepartmentsByUnitsList = ( } => { const [itemsList, setItemsList] = useState(() => new RecordList()); const reload = useCallback(() => setItemsList(new RecordList()), []); - const endpoint = `livechat/departments.available-by-unit/${ - options.unitId || 'none' - }` as 'livechat/departments.by-unit/'; + const endpoint = `livechat/departments.available-by-unit/${options.unitId || 'none'}` as const; const getDepartments = useEndpoint('GET', endpoint); diff --git a/definition/rest/v1/omnichannel.ts b/definition/rest/v1/omnichannel.ts index f517fefa039b0..80d85f4ba4bf7 100644 --- a/definition/rest/v1/omnichannel.ts +++ b/definition/rest/v1/omnichannel.ts @@ -53,8 +53,13 @@ export type OmnichannelEndpoints = { department: ILivechatDepartment; }; }; - 'livechat/departments.by-unit/': { - path: `livechat/departments.by-unit/${ string }`; + 'livechat/departments.by-unit/:id': { + GET: (params: { text: string; offset: number; count: number }) => { + departments: ILivechatDepartment[]; + total: number; + }; + }; + 'livechat/departments.available-by-unit/:id': { GET: (params: { text: string; offset: number; count: number }) => { departments: ILivechatDepartment[]; total: number;