skapi-js 1.0.17-alpha → 1.0.18-alpha

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.
@@ -28,15 +28,37 @@ export default class Skapi {
28
28
  };
29
29
  private __connection;
30
30
  private __authConnection;
31
+ private __socket;
32
+ private __socket_group;
31
33
  constructor(service: string, owner: string, options?: {
32
34
  autoLogin: boolean;
33
35
  });
34
36
  updateConnection(): Promise<Connection>;
35
37
  private checkAdmin;
36
38
  private request;
37
- private getSubscribedTo;
38
- private getSubscribers;
39
39
  normalizeRecord: any;
40
+ connectRealtime(cb: (rt: {
41
+ status: 'message' | 'error' | 'success' | 'close' | 'notice';
42
+ message: any;
43
+ }) => Promise<void>): any;
44
+ closeRealtime(): Promise<void>;
45
+ getRealtimeUsers(params: {
46
+ group: string;
47
+ user_id?: string;
48
+ }, fetchOptions?: FetchOptions): Promise<DatabaseResponse<string[]>>;
49
+ postRealtime(message: any, recipient: string): Promise<{
50
+ status: 'success';
51
+ message: 'Message sent.';
52
+ } | {
53
+ status: 'error';
54
+ message: 'Realtime connection is not open.';
55
+ }>;
56
+ joinRealtime(params: {
57
+ group: string | null;
58
+ }): Promise<{
59
+ status: 'success';
60
+ message: string;
61
+ }>;
40
62
  getConnection(): Promise<Connection>;
