skapi-js 0.2.0 → 0.2.2

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.
@@ -0,0 +1,915 @@
1
+ import SkapiError from '../main/error';
2
+ import { extractFormMeta, generateRandom } from '../utils/utils';
3
+ import validator from '../utils/validator';
4
+ import { request } from './request';
5
+ import { checkAdmin } from './user';
6
+ const __index_number_range = 4503599627370496;
7
+ function normalizeRecord(record) {
8
+ function base_decode(chars) {
9
+ let charset = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
10
+ return chars.split('').reverse().reduce((prev, curr, i) => prev + (charset.indexOf(curr) * (62 ** i)), 0);
11
+ }
12
+ const output = {
13
+ user_id: '',
14
+ record_id: '',
15
+ updated: 0,
16
+ uploaded: 0,
17
+ table: {
18
+ name: '',
19
+ access_group: 0
20
+ },
21
+ reference: {
22
+ reference_limit: null,
23
+ allow_multiple_reference: true,
24
+ referenced_count: 0
25
+ },
26
+ ip: '',
27
+ bin: []
28
+ };
29
+ const keys = {
30
+ 'ip': (r) => {
31
+ output.ip = r;
32
+ },
33
+ 'rec': (r) => {
34
+ if (!r)
35
+ return;
36
+ output.record_id = r;
37
+ let base62timestamp = r.substring(0, r.length - 9);
38
+ let uploaded = base_decode(base62timestamp);
39
+ output.uploaded = uploaded;
40
+ },
41
+ 'usr': (r) => {
42
+ output.user_id = r;
43
+ },
44
+ 'tbl': (r) => {
45
+ if (!r)
46
+ return;
47
+ let rSplit = r.split('/');
48
+ output.table.name = rSplit[0];
49
+ output.table.access_group = rSplit[2] == '**' ? 'private' : parseInt(rSplit[2]);
50
+ if (rSplit?.[3]) {
51
+ output.table.subscription = {
52
+ user_id: rSplit[3],
53
+ group: parseInt(rSplit[4])
54
+ };
55
+ }
56
+ },
57
+ 'usr_tbl': (r) => {
58
+ let rSplit = r.split('/');
59
+ output.user_id = rSplit[0];
60
+ output.table.name = rSplit[1];
61
+ output.table.access_group = rSplit[3] == '**' ? 'private' : parseInt(rSplit[3]);
62
+ if (rSplit?.[4]) {
63
+ output.table.subscription = {
64
+ user_id: rSplit[4],
65
+ group: parseInt(rSplit[5])
66
+ };
67
+ }
68
+ },
69
+ 'idx': (r) => {
70
+ if (!r)
71
+ return;
72
+ let rSplit = r.split('!');
73
+ let name = rSplit.splice(0, 1)[0];
74
+ let value = normalizeTypedString('!' + rSplit.join('!'));
75
+ output.index = {
76
+ name,
77
+ value
78
+ };
79
+ },
80
+ 'ref': (r) => {
81
+ if (!r)
82
+ return;
83
+ output.reference.record_id = r.split('/')[0];
84
+ },
85
+ 'tags': (r) => {
86
+ output.tags = r;
87
+ },
88
+ 'upd': (r) => {
89
+ output.updated = r;
90
+ },
91
+ 'acpt_mrf': (r) => {
92
+ output.reference.allow_multiple_reference = r;
93
+ },
94
+ 'ref_limt': (r) => {
95
+ output.reference.reference_limit = r;
96
+ },
97
+ 'rfd': (r) => {
98
+ output.reference.referenced_count = r;
99
+ },
100
+ 'bin': (r) => {
101
+ output.bin = r;
102
+ },
103
+ 'data': (r) => {
104
+ let data = r;
105
+ if (r === '!D%{}') {
106
+ data = {};
107
+ }
108
+ else if (r === '!L%[]') {
109
+ data = [];
110
+ }
111
+ output.data = data;
112
+ }
113
+ };
114
+ if (record.record_id) {
115
+ return record;
116
+ }
117
+ for (let k in keys) {
118
+ if (record.hasOwnProperty(k)) {
119
+ keys[k](record[k]);
120
+ }
121
+ }
122
+ return output;
123
+ }
124
+ function normalizeTypedString(v) {
125
+ let value = v.substring(3);
126
+ let type = v.substring(0, 3);
127
+ switch (type) {
128
+ case "!S%":
129
+ return value;
130
+ case "!N%":
131
+ return Number(value) - 4503599627370496;
132
+ case "!B%":
133
+ return value === '1';
134
+ case "!L%":
135
+ case "!D%":
136
+ try {
137
+ return JSON.parse(value);
138
+ }
139
+ catch (err) {
140
+ throw new SkapiError('Value parse error.', { code: 'PARSE_ERROR' });
141
+ }
142
+ default:
143
+ return v;
144
+ }
145
+ }
146
+ export async function deleteFiles(params) {
147
+ let isAdmin = await checkAdmin.bind(this)();
148
+ let { service = this.service, endpoints, storage = 'records' } = params;
149
+ if (storage === 'host' && !isAdmin) {
150
+ throw new SkapiError("No access", { code: 'INVALID_REQUEST' });
151
+ }
152
+ if (typeof endpoints === 'string') {
153
+ endpoints = [endpoints];
154
+ }
155
+ if (!Array.isArray(endpoints)) {
156
+ throw new SkapiError('"endpoints" should be type: array | string.', { code: 'INVALID_PARAMETER' });
157
+ }
158
+ if (storage !== 'host' && storage !== 'records') {
159
+ throw new SkapiError('"storage" should be type: "records" | "host".', { code: 'INVALID_PARAMETER' });
160
+ }
161
+ return request.bind(this)('del-files', {
162
+ service,
163
+ endpoints,
164
+ storage
165
+ }, { auth: true, method: 'post' });
166
+ }
167
+ export async function uploadFiles(fileList, params) {
168
+ let isAdmin = await checkAdmin.bind(this)();
169
+ if (fileList instanceof SubmitEvent) {
170
+ fileList = fileList.target;
171
+ }
172
+ if (fileList instanceof HTMLFormElement) {
173
+ fileList = new FormData(fileList);
174
+ }
175
+ if (fileList instanceof FormData) {
176
+ let fileEntries = [];
177
+ for (let entry of fileList.entries()) {
178
+ let value = entry[1];
179
+ if (value instanceof File) {
180
+ fileEntries.push(value);
181
+ }
182
+ }
183
+ fileList = fileEntries;
184
+ }
185
+ if (!(fileList[0] instanceof File)) {
186
+ throw new SkapiError('"fileList" should be a FileList or array of File object.', { code: 'INVALID_PARAMETER' });
187
+ }
188
+ let reserved_key = generateRandom();
189
+ let getSignedParams = {
190
+ reserved_key,
191
+ service: params?.service || this.service,
192
+ request: params?.request || 'post'
193
+ };
194
+ if (getSignedParams.request === 'host') {
195
+ if (!isAdmin) {
196
+ throw new SkapiError('The user has no access.', { code: 'INVALID_REQUEST' });
197
+ }
198
+ getSignedParams.request === 'post-host';
199
+ }
200
+ if (params?.record_id) {
201
+ getSignedParams.id = params.record_id;
202
+ }
203
+ else if (!isAdmin) {
204
+ throw new SkapiError('Record ID is required.', { code: 'INVALID_PARAMETER' });
205
+ }
206
+ let xhr;
207
+ let fetchProgress = (url, body, progressCallback) => {
208
+ return new Promise((res, rej) => {
209
+ xhr = new XMLHttpRequest();
210
+ xhr.open('POST', url);
211
+ xhr.onload = (e) => {
212
+ let result = xhr.responseText;
213
+ try {
214
+ result = JSON.parse(result);
215
+ }
216
+ catch (err) { }
217
+ if (xhr.status >= 200 && xhr.status < 300) {
218
+ let result = xhr.responseText;
219
+ try {
220
+ result = JSON.parse(result);
221
+ }
222
+ catch (err) { }
223
+ res(result);
224
+ }
225
+ else {
226
+ rej(result);
227
+ }
228
+ };
229
+ xhr.onerror = () => rej('Network error');
230
+ xhr.onabort = () => rej('Aborted');
231
+ xhr.ontimeout = () => rej('Timeout');
232
+ if (xhr.upload && typeof params.progress === 'function') {
233
+ xhr.upload.onprogress = progressCallback;
234
+ }
235
+ xhr.send(body);
236
+ });
237
+ };
238
+ let completed = [];
239
+ let failed = [];
240
+ function toBase62(num) {
241
+ const base62Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
242
+ if (num === 0)
243
+ return base62Chars[0];
244
+ let result = '';
245
+ while (num > 0) {
246
+ result = base62Chars[num % 62] + result;
247
+ num = Math.floor(num / 62);
248
+ }
249
+ return result;
250
+ }
251
+ for (let f of fileList) {
252
+ let signedParams = Object.assign({
253
+ key: f.name,
254
+ sizeKey: toBase62(f.size),
255
+ contentType: f.type || null
256
+ }, getSignedParams);
257
+ let { fields = null, url } = await request.bind(this)('get-signed-url', signedParams, { auth: true });
258
+ let form = new FormData();
259
+ for (let name in fields) {
260
+ form.append(name, fields[name]);
261
+ }
262
+ form.append('file', f);
263
+ try {
264
+ await fetchProgress(url, form, (p) => {
265
+ params.progress({
266
+ status: 'upload',
267
+ progress: p.loaded / p.total * 100,
268
+ currentFile: f,
269
+ completed,
270
+ failed,
271
+ loaded: p.loaded,
272
+ total: p.total,
273
+ abort: () => xhr.abort()
274
+ });
275
+ });
276
+ completed.push(f);
277
+ }
278
+ catch (err) {
279
+ failed.push(f);
280
+ }
281
+ }
282
+ return { completed, failed };
283
+ }
284
+ export async function getFile(url, config) {
285
+ if (typeof url !== 'string') {
286
+ throw new SkapiError('"url" should be type: string.', { code: 'INVALID_PARAMETER' });
287
+ }
288
+ if (!config?.noCdn) {
289
+ validator.Url(url);
290
+ }
291
+ let isValidEndpoint = false;
292
+ let splitUrl = url.split('/');
293
+ let host = splitUrl[2];
294
+ let splitHost = host.split('.');
295
+ let subdomain = null;
296
+ if (splitHost.length === 3 && splitHost[1] === 'skapi') {
297
+ subdomain = splitHost[0];
298
+ isValidEndpoint = true;
299
+ }
300
+ let target_key = splitUrl.slice(3);
301
+ if (!isValidEndpoint) {
302
+ if (target_key[0] !== 'auth' && target_key[0] !== 'publ') {
303
+ throw new SkapiError('Invalid file url.', { code: 'INVALID_PARAMETER' });
304
+ }
305
+ try {
306
+ validator.UserId(target_key[2]);
307
+ validator.UserId(target_key[3]);
308
+ }
309
+ catch {
310
+ throw new SkapiError('Invalid file url.', { code: 'INVALID_PARAMETER' });
311
+ }
312
+ }
313
+ let service = subdomain ? null : target_key[1];
314
+ validator.Params(config, {
315
+ expiration: ['number', () => 60],
316
+ noCdn: ['boolean', () => false],
317
+ dataType: ['base64', 'blob', 'endpoint', 'download', () => 'download']
318
+ }, [], ['progress']);
319
+ let needAuth = target_key[0] == 'auth';
320
+ let filename = url.split('/').slice(-1)[0];
321
+ if (config?.noCdn || needAuth && (config?.dataType === 'download' || config?.dataType === 'endpoint')) {
322
+ let params = {
323
+ request: subdomain ? 'get-host' : 'get',
324
+ id: subdomain || target_key[5],
325
+ key: url
326
+ };
327
+ if (service) {
328
+ params.service = service;
329
+ }
330
+ url = (await request.bind(this)('get-signed-url', params, { auth: true })).url;
331
+ }
332
+ if (config?.dataType === 'download') {
333
+ let a = document.createElement('a');
334
+ a.href = url;
335
+ document.body.appendChild(a);
336
+ a.setAttribute('download', filename);
337
+ a.target = '_blank';
338
+ a.click();
339
+ document.body.removeChild(a);
340
+ return null;
341
+ }
342
+ if (config?.dataType === 'endpoint') {
343
+ return url;
344
+ }
345
+ let blob = await request.bind(this)(url, { service: service || this.service }, { method: 'get', auth: needAuth, contentType: null, responseType: 'blob', fetchOptions: { progress: config?.progress } });
346
+ if (config?.dataType === 'base64') {
347
+ function blobToBase64(blob) {
348
+ return new Promise((resolve, _) => {
349
+ const reader = new FileReader();
350
+ reader.onloadend = () => resolve(reader.result);
351
+ reader.readAsDataURL(blob);
352
+ });
353
+ }
354
+ return blobToBase64(blob);
355
+ }
356
+ return blob;
357
+ }
358
+ export async function getRecords(query, fetchOptions) {
359
+ await this.__connection;
360
+ const indexTypes = {
361
+ '$updated': 'number',
362
+ '$uploaded': 'number',
363
+ '$referenced_count': 'number'
364
+ };
365
+ if (typeof query?.table === 'string') {
366
+ query.table = {
367
+ name: query.table,
368
+ access_group: 0
369
+ };
370
+ }
371
+ const struct = {
372
+ table: {
373
+ name: 'string',
374
+ access_group: ['number', 'private', 'public', 'authorized'],
375
+ subscription: {
376
+ user_id: (v) => validator.UserId(v, 'User ID in "subscription.user_id"'),
377
+ group: (v) => {
378
+ if (typeof v !== 'number') {
379
+ throw new SkapiError('"subscription.group" should be type: number.', { code: 'INVALID_PARAMETER' });
380
+ }
381
+ if (v > 99 || v < 0) {
382
+ throw new SkapiError('"subscription.group" should be within range: 0 ~ 99.', { code: 'INVALID_PARAMETER' });
383
+ }
384
+ return v;
385
+ }
386
+ }
387
+ },
388
+ reference: 'string',
389
+ index: {
390
+ name: (v) => {
391
+ if (typeof v !== 'string') {
392
+ throw new SkapiError('"index.name" should be type: string.', { code: 'INVALID_PARAMETER' });
393
+ }
394
+ if (indexTypes.hasOwnProperty(v)) {
395
+ return v;
396
+ }
397
+ if (['$uploaded', '$updated', '$referenced_count', '$user_id'].includes(v)) {
398
+ return v;
399
+ }
400
+ return validator.specialChars(v, 'index.name', true, false);
401
+ },
402
+ value: (v) => {
403
+ if (query.index?.name && indexTypes.hasOwnProperty(query.index.name)) {
404
+ let tp = indexTypes[query.index.name];
405
+ if (typeof v === tp) {
406
+ return v;
407
+ }
408
+ else {
409
+ throw new SkapiError(`"index.value" should be type: ${tp}.`, { code: 'INVALID_PARAMETER' });
410
+ }
411
+ }
412
+ if (typeof v === 'number') {
413
+ if (v > __index_number_range || v < -__index_number_range) {
414
+ throw new SkapiError(`Number value should be within range -${__index_number_range} ~ +${__index_number_range}`, { code: 'INVALID_PARAMETER' });
415
+ }
416
+ return v;
417
+ }
418
+ else if (typeof v === 'boolean') {
419
+ return v;
420
+ }
421
+ else {
422
+ if ('$user_id' == query.index?.name) {
423
+ return validator.UserId(v);
424
+ }
425
+ return validator.specialChars(v, 'index.value', false, true);
426
+ }
427
+ },
428
+ condition: ['gt', 'gte', 'lt', 'lte', '>', '>=', '<', '<=', '=', 'eq', '!=', 'ne'],
429
+ range: (v) => {
430
+ if (!query.index || !('value' in query.index)) {
431
+ throw new SkapiError('"index.value" is required.', { code: 'INVALID_PARAMETER' });
432
+ }
433
+ if (query.index.name === '$record_id') {
434
+ throw new SkapiError(`Cannot do "index.range" on ${query.index.name}`, { code: 'INVALID_PARAMETER' });
435
+ }
436
+ if (typeof query.index.value !== typeof v) {
437
+ throw new SkapiError('"index.range" type should match the type of "index.value".', { code: 'INVALID_PARAMETER' });
438
+ }
439
+ if (typeof v === 'string') {
440
+ return validator.specialChars(v, 'index.value');
441
+ }
442
+ return v;
443
+ }
444
+ },
445
+ tag: 'string',
446
+ private_access_key: 'string'
447
+ };
448
+ if (query?.table) {
449
+ if (query.table.access_group === 'public') {
450
+ query.table.access_group = 0;
451
+ }
452
+ else if (query.table.access_group === 'authorized') {
453
+ query.table.access_group = 1;
454
+ }
455
+ if (typeof query.table.access_group === 'number') {
456
+ if (!this.__user) {
457
+ if (0 < query.table.access_group) {
458
+ throw new SkapiError("User has no access", { code: 'INVALID_REQUEST' });
459
+ }
460
+ }
461
+ else if (this.user.access_group < query.table.access_group) {
462
+ throw new SkapiError("User has no access", { code: 'INVALID_REQUEST' });
463
+ }
464
+ }
465
+ }
466
+ if (query?.index && !query.index?.name) {
467
+ throw new SkapiError('"index.name" is required when using "index" parameter.', { code: 'INVALID_REQUEST' });
468
+ }
469
+ if (query?.record_id) {
470
+ validator.specialChars(query.record_id, 'record_id', false, false);
471
+ let outputObj = { record_id: query.record_id };
472
+ if (query?.service) {
473
+ outputObj.service = query.service;
474
+ }
475
+ query = outputObj;
476
+ }
477
+ else {
478
+ let ref_user;
479
+ if (!this.session && query.table?.access_group === 'private') {
480
+ throw new SkapiError('Unsigned users have no access to private records.', { code: 'INVALID_REQUEST' });
481
+ }
482
+ if (query.reference) {
483
+ try {
484
+ ref_user = validator.UserId(query?.reference);
485
+ }
486
+ catch (err) {
487
+ }
488
+ }
489
+ query = validator.Params(query || {}, struct, ref_user ? [] : ['table']);
490
+ if (query.table?.subscription && !this.session) {
491
+ throw new SkapiError('Unsigned users have no access to subscription records.', { code: 'INVALID_REQUEST' });
492
+ }
493
+ }
494
+ let auth = query.hasOwnProperty('access_group') && query.table.access_group ? true : !!this.__user;
495
+ let result = await request.bind(this)('get-records', query, {
496
+ fetchOptions,
497
+ auth,
498
+ method: auth ? 'post' : 'get'
499
+ });
500
+ for (let i in result.list) {
501
+ result.list[i] = normalizeRecord(result.list[i]);
502
+ }
503
+ ;
504
+ return result;
505
+ }
506
+ export async function postRecord(form, config) {
507
+ let isAdmin = await this.checkAdmin();
508
+ if (!config) {
509
+ throw new SkapiError('"config" argument is required.', { code: 'INVALID_PARAMETER' });
510
+ }
511
+ if (!this.user) {
512
+ throw new SkapiError('Login is required.', { code: 'INVALID_REQUEST' });
513
+ }
514
+ let fetchOptions = {};
515
+ if (typeof config?.formData === 'function') {
516
+ fetchOptions.formData = config.formData;
517
+ delete config.formData;
518
+ }
519
+ if (typeof config.table === 'string') {
520
+ config.table = {
521
+ name: config.table
522
+ };
523
+ if (!config.record_id) {
524
+ config.table.access_group = 0;
525
+ }
526
+ }
527
+ let progress = config.progress || null;
528
+ config = validator.Params(config || {}, {
529
+ record_id: 'string',
530
+ table: {
531
+ name: 'string',
532
+ subscription_group: ['number', null],
533
+ access_group: ['number', 'private', 'public', 'authorized']
534
+ },
535
+ reference: {
536
+ record_id: ['string', null],
537
+ reference_limit: (v) => {
538
+ if (v === null) {
539
+ return null;
540
+ }
541
+ else if (typeof v === 'number') {
542
+ if (0 > v) {
543
+ throw new SkapiError(`"reference_limit" should be >= 0`, { code: 'INVALID_PARAMETER' });
544
+ }
545
+ if (v > 4503599627370546) {
546
+ throw new SkapiError(`"reference_limit" should be <= 4503599627370546`, { code: 'INVALID_PARAMETER' });
547
+ }
548
+ return v;
549
+ }
550
+ throw new SkapiError(`"reference_limit" should be type: <number | null>`, { code: 'INVALID_PARAMETER' });
551
+ },
552
+ allow_multiple_reference: 'boolean',
553
+ },
554
+ index: {
555
+ name: 'string',
556
+ value: ['string', 'number', 'boolean']
557
+ },
558
+ tags: (v) => {
559
+ if (v === null) {
560
+ return v;
561
+ }
562
+ if (typeof v === 'string') {
563
+ return [v];
564
+ }
565
+ if (Array.isArray(v)) {
566
+ for (let i of v) {
567
+ if (typeof i !== 'string') {
568
+ throw new SkapiError(`"tags" should be type: <string | string[]>`, { code: 'INVALID_PARAMETER' });
569
+ }
570
+ validator.specialChars(v, 'tag', false, true);
571
+ }
572
+ return v;
573
+ }
574
+ throw new SkapiError(`"tags" should be type: <string | string[]>`, { code: 'INVALID_PARAMETER' });
575
+ }
576
+ }, [], ['response', 'onerror', 'progress'], null);
577
+ if (!config?.table && !config?.record_id) {
578
+ throw new SkapiError('Either "record_id" or "table" should have a value.', { code: 'INVALID_PARAMETER' });
579
+ }
580
+ if (config.table) {
581
+ if (config.table.access_group === 'public') {
582
+ config.table.access_group = 0;
583
+ }
584
+ else if (config.table.access_group === 'authorized') {
585
+ config.table.access_group = 1;
586
+ }
587
+ if (typeof config.table.access_group === 'number') {
588
+ if (!isAdmin && this.user.access_group < config.table.access_group) {
589
+ throw new SkapiError("User has no access", { code: 'INVALID_REQUEST' });
590
+ }
591
+ }
592
+ if (!config.table.name) {
593
+ throw new SkapiError('"table.name" cannot be empty string.', { code: 'INVALID_PARAMETER' });
594
+ }
595
+ if (isAdmin) {
596
+ if (config.table.access_group === 'private') {
597
+ throw new SkapiError('Service owner cannot write private records.', { code: 'INVALID_REQUEST' });
598
+ }
599
+ if (config.table.hasOwnProperty('subscription_group')) {
600
+ throw new SkapiError('Service owner cannot write to subscription table.', { code: 'INVALID_REQUEST' });
601
+ }
602
+ }
603
+ if (typeof config.table?.subscription_group === 'number' && config.table.subscription_group < 0 || config.table.subscription_group > 99) {
604
+ throw new SkapiError("Subscription group should be within range: 0 ~ 99", { code: 'INVALID_PARAMETER' });
605
+ }
606
+ }
607
+ delete config.response;
608
+ delete config.onerror;
609
+ delete config.progress;
610
+ if (config.index) {
611
+ if (!config.index.name || typeof config.index.name !== 'string') {
612
+ throw new SkapiError('"index.name" is required. type: string.', { code: 'INVALID_PARAMETER' });
613
+ }
614
+ if (!['$uploaded', '$updated', '$referenced_count', '$user_id'].includes(config.index.name)) {
615
+ validator.specialChars(config.index.name, 'index name', true);
616
+ }
617
+ if (!config.index.hasOwnProperty('value')) {
618
+ throw new SkapiError('"index.value" is required.', { code: 'INVALID_PARAMETER' });
619
+ }
620
+ if (typeof config.index.value === 'string') {
621
+ validator.specialChars(config.index.value, 'index value', false, true);
622
+ }
623
+ else if (typeof config.index.value === 'number') {
624
+ if (config.index.value > __index_number_range || config.index.value < -__index_number_range) {
625
+ throw new SkapiError(`Number value should be within range -${__index_number_range} ~ +${__index_number_range}`, { code: 'INVALID_PARAMETER' });
626
+ }
627
+ }
628
+ }
629
+ let options = { auth: true };
630
+ let postData = null;
631
+ if ((form instanceof HTMLFormElement) || (form instanceof FormData) || (form instanceof SubmitEvent)) {
632
+ let toConvert = (form instanceof SubmitEvent) ? form.target : form;
633
+ let formData = !(form instanceof FormData) ? new FormData(toConvert) : form;
634
+ let formMeta = extractFormMeta(form);
635
+ options.meta = config;
636
+ if (Object.keys(formMeta.meta).length) {
637
+ options.meta.data = formMeta.meta;
638
+ }
639
+ let formToRemove = {};
640
+ for (let [key, value] of formData.entries()) {
641
+ if (formMeta.meta.hasOwnProperty(key) && !(value instanceof Blob)) {
642
+ let f = formData.getAll(key);
643
+ let f_idx = f.indexOf(value);
644
+ if (formToRemove.hasOwnProperty(key)) {
645
+ formToRemove[key].push(f_idx);
646
+ }
647
+ else {
648
+ formToRemove[key] = [f_idx];
649
+ }
650
+ }
651
+ }
652
+ if (Object.keys(formToRemove).length) {
653
+ for (let key in formToRemove) {
654
+ let values = formData.getAll(key);
655
+ let val_len = values.length;
656
+ while (val_len--) {
657
+ if (formToRemove[key].includes(val_len)) {
658
+ values.splice(val_len, 1);
659
+ }
660
+ }
661
+ formData.delete(key);
662
+ for (let dat of values) {
663
+ formData.append(key, dat, dat instanceof File ? dat.name : null);
664
+ }
665
+ }
666
+ }
667
+ postData = formData;
668
+ }
669
+ else {
670
+ postData = Object.assign({ data: form }, config);
671
+ }
672
+ if (typeof progress === 'function') {
673
+ fetchOptions.progress = progress;
674
+ }
675
+ if (Object.keys(fetchOptions).length) {
676
+ Object.assign(options, { fetchOptions });
677
+ }
678
+ return normalizeRecord(await request.bind(this)('post-record', postData, options));
679
+ }
680
+ export async function getTables(query, fetchOptions) {
681
+ let res = await request.bind(this)('get-table', validator.Params(query || {}, {
682
+ table: 'string',
683
+ condition: ['gt', 'gte', 'lt', 'lte', '>', '>=', '<', '<=', '=', 'eq', '!=', 'ne']
684
+ }), Object.assign({ auth: true }, { fetchOptions }));
685
+ let convert = {
686
+ 'cnt_rec': 'number_of_records',
687
+ 'tbl': 'table',
688
+ 'srvc': 'service'
689
+ };
690
+ if (Array.isArray(res.list)) {
691
+ for (let t of res.list) {
692
+ for (let k in convert) {
693
+ if (t.hasOwnProperty(k)) {
694
+ t[convert[k]] = t[k];
695
+ delete t[k];
696
+ }
697
+ }
698
+ }
699
+ }
700
+ return res;
701
+ }
702
+ export async function getIndexes(query, fetchOptions) {
703
+ let p = validator.Params(query || {}, {
704
+ table: 'string',
705
+ index: (v) => validator.specialChars(v, 'index name', true, false),
706
+ order: {
707
+ by: [
708
+ 'average_number',
709
+ 'total_number',
710
+ 'number_count',
711
+ 'average_bool',
712
+ 'total_bool',
713
+ 'bool_count',
714
+ 'string_count',
715
+ 'index_name',
716
+ 'number_of_records'
717
+ ],
718
+ value: ['string', 'number', 'boolean'],
719
+ condition: ['gt', 'gte', 'lt', 'lte', '>', '>=', '<', '<=', '=', 'eq', '!=', 'ne']
720
+ }
721
+ }, ['table']);
722
+ if (p.hasOwnProperty('order')) {
723
+ if (!p.order?.by) {
724
+ throw new SkapiError('"order.by" is required.', { code: 'INVALID_PARAMETER' });
725
+ }
726
+ if (p.order.hasOwnProperty('condition') && !p.order.hasOwnProperty('value')) {
727
+ throw new SkapiError('"value" is required for "condition".', { code: 'INVALID_PARAMETER' });
728
+ }
729
+ if (p.hasOwnProperty('index')) {
730
+ if (p.index.substring(p.index.length - 1) !== '.') {
731
+ throw new SkapiError('"index" should be a parent index name of the compound index when using "order.by"', { code: 'INVALID_PARAMETER' });
732
+ }
733
+ }
734
+ }
735
+ let res = await request.bind(this)('get-index', p, Object.assign({ auth: true }, { fetchOptions }));
736
+ let convert = {
737
+ 'cnt_bool': 'boolean_count',
738
+ 'cnt_numb': 'number_count',
739
+ 'totl_numb': 'total_number',
740
+ 'totl_bool': 'total_bool',
741
+ 'avrg_numb': 'average_number',
742
+ 'avrg_bool': 'average_bool',
743
+ 'cnt_str': 'string_count'
744
+ };
745
+ if (Array.isArray(res.list)) {
746
+ res.list = res.list.map((i) => {
747
+ let iSplit = i.idx.split('/');
748
+ let resolved = {
749
+ table: iSplit[1],
750
+ index: iSplit[2],
751
+ number_of_records: i.cnt_rec
752
+ };
753
+ for (let k in convert) {
754
+ if (i?.[k]) {
755
+ resolved[convert[k]] = i[k];
756
+ }
757
+ }
758
+ return resolved;
759
+ });
760
+ }
761
+ return res;
762
+ }
763
+ export async function getTags(query, fetchOptions) {
764
+ let res = await request.bind(this)('get-tag', validator.Params(query || {}, {
765
+ table: 'string',
766
+ tag: 'string',
767
+ condition: ['gt', 'gte', 'lt', 'lte', '>', '>=', '<', '<=', '=', 'eq', '!=', 'ne']
768
+ }), Object.assign({ auth: true }, { fetchOptions }));
769
+ if (Array.isArray(res.list)) {
770
+ for (let i in res.list) {
771
+ let item = res.list[i];
772
+ let tSplit = item.tag.split('/');
773
+ res.list[i] = {
774
+ table: tSplit[1],
775
+ tag: tSplit[0],
776
+ number_of_records: item.cnt_rec
777
+ };
778
+ }
779
+ }
780
+ return res;
781
+ }
782
+ export async function deleteRecords(params) {
783
+ let isAdmin = await this.checkAdmin();
784
+ if (isAdmin && !params?.service) {
785
+ throw new SkapiError('Service ID is required.', { code: 'INVALID_PARAMETER' });
786
+ }
787
+ if (params?.record_id) {
788
+ return await request.bind(this)('del-records', {
789
+ service: params.service || this.service,
790
+ record_id: (v => {
791
+ let id = validator.specialChars(v, 'record_id', false, false);
792
+ if (typeof id === 'string') {
793
+ return [id];
794
+ }
795
+ if (id.length > 100) {
796
+ throw new SkapiError('"record_id" should not exceed 100 items.', { code: 'INVALID_PARAMETER' });
797
+ }
798
+ return id;
799
+ })(params.record_id)
800
+ }, { auth: true });
801
+ }
802
+ else {
803
+ if (!params?.table) {
804
+ if (isAdmin) {
805
+ return null;
806
+ }
807
+ throw new SkapiError('Either "table" or "record_id" is required.', { code: 'INVALID_PARAMETER' });
808
+ }
809
+ let struct = {
810
+ access_group: (v) => {
811
+ if (typeof v === 'string' && ['private', 'public', 'authorized'].includes(v)) {
812
+ switch (v) {
813
+ case 'private':
814
+ return v;
815
+ case 'public':
816
+ return 0;
817
+ case 'authorized':
818
+ return 1;
819
+ }
820
+ }
821
+ else if (typeof v === 'number' && v >= 0 && v < 100) {
822
+ return v;
823
+ }
824
+ throw new SkapiError('Invalid "table.access_group". Access group should be type <number (0~99) | "private" | "public" | "authorized">.', { code: 'INVALID_PARAMETER' });
825
+ },
826
+ name: 'string',
827
+ subscription: (v) => {
828
+ if (isAdmin) {
829
+ return validator.UserId(v, 'User ID in "table.subscription"');
830
+ }
831
+ throw new SkapiError('"table.subscription" is an invalid parameter key.', { code: 'INVALID_PARAMETER' });
832
+ },
833
+ subscription_group: (v) => {
834
+ if (isAdmin && typeof params?.table?.subscription !== 'string') {
835
+ throw new SkapiError('"table.subscription" is required.', { code: 'INVALID_PARAMETER' });
836
+ }
837
+ if (typeof v === 'number') {
838
+ if (v >= 0 && v < 99) {
839
+ return v;
840
+ }
841
+ }
842
+ throw new SkapiError('Subscription group should be between 0 ~ 99.', { code: 'INVALID_PARAMETER' });
843
+ }
844
+ };
845
+ params.table = validator.Params(params.table || {}, struct, isAdmin ? [] : ['name']);
846
+ }
847
+ return await request.bind(this)('del-records', params, { auth: true });
848
+ }
849
+ export async function grantPrivateRecordAccess(params) {
850
+ if (!params.record_id) {
851
+ throw new SkapiError(`Record ID is required.`, { code: 'INVALID_PARAMETER' });
852
+ }
853
+ if (!params.user_id || Array.isArray(params.user_id) && !params.user_id.length) {
854
+ throw new SkapiError(`User ID is required.`, { code: 'INVALID_PARAMETER' });
855
+ }
856
+ return recordAccess({
857
+ record_id: params.record_id,
858
+ user_id: params.user_id || null,
859
+ execute: 'add'
860
+ });
861
+ }
862
+ export async function removePrivateRecordAccess(params) {
863
+ if (!params.record_id) {
864
+ throw new SkapiError(`Record ID is required.`, { code: 'INVALID_PARAMETER' });
865
+ }
866
+ if (!params.user_id || Array.isArray(params.user_id) && !params.user_id.length) {
867
+ throw new SkapiError(`User ID is required.`, { code: 'INVALID_PARAMETER' });
868
+ }
869
+ return recordAccess({
870
+ record_id: params.record_id,
871
+ user_id: params.user_id || null,
872
+ execute: 'remove'
873
+ });
874
+ }
875
+ export async function listPrivateRecordAccess(params) {
876
+ return recordAccess({
877
+ record_id: params.record_id,
878
+ user_id: params.user_id || null,
879
+ execute: 'list'
880
+ });
881
+ }
882
+ export async function requestPrivateRecordAccessKey(record_id) {
883
+ await request.bind(this)('request-private-access-key', { record_id }, { auth: true });
884
+ }
885
+ async function recordAccess(params) {
886
+ let execute = params.execute;
887
+ let req = validator.Params(params, {
888
+ record_id: 'string',
889
+ user_id: (v) => {
890
+ if (!v) {
891
+ if (execute == 'list') {
892
+ return null;
893
+ }
894
+ throw new SkapiError(`User ID is required.`, { code: 'INVALID_PARAMETER' });
895
+ }
896
+ let id = validator.specialChars(v, 'user id', false, false);
897
+ if (typeof id === 'string') {
898
+ return [id];
899
+ }
900
+ if (id.length > 100) {
901
+ throw new SkapiError(`Cannot process more than 100 users at once.`, { code: 'INVALID_REQUEST' });
902
+ }
903
+ return id;
904
+ },
905
+ execute: ['add', 'remove', 'list']
906
+ }, [
907
+ 'execute',
908
+ 'record_id',
909
+ 'user_id'
910
+ ]);
911
+ if (!req.user_id) {
912
+ req.user_id = null;
913
+ }
914
+ await request.bind(this)('grant-private-access', req, { auth: true });
915
+ }