diff --git a/.gitmodules b/.gitmodules index ad1151c..d3b4737 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,18 +4,18 @@ [submodule "bookstore"] path = bookstore url = https://github.com/capire/bookstore -[submodule "reviews"] - path = reviews - url = https://github.com/capire/reviews +[submodule "common"] + path = common + url = https://github.com/capire/common [submodule "orders"] path = orders url = https://github.com/capire/orders +[submodule "reviews"] + path = reviews + url = https://github.com/capire/reviews [submodule "shared-db"] path = shared-db url = https://github.com/capire/shared-db -[submodule "common"] - path = common - url = https://github.com/capire/common -[submodule "data-viewer"] +[submodule "inspectr"] path = inspectr url = https://github.com/capire/data-viewer diff --git a/inspectr b/inspectr new file mode 160000 index 0000000..b1a24b5 --- /dev/null +++ b/inspectr @@ -0,0 +1 @@ +Subproject commit b1a24b509ed74e62236f8c4d1b0d685f5662c076 diff --git a/inspectr/.gitignore b/inspectr/.gitignore deleted file mode 100644 index e43b0f9..0000000 --- a/inspectr/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.DS_Store diff --git a/inspectr/.reuse/dep5 b/inspectr/.reuse/dep5 deleted file mode 100644 index dc3640d..0000000 --- a/inspectr/.reuse/dep5 +++ /dev/null @@ -1,29 +0,0 @@ -Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: cloud-cap-samples -Upstream-Contact: -Source: https://github.com/SAP-samples/cloud-cap-samples -Disclaimer: The code in this project may include calls to APIs (“API Calls”) of - SAP or third-party products or services developed outside of this project - (“External Products”). - “APIs” means application programming interfaces, as well as their respective - specifications and implementing code that allows software to communicate with - other software. - API Calls to External Products are not licensed under the open source license - that governs this project. The use of such API Calls and related External - Products are subject to applicable additional agreements with the relevant - provider of the External Products. In no event shall the open source license - that governs this project grant any rights in or to any External Products,or - alter, expand or supersede any terms of the applicable additional agreements. - If you have a valid license agreement with SAP for the use of a particular SAP - External Product, then you may make use of any API Calls included in this - project’s code for that SAP External Product, subject to the terms of such - license agreement. If you do not have a valid license agreement for the use of - a particular SAP External Product, then you may only make use of any API Calls - in this project for that SAP External Product for your internal, non-productive - and non-commercial test and evaluation of such API Calls. Nothing herein grants - you any rights to use or access any SAP External Product, or provide any third - parties the right to use of access any SAP External Product, through API Calls. - -Files: *.* -Copyright: 2019-2025 SAP SE or an SAP affiliate company and cap-cloud-samples -License: Apache-2.0 diff --git a/inspectr/app/viewer/app.js b/inspectr/app/viewer/app.js deleted file mode 100644 index 561a61a..0000000 --- a/inspectr/app/viewer/app.js +++ /dev/null @@ -1,119 +0,0 @@ -/* global Vue axios */ //> from vue.html -const GET = (url) => axios.get('/odata/v4/-data'+url) -const storageGet = (key, def) => localStorage.getItem('data-viewer:'+key) || def -const storageSet = (key, val) => localStorage.setItem('data-viewer:'+key, val) -const columnKeysFirst = (c1, c2) => { - if (c1.isKey && !c2.isKey) return -1 - if (!c1.isKey && c2.isKey) return 1 - if (c1.isKey && c2.isKey) return c1.name.localeCompare(c2.name) - return 0 // retain natural order of normal columns -} - -const vue = Vue.createApp ({ - - data() { return { - error: undefined, - dataSource: storageGet('data-source', 'db'), - skip: storageGet('skip', 0), - top: storageGet('top', 20), - entity: storageGet('entity') ? JSON.parse(storageGet('entity')) : undefined, - entities: [], - columns: [], - data: [], - rowDetails: {}, - rowKey: storageGet('rowKey') - }}, - - watch: { - dataSource: (v) => { storageSet('data-source', v); vue.fetchEntities() }, - skip: (v) => { storageSet('skip', v); if (vue.entity) vue.fetchData() }, - top: (v) => { storageSet('top', v); if (vue.entity) vue.fetchData() }, - }, - - methods: { - - async fetchEntities () { - let url = `/Entities` - if (vue.dataSource === 'db') url += `?dataSource=db` - const {data} = await GET(url) - vue.entities = data.value - vue.entities.forEach(entity => entity.columns.sort(columnKeysFirst)) - const entity = vue.entity && vue.entities.find(e => e.name === vue.entity.name) - if (entity) { // restore selection from previous fetch - vue.columns = entity.columns - await vue.fetchData(entity) - } else { - vue.entity = undefined - vue.columns = [] - vue.data = [] - vue.rowDetails = {} - } - }, - - async inspectEntity (eve) { - const entity = vue.entity = vue.entities [eve.currentTarget.rowIndex-1] - storageSet('entity', JSON.stringify(entity)) - vue.columns = vue.entities.find(e => e.name === entity.name).columns - return await this.fetchData() - }, - - async fetchData () { - let url = `/Data?entity=${vue.entity.name}&$skip=${vue.skip}&$top=${vue.top}` - if (vue.dataSource === 'db') url += `&dataSource=db` - - try { - const {data} = await GET(url) - // sort data along column order - const columnIndexes = {} - vue.columns.forEach((col, i) => columnIndexes[col.name] = i) - vue.data = data.value.map(d => d.record - .sort((r1, r2) => columnIndexes[r1.column] - columnIndexes[r2.column]) - .map(r => r.data) - ) - const row = vue.data.find(data => vue._makeRowKey(data) === vue.rowKey) - if (row) vue._setRowDetails(row) - else vue.rowDetails = {} - vue.error = undefined - } catch (err) { - vue.data = [] - vue.rowDetails = {} - if (err.response?.data?.error) { - vue.error = err.response.data.error - } else { - vue.error = { code:err.code, message:err.message } - } - } - - }, - - inspectRow (eve) { - vue.rowDetails = {} - const selectedRow = eve.currentTarget.rowIndex-1 - vue.rowKey = vue._makeRowKey(vue.data[selectedRow]) - storageSet('rowKey', vue.rowKey) - vue._setRowDetails(vue.data[selectedRow]) - }, - - _setRowDetails(row) { - vue.rowDetails = {} - row.forEach((line, colIndex) => { - vue.rowDetails[vue.columns[colIndex].name] = line - }) - }, - - _makeRowKey(row) { - // to identify a row, build a key string out of all key columns' values - return row - .filter((_, colIndex) => vue.columns[colIndex] && vue.columns[colIndex].isKey) - .reduce(((prev, next) => prev += next), '') - }, - - isActiveRow(row) { - return vue._makeRowKey(row) === vue.rowKey - } - - } -}) -.mount('#app') - -vue.fetchEntities() diff --git a/inspectr/app/viewer/index.html b/inspectr/app/viewer/index.html deleted file mode 100644 index 9fd723a..0000000 --- a/inspectr/app/viewer/index.html +++ /dev/null @@ -1,95 +0,0 @@ - - - - - Data Browser - - - - - - - - - -
- -

