Skip to content

Commit 2792f1e

Browse files
committed
feat: enhance README and add PostgreSQL support with profile-specific model exposure tests
1 parent 466b296 commit 2792f1e

7 files changed

Lines changed: 170 additions & 28 deletions

File tree

cap/.env

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Local development environment variables for CAP
2+
# Default runtime profile uses SQLite.
3+
# For PostgreSQL profile (`npm run pg`), provide credentials via env vars instead of package.json.
4+
CDS_REQUIRES_DB_CREDENTIALS_USER=<postgres-user>
5+
CDS_REQUIRES_DB_CREDENTIALS_PASSWORD=<postgres-password>
6+
7+
# Optional overrides for PostgreSQL profile:
8+
# CDS_REQUIRES_DB_CREDENTIALS_HOST=localhost
9+
# CDS_REQUIRES_DB_CREDENTIALS_PORT=5432
10+
# CDS_REQUIRES_DB_CREDENTIALS_DB=postgres

cap/README.md

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,38 @@ Welcome to your new project.
55
It contains these folders and files, following our recommended project layout:
66

77
File or Folder | Purpose
8-
---------|----------
8+
--------- | ---------
99
`app/` | content for UI frontends goes here
1010
`db/` | your domain models and data go here
1111
`srv/` | your service models and code go here
1212
`package.json` | project metadata and configuration
1313
`readme.md` | this getting started guide
1414

15-
1615
## Next Steps
1716

18-
- Open a new terminal and run `cds watch`
17+
- Open a new terminal and run `cds watch`
1918
- (in VS Code simply choose _**Terminal** > Run Task > cds watch_)
2019
- Start adding content, for example, a [db/schema.cds](db/schema.cds).
2120

22-
2321
## Learn More
2422

25-
Learn more at https://cap.cloud.sap/docs/get-started/.
23+
Learn more at <https://cap.cloud.sap/docs/get-started/>.
24+
25+
## Runtime Profiles in This Project
26+
27+
- **Default / development**: SQLite (`db.sqlite`) with `db/sqlite` model extensions.
28+
- **hybrid** (`npm run watch`): HANA + `db/hana` model extensions (preferred local integration profile).
29+
- **pg** (`npm run pg`): PostgreSQL + `db/postgres` model extensions.
30+
- **production**: HANA + `db/hana` model extensions.
31+
32+
## CDS Configuration Notes
33+
34+
- `cds.requires.queue: true` is intentional (persistent outbox queue).
35+
- `cds.requires.middlewares: true` is intentional (CAP middleware chain enabled).
36+
- PostgreSQL credentials are intentionally **not hardcoded** in `package.json`; provide them via `.env` (see `cap/.env`).
37+
38+
## Validation Commands
39+
40+
- Run full model tests: `npm run test`
41+
- Run profile-scoping regression tests only: `npm run test:profile`
42+
- CI tip: use `npm run test:profile` as a fast gate for profile-specific model/service exposure checks.

cap/db/postgres/index.cds

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using from '../schema';
2+
using {StarWarsPeople} from '../../srv/people-service';
3+
4+
define view star.wars.peopleFirstNamePg as
5+
select from star.wars.People {
6+
key ID,
7+
name,
8+
split_part(name, ' ', 1) as first_name : String
9+
};
10+
11+
extend service StarWarsPeople with {
12+
@readonly
13+
entity peopleFirstNamePg as projection on star.wars.peopleFirstNamePg;
14+
};

cap/db/sqlite/index.cds

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
using {star.wars} from '../schema';
1+
using from '../schema';

