@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.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const require$$0$4 = require("socket.io");
|
|
3
3
|
const node_async_hooks = require("node:async_hooks");
|
|
4
|
-
const dates$1 = require("date-fns");
|
|
5
4
|
const require$$1 = require("crypto");
|
|
5
|
+
const dates$1 = require("date-fns");
|
|
6
6
|
const require$$0$5 = require("child_process");
|
|
7
7
|
const require$$0$6 = require("os");
|
|
8
8
|
const require$$0$8 = require("path");
|
|
@@ -35,8 +35,8 @@ function _interopNamespace(e) {
|
|
|
35
35
|
return Object.freeze(n);
|
|
36
36
|
}
|
|
37
37
|
const require$$0__default = /* @__PURE__ */ _interopDefault(require$$0$4);
|
|
38
|
-
const dates__namespace = /* @__PURE__ */ _interopNamespace(dates$1);
|
|
39
38
|
const require$$1__default = /* @__PURE__ */ _interopDefault(require$$1);
|
|
39
|
+
const dates__namespace = /* @__PURE__ */ _interopNamespace(dates$1);
|
|
40
40
|
const require$$0__default$1 = /* @__PURE__ */ _interopDefault(require$$0$5);
|
|
41
41
|
const require$$0__default$2 = /* @__PURE__ */ _interopDefault(require$$0$6);
|
|
42
42
|
const require$$0__default$4 = /* @__PURE__ */ _interopDefault(require$$0$8);
|
|
@@ -83,10 +83,10 @@ const require$$0$3 = {
|
|
|
83
83
|
strapi: strapi$1
|
|
84
84
|
};
|
|
85
85
|
const pluginPkg = require$$0$3;
|
|
86
|
-
const pluginId$
|
|
87
|
-
var pluginId_1 = { pluginId: pluginId$
|
|
88
|
-
const { pluginId: pluginId$
|
|
89
|
-
function getService$3({ name, plugin = pluginId$
|
|
86
|
+
const pluginId$9 = pluginPkg.strapi.name;
|
|
87
|
+
var pluginId_1 = { pluginId: pluginId$9 };
|
|
88
|
+
const { pluginId: pluginId$8 } = pluginId_1;
|
|
89
|
+
function getService$3({ name, plugin = pluginId$8, type: type2 = "plugin" }) {
|
|
90
90
|
let serviceUID = `${type2}::${plugin}`;
|
|
91
91
|
if (name && name.length) {
|
|
92
92
|
serviceUID += `.${name}`;
|
|
@@ -108,11 +108,24 @@ async function handshake$2(socket, next) {
|
|
|
108
108
|
try {
|
|
109
109
|
let room;
|
|
110
110
|
if (strategy2 && strategy2.length) {
|
|
111
|
-
|
|
111
|
+
let strategyType;
|
|
112
|
+
if (strategy2 === "jwt") {
|
|
113
|
+
strategyType = "role";
|
|
114
|
+
} else if (strategy2 === "admin-jwt") {
|
|
115
|
+
strategyType = "admin";
|
|
116
|
+
} else {
|
|
117
|
+
strategyType = "token";
|
|
118
|
+
}
|
|
112
119
|
const ctx = await strategyService[strategyType].authenticate(auth);
|
|
113
120
|
room = strategyService[strategyType].getRoomName(ctx);
|
|
121
|
+
if (strategyType === "admin") {
|
|
122
|
+
socket.adminUser = ctx;
|
|
123
|
+
}
|
|
114
124
|
} else if (strapi.plugin("users-permissions")) {
|
|
115
|
-
const role = await strapi.
|
|
125
|
+
const role = await strapi.documents("plugin::users-permissions.role").findFirst({
|
|
126
|
+
filters: { type: "public" },
|
|
127
|
+
fields: ["id", "name"]
|
|
128
|
+
});
|
|
116
129
|
room = strategyService["role"].getRoomName(role);
|
|
117
130
|
}
|
|
118
131
|
if (room) {
|
|
@@ -143,7 +156,7 @@ var constants$7 = {
|
|
|
143
156
|
const { Server } = require$$0__default.default;
|
|
144
157
|
const { handshake } = middleware;
|
|
145
158
|
const { getService: getService$1 } = getService_1;
|
|
146
|
-
const { pluginId: pluginId$
|
|
159
|
+
const { pluginId: pluginId$7 } = pluginId_1;
|
|
147
160
|
const { API_TOKEN_TYPE: API_TOKEN_TYPE$1 } = constants$7;
|
|
148
161
|
let SocketIO$2 = class SocketIO {
|
|
149
162
|
constructor(options) {
|
|
@@ -263,11 +276,11 @@ function requireSanitizeSensitiveFields() {
|
|
|
263
276
|
return sanitizeSensitiveFields;
|
|
264
277
|
}
|
|
265
278
|
const { SocketIO: SocketIO2 } = structures;
|
|
266
|
-
const { pluginId: pluginId$
|
|
279
|
+
const { pluginId: pluginId$6 } = pluginId_1;
|
|
267
280
|
async function bootstrapIO$1({ strapi: strapi2 }) {
|
|
268
|
-
const settingsService = strapi2.plugin(pluginId$
|
|
281
|
+
const settingsService = strapi2.plugin(pluginId$6).service("settings");
|
|
269
282
|
const settings2 = await settingsService.getSettings();
|
|
270
|
-
const monitoringService = strapi2.plugin(pluginId$
|
|
283
|
+
const monitoringService = strapi2.plugin(pluginId$6).service("monitoring");
|
|
271
284
|
const serverOptions = {
|
|
272
285
|
cors: {
|
|
273
286
|
origin: settings2.cors?.origins || ["http://localhost:3000"],
|
|
@@ -400,6 +413,11 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
|
|
|
400
413
|
}
|
|
401
414
|
});
|
|
402
415
|
}
|
|
416
|
+
const presenceService = strapi2.plugin(pluginId$6).service("presence");
|
|
417
|
+
const previewService = strapi2.plugin(pluginId$6).service("preview");
|
|
418
|
+
if (settings2.presence?.enabled !== false) {
|
|
419
|
+
presenceService.startCleanupInterval();
|
|
420
|
+
}
|
|
403
421
|
io2.server.on("connection", (socket) => {
|
|
404
422
|
const clientIp = socket.handshake.address || "unknown";
|
|
405
423
|
const username = socket.user?.username || "anonymous";
|
|
@@ -411,6 +429,10 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
|
|
|
411
429
|
user: socket.user || null
|
|
412
430
|
});
|
|
413
431
|
}
|
|
432
|
+
if (settings2.presence?.enabled !== false) {
|
|
433
|
+
const user = socket.user || socket.adminUser;
|
|
434
|
+
presenceService.registerConnection(socket.id, user);
|
|
435
|
+
}
|
|
414
436
|
if (settings2.rooms?.autoJoinByRole) {
|
|
415
437
|
const userRole = socket.user?.role || "public";
|
|
416
438
|
const rooms = settings2.rooms.autoJoinByRole[userRole] || [];
|
|
@@ -455,6 +477,70 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
|
|
|
455
477
|
const rooms = Array.from(socket.rooms).filter((r) => r !== socket.id);
|
|
456
478
|
if (callback) callback({ success: true, rooms });
|
|
457
479
|
});
|
|
480
|
+
socket.on("presence:join", async ({ uid, documentId }, callback) => {
|
|
481
|
+
if (settings2.presence?.enabled === false) {
|
|
482
|
+
if (callback) callback({ success: false, error: "Presence is disabled" });
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (!uid || !documentId) {
|
|
486
|
+
if (callback) callback({ success: false, error: "uid and documentId are required" });
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const result = await presenceService.joinEntity(socket.id, uid, documentId);
|
|
490
|
+
if (callback) callback(result);
|
|
491
|
+
});
|
|
492
|
+
socket.on("presence:leave", async ({ uid, documentId }, callback) => {
|
|
493
|
+
if (settings2.presence?.enabled === false) {
|
|
494
|
+
if (callback) callback({ success: false, error: "Presence is disabled" });
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (!uid || !documentId) {
|
|
498
|
+
if (callback) callback({ success: false, error: "uid and documentId are required" });
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const result = await presenceService.leaveEntity(socket.id, uid, documentId);
|
|
502
|
+
if (callback) callback(result);
|
|
503
|
+
});
|
|
504
|
+
socket.on("presence:heartbeat", (callback) => {
|
|
505
|
+
const result = presenceService.heartbeat(socket.id);
|
|
506
|
+
if (callback) callback(result);
|
|
507
|
+
});
|
|
508
|
+
socket.on("presence:typing", ({ uid, documentId, fieldName }) => {
|
|
509
|
+
if (settings2.presence?.enabled === false) return;
|
|
510
|
+
presenceService.broadcastTyping(socket.id, uid, documentId, fieldName);
|
|
511
|
+
});
|
|
512
|
+
socket.on("presence:check", async ({ uid, documentId }, callback) => {
|
|
513
|
+
if (settings2.presence?.enabled === false) {
|
|
514
|
+
if (callback) callback({ success: false, error: "Presence is disabled" });
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const editors = await presenceService.getEntityEditors(uid, documentId);
|
|
518
|
+
if (callback) callback({ success: true, editors, isBeingEdited: editors.length > 0 });
|
|
519
|
+
});
|
|
520
|
+
socket.on("preview:subscribe", async ({ uid, documentId }, callback) => {
|
|
521
|
+
if (settings2.livePreview?.enabled === false) {
|
|
522
|
+
if (callback) callback({ success: false, error: "Live preview is disabled" });
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
if (!uid || !documentId) {
|
|
526
|
+
if (callback) callback({ success: false, error: "uid and documentId are required" });
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const result = await previewService.subscribe(socket.id, uid, documentId);
|
|
530
|
+
if (callback) callback(result);
|
|
531
|
+
});
|
|
532
|
+
socket.on("preview:unsubscribe", ({ uid, documentId }, callback) => {
|
|
533
|
+
if (!uid || !documentId) {
|
|
534
|
+
if (callback) callback({ success: false, error: "uid and documentId are required" });
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const result = previewService.unsubscribe(socket.id, uid, documentId);
|
|
538
|
+
if (callback) callback(result);
|
|
539
|
+
});
|
|
540
|
+
socket.on("preview:field-change", ({ uid, documentId, fieldName, value }) => {
|
|
541
|
+
if (settings2.livePreview?.enabled === false) return;
|
|
542
|
+
previewService.emitFieldChange(socket.id, uid, documentId, fieldName, value);
|
|
543
|
+
});
|
|
458
544
|
socket.on("subscribe-entity", async ({ uid, id }, callback) => {
|
|
459
545
|
if (settings2.entitySubscriptions?.enabled === false) {
|
|
460
546
|
if (callback) callback({ success: false, error: "Entity subscriptions are disabled" });
|
|
@@ -589,7 +675,7 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
|
|
|
589
675
|
strapi2.log.debug(`socket.io: Private message from ${socket.id} to ${to}`);
|
|
590
676
|
if (callback) callback({ success: true });
|
|
591
677
|
});
|
|
592
|
-
socket.on("disconnect", (reason) => {
|
|
678
|
+
socket.on("disconnect", async (reason) => {
|
|
593
679
|
if (settings2.monitoring?.enableConnectionLogging) {
|
|
594
680
|
strapi2.log.info(`socket.io: Client disconnected (id: ${socket.id}, user: ${username}, reason: ${reason})`);
|
|
595
681
|
monitoringService.logEvent("disconnect", {
|
|
@@ -598,6 +684,12 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
|
|
|
598
684
|
user: socket.user || null
|
|
599
685
|
});
|
|
600
686
|
}
|
|
687
|
+
if (settings2.presence?.enabled !== false) {
|
|
688
|
+
await presenceService.unregisterConnection(socket.id);
|
|
689
|
+
}
|
|
690
|
+
if (settings2.livePreview?.enabled !== false) {
|
|
691
|
+
previewService.cleanupSocket(socket.id);
|
|
692
|
+
}
|
|
601
693
|
});
|
|
602
694
|
socket.on("error", (error2) => {
|
|
603
695
|
strapi2.log.error(`socket.io: Socket error (id: ${socket.id}): ${error2.message}`);
|
|
@@ -741,17 +833,52 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
|
|
|
741
833
|
}
|
|
742
834
|
});
|
|
743
835
|
const enabledContentTypes = allContentTypes.size;
|
|
836
|
+
strapi2.$io.presence = {
|
|
837
|
+
/**
|
|
838
|
+
* Get editors for an entity
|
|
839
|
+
*/
|
|
840
|
+
getEditors: (uid, documentId) => presenceService.getEntityEditors(uid, documentId),
|
|
841
|
+
/**
|
|
842
|
+
* Check if entity is being edited
|
|
843
|
+
*/
|
|
844
|
+
isBeingEdited: (uid, documentId) => presenceService.isEntityBeingEdited(uid, documentId),
|
|
845
|
+
/**
|
|
846
|
+
* Get presence statistics
|
|
847
|
+
*/
|
|
848
|
+
getStats: () => presenceService.getStats()
|
|
849
|
+
};
|
|
850
|
+
strapi2.$io.preview = {
|
|
851
|
+
/**
|
|
852
|
+
* Emit draft change to preview subscribers
|
|
853
|
+
*/
|
|
854
|
+
emitDraftChange: (uid, documentId, data, diff2) => previewService.emitDraftChange(uid, documentId, data, diff2),
|
|
855
|
+
/**
|
|
856
|
+
* Emit publish event
|
|
857
|
+
*/
|
|
858
|
+
emitPublish: (uid, documentId, data) => previewService.emitPublish(uid, documentId, data),
|
|
859
|
+
/**
|
|
860
|
+
* Emit unpublish event
|
|
861
|
+
*/
|
|
862
|
+
emitUnpublish: (uid, documentId) => previewService.emitUnpublish(uid, documentId),
|
|
863
|
+
/**
|
|
864
|
+
* Get preview statistics
|
|
865
|
+
*/
|
|
866
|
+
getStats: () => previewService.getStats()
|
|
867
|
+
};
|
|
744
868
|
const origins = settings2.cors?.origins?.join(", ") || "http://localhost:3000";
|
|
745
869
|
const features = [];
|
|
746
870
|
if (settings2.redis?.enabled) features.push("Redis");
|
|
747
871
|
if (settings2.namespaces?.enabled) features.push(`Namespaces(${Object.keys(settings2.namespaces.list || {}).length})`);
|
|
748
872
|
if (settings2.security?.rateLimiting?.enabled) features.push("RateLimit");
|
|
873
|
+
if (settings2.presence?.enabled !== false) features.push("Presence");
|
|
874
|
+
if (settings2.livePreview?.enabled !== false) features.push("LivePreview");
|
|
875
|
+
if (settings2.fieldLevelChanges?.enabled !== false) features.push("FieldDiff");
|
|
749
876
|
strapi2.log.info(`socket.io: Plugin initialized`);
|
|
750
|
-
strapi2.log.info(`
|
|
751
|
-
strapi2.log.info(`
|
|
752
|
-
strapi2.log.info(`
|
|
877
|
+
strapi2.log.info(` - Origins: ${origins}`);
|
|
878
|
+
strapi2.log.info(` - Content Types: ${enabledContentTypes}`);
|
|
879
|
+
strapi2.log.info(` - Max Connections: ${settings2.connection?.maxConnections || 1e3}`);
|
|
753
880
|
if (features.length > 0) {
|
|
754
|
-
strapi2.log.info(`
|
|
881
|
+
strapi2.log.info(` - Features: ${features.join(", ")}`);
|
|
755
882
|
}
|
|
756
883
|
}
|
|
757
884
|
var io = { bootstrapIO: bootstrapIO$1 };
|
|
@@ -825,7 +952,7 @@ function getTransactionCtx() {
|
|
|
825
952
|
}
|
|
826
953
|
return transactionCtx;
|
|
827
954
|
}
|
|
828
|
-
const { pluginId: pluginId$
|
|
955
|
+
const { pluginId: pluginId$5 } = pluginId_1;
|
|
829
956
|
function scheduleAfterTransaction(callback, delay = 0) {
|
|
830
957
|
const runner = () => setTimeout(callback, delay);
|
|
831
958
|
const ctx = getTransactionCtx();
|
|
@@ -912,17 +1039,47 @@ async function bootstrapLifecycles$1({ strapi: strapi2 }) {
|
|
|
912
1039
|
}, 50);
|
|
913
1040
|
}
|
|
914
1041
|
};
|
|
1042
|
+
subscriber.beforeUpdate = async (event) => {
|
|
1043
|
+
if (!isActionEnabled(strapi2, uid, "update")) return;
|
|
1044
|
+
const fieldLevelEnabled = strapi2.$ioSettings?.fieldLevelChanges?.enabled !== false;
|
|
1045
|
+
if (!fieldLevelEnabled) return;
|
|
1046
|
+
try {
|
|
1047
|
+
const documentId = event.params.where?.documentId || event.params.documentId;
|
|
1048
|
+
if (!documentId) return;
|
|
1049
|
+
const existing = await strapi2.documents(uid).findOne({ documentId });
|
|
1050
|
+
if (existing) {
|
|
1051
|
+
if (!event.state.io) event.state.io = {};
|
|
1052
|
+
event.state.io.previousData = JSON.parse(JSON.stringify(existing));
|
|
1053
|
+
}
|
|
1054
|
+
} catch (error2) {
|
|
1055
|
+
strapi2.log.debug(`socket.io: Could not fetch previous data for diff: ${error2.message}`);
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
915
1058
|
subscriber.afterUpdate = async (event) => {
|
|
916
1059
|
if (!isActionEnabled(strapi2, uid, "update")) return;
|
|
917
|
-
const
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
data: JSON.parse(JSON.stringify(event.result))
|
|
921
|
-
// Deep clone
|
|
922
|
-
};
|
|
1060
|
+
const newData = JSON.parse(JSON.stringify(event.result));
|
|
1061
|
+
const previousData = event.state.io?.previousData || null;
|
|
1062
|
+
const modelInfo = { singularName: event.model.singularName, uid: event.model.uid };
|
|
923
1063
|
scheduleAfterTransaction(() => {
|
|
924
1064
|
try {
|
|
925
|
-
strapi2
|
|
1065
|
+
const diffService = strapi2.plugin(pluginId$5).service("diff");
|
|
1066
|
+
const previewService = strapi2.plugin(pluginId$5).service("preview");
|
|
1067
|
+
const fieldLevelEnabled = strapi2.$ioSettings?.fieldLevelChanges?.enabled !== false;
|
|
1068
|
+
let eventPayload;
|
|
1069
|
+
if (fieldLevelEnabled && previousData && diffService) {
|
|
1070
|
+
eventPayload = diffService.createEventPayload("update", modelInfo, previousData, newData);
|
|
1071
|
+
} else {
|
|
1072
|
+
eventPayload = {
|
|
1073
|
+
event: "update",
|
|
1074
|
+
schema: modelInfo,
|
|
1075
|
+
data: newData
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
strapi2.$io.emit(eventPayload);
|
|
1079
|
+
if (previewService && newData.documentId) {
|
|
1080
|
+
const diff2 = fieldLevelEnabled ? eventPayload.diff : null;
|
|
1081
|
+
previewService.emitDraftChange(uid, newData.documentId, newData, diff2);
|
|
1082
|
+
}
|
|
926
1083
|
} catch (error2) {
|
|
927
1084
|
strapi2.log.debug(`socket.io: Could not emit update event for ${uid}:`, error2.message);
|
|
928
1085
|
}
|
|
@@ -1023,14 +1180,14 @@ var config$1 = {
|
|
|
1023
1180
|
validator(config2) {
|
|
1024
1181
|
}
|
|
1025
1182
|
};
|
|
1026
|
-
const { pluginId: pluginId$
|
|
1183
|
+
const { pluginId: pluginId$4 } = pluginId_1;
|
|
1027
1184
|
var settings$3 = ({ strapi: strapi2 }) => ({
|
|
1028
1185
|
/**
|
|
1029
1186
|
* GET /io/settings
|
|
1030
1187
|
* Retrieve current plugin settings
|
|
1031
1188
|
*/
|
|
1032
1189
|
async getSettings(ctx) {
|
|
1033
|
-
const settingsService = strapi2.plugin(pluginId$
|
|
1190
|
+
const settingsService = strapi2.plugin(pluginId$4).service("settings");
|
|
1034
1191
|
const settings2 = await settingsService.getSettings();
|
|
1035
1192
|
ctx.body = { data: settings2 };
|
|
1036
1193
|
},
|
|
@@ -1039,7 +1196,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
|
|
|
1039
1196
|
* Update plugin settings and hot-reload Socket.IO
|
|
1040
1197
|
*/
|
|
1041
1198
|
async updateSettings(ctx) {
|
|
1042
|
-
const settingsService = strapi2.plugin(pluginId$
|
|
1199
|
+
const settingsService = strapi2.plugin(pluginId$4).service("settings");
|
|
1043
1200
|
const { body } = ctx.request;
|
|
1044
1201
|
await settingsService.getSettings();
|
|
1045
1202
|
const updatedSettings = await settingsService.setSettings(body);
|
|
@@ -1072,7 +1229,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
|
|
|
1072
1229
|
* Get connection and event statistics
|
|
1073
1230
|
*/
|
|
1074
1231
|
async getStats(ctx) {
|
|
1075
|
-
const monitoringService = strapi2.plugin(pluginId$
|
|
1232
|
+
const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
|
|
1076
1233
|
const connectionStats = monitoringService.getConnectionStats();
|
|
1077
1234
|
const eventStats = monitoringService.getEventStats();
|
|
1078
1235
|
ctx.body = {
|
|
@@ -1087,7 +1244,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
|
|
|
1087
1244
|
* Get recent event log
|
|
1088
1245
|
*/
|
|
1089
1246
|
async getEventLog(ctx) {
|
|
1090
|
-
const monitoringService = strapi2.plugin(pluginId$
|
|
1247
|
+
const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
|
|
1091
1248
|
const limit = parseInt(ctx.query.limit) || 50;
|
|
1092
1249
|
const log = monitoringService.getEventLog(limit);
|
|
1093
1250
|
ctx.body = { data: log };
|
|
@@ -1097,7 +1254,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
|
|
|
1097
1254
|
* Send a test event
|
|
1098
1255
|
*/
|
|
1099
1256
|
async sendTestEvent(ctx) {
|
|
1100
|
-
const monitoringService = strapi2.plugin(pluginId$
|
|
1257
|
+
const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
|
|
1101
1258
|
const { eventName, data } = ctx.request.body;
|
|
1102
1259
|
try {
|
|
1103
1260
|
const result = monitoringService.sendTestEvent(eventName || "test", data || {});
|
|
@@ -1111,7 +1268,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
|
|
|
1111
1268
|
* Reset monitoring statistics
|
|
1112
1269
|
*/
|
|
1113
1270
|
async resetStats(ctx) {
|
|
1114
|
-
const monitoringService = strapi2.plugin(pluginId$
|
|
1271
|
+
const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
|
|
1115
1272
|
monitoringService.resetStats();
|
|
1116
1273
|
ctx.body = { data: { success: true } };
|
|
1117
1274
|
},
|
|
@@ -1135,7 +1292,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
|
|
|
1135
1292
|
* Get lightweight stats for dashboard widget
|
|
1136
1293
|
*/
|
|
1137
1294
|
async getMonitoringStats(ctx) {
|
|
1138
|
-
const monitoringService = strapi2.plugin(pluginId$
|
|
1295
|
+
const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
|
|
1139
1296
|
const connectionStats = monitoringService.getConnectionStats();
|
|
1140
1297
|
const eventStats = monitoringService.getEventStats();
|
|
1141
1298
|
ctx.body = {
|
|
@@ -1154,13 +1311,95 @@ var settings$3 = ({ strapi: strapi2 }) => ({
|
|
|
1154
1311
|
};
|
|
1155
1312
|
}
|
|
1156
1313
|
});
|
|
1314
|
+
const { randomUUID } = require$$1__default.default;
|
|
1315
|
+
const sessionTokens = /* @__PURE__ */ new Map();
|
|
1316
|
+
setInterval(() => {
|
|
1317
|
+
const now = Date.now();
|
|
1318
|
+
for (const [token, session] of sessionTokens.entries()) {
|
|
1319
|
+
if (session.expiresAt < now) {
|
|
1320
|
+
sessionTokens.delete(token);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
}, 5 * 60 * 1e3);
|
|
1324
|
+
var presence$3 = ({ strapi: strapi2 }) => ({
|
|
1325
|
+
/**
|
|
1326
|
+
* Creates a session token for admin users to connect to Socket.IO
|
|
1327
|
+
* @param {object} ctx - Koa context
|
|
1328
|
+
*/
|
|
1329
|
+
async createSession(ctx) {
|
|
1330
|
+
const adminUser = ctx.state.user;
|
|
1331
|
+
if (!adminUser) {
|
|
1332
|
+
strapi2.log.warn("[plugin-io] Presence session requested without admin user");
|
|
1333
|
+
return ctx.unauthorized("Admin authentication required");
|
|
1334
|
+
}
|
|
1335
|
+
try {
|
|
1336
|
+
const token = randomUUID();
|
|
1337
|
+
const expiresAt = Date.now() + 2 * 60 * 1e3;
|
|
1338
|
+
sessionTokens.set(token, {
|
|
1339
|
+
token,
|
|
1340
|
+
user: {
|
|
1341
|
+
id: adminUser.id,
|
|
1342
|
+
email: adminUser.email,
|
|
1343
|
+
firstname: adminUser.firstname,
|
|
1344
|
+
lastname: adminUser.lastname
|
|
1345
|
+
},
|
|
1346
|
+
expiresAt
|
|
1347
|
+
});
|
|
1348
|
+
strapi2.log.info(`[plugin-io] Presence session created for admin user: ${adminUser.email}`);
|
|
1349
|
+
ctx.body = {
|
|
1350
|
+
token,
|
|
1351
|
+
user: {
|
|
1352
|
+
id: adminUser.id,
|
|
1353
|
+
email: adminUser.email,
|
|
1354
|
+
firstname: adminUser.firstname,
|
|
1355
|
+
lastname: adminUser.lastname
|
|
1356
|
+
},
|
|
1357
|
+
wsPath: "/socket.io",
|
|
1358
|
+
wsUrl: `${ctx.protocol}://${ctx.host}`
|
|
1359
|
+
};
|
|
1360
|
+
} catch (error2) {
|
|
1361
|
+
strapi2.log.error("[plugin-io] Failed to create presence session:", error2);
|
|
1362
|
+
return ctx.internalServerError("Failed to create session");
|
|
1363
|
+
}
|
|
1364
|
+
},
|
|
1365
|
+
/**
|
|
1366
|
+
* Validates and consumes a session token (one-time use)
|
|
1367
|
+
* @param {string} token - Session token to validate
|
|
1368
|
+
* @returns {object|null} Session data or null if invalid/expired
|
|
1369
|
+
*/
|
|
1370
|
+
consumeSessionToken(token) {
|
|
1371
|
+
if (!token) {
|
|
1372
|
+
return null;
|
|
1373
|
+
}
|
|
1374
|
+
const session = sessionTokens.get(token);
|
|
1375
|
+
if (!session) {
|
|
1376
|
+
return null;
|
|
1377
|
+
}
|
|
1378
|
+
if (session.expiresAt < Date.now()) {
|
|
1379
|
+
sessionTokens.delete(token);
|
|
1380
|
+
return null;
|
|
1381
|
+
}
|
|
1382
|
+
return session;
|
|
1383
|
+
}
|
|
1384
|
+
});
|
|
1157
1385
|
const settings$2 = settings$3;
|
|
1386
|
+
const presence$2 = presence$3;
|
|
1158
1387
|
var controllers$1 = {
|
|
1159
|
-
settings: settings$2
|
|
1388
|
+
settings: settings$2,
|
|
1389
|
+
presence: presence$2
|
|
1160
1390
|
};
|
|
1161
1391
|
var admin$1 = {
|
|
1162
1392
|
type: "admin",
|
|
1163
1393
|
routes: [
|
|
1394
|
+
// Presence Session - issues JWT token for Socket.IO connection
|
|
1395
|
+
{
|
|
1396
|
+
method: "POST",
|
|
1397
|
+
path: "/presence/session",
|
|
1398
|
+
handler: "presence.createSession",
|
|
1399
|
+
config: {
|
|
1400
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
1401
|
+
}
|
|
1402
|
+
},
|
|
1164
1403
|
{
|
|
1165
1404
|
method: "GET",
|
|
1166
1405
|
path: "/settings",
|
|
@@ -21500,9 +21739,9 @@ function padZeros(value, tok, options) {
|
|
|
21500
21739
|
if (!tok.isPadded) {
|
|
21501
21740
|
return value;
|
|
21502
21741
|
}
|
|
21503
|
-
let
|
|
21742
|
+
let diff2 = Math.abs(tok.maxLen - String(value).length);
|
|
21504
21743
|
let relax = options.relaxZeros !== false;
|
|
21505
|
-
switch (
|
|
21744
|
+
switch (diff2) {
|
|
21506
21745
|
case 0:
|
|
21507
21746
|
return "";
|
|
21508
21747
|
case 1:
|
|
@@ -21510,7 +21749,7 @@ function padZeros(value, tok, options) {
|
|
|
21510
21749
|
case 2:
|
|
21511
21750
|
return relax ? "0{0,2}" : "00";
|
|
21512
21751
|
default: {
|
|
21513
|
-
return relax ? `0{0,${
|
|
21752
|
+
return relax ? `0{0,${diff2}}` : `0{${diff2}}`;
|
|
21514
21753
|
}
|
|
21515
21754
|
}
|
|
21516
21755
|
}
|
|
@@ -29433,6 +29672,32 @@ var strategies = ({ strapi: strapi2 }) => {
|
|
|
29433
29672
|
const apiTokenService = getService({ type: "admin", plugin: "api-token" });
|
|
29434
29673
|
const jwtService = getService({ name: "jwt", plugin: "users-permissions" });
|
|
29435
29674
|
const userService = getService({ name: "user", plugin: "users-permissions" });
|
|
29675
|
+
const admin2 = {
|
|
29676
|
+
name: "io-admin",
|
|
29677
|
+
credentials: function(user) {
|
|
29678
|
+
return `${this.name}-${user.id}`;
|
|
29679
|
+
},
|
|
29680
|
+
authenticate: async function(auth) {
|
|
29681
|
+
const token2 = auth.token;
|
|
29682
|
+
if (!token2) {
|
|
29683
|
+
throw new UnauthorizedError2("Invalid admin credentials");
|
|
29684
|
+
}
|
|
29685
|
+
try {
|
|
29686
|
+
const presenceController = strapi2.plugin("io").controller("presence");
|
|
29687
|
+
const session = presenceController.consumeSessionToken(token2);
|
|
29688
|
+
if (!session) {
|
|
29689
|
+
throw new UnauthorizedError2("Invalid or expired session token");
|
|
29690
|
+
}
|
|
29691
|
+
return session.user;
|
|
29692
|
+
} catch (error2) {
|
|
29693
|
+
strapi2.log.warn("[plugin-io] Admin session verification failed:", error2.message);
|
|
29694
|
+
throw new UnauthorizedError2("Invalid admin credentials");
|
|
29695
|
+
}
|
|
29696
|
+
},
|
|
29697
|
+
getRoomName: function(user) {
|
|
29698
|
+
return `${this.name}-user-${user.id}`;
|
|
29699
|
+
}
|
|
29700
|
+
};
|
|
29436
29701
|
const role = {
|
|
29437
29702
|
name: "io-role",
|
|
29438
29703
|
credentials: function(role2) {
|
|
@@ -29575,6 +29840,7 @@ var strategies = ({ strapi: strapi2 }) => {
|
|
|
29575
29840
|
}
|
|
29576
29841
|
};
|
|
29577
29842
|
return {
|
|
29843
|
+
admin: admin2,
|
|
29578
29844
|
role,
|
|
29579
29845
|
token
|
|
29580
29846
|
};
|
|
@@ -29668,12 +29934,12 @@ function transformEntry(entry, type2) {
|
|
|
29668
29934
|
// meta: {},
|
|
29669
29935
|
};
|
|
29670
29936
|
}
|
|
29671
|
-
const { pluginId: pluginId$
|
|
29937
|
+
const { pluginId: pluginId$3 } = pluginId_1;
|
|
29672
29938
|
var settings$1 = ({ strapi: strapi2 }) => {
|
|
29673
29939
|
const getPluginStore = () => {
|
|
29674
29940
|
return strapi2.store({
|
|
29675
29941
|
type: "plugin",
|
|
29676
|
-
name: pluginId$
|
|
29942
|
+
name: pluginId$3
|
|
29677
29943
|
});
|
|
29678
29944
|
};
|
|
29679
29945
|
const getDefaultSettings = () => ({
|
|
@@ -29776,6 +30042,41 @@ var settings$1 = ({ strapi: strapi2 }) => {
|
|
|
29776
30042
|
enableConnectionLogging: true,
|
|
29777
30043
|
enableEventLogging: false,
|
|
29778
30044
|
maxEventLogSize: 100
|
|
30045
|
+
},
|
|
30046
|
+
// Presence System (Collaboration Awareness)
|
|
30047
|
+
presence: {
|
|
30048
|
+
enabled: true,
|
|
30049
|
+
// Enable presence tracking
|
|
30050
|
+
heartbeatInterval: 3e4,
|
|
30051
|
+
// Heartbeat interval in ms
|
|
30052
|
+
staleTimeout: 6e4,
|
|
30053
|
+
// Time before connection considered stale
|
|
30054
|
+
showAvatars: true,
|
|
30055
|
+
// Show user avatars in UI
|
|
30056
|
+
showTypingIndicator: true
|
|
30057
|
+
// Show typing indicators
|
|
30058
|
+
},
|
|
30059
|
+
// Live Preview (Real-time Draft Updates)
|
|
30060
|
+
livePreview: {
|
|
30061
|
+
enabled: true,
|
|
30062
|
+
// Enable live preview
|
|
30063
|
+
draftEvents: true,
|
|
30064
|
+
// Emit events for draft changes
|
|
30065
|
+
debounceMs: 300,
|
|
30066
|
+
// Debounce field changes
|
|
30067
|
+
maxSubscriptionsPerSocket: 50
|
|
30068
|
+
// Max preview subscriptions per socket
|
|
30069
|
+
},
|
|
30070
|
+
// Field-level Changes (Diff-based Updates)
|
|
30071
|
+
fieldLevelChanges: {
|
|
30072
|
+
enabled: true,
|
|
30073
|
+
// Enable field-level diff
|
|
30074
|
+
includeFullData: false,
|
|
30075
|
+
// Include full data alongside diff
|
|
30076
|
+
excludeFields: ["updatedAt", "updatedBy", "createdAt", "createdBy"],
|
|
30077
|
+
// Fields to exclude from diff
|
|
30078
|
+
maxDiffDepth: 3
|
|
30079
|
+
// Maximum nesting depth for diff
|
|
29779
30080
|
}
|
|
29780
30081
|
});
|
|
29781
30082
|
return {
|
|
@@ -29816,7 +30117,7 @@ var settings$1 = ({ strapi: strapi2 }) => {
|
|
|
29816
30117
|
getDefaultSettings
|
|
29817
30118
|
};
|
|
29818
30119
|
};
|
|
29819
|
-
const { pluginId } = pluginId_1;
|
|
30120
|
+
const { pluginId: pluginId$2 } = pluginId_1;
|
|
29820
30121
|
var monitoring$1 = ({ strapi: strapi2 }) => {
|
|
29821
30122
|
let eventLog = [];
|
|
29822
30123
|
let eventStats = {
|
|
@@ -29960,17 +30261,795 @@ var monitoring$1 = ({ strapi: strapi2 }) => {
|
|
|
29960
30261
|
}
|
|
29961
30262
|
};
|
|
29962
30263
|
};
|
|
30264
|
+
const { pluginId: pluginId$1 } = pluginId_1;
|
|
30265
|
+
var presence$1 = ({ strapi: strapi2 }) => {
|
|
30266
|
+
const activeConnections = /* @__PURE__ */ new Map();
|
|
30267
|
+
const entityEditors = /* @__PURE__ */ new Map();
|
|
30268
|
+
let cleanupInterval = null;
|
|
30269
|
+
const getEntityKey = (uid, documentId) => `${uid}:${documentId}`;
|
|
30270
|
+
const getPresenceSettings = () => {
|
|
30271
|
+
const settings2 = strapi2.$ioSettings || {};
|
|
30272
|
+
return {
|
|
30273
|
+
enabled: settings2.presence?.enabled ?? true,
|
|
30274
|
+
heartbeatInterval: settings2.presence?.heartbeatInterval ?? 3e4,
|
|
30275
|
+
staleTimeout: settings2.presence?.staleTimeout ?? 6e4,
|
|
30276
|
+
showAvatars: settings2.presence?.showAvatars ?? true
|
|
30277
|
+
};
|
|
30278
|
+
};
|
|
30279
|
+
const broadcastPresenceUpdate = async (uid, documentId) => {
|
|
30280
|
+
const io2 = strapi2.$io?.server;
|
|
30281
|
+
if (!io2) return;
|
|
30282
|
+
const entityKey = getEntityKey(uid, documentId);
|
|
30283
|
+
const editorSocketIds = entityEditors.get(entityKey) || /* @__PURE__ */ new Set();
|
|
30284
|
+
const editors = [];
|
|
30285
|
+
for (const socketId of editorSocketIds) {
|
|
30286
|
+
const connection = activeConnections.get(socketId);
|
|
30287
|
+
if (connection?.user) {
|
|
30288
|
+
editors.push({
|
|
30289
|
+
socketId,
|
|
30290
|
+
user: {
|
|
30291
|
+
id: connection.user.id,
|
|
30292
|
+
username: connection.user.username,
|
|
30293
|
+
email: connection.user.email,
|
|
30294
|
+
firstname: connection.user.firstname,
|
|
30295
|
+
lastname: connection.user.lastname
|
|
30296
|
+
},
|
|
30297
|
+
joinedAt: connection.entities?.get(entityKey) || Date.now()
|
|
30298
|
+
});
|
|
30299
|
+
}
|
|
30300
|
+
}
|
|
30301
|
+
const roomName = `presence:${entityKey}`;
|
|
30302
|
+
io2.to(roomName).emit("presence:update", {
|
|
30303
|
+
uid,
|
|
30304
|
+
documentId,
|
|
30305
|
+
editors,
|
|
30306
|
+
count: editors.length,
|
|
30307
|
+
timestamp: Date.now()
|
|
30308
|
+
});
|
|
30309
|
+
strapi2.log.debug(`socket.io: Presence update for ${entityKey} - ${editors.length} editor(s)`);
|
|
30310
|
+
};
|
|
30311
|
+
return {
|
|
30312
|
+
/**
|
|
30313
|
+
* Registers a new socket connection for presence tracking
|
|
30314
|
+
* @param {string} socketId - Socket ID
|
|
30315
|
+
* @param {object} user - User object (can be null for anonymous)
|
|
30316
|
+
*/
|
|
30317
|
+
registerConnection(socketId, user = null) {
|
|
30318
|
+
const settings2 = getPresenceSettings();
|
|
30319
|
+
if (!settings2.enabled) return;
|
|
30320
|
+
activeConnections.set(socketId, {
|
|
30321
|
+
user,
|
|
30322
|
+
entities: /* @__PURE__ */ new Map(),
|
|
30323
|
+
// entityKey -> joinedAt timestamp
|
|
30324
|
+
lastSeen: Date.now(),
|
|
30325
|
+
connectedAt: Date.now()
|
|
30326
|
+
});
|
|
30327
|
+
strapi2.log.debug(`socket.io: Presence registered for socket ${socketId}`);
|
|
30328
|
+
},
|
|
30329
|
+
/**
|
|
30330
|
+
* Unregisters a socket connection and cleans up all entity presence
|
|
30331
|
+
* @param {string} socketId - Socket ID
|
|
30332
|
+
*/
|
|
30333
|
+
async unregisterConnection(socketId) {
|
|
30334
|
+
const connection = activeConnections.get(socketId);
|
|
30335
|
+
if (!connection) return;
|
|
30336
|
+
if (connection.entities) {
|
|
30337
|
+
for (const entityKey of connection.entities.keys()) {
|
|
30338
|
+
const [uid, documentId] = entityKey.split(":");
|
|
30339
|
+
await this.leaveEntity(socketId, uid, documentId, false);
|
|
30340
|
+
}
|
|
30341
|
+
}
|
|
30342
|
+
activeConnections.delete(socketId);
|
|
30343
|
+
strapi2.log.debug(`socket.io: Presence unregistered for socket ${socketId}`);
|
|
30344
|
+
},
|
|
30345
|
+
/**
|
|
30346
|
+
* User joins an entity for editing
|
|
30347
|
+
* @param {string} socketId - Socket ID
|
|
30348
|
+
* @param {string} uid - Content type UID
|
|
30349
|
+
* @param {string} documentId - Document ID
|
|
30350
|
+
* @returns {object} Join result with current editors
|
|
30351
|
+
*/
|
|
30352
|
+
async joinEntity(socketId, uid, documentId) {
|
|
30353
|
+
const settings2 = getPresenceSettings();
|
|
30354
|
+
if (!settings2.enabled) {
|
|
30355
|
+
return { success: false, error: "Presence is disabled" };
|
|
30356
|
+
}
|
|
30357
|
+
const connection = activeConnections.get(socketId);
|
|
30358
|
+
if (!connection) {
|
|
30359
|
+
return { success: false, error: "Socket not registered for presence" };
|
|
30360
|
+
}
|
|
30361
|
+
const entityKey = getEntityKey(uid, documentId);
|
|
30362
|
+
if (!entityEditors.has(entityKey)) {
|
|
30363
|
+
entityEditors.set(entityKey, /* @__PURE__ */ new Set());
|
|
30364
|
+
}
|
|
30365
|
+
entityEditors.get(entityKey).add(socketId);
|
|
30366
|
+
connection.entities.set(entityKey, Date.now());
|
|
30367
|
+
connection.lastSeen = Date.now();
|
|
30368
|
+
const io2 = strapi2.$io?.server;
|
|
30369
|
+
const socket = io2?.sockets.sockets.get(socketId);
|
|
30370
|
+
if (socket) {
|
|
30371
|
+
socket.join(`presence:${entityKey}`);
|
|
30372
|
+
}
|
|
30373
|
+
await broadcastPresenceUpdate(uid, documentId);
|
|
30374
|
+
strapi2.log.info(`socket.io: User ${connection.user?.username || "anonymous"} joined entity ${entityKey}`);
|
|
30375
|
+
return {
|
|
30376
|
+
success: true,
|
|
30377
|
+
entityKey,
|
|
30378
|
+
editors: await this.getEntityEditors(uid, documentId)
|
|
30379
|
+
};
|
|
30380
|
+
},
|
|
30381
|
+
/**
|
|
30382
|
+
* User leaves an entity
|
|
30383
|
+
* @param {string} socketId - Socket ID
|
|
30384
|
+
* @param {string} uid - Content type UID
|
|
30385
|
+
* @param {string} documentId - Document ID
|
|
30386
|
+
* @param {boolean} broadcast - Whether to broadcast update (default: true)
|
|
30387
|
+
* @returns {object} Leave result
|
|
30388
|
+
*/
|
|
30389
|
+
async leaveEntity(socketId, uid, documentId, broadcast = true) {
|
|
30390
|
+
const settings2 = getPresenceSettings();
|
|
30391
|
+
if (!settings2.enabled) {
|
|
30392
|
+
return { success: false, error: "Presence is disabled" };
|
|
30393
|
+
}
|
|
30394
|
+
const entityKey = getEntityKey(uid, documentId);
|
|
30395
|
+
const connection = activeConnections.get(socketId);
|
|
30396
|
+
const editors = entityEditors.get(entityKey);
|
|
30397
|
+
if (editors) {
|
|
30398
|
+
editors.delete(socketId);
|
|
30399
|
+
if (editors.size === 0) {
|
|
30400
|
+
entityEditors.delete(entityKey);
|
|
30401
|
+
}
|
|
30402
|
+
}
|
|
30403
|
+
if (connection?.entities) {
|
|
30404
|
+
connection.entities.delete(entityKey);
|
|
30405
|
+
}
|
|
30406
|
+
const io2 = strapi2.$io?.server;
|
|
30407
|
+
const socket = io2?.sockets.sockets.get(socketId);
|
|
30408
|
+
if (socket) {
|
|
30409
|
+
socket.leave(`presence:${entityKey}`);
|
|
30410
|
+
}
|
|
30411
|
+
if (broadcast) {
|
|
30412
|
+
await broadcastPresenceUpdate(uid, documentId);
|
|
30413
|
+
}
|
|
30414
|
+
strapi2.log.debug(`socket.io: Socket ${socketId} left entity ${entityKey}`);
|
|
30415
|
+
return { success: true, entityKey };
|
|
30416
|
+
},
|
|
30417
|
+
/**
|
|
30418
|
+
* Gets all editors currently editing an entity
|
|
30419
|
+
* @param {string} uid - Content type UID
|
|
30420
|
+
* @param {string} documentId - Document ID
|
|
30421
|
+
* @returns {Array} List of editors with user info
|
|
30422
|
+
*/
|
|
30423
|
+
async getEntityEditors(uid, documentId) {
|
|
30424
|
+
const entityKey = getEntityKey(uid, documentId);
|
|
30425
|
+
const editorSocketIds = entityEditors.get(entityKey) || /* @__PURE__ */ new Set();
|
|
30426
|
+
const editors = [];
|
|
30427
|
+
for (const socketId of editorSocketIds) {
|
|
30428
|
+
const connection = activeConnections.get(socketId);
|
|
30429
|
+
if (connection?.user) {
|
|
30430
|
+
editors.push({
|
|
30431
|
+
socketId,
|
|
30432
|
+
user: {
|
|
30433
|
+
id: connection.user.id,
|
|
30434
|
+
username: connection.user.username,
|
|
30435
|
+
email: connection.user.email,
|
|
30436
|
+
firstname: connection.user.firstname,
|
|
30437
|
+
lastname: connection.user.lastname
|
|
30438
|
+
},
|
|
30439
|
+
joinedAt: connection.entities?.get(entityKey) || Date.now()
|
|
30440
|
+
});
|
|
30441
|
+
}
|
|
30442
|
+
}
|
|
30443
|
+
return editors;
|
|
30444
|
+
},
|
|
30445
|
+
/**
|
|
30446
|
+
* Updates heartbeat for a socket to keep presence alive
|
|
30447
|
+
* @param {string} socketId - Socket ID
|
|
30448
|
+
* @returns {object} Heartbeat result
|
|
30449
|
+
*/
|
|
30450
|
+
heartbeat(socketId) {
|
|
30451
|
+
const connection = activeConnections.get(socketId);
|
|
30452
|
+
if (!connection) {
|
|
30453
|
+
return { success: false, error: "Socket not registered" };
|
|
30454
|
+
}
|
|
30455
|
+
connection.lastSeen = Date.now();
|
|
30456
|
+
return { success: true, lastSeen: connection.lastSeen };
|
|
30457
|
+
},
|
|
30458
|
+
/**
|
|
30459
|
+
* Cleans up stale connections that haven't sent heartbeat
|
|
30460
|
+
* @returns {number} Number of connections cleaned up
|
|
30461
|
+
*/
|
|
30462
|
+
async cleanup() {
|
|
30463
|
+
const settings2 = getPresenceSettings();
|
|
30464
|
+
const staleTimeout = settings2.staleTimeout;
|
|
30465
|
+
const now = Date.now();
|
|
30466
|
+
let cleanedUp = 0;
|
|
30467
|
+
for (const [socketId, connection] of activeConnections) {
|
|
30468
|
+
if (now - connection.lastSeen > staleTimeout) {
|
|
30469
|
+
await this.unregisterConnection(socketId);
|
|
30470
|
+
cleanedUp++;
|
|
30471
|
+
}
|
|
30472
|
+
}
|
|
30473
|
+
if (cleanedUp > 0) {
|
|
30474
|
+
strapi2.log.info(`socket.io: Presence cleanup removed ${cleanedUp} stale connection(s)`);
|
|
30475
|
+
}
|
|
30476
|
+
return cleanedUp;
|
|
30477
|
+
},
|
|
30478
|
+
/**
|
|
30479
|
+
* Starts the cleanup interval
|
|
30480
|
+
*/
|
|
30481
|
+
startCleanupInterval() {
|
|
30482
|
+
const settings2 = getPresenceSettings();
|
|
30483
|
+
if (!settings2.enabled) return;
|
|
30484
|
+
cleanupInterval = setInterval(() => {
|
|
30485
|
+
this.cleanup();
|
|
30486
|
+
}, 6e4);
|
|
30487
|
+
strapi2.log.debug("socket.io: Presence cleanup interval started");
|
|
30488
|
+
},
|
|
30489
|
+
/**
|
|
30490
|
+
* Stops the cleanup interval
|
|
30491
|
+
*/
|
|
30492
|
+
stopCleanupInterval() {
|
|
30493
|
+
if (cleanupInterval) {
|
|
30494
|
+
clearInterval(cleanupInterval);
|
|
30495
|
+
cleanupInterval = null;
|
|
30496
|
+
}
|
|
30497
|
+
},
|
|
30498
|
+
/**
|
|
30499
|
+
* Gets presence statistics
|
|
30500
|
+
* @returns {object} Presence stats
|
|
30501
|
+
*/
|
|
30502
|
+
getStats() {
|
|
30503
|
+
const totalConnections = activeConnections.size;
|
|
30504
|
+
const totalEntitiesBeingEdited = entityEditors.size;
|
|
30505
|
+
let authenticated = 0;
|
|
30506
|
+
let anonymous = 0;
|
|
30507
|
+
for (const connection of activeConnections.values()) {
|
|
30508
|
+
if (connection.user) {
|
|
30509
|
+
authenticated++;
|
|
30510
|
+
} else {
|
|
30511
|
+
anonymous++;
|
|
30512
|
+
}
|
|
30513
|
+
}
|
|
30514
|
+
return {
|
|
30515
|
+
totalConnections,
|
|
30516
|
+
authenticated,
|
|
30517
|
+
anonymous,
|
|
30518
|
+
totalEntitiesBeingEdited,
|
|
30519
|
+
entities: Array.from(entityEditors.entries()).map(([key, editors]) => ({
|
|
30520
|
+
entityKey: key,
|
|
30521
|
+
editorCount: editors.size
|
|
30522
|
+
}))
|
|
30523
|
+
};
|
|
30524
|
+
},
|
|
30525
|
+
/**
|
|
30526
|
+
* Gets all entities a user is currently editing
|
|
30527
|
+
* @param {string} socketId - Socket ID
|
|
30528
|
+
* @returns {Array} List of entity keys
|
|
30529
|
+
*/
|
|
30530
|
+
getUserEntities(socketId) {
|
|
30531
|
+
const connection = activeConnections.get(socketId);
|
|
30532
|
+
if (!connection) return [];
|
|
30533
|
+
return Array.from(connection.entities.keys());
|
|
30534
|
+
},
|
|
30535
|
+
/**
|
|
30536
|
+
* Checks if an entity is being edited by anyone
|
|
30537
|
+
* @param {string} uid - Content type UID
|
|
30538
|
+
* @param {string} documentId - Document ID
|
|
30539
|
+
* @returns {boolean} True if entity has editors
|
|
30540
|
+
*/
|
|
30541
|
+
isEntityBeingEdited(uid, documentId) {
|
|
30542
|
+
const entityKey = getEntityKey(uid, documentId);
|
|
30543
|
+
const editors = entityEditors.get(entityKey);
|
|
30544
|
+
return editors ? editors.size > 0 : false;
|
|
30545
|
+
},
|
|
30546
|
+
/**
|
|
30547
|
+
* Broadcasts a typing indicator for an entity
|
|
30548
|
+
* @param {string} socketId - Socket ID of typing user
|
|
30549
|
+
* @param {string} uid - Content type UID
|
|
30550
|
+
* @param {string} documentId - Document ID
|
|
30551
|
+
* @param {string} fieldName - Name of field being edited
|
|
30552
|
+
*/
|
|
30553
|
+
broadcastTyping(socketId, uid, documentId, fieldName) {
|
|
30554
|
+
const io2 = strapi2.$io?.server;
|
|
30555
|
+
if (!io2) return;
|
|
30556
|
+
const connection = activeConnections.get(socketId);
|
|
30557
|
+
if (!connection?.user) return;
|
|
30558
|
+
const entityKey = getEntityKey(uid, documentId);
|
|
30559
|
+
const roomName = `presence:${entityKey}`;
|
|
30560
|
+
const socket = io2.sockets.sockets.get(socketId);
|
|
30561
|
+
if (socket) {
|
|
30562
|
+
socket.to(roomName).emit("presence:typing", {
|
|
30563
|
+
uid,
|
|
30564
|
+
documentId,
|
|
30565
|
+
user: {
|
|
30566
|
+
id: connection.user.id,
|
|
30567
|
+
username: connection.user.username
|
|
30568
|
+
},
|
|
30569
|
+
fieldName,
|
|
30570
|
+
timestamp: Date.now()
|
|
30571
|
+
});
|
|
30572
|
+
}
|
|
30573
|
+
}
|
|
30574
|
+
};
|
|
30575
|
+
};
|
|
30576
|
+
const { pluginId } = pluginId_1;
|
|
30577
|
+
var preview$1 = ({ strapi: strapi2 }) => {
|
|
30578
|
+
const previewSubscribers = /* @__PURE__ */ new Map();
|
|
30579
|
+
const socketState = /* @__PURE__ */ new Map();
|
|
30580
|
+
const getEntityKey = (uid, documentId) => `${uid}:${documentId}`;
|
|
30581
|
+
const getPreviewSettings = () => {
|
|
30582
|
+
const settings2 = strapi2.$ioSettings || {};
|
|
30583
|
+
return {
|
|
30584
|
+
enabled: settings2.livePreview?.enabled ?? true,
|
|
30585
|
+
draftEvents: settings2.livePreview?.draftEvents ?? true,
|
|
30586
|
+
debounceMs: settings2.livePreview?.debounceMs ?? 300,
|
|
30587
|
+
maxSubscriptionsPerSocket: settings2.livePreview?.maxSubscriptionsPerSocket ?? 50
|
|
30588
|
+
};
|
|
30589
|
+
};
|
|
30590
|
+
const emitToSubscribers = (uid, documentId, eventType, data) => {
|
|
30591
|
+
const io2 = strapi2.$io?.server;
|
|
30592
|
+
if (!io2) return;
|
|
30593
|
+
const entityKey = getEntityKey(uid, documentId);
|
|
30594
|
+
const subscribers = previewSubscribers.get(entityKey);
|
|
30595
|
+
if (!subscribers || subscribers.size === 0) return;
|
|
30596
|
+
const roomName = `preview:${entityKey}`;
|
|
30597
|
+
io2.to(roomName).emit(eventType, {
|
|
30598
|
+
uid,
|
|
30599
|
+
documentId,
|
|
30600
|
+
...data,
|
|
30601
|
+
timestamp: Date.now()
|
|
30602
|
+
});
|
|
30603
|
+
strapi2.log.debug(`socket.io: Preview event '${eventType}' sent to ${subscribers.size} subscriber(s) for ${entityKey}`);
|
|
30604
|
+
};
|
|
30605
|
+
return {
|
|
30606
|
+
/**
|
|
30607
|
+
* Subscribes a socket to preview updates for an entity
|
|
30608
|
+
* @param {string} socketId - Socket ID
|
|
30609
|
+
* @param {string} uid - Content type UID
|
|
30610
|
+
* @param {string} documentId - Document ID
|
|
30611
|
+
* @returns {object} Subscription result
|
|
30612
|
+
*/
|
|
30613
|
+
async subscribe(socketId, uid, documentId) {
|
|
30614
|
+
const settings2 = getPreviewSettings();
|
|
30615
|
+
if (!settings2.enabled) {
|
|
30616
|
+
return { success: false, error: "Live preview is disabled" };
|
|
30617
|
+
}
|
|
30618
|
+
const entityKey = getEntityKey(uid, documentId);
|
|
30619
|
+
const io2 = strapi2.$io?.server;
|
|
30620
|
+
const socket = io2?.sockets.sockets.get(socketId);
|
|
30621
|
+
if (!socket) {
|
|
30622
|
+
return { success: false, error: "Socket not found" };
|
|
30623
|
+
}
|
|
30624
|
+
const currentSubs = Array.from(socket.rooms).filter((r) => r.startsWith("preview:")).length;
|
|
30625
|
+
if (currentSubs >= settings2.maxSubscriptionsPerSocket) {
|
|
30626
|
+
return { success: false, error: `Maximum preview subscriptions (${settings2.maxSubscriptionsPerSocket}) reached` };
|
|
30627
|
+
}
|
|
30628
|
+
if (!previewSubscribers.has(entityKey)) {
|
|
30629
|
+
previewSubscribers.set(entityKey, /* @__PURE__ */ new Set());
|
|
30630
|
+
}
|
|
30631
|
+
previewSubscribers.get(entityKey).add(socketId);
|
|
30632
|
+
socket.join(`preview:${entityKey}`);
|
|
30633
|
+
if (!socketState.has(socketId)) {
|
|
30634
|
+
socketState.set(socketId, { debounceTimers: /* @__PURE__ */ new Map() });
|
|
30635
|
+
}
|
|
30636
|
+
strapi2.log.debug(`socket.io: Socket ${socketId} subscribed to preview for ${entityKey}`);
|
|
30637
|
+
try {
|
|
30638
|
+
const entity = await strapi2.documents(uid).findOne({ documentId });
|
|
30639
|
+
if (entity) {
|
|
30640
|
+
socket.emit("preview:initial", {
|
|
30641
|
+
uid,
|
|
30642
|
+
documentId,
|
|
30643
|
+
data: entity,
|
|
30644
|
+
timestamp: Date.now()
|
|
30645
|
+
});
|
|
30646
|
+
}
|
|
30647
|
+
} catch (err) {
|
|
30648
|
+
strapi2.log.warn(`socket.io: Could not fetch initial preview data for ${entityKey}: ${err.message}`);
|
|
30649
|
+
}
|
|
30650
|
+
return {
|
|
30651
|
+
success: true,
|
|
30652
|
+
entityKey,
|
|
30653
|
+
subscriberCount: previewSubscribers.get(entityKey).size
|
|
30654
|
+
};
|
|
30655
|
+
},
|
|
30656
|
+
/**
|
|
30657
|
+
* Unsubscribes a socket from preview updates
|
|
30658
|
+
* @param {string} socketId - Socket ID
|
|
30659
|
+
* @param {string} uid - Content type UID
|
|
30660
|
+
* @param {string} documentId - Document ID
|
|
30661
|
+
* @returns {object} Unsubscription result
|
|
30662
|
+
*/
|
|
30663
|
+
unsubscribe(socketId, uid, documentId) {
|
|
30664
|
+
const entityKey = getEntityKey(uid, documentId);
|
|
30665
|
+
const subscribers = previewSubscribers.get(entityKey);
|
|
30666
|
+
if (subscribers) {
|
|
30667
|
+
subscribers.delete(socketId);
|
|
30668
|
+
if (subscribers.size === 0) {
|
|
30669
|
+
previewSubscribers.delete(entityKey);
|
|
30670
|
+
}
|
|
30671
|
+
}
|
|
30672
|
+
const io2 = strapi2.$io?.server;
|
|
30673
|
+
const socket = io2?.sockets.sockets.get(socketId);
|
|
30674
|
+
if (socket) {
|
|
30675
|
+
socket.leave(`preview:${entityKey}`);
|
|
30676
|
+
}
|
|
30677
|
+
const state = socketState.get(socketId);
|
|
30678
|
+
if (state?.debounceTimers.has(entityKey)) {
|
|
30679
|
+
clearTimeout(state.debounceTimers.get(entityKey));
|
|
30680
|
+
state.debounceTimers.delete(entityKey);
|
|
30681
|
+
}
|
|
30682
|
+
strapi2.log.debug(`socket.io: Socket ${socketId} unsubscribed from preview for ${entityKey}`);
|
|
30683
|
+
return { success: true, entityKey };
|
|
30684
|
+
},
|
|
30685
|
+
/**
|
|
30686
|
+
* Cleans up all subscriptions for a socket
|
|
30687
|
+
* @param {string} socketId - Socket ID
|
|
30688
|
+
*/
|
|
30689
|
+
cleanupSocket(socketId) {
|
|
30690
|
+
for (const [entityKey, subscribers] of previewSubscribers) {
|
|
30691
|
+
if (subscribers.has(socketId)) {
|
|
30692
|
+
subscribers.delete(socketId);
|
|
30693
|
+
if (subscribers.size === 0) {
|
|
30694
|
+
previewSubscribers.delete(entityKey);
|
|
30695
|
+
}
|
|
30696
|
+
}
|
|
30697
|
+
}
|
|
30698
|
+
const state = socketState.get(socketId);
|
|
30699
|
+
if (state) {
|
|
30700
|
+
for (const timerId of state.debounceTimers.values()) {
|
|
30701
|
+
clearTimeout(timerId);
|
|
30702
|
+
}
|
|
30703
|
+
socketState.delete(socketId);
|
|
30704
|
+
}
|
|
30705
|
+
},
|
|
30706
|
+
/**
|
|
30707
|
+
* Emits a draft change event to preview subscribers
|
|
30708
|
+
* @param {string} uid - Content type UID
|
|
30709
|
+
* @param {string} documentId - Document ID
|
|
30710
|
+
* @param {object} data - Changed data
|
|
30711
|
+
* @param {object} diff - Field-level diff (optional)
|
|
30712
|
+
*/
|
|
30713
|
+
emitDraftChange(uid, documentId, data, diff2 = null) {
|
|
30714
|
+
const settings2 = getPreviewSettings();
|
|
30715
|
+
if (!settings2.enabled || !settings2.draftEvents) return;
|
|
30716
|
+
emitToSubscribers(uid, documentId, "preview:change", {
|
|
30717
|
+
data,
|
|
30718
|
+
diff: diff2,
|
|
30719
|
+
isDraft: true
|
|
30720
|
+
});
|
|
30721
|
+
},
|
|
30722
|
+
/**
|
|
30723
|
+
* Emits a debounced field change event
|
|
30724
|
+
* @param {string} socketId - Socket ID of the editor
|
|
30725
|
+
* @param {string} uid - Content type UID
|
|
30726
|
+
* @param {string} documentId - Document ID
|
|
30727
|
+
* @param {string} fieldName - Name of changed field
|
|
30728
|
+
* @param {*} value - New field value
|
|
30729
|
+
*/
|
|
30730
|
+
emitFieldChange(socketId, uid, documentId, fieldName, value) {
|
|
30731
|
+
const settings2 = getPreviewSettings();
|
|
30732
|
+
if (!settings2.enabled) return;
|
|
30733
|
+
const entityKey = getEntityKey(uid, documentId);
|
|
30734
|
+
const state = socketState.get(socketId);
|
|
30735
|
+
if (state?.debounceTimers.has(entityKey)) {
|
|
30736
|
+
clearTimeout(state.debounceTimers.get(entityKey));
|
|
30737
|
+
}
|
|
30738
|
+
const timerId = setTimeout(() => {
|
|
30739
|
+
emitToSubscribers(uid, documentId, "preview:field", {
|
|
30740
|
+
fieldName,
|
|
30741
|
+
value,
|
|
30742
|
+
editorSocketId: socketId
|
|
30743
|
+
});
|
|
30744
|
+
state?.debounceTimers.delete(entityKey);
|
|
30745
|
+
}, settings2.debounceMs);
|
|
30746
|
+
if (state) {
|
|
30747
|
+
state.debounceTimers.set(entityKey, timerId);
|
|
30748
|
+
}
|
|
30749
|
+
},
|
|
30750
|
+
/**
|
|
30751
|
+
* Emits publish event to preview subscribers
|
|
30752
|
+
* @param {string} uid - Content type UID
|
|
30753
|
+
* @param {string} documentId - Document ID
|
|
30754
|
+
* @param {object} data - Published data
|
|
30755
|
+
*/
|
|
30756
|
+
emitPublish(uid, documentId, data) {
|
|
30757
|
+
emitToSubscribers(uid, documentId, "preview:publish", {
|
|
30758
|
+
data,
|
|
30759
|
+
isDraft: false
|
|
30760
|
+
});
|
|
30761
|
+
},
|
|
30762
|
+
/**
|
|
30763
|
+
* Emits unpublish event to preview subscribers
|
|
30764
|
+
* @param {string} uid - Content type UID
|
|
30765
|
+
* @param {string} documentId - Document ID
|
|
30766
|
+
*/
|
|
30767
|
+
emitUnpublish(uid, documentId) {
|
|
30768
|
+
emitToSubscribers(uid, documentId, "preview:unpublish", {
|
|
30769
|
+
isDraft: true
|
|
30770
|
+
});
|
|
30771
|
+
},
|
|
30772
|
+
/**
|
|
30773
|
+
* Gets the number of preview subscribers for an entity
|
|
30774
|
+
* @param {string} uid - Content type UID
|
|
30775
|
+
* @param {string} documentId - Document ID
|
|
30776
|
+
* @returns {number} Subscriber count
|
|
30777
|
+
*/
|
|
30778
|
+
getSubscriberCount(uid, documentId) {
|
|
30779
|
+
const entityKey = getEntityKey(uid, documentId);
|
|
30780
|
+
return previewSubscribers.get(entityKey)?.size || 0;
|
|
30781
|
+
},
|
|
30782
|
+
/**
|
|
30783
|
+
* Gets all entities with active preview subscribers
|
|
30784
|
+
* @returns {Array} List of entity keys with subscriber counts
|
|
30785
|
+
*/
|
|
30786
|
+
getActivePreviewEntities() {
|
|
30787
|
+
const entities = [];
|
|
30788
|
+
for (const [entityKey, subscribers] of previewSubscribers) {
|
|
30789
|
+
const [uid, documentId] = entityKey.split(":");
|
|
30790
|
+
entities.push({
|
|
30791
|
+
uid,
|
|
30792
|
+
documentId,
|
|
30793
|
+
entityKey,
|
|
30794
|
+
subscriberCount: subscribers.size
|
|
30795
|
+
});
|
|
30796
|
+
}
|
|
30797
|
+
return entities;
|
|
30798
|
+
},
|
|
30799
|
+
/**
|
|
30800
|
+
* Checks if live preview is enabled
|
|
30801
|
+
* @returns {boolean} True if enabled
|
|
30802
|
+
*/
|
|
30803
|
+
isEnabled() {
|
|
30804
|
+
return getPreviewSettings().enabled;
|
|
30805
|
+
},
|
|
30806
|
+
/**
|
|
30807
|
+
* Gets preview statistics
|
|
30808
|
+
* @returns {object} Preview stats
|
|
30809
|
+
*/
|
|
30810
|
+
getStats() {
|
|
30811
|
+
let totalSubscriptions = 0;
|
|
30812
|
+
for (const subscribers of previewSubscribers.values()) {
|
|
30813
|
+
totalSubscriptions += subscribers.size;
|
|
30814
|
+
}
|
|
30815
|
+
return {
|
|
30816
|
+
totalEntitiesWithSubscribers: previewSubscribers.size,
|
|
30817
|
+
totalSubscriptions,
|
|
30818
|
+
entities: this.getActivePreviewEntities()
|
|
30819
|
+
};
|
|
30820
|
+
}
|
|
30821
|
+
};
|
|
30822
|
+
};
|
|
30823
|
+
var diff$1 = ({ strapi: strapi2 }) => {
|
|
30824
|
+
const getDiffSettings = () => {
|
|
30825
|
+
const settings2 = strapi2.$ioSettings || {};
|
|
30826
|
+
return {
|
|
30827
|
+
enabled: settings2.fieldLevelChanges?.enabled ?? true,
|
|
30828
|
+
includeFullData: settings2.fieldLevelChanges?.includeFullData ?? false,
|
|
30829
|
+
excludeFields: settings2.fieldLevelChanges?.excludeFields ?? ["updatedAt", "updatedBy", "createdAt", "createdBy"],
|
|
30830
|
+
maxDiffDepth: settings2.fieldLevelChanges?.maxDiffDepth ?? 3
|
|
30831
|
+
};
|
|
30832
|
+
};
|
|
30833
|
+
const isPlainObject2 = (value) => {
|
|
30834
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
|
|
30835
|
+
};
|
|
30836
|
+
const isEqual2 = (a, b) => {
|
|
30837
|
+
if (a === b) return true;
|
|
30838
|
+
if (a === null || b === null) return a === b;
|
|
30839
|
+
if (typeof a !== typeof b) return false;
|
|
30840
|
+
if (a instanceof Date && b instanceof Date) {
|
|
30841
|
+
return a.getTime() === b.getTime();
|
|
30842
|
+
}
|
|
30843
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
30844
|
+
if (a.length !== b.length) return false;
|
|
30845
|
+
return a.every((item, index2) => isEqual2(item, b[index2]));
|
|
30846
|
+
}
|
|
30847
|
+
if (isPlainObject2(a) && isPlainObject2(b)) {
|
|
30848
|
+
const keysA = Object.keys(a);
|
|
30849
|
+
const keysB = Object.keys(b);
|
|
30850
|
+
if (keysA.length !== keysB.length) return false;
|
|
30851
|
+
return keysA.every((key) => isEqual2(a[key], b[key]));
|
|
30852
|
+
}
|
|
30853
|
+
return false;
|
|
30854
|
+
};
|
|
30855
|
+
const safeClone = (value) => {
|
|
30856
|
+
if (value === null || value === void 0) return value;
|
|
30857
|
+
if (value instanceof Date) return value.toISOString();
|
|
30858
|
+
if (Array.isArray(value)) return value.map(safeClone);
|
|
30859
|
+
if (isPlainObject2(value)) {
|
|
30860
|
+
const cloned = {};
|
|
30861
|
+
for (const [key, val] of Object.entries(value)) {
|
|
30862
|
+
cloned[key] = safeClone(val);
|
|
30863
|
+
}
|
|
30864
|
+
return cloned;
|
|
30865
|
+
}
|
|
30866
|
+
return value;
|
|
30867
|
+
};
|
|
30868
|
+
const calculateDiffInternal = (oldData, newData, options = {}, depth2 = 0) => {
|
|
30869
|
+
const { excludeFields = [], maxDiffDepth = 3 } = options;
|
|
30870
|
+
const diff2 = {};
|
|
30871
|
+
if (!oldData || !newData) {
|
|
30872
|
+
return { _replaced: true, old: safeClone(oldData), new: safeClone(newData) };
|
|
30873
|
+
}
|
|
30874
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(oldData || {}), ...Object.keys(newData || {})]);
|
|
30875
|
+
for (const key of allKeys) {
|
|
30876
|
+
if (excludeFields.includes(key)) continue;
|
|
30877
|
+
const oldValue = oldData?.[key];
|
|
30878
|
+
const newValue = newData?.[key];
|
|
30879
|
+
if (isEqual2(oldValue, newValue)) continue;
|
|
30880
|
+
if (isPlainObject2(oldValue) && isPlainObject2(newValue) && depth2 < maxDiffDepth) {
|
|
30881
|
+
const nestedDiff = calculateDiffInternal(oldValue, newValue, options, depth2 + 1);
|
|
30882
|
+
if (Object.keys(nestedDiff).length > 0) {
|
|
30883
|
+
diff2[key] = nestedDiff;
|
|
30884
|
+
}
|
|
30885
|
+
} else {
|
|
30886
|
+
diff2[key] = {
|
|
30887
|
+
old: safeClone(oldValue),
|
|
30888
|
+
new: safeClone(newValue)
|
|
30889
|
+
};
|
|
30890
|
+
}
|
|
30891
|
+
}
|
|
30892
|
+
return diff2;
|
|
30893
|
+
};
|
|
30894
|
+
return {
|
|
30895
|
+
/**
|
|
30896
|
+
* Calculates field-level diff between old and new data
|
|
30897
|
+
* @param {object} oldData - Previous data state
|
|
30898
|
+
* @param {object} newData - New data state
|
|
30899
|
+
* @returns {object} Diff result with changed fields and metadata
|
|
30900
|
+
*/
|
|
30901
|
+
calculateDiff(oldData, newData) {
|
|
30902
|
+
const settings2 = getDiffSettings();
|
|
30903
|
+
if (!settings2.enabled) {
|
|
30904
|
+
return {
|
|
30905
|
+
enabled: false,
|
|
30906
|
+
hasChanges: !isEqual2(oldData, newData),
|
|
30907
|
+
diff: null,
|
|
30908
|
+
fullData: newData
|
|
30909
|
+
};
|
|
30910
|
+
}
|
|
30911
|
+
const diff2 = calculateDiffInternal(oldData, newData, {
|
|
30912
|
+
excludeFields: settings2.excludeFields,
|
|
30913
|
+
maxDiffDepth: settings2.maxDiffDepth
|
|
30914
|
+
});
|
|
30915
|
+
const changedFields = Object.keys(diff2);
|
|
30916
|
+
const hasChanges = changedFields.length > 0;
|
|
30917
|
+
const result = {
|
|
30918
|
+
enabled: true,
|
|
30919
|
+
hasChanges,
|
|
30920
|
+
changedFields,
|
|
30921
|
+
changedFieldCount: changedFields.length,
|
|
30922
|
+
diff: hasChanges ? diff2 : null,
|
|
30923
|
+
timestamp: Date.now()
|
|
30924
|
+
};
|
|
30925
|
+
if (settings2.includeFullData) {
|
|
30926
|
+
result.fullData = newData;
|
|
30927
|
+
}
|
|
30928
|
+
return result;
|
|
30929
|
+
},
|
|
30930
|
+
/**
|
|
30931
|
+
* Applies a diff to a target object
|
|
30932
|
+
* @param {object} target - Target object to apply diff to
|
|
30933
|
+
* @param {object} diff - Diff to apply
|
|
30934
|
+
* @returns {object} Updated target object
|
|
30935
|
+
*/
|
|
30936
|
+
applyDiff(target, diff2) {
|
|
30937
|
+
if (!diff2 || typeof diff2 !== "object") return target;
|
|
30938
|
+
const result = { ...target };
|
|
30939
|
+
for (const [key, change] of Object.entries(diff2)) {
|
|
30940
|
+
if (change._replaced) {
|
|
30941
|
+
result[key] = change.new;
|
|
30942
|
+
} else if (change.old !== void 0 && change.new !== void 0) {
|
|
30943
|
+
result[key] = change.new;
|
|
30944
|
+
} else if (isPlainObject2(change)) {
|
|
30945
|
+
result[key] = this.applyDiff(result[key] || {}, change);
|
|
30946
|
+
}
|
|
30947
|
+
}
|
|
30948
|
+
return result;
|
|
30949
|
+
},
|
|
30950
|
+
/**
|
|
30951
|
+
* Validates if a diff is applicable to a content type
|
|
30952
|
+
* @param {string} uid - Content type UID
|
|
30953
|
+
* @param {object} diff - Diff to validate
|
|
30954
|
+
* @returns {object} Validation result
|
|
30955
|
+
*/
|
|
30956
|
+
validateDiff(uid, diff2) {
|
|
30957
|
+
if (!diff2) {
|
|
30958
|
+
return { valid: true, errors: [] };
|
|
30959
|
+
}
|
|
30960
|
+
const contentType = strapi2.contentTypes[uid];
|
|
30961
|
+
if (!contentType) {
|
|
30962
|
+
return { valid: false, errors: [`Content type ${uid} not found`] };
|
|
30963
|
+
}
|
|
30964
|
+
const errors2 = [];
|
|
30965
|
+
const attributes = contentType.attributes || {};
|
|
30966
|
+
for (const field of Object.keys(diff2)) {
|
|
30967
|
+
if (!attributes[field] && field !== "id" && field !== "documentId") {
|
|
30968
|
+
errors2.push(`Field '${field}' does not exist in ${uid}`);
|
|
30969
|
+
}
|
|
30970
|
+
}
|
|
30971
|
+
return {
|
|
30972
|
+
valid: errors2.length === 0,
|
|
30973
|
+
errors: errors2
|
|
30974
|
+
};
|
|
30975
|
+
},
|
|
30976
|
+
/**
|
|
30977
|
+
* Creates an event payload with diff information
|
|
30978
|
+
* @param {string} eventType - Event type (create, update, delete)
|
|
30979
|
+
* @param {object} schema - Content type schema info
|
|
30980
|
+
* @param {object} oldData - Previous data (null for create)
|
|
30981
|
+
* @param {object} newData - New data (null for delete)
|
|
30982
|
+
* @returns {object} Event payload with diff
|
|
30983
|
+
*/
|
|
30984
|
+
createEventPayload(eventType, schema2, oldData, newData) {
|
|
30985
|
+
const settings2 = getDiffSettings();
|
|
30986
|
+
if (eventType === "create") {
|
|
30987
|
+
return {
|
|
30988
|
+
event: eventType,
|
|
30989
|
+
schema: { singularName: schema2.singularName, uid: schema2.uid },
|
|
30990
|
+
data: newData,
|
|
30991
|
+
diff: null,
|
|
30992
|
+
timestamp: Date.now()
|
|
30993
|
+
};
|
|
30994
|
+
}
|
|
30995
|
+
if (eventType === "delete") {
|
|
30996
|
+
return {
|
|
30997
|
+
event: eventType,
|
|
30998
|
+
schema: { singularName: schema2.singularName, uid: schema2.uid },
|
|
30999
|
+
data: { id: oldData?.id, documentId: oldData?.documentId },
|
|
31000
|
+
deletedData: settings2.includeFullData ? oldData : null,
|
|
31001
|
+
diff: null,
|
|
31002
|
+
timestamp: Date.now()
|
|
31003
|
+
};
|
|
31004
|
+
}
|
|
31005
|
+
const diffResult = this.calculateDiff(oldData, newData);
|
|
31006
|
+
const payload = {
|
|
31007
|
+
event: eventType,
|
|
31008
|
+
schema: { singularName: schema2.singularName, uid: schema2.uid },
|
|
31009
|
+
documentId: newData?.documentId || newData?.id,
|
|
31010
|
+
diff: diffResult.diff,
|
|
31011
|
+
changedFields: diffResult.changedFields,
|
|
31012
|
+
hasChanges: diffResult.hasChanges,
|
|
31013
|
+
timestamp: Date.now()
|
|
31014
|
+
};
|
|
31015
|
+
if (settings2.includeFullData || !settings2.enabled) {
|
|
31016
|
+
payload.data = newData;
|
|
31017
|
+
}
|
|
31018
|
+
return payload;
|
|
31019
|
+
},
|
|
31020
|
+
/**
|
|
31021
|
+
* Checks if diff feature is enabled
|
|
31022
|
+
* @returns {boolean} True if enabled
|
|
31023
|
+
*/
|
|
31024
|
+
isEnabled() {
|
|
31025
|
+
return getDiffSettings().enabled;
|
|
31026
|
+
},
|
|
31027
|
+
/**
|
|
31028
|
+
* Gets current diff settings
|
|
31029
|
+
* @returns {object} Current settings
|
|
31030
|
+
*/
|
|
31031
|
+
getSettings() {
|
|
31032
|
+
return getDiffSettings();
|
|
31033
|
+
}
|
|
31034
|
+
};
|
|
31035
|
+
};
|
|
29963
31036
|
const strategy = strategies;
|
|
29964
31037
|
const sanitize = sanitize_1;
|
|
29965
31038
|
const transform = transform$1;
|
|
29966
31039
|
const settings = settings$1;
|
|
29967
31040
|
const monitoring = monitoring$1;
|
|
31041
|
+
const presence = presence$1;
|
|
31042
|
+
const preview = preview$1;
|
|
31043
|
+
const diff = diff$1;
|
|
29968
31044
|
var services$1 = {
|
|
29969
31045
|
sanitize,
|
|
29970
31046
|
strategy,
|
|
29971
31047
|
transform,
|
|
29972
31048
|
settings,
|
|
29973
|
-
monitoring
|
|
31049
|
+
monitoring,
|
|
31050
|
+
presence,
|
|
31051
|
+
preview,
|
|
31052
|
+
diff
|
|
29974
31053
|
};
|
|
29975
31054
|
const bootstrap = bootstrap_1;
|
|
29976
31055
|
const config = config$1;
|