ofsc-utility 1.0.20 → 1.0.22

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.
@@ -1,9 +1,44 @@
1
1
  type AnyObject = {
2
2
  [key: string]: any;
3
3
  };
4
+ /**
5
+ * Recursively flattens an object into a single-level object with
6
+ * underscore-separated keys.
7
+ *
8
+ * Example: { a: { b: 1 } } -> { "a_b": 1 }
9
+ *
10
+ * Usage: exported helper used when callers need a flat row-style
11
+ * representation (e.g., CSV export). Not used internally in this
12
+ * module but available for other modules to import.
13
+ *
14
+ * @param obj - The object to flatten.
15
+ * @param parentKey - Internal recursion prefix (do not pass normally).
16
+ * @param result - Internal accumulator (do not pass normally).
17
+ * @returns The flattened object.
18
+ */
4
19
  export declare function flattenObject(obj: AnyObject, parentKey?: string, result?: AnyObject): AnyObject;
5
20
  export declare function downloadAllEventsOfDay(clientId: string, clientSecret: string, instanceUrl: string, subscriptionId: string, sinceDate: string, onlyData: boolean): Promise<any[]>;
6
21
  export declare function downloadAllEventsOfDayCSV(clientId: string, clientSecret: string, instanceUrl: string, subscriptionId: string, sinceDate: string, onlyData: boolean): Promise<any[]>;
