Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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);
}
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
63 changes: 51 additions & 12 deletions src/management/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,45 @@ export class ManagementClient {
this.transport.close();
}

private normalizeListPayload<T>(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<string, any>,
): RolePermissionObject {
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
const data = { ...(payload as Record<string, any>) };
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<Record<string, any>>(payload).map((item) =>
this.normalizePermissionObjectPayload(item, item),
);
}

// ------------------------------------------------------------------ //
// Organization operations
// ------------------------------------------------------------------ //
Expand Down Expand Up @@ -428,21 +467,21 @@ export class ManagementClient {
contentType: string,
): Promise<RolePermissionObject[]> {
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(
roleKey: ManagementRoleRef,
payload: Record<string, any>,
): Promise<RolePermissionObject> {
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(
Expand Down Expand Up @@ -531,21 +570,21 @@ export class ManagementClient {
contentType: string,
): Promise<RolePermissionObject[]> {
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(
roleKey: FluxRoleRef,
payload: Record<string, any>,
): Promise<RolePermissionObject> {
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(
Expand Down
36 changes: 28 additions & 8 deletions tests/management/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
});
Expand Down