Skip to content

Commit 26fd512

Browse files
committed
Provide a CAP bookstore sample
1 parent f44dea6 commit 26fd512

11 files changed

Lines changed: 1431 additions & 0 deletions

File tree

cap-bookstore/app/index.html

Lines changed: 700 additions & 0 deletions
Large diffs are not rendered by default.

cap-bookstore/docker/Dockerfile

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Multi-stage build for CAP application
2+
FROM node:20-alpine AS builder
3+
4+
# Set working directory
5+
WORKDIR /app
6+
7+
# Copy package files
8+
COPY package*.json ./
9+
10+
# Install dependencies
11+
RUN npm ci --only=production
12+
13+
FROM node:20-alpine AS runtime
14+
15+
# Install system dependencies
16+
RUN apk add --no-cache dumb-init postgresql-client
17+
18+
# Install @sap/cds-dk globally for production deployment
19+
RUN npm install -g @sap/cds-dk
20+
21+
# Create app user
22+
RUN addgroup -g 1001 -S nodejs && \
23+
adduser -S -u 1001 -G nodejs nodejs
24+
25+
# Set working directory
26+
WORKDIR /app
27+
28+
# Copy built application
29+
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
30+
COPY --chown=nodejs:nodejs . .
31+
32+
# Remove unnecessary files
33+
RUN rm -rf .git .vscode *.md
34+
35+
# Switch to non-root user
36+
USER nodejs
37+
38+
# Expose port
39+
EXPOSE 4004
40+
41+
# Health check
42+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
43+
CMD node -e "require('http').get('http://localhost:4004', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"
44+
45+
# Start application
46+
ENTRYPOINT ["dumb-init", "--"]
47+
CMD ["./start-postgres.sh"]

