adminizer 4.3.0-build.80 → 4.3.0-build.82
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/assets/add-CZdDpNV2.js +1 -0
- package/assets/add-form-BjXOLDij.js +1 -0
- package/assets/add-group-form-BNmpvW8K.js +1 -0
- package/assets/add-group-urhA47Qf.js +1 -0
- package/assets/add-user-CzWUu7wG.js +1 -0
- package/assets/{add-user-form-yp020h4r.js → add-user-form-DrclnTOd.js} +1 -1
- package/assets/{app-CpGtjhxF.css → app-CSVBI-Lw.css} +1 -1
- package/assets/app-layout-CbyH1MDK.js +1 -0
- package/assets/{app-shell-xEVbINZV.js → app-shell-CHEYhVco.js} +1 -1
- package/assets/{app-sidebar-4lm9f_CG.js → app-sidebar-DeF8WNmW.js} +1 -1
- package/assets/app-sidebar-header-8KWnVpkN.js +1 -0
- package/assets/app.js +1628 -1628
- package/assets/breadcrumbs-C439DWrC.js +1 -0
- package/assets/catalog-CCSagWKW.js +6 -0
- package/assets/dashboard-DUNXib6b.js +3 -0
- package/assets/date-picker-DbJ8nHp5.js +1 -0
- package/assets/del-modal-_b3CSNHb.js +1 -0
- package/assets/{dynamic-controls-PxBg8yMz.js → dynamic-controls-BOYmRX_z.js} +1 -1
- package/assets/{field-renderer-CsBa_tMB.css → field-renderer-CRNLShJD.css} +1 -1
- package/assets/field-renderer-D0crIeQS.js +407 -0
- package/assets/{form-CSTeMDf5.js → form-B6LFaTgS.js} +1 -1
- package/assets/form-C8pRhEOA.js +1 -0
- package/assets/geo-json-uSUIgJf5.js +4 -0
- package/assets/{handsontable-D6MvFmcM.js → handsontable-PVakPtJV.js} +55 -55
- package/assets/home-D--JrFFj.js +2 -0
- package/assets/icon-DM49pZW3.js +1 -0
- package/assets/{init-user-L-oxFTOv.js → init-user-DNrB8Xt5.js} +1 -1
- package/assets/input-error--IOnWghB.js +1 -0
- package/assets/jsoneditor-D95dIAP2.js +3878 -0
- package/assets/list-CQivvagG.js +1 -0
- package/assets/{list-table-vIvydJV2.js → list-table-h_zXp43g.js} +4 -4
- package/assets/{login-DLMJ6l_r.js → login-6Xb8bcPq.js} +1 -1
- package/assets/manifest.json +259 -260
- package/assets/material-icon-BedgD2NE.js +1 -0
- package/assets/{media-manager-ASGJ4PcS.js → media-manager-Bms57WSN.js} +2 -2
- package/assets/module-DqrIORMW.js +1 -0
- package/assets/{monaco-B1uVVM7f.js → monaco-ICtx6xoc.js} +1 -1
- package/assets/{nav-footer-DbGCtmmN.js → nav-footer-BwkQkwow.js} +1 -1
- package/assets/{nav-main-BcWhl71g.js → nav-main-9Eb_A0rf.js} +1 -1
- package/assets/nav-user-CRAfyE-x.js +1 -0
- package/assets/notification-db0RD9I6.js +27 -0
- package/assets/{pagination-render-BceTnewI.js → pagination-render-D3UyVAlr.js} +1 -1
- package/assets/{register-C_W_hOmq.js → register-BIyjCy8M.js} +1 -1
- package/assets/tabs-BK_5wdPA.js +1 -0
- package/assets/theme-switcher-CKiJbMkh.js +1 -0
- package/assets/toast-editor-BZM5RHph.js +845 -0
- package/assets/{user-info-BrWerGLc.js → user-info-CcYdmjMZ.js} +1 -1
- package/assets/{user-menu-content-D61_auxX.js → user-menu-content-DbUk8ZeY.js} +1 -1
- package/assets/vanilla-picker-l5rcX3cq.js +8 -0
- package/assets/{vendor-DoC2WAmd.js → vendor-wzZzN_Jv.js} +2 -2
- package/assets/welcome-CZlWN4gM.js +1 -0
- package/controllers/add.js +1 -1
- package/controllers/catalog/FrontentCatalogAdapter.d.ts +4 -4
- package/controllers/edit.js +1 -15
- package/controllers/list.js +2 -2
- package/controllers/login.js +2 -2
- package/controllers/navigation/edit.js +1 -1
- package/controllers/notifications/NotificationController.d.ts +5 -5
- package/controllers/notifications/NotificationController.js +77 -78
- package/controllers/remove.js +1 -11
- package/controllers/view.js +1 -1
- package/helpers/configHelper.d.ts +1 -1
- package/helpers/fieldsHelper.d.ts +1 -1
- package/helpers/fieldsHelper.js +1 -1
- package/helpers/inertiaAddHelper.d.ts +1 -1
- package/helpers/translationHelper.d.ts +1 -1
- package/helpers/translationHelper.js +1 -1
- package/index.d.ts +14 -13
- package/index.js +14 -13
- package/interfaces/adminpanelConfig.d.ts +1 -1
- package/interfaces/global.d.ts +3 -3
- package/interfaces/types.d.ts +10 -1
- package/lib/Adminizer.d.ts +7 -7
- package/lib/Adminizer.js +7 -7
- package/lib/{v4/DataAccessor.d.ts → DataAccessor.d.ts} +4 -4
- package/lib/{v4/DataAccessor.js → DataAccessor.js} +3 -3
- package/lib/{v4/I18n.js → I18n.js} +2 -2
- package/lib/{v4/PolicyManager.d.ts → PolicyManager.d.ts} +1 -1
- package/lib/{v4/PolicyManager.js → PolicyManager.js} +2 -2
- package/lib/{v4/catalog → catalog}/AbstractCatalog.d.ts +1 -1
- package/lib/{v4/catalog → catalog}/Navigation.d.ts +2 -2
- package/lib/{v4/catalog → catalog}/Navigation.js +1 -1
- package/lib/{v4/controls → controls}/AbstractControls.d.ts +1 -1
- package/lib/{v4/controls → controls}/codeEditor/MonacoEditor.d.ts +1 -1
- package/lib/{v4/controls → controls}/geojsoneditor/GeoEditor.d.ts +1 -1
- package/lib/{v4/controls → controls}/jsoneditor/JsonEditor.d.ts +1 -1
- package/lib/{v4/controls → controls}/markdown/ToastUiEditor.d.ts +1 -1
- package/lib/{v4/controls → controls}/table/Handsontable.d.ts +1 -1
- package/lib/{v4/controls → controls}/wysiwyg/CKeditor.d.ts +1 -1
- package/lib/{v4/datatable → datatable}/NodeTable.d.ts +1 -1
- package/lib/{v4/model → model}/AbstractModel.d.ts +12 -0
- package/lib/model/AbstractModel.js +178 -0
- package/lib/{v4/model → model}/ModelHandler.js +1 -1
- package/lib/{v4/model → model}/adapter/sequelize.d.ts +2 -4
- package/lib/{v4/model → model}/adapter/sequelize.js +33 -13
- package/lib/{v4/notifications → notifications}/AbstractNotificationService.d.ts +14 -7
- package/lib/notifications/AbstractNotificationService.js +233 -0
- package/lib/{v4/notifications → notifications}/GeneralNotificationService.d.ts +1 -1
- package/lib/{v4/notifications → notifications}/GeneralNotificationService.js +4 -2
- package/lib/notifications/NotificationHandler.d.ts +27 -0
- package/lib/notifications/NotificationHandler.js +41 -0
- package/lib/{v4/notifications → notifications}/SystemNotificationService.d.ts +5 -4
- package/lib/notifications/SystemNotificationService.js +173 -0
- package/lib/{v4/widgets → widgets}/abstractCustom.d.ts +1 -1
- package/lib/{v4/widgets → widgets}/abstractLink.d.ts +1 -1
- package/lib/{v4/widgets → widgets}/abstractWidgetBase.d.ts +1 -1
- package/lib/{v4/widgets → widgets}/widgetHandler.d.ts +2 -2
- package/migrations/index.d.ts +3 -0
- package/migrations/index.js +4 -0
- package/migrations/umzug/0001.d.ts +7 -0
- package/migrations/umzug/0001.js +119 -0
- package/migrations/umzug.d.ts +2 -0
- package/migrations/umzug.js +8 -0
- package/models/NotificationAP.d.ts +8 -0
- package/models/NotificationAP.js +8 -0
- package/models/UserAP.d.ts +1 -1
- package/package.json +5 -4
- package/system/Router.js +4 -8
- package/system/bindControls.js +6 -6
- package/system/bindInertia.js +4 -3
- package/system/bindNavigation.js +1 -1
- package/system/bindNotifications.js +3 -3
- package/system/bindReqFunctions.js +8 -25
- package/system/defaults.js +1 -1
- package/ui/components/ui/table.d.ts +1 -1
- package/ui/components/ui/table.js +2 -2
- package/assets/add-DSapOiv0.js +0 -1
- package/assets/add-form-C04ZI5LT.js +0 -1
- package/assets/add-group-DWR7SBTO.js +0 -1
- package/assets/add-group-form-CyRwxTs3.js +0 -1
- package/assets/add-user-acATGxUL.js +0 -1
- package/assets/app-layout-BmT5KQwc.js +0 -1
- package/assets/app-sidebar-header-CxXmzszq.js +0 -1
- package/assets/breadcrumbs-livOfa2Z.js +0 -1
- package/assets/catalog-CG_EB2kz.js +0 -6
- package/assets/dashboard-CsmJfdLb.js +0 -3
- package/assets/date-picker-By3kj-Rx.js +0 -1
- package/assets/del-modal-DlcbcdR5.js +0 -1
- package/assets/field-renderer-Bp1lfm0v.js +0 -407
- package/assets/form-CxHRGp7p.js +0 -1
- package/assets/geo-json-CCgjEQfu.js +0 -4
- package/assets/home-D00XjLjD.js +0 -2
- package/assets/icon-ButfCBrt.js +0 -1
- package/assets/input-error-CHsDLcK0.js +0 -1
- package/assets/jsoneditor-CBqATVR0.js +0 -3833
- package/assets/list-4TOTsjqE.js +0 -1
- package/assets/material-icon-sXk4BK-N.js +0 -1
- package/assets/module-mxngd2cA.js +0 -1
- package/assets/nav-user-BN2veV3l.js +0 -1
- package/assets/notification-BEfA5sT6.js +0 -1
- package/assets/tabs-CgZW_LLn.js +0 -1
- package/assets/theme-switcher-D1G6OSMc.js +0 -1
- package/assets/toast-editor-CfgZXoQZ.js +0 -845
- package/assets/vanilla-picker-B6E6ObS_.js +0 -8
- package/assets/welcome-FHRCdke0.js +0 -1
- package/lib/v4/model/AbstractModel.js +0 -79
- package/lib/v4/notifications/AbstractNotificationService.js +0 -150
- package/lib/v4/notifications/NotificationHandler.d.ts +0 -69
- package/lib/v4/notifications/NotificationHandler.js +0 -127
- package/lib/v4/notifications/SystemNotificationService.js +0 -124
- package/migrations/db-migrate/20230303171134-adminpanel-init.js +0 -122
- package/migrations/db-migrate/20231223215835-add-widgets.js +0 -32
- package/migrations/db-migrate/20240804191001-added-navigationap.js +0 -53
- package/migrations/db-migrate/20241121184236-v4-init.js +0 -134
- /package/lib/{v4/I18n.d.ts → I18n.d.ts} +0 -0
- /package/lib/{v4/POWCaptcha.d.ts → POWCaptcha.d.ts} +0 -0
- /package/lib/{v4/POWCaptcha.js → POWCaptcha.js} +0 -0
- /package/lib/{v4/catalog → catalog}/AbstractCatalog.js +0 -0
- /package/lib/{v4/catalog → catalog}/CatalogHandler.d.ts +0 -0
- /package/lib/{v4/catalog → catalog}/CatalogHandler.js +0 -0
- /package/lib/{v4/controls → controls}/AbstractControls.js +0 -0
- /package/lib/{v4/controls → controls}/ControlsHandler.d.ts +0 -0
- /package/lib/{v4/controls → controls}/ControlsHandler.js +0 -0
- /package/lib/{v4/controls → controls}/codeEditor/MonacoEditor.js +0 -0
- /package/lib/{v4/controls → controls}/geojsoneditor/GeoEditor.js +0 -0
- /package/lib/{v4/controls → controls}/jsoneditor/JsonEditor.js +0 -0
- /package/lib/{v4/controls → controls}/markdown/ToastUiEditor.js +0 -0
- /package/lib/{v4/controls → controls}/table/Handsontable.js +0 -0
- /package/lib/{v4/controls → controls}/wysiwyg/CKeditor.js +0 -0
- /package/lib/{v4/datatable → datatable}/NodeTable.js +0 -0
- /package/lib/{v4/helper → helper}/jwt.d.ts +0 -0
- /package/lib/{v4/helper → helper}/jwt.js +0 -0
- /package/lib/{v4/inertia → inertia}/flash.d.ts +0 -0
- /package/lib/{v4/inertia → inertia}/flash.js +0 -0
- /package/lib/{v4/inertia → inertia}/inertiaAdapter.d.ts +0 -0
- /package/lib/{v4/inertia → inertia}/inertiaAdapter.js +0 -0
- /package/lib/{v4/model → model}/ModelHandler.d.ts +0 -0
- /package/lib/{v4/model → model}/adapter/waterline.d.ts +0 -0
- /package/lib/{v4/model → model}/adapter/waterline.js +0 -0
- /package/lib/{v4/widgets → widgets}/abstractAction.d.ts +0 -0
- /package/lib/{v4/widgets → widgets}/abstractAction.js +0 -0
- /package/lib/{v4/widgets → widgets}/abstractCustom.js +0 -0
- /package/lib/{v4/widgets → widgets}/abstractInfo.d.ts +0 -0
- /package/lib/{v4/widgets → widgets}/abstractInfo.js +0 -0
- /package/lib/{v4/widgets → widgets}/abstractLink.js +0 -0
- /package/lib/{v4/widgets → widgets}/abstractSwitch.d.ts +0 -0
- /package/lib/{v4/widgets → widgets}/abstractSwitch.js +0 -0
- /package/lib/{v4/widgets → widgets}/abstractWidgetBase.js +0 -0
- /package/lib/{v4/widgets → widgets}/widgetHandler.js +0 -0
|
@@ -15,7 +15,11 @@ function generateAssociationsFromSchema(models, schemas) {
|
|
|
15
15
|
const targetModel = models[field.collection];
|
|
16
16
|
const targetSchema = schemas[field.collection];
|
|
17
17
|
const inverseField = targetSchema?.[field.via];
|
|
18
|
-
|
|
18
|
+
// Normalize join table to lowercase for consistency across dialects
|
|
19
|
+
const throughTableName = [modelName, field.collection]
|
|
20
|
+
.sort()
|
|
21
|
+
.join("")
|
|
22
|
+
.toLowerCase();
|
|
19
23
|
// 💡 M:N связь
|
|
20
24
|
if (inverseField && inverseField.collection === modelName) {
|
|
21
25
|
model.belongsToMany(targetModel, {
|
|
@@ -444,7 +448,7 @@ export class SequelizeModel extends AbstractModel {
|
|
|
444
448
|
where,
|
|
445
449
|
include: Object.values(this.model.associations)
|
|
446
450
|
});
|
|
447
|
-
return reloaded.map(r => r.get({ plain: true }));
|
|
451
|
+
return reloaded.map((r) => r.get({ plain: true }));
|
|
448
452
|
}
|
|
449
453
|
// --- DESTROY ONE ---
|
|
450
454
|
async _destroyOne(criteria) {
|
|
@@ -514,7 +518,7 @@ export class SequelizeModel extends AbstractModel {
|
|
|
514
518
|
}
|
|
515
519
|
}
|
|
516
520
|
}
|
|
517
|
-
const raw = records.map(r => r.get({ plain: true }));
|
|
521
|
+
const raw = records.map((r) => r.get({ plain: true }));
|
|
518
522
|
await this.model.destroy({ where });
|
|
519
523
|
return raw;
|
|
520
524
|
}
|
|
@@ -529,11 +533,9 @@ export class SequelizeModel extends AbstractModel {
|
|
|
529
533
|
return Object.keys(this.model.associations).map(key => ({ association: key }));
|
|
530
534
|
}
|
|
531
535
|
}
|
|
532
|
-
/** SequelizeAdapter — адаптер, заменяющий WaterlineAdapter */
|
|
533
536
|
export class SequelizeAdapter extends AbstractAdapter {
|
|
534
537
|
sequelize;
|
|
535
538
|
Model = SequelizeModel;
|
|
536
|
-
alter = false;
|
|
537
539
|
constructor(sequelize) {
|
|
538
540
|
super("sequelize", sequelize);
|
|
539
541
|
this.sequelize = sequelize;
|
|
@@ -552,18 +554,33 @@ export class SequelizeAdapter extends AbstractAdapter {
|
|
|
552
554
|
const model = this.getModel(modelName);
|
|
553
555
|
return model?.getAttributes();
|
|
554
556
|
}
|
|
555
|
-
/**Registration of system models*/
|
|
557
|
+
/** Registration of system models */
|
|
556
558
|
static async registerSystemModels(sequelize, alter = true) {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
559
|
+
// Try multiple possible model locations
|
|
560
|
+
const possiblePaths = [
|
|
561
|
+
path.resolve(import.meta.dirname, "../../../../models"),
|
|
562
|
+
path.resolve(import.meta.dirname, "../../../../src/models"),
|
|
563
|
+
path.resolve(import.meta.dirname, "../../../models"),
|
|
564
|
+
path.resolve(import.meta.dirname, "../../../src/models"),
|
|
565
|
+
path.resolve(process.cwd(), "node_modules/adminizer/src/models"),
|
|
566
|
+
path.resolve(process.cwd(), "node_modules/adminizer/models")
|
|
567
|
+
];
|
|
568
|
+
let modelsDir = null;
|
|
569
|
+
for (const possiblePath of possiblePaths) {
|
|
570
|
+
if (fs.existsSync(possiblePath)) {
|
|
571
|
+
modelsDir = possiblePath;
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
560
574
|
}
|
|
561
|
-
|
|
575
|
+
if (!modelsDir) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
let files = fs.readdirSync(modelsDir).filter((f) => f.endsWith(".js"));
|
|
562
579
|
if (!files.length) {
|
|
563
|
-
files = fs.readdirSync(modelsDir).filter(f => f.endsWith(".ts") && !f.endsWith(".d.ts"));
|
|
580
|
+
files = fs.readdirSync(modelsDir).filter((f) => f.endsWith(".ts") && !f.endsWith(".d.ts"));
|
|
564
581
|
}
|
|
565
582
|
if (!files.length) {
|
|
566
|
-
|
|
583
|
+
return;
|
|
567
584
|
}
|
|
568
585
|
const schemas = {};
|
|
569
586
|
for (const file of files) {
|
|
@@ -575,9 +592,12 @@ export class SequelizeAdapter extends AbstractAdapter {
|
|
|
575
592
|
generateSequelizeModel(sequelize, modelName, definition);
|
|
576
593
|
}
|
|
577
594
|
generateAssociationsFromSchema(sequelize.models, schemas);
|
|
578
|
-
|
|
595
|
+
// Check ORM_ALTER environment variable before sync
|
|
596
|
+
if (process.env.ORM_ALTER !== 'false' && alter) {
|
|
579
597
|
await sequelize.sync();
|
|
580
598
|
}
|
|
599
|
+
else {
|
|
600
|
+
}
|
|
581
601
|
}
|
|
582
602
|
}
|
|
583
603
|
/**Generation of the SEQUELIZE model from the definition, analogue of Waterinecollection.extenD*/
|
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
import { EventEmitter } from 'events';
|
|
2
|
-
import { Adminizer } from '
|
|
3
|
-
import { INotification, INotificationEvent } from '
|
|
2
|
+
import { Adminizer } from '../Adminizer';
|
|
3
|
+
import { INotification, INotificationEvent } from '../../interfaces/types';
|
|
4
|
+
import { NotificationAPModel } from "../../models/NotificationAP";
|
|
5
|
+
import { UserAP } from "../../models/UserAP";
|
|
4
6
|
export declare abstract class AbstractNotificationService extends EventEmitter {
|
|
5
|
-
protected clients: Map<string, (event: INotificationEvent) => void
|
|
7
|
+
protected clients: Map<number, Map<string, (event: INotificationEvent) => void>>;
|
|
6
8
|
protected adminizer: Adminizer;
|
|
7
9
|
abstract readonly notificationClass: string;
|
|
8
10
|
abstract readonly icon: string;
|
|
9
11
|
abstract readonly iconColor: string;
|
|
10
12
|
constructor(adminizer: Adminizer);
|
|
11
|
-
|
|
13
|
+
private _bindAccessRight;
|
|
14
|
+
addClient(clientId: string, sendFn: (event: INotificationEvent) => void, user: UserAP): void;
|
|
12
15
|
removeClient(clientId: string): void;
|
|
16
|
+
getUserClients(userId: number): Map<string, (event: INotificationEvent) => void>;
|
|
17
|
+
getClientCount(): number;
|
|
18
|
+
getAllClients(): Map<string, (event: INotificationEvent) => void>;
|
|
13
19
|
abstract dispatchNotification(notification: Omit<INotification, 'id' | 'createdAt' | 'notificationClass' | 'icon'>): Promise<boolean>;
|
|
14
20
|
protected broadcast(event: INotificationEvent): void;
|
|
15
21
|
sendHeartbeat(clientId: string): void;
|
|
16
|
-
protected generateId(): string;
|
|
17
|
-
getClientCount(): number;
|
|
18
22
|
protected createUserNotification(notificationId: string, userId?: number): Promise<void>;
|
|
19
23
|
protected getUserNotification(notificationId: string, userId: number): Promise<any>;
|
|
20
|
-
getNotifications(userId: number, limit?: number, unreadOnly?: boolean): Promise<INotification[]>;
|
|
24
|
+
getNotifications(userId: number, limit?: number, skip?: number, unreadOnly?: boolean): Promise<INotification[]>;
|
|
25
|
+
protected prepareNotification(notificationsDB: NotificationAPModel[], userId: number): Promise<INotification[]>;
|
|
26
|
+
search(s: string, userId: number): Promise<INotification[]>;
|
|
21
27
|
markAsRead(userId: number, id: string): Promise<void>;
|
|
28
|
+
markAllAsRead(userId: number): Promise<void>;
|
|
22
29
|
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { Adminizer } from '../Adminizer';
|
|
3
|
+
export class AbstractNotificationService extends EventEmitter {
|
|
4
|
+
clients = new Map();
|
|
5
|
+
adminizer;
|
|
6
|
+
constructor(adminizer) {
|
|
7
|
+
super();
|
|
8
|
+
this.adminizer = adminizer;
|
|
9
|
+
this._bindAccessRight();
|
|
10
|
+
}
|
|
11
|
+
_bindAccessRight() {
|
|
12
|
+
setTimeout(() => {
|
|
13
|
+
this.adminizer.accessRightsHelper.registerToken({
|
|
14
|
+
id: `notification-${this.notificationClass}`,
|
|
15
|
+
name: this.notificationClass,
|
|
16
|
+
description: `Access to notification ${this.notificationClass}`,
|
|
17
|
+
department: 'notification',
|
|
18
|
+
});
|
|
19
|
+
}, 100);
|
|
20
|
+
}
|
|
21
|
+
// Добавление клиента
|
|
22
|
+
addClient(clientId, sendFn, user) {
|
|
23
|
+
const userId = user.id;
|
|
24
|
+
// Если у пользователя еще нет Map клиентов - создаем
|
|
25
|
+
if (!this.clients.has(userId)) {
|
|
26
|
+
this.clients.set(userId, new Map());
|
|
27
|
+
}
|
|
28
|
+
// Получаем Map клиентов пользователя и добавляем нового клиента
|
|
29
|
+
const userClients = this.clients.get(userId);
|
|
30
|
+
userClients.set(clientId, sendFn);
|
|
31
|
+
Adminizer.log.info(`[${this.notificationClass}] Client ${clientId} connected for user ${userId}. Total users: ${this.clients.size}, user clients: ${userClients.size}`);
|
|
32
|
+
}
|
|
33
|
+
// Удаление клиента
|
|
34
|
+
removeClient(clientId) {
|
|
35
|
+
// Ищем клиента во всех пользовательских Map
|
|
36
|
+
for (const [userId, userClients] of this.clients.entries()) {
|
|
37
|
+
if (userClients.has(clientId)) {
|
|
38
|
+
userClients.delete(clientId);
|
|
39
|
+
// Если у пользователя больше нет клиентов - удаляем его Map
|
|
40
|
+
if (userClients.size === 0) {
|
|
41
|
+
this.clients.delete(userId);
|
|
42
|
+
}
|
|
43
|
+
Adminizer.log.info(`[${this.notificationClass}] Client ${clientId} disconnected from user ${userId}. Total users: ${this.clients.size}`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
Adminizer.log.warn(`[${this.notificationClass}] Client ${clientId} not found for removal`);
|
|
48
|
+
}
|
|
49
|
+
// Получение клиентов конкретного пользователя
|
|
50
|
+
getUserClients(userId) {
|
|
51
|
+
return this.clients.get(userId) || new Map();
|
|
52
|
+
}
|
|
53
|
+
// Получение количества подключенных клиентов
|
|
54
|
+
getClientCount() {
|
|
55
|
+
return this.clients.size;
|
|
56
|
+
}
|
|
57
|
+
// Получение всех клиентов (для обратной совместимости)
|
|
58
|
+
getAllClients() {
|
|
59
|
+
const allClients = new Map();
|
|
60
|
+
for (const userClients of this.clients.values()) {
|
|
61
|
+
for (const [clientId, sendFn] of userClients.entries()) {
|
|
62
|
+
allClients.set(clientId, sendFn);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return allClients;
|
|
66
|
+
}
|
|
67
|
+
// Рассылка события всем клиентам
|
|
68
|
+
broadcast(event) {
|
|
69
|
+
for (const userClients of this.clients.values()) {
|
|
70
|
+
userClients.forEach((sendFn, clientId) => {
|
|
71
|
+
try {
|
|
72
|
+
sendFn(event);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
Adminizer.log.error(`[${this.notificationClass}] Error sending to client ${clientId}:`, error);
|
|
76
|
+
this.removeClient(clientId);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Отправка heartbeat
|
|
82
|
+
sendHeartbeat(clientId) {
|
|
83
|
+
for (const userClients of this.clients.values()) {
|
|
84
|
+
const sendFn = userClients.get(clientId);
|
|
85
|
+
if (sendFn) {
|
|
86
|
+
const heartbeatEvent = {
|
|
87
|
+
type: 'heartbeat',
|
|
88
|
+
data: 'ping'
|
|
89
|
+
};
|
|
90
|
+
sendFn(heartbeatEvent);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Метод для создания записи в UserNotificationAP
|
|
96
|
+
async createUserNotification(notificationId, userId) {
|
|
97
|
+
if (this.adminizer.modelHandler.model.has('usernotificationap') && userId) {
|
|
98
|
+
try {
|
|
99
|
+
await this.adminizer.modelHandler.model.get('usernotificationap')["_create"]({
|
|
100
|
+
userId: userId,
|
|
101
|
+
notificationId: notificationId,
|
|
102
|
+
read: false
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
Adminizer.log.error(`Error creating user notification record:`, error);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Метод для получения UserNotificationAP по ID уведомления и пользователя
|
|
111
|
+
async getUserNotification(notificationId, userId) {
|
|
112
|
+
if (this.adminizer.modelHandler.model.has('usernotificationap')) {
|
|
113
|
+
try {
|
|
114
|
+
return await this.adminizer.modelHandler.model.get('usernotificationap')["_findOne"]({
|
|
115
|
+
where: { notificationId: notificationId, userId: userId }
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
Adminizer.log.error(`Error getting user notification:`, error);
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
async getNotifications(userId, limit = 20, skip = 0, unreadOnly = false) {
|
|
126
|
+
if (!this.adminizer.modelHandler.model.has('notificationap')) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
let query = { notificationClass: this.notificationClass };
|
|
131
|
+
// Если запрашиваются уведомления для конкретного пользователя получаем ID уведомлений пользователя
|
|
132
|
+
const userNotifications = await this.adminizer.modelHandler.model.get('usernotificationap')["_find"]({
|
|
133
|
+
where: {
|
|
134
|
+
userId: userId
|
|
135
|
+
}
|
|
136
|
+
}, { populate: [['notificationId', {}]] });
|
|
137
|
+
const notificationIds = userNotifications.map((un) => un.notificationId.id);
|
|
138
|
+
if (unreadOnly) {
|
|
139
|
+
// Только непрочитанные
|
|
140
|
+
query.id = userNotifications
|
|
141
|
+
.filter((un) => !un.read)
|
|
142
|
+
.map((un) => un.notificationId.id);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
query.id = notificationIds;
|
|
146
|
+
}
|
|
147
|
+
if (query.id.length === 0)
|
|
148
|
+
return [];
|
|
149
|
+
const notificationsDB = await this.adminizer.modelHandler.model.get('notificationap')["_find"]({
|
|
150
|
+
where: query,
|
|
151
|
+
sort: 'createdAt DESC',
|
|
152
|
+
limit: limit,
|
|
153
|
+
skip: skip
|
|
154
|
+
});
|
|
155
|
+
return await this.prepareNotification(notificationsDB, userId);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
Adminizer.log.error('Error fetching notifications:', error);
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async prepareNotification(notificationsDB, userId) {
|
|
163
|
+
let notifications = [];
|
|
164
|
+
for (const notification of notificationsDB) {
|
|
165
|
+
let readStatus = false;
|
|
166
|
+
// Получаем статус прочтения из UserNotificationAP
|
|
167
|
+
const userNotification = await this.getUserNotification(notification.id, userId);
|
|
168
|
+
readStatus = userNotification ? userNotification.read : false;
|
|
169
|
+
notifications.push({
|
|
170
|
+
...notification,
|
|
171
|
+
read: readStatus,
|
|
172
|
+
icon: {
|
|
173
|
+
icon: this.icon,
|
|
174
|
+
iconColor: this.iconColor
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return notifications;
|
|
179
|
+
}
|
|
180
|
+
async search(s, userId) {
|
|
181
|
+
try {
|
|
182
|
+
const userNotifications = await this.adminizer.modelHandler.model.get('usernotificationap')["_find"]({
|
|
183
|
+
where: {
|
|
184
|
+
userId: userId
|
|
185
|
+
}
|
|
186
|
+
}, { populate: [['notificationId', {}]] });
|
|
187
|
+
const notificationIds = userNotifications.map((un) => un.notificationId.id);
|
|
188
|
+
if (notificationIds.length === 0)
|
|
189
|
+
return [];
|
|
190
|
+
const notificationsDB = await this.adminizer.modelHandler.model.get('notificationap')["_find"]({
|
|
191
|
+
where: {
|
|
192
|
+
message: { contains: s },
|
|
193
|
+
id: notificationIds,
|
|
194
|
+
notificationClass: this.notificationClass
|
|
195
|
+
},
|
|
196
|
+
sort: "createdAt DESC"
|
|
197
|
+
});
|
|
198
|
+
return await this.prepareNotification(notificationsDB, userId);
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
Adminizer.log.error('Error searching notifications:', error);
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async markAsRead(userId, id) {
|
|
206
|
+
try {
|
|
207
|
+
const userNotification = await this.getUserNotification(id, userId);
|
|
208
|
+
if (userNotification) {
|
|
209
|
+
await this.adminizer.modelHandler.model.get('usernotificationap')["_update"]({
|
|
210
|
+
id: userNotification.id
|
|
211
|
+
}, { read: true });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
Adminizer.log.error('Error marking notification as read:', error);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async markAllAsRead(userId) {
|
|
219
|
+
try {
|
|
220
|
+
const userNotifications = await this.adminizer.modelHandler.model.get('usernotificationap')["_find"]({
|
|
221
|
+
where: { userId: userId }
|
|
222
|
+
});
|
|
223
|
+
for (const userNotification of userNotifications) {
|
|
224
|
+
await this.adminizer.modelHandler.model.get('usernotificationap')["_update"]({
|
|
225
|
+
where: { id: userNotification.id },
|
|
226
|
+
}, { read: true });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
Adminizer.log.error('Error marking all notifications as read:', error);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AbstractNotificationService } from './AbstractNotificationService';
|
|
2
|
-
import { INotification } from '
|
|
2
|
+
import { INotification } from '../../interfaces/types';
|
|
3
3
|
export declare class GeneralNotificationService extends AbstractNotificationService {
|
|
4
4
|
readonly notificationClass = "general";
|
|
5
5
|
readonly icon: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AbstractNotificationService } from './AbstractNotificationService';
|
|
2
|
-
import { Adminizer } from '
|
|
2
|
+
import { Adminizer } from '../Adminizer';
|
|
3
3
|
export class GeneralNotificationService extends AbstractNotificationService {
|
|
4
4
|
notificationClass = 'general';
|
|
5
5
|
icon = 'info';
|
|
@@ -23,7 +23,9 @@ export class GeneralNotificationService extends AbstractNotificationService {
|
|
|
23
23
|
const users = await this.adminizer.modelHandler.model.get('userap')["_find"]({});
|
|
24
24
|
for (const user of users) {
|
|
25
25
|
try {
|
|
26
|
-
|
|
26
|
+
if (this.adminizer.accessRightsHelper.hasPermission(`notification-${this.notificationClass}`, user)) {
|
|
27
|
+
await this.createUserNotification(notificationDB.id, user.id);
|
|
28
|
+
}
|
|
27
29
|
}
|
|
28
30
|
catch (error) {
|
|
29
31
|
Adminizer.log.error('Error creating UserNotificationAP:', error);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { AbstractNotificationService } from "./AbstractNotificationService";
|
|
3
|
+
import { INotification } from '../../interfaces/types';
|
|
4
|
+
export declare class NotificationHandler extends EventEmitter {
|
|
5
|
+
private services;
|
|
6
|
+
constructor();
|
|
7
|
+
/**
|
|
8
|
+
* Register a new notification service
|
|
9
|
+
* @param service
|
|
10
|
+
*/
|
|
11
|
+
registerService(service: AbstractNotificationService): void;
|
|
12
|
+
/**
|
|
13
|
+
* Get a notification service by class
|
|
14
|
+
* @param notificationClass
|
|
15
|
+
*/
|
|
16
|
+
getService(notificationClass: string): AbstractNotificationService | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Get all registered services
|
|
19
|
+
*/
|
|
20
|
+
getAllServices(): AbstractNotificationService[];
|
|
21
|
+
/**
|
|
22
|
+
* Dispatch a notification to a service
|
|
23
|
+
* @param notificationClass
|
|
24
|
+
* @param notification
|
|
25
|
+
*/
|
|
26
|
+
dispatchNotification(notificationClass: string, notification: Omit<INotification, 'id' | 'createdAt' | 'notificationClass' | 'icon'>): Promise<boolean>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { Adminizer } from '../Adminizer';
|
|
3
|
+
export class NotificationHandler extends EventEmitter {
|
|
4
|
+
services = new Map();
|
|
5
|
+
constructor() {
|
|
6
|
+
super();
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Register a new notification service
|
|
10
|
+
* @param service
|
|
11
|
+
*/
|
|
12
|
+
registerService(service) {
|
|
13
|
+
this.services.set(service.notificationClass, service);
|
|
14
|
+
Adminizer.log.info(`Notification service registered: ${service.notificationClass}`);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Get a notification service by class
|
|
18
|
+
* @param notificationClass
|
|
19
|
+
*/
|
|
20
|
+
getService(notificationClass) {
|
|
21
|
+
return this.services.get(notificationClass);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Get all registered services
|
|
25
|
+
*/
|
|
26
|
+
getAllServices() {
|
|
27
|
+
return Array.from(this.services.values());
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Dispatch a notification to a service
|
|
31
|
+
* @param notificationClass
|
|
32
|
+
* @param notification
|
|
33
|
+
*/
|
|
34
|
+
async dispatchNotification(notificationClass, notification) {
|
|
35
|
+
const service = this.getService(notificationClass);
|
|
36
|
+
if (!service) {
|
|
37
|
+
throw new Error(`Notification service not found: ${notificationClass}`);
|
|
38
|
+
}
|
|
39
|
+
return service.dispatchNotification(notification);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AbstractNotificationService } from './AbstractNotificationService';
|
|
2
|
-
import { INotification } from '
|
|
2
|
+
import { INotification } from '../../interfaces/types';
|
|
3
3
|
export declare class SystemNotificationService extends AbstractNotificationService {
|
|
4
4
|
readonly notificationClass = "system";
|
|
5
5
|
readonly icon: string;
|
|
@@ -7,12 +7,13 @@ export declare class SystemNotificationService extends AbstractNotificationServi
|
|
|
7
7
|
private crudChannels;
|
|
8
8
|
dispatchNotification(notification: Omit<INotification, 'id' | 'createdAt' | 'notificationClass' | 'icon'>): Promise<boolean>;
|
|
9
9
|
private broadcastToChannel;
|
|
10
|
-
addClientToChannel(clientId: string, channel: string): void;
|
|
11
|
-
removeClientFromChannel(clientId: string, channel: string): void;
|
|
12
|
-
removeClientFromAllChannels(clientId: string): void;
|
|
10
|
+
addClientToChannel(clientId: string, channel: string, userId: number): void;
|
|
11
|
+
removeClientFromChannel(clientId: string, channel: string, userId: number): void;
|
|
12
|
+
removeClientFromAllChannels(clientId: string, userId: number): void;
|
|
13
13
|
removeClient(clientId: string): void;
|
|
14
14
|
logSystemEvent(title: string, message: string, channel?: string, metadata?: Record<string | number, any>): Promise<boolean>;
|
|
15
15
|
logCreatedEvent(title: string, message: string, metadata?: Record<string | number, any>): Promise<boolean>;
|
|
16
16
|
logUpdatedEvent(title: string, message: string, metadata?: Record<string | number, any>): Promise<boolean>;
|
|
17
17
|
logDeletedEvent(title: string, message: string, metadata?: Record<string | number, any>): Promise<boolean>;
|
|
18
|
+
getUserChannels(userId: number): Map<string, Set<string>>;
|
|
18
19
|
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { AbstractNotificationService } from './AbstractNotificationService';
|
|
2
|
+
import { Adminizer } from '../Adminizer';
|
|
3
|
+
export class SystemNotificationService extends AbstractNotificationService {
|
|
4
|
+
notificationClass = 'system';
|
|
5
|
+
icon = 'settings';
|
|
6
|
+
iconColor = '#1eb707';
|
|
7
|
+
// Изменяем структуру каналов: храним по userId -> channel -> clientIds
|
|
8
|
+
crudChannels = new Map();
|
|
9
|
+
async dispatchNotification(notification) {
|
|
10
|
+
const fullNotification = {
|
|
11
|
+
...notification,
|
|
12
|
+
notificationClass: this.notificationClass,
|
|
13
|
+
channel: notification.channel ?? ''
|
|
14
|
+
};
|
|
15
|
+
let notificationDB;
|
|
16
|
+
// Сохраняем в базу
|
|
17
|
+
if (this.adminizer.modelHandler.model.has('notificationap')) {
|
|
18
|
+
try {
|
|
19
|
+
notificationDB = await this.adminizer.modelHandler.model.get('notificationap')["_create"](fullNotification);
|
|
20
|
+
const users = await this.adminizer.modelHandler.model.get('userap')["_find"]({});
|
|
21
|
+
for (const user of users) {
|
|
22
|
+
try {
|
|
23
|
+
if (this.adminizer.accessRightsHelper.hasPermission(`notification-${this.notificationClass}`, user)) {
|
|
24
|
+
await this.createUserNotification(notificationDB.id, user.id);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
Adminizer.log.error('Error creating UserNotificationAP:', error);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const event = {
|
|
32
|
+
type: 'notification',
|
|
33
|
+
data: {
|
|
34
|
+
...notificationDB,
|
|
35
|
+
icon: {
|
|
36
|
+
icon: this.icon,
|
|
37
|
+
iconColor: this.iconColor
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
notificationClass: this.notificationClass,
|
|
41
|
+
userId: notification.userId ?? null,
|
|
42
|
+
channel: notification.channel ?? 'system'
|
|
43
|
+
};
|
|
44
|
+
// Отправляем на все каналы или на конкретный канал
|
|
45
|
+
if (notification.channel) {
|
|
46
|
+
this.broadcastToChannel(notification.channel, event);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
this.broadcastToChannel('system', event);
|
|
50
|
+
}
|
|
51
|
+
Adminizer.log.info(`[System] Notification dispatched: ${fullNotification.title}`);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
Adminizer.log.error('Error saving system notification to database:', error);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
// Обновляем broadcastToChannel для работы с новой структурой
|
|
61
|
+
broadcastToChannel(channel, event) {
|
|
62
|
+
// Отправляем всем пользователям, подписанным на этот канал
|
|
63
|
+
this.crudChannels.forEach((userChannels, userId) => {
|
|
64
|
+
const channelClients = userChannels.get(channel);
|
|
65
|
+
if (channelClients) {
|
|
66
|
+
const userClients = this.clients.get(userId);
|
|
67
|
+
if (userClients) {
|
|
68
|
+
channelClients.forEach(clientId => {
|
|
69
|
+
const sendFn = userClients.get(clientId);
|
|
70
|
+
if (sendFn) {
|
|
71
|
+
try {
|
|
72
|
+
sendFn(event);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
Adminizer.log.error(`[${this.notificationClass}] Error sending to client ${clientId} on channel ${channel}:`, error);
|
|
76
|
+
this.removeClient(clientId);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// Добавляем клиента к каналу с привязкой к пользователю
|
|
85
|
+
addClientToChannel(clientId, channel, userId) {
|
|
86
|
+
if (!this.crudChannels.has(userId)) {
|
|
87
|
+
this.crudChannels.set(userId, new Map());
|
|
88
|
+
}
|
|
89
|
+
const userChannels = this.crudChannels.get(userId);
|
|
90
|
+
if (!userChannels.has(channel)) {
|
|
91
|
+
userChannels.set(channel, new Set());
|
|
92
|
+
}
|
|
93
|
+
userChannels.get(channel).add(clientId);
|
|
94
|
+
Adminizer.log.info(`[${this.notificationClass}] Client ${clientId} (user ${userId}) added to channel ${channel}`);
|
|
95
|
+
}
|
|
96
|
+
// Удаляем клиента из канала конкретного пользователя
|
|
97
|
+
removeClientFromChannel(clientId, channel, userId) {
|
|
98
|
+
const userChannels = this.crudChannels.get(userId);
|
|
99
|
+
if (userChannels) {
|
|
100
|
+
const channelClients = userChannels.get(channel);
|
|
101
|
+
if (channelClients) {
|
|
102
|
+
channelClients.delete(clientId);
|
|
103
|
+
Adminizer.log.info(`[${this.notificationClass}] Client ${clientId} (user ${userId}) removed from channel ${channel}`);
|
|
104
|
+
// Если в канале больше нет клиентов - удаляем канал
|
|
105
|
+
if (channelClients.size === 0) {
|
|
106
|
+
userChannels.delete(channel);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Если у пользователя больше нет каналов - удаляем запись пользователя
|
|
110
|
+
if (userChannels.size === 0) {
|
|
111
|
+
this.crudChannels.delete(userId);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Удаляем клиента из всех каналов пользователя
|
|
116
|
+
removeClientFromAllChannels(clientId, userId) {
|
|
117
|
+
const userChannels = this.crudChannels.get(userId);
|
|
118
|
+
if (userChannels) {
|
|
119
|
+
userChannels.forEach((clients, channel) => {
|
|
120
|
+
clients.delete(clientId);
|
|
121
|
+
Adminizer.log.info(`[${this.notificationClass}] Client ${clientId} (user ${userId}) removed from channel ${channel}`);
|
|
122
|
+
// Если в канале больше нет клиентов - удаляем канал
|
|
123
|
+
if (clients.size === 0) {
|
|
124
|
+
userChannels.delete(channel);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
// Если у пользователя больше нет каналов - удаляем запись пользователя
|
|
128
|
+
if (userChannels.size === 0) {
|
|
129
|
+
this.crudChannels.delete(userId);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Переопределяем removeClient для очистки каналов
|
|
134
|
+
removeClient(clientId) {
|
|
135
|
+
// Находим userId по clientId
|
|
136
|
+
let foundUserId = null;
|
|
137
|
+
for (const [userId, userClients] of this.clients.entries()) {
|
|
138
|
+
if (userClients.has(clientId)) {
|
|
139
|
+
foundUserId = userId;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Удаляем клиента из основного хранилища
|
|
144
|
+
super.removeClient(clientId);
|
|
145
|
+
// Удаляем клиента из каналов
|
|
146
|
+
if (foundUserId !== null) {
|
|
147
|
+
this.removeClientFromAllChannels(clientId, foundUserId);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Специальный метод для системных событий с указанием канала
|
|
151
|
+
async logSystemEvent(title, message, channel, metadata) {
|
|
152
|
+
return this.dispatchNotification({
|
|
153
|
+
title: title,
|
|
154
|
+
message: message,
|
|
155
|
+
metadata: metadata,
|
|
156
|
+
channel: channel
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
// Методы для CRUD операций
|
|
160
|
+
async logCreatedEvent(title, message, metadata) {
|
|
161
|
+
return this.logSystemEvent(title, message, 'created', metadata);
|
|
162
|
+
}
|
|
163
|
+
async logUpdatedEvent(title, message, metadata) {
|
|
164
|
+
return this.logSystemEvent(title, message, 'updated', metadata);
|
|
165
|
+
}
|
|
166
|
+
async logDeletedEvent(title, message, metadata) {
|
|
167
|
+
return this.logSystemEvent(title, message, 'deleted', metadata);
|
|
168
|
+
}
|
|
169
|
+
// Новый метод для получения каналов пользователя
|
|
170
|
+
getUserChannels(userId) {
|
|
171
|
+
return this.crudChannels.get(userId) || new Map();
|
|
172
|
+
}
|
|
173
|
+
}
|