cap/package.json

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"load_sqlite": "cds bind --exec node convertData.js --profile sqlite",
3939
"load_pg": "cds bind --exec node convertData.js --profile pg",
4040
"test": "node --env-file=test/test.env --test --test-timeout 60000 test/model.test.js",
41+
"test:profile": "node --env-file=test/test.env --test --test-timeout 60000 --test-name-pattern \"Profile-specific model exposure\" test/model.test.js",
4142
"test:migration": "node --test test/convertData.test.js test/convertData.report.test.js",
4243
"start": "cds-serve",
4344
"watch": "cds watch --profile hybrid --livereload false",
@@ -74,20 +75,22 @@
7475
},
7576
"deploy-format": "hdbtable"
7677
},
78+
"sql": {
79+
"native_hana_associations": false
80+
},
7781
"requires": {
7882
"queue": true,
79-
"[sqlite]": {
80-
"db": {
81-
"kind": "sqlite",
82-
"impl": "@cap-js/sqlite",
83-
"credentials": {
84-
"url": "db.sqlite"
85-
},
86-
"schema_evolution": "auto"
83+
"middlewares": true,
84+
"db": {
85+
"kind": "sqlite",
86+
"impl": "@cap-js/sqlite",
87+
"credentials": {
88+
"url": "db.sqlite"
8789
},
88-
"db-ext": {
89-
"model": "db/sqlite"
90-
}
90+
"schema_evolution": "auto"
91+
},
92+
"db-ext": {
93+
"model": "db/sqlite"
9194
},
9295
"[pg]": {
9396
"db": {
@@ -98,22 +101,30 @@
98101
"credentials": {
99102
"host": "localhost",
100103
"port": 5432,
101-
"user": "postgres",
102-
"password": "postgres",
103104
"db": "postgres"
104105
}
105106
},
106107
"db-ext": {
107-
"model": "db/sqlite"
108+
"model": "db/postgres"
108109
}
109110
},
110-
"middlewares": true,
111-
"db": {
112-
"kind": "hana-cloud",
113-
"impl": "@cap-js/hana"
111+
"[hybrid]": {
112+
"db": {
113+
"kind": "hana-cloud",
114+
"impl": "@cap-js/hana"
115+
},
116+
"db-ext": {
117+
"model": "db/hana"
118+
}
114119
},
115-
"db-ext": {
116-
"model": "db/hana"
120+
"[production]": {
121+
"db": {
122+
"kind": "hana-cloud",
123+
"impl": "@cap-js/hana"
124+
},
125+
"db-ext": {
126+
"model": "db/hana"
127+
}
117128
},
118129
"messaging": {
119130
"[production]": {
@@ -122,7 +133,7 @@
122133
"[development]": {
123134
"kind": "file-based-messaging"
124135
},
125-
"[hybrid!]": {
136+
"[hybrid]": {
126137
"kind": "file-based-messaging"
127138
}
128139
}

cap/srv/people-service.cds

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ service StarWarsPeople @(path : 'StarWarsPeople') {
4646
@readonly : true
4747
entity skinColors @(cds.redirection.target : false) as projection on StarWars.skinColors;
4848

49-
5049
entity Film2People as projection on StarWars.Film2People {
5150
* , people : redirected to People
5251
};

cap/test/model.test.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ process.env.CDS_REQUIRES_DB_CREDENTIALS_URL = ':memory:'
1919

2020
const { describe, it, before, after } = require('node:test')
2121
const assert = require('node:assert/strict')
22+
const { execFileSync } = require('node:child_process')
2223
const cds = require('@sap/cds')
2324

2425
// Convenience shortcuts from the CDS query builder
@@ -627,4 +628,94 @@ describe('Star Wars CDS Model Tests', () => {
627628
assert.equal(data.value[0].homeworld?.name, 'Species Homeworld-speciestest')
628629
})
629630
})
631+
632+
// ─────────────────────────────────────────────────────────────────────────
633+
// Profile-specific model exposure (pg only)
634+
// peopleFirstNamePg is intentionally added via db/postgres model extension
635+
// and must not leak into default/sqlite or hybrid/hana runtime profiles.
636+
// ─────────────────────────────────────────────────────────────────────────
637+
describe('Profile-specific model exposure (pg only)', () => {
638+
it('default sqlite profile does NOT include peopleFirstNamePg artifacts', async () => {
639+
const model = await cds.load('*')
640+
assert.equal(
641+
model.definitions['star.wars.peopleFirstNamePg'] !== undefined,
642+
false,
643+
'star.wars.peopleFirstNamePg must not be present in default sqlite profile'
644+
)
645+
assert.equal(
646+
model.definitions['StarWarsPeople.peopleFirstNamePg'] !== undefined,
647+
false,
648+
'StarWarsPeople.peopleFirstNamePg must not be exposed in default sqlite profile'
649+
)
650+
})
651+
652+
it('pg profile includes both db view and service projection artifacts', () => {
653+
const script = `
654+
process.env.CDS_ENV = 'pg'
655+
const cds = require('@sap/cds')
656+
cds.load('*').then((model) => {
657+
const result = {
658+
view: !!model.definitions['star.wars.peopleFirstNamePg'],
659+
service: !!model.definitions['StarWarsPeople.peopleFirstNamePg']
660+
}
661+
console.log(JSON.stringify(result))
662+
}).catch((err) => {
663+
console.error(err)
664+
process.exit(1)
665+
})
666+
`
667+
668+
const output = execFileSync(process.execPath, ['-e', script], {
669+
cwd: __dirname + '/..',
670+
encoding: 'utf8'
671+
})
672+
673+
const jsonLine = output
674+
.split(/\r?\n/)
675+
.map(l => l.trim())
676+
.filter(Boolean)
677+
.findLast(l => l.startsWith('{') && l.endsWith('}'))
678+
679+
assert.ok(jsonLine, 'Expected JSON result from pg profile model check')
680+
const result = JSON.parse(jsonLine)
681+
682+
assert.equal(result.view, true, 'pg profile should include star.wars.peopleFirstNamePg')
683+
assert.equal(result.service, true, 'pg profile should include StarWarsPeople.peopleFirstNamePg')
684+
})
685+
686+
it('pg profile includes peopleFirstNamePg in StarWarsPeople metadata document', () => {
687+
const script = `
688+
process.env.CDS_ENV = 'pg'
689+
const cds = require('@sap/cds')
690+
691+
;(async () => {
692+
const model = await cds.load('*')
693+
const edmx = cds.compile.to.edmx(model, { service: 'StarWarsPeople' })
694+
console.log(JSON.stringify({
695+
hasEntity: String(edmx).includes('peopleFirstNamePg')
696+
}))
697+
process.exit(0)
698+
})().catch((err) => {
699+
console.error(err)
700+
process.exit(1)
701+
})
702+
`
703+
704+
const output = execFileSync(process.execPath, ['-e', script], {
705+
cwd: __dirname + '/..',
706+
encoding: 'utf8'
707+
})
708+
709+
const jsonLine = output
710+
.split(/\r?\n/)
711+
.map(l => l.trim())
712+
.filter(Boolean)
713+
.findLast(l => l.startsWith('{') && l.endsWith('}'))
714+
715+
assert.ok(jsonLine, 'Expected JSON result from pg profile metadata endpoint check')
716+
const result = JSON.parse(jsonLine)
717+
718+
assert.equal(result.hasEntity, true, 'Expected peopleFirstNamePg in pg profile StarWarsPeople metadata')
719+
})
720+
})
630721
})

0 commit comments

Comments
 (0)