Skip to content
Open
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
2 changes: 0 additions & 2 deletions __tests__/bookshop/ord/custom.ord.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
{
"namespace": "should not update since has conflict with cdsrc.json",
"openResourceDiscovery": "should not update since not defined in cdsrc.json",
"packages": [
{
"description": "Description for capire bookshop ord sample version 2",
Expand Down
11 changes: 0 additions & 11 deletions __tests__/unit/__snapshots__/extend-ord-with-custom.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,6 @@ exports[`extendOrdWithCustom extend-ord-with-custom should enhance the list of g
}
`;

exports[`extendOrdWithCustom extend-ord-with-custom should ignore and log warn if found ord top-level primitive property in customOrdFile 1`] = `
{
"packages": [
{
"localId": "smDataProducts",
"ordId": "sap.sm:package:smDataProducts:v1",
},
],
}
`;

exports[`extendOrdWithCustom extend-ord-with-custom should should patch the existing generated ord resources 1`] = `
{
"apiResources": [
Expand Down
2 changes: 1 addition & 1 deletion __tests__/unit/__snapshots__/ord.e2e.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -3550,4 +3550,4 @@ exports[`Tests for products and packages should use valid custom products ordId
},
],
}
`;
`;
11 changes: 10 additions & 1 deletion __tests__/unit/defaults.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const path = require("path");
const cds = require("@sap/cds");

const defaults = require("../../lib/defaults");
const { DOCUMENT_PERSPECTIVES, ORD_ACCESS_STRATEGY } = require("../../lib/constants");
const cds = require("@sap/cds");

jest.mock("fs", () => {
return {
Expand All @@ -17,7 +19,14 @@ describe("defaults", () => {

describe("openResourceDiscovery", () => {
it("should return default value", () => {
const packageJson = require(path.join(__dirname, "..", "..", "package.json"));

expect(defaults.openResourceDiscovery).toMatchSnapshot();
expect(
packageJson.devDependencies["@open-resource-discovery/specification"].startsWith(
defaults.openResourceDiscovery,
),
).toBe(true);
});
});

Expand Down
34 changes: 13 additions & 21 deletions __tests__/unit/extend-ord-with-custom.test.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
const cds = require("@sap/cds");
const path = require("path");
const {
MERGE_STRATEGIES,
getCustomORDContent,
compareAndHandleCustomORDContentWithExistingContent,
} = require("../../lib/extend-ord-with-custom");

describe("MERGE_STRATEGIES", () => {
it("ensure that all known document properties have a merge strategy", () => {
const schema = require("@open-resource-discovery/specification/dist/generated/spec/v1/schemas/Document.schema.json");

expect(Object.keys(MERGE_STRATEGIES).sort()).toEqual(Object.keys(schema.properties).sort());
});
});

describe("extendOrdWithCustom", () => {
let appConfig = {};
let warningSpy;

beforeAll(() => {
cds.env = {};
warningSpy = jest.spyOn(console, "warn");
});

beforeEach(() => {
Expand All @@ -28,35 +35,20 @@ describe("extendOrdWithCustom", () => {
});

describe("extend-ord-with-custom", () => {
it("should return undefined if there is no customOrdContentFile property in the .cdsrc.json", () => {
it("should return empty object if there is no customOrdContentFile property in the .cdsrc.json", () => {
appConfig.env.customOrdContentFile = undefined;

const result = getCustomORDContent(appConfig);

expect(result).toEqual(undefined);
expect(result).toEqual({});
});

it("should return undefined if customOrdContentFile property in the .cdsrc.json points to NON-EXISTING custom ord file", () => {
it("should return empty object if customOrdContentFile property in the .cdsrc.json points to NON-EXISTING custom ord file", () => {
appConfig.env.customOrdContentFile = "./ord/NotExistingCustom.ord.json";

const result = getCustomORDContent(appConfig);

expect(result).toEqual(undefined);
});

it("should ignore and log warn if found ord top-level primitive property in customOrdFile", () => {
prepareTestEnvironment({ namespace: "sap.sample" }, appConfig, "testCustomORDContentFileThrowErrors.json");

const result = compareAndHandleCustomORDContentWithExistingContent({}, getCustomORDContent(appConfig));

expect(warningSpy).toHaveBeenCalledTimes(3);
expect(warningSpy).toHaveBeenCalledWith(
"[ord-plugin] -",
expect.stringContaining("Found ord top level primitive ord property in customOrdFile:"),
expect.anything(),
expect.stringContaining("Please define it in .cdsrc.json."),
);
expect(result).toMatchSnapshot();
expect(result).toEqual({});
});

it("should add new ord resources that are not supported by cap framework", () => {
Expand Down
131 changes: 80 additions & 51 deletions lib/extend-ord-with-custom.js
Original file line number Diff line number Diff line change
@@ -1,68 +1,97 @@
const { CONTENT_MERGE_KEY } = require("./constants");
const cds = require("@sap/cds");
const fs = require("fs");
const Logger = require("./logger");
const path = require("path");
const _ = require("lodash");

function cleanNullProperties(obj) {
for (const key in obj) {
if (obj[key] === null) {
delete obj[key];
} else if (typeof obj[key] === "object") {
cleanNullProperties(obj[key]);
}
}
return obj;
}

function patchGeneratedOrdResources(destinationObj, sourceObj) {
const destObj = _.keyBy(destinationObj, CONTENT_MERGE_KEY);
const srcObj = _.keyBy(sourceObj, CONTENT_MERGE_KEY);
const SCALAR_TYPES = Object.freeze(["number", "string", "boolean"]);

for (const ordId in srcObj) {
destObj[ordId] =
ordId in destObj
? _.assignWith(structuredClone(destObj[ordId]), structuredClone(srcObj[ordId]))
: srcObj[ordId];
}
const MERGE_STRATEGIES = Object.freeze({
Comment thread
zongqichen marked this conversation as resolved.
// Directly replace scalar values and scalar value arrays
$schema: (current, custom) => custom,
description: (current, custom) => custom,
perspective: (current, custom) => custom,
openResourceDiscovery: (current, custom) => custom,
policyLevel: (current, custom) => custom,
customPolicyLevel: (current, custom) => custom,
policyLevels: (current, custom) => prune(custom),

return cleanNullProperties(Object.values(destObj));
}
// Perform simple merge for objects
describedSystemType: (current, custom) => prune(_.assign(current, custom)),
describedSystemVersion: (current, custom) => prune(_.assign(current, custom)),
describedSystemInstance: (current, custom) => prune(_.assign(current, custom)),

function compareAndHandleCustomORDContentWithExistingContent(ordContent, customORDContent) {
const clonedOrdContent = structuredClone(ordContent);
for (const key in customORDContent) {
const propertyType = typeof customORDContent[key];
if (propertyType !== "object" && propertyType !== "array") {
Logger.warn(
"Found ord top level primitive ord property in customOrdFile:",
key,
". Please define it in .cdsrc.json.",
);
// Perform smart merge for arrays of objects
agents: (current, custom) => merge(current, custom, ["ordId"]),
vendors: (current, custom) => merge(current, custom, ["ordId"]),
products: (current, custom) => merge(current, custom, ["ordId"]),
packages: (current, custom) => merge(current, custom, ["ordId"]),
groups: (current, custom) => merge(current, custom, ["groupId"]),
entityTypes: (current, custom) => merge(current, custom, ["ordId"]),
apiResources: (current, custom) => merge(current, custom, ["ordId"]),
capabilities: (current, custom) => merge(current, custom, ["ordId"]),
dataProducts: (current, custom) => merge(current, custom, ["ordId"]),
eventResources: (current, custom) => merge(current, custom, ["ordId"]),
groupTypes: (current, custom) => merge(current, custom, ["groupTypeId"]),
consumptionBundles: (current, custom) => merge(current, custom, ["ordId"]),
integrationDependencies: (current, custom) => merge(current, custom, ["ordId"]),
tombstones: (current, custom) => merge(current, custom, ["ordId", "groupId", "groupTypeId"]),
});

continue;
}
if (key in ordContent) {
clonedOrdContent[key] = patchGeneratedOrdResources(clonedOrdContent[key], customORDContent[key]);
} else {
clonedOrdContent[key] = customORDContent[key];
}
function prune(entity) {
if (Array.isArray(entity)) {
return entity
.filter((value) => ![null, undefined].includes(value))
.map((value) => (SCALAR_TYPES.includes(typeof value) ? value : prune(value)));
}
return clonedOrdContent;

return Object.fromEntries(
Object.entries(entity)
.filter(([, value]) => ![null, undefined].includes(value))
.map(([key, value]) => [key, SCALAR_TYPES.includes(typeof value) ? value : prune(value)]),
);
}

function getCustomORDContent(appConfig) {
if (!appConfig.env?.customOrdContentFile) return;
const pathToCustomORDContent = path.join(cds.root, appConfig.env?.customOrdContentFile);
if (!fs.existsSync(pathToCustomORDContent)) {
Logger.error("Custom ORD content file not found at", pathToCustomORDContent);
return;
}
return require(pathToCustomORDContent);
function merge(target, source, keys) {
const iteratee = (entity) => keys.map((key) => entity[key] || "").join(".");
const sources = _.keyBy(source || [], iteratee);
const targets = _.keyBy(target || [], iteratee);

return Array.from(new Set([...Object.keys(targets), ...Object.keys(sources)])) //
.map((key) => _.assign(structuredClone(targets[key]), structuredClone(sources[key])))
.map(prune);
}

module.exports = {
getCustomORDContent,
compareAndHandleCustomORDContentWithExistingContent,
MERGE_STRATEGIES,
getCustomORDContent: (configuration) => {
if (!configuration.env?.customOrdContentFile) return {};

const file = path.join(cds.root, configuration.env?.customOrdContentFile);
if (!fs.existsSync(file)) {
Logger.error("Custom ORD content file not found at", file);
return {};
}

return JSON.parse(fs.readFileSync(file, "utf8"));
},
compareAndHandleCustomORDContentWithExistingContent: (ordContent, customORDContent) => {
return Object.fromEntries([
// Process elements found only in 'ordContent'
...Object.entries(ordContent)
.filter(([key]) => !(key in customORDContent))
.map(([key, value]) => [key, structuredClone(value)]),

// Process elements found only in 'customORDContent'
...Object.entries(customORDContent)
.filter(([key]) => !(key in ordContent))
.map(([key, value]) => [key, structuredClone(value)]),

// Process elements found in both 'ordContent' and 'customORDContent'
...Object.entries(customORDContent)
.filter(([key]) => key in ordContent)
.filter(([, value]) => ![null, undefined].includes(value))
.map(([key, value]) => [key, MERGE_STRATEGIES[key](structuredClone(ordContent[key]), value)]),
]);
},
};
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
"express": "^5.0.0",
"jest": "^30.0.0",
"prettier": "3.8.3",
"supertest": "^7.0.0"
"supertest": "^7.0.0",
"@open-resource-discovery/specification": "1.14.5"
},
"peerDependencies": {
"@sap/cds": ">=8.9.4"
Expand Down