41
63
  getProfile(options?: {
42
64
  refreshToken: boolean;
package/js/main/skapi.js CHANGED
@@ -7,8 +7,9 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import SkapiError from './error';
8
8
  import validator from '../utils/validator';
9
9
  import { getRecords, postRecord, deleteRecords, getTables, getIndexes, getTags, uploadFiles, getFile, grantPrivateRecordAccess, removePrivateRecordAccess, listPrivateRecordAccess, requestPrivateRecordAccessKey, deleteFiles, normalizeRecord } from '../methods/database';
10
+ import { connectRealtime, joinRealtime, postRealtime, closeRealtime, getRealtimeUsers } from '../methods/realtime';
10
11
  import { request, secureRequest, mock, getFormResponse, formHandler, getConnection } from '../methods/request';
11
- import { subscribe, unsubscribe, blockSubscriber, unblockSubscriber, getSubscribers, getSubscribedTo, getSubscriptions, subscribeNewsletter, getNewsletters, unsubscribeNewsletter, getNewsletterSubscription } from '../methods/subscription';
12
+ import { subscribe, unsubscribe, blockSubscriber, unblockSubscriber, getSubscriptions, subscribeNewsletter, getNewsletters, unsubscribeNewsletter, getNewsletterSubscription } from '../methods/subscription';
12
13
  import { checkAdmin, getProfile, logout, recoverAccount, resendSignupConfirmation, authentication, login, signup, disableAccount, resetPassword, verifyEmail, verifyPhoneNumber, forgotPassword, changePassword, updateProfile, getUsers, setUserPool, userPool, lastVerifiedEmail, requestUsernameChange } from '../methods/user';
13
14
  export default class Skapi {
14
15
  get user() {
@@ -22,7 +23,7 @@ export default class Skapi {
22
23
  set user(value) {
23
24
  }
24
25
  constructor(service, owner, options) {
25
- this.version = '1.0.17-alpha';
26
+ this.version = '1.0.18-alpha';
26
27
  this.session = null;
27
28
  this.connection = null;
28
29
  this.host = 'skapi';
@@ -84,8 +85,6 @@ export default class Skapi {
84
85
  };
85
86
  this.checkAdmin = checkAdmin.bind(this);
86
87
  this.request = request.bind(this);
87
- this.getSubscribedTo = getSubscribedTo.bind(this);
88
- this.getSubscribers = getSubscribers.bind(this);
89
88
  this.normalizeRecord = normalizeRecord.bind(this);
90
89
  if (typeof service !== 'string' || typeof owner !== 'string') {
91
90
  throw new SkapiError('"service" and "owner" should be type <string>.', { code: 'INVALID_PARAMETER' });
@@ -192,6 +191,21 @@ export default class Skapi {
192
191
  }, { bypassAwaitConnection: true, method: 'get' });
193
192
  return this.connection;
194
193
  }
194
+ connectRealtime(cb) {
195
+ return connectRealtime.bind(this)(cb);
196
+ }
197
+ closeRealtime() {
198
+ return closeRealtime.bind(this)();
199
+ }
200
+ getRealtimeUsers(params, fetchOptions) {
201
+ return getRealtimeUsers.bind(this)(params, fetchOptions);
202
+ }
203
+ postRealtime(message, recipient) {
204
+ return postRealtime.bind(this)(message, recipient);
205
+ }
206
+ joinRealtime(params) {
207
+ return joinRealtime.bind(this)(params);
208
+ }
195
209
  getConnection() {
196
210
  return getConnection.bind(this)();
197
211
  }
@@ -291,6 +305,15 @@ export default class Skapi {
291
305
  return subscribeNewsletter.bind(this)(form);
292
306
  }
293
307
  }
308
+ __decorate([
309
+ formHandler()
310
+ ], Skapi.prototype, "getRealtimeUsers", null);
311
+ __decorate([
312
+ formHandler()
313
+ ], Skapi.prototype, "postRealtime", null);
314
+ __decorate([
315
+ formHandler()
316
+ ], Skapi.prototype, "joinRealtime", null);
294
317
  __decorate([
295
318
  formHandler()
296
319
  ], Skapi.prototype, "getConnection", null);
@@ -555,6 +555,9 @@ export async function getRecords(query, fetchOptions) {
555
555
  throw new SkapiError("User has no access", { code: 'INVALID_REQUEST' });
556
556
  }
557
557
  }
558
+ if (!query.table.hasOwnProperty('access_group')) {
559
+ query.table.access_group = query.table?.subscription ? 1 : 0;
560
+ }
558
561
  }
559
562
  if (query?.index && !query.index?.name) {
560
563
  throw new SkapiError('"index.name" is required when using "index" parameter.', { code: 'INVALID_REQUEST' });
@@ -725,7 +728,7 @@ export async function postRecord(form, config) {
725
728
  if (!config_chkd?.table && !config_chkd?.record_id) {
726
729
  throw new SkapiError('Either "record_id" or "table" should have a value.', { code: 'INVALID_PARAMETER' });
727
730
  }
728
- if (typeof config_chkd.table !== 'string' && config_chkd.table) {
731
+ if (config_chkd.table) {
729
732
  if (config_chkd.table.access_group === 'public') {
730
733
  config_chkd.table.access_group = 0;
731
734
  }
@@ -747,8 +750,8 @@ export async function postRecord(form, config) {
747
750
  }
748
751
  }
749
752
  if (config_chkd.table?.subscription) {
750
- if (!config_chkd?.record_id && !config_chkd.table?.access_group) {
751
- throw new SkapiError('Public records cannot require subscription.', { code: 'INVALID_REQUEST' });
753
+ if (!config_chkd?.record_id && !config_chkd.table.hasOwnProperty('access_group')) {
754
+ config_chkd.table.access_group = 1;
752
755
  }
753
756
  if (config_chkd.table.access_group === 0) {
754
757
  throw new SkapiError('Public records cannot require subscription.', { code: 'INVALID_REQUEST' });
@@ -0,0 +1,27 @@
1
+ import { DatabaseResponse, FetchOptions } from '../Types';
2
+ type RealTimeCallback = (rt: {
3
+ status: 'message' | 'error' | 'success' | 'close' | 'notice';
4
+ message: string | {
5
+ [key: string]: any;
6
+ };
7
+ }) => void;
8
+ export declare function closeRealtime(): Promise<void>;
9
+ export declare function connectRealtime(cb: RealTimeCallback, delay?: number): Promise<void>;
10
+ export declare function postRealtime(message: any, recipient: string): Promise<{
11
+ status: 'success';
12
+ message: 'Message sent.';
13
+ } | {
14
+ status: 'error';
15
+ message: 'Realtime connection is not open.';
16
+ }>;
17
+ export declare function joinRealtime(params: {
18
+ group?: string | null;
19
+ }): Promise<{
20
+ status: 'success';
21
+ message: string;
22
+ }>;
23
+ export declare function getRealtimeUsers(params: {
24
+ group: string;
25
+ user_id?: string;
26
+ }, fetchOptions?: FetchOptions): Promise<DatabaseResponse<string[]>>;
27
+ export {};
@@ -0,0 +1,159 @@
1
+ import SkapiError from '../main/error';
2
+ import validator from '../utils/validator';
3
+ import { extractFormMeta } from '../utils/utils';
4
+ import { request } from './request';
5
+ async function prepareWebsocket() {
6
+ await this.getProfile();
7
+ if (!this.session) {
8
+ throw new SkapiError(`No access.`, { code: 'INVALID_REQUEST' });
9
+ }
10
+ let r = await this.record_endpoint;
11
+ return new WebSocket(r.websocket_private + '?token=' + this.session.accessToken.jwtToken);
12
+ }
13
+ let reconnectAttempts = 0;
14
+ export async function closeRealtime() {
15
+ let socket = this.__socket ? await this.__socket : this.__socket;
16
+ if (socket) {
17
+ socket.close();
18
+ }
19
+ this.__socket = null;
20
+ this.__socket_group = null;
21
+ return null;
22
+ }
23
+ export function connectRealtime(cb, delay = 0) {
24
+ if (typeof cb !== 'function') {
25
+ throw new SkapiError(`Callback must be a function.`, { code: 'INVALID_REQUEST' });
26
+ }
27
+ if (reconnectAttempts || !(this.__socket instanceof Promise)) {
28
+ this.__socket = new Promise(resolve => {
29
+ setTimeout(async () => {
30
+ await this.__connection;
31
+ let socket = await prepareWebsocket.bind(this)();
32
+ socket.onopen = () => {
33
+ reconnectAttempts = 0;
34
+ cb({ status: 'success', message: 'Connected to WebSocket server.' });
35
+ if (this.__socket_group) {
36
+ socket.send(JSON.stringify({
37
+ action: 'joinGroup',
38
+ rid: this.__socket_group,
39
+ token: this.session.accessToken.jwtToken
40
+ }));
41
+ }
42
+ resolve(socket);
43
+ };
44
+ socket.onmessage = event => {
45
+ let data = JSON.parse(decodeURI(event.data));
46
+ if (data?.['#notice']) {
47
+ cb({ status: 'notice', message: data['#notice'] });
48
+ }
49
+ else {
50
+ cb({ status: 'message', message: data });
51
+ }
52
+ };
53
+ socket.onclose = event => {
54
+ if (event.wasClean) {
55
+ cb({ status: 'close', message: 'WebSocket connection closed.' });
56
+ this.__socket = null;
57
+ this.__socket_group = null;
58
+ }
59
+ else {
60
+ const maxAttempts = 10;
61
+ reconnectAttempts++;
62
+ if (reconnectAttempts < maxAttempts) {
63
+ let delay = Math.min(1000 * (2 ** reconnectAttempts), 30000);
64
+ cb({ status: 'error', message: `Skapi: WebSocket connection error. Reconnecting in ${delay / 1000} seconds...` });
65
+ connectRealtime.bind(this)(cb, delay);
66
+ }
67
+ else {
68
+ cb({ status: 'error', message: 'Skapi: WebSocket connection error. Max reconnection attempts reached.' });
69
+ this.__socket = null;
70
+ }
71
+ }
72
+ };
73
+ socket.onerror = () => {
74
+ cb({ status: 'error', message: 'Skapi: WebSocket connection error.' });
75
+ throw new SkapiError(`Skapi: WebSocket connection error.`, { code: 'ERROR' });
76
+ };
77
+ }, delay);
78
+ });
79
+ }
80
+ return this.__socket;
81
+ }
82
+ export async function postRealtime(message, recipient) {
83
+ let socket = this.__socket ? await this.__socket : this.__socket;
84
+ if (!socket) {
85
+ throw new SkapiError(`No realtime connection. Execute connectRealtime() before this method.`, { code: 'INVALID_REQUEST' });
86
+ }
87
+ if (!recipient) {
88
+ throw new SkapiError(`No recipient.`, { code: 'INVALID_REQUEST' });
89
+ }
90
+ if (message instanceof FormData || message instanceof SubmitEvent || message instanceof HTMLFormElement) {
91
+ message = extractFormMeta(message).meta;
92
+ }
93
+ if (socket.readyState === 1) {
94
+ try {
95
+ validator.UserId(recipient);
96
+ socket.send(JSON.stringify({
97
+ action: 'sendMessage',
98
+ uid: recipient,
99
+ content: message,
100
+ token: this.session.accessToken.jwtToken
101
+ }));
102
+ }
103
+ catch (err) {
104
+ if (this.__socket_group !== recipient) {
105
+ throw new SkapiError(`User has not joined to the recipient group. Run joinRealtime("${recipient}")`, { code: 'INVALID_REQUEST' });
106
+ }
107
+ socket.send(JSON.stringify({
108
+ action: 'broadcast',
109
+ rid: recipient,
110
+ content: message,
111
+ token: this.session.accessToken.jwtToken
112
+ }));
113
+ }
114
+ return { status: 'success', message: 'Message sent.' };
115
+ }
116
+ return { status: 'error', message: 'Realtime connection is not open.' };
117
+ }
118
+ export async function joinRealtime(params) {
119
+ let socket = this.__socket ? await this.__socket : this.__socket;
120
+ if (!socket) {
121
+ throw new SkapiError(`No realtime connection. Execute connectRealtime() before this method.`, { code: 'INVALID_REQUEST' });
122
+ }
123
+ if (params instanceof FormData || params instanceof SubmitEvent || params instanceof HTMLFormElement) {
124
+ params = extractFormMeta(params).meta;
125
+ }
126
+ let { group = null } = params;
127
+ if (!group && !this.__socket_group) {
128
+ return { status: 'success', message: 'Left realtime message group.' };
129
+ }
130
+ if (group !== null && typeof group !== 'string') {
131
+ throw new SkapiError(`"group" must be a string | null.`, { code: 'INVALID_PARAMETER' });
132
+ }
133
+ socket.send(JSON.stringify({
134
+ action: 'joinGroup',
135
+ rid: group,
136
+ token: this.session.accessToken.jwtToken
137
+ }));
138
+ this.__socket_group = group;
139
+ return { status: 'success', message: group ? `Joined realtime message group: "${group}".` : 'Left realtime message group.' };
140
+ }
141
+ export async function getRealtimeUsers(params, fetchOptions) {
142
+ await this.__connection;
143
+ params = validator.Params(params, {
144
+ user_id: (v) => validator.UserId(v, 'User ID in "user_id"'),
145
+ group: 'string'
146
+ }, ['group']);
147
+ if (!params.group) {
148
+ throw new SkapiError(`"group" is required.`, { code: 'INVALID_PARAMETER' });
149
+ }
150
+ let res = request.bind(this)('get-ws-group', params, {
151
+ fetchOptions,
152
+ auth: true,
153
+ method: 'post'
154
+ });
155
+ for (let i = 0; i < res.list.length; i++) {
156
+ res[i] = res[i].uid.split('#')[1];
157
+ }
158
+ return res;
159
+ }
@@ -91,6 +91,7 @@ export async function request(url, data = null, options) {
91
91
  case 'get-signed-url':
92
92
  case 'grant-private-access':
93
93
  case 'request-private-access-key':
94
+ case 'get-ws-group':
94
95
  case 'del-files':
95
96
  return {
96
97
  private: record.record_private,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skapi-js",
3
- "version": "1.0.17-alpha",
3
+ "version": "1.0.18-alpha",
4
4
  "description": "Javascript library for Skapi: Complete JAM Stack, front-end driven serverless backend API service.",
5
5
  "main": "./dist/skapi.module.js",
6
6
  "types": "./js/Main.d.ts",