(AppController);
13 | });
14 |
15 | describe('root', () => {
16 | it('should return "Hello World!"', () => {
17 | expect(appController.getHello()).toBe('Hello World!');
18 | });
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/samples/cds-sample-application/src/app.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get } from '@nestjs/common';
2 |
3 | @Controller()
4 | export class AppController {
5 | constructor() {}
6 |
7 | @Get()
8 | getHello(): string {
9 | return 'Hello World!';
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/samples/cds-sample-application/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { AppController } from './app.controller';
3 |
4 | @Module({
5 | controllers: [AppController]
6 | })
7 | export class AppModule {}
8 |
--------------------------------------------------------------------------------
/samples/cds-sample-application/src/business-partner/business-partner-service-handler.ts:
--------------------------------------------------------------------------------
1 | import { cloudBusinessPartnerService as businessPartnerService } from '../generated/cloud-business-partner-service';
2 | const { businessPartnerApi } = businessPartnerService();
3 |
4 | /**
5 | * Service implementation for the cds service defined in /srv/business-partner-service.cds
6 | * Annotation @impl is used in service definition file (.cds) to specify alternative paths (relative to dist/) to load implementations from
7 | * Note: The name of service handler should match the cds service name
8 | */
9 | export const BupaService = (srv) => {
10 | srv.on('getByKey', async (oRequest) => {
11 | const param = oRequest.data.param;
12 | const partner = await businessPartnerApi
13 | .requestBuilder()
14 | .getByKey(param)
15 | .execute({ destinationName: 'myDestinationName' });
16 | oRequest.reply(partner);
17 | });
18 |
19 | srv.on('getAll', async (oRequest) => {
20 | const partners = await businessPartnerApi
21 | .requestBuilder()
22 | .getAll()
23 | .top(5)
24 | .execute({ destinationName: 'myDestinationName' });
25 | oRequest.reply(partners);
26 | });
27 | };
28 |
--------------------------------------------------------------------------------
/samples/cds-sample-application/src/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { AppModule } from './app.module';
3 | import cds = require('@sap/cds');
4 |
5 | async function bootstrap() {
6 | const app = await NestFactory.create(AppModule);
7 | await cds.connect.to('db');
8 | await cds
9 | .serve('all')
10 | .in(app);
11 | await app.listen(process.env.PORT || 3000);
12 | }
13 | bootstrap();
14 |
--------------------------------------------------------------------------------
/samples/cds-sample-application/srv/business-partner-service.cds:
--------------------------------------------------------------------------------
1 | using { bupa as my } from '../db/data-model';
2 |
3 | @impl:'dist/business-partner/business-partner-service-handler'
4 | service BupaService {
5 | entity CapBusinessPartner @readonly as projection on my.CapBusinessPartner;
6 |
7 | function getByKey(param : String) returns CapBusinessPartner;
8 | function getAll() returns array of CapBusinessPartner;
9 | }
--------------------------------------------------------------------------------
/samples/cds-sample-application/test/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { INestApplication } from '@nestjs/common';
3 | import * as request from 'supertest';
4 | import { AppModule } from './../src/app.module';
5 | import { setTestDestination } from '@sap-cloud-sdk/test-util';
6 | import nock = require('nock');
7 | import cds = require('@sap/cds');
8 |
9 | describe('AppController (e2e)', () => {
10 | let app: INestApplication;
11 |
12 | beforeAll(async () => {
13 | const moduleFixture: TestingModule = await Test.createTestingModule({
14 | imports: [AppModule],
15 | }).compile();
16 |
17 | app = moduleFixture.createNestApplication();
18 | await cds.connect.to('db');
19 | await cds
20 | .serve('all')
21 | .in(app);
22 | await app.init();
23 |
24 | setTestDestination({
25 | name: 'myDestinationName',
26 | url: 'https://my-destination-url.com',
27 | isTestDestination: true
28 | });
29 | });
30 |
31 | it('tests service handler implementation', () => {
32 | nock('https://my-destination-url.com')
33 | .get(/.*/)
34 | .reply(200);
35 |
36 | return request(app.getHttpServer())
37 | .get('/bupa/getAll()')
38 | .expect(200)
39 | .expect({"@odata.context":"$metadata#CapBusinessPartner","value":[]});
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/samples/cds-sample-application/test/jest-e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "moduleFileExtensions": ["js", "json", "ts"],
3 | "rootDir": ".",
4 | "testEnvironment": "node",
5 | "testRegex": ".e2e-spec.ts$",
6 | "transform": {
7 | "^.+\\.(t|j)s$": "ts-jest"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/samples/cds-sample-application/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": [
4 | "node_modules",
5 | "test",
6 | "dist",
7 | "**/*spec.ts"
8 | ]
9 | }
--------------------------------------------------------------------------------
/samples/cds-sample-application/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "allowSyntheticDefaultImports": true,
9 | "target": "es2017",
10 | "sourceMap": true,
11 | "outDir": "./dist",
12 | "baseUrl": "./",
13 | "incremental": true
14 | }
15 | }
--------------------------------------------------------------------------------
/samples/cf-multi-tenant-application/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/prettierrc",
3 | "singleQuote": true,
4 | "filepath": "*.ts",
5 | "trailingComma": "none",
6 | "arrowParens": "avoid"
7 | }
8 |
--------------------------------------------------------------------------------
/samples/cf-multi-tenant-application/README.md:
--------------------------------------------------------------------------------
1 | # SAP Cloud SDK for JavaScript Multi-Tenant Sample Application
2 |
3 | ## Description
4 |
5 | The code sample in this directory is a reference for the [multi-tenant application tutorial](https://sap.github.io/cloud-sdk/docs/js/tutorials/multi-tenant-application).
6 | You need to make adjustments to the code before deployment.
7 | The terms you need to replace are given in all caps and start with _`YOUR_`_ e.g. _`YOUR_REGION`_.
8 |
9 | Also, note that the samples here are intended as a didactic example and are not necessarily a best practice.
10 | The repositories' structure is as follows:
11 |
12 | - [./multi-tenant-app](./multi-tenant-app) - Contains the code of the multi-tenant application.
13 | It contains a simple service endpoint and logic to be executed on subscription and unsubscription.
14 | - [./approuter](./approuter) - Contains the approuter necessary to attach JSON web tokens to the application.
15 | - [./service-config](./service-config) - Directory configurations for service instances.
16 |
17 | ## Requirements
18 |
19 | The minimal requirements are:
20 |
21 | - A terminal to execute commands
22 | - A recent version of node and npm installed e.g. node 14 and npm 6
23 | - A recent installation of the [Cloud Foundry command line interface](https://developers.sap.com/tutorials/cp-cf-download-cli.html)
24 | - An IDE or a text editor of your choice
25 | - Access to a [SAP Business Technology Platform](https://www.sap.com/products/business-technology-platform.html) account
26 | - Entitlement to use resources like service instance creation and application processing units
27 | - Permission to deploy applications and create service instances
28 |
29 | ## Download and Deployment
30 |
31 | To download the application run:
32 |
33 | ```
34 | git clone \
35 | --depth 1 \
36 | --filter=blob:none \
37 | --sparse \
38 | https://github.com/SAP-samples/cloud-sdk-js.git \
39 | ;
40 | cd cloud-sdk-js
41 | git sparse-checkout set samples/cf-multi-tenant-application
42 | ```
43 |
44 | ### Create Services on SAP BTP Cloud Foundry
45 |
46 | Before you can deploy the application, you have to create a `destination` and `xsuaa` service instance.
47 | Their name should match the one that is used in the application `manifest.yml`, in this case:
48 |
49 | - `destination`
50 | - `xsuaa`
51 |
52 | ### Deploy the Application to SAP BTP Cloud Foundry
53 |
54 | Run `cf push` in the application directory to deploy the application.
55 | Please follow the steps described in the [multi-tenant application tutorial](https://sap.github.io/cloud-sdk/docs/js/tutorials/multi-tenant-application) for the deployment steps of all components.
56 |
--------------------------------------------------------------------------------
/samples/cf-multi-tenant-application/approuter/manifest.yml:
--------------------------------------------------------------------------------
1 | applications:
2 | - name: approuter
3 | routes:
4 | - route: 'route-prefix-YOUR_SUBDOMAIN.cfapps.YOUR_REGION.hana.ondemand.com'
5 | path: .
6 | memory: 128M
7 | buildpacks:
8 | - nodejs_buildpack
9 | env:
10 | TENANT_HOST_PATTERN: 'route-prefix-(.*).cfapps.YOUR_REGION.hana.ondemand.com'
11 | destinations: >
12 | [
13 | {"name":"multi-tenant-app","url":"https://multi-tenant-app.cfapps.YOUR_REGION.hana.ondemand.com","forwardAuthToken":true}
14 | ]
15 | services:
16 | - xsuaa
17 | - destination
18 |
--------------------------------------------------------------------------------
/samples/cf-multi-tenant-application/approuter/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "approuter",
3 | "dependencies": {
4 | "@sap/approuter": "latest"
5 | },
6 | "scripts": {
7 | "start": "node node_modules/@sap/approuter/approuter.js"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/samples/cf-multi-tenant-application/approuter/xs-app.json:
--------------------------------------------------------------------------------
1 | {
2 | "welcomeFile": "index.html",
3 | "routes": [
4 | {
5 | "source": "(.*)",
6 | "target": "/$1",
7 | "destination": "multi-tenant-app"
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/samples/cf-multi-tenant-application/multi-tenant-app/manifest.yml:
--------------------------------------------------------------------------------
1 | applications:
2 | - name: multi-tenant-app
3 | path: .
4 | memory: 256M
5 | buildpacks:
6 | - nodejs_buildpack
7 | services:
8 | - destination
9 | - xsuaa
10 | routes:
11 | - route: 'multi-tenant-app.cfapps.YOUR_REGION.hana.ondemand.com'
12 |
--------------------------------------------------------------------------------
/samples/cf-multi-tenant-application/multi-tenant-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "multi-tenant-app",
3 | "version": "1.0.0",
4 | "description": "Example application using the SAP Cloud SDK for JavaScript.",
5 | "scripts": {
6 | "start": "node -r ts-node/register --inspect src/server.ts",
7 | "debug": "node -r ts-node/register --inspect-brk src/server.ts"
8 | },
9 | "dependencies": {
10 | "express": "^4",
11 | "@sap/xsenv": "^3.3.2",
12 | "cfenv": "^1.2.4",
13 | "@sap-cloud-sdk/util": "^3.0.0",
14 | "@sap-cloud-sdk/connectivity": "^3.0.0",
15 | "@sap-cloud-sdk/http-client": "^3.0.0",
16 | "@types/node": "^22.0.0",
17 | "ts-node": "^10",
18 | "typescript": "~5.7.2"
19 | },
20 | "devDependencies": {},
21 | "engines": {
22 | "node": "^20.0.0",
23 | "npm": "^10.0.0"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/samples/cf-multi-tenant-application/multi-tenant-app/src/application.ts:
--------------------------------------------------------------------------------
1 | import * as bodyParser from 'body-parser';
2 | import express from 'express';
3 | import { serviceRoute } from './service-endpoint';
4 | import { dependencyRoute } from './dependencies-endpoint';
5 | import { subscribeRoute, unsubscribeRoute } from './subscription-endpoint';
6 | import { join } from 'path';
7 | import { readFileSync } from 'fs';
8 |
9 | class App {
10 | public app: express.Application;
11 | public indexPage = readFileSync(join(__dirname, 'index.html'), {
12 | encoding: 'utf-8'
13 | });
14 |
15 | constructor() {
16 | this.app = express();
17 | this.config();
18 | this.routes();
19 | }
20 |
21 | private config(): void {
22 | this.app.use(bodyParser.json());
23 | this.app.use(bodyParser.urlencoded({ extended: false }));
24 | }
25 |
26 | private routes(): void {
27 | const router = express.Router();
28 |
29 | router.get('/service', serviceRoute);
30 | router.get('/dependencies', dependencyRoute);
31 | router.put('/subscription/:subscriberTenantId', subscribeRoute);
32 | router.delete('/subscription/:subscriberTenantId', unsubscribeRoute);
33 | router.get('/index.html', (req, res) => {
34 | res.send(this.indexPage);
35 | });
36 | this.app.use('/', router);
37 | }
38 | }
39 |
40 | export default new App().app;
41 |
--------------------------------------------------------------------------------
/samples/cf-multi-tenant-application/multi-tenant-app/src/dependencies-endpoint.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from 'express';
2 | import * as xsenv from '@sap/xsenv';
3 |
4 | const relevantServices = ['destination'];
5 |
6 | export function dependencyRoute(req: Request, res: Response): void {
7 | res.status(200).send(
8 | relevantServices
9 | .map(s => {
10 | const services = xsenv.filterCFServices({ label: s });
11 |
12 | return services && services.length
13 | ? {
14 | appId:
15 | services[0].credentials.xsappname ||
16 | services[0].credentials.uaa.xsappname,
17 | appName: s
18 | }
19 | : null;
20 | })
21 | .filter(elem => elem)
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/samples/cf-multi-tenant-application/multi-tenant-app/src/index.html:
--------------------------------------------------------------------------------
1 | Multi-tenant sample application
2 |
3 | /service to trigger the sample logic
4 |
--------------------------------------------------------------------------------
/samples/cf-multi-tenant-application/multi-tenant-app/src/server.ts:
--------------------------------------------------------------------------------
1 | import app from './application';
2 |
3 | const port = 8080;
4 |
5 | app.listen(port, () => {
6 | // eslint-disable-next-line no-console
7 | console.log('Express server listening on port ' + port);
8 | });
9 |
--------------------------------------------------------------------------------
/samples/cf-multi-tenant-application/multi-tenant-app/src/service-endpoint.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from 'express';
2 | import {
3 | decodeJwt,
4 | getDestination,
5 | retrieveJwt,
6 | subscriberFirst
7 | } from '@sap-cloud-sdk/connectivity';
8 | import { createLogger } from '@sap-cloud-sdk/util';
9 |
10 | const logger = createLogger('destination');
11 |
12 | export async function serviceRoute(req: Request, res: Response): Promise {
13 | try {
14 | const jwt = retrieveJwt(req);
15 | const tenantText = jwt
16 | ? `You are on tenant: ${decodeJwt(jwt).zid}.`
17 | : `No jwt given in request. Provider tenant used.`;
18 | const destination = await getDestination({
19 | destinationName: 'myDestination',
20 | selectionStrategy: subscriberFirst,
21 | jwt
22 | });
23 | if (destination) {
24 | res.status(200).send(
25 | `${tenantText}.
26 | The destination description is: ${destination.originalProperties.Description}.`
27 | );
28 | } else {
29 | res.status(404).send(`Destination with name 'myDestination' not found.`);
30 | }
31 | } catch (e) {
32 | logger.error(e);
33 | res.status(500).send('Error in retrieving destination - look at the logs.');
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/samples/cf-multi-tenant-application/multi-tenant-app/src/subscription-endpoint.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from 'express';
2 | import { createLogger } from '@sap-cloud-sdk/util';
3 | import {
4 | bindRoute,
5 | createRoute,
6 | deleteRoute,
7 | getCfGuids,
8 | getLandscape
9 | } from './subscription-util';
10 |
11 | const logger = createLogger('subscription');
12 | const appRouterName = 'approuter';
13 |
14 | export async function subscribeRoute(req: Request, res: Response) {
15 | try {
16 | const subscribedSubdomain = req.body.subscribedSubdomain;
17 | const subscriberRoute = `https://route-prefix-${subscribedSubdomain}.${getLandscape()}`;
18 | logger.info(`subscribe: ${subscriberRoute}`);
19 |
20 | const guids = await getCfGuids(appRouterName);
21 | const routeGuid = await createRoute(subscribedSubdomain, guids);
22 | await bindRoute(routeGuid, guids);
23 |
24 | res.status(200).send(subscriberRoute);
25 | } catch (e) {
26 | res.status(500).send(e.message);
27 | }
28 | }
29 |
30 | export async function unsubscribeRoute(req: Request, res: Response) {
31 | const subscribedSubdomain = req.body.subscribedSubdomain;
32 | logger.info(`un-subscribe: ${subscribedSubdomain}`);
33 | await deleteRoute(subscribedSubdomain);
34 | res.status(200).send('Unsubscribed.');
35 | }
36 |
--------------------------------------------------------------------------------
/samples/cf-multi-tenant-application/multi-tenant-app/src/subscription-util.ts:
--------------------------------------------------------------------------------
1 | import { executeHttpRequest } from '@sap-cloud-sdk/http-client';
2 | import * as cfEnv from 'cfenv';
3 | const appRouterName = 'approuter';
4 | const routePrefix = 'route-prefix';
5 | const cfApiDestination = { destinationName: 'cf-api', useCache: true };
6 |
7 | type Guids = {
8 | appGuid: string;
9 | domainGuid: string;
10 | spaceGuid: string;
11 | };
12 |
13 | export async function getCfGuids(appName: string): Promise {
14 | const spaceGuid = cfEnv.getAppEnv().app.space_id;
15 | const orgGuid = cfEnv.getAppEnv().app.organization_id;
16 |
17 | const {
18 | data: {
19 | resources: [{ guid: appGuid }]
20 | }
21 | } = await executeHttpRequest(cfApiDestination, {
22 | method: 'get',
23 | url: `/v3/apps?organization_guids=${orgGuid}&space_guids=${spaceGuid}&names=${appName}`
24 | });
25 |
26 | try {
27 | const {
28 | data: {
29 | resources: [{ guid: domainGuid }]
30 | }
31 | } = await executeHttpRequest(cfApiDestination, {
32 | method: 'get',
33 | url: `/v3/domains?names=${encodeURI(getLandscape())}`
34 | });
35 | return { appGuid, domainGuid, spaceGuid };
36 | } catch (e) {
37 | console.log(e);
38 | }
39 | }
40 |
41 | export function getLandscape(): string {
42 | const result = cfEnv
43 | .getAppEnv()
44 | .app.application_uris[0].split('.')
45 | .slice(1)
46 | .join('.');
47 | return result;
48 | }
49 |
50 | function getRoutePath(subscribedSubdomain: string): string {
51 | return `${routePrefix}-${subscribedSubdomain}`;
52 | }
53 |
54 | export async function bindRoute(routeGuid: string, guids: Guids) {
55 | const bindRouteBody = {
56 | destinations: [
57 | {
58 | app: {
59 | guid: guids.appGuid
60 | }
61 | }
62 | ]
63 | };
64 | return executeHttpRequest(cfApiDestination, {
65 | url: `/v3/routes/${routeGuid}/destinations`,
66 | method: 'post',
67 | data: bindRouteBody
68 | });
69 | }
70 |
71 | export async function createRoute(
72 | subscribedSubdomain: string,
73 | guids: Guids
74 | ): Promise {
75 | const createRouteBody = {
76 | host: getRoutePath(subscribedSubdomain),
77 | relationships: {
78 | space: {
79 | data: {
80 | guid: guids.spaceGuid
81 | }
82 | },
83 | domain: {
84 | data: {
85 | guid: guids.domainGuid
86 | }
87 | }
88 | }
89 | };
90 | const createdRoute = (
91 | await executeHttpRequest(cfApiDestination, {
92 | url: '/v3/routes',
93 | method: 'post',
94 | data: createRouteBody
95 | })
96 | ).data;
97 | return createdRoute.guid;
98 | }
99 |
100 | export async function deleteRoute(subscribedSubdomain: string) {
101 | try {
102 | const { appGuid } = await getCfGuids(appRouterName);
103 | const routes = (
104 | await executeHttpRequest(cfApiDestination, {
105 | method: 'get',
106 | url: `/v3/apps/${appGuid}/routes?hosts=${getRoutePath(
107 | subscribedSubdomain
108 | )}`
109 | })
110 | ).data;
111 | await Promise.all(
112 | routes.resources.map(ele =>
113 | executeHttpRequest(cfApiDestination, {
114 | method: 'delete',
115 | url: `/v3/routes/${ele.guid}`
116 | })
117 | )
118 | );
119 | } catch (e) {
120 | console.log(e.msg);
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/samples/cf-multi-tenant-application/multi-tenant-app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["es2015", "dom"],
5 | "esModuleInterop": true
6 | },
7 | "typeAcquisition": {
8 | "enable": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/samples/cf-multi-tenant-application/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cf-multi-tenant-application",
3 | "lockfileVersion": 2,
4 | "requires": true,
5 | "packages": {
6 | "": {
7 | "devDependencies": {
8 | "prettier": "^3.4.2"
9 | },
10 | "engines": {
11 | "node": "^22.0.0",
12 | "npm": "^10.0.0"
13 | }
14 | },
15 | "node_modules/prettier": {
16 | "version": "3.4.2",
17 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
18 | "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
19 | "dev": true,
20 | "bin": {
21 | "prettier": "bin/prettier.cjs"
22 | },
23 | "engines": {
24 | "node": ">=14"
25 | },
26 | "funding": {
27 | "url": "https://github.com/prettier/prettier?sponsor=1"
28 | }
29 | }
30 | },
31 | "dependencies": {
32 | "prettier": {
33 | "version": "3.4.2",
34 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
35 | "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
36 | "dev": true
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/samples/cf-multi-tenant-application/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "format": "prettier --write . "
4 | },
5 | "dependencies": {},
6 | "devDependencies": {
7 | "prettier": "^3.4.2"
8 | },
9 | "engines": {
10 | "node": "^22.0.0",
11 | "npm": "^10.0.0"
12 | }
13 | }
--------------------------------------------------------------------------------
/samples/cf-multi-tenant-application/service-config/saas-registry-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "xsappname": "xs-multi-tenant-sample-app",
3 | "appName": "multi-tenant-app",
4 | "providerTenantId": "YOUR_TENANT_GUID",
5 | "displayName": "multi tenant example application",
6 | "appUrls": {
7 | "getDependencies": "https://multi-tenant-app.cfapps.YOUR_REGION.hana.ondemand.com/dependencies",
8 | "onSubscription": "https://multi-tenant-app.cfapps.YOUR_REGION.hana.ondemand.com/subscription/{tenantId}"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/samples/cf-multi-tenant-application/service-config/xs-security.json:
--------------------------------------------------------------------------------
1 | {
2 | "xsappname": "xs-multi-tenant-sample-app",
3 | "tenant-mode": "shared",
4 | "oauth2-configuration": {
5 | "credential-types": ["instance-secret"]
6 | },
7 | "scopes": [
8 | {
9 | "name": "$XSAPPNAME.Display",
10 | "description": "display"
11 | },
12 | {
13 | "name": "$XSAPPNAME.Callback",
14 | "description": "With this scope set, the callbacks for tenant onboarding, offboarding and getDependencies can be called (according to the docs at least, we don't actually check for any scopes in our e2e test app).",
15 | "grant-as-authority-to-apps": [
16 | "$XSAPPNAME(application,sap-provisioning,tenant-onboarding)"
17 | ]
18 | }
19 | ],
20 | "role-templates": [
21 | {
22 | "name": "Viewer",
23 | "description": "Required to view things in our solution",
24 | "scope-references": ["$XSAPPNAME.Display", "uaa.user"]
25 | }
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/README.md:
--------------------------------------------------------------------------------
1 | # SAP Cloud SDK for JS Cloud Foundry Sample Application
2 |
3 | ## Description
4 | This repository contains our Cloud Foundry sample application.
5 |
6 | The repositories structure is as following:
7 |
8 | - `/approuter` - Contains the approuter, a packaging script and a manifest which is used in the deployment
9 | - `/src` - Contains the application's source code with its 4 endpoints for cloud, onpremise and principal propagation
10 | - `/e2e-tests` - Contains the cypress tests that test all endpoints after deployment
11 | - `manifest.yml` - Manifest to deploy to SAP BTP Cloud Foundry
12 |
13 | If you want to locally trigger the e2e-tests, create a `cypress.env.json` file in the `/e2e-tests` directory containing the credentials for the IdP in the format:
14 |
15 | ```
16 | {
17 | "username": "username",
18 | "password": "password"
19 | "url": "url"
20 | }
21 | ```
22 |
23 | ## Requirements
24 | The minimal requirements are:
25 | - A terminal to execute commands
26 | - A recent version of node and npm installed e.g. node 14 and npm 6
27 | - An IDE or a text editor of your choice
28 |
29 | If you want to explore the possibilities beyond local tests you need:
30 | - Access to a [SAP Business Technology Platform](https://www.sap.com/products/business-technology-platform.html) account
31 | - Entitlement to use resources like service instance creation and application processing units
32 | - Permission to deploy applications and create service instances
33 |
34 | ## Download and Installation
35 | To download the application run:
36 |
37 | ```
38 | git clone \
39 | --depth 1 \
40 | --filter=blob:none \
41 | --sparse \
42 | https://github.com/SAP-samples/cloud-sdk-js.git \
43 | ;
44 | cd cloud-sdk-js
45 | git sparse-checkout set samples/cf-sample-application
46 | ```
47 |
48 | ### Generate oData Client
49 |
50 | The following service definitions in `EDMX` format are already downloaded in the folder `resources/service-specs`:
51 | - [Business Partner service onPremise](https://api.sap.com/api/OP_API_BUSINESS_PARTNER_SRV/overview)
52 | - [Business Partner service cloud](https://api.sap.com/api/API_BUSINESS_PARTNER/overview)
53 |
54 | The clients are generated using the `npm run generate-client` command. This command is executed automatically in the `postinstall` step after you execute `npm i `.
55 |
56 | **Note** These services are licensed under the terms of [SAP API Information License](../../LICENSES/LicenseRef-API-Definition-File-License.txt). This limits their use to development purposes only.
57 |
58 | ### Create Services on SAP BTP Cloud Foundry
59 | Before you can deploy the application and approuter, you have to create a `destination`, `xsuaa`, and `connectivity` service instance.
60 | Their name should match the one that is used in the `manifest.yml`, in this case:
61 |
62 | ```
63 | - sample-destination-service
64 | - sample-xsuaa-service
65 | - sample-connectivity-service
66 | ```
67 |
68 | ### Deploy the Approuter and Application to SAP BTP Cloud Foundry
69 | 1. Change all occurrences of `` to your respective values, this includes destinations, your Subdomain, etc.
70 | 2. Change the `xs-app.json` in the approuter directory to use either an IdP associated with your subaccount, or no IdP at all.
71 | 3. Run `npm run deploy` in the root, as well as in the `approuter` directory to deploy the application to SAP BTP Cloud Foundry.
72 | 4. After both the `approuter` and application are deployed, open the `approuter's` url to access the deployed application.
73 |
74 | ### Code Placeholders
75 | If anything isn't working as intended, search for ``, as all parts that have to be adapted contain this placeholder.
76 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/approuter/manifest.yml:
--------------------------------------------------------------------------------
1 | applications:
2 | - name: approuter
3 | routes:
4 | # Replace the subpath with your subaccount, to match the TENANT_HOST_PATTERN
5 | # Example: approuter-.cfapps.sap.hana.ondemand.com
6 | - route: approuter-s4sdk.cfapps.sap.hana.ondemand.com
7 | memory: 512M
8 | buildpack: nodejs_buildpack
9 | command: npm run start
10 | path: ./approuter.zip
11 | services:
12 | # Use the same services across approuter and application
13 | - sample-xsuaa-service
14 | env:
15 | TENANT_HOST_PATTERN: 'approuter-(.*).cfapps.sap.hana.ondemand.com'
16 | # Replace the url with your application's url
17 | # Example: sdk-sample-application.
18 | destinations: '[{"name":"backend", "url":"https://sdk-sample-application.cfapps.sap.hana.ondemand.com", "forwardAuthToken": true}]'
19 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/approuter/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "approuter",
3 | "dependencies": {
4 | "@sap/approuter": "20.2.0",
5 | "package.json": "2.0.1"
6 | },
7 | "scripts": {
8 | "start": "node node_modules/@sap/approuter/approuter.js",
9 | "package": "./package.sh",
10 | "deploy": "npm run package && cf push",
11 | "deploy:docker": "docker build -t docker-cloudsdk.common.repositories.cloud.sap/k8s-approuter:latest . && docker push docker-cloudsdk.common.repositories.cloud.sap/k8s-approuter:latest"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/approuter/package.sh:
--------------------------------------------------------------------------------
1 | {
2 | for file in xs-app.json .npmrc package.json ; do
3 | if [[ ! -e $file ]] ; then echo -e "\e[33m[WARNING] $file does not exist\e[0m"; fi ;
4 | done
5 | zip -r approuter.zip .
6 | }
--------------------------------------------------------------------------------
/samples/cf-sample-application/approuter/static-resources/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Hello World
5 |
6 |
7 | Hello world!
8 |
9 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/approuter/xs-app.json:
--------------------------------------------------------------------------------
1 | {
2 | "welcomeFile": "/web-pages/index.html",
3 | "routes": [
4 | {
5 | "source": "/backend-app/(.*)",
6 | "target": "$1",
7 | "destination": "backend",
8 | "identityProvider": ""
9 | },
10 | {
11 | "source": "/web-pages/(.*)",
12 | "target": "$1",
13 | "localDir": "static-resources",
14 | "identityProvider": ""
15 | }]
16 | }
--------------------------------------------------------------------------------
/samples/cf-sample-application/approuter/xs-security.json:
--------------------------------------------------------------------------------
1 | {
2 | "xsappname": "sample-xsuaa-service",
3 | "tenant-mode": "shared"
4 | }
--------------------------------------------------------------------------------
/samples/cf-sample-application/e2e-tests/cypress.json:
--------------------------------------------------------------------------------
1 | {"chromeWebSecurity": false}
2 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/e2e-tests/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
6 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/e2e-tests/cypress/integration/e2e.spec.ts:
--------------------------------------------------------------------------------
1 | describe('e2e tests for k8s test app', () => {
2 | function checkEndpoint(endpoint: string) {
3 | cy.visit('https://' + Cypress.env('url'));
4 |
5 | cy.get('#j_username')
6 | .type(Cypress.env('username'))
7 | .get('#j_password')
8 | .type(Cypress.env('password'))
9 | .get('#logOnFormSubmit')
10 | .click();
11 |
12 | cy.request(`backend-app/${endpoint}`).then((resp) => {
13 | expect(resp.status).to.eq(200);
14 | });
15 | }
16 | it('tests cloud endpoint', () => {
17 | checkEndpoint('cloud-business-partner');
18 | });
19 |
20 | it('tests onpremise endpoint', () => {
21 | checkEndpoint('onpremise-business-partner');
22 | });
23 |
24 | it('tests principal propagation endpoint', () => {
25 | checkEndpoint('principal-business-partner');
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/e2e-tests/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | ///
2 | // ***********************************************************
3 | // This example plugins/index.js can be used to load plugins
4 | //
5 | // You can change the location of this file or turn off loading
6 | // the plugins file with the 'pluginsFile' configuration option.
7 | //
8 | // You can read more here:
9 | // https://on.cypress.io/plugins-guide
10 | // ***********************************************************
11 |
12 | // This function is called when a project is opened or re-opened (e.g. due to
13 | // the project's config changing)
14 |
15 | /**
16 | * @type {Cypress.PluginConfig}
17 | */
18 | // eslint-disable-next-line no-unused-vars
19 | module.exports = (on, config) => {
20 | // `on` is used to hook into various events Cypress emits
21 | // `config` is the resolved Cypress config
22 | }
23 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/e2e-tests/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add('login', (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This will overwrite an existing command --
25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
26 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/e2e-tests/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/e2e-tests/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@sap-cloud-sdk/samples",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "scripts": {
6 | "test": "npx cypress run"
7 | },
8 | "author": "",
9 | "license": "Apache-2.0",
10 | "devDependencies": {
11 | "cypress": "^12.0.0",
12 | "typescript": "~5.7.2"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/e2e-tests/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["es5", "dom"],
5 | "types": ["cypress"]
6 | },
7 | "include": ["**/*.ts"]
8 | }
--------------------------------------------------------------------------------
/samples/cf-sample-application/manifest.yml:
--------------------------------------------------------------------------------
1 | applications:
2 | - name: sdk-sample-application
3 | path: ./
4 | buildpacks:
5 | - nodejs_buildpack
6 | memory: 512M
7 | instances: 2
8 | command: npm run start:prod
9 | services:
10 | - sample-destination-service
11 | - sample-xsuaa-service
12 | - sample-connectivity-service
13 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "collection": "@nestjs/schematics",
3 | "sourceRoot": "src"
4 | }
5 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@sap-cloud-sdk/samples",
3 | "version": "0.0.1",
4 | "description": "SAP Cloud SDK for JS - Sample application",
5 | "private": true,
6 | "license": "Apache-2.0",
7 | "scripts": {
8 | "prebuild": "rimraf dist",
9 | "build": "nest build",
10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
11 | "start": "nest start",
12 | "start:dev": "nest start --watch",
13 | "start:debug": "nest start --debug --watch",
14 | "start:prod": "node dist/main",
15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
16 | "test": "jest",
17 | "test:watch": "jest --watch",
18 | "test:cov": "jest --coverage",
19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
20 | "test:e2e": "jest --config ./test/jest-e2e.json",
21 | "ci-build": "echo \"Use this to compile or minify your application\"",
22 | "deploy": "npm run build && cf push",
23 | "deploy:docker": "npm run build && docker build -t docker-cloudsdk.common.repositories.cloud.sap/k8s-e2e-app:latest . && docker push docker-cloudsdk.common.repositories.cloud.sap/k8s-e2e-app:latest",
24 | "deploy:pipeline": "docker build -t docker-cloudsdk.common.repositories.cloud.sap/sdkjs-k8s-pipeline:latest ./pipeline && docker push docker-cloudsdk.common.repositories.cloud.sap/sdkjs-k8s-pipeline:latest",
25 | "generate-client": "npx generate-odata-client --input resources/service-specs --outputDir src/generated --optionsPerService=resources/service-specs/options-per-service.json --clearOutputDir --transpile=false"
26 | },
27 | "dependencies": {
28 | "@nestjs/common": "^9.4.3",
29 | "@nestjs/core": "^9.4.3",
30 | "@nestjs/platform-express": "^9.4.3",
31 | "@sap-cloud-sdk/connectivity": "^4.0.0",
32 | "@sap-cloud-sdk/odata-v2": "^3.0.0",
33 | "@sap/xsenv": "^3.4.0",
34 | "@sap/xssec": "^3.2.17",
35 | "passport": "^0.7.0",
36 | "reflect-metadata": "^0.1.13",
37 | "rimraf": "^3.0.2",
38 | "rxjs": "^7.8.1",
39 | "webpack": "^5.80.0"
40 | },
41 | "devDependencies": {
42 | "@nestjs/cli": "^9.5.0",
43 | "@nestjs/schematics": "^9.2.0",
44 | "@nestjs/testing": "^9.4.3",
45 | "@sap-cloud-sdk/test-util": "^3.0.0",
46 | "@sap-cloud-sdk/generator": "^3.0.0",
47 | "@types/express": "^4.17.17",
48 | "@types/jest": "^29.5.4",
49 | "@types/node": "^22.10.5",
50 | "@types/supertest": "^2.0.12",
51 | "@typescript-eslint/eslint-plugin": "^5.59.0",
52 | "@typescript-eslint/parser": "^5.59.0",
53 | "eslint": "^8.38.0",
54 | "eslint-config-prettier": "^8.8.0",
55 | "eslint-plugin-prettier": "^4.2.1",
56 | "jest": "29.6.4",
57 | "prettier": "^3.4.2",
58 | "supertest": "^6.3.3",
59 | "ts-jest": "^29.1.1",
60 | "ts-loader": "^9.4.4",
61 | "ts-node": "^10.9.1",
62 | "tsconfig-paths": "^4.2.0",
63 | "typescript": "~5.7.2"
64 | },
65 | "jest": {
66 | "moduleFileExtensions": [
67 | "js",
68 | "json",
69 | "ts"
70 | ],
71 | "rootDir": "src",
72 | "testRegex": ".*\\.spec\\.ts$",
73 | "transform": {
74 | "^.+\\.(t|j)s$": "ts-jest"
75 | },
76 | "collectCoverageFrom": [
77 | "**/*.(t|j)s"
78 | ],
79 | "coverageDirectory": "../coverage",
80 | "testEnvironment": "node"
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/resources/service-specs/options-per-service.json:
--------------------------------------------------------------------------------
1 | {
2 | "resources/service-specs/OP_API_BUSINESS_PARTNER_SRV.edmx": {
3 | "packageName": "op-business-partner-service",
4 | "directoryName": "op-business-partner-service",
5 | "basePath": "/sap/opu/odata/sap/API_BUSINESS_PARTNER"
6 | },
7 | "resources/service-specs/API_BUSINESS_PARTNER.edmx": {
8 | "packageName": "cloud-business-partner-service",
9 | "directoryName": "cloud-business-partner-service",
10 | "basePath": "/sap/opu/odata/sap/API_BUSINESS_PARTNER"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/src/app.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { AppController } from './app.controller';
3 | import { AppService } from './app.service';
4 |
5 | describe('AppController', () => {
6 | let appController: AppController;
7 |
8 | beforeEach(async () => {
9 | const app: TestingModule = await Test.createTestingModule({
10 | controllers: [AppController],
11 | providers: [AppService],
12 | }).compile();
13 |
14 | appController = app.get(AppController);
15 | });
16 |
17 | describe('root', () => {
18 | it('should return "Hello World!"', () => {
19 | expect(appController.getHello()).toBe('Hello World!');
20 | });
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/src/app.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get, Req } from '@nestjs/common';
2 | import { BusinessPartner as BusinessPartnerCloud } from './generated/cloud-business-partner-service';
3 | import { BusinessPartner as BusinessPartnerOp } from './generated/op-business-partner-service';
4 | import { OnpremiseBusinessPartnerService } from './onpremise-business-partner/onpremise-business-partner.service';
5 | import { AppService } from './app.service';
6 | import { CloudBusinessPartnerService } from './cloud-business-partner/cloud-business-partner.service';
7 | import { PrincipalBusinessPartnerService } from './principal-business-partner/principal-business-partner.service';
8 | import { Request } from 'express';
9 | import { LoadtestService } from './loadtest/loadtest.service';
10 |
11 | @Controller()
12 | export class AppController {
13 | constructor(
14 | private readonly appService: AppService,
15 | private readonly cloudBusinessPartnerService: CloudBusinessPartnerService,
16 | private readonly onpremiseBusinessPartnerService: OnpremiseBusinessPartnerService,
17 | private readonly principalBusinessPartnerService: PrincipalBusinessPartnerService,
18 | private readonly loadtestService: LoadtestService,
19 | ) {}
20 |
21 | @Get()
22 | getHello(): string {
23 | return this.appService.getHello();
24 | }
25 |
26 | @Get('cloud-business-partner')
27 | getCloudBusinessPartner(): Promise {
28 | return this.cloudBusinessPartnerService.getFiveBusinessPartners();
29 | }
30 |
31 | @Get('onpremise-business-partner')
32 | getOnpremiseBusinessPartner(): Promise {
33 | return this.onpremiseBusinessPartnerService.getFiveBusinessPartners();
34 | }
35 |
36 | @Get('principal-business-partner')
37 | getPrincipalBusinessPartner(
38 | @Req() request: Request,
39 | ): Promise {
40 | return this.principalBusinessPartnerService.getFiveBusinessPartners(
41 | request,
42 | );
43 | }
44 |
45 | @Get('loadtest')
46 | calculateExpensiveNumber(): number {
47 | return this.loadtestService.calculateExpensiveNumber();
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { AppController } from './app.controller';
3 | import { AppService } from './app.service';
4 | import { OnpremiseBusinessPartnerService } from './onpremise-business-partner/onpremise-business-partner.service';
5 | import { CloudBusinessPartnerService } from './cloud-business-partner/cloud-business-partner.service';
6 | import { PrincipalBusinessPartnerService } from './principal-business-partner/principal-business-partner.service';
7 | import { LoadtestService } from './loadtest/loadtest.service';
8 |
9 | @Module({
10 | imports: [],
11 | controllers: [AppController],
12 | providers: [
13 | AppService,
14 | OnpremiseBusinessPartnerService,
15 | CloudBusinessPartnerService,
16 | PrincipalBusinessPartnerService,
17 | LoadtestService,
18 | ],
19 | })
20 | export class AppModule {}
21 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/src/app.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 |
3 | @Injectable()
4 | export class AppService {
5 | getHello(): string {
6 | return 'Hello World!';
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/src/cloud-business-partner/cloud-business-partner.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { CloudBusinessPartnerService } from './cloud-business-partner.service';
3 |
4 | describe('CloudBusinessPartnerService', () => {
5 | let service: CloudBusinessPartnerService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [CloudBusinessPartnerService],
10 | }).compile();
11 |
12 | service = module.get(
13 | CloudBusinessPartnerService,
14 | );
15 | });
16 |
17 | it('should be defined', () => {
18 | expect(service).toBeDefined();
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/src/cloud-business-partner/cloud-business-partner.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import {
3 | BusinessPartner,
4 | cloudBusinessPartnerService as businessPartnerService,
5 | } from '../generated/cloud-business-partner-service';
6 | const { businessPartnerApi } = businessPartnerService();
7 |
8 | @Injectable()
9 | export class CloudBusinessPartnerService {
10 | async getFiveBusinessPartners(): Promise {
11 | return businessPartnerApi
12 | .requestBuilder()
13 | .getAll()
14 | .top(5)
15 | // the destination should point at a cloud basic auth destination
16 | // Example:
17 | .execute({ destinationName: 'myCloudDestination' });
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/src/loadtest/loadtest.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { LoadtestService } from './loadtest.service';
3 |
4 | describe('LoadtestService', () => {
5 | let service: LoadtestService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [LoadtestService],
10 | }).compile();
11 |
12 | service = module.get(LoadtestService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/src/loadtest/loadtest.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 |
3 | @Injectable()
4 | export class LoadtestService {
5 | calculateExpensiveNumber(): number {
6 | var expensiveNumber = 0;
7 | for(var i = 0; i<1000; i++){
8 | for(var j = 0; j<1000; j++){
9 | expensiveNumber += i*j*Date.now()
10 | }
11 | }
12 | return expensiveNumber;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/src/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { AppModule } from './app.module';
3 | import { JWTStrategy } from '@sap/xssec';
4 | import { getServices } from '@sap/xsenv';
5 | import * as passport from 'passport';
6 |
7 | // Use the same xsuaa across the entire application
8 | const xsuaa = getServices({ xsuaa: { name: 'sample-xsuaa-service' } }).xsuaa;
9 | passport.use(new JWTStrategy(xsuaa));
10 |
11 | async function bootstrap() {
12 | const app = await NestFactory.create(AppModule);
13 | app.use(passport.initialize());
14 | app.use(passport.authenticate('JWT', { session: false }));
15 | await app.listen(process.env.PORT || 3000);
16 | }
17 | bootstrap();
18 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/src/onpremise-business-partner/onpremise-business-partner.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { OnpremiseBusinessPartnerService } from './onpremise-business-partner.service';
3 |
4 | describe('OnpremiseBusinessPartnerService', () => {
5 | let service: OnpremiseBusinessPartnerService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [OnpremiseBusinessPartnerService],
10 | }).compile();
11 |
12 | service = module.get(
13 | OnpremiseBusinessPartnerService,
14 | );
15 | });
16 |
17 | it('should be defined', () => {
18 | expect(service).toBeDefined();
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/src/onpremise-business-partner/onpremise-business-partner.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import {
3 | BusinessPartner,
4 | opBusinessPartnerService as businessPartnerService,
5 | } from '../generated/op-business-partner-service';
6 | const { businessPartnerApi } = businessPartnerService();
7 |
8 | @Injectable()
9 | export class OnpremiseBusinessPartnerService {
10 | async getFiveBusinessPartners(): Promise {
11 | return businessPartnerApi
12 | .requestBuilder()
13 | .getAll()
14 | .top(5)
15 | // the destination should point at a onpremise basic authentcation destination
16 | // Example:
17 | .execute({ destinationName: 'myOnpremiseDestination' });
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/src/principal-business-partner/principal-business-partner.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { PrincipalBusinessPartnerService } from './principal-business-partner.service';
3 |
4 | describe('PrincipalBusinessPartnerService', () => {
5 | let service: PrincipalBusinessPartnerService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [PrincipalBusinessPartnerService],
10 | }).compile();
11 |
12 | service = module.get(PrincipalBusinessPartnerService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/src/principal-business-partner/principal-business-partner.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import {
3 | BusinessPartner,
4 | opBusinessPartnerService as businessPartnerService,
5 | } from '../generated/op-business-partner-service';
6 | import { retrieveJwt } from '@sap-cloud-sdk/connectivity';
7 | import { Request } from 'express';
8 | const { businessPartnerApi } = businessPartnerService();
9 |
10 | @Injectable()
11 | export class PrincipalBusinessPartnerService {
12 | async getFiveBusinessPartners(request: Request): Promise {
13 | return businessPartnerApi
14 | .requestBuilder()
15 | .getAll()
16 | .top(5)
17 | .execute({
18 | // the destination should point at a principal propagation destination
19 | // Example:
20 | destinationName: 'myPrincipalPropagationDestination',
21 | jwt: retrieveJwt(request),
22 | });
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/systems.json:
--------------------------------------------------------------------------------
1 | {
2 | "systems": [{
3 | "alias": "EXAMPLE",
4 | "uri": "https://example.com"
5 | }]
6 | }
7 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/test/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { INestApplication } from '@nestjs/common';
3 | import * as request from 'supertest';
4 | import { AppModule } from './../src/app.module';
5 |
6 | describe('AppController (e2e)', () => {
7 | let app: INestApplication;
8 |
9 | beforeEach(async () => {
10 | const moduleFixture: TestingModule = await Test.createTestingModule({
11 | imports: [AppModule],
12 | }).compile();
13 |
14 | app = moduleFixture.createNestApplication();
15 | await app.init();
16 | });
17 |
18 | it('/ (GET)', () => {
19 | return request(app.getHttpServer())
20 | .get('/')
21 | .expect(200)
22 | .expect('Hello World!');
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/test/jest-e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "moduleFileExtensions": ["js", "json", "ts"],
3 | "rootDir": ".",
4 | "testEnvironment": "node",
5 | "testRegex": ".e2e-spec.ts$",
6 | "transform": {
7 | "^.+\\.(t|j)s$": "ts-jest"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/samples/cf-sample-application/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "allowSyntheticDefaultImports": true,
9 | "target": "es2017",
10 | "sourceMap": true,
11 | "outDir": "./dist",
12 | "baseUrl": "./",
13 | "incremental": true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/README.md:
--------------------------------------------------------------------------------
1 | # SAP Cloud SDK for JS Kubernetes End-2-End Application
2 | This directory is structured the following way:
3 |
4 | - `./application` contains our Heln sample application
5 | - `./helm-chart` contains the Helm chart, which we use to deploy the application
6 | - `./sap-btp-operator` contains the Helm chart of the SAP BTP Operator, together with a self-signed issuer, all services which are related to our application, as well as a simple `values.yaml` for the helm chart
7 |
8 | ## Download and Installation
9 | To download the application run:
10 |
11 | ```
12 | git clone \
13 | --depth 1 \
14 | --filter=blob:none \
15 | --sparse \
16 | https://github.com/SAP-samples/cloud-sdk-js.git \
17 | ;
18 | cd cloud-sdk-js
19 | git sparse-checkout set samples/helm-sample-application
20 | ```
21 |
22 | To deploy the application, follow the guidelines in the [application directory](./application/README.md).
23 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:14-alpine
2 |
3 | WORKDIR /usr/src/app
4 |
5 | COPY package.json .
6 |
7 | RUN npm install -g npm@latest
8 |
9 | RUN npm install --unsafe-perm --production
10 |
11 | COPY . ./
12 |
13 | EXPOSE 3000
14 | CMD ["npm", "run", "start:prod"]
15 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/README.md:
--------------------------------------------------------------------------------
1 | # SAP Cloud SDK for JS Kubernetes End-2-End Application
2 |
3 | This repository contains our end-2-end application which we use to test the sdk's functionality in a Kubernetes environment.
4 | The application is deployed together with our Helm chart located [here](https://github.tools.sap/cloudsdk/k8s-sdkjs-chart/blob/main/README.md).
5 |
6 | The repositories structure is as following:
7 |
8 | - `/approuter` - Contains the approuter, a packaging script and a Dockerfile which is used in the deployment
9 | - `/pipeline` - Contains the pipeline's Dockerfile which contains all tools to run the pipeline
10 | - `/src` - Contains the application's source code with its 4 endpoints for cloud, onpremise and principal propagation
11 | - `/e2e-tests` - Contains the cypress tests that test all endpoints after deployment
12 | - `./github/workflows` - Contains the pipeline that builds, packages, deploys and tests the application
13 | - `Dockerfile` - Packages the application as Dockerimage to be used in Kubernetes
14 |
15 | If you want to locally trigger the e2e-tests, create a `cypress.env.json` file in the `/e2e-tests` directory containing the credentials for the IdP in the format:
16 |
17 | ```
18 | {
19 | "username": "username",
20 | "password": "password"
21 | "url": "url"
22 | }
23 | ```
24 |
25 | ## Requirements
26 | The minimal requirements are:
27 | - A terminal to execute commands
28 | - A recent version of node and npm installed e.g. node 14 and npm 6
29 | - An IDE or a text editor of your choice
30 |
31 | If you want to explore the possibilities beyond local tests you need:
32 | - Access to a [SAP Business Technology Platform](https://www.sap.com/products/business-technology-platform.html) account
33 | - Entitlement to use resources like service instance creation and application processing units
34 | - Permission to create service instances
35 | - Access to a `Docker` repository
36 | - A Kubernetes Cluster which runs:
37 | - The SAP BTP Operator
38 | - The SAP Connectivity Proxy
39 |
40 | ## Download and Installation
41 | To download the application run:
42 |
43 | ```
44 | git clone \
45 | --depth 1 \
46 | --filter=blob:none \
47 | --sparse \
48 | https://github.com/SAP-samples/cloud-sdk-js.git \
49 | ;
50 | cd cloud-sdk-js
51 | git sparse-checkout set samples/helm-sample-application
52 | ```
53 |
54 | ### Generate oData Client
55 |
56 | The following service definitions in `EDMX` format are already downloaded in the folder `resources/service-specs`:
57 | - [Business Partner service onPremise](https://api.sap.com/api/OP_API_BUSINESS_PARTNER_SRV/overview)
58 | - [Business Partner service cloud](https://api.sap.com/api/API_BUSINESS_PARTNER/overview)
59 |
60 | The clients are generated using the `npm run generate-client` command. This command is executed automatically in the `postinstall` step after you execute `npm i `.
61 |
62 | **Note** These services are licensed under the terms of [SAP API Information License](../../../LICENSES/LicenseRef-API-Definition-File-License.txt). This limits their use to development purposes only.
63 |
64 | ### Deploy to Docker
65 | 1. In the `package.json`, change the `deploy:docker` and `deploy:pipeline` scripts to point at your docker repository.
66 | 2. Change the the `deploy:docker` script in the approuter's `package.json` to point at your docker repository.
67 | 3. Deploy the Docker images to your repository with `npm run deploy:docker` and `npm run deploy:pipeline` in case you want to use the pipeline.
68 |
69 | ### Deploy to Kubernetes
70 |
71 | To deploy this application to Kubernetes, first you have to deploy all the services this application has to use, which includes:
72 | - the `connectivity` service
73 | - the `xsuaa` service
74 | - the `destination` service
75 |
76 | To deploy them simply `kubectl apply -f` the yaml files under `sap-btp-operator/services`.
77 |
78 | After deploying the services, all you have to do is editing the `values.yaml` file under `helm-chart`.
79 | The helm chart's [README](../helm-chart/README.md) should explain to you what values you can use and which you have to change.
80 |
81 | Once you have changed the `values.yaml`, run `helm install e2e-app k8s-e2e-app-helm-0.1.5.tgz --values values.yaml` and you should be good to go.
82 |
83 | For more detailed information on Kubernetes deployment, check out our [Kubernetes migration guide](https://sap.github.io/cloud-sdk/docs/js/guides/migrate-sdk-application-from-btp-cf-to-kubernetes).
84 |
85 | The architecture of this application, together with its dependencies, the connectivity proxy, and the services created by the sap-btp-operator, is depicted in the following architecture diagram:
86 |
87 |
88 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/approuter/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:14
2 |
3 | # Create app directory
4 | WORKDIR /usr/src/app
5 |
6 | # Install app dependencies
7 | # A wildcard is used to ensure both package.json AND package-lock.json are copied
8 | # where available (npm@5+)
9 | COPY package*.json ./
10 |
11 | RUN npm config set @sap:registry https://registry.npmjs.org
12 | RUN npm install
13 |
14 | # Bundle app source
15 | COPY . .
16 |
17 | EXPOSE 5000
18 | CMD [ "npm", "start" ]
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/approuter/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "approuter",
3 | "dependencies": {
4 | "@sap/approuter": "20.2.0",
5 | "package.json": "2.0.1"
6 | },
7 | "scripts": {
8 | "start": "node node_modules/@sap/approuter/approuter.js",
9 | "package": "./package.sh",
10 | "deploy": "npm run package && cf push",
11 | "deploy:docker": "docker build -t docker-cloudsdk.common.repositories.cloud.sap/k8s-approuter:latest . && docker push docker-cloudsdk.common.repositories.cloud.sap/k8s-approuter:latest"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/approuter/static-resources/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Hello World
5 |
6 |
7 | Hello world!
8 |
9 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/e2e-tests/cypress.json:
--------------------------------------------------------------------------------
1 | {"chromeWebSecurity": false}
2 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/e2e-tests/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
6 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/e2e-tests/cypress/integration/e2e.spec.ts:
--------------------------------------------------------------------------------
1 | describe('e2e tests for k8s test app', () => {
2 | function checkEndpoint(endpoint: string) {
3 | cy.visit('https://' + Cypress.env('url'));
4 |
5 | cy.get('#j_username')
6 | .type(Cypress.env('username'))
7 | .get('#j_password')
8 | .type(Cypress.env('password'))
9 | .get('#logOnFormSubmit')
10 | .click();
11 |
12 | cy.request(`backend-app/${endpoint}`).then((resp) => {
13 | expect(resp.status).to.eq(200);
14 | });
15 | }
16 | it('tests cloud endpoint', () => {
17 | checkEndpoint('cloud-business-partner');
18 | });
19 |
20 | it('tests onpremise endpoint', () => {
21 | checkEndpoint('onpremise-business-partner');
22 | });
23 |
24 | it('tests principal propagation endpoint', () => {
25 | checkEndpoint('principal-business-partner');
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/e2e-tests/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | ///
2 | // ***********************************************************
3 | // This example plugins/index.js can be used to load plugins
4 | //
5 | // You can change the location of this file or turn off loading
6 | // the plugins file with the 'pluginsFile' configuration option.
7 | //
8 | // You can read more here:
9 | // https://on.cypress.io/plugins-guide
10 | // ***********************************************************
11 |
12 | // This function is called when a project is opened or re-opened (e.g. due to
13 | // the project's config changing)
14 |
15 | /**
16 | * @type {Cypress.PluginConfig}
17 | */
18 | // eslint-disable-next-line no-unused-vars
19 | module.exports = (on, config) => {
20 | // `on` is used to hook into various events Cypress emits
21 | // `config` is the resolved Cypress config
22 | }
23 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/e2e-tests/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add('login', (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This will overwrite an existing command --
25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
26 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/e2e-tests/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/e2e-tests/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@sap-cloud-sdk/samples",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "scripts": {
6 | "test": "npx cypress run"
7 | },
8 | "author": "",
9 | "license": "Apache-2.0",
10 | "devDependencies": {
11 | "cypress": "^12.0.0",
12 | "typescript": "~5.7.2"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/e2e-tests/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["es5", "dom"],
5 | "types": ["cypress"]
6 | },
7 | "include": ["**/*.ts"]
8 | }
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/images/cluster_arch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SAP-samples/cloud-sdk-js/1bfa08bcdc2694d96bef8798d849afc4c10ec4a8/samples/helm-sample-application/application/images/cluster_arch.png
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "collection": "@nestjs/schematics",
3 | "sourceRoot": "src"
4 | }
5 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@sap-cloud-sdk/samples",
3 | "version": "0.0.1",
4 | "description": "SAP Cloud SDK for JS - Sample application",
5 | "private": true,
6 | "license": "Apache-2.0",
7 | "scripts": {
8 | "prebuild": "rimraf dist",
9 | "build": "nest build",
10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
11 | "start": "nest start",
12 | "start:dev": "nest start --watch",
13 | "start:debug": "nest start --debug --watch",
14 | "start:prod": "node dist/main",
15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
16 | "test": "jest",
17 | "test:watch": "jest --watch",
18 | "test:cov": "jest --coverage",
19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
20 | "test:e2e": "jest --config ./test/jest-e2e.json",
21 | "ci-build": "echo \"Use this to compile or minify your application\"",
22 | "deploy": "npm run build && cf push",
23 | "deploy:docker": "npm run build && docker build -t docker-cloudsdk.common.repositories.cloud.sap/k8s-e2e-app:latest . && docker push docker-cloudsdk.common.repositories.cloud.sap/k8s-e2e-app:latest",
24 | "deploy:pipeline": "docker build -t docker-cloudsdk.common.repositories.cloud.sap/sdkjs-k8s-pipeline:latest ./pipeline && docker push docker-cloudsdk.common.repositories.cloud.sap/sdkjs-k8s-pipeline:latest",
25 | "generate-client": "npx generate-odata-client --input resources/service-specs --outputDir src/generated --optionsPerService=resources/service-specs/options-per-service.json --clearOutputDir --transpile=false"
26 | },
27 | "dependencies": {
28 | "@nestjs/common": "^9.4.3",
29 | "@nestjs/core": "^9.4.3",
30 | "@nestjs/platform-express": "^9.4.3",
31 | "@sap-cloud-sdk/connectivity": "^4.0.0",
32 | "@sap-cloud-sdk/odata-v2": "^3.0.0",
33 | "@sap/xsenv": "^3.4.0",
34 | "@sap/xssec": "^3.2.17",
35 | "passport": "^0.7.0",
36 | "reflect-metadata": "^0.1.13",
37 | "rimraf": "^3.0.2",
38 | "rxjs": "^7.8.1",
39 | "webpack": "^5.80.0"
40 | },
41 | "devDependencies": {
42 | "@nestjs/cli": "^9.5.0",
43 | "@nestjs/schematics": "^9.2.0",
44 | "@nestjs/testing": "^9.4.3",
45 | "@sap-cloud-sdk/test-util": "^3.0.0",
46 | "@sap-cloud-sdk/generator": "^3.0.0",
47 | "@types/express": "^4.17.17",
48 | "@types/jest": "^29.5.4",
49 | "@types/node": "^22.10.5",
50 | "@types/supertest": "^2.0.12",
51 | "@typescript-eslint/eslint-plugin": "^5.59.0",
52 | "@typescript-eslint/parser": "^5.59.0",
53 | "eslint": "^8.38.0",
54 | "eslint-config-prettier": "^8.8.0",
55 | "eslint-plugin-prettier": "^4.2.1",
56 | "jest": "29.6.4",
57 | "prettier": "^3.4.2",
58 | "supertest": "^6.3.3",
59 | "ts-jest": "^29.1.1",
60 | "ts-loader": "^9.4.4",
61 | "ts-node": "^10.9.1",
62 | "tsconfig-paths": "^4.2.0",
63 | "typescript": "~5.7.2"
64 | },
65 | "jest": {
66 | "moduleFileExtensions": [
67 | "js",
68 | "json",
69 | "ts"
70 | ],
71 | "rootDir": "src",
72 | "testRegex": ".*\\.spec\\.ts$",
73 | "transform": {
74 | "^.+\\.(t|j)s$": "ts-jest"
75 | },
76 | "collectCoverageFrom": [
77 | "**/*.(t|j)s"
78 | ],
79 | "coverageDirectory": "../coverage",
80 | "testEnvironment": "node"
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/pipeline/Dockerfile:
--------------------------------------------------------------------------------
1 | # Start from the docker image which contains mvn
2 | FROM ubuntu:20.04
3 |
4 | # Install docker
5 | ## Update the apt package index and install packages to allow apt to use a repository over HTTPS:
6 | RUN apt-get update
7 | RUN apt-get install -y \
8 | apt-transport-https \
9 | ca-certificates \
10 | curl \
11 | gnupg \
12 | lsb-release
13 |
14 | ## Add Docker’s official GPG key:
15 | RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
16 |
17 | ## Use the following command to set up the stable repository.
18 | RUN echo \
19 | "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
20 | $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
21 |
22 | ## Install Docker Engine
23 | RUN apt-get update
24 | RUN DEBIAN_FRONTEND=noninteractive apt-get install -y docker-ce docker-ce-cli containerd.io
25 |
26 | # Install git
27 | RUN apt install -y git-all
28 |
29 | # Install kubectl
30 | RUN curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
31 | RUN install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
32 |
33 | # Add core dependencies for cypress
34 | RUN apt-get install -y libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb
35 |
36 | # Install node 14
37 | RUN curl -fsSL https://deb.nodesource.com/setup_14.x | bash -
38 | RUN apt-get install -y nodejs
39 |
40 | # Update npm version to use lockFileV2
41 | RUN npm install -g npm@latest
42 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/resources/service-specs/options-per-service.json:
--------------------------------------------------------------------------------
1 | {
2 | "resources/service-specs/API_BUSINESS_PARTNER.edmx": {
3 | "packageName": "cloud-business-partner-service",
4 | "directoryName": "cloud-business-partner-service",
5 | "basePath": "/sap/opu/odata/sap/API_BUSINESS_PARTNER"
6 | },
7 | "resources/service-specs/OP_API_BUSINESS_PARTNER_SRV.edmx": {
8 | "packageName": "op-business-partner-service",
9 | "directoryName": "op-business-partner-service",
10 | "basePath": "/sap/opu/odata/sap/API_BUSINESS_PARTNER"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/src/app.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { AppController } from './app.controller';
3 | import { AppService } from './app.service';
4 |
5 | describe('AppController', () => {
6 | let appController: AppController;
7 |
8 | beforeEach(async () => {
9 | const app: TestingModule = await Test.createTestingModule({
10 | controllers: [AppController],
11 | providers: [AppService],
12 | }).compile();
13 |
14 | appController = app.get(AppController);
15 | });
16 |
17 | describe('root', () => {
18 | it('should return "Hello World!"', () => {
19 | expect(appController.getHello()).toBe('Hello World!');
20 | });
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/src/app.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get, Req } from '@nestjs/common';
2 | import { BusinessPartner as BusinessPartnerCloud } from './generated/cloud-business-partner-service';
3 | import { BusinessPartner as BusinessPartnerOp } from './generated/op-business-partner-service';
4 | import { OnpremiseBusinessPartnerService } from './onpremise-business-partner/onpremise-business-partner.service';
5 | import { AppService } from './app.service';
6 | import { CloudBusinessPartnerService } from './cloud-business-partner/cloud-business-partner.service';
7 | import { PrincipalBusinessPartnerService } from './principal-business-partner/principal-business-partner.service';
8 | import { Request } from 'express';
9 | import { LoadtestService } from './loadtest/loadtest.service';
10 |
11 | @Controller()
12 | export class AppController {
13 | constructor(
14 | private readonly appService: AppService,
15 | private readonly cloudBusinessPartnerService: CloudBusinessPartnerService,
16 | private readonly onpremiseBusinessPartnerService: OnpremiseBusinessPartnerService,
17 | private readonly principalBusinessPartnerService: PrincipalBusinessPartnerService,
18 | private readonly loadtestService: LoadtestService,
19 | ) {}
20 |
21 | @Get()
22 | getHello(): string {
23 | return this.appService.getHello();
24 | }
25 |
26 | @Get('cloud-business-partner')
27 | getCloudBusinessPartner(): Promise {
28 | return this.cloudBusinessPartnerService.getFiveBusinessPartners();
29 | }
30 |
31 | @Get('onpremise-business-partner')
32 | getOnpremiseBusinessPartner(): Promise {
33 | return this.onpremiseBusinessPartnerService.getFiveBusinessPartners();
34 | }
35 |
36 | @Get('principal-business-partner')
37 | getPrincipalBusinessPartner(
38 | @Req() request: Request,
39 | ): Promise {
40 | return this.principalBusinessPartnerService.getFiveBusinessPartners(
41 | request,
42 | );
43 | }
44 |
45 | @Get('loadtest')
46 | calculateExpensiveNumber(): number {
47 | return this.loadtestService.calculateExpensiveNumber();
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { AppController } from './app.controller';
3 | import { AppService } from './app.service';
4 | import { OnpremiseBusinessPartnerService } from './onpremise-business-partner/onpremise-business-partner.service';
5 | import { CloudBusinessPartnerService } from './cloud-business-partner/cloud-business-partner.service';
6 | import { PrincipalBusinessPartnerService } from './principal-business-partner/principal-business-partner.service';
7 | import { LoadtestService } from './loadtest/loadtest.service';
8 |
9 | @Module({
10 | imports: [],
11 | controllers: [AppController],
12 | providers: [
13 | AppService,
14 | OnpremiseBusinessPartnerService,
15 | CloudBusinessPartnerService,
16 | PrincipalBusinessPartnerService,
17 | LoadtestService,
18 | ],
19 | })
20 | export class AppModule {}
21 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/src/app.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 |
3 | @Injectable()
4 | export class AppService {
5 | getHello(): string {
6 | return 'Hello World!';
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/src/cloud-business-partner/cloud-business-partner.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { CloudBusinessPartnerService } from './cloud-business-partner.service';
3 |
4 | describe('CloudBusinessPartnerService', () => {
5 | let service: CloudBusinessPartnerService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [CloudBusinessPartnerService],
10 | }).compile();
11 |
12 | service = module.get(
13 | CloudBusinessPartnerService,
14 | );
15 | });
16 |
17 | it('should be defined', () => {
18 | expect(service).toBeDefined();
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/src/cloud-business-partner/cloud-business-partner.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import {
3 | BusinessPartner,
4 | cloudBusinessPartnerService as businessPartnerService,
5 | } from '../generated/cloud-business-partner-service';
6 | const { businessPartnerApi } = businessPartnerService();
7 |
8 | const destinationName: string = process.env.CLOUD_DESTINATION;
9 | @Injectable()
10 | export class CloudBusinessPartnerService {
11 | async getFiveBusinessPartners(): Promise {
12 | return businessPartnerApi
13 | .requestBuilder()
14 | .getAll()
15 | .top(5)
16 | .execute({ destinationName: destinationName });
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/src/loadtest/loadtest.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { LoadtestService } from './loadtest.service';
3 |
4 | describe('LoadtestService', () => {
5 | let service: LoadtestService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [LoadtestService],
10 | }).compile();
11 |
12 | service = module.get(LoadtestService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/src/loadtest/loadtest.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 |
3 | @Injectable()
4 | export class LoadtestService {
5 | calculateExpensiveNumber(): number {
6 | let expensiveNumber = 0;
7 | for (let i = 0; i < 1000; i++) {
8 | for (let j = 0; j < 1000; j++) {
9 | expensiveNumber += i * j * Date.now();
10 | }
11 | }
12 | return expensiveNumber;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/src/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { AppModule } from './app.module';
3 | import { JWTStrategy } from '@sap/xssec';
4 | import { getServices } from '@sap/xsenv';
5 | import * as passport from 'passport';
6 |
7 | const xsuaa = getServices({ xsuaa: { name: 'operator-xsuaa-service' } }).xsuaa;
8 | passport.use(new JWTStrategy(xsuaa));
9 |
10 | async function bootstrap() {
11 | const app = await NestFactory.create(AppModule);
12 | app.use(passport.initialize());
13 | app.use(passport.authenticate('JWT', { session: false }));
14 | await app.listen(process.env.PORT || 3000);
15 | }
16 | bootstrap();
17 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/src/onpremise-business-partner/onpremise-business-partner.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { OnpremiseBusinessPartnerService } from './onpremise-business-partner.service';
3 |
4 | describe('OnpremiseBusinessPartnerService', () => {
5 | let service: OnpremiseBusinessPartnerService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [OnpremiseBusinessPartnerService],
10 | }).compile();
11 |
12 | service = module.get(
13 | OnpremiseBusinessPartnerService,
14 | );
15 | });
16 |
17 | it('should be defined', () => {
18 | expect(service).toBeDefined();
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/src/onpremise-business-partner/onpremise-business-partner.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import {
3 | BusinessPartner,
4 | opBusinessPartnerService as businessPartnerService,
5 | } from '../generated/op-business-partner-service';
6 | const { businessPartnerApi } = businessPartnerService();
7 |
8 | const destinationName: string = process.env.ONPREMISE_DESTINATION;
9 | @Injectable()
10 | export class OnpremiseBusinessPartnerService {
11 | async getFiveBusinessPartners(): Promise {
12 | return businessPartnerApi
13 | .requestBuilder()
14 | .getAll()
15 | .top(5)
16 | .execute({ destinationName: destinationName });
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/src/principal-business-partner/principal-business-partner.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { PrincipalBusinessPartnerService } from './principal-business-partner.service';
3 |
4 | describe('PrincipalBusinessPartnerService', () => {
5 | let service: PrincipalBusinessPartnerService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [PrincipalBusinessPartnerService],
10 | }).compile();
11 |
12 | service = module.get(PrincipalBusinessPartnerService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/src/principal-business-partner/principal-business-partner.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import {
3 | BusinessPartner,
4 | opBusinessPartnerService as businessPartnerService,
5 | } from '../generated/op-business-partner-service';
6 | import { retrieveJwt } from '@sap-cloud-sdk/connectivity';
7 | import { Request } from 'express';
8 | const { businessPartnerApi } = businessPartnerService();
9 |
10 | const destinationName: string = process.env.PRINCIPAL_PROPAGATION_DESTINATION;
11 | @Injectable()
12 | export class PrincipalBusinessPartnerService {
13 | async getFiveBusinessPartners(request: Request): Promise {
14 | return businessPartnerApi
15 | .requestBuilder()
16 | .getAll()
17 | .top(5)
18 | .execute({
19 | destinationName: destinationName,
20 | jwt: retrieveJwt(request),
21 | });
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/systems.json:
--------------------------------------------------------------------------------
1 | {
2 | "systems": [{
3 | "alias": "EXAMPLE",
4 | "uri": "https://example.com"
5 | }]
6 | }
7 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/test/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { INestApplication } from '@nestjs/common';
3 | import * as request from 'supertest';
4 | import { AppModule } from './../src/app.module';
5 |
6 | describe('AppController (e2e)', () => {
7 | let app: INestApplication;
8 |
9 | beforeEach(async () => {
10 | const moduleFixture: TestingModule = await Test.createTestingModule({
11 | imports: [AppModule],
12 | }).compile();
13 |
14 | app = moduleFixture.createNestApplication();
15 | await app.init();
16 | });
17 |
18 | it('/ (GET)', () => {
19 | return request(app.getHttpServer())
20 | .get('/')
21 | .expect(200)
22 | .expect('Hello World!');
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/test/jest-e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "moduleFileExtensions": ["js", "json", "ts"],
3 | "rootDir": ".",
4 | "testEnvironment": "node",
5 | "testRegex": ".e2e-spec.ts$",
6 | "transform": {
7 | "^.+\\.(t|j)s$": "ts-jest"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/application/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "allowSyntheticDefaultImports": true,
9 | "target": "es2017",
10 | "sourceMap": true,
11 | "outDir": "./dist",
12 | "baseUrl": "./",
13 | "incremental": true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/helm-chart/k8s-e2e-app-helm-0.1.6.tgz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SAP-samples/cloud-sdk-js/1bfa08bcdc2694d96bef8798d849afc4c10ec4a8/samples/helm-sample-application/helm-chart/k8s-e2e-app-helm-0.1.6.tgz
--------------------------------------------------------------------------------
/samples/helm-sample-application/helm-chart/k8s-e2e-app-helm/.helmignore:
--------------------------------------------------------------------------------
1 | # Patterns to ignore when building packages.
2 | # This supports shell glob matching, relative path matching, and
3 | # negation (prefixed with !). Only one pattern per line.
4 | .DS_Store
5 | # Common VCS dirs
6 | .git/
7 | .gitignore
8 | .bzr/
9 | .bzrignore
10 | .hg/
11 | .hgignore
12 | .svn/
13 | # Common backup files
14 | *.swp
15 | *.bak
16 | *.tmp
17 | *.orig
18 | *~
19 | # Various IDEs
20 | .project
21 | .idea/
22 | *.tmproj
23 | .vscode/
24 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/helm-chart/k8s-e2e-app-helm/Chart.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v2
2 | appVersion: 1.0.0
3 | dependencies:
4 | - name: app-chart
5 | repository: file://charts/app-chart
6 | version: 0.1.0
7 | - name: approuter-chart
8 | repository: file://charts/approuter-chart
9 | version: 0.1.0
10 | description: A Helm chart for Kubernetes
11 | name: k8s-e2e-app-helm
12 | type: application
13 | version: 0.1.5
14 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/helm-chart/k8s-e2e-app-helm/charts/app-chart/.helmignore:
--------------------------------------------------------------------------------
1 | # Patterns to ignore when building packages.
2 | # This supports shell glob matching, relative path matching, and
3 | # negation (prefixed with !). Only one pattern per line.
4 | .DS_Store
5 | # Common VCS dirs
6 | .git/
7 | .gitignore
8 | .bzr/
9 | .bzrignore
10 | .hg/
11 | .hgignore
12 | .svn/
13 | # Common backup files
14 | *.swp
15 | *.bak
16 | *.tmp
17 | *.orig
18 | *~
19 | # Various IDEs
20 | .project
21 | .idea/
22 | *.tmproj
23 | .vscode/
24 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/helm-chart/k8s-e2e-app-helm/charts/app-chart/Chart.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v2
2 | appVersion: 1.0.0
3 | description: A Helm chart for Kubernetes
4 | name: app-chart
5 | type: application
6 | version: 0.1.0
7 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/helm-chart/k8s-e2e-app-helm/charts/app-chart/templates/NOTES.txt:
--------------------------------------------------------------------------------
1 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "app-chart.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
2 | export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
3 | echo "Visit http://127.0.0.1:8080 to use your application"
4 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
5 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/helm-chart/k8s-e2e-app-helm/charts/app-chart/templates/_helpers.tpl:
--------------------------------------------------------------------------------
1 | {{/*
2 | Expand the name of the chart.
3 | */}}
4 | {{- define "app-chart.name" -}}
5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
6 | {{- end }}
7 |
8 | {{/*
9 | Create a default fully qualified app name.
10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
11 | If release name contains chart name it will be used as a full name.
12 | */}}
13 | {{- define "app-chart.fullname" -}}
14 | {{- if .Values.fullnameOverride }}
15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
16 | {{- else }}
17 | {{- $name := default .Chart.Name .Values.nameOverride }}
18 | {{- if contains $name .Release.Name }}
19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }}
20 | {{- else }}
21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
22 | {{- end }}
23 | {{- end }}
24 | {{- end }}
25 |
26 | {{/*
27 | Create chart name and version as used by the chart label.
28 | */}}
29 | {{- define "app-chart.chart" -}}
30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
31 | {{- end }}
32 |
33 | {{/*
34 | Common labels
35 | */}}
36 | {{- define "app-chart.labels" -}}
37 | helm.sh/chart: {{ include "app-chart.chart" . }}
38 | {{ include "app-chart.selectorLabels" . }}
39 | {{- if .Chart.AppVersion }}
40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
41 | {{- end }}
42 | app.kubernetes.io/managed-by: {{ .Release.Service }}
43 | {{- end }}
44 |
45 | {{/*
46 | Selector labels
47 | */}}
48 | {{- define "app-chart.selectorLabels" -}}
49 | app.kubernetes.io/name: {{ include "app-chart.name" . }}
50 | app.kubernetes.io/instance: {{ .Release.Name }}
51 | {{- end }}
52 |
53 | {{/*
54 | Create the name of the service account to use
55 | */}}
56 | {{- define "app-chart.serviceAccountName" -}}
57 | {{- dig "global" "serviceAccountname" "default" (.Values | merge (dict)) }}
58 | {{- end }}
59 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/helm-chart/k8s-e2e-app-helm/charts/app-chart/templates/configmap.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: ConfigMap
3 | metadata:
4 | name: {{ include "app-chart.fullname" . }}-config
5 | data:
6 | cloud_destination: {{ .Values.cloudDestination | quote }}
7 | onpremise_destination: {{ .Values.onPremiseDestination | quote }}
8 | principal_propagation_destination: {{ .Values.principalPropagationDestination | quote }}
--------------------------------------------------------------------------------
/samples/helm-sample-application/helm-chart/k8s-e2e-app-helm/charts/app-chart/templates/deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: {{ include "app-chart.fullname" . }}
5 | labels:
6 | {{- include "app-chart.labels" . | nindent 4 }}
7 | spec:
8 | replicas: 2
9 | selector:
10 | matchLabels:
11 | {{- include "app-chart.selectorLabels" . | nindent 6 }}
12 | template:
13 | metadata:
14 | labels:
15 | {{- include "app-chart.selectorLabels" . | nindent 8 }}
16 | spec:
17 | {{- with .Values.imagePullSecrets }}
18 | imagePullSecrets:
19 | {{- toYaml . | nindent 8 }}
20 | {{- end }}
21 | serviceAccountName: {{ include "app-chart.serviceAccountName" . }}
22 | containers:
23 | - name: {{ .Chart.Name }}
24 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
25 | ports:
26 | - containerPort: 3000
27 | resources:
28 | {{- toYaml .Values.resources | nindent 12 }}
29 | env:
30 | - name: CLOUD_DESTINATION
31 | valueFrom:
32 | configMapKeyRef:
33 | name: {{ include "app-chart.fullname" . }}-config
34 | key: cloud_destination
35 | - name: ONPREMISE_DESTINATION
36 | valueFrom:
37 | configMapKeyRef:
38 | name: {{ include "app-chart.fullname" . }}-config
39 | key: onpremise_destination
40 | - name: PRINCIPAL_PROPAGATION_DESTINATION
41 | valueFrom:
42 | configMapKeyRef:
43 | name: {{ include "app-chart.fullname" . }}-config
44 | key: principal_propagation_destination
45 | volumeMounts:
46 | - name: destination-volume
47 | mountPath: {{ printf "/etc/secrets/sapcp/destination/%s" .Values.destinationBinding | quote }}
48 | readOnly: true
49 | - name: xsuaa-volume
50 | mountPath: {{ printf "/etc/secrets/sapcp/xsuaa/%s" .Values.xsuaaBinding | quote }}
51 | readOnly: true
52 | - name: connectivity-volume
53 | mountPath: {{ printf "/etc/secrets/sapcp/connectivity/%s" .Values.connectivityBinding | quote }}
54 | readOnly: true
55 | volumes:
56 | - name: destination-volume
57 | secret:
58 | secretName: {{ .Values.destinationBinding | quote }}
59 | - name: xsuaa-volume
60 | secret:
61 | secretName: {{ .Values.xsuaaBinding | quote }}
62 | - name: connectivity-volume
63 | secret:
64 | secretName: {{ .Values.connectivityBinding | quote }}
65 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/helm-chart/k8s-e2e-app-helm/charts/app-chart/templates/service.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: {{ include "app-chart.fullname" . }}-svc
5 | labels:
6 | {{- include "app-chart.labels" . | nindent 4 }}
7 | spec:
8 | ports:
9 | - port: 8080
10 | targetPort: 3000
11 | selector:
12 | {{- include "app-chart.selectorLabels" . | nindent 4 }}
13 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/helm-chart/k8s-e2e-app-helm/charts/app-chart/templates/tests/test-connection.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Pod
3 | metadata:
4 | name: "{{ include "app-chart.fullname" . }}-test-connection"
5 | labels:
6 | {{- include "app-chart.labels" . | nindent 4 }}
7 | annotations:
8 | "helm.sh/hook": test
9 | spec:
10 | containers:
11 | - name: wget
12 | image: busybox
13 | command: ['wget']
14 | args: ['{{ include "app-chart.fullname" . }}:8080']
15 | restartPolicy: Never
16 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/helm-chart/k8s-e2e-app-helm/charts/app-chart/values.yaml:
--------------------------------------------------------------------------------
1 | image:
2 | repository: docker-cloudsdk.common.repositories.cloud.sap/k8s-e2e-app
3 | tag: latest
4 | resources:
5 | requests:
6 | memory: "256Mi"
7 | cpu: "500m"
8 | limits:
9 | memory: "512Mi"
10 | cpu: "1000m"
--------------------------------------------------------------------------------
/samples/helm-sample-application/helm-chart/k8s-e2e-app-helm/charts/approuter-chart/.helmignore:
--------------------------------------------------------------------------------
1 | # Patterns to ignore when building packages.
2 | # This supports shell glob matching, relative path matching, and
3 | # negation (prefixed with !). Only one pattern per line.
4 | .DS_Store
5 | # Common VCS dirs
6 | .git/
7 | .gitignore
8 | .bzr/
9 | .bzrignore
10 | .hg/
11 | .hgignore
12 | .svn/
13 | # Common backup files
14 | *.swp
15 | *.bak
16 | *.tmp
17 | *.orig
18 | *~
19 | # Various IDEs
20 | .project
21 | .idea/
22 | *.tmproj
23 | .vscode/
24 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/helm-chart/k8s-e2e-app-helm/charts/approuter-chart/Chart.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v2
2 | appVersion: 1.0.0
3 | description: A Helm chart for Kubernetes
4 | name: approuter-chart
5 | type: application
6 | version: 0.1.0
7 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/helm-chart/k8s-e2e-app-helm/charts/approuter-chart/templates/NOTES.txt:
--------------------------------------------------------------------------------
1 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "approuter-chart.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
2 | export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
3 | echo "Visit http://127.0.0.1:8080 to use your application"
4 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
5 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/helm-chart/k8s-e2e-app-helm/charts/approuter-chart/templates/_helpers.tpl:
--------------------------------------------------------------------------------
1 | {{/*
2 | Expand the name of the chart.
3 | */}}
4 | {{- define "approuter-chart.name" -}}
5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
6 | {{- end }}
7 |
8 | {{/*
9 | Create a default fully qualified app name.
10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
11 | If release name contains chart name it will be used as a full name.
12 | */}}
13 | {{- define "approuter-chart.fullname" -}}
14 | {{- if .Values.fullnameOverride }}
15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
16 | {{- else }}
17 | {{- $name := default .Chart.Name .Values.nameOverride }}
18 | {{- if contains $name .Release.Name }}
19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }}
20 | {{- else }}
21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
22 | {{- end }}
23 | {{- end }}
24 | {{- end }}
25 |
26 | {{/*
27 | Create chart name and version as used by the chart label.
28 | */}}
29 | {{- define "approuter-chart.chart" -}}
30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
31 | {{- end }}
32 |
33 | {{/*
34 | Common labels
35 | */}}
36 | {{- define "approuter-chart.labels" -}}
37 | helm.sh/chart: {{ include "approuter-chart.chart" . }}
38 | {{ include "approuter-chart.selectorLabels" . }}
39 | {{- if .Chart.AppVersion }}
40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
41 | {{- end }}
42 | app.kubernetes.io/managed-by: {{ .Release.Service }}
43 | {{- end }}
44 |
45 | {{/*
46 | Selector labels
47 | */}}
48 | {{- define "approuter-chart.selectorLabels" -}}
49 | app.kubernetes.io/name: {{ include "approuter-chart.name" . }}
50 | app.kubernetes.io/instance: {{ .Release.Name }}
51 | {{- end }}
52 |
53 | {{/*
54 | Create the name of the service account to use
55 | */}}
56 | {{- define "approuter-chart.serviceAccountName" -}}
57 | {{- dig "global" "serviceAccountname" "default" (.Values | merge (dict)) }}
58 | {{- end }}
59 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/helm-chart/k8s-e2e-app-helm/charts/approuter-chart/templates/configmap.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: ConfigMap
3 | metadata:
4 | name: {{ include "approuter-chart.fullname" . }}-config
5 | data:
6 | {{- if .Values.config.idp }}
7 | xs-app.json: {{ printf "{\"welcomeFile\":\"/web-pages/index.html\",\"routes\":[{\"source\":\"/backend-app/(.*)\",\"target\":\"$1\",\"destination\":\"backend\",\"identityProvider\":\"%s\"},{\"source\":\"/web-pages/(.*)\",\"target\":\"$1\",\"localDir\":\"static-resources\",\"identityProvider\":\"%s\"}]}" .Values.config.idp .Values.config.idp | toPrettyJson }}
8 | {{- else }}
9 | xs-app.json: {{ .Values.config.json | toPrettyJson | quote }}
10 | {{ end -}}
11 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/helm-chart/k8s-e2e-app-helm/charts/approuter-chart/templates/deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: {{ include "approuter-chart.fullname" . }}
5 | labels:
6 | {{- include "approuter-chart.labels" . | nindent 4 }}
7 | spec:
8 | replicas: 1
9 | selector:
10 | matchLabels:
11 | {{- include "approuter-chart.selectorLabels" . | nindent 6 }}
12 | template:
13 | metadata:
14 | labels:
15 | {{- include "approuter-chart.selectorLabels" . | nindent 8 }}
16 | spec:
17 | {{- with .Values.imagePullSecrets }}
18 | imagePullSecrets:
19 | {{- toYaml . | nindent 8 }}
20 | {{- end }}
21 | serviceAccountName: {{ include "approuter-chart.serviceAccountName" . }}
22 | containers:
23 | - name: {{ .Chart.Name }}
24 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
25 | ports:
26 | - containerPort: 5000
27 | resources:
28 | {{- toYaml .Values.resources | nindent 12 }}
29 | env:
30 | - name: PORT
31 | value: "5000"
32 | - name: destinations
33 | value: '[{"name":"backend", "url":"http://{{ printf "%s-%s" .Release.Name "app-chart" | trunc 63 | trimSuffix "-" }}-svc:8080/", "forwardAuthToken": true}]'
34 | - name: TENANT_HOST_PATTERN
35 | value: {{ .Values.config.pattern | quote }}
36 | volumeMounts:
37 | - name: xsuaa-volume
38 | mountPath: {{ printf "/etc/secrets/sapcp/xsuaa/%s" .Values.xsuaaBinding | quote}}
39 | readOnly: true
40 | - name: approuter-volume
41 | mountPath: "/usr/src/app/xs-app.json"
42 | subPath: "xs-app.json"
43 | readOnly: true
44 | volumes:
45 | - name: xsuaa-volume
46 | secret:
47 | secretName: {{ .Values.xsuaaBinding | quote}}
48 | - name: approuter-volume
49 | configMap:
50 | name: {{ include "approuter-chart.fullname" . }}-config
51 | items:
52 | - key: xs-app.json
53 | path: xs-app.json
54 |
55 |
56 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/helm-chart/k8s-e2e-app-helm/charts/approuter-chart/templates/service.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: {{ include "approuter-chart.fullname" . }}-svc
5 | labels:
6 | {{- include "approuter-chart.labels" . | nindent 4 }}
7 | spec:
8 | ports:
9 | - port: 8080
10 | targetPort: 5000
11 | selector:
12 | {{- include "approuter-chart.selectorLabels" . | nindent 4 }}
13 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/helm-chart/k8s-e2e-app-helm/charts/approuter-chart/templates/tests/test-connection.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Pod
3 | metadata:
4 | name: "{{ include "approuter-chart.fullname" . }}-test-connection"
5 | labels:
6 | {{- include "approuter-chart.labels" . | nindent 4 }}
7 | annotations:
8 | "helm.sh/hook": test
9 | spec:
10 | containers:
11 | - name: wget
12 | image: busybox
13 | command: ['wget']
14 | args: ['{{ include "approuter-chart.fullname" . }}:8080']
15 | restartPolicy: Never
16 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/helm-chart/k8s-e2e-app-helm/charts/approuter-chart/values.yaml:
--------------------------------------------------------------------------------
1 | xsuaaBinding:
2 | config:
3 | idp:
4 | pattern:
5 | image:
6 | repository: docker-cloudsdk.common.repositories.cloud.sap/k8s-approuter
7 | tag: latest
8 | resources:
9 | requests:
10 | memory: "256Mi"
11 | cpu: "500m"
12 | limits:
13 | memory: "512Mi"
14 | cpu: "1000m"
--------------------------------------------------------------------------------
/samples/helm-sample-application/helm-chart/k8s-e2e-app-helm/templates/NOTES.txt:
--------------------------------------------------------------------------------
1 | 1. These routes were created with this chart:
2 | {{- if .Values.expose.enabled }}
3 | {{- if .Values.expose.ingress.shortRoute }}
4 | - https://{{ .Values.expose.ingress.shortRoute }}
5 | {{- end }}
6 | {{- if .Values.expose.ingress.exposedRoute }}
7 | - https://{{ .Values.expose.ingress.exposedRoute }}
8 | {{- end }}
9 | {{- if .Values.expose.ingress.connectivityProxyRoute }}
10 | - https://{{ .Values.expose.ingress.connectivityProxyRoute }}
11 | {{- end }}
12 | {{- end }}
13 |
14 | Use the following commands to connect to your application from within the cluster:
15 |
16 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name=approuter-chart,app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
17 | export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
18 | echo "Visit http://127.0.0.1:8080 to use your application"
19 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
20 |
21 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/helm-chart/k8s-e2e-app-helm/templates/_helpers.tpl:
--------------------------------------------------------------------------------
1 | {{/*
2 | Expand the name of the chart.
3 | */}}
4 | {{- define "k8s-e2e-app-helm.name" -}}
5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
6 | {{- end }}
7 |
8 | {{/*
9 | Create a default fully qualified app name.
10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
11 | If release name contains chart name it will be used as a full name.
12 | */}}
13 | {{- define "k8s-e2e-app-helm.fullname" -}}
14 | {{- if .Values.fullnameOverride }}
15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
16 | {{- else }}
17 | {{- $name := default .Chart.Name .Values.nameOverride }}
18 | {{- if contains $name .Release.Name }}
19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }}
20 | {{- else }}
21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
22 | {{- end }}
23 | {{- end }}
24 | {{- end }}
25 |
26 | {{/*
27 | Create chart name and version as used by the chart label.
28 | */}}
29 | {{- define "k8s-e2e-app-helm.chart" -}}
30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
31 | {{- end }}
32 |
33 | {{/*
34 | Common labels
35 | */}}
36 | {{- define "k8s-e2e-app-helm.labels" -}}
37 | helm.sh/chart: {{ include "k8s-e2e-app-helm.chart" . }}
38 | {{ include "k8s-e2e-app-helm.selectorLabels" . }}
39 | {{- if .Chart.AppVersion }}
40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
41 | {{- end }}
42 | app.kubernetes.io/managed-by: {{ .Release.Service }}
43 | {{- end }}
44 |
45 | {{/*
46 | Selector labels
47 | */}}
48 | {{- define "k8s-e2e-app-helm.selectorLabels" -}}
49 | app.kubernetes.io/name: {{ include "k8s-e2e-app-helm.name" . }}
50 | app.kubernetes.io/instance: {{ .Release.Name }}
51 | {{- end }}
52 |
53 | {{/*
54 | Create the name of the service account to use
55 | */}}
56 | {{- define "k8s-e2e-app-helm.serviceAccountName" -}}
57 | {{- dig "global" "serviceAccountname" "default" (.Values | merge (dict)) }}
58 | {{- end }}
59 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/helm-chart/k8s-e2e-app-helm/templates/api-gateway.yaml:
--------------------------------------------------------------------------------
1 | {{- if and (.Values.expose.enabled) (eq .Values.expose.environment "kyma") -}}
2 | {{- $name := default "approuter-chart" .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
3 | {{- $fullName := include "k8s-e2e-app-helm.fullname" . -}}
4 | {{- $svcName := index .Values "approuter-chart" "name" | default (printf "%s-%s-svc" .Release.Name $name) | trunc 63 | trimSuffix "-" | quote -}}
5 | {{- $svcPort := dig "approuter-chart" "service" "port" 8080 (.Values | merge (dict)) -}}
6 |
7 | apiVersion: gateway.kyma-project.io/v1beta1
8 | kind: APIRule
9 | metadata:
10 | name: {{ $fullName }}-api-rule
11 | labels:
12 | {{- include "k8s-e2e-app-helm.labels" . | nindent 4 }}
13 | spec:
14 | gateway: kyma-gateway.kyma-system.svc.cluster.local
15 | host: {{ index .Values.expose "api-rule" "host" }}
16 | service:
17 | name: {{ $svcName }}
18 | port: {{ $svcPort }}
19 | rules:
20 | - path: /.*
21 | methods:
22 | - GET
23 | - POST
24 | - DELETE
25 | mutators: []
26 | accessStrategies:
27 | - handler: noop
28 | config: {}
29 |
30 | {{- end }}
31 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/helm-chart/k8s-e2e-app-helm/templates/ingress.yaml:
--------------------------------------------------------------------------------
1 | {{- if and (.Values.expose.enabled) (eq .Values.expose.environment "gardener") -}}
2 | {{- $name := default "approuter-chart" .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
3 | {{- $fullName := include "k8s-e2e-app-helm.fullname" . -}}
4 | {{- $svcName := index .Values "approuter-chart" "name" | default (printf "%s-%s-svc" .Release.Name $name) | trunc 63 | trimSuffix "-" | quote -}}
5 | {{- $svcPort := dig "approuter-chart" "service" "port" 8080 (.Values | merge (dict)) -}}
6 | apiVersion: networking.k8s.io/v1
7 | kind: Ingress
8 | metadata:
9 | name: {{ $fullName }}-ingress
10 | annotations:
11 | nginx.ingress.kubernetes.io/affinity: "cookie"
12 | nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
13 | nginx.ingress.kubernetes.io/session-cookie-name: "JSESSIONID"
14 | {{- if eq .Values.expose.environment "gardener" }}
15 | cert.gardener.cloud/purpose: "managed"
16 | {{- else }}
17 | kubernetes.io/ingress.class: "nginx"
18 | cert-manager.io/cluster-issuer: {{ .Values.expose.ingress.issuer.name | default "letsencrypt-production" | quote }}
19 | {{ end }}
20 | spec:
21 | tls:
22 | - hosts:
23 | {{- if .Values.expose.ingress.shortRoute }}
24 | - {{.Values.expose.ingress.shortRoute | quote }}
25 | {{ end -}}
26 | - {{ .Values.expose.ingress.exposedRoute | quote }}
27 | {{- if .Values.expose.ingress.connectivityProxyRoute }}
28 | - {{ .Values.expose.ingress.connectivityProxyRoute | quote }}
29 | {{ end -}}
30 | secretName: {{ .Values.expose.ingress.secretName | default "tls-secret" | quote }}
31 | rules:
32 | - host: {{ .Values.expose.ingress.exposedRoute | quote }}
33 | http:
34 | paths:
35 | - path: /
36 | pathType: Prefix
37 | backend:
38 | service:
39 | name: {{ $svcName }}
40 | port:
41 | number: {{ $svcPort }}
42 |
43 | {{- end }}
44 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/helm-chart/k8s-e2e-app-helm/templates/issuer.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.expose.enabled -}}
2 | {{- if and (eq .Values.expose.environment "generic") (not .Values.expose.name) -}}
3 | apiVersion: cert-manager.io/v1
4 | kind: ClusterIssuer
5 | metadata:
6 | name: letsencrypt-production
7 | spec:
8 | acme:
9 | # You must replace this email address with your own.
10 | # Let's Encrypt will use this to contact you about expiring
11 | # certificates, and issues related to your account.
12 | email: {{ .Values.expose.ingress.issuer.email | quote }}
13 | server: https://acme-v02.api.letsencrypt.org/directory
14 | privateKeySecretRef:
15 | # Secret resource that will be used to store the account's private key.
16 | name: {{ .Values.expose.ingress.issuer.privateKeySecretRef | default "tls-private-key" | quote }}
17 | # Add a single challenge solver, HTTP01 using nginx
18 | solvers:
19 | - http01:
20 | ingress:
21 | class: nginx
22 | {{- end -}}
23 | {{- end -}}
--------------------------------------------------------------------------------
/samples/helm-sample-application/helm-chart/k8s-e2e-app-helm/values.yaml:
--------------------------------------------------------------------------------
1 | app-chart:
2 | cloudDestination:
3 | onPremiseDestination:
4 | principalPropagationDestination:
5 | destinationBinding:
6 | connectivityBinding:
7 | xsuaaBinding:
8 | imagePullSecrets:
9 | - name:
10 | approuter-chart:
11 | config:
12 | idp:
13 | pattern:
14 | xsuaaBinding:
15 | imagePullSecrets:
16 | - name:
17 | expose:
18 | enabled:
19 | environment:
20 | ingress:
21 | shortRoute:
22 | exposedRoute:
23 | connectivityProxyRoute:
24 | api-rule:
25 | host:
--------------------------------------------------------------------------------
/samples/helm-sample-application/helm-chart/values.yaml:
--------------------------------------------------------------------------------
1 | app-chart:
2 | # A cloud destination with basic authentication
3 | cloudDestination: myCloudDestination
4 | # A onpremise destination with basic authentication
5 | onPremiseDestination: myOnpremiseDestination
6 | # A onpremise destination with principal propagation
7 | principalPropagationDestination: myPrincipalPropagationDestination
8 | destinationBinding: operator-destination-service
9 | connectivityBinding: operator-connectivity-service
10 | xsuaaBinding: operator-xsuaa-service
11 | imagePullSecrets:
12 | # A secret containing the credentials to access your docker registry
13 | - name: docker-registry-secret
14 | approuter-chart:
15 | config:
16 | # An Identity Provider which is in your subaccount
17 | idp: default
18 | # The TENANT_HOST_PATTERN your approuter will use to point at the right subaccount
19 | pattern: (.*).e2e.ingress.cloud-sdk-js.sdktests.shoot.canary.k8s-hana.ondemand.com
20 | xsuaaBinding: operator-xsuaa-service
21 | # A secret containing the credentials to access your docker registry
22 | imagePullSecrets:
23 | - name: docker-registry-secret
24 | expose:
25 | enabled: true
26 | # Your cluster enviroment (see README table for more information)
27 | environment: gardener
28 | ingress:
29 | # Your domain
30 | shortRoute: cloud-sdk-js.sdktests.shoot.canary.k8s-hana.ondemand.com
31 | # The route your approuter will use, with a wildcard to enable multi-tenancy
32 | exposedRoute: "*.e2e.ingress.cloud-sdk-js.sdktests.shoot.canary.k8s-hana.ondemand.com"
33 | # The route your connectivity proxy will use to create a tunnel to your cloud connector
34 | connectivityProxyRoute: connectivitytunnel.ingress.cloud-sdk-js.sdktests.shoot.canary.k8s-hana.ondemand.com
35 | api-rule:
36 | # The hostname for the exposed api-gateway (see README table for more information)
37 | host: e2e-app-
--------------------------------------------------------------------------------
/samples/helm-sample-application/sap-btp-operator/certificates/self-signed-issuer.yml:
--------------------------------------------------------------------------------
1 | apiVersion: cert-manager.io/v1
2 | kind: Issuer
3 | metadata:
4 | name: selfsigned-issuer
5 | spec:
6 | selfSigned: {}
7 | ---
8 | apiVersion: cert-manager.io/v1
9 | kind: ClusterIssuer
10 | metadata:
11 | name: selfsigned-cluster-issuer
12 | spec:
13 | selfSigned: {}
--------------------------------------------------------------------------------
/samples/helm-sample-application/sap-btp-operator/sap-btp-operator-v0.1.9.tgz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SAP-samples/cloud-sdk-js/1bfa08bcdc2694d96bef8798d849afc4c10ec4a8/samples/helm-sample-application/sap-btp-operator/sap-btp-operator-v0.1.9.tgz
--------------------------------------------------------------------------------
/samples/helm-sample-application/sap-btp-operator/services/connectivity/operator-connectivity-binding.yml:
--------------------------------------------------------------------------------
1 | apiVersion: services.cloud.sap.com/v1alpha1
2 | kind: ServiceBinding
3 | metadata:
4 | name: operator-connectivity-service
5 | spec:
6 | serviceInstanceName: operator-connectivity-service
--------------------------------------------------------------------------------
/samples/helm-sample-application/sap-btp-operator/services/connectivity/operator-connectivity-service.yml:
--------------------------------------------------------------------------------
1 | apiVersion: services.cloud.sap.com/v1alpha1
2 | kind: ServiceInstance
3 | metadata:
4 | name: operator-connectivity-service
5 | spec:
6 | serviceOfferingName: connectivity
7 | servicePlanName: connectivity_proxy
8 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/sap-btp-operator/services/destination/operator-destination-binding.yml:
--------------------------------------------------------------------------------
1 | apiVersion: services.cloud.sap.com/v1alpha1
2 | kind: ServiceBinding
3 | metadata:
4 | name: operator-destination-service
5 | spec:
6 | serviceInstanceName: operator-destination-service
--------------------------------------------------------------------------------
/samples/helm-sample-application/sap-btp-operator/services/destination/operator-destination-service.yml:
--------------------------------------------------------------------------------
1 | apiVersion: services.cloud.sap.com/v1alpha1
2 | kind: ServiceInstance
3 | metadata:
4 | name: operator-destination-service
5 | spec:
6 | serviceOfferingName: destination
7 | servicePlanName: lite
8 |
--------------------------------------------------------------------------------
/samples/helm-sample-application/sap-btp-operator/services/xsuaa/operator-xsuaa-binding.yml:
--------------------------------------------------------------------------------
1 | apiVersion: services.cloud.sap.com/v1alpha1
2 | kind: ServiceBinding
3 | metadata:
4 | name: operator-xsuaa-service
5 | spec:
6 | serviceInstanceName: operator-xsuaa-service
--------------------------------------------------------------------------------
/samples/helm-sample-application/sap-btp-operator/services/xsuaa/operator-xsuaa-service.yml:
--------------------------------------------------------------------------------
1 | apiVersion: services.cloud.sap.com/v1alpha1
2 | kind: ServiceInstance
3 | metadata:
4 | name: operator-xsuaa-service
5 | spec:
6 | serviceOfferingName: xsuaa
7 | servicePlanName: application
8 | parameters:
9 | xsappname: kubernetes-xsuaa
10 | tenant-mode: shared
11 | scopes:
12 | - name: "$XSAPPNAME.Callback"
13 | description: "With this scope set, the callbacks for tenant onboarding, offboarding and getDependencies can be called."
14 | grant-as-authority-to-apps :
15 | - $XSAPPNAME(application,sap-provisioning,tenant-onboarding)
16 | role-templates:
17 | - name: TOKEN_EXCHANGE
18 | description: Token exchange
19 | scope-references:
20 | - uaa.user
21 | - name: "MultitenancyCallbackRoleTemplate"
22 | description: "Call callback-services of applications"
23 | scope-references:
24 | - "$XSAPPNAME.Callback"
25 | oauth2-configuration:
26 | grant-types:
27 | - authorization_code
28 | - client_credentials
29 | - password
30 | - refresh_token
31 | - urn:ietf:params:oauth:grant-type:saml2-bearer
32 | - user_token
33 | - client_x509
34 | - urn:ietf:params:oauth:grant-type:jwt-bearer
35 | redirect-uris:
36 | - https://*/**
--------------------------------------------------------------------------------
/samples/helm-sample-application/sap-btp-operator/values.yaml:
--------------------------------------------------------------------------------
1 | manager:
2 | secret:
3 | clientid:
4 | clientsecret:
5 | url: https://service-manager.cfapps.sap.hana.ondemand.com
6 | tokenurl: https://s4sdk.authentication.sap.hana.ondemand.com
7 |
--------------------------------------------------------------------------------
/samples/http-client-examples/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/samples/http-client-examples/README.md:
--------------------------------------------------------------------------------
1 | # HTTP Client Examples
2 |
3 | Examples for using the HTTP Client in SAP Cloud SDK for JavaScript.
4 |
5 | Refer to [the documentation](https://sap.github.io/cloud-sdk/docs/js/features/connectivity/generic-http-client) for a description of HTTP Client.
6 |
7 | The examples are implemented in [http-client.spec.ts](./http-client.spec.ts) and in [http-client-httpbin.spec.ts](./http-client-httpbin.spec.ts).
8 | The purpose of those examples is to provide usable sample code for the examples mentioned the documentation.
9 |
10 | ## Instructions to run locally
11 |
12 | ### Install dependencies
13 |
14 | ```
15 | npm ci
16 | ```
17 |
18 | ### Run tests
19 |
20 | They are built as tests which demonstrate how to use the HTTP Client.
21 |
22 | To run the tests, execute the following command:
23 |
24 | ```
25 | npm run test
26 | ```
27 |
28 | This will start a local [express](https://expressjs.com/)-based HTTP server and perform a few example requests using the HTTP Client.
29 |
--------------------------------------------------------------------------------
/samples/http-client-examples/http-client-httpbin.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | executeHttpRequest,
3 | executeHttpRequestWithOrigin,
4 | ParameterEncoder
5 | } from '@sap-cloud-sdk/http-client';
6 |
7 | /*
8 |
9 | This spec file includes usage examples for the HTTP client that make use of the httpbin.org service.
10 | The examples in this file should be exactly the same as on https://sap.github.io/cloud-sdk/docs/js/features/connectivity/generic-http-client
11 |
12 | */
13 |
14 | describe('HTTP Client Usage Examples using httpbin.org', () => {
15 | it('Show simple HTTP Post without fetchCsrfToken', async () => {
16 | const response = await executeHttpRequest(
17 | {
18 | url: `https://httpbin.org/post`
19 | },
20 | {
21 | method: 'post'
22 | },
23 | {
24 | fetchCsrfToken: false
25 | }
26 | );
27 | expect(response).toBeDefined();
28 | expect(response.status).toBe(200);
29 | });
30 |
31 | it('Customized Parameter Encoding', async () => {
32 | const myCustomParameterEncodingFunction: ParameterEncoder = function (
33 | params: Record
34 | ): Record {
35 | const encodedParams: Record = {};
36 |
37 | for (const k in params) {
38 | // Customize your required encoding logic here
39 | encodedParams[k] = params[k].toString().replace('x', 'y');
40 | }
41 |
42 | return encodedParams;
43 | };
44 |
45 | const response = await executeHttpRequest(
46 | {
47 | url: 'https://httpbin.org/anything'
48 | },
49 | {
50 | method: 'get',
51 | params: {
52 | param1: 'a/bx',
53 | param2: 'x1'
54 | },
55 | // Pass your custom encoding function
56 | parameterEncoder: myCustomParameterEncodingFunction
57 | }
58 | );
59 |
60 | expect(response.data.args).toEqual({
61 | param1: 'a/by',
62 | param2: 'y1'
63 | });
64 | });
65 |
66 | it('executeHttpRequestWithOrigin', async () => {
67 | const response = await executeHttpRequestWithOrigin(
68 | {
69 | url: 'https://httpbin.org/anything'
70 | },
71 | {
72 | method: 'get',
73 | headers: {
74 | custom: { apiKey: 'custom-header' },
75 | requestConfig: { apiKey: 'default-header' }
76 | },
77 | params: {
78 | custom: { myParam: 'custom-param' },
79 | requestConfig: { myParam: 'default-param' }
80 | }
81 | }
82 | );
83 |
84 | expect(response.request.path).toEqual('/anything?myParam=custom-param');
85 | expect(response.data.args).toEqual({
86 | myParam: 'custom-param'
87 | });
88 | });
89 | });
90 |
--------------------------------------------------------------------------------
/samples/http-client-examples/jest.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | clearMocks: true,
3 | preset: 'ts-jest'
4 | };
5 |
--------------------------------------------------------------------------------
/samples/http-client-examples/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@sap-cloud-sdk/samples",
3 | "version": "1.0.0",
4 | "description": "SAP Cloud SDK for JS - Examples on HTTP Client",
5 | "scripts": {
6 | "test": "jest"
7 | },
8 | "license": "Apache-2.0",
9 | "keywords": [],
10 | "author": "",
11 | "dependencies": {
12 | "@sap-cloud-sdk/http-client": "^3.0.0",
13 | "@sap-cloud-sdk/util": "^3.0.0",
14 | "concurrently": "^7.3.0",
15 | "express": "^4.18.1",
16 | "ts-node": "^10.9.1",
17 | "typescript": "~5.7.2"
18 | },
19 | "devDependencies": {
20 | "@types/express": "^4.17.13",
21 | "@types/jest": "^28.1.7",
22 | "@types/node": "^22.0.0",
23 | "jest": "^28.1.3",
24 | "prettier": "^3.4.2",
25 | "ts-jest": "^28.0.8"
26 | }
27 | }
--------------------------------------------------------------------------------
/samples/http-client-examples/server.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | const app = express();
3 |
4 | app.get('', async (req: express.Request, res: express.Response) => {
5 | res.status(200).send();
6 | });
7 |
8 | app.head('/csrf-token', async (req: express.Request, res: express.Response) => {
9 | res
10 | .status(200)
11 | .header('x-csrf-token', 'abc')
12 | .header('set-cookie', 'foo=bar')
13 | .send();
14 | });
15 |
16 | app.post('/csrf-token', async (req: express.Request, res: express.Response) => {
17 | if (req.headers['x-csrf-token'] === 'abc') {
18 | res.send('Request with token');
19 | } else {
20 | res.send('Request without token');
21 | }
22 | });
23 |
24 | app.post('/post-without-csrf-token', async (req: express.Request, res: express.Response) => {
25 | res.send();
26 | });
27 |
28 | app.get('/encoding', async (req: express.Request, res: express.Response) => {
29 | res.send(req.url);
30 | });
31 |
32 | app.get('/origin', async (req: express.Request, res: express.Response) => {
33 | const result = req.headers;
34 | result['requestUrl'] = req.url;
35 | res.send(JSON.stringify(result));
36 | });
37 |
38 | app.get('/ping', async (req: express.Request, res: express.Response) => {
39 | res.send('pong');
40 | });
41 |
42 | export default app;
43 |
--------------------------------------------------------------------------------
/samples/http-client-examples/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig to read more about this file */
4 | "target": "es2017",
5 | "module": "esnext",
6 | "moduleResolution": "node",
7 | "esModuleInterop": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "strict": true,
10 | "skipLibCheck": true
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:14-alpine
2 |
3 | WORKDIR /usr/src/app
4 |
5 | COPY package.json .
6 |
7 | RUN npm install -g npm@latest
8 |
9 | RUN npm install --unsafe-perm --production
10 |
11 | COPY . ./
12 |
13 | EXPOSE 3000
14 | CMD ["npm", "run", "start:prod"]
15 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/README.md:
--------------------------------------------------------------------------------
1 | # SAP Cloud SDK for JS Kubernetes Sample Application
2 |
3 | ## Description
4 | This repository contains our Kubernetes Sample Application.
5 |
6 | The repositories structure is as following:
7 |
8 | - `/approuter` - Contains the approuter, a packaging script and a Dockerfile which is used in the deployment
9 | - `/pipeline` - Contains the pipeline's Dockerfile which contains all tools to run the pipeline
10 | - `/src` - Contains the application's source code with its 4 endpoints for cloud, onpremise and principal propagation
11 | - `/e2e-tests` - Contains the cypress tests that test all endpoints after deployment
12 | - `./github/workflows` - Contains the pipeline that builds, packages, deploys, and tests the application
13 | - `Dockerfile` - Packages the application as Dockerimage to be used in Kubernetes
14 |
15 | If you want to locally trigger the e2e-tests, create a `cypress.env.json` file in the `/e2e-tests` directory containing the credentials for the IdP in the format:
16 |
17 | ```
18 | {
19 | "username": "username",
20 | "password": "password"
21 | "url": "url"
22 | }
23 | ```
24 |
25 | ## Requirements
26 | The minimal requirements are:
27 | - A terminal to execute commands
28 | - A recent version of node and npm installed e.g. node 14 and npm 6
29 | - An IDE or a text editor of your choice
30 |
31 | If you want to explore the possibilities beyond local tests you need:
32 | - Access to a [SAP Business Technology Platform](https://www.sap.com/products/business-technology-platform.html) account
33 | - Entitlement to use resources like service instance creation and application processing units
34 | - Permission to create service instances
35 | - Access to a `Docker` repository
36 | - A Kubernetes Cluster which runs:
37 | - The SAP BTP Operator
38 | - The SAP Connectivity Proxy
39 | ## Download and Installation
40 | To download the application run:
41 |
42 | ```
43 | git clone \
44 | --depth 1 \
45 | --filter=blob:none \
46 | --sparse \
47 | https://github.com/SAP-samples/cloud-sdk-js.git \
48 | ;
49 | cd cloud-sdk-js
50 | git sparse-checkout set samples/k8s-sample-application
51 | ```
52 |
53 | ### Generate oData Client
54 |
55 | The following service definitions in `EDMX` format are already downloaded in the folder `resources/service-specs`:
56 | - [Business Partner service onPremise](https://api.sap.com/api/OP_API_BUSINESS_PARTNER_SRV/overview)
57 | - [Business Partner service cloud](https://api.sap.com/api/API_BUSINESS_PARTNER/overview)
58 |
59 | The clients are generated using the `npm run generate-client` command. This command is executed automatically in the `postinstall` step after you execute `npm i `.
60 |
61 | **Note** These services are licensed under the terms of [SAP API Information License](../../LICENSES/LicenseRef-API-Definition-File-License.txt). This limits their use to development purposes only.
62 |
63 |
64 | ### Deploy to Docker
65 | 1. In the `package.json`, change the `deploy:docker` and `deploy:pipeline` scripts to point at your docker repository.
66 | 2. Change the the `deploy:docker` script in the approuter's `package.json` to point at your docker repository.
67 | 3. Change the `xs-app.json` in the approuter directory to use either an IdP of your choice, or no IdP at all.
68 | 4. Deploy the Docker images to your repository with `npm run deploy:docker` and `npm run deploy:pipeline` in case you want to use the pipeline.
69 |
70 | ### Deploy to Kubernetes
71 | 1. Update the `deployment.yml` to use your Docker images and the `ingress.yml` to use the domain associated with your cluster.
72 | 2. Use the same domain in the `TENANT_HOST_PATTERN` in the approuter's `deployment.yml` which you specified in the `ingress.yml`.
73 | 3. Deploy the services in `k8s_files/operator/services`.
74 | 4. Now you can deploy the application and approuter with the Kubernetes files in `k8s_files/app`, `k8s_files/approuter` and the `ingress.yml` with `kubectl apply -f`.
75 |
76 | ### Code Placeholders
77 | If anything isn't working as intended, search for ``, as all parts that have to be adapted contain this placeholder.
78 |
79 | For more detailed information on Kubernetes deployment, check out our [Kubernetes migration guide](https://sap.github.io/cloud-sdk/docs/js/guides/migrate-sdk-application-from-btp-cf-to-kubernetes).
80 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/approuter/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:14
2 |
3 | # Create app directory
4 | WORKDIR /usr/src/app
5 |
6 | # Install app dependencies
7 | # A wildcard is used to ensure both package.json AND package-lock.json are copied
8 | # where available (npm@5+)
9 | COPY package*.json ./
10 |
11 | RUN npm config set @sap:registry https://registry.npmjs.org
12 | RUN npm install
13 |
14 | # Bundle app source
15 | COPY . .
16 |
17 | EXPOSE 5000
18 | CMD [ "npm", "start" ]
--------------------------------------------------------------------------------
/samples/k8s-sample-application/approuter/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "approuter",
3 | "dependencies": {
4 | "@sap/approuter": "20.2.0",
5 | "package.json": "2.0.1"
6 | },
7 | "scripts": {
8 | "start": "node node_modules/@sap/approuter/approuter.js",
9 | "package": "./package.sh",
10 | "deploy": "npm run package && cf push",
11 | "deploy:docker": "docker build -t docker-cloudsdk.common.repositories.cloud.sap/k8s-approuter:latest . && docker push docker-cloudsdk.common.repositories.cloud.sap/k8s-approuter:latest"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/approuter/static-resources/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Hello World
5 |
6 |
7 | Hello world!
8 |
9 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/approuter/xs-app.json:
--------------------------------------------------------------------------------
1 | {
2 | "welcomeFile": "/web-pages/index.html",
3 | "routes": [
4 | {
5 | "source": "/backend-app/(.*)",
6 | "target": "$1",
7 | "destination": "backend",
8 | "identityProvider": ""
9 | },
10 | {
11 | "source": "/web-pages/(.*)",
12 | "target": "$1",
13 | "localDir": "static-resources",
14 | "identityProvider": ""
15 | }]
16 | }
--------------------------------------------------------------------------------
/samples/k8s-sample-application/e2e-tests/cypress.json:
--------------------------------------------------------------------------------
1 | {"chromeWebSecurity": false}
2 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/e2e-tests/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
6 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/e2e-tests/cypress/integration/e2e.spec.ts:
--------------------------------------------------------------------------------
1 | describe('e2e tests for k8s test app', () => {
2 | function checkEndpoint(endpoint: string) {
3 | cy.visit('https://' + Cypress.env('url'));
4 |
5 | cy.get('#j_username')
6 | .type(Cypress.env('username'))
7 | .get('#j_password')
8 | .type(Cypress.env('password'))
9 | .get('#logOnFormSubmit')
10 | .click();
11 |
12 | cy.request(`backend-app/${endpoint}`).then((resp) => {
13 | expect(resp.status).to.eq(200);
14 | });
15 | }
16 | it('tests cloud endpoint', () => {
17 | checkEndpoint('cloud-business-partner');
18 | });
19 |
20 | it('tests onpremise endpoint', () => {
21 | checkEndpoint('onpremise-business-partner');
22 | });
23 |
24 | it('tests principal propagation endpoint', () => {
25 | checkEndpoint('principal-business-partner');
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/e2e-tests/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | ///
2 | // ***********************************************************
3 | // This example plugins/index.js can be used to load plugins
4 | //
5 | // You can change the location of this file or turn off loading
6 | // the plugins file with the 'pluginsFile' configuration option.
7 | //
8 | // You can read more here:
9 | // https://on.cypress.io/plugins-guide
10 | // ***********************************************************
11 |
12 | // This function is called when a project is opened or re-opened (e.g. due to
13 | // the project's config changing)
14 |
15 | /**
16 | * @type {Cypress.PluginConfig}
17 | */
18 | // eslint-disable-next-line no-unused-vars
19 | module.exports = (on, config) => {
20 | // `on` is used to hook into various events Cypress emits
21 | // `config` is the resolved Cypress config
22 | }
23 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/e2e-tests/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add('login', (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This will overwrite an existing command --
25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
26 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/e2e-tests/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/e2e-tests/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@sap-cloud-sdk/samples",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "scripts": {
6 | "test": "npx cypress run"
7 | },
8 | "author": "",
9 | "license": "Apache-2.0",
10 | "devDependencies": {
11 | "cypress": "^12.0.0",
12 | "typescript": "~5.7.2"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/e2e-tests/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["es5", "dom"],
5 | "types": ["cypress"]
6 | },
7 | "include": ["**/*.ts"]
8 | }
--------------------------------------------------------------------------------
/samples/k8s-sample-application/k8s_files/app/deployment.yml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: sdkjs-e2e-deployment
5 | spec:
6 | replicas: 2
7 | selector:
8 | matchLabels:
9 | app: sdkjs-e2e
10 | template:
11 | metadata:
12 | labels:
13 | app: sdkjs-e2e
14 | spec:
15 | containers:
16 | - name: sdkjs-e2e
17 | # Replace this with the application's docker image, follwing this syntax: Repository/Image:Tag
18 | # Example: /:
19 | image: docker-cloudsdk.docker.repositories.sap.ondemand.com/k8s-e2e-app:latest
20 | resources:
21 | requests:
22 | memory: "256Mi"
23 | cpu: "500m"
24 | limits:
25 | memory: "512Mi"
26 | cpu: "1000m"
27 | ports:
28 | - containerPort: 3000
29 | volumeMounts:
30 | - name: destination-volume
31 | mountPath: "/etc/secrets/sapcp/destination/operator-destination-service"
32 | readOnly: true
33 | - name: xsuaa-volume
34 | mountPath: "/etc/secrets/sapcp/xsuaa/operator-xsuaa-service"
35 | readOnly: true
36 | - name: connectivity-volume
37 | mountPath: "/etc/secrets/sapcp/connectivity/operator-connectivity-service"
38 | readOnly: true
39 | imagePullSecrets:
40 | # Replace this with your docker registry secret
41 | # Example:
42 | - name: docker-registry-secret
43 | volumes:
44 | - name: destination-volume
45 | secret:
46 | secretName: operator-destination-service
47 | - name: xsuaa-volume
48 | secret:
49 | secretName: operator-xsuaa-service
50 | - name: connectivity-volume
51 | secret:
52 | secretName: operator-connectivity-service
--------------------------------------------------------------------------------
/samples/k8s-sample-application/k8s_files/app/k8s-e2e-service.yml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: sdkjs-e2e-svc
5 | spec:
6 | selector:
7 | app: sdkjs-e2e
8 | ports:
9 | - port: 8080
10 | targetPort: 3000
11 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/k8s_files/approuter/approuter-service.yml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | labels:
5 | app: approuter
6 | name: approuter-svc
7 | spec:
8 | ports:
9 | - port: 8080
10 | targetPort: 5000
11 | selector:
12 | app: approuter
--------------------------------------------------------------------------------
/samples/k8s-sample-application/k8s_files/approuter/deployment.yml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: approuter
5 | labels:
6 | app: approuter
7 | spec:
8 | replicas: 1
9 | selector:
10 | matchLabels:
11 | app: approuter
12 | template:
13 | metadata:
14 | labels:
15 | app: approuter
16 | spec:
17 | containers:
18 | - # Replace this with the approuter's docker image, follwing this syntax: Repository/Image:Tag
19 | # Example: /:
20 | image: docker-cloudsdk.docker.repositories.sap.ondemand.com/k8s-approuter:latest
21 | resources:
22 | requests:
23 | memory: "256Mi"
24 | cpu: "250m"
25 | limits:
26 | memory: "512Mi"
27 | cpu: "500m"
28 | name: approuter
29 | volumeMounts:
30 | - name: xsuaa-volume
31 | mountPath: "/etc/secrets/sapcp/xsuaa/operator-xsuaa-service"
32 | readOnly: true
33 | env:
34 | - name: PORT
35 | value: "5000"
36 | - name: destinations
37 | value: '[{"name":"backend", "url":"http://sdkjs-e2e-svc:8080/", "forwardAuthToken": true}]'
38 | - name: TENANT_HOST_PATTERN
39 | # Replace this with your wildcard subdoamin, has to match the ingress, with the pattern: (.*).Subdomain.Domain
40 | # Example: (.*)..
41 | value: (.*).e2e.ingress.cloud-sdk-js.sdktests.shoot.canary.k8s-hana.ondemand.com
42 | imagePullSecrets:
43 | # Replace this with your docker registry secret
44 | # Example:
45 | - name: docker-registry-secret
46 | volumes:
47 | - name: xsuaa-volume
48 | secret:
49 | secretName: operator-xsuaa-service
50 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/k8s_files/ingress.yml:
--------------------------------------------------------------------------------
1 | apiVersion: networking.k8s.io/v1
2 | kind: Ingress
3 | metadata:
4 | name: sdkjs-e2e-ingress
5 | annotations:
6 | nginx.ingress.kubernetes.io/affinity: "cookie"
7 | nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
8 | nginx.ingress.kubernetes.io/session-cookie-name: "JSESSIONID"
9 | cert.gardener.cloud/purpose: managed
10 | spec:
11 | tls:
12 | - hosts:
13 | # Replace with root domain
14 | # Example:
15 | - cloud-sdk-js.sdktests.shoot.canary.k8s-hana.ondemand.com
16 | # Replace with subdomain which includes a wildcard for multi-tenancy, with the pattern: "*.Subdomain.Domain"
17 | # Example: "*.."
18 | - "*.e2e.ingress.cloud-sdk-js.sdktests.shoot.canary.k8s-hana.ondemand.com"
19 | # Replace with domain for the connectivity-proxy, with the pattern: Subdomain.Domain
20 | # Example: .
21 | - connectivitytunnel.ingress.cloud-sdk-js.sdktests.shoot.canary.k8s-hana.ondemand.com
22 | secretName: secret-tls
23 | rules:
24 | # Replace with subdomain which includes a wildcard for multi-tenancy, with the pattern: "*.Subdomain.Domain"
25 | # Example: "*.."
26 | - host: "*.e2e.ingress.cloud-sdk-js.sdktests.shoot.canary.k8s-hana.ondemand.com"
27 | http:
28 | paths:
29 | - path: /
30 | pathType: Prefix
31 | backend:
32 | service:
33 | name: approuter-svc
34 | port:
35 | number: 8080
36 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/k8s_files/operator/certificates/self-signed-issuer.yml:
--------------------------------------------------------------------------------
1 | apiVersion: cert-manager.io/v1
2 | kind: Issuer
3 | metadata:
4 | name: selfsigned-issuer
5 | spec:
6 | selfSigned: {}
7 | ---
8 | apiVersion: cert-manager.io/v1
9 | kind: ClusterIssuer
10 | metadata:
11 | name: selfsigned-cluster-issuer
12 | spec:
13 | selfSigned: {}
--------------------------------------------------------------------------------
/samples/k8s-sample-application/k8s_files/operator/services/connectivity/operator-connectivity-binding.yml:
--------------------------------------------------------------------------------
1 | apiVersion: services.cloud.sap.com/v1alpha1
2 | kind: ServiceBinding
3 | metadata:
4 | name: operator-connectivity-service
5 | spec:
6 | serviceInstanceName: operator-connectivity-service
--------------------------------------------------------------------------------
/samples/k8s-sample-application/k8s_files/operator/services/connectivity/operator-connectivity-service.yml:
--------------------------------------------------------------------------------
1 | apiVersion: services.cloud.sap.com/v1alpha1
2 | kind: ServiceInstance
3 | metadata:
4 | name: operator-connectivity-service
5 | spec:
6 | serviceOfferingName: connectivity
7 | servicePlanName: connectivity_proxy
8 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/k8s_files/operator/services/destination/operator-destination-binding.yml:
--------------------------------------------------------------------------------
1 | apiVersion: services.cloud.sap.com/v1alpha1
2 | kind: ServiceBinding
3 | metadata:
4 | name: operator-destination-service
5 | spec:
6 | serviceInstanceName: operator-destination-service
--------------------------------------------------------------------------------
/samples/k8s-sample-application/k8s_files/operator/services/destination/operator-destination-service.yml:
--------------------------------------------------------------------------------
1 | apiVersion: services.cloud.sap.com/v1alpha1
2 | kind: ServiceInstance
3 | metadata:
4 | name: operator-destination-service
5 | spec:
6 | serviceOfferingName: destination
7 | servicePlanName: lite
8 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/k8s_files/operator/services/xsuaa/operator-xsuaa-binding.yml:
--------------------------------------------------------------------------------
1 | apiVersion: services.cloud.sap.com/v1alpha1
2 | kind: ServiceBinding
3 | metadata:
4 | name: operator-xsuaa-service
5 | spec:
6 | serviceInstanceName: operator-xsuaa-service
--------------------------------------------------------------------------------
/samples/k8s-sample-application/k8s_files/operator/services/xsuaa/operator-xsuaa-service.yml:
--------------------------------------------------------------------------------
1 | apiVersion: services.cloud.sap.com/v1alpha1
2 | kind: ServiceInstance
3 | metadata:
4 | name: operator-xsuaa-service
5 | spec:
6 | serviceOfferingName: xsuaa
7 | servicePlanName: application
8 | parameters:
9 | xsappname: kubernetes-xsuaa
10 | tenant-mode: shared
11 | scopes:
12 | - name: "$XSAPPNAME.Callback"
13 | description: "With this scope set, the callbacks for tenant onboarding, offboarding and getDependencies can be called."
14 | grant-as-authority-to-apps :
15 | - $XSAPPNAME(application,sap-provisioning,tenant-onboarding)
16 | role-templates:
17 | - name: TOKEN_EXCHANGE
18 | description: Token exchange
19 | scope-references:
20 | - uaa.user
21 | - name: "MultitenancyCallbackRoleTemplate"
22 | description: "Call callback-services of applications"
23 | scope-references:
24 | - "$XSAPPNAME.Callback"
25 | oauth2-configuration:
26 | grant-types:
27 | - authorization_code
28 | - client_credentials
29 | - password
30 | - refresh_token
31 | - urn:ietf:params:oauth:grant-type:saml2-bearer
32 | - user_token
33 | - client_x509
34 | - urn:ietf:params:oauth:grant-type:jwt-bearer
35 | redirect-uris:
36 | - https://*/**
--------------------------------------------------------------------------------
/samples/k8s-sample-application/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "collection": "@nestjs/schematics",
3 | "sourceRoot": "src"
4 | }
5 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@sap-cloud-sdk/samples",
3 | "version": "0.0.1",
4 | "description": "SAP Cloud SDK for JS - Sample application",
5 | "private": true,
6 | "license": "Apache-2.0",
7 | "scripts": {
8 | "prebuild": "rimraf dist",
9 | "build": "nest build",
10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
11 | "start": "nest start",
12 | "start:dev": "nest start --watch",
13 | "start:debug": "nest start --debug --watch",
14 | "start:prod": "node dist/main",
15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
16 | "test": "jest",
17 | "test:watch": "jest --watch",
18 | "test:cov": "jest --coverage",
19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
20 | "test:e2e": "jest --config ./test/jest-e2e.json",
21 | "ci-build": "echo \"Use this to compile or minify your application\"",
22 | "deploy": "npm run build && cf push",
23 | "deploy:docker": "npm run build && docker build -t docker-cloudsdk.common.repositories.cloud.sap/k8s-e2e-app:latest . && docker push docker-cloudsdk.common.repositories.cloud.sap/k8s-e2e-app:latest",
24 | "deploy:pipeline": "docker build -t docker-cloudsdk.common.repositories.cloud.sap/sdkjs-k8s-pipeline:latest ./pipeline && docker push docker-cloudsdk.common.repositories.cloud.sap/sdkjs-k8s-pipeline:latest",
25 | "generate-client": "npx generate-odata-client --input resources/service-specs --outputDir src/generated --optionsPerService=resources/service-specs/options-per-service.json --clearOutputDir --transpile=false"
26 | },
27 | "dependencies": {
28 | "@nestjs/common": "^9.4.3",
29 | "@nestjs/core": "^9.4.3",
30 | "@nestjs/platform-express": "^9.4.3",
31 | "@sap-cloud-sdk/connectivity": "^4.0.0",
32 | "@sap-cloud-sdk/odata-v2": "^3.0.0",
33 | "@sap/xsenv": "^3.4.0",
34 | "@sap/xssec": "^3.2.17",
35 | "passport": "^0.7.0",
36 | "reflect-metadata": "^0.1.13",
37 | "rimraf": "^3.0.2",
38 | "rxjs": "^7.8.1",
39 | "webpack": "^5.80.0"
40 | },
41 | "devDependencies": {
42 | "@nestjs/cli": "^9.5.0",
43 | "@nestjs/schematics": "^9.2.0",
44 | "@nestjs/testing": "^9.4.3",
45 | "@sap-cloud-sdk/test-util": "^3.0.0",
46 | "@sap-cloud-sdk/generator": "^3.0.0",
47 | "@types/express": "^4.17.17",
48 | "@types/jest": "^29.5.4",
49 | "@types/node": "^22.10.5",
50 | "@types/supertest": "^2.0.12",
51 | "@typescript-eslint/eslint-plugin": "^5.59.0",
52 | "@typescript-eslint/parser": "^5.59.0",
53 | "eslint": "^8.38.0",
54 | "eslint-config-prettier": "^8.8.0",
55 | "eslint-plugin-prettier": "^4.2.1",
56 | "jest": "29.6.4",
57 | "prettier": "^3.4.2",
58 | "supertest": "^6.3.3",
59 | "ts-jest": "^29.1.1",
60 | "ts-loader": "^9.4.4",
61 | "ts-node": "^10.9.1",
62 | "tsconfig-paths": "^4.2.0",
63 | "typescript": "~5.7.2"
64 | },
65 | "jest": {
66 | "moduleFileExtensions": [
67 | "js",
68 | "json",
69 | "ts"
70 | ],
71 | "rootDir": "src",
72 | "testRegex": ".*\\.spec\\.ts$",
73 | "transform": {
74 | "^.+\\.(t|j)s$": "ts-jest"
75 | },
76 | "collectCoverageFrom": [
77 | "**/*.(t|j)s"
78 | ],
79 | "coverageDirectory": "../coverage",
80 | "testEnvironment": "node"
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/pipeline/Dockerfile:
--------------------------------------------------------------------------------
1 | # Start from the docker image which contains mvn
2 | FROM ubuntu:20.04
3 |
4 | # Install docker
5 | ## Update the apt package index and install packages to allow apt to use a repository over HTTPS:
6 | RUN apt-get update
7 | RUN apt-get install -y \
8 | apt-transport-https \
9 | ca-certificates \
10 | curl \
11 | gnupg \
12 | lsb-release
13 |
14 | ## Add Docker’s official GPG key:
15 | RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
16 |
17 | ## Use the following command to set up the stable repository.
18 | RUN echo \
19 | "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
20 | $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
21 |
22 | ## Install Docker Engine
23 | RUN apt-get update
24 | RUN DEBIAN_FRONTEND=noninteractive apt-get install -y docker-ce docker-ce-cli containerd.io
25 |
26 | # Install git
27 | RUN apt install -y git-all
28 |
29 | # Install kubectl
30 | RUN curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
31 | RUN install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
32 |
33 | # Add core dependencies for cypress
34 | RUN apt-get install -y libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb
35 |
36 | # Install node 14
37 | RUN curl -fsSL https://deb.nodesource.com/setup_14.x | bash -
38 | RUN apt-get install -y nodejs
39 |
40 | # Update npm version to use lockFileV2
41 | RUN npm install -g npm@latest
42 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/resources/service-specs/options-per-service.json:
--------------------------------------------------------------------------------
1 | {
2 | "resources/service-specs/API_BUSINESS_PARTNER.edmx": {
3 | "packageName": "cloud-business-partner-service",
4 | "directoryName": "cloud-business-partner-service",
5 | "basePath": "/sap/opu/odata/sap/API_BUSINESS_PARTNER"
6 | },
7 | "resources/service-specs/OP_API_BUSINESS_PARTNER_SRV.edmx": {
8 | "packageName": "op-business-partner-service",
9 | "directoryName": "op-business-partner-service",
10 | "basePath": "/sap/opu/odata/sap/API_BUSINESS_PARTNER"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/src/app.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { AppController } from './app.controller';
3 | import { AppService } from './app.service';
4 |
5 | describe('AppController', () => {
6 | let appController: AppController;
7 |
8 | beforeEach(async () => {
9 | const app: TestingModule = await Test.createTestingModule({
10 | controllers: [AppController],
11 | providers: [AppService],
12 | }).compile();
13 |
14 | appController = app.get(AppController);
15 | });
16 |
17 | describe('root', () => {
18 | it('should return "Hello World!"', () => {
19 | expect(appController.getHello()).toBe('Hello World!');
20 | });
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/src/app.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get, Req } from '@nestjs/common';
2 | import { BusinessPartner as BusinessPartnerCloud } from './generated/cloud-business-partner-service';
3 | import { BusinessPartner as BusinessPartnerOp } from './generated/op-business-partner-service';
4 | import { OnpremiseBusinessPartnerService } from './onpremise-business-partner/onpremise-business-partner.service';
5 | import { AppService } from './app.service';
6 | import { CloudBusinessPartnerService } from './cloud-business-partner/cloud-business-partner.service';
7 | import { PrincipalBusinessPartnerService } from './principal-business-partner/principal-business-partner.service';
8 | import { Request } from 'express';
9 |
10 | @Controller()
11 | export class AppController {
12 | constructor(
13 | private readonly appService: AppService,
14 | private readonly cloudBusinessPartnerService: CloudBusinessPartnerService,
15 | private readonly onpremiseBusinessPartnerService: OnpremiseBusinessPartnerService,
16 | private readonly principalBusinessPartnerService: PrincipalBusinessPartnerService
17 | ) {}
18 |
19 | @Get()
20 | getHello(): string {
21 | return this.appService.getHello();
22 | }
23 |
24 | @Get('cloud-business-partner')
25 | getCloudBusinessPartner(): Promise {
26 | return this.cloudBusinessPartnerService.getFiveBusinessPartners();
27 | }
28 |
29 | @Get('onpremise-business-partner')
30 | getOnpremiseBusinessPartner(): Promise {
31 | return this.onpremiseBusinessPartnerService.getFiveBusinessPartners();
32 | }
33 |
34 | @Get('principal-business-partner')
35 | getPrincipalBusinessPartner(
36 | @Req() request: Request,
37 | ): Promise {
38 | return this.principalBusinessPartnerService.getFiveBusinessPartners(
39 | request,
40 | );
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { AppController } from './app.controller';
3 | import { AppService } from './app.service';
4 | import { OnpremiseBusinessPartnerService } from './onpremise-business-partner/onpremise-business-partner.service';
5 | import { CloudBusinessPartnerService } from './cloud-business-partner/cloud-business-partner.service';
6 | import { PrincipalBusinessPartnerService } from './principal-business-partner/principal-business-partner.service';
7 |
8 | @Module({
9 | imports: [],
10 | controllers: [AppController],
11 | providers: [
12 | AppService,
13 | OnpremiseBusinessPartnerService,
14 | CloudBusinessPartnerService,
15 | PrincipalBusinessPartnerService
16 | ],
17 | })
18 | export class AppModule {}
19 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/src/app.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 |
3 | @Injectable()
4 | export class AppService {
5 | getHello(): string {
6 | return 'Hello World!';
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/src/cloud-business-partner/cloud-business-partner.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { CloudBusinessPartnerService } from './cloud-business-partner.service';
3 |
4 | describe('CloudBusinessPartnerService', () => {
5 | let service: CloudBusinessPartnerService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [CloudBusinessPartnerService],
10 | }).compile();
11 |
12 | service = module.get(
13 | CloudBusinessPartnerService,
14 | );
15 | });
16 |
17 | it('should be defined', () => {
18 | expect(service).toBeDefined();
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/src/cloud-business-partner/cloud-business-partner.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import {
3 | BusinessPartner,
4 | cloudBusinessPartnerService as businessPartnerService,
5 | } from '../generated/cloud-business-partner-service';
6 | const { businessPartnerApi } = businessPartnerService();
7 |
8 | @Injectable()
9 | export class CloudBusinessPartnerService {
10 | async getFiveBusinessPartners(): Promise {
11 | return businessPartnerApi
12 | .requestBuilder()
13 | .getAll()
14 | .top(5)
15 | // the destination should point at a cloud basic auth destination
16 | // Example:
17 | .execute({ destinationName: 'myCloudDestination' });
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/src/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { AppModule } from './app.module';
3 | import { JWTStrategy } from '@sap/xssec';
4 | import { getServices } from '@sap/xsenv';
5 | import * as passport from 'passport';
6 |
7 | const xsuaa = getServices({ xsuaa: { name: 'operator-xsuaa-service' } }).xsuaa;
8 | passport.use(new JWTStrategy(xsuaa));
9 |
10 | async function bootstrap() {
11 | const app = await NestFactory.create(AppModule);
12 | app.use(passport.initialize());
13 | app.use(passport.authenticate('JWT', { session: false }));
14 | await app.listen(process.env.PORT || 3000);
15 | }
16 | bootstrap();
17 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/src/onpremise-business-partner/onpremise-business-partner.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { OnpremiseBusinessPartnerService } from './onpremise-business-partner.service';
3 |
4 | describe('OnpremiseBusinessPartnerService', () => {
5 | let service: OnpremiseBusinessPartnerService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [OnpremiseBusinessPartnerService],
10 | }).compile();
11 |
12 | service = module.get(
13 | OnpremiseBusinessPartnerService,
14 | );
15 | });
16 |
17 | it('should be defined', () => {
18 | expect(service).toBeDefined();
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/src/onpremise-business-partner/onpremise-business-partner.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import {
3 | BusinessPartner,
4 | opBusinessPartnerService as businessPartnerService,
5 | } from '../generated/op-business-partner-service';
6 | const { businessPartnerApi } = businessPartnerService();
7 |
8 | @Injectable()
9 | export class OnpremiseBusinessPartnerService {
10 | async getFiveBusinessPartners(): Promise {
11 | return businessPartnerApi
12 | .requestBuilder()
13 | .getAll()
14 | .top(5)
15 | // the destination should point at a onpremise basic authentcation destination
16 | // Example:
17 | .execute({ destinationName: 'myOnpremiseDestination' });
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/src/principal-business-partner/principal-business-partner.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { PrincipalBusinessPartnerService } from './principal-business-partner.service';
3 |
4 | describe('PrincipalBusinessPartnerService', () => {
5 | let service: PrincipalBusinessPartnerService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [PrincipalBusinessPartnerService],
10 | }).compile();
11 |
12 | service = module.get(PrincipalBusinessPartnerService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/src/principal-business-partner/principal-business-partner.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import {
3 | BusinessPartner,
4 | opBusinessPartnerService as businessPartnerService,
5 | } from '../generated/op-business-partner-service';
6 | import { retrieveJwt } from '@sap-cloud-sdk/connectivity';
7 | import { Request } from 'express';
8 | const { businessPartnerApi } = businessPartnerService();
9 |
10 | @Injectable()
11 | export class PrincipalBusinessPartnerService {
12 | async getFiveBusinessPartners(request: Request): Promise {
13 | return businessPartnerApi
14 | .requestBuilder()
15 | .getAll()
16 | .top(5)
17 | .execute({
18 | // the destination should point at a principal propagation destination
19 | // Example:
20 | destinationName: 'myPrincipalPropagationDestination',
21 | jwt: retrieveJwt(request),
22 | });
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/systems.json:
--------------------------------------------------------------------------------
1 | {
2 | "systems": [{
3 | "alias": "EXAMPLE",
4 | "uri": "https://example.com"
5 | }]
6 | }
7 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/test/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { INestApplication } from '@nestjs/common';
3 | import * as request from 'supertest';
4 | import { AppModule } from './../src/app.module';
5 |
6 | describe('AppController (e2e)', () => {
7 | let app: INestApplication;
8 |
9 | beforeEach(async () => {
10 | const moduleFixture: TestingModule = await Test.createTestingModule({
11 | imports: [AppModule],
12 | }).compile();
13 |
14 | app = moduleFixture.createNestApplication();
15 | await app.init();
16 | });
17 |
18 | it('/ (GET)', () => {
19 | return request(app.getHttpServer())
20 | .get('/')
21 | .expect(200)
22 | .expect('Hello World!');
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/test/jest-e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "moduleFileExtensions": ["js", "json", "ts"],
3 | "rootDir": ".",
4 | "testEnvironment": "node",
5 | "testRegex": ".e2e-spec.ts$",
6 | "transform": {
7 | "^.+\\.(t|j)s$": "ts-jest"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/samples/k8s-sample-application/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "allowSyntheticDefaultImports": true,
9 | "target": "es2017",
10 | "sourceMap": true,
11 | "outDir": "./dist",
12 | "baseUrl": "./",
13 | "incremental": true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/samples/resilience-examples/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: { node: true, jest: true },
3 | extends: ['@sap-cloud-sdk'],
4 | parser: '@typescript-eslint/parser',
5 | parserOptions: {
6 | project: {
7 | extends: 'tsconfig.json',
8 | include: ['**/*.ts'],
9 | exclude: ['**/generated/**']
10 | },
11 | sourceType: 'module'
12 | },
13 | ignorePatterns: ['generated'],
14 | plugins: ['@typescript-eslint', 'header', 'import', 'prettier'],
15 | rules: {},
16 | overrides: []
17 | };
18 |
--------------------------------------------------------------------------------
/samples/resilience-examples/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/prettierrc",
3 | "singleQuote": true,
4 | "filepath": "*.ts",
5 | "trailingComma": "none",
6 | "arrowParens": "avoid",
7 | "endOfLine": "lf"
8 | }
9 |
--------------------------------------------------------------------------------
/samples/resilience-examples/README.md:
--------------------------------------------------------------------------------
1 | # SAP Cloud SDK for JS Resilience examples
2 |
3 | ## Description
4 |
5 | This folder contains a few simple examples about resilience and the SDK.
6 | It shows the usage of the default resilience middlewares and also how to build and use a custom one.
7 |
8 | There are custom implementation examples of resilience middlewares in the `resilience.ts`.
9 | You find the usage in the `resilience.spec.ts` file.
10 |
11 | ### Generate oData Client
12 |
13 | The [Business Partner service cloud](https://api.sap.com/api/API_BUSINESS_PARTNER/overview) service definitions in `EDMX` format is already downloaded in the folder `resources/service-specs`. The client is generated using the `npm run generate-client` command. This command is executed automatically in the `postinstall` step after you execute `npm i `.
14 |
15 | **Note** These service is licensed under the terms of [SAP API Information License](../../LICENSES/LicenseRef-API-Definition-File-License.txt). This limits its use to development purposes only.
16 |
--------------------------------------------------------------------------------
/samples/resilience-examples/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@sap-cloud-sdk/samples",
3 | "version": "0.0.1",
4 | "description": "SAP Cloud SDK for JS - Examples on Resilience",
5 | "author": "Frank Essenberger",
6 | "private": true,
7 | "license": "Apache-2.0",
8 | "scripts": {
9 | "postinstall": "npm run generate-client",
10 | "lint": "eslint \"src/**/*.ts\" && yarn prettier .",
11 | "lint:fix": "eslint \"src/**/*.ts\" --fix && yarn prettier . -w",
12 | "test": "jest",
13 | "generate-client": "npx generate-odata-client --input resources/service-specs --outputDir src/generated --clearOutputDir --transpile=false --optionsPerService=resources/service-specs/options-per-service.json"
14 | },
15 | "dependencies": {
16 | "@sap-cloud-sdk/odata-v2": "^3.0.0",
17 | "@sap-cloud-sdk/http-client": "^3.0.0",
18 | "@sap-cloud-sdk/connectivity": "^3.0.0",
19 | "@sap-cloud-sdk/util": "^3.0.0",
20 | "@sap-cloud-sdk/resilience": "^3.0.0",
21 | "@sap-cloud-sdk/generator": "^3.0.0"
22 | },
23 | "devDependencies": {
24 | "@types/jest": "^27.4.0",
25 | "@typescript-eslint/eslint-plugin": "^5.21.0",
26 | "@typescript-eslint/parser": "^5.33.1",
27 | "@sap-cloud-sdk/eslint-config": "^3.0.0",
28 | "eslint": "^8.30.0",
29 | "eslint-config-prettier": "^8.0.0",
30 | "eslint-plugin-header": "^3.0.0",
31 | "eslint-plugin-import": "^2.20.2",
32 | "eslint-plugin-jest": "^27.1.7",
33 | "eslint-plugin-jsdoc": "^39.6.4",
34 | "eslint-plugin-prettier": "^4.2.1",
35 | "eslint-plugin-regex": "^1.10.0",
36 | "eslint-plugin-unused-imports": "^2.0.0",
37 | "jest": "29.3.1",
38 | "nock": "^13.2.4",
39 | "prettier": "^3.4.2",
40 | "ts-jest": "^29.0.3",
41 | "ts-loader": "^9.2.6",
42 | "ts-node": "^10.4.0",
43 | "typescript": "~5.7.2"
44 | },
45 | "jest": {
46 | "moduleFileExtensions": [
47 | "js",
48 | "json",
49 | "ts"
50 | ],
51 | "rootDir": "src",
52 | "testRegex": ".*\\.spec\\.ts$",
53 | "transform": {
54 | "^.+\\.(t|j)s$": "ts-jest"
55 | },
56 | "collectCoverageFrom": [
57 | "**/*.(t|j)s"
58 | ],
59 | "coverageDirectory": "../coverage",
60 | "testEnvironment": "node"
61 | }
62 | }
--------------------------------------------------------------------------------
/samples/resilience-examples/resources/service-specs/options-per-service.json:
--------------------------------------------------------------------------------
1 | {
2 | "resources/service-specs/API_BUSINESS_PARTNER.edmx": {
3 | "packageName": "cloud-business-partner-service",
4 | "directoryName": "cloud-business-partner-service",
5 | "basePath": "/sap/opu/odata/sap/API_BUSINESS_PARTNER"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/samples/resilience-examples/src/resilience.ts:
--------------------------------------------------------------------------------
1 | import { executeHttpRequest, HttpMiddleware } from '@sap-cloud-sdk/http-client';
2 | import { HttpDestination } from '@sap-cloud-sdk/connectivity';
3 | import { createLogger } from '@sap-cloud-sdk/util';
4 |
5 | /**
6 | * Middleware calling a fallback system if the initial request fails.
7 | * This is just a sample and in a real implementation you should consider what part of the request config should be forwarded to the new system.
8 | * @param fallbackSystem - System which is called if the initial request fails.
9 | * @returns The middleware adding a fallback.
10 | */
11 | export function fallbackMiddleware(
12 | fallbackSystem: HttpDestination
13 | ): HttpMiddleware {
14 | return options => async arg => {
15 | try {
16 | return await options.fn(arg);
17 | } catch (e) {
18 | return executeHttpRequest(fallbackSystem);
19 | }
20 | };
21 | }
22 |
23 | const logger = createLogger('http-logs');
24 |
25 | /**
26 | * Middleware logging the username if the request fails with 403.
27 | * For this example basic authentication is assumed, but you could also decode a JWT and take the userId from there.
28 | * @returns The logging middleware.
29 | */
30 | export function loggingMiddleware(): HttpMiddleware {
31 | return options => async arg => {
32 | try {
33 | return await options.fn(arg);
34 | } catch (err) {
35 | const [authType, authorizationHeader] = arg.headers['authorization']
36 | .toString()
37 | .split(' ');
38 | if (authType.toLowerCase() === 'basic' && err.response.status === 403) {
39 | const decoded = Buffer.from(authorizationHeader, 'base64').toString(
40 | 'utf8'
41 | );
42 | logger.error(
43 | `The user ${
44 | decoded.split(':')[0]
45 | } is not authorized to do the request.`
46 | );
47 | }
48 | throw err;
49 | }
50 | };
51 | }
52 |
--------------------------------------------------------------------------------
/samples/resilience-examples/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "allowSyntheticDefaultImports": true,
9 | "moduleResolution": "node",
10 | "lib": ["ES2017"],
11 | "esModuleInterop": true,
12 | "target": "es2017",
13 | "sourceMap": true,
14 | "outDir": "./dist",
15 | "baseUrl": "./"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/scripts/clean-install-all-samples.ts:
--------------------------------------------------------------------------------
1 | import execa from 'execa';
2 | import { resolve } from 'path';
3 | import { samples } from './samples-folders';
4 |
5 | /**
6 | * We can not use workspaces because we want single package-lock.json files in each samples.
7 | * Also the e2e test and update repo https://github.tools.sap/cloudsdk/k8s-e2e-app replaces the local pacakge.json files with one with the same name.
8 | */
9 | async function forAll() {
10 | for (let i = 0; i < samples.length; i++) {
11 | const folder = samples[i];
12 | const cwd = resolve(__dirname, '../', folder);
13 | let process = execa('rm', ['-rf', 'node_modules', 'package-lock.json'], {
14 | cwd
15 | });
16 | process.stdout?.on('data', (data: any) => console.log(data.toString()));
17 | await process;
18 | console.log(`Sample ${folder} cleaned`);
19 | process = execa('npm', ['install'], { cwd });
20 | process.stdout?.on('data', (data: any) => console.log(data.toString()));
21 | await process;
22 | console.log(`Sample ${folder} installed`);
23 | }
24 | }
25 |
26 | forAll();
27 |
--------------------------------------------------------------------------------
/scripts/fetch-service-spec-cli.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path';
2 | import { writeFilesToServiceSpecFolder } from './fetch-service-spec';
3 |
4 | /**
5 | * Helper script to fetch the service specs to all samples which need it.
6 | */
7 | const cfSampleApplication = resolve(
8 | __dirname,
9 | '../samples/cf-sample-application/resources/service-specs'
10 | );
11 | writeFilesToServiceSpecFolder(cfSampleApplication);
12 |
13 | const helmSampleApplication = resolve(
14 | __dirname,
15 | '../samples/helm-sample-application/application/resources/service-specs'
16 | );
17 | writeFilesToServiceSpecFolder(helmSampleApplication);
18 |
19 | const k8sSampleApplication = resolve(
20 | __dirname,
21 | '../samples/k8s-sample-application/resources/service-specs'
22 | );
23 | writeFilesToServiceSpecFolder(k8sSampleApplication);
24 |
--------------------------------------------------------------------------------
/scripts/fetch-service-spec.ts:
--------------------------------------------------------------------------------
1 | import Axios, { AxiosResponse } from 'axios';
2 | import * as dotenv from 'dotenv';
3 | import { promises } from 'fs';
4 | import { resolve, join } from 'path';
5 |
6 | dotenv.config();
7 | const { writeFile, rm, mkdir } = promises;
8 |
9 | export const productiveApiHubUrl =
10 | 'https://cloudintegration.hana.ondemand.com/';
11 |
12 | async function fetchApiContent(artifactName: string): Promise {
13 | const requestURL = `odata/1.0/catalog.svc/APIContent.APIs(Name='${artifactName}')/$value?type=EDMX`;
14 | const response = await Axios.request({
15 | method: 'GET',
16 | url: `${productiveApiHubUrl}${requestURL}`,
17 | headers: { authorization: getBasicHeaderFromEnv() }
18 | });
19 | console.info(`Service definition fetched: ${artifactName}.`);
20 | return response.data;
21 | }
22 |
23 | function getBasicHeaderFromEnv(): string {
24 | if (!process.env.apiHubUser) {
25 | console.error('User not given in process.env.apiHubUser');
26 | throw new Error('User not given in process.env.apiHubUser');
27 | }
28 | if (!process.env.apiHubUserPassword) {
29 | console.error('Password not given in process.env.apiHubUserPassword');
30 | throw new Error('Password not given in process.env.apiHubUserPassword');
31 | }
32 | return basicHeader(process.env.apiHubUser, process.env.apiHubUserPassword);
33 | }
34 |
35 | function basicHeader(username: string, password: string): string {
36 | const encoded = Buffer.from(`${username}:${password}`).toString('base64');
37 | return 'Basic ' + encoded;
38 | }
39 | async function writeServiceDefintion(
40 | specRoot: string,
41 | content: string,
42 | fileName: string
43 | ): Promise {
44 | const path = join(specRoot, fileName);
45 | await writeFile(path, content, { encoding: 'utf-8' });
46 | console.info(`Service definition written: ${path}.`);
47 | }
48 |
49 | export async function writeFilesToServiceSpecFolder(specRoot: string) {
50 | await cleanUp(specRoot);
51 | const contentCloud = await fetchApiContent('API_BUSINESS_PARTNER');
52 | await writeServiceDefintion(
53 | specRoot,
54 | contentCloud,
55 | 'API_BUSINESS_PARTNER.edmx'
56 | );
57 | const contentOp = await fetchApiContent('OP_API_BUSINESS_PARTNER_SRV');
58 | await writeServiceDefintion(
59 | specRoot,
60 | contentOp,
61 | 'OP_API_BUSINESS_PARTNER_SRV.edmx'
62 | );
63 | await createServiceMapping(specRoot);
64 | }
65 |
66 | async function cleanUp(specRoot: string) {
67 | await rm(specRoot, { recursive: true });
68 | await mkdir(specRoot);
69 | }
70 |
71 | async function createServiceMapping(specRoot: string) {
72 | const content = {
73 | 'resources/service-specs/API_BUSINESS_PARTNER.edmx': {
74 | directoryName: 'cloud-business-partner-service',
75 | basePath: '/sap/opu/odata/sap/API_BUSINESS_PARTNER',
76 | packageName: 'cloud-business-partner-service'
77 | },
78 | 'resources/service-specs/OP_API_BUSINESS_PARTNER_SRV.edmx': {
79 | directoryName: 'op-business-partner-service',
80 | basePath: '/sap/opu/odata/sap/API_BUSINESS_PARTNER',
81 | packageName: 'op-business-partner-service'
82 | }
83 | };
84 | await writeFile(
85 | join(specRoot, 'options-per-service.json'),
86 | JSON.stringify(content, null, 2),
87 | { encoding: 'utf-8' }
88 | );
89 | }
90 |
--------------------------------------------------------------------------------
/scripts/samples-folders.ts:
--------------------------------------------------------------------------------
1 | export const samples = [
2 | 'samples/cds-sample-application',
3 | 'samples/cf-multi-tenant-application',
4 | 'samples/cf-sample-application',
5 | 'samples/helm-sample-application/application',
6 | 'samples/http-client-examples',
7 | 'samples/k8s-sample-application',
8 | 'samples/resilience-examples'
9 | ];
10 |
--------------------------------------------------------------------------------
/scripts/set-sdk-version.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path';
2 | import execa from 'execa';
3 | import { readFileSync } from 'fs';
4 | import { promises } from 'fs';
5 | import { samples } from './samples-folders';
6 |
7 | const sdkVersionToSet = 'next';
8 | const { writeFile } = promises;
9 |
10 | /**
11 | * We can not use workspaces because we want single package-lock.json files in each samples.
12 | * Also the e2e test and update repo https://github.tools.sap/cloudsdk/k8s-e2e-app replaces the local pacakge.json files with one with the same name.
13 | */
14 | async function forAll() {
15 | for (let i = 0; i < samples.length; i++) {
16 | const packageJsonPath = resolve(
17 | __dirname,
18 | '../',
19 | samples[i],
20 | 'package.json'
21 | );
22 | const jsonContent = JSON.parse(
23 | readFileSync(packageJsonPath, { encoding: 'utf-8' })
24 | );
25 | setSdkVersion(jsonContent.dependencies);
26 | setSdkVersion(jsonContent.devDependencies);
27 | await writeFile(packageJsonPath, JSON.stringify(jsonContent, null, 2), {
28 | encoding: 'utf-8'
29 | });
30 | console.log(`Package.json updated in ${samples[i]}`);
31 | }
32 | }
33 |
34 | function setSdkVersion(dependencies: Record) {
35 | Object.keys(dependencies).forEach(key => {
36 | if (key.startsWith('@sap-cloud-sdk')) {
37 | dependencies[key] = 'next';
38 | }
39 | });
40 | }
41 |
42 | forAll();
43 |
--------------------------------------------------------------------------------