Skip to content
This repository has been archived by the owner on Dec 23, 2021. It is now read-only.

Commit

Permalink
feat: action support for odata model
Browse files Browse the repository at this point in the history
  • Loading branch information
Soontao committed Jul 31, 2020
1 parent 0e255f7 commit b4ece29
Show file tree
Hide file tree
Showing 11 changed files with 97 additions and 51 deletions.
27 changes: 19 additions & 8 deletions src/example/typed_simple_server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import isUndefined from '@newdash/newdash/isUndefined';
import 'reflect-metadata';
import { createConnection } from 'typeorm';
import { BaseODataModel, createTypedODataServer, Edm, odata, ODataColumn, ODataEntitySetName, ODataHttpContext, ODataModel, ODataNavigation } from '../lib';
import { BaseODataModel, createTypedODataServer, Edm, odata, ODataColumn, ODataEntitySetName, ODataHttpContext, ODataModel, ODataNavigation, ResourceNotFoundError } from '../lib';


@ODataModel()
Expand Down Expand Up @@ -69,25 +69,36 @@ class Teacher extends BaseODataModel {

@Edm.Action
async addClass(@Edm.Int32 classId: number, @odata.context ctx: ODataHttpContext) {
// 'this' is bounded odata response object, is not entity instance
console.log(classId)
const repo = await this._getRepository(ctx, Class)
const c = await repo.findOne(classId)
if (isUndefined(c)) {
throw new ResourceNotFoundError(`not found instance class[${classId}]`)
}
c.teacherOneId = this.id
await c.save()
}

@Edm.Collection(Edm.String)
@Edm.Function
async queryClass(@odata.context ctx) {
const qr = await this._getQueryRunner(ctx);
const items = await qr.query(`select name from class where teacherOneId = :id`, [this.id])
return items.map(item => item.name)
}

}

