docpouch-client 0.8.2 → 0.8.4
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/dist/index.d.ts +17 -7
- package/dist/index.js +198 -24
- package/dist/types.d.ts +17 -2
- package/dist/types.js +1 -2
- package/jest.config.js +18 -0
- package/package.json +34 -23
- package/src/__tests__/index.test.ts +489 -0
- package/src/index.ts +230 -24
- package/src/types.ts +11 -1
- package/tsconfig.json +4 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
import type { I_UserEntry, I_UserLogin, I_UserCreation, I_UserUpdate, I_UserDisplay, I_DocumentEntry, I_DataStructure, I_LoginResponse, I_DocumentQuery, I_StructureCreation, I_WsMessage } from "./types.js";
|
|
2
|
-
import {
|
|
3
|
-
export default class
|
|
1
|
+
import type { I_UserEntry, I_UserLogin, I_UserCreation, I_UserUpdate, I_UserDisplay, I_DocumentEntry, I_DataStructure, I_LoginResponse, I_DocumentQuery, I_StructureCreation, I_WsMessage, I_EventString, I_DocumentType } from "./types.js";
|
|
2
|
+
import { Socket } from "socket.io-client";
|
|
3
|
+
export default class dbPouchClient {
|
|
4
4
|
baseUrl: string;
|
|
5
|
-
private
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
private authToken;
|
|
6
|
+
socket: Socket;
|
|
7
|
+
callbackFunction: ((event: I_EventString, data: I_WsMessage) => void) | undefined;
|
|
8
|
+
realTimeSync: boolean;
|
|
9
|
+
private connectionInProgress;
|
|
10
|
+
constructor(host: string, port?: number, callback?: (event: I_EventString, data: I_WsMessage) => void);
|
|
11
|
+
private setupPermanentSocketListeners;
|
|
12
|
+
setRealTimeSync: (newRealTimeSync: boolean) => void;
|
|
13
|
+
private initWebSocket;
|
|
8
14
|
private request;
|
|
9
15
|
login(credentials: I_UserLogin): Promise<I_LoginResponse | null>;
|
|
10
16
|
listUsers(): Promise<I_UserEntry[]>;
|
|
@@ -20,6 +26,10 @@ export default class Index {
|
|
|
20
26
|
getStructures(): Promise<I_DataStructure[]>;
|
|
21
27
|
updateStructure(structureID: string, structureData: I_DataStructure): Promise<void>;
|
|
22
28
|
removeStructure(structureID: string): Promise<void>;
|
|
29
|
+
createType(type: I_DocumentType): Promise<I_DocumentType>;
|
|
30
|
+
removeType(typeID: string): Promise<void>;
|
|
31
|
+
getTypes(): Promise<I_DocumentType[]>;
|
|
23
32
|
setToken(token: string | null): void;
|
|
24
|
-
|
|
33
|
+
getVersion(): string;
|
|
34
|
+
debugSocketConnection(): void;
|
|
25
35
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,32 +1,154 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
this.
|
|
7
|
-
this.
|
|
8
|
-
this.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
this.
|
|
1
|
+
import { io } from "socket.io-client";
|
|
2
|
+
import packetJson from '../package.json';
|
|
3
|
+
export default class dbPouchClient {
|
|
4
|
+
constructor(host, port = 80, callback) {
|
|
5
|
+
this.authToken = null;
|
|
6
|
+
this.realTimeSync = false;
|
|
7
|
+
this.connectionInProgress = false;
|
|
8
|
+
this.setRealTimeSync = (newRealTimeSync) => {
|
|
9
|
+
console.log(`Setting realtime sync to: ${newRealTimeSync}. Current setting: ${this.realTimeSync}`);
|
|
10
|
+
// Skip if the setting isn't changing
|
|
11
|
+
if (newRealTimeSync === this.realTimeSync) {
|
|
12
|
+
console.log("Realtime sync setting unchanged, skipping");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
this.realTimeSync = newRealTimeSync;
|
|
16
|
+
if (newRealTimeSync && this.authToken) {
|
|
17
|
+
console.log("Activating realtime updates");
|
|
18
|
+
// Ensure we're not in the middle of another connection attempt
|
|
19
|
+
if (this.connectionInProgress) {
|
|
20
|
+
console.log("Connection already in progress, waiting before initializing");
|
|
21
|
+
setTimeout(() => this.initWebSocket(), 500);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
this.initWebSocket();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
else if (!newRealTimeSync) {
|
|
28
|
+
console.log("Deactivating realtime updates");
|
|
29
|
+
if (this.socket.connected) {
|
|
30
|
+
console.log("Disconnecting socket");
|
|
31
|
+
this.socket.disconnect();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
this.baseUrl = host;
|
|
36
|
+
const socketUrl = host.includes('://') ? host : `http://${host}`;
|
|
37
|
+
// Don't append port twice if it's already in the URL
|
|
38
|
+
const socketUrlWithPort = socketUrl.includes(':') && !socketUrl.endsWith(':')
|
|
39
|
+
? socketUrl
|
|
40
|
+
: `${socketUrl}:${port}`;
|
|
41
|
+
console.log(`Initializing Socket.IO with URL: ${socketUrlWithPort}, path: /socket.io`);
|
|
42
|
+
this.socket = io(`${socketUrlWithPort}`, {
|
|
43
|
+
autoConnect: false,
|
|
44
|
+
transports: ['websocket'], // Try websocket only first
|
|
45
|
+
reconnection: true,
|
|
46
|
+
reconnectionAttempts: 5,
|
|
47
|
+
reconnectionDelay: 1000,
|
|
48
|
+
forceNew: true, // Force a new connection
|
|
49
|
+
auth: {
|
|
50
|
+
token: null // Will be set later
|
|
51
|
+
},
|
|
52
|
+
path: '/socket.io'
|
|
53
|
+
});
|
|
54
|
+
this.callbackFunction = callback;
|
|
55
|
+
this.setupPermanentSocketListeners();
|
|
56
|
+
}
|
|
57
|
+
setupPermanentSocketListeners() {
|
|
58
|
+
// These are permanent listeners that won't be removed
|
|
59
|
+
this.socket.on('connect_error', (error) => {
|
|
60
|
+
console.error('Socket connection error:', error.message);
|
|
61
|
+
this.connectionInProgress = false;
|
|
62
|
+
});
|
|
63
|
+
this.socket.on('connect', () => {
|
|
64
|
+
console.log('Socket connected successfully with ID:', this.socket.id);
|
|
65
|
+
this.connectionInProgress = false;
|
|
66
|
+
});
|
|
67
|
+
this.socket.on('disconnect', (reason) => {
|
|
68
|
+
console.log('Socket disconnected. Reason:', reason);
|
|
69
|
+
this.connectionInProgress = false;
|
|
70
|
+
});
|
|
71
|
+
this.socket.on('error', (error) => {
|
|
72
|
+
console.error('Socket error:', error);
|
|
73
|
+
this.connectionInProgress = false;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
initWebSocket() {
|
|
77
|
+
console.log("initWebSocket called. Auth token present:", !!this.authToken, "Connection in progress:", this.connectionInProgress, "Socket connected:", this.socket.connected);
|
|
78
|
+
if (!this.authToken) {
|
|
79
|
+
console.log("Skipping WebSocket initialization: No auth token");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (this.connectionInProgress) {
|
|
83
|
+
console.log("Connection already in progress, skipping initialization");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (this.socket.connected) {
|
|
87
|
+
console.log("Socket already connected with ID:", this.socket.id);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
this.connectionInProgress = true;
|
|
91
|
+
try {
|
|
92
|
+
console.log("Setting up WebSocket connection with token");
|
|
93
|
+
// Update the auth token
|
|
94
|
+
this.socket.auth = { token: this.authToken };
|
|
95
|
+
// Remove any dynamic event listeners that might have been added
|
|
96
|
+
this.socket.offAny();
|
|
97
|
+
// Set up event handler for application events
|
|
98
|
+
this.socket.onAny((event, data) => {
|
|
99
|
+
if (event === "heartbeatPing") {
|
|
100
|
+
console.log("Ping event received:", data);
|
|
101
|
+
this.socket.emit("heartbeatPong", Date.now());
|
|
102
|
+
}
|
|
103
|
+
else if (this.callbackFunction) {
|
|
104
|
+
this.callbackFunction(event, data);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
// Connect to the server
|
|
108
|
+
console.log("Connecting socket with auth token");
|
|
109
|
+
this.socket.connect();
|
|
110
|
+
// Add a timeout to detect if connection is taking too long
|
|
111
|
+
setTimeout(() => {
|
|
112
|
+
if (this.connectionInProgress) {
|
|
113
|
+
console.warn("Socket connection attempt timed out after 5 seconds");
|
|
114
|
+
this.connectionInProgress = false;
|
|
115
|
+
// If we're still not connected after the timeout, try again with polling
|
|
116
|
+
if (!this.socket.connected) {
|
|
117
|
+
console.log("Retrying connection with polling transport");
|
|
118
|
+
this.socket.io.opts.transports = ['polling', 'websocket'];
|
|
119
|
+
this.socket.connect();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}, 5000);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
console.error('Error in initWebSocket:', error);
|
|
126
|
+
this.connectionInProgress = false;
|
|
12
127
|
}
|
|
13
128
|
}
|
|
14
129
|
async request(endpoint, method, body, requiresAuth = true) {
|
|
15
130
|
const headers = {
|
|
16
|
-
'Content-Type': 'application/json'
|
|
131
|
+
'Content-Type': 'application/json',
|
|
17
132
|
};
|
|
18
|
-
if (requiresAuth && this.
|
|
19
|
-
headers['Authorization'] = `Bearer ${this.
|
|
20
|
-
|
|
133
|
+
if (requiresAuth && this.authToken)
|
|
134
|
+
headers['Authorization'] = `Bearer ${this.authToken}`;
|
|
135
|
+
if (this.socket.id)
|
|
136
|
+
headers['X-Socket-ID'] = this.socket.id;
|
|
21
137
|
const options = {
|
|
22
138
|
method,
|
|
23
139
|
headers,
|
|
24
140
|
body: body ? JSON.stringify(body) : undefined
|
|
25
141
|
};
|
|
26
|
-
|
|
142
|
+
// Ensure endpoint starts with /
|
|
143
|
+
const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
|
144
|
+
// Ensure baseUrl doesn't end with /
|
|
145
|
+
const normalizedBaseUrl = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl;
|
|
146
|
+
const url = `${normalizedBaseUrl}${normalizedEndpoint}`;
|
|
147
|
+
console.log(`Making ${method} request to: ${url}`);
|
|
148
|
+
const response = await fetch(url, options);
|
|
27
149
|
if (!response.ok) {
|
|
28
150
|
if (response.status === 401 || response.status === 403) {
|
|
29
|
-
this.
|
|
151
|
+
this.authToken = null;
|
|
30
152
|
}
|
|
31
153
|
throw new Error(`API error: ${response.status} ${response.statusText}`);
|
|
32
154
|
}
|
|
@@ -36,7 +158,11 @@ class Index {
|
|
|
36
158
|
async login(credentials) {
|
|
37
159
|
const response = await this.request('/users/login', 'POST', credentials, false);
|
|
38
160
|
if (response.token) {
|
|
39
|
-
this.
|
|
161
|
+
this.authToken = response.token;
|
|
162
|
+
// Reconnect websocket with new token if realtime sync is enabled
|
|
163
|
+
if (this.realTimeSync) {
|
|
164
|
+
this.initWebSocket();
|
|
165
|
+
}
|
|
40
166
|
return { token: response.token, isAdmin: response.isAdmin };
|
|
41
167
|
}
|
|
42
168
|
return null;
|
|
@@ -61,7 +187,7 @@ class Index {
|
|
|
61
187
|
return await this.request('/docs/list', 'GET');
|
|
62
188
|
}
|
|
63
189
|
async fetchDocument(queryObject) {
|
|
64
|
-
return await this.request(`/docs/fetch/`, '
|
|
190
|
+
return await this.request(`/docs/fetch/`, 'POST', queryObject);
|
|
65
191
|
}
|
|
66
192
|
async updateDocument(documentID, documentData) {
|
|
67
193
|
await this.request(`/docs/update/${documentID}`, 'PATCH', documentData);
|
|
@@ -82,12 +208,60 @@ class Index {
|
|
|
82
208
|
async removeStructure(structureID) {
|
|
83
209
|
await this.request(`/structures/remove/${structureID}`, 'DELETE');
|
|
84
210
|
}
|
|
211
|
+
// Data Type Endpoints
|
|
212
|
+
async createType(type) {
|
|
213
|
+
return await this.request('/types/write', 'POST', type);
|
|
214
|
+
}
|
|
215
|
+
async removeType(typeID) {
|
|
216
|
+
return await this.request(`/types/remove/${typeID}`, 'DELETE');
|
|
217
|
+
}
|
|
218
|
+
async getTypes() {
|
|
219
|
+
return await this.request('/types/list', 'GET');
|
|
220
|
+
}
|
|
85
221
|
setToken(token) {
|
|
86
|
-
|
|
222
|
+
console.log("Setting token to:", token ? "***token***" : "null");
|
|
223
|
+
const tokenChanged = this.authToken !== token;
|
|
224
|
+
this.authToken = token;
|
|
225
|
+
if (!tokenChanged) {
|
|
226
|
+
console.log("Token unchanged, no need to reconnect");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
// If we have a new token and realtime sync is enabled
|
|
230
|
+
if (token && this.realTimeSync) {
|
|
231
|
+
console.log("New token set, will initialize WebSocket");
|
|
232
|
+
// Ensure any existing connection is closed first
|
|
233
|
+
if (this.socket.connected) {
|
|
234
|
+
console.log("Disconnecting existing socket before reconnecting with new token");
|
|
235
|
+
this.socket.disconnect();
|
|
236
|
+
}
|
|
237
|
+
// Wait a moment for the disconnect to complete
|
|
238
|
+
setTimeout(() => {
|
|
239
|
+
console.log("Initializing WebSocket with new token");
|
|
240
|
+
this.initWebSocket();
|
|
241
|
+
}, 300);
|
|
242
|
+
}
|
|
243
|
+
// If token was cleared or realtime sync is disabled
|
|
244
|
+
else if (this.socket.connected) {
|
|
245
|
+
console.log("Token cleared or realtime sync disabled, disconnecting");
|
|
246
|
+
this.socket.disconnect();
|
|
247
|
+
}
|
|
87
248
|
}
|
|
88
|
-
|
|
89
|
-
return
|
|
249
|
+
getVersion() {
|
|
250
|
+
return packetJson.version;
|
|
251
|
+
}
|
|
252
|
+
debugSocketConnection() {
|
|
253
|
+
console.log("Socket connection debug info:");
|
|
254
|
+
console.log("- Connected:", this.socket.connected);
|
|
255
|
+
console.log("- Socket ID:", this.socket.id);
|
|
256
|
+
console.log("- Auth token present:", !!this.authToken);
|
|
257
|
+
console.log("- Connection in progress:", this.connectionInProgress);
|
|
258
|
+
console.log("- Realtime sync enabled:", this.realTimeSync);
|
|
259
|
+
console.log("- Socket options:", this.socket.io.opts);
|
|
260
|
+
// Try to force reconnection
|
|
261
|
+
if (!this.socket.connected && this.authToken && this.realTimeSync) {
|
|
262
|
+
console.log("Attempting to force reconnection...");
|
|
263
|
+
this.socket.auth = { token: this.authToken };
|
|
264
|
+
this.socket.connect();
|
|
265
|
+
}
|
|
90
266
|
}
|
|
91
267
|
}
|
|
92
|
-
exports.default = Index;
|
|
93
|
-
module.exports = Index;
|
package/dist/types.d.ts
CHANGED
|
@@ -9,17 +9,23 @@ export interface I_UserCreation {
|
|
|
9
9
|
name: string;
|
|
10
10
|
password: string;
|
|
11
11
|
email?: string;
|
|
12
|
+
department: string;
|
|
13
|
+
group: string;
|
|
12
14
|
isAdmin: boolean;
|
|
13
15
|
}
|
|
14
16
|
export interface I_UserUpdate {
|
|
15
17
|
name?: string;
|
|
16
18
|
password?: string;
|
|
17
19
|
email?: string;
|
|
20
|
+
department?: string;
|
|
21
|
+
group?: string;
|
|
18
22
|
isAdmin?: boolean;
|
|
19
23
|
}
|
|
20
24
|
export interface I_UserDisplay {
|
|
21
25
|
_id: string;
|
|
22
26
|
username: string;
|
|
27
|
+
department: string;
|
|
28
|
+
group: string;
|
|
23
29
|
email?: string;
|
|
24
30
|
}
|
|
25
31
|
export interface I_LoginResponse {
|
|
@@ -78,6 +84,15 @@ export interface I_StructureUpdate {
|
|
|
78
84
|
reference?: any;
|
|
79
85
|
fields?: any[];
|
|
80
86
|
}
|
|
87
|
+
export interface I_DocumentType {
|
|
88
|
+
_id?: string;
|
|
89
|
+
type: number;
|
|
90
|
+
subType: number;
|
|
91
|
+
name: string;
|
|
92
|
+
description?: string;
|
|
93
|
+
defaultStructureID?: string;
|
|
94
|
+
}
|
|
95
|
+
export type I_EventString = 'subscribe' | 'unsubscribe' | 'heartbeatPong' | "heartbeatPing" | "newDocument" | "newStructure" | "newUser" | "removedID" | "changedDocument" | "changedStructure" | "changedUser" | "confirmSubscription" | "confirmUnsubscription" | "removedUser" | "removedStructure" | "removedDocument";
|
|
81
96
|
export interface I_WsMessage {
|
|
82
97
|
newDocument?: I_DocumentEntry;
|
|
83
98
|
newStructure?: I_StructureEntry;
|
|
@@ -88,6 +103,6 @@ export interface I_WsMessage {
|
|
|
88
103
|
changedUser?: I_UserUpdate;
|
|
89
104
|
confirmSubscription?: boolean;
|
|
90
105
|
confirmUnsubscription?: boolean;
|
|
91
|
-
|
|
92
|
-
|
|
106
|
+
heartbeatPing?: number;
|
|
107
|
+
heartbeatPong?: number;
|
|
93
108
|
}
|
package/dist/types.js
CHANGED
package/jest.config.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
preset: 'ts-jest',
|
|
3
|
+
testEnvironment: 'node',
|
|
4
|
+
moduleNameMapper: {
|
|
5
|
+
'^(\\.{1,2}/.*)\\.js$': '$1',
|
|
6
|
+
},
|
|
7
|
+
transform: {
|
|
8
|
+
'^.+\\.tsx?$': ['ts-jest', {
|
|
9
|
+
useESM: true,
|
|
10
|
+
}],
|
|
11
|
+
},
|
|
12
|
+
extensionsToTreatAsEsm: ['.ts'],
|
|
13
|
+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
|
14
|
+
testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'],
|
|
15
|
+
collectCoverage: true,
|
|
16
|
+
coverageDirectory: 'coverage',
|
|
17
|
+
coverageReporters: ['text', 'lcov'],
|
|
18
|
+
};
|
package/package.json
CHANGED
|
@@ -1,23 +1,34 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "docpouch-client",
|
|
3
|
-
"version": "0.8.
|
|
4
|
-
"main": "dist/index.js",
|
|
5
|
-
"types": "dist/index.d.ts",
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "docpouch-client",
|
|
3
|
+
"version": "0.8.4",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": "./dist/index.js",
|
|
9
|
+
"require": "./dist/index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"test": "jest"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"docPouch",
|
|
19
|
+
"API"
|
|
20
|
+
],
|
|
21
|
+
"author": "Jan Frecè",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"description": "A small class to more easily access the docPouch API",
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/jest": "^29.5.14",
|
|
26
|
+
"@types/node": "^24.0.1",
|
|
27
|
+
"jest": "^30.0.0",
|
|
28
|
+
"ts-jest": "^29.4.0"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"socket.io-client": "^4.8.1",
|
|
32
|
+
"typescript": "^5.8.3"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import dbPouchClient from '../index.js';
|
|
2
|
+
import packetJson from '../../package.json';
|
|
3
|
+
import {
|
|
4
|
+
I_UserLogin,
|
|
5
|
+
I_UserCreation,
|
|
6
|
+
I_UserUpdate,
|
|
7
|
+
I_DocumentEntry,
|
|
8
|
+
I_DocumentQuery,
|
|
9
|
+
I_StructureCreation,
|
|
10
|
+
I_DataStructure,
|
|
11
|
+
} from '../types.js';
|
|
12
|
+
|
|
13
|
+
// Mock socket.io-client
|
|
14
|
+
jest.mock('socket.io-client', () => {
|
|
15
|
+
const mockSocket = {
|
|
16
|
+
connect: jest.fn(),
|
|
17
|
+
emit: jest.fn(),
|
|
18
|
+
onAny: jest.fn(),
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
io: jest.fn(() => mockSocket),
|
|
22
|
+
Socket: jest.requireActual('socket.io-client').Socket,
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Mock fetch API
|
|
27
|
+
global.fetch = jest.fn();
|
|
28
|
+
const mockFetch = global.fetch as jest.Mock;
|
|
29
|
+
|
|
30
|
+
describe('Index Class', () => {
|
|
31
|
+
let index: dbPouchClient;
|
|
32
|
+
const baseUrl = 'http://example.com';
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
// Reset mocks before each test
|
|
36
|
+
jest.clearAllMocks();
|
|
37
|
+
mockFetch.mockClear();
|
|
38
|
+
|
|
39
|
+
// Create a new instance of Index for each test
|
|
40
|
+
index = new dbPouchClient(baseUrl);
|
|
41
|
+
|
|
42
|
+
// Mock successful fetch response
|
|
43
|
+
mockFetch.mockImplementation(() =>
|
|
44
|
+
Promise.resolve({
|
|
45
|
+
ok: true,
|
|
46
|
+
status: 200,
|
|
47
|
+
statusText: 'OK',
|
|
48
|
+
json: () => Promise.resolve({}),
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Test constructor
|
|
54
|
+
describe('constructor', () => {
|
|
55
|
+
it('should initialize with the provided baseUrl', () => {
|
|
56
|
+
expect(index.baseUrl).toBe(baseUrl);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should initialize socket with autoConnect: false by default', () => {
|
|
60
|
+
expect(require('socket.io-client').io).toHaveBeenCalledWith(baseUrl, { autoConnect: false });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should connect socket and set up event handler when callback is provided', () => {
|
|
64
|
+
const mockCallback = jest.fn();
|
|
65
|
+
const indexWithCallback = new dbPouchClient(baseUrl, mockCallback);
|
|
66
|
+
|
|
67
|
+
expect(indexWithCallback.socket.connect).toHaveBeenCalled();
|
|
68
|
+
expect(indexWithCallback.socket.onAny).toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Test utility methods
|
|
73
|
+
describe('utility methods', () => {
|
|
74
|
+
it('should set and get token correctly', () => {
|
|
75
|
+
const token = 'test-token';
|
|
76
|
+
index.setToken(token);
|
|
77
|
+
expect(index.getToken()).toBe(token);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should return null when token is not set', () => {
|
|
81
|
+
expect(index.getToken()).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should return the correct version', () => {
|
|
85
|
+
// Mock package.json
|
|
86
|
+
jest.mock('../../package.json', () => ({ version: packetJson.version }), { virtual: true });
|
|
87
|
+
|
|
88
|
+
// We need to re-create the index instance to pick up the mocked package.json
|
|
89
|
+
const newIndex = new dbPouchClient(baseUrl);
|
|
90
|
+
expect(newIndex.getVersion()).toBe(packetJson.version);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Test user administration endpoints
|
|
95
|
+
describe('user administration endpoints', () => {
|
|
96
|
+
it('should login successfully and store token', async () => {
|
|
97
|
+
const credentials: I_UserLogin = { name: 'testuser', password: 'password' };
|
|
98
|
+
const mockResponse = { token: 'test-token', isAdmin: true };
|
|
99
|
+
|
|
100
|
+
mockFetch.mockImplementationOnce(() =>
|
|
101
|
+
Promise.resolve({
|
|
102
|
+
ok: true,
|
|
103
|
+
status: 200,
|
|
104
|
+
statusText: 'OK',
|
|
105
|
+
json: () => Promise.resolve(mockResponse),
|
|
106
|
+
})
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const result = await index.login(credentials);
|
|
110
|
+
|
|
111
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
112
|
+
`${baseUrl}/users/login`,
|
|
113
|
+
expect.objectContaining({
|
|
114
|
+
method: 'POST',
|
|
115
|
+
body: JSON.stringify(credentials),
|
|
116
|
+
})
|
|
117
|
+
);
|
|
118
|
+
expect(result).toEqual(mockResponse);
|
|
119
|
+
expect(index.getToken()).toBe('test-token');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should return null when login fails', async () => {
|
|
123
|
+
const credentials: I_UserLogin = { name: 'testuser', password: 'wrong-password' };
|
|
124
|
+
|
|
125
|
+
mockFetch.mockImplementationOnce(() =>
|
|
126
|
+
Promise.resolve({
|
|
127
|
+
ok: true,
|
|
128
|
+
status: 200,
|
|
129
|
+
statusText: 'OK',
|
|
130
|
+
json: () => Promise.resolve({}),
|
|
131
|
+
})
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const result = await index.login(credentials);
|
|
135
|
+
|
|
136
|
+
expect(result).toBeNull();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should list users', async () => {
|
|
140
|
+
const mockUsers = [{ _id: '1', name: 'user1', password: 'pass1', department: 'dept1', group: 'group1', isAdmin: false }];
|
|
141
|
+
|
|
142
|
+
mockFetch.mockImplementationOnce(() =>
|
|
143
|
+
Promise.resolve({
|
|
144
|
+
ok: true,
|
|
145
|
+
status: 200,
|
|
146
|
+
statusText: 'OK',
|
|
147
|
+
json: () => Promise.resolve(mockUsers),
|
|
148
|
+
})
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const result = await index.listUsers();
|
|
152
|
+
|
|
153
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
154
|
+
`${baseUrl}/users/list`,
|
|
155
|
+
expect.objectContaining({
|
|
156
|
+
method: 'GET',
|
|
157
|
+
})
|
|
158
|
+
);
|
|
159
|
+
expect(result).toEqual(mockUsers);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should update a user', async () => {
|
|
163
|
+
const userId = '1';
|
|
164
|
+
const userData: I_UserUpdate = { name: 'updated-user', department: 'new-dept' };
|
|
165
|
+
|
|
166
|
+
await index.updateUser(userId, userData);
|
|
167
|
+
|
|
168
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
169
|
+
`${baseUrl}/users/update/${userId}`,
|
|
170
|
+
expect.objectContaining({
|
|
171
|
+
method: 'PATCH',
|
|
172
|
+
body: JSON.stringify(userData),
|
|
173
|
+
})
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should create a user', async () => {
|
|
178
|
+
const userData: I_UserCreation = {
|
|
179
|
+
name: 'newuser',
|
|
180
|
+
password: 'password',
|
|
181
|
+
department: 'dept',
|
|
182
|
+
group: 'group',
|
|
183
|
+
isAdmin: false
|
|
184
|
+
};
|
|
185
|
+
const mockResponse = { _id: '2', username: 'newuser', department: 'dept', group: 'group' };
|
|
186
|
+
|
|
187
|
+
mockFetch.mockImplementationOnce(() =>
|
|
188
|
+
Promise.resolve({
|
|
189
|
+
ok: true,
|
|
190
|
+
status: 200,
|
|
191
|
+
statusText: 'OK',
|
|
192
|
+
json: () => Promise.resolve(mockResponse),
|
|
193
|
+
})
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const result = await index.createUser(userData);
|
|
197
|
+
|
|
198
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
199
|
+
`${baseUrl}/users/create`,
|
|
200
|
+
expect.objectContaining({
|
|
201
|
+
method: 'POST',
|
|
202
|
+
body: JSON.stringify(userData),
|
|
203
|
+
})
|
|
204
|
+
);
|
|
205
|
+
expect(result).toEqual(mockResponse);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should remove a user', async () => {
|
|
209
|
+
const userId = '1';
|
|
210
|
+
|
|
211
|
+
await index.removeUser(userId);
|
|
212
|
+
|
|
213
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
214
|
+
`${baseUrl}/users/remove/${userId}`,
|
|
215
|
+
expect.objectContaining({
|
|
216
|
+
method: 'DELETE',
|
|
217
|
+
})
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Test document management endpoints
|
|
223
|
+
describe('document management endpoints', () => {
|
|
224
|
+
it('should create a document', async () => {
|
|
225
|
+
const document: I_DocumentEntry = {
|
|
226
|
+
_id: '1',
|
|
227
|
+
title: 'Test Document',
|
|
228
|
+
type: 1,
|
|
229
|
+
subType: 2,
|
|
230
|
+
content: { data: 'test content' },
|
|
231
|
+
owner: 'user1'
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
mockFetch.mockImplementationOnce(() =>
|
|
235
|
+
Promise.resolve({
|
|
236
|
+
ok: true,
|
|
237
|
+
status: 200,
|
|
238
|
+
statusText: 'OK',
|
|
239
|
+
json: () => Promise.resolve(document),
|
|
240
|
+
})
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const result = await index.createDocument(document);
|
|
244
|
+
|
|
245
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
246
|
+
`${baseUrl}/docs/create`,
|
|
247
|
+
expect.objectContaining({
|
|
248
|
+
method: 'POST',
|
|
249
|
+
body: JSON.stringify(document),
|
|
250
|
+
})
|
|
251
|
+
);
|
|
252
|
+
expect(result).toEqual(document);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should list documents', async () => {
|
|
256
|
+
const mockDocuments = [
|
|
257
|
+
{ _id: '1', title: 'Doc1', type: 1, subType: 1, content: {}, owner: 'user1' },
|
|
258
|
+
{ _id: '2', title: 'Doc2', type: 2, subType: 2, content: {}, owner: 'user2' }
|
|
259
|
+
];
|
|
260
|
+
|
|
261
|
+
mockFetch.mockImplementationOnce(() =>
|
|
262
|
+
Promise.resolve({
|
|
263
|
+
ok: true,
|
|
264
|
+
status: 200,
|
|
265
|
+
statusText: 'OK',
|
|
266
|
+
json: () => Promise.resolve(mockDocuments),
|
|
267
|
+
})
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const result = await index.listDocuments();
|
|
271
|
+
|
|
272
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
273
|
+
`${baseUrl}/docs/list`,
|
|
274
|
+
expect.objectContaining({
|
|
275
|
+
method: 'GET',
|
|
276
|
+
})
|
|
277
|
+
);
|
|
278
|
+
expect(result).toEqual(mockDocuments);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should fetch documents based on query', async () => {
|
|
282
|
+
const query: I_DocumentQuery = { type: 1, owner: 'user1' };
|
|
283
|
+
const mockDocuments = [
|
|
284
|
+
{ _id: '1', title: 'Doc1', type: 1, subType: 1, content: {}, owner: 'user1' }
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
mockFetch.mockImplementationOnce(() =>
|
|
288
|
+
Promise.resolve({
|
|
289
|
+
ok: true,
|
|
290
|
+
status: 200,
|
|
291
|
+
statusText: 'OK',
|
|
292
|
+
json: () => Promise.resolve(mockDocuments),
|
|
293
|
+
})
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
const result = await index.fetchDocument(query);
|
|
297
|
+
|
|
298
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
299
|
+
`${baseUrl}/docs/fetch/`,
|
|
300
|
+
expect.objectContaining({
|
|
301
|
+
method: 'POST',
|
|
302
|
+
body: JSON.stringify(query),
|
|
303
|
+
})
|
|
304
|
+
);
|
|
305
|
+
expect(result).toEqual(mockDocuments);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('should update a document', async () => {
|
|
309
|
+
const documentId = '1';
|
|
310
|
+
const documentData: I_DocumentEntry = {
|
|
311
|
+
_id: '1',
|
|
312
|
+
title: 'Updated Document',
|
|
313
|
+
type: 1,
|
|
314
|
+
subType: 2,
|
|
315
|
+
content: { data: 'updated content' },
|
|
316
|
+
owner: 'user1'
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
await index.updateDocument(documentId, documentData);
|
|
320
|
+
|
|
321
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
322
|
+
`${baseUrl}/docs/update/${documentId}`,
|
|
323
|
+
expect.objectContaining({
|
|
324
|
+
method: 'PATCH',
|
|
325
|
+
body: JSON.stringify(documentData),
|
|
326
|
+
})
|
|
327
|
+
);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('should remove a document', async () => {
|
|
331
|
+
const documentId = '1';
|
|
332
|
+
|
|
333
|
+
await index.removeDocument(documentId);
|
|
334
|
+
|
|
335
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
336
|
+
`${baseUrl}/docs/remove/${documentId}`,
|
|
337
|
+
expect.objectContaining({
|
|
338
|
+
method: 'DELETE',
|
|
339
|
+
})
|
|
340
|
+
);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Test data structure endpoints
|
|
345
|
+
describe('data structure endpoints', () => {
|
|
346
|
+
it('should create a structure', async () => {
|
|
347
|
+
const structure: I_StructureCreation = {
|
|
348
|
+
name: 'Test Structure',
|
|
349
|
+
description: 'Test Description',
|
|
350
|
+
fields: [{ name: 'field1', type: 'string' }]
|
|
351
|
+
};
|
|
352
|
+
const mockResponse: I_DataStructure = {
|
|
353
|
+
_id: '1',
|
|
354
|
+
name: 'Test Structure',
|
|
355
|
+
description: 'Test Description',
|
|
356
|
+
fields: [{ name: 'field1', type: 'string' }]
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
mockFetch.mockImplementationOnce(() =>
|
|
360
|
+
Promise.resolve({
|
|
361
|
+
ok: true,
|
|
362
|
+
status: 200,
|
|
363
|
+
statusText: 'OK',
|
|
364
|
+
json: () => Promise.resolve(mockResponse),
|
|
365
|
+
})
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
const result = await index.createStructure(structure);
|
|
369
|
+
|
|
370
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
371
|
+
`${baseUrl}/structures/create`,
|
|
372
|
+
expect.objectContaining({
|
|
373
|
+
method: 'POST',
|
|
374
|
+
body: JSON.stringify(structure),
|
|
375
|
+
})
|
|
376
|
+
);
|
|
377
|
+
expect(result).toEqual(mockResponse);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('should get structures', async () => {
|
|
381
|
+
const mockStructures = [
|
|
382
|
+
{ _id: '1', name: 'Structure1', description: 'Desc1', fields: [] },
|
|
383
|
+
{ _id: '2', name: 'Structure2', description: 'Desc2', fields: [] }
|
|
384
|
+
];
|
|
385
|
+
|
|
386
|
+
mockFetch.mockImplementationOnce(() =>
|
|
387
|
+
Promise.resolve({
|
|
388
|
+
ok: true,
|
|
389
|
+
status: 200,
|
|
390
|
+
statusText: 'OK',
|
|
391
|
+
json: () => Promise.resolve(mockStructures),
|
|
392
|
+
})
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
const result = await index.getStructures();
|
|
396
|
+
|
|
397
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
398
|
+
`${baseUrl}/structures/list`,
|
|
399
|
+
expect.objectContaining({
|
|
400
|
+
method: 'GET',
|
|
401
|
+
})
|
|
402
|
+
);
|
|
403
|
+
expect(result).toEqual(mockStructures);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('should update a structure', async () => {
|
|
407
|
+
const structureId = '1';
|
|
408
|
+
const structureData: I_DataStructure = {
|
|
409
|
+
name: 'Updated Structure',
|
|
410
|
+
description: 'Updated Description',
|
|
411
|
+
fields: [{ name: 'updatedField', type: 'number' }]
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
await index.updateStructure(structureId, structureData);
|
|
415
|
+
|
|
416
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
417
|
+
`${baseUrl}/structures/update/${structureId}`,
|
|
418
|
+
expect.objectContaining({
|
|
419
|
+
method: 'PATCH',
|
|
420
|
+
body: JSON.stringify(structureData),
|
|
421
|
+
})
|
|
422
|
+
);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('should remove a structure', async () => {
|
|
426
|
+
const structureId = '1';
|
|
427
|
+
|
|
428
|
+
await index.removeStructure(structureId);
|
|
429
|
+
|
|
430
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
431
|
+
`${baseUrl}/structures/remove/${structureId}`,
|
|
432
|
+
expect.objectContaining({
|
|
433
|
+
method: 'DELETE',
|
|
434
|
+
})
|
|
435
|
+
);
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// Test error handling
|
|
440
|
+
describe('error handling', () => {
|
|
441
|
+
it('should throw an error when API request fails', async () => {
|
|
442
|
+
mockFetch.mockImplementationOnce(() =>
|
|
443
|
+
Promise.resolve({
|
|
444
|
+
ok: false,
|
|
445
|
+
status: 500,
|
|
446
|
+
statusText: 'Internal Server Error',
|
|
447
|
+
})
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
await expect(index.listUsers()).rejects.toThrow('API error: 500 Internal Server Error');
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('should clear token when receiving 401 or 403 status', async () => {
|
|
454
|
+
// Set a token first
|
|
455
|
+
index.setToken('test-token');
|
|
456
|
+
|
|
457
|
+
// Mock 401 response
|
|
458
|
+
mockFetch.mockImplementationOnce(() =>
|
|
459
|
+
Promise.resolve({
|
|
460
|
+
ok: false,
|
|
461
|
+
status: 401,
|
|
462
|
+
statusText: 'Unauthorized',
|
|
463
|
+
})
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
await expect(index.listUsers()).rejects.toThrow('API error: 401 Unauthorized');
|
|
467
|
+
expect(index.getToken()).toBeNull();
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// Test WebSocket functionality
|
|
472
|
+
describe('WebSocket functionality', () => {
|
|
473
|
+
it('should connect socket when callback is provided', () => {
|
|
474
|
+
const mockCallback = jest.fn();
|
|
475
|
+
const indexWithCallback = new dbPouchClient(baseUrl, mockCallback);
|
|
476
|
+
|
|
477
|
+
// Verify that connect was called
|
|
478
|
+
expect(indexWithCallback.socket.connect).toHaveBeenCalled();
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('should set up event handler when callback is provided', () => {
|
|
482
|
+
const mockCallback = jest.fn();
|
|
483
|
+
const indexWithCallback = new dbPouchClient(baseUrl, mockCallback);
|
|
484
|
+
|
|
485
|
+
// Verify that onAny was called
|
|
486
|
+
expect(indexWithCallback.socket.onAny).toHaveBeenCalled();
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -6,42 +6,179 @@ import type {
|
|
|
6
6
|
I_UserDisplay,
|
|
7
7
|
I_DocumentEntry,
|
|
8
8
|
I_DataStructure,
|
|
9
|
-
I_LoginResponse, I_DocumentQuery, I_StructureCreation, I_WsMessage, I_EventString
|
|
9
|
+
I_LoginResponse, I_DocumentQuery, I_StructureCreation, I_WsMessage, I_EventString, I_DocumentType
|
|
10
10
|
} from "./types.js";
|
|
11
11
|
import {io, Socket} from "socket.io-client";
|
|
12
12
|
import packetJson from '../package.json'
|
|
13
13
|
|
|
14
|
-
export default class
|
|
14
|
+
export default class dbPouchClient {
|
|
15
15
|
baseUrl: string;
|
|
16
|
-
private
|
|
17
|
-
socket: Socket
|
|
16
|
+
private authToken: string | null = null;
|
|
17
|
+
socket: Socket;
|
|
18
|
+
callbackFunction;
|
|
19
|
+
realTimeSync: boolean = false;
|
|
20
|
+
private connectionInProgress = false;
|
|
18
21
|
|
|
19
|
-
constructor(baseUrl: string, callback?: (event: I_EventString, data: I_WsMessage) => void) {
|
|
20
|
-
this.baseUrl = baseUrl;
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
constructor(host: string, port: number = 80, callback?: (event: I_EventString, data: I_WsMessage) => void) {
|
|
24
|
+
this.baseUrl = host;
|
|
25
|
+
|
|
26
|
+
const socketUrl = host.includes('://') ? host : `http://${host}`;
|
|
27
|
+
|
|
28
|
+
// Don't append port twice if it's already in the URL
|
|
29
|
+
const socketUrlWithPort = socketUrl.includes(':') && !socketUrl.endsWith(':')
|
|
30
|
+
? socketUrl
|
|
31
|
+
: `${socketUrl}:${port}`;
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
console.log(`Initializing Socket.IO with URL: ${socketUrlWithPort}, path: /socket.io`);
|
|
35
|
+
|
|
36
|
+
this.socket = io(`${socketUrlWithPort}`, {
|
|
37
|
+
autoConnect: false,
|
|
38
|
+
transports: ['websocket'], // Try websocket only first
|
|
39
|
+
reconnection: true,
|
|
40
|
+
reconnectionAttempts: 5,
|
|
41
|
+
reconnectionDelay: 1000,
|
|
42
|
+
forceNew: true, // Force a new connection
|
|
43
|
+
auth: {
|
|
44
|
+
token: null // Will be set later
|
|
45
|
+
},
|
|
46
|
+
path: '/socket.io'
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
this.callbackFunction = callback;
|
|
50
|
+
|
|
51
|
+
this.setupPermanentSocketListeners();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private setupPermanentSocketListeners() {
|
|
55
|
+
// These are permanent listeners that won't be removed
|
|
56
|
+
this.socket.on('connect_error', (error) => {
|
|
57
|
+
console.error('Socket connection error:', error.message);
|
|
58
|
+
this.connectionInProgress = false;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
this.socket.on('connect', () => {
|
|
62
|
+
console.log('Socket connected successfully with ID:', this.socket.id);
|
|
63
|
+
this.connectionInProgress = false;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
this.socket.on('disconnect', (reason) => {
|
|
67
|
+
console.log('Socket disconnected. Reason:', reason);
|
|
68
|
+
this.connectionInProgress = false;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
this.socket.on('error', (error) => {
|
|
72
|
+
console.error('Socket error:', error);
|
|
73
|
+
this.connectionInProgress = false;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
setRealTimeSync = (newRealTimeSync: boolean) => {
|
|
78
|
+
console.log(`Setting realtime sync to: ${newRealTimeSync}. Current setting: ${this.realTimeSync}`);
|
|
79
|
+
|
|
80
|
+
// Skip if the setting isn't changing
|
|
81
|
+
if (newRealTimeSync === this.realTimeSync) {
|
|
82
|
+
console.log("Realtime sync setting unchanged, skipping");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.realTimeSync = newRealTimeSync;
|
|
87
|
+
|
|
88
|
+
if (newRealTimeSync && this.authToken) {
|
|
89
|
+
console.log("Activating realtime updates");
|
|
90
|
+
|
|
91
|
+
// Ensure we're not in the middle of another connection attempt
|
|
92
|
+
if (this.connectionInProgress) {
|
|
93
|
+
console.log("Connection already in progress, waiting before initializing");
|
|
94
|
+
setTimeout(() => this.initWebSocket(), 500);
|
|
95
|
+
} else {
|
|
96
|
+
this.initWebSocket();
|
|
97
|
+
}
|
|
98
|
+
} else if (!newRealTimeSync) {
|
|
99
|
+
console.log("Deactivating realtime updates");
|
|
100
|
+
if (this.socket.connected) {
|
|
101
|
+
console.log("Disconnecting socket");
|
|
102
|
+
this.socket.disconnect();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private initWebSocket() {
|
|
108
|
+
console.log("initWebSocket called. Auth token present:", !!this.authToken,
|
|
109
|
+
"Connection in progress:", this.connectionInProgress,
|
|
110
|
+
"Socket connected:", this.socket.connected);
|
|
111
|
+
|
|
112
|
+
if (!this.authToken) {
|
|
113
|
+
console.log("Skipping WebSocket initialization: No auth token");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (this.connectionInProgress) {
|
|
118
|
+
console.log("Connection already in progress, skipping initialization");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (this.socket.connected) {
|
|
123
|
+
console.log("Socket already connected with ID:", this.socket.id);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this.connectionInProgress = true;
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
console.log("Setting up WebSocket connection with token");
|
|
131
|
+
|
|
132
|
+
// Update the auth token
|
|
133
|
+
this.socket.auth = { token: this.authToken };
|
|
134
|
+
|
|
135
|
+
// Remove any dynamic event listeners that might have been added
|
|
136
|
+
this.socket.offAny();
|
|
137
|
+
|
|
138
|
+
// Set up event handler for application events
|
|
25
139
|
this.socket.onAny((event: I_EventString, data: I_WsMessage) => {
|
|
26
140
|
if (event === "heartbeatPing") {
|
|
27
141
|
console.log("Ping event received:", data);
|
|
28
142
|
this.socket.emit("heartbeatPong", Date.now());
|
|
29
143
|
}
|
|
30
|
-
else {
|
|
31
|
-
|
|
144
|
+
else if (this.callbackFunction){
|
|
145
|
+
this.callbackFunction(event, data);
|
|
32
146
|
}
|
|
33
147
|
});
|
|
148
|
+
|
|
149
|
+
// Connect to the server
|
|
150
|
+
console.log("Connecting socket with auth token");
|
|
151
|
+
this.socket.connect();
|
|
152
|
+
|
|
153
|
+
// Add a timeout to detect if connection is taking too long
|
|
154
|
+
setTimeout(() => {
|
|
155
|
+
if (this.connectionInProgress) {
|
|
156
|
+
console.warn("Socket connection attempt timed out after 5 seconds");
|
|
157
|
+
this.connectionInProgress = false;
|
|
158
|
+
|
|
159
|
+
// If we're still not connected after the timeout, try again with polling
|
|
160
|
+
if (!this.socket.connected) {
|
|
161
|
+
console.log("Retrying connection with polling transport");
|
|
162
|
+
this.socket.io.opts.transports = ['polling', 'websocket'];
|
|
163
|
+
this.socket.connect();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}, 5000);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error('Error in initWebSocket:', error);
|
|
169
|
+
this.connectionInProgress = false;
|
|
34
170
|
}
|
|
35
171
|
}
|
|
36
172
|
|
|
37
173
|
private async request<T>(endpoint: string, method: string, body?: any, requiresAuth: boolean = true): Promise<T> {
|
|
38
174
|
const headers: HeadersInit = {
|
|
39
|
-
'Content-Type': 'application/json'
|
|
175
|
+
'Content-Type': 'application/json',
|
|
40
176
|
};
|
|
41
177
|
|
|
42
|
-
if (requiresAuth && this.
|
|
43
|
-
headers['Authorization'] = `Bearer ${this.
|
|
44
|
-
|
|
178
|
+
if (requiresAuth && this.authToken)
|
|
179
|
+
headers['Authorization'] = `Bearer ${this.authToken}`;
|
|
180
|
+
if (this.socket.id)
|
|
181
|
+
headers['X-Socket-ID'] = this.socket.id;
|
|
45
182
|
|
|
46
183
|
const options: RequestInit = {
|
|
47
184
|
method,
|
|
@@ -49,11 +186,19 @@ export default class Index {
|
|
|
49
186
|
body: body ? JSON.stringify(body) : undefined
|
|
50
187
|
};
|
|
51
188
|
|
|
52
|
-
|
|
189
|
+
// Ensure endpoint starts with /
|
|
190
|
+
const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
|
191
|
+
// Ensure baseUrl doesn't end with /
|
|
192
|
+
const normalizedBaseUrl = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl;
|
|
193
|
+
|
|
194
|
+
const url = `${normalizedBaseUrl}${normalizedEndpoint}`;
|
|
195
|
+
console.log(`Making ${method} request to: ${url}`);
|
|
196
|
+
|
|
197
|
+
const response = await fetch(url, options);
|
|
53
198
|
|
|
54
199
|
if (!response.ok) {
|
|
55
200
|
if (response.status === 401 || response.status === 403) {
|
|
56
|
-
this.
|
|
201
|
+
this.authToken = null;
|
|
57
202
|
}
|
|
58
203
|
throw new Error(`API error: ${response.status} ${response.statusText}`);
|
|
59
204
|
}
|
|
@@ -65,7 +210,13 @@ export default class Index {
|
|
|
65
210
|
async login(credentials: I_UserLogin): Promise<I_LoginResponse | null> {
|
|
66
211
|
const response = await this.request<I_LoginResponse>('/users/login', 'POST', credentials, false);
|
|
67
212
|
if (response.token) {
|
|
68
|
-
this.
|
|
213
|
+
this.authToken = response.token;
|
|
214
|
+
|
|
215
|
+
// Reconnect websocket with new token if realtime sync is enabled
|
|
216
|
+
if (this.realTimeSync) {
|
|
217
|
+
this.initWebSocket();
|
|
218
|
+
}
|
|
219
|
+
|
|
69
220
|
return {token: response.token, isAdmin: response.isAdmin};
|
|
70
221
|
}
|
|
71
222
|
return null;
|
|
@@ -125,17 +276,72 @@ export default class Index {
|
|
|
125
276
|
await this.request<void>(`/structures/remove/${structureID}`, 'DELETE');
|
|
126
277
|
}
|
|
127
278
|
|
|
279
|
+
// Data Type Endpoints
|
|
280
|
+
async createType(type: I_DocumentType): Promise<I_DocumentType> {
|
|
281
|
+
return await this.request<I_DocumentType>('/types/write', 'POST', type);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async removeType(typeID: string) {
|
|
285
|
+
return await this.request<void>(`/types/remove/${typeID}`, 'DELETE');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async getTypes(): Promise<I_DocumentType[]> {
|
|
289
|
+
return await this.request<I_DocumentType[]>('/types/list', 'GET');
|
|
290
|
+
}
|
|
291
|
+
|
|
128
292
|
setToken(token: string | null): void {
|
|
129
|
-
|
|
293
|
+
console.log("Setting token to:", token ? "***token***" : "null");
|
|
294
|
+
|
|
295
|
+
const tokenChanged = this.authToken !== token;
|
|
296
|
+
this.authToken = token;
|
|
297
|
+
|
|
298
|
+
if (!tokenChanged) {
|
|
299
|
+
console.log("Token unchanged, no need to reconnect");
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// If we have a new token and realtime sync is enabled
|
|
304
|
+
if (token && this.realTimeSync) {
|
|
305
|
+
console.log("New token set, will initialize WebSocket");
|
|
306
|
+
|
|
307
|
+
// Ensure any existing connection is closed first
|
|
308
|
+
if (this.socket.connected) {
|
|
309
|
+
console.log("Disconnecting existing socket before reconnecting with new token");
|
|
310
|
+
this.socket.disconnect();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Wait a moment for the disconnect to complete
|
|
314
|
+
setTimeout(() => {
|
|
315
|
+
console.log("Initializing WebSocket with new token");
|
|
316
|
+
this.initWebSocket();
|
|
317
|
+
}, 300);
|
|
318
|
+
}
|
|
319
|
+
// If token was cleared or realtime sync is disabled
|
|
320
|
+
else if (this.socket.connected) {
|
|
321
|
+
console.log("Token cleared or realtime sync disabled, disconnecting");
|
|
322
|
+
this.socket.disconnect();
|
|
323
|
+
}
|
|
130
324
|
}
|
|
131
325
|
|
|
132
|
-
|
|
326
|
+
getVersion() {
|
|
133
327
|
return packetJson.version;
|
|
134
328
|
}
|
|
135
329
|
|
|
136
|
-
|
|
137
|
-
|
|
330
|
+
debugSocketConnection(): void {
|
|
331
|
+
console.log("Socket connection debug info:");
|
|
332
|
+
console.log("- Connected:", this.socket.connected);
|
|
333
|
+
console.log("- Socket ID:", this.socket.id);
|
|
334
|
+
console.log("- Auth token present:", !!this.authToken);
|
|
335
|
+
console.log("- Connection in progress:", this.connectionInProgress);
|
|
336
|
+
console.log("- Realtime sync enabled:", this.realTimeSync);
|
|
337
|
+
console.log("- Socket options:", this.socket.io.opts);
|
|
338
|
+
|
|
339
|
+
// Try to force reconnection
|
|
340
|
+
if (!this.socket.connected && this.authToken && this.realTimeSync) {
|
|
341
|
+
console.log("Attempting to force reconnection...");
|
|
342
|
+
this.socket.auth = { token: this.authToken };
|
|
343
|
+
this.socket.connect();
|
|
344
|
+
}
|
|
138
345
|
}
|
|
139
|
-
}
|
|
140
346
|
|
|
141
|
-
|
|
347
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -104,10 +104,20 @@ export interface I_StructureUpdate {
|
|
|
104
104
|
fields?: any[];
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
// Document type related types
|
|
108
|
+
export interface I_DocumentType {
|
|
109
|
+
_id?: string;
|
|
110
|
+
type: number;
|
|
111
|
+
subType: number;
|
|
112
|
+
name: string;
|
|
113
|
+
description?: string;
|
|
114
|
+
defaultStructureID?: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
107
117
|
// WebSocket-related types
|
|
108
118
|
export type I_EventString = 'subscribe' | 'unsubscribe' | 'heartbeatPong' | "heartbeatPing" | "newDocument" | "newStructure" |
|
|
109
119
|
"newUser" | "removedID" | "changedDocument" | "changedStructure" | "changedUser" | "confirmSubscription" |
|
|
110
|
-
"confirmUnsubscription";
|
|
120
|
+
"confirmUnsubscription" | "removedUser" | "removedStructure" | "removedDocument";
|
|
111
121
|
|
|
112
122
|
export interface I_WsMessage {
|
|
113
123
|
newDocument?: I_DocumentEntry;
|
package/tsconfig.json
CHANGED
|
@@ -12,8 +12,10 @@
|
|
|
12
12
|
"rootDir": "./src",
|
|
13
13
|
"declaration": true,
|
|
14
14
|
"emitDeclarationOnly": false,
|
|
15
|
-
"resolveJsonModule": true
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"isolatedModules": true,
|
|
17
|
+
"allowSyntheticDefaultImports": true
|
|
16
18
|
},
|
|
17
19
|
"include": ["**/*.ts", "**/*.tsx"],
|
|
18
20
|
"exclude": ["node_modules", "**/*.test.ts", "**/*.types.ts", "./dist/**/*"]
|
|
19
|
-
}
|
|
21
|
+
}
|