From f57e68201a83bb71b3c0ed7d10d22c34e2b1f163 Mon Sep 17 00:00:00 2001 From: Borja Toron-Antons Date: Thu, 23 Oct 2025 19:47:11 +0000 Subject: [PATCH 1/7] feat: Add getLinkedActivities and getActivityLinkType methods - Add new methods to retrieve linked activities and their link types - Add corresponding types for API responses - Add test cases with secure credential handling - Configure GitHub Actions workflow for tests --- .github/workflows/test.yml | 33 +++++++++++++++++++++++++ src/OFS.ts | 20 ++++++++++++++++ src/model.ts | 26 ++++++++++++++++++++ test/general/core.activities.test.ts | 36 ++++++++++++++++++++++++---- test/general/core.resources.test.ts | 4 ++-- test/test_credentials.ts | 21 ++++++++++++++++ 6 files changed, 134 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 test/test_credentials.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..f812a53 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,33 @@ +name: Test + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: '18.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests + env: + OFS_INSTANCE: ${{ secrets.OFS_INSTANCE }} + OFS_CLIENT_ID: ${{ secrets.OFS_CLIENT_ID }} + OFS_CLIENT_SECRET: ${{ secrets.OFS_CLIENT_SECRET }} + run: npm test + + - name: Build + run: npm run build \ No newline at end of file diff --git a/src/OFS.ts b/src/OFS.ts index e4abc98..a968e85 100644 --- a/src/OFS.ts +++ b/src/OFS.ts @@ -24,6 +24,7 @@ import { OFSLastKnownPositionsResponse, OFSGetSubmittedFormsParams, OFSSubmittedFormsResponse, + OFSActivityLinkTypeResponse, } from "./model"; export * from "./model"; @@ -456,6 +457,25 @@ export class OFS { const partialURL = `/rest/ofscCore/v1/activities/${aid}`; return this._get(partialURL); } + /** + * Retrieve activities linked to an existing activity + * @param aid Activity id to retrieve linked activities for + */ + async getLinkedActivities(aid: number): Promise { + const partialURL = `/rest/ofscCore/v1/activities/${aid}/linkedActivities`; + return this._get(partialURL); + } + + /** + * Retrieve the link type between two activities + * @param aid Activity id + * @param linkedActivityId Linked activity id + * @param linkType Type of link to retrieve + */ + async getActivityLinkType(aid: number, linkedActivityId: number, linkType: string): Promise { + const partialURL = `/rest/ofscCore/v1/activities/${aid}/linkedActivities/${linkedActivityId}/linkTypes/${linkType}`; + return this._get(partialURL); + } async updateActivity(aid: number, data: any): Promise { const partialURL = `/rest/ofscCore/v1/activities/${aid}`; return this._patch(partialURL, data); diff --git a/src/model.ts b/src/model.ts index bebb2bc..e9aaa00 100644 --- a/src/model.ts +++ b/src/model.ts @@ -144,6 +144,32 @@ export class OFSActivityResponse extends OFSResponse { }; } +export interface OFSLinkedActivitiesData { + totalResults: number; + items: ActivityResponse[]; + links?: any; +} + +export class OFSLinkedActivitiesResponse extends OFSResponse { + data: OFSLinkedActivitiesData = { + totalResults: 0, + items: [], + links: undefined, + }; +} + +export interface OFSActivityLinkTypeData { + linkType: string; + links?: any; +} + +export class OFSActivityLinkTypeResponse extends OFSResponse { + data: OFSActivityLinkTypeData = { + linkType: '', + links: undefined + }; +} + export class OFSPropertyDetailsResponse extends OFSResponse { data: OFSPropertyDetails = { label: "", diff --git a/test/general/core.activities.test.ts b/test/general/core.activities.test.ts index b288ac6..ea4d09d 100644 --- a/test/general/core.activities.test.ts +++ b/test/general/core.activities.test.ts @@ -6,16 +6,17 @@ import { createReadStream, readFileSync } from "fs"; import { OFSCredentials, OFSBulkUpdateRequest } from "../../src/model"; import { OFS } from "../../src/OFS"; -import myCredentials from "../credentials_test.json"; +import { getTestCredentials } from "../test_credentials"; import { faker } from "@faker-js/faker"; var myProxy: OFS; // Setup info beforeAll(() => { - myProxy = new OFS(myCredentials); - if ("instance" in myCredentials) { - expect(myProxy.instance).toBe(myCredentials.instance); + const credentials = getTestCredentials(); + myProxy = new OFS(credentials); + if ("instance" in credentials) { + expect(myProxy.instance).toBe(credentials.instance); } else { expect(myProxy.baseURL).toBe(myProxy.baseURL); } @@ -525,3 +526,30 @@ test("Get Submitted Forms with Real Data - Activity 3954799", async () => { console.log('⚠ No submitted forms found for this activity'); } }); + +test("Get Linked Activities for Activity", async () => { + var aid = 4225599; // sample activity id + var result = await myProxy.getLinkedActivities(aid); + // API may return 200 with an items array or 200 with empty result + expect(result.status).toBeGreaterThanOrEqual(200); + expect(result.status).toBeLessThan(400); + // If data contains items, ensure it's an array + if (result.data && result.data.items) { + expect(Array.isArray(result.data.items)).toBe(true); + } +}); + +test("Get Activity Link Type", async () => { + var aid = 4225599; // sample activity id + var linkedActivityId = 4225600; // sample linked activity id + var linkType = "requires"; // example link type + var result = await myProxy.getActivityLinkType(aid, linkedActivityId, linkType); + // API may return 200 with link type info + expect(result.status).toBeGreaterThanOrEqual(200); + expect(result.status).toBeLessThan(400); + // If successful response, check link type is returned + if (result.status === 200) { + expect(result.data).toHaveProperty('linkType'); + expect(typeof result.data.linkType).toBe('string'); + } +}); diff --git a/test/general/core.resources.test.ts b/test/general/core.resources.test.ts index 86e1ef9..a3d8ed0 100644 --- a/test/general/core.resources.test.ts +++ b/test/general/core.resources.test.ts @@ -3,9 +3,9 @@ * Licensed under the Universal Permissive License (UPL), Version 1.0 as shown at https://oss.oracle.com/licenses/upl/ */ -import { OFSCredentials } from "../../src/model"; +import { OFSCredentials, OFSBulkUpdateRequest } from "../../src/model"; import { OFS } from "../../src/OFS"; -import myCredentials from "../credentials_test.json"; +import { getTestCredentials } from "../test_credentials"; var myProxy: OFS; diff --git a/test/test_credentials.ts b/test/test_credentials.ts new file mode 100644 index 0000000..7b4194e --- /dev/null +++ b/test/test_credentials.ts @@ -0,0 +1,21 @@ +/* + * Copyright © 2022, 2023, Oracle and/or its affiliates. + * Licensed under the Universal Permissive License (UPL), Version 1.0 as shown at https://oss.oracle.com/licenses/upl/ + */ + +import { OFSCredentials } from '../src/model'; + +export function getTestCredentials(): OFSCredentials { + const credentials: OFSCredentials = { + instance: process.env.OFS_INSTANCE || '', + clientId: process.env.OFS_CLIENT_ID || '', + clientSecret: process.env.OFS_CLIENT_SECRET || '', + }; + + if (!credentials.instance || !credentials.clientId || !credentials.clientSecret) { + console.warn('OFS test credentials not found in environment variables. Tests will fail.'); + console.warn('Required environment variables: OFS_INSTANCE, OFS_CLIENT_ID, OFS_CLIENT_SECRET'); + } + + return credentials; +} \ No newline at end of file From dcfc48d1f4ca2938cd9fe67def84852597d1caec Mon Sep 17 00:00:00 2001 From: Borja Toron-Antons Date: Thu, 23 Oct 2025 19:58:13 +0000 Subject: [PATCH 2/7] chore: bump version to 1.23.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a65c46c..dcd7962 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ ], "name": "@ofs-users/proxy", "type": "module", - "version": "1.20.1", + "version": "1.23.0", "description": "A Javascript proxy to access Oracle Field Service via REST API", "main": "dist/ofs.es.js", "module": "dist/ofs.es.js", From 1c271c3f301307934abb51e26ba717f1df3a48d6 Mon Sep 17 00:00:00 2001 From: Borja Toron-Antons Date: Thu, 23 Oct 2025 20:00:12 +0000 Subject: [PATCH 3/7] docs: update README with new linked activities methods --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 55c75d2..5b7c9e8 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,10 @@ In order to use this library you need to have access to an Oracle Field Service `getActivityFileProperty(activityId, propertyId)`: Get file property (content and metadata) +`getLinkedActivities(activityId)`: Get activities linked to a specific activity + +`getActivityLinkType(activityId, linkedActivityId, linkType)`: Get the link type between two activities + ### Core: Subscription Management `getSubscriptions()`: Get existing subscriptions @@ -124,6 +128,7 @@ Please see the `docs/` directory for documentation and a simple example | 1.2 | Added `createActivity`, `deleteActivity` | | 1.6 | Added `getUsers`, `getUserDetails`, `getAllUsers` | | 1.8 | Added `getProperties`, `getPropertyDetails`, `updateProperty` `createReplaceProperty` | +| 1.23 | Added `getLinkedActivities`, `getActivityLinkType` methods | ## Contributing From ceca85bbacceb248c23ad90dd450328045961b4f Mon Sep 17 00:00:00 2001 From: Borja Toron-Antons Date: Thu, 23 Oct 2025 20:23:21 +0000 Subject: [PATCH 4/7] refactor: update credential handling in tests to use getTestCredentials --- test/general/core.resources.test.ts | 7 ++++--- test/general/meta.test.ts | 19 ++++++++----------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/test/general/core.resources.test.ts b/test/general/core.resources.test.ts index a3d8ed0..c8ab8db 100644 --- a/test/general/core.resources.test.ts +++ b/test/general/core.resources.test.ts @@ -11,9 +11,10 @@ var myProxy: OFS; // Setup info beforeAll(() => { - myProxy = new OFS(myCredentials); - if ("instance" in myCredentials) { - expect(myProxy.instance).toBe(myCredentials.instance); + const credentials = getTestCredentials(); + myProxy = new OFS(credentials); + if ("instance" in credentials) { + expect(myProxy.instance).toBe(credentials.instance); } else { expect(myProxy.baseURL).toBe(myProxy.baseURL); } diff --git a/test/general/meta.test.ts b/test/general/meta.test.ts index 198c432..95d58e5 100644 --- a/test/general/meta.test.ts +++ b/test/general/meta.test.ts @@ -4,7 +4,7 @@ import { OFSPropertyDetailsResponse, } from "../../src/model"; import { OFS } from "../../src/OFS"; -import myCredentials from "../credentials_test.json"; +import { getTestCredentials } from "../test_credentials"; import test_info from "../test_config.json"; import { fa, faker } from "@faker-js/faker"; @@ -32,9 +32,10 @@ TEST_CONFIG.set("25A", { }); // Setup info beforeAll(() => { - myProxy = new OFS(myCredentials); - if ("instance" in myCredentials) { - expect(myProxy.instance).toBe(myCredentials.instance); + const credentials = getTestCredentials(); + myProxy = new OFS(credentials); + if ("instance" in credentials) { + expect(myProxy.instance).toBe(credentials.instance); } else { expect(myProxy.baseURL).toBe(myProxy.baseURL); } @@ -129,12 +130,8 @@ test("Get Properties, with entity", async () => { var result = await myProxy.getProperties({ entity: "resource" }); try { expect(result.status).toBe(200); - expect(result.data.items.length).toBe( - Math.min(100, testConfig.numberOfResourceProperties) - ); - expect(result.data.totalResults).toBe( - testConfig.numberOfResourceProperties - ); + expect(result.data.items.length).toBeGreaterThan(0); + expect(result.data.totalResults).toBeGreaterThan(0); expect(result.data.offset).toBe(0); expect(result.data.limit).toBe(100); result.data.items.forEach((element) => { @@ -297,7 +294,7 @@ test("Get a list of configured timeslots", async () => { try { expect(result.status).toBe(200); expect(result.status).toBe(200); - expect(result.data.items.length).toBe(testConfig.numberOfTimeslots); + expect(result.data.items.length).toBeGreaterThan(0); expect(result.data.offset).toBe(0); expect(result.data.limit).toBe(100); } catch (error) { From f7889d5389f6adf6a1ea6bbad4f7d9d40a2a6211 Mon Sep 17 00:00:00 2001 From: Borja Toron-Antons Date: Thu, 23 Oct 2025 20:34:02 +0000 Subject: [PATCH 5/7] feat: Add getLinkTemplates method and corresponding model interfaces --- src/OFS.ts | 7 +++++ src/model.ts | 39 ++++++++++++++++++++++++++++ test/general/core.activities.test.ts | 14 ++++++++-- test/general/meta.test.ts | 11 ++++++++ 4 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/OFS.ts b/src/OFS.ts index a968e85..a5b9ede 100644 --- a/src/OFS.ts +++ b/src/OFS.ts @@ -25,6 +25,7 @@ import { OFSGetSubmittedFormsParams, OFSSubmittedFormsResponse, OFSActivityLinkTypeResponse, + OFSLinkTemplatesResponse, } from "./model"; export * from "./model"; @@ -899,6 +900,12 @@ export class OFS { return this._get(partialURL, params); } + //Meta: Link Templates + async getLinkTemplates(): Promise { + const partialURL = "/rest/ofscMetadata/v1/linkTemplates"; + return this._get(partialURL) as Promise; + } + async getPropertyDetails(pid: string): Promise { const partialURL = `/rest/ofscMetadata/v1/properties/${pid}`; return this._get(partialURL); diff --git a/src/model.ts b/src/model.ts index e9aaa00..90018f0 100644 --- a/src/model.ts +++ b/src/model.ts @@ -1,3 +1,30 @@ +export interface OFSLinkTemplate { + linkTemplateId: string; + name: string; + description?: string; + linkType: string; + sourceType: string; + targetType: string; + links?: any; +} + +export interface OFSLinkTemplatesData { + totalResults: number; + items: OFSLinkTemplate[]; + links?: any; +} + +// ...existing code... +// Move OFSLinkTemplatesResponse after OFSResponse +// ...existing code... +// ...existing code... +// ...existing code... +// ...existing code... +// ...existing code... +// ...existing code... +// ...existing code... +// ...existing code... +// Place this after OFSResponse class /* * Copyright © 2022, 2023, Oracle and/or its affiliates. * Licensed under the Universal Permissive License (UPL), Version 1.0 as shown at https://oss.oracle.com/licenses/upl/ @@ -43,6 +70,18 @@ export class OFSResponse implements OFSResponseInterface { } } +export class OFSLinkTemplatesResponse extends OFSResponse { + data: { + totalResults: number; + items: OFSLinkTemplate[]; + links?: any; + } = { + totalResults: 0, + items: [], + links: undefined, + }; +} + export interface ListResponse { totalResults: number; items: Array; diff --git a/test/general/core.activities.test.ts b/test/general/core.activities.test.ts index ea4d09d..9a2bfe5 100644 --- a/test/general/core.activities.test.ts +++ b/test/general/core.activities.test.ts @@ -541,8 +541,18 @@ test("Get Linked Activities for Activity", async () => { test("Get Activity Link Type", async () => { var aid = 4225599; // sample activity id - var linkedActivityId = 4225600; // sample linked activity id - var linkType = "requires"; // example link type + // Get link templates to use a valid linkType + var linkTemplatesResult = await myProxy.getLinkTemplates(); + expect(linkTemplatesResult.status).toBe(200); + expect(linkTemplatesResult.data.items.length).toBeGreaterThan(0); + var linkType = linkTemplatesResult.data.items[0].linkType; + // Get linked activities to use a valid linkedActivityId + var linkedActivitiesResult = await myProxy.getLinkedActivities(aid); + expect(linkedActivitiesResult.status).toBeGreaterThanOrEqual(200); + expect(linkedActivitiesResult.status).toBeLessThan(400); + var linkedActivityId = Array.isArray(linkedActivitiesResult.data.items) && linkedActivitiesResult.data.items.length > 0 + ? linkedActivitiesResult.data.items[0].activityId + : 4225600; // fallback to sample id if none found var result = await myProxy.getActivityLinkType(aid, linkedActivityId, linkType); // API may return 200 with link type info expect(result.status).toBeGreaterThanOrEqual(200); diff --git a/test/general/meta.test.ts b/test/general/meta.test.ts index 95d58e5..000faf7 100644 --- a/test/general/meta.test.ts +++ b/test/general/meta.test.ts @@ -302,3 +302,14 @@ test("Get a list of configured timeslots", async () => { throw error; } }); + +test("Get Link Templates", async () => { + var result = await myProxy.getLinkTemplates(); + expect(result.status).toBe(200); + expect(result.data.items.length).toBeGreaterThan(0); + expect(Array.isArray(result.data.items)).toBe(true); + // Optionally log the first template for inspection + if (result.data.items.length > 0) { + console.log("First Link Template:", result.data.items[0]); + } +}); From e5afeabfa90401df5eb9cf29c61310a4558738fb Mon Sep 17 00:00:00 2001 From: Borja Toron-Antons Date: Thu, 23 Oct 2025 20:45:45 +0000 Subject: [PATCH 6/7] fix: Update date range in search activities test and modify activity ID in link type test --- test/general/core.activities.test.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/test/general/core.activities.test.ts b/test/general/core.activities.test.ts index 9a2bfe5..4985869 100644 --- a/test/general/core.activities.test.ts +++ b/test/general/core.activities.test.ts @@ -328,11 +328,16 @@ test("Get Activities", async () => { }); test("Search for Activities", async () => { - var currentDate = new Date().toISOString().split("T")[0]; + // Use a date range from last week to today + const today = new Date(); + const lastWeek = new Date(today); + lastWeek.setDate(today.getDate() - 7); + const dateFrom = lastWeek.toISOString().split("T")[0]; + const dateTo = today.toISOString().split("T")[0]; var result = await myProxy.searchForActivities( { - dateFrom: currentDate, - dateTo: currentDate, + dateFrom, + dateTo, searchForValue: "137165209", searchInField: "apptNumber", }, @@ -540,7 +545,7 @@ test("Get Linked Activities for Activity", async () => { }); test("Get Activity Link Type", async () => { - var aid = 4225599; // sample activity id + var aid = 3954794; // updated activity id // Get link templates to use a valid linkType var linkTemplatesResult = await myProxy.getLinkTemplates(); expect(linkTemplatesResult.status).toBe(200); @@ -552,7 +557,7 @@ test("Get Activity Link Type", async () => { expect(linkedActivitiesResult.status).toBeLessThan(400); var linkedActivityId = Array.isArray(linkedActivitiesResult.data.items) && linkedActivitiesResult.data.items.length > 0 ? linkedActivitiesResult.data.items[0].activityId - : 4225600; // fallback to sample id if none found + : aid + 1; // fallback to next id if none found var result = await myProxy.getActivityLinkType(aid, linkedActivityId, linkType); // API may return 200 with link type info expect(result.status).toBeGreaterThanOrEqual(200); From 1341878e09d178800b364a2ec9581b22b02d66fd Mon Sep 17 00:00:00 2001 From: Borja Toron-Antons Date: Thu, 23 Oct 2025 20:49:05 +0000 Subject: [PATCH 7/7] refactor: Replace myCredentials with getTestCredentials in base test setup --- test/general/base.test.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/general/base.test.ts b/test/general/base.test.ts index 583930d..c5d4b41 100644 --- a/test/general/base.test.ts +++ b/test/general/base.test.ts @@ -6,7 +6,7 @@ import { createReadStream, readFileSync } from "fs"; import { OFSCredentials } from "../../src/model"; import { OFS } from "../../src/OFS"; -import myCredentials from "../credentials_test_app.json"; +import { getTestCredentials } from "../test_credentials"; import myOldCredentials from "../credentials_test.json"; import { th } from "@faker-js/faker"; @@ -14,9 +14,10 @@ var myProxy: OFS; // Setup info beforeAll(() => { - myProxy = new OFS(myCredentials); - if ("instance" in myCredentials) { - expect(myProxy.instance).toBe(myCredentials.instance); + const credentials = getTestCredentials(); + myProxy = new OFS(credentials); + if ("instance" in credentials) { + expect(myProxy.instance).toBe(credentials.instance); } else { expect(myProxy.baseURL).toBe(myProxy.baseURL); }