22
+ /**
23
+ * Generate a deterministic SHA-256 hash for an event item.
24
+ *
25
+ * This uses a stable stringification of the selected fields so that
26
+ * object property ordering does not change the resulting hash. Useful
27
+ * for deduplication or consistent event identity across runs.
28
+ *
29
+ * @param item - Object containing `eventType`, `activityId`, `time`, and `activityChanges`.
30
+ * @returns Hex-encoded SHA-256 hash string representing the event.
31
+ */
32
+ export declare function generateHash(item: {
33
+ eventType: string;
34
+ activityId: string | number;
35
+ time: string;
36
+ activityChanges: unknown;
37
+ inventoryChanges?: unknown;
38
+ resourceDetails: {
39
+ resourceId: string | number;
40
+ };
41
+ }): string;
7
42
  /**
8
43
  * Events of last two minutes
9
44
  * @param clientId
@@ -6,11 +6,28 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.flattenObject = flattenObject;
7
7
  exports.downloadAllEventsOfDay = downloadAllEventsOfDay;
8
8
  exports.downloadAllEventsOfDayCSV = downloadAllEventsOfDayCSV;
9
+ exports.generateHash = generateHash;
9
10
  exports.downloadAllEventsOfDLastTwoMinutes = downloadAllEventsOfDLastTwoMinutes;
11
+ const node_crypto_1 = __importDefault(require("node:crypto"));
10
12
  const path_1 = __importDefault(require("path"));
11
13
  const index_1 = require("../oauthTokenService/index");
12
14
  const utilities_1 = require("../utilities");
13
15
  const index_2 = require("../utilities/index");
16
+ /**
17
+ * Recursively flattens an object into a single-level object with
18
+ * underscore-separated keys.
19
+ *
20
+ * Example: { a: { b: 1 } } -> { "a_b": 1 }
21
+ *
22
+ * Usage: exported helper used when callers need a flat row-style
23
+ * representation (e.g., CSV export). Not used internally in this
24
+ * module but available for other modules to import.
25
+ *
26
+ * @param obj - The object to flatten.
27
+ * @param parentKey - Internal recursion prefix (do not pass normally).
28
+ * @param result - Internal accumulator (do not pass normally).
29
+ * @returns The flattened object.
30
+ */
14
31
  function flattenObject(obj, parentKey = "", result = {}) {
15
32
  for (const key in obj) {
16
33
  const newKey = parentKey ? `${parentKey}_${key}` : key;
@@ -25,6 +42,21 @@ function flattenObject(obj, parentKey = "", result = {}) {
25
42
  }
26
43
  return result;
27
44
  }
45
+ /**
46
+ * Fetch a single page of events via `fetchWithRetry` and return the
47
+ * received token and typed `EventResponse` data.
48
+ *
49
+ * This is a small wrapper around `fetchWithRetry` that normalizes the
50
+ * response shape for the callers in this module. It is used by both
51
+ * `downloadAllEventsOfDayCSV` and `downloadAllEventsOfDLastTwoMinutes`.
52
+ *
53
+ * @param url - Full URL for the events endpoint page.
54
+ * @param token - Current OAuth token (may be refreshed by fetchWithRetry).
55
+ * @param clientId - OAuth client id used by `fetchWithRetry` if token refresh needed.
56
+ * @param clientSecret - OAuth client secret used by `fetchWithRetry`.
57
+ * @param instanceUrl - Instance host used by `fetchWithRetry`.
58
+ * @returns Object with `token` (possibly refreshed) and `data` typed as `EventResponse`.
59
+ */
28
60
  async function fetchEventsPage(url, token, clientId, clientSecret, instanceUrl) {
29
61
  const res = await (0, utilities_1.fetchWithRetry)(url, clientId, clientSecret, instanceUrl, token);
30
62
  return {
@@ -32,6 +64,17 @@ async function fetchEventsPage(url, token, clientId, clientSecret, instanceUrl)
32
64
  data: res.data
33
65
  };
34
66
  }
67
+ /**
68
+ * Return the next calendar day in `YYYY-MM-DD` format for a given
69
+ * `YYYY-MM-DD` input string.
70
+ *
71
+ * Usage: used by `processEventItems` to detect when fetched event
72
+ * pages cross into the following day, allowing the downloader to stop
73
+ * collecting events for the requested date.
74
+ *
75
+ * @param dateString - Date string in `YYYY-MM-DD` form.
76
+ * @returns Date string for the next day in `YYYY-MM-DD`.
77
+ */
35
78
  function getNextDay(dateString) {
36
79
  // Parse manually to avoid timezone issues
37
80
  const [year, month, day] = dateString.split('-').map(Number);
@@ -43,6 +86,25 @@ function getNextDay(dateString) {
43
86
  const d = pad(date.getDate());
44
87
  return `${y}-${m}-${d}`;
45
88
  }
89
+ /**
90
+ * Process an array of event items and push them into `output` while
91
+ * enforcing a date boundary.
92
+ *
93
+ * Behavior:
94
+ * - If an event's `time` indicates it belongs to the next calendar
95
+ * day (based on `sinceDate`) the function logs and returns `false`
96
+ * to signal the caller to stop pagination.
97
+ * - Otherwise, it extracts `activityId` from `activityDetails` and
98
+ * pushes a top-level object into `output`.
99
+ *
100
+ * Usage: called from `downloadAllEventsOfDayCSV` during page
101
+ * iteration to accumulate events for the requested date.
102
+ *
103
+ * @param items - Event items from a page response.
104
+ * @param sinceDate - The starting date (YYYY-MM-DD) used to detect day boundaries.
105
+ * @param output - Accumulator array to receive processed events.
106
+ * @returns `true` to continue pagination, `false` to stop.
107
+ */
46
108
  function processEventItems(items, sinceDate, output) {
47
109
  for (const item of items) {
48
110
  const eventTime = item.time;
@@ -59,10 +121,37 @@ function processEventItems(items, sinceDate, output) {
59
121
  }
60
122
  async function downloadAllEventsOfDay(clientId, clientSecret, instanceUrl, subscriptionId, sinceDate, onlyData) {
61
123
  console.log("Downloading events...OnlyData:", onlyData);
124
+ // Convenience wrapper: returns the same data as
125
+ // `downloadAllEventsOfDayCSV` but kept as a separate exported
126
+ // function for readability and future extension.
62
127
  let data = await downloadAllEventsOfDayCSV(clientId, clientSecret, instanceUrl, subscriptionId, sinceDate, onlyData);
63
128
  return data;
64
129
  }
65
130
  async function downloadAllEventsOfDayCSV(clientId, clientSecret, instanceUrl, subscriptionId, sinceDate, onlyData) {
131
+ /**
132
+ * Download all events for a specific calendar day and return them as an
133
+ * array. Internally handles pagination and will stop when pages cross
134
+ * into the next day.
135
+ *
136
+ * Key behaviors and usage:
137
+ * - Builds an initial request for the given `sinceDate` and pages
138
+ * through results using `fetchEventsPage`.
139
+ * - Uses `processEventItems` to add items to the `events` array and
140
+ * stop when the date boundary is reached.
141
+ * - If `onlyData` is false the collected events are saved to a CSV on
142
+ * disk via `saveCsv`.
143
+ *
144
+ * Called by: `downloadAllEventsOfDay` (simple wrapper) and can be
145
+ * imported directly by other modules that need the raw event list.
146
+ *
147
+ * @param clientId - OAuth client id for token operations.
148
+ * @param clientSecret - OAuth client secret for token operations.
149
+ * @param instanceUrl - Instance host used to construct the endpoint.
150
+ * @param subscriptionId - Events subscription id to fetch.
151
+ * @param sinceDate - Date string in `YYYY-MM-DD` to fetch events for.
152
+ * @param onlyData - When true, skip saving CSV to disk and return data only.
153
+ * @returns Array of event objects collected for the requested date.
154
+ */
66
155
  // All collected events
67
156
  const events = [];
68
157
  // Build initial request URL
@@ -126,6 +215,52 @@ async function downloadAllEventsOfDayCSV(clientId, clientSecret, instanceUrl, su
126
215
  }
127
216
  return events;
128
217
  }
218
+ /**
219
+ * Deterministically stringify a value for stable hashing.
220
+ *
221
+ * - Sorts object keys so property order does not affect output.
222
+ * - Recursively processes arrays and objects.
223
+ * - Falls back to `JSON.stringify` for primitive values.
224
+ *
225
+ * This is used by `generateHash` to produce a stable representation
226
+ * of event payloads before hashing.
227
+ *
228
+ * @param value - The value to stringify (object, array, or primitive).
229
+ * @returns A deterministic string representation of `value`.
230
+ */
231
+ function stableStringify(value) {
232
+ if (Array.isArray(value)) {
233
+ return `[${value.map(stableStringify).join(",")}]`;
234
+ }
235
+ if (value && typeof value === "object") {
236
+ return `{${Object.keys(value)
237
+ .sort()
238
+ .map(key => `"${key}":${stableStringify(value[key])}`)
239
+ .join(",")}}`;
240
+ }
241
+ return JSON.stringify(value);
242
+ }
243
+ /**
244
+ * Generate a deterministic SHA-256 hash for an event item.
245
+ *
246
+ * This uses a stable stringification of the selected fields so that
247
+ * object property ordering does not change the resulting hash. Useful
248
+ * for deduplication or consistent event identity across runs.
249
+ *
250
+ * @param item - Object containing `eventType`, `activityId`, `time`, and `activityChanges`.
251
+ * @returns Hex-encoded SHA-256 hash string representing the event.
252
+ */
253
+ function generateHash(item) {
254
+ return node_crypto_1.default
255
+ .createHash("sha256")
256
+ .update(stableStringify({
257
+ eventType: item.eventType,
258
+ activityId: item.activityId || item.resourceDetails.resourceId,
259
+ time: item.time,
260
+ activityChanges: item.activityChanges || item.inventoryChanges || {},
261
+ }))
262
+ .digest("hex");
263
+ }
129
264
  /**
130
265
  * Events of last two minutes
131
266
  * @param clientId
@@ -135,6 +270,25 @@ async function downloadAllEventsOfDayCSV(clientId, clientSecret, instanceUrl, su
135
270
  * @returns
136
271
  */
137
272
  async function downloadAllEventsOfDLastTwoMinutes(clientId, clientSecret, instanceUrl, subscriptionId) {
273
+ /**
274
+ * Download events from the last ~180 seconds and return them as an
275
+ * array.
276
+ *
277
+ * Differences from `downloadAllEventsOfDayCSV`:
278
+ * - The `since` timestamp is computed using `getTimeBefore180SecondsAlt`.
279
+ * - Validates the generated timestamp with `validateDateTimeStrict`.
280
+ * - For each fetched item, this function attaches a `uniqueId` field
281
+ * generated by `generateHash` (used for deduplication or tracing).
282
+ *
283
+ * Usage: lightweight polling helper to retrieve recent events for
284
+ * short-lived processing or monitoring.
285
+ *
286
+ * @param clientId - OAuth client id.
287
+ * @param clientSecret - OAuth client secret.
288
+ * @param instanceUrl - Instance host.
289
+ * @param subscriptionId - Subscription id for events.
290
+ * @returns Array of recent event objects.
291
+ */
138
292
  // All collected events
139
293
  const events = [];
140
294
  const since = (0, index_2.getTimeBefore180SecondsAlt)();
@@ -173,8 +327,8 @@ async function downloadAllEventsOfDLastTwoMinutes(clientId, clientSecret, instan
173
327
  // Prevent infinite looping
174
328
  if (nextPage === lastSeenPage) {
175
329
  repeatedPageCount++;
176
- if (repeatedPageCount > 10) {
177
- console.warn("⚠️ Pagination repeating same page more than 10 times. Stopping.");
330
+ if (repeatedPageCount > 15) {
331
+ console.warn("⚠️ Pagination repeating same page more than 15 times. Stopping.");
178
332
  break;
179
333
  }
180
334
  }
@@ -188,6 +342,9 @@ async function downloadAllEventsOfDLastTwoMinutes(clientId, clientSecret, instan
188
342
  break;
189
343
  }
190
344
  for (let k of page.items) {
345
+ k["uniqueId"] = generateHash(k);
346
+ k["Change"] = k.activityChanges || k.inventoryChanges || {};
347
+ k["Id"] = k.activityId || k.resourceDetails?.resourceId || '-';
191
348
  events.push(k);
192
349
  }
193
350
  }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Example script to fetch events using the `src/events` helpers.
3
+ *
4
+ * This copy lives under `src/` so `tsc` will emit it into `dist/scripts/`.
5
+ *
6
+ * Environment variables:
7
+ * - CLIENT_ID
8
+ * - CLIENT_SECRET
9
+ * - INSTANCE_URL (host portion, e.g. example-instance)
10
+ * - SUBSCRIPTION_ID
11
+ * - SINCE_DATE (optional) - YYYY-MM-DD to fetch a full day's events
12
+ * - ONLY_DATA (optional) - when "true", skip saving CSV in downloader
13
+ *
14
+ compile + run:
15
+ * tsc && node dist/events/scripts/fetch-events-example.js
16
+ */
17
+ export {};
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ /**
3
+ * Example script to fetch events using the `src/events` helpers.
4
+ *
5
+ * This copy lives under `src/` so `tsc` will emit it into `dist/scripts/`.
6
+ *
7
+ * Environment variables:
8
+ * - CLIENT_ID
9
+ * - CLIENT_SECRET
10
+ * - INSTANCE_URL (host portion, e.g. example-instance)
11
+ * - SUBSCRIPTION_ID
12
+ * - SINCE_DATE (optional) - YYYY-MM-DD to fetch a full day's events
13
+ * - ONLY_DATA (optional) - when "true", skip saving CSV in downloader
14
+ *
15
+ compile + run:
16
+ * tsc && node dist/events/scripts/fetch-events-example.js
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ const __1 = require("..");
20
+ const clientId = process.env.CLIENT_ID;
21
+ const clientSecret = process.env.CLIENT_SECRET;
22
+ const instanceUrl = process.env.INSTANCE_URL;
23
+ const subscriptionId = process.env.SUBSCRIPTION_ID;
24
+ if (!clientId || !clientSecret || !instanceUrl || !subscriptionId) {
25
+ console.error("Missing required env vars: CLIENT_ID, CLIENT_SECRET, INSTANCE_URL, SUBSCRIPTION_ID");
26
+ process.exit(1);
27
+ }
28
+ (async () => {
29
+ try {
30
+ if (process.env.SINCE_DATE) {
31
+ const sinceDate = process.env.SINCE_DATE;
32
+ const onlyData = process.env.ONLY_DATA === "true";
33
+ console.log(`Fetching events for day ${sinceDate}...`);
34
+ const events = await (0, __1.downloadAllEventsOfDayCSV)(clientId, clientSecret, instanceUrl, subscriptionId, sinceDate, onlyData);
35
+ console.log(`Fetched ${events.length} events for ${sinceDate}`);
36
+ if (events.length > 0)
37
+ console.log("Sample:", events[0]);
38
+ }
39
+ else {
40
+ console.log("Fetching events from last ~180 seconds...");
41
+ const recent = await (0, __1.downloadAllEventsOfDLastTwoMinutes)(clientId, clientSecret, instanceUrl, subscriptionId);
42
+ console.log(`Fetched ${recent.length} recent events`);
43
+ if (recent.length > 0)
44
+ console.log("Sample:", recent[0]);
45
+ }
46
+ }
47
+ catch (err) {
48
+ console.error("Error fetching events:", err);
49
+ process.exit(2);
50
+ }
51
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ofsc-utility",
3
- "version": "1.0.20",
3
+ "version": "1.0.22",
4
4
  "description": "A wrapper for Oracle Field Service REST API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -45,10 +45,11 @@
45
45
  "typescript": "^5.9.3"
46
46
  },
47
47
  "dependencies": {
48
+ "dotenv": "^17.4.2",
48
49
  "node-fetch": "^2.7.0",
49
50
  "tslib": "^2.8.1",
50
51
  "xlsx": "^0.18.5",
51
52
  "xlsx-js-style": "^1.2.0",
52
53
  "xmldom": "^0.6.0"
53
54
  }
54
- }
55
+ }