Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions spec/entitySetFunction.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {serviceRouter} from '@themost/express';
import {TestPassportStrategy} from './passport';
import request from 'supertest';
import { finalizeDataApplication } from './utils';
import { DataConfigurationStrategy } from '@themost/data';

describe('EntitySetFunction', () => {
let app;
Expand All @@ -15,6 +16,12 @@ describe('EntitySetFunction', () => {
app = express();
// create a new instance of data application
const dataApplication = new ExpressDataApplication(path.resolve(__dirname, 'test/config'));
const dataConfiguration = dataApplication.getConfiguration().getStrategy(DataConfigurationStrategy);
// change sqlite database path
const adapter = dataConfiguration.adapters.find((adapter) => adapter.default);
if (adapter.invariantName === 'sqlite') {
adapter.options.database = path.resolve(__dirname, 'test/db/local.db');
}
app.use(express.json({
reviver: dateReviver
}));
Expand Down
8 changes: 2 additions & 6 deletions spec/formatter.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,13 @@ describe('ResponseFormatter', () => {
.set('Accept', 'application/json');
expect(response.status).toBe(200);
expect(response.body).toBeTruthy();
expect(response.body).toEqual({
message: 'hey'
});
expect(response.body.message).toEqual('hey');

response = await request(app)
.get('/message')
expect(response.status).toBe(200);
expect(response.body).toBeTruthy();
expect(response.body).toEqual({
message: 'hey'
});
expect(response.body.message).toEqual('hey');
});

it('should use formatter to get xml', async () => {
Expand Down
7 changes: 7 additions & 0 deletions spec/service.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {serviceRouter, getServiceRouter} from '@themost/express';
import {dateReviver} from '@themost/express';
import { ApplicationService } from '@themost/common';
import { finalizeDataApplication } from './utils';
import { DataConfigurationStrategy } from '@themost/data';

class ServiceRouterExtension extends ApplicationService {
constructor(app) {
Expand Down Expand Up @@ -72,6 +73,12 @@ describe('serviceRouter', () => {
app = express();
// create a new instance of data application
const dataApplication = new ExpressDataApplication(path.resolve(__dirname, 'test/config'));
const dataConfiguration = dataApplication.getConfiguration().getStrategy(DataConfigurationStrategy);
// change sqlite database path
const adapter = dataConfiguration.adapters.find((adapter) => adapter.default);
if (adapter.invariantName === 'sqlite') {
adapter.options.database = path.resolve(__dirname, 'test/db/local.db');
}
app.use(express.json({
reviver: dateReviver
}));
Expand Down
Binary file modified spec/test/db/local.db
Binary file not shown.
165 changes: 165 additions & 0 deletions src/attribute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { ODataModelBuilder } from '@themost/data';

/**
* @abstract
*/
class AppendAttribute {
constructor() {
this.name = 'responseFormatter';
}
/**
*
* @param {Request} req
* @param {Response} res
* @returns {*}
*/
// eslint-disable-next-line no-unused-vars
append(req, res) {
return {};
}

}

class AppendContextAttribute extends AppendAttribute {
constructor() {
super();
this.name = 'contextUrl';
}

/**
*
* @param {Request} req
* @param {Response} res
* @returns {*}
*/
// eslint-disable-next-line no-unused-vars
append(req, res) {
/**
* @type {import('./app').ExpressDataContext}
*/
const context = req.context;
// if entity set is not defined
if (req.entitySet == null) {
// do nothing and exit
return {};
}
// add odata attributes
// @odata.context
let serviceRoot = context.getConfiguration().getSourceAt('settings/builder/serviceRoot');
if (serviceRoot == null) {
// try to get service root from request
const host = req.get('x-forwarded-host') || req.get('host');
const protocol = req.get('x-forwarded-proto') || req.protocol;
/**
* @type {string}
*/
let baseUrl = req.baseUrl || req.path;
if (baseUrl.endsWith('/') === false) {
baseUrl += '/';
}
serviceRoot = new URL(baseUrl, `${protocol}://${host}`).toString();
}
const builder = context.application.getStrategy(ODataModelBuilder);
/**
* @type {URL}
*/
const metadataUrl = new URL('./$metadata', serviceRoot);
// entity set
if (req.route && req.route.path === '/:entitySet/:id') {
metadataUrl.hash = `${req.params.entitySet}/entity`;
// entity
} else if (req.route && req.route.path === '/:entitySet') {
metadataUrl.hash = `${req.params.entitySet}`;
// navigation property
} else if (req.entitySet && req.route && req.route.path === '/:entitySet/:id/:navigationProperty') {
const navigationProperty = builder.getEntitySet(req.entitySet.name).getEntityTypeNavigationProperty(req.params.navigationProperty);
if (navigationProperty) {
const navigationEntitySet = builder.getEntityTypeEntitySet(navigationProperty.type);
metadataUrl.hash = navigationEntitySet ? `${navigationEntitySet.name}/entity` : `${req.params.entitySet}/${req.params.id}/${req.params.navigationProperty}`;
} else {
metadataUrl.hash = `${req.params.entitySet}/${req.params.id}/${req.params.navigationProperty}`;
}
// entity set function
} else if (req.route && req.route.path === '/:entitySet/:entitySetFunction') {
const entitySet = builder.getEntitySet(req.params.entitySet);
const collection = entitySet && entitySet.entityType && entitySet.entityType.collection;
if (collection) {
/**
* get action
* @type {import('@themost/data').FunctionConfiguration}
*/
const entitySetFunction = collection.hasFunction(req.params.entitySetFunction);
// if action returns only one item
if (entitySetFunction && entitySetFunction.returnType) {
// try to find entity set of that item
const entitySetActionReturnSet = builder.getEntityTypeEntitySet(entitySetFunction.returnType);
// and format metadata url hash
if (entitySetActionReturnSet) {
metadataUrl.hash = `${entitySetActionReturnSet.name}/entity`; // e.g. $metadata#People/entity
} else {
metadataUrl.hash = `${entitySetFunction.returnType}`; // e.g. $metadata#String
}
} else if (entitySetFunction && entitySetFunction.returnCollectionType) { // if function returns a collection
// try to find entity set of that collection
const entitySetActionReturnSet = builder.getEntityTypeEntitySet(entitySetFunction.returnCollectionType);
// and format metadata url hash
if (entitySetActionReturnSet) {
metadataUrl.hash = `${entitySetActionReturnSet.name}`; // e.g. $metadata#People
} else {
metadataUrl.hash = `Collection(${entitySetFunction.returnCollectionType})`; // e.g. $metadata#Collection(String)
}
}
}
if (typeof metadataUrl.hash === 'undefined') {
metadataUrl.hash = `${req.params.entitySet}${req.params.entitySetFunction}`;
}
// entity set action
} else if (req.route && req.route.path === '/:entitySet/:entitySetAction') {
// get current entity set
const entitySet = builder.getEntitySet(req.params.entitySet);
// get current entity set collection
const collection = entitySet && entitySet.entityType && entitySet.entityType.collection;
if (collection) {
/**
* get action
* @type {import('@themost/data').ActionConfiguration}
*/
const entitySetAction = collection.hasAction(req.params.entitySetAction);
// if action returns only one item
if (entitySetAction && entitySetAction.returnType) {
// try to find entity set of that item
const entitySetActionReturnSet = builder.getEntityTypeEntitySet(entitySetAction.returnType);
// and format metadata url hash
if (entitySetActionReturnSet) {
metadataUrl.hash = `${entitySetActionReturnSet.name}/entity`; // e.g. $metadata#People/entity
} else {
metadataUrl.hash = `${entitySetAction.returnType}`; // e.g. $metadata#String
}
}
// if action returns a collection
else
if (entitySetAction && entitySetAction.returnCollectionType) {
// try to find entity set of that collection
const entitySetActionReturnSet = builder.getEntityTypeEntitySet(entitySetAction.returnCollectionType);
// and format metadata url hash
if (entitySetActionReturnSet) {
metadataUrl.hash = `${entitySetActionReturnSet.name}`; // e.g. $metadata#People
} else {
metadataUrl.hash = `Collection(${entitySetAction.returnCollectionType})`; // e.g. $metadata#Collection(String)
}
}
}
if (typeof metadataUrl.hash === 'undefined') {
metadataUrl.hash = `${req.params.entitySet}/${req.params.entitySetAction}`;
}
}
return {
'@odata.context': metadataUrl.toString(),
}
}
}

export {
AppendAttribute,
AppendContextAttribute
}
83 changes: 81 additions & 2 deletions src/formatter.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,51 @@
// MOST Web Framework 2.0 Codename Blueshift Copyright (c) 2019-2023, THEMOST LP All rights reserved
import {AbstractMethodError, ApplicationService} from '@themost/common';
import {AbstractMethodError, ApplicationService, HttpBadRequestError, HttpServerError} from '@themost/common';
import { ODataModelBuilder } from '@themost/data';
import {XSerializer} from '@themost/xml';
import {Readable} from 'stream';
import { AppendContextAttribute } from './attribute';

const MimeTypeRegex = '([a-zA-Z*]+)\\/([a-zA-Z0-9\\-*.+]+)';
const MimeTypeParamRegex = '^([a-zA-Z0-9\\-._+]+)="?([a-zA-Z0-9\\-._+]+)"?$';

/**
*
* @param {string} format
* @returns {Map<string,{type: string,subType:string,params:Map<string,string>}>}
*/
function parseFormatParam(format) {
const results = new Map();
if (format == null) {
return results;
}
if (format.length === 0) {
return results;
}
const parts = format.split(',');
return parts.reduce((parts, part) => {
const tokens = part.trim().split(';');
const mimeType = tokens.shift();
const matches = new RegExp(MimeTypeRegex, 'g').exec(mimeType);
if (matches) {
const [,type, subType] = matches;
const params = tokens.reduce((previous, current) => {
const paramMatches = new RegExp(MimeTypeParamRegex, 'g').exec(current);
if (paramMatches) {
const [,name, value] = paramMatches;
previous.set(name, value);
}
return previous;
}, new Map());
parts.set(mimeType, {
type,
subType,
params
});
return parts
}
}, results);
}


class HttpResponseFormatter {
/**
Expand Down Expand Up @@ -44,20 +88,55 @@ class XmlResponseFormatter extends HttpResponseFormatter {
}
}


class JsonResponseFormatter extends HttpResponseFormatter {
/**
*
* @param {Request} req
* @param {Response} res
*/
execute(req, res) {
// get $format query parameter or accept header
const formatParam = req.query.$format || req.get('accept');
// get formats
const formats = parseFormatParam(formatParam);
// get application/json mime type
const mimeType = formats.get('application/json');
// get odata.metadata parameter (the default is minimal)
let metadata = 'minimal';
if (mimeType && mimeType.params.has('odata.metadata')) {
metadata = mimeType.params.get('odata.metadata');
}
/**
* @type {import('./app').ExpressDataContext}
*/
const context = req.context;
if (context == null) {
throw new HttpServerError('Missing context', 'Request context cannot be empty.');
}
const builder = context.application.getStrategy(ODataModelBuilder);
if (builder == null) {
throw new HttpServerError('Missing model builder', 'ODataModelBuilder service has not been initialized.');
}
// if data is empty
if (this.data == null) {
// return no content
return res.status(204).type('application/json').send();
}
res.json(this.data);
res.setHeader('OData-Version', '4.0');
const extraAttributes = {};
if (metadata !== 'none') {
Object.assign(extraAttributes, new AppendContextAttribute().append(req, res));
}
if (Array.isArray(this.data)) {
return res.json(Object.assign(extraAttributes, {
value: this.data
}));
}
res.json(Object.assign(extraAttributes, this.data));

}

}

class ResponseFormatterWrapper {
Expand Down
Loading