@tellescope/sdk 0.0.5 → 0.0.9

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.
Files changed (45) hide show
  1. package/lib/cjs/enduser.d.ts +346 -13
  2. package/lib/cjs/enduser.d.ts.map +1 -1
  3. package/lib/cjs/enduser.js +135 -14
  4. package/lib/cjs/enduser.js.map +1 -1
  5. package/lib/cjs/sdk.d.ts +107 -20
  6. package/lib/cjs/sdk.d.ts.map +1 -1
  7. package/lib/cjs/sdk.js +116 -32
  8. package/lib/cjs/sdk.js.map +1 -1
  9. package/lib/cjs/session.d.ts +37 -176
  10. package/lib/cjs/session.d.ts.map +1 -1
  11. package/lib/cjs/session.js +131 -34
  12. package/lib/cjs/session.js.map +1 -1
  13. package/lib/cjs/tests/public_endpoint_tests.js +3 -3
  14. package/lib/cjs/tests/public_endpoint_tests.js.map +1 -1
  15. package/lib/cjs/tests/socket_tests.js +42 -31
  16. package/lib/cjs/tests/socket_tests.js.map +1 -1
  17. package/lib/cjs/tests/tests.js +232 -63
  18. package/lib/cjs/tests/tests.js.map +1 -1
  19. package/lib/esm/enduser.d.ts +346 -13
  20. package/lib/esm/enduser.d.ts.map +1 -1
  21. package/lib/esm/enduser.js +132 -12
  22. package/lib/esm/enduser.js.map +1 -1
  23. package/lib/esm/sdk.d.ts +107 -20
  24. package/lib/esm/sdk.d.ts.map +1 -1
  25. package/lib/esm/sdk.js +116 -32
  26. package/lib/esm/sdk.js.map +1 -1
  27. package/lib/esm/session.d.ts +37 -176
  28. package/lib/esm/session.d.ts.map +1 -1
  29. package/lib/esm/session.js +130 -32
  30. package/lib/esm/session.js.map +1 -1
  31. package/lib/esm/tests/public_endpoint_tests.js +3 -3
  32. package/lib/esm/tests/public_endpoint_tests.js.map +1 -1
  33. package/lib/esm/tests/socket_tests.js +41 -30
  34. package/lib/esm/tests/socket_tests.js.map +1 -1
  35. package/lib/esm/tests/tests.js +228 -64
  36. package/lib/esm/tests/tests.js.map +1 -1
  37. package/lib/tsconfig.tsbuildinfo +1 -1
  38. package/package.json +11 -9
  39. package/src/enduser.ts +93 -20
  40. package/src/sdk.ts +71 -43
  41. package/src/session.ts +122 -34
  42. package/src/tests/public_endpoint_tests.ts +6 -4
  43. package/src/tests/socket_tests.ts +37 -16
  44. package/src/tests/tests.ts +147 -25
  45. package/tsconfig.json +0 -1
package/src/enduser.ts CHANGED
@@ -1,45 +1,108 @@
1
1
  import { io } from 'socket.io-client'
2
2
 
3
- import { Session, SessionOptions, APIQuery, defaultQueries } from "./session"
4
- import {
5
- PublicEndpoints,
6
- } from "./public"
3
+ import { Session, SessionOptions, APIQuery } from "./session"
4
+ import { url_safe_path } from "@tellescope/utilities"
7
5
 
6
+ import { S3PresignedPost } from "@tellescope/types-utilities"
8
7
  import {
9
8
  ClientModelForName,
9
+ ClientModelForName_required,
10
10
  Enduser,
11
+ File,
11
12
  } from "@tellescope/types-client"
12
13
  import { stringValidator } from "@tellescope/validation";
13
14
 
14
15
  export interface EnduserSessionOptions extends SessionOptions {}
15
16
 
