@tellescope/sdk 0.0.24 → 0.0.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tellescope/sdk",
3
- "version": "0.0.24",
3
+ "version": "0.0.28",
4
4
  "description": "Code for interacting with the Tellescope API",
5
5
  "main": "./lib/cjs/sdk.js",
6
6
  "module": "./lib/esm/sdk.js",
@@ -32,14 +32,14 @@
32
32
  },
33
33
  "homepage": "https://github.com/tellescope-os/tellescope#readme",
34
34
  "dependencies": {
35
- "@tellescope/constants": "^0.0.24",
36
- "@tellescope/schema": "^0.0.24",
37
- "@tellescope/testing": "^0.0.24",
38
- "@tellescope/types-client": "^0.0.24",
39
- "@tellescope/types-models": "^0.0.24",
40
- "@tellescope/types-utilities": "^0.0.24",
41
- "@tellescope/utilities": "^0.0.24",
42
- "@tellescope/validation": "^0.0.24",
35
+ "@tellescope/constants": "^0.0.28",
36
+ "@tellescope/schema": "^0.0.28",
37
+ "@tellescope/testing": "^0.0.28",
38
+ "@tellescope/types-client": "^0.0.28",
39
+ "@tellescope/types-models": "^0.0.28",
40
+ "@tellescope/types-utilities": "^0.0.28",
41
+ "@tellescope/utilities": "^0.0.28",
42
+ "@tellescope/validation": "^0.0.28",
43
43
  "axios": "^0.21.1",
44
44
  "express": "^4.17.1",
45
45
  "form-data": "^4.0.0",
@@ -56,5 +56,5 @@
56
56
  "publishConfig": {
57
57
  "access": "public"
58
58
  },
59
- "gitHead": "b43edb136f319f3afac336237c7bcb030bb3c60d"
59
+ "gitHead": "90f22163cadb40fbba2c44561a56fd4ea6530c16"
60
60
  }
package/src/enduser.ts CHANGED
@@ -4,10 +4,9 @@ import { Session, SessionOptions } from "./session"
4
4
  import { APIQuery } from "./sdk"
5
5
  import { url_safe_path } from "@tellescope/utilities"
6
6
 
7
- import { S3PresignedPost, UserIdentity } from "@tellescope/types-utilities"
7
+ import { FileBlob, FileBuffer, FileDetails, ReactNativeFile, S3PresignedPost, UserIdentity } from "@tellescope/types-utilities"
8
8
  import {
9
9
  Attendee,
10
- AttendeeInfo,
11
10
  } from "@tellescope/types-models"
