@webiny/api-websockets-sql 0.0.0-unstable.0156a4c312
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +11 -0
- package/TableName/TableName.d.ts +6 -0
- package/TableName/TableName.js +12 -0
- package/TableName/TableName.js.map +1 -0
- package/TableName/abstractions.d.ts +7 -0
- package/TableName/abstractions.js +5 -0
- package/TableName/abstractions.js.map +1 -0
- package/WebsocketsConnectionRegistry.d.ts +27 -0
- package/WebsocketsConnectionRegistry.js +235 -0
- package/WebsocketsConnectionRegistry.js.map +1 -0
- package/WebsocketsSqlFeature.d.ts +8 -0
- package/WebsocketsSqlFeature.js +14 -0
- package/WebsocketsSqlFeature.js.map +1 -0
- package/index.d.ts +3 -0
- package/index.js +2 -0
- package/package.json +44 -0
- package/toIsoString.d.ts +14 -0
- package/toIsoString.js +15 -0
- package/toIsoString.js.map +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Webiny
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# @webiny/api-websockets-sql
|
|
2
|
+
|
|
3
|
+
> [!NOTE]
|
|
4
|
+
> This package is part of the [Webiny](https://www.webiny.com) monorepo.
|
|
5
|
+
> It’s **included in every Webiny project by default** and is not meant to be used as a standalone package.
|
|
6
|
+
|
|
7
|
+
📘 **Documentation:** [https://www.webiny.com/docs](https://www.webiny.com/docs)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
_This README file is automatically generated during the publish process._
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TableName/TableName.js","sources":["../../src/TableName/TableName.ts"],"sourcesContent":["import { TableName as TableNameAbstraction } from \"~/TableName/abstractions.js\";\n\nexport class TableName implements TableNameAbstraction.Interface {\n private readonly prefix;\n\n public constructor(prefix?: string) {\n this.prefix = prefix;\n }\n\n public resolve(name: string): string {\n if (!this.prefix) {\n return name;\n }\n return `${this.prefix}_${name}`;\n }\n}\n"],"names":["TableName","prefix","name"],"mappings":"AAEO,MAAMA;IAGT,YAAmBC,MAAe,CAAE;QAChC,IAAI,CAAC,MAAM,GAAGA;IAClB;IAEO,QAAQC,IAAY,EAAU;QACjC,IAAI,CAAC,IAAI,CAAC,MAAM,EACZ,OAAOA;QAEX,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,EAAEA,MAAM;IACnC;AACJ"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TableName/abstractions.js","sources":["../../src/TableName/abstractions.ts"],"sourcesContent":["import { createAbstraction } from \"@webiny/feature/exports/api.js\";\n\nexport interface ITableName {\n resolve(name: string): string;\n}\n\nexport const TableName = createAbstraction<ITableName>(\"Websockets/Sql/TableName\");\n\nexport namespace TableName {\n export type Interface = ITableName;\n}\n"],"names":["TableName","createAbstraction"],"mappings":";AAMO,MAAMA,YAAYC,kBAA8B"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { KnexClient } from "@webiny/api-core-sql";
|
|
2
|
+
import { ConnectionRegistry } from "@webiny/api-websockets/exports/api.js";
|
|
3
|
+
import { TableName } from "./TableName/abstractions.js";
|
|
4
|
+
declare class WebsocketsConnectionRegistryImpl implements ConnectionRegistry.Interface {
|
|
5
|
+
private readonly knex;
|
|
6
|
+
private readonly tableName;
|
|
7
|
+
constructor(knex: KnexClient.Interface, tableName: TableName.Interface);
|
|
8
|
+
register(event: ConnectionRegistry.RegisterParams): Promise<ConnectionRegistry.Data>;
|
|
9
|
+
unregister(event: ConnectionRegistry.UnregisterParams): Promise<void>;
|
|
10
|
+
listViaConnections(connections: string[]): Promise<ConnectionRegistry.Data[]>;
|
|
11
|
+
listViaIdentity(identity: string): Promise<ConnectionRegistry.Data[]>;
|
|
12
|
+
listViaTenant(tenant: string): Promise<ConnectionRegistry.Data[]>;
|
|
13
|
+
listAll(): Promise<ConnectionRegistry.Data[]>;
|
|
14
|
+
updateLastSeen(connectionId: string): Promise<void>;
|
|
15
|
+
listStale(olderThan: Date): Promise<ConnectionRegistry.Data[]>;
|
|
16
|
+
private prepareTable;
|
|
17
|
+
private ensureTable;
|
|
18
|
+
private createTable;
|
|
19
|
+
private migrateTable;
|
|
20
|
+
private migrateLastSeen;
|
|
21
|
+
private migrateOwner;
|
|
22
|
+
private toData;
|
|
23
|
+
}
|
|
24
|
+
export declare const WebsocketsConnectionRegistry: typeof WebsocketsConnectionRegistryImpl & {
|
|
25
|
+
__abstraction: import("@webiny/di").Abstraction<import("@webiny/api-websockets/features/ConnectionRegistry/abstractions").IWebsocketsConnectionRegistry>;
|
|
26
|
+
};
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { WebinyError } from "@webiny/error";
|
|
2
|
+
import { KnexClient } from "@webiny/api-core-sql";
|
|
3
|
+
import { ConnectionRegistry } from "@webiny/api-websockets/exports/api.js";
|
|
4
|
+
import { TableName } from "./TableName/abstractions.js";
|
|
5
|
+
import { toIsoString } from "./toIsoString.js";
|
|
6
|
+
const BASE_TABLE_NAME = "WebsocketsConnections";
|
|
7
|
+
const applyOnce = async (isDone, change)=>{
|
|
8
|
+
const done = await isDone();
|
|
9
|
+
if (done) return;
|
|
10
|
+
try {
|
|
11
|
+
await change();
|
|
12
|
+
} catch (error) {
|
|
13
|
+
const doneByAnotherCall = await isDone();
|
|
14
|
+
if (doneByAnotherCall) return;
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
class WebsocketsConnectionRegistryImpl {
|
|
19
|
+
constructor(knex, tableName){
|
|
20
|
+
this.knex = knex;
|
|
21
|
+
this.tableName = tableName.resolve(BASE_TABLE_NAME);
|
|
22
|
+
}
|
|
23
|
+
async register(event) {
|
|
24
|
+
try {
|
|
25
|
+
await this.prepareTable();
|
|
26
|
+
const row = {
|
|
27
|
+
connectionId: event.connectionId,
|
|
28
|
+
identityId: event.identity.id,
|
|
29
|
+
identityDisplayName: event.identity.displayName,
|
|
30
|
+
identityType: event.identity.type,
|
|
31
|
+
tenant: event.tenant,
|
|
32
|
+
endpoint: event.endpoint,
|
|
33
|
+
connectedOn: event.connectedOn,
|
|
34
|
+
lastSeen: new Date().toISOString(),
|
|
35
|
+
owner: event.owner ?? null
|
|
36
|
+
};
|
|
37
|
+
await this.knex.client(this.tableName).insert(row);
|
|
38
|
+
return this.toData(row);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
throw WebinyError.from(err, {
|
|
41
|
+
message: "Could not register websockets connection.",
|
|
42
|
+
code: "REGISTER_CONNECTION_ERROR",
|
|
43
|
+
data: {
|
|
44
|
+
event
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async unregister(event) {
|
|
50
|
+
try {
|
|
51
|
+
await this.prepareTable();
|
|
52
|
+
const existing = await this.knex.client(this.tableName).where("connectionId", event.connectionId).first();
|
|
53
|
+
if (!existing) throw new WebinyError(`Connection "${event.connectionId}" does not exist.`, "CONNECTION_NOT_FOUND", {
|
|
54
|
+
connectionId: event.connectionId
|
|
55
|
+
});
|
|
56
|
+
await this.knex.client(this.tableName).where("connectionId", event.connectionId).delete();
|
|
57
|
+
} catch (err) {
|
|
58
|
+
throw WebinyError.from(err, {
|
|
59
|
+
message: "Could not unregister websockets connection.",
|
|
60
|
+
code: "UNREGISTER_CONNECTION_ERROR",
|
|
61
|
+
data: {
|
|
62
|
+
event
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async listViaConnections(connections) {
|
|
68
|
+
try {
|
|
69
|
+
await this.prepareTable();
|
|
70
|
+
const rows = await this.knex.client(this.tableName).whereIn("connectionId", connections);
|
|
71
|
+
return rows.map((row)=>this.toData(row));
|
|
72
|
+
} catch (err) {
|
|
73
|
+
throw WebinyError.from(err, {
|
|
74
|
+
message: "Could not list websockets connections by connection IDs.",
|
|
75
|
+
code: "LIST_VIA_CONNECTIONS_ERROR",
|
|
76
|
+
data: {
|
|
77
|
+
connections
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async listViaIdentity(identity) {
|
|
83
|
+
try {
|
|
84
|
+
await this.prepareTable();
|
|
85
|
+
const rows = await this.knex.client(this.tableName).where("identityId", identity);
|
|
86
|
+
return rows.map((row)=>this.toData(row));
|
|
87
|
+
} catch (err) {
|
|
88
|
+
throw WebinyError.from(err, {
|
|
89
|
+
message: "Could not list websockets connections by identity.",
|
|
90
|
+
code: "LIST_VIA_IDENTITY_ERROR",
|
|
91
|
+
data: {
|
|
92
|
+
identity
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async listViaTenant(tenant) {
|
|
98
|
+
try {
|
|
99
|
+
await this.prepareTable();
|
|
100
|
+
const rows = await this.knex.client(this.tableName).where("tenant", tenant);
|
|
101
|
+
return rows.map((row)=>this.toData(row));
|
|
102
|
+
} catch (err) {
|
|
103
|
+
throw WebinyError.from(err, {
|
|
104
|
+
message: "Could not list websockets connections by tenant.",
|
|
105
|
+
code: "LIST_VIA_TENANT_ERROR",
|
|
106
|
+
data: {
|
|
107
|
+
tenant
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async listAll() {
|
|
113
|
+
try {
|
|
114
|
+
await this.prepareTable();
|
|
115
|
+
const rows = await this.knex.client(this.tableName).select("*");
|
|
116
|
+
return rows.map((row)=>this.toData(row));
|
|
117
|
+
} catch (err) {
|
|
118
|
+
throw WebinyError.from(err, {
|
|
119
|
+
message: "Could not list all websockets connections.",
|
|
120
|
+
code: "LIST_ALL_CONNECTIONS_ERROR"
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async updateLastSeen(connectionId) {
|
|
125
|
+
try {
|
|
126
|
+
await this.prepareTable();
|
|
127
|
+
await this.knex.client(this.tableName).where("connectionId", connectionId).update({
|
|
128
|
+
lastSeen: new Date().toISOString()
|
|
129
|
+
});
|
|
130
|
+
} catch (err) {
|
|
131
|
+
throw WebinyError.from(err, {
|
|
132
|
+
message: "Could not update lastSeen for websockets connection.",
|
|
133
|
+
code: "UPDATE_LAST_SEEN_ERROR",
|
|
134
|
+
data: {
|
|
135
|
+
connectionId
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async listStale(olderThan) {
|
|
141
|
+
try {
|
|
142
|
+
await this.prepareTable();
|
|
143
|
+
const threshold = olderThan.toISOString();
|
|
144
|
+
const rows = await this.knex.client(this.tableName).where((qb)=>{
|
|
145
|
+
qb.where("lastSeen", "<", threshold).orWhereNull("lastSeen");
|
|
146
|
+
});
|
|
147
|
+
return rows.map((row)=>this.toData(row));
|
|
148
|
+
} catch (err) {
|
|
149
|
+
throw WebinyError.from(err, {
|
|
150
|
+
message: "Could not list stale websockets connections.",
|
|
151
|
+
code: "LIST_STALE_CONNECTIONS_ERROR",
|
|
152
|
+
data: {
|
|
153
|
+
olderThan
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async prepareTable() {
|
|
159
|
+
await this.ensureTable();
|
|
160
|
+
await this.migrateTable();
|
|
161
|
+
await this.migrateLastSeen();
|
|
162
|
+
await this.migrateOwner();
|
|
163
|
+
}
|
|
164
|
+
async ensureTable() {
|
|
165
|
+
await applyOnce(()=>this.knex.client.schema.hasTable(this.tableName), ()=>this.createTable());
|
|
166
|
+
}
|
|
167
|
+
async createTable() {
|
|
168
|
+
await this.knex.client.schema.createTable(this.tableName, (table)=>{
|
|
169
|
+
table.text("connectionId").notNullable().primary();
|
|
170
|
+
table.text("identityId").notNullable();
|
|
171
|
+
table.text("identityDisplayName").notNullable();
|
|
172
|
+
table.text("identityType").notNullable();
|
|
173
|
+
table.text("tenant").notNullable();
|
|
174
|
+
table.text("endpoint").notNullable();
|
|
175
|
+
table.datetime("connectedOn").notNullable();
|
|
176
|
+
table.datetime("lastSeen").nullable();
|
|
177
|
+
table.text("owner").nullable();
|
|
178
|
+
table.index([
|
|
179
|
+
"identityId"
|
|
180
|
+
]);
|
|
181
|
+
table.index([
|
|
182
|
+
"tenant"
|
|
183
|
+
]);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
async migrateTable() {
|
|
187
|
+
const hasEndpoint = await this.knex.client.schema.hasColumn(this.tableName, "endpoint");
|
|
188
|
+
if (hasEndpoint) return;
|
|
189
|
+
await this.knex.client.schema.alterTable(this.tableName, (table)=>{
|
|
190
|
+
table.text("endpoint").nullable();
|
|
191
|
+
});
|
|
192
|
+
await this.knex.client(this.tableName).update({
|
|
193
|
+
endpoint: this.knex.client.raw("'https://' || \"domainName\" || '/' || \"stage\"")
|
|
194
|
+
});
|
|
195
|
+
await this.knex.client.schema.alterTable(this.tableName, (table)=>{
|
|
196
|
+
table.text("endpoint").notNullable().alter();
|
|
197
|
+
table.dropColumn("domainName");
|
|
198
|
+
table.dropColumn("stage");
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
async migrateLastSeen() {
|
|
202
|
+
await applyOnce(()=>this.knex.client.schema.hasColumn(this.tableName, "lastSeen"), ()=>this.knex.client.schema.alterTable(this.tableName, (table)=>{
|
|
203
|
+
table.datetime("lastSeen").nullable();
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
async migrateOwner() {
|
|
207
|
+
await applyOnce(()=>this.knex.client.schema.hasColumn(this.tableName, "owner"), ()=>this.knex.client.schema.alterTable(this.tableName, (table)=>{
|
|
208
|
+
table.text("owner").nullable();
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
toData(row) {
|
|
212
|
+
return {
|
|
213
|
+
connectionId: row.connectionId,
|
|
214
|
+
identity: {
|
|
215
|
+
id: row.identityId,
|
|
216
|
+
displayName: row.identityDisplayName,
|
|
217
|
+
type: row.identityType
|
|
218
|
+
},
|
|
219
|
+
tenant: row.tenant,
|
|
220
|
+
endpoint: row.endpoint,
|
|
221
|
+
connectedOn: toIsoString(row.connectedOn),
|
|
222
|
+
owner: row.owner ?? void 0
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const WebsocketsConnectionRegistry = ConnectionRegistry.createImplementation({
|
|
227
|
+
implementation: WebsocketsConnectionRegistryImpl,
|
|
228
|
+
dependencies: [
|
|
229
|
+
KnexClient,
|
|
230
|
+
TableName
|
|
231
|
+
]
|
|
232
|
+
});
|
|
233
|
+
export { WebsocketsConnectionRegistry };
|
|
234
|
+
|
|
235
|
+
//# sourceMappingURL=WebsocketsConnectionRegistry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WebsocketsConnectionRegistry.js","sources":["../src/WebsocketsConnectionRegistry.ts"],"sourcesContent":["import { WebinyError } from \"@webiny/error\";\nimport { KnexClient } from \"@webiny/api-core-sql\";\nimport { ConnectionRegistry } from \"@webiny/api-websockets/exports/api.js\";\nimport { TableName } from \"~/TableName/abstractions.js\";\nimport { toIsoString } from \"~/toIsoString.js\";\n\ninterface ConnectionRow {\n connectionId: string;\n identityId: string;\n identityDisplayName: string;\n identityType: string;\n tenant: string;\n endpoint: string;\n connectedOn: string;\n lastSeen: string | null;\n owner: string | null;\n}\n\nconst BASE_TABLE_NAME = \"WebsocketsConnections\";\n\n/*\n * Runs `change` unless `isDone` already holds. After an upgrade every browser reconnects at once, so\n * several calls, in one process or in several, can find the same table or column missing; all but\n * the first then fail because it exists by now. A failure after which `isDone` holds was one of\n * those and is not an error.\n */\nconst applyOnce = async (\n isDone: () => Promise<boolean>,\n change: () => Promise<unknown>\n): Promise<void> => {\n const done = await isDone();\n if (done) {\n return;\n }\n try {\n await change();\n } catch (error) {\n const doneByAnotherCall = await isDone();\n if (doneByAnotherCall) {\n return;\n }\n throw error;\n }\n};\n\nclass WebsocketsConnectionRegistryImpl implements ConnectionRegistry.Interface {\n private readonly knex;\n private readonly tableName;\n\n public constructor(knex: KnexClient.Interface, tableName: TableName.Interface) {\n this.knex = knex;\n this.tableName = tableName.resolve(BASE_TABLE_NAME);\n }\n\n public async register(\n event: ConnectionRegistry.RegisterParams\n ): Promise<ConnectionRegistry.Data> {\n try {\n await this.prepareTable();\n\n const row: ConnectionRow = {\n connectionId: event.connectionId,\n identityId: event.identity.id,\n identityDisplayName: event.identity.displayName,\n identityType: event.identity.type,\n tenant: event.tenant,\n endpoint: event.endpoint,\n connectedOn: event.connectedOn,\n lastSeen: new Date().toISOString(),\n owner: event.owner ?? null\n };\n\n await this.knex.client<ConnectionRow>(this.tableName).insert(row);\n\n return this.toData(row);\n } catch (err) {\n throw WebinyError.from(err, {\n message: \"Could not register websockets connection.\",\n code: \"REGISTER_CONNECTION_ERROR\",\n data: { event }\n });\n }\n }\n\n public async unregister(event: ConnectionRegistry.UnregisterParams): Promise<void> {\n try {\n await this.prepareTable();\n\n const existing = await this.knex\n .client<ConnectionRow>(this.tableName)\n .where(\"connectionId\", event.connectionId)\n .first();\n\n if (!existing) {\n throw new WebinyError(\n `Connection \"${event.connectionId}\" does not exist.`,\n \"CONNECTION_NOT_FOUND\",\n { connectionId: event.connectionId }\n );\n }\n\n await this.knex\n .client<ConnectionRow>(this.tableName)\n .where(\"connectionId\", event.connectionId)\n .delete();\n } catch (err) {\n throw WebinyError.from(err, {\n message: \"Could not unregister websockets connection.\",\n code: \"UNREGISTER_CONNECTION_ERROR\",\n data: { event }\n });\n }\n }\n\n public async listViaConnections(connections: string[]): Promise<ConnectionRegistry.Data[]> {\n try {\n await this.prepareTable();\n\n const rows = await this.knex\n .client<ConnectionRow>(this.tableName)\n .whereIn(\"connectionId\", connections);\n\n return rows.map(row => this.toData(row));\n } catch (err) {\n throw WebinyError.from(err, {\n message: \"Could not list websockets connections by connection IDs.\",\n code: \"LIST_VIA_CONNECTIONS_ERROR\",\n data: { connections }\n });\n }\n }\n\n public async listViaIdentity(identity: string): Promise<ConnectionRegistry.Data[]> {\n try {\n await this.prepareTable();\n\n const rows = await this.knex\n .client<ConnectionRow>(this.tableName)\n .where(\"identityId\", identity);\n\n return rows.map(row => this.toData(row));\n } catch (err) {\n throw WebinyError.from(err, {\n message: \"Could not list websockets connections by identity.\",\n code: \"LIST_VIA_IDENTITY_ERROR\",\n data: { identity }\n });\n }\n }\n\n public async listViaTenant(tenant: string): Promise<ConnectionRegistry.Data[]> {\n try {\n await this.prepareTable();\n\n const rows = await this.knex\n .client<ConnectionRow>(this.tableName)\n .where(\"tenant\", tenant);\n\n return rows.map(row => this.toData(row));\n } catch (err) {\n throw WebinyError.from(err, {\n message: \"Could not list websockets connections by tenant.\",\n code: \"LIST_VIA_TENANT_ERROR\",\n data: { tenant }\n });\n }\n }\n\n public async listAll(): Promise<ConnectionRegistry.Data[]> {\n try {\n await this.prepareTable();\n\n const rows = await this.knex.client<ConnectionRow>(this.tableName).select(\"*\");\n\n return rows.map(row => this.toData(row));\n } catch (err) {\n throw WebinyError.from(err, {\n message: \"Could not list all websockets connections.\",\n code: \"LIST_ALL_CONNECTIONS_ERROR\"\n });\n }\n }\n\n public async updateLastSeen(connectionId: string): Promise<void> {\n try {\n await this.prepareTable();\n\n await this.knex\n .client<ConnectionRow>(this.tableName)\n .where(\"connectionId\", connectionId)\n .update({ lastSeen: new Date().toISOString() });\n } catch (err) {\n throw WebinyError.from(err, {\n message: \"Could not update lastSeen for websockets connection.\",\n code: \"UPDATE_LAST_SEEN_ERROR\",\n data: { connectionId }\n });\n }\n }\n\n public async listStale(olderThan: Date): Promise<ConnectionRegistry.Data[]> {\n try {\n await this.prepareTable();\n\n const threshold = olderThan.toISOString();\n\n const rows = await this.knex.client<ConnectionRow>(this.tableName).where(qb => {\n qb.where(\"lastSeen\", \"<\", threshold).orWhereNull(\"lastSeen\");\n });\n\n return rows.map(row => this.toData(row));\n } catch (err) {\n throw WebinyError.from(err, {\n message: \"Could not list stale websockets connections.\",\n code: \"LIST_STALE_CONNECTIONS_ERROR\",\n data: { olderThan }\n });\n }\n }\n\n private async prepareTable(): Promise<void> {\n await this.ensureTable();\n await this.migrateTable();\n await this.migrateLastSeen();\n await this.migrateOwner();\n }\n\n /*\n * `schema` is read fresh in each call below: it is a new builder on every read, and one builder\n * reused would run the statements queued on it before.\n */\n private async ensureTable(): Promise<void> {\n await applyOnce(\n () => this.knex.client.schema.hasTable(this.tableName),\n () => this.createTable()\n );\n }\n\n private async createTable(): Promise<void> {\n await this.knex.client.schema.createTable(this.tableName, table => {\n table.text(\"connectionId\").notNullable().primary();\n table.text(\"identityId\").notNullable();\n table.text(\"identityDisplayName\").notNullable();\n table.text(\"identityType\").notNullable();\n table.text(\"tenant\").notNullable();\n table.text(\"endpoint\").notNullable();\n table.datetime(\"connectedOn\").notNullable();\n table.datetime(\"lastSeen\").nullable();\n table.text(\"owner\").nullable();\n table.index([\"identityId\"]);\n table.index([\"tenant\"]);\n });\n }\n\n private async migrateTable(): Promise<void> {\n const hasEndpoint = await this.knex.client.schema.hasColumn(this.tableName, \"endpoint\");\n if (hasEndpoint) {\n return;\n }\n\n await this.knex.client.schema.alterTable(this.tableName, table => {\n table.text(\"endpoint\").nullable();\n });\n\n await this.knex.client(this.tableName).update({\n endpoint: this.knex.client.raw(\"'https://' || \\\"domainName\\\" || '/' || \\\"stage\\\"\")\n });\n\n await this.knex.client.schema.alterTable(this.tableName, table => {\n table.text(\"endpoint\").notNullable().alter();\n table.dropColumn(\"domainName\");\n table.dropColumn(\"stage\");\n });\n }\n\n private async migrateLastSeen(): Promise<void> {\n await applyOnce(\n () => this.knex.client.schema.hasColumn(this.tableName, \"lastSeen\"),\n () => {\n return this.knex.client.schema.alterTable(this.tableName, table => {\n table.datetime(\"lastSeen\").nullable();\n });\n }\n );\n }\n\n private async migrateOwner(): Promise<void> {\n await applyOnce(\n () => this.knex.client.schema.hasColumn(this.tableName, \"owner\"),\n () => {\n return this.knex.client.schema.alterTable(this.tableName, table => {\n table.text(\"owner\").nullable();\n });\n }\n );\n }\n\n private toData(row: ConnectionRow): ConnectionRegistry.Data {\n return {\n connectionId: row.connectionId,\n identity: {\n id: row.identityId,\n displayName: row.identityDisplayName,\n type: row.identityType\n },\n tenant: row.tenant,\n endpoint: row.endpoint,\n connectedOn: toIsoString(row.connectedOn),\n owner: row.owner ?? undefined\n };\n }\n}\n\nexport const WebsocketsConnectionRegistry = ConnectionRegistry.createImplementation({\n implementation: WebsocketsConnectionRegistryImpl,\n dependencies: [KnexClient, TableName]\n});\n"],"names":["BASE_TABLE_NAME","applyOnce","isDone","change","done","error","doneByAnotherCall","WebsocketsConnectionRegistryImpl","knex","tableName","event","row","Date","err","WebinyError","existing","connections","rows","identity","tenant","connectionId","olderThan","threshold","qb","table","hasEndpoint","toIsoString","undefined","WebsocketsConnectionRegistry","ConnectionRegistry","KnexClient","TableName"],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Container } from "@webiny/feature/api";
|
|
2
|
+
export interface WebsocketsSqlConfig {
|
|
3
|
+
tableNamePrefix?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare const WebsocketsSqlFeature: {
|
|
6
|
+
name: string;
|
|
7
|
+
register(container: Container, context?: WebsocketsSqlConfig | undefined): void;
|
|
8
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createFeature } from "@webiny/feature/api";
|
|
2
|
+
import { TableName } from "./TableName/abstractions.js";
|
|
3
|
+
import { TableName as TableName_js_TableName } from "./TableName/TableName.js";
|
|
4
|
+
import { WebsocketsConnectionRegistry } from "./WebsocketsConnectionRegistry.js";
|
|
5
|
+
const WebsocketsSqlFeature = createFeature({
|
|
6
|
+
name: "WebsocketsSql",
|
|
7
|
+
register (container, config) {
|
|
8
|
+
container.registerInstance(TableName, new TableName_js_TableName(config?.tableNamePrefix));
|
|
9
|
+
container.register(WebsocketsConnectionRegistry);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
export { WebsocketsSqlFeature };
|
|
13
|
+
|
|
14
|
+
//# sourceMappingURL=WebsocketsSqlFeature.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WebsocketsSqlFeature.js","sources":["../src/WebsocketsSqlFeature.ts"],"sourcesContent":["import { type Container, createFeature } from \"@webiny/feature/api\";\nimport { TableName as TableNameAbstraction } from \"~/TableName/abstractions.js\";\nimport { TableName } from \"~/TableName/TableName.js\";\nimport { WebsocketsConnectionRegistry } from \"./WebsocketsConnectionRegistry.js\";\n\nexport interface WebsocketsSqlConfig {\n tableNamePrefix?: string;\n}\n\nexport const WebsocketsSqlFeature = createFeature<WebsocketsSqlConfig | undefined>({\n name: \"WebsocketsSql\",\n register(container: Container, config?) {\n container.registerInstance(TableNameAbstraction, new TableName(config?.tableNamePrefix));\n container.register(WebsocketsConnectionRegistry);\n }\n});\n"],"names":["WebsocketsSqlFeature","createFeature","container","config","TableNameAbstraction","TableName","WebsocketsConnectionRegistry"],"mappings":";;;;AASO,MAAMA,uBAAuBC,cAA+C;IAC/E,MAAM;IACN,UAASC,SAAoB,EAAEC,MAAO;QAClCD,UAAU,gBAAgB,CAACE,WAAsB,IAAIC,uBAAUF,QAAQ;QACvED,UAAU,QAAQ,CAACI;IACvB;AACJ"}
|
package/index.d.ts
ADDED
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@webiny/api-websockets-sql",
|
|
3
|
+
"version": "0.0.0-unstable.0156a4c312",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"@webiny/api-websockets",
|
|
7
|
+
"storage-operations",
|
|
8
|
+
"sql",
|
|
9
|
+
"sau:sql"
|
|
10
|
+
],
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/webiny/webiny-js.git"
|
|
14
|
+
},
|
|
15
|
+
"description": "SQL storage operations for @webiny/api-websockets.",
|
|
16
|
+
"author": "Webiny Ltd.",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": "./index.js",
|
|
20
|
+
"./*": "./*"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@webiny/api-core-sql": "0.0.0-unstable.0156a4c312",
|
|
24
|
+
"@webiny/api-websockets": "0.0.0-unstable.0156a4c312",
|
|
25
|
+
"@webiny/error": "0.0.0-unstable.0156a4c312",
|
|
26
|
+
"@webiny/feature": "0.0.0-unstable.0156a4c312",
|
|
27
|
+
"knex": "3.3.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@webiny/api-core": "0.0.0-unstable.0156a4c312",
|
|
31
|
+
"@webiny/build-tools": "0.0.0-unstable.0156a4c312",
|
|
32
|
+
"@webiny/di": "2.0.0",
|
|
33
|
+
"better-sqlite3": "13.0.3",
|
|
34
|
+
"rimraf": "6.1.3",
|
|
35
|
+
"typescript": "7.0.2",
|
|
36
|
+
"vitest": "5.0.5"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"webiny": {
|
|
42
|
+
"publishFrom": "dist"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/toIsoString.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize a timestamp read out of a `datetime` column to a UTC ISO string.
|
|
3
|
+
*
|
|
4
|
+
* We always write ISO strings, but the column's read shape is the driver's decision: node-postgres
|
|
5
|
+
* and mysql2 parse it into a `Date`, better-sqlite3 hands back the text it was given, and some
|
|
6
|
+
* configurations return "2026-08-04 17:02:06" with no `T` and no zone. That wall-clock is UTC,
|
|
7
|
+
* because that is how it was written, so the space form is read back as UTC.
|
|
8
|
+
*
|
|
9
|
+
* Taking `unknown` keeps the variance out of the row type, so the rest of the registry can treat a
|
|
10
|
+
* timestamp as the `string` it is declared to be. Anything unparseable throws rather than being
|
|
11
|
+
* stringified: these values are compared against a cutoff to decide which connections are still
|
|
12
|
+
* live, and a quietly malformed one drops a user's connections with no error to show for it.
|
|
13
|
+
*/
|
|
14
|
+
export declare const toIsoString: (value: unknown) => string;
|
package/toIsoString.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { WebinyError } from "@webiny/error";
|
|
2
|
+
const toIsoString = (value)=>{
|
|
3
|
+
if (value instanceof Date) return value.toISOString();
|
|
4
|
+
if ("string" == typeof value) {
|
|
5
|
+
const normalized = value.includes(" ") && !value.includes("T") ? `${value.replace(" ", "T")}Z` : value;
|
|
6
|
+
const parsed = new Date(normalized);
|
|
7
|
+
if (!Number.isNaN(parsed.getTime())) return parsed.toISOString();
|
|
8
|
+
}
|
|
9
|
+
throw new WebinyError("Could not read a websockets connection timestamp.", "INVALID_CONNECTION_TIMESTAMP", {
|
|
10
|
+
value
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
export { toIsoString };
|
|
14
|
+
|
|
15
|
+
//# sourceMappingURL=toIsoString.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toIsoString.js","sources":["../src/toIsoString.ts"],"sourcesContent":["import { WebinyError } from \"@webiny/error\";\n\n/**\n * Normalize a timestamp read out of a `datetime` column to a UTC ISO string.\n *\n * We always write ISO strings, but the column's read shape is the driver's decision: node-postgres\n * and mysql2 parse it into a `Date`, better-sqlite3 hands back the text it was given, and some\n * configurations return \"2026-08-04 17:02:06\" with no `T` and no zone. That wall-clock is UTC,\n * because that is how it was written, so the space form is read back as UTC.\n *\n * Taking `unknown` keeps the variance out of the row type, so the rest of the registry can treat a\n * timestamp as the `string` it is declared to be. Anything unparseable throws rather than being\n * stringified: these values are compared against a cutoff to decide which connections are still\n * live, and a quietly malformed one drops a user's connections with no error to show for it.\n */\nexport const toIsoString = (value: unknown): string => {\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n if (typeof value === \"string\") {\n const normalized =\n value.includes(\" \") && !value.includes(\"T\") ? `${value.replace(\" \", \"T\")}Z` : value;\n\n const parsed = new Date(normalized);\n if (!Number.isNaN(parsed.getTime())) {\n return parsed.toISOString();\n }\n }\n\n throw new WebinyError(\n \"Could not read a websockets connection timestamp.\",\n \"INVALID_CONNECTION_TIMESTAMP\",\n { value }\n );\n};\n"],"names":["toIsoString","value","Date","normalized","parsed","Number","WebinyError"],"mappings":";AAeO,MAAMA,cAAc,CAACC;IACxB,IAAIA,iBAAiBC,MACjB,OAAOD,MAAM,WAAW;IAG5B,IAAI,AAAiB,YAAjB,OAAOA,OAAoB;QAC3B,MAAME,aACFF,MAAM,QAAQ,CAAC,QAAQ,CAACA,MAAM,QAAQ,CAAC,OAAO,GAAGA,MAAM,OAAO,CAAC,KAAK,KAAK,CAAC,CAAC,GAAGA;QAElF,MAAMG,SAAS,IAAIF,KAAKC;QACxB,IAAI,CAACE,OAAO,KAAK,CAACD,OAAO,OAAO,KAC5B,OAAOA,OAAO,WAAW;IAEjC;IAEA,MAAM,IAAIE,YACN,qDACA,gCACA;QAAEL;IAAM;AAEhB"}
|