cap-bookstore/init-db.js

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
const cds = require('@sap/cds');
2+
const { Client } = require('pg');
3+
const fs = require('fs');
4+
const path = require('path');
5+
const { getPostgresConfig } = require('./lib/postgres-config');
6+
7+
// Helper function to parse CSV files
8+
function parseCSV(filePath) {
9+
try {
10+
const content = fs.readFileSync(filePath, 'utf8');
11+
const lines = content.trim().split('\n');
12+
const headers = lines[0].split(',');
13+
14+
return lines.slice(1).map(line => {
15+
const values = line.split(',');
16+
const row = {};
17+
headers.forEach((header, index) => {
18+
let value = values[index];
19+
20+
// Handle quoted values and remove quotes
21+
if (value.startsWith('"') && value.endsWith('"')) {
22+
value = value.slice(1, -1);
23+
}
24+
25+
// Convert empty strings to null
26+
if (value === '') {
27+
value = null;
28+
}
29+
30+
row[header] = value;
31+
});
32+
return row;
33+
});
34+
} catch (error) {
35+
console.warn(`⚠️ Could not read CSV file ${filePath}:`, error.message);
36+
return [];
37+
}
38+
}
39+
40+
async function initializeDatabase() {
41+
try {
42+
console.log('🚀 Starting PostgreSQL database initialization...');
43+
44+
// Direct PostgreSQL connection for initial setup
45+
const client = new Client(getPostgresConfig());
46+
47+
await client.connect();
48+
console.log('✅ Connected to PostgreSQL');
49+
50+
// Check if tables already exist
51+
const tableCheck = await client.query(`
52+
SELECT table_name
53+
FROM information_schema.tables
54+
WHERE table_schema = 'public'
55+
AND table_name LIKE 'sap_%'
56+
`);
57+
58+
if (tableCheck.rows.length > 0) {
59+
console.log('ℹ️ Database already initialized, checking schema migration...');
60+
61+
// Check if old schema exists and needs migration
62+
const oldAuthorColumn = await client.query(`
63+
SELECT column_name
64+
FROM information_schema.columns
65+
WHERE table_name = 'sap_capire_bookstore_authors'
66+
AND column_name = 'birth_date'
67+
`);
68+
69+
const oldBookColumn = await client.query(`
70+
SELECT column_name
71+
FROM information_schema.columns
72+
WHERE table_name = 'sap_capire_bookstore_books'
73+
AND column_name = 'author'
74+
`);
75+
76+
if (oldAuthorColumn.rows.length > 0 || oldBookColumn.rows.length > 0) {
77+
console.log('🔄 Migrating old schema to new schema...');
78+
79+
// Drop old tables to recreate with new structure
80+
await client.query('DROP TABLE IF EXISTS sap_capire_bookstore_books CASCADE');
81+
await client.query('DROP TABLE IF EXISTS sap_capire_bookstore_authors CASCADE');
82+
83+
console.log('🏗️ Creating new database schema...');
84+
await createTables();
85+
} else {
86+
console.log('✅ Schema is up to date');
87+
}
88+
} else {
89+
console.log('🏗️ Creating database schema...');
90+
await createTables();
91+
}
92+
93+
async function createTables() {
94+
95+
// Create Authors table first (referenced by Books)
96+
await client.query(`
97+
CREATE TABLE IF NOT EXISTS sap_capire_bookstore_authors (
98+
id VARCHAR(36) PRIMARY KEY,
99+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
100+
created_by VARCHAR(255),
101+
modified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
102+
modified_by VARCHAR(255),
103+
name VARCHAR(255),
104+
date_of_birth DATE,
105+
nationality VARCHAR(255),
106+
biography TEXT
107+
)
108+
`);
109+
110+
// Create Books table
111+
await client.query(`
112+
CREATE TABLE IF NOT EXISTS sap_capire_bookstore_books (
113+
id VARCHAR(36) PRIMARY KEY,
114+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
115+
created_by VARCHAR(255),
116+
modified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
117+
modified_by VARCHAR(255),
118+
title VARCHAR(255),
119+
author_id VARCHAR(36),
120+
genre VARCHAR(255),
121+
price DECIMAL(10, 2),
122+
currency_code VARCHAR(3) DEFAULT 'USD',
123+
stock INTEGER DEFAULT 0,
124+
description TEXT,
125+
publisher VARCHAR(255),
126+
published_at DATE,
127+
isbn VARCHAR(13),
128+
FOREIGN KEY (author_id) REFERENCES sap_capire_bookstore_authors(id)
129+
)
130+
`);
131+
132+
// Create Currencies table
133+
await client.query(`
134+
CREATE TABLE IF NOT EXISTS sap_common_currencies (
135+
code VARCHAR(3) PRIMARY KEY,
136+
symbol VARCHAR(5),
137+
minor_unit INTEGER,
138+
name VARCHAR(255),
139+
descr VARCHAR(255)
140+
)
141+
`);
142+
143+
console.log('✅ Database schema created');
144+
}
145+
146+
// Load sample data from CSV files
147+
console.log('📊 Loading sample data from CSV files...');
148+
149+
// Load currencies from CSV
150+
const currenciesPath = path.join(__dirname, 'db/data/sap.common-Currencies.csv');
151+
const currenciesData = parseCSV(currenciesPath);
152+
153+
console.log(`💰 Found ${currenciesData.length} currencies in CSV`);
154+
for (const currency of currenciesData) {
155+
await client.query(`
156+
INSERT INTO sap_common_currencies (code, symbol, minor_unit, name, descr)
157+
VALUES ($1, $2, $3, $4, $5)
158+
ON CONFLICT (code) DO NOTHING
159+
`, [currency.code, currency.symbol, parseInt(currency.minorUnit), currency.name, currency.descr]);
160+
}
161+
162+
// Load authors from CSV first (due to foreign key constraint)
163+
const authorsPath = path.join(__dirname, 'db/data/sap.capire.bookstore-Authors.csv');
164+
const authorsData = parseCSV(authorsPath);
165+
166+
console.log(`✍️ Found ${authorsData.length} authors in CSV`);
167+
for (const author of authorsData) {
168+
await client.query(`
169+
INSERT INTO sap_capire_bookstore_authors
170+
(id, name, date_of_birth, nationality, biography, created_by)
171+
VALUES ($1, $2, $3, $4, $5, $6)
172+
ON CONFLICT (id) DO NOTHING
173+
`, [
174+
author.ID,
175+
author.name,
176+
author.dateOfBirth,
177+
author.nationality,
178+
author.biography,
179+
author.createdBy || 'system'
180+
]);
181+
}
182+
183+
// Load books from CSV
184+
const booksPath = path.join(__dirname, 'db/data/sap.capire.bookstore-Books.csv');
185+
const booksData = parseCSV(booksPath);
186+
187+
console.log(`📚 Found ${booksData.length} books in CSV`);
188+
for (const book of booksData) {
189+
await client.query(`
190+
INSERT INTO sap_capire_bookstore_books
191+
(id, title, author_id, genre, price, currency_code, stock, description, publisher, published_at, isbn, created_by)
192+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
193+
ON CONFLICT (id) DO NOTHING
194+
`, [
195+
book.ID,
196+
book.title,
197+
book.author_ID,
198+
book.genre,
199+
parseFloat(book.price),
200+
book.currency_code,
201+
parseInt(book.stock),
202+
book.description,
203+
book.publisher,
204+
book.publishedAt,
205+
book.isbn,
206+
book.createdBy || 'system'
207+
]);
208+
}
209+
210+
await client.end();
211+
212+
// Test CDS connection
213+
console.log('� Testing CDS connection...');
214+
const db = await cds.connect.to('db');
215+
const bookCount = await db.run('SELECT COUNT(*) as count FROM sap_capire_bookstore_books');
216+
console.log(`✅ Database initialization completed! Found ${bookCount[0].count} books.`);
217+
await db.disconnect();
218+
219+
console.log('🎉 Database ready!');
220+
process.exit(0);
221+
222+
} catch (error) {
223+
console.error('❌ Database initialization failed:', error.message);
224+
process.exit(1);
225+
}
226+
}
227+
228+
// Run initialization
229+
initializeDatabase();

cap-bookstore/k8s/apirule.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
apiVersion: gateway.kyma-project.io/v2
2+
kind: APIRule
3+
metadata:
4+
name: bookstore-api
5+
namespace: cap-bookstore
6+
labels:
7+
app: bookstore
8+
spec:
9+
hosts:
10+
- {YOUR_HOSTNAME}
11+
service:
12+
name: bookstore-service
13+
namespace: cap-bookstore
14+
port: 80
15+
gateway: kyma-system/kyma-gateway
16+
rules:
17+
- path: /{**}
18+
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]
19+
noAuth: true

cap-bookstore/k8s/configmap.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
apiVersion: v1
2+
kind: ConfigMap
3+
metadata:
4+
name: bookstore-postgres-config
5+
namespace: cap-bookstore
6+
data:
7+
CDS_REQUIRES_KIND: "postgres"
8+
NODE_ENV: "production"

0 commit comments

Comments
 (0)