cap-cds-mongoose
Version:
MongoDB (Mongoose) persistence adapter & deployer for SAP CAP (works like @cap-js/hana / @cap-js/sqlite)
129 lines (109 loc) • 5.37 kB
JavaScript
// lib/adapter.js
const cds = require("@sap/cds");
const { connect } = require("./connection");
const { getModel } = require("./mapper");
class MongooseAdapter extends cds.Service {
constructor(csn, options = {}) {
super(csn);
this.csn = csn;
this.options = options || {};
}
async init() {
// Connect to MongoDB first
await connect(process.env.MONGO_URL);
// register generic handlers
this.on("READ", this._onRead.bind(this));
this.on("CREATE", this._onCreate.bind(this));
this.on("UPDATE", this._onUpdate.bind(this));
this.on("DELETE", this._onDelete.bind(this));
return super.init && super.init();
}
// Helper to resolve model from request target
_resolveModel(req) {
const target = req.target || req._target || req.event && req.event.target;
// target may be entity object or string
let entityName = (typeof target === "string") ? target : (target && target.name);
if (!entityName && req.path && req.path.length) {
// fallback: try first path element
entityName = req.path[0].id || req.path[0].ref && req.path[0].ref[0];
}
// Try to find entity in csn model definitions if needed
const entity = this.csn.definitions && this.csn.definitions[entityName] ? this.csn.definitions[entityName] : req.target;
return getModel(this.csn, entity);
}
async _onRead(req) {
const model = this._resolveModel(req);
// Build a Mongoose query using minimal parts of req.query (where, limit, select)
let q = model.find();
// Where clause: translate simple filters (if CDS query present)
if (req.query && req.query.SELECT && req.query.SELECT.where) {
// naive conversion: let CAP generate filter object from req.query
// fallback: if req.data provides direct where, use it
// For complex filter translation you'd need a dedicated parser - keep basic support here
// If req.data is an object, treat it as where clause
// Many CAP requests will be processed by the framework before hitting adapter. We'll allow req.query to be used if it's object-like.
}
if (req.query && req.query.SELECT && req.query.SELECT.limit) {
q = q.limit(req.query.SELECT.limit);
}
if (req.query && req.query.SELECT && req.query.SELECT.offset) {
q = q.skip(req.query.SELECT.offset);
}
// Handle projections (SELECT columns)
if (req.query && req.query.SELECT && req.query.SELECT.columns) {
// build projection string
const cols = req.query.SELECT.columns.map(c => (c.ref ? c.ref.join(".") : c.as || c)) /* best-effort */;
q = q.select(cols.join(" "));
}
// Handle expand -> use populate
if (req.query && req.query.SELECT && req.query.SELECT.expand) {
// req.query.SELECT.expand is object keyed by association name in CDS AST
const expands = Object.keys(req.query.SELECT.expand);
for (const exp of expands) {
q = q.populate(exp);
}
} else if (req._query && req._query.SELECT && req._query.SELECT.expand) {
// sometimes framework uses _query
const expands = Object.keys(req._query.SELECT.expand);
for (const exp of expands) q = q.populate(exp);
}
// If client provided req.data as filter (simple use-case)
if (req.data && typeof req.data === "object" && Object.keys(req.data).length) {
q = model.find(req.data);
}
const res = await q.lean().exec();
return res;
}
async _onCreate(req) {
const model = this._resolveModel(req);
// allow single or bulk
const payload = Array.isArray(req.data) ? req.data : [req.data];
const docs = await model.insertMany(payload);
// return inserted docs (lean)
return Array.isArray(req.data) ? docs : docs[0];
}
async _onUpdate(req) {
const model = this._resolveModel(req);
// CAP passes keys in req.data (or req.params) depending on call site.
// We'll support:
// - req.data: object with key/fields to change and optional where
// - req.params[0] if CAP used action-like calls.
const where = req.query && req.query.SELECT && req.query.SELECT.where ? req.query.SELECT.where : req.data && req.data.where ? req.data.where : req.params && req.params.length ? req.params[0] : {};
const patch = req.data && req.data._patch ? req.data._patch : req.data && !req.data.where ? req.data : (req.data && req.data.patch) || {};
// If no explicit where, try to use keys from req.data (best-effort)
if ((!where || Object.keys(where).length === 0) && patch && Object.keys(patch).length === 0) {
// Use req.data as patch, remove keys which look like selectors? best-effort
}
// Use updateMany for multiple rows or findOneAndUpdate for single if keys provided
// Simpler approach: attempt updateMany with where and patch
await model.updateMany(where, { $set: patch }).exec();
return {}; // CAP expects some confirmation; adapt if more specific return needed
}
async _onDelete(req) {
const model = this._resolveModel(req);
const where = (req.query && req.query.SELECT && req.query.SELECT.where) || req.data || (req.params && req.params[0]) || {};
await model.deleteMany(where).exec();
return {};
}
}
module.exports = MongooseAdapter;