From 2092bd5bb2699045c59035f0bbbe025419916f5b Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Fri, 13 Jun 2025 10:25:38 +0200 Subject: [PATCH 1/3] chore: added ability to define attributes Signed-off-by: benjdlambert --- .../actions/actionsServiceFactory.test.ts | 10 ++ .../DefaultActionsRegistryService.ts | 10 +- .../actionsRegistryServiceFactory.test.ts | 103 ++++++++++++++++++ .../definitions/ActionsRegistryService.ts | 5 + .../services/definitions/ActionsService.ts | 5 + packages/backend-test-utils/package.json | 2 + yarn.lock | 2 + 7 files changed, 135 insertions(+), 2 deletions(-) diff --git a/packages/backend-defaults/src/entrypoints/actions/actionsServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/actions/actionsServiceFactory.test.ts index 7b7285c89a..58a79c6a2a 100644 --- a/packages/backend-defaults/src/entrypoints/actions/actionsServiceFactory.test.ts +++ b/packages/backend-defaults/src/entrypoints/actions/actionsServiceFactory.test.ts @@ -70,6 +70,11 @@ describe('actionsServiceFactory', () => { input: {}, output: {}, }, + attributes: { + destructive: false, + idempotent: false, + readOnly: false, + }, }; beforeEach(() => { @@ -306,6 +311,11 @@ describe('actionsServiceFactory', () => { type: 'object', }, }, + attributes: { + destructive: false, + idempotent: false, + readOnly: false, + }, title: 'Test', }, ], diff --git a/packages/backend-defaults/src/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts b/packages/backend-defaults/src/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts index 2ecf4d5006..cddb04c817 100644 --- a/packages/backend-defaults/src/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts +++ b/packages/backend-defaults/src/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts @@ -67,13 +67,19 @@ export class DefaultActionsRegistryService implements ActionsRegistryService { actions: Array.from(this.actions.entries()).map(([id, action]) => ({ id, ...action, + attributes: { + // todo(blam): what's safe defaults? + destructive: action.attributes?.destructive ?? false, + idempotent: action.attributes?.idempotent ?? false, + readOnly: action.attributes?.readOnly ?? false, + }, schema: { input: action.schema?.input ? zodToJsonSchema(action.schema.input(z)) - : zodToJsonSchema(z.any()), + : zodToJsonSchema(z.object({})), output: action.schema?.output ? zodToJsonSchema(action.schema.output(z)) - : zodToJsonSchema(z.any()), + : zodToJsonSchema(z.object({})), }, })), }); diff --git a/packages/backend-defaults/src/entrypoints/actionsRegistry/actionsRegistryServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/actionsRegistry/actionsRegistryServiceFactory.test.ts index 9d6674c0cf..e885cb10a5 100644 --- a/packages/backend-defaults/src/entrypoints/actionsRegistry/actionsRegistryServiceFactory.test.ts +++ b/packages/backend-defaults/src/entrypoints/actionsRegistry/actionsRegistryServiceFactory.test.ts @@ -181,6 +181,109 @@ describe('actionsRegistryServiceFactory', () => { }); }); + it('should set default attributes', async () => { + const pluginSubject = createBackendPlugin({ + pluginId: 'my-plugin', + register(reg) { + reg.registerInit({ + deps: { + actionsRegistry: coreServices.actionsRegistry, + }, + async init({ actionsRegistry }) { + actionsRegistry.register({ + name: 'test', + title: 'Test', + description: 'Test', + schema: { + input: z => z.object({}), + output: z => z.object({}), + }, + action: async () => ({ output: { ok: true } }), + }); + }, + }); + }, + }); + + const { server } = await startTestBackend({ + features: [pluginSubject, ...defaultServices], + }); + + const { body, status } = await request(server).get( + '/api/my-plugin/.backstage/actions/v1/actions', + ); + + expect(status).toBe(200); + + expect(body).toMatchObject({ + actions: [ + { + name: 'test', + attributes: { + destructive: false, + idempotent: false, + readOnly: false, + }, + }, + ], + }); + }); + + it('should allow setting attributes', async () => { + const pluginSubject = createBackendPlugin({ + pluginId: 'my-plugin', + register(reg) { + reg.registerInit({ + deps: { + actionsRegistry: coreServices.actionsRegistry, + }, + async init({ actionsRegistry }) { + actionsRegistry.register({ + name: 'test', + title: 'Test', + description: 'Test', + attributes: { + destructive: true, + idempotent: true, + readOnly: true, + }, + schema: { + input: z => z.object({}), + output: z => z.object({}), + }, + action: async () => ({ output: { ok: true } }), + }); + }, + }); + }, + }); + + const { server } = await startTestBackend({ + features: [pluginSubject, ...defaultServices], + }); + + const { body, status } = await request(server).get( + '/api/my-plugin/.backstage/actions/v1/actions', + ); + + expect(status).toBe(200); + + expect(body).toMatchObject({ + actions: [ + { + name: 'test', + title: 'Test', + description: 'Test', + attributes: { + destructive: true, + idempotent: true, + readOnly: true, + }, + }, + ], + }); + }); + it('should forces registration of input and output schema as objects', async () => { const pluginSubject = createBackendPlugin({ pluginId: 'my-plugin', diff --git a/packages/backend-plugin-api/src/services/definitions/ActionsRegistryService.ts b/packages/backend-plugin-api/src/services/definitions/ActionsRegistryService.ts index 44df71f41b..6c0936f85c 100644 --- a/packages/backend-plugin-api/src/services/definitions/ActionsRegistryService.ts +++ b/packages/backend-plugin-api/src/services/definitions/ActionsRegistryService.ts @@ -40,6 +40,11 @@ export type ActionsRegistryActionOptions< input: (zod: typeof z) => TInputSchema; output: (zod: typeof z) => TOutputSchema; }; + attributes?: { + destructive?: boolean; + idempotent?: boolean; + readOnly?: boolean; + }; action: ( context: ActionsRegistryActionContext, ) => Promise< diff --git a/packages/backend-plugin-api/src/services/definitions/ActionsService.ts b/packages/backend-plugin-api/src/services/definitions/ActionsService.ts index 69b13afaaa..d595008a42 100644 --- a/packages/backend-plugin-api/src/services/definitions/ActionsService.ts +++ b/packages/backend-plugin-api/src/services/definitions/ActionsService.ts @@ -29,6 +29,11 @@ export type ActionsServiceAction = { input: JSONSchema7; output: JSONSchema7; }; + attributes: { + readOnly: boolean; + destructive: boolean; + idempotent: boolean; + }; }; /** diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index d58720723d..1d36f77026 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -59,12 +59,14 @@ "@keyv/valkey": "^1.0.1", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5", + "@types/json-schema": "^7.0.6", "@types/keyv": "^4.2.0", "@types/qs": "^6.9.6", "better-sqlite3": "^11.0.0", "cookie": "^0.7.0", "express": "^4.17.1", "fs-extra": "^11.0.0", + "json-schema": "^0.4.0", "keyv": "^5.2.1", "knex": "^3.0.0", "mysql2": "^3.0.0", diff --git a/yarn.lock b/yarn.lock index b09bfdd787..8fde916ae8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3784,6 +3784,7 @@ __metadata: "@types/express": "npm:^4.17.6" "@types/express-serve-static-core": "npm:^4.17.5" "@types/jest": "npm:*" + "@types/json-schema": "npm:^7.0.6" "@types/keyv": "npm:^4.2.0" "@types/qs": "npm:^6.9.6" "@types/supertest": "npm:^2.0.8" @@ -3791,6 +3792,7 @@ __metadata: cookie: "npm:^0.7.0" express: "npm:^4.17.1" fs-extra: "npm:^11.0.0" + json-schema: "npm:^0.4.0" keyv: "npm:^5.2.1" knex: "npm:^3.0.0" mysql2: "npm:^3.0.0" From 52e0626d94331c5e3621d7ddd118178733bcb577 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Fri, 13 Jun 2025 10:27:44 +0200 Subject: [PATCH 2/3] chore: updated API reports Signed-off-by: benjdlambert --- packages/backend-plugin-api/report.api.md | 10 ++++++++++ packages/backend-test-utils/package.json | 2 -- yarn.lock | 2 -- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/backend-plugin-api/report.api.md b/packages/backend-plugin-api/report.api.md index 3b24fcb012..d5030adb9c 100644 --- a/packages/backend-plugin-api/report.api.md +++ b/packages/backend-plugin-api/report.api.md @@ -48,6 +48,11 @@ export type ActionsRegistryActionOptions< input: (zod: typeof z) => TInputSchema; output: (zod: typeof z) => TOutputSchema; }; + attributes?: { + destructive?: boolean; + idempotent?: boolean; + readOnly?: boolean; + }; action: (context: ActionsRegistryActionContext) => Promise< z.infer extends void ? void @@ -94,6 +99,11 @@ export type ActionsServiceAction = { input: JSONSchema7; output: JSONSchema7; }; + attributes: { + readOnly: boolean; + destructive: boolean; + idempotent: boolean; + }; }; // @public diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index 1d36f77026..d58720723d 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -59,14 +59,12 @@ "@keyv/valkey": "^1.0.1", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5", - "@types/json-schema": "^7.0.6", "@types/keyv": "^4.2.0", "@types/qs": "^6.9.6", "better-sqlite3": "^11.0.0", "cookie": "^0.7.0", "express": "^4.17.1", "fs-extra": "^11.0.0", - "json-schema": "^0.4.0", "keyv": "^5.2.1", "knex": "^3.0.0", "mysql2": "^3.0.0", diff --git a/yarn.lock b/yarn.lock index 8fde916ae8..b09bfdd787 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3784,7 +3784,6 @@ __metadata: "@types/express": "npm:^4.17.6" "@types/express-serve-static-core": "npm:^4.17.5" "@types/jest": "npm:*" - "@types/json-schema": "npm:^7.0.6" "@types/keyv": "npm:^4.2.0" "@types/qs": "npm:^6.9.6" "@types/supertest": "npm:^2.0.8" @@ -3792,7 +3791,6 @@ __metadata: cookie: "npm:^0.7.0" express: "npm:^4.17.1" fs-extra: "npm:^11.0.0" - json-schema: "npm:^0.4.0" keyv: "npm:^5.2.1" knex: "npm:^3.0.0" mysql2: "npm:^3.0.0" From 31dcaf72ae0826258e13820556157030f7fc12be Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Mon, 16 Jun 2025 14:58:49 +0200 Subject: [PATCH 3/3] chore: destructive by default Signed-off-by: benjdlambert --- .../src/entrypoints/actions/actionsServiceFactory.test.ts | 2 +- .../actionsRegistry/DefaultActionsRegistryService.ts | 5 +++-- .../actionsRegistry/actionsRegistryServiceFactory.test.ts | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/backend-defaults/src/entrypoints/actions/actionsServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/actions/actionsServiceFactory.test.ts index 58a79c6a2a..c43945f1d5 100644 --- a/packages/backend-defaults/src/entrypoints/actions/actionsServiceFactory.test.ts +++ b/packages/backend-defaults/src/entrypoints/actions/actionsServiceFactory.test.ts @@ -312,7 +312,7 @@ describe('actionsServiceFactory', () => { }, }, attributes: { - destructive: false, + destructive: true, idempotent: false, readOnly: false, }, diff --git a/packages/backend-defaults/src/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts b/packages/backend-defaults/src/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts index cddb04c817..bee3f7d565 100644 --- a/packages/backend-defaults/src/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts +++ b/packages/backend-defaults/src/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts @@ -68,8 +68,9 @@ export class DefaultActionsRegistryService implements ActionsRegistryService { id, ...action, attributes: { - // todo(blam): what's safe defaults? - destructive: action.attributes?.destructive ?? false, + // Inspired by the @modelcontextprotocol/sdk defaults for the hints. + // https://github.com/modelcontextprotocol/typescript-sdk/blob/dd69efa1de8646bb6b195ff8d5f52e13739f4550/src/types.ts#L777-L812 + destructive: action.attributes?.destructive ?? true, idempotent: action.attributes?.idempotent ?? false, readOnly: action.attributes?.readOnly ?? false, }, diff --git a/packages/backend-defaults/src/entrypoints/actionsRegistry/actionsRegistryServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/actionsRegistry/actionsRegistryServiceFactory.test.ts index e885cb10a5..2de6272365 100644 --- a/packages/backend-defaults/src/entrypoints/actionsRegistry/actionsRegistryServiceFactory.test.ts +++ b/packages/backend-defaults/src/entrypoints/actionsRegistry/actionsRegistryServiceFactory.test.ts @@ -220,7 +220,7 @@ describe('actionsRegistryServiceFactory', () => { { name: 'test', attributes: { - destructive: false, + destructive: true, idempotent: false, readOnly: false, }, @@ -243,7 +243,7 @@ describe('actionsRegistryServiceFactory', () => { title: 'Test', description: 'Test', attributes: { - destructive: true, + destructive: false, idempotent: true, readOnly: true, }, @@ -275,7 +275,7 @@ describe('actionsRegistryServiceFactory', () => { title: 'Test', description: 'Test', attributes: { - destructive: true, + destructive: false, idempotent: true, readOnly: true, },