diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..646ac51 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +node_modules/ diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..0a9a962 --- /dev/null +++ b/.npmignore @@ -0,0 +1,2 @@ +.DS_Store +.git* diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e69de29 diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..e69de29 diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..e69de29 diff --git a/example/examples.js b/example/examples.js new file mode 100644 index 0000000..7b48421 --- /dev/null +++ b/example/examples.js @@ -0,0 +1,82 @@ +// old atempts, left for some ideas + + +User = typedef({ + id: + name: + username: +}); + +// warn about unused args +// warn about 404s + +// try non-declarative way! +// start from single url definition + +// result (or message) format: +{ + meta: { + httpResponseCode: Number + }, + error: Object, // usually an exception thrown + data: Object, + objects: { + Object: Object, // id-to-object map of referenced objects + } +} +// any object can have _type property, indicating type +// api method can return: +new HintedResult({ + data: Object, + objects: { + objectTypeString: { + objectId: object + } + }, + objectIds: { + objectTypeString: [objectsId,] + } +}) // to hint upper middleware to provide referenced objects along with response +var unwrap = function(possibleHinted) { + if (possibleHinted instanceof HintedResult) + { + return possibleHinted.data; + } + + return possibleHinted; +} + +// possible errors list +// multiple queries in one sharing common dict of referenced objects + +{ + path: 'user', // list of users + req: [auth], + opt: {read: [range]}, + read: [User.username.opt, {_returns: List(User._short)}], + update: [User.except('password'), {_returns: User.id}], + + code: require('./api/user').UserList +} + +{ + path: 'user/:id', // particular user + args: {id: User.id}, + req: [auth], + read: {_returns: User.except('password')}, + update: User.except('email', 'password'), + del: {}, + props: [User.username, User.name], + + code: require('./api/user').User +} + +{ + path: 'user/:id/email', // user's email + args: {id: User.id}, + req: [auth, secure], + call: [ctx.authInfo, args.id, + User.email, User.password], + + code: require('./api/user').User.updateEmail, +} diff --git a/lib/app.js b/lib/app.js new file mode 100644 index 0000000..e6dc016 --- /dev/null +++ b/lib/app.js @@ -0,0 +1,64 @@ +var abstractMethod = require('./tools/abstract_method').abstractMethod; + +var Lowlevel = require('./lowlevel'); +var Units = require('./units').Units; + + +var App = function () { + this.config = null; + this.lowlevel = null; + this.units = null; + this.contract = null; +}; + +App.prototype.defineConfig = abstractMethod; +App.prototype.applyContract = abstractMethod; + +App.prototype.start = function () { + this.lowlevel.start(); +}; + +App.prototype.init = function () { + this.defineConfig(); + this.prepareLowlevel(); + this.prepareUnits(); + this.applyContract(); +}; + +App.prototype.prepareLowlevel = function () { + this.defineLowlevel(); + this.initLowlevel(); +}; + +App.prototype.defineLowlevel = function () { + this.lowlevel = new Lowlevel(this.config); +}; + +App.prototype.initLowlevel = function () { + this.lowlevel.init(); +}; + +App.prototype.prepareUnits = function () { + this.defineUnits(); + this.addUnits(); + this.initUnits(); +}; + +App.prototype.defineUnits = function () { + this.units = new Units(); +}; + +App.prototype.addUnits = function () { +}; + +App.prototype.initUnits = function () { + this.units.init(); +}; + +App.prototype.setContract = function (contract) { + this.contract = contract; + this.lowlevel.setHandler(contract); +}; + + +module.exports = App; diff --git a/lib/contract.js b/lib/contract.js new file mode 100644 index 0000000..de1352e --- /dev/null +++ b/lib/contract.js @@ -0,0 +1,45 @@ +var Resource = require('./resource').Resource; + + +var Contract = function (path) { + this.path = path; + this.items = []; +}; + +Contract.prototype.add = function (item) { + if (typeof item === 'object') + { + item = new Resource(item); + } + + this.items.push(item); +}; + +Contract.prototype.handle = function (ctx, next) { + var handlerChain = this.resolve(ctx); + if (handlerChain != null) + { + handlerChain.execute(ctx, next); + } + else + { + next(); + } +}; + +Contract.prototype.resolve = function (ctx) { + for (var k in this.items) + { + var item = this.items[k]; + var result = item.resolve(ctx); + if (result != null) + { + return result; + } + } + + return null; +}; + + +module.exports = Contract; diff --git a/lib/ctx.js b/lib/ctx.js new file mode 100644 index 0000000..09f0ffc --- /dev/null +++ b/lib/ctx.js @@ -0,0 +1,16 @@ +var Ctx = function () { + this.web = null; + this.socket = null; + + this.path = null; + this.method = null; + + this.isProcessed = false; +}; + +Ctx.prototype.processed = function () { + this.isProcessed = true; +}; + + +module.exports = Ctx; diff --git a/lib/handlers.js b/lib/handlers.js new file mode 100644 index 0000000..d5480c7 --- /dev/null +++ b/lib/handlers.js @@ -0,0 +1,80 @@ +var socketSupport = function (ctx, next) { + var message = JSON.parse(ctx.socket.message); + var meta = message.meta; + + ctx.meta = meta; + ctx.path = meta.path; + ctx.method = meta.method; + + + result = { + ctx: ctx, + data: message.data + }; + + next(null, ctx); +}; + +var socketStickConnection = function () { + var connectionUserData = connection.userData; + + if (connectionUserData.userId == null) + { + if (userId == null) + { + throw Error('Invalid auth data'); + } + else + { + if (connectionUserData.endPointId != null) + { + throw Error('Connection improperly initialized'); + } + + connectionUserData.userId = userId; + connectionUserData.endPointId = endPointId; + + this.connections.onIdentificationProvided(connection); + } + } + else if (connectionUserData.userId != userId || connectionUserData.endPointId != endPointId) + { + throw Error('Invalid auth data'); + } +}; + +var auth = function (ctx, next) { + var authToken = ctx.meta.auth; + if (authToken != null) + { + userId = this.authenticate(authToken); + } + + ctx.auth = { + userId: userId + }; + + next(null, ctx); +}; + +var clientData = function (ctx, next) { + var meta = ctx.meta; + + ctx.clientInfo = { + endPointId: meta.endPointId, + serial: meta.serial, + userId: ctx.auth.userId + }; + + return next(); +}; + +var data = function (ctx, next) { + + return next(); +}; + +var ret = function (ctx, next) { + + return next(); +}; diff --git a/lib/handlers/handler.js b/lib/handlers/handler.js new file mode 100644 index 0000000..352974b --- /dev/null +++ b/lib/handlers/handler.js @@ -0,0 +1,13 @@ +var Handler = function () { +}; + +Handler.prototype.handle = function (ctx, next) { + next(ctx); +}; + +Handler.prototype.setup = function (resource) { + return true; +}; + + +module.exports = Handler; diff --git a/lib/handlers/impl.js b/lib/handlers/impl.js new file mode 100644 index 0000000..fa2c905 --- /dev/null +++ b/lib/handlers/impl.js @@ -0,0 +1,25 @@ +var inherits = require('util').inherits; + +var Handler = require('./handler'); + + +var Impl = function (f) { + this.impl = f; +}; +inherits(Impl, Handler); + +Impl.prototype.setup = function (chain) { + chain.impl = this.impl; + return false; +}; + + +var impl = function (f) { + return new Impl(f); +}; + + +module.exports = { + Impl: Impl, + impl: impl +}; diff --git a/lib/handlers/ret.js b/lib/handlers/ret.js new file mode 100644 index 0000000..9afedba --- /dev/null +++ b/lib/handlers/ret.js @@ -0,0 +1,33 @@ +var inherits = require('util').inherits; + +var Handler = require('./handler'); + + +var Ret = function () { +}; +inherits(Ret, Handler); + +Ret.prototype.handle = function (ctx, next) { + if (ctx.currentHandlerChain.impl == null) + { + throw new Error('No impl defined'); + } + + var handleResult = function (error, result) { + ctx.web.res.send(result); + ctx.processed(); + next(ctx); + }; + + ctx.currentHandlerChain.impl(ctx, handleResult); +}; + +var ret = function () { + return new Ret(); +}; + + +module.exports = { + Ret: Ret, + ret: ret +}; diff --git a/lib/lowlevel.js b/lib/lowlevel.js new file mode 100644 index 0000000..c7b9cef --- /dev/null +++ b/lib/lowlevel.js @@ -0,0 +1,45 @@ +var Lowlevel = function (config) { + this.config = config; + + this.webServer = null; + this.web = null; + + this.socketServer = null; + this.socket = null; +}; + +Lowlevel.prototype.init = function () { + var express = this.config.lib.express; + var sockjs = this.config.lib.sockjs; + + var settings = this.config.settings; + + this.webServer = express.createServer(); + this.web = this.config.web.configure(this.webServer, settings); + + if (settings.socket && !settings.socket.disable) + { + this.socketServer = sockjs.createServer({ + prefix: settings.getSocketPrefix() + }); + this.socket = this.config.socket.configure(this.socketServer); + + this.socketServer.installHandlers(this.webServer); + } +}; + +Lowlevel.prototype.setHandler = function (handler) { + this.web.setHandler(handler); + if (this.socket != null) + { + this.socket.setHandler(handler); + } +}; + +Lowlevel.prototype.start = function () { + var listenSettings = this.config.settings.listen; + this.webServer.listen(listenSettings.port, listenSettings.address); +}; + + +module.exports = Lowlevel; diff --git a/lib/resource.js b/lib/resource.js new file mode 100644 index 0000000..091cca0 --- /dev/null +++ b/lib/resource.js @@ -0,0 +1,137 @@ +var Handler = require('./handlers/handler'); + + +var HandlerChain = function (resource) { + this.resource = resource; + this.handlers = []; + + this.impl = null; +}; + +HandlerChain.prototype.add = function (handler) { + if (handler instanceof Handler) + { + if (handler.setup(this)) + { + this.handlers.push(handler); + } + } +}; + +HandlerChain.prototype.beforeExecute = function (ctx) { + ctx.currentHandlerChain = this; +}; + +HandlerChain.prototype.afterExecute = function (ctx) { + ctx.currentHandlerChain = null; +}; + +HandlerChain.prototype.execute = function (ctx, next) { + this.beforeExecute(ctx); + + var self = this; + + var handlers = this.handlers; + var i = 0; + var nextInChain = function (ctx) { + if (i >= handlers.length || ctx.isProcessed) + { + self.afterExecute(ctx); + ctx.currentHandlerChain = null; + + if (!ctx.isProcessed) + { + next(); + } + + return; + } + + var f = handlers[i++]; + if (f instanceof Handler) + { + f.handle(ctx, nextInChain); + } + else + { + f(ctx, nextInChain); + } + }; + + nextInChain(ctx); +}; + + +var MethodMapper = function () { +}; + +MethodMapper.prototype.getLogicalMethod = function (ctx) { + return { + GET: 'get', + POST: 'update', + DELETE: 'del' + }[ctx.method]; +}; + + +var Resource = function (info) { + this.path = info.path; + + if (info.call != null) + { + if (info.get != null || info.update != null || info.del != null) + { + throw new BadResourceError('Call-resource must not include other type handlers'); + } + } + + this.handlers = { + call: this.processHandlers(info.call), + get: this.processHandlers(info.get), + update: this.processHandlers(info.update), + del: this.processHandlers(info.del) + }; + + this.methodMapper = new MethodMapper(); +}; + +Resource.prototype.processHandlers = function (handlers) { + if (handlers == null) + { + return null; + } + + var chain = new HandlerChain(this); + + for (var k in handlers) + { + chain.add(handlers[k]); + } + + return chain; +}; + +Resource.prototype.resolve = function (ctx) { + var result = null; + var logicalMethod = this.methodMapper.getLogicalMethod(ctx); + + if (this.path == ctx.path) + { + for (var method in this.handlers) + { + if (method == logicalMethod) + { + result = this.handlers[method]; + break; + } + } + } + + return result; +}; + + +module.exports = { + HandlerChain: HandlerChain, + Resource: Resource +}; diff --git a/lib/socket/connections.js b/lib/socket/connections.js new file mode 100644 index 0000000..6cfa480 --- /dev/null +++ b/lib/socket/connections.js @@ -0,0 +1,72 @@ +var ConnectionsDict = require('./connections_dict'); + + +var Connections = function() { + this.connections = {}; + + this.userConnections = new ConnectionsDict(); + this.endPointConnections = new ConnectionsDict(); +}; + +Connections.prototype.difference = function(connections, connectionsToNotInclude) { + var result = {}; + for (var id in connections) + { + if (!(id in connectionsToNotInclude)) + { + result[id] = connections[id]; + } + } + + return result; +}; + +Connections.prototype.getEndPointKey = function(userId, endPointId) { + return [userId, endPointId].join('\n'); +}; + +Connections.prototype.add = function(connection) { + var id = connection.id; + this.connections[id] = connection; +}; + +Connections.prototype.remove = function(connection) { + var id = connection.id; + delete this.connections[id]; + + var userId = connection.userData.userId; + + if (userId != null) + { + this.userConnections.remove(userId, connection); + + var endPointId = connection.userData.endPointId; + if (endPointId != null) + { + var endPointKey = this.getEndPointKey(userId, endPointId); + this.endPointConnections.remove(endPointKey, connection); + } + } +}; + +Connections.prototype.onIdentificationProvided = function(connection) { + var userId = connection.userData.userId; + var endPointId = connection.userData.endPointId; + + this.userConnections.add(userId, connection); + + var endPointKey = this.getEndPointKey(userId, endPointId); + this.endPointConnections.add(endPointKey, connection); +}; + +Connections.prototype.getUserConnections = function(userId) { + return this.userConnections.get(userId); +}; + +Connections.prototype.getEndPointConnections = function(userId, endPointId) { + var endPointKey = this.getEndPointKey(userId, endPointId); + return this.endPointConnections.get(endPointKey); +}; + + +module.exports = Connections; diff --git a/lib/socket/connections_dict.js b/lib/socket/connections_dict.js new file mode 100644 index 0000000..1c121f8 --- /dev/null +++ b/lib/socket/connections_dict.js @@ -0,0 +1,22 @@ +var inherits = require('util').inherits; + +var DoubleDict = require('../tools/double_dict'); + + +var ConnectionsDict = function() { + ConnectionsDict.super_.call(this); +}; +inherits(ConnectionsDict, DoubleDict); + +ConnectionsDict.prototype.add = function(key, connection) { + var subKey = connection.id; + ConnectionsDict.super_.prototype.add.call(this, key, subKey, connection); +}; + +ConnectionsDict.prototype.remove = function(key, connection) { + var subKey = connection.id; + ConnectionsDict.super_.prototype.remove.call(this, key, subKey); +}; + + +module.exports = ConnectionsDict; diff --git a/lib/socket/mechanics.js b/lib/socket/mechanics.js new file mode 100644 index 0000000..44a0617 --- /dev/null +++ b/lib/socket/mechanics.js @@ -0,0 +1,57 @@ +var Connections = require('./connections'); +var Transport = require('./transport'); + + +var Mechanics = function() { + this.connections = null; + this.transport = null; + this.handler = null; + + this.init(); +}; + +Mechanics.prototype.init = function() { + this.defineConnections(); + this.defineTransport(); +}; + +Mechanics.prototype.defineConnections = function() { + this.connections = new Connections(); +}; + +Mechanics.prototype.defineTransport = function() { + this.transport = new Transport(); +}; + +Mechanics.prototype.setHandler = function (handler) { + this.handler = handler; +}; + +Mechanics.prototype.onConnect = function(connection) { + console.log('Connected:', connection.id); + connection.userData = {}; + this.connections.add(connection); +}; + +Mechanics.prototype.onDisconnect = function(connection) { + console.log('Disconnected:', connection.id); + this.connections.remove(connection); +}; + +Mechanics.prototype.onMessage = function(connection, message) { + console.log('\nConnection <'+connection.id+'> message:\n', message); + +/* var ctx = new Ctx(); + + ctx.path = + ctx.method = + + ctx.socket = { + }; + + var result = this.handler.handle(ctx, next); + this.transport.sendResult(connection, ctx, result);*/ +}; + + +module.exports = Mechanics; diff --git a/lib/socket/transport.js b/lib/socket/transport.js new file mode 100644 index 0000000..bed5582 --- /dev/null +++ b/lib/socket/transport.js @@ -0,0 +1,47 @@ +var Transport = function() { +}; + +Transport.prototype.encode = function(data, meta) { + if (meta == null) + { + meta = {}; + } + if (data == null) + { + data = null; + } + + return JSON.stringify({ + meta: meta, + data: data + }); +}; + +Transport.prototype.send = function(recipientConnections, data) { + if (recipientConnections == null || recipientConnections.length == 0) + { + return; + } + + var message = this.encode(data); + + for (var k in recipientConnections) + { + var connection = recipientConnections[k]; + connection.write(message); + } +}; + +Transport.prototype.sendResult = function(connection, ctx, result) { + var message = this.encode(result, { + requestSerial: ctx.clientInfo.serial + }); + connection.write(message); +}; + +Transport.prototype.authenticate = function(authToken) { + return authToken; // TODO replace with normal auth mechanism!!! +}; + + +module.exports = Transport; diff --git a/lib/tools/abstract_method.js b/lib/tools/abstract_method.js new file mode 100644 index 0000000..d832f4f --- /dev/null +++ b/lib/tools/abstract_method.js @@ -0,0 +1,17 @@ +var inherits = require('util').inherits; + + +var AbstractMethodCallError = function () { +}; +inherits(AbstractMethodCallError, Error); + + +var abstractMethod = function () { + throw new AbstractMethodCallError(); +}; + + +module.exports = { + AbstractMethodCallError: AbstractMethodCallError, + abstractMethod: abstractMethod +}; diff --git a/lib/tools/double_dict.js b/lib/tools/double_dict.js new file mode 100644 index 0000000..2ac278a --- /dev/null +++ b/lib/tools/double_dict.js @@ -0,0 +1,38 @@ +var DoubleDict = function() { + this.dict = {}; +}; + +DoubleDict.prototype.get = function(key, createIfNotExists) { + var result = this.dict[key]; + + if (result == null) + { + result = {}; + if (createIfNotExists) + { + this.dict[key] = result; + } + } + + return result; +}; + +DoubleDict.prototype.add = function(key, subKey, object) { + this.get(key, true)[subKey] = object; +}; + +DoubleDict.prototype.remove = function(key, subKey) { + var objects = this.get(key); + + if (objects.length > 0) + { + delete objects[subKey]; + if (objects.length == 0) + { + delete this.objects[key]; + } + } +}; + + +module.exports = DoubleDict; diff --git a/lib/units.js b/lib/units.js new file mode 100644 index 0000000..1a63d01 --- /dev/null +++ b/lib/units.js @@ -0,0 +1,81 @@ +var inherits = require('util').inherits; + +var abstractMethod = require('./tools/abstract_method').abstractMethod; + + +var DuplicateUnitError = function (key) { + this.key = key; +}; +inherits(DuplicateUnitError, Error); + + +var UnitRequiredError = function (key) { + this.key = key; +}; +inherits(UnitRequiredError, Error); + + +var Units = function () { + this.units = {}; + this.needInit = {}; +}; + +Units.prototype.addReady = function (key, unit) { + this.add(key, unit, true); +}; + +Units.prototype.add = function (key, unit, skipInit) { + if (key in this.units) + { + throw new DuplicateUnitError(key); + } + + this.units[key] = unit; + + if (!skipInit) + { + this.needInit[key] = true; + } +}; + +Units.prototype.get = function (key) { + return this.units[key]; +}; + +Units.prototype.require = function (key) { + var unit = this.get(key); + if (unit == null) + { + throw new UnitRequiredError(key); + } + return unit; +}; + +Units.prototype.init = function () { + for (var key in this.needInit) + { + if (this.needInit[key]) + { + this.units[key].init(); + } + } +}; + + +var Unit = function (units) { + this.units = units; +}; + +Unit.prototype.init = abstractMethod; + +Unit.prototype.require = function (key) { + return this.units.require(key); +}; + + +module.exports = { + DuplicateUnitError: DuplicateUnitError, + UnitRequiredError: UnitRequiredError, + Units: Units, + Unit: Unit +}; diff --git a/lib/web/mechanics.js b/lib/web/mechanics.js new file mode 100644 index 0000000..7037319 --- /dev/null +++ b/lib/web/mechanics.js @@ -0,0 +1,40 @@ +var Ctx = require('../ctx'); + + +var Mechanics = function () { + this.handler = null; + this.middleware = this.createMiddleware(); +}; + +Mechanics.prototype.setHandler = function (handler) { + this.handler = handler; +}; + +Mechanics.prototype.middlewareHandle = function (req, res, next) { + if (this.handler == null) + { + throw new Error('No handler set'); + } + + var ctx = new Ctx(); + + ctx.path = req.path; + ctx.method = req.method; + + ctx.web = { + req: req, + res: res + }; + + this.handler.handle(ctx, next); +}; + +Mechanics.prototype.createMiddleware = function () { + var self = this; + return function (req, res, next) { + self.middlewareHandle(req, res, next); + }; +}; + + +module.exports = Mechanics; diff --git a/package.json b/package.json new file mode 100644 index 0000000..ae50d34 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "apis", + "version": "0.0.1", + "description": "Library for creation web and websocket restful APIs", + "keywords": ["api", "rest", "web", "websocket"], + "author": { + "name": "Dmitry Smolin", + "email": "dimsmol@gmail.com" + }, + "preferGlobal": false, + "private": false, + "engines": { + "node": "~0.6.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/dimsmol/apis" + }, + "directories": { + "lib": "./lib", + "bin": "./bin", + "doc": "./doc", + "example": "./example" + }, + "main": "./lib/index" +}