sakuraapi / core Goto Github PK
View Code? Open in Web Editor NEWMongoDB and TypeScript MEAN Stack Framework for NodeJS
Home Page: https://blog.sakuraapi.com
License: BSD 3-Clause "New" or "Revised" License
MongoDB and TypeScript MEAN Stack Framework for NodeJS
Home Page: https://blog.sakuraapi.com
License: BSD 3-Clause "New" or "Revised" License
Since @Routable classes now auto bootstrap, there may be circumstances where a user may not want that behavior.
Proposal:
@Routable({
autoRoute: false
})
class SomeClass {}
This is needed for testing purposes.
Maybe: http://inversify.io/
Initial pass at routing based on decorators.
As a developer, I'd like to be able to add routes declaratively via typescript decorators to simplify the wiring up of APIs.
For example:
@routable({
baseUrl: 'test'
})
class Test {
constructor() {
}
@route({
path: '/',
method: 'get'
})
someMethod(req: express.Request, res: express.Response) {
res.status(200).send({});
}
@route({
path: 'someOtherMethod/',
method: 'post'
})
someOtherMethod(req: express.Request, res: express.Response) {
res.status(200).send({});
}
}
This can then be added to the router with sakuraApi.route(Test)
.
Is it sufficient to use TypeScript to type a property as ObjectID or do we need to have an explicit option... something:
@Model({
//...
})
export class SomeModelObject {
@Db({field: 'oid', model: ObjectID})
@Json()
ownerId: ObjectID;
}
Because of work done on #59, @Db
now supports a model
option. This can be augmented such that any property with an @Db({model:...})
decorator could be used for this purpose.
@Db({model: ...})
See also #72
See @Json
for an example of the newly preferred way to handle meta data. See here for an example of what needs refactoring.
Functionality TBD, but basically make sure a property is in a valid state before persisting it to the DB.
There needs to be some kind of story about how middleware gets done....
There needs to be a story about how an @Routable() class's properties can also be persisted to MongoDB.
@Routable({
baseUrl: 'user'
})
class User {
fisrtName: string;
lastName: string;
@Route({
path: '/:id',
method: 'get'
})
getUserById(...) { }
@Route({
method: 'post'
})
saveUser(...) {}
}
How does that thing get persisted?
fromDb and fromJson should be able to take either a single json object or an array.
@Routable({
model: User,
baseUrl: 'user'
beforeAll: [],
afterAll: []
})
class UserApi {
sanitizer(req, res) { }
@Route({
method: 'get',
path: ':id'
before: [ this.sanitizer, this.handleGetBefore ]
after: [ this.handleGetAfter1, this.handleGetAfter2 ]
})
handleGet(req, res) {
}
handleGetBefore(req, res) {}
handleGetAfter1(req, res) {}
handleGetAfter2(req, res) {}
@Route({
before: this.sanitizer
after: [this.getAllRouteHandler, this.handleGetAllAfter]
})
handleGetAll(req, res) {
// this method will get executed first, then it will run the default getAllRouteHandler
// then this.handleGetAllAfter
}
handleGetAllAfter(req, res) {}
}
Right now, you can't do this:
// ...
@Routable()
class SomeClass {
constructor() {
SakuraApi.instance.route(this);
}
}
The reason why is the sakuraApiClassRoutes
array hasn't been added to the object yet. See: routable.ts:Routable(...).
Perhaps this could be refactored so the above scenario can become the pattern for bootstrapping a routable class.
Alternatively, a decorator could do the job:
// ...
@Routable()
@AddToRouter
class SomeClass {
constructor() {
SakuraApi.instance.route(this);
}
}
Another possibility is maybe SakuraApi.instance.route(...) can be called from within Routable(...)??
export class GetSomethingBody {
@Desc('Description name field')
@Required() @Default('')
name: string;
}
@Model()
@Desc('SomeModel is a Model that changes the world')
export class SomeModel {
@Desc('Description of name field')
@Db() @Json()
name: string;
}
@Routable({
baseUrl: 'some-api-class'
})
export class SomeApiClass {
@Route({
authenticator: AuthAudience,
method: 'get',
path: 'get-something',
expects: GetSomethingBody,
returns: [SomeModel, SomeOtherModel]
})
@Desc('get-something is a handler that handles the things that need to be handled')
getHandler(req: Request, res: Response, next: NextFunction) {
}
}
@Route({
path: '/',
method: 'get'
})
somePostHandler(req, res) {
res.Error(new Error('something's wrong'), {
status: 400
});
// or
res.Error(new Error('something's wrong'), 400);
}
I changed the @route path behavior to default an empty path value to the method's name. The implementation needs to be checked to make sure that '/' is still allowed, otherwise you won't be able to route to '/', which is desirable behavior in most circumstances.
I guess there's a question here about what the right behavior is... should @route({}) default path to the method name (e.g., someBaseURI/someMethodName
) or should it default the route to the baseURI (e.g., someBaseURI/
)?
Possibilities:
Option 1:
path:''
= baseURI/
path:null/undefined
(or left out) = baseURI/methodName
Option 2:
path
is always explicitly required or an exception is thrown
Option 3:
path:''
or path:null/undefined
always resolve to baseURI/
Option 4: ???
In his book, "REST API Design Rulebook", Mark Masse proposes the following:
This seems like a good place to start?
SakuraApi
needs to have a property (e.g., dbConnections
) from which @Model
(etc) will be able to grab a reference to a db connection.
This ticket should also handle how the configuration for these db connections is defined (using the SakuraApiConfig
system).
There are a number of directives from #13 that cannot be implemented until #5 is implemented. See the epic:db
label.
@Model
dbConfig
for ModelOption
fromDb
static method to instantiate a Model Object from a DB resultfromDbArray
static method to instantiate an array of Model Objects from DB resultstoDb
static method@Required
-- see #36@Db
@Validate
When developers commit changes, Travis should run unit tests and other necessary checks.
Functions should be outside of the Model return function so that typedoc picks up the documentation comments.
There needs to be some kind of decorator for required fields... the behavior needs to be thought out. It's tied to at least fromJson
... maybe more... see #21.
@Routable({
baseUrl: 'use',
model: User,
suppressApi: ['delete']
})
class UserApi {
@Route({
method: 'get',
model: UserVote
})
getSomethingElse() {
}
}
The idea here is that @Routable
will take a optional model property which can be any Class that's decorated with @Model
. If a model is provided, by default, the @Routable
class will support:
Since the @Routable
optional suppressApi
property has the string delete
provided in its array, a route for DELETE model/ will not be created.
Alternatively, exposeApi
also takes an array of strings, but it defaults to all APIs being suppressed except those explicitly exposed.
APIs include:
It should just be instantiated during the server bootstrap and then get imported elsewhere where needed. This will require changes to various decorators like @Model
and @Routable
.
It's not binding properly for the context of this.
For example: https://localhost:8001/api/v0/
To Do:
Make it easier to isolate where a failure takes place. Using: jasmine-spec-reporter
@Model
dbConfig
for ModelOption
suppressInjection
for ModelOption
toJson
instance method;toJsonString
instance method;fromJson
static method;fromJsonArray
static method;fromDb
static method;@Json
@Default
@Required
@Db
@Private
@Validate
@Route
into a separate file from @Routable
@Route
from @Routable
testsA set of route handlers are setup with an @Routable class:
@Routable()
class SomeSetOfRouteHandlers {
@Route({
path: '/',
method: 'get'
})
someGetHandler(req, res) {
}
@Route({
path: '/',
method: 'post'
})
somePostHandler(req, res) {
}
}
This class is instantiated by the @Routable decorator and added to the Express router through a call to SakuraApi.instance.route(this);
.
Because there's a single instance of this class responsible for route handlers, it's not an appropriate place to represent a model (i.e., there's only one of these @Routable objects handling routes, but each request will be handling different objects that are relevant to various models).
Possible Solution?:
@Model({
// these get injected as a dbConfig property that can be used when
// manually writing to the DB
dbConfig: {
db: 'someDatabase',
collection: 'someCollection'
},
// if not defined, perhaps look at the configs for a dbModelBindings.json file
// (db.ModelBindings.dev.json, etc)
suppressInjection: ['create']
})
class User {
id: ObjectId; // object Id will have timestamp capabilities
// & uuid generation on instantiation
// the id property will automatically be recognized as
// being the _id for MongoDB
// perhaps these decorators create getters and setters that manipulate an
// injected _propertyName variable and decorate the getter with various
// functions that implement the Decorator behaviors?
@Required
@json('fname') @db('fn')
firstName: string;
@Required
@json({'name: "fname"', excludeNull: true})
@db({field: 'fn', excludeNull: true})
lastName: string;
@Required @Validate(emailValidator)
@db('em')
email: string;
@Private // i.e., filter this before sending to client
ssn: string;
constructor(json: any) {
json = json || {}; // perhaps this can be done in the @Model logic
this.firstName = json.firstName || '';
this.lastName = json.lastName || '';
this.email = json.email || '';
}
save() : Promise<null> {
// persist to MongoDB
super.save();
// if nothing's added, you don't have to override the save()
// method.
// There will need to be some kind of DataSource definition that binds an
// @Model to a specific DB / Collection... perhaps that can be done in
// the @Model({})
}
static getById(id) : Promise<User> {
// retrieve from MongoDB
}
static get(filter:any) : Promise<User[]> {
// get list from MongoDB
}
deleteById(id) : Promise<null> {
// delete from MongoDB
}
emailValidator(email:string) {
return (...meets some proper condition...) ? true : false;
}
}
Then, from within the @Routable class @route method:
@Route({
path: '/',
method: 'post',
model: User
})
somePostHandler(req, res) {
req.model.save();
// where .model was patched onto req automatically.
}
@Route({
path: '/',
method: 'get'
})
somePostHandler(req, res) {
res.send(200).json(User.get(req.query.id));
// this requires a lot more thought... what about error handling, etc?
}
@model({
dbConfig: {
db: 'userdb',
collection: 'users',
indexes: [
{
spec: 'email',
options: {
unique: true
}
}
]
}
})
SomeClass {
}
Implement the initial project structure, testing, etc. I.e., get to the point where you have a basic project foundation on which to build.
https://travis-ci.org/sakuraapi/api/builds/220353206
It's passing tests locally...
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.