16
- type EnduserAccessibleModels = "chat_rooms" | 'chats'
17
+ type EnduserAccessibleModels = "chat_rooms" | 'chats' | 'files'
18
+
19
+ export const defaultQueries = <N extends keyof ClientModelForName>(
20
+ s: EnduserSession, n: keyof ClientModelForName_required
21
+ ): APIQuery<N> => {
17
22
 
18
- type Queries = { [K in EnduserAccessibleModels]: APIQuery<K> }
23
+ const safeName = url_safe_path(n)
24
+ const singularName = (safeName).substring(0, safeName.length - 1)
19
25
 
20
- const loadDefaultQueries = (s: Session): { [K in EnduserAccessibleModels] : APIQuery<K> } => ({
26
+ return {
27
+ createOne: o => s._POST(`/v1/${singularName}`, o),
28
+ createSome: os => s._POST(`/v1/${safeName}`, { create: os }),
29
+ getOne: (id, filter) => s._GET(`/v1/${singularName}/${id}`, { filter }),
30
+ getSome: (o) => s._GET(`/v1/${safeName}`, o),
31
+ updateOne: (id, updates, options) => s._PATCH(`/v1/${singularName}/${id}`, { updates, options }),
32
+ deleteOne: id => s._DELETE(`/v1/${singularName}/${id}`),
33
+ }
34
+ }
35
+
36
+ type EnduserQueries = { [K in EnduserAccessibleModels]: APIQuery<K> } & {
37
+ endusers: {
38
+ logout: () => Promise<void>;
39
+ },
40
+ users: {
41
+ display_names: () => Promise<{ fname: string, lname: string, id: string }[]>
42
+ },
43
+ files: {
44
+ prepare_file_upload: (args: { name: string, size: number, type: string }) => Promise<{ presignedUpload: S3PresignedPost, file: File }>,
45
+ file_download_URL: (args: { secureName: string }) => Promise<{ downloadURL: string }>,
46
+ },
47
+ }
48
+
49
+
50
+ const loadDefaultQueries = (s: EnduserSession): { [K in EnduserAccessibleModels] : APIQuery<K> } => ({
21
51
  chat_rooms: defaultQueries(s, 'chat_rooms'),
22
52
  chats: defaultQueries(s, 'chats'),
53
+ files: defaultQueries(s, 'files'),
23
54
  })
24
55
 
25
56
 
26
57
  export class EnduserSession extends Session {
27
- session = new Session();
28
- enduser: Enduser;
29
- api: Queries;
58
+ userInfo!: Enduser;
59
+ api: EnduserQueries;
30
60
 
31
61
  constructor(o?: EnduserSessionOptions) {
32
- super(o)
33
- this.enduser = {} as Enduser
62
+ super({ ...o, cacheKey: o?.cacheKey || "tellescope_enduser" })
63
+
64
+ this.api = loadDefaultQueries(this) as EnduserQueries
65
+
66
+ this.api.endusers = {
67
+ logout: () => this._POST('/v1/logout-enduser'),
68
+ }
69
+ this.api.users = {
70
+ display_names: () => this._GET<{}, { fname: string, lname: string, id: string }[] >(`/v1/user-display-names`),
71
+ }
72
+
73
+ // files have defaultQueries
74
+ this.api.files.prepare_file_upload = a => this._POST(`/v1/prepare-file-upload`, a)
75
+ this.api.files.file_download_URL = a => this._GET('/v1/file-download-URL', a)
76
+
77
+ // if (this.authToken) this.refresh_session()
78
+ }
34
79
 
35
- this.api = loadDefaultQueries(this) as Queries
80
+ _POST = async <A,R=void>(endpoint: string, args?: A, authenticated=true) => {
81
+ await this.refresh_session_if_expiring_soon()
82
+ return await this.POST<A,R>(endpoint, args, authenticated)
83
+ }
84
+
85
+ _GET = async <A,R=void>(endpoint: string, params?: A, authenticated=true) => {
86
+ await this.refresh_session_if_expiring_soon()
87
+ return await this.GET<A,R>(endpoint, params, authenticated)
88
+ }
89
+
90
+ _PATCH = async <A,R=void>(endpoint: string, params?: A, authenticated=true) => {
91
+ await this.refresh_session_if_expiring_soon()
92
+ return await this.PATCH<A,R>(endpoint, params, authenticated)
93
+ }
94
+
95
+ _DELETE = async <A,R=void>(endpoint: string, args?: A, authenticated=true) => {
96
+ await this.refresh_session_if_expiring_soon()
97
+ return await this.DELETE<A,R>(endpoint, args, authenticated)
36
98
  }
37
99
 
38
100
  handle_new_session = async ({ authToken, enduser }: { authToken: string, enduser: Enduser }) => {
101
+ this.sessionStart = Date.now()
39
102
  this.setAuthToken(authToken)
40
- this.enduser = enduser
103
+ this.setUserInfo(enduser)
41
104
 
42
- this.socket = io(`${this.host}/${enduser.businessId}/${enduser.id}`, { transports: ['websocket'] }); // supporting polling requires sticky session at load balancer
105
+ this.socket = io(`${this.host}/${enduser.businessId}`, { transports: ['websocket'] }); // supporting polling requires sticky session at load balancer
43
106
  this.socket.on('disconnect', () => { this.socketAuthenticated = false })
44
107
  this.socket.on('authenticated', () => { this.socketAuthenticated = true })
45
108
 
@@ -52,11 +115,21 @@ export class EnduserSession extends Session {
52
115
  await this.POST<{email: string, password: string }, { authToken: string, enduser: Enduser }>('/v1/login-enduser', { email, password })
53
116
  )
54
117
 
55
- subscribe = (rooms: { [index: string]: keyof ClientModelForName } ) => this.EMIT('join-rooms', { rooms })
118
+ refresh_session = async () => {
119
+ const { enduser, authToken } = await this.POST<{}, { enduser: Enduser } & { authToken: string }>('/v1/refresh-enduser-session')
120
+ return this.handle_new_session({ authToken, enduser })
121
+ }
56
122
 
57
- handle_events = ( handlers: { [index: string]: (a: any) => void } ) => {
58
- for (const handler in handlers) this.ON(handler, handlers[handler])
59
- }
123
+ refresh_session_if_expiring_soon = async () => {
124
+ const elapsedSessionMS = Date.now() - (this.sessionStart || Date.now())
125
+
126
+ if (this.AUTO_REFRESH_MS < elapsedSessionMS) {
127
+ return await this.refresh_session()
128
+ }
129
+ }
60
130
 
61
- unsubscribe = (roomIds: string[]) => this.EMIT('leave-rooms', { roomIds })
131
+ logout = async () => {
132
+ this.clearState()
133
+ await this.api.endusers.logout().catch(console.error)
134
+ }
62
135
  }
package/src/sdk.ts CHANGED
@@ -1,5 +1,3 @@
1
- import { io } from 'socket.io-client'
2
-
3
1
  import {
4
2
  JourneyState,
5
3
  UserSession,
@@ -11,7 +9,9 @@ import {
11
9
  ClientModelForName_required,
12
10
  ClientModelForName_updatesDisabled,
13
11
  Enduser,
12
+ File,
14
13
  } from "@tellescope/types-client"
14
+ import { CustomUpdateOptions, SortOption, S3PresignedPost } from "@tellescope/types-utilities"
15
15
  import { url_safe_path } from "@tellescope/utilities"
16
16
 
17
17
  import { Session as SessionManager, SessionOptions, Filter } from "./session"
@@ -30,29 +30,29 @@ export interface APIQuery<
30
30
  createOne: (t: CREATE) => Promise<T>;
31
31
  createSome: (ts: CREATE[]) => Promise<{ created: T[], errors: object[] }>;
32
32
  getOne: (id: string, filter?: Filter<Partial<T>>) => Promise<T>;
33
- getSome: (o?: { lastId?: string, limit?: number, sort?: SortOption, threadKey?: string }, f?: Filter<Partial<T>>) => Promise<T[]>
34
- updateOne: (id: string, updates: UPDATE, options?: CustomUpdateOptions) => Promise<void>;
33
+ getSome: (o?: { lastId?: string, limit?: number, sort?: SortOption, threadKey?: string, filter?: Filter<Partial<T>> }) => Promise<T[]>
34
+ updateOne: (id: string, updates: UPDATE, options?: CustomUpdateOptions) => Promise<T>;
35
35
  deleteOne: (id: string) => Promise<void>;
36
36
  }
37
37
 
38
38
  export const defaultQueries = <N extends keyof ClientModelForName>(
39
- s: SessionManager, n: keyof ClientModelForName_required
39
+ s: Session, n: keyof ClientModelForName_required
40
40
  ): APIQuery<N> => {
41
41
 
42
42
  const safeName = url_safe_path(n)
43
43
  const singularName = (safeName).substring(0, safeName.length - 1)
44
44
 
45
45
  return {
46
- createOne: o => s.POST(`/v1/${singularName}`, o),
47
- createSome: os => s.POST(`/v1/${safeName}`, { create: os }),
48
- getOne: (id, filter) => s.GET(`/v1/${singularName}/${id}`, { filter }),
49
- getSome: (o, filter) => s.GET(`/v1/${safeName}`, { ...o, filter }),
50
- updateOne: (id, updates, options) => s.PATCH(`/v1/${singularName}/${id}`, { updates, options }),
51
- deleteOne: id => s.DELETE(`/v1/${singularName}/${id}`),
46
+ createOne: o => s._POST(`/v1/${singularName}`, o),
47
+ createSome: os => s._POST(`/v1/${safeName}`, { create: os }),
48
+ getOne: (id, filter) => s._GET(`/v1/${singularName}/${id}`, { filter }),
49
+ getSome: (o) => s._GET(`/v1/${safeName}`, o),
50
+ updateOne: (id, updates, options) => s._PATCH(`/v1/${singularName}/${id}`, { updates, options }),
51
+ deleteOne: id => s._DELETE(`/v1/${singularName}/${id}`),
52
52
  }
53
53
  }
54
54
 
55
- const loadDefaultQueries = (s: SessionManager): { [K in keyof ClientModelForName] : APIQuery<K> } => ({
55
+ const loadDefaultQueries = (s: Session): { [K in keyof ClientModelForName] : APIQuery<K> } => ({
56
56
  endusers: defaultQueries(s, 'endusers'),
57
57
  engagement_events: defaultQueries(s, 'engagement_events'),
58
58
  journeys: defaultQueries(s, 'journeys'),
@@ -63,52 +63,86 @@ const loadDefaultQueries = (s: SessionManager): { [K in keyof ClientModelForName
63
63
  chat_rooms: defaultQueries(s, 'chat_rooms'),
64
64
  chats: defaultQueries(s, 'chats'),
65
65
  users: defaultQueries(s, 'users'),
66
- templates: defaultQueries(s, 'templates') ,
66
+ templates: defaultQueries(s, 'templates'),
67
+ files: defaultQueries(s, 'files'),
67
68
  })
68
69
 
69
70
  type Queries = { [K in keyof ClientModelForName]: APIQuery<K> } & {
70
71
  journeys: {
71
- updateState: (id: string, name: string, updates: JourneyState) => Promise<void>
72
+ updateState: (args: { id: string, name: string, updates: JourneyState }) => Promise<void>
72
73
  },
73
74
  endusers: {
74
- setPassword: (id: string, password: string) => Promise<void>,
75
- isAuthenticated: (id: string, authToken: string) => Promise<{ isAuthenticated: boolean, enduser: Enduser }>
76
- }
75
+ setPassword: (args: { id: string, password: string }) => Promise<void>,
76
+ isAuthenticated: (args: { id: string, authToken: string }) => Promise<{ isAuthenticated: boolean, enduser: Enduser }>
77
+ },
78
+ users: {
79
+ display_names: () => Promise<{ fname: string, lname: string, id: string }[]>,
80
+ },
81
+ files: {
82
+ prepare_file_upload: (args: { name: string, size: number, type: string }) => Promise<{ presignedUpload: S3PresignedPost, file: File }>,
83
+ file_download_URL: (args: { secureName: string }) => Promise<{ downloadURL: string }>,
84
+ },
77
85
  }
78
86
 
79
- export class Session extends SessionManager{
87
+ export class Session extends SessionManager {
80
88
  api: Queries;
81
- userInfo: UserSession;
89
+ userInfo!: UserSession;
82
90
 
83
91
  constructor(o?: SessionOptions) {
84
- super(o)
85
- this.userInfo = {} as UserSession
92
+ super({ ...o, cacheKey: o?.cacheKey || "tellescope_user" })
86
93
  const queries = loadDefaultQueries(this) as Queries
87
94
 
88
- queries.journeys.updateState = (id, name, updates) => this.PATCH(`/v1/journey/${id}/state/${name}`, { updates })
89
- queries.endusers.setPassword = (id, password) => this.POST(`/v1/set-enduser-password`, { id, password })
90
- queries.endusers.isAuthenticated = (id, authToken) => this.GET(`/v1/enduser-is-authenticated`, { id, authToken })
95
+ queries.journeys.updateState = ({id, name, updates}) => this._PATCH(`/v1/journey/${id}/state/${name}`, { updates })
96
+ queries.endusers.setPassword = ({id, password}) => this._POST(`/v1/set-enduser-password`, { id, password })
97
+ queries.endusers.isAuthenticated = ({id, authToken}) => this._GET(`/v1/enduser-is-authenticated`, { id, authToken })
98
+ queries.users.display_names = () => this._GET<{}, { fname: string, lname: string, id: string }[]>(`/v1/user-display-names`),
99
+ queries.files.prepare_file_upload = (args) => this._POST(`/v1/prepare-file-upload`, args),
100
+ queries.files.file_download_URL = a => this._GET('/v1/file-download-URL', a),
91
101
 
92
102
  this.api = queries
103
+
104
+ // if (this.userInfo) this.refresh_session()
93
105
  }
94
106
 
107
+ _POST = async <A,R=void>(endpoint: string, args?: A, authenticated=true) => {
108
+ await this.refresh_session_if_expiring_soon()
109
+ return await this.POST<A,R>(endpoint, args, authenticated)
110
+ }
95
111
 
96
- handle_new_session = async ({ authToken, ...userInfo }: UserSession & { authToken: string }) => {
97
- this.setAuthToken(authToken)
98
- this.userInfo = userInfo
112
+ _GET = async <A,R=void>(endpoint: string, params?: A, authenticated=true) => {
113
+ await this.refresh_session_if_expiring_soon()
114
+ return await this.GET<A,R>(endpoint, params, authenticated)
115
+ }
99
116
 
100
- this.socket = io(`${this.host}/${userInfo.organization}`, { transports: ['websocket'] }); // supporting polling requires sticky session at load balancer
101
- this.socket.on('disconnect', () => { this.socketAuthenticated = false })
102
- this.socket.on('authenticated', () => { this.socketAuthenticated = true })
117
+ _PATCH = async <A,R=void>(endpoint: string, params?: A, authenticated=true) => {
118
+ await this.refresh_session_if_expiring_soon()
119
+ return await this.PATCH<A,R>(endpoint, params, authenticated)
120
+ }
103
121
 
104
- this.socket.emit('authenticate', authToken)
122
+ _DELETE = async <A,R=void>(endpoint: string, args?: A, authenticated=true) => {
123
+ await this.refresh_session_if_expiring_soon()
124
+ return await this.DELETE<A,R>(endpoint, args, authenticated)
125
+ }
126
+
127
+ handle_new_session = async ({ authToken, ...userInfo }: UserSession & { authToken: string }) => {
128
+ this.sessionStart = Date.now()
129
+ this.setAuthToken(authToken)
130
+ this.setUserInfo(userInfo)
131
+ this.authenticate_socket()
105
132
 
106
133
  return { authToken, ...userInfo }
107
134
  }
108
135
 
109
136
  refresh_session = async () => {
110
- const { userInfo, authToken } = await this.GET<{}, { userInfo: UserSession } & { authToken: string }>('/refresh-session')
111
- await this.handle_new_session({ ...userInfo, authToken })
137
+ console.log('refreshing session')
138
+ const { user, authToken } = await this.POST<{}, { user: UserSession } & { authToken: string }>('/v1/refresh-session')
139
+ await this.handle_new_session({ ...user, authToken })
140
+ return { user, authToken }
141
+ }
142
+ refresh_session_if_expiring_soon = async () => {
143
+ const elapsedSessionMS = Date.now() - (this.sessionStart || Date.now())
144
+
145
+ if (this.AUTO_REFRESH_MS < elapsedSessionMS) { return await this.refresh_session()}
112
146
  }
113
147
 
114
148
  authenticate = async (email: string, password: string, url?: string) => {
@@ -118,17 +152,11 @@ export class Session extends SessionManager{
118
152
  await this.POST<{email: string, password: string }, UserSession & { authToken: string }>('/submit-login', { email, password })
119
153
  )
120
154
  }
155
+ logout = async () => {
156
+ this.clearState()
157
+ await this.POST('/logout-api').catch(console.error)
158
+ }
121
159
 
122
- subscribe = (rooms: { [index: string]: keyof ClientModelForName } ) => this.EMIT('join-rooms', { rooms })
123
-
124
- handle_events = ( handlers: { [index: string]: (a: any) => void } ) => {
125
- for (const handler in handlers) this.ON(handler, handlers[handler])
126
- }
127
-
128
- unsubscribe = (roomIds: string[]) => this.EMIT('leave-rooms', { roomIds })
129
-
130
- socket_is_authenticated = () => this.socketAuthenticated
131
- logout = () => this.POST('/logout-api')
132
160
  reset_db = () => this.POST('/reset-demo')
133
161
  test_online = () => this.GET<{}, string>('/v1')
134
162
  test_authenticated = () => this.GET<{}, string>('/v1/test-authenticated')
package/src/session.ts CHANGED
@@ -1,9 +1,19 @@
1
1
  import axios from "axios"
2
- import { Socket } from 'socket.io-client'
3
-
4
- import { } from "@tellescope/types-models"
5
- import { ClientModelForName_required, ClientModelForName_readonly, ClientModelForName_updatesDisabled } from "@tellescope/types-client"
6
- import { url_safe_path } from "@tellescope/utilities"
2
+ import NodeFormData from "form-data"
3
+ import { Socket, io } from 'socket.io-client'
4
+
5
+ import {
6
+ CustomUpdateOptions,
7
+ FileBlob,
8
+ SortOption,
9
+ S3PresignedPost,
10
+ } from "@tellescope/types-utilities"
11
+ import {
12
+ ClientModelForName,
13
+ ClientModelForName_required,
14
+ ClientModelForName_readonly,
15
+ ClientModelForName_updatesDisabled
16
+ } from "@tellescope/types-client"
7
17
 
8
18
  export const DEFAULT_HOST = 'https://api.tellescope.com'
9
19
 
@@ -11,6 +21,12 @@ export interface SessionOptions {
11
21
  apiKey?: string;
12
22
  authToken?: string;
13
23
  host?: string;
24
+ cacheKey?: string;
25
+ handleUnauthenticated?: () => Promise<void>;
26
+ }
27
+
28
+ interface RequestOptions {
29
+ refresh_session?: () => Promise<any>,
14
30
  }
15
31
 
16
32
  export type Filter<T> = { [K in keyof T]: T[K] }
@@ -27,27 +43,10 @@ export interface APIQuery<
27
43
  createSome: (ts: CREATE[]) => Promise<{ created: T[], errors: object[] }>;
28
44
  getOne: (id: string, filter?: Filter<Partial<T>>) => Promise<T>;
29
45
  getSome: (o?: { lastId?: string, limit?: number, sort?: SortOption, threadKey?: string }, f?: Filter<Partial<T>>) => Promise<T[]>
30
- updateOne: (id: string, updates: UPDATE, options?: CustomUpdateOptions) => Promise<void>;
46
+ updateOne: (id: string, updates: UPDATE, options?: CustomUpdateOptions) => Promise<T>;
31
47
  deleteOne: (id: string) => Promise<void>;
32
48
  }
33
49
 
34
- export const defaultQueries = <N extends keyof ClientModelForName>(
35
- s: Session, n: keyof ClientModelForName_required
36
- ): APIQuery<N> => {
37
-
38
- const safeName = url_safe_path(n)
39
- const singularName = (safeName).substring(0, safeName.length - 1)
40
-
41
- return {
42
- createOne: o => s.POST(`/v1/${singularName}`, o),
43
- createSome: os => s.POST(`/v1/${safeName}`, { create: os }),
44
- getOne: (id, filter) => s.GET(`/v1/${singularName}/${id}`, { filter }),
45
- getSome: (o, filter) => s.GET(`/v1/${safeName}`, { ...o, filter }),
46
- updateOne: (id, updates, options) => s.PATCH(`/v1/${singularName}/${id}`, { updates, options }),
47
- deleteOne: id => s.DELETE(`/v1/${singularName}/${id}`),
48
- }
49
- }
50
-
51
50
  const generateBearer = (authToken: string) => `Bearer ${authToken}`
52
51
 
53
52
  const parseError = (err: any) => {
@@ -59,32 +58,80 @@ const parseError = (err: any) => {
59
58
  return err
60
59
  }
61
60
 
61
+ const DEFAULT_AUTHTOKEN_KEY = 'tellescope_authToken'
62
+ const IN_BROWSER = typeof window !== 'undefined'
63
+ const has_local_storage = () => typeof window !== 'undefined' && !!window.localStorage
64
+ const set_cache = (key: string, authToken: string) => has_local_storage() && (window.localStorage[key] = authToken)
65
+ const access_cache = (key=DEFAULT_AUTHTOKEN_KEY) => has_local_storage() ? window.localStorage[key] : undefined
66
+
62
67
  export class Session {
63
68
  host: string;
64
69
  authToken: string;
70
+ cacheKey: string;
65
71
  apiKey?: string;
66
72
  socket?: Socket;
73
+ handleUnauthenticated?: SessionOptions['handleUnauthenticated']
67
74
  socketAuthenticated: boolean;
75
+ userInfo: { businessId?: string };
76
+ sessionStart = Date.now();
77
+ AUTO_REFRESH_MS = 3600000 // 1hr elapsed
68
78
 
69
79
  config: { headers: { Authorization: string }};
70
80
 
71
- constructor(o={} as SessionOptions) {
81
+ constructor(o={} as SessionOptions & RequestOptions) {
72
82
  this.host= o.host ?? DEFAULT_HOST
73
- this.authToken = o.authToken ?? '';
74
- this.config = { headers: { Authorization: generateBearer(o.authToken ?? '') } }
75
83
  this.apiKey = o.apiKey ?? '';
76
84
  this.socket = undefined as Socket | undefined
77
85
  this.socketAuthenticated = false
86
+ this.handleUnauthenticated = o.handleUnauthenticated
87
+
88
+ this.cacheKey = o.cacheKey || DEFAULT_AUTHTOKEN_KEY
89
+ this.authToken = o.authToken ?? access_cache(o.cacheKey) ?? '';
90
+ this.userInfo = JSON.parse(access_cache(o.cacheKey + 'userInfo') || '{}');
91
+ if (this.authToken) {
92
+ set_cache(this.cacheKey, this.authToken)
93
+ this.authenticate_socket()
94
+ }
95
+ this.config = { headers: { Authorization: generateBearer(this.authToken ?? '') } } // initialize after authToken
78
96
  }
97
+
98
+ resolve_field = async <T>(p: () => Promise<T>, field: keyof T) => (await p())[field]
79
99
 
80
100
  setAuthToken = (a: string) => {
81
101
  this.authToken = a;
82
102
  this.config.headers.Authorization = generateBearer(a);
103
+ set_cache(this.cacheKey, a)
83
104
  }
84
- // setApiKey = (k: string) => apiKey = k
85
105
 
86
- getAuthInfo = (requiresAuth?: boolean) => requiresAuth && this.apiKey ? { apiKey: this.apiKey } : { }
106
+ setUserInfo = (u: { businessId: string }) => {
107
+ this.userInfo = u;
108
+ set_cache(this.cacheKey + 'userInfo', JSON.stringify(u))
109
+ }
110
+
111
+ clearCache = () => {
112
+ set_cache(this.cacheKey, '')
113
+ set_cache(this.cacheKey + 'userInfo', '')
114
+ }
115
+
116
+ clearState = () => {
117
+ this.apiKey = ''
118
+ this.authToken = ''
119
+ this.userInfo = { }
120
+ this.clearCache()
121
+ }
87
122
 
123
+ getAuthInfo = (requiresAuth?: boolean) => requiresAuth && this.apiKey ? { apiKey: this.apiKey } : { }
124
+
125
+ errorHandler = async (_err: any) => {
126
+ const err = parseError(_err)
127
+ if (err === 'Unauthenticated') {
128
+ this.authToken = ''
129
+ this.clearCache()
130
+ await this.handleUnauthenticated?.()
131
+ }
132
+
133
+ return err
134
+ }
88
135
  POST = async <A,R=void>(endpoint: string, args?: A, authenticated=true) => {
89
136
  try {
90
137
  return (await axios.post(
@@ -92,7 +139,7 @@ export class Session {
92
139
  { ...args, ...this.getAuthInfo(authenticated) },
93
140
  this.config)
94
141
  ).data as R
95
- } catch(err) { throw parseError(err) }
142
+ } catch(err) { throw await this.errorHandler(err) }
96
143
  }
97
144
 
98
145
  GET = async <A,R=void>(endpoint: string, params?: A, authenticated=true) => {
@@ -102,7 +149,7 @@ export class Session {
102
149
  { params: { ...params, ...this.getAuthInfo(authenticated) },
103
150
  headers: this.config.headers })
104
151
  ).data as R
105
- } catch(err) { throw parseError(err) }
152
+ } catch(err) { throw await this.errorHandler(err) }
106
153
  }
107
154
 
108
155
  PATCH = async <A,R=void>(endpoint: string, params?: A, authenticated=true) => {
@@ -112,7 +159,7 @@ export class Session {
112
159
  { ...params, ...this.getAuthInfo(authenticated) },
113
160
  this.config)
114
161
  ).data as R
115
- } catch(err) { throw parseError(err) }
162
+ } catch(err) { throw await this.errorHandler(err) }
116
163
  }
117
164
 
118
165
  DELETE = async <A,R=void>(endpoint: string, args?: A, authenticated=true) => {
@@ -122,14 +169,55 @@ export class Session {
122
169
  { data: { ...args, ...this.getAuthInfo(authenticated) },
123
170
  headers: this.config.headers })
124
171
  ).data as R
125
- } catch(err) { throw parseError(err) }
172
+ } catch(err) { throw await this.errorHandler(err) }
173
+ }
174
+
175
+ UPLOAD = async (presigned: S3PresignedPost, file?: FileBlob, buffer?: FileBlob) => {
176
+ const formData = IN_BROWSER ? new FormData() : new NodeFormData();
177
+ Object.keys(presigned.fields).forEach(key => {
178
+ formData.append(key, presigned.fields[key]);
179
+ });
180
+
181
+ // has to be appended last
182
+ if (!IN_BROWSER) file = buffer || file // use raw buffer if file is Node Blob
183
+ formData.append("file", file as Blob)
184
+
185
+ try {
186
+ await axios.post(presigned.url, formData, {
187
+ headers: IN_BROWSER
188
+ ? { 'Content-Type': 'multipart/form-data'}
189
+ : {
190
+ ...(formData as NodeFormData).getHeaders(),
191
+ 'Content-Length' : (formData as any).maxDataSize
192
+ }
193
+ })
194
+ }
195
+ catch(err) { console.error(err); throw err }
126
196
  }
127
197
 
128
- EMIT = async (route: string, args: object, authenticated=true) => {
198
+ EMIT = async (route: string, args: object, authenticated=true, options={} as RequestOptions) => {
129
199
  this.socket?.emit(route, { ...args, ...authenticated ? { authToken: this.authToken } : {} } )
130
200
  }
131
201
 
132
202
  ON = <T={}>(s: string, callback: (a: T) => void) => this.socket?.on(s, callback)
133
203
 
134
- authenticate_socket = () => !!this.socket?.emit('authenticate', this.authToken)
204
+ subscribe = (rooms: { [index: string]: keyof ClientModelForName }, handlers?: { [index: string]: (a: any) => void } ) => {
205
+ if (handlers) { this.handle_events(handlers) }
206
+ this.EMIT('join-rooms', { rooms })
207
+ }
208
+
209
+ handle_events = ( handlers: { [index: string]: (a: any) => void } ) => {
210
+ for (const handler in handlers) this.ON(handler, handlers[handler])
211
+ }
212
+
213
+ unsubscribe = (roomIds: string[]) => this.EMIT('leave-rooms', { roomIds })
214
+ removeAllSocketListeners = (s: string) => this.socket?.removeAllListeners(s)
215
+
216
+ authenticate_socket = () => {
217
+ this.socket = io(`${this.host}/${this.userInfo.businessId}`, { transports: ['websocket'] }); // supporting polling requires sticky session at load balancer
218
+ this.socket.on('disconnect', () => { this.socketAuthenticated = false })
219
+ this.socket.on('authenticated', () => { this.socketAuthenticated = true })
220
+
221
+ this.socket.emit('authenticate', this.authToken)
222
+ }
135
223
  }
@@ -2,9 +2,11 @@ import {
2
2
  assert,
3
3
  async_test,
4
4
  log_header,
5
- objects_equivalent,
6
5
  wait,
7
6
  } from "@tellescope/testing"
7
+ import {
8
+ objects_equivalent,
9
+ } from "@tellescope/utilities"
8
10
  import { Session, /* APIQuery */ } from "../sdk"
9
11
  import { PublicEndpoints } from "../public"
10
12
 
@@ -30,14 +32,14 @@ const enduser_login_tests = async () => {
30
32
  )
31
33
  await async_test(
32
34
  'setPassword',
33
- () => sdk.api.endusers.setPassword(e.id, password),
35
+ () => sdk.api.endusers.setPassword({ id: e.id, password }),
34
36
  { onResult: _ => true }
35
37
  )
36
38
 
37
39
  let authToken = 'placeholder'
38
40
  await async_test(
39
41
  'isAuthenticated (no)',
40
- () => sdk.api.endusers.isAuthenticated(e.id, authToken),
42
+ () => sdk.api.endusers.isAuthenticated({ id: e.id, authToken }),
41
43
  { onResult: ({ isAuthenticated, enduser }) => isAuthenticated === false && enduser === null }
42
44
  )
43
45
  await async_test(
@@ -47,7 +49,7 @@ const enduser_login_tests = async () => {
47
49
  )
48
50
  await async_test(
49
51
  'isAuthenticated (yes)',
50
- () => sdk.api.endusers.isAuthenticated(e.id, authToken),
52
+ () => sdk.api.endusers.isAuthenticated({ id: e.id, authToken }),
51
53
  { onResult: ({ isAuthenticated, enduser }) => isAuthenticated === true && enduser?.id === e.id }
52
54
  )
53
55
  }