@serhii.mazur/directus-gu-logs 1.0.6 → 1.0.8

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 CHANGED
@@ -8,31 +8,32 @@ A lightweight utility class for logging errors, creating activity logs, and send
8
8
 
9
9
  You need to create a collection named `logs` with the following fields:
10
10
 
11
- | Field Name | Type | Description |
12
- | --------------- | -------- | ------------------------ |
13
- | `collection` | string | Name of the collection |
14
- | `date_created` | datetime | Timestamp of the log |
15
- | `extension` | string | Extension identifier |
16
- | `function_name` | string | Name of the function |
17
- | `error` | code | Error message/code block |
11
+ | Field Name | Type | Description |
12
+ | --------------- | --------- | ---------------------------- |
13
+ | `collection` | string | Name of the collection |
14
+ | `date_created` | datetime | Timestamp of the log |
15
+ | `extension` | string | Extension identifier |
16
+ | `function_name` | string | Name of the function |
17
+ | `error` | text/code | Error message or stack trace |
18
18
 
19
19
  ### 2. Global Collection (global)
20
20
 
21
21
  Add the following field to the `global` collection:
22
22
 
23
- | Field Name | Type | Description |
24
- | ------------------ | -------------------- | --------------------------------- |
25
- | `notice_recipient` | m2o → directus_users | Recipient for error notifications |
23
+ | Field Name | Type | Description |
24
+ | ------------------------ | ------------------------------- | ---------------------------------- |
25
+ | `error_notice_recipient` | m2m → directus_users (junction) | Recipients for error notifications |
26
26
 
27
- This recipient will be used to send internal notifications when errors occur.
27
+ All linked users from this field will receive internal notifications when errors occur.
28
28
 
29
29
  ## 🚀 Features
30
30
 
31
- - Save error logs to the logs collection
32
-
33
- - Create Directus activity records
34
-
35
- - Send internal Directus notifications
31
+ - Save structured error logs to a collection
32
+ - Create Directus activity records
33
+ - Send internal Directus notifications
34
+ - Automatic recipient resolution from global settings
35
+ - Support for manual recipient override
36
+ - Includes project metadata in notifications (environment, URL, timestamp)
36
37
 
37
38
  ## 📦 Installation
38
39
 
@@ -45,10 +46,77 @@ npm i @serhii.mazur/directus-gu-logs
45
46
  ```ts
46
47
  import { Logs } from "@serhii.mazur/directus-gu-logs";
47
48
 
48
- const logs = new Logs(context, "my-extension", "logs");
49
+ const logs = new Logs(context, "my-extension");
50
+
51
+ // Save error log
52
+ await logs.printLogs("myFunction", "Something went wrong");
49
53
 
50
- await logs.printLogs("myFunction", "message");
51
- await logs.createActivity("create", "collection", "id");
54
+ // Create activity record
55
+ await logs.createActivity("create", "collection_name", "item_id");
56
+
57
+ // Send notification (auto recipients from global settings)
52
58
  await logs.createNotification("An error occurred");
53
- await logs.createNotification("An error occurred", "Custom subject", "recipient_id");
59
+
60
+ // Send notification with custom subject
61
+ await logs.createNotification("An error occurred", "Custom Subject");
62
+
63
+ // Send notification to specific recipient
64
+ await logs.createNotification("An error occurred", "Custom Subject", "user_id");
65
+
66
+ // Send notification with related collection + item
67
+ await logs.createNotification("An error occurred", "Custom Subject", null, "collection_name", "item_id");
68
+ ```
69
+
70
+ ---
71
+
72
+ ## ⚙️ Constructor
73
+
74
+ ```ts
75
+ constructor(
76
+ context: ApiExtensionContext,
77
+ extension: string,
78
+ collectionName: string = "logs"
79
+ )
54
80
  ```
