skapi-js 0.0.52 → 0.0.54

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