@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.
- package/lib/cjs/enduser.d.ts +346 -13
- package/lib/cjs/enduser.d.ts.map +1 -1
- package/lib/cjs/enduser.js +135 -14
- package/lib/cjs/enduser.js.map +1 -1
- package/lib/cjs/sdk.d.ts +107 -20
- package/lib/cjs/sdk.d.ts.map +1 -1
- package/lib/cjs/sdk.js +116 -32
- package/lib/cjs/sdk.js.map +1 -1
- package/lib/cjs/session.d.ts +37 -176
- package/lib/cjs/session.d.ts.map +1 -1
- package/lib/cjs/session.js +131 -34
- package/lib/cjs/session.js.map +1 -1
- package/lib/cjs/tests/public_endpoint_tests.js +3 -3
- package/lib/cjs/tests/public_endpoint_tests.js.map +1 -1
- package/lib/cjs/tests/socket_tests.js +42 -31
- package/lib/cjs/tests/socket_tests.js.map +1 -1
- package/lib/cjs/tests/tests.js +232 -63
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/esm/enduser.d.ts +346 -13
- package/lib/esm/enduser.d.ts.map +1 -1
- package/lib/esm/enduser.js +132 -12
- package/lib/esm/enduser.js.map +1 -1
- package/lib/esm/sdk.d.ts +107 -20
- package/lib/esm/sdk.d.ts.map +1 -1
- package/lib/esm/sdk.js +116 -32
- package/lib/esm/sdk.js.map +1 -1
- package/lib/esm/session.d.ts +37 -176
- package/lib/esm/session.d.ts.map +1 -1
- package/lib/esm/session.js +130 -32
- package/lib/esm/session.js.map +1 -1
- package/lib/esm/tests/public_endpoint_tests.js +3 -3
- package/lib/esm/tests/public_endpoint_tests.js.map +1 -1
- package/lib/esm/tests/socket_tests.js +41 -30
- package/lib/esm/tests/socket_tests.js.map +1 -1
- package/lib/esm/tests/tests.js +228 -64
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +11 -9
- package/src/enduser.ts +93 -20
- package/src/sdk.ts +71 -43
- package/src/session.ts +122 -34
- package/src/tests/public_endpoint_tests.ts +6 -4
- package/src/tests/socket_tests.ts +37 -16
- package/src/tests/tests.ts +147 -25
- 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
|
|
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
|
-
|
|
23
|
+
const safeName = url_safe_path(n)
|
|
24
|
+
const singularName = (safeName).substring(0, safeName.length - 1)
|
|
19
25
|
|
|
20
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
api: Queries;
|
|
58
|
+
userInfo!: Enduser;
|
|
59
|
+
api: EnduserQueries;
|
|
30
60
|
|
|
31
61
|
constructor(o?: EnduserSessionOptions) {
|
|
32
|
-
super(o)
|
|
33
|
-
|
|
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
|
-
|
|
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
|
|
103
|
+
this.setUserInfo(enduser)
|
|
41
104
|
|
|
42
|
-
this.socket = io(`${this.host}/${enduser.businessId}
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
|
34
|
-
updateOne: (id: string, updates: UPDATE, options?: CustomUpdateOptions) => Promise<
|
|
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:
|
|
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.
|
|
47
|
-
createSome: os => s.
|
|
48
|
-
getOne: (id, filter) => s.
|
|
49
|
-
getSome: (o
|
|
50
|
-
updateOne: (id, updates, options) => s.
|
|
51
|
-
deleteOne: id => s.
|
|
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:
|
|
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
|
|
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.
|
|
89
|
-
queries.endusers.setPassword = (id, password) => this.
|
|
90
|
-
queries.endusers.isAuthenticated = (id, authToken) => this.
|
|
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
|
-
|
|
97
|
-
this.
|
|
98
|
-
this.
|
|
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
|
-
|
|
101
|
-
this.
|
|
102
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
await this.
|
|
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
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
|
|
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<
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
}
|