diff --git a/README.md b/README.md index ab8eb37..e5a9c07 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![npm version](https://img.shields.io/npm/v/@foxnose/sdk)](https://www.npmjs.com/package/@foxnose/sdk) [![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE) [![Node.js](https://img.shields.io/badge/node-%3E%3D18-green)](https://nodejs.org) +[![codecov](https://codecov.io/gh/FoxNoseTech/foxnose-typescript/branch/main/graph/badge.svg)](https://codecov.io/gh/FoxNoseTech/foxnose-typescript) Official TypeScript SDK for the [FoxNose](https://foxnose.net/?utm_source=github&utm_medium=repository&utm_campaign=foxnose-typescript) platform — a managed knowledge layer for RAG and AI agents. @@ -177,9 +178,9 @@ try { await client.getResource('folder', 'nonexistent-key'); } catch (err) { if (err instanceof FoxnoseAPIError) { - console.error(err.statusCode); // 404 - console.error(err.errorCode); // "not_found" - console.error(err.detail); // Additional error details + console.error(err.statusCode); // 404 + console.error(err.errorCode); // "not_found" + console.error(err.detail); // Additional error details } else if (err instanceof FoxnoseTransportError) { console.error('Network error:', err.message); } @@ -204,7 +205,7 @@ const result = await client.batchUpsertResources('folder-key', items, { }); console.log(result.succeeded.length); // Successfully upserted -console.log(result.failed.length); // Failed items with errors +console.log(result.failed.length); // Failed items with errors ``` ## Development diff --git a/package.json b/package.json index a35f69c..7b71959 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@foxnose/sdk", - "version": "0.2.1", + "version": "0.2.2", "description": "Official FoxNose SDK for TypeScript and JavaScript", "license": "Apache-2.0", "type": "module", diff --git a/src/management/client.ts b/src/management/client.ts index fe156e6..2094a08 100644 --- a/src/management/client.ts +++ b/src/management/client.ts @@ -129,6 +129,45 @@ export class ManagementClient { this.transport.close(); } + private normalizeListPayload(payload: unknown): T[] { + if (Array.isArray(payload)) { + return payload as T[]; + } + if (payload && typeof payload === 'object') { + const results = (payload as { results?: unknown }).results; + if (Array.isArray(results)) { + return results as T[]; + } + return [payload as T]; + } + return []; + } + + private normalizePermissionObjectPayload( + payload: unknown, + requestPayload: Record, + ): RolePermissionObject { + if (payload && typeof payload === 'object' && !Array.isArray(payload)) { + const data = { ...(payload as Record) }; + if (!('object_key' in data) && 'object' in data) { + data.object_key = data.object; + delete data.object; + } + return data as RolePermissionObject; + } + + return { + content_type: requestPayload.content_type, + object_key: requestPayload.object_key ?? requestPayload.object, + }; + } + + private normalizePermissionObjectListPayload(payload: unknown): RolePermissionObject[] { + return this.normalizeListPayload>(payload).map((item) => + this.normalizePermissionObjectPayload(item, item), + ); + } + // ------------------------------------------------------------------ // // Organization operations // ------------------------------------------------------------------ // @@ -428,11 +467,10 @@ export class ManagementClient { contentType: string, ): Promise { const key = resolveKey(roleKey); - const payload = - (await this.request('GET', `${this.paths.rolePermissionObjectsRoot(key)}/`, { - params: { content_type: contentType }, - })) ?? []; - return Array.isArray(payload) ? payload : [payload]; + const payload = await this.request('GET', `${this.paths.rolePermissionObjectsRoot(key)}/`, { + params: { content_type: contentType }, + }); + return this.normalizePermissionObjectListPayload(payload); } async addManagementPermissionObject( @@ -440,9 +478,10 @@ export class ManagementClient { payload: Record, ): Promise { const key = resolveKey(roleKey); - return this.request('POST', `${this.paths.rolePermissionObjectsRoot(key)}/`, { + const data = await this.request('POST', `${this.paths.rolePermissionObjectsRoot(key)}/`, { jsonBody: payload, }); + return this.normalizePermissionObjectPayload(data, payload); } async deleteManagementPermissionObject( @@ -531,11 +570,10 @@ export class ManagementClient { contentType: string, ): Promise { const key = resolveKey(roleKey); - const payload = - (await this.request('GET', `${this.paths.fluxRolePermissionObjectsRoot(key)}/`, { - params: { content_type: contentType }, - })) ?? []; - return Array.isArray(payload) ? payload : [payload]; + const payload = await this.request('GET', `${this.paths.fluxRolePermissionObjectsRoot(key)}/`, { + params: { content_type: contentType }, + }); + return this.normalizePermissionObjectListPayload(payload); } async addFluxPermissionObject( @@ -543,9 +581,10 @@ export class ManagementClient { payload: Record, ): Promise { const key = resolveKey(roleKey); - return this.request('POST', `${this.paths.fluxRolePermissionObjectsRoot(key)}/`, { + const data = await this.request('POST', `${this.paths.fluxRolePermissionObjectsRoot(key)}/`, { jsonBody: payload, }); + return this.normalizePermissionObjectPayload(data, payload); } async deleteFluxPermissionObject( diff --git a/tests/management/client.test.ts b/tests/management/client.test.ts index 0db824e..c007da6 100644 --- a/tests/management/client.test.ts +++ b/tests/management/client.test.ts @@ -360,16 +360,25 @@ describe('ManagementClient', () => { describe('Management Permission Objects', () => { it('listManagementPermissionObjects', async () => { - const objs = [{ content_type: 'folder', object_key: 'f1' }]; - setupMockFetch(objs); + const payload = { + count: 1, + next: null, + previous: null, + results: [{ content_type: 'folder', object: 'f1' }], + }; + const fetchMock = setupMockFetch(payload); const client = createClient(); const result = await client.listManagementPermissionObjects('role-1', 'folder'); - expect(result).toEqual(objs); + expect(result).toEqual([{ content_type: 'folder', object_key: 'f1' }]); + expect(fetchMock.mock.calls[0][0]).toContain( + '/permissions/management-api/roles/role-1/permissions/objects/', + ); + expect(fetchMock.mock.calls[0][0]).toContain('content_type=folder'); }); it('addManagementPermissionObject', async () => { const obj = { content_type: 'folder', object_key: 'f1' }; - setupMockFetch(obj); + globalThis.fetch = vi.fn(async () => new Response(null, { status: 201 })); const client = createClient(); const result = await client.addManagementPermissionObject('role-1', obj); expect(result).toEqual(obj); @@ -445,15 +454,26 @@ describe('ManagementClient', () => { describe('Flux Permission Objects', () => { it('listFluxPermissionObjects', async () => { - const objs = [{ content_type: 'folder', object_key: 'f1' }]; - setupMockFetch(objs); + const payload = { + count: 1, + next: null, + previous: null, + results: [{ content_type: 'flux-apis', object: 'api-1' }], + }; + const fetchMock = setupMockFetch(payload); const client = createClient(); - expect(await client.listFluxPermissionObjects('fr-1', 'folder')).toEqual(objs); + expect(await client.listFluxPermissionObjects('fr-1', 'flux-apis')).toEqual([ + { content_type: 'flux-apis', object_key: 'api-1' }, + ]); + expect(fetchMock.mock.calls[0][0]).toContain( + '/permissions/flux-api/roles/fr-1/permissions/objects/', + ); + expect(fetchMock.mock.calls[0][0]).toContain('content_type=flux-apis'); }); it('addFluxPermissionObject', async () => { const obj = { content_type: 'folder', object_key: 'f1' }; - setupMockFetch(obj); + globalThis.fetch = vi.fn(async () => new Response(null, { status: 201 })); const client = createClient(); expect(await client.addFluxPermissionObject('fr-1', obj)).toEqual(obj); });