Skip to content

feat: Implement Extension API for Integration Dependencies and add related tests#399

Open
f-nie wants to merge 10 commits intocap-js:mainfrom
f-nie:feat/eventBroker
Open

feat: Implement Extension API for Integration Dependencies and add related tests#399
f-nie wants to merge 10 commits intocap-js:mainfrom
f-nie:feat/eventBroker

Conversation

@f-nie
Copy link
Copy Markdown

@f-nie f-nie commented Mar 17, 2026

Closes #372, Closes #208

Summary

This PR introduces an Extension Registry pattern that enables external plugins (e.g., @cap-js/event-broker) to register Integration Dependency providers with the ORD plugin at runtime.

Motivation

Applications using @cap-js/event-broker to consume external CloudEvents should be able to document these dependencies in their ORD metadata. The original issue proposed adding Event Broker detection logic directly to the ORD plugin. However, this approach would create tight coupling between plugins.

Instead, this PR implements a provider registry pattern (similar to the Java plugin's Spring DI OrdIntegrationDependencyProvider beans) that allows external plugins to contribute Integration Dependency data without modifying the ORD plugin.

Changes

New Files

File Description
lib/extensionRegistry.js Extension Registry API for provider registration

Modified Files

File Description
lib/integrationDependency.js Added createEventIntegrationDependency() that consumes provider data
cds-plugin.js Exports registerIntegrationDependencyProvider() for external plugins
memory-bank/systemPatterns.md Documentation of Extension Registry pattern
.github/copilot-instructions.md Updated file map with new modules

New Tests

File Description
__tests__/unit/extensionRegistry.test.js Unit tests for registry functions
__tests__/unit/extensionRegistry.integration.test.js Integration tests with Integration Dependency

API

Provider Registration (for external plugins)

const ord = require("@cap-js/ord");

ord.registerIntegrationDependencyProvider(() => ({
  eventResources: [
    {
      ordId: "sap.s4:eventResource:CE_SALESORDEREVENTS:v1",
      events: [
        "sap.s4.beh.salesorder.v1.SalesOrder.Changed.v1",
        "sap.s4.beh.salesorder.v1.SalesOrder.Created.v1",
      ],
    },
  ],
}));

Provider Contract

Providers return { eventResources: [{ ordId, events }] } or null:

  • ordId: The external event resource ORD ID (from SAP Business Accelerator Hub)
  • events: Array of event type names that the application subscribes to (multiple events can belong to the same event resource)

The ORD plugin transforms this into the final ORD subset structure:

{
  "integrationDependencies": [
    {
      "ordId": "customer.app:integrationDependency:consumedEvents:v1",
      "title": "Consumed Events",
      "aspects": [
        {
          "eventResources": [
            {
              "ordId": "sap.s4:eventResource:CE_SALESORDEREVENTS:v1",
              "subset": [
                {
                  "eventType": "sap.s4.beh.salesorder.v1.SalesOrder.Changed.v1"
                },
                {
                  "eventType": "sap.s4.beh.salesorder.v1.SalesOrder.Created.v1"
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

Design Decisions

Why Extension Registry instead of direct Event Broker detection?

Approach Pros Cons
Direct detection (original proposal) Single plugin change Tight coupling, ORD plugin needs Event Broker knowledge
Extension Registry (this PR) Loose coupling, extensible Requires changes in both plugins

The Extension Registry pattern:

  • Follows Java plugin architecture (Spring DI beans)
  • Allows any plugin to contribute Integration Dependencies
  • Keeps Event Broker logic in Event Broker plugin
  • ORD plugin remains agnostic of specific messaging implementations

Why providers return simple { ordId, events } instead of full ORD structure?

  • Simplicity: External plugins don't need to know ORD structure details
  • Consistency: ORD plugin controls the final structure (ordId format, packages, visibility)
  • Maintainability: Changes to ORD spec only require ORD plugin updates

Runtime-Only Support

Like the Java plugin, Integration Dependencies from providers are only available at runtime (cds.once('served')). Build-time generation would require a different mechanism (not implemented).

Testing

npm test -- __tests__/unit/extensionRegistry
npm test -- __tests__/unit/integrationDependency

All 37 tests pass.

Checklist

  • Memory Bank updated (systemPatterns.md)
  • Copilot instructions updated
  • Unit tests added
  • Integration tests added
  • Lint passes
  • No snapshot changes required

References

Related PR for Event Broker plugin to consume the extension registry: cap-js/event-broker#48

Copy link
Copy Markdown
Contributor

@hyperspace-insights hyperspace-insights bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR introduces a well-structured Extension Registry pattern with good test coverage. There are four issues to address: the most critical is the spread ordering in createEventIntegrationDependency that allows user config to silently overwrite computed structural fields (including the aspects array containing the provider-merged event resources); the JSDoc example in cds-plugin.js documents the wrong provider contract which will mislead consumers; a noisy Logger.log fires on every ORD generation for the legitimate "no events subscribed" case; and one test block exercises only inline test logic rather than any plugin code.

PR Bot Information

Version: 1.18.10 | 📖 Documentation | 🚨 Create Incident | 💬 Feedback

  • Event Trigger: pull_request.opened
  • Correlation ID: aefffa00-222d-11f1-996c-2f03946238c3
  • LLM: anthropic--claude-4.6-sonnet
  • Agent Instructions:

Comment on lines +195 to +211
return {
ordId: `${appConfig.ordNamespace}:${ORD_RESOURCE_TYPE.integrationDependency}:${EVENT_INTEGRATION_DEPENDENCY_RESOURCE_NAME}:v1`,
title: "Consumed Events",
version: "1.0.0",
releaseStatus: "active",
visibility: RESOURCE_VISIBILITY.public,
mandatory: false,
partOfPackage: packageId,
aspects: [
{
title: "Subscribed Event Types",
mandatory: false,
eventResources: allEventResources,
...eventIntegrationDepConfig.aspect, // Allow aspect customization
},
],
...eventIntegrationDepConfig, // Allow top-level customization (except computed fields)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Spread of eventIntegrationDepConfig can silently overwrite computed fields

...eventIntegrationDepConfig is spread at the top level of the returned object after aspects, ordId, partOfPackage, etc. are already set. This means a user who places any of these keys (e.g. ordId, partOfPackage, aspects) inside cdsrc.json's eventIntegrationDependency section will overwrite the values that were intentionally computed by this function — including the structured aspects array containing all provider-merged eventResources.

The same pattern exists in createIntegrationDependency for integrationDepConfig, so this is a pre-existing issue surfaced by the new code. However the aspects array here is the critical payload of the new feature and is silently overridable.

Consider placing the spread before the computed fields, or explicitly omitting dangerous keys:

Suggested change
return {
ordId: `${appConfig.ordNamespace}:${ORD_RESOURCE_TYPE.integrationDependency}:${EVENT_INTEGRATION_DEPENDENCY_RESOURCE_NAME}:v1`,
title: "Consumed Events",
version: "1.0.0",
releaseStatus: "active",
visibility: RESOURCE_VISIBILITY.public,
mandatory: false,
partOfPackage: packageId,
aspects: [
{
title: "Subscribed Event Types",
mandatory: false,
eventResources: allEventResources,
...eventIntegrationDepConfig.aspect, // Allow aspect customization
},
],
...eventIntegrationDepConfig, // Allow top-level customization (except computed fields)
// Destructure to prevent eventIntegrationDepConfig from overriding computed structural fields
const { aspect: _, aspects: _aspects, ordId: _ordId, partOfPackage: _pkg, ...safeTopLevelConfig } = eventIntegrationDepConfig;
return {
ordId: `${appConfig.ordNamespace}:${ORD_RESOURCE_TYPE.integrationDependency}:${EVENT_INTEGRATION_DEPENDENCY_RESOURCE_NAME}:v1`,
title: "Consumed Events",
version: "1.0.0",
releaseStatus: "active",
visibility: RESOURCE_VISIBILITY.public,
mandatory: false,
partOfPackage: packageId,
...safeTopLevelConfig, // Allow top-level customization (except structural/computed fields)
aspects: [
{
title: "Subscribed Event Types",
mandatory: false,
...eventIntegrationDepConfig.aspect, // Allow aspect customization
eventResources: allEventResources,
},
],
};

Double-check suggestion before committing. Edit this comment for amendments.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful

Comment on lines +29 to +34
* @example
* const ord = require("@cap-js/ord");
* ord.registerIntegrationDependencyProvider(() => ({
* namespace: "sap.s4",
* events: ["sap.s4.beh.salesorder.v1.SalesOrder.Changed.v1"]
* }));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: JSDoc example in cds-plugin.js shows the old provider contract, not the one actually implemented

The @example block documents the provider returning { namespace, events }, but the actual contract (as implemented in extensionRegistry.js and all tests) is { eventResources: [{ ordId, events }] }. A consumer reading only this file will write a non-functional provider that silently produces no output (filtered out in getProvidedIntegrationDependencies).

Should update the example to match the real contract:

Suggested change
* @example
* const ord = require("@cap-js/ord");
* ord.registerIntegrationDependencyProvider(() => ({
* namespace: "sap.s4",
* events: ["sap.s4.beh.salesorder.v1.SalesOrder.Changed.v1"]
* }));
* @example
* const ord = require("@cap-js/ord");
* ord.registerIntegrationDependencyProvider(() => ({
* eventResources: [
* {
* ordId: "sap.s4:eventResource:CE_SALESORDEREVENTS:v1",
* events: ["sap.s4.beh.salesorder.v1.SalesOrder.Changed.v1"]
* }
* ]
* }));
*/

Double-check suggestion before committing. Edit this comment for amendments.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful

Comment on lines +80 to +84
.filter((result) => {
// Validate result structure - must have eventResources array
if (!result) return false;
if (!Array.isArray(result.eventResources) || result.eventResources.length === 0) {
Logger.log("Integration Dependency provider returned no eventResources");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic Error: getProvidedIntegrationDependencies filters out results with an empty eventResources array with a Logger.log, but the same empty-array case is also checked (and silently ignored) in createEventIntegrationDependency at line 165–175. This is fine for defence-in-depth, but the Logger.log message "Integration Dependency provider returned no eventResources" fires even for a provider that returned { eventResources: [] } after a legitimate "no events subscribed right now" state — which is a valid runtime condition, not something to warn about.

More critically, a provider that returns { eventResources: [] } will cause this log to fire at INFO level on every ORD document generation even when the application has simply not subscribed to any events. Consider silently dropping empty results without logging, or logging at debug level only.

Suggested change
.filter((result) => {
// Validate result structure - must have eventResources array
if (!result) return false;
if (!Array.isArray(result.eventResources) || result.eventResources.length === 0) {
Logger.log("Integration Dependency provider returned no eventResources");
.filter((result) => {
// Validate result structure - must have non-empty eventResources array
if (!result) return false;
if (!Array.isArray(result.eventResources) || result.eventResources.length === 0) {
return false;
}
return true;
});

Double-check suggestion before committing. Edit this comment for amendments.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful

Comment on lines +168 to +199
describe("Event Broker config-based eventResources simulation", () => {
it("should build eventResources from config and subscribed events", () => {
// Simulate the Event Broker's buildEventResourcesFromConfig logic
const eventResourcesConfig = [
{
ordId: "sap.s4:eventResource:CE_BUSINESSPARTNEREVENTS:v1",
eventTypes: ["sap.s4.beh.businesspartner.v1.BusinessPartner.Changed.v1"],
},
];
const subscribedEvents = ["sap.s4.beh.businesspartner.v1.BusinessPartner.Changed.v1"];

// Simulate matching logic - Event Broker returns {ordId, events}
const subscribedSet = new Set(subscribedEvents);
const eventResources = [];

for (const config of eventResourcesConfig) {
const matchedEventTypes = config.eventTypes.filter((et) => subscribedSet.has(et));
if (matchedEventTypes.length > 0) {
eventResources.push({
ordId: config.ordId,
events: matchedEventTypes, // Simple array, not subset structure
});
}
}

expect(eventResources).toHaveLength(1);
expect(eventResources[0].ordId).toBe("sap.s4:eventResource:CE_BUSINESSPARTNEREVENTS:v1");
expect(eventResources[0].events).toHaveLength(1);
expect(eventResources[0].events[0]).toBe("sap.s4.beh.businesspartner.v1.BusinessPartner.Changed.v1");
});
});
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic Error: The last describe block ("Event Broker config-based eventResources simulation") does not test any code from the ORD plugin — it only exercises inline logic written directly in the test body. This means it provides no coverage guarantee for the actual integration between Event Broker and the ORD plugin, and could pass even if getIntegrationDependencies or extensionRegistry were completely broken.

Consider either removing this test (it tests a hypothetical external plugin's internal logic) or converting it into an actual assertion against getIntegrationDependencies by registering a provider that simulates this matching logic.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful

f-nie added 2 commits March 17, 2026 19:29
- Resolve conflicts in copilot-instructions.md, systemPatterns.md, integration-dependency.test.js
- Rename extensionRegistry.js to extension-registry.js (consistent naming convention)
- Rename extension registry test files to kebab-case
- Update all imports to use new kebab-case file names
- Prevent config spread from silently overwriting computed fields (ordId, partOfPackage, aspects)
  Use _.omit to exclude dangerous keys before spreading user config
- Fix JSDoc example in cds-plugin.js to show correct provider contract
  Old: { namespace, events } -> New: { eventResources: [{ ordId, events }] }
- Change Logger.log to Logger.debug for empty eventResources
  Empty array is valid runtime state (no events subscribed)
- Convert simulation test to actual integration test
  Now registers provider and tests via getIntegrationDependencies
@hyperspace-insights
Copy link
Copy Markdown
Contributor

Summary

The following content is AI-generated and provides a summary of the pull request:


Implement Extension API for Integration Dependencies

New Feature

✨ Introduces an Extension Registry pattern that enables external plugins (e.g., @cap-js/event-broker) to register Integration Dependency providers with the ORD plugin at runtime. This allows consumed external CloudEvents to be documented as proper integrationDependencies in the ORD metadata rather than as eventResources, resolving the incorrect classification reported in issue #208.

The design follows a loose-coupling approach (similar to the Java plugin's Spring DI OrdIntegrationDependencyProvider beans), keeping Event Broker logic in the Event Broker plugin while the ORD plugin remains agnostic of specific messaging implementations.

Provider Registration API (for external plugins):

const ord = require("@cap-js/ord");
ord.registerIntegrationDependencyProvider(() => ({
    eventResources: [
        {
            ordId: "sap.s4:eventResource:CE_SALESORDEREVENTS:v1",
            events: ["sap.s4.beh.salesorder.v1.SalesOrder.Changed.v1"],
        },
    ],
}));

Changes

  • lib/extension-registry.js: New — Core Extension Registry module with registerIntegrationDependencyProvider(), getProvidedIntegrationDependencies(), and hasIntegrationDependencyProviders() functions. Provider errors are caught and logged without breaking ORD generation.
  • lib/integration-dependency.js: Added createEventIntegrationDependency() that collects eventResources from all registered providers and builds the ORD integrationDependency structure with proper subset entries. Also refactored getIntegrationDependencies() to support both API-based and event-based dependencies, and applied safe config merging to prevent silent overwrites of computed fields.
  • cds-plugin.js: Exports registerIntegrationDependencyProvider as the public Extension API.
  • __tests__/unit/extension-registry.test.js: New — Unit tests for registry registration, validation, multi-provider support, and error handling.
  • __tests__/unit/extension-registry.integration.test.js: New — Integration tests simulating the Event Broker plugin registering providers and verifying the generated ORD output.
  • __tests__/unit/integration-dependency.test.js: Added tests for createEventIntegrationDependency, config-based customization, and multi-provider merging.
  • README.md: Documented the Extension API for external plugins with usage examples and generated ORD output.
  • memory-bank/systemPatterns.md: Added the Extension Registry Pattern documentation.
  • .github/copilot-instructions.md: Updated file map to include extension-registry.js and integration-dependency.js.

Jira Issues

GitHub Issues:

  • #372: Generate ORD Integration Dependencies for consumed Event Broker events
  • #208: Consumed S/4 Events are exposed as eventResources instead as integrationDependencies

  • 🔄 Regenerate and Update Summary
  • ✏️ Insert as PR Description (deletes this comment)
  • 🗑️ Delete comment
PR Bot Information

Version: 1.19.1 | 📖 Documentation | 🚨 Create Incident | 💬 Feedback


💌 Have ideas or want to contribute? Create an issue and share your thoughts with us!
📑 Check out the documentation for more information.
📬 Subscribe to the Hyperspace PR Bot DL to get the latest announcements and pilot features!

Made with ❤️ by Hyperspace.

Copy link
Copy Markdown
Contributor

@hyperspace-insights hyperspace-insights bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR introduces a well-structured Extension Registry pattern, but has a critical documentation bug: the README's provider contract example and description show the wrong API shape ({ namespace, events } instead of { eventResources: [{ ordId, events }] }), which would cause silent failures for any external plugin author following the docs. Additionally, the memory bank has a duplicate section number and an incorrect file path reference. The redundant guard in createEventIntegrationDependency should also be cleaned up for clarity.

PR Bot Information

Version: 1.19.1 | 📖 Documentation | 🚨 Create Incident | 💬 Feedback

  • LLM: anthropic--claude-4.6-sonnet
  • Agent Instructions:
  • Correlation ID: d54410a0-23b0-11f1-8328-7f939c0f1521
  • Event Trigger: pull_request.ready_for_review

f-nie and others added 6 commits March 19, 2026 18:06
Co-authored-by: hyperspace-insights[bot] <209611008+hyperspace-insights[bot]@users.noreply.github.com>
Co-authored-by: hyperspace-insights[bot] <209611008+hyperspace-insights[bot]@users.noreply.github.com>
Co-authored-by: hyperspace-insights[bot] <209611008+hyperspace-insights[bot]@users.noreply.github.com>
Co-authored-by: hyperspace-insights[bot] <209611008+hyperspace-insights[bot]@users.noreply.github.com>
Co-authored-by: hyperspace-insights[bot] <209611008+hyperspace-insights[bot]@users.noreply.github.com>
@zongqichen zongqichen requested review from mlakov and zongqichen March 23, 2026 14:21
@mlakov
Copy link
Copy Markdown
Contributor

mlakov commented Mar 27, 2026

After reviewing the PR and getting myself familiar with the context - I do believe that this new feature is having a great overlap with the customOrdContentFile custom configuration (see: documentation). Using the abovementioned feature allos for specifying custom integration dependency entries in a rather generic way, one that I personally find more intuitive and user-friendly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants