Some checks failed
farmcontrol/farmcontrol-api/pipeline/head There was a failure building this commit
165 lines
5.2 KiB
JavaScript
165 lines
5.2 KiB
JavaScript
import log4js from 'log4js';
|
|
import mongoose from 'mongoose';
|
|
import config from '../config.js';
|
|
import { listObjects } from './database.js';
|
|
|
|
const logger = log4js.getLogger('DatabaseOData');
|
|
logger.level = config.server.logLevel;
|
|
|
|
const EXCLUDED_PATHS = ['appPasswordHash', 'secret', '__v'];
|
|
|
|
/** Check if path is an embedded object (has nested schema, not ObjectId ref). Excludes arrays. */
|
|
function isEmbeddedObject(path) {
|
|
if (!path) return false;
|
|
if (path.options?.type === mongoose.Schema.Types.ObjectId) return false;
|
|
if (path.instance === 'Array') return false;
|
|
return path.schema != null || path.caster?.schema != null;
|
|
}
|
|
|
|
/** Get all top-level property keys declared in OData metadata for a schema. */
|
|
function getDeclaredPropertyKeys(schema) {
|
|
if (!schema?.paths) return [];
|
|
return Object.keys(schema.paths)
|
|
.filter((k) => !k.includes('.'))
|
|
.filter((k) => !EXCLUDED_PATHS.includes(k))
|
|
.filter((k) => schema.paths[k]?.options?.select !== false);
|
|
}
|
|
|
|
/**
|
|
* Ensure all declared properties are present in each document. OData clients (e.g. Excel)
|
|
* expect every metadata property to exist; omit none. Use null for missing/invalid values.
|
|
*/
|
|
function normalizeODataResponse(data, schema) {
|
|
const declaredKeys = getDeclaredPropertyKeys(schema);
|
|
if (declaredKeys.length === 0) return data;
|
|
|
|
function normalize(doc) {
|
|
if (!doc || typeof doc !== 'object' || Array.isArray(doc)) return doc;
|
|
const result = { ...doc };
|
|
for (const key of declaredKeys) {
|
|
const val = result[key];
|
|
if (isEmbeddedObject(schema.paths[key])) {
|
|
// ComplexType: object or null only
|
|
const isValid = val != null && typeof val === 'object' && !Array.isArray(val);
|
|
result[key] = isValid ? val : null;
|
|
} else {
|
|
// Primitive, array, etc.: value or null (never omit)
|
|
result[key] = val !== undefined && val !== null ? val : null;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
return Array.isArray(data) ? data.map(normalize) : normalize(data);
|
|
}
|
|
|
|
/**
|
|
* Convert expanded ObjectId refs ({ _id: x }) back to primitive strings for OData.
|
|
* OData metadata declares refs as Edm.String; clients expect primitives, not objects.
|
|
*/
|
|
function compactObjectIdsForOData(data) {
|
|
function isExpandedRef(val) {
|
|
return val && typeof val === 'object' && !Array.isArray(val) && Object.keys(val).length === 1 && '_id' in val;
|
|
}
|
|
function compact(val) {
|
|
if (Array.isArray(val)) return val.map(compact);
|
|
if (isExpandedRef(val)) return val._id?.toString?.() ?? val._id;
|
|
if (val instanceof Date || val instanceof Buffer) return val; // pass through primitives
|
|
if (val && typeof val === 'object') {
|
|
const result = {};
|
|
for (const [k, v] of Object.entries(val)) result[k] = compact(v);
|
|
return result;
|
|
}
|
|
return val;
|
|
}
|
|
return Array.isArray(data) ? data.map(compact) : compact(data);
|
|
}
|
|
|
|
/**
|
|
* Translate filter keys ending with ._id for Mongoose compatibility
|
|
* @param {Object} filter - Filter object (will be mutated)
|
|
*/
|
|
function translateFilterKeys(filter) {
|
|
Object.keys(filter).forEach((key) => {
|
|
if (key.endsWith('._id')) {
|
|
const baseKey = key.slice(0, -4);
|
|
filter[baseKey] = filter[key];
|
|
delete filter[key];
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* List objects with OData-compatible response format.
|
|
* Works like listObjects but returns { @odata.context, @odata.count?, value }.
|
|
*
|
|
* @param {Object} options - Same options as listObjects, plus:
|
|
* @param {boolean} [options.count=false] - When true, include @odata.count (total before $skip/$top)
|
|
* @returns {Promise<Object>} OData-formatted response or { error, code } on failure
|
|
*/
|
|
export const listObjectsOData = async ({
|
|
model,
|
|
populate = [],
|
|
page = 1,
|
|
limit = 25,
|
|
filter = {},
|
|
sort = '',
|
|
order = 'ascend',
|
|
pagination = true,
|
|
project,
|
|
count = false,
|
|
}) => {
|
|
try {
|
|
const filterCopy = JSON.parse(JSON.stringify(filter));
|
|
translateFilterKeys(filterCopy);
|
|
|
|
const [data, total] = await Promise.all([
|
|
listObjects({
|
|
model,
|
|
populate,
|
|
page,
|
|
limit,
|
|
filter: filterCopy,
|
|
sort,
|
|
order,
|
|
pagination,
|
|
project,
|
|
}),
|
|
count ? model.countDocuments(filterCopy) : Promise.resolve(undefined),
|
|
]);
|
|
|
|
if (data?.error) {
|
|
return data;
|
|
}
|
|
|
|
// Strip __v from each document (Mongoose version key - causes OData client issues)
|
|
const sanitizedData = Array.isArray(data)
|
|
? data.map(({ __v, ...doc }) => doc)
|
|
: data;
|
|
|
|
// Convert expanded ObjectId refs ({ _id: x }) to primitive strings - OData clients expect Edm.String
|
|
let odataData = compactObjectIdsForOData(sanitizedData);
|
|
|
|
// Ensure all declared properties are present; use null for missing values (Excel requires this)
|
|
odataData = normalizeODataResponse(odataData, model.schema);
|
|
|
|
const baseUrl = config.app?.urlApi || '';
|
|
const modelName = model.modelName;
|
|
const context = `${baseUrl}/odata/$metadata#${modelName}`;
|
|
|
|
const response = {
|
|
'@odata.context': context,
|
|
value: odataData,
|
|
};
|
|
|
|
if (count && total !== undefined) {
|
|
response['@odata.count'] = total;
|
|
}
|
|
|
|
return response;
|
|
} catch (error) {
|
|
logger.error('OData list error:', error);
|
|
return { error, code: 500 };
|
|
}
|
|
};
|