@openinc/parse-server-opendash 3.32.4 → 3.32.6

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.
@@ -533,4 +533,77 @@ exports.customoptions = {
533
533
  public: false,
534
534
  description: "Comma separated list of class names which should be excluded from ChangeLog entries.",
535
535
  },
536
+ CHANGELOG_PUBLISH_ENABLE: {
537
+ env: "OPENINC_PARSE_CHANGELOG_PUBLISH_ENABLE",
538
+ type: "boolean",
539
+ required: false,
540
+ secret: false,
541
+ public: false,
542
+ default: "false",
543
+ description: "Enable publishing of ChangeLog entries to RabbitMQ.",
544
+ },
545
+ CHANGELOG_PUBLISH_HOST: {
546
+ env: "OPENINC_PARSE_CHANGELOG_PUBLISH_HOST",
547
+ type: "string",
548
+ required: false,
549
+ secret: false,
550
+ public: false,
551
+ description: "RabbitMQ host for publishing ChangeLog entries.",
552
+ },
553
+ CHANGELOG_PUBLISH_VHOST: {
554
+ env: "OPENINC_PARSE_CHANGELOG_PUBLISH_VHOST",
555
+ type: "string",
556
+ required: false,
557
+ secret: false,
558
+ public: false,
559
+ description: "RabbitMQ virtual host for publishing ChangeLog entries.",
560
+ },
561
+ CHANGELOG_PUBLISH_PORT: {
562
+ env: "OPENINC_PARSE_CHANGELOG_PUBLISH_PORT",
563
+ type: "int",
564
+ required: false,
565
+ secret: false,
566
+ public: false,
567
+ description: "RabbitMQ port for publishing ChangeLog entries.",
568
+ },
569
+ CHANGELOG_PUBLISH_MANAGEMENT_PORT: {
570
+ env: "OPENINC_PARSE_CHANGELOG_PUBLISH_MANAGEMENT_PORT",
571
+ type: "int",
572
+ required: false,
573
+ secret: false,
574
+ public: false,
575
+ description: "RabbitMQ management port for publishing ChangeLog entries.",
576
+ },
577
+ CHANGELOG_PUBLISH_USERNAME: {
578
+ env: "OPENINC_PARSE_CHANGELOG_PUBLISH_USERNAME",
579
+ type: "string",
580
+ required: false,
581
+ secret: false,
582
+ public: false,
583
+ description: "RabbitMQ username for publishing ChangeLog entries.",
584
+ },
585
+ CHANGELOG_PUBLISH_PASSWORD: {
586
+ env: "OPENINC_PARSE_CHANGELOG_PUBLISH_PASSWORD",
587
+ type: "string",
588
+ required: false,
589
+ secret: true,
590
+ public: false,
591
+ description: "RabbitMQ password for publishing ChangeLog entries.",
592
+ },
593
+ CHANGELOG_PUBLISH_EXCHANGE: {
594
+ env: "OPENINC_PARSE_CHANGELOG_PUBLISH_EXCHANGE",
595
+ type: "string",
596
+ required: false,
597
+ secret: false,
598
+ public: false,
599
+ description: "RabbitMQ exchange for publishing ChangeLog entries.",
600
+ },
601
+ CHANGELOG_PUBLISH_TOPIC_PREFIX: {
602
+ env: "OPENINC_PARSE_CHANGELOG_PUBLISH_TOPIC_PREFIX",
603
+ type: "string",
604
+ required: false,
605
+ secret: false,
606
+ public: false,
607
+ description: "RabbitMQ topic prefix for publishing ChangeLog entries.",
608
+ },
536
609
  };
@@ -1,4 +1,8 @@
1
1
  import { ConfigState, customoptions } from "..";
