From 94bcb10027a6ea10904aa2302c9c3db1373692eb Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Thu, 25 Jun 2026 14:23:57 +0200 Subject: [PATCH 1/2] refactor(agent-client): move test-only permission overrides to agent-testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The permission-override mechanism (overridePermissions field, the overrideCollectionPermission / overrideActionPermission / clearPermissionOverride methods, and the *PermissionsOverride types) was only ever used by agent-testing, yet it lived on the production RemoteAgentClient base class — exposing no-op methods on the prod client API. Move it to TestableAgentBase (agent-testing), the sole consumer: - agent-client: drop the field/methods/types from RemoteAgentClient and the dead overridePermissions param from createRemoteAgentClient. - agent-testing: TestableAgentBase now holds overridePermissions + the override methods; the types live in a new permission-overrides module used by the sandbox and createAgentTestClient. BREAKING CHANGE: @forestadmin/agent-client no longer exports createRemoteAgentClient's overridePermissions option nor the PermissionsOverride / CollectionPermissionsOverride / SmartActionPermissionsOverride types (test-only; use @forestadmin/agent-testing). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/domains/remote-agent-client.ts | 63 -------------- packages/agent-client/src/index.ts | 14 +-- .../test/domains/remote-agent-client.test.ts | 81 ------------------ packages/agent-client/test/index.test.ts | 19 ----- .../src/forest-server-sandbox.ts | 2 +- packages/agent-testing/src/index.ts | 2 +- .../agent-testing/src/permission-overrides.ts | 28 ++++++ .../agent-testing/src/testable-agent-base.ts | 50 +++++++++++ .../test/testable-agent-base.test.ts | 85 +++++++++++++++++++ 9 files changed, 166 insertions(+), 178 deletions(-) create mode 100644 packages/agent-testing/src/permission-overrides.ts create mode 100644 packages/agent-testing/test/testable-agent-base.test.ts diff --git a/packages/agent-client/src/domains/remote-agent-client.ts b/packages/agent-client/src/domains/remote-agent-client.ts index e081d1902c..1cc8f4bf40 100644 --- a/packages/agent-client/src/domains/remote-agent-client.ts +++ b/packages/agent-client/src/domains/remote-agent-client.ts @@ -1,37 +1,9 @@ import type { ActionEndpointsByCollection } from './action'; import type HttpRequester from '../http-requester'; -import type { RawTreeWithSources } from '@forestadmin/forestadmin-client'; import Chart from './chart'; import Collection from './collection'; -export type SmartActionPermissionsOverride = Partial<{ - triggerEnabled: boolean; - triggerConditions: RawTreeWithSources; - approvalRequired: boolean; - approvalRequiredConditions: RawTreeWithSources; - userApprovalEnabled: boolean; - userApprovalConditions: RawTreeWithSources; - selfApprovalEnabled: boolean; -}>; - -export type CollectionPermissionsOverride = Partial<{ - browseEnabled: boolean; - deleteEnabled: boolean; - editEnabled: boolean; - exportEnabled: boolean; - addEnabled: boolean; - readEnabled: boolean; -}>; - -export type PermissionsOverride = Record< - string, - { - collection: CollectionPermissionsOverride; - actions: Record; - } ->; - type CollectionName = keyof T & string; export default class RemoteAgentClient< @@ -39,49 +11,14 @@ export default class RemoteAgentClient< > extends Chart { protected actionEndpoints?: ActionEndpointsByCollection; - private overridePermissions?: (permissions: PermissionsOverride) => Promise; - constructor(params?: { actionEndpoints?: ActionEndpointsByCollection; httpRequester: HttpRequester; - overridePermissions?: (permissions: PermissionsOverride) => Promise; }) { super(); if (!params) return; this.httpRequester = params.httpRequester; this.actionEndpoints = params.actionEndpoints; - this.overridePermissions = params.overridePermissions; - } - - async overrideCollectionPermission( - collectionName: CollectionName, - permissions: CollectionPermissionsOverride, - ) { - await this.overridePermissions?.({ - [collectionName]: { - collection: permissions, - actions: {}, - }, - }); - } - - async overrideActionPermission( - collectionName: CollectionName, - actionName: string, - permissions: SmartActionPermissionsOverride, - ) { - await this.overridePermissions?.({ - [collectionName]: { - collection: {}, - actions: { - [actionName]: permissions, - }, - }, - }); - } - - async clearPermissionOverride() { - await this.overridePermissions?.({}); } collection(name: CollectionName): Collection { diff --git a/packages/agent-client/src/index.ts b/packages/agent-client/src/index.ts index e4050668e1..ac24ae8889 100644 --- a/packages/agent-client/src/index.ts +++ b/packages/agent-client/src/index.ts @@ -1,9 +1,4 @@ import type { ActionEndpointsByCollection } from './domains/action'; -import type { - CollectionPermissionsOverride, - PermissionsOverride, - SmartActionPermissionsOverride, -} from './domains/remote-agent-client'; import ActionFieldJson from './action-fields/action-field-json'; import ActionFieldStringList from './action-fields/action-field-string-list'; @@ -20,15 +15,9 @@ export { ActionRequiresApprovalError, ActionFormValidationError, }; -export type { - ActionEndpointsByCollection, - CollectionPermissionsOverride, - PermissionsOverride, - SmartActionPermissionsOverride, -}; +export type { ActionEndpointsByCollection }; export function createRemoteAgentClient(params: { - overridePermissions?: (permissions: PermissionsOverride) => Promise; actionEndpoints?: ActionEndpointsByCollection; token?: string; url: string; @@ -38,7 +27,6 @@ export function createRemoteAgentClient(params: { return new RemoteAgentClient({ actionEndpoints: params.actionEndpoints, httpRequester, - overridePermissions: params.overridePermissions, }); } diff --git a/packages/agent-client/test/domains/remote-agent-client.test.ts b/packages/agent-client/test/domains/remote-agent-client.test.ts index a62c946163..c9b60aec58 100644 --- a/packages/agent-client/test/domains/remote-agent-client.test.ts +++ b/packages/agent-client/test/domains/remote-agent-client.test.ts @@ -7,7 +7,6 @@ jest.mock('../../src/http-requester'); describe('RemoteAgentClient', () => { let httpRequester: jest.Mocked; let client: RemoteAgentClient; - let overridePermissionsMock: jest.Mock; beforeEach(() => { jest.clearAllMocks(); @@ -15,7 +14,6 @@ describe('RemoteAgentClient', () => { query: jest.fn(), stream: jest.fn(), } as any; - overridePermissionsMock = jest.fn().mockResolvedValue(undefined); client = new RemoteAgentClient({ httpRequester, actionEndpoints: { @@ -29,7 +27,6 @@ describe('RemoteAgentClient', () => { }, }, }, - overridePermissions: overridePermissionsMock, }); }); @@ -55,82 +52,4 @@ describe('RemoteAgentClient', () => { expect(collection).toBeDefined(); }); }); - - describe('overrideCollectionPermission', () => { - it('should call overridePermissions with collection permissions', async () => { - await client.overrideCollectionPermission('users', { - browseEnabled: true, - editEnabled: false, - }); - - expect(overridePermissionsMock).toHaveBeenCalledWith({ - users: { - collection: { - browseEnabled: true, - editEnabled: false, - }, - actions: {}, - }, - }); - }); - - it('should not throw if overridePermissions is not provided', async () => { - const clientWithoutOverride = new RemoteAgentClient({ - httpRequester, - }); - - await expect( - clientWithoutOverride.overrideCollectionPermission('users', { browseEnabled: true }), - ).resolves.toBeUndefined(); - }); - }); - - describe('overrideActionPermission', () => { - it('should call overridePermissions with action permissions', async () => { - await client.overrideActionPermission('users', 'sendEmail', { - triggerEnabled: true, - approvalRequired: true, - }); - - expect(overridePermissionsMock).toHaveBeenCalledWith({ - users: { - collection: {}, - actions: { - sendEmail: { - triggerEnabled: true, - approvalRequired: true, - }, - }, - }, - }); - }); - - it('should not throw if overridePermissions is not provided', async () => { - const clientWithoutOverride = new RemoteAgentClient({ - httpRequester, - }); - - await expect( - clientWithoutOverride.overrideActionPermission('users', 'sendEmail', { - triggerEnabled: true, - }), - ).resolves.toBeUndefined(); - }); - }); - - describe('clearPermissionOverride', () => { - it('should call overridePermissions with empty object', async () => { - await client.clearPermissionOverride(); - - expect(overridePermissionsMock).toHaveBeenCalledWith({}); - }); - - it('should not throw if overridePermissions is not provided', async () => { - const clientWithoutOverride = new RemoteAgentClient({ - httpRequester, - }); - - await expect(clientWithoutOverride.clearPermissionOverride()).resolves.toBeUndefined(); - }); - }); }); diff --git a/packages/agent-client/test/index.test.ts b/packages/agent-client/test/index.test.ts index 73f19954f2..f3c3e8a2fd 100644 --- a/packages/agent-client/test/index.test.ts +++ b/packages/agent-client/test/index.test.ts @@ -42,25 +42,6 @@ describe('createRemoteAgentClient', () => { expect(collection).toBeDefined(); }); - it('should pass overridePermissions function to the client', async () => { - const overridePermissions = jest.fn().mockResolvedValue(undefined); - - const client = createRemoteAgentClient({ - url: 'https://api.example.com', - token: 'test-token', - overridePermissions, - }); - - await client.overrideCollectionPermission('users' as any, { browseEnabled: true }); - - expect(overridePermissions).toHaveBeenCalledWith({ - users: { - collection: { browseEnabled: true }, - actions: {}, - }, - }); - }); - it('should provide a working client that can access collections', () => { const client = createRemoteAgentClient({ url: 'https://api.example.com', diff --git a/packages/agent-testing/src/forest-server-sandbox.ts b/packages/agent-testing/src/forest-server-sandbox.ts index 3db23def99..31153cda8c 100644 --- a/packages/agent-testing/src/forest-server-sandbox.ts +++ b/packages/agent-testing/src/forest-server-sandbox.ts @@ -2,7 +2,7 @@ import type { CollectionPermissionsOverride, PermissionsOverride, SmartActionPermissionsOverride, -} from '@forestadmin/agent-client'; +} from './permission-overrides'; import type { ForestSchema } from '@forestadmin/forestadmin-client'; import type { EnvironmentCollectionPermissionsV4, diff --git a/packages/agent-testing/src/index.ts b/packages/agent-testing/src/index.ts index 75a937dab4..3dc6c3126a 100644 --- a/packages/agent-testing/src/index.ts +++ b/packages/agent-testing/src/index.ts @@ -1,6 +1,6 @@ +import type { PermissionsOverride } from './permission-overrides'; import type { TestableAgentOptions } from './types'; import type { Agent } from '@forestadmin/agent'; -import type { PermissionsOverride } from '@forestadmin/agent-client'; import type { TSchema } from '@forestadmin/datasource-customizer'; import type { ForestSchema } from '@forestadmin/forestadmin-client'; diff --git a/packages/agent-testing/src/permission-overrides.ts b/packages/agent-testing/src/permission-overrides.ts new file mode 100644 index 0000000000..514adaf17c --- /dev/null +++ b/packages/agent-testing/src/permission-overrides.ts @@ -0,0 +1,28 @@ +import type { RawTreeWithSources } from '@forestadmin/forestadmin-client'; + +export type SmartActionPermissionsOverride = Partial<{ + triggerEnabled: boolean; + triggerConditions: RawTreeWithSources; + approvalRequired: boolean; + approvalRequiredConditions: RawTreeWithSources; + userApprovalEnabled: boolean; + userApprovalConditions: RawTreeWithSources; + selfApprovalEnabled: boolean; +}>; + +export type CollectionPermissionsOverride = Partial<{ + browseEnabled: boolean; + deleteEnabled: boolean; + editEnabled: boolean; + exportEnabled: boolean; + addEnabled: boolean; + readEnabled: boolean; +}>; + +export type PermissionsOverride = Record< + string, + { + collection: CollectionPermissionsOverride; + actions: Record; + } +>; diff --git a/packages/agent-testing/src/testable-agent-base.ts b/packages/agent-testing/src/testable-agent-base.ts index db1497dcf8..049c00e3e5 100644 --- a/packages/agent-testing/src/testable-agent-base.ts +++ b/packages/agent-testing/src/testable-agent-base.ts @@ -1,12 +1,62 @@ +import type { + CollectionPermissionsOverride, + PermissionsOverride, + SmartActionPermissionsOverride, +} from './permission-overrides'; +import type { ActionEndpointsByCollection, HttpRequester } from '@forestadmin/agent-client'; import type { TSchema } from '@forestadmin/datasource-customizer'; import { RemoteAgentClient } from '@forestadmin/agent-client'; import Benchmark from './benchmark'; +type CollectionName = keyof T & string; + export default class TestableAgentBase< TypingsSchema extends TSchema = TSchema, > extends RemoteAgentClient { + private readonly overridePermissions?: (permissions: PermissionsOverride) => Promise; + + constructor(params?: { + actionEndpoints?: ActionEndpointsByCollection; + httpRequester: HttpRequester; + overridePermissions?: (permissions: PermissionsOverride) => Promise; + }) { + super(params); + this.overridePermissions = params?.overridePermissions; + } + + async overrideCollectionPermission( + collectionName: CollectionName, + permissions: CollectionPermissionsOverride, + ) { + await this.overridePermissions?.({ + [collectionName]: { + collection: permissions, + actions: {}, + }, + }); + } + + async overrideActionPermission( + collectionName: CollectionName, + actionName: string, + permissions: SmartActionPermissionsOverride, + ) { + await this.overridePermissions?.({ + [collectionName]: { + collection: {}, + actions: { + [actionName]: permissions, + }, + }, + }); + } + + async clearPermissionOverride() { + await this.overridePermissions?.({}); + } + benchmark(): Benchmark { return new Benchmark(); } diff --git a/packages/agent-testing/test/testable-agent-base.test.ts b/packages/agent-testing/test/testable-agent-base.test.ts new file mode 100644 index 0000000000..5570b93f2e --- /dev/null +++ b/packages/agent-testing/test/testable-agent-base.test.ts @@ -0,0 +1,85 @@ +import type { PermissionsOverride } from '../src/permission-overrides'; +import type { HttpRequester } from '@forestadmin/agent-client'; + +import TestableAgentBase from '../src/testable-agent-base'; + +describe('TestableAgentBase', () => { + let overridePermissions: jest.Mock, [PermissionsOverride]>; + let client: TestableAgentBase; + + beforeEach(() => { + overridePermissions = jest.fn().mockResolvedValue(undefined); + client = new TestableAgentBase({ + httpRequester: {} as unknown as HttpRequester, + overridePermissions, + }); + }); + + describe('overrideCollectionPermission', () => { + it('should call overridePermissions with collection permissions', async () => { + await client.overrideCollectionPermission('users', { + browseEnabled: true, + editEnabled: false, + }); + + expect(overridePermissions).toHaveBeenCalledWith({ + users: { + collection: { browseEnabled: true, editEnabled: false }, + actions: {}, + }, + }); + }); + + it('should not throw when overridePermissions is not provided', async () => { + const clientWithout = new TestableAgentBase({ + httpRequester: {} as unknown as HttpRequester, + }); + + await expect( + clientWithout.overrideCollectionPermission('users', { browseEnabled: true }), + ).resolves.toBeUndefined(); + }); + }); + + describe('overrideActionPermission', () => { + it('should call overridePermissions with action permissions', async () => { + await client.overrideActionPermission('users', 'sendEmail', { + triggerEnabled: true, + approvalRequired: true, + }); + + expect(overridePermissions).toHaveBeenCalledWith({ + users: { + collection: {}, + actions: { sendEmail: { triggerEnabled: true, approvalRequired: true } }, + }, + }); + }); + + it('should not throw when overridePermissions is not provided', async () => { + const clientWithout = new TestableAgentBase({ + httpRequester: {} as unknown as HttpRequester, + }); + + await expect( + clientWithout.overrideActionPermission('users', 'sendEmail', { triggerEnabled: true }), + ).resolves.toBeUndefined(); + }); + }); + + describe('clearPermissionOverride', () => { + it('should call overridePermissions with an empty object', async () => { + await client.clearPermissionOverride(); + + expect(overridePermissions).toHaveBeenCalledWith({}); + }); + + it('should not throw when overridePermissions is not provided', async () => { + const clientWithout = new TestableAgentBase({ + httpRequester: {} as unknown as HttpRequester, + }); + + await expect(clientWithout.clearPermissionOverride()).resolves.toBeUndefined(); + }); + }); +}); From feaed65be148e5e212f65719f7edbfe8d5447a66 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Thu, 25 Jun 2026 14:49:41 +0200 Subject: [PATCH 2/2] chore: retrigger CI (bust stale agent-client build cache)