Data Browser – {{ entity ? entity.name : '' }}

- -
- - -
-
- - - - -
-
- - - - - - - -
{{ col.name }}
{{ d }}
-
-
- Error: {{ error.code ? error.code + ' – ' + error.message : error.message }} -
-

-
- - - - - -
{{ value }}{{ key }}
-
-
-
- -
- - - diff --git a/inspectr/cds-plugin.js b/inspectr/cds-plugin.js deleted file mode 100644 index 732e001..0000000 --- a/inspectr/cds-plugin.js +++ /dev/null @@ -1,4 +0,0 @@ -const cds = require("@sap/cds") -cds.on ('served', ()=> { - cds.app.serve ('/data') .from ('@capire/data-viewer','app/viewer') -}) diff --git a/inspectr/index.cds b/inspectr/index.cds deleted file mode 100644 index d16b292..0000000 --- a/inspectr/index.cds +++ /dev/null @@ -1 +0,0 @@ -using from './srv/data-service'; \ No newline at end of file diff --git a/inspectr/package.json b/inspectr/package.json deleted file mode 100644 index e27369a..0000000 --- a/inspectr/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "@capire/data-viewer", - "version": "0.1.0", - "description": "A generic browser for data", - "dependencies": { - "@sap/cds": ">=5.0.4" - }, - "files": [ - "app", - "srv", - "index.cds" - ] -} diff --git a/inspectr/srv/data-service.cds b/inspectr/srv/data-service.cds deleted file mode 100644 index 606afea..0000000 --- a/inspectr/srv/data-service.cds +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Exposes data + entity metadata - */ -@requires:'authenticated-user' -@odata service DataService @( path:'-data' ) { - - /** - * Metadata like name and columns/elements - */ - entity Entities @cds.persistence.skip { - key name : String; - columns: Composition of many { - name : String; - type : String; - isKey: Boolean; - } - } - - /** - * The actual data, organized by column name - */ - entity Data @cds.persistence.skip { - key ID : String; // to be OData-compliant - record : array of { - column : String; - data : String; - } - } - -} diff --git a/inspectr/srv/data-service.js b/inspectr/srv/data-service.js deleted file mode 100644 index e0a5c77..0000000 --- a/inspectr/srv/data-service.js +++ /dev/null @@ -1,63 +0,0 @@ -const cds = require('@sap/cds') -const log = cds.log('data') - -class DataService extends cds.ApplicationService { init(){ - - this.on ('READ', 'Entities', req => { - const { dataSource } = req.req.query - const srvPrefixes = cds.db.model.all('service').map(srv => srv.name+'.') - const dataSourceFilter = dataSource === 'db' - ? e => e['@cds.persistence.skip'] !== true // for DB, excl. entities w/o persistence - : e => !!srvPrefixes.find(srvName => e.name.startsWith(srvName)) // only entities reachable from a service - - return cds.db.model.all('entity') - .filter (e => req.data && req.data.name ? e.name === req.data.name : true) // honor name filter from request, if any - .filter (e => !e.name.startsWith('DRAFT.')) // exclude synthetic stuff - .filter (e => !e.name.startsWith('DataService.')) // exclude this service - .filter (dataSourceFilter) - .sort((e1, e2) => e1.name.localeCompare(e2.name)) - .map(e => { - const columns = Object.entries(e.elements) - .filter(([,el]) => !(el instanceof cds.Association)) // exclude assocs+compositions - .map(([name, el]) => { return { name, type: el.type, isKey:!!el.key }}) - return { name: e.name, columns } - }) - }) - - this.on ('READ', 'Data', async req => { - const { entity: entityName, dataSource: dataSourceName } = req.req.query - if (!entityName) return req.reject(400, `Must provide 'entity' query`) - const entity = cds.db.model.definitions[entityName] - if (!entity) return req.reject(404, 'No such entity: ' + entityName) - - const query = SELECT.from(entity) - query.SELECT.limit = req.query.SELECT.limit // forward $skip / $top - - const dataSource = findDataSource(dataSourceName, entityName) - let res = await dataSource.run(query) - if (!Array.isArray(res)) res = [res] // singleton result - return res.map((line) => { - const record = Object.entries(line).map(([column, data]) => ({ column, data })) - return { - record, - ID: cds.utils.uuid() // just to be OData-compliant - } - }) - }) - - return super.init() -}} - -module.exports = { DataService } - -/** @returns {cds.Service} */ -function findDataSource(dataSourceName, entityName) { - for (let srv of Object.values(cds.services)) { // all connected services - if (!srv.name) continue // FIXME intermediate/pending in cds.services ? - if (dataSourceName === srv.name || entityName.startsWith(srv.name+'.')) { - log._debug && log.debug(`using ${srv.name} as data source`) - return srv - } - } - return cds.services.db // fallback -}