2
+ /**
3
+ * Singleton class to manage configuration state.
4
+ * Use this to get values from process.env based on the defined custom options.
5
+ */
2
6
  export default class ConfigInstance {
3
7
  private static instance;
4
8
  static getInstance(): ConfigState<Extract<keyof typeof customoptions, string>>;
@@ -1,6 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const __1 = require("..");
4
+ /**
5
+ * Singleton class to manage configuration state.
6
+ * Use this to get values from process.env based on the defined custom options.
7
+ */
4
8
  class ConfigInstance {
5
9
  static getInstance() {
6
10
  if (!ConfigInstance.instance) {
@@ -0,0 +1,26 @@
1
+ export type AMQPConnectionParameters = {
2
+ topicPrefix?: string;
3
+ host: string;
4
+ vhost: string;
5
+ port: number;
6
+ managementPort: number;
7
+ username: string;
8
+ password: string;
9
+ exchange: string;
10
+ };
11
+ export declare class AMQPBusConnection {
12
+ private connectionParams;
13
+ private connection;
14
+ private channel;
15
+ private reconnectTimer;
16
+ private attempt;
17
+ private isIntentionalDisconnect;
18
+ constructor(conParams: AMQPConnectionParameters);
19
+ connect(): Promise<void>;
20
+ private establishConnection;
21
+ private handleConnectionLost;
22
+ disconnect(): Promise<void>;
23
+ publish(topic: string, message: any): Promise<void>;
24
+ private ensureVHostAndExchange;
25
+ private performRequest;
26
+ }
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AMQPBusConnection = void 0;
4
+ const amqp = require("amqplib");
5
+ class AMQPBusConnection {
6
+ constructor(conParams) {
7
+ this.attempt = 0;
8
+ this.isIntentionalDisconnect = false;
9
+ this.connectionParams = conParams;
10
+ }
11
+ async connect() {
12
+ this.isIntentionalDisconnect = false;
13
+ await this.establishConnection();
14
+ }
15
+ async establishConnection() {
16
+ try {
17
+ await this.ensureVHostAndExchange();
18
+ const { host, port, vhost, username, password } = this.connectionParams;
19
+ const url = `amqp://${username}:${password}@${host}:${port}/${vhost}`;
20
+ console.log(`AMQP Connecting to ${host}:${port}/${vhost}...`);
21
+ this.connection = await amqp.connect(url);
22
+ this.connection.on("error", (err) => {
23
+ console.error("AMQP Connection error:", err);
24
+ this.handleConnectionLost();
25
+ });
26
+ this.connection.on("close", () => {
27
+ if (!this.isIntentionalDisconnect) {
28
+ console.warn("AMQP Connection closed unexpectedly");
29
+ this.handleConnectionLost();
30
+ }
31
+ });
32
+ this.channel = await this.connection.createChannel();
33
+ console.log("AMQP Connected");
34
+ this.attempt = 0;
35
+ }
36
+ catch (err) {
37
+ console.error("Failed to establish AMQP connection:", err);
38
+ this.handleConnectionLost();
39
+ }
40
+ }
41
+ handleConnectionLost() {
42
+ this.connection = null;
43
+ this.channel = null;
44
+ if (this.reconnectTimer)
45
+ return;
46
+ if (this.isIntentionalDisconnect)
47
+ return;
48
+ // Max 30s
49
+ const timeout = Math.min(1000 * Math.pow(2, this.attempt), 30000);
50
+ this.attempt++;
51
+ console.log(`AMQP Reconnecting in ${timeout}ms (Attempt ${this.attempt})...`);
52
+ this.reconnectTimer = setTimeout(async () => {
53
+ this.reconnectTimer = undefined;
54
+ await this.establishConnection();
55
+ }, timeout);
56
+ }
57
+ async disconnect() {
58
+ this.isIntentionalDisconnect = true;
59
+ if (this.reconnectTimer) {
60
+ clearTimeout(this.reconnectTimer);
61
+ this.reconnectTimer = undefined;
62
+ }
63
+ if (this.channel) {
64
+ try {
65
+ await this.channel.close();
66
+ }
67
+ catch (e) {
68
+ // ignore
69
+ }
70
+ this.channel = null;
71
+ }
72
+ if (this.connection) {
73
+ try {
74
+ await this.connection.close();
75
+ }
76
+ catch (e) {
77
+ // ignore
78
+ }
79
+ this.connection = null;
80
+ }
81
+ console.log("AMQP Disconnected");
82
+ }
83
+ async publish(topic, message) {
84
+ if (!this.channel) {
85
+ throw new Error("AMQP Channel not available");
86
+ }
87
+ // Check if exchange is configured
88
+ const exchange = this.connectionParams.exchange;
89
+ // Convert message to Buffer if it's not
90
+ let content;
91
+ if (Buffer.isBuffer(message)) {
92
+ content = message;
93
+ }
94
+ else if (typeof message === "string") {
95
+ content = Buffer.from(message);
96
+ }
97
+ else {
98
+ content = Buffer.from(JSON.stringify(message));
99
+ }
100
+ this.channel.publish(exchange, this.connectionParams.topicPrefix
101
+ ? `${this.connectionParams.topicPrefix}.${topic}`
102
+ : topic, content);
103
+ }
104
+ async ensureVHostAndExchange() {
105
+ const { host, vhost, username, password, exchange, managementPort } = this.connectionParams;
106
+ const port = managementPort || 15672;
107
+ const auth = Buffer.from(`${username}:${password}`).toString("base64");
108
+ // 1. Ensure vHost exists
109
+ console.log(`Ensuring vHost '${vhost}' exists on ${host}:${port}...`);
110
+ await this.performRequest(host, port, "PUT", `/api/vhosts/${encodeURIComponent(vhost)}`, auth);
111
+ // 2. Ensure Exchange exists
112
+ console.log(`Ensuring Exchange '${exchange}' exists on ${host}:${port}...`);
113
+ await this.performRequest(host, port, "PUT", `/api/exchanges/${encodeURIComponent(vhost)}/${encodeURIComponent(exchange)}`, auth, JSON.stringify({
114
+ type: "topic",
115
+ auto_delete: false,
116
+ durable: true,
117
+ internal: false,
118
+ arguments: {},
119
+ }));
120
+ }
121
+ async performRequest(host, port, method, path, auth, body) {
122
+ const url = `http://${host}:${port}${path}`;
123
+ const response = await fetch(url, {
124
+ method,
125
+ headers: {
126
+ Authorization: `Basic ${auth}`,
127
+ "Content-Type": "application/json",
128
+ },
129
+ body,
130
+ });
131
+ if (!response.ok) {
132
+ const text = await response.text();
133
+ throw new Error(`RabbitMQ API Request failed: ${response.status} ${response.statusText} - ${text}`);
134
+ }
135
+ }
136
+ }
137
+ exports.AMQPBusConnection = AMQPBusConnection;
@@ -28,7 +28,7 @@ async function init() {
28
28
  if (form.next) {
29
29
  delete form.next;
30
30
  }
31
- object.set("form", {});
31
+ // object.set("form", {});
32
32
  let sCount = 1;
33
33
  const newEntries = [];
34
34
  res.forEach((element, idx) => {
@@ -38,15 +38,16 @@ async function init() {
38
38
  form["step" + (sCount - 1)].next = "step" + sCount;
39
39
  }
40
40
  });
41
- request.context.pages = res;
41
+ // request.context.pages = res;
42
42
  object.set("entries", newEntries);
43
43
  object.set("form", form);
44
44
  }
45
45
  });
46
46
  (0, schema_1.afterSaveHook)(types_1.BDE_Form, async (request) => {
47
47
  const { object, original, user } = request;
48
- for (const page of request.context.pages) {
49
- page.save(null, { useMasterKey: true });
50
- }
48
+ //Unclear why it was added...
49
+ // for (const page of request.context.pages as BDE_Page[]) {
50
+ // page.save(null, { useMasterKey: true });
51
+ // }
51
52
  });
52
53
  }
@@ -1,19 +1,42 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.init = init;
4
- const schema_1 = require("../features/schema");
5
4
  const scheduleToEvent_1 = require("../features/openservice/schedules/calendarSync/functions/scheduleToEvent");
6
5
  const CalendarManager_1 = require("../features/openservice/schedules/calendarSync/service/CalendarManager");
6
+ const schema_1 = require("../features/schema");
7
7
  const types_1 = require("../types");
8
8
  async function init() {
9
9
  (0, schema_1.beforeSaveHook)(types_1.Maintenance_Schedule, async (request) => {
10
10
  const { object, original, user } = request;
11
11
  await (0, schema_1.defaultHandler)(request);
12
- await (0, schema_1.defaultAclHandler)(request);
12
+ // await defaultAclHandler(request, { allowCustomACL: true });
13
13
  });
14
14
  (0, schema_1.afterSaveHook)(types_1.Maintenance_Schedule, async (request) => {
15
15
  const { object, original, user, master } = request;
16
16
  if (!original) {
17
+ // Set ACL for new Maintenance_Schedule
18
+ // If is new: User should see but not tenant users
19
+ let acl = object.getACL();
20
+ if (!acl) {
21
+ acl = new Parse.ACL();
22
+ }
23
+ // If this is a new object, the acl will be set to the user who saved it only.
24
+ if (user) {
25
+ const tenant = object.get("tenant") || user.get("tenant");
26
+ if (tenant) {
27
+ acl.setRoleReadAccess("od-tenant-user-" + tenant.id, false);
28
+ }
29
+ acl.setReadAccess(user.id, true);
30
+ acl.setWriteAccess(user.id, true);
31
+ }
32
+ acl.setRoleReadAccess("od-admin", true);
33
+ acl.setRoleWriteAccess("od-admin", true);
34
+ acl.setPublicReadAccess(false);
35
+ acl.setPublicWriteAccess(false);
36
+ object.setACL(acl);
37
+ if (!master) {
38
+ await object.save(null, { useMasterKey: true });
39
+ }
17
40
  await addToTemplateSources(object);
18
41
  if (!master) {
19
42
  await addToCalendar(object);
@@ -39,6 +62,11 @@ async function init() {
39
62
  }
40
63
  });
41
64
  }
65
+ /**
66
+ * Add schedule's source to its template's sources relation
67
+ * @param schedule Maintenance_Schedule
68
+ * @returns Promise<void>
69
+ */
42
70
  async function addToTemplateSources(schedule) {
43
71
  const template = schedule.get("template");
44
72
  if (!template)
@@ -6,13 +6,33 @@ const types_1 = require("../types");
6
6
  async function init() {
7
7
  (0, schema_1.beforeSaveHook)(types_1.Maintenance_Schedule_Template, async (request) => {
8
8
  const { object, original, user } = request;
9
+ // If this is a new object, the acl will be set to the user who saved it only.
10
+ if (object.isNew()) {
11
+ let acl = object.getACL();
12
+ if (!acl) {
13
+ acl = new Parse.ACL();
14
+ }
15
+ if (user) {
16
+ const tenant = object.get("tenant") || user.get("tenant");
17
+ if (tenant) {
18
+ acl.setRoleReadAccess("od-tenant-user-" + tenant.id, false);
19
+ }
20
+ acl.setReadAccess(user.id, true);
21
+ acl.setWriteAccess(user.id, true);
22
+ }
23
+ acl.setRoleReadAccess("od-admin", true);
24
+ acl.setRoleWriteAccess("od-admin", true);
25
+ acl.setPublicReadAccess(false);
26
+ acl.setPublicWriteAccess(false);
27
+ object.setACL(acl);
28
+ }
9
29
  await (0, schema_1.defaultHandler)(request);
10
- await (0, schema_1.defaultAclHandler)(request);
30
+ // await defaultAclHandler(request, { allowCustomACL: true });
11
31
  });
12
32
  (0, schema_1.afterSaveHook)(types_1.Maintenance_Schedule_Template, async (request) => {
13
33
  const { object, original, user } = request;
14
- if (object)
15
- await createSchedulesWithTemplate(object);
34
+ if (object && user)
35
+ await createSchedulesWithTemplate(object, user);
16
36
  });
17
37
  (0, schema_1.beforeDeleteHook)(types_1.Maintenance_Schedule_Template, async (request) => {
18
38
  const { object, original, user } = request;
@@ -25,7 +45,7 @@ async function init() {
25
45
  * Creates schedules based on the provided template.
26
46
  * @param template the maintenance schedule template
27
47
  */
28
- async function createSchedulesWithTemplate(template) {
48
+ async function createSchedulesWithTemplate(template, user) {
29
49
  // find existing schedules based on the template
30
50
  const matchingSchedules = await new Parse.Query(types_1.Maintenance_Schedule)
31
51
  .equalTo("template", template)
@@ -49,6 +69,20 @@ async function createSchedulesWithTemplate(template) {
49
69
  enabled: true,
50
70
  tenant: template.get("tenant"),
51
71
  });
52
- await newSchedule.save(null, { useMasterKey: true });
72
+ const savedSchedule = await newSchedule.save(null, {
73
+ sessionToken: user.getSessionToken(),
74
+ });
75
+ // Add steps to the schedule steps relation
76
+ const templateStepsRelation = template.relation("steps");
77
+ const scheduleStepsRelation = savedSchedule.relation("steps");
78
+ const templateSteps = await templateStepsRelation
79
+ .query()
80
+ .findAll({ useMasterKey: true });
81
+ for (const step of templateSteps) {
82
+ scheduleStepsRelation.add(step);
83
+ }
84
+ await savedSchedule.save(null, {
85
+ sessionToken: user.getSessionToken(),
86
+ });
53
87
  }
54
88
  }
@@ -1,10 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.init = init;
4
+ const AMQPBusConnection_1 = require("../features/openware/services/AMQPBusConnection");
4
5
  const schema_1 = require("../features/schema");
5
6
  const types_1 = require("../types");
6
7
  async function init() {
7
8
  const schema = await Parse.Schema.all();
9
+ const publishEnabled = (process.env.OPENINC_PARSE_CHANGELOG_PUBLISH_ENABLE || "FALSE").toUpperCase() === "TRUE";
10
+ console.log(`ChangeLog publishing enabled: ${publishEnabled}`);
8
11
  const blacklistedClasses = [
9
12
  "_Session",
10
13
  "_Installation",
@@ -22,6 +25,38 @@ async function init() {
22
25
  object.getACL().setReadAccess(user.id, true);
23
26
  }
24
27
  });
28
+ if (publishEnabled) {
29
+ const params = {
30
+ host: process.env.OPENINC_PARSE_CHANGELOG_PUBLISH_HOST || "localhost",
31
+ vhost: process.env.OPENINC_PARSE_CHANGELOG_PUBLISH_VHOST || "configuration",
32
+ port: parseInt(process.env.OPENINC_PARSE_CHANGELOG_PUBLISH_PORT || "5672"),
33
+ username: process.env.OPENINC_PARSE_CHANGELOG_PUBLISH_USERNAME || "guest",
34
+ password: process.env.OPENINC_PARSE_CHANGELOG_PUBLISH_PASSWORD || "guest",
35
+ exchange: process.env.OPENINC_PARSE_CHANGELOG_PUBLISH_EXCHANGE || "topic",
36
+ topicPrefix: process.env.OPENINC_PARSE_CHANGELOG_PUBLISH_TOPIC_PREFIX || "config",
37
+ managementPort: parseInt(process.env.OPENINC_PARSE_CHANGELOG_PUBLISH_MANAGEMENT_PORT || "15672"),
38
+ };
39
+ const connection = new AMQPBusConnection_1.AMQPBusConnection(params);
40
+ try {
41
+ const { host, port, vhost, username, password } = params;
42
+ const url = `amqp://${username}:*****@${host}:${port}/${vhost}`;
43
+ console.log(`Establishing Connection to ${url} for ChangeLog publishing...`);
44
+ await connection.connect();
45
+ console.log(`Connected to AMQPBus at ${url} for ChangeLog publishing.`);
46
+ (0, schema_1.afterSaveHook)(types_1.Changelog.className, async (request) => {
47
+ const { object: changelog } = request;
48
+ const msg = JSON.stringify(changelog.toJSON());
49
+ const topic = `${params.topicPrefix && params.topicPrefix.length > 0
50
+ ? params.topicPrefix + "."
51
+ : ""}${changelog.get("nameOfClass")}`;
52
+ await connection.publish(topic, msg);
53
+ console.log(`Published ChangeLog entry [${topic}]: ${msg}`);
54
+ });
55
+ }
56
+ catch (e) {
57
+ console.error("Failed to connect to AMQPBus for ChangeLog publishing:", e);
58
+ }
59
+ }
25
60
  schema.forEach((classSchema) => {
26
61
  if (!classSchema.className ||
27
62
  classSchema.className === types_1.Changelog.className ||
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@openinc/parse-server-opendash",
3
- "version": "3.32.4",
3
+ "version": "3.32.6",
4
4
  "description": "Parse Server Cloud Code for open.INC Stack.",
5
- "packageManager": "pnpm@10.26.2",
5
+ "packageManager": "pnpm@10.28.0",
6
6
  "keywords": [
7
7
  "parse",
8
8
  "opendash"
@@ -67,27 +67,28 @@
67
67
  },
68
68
  "dependencies": {
69
69
  "@openinc/parse-server-schema": "^3.0.7",
70
- "@opentelemetry/api-logs": "^0.208.0",
71
- "@opentelemetry/auto-instrumentations-node": "^0.67.3",
72
- "@opentelemetry/exporter-logs-otlp-http": "^0.208.0",
73
- "@opentelemetry/exporter-trace-otlp-http": "^0.208.0",
74
- "@opentelemetry/resources": "^2.2.0",
75
- "@opentelemetry/sdk-logs": "^0.208.0",
76
- "@opentelemetry/sdk-node": "^0.208.0",
77
- "@opentelemetry/semantic-conventions": "^1.38.0",
78
- "@opentelemetry/winston-transport": "^0.19.0",
79
- "rimraf": "^6.1.2",
70
+ "@opentelemetry/api-logs": "^0.210.0",
71
+ "@opentelemetry/auto-instrumentations-node": "^0.68.0",
72
+ "@opentelemetry/exporter-logs-otlp-http": "^0.210.0",
73
+ "@opentelemetry/exporter-trace-otlp-http": "^0.210.0",
74
+ "@opentelemetry/resources": "^2.4.0",
75
+ "@opentelemetry/sdk-logs": "^0.210.0",
76
+ "@opentelemetry/sdk-node": "^0.210.0",
77
+ "@opentelemetry/semantic-conventions": "^1.39.0",
78
+ "@opentelemetry/winston-transport": "^0.20.0",
79
+ "amqplib": "^0.10.9",
80
80
  "cron": "^4.4.0",
81
81
  "dayjs": "^1.11.19",
82
82
  "fast-equals": "^5.4.0",
83
- "i18next": "^25.7.3",
83
+ "i18next": "^25.7.4",
84
84
  "i18next-fs-backend": "^2.6.1",
85
85
  "jsonwebtoken": "^9.0.3",
86
- "jwks-rsa": "^3.2.0",
86
+ "jwks-rsa": "^3.2.1",
87
87
  "nodemailer": "^7.0.12",
88
88
  "nunjucks": "^3.2.4",
89
89
  "parse-server": "^9.1.1",
90
90
  "pdf-img-convert": "2.0.0",
91
+ "rimraf": "^6.1.2",
91
92
  "semantic-release": "^25.0.2",
92
93
  "table": "^6.9.0",
93
94
  "web-push": "^3.6.7",
@@ -102,8 +103,8 @@
102
103
  "@semantic-release/npm": "^13.1.3",
103
104
  "@semantic-release/release-notes-generator": "^14.1.0",
104
105
  "@types/jsonwebtoken": "^9.0.10",
105
- "@types/node": "^24.10.4",
106
- "@types/nodemailer": "^7.0.4",
106
+ "@types/node": "^24.10.9",
107
+ "@types/nodemailer": "^7.0.5",
107
108
  "@types/nunjucks": "^3.2.6",
108
109
  "@types/parse": "^3.0.9",
109
110
  "@types/safe-timers": "^1.1.2",
@@ -111,7 +112,7 @@
111
112
  "concurrently": "^9.2.1",
112
113
  "semantic-release-export-data": "^1.2.0",
113
114
  "ts-node": "^10.9.2",
114
- "typedoc": "^0.28.15",
115
+ "typedoc": "^0.28.16",
115
116
  "typedoc-plugin-markdown": "^4.9.0",
116
117
  "typescript": "^5.9.3"
117
118
  }