81
+
82
+ | Param | Type | Description |
83
+ | ---------------- | ------------------- | ------------------------------------ |
84
+ | `context` | ApiExtensionContext | Directus extension context |
85
+ | `extension` | string | Name of your extension |
86
+ | `collectionName` | string (optional) | Logs collection name (default: logs) |
87
+
88
+ ## 🧠 How It Works
89
+
90
+ ### Logging
91
+
92
+ - Saves structured logs into the logs collection
93
+ - Automatically attaches timestamp and extension name
94
+ - Fallback to console if DB write fails
95
+
96
+ ---
97
+
98
+ ### Activity Tracking
99
+
100
+ - Uses Directus ActivityService
101
+ - Automatically includes:
102
+ - user
103
+ - IP address
104
+ - user agent
105
+ - origin
106
+
107
+ ---
108
+
109
+ ### Notifications
110
+
111
+ - Resolves recipients in this order:
112
+ 1. Explicit recipientOverride
113
+ 2. global.notice_recipient
114
+ 3. global.developer_notice_recipient
115
+ - Automatically:
116
+ - Deduplicates recipients
117
+ - Includes project metadata:
118
+ - Project name
119
+ - Backend URL
120
+ - Environment (branch)
121
+ - UTC timestamp
122
+ - Sends one notification per recipient
package/dist/index.js CHANGED
@@ -1,3 +1,14 @@
1
+ function extractRecipientIds(rows) {
2
+ const recipients = rows
3
+ ?.map(row => {
4
+ const user = row?.directus_users_id;
5
+ if (typeof user === "string")
6
+ return user;
7
+ return typeof user?.id === "string" ? user.id : null;
8
+ })
9
+ .filter((value) => Boolean(value)) ?? [];
10
+ return Array.from(new Set(recipients));
11
+ }
1
12
  export class Logs {
2
13
  constructor(context, extension, collectionName = "logs") {
3
14
  this.context = context;
@@ -59,14 +70,23 @@ export class Logs {
59
70
  try {
60
71
  const schema = await this.getSchema();
61
72
  const { database, services } = this.context;
73
+ const globalService = new services.ItemsService("global", {
74
+ database,
75
+ schema,
76
+ });
62
77
  // Check for passed recipient, fallback to global settings
63
- let recipient = recipientOverride;
64
- if (!recipient) {
65
- const globalSettings = await database.select("notice_recipient").from("global").first();
66
- recipient = globalSettings?.notice_recipient;
78
+ let recipients = [];
79
+ if (recipientOverride) {
80
+ recipients = [recipientOverride];
81
+ }
82
+ else {
83
+ const globalSettings = await globalService.readSingleton({
84
+ fields: ["error_notice_recipient.directus_users_id.id"],
85
+ });
86
+ recipients = extractRecipientIds(globalSettings?.error_notice_recipient);
67
87
  }
68
- if (!recipient) {
69
- this.printLogs(this.extension, "No recipient defined (override or global settings)");
88
+ if (recipients.length === 0) {
89
+ this.printLogs(this.extension, "No recipients defined (override or global settings)");
70
90
  return;
71
91
  }
72
92
  const notificationService = new services.NotificationsService({ schema });
@@ -95,14 +115,16 @@ ${message}<br><br>
95
115
  <strong>Backend URL:</strong> <a href="${backendUrl}" target="_blank">${backendUrl}</a><br>
96
116
  <strong>Date/Time (UTC):</strong> ${timestamp}
97
117
  `.trim();
98
- await notificationService.createOne({
99
- recipient,
100
- sender: recipient,
101
- subject,
102
- message: fullMessage,
103
- collection,
104
- item,
105
- });
118
+ for (const recipient of recipients) {
119
+ await notificationService.createOne({
120
+ recipient,
121
+ sender: recipient,
122
+ subject,
123
+ message: fullMessage,
124
+ collection,
125
+ item,
126
+ });
127
+ }
106
128
  }
107
129
  catch (error) {
108
130
  console.error("❌ Failed to create notification:", error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serhii.mazur/directus-gu-logs",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Helper class Logs for using in Directus extensions",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/index.ts CHANGED
@@ -9,6 +9,23 @@ interface LogEntry {
9
9
  error: string;
10
10
  }
11
11
 
12
+ interface RecipientLinkRow {
13
+ directus_users_id?: string | { id?: string | null } | null;
14
+ }
15
+
16
+ function extractRecipientIds(rows: RecipientLinkRow[] | undefined): string[] {
17
+ const recipients =
18
+ rows
19
+ ?.map(row => {
20
+ const user = row?.directus_users_id;
21
+ if (typeof user === "string") return user;
22
+ return typeof user?.id === "string" ? user.id : null;
23
+ })
24
+ .filter((value): value is string => Boolean(value)) ?? [];
25
+
26
+ return Array.from(new Set(recipients));
27
+ }
28
+
12
29
  export class Logs {
13
30
  protected context: ApiExtensionContext;
14
31
  protected extension: string;
@@ -88,16 +105,26 @@ export class Logs {
88
105
  try {
89
106
  const schema = await this.getSchema();
90
107
  const { database, services } = this.context;
108
+ const globalService = new services.ItemsService("global", {
109
+ database,
110
+ schema,
111
+ });
91
112
 
92
113
  // Check for passed recipient, fallback to global settings
93
- let recipient = recipientOverride;
94
- if (!recipient) {
95
- const globalSettings = await database.select("notice_recipient").from("global").first();
96
- recipient = globalSettings?.notice_recipient;
114
+ let recipients: string[] = [];
115
+
116
+ if (recipientOverride) {
117
+ recipients = [recipientOverride];
118
+ } else {
119
+ const globalSettings = await globalService.readSingleton({
120
+ fields: ["error_notice_recipient.directus_users_id.id"],
121
+ });
122
+
123
+ recipients = extractRecipientIds(globalSettings?.error_notice_recipient);
97
124
  }
98
125
 
99
- if (!recipient) {
100
- this.printLogs(this.extension, "No recipient defined (override or global settings)");
126
+ if (recipients.length === 0) {
127
+ this.printLogs(this.extension, "No recipients defined (override or global settings)");
101
128
  return;
102
129
  }
103
130
 
@@ -105,9 +132,11 @@ export class Logs {
105
132
 
106
133
  // Project Data
107
134
  const settings = await database.select("project_name").from("directus_settings").first();
135
+
108
136
  const projectName = settings?.project_name || "Unknown Project";
109
137
  const backendUrl = process.env.BACKEND_URL || this.context.env?.PUBLIC_URL || "Unknown URL";
110
138
  const environment = process.env.BRANCH || "dev";
139
+
111
140
  const now = new Date();
112
141
  const timestamp = new Intl.DateTimeFormat("en-US", {
113
142
  month: "2-digit",
@@ -131,14 +160,16 @@ ${message}<br><br>
131
160
  <strong>Date/Time (UTC):</strong> ${timestamp}
132
161
  `.trim();
133
162
 
134
- await notificationService.createOne({
135
- recipient,
136
- sender: recipient,
137
- subject,
138
- message: fullMessage,
139
- collection,
140
- item,
141
- });
163
+ for (const recipient of recipients) {
164
+ await notificationService.createOne({
165
+ recipient,
166
+ sender: recipient,
167
+ subject,
168
+ message: fullMessage,
169
+ collection,
170
+ item,
171
+ });
172
+ }
142
173
  } catch (error) {
143
174
  console.error("❌ Failed to create notification:", error);
144
175
  }