From 9d81b82dde063bf963ec9a77ee0c7053034967a3 Mon Sep 17 00:00:00 2001 From: Aiko Mastboom Date: Tue, 16 Apr 2013 20:30:54 +0200 Subject: [PATCH] initial commit --- .gitignore | 9 ++ README.md | 1 + index.js | 35 ++++++ mongodata.js | 211 +++++++++++++++++++++++++++++++++++ package.json | 29 +++++ preview.js | 106 ++++++++++++++++++ public/behaviour.html | 79 +++++++++++++ public/index.html | 79 +++++++++++++ public/style.css | 44 ++++++++ public/style.html | 79 +++++++++++++ responder.js | 36 ++++++ server.js | 251 ++++++++++++++++++++++++++++++++++++++++++ test/test.server.js | 0 13 files changed, 959 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 index.js create mode 100644 mongodata.js create mode 100644 package.json create mode 100644 preview.js create mode 100644 public/behaviour.html create mode 100644 public/index.html create mode 100644 public/style.css create mode 100644 public/style.html create mode 100644 responder.js create mode 100644 server.js create mode 100644 test/test.server.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a07aabf --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.hg +.idea +*.iml +.DS_Store +*.orig +*.swp +fleet.json +*.log +thing diff --git a/README.md b/README.md new file mode 100644 index 0000000..b43bf86 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +README.md diff --git a/index.js b/index.js new file mode 100644 index 0000000..d94ae49 --- /dev/null +++ b/index.js @@ -0,0 +1,35 @@ +var connect = require('connect'); +var express = require('express'); +var instance = require('./server.js'); + +process.title = "Prototyper"; + +var config = { + errors: true, + debug: false, + port: 8000, + mongo: { + server: "mongodb://silo01.local:27017/Prototyper", + options: { + server: { + maxPoolSize:10, + auto_reconnect:true + } + } + }, + share: { + sockjs: {}, + db: {type: 'none'} + } +}; + +var server = express.createServer(); +server.use(connect.logger()); +server.use(express.static(__dirname + '/public')); + +instance(server, config); + +server.listen(config.port, function (err) { +// console.log('routes',server.routes); + console.log('Server running at http://127.0.0.1:', config.port); +}); diff --git a/mongodata.js b/mongodata.js new file mode 100644 index 0000000..7d6579c --- /dev/null +++ b/mongodata.js @@ -0,0 +1,211 @@ +var MongoClient = require('mongodb').MongoClient; +var ObjectID = require('mongodb').ObjectID; + + +module.exports = function (config) { + + function getMongoContent(options, callback) { + config.debug && console.log('getMongoContent options', options); + MongoClient.connect(config.mongo.server, config.mongo.options, function (err, db) { + if (err) { + config.debug && console.log('ERR1 getMongoContent',err); + return callback(err); + } + db.collection(options.collection, function (err, col) { + if (err) { + config.debug && console.log('ERR2 getMongoContent',err); + return callback(err); + } + if (options.query._id && !(options.query._id instanceof Object)) { + options.query._id = new ObjectID.createFromHexString(options.query._id); + } + col.findOne(options.query, function (err, result) { + if (err) { + config.debug && console.log('ERR3 getMongoContent',err); + return callback(err); + } + if (!result) { + return callback(new Error('Data not found ' + options.collection + '/' + JSON.stringify(options.query))); + } + return callback(null, result, col); + }); + }); + }); + } + + function getMongoAttribute(options, callback) { + config.debug && console.log('getMongoAttribute options', options); + return getMongoContent(options, function (err, result) { + if (err) { + config.errors && console.log('ERR1 getMongoAttribute',err); + return callback(err); + } + var attribute_options = null; + config.debug && console.log('getMongoAttribute result', result); + if (result && result.hasOwnProperty(options.attribute)) { + attribute_options = { + collection: options.collection, + query: {_id: result[options.attribute].guid} + }; + config.debug && console.log('getMongoAttribute attribute_options', attribute_options); + + return getMongoContent(attribute_options, function (err, attribute_result) { + if (err) { + config.errors && console.log('ERR2 getMongoAttribute',err); + return callback(err); + } + config.debug && console.log('getMongoAttribute attribute_result', attribute_result); + return callback(err, attribute_result); + }); + } else { + config.debug && console.log('getMongoAttribute try direct lookup'); + attribute_options = { + collection: options.collection, + query: { + parent: result._id, + name: result.name+'.'+options.attribute + } + }; + config.debug && console.log('getMongoAttribute attribute_options', attribute_options); + + return getMongoContent(attribute_options, function (err, attribute_result) { + if (err) { + config.errors && console.log('ERR3 getMongoAttribute',err); + return callback(err); + } + if (attribute_result) { + config.debug && console.log('getMongoAttribute direct attribute_result', attribute_result); + return callback(err, attribute_result); + } else { + if (options.type) { + if (options.type == 'json') { + config.errors && console.log('ERR4 getMongoAttribute empty json {}'); + + return callback(null, '{}'); + } + if (options.type == 'text') { + config.errors && console.log('ERR5 getMongoAttribute empty string'); + return callback(null, ''); + } + } + return callback(new Error('Data not found ' + options.collection + '/' + JSON.stringify(options.query) + '.' + options.attribute)); + } + }); + + } + }) + } + function saveData(collection, data, callback) { + if (data._id && !(data._id instanceof Object)) { + data._id = new ObjectID.createFromHexString(data._id); + } + config.debug && console.log('saving', data._id); + collection.save(data, {safe:true}, callback); + } + + function setMongoContent(data, options, callback) { + config.debug && console.log('setMongoContent options', options); + MongoClient.connect(config.mongo.server, config.mongo.options, function (err, db) { + if (err) { + config.errors && console.log('ERR1 setMongoContent',err); + return callback(err); + } + db.collection(options.collection, function (err, col) { + if (err) { + config.errors && console.log('ERR2 setMongoContent',err); + return callback(err); + } + if (options.operation) { + data.version = options.operation.v; + } + if (!data._id) { + config.debug && console.log('setMongoContent lookup by query',options.query); + col.findOne(options.query, function (err, result) { + if (result){ + data._id=result._id; + } + return saveData(col, data, callback); + }) + } else { + return saveData(col, data, callback); + } + }); + }); + } + + function setMongoAttribute(data, options, callback) { + config.debug && console.log('setMongoAttribute options', options); + getMongoContent(options, function (err, result, col) { + if (err) { + config.errors && console.log('ERR1 setMongoAttribute',err); + return callback(err); + } + var attribute_options = { + collection: options.collection + }; + if (result.hasOwnProperty(options.attribute)) { + config.debug && console.log('getMongoAttribute parent found, get child and save'); + attribute_options.query = {_id: result[options.attribute].guid}; + getMongoContent(attribute_options, function (err, attribute_result, col) { + if (err) { + config.errors && console.log('ERR2 setMongoAttribute',err); + return callback(err); + } + attribute_result[options.attribute] = data; + if (options.operation) { + attribute_result.version = options.operation.v; + } + attribute_result.parent = result._id; + return saveData(col, attribute_result, callback); + }); + } else { + config.debug && console.log('getMongoAttribute try direct lookup'); + attribute_options = { + collection: options.collection, + query: { + parent: result._id, + name: result.name+'.'+options.attribute + } + }; + config.debug && console.log('getMongoAttribute attribute_options', attribute_options); + return getMongoContent(attribute_options, function (err, attribute_result) { + if (attribute_result) { + config.debug && console.log('getMongoAttribute found lost attribute, reconnect'); + result[options.attribute] = { guid: attribute_result._id }; + return saveData(col, result, callback); + + } else { + config.debug && console.log('getMongoAttribute field does not exist yet. need to create doc first.'); + var attribute_data = { + name: result.name + '.' + options.attribute, + parent: result._id + }; + attribute_data[options.attribute] = data; + if (options.operation) { + attribute_data.version = options.operation.v; + } + + return saveData(col, attribute_data, function (err, attribute_result) { + if(err){ + config.errors && console.log('ERR3 setMongoAttribute',err); + } + result[options.attribute] = { guid: attribute_result._id }; + return saveData(col, result, callback); + }) + } + }); + } + }) + } + + return { + getMongoAttribute: getMongoAttribute, + getMongoContent: getMongoContent, + setMongoAttribute: setMongoAttribute, + setMongoContent: setMongoContent + }; +}; + + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..5bfa8d7 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "Prototyper", + "version": "0.0.0", + "main": "index.js", + "private": "true", + "dependencies": { + "share": "~0.6.0", + "connect": "~2.7.5", + "express": "~2.5.11", + "mongodb": "~1.2.14", + "handlebars": "~1.0.10", + "underscore": "~1.4.4", + "less": "~1.3.3" + }, + "devDependencies": { + "sockjs": "~0.3.5" + }, + "repository": "", + "author": "Aiko Mastboom", + "license": "BSD", + "readmeFilename": "README.md", + "directories": { + "test": "test" + }, + "description": "README.md", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/preview.js b/preview.js new file mode 100644 index 0000000..2b85f8c --- /dev/null +++ b/preview.js @@ -0,0 +1,106 @@ +var Handlebars = require('handlebars'); +var _ = require('underscore'); +var less = require('less'); + +module.exports = function (config) { + + var sourceHead = + '\n' + + '{{#if debug}}' + + '\n' + + '\n' + + '{{else}}' + + '\n' + + '{{/if}}'; + + var sourceBody = + '\n' + + '\n' + + '\n' + + '\n' + + '\n'; + + var templateHead = Handlebars.compile(sourceHead); + var templateBody = Handlebars.compile(sourceBody); + + + var getCSS = function (callback) { + contentInstance.getAllOrderedObjects('Stylesheet', function (err, dataObjects) { + if (err) { + return callback(err); + } + + var stylesheets = _.map(dataObjects, function(dataObject) { + return "\n/* -- " + dataObject.get('title') + ' -- */\n\n' + + dataObject.get('body'); + }).join('\n'); + + function lessCompilationError(stylesheets, err, callback){ + stylesheets = "/*\n -- The following ERROR(s) occurred during less compilation:\n\n" + + err.message + + "\n\n -- you can add ?debug to the preview url to enable browser side less compilation." + + "\n\n0: this is line 0 */\n" + + stylesheets; + return callback(null, stylesheets); + } + + try { + less.render(stylesheets, function (err, css) { + if (err) { + return lessCompilationError(stylesheets, err, callback); + } + return callback(err, css); + }); + } catch (err){ + return lessCompilationError(stylesheets, err, callback); + } + }); + }; + + var getJS = function (callback) { + contentInstance.getAllOrderedObjects('Behaviour', function (err, dataObjects) { + if (err) { + return callback(err); + } + + var scripts = _.map(dataObjects, function(dataObject) { + return dataObject.get('body'); + }).join('\n'); + + callback(null, scripts); + }); + }; + + var getPreviewHTML = function (options, content, callback) { + + var html = replaceMarkers(content, templateHead(options), templateBody(options)); + + callback(null, html); + }; + + var replaceMarkers = function (html, styleMarkerReplacement, jsMarkerReplacement) { + + function replace(marker, replacement) { + var regExp = new RegExp(''); + if (html.match(regExp)){ + html = html.replace(regExp, replacement); + } else { + html = html.replace('', '\n'); + } + } + + replace('ipe_style_marker', styleMarkerReplacement); + replace('ipe_js_marker', jsMarkerReplacement); + + return html; + }; + + + return { + getCSS: getCSS, + getJS: getJS, + getPreviewHTML: getPreviewHTML, + _replaceMarkers: replaceMarkers + }; +}; + diff --git a/public/behaviour.html b/public/behaviour.html new file mode 100644 index 0000000..e32c812 --- /dev/null +++ b/public/behaviour.html @@ -0,0 +1,79 @@ + + + + + app/main/behaviour + + + + + +
+ + + + + + + + + + + + + + + + + + + + + diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..2b4ec14 --- /dev/null +++ b/public/index.html @@ -0,0 +1,79 @@ + + + + + app/main/index + + + + + +
+ + + + + + + + + + + + + + + + + + + + + diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..bf06a6c --- /dev/null +++ b/public/style.css @@ -0,0 +1,44 @@ +body { + overflow: hidden; +} + +.ace_bold { + font-weight:bold; +} + +.ace_italic { + font-style:italic; +} + +#header { + position: fixed; + top: 0; + right: 0; + height: 30px; + width: 100%; + background-color: black; +} + +#htext { + line-height: 30px; + vertical-align: middle; + + padding-left: 10px; + color: white; + font-family: baskerville, palatino, 'palatino linotype', georgia,serif; +} + +#editor { + margin: 0; + position: absolute; + top: 30px; + bottom: 0; + left: 0; + right: 0; +} + +#htext a { + color: #aaf; + font-weight: bold; +} + diff --git a/public/style.html b/public/style.html new file mode 100644 index 0000000..0ac764d --- /dev/null +++ b/public/style.html @@ -0,0 +1,79 @@ + + + + + app/main/style + + + + + +
+ + + + + + + + + + + + + + + + + + + + + diff --git a/responder.js b/responder.js new file mode 100644 index 0000000..b7dab54 --- /dev/null +++ b/responder.js @@ -0,0 +1,36 @@ +var mimetypes = { + 'js': 'application/javascript', + 'html': 'text/html', + 'text': 'text/plain', + 'css': 'text/css', + 'less': 'text/css' +}; + +var getMimeType = function (ext) { + if (mimetypes[ext]) { + return mimetypes[ext]; + } + + return mimetypes.text; +}; + +module.exports = function (options, res, next) { + var responder = function (err, result) { + console.log('err',err); + if (err) { + if (/Data not found*/.test(err.message)) { + res.status(404); + } + return next(err.message); + } + console.log('responder options',options); + var contentType = getMimeType(options.ext); + res.setHeader('Content-Type', contentType); + var content = result; + if (options.attribute) { + content = result[options.attribute]; + } + res.send(content); + }; + return responder; +}; diff --git a/server.js b/server.js new file mode 100644 index 0000000..ff7e43e --- /dev/null +++ b/server.js @@ -0,0 +1,251 @@ +var sharejs = require('share'); +var mongodata = require('./mongodata.js'); +var responder = require('./responder.js'); +var preview = require('./preview.js'); + +module.exports = function (server, config) { + + // Attach the sharejs REST and Socket.io interfaces to the server + sharejs.server.attach(server, config.share); + var model = server.model; + + + var mongodataInstance = mongodata(config); + + server.get('/data/:collection/:guid/:attribute.:ext(css|less|js|html)', + function getMongoAttribute(req, res, next) { + config.debug && console.log('/data/:collection/:guid/:attribute.:ext(less|js|html)'); + var options = { + collection: req.params.collection, + attribute: req.params.attribute, + ext: req.params.ext, + query: {_id:req.params.guid} + }; + mongodataInstance.getMongoAttribute(options, + responder(options, res, next) + ); + } + ); + server.get('/data/:collection/:guid.:ext(json)', + function getMongoContent(req, res, next) { + config.debug && console.log('/data/:collection/:guid.:ext(json)'); + var options = { + collection: req.params.collection, + ext: req.params.ext, + query: {_id:req.params.guid} + }; + mongodataInstance.getMongoContent(options, + responder(options, res, next) + ); + } + ); + + server.get('/content/:collection/:name/:attribute.:ext(css|less|js|html)', + function getMongoAttribute(req, res, next) { + config.debug && console.log('/content/:collection/:name/:attribute.:ext(less|js|html)'); + var options = { + collection: req.params.collection, + attribute: req.params.attribute, + ext: req.params.ext, + query: {name:req.params.name} + }; + mongodataInstance.getMongoAttribute(options, + responder(options, res, next) + ); + } + ); + server.get('/content/:collection/:name.:ext(json)', + function getMongoContent(req, res, next) { + config.debug && console.log('/content/:collection/:name.:ext(json)'); + var options = { + collection: req.params.collection, + ext: req.params.ext, + query: {name:req.params.name} + }; + mongodataInstance.getMongoContent(options, + responder(options, res, next) + ); + } + ); + + function handleMongoGetResult(options) { + function handleResult(err, result) { + if (err) { + config.errors && console.log('ERR1 handleMongoGetResult.handleResult Error retrieving document ', options.collection, JSON.stringify(options.query), options.attribute || "", err); + } else { + if (result) { + var operation = null; + config.debug && console.log('handleMongoGetResult options',options, result); + var data = result; + if (options.attribute) { + data = result[options.attribute]; + } + var version = 0; + if (options.type == 'json') { + if (data instanceof String) { + data = JSON.parse(data); + } + operation = { op: [ + { p: [], oi: data, od: null } + ], v: version }; + } else if (options.type == 'text') { + operation = { op: [ + {i: data, p: 0} + ], v: version }; + } + if (operation) { + model.applyOp(options.documentId, operation, function appliedOp(error, version) { + options.debug && console.log('getResult applyOp version', version); + if (error) { + options.error && console.log('ERR2 handleMongoGetResult', error); + } + }); + } + } + } + } + + return handleResult; + } + + model.on('create', function populateDocument(documentId, data) { + console.log('Populating a doc in channel', documentId, data); + var splitId = documentId.split(':'); + var options = { + documentId: documentId, + type: splitId[0], + collection: splitId[1], + attribute: null + }; + if (splitId.length == 4) { + options.query= {_id:splitId[2]}; + options.attribute = splitId[3]; + mongodataInstance.getMongoAttribute(options, handleMongoGetResult(options)); + } else { + options.query={name:splitId[2]}; + mongodataInstance.getMongoContent(options, handleMongoGetResult(options)); + } + }); + + + function handleMongoSetResult(options, current, callback) { + function handleResult(err, result) { + if (err) { + config.errors && console.log('ERR1 handleMongoSetResult Error while saving document ', options.collection, JSON.stringify(options.query), options.attribute || "", err); + return callback && callback(err); + } + config.debug && console.log('current', current, 'result',result, 'options',options); + if ((!current || !current.name) && result.name) { + var operation = { op: [ + { p: ['name'], oi: result.name, od: null } + ], v: options.operation.v }; + model.applyOp(options.documentId, operation, function appliedOp(error, version) { + config.debug && console.log('setResult applyOp version', version); + if (error) { + config.error && console.log('ERR2 handleMongoSetResult',error); + return callback && callback(error); + } + return callback && callback(null,version); + }); + } + } + + return handleResult; + } + + function handleMongoAttributeSetResult(options, current, callback) { + function handleResult(err, result) { + if (err) { + config.errors && console.log('ERR1 handleMongoAttributeSetResult Error while saving document ', options.collection, JSON.stringify(options.query), options.attribute || "", err); + return callback && callback(err); + } + options.debug && console.log('current', current, 'result',result); + if (result.hasOwnProperty('_id')) { + config.debug && console.log('// new object created. need to update the parent object.'); + var pieces = options.documentId.split(':'); + var parentDocId = pieces[0]+ ':' + pieces[1] + ':' + pieces[2]; + var operation = { op: [ + { p: [options.attribute], oi: { guid: result._id } , od: null } + ], v: options.operation.v }; + + model.applyOp(parentDocId, operation, function appliedOp(error, version) { + config.debug && console.log('setResult applyOp parent version', version); + if (error) { + config.error && console.log('ERR2 handleMongoAttributeSetResult',error); + return callback && callback(error); + } + return callback && callback(null,version); + }) + } + } + + return handleResult; + } + + // 'applyOp' event is fired when an operational transform is applied to to a shareDoc + // a shareDoc has changed and needs to be saved to mongo + model.on('applyOp', function persistDocument(documentId, operation, current, previous) { + config.debug && console.log('applyOp',documentId,operation,current); + var splitId = documentId.split(':'); + var options = { + documentId: documentId, + type: splitId[0], + collection: splitId[1], + attribute: null, + operation: operation + }; + if (splitId.length == 4) { + options.query= {_id:splitId[2]}; + options.attribute = splitId[3]; + var data = current; + if (options.type == 'json') { + data = JSON.stringify(current); + } + mongodataInstance.setMongoAttribute(data, options, handleMongoAttributeSetResult(options, current, function (err, result) { + if(err) { + config.errors && console.log('ERR1 applyOp', err); + } + })); + } else { + options.query={name:splitId[2]}; + mongodataInstance.setMongoContent(current, options, handleMongoSetResult(options, current, function (err, result) { + if(err) { + config.errors && console.log('ERR2 applyOp', err); + } + })); + } + }); + + var previewInstance = preview(config); + + server.get('/page/:collection/:name.:ext(html)', + function getPreviewContent(req, res, next) { + config.debug && console.log('/page/:collection/:name.:ext(html)'); + var options = { + collection: req.params.collection, + ext: req.params.ext, + query: {name:req.params.name}, + debug: req.query && req.query.hasOwnProperty('debug') + }; + mongodataInstance.getMongoContent(options, function (err, result) { + if (err) { + responder(options, res, next)(err, result); + } + if (result) { + var attribute_parts = options.query.name.split('.'); + var attribute = attribute_parts[attribute_parts.length-1]; + var content = result[attribute]; + options.name=attribute_parts[0]; + //options.attribute=attribute; + config.debug && console.log('getPreviewContent content',content); + previewInstance.getPreviewHTML(options, content, + responder(options, res, next) + ); + } + }); + } + ); + + + +}; \ No newline at end of file diff --git a/test/test.server.js b/test/test.server.js new file mode 100644 index 0000000..e69de29