12
11
  import {
13
12
  ClientModelForName,
@@ -20,7 +19,7 @@ import {
20
19
 
21
20
  export interface EnduserSessionOptions extends SessionOptions {}
22
21
 
23
- type EnduserAccessibleModels = "chat_rooms" | 'chats' | 'files' | 'tickets'
22
+ type EnduserAccessibleModels = 'endusers' | "chat_rooms" | 'chats' | 'files' | 'tickets'
24
23
 
25
24
  export const defaultQueries = <N extends keyof ClientModelForName>(
26
25
  s: EnduserSession, n: keyof ClientModelForName_required
@@ -48,7 +47,7 @@ type EnduserQueries = { [K in EnduserAccessibleModels]: APIQuery<K> } & {
48
47
  display_info: () => Promise<UserDisplayInfo[]>
49
48
  },
50
49
  files: {
51
- prepare_file_upload: (args: { name: string, size: number, type: string }) => Promise<{ presignedUpload: S3PresignedPost, file: File }>,
50
+ prepare_file_upload: (args: FileDetails) => Promise<{ presignedUpload: S3PresignedPost, file: File }>,
52
51
  file_download_URL: (args: { secureName: string }) => Promise<{ downloadURL: string }>,
53
52
  },
54
53
  meetings: {
@@ -61,6 +60,7 @@ type EnduserQueries = { [K in EnduserAccessibleModels]: APIQuery<K> } & {
61
60
  const loadDefaultQueries = (s: EnduserSession): { [K in EnduserAccessibleModels] : APIQuery<K> } => ({
62
61
  chat_rooms: defaultQueries(s, 'chat_rooms'),
63
62
  chats: defaultQueries(s, 'chats'),
63
+ endusers: defaultQueries(s, 'endusers'),
64
64
  files: defaultQueries(s, 'files'),
65
65
  tickets: defaultQueries(s, 'tickets'),
66
66
  })
@@ -76,9 +76,8 @@ export class EnduserSession extends Session {
76
76
 
77
77
  this.api = loadDefaultQueries(this) as EnduserQueries
78
78
 
79
- this.api.endusers = {
80
- logout: () => this._POST('/v1/logout-enduser'),
81
- }
79
+ this.api.endusers.logout = () => this._POST('/v1/logout-enduser'),
80
+
82
81
  this.api.users = {
83
82
  display_info: () => this._GET<{}, UserDisplayInfo[] >(`/v1/user-display-info`),
84
83
  }
@@ -114,6 +113,13 @@ export class EnduserSession extends Session {
114
113
  return await this.DELETE<A,R>(endpoint, args, authenticated)
115
114
  }
116
115
 
116
+ prepare_and_upload_file = async (details: FileDetails, file: Blob | Buffer | ReactNativeFile) => {
117
+ const { name, size, type } = details
118
+ const { presignedUpload, file: { secureName } } = await this.api.files.prepare_file_upload({ name, size, type })
119
+ await this.UPLOAD(presignedUpload, file)
120
+ return { secureName }
121
+ }
122
+
117
123
  handle_new_session = async ({ authToken, enduser }: { authToken: string, enduser: Enduser }) => {
118
124
  this.sessionStart = Date.now()
119
125
  this.setAuthToken(authToken)
package/src/sdk.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import {
2
- AttendeeInfo,
3
2
  JourneyState,
4
3
  UserSession,
5
4
  MeetingInfo,
@@ -18,7 +17,7 @@ import {
18
17
  File,
19
18
  Meeting,
20
19
  } from "@tellescope/types-client"
21
- import { CustomUpdateOptions, SortOption, S3PresignedPost, UserIdentity } from "@tellescope/types-utilities"
20
+ import { CustomUpdateOptions, SortOption, S3PresignedPost, UserIdentity, FileBlob, FileBuffer, FileDetails, ReactNativeFile } from "@tellescope/types-utilities"
22
21
  import { url_safe_path } from "@tellescope/utilities"
23
22
 
24
23
  import { Session as SessionManager, SessionOptions } from "./session"
@@ -93,14 +92,14 @@ type Queries = { [K in keyof ClientModelForName]: APIQuery<K> } & {
93
92
  },
94
93
  endusers: {
95
94
  set_password: (args: { id: string, password: string }) => Promise<void>,
96
- is_authenticated: (args: { id: string, authToken: string }) => Promise<{ isAuthenticated: boolean, enduser: Enduser }>
95
+ is_authenticated: (args: { id?: string, authToken: string }) => Promise<{ isAuthenticated: boolean, enduser: Enduser }>
97
96
  generate_auth_token: (args: { id?: string, phone?: string, email?: string, externalId?: string }) => Promise<{ authToken: string, enduser: Enduser }>
98
97
  },
99
98
  users: {
100
99
  display_names: () => Promise<{ fname: string, lname: string, id: string }[]>,
101
100
  },
102
101
  files: {
103
- prepare_file_upload: (args: { name: string, size: number, type: string }) => Promise<{ presignedUpload: S3PresignedPost, file: File }>,
102
+ prepare_file_upload: (args: FileDetails) => Promise<{ presignedUpload: S3PresignedPost, file: File }>,
104
103
  file_download_URL: (args: { secureName: string }) => Promise<{ downloadURL: string }>,
105
104
  },
106
105
  meetings: {
@@ -148,8 +147,6 @@ export class Session extends SessionManager {
148
147
  queries.webhooks.update = a => this._PATCH('/v1/update-webhooks', a)
149
148
 
150
149
  this.api = queries
151
-
152
- // if (this.userInfo) this.refresh_session()
153
150
  }
154
151
 
155
152
  _POST = async <A,R=void>(endpoint: string, args?: A, authenticated=true) => {
@@ -205,6 +202,13 @@ export class Session extends SessionManager {
205
202
  await this.POST('/logout-api').catch(console.error)
206
203
  }
207
204
 
205
+ prepare_and_upload_file = async (details: FileDetails, file: Blob | Buffer | ReactNativeFile) => {
206
+ const { name, size, type } = details
207
+ const { presignedUpload, file: { secureName } } = await this.api.files.prepare_file_upload({ name, size, type })
208
+ await this.UPLOAD(presignedUpload, file)
209
+ return { secureName }
210
+ }
211
+
208
212
  reset_db = () => this.POST('/reset-demo')
209
213
  test_online = () => this.GET<{}, string>('/v1')
210
214
  test_authenticated = () => this.GET<{}, string>('/v1/test-authenticated')
package/src/session.ts CHANGED
@@ -4,6 +4,7 @@ import { Socket, io } from 'socket.io-client'
4
4
 
5
5
  import {
6
6
  FileBlob,
7
+ ReactNativeFile,
7
8
  S3PresignedPost,
8
9
  } from "@tellescope/types-utilities"
9
10
  import {
@@ -152,27 +153,17 @@ export class Session {
152
153
  } catch(err) { throw await this.errorHandler(err) }
153
154
  }
154
155
 
155
- UPLOAD = async (presigned: S3PresignedPost, file?: FileBlob, buffer?: FileBlob) => {
156
- const formData = IN_BROWSER ? new FormData() : new NodeFormData();
156
+ UPLOAD = async (presigned: S3PresignedPost, file: Blob | Buffer | ReactNativeFile) => {
157
+ const formData = new NodeFormData();
157
158
  Object.keys(presigned.fields).forEach(key => {
158
- formData.append(key, presigned.fields[key]);
159
+ formData.append(key, presigned.fields[key as keyof S3PresignedPost['fields']]);
159
160
  });
161
+ formData.append("file", file) // must be appended last
160
162
 
161
- // has to be appended last
162
- if (!IN_BROWSER) file = buffer || file // use raw buffer if file is Node Blob
163
- formData.append("file", file as Blob)
163
+ const headers = formData.getHeaders ? { ...formData.getHeaders(), 'Content-Length' : (formData as any).maxDataSize }
164
+ : { 'Content-Type': 'multipart/form-data' }
164
165
 
165
- try {
166
- await axios.post(presigned.url, formData, {
167
- headers: IN_BROWSER
168
- ? { 'Content-Type': 'multipart/form-data'}
169
- : {
170
- ...(formData as NodeFormData).getHeaders(),
171
- 'Content-Length' : (formData as any).maxDataSize
172
- }
173
- })
174
- }
175
- catch(err) { console.error(err); throw err }
166
+ await axios.post(presigned.url, formData, { headers })
176
167
  }
177
168
 
178
169
  DOWNLOAD = async (downloadURL: string) => {
@@ -181,6 +172,9 @@ export class Session {
181
172
  }
182
173
 
183
174
  EMIT = async (route: string, args: object, authenticated=true, options={} as RequestOptions) => {
175
+ if (!this.socket) {
176
+ this.authenticate_socket() // sets namespace correctly
177
+ }
184
178
  this.socket?.emit(route, { ...args, ...authenticated ? { authToken: this.authToken } : {} } )
185
179
  }
186
180
 
@@ -199,6 +193,7 @@ export class Session {
199
193
  removeAllSocketListeners = (s: string) => this.socket?.removeAllListeners(s)
200
194
 
201
195
  authenticate_socket = () => {
196
+ if (!this.userInfo?.businessId) return
202
197
  this.socket = io(`${this.host}/${this.userInfo.businessId}`, { transports: ['websocket'] }); // supporting polling requires sticky session at load balancer
203
198
  this.socket.on('disconnect', () => { this.socketAuthenticated = false })
204
199
  this.socket.on('authenticated', () => { this.socketAuthenticated = true })
@@ -49,6 +49,7 @@ import {
49
49
  const host = process.env.TEST_URL || 'http://localhost:8080'
50
50
  const [email, password] = [process.env.TEST_EMAIL, process.env.TEST_PASSWORD]
51
51
  const [email2, password2] = [process.env.TEST_EMAIL_2, process.env.TEST_PASSWORD_2]
52
+ const [nonAdminEmail, nonAdminPassword] = [process.env.NON_ADMIN_EMAIL, process.env.NON_ADMIN_PASSWORD]
52
53
 
53
54
  const userId = '60398b0231a295e64f084fd9'
54
55
 
@@ -73,10 +74,11 @@ const userId = '60398b0231a295e64f084fd9'
73
74
 
74
75
  const sdk = new Session({ host })
75
76
  const sdkOther = new Session({ host, apiKey: "ba745e25162bb95a795c5fa1af70df188d93c4d3aac9c48b34a5c8c9dd7b80f7" })
77
+ const sdkNonAdmin = new Session({ host })
76
78
  const enduserSDK = new EnduserSession({ host })
77
79
  // const sdkOtherEmail = "sebass@tellescope.com"
78
80
 
79
- if (!(email && password && email2 && password2)) {
81
+ if (!(email && password && email2 && password2 && nonAdminEmail && nonAdminPassword)) {
80
82
  console.error("Set TEST_EMAIL and TEST_PASSWORD")
81
83
  process.exit()
82
84
  }
@@ -293,8 +295,13 @@ const generateEnduserAuthTests = async () => {
293
295
 
294
296
  const { authToken, enduser } = await sdk.api.endusers.generate_auth_token({ id: e.id })
295
297
  assert(!!authToken && !!enduser, 'invalid returned values', 'Generate authToken and get enduser')
296
- const { isAuthenticated } = await sdk.api.endusers.is_authenticated({ id: enduser.id, authToken })
298
+ let { isAuthenticated } = await sdk.api.endusers.is_authenticated({ id: enduser.id, authToken })
297
299
  assert(isAuthenticated, 'invalid authToken generated for enduser', 'Generate authToken for enduser is valid')
300
+ assert(
301
+ (await sdk.api.endusers.is_authenticated({ authToken })).isAuthenticated,
302
+ 'id omitted results in failed authentication',
303
+ 'id optional for is_authenticated'
304
+ )
298
305
 
299
306
  const { authToken: authTokenUID, enduser: enduser2 } = await sdk.api.endusers.generate_auth_token({ externalId })
300
307
  assert(!!authTokenUID && !!enduser2, 'invalid returned values eid', 'Generate authToken and get enduser eid')
@@ -967,6 +974,7 @@ const enduserAccessTests = async () => {
967
974
  const password = 'testpassword'
968
975
 
969
976
  const enduser = await sdk.api.endusers.createOne({ email })
977
+ const enduser2 = await sdk.api.endusers.createOne({ email: 'hi' + email })
970
978
  await sdk.api.endusers.set_password({ id: enduser.id, password }).catch(console.error)
971
979
  await enduserSDK.authenticate(email, password).catch(console.error)
972
980
 
@@ -1019,6 +1027,17 @@ const enduserAccessTests = async () => {
1019
1027
  }
1020
1028
  }
1021
1029
 
1030
+ await async_test(
1031
+ `enduser can update self`,
1032
+ () => enduserSDK.api.endusers.updateOne(enduser.id, { fname: "Sebastian", lname: "Coates" }),
1033
+ { onResult: e => e.id === enduser.id && e.fname === 'Sebastian' && e.lname === "Coates" }
1034
+ )
1035
+ await async_test(
1036
+ `enduser can't update other enduser`,
1037
+ () => enduserSDK.api.endusers.updateOne(enduser2.id, { fname: "Shouldn't Work"}),
1038
+ { shouldError: true, onError: e => e.message === "Could not find a record for the given id" }
1039
+ )
1040
+
1022
1041
  const ticketAccessible = await sdk.api.tickets.createOne({ enduserId: enduser.id, title: "Accessible ticket" })
1023
1042
  const ticketInaccessible = await sdk.api.tickets.createOne({ enduserId: PLACEHOLDER_ID, title: "Inaccessible ticket" })
1024
1043
  await async_test(
@@ -1045,6 +1064,7 @@ const enduserAccessTests = async () => {
1045
1064
  await sdk.api.tickets.deleteOne(ticketAccessible.id)
1046
1065
  await sdk.api.tickets.deleteOne(ticketInaccessible.id)
1047
1066
  await sdk.api.endusers.deleteOne(enduser.id)
1067
+ await sdk.api.endusers.deleteOne(enduser2.id)
1048
1068
  }
1049
1069
 
1050
1070
  const files_tests = async () => {
@@ -1053,7 +1073,7 @@ const files_tests = async () => {
1053
1073
  const { presignedUpload, file } = await sdk.api.files.prepare_file_upload({
1054
1074
  name: 'Test File', size: buff.byteLength, type: 'text/plain'
1055
1075
  })
1056
- await sdk.UPLOAD(presignedUpload, undefined, buff as any)
1076
+ await sdk.UPLOAD(presignedUpload, buff)
1057
1077
 
1058
1078
  const { downloadURL } = await sdk.api.files.file_download_URL({ secureName: file.secureName })
1059
1079
  const downloaded: string = await sdk.DOWNLOAD(downloadURL)
@@ -1075,6 +1095,44 @@ const enduser_session_tests = async () => {
1075
1095
  await sdk.api.endusers.deleteOne(enduser.id)
1076
1096
  }
1077
1097
 
1098
+ const users_tests = async () => {
1099
+ log_header("Users Tests")
1100
+ /* Update user tests */
1101
+ await async_test(
1102
+ `update user (non-admin, other user)`,
1103
+ () => sdkNonAdmin.api.users.updateOne(sdk.userInfo.id, { fname: 'Failure' }),
1104
+ { shouldError: true, onError: e => e.message === "Only admin users can update others' profiles" }
1105
+ )
1106
+ await async_test(
1107
+ `update user (non-admin, self)`,
1108
+ () => sdkNonAdmin.api.users.updateOne(sdkNonAdmin.userInfo.id, { fname: 'Updated' }),
1109
+ { onResult: u => u.id === sdkNonAdmin.userInfo.id && u.fname === "Updated" }
1110
+ )
1111
+ await async_test(
1112
+ `verify user update with admin get`,
1113
+ () => sdk.api.users.getOne(sdkNonAdmin.userInfo.id),
1114
+ { onResult: u => u.id === sdkNonAdmin.userInfo.id && u.fname === "Updated" }
1115
+ )
1116
+
1117
+ // reset fname to "Non" if this test throws, otherwise will falsely pass on next run
1118
+ // NOT Supported behavior any more
1119
+ // assert(sdkNonAdmin.userInfo.fname === 'Updated', 'refresh_session not called on self after update', 'sdk updated on user update')
1120
+
1121
+ await async_test(
1122
+ `update user (admin, other user)`,
1123
+ () => sdk.api.users.updateOne(sdkNonAdmin.userInfo.id, { fname: 'Non' }), // change back
1124
+ { onResult: u => u.id === sdkNonAdmin.userInfo.id && u.fname === "Non" }
1125
+ )
1126
+ // sdkNonAdmin.userInfo.fname = 'Non' // update back in sdk instance as well
1127
+
1128
+ await async_test(
1129
+ `verify user update with admin get`,
1130
+ () => sdk.api.users.getOne(sdkNonAdmin.userInfo.id),
1131
+ { onResult: u => u.id === sdkNonAdmin.userInfo.id && u.fname === "Non" }
1132
+ )
1133
+
1134
+ }
1135
+
1078
1136
  const tests: { [K in keyof ClientModelForName]: () => void } = {
1079
1137
  chats: chat_tests,
1080
1138
  endusers: enduser_tests,
@@ -1085,7 +1143,7 @@ const tests: { [K in keyof ClientModelForName]: () => void } = {
1085
1143
  emails: email_tests,
1086
1144
  sms_messages: sms_tests,
1087
1145
  chat_rooms: chat_room_tests,
1088
- users: () => {},
1146
+ users: users_tests,
1089
1147
  templates: () => {},
1090
1148
  files: files_tests,
1091
1149
  tickets: () => {},
@@ -1098,7 +1156,10 @@ const tests: { [K in keyof ClientModelForName]: () => void } = {
1098
1156
  log_header("API")
1099
1157
 
1100
1158
  try {
1101
- await sdk.authenticate(email, password)
1159
+ await Promise.all([
1160
+ sdk.authenticate(email, password),
1161
+ sdkNonAdmin.authenticate(nonAdminEmail, nonAdminPassword),
1162
+ ])
1102
1163
  await setup_tests()
1103
1164
  await multi_tenant_tests() // should come right after setup tests
1104
1165
  await badInputTests()