From 1ed523ab5a3b917942bffbff89b4b405273027da Mon Sep 17 00:00:00 2001 From: "Dr. David A. Kunz" Date: Mon, 28 Jul 2025 09:25:55 +0200 Subject: [PATCH 1/3] revert manual path --- lib/setModel.js | 137 ++++++++++++-------------------------------- tests/tools.test.js | 4 +- 2 files changed, 39 insertions(+), 102 deletions(-) diff --git a/lib/setModel.js b/lib/setModel.js index dffc99a..4153674 100644 --- a/lib/setModel.js +++ b/lib/setModel.js @@ -31,51 +31,13 @@ async function compileModel(path) { if (!compiled.definitions || Object.keys(compiled.definitions).length === 0) { throw new Error(`Compiled CDS model is invalid or empty for path: ${path}`) } - try { - compiled = cds.compile.for.odata(compiled) // needed for drafts - } catch { - // nothing to do - } - - augmentModel(compiled) - - const endTime = Date.now() - const compileDuration = endTime - startTime - - // Only do it once - if (!changeWatcher) { - const intervalMs = process.env.CDS_MCP_REFRESH_MS - ? parseInt(process.env.CDS_MCP_REFRESH_MS, 10) - : Math.max(compileDuration * 10, 20000) - changeWatcher = setInterval(async () => { - const hasChanged = await cdsFilesChanged(path) - if (hasChanged) { - await refreshModel(path) - } - }, intervalMs).unref() // Uses CDS_MCP_REFRESH_MS if set, otherwise defaults to 10x compile duration or 20s - } - return compiled -} - -// Refreshes the CDS model, only replaces cds.model if compilation succeeds -async function refreshModel(path) { - try { - const compiled = await compileModel(path) - cds.model = compiled - return compiled - } catch { - // If anything goes wrong, cds.model remains untouched - } -} - -// --- Helper functions below --- + compiled = cds.compile.for.nodejs(compiled) + const serviceInfo = cds.compile.to.serviceinfo(compiled) -// Augments the compiled CDS model with endpoints and exposed entities -function augmentModel(compiled) { - for (const defName in compiled.definitions) { - // Add name for each definition - const def = compiled.definitions[defName] - def.name = defName + // merge with definitions + for (const info of serviceInfo) { + const def = compiled.definitions[info.name] + Object.assign(def, info) } const _entities_in = (srv, compiled) => { @@ -86,81 +48,56 @@ function augmentModel(compiled) { if (e['@cds.autoexposed'] && !e['@cds.autoexpose']) continue if (/DraftAdministrativeData$/.test(e.name)) continue if (/[._]texts$/.test(e.name)) continue - // ignore for now - // if (cds.env.effective.odata.containment && service.definition._containedEntities.has(e.name)) continue + if (cds.env.effective.odata.containment && service.definition._containedEntities.has(e.name)) continue exposed.push(each) } return exposed } - // construct endpoint for each entity and add it to its definition - Object.keys(compiled.definitions) - .filter(name => compiled.definitions[name].kind === 'service') - .map(name => { - const srv = compiled.definitions[name] - srv.endpoints = getEndpoints(srv) - return srv - }) + compiled.services .flatMap(srv => srv.endpoints.map(endpoint => ({ srv, endpoint }))) .map(({ srv, endpoint }) => { const entities = _entities_in(srv, compiled) - srv.exposedEntities = [] for (const e of entities) { const eRelName = e.slice(srv.name.length + 1) - srv.exposedEntities.push(eRelName) const path = endpoint.path + eRelName.replace(/\./g, '_') const def = compiled.definitions[e] def.endpoints ??= [] def.endpoints.push({ kind: endpoint.kind, path }) + // Add fully qualified entity names to each service as 'exposedEntities' + for (const service of compiled.services) { + service.exposedEntities = _entities_in(service, compiled) + } } }) -} -// Partially taken over from @sap/cds, to avoid `compile.for.nodejs` and `compile.to.serviceinfo` -// or starting the real application. -// Custom servers (with paths defined in code) are not supported. -// TODO: Check how it works in Java. -const getEndpoints = srv => { - const _slugified = name => - /[^.]+$/ - .exec(name)[0] //> my.very.CatalogService --> CatalogService - .replace(/Service$/, '') //> CatalogService --> Catalog - .replace(/_/g, '-') //> foo_bar_baz --> foo-bar-baz - .replace(/([a-z0-9])([A-Z])/g, (_, c, C) => c + '-' + C) //> ODataFooBarX9 --> OData-Foo-Bar-X9 - .toLowerCase() //> FOO --> foo - let annos = srv['@protocol'] - if (annos) { - if (annos === 'none' || annos['='] === 'none') return [] - if (!annos.reduce) annos = [annos] - } else { - annos = [] - for (const kind of ['odata', 'rest']) { - let path = srv['@' + kind] || srv['@protocol.' + kind] - if (path) annos.push({ kind, path }) - } - } - - if (!annos.length) annos.push({ kind: 'odata' }) + const endTime = Date.now() + const compileDuration = endTime - startTime - const endpoints = annos.map(each => { - let { kind = each['='] || each, path } = each - if (typeof path !== 'string') path = srv['@path'] || _slugified(srv.name) - if (path[0] !== '/') - path = - { - 'odata-v4': '/odata/v4', - odata: '/odata/v4', - 'odata-v2': '/odata/v2', - rest: '/rest', - hcql: '/hcql' - }[kind] + - '/' + - path // prefix with protocol path - if (!path.endsWith('/')) path = path + '/' - return { kind, path } - }) + // Only do it once + if (!changeWatcher) { + const intervalMs = process.env.CDS_MCP_REFRESH_MS + ? parseInt(process.env.CDS_MCP_REFRESH_MS, 10) + : Math.max(compileDuration * 10, 20000) + changeWatcher = setInterval(async () => { + const hasChanged = await cdsFilesChanged(path) + if (hasChanged) { + await refreshModel(path) + } + }, intervalMs).unref() // Uses CDS_MCP_REFRESH_MS if set, otherwise defaults to 10x compile duration or 20s + } + return compiled +} - return endpoints +// Refreshes the CDS model, only replaces cds.model if compilation succeeds +async function refreshModel(path) { + try { + const compiled = await compileModel(path) + cds.model = compiled + return compiled + } catch { + // If anything goes wrong, cds.model remains untouched + } } // Global cache object for CDS file timestamps diff --git a/tests/tools.test.js b/tests/tools.test.js index 80da385..be281f7 100644 --- a/tests/tools.test.js +++ b/tests/tools.test.js @@ -20,7 +20,7 @@ test.describe('tools', () => { assert(Array.isArray(result[0].endpoints), 'Should contain endpoints') assert.equal(result[0].name, 'AdminService', 'Should find Adminservice.Books service') assert.equal(result[0].endpoints[0].kind, 'odata', 'Should contain odata endpoint kind') - assert.equal(result[0].endpoints[0].path, '/odata/v4/admin/', 'Should contain endpoint path') + assert.equal(result[0].endpoints[0].path, 'odata/v4/admin/', 'Should contain endpoint path') }) test('search_cds_definitions: fuzzy search for Books entity', async () => { @@ -35,7 +35,7 @@ test.describe('tools', () => { assert(books[0].name, 'AdminService.Books', 'Should find AdminService.Books entity') assert(Array.isArray(books[0].endpoints), 'Should contain endpoints') assert.equal(books[0].endpoints[0].kind, 'odata', 'Should contain odata endpoint kind') - assert.equal(books[0].endpoints[0].path, '/odata/v4/admin/Books', 'Should contain endpoint path') + assert.equal(books[0].endpoints[0].path, 'odata/v4/admin/Books', 'Should contain endpoint path') }) test('list_all_cds_definition_names: should list all entities', async () => { From b802acea9abf9a9d32636b2fe870c7e9466c93bb Mon Sep 17 00:00:00 2001 From: "Dr. David A. Kunz" Date: Mon, 28 Jul 2025 11:14:55 +0200 Subject: [PATCH 2/3] . --- lib/setModel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/setModel.js b/lib/setModel.js index 4153674..d43ba1f 100644 --- a/lib/setModel.js +++ b/lib/setModel.js @@ -31,7 +31,7 @@ async function compileModel(path) { if (!compiled.definitions || Object.keys(compiled.definitions).length === 0) { throw new Error(`Compiled CDS model is invalid or empty for path: ${path}`) } - compiled = cds.compile.for.nodejs(compiled) + compiled = cds.compile.for.odata(compiled) // to include drafts const serviceInfo = cds.compile.to.serviceinfo(compiled) // merge with definitions From 164c4dc1e3f080e2a267c03f2ac4ebd1152357d9 Mon Sep 17 00:00:00 2001 From: "Dr. David A. Kunz" Date: Mon, 28 Jul 2025 14:02:33 +0200 Subject: [PATCH 3/3] . --- lib/setModel.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/setModel.js b/lib/setModel.js index d43ba1f..da9ca0d 100644 --- a/lib/setModel.js +++ b/lib/setModel.js @@ -40,11 +40,11 @@ async function compileModel(path) { Object.assign(def, info) } - const _entities_in = (srv, compiled) => { - const exposed = [] - const entities = Object.keys(compiled.definitions).filter(name => name.startsWith(srv.name + '.')) - for (let each of entities) { - const e = compiled.definitions[each] + const _entities_in = service => { + const exposed = [], + { entities } = service + for (let each in entities) { + const e = entities[each] if (e['@cds.autoexposed'] && !e['@cds.autoexpose']) continue if (/DraftAdministrativeData$/.test(e.name)) continue if (/[._]texts$/.test(e.name)) continue @@ -57,20 +57,20 @@ async function compileModel(path) { compiled.services .flatMap(srv => srv.endpoints.map(endpoint => ({ srv, endpoint }))) .map(({ srv, endpoint }) => { - const entities = _entities_in(srv, compiled) + const entities = _entities_in(srv) for (const e of entities) { - const eRelName = e.slice(srv.name.length + 1) - const path = endpoint.path + eRelName.replace(/\./g, '_') - const def = compiled.definitions[e] + const path = endpoint.path + e.replace(/\./g, '_') + const def = compiled.definitions[srv.name + '.' + e] def.endpoints ??= [] def.endpoints.push({ kind: endpoint.kind, path }) // Add fully qualified entity names to each service as 'exposedEntities' - for (const service of compiled.services) { - service.exposedEntities = _entities_in(service, compiled) - } } }) + for (const service of compiled.services) { + service.exposedEntities = _entities_in(service, compiled) + } + const endTime = Date.now() const compileDuration = endTime - startTime