hana-cli
Version:
HANA Developer Command Line Interface
707 lines (652 loc) • 21.3 kB
JavaScript
import * as base from '../utils/base.js'
import * as dbInspect from '../utils/dbInspect.js'
//import * as conn from '../utils/connections.js'
// @ts-ignore
import cds from '@sap/cds'
global.__xRef = []
export const command = 'cds [schema] [table]'
export const aliases = ['cdsPreview']
export const describe = base.bundle.getText("cds")
export const builder = base.getBuilder({
table: {
alias: ['t', 'Table'],
type: 'string',
desc: base.bundle.getText("table")
},
schema: {
alias: ['s', 'Schema'],
type: 'string',
default: '**CURRENT_SCHEMA**',
desc: base.bundle.getText("schema")
},
view: {
alias: ['v', 'View'],
type: 'boolean',
default: false,
desc: base.bundle.getText("viewOpt")
},
useHanaTypes: {
alias: ['hana'],
type: 'boolean',
default: false,
desc: base.bundle.getText("useHanaTypes")
},
useQuoted: {
alias: ['q', 'quoted', 'quotedIdentifiers'],
desc: base.bundle.getText("gui.useQuoted"),
type: 'boolean',
default: false
},
port: {
alias: ['p'],
type: 'number',
default: false,
desc: base.bundle.getText("port")
}
})
export async function handler(argv) {
base.promptHandler(argv, cdsBuild, {
table: {
description: base.bundle.getText("table"),
type: 'string',
required: true,
ask: () => {
return false
}
},
schema: {
description: base.bundle.getText("schema"),
type: 'string',
required: true
},
view: {
description: base.bundle.getText("viewOpt"),
type: 'boolean',
required: true,
ask: () => {
return false
}
},
useHanaTypes: {
description: base.bundle.getText("useHanaTypes"),
type: 'boolean'
},
useQuoted: {
description: base.bundle.getText("gui.useQuoted"),
type: 'boolean'
},
port: {
description: base.bundle.getText("port"),
required: false,
ask: () => {
return false
}
}
})
}
export async function cdsBuild(prompts) {
base.debug('cds')
try {
base.setPrompts(prompts)
const db = await base.createDBConnection()
let schema = await base.dbClass.schemaCalc(prompts, db)
let object, fields, constraints, cdsSource
dbInspect.options.useHanaTypes = prompts.useHanaTypes
dbInspect.options.useQuoted = prompts.useQuoted
dbInspect.options.noColons = true
if (!prompts.view) {
object = await dbInspect.getTable(db, schema, prompts.table)
fields = await dbInspect.getTableFields(db, object[0].TABLE_OID)
constraints = await dbInspect.getConstraints(db, object)
} else {
object = await dbInspect.getView(db, schema, prompts.table)
fields = await dbInspect.getViewFields(db, object[0].VIEW_OID)
}
cdsSource =
//
`
@protocol: ['odata-v4', 'graphql']
service HanaCli { `
if (process.env.VCAP_SERVICES) {
let vcap = JSON.parse(process.env.VCAP_SERVICES)
vcap.hana[0].credentials.schema = object[0].SCHEMA_NAME
vcap.hana.splice(1, 100)
process.env.VCAP_SERVICES = JSON.stringify(vcap)
}
if (!cds.env.requires.db.credentials) {
const settings = db.client._settings
let out = {}
out.schema = object[0].SCHEMA_NAME
out.url = settings.url
out.certificate = settings.certificate
out.database_id = settings.database_id
out.driver = settings.driver
out.hdi_user = settings.hdi_user
out.hdi_password = settings.hdi_password
out.host = settings.host
out.user = settings.user
out.password = settings.password
out.port = settings.port
out.pooling = settings.pooling
out.encrypt = true
out.sslValidateCertificate = false
out.useTLS = true
cds.env.requires.db.credentials = out
}
cdsSource +=
`@(Capabilities: {
InsertRestrictions: {Insertable: true},
UpdateRestrictions: {Updatable: true},
DeleteRestrictions: {Deletable: true}
},
HeaderInfo: {
TypeName: '${prompts.table}',
Title:'${prompts.table}'
},
UI: {
LineItem: [ \n`;
for (let field of fields) {
if (prompts.useQuoted) {
cdsSource += `{$Type: 'UI.DataField', Value: ![${field.COLUMN_NAME}], ![@UI.Importance]:#High}, \n`
} else {
cdsSource += `{$Type: 'UI.DataField', Value: ${field.COLUMN_NAME}, ![@UI.Importance]:#High}, \n`
}
}
cdsSource +=
`], \n`
cdsSource +=
` Facets: [
{$Type: 'UI.ReferenceFacet', Target: '@UI.FieldGroup#Main', ![@UI.Importance]: #High}
],
FieldGroup#Main: { \n
Data: [ \n`
for (let field of fields) {
cdsSource += `{$Type: 'UI.DataField', Value: ${field.COLUMN_NAME}, ![@UI.Importance]:#High}, \n`
}
cdsSource +=
`] },\n`
cdsSource += ` SelectionFields: [ `;
for (let field of fields) {
cdsSource += `${field.COLUMN_NAME}, \n`
}
cdsSource +=
`] \n`
cdsSource += `} )\n`
if (!prompts.view) {
console.log(`Schema: ${schema}, Table: ${prompts.table}`)
object = await dbInspect.getTable(db, schema, prompts.table)
fields = await dbInspect.getTableFields(db, object[0].TABLE_OID)
constraints = await dbInspect.getConstraints(db, object)
let tableSource = await dbInspect.formatCDS(db, object, fields, constraints, "table", "preview")
cdsSource +=
`@cds.persistence.skip\n ${tableSource} \n }`
} else {
console.log(`Schema: ${schema}, View: ${prompts.table}`)
object = await dbInspect.getView(db, schema, prompts.table)
fields = await dbInspect.getViewFields(db, object[0].VIEW_OID)
let viewSource = await dbInspect.formatCDS(db, object, fields, null, "view", "preview")
cdsSource +=
`${viewSource} \n }`
}
await cdsServerSetup(prompts, cdsSource)
return base.end()
} catch (error) {
base.error(error)
}
}
async function cdsServerSetup(prompts, cdsSource) {
base.debug('cdsServerSetup')
const { default: Server } = await import('http')
const port = process.env.PORT || prompts.port || 3010
if (!(/^[1-9]\d*$/.test(port) && 1 <= 1 * port && 1 * port <= 65535)) {
return base.error(`${port} ${base.bundle.getText("errPort")}`)
}
const server = Server.createServer()
const express = base.require('express')
var app = express()
//CDS OData Service
let vcap = ''
let options = {
"db": {
kind: "hana",
logLevel: "error"
}
}
if (process.env.VCAP_SERVICES) {
vcap = JSON.parse(process.env.VCAP_SERVICES)
// @ts-ignore
options.db.credentials = vcap.hana[0].credentials
}
// @ts-ignore
cds.connect()//options)
let odataURL = "/odata/v4/opensap.hana.CatalogService/"
let entity = prompts.table
base.debug(`Entity Before ${entity}`)
entity = entity.replace(/\./g, "_")
entity = entity.replace(/::/g, "_")
let graphQLEntity = entity.replace(/_/g, ".")
base.debug(`GraphQL Entity After ${graphQLEntity}`)
// entity = entity.replace(/:/g, "")
base.debug(cdsSource)
// @ts-ignore
cds.serve('all').from(await cds.parse(cdsSource), {
crashOnError: false
})
.at(odataURL)
.in(app)
//.to('fiori')
.with(srv => {
// @ts-ignore
srv.on(['READ'], [entity, `HanaCli.${graphQLEntity}`], async (req) => {
base.debug(`In Read Exit ${prompts.table}`)
let query1 = await cds.parse.cql(`SELECT from ${prompts.table}`)
// @ts-ignore
base.debug(req.query)
// @ts-ignore
query1.SELECT.one = req.query.SELECT.one
// @ts-ignore
req.query.SELECT.from = query1.SELECT.from
// query1.SELECT = req.query.SELECT
// @ts-ignore
query1.SELECT.limit = req.query.SELECT.limit
//query1.SELECT.search = req.query.SELECT.search
// @ts-ignore
query1.SELECT.where = req.query.SELECT.where
//query1.SELECT.count = req.query.SELECT.count
// @ts-ignore
query1.SELECT.orderBy = req.query.SELECT.orderBy
//query1.SELECT.columns = req.query.SELECT.columns
req.query = query1
//let query = "SELECT "
/* if (req.query.SELECT.count === true){
// @ts-ignore
const db = new base.dbClass(await conn.createConnection(prompts))
query += `COUNT(*) AS "counted" FROM "${prompts.table}"`
base.debug(query)
let count = await db.execSQL(query)
base.debug(count)
return (count)
} */
// @ts-ignore
base.debug(JSON.stringify(req.query))
// @ts-ignore
if (req.query.SELECT.columns) { //&& req.query.SELECT.columns[0].func) {
// @ts-ignore
for (let column of req.query.SELECT.columns) {
for (let xref of global.__xRef) {
if (column.ref[0] === xref.after) {
column.ref[0] = xref.after
}
}
}
}
//Req Parameters for Single Record GET
if (req.params) {
if (req.params.length > 0) {
// @ts-ignore
const { SELECT } = req.query
SELECT.where = []
// for (let param of req.params) {
// @ts-ignore
// for (let property in param) {
SELECT.where.push({ "ref": ["ID"] })
SELECT.where.push("=")
SELECT.where.push({ "val": req.params[0] })
// SELECT.where.push("and")
// }
// }
// SELECT.where.splice(-1, 1)
}
}
//Where
// @ts-ignore
if (req.query.SELECT.where) {
// @ts-ignore
for (let where of req.query.SELECT.where) {
if (where.ref) {
for (let xref of global.__xRef) {
if (where.ref[0] === xref.after) {
where.ref[0] = xref.after
}
}
}
}
}
//Order By
// @ts-ignore
if (req.query.SELECT.orderBy) {
//query += ` ORDER BY `
// @ts-ignore
for (let orderBy of req.query.SELECT.orderBy) {
for (let xref of global.__xRef) {
if (orderBy.ref[0] === xref.after) {
orderBy.ref[0] = xref.after
}
}
}
}
base.debug(req.query)
req.reply(await cds.tx(req).run(req.query))
})
})
.catch((err) => {
console.log(err)
process.exit(1)
})
//Swagger UI
Object.defineProperty(cds.compile.to, 'openapi', { configurable: true, get: () => base.require('@sap/cds-dk/lib/compile/openapi') })
try {
// @ts-ignore
let metadata = await cds.compile.to.openapi(cds.parse(cdsSource), {
service: 'HanaCli',
servicePath: '/odata/v4/opensap.hana.CatalogService/',
'openapi:url': '/odata/v4/opensap.hana.CatalogService/',
'openapi:diagram': true,
to: 'openapi'
})
let serveOptions = {
explorer: true
}
const swaggerUi = await import('swagger-ui-express')
app.use('/api/api-docs', swaggerUi.serve, swaggerUi.setup(metadata, serveOptions))
app.get('/', (_, res) => res.send(getIndex(odataURL, entity)))
app.get('/fiori.html', (_, res) => {
const manifest = _manifest(odataURL, entity, prompts.table)
res.send(fiori(manifest, odataURL, entity))
})
app.get('/app/Component.js', (_, res) => {
const manifest = _manifest(odataURL, entity, prompts.table)
const content = `sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) {
"use strict";
return AppComponent.extend("preview.Component", {
metadata: { manifest: ${JSON.stringify(manifest, null, 2)} }
});
});`
res.send(content)
})
//Start the Server
server.on("request", app)
server.listen(port, async () => {
// @ts-ignore
let serverAddr = `http://localhost:${server.address().port}`
console.info(`HTTP Server: ${serverAddr}`)
//GraphQL
/* const GraphQLAdapter = base.require('@cap-js/graphql/lib/GraphQLAdapter') //require('@sap/cds-graphql/lib')
const adapter = new GraphQLAdapter(cds.services, { graphiql: true, path: '/graphql' })
app.use('/graphql', adapter) */
// app.use(new GraphQLAdapter(cds.services, { graphiql: true }))
// console.log("serving GraphQL endpoint for all services { at: '/graphql' }")
const { default: open } = await import('open')
open(serverAddr)
})
}
catch (e) {
if (e.code !== 'MODULE_NOT_FOUND') {
// Re-throw not "Module not found" errors
throw e
}
throw base.bundle.getText("cds-dk")
}
return
}
export function getIndex(odataURL, entity) {
base.debug('getIndex')
return `
<html>
<head>
<meta name="color-scheme" content="dark light">
<style>
body {
font-family: Avenir Next, sans-serif;
margin: 44px;
line-height: 1.5em;
}
h1 {
margin-bottom: 0
}
h1 + .subtitle {
margin: .2em;
font-weight: 300;
}
h1, h2, h3 {
font-weight: 400;
}
h1, a {
text-decoration: none;
}
a.preview {
font-size: 90%;
}
footer {
border-top: .5px solid;
margin-top: 44px;
padding-top: 22px;
width: 400px;
font-size: 90%;
}
@media (prefers-color-scheme: dark) {
body {
background:#001119;
color: #789;
}
h1 + .subtitle {
color:#fb0;
}
h1, a {
color:#fb0;
}
h2, h3 {
color:#89a;
}
a.preview {
color: #678;
}
footer {
border-top: .5px solid #456;
color: #567;
}
}
</style>
</head>
<body>
<h1>${base.bundle.getText("cdsIndex")}</h1>
<p class="subtitle"> These are the paths currently served ...
<h2> Web Applications: </h2>
<h3><a href="/fiori.html">Fiori Test UI</a></h3>
<h3><a href="/api/api-docs/">Swagger UI</a></h3>
<h2> Service Endpoints: </h2>
<h3>
<a href="${odataURL}">${odataURL}</a> /
<a href="${odataURL}$metadata">$metadata</a>
</h3>
<ul>
<li>
<a href="/odata/v4/opensap.hana.CatalogService/${entity}">${entity}</a>
</li>
</ul>
</body>
</html>`
}
export function _manifest(odataURL, entity, table) {
base.debug(`_manifest ${odataURL} ${entity} ${table}`)
//const serviceProv = odataURL
const serviceInfo = entity
const manifest = {
_version: '1.8.0',
'sap.app': {
id: 'preview',
type: 'application',
title: `Preview of ${table}`,
description: 'Preview Application',
dataSources: {
mainService: {
uri: `${odataURL}`,
type: 'OData',
settings: {
odataVersion: '4.0'
}
}
},
},
'sap.ui5': {
dependencies: {
libs: {
'sap.fe.templates': {}
}
},
models: {
'': {
dataSource: 'mainService',
//preload: true,
settings: {
synchronizationMode: 'None',
operationMode: 'Server',
autoExpandSelect: true,
earlyRequests: true,
groupProperties: {
default: {
submit: 'Auto'
}
}
}
}
},
routing: {
routes: [
{
name: `${entity}ListRoute`,
target: `${entity}ListTarget`,
pattern: ':?query:',
},
{
name: `${entity}DetailsRoute`,
target: `${entity}DetailsTarget`,
pattern: `${entity}({key}):?query:`,
}
],
targets: {
[`${entity}ListTarget`]: {
type: 'Component',
id: `${entity}ListTarget`,
name: 'sap.fe.templates.ListReport',
options: {
settings: {
entitySet: `${entity}`,
initialLoad: true,
navigation: {
[`${entity}`]: {
detail: {
route: `${entity}DetailsRoute`
}
}
}
}
}
},
[`${entity}DetailsTarget`]: {
type: 'Component',
id: `${entity}DetailsTarget`,
name: 'sap.fe.templates.ObjectPage',
options: {
settings: {
entitySet: `${entity}`,
navigation: {}
}
}
}
}
},
},
contentDensities: {
compact: true,
cozy: true
},
'sap.ui': {
technology: 'UI5',
fullWidth: true
},
'sap.fiori': {
registrationIds: [],
archeType: 'transactional'
},
}
const { routing } = manifest['sap.ui5']
for (const { navProperty, targetEntity } of serviceInfo) {
// add a route for the navigation property
routing.routes.push(
{
name: `${navProperty}Route`,
target: `${navProperty}Target`,
pattern: `${entity}({key})/${navProperty}({key2}):?query:`,
}
)
// add a route target leading to the target entity
routing.targets[`${navProperty}Target`] = {
type: 'Component',
id: `${navProperty}Target`,
name: 'sap.fe.templates.ObjectPage',
options: {
// @ts-ignore
settings: {
entitySet: targetEntity
}
}
}
// wire the new route from the source entity's navigation (see above)
routing.targets[`${entity}DetailsTarget`].options.settings.navigation[navProperty] = {
detail: {
route: `${navProperty}Route`
}
}
}
return manifest
}
export function fiori(manifest, odataURL, entity,) {
base.debug(`fiori ${odataURL} ${entity}`)
// @ts-ignore
let ui5Version = cds.env.preview && cds.env.preview.ui5 && cds.env.preview.ui5.version
ui5Version = ui5Version ? ui5Version + '/' : ''
base.debug(`SAPUI5 Version ${ui5Version}`)
return `
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${manifest['sap.app'].title}</title>
<script>
window["sap-ushell-config"] = {
defaultRenderer: "fiori2",
applications: {
"hanacli-preview": {
title: "Browse ${entity}",
description: "from ${odataURL}",
additionalInformation: "SAPUI5.Component=app",
applicationType : "URL",
url: "./app",
navigationMode: "embedded"
}
}
}
</script>
<script id="sap-ushell-bootstrap" src="https://ui5.sap.com/${ui5Version}test-resources/sap/ushell/bootstrap/sandbox.js"></script>
<script id="sap-ui-bootstrap" src="https://ui5.sap.com/${ui5Version}resources/sap-ui-core.js"
data-sap-ui-libs="sap.m, sap.ushell, sap.collaboration, sap.ui.layout" data-sap-ui-compatVersion="edge"
data-sap-ui-theme="sap_horizon" data-sap-ui-frameOptions="allow"></script>
<script src="https://ui5.sap.com/${ui5Version}test-resources/sap/ushell/bootstrap/standalone.js"></script>
<script>
// load and register Fiori2 icon font
jQuery.sap.require("sap.ushell.iconfonts");
jQuery.sap.require("sap.ushell.services.AppConfiguration");
sap.ushell.iconfonts.registerFiori2IconFont();
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
sap.ui.getCore().applyTheme( "sap_horizon_dark" )
}
sap.ui.getCore().attachInit(function() { sap.ushell.Container.createRenderer().placeAt("content") })
</script>
</head>
<body class="sapUiBody sapUShellFullHeight" id="content"></body>
</html>
`
}