@strapi-community/plugin-io 5.0.6 → 5.1.0
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/README.md +47 -1
- package/dist/_chunks/LivePresencePanel-BeNq_EnQ.mjs +387 -0
- package/dist/_chunks/LivePresencePanel-CNaEK-Gk.js +389 -0
- package/dist/_chunks/{MonitoringPage-DLZdTZpg.mjs → MonitoringPage-Bn9XJSlg.mjs} +1 -1
- package/dist/_chunks/{MonitoringPage-HxHK1nFr.js → MonitoringPage-K5Y3hhKF.js} +1 -1
- package/dist/_chunks/{SettingsPage-88RdJkLy.js → SettingsPage-4OkXJAjU.js} +250 -148
- package/dist/_chunks/{SettingsPage-DBIu309c.mjs → SettingsPage-DMbMGU6J.mjs} +250 -148
- package/dist/_chunks/{index-BVQ20t1c.js → index--2NeIKGR.js} +15 -5
- package/dist/_chunks/{index-DLXtrAtk.mjs → index-CzvX8YTe.mjs} +15 -5
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +1120 -41
- package/dist/server/index.mjs +1119 -40
- package/package.json +2 -1
package/dist/server/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import require$$0$4 from "socket.io";
|
|
2
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
import require$$1 from "crypto";
|
|
3
4
|
import * as dates$1 from "date-fns";
|
|
4
5
|
import dates__default from "date-fns";
|
|
5
|
-
import require$$1 from "crypto";
|
|
6
6
|
import require$$0$5 from "child_process";
|
|
7
7
|
import require$$0$6 from "os";
|
|
8
8
|
import require$$0$8 from "path";
|
|
@@ -51,10 +51,10 @@ const require$$0$3 = {
|
|
|
51
51
|
strapi: strapi$1
|
|
52
52
|
};
|
|
53
53
|
const pluginPkg = require$$0$3;
|
|
54
|
-
const pluginId$
|
|
55
|
-
var pluginId_1 = { pluginId: pluginId$
|
|
56
|
-
const { pluginId: pluginId$
|
|
57
|
-
function getService$3({ name, plugin = pluginId$
|
|
54
|
+
const pluginId$9 = pluginPkg.strapi.name;
|
|
55
|
+
var pluginId_1 = { pluginId: pluginId$9 };
|
|
56
|
+
const { pluginId: pluginId$8 } = pluginId_1;
|
|
57
|
+
function getService$3({ name, plugin = pluginId$8, type: type2 = "plugin" }) {
|
|
58
58
|
let serviceUID = `${type2}::${plugin}`;
|
|
59
59
|
if (name && name.length) {
|
|
60
60
|
serviceUID += `.${name}`;
|
|
@@ -76,11 +76,24 @@ async function handshake$2(socket, next) {
|
|
|
76
76
|
try {
|
|
77
77
|
let room;
|
|
78
78
|
if (strategy2 && strategy2.length) {
|
|
79
|
-
|
|
79
|
+
let strategyType;
|
|
80
|
+
if (strategy2 === "jwt") {
|
|
81
|
+
strategyType = "role";
|
|
82
|
+
} else if (strategy2 === "admin-jwt") {
|
|
83
|
+
strategyType = "admin";
|
|
84
|
+
} else {
|
|
85
|
+
strategyType = "token";
|
|
86
|
+
}
|
|
80
87
|
const ctx = await strategyService[strategyType].authenticate(auth);
|
|
81
88
|
room = strategyService[strategyType].getRoomName(ctx);
|
|
89
|
+
if (strategyType === "admin") {
|
|
90
|
+
socket.adminUser = ctx;
|
|
91
|
+
}
|
|
82
92
|
} else if (strapi.plugin("users-permissions")) {
|
|
83
|
-
const role = await strapi.
|
|
93
|
+
const role = await strapi.documents("plugin::users-permissions.role").findFirst({
|
|
94
|
+
filters: { type: "public" },
|
|
95
|
+
fields: ["id", "name"]
|
|
96
|
+
});
|
|
84
97
|
room = strategyService["role"].getRoomName(role);
|
|
85
98
|
}
|
|
86
99
|
if (room) {
|
|
@@ -111,7 +124,7 @@ var constants$7 = {
|
|
|
111
124
|
const { Server } = require$$0$4;
|
|
112
125
|
const { handshake } = middleware;
|
|
113
126
|
const { getService: getService$1 } = getService_1;
|
|
114
|
-
const { pluginId: pluginId$
|
|
127
|
+
const { pluginId: pluginId$7 } = pluginId_1;
|
|
115
128
|
const { API_TOKEN_TYPE: API_TOKEN_TYPE$1 } = constants$7;
|
|
116
129
|
let SocketIO$2 = class SocketIO {
|
|
117
130
|
constructor(options) {
|
|
@@ -231,11 +244,11 @@ function requireSanitizeSensitiveFields() {
|
|
|
231
244
|
return sanitizeSensitiveFields;
|
|
232
245
|
}
|
|
233
246
|
const { SocketIO: SocketIO2 } = structures;
|
|
234
|
-
const { pluginId: pluginId$
|
|
247
|
+
const { pluginId: pluginId$6 } = pluginId_1;
|
|
235
248
|
async function bootstrapIO$1({ strapi: strapi2 }) {
|
|
236
|
-
const settingsService = strapi2.plugin(pluginId$
|
|
249
|
+
const settingsService = strapi2.plugin(pluginId$6).service("settings");
|
|
237
250
|
const settings2 = await settingsService.getSettings();
|
|
238
|
-
const monitoringService = strapi2.plugin(pluginId$
|
|
251
|
+
const monitoringService = strapi2.plugin(pluginId$6).service("monitoring");
|
|
239
252
|
const serverOptions = {
|
|
240
253
|
cors: {
|
|
241
254
|
origin: settings2.cors?.origins || ["http://localhost:3000"],
|
|
@@ -368,6 +381,11 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
|
|
|
368
381
|
}
|
|
369
382
|
});
|
|
370
383
|
}
|
|
384
|
+
const presenceService = strapi2.plugin(pluginId$6).service("presence");
|
|
385
|
+
const previewService = strapi2.plugin(pluginId$6).service("preview");
|
|
386
|
+
if (settings2.presence?.enabled !== false) {
|
|
387
|
+
presenceService.startCleanupInterval();
|
|
388
|
+
}
|
|
371
389
|
io2.server.on("connection", (socket) => {
|
|
372
390
|
const clientIp = socket.handshake.address || "unknown";
|
|
373
391
|
const username = socket.user?.username || "anonymous";
|
|
@@ -379,6 +397,10 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
|
|
|
379
397
|
user: socket.user || null
|
|
380
398
|
});
|
|
381
399
|
}
|
|
400
|
+
if (settings2.presence?.enabled !== false) {
|
|
401
|
+
const user = socket.user || socket.adminUser;
|
|
402
|
+
presenceService.registerConnection(socket.id, user);
|
|
403
|
+
}
|
|
382
404
|
if (settings2.rooms?.autoJoinByRole) {
|
|
383
405
|
const userRole = socket.user?.role || "public";
|
|
384
406
|
const rooms = settings2.rooms.autoJoinByRole[userRole] || [];
|
|
@@ -423,6 +445,70 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
|
|
|
423
445
|
const rooms = Array.from(socket.rooms).filter((r) => r !== socket.id);
|
|
424
446
|
if (callback) callback({ success: true, rooms });
|
|
425
447
|
});
|
|
448
|
+
socket.on("presence:join", async ({ uid, documentId }, callback) => {
|
|
449
|
+
if (settings2.presence?.enabled === false) {
|
|
450
|
+
if (callback) callback({ success: false, error: "Presence is disabled" });
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
if (!uid || !documentId) {
|
|
454
|
+
if (callback) callback({ success: false, error: "uid and documentId are required" });
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const result = await presenceService.joinEntity(socket.id, uid, documentId);
|
|
458
|
+
if (callback) callback(result);
|
|
459
|
+
});
|
|
460
|
+
socket.on("presence:leave", async ({ uid, documentId }, callback) => {
|
|
461
|
+
if (settings2.presence?.enabled === false) {
|
|
462
|
+
if (callback) callback({ success: false, error: "Presence is disabled" });
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (!uid || !documentId) {
|
|
466
|
+
if (callback) callback({ success: false, error: "uid and documentId are required" });
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const result = await presenceService.leaveEntity(socket.id, uid, documentId);
|
|
470
|
+
if (callback) callback(result);
|
|
471
|
+
});
|
|
472
|
+
socket.on("presence:heartbeat", (callback) => {
|
|
473
|
+
const result = presenceService.heartbeat(socket.id);
|
|
474
|
+
if (callback) callback(result);
|
|
475
|
+
});
|
|
476
|
+
socket.on("presence:typing", ({ uid, documentId, fieldName }) => {
|
|
477
|
+
if (settings2.presence?.enabled === false) return;
|
|
478
|
+
presenceService.broadcastTyping(socket.id, uid, documentId, fieldName);
|
|
479
|
+
});
|
|
480
|
+
socket.on("presence:check", async ({ uid, documentId }, callback) => {
|
|
481
|
+
if (settings2.presence?.enabled === false) {
|
|
482
|
+
if (callback) callback({ success: false, error: "Presence is disabled" });
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const editors = await presenceService.getEntityEditors(uid, documentId);
|
|
486
|
+
if (callback) callback({ success: true, editors, isBeingEdited: editors.length > 0 });
|
|
487
|
+
});
|
|
488
|
+
socket.on("preview:subscribe", async ({ uid, documentId }, callback) => {
|
|
489
|
+
if (settings2.livePreview?.enabled === false) {
|
|
490
|
+
if (callback) callback({ success: false, error: "Live preview is disabled" });
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (!uid || !documentId) {
|
|
494
|
+
if (callback) callback({ success: false, error: "uid and documentId are required" });
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const result = await previewService.subscribe(socket.id, uid, documentId);
|
|
498
|
+
if (callback) callback(result);
|
|
499
|
+
});
|
|
500
|
+
socket.on("preview:unsubscribe", ({ uid, documentId }, callback) => {
|
|
501
|
+
if (!uid || !documentId) {
|
|
502
|
+
if (callback) callback({ success: false, error: "uid and documentId are required" });
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const result = previewService.unsubscribe(socket.id, uid, documentId);
|
|
506
|
+
if (callback) callback(result);
|
|
507
|
+
});
|
|
508
|
+
socket.on("preview:field-change", ({ uid, documentId, fieldName, value }) => {
|
|
509
|
+
if (settings2.livePreview?.enabled === false) return;
|
|
510
|
+
previewService.emitFieldChange(socket.id, uid, documentId, fieldName, value);
|
|
511
|
+
});
|
|
426
512
|
socket.on("subscribe-entity", async ({ uid, id }, callback) => {
|
|
427
513
|
if (settings2.entitySubscriptions?.enabled === false) {
|
|
428
514
|
if (callback) callback({ success: false, error: "Entity subscriptions are disabled" });
|
|
@@ -557,7 +643,7 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
|
|
|
557
643
|
strapi2.log.debug(`socket.io: Private message from ${socket.id} to ${to}`);
|
|
558
644
|
if (callback) callback({ success: true });
|
|
559
645
|
});
|
|
560
|
-
socket.on("disconnect", (reason) => {
|
|
646
|
+
socket.on("disconnect", async (reason) => {
|
|
561
647
|
if (settings2.monitoring?.enableConnectionLogging) {
|
|
562
648
|
strapi2.log.info(`socket.io: Client disconnected (id: ${socket.id}, user: ${username}, reason: ${reason})`);
|
|
563
649
|
monitoringService.logEvent("disconnect", {
|
|
@@ -566,6 +652,12 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
|
|
|
566
652
|
user: socket.user || null
|
|
567
653
|
});
|
|
568
654
|
}
|
|
655
|
+
if (settings2.presence?.enabled !== false) {
|
|
656
|
+
await presenceService.unregisterConnection(socket.id);
|
|
657
|
+
}
|
|
658
|
+
if (settings2.livePreview?.enabled !== false) {
|
|
659
|
+
previewService.cleanupSocket(socket.id);
|
|
660
|
+
}
|
|
569
661
|
});
|
|
570
662
|
socket.on("error", (error2) => {
|
|
571
663
|
strapi2.log.error(`socket.io: Socket error (id: ${socket.id}): ${error2.message}`);
|
|
@@ -709,17 +801,52 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
|
|
|
709
801
|
}
|
|
710
802
|
});
|
|
711
803
|
const enabledContentTypes = allContentTypes.size;
|
|
804
|
+
strapi2.$io.presence = {
|
|
805
|
+
/**
|
|
806
|
+
* Get editors for an entity
|
|
807
|
+
*/
|
|
808
|
+
getEditors: (uid, documentId) => presenceService.getEntityEditors(uid, documentId),
|
|
809
|
+
/**
|
|
810
|
+
* Check if entity is being edited
|
|
811
|
+
*/
|
|
812
|
+
isBeingEdited: (uid, documentId) => presenceService.isEntityBeingEdited(uid, documentId),
|
|
813
|
+
/**
|
|
814
|
+
* Get presence statistics
|
|
815
|
+
*/
|
|
816
|
+
getStats: () => presenceService.getStats()
|
|
817
|
+
};
|
|
818
|
+
strapi2.$io.preview = {
|
|
819
|
+
/**
|
|
820
|
+
* Emit draft change to preview subscribers
|
|
821
|
+
*/
|
|
822
|
+
emitDraftChange: (uid, documentId, data, diff2) => previewService.emitDraftChange(uid, documentId, data, diff2),
|
|
823
|
+
/**
|
|
824
|
+
* Emit publish event
|
|
825
|
+
*/
|
|
826
|
+
emitPublish: (uid, documentId, data) => previewService.emitPublish(uid, documentId, data),
|
|
827
|
+
/**
|
|
828
|
+
* Emit unpublish event
|
|
829
|
+
*/
|
|
830
|
+
emitUnpublish: (uid, documentId) => previewService.emitUnpublish(uid, documentId),
|
|
831
|
+
/**
|
|
832
|
+
* Get preview statistics
|
|
833
|
+
*/
|
|
834
|
+
getStats: () => previewService.getStats()
|
|
835
|
+
};
|
|
712
836
|
const origins = settings2.cors?.origins?.join(", ") || "http://localhost:3000";
|
|
713
837
|
const features = [];
|
|
714
838
|
if (settings2.redis?.enabled) features.push("Redis");
|
|
715
839
|
if (settings2.namespaces?.enabled) features.push(`Namespaces(${Object.keys(settings2.namespaces.list || {}).length})`);
|
|
716
840
|
if (settings2.security?.rateLimiting?.enabled) features.push("RateLimit");
|
|
841
|
+
if (settings2.presence?.enabled !== false) features.push("Presence");
|
|
842
|
+
if (settings2.livePreview?.enabled !== false) features.push("LivePreview");
|
|
843
|
+
if (settings2.fieldLevelChanges?.enabled !== false) features.push("FieldDiff");
|
|
717
844
|
strapi2.log.info(`socket.io: Plugin initialized`);
|
|
718
|
-
strapi2.log.info(`
|
|
719
|
-
strapi2.log.info(`
|
|
720
|
-
strapi2.log.info(`
|
|
845
|
+
strapi2.log.info(` - Origins: ${origins}`);
|
|
846
|
+
strapi2.log.info(` - Content Types: ${enabledContentTypes}`);
|
|
847
|
+
strapi2.log.info(` - Max Connections: ${settings2.connection?.maxConnections || 1e3}`);
|
|
721
848
|
if (features.length > 0) {
|
|
722
|
-
strapi2.log.info(`
|
|
849
|
+
strapi2.log.info(` - Features: ${features.join(", ")}`);
|
|
723
850
|
}
|
|
724
851
|
}
|
|
725
852
|
var io = { bootstrapIO: bootstrapIO$1 };
|
|
@@ -793,7 +920,7 @@ function getTransactionCtx() {
|
|
|
793
920
|
}
|
|
794
921
|
return transactionCtx;
|
|
795
922
|
}
|
|
796
|
-
const { pluginId: pluginId$
|
|
923
|
+
const { pluginId: pluginId$5 } = pluginId_1;
|
|
797
924
|
function scheduleAfterTransaction(callback, delay = 0) {
|
|
798
925
|
const runner = () => setTimeout(callback, delay);
|
|
799
926
|
const ctx = getTransactionCtx();
|
|
@@ -880,17 +1007,47 @@ async function bootstrapLifecycles$1({ strapi: strapi2 }) {
|
|
|
880
1007
|
}, 50);
|
|
881
1008
|
}
|
|
882
1009
|
};
|
|
1010
|
+
subscriber.beforeUpdate = async (event) => {
|
|
1011
|
+
if (!isActionEnabled(strapi2, uid, "update")) return;
|
|
1012
|
+
const fieldLevelEnabled = strapi2.$ioSettings?.fieldLevelChanges?.enabled !== false;
|
|
1013
|
+
if (!fieldLevelEnabled) return;
|
|
1014
|
+
try {
|
|
1015
|
+
const documentId = event.params.where?.documentId || event.params.documentId;
|
|
1016
|
+
if (!documentId) return;
|
|
1017
|
+
const existing = await strapi2.documents(uid).findOne({ documentId });
|
|
1018
|
+
if (existing) {
|
|
1019
|
+
if (!event.state.io) event.state.io = {};
|
|
1020
|
+
event.state.io.previousData = JSON.parse(JSON.stringify(existing));
|
|
1021
|
+
}
|
|
1022
|
+
} catch (error2) {
|
|
1023
|
+
strapi2.log.debug(`socket.io: Could not fetch previous data for diff: ${error2.message}`);
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
883
1026
|
subscriber.afterUpdate = async (event) => {
|
|
884
1027
|
if (!isActionEnabled(strapi2, uid, "update")) return;
|
|
885
|
-
const
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
data: JSON.parse(JSON.stringify(event.result))
|
|
889
|
-
// Deep clone
|
|
890
|
-
};
|
|
1028
|
+
const newData = JSON.parse(JSON.stringify(event.result));
|
|
1029
|
+
const previousData = event.state.io?.previousData || null;
|
|
1030
|
+
const modelInfo = { singularName: event.model.singularName, uid: event.model.uid };
|
|
891
1031
|
scheduleAfterTransaction(() => {
|
|
892
1032
|
try {
|
|
893
|
-
strapi2
|
|
1033
|
+
const diffService = strapi2.plugin(pluginId$5).service("diff");
|
|
1034
|
+
const previewService = strapi2.plugin(pluginId$5).service("preview");
|
|
1035
|
+
const fieldLevelEnabled = strapi2.$ioSettings?.fieldLevelChanges?.enabled !== false;
|
|
1036
|
+
let eventPayload;
|
|
1037
|
+
if (fieldLevelEnabled && previousData && diffService) {
|
|
1038
|
+
eventPayload = diffService.createEventPayload("update", modelInfo, previousData, newData);
|
|
1039
|
+
} else {
|
|
1040
|
+
eventPayload = {
|
|
1041
|
+
event: "update",
|
|
1042
|
+
schema: modelInfo,
|
|
1043
|
+
data: newData
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
strapi2.$io.emit(eventPayload);
|
|
1047
|
+
if (previewService && newData.documentId) {
|
|
1048
|
+
const diff2 = fieldLevelEnabled ? eventPayload.diff : null;
|
|
1049
|
+
previewService.emitDraftChange(uid, newData.documentId, newData, diff2);
|
|
1050
|
+
}
|
|
894
1051
|
} catch (error2) {
|
|
895
1052
|
strapi2.log.debug(`socket.io: Could not emit update event for ${uid}:`, error2.message);
|
|
896
1053
|
}
|
|
@@ -991,14 +1148,14 @@ var config$1 = {
|
|
|
991
1148
|
validator(config2) {
|
|
992
1149
|
}
|
|
993
1150
|
};
|
|
994
|
-
const { pluginId: pluginId$
|
|
1151
|
+
const { pluginId: pluginId$4 } = pluginId_1;
|
|
995
1152
|
var settings$3 = ({ strapi: strapi2 }) => ({
|
|
996
1153
|
/**
|
|
997
1154
|
* GET /io/settings
|
|
998
1155
|
* Retrieve current plugin settings
|
|
999
1156
|
*/
|
|
1000
1157
|
async getSettings(ctx) {
|
|
1001
|
-
const settingsService = strapi2.plugin(pluginId$
|
|
1158
|
+
const settingsService = strapi2.plugin(pluginId$4).service("settings");
|
|
1002
1159
|
const settings2 = await settingsService.getSettings();
|
|
1003
1160
|
ctx.body = { data: settings2 };
|
|
1004
1161
|
},
|
|
@@ -1007,7 +1164,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
|
|
|
1007
1164
|
* Update plugin settings and hot-reload Socket.IO
|
|
1008
1165
|
*/
|
|
1009
1166
|
async updateSettings(ctx) {
|
|
1010
|
-
const settingsService = strapi2.plugin(pluginId$
|
|
1167
|
+
const settingsService = strapi2.plugin(pluginId$4).service("settings");
|
|
1011
1168
|
const { body } = ctx.request;
|
|
1012
1169
|
await settingsService.getSettings();
|
|
1013
1170
|
const updatedSettings = await settingsService.setSettings(body);
|
|
@@ -1040,7 +1197,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
|
|
|
1040
1197
|
* Get connection and event statistics
|
|
1041
1198
|
*/
|
|
1042
1199
|
async getStats(ctx) {
|
|
1043
|
-
const monitoringService = strapi2.plugin(pluginId$
|
|
1200
|
+
const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
|
|
1044
1201
|
const connectionStats = monitoringService.getConnectionStats();
|
|
1045
1202
|
const eventStats = monitoringService.getEventStats();
|
|
1046
1203
|
ctx.body = {
|
|
@@ -1055,7 +1212,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
|
|
|
1055
1212
|
* Get recent event log
|
|
1056
1213
|
*/
|
|
1057
1214
|
async getEventLog(ctx) {
|
|
1058
|
-
const monitoringService = strapi2.plugin(pluginId$
|
|
1215
|
+
const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
|
|
1059
1216
|
const limit = parseInt(ctx.query.limit) || 50;
|
|
1060
1217
|
const log = monitoringService.getEventLog(limit);
|
|
1061
1218
|
ctx.body = { data: log };
|
|
@@ -1065,7 +1222,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
|
|
|
1065
1222
|
* Send a test event
|
|
1066
1223
|
*/
|
|
1067
1224
|
async sendTestEvent(ctx) {
|
|
1068
|
-
const monitoringService = strapi2.plugin(pluginId$
|
|
1225
|
+
const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
|
|
1069
1226
|
const { eventName, data } = ctx.request.body;
|
|
1070
1227
|
try {
|
|
1071
1228
|
const result = monitoringService.sendTestEvent(eventName || "test", data || {});
|
|
@@ -1079,7 +1236,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
|
|
|
1079
1236
|
* Reset monitoring statistics
|
|
1080
1237
|
*/
|
|
1081
1238
|
async resetStats(ctx) {
|
|
1082
|
-
const monitoringService = strapi2.plugin(pluginId$
|
|
1239
|
+
const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
|
|
1083
1240
|
monitoringService.resetStats();
|
|
1084
1241
|
ctx.body = { data: { success: true } };
|
|
1085
1242
|
},
|
|
@@ -1103,7 +1260,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
|
|
|
1103
1260
|
* Get lightweight stats for dashboard widget
|
|
1104
1261
|
*/
|
|
1105
1262
|
async getMonitoringStats(ctx) {
|
|
1106
|
-
const monitoringService = strapi2.plugin(pluginId$
|
|
1263
|
+
const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
|
|
1107
1264
|
const connectionStats = monitoringService.getConnectionStats();
|
|
1108
1265
|
const eventStats = monitoringService.getEventStats();
|
|
1109
1266
|
ctx.body = {
|
|
@@ -1122,13 +1279,95 @@ var settings$3 = ({ strapi: strapi2 }) => ({
|
|
|
1122
1279
|
};
|
|
1123
1280
|
}
|
|
1124
1281
|
});
|
|
1282
|
+
const { randomUUID } = require$$1;
|
|
1283
|
+
const sessionTokens = /* @__PURE__ */ new Map();
|
|
1284
|
+
setInterval(() => {
|
|
1285
|
+
const now = Date.now();
|
|
1286
|
+
for (const [token, session] of sessionTokens.entries()) {
|
|
1287
|
+
if (session.expiresAt < now) {
|
|
1288
|
+
sessionTokens.delete(token);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}, 5 * 60 * 1e3);
|
|
1292
|
+
var presence$3 = ({ strapi: strapi2 }) => ({
|
|
1293
|
+
/**
|
|
1294
|
+
* Creates a session token for admin users to connect to Socket.IO
|
|
1295
|
+
* @param {object} ctx - Koa context
|
|
1296
|
+
*/
|
|
1297
|
+
async createSession(ctx) {
|
|
1298
|
+
const adminUser = ctx.state.user;
|
|
1299
|
+
if (!adminUser) {
|
|
1300
|
+
strapi2.log.warn("[plugin-io] Presence session requested without admin user");
|
|
1301
|
+
return ctx.unauthorized("Admin authentication required");
|
|
1302
|
+
}
|
|
1303
|
+
try {
|
|
1304
|
+
const token = randomUUID();
|
|
1305
|
+
const expiresAt = Date.now() + 2 * 60 * 1e3;
|
|
1306
|
+
sessionTokens.set(token, {
|
|
1307
|
+
token,
|
|
1308
|
+
user: {
|
|
1309
|
+
id: adminUser.id,
|
|
1310
|
+
email: adminUser.email,
|
|
1311
|
+
firstname: adminUser.firstname,
|
|
1312
|
+
lastname: adminUser.lastname
|
|
1313
|
+
},
|
|
1314
|
+
expiresAt
|
|
1315
|
+
});
|
|
1316
|
+
strapi2.log.info(`[plugin-io] Presence session created for admin user: ${adminUser.email}`);
|
|
1317
|
+
ctx.body = {
|
|
1318
|
+
token,
|
|
1319
|
+
user: {
|
|
1320
|
+
id: adminUser.id,
|
|
1321
|
+
email: adminUser.email,
|
|
1322
|
+
firstname: adminUser.firstname,
|
|
1323
|
+
lastname: adminUser.lastname
|
|
1324
|
+
},
|
|
1325
|
+
wsPath: "/socket.io",
|
|
1326
|
+
wsUrl: `${ctx.protocol}://${ctx.host}`
|
|
1327
|
+
};
|
|
1328
|
+
} catch (error2) {
|
|
1329
|
+
strapi2.log.error("[plugin-io] Failed to create presence session:", error2);
|
|
1330
|
+
return ctx.internalServerError("Failed to create session");
|
|
1331
|
+
}
|
|
1332
|
+
},
|
|
1333
|
+
/**
|
|
1334
|
+
* Validates and consumes a session token (one-time use)
|
|
1335
|
+
* @param {string} token - Session token to validate
|
|
1336
|
+
* @returns {object|null} Session data or null if invalid/expired
|
|
1337
|
+
*/
|
|
1338
|
+
consumeSessionToken(token) {
|
|
1339
|
+
if (!token) {
|
|
1340
|
+
return null;
|
|
1341
|
+
}
|
|
1342
|
+
const session = sessionTokens.get(token);
|
|
1343
|
+
if (!session) {
|
|
1344
|
+
return null;
|
|
1345
|
+
}
|
|
1346
|
+
if (session.expiresAt < Date.now()) {
|
|
1347
|
+
sessionTokens.delete(token);
|
|
1348
|
+
return null;
|
|
1349
|
+
}
|
|
1350
|
+
return session;
|
|
1351
|
+
}
|
|
1352
|
+
});
|
|
1125
1353
|
const settings$2 = settings$3;
|
|
1354
|
+
const presence$2 = presence$3;
|
|
1126
1355
|
var controllers$1 = {
|
|
1127
|
-
settings: settings$2
|
|
1356
|
+
settings: settings$2,
|
|
1357
|
+
presence: presence$2
|
|
1128
1358
|
};
|
|
1129
1359
|
var admin$1 = {
|
|
1130
1360
|
type: "admin",
|
|
1131
1361
|
routes: [
|
|
1362
|
+
// Presence Session - issues JWT token for Socket.IO connection
|
|
1363
|
+
{
|
|
1364
|
+
method: "POST",
|
|
1365
|
+
path: "/presence/session",
|
|
1366
|
+
handler: "presence.createSession",
|
|
1367
|
+
config: {
|
|
1368
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
1369
|
+
}
|
|
1370
|
+
},
|
|
1132
1371
|
{
|
|
1133
1372
|
method: "GET",
|
|
1134
1373
|
path: "/settings",
|
|
@@ -21468,9 +21707,9 @@ function padZeros(value, tok, options) {
|
|
|
21468
21707
|
if (!tok.isPadded) {
|
|
21469
21708
|
return value;
|
|
21470
21709
|
}
|
|
21471
|
-
let
|
|
21710
|
+
let diff2 = Math.abs(tok.maxLen - String(value).length);
|
|
21472
21711
|
let relax = options.relaxZeros !== false;
|
|
21473
|
-
switch (
|
|
21712
|
+
switch (diff2) {
|
|
21474
21713
|
case 0:
|
|
21475
21714
|
return "";
|
|
21476
21715
|
case 1:
|
|
@@ -21478,7 +21717,7 @@ function padZeros(value, tok, options) {
|
|
|
21478
21717
|
case 2:
|
|
21479
21718
|
return relax ? "0{0,2}" : "00";
|
|
21480
21719
|
default: {
|
|
21481
|
-
return relax ? `0{0,${
|
|
21720
|
+
return relax ? `0{0,${diff2}}` : `0{${diff2}}`;
|
|
21482
21721
|
}
|
|
21483
21722
|
}
|
|
21484
21723
|
}
|
|
@@ -29401,6 +29640,32 @@ var strategies = ({ strapi: strapi2 }) => {
|
|
|
29401
29640
|
const apiTokenService = getService({ type: "admin", plugin: "api-token" });
|
|
29402
29641
|
const jwtService = getService({ name: "jwt", plugin: "users-permissions" });
|
|
29403
29642
|
const userService = getService({ name: "user", plugin: "users-permissions" });
|
|
29643
|
+
const admin2 = {
|
|
29644
|
+
name: "io-admin",
|
|
29645
|
+
credentials: function(user) {
|
|
29646
|
+
return `${this.name}-${user.id}`;
|
|
29647
|
+
},
|
|
29648
|
+
authenticate: async function(auth) {
|
|
29649
|
+
const token2 = auth.token;
|
|
29650
|
+
if (!token2) {
|
|
29651
|
+
throw new UnauthorizedError2("Invalid admin credentials");
|
|
29652
|
+
}
|
|
29653
|
+
try {
|
|
29654
|
+
const presenceController = strapi2.plugin("io").controller("presence");
|
|
29655
|
+
const session = presenceController.consumeSessionToken(token2);
|
|
29656
|
+
if (!session) {
|
|
29657
|
+
throw new UnauthorizedError2("Invalid or expired session token");
|
|
29658
|
+
}
|
|
29659
|
+
return session.user;
|
|
29660
|
+
} catch (error2) {
|
|
29661
|
+
strapi2.log.warn("[plugin-io] Admin session verification failed:", error2.message);
|
|
29662
|
+
throw new UnauthorizedError2("Invalid admin credentials");
|
|
29663
|
+
}
|
|
29664
|
+
},
|
|
29665
|
+
getRoomName: function(user) {
|
|
29666
|
+
return `${this.name}-user-${user.id}`;
|
|
29667
|
+
}
|
|
29668
|
+
};
|
|
29404
29669
|
const role = {
|
|
29405
29670
|
name: "io-role",
|
|
29406
29671
|
credentials: function(role2) {
|
|
@@ -29543,6 +29808,7 @@ var strategies = ({ strapi: strapi2 }) => {
|
|
|
29543
29808
|
}
|
|
29544
29809
|
};
|
|
29545
29810
|
return {
|
|
29811
|
+
admin: admin2,
|
|
29546
29812
|
role,
|
|
29547
29813
|
token
|
|
29548
29814
|
};
|
|
@@ -29636,12 +29902,12 @@ function transformEntry(entry, type2) {
|
|
|
29636
29902
|
// meta: {},
|
|
29637
29903
|
};
|
|
29638
29904
|
}
|
|
29639
|
-
const { pluginId: pluginId$
|
|
29905
|
+
const { pluginId: pluginId$3 } = pluginId_1;
|
|
29640
29906
|
var settings$1 = ({ strapi: strapi2 }) => {
|
|
29641
29907
|
const getPluginStore = () => {
|
|
29642
29908
|
return strapi2.store({
|
|
29643
29909
|
type: "plugin",
|
|
29644
|
-
name: pluginId$
|
|
29910
|
+
name: pluginId$3
|
|
29645
29911
|
});
|
|
29646
29912
|
};
|
|
29647
29913
|
const getDefaultSettings = () => ({
|
|
@@ -29744,6 +30010,41 @@ var settings$1 = ({ strapi: strapi2 }) => {
|
|
|
29744
30010
|
enableConnectionLogging: true,
|
|
29745
30011
|
enableEventLogging: false,
|
|
29746
30012
|
maxEventLogSize: 100
|
|
30013
|
+
},
|
|
30014
|
+
// Presence System (Collaboration Awareness)
|
|
30015
|
+
presence: {
|
|
30016
|
+
enabled: true,
|
|
30017
|
+
// Enable presence tracking
|
|
30018
|
+
heartbeatInterval: 3e4,
|
|
30019
|
+
// Heartbeat interval in ms
|
|
30020
|
+
staleTimeout: 6e4,
|
|
30021
|
+
// Time before connection considered stale
|
|
30022
|
+
showAvatars: true,
|
|
30023
|
+
// Show user avatars in UI
|
|
30024
|
+
showTypingIndicator: true
|
|
30025
|
+
// Show typing indicators
|
|
30026
|
+
},
|
|
30027
|
+
// Live Preview (Real-time Draft Updates)
|
|
30028
|
+
livePreview: {
|
|
30029
|
+
enabled: true,
|
|
30030
|
+
// Enable live preview
|
|
30031
|
+
draftEvents: true,
|
|
30032
|
+
// Emit events for draft changes
|
|
30033
|
+
debounceMs: 300,
|
|
30034
|
+
// Debounce field changes
|
|
30035
|
+
maxSubscriptionsPerSocket: 50
|
|
30036
|
+
// Max preview subscriptions per socket
|
|
30037
|
+
},
|
|
30038
|
+
// Field-level Changes (Diff-based Updates)
|
|
30039
|
+
fieldLevelChanges: {
|
|
30040
|
+
enabled: true,
|
|
30041
|
+
// Enable field-level diff
|
|
30042
|
+
includeFullData: false,
|
|
30043
|
+
// Include full data alongside diff
|
|
30044
|
+
excludeFields: ["updatedAt", "updatedBy", "createdAt", "createdBy"],
|
|
30045
|
+
// Fields to exclude from diff
|
|
30046
|
+
maxDiffDepth: 3
|
|
30047
|
+
// Maximum nesting depth for diff
|
|
29747
30048
|
}
|
|
29748
30049
|
});
|
|
29749
30050
|
return {
|
|
@@ -29784,7 +30085,7 @@ var settings$1 = ({ strapi: strapi2 }) => {
|
|
|
29784
30085
|
getDefaultSettings
|
|
29785
30086
|
};
|
|
29786
30087
|
};
|
|
29787
|
-
const { pluginId } = pluginId_1;
|
|
30088
|
+
const { pluginId: pluginId$2 } = pluginId_1;
|
|
29788
30089
|
var monitoring$1 = ({ strapi: strapi2 }) => {
|
|
29789
30090
|
let eventLog = [];
|
|
29790
30091
|
let eventStats = {
|
|
@@ -29928,17 +30229,795 @@ var monitoring$1 = ({ strapi: strapi2 }) => {
|
|
|
29928
30229
|
}
|
|
29929
30230
|
};
|
|
29930
30231
|
};
|
|
30232
|
+
const { pluginId: pluginId$1 } = pluginId_1;
|
|
30233
|
+
var presence$1 = ({ strapi: strapi2 }) => {
|
|
30234
|
+
const activeConnections = /* @__PURE__ */ new Map();
|
|
30235
|
+
const entityEditors = /* @__PURE__ */ new Map();
|
|
30236
|
+
let cleanupInterval = null;
|
|
30237
|
+
const getEntityKey = (uid, documentId) => `${uid}:${documentId}`;
|
|
30238
|
+
const getPresenceSettings = () => {
|
|
30239
|
+
const settings2 = strapi2.$ioSettings || {};
|
|
30240
|
+
return {
|
|
30241
|
+
enabled: settings2.presence?.enabled ?? true,
|
|
30242
|
+
heartbeatInterval: settings2.presence?.heartbeatInterval ?? 3e4,
|
|
30243
|
+
staleTimeout: settings2.presence?.staleTimeout ?? 6e4,
|
|
30244
|
+
showAvatars: settings2.presence?.showAvatars ?? true
|
|
30245
|
+
};
|
|
30246
|
+
};
|
|
30247
|
+
const broadcastPresenceUpdate = async (uid, documentId) => {
|
|
30248
|
+
const io2 = strapi2.$io?.server;
|
|
30249
|
+
if (!io2) return;
|
|
30250
|
+
const entityKey = getEntityKey(uid, documentId);
|
|
30251
|
+
const editorSocketIds = entityEditors.get(entityKey) || /* @__PURE__ */ new Set();
|
|
30252
|
+
const editors = [];
|
|
30253
|
+
for (const socketId of editorSocketIds) {
|
|
30254
|
+
const connection = activeConnections.get(socketId);
|
|
30255
|
+
if (connection?.user) {
|
|
30256
|
+
editors.push({
|
|
30257
|
+
socketId,
|
|
30258
|
+
user: {
|
|
30259
|
+
id: connection.user.id,
|
|
30260
|
+
username: connection.user.username,
|
|
30261
|
+
email: connection.user.email,
|
|
30262
|
+
firstname: connection.user.firstname,
|
|
30263
|
+
lastname: connection.user.lastname
|
|
30264
|
+
},
|
|
30265
|
+
joinedAt: connection.entities?.get(entityKey) || Date.now()
|
|
30266
|
+
});
|
|
30267
|
+
}
|
|
30268
|
+
}
|
|
30269
|
+
const roomName = `presence:${entityKey}`;
|
|
30270
|
+
io2.to(roomName).emit("presence:update", {
|
|
30271
|
+
uid,
|
|
30272
|
+
documentId,
|
|
30273
|
+
editors,
|
|
30274
|
+
count: editors.length,
|
|
30275
|
+
timestamp: Date.now()
|
|
30276
|
+
});
|
|
30277
|
+
strapi2.log.debug(`socket.io: Presence update for ${entityKey} - ${editors.length} editor(s)`);
|
|
30278
|
+
};
|
|
30279
|
+
return {
|
|
30280
|
+
/**
|
|
30281
|
+
* Registers a new socket connection for presence tracking
|
|
30282
|
+
* @param {string} socketId - Socket ID
|
|
30283
|
+
* @param {object} user - User object (can be null for anonymous)
|
|
30284
|
+
*/
|
|
30285
|
+
registerConnection(socketId, user = null) {
|
|
30286
|
+
const settings2 = getPresenceSettings();
|
|
30287
|
+
if (!settings2.enabled) return;
|
|
30288
|
+
activeConnections.set(socketId, {
|
|
30289
|
+
user,
|
|
30290
|
+
entities: /* @__PURE__ */ new Map(),
|
|
30291
|
+
// entityKey -> joinedAt timestamp
|
|
30292
|
+
lastSeen: Date.now(),
|
|
30293
|
+
connectedAt: Date.now()
|
|
30294
|
+
});
|
|
30295
|
+
strapi2.log.debug(`socket.io: Presence registered for socket ${socketId}`);
|
|
30296
|
+
},
|
|
30297
|
+
/**
|
|
30298
|
+
* Unregisters a socket connection and cleans up all entity presence
|
|
30299
|
+
* @param {string} socketId - Socket ID
|
|
30300
|
+
*/
|
|
30301
|
+
async unregisterConnection(socketId) {
|
|
30302
|
+
const connection = activeConnections.get(socketId);
|
|
30303
|
+
if (!connection) return;
|
|
30304
|
+
if (connection.entities) {
|
|
30305
|
+
for (const entityKey of connection.entities.keys()) {
|
|
30306
|
+
const [uid, documentId] = entityKey.split(":");
|
|
30307
|
+
await this.leaveEntity(socketId, uid, documentId, false);
|
|
30308
|
+
}
|
|
30309
|
+
}
|
|
30310
|
+
activeConnections.delete(socketId);
|
|
30311
|
+
strapi2.log.debug(`socket.io: Presence unregistered for socket ${socketId}`);
|
|
30312
|
+
},
|
|
30313
|
+
/**
|
|
30314
|
+
* User joins an entity for editing
|
|
30315
|
+
* @param {string} socketId - Socket ID
|
|
30316
|
+
* @param {string} uid - Content type UID
|
|
30317
|
+
* @param {string} documentId - Document ID
|
|
30318
|
+
* @returns {object} Join result with current editors
|
|
30319
|
+
*/
|
|
30320
|
+
async joinEntity(socketId, uid, documentId) {
|
|
30321
|
+
const settings2 = getPresenceSettings();
|
|
30322
|
+
if (!settings2.enabled) {
|
|
30323
|
+
return { success: false, error: "Presence is disabled" };
|
|
30324
|
+
}
|
|
30325
|
+
const connection = activeConnections.get(socketId);
|
|
30326
|
+
if (!connection) {
|
|
30327
|
+
return { success: false, error: "Socket not registered for presence" };
|
|
30328
|
+
}
|
|
30329
|
+
const entityKey = getEntityKey(uid, documentId);
|
|
30330
|
+
if (!entityEditors.has(entityKey)) {
|
|
30331
|
+
entityEditors.set(entityKey, /* @__PURE__ */ new Set());
|
|
30332
|
+
}
|
|
30333
|
+
entityEditors.get(entityKey).add(socketId);
|
|
30334
|
+
connection.entities.set(entityKey, Date.now());
|
|
30335
|
+
connection.lastSeen = Date.now();
|
|
30336
|
+
const io2 = strapi2.$io?.server;
|
|
30337
|
+
const socket = io2?.sockets.sockets.get(socketId);
|
|
30338
|
+
if (socket) {
|
|
30339
|
+
socket.join(`presence:${entityKey}`);
|
|
30340
|
+
}
|
|
30341
|
+
await broadcastPresenceUpdate(uid, documentId);
|
|
30342
|
+
strapi2.log.info(`socket.io: User ${connection.user?.username || "anonymous"} joined entity ${entityKey}`);
|
|
30343
|
+
return {
|
|
30344
|
+
success: true,
|
|
30345
|
+
entityKey,
|
|
30346
|
+
editors: await this.getEntityEditors(uid, documentId)
|
|
30347
|
+
};
|
|
30348
|
+
},
|
|
30349
|
+
/**
|
|
30350
|
+
* User leaves an entity
|
|
30351
|
+
* @param {string} socketId - Socket ID
|
|
30352
|
+
* @param {string} uid - Content type UID
|
|
30353
|
+
* @param {string} documentId - Document ID
|
|
30354
|
+
* @param {boolean} broadcast - Whether to broadcast update (default: true)
|
|
30355
|
+
* @returns {object} Leave result
|
|
30356
|
+
*/
|
|
30357
|
+
async leaveEntity(socketId, uid, documentId, broadcast = true) {
|
|
30358
|
+
const settings2 = getPresenceSettings();
|
|
30359
|
+
if (!settings2.enabled) {
|
|
30360
|
+
return { success: false, error: "Presence is disabled" };
|
|
30361
|
+
}
|
|
30362
|
+
const entityKey = getEntityKey(uid, documentId);
|
|
30363
|
+
const connection = activeConnections.get(socketId);
|
|
30364
|
+
const editors = entityEditors.get(entityKey);
|
|
30365
|
+
if (editors) {
|
|
30366
|
+
editors.delete(socketId);
|
|
30367
|
+
if (editors.size === 0) {
|
|
30368
|
+
entityEditors.delete(entityKey);
|
|
30369
|
+
}
|
|
30370
|
+
}
|
|
30371
|
+
if (connection?.entities) {
|
|
30372
|
+
connection.entities.delete(entityKey);
|
|
30373
|
+
}
|
|
30374
|
+
const io2 = strapi2.$io?.server;
|
|
30375
|
+
const socket = io2?.sockets.sockets.get(socketId);
|
|
30376
|
+
if (socket) {
|
|
30377
|
+
socket.leave(`presence:${entityKey}`);
|
|
30378
|
+
}
|
|
30379
|
+
if (broadcast) {
|
|
30380
|
+
await broadcastPresenceUpdate(uid, documentId);
|
|
30381
|
+
}
|
|
30382
|
+
strapi2.log.debug(`socket.io: Socket ${socketId} left entity ${entityKey}`);
|
|
30383
|
+
return { success: true, entityKey };
|
|
30384
|
+
},
|
|
30385
|
+
/**
|
|
30386
|
+
* Gets all editors currently editing an entity
|
|
30387
|
+
* @param {string} uid - Content type UID
|
|
30388
|
+
* @param {string} documentId - Document ID
|
|
30389
|
+
* @returns {Array} List of editors with user info
|
|
30390
|
+
*/
|
|
30391
|
+
async getEntityEditors(uid, documentId) {
|
|
30392
|
+
const entityKey = getEntityKey(uid, documentId);
|
|
30393
|
+
const editorSocketIds = entityEditors.get(entityKey) || /* @__PURE__ */ new Set();
|
|
30394
|
+
const editors = [];
|
|
30395
|
+
for (const socketId of editorSocketIds) {
|
|
30396
|
+
const connection = activeConnections.get(socketId);
|
|
30397
|
+
if (connection?.user) {
|
|
30398
|
+
editors.push({
|
|
30399
|
+
socketId,
|
|
30400
|
+
user: {
|
|
30401
|
+
id: connection.user.id,
|
|
30402
|
+
username: connection.user.username,
|
|
30403
|
+
email: connection.user.email,
|
|
30404
|
+
firstname: connection.user.firstname,
|
|
30405
|
+
lastname: connection.user.lastname
|
|
30406
|
+
},
|
|
30407
|
+
joinedAt: connection.entities?.get(entityKey) || Date.now()
|
|
30408
|
+
});
|
|
30409
|
+
}
|
|
30410
|
+
}
|
|
30411
|
+
return editors;
|
|
30412
|
+
},
|
|
30413
|
+
/**
|
|
30414
|
+
* Updates heartbeat for a socket to keep presence alive
|
|
30415
|
+
* @param {string} socketId - Socket ID
|
|
30416
|
+
* @returns {object} Heartbeat result
|
|
30417
|
+
*/
|
|
30418
|
+
heartbeat(socketId) {
|
|
30419
|
+
const connection = activeConnections.get(socketId);
|
|
30420
|
+
if (!connection) {
|
|
30421
|
+
return { success: false, error: "Socket not registered" };
|
|
30422
|
+
}
|
|
30423
|
+
connection.lastSeen = Date.now();
|
|
30424
|
+
return { success: true, lastSeen: connection.lastSeen };
|
|
30425
|
+
},
|
|
30426
|
+
/**
|
|
30427
|
+
* Cleans up stale connections that haven't sent heartbeat
|
|
30428
|
+
* @returns {number} Number of connections cleaned up
|
|
30429
|
+
*/
|
|
30430
|
+
async cleanup() {
|
|
30431
|
+
const settings2 = getPresenceSettings();
|
|
30432
|
+
const staleTimeout = settings2.staleTimeout;
|
|
30433
|
+
const now = Date.now();
|
|
30434
|
+
let cleanedUp = 0;
|
|
30435
|
+
for (const [socketId, connection] of activeConnections) {
|
|
30436
|
+
if (now - connection.lastSeen > staleTimeout) {
|
|
30437
|
+
await this.unregisterConnection(socketId);
|
|
30438
|
+
cleanedUp++;
|
|
30439
|
+
}
|
|
30440
|
+
}
|
|
30441
|
+
if (cleanedUp > 0) {
|
|
30442
|
+
strapi2.log.info(`socket.io: Presence cleanup removed ${cleanedUp} stale connection(s)`);
|
|
30443
|
+
}
|
|
30444
|
+
return cleanedUp;
|
|
30445
|
+
},
|
|
30446
|
+
/**
|
|
30447
|
+
* Starts the cleanup interval
|
|
30448
|
+
*/
|
|
30449
|
+
startCleanupInterval() {
|
|
30450
|
+
const settings2 = getPresenceSettings();
|
|
30451
|
+
if (!settings2.enabled) return;
|
|
30452
|
+
cleanupInterval = setInterval(() => {
|
|
30453
|
+
this.cleanup();
|
|
30454
|
+
}, 6e4);
|
|
30455
|
+
strapi2.log.debug("socket.io: Presence cleanup interval started");
|
|
30456
|
+
},
|
|
30457
|
+
/**
|
|
30458
|
+
* Stops the cleanup interval
|
|
30459
|
+
*/
|
|
30460
|
+
stopCleanupInterval() {
|
|
30461
|
+
if (cleanupInterval) {
|
|
30462
|
+
clearInterval(cleanupInterval);
|
|
30463
|
+
cleanupInterval = null;
|
|
30464
|
+
}
|
|
30465
|
+
},
|
|
30466
|
+
/**
|
|
30467
|
+
* Gets presence statistics
|
|
30468
|
+
* @returns {object} Presence stats
|
|
30469
|
+
*/
|
|
30470
|
+
getStats() {
|
|
30471
|
+
const totalConnections = activeConnections.size;
|
|
30472
|
+
const totalEntitiesBeingEdited = entityEditors.size;
|
|
30473
|
+
let authenticated = 0;
|
|
30474
|
+
let anonymous = 0;
|
|
30475
|
+
for (const connection of activeConnections.values()) {
|
|
30476
|
+
if (connection.user) {
|
|
30477
|
+
authenticated++;
|
|
30478
|
+
} else {
|
|
30479
|
+
anonymous++;
|
|
30480
|
+
}
|
|
30481
|
+
}
|
|
30482
|
+
return {
|
|
30483
|
+
totalConnections,
|
|
30484
|
+
authenticated,
|
|
30485
|
+
anonymous,
|
|
30486
|
+
totalEntitiesBeingEdited,
|
|
30487
|
+
entities: Array.from(entityEditors.entries()).map(([key, editors]) => ({
|
|
30488
|
+
entityKey: key,
|
|
30489
|
+
editorCount: editors.size
|
|
30490
|
+
}))
|
|
30491
|
+
};
|
|
30492
|
+
},
|
|
30493
|
+
/**
|
|
30494
|
+
* Gets all entities a user is currently editing
|
|
30495
|
+
* @param {string} socketId - Socket ID
|
|
30496
|
+
* @returns {Array} List of entity keys
|
|
30497
|
+
*/
|
|
30498
|
+
getUserEntities(socketId) {
|
|
30499
|
+
const connection = activeConnections.get(socketId);
|
|
30500
|
+
if (!connection) return [];
|
|
30501
|
+
return Array.from(connection.entities.keys());
|
|
30502
|
+
},
|
|
30503
|
+
/**
|
|
30504
|
+
* Checks if an entity is being edited by anyone
|
|
30505
|
+
* @param {string} uid - Content type UID
|
|
30506
|
+
* @param {string} documentId - Document ID
|
|
30507
|
+
* @returns {boolean} True if entity has editors
|
|
30508
|
+
*/
|
|
30509
|
+
isEntityBeingEdited(uid, documentId) {
|
|
30510
|
+
const entityKey = getEntityKey(uid, documentId);
|
|
30511
|
+
const editors = entityEditors.get(entityKey);
|
|
30512
|
+
return editors ? editors.size > 0 : false;
|
|
30513
|
+
},
|
|
30514
|
+
/**
|
|
30515
|
+
* Broadcasts a typing indicator for an entity
|
|
30516
|
+
* @param {string} socketId - Socket ID of typing user
|
|
30517
|
+
* @param {string} uid - Content type UID
|
|
30518
|
+
* @param {string} documentId - Document ID
|
|
30519
|
+
* @param {string} fieldName - Name of field being edited
|
|
30520
|
+
*/
|
|
30521
|
+
broadcastTyping(socketId, uid, documentId, fieldName) {
|
|
30522
|
+
const io2 = strapi2.$io?.server;
|
|
30523
|
+
if (!io2) return;
|
|
30524
|
+
const connection = activeConnections.get(socketId);
|
|
30525
|
+
if (!connection?.user) return;
|
|
30526
|
+
const entityKey = getEntityKey(uid, documentId);
|
|
30527
|
+
const roomName = `presence:${entityKey}`;
|
|
30528
|
+
const socket = io2.sockets.sockets.get(socketId);
|
|
30529
|
+
if (socket) {
|
|
30530
|
+
socket.to(roomName).emit("presence:typing", {
|
|
30531
|
+
uid,
|
|
30532
|
+
documentId,
|
|
30533
|
+
user: {
|
|
30534
|
+
id: connection.user.id,
|
|
30535
|
+
username: connection.user.username
|
|
30536
|
+
},
|
|
30537
|
+
fieldName,
|
|
30538
|
+
timestamp: Date.now()
|
|
30539
|
+
});
|
|
30540
|
+
}
|
|
30541
|
+
}
|
|
30542
|
+
};
|
|
30543
|
+
};
|
|
30544
|
+
const { pluginId } = pluginId_1;
|
|
30545
|
+
var preview$1 = ({ strapi: strapi2 }) => {
|
|
30546
|
+
const previewSubscribers = /* @__PURE__ */ new Map();
|
|
30547
|
+
const socketState = /* @__PURE__ */ new Map();
|
|
30548
|
+
const getEntityKey = (uid, documentId) => `${uid}:${documentId}`;
|
|
30549
|
+
const getPreviewSettings = () => {
|
|
30550
|
+
const settings2 = strapi2.$ioSettings || {};
|
|
30551
|
+
return {
|
|
30552
|
+
enabled: settings2.livePreview?.enabled ?? true,
|
|
30553
|
+
draftEvents: settings2.livePreview?.draftEvents ?? true,
|
|
30554
|
+
debounceMs: settings2.livePreview?.debounceMs ?? 300,
|
|
30555
|
+
maxSubscriptionsPerSocket: settings2.livePreview?.maxSubscriptionsPerSocket ?? 50
|
|
30556
|
+
};
|
|
30557
|
+
};
|
|
30558
|
+
const emitToSubscribers = (uid, documentId, eventType, data) => {
|
|
30559
|
+
const io2 = strapi2.$io?.server;
|
|
30560
|
+
if (!io2) return;
|
|
30561
|
+
const entityKey = getEntityKey(uid, documentId);
|
|
30562
|
+
const subscribers = previewSubscribers.get(entityKey);
|
|
30563
|
+
if (!subscribers || subscribers.size === 0) return;
|
|
30564
|
+
const roomName = `preview:${entityKey}`;
|
|
30565
|
+
io2.to(roomName).emit(eventType, {
|
|
30566
|
+
uid,
|
|
30567
|
+
documentId,
|
|
30568
|
+
...data,
|
|
30569
|
+
timestamp: Date.now()
|
|
30570
|
+
});
|
|
30571
|
+
strapi2.log.debug(`socket.io: Preview event '${eventType}' sent to ${subscribers.size} subscriber(s) for ${entityKey}`);
|
|
30572
|
+
};
|
|
30573
|
+
return {
|
|
30574
|
+
/**
|
|
30575
|
+
* Subscribes a socket to preview updates for an entity
|
|
30576
|
+
* @param {string} socketId - Socket ID
|
|
30577
|
+
* @param {string} uid - Content type UID
|
|
30578
|
+
* @param {string} documentId - Document ID
|
|
30579
|
+
* @returns {object} Subscription result
|
|
30580
|
+
*/
|
|
30581
|
+
async subscribe(socketId, uid, documentId) {
|
|
30582
|
+
const settings2 = getPreviewSettings();
|
|
30583
|
+
if (!settings2.enabled) {
|
|
30584
|
+
return { success: false, error: "Live preview is disabled" };
|
|
30585
|
+
}
|
|
30586
|
+
const entityKey = getEntityKey(uid, documentId);
|
|
30587
|
+
const io2 = strapi2.$io?.server;
|
|
30588
|
+
const socket = io2?.sockets.sockets.get(socketId);
|
|
30589
|
+
if (!socket) {
|
|
30590
|
+
return { success: false, error: "Socket not found" };
|
|
30591
|
+
}
|
|
30592
|
+
const currentSubs = Array.from(socket.rooms).filter((r) => r.startsWith("preview:")).length;
|
|
30593
|
+
if (currentSubs >= settings2.maxSubscriptionsPerSocket) {
|
|
30594
|
+
return { success: false, error: `Maximum preview subscriptions (${settings2.maxSubscriptionsPerSocket}) reached` };
|
|
30595
|
+
}
|
|
30596
|
+
if (!previewSubscribers.has(entityKey)) {
|
|
30597
|
+
previewSubscribers.set(entityKey, /* @__PURE__ */ new Set());
|
|
30598
|
+
}
|
|
30599
|
+
previewSubscribers.get(entityKey).add(socketId);
|
|
30600
|
+
socket.join(`preview:${entityKey}`);
|
|
30601
|
+
if (!socketState.has(socketId)) {
|
|
30602
|
+
socketState.set(socketId, { debounceTimers: /* @__PURE__ */ new Map() });
|
|
30603
|
+
}
|
|
30604
|
+
strapi2.log.debug(`socket.io: Socket ${socketId} subscribed to preview for ${entityKey}`);
|
|
30605
|
+
try {
|
|
30606
|
+
const entity = await strapi2.documents(uid).findOne({ documentId });
|
|
30607
|
+
if (entity) {
|
|
30608
|
+
socket.emit("preview:initial", {
|
|
30609
|
+
uid,
|
|
30610
|
+
documentId,
|
|
30611
|
+
data: entity,
|
|
30612
|
+
timestamp: Date.now()
|
|
30613
|
+
});
|
|
30614
|
+
}
|
|
30615
|
+
} catch (err) {
|
|
30616
|
+
strapi2.log.warn(`socket.io: Could not fetch initial preview data for ${entityKey}: ${err.message}`);
|
|
30617
|
+
}
|
|
30618
|
+
return {
|
|
30619
|
+
success: true,
|
|
30620
|
+
entityKey,
|
|
30621
|
+
subscriberCount: previewSubscribers.get(entityKey).size
|
|
30622
|
+
};
|
|
30623
|
+
},
|
|
30624
|
+
/**
|
|
30625
|
+
* Unsubscribes a socket from preview updates
|
|
30626
|
+
* @param {string} socketId - Socket ID
|
|
30627
|
+
* @param {string} uid - Content type UID
|
|
30628
|
+
* @param {string} documentId - Document ID
|
|
30629
|
+
* @returns {object} Unsubscription result
|
|
30630
|
+
*/
|
|
30631
|
+
unsubscribe(socketId, uid, documentId) {
|
|
30632
|
+
const entityKey = getEntityKey(uid, documentId);
|
|
30633
|
+
const subscribers = previewSubscribers.get(entityKey);
|
|
30634
|
+
if (subscribers) {
|
|
30635
|
+
subscribers.delete(socketId);
|
|
30636
|
+
if (subscribers.size === 0) {
|
|
30637
|
+
previewSubscribers.delete(entityKey);
|
|
30638
|
+
}
|
|
30639
|
+
}
|
|
30640
|
+
const io2 = strapi2.$io?.server;
|
|
30641
|
+
const socket = io2?.sockets.sockets.get(socketId);
|
|
30642
|
+
if (socket) {
|
|
30643
|
+
socket.leave(`preview:${entityKey}`);
|
|
30644
|
+
}
|
|
30645
|
+
const state = socketState.get(socketId);
|
|
30646
|
+
if (state?.debounceTimers.has(entityKey)) {
|
|
30647
|
+
clearTimeout(state.debounceTimers.get(entityKey));
|
|
30648
|
+
state.debounceTimers.delete(entityKey);
|
|
30649
|
+
}
|
|
30650
|
+
strapi2.log.debug(`socket.io: Socket ${socketId} unsubscribed from preview for ${entityKey}`);
|
|
30651
|
+
return { success: true, entityKey };
|
|
30652
|
+
},
|
|
30653
|
+
/**
|
|
30654
|
+
* Cleans up all subscriptions for a socket
|
|
30655
|
+
* @param {string} socketId - Socket ID
|
|
30656
|
+
*/
|
|
30657
|
+
cleanupSocket(socketId) {
|
|
30658
|
+
for (const [entityKey, subscribers] of previewSubscribers) {
|
|
30659
|
+
if (subscribers.has(socketId)) {
|
|
30660
|
+
subscribers.delete(socketId);
|
|
30661
|
+
if (subscribers.size === 0) {
|
|
30662
|
+
previewSubscribers.delete(entityKey);
|
|
30663
|
+
}
|
|
30664
|
+
}
|
|
30665
|
+
}
|
|
30666
|
+
const state = socketState.get(socketId);
|
|
30667
|
+
if (state) {
|
|
30668
|
+
for (const timerId of state.debounceTimers.values()) {
|
|
30669
|
+
clearTimeout(timerId);
|
|
30670
|
+
}
|
|
30671
|
+
socketState.delete(socketId);
|
|
30672
|
+
}
|
|
30673
|
+
},
|
|
30674
|
+
/**
|
|
30675
|
+
* Emits a draft change event to preview subscribers
|
|
30676
|
+
* @param {string} uid - Content type UID
|
|
30677
|
+
* @param {string} documentId - Document ID
|
|
30678
|
+
* @param {object} data - Changed data
|
|
30679
|
+
* @param {object} diff - Field-level diff (optional)
|
|
30680
|
+
*/
|
|
30681
|
+
emitDraftChange(uid, documentId, data, diff2 = null) {
|
|
30682
|
+
const settings2 = getPreviewSettings();
|
|
30683
|
+
if (!settings2.enabled || !settings2.draftEvents) return;
|
|
30684
|
+
emitToSubscribers(uid, documentId, "preview:change", {
|
|
30685
|
+
data,
|
|
30686
|
+
diff: diff2,
|
|
30687
|
+
isDraft: true
|
|
30688
|
+
});
|
|
30689
|
+
},
|
|
30690
|
+
/**
|
|
30691
|
+
* Emits a debounced field change event
|
|
30692
|
+
* @param {string} socketId - Socket ID of the editor
|
|
30693
|
+
* @param {string} uid - Content type UID
|
|
30694
|
+
* @param {string} documentId - Document ID
|
|
30695
|
+
* @param {string} fieldName - Name of changed field
|
|
30696
|
+
* @param {*} value - New field value
|
|
30697
|
+
*/
|
|
30698
|
+
emitFieldChange(socketId, uid, documentId, fieldName, value) {
|
|
30699
|
+
const settings2 = getPreviewSettings();
|
|
30700
|
+
if (!settings2.enabled) return;
|
|
30701
|
+
const entityKey = getEntityKey(uid, documentId);
|
|
30702
|
+
const state = socketState.get(socketId);
|
|
30703
|
+
if (state?.debounceTimers.has(entityKey)) {
|
|
30704
|
+
clearTimeout(state.debounceTimers.get(entityKey));
|
|
30705
|
+
}
|
|
30706
|
+
const timerId = setTimeout(() => {
|
|
30707
|
+
emitToSubscribers(uid, documentId, "preview:field", {
|
|
30708
|
+
fieldName,
|
|
30709
|
+
value,
|
|
30710
|
+
editorSocketId: socketId
|
|
30711
|
+
});
|
|
30712
|
+
state?.debounceTimers.delete(entityKey);
|
|
30713
|
+
}, settings2.debounceMs);
|
|
30714
|
+
if (state) {
|
|
30715
|
+
state.debounceTimers.set(entityKey, timerId);
|
|
30716
|
+
}
|
|
30717
|
+
},
|
|
30718
|
+
/**
|
|
30719
|
+
* Emits publish event to preview subscribers
|
|
30720
|
+
* @param {string} uid - Content type UID
|
|
30721
|
+
* @param {string} documentId - Document ID
|
|
30722
|
+
* @param {object} data - Published data
|
|
30723
|
+
*/
|
|
30724
|
+
emitPublish(uid, documentId, data) {
|
|
30725
|
+
emitToSubscribers(uid, documentId, "preview:publish", {
|
|
30726
|
+
data,
|
|
30727
|
+
isDraft: false
|
|
30728
|
+
});
|
|
30729
|
+
},
|
|
30730
|
+
/**
|
|
30731
|
+
* Emits unpublish event to preview subscribers
|
|
30732
|
+
* @param {string} uid - Content type UID
|
|
30733
|
+
* @param {string} documentId - Document ID
|
|
30734
|
+
*/
|
|
30735
|
+
emitUnpublish(uid, documentId) {
|
|
30736
|
+
emitToSubscribers(uid, documentId, "preview:unpublish", {
|
|
30737
|
+
isDraft: true
|
|
30738
|
+
});
|
|
30739
|
+
},
|
|
30740
|
+
/**
|
|
30741
|
+
* Gets the number of preview subscribers for an entity
|
|
30742
|
+
* @param {string} uid - Content type UID
|
|
30743
|
+
* @param {string} documentId - Document ID
|
|
30744
|
+
* @returns {number} Subscriber count
|
|
30745
|
+
*/
|
|
30746
|
+
getSubscriberCount(uid, documentId) {
|
|
30747
|
+
const entityKey = getEntityKey(uid, documentId);
|
|
30748
|
+
return previewSubscribers.get(entityKey)?.size || 0;
|
|
30749
|
+
},
|
|
30750
|
+
/**
|
|
30751
|
+
* Gets all entities with active preview subscribers
|
|
30752
|
+
* @returns {Array} List of entity keys with subscriber counts
|
|
30753
|
+
*/
|
|
30754
|
+
getActivePreviewEntities() {
|
|
30755
|
+
const entities = [];
|
|
30756
|
+
for (const [entityKey, subscribers] of previewSubscribers) {
|
|
30757
|
+
const [uid, documentId] = entityKey.split(":");
|
|
30758
|
+
entities.push({
|
|
30759
|
+
uid,
|
|
30760
|
+
documentId,
|
|
30761
|
+
entityKey,
|
|
30762
|
+
subscriberCount: subscribers.size
|
|
30763
|
+
});
|
|
30764
|
+
}
|
|
30765
|
+
return entities;
|
|
30766
|
+
},
|
|
30767
|
+
/**
|
|
30768
|
+
* Checks if live preview is enabled
|
|
30769
|
+
* @returns {boolean} True if enabled
|
|
30770
|
+
*/
|
|
30771
|
+
isEnabled() {
|
|
30772
|
+
return getPreviewSettings().enabled;
|
|
30773
|
+
},
|
|
30774
|
+
/**
|
|
30775
|
+
* Gets preview statistics
|
|
30776
|
+
* @returns {object} Preview stats
|
|
30777
|
+
*/
|
|
30778
|
+
getStats() {
|
|
30779
|
+
let totalSubscriptions = 0;
|
|
30780
|
+
for (const subscribers of previewSubscribers.values()) {
|
|
30781
|
+
totalSubscriptions += subscribers.size;
|
|
30782
|
+
}
|
|
30783
|
+
return {
|
|
30784
|
+
totalEntitiesWithSubscribers: previewSubscribers.size,
|
|
30785
|
+
totalSubscriptions,
|
|
30786
|
+
entities: this.getActivePreviewEntities()
|
|
30787
|
+
};
|
|
30788
|
+
}
|
|
30789
|
+
};
|
|
30790
|
+
};
|
|
30791
|
+
var diff$1 = ({ strapi: strapi2 }) => {
|
|
30792
|
+
const getDiffSettings = () => {
|
|
30793
|
+
const settings2 = strapi2.$ioSettings || {};
|
|
30794
|
+
return {
|
|
30795
|
+
enabled: settings2.fieldLevelChanges?.enabled ?? true,
|
|
30796
|
+
includeFullData: settings2.fieldLevelChanges?.includeFullData ?? false,
|
|
30797
|
+
excludeFields: settings2.fieldLevelChanges?.excludeFields ?? ["updatedAt", "updatedBy", "createdAt", "createdBy"],
|
|
30798
|
+
maxDiffDepth: settings2.fieldLevelChanges?.maxDiffDepth ?? 3
|
|
30799
|
+
};
|
|
30800
|
+
};
|
|
30801
|
+
const isPlainObject2 = (value) => {
|
|
30802
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
|
|
30803
|
+
};
|
|
30804
|
+
const isEqual2 = (a, b) => {
|
|
30805
|
+
if (a === b) return true;
|
|
30806
|
+
if (a === null || b === null) return a === b;
|
|
30807
|
+
if (typeof a !== typeof b) return false;
|
|
30808
|
+
if (a instanceof Date && b instanceof Date) {
|
|
30809
|
+
return a.getTime() === b.getTime();
|
|
30810
|
+
}
|
|
30811
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
30812
|
+
if (a.length !== b.length) return false;
|
|
30813
|
+
return a.every((item, index2) => isEqual2(item, b[index2]));
|
|
30814
|
+
}
|
|
30815
|
+
if (isPlainObject2(a) && isPlainObject2(b)) {
|
|
30816
|
+
const keysA = Object.keys(a);
|
|
30817
|
+
const keysB = Object.keys(b);
|
|
30818
|
+
if (keysA.length !== keysB.length) return false;
|
|
30819
|
+
return keysA.every((key) => isEqual2(a[key], b[key]));
|
|
30820
|
+
}
|
|
30821
|
+
return false;
|
|
30822
|
+
};
|
|
30823
|
+
const safeClone = (value) => {
|
|
30824
|
+
if (value === null || value === void 0) return value;
|
|
30825
|
+
if (value instanceof Date) return value.toISOString();
|
|
30826
|
+
if (Array.isArray(value)) return value.map(safeClone);
|
|
30827
|
+
if (isPlainObject2(value)) {
|
|
30828
|
+
const cloned = {};
|
|
30829
|
+
for (const [key, val] of Object.entries(value)) {
|
|
30830
|
+
cloned[key] = safeClone(val);
|
|
30831
|
+
}
|
|
30832
|
+
return cloned;
|
|
30833
|
+
}
|
|
30834
|
+
return value;
|
|
30835
|
+
};
|
|
30836
|
+
const calculateDiffInternal = (oldData, newData, options = {}, depth2 = 0) => {
|
|
30837
|
+
const { excludeFields = [], maxDiffDepth = 3 } = options;
|
|
30838
|
+
const diff2 = {};
|
|
30839
|
+
if (!oldData || !newData) {
|
|
30840
|
+
return { _replaced: true, old: safeClone(oldData), new: safeClone(newData) };
|
|
30841
|
+
}
|
|
30842
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(oldData || {}), ...Object.keys(newData || {})]);
|
|
30843
|
+
for (const key of allKeys) {
|
|
30844
|
+
if (excludeFields.includes(key)) continue;
|
|
30845
|
+
const oldValue = oldData?.[key];
|
|
30846
|
+
const newValue = newData?.[key];
|
|
30847
|
+
if (isEqual2(oldValue, newValue)) continue;
|
|
30848
|
+
if (isPlainObject2(oldValue) && isPlainObject2(newValue) && depth2 < maxDiffDepth) {
|
|
30849
|
+
const nestedDiff = calculateDiffInternal(oldValue, newValue, options, depth2 + 1);
|
|
30850
|
+
if (Object.keys(nestedDiff).length > 0) {
|
|
30851
|
+
diff2[key] = nestedDiff;
|
|
30852
|
+
}
|
|
30853
|
+
} else {
|
|
30854
|
+
diff2[key] = {
|
|
30855
|
+
old: safeClone(oldValue),
|
|
30856
|
+
new: safeClone(newValue)
|
|
30857
|
+
};
|
|
30858
|
+
}
|
|
30859
|
+
}
|
|
30860
|
+
return diff2;
|
|
30861
|
+
};
|
|
30862
|
+
return {
|
|
30863
|
+
/**
|
|
30864
|
+
* Calculates field-level diff between old and new data
|
|
30865
|
+
* @param {object} oldData - Previous data state
|
|
30866
|
+
* @param {object} newData - New data state
|
|
30867
|
+
* @returns {object} Diff result with changed fields and metadata
|
|
30868
|
+
*/
|
|
30869
|
+
calculateDiff(oldData, newData) {
|
|
30870
|
+
const settings2 = getDiffSettings();
|
|
30871
|
+
if (!settings2.enabled) {
|
|
30872
|
+
return {
|
|
30873
|
+
enabled: false,
|
|
30874
|
+
hasChanges: !isEqual2(oldData, newData),
|
|
30875
|
+
diff: null,
|
|
30876
|
+
fullData: newData
|
|
30877
|
+
};
|
|
30878
|
+
}
|
|
30879
|
+
const diff2 = calculateDiffInternal(oldData, newData, {
|
|
30880
|
+
excludeFields: settings2.excludeFields,
|
|
30881
|
+
maxDiffDepth: settings2.maxDiffDepth
|
|
30882
|
+
});
|
|
30883
|
+
const changedFields = Object.keys(diff2);
|
|
30884
|
+
const hasChanges = changedFields.length > 0;
|
|
30885
|
+
const result = {
|
|
30886
|
+
enabled: true,
|
|
30887
|
+
hasChanges,
|
|
30888
|
+
changedFields,
|
|
30889
|
+
changedFieldCount: changedFields.length,
|
|
30890
|
+
diff: hasChanges ? diff2 : null,
|
|
30891
|
+
timestamp: Date.now()
|
|
30892
|
+
};
|
|
30893
|
+
if (settings2.includeFullData) {
|
|
30894
|
+
result.fullData = newData;
|
|
30895
|
+
}
|
|
30896
|
+
return result;
|
|
30897
|
+
},
|
|
30898
|
+
/**
|
|
30899
|
+
* Applies a diff to a target object
|
|
30900
|
+
* @param {object} target - Target object to apply diff to
|
|
30901
|
+
* @param {object} diff - Diff to apply
|
|
30902
|
+
* @returns {object} Updated target object
|
|
30903
|
+
*/
|
|
30904
|
+
applyDiff(target, diff2) {
|
|
30905
|
+
if (!diff2 || typeof diff2 !== "object") return target;
|
|
30906
|
+
const result = { ...target };
|
|
30907
|
+
for (const [key, change] of Object.entries(diff2)) {
|
|
30908
|
+
if (change._replaced) {
|
|
30909
|
+
result[key] = change.new;
|
|
30910
|
+
} else if (change.old !== void 0 && change.new !== void 0) {
|
|
30911
|
+
result[key] = change.new;
|
|
30912
|
+
} else if (isPlainObject2(change)) {
|
|
30913
|
+
result[key] = this.applyDiff(result[key] || {}, change);
|
|
30914
|
+
}
|
|
30915
|
+
}
|
|
30916
|
+
return result;
|
|
30917
|
+
},
|
|
30918
|
+
/**
|
|
30919
|
+
* Validates if a diff is applicable to a content type
|
|
30920
|
+
* @param {string} uid - Content type UID
|
|
30921
|
+
* @param {object} diff - Diff to validate
|
|
30922
|
+
* @returns {object} Validation result
|
|
30923
|
+
*/
|
|
30924
|
+
validateDiff(uid, diff2) {
|
|
30925
|
+
if (!diff2) {
|
|
30926
|
+
return { valid: true, errors: [] };
|
|
30927
|
+
}
|
|
30928
|
+
const contentType = strapi2.contentTypes[uid];
|
|
30929
|
+
if (!contentType) {
|
|
30930
|
+
return { valid: false, errors: [`Content type ${uid} not found`] };
|
|
30931
|
+
}
|
|
30932
|
+
const errors2 = [];
|
|
30933
|
+
const attributes = contentType.attributes || {};
|
|
30934
|
+
for (const field of Object.keys(diff2)) {
|
|
30935
|
+
if (!attributes[field] && field !== "id" && field !== "documentId") {
|
|
30936
|
+
errors2.push(`Field '${field}' does not exist in ${uid}`);
|
|
30937
|
+
}
|
|
30938
|
+
}
|
|
30939
|
+
return {
|
|
30940
|
+
valid: errors2.length === 0,
|
|
30941
|
+
errors: errors2
|
|
30942
|
+
};
|
|
30943
|
+
},
|
|
30944
|
+
/**
|
|
30945
|
+
* Creates an event payload with diff information
|
|
30946
|
+
* @param {string} eventType - Event type (create, update, delete)
|
|
30947
|
+
* @param {object} schema - Content type schema info
|
|
30948
|
+
* @param {object} oldData - Previous data (null for create)
|
|
30949
|
+
* @param {object} newData - New data (null for delete)
|
|
30950
|
+
* @returns {object} Event payload with diff
|
|
30951
|
+
*/
|
|
30952
|
+
createEventPayload(eventType, schema2, oldData, newData) {
|
|
30953
|
+
const settings2 = getDiffSettings();
|
|
30954
|
+
if (eventType === "create") {
|
|
30955
|
+
return {
|
|
30956
|
+
event: eventType,
|
|
30957
|
+
schema: { singularName: schema2.singularName, uid: schema2.uid },
|
|
30958
|
+
data: newData,
|
|
30959
|
+
diff: null,
|
|
30960
|
+
timestamp: Date.now()
|
|
30961
|
+
};
|
|
30962
|
+
}
|
|
30963
|
+
if (eventType === "delete") {
|
|
30964
|
+
return {
|
|
30965
|
+
event: eventType,
|
|
30966
|
+
schema: { singularName: schema2.singularName, uid: schema2.uid },
|
|
30967
|
+
data: { id: oldData?.id, documentId: oldData?.documentId },
|
|
30968
|
+
deletedData: settings2.includeFullData ? oldData : null,
|
|
30969
|
+
diff: null,
|
|
30970
|
+
timestamp: Date.now()
|
|
30971
|
+
};
|
|
30972
|
+
}
|
|
30973
|
+
const diffResult = this.calculateDiff(oldData, newData);
|
|
30974
|
+
const payload = {
|
|
30975
|
+
event: eventType,
|
|
30976
|
+
schema: { singularName: schema2.singularName, uid: schema2.uid },
|
|
30977
|
+
documentId: newData?.documentId || newData?.id,
|
|
30978
|
+
diff: diffResult.diff,
|
|
30979
|
+
changedFields: diffResult.changedFields,
|
|
30980
|
+
hasChanges: diffResult.hasChanges,
|
|
30981
|
+
timestamp: Date.now()
|
|
30982
|
+
};
|
|
30983
|
+
if (settings2.includeFullData || !settings2.enabled) {
|
|
30984
|
+
payload.data = newData;
|
|
30985
|
+
}
|
|
30986
|
+
return payload;
|
|
30987
|
+
},
|
|
30988
|
+
/**
|
|
30989
|
+
* Checks if diff feature is enabled
|
|
30990
|
+
* @returns {boolean} True if enabled
|
|
30991
|
+
*/
|
|
30992
|
+
isEnabled() {
|
|
30993
|
+
return getDiffSettings().enabled;
|
|
30994
|
+
},
|
|
30995
|
+
/**
|
|
30996
|
+
* Gets current diff settings
|
|
30997
|
+
* @returns {object} Current settings
|
|
30998
|
+
*/
|
|
30999
|
+
getSettings() {
|
|
31000
|
+
return getDiffSettings();
|
|
31001
|
+
}
|
|
31002
|
+
};
|
|
31003
|
+
};
|
|
29931
31004
|
const strategy = strategies;
|
|
29932
31005
|
const sanitize = sanitize_1;
|
|
29933
31006
|
const transform = transform$1;
|
|
29934
31007
|
const settings = settings$1;
|
|
29935
31008
|
const monitoring = monitoring$1;
|
|
31009
|
+
const presence = presence$1;
|
|
31010
|
+
const preview = preview$1;
|
|
31011
|
+
const diff = diff$1;
|
|
29936
31012
|
var services$1 = {
|
|
29937
31013
|
sanitize,
|
|
29938
31014
|
strategy,
|
|
29939
31015
|
transform,
|
|
29940
31016
|
settings,
|
|
29941
|
-
monitoring
|
|
31017
|
+
monitoring,
|
|
31018
|
+
presence,
|
|
31019
|
+
preview,
|
|
31020
|
+
diff
|
|
29942
31021
|
};
|
|
29943
31022
|
const bootstrap = bootstrap_1;
|
|
29944
31023
|
const config = config$1;
|