const run = async () => {

const entities = [Student, Class, Teacher, Profile]
const conn = await createConnection({
const server = await createTypedODataServer({
name: 'default',
type: 'sqljs',
synchronize: true,
logging: true,
cache: true,
entities
});

const server = await createTypedODataServer(conn.name, ...entities);
}, ...entities);

const s = server.create(50000);

Expand Down
20 changes: 8 additions & 12 deletions src/lib/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,29 +22,25 @@ export class ServerInternalError extends HttpRequestError {
}

export class NotImplementedError extends HttpRequestError {
static MESSAGE: string = 'Not implemented.';
constructor(message?: string) {
super(501, message || NotImplementedError.MESSAGE);
constructor(message: string = 'Not implemented.') {
super(501, message);
}
}

export class ResourceNotFoundError extends HttpRequestError {
static MESSAGE: string = 'Resource not found.';
constructor() {
super(404, ResourceNotFoundError.MESSAGE);
constructor(message = 'Resource not found.') {
super(404, message);
}
}

export class MethodNotAllowedError extends HttpRequestError {
static MESSAGE: string = 'Method not allowed.';
constructor() {
super(405, MethodNotAllowedError.MESSAGE);
constructor(message = 'Method not allowed.') {
super(405, message);
}
}

export class UnsupportedMediaTypeError extends HttpRequestError {
static MESSAGE: string = 'Unsupported media type.';
constructor() {
super(415, UnsupportedMediaTypeError.MESSAGE);
constructor(message = 'Unsupported media type.') {
super(415, message);
}
}
12 changes: 11 additions & 1 deletion src/lib/processor/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { IODataResult } from '../index';
import * as odata from '../odata';
import { ODataResult } from '../result';
import { ODataHttpContext, ODataServer } from '../server';
import { BaseODataModel } from '../typeorm';
import { isIterator, isPromise, isStream } from '../utils';
import { NavigationPart, ODATA_TYPE, ResourcePathVisitor } from '../visitor';
import { fnCaller } from './fnCaller';
Expand Down Expand Up @@ -1207,8 +1208,16 @@ export class ODataProcessor extends Transform {
// entity bound operation
// e.g. POST /Teachers(1)/Default.addClass {payload}
if (entityBoundOp) {
scope = result.body;

// use original result for typed odata model
if (result.elementType && result.elementType.prototype instanceof BaseODataModel) {
scope = result.getOriginalResult();
} else {
scope = result.body;
}

returnType = <Function>Edm.getReturnType(elementType, boundOpName, this.serverType.container);

if (Edm.isAction(elementType, boundOpName) ||
schemas.some((schema) =>
schema.actions.some((action) =>
Expand Down Expand Up @@ -1454,6 +1463,7 @@ export class ODataProcessor extends Transform {
const context: any = {
'@odata.context': this.options.metadata != ODataMetadataType.none ? this.odataContext : undefined
};

const elementType = result.elementType = jsPrimitiveTypes.indexOf(result.elementType) >= 0 || result.elementType == String || typeof result.elementType != 'function' ? ctrlType : result.elementType;
if (typeof result.body == 'object' && result.body) {
if (typeof result.body['@odata.count'] == 'number') {
Expand Down
8 changes: 8 additions & 0 deletions src/lib/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ export class ODataResult<T = {}> {
contentType: string
stream?: any

private _originalResult?: any

constructor(statusCode: number, contentType?: string, result?: any) {
this.statusCode = statusCode;
if (typeof result != 'undefined') {
Expand All @@ -68,6 +70,11 @@ export class ODataResult<T = {}> {
}
this.contentType = contentType || 'application/json';
}
this._originalResult = result;
}

public getOriginalResult() {
return this._originalResult;
}

static Created = async function Created(result: any, contentType?: string): Promise<ODataResult> {
Expand Down Expand Up @@ -105,4 +112,5 @@ export class ODataResult<T = {}> {
}
return Promise.resolve(new ODataResult(204, contentType));
}

}
4 changes: 2 additions & 2 deletions src/lib/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,9 +335,9 @@ export function ODataErrorHandler(err, _, res, next) {
res.send({
error: {
code: statusCode,
message: err.message
message: err.message,
// logger
// stack: process.env.ODATA_V4_DISABLE_STACKTRACE ? undefined : err.stack
stack: process.env.ODATA_V4_ENABLE_STACKTRACE ? undefined : err.stack
}
});
} else {
Expand Down
3 changes: 2 additions & 1 deletion src/lib/typeorm/connection.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createConnection } from 'typeorm';
import { TypedController } from './controller';
import { BaseODataModel } from './model';

const KEY_CONN_NAME = 'odata:controller:connection';

Expand All @@ -19,7 +20,7 @@ export function withConnection(connectionName: string = 'default') {
* getConnectName for typed controller
* @param target
*/
export function getConnectionName(target: typeof TypedController) {
export function getConnectionName(target: typeof TypedController | typeof BaseODataModel) {
return Reflect.getMetadata(KEY_CONN_NAME, target);
}

Expand Down
2 changes: 1 addition & 1 deletion src/lib/typeorm/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export class TypedController<T extends typeof BaseODataModel = any> extends ODat
data = await repo.find();
}

if (data.length >0) {
if (data.length > 0) {
await this._executeHooks({
context: ctx, hookType: HookType.afterLoad, data
});
Expand Down
25 changes: 23 additions & 2 deletions src/lib/typeorm/model.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
import { BaseEntity } from 'typeorm';

import { BaseEntity, getConnection, Repository } from 'typeorm';
import { ODataHttpContext } from '../server';
import { getConnectionName } from './connection';
import { getOrCreateTransaction } from './transaction';

export class BaseODataModel extends BaseEntity {

protected async _getConnection(ctx: ODataHttpContext) {
return (await this._getQueryRunner(ctx)).connection;
}

protected async _getEntityManager(ctx: ODataHttpContext) {
return (await this._getQueryRunner(ctx)).manager;
}

protected async _getQueryRunner(ctx: ODataHttpContext) {
// @ts-ignore
return getOrCreateTransaction(getConnection(getConnectionName(this.constructor)), ctx);
}

protected async _getRepository(ctx: ODataHttpContext): Promise<Repository<this>>
protected async _getRepository<M extends typeof BaseODataModel>(ctx: ODataHttpContext, entity?: M): Promise<Repository<InstanceType<M>>>
protected async _getRepository(ctx: any, entity?: any) {
return (await this._getConnection(ctx)).getRepository(entity || this.constructor);
}

}
22 changes: 11 additions & 11 deletions src/test/execute.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe('OData execute', () => {
});

return TestServer.execute('/EntitySet(1)', 'GET').then((result) => {
expect(result).toEqual({
expect(result).toMatchObject({
statusCode: 200,
body: {
'@odata.context': 'http://localhost/$metadata#EntitySet/$entity',
Expand All @@ -80,7 +80,7 @@ describe('OData execute', () => {
});

return TestServer.execute('/EntitySet(1)', 'GET').then((result) => {
expect(result).toEqual({
expect(result).toMatchObject({
statusCode: 200,
body: {
'@odata.context': 'http://localhost/$metadata#EntitySet/$entity',
Expand All @@ -100,7 +100,7 @@ describe('OData execute', () => {
statusCode: 204
});
return TestServer.execute("/Products('578f2b8c12eaebabec4af242')/Category", 'GET').then((result) => {
expect(result).toEqual({
expect(result).toMatchObject({
statusCode: 200,
body: extend({
'@odata.context': 'http://localhost/$metadata#Categories/$entity'
Expand Down Expand Up @@ -130,7 +130,7 @@ describe('OData execute', () => {
statusCode: 204
});
return TestServer.execute("/Products('578f2b8c12eaebabec4af242')/Category", 'GET').then((result) => {
expect(result).toEqual({
expect(result).toMatchObject({
statusCode: 200,
body: extend({
'@odata.context': 'http://localhost/$metadata#Categories/$entity'
Expand Down Expand Up @@ -161,7 +161,7 @@ describe('OData execute', () => {
statusCode: 204
});
return TestServer.execute("/Products('578f2b8c12eaebabec4af284')/Category", 'GET').then((result) => {
expect(result).toEqual({
expect(result).toMatchObject({
statusCode: 200,
body: extend({
'@odata.context': 'http://localhost/$metadata#Categories/$entity'
Expand All @@ -182,7 +182,7 @@ describe('OData execute', () => {
statusCode: 204
});
return TestServer.execute("/Products('578f2b8c12eaebabec4af286')/Category", 'GET').then((result) => {
expect(result).toEqual({
expect(result).toMatchObject({
statusCode: 200,
body: extend({
'@odata.context': 'http://localhost/$metadata#Categories/$entity'
Expand Down Expand Up @@ -217,7 +217,7 @@ describe('OData execute', () => {
statusCode: 204
});
return TestServer.execute("/Products('578f2b8c12eaebabec4af286')/Category", 'GET').then((result) => {
expect(result).toEqual({
expect(result).toMatchObject({
statusCode: 200,
body: extend({
'@odata.context': 'http://localhost/$metadata#Categories/$entity'
Expand All @@ -244,7 +244,7 @@ describe('OData execute', () => {
ctx.url = '/EntitySet(1)';
ctx.method = 'GET';
return TestServer.execute(ctx).then((result) => {
expect(result).toEqual({
expect(result).toMatchObject({
statusCode: 200,
body: {
'@odata.context': 'http://localhost/$metadata#EntitySet/$entity',
Expand Down Expand Up @@ -273,7 +273,7 @@ describe('OData execute', () => {
ctx.url = '/EntitySet(1)';
ctx.method = 'GET';
return TestServer.execute(ctx).then((result) => {
expect(result).toEqual({
expect(result).toMatchObject({
statusCode: 200,
body: {
'@odata.context': 'http://localhost/$metadata#EntitySet/$entity',
Expand Down Expand Up @@ -320,7 +320,7 @@ describe('OData execute', () => {
});

it('stream property with ODataStream POST', () => TestServer.execute('/ImagesControllerEntitySet(1)/Data2', 'POST', fs.createReadStream(path.join(__dirname, 'fixtures', 'logo_jaystack.png'))).then((result) => {
expect(result).toEqual({
expect(result).toMatchObject({
statusCode: 204
});
expect(fs.readFileSync(path.join(__dirname, 'fixtures', 'logo_jaystack.png'))).toEqual(fs.readFileSync(path.join(__dirname, 'fixtures', 'tmp.png')));
Expand Down Expand Up @@ -351,7 +351,7 @@ describe('OData execute', () => {
});

it('should return 204 after POST Data2 using generator function that yields stream', () => TestServer.execute('/Images2ControllerEntitySet(1)/Data2', 'POST', fs.createReadStream(path.join(__dirname, 'fixtures', 'logo_jaystack.png'))).then((result) => {
expect(result).toEqual({
expect(result).toMatchObject({
statusCode: 204
});
expect(fs.readFileSync(path.join(__dirname, 'fixtures', 'logo_jaystack.png'))).toEqual(fs.readFileSync(path.join(__dirname, 'fixtures', 'tmp.png')));
Expand Down
11 changes: 5 additions & 6 deletions src/test/projection.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { ODataServer, ODataController, odata, Edm, ODataMetadataType } from '../lib/index';
import * as request from 'request-promise';
import { Edm, odata, ODataController, ODataMetadataType, ODataServer } from '../lib/index';


class Address {
Expand Down Expand Up @@ -45,7 +44,7 @@ class UsersController extends ODataController {
class TestServer extends ODataServer {}

describe('OData projection', () => {
it('should return projected entities when using $select', () => TestServer.execute('/Users?$select=Id').then((result) => expect(result).toEqual({
it('should return projected entities when using $select', () => TestServer.execute('/Users?$select=Id').then((result) => expect(result).toMatchObject({
statusCode: 200,
body: {
'@odata.context': 'http://localhost/$metadata#Users(Id)',
Expand All @@ -58,7 +57,7 @@ describe('OData projection', () => {
elementType: User
})));

it('should return projected entities with complex type when using $select', () => TestServer.execute('/Users?$select=Address').then((result) => expect(result).toEqual({
it('should return projected entities with complex type when using $select', () => TestServer.execute('/Users?$select=Address').then((result) => expect(result).toMatchObject({
statusCode: 200,
body: {
'@odata.context': 'http://localhost/$metadata#Users(Address)',
Expand All @@ -76,7 +75,7 @@ describe('OData projection', () => {
elementType: User
})));

it('should return projected entities with projected complex type when using $select', () => TestServer.execute('/Users?$select=Address/City').then((result) => expect(result).toEqual({
it('should return projected entities with projected complex type when using $select', () => TestServer.execute('/Users?$select=Address/City').then((result) => expect(result).toMatchObject({
statusCode: 200,
body: {
'@odata.context': 'http://localhost/$metadata#Users(Address/City)',
Expand All @@ -94,7 +93,7 @@ describe('OData projection', () => {
it('should return projected entities with projected complex type when using $select and odata.metadata=full', () => TestServer.execute({
url: '/Users?$select=Address/City,Address/Nr',
metadata: ODataMetadataType.full
}).then((result) => expect(result).toEqual({
}).then((result) => expect(result).toMatchObject({
statusCode: 200,
body: {
'@odata.context': 'http://localhost/$metadata#Users(Address/City,Address/Nr)',
Expand Down

0 comments on commit b4ece29

Please sign in to comment.