From 7c4a5826ed9cca07b3fb98c17df0ffc14ddbf8e4 Mon Sep 17 00:00:00 2001 From: Dmitry Borisov Date: Wed, 12 Nov 2025 16:01:47 +0300 Subject: [PATCH 1/7] add ApplicationClient --- src/common/index.ts | 1 + src/core/ApplicationClient.ts | 427 ++++++++++++++++++++++++++++++++++ 2 files changed, 428 insertions(+) create mode 100644 src/core/ApplicationClient.ts diff --git a/src/common/index.ts b/src/common/index.ts index 31d89b8..2cf5b82 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -42,6 +42,7 @@ export type { WorkflowRun, ExtendedWorkflowDef, ExtendedEventExecution, + ExtendedConductorApplication, EventMessage, HumanTaskUser, HumanTaskDefinition, diff --git a/src/core/ApplicationClient.ts b/src/core/ApplicationClient.ts new file mode 100644 index 0000000..5d6dfb9 --- /dev/null +++ b/src/core/ApplicationClient.ts @@ -0,0 +1,427 @@ +import { ApplicationResource } from "../common/open-api/sdk.gen"; +import { Client } from "../common/open-api/client"; +import { handleSdkError } from "./helpers"; +import type { Tag, ExtendedConductorApplication } from "../common"; + +export class ApplicationClient { + public readonly _client: Client; + + constructor(client: Client) { + this._client = client; + } + + /** + * Get all applications + * @returns {Promise} + * @throws {ConductorSdkError} + */ + public async getAllApplications(): Promise { + try { + const { data } = await ApplicationResource.listApplications({ + client: this._client, + throwOnError: true, + }); + + return data; + } catch (error: unknown) { + handleSdkError(error, `Failed to get all applications`); + } + } + + // TODO: check acceptable data while testing + /** + * Create an application + * @param {string} applicationName + * @returns {Promise} + * @throws {ConductorSdkError} + */ + public async createApplication( + applicationName: string + ): Promise { + try { + const { data } = await ApplicationResource.createApplication({ + body: { name: applicationName }, + client: this._client, + throwOnError: true, + }); + + return data; + } catch (error: unknown) { + handleSdkError(error, `Failed to create application`); + } + } + + // TODO: check return data while testing + /** + * Get application id by access key id + * @param {string} accessKeyId + * @returns {Promise>} + * @throws {ConductorSdkError} + */ + public async getAppByAccessKeyId( + accessKeyId: string + ): Promise> { + try { + const { data } = await ApplicationResource.getAppByAccessKeyId({ + path: { accessKeyId }, + client: this._client, + throwOnError: true, + }); + + return data; + } catch (error: unknown) { + handleSdkError( + error, + `Failed to get application by access key id: ${accessKeyId}` + ); + } + } + + /** + * Delete an access key + * @param {string} applicationId + * @param {string} keyId + * @returns {Promise} + * @throws {ConductorSdkError} + */ + public async deleteAccessKey( + applicationId: string, + keyId: string + ): Promise { + try { + await ApplicationResource.deleteAccessKey({ + path: { applicationId, keyId }, + client: this._client, + throwOnError: true, + }); + } catch (error: unknown) { + handleSdkError( + error, + `Failed to delete access key ${keyId} for application ${applicationId}` + ); + } + } + + // TODO: check return data while testing + /** + * Toggle the status of an access key + * @param {string} applicationId + * @param {string} keyId + * @returns {Promise>} + * @throws {ConductorSdkError} + */ + public async toggleAccessKeyStatus( + applicationId: string, + keyId: string + ): Promise> { + try { + const { data } = await ApplicationResource.toggleAccessKeyStatus({ + path: { applicationId, keyId }, + client: this._client, + throwOnError: true, + }); + + return data; + } catch (error: unknown) { + handleSdkError( + error, + `Failed to toggle access key status ${keyId} for application ${applicationId}` + ); + } + } + + /** + * Remove role from application user + * @param {string} applicationId + * @param {string} role + * @returns {Promise} + * @throws {ConductorSdkError} + */ + public async removeRoleFromApplicationUser( + applicationId: string, + role: string + ): Promise { + try { + const { data } = await ApplicationResource.removeRoleFromApplicationUser({ + path: { applicationId, role }, + client: this._client, + throwOnError: true, + }); + + return data; + } catch (error: unknown) { + handleSdkError( + error, + `Failed to remove role ${role} from application user ${applicationId}` + ); + } + } + + /** + * Add role to application user + * @param {string} applicationId + * @param {string} role + * @returns {Promise} + * @throws {ConductorSdkError} + */ + public async addRoleToApplicationUser( + applicationId: string, + role: string + ): Promise { + try { + const { data } = await ApplicationResource.addRoleToApplicationUser({ + path: { applicationId, role }, + client: this._client, + throwOnError: true, + }); + + return data; + } catch (error: unknown) { + handleSdkError( + error, + `Failed to add role ${role} to application user ${applicationId}` + ); + } + } + + /** + * Delete an application + * @param {string} applicationId + * @returns {Promise} + * @throws {ConductorSdkError} + */ + public async deleteApplication(applicationId: string): Promise { + try { + await ApplicationResource.deleteApplication({ + path: { id: applicationId }, + client: this._client, + throwOnError: true, + }); + } catch (error: unknown) { + handleSdkError(error, `Failed to delete application: ${applicationId}`); + } + } + + /** + * Get an application by id + * @param {string} applicationId + * @returns {Promise} + * @throws {ConductorSdkError} + */ + public async getApplication( + applicationId: string + ): Promise { + try { + const { data } = await ApplicationResource.getApplication({ + path: { id: applicationId }, + client: this._client, + throwOnError: true, + }); + + return data; + } catch (error: unknown) { + handleSdkError(error, `Failed to get application: ${applicationId}`); + } + } + + /** + * Update an application + * @param {string} applicationId + * @param {string} newApplicationName + * @returns {Promise} + * @throws {ConductorSdkError} + */ + public async updateApplication( + applicationId: string, + newApplicationName: string + ): Promise { + try { + const { data } = await ApplicationResource.updateApplication({ + path: { id: applicationId }, + body: { name: newApplicationName }, + client: this._client, + throwOnError: true, + }); + + return data; + } catch (error: unknown) { + handleSdkError(error, `Failed to update application ${applicationId}`); + } + } + + //TODO: check return data while testing + /** + * Get application's access keys + * @param {string} applicationId + * @returns {Promise>} + * @throws {ConductorSdkError} + */ + public async getAccessKeys( + applicationId: string + ): Promise> { + try { + const { data } = await ApplicationResource.getAccessKeys({ + path: { id: applicationId }, + client: this._client, + throwOnError: true, + }); + + return data; + } catch (error: unknown) { + handleSdkError( + error, + `Failed to get access keys for application: ${applicationId}` + ); + } + } + + //TODO: check return data while testing + /** + * Create an access key for an application + * @param {string} applicationId + * @returns {Promise>} + * @throws {ConductorSdkError} + */ + public async createAccessKey( + applicationId: string + ): Promise> { + try { + const { data } = await ApplicationResource.createAccessKey({ + path: { id: applicationId }, + client: this._client, + throwOnError: true, + }); + + return data; + } catch (error: unknown) { + handleSdkError( + error, + `Failed to create access key for application: ${applicationId}` + ); + } + } + + /** + * Delete application tags + * @param {string} applicationId + * @param {Tag[]} tags + * @returns {Promise} + * @throws {ConductorSdkError} + */ + public async deleteApplicationTags( + applicationId: string, + tags: Tag[] + ): Promise { + try { + await ApplicationResource.deleteTagForApplication({ + path: { id: applicationId }, + body: tags, + client: this._client, + throwOnError: true, + }); + } catch (error: unknown) { + handleSdkError( + error, + `Failed to delete tags for application: ${applicationId}` + ); + } + } + + /** + * Delete a single application tag + * @param {string} applicationId + * @param {Tag} tag + * @returns {Promise} + * @throws {ConductorSdkError} + */ + public async deleteApplicationTag( + applicationId: string, + tag: Tag + ): Promise { + try { + await ApplicationResource.deleteTagForApplication({ + path: { id: applicationId }, + body: [tag], + client: this._client, + throwOnError: true, + }); + } catch (error: unknown) { + handleSdkError( + error, + `Failed to delete a tag for application: ${applicationId}` + ); + } + } + + /** + * Get application tags + * @param {string} applicationId + * @returns {Promise} + * @throws {ConductorSdkError} + */ + public async getApplicationTags(applicationId: string): Promise { + try { + const { data } = await ApplicationResource.getTagsForApplication({ + path: { id: applicationId }, + client: this._client, + throwOnError: true, + }); + + return data; + } catch (error: unknown) { + handleSdkError( + error, + `Failed to get tags for application: ${applicationId}` + ); + } + } + + /** + * Add application tags + * @param {string} applicationId + * @param {Tag[]} tags + * @returns {Promise} + * @throws {ConductorSdkError} + */ + public async addApplicationTags( + applicationId: string, + tags: Tag[] + ): Promise { + try { + await ApplicationResource.putTagForApplication({ + path: { id: applicationId }, + body: tags, + client: this._client, + throwOnError: true, + }); + } catch (error: unknown) { + handleSdkError(error, `Failed to add application tags: ${applicationId}`); + } + } + + /** + * Add a single application tag + * @param {string} applicationId + * @param {Tag} tag + * @returns {Promise} + * @throws {ConductorSdkError} + */ + public async addApplicationTag( + applicationId: string, + tag: Tag + ): Promise { + try { + await ApplicationResource.putTagForApplication({ + path: { id: applicationId }, + body: [tag], + client: this._client, + throwOnError: true, + }); + } catch (error: unknown) { + handleSdkError( + error, + `Failed to add an application tag: ${applicationId}` + ); + } + } +} From 0af288dda518dce8997939d9e4294d6d77187226 Mon Sep 17 00:00:00 2001 From: Dmitry Borisov Date: Thu, 13 Nov 2025 15:38:42 +0300 Subject: [PATCH 2/7] Update version (2.4.0) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 77790e0..120a66d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@io-orkes/conductor-javascript", "description": "Typescript Client for Netflix Conductor", - "version": "v2.3.0", + "version": "v2.4.0", "private": false, "homepage": "https://orkes.io", "repository": { From 386cf18acc7fd5e0716e84b3d5462c9f514c3a73 Mon Sep 17 00:00:00 2001 From: Dmitry Borisov Date: Thu, 13 Nov 2025 21:07:19 +0300 Subject: [PATCH 3/7] add tests, fix open-api spec issues --- .../common/ApplicationClient.test.ts | 575 ++++++++++++++++++ src/common/index.ts | 12 +- src/common/types.ts | 21 + src/core/ApplicationClient.ts | 66 +- src/core/index.ts | 4 +- 5 files changed, 637 insertions(+), 41 deletions(-) create mode 100644 integration-tests/common/ApplicationClient.test.ts diff --git a/integration-tests/common/ApplicationClient.test.ts b/integration-tests/common/ApplicationClient.test.ts new file mode 100644 index 0000000..a5ca8a0 --- /dev/null +++ b/integration-tests/common/ApplicationClient.test.ts @@ -0,0 +1,575 @@ +import { + expect, + describe, + test, + jest, + beforeAll, + afterEach, +} from "@jest/globals"; +import { orkesConductorClient } from "../../src/orkes"; +import { ApplicationClient } from "../../src/core"; +import type { Tag } from "../../src/common"; + +describe("ApplicationClient", () => { + jest.setTimeout(60000); + + let applicationClient: ApplicationClient; + const testAppsToCleanup: string[] = []; + + beforeAll(async () => { + applicationClient = new ApplicationClient(await orkesConductorClient()); + }); + + afterEach(async () => { + for (const appId of testAppsToCleanup) { + try { + await applicationClient.deleteApplication(appId); + } catch (error: unknown) { + console.debug(`Failed to delete application ${appId}:`, error); + } + } + testAppsToCleanup.length = 0; + }); + + // Helper function to create unique names + const createUniqueName = (prefix: string) => + `jsSdkTest-${prefix}-${Date.now()}`; + + describe("Application Management", () => { + test("Should create a new application", async () => { + const appName = createUniqueName("test-app-create"); + + const createdApp = await applicationClient.createApplication(appName); + + expect(createdApp).toBeDefined(); + expect(createdApp.name).toEqual(appName); + expect(createdApp.id).toBeDefined(); + expect(typeof createdApp.id).toBe("string"); + expect(createdApp.createdBy).toBeDefined(); + expect(typeof createdApp.createTime).toBe("number"); + + testAppsToCleanup.push(createdApp.id); + }); + + test("Should get all applications", async () => { + const appName = createUniqueName("test-app-get-all"); + + const createdApp = await applicationClient.createApplication(appName); + + expect(createdApp.id).toBeDefined(); + + const applications = await applicationClient.getAllApplications(); + + expect(Array.isArray(applications)).toBe(true); + expect(applications.length).toBeGreaterThan(0); + + const foundApp = applications.find((app) => app.id === createdApp.id); + expect(foundApp).toBeDefined(); + expect(foundApp?.name).toEqual(appName); + expect(foundApp?.id).toEqual(createdApp.id); + + testAppsToCleanup.push(createdApp.id); + }); + + test("Should get an application by id", async () => { + const appName = createUniqueName("test-app-get-by-id"); + + const createdApp = await applicationClient.createApplication(appName); + + expect(createdApp.id).toBeDefined(); + + const retrievedApp = await applicationClient.getApplication( + createdApp.id + ); + + expect(retrievedApp).toBeDefined(); + expect(retrievedApp.id).toEqual(createdApp.id); + expect(retrievedApp.name).toEqual(appName); + expect(retrievedApp.createdBy).toBeDefined(); + expect(typeof retrievedApp.createTime).toBe("number"); + + testAppsToCleanup.push(createdApp.id); + }); + + test("Should update an application", async () => { + const appName = createUniqueName("test-app-update"); + const newAppName = createUniqueName("test-app-updated"); + + const createdApp = await applicationClient.createApplication(appName); + + expect(createdApp.id).toBeDefined(); + + const updatedApp = await applicationClient.updateApplication( + createdApp.id, + newAppName + ); + + expect(updatedApp).toBeDefined(); + expect(updatedApp.id).toEqual(createdApp.id); + expect(updatedApp.name).toEqual(newAppName); + expect(updatedApp.name).not.toEqual(appName); + + const retrievedApp = await applicationClient.getApplication( + createdApp.id + ); + expect(retrievedApp.name).toEqual(newAppName); + + testAppsToCleanup.push(createdApp.id); + }); + + test("Should delete an application", async () => { + const appName = createUniqueName("test-app"); + + const createdApp = await applicationClient.createApplication(appName); + expect(createdApp.id).toBeDefined(); + + const retrievedApp = await applicationClient.getApplication( + createdApp.id + ); + expect(retrievedApp.id).toEqual(createdApp.id); + + await expect( + applicationClient.deleteApplication(createdApp.id) + ).resolves.not.toThrow(); + + await expect( + applicationClient.getApplication(createdApp.id) + ).rejects.toThrow(); + }); + }); + + describe("Access Key Management", () => { + test("Should create an access key for an application", async () => { + const appName = createUniqueName("test-app-access-key-create"); + + const createdApp = await applicationClient.createApplication(appName); + + expect(createdApp.id).toBeDefined(); + + const accessKey = await applicationClient.createAccessKey(createdApp.id); + + expect(accessKey).toBeDefined(); + expect(typeof accessKey).toBe("object"); + expect(accessKey.id).toBeDefined(); + expect(typeof accessKey.id).toBe("string"); + expect(accessKey.secret).toBeDefined(); + expect(typeof accessKey.secret).toBe("string"); + + testAppsToCleanup.push(createdApp.id); + }); + + test("Should get access keys for an application", async () => { + const appName = createUniqueName("test-app-get-access-keys"); + + const createdApp = await applicationClient.createApplication(appName); + expect(createdApp.id).toBeDefined(); + + const createdKey = await applicationClient.createAccessKey(createdApp.id); + expect(createdKey.id).toBeDefined(); + + const accessKeys = await applicationClient.getAccessKeys(createdApp.id); + + expect(accessKeys).toBeDefined(); + expect(Array.isArray(accessKeys)).toBe(true); + expect(accessKeys.length).toBeGreaterThan(0); + + const foundKey = accessKeys.find((key) => key.id === createdKey.id); + expect(foundKey).toBeDefined(); + expect(foundKey?.id).toEqual(createdKey.id); + + testAppsToCleanup.push(createdApp.id); + }); + + test("Should toggle access key status", async () => { + const appName = createUniqueName("test-app"); + + const createdApp = await applicationClient.createApplication(appName); + expect(createdApp.id).toBeDefined(); + + const accessKey = await applicationClient.createAccessKey(createdApp.id); + + expect(accessKey.id).toBeDefined(); + + const toggleResult = await applicationClient.toggleAccessKeyStatus( + createdApp.id, + accessKey.id + ); + + expect(toggleResult).toBeDefined(); + expect(typeof toggleResult).toBe("object"); + expect(toggleResult.id).toEqual(accessKey.id); + expect(toggleResult.createdAt).toBeDefined(); + expect(typeof toggleResult.createdAt).toBe("number"); + expect(toggleResult.status).toEqual("INACTIVE"); + + testAppsToCleanup.push(createdApp.id); + }); + + test("Should delete an access key", async () => { + const appName = createUniqueName("test-app"); + + const createdApp = await applicationClient.createApplication(appName); + expect(createdApp.id).toBeDefined(); + + const accessKey = await applicationClient.createAccessKey(createdApp.id); + expect(accessKey.id).toBeDefined(); + + await expect( + applicationClient.deleteAccessKey(createdApp.id, accessKey.id) + ).resolves.not.toThrow(); + + testAppsToCleanup.push(createdApp.id); + }); + + test("Should get app by access key id", async () => { + const appName = createUniqueName("test-app-get-by-access-key"); + + const createdApp = await applicationClient.createApplication(appName); + expect(createdApp.id).toBeDefined(); + + const accessKey = await applicationClient.createAccessKey(createdApp.id); + expect(accessKey.id).toBeDefined(); + + const appByAccessKey = await applicationClient.getAppByAccessKeyId( + accessKey.id + ); + + expect(appByAccessKey).toBeDefined(); + expect(typeof appByAccessKey).toBe("object"); + expect(appByAccessKey.id).toEqual(createdApp.id); + expect(appByAccessKey.name).toEqual(appName); + expect(appByAccessKey.createdBy).toBeDefined(); + expect(typeof appByAccessKey.createTime).toBe("number"); + + testAppsToCleanup.push(createdApp.id); + }); + }); + + describe("Role Management", () => { + test("Should add and remove role from application user", async () => { + const appName = createUniqueName("test-app-role"); + const role = "USER"; + + const createdApp = await applicationClient.createApplication(appName); + expect(createdApp.id).toBeDefined(); + + await expect( + applicationClient.addRoleToApplicationUser(createdApp.id, role) + ).resolves.not.toThrow(); + + await expect( + applicationClient.removeRoleFromApplicationUser(createdApp.id, role) + ).resolves.not.toThrow(); + + testAppsToCleanup.push(createdApp.id); + }); + }); + + describe("Tag Management", () => { + test("Should add a single tag to an application", async () => { + const appName = createUniqueName("test-app-add-tag"); + + const createdApp = await applicationClient.createApplication(appName); + expect(createdApp.id).toBeDefined(); + + const tag: Tag = { + key: "test-key", + value: "test-value", + }; + + await expect( + applicationClient.addApplicationTag(createdApp.id, tag) + ).resolves.not.toThrow(); + + const tags = await applicationClient.getApplicationTags(createdApp.id); + + expect(Array.isArray(tags)).toBe(true); + expect(tags.length).toBeGreaterThanOrEqual(1); + + const foundTag = tags.find( + (t) => t.key === tag.key && t.value === tag.value + ); + expect(foundTag).toBeDefined(); + expect(foundTag?.key).toEqual(tag.key); + expect(foundTag?.value).toEqual(tag.value); + + testAppsToCleanup.push(createdApp.id); + }); + + test("Should add multiple tags to an application", async () => { + const appName = createUniqueName("test-app"); + + const createdApp = await applicationClient.createApplication(appName); + expect(createdApp.id).toBeDefined(); + + const tags: Tag[] = [ + { key: "test-key-1", value: "test-value-1" }, + { key: "test-key-2", value: "test-value-2" }, + { key: "environment", value: "test" }, + ]; + + await expect( + applicationClient.addApplicationTags(createdApp.id, tags) + ).resolves.not.toThrow(); + + const retrievedTags = await applicationClient.getApplicationTags( + createdApp.id + ); + + expect(Array.isArray(retrievedTags)).toBe(true); + expect(retrievedTags.length).toBeGreaterThanOrEqual(tags.length); + + tags.forEach((expectedTag) => { + const foundTag = retrievedTags.find( + (tag) => + tag.key === expectedTag.key && tag.value === expectedTag.value + ); + expect(foundTag).toBeDefined(); + expect(foundTag?.key).toEqual(expectedTag.key); + expect(foundTag?.value).toEqual(expectedTag.value); + }); + + testAppsToCleanup.push(createdApp.id); + }); + + test("Should get tags for an application", async () => { + const appName = createUniqueName("test-app"); + + const createdApp = await applicationClient.createApplication(appName); + expect(createdApp.id).toBeDefined(); + + const expectedTags: Tag[] = [ + { key: "test-key-1", value: "test-value-1" }, + { key: "test-key-2", value: "test-value-2" }, + ]; + + await applicationClient.addApplicationTags(createdApp.id, expectedTags); + + const retrievedTags = await applicationClient.getApplicationTags( + createdApp.id + ); + + expect(Array.isArray(retrievedTags)).toBe(true); + expect(retrievedTags.length).toBeGreaterThanOrEqual(expectedTags.length); + + expectedTags.forEach((expectedTag) => { + const foundTag = retrievedTags.find( + (tag) => + tag.key === expectedTag.key && tag.value === expectedTag.value + ); + expect(foundTag).toBeDefined(); + }); + + testAppsToCleanup.push(createdApp.id); + }); + + test("Should delete a single tag from an application", async () => { + const appName = createUniqueName("test-app"); + + const createdApp = await applicationClient.createApplication(appName); + expect(createdApp.id).toBeDefined(); + + const tags: Tag[] = [ + { key: "test-key-1", value: "test-value-1" }, + { key: "test-key-2", value: "test-value-2" }, + ]; + + await applicationClient.addApplicationTags(createdApp.id, tags); + + const tagToDelete = tags[0]; + const remainingTag = tags[1]; + + await expect( + applicationClient.deleteApplicationTag(createdApp.id, tagToDelete) + ).resolves.not.toThrow(); + + const retrievedTags = await applicationClient.getApplicationTags( + createdApp.id + ); + + const foundDeletedTag = retrievedTags.find( + (tag) => tag.key === tagToDelete.key && tag.value === tagToDelete.value + ); + expect(foundDeletedTag).toBeUndefined(); + + const foundRemainingTag = retrievedTags.find( + (tag) => + tag.key === remainingTag.key && tag.value === remainingTag.value + ); + expect(foundRemainingTag).toBeDefined(); + + testAppsToCleanup.push(createdApp.id); + }); + + test("Should delete multiple tags from an application", async () => { + const appName = createUniqueName("test-app"); + + const createdApp = await applicationClient.createApplication(appName); + expect(createdApp.id).toBeDefined(); + + if (!createdApp.id) return; + testAppsToCleanup.push(createdApp.id); + + const tags: Tag[] = [ + { key: "test-key-1", value: "test-value-1" }, + { key: "test-key-2", value: "test-value-2" }, + { key: "test-key-3", value: "test-value-3" }, + ]; + + await applicationClient.addApplicationTags(createdApp.id, tags); + + const tagsToDelete = [tags[0], tags[1]]; + const remainingTag = tags[2]; + + await expect( + applicationClient.deleteApplicationTags(createdApp.id, tagsToDelete) + ).resolves.not.toThrow(); + + const retrievedTags = await applicationClient.getApplicationTags( + createdApp.id + ); + + tagsToDelete.forEach((deletedTag) => { + const foundTag = retrievedTags.find( + (tag) => tag.key === deletedTag.key && tag.value === deletedTag.value + ); + expect(foundTag).toBeUndefined(); + }); + + const foundRemainingTag = retrievedTags.find( + (tag) => + tag.key === remainingTag.key && tag.value === remainingTag.value + ); + expect(foundRemainingTag).toBeDefined(); + }); + }); + + describe("Error Handling", () => { + test("Should throw error when getting non-existent application", async () => { + const nonExistentId = createUniqueName("non-existent-app-id"); + + await expect( + applicationClient.getApplication(nonExistentId) + ).rejects.toThrow(); + }); + + test("Should throw error when updating non-existent application", async () => { + const nonExistentId = createUniqueName("non-existent-app-id"); + const newName = createUniqueName("new-name"); + + await expect( + applicationClient.updateApplication(nonExistentId, newName) + ).rejects.toThrow(); + }); + + test("Should throw error when deleting non-existent application", async () => { + const nonExistentId = createUniqueName("non-existent-app-id"); + + await expect( + applicationClient.deleteApplication(nonExistentId) + ).rejects.toThrow(); + }); + + test("Should throw error when creating access key for non-existent application", async () => { + const nonExistentId = createUniqueName("non-existent-app-id"); + + await expect( + applicationClient.createAccessKey(nonExistentId) + ).rejects.toThrow(); + }); + + test("Should throw error when getting access keys for non-existent application", async () => { + const nonExistentId = createUniqueName("non-existent-app-id"); + + await expect( + applicationClient.getAccessKeys(nonExistentId) + ).rejects.toThrow(); + }); + + test("Should throw error when deleting non-existent access key", async () => { + const appName = createUniqueName("test-app"); + + const createdApp = await applicationClient.createApplication(appName); + expect(createdApp.id).toBeDefined(); + + const nonExistentKeyId = createUniqueName("non-existent-key-id"); + + await expect( + applicationClient.deleteAccessKey(createdApp.id, nonExistentKeyId) + ).rejects.toThrow(); + + testAppsToCleanup.push(createdApp.id); + }); + + test("Should throw error when toggling status of non-existent access key", async () => { + const appName = createUniqueName("test-app"); + + const createdApp = await applicationClient.createApplication(appName); + expect(createdApp.id).toBeDefined(); + + const nonExistentKeyId = createUniqueName("non-existent-key-id"); + await expect( + applicationClient.toggleAccessKeyStatus(createdApp.id, nonExistentKeyId) + ).rejects.toThrow(); + + testAppsToCleanup.push(createdApp.id); + }); + + test("Should throw error when getting app by non-existent access key id", async () => { + const nonExistentKeyId = createUniqueName("non-existent-key-id"); + + await expect( + applicationClient.getAppByAccessKeyId(nonExistentKeyId) + ).rejects.toThrow(); + }); + + test("Should throw error when adding role to non-existent application", async () => { + const nonExistentId = createUniqueName("non-existent-app-id"); + const role = "USER"; + + await expect( + applicationClient.addRoleToApplicationUser(nonExistentId, role) + ).rejects.toThrow(); + }); + + test("Should throw error when removing role from non-existent application", async () => { + const nonExistentId = createUniqueName("non-existent-app-id"); + const role = "USER"; + + await expect( + applicationClient.removeRoleFromApplicationUser(nonExistentId, role) + ).rejects.toThrow(); + }); + + test("Should throw error when creating application with empty name", async () => { + await expect(applicationClient.createApplication("")).rejects.toThrow(); + }); + + // TODO: Uncomment these tests after BE update with related fixes + // test("Should throw error when getting tags for non-existent application", async () => { + // const nonExistentId = createUniqueName("non-existent-app-id"); + + // await expect( + // applicationClient.getApplicationTags(nonExistentId) + // ).rejects.toThrow(); + // }); + + // test("Should throw error when adding tags to non-existent application", async () => { + // const nonExistentId = createUniqueName("non-existent-app-id"); + // const tags: Tag[] = [{ key: "test", value: "test" }]; + + // await expect( + // applicationClient.addApplicationTags(nonExistentId, tags) + // ).rejects.toThrow(); + // }); + + // test("Should throw error when deleting tags from non-existent application", async () => { + // const nonExistentId = createUniqueName("non-existent-app-id"); + // const tags: Tag[] = [{ key: "test", value: "test" }]; + + // await expect( + // applicationClient.deleteApplicationTags(nonExistentId, tags) + // ).rejects.toThrow(); + // }); + }); +}); diff --git a/src/common/index.ts b/src/common/index.ts index 2cf5b82..c3aa803 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -42,7 +42,7 @@ export type { WorkflowRun, ExtendedWorkflowDef, ExtendedEventExecution, - ExtendedConductorApplication, + // ExtendedConductorApplication, TODO: restore after OpenAPI spec update and remove export from ./types block EventMessage, HumanTaskUser, HumanTaskDefinition, @@ -55,7 +55,13 @@ export type { HumanTaskEntry, } from "./open-api"; -export type { ExtendedTaskDef, SignalResponse } from "./types"; +export type { + ExtendedTaskDef, + SignalResponse, + AccessKey, + AccessKeyInfo, + ExtendedConductorApplication, +} from "./types"; // todo: remove after April 2026 (backward compatibility types) export * from "./deprecated-types"; @@ -64,7 +70,7 @@ export * from "./deprecated-types"; * Export types needed for client's return type in case if user is building another lib on top of sdk with declaration files * @deprecated * to import all the types below manually while using SDK since these types could change in future without backward compatibility - * TODO: remove after April 2026 + * TODO: remove after April 2026 */ export type { Auth, diff --git a/src/common/types.ts b/src/common/types.ts index fd7e5fe..4af3ff1 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -1,6 +1,8 @@ import { ExtendedTaskDef as OpenApiExtendedTaskDef, SignalResponse as OpenApiSignalResponse, + ExtendedConductorApplication as OpenApiExtendedConductorApplication, + Tag, } from "./open-api"; import { Task } from "./"; @@ -264,3 +266,22 @@ export interface SignalResponse extends OpenApiSignalResponse { taskDefName?: string; workflowType?: string; } + +// TODO: need to remove this once AccessKey type is added to OpenAPI spec +export interface AccessKey { + id: string; + secret: string; +} + +// TODO: need to remove this once type is added to OpenAPI spec +export interface AccessKeyInfo { + id: string; + createdAt: number; + status: "ACTIVE" | "INACTIVE"; +} + +// TODO: need to remove this once ExtendedConductorApplication type is corrected in OpenAPI spec +export interface ExtendedConductorApplication + extends Required> { + tags?: Array; +} diff --git a/src/core/ApplicationClient.ts b/src/core/ApplicationClient.ts index 5d6dfb9..5a35f5c 100644 --- a/src/core/ApplicationClient.ts +++ b/src/core/ApplicationClient.ts @@ -1,7 +1,12 @@ import { ApplicationResource } from "../common/open-api/sdk.gen"; import { Client } from "../common/open-api/client"; import { handleSdkError } from "./helpers"; -import type { Tag, ExtendedConductorApplication } from "../common"; +import type { + Tag, + ExtendedConductorApplication, + AccessKey, + AccessKeyInfo, +} from "../common"; export class ApplicationClient { public readonly _client: Client; @@ -22,13 +27,12 @@ export class ApplicationClient { throwOnError: true, }); - return data; + return data as ExtendedConductorApplication[]; // TODO: remove cast after OpenApi spec type update } catch (error: unknown) { handleSdkError(error, `Failed to get all applications`); } } - // TODO: check acceptable data while testing /** * Create an application * @param {string} applicationName @@ -45,22 +49,21 @@ export class ApplicationClient { throwOnError: true, }); - return data; + return data as unknown as ExtendedConductorApplication; // TODO: remove cast after OpenApi spec type update } catch (error: unknown) { handleSdkError(error, `Failed to create application`); } } - // TODO: check return data while testing /** - * Get application id by access key id + * Get application by access key id * @param {string} accessKeyId - * @returns {Promise>} + * @returns {Promise} * @throws {ConductorSdkError} */ public async getAppByAccessKeyId( accessKeyId: string - ): Promise> { + ): Promise { try { const { data } = await ApplicationResource.getAppByAccessKeyId({ path: { accessKeyId }, @@ -68,7 +71,7 @@ export class ApplicationClient { throwOnError: true, }); - return data; + return data as unknown as ExtendedConductorApplication; // TODO: remove cast after OpenApi spec type update } catch (error: unknown) { handleSdkError( error, @@ -102,18 +105,17 @@ export class ApplicationClient { } } - // TODO: check return data while testing /** * Toggle the status of an access key * @param {string} applicationId * @param {string} keyId - * @returns {Promise>} + * @returns {Promise} * @throws {ConductorSdkError} */ public async toggleAccessKeyStatus( applicationId: string, keyId: string - ): Promise> { + ): Promise { try { const { data } = await ApplicationResource.toggleAccessKeyStatus({ path: { applicationId, keyId }, @@ -121,7 +123,7 @@ export class ApplicationClient { throwOnError: true, }); - return data; + return data as unknown as AccessKeyInfo; // TODO: remove cast after OpenApi spec type update } catch (error: unknown) { handleSdkError( error, @@ -134,21 +136,19 @@ export class ApplicationClient { * Remove role from application user * @param {string} applicationId * @param {string} role - * @returns {Promise} + * @returns {Promise} * @throws {ConductorSdkError} */ public async removeRoleFromApplicationUser( applicationId: string, role: string - ): Promise { + ): Promise { try { - const { data } = await ApplicationResource.removeRoleFromApplicationUser({ + await ApplicationResource.removeRoleFromApplicationUser({ path: { applicationId, role }, client: this._client, throwOnError: true, }); - - return data; } catch (error: unknown) { handleSdkError( error, @@ -161,21 +161,19 @@ export class ApplicationClient { * Add role to application user * @param {string} applicationId * @param {string} role - * @returns {Promise} + * @returns {Promise} * @throws {ConductorSdkError} */ public async addRoleToApplicationUser( applicationId: string, role: string - ): Promise { + ): Promise { try { - const { data } = await ApplicationResource.addRoleToApplicationUser({ + await ApplicationResource.addRoleToApplicationUser({ path: { applicationId, role }, client: this._client, throwOnError: true, }); - - return data; } catch (error: unknown) { handleSdkError( error, @@ -218,7 +216,7 @@ export class ApplicationClient { throwOnError: true, }); - return data; + return data as unknown as ExtendedConductorApplication; // TODO: remove cast after OpenApi spec type update } catch (error: unknown) { handleSdkError(error, `Failed to get application: ${applicationId}`); } @@ -243,22 +241,19 @@ export class ApplicationClient { throwOnError: true, }); - return data; + return data as unknown as ExtendedConductorApplication; // TODO: remove cast after OpenApi spec type update } catch (error: unknown) { handleSdkError(error, `Failed to update application ${applicationId}`); } } - //TODO: check return data while testing /** * Get application's access keys * @param {string} applicationId - * @returns {Promise>} + * @returns {Promise} * @throws {ConductorSdkError} */ - public async getAccessKeys( - applicationId: string - ): Promise> { + public async getAccessKeys(applicationId: string): Promise { try { const { data } = await ApplicationResource.getAccessKeys({ path: { id: applicationId }, @@ -266,7 +261,7 @@ export class ApplicationClient { throwOnError: true, }); - return data; + return data as unknown as AccessKeyInfo[]; // TODO: remove cast after OpenApi spec update } catch (error: unknown) { handleSdkError( error, @@ -275,16 +270,13 @@ export class ApplicationClient { } } - //TODO: check return data while testing /** * Create an access key for an application * @param {string} applicationId - * @returns {Promise>} + * @returns {Promise} * @throws {ConductorSdkError} */ - public async createAccessKey( - applicationId: string - ): Promise> { + public async createAccessKey(applicationId: string): Promise { try { const { data } = await ApplicationResource.createAccessKey({ path: { id: applicationId }, @@ -292,7 +284,7 @@ export class ApplicationClient { throwOnError: true, }); - return data; + return data as unknown as AccessKey; // TODO: remove cast after OpenApi spec update } catch (error: unknown) { handleSdkError( error, diff --git a/src/core/index.ts b/src/core/index.ts index 959879f..a4a05b1 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -7,4 +7,6 @@ export * from "./schedulerClient"; export * from "./taskClient"; export * from "./templateClient"; export * from "./metadataClient"; -export * from "./eventClient"; \ No newline at end of file +export * from "./eventClient"; +export * from "./serviceRegistryClient"; +export * from "./ApplicationClient"; \ No newline at end of file From c311e209187b5108975b75baa776e5e052847cec Mon Sep 17 00:00:00 2001 From: Dmitry Borisov Date: Thu, 13 Nov 2025 21:07:44 +0300 Subject: [PATCH 4/7] make linter happy --- src/common/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/types.ts b/src/common/types.ts index 4af3ff1..8024fc2 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -283,5 +283,5 @@ export interface AccessKeyInfo { // TODO: need to remove this once ExtendedConductorApplication type is corrected in OpenAPI spec export interface ExtendedConductorApplication extends Required> { - tags?: Array; + tags?: Tag[]; } From ebaaf499c87586e96b6c5f061c86b47fdf749aa5 Mon Sep 17 00:00:00 2001 From: Dmitry Borisov Date: Thu, 13 Nov 2025 21:11:07 +0300 Subject: [PATCH 5/7] make "Should get access keys for an application" test more specific --- integration-tests/common/ApplicationClient.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/integration-tests/common/ApplicationClient.test.ts b/integration-tests/common/ApplicationClient.test.ts index a5ca8a0..a262eb7 100644 --- a/integration-tests/common/ApplicationClient.test.ts +++ b/integration-tests/common/ApplicationClient.test.ts @@ -176,6 +176,10 @@ describe("ApplicationClient", () => { const foundKey = accessKeys.find((key) => key.id === createdKey.id); expect(foundKey).toBeDefined(); expect(foundKey?.id).toEqual(createdKey.id); + expect(foundKey?.createdAt).toBeDefined(); + expect(typeof foundKey?.createdAt).toBe("number"); + expect(foundKey?.status).toBeDefined(); + expect(["ACTIVE", "INACTIVE"]).toContain(foundKey?.status); testAppsToCleanup.push(createdApp.id); }); From a9461f42bc90b9b04496b4b4535f6924287a170a Mon Sep 17 00:00:00 2001 From: Dmitry Borisov Date: Fri, 14 Nov 2025 13:11:58 +0300 Subject: [PATCH 6/7] add docs --- README.md | 134 ++++++ docs/api-reference/application-client.md | 528 +++++++++++++++++++++++ 2 files changed, 662 insertions(+) create mode 100644 docs/api-reference/application-client.md diff --git a/README.md b/README.md index 640c174..7c3c0a1 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,14 @@ Show support for the Conductor OSS. Please help spread the awareness by starrin - [Step 3: Publish Events](#step-3-publish-events) - [Step 4: Monitor Event Processing](#step-4-monitor-event-processing) - [Step 5: Manage Event Handlers](#step-5-manage-event-handlers) +- [Applications](#applications) + - [The ApplicationClient](#the-applicationclient) + - [Quick Start: Managing Applications](#quick-start-managing-applications) + - [Step 1: Create an ApplicationClient](#step-1-create-an-applicationclient) + - [Step 2: Create an Application](#step-2-create-an-application) + - [Step 3: Generate Access Keys](#step-3-generate-access-keys) + - [Step 4: Manage Application Roles](#step-4-manage-application-roles) + - [Step 5: Manage Applications](#step-5-manage-applications) - [Human Tasks](#human-tasks) - [The HumanExecutor and TemplateClient](#the-humanexecutor-and-templateclient) - [Quick Start: Creating and Managing a Human Task](#quick-start-creating-and-managing-a-human-task) @@ -828,6 +836,132 @@ Event handlers support various actions: For a complete method reference, see the [EventClient API Reference](docs/api-reference/event-client.md). +## Applications + +Applications in Conductor are security entities that enable programmatic access to the Conductor API. Each application can have multiple access keys for authentication and can be assigned roles to control what operations it can perform. + +### The ApplicationClient + +The `ApplicationClient` manages applications, access keys, and roles. For a complete method reference, see the [ApplicationClient API Reference](docs/api-reference/application-client.md). + +### Quick Start: Managing Applications + +Here's how to create and manage applications in Conductor: + +#### Step 1: Create an ApplicationClient + +First, create an instance of the `ApplicationClient`: + +```typescript +import { ApplicationClient } from "@io-orkes/conductor-javascript"; + +const appClient = new ApplicationClient(client); +``` + +#### Step 2: Create an Application + +Create a new application to represent your service or integration: + +```typescript +// Create a new application +const app = await appClient.createApplication("payment-service"); +console.log(`Created application: ${app.id}`); +``` + +#### Step 3: Generate Access Keys + +Create access keys for the application to authenticate API requests: + +```typescript +// Create an access key +const accessKey = await appClient.createAccessKey(app.id); +console.log(`Key ID: ${accessKey.id}`); +console.log(`Key Secret: ${accessKey.secret}`); // Save this immediately! + +// The secret is only shown once - store it securely +// Use these credentials to create authenticated clients +const authenticatedClient = await orkesConductorClient({ + serverUrl: "https://play.orkes.io/api", + keyId: accessKey.id, + keySecret: accessKey.secret +}); +``` + +#### Step 4: Manage Application Roles + +Grant the application permissions by adding roles: + +```typescript +// Add roles to the application +await appClient.addRoleToApplicationUser(app.id, "WORKFLOW_EXECUTOR"); +await appClient.addRoleToApplicationUser(app.id, "WORKER"); + +console.log("Application can now execute workflows and run workers"); +``` + +**Common Roles:** +- `WORKFLOW_EXECUTOR` - Start and manage workflow executions +- `WORKER` - Poll for and execute tasks +- `METADATA_MANAGER` - Manage workflow and task definitions +- `APPLICATION_MANAGER` - Manage applications and access keys + +#### Step 5: Manage Applications + +Manage the lifecycle of your applications: + +```typescript +// List all applications +const applications = await appClient.getAllApplications(); +console.log(`Total applications: ${applications.length}`); + +// Get a specific application +const myApp = await appClient.getApplication(app.id); +console.log(`Application name: ${myApp.name}`); + +// Update application name +await appClient.updateApplication(app.id, "payment-service-v2"); + +// Get all access keys for an application +const keys = await appClient.getAccessKeys(app.id); +console.log(`Application has ${keys.length} access keys`); + +// Toggle access key status (ACTIVE/INACTIVE) +await appClient.toggleAccessKeyStatus(app.id, accessKey.id); + +// Remove a role from the application +await appClient.removeRoleFromApplicationUser(app.id, "WORKER"); + +// Delete an access key +await appClient.deleteAccessKey(app.id, accessKey.id); + +// Delete the application +await appClient.deleteApplication(app.id); +``` + +**Tagging Applications:** + +Organize applications with tags for better management: + +```typescript +// Add tags to an application +await appClient.addApplicationTags(app.id, [ + { key: "environment", value: "production" }, + { key: "team", value: "payments" }, + { key: "cost-center", value: "engineering" } +]); + +// Get application tags +const tags = await appClient.getApplicationTags(app.id); + +// Delete specific tags +await appClient.deleteApplicationTag(app.id, { + key: "cost-center", + value: "engineering" +}); +``` + +For a complete method reference, see the [ApplicationClient API Reference](docs/api-reference/application-client.md). + ## Human Tasks Human tasks integrate human interaction into your automated workflows. They pause a workflow until a person provides input, such as an approval, a correction, or additional information. diff --git a/docs/api-reference/application-client.md b/docs/api-reference/application-client.md new file mode 100644 index 0000000..c177493 --- /dev/null +++ b/docs/api-reference/application-client.md @@ -0,0 +1,528 @@ +# ApplicationClient API Reference + +The `ApplicationClient` manages applications in Conductor. Applications are security entities that can be granted access keys and roles to interact with Conductor workflows and tasks. + +## Constructor + +### `new ApplicationClient(client: Client)` + +Creates a new `ApplicationClient`. + +**Parameters:** + +- `client` (`Client`): An instance of `Client`. + +--- + +## Methods + +### `getAllApplications(): Promise` + +Gets all applications registered in Conductor. + +**Returns:** + +- `Promise`: An array of all applications. + +**Example:** + +```typescript +import { ApplicationClient } from "@io-orkes/conductor-javascript"; + +const appClient = new ApplicationClient(client); + +// Get all applications +const applications = await appClient.getAllApplications(); +console.log(`Found ${applications.length} applications`); +``` + +--- + +### `createApplication(applicationName: string): Promise` + +Creates a new application. + +**Parameters:** + +- `applicationName` (`string`): The name of the application to create. + +**Returns:** + +- `Promise`: The created application. + +**Example:** + +```typescript +import { ApplicationClient } from "@io-orkes/conductor-javascript"; + +const appClient = new ApplicationClient(client); + +// Create a new application +const app = await appClient.createApplication("my-service"); +console.log(`Created application: ${app.id}`); +``` + +--- + +### `getApplication(applicationId: string): Promise` + +Gets an application by its ID. + +**Parameters:** + +- `applicationId` (`string`): The ID of the application. + +**Returns:** + +- `Promise`: The application. + +**Example:** + +```typescript +import { ApplicationClient } from "@io-orkes/conductor-javascript"; + +const appClient = new ApplicationClient(client); + +// Get a specific application +const app = await appClient.getApplication("app-123"); +console.log(`Application name: ${app.name}`); +``` + +--- + +### `getAppByAccessKeyId(accessKeyId: string): Promise` + +Gets an application by its access key ID. + +**Parameters:** + +- `accessKeyId` (`string`): The access key ID. + +**Returns:** + +- `Promise`: The application associated with the access key. + +**Example:** + +```typescript +import { ApplicationClient } from "@io-orkes/conductor-javascript"; + +const appClient = new ApplicationClient(client); + +// Get application by access key +const app = await appClient.getAppByAccessKeyId("key-123"); +console.log(`Application: ${app.name}`); +``` + +--- + +### `updateApplication(applicationId: string, newApplicationName: string): Promise` + +Updates an application's name. + +**Parameters:** + +- `applicationId` (`string`): The ID of the application to update. +- `newApplicationName` (`string`): The new name for the application. + +**Returns:** + +- `Promise`: The updated application. + +**Example:** + +```typescript +import { ApplicationClient } from "@io-orkes/conductor-javascript"; + +const appClient = new ApplicationClient(client); + +// Update application name +const app = await appClient.updateApplication("app-123", "my-service-v2"); +console.log(`Updated application name to: ${app.name}`); +``` + +--- + +### `deleteApplication(applicationId: string): Promise` + +Deletes an application. + +**Parameters:** + +- `applicationId` (`string`): The ID of the application to delete. + +**Returns:** + +- `Promise` + +**Example:** + +```typescript +import { ApplicationClient } from "@io-orkes/conductor-javascript"; + +const appClient = new ApplicationClient(client); + +// Delete an application +await appClient.deleteApplication("app-123"); +console.log("Application deleted"); +``` + +--- + +### `getAccessKeys(applicationId: string): Promise` + +Gets all access keys for an application. + +**Parameters:** + +- `applicationId` (`string`): The ID of the application. + +**Returns:** + +- `Promise`: An array of access key information. + +**Example:** + +```typescript +import { ApplicationClient } from "@io-orkes/conductor-javascript"; + +const appClient = new ApplicationClient(client); + +// Get access keys for an application +const keys = await appClient.getAccessKeys("app-123"); +console.log(`Found ${keys.length} access keys`); +keys.forEach((key) => { + console.log(`Key ${key.id}: ${key.status}`); +}); +``` + +--- + +### `createAccessKey(applicationId: string): Promise` + +Creates a new access key for an application. + +**Important:** Save the access key secret immediately after creation - it cannot be retrieved later. + +**Parameters:** + +- `applicationId` (`string`): The ID of the application. + +**Returns:** + +- `Promise`: The created access key with its secret. + +**Example:** + +```typescript +import { ApplicationClient } from "@io-orkes/conductor-javascript"; + +const appClient = new ApplicationClient(client); + +// Create a new access key +const accessKey = await appClient.createAccessKey("app-123"); +console.log(`Key ID: ${accessKey.id}`); +console.log(`Key Secret: ${accessKey.secret}`); // Save this immediately! +``` + +--- + +### `deleteAccessKey(applicationId: string, keyId: string): Promise` + +Deletes an access key. + +**Parameters:** + +- `applicationId` (`string`): The ID of the application. +- `keyId` (`string`): The ID of the access key to delete. + +**Returns:** + +- `Promise` + +**Example:** + +```typescript +import { ApplicationClient } from "@io-orkes/conductor-javascript"; + +const appClient = new ApplicationClient(client); + +// Delete an access key +await appClient.deleteAccessKey("app-123", "key-456"); +console.log("Access key deleted"); +``` + +--- + +### `toggleAccessKeyStatus(applicationId: string, keyId: string): Promise` + +Toggles the status of an access key between `ACTIVE` and `INACTIVE`. + +**Parameters:** + +- `applicationId` (`string`): The ID of the application. +- `keyId` (`string`): The ID of the access key. + +**Returns:** + +- `Promise`: The updated access key information. + +**Example:** + +```typescript +import { ApplicationClient } from "@io-orkes/conductor-javascript"; + +const appClient = new ApplicationClient(client); + +// Toggle access key status +const keyInfo = await appClient.toggleAccessKeyStatus("app-123", "key-456"); +console.log(`Access key is now ${keyInfo.status}`); +``` + +--- + +### `addRoleToApplicationUser(applicationId: string, role: string): Promise` + +Adds a role to an application user. + +**Parameters:** + +- `applicationId` (`string`): The ID of the application. +- `role` (`string`): The role to add. + +**Returns:** + +- `Promise` + +**Example:** + +```typescript +import { ApplicationClient } from "@io-orkes/conductor-javascript"; + +const appClient = new ApplicationClient(client); + +// Add a role to the application +await appClient.addRoleToApplicationUser("app-123", "WORKFLOW_EXECUTOR"); +console.log("Role added"); +``` + +--- + +### `removeRoleFromApplicationUser(applicationId: string, role: string): Promise` + +Removes a role from an application user. + +**Parameters:** + +- `applicationId` (`string`): The ID of the application. +- `role` (`string`): The role to remove. + +**Returns:** + +- `Promise` + +**Example:** + +```typescript +import { ApplicationClient } from "@io-orkes/conductor-javascript"; + +const appClient = new ApplicationClient(client); + +// Remove a role from the application +await appClient.removeRoleFromApplicationUser("app-123", "WORKFLOW_EXECUTOR"); +console.log("Role removed"); +``` + +--- + +### `getApplicationTags(applicationId: string): Promise` + +Gets all tags associated with an application. + +**Parameters:** + +- `applicationId` (`string`): The ID of the application. + +**Returns:** + +- `Promise`: An array of tags. + +**Example:** + +```typescript +import { ApplicationClient } from "@io-orkes/conductor-javascript"; + +const appClient = new ApplicationClient(client); + +// Get tags for an application +const tags = await appClient.getApplicationTags("app-123"); +console.log(`Application has ${tags.length} tags`); +``` + +--- + +### `addApplicationTags(applicationId: string, tags: Tag[]): Promise` + +Adds multiple tags to an application (replaces existing tags). + +**Parameters:** + +- `applicationId` (`string`): The ID of the application. +- `tags` (`Tag[]`): An array of tags to add. + +**Returns:** + +- `Promise` + +**Example:** + +```typescript +import { ApplicationClient } from "@io-orkes/conductor-javascript"; + +const appClient = new ApplicationClient(client); + +// Add tags to an application +await appClient.addApplicationTags("app-123", [ + { key: "environment", value: "production" }, + { key: "team", value: "backend" }, + { key: "service", value: "payment" }, +]); +``` + +--- + +### `addApplicationTag(applicationId: string, tag: Tag): Promise` + +Adds a single tag to an application. + +**Parameters:** + +- `applicationId` (`string`): The ID of the application. +- `tag` (`Tag`): The tag to add. + +**Returns:** + +- `Promise` + +**Example:** + +```typescript +import { ApplicationClient } from "@io-orkes/conductor-javascript"; + +const appClient = new ApplicationClient(client); + +// Add a single tag +await appClient.addApplicationTag("app-123", { + key: "version", + value: "2.0", +}); +``` + +--- + +### `deleteApplicationTags(applicationId: string, tags: Tag[]): Promise` + +Deletes multiple tags from an application. + +**Parameters:** + +- `applicationId` (`string`): The ID of the application. +- `tags` (`Tag[]`): An array of tags to delete. + +**Returns:** + +- `Promise` + +**Example:** + +```typescript +import { ApplicationClient } from "@io-orkes/conductor-javascript"; + +const appClient = new ApplicationClient(client); + +// Delete multiple tags +await appClient.deleteApplicationTags("app-123", [ + { key: "environment", value: "production" }, + { key: "team", value: "backend" }, +]); +``` + +--- + +### `deleteApplicationTag(applicationId: string, tag: Tag): Promise` + +Deletes a specific tag from an application. + +**Parameters:** + +- `applicationId` (`string`): The ID of the application. +- `tag` (`Tag`): The tag to delete (must match both `key` and `value`). + +**Returns:** + +- `Promise` + +**Example:** + +```typescript +import { ApplicationClient } from "@io-orkes/conductor-javascript"; + +const appClient = new ApplicationClient(client); + +// Delete a specific tag +await appClient.deleteApplicationTag("app-123", { + key: "version", + value: "2.0", +}); +``` + +--- + +## Type Definitions + +### `ExtendedConductorApplication` + +```typescript +export interface ExtendedConductorApplication { + id: string; + name: string; + createdBy: string; + createTime: number; + updatedBy: string; + updateTime: number; + tags?: Array; +}; +``` + +### `AccessKey` + +```typescript +export interface AccessKey { + id: string; + secret: string; +}; +``` + +### `AccessKeyInfo` + +```typescript +export interface AccessKeyInfo { + id: string; + createdAt: number; + status: "ACTIVE" | "INACTIVE"; +}; +``` + +### `Tag` + +```typescript +export type Tag = { + key?: string; + /** + * @deprecated + */ + type?: string; + value?: string; +}; +``` + From 257c48530942c6d75dce660533fbdbbe8c5b32fe Mon Sep 17 00:00:00 2001 From: Dmitry Borisov Date: Fri, 14 Nov 2025 13:49:26 +0300 Subject: [PATCH 7/7] Add ApplicationRole --- README.md | 24 +++++++--- docs/api-reference/application-client.md | 45 +++++++++++++++++-- .../common/ApplicationClient.test.ts | 8 ++-- src/common/index.ts | 1 + src/common/types.ts | 14 ++++++ src/core/ApplicationClient.ts | 9 ++-- 6 files changed, 83 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 7c3c0a1..c719bee 100644 --- a/README.md +++ b/README.md @@ -853,7 +853,7 @@ Here's how to create and manage applications in Conductor: First, create an instance of the `ApplicationClient`: ```typescript -import { ApplicationClient } from "@io-orkes/conductor-javascript"; +import { ApplicationClient, ApplicationRole } from "@io-orkes/conductor-javascript"; const appClient = new ApplicationClient(client); ``` @@ -892,18 +892,30 @@ const authenticatedClient = await orkesConductorClient({ Grant the application permissions by adding roles: ```typescript +import { ApplicationRole } from "@io-orkes/conductor-javascript"; + // Add roles to the application -await appClient.addRoleToApplicationUser(app.id, "WORKFLOW_EXECUTOR"); -await appClient.addRoleToApplicationUser(app.id, "WORKER"); +await appClient.addApplicationRole(app.id, "WORKFLOW_MANAGER"); +await appClient.addApplicationRole(app.id, "WORKER"); console.log("Application can now execute workflows and run workers"); ``` -**Common Roles:** -- `WORKFLOW_EXECUTOR` - Start and manage workflow executions -- `WORKER` - Poll for and execute tasks +**Available Roles:** + +The SDK provides an `ApplicationRole` type with the following options: + +- `ADMIN` - Full administrative access to all resources +- `WORKFLOW_MANAGER` - Start and manage workflow executions +- `WORKER` - Poll for and execute assigned tasks +- `UNRESTRICTED_WORKER` - Can execute any task without restrictions - `METADATA_MANAGER` - Manage workflow and task definitions - `APPLICATION_MANAGER` - Manage applications and access keys +- `APPLICATION_CREATOR` - Can create new applications +- `USER` - Standard user access +- `USER_READ_ONLY` - Read-only access to resources +- `METADATA_API` - API access to metadata operations +- `PROMPT_MANAGER` - Can manage AI prompts and templates #### Step 5: Manage Applications diff --git a/docs/api-reference/application-client.md b/docs/api-reference/application-client.md index c177493..c5cd484 100644 --- a/docs/api-reference/application-client.md +++ b/docs/api-reference/application-client.md @@ -281,14 +281,14 @@ console.log(`Access key is now ${keyInfo.status}`); --- -### `addRoleToApplicationUser(applicationId: string, role: string): Promise` +### `addApplicationRole(applicationId: string, role: ApplicationRole): Promise` Adds a role to an application user. **Parameters:** - `applicationId` (`string`): The ID of the application. -- `role` (`string`): The role to add. +- `role` (`ApplicationRole`): The role to add. **Returns:** @@ -297,13 +297,17 @@ Adds a role to an application user. **Example:** ```typescript -import { ApplicationClient } from "@io-orkes/conductor-javascript"; +import { ApplicationClient, ApplicationRole } from "@io-orkes/conductor-javascript"; const appClient = new ApplicationClient(client); // Add a role to the application -await appClient.addRoleToApplicationUser("app-123", "WORKFLOW_EXECUTOR"); +await appClient.addApplicationRole("app-123", "WORKFLOW_MANAGER"); console.log("Role added"); + +// You can also use the ApplicationRole type +const role: ApplicationRole = "METADATA_MANAGER"; +await appClient.addApplicationRole("app-123", role); ``` --- @@ -526,3 +530,36 @@ export type Tag = { }; ``` +### `ApplicationRole` + +Defines the available roles that can be assigned to an application. + +```typescript +export type ApplicationRole = + | "ADMIN" + | "UNRESTRICTED_WORKER" + | "METADATA_MANAGER" + | "WORKFLOW_MANAGER" + | "APPLICATION_MANAGER" + | "USER" + | "USER_READ_ONLY" + | "WORKER" + | "APPLICATION_CREATOR" + | "METADATA_API" + | "PROMPT_MANAGER"; +``` + +**Role Descriptions:** + +- `ADMIN` - Full administrative access to all resources +- `UNRESTRICTED_WORKER` - Can execute any task without restrictions +- `METADATA_MANAGER` - Can manage workflow and task definitions +- `WORKFLOW_MANAGER` - Can manage workflow executions +- `APPLICATION_MANAGER` - Can manage applications and access keys +- `USER` - Standard user access +- `USER_READ_ONLY` - Read-only access to resources +- `WORKER` - Can poll for and execute assigned tasks +- `APPLICATION_CREATOR` - Can create new applications +- `METADATA_API` - API access to metadata operations +- `PROMPT_MANAGER` - Can manage AI prompts and templates + diff --git a/integration-tests/common/ApplicationClient.test.ts b/integration-tests/common/ApplicationClient.test.ts index a262eb7..96548d4 100644 --- a/integration-tests/common/ApplicationClient.test.ts +++ b/integration-tests/common/ApplicationClient.test.ts @@ -250,15 +250,15 @@ describe("ApplicationClient", () => { }); describe("Role Management", () => { - test("Should add and remove role from application user", async () => { + test("Should add and remove role from application", async () => { const appName = createUniqueName("test-app-role"); - const role = "USER"; + const role = "APPLICATION_MANAGER"; const createdApp = await applicationClient.createApplication(appName); expect(createdApp.id).toBeDefined(); await expect( - applicationClient.addRoleToApplicationUser(createdApp.id, role) + applicationClient.addApplicationRole(createdApp.id, role) ).resolves.not.toThrow(); await expect( @@ -532,7 +532,7 @@ describe("ApplicationClient", () => { const role = "USER"; await expect( - applicationClient.addRoleToApplicationUser(nonExistentId, role) + applicationClient.addApplicationRole(nonExistentId, role) ).rejects.toThrow(); }); diff --git a/src/common/index.ts b/src/common/index.ts index c3aa803..8ef89c2 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -61,6 +61,7 @@ export type { AccessKey, AccessKeyInfo, ExtendedConductorApplication, + ApplicationRole, } from "./types"; // todo: remove after April 2026 (backward compatibility types) diff --git a/src/common/types.ts b/src/common/types.ts index 8024fc2..a1f4312 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -280,6 +280,20 @@ export interface AccessKeyInfo { status: "ACTIVE" | "INACTIVE"; } +// TODO: need to remove this once ApplicationRole type is added to OpenAPI spec +export type ApplicationRole = + | "ADMIN" + | "UNRESTRICTED_WORKER" + | "METADATA_MANAGER" + | "WORKFLOW_MANAGER" + | "APPLICATION_MANAGER" + | "USER" + | "USER_READ_ONLY" + | "WORKER" + | "APPLICATION_CREATOR" + | "METADATA_API" + | "PROMPT_MANAGER"; + // TODO: need to remove this once ExtendedConductorApplication type is corrected in OpenAPI spec export interface ExtendedConductorApplication extends Required> { diff --git a/src/core/ApplicationClient.ts b/src/core/ApplicationClient.ts index 5a35f5c..8c1d388 100644 --- a/src/core/ApplicationClient.ts +++ b/src/core/ApplicationClient.ts @@ -6,6 +6,7 @@ import type { ExtendedConductorApplication, AccessKey, AccessKeyInfo, + ApplicationRole, } from "../common"; export class ApplicationClient { @@ -158,15 +159,15 @@ export class ApplicationClient { } /** - * Add role to application user + * Add role to application * @param {string} applicationId - * @param {string} role + * @param {ApplicationRole} role * @returns {Promise} * @throws {ConductorSdkError} */ - public async addRoleToApplicationUser( + public async addApplicationRole( applicationId: string, - role: string + role: ApplicationRole ): Promise { try { await ApplicationResource.addRoleToApplicationUser({