skapi-js 0.0.1
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/LICENSE +201 -0
- package/README.md +53 -0
- package/dist/skapi.js +3 -0
- package/dist/skapi.js.LICENSE.txt +21 -0
- package/dist/skapi.js.map +1 -0
- package/package.json +35 -0
- package/src/Api.ts +3 -0
- package/src/Types.ts +385 -0
- package/src/decorators.ts +94 -0
- package/src/skapi.ts +3609 -0
- package/src/skapi_error.ts +43 -0
- package/src/utils.ts +735 -0
- package/tsconfig.json +116 -0
- package/webpack.config.js +21 -0
package/src/skapi.ts
ADDED
|
@@ -0,0 +1,3609 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CognitoUserPool,
|
|
3
|
+
CognitoUserAttribute,
|
|
4
|
+
CognitoUser,
|
|
5
|
+
AuthenticationDetails,
|
|
6
|
+
CognitoUserSession
|
|
7
|
+
} from 'amazon-cognito-identity-js';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
RecordData,
|
|
11
|
+
User,
|
|
12
|
+
Form,
|
|
13
|
+
FormCallbacks,
|
|
14
|
+
UserProfile,
|
|
15
|
+
PostRecordParams,
|
|
16
|
+
FetchOptions,
|
|
17
|
+
SubscriberGroup,
|
|
18
|
+
SubscriberFetch,
|
|
19
|
+
FetchResponse,
|
|
20
|
+
GetRecordParams,
|
|
21
|
+
QueryParams,
|
|
22
|
+
Newsletters
|
|
23
|
+
} from './Types';
|
|
24
|
+
import SkapiError from './skapi_error';
|
|
25
|
+
import { formResponse } from './decorators';
|
|
26
|
+
import {
|
|
27
|
+
checkParams,
|
|
28
|
+
extractFormMetaData,
|
|
29
|
+
validateUserId,
|
|
30
|
+
validateBirthdate,
|
|
31
|
+
validateEmail,
|
|
32
|
+
validatePassword,
|
|
33
|
+
validatePhoneNumber,
|
|
34
|
+
validateUrl,
|
|
35
|
+
normalize_record_data,
|
|
36
|
+
checkWhiteSpaceAndSpecialChars,
|
|
37
|
+
sha256
|
|
38
|
+
} from './utils';
|
|
39
|
+
|
|
40
|
+
type StartKeys = {
|
|
41
|
+
/** List of startkeys */
|
|
42
|
+
[url: string]: {
|
|
43
|
+
[hashedParams: string]: string[];
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type CachedRequests = {
|
|
48
|
+
/** Cached url requests */
|
|
49
|
+
[url: string]: {
|
|
50
|
+
/** Array of data stored in hashed params key */
|
|
51
|
+
[hashedParams: string]: FetchResponse;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
type Connection = {
|
|
56
|
+
/** Connection locale */
|
|
57
|
+
locale: string;
|
|
58
|
+
/** Name of the connected service */
|
|
59
|
+
name: string;
|
|
60
|
+
/** Id of the service owner */
|
|
61
|
+
owner: string;
|
|
62
|
+
/** E-Mail address of the service owner */
|
|
63
|
+
owner_email: string;
|
|
64
|
+
/** Service id */
|
|
65
|
+
service: string;
|
|
66
|
+
/** 13 digits timestamp of the service creation */
|
|
67
|
+
timestamp: number;
|
|
68
|
+
/** hash string used for login */
|
|
69
|
+
hash?: string;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* All the methods used in Skapi are promises.<br>
|
|
73
|
+
* Use async/await or Promise.then() to interact with backend.<br>
|
|
74
|
+
*
|
|
75
|
+
* <b>Example A: Using async await</b>
|
|
76
|
+
* ```
|
|
77
|
+
* async function app() {
|
|
78
|
+
* try {
|
|
79
|
+
* let skapi = await Skapi.connect('xxxxxxxxxxxxxx', 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx');
|
|
80
|
+
*
|
|
81
|
+
* console.log(skapi.connection); // connection to Skapi!
|
|
82
|
+
*
|
|
83
|
+
* // - Here is where all your code should be written -
|
|
84
|
+
*
|
|
85
|
+
* } catch(error) {
|
|
86
|
+
* console.log('Connection error');
|
|
87
|
+
* }
|
|
88
|
+
* }
|
|
89
|
+
*
|
|
90
|
+
* // Run your application
|
|
91
|
+
* app();
|
|
92
|
+
* ```
|
|
93
|
+
*
|
|
94
|
+
* <b>Example B: Using Promise.then()</b>
|
|
95
|
+
* ```
|
|
96
|
+
* Skapi.connect('xxxxxxxxxxxxxx', 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx').then(async skapi => {
|
|
97
|
+
*
|
|
98
|
+
* console.log(skapi.connection); // connection to Skapi!
|
|
99
|
+
*
|
|
100
|
+
* // - Here is where all your code should be written -
|
|
101
|
+
*
|
|
102
|
+
* }).catch(error => {
|
|
103
|
+
* console.log('Connection error');
|
|
104
|
+
* });
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
export default class Skapi {
|
|
108
|
+
// privates
|
|
109
|
+
private cognitoUser: CognitoUser | null = null;
|
|
110
|
+
private __disabledAccount: string | null = null;
|
|
111
|
+
private __serviceHash: Record<string, string> = {};
|
|
112
|
+
private __pendingRequest: Record<string, Promise<any>> = {};
|
|
113
|
+
private __cached_requests: CachedRequests = {};
|
|
114
|
+
private __startKey_keys: StartKeys = {};
|
|
115
|
+
private __request_signup_confirmation: string | null = null;
|
|
116
|
+
private __index_number_range = 4503599627370496;
|
|
117
|
+
private service: string;
|
|
118
|
+
private service_owner: string;
|
|
119
|
+
|
|
120
|
+
// true when session is stored successfully to session storage
|
|
121
|
+
// this property prevents duplicate stores when window closes on some device
|
|
122
|
+
private __class_properties_has_been_cached = false;
|
|
123
|
+
|
|
124
|
+
private session: Record<string, any> | null = null;
|
|
125
|
+
|
|
126
|
+
// public
|
|
127
|
+
|
|
128
|
+
/** Current logged in user object. null if not logged. */
|
|
129
|
+
user: User | null = null;
|
|
130
|
+
/** Connected service object. null if connection failed. */
|
|
131
|
+
connection: Connection | null = null;
|
|
132
|
+
host: string = 'skapi';
|
|
133
|
+
hostDomain: string = 'skapi.com';
|
|
134
|
+
userPool: CognitoUserPool | null = null;
|
|
135
|
+
admin_endpoint: Promise<Record<string, any>>;
|
|
136
|
+
record_endpoint: Promise<Record<string, any>>;
|
|
137
|
+
|
|
138
|
+
regex = {
|
|
139
|
+
validateUserId(val: string) {
|
|
140
|
+
try {
|
|
141
|
+
validateUserId(val);
|
|
142
|
+
return true;
|
|
143
|
+
} catch (err) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
validateUrl(val: string | string[]) {
|
|
148
|
+
try {
|
|
149
|
+
validateUrl(val);
|
|
150
|
+
return true;
|
|
151
|
+
} catch (err) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
validatePhoneNumber(val: string) {
|
|
156
|
+
try {
|
|
157
|
+
validatePhoneNumber(val);
|
|
158
|
+
return true;
|
|
159
|
+
} catch (err) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
validateBirthdate(val: string) {
|
|
164
|
+
try {
|
|
165
|
+
validateBirthdate(val);
|
|
166
|
+
return true;
|
|
167
|
+
} catch (err) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
validateEmail(val: string) {
|
|
172
|
+
try {
|
|
173
|
+
validateEmail(val);
|
|
174
|
+
return true;
|
|
175
|
+
} catch (err) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/** @ignore */
|
|
182
|
+
__connection: Promise<Connection | null>;
|
|
183
|
+
|
|
184
|
+
// skapi int range -4503599627370545 ~ 4503599627370546
|
|
185
|
+
|
|
186
|
+
constructor(service_id: string, service_owner: string) {
|
|
187
|
+
if (typeof service_id !== 'string' || typeof service_owner !== 'string') {
|
|
188
|
+
throw new SkapiError('"service_id" and "service_owner" should be type <string>.', { code: 'INVALID_PARAMETER' });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!service_id || !service_owner) {
|
|
192
|
+
throw new SkapiError('"service_id" and "service_owner" is required', { code: 'INVALID_PARAMETER' });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (service_owner !== this.host) {
|
|
196
|
+
validateUserId(service_owner, '"service_owner"');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
this.service = service_id;
|
|
200
|
+
this.service_owner = service_owner;
|
|
201
|
+
|
|
202
|
+
// get endpoints
|
|
203
|
+
const cdn_domain = 'https://dkls9pxkgz855.cloudfront.net'; // don't change this
|
|
204
|
+
let sreg = service_id.substring(0, 4);
|
|
205
|
+
this.admin_endpoint = fetch(`${cdn_domain}/${sreg}/admin.json`)
|
|
206
|
+
.then(response => response.blob())
|
|
207
|
+
.then(blob => new Promise((resolve, reject) => {
|
|
208
|
+
const reader = new FileReader();
|
|
209
|
+
reader.onloadend = () => resolve(reader.result);
|
|
210
|
+
reader.onerror = reject;
|
|
211
|
+
reader.readAsDataURL(blob);
|
|
212
|
+
}))
|
|
213
|
+
.then(data => typeof data === 'string' ? JSON.parse(window.atob(data.split(',')[1])) : null);
|
|
214
|
+
|
|
215
|
+
this.record_endpoint = fetch(`${cdn_domain}/${sreg}/record.json`)
|
|
216
|
+
.then(response => response.blob())
|
|
217
|
+
.then(blob => new Promise((resolve, reject) => {
|
|
218
|
+
const reader = new FileReader();
|
|
219
|
+
reader.onloadend = () => resolve(reader.result);
|
|
220
|
+
reader.onerror = reject;
|
|
221
|
+
reader.readAsDataURL(blob);
|
|
222
|
+
}))
|
|
223
|
+
.then(data => typeof data === 'string' ? JSON.parse(window.atob(data.split(',')[1])) : null);
|
|
224
|
+
|
|
225
|
+
// connects to server
|
|
226
|
+
this.__connection = (async (skapi: Skapi): Promise<Connection | null> => {
|
|
227
|
+
if (!window.sessionStorage) {
|
|
228
|
+
throw new Error(`This browser does not support skapi.`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const restore = JSON.parse(window.sessionStorage.getItem(`${service_id}#${service_owner}`) || 'null');
|
|
232
|
+
|
|
233
|
+
if (restore?.connection) {
|
|
234
|
+
// apply all data to class properties
|
|
235
|
+
for (let k in restore) {
|
|
236
|
+
skapi[k] = restore[k];
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const admin_endpoint = await skapi.admin_endpoint;
|
|
241
|
+
|
|
242
|
+
skapi.userPool = new CognitoUserPool({
|
|
243
|
+
UserPoolId: admin_endpoint.userpool_id,
|
|
244
|
+
ClientId: admin_endpoint.userpool_client
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
await Promise.all([
|
|
248
|
+
skapi.updateServiceInformation(),
|
|
249
|
+
skapi.authentication().updateSession({ refreshToken: !!restore?.connection }).catch(err => {
|
|
250
|
+
skapi.user = null;
|
|
251
|
+
})
|
|
252
|
+
]);
|
|
253
|
+
|
|
254
|
+
const storeClassProperties = () => {
|
|
255
|
+
if (skapi.__class_properties_has_been_cached) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let data: Record<string, any> = {};
|
|
260
|
+
|
|
261
|
+
const to_be_cached = [
|
|
262
|
+
'__startKey_keys', // startKey key : {}
|
|
263
|
+
'__cached_requests', // cached records : {}
|
|
264
|
+
'__request_signup_confirmation', // for resend signup confirmation : null
|
|
265
|
+
'connection', // service info : null
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
if (skapi.connection) {
|
|
269
|
+
for (let k of to_be_cached) {
|
|
270
|
+
data[k] = skapi[k];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
sessionStorage.setItem(`${service_id}#${service_owner}`, JSON.stringify(data));
|
|
274
|
+
skapi.__class_properties_has_been_cached = true;
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// attach event to save session on close
|
|
279
|
+
window.addEventListener('beforeunload', storeClassProperties);
|
|
280
|
+
window.addEventListener("visibilitychange", storeClassProperties);
|
|
281
|
+
|
|
282
|
+
return skapi.connection;
|
|
283
|
+
|
|
284
|
+
})(this).catch(err => { throw err; });
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Connects to your Skapi web service.</br>
|
|
289
|
+
* Use your service ID and your user ID as an argument.<br>
|
|
290
|
+
* Once successful, Skapi class object will be returned.<br>
|
|
291
|
+
*
|
|
292
|
+
* <h5>IMPORTANT NOTE!</h5>
|
|
293
|
+
* When setting up your web services in Skapi, always set your service cors url on production.<br>
|
|
294
|
+
* If the cors is not set, other people can use your web service apis on their websites as well.<br>
|
|
295
|
+
* Refer: <a href='www.google.com'>Setting up cors</a>
|
|
296
|
+
*
|
|
297
|
+
* ```
|
|
298
|
+
* Skapi.connect('xxxxxxxxxxxxxx', 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx').then(async skapi => {
|
|
299
|
+
*
|
|
300
|
+
* console.log(skapi.connection); // connection to Skapi!
|
|
301
|
+
*
|
|
302
|
+
* if(skapi.user) {
|
|
303
|
+
* // user is logged in!
|
|
304
|
+
* }
|
|
305
|
+
*
|
|
306
|
+
* else {
|
|
307
|
+
* // user is not logged in.
|
|
308
|
+
* }
|
|
309
|
+
*
|
|
310
|
+
* // - code -
|
|
311
|
+
*
|
|
312
|
+
* }).catch(error => {
|
|
313
|
+
* console.log('Connection error'); //'connection failed.'
|
|
314
|
+
* });
|
|
315
|
+
* ```
|
|
316
|
+
* @category Connection
|
|
317
|
+
*/
|
|
318
|
+
static async connect(
|
|
319
|
+
/** 22 character service id */
|
|
320
|
+
service_id: string,
|
|
321
|
+
/** 36 character user id */
|
|
322
|
+
service_owner: string
|
|
323
|
+
): Promise<Skapi> {
|
|
324
|
+
const skapi = new Skapi(service_id, service_owner);
|
|
325
|
+
await skapi.__connection;
|
|
326
|
+
return skapi;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private authentication() {
|
|
330
|
+
if (!this.userPool) {
|
|
331
|
+
throw new SkapiError('User pool is missing', { code: 'INVALID_REQUEST' });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const updateSession = (option?: {
|
|
335
|
+
refreshToken?: boolean;
|
|
336
|
+
}): Promise<User> => {
|
|
337
|
+
// fetch session, update user info
|
|
338
|
+
let { refreshToken = false } = option || {};
|
|
339
|
+
// console.log('%cUpdate session', 'background-color:tomato;color:white;');
|
|
340
|
+
// console.log({ refreshToken, refreshUserData });
|
|
341
|
+
|
|
342
|
+
return new Promise((res, rej) => {
|
|
343
|
+
let currentUser: CognitoUser | null = this.userPool?.getCurrentUser() || null;
|
|
344
|
+
this.cognitoUser = currentUser;
|
|
345
|
+
|
|
346
|
+
if (currentUser === null) {
|
|
347
|
+
rej(null);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
currentUser.getSession((err: any, session: CognitoUserSession) => {
|
|
351
|
+
// console.log('%cGet session', 'background-color:tomato;color:white;');
|
|
352
|
+
// console.log({ err, session });
|
|
353
|
+
if (err) {
|
|
354
|
+
rej(err);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const updateAttributes = async (sessionToMerge: Record<string, any>) => {
|
|
358
|
+
// console.log('%cUpdate attributes', 'background-color:tomato;color:white;');
|
|
359
|
+
// console.log({ sessionToMerge });
|
|
360
|
+
if (sessionToMerge.isValid() && currentUser) {
|
|
361
|
+
currentUser.getUserAttributes((attrErr, attributes) => {
|
|
362
|
+
// console.log('%cGet user attributes', 'background-color:tomato;color:white;');
|
|
363
|
+
// console.log({ attrErr, attributes });
|
|
364
|
+
if (attrErr) {
|
|
365
|
+
rej(attrErr);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
else {
|
|
369
|
+
let normalized_attributes: Record<string, any> = {};
|
|
370
|
+
let user: any = {};
|
|
371
|
+
|
|
372
|
+
for (let i of (attributes as CognitoUserAttribute[])) {
|
|
373
|
+
normalized_attributes[i.Name] = i.Value;
|
|
374
|
+
let key_1 = i.Name.replace('custom:', '');
|
|
375
|
+
user[key_1] = i.Value;
|
|
376
|
+
|
|
377
|
+
if (i.Name === 'custom:service' && normalized_attributes[i.Name] !== this.service) {
|
|
378
|
+
rej(new SkapiError('The user is not registered to the service.', { code: 'INVALID_REQUEST' }));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
for (let k of [
|
|
384
|
+
'address_public',
|
|
385
|
+
'birthdate_public',
|
|
386
|
+
'email_public',
|
|
387
|
+
'email_subscription',
|
|
388
|
+
'gender_public',
|
|
389
|
+
'phone_number_public'
|
|
390
|
+
]) {
|
|
391
|
+
if (k.includes('_public')) {
|
|
392
|
+
if (user.hasOwnProperty(k.split('_')[0])) {
|
|
393
|
+
user[k] = user.hasOwnProperty(k) ? Number(user[k]) : 0;
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
delete user[k];
|
|
397
|
+
}
|
|
398
|
+
} else {
|
|
399
|
+
user[k] = user.hasOwnProperty(k) ? Number(user[k]) : 0;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
for (let k of [
|
|
404
|
+
'email_verified',
|
|
405
|
+
'phone_number_verified'
|
|
406
|
+
]) {
|
|
407
|
+
if (user[k.split('_')[0]]) {
|
|
408
|
+
user[k] = user.hasOwnProperty(k) ? user[k] === 'true' : false;
|
|
409
|
+
}
|
|
410
|
+
else if (user.hasOwnProperty(k)) {
|
|
411
|
+
delete user[k];
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// console.log('%cMerging attributes to session', 'background-color:tomato;color:white;');
|
|
416
|
+
sessionToMerge.attributes = normalized_attributes;
|
|
417
|
+
this.session = sessionToMerge;
|
|
418
|
+
|
|
419
|
+
user.access_group = Number(this.session.idToken.payload.access_group);
|
|
420
|
+
user.user_id = user.sub;
|
|
421
|
+
delete user.sub;
|
|
422
|
+
this.user = user;
|
|
423
|
+
res(user);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
else {
|
|
429
|
+
// console.log('%cInvalid session', 'background-color:tomato;color:white;');
|
|
430
|
+
rej(new SkapiError('Invalid session.', { code: 'INVALID_REQUEST' }));
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
if (session) {
|
|
435
|
+
if (refreshToken) {
|
|
436
|
+
// console.log('%cRefresh token', 'background-color:tomato;color:white;');
|
|
437
|
+
// console.log({ refreshToken });
|
|
438
|
+
currentUser?.refreshSession(session.getRefreshToken(), (refreshErr, refreshedSession) => {
|
|
439
|
+
if (refreshErr) {
|
|
440
|
+
// console.log('%cRefresh token error', 'background-color:tomato;color:white;');
|
|
441
|
+
// console.log({ refreshErr });
|
|
442
|
+
rej(refreshErr);
|
|
443
|
+
}
|
|
444
|
+
updateAttributes(refreshedSession).catch(err => rej(err));
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
else {
|
|
449
|
+
// console.log('%cLoading session', 'background-color:tomato;color:white;');
|
|
450
|
+
// console.log({ refreshToken });
|
|
451
|
+
updateAttributes(session).catch(err => rej(err));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
} else {
|
|
455
|
+
rej(new SkapiError('Current session does not exist.', { code: 'INVALID_REQUEST' }));
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
const createCognitoUser = async (email: string) => {
|
|
462
|
+
// console.log('%cCreate cognito user', 'background-color:tomato;color:white;');
|
|
463
|
+
let hash = null;
|
|
464
|
+
|
|
465
|
+
if (email) {
|
|
466
|
+
hash = this.__serviceHash[email] || (await this.updateServiceInformation({ request_hash: email })).hash;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
else {
|
|
470
|
+
if (this.session) {
|
|
471
|
+
hash = this.session.idToken.payload['cognito:username'];
|
|
472
|
+
} else {
|
|
473
|
+
throw new SkapiError('E-Mail is required.', { code: 'INVALID_PARAMETER' });
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
cognitoUser: new CognitoUser({
|
|
479
|
+
Username: hash,
|
|
480
|
+
Pool: this.userPool
|
|
481
|
+
}),
|
|
482
|
+
cognitoUsername: hash
|
|
483
|
+
};
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
const authenticateUser = (username: string, password: string): Promise<User> => {
|
|
487
|
+
// console.log('%cAuthenticate user', 'background-color:tomato;color:white;');
|
|
488
|
+
return new Promise(async (res, rej) => {
|
|
489
|
+
this.logout();
|
|
490
|
+
this.__request_signup_confirmation = null;
|
|
491
|
+
this.__disabledAccount = null;
|
|
492
|
+
|
|
493
|
+
let initUser = await createCognitoUser(username);
|
|
494
|
+
this.cognitoUser = initUser.cognitoUser;
|
|
495
|
+
username = initUser.cognitoUsername;
|
|
496
|
+
|
|
497
|
+
let authenticationDetails = new AuthenticationDetails({
|
|
498
|
+
Username: username,
|
|
499
|
+
Password: password
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
this.cognitoUser.authenticateUser(authenticationDetails, {
|
|
503
|
+
newPasswordRequired: (userAttributes, requiredAttributes) => {
|
|
504
|
+
this.__request_signup_confirmation = username;
|
|
505
|
+
rej(new SkapiError("User's signup confirmation is required.", { code: 'SIGNUP_CONFIRMATION_NEEDED' }));
|
|
506
|
+
},
|
|
507
|
+
onSuccess: (logged) => {
|
|
508
|
+
// console.log('%cAuthenticate user success', 'background-color:tomato;color:white;');
|
|
509
|
+
// console.log({ logged });
|
|
510
|
+
updateSession().then(session => {
|
|
511
|
+
res(session);
|
|
512
|
+
});
|
|
513
|
+
},
|
|
514
|
+
onFailure: (err: any) => {
|
|
515
|
+
let error: [string, string] = [err.message || 'Failed to authenticate user.', err?.code || 'INVALID_REQUEST'];
|
|
516
|
+
|
|
517
|
+
if (err.code === "NotAuthorizedException") {
|
|
518
|
+
if (err.message === "User is disabled.") {
|
|
519
|
+
this.__disabledAccount = username;
|
|
520
|
+
error = ['This account is disabled.', 'USER_IS_DISABLED'];
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
else {
|
|
524
|
+
error = ['Incorrect username or password.', 'INCORRECT_USERNAME_OR_PASSWORD'];
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
rej(new SkapiError(error[0], { code: error[1], cause: err }));
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
return { updateSession, authenticateUser, createCognitoUser };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* @ignore
|
|
539
|
+
*/
|
|
540
|
+
async requireAdmin(option?: { ignoreVerification?: boolean; throwError?: boolean; }) {
|
|
541
|
+
if (this.session?.attributes?.['custom:service'] === this.service) {
|
|
542
|
+
// logged in
|
|
543
|
+
if (this.session?.attributes?.['custom:service_owner'] === this.host) {
|
|
544
|
+
if (!option?.ignoreVerification && !this.session?.attributes?.email_verified) {
|
|
545
|
+
throw new SkapiError('Email verification is required.', { code: 'INVALID_REQUEST' });
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (option?.throwError) {
|
|
552
|
+
throw new SkapiError('Admin access is required.', { code: 'INVALID_REQUEST' });
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
} else {
|
|
556
|
+
// not logged
|
|
557
|
+
this.logout();
|
|
558
|
+
|
|
559
|
+
if (option?.throwError) {
|
|
560
|
+
throw new SkapiError('User login is required.', { code: 'INVALID_REQUEST' });
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Fetch blob from url.</br>
|
|
569
|
+
* User can get saved file as blob from url.<br>
|
|
570
|
+
* getBlob can be used to fetch file that are user login is required.
|
|
571
|
+
*
|
|
572
|
+
* ```
|
|
573
|
+
* let fileBlob = await skapi.getBlob({url: 'http://blob.url'});
|
|
574
|
+
* ```
|
|
575
|
+
* @category Connection
|
|
576
|
+
* @param params.url - file url to get.
|
|
577
|
+
* @param _option.service - Service Id. Only works on admin account.
|
|
578
|
+
*/
|
|
579
|
+
async getBlob(params: { url: string; }, option?: { service: string; }): Promise<Blob> {
|
|
580
|
+
|
|
581
|
+
let p = checkParams(params, {
|
|
582
|
+
url: (v: string) => validateUrl(v)
|
|
583
|
+
}, ['url']);
|
|
584
|
+
|
|
585
|
+
return await this.request(p.url, option || null, { method: 'get', auth: p.url.includes('/auth/'), contentType: null, responseType: 'blob' });
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Sends post request to your custom server using Skapi's secure API layer.</br>
|
|
590
|
+
* You must set your secret API key from the Skapi's admin page.</br>
|
|
591
|
+
* On your server side, you must verify your secret API key.<br>
|
|
592
|
+
* Skapi API layer can process your requests both synchronously and asynchronously.<br>
|
|
593
|
+
* You can request multiple process using arrays.<br>
|
|
594
|
+
* Skapi will process your requests in order.</br>
|
|
595
|
+
* The sync process will be chained in order during process.<br>
|
|
596
|
+
* Refer: <a href='www.google.com'>Setting secret api key</a>
|
|
597
|
+
*
|
|
598
|
+
* <h6>Example</h6>
|
|
599
|
+
*
|
|
600
|
+
* ```
|
|
601
|
+
* let call = await skapi.secureRequest(
|
|
602
|
+
* url: 'http://my.website.com/myapi',
|
|
603
|
+
* data: {
|
|
604
|
+
* some_data: 'Hello'
|
|
605
|
+
* }
|
|
606
|
+
* )
|
|
607
|
+
*
|
|
608
|
+
* console.log(call)
|
|
609
|
+
* // {
|
|
610
|
+
* // response: <any>
|
|
611
|
+
* // statusCode: <number>
|
|
612
|
+
* // url: 'http://my.website.com/myapi'
|
|
613
|
+
* // }
|
|
614
|
+
* ```
|
|
615
|
+
*
|
|
616
|
+
*
|
|
617
|
+
* <h6>Nodejs Example</h6>
|
|
618
|
+
*
|
|
619
|
+
* ```
|
|
620
|
+
* const http = require('http');
|
|
621
|
+
* http.createServer(function (request, response) {
|
|
622
|
+
* if (request.url === '/myapi') {
|
|
623
|
+
* if (request.method === 'POST') {
|
|
624
|
+
* let body = '';
|
|
625
|
+
*
|
|
626
|
+
* request.on('data', function (data) {
|
|
627
|
+
* body += data;
|
|
628
|
+
* });
|
|
629
|
+
*
|
|
630
|
+
* request.on('end', function () {
|
|
631
|
+
* body = JSON.parse(body);
|
|
632
|
+
* console.log(body);
|
|
633
|
+
* // {
|
|
634
|
+
* // user: {
|
|
635
|
+
* // user_id: '',
|
|
636
|
+
* // address: '',
|
|
637
|
+
* // phone_number: '',
|
|
638
|
+
* // email: '',
|
|
639
|
+
* // name: '',
|
|
640
|
+
* // locale: '',
|
|
641
|
+
* // request_locale: ''
|
|
642
|
+
* // },
|
|
643
|
+
* // data: {
|
|
644
|
+
* // some_data: 'Hello',
|
|
645
|
+
* // },
|
|
646
|
+
* // api_key: 'your api secret key'
|
|
647
|
+
* // }
|
|
648
|
+
*
|
|
649
|
+
* if (body.api_key && body.api_key === 'your api secret key') {
|
|
650
|
+
* response.writeHead(200, {'Content-Type': 'text/html'});
|
|
651
|
+
* // do something
|
|
652
|
+
* response.end('success');
|
|
653
|
+
* } else {
|
|
654
|
+
* response.writeHead(401, {'Content-Type': 'text/html'});
|
|
655
|
+
* response.end("api key mismatch");
|
|
656
|
+
* }
|
|
657
|
+
* });
|
|
658
|
+
* }
|
|
659
|
+
* }
|
|
660
|
+
* }).listen(3000);
|
|
661
|
+
* ```
|
|
662
|
+
*
|
|
663
|
+
*
|
|
664
|
+
* <h6>Python Example</h6>
|
|
665
|
+
*
|
|
666
|
+
* ```
|
|
667
|
+
* from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
668
|
+
* import json
|
|
669
|
+
*
|
|
670
|
+
* class MyServer(BaseHTTPRequestHandler):
|
|
671
|
+
* def do_request(self):
|
|
672
|
+
* if self.path == '/myapi':
|
|
673
|
+
* content_length = int(self.headers['Content-Length'])
|
|
674
|
+
* body = json.loads(self.rfile.read(content_length).decode('utf-8'))
|
|
675
|
+
* print(body)
|
|
676
|
+
* # {
|
|
677
|
+
* # 'user': {
|
|
678
|
+
* # 'user_id': '',
|
|
679
|
+
* # 'address': '',
|
|
680
|
+
* # 'phone_number': '',
|
|
681
|
+
* # 'email': '',
|
|
682
|
+
* # 'name': '',
|
|
683
|
+
* # 'locale': '',
|
|
684
|
+
* # 'request_locale': ''
|
|
685
|
+
* # },
|
|
686
|
+
* # 'data': {
|
|
687
|
+
* # 'some_data': 'Hello',
|
|
688
|
+
* # },
|
|
689
|
+
* # 'api_key': 'your api secret key'
|
|
690
|
+
* # }
|
|
691
|
+
*
|
|
692
|
+
* if 'api_key' in body and body['api_key'] == 'your api secret key':
|
|
693
|
+
* self.send_response(200)
|
|
694
|
+
* self.send_header("Content-type", "text/html")
|
|
695
|
+
* self.end_headers()
|
|
696
|
+
* self.wfile.write(b'\n success')
|
|
697
|
+
* else:
|
|
698
|
+
* self.send_response(401)
|
|
699
|
+
* self.send_header("Content-type", "text/html")
|
|
700
|
+
* self.end_headers()
|
|
701
|
+
* self.wfile.write(b'api key mismatch')
|
|
702
|
+
*
|
|
703
|
+
*
|
|
704
|
+
* myServer = HTTPServer(("", 3000), MyServer)
|
|
705
|
+
*
|
|
706
|
+
* try:
|
|
707
|
+
* myServer.serve_forever()
|
|
708
|
+
* except KeyboardInterrupt:
|
|
709
|
+
* myServer.server_close()
|
|
710
|
+
* ```
|
|
711
|
+
* @category Connection
|
|
712
|
+
*/
|
|
713
|
+
async secureRequest<RequestParams = {
|
|
714
|
+
/** Request url */
|
|
715
|
+
url: string;
|
|
716
|
+
/** Request data */
|
|
717
|
+
data?: any;
|
|
718
|
+
/** requests are sync when true */
|
|
719
|
+
sync?: boolean;
|
|
720
|
+
}>(request: RequestParams | RequestParams[]): Promise<any> {
|
|
721
|
+
let paramsStruct = {
|
|
722
|
+
url: (v: string) => {
|
|
723
|
+
return validateUrl(v);
|
|
724
|
+
},
|
|
725
|
+
data: null,
|
|
726
|
+
sync: ['boolean', () => true]
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
if (Array.isArray(request)) {
|
|
730
|
+
for (let r of request) {
|
|
731
|
+
r = checkParams(r, paramsStruct);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
else {
|
|
736
|
+
request = checkParams(request, paramsStruct);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return await this.request('post-secure', request, { auth: true });
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Retrives respond data from form request.
|
|
744
|
+
*
|
|
745
|
+
* ```
|
|
746
|
+
* let respond = skapi.getResponse();
|
|
747
|
+
* ```
|
|
748
|
+
* @category Connection
|
|
749
|
+
*/
|
|
750
|
+
getFormResponse(): any {
|
|
751
|
+
let params = new URLSearchParams(window.location.search);
|
|
752
|
+
|
|
753
|
+
for (let [key, val] of params.entries()) {
|
|
754
|
+
// key = response key sha256
|
|
755
|
+
// val = timestamp
|
|
756
|
+
if (key.substring(0, 5) !== 'form-') {
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
let stored = window.sessionStorage.getItem(key);
|
|
761
|
+
|
|
762
|
+
if (stored) {
|
|
763
|
+
window.sessionStorage.removeItem(key);
|
|
764
|
+
try {
|
|
765
|
+
stored = JSON.parse(stored);
|
|
766
|
+
} catch (err) { }
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (typeof stored === 'object' && (stored as Record<string, any>)[val]) {
|
|
770
|
+
return (stored as Record<string, any>)[val];
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
else {
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
return null;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// internals below
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* @ignore
|
|
785
|
+
*/
|
|
786
|
+
async request(
|
|
787
|
+
url: string,
|
|
788
|
+
data: Form = null,
|
|
789
|
+
options: {
|
|
790
|
+
fetchOptions?: FetchOptions & FormCallbacks;
|
|
791
|
+
auth?: boolean;
|
|
792
|
+
method?: string;
|
|
793
|
+
meta?: Record<string, any>;
|
|
794
|
+
bypassAwaitConnection?: boolean;
|
|
795
|
+
responseType?: string;
|
|
796
|
+
contentType?: string;
|
|
797
|
+
} = {}): Promise<any> {
|
|
798
|
+
|
|
799
|
+
let {
|
|
800
|
+
auth = false,
|
|
801
|
+
method = 'post',
|
|
802
|
+
meta = null, // content meta
|
|
803
|
+
bypassAwaitConnection = false
|
|
804
|
+
} = options;
|
|
805
|
+
|
|
806
|
+
let __connection = bypassAwaitConnection ? null : (await this.__connection);
|
|
807
|
+
let token = auth ? this.session?.idToken?.jwtToken : null; // idToken
|
|
808
|
+
|
|
809
|
+
if (auth) {
|
|
810
|
+
if (!token) {
|
|
811
|
+
this.logout();
|
|
812
|
+
throw new SkapiError('User login is required.', { code: 'INVALID_REQUEST' });
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
let isExternalUrl = '';
|
|
817
|
+
try {
|
|
818
|
+
isExternalUrl = validateUrl(url);
|
|
819
|
+
} catch (err) {
|
|
820
|
+
// is not an external url
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
const getEndpoint = async (dest: string, auth: boolean) => {
|
|
824
|
+
const endpoints = await Promise.all([
|
|
825
|
+
this.admin_endpoint,
|
|
826
|
+
this.record_endpoint
|
|
827
|
+
]);
|
|
828
|
+
|
|
829
|
+
const admin = endpoints[0];
|
|
830
|
+
const record = endpoints[1];
|
|
831
|
+
const get_ep = () => {
|
|
832
|
+
switch (dest) {
|
|
833
|
+
case 'get-serviceletters':
|
|
834
|
+
case 'delete-newsletter':
|
|
835
|
+
case 'block-account':
|
|
836
|
+
case 'register-service':
|
|
837
|
+
case 'get-users':
|
|
838
|
+
case 'post-userdata':
|
|
839
|
+
case 'remove-account':
|
|
840
|
+
case 'post-secure':
|
|
841
|
+
case 'get-newsletters':
|
|
842
|
+
case 'subscribe-newsletter':
|
|
843
|
+
case 'signup':
|
|
844
|
+
case 'confirm-signup':
|
|
845
|
+
case 'recover-account':
|
|
846
|
+
case 'service':
|
|
847
|
+
return {
|
|
848
|
+
public: admin.admin_public,
|
|
849
|
+
private: admin.admin_private
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
case 'post-record':
|
|
853
|
+
case 'get-records':
|
|
854
|
+
case 'subscription':
|
|
855
|
+
case 'get-subscription':
|
|
856
|
+
case 'del-records':
|
|
857
|
+
case 'get-table':
|
|
858
|
+
case 'get-tag':
|
|
859
|
+
case 'get-index':
|
|
860
|
+
case 'storage-info':
|
|
861
|
+
return {
|
|
862
|
+
private: record.record_private,
|
|
863
|
+
public: record.record_public
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
return get_ep()[auth ? 'private' : 'public'] + dest;
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
let endpoint = isExternalUrl || (await getEndpoint(url, !!auth));
|
|
872
|
+
let service = this.session?.attributes?.['custom:service'] || __connection?.service || this.service;
|
|
873
|
+
let service_owner = this.session?.attributes?.['custom:service_owner'] || __connection?.owner || this.service_owner;
|
|
874
|
+
|
|
875
|
+
if (meta) {
|
|
876
|
+
if (typeof meta === 'object' && !Array.isArray(meta)) {
|
|
877
|
+
meta = JSON.parse(JSON.stringify(meta));
|
|
878
|
+
}
|
|
879
|
+
else {
|
|
880
|
+
throw new SkapiError('Invalid meta data.', { code: 'INVALID_REQUEST' });
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (Array.isArray(data) || data && typeof data !== 'object') {
|
|
885
|
+
throw new SkapiError('Request data should be a JSON Object | FormData | HTMLFormElement.', { code: 'INVALID_REQUEST' });
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/* compose meta to send */
|
|
889
|
+
let required = { service, service_owner };
|
|
890
|
+
|
|
891
|
+
// set fetch options
|
|
892
|
+
let fetchOptions = {};
|
|
893
|
+
let { refresh = false } = options.fetchOptions || {};
|
|
894
|
+
|
|
895
|
+
if (options.fetchOptions && Object.keys(options.fetchOptions).length) {
|
|
896
|
+
// record fetch options
|
|
897
|
+
let fetOpt = checkParams(
|
|
898
|
+
{
|
|
899
|
+
limit: options?.fetchOptions?.limit || 100,
|
|
900
|
+
startKey: options?.fetchOptions?.startKey || null,
|
|
901
|
+
ascending: typeof options?.fetchOptions?.ascending === 'boolean' ? options.fetchOptions.ascending : true
|
|
902
|
+
},
|
|
903
|
+
{
|
|
904
|
+
limit: ['number', () => 100],
|
|
905
|
+
startKey: null,
|
|
906
|
+
ascending: ['boolean', () => true]
|
|
907
|
+
}
|
|
908
|
+
);
|
|
909
|
+
|
|
910
|
+
if (fetOpt.hasOwnProperty('limit') && typeof fetOpt.limit === 'number') {
|
|
911
|
+
if (fetOpt.limit > 1000) {
|
|
912
|
+
throw new SkapiError('Fetch limit should be below 1000.', { code: 'INVALID_REQUEST' });
|
|
913
|
+
}
|
|
914
|
+
Object.assign(fetchOptions, { limit: fetOpt.limit });
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (fetOpt.hasOwnProperty('startKey') && typeof fetOpt.startKey === 'object' && fetOpt.startKey && Object.keys(fetOpt.startKey)) {
|
|
918
|
+
Object.assign(fetchOptions, { startKey: fetOpt.startKey });
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if (fetOpt.hasOwnProperty('ascending') && typeof fetOpt.ascending === 'boolean') {
|
|
922
|
+
Object.assign(fetchOptions, { ascending: fetOpt.ascending });
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
Object.assign(required, fetchOptions);
|
|
927
|
+
|
|
928
|
+
// extract html form meta. null if not formdata.
|
|
929
|
+
let metaData = extractFormMetaData(data);
|
|
930
|
+
let isForm = metaData?.meta;
|
|
931
|
+
let fileKeys = metaData?.files || [];
|
|
932
|
+
|
|
933
|
+
let normalize_form = async (data: any, isForm: any, addRequired = false) => {
|
|
934
|
+
if (isForm === null) {
|
|
935
|
+
return data;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
let isFormEl = false;
|
|
939
|
+
if (data instanceof HTMLFormElement) {
|
|
940
|
+
data = new FormData(data);
|
|
941
|
+
isFormEl = true;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
if (!(data instanceof FormData)) {
|
|
945
|
+
return data;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (addRequired) {
|
|
949
|
+
// !do not change the order of iterations below!
|
|
950
|
+
for (let k in required) {
|
|
951
|
+
if (required[k] !== undefined) {
|
|
952
|
+
data.set(k, typeof required[k] === 'string' ? required[k] : new Blob([JSON.stringify(required[k])], {
|
|
953
|
+
type: 'application/json'
|
|
954
|
+
}));
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
for (let k in isForm) {
|
|
960
|
+
if (isForm[k] !== undefined) {
|
|
961
|
+
data[fileKeys.includes(k) ? "append" : "set"](k, typeof isForm[k] === 'string' ? isForm[k] : new Blob([JSON.stringify(isForm[k])], {
|
|
962
|
+
type: 'application/json'
|
|
963
|
+
}));
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
if (options?.fetchOptions?.formData) {
|
|
968
|
+
if (typeof options?.fetchOptions?.formData === 'function') {
|
|
969
|
+
if (!isFormEl) {
|
|
970
|
+
throw new SkapiError('Form data should be HTMLFormElement when formData() callback is used.', { code: 'INVALID_PARAMETER' });
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
let cb = options.fetchOptions.formData((data as FormData));
|
|
974
|
+
if (cb instanceof Promise) {
|
|
975
|
+
cb = await cb;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
if (cb instanceof FormData) {
|
|
979
|
+
data = cb;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
else {
|
|
983
|
+
throw new SkapiError('Callback for extractFormData() should return FormData', { code: 'INVALID_PARAMETER' });
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
else {
|
|
988
|
+
throw new SkapiError('Callback "formData" should type: function.', { code: 'INVALID_PARAMETER' });
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
return data;
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
if (meta) {
|
|
996
|
+
// add required to meta
|
|
997
|
+
// when meta has data, form data will be nested in 'data' key by the backend
|
|
998
|
+
meta = Object.assign(required, meta);
|
|
999
|
+
data = await normalize_form(data, isForm);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
else {
|
|
1003
|
+
// add required to data
|
|
1004
|
+
if (!data && isForm === null) {
|
|
1005
|
+
data = required;
|
|
1006
|
+
}
|
|
1007
|
+
else {
|
|
1008
|
+
if (isForm) {
|
|
1009
|
+
// data is either HTMLFormElement | FormData
|
|
1010
|
+
data = await normalize_form(data, isForm, true);
|
|
1011
|
+
}
|
|
1012
|
+
else {
|
|
1013
|
+
data = Object.assign(required, data);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
let requestKey = this.load_startKey_keys({
|
|
1019
|
+
params: Object.assign(isForm || (data || {}), required || {}),
|
|
1020
|
+
url: isExternalUrl || url,
|
|
1021
|
+
refresh: isForm ? true : refresh // should not use startKey when post is a form
|
|
1022
|
+
}); // returns requrestKey | cached data
|
|
1023
|
+
|
|
1024
|
+
if (requestKey && typeof requestKey === 'object') {
|
|
1025
|
+
return requestKey;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
if (typeof requestKey === 'string') {
|
|
1029
|
+
if (!(this.__pendingRequest[requestKey] instanceof Promise)) {
|
|
1030
|
+
// new request
|
|
1031
|
+
|
|
1032
|
+
let headers: Record<string, any> = {
|
|
1033
|
+
'Accept': '*/*'
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
if (token) {
|
|
1037
|
+
headers.Authorization = token;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
if (meta) {
|
|
1041
|
+
headers["Content-Meta"] = JSON.stringify(meta);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
if (options.hasOwnProperty('contentType')) {
|
|
1045
|
+
if (options?.contentType) {
|
|
1046
|
+
headers["Content-Type"] = options.contentType;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
else if (!(data instanceof FormData)) {
|
|
1051
|
+
headers["Content-Type"] = 'application/json';
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
let opt: RequestInit & { responseType?: string | null, headers: Record<string, any>; } = { headers };
|
|
1055
|
+
if (options?.responseType) {
|
|
1056
|
+
opt.responseType = options.responseType;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// pending call request
|
|
1060
|
+
// this prevents recursive calls
|
|
1061
|
+
if (method === 'post') {
|
|
1062
|
+
this.__pendingRequest[requestKey] = this._post(endpoint, data, opt);
|
|
1063
|
+
}
|
|
1064
|
+
else if (method === 'get') {
|
|
1065
|
+
this.__pendingRequest[requestKey] = this._get(endpoint, data, opt);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
try {
|
|
1070
|
+
let response = await this.__pendingRequest[requestKey];
|
|
1071
|
+
|
|
1072
|
+
// should not use startKey when post is a form (is a post)
|
|
1073
|
+
if (isForm) {
|
|
1074
|
+
return response;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
else {
|
|
1078
|
+
return await this.update_startKey_keys({
|
|
1079
|
+
hashedParam: requestKey,
|
|
1080
|
+
url,
|
|
1081
|
+
response
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
} catch (err) {
|
|
1086
|
+
throw err;
|
|
1087
|
+
} finally {
|
|
1088
|
+
// remove promise
|
|
1089
|
+
if (requestKey && this.__pendingRequest.hasOwnProperty(requestKey)) {
|
|
1090
|
+
delete this.__pendingRequest[requestKey];
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
// cache, handle database records
|
|
1096
|
+
|
|
1097
|
+
private load_startKey_keys(option: {
|
|
1098
|
+
params: Record<string, any>;
|
|
1099
|
+
url: string;
|
|
1100
|
+
refresh?: boolean;
|
|
1101
|
+
}): string | FetchResponse {
|
|
1102
|
+
|
|
1103
|
+
let { params = {}, url, refresh = false } = option || {};
|
|
1104
|
+
|
|
1105
|
+
if (params.hasOwnProperty('startKey')) {
|
|
1106
|
+
if (
|
|
1107
|
+
typeof params.startKey !== 'object' && !Object.keys(params.startKey).length ||
|
|
1108
|
+
// params.startKey !== 'start' && params.startKey !== 'end'
|
|
1109
|
+
params.startKey !== 'end'
|
|
1110
|
+
) {
|
|
1111
|
+
throw new SkapiError(`"${params.startKey}" is invalid startKey key.`, { code: 'INVALID_PARAMETER' });
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
switch (params.startKey) {
|
|
1115
|
+
case 'end':
|
|
1116
|
+
// end is always end
|
|
1117
|
+
return {
|
|
1118
|
+
list: [],
|
|
1119
|
+
startKey: 'end',
|
|
1120
|
+
endOfList: true
|
|
1121
|
+
};
|
|
1122
|
+
|
|
1123
|
+
case 'start':
|
|
1124
|
+
// deletes referenced object key
|
|
1125
|
+
delete params.startKey;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
let toHash = (() => {
|
|
1130
|
+
if (params && typeof params === 'object' && Object.keys(params).length) {
|
|
1131
|
+
// hash request parameters
|
|
1132
|
+
let paramsHash = JSON.parse(JSON.stringify(params));
|
|
1133
|
+
|
|
1134
|
+
function orderObjectKeys(obj: Record<string, any>) {
|
|
1135
|
+
function sortObject(obj: Record<string, any>): Record<string, any> {
|
|
1136
|
+
if (typeof obj === 'object' && obj) {
|
|
1137
|
+
return Object.keys(obj).sort().reduce((res, key) => ((res as any)[key] = obj[key], res), {});
|
|
1138
|
+
}
|
|
1139
|
+
return obj;
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
let _obj = sortObject(obj);
|
|
1143
|
+
for (let k in _obj) {
|
|
1144
|
+
if (_obj[k] && typeof _obj[k] === 'object') {
|
|
1145
|
+
_obj[k] = sortObject(obj[k]);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
return _obj;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
return JSON.stringify(orderObjectKeys(paramsHash)) + url + this.service;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
return url + this.service;
|
|
1156
|
+
})();
|
|
1157
|
+
|
|
1158
|
+
// let hashedParams = createHash('sha256').update(toHash).digest('hex');
|
|
1159
|
+
let hashedParams = sha256((() => {
|
|
1160
|
+
if (params && typeof params === 'object' && Object.keys(params).length) {
|
|
1161
|
+
// hash request parameters
|
|
1162
|
+
let paramsHash = JSON.parse(JSON.stringify(params));
|
|
1163
|
+
|
|
1164
|
+
function orderObjectKeys(obj: Record<string, any>) {
|
|
1165
|
+
function sortObject(obj: Record<string, any>): Record<string, any> {
|
|
1166
|
+
if (typeof obj === 'object' && obj) {
|
|
1167
|
+
return Object.keys(obj).sort().reduce((res, key) => ((res as any)[key] = obj[key], res), {});
|
|
1168
|
+
}
|
|
1169
|
+
return obj;
|
|
1170
|
+
};
|
|
1171
|
+
|
|
1172
|
+
let _obj = sortObject(obj);
|
|
1173
|
+
for (let k in _obj) {
|
|
1174
|
+
if (_obj[k] && typeof _obj[k] === 'object') {
|
|
1175
|
+
_obj[k] = sortObject(obj[k]);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
return _obj;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
return JSON.stringify(orderObjectKeys(paramsHash)) + url + this.service;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
return url + this.service;
|
|
1186
|
+
})());
|
|
1187
|
+
|
|
1188
|
+
if (refresh && this.__startKey_keys?.[url]?.[hashedParams]) {
|
|
1189
|
+
// init cache, init startKey
|
|
1190
|
+
|
|
1191
|
+
if (this.__cached_requests?.[url] && this.__cached_requests?.[url]?.[hashedParams]) {
|
|
1192
|
+
// delete cached data start
|
|
1193
|
+
delete this.__cached_requests[url][hashedParams];
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
if (Array.isArray(this.__startKey_keys[url][hashedParams]) && this.__startKey_keys[url][hashedParams].length) {
|
|
1197
|
+
// delete cache of all startkeys
|
|
1198
|
+
for (let p of this.__startKey_keys[url][hashedParams]) {
|
|
1199
|
+
let hashedParams_cached = hashedParams + sha256(JSON.stringify(p));
|
|
1200
|
+
// let hashedParams_cached = hashedParams + createHash('sha256').update(JSON.stringify(p)).digest('hex');
|
|
1201
|
+
|
|
1202
|
+
if (this.__cached_requests?.[url] && this.__cached_requests?.[url]?.[hashedParams_cached]) {
|
|
1203
|
+
delete this.__cached_requests[url][hashedParams_cached];
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// delete start key lists
|
|
1209
|
+
delete this.__startKey_keys[url][hashedParams];
|
|
1210
|
+
|
|
1211
|
+
return hashedParams;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
if (!Array.isArray(this.__startKey_keys?.[url]?.[hashedParams])) {
|
|
1215
|
+
// startkey does not exists
|
|
1216
|
+
return hashedParams;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// hashed params exists
|
|
1220
|
+
let list_of_startKeys = this.__startKey_keys[url][hashedParams]; // [{<startKey key>}, ...'end']
|
|
1221
|
+
let last_startKey_key = list_of_startKeys[list_of_startKeys.length - 1];
|
|
1222
|
+
let cache_hashedParams = hashedParams;
|
|
1223
|
+
// if (last_startKey_key !== 'start') {
|
|
1224
|
+
if (last_startKey_key) {
|
|
1225
|
+
// use last start key
|
|
1226
|
+
|
|
1227
|
+
if (last_startKey_key === 'end') {
|
|
1228
|
+
return {
|
|
1229
|
+
list: [],
|
|
1230
|
+
startKey: 'end',
|
|
1231
|
+
endOfList: true
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
else {
|
|
1236
|
+
// cache_hashedParams += createHash('sha256').update(last_startKey_key).digest('hex');
|
|
1237
|
+
cache_hashedParams += sha256(last_startKey_key);
|
|
1238
|
+
params.startKey = JSON.parse(last_startKey_key);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
if (this.__cached_requests?.[url]?.[cache_hashedParams]) {
|
|
1243
|
+
// return data if there is cache
|
|
1244
|
+
return this.__cached_requests?.[url]?.[cache_hashedParams];
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
return hashedParams;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
private update_startKey_keys = async (option: Record<string, any>) => {
|
|
1251
|
+
let { hashedParam, url, response } = option;
|
|
1252
|
+
let fetched = null;
|
|
1253
|
+
|
|
1254
|
+
if (response instanceof Promise) {
|
|
1255
|
+
fetched = await response;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
else {
|
|
1259
|
+
fetched = response;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
if (
|
|
1263
|
+
typeof fetched !== 'object' ||
|
|
1264
|
+
!fetched.hasOwnProperty('startKey') ||
|
|
1265
|
+
!hashedParam ||
|
|
1266
|
+
!url
|
|
1267
|
+
) {
|
|
1268
|
+
// no startkey no caching
|
|
1269
|
+
return fetched;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// has start key
|
|
1273
|
+
// startkey is key for next fetch
|
|
1274
|
+
|
|
1275
|
+
// this.__startKey_keys[url] = {
|
|
1276
|
+
// [hashedParam]: ['{<startKey key>}', ...'end'],
|
|
1277
|
+
// ...
|
|
1278
|
+
// }
|
|
1279
|
+
|
|
1280
|
+
// this.__cached_requests[url][hashsedParams + sha256(JSON.stringify(startKey))] = {
|
|
1281
|
+
// data
|
|
1282
|
+
// ...
|
|
1283
|
+
// }
|
|
1284
|
+
|
|
1285
|
+
if (!this.__startKey_keys.hasOwnProperty(url)) {
|
|
1286
|
+
// create url key to store startKey key list if it doesnt exists
|
|
1287
|
+
this.__startKey_keys[url] = {};
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
if (!this.__cached_requests?.[url]) {
|
|
1291
|
+
this.__cached_requests[url] = {};
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
this.__cached_requests[url][hashedParam] = fetched;
|
|
1295
|
+
|
|
1296
|
+
if (!this.__startKey_keys[url].hasOwnProperty(hashedParam)) {
|
|
1297
|
+
this.__startKey_keys[url][hashedParam] = [];
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
let startKey_string = JSON.stringify(fetched.startKey);
|
|
1301
|
+
if (!this.__startKey_keys[url][hashedParam].includes(startKey_string)) {
|
|
1302
|
+
this.__startKey_keys[url][hashedParam].push(startKey_string);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
this.__cached_requests[url][hashedParam] = fetched;
|
|
1306
|
+
|
|
1307
|
+
return Object.assign({ startKey_list: this.__startKey_keys[url][hashedParam] }, fetched);
|
|
1308
|
+
};
|
|
1309
|
+
|
|
1310
|
+
private _fetch = async (url: string, opt: RequestInit, responseType: string) => {
|
|
1311
|
+
|
|
1312
|
+
let response: Record<string, any> = await fetch(url, opt);
|
|
1313
|
+
|
|
1314
|
+
if (responseType) {
|
|
1315
|
+
if (response.status === 200) {
|
|
1316
|
+
return await response[responseType]();
|
|
1317
|
+
} else {
|
|
1318
|
+
throw response;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
let received = await response.text();
|
|
1323
|
+
try {
|
|
1324
|
+
received = JSON.parse(received);
|
|
1325
|
+
} catch (err) {
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
if (response.status === 200) {
|
|
1329
|
+
if (typeof received === 'object' && opt.method === 'GET' && received.hasOwnProperty('body')) {
|
|
1330
|
+
try {
|
|
1331
|
+
received = JSON.parse(received.body);
|
|
1332
|
+
} catch (err) { }
|
|
1333
|
+
}
|
|
1334
|
+
return received;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
else {
|
|
1338
|
+
let status = response?.status;
|
|
1339
|
+
let errCode = [
|
|
1340
|
+
'INVALID_CORS',
|
|
1341
|
+
'INVALID_REQUEST',
|
|
1342
|
+
'SERVICE_DISABLED',
|
|
1343
|
+
'INVALID_PARAMETER',
|
|
1344
|
+
'ERROR',
|
|
1345
|
+
'EXISTS',
|
|
1346
|
+
'NOT_EXISTS'
|
|
1347
|
+
];
|
|
1348
|
+
|
|
1349
|
+
if (typeof received === 'object' && received?.message) {
|
|
1350
|
+
let code = ((status ? status.toString() : null) || 'ERROR');
|
|
1351
|
+
throw new SkapiError(received?.message, { code: code });
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
else if (typeof received === 'string') {
|
|
1355
|
+
let errMsg = received.split(':');
|
|
1356
|
+
let code = errMsg.splice(0, 1)[0];
|
|
1357
|
+
throw new SkapiError(errMsg.join(''), { code: (errCode.includes(code) ? code : 'ERROR') });
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
throw response;
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
|
|
1364
|
+
private _post(url: string, params: Record<string, any>, option: RequestInit & { responseType?: string | null, headers: Record<string, any>; }) {
|
|
1365
|
+
let responseType = null;
|
|
1366
|
+
if (option.hasOwnProperty('responseType')) {
|
|
1367
|
+
responseType = option.responseType;
|
|
1368
|
+
delete option.responseType;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
let opt = Object.assign(
|
|
1372
|
+
{
|
|
1373
|
+
method: 'POST'
|
|
1374
|
+
},
|
|
1375
|
+
option,
|
|
1376
|
+
{
|
|
1377
|
+
body: params instanceof FormData ? params : JSON.stringify(params)
|
|
1378
|
+
}
|
|
1379
|
+
);
|
|
1380
|
+
|
|
1381
|
+
return this._fetch(url, opt, responseType);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
private _get = async (url: string, params: Record<string, any>, option: RequestInit & { responseType?: string | null, headers: Record<string, any>; }) => {
|
|
1385
|
+
if (params && typeof params === 'object' && Object.keys(params).length) {
|
|
1386
|
+
if (url.substring(url.length - 1) !== '?') {
|
|
1387
|
+
url = url + '?';
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
let query = Object.keys(params)
|
|
1391
|
+
.map(k => {
|
|
1392
|
+
let value = params[k];
|
|
1393
|
+
if (typeof value !== 'string') {
|
|
1394
|
+
value = JSON.stringify(value);
|
|
1395
|
+
}
|
|
1396
|
+
return encodeURIComponent(k) + '=' + encodeURIComponent(value);
|
|
1397
|
+
})
|
|
1398
|
+
.join('&');
|
|
1399
|
+
|
|
1400
|
+
url += query;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
let responseType = null;
|
|
1404
|
+
if (option.hasOwnProperty('responseType')) {
|
|
1405
|
+
responseType = option.responseType;
|
|
1406
|
+
delete option.responseType;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
let opt = Object.assign(
|
|
1410
|
+
{
|
|
1411
|
+
method: 'GET'
|
|
1412
|
+
},
|
|
1413
|
+
option
|
|
1414
|
+
);
|
|
1415
|
+
|
|
1416
|
+
return this._fetch(url, opt, responseType);
|
|
1417
|
+
};
|
|
1418
|
+
|
|
1419
|
+
/**
|
|
1420
|
+
* Uploads data to your service database.<br>
|
|
1421
|
+
* We will call data in database as record.
|
|
1422
|
+
* <ul>
|
|
1423
|
+
* <li>
|
|
1424
|
+
* <b>About tables:</b><br>
|
|
1425
|
+
* When uploading new records, table setting is required.<br>
|
|
1426
|
+
* Database table are created automatically on upload.<br>
|
|
1427
|
+
* You can have infinite numbers of tables.<br>
|
|
1428
|
+
* You cannot change table settings once it's uploaded.<br>
|
|
1429
|
+
* Database table will be deleted automatically if there is no record in the table.<br>
|
|
1430
|
+
* <br>
|
|
1431
|
+
* <b>NOTE:</b> Whitespace or special characters are not allowed in table name.<br>
|
|
1432
|
+
* </li>
|
|
1433
|
+
* <li>
|
|
1434
|
+
* <b>About Index:</b><br>
|
|
1435
|
+
* Index help you to categorize records paired by "name" and "value".<br>
|
|
1436
|
+
* Index caculates total sum of values if the value is number or boolean.<br>
|
|
1437
|
+
* <b>NOTE:</b> Whitespace or special characters are not allowed in index name or value.<br>
|
|
1438
|
+
* </li>
|
|
1439
|
+
* <li>
|
|
1440
|
+
* <b>About Tags:</b><br>
|
|
1441
|
+
* Tags let's you categorize records by given tags.<br>
|
|
1442
|
+
* You can have multiple tags on single record.<br>
|
|
1443
|
+
* <b>NOTE:</b> Whitespace or special characters are not allowed in tags.<br>
|
|
1444
|
+
* </li>
|
|
1445
|
+
* </ul>
|
|
1446
|
+
* @category Database
|
|
1447
|
+
*/
|
|
1448
|
+
@formResponse()
|
|
1449
|
+
async postRecord(
|
|
1450
|
+
/** Any type of data to store. If undefined, does not update the data. */
|
|
1451
|
+
form: Form | any,
|
|
1452
|
+
option?: PostRecordParams & FormCallbacks
|
|
1453
|
+
): Promise<RecordData> {
|
|
1454
|
+
|
|
1455
|
+
let is_admin = await this.requireAdmin({ ignoreVerification: true });
|
|
1456
|
+
|
|
1457
|
+
let { formData } = option;
|
|
1458
|
+
let fetchOptions: Record<string, any> = {};
|
|
1459
|
+
|
|
1460
|
+
if (typeof formData === 'function') {
|
|
1461
|
+
fetchOptions.formData = formData;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
option = checkParams(option || {}, {
|
|
1465
|
+
record_id: 'string',
|
|
1466
|
+
access_group: ['number', 'private'],
|
|
1467
|
+
table: ['string', () => {
|
|
1468
|
+
if (!option?.record_id) {
|
|
1469
|
+
throw new SkapiError('Either "record_id" or "table" should have a value.', { code: 'INVALID_PARAMETER' });
|
|
1470
|
+
}
|
|
1471
|
+
}],
|
|
1472
|
+
subscription_group: 'number',
|
|
1473
|
+
reference: 'string',
|
|
1474
|
+
index: {
|
|
1475
|
+
name: 'string',
|
|
1476
|
+
value: ['string', 'number', 'boolean']
|
|
1477
|
+
},
|
|
1478
|
+
tags: (v: string | string[]) => {
|
|
1479
|
+
if (v === null) {
|
|
1480
|
+
return v;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
if (typeof v === 'string') {
|
|
1484
|
+
return [v];
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
if (Array.isArray(v)) {
|
|
1488
|
+
for (let i of v) {
|
|
1489
|
+
if (typeof i !== 'string') {
|
|
1490
|
+
throw new SkapiError(`"tags" should be type: <string | string[]>`, { code: 'INVALID_PARAMETER' });
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
return v;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
throw new SkapiError(`"tags" should be type: <string | string[]>`, { code: 'INVALID_PARAMETER' });
|
|
1497
|
+
},
|
|
1498
|
+
config: {
|
|
1499
|
+
reference_limit: ['number', null],
|
|
1500
|
+
allow_multiple_reference: 'boolean',
|
|
1501
|
+
private_access: (v: string | string[]) => {
|
|
1502
|
+
let param = 'config.private_access';
|
|
1503
|
+
|
|
1504
|
+
if (v && typeof v === 'string') {
|
|
1505
|
+
v = [v];
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
if (Array.isArray(v)) {
|
|
1509
|
+
for (let u of v) {
|
|
1510
|
+
validateUserId(u, `User ID in "${param}"`);
|
|
1511
|
+
|
|
1512
|
+
if (this.user && u === this.user.user_id) {
|
|
1513
|
+
throw new SkapiError(`"${param}" should not be the uploader's user ID.`, { code: 'INVALID_PARAMETER' });
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
else {
|
|
1519
|
+
throw new SkapiError(`"${param}" should be an array of user ID.`, { code: 'INVALID_PARAMETER' });
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
return v;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}, [], ['response', 'formData', 'onerror']);
|
|
1526
|
+
|
|
1527
|
+
// callbacks should be removed after checkparams
|
|
1528
|
+
delete option.response;
|
|
1529
|
+
delete option.formData;
|
|
1530
|
+
delete option.onerror;
|
|
1531
|
+
|
|
1532
|
+
if (option?.index) {
|
|
1533
|
+
// index name allows periods. white space is invalid.
|
|
1534
|
+
if (!option.index?.name || typeof option.index?.name !== 'string') {
|
|
1535
|
+
throw new SkapiError('"index.name" is required. type: string.', { code: 'INVALID_PARAMETER' });
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
checkWhiteSpaceAndSpecialChars(option.index.name, 'index name', true);
|
|
1539
|
+
|
|
1540
|
+
if (!option.index.hasOwnProperty('value')) {
|
|
1541
|
+
throw new SkapiError('"index.value" is required.', { code: 'INVALID_PARAMETER' });
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
if (typeof option.index.value === 'string') {
|
|
1545
|
+
// index name allows periods. white space is invalid.
|
|
1546
|
+
checkWhiteSpaceAndSpecialChars(option.index.value, 'index value', false, true);
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
else if (typeof option.index.value === 'number') {
|
|
1550
|
+
if (option.index.value > this.__index_number_range || option.index.value < -this.__index_number_range) {
|
|
1551
|
+
throw new SkapiError(`Number value should be within range -${this.__index_number_range} ~ +${this.__index_number_range}`, { code: 'INVALID_PARAMETER' });
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
|
|
1557
|
+
if (is_admin) {
|
|
1558
|
+
if (option?.access_group === 'private') {
|
|
1559
|
+
throw new SkapiError('Service owner cannot write private records.', { code: 'INVALID_REQUEST' });
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
if (option.hasOwnProperty('subscription_group')) {
|
|
1563
|
+
throw new SkapiError('Service owner cannot write to subscription table.', { code: 'INVALID_REQUEST' });
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
let options = { auth: true };
|
|
1568
|
+
let postData = null;
|
|
1569
|
+
|
|
1570
|
+
if (form instanceof HTMLFormElement || form instanceof FormData) {
|
|
1571
|
+
Object.assign(options, { meta: option });
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
else {
|
|
1575
|
+
postData = Object.assign({ data: form }, option);
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
if (Object.keys(fetchOptions).length) {
|
|
1579
|
+
Object.assign(options, { fetchOptions });
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
return normalize_record_data(await this.request('post-record', postData || form, options));
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
/**
|
|
1586
|
+
* Get records from service database.
|
|
1587
|
+
* @category Database
|
|
1588
|
+
*/
|
|
1589
|
+
async getRecords(params: GetRecordParams, fetchOptions?: FetchOptions): Promise<FetchResponse> {
|
|
1590
|
+
const indexTypes = {
|
|
1591
|
+
'$record_id': 'string',
|
|
1592
|
+
'$updated': 'number',
|
|
1593
|
+
'$uploaded': 'number',
|
|
1594
|
+
'$referenced_count': 'number'
|
|
1595
|
+
};
|
|
1596
|
+
|
|
1597
|
+
const struct = {
|
|
1598
|
+
table: 'string',
|
|
1599
|
+
reference: 'string',
|
|
1600
|
+
access_group: ['number', 'private'],
|
|
1601
|
+
subscription: {
|
|
1602
|
+
user_id: (v: string) => validateUserId(v, 'User ID in "subscription.user_id"'),
|
|
1603
|
+
group: 'number'
|
|
1604
|
+
},
|
|
1605
|
+
index: {
|
|
1606
|
+
name: (v: string) => {
|
|
1607
|
+
if (typeof v !== 'string') {
|
|
1608
|
+
throw new SkapiError('"index.name" should be type: string.', { code: 'INVALID_PARAMETER' });
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
if (indexTypes.hasOwnProperty(v)) {
|
|
1612
|
+
return v;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
return checkWhiteSpaceAndSpecialChars(v, 'index.name', true, false);
|
|
1616
|
+
},
|
|
1617
|
+
value: (v: number | boolean | string) => {
|
|
1618
|
+
if (params.index?.name && indexTypes.hasOwnProperty(params.index.name)) {
|
|
1619
|
+
let tp = indexTypes[params.index.name];
|
|
1620
|
+
|
|
1621
|
+
if (typeof v === tp) {
|
|
1622
|
+
return v;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
else {
|
|
1626
|
+
throw new SkapiError(`"index.value" should be type: ${tp}.`, { code: 'INVALID_PARAMETER' });
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
if (typeof v === 'number') {
|
|
1631
|
+
if (v > this.__index_number_range || v < -this.__index_number_range) {
|
|
1632
|
+
throw new SkapiError(`Number value should be within range -${this.__index_number_range} ~ +${this.__index_number_range}`, { code: 'INVALID_PARAMETER' });
|
|
1633
|
+
}
|
|
1634
|
+
return v;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
else if (typeof v === 'boolean') {
|
|
1638
|
+
return v;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
else {
|
|
1642
|
+
// is string
|
|
1643
|
+
return checkWhiteSpaceAndSpecialChars((v as string), 'index.value', false, true);
|
|
1644
|
+
}
|
|
1645
|
+
},
|
|
1646
|
+
condition: ['gt', 'gte', 'lt', 'lte', '>', '>=', '<', '<=', '=', 'eq', '!=', 'ne'],
|
|
1647
|
+
range: (v: number | boolean | string) => {
|
|
1648
|
+
if (!('value' in params.index)) {
|
|
1649
|
+
throw new SkapiError('"index.value" is required.', { code: 'INVALID_PARAMETER' });
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
if (params.index.name === '$record_id') {
|
|
1653
|
+
throw new SkapiError(`Cannot do "index.range" on ${params.index.name}`, { code: 'INVALID_PARAMETER' });
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
if (typeof params.index.value !== typeof v) {
|
|
1657
|
+
throw new SkapiError('"index.range" type should match the type of "index.value".', { code: 'INVALID_PARAMETER' });
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
if (typeof v === 'string') {
|
|
1661
|
+
return checkWhiteSpaceAndSpecialChars(v, 'index.value');
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
return v;
|
|
1665
|
+
}
|
|
1666
|
+
},
|
|
1667
|
+
tags: (v: string | string[]) => {
|
|
1668
|
+
if (typeof v === 'string') {
|
|
1669
|
+
return [v];
|
|
1670
|
+
}
|
|
1671
|
+
else if (Array.isArray(v)) {
|
|
1672
|
+
if (v.length > 10) {
|
|
1673
|
+
throw new SkapiError('Cannot query more than 10 tags at once.', { code: 'INVALID_REQUEST' });
|
|
1674
|
+
}
|
|
1675
|
+
for (let s of v) {
|
|
1676
|
+
if (typeof s !== 'string') {
|
|
1677
|
+
throw new SkapiError('Tags should be type: <string | string[]>', { code: 'INVALID_PARAMETER' });
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
return v;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
};
|
|
1684
|
+
|
|
1685
|
+
params = checkParams(params || {}, struct, ['table']);
|
|
1686
|
+
|
|
1687
|
+
if (params?.subscription && !this.session) {
|
|
1688
|
+
throw new SkapiError('Requires login.', { code: 'INVALID_REQUEST' });
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
if (params?.tags) {
|
|
1692
|
+
let tagFetch = [];
|
|
1693
|
+
let getStartKey = fetchOptions?.startKey || null;
|
|
1694
|
+
|
|
1695
|
+
for (let t of params.tags) {
|
|
1696
|
+
let params_copy = JSON.parse(JSON.stringify(params));
|
|
1697
|
+
params_copy.tag = t;
|
|
1698
|
+
delete params_copy.tags;
|
|
1699
|
+
|
|
1700
|
+
let fetchOpt = fetchOptions ? JSON.parse(JSON.stringify(fetchOptions)) : null;
|
|
1701
|
+
if (fetchOpt) {
|
|
1702
|
+
delete fetchOpt.startKey;
|
|
1703
|
+
|
|
1704
|
+
if (getStartKey && getStartKey?.[t]) {
|
|
1705
|
+
fetchOpt.startKey = getStartKey[t];
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
tagFetch.push(this.request(
|
|
1710
|
+
'get-records',
|
|
1711
|
+
params_copy,
|
|
1712
|
+
Object.assign(
|
|
1713
|
+
{ auth: params.hasOwnProperty('access_group') && (params.access_group === 'private' || params.access_group > 0) ? true : !!this.session },
|
|
1714
|
+
{ fetchOptions: fetchOpt }
|
|
1715
|
+
)));
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
let list = [];
|
|
1719
|
+
let startKey = {};
|
|
1720
|
+
let res_all = await Promise.all(tagFetch);
|
|
1721
|
+
|
|
1722
|
+
for (let res of res_all) {
|
|
1723
|
+
for (let i in res.list) {
|
|
1724
|
+
if (tagFetch.includes(res.list[i].rec)) {
|
|
1725
|
+
continue;
|
|
1726
|
+
}
|
|
1727
|
+
tagFetch.push(res.list[i]);
|
|
1728
|
+
list.push(normalize_record_data(res.list[i]));
|
|
1729
|
+
};
|
|
1730
|
+
if (res.startKey) {
|
|
1731
|
+
if (Array.isArray(params.tags)) {
|
|
1732
|
+
let tag = params.tags.splice(0, 1);
|
|
1733
|
+
startKey[tag[0]] = res.startKey;
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
let endOfList = true;
|
|
1739
|
+
for (let k in startKey) {
|
|
1740
|
+
if (startKey[k] && startKey[k] !== 'end') {
|
|
1741
|
+
endOfList = false;
|
|
1742
|
+
break;
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
return {
|
|
1747
|
+
list,
|
|
1748
|
+
endOfList,
|
|
1749
|
+
startKey
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
let result = await this.request(
|
|
1754
|
+
'get-records',
|
|
1755
|
+
params,
|
|
1756
|
+
Object.assign(
|
|
1757
|
+
{ auth: params.hasOwnProperty('access_group') && (params.access_group === 'private' || params.access_group > 0) ? true : !!this.user },
|
|
1758
|
+
{ fetchOptions }
|
|
1759
|
+
)
|
|
1760
|
+
);
|
|
1761
|
+
|
|
1762
|
+
for (let i in result.list) { result.list[i] = normalize_record_data(result.list[i]); };
|
|
1763
|
+
|
|
1764
|
+
return result;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
/**
|
|
1768
|
+
* Retrieve table info of record database.
|
|
1769
|
+
* Get table info of record database.
|
|
1770
|
+
*
|
|
1771
|
+
* ```
|
|
1772
|
+
* // Get information in 'MyTable'.
|
|
1773
|
+
* let getTable = await skapi.getTable({
|
|
1774
|
+
* table: 'MyTable'
|
|
1775
|
+
* });
|
|
1776
|
+
*
|
|
1777
|
+
* // Get all list of tables in service in lexographical order.
|
|
1778
|
+
* let getTablePrivate = await skapi.getTable();
|
|
1779
|
+
*
|
|
1780
|
+
* // Get all list of tables in service in lexographical order.
|
|
1781
|
+
* let getTablePrivate = await skapi.getTable();
|
|
1782
|
+
* ```
|
|
1783
|
+
*/
|
|
1784
|
+
async getTable(
|
|
1785
|
+
params: {
|
|
1786
|
+
/** Table name. If omitted fetch all list of tables. */
|
|
1787
|
+
table?: string;
|
|
1788
|
+
condition?: string;
|
|
1789
|
+
},
|
|
1790
|
+
fetchOptions?: FetchOptions
|
|
1791
|
+
): Promise<FetchResponse> {
|
|
1792
|
+
let res = await this.request('get-table', checkParams(params || {}, {
|
|
1793
|
+
table: 'string',
|
|
1794
|
+
condition: ['gt', 'gte', 'lt', 'lte', '>', '>=', '<', '<=', '=', 'eq', '!=', 'ne']
|
|
1795
|
+
}), Object.assign({ auth: true }, { fetchOptions }));
|
|
1796
|
+
|
|
1797
|
+
let convert = {
|
|
1798
|
+
'cnt_rec': 'number_of_records',
|
|
1799
|
+
'tbl': 'table',
|
|
1800
|
+
'srvc': 'service'
|
|
1801
|
+
};
|
|
1802
|
+
|
|
1803
|
+
if (Array.isArray(res.list)) {
|
|
1804
|
+
for (let t of res.list) {
|
|
1805
|
+
for (let k in convert) {
|
|
1806
|
+
if (t.hasOwnProperty(k)) {
|
|
1807
|
+
t[convert[k]] = t[k];
|
|
1808
|
+
delete t[k];
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
return res;
|
|
1815
|
+
}
|
|
1816
|
+
/**
|
|
1817
|
+
* Retrieve index info of record database.
|
|
1818
|
+
* Get index info of record database.
|
|
1819
|
+
*
|
|
1820
|
+
* ```
|
|
1821
|
+
*
|
|
1822
|
+
* // Get info of "Gold" index in "MyTable" table.
|
|
1823
|
+
* let getIndex = await skapi.getIndex({
|
|
1824
|
+
* index: 'Gold',
|
|
1825
|
+
* table: 'MyTable'
|
|
1826
|
+
* });
|
|
1827
|
+
*
|
|
1828
|
+
* // Get all index in average value order in "MyTable" table.
|
|
1829
|
+
* let getIndexAll = await skapi.getIndex({
|
|
1830
|
+
* order_by: {
|
|
1831
|
+
* name: 'average_number'
|
|
1832
|
+
* },
|
|
1833
|
+
* table: 'MyTable'
|
|
1834
|
+
* });
|
|
1835
|
+
*
|
|
1836
|
+
* ```
|
|
1837
|
+
* @category Database
|
|
1838
|
+
*/
|
|
1839
|
+
async getIndex(
|
|
1840
|
+
params: {
|
|
1841
|
+
/** Table name */
|
|
1842
|
+
table: string;
|
|
1843
|
+
/** Index name. When period is at the end of name, querys nested index keys. */
|
|
1844
|
+
index?: string;
|
|
1845
|
+
/** Queries order by */
|
|
1846
|
+
order_by: {
|
|
1847
|
+
/** Key name to order. */
|
|
1848
|
+
name: 'average_number' | 'total_number' | 'number_count' | 'average_bool' | 'total_bool' | 'bool_count' | 'string_count' | 'index_name';
|
|
1849
|
+
/** Value to query. */
|
|
1850
|
+
value?: number | boolean | string;
|
|
1851
|
+
/** "order_by.value" is required for condition. */
|
|
1852
|
+
condition?: 'gt' | 'gte' | 'lt' | 'lte' | '>' | '>=' | '<' | '<=' | '=' | 'eq' | '!=' | 'ne';
|
|
1853
|
+
};
|
|
1854
|
+
},
|
|
1855
|
+
fetchOptions?: FetchOptions
|
|
1856
|
+
): Promise<FetchResponse> {
|
|
1857
|
+
|
|
1858
|
+
let p = checkParams(
|
|
1859
|
+
params || {},
|
|
1860
|
+
{
|
|
1861
|
+
table: 'string',
|
|
1862
|
+
index: (v: string) => checkWhiteSpaceAndSpecialChars(v, 'index name', true, false),
|
|
1863
|
+
order_by: {
|
|
1864
|
+
name: [
|
|
1865
|
+
'average_number',
|
|
1866
|
+
'total_number',
|
|
1867
|
+
'number_count',
|
|
1868
|
+
'average_bool',
|
|
1869
|
+
'total_bool',
|
|
1870
|
+
'bool_count',
|
|
1871
|
+
'string_count',
|
|
1872
|
+
'index_name'
|
|
1873
|
+
],
|
|
1874
|
+
value: ['string', 'number', 'boolean'],
|
|
1875
|
+
condition: ['gt', 'gte', 'lt', 'lte', '>', '>=', '<', '<=', '=', 'eq', '!=', 'ne']
|
|
1876
|
+
}
|
|
1877
|
+
},
|
|
1878
|
+
['table']
|
|
1879
|
+
);
|
|
1880
|
+
|
|
1881
|
+
if (p.hasOwnProperty('order_by')) {
|
|
1882
|
+
if (p.order_by === 'index_name') {
|
|
1883
|
+
if (!p.hasOwnProperty('index')) {
|
|
1884
|
+
throw new SkapiError('"index" is required for ordered by "index_name".', { code: 'INVALID_PARAMETER' });
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
if (p.index.substring(p.index.length - 1) !== '.') {
|
|
1888
|
+
throw new SkapiError('"index" should be parent "index name".', { code: 'INVALID_PARAMETER' });
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
if (p.order_by.hasOwnProperty('condition') && !p.order_by.hasOwnProperty('value')) {
|
|
1893
|
+
throw new SkapiError('"value" is required for "condition".', { code: 'INVALID_PARAMETER' });
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
let res = await this.request(
|
|
1898
|
+
'get-index',
|
|
1899
|
+
p,
|
|
1900
|
+
Object.assign(
|
|
1901
|
+
{ auth: true },
|
|
1902
|
+
{ fetchOptions }
|
|
1903
|
+
)
|
|
1904
|
+
);
|
|
1905
|
+
|
|
1906
|
+
let convert = {
|
|
1907
|
+
'cnt_bool': 'boolean_count',
|
|
1908
|
+
'cnt_numb': 'number_count',
|
|
1909
|
+
'totl_numb': 'total_number',
|
|
1910
|
+
'totl_bool': 'total_bool',
|
|
1911
|
+
'avrg_numb': 'average_number',
|
|
1912
|
+
'avrg_bool': 'average_bool',
|
|
1913
|
+
'cnt_str': 'string_count'
|
|
1914
|
+
};
|
|
1915
|
+
|
|
1916
|
+
if (Array.isArray(res.list)) {
|
|
1917
|
+
res.list = res.list.map((i: Record<string, any>) => {
|
|
1918
|
+
let iSplit = i.idx.split('/');
|
|
1919
|
+
let resolved: Record<string, any> = {
|
|
1920
|
+
table: iSplit[1],
|
|
1921
|
+
index: iSplit[2],
|
|
1922
|
+
number_of_records: i.cnt_rec
|
|
1923
|
+
};
|
|
1924
|
+
|
|
1925
|
+
for (let k in convert) {
|
|
1926
|
+
if (i?.[k]) {
|
|
1927
|
+
resolved[convert[k]] = i[k];
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
if (resolved?.number_of_number_values) {
|
|
1931
|
+
resolved.average_of_number_values = i.totl_numb / i.cnt_numb;
|
|
1932
|
+
}
|
|
1933
|
+
if (resolved?.number_of_boolean_values) {
|
|
1934
|
+
resolved.average_of_boolean_values = i.totl_bool / i.cnt_bool;
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
return resolved;
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
return res;
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
|
|
1946
|
+
/**
|
|
1947
|
+
* Retrieve filter info of database table.
|
|
1948
|
+
*
|
|
1949
|
+
* ```
|
|
1950
|
+
* // Get all tags from "MyTable" in record number orders.
|
|
1951
|
+
* let getTagAll = await skapi.getTag({
|
|
1952
|
+
* table: "MyTable"
|
|
1953
|
+
* });
|
|
1954
|
+
*
|
|
1955
|
+
* // Get info on 'Gold' from "MyTable".
|
|
1956
|
+
* let getTagInfo = await skapi.getTag({
|
|
1957
|
+
* table: "MyTable",
|
|
1958
|
+
* tag: 'Gold'
|
|
1959
|
+
* });
|
|
1960
|
+
* ```
|
|
1961
|
+
* @category Database
|
|
1962
|
+
*/
|
|
1963
|
+
async getTag(
|
|
1964
|
+
params: {
|
|
1965
|
+
/** Table name */
|
|
1966
|
+
table: string;
|
|
1967
|
+
/** Tag name */
|
|
1968
|
+
tag: string;
|
|
1969
|
+
/** String query condition for tag name. */
|
|
1970
|
+
condition: 'gt' | 'gte' | 'lt' | 'lte' | '>' | '>=' | '<' | '<=' | '=' | 'eq' | '!=' | 'ne';
|
|
1971
|
+
},
|
|
1972
|
+
fetchOptions?: FetchOptions
|
|
1973
|
+
): Promise<FetchResponse> {
|
|
1974
|
+
|
|
1975
|
+
let res = await this.request(
|
|
1976
|
+
'get-tag',
|
|
1977
|
+
checkParams(params || {},
|
|
1978
|
+
{
|
|
1979
|
+
table: 'string',
|
|
1980
|
+
tag: 'string',
|
|
1981
|
+
condition: ['gt', 'gte', 'lt', 'lte', '>', '>=', '<', '<=', '=', 'eq', '!=', 'ne']
|
|
1982
|
+
}
|
|
1983
|
+
),
|
|
1984
|
+
Object.assign({ auth: true }, { fetchOptions })
|
|
1985
|
+
);
|
|
1986
|
+
|
|
1987
|
+
if (Array.isArray(res.list)) {
|
|
1988
|
+
for (let i in res.list) {
|
|
1989
|
+
let item = res.list[i];
|
|
1990
|
+
let tSplit = item.tag.split('/');
|
|
1991
|
+
res.list[i] = {
|
|
1992
|
+
table: tSplit[1],
|
|
1993
|
+
tag: tSplit[0],
|
|
1994
|
+
number_of_records: item.cnt_rec
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
return res;
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
/**
|
|
2003
|
+
* Deletes specific records or bulk of records under certain table, index, tag.
|
|
2004
|
+
* <br>
|
|
2005
|
+
* <b>WARNING:</b> Deleted record cannot be restored.
|
|
2006
|
+
*
|
|
2007
|
+
* ```
|
|
2008
|
+
* // Delete record
|
|
2009
|
+
* // users wont be able to delete other users record.
|
|
2010
|
+
* await skapi.deleteRecords({
|
|
2011
|
+
* record_id: ['record id 1', 'record id 2']
|
|
2012
|
+
* });
|
|
2013
|
+
*
|
|
2014
|
+
* // Delete all my record in "MyTable" in access group 1.
|
|
2015
|
+
* await skapi.deleteRecords({
|
|
2016
|
+
* access_group: 1,
|
|
2017
|
+
* table: {
|
|
2018
|
+
* name: 'MyTable'
|
|
2019
|
+
* }
|
|
2020
|
+
* });
|
|
2021
|
+
*
|
|
2022
|
+
* // Delete all record in subscription table of group 1 in "MyTable" in access group 0.
|
|
2023
|
+
* await skapi.deleteRecords({
|
|
2024
|
+
* access_group: 0,
|
|
2025
|
+
* table: {
|
|
2026
|
+
* name: 'MyTable',
|
|
2027
|
+
* subscription_group: 1
|
|
2028
|
+
* }
|
|
2029
|
+
* });
|
|
2030
|
+
*
|
|
2031
|
+
* // (for admin) Delete all record in the service
|
|
2032
|
+
* await skapi.deleteRecords({
|
|
2033
|
+
* service: 'xxxxxxxx'
|
|
2034
|
+
* });
|
|
2035
|
+
*
|
|
2036
|
+
* // (for admin) Delete all record in "MyTable"
|
|
2037
|
+
* // admin can delete all records in table regardless access group or subscription tables.
|
|
2038
|
+
* await skapi.deleteRecords({
|
|
2039
|
+
* service: 'xxxxxxxx',
|
|
2040
|
+
* table: {
|
|
2041
|
+
* name: 'MyTable'
|
|
2042
|
+
* }
|
|
2043
|
+
* });
|
|
2044
|
+
* // (for admin) Delete all record in "MyTable"
|
|
2045
|
+
*
|
|
2046
|
+
* // admin can delete all records in users subscription table from target access group.
|
|
2047
|
+
* await skapi.deleteRecords({
|
|
2048
|
+
* service: 'xxxxxxxx',
|
|
2049
|
+
* access_group: 1,
|
|
2050
|
+
* table: {
|
|
2051
|
+
* name: 'MyTable',
|
|
2052
|
+
* subscription: 'user_id',
|
|
2053
|
+
* subscription_group: 1
|
|
2054
|
+
* }
|
|
2055
|
+
* });
|
|
2056
|
+
*
|
|
2057
|
+
* ```
|
|
2058
|
+
* @category Database
|
|
2059
|
+
*/
|
|
2060
|
+
async deleteRecords(params: {
|
|
2061
|
+
/**
|
|
2062
|
+
* (only admin) Service ID.
|
|
2063
|
+
*/
|
|
2064
|
+
service?: string;
|
|
2065
|
+
/**
|
|
2066
|
+
* Record ID(s) to delete.<br>
|
|
2067
|
+
* table parameter is not needed when record_id is given.
|
|
2068
|
+
*/
|
|
2069
|
+
record_id?: string | string[];
|
|
2070
|
+
/**
|
|
2071
|
+
* Access group number.<br>
|
|
2072
|
+
*/
|
|
2073
|
+
access_group: number | 'private';
|
|
2074
|
+
/**
|
|
2075
|
+
* Table to delete.<br>
|
|
2076
|
+
*/
|
|
2077
|
+
table: {
|
|
2078
|
+
/** Table name. */
|
|
2079
|
+
name: string;
|
|
2080
|
+
/** @ignore */
|
|
2081
|
+
subscription: string;
|
|
2082
|
+
/**
|
|
2083
|
+
* Subscription group number.<br>
|
|
2084
|
+
* Access group is required.
|
|
2085
|
+
*/
|
|
2086
|
+
subscription_group?: number;
|
|
2087
|
+
};
|
|
2088
|
+
}): Promise<string> {
|
|
2089
|
+
let isAdmin = await this.requireAdmin();
|
|
2090
|
+
if (isAdmin && !params?.service) {
|
|
2091
|
+
throw new SkapiError('Service ID is required.', { code: 'INVALID_PARAMETER' });
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
if (!isAdmin && !params?.table) {
|
|
2095
|
+
throw new SkapiError('"table" is required.', { code: 'INVALID_PARAMETER' });
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
if (params?.record_id) {
|
|
2099
|
+
return await this.request('del-records', {
|
|
2100
|
+
service: params.service,
|
|
2101
|
+
record_id: (v => {
|
|
2102
|
+
let id = checkWhiteSpaceAndSpecialChars(v, 'record_id', false);
|
|
2103
|
+
if (typeof id === 'string') {
|
|
2104
|
+
return [id];
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
return id;
|
|
2108
|
+
})(params.record_id)
|
|
2109
|
+
}, { auth: true });
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
else {
|
|
2113
|
+
if (!params?.table) {
|
|
2114
|
+
throw new SkapiError('Either "table" or "record_id" is required.', { code: 'INVALID_PARAMETER' });
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
let struct = {
|
|
2118
|
+
access_group: ['number', 'private'],
|
|
2119
|
+
table: {
|
|
2120
|
+
name: 'string',
|
|
2121
|
+
subscription: (v: string) => {
|
|
2122
|
+
if (isAdmin) {
|
|
2123
|
+
// admin targets user id
|
|
2124
|
+
return validateUserId((v as string), 'User ID in "table.subscription"');
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
throw new SkapiError('"table.subscription" is an invalid parameter key.', { code: 'INVALID_PARAMETER' });
|
|
2128
|
+
},
|
|
2129
|
+
subscription_group: (v: number) => {
|
|
2130
|
+
if (isAdmin && typeof params?.table?.subscription !== 'string') {
|
|
2131
|
+
throw new SkapiError('"table.subscription" is required.', { code: 'INVALID_PARAMETER' });
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
if (typeof v === 'number') {
|
|
2135
|
+
if (v > 0 && v < 99) {
|
|
2136
|
+
return v;
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
throw new SkapiError('Subscription group should be between 0 ~ 99.', { code: 'INVALID_PARAMETER' });
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
};
|
|
2144
|
+
|
|
2145
|
+
params = checkParams(params || {}, struct, isAdmin ? ['service'] : ['table', 'access_group']);
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
return await this.request('del-records', params, { auth: true });
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
|
|
2152
|
+
//<_subscriptions>
|
|
2153
|
+
/**
|
|
2154
|
+
* Anyone who submits their E-Mail address will receive newsletters from you.<br>
|
|
2155
|
+
* The newsletters you send out will have unsubscribe link at the bottom.<br>
|
|
2156
|
+
* Both Signed and unsigned users can subscribe to your newsletter.<br>
|
|
2157
|
+
* Refer: <a href='www.google.com'>Sending out newsletters</a>
|
|
2158
|
+
* ```
|
|
2159
|
+
* let params = {
|
|
2160
|
+
* email: 'visitors@email.com',
|
|
2161
|
+
* bypassWelcome: false // Send out welcome E-Mails on submit
|
|
2162
|
+
* };
|
|
2163
|
+
*
|
|
2164
|
+
* skapi.subscribeNewsletter(params);
|
|
2165
|
+
* ```
|
|
2166
|
+
* @category Subscriptions
|
|
2167
|
+
*/
|
|
2168
|
+
@formResponse()
|
|
2169
|
+
async subscribeNewsletter(
|
|
2170
|
+
form: Form | {
|
|
2171
|
+
/** Newsletter subscriber's E-Mail. 64 character max. */
|
|
2172
|
+
email: string,
|
|
2173
|
+
/**
|
|
2174
|
+
* Subscriber will receive a welcome E-Mail if set to false.<br>
|
|
2175
|
+
* The welcome E-Mail is the same E-Mail that is sent when the new user successfully creates an account on your web services.<br>
|
|
2176
|
+
* To save your operation cost, it is advised to redirect the users to your welcome page once subscription is successful.<br>
|
|
2177
|
+
* Refer: <a href="www.google.com">Setting up E-Mail templates</a><br>
|
|
2178
|
+
*/
|
|
2179
|
+
bypassWelcome: boolean;
|
|
2180
|
+
},
|
|
2181
|
+
option: FormCallbacks
|
|
2182
|
+
): Promise<string> {
|
|
2183
|
+
let params = checkParams(
|
|
2184
|
+
form || {},
|
|
2185
|
+
{
|
|
2186
|
+
email: (v: string) => validateEmail(v),
|
|
2187
|
+
bypassWelcome: ['boolean', () => true]
|
|
2188
|
+
},
|
|
2189
|
+
['email']
|
|
2190
|
+
);
|
|
2191
|
+
|
|
2192
|
+
return await this.request('subscribe-newsletter', params);
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
|
|
2196
|
+
private async subscriptionGroupCheck(option: Record<string, any>) {
|
|
2197
|
+
await this.__connection;
|
|
2198
|
+
option = checkParams(option, {
|
|
2199
|
+
userId: (v: string) => validateUserId(v, '"userId"'),
|
|
2200
|
+
group: (v: number | string) => {
|
|
2201
|
+
if (v === '*') {
|
|
2202
|
+
return v;
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
if (typeof v !== 'number') {
|
|
2206
|
+
throw new SkapiError('"group" should be type: number.', { code: 'INVALID_PARAMETER' });
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
else if (v < 1 && v > 9) {
|
|
2210
|
+
throw new SkapiError('"group" should be within range 1 ~ 9.', { code: 'INVALID_PARAMETER' });
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
return v;
|
|
2214
|
+
},
|
|
2215
|
+
emailSubscription: ['boolean']
|
|
2216
|
+
}, ['userId', 'group']);
|
|
2217
|
+
|
|
2218
|
+
if (this.user && option.userId === this.user.user_id) {
|
|
2219
|
+
throw new SkapiError(`"userId" cannot be the user's own ID.`, { code: 'INVALID_PARAMETER' });
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
return option;
|
|
2223
|
+
}
|
|
2224
|
+
/**
|
|
2225
|
+
* Subscribes user's account to another account or updates email_subscription state.<br>
|
|
2226
|
+
* User cannot subscribe to email if they did not verify their email.<br>
|
|
2227
|
+
* This can be used for user following, content restrictions when building social media services.<br>
|
|
2228
|
+
* Refer: <a href='www.google.com'>How to use subscription systems</a><br>
|
|
2229
|
+
*
|
|
2230
|
+
* ```
|
|
2231
|
+
* // user subscribes to another user with email subscription
|
|
2232
|
+
* await skapi.subscribe({
|
|
2233
|
+
* userId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
|
|
2234
|
+
* group: 1,
|
|
2235
|
+
* emailSubscription: true
|
|
2236
|
+
* })
|
|
2237
|
+
* ```
|
|
2238
|
+
* @category Subscriptions
|
|
2239
|
+
*/
|
|
2240
|
+
@formResponse()
|
|
2241
|
+
async subscribe(
|
|
2242
|
+
option: SubscriberGroup
|
|
2243
|
+
) {
|
|
2244
|
+
let { userId, group } = await this.subscriptionGroupCheck(option);
|
|
2245
|
+
|
|
2246
|
+
if (group === '*') {
|
|
2247
|
+
throw new SkapiError('Cannot subscribe to all groups at once.', { code: 'INVALID_PARAMETER' });
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
return await this.request('subscription', {
|
|
2251
|
+
subscribe: userId,
|
|
2252
|
+
group
|
|
2253
|
+
}, { auth: true });
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
/**
|
|
2257
|
+
* Unsubscribes user's account from another account or service.
|
|
2258
|
+
* ```
|
|
2259
|
+
* // user unsubscribes from another user
|
|
2260
|
+
* await skapi.unsubscribe({
|
|
2261
|
+
* userId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
|
|
2262
|
+
* group: 2
|
|
2263
|
+
* })
|
|
2264
|
+
* ```
|
|
2265
|
+
* @category Subscriptions
|
|
2266
|
+
*/
|
|
2267
|
+
@formResponse()
|
|
2268
|
+
async unsubscribe(option: SubscriberGroup) {
|
|
2269
|
+
let { userId, group } = await this.subscriptionGroupCheck(option);
|
|
2270
|
+
|
|
2271
|
+
return await this.request('subscription', {
|
|
2272
|
+
unsubscribe: userId,
|
|
2273
|
+
group
|
|
2274
|
+
}, { auth: true });
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
/**
|
|
2278
|
+
* Account owner can block user from their account subscription.
|
|
2279
|
+
* ```
|
|
2280
|
+
* // account owner blocks user from group 2
|
|
2281
|
+
* await skapi.blockSubscriber({
|
|
2282
|
+
* userId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
|
|
2283
|
+
* group: 2
|
|
2284
|
+
* })
|
|
2285
|
+
* // account owner blocks user from all group
|
|
2286
|
+
* await skapi.blockSubscriber({
|
|
2287
|
+
* userId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
|
|
2288
|
+
* })
|
|
2289
|
+
* ```
|
|
2290
|
+
* @category Subscriptions
|
|
2291
|
+
*/
|
|
2292
|
+
@formResponse()
|
|
2293
|
+
async blockSubscriber(option: SubscriberGroup): Promise<string> {
|
|
2294
|
+
let { userId, group } = await this.subscriptionGroupCheck(option);
|
|
2295
|
+
return await this.request('subscription', { block: userId, group }, { auth: true });
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
/**
|
|
2299
|
+
* Account owner can unblock user from their account subscription.
|
|
2300
|
+
* ```
|
|
2301
|
+
* // account owner unblocks user from group 2
|
|
2302
|
+
* await skapi.unblockSubscriber({
|
|
2303
|
+
* userId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
|
|
2304
|
+
* group: 2
|
|
2305
|
+
* })
|
|
2306
|
+
*
|
|
2307
|
+
* // account owner unblocks user from all group
|
|
2308
|
+
* await skapi.unblockSubscriber({
|
|
2309
|
+
* userId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
|
|
2310
|
+
* })
|
|
2311
|
+
* ```
|
|
2312
|
+
* @category Subscriptions
|
|
2313
|
+
*/
|
|
2314
|
+
@formResponse()
|
|
2315
|
+
async unblockSubscriber(option: SubscriberGroup): Promise<string> {
|
|
2316
|
+
let { userId, group } = await this.subscriptionGroupCheck(option);
|
|
2317
|
+
return await this.request('subscription', { unblock: userId, group }, { auth: true });
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
/**
|
|
2321
|
+
* Get user's subscriptions
|
|
2322
|
+
* @ignore
|
|
2323
|
+
*/
|
|
2324
|
+
async getUserSubscriptions(option: SubscriberFetch): Promise<FetchResponse> {
|
|
2325
|
+
await this.__connection;
|
|
2326
|
+
option = checkParams(option, {
|
|
2327
|
+
userId: (v: string) => {
|
|
2328
|
+
try {
|
|
2329
|
+
return validateUserId(v, '"userId"');
|
|
2330
|
+
} catch (err) {
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
try {
|
|
2334
|
+
return validateEmail(v);
|
|
2335
|
+
} catch (err) {
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
throw new SkapiError('"subscriber" should be either valid user ID or E-Mail.', { code: 'INVALID_PARAMETER' });
|
|
2339
|
+
},
|
|
2340
|
+
group: 'number',
|
|
2341
|
+
emailSubscription: 'boolean'
|
|
2342
|
+
}) || {};
|
|
2343
|
+
|
|
2344
|
+
return this.getSubscriptions({
|
|
2345
|
+
subscriber: option.userId || this.user?.user_id,
|
|
2346
|
+
group: option.group,
|
|
2347
|
+
emailSubscription: option?.emailSubscription || undefined
|
|
2348
|
+
});
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
|
|
2352
|
+
/**
|
|
2353
|
+
* Get user's subscribers
|
|
2354
|
+
* @ignore
|
|
2355
|
+
*/
|
|
2356
|
+
async getUserSubscribers(option: SubscriberFetch): Promise<FetchResponse> {
|
|
2357
|
+
await this.__connection;
|
|
2358
|
+
option = checkParams(option, {
|
|
2359
|
+
userId: (v: string) => validateUserId(v, '"userId"'),
|
|
2360
|
+
group: 'number',
|
|
2361
|
+
emailSubscription: 'boolean'
|
|
2362
|
+
}) || {};
|
|
2363
|
+
|
|
2364
|
+
let subParams = {
|
|
2365
|
+
subscription: option.userId || this.user?.user_id,
|
|
2366
|
+
group: option.group,
|
|
2367
|
+
emailSubscription: option.emailSubscription
|
|
2368
|
+
};
|
|
2369
|
+
|
|
2370
|
+
return this.getSubscriptions(subParams);
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
/**
|
|
2374
|
+
* Get user's subscriptions / subscribers
|
|
2375
|
+
*
|
|
2376
|
+
* @category Subscriptions
|
|
2377
|
+
*/
|
|
2378
|
+
async getSubscriptions(
|
|
2379
|
+
params: {
|
|
2380
|
+
/** Subscribers user id | E-Mail for newsletter subscribers. */
|
|
2381
|
+
subscriber?: string;
|
|
2382
|
+
/** Subscription id. User id that subscriber has subscribed to. */
|
|
2383
|
+
subscription?: string;
|
|
2384
|
+
/** subscription group. if omitted, will fetch all groups. */
|
|
2385
|
+
group?: number;
|
|
2386
|
+
/** True | False to fetch service email subscribers. If omitted, will fetch all subscribers. */
|
|
2387
|
+
emailSubscription?: boolean;
|
|
2388
|
+
/** Fetch blocked subscription when True */
|
|
2389
|
+
blocked?: boolean;
|
|
2390
|
+
},
|
|
2391
|
+
fetchOptions?: FetchOptions,
|
|
2392
|
+
/** @ignore */
|
|
2393
|
+
_mapper?: Function
|
|
2394
|
+
): Promise<FetchResponse> {
|
|
2395
|
+
let isNewsletterSub = false;
|
|
2396
|
+
|
|
2397
|
+
params = checkParams(params, {
|
|
2398
|
+
subscriber: (v: string) => {
|
|
2399
|
+
try {
|
|
2400
|
+
return validateUserId(v, 'User ID in "subscriber"');
|
|
2401
|
+
} catch (err) {
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
try {
|
|
2405
|
+
let isEmail = validateEmail(v);
|
|
2406
|
+
isNewsletterSub = true;
|
|
2407
|
+
return isEmail;
|
|
2408
|
+
} catch (err) {
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
throw new SkapiError('"subscriber" should be either valid user ID or E-Mail.', { code: 'INVALID_PARAMETER' });
|
|
2412
|
+
},
|
|
2413
|
+
group: 'number',
|
|
2414
|
+
subscription: (v: string) => {
|
|
2415
|
+
// can be
|
|
2416
|
+
try {
|
|
2417
|
+
return validateUserId(v, 'User ID in "subscription"');
|
|
2418
|
+
} catch (err) {
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
if (typeof v === 'string' && v.length === 14) {
|
|
2422
|
+
return v;
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
throw new SkapiError('"subscriber" should be either valid service ID or user ID.', { code: 'INVALID_PARAMETER' });
|
|
2426
|
+
},
|
|
2427
|
+
emailSubscription: 'boolean',
|
|
2428
|
+
blocked: 'boolean'
|
|
2429
|
+
});
|
|
2430
|
+
|
|
2431
|
+
if (isNewsletterSub && !params?.subscription) {
|
|
2432
|
+
params.subscription = this.service;
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
if (!params.subscriber && !params.subscription) {
|
|
2436
|
+
throw new SkapiError('At least either "subscriber" or "subscription" should have a value.', { code: 'INVALID_PARAMETER' });
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
let response = await this.request('get-subscription', params, Object.assign({ auth: !isNewsletterSub }, { fetchOptions }));
|
|
2440
|
+
|
|
2441
|
+
response.list = response.list.map(_mapper || ((s: Record<string, any>) => {
|
|
2442
|
+
let subscription: Record<string, any> = {};
|
|
2443
|
+
let subSplit = s.sub.split('#');
|
|
2444
|
+
subscription.subscriber = subSplit[2];
|
|
2445
|
+
subscription.subscription = subSplit[0];
|
|
2446
|
+
subscription.group = parseInt(subSplit[1]);
|
|
2447
|
+
subscription.timestamp = s.stmp;
|
|
2448
|
+
subscription.blocked = s.grp.substring(0, 1) === 'N';
|
|
2449
|
+
|
|
2450
|
+
return subscription;
|
|
2451
|
+
}));
|
|
2452
|
+
|
|
2453
|
+
return response;
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
|
|
2457
|
+
/**
|
|
2458
|
+
* Get newsletters and service letters that service owner sent out.
|
|
2459
|
+
* You can make use of your sent newsletters as an article for your web services.
|
|
2460
|
+
* ```
|
|
2461
|
+
* @category Subscriptions
|
|
2462
|
+
*/
|
|
2463
|
+
async getNewsletters(
|
|
2464
|
+
params?: {
|
|
2465
|
+
/**
|
|
2466
|
+
* Search points.<br>
|
|
2467
|
+
* 'message_id' and 'subject' value should be string.<br>
|
|
2468
|
+
* Others numbers.
|
|
2469
|
+
*/
|
|
2470
|
+
searchFor: 'message_id' | 'timestamp' | 'read' | 'complaint' | 'subject';
|
|
2471
|
+
value: string | number;
|
|
2472
|
+
range: string | number;
|
|
2473
|
+
/**
|
|
2474
|
+
* Defaults to '='
|
|
2475
|
+
*/
|
|
2476
|
+
condition?: '>' | '>=' | '=' | '<' | '<=' | 'gt' | 'gte' | 'eq' | 'lt' | 'lte';
|
|
2477
|
+
/**
|
|
2478
|
+
* 'newsletter' for newsletters.<br>
|
|
2479
|
+
* Numbers for service letter sent to corresponding access groups.
|
|
2480
|
+
*/
|
|
2481
|
+
group: 'newsletter' | number;
|
|
2482
|
+
},
|
|
2483
|
+
fetchOptions?: FetchOptions
|
|
2484
|
+
): Promise<Newsletters> {
|
|
2485
|
+
let isAdmin = await this.requireAdmin();
|
|
2486
|
+
|
|
2487
|
+
let searchType = {
|
|
2488
|
+
'message_id': 'string',
|
|
2489
|
+
'timestamp': 'number',
|
|
2490
|
+
'read': 'number',
|
|
2491
|
+
'complaint': 'number',
|
|
2492
|
+
'subject': 'string'
|
|
2493
|
+
};
|
|
2494
|
+
|
|
2495
|
+
if (!params) {
|
|
2496
|
+
if (!fetchOptions) {
|
|
2497
|
+
fetchOptions = {};
|
|
2498
|
+
}
|
|
2499
|
+
fetchOptions.ascending = false;
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
let _params = params || {
|
|
2503
|
+
searchFor: 'timestamp',
|
|
2504
|
+
value: 0,
|
|
2505
|
+
condition: '>'
|
|
2506
|
+
};
|
|
2507
|
+
|
|
2508
|
+
params = checkParams(_params, {
|
|
2509
|
+
searchFor: ['message_id', 'timestamp', 'read', 'complaint', 'group', 'subject'],
|
|
2510
|
+
value: (v: number | string) => {
|
|
2511
|
+
if (typeof v !== searchType[_params.searchFor]) {
|
|
2512
|
+
throw new SkapiError(`"value" type does not match the type of "${_params.searchFor}" index.`, { code: 'INVALID_PARAMETER' });
|
|
2513
|
+
}
|
|
2514
|
+
else if (typeof v === 'string' && !v) {
|
|
2515
|
+
throw new SkapiError('"value" should not be empty string.', { code: 'INVALID_PARAMETER' });
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
return v;
|
|
2519
|
+
},
|
|
2520
|
+
range: (v: number | string) => {
|
|
2521
|
+
if (!_params.hasOwnProperty('value') || typeof v !== typeof _params.value) {
|
|
2522
|
+
throw new SkapiError('"range" should match type of "value".', { code: 'INVALID_PARAMETER' });
|
|
2523
|
+
}
|
|
2524
|
+
return v;
|
|
2525
|
+
},
|
|
2526
|
+
condition: ['>', '>=', '=', '<', '<=', 'gt', 'gte', 'eq', 'lt', 'lte', () => '='],
|
|
2527
|
+
group: (x: string | number) => {
|
|
2528
|
+
if (x !== 'newsletter' && !this.session) {
|
|
2529
|
+
throw new SkapiError('User should be logged in.', { code: 'INVALID_REQUEST' });
|
|
2530
|
+
}
|
|
2531
|
+
if (typeof x === 'string' && x !== 'newsletter') {
|
|
2532
|
+
throw new SkapiError('Group should be either "newsletter" or access group number.', { code: 'INVALID_PARAMETER' });
|
|
2533
|
+
}
|
|
2534
|
+
if (!isAdmin && x > parseInt(this.session.idToken.payload.access_group)) {
|
|
2535
|
+
throw new SkapiError('User has no access.', { code: 'INVALID_REQUEST' });
|
|
2536
|
+
}
|
|
2537
|
+
if (x === 'newsletter') {
|
|
2538
|
+
return 0;
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
return x;
|
|
2542
|
+
}
|
|
2543
|
+
}, ['searchFor', 'value', 'group']);
|
|
2544
|
+
|
|
2545
|
+
let mails = await this.request(
|
|
2546
|
+
params.group === 0 ? 'get-newsletters' : 'get-serviceletters',
|
|
2547
|
+
params,
|
|
2548
|
+
Object.assign({ method: 'get', auth: params.group !== 0 }, { fetchOptions })
|
|
2549
|
+
);
|
|
2550
|
+
|
|
2551
|
+
let remap = {
|
|
2552
|
+
'message_id': 'mid',
|
|
2553
|
+
'timestamp': 'stmp',
|
|
2554
|
+
'complaint': 'cmpl',
|
|
2555
|
+
'read': 'read',
|
|
2556
|
+
'subject': 'subj',
|
|
2557
|
+
'bounced': 'bnce',
|
|
2558
|
+
'url': 'url'
|
|
2559
|
+
};
|
|
2560
|
+
let defaults = {
|
|
2561
|
+
'message_id': '',
|
|
2562
|
+
'timestamp': 0,
|
|
2563
|
+
'complaint': 0,
|
|
2564
|
+
'read': 0,
|
|
2565
|
+
'subject': '',
|
|
2566
|
+
'bounced': 0,
|
|
2567
|
+
'url': ''
|
|
2568
|
+
};
|
|
2569
|
+
|
|
2570
|
+
mails.list = mails.list.map(m => {
|
|
2571
|
+
let remapped = {};
|
|
2572
|
+
for (let k in remap) {
|
|
2573
|
+
remapped[k] = m[remap[k]] || defaults[remap[k]];
|
|
2574
|
+
}
|
|
2575
|
+
return remapped;
|
|
2576
|
+
});
|
|
2577
|
+
|
|
2578
|
+
return mails;
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
|
|
2582
|
+
//<_user>
|
|
2583
|
+
/**
|
|
2584
|
+
* Signup creates new user account to the service.<br>
|
|
2585
|
+
* You can let users confirm their signup by sending out signup confirmation E-Mail.<br>
|
|
2586
|
+
* Once the E-Mail is confirmed, user will receive your welcome E-Mail and will be able to login.<br>
|
|
2587
|
+
* The welcome E-Mail is the same E-Mail that is sent when visitor subscribes to your newsletters.<br>
|
|
2588
|
+
* It is advised to let your users to confirm their signup to prevent automated bots.<br>
|
|
2589
|
+
* Signup confirmation E-Mails will have a link that verifies the user.<br>
|
|
2590
|
+
* If option.confirmation is set to a url string, signup confirmation link will redirect the user to that url.<br>
|
|
2591
|
+
* Welcome emails will only be sent after a user logs in if option.confirmation is set to true.
|
|
2592
|
+
* Common pratice would be to setup a url to redihashedParamsrect users to your 'thankyou for signing up' page.<br>
|
|
2593
|
+
* If the parameter is set to true, user will be redirected to an empty html page that shows success message with your web service link below.<br>
|
|
2594
|
+
* Refer: <a href="www.google.com">Setting up E-Mail templates</a><br>
|
|
2595
|
+
*
|
|
2596
|
+
* ```
|
|
2597
|
+
* let params = {
|
|
2598
|
+
* email: 'login@email.com',
|
|
2599
|
+
* password: 'password',
|
|
2600
|
+
* name: 'Baksa',
|
|
2601
|
+
* phone_number: "+0012341234"
|
|
2602
|
+
* };
|
|
2603
|
+
*
|
|
2604
|
+
* let option = {
|
|
2605
|
+
* confirmation: "http://baksa.com/thankyouforsigningup"
|
|
2606
|
+
* };
|
|
2607
|
+
*
|
|
2608
|
+
* await skapi.signup(params, option);
|
|
2609
|
+
*
|
|
2610
|
+
* // signup confirmation E-Mail is sent
|
|
2611
|
+
* ```
|
|
2612
|
+
* @category User
|
|
2613
|
+
*/
|
|
2614
|
+
@formResponse()
|
|
2615
|
+
async signup(
|
|
2616
|
+
form: Form | UserProfile | { email: String, password: String; },
|
|
2617
|
+
option?: {
|
|
2618
|
+
/**
|
|
2619
|
+
* When true, the service will send out confirmation E-Mail.
|
|
2620
|
+
* User will not be able to signin to their account unless they have confirm their email.
|
|
2621
|
+
*/
|
|
2622
|
+
confirmation?: boolean;
|
|
2623
|
+
/**
|
|
2624
|
+
* Automatically login to account after signup. Will not work if E-Mail confirmation is required.
|
|
2625
|
+
*/
|
|
2626
|
+
login?: boolean;
|
|
2627
|
+
} & FormCallbacks): Promise<User | "SUCCESS: The account has been created. User's email confirmation is required." | 'SUCCESS: The account has been created.'> {
|
|
2628
|
+
|
|
2629
|
+
this.logout();
|
|
2630
|
+
|
|
2631
|
+
let params = checkParams(form || {}, {
|
|
2632
|
+
email: (v: string) => validateEmail(v),
|
|
2633
|
+
password: (v: string) => validatePassword(v),
|
|
2634
|
+
name: 'string',
|
|
2635
|
+
address: 'string',
|
|
2636
|
+
gender: 'string',
|
|
2637
|
+
birthdate: (v: string) => validateBirthdate(v),
|
|
2638
|
+
phone_number: (v: string) => validatePhoneNumber(v),
|
|
2639
|
+
email_public: ['boolean', () => false],
|
|
2640
|
+
address_public: ['boolean', () => false],
|
|
2641
|
+
gender_public: ['boolean', () => false],
|
|
2642
|
+
birthdate_public: ['boolean', () => false],
|
|
2643
|
+
phone_number_public: ['boolean', () => false],
|
|
2644
|
+
email_subscription: ['boolean', () => false]
|
|
2645
|
+
}, ['email', 'password']);
|
|
2646
|
+
|
|
2647
|
+
option = checkParams(option || {}, {
|
|
2648
|
+
confirmation: (v: string | boolean) => {
|
|
2649
|
+
if (typeof v === 'string') {
|
|
2650
|
+
return validateUrl(v);
|
|
2651
|
+
}
|
|
2652
|
+
else if (typeof v === 'boolean') {
|
|
2653
|
+
return v;
|
|
2654
|
+
}
|
|
2655
|
+
else {
|
|
2656
|
+
throw new SkapiError('"option.confirmation" should be type: <string | boolean>.', { code: 'INVALID_PARAMETER' });
|
|
2657
|
+
}
|
|
2658
|
+
},
|
|
2659
|
+
login: (v: boolean) => {
|
|
2660
|
+
if (typeof v === 'boolean') {
|
|
2661
|
+
if (option.confirmation && v) {
|
|
2662
|
+
throw new SkapiError('"login" is not allowed when "option.confirmation" is true.', { code: 'INVALID_PARAMETER' });
|
|
2663
|
+
}
|
|
2664
|
+
return v;
|
|
2665
|
+
}
|
|
2666
|
+
throw new SkapiError('"option.login" should be type: boolean.', { code: 'INVALID_PARAMETER' });
|
|
2667
|
+
}
|
|
2668
|
+
});
|
|
2669
|
+
|
|
2670
|
+
let {
|
|
2671
|
+
login = false,
|
|
2672
|
+
confirmation = false
|
|
2673
|
+
} = option || {};
|
|
2674
|
+
|
|
2675
|
+
if (!confirmation && params.email_public) {
|
|
2676
|
+
throw new SkapiError('"option.confirmation" should be true if "email_public" is set to true.', { code: 'INVALID_PARAMETER' });
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
await this.request("signup", params, { meta: option });
|
|
2680
|
+
|
|
2681
|
+
if (confirmation) {
|
|
2682
|
+
return "SUCCESS: The account has been created. User's email confirmation is required.";
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
else if (login) {
|
|
2686
|
+
return await this.login({ email: params.email, password: params.password });
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
else {
|
|
2690
|
+
return 'SUCCESS: The account has been created.';
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
/**
|
|
2695
|
+
* Logout user and delete user session data.
|
|
2696
|
+
*
|
|
2697
|
+
* ```
|
|
2698
|
+
* await skapi.logout();
|
|
2699
|
+
* ```
|
|
2700
|
+
*
|
|
2701
|
+
* @category User
|
|
2702
|
+
*/
|
|
2703
|
+
logout(): 'SUCCESS: The user has been logged out.' {
|
|
2704
|
+
if (this.cognitoUser) {
|
|
2705
|
+
this.cognitoUser.signOut();
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
let to_be_erased = {
|
|
2709
|
+
'session': null,
|
|
2710
|
+
'__startKey_keys': {},
|
|
2711
|
+
'__cached_requests': {},
|
|
2712
|
+
'user': null
|
|
2713
|
+
};
|
|
2714
|
+
|
|
2715
|
+
for (let k in to_be_erased) {
|
|
2716
|
+
this[k] = to_be_erased[k];
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
return 'SUCCESS: The user has been logged out.';
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
/**
|
|
2723
|
+
* Resends signup confirmation E-Mail.<br>
|
|
2724
|
+
* User needs at least one signin attempt.
|
|
2725
|
+
*
|
|
2726
|
+
* ```
|
|
2727
|
+
* // user tries to signin
|
|
2728
|
+
* try {
|
|
2729
|
+
* await skapi.login('baksa@email.com','password');
|
|
2730
|
+
* } catch(failed) {
|
|
2731
|
+
* console.log(failed); // SIGNUP_CONFIRMATION_NEEDED: ...
|
|
2732
|
+
*
|
|
2733
|
+
* // now you can resend signup confirmation E-Mail to the user.
|
|
2734
|
+
* await skapi.resendSignupConfirmation("http://baksa.com/thankyouforsigningup");
|
|
2735
|
+
* }
|
|
2736
|
+
*
|
|
2737
|
+
* ```
|
|
2738
|
+
*
|
|
2739
|
+
* @category User
|
|
2740
|
+
*/
|
|
2741
|
+
async resendSignupConfirmation(
|
|
2742
|
+
/** Redirect url on confirmation success. */
|
|
2743
|
+
redirect: boolean | string = false
|
|
2744
|
+
): Promise<string> {
|
|
2745
|
+
if (redirect && typeof redirect === 'string') {
|
|
2746
|
+
redirect = (validateUrl(redirect) as string);
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
else if (typeof redirect !== 'boolean') {
|
|
2750
|
+
throw new SkapiError('Argument should be type: <boolean | string>.', { code: 'INVALID_REQUEST' });
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
if (!this.__request_signup_confirmation) {
|
|
2754
|
+
throw new SkapiError('Least one signin attempt is required.', { code: 'INVALID_REQUEST' });
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
let resend = await this.request("confirm-signup", {
|
|
2758
|
+
username: this.__request_signup_confirmation,
|
|
2759
|
+
redirect
|
|
2760
|
+
});
|
|
2761
|
+
|
|
2762
|
+
this.__request_signup_confirmation = null;
|
|
2763
|
+
return resend;
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
/**
|
|
2767
|
+
* Recovers disabled account.<br>
|
|
2768
|
+
* User needs at least one signin attempt of the disabled account.</br>
|
|
2769
|
+
* User should have their E-Mail verified in their disabled account to receive account recovery E-Mail.<br>
|
|
2770
|
+
* It will not be possible to recover unverified accounts.
|
|
2771
|
+
*
|
|
2772
|
+
* ```
|
|
2773
|
+
* // user tries to signin
|
|
2774
|
+
* try {
|
|
2775
|
+
* await skapi.signin('baksa@email.com','password');
|
|
2776
|
+
* } catch(failed) {
|
|
2777
|
+
* console.log(failed); // USER_IS_DISABLED: ...
|
|
2778
|
+
*
|
|
2779
|
+
* // now you can send recover account E-Mail to the user.
|
|
2780
|
+
* await skapi.recoverAccount("http://baksa.com/welcomeback");
|
|
2781
|
+
* }
|
|
2782
|
+
*
|
|
2783
|
+
* ```
|
|
2784
|
+
*
|
|
2785
|
+
* @category User
|
|
2786
|
+
* @param redirect Redirect url on recover account success.
|
|
2787
|
+
*/
|
|
2788
|
+
async recoverAccount(
|
|
2789
|
+
/** Redirect url on confirmation success. */
|
|
2790
|
+
redirect: boolean | string = false
|
|
2791
|
+
): Promise<string> {
|
|
2792
|
+
|
|
2793
|
+
if (typeof redirect === 'string') {
|
|
2794
|
+
validateUrl(redirect);
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
else if (typeof redirect !== 'boolean') {
|
|
2798
|
+
throw new SkapiError('Argument should be type: <boolean | string>.', { code: 'INVALID_REQUEST' });
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
if (!this.__disabledAccount) {
|
|
2802
|
+
throw new SkapiError('Least one signin attempt of disabled account is required.', { code: 'INVALID_REQUEST' });
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
let resend = await this.request("recover-account", { username: this.__disabledAccount, redirect });
|
|
2806
|
+
this.__disabledAccount = null;
|
|
2807
|
+
return resend;
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
|
|
2811
|
+
/**
|
|
2812
|
+
* Logs user to the service.<br>
|
|
2813
|
+
* <h6>DO NOT LEAVE ANY EMAIL AND PASSWORD ON FRONTEND JAVASCRIPT</h6>
|
|
2814
|
+
* Always let users input their own signin information.<br>
|
|
2815
|
+
* <b>Note: User is automatically logged in.</b>
|
|
2816
|
+
*
|
|
2817
|
+
* ```
|
|
2818
|
+
* let user = await skapi.login({
|
|
2819
|
+
* email: 'user@email.com',
|
|
2820
|
+
* password: 'password'
|
|
2821
|
+
* }); // returns user information on success
|
|
2822
|
+
* ```
|
|
2823
|
+
*
|
|
2824
|
+
* @category User
|
|
2825
|
+
*/
|
|
2826
|
+
@formResponse()
|
|
2827
|
+
login(
|
|
2828
|
+
form: Form | {
|
|
2829
|
+
/** E-Mail for signin. 64 character max. */
|
|
2830
|
+
email: string;
|
|
2831
|
+
/** Password for signin. Should be at least 6 characters. */
|
|
2832
|
+
password: string;
|
|
2833
|
+
},
|
|
2834
|
+
option?: FormCallbacks): Promise<User> {
|
|
2835
|
+
this.logout();
|
|
2836
|
+
let params = checkParams(form, {
|
|
2837
|
+
email: (v: string) => validateEmail(v),
|
|
2838
|
+
password: (v: string) => validatePassword(v)
|
|
2839
|
+
}, ['email', 'password']);
|
|
2840
|
+
|
|
2841
|
+
return this.authentication().authenticateUser(params.email, params.password);
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
private verifyAttribute(attribute: string, code?: string): Promise<string> {
|
|
2845
|
+
if (!this.cognitoUser) {
|
|
2846
|
+
throw new SkapiError('The user has to be logged in.', { code: 'INVALID_REQUEST' });
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
return new Promise((res, rej) => {
|
|
2850
|
+
let callback = {
|
|
2851
|
+
onSuccess: (result: any) => {
|
|
2852
|
+
if (code) {
|
|
2853
|
+
this.authentication().updateSession({ refreshToken: true }).then(
|
|
2854
|
+
() => {
|
|
2855
|
+
if (this.user) {
|
|
2856
|
+
this.user[attribute + '_verified'] = true;
|
|
2857
|
+
}
|
|
2858
|
+
res(`SUCCESS: "${attribute}" is verified.`);
|
|
2859
|
+
}
|
|
2860
|
+
).catch(err => {
|
|
2861
|
+
rej(err);
|
|
2862
|
+
});
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
else {
|
|
2866
|
+
res('SUCCESS: Verification code has been sent.');
|
|
2867
|
+
}
|
|
2868
|
+
},
|
|
2869
|
+
onFailure: (err: Record<string, any>) => {
|
|
2870
|
+
rej(
|
|
2871
|
+
new SkapiError(
|
|
2872
|
+
err.message || 'Failed to request verification code.',
|
|
2873
|
+
{
|
|
2874
|
+
code: err?.code
|
|
2875
|
+
}
|
|
2876
|
+
)
|
|
2877
|
+
);
|
|
2878
|
+
},
|
|
2879
|
+
inputVerificationCode: null
|
|
2880
|
+
};
|
|
2881
|
+
|
|
2882
|
+
if (code) {
|
|
2883
|
+
this.cognitoUser?.verifyAttribute(attribute, code, callback);
|
|
2884
|
+
}
|
|
2885
|
+
else {
|
|
2886
|
+
this.cognitoUser?.getAttributeVerificationCode(attribute, callback);
|
|
2887
|
+
}
|
|
2888
|
+
});
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2891
|
+
/**
|
|
2892
|
+
* Verifies user's E-Mail.<br>
|
|
2893
|
+
* The account has to be signed in.<br>
|
|
2894
|
+
* For the verification E-Mail/SMS, template 'template_verification' will be used.<br>
|
|
2895
|
+
* Refer: <a href="www.google.com">Setting up E-Mail templates</a><br>
|
|
2896
|
+
*
|
|
2897
|
+
* ```
|
|
2898
|
+
* async skapi.verifyEmail();
|
|
2899
|
+
* // Signed in user receives verification code via E-Mail.
|
|
2900
|
+
*
|
|
2901
|
+
* // Execute the method again with verification code as a additional argument.
|
|
2902
|
+
* async skapi.verifyEmail({code});
|
|
2903
|
+
* // User E-Mail is now verified
|
|
2904
|
+
* ```
|
|
2905
|
+
*
|
|
2906
|
+
* @category User
|
|
2907
|
+
*/
|
|
2908
|
+
@formResponse()
|
|
2909
|
+
verifyEmail(
|
|
2910
|
+
form?: Form | {
|
|
2911
|
+
/** Verification code. */
|
|
2912
|
+
code: string | number;
|
|
2913
|
+
},
|
|
2914
|
+
option?: FormCallbacks
|
|
2915
|
+
): Promise<string> {
|
|
2916
|
+
|
|
2917
|
+
let code = (form ? checkParams(form, {
|
|
2918
|
+
code: ['number', 'string']
|
|
2919
|
+
}) : {}).code || '';
|
|
2920
|
+
|
|
2921
|
+
return this.verifyAttribute('email', code.toString());
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
|
|
2925
|
+
/**
|
|
2926
|
+
* Verifies user's mobile phone number.<br>
|
|
2927
|
+
* The account has to be signed in.<br>
|
|
2928
|
+
* For the verification E-Mail/SMS, template 'template_verification' will be used.<br>
|
|
2929
|
+
* Refer: <a href="www.google.com">Setting up E-Mail templates</a><br>
|
|
2930
|
+
*
|
|
2931
|
+
* ```
|
|
2932
|
+
* async skapi.verifyMobile();
|
|
2933
|
+
* // Signed in user receives verification code via SMS.
|
|
2934
|
+
*
|
|
2935
|
+
* async skapi.verifyMobile({code});
|
|
2936
|
+
* // User phone is now verified
|
|
2937
|
+
* ```
|
|
2938
|
+
*
|
|
2939
|
+
* @category User
|
|
2940
|
+
*/
|
|
2941
|
+
@formResponse()
|
|
2942
|
+
verifyMobile(
|
|
2943
|
+
form?: Form | {
|
|
2944
|
+
/** Verification code. */
|
|
2945
|
+
code: string | number;
|
|
2946
|
+
},
|
|
2947
|
+
option?: FormCallbacks): Promise<string> {
|
|
2948
|
+
|
|
2949
|
+
let code = (form ? checkParams(form, {
|
|
2950
|
+
code: ['number', 'string']
|
|
2951
|
+
}) : {}).code || null;
|
|
2952
|
+
|
|
2953
|
+
return this.verifyAttribute('phone_number', code.toString());
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
|
|
2957
|
+
/**
|
|
2958
|
+
* Users can request password reset when password is forgotten.<br>
|
|
2959
|
+
* <h6>IMPORTANT</h6>
|
|
2960
|
+
* If the user's account does not have any verified E-Mail, user will not be able to receive any verification E-Mail.<br>
|
|
2961
|
+
* Advise users to verify their E-Mails.<br>
|
|
2962
|
+
*
|
|
2963
|
+
* ```
|
|
2964
|
+
* async skapi.forgotPassword({ email: 'your@email.com' });
|
|
2965
|
+
* // User receives verification E-Mail with verification code.
|
|
2966
|
+
*
|
|
2967
|
+
* // enter verification code and new password as arguments.
|
|
2968
|
+
* async skapi.resetPassword({ email: 'your@email.com', code, new_password });
|
|
2969
|
+
*
|
|
2970
|
+
* // users account password is now set to new_password.
|
|
2971
|
+
* ```
|
|
2972
|
+
*
|
|
2973
|
+
* @category User
|
|
2974
|
+
*/
|
|
2975
|
+
@formResponse()
|
|
2976
|
+
async forgotPassword(
|
|
2977
|
+
form: Form | {
|
|
2978
|
+
/** Signin E-Mail. */
|
|
2979
|
+
email: string;
|
|
2980
|
+
},
|
|
2981
|
+
option?: FormCallbacks): Promise<string> {
|
|
2982
|
+
|
|
2983
|
+
await this.__connection;
|
|
2984
|
+
|
|
2985
|
+
let params = checkParams(form, {
|
|
2986
|
+
email: (v: string) => validateEmail(v)
|
|
2987
|
+
}, ['email']);
|
|
2988
|
+
|
|
2989
|
+
return new Promise(async (res, rej) => {
|
|
2990
|
+
let cognitoUser = (await this.authentication().createCognitoUser(params.email)).cognitoUser;
|
|
2991
|
+
cognitoUser.forgotPassword({
|
|
2992
|
+
onSuccess: result => {
|
|
2993
|
+
res("SUCCESS: Verification code has been sent.");
|
|
2994
|
+
},
|
|
2995
|
+
onFailure: (err: any) => {
|
|
2996
|
+
rej(new SkapiError(err?.message || 'Failed to send verification code.', { code: err?.code || 'ERROR' }));
|
|
2997
|
+
}
|
|
2998
|
+
});
|
|
2999
|
+
});
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
/**
|
|
3003
|
+
* Users can request password reset when password is forgotten.<br>
|
|
3004
|
+
* <h6>IMPORTANT</h6>
|
|
3005
|
+
* If the user's account does not have any verified E-Mail, user will not be able to receive any verification E-Mail.<br>
|
|
3006
|
+
* Advise users to verify their E-Mails.<br>
|
|
3007
|
+
*
|
|
3008
|
+
* ```
|
|
3009
|
+
* async skapi.forgotPassword({ email: 'your@email.com' });
|
|
3010
|
+
* // User receives verification E-Mail with verification code.
|
|
3011
|
+
*
|
|
3012
|
+
* // enter verification code and new password as arguments.
|
|
3013
|
+
* async skapi.resetPassword({ email: 'your@email.com', code, new_password });
|
|
3014
|
+
*
|
|
3015
|
+
* // users account password is now set to new_password.
|
|
3016
|
+
* ```
|
|
3017
|
+
*
|
|
3018
|
+
* @category User
|
|
3019
|
+
|
|
3020
|
+
*/
|
|
3021
|
+
@formResponse()
|
|
3022
|
+
resetPassword(form: Form | {
|
|
3023
|
+
/** Signin E-Mail */
|
|
3024
|
+
email: string;
|
|
3025
|
+
/** The verification code user has received. */
|
|
3026
|
+
code: string | number;
|
|
3027
|
+
/** New password to set. Verification code is required. */
|
|
3028
|
+
new_password: string;
|
|
3029
|
+
}, option?: FormCallbacks): Promise<string> {
|
|
3030
|
+
|
|
3031
|
+
let params = checkParams(form, {
|
|
3032
|
+
email: (v: string) => validateEmail(v),
|
|
3033
|
+
code: ['number', 'string'],
|
|
3034
|
+
new_password: (v: string) => validatePassword(v)
|
|
3035
|
+
}, ['email', 'code', 'new_password']);
|
|
3036
|
+
|
|
3037
|
+
let code = params.code, new_password = params.new_password;
|
|
3038
|
+
|
|
3039
|
+
if (typeof code === 'number') {
|
|
3040
|
+
code = code.toString();
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
return new Promise(async (res, rej) => {
|
|
3044
|
+
let cognitoUser = (await this.authentication().createCognitoUser(params.email)).cognitoUser;
|
|
3045
|
+
|
|
3046
|
+
cognitoUser.confirmPassword(code, new_password, {
|
|
3047
|
+
onSuccess: result => {
|
|
3048
|
+
res("SUCCESS: New password has been set.");
|
|
3049
|
+
},
|
|
3050
|
+
onFailure: (err: any) => {
|
|
3051
|
+
rej(new SkapiError(err?.message || 'Failed to reset password.', { code: err?.code }));
|
|
3052
|
+
}
|
|
3053
|
+
});
|
|
3054
|
+
});
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
/**
|
|
3058
|
+
* Disables user account.</br>
|
|
3059
|
+
* All the data related to the account will be deleted after 3 months.</br>
|
|
3060
|
+
* Within 3 months, user can recover their account.
|
|
3061
|
+
* ```
|
|
3062
|
+
* async skapi.disableAccount();
|
|
3063
|
+
* // Account is disabled.
|
|
3064
|
+
* ```
|
|
3065
|
+
* @category User
|
|
3066
|
+
*/
|
|
3067
|
+
async disableAccount(): Promise<string> {
|
|
3068
|
+
await this.__connection;
|
|
3069
|
+
|
|
3070
|
+
if (this.user && Array.isArray(this.user.services)) {
|
|
3071
|
+
for (let s of this.user.services) {
|
|
3072
|
+
if (s.active) {
|
|
3073
|
+
throw new SkapiError('All services needs to be disabled.', { code: 'INVALID_REQUEST' });
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
let result = await this.request('remove-account', { disable: true }, { auth: true });
|
|
3079
|
+
this.logout();
|
|
3080
|
+
return result;
|
|
3081
|
+
}
|
|
3082
|
+
|
|
3083
|
+
//usersetting
|
|
3084
|
+
/**
|
|
3085
|
+
* Updates user's settings.<br>
|
|
3086
|
+
* The user needs to be logged in.
|
|
3087
|
+
*
|
|
3088
|
+
* ```
|
|
3089
|
+
* await skapi.updateUserSettings({
|
|
3090
|
+
* name: 'John Lennon',
|
|
3091
|
+
* gender: 'Male',
|
|
3092
|
+
* address: 'Liver pool',
|
|
3093
|
+
* email_public: true
|
|
3094
|
+
* });
|
|
3095
|
+
*
|
|
3096
|
+
* // User setting is updated.
|
|
3097
|
+
* ```
|
|
3098
|
+
* @category User
|
|
3099
|
+
*/
|
|
3100
|
+
@formResponse()
|
|
3101
|
+
async updateUserSettings(
|
|
3102
|
+
form: Form | UserProfile & {
|
|
3103
|
+
/** Set new password. *Current password is required. */
|
|
3104
|
+
new_password?: string;
|
|
3105
|
+
/** Current password. Only required when setting new password. */
|
|
3106
|
+
current_password?: string;
|
|
3107
|
+
},
|
|
3108
|
+
option?: FormCallbacks
|
|
3109
|
+
): Promise<User> {
|
|
3110
|
+
await this.__connection;
|
|
3111
|
+
|
|
3112
|
+
let attr: CognitoUserAttribute = this.session?.attributes;
|
|
3113
|
+
|
|
3114
|
+
if (!attr || !this.cognitoUser) {
|
|
3115
|
+
throw new SkapiError('User has to be logged in.', { code: 'INVALID_REQUEST' });
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
let params = checkParams(form || {}, {
|
|
3119
|
+
email: (v: string) => validateEmail(v),
|
|
3120
|
+
name: 'string',
|
|
3121
|
+
address: 'string',
|
|
3122
|
+
gender: 'string',
|
|
3123
|
+
birthdate: (v: string) => validateBirthdate(v),
|
|
3124
|
+
phone_number: (v: string) => validatePhoneNumber(v),
|
|
3125
|
+
email_public: 'boolean',
|
|
3126
|
+
phone_number_public: 'boolean',
|
|
3127
|
+
address_public: 'boolean',
|
|
3128
|
+
gender_public: 'boolean',
|
|
3129
|
+
birthdate_public: 'boolean',
|
|
3130
|
+
email_subscription: 'boolean',
|
|
3131
|
+
current_password: (v: string) => validatePassword(v),
|
|
3132
|
+
new_password: (v: string) => validatePassword(v)
|
|
3133
|
+
});
|
|
3134
|
+
|
|
3135
|
+
if (params && typeof params === 'object' && !Object.keys(params).length) {
|
|
3136
|
+
return this.user;
|
|
3137
|
+
}
|
|
3138
|
+
|
|
3139
|
+
if (params.new_password || params.current_password) {
|
|
3140
|
+
if (!params.current_password) {
|
|
3141
|
+
throw new SkapiError('"current_password" is needed to change the password.', { code: 'INVALID_PARAMETER' });
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
if (!params.new_password) {
|
|
3145
|
+
throw new SkapiError('"new_password" is needed to change password.', { code: 'INVALID_PARAMETER' });
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
if (params.new_password !== params.current_password) {
|
|
3149
|
+
await new Promise((res, rej) => {
|
|
3150
|
+
this.cognitoUser.changePassword(
|
|
3151
|
+
params.current_password,
|
|
3152
|
+
params.new_password,
|
|
3153
|
+
(err: any, result: any) => {
|
|
3154
|
+
if (err) {
|
|
3155
|
+
rej(new SkapiError(err?.message || 'Failed to change users password.', { code: err?.code || err?.name }));
|
|
3156
|
+
}
|
|
3157
|
+
|
|
3158
|
+
res(result);
|
|
3159
|
+
});
|
|
3160
|
+
});
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
delete params.current_password;
|
|
3164
|
+
delete params.new_password;
|
|
3165
|
+
}
|
|
3166
|
+
|
|
3167
|
+
// set alternative signin email
|
|
3168
|
+
if (params.email) {
|
|
3169
|
+
let connect = await this.updateServiceInformation({ request_hash: params.email });
|
|
3170
|
+
params['preferred_username'] = connect.hash;
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
let collision = [
|
|
3174
|
+
['email_subscription', 'email_verified', "User's E-Mail should be verified to set"],
|
|
3175
|
+
['email_public', 'email_verified', "User's E-Mail should be verified to set"],
|
|
3176
|
+
['phone_number_public', 'phone_number_verified', "User's phone number should be verified to set"]
|
|
3177
|
+
];
|
|
3178
|
+
|
|
3179
|
+
if (this.user) {
|
|
3180
|
+
for (let c of collision) {
|
|
3181
|
+
if (params[c[0]] && !this.user[c[1]]) {
|
|
3182
|
+
throw new SkapiError(`${c[2]} "${c[0]}" to true.`, { code: 'INVALID_PARAMETER' });
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
|
|
3187
|
+
let customAttr = [
|
|
3188
|
+
'email_public',
|
|
3189
|
+
'phone_number_public',
|
|
3190
|
+
'address_public',
|
|
3191
|
+
'gender_public',
|
|
3192
|
+
'birthdate_public',
|
|
3193
|
+
'email_subscription'
|
|
3194
|
+
];
|
|
3195
|
+
|
|
3196
|
+
// delete unchanged values, convert key names to cognito attributes
|
|
3197
|
+
for (let k in params) {
|
|
3198
|
+
if (params[k] === attr[k]) {
|
|
3199
|
+
delete params[k];
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
else if (customAttr.includes(k)) {
|
|
3203
|
+
let parseValue = params[k];
|
|
3204
|
+
|
|
3205
|
+
if (typeof parseValue === 'boolean')
|
|
3206
|
+
parseValue = parseValue ? '1' : '0';
|
|
3207
|
+
|
|
3208
|
+
params['custom:' + k] = parseValue;
|
|
3209
|
+
delete params[k];
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
|
|
3213
|
+
if (params && typeof params === 'object' && Object.keys(params).length) {
|
|
3214
|
+
let toSet: Array<CognitoUserAttribute> = [];
|
|
3215
|
+
for (let key in params) {
|
|
3216
|
+
toSet.push(new CognitoUserAttribute({
|
|
3217
|
+
Name: key,
|
|
3218
|
+
Value: params[key]
|
|
3219
|
+
}));
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3222
|
+
await new Promise((res, rej) => {
|
|
3223
|
+
this.cognitoUser?.updateAttributes(
|
|
3224
|
+
toSet,
|
|
3225
|
+
(err: any, result: any) => {
|
|
3226
|
+
if (err) {
|
|
3227
|
+
rej(
|
|
3228
|
+
[
|
|
3229
|
+
err?.code || err?.name,
|
|
3230
|
+
err?.message || `Failed to update user settings.`
|
|
3231
|
+
]
|
|
3232
|
+
);
|
|
3233
|
+
}
|
|
3234
|
+
res(result);
|
|
3235
|
+
});
|
|
3236
|
+
});
|
|
3237
|
+
|
|
3238
|
+
await this.authentication().updateSession({ refreshToken: true });
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
return this.user;
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
|
|
3245
|
+
/**
|
|
3246
|
+
* Uploads user's account profile data.<br>
|
|
3247
|
+
* The account needs to be signed in.
|
|
3248
|
+
*
|
|
3249
|
+
* ```
|
|
3250
|
+
* await skapi.updateUserData(
|
|
3251
|
+
* {
|
|
3252
|
+
* whoAmI: 'John Lennon',
|
|
3253
|
+
* myFavoriteFruits: ['Strawberry', 'Apple'],
|
|
3254
|
+
* meAndYoko: {
|
|
3255
|
+
* pictures: ['traveling.jpg', 'in_the_studio.jpg'],
|
|
3256
|
+
* unreleased_videos: ['give_a_peace_a_chance.mpg']
|
|
3257
|
+
* },
|
|
3258
|
+
* secret: 'Walrus was paul'
|
|
3259
|
+
* },
|
|
3260
|
+
* {
|
|
3261
|
+
* private: ['meAndYoko', 'secret']
|
|
3262
|
+
* }
|
|
3263
|
+
* );
|
|
3264
|
+
*
|
|
3265
|
+
* // userdata.meAndYoko, userdata.secret will not be public
|
|
3266
|
+
* ```
|
|
3267
|
+
* @category User
|
|
3268
|
+
*/
|
|
3269
|
+
@formResponse()
|
|
3270
|
+
async uploadUserData(
|
|
3271
|
+
/** Form element or object. */
|
|
3272
|
+
form: Form,
|
|
3273
|
+
option?: FormCallbacks & {
|
|
3274
|
+
/** Lists of key to make private. */
|
|
3275
|
+
private: string[];
|
|
3276
|
+
}): Promise<User> {
|
|
3277
|
+
|
|
3278
|
+
option = checkParams(option || {}, {
|
|
3279
|
+
private: 'array'
|
|
3280
|
+
});
|
|
3281
|
+
|
|
3282
|
+
let opt = {
|
|
3283
|
+
auth: true
|
|
3284
|
+
};
|
|
3285
|
+
|
|
3286
|
+
if (option.private) {
|
|
3287
|
+
Object.assign(opt, { meta: { '__private__': option.private } });
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
await this.request('post-userdata', form, opt);
|
|
3291
|
+
await this.authentication().updateSession();
|
|
3292
|
+
return this.user;
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3295
|
+
/**
|
|
3296
|
+
* Query and fetch user account database.<br>
|
|
3297
|
+
* Any attribute that user has set to private will not be searchable.<br>
|
|
3298
|
+
* If the fetched list of users exceeds 100 users, you can run the same method with the same parameters to get the next batch of list of users. (auto startKey)<br>
|
|
3299
|
+
* If the argument is empty, It will fetch all users in the service in order of signuped timestamp.<br>
|
|
3300
|
+
* User login is required.<br>
|
|
3301
|
+
* You can search for user's information based on:<br>
|
|
3302
|
+
* 'user_id' | 'email' | 'phone_number' | 'name' | 'address' | 'group' | 'email_subscription' | 'gender' | 'birthdate' | 'locale' | 'subscribers'</br>
|
|
3303
|
+
* User's will not be on the database if they did not login after signup.
|
|
3304
|
+
*
|
|
3305
|
+
* ```
|
|
3306
|
+
* let getPauls = await skapi.getUsers({
|
|
3307
|
+
* searchFor: 'name',
|
|
3308
|
+
* value: 'paul', // search all user profile names in the connection service that is/start's with 'paul'
|
|
3309
|
+
* condition: '>='
|
|
3310
|
+
* });
|
|
3311
|
+
*
|
|
3312
|
+
* let getAllUsers = await skapi.getUsers({
|
|
3313
|
+
* searchFor: 'group',
|
|
3314
|
+
* value: 1 // search all users in group 1
|
|
3315
|
+
* });
|
|
3316
|
+
*
|
|
3317
|
+
* // returns -
|
|
3318
|
+
* // {
|
|
3319
|
+
* // list: [<object | user objects>, ...],
|
|
3320
|
+
* // startKey: <object>,
|
|
3321
|
+
* // endOfList: false // if true, more list can be fetched when method is executed again.
|
|
3322
|
+
* // }
|
|
3323
|
+
* ```
|
|
3324
|
+
*
|
|
3325
|
+
* form.searchFor: Index to search.
|
|
3326
|
+
* Search for user's information based on:<br>
|
|
3327
|
+
* - 'user_id'<br>
|
|
3328
|
+
* - 'email'<br>
|
|
3329
|
+
* - 'phone_number'<br>
|
|
3330
|
+
* - 'name'<br>
|
|
3331
|
+
* - 'address'<br>
|
|
3332
|
+
* - 'group'<br>
|
|
3333
|
+
* - 'email_subscription'<br>
|
|
3334
|
+
* - 'gender'<br>
|
|
3335
|
+
* - 'birthdate'<br>
|
|
3336
|
+
* - 'locale'<br>
|
|
3337
|
+
* - 'subscribers'<br>
|
|
3338
|
+
* - 'timestamp'<br>
|
|
3339
|
+
* form.value: Value to "searchFor":<br>
|
|
3340
|
+
* If user has set certain information private, it will not be searchable.<br>
|
|
3341
|
+
* Certain columns have specific types:<br>
|
|
3342
|
+
|
|
3343
|
+
* Search for user's information based on:<br>
|
|
3344
|
+
* - 'user_id'<br>
|
|
3345
|
+
* 36 character unique user id. If omitted it queries profile for currently signed in account.<br>
|
|
3346
|
+
* Conditions don't apply.<br>
|
|
3347
|
+
|
|
3348
|
+
* - 'blocked'<br>
|
|
3349
|
+
* Boolean. Queries users blocked state.<br>
|
|
3350
|
+
* Conditions don't apply.<br>
|
|
3351
|
+
|
|
3352
|
+
* - 'email'<br>
|
|
3353
|
+
* User's email.
|
|
3354
|
+
* Conditions don't apply.
|
|
3355
|
+
|
|
3356
|
+
* - 'phone_number'<br>
|
|
3357
|
+
* User phone number including region code. ex) +0012341234
|
|
3358
|
+
|
|
3359
|
+
* - 'name'<br>
|
|
3360
|
+
* Name that user have set on their account.
|
|
3361
|
+
|
|
3362
|
+
* - 'address'<br>
|
|
3363
|
+
* User's address.
|
|
3364
|
+
|
|
3365
|
+
* - 'group'<br>
|
|
3366
|
+
* User's account group number.
|
|
3367
|
+
|
|
3368
|
+
* - 'email_subscription'<br>
|
|
3369
|
+
* E-Mail subscribed user's group number.
|
|
3370
|
+
|
|
3371
|
+
* - 'gender'<br>
|
|
3372
|
+
* User's gender.
|
|
3373
|
+
|
|
3374
|
+
* - 'birthdate'<br>
|
|
3375
|
+
* User's birthdate.
|
|
3376
|
+
fetch
|
|
3377
|
+
* - 'locale'<br>
|
|
3378
|
+
* User's locale.
|
|
3379
|
+
|
|
3380
|
+
* - 'subscribers'<br>
|
|
3381
|
+
* Number of user's subscribers.
|
|
3382
|
+
|
|
3383
|
+
* - 'timestamp'<br>
|
|
3384
|
+
* User's account creation timestamp.
|
|
3385
|
+
*
|
|
3386
|
+
* @category User
|
|
3387
|
+
*/
|
|
3388
|
+
async getUsers(
|
|
3389
|
+
params?: QueryParams | null,
|
|
3390
|
+
fetchOptions?: FetchOptions
|
|
3391
|
+
): Promise<User | FetchResponse> {
|
|
3392
|
+
|
|
3393
|
+
let isAdmin = await this.requireAdmin({
|
|
3394
|
+
ignoreVerification: true
|
|
3395
|
+
});
|
|
3396
|
+
|
|
3397
|
+
if (!params) {
|
|
3398
|
+
if (!fetchOptions) {
|
|
3399
|
+
fetchOptions = {};
|
|
3400
|
+
}
|
|
3401
|
+
fetchOptions.ascending = false;
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3404
|
+
// set default value
|
|
3405
|
+
params = params || {
|
|
3406
|
+
searchFor: 'timestamp',
|
|
3407
|
+
condition: '>',
|
|
3408
|
+
value: 0
|
|
3409
|
+
};
|
|
3410
|
+
|
|
3411
|
+
const searchForTypes = {
|
|
3412
|
+
'user_id': (v: string) => validateUserId(v),
|
|
3413
|
+
'name': 'string',
|
|
3414
|
+
'email': (v: string) => validateEmail(v),
|
|
3415
|
+
'phone_number': (v: string) => validatePhoneNumber(v),
|
|
3416
|
+
'address': 'string',
|
|
3417
|
+
'gender': 'string',
|
|
3418
|
+
'birthdate': (v: string) => validateBirthdate(v),
|
|
3419
|
+
'locale': (v: string) => {
|
|
3420
|
+
if (typeof v !== 'string' || typeof v === 'string' && v.length > 5) {
|
|
3421
|
+
throw new SkapiError('Value of "locale" should be a country code.');
|
|
3422
|
+
}
|
|
3423
|
+
return v;
|
|
3424
|
+
},
|
|
3425
|
+
'subscribers': 'number',
|
|
3426
|
+
'timestamp': 'number',
|
|
3427
|
+
'group': 'number',
|
|
3428
|
+
'email_subscription': (v: number) => {
|
|
3429
|
+
if (!isAdmin) {
|
|
3430
|
+
throw new SkapiError('Only admin is allowed to search "email_subscription".', { code: 'INVALID_REQUEST' });
|
|
3431
|
+
}
|
|
3432
|
+
return v;
|
|
3433
|
+
},
|
|
3434
|
+
'blocked': (v: boolean) => {
|
|
3435
|
+
if (v) {
|
|
3436
|
+
return 'by_admin:suspended';
|
|
3437
|
+
}
|
|
3438
|
+
else {
|
|
3439
|
+
return 'by_admin:approved';
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
};
|
|
3443
|
+
|
|
3444
|
+
let required = ['searchFor', 'value'];
|
|
3445
|
+
|
|
3446
|
+
params = checkParams(params, {
|
|
3447
|
+
searchFor: [
|
|
3448
|
+
'user_id',
|
|
3449
|
+
'name',
|
|
3450
|
+
'email',
|
|
3451
|
+
'phone_number',
|
|
3452
|
+
'address',
|
|
3453
|
+
'gender',
|
|
3454
|
+
'birthdate',
|
|
3455
|
+
'locale',
|
|
3456
|
+
'subscribers',
|
|
3457
|
+
'timestamp',
|
|
3458
|
+
'group',
|
|
3459
|
+
'email_subscription',
|
|
3460
|
+
'blocked'
|
|
3461
|
+
],
|
|
3462
|
+
condition: ['>', '>=', '=', '<', '<=', 'gt', 'gte', 'eq', 'lt', 'lte', () => '='],
|
|
3463
|
+
value: (v: any) => {
|
|
3464
|
+
let checker = searchForTypes[params.searchFor];
|
|
3465
|
+
if (typeof checker === 'function') {
|
|
3466
|
+
return checker(v);
|
|
3467
|
+
}
|
|
3468
|
+
|
|
3469
|
+
else if (typeof v !== checker) {
|
|
3470
|
+
throw new SkapiError(`Value does not match the type of "${params.searchFor}" index.`, { code: 'INVALID_PARAMETER' });
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3473
|
+
return v;
|
|
3474
|
+
},
|
|
3475
|
+
range: (v: any) => {
|
|
3476
|
+
let checker = searchForTypes[params.searchFor];
|
|
3477
|
+
if (typeof checker === 'function') {
|
|
3478
|
+
return checker(v);
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3481
|
+
else if (typeof v !== checker) {
|
|
3482
|
+
throw new SkapiError(`Range does not match the type of "${params.searchFor}" index.`, { code: 'INVALID_PARAMETER' });
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
return v;
|
|
3486
|
+
}
|
|
3487
|
+
}, required);
|
|
3488
|
+
|
|
3489
|
+
if (params?.condition && params?.condition !== '=' && params.hasOwnProperty('range')) {
|
|
3490
|
+
throw new SkapiError('Conditions does not apply on range search.', { code: 'INVALID_PARAMETER' });
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3493
|
+
if (['user_id', 'blocked'].includes(params.searchFor) && params.condition !== '=') {
|
|
3494
|
+
throw new SkapiError(`Condition is not allowed on "${params.searchFor}"`, { code: 'INVALID_PARAMETER' });
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
if (params.searchFor === 'blocked') {
|
|
3498
|
+
// backend uses 'suspended' for column name
|
|
3499
|
+
params.searchFor = 'suspended';
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
if (typeof params?.value === 'string' && !params?.value) {
|
|
3503
|
+
throw new SkapiError('Value should not be an empty string.', { code: 'INVALID_PARAMETER' });
|
|
3504
|
+
}
|
|
3505
|
+
|
|
3506
|
+
if (typeof params?.searchFor === 'string' && !params?.searchFor) {
|
|
3507
|
+
throw new SkapiError('"searchFor" should not be an empty string.', { code: 'INVALID_PARAMETER' });
|
|
3508
|
+
}
|
|
3509
|
+
|
|
3510
|
+
let isSelfProfile = params.searchFor === 'user_id' && params.value === this.session.idToken.payload.sub;
|
|
3511
|
+
|
|
3512
|
+
if (isAdmin && !isSelfProfile && !(params as any).service) {
|
|
3513
|
+
throw new SkapiError('Service ID is required.', { code: 'INVALID_PARAMETER' });
|
|
3514
|
+
}
|
|
3515
|
+
|
|
3516
|
+
let result = await this.request(
|
|
3517
|
+
'get-users',
|
|
3518
|
+
params,
|
|
3519
|
+
{ auth: true, fetchOptions }
|
|
3520
|
+
);
|
|
3521
|
+
|
|
3522
|
+
if (!result.list[0] || !isSelfProfile || result.list.length > 1) {
|
|
3523
|
+
return result;
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
let user = result.list[0];
|
|
3527
|
+
// append user session data
|
|
3528
|
+
Object.assign(user, this.user);
|
|
3529
|
+
|
|
3530
|
+
user._what_public_see = JSON.parse(JSON.stringify(user));
|
|
3531
|
+
|
|
3532
|
+
let public_keys = [
|
|
3533
|
+
'service',
|
|
3534
|
+
'user_id',
|
|
3535
|
+
'name',
|
|
3536
|
+
'locale',
|
|
3537
|
+
'address',
|
|
3538
|
+
'birthdate',
|
|
3539
|
+
'phone_number',
|
|
3540
|
+
'email',
|
|
3541
|
+
'gender',
|
|
3542
|
+
'subscribers',
|
|
3543
|
+
'timestamp',
|
|
3544
|
+
'group',
|
|
3545
|
+
'log',
|
|
3546
|
+
'user_data'
|
|
3547
|
+
];
|
|
3548
|
+
|
|
3549
|
+
// remove unnecessary keys in _what_public_see
|
|
3550
|
+
for (let k in user._what_public_see) {
|
|
3551
|
+
if (!public_keys.includes(k)) {
|
|
3552
|
+
delete user._what_public_see[k];
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
return user;
|
|
3557
|
+
}
|
|
3558
|
+
|
|
3559
|
+
private async updateServiceInformation(params?: { request_hash: string; }): Promise<Connection> {
|
|
3560
|
+
|
|
3561
|
+
let request = null;
|
|
3562
|
+
let connectedService: Record<string, any> = {};
|
|
3563
|
+
|
|
3564
|
+
if (params?.request_hash) {
|
|
3565
|
+
// hash request
|
|
3566
|
+
|
|
3567
|
+
if (this.__serviceHash[params.request_hash]) {
|
|
3568
|
+
// has hash
|
|
3569
|
+
Object.assign(connectedService, { hash: this.__serviceHash[params.request_hash] });
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
else {
|
|
3573
|
+
// request signin hash
|
|
3574
|
+
request = {
|
|
3575
|
+
request_hash: validateEmail(params.request_hash)
|
|
3576
|
+
};
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
|
|
3580
|
+
if (!this.connection || this.connection.service !== this.service || request) {
|
|
3581
|
+
// has hash request or need new connection request
|
|
3582
|
+
|
|
3583
|
+
if (request === null) {
|
|
3584
|
+
request = {};
|
|
3585
|
+
}
|
|
3586
|
+
|
|
3587
|
+
// assign service id and owner to request
|
|
3588
|
+
Object.assign(request, {
|
|
3589
|
+
service: this.service,
|
|
3590
|
+
service_owner: this.service_owner
|
|
3591
|
+
});
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3594
|
+
// post request if needed
|
|
3595
|
+
Object.assign(connectedService, (request ? await this.request('service', request, { bypassAwaitConnection: true }) : this.connection));
|
|
3596
|
+
|
|
3597
|
+
if (params?.request_hash) {
|
|
3598
|
+
// cache hash if needed
|
|
3599
|
+
this.__serviceHash[params.request_hash] = connectedService.hash;
|
|
3600
|
+
}
|
|
3601
|
+
|
|
3602
|
+
// deep copy, save connection service info without hash
|
|
3603
|
+
let connection = JSON.parse(JSON.stringify(connectedService));
|
|
3604
|
+
delete connection.hash;
|
|
3605
|
+
this.connection = connection;
|
|
3606
|
+
|
|
3607
|
+
return connectedService as Connection;
|
|
3608
|
+
}
|
|
3609
|
+
}
|