diff --git a/jest.config.ts b/jest.config.ts index a18e467a..44fd3d6b 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -89,6 +89,11 @@ const config: Config.InitialOptions = { testMatch: ['/packages/subaccounts/__tests__/**/*.test.ts'], coveragePathIgnorePatterns: ['node_modules', '__tests__'], }, + { + displayName: 'USER', + testMatch: ['/packages/user/__tests__/**/*.test.ts'], + coveragePathIgnorePatterns: ['node_modules', '__tests__'], + }, { displayName: 'VERIFY', testMatch: ['/packages/verify/__tests__/**/*.test.ts'], diff --git a/packages/user/README.md b/packages/user/README.md new file mode 100644 index 00000000..e24e3a37 --- /dev/null +++ b/packages/user/README.md @@ -0,0 +1,71 @@ +# Vonage Users SDK for Node.js + +![GitHub Workflow Status (branch)](https://img.shields.io/github/actions/workflow/status/Vonage/vonage-node-sdk/ci.yml?branch=3.x) [![Codecov](https://img.shields.io/codecov/c/github/vonage/vonage-node-sdk?label=Codecov&logo=codecov&style=flat-square)](https://codecov.io/gh/Vonage/vonage-server-sdk) ![Latest Release](https://img.shields.io/github/v/release/vonage/vonage-node-sdk?logo=npm&style=flat-square) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg?style=flat-square)](../../CODE_OF_CONDUCT.md) [![License](https://img.shields.io/npm/l/@vonage/server-sdk?label=License&style=flat-square)](../../LICENSE.TXT) + +Vonage + +This is the Vonage Users SDK for Node.js for use with [Vonage APIs](https://www.vonage.com/). To use it you will need a Vonage account. Sign up [for free][signup] at vonage.com. + +For full API documentation refer to [developer.nexmo.com](https://developer.nexmo.com/). + +* [Installation](#installation) +* [Usage](#usage) +* [Promises](#promises) + +## Installation + +### With NPM + +```bash +npm install @vonage/users +``` + +### With Yarn + +```bash +yarn add @vonage/users +``` + +## Usage + +The SDK can be used standalone from the main [Vonage Server SDK for Node.js](https://github.com/vonage/vonage-node-sdk) +if you only need to use the Users API. All you need to do +is `require('@vonage/users')`, and use the returned object to create your own +client. + +```js +const {Auth} = require('@vonage/auth'); +const {User} = require('@vonage/user'); + +const usersClient = new User(new Auth({ + apiKey: API_KEY, + apiSecret: API_SECRET, + applicationId: APP_ID, + privateKey: PRIVATE_KEY_PATH, +}), options); +``` + +## Promises + +Most methods that interact with the Vonage API uses Promises. You can either resolve these yourself, or use `await` to +wait for a response. + +```js +const resp = await usersClient.getUser(USER_ID); + +usersClient.getUser(USER_ID) + .then(resp => console.log(resp)) + .catch(err => console.error(err)); +``` + +## Testing + +Run: + +```bash +npm run test +``` + +[signup]: https://dashboard.nexmo.com/sign-up?utm_source=DEV_REL&utm_medium=github&utm_campaign=node-server-sdk + +[license]: https://github.com/Vonage/vonage-node-sdk/blob/3.x/LICENSE.txt diff --git a/packages/user/__tests__/__dataSets__/create.ts b/packages/user/__tests__/__dataSets__/create.ts new file mode 100644 index 00000000..5ba58c0d --- /dev/null +++ b/packages/user/__tests__/__dataSets__/create.ts @@ -0,0 +1,62 @@ +import { UserResponse } from '../../lib'; + +import { BASE_URL, testUser, testUserOne, userToApi } from '../common'; + +const createUser = userToApi(testUser); +delete createUser.id; +export default [ + { + label: 'create simple user', + requests: [ + [ + `/v1/users`, + 'POST', + { + name: testUserOne.name, + }, + ], + ], + responses: [ + [ + 200, + { + ...userToApi(testUserOne), + id: testUserOne.id, + _links: { + self: { + href: `${BASE_URL}/v1/users/${testUserOne.id}`, + }, + }, + } as UserResponse, + ], + ], + clientMethod: 'createUser', + parameters: [testUserOne], + generator: false, + error: false, + expected: testUserOne, + }, + { + label: 'create user', + requests: [[`/v1/users`, 'POST', createUser]], + responses: [ + [ + 200, + { + ...userToApi(testUser), + id: testUser.id, + _links: { + self: { + href: `${BASE_URL}/v1/users/${testUser.id}`, + }, + }, + } as UserResponse, + ], + ], + clientMethod: 'createUser', + parameters: [testUser], + generator: false, + error: false, + expected: testUser, + }, +]; diff --git a/packages/user/__tests__/__dataSets__/delete.ts b/packages/user/__tests__/__dataSets__/delete.ts new file mode 100644 index 00000000..b69a3163 --- /dev/null +++ b/packages/user/__tests__/__dataSets__/delete.ts @@ -0,0 +1,14 @@ +import { testUser } from '../common'; + +export default [ + { + label: 'delete simple user', + requests: [[`/v1/users/${testUser.id}`, 'DELETE']], + responses: [[204]], + clientMethod: 'deleteUser', + parameters: [testUser.id], + generator: false, + error: false, + expected: undefined, + }, +]; diff --git a/packages/user/__tests__/__dataSets__/get.ts b/packages/user/__tests__/__dataSets__/get.ts new file mode 100644 index 00000000..c90860f9 --- /dev/null +++ b/packages/user/__tests__/__dataSets__/get.ts @@ -0,0 +1,44 @@ +import { Client } from '@vonage/server-client'; +import { UserResponse } from '../../lib'; + +import { BASE_URL, testUser } from '../common'; + +export default [ + { + label: 'get user', + requests: [[`/v1/users/${testUser.id}`, 'GET']], + responses: [ + [ + 200, + { + ...Client.transformers.snakeCaseObjectKeys(testUser, true), + properties: { + custom_data: { + ...testUser.properties?.customData, + }, + }, + channels: { + ...testUser.channels, + websocket: [ + { + 'content-type': testUser.channels?.websocket[0].contentType, + headers: testUser.channels?.websocket[0].headers, + uri: testUser.channels?.websocket[0].uri, + }, + ], + }, + _links: { + self: { + href: `${BASE_URL}/v1/users/${testUser.id}`, + }, + }, + } as UserResponse, + ], + ], + clientMethod: 'getUser', + parameters: [testUser.id], + generator: false, + error: false, + expected: testUser, + }, +]; diff --git a/packages/user/__tests__/__dataSets__/index.ts b/packages/user/__tests__/__dataSets__/index.ts new file mode 100644 index 00000000..25cf14b8 --- /dev/null +++ b/packages/user/__tests__/__dataSets__/index.ts @@ -0,0 +1,28 @@ +import userTests from './get'; +import listTests from './list'; +import createTests from './create'; +import updateTests from './put'; +import deleteTests from './delete'; + +export default [ + { + label: 'Get User', + tests: userTests, + }, + { + label: 'List Users', + tests: listTests, + }, + { + label: 'Create User', + tests: createTests, + }, + { + label: 'Update User', + tests: updateTests, + }, + { + label: 'Delete User', + tests: deleteTests, + }, +]; diff --git a/packages/user/__tests__/__dataSets__/list.ts b/packages/user/__tests__/__dataSets__/list.ts new file mode 100644 index 00000000..113d6a8c --- /dev/null +++ b/packages/user/__tests__/__dataSets__/list.ts @@ -0,0 +1,163 @@ +import { Client } from '@vonage/server-client'; +import { + UserType, + UserPageResponse, + UserListParameters, + SortOrder, +} from '../../lib'; + +import { + BASE_URL, + testUser, + testUserOne, + testUserTwo, + userToApi, +} from '../common'; + +export default [ + { + label: 'get one page', + requests: [[`/v1/users?`, 'GET']], + responses: [ + [ + 200, + { + page_size: 100, + _embedded: { + users: [ + userToApi(testUser), + userToApi(testUserOne), + userToApi(testUserTwo), + ], + }, + _links: { + self: { + href: `${BASE_URL}/v1/users`, + }, + }, + } as UserPageResponse, + ], + ], + clientMethod: 'getUserPage', + parameters: [], + generator: false, + error: false, + expected: { + page_size: 100, + _embedded: { + users: [ + userToApi(testUser), + userToApi(testUserOne), + userToApi(testUserTwo), + ], + }, + _links: { + self: { + href: `${BASE_URL}/v1/users`, + }, + }, + }, + }, + { + label: 'get one page with params', + requests: [ + [`/v1/users?page_size=1&order=ASC&cursor=foo&name=user_one`, 'GET'], + ], + responses: [ + [ + 200, + { + page_size: 1, + _embedded: { + users: [userToApi(testUser)], + }, + _links: { + self: { + href: `${BASE_URL}v1/users`, + }, + }, + } as UserPageResponse, + ], + ], + clientMethod: 'getUserPage', + parameters: [ + { + pageSize: 1, + order: SortOrder.ASC, + cursor: 'foo', + name: 'user_one', + } as UserListParameters, + ], + generator: false, + error: false, + expected: { + page_size: 1, + _embedded: { + users: [userToApi(testUser)], + }, + _links: { + self: { + href: `${BASE_URL}v1/users`, + }, + }, + }, + }, + { + label: 'get all pages', + requests: [ + [`/v1/users?`, 'GET'], + [`/v1/users?cursor=fizz`, 'GET'], + ], + responses: [ + [ + 200, + { + _embedded: { + users: [userToApi(testUser), userToApi(testUserOne)], + }, + _links: { + self: { + href: `${BASE_URL}v1/users`, + }, + next: { + href: `${BASE_URL}v1/users?cursor=fizz`, + }, + }, + } as UserPageResponse, + ], + [ + 200, + { + _embedded: { + users: [userToApi(testUserTwo)], + }, + _links: { + self: { + href: `${BASE_URL}v1/users`, + }, + prev: { + href: `${BASE_URL}v1/users?`, + }, + }, + } as UserPageResponse, + ], + ], + clientMethod: 'listAllUsers', + parameters: [], + generator: true, + error: false, + expected: [ + { + ...Client.transformers.camelCaseObjectKeys(testUser, true, true), + properties: { + customData: testUser.properties?.customData, + }, + channels: { + ...testUser.channels, + }, + } as UserType, + Client.transformers.camelCaseObjectKeys(testUserOne, true, true), + Client.transformers.camelCaseObjectKeys(testUserTwo, true, true), + ], + }, +]; diff --git a/packages/user/__tests__/__dataSets__/put.ts b/packages/user/__tests__/__dataSets__/put.ts new file mode 100644 index 00000000..787e3342 --- /dev/null +++ b/packages/user/__tests__/__dataSets__/put.ts @@ -0,0 +1,32 @@ +import { UserResponse } from '../../lib'; + +import { BASE_URL, testUser, userToApi } from '../common'; + +const updateUser = userToApi(testUser); +delete updateUser.id; + +export default [ + { + label: 'update user', + requests: [[`/v1/users/${testUser.id}`, 'PUT', updateUser]], + responses: [ + [ + 200, + { + ...updateUser, + id: testUser.id, + _links: { + self: { + href: `${BASE_URL}/v1/users/${testUser.id}`, + }, + }, + } as UserResponse, + ], + ], + clientMethod: 'updateUser', + parameters: [testUser], + generator: false, + error: false, + expected: testUser, + }, +]; diff --git a/packages/user/__tests__/common.ts b/packages/user/__tests__/common.ts new file mode 100644 index 00000000..e75d4fe9 --- /dev/null +++ b/packages/user/__tests__/common.ts @@ -0,0 +1,102 @@ +import { Client } from '@vonage/server-client'; +import { UserResponse, UserType, WebSocketChannelResponse } from '../lib'; + +export const BASE_URL = 'https://api.nexmo.com/'; + +export const userToApi = (user: UserType): UserResponse => { + const apiUser = Client.transformers.snakeCaseObjectKeys(user, true); + if (user.properties) { + apiUser.properties = { + custom_data: user.properties.customData, + }; + } + + if (user.channels?.websocket) { + apiUser.channels.websocket = [ + { + uri: user.channels.websocket[0].uri, + headers: user.channels.websocket[0].headers, + 'content-type': user.channels.websocket[0].contentType, + } as WebSocketChannelResponse, + ]; + } + + return apiUser; +}; + +export const testUser = Object.freeze({ + id: 'USR-00000000-0000-0000-0000-000000000000', + name: 'jimmy', + displayName: 'Jimmy', + imageUrl: 'https://example.com/image.png', + properties: { + customData: { + custom_key: 'custom_value', + fizzBuzz: 'fooBar', + }, + }, + channels: { + pstn: [ + { + number: 123457, + }, + ], + sip: [ + { + uri: 'sip:4442138907@sip.example.com;transport=tls', + username: 'New SIP', + password: 'Password', + }, + ], + vbc: [ + { + extension: '403', + }, + ], + websocket: [ + { + uri: 'wss://example.com/socket', + contentType: 'audio/l16;rate=16000', + headers: { + customerId: 'ABC123', + 'x-fizz-buzz': 'foo-bar', + }, + }, + ], + sms: [ + { + number: '15055034455', + }, + ], + mms: [ + { + number: '15058425662', + }, + ], + whatsapp: [ + { + number: '15052427700', + }, + ], + viber: [ + { + number: '14023429288', + }, + ], + messenger: [ + { + id: '12345abcd', + }, + ], + }, +}) as UserType; + +export const testUserOne = Object.freeze({ + id: 'USR-00000000-0000-0000-0000-000000000001', + name: 'user_one', +}) as UserType; + +export const testUserTwo = Object.freeze({ + id: 'USR-00000000-0000-0000-0000-000000000002', + name: 'user_two', +}) as UserType; diff --git a/packages/user/__tests__/private.test.key b/packages/user/__tests__/private.test.key new file mode 100644 index 00000000..d127da04 --- /dev/null +++ b/packages/user/__tests__/private.test.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDTH8cEhJKsu2hB +ucgs0Blf3tvgdXuIa5sMRI3zIZGok8jqaj6DC0WdM1eiJlQCWnVL0vR25MkopMZN +MTphoaVpdK9wywMVx7PsgMCDXJjW77QFJFbbcbV9KG2xvMsjJGttmVrn+9aPpCnX +yT27bWkTR0k/wCAvra/st5MDKg2kB3SMc+97zlO3GqIkJJKfKYQFAO8oRBcsZ1Bx +ydPWTQvyLYW15Cjv30aIGLfceFwDeUOBgBsesGFtcXvVF6sVxd5D/USunX/9es95 +Rdr4+9Qq4tIKZRkBz2KWD0eo256wmmq2lQoGaR9x6XgLHhUlsi6OXILcyChi2Qcc +a01Hn7YvAgMBAAECggEAJsS+lIdNsddmIS+e4Q/DoRW49aJNMXNlEN8j2+Itr7GX +ougom4K94UyUyotUQOxgfrB5wL1pbQO5AGLKUDRRPii1sLYu1liKIyNPdq/RxyJU +Qd927awXQiji39EF0mm1KnaPOWtG7rCcGGp1Yg4Izgf4nPLIVkkENalOHzYhNB3u +4W4OIT49iw/auBF4wnl1RmXWXjkxDuk2cYT28a8hWqyQjJqXTsO+u4BaXYxSf4nP +Be2yoUEFRbcxvJrhEpfODhPP83I1EBipJkhUTc5WMb/vtH2b49+TYd2tPR0LOxom +mcNUWF6++ae+vL6K8Dlfcvx+CA7g7KBHHcgFCzn7GQKBgQDzc2ow5LlQQ/VfXZTz +n07V/QgVQ15sA5Cf/gsvmwnGPy06Qx/WRHsz6NG8nvW2mHZwfDIHuLjBW1gcssEx +mLpqav5XLZfSyjjRO/AxLIfJDx/aARp3+7Ny5aY2e3wtNx8wz4J80i7P+eX3fETM +70cWhc2PvYMDjG+O7cDW2FWAFwKBgQDeAcc/FBHLl9/HqiBvYf/Y/k0t1TUoHujO +PSbP6SaN06JnvJmBANyED7sWeIPuoRFXXEr4Auu7y0C55Wlsno/ImTbJsopZ1rgU +k5q4t9vcu7cGiOr7L7UkySNYZqRjwvKEJ610COexTThSwl0v3GNLP8r4AMdBaqdK +uO6fVfxxqQKBgFc5ne2Atai9gJe3ltum0382FoRPy+/VYyb/xZA780eVcSXz0N9b +T+0sWKFFLvJKM/1fcil0FLYqfSpjHXcgqoHgbdpcWo5KqArHd+qWctwl0Bqy1IHy +q7vZ7jCNE9O7cTBy2OTSBbW8apm+a4Qlowm9zQXYN624zmueYb5YamHnAoGAZvJA +KHnv/o7AkF/NhpjVARR7SYOSkLY0kl48/zBIVoAK0TvdmrqBhyOcR8E+vIsn9XCw +uuzvzzdjHlDJYDruxcB2bXVDPoGY/sGrf3iSlXreVkNrY2st/o7euwFtvW0K9ElJ +34K5nbgHJClI+QajbKN6RSJnQ2hnhvjWfkBrPXECgYEA4MCEm9EyrguEO51am8va +OjIAiQMkj/iyjsMDL8eH0VM+OMdieHwbkKyajyrB9dFikvWfxiuo3dU1N5vJTzty +LmzkB8M/rKlAYKD8iKA8cRun4tKzRepHT3JPMu0GYTfcP9ovs5F3aEjX+UuWOO7n +doWDENAr/VU1RNCDwFdxYFg= +-----END PRIVATE KEY----- diff --git a/packages/user/__tests__/user.test.ts b/packages/user/__tests__/user.test.ts new file mode 100644 index 00000000..95a7ce2c --- /dev/null +++ b/packages/user/__tests__/user.test.ts @@ -0,0 +1,78 @@ +import { User } from '../lib/index'; +import nock from 'nock'; +import { Auth } from '@vonage/auth'; +import { BASE_URL } from './common'; +import testDataSets from './__dataSets__/index'; +import { readFileSync } from 'fs'; + +const key = readFileSync(`${__dirname}/private.test.key`).toString(); + +const getResults = async ( + generator: boolean, + client: User, + clientMethod: string, + parameters: Array, +): Promise> => { + if (!generator) { + return await client[clientMethod](...parameters); + } + + const results = []; + for await (const result of client[clientMethod](...parameters)) { + results.push(result as never); + } + + return results; +}; + +describe.each(testDataSets)('$label', ({ tests }) => { + let client: User | null; + let scope; + + beforeEach(function () { + client = new User( + new Auth({ + privateKey: key, + applicationId: 'my-application', + }), + ); + + scope = nock(BASE_URL, { + reqheaders: { + authorization: (value) => value.startsWith('Bearer '), + }, + }).persist(); + }); + + afterEach(function () { + client = null; + scope = null; + nock.cleanAll(); + }); + + test.each(tests)( + 'Can $label using: $clientMethod', + async ({ + requests, + responses, + clientMethod, + parameters, + expected, + generator = false, + }) => { + requests.forEach((request, index) => { + scope.intercept(...request).reply(...responses[index]); + }); + + const results = await getResults( + generator, + client as User, + clientMethod, + parameters, + ); + + expect(results).toEqual(expected); + expect(nock.isDone()).toBeTruthy(); + }, + ); +}); diff --git a/packages/user/lib/enums/index.ts b/packages/user/lib/enums/index.ts new file mode 100644 index 00000000..299ee908 --- /dev/null +++ b/packages/user/lib/enums/index.ts @@ -0,0 +1 @@ +export * from './userSort'; diff --git a/packages/user/lib/enums/userSort.ts b/packages/user/lib/enums/userSort.ts new file mode 100644 index 00000000..c8f98818 --- /dev/null +++ b/packages/user/lib/enums/userSort.ts @@ -0,0 +1,4 @@ +export enum SortOrder { + ASC = 'ASC', + DESC = 'DESC', +} diff --git a/packages/user/lib/index.ts b/packages/user/lib/index.ts new file mode 100644 index 00000000..64b7b33e --- /dev/null +++ b/packages/user/lib/index.ts @@ -0,0 +1,3 @@ +export * from './types/index'; +export * from './enums/index'; +export * from './user'; diff --git a/packages/user/lib/types/index.ts b/packages/user/lib/types/index.ts new file mode 100644 index 00000000..8925cee2 --- /dev/null +++ b/packages/user/lib/types/index.ts @@ -0,0 +1,13 @@ +export * from './messengerChannel'; +export * from './mmsChannel'; +export * from './parameters/index'; +export * from './pstnChannel'; +export * from './requests/index'; +export * from './responses/index'; +export * from './sipChannel'; +export * from './smsChannel'; +export * from './userType'; +export * from './vbcChannel'; +export * from './viberChannel'; +export * from './websocketChannel'; +export * from './whatsappChannel'; diff --git a/packages/user/lib/types/messengerChannel.ts b/packages/user/lib/types/messengerChannel.ts new file mode 100644 index 00000000..c48acc02 --- /dev/null +++ b/packages/user/lib/types/messengerChannel.ts @@ -0,0 +1,3 @@ +export type MessengerChannel = { + id: string; +}; diff --git a/packages/user/lib/types/mmsChannel.ts b/packages/user/lib/types/mmsChannel.ts new file mode 100644 index 00000000..2f1ecbbc --- /dev/null +++ b/packages/user/lib/types/mmsChannel.ts @@ -0,0 +1,3 @@ +export type MmsChannel = { + number: string; +}; diff --git a/packages/user/lib/types/parameters/index.ts b/packages/user/lib/types/parameters/index.ts new file mode 100644 index 00000000..85327976 --- /dev/null +++ b/packages/user/lib/types/parameters/index.ts @@ -0,0 +1 @@ +export * from './userListParameters'; diff --git a/packages/user/lib/types/parameters/userListParameters.ts b/packages/user/lib/types/parameters/userListParameters.ts new file mode 100644 index 00000000..47cdc200 --- /dev/null +++ b/packages/user/lib/types/parameters/userListParameters.ts @@ -0,0 +1,8 @@ +import { SortOrder } from '../../enums/userSort'; + +export type UserListParameters = { + pageSize?: number; + order?: SortOrder; + cursor?: string; + name?: string; +}; diff --git a/packages/user/lib/types/pstnChannel.ts b/packages/user/lib/types/pstnChannel.ts new file mode 100644 index 00000000..d9b05837 --- /dev/null +++ b/packages/user/lib/types/pstnChannel.ts @@ -0,0 +1,3 @@ +export type PstnChannel = { + number: number; +}; diff --git a/packages/user/lib/types/requests/index.ts b/packages/user/lib/types/requests/index.ts new file mode 100644 index 00000000..50ff6d46 --- /dev/null +++ b/packages/user/lib/types/requests/index.ts @@ -0,0 +1,2 @@ +export * from './userRequest'; +export * from './websocketChannelRequest'; diff --git a/packages/user/lib/types/requests/userRequest.ts b/packages/user/lib/types/requests/userRequest.ts new file mode 100644 index 00000000..8a421154 --- /dev/null +++ b/packages/user/lib/types/requests/userRequest.ts @@ -0,0 +1,12 @@ +import { UserType } from '../index'; +import { WebSocketChannelRequest } from './websocketChannelRequest'; + +export type UserRequest = { + image_url: string; + properties: { + custom_data: Record; + }; + channels: { + websocket: Array; + } & Omit, 'websocket'>; +} & Omit; diff --git a/packages/user/lib/types/requests/websocketChannelRequest.ts b/packages/user/lib/types/requests/websocketChannelRequest.ts new file mode 100644 index 00000000..fea89d05 --- /dev/null +++ b/packages/user/lib/types/requests/websocketChannelRequest.ts @@ -0,0 +1,5 @@ +import { WebsocketChannel } from '../websocketChannel'; + +export type WebSocketChannelRequest = { + 'content-type': string; +} & Omit; diff --git a/packages/user/lib/types/responses/index.ts b/packages/user/lib/types/responses/index.ts new file mode 100644 index 00000000..4312567f --- /dev/null +++ b/packages/user/lib/types/responses/index.ts @@ -0,0 +1,3 @@ +export * from './userPageResponse'; +export * from './userResponse'; +export * from './websocketChannelResponse'; diff --git a/packages/user/lib/types/responses/userPageResponse.ts b/packages/user/lib/types/responses/userPageResponse.ts new file mode 100644 index 00000000..d03f8093 --- /dev/null +++ b/packages/user/lib/types/responses/userPageResponse.ts @@ -0,0 +1,9 @@ +import { APILinks } from '@vonage/server-client'; +import { UserResponse } from './userResponse'; + +export type UserPageResponse = { + page_size: number; + _embedded: { + users: Array; + }; +} & APILinks; diff --git a/packages/user/lib/types/responses/userResponse.ts b/packages/user/lib/types/responses/userResponse.ts new file mode 100644 index 00000000..8e228ca1 --- /dev/null +++ b/packages/user/lib/types/responses/userResponse.ts @@ -0,0 +1,4 @@ +import { APILinks } from '@vonage/server-client'; +import { UserRequest } from '../requests/userRequest'; + +export type UserResponse = UserRequest & APILinks; diff --git a/packages/user/lib/types/responses/websocketChannelResponse.ts b/packages/user/lib/types/responses/websocketChannelResponse.ts new file mode 100644 index 00000000..0ea36f5a --- /dev/null +++ b/packages/user/lib/types/responses/websocketChannelResponse.ts @@ -0,0 +1,5 @@ +import { WebsocketChannel } from '../websocketChannel'; + +export type WebSocketChannelResponse = { + 'content-type': string; +} & Omit; diff --git a/packages/user/lib/types/sipChannel.ts b/packages/user/lib/types/sipChannel.ts new file mode 100644 index 00000000..4856afad --- /dev/null +++ b/packages/user/lib/types/sipChannel.ts @@ -0,0 +1,5 @@ +export type SipChannel = { + uri: string; + username: string; + password: string; +}; diff --git a/packages/user/lib/types/smsChannel.ts b/packages/user/lib/types/smsChannel.ts new file mode 100644 index 00000000..a93cc725 --- /dev/null +++ b/packages/user/lib/types/smsChannel.ts @@ -0,0 +1,3 @@ +export type SmsChannel = { + number: string; +}; diff --git a/packages/user/lib/types/userType.ts b/packages/user/lib/types/userType.ts new file mode 100644 index 00000000..6791f8e1 --- /dev/null +++ b/packages/user/lib/types/userType.ts @@ -0,0 +1,30 @@ +import { ViberChannel } from './viberChannel'; +import { WhatsappChannel } from './whatsappChannel'; +import { MmsChannel } from './mmsChannel'; +import { SmsChannel } from './smsChannel'; +import { WebsocketChannel } from './websocketChannel'; +import { VbcChannel } from './vbcChannel'; +import { MessengerChannel } from './messengerChannel'; +import { PstnChannel } from './pstnChannel'; +import { SipChannel } from './sipChannel'; + +export type UserType = { + id?: string; + name?: string; + displayName?: string; + imageUrl?: string; + properties?: { + customData: Record; + }; + channels?: { + pstn?: Array; + sip?: Array; + vbc?: Array; + websocket?: Array; + sms?: Array; + mms?: Array; + whatsapp?: Array; + viber?: Array; + messenger?: Array; + }; +}; diff --git a/packages/user/lib/types/vbcChannel.ts b/packages/user/lib/types/vbcChannel.ts new file mode 100644 index 00000000..bbe9b854 --- /dev/null +++ b/packages/user/lib/types/vbcChannel.ts @@ -0,0 +1,3 @@ +export type VbcChannel = { + extension: string; +}; diff --git a/packages/user/lib/types/viberChannel.ts b/packages/user/lib/types/viberChannel.ts new file mode 100644 index 00000000..1880d5e0 --- /dev/null +++ b/packages/user/lib/types/viberChannel.ts @@ -0,0 +1,3 @@ +export type ViberChannel = { + number: string; +}; diff --git a/packages/user/lib/types/websocketChannel.ts b/packages/user/lib/types/websocketChannel.ts new file mode 100644 index 00000000..80b9946a --- /dev/null +++ b/packages/user/lib/types/websocketChannel.ts @@ -0,0 +1,5 @@ +export type WebsocketChannel = { + uri: string; + contentType?: string; + headers?: Record; +}; diff --git a/packages/user/lib/types/whatsappChannel.ts b/packages/user/lib/types/whatsappChannel.ts new file mode 100644 index 00000000..f4d35cfa --- /dev/null +++ b/packages/user/lib/types/whatsappChannel.ts @@ -0,0 +1,3 @@ +export type WhatsappChannel = { + number: string; +}; diff --git a/packages/user/lib/user.ts b/packages/user/lib/user.ts new file mode 100644 index 00000000..91b36c21 --- /dev/null +++ b/packages/user/lib/user.ts @@ -0,0 +1,143 @@ +import { AuthenticationType, Client } from '@vonage/server-client'; +import { + UserListParameters, + UserPageResponse, + UserResponse, + UserType, + WebSocketChannelRequest, + WebSocketChannelResponse, + WebsocketChannel, +} from './types/index'; + +const apiUserToUser = (apiUser: UserResponse): UserType => { + const user = Client.transformers.camelCaseObjectKeys(apiUser, true); + + delete user.links; + delete user.properties?.customData; + if (apiUser.properties) { + user.properties.customData = apiUser.properties?.custom_data; + } + + if (apiUser?.channels?.websocket) { + user.channels.websocket = apiUser.channels.websocket.map( + (apiSocket: WebSocketChannelResponse): WebsocketChannel => { + const socket = { + uri: apiSocket.uri, + } as WebsocketChannel; + + if (apiSocket['content-type']) { + socket.contentType = apiSocket['content-type']; + } + + if (apiSocket.headers) { + socket.headers = apiSocket.headers; + } + + return socket; + }, + ); + } + + return user; +}; + +const userToAPI = (user: UserType): UserResponse => { + const apiUser = Client.transformers.snakeCaseObjectKeys(user, true); + // preserve user properties + if (apiUser.properties?.custom_data) { + apiUser.properties.custom_data = user.properties.customData; + } + + // Websockets will change when transformed + if (apiUser.channels?.websocket) { + apiUser.channels.websocket = user.channels.websocket.map( + (socket: WebsocketChannel): WebSocketChannelRequest => { + const apiSocket = { + uri: socket.uri, + } as WebSocketChannelRequest; + + // restore content type + if (socket.contentType) { + apiSocket['content-type'] = socket.contentType; + } + + // preserve headers + if (socket.headers) { + apiSocket.headers = socket.headers; + } + + return apiSocket; + }, + ); + } + + delete apiUser.id; + + return apiUser; +}; + +export class User extends Client { + protected authType = AuthenticationType.JWT; + + async *listAllUsers( + params: UserListParameters = {}, + ): AsyncGenerator { + let cursor = params.cursor; + do { + if (cursor) { + params.cursor = cursor; + } + const resp = await this.getUserPage(params); + + yield* resp._embedded?.users.map(apiUserToUser); + const next = resp._links?.next?.href + ? new URL(resp._links.next.href) + : null; + + cursor = next ? next.searchParams.get('cursor') : null; + } while (cursor); + } + + async getUserPage( + params: UserListParameters = {}, + ): Promise { + const resp = await this.sendGetRequest( + `${this.config.apiHost}/v1/users`, + Client.transformers.snakeCaseObjectKeys(params, true), + ); + + return resp.data; + } + + async createUser(user: UserType): Promise { + const resp = await this.sendPostRequest( + `${this.config.apiHost}/v1/users`, + userToAPI(user), + ); + + return apiUserToUser(resp.data); + } + + async getUser(userId: string): Promise { + const resp = await this.sendGetRequest( + `${this.config.apiHost}/v1/users/${userId}`, + ); + + return apiUserToUser(resp.data); + } + + async updateUser(user: UserType): Promise { + const resp = await this.sendPutRequest( + `${this.config.apiHost}/v1/users/${user.id}`, + userToAPI(user), + ); + + return apiUserToUser(resp.data); + } + + async deleteUser(userId: string): Promise { + await this.sendDeleteRequest( + `${this.config.apiHost}/v1/users/${userId}`, + ); + } +} diff --git a/packages/user/package.json b/packages/user/package.json new file mode 100644 index 00000000..10cbc74c --- /dev/null +++ b/packages/user/package.json @@ -0,0 +1,35 @@ +{ + "name": "@vonage/users", + "version": "1.0.0", + "description": "The concept of a user exists in Vonage APIs, you can associate one with a user in your own application if you choose. A user can have multiple memberships to conversations and can communicate with other users through various different mediums.", + "homepage": "https://github.com/vonage/vonage-node-sdk/tree/main/packages/user#readme", + "bugs": { + "url": "https://github.com/Vonage/vonage-node-sdk/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Vonage/vonage-node-sdk.git" + }, + "license": "Apache-2.0", + "author": "Chuck MANCHUCK Reeves ", + "main": "lib/user.js", + "directories": { + "lib": "lib", + "test": "__tests__" + }, + "files": [ + "lib" + ], + "scripts": { + "build": "npm run clean && npm run compile", + "clean": "npx shx rm -rf dist tsconfig.tsbuildinfo", + "compile": "npx tsc --build --verbose" + }, + "dependencies": { + "@vonage/server-client": "1.5.0" + }, + "devDependencies": { + "@vonage/auth": "1.4.0", + "nock": "13.3.2" + } +} diff --git a/packages/user/tsconfig.json b/packages/user/tsconfig.json new file mode 100644 index 00000000..e9b438f7 --- /dev/null +++ b/packages/user/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../tsconfig.json", + + "compilerOptions": { + "rootDir": "lib", + "outDir": "dist" + }, + + "exclude": [ + "__tests__", + "jest.config.js", + "dist" + ], + + "references": [ + { "path": "../auth" }, + { "path": "../server-client" }, + { "path": "../vetch" } + ], + + "ts-node": { + "esm": true + } +}