├── .gitattributes
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── Web.Debug.config
├── Web.config
├── app.js
├── bin
├── ChangeConfig.ps1
├── Microsoft.NodejsTools.WebRole.dll
├── download.ps1
├── node.cmd
├── setup_web.cmd
└── www
├── bower.json
├── config
├── azure.json
├── default.json
├── redis.json
└── test.json
├── http.js
├── models
├── Patch.js
├── RepositoryIndex.js
├── Rule.js
└── rules
│ ├── AlwaysOffRule.js
│ ├── AlwaysOnRule.js
│ ├── ArrayContainsRule.js
│ ├── StringMatchesRule.js
│ └── index.js
├── package.json
├── providers
├── DataProvider.js
├── DocumentDbDataProvider.js
├── InProcessDataProvider.js
└── RedisDataProvider.js
├── public
├── js
│ └── rennet.js
└── stylesheets
│ ├── style.css
│ └── style.styl
├── rennet.njsproj
├── rennet.sln
├── routes
├── api.js
└── index.js
├── services
├── DataService.js
├── PatchService.js
├── RennetService.js
└── RepositoryIndexService.js
├── tests
├── dataProviderTests.js
├── demo.cmd
├── demo.sh
├── patchTests.js
├── rennetServiceTests.js
└── ruleTests.js
└── views
├── error.jade
├── index.jade
└── layout.jade
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
4 | # Custom for Visual Studio
5 | *.cs diff=csharp
6 | *.sln merge=union
7 | *.csproj merge=union
8 | *.vbproj merge=union
9 | *.fsproj merge=union
10 | *.dbproj merge=union
11 |
12 | # Standard to msysgit
13 | *.doc diff=astextplain
14 | *.DOC diff=astextplain
15 | *.docx diff=astextplain
16 | *.DOCX diff=astextplain
17 | *.dot diff=astextplain
18 | *.DOT diff=astextplain
19 | *.pdf diff=astextplain
20 | *.PDF diff=astextplain
21 | *.rtf diff=astextplain
22 | *.RTF diff=astextplain
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # Compiled binary addons (http://nodejs.org/api/addons.html)
20 | build/Release
21 |
22 | # Dependency directory
23 | # Commenting this out is preferred by some people, see
24 | # https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
25 | node_modules
26 | bower_components
27 |
28 | # Users Environment Variables
29 | .lock-wscript
30 |
31 | # =========================
32 | # Operating System Files
33 | # =========================
34 |
35 | # OSX
36 | # =========================
37 |
38 | .DS_Store
39 | .AppleDouble
40 | .LSOverride
41 |
42 | # Icon must end with two \r
43 | Icon
44 |
45 |
46 | # Thumbnails
47 | ._*
48 |
49 | # Files that might appear on external disk
50 | .Spotlight-V100
51 | .Trashes
52 |
53 | # Directories potentially created on remote AFP share
54 | .AppleDB
55 | .AppleDesktop
56 | Network Trash Folder
57 | Temporary Items
58 | .apdisk
59 |
60 | # Windows
61 | # =========================
62 |
63 | # Windows image file caches
64 | Thumbs.db
65 | ehthumbs.db
66 |
67 | # Folder config file
68 | Desktop.ini
69 |
70 | # Recycle Bin used on file shares
71 | $RECYCLE.BIN/
72 |
73 | # Windows Installer files
74 | *.cab
75 | *.msi
76 | *.msm
77 | *.msp
78 |
79 | # Node tools for Visual Studio
80 | .ntvs_analysis.dat
81 | *.suo
82 |
83 | #WebStorm/IntelliJ projects
84 | .idea
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to contribute
2 | Feel free to fork and submit pull requests. File issues if you have them.
3 |
4 | This project is maintained by JD Conley (jd@realcrowd.com). Contact him if you have any thoughts or questions.
5 |
6 | ## Dev environment setup in Visual Studio / Windows
7 | 1. Install Msysgit (http://msysgit.github.io/)
8 | - Make sure to choose the "Run Git from the Windows Command Prompt" option during setup, which will add Git to your path
9 | 2. Install node.js, Visual Studio 2013, and Node.js Tools for Visual Studio (https://nodejstools.codeplex.com/wikipage?title=Installation)
10 | 3. Open project, right click the npm node in solution explorer and choose "Install missing npm packages"
11 | 4. Open Node.js interactive window in Visual Studio (View -> Other Windows -> Node.js Interactive Window)
12 | 5. Install bower globally so it is available in your command prompt:
13 |
14 | ```
15 | > .npm install -g bower
16 | ```
17 |
18 | 6. Open a command prompt in the project root (rennet/rennet) and install/restore bower packages
19 |
20 | ```
21 | > bower install
22 | ```
23 |
24 | ## Running the tests
25 | Mocha tests are in the "tests" directory. They should be run with environment variable NODE_ENV=test. We currently run them from within WebStorm or Visual Studio.
26 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 RealCrowd, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # rennet
2 | Rennet is a JSON document database that enables branching, chaining and merging of documents based on a simple rules pipeline. This allows you to read data that is transformed dependent on the current context of the application. Let's call this "context-aware data".
3 |
4 | A clean separation of metadata and code is a key enabler for rapid development on teams. It enables developers to continuously ship code and limits the impact of bugs. With context-aware data provided by something like rennet, developers can ship code, enable QA to test it, expose it to subsets of users/environments, and can place ultimate control of the product in the hands of the product owner.
5 |
6 | In video games this is very common practice, with data files defining everything about the game. Nowadays game data is modified on the fly in production over the internet, and integrated with A/B testing, in order to increase key performance indicators. It is less common in web or app development, but just as useful. At [RealCrowd](https://www.realcrowd.com) we use this primarily to simplify parallel development and continuous integration, with developers committing to the master branch and deploying to production often with limited impact on the end user.
7 |
8 | Some especially useful areas for context-aware data:
9 |
10 | 1. Environment and user-based configuration of features
11 | 2. Authorization
12 | 3. A/B testing
13 | 4. Dynamic configuration in general
14 | 5. Localization
15 |
16 | Rennet is currently an api-only system, but we have plans to build a management UI for it. By default it starts up with in-process ephemeral storage. This makes development and testing fast, but is not usable in production. A [Redis](http://redis.io/) as well as an [Azure DocumentDb](http://azure.microsoft.com/en-us/services/documentdb/) storage provider are included for persistent storage of documents. Data storage is abstracted into what we call [Providers](https://github.com/realcrowd/rennet/tree/master/providers) so new storage options are easy to create.
17 |
18 | ## Usage
19 | Imagine you have an application called "GitHub" that enables collaboration around git repositories. You want to both make some money and encourage open source development, so you decide to charge users for the ability to host private repositories. You want to set different pricing tiers and offer a different number of private repositories at each tier. This is pretty easy to do in code or with any ole database. But, things get a bit more complex if, say, you want to A/B test the number of tiers, or only roll it out to a percentage of your user base, or have different settings for the QA environment. The code for these scenarios can turn to spaghetti very quickly.
20 |
21 | With this configuration example, rennet will toggle the configuration for the private repositories feature in different deployment environments and for users with different pricing tiers in production. This creates a nice separation of concerns as the GitHub application no longer needs to have code for _why_ a given feature is on or off in the current context, just that it is.
22 |
23 | Check out the demo script in the [tests](https://github.com/realcrowd/rennet/tree/master/tests) directory if you want to run this entire usage example from one script. Curl is required to be in the path and rennet must be running locally on port 1337 for the script to run.
24 |
25 | ### Create a Repository
26 | This creates an empty repository with an ID of "github". You can store any other data you'd like on this document, but id is required. Later we'll add some branches into this repository to start exposing transformed data.
27 |
28 | ```
29 | curl -X POST -H "Content-Type: application/json" -d "{\"id\":\"github\"}" http://localhost:1337/api/v1/repository/github
30 | ```
31 |
32 | ### Add Patches to Repository
33 | All documents stored in the database are patches. Patches are ultimately grouped together in branches, and applied in the order specified in the branch. Each patch also has a rule that determines if it will be applied in the current context.
34 |
35 | Let's create a few patches. We'll max out the "privateRepository" feature in the QA environment, set it to 0 by default in production, and set the correct value for users in the paid plans.
36 |
37 | For this we'll use the [StringMatchesRule](https://github.com/realcrowd/rennet/blob/master/models/rules/StringMatchesRule.js) to determine the plan that the user is a part of and apply the correct patch to the features data. See the [rules directory](https://github.com/realcrowd/rennet/tree/master/models/rules) for a list of the supported rules.
38 |
39 | ```
40 | curl -X POST -H "Content-Type: application/json" -d "{\"id\":\"defaultFeatures\",\"data\":{\"features\":{\"privateRepository\":0}}}" http://localhost:1337/api/v1/repository/github/patch/defaultFeatures
41 |
42 | curl -X POST -H "Content-Type: application/json" -d "{\"id\":\"microFeatures\",\"rule\":{\"name\":\"StringMatchesRule\",\"arguments\":{\"jsonPath\":\"$.user.plan\",\"matches\":\"micro\"}},\"data\":{\"features\":{\"privateRepository\":5}}}" http://localhost:1337/api/v1/repository/github/patch/microFeatures
43 |
44 | curl -X POST -H "Content-Type: application/json" -d "{\"id\":\"smallFeatures\",\"rule\":{\"name\":\"StringMatchesRule\",\"arguments\":{\"jsonPath\":\"$.user.plan\",\"matches\":\"small\"}},\"data\":{\"features\":{\"privateRepository\":10}}}" http://localhost:1337/api/v1/repository/github/patch/smallFeatures
45 |
46 | curl -X POST -H "Content-Type: application/json" -d "{\"id\":\"mediumFeatures\",\"rule\":{\"name\":\"StringMatchesRule\",\"arguments\":{\"jsonPath\":\"$.user.plan\",\"matches\":\"medium\"}},\"data\":{\"features\":{\"privateRepository\":20}}}" http://localhost:1337/api/v1/repository/github/patch/mediumFeatures
47 |
48 | curl -X POST -H "Content-Type: application/json" -d "{\"id\":\"largeFeatures\",\"rule\":{\"name\":\"StringMatchesRule\",\"arguments\":{\"jsonPath\":\"$.user.plan\",\"matches\":\"large\"}},\"data\":{\"features\":{\"privateRepository\":50}}}" http://localhost:1337/api/v1/repository/github/patch/largeFeatures
49 |
50 | curl -X POST -H "Content-Type: application/json" -d "{\"id\":\"testingFeatures\",\"data\":{\"features\":{\"privateRepository\":1000}}}" http://localhost:1337/api/v1/repository/github/patch/testingFeatures
51 | ```
52 |
53 | ### Create Branches with Patches
54 | Now we'll update the repository index to define a few branches and put the patches we created to use. We define a "master" branch that holds the default configuration for the application. We define a "qa" branch which contains all the patches from the "master" branch, but also applies the "testingFeatures" patch. We also define a "prod" branch that contains the "master" patches and all the patches with rules based on the user's plan.
55 |
56 | In a typical application the qa and prod environments will have much different configurations with various features being in different states of testing, different percentage of the user base with the feature enabled, etc.
57 |
58 | ```
59 | curl -X PUT -H "Content-Type: application/json" -d "{\"id\":\"github\",\"branches\":{\"master\":{\"patches\":[\"defaultFeatures\"]},\"qa\":{\"patches\":[\"branch:master\",\"testingFeatures\"]},\"prod\":{\"patches\":[\"branch:master\",\"microFeatures\",\"smallFeatures\",\"mediumFeatures\",\"largeFeatures\"]}}}" http://localhost:1337/api/v1/repository/github
60 | ```
61 |
62 | ### Apply a Branch to a Context
63 | Now we can use all this data we stored. POST to the repository and branch you want to use with your application's context. In this case we're just including the expected "user.plan" object that is used in the patch rules. You might also include user id, user roles, a/b test group id, data center id, operating system type, computer name, etc to further vary the data.
64 |
65 | Notice we also include a "data" node in the context. The patches apply directly to the context at the node that is specified in the [patch](https://github.com/realcrowd/rennet/blob/master/models/Patch.js). The default location is "$.data", but you can change that in the patch. We use the [JSONPath package](https://www.npmjs.org/package/JSONPath) for locating where in the document hierarchy to apply patches and evaluate rules.
66 |
67 | ```
68 | curl -X POST -H "Content-Type: application/json" -d "{\"user\":{\"plan\":\"free\"},\"data\":{}}" http://localhost:1337/api/v1/repository/github/branch/qa/context
69 |
70 | curl -X POST -H "Content-Type: application/json" -d "{\"user\":{\"plan\":\"free\"},\"data\":{}}" http://localhost:1337/api/v1/repository/github/branch/prod/context
71 |
72 | curl -X POST -H "Content-Type: application/json" -d "{\"user\":{\"plan\":\"medium\"},\"data\":{}}" http://localhost:1337/api/v1/repository/github/branch/prod/context
73 | ```
74 |
75 | ## How to run me
76 |
77 | ```
78 | git clone https://github.com/realcrowd/rennet.git
79 |
80 | cd rennet
81 |
82 | npm install -g bower
83 |
84 | npm install
85 |
86 | bower install
87 |
88 | node http.js
89 | ```
90 |
91 | ## Contributing
92 | See our [Contributing](https://github.com/realcrowd/rennet/blob/master/CONTRIBUTING.md) doc for details on contributing. Just do it.
93 |
94 | ## TODO
95 | * Setup 'npm test'
96 | * DELETE support
97 | * Etag support (etag, if-match, if-none-match) for optimistic concurrency control
98 | * Logging
99 | * Performance monitoring
100 | * Load testing
101 | * Caching providers
102 | * User interface for managing patches and rules
103 | * More rules. i.e. multi rule, number comparison `>, <, >=, <=, ==, %`, user-defined script, ?
104 | * MongoDB, filesystem, other document storage options (what do you want?)
105 | * Client libraries
106 | * JSON error formatting
107 | * Authentication/Authorization
108 | * Refactor services to remove some boilerplate
--------------------------------------------------------------------------------
/Web.Debug.config:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
21 |
22 |
23 |
24 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/Web.config:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var path = require('path');
3 | var logger = require('morgan');
4 | var bodyParser = require('body-parser');
5 |
6 | var routes = require('./routes/index');
7 | var apiRoutes = require('./routes/api');
8 |
9 | var app = express();
10 |
11 | // view engine setup
12 | app.set('views', path.join(__dirname, 'views'));
13 | app.set('view engine', 'jade');
14 |
15 | app.use(logger('dev'));
16 | app.use(bodyParser.json());
17 | app.use(bodyParser.urlencoded({ extended: false }));
18 | app.use(require('stylus').middleware(path.join(__dirname, 'public')));
19 | app.use(express.static(path.join(__dirname, 'public')));
20 | app.use('/bower_components', express.static(path.join(__dirname, 'bower_components')));
21 | app.use('/', routes);
22 | app.use('/api/v1', apiRoutes);
23 |
24 | // catch 404 and forward to error handler
25 | app.use(function (req, res, next) {
26 | var err = new Error('Not Found');
27 | err.status = 404;
28 | next(err);
29 | });
30 |
31 | // error handlers
32 |
33 | // development error handler
34 | // will print stacktrace
35 | if (app.get('env') === 'development') {
36 | app.use(function (err, req, res, next) {
37 | res.status(err.status || 500);
38 | res.render('error', {
39 | message: err.message,
40 | error: err
41 | });
42 | });
43 | }
44 |
45 | // production error handler
46 | // no stacktraces leaked to user
47 | app.use(function (err, req, res, next) {
48 | res.status(err.status || 500);
49 | res.render('error', {
50 | message: err.message,
51 | error: {}
52 | });
53 | });
54 |
55 | module.exports = app;
56 |
--------------------------------------------------------------------------------
/bin/ChangeConfig.ps1:
--------------------------------------------------------------------------------
1 | $configFile = $args[0]
2 |
3 | Write-Host "Adding iisnode section to config file '$configFile'"
4 | $config = New-Object System.Xml.XmlDocument
5 | $config.load($configFile)
6 | $xpath = $config.CreateNavigator()
7 | $parentElement = $xpath.SelectSingleNode("//configuration/configSections/sectionGroup[@name='system.webServer']")
8 | $iisnodeElement = $parentElement.SelectSingleNode("//section[@name='iisnode']")
9 | if ($iisnodeElement) {
10 | Write-Host "Removing existing iisnode section from config file '$configFile'"
11 | $iisnodeElement.DeleteSelf()
12 | }
13 |
14 | $parentElement.AppendChild("")
15 | $config.Save($configFile)
16 |
--------------------------------------------------------------------------------
/bin/Microsoft.NodejsTools.WebRole.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realcrowd/rennet/a2eae14a449c656fe0e1525bed5f30353cdb2099/bin/Microsoft.NodejsTools.WebRole.dll
--------------------------------------------------------------------------------
/bin/download.ps1:
--------------------------------------------------------------------------------
1 | $runtimeUrl = $args[0]
2 | $overrideUrl = $args[1]
3 | $current = [string] (Get-Location -PSProvider FileSystem)
4 | $client = New-Object System.Net.WebClient
5 |
6 | function downloadWithRetry {
7 | param([string]$url, [string]$dest, [int]$retry)
8 | Write-Host
9 | Write-Host "Attempt: $retry"
10 | Write-Host
11 | trap {
12 | Write-Host $_.Exception.ToString()
13 | if ($retry -lt 5) {
14 | $retry=$retry+1
15 | Write-Host
16 | Write-Host "Waiting 5 seconds and retrying"
17 | Write-Host
18 | Start-Sleep -s 5
19 | downloadWithRetry $url $dest $retry $client
20 | }
21 | else {
22 | Write-Host "Download failed"
23 | throw "Max number of retries downloading [5] exceeded"
24 | }
25 | }
26 | $client.downloadfile($url, $dest)
27 | }
28 |
29 | function download($url, $dest) {
30 | Write-Host "Downloading $url"
31 | downloadWithRetry $url $dest 1
32 | }
33 |
34 | function copyOnVerify($file, $output) {
35 | Write-Host "Verifying $file"
36 | $verify = Get-AuthenticodeSignature $file
37 | Out-Host -InputObject $verify
38 | if ($verify.Status -ne "Valid") {
39 | throw "Invalid signature for runtime package $file"
40 | }
41 | else {
42 | mv $file $output
43 | }
44 | }
45 |
46 | if ($overrideUrl) {
47 | Write-Host "Using override url: $overrideUrl"
48 | $url = $overrideUrl
49 | }
50 | else {
51 | $url = $runtimeUrl
52 | }
53 |
54 | foreach($singleUrl in $url -split ";")
55 | {
56 | $suffix = Get-Random
57 | $downloaddir = $current + "\sandbox" + $suffix
58 | mkdir $downloaddir
59 | $dest = $downloaddir + "\sandbox.exe"
60 | download $singleUrl $dest
61 | $final = $downloaddir + "\runtime.exe"
62 | copyOnVerify $dest $final
63 | if (Test-Path -LiteralPath $final)
64 | {
65 | cd $downloaddir
66 | if ($host.Version.Major -eq 3)
67 | {
68 | .\runtime.exe -y | Out-Null
69 | .\setup.cmd
70 | }
71 | else
72 | {
73 | Start-Process -FilePath $final -ArgumentList -y -Wait
74 | $cmd = $downloaddir + "\setup.cmd"
75 | Start-Process -FilePath $cmd -Wait
76 | }
77 | }
78 | else
79 | {
80 | throw "Unable to verify package"
81 | }
82 | cd $current
83 | if (Test-Path -LiteralPath $downloaddir)
84 | {
85 | Remove-Item -LiteralPath $downloaddir -Force -Recurse
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/bin/node.cmd:
--------------------------------------------------------------------------------
1 | node.exe %1 %2 %3
--------------------------------------------------------------------------------
/bin/setup_web.cmd:
--------------------------------------------------------------------------------
1 | @echo on
2 |
3 | cd /d "%~dp0"
4 |
5 | if "%EMULATED%"=="true" if DEFINED APPCMD goto emulator_setup
6 | if "%EMULATED%"== "true" exit /b 0
7 |
8 | echo Granting permissions for Network Service to the web root directory...
9 | icacls ..\ /grant "Network Service":(OI)(CI)W
10 | if %ERRORLEVEL% neq 0 goto error
11 | echo OK
12 |
13 | echo Configuring powershell permissions
14 | powershell -c "set-executionpolicy unrestricted"
15 |
16 | echo Downloading and installing runtime components
17 | powershell .\download.ps1 '%RUNTIMEURL%' '%RUNTIMEURLOVERRIDE%'
18 | if %ERRORLEVEL% neq 0 goto error
19 |
20 | echo SUCCESS
21 | exit /b 0
22 |
23 | :error
24 | echo FAILED
25 | exit /b -1
26 |
27 | :emulator_setup
28 | echo Running in emulator adding iisnode to application host config
29 | FOR /F "tokens=1,2 delims=/" %%a in ("%APPCMD%") DO set FN=%%a&set OPN=%%b
30 | if "%OPN%"=="%OPN:apphostconfig:=%" (
31 | echo "Could not parse appcmd '%appcmd% for configuration file, exiting"
32 | goto error
33 | )
34 |
35 | set IISNODE_BINARY_DIRECTORY=%programfiles(x86)%\iisnode-dev\release\x64
36 | set IISNODE_SCHEMA=%programfiles(x86)%\iisnode-dev\release\x64\iisnode_schema.xml
37 |
38 | if "%PROCESSOR_ARCHITECTURE%"=="AMD64" goto start
39 | set IISNODE_BINARY_DIRECTORY=%programfiles%\iisnode-dev\release\x86
40 | set IISNODE_SCHEMA=%programfiles%\iisnode-dev\release\x86\iisnode_schema_x86.xml
41 |
42 |
43 | :start
44 | set
45 |
46 | echo Using iisnode binaries location '%IISNODE_BINARY_DIRECTORY%'
47 | echo installing iisnode module using AppCMD alias %appcmd%
48 | %appcmd% install module /name:"iisnode" /image:"%IISNODE_BINARY_DIRECTORY%\iisnode.dll"
49 |
50 | set apphostconfigfile=%OPN:apphostconfig:=%
51 | powershell -c "set-executionpolicy unrestricted"
52 | powershell .\ChangeConfig.ps1 %apphostconfigfile%
53 | if %ERRORLEVEL% neq 0 goto error
54 |
55 | copy /y "%IISNODE_SCHEMA%" "%programfiles%\IIS Express\config\schema\iisnode_schema.xml"
56 | if %ERRORLEVEL% neq 0 goto error
57 | exit /b 0
58 |
--------------------------------------------------------------------------------
/bin/www:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | var debug = require('debug')('rennet');
3 | var app = require('../app');
4 |
5 | app.set('port', process.env.PORT || 3000);
6 |
7 | var server = app.listen(app.get('port'), function() {
8 | debug('Express server listening on port ' + server.address().port);
9 | });
10 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rennet",
3 | "version": "1.0.0",
4 | "authors": [
5 | "JD Conley "
6 | ],
7 | "description": "Coagulating application metadata",
8 | "main": "app.js",
9 | "moduleType": [
10 | "node"
11 | ],
12 | "keywords": [
13 | "metadata",
14 | "features",
15 | "stuff"
16 | ],
17 | "license": "MIT",
18 | "homepage": "https://github.com/realcrowd/rennet",
19 | "private": true,
20 | "ignore": [
21 | "**/.*",
22 | "node_modules",
23 | "bower_components",
24 | "test",
25 | "tests"
26 | ],
27 | "dependencies": {
28 | "angularjs": "~1.3.2",
29 | "bootstrap": "~3.3.0",
30 | "html5shiv": "~3.7.2",
31 | "respond": "~1.4.2"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/config/azure.json:
--------------------------------------------------------------------------------
1 | {
2 | "PatchService": {
3 | "dataProvider":{
4 | "module": "DocumentDbDataProvider",
5 | "configKey": "DocumentDb"
6 | }
7 | },
8 | "RepositoryIndexService": {
9 | "dataProvider":{
10 | "module": "DocumentDbDataProvider",
11 | "configKey": "DocumentDb"
12 | }
13 | },
14 | "DocumentDb": {
15 | "urlConnection": "https://rennet-oss.documents.azure.com:443/",
16 | "auth": {"masterKey":"7rvoVz+UvuFOnxjCGbMilgdRlJX3r1KS8VWwaYVoDWo87KbXggUqa98shuTAVCeW9kn3a2I1PwH5qMB+Q659ng=="},
17 | "databaseDefinition": {"id": "rennet-demo"},
18 | "collectionDefinition": {"id": "rennet"},
19 | "connectionPolicy": null,
20 | "consistencyLevel": "Session"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/config/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "PatchService": {
3 | "dataProvider": {
4 | "module": "InProcessDataProvider"
5 | }
6 | },
7 | "RepositoryIndexService": {
8 | "dataProvider": {
9 | "module": "InProcessDataProvider"
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/config/redis.json:
--------------------------------------------------------------------------------
1 | {
2 | "PatchService": {
3 | "dataProvider":{
4 | "module": "RedisDataProvider",
5 | "configKey": "Redis"
6 | }
7 | },
8 | "RepositoryIndexService": {
9 | "dataProvider":{
10 | "module": "RedisDataProvider",
11 | "configKey": "Redis"
12 | }
13 | },
14 | "Redis": {
15 | "host": "127.0.0.1",
16 | "port": 6379,
17 | "options": {}
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/config/test.json:
--------------------------------------------------------------------------------
1 | {
2 | "DocumentDb": {
3 | "urlConnection": "https://rennet-oss.documents.azure.com:443/",
4 | "auth": {"masterKey":"7rvoVz+UvuFOnxjCGbMilgdRlJX3r1KS8VWwaYVoDWo87KbXggUqa98shuTAVCeW9kn3a2I1PwH5qMB+Q659ng=="},
5 | "databaseDefinition": {"id": "rennet-demo"},
6 | "collectionDefinition": {"id": "rennet-tests"},
7 | "connectionPolicy":null,
8 | "consistencyLevel":"Session"
9 | },
10 | "Redis": {
11 | "host": "127.0.0.1",
12 | "port": 6379,
13 | "options": {}
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/http.js:
--------------------------------------------------------------------------------
1 | var app = require('./app');
2 | var http = require('http');
3 |
4 | app.set('port', process.env.PORT || 1337);
5 |
6 | var server = http.createServer(app);
7 | server.listen(app.get('port'), function(){
8 | console.log("Rennet server listening on port " + app.get('port'));
9 | });
10 |
--------------------------------------------------------------------------------
/models/Patch.js:
--------------------------------------------------------------------------------
1 | var Q = require('q');
2 | var extend = require('extend');
3 | var jsonPathEngine = require('JSONPath');
4 |
5 | var Patch = function (obj) {
6 | this.id = null;
7 | this.data = {};
8 | this.applyAtJsonPath = "$.data"; //apply at the "data" node of the context by default
9 | this.rule = null;
10 |
11 | for (var prop in obj) {
12 | this[prop] = obj[prop];
13 | }
14 |
15 | //Setup expected prototype for the rule
16 | if (this.rule && this.rule.name) {
17 | var RuleConstructor = require('./rules')[this.rule.name];
18 | this.rule = new RuleConstructor(this.rule);
19 | }
20 | };
21 |
22 | Patch.prototype.shouldApply = function (context) {
23 | //No rule means apply no matter what, for simplicity of the JSON.
24 | if (!this.rule) {
25 | return Q(true);
26 | }
27 |
28 | return this.rule.evaluate(context);
29 | };
30 |
31 | Patch.prototype.apply = function (context) {
32 | var matchingNodes = jsonPathEngine.eval(context, this.applyAtJsonPath);
33 | if (matchingNodes.length == 0) {
34 | throw new Error('Nothing found in context at "' + this.applyAtJsonPath + '". Unable to apply patch.')
35 | }
36 |
37 | //go patch all the paths it found
38 | var len = matchingNodes.length;
39 | for (var i = 0; i < len; i++) {
40 | extend(true, matchingNodes[i], this.data);
41 | }
42 |
43 | return Q(context);
44 | };
45 |
46 | module.exports = Patch;
--------------------------------------------------------------------------------
/models/RepositoryIndex.js:
--------------------------------------------------------------------------------
1 | var RepositoryIndex = function (obj) {
2 | this.id = null;
3 | this.branches = {};
4 |
5 | for (var prop in obj) {
6 | this[prop] = obj[prop];
7 | }
8 | };
9 |
10 | module.exports = RepositoryIndex;
--------------------------------------------------------------------------------
/models/Rule.js:
--------------------------------------------------------------------------------
1 | var Rule = function () {
2 | this.name = null;
3 | this.arguments = {};
4 | };
5 |
6 | Rule.prototype.evaluate = function (context) {
7 | throw new Error("Base Rule cannot be evaluated. Use one of the subclasses in the 'rules' namespace instead.");
8 | };
9 |
10 | module.exports = Rule;
--------------------------------------------------------------------------------
/models/rules/AlwaysOffRule.js:
--------------------------------------------------------------------------------
1 | var Rule = require('../Rule');
2 | var Q = require('q');
3 |
4 | var AlwaysOffRule = function (obj) {
5 | this.name = "AlwaysOffRule";
6 | this.arguments = {};
7 |
8 | for (var prop in obj) {
9 | this[prop] = obj[prop];
10 | }
11 | };
12 |
13 | AlwaysOffRule.prototype = new Rule();
14 |
15 | AlwaysOffRule.prototype.evaluate = function(context) {
16 | return Q(false);
17 | };
18 |
19 | module.exports = AlwaysOffRule;
--------------------------------------------------------------------------------
/models/rules/AlwaysOnRule.js:
--------------------------------------------------------------------------------
1 | var Rule = require('../Rule');
2 | var Q = require('q');
3 |
4 | var AlwaysOnRule = function (obj) {
5 | this.name = "AlwaysOnRule";
6 | this.arguments = {};
7 |
8 | for (var prop in obj) {
9 | this[prop] = obj[prop];
10 | }
11 | };
12 |
13 | AlwaysOnRule.prototype = new Rule();
14 |
15 | AlwaysOnRule.prototype.evaluate = function(context) {
16 | return Q(true);
17 | };
18 |
19 | module.exports = AlwaysOnRule;
--------------------------------------------------------------------------------
/models/rules/ArrayContainsRule.js:
--------------------------------------------------------------------------------
1 | var Rule = require('../Rule');
2 | var jsonPathEngine = require('JSONPath');
3 | var Q = require('q');
4 |
5 | var ArrayContainsRule = function (obj) {
6 | this.name = "ArrayContainsRule";
7 | this.arguments = {};
8 |
9 | for (var prop in obj) {
10 | this[prop] = obj[prop];
11 | }
12 | };
13 | ArrayContainsRule.prototype = new Rule();
14 |
15 | ArrayContainsRule.prototype.arguments = {
16 | jsonPath: '', //Where on the context this array lives
17 | matches: '' //Regex match for the item in the array
18 | };
19 |
20 | ArrayContainsRule.prototype.evaluate = function (context) {
21 | var arrayToEvaluate = jsonPathEngine.eval(context, this.arguments.jsonPath);
22 |
23 | if (!Array.isArray(arrayToEvaluate)) {
24 | return Q(false);
25 | }
26 |
27 | var rx = new RegExp(this.arguments.matches);
28 | var len = arrayToEvaluate.length;
29 | for (var i = 0; i < len; i++) {
30 | if (String(arrayToEvaluate[i]).match(rx) !== null) {
31 | return Q(true);
32 | }
33 | }
34 |
35 | return Q(false);
36 | };
37 |
38 | module.exports = ArrayContainsRule;
--------------------------------------------------------------------------------
/models/rules/StringMatchesRule.js:
--------------------------------------------------------------------------------
1 | var Rule = require('../Rule');
2 | var jsonPathEngine = require('JSONPath');
3 | var Q = require('q');
4 |
5 | var StringMatchesRule = function (obj) {
6 | this.name = "StringMatchesRule";
7 | this.arguments = {};
8 |
9 | for (var prop in obj) {
10 | this[prop] = obj[prop];
11 | }
12 | };
13 | StringMatchesRule.prototype = new Rule();
14 |
15 | StringMatchesRule.prototype.arguments = {
16 | jsonPath: '', //Where on the context this value lives (https://github.com/s3u/JSONPath)
17 | matches: '' //Regex match for the value
18 | };
19 |
20 | StringMatchesRule.prototype.evaluate = function (context) {
21 | var matchingNodes = jsonPathEngine.eval(context, this.arguments.jsonPath);
22 |
23 | if (matchingNodes.length == 0) {
24 | throw new Error('Nothing found in context at "' + applyAtJsonPath + '". Unable to apply patch.')
25 | }
26 |
27 | //make sure the strings all match
28 | //todo: make this behavior configurable
29 | var len = matchingNodes.length;
30 | for (var i = 0; i < len; i++) {
31 | if (String(matchingNodes[i]).match(this.arguments.matches) === null) {
32 | return Q(false);
33 | }
34 | }
35 |
36 | return Q(true);
37 | };
38 |
39 | module.exports = StringMatchesRule;
--------------------------------------------------------------------------------
/models/rules/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | AlwaysOnRule:require('./AlwaysOnRule'),
3 | AlwaysOffRule:require('./AlwaysOffRule'),
4 | ArrayContainsRule:require('./ArrayContainsRule'),
5 | StringMatchesRule:require('./StringMatchesRule')
6 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rennet",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "node ./bin/www"
7 | },
8 | "description": "rennet",
9 | "author": {
10 | "name": "JD Conley",
11 | "email": "jd@realcrowd.com"
12 | },
13 | "dependencies": {
14 | "JSONPath": "^0.10.0",
15 | "body-parser": "~1.8.1",
16 | "bower": "^1.3.12",
17 | "config": "^1.7.0",
18 | "debug": "~2.0.0",
19 | "documentdb": "0.9.1",
20 | "express": "~4.9.0",
21 | "extend": "^2.0.0",
22 | "jade": "~1.6.0",
23 | "mocha": "^2.0.1",
24 | "morgan": "~1.3.0",
25 | "node-extend": "^0.2.0",
26 | "node-uuid": "^1.4.2",
27 | "q": "^1.1.1",
28 | "redis": "^0.12.1",
29 | "serve-favicon": "~2.1.3",
30 | "stylus": "0.42.3"
31 | },
32 | "devDependencies": {
33 | "mocha": "^2.0.1"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/providers/DataProvider.js:
--------------------------------------------------------------------------------
1 | var Q = require('q');
2 |
3 | var DataProvider = function() {
4 | };
5 |
6 | DataProvider.prototype.getDocument = function(id) {
7 | throw new Error("Not implemented");
8 | };
9 |
10 | DataProvider.prototype.getDocuments = function(ids) {
11 | //naive parallel implementation is the default.
12 | //smart providers will do this on their own.
13 | var dataProvider = this;
14 | return Q().then(function(){
15 | return ids.map(dataProvider.getDocument, dataProvider);
16 | }).all();
17 | };
18 |
19 | DataProvider.prototype.putDocument = function(id, document) {
20 | throw new Error("Not implemented");
21 | };
22 |
23 | DataProvider.prototype.applyConfiguration = function(config) {
24 | throw new Error("Not implemented");
25 | };
26 |
27 | module.exports = DataProvider;
--------------------------------------------------------------------------------
/providers/DocumentDbDataProvider.js:
--------------------------------------------------------------------------------
1 | var Q = require('q');
2 | var DocumentClientWrapper = require('documentdb').DocumentClientWrapper;
3 | var extend = require('extend');
4 | var DataProvider = require('./DataProvider');
5 |
6 | var DocumentDbDataProvider = function(config) {
7 | this.client = null;
8 | this.database = null;
9 | this.collection = null;
10 |
11 | if (config) {
12 | this.applyConfiguration(config);
13 | }
14 | };
15 |
16 | DocumentDbDataProvider.prototype = new DataProvider();
17 |
18 | DocumentDbDataProvider.prototype.applyConfiguration = function(config) {
19 | this.config = config;
20 |
21 | this.client = new DocumentClientWrapper(
22 | this.config.urlConnection,
23 | this.config.auth,
24 | this.config.connectionPolicy,
25 | this.config.consistencyLevel);
26 | };
27 |
28 |
29 | DocumentDbDataProvider.prototype.getDocumentClient = function() {
30 | if (this.database && this.collection && this.client) {
31 | return Q(this.client);
32 | }
33 |
34 | var that = this;
35 |
36 | return that.getOrCreateDatabase(that.client, that.config.databaseDefinition)
37 | .then(function(database) {
38 | that.database = database;
39 | return that.getOrCreateCollection(that.client, database, that.config.collectionDefinition)
40 | .then(function(collection){
41 | that.collection = collection;
42 | return that.client;
43 | });
44 | });
45 | };
46 |
47 | DocumentDbDataProvider.prototype.getDocument = function(id) {
48 | var that = this;
49 | return this.getDocumentClient()
50 | .then(function(client) {
51 | return client
52 | .queryDocuments(that.collection._self, 'SELECT * FROM root r WHERE r.id="' + that.escapeParam(id) + '"')
53 | .toArrayAsync()
54 | .then(function(queryResult){
55 | if (queryResult.feed.length === 0) {
56 | return null;
57 | }
58 |
59 | return queryResult.feed[0];
60 | });
61 | });
62 | };
63 |
64 | DocumentDbDataProvider.prototype.putDocument = function(id, document) {
65 | var that = this;
66 |
67 | return that.getDocument(id)
68 | .then(function(updatedDocument){
69 | if (updatedDocument) {
70 | extend(true, updatedDocument, document);
71 |
72 | return that.client.replaceDocumentAsync(updatedDocument._self, updatedDocument);
73 | }
74 |
75 | updatedDocument = {
76 | id: id
77 | };
78 |
79 | extend(true, updatedDocument, document);
80 |
81 | return that.client.createDocumentAsync(that.collection._self, updatedDocument);
82 | })
83 | .then(function(createOrReplaceResult){
84 | return createOrReplaceResult.resource;
85 | });
86 | };
87 |
88 | DocumentDbDataProvider.prototype.escapeParam = function(paramValue) {
89 | return paramValue.replace('"', '\\"');
90 | };
91 |
92 | DocumentDbDataProvider.prototype.getOrCreateRootEntity = function(definition, queryFunc, queryThisArg, createFunc, createThisArg) {
93 | return queryFunc.call(queryThisArg, 'SELECT * FROM root r WHERE r.id="' + this.escapeParam(definition.id) + '"')
94 | .toArrayAsync()
95 | .then(function(queryResult) {
96 | if (queryResult.feed.length === 0) {
97 | return createFunc.call(createThisArg, definition)
98 | .then(function(createResult){
99 | return createResult.resource;
100 | });
101 | }
102 |
103 | return queryResult.feed[0];
104 | });
105 | };
106 |
107 | DocumentDbDataProvider.prototype.getOrCreateCollection = function(client, database, collectionDefinition) {
108 | return this.getOrCreateRootEntity(
109 | collectionDefinition,
110 | function(query){return client.queryCollections(database._self, query);},
111 | this,
112 | function(definition) { return client.createCollectionAsync(database._self, definition); },
113 | this);
114 | };
115 |
116 | DocumentDbDataProvider.prototype.getOrCreateDatabase = function(client, databaseDefinition) {
117 | return this.getOrCreateRootEntity(
118 | databaseDefinition,
119 | client.queryDatabases,
120 | client,
121 | client.createDatabaseAsync,
122 | client);
123 | };
124 |
125 | module.exports = DocumentDbDataProvider;
--------------------------------------------------------------------------------
/providers/InProcessDataProvider.js:
--------------------------------------------------------------------------------
1 | var DataProvider = require('./DataProvider');
2 | var Q = require('q');
3 |
4 | var InProcessDataProvider = function(config) {
5 | this.config = config;
6 | this.data = {};
7 | };
8 |
9 | InProcessDataProvider.prototype = new DataProvider();
10 |
11 | InProcessDataProvider.prototype.getDocument = function(id) {
12 | if (!this.data.hasOwnProperty(id)) {
13 | return Q(null);
14 | }
15 |
16 | var docAsJson = this.data[id];
17 | var document = JSON.parse(docAsJson);
18 |
19 | return Q(document);
20 | };
21 |
22 | InProcessDataProvider.prototype.putDocument = function(id, document) {
23 | var docAsJson = JSON.stringify(document);
24 | this.data[id] = docAsJson;
25 |
26 | var cloneForReturn = JSON.parse(docAsJson);
27 | return Q(cloneForReturn);
28 | };
29 |
30 | InProcessDataProvider.prototype.applyConfiguration = function(config) {
31 | this.config = config;
32 | };
33 |
34 |
35 | module.exports = InProcessDataProvider;
--------------------------------------------------------------------------------
/providers/RedisDataProvider.js:
--------------------------------------------------------------------------------
1 | var DataProvider = require('./DataProvider');
2 | var Q = require('q');
3 | var redis = require('redis');
4 | var extend = require('extend');
5 |
6 | var RedisDataProvider = function(config) {
7 | this.client = null;
8 |
9 | this.applyConfiguration(config);
10 | };
11 |
12 | RedisDataProvider.prototype = new DataProvider();
13 |
14 | RedisDataProvider.prototype.getDocumentId = function(id) {
15 | return "rennet:doc:" + id;
16 | };
17 |
18 | RedisDataProvider.prototype.getDocument = function(id) {
19 | return Q.ninvoke(this.client, "get", this.getDocumentId(id))
20 | .then(function(docAsJson) {
21 | return JSON.parse(docAsJson);
22 | });
23 | };
24 |
25 | RedisDataProvider.prototype.putDocument = function(id, document) {
26 | var docAsJson = JSON.stringify(document);
27 | return Q.ninvoke(this.client, "set", this.getDocumentId(id), docAsJson)
28 | .then(function() {
29 | return document;
30 | });
31 | };
32 |
33 | RedisDataProvider.prototype.applyConfiguration = function(config) {
34 | this.config = config;
35 |
36 | var defaultConfig = {
37 | host: '127.0.0.1',
38 | port: 6379,
39 | options: {}
40 | };
41 |
42 | this.config = extend(true, defaultConfig, config);
43 |
44 | this.client = redis.createClient(this.config.port, this.config.host, this.config.options);
45 | };
46 |
47 |
48 | module.exports = RedisDataProvider;
--------------------------------------------------------------------------------
/public/js/rennet.js:
--------------------------------------------------------------------------------
1 | var rennetApp = angular.module('rennetApp', []);
2 |
3 | rennetApp.controller('RennetController', ['$scope', function($scope) {
4 | $scope.value = new Date();
5 | }]);
--------------------------------------------------------------------------------
/public/stylesheets/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding: 50px;
3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
4 | }
5 | a {
6 | color: #00b7ff;
7 | }
8 |
--------------------------------------------------------------------------------
/public/stylesheets/style.styl:
--------------------------------------------------------------------------------
1 | body
2 | padding: 50px
3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif
4 | a
5 | color: #00B7FF
--------------------------------------------------------------------------------
/rennet.njsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 11.0
5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
6 | rennet
7 | rennet
8 |
9 |
10 |
11 | Debug
12 | 2.0
13 | da8ae268-f9be-4cf1-97f0-425547866e53
14 | .
15 | bin\www
16 |
17 |
18 | .
19 | .
20 | v4.0
21 | {3AF33F2E-1136-4D97-BBB7-1795711AC8B8};{349c5851-65df-11da-9384-00065b846f21};{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}
22 | ShowAllFiles
23 | 1337
24 | true
25 |
26 |
27 | true
28 |
29 |
30 | true
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | Code
72 | Mocha
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 | False
149 | True
150 | 0
151 | /
152 | http://localhost:48022/
153 | False
154 | True
155 | http://localhost:1337
156 | False
157 |
158 |
159 |
160 |
161 |
162 |
163 | CurrentPage
164 | True
165 | False
166 | False
167 | False
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 | False
177 | False
178 |
179 |
180 |
181 |
182 |
--------------------------------------------------------------------------------
/rennet.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 2013
4 | VisualStudioVersion = 12.0.30723.0
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}") = "rennet", "rennet.njsproj", "{DA8AE268-F9BE-4CF1-97F0-425547866E53}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {DA8AE268-F9BE-4CF1-97F0-425547866E53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {DA8AE268-F9BE-4CF1-97F0-425547866E53}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {DA8AE268-F9BE-4CF1-97F0-425547866E53}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {DA8AE268-F9BE-4CF1-97F0-425547866E53}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | EndGlobal
23 |
--------------------------------------------------------------------------------
/routes/api.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var router = express.Router();
3 | var RennetService = require('../services/RennetService');
4 | var rennetService = new RennetService();
5 |
6 | var putRepository = function(req, res, next) {
7 | //TODO: handle etags
8 |
9 | rennetService.repositoryIndexService
10 | .putRepositoryIndex(req.body)
11 | .then(function(repositoryIndex){
12 | res.send(repositoryIndex);
13 | })
14 | .catch(next);
15 | };
16 |
17 | /*
18 | * POST/PUT to create or update a repository
19 | */
20 | router.post('/repository/:repositoryId', putRepository);
21 | router.put('/repository/:repositoryId', putRepository);
22 |
23 | router.get('/repository/:repositoryId', function (req, res, next) {
24 | rennetService.repositoryIndexService
25 | .getRepositoryIndex(req.params.repositoryId)
26 | .then(function(repositoryIndex){
27 | res.send(repositoryIndex);
28 | })
29 | .catch(next);
30 | });
31 |
32 | var putPatch = function(req, res, next) {
33 | //TODO: handle etags
34 |
35 | rennetService.patchService
36 | .putPatch(req.params.repositoryId, req.body)
37 | .then(function(patch){
38 | res.send(patch);
39 | })
40 | .catch(next);
41 | };
42 |
43 |
44 | /*
45 | * POST/PUT to create or update a patch
46 | */
47 | router.post('/repository/:repositoryId/patch/:patchId', putPatch);
48 | router.put('/repository/:repositoryId/patch/:patchId', putPatch);
49 |
50 | router.get('/repository/:repositoryId/patch/:patchId', function (req, res, next) {
51 | rennetService.patchService
52 | .getPatch(req.params.repositoryId, req.params.patchId)
53 | .then(function(patch){
54 | res.send(patch);
55 | })
56 | .catch(next);
57 | });
58 |
59 | /*
60 | * POST the context, returning the data set for the repository/branch
61 | */
62 | router.post('/repository/:repositoryId/branch/:branchId/context', function (req, res, next) {
63 |
64 | rennetService
65 | .resolveContext(req.params.repositoryId, req.params.branchId, req.body)
66 | .then(function(context){
67 | res.send(context);
68 | })
69 | .catch(next);
70 | });
71 |
72 | module.exports = router;
--------------------------------------------------------------------------------
/routes/index.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var router = express.Router();
3 |
4 | /* GET home page. */
5 | router.get('/', function (req, res) {
6 | res.render('index', { title: 'Rennet' });
7 | });
8 |
9 | module.exports = router;
--------------------------------------------------------------------------------
/services/DataService.js:
--------------------------------------------------------------------------------
1 | var Q = require('q');
2 | var extend = require('extend');
3 | var config = require('config');
4 |
5 | var DataService = function() {
6 | };
7 |
8 | DataService.prototype.applyConfiguration = function(configuration) {
9 | if (typeof configuration == "string" || configuration instanceof String) {
10 | //load config from config section
11 | this.configuration = config.get(configuration);
12 | } else {
13 | //load config from object
14 | this.configuration = configuration;
15 | }
16 |
17 | //load our data provider based on our configuration
18 | var DataProvider = require('../providers/' + this.configuration.dataProvider.module);
19 | this.dataProvider = new DataProvider();
20 | var dataProviderConfig = (this.configuration.dataProvider.hasOwnProperty("configKey") && config.has(this.configuration.dataProvider.configKey))
21 | ? config.get(this.configuration.dataProvider.configKey)
22 | : {};
23 | this.dataProvider.applyConfiguration(dataProviderConfig);
24 | };
25 |
26 | module.exports = DataService;
--------------------------------------------------------------------------------
/services/PatchService.js:
--------------------------------------------------------------------------------
1 | var Q = require('q');
2 | var Patch = require('../models/Patch');
3 | var DataService = require('./DataService');
4 |
5 | var PatchService = function(configuration){
6 | configuration = configuration || "PatchService";
7 | this.applyConfiguration(configuration);
8 | };
9 |
10 | PatchService.prototype = new DataService();
11 |
12 | PatchService.prototype.getPatchDocumentId = function(repositoryId, id) {
13 | return "Patch-" + repositoryId + "-" + id;
14 | };
15 |
16 | PatchService.prototype.getPatch = function(repositoryId, id) {
17 | var documentId = this.getPatchDocumentId(repositoryId, id);
18 | return this.dataProvider
19 | .getDocument(documentId)
20 | .then(function(document){
21 | return new Patch(document.data);
22 | });
23 | };
24 |
25 | PatchService.prototype.getPatches = function(repositoryId, patchIds) {
26 | var patchService = this;
27 |
28 | return this.dataProvider
29 | .getDocuments(patchIds.map(function(patchId) {
30 | return patchService.getPatchDocumentId(repositoryId, patchId);
31 | }))
32 | .then(function(documents) {
33 | return documents.map(function(document){
34 | return new Patch(document.data);
35 | });
36 | });
37 | };
38 |
39 | PatchService.prototype.putPatch = function(repositoryId, patch) {
40 | var documentId = this.getPatchDocumentId(repositoryId, patch.id);
41 | return this.dataProvider
42 | .putDocument(documentId, { data: patch, type: "Patch" })
43 | .then(function(document){
44 | return new Patch(document.data);
45 | });
46 | };
47 |
48 | module.exports = PatchService;
--------------------------------------------------------------------------------
/services/RennetService.js:
--------------------------------------------------------------------------------
1 | var Q = require('q');
2 | var Patch = require('../models/Patch');
3 | var PatchService = require('./PatchService');
4 | var RepositoryIndexService = require('./RepositoryIndexService');
5 |
6 | var RennetService = function(configuration) {
7 | this.branchPatchIdPrefix = 'branch:';
8 |
9 | if (configuration && configuration.patchService) {
10 | this.patchService = new PatchService(configuration.patchService);
11 | } else {
12 | this.patchService = new PatchService()
13 | }
14 |
15 | if (configuration && configuration.repositoryIndexService) {
16 | this.repositoryIndexService = new RepositoryIndexService(configuration.repositoryIndexService);
17 | } else {
18 | this.repositoryIndexService = new RepositoryIndexService();
19 | }
20 | };
21 |
22 | RennetService.prototype.expandPatchList = function(repositoryIndex, branch, depth) {
23 | var results = [];
24 |
25 | depth = (depth || 0) + 1;
26 |
27 | //arbitrary max depth. todo: reference loop checking
28 | if (depth > 100) {
29 | throw new Error("Max branch depth of 100 reached. Consider nesting fewer branches. Did you create an infinite reference loop?");
30 | }
31 |
32 | var numPatchesInBranch = branch.patches.length;
33 | for (var i = 0; i < numPatchesInBranch; i++)
34 | {
35 | var patchId = branch.patches[i];
36 | if (patchId.indexOf(this.branchPatchIdPrefix) === 0) {
37 | var subBranchId = patchId.substring(this.branchPatchIdPrefix.length);
38 | var subBranchPatchIds = this.expandPatchList(repositoryIndex, repositoryIndex.branches[subBranchId], depth);
39 | var numSubPatches = subBranchPatchIds.length;
40 | for (var j = 0; j < numSubPatches; j++) {
41 | results.push(subBranchPatchIds[j]);
42 | }
43 | } else {
44 | results.push(patchId);
45 | }
46 | }
47 |
48 | return results;
49 | };
50 |
51 | RennetService.prototype.resolveContext = function(repositoryId, branchId, context) {
52 | if (!repositoryId) {
53 | throw new Error("A repositoryId is required");
54 | }
55 | if (!branchId) {
56 | throw new Error("A branchId is required");
57 | }
58 |
59 | var patchService = this.patchService;
60 | var rennetService = this;
61 |
62 | //get the repository index so we know how to apply patches
63 | return this.repositoryIndexService.getRepositoryIndex(repositoryId)
64 | .then(function(repositoryIndex) {
65 | if (!repositoryIndex) {
66 | throw new Error("Repository Index not found for " + repositoryId);
67 | }
68 |
69 | if (!repositoryIndex.branches[branchId]) {
70 | throw new Error(branchId + " branchId not found in repository index");
71 | }
72 |
73 | //the expanded, in order, list of patches to apply
74 | var patchList = rennetService.expandPatchList(repositoryIndex, repositoryIndex.branches[branchId]);
75 |
76 | //this is going to load all the patches into memory a couple times.
77 | //might want to be smarter about this in the future and add more round trips to the provider.
78 | return patchService.getPatches(repositoryId, patchList);
79 | })
80 | .then(function(patches) {
81 | //we have the list of patches to apply, in order. do it.
82 | //chain the promises so they execute serially and not in parallel
83 | var currentPromise = Q();
84 | var promises = patches.map(function(patch) {
85 | return currentPromise = currentPromise.then(function() {
86 | return patch.shouldApply(context);
87 | }).then(function(shouldApply){
88 | if (shouldApply) {
89 | return patch.apply(context);
90 | } else {
91 | return context;
92 | }
93 | });
94 | });
95 |
96 | return Q.all(promises)
97 | .then(function(results) {
98 | return context;
99 | });
100 | });
101 | };
102 |
103 | module.exports = RennetService;
--------------------------------------------------------------------------------
/services/RepositoryIndexService.js:
--------------------------------------------------------------------------------
1 | var RepositoryIndex = require('../models/RepositoryIndex');
2 | var DataService = require('./DataService');
3 |
4 | var RepositoryIndexService = function(configuration){
5 | configuration = configuration || "RepositoryIndexService";
6 | this.applyConfiguration(configuration);
7 | };
8 |
9 | RepositoryIndexService.prototype = new DataService();
10 |
11 |
12 | RepositoryIndexService.prototype.getRepositoryIndexDocumentId = function(repositoryId) {
13 | return "RepositoryIndex-" + repositoryId;
14 | };
15 |
16 | RepositoryIndexService.prototype.getRepositoryIndex = function(repositoryId) {
17 | var documentId = this.getRepositoryIndexDocumentId(repositoryId);
18 | return this.dataProvider
19 | .getDocument(documentId)
20 | .then(function(document){
21 | return new RepositoryIndex(document.data);
22 | });
23 | };
24 |
25 | RepositoryIndexService.prototype.putRepositoryIndex = function(repositoryIndex) {
26 | var documentId = this.getRepositoryIndexDocumentId(repositoryIndex.id);
27 | return this.dataProvider
28 | .putDocument(documentId, { data: repositoryIndex, type: "RepositoryIndex" })
29 | .then(function(document){
30 | return new RepositoryIndex(document.data);
31 | });
32 | };
33 |
34 | module.exports = RepositoryIndexService;
35 |
--------------------------------------------------------------------------------
/tests/dataProviderTests.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 | var uuid = require('node-uuid');
3 | var config = require('config');
4 | var Q = require('q');
5 |
6 | describe('Data Providers', function () {
7 | var providersToTest = [
8 | {
9 | constructor: require('../providers/InProcessDataProvider'),
10 | configuration: null
11 | },
12 | {
13 | constructor: require('../providers/DocumentDbDataProvider'),
14 | configuration: config.get('DocumentDb')
15 | },
16 | {
17 | constructor: require('../providers/RedisDataProvider'),
18 | configuration: config.get('Redis')
19 | }
20 | ];
21 |
22 | it('can CRU', function(done){
23 | var currentPromise = Q();
24 | var expectedObjs = [];
25 |
26 | providersToTest.map(function(providerDefinition) {
27 | var Provider = providerDefinition.constructor;
28 | var provider = new Provider();
29 | if (providerDefinition.configuration) {
30 | provider.applyConfiguration(providerDefinition.configuration);
31 | }
32 |
33 | currentPromise = currentPromise.then(function(){
34 | var obj = {
35 | id: uuid.v1(),
36 | branches: {
37 | blah: [uuid.v1()]
38 | }};
39 | expectedObjs.push(obj);
40 | return provider.putDocument(obj.id, obj);
41 | }).then(function(obj){
42 | return provider.getDocument(obj.id);
43 | }).then(function(obj){
44 | var objToValidate = expectedObjs.shift();
45 | assert.equal(obj.id, objToValidate.id);
46 | assert.equal(obj.branches.blah[0], objToValidate.branches.blah[0]);
47 |
48 | objToValidate.branches.blah.push(uuid.v1());
49 |
50 | return provider
51 | .putDocument(objToValidate.id, objToValidate)
52 | .then(function(updatedObj){
53 | return provider.getDocument(updatedObj.id);
54 | })
55 | .then(function(readObj){
56 | assert.equal(readObj.branches.blah[0], objToValidate.branches.blah[0]);
57 | assert.equal(readObj.branches.blah[1], objToValidate.branches.blah[1]);
58 | });
59 | });
60 | });
61 |
62 | currentPromise
63 | .then(function(){
64 | done();
65 | })
66 | .catch(done);
67 | });
68 | });
--------------------------------------------------------------------------------
/tests/demo.cmd:
--------------------------------------------------------------------------------
1 | ::Create repo
2 | curl -X POST -H "Content-Type: application/json" -d "{\"id\":\"github\"}" http://localhost:1337/api/v1/repository/github
3 |
4 | ::Create patches in repo
5 | curl -X POST -H "Content-Type: application/json" -d "{\"id\":\"defaultFeatures\",\"data\":{\"features\":{\"privateRepository\":0}}}" http://localhost:1337/api/v1/repository/github/patch/defaultFeatures
6 | curl -X POST -H "Content-Type: application/json" -d "{\"id\":\"microFeatures\",\"rule\":{\"name\":\"StringMatchesRule\",\"arguments\":{\"jsonPath\":\"$.user.plan\",\"matches\":\"micro\"}},\"data\":{\"features\":{\"privateRepository\":5}}}" http://localhost:1337/api/v1/repository/github/patch/microFeatures
7 | curl -X POST -H "Content-Type: application/json" -d "{\"id\":\"smallFeatures\",\"rule\":{\"name\":\"StringMatchesRule\",\"arguments\":{\"jsonPath\":\"$.user.plan\",\"matches\":\"small\"}},\"data\":{\"features\":{\"privateRepository\":10}}}" http://localhost:1337/api/v1/repository/github/patch/smallFeatures
8 | curl -X POST -H "Content-Type: application/json" -d "{\"id\":\"mediumFeatures\",\"rule\":{\"name\":\"StringMatchesRule\",\"arguments\":{\"jsonPath\":\"$.user.plan\",\"matches\":\"medium\"}},\"data\":{\"features\":{\"privateRepository\":20}}}" http://localhost:1337/api/v1/repository/github/patch/mediumFeatures
9 | curl -X POST -H "Content-Type: application/json" -d "{\"id\":\"largeFeatures\",\"rule\":{\"name\":\"StringMatchesRule\",\"arguments\":{\"jsonPath\":\"$.user.plan\",\"matches\":\"large\"}},\"data\":{\"features\":{\"privateRepository\":50}}}" http://localhost:1337/api/v1/repository/github/patch/largeFeatures
10 | curl -X POST -H "Content-Type: application/json" -d "{\"id\":\"testingFeatures\",\"data\":{\"features\":{\"privateRepository\":1000}}}" http://localhost:1337/api/v1/repository/github/patch/testingFeatures
11 |
12 | ::Modify repo to define branches
13 | curl -X PUT -H "Content-Type: application/json" -d "{\"id\":\"github\",\"branches\":{\"master\":{\"patches\":[\"defaultFeatures\"]},\"qa\":{\"patches\":[\"branch:master\",\"testingFeatures\"]},\"prod\":{\"patches\":[\"branch:master\",\"microFeatures\",\"smallFeatures\",\"mediumFeatures\",\"largeFeatures\"]}}}" http://localhost:1337/api/v1/repository/github
14 |
15 | ::Use various repo branches with different contexts
16 | curl -X POST -H "Content-Type: application/json" -d "{\"user\":{\"plan\":\"free\"},\"data\":{}}" http://localhost:1337/api/v1/repository/github/branch/qa/context
17 | curl -X POST -H "Content-Type: application/json" -d "{\"user\":{\"plan\":\"free\"},\"data\":{}}" http://localhost:1337/api/v1/repository/github/branch/prod/context
18 | curl -X POST -H "Content-Type: application/json" -d "{\"user\":{\"plan\":\"medium\"},\"data\":{}}" http://localhost:1337/api/v1/repository/github/branch/prod/context
--------------------------------------------------------------------------------
/tests/demo.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #Create repo
4 | curl -X POST -H "Content-Type: application/json" -d "{\"id\":\"github\"}" http://localhost:1337/api/v1/repository/github
5 |
6 | #Create patches in repo
7 | curl -X POST -H "Content-Type: application/json" -d "{\"id\":\"defaultFeatures\",\"data\":{\"features\":{\"privateRepository\":0}}}" http://localhost:1337/api/v1/repository/github/patch/defaultFeatures
8 | curl -X POST -H "Content-Type: application/json" -d "{\"id\":\"microFeatures\",\"rule\":{\"name\":\"StringMatchesRule\",\"arguments\":{\"jsonPath\":\"$.user.plan\",\"matches\":\"micro\"}},\"data\":{\"features\":{\"privateRepository\":5}}}" http://localhost:1337/api/v1/repository/github/patch/microFeatures
9 | curl -X POST -H "Content-Type: application/json" -d "{\"id\":\"smallFeatures\",\"rule\":{\"name\":\"StringMatchesRule\",\"arguments\":{\"jsonPath\":\"$.user.plan\",\"matches\":\"small\"}},\"data\":{\"features\":{\"privateRepository\":10}}}" http://localhost:1337/api/v1/repository/github/patch/smallFeatures
10 | curl -X POST -H "Content-Type: application/json" -d "{\"id\":\"mediumFeatures\",\"rule\":{\"name\":\"StringMatchesRule\",\"arguments\":{\"jsonPath\":\"$.user.plan\",\"matches\":\"medium\"}},\"data\":{\"features\":{\"privateRepository\":20}}}" http://localhost:1337/api/v1/repository/github/patch/mediumFeatures
11 | curl -X POST -H "Content-Type: application/json" -d "{\"id\":\"largeFeatures\",\"rule\":{\"name\":\"StringMatchesRule\",\"arguments\":{\"jsonPath\":\"$.user.plan\",\"matches\":\"large\"}},\"data\":{\"features\":{\"privateRepository\":50}}}" http://localhost:1337/api/v1/repository/github/patch/largeFeatures
12 | curl -X POST -H "Content-Type: application/json" -d "{\"id\":\"testingFeatures\",\"data\":{\"features\":{\"privateRepository\":1000}}}" http://localhost:1337/api/v1/repository/github/patch/testingFeatures
13 |
14 | #Modify repo to define branches
15 | curl -X PUT -H "Content-Type: application/json" -d "{\"id\":\"github\",\"branches\":{\"master\":{\"patches\":[\"defaultFeatures\"]},\"qa\":{\"patches\":[\"branch:master\",\"testingFeatures\"]},\"prod\":{\"patches\":[\"branch:master\",\"microFeatures\",\"smallFeatures\",\"mediumFeatures\",\"largeFeatures\"]}}}" http://localhost:1337/api/v1/repository/github
16 |
17 | #Use various repo branches with different contexts
18 | curl -X POST -H "Content-Type: application/json" -d "{\"user\":{\"plan\":\"free\"},\"data\":{}}" http://localhost:1337/api/v1/repository/github/branch/qa/context
19 | curl -X POST -H "Content-Type: application/json" -d "{\"user\":{\"plan\":\"free\"},\"data\":{}}" http://localhost:1337/api/v1/repository/github/branch/prod/context
20 | curl -X POST -H "Content-Type: application/json" -d "{\"user\":{\"plan\":\"medium\"},\"data\":{}}" http://localhost:1337/api/v1/repository/github/branch/prod/context
--------------------------------------------------------------------------------
/tests/patchTests.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 | var Patch = require('../models/Patch');
3 | var rules = require('../models/rules');
4 |
5 | describe('Patch', function () {
6 | it('constructs itself as expected', function () {
7 | var patch = new Patch();
8 | assert.equal(patch.id, null, "id is supposed to be null");
9 | assert.equal(patch.rule, null, "rule is supposed to be null");
10 | assert.equal(patch.applyAtJsonPath, '$.data', "default applyAtJsonPath is unexpected");
11 | assert.ok(patch.data != null && typeof patch.data === 'object', "data should be an object");
12 | });
13 |
14 | it('applies a patch', function (done) {
15 | var level1 = new Patch();
16 | level1.data = {
17 | 'features': {
18 | 'a': false
19 | }
20 | };
21 | level1.rule = new rules.AlwaysOnRule();
22 |
23 | var context = {
24 | 'data': {
25 | 'features': {
26 | 'a': true,
27 | 'b': false
28 | }
29 | }
30 | };
31 |
32 | level1
33 | .apply(context)
34 | .then(function (patchedContext) {
35 | var expectedData = {
36 | 'features': {
37 | 'a': false,
38 | 'b': false
39 | }
40 | };
41 | assert.deepEqual(context.data, expectedData, 'Unexpected patch/merge result');
42 | done();
43 | })
44 | .catch(done);
45 | });
46 |
47 | it('shouldApply works', function (done) {
48 | var level1 = new Patch();
49 | level1.rule = new rules.AlwaysOnRule();
50 |
51 | level1
52 | .shouldApply({})
53 | .then(function (shouldApply) {
54 | assert.ok(shouldApply, 'Should apply should have been true');
55 | done();
56 | })
57 | .catch(done);
58 | });
59 |
60 | it('shouldApply works negative', function (done) {
61 | var level1 = new Patch();
62 | level1.rule = new rules.AlwaysOffRule();
63 |
64 | level1
65 | .shouldApply({})
66 | .then(function (shouldApply) {
67 | assert.ok(!shouldApply, 'Should apply should have been false');
68 | done();
69 | })
70 | .catch(done);
71 | });
72 |
73 | it ('errors on unknown rule name in constructor', function() {
74 | assert.throws(function(){
75 | new Patch({
76 | rule: {name:'thisisnotthenameofarule'}
77 | });
78 | });
79 | });
80 |
81 | it ('constructs expected rule in constructor', function() {
82 | var patch = new Patch({
83 | rule: {name:'AlwaysOnRule'}
84 | });
85 | assert.equal(patch.rule.name, 'AlwaysOnRule');
86 | var AlwaysOnRule = require('../models/rules/AlwaysOnRule');
87 | assert.ok(patch.rule instanceof AlwaysOnRule);
88 | });
89 |
90 | });
--------------------------------------------------------------------------------
/tests/rennetServiceTests.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 | var Patch = require('../models/Patch');
3 | var RepositoryIndex = require('../models/RepositoryIndex');
4 | var rules = require('../models/rules');
5 | var RennetService = require('../services/RennetService');
6 |
7 | describe('Rennet Service', function () {
8 | it('can resolve a branch with one and only one patch', function (done) {
9 | var service = new RennetService();
10 | var repositoryId = 'test';
11 |
12 | //test data
13 | service.patchService.putPatch(repositoryId, new Patch({
14 | id: 'constants',
15 | data: {
16 | constants: {
17 | 'one': 1,
18 | 'two': 'two',
19 | 'three': null
20 | }
21 | },
22 | rule: new rules.AlwaysOnRule()
23 | }));
24 |
25 | service.repositoryIndexService.putRepositoryIndex(new RepositoryIndex({
26 | id: repositoryId,
27 | branches: {
28 | baseline: {
29 | patches : ['constants']
30 | }
31 | }
32 | }));
33 |
34 | //resolution!
35 | service
36 | .resolveContext(repositoryId, 'baseline', {data:{}})
37 | .then(function(context){
38 | assert.ok(context, 'context not truthy');
39 | assert.equal(context.data.constants.three, null, 'three value not correct');
40 | assert.equal(context.data.constants.two, 'two', 'two value not correct');
41 | assert.equal(context.data.constants.one, 1, 'one value not correct');
42 | done();
43 | })
44 | .catch(done);
45 |
46 | });
47 |
48 | it('can resolve a simple patch', function (done) {
49 | var service = new RennetService();
50 | var repositoryId = 'test';
51 |
52 | //test data
53 | service.patchService.putPatch(repositoryId, new Patch({
54 | id: 'constants',
55 | data: {
56 | constants: {
57 | 'one': 1,
58 | 'two': 'two',
59 | 'three': null
60 | }
61 | },
62 | rule: new rules.AlwaysOnRule()
63 | }));
64 |
65 | service.patchService.putPatch(repositoryId, new Patch({
66 | id: 'two_is_three',
67 | data: {
68 | constants: {
69 | 'two': 'three'
70 | }
71 | },
72 | rule: new rules.AlwaysOnRule()
73 | }));
74 |
75 | service.repositoryIndexService.putRepositoryIndex(new RepositoryIndex({
76 | id: repositoryId,
77 | branches: {
78 | baseline: {
79 | patches : ['constants']
80 | },
81 | patched: {
82 | patches: ['branch:baseline','two_is_three']
83 | }
84 | }
85 | }));
86 |
87 | //resolution!
88 | service
89 | .resolveContext(repositoryId, 'patched', {data:{}})
90 | .then(function(context){
91 | assert.ok(context, 'context not truthy');
92 | assert.equal(context.data.constants.three, null, 'three value not correct');
93 | assert.equal(context.data.constants.two, 'three', 'two value not correct');
94 | assert.equal(context.data.constants.one, 1, 'one value not correct');
95 | done();
96 | })
97 | .catch(done);
98 |
99 | });
100 |
101 | it('likes nesting', function (done) {
102 | var service = new RennetService();
103 | var repositoryId = 'test';
104 |
105 | //test data
106 | service.patchService.putPatch(repositoryId, new Patch({
107 | id: 'constants',
108 | data: {
109 | constants: {
110 | 'one': 1,
111 | 'two': 'two',
112 | 'three': null
113 | }
114 | },
115 | rule: new rules.AlwaysOnRule()
116 | }));
117 |
118 | service.patchService.putPatch(repositoryId, new Patch({
119 | id: 'two_is_three',
120 | data: {
121 | constants: {
122 | 'two': 'three'
123 | }
124 | },
125 | rule: new rules.AlwaysOnRule()
126 | }));
127 |
128 | service.patchService.putPatch(repositoryId, new Patch({
129 | id: 'three_is_three',
130 | data: {
131 | constants: {
132 | 'three': 'three'
133 | }
134 | },
135 | rule: new rules.AlwaysOnRule()
136 | }));
137 |
138 | service.patchService.putPatch(repositoryId, new Patch({
139 | id: 'add_four',
140 | data: {
141 | constants: {
142 | 'four': 'four'
143 | }
144 | },
145 | rule: new rules.AlwaysOnRule()
146 | }));
147 |
148 | service.patchService.putPatch(repositoryId, new Patch({
149 | id: 'add_features_table',
150 | data: {
151 | features: {
152 | 'do_it': true,
153 | 'dont_do_it': false
154 | }
155 | },
156 | rule: new rules.AlwaysOnRule()
157 | }));
158 |
159 | service.repositoryIndexService.putRepositoryIndex(new RepositoryIndex({
160 | id: repositoryId,
161 | branches: {
162 | baseline: {
163 | patches : ['constants']
164 | },
165 | patched: {
166 | patches: ['branch:baseline','two_is_three']
167 | },
168 | superPatched: {
169 | patches: ['branch:patched','three_is_three']
170 | },
171 | superDuperPatched: {
172 | patches: ['branch:superPatched','add_four', 'add_features_table']
173 | }
174 | }
175 | }));
176 |
177 | //resolution!
178 | service
179 | .resolveContext(repositoryId, 'superDuperPatched', {data:{}})
180 | .then(function(context){
181 | assert.ok(context, 'context not truthy');
182 | assert.equal(context.data.constants.four, 'four', 'four value not correct');
183 | assert.equal(context.data.constants.three, 'three', 'three value not correct');
184 | assert.equal(context.data.constants.two, 'three', 'two value not correct');
185 | assert.equal(context.data.constants.one, 1, 'one value not correct');
186 | assert.ok(context.data.features, 'features table not truthy');
187 | assert.equal(context.data.features.do_it, true, 'features.do_it not true');
188 | assert.equal(context.data.features.dont_do_it, false, 'features.dont_do_it not false');
189 | done();
190 | })
191 | .catch(done);
192 | });
193 |
194 | it('errors on overflow', function (done) {
195 | var service = new RennetService();
196 | var repositoryId = 'test';
197 |
198 | //test data
199 | service.patchService.putPatch(repositoryId, new Patch({
200 | id: 'constants',
201 | data: {
202 | constants: {
203 | 'one': 1,
204 | 'two': 'two',
205 | 'three': null
206 | }
207 | },
208 | rule: new rules.AlwaysOnRule()
209 | }));
210 |
211 | service.repositoryIndexService.putRepositoryIndex(new RepositoryIndex({
212 | id: repositoryId,
213 | branches: {
214 | baseline: {
215 | patches : ['constants', 'branch:patched']
216 | },
217 | patched: {
218 | patches: ['branch:baseline']
219 | }
220 | }
221 | }));
222 |
223 | //resolution!
224 | service
225 | .resolveContext(repositoryId, 'superDuperPatched', {data:{}})
226 | .then(function(context){
227 | done(new Error("Should have received a catch callback"));
228 | })
229 | .catch(function(error){
230 | done();
231 | });
232 | });
233 |
234 | it('applies patches in order', function (done) {
235 | var service = new RennetService();
236 | var repositoryId = 'test';
237 |
238 | //test data
239 | service.patchService.putPatch(repositoryId, new Patch({
240 | id: 'constants',
241 | data: {
242 | constants: {
243 | 'one': 1,
244 | 'two': 'two',
245 | 'three': null
246 | }
247 | },
248 | rule: new rules.AlwaysOnRule()
249 | }));
250 |
251 | service.patchService.putPatch(repositoryId, new Patch({
252 | id: 'two_is_three',
253 | data: {
254 | constants: {
255 | 'two': 'three'
256 | }
257 | },
258 | rule: new rules.AlwaysOnRule()
259 | }));
260 |
261 | service.patchService.putPatch(repositoryId, new Patch({
262 | id: 'two_is_2',
263 | data: {
264 | constants: {
265 | 'two': 2
266 | }
267 | },
268 | rule: new rules.AlwaysOnRule()
269 | }));
270 |
271 | service.patchService.putPatch(repositoryId, new Patch({
272 | id: 'two_is_four',
273 | data: {
274 | constants: {
275 | 'two': 'four'
276 | }
277 | },
278 | rule: new rules.AlwaysOnRule()
279 | }));
280 |
281 | service.repositoryIndexService.putRepositoryIndex(new RepositoryIndex({
282 | id: repositoryId,
283 | branches: {
284 | baseline: {
285 | patches : ['constants','two_is_three','two_is_four','two_is_2']
286 | }
287 | }
288 | }));
289 |
290 | //resolution!
291 | service
292 | .resolveContext(repositoryId, 'baseline', {data:{}})
293 | .then(function(context){
294 | assert.equal(context.data.constants.two, 2, 'two value not correct');
295 | done();
296 | })
297 | .catch(done);
298 | });
299 | });
--------------------------------------------------------------------------------
/tests/ruleTests.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 | var rules = require('../models/rules');
3 |
4 | describe('AlwaysOnRule', function () {
5 | it('works correctly', function (done) {
6 | var rule = new rules.AlwaysOnRule();
7 | rule.evaluate({})
8 | .then(function(on){
9 | assert.ok(on, 'AlwaysOnRule was not on');
10 | done();
11 | })
12 | .catch(done);
13 | });
14 | it('works even with null data', function (done) {
15 | var rule = new rules.AlwaysOnRule();
16 | rule.evaluate(null)
17 | .then(function(on){
18 | assert.ok(on, 'AlwaysOnRule was not on');
19 | done();
20 | })
21 | .catch(done);
22 | });
23 | });
24 |
25 | describe('AlwaysOffRule', function () {
26 | it('works correctly', function (done) {
27 | var rule = new rules.AlwaysOffRule();
28 | rule.evaluate({})
29 | .then(function(on){
30 | assert.ok(!on, 'AlwaysOffRule was not off');
31 | done();
32 | })
33 | .catch(done);
34 | });
35 | it('works even with null data', function (done) {
36 | var rule = new rules.AlwaysOffRule();
37 | rule.evaluate(null)
38 | .then(function(on){
39 | assert.ok(!on, 'AlwaysOffRule was not off');
40 | done();
41 | })
42 | .catch(done);
43 | });
44 | });
45 |
46 | describe('StringMatchesRule', function () {
47 | it('works correctly', function (done) {
48 | var rule = new rules.StringMatchesRule();
49 | rule.arguments.jsonPath = '$.environment';
50 | rule.arguments.matches = 'live';
51 |
52 | rule.evaluate({'environment':'live'})
53 | .then(function(on){
54 | assert.ok(on, 'StringMatchesRule was not on');
55 | done();
56 | })
57 | .catch(done);
58 | });
59 |
60 | it('works in the negative', function (done) {
61 | var rule = new rules.StringMatchesRule();
62 | rule.arguments.jsonPath = '$.environment';
63 | rule.arguments.matches = 'live';
64 |
65 | rule.evaluate({'environment':'dev'})
66 | .then(function(on){
67 | assert.ok(!on, 'StringMatchesRule was on');
68 | done();
69 | })
70 | .catch(done);
71 | });
72 |
73 | it('works with multiple results', function (done) {
74 | var rule = new rules.StringMatchesRule();
75 | rule.arguments.jsonPath = '$..environment';
76 | rule.arguments.matches = 'live';
77 |
78 | rule.evaluate({
79 | 'environment':'live',
80 | 'nested': {
81 | 'environment':'live'
82 | }
83 | })
84 | .then(function(on){
85 | assert.ok(on, 'StringMatchesRule was not on');
86 | done();
87 | })
88 | .catch(done);
89 | });
90 |
91 | it('fails with AND on multiple results', function (done) {
92 | var rule = new rules.StringMatchesRule();
93 | rule.arguments.jsonPath = '$..environment';
94 | rule.arguments.matches = 'live';
95 |
96 | rule.evaluate({
97 | 'environment':'live',
98 | 'nested': {
99 | 'environment':'dev'
100 | }
101 | })
102 | .then(function(on){
103 | assert.ok(!on, 'StringMatchesRule was on');
104 | done();
105 | })
106 | .catch(done);
107 | });
108 | });
109 |
110 | describe('ArrayContainsRule', function () {
111 | it('works correctly', function (done) {
112 | var rule = new rules.ArrayContainsRule();
113 | rule.arguments.jsonPath = '$.environments';
114 | rule.arguments.matches = 'live';
115 |
116 | rule.evaluate({'environments':['dev', 'live']})
117 | .then(function(on){
118 | assert.ok(on, 'ArrayContainsRule was not on');
119 | done();
120 | })
121 | .catch(done);
122 | });
123 |
124 | it('works with multiple matches', function (done) {
125 | var rule = new rules.ArrayContainsRule();
126 | rule.arguments.jsonPath = '$.environments';
127 | rule.arguments.matches = 'live';
128 |
129 | rule.evaluate({'environments':['live', 'live', 'dev', 'live']})
130 | .then(function(on){
131 | assert.ok(on, 'ArrayContainsRule was not on');
132 | done();
133 | })
134 | .catch(done);
135 | });
136 |
137 | it('works in the negative', function (done) {
138 | var rule = new rules.ArrayContainsRule();
139 | rule.arguments.jsonPath = '$.environments';
140 | rule.arguments.matches = 'live';
141 |
142 | rule.evaluate({'environments':['qa', 'dev']})
143 | .then(function(on){
144 | assert.ok(!on, 'ArrayContainsRule was on');
145 | done();
146 | })
147 | .catch(done);
148 | });
149 |
150 | it('works in the negative with empty array', function (done) {
151 | var rule = new rules.ArrayContainsRule();
152 | rule.arguments.jsonPath = '$.environments';
153 | rule.arguments.matches = 'live';
154 |
155 | rule.evaluate({'environments':[]})
156 | .then(function(on){
157 | assert.ok(!on, 'ArrayContainsRule was on');
158 | done();
159 | })
160 | .catch(done);
161 | });
162 |
163 | it('works in the negative with null array', function (done) {
164 | var rule = new rules.ArrayContainsRule();
165 | rule.arguments.jsonPath = '$.environments';
166 | rule.arguments.matches = 'live';
167 |
168 | rule.evaluate({'environments':null})
169 | .then(function(on){
170 | assert.ok(!on, 'ArrayContainsRule was on');
171 | done();
172 | })
173 | .catch(done);
174 | });
175 |
176 | it('works in the negative with invalid jsonPath', function (done) {
177 | var rule = new rules.ArrayContainsRule();
178 | rule.arguments.jsonPath = '$.thisNodeDoesNotExist';
179 | rule.arguments.matches = 'live';
180 |
181 | rule.evaluate({'environments':null})
182 | .then(function(on){
183 | assert.ok(!on, 'ArrayContainsRule was on');
184 | done();
185 | })
186 | .catch(done);
187 | });
188 | });
189 |
--------------------------------------------------------------------------------
/views/error.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | h1= message
5 | h2= error.status
6 | pre #{error.stack}
--------------------------------------------------------------------------------
/views/index.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | h1= title
5 | div(ng-controller="RennetController")
6 | p Nothing to see here yet... The date is {{value}}.
7 |
--------------------------------------------------------------------------------
/views/layout.jade:
--------------------------------------------------------------------------------
1 | mixin ie(condition)
2 | |
5 |
6 | doctype html
7 | html(ng-app="rennetApp", lang="en")
8 | head
9 | title= title
10 | meta(charset="utf-8")
11 | meta(http-equiv="X-UA-Compatible", content="IE=edge")
12 | meta(name="viewport", content="width=device-width, initial-scale=1")
13 |
14 | link(rel='stylesheet', href='/bower_components/bootstrap/dist/css/bootstrap.css')
15 | link(rel='stylesheet', href='/bower_components/bootstrap/dist/css/bootstrap-theme.css')
16 |
17 | +ie('if lt IE 9')
18 | script(src='/bower_components/html5shiv/dist/html5shiv.min.js', type='text/javascript')
19 | script(src='/bower_components/respond/dest/respond.min.js', type='text/javascript')
20 | body
21 | block content
22 | script(src='/bower_components/jquery/dist/jquery.js', type='text/javascript')
23 | script(src='/bower_components/bootstrap/dist/js/bootstrap.js', type='text/javascript')
24 | script(src='/bower_components/angularjs/angular.js', type='text/javascript')
25 | script(src='/js/rennet.js', type='text/javascript')
--------------------------------------------------------------------------------