├── .gitignore ├── LICENSE ├── README.md ├── _shared ├── relations.js └── schemas.js ├── client ├── angular1 │ ├── .gitignore │ ├── package.json │ ├── src │ │ ├── app.js │ │ ├── edit.js │ │ ├── header.js │ │ ├── index.js │ │ ├── post.js │ │ ├── posts.js │ │ └── store.js │ └── webpack.config.js ├── angular2 │ ├── .gitignore │ ├── package.json │ ├── src │ │ ├── app.ts │ │ ├── edit.ts │ │ ├── header.ts │ │ ├── index.ts │ │ ├── post.ts │ │ ├── posts.ts │ │ └── store.ts │ ├── tsconfig.json │ ├── typings.json │ └── webpack.config.js ├── angular2_firebase │ ├── .gitignore │ ├── manual_typings │ │ ├── firebase3 │ │ │ └── firebase3.d.ts │ │ └── manual_typings.d.ts │ ├── package.json │ ├── src │ │ ├── app.ts │ │ ├── edit.ts │ │ ├── header.ts │ │ ├── index.ts │ │ ├── post.ts │ │ ├── posts.ts │ │ └── store.ts │ ├── tsconfig.json │ ├── typings.json │ └── webpack.config.js ├── public │ ├── .gitignore │ ├── favicon.ico │ ├── index.css │ └── index.html └── react │ ├── .gitignore │ ├── package.json │ ├── src │ ├── app.js │ ├── edit.js │ ├── header.js │ ├── index.js │ ├── post.js │ ├── posts.js │ ├── store.js │ └── user.js │ └── webpack.config.js └── server ├── .gitignore ├── config.default.json ├── package.json ├── rethinkdb ├── .gitignore ├── README.md └── app │ ├── app.js │ ├── container.js │ ├── controllers │ ├── comments.js │ ├── posts.js │ └── users.js │ └── default.config.js ├── sql ├── .gitignore ├── Procfile ├── README.md ├── app │ ├── app.js │ ├── container.js │ ├── controllers │ │ ├── comments.js │ │ ├── posts.js │ │ └── users.js │ ├── lib │ │ ├── messageService.js │ │ └── safeCall.js │ ├── middleware │ │ ├── errorHandler.js │ │ ├── queryRewrite.js │ │ └── rewriteRelations.js │ └── models │ │ ├── Comment.js │ │ ├── Post.js │ │ └── User.js └── package.json └── src ├── adapters ├── index.js ├── mongodb.js ├── mysql.js ├── postgres.js ├── rethinkdb.js └── sqlite.js ├── index.js ├── middleware.js ├── store.js └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bower_components/ 3 | 4 | .idea/ 5 | 6 | *.iml 7 | 8 | coverage/ 9 | 10 | rethinkdb_data/ 11 | 12 | local.config.js 13 | 14 | server/sql/.git/ 15 | server/sql/.gitmodules 16 | server/sql/public/ 17 | 18 | *.log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2015 Jason Dobry 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | js-data logo 2 | 3 | # js-data-examples 4 | 5 | An example blog application using js-data v3. 6 | 7 | Relation and schema definitions are shared between the client and the server. 8 | 9 | ## Run the app 10 | 11 | ### Pick a client 12 | 13 | 1. `cd client` 14 | 1. Pick a client 15 | 1. `cd ` 16 | 1. `npm install` 17 | 1. `npm run bundle` 18 | 19 | ### Start the server 20 | 21 | 1. `cd server` 22 | 1. `cp config.default.json config.json` 23 | 1. Edit `config.json` as appropriate 24 | 1. `npm install` 25 | 1. And if MongoDB also run: `npm install mongodb bson` 26 | 1. And if MySQL also run: `npm install knex mysql` 27 | 1. And if Postgres also run: `npm install knex pg` 28 | 1. And if Rethinkdb also run: `npm install rethinkdbdash` 29 | 1. And if Sqlite also run: `npm install knex sqlite3` 30 | 1. `npm start` 31 | 32 | ## Clients 33 | 34 | - [x] - [Angular 1.x](https://github.com/js-data/js-data-examples/tree/master/client/angular1) 35 | - [x] - [Angular 2.x](https://github.com/js-data/js-data-examples/tree/master/client/angular2) 36 | - [ ] - Aurelia 37 | - [ ] - Ember 38 | - [ ] - Mithril 39 | - [x] - [React](https://github.com/js-data/js-data-examples/tree/master/client/react) 40 | - [ ] - Vue 41 | 42 | ## Servers 43 | 44 | - [ ] - MongoDB 45 | - [ ] - MySQL 46 | - [ ] - Postgres 47 | - [x] - [RethinkDB](https://github.com/js-data/js-data-examples/tree/master/server/rethinkdb) 48 | - [ ] - Sqlite 49 | 50 | ## License 51 | 52 | The MIT License (MIT) 53 | 54 | Copyright (c) 2014-2016 js-data-examples authors 55 | 56 | Permission is hereby granted, free of charge, to any person obtaining a copy 57 | of this software and associated documentation files (the "Software"), to deal 58 | in the Software without restriction, including without limitation the rights 59 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 60 | copies of the Software, and to permit persons to whom the Software is 61 | furnished to do so, subject to the following conditions: 62 | 63 | The above copyright notice and this permission notice shall be included in all 64 | copies or substantial portions of the Software. 65 | 66 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 67 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 68 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 69 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 70 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 71 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 72 | SOFTWARE. 73 | -------------------------------------------------------------------------------- /_shared/relations.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | exports.user = { 4 | hasMany: { 5 | post: { 6 | // database column, e.g. console.log(post.user_id) // 2 7 | foreignKey: 'user_id', 8 | // reference to related objects in memory, e.g. user.posts 9 | localField: 'posts' 10 | }, 11 | comment: { 12 | // database column, e.g. console.log(comment.user_id) // 16 13 | foreignKey: 'user_id', 14 | // reference to related objects in memory, e.g. user.comments 15 | localField: 'comments' 16 | } 17 | } 18 | } 19 | 20 | exports.post = { 21 | belongsTo: { 22 | // comment belongsTo user 23 | user: { 24 | // database column, e.g. console.log(comment.user_id) // 2 25 | foreignKey: 'user_id', 26 | // reference to related object in memory, e.g. post.user 27 | localField: 'user' 28 | } 29 | }, 30 | hasMany: { 31 | comment: { 32 | // database column, e.g. console.log(comment.post_id) // 5 33 | foreignKey: 'post_id', 34 | // reference to related objects in memory, e.g. post.comments 35 | localField: 'comments' 36 | } 37 | } 38 | } 39 | 40 | exports.comment = { 41 | belongsTo: { 42 | // comment belongsTo user 43 | user: { 44 | // database column, e.g. console.log(comment.user_id) // 16 45 | foreignKey: 'user_id', 46 | // reference to related object in memory, e.g. comment.user 47 | localField: 'user' 48 | }, 49 | // comment belongsTo post 50 | post: { 51 | // database column, e.g. console.log(comment.post_id) // 5 52 | foreignKey: 'post_id', 53 | // reference to related object in memory, e.g. comment.post 54 | localField: 'post' 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /_shared/schemas.js: -------------------------------------------------------------------------------- 1 | import { Schema } from 'js-data'; 2 | 3 | export const user = new Schema({ 4 | $schema: 'http://json-schema.org/draft-04/schema#', // optional 5 | title: 'User', // optional 6 | description: 'Schema for User records', // optional 7 | type: 'object', 8 | properties: { 9 | id: { type: 'number' }, 10 | name: { type: 'string' } 11 | } 12 | }); 13 | 14 | export const post = new Schema({ 15 | type: 'object', 16 | properties: { 17 | id: { type: 'number' }, 18 | // Only the DataStore and SimpleStore components care about the "indexed" attribute 19 | user_id: { type: 'number', indexed: true }, 20 | title: { type: 'string' }, 21 | content: { type: 'string' }, 22 | date_published: { type: ['string', 'null'] } 23 | } 24 | }); 25 | 26 | export const comment = new Schema({ 27 | type: 'object', 28 | properties: { 29 | id: { type: 'number' }, 30 | // Only the DataStore and SimpleStore components care about the "indexed" attribute 31 | post_id: { type: 'number', indexed: true }, 32 | // Only the DataStore and SimpleStore components care about the "indexed" attribute 33 | user_id: { type: 'number', indexed: true }, 34 | content: { type: 'string' } 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /client/angular1/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /client/angular1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-data-examples-angular1", 3 | "description": "Angular 1.x client for js-data-examples", 4 | "version": "3.0.0-alpha.1", 5 | "homepage": "https://github.com/js-data/js-data-examples", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/js-data/js-data-examples.git" 9 | }, 10 | "author": "js-data-examples project authors", 11 | "license": "MIT", 12 | "private": true, 13 | "scripts": { 14 | "bundle": "webpack --config webpack.config.js", 15 | "watch": "webpack --config webpack.config.js --watch" 16 | }, 17 | "babel": { 18 | "presets": [ 19 | "es2015" 20 | ] 21 | }, 22 | "devDependencies": { 23 | "babel-core": "^6.7.4", 24 | "babel-loader": "^6.2.4", 25 | "babel-preset-es2015": "^6.6.0", 26 | "babel-preset-react": "^6.5.0", 27 | "webpack": "^1.12.14" 28 | }, 29 | "dependencies": { 30 | "angular": "^1.5.3", 31 | "angular-route": "^1.5.3", 32 | "js-data": "^3.0.0-alpha.28", 33 | "js-data-http": "^3.0.0-alpha.10" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /client/angular1/src/app.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular' 2 | import ngRoute from 'angular-route' 3 | 4 | import {store} from './store' 5 | 6 | import {Header} from './header' 7 | import {PostsConfig, Posts} from './posts' 8 | import {PostConfig, Post} from './post' 9 | import {EditConfig, Edit} from './edit' 10 | 11 | angular.module('app', [ngRoute]) 12 | .config(['$locationProvider', ($locationProvider) => { 13 | $locationProvider.html5Mode(true).hashPrefix('!') 14 | }]) 15 | .config(PostsConfig) 16 | .config(PostConfig) 17 | .config(EditConfig) 18 | .directive('mainHeader', Header) 19 | .controller('PostsCtrl', Posts) 20 | .controller('PostCtrl', Post) 21 | .controller('EditCtrl', Edit) 22 | .run(['$rootScope', ($rootScope) => { 23 | // Fetch the current user, if any 24 | store.getMapper('user').getLoggedInUser().then((user) => { 25 | $rootScope.loggedInUser = user 26 | }) 27 | $rootScope.$on('$routeChangeSuccess', ($event, next) => { 28 | if (next) { 29 | $rootScope.path = next.$$route.originalPath.substr(1).split('/')[0] 30 | } 31 | }) 32 | }]) 33 | -------------------------------------------------------------------------------- /client/angular1/src/edit.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular' 2 | import {store} from './store' 3 | 4 | export const EditConfig = ['$routeProvider', function ($routeProvider) { 5 | $routeProvider.when('/posts/:id/edit', { 6 | template: `
7 |
8 |
9 | 10 | 12 |
13 |
14 | 15 | 17 |
18 |
19 | 20 |
21 |
22 |
`, 23 | controller: 'EditCtrl', 24 | controllerAs: 'EditCtrl', 25 | resolve: { 26 | post: ['$route', function ($route) { 27 | const id = $route.current.params.id 28 | if (id && id !== 'new') { 29 | return store.find('post', id) 30 | } 31 | }] 32 | } 33 | }) 34 | }] 35 | 36 | export const Edit = ['$route', '$routeParams', '$location', '$timeout', function ($route, $routeParams, $location, $timeout) { 37 | this.post = $route.current.locals.post || store.createRecord('post') 38 | 39 | this.onSubmit = function () { 40 | const id = $routeParams.id 41 | const props = { 42 | title: this.post.title, 43 | content: this.post.content 44 | } 45 | let promise 46 | if (id === 'new') { 47 | promise = store.create('post', props) 48 | } else { 49 | promise = store.update('post', id, props) 50 | } 51 | promise.then((post) => { 52 | $timeout(() => { 53 | $location.path(`/posts/${post.id}`) 54 | }) 55 | }, console.error) 56 | } 57 | }] 58 | -------------------------------------------------------------------------------- /client/angular1/src/header.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular' 2 | 3 | const config = { 4 | restrict: 'E', 5 | replace: true, 6 | template: ``, 44 | controllerAs: 'HeaderCtrl', 45 | controller () { 46 | this.login = function ($event) { 47 | $event.preventDefault() 48 | window.location = '/auth/github' 49 | } 50 | } 51 | } 52 | 53 | export const Header = function () { 54 | return config 55 | } 56 | -------------------------------------------------------------------------------- /client/angular1/src/index.js: -------------------------------------------------------------------------------- 1 | document.getElementsByTagName('html')[0].setAttribute('data-ng-app', 'app') 2 | 3 | const container = document.createElement('div') 4 | container.className = 'container' 5 | 6 | const mainContainer = document.createElement('div') 7 | mainContainer.setAttribute('data-ng-view', '') 8 | mainContainer.className = 'container main-container' 9 | 10 | container.appendChild(document.createElement('main-header')) 11 | container.appendChild(mainContainer) 12 | 13 | document.getElementById('app').appendChild(container) 14 | 15 | import './app' 16 | -------------------------------------------------------------------------------- /client/angular1/src/post.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular' 2 | import {store} from './store' 3 | 4 | export const PostConfig = ['$routeProvider', function ($routeProvider) { 5 | $routeProvider.when('/posts/:id', { 6 | template: `
7 | 11 |
{{ PostCtrl.post.content }}
12 |
`, 13 | controller: 'PostCtrl', 14 | controllerAs: 'PostCtrl', 15 | resolve: { 16 | post: ['$route', function ($route) { 17 | return store.find('post', $route.current.params.id) 18 | }] 19 | } 20 | }) 21 | }] 22 | 23 | export const Post = ['$scope', '$timeout', '$route', '$routeParams', function ($scope, $timeout, $route, $routeParams) { 24 | this.post = $route.current.locals.post 25 | 26 | this.onChange = function () { 27 | $timeout(() => { 28 | this.post = store.get('post', $routeParams.id) 29 | }) 30 | } 31 | 32 | store.on('all', this.onChange, this) 33 | 34 | $scope.$on('$destroy', () => { 35 | store.off('all', this.onChange) 36 | }) 37 | }] 38 | -------------------------------------------------------------------------------- /client/angular1/src/posts.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular' 2 | import {store} from './store' 3 | 4 | const PAGE_SIZE = 5 5 | 6 | /** 7 | * @param {number} pageNum The page to retrieve. 8 | * @return {Promise} A Promise that resolves to a Page object. 9 | */ 10 | function fetchPage (pageNum) { 11 | return store.getMapper('post').findAll({ 12 | limit: PAGE_SIZE, 13 | offset: (pageNum - 1) * PAGE_SIZE, 14 | orderBy: [['created_at', 'desc']] 15 | }).then(function (page) { 16 | page.data = store.add('post', page.data) 17 | return page 18 | }) 19 | } 20 | 21 | export const PostsConfig = ['$routeProvider', function ($routeProvider) { 22 | $routeProvider.when('/', { 23 | template: `
24 |

25 | {{ post.title }} 26 | {{ post.created_at | date }} 27 |

28 |
{{ post.content | limitTo:200 }}...
29 |
30 |
31 |
32 | No posts yet... 33 |
34 | `, 63 | controller: 'PostsCtrl', 64 | controllerAs: 'PostsCtrl', 65 | resolve: { 66 | page: function () { 67 | return fetchPage(1) 68 | } 69 | } 70 | }) 71 | }] 72 | 73 | export const Posts = ['$timeout', '$route', function ($timeout, $route) { 74 | this.PAGE_SIZE = PAGE_SIZE 75 | this.currentPage = $route.current.locals.page.page 76 | this.total = $route.current.locals.page.total 77 | this.posts = $route.current.locals.page.data 78 | 79 | this.fetchPage = function (pageNum) { 80 | fetchPage(pageNum).then((page) => { 81 | this.currentPage = page.page 82 | this.total = page.total 83 | $timeout(() => { 84 | this.posts = store.filter('post', { 85 | limit: this.PAGE_SIZE, 86 | offset: (this.currentPage - 1) * this.PAGE_SIZE, 87 | orderBy: [['created_at', 'DESC']] 88 | }) 89 | }) 90 | }) 91 | } 92 | 93 | this.prev = function (pageDecrease) { 94 | if (this.currentPage > 1) { 95 | this.fetchPage(this.currentPage - pageDecrease) 96 | } 97 | } 98 | 99 | this.next = function (pageIncrease) { 100 | if ((this.currentPage * this.PAGE_SIZE) < this.total) { 101 | this.fetchPage(this.currentPage + pageIncrease) 102 | } 103 | } 104 | }] 105 | -------------------------------------------------------------------------------- /client/angular1/src/store.js: -------------------------------------------------------------------------------- 1 | // DataStore is mostly recommended for use in the browser 2 | import { 3 | DataStore, 4 | Schema, 5 | utils 6 | } from 'js-data' 7 | import HttpAdapter from 'js-data-http' 8 | const schemas = require('../../../_shared/schemas')(Schema) 9 | import * as relations from '../../../_shared/relations' 10 | 11 | const convertToDate = function (record) { 12 | if (typeof record.created_at === 'string') { 13 | record.created_at = new Date(record.created_at) 14 | } 15 | if (typeof record.updated_at === 'string') { 16 | record.updated_at = new Date(record.updated_at) 17 | } 18 | } 19 | 20 | export const adapter = new HttpAdapter({ 21 | // Our API sits behind the /api path 22 | basePath: '/api' 23 | }) 24 | export const store = new DataStore({ 25 | mapperDefaults: { 26 | // Override the original to make sure the date properties are actually Date 27 | // objects 28 | createRecord (props, opts) { 29 | const result = this.constructor.prototype.createRecord.call(this, props, opts) 30 | if (Array.isArray(result)) { 31 | result.forEach(convertToDate) 32 | } else if (this.is(result)) { 33 | convertToDate(result) 34 | } 35 | return result 36 | } 37 | } 38 | }) 39 | 40 | store.registerAdapter('http', adapter, { default: true }) 41 | 42 | // The User Resource 43 | store.defineMapper('user', { 44 | // Our API endpoints use plural form in the path 45 | endpoint: 'users', 46 | schema: schemas.user, 47 | relations: relations.user, 48 | getLoggedInUser () { 49 | if (this.loggedInUser) { 50 | return utils.resolve(this.loggedInUser) 51 | } 52 | return store.getAdapter('http').GET('/api/users/loggedInUser') 53 | .then((response) => { 54 | const user = this.loggedInUser = response.data 55 | if (user) { 56 | this.loggedInUser = store.add('user', user) 57 | } 58 | return this.loggedInUser 59 | }) 60 | } 61 | }) 62 | 63 | // The Post Resource 64 | store.defineMapper('post', { 65 | // Our API endpoints use plural form in the path 66 | endpoint: 'posts', 67 | schema: schemas.post, 68 | relations: relations.post, 69 | // "GET /posts" doesn't return data as JSData expects, so we override the 70 | // default "wrap" method and add some extra logic to make sure that the 71 | // correct data gets turned into Record instances 72 | wrap (data, opts) { 73 | if (opts.op === 'afterFindAll') { 74 | data.data = this.createRecord(data.data) 75 | return data 76 | } else { 77 | return this.createRecord(data) 78 | } 79 | } 80 | }) 81 | 82 | // The Comment Resource 83 | store.defineMapper('comment', { 84 | // Our API endpoints use plural form in the path 85 | endpoint: 'comments', 86 | schema: schemas.comment, 87 | relations: relations.comment 88 | }) 89 | -------------------------------------------------------------------------------- /client/angular1/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | 3 | module.exports = { 4 | devtool: 'source-map', 5 | entry: './src/index.js', 6 | output: { 7 | path: '../public', 8 | publicPath: '/public/', 9 | filename: 'bundle.js' 10 | }, 11 | module: { 12 | loaders: [ 13 | { 14 | test: /\.js$/, 15 | exclude: /node_modules/, 16 | loader: 'babel-loader' 17 | } 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/angular2/.gitignore: -------------------------------------------------------------------------------- 1 | typings 2 | node_modules -------------------------------------------------------------------------------- /client/angular2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-data-examples-angular2", 3 | "description": "Angular 2.x client for js-data-examples", 4 | "version": "3.0.0-alpha.1", 5 | "homepage": "https://github.com/js-data/js-data-examples", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/js-data/js-data-examples.git" 9 | }, 10 | "author": "js-data-examples project authors", 11 | "license": "MIT", 12 | "private": true, 13 | "scripts": { 14 | "bundle": "webpack --config webpack.config.js", 15 | "watch": "webpack --config webpack.config.js --watch", 16 | "typings": "typings", 17 | "postinstall": "typings install" 18 | }, 19 | "devDependencies": { 20 | "ts-loader": "^0.8.2", 21 | "typescript": "^1.8.10", 22 | "typings": "^0.7.11", 23 | "webpack": "^1.13.1" 24 | }, 25 | "dependencies": { 26 | "@angular/common": "2.0.0-rc.1", 27 | "@angular/compiler": "2.0.0-rc.1", 28 | "@angular/core": "2.0.0-rc.1", 29 | "@angular/http": "2.0.0-rc.1", 30 | "@angular/platform-browser": "2.0.0-rc.1", 31 | "@angular/platform-browser-dynamic": "2.0.0-rc.1", 32 | "@angular/router": "2.0.0-rc.1", 33 | "@angular/router-deprecated": "2.0.0-rc.1", 34 | "@angular/upgrade": "2.0.0-rc.1", 35 | "es6-promise": "^3.1.2", 36 | "es6-shim": "^0.35.0", 37 | "js-data": "^3.0.0-beta.5", 38 | "js-data-http": "^3.0.0-beta.5", 39 | "reflect-metadata": "^0.1.3", 40 | "rxjs": "^5.0.0-beta.6", 41 | "zone.js": "^0.6.12" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /client/angular2/src/app.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core' 2 | import {ROUTER_DIRECTIVES, RouteConfig} from '@angular/router-deprecated' 3 | import {HeaderComponent} from './header' 4 | import {PostsComponent} from './posts' 5 | import {PostComponent} from './post' 6 | import {EditComponent} from './edit' 7 | import {store, IUser, IUserMapper} from './store' 8 | 9 | const UserMapper = store.getMapper('user') 10 | 11 | @Component({ 12 | selector: '#app', 13 | template: ` 14 |
15 |
16 |
17 | 18 |
19 |
20 | `, 21 | directives: [ROUTER_DIRECTIVES, HeaderComponent] 22 | }) 23 | @RouteConfig([ 24 | { path: '/', component: PostsComponent, name: 'Posts' }, 25 | { path: '/posts/:id', component: PostComponent, name: 'Post' }, 26 | { path: '/posts/:id/edit', component: EditComponent, name: 'Edit' } 27 | ]) 28 | export class AppComponent { 29 | loggedInUser: IUser 30 | 31 | constructor () { 32 | // Fetch the current user, if any 33 | UserMapper.getLoggedInUser().then((user) => { 34 | this.loggedInUser = user 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /client/angular2/src/edit.ts: -------------------------------------------------------------------------------- 1 | import {Component,Input} from '@angular/core' 2 | import {Router, RouteParams} from '@angular/router-deprecated' 3 | import {store, IPost} from './store' 4 | 5 | @Component({ 6 | selector: 'edit', 7 | template: ` 8 |
9 |
10 |
11 | 12 | 14 |
15 |
16 | 17 | 19 |
20 |
21 | 22 |
23 |
24 |
25 | ` 26 | }) 27 | export class EditComponent { 28 | post: IPost = store.createRecord('post') 29 | 30 | constructor (private _router: Router, private _routeParams: RouteParams) { 31 | const id = _routeParams.get('id') 32 | if (id && id !== 'new') { 33 | store.find('post', id).then((post) => { 34 | this.post.title = post.title 35 | this.post.content = post.content 36 | }) 37 | } 38 | } 39 | onSubmit () { 40 | const id = this._routeParams.get('id') 41 | const props = { 42 | title: this.post.title, 43 | content: this.post.content 44 | } 45 | let promise 46 | if (id === 'new') { 47 | promise = store.create('post', props) 48 | } else { 49 | promise = store.update('post', id, props) 50 | } 51 | promise.then((post) => { 52 | this._router.navigate(['Post', { id: post.id }]) 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /client/angular2/src/header.ts: -------------------------------------------------------------------------------- 1 | import {Component,Input} from '@angular/core' 2 | import {ROUTER_DIRECTIVES} from '@angular/router-deprecated' 3 | import {IUser} from './store' 4 | 5 | @Component({ 6 | selector: 'header', 7 | template: ` 8 | 44 | `, 45 | directives: [ROUTER_DIRECTIVES] 46 | }) 47 | export class HeaderComponent { 48 | @Input() loggedInUser: IUser 49 | } 50 | -------------------------------------------------------------------------------- /client/angular2/src/index.ts: -------------------------------------------------------------------------------- 1 | import 'es6-shim' 2 | import 'es6-promise' 3 | import '../node_modules/zone.js/dist/zone' 4 | // import 'zone.js/lib/browser/zone-microtask' 5 | // import 'zone.js' 6 | import 'reflect-metadata' 7 | 8 | import {bootstrap} from '@angular/platform-browser-dynamic' 9 | import {provide} from '@angular/core' 10 | import {LocationStrategy, HashLocationStrategy} from '@angular/common' 11 | import {ROUTER_PROVIDERS} from '@angular/router-deprecated' 12 | import {AppComponent} from './app' 13 | 14 | bootstrap(AppComponent, [ 15 | ROUTER_PROVIDERS, 16 | provide(LocationStrategy, {useClass: HashLocationStrategy}) 17 | ]) 18 | -------------------------------------------------------------------------------- /client/angular2/src/post.ts: -------------------------------------------------------------------------------- 1 | import {Component,Input} from '@angular/core' 2 | import {ROUTER_DIRECTIVES, RouteParams} from '@angular/router-deprecated' 3 | import {store, IPost} from './store' 4 | 5 | @Component({ 6 | selector: 'post', 7 | template: ` 8 |
9 | 13 |
{{ post?.content }}
14 |
15 | `, 16 | directives: [ROUTER_DIRECTIVES] 17 | }) 18 | export class PostComponent { 19 | post: IPost 20 | 21 | constructor (private _routeParams: RouteParams) { 22 | store.find('post', _routeParams.get('id')).then((post) => { 23 | this.post = post 24 | store.on('all', this.onChange, this) 25 | }) 26 | } 27 | ngOnDestroy () { store.off('all', this.onChange) } 28 | onChange () { this.post = store.get('post', this._routeParams.get('id')) } 29 | } 30 | -------------------------------------------------------------------------------- /client/angular2/src/posts.ts: -------------------------------------------------------------------------------- 1 | import {Component,Input} from '@angular/core' 2 | import {ROUTER_DIRECTIVES} from '@angular/router-deprecated' 3 | import {store, IPost} from './store' 4 | 5 | const PAGE_SIZE: number = 5 6 | 7 | interface IPage { 8 | total: number 9 | page: number 10 | data: IPost[] 11 | } 12 | 13 | /** 14 | * @param {number} pageNum The page to retrieve. 15 | * @return {Promise} A Promise that resolves to an IPage instance. 16 | */ 17 | function fetchPage (pageNum: number): Promise { 18 | return store.getMapper('post').findAll({ 19 | limit: PAGE_SIZE, 20 | offset: (pageNum - 1) * PAGE_SIZE, 21 | // We want the newest posts first 22 | orderBy: [['created_at', 'desc']] 23 | }).then(function (page) { 24 | // Since we didn't use DataStore#findAll to fetch the data (we used the 25 | // Post Mapper directly), we need to make sure the posts get injected into 26 | // the data store 27 | page.data = store.add('post', page.data) 28 | return page 29 | }) 30 | } 31 | 32 | @Component({ 33 | selector: 'posts', 34 | template: `
35 |
36 |

37 | {{ post.title }} 38 | {{ post.created_at | date }} 39 |

40 |
{{ post.content }}
41 |
42 |
No posts yet...
43 | 70 |
`, 71 | directives: [ROUTER_DIRECTIVES] 72 | }) 73 | export class PostsComponent { 74 | posts: IPost[] = [] 75 | currentPage: number = 1 76 | total: number = 0 77 | PAGE_SIZE: number = PAGE_SIZE 78 | 79 | constructor () { 80 | this.fetchPage(this.currentPage) 81 | } 82 | fetchPage (pageNum: number): void { 83 | fetchPage(pageNum).then((page) => { 84 | this.currentPage = page.page 85 | this.total = page.total 86 | this.posts = store.filter('post', { 87 | limit: this.PAGE_SIZE, 88 | offset: (this.currentPage - 1) * this.PAGE_SIZE, 89 | orderBy: [['created_at', 'DESC']] 90 | }) 91 | }) 92 | } 93 | prev ($event: Event, pageDecrease: number): void { 94 | $event.preventDefault() 95 | if (this.currentPage > 1) { 96 | this.fetchPage(this.currentPage - pageDecrease) 97 | } 98 | } 99 | next ($event: Event, pageIncrease: number): void { 100 | $event.preventDefault() 101 | if ((this.currentPage * this.PAGE_SIZE) < this.total) { 102 | this.fetchPage(this.currentPage + pageIncrease) 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /client/angular2/src/store.ts: -------------------------------------------------------------------------------- 1 | // DataStore is mostly recommended for use in the browser 2 | import { 3 | DataStore, 4 | Mapper, 5 | Record, 6 | Schema, 7 | utils 8 | } from 'js-data' 9 | import {HttpAdapter} from 'js-data-http' 10 | 11 | declare var require: any 12 | 13 | const schemas = require('../../../_shared/schemas')(Schema) 14 | const relations = require('../../../_shared/relations') 15 | 16 | const convertToDate = function (record) { 17 | if (typeof record.created_at === 'string') { 18 | record.created_at = new Date(record.created_at) 19 | } 20 | if (typeof record.updated_at === 'string') { 21 | record.updated_at = new Date(record.updated_at) 22 | } 23 | } 24 | 25 | export const adapter = new HttpAdapter({ 26 | // Our API sits behind the /api path 27 | basePath: '/api' 28 | }) 29 | export const store = new DataStore({ 30 | mapperDefaults: { 31 | // Override the original to make sure the date properties are actually Date 32 | // objects 33 | createRecord (props, opts) { 34 | const result = this.constructor.prototype.createRecord.call(this, props, opts) 35 | if (Array.isArray(result)) { 36 | result.forEach(convertToDate) 37 | } else if (this.is(result)) { 38 | convertToDate(result) 39 | } 40 | return result 41 | } 42 | } 43 | }) 44 | 45 | store.registerAdapter('http', adapter, { default: true }) 46 | 47 | export interface IUser extends Record { 48 | id: string|number 49 | displayName: string 50 | username: string 51 | created_at: string|Date 52 | updated_at: string|Date 53 | } 54 | 55 | export interface IUserMapper extends Mapper { 56 | loggedInUser: IUser 57 | getLoggedInUser(): Promise 58 | } 59 | 60 | // The User Resource 61 | store.defineMapper('user', { 62 | // Our API endpoints use plural form in the path 63 | endpoint: 'users', 64 | schema: schemas.user, 65 | relations: relations.user, 66 | getLoggedInUser (): Promise { 67 | if (this.loggedInUser) { 68 | return utils.resolve(this.loggedInUser) 69 | } 70 | return store.getAdapter('http').GET('/api/users/loggedInUser') 71 | .then((response) => { 72 | const user = this.loggedInUser = response.data 73 | if (user) { 74 | this.loggedInUser = store.add('user', user) 75 | } 76 | return this.loggedInUser 77 | }) 78 | } 79 | }) 80 | 81 | export interface IPost extends Record { 82 | id: string|number 83 | title: string 84 | content: string 85 | user_id: string 86 | created_at: string|Date 87 | updated_at: string|Date 88 | } 89 | 90 | // The Post Resource 91 | store.defineMapper('post', { 92 | // Our API endpoints use plural form in the path 93 | endpoint: 'posts', 94 | schema: schemas.post, 95 | relations: relations.post, 96 | // "GET /posts" doesn't return data as JSData expects, so we override the 97 | // default "wrap" method and add some extra logic to make sure that the 98 | // correct data gets turned into Record instances 99 | wrap (data, opts) { 100 | console.log(opts.op, data.data) 101 | if (opts.op === 'afterFindAll') { 102 | data.data = this.createRecord(data.data) 103 | return data 104 | } else { 105 | // Normally JSData expects the returned data to be directly wrappable 106 | return this.createRecord(data) 107 | } 108 | } 109 | }) 110 | 111 | export interface IComment extends Record { 112 | id: string|number 113 | user_id: string 114 | post_id: string 115 | created_at: string|Date 116 | updated_at: string|Date 117 | } 118 | 119 | // The Comment Resource 120 | store.defineMapper('comment', { 121 | // Our API endpoints use plural form in the path 122 | endpoint: 'comments', 123 | schema: schemas.comment, 124 | relations: relations.comment 125 | }) 126 | -------------------------------------------------------------------------------- /client/angular2/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "removeComments": false, 10 | "noImplicitAny": false 11 | }, 12 | "exclude": [ 13 | "node_modules" 14 | ], 15 | "files": [ 16 | "typings/main.d.ts", 17 | "src/index.ts" 18 | ], 19 | "compileOnSave": false, 20 | "buildOnSave": false 21 | } 22 | -------------------------------------------------------------------------------- /client/angular2/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ambientDependencies": { 3 | "es6-shim": "github:DefinitelyTyped/DefinitelyTyped/es6-shim/es6-shim.d.ts" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /client/angular2/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | 3 | module.exports = { 4 | entry: './src/index', 5 | output: { 6 | path: '../public', 7 | publicPath: '/public/', 8 | filename: 'bundle.js' 9 | }, 10 | resolve: { 11 | extensions: ['', '.js', '.ts'] 12 | }, 13 | devtool: 'source-map', 14 | module: { 15 | loaders: [ 16 | { 17 | test: /\.ts/, 18 | loaders: ['ts-loader'], 19 | exclude: /node_modules/ 20 | } 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /client/angular2_firebase/.gitignore: -------------------------------------------------------------------------------- 1 | typings 2 | node_modules -------------------------------------------------------------------------------- /client/angular2_firebase/manual_typings/firebase3/firebase3.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 1. Delete goog namespaces 4 | 2. Delete look of disaproval 5 | 3. typealias firebase.Promise to Promise 6 | 4. Union type FirebaseOAuthProvider 7 | 5. Remove _noStructuralTyping from Promise classes 8 | 6. Remove catch() and then() declarations from firebase.Thenable, and extend Promise. 9 | 10 | */ 11 | declare interface FirebaseService { 12 | INTERNAL: Object; 13 | app: firebase.app.App; 14 | } 15 | 16 | declare interface FirebaseServiceNamespace { 17 | app(app?: firebase.app.App): FirebaseService; 18 | } 19 | 20 | declare interface Observer { 21 | complete(): any; 22 | error(error: Object): any; 23 | next(value: any): any; 24 | } 25 | 26 | 27 | declare type FirebaseOAuthProvider = firebase.auth.GithubAuthProvider | 28 | firebase.auth.GoogleAuthProvider | 29 | firebase.auth.FacebookAuthProvider; 30 | 31 | declare class Promise_Instance implements PromiseLike { 32 | constructor(resolver: (a: (a?: TYPE | PromiseLike | { then: any }) => any, b: (a?: any) => any) => any); 33 | catch(onRejected: (a: any) => RESULT): Promise; 34 | then(opt_onFulfilled?: (a: TYPE) => VALUE, opt_onRejected?: (a: any) => any): RESULT; 35 | } 36 | 37 | declare namespace firebase { 38 | type AuthTokenData = { accessToken: string, expirationTime: number, refreshToken: string }; 39 | } 40 | declare namespace firebase { 41 | type AuthTokenListener = (a: string) => void; 42 | } 43 | declare namespace firebase { 44 | type CompleteFn = () => void; 45 | } 46 | declare namespace firebase { 47 | type ErrorFn = (a: Object) => void; 48 | } 49 | declare namespace firebase { 50 | interface FirebaseError { 51 | code: string; 52 | message: string; 53 | name: string; 54 | stack: string; 55 | } 56 | } 57 | declare namespace firebase { 58 | type NextFn = (a: any) => void; 59 | } 60 | declare namespace firebase { 61 | class Promise extends Promise_Instance { 62 | static all(values: firebase.Promise[]): firebase.Promise; 63 | static reject(error: Object): firebase.Promise; 64 | static resolve(value: T): firebase.Promise; 65 | } 66 | class Promise_Instance implements firebase.Thenable { 67 | constructor(resolver: (a?: (a: T) => void, b?: (a: Object) => void) => any); 68 | catch(onReject?: (a: Object) => any): firebase.Thenable; 69 | then(onResolve?: (a: T) => any, onReject?: (a: Object) => any): firebase.Promise; 70 | } 71 | } 72 | declare namespace firebase { 73 | var SDK_VERSION: string; 74 | } 75 | declare namespace firebase { 76 | type Subscribe = (a?: ((a: any) => void) | Observer, b?: (a: Object) => void, c?: () => void) => () => void; 77 | } 78 | declare namespace firebase { 79 | interface Thenable extends Promise { 80 | //Removed definitions of catch() and then(), and extended Promise. 81 | } 82 | } 83 | declare namespace firebase { 84 | type Unsubscribe = () => void; 85 | } 86 | declare namespace firebase { 87 | interface User extends firebase.UserInfo { 88 | delete(): firebase.Promise; 89 | emailVerified: boolean; 90 | getToken(opt_forceRefresh?: boolean): firebase.Promise; 91 | isAnonymous: boolean; 92 | link(credential: firebase.auth.AuthCredential): firebase.Promise; 93 | linkWithPopup(provider: firebase.auth.AuthProvider): firebase.Promise<{ credential: firebase.auth.AuthCredential, user: firebase.User }>; 94 | linkWithRedirect(provider: firebase.auth.AuthProvider): firebase.Promise; 95 | providerData: (firebase.UserInfo)[]; 96 | reauthenticate(credential: firebase.auth.AuthCredential): firebase.Promise; 97 | refreshToken: string; 98 | reload(): firebase.Promise; 99 | sendEmailVerification(): firebase.Promise; 100 | unlink(providerId: string): firebase.Promise; 101 | updateEmail(newEmail: string): firebase.Promise; 102 | updatePassword(newPassword: string): firebase.Promise; 103 | updateProfile(profile: { displayName: string, photoURL: string }): firebase.Promise; 104 | } 105 | } 106 | declare namespace firebase { 107 | interface UserInfo { 108 | displayName: string; 109 | email: string; 110 | photoURL: string; 111 | providerId: string; 112 | uid: string; 113 | } 114 | } 115 | declare namespace firebase { 116 | function app(name: string): firebase.app.App; 117 | } 118 | declare namespace firebase.app { 119 | interface App { 120 | INTERNAL: Object; 121 | auth(): firebase.auth.Auth; 122 | database(): firebase.database.Database; 123 | delete(): firebase.Promise; 124 | name: string; 125 | options: Object; 126 | storage(): firebase.storage.Storage; 127 | } 128 | } 129 | declare namespace firebase { 130 | var apps: (firebase.app.App)[]; 131 | } 132 | declare namespace firebase { 133 | function auth(app?: firebase.app.App): firebase.auth.Auth; 134 | } 135 | declare namespace firebase.auth { 136 | interface ActionCodeInfo { 137 | } 138 | } 139 | declare namespace firebase.auth { 140 | interface Auth { 141 | app: firebase.app.App; 142 | applyActionCode(code: string): firebase.Promise; 143 | checkActionCode(code: string): firebase.Promise; 144 | confirmPasswordReset(code: string, newPassword: string): firebase.Promise; 145 | createUserWithEmailAndPassword(email: string, password: string): firebase.Promise; 146 | currentUser: firebase.User; 147 | fetchProvidersForEmail(email: string): firebase.Promise; 148 | getRedirectResult(): firebase.Promise<{ credential: firebase.auth.AuthCredential, user: firebase.User }>; 149 | onAuthStateChanged(nextOrObserver: Object, opt_error?: (a: firebase.auth.Error) => any, opt_completed?: () => any): () => any; 150 | sendPasswordResetEmail(email: string): firebase.Promise; 151 | signInAnonymously(): firebase.Promise; 152 | signInWithCredential(credential: firebase.auth.AuthCredential): firebase.Promise; 153 | signInWithCustomToken(token: string): firebase.Promise; 154 | signInWithEmailAndPassword(email: string, password: string): firebase.Promise; 155 | signInWithPopup(provider: firebase.auth.AuthProvider): firebase.Promise<{ credential: firebase.auth.AuthCredential, user: firebase.User }>; 156 | signInWithRedirect(provider: firebase.auth.AuthProvider): firebase.Promise; 157 | signOut(): firebase.Promise; 158 | verifyPasswordResetCode(code: string): firebase.Promise; 159 | } 160 | } 161 | declare namespace firebase.auth { 162 | interface AuthCredential { 163 | provider: string; 164 | } 165 | } 166 | declare namespace firebase.auth { 167 | interface AuthProvider { 168 | providerId: string; 169 | } 170 | } 171 | declare namespace firebase.auth { 172 | class EmailAuthProvider extends EmailAuthProvider_Instance { 173 | static PROVIDER_ID: string; 174 | } 175 | class EmailAuthProvider_Instance implements firebase.auth.AuthProvider { 176 | private noStructuralTyping_: any; 177 | credential(email: string, password: string): firebase.auth.AuthCredential; 178 | providerId: string; 179 | } 180 | } 181 | declare namespace firebase.auth { 182 | interface Error { 183 | code: string; 184 | message: string; 185 | } 186 | } 187 | declare namespace firebase.auth { 188 | class FacebookAuthProvider extends FacebookAuthProvider_Instance { 189 | static PROVIDER_ID: string; 190 | } 191 | class FacebookAuthProvider_Instance implements firebase.auth.AuthProvider { 192 | private noStructuralTyping_: any; 193 | addScope(scope: string): any; 194 | credential(token: string): firebase.auth.AuthCredential; 195 | providerId: string; 196 | } 197 | } 198 | declare namespace firebase.auth { 199 | class GithubAuthProvider extends GithubAuthProvider_Instance { 200 | static PROVIDER_ID: string; 201 | // TODO fix upstream 202 | static credential(token: string): firebase.auth.AuthCredential; 203 | } 204 | class GithubAuthProvider_Instance implements firebase.auth.AuthProvider { 205 | private noStructuralTyping_: any; 206 | addScope(scope: string): any; 207 | providerId: string; 208 | } 209 | } 210 | declare namespace firebase.auth { 211 | class GoogleAuthProvider extends GoogleAuthProvider_Instance { 212 | static PROVIDER_ID: string; 213 | } 214 | class GoogleAuthProvider_Instance implements firebase.auth.AuthProvider { 215 | private noStructuralTyping_: any; 216 | addScope(scope: string): any; 217 | credential(idToken?: string, accessToken?: string): firebase.auth.AuthCredential; 218 | providerId: string; 219 | } 220 | } 221 | declare namespace firebase.auth { 222 | class TwitterAuthProvider extends TwitterAuthProvider_Instance { 223 | static PROVIDER_ID: string; 224 | // TODO fix this upstream 225 | static credential(token: string, secret: string): firebase.auth.AuthCredential; 226 | } 227 | class TwitterAuthProvider_Instance implements firebase.auth.AuthProvider { 228 | private noStructuralTyping_: any; 229 | providerId: string; 230 | } 231 | } 232 | declare namespace firebase.auth { 233 | type UserCredential = { credential: firebase.auth.AuthCredential, user: firebase.User }; 234 | } 235 | declare namespace firebase { 236 | function database(app?: firebase.app.App): firebase.database.Database; 237 | } 238 | declare namespace firebase.database { 239 | interface DataSnapshot { 240 | child(path: string): firebase.database.DataSnapshot; 241 | exists(): boolean; 242 | exportVal(): any; 243 | forEach(action: (a: firebase.database.DataSnapshot) => boolean): boolean; 244 | getPriority(): string | number; 245 | hasChild(path: string): boolean; 246 | hasChildren(): boolean; 247 | key: string; 248 | numChildren(): number; 249 | ref: firebase.database.Reference; 250 | val(): any; 251 | } 252 | } 253 | declare namespace firebase.database { 254 | interface Database { 255 | app: firebase.app.App; 256 | goOffline(): any; 257 | goOnline(): any; 258 | ref(path?: string): firebase.database.Reference; 259 | refFromURL(url: string): firebase.database.Reference; 260 | } 261 | } 262 | declare namespace firebase.database { 263 | interface OnDisconnect { 264 | cancel(onComplete?: (a: Object) => any): firebase.Promise; 265 | remove(onComplete?: (a: Object) => any): firebase.Promise; 266 | set(value: any, onComplete?: (a: Object) => any): firebase.Promise; 267 | setWithPriority(value: any, priority: number | string, onComplete?: (a: Object) => any): firebase.Promise; 268 | update(values: Object, onComplete?: (a: Object) => any): firebase.Promise; 269 | } 270 | } 271 | declare namespace firebase.database { 272 | interface Query { 273 | endAt(value: number | string | boolean, key?: string): firebase.database.Query; 274 | equalTo(value: number | string | boolean, key?: string): firebase.database.Query; 275 | limitToFirst(limit: number): firebase.database.Query; 276 | limitToLast(limit: number): firebase.database.Query; 277 | off(eventType?: string, callback?: (a: firebase.database.DataSnapshot, b?: string) => any, context?: Object): any; 278 | on(eventType: string, callback: (a: firebase.database.DataSnapshot, b?: string) => any, cancelCallbackOrContext?: Object, context?: Object): (a: firebase.database.DataSnapshot, b?: string) => any; 279 | once(eventType: string, callback?: (a: firebase.database.DataSnapshot, b?: string) => any): firebase.Promise; 280 | orderByChild(path: string): firebase.database.Query; 281 | orderByKey(): firebase.database.Query; 282 | orderByPriority(): firebase.database.Query; 283 | orderByValue(): firebase.database.Query; 284 | ref: firebase.database.Reference; 285 | startAt(value: number | string | boolean, key?: string): firebase.database.Query; 286 | toString(): string; 287 | } 288 | } 289 | declare namespace firebase.database { 290 | interface Reference extends firebase.database.Query { 291 | child(path: string): firebase.database.Reference; 292 | key: string; 293 | onDisconnect(): firebase.database.OnDisconnect; 294 | parent: firebase.database.Reference; 295 | push(value?: any, onComplete?: (a: Object) => any): firebase.database.ThenableReference; 296 | remove(onComplete?: (a: Object) => any): firebase.Promise; 297 | root: firebase.database.Reference; 298 | set(value: any, onComplete?: (a: Object) => any): firebase.Promise; 299 | setPriority(priority: string | number, onComplete: (a: Object) => any): firebase.Promise; 300 | setWithPriority(newVal: any, newPriority: string | number, onComplete?: (a: Object) => any): firebase.Promise; 301 | transaction(transactionUpdate: (a: any) => any, onComplete?: (a: Object, b: boolean, c: firebase.database.DataSnapshot) => any, applyLocally?: boolean): firebase.Promise<{ committed: boolean, snapshot: firebase.database.DataSnapshot }>; 302 | update(values: Object, onComplete?: (a: Object) => any): firebase.Promise; 303 | } 304 | } 305 | declare namespace firebase.database.ServerValue { 306 | } 307 | declare namespace firebase.database { 308 | interface ThenableReference extends firebase.database.Reference, firebase.Thenable { 309 | } 310 | } 311 | declare namespace firebase.database { 312 | function enableLogging(logger?: any, persistent?: boolean): any; 313 | } 314 | declare namespace firebase { 315 | function initializeApp(options: Object, name?: string): firebase.app.App; 316 | } 317 | declare namespace firebase { 318 | function storage(app?: firebase.app.App): firebase.storage.Storage; 319 | } 320 | declare namespace firebase.storage { 321 | interface FullMetadata extends firebase.storage.UploadMetadata { 322 | bucket: string; 323 | downloadURLs: string[]; 324 | fullPath: string; 325 | generation: string; 326 | metageneration: string; 327 | name: string; 328 | size: number; 329 | timeCreated: string; 330 | updated: string; 331 | } 332 | } 333 | declare namespace firebase.storage { 334 | interface Reference { 335 | bucket: string; 336 | child(path: string): firebase.storage.Reference; 337 | delete(): Promise; 338 | fullPath: string; 339 | getDownloadURL(): Promise; 340 | getMetadata(): Promise; 341 | name: string; 342 | parent: firebase.storage.Reference; 343 | put(blob: Blob, metadata?: firebase.storage.UploadMetadata): firebase.storage.UploadTask; 344 | root: firebase.storage.Reference; 345 | storage: firebase.storage.Storage; 346 | toString(): string; 347 | updateMetadata(metadata: firebase.storage.SettableMetadata): Promise; 348 | } 349 | } 350 | declare namespace firebase.storage { 351 | interface SettableMetadata { 352 | cacheControl: string; 353 | contentDisposition: string; 354 | contentEncoding: string; 355 | contentLanguage: string; 356 | contentType: string; 357 | customMetadata: { [key: string]: string }; 358 | } 359 | } 360 | declare namespace firebase.storage { 361 | interface Storage { 362 | app: firebase.app.App; 363 | maxOperationRetryTime: number; 364 | maxUploadRetryTime: number; 365 | ref(path?: string): firebase.storage.Reference; 366 | refFromURL(url: string): firebase.storage.Reference; 367 | setMaxOperationRetryTime(time: number): any; 368 | setMaxUploadRetryTime(time: number): any; 369 | } 370 | } 371 | declare namespace firebase.storage { 372 | type TaskEvent = string; 373 | var TaskEvent: { 374 | STATE_CHANGED: TaskEvent, 375 | }; 376 | } 377 | declare namespace firebase.storage { 378 | type TaskState = string; 379 | var TaskState: { 380 | CANCELED: TaskState, 381 | ERROR: TaskState, 382 | PAUSED: TaskState, 383 | RUNNING: TaskState, 384 | SUCCESS: TaskState, 385 | }; 386 | } 387 | declare namespace firebase.storage { 388 | interface UploadMetadata extends firebase.storage.SettableMetadata { 389 | md5Hash: string; 390 | } 391 | } 392 | declare namespace firebase.storage { 393 | interface UploadTask { 394 | cancel(): boolean; 395 | on(event: firebase.storage.TaskEvent, nextOrObserver?: Object, error?: (a: Object) => any, complete?: () => any): (...a: any[]) => any; 396 | pause(): boolean; 397 | resume(): boolean; 398 | snapshot: firebase.storage.UploadTaskSnapshot; 399 | } 400 | } 401 | declare namespace firebase.storage { 402 | interface UploadTaskSnapshot { 403 | bytesTransferred: number; 404 | downloadURL: string; 405 | metadata: firebase.storage.FullMetadata; 406 | ref: firebase.storage.Reference; 407 | state: firebase.storage.TaskState; 408 | task: firebase.storage.UploadTask; 409 | totalBytes: number; 410 | } 411 | } 412 | 413 | declare module 'firebase' { 414 | export = firebase; 415 | } 416 | -------------------------------------------------------------------------------- /client/angular2_firebase/manual_typings/manual_typings.d.ts: -------------------------------------------------------------------------------- 1 | /// -------------------------------------------------------------------------------- /client/angular2_firebase/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-data-examples-angular2", 3 | "description": "Angular 2.x client for js-data-examples", 4 | "version": "2.0.0-alpha.1", 5 | "homepage": "https://github.com/js-data/js-data-examples", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/js-data/js-data-examples.git" 9 | }, 10 | "author": "js-data-examples project authors", 11 | "license": "MIT", 12 | "private": true, 13 | "scripts": { 14 | "bundle": "webpack --config webpack.config.js", 15 | "watch": "webpack --config webpack.config.js --watch", 16 | "typings": "typings", 17 | "postinstall": "typings install" 18 | }, 19 | "devDependencies": { 20 | "ts-loader": "^0.8.1", 21 | "typescript": "^1.8.10", 22 | "typings": "^0.7.11", 23 | "webpack": "^1.13.1" 24 | }, 25 | "dependencies": { 26 | "@angular/common": "2.0.0-rc.1", 27 | "@angular/compiler": "2.0.0-rc.1", 28 | "@angular/core": "2.0.0-rc.1", 29 | "@angular/http": "2.0.0-rc.1", 30 | "@angular/platform-browser": "2.0.0-rc.1", 31 | "@angular/platform-browser-dynamic": "2.0.0-rc.1", 32 | "@angular/router": "2.0.0-rc.1", 33 | "@angular/router-deprecated": "2.0.0-rc.1", 34 | "@angular/upgrade": "2.0.0-rc.1", 35 | "firebase": "3.0.5", 36 | "es6-promise": "^3.1.2", 37 | "es6-shim": "^0.35.0", 38 | "js-data": "^3.0.0-beta.7", 39 | "js-data-firebase": "^3.0.0-beta.2", 40 | "reflect-metadata": "^0.1.3", 41 | "rxjs": "^5.0.0-beta.7", 42 | "zone.js": "^0.6.12" 43 | } 44 | } -------------------------------------------------------------------------------- /client/angular2_firebase/src/app.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core' 2 | import {ROUTER_DIRECTIVES, RouteConfig} from '@angular/router-deprecated' 3 | import {HeaderComponent} from './header' 4 | import {PostsComponent} from './posts' 5 | import {PostComponent} from './post' 6 | import {EditComponent} from './edit' 7 | import {store, IUser, IUserMapper} from './store' 8 | 9 | const UserMapper = store.getMapper('user') 10 | 11 | @Component({ 12 | selector: '#app', 13 | template: ` 14 |
15 |
16 |
17 | 18 |
19 |
20 | `, 21 | directives: [ROUTER_DIRECTIVES, HeaderComponent] 22 | }) 23 | @RouteConfig([ 24 | { path: '/', component: PostsComponent, name: 'Posts' }, 25 | { path: '/posts/:id', component: PostComponent, name: 'Post' }, 26 | { path: '/posts/:id/edit', component: EditComponent, name: 'Edit' } 27 | ]) 28 | export class AppComponent { 29 | loggedInUser: IUser 30 | 31 | constructor () { 32 | // Fetch the current user, if any 33 | UserMapper.getLoggedInUser().then((user) => { 34 | this.loggedInUser = user 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /client/angular2_firebase/src/edit.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from '@angular/core' 2 | import {Router, RouteParams} from '@angular/router-deprecated' 3 | import {store, IUserMapper, IUser, IPost} from './store' 4 | const UserMapper = store.getMapper('user') 5 | 6 | @Component({ 7 | selector: 'edit', 8 | template: ` 9 |
10 |
11 |
12 | 13 | 15 |
16 |
17 | 18 | 20 |
21 |
22 | 23 |
24 |
25 |
26 | ` 27 | }) 28 | 29 | export class EditComponent { 30 | post: IPost = store.createRecord('post') 31 | constructor(private _router: Router, private _routeParams: RouteParams) { 32 | const id = _routeParams.get('id') 33 | if (id && id !== 'new') { 34 | store.find('post', id).then((post) => { 35 | this.post.title = post.title 36 | this.post.content = post.content 37 | 38 | }) 39 | } 40 | } 41 | onSubmit(event) { 42 | event.preventDefault() 43 | const id = this._routeParams.get('id') 44 | const props = { 45 | user_id: UserMapper.loggedInUser.id, 46 | title: this.post.title, 47 | content: this.post.content 48 | } 49 | let promise 50 | if (id === 'new') { 51 | promise = store.create('post', props) 52 | } else { 53 | promise = store.update('post', id, props) 54 | } 55 | promise.then((post) => { 56 | this._router.navigate(['Post', { id: post.id }]) 57 | }) 58 | 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /client/angular2_firebase/src/header.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from '@angular/core' 2 | import {ROUTER_DIRECTIVES} from '@angular/router-deprecated' 3 | import {store, IUser, IUserMapper} from './store' 4 | const UserMapper = store.getMapper('user') 5 | 6 | @Component({ 7 | selector: 'header', 8 | template: ` 9 | 45 | `, 46 | directives: [ROUTER_DIRECTIVES] 47 | }) 48 | export class HeaderComponent { 49 | @Input() loggedInUser: IUser 50 | 51 | public login(provider: string) { 52 | //select provider based on input 53 | let gitHubProvider = new firebase.auth.GithubAuthProvider(); 54 | UserMapper.login(gitHubProvider).then(user => { 55 | this.loggedInUser = user 56 | }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /client/angular2_firebase/src/index.ts: -------------------------------------------------------------------------------- 1 | import 'es6-shim' 2 | import 'es6-promise' 3 | import '../node_modules/zone.js/dist/zone' 4 | import 'reflect-metadata' 5 | 6 | import { bootstrap } from '@angular/platform-browser-dynamic'; 7 | import {provide } from '@angular/core' 8 | import { ROUTER_PROVIDERS, } from '@angular/router-deprecated'; 9 | import {LocationStrategy, HashLocationStrategy, APP_BASE_HREF} from '@angular/common'; 10 | import { AppComponent } from './app'; 11 | 12 | bootstrap(AppComponent, [ 13 | ROUTER_PROVIDERS, 14 | provide(LocationStrategy, { useClass: HashLocationStrategy }) 15 | ]); -------------------------------------------------------------------------------- /client/angular2_firebase/src/post.ts: -------------------------------------------------------------------------------- 1 | import {Component,Input} from '@angular/core' 2 | import {ROUTER_DIRECTIVES, RouteParams} from '@angular/router-deprecated' 3 | import {store, IPost} from './store' 4 | 5 | @Component({ 6 | selector: 'post', 7 | template: ` 8 |
9 | 13 |
{{ post?.content }}
14 |
15 | `, 16 | directives: [ROUTER_DIRECTIVES] 17 | }) 18 | export class PostComponent { 19 | post: IPost 20 | 21 | constructor (private _routeParams: RouteParams) { 22 | store.find('post', _routeParams.get('id')).then((post) => { 23 | this.post = post 24 | store.on('all', this.onChange, this) 25 | }) 26 | } 27 | ngOnDestroy () { store.off('all', this.onChange) } 28 | onChange () { this.post = store.get('post', this._routeParams.get('id')) } 29 | } 30 | -------------------------------------------------------------------------------- /client/angular2_firebase/src/posts.ts: -------------------------------------------------------------------------------- 1 | import {Component,Input} from '@angular/core' 2 | import {ROUTER_DIRECTIVES} from '@angular/router-deprecated' 3 | import {store, IPost} from './store' 4 | 5 | const PAGE_SIZE: number = 5 6 | 7 | interface IPage { 8 | total: number 9 | page: number 10 | data: IPost[] 11 | } 12 | 13 | /** 14 | * @param {number} pageNum The page to retrieve. 15 | * @return {Promise} A Promise that resolves to an IPage instance. 16 | */ 17 | function fetchPage (pageNum: number): Promise { 18 | return store.getMapper('post').findAll({ 19 | limit: PAGE_SIZE, 20 | offset: (pageNum - 1) * PAGE_SIZE, 21 | // We want the newest posts first 22 | orderBy: [['created_at', 'desc']] 23 | }).then(function (data) { 24 | // Since we didn't use DataStore#findAll to fetch the data (we used the 25 | // Post Mapper directly), we need to make sure the posts get injected into 26 | // the data store 27 | debugger 28 | let page = { 29 | page: pageNum, 30 | total: data.length, 31 | data: data 32 | } 33 | page.data = store.add('post', page.data) 34 | return page 35 | }) 36 | } 37 | 38 | @Component({ 39 | selector: 'posts', 40 | template: `
41 |
42 |

43 | {{ post.title }} 44 | {{ post.created_at | date }} 45 |

46 |
{{ post.content }}
47 |
48 |
No posts yet...
49 | 76 |
`, 77 | directives: [ROUTER_DIRECTIVES] 78 | }) 79 | export class PostsComponent { 80 | posts: IPost[] = [] 81 | currentPage: number = 1 82 | total: number = 0 83 | PAGE_SIZE: number = PAGE_SIZE 84 | 85 | constructor () { 86 | this.fetchPage(this.currentPage) 87 | } 88 | fetchPage (pageNum: number): void { 89 | fetchPage(pageNum).then((page) => { 90 | this.currentPage = page.page 91 | this.total = page.total 92 | this.posts = store.filter('post', { 93 | limit: this.PAGE_SIZE, 94 | offset: (this.currentPage - 1) * this.PAGE_SIZE, 95 | orderBy: [['created_at', 'DESC']] 96 | }) 97 | }) 98 | } 99 | prev ($event: Event, pageDecrease: number): void { 100 | $event.preventDefault() 101 | if (this.currentPage > 1) { 102 | this.fetchPage(this.currentPage - pageDecrease) 103 | } 104 | } 105 | next ($event: Event, pageIncrease: number): void { 106 | $event.preventDefault() 107 | if ((this.currentPage * this.PAGE_SIZE) < this.total) { 108 | this.fetchPage(this.currentPage + pageIncrease) 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /client/angular2_firebase/src/store.ts: -------------------------------------------------------------------------------- 1 | // DataStore is mostly recommended for use in the browser 2 | import { 3 | DataStore, 4 | Mapper, 5 | Record, 6 | Schema, 7 | utils 8 | } from 'js-data' 9 | import {FirebaseAdapter, IBaseFirebaseAdapter} from 'js-data-firebase' 10 | 11 | declare var require: any 12 | 13 | const schemas = require('../../../_shared/schemas')(Schema) 14 | const relations = require('../../../_shared/relations') 15 | 16 | const convertToDate = function (record) { 17 | if (typeof record.created_at === 'string') { 18 | record.created_at = new Date(record.created_at) 19 | } 20 | if (typeof record.updated_at === 'string') { 21 | record.updated_at = new Date(record.updated_at) 22 | } 23 | } 24 | 25 | const config = { 26 | apiKey: "AIzaSyBm19OK4uJG7opzSRcS91jp1zTP7FYk258", 27 | authDomain: "js-data-firebase-v3.firebaseapp.com", 28 | databaseURL: "https://js-data-firebase-v3.firebaseio.com/", 29 | storageBucket: "", 30 | }; 31 | firebase.initializeApp(config); 32 | export const firebaseAdapter: IBaseFirebaseAdapter = new FirebaseAdapter({ 33 | // Our API sits behind the /api path 34 | db: firebase.database() 35 | }) 36 | export const store = new DataStore({ 37 | mapperDefaults: { 38 | // Override the original to make sure the date properties are actually Date 39 | // objects 40 | beforeCreate(props, opts) { 41 | props.created_at = new Date().toISOString() 42 | props.updated_at = new Date().toISOString() 43 | return props 44 | }, 45 | beforeUpdate(props, opts) { 46 | props.updated_at = new Date().toISOString() 47 | return props 48 | }, 49 | createRecord(props, opts) { 50 | const result = this.constructor.prototype.createRecord.call(this, props, opts) 51 | if (Array.isArray(result)) { 52 | result.forEach(convertToDate) 53 | } else if (this.is(result)) { 54 | convertToDate(result) 55 | } 56 | return result 57 | } 58 | } 59 | }) 60 | 61 | store.registerAdapter('firebase', firebaseAdapter, { default: true }) 62 | 63 | export interface IUser extends Record { 64 | id: string | number 65 | displayName: string 66 | username: string 67 | created_at: string | Date 68 | updated_at: string | Date 69 | } 70 | 71 | export interface IUserMapper extends Mapper { 72 | loggedInUser: IUser 73 | getLoggedInUser(): Promise 74 | login(user: firebase.auth.AuthProvider): Promise 75 | } 76 | 77 | // The User Resource 78 | store.defineMapper('user', { 79 | // Our API endpoints use plural form in the path 80 | endpoint: 'users', 81 | schema: schemas.user, 82 | relations: relations.user, 83 | getLoggedInUser(): Promise { 84 | if (this.loggedInUser) { 85 | return utils.resolve(this.loggedInUser) 86 | } 87 | //check local storage for user 88 | //todo figure this out. Doesn't work in v2 89 | // const authData = firebase.auth().currentUser 90 | // debugger 91 | // if (authData) { 92 | // return store.find('user', authData.uid).then(user => { 93 | // return this.loggedInUser = user 94 | // }) 95 | else { 96 | return utils.resolve(null) 97 | } 98 | }, 99 | login(provider: firebase.auth.AuthProvider): Promise { 100 | return firebaseAdapter.db.app.auth().signInWithPopup(provider).then((authData) => { 101 | console.log(authData) 102 | return store.create('user', { 103 | id: authData.user.uid, 104 | displayName: authData.user.displayName, 105 | name: authData.user.providerData[0].email 106 | }).then(user => { 107 | this.loggedInUser = user 108 | return this.loggedInUser 109 | }) 110 | }) 111 | } 112 | }) 113 | 114 | export interface IPost extends Record { 115 | id: string | number 116 | title: string 117 | content: string 118 | user_id: string 119 | created_at: string | Date 120 | updated_at: string | Date 121 | } 122 | 123 | // The Post Resource 124 | store.defineMapper('post', { 125 | // Our API endpoints use plural form in the path 126 | endpoint: 'posts', 127 | schema: schemas.post, 128 | relations: relations.post 129 | }) 130 | 131 | export interface IComment extends Record { 132 | id: string | number 133 | user_id: string 134 | post_id: string 135 | created_at: string | Date 136 | updated_at: string | Date 137 | } 138 | 139 | // The Comment Resource 140 | store.defineMapper('comment', { 141 | // Our API endpoints use plural form in the path 142 | endpoint: 'comments', 143 | schema: schemas.comment, 144 | relations: relations.comment 145 | }) 146 | -------------------------------------------------------------------------------- /client/angular2_firebase/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "removeComments": false, 10 | "noImplicitAny": false 11 | }, 12 | "exclude": [ 13 | "node_modules" 14 | ], 15 | "files": [ 16 | "typings/main.d.ts", 17 | "src/index.ts", 18 | "manual_typings/firebase3/firebase3.d.ts" 19 | ], 20 | "compileOnSave": false, 21 | "buildOnSave": false 22 | } 23 | -------------------------------------------------------------------------------- /client/angular2_firebase/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ambientDependencies": { 3 | "es6-shim": "github:DefinitelyTyped/DefinitelyTyped/es6-shim/es6-shim.d.ts" 4 | } 5 | } -------------------------------------------------------------------------------- /client/angular2_firebase/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | 3 | module.exports = { 4 | entry: './src/index', 5 | output: { 6 | path: '../public', 7 | publicPath: '/public/', 8 | filename: 'bundle.js' 9 | }, 10 | resolve: { 11 | extensions: ['', '.js', '.ts'] 12 | }, 13 | devtool: 'source-map', 14 | module: { 15 | loaders: [ 16 | { 17 | test: /\.ts/, 18 | loaders: ['ts-loader'], 19 | exclude: /node_modules/ 20 | } 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /client/public/.gitignore: -------------------------------------------------------------------------------- 1 | bundle.* -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/js-data/js-data-examples/76af86b8edd5ca78c96ee9fae9fc1cba1b2beeb1/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 20px; 3 | padding-bottom: 20px; 4 | } 5 | 6 | .main-container { 7 | margin-top: 70px; 8 | } 9 | 10 | /* Everything but the jumbotron gets side spacing for mobile first views */ 11 | .header, 12 | .footer { 13 | padding-right: 15px; 14 | padding-left: 15px; 15 | } 16 | 17 | /* Custom page header */ 18 | .header { 19 | padding-bottom: 20px; 20 | border-bottom: 1px solid #e5e5e5; 21 | } 22 | /* Make the masthead heading the same height as the navigation */ 23 | .header h3 { 24 | margin-top: 0; 25 | margin-bottom: 0; 26 | line-height: 40px; 27 | } 28 | 29 | /* Custom page footer */ 30 | .footer { 31 | padding-top: 19px; 32 | color: #777; 33 | border-top: 1px solid #e5e5e5; 34 | } 35 | 36 | /* Customize container */ 37 | @media (min-width: 768px) { 38 | .container { 39 | max-width: 730px; 40 | } 41 | } 42 | .container-narrow > hr { 43 | margin: 30px 0; 44 | } 45 | 46 | /* Responsive: Portrait tablets and up */ 47 | @media screen and (min-width: 768px) { 48 | /* Remove the padding we set earlier */ 49 | .header, 50 | .footer { 51 | padding-right: 0; 52 | padding-left: 0; 53 | } 54 | /* Space out the masthead */ 55 | .header { 56 | margin-bottom: 30px; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | js-data-examples 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /client/react/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /client/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-data-examples-react", 3 | "description": "React client for js-data-examples", 4 | "version": "3.0.0-alpha.1", 5 | "homepage": "https://github.com/js-data/js-data-examples", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/js-data/js-data-examples.git" 9 | }, 10 | "author": "js-data-examples project authors", 11 | "license": "MIT", 12 | "private": true, 13 | "scripts": { 14 | "bundle": "webpack --config webpack.config.js", 15 | "watch": "webpack --config webpack.config.js --watch" 16 | }, 17 | "babel": { 18 | "presets": [ 19 | "es2015", 20 | "react" 21 | ] 22 | }, 23 | "devDependencies": { 24 | "babel-core": "^6.7.4", 25 | "babel-loader": "^6.2.4", 26 | "babel-preset-es2015": "^6.6.0", 27 | "babel-preset-react": "^6.5.0", 28 | "webpack": "^1.12.14" 29 | }, 30 | "dependencies": { 31 | "js-data": "^3.0.0-alpha.28", 32 | "js-data-http": "^3.0.0-alpha.10", 33 | "react": "^0.14.8", 34 | "react-dom": "^0.14.8", 35 | "react-router": "^2.0.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /client/react/src/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Link} from 'react-router' 3 | import {store} from './store' 4 | import Header from './header' 5 | 6 | export default class App extends React.Component { 7 | constructor (props) { 8 | super(props) 9 | // Fetch the current user, if any 10 | store.getMapper('user').getLoggedInUser() 11 | this.state = this.getState() 12 | } 13 | componentDidMount () { store.on('all', this.onChange, this) } 14 | componentWillUnmount () { store.off('all', this.onChange) } 15 | onChange () { this.setState(this.getState()) } 16 | getState () { return { loggedInUser: store.getMapper('user').loggedInUser } } 17 | render () { 18 | const loggedInUser = this.state.loggedInUser 19 | let link = New post 20 | if (!loggedInUser) { 21 | link = Login to create a post 22 | } 23 | return ( 24 |
25 |
26 |
{this.props.children}
27 |
28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /client/react/src/edit.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {store} from './store' 3 | 4 | export default class Edit extends React.Component { 5 | constructor (props) { 6 | super(props) 7 | const id = this.props.params.id 8 | if (id && id !== 'new') { 9 | store.find('post', id).then((post) => { 10 | this.setState({ 11 | title: post.title, 12 | content: post.content 13 | }) 14 | }) 15 | } 16 | this.state = { title: '', content: '' } 17 | } 18 | onTitleChange (e) { 19 | e.preventDefault() 20 | this.setState({ title: e.target.value }) 21 | } 22 | onContentChange (e) { 23 | e.preventDefault() 24 | this.setState({ content: e.target.value }) 25 | } 26 | onSubmit (e) { 27 | const id = this.props.params.id 28 | e.preventDefault() 29 | const props = { 30 | title: this.state.title, 31 | content: this.state.content 32 | } 33 | let promise 34 | if (id === 'new') { 35 | promise = store.create('post', props) 36 | } else { 37 | promise = store.update('post', id, props) 38 | } 39 | promise.then((post) => { 40 | this.context.router.push(`/posts/${post.id}`) 41 | }) 42 | } 43 | render () { 44 | return ( 45 |
46 |
47 |
48 | 49 | 51 |
52 |
53 | 54 | 56 |
57 |
58 | 59 |
60 |
61 |
62 | ) 63 | } 64 | } 65 | 66 | Edit.contextTypes = { router: React.PropTypes.object } 67 | -------------------------------------------------------------------------------- /client/react/src/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Router, Route, hashHistory, IndexRoute, Link } from 'react-router' 3 | import {store} from './store' 4 | 5 | export default React.createClass({ 6 | render () { 7 | const loggedInUser = this.props.loggedInUser 8 | const path = window.location.pathname 9 | const links = [ 10 |
  • 11 | Home 12 |
  • 13 | ] 14 | if (loggedInUser) { 15 | links.push( 16 |
  • 17 | New post 18 |
  • 19 | ) 20 | links.push( 21 |
  • 22 | 23 | Hi {loggedInUser.displayName || loggedInUser.username}! 24 | 25 |
  • 26 | ) 27 | } else { 28 | links.push(
  • Login with Github
  • ) 29 | } 30 | return ( 31 |
    32 |
    33 |
    34 | js-data blog example 35 | 40 |
    41 | 52 |
    53 |
    54 | ) 55 | } 56 | }) 57 | -------------------------------------------------------------------------------- /client/react/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {render} from 'react-dom' 3 | import {Router, Route, browserHistory, IndexRoute} from 'react-router' 4 | import App from './app' 5 | import Posts from './posts' 6 | import Post from './post' 7 | import Edit from './edit' 8 | 9 | render(( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ), document.getElementById('app')) 18 | -------------------------------------------------------------------------------- /client/react/src/post.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {store} from './store' 3 | 4 | export default class Post extends React.Component{ 5 | constructor (props) { 6 | super(props) 7 | store.find('post', this.props.params.id) 8 | this.state = this.getState() 9 | } 10 | componentDidMount () { store.on('all', this.onChange, this) } 11 | componentWillUnmount () { store.off('all', this.onChange, this) } 12 | onChange () { this.setState(this.getState()) } 13 | getState () { 14 | return { 15 | post: store.get('post', this.props.params.id) || {} 16 | } 17 | } 18 | render () { 19 | return ( 20 |
    21 |

    22 | {this.state.post.title} 23 | {this.state.post.created_at} 24 |

    25 |
    {this.state.post.content}
    26 |
    27 | ) 28 | } 29 | } 30 | 31 | Post.contextTypes = { router: React.PropTypes.object } 32 | -------------------------------------------------------------------------------- /client/react/src/posts.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Link} from 'react-router' 3 | import {store} from './store' 4 | 5 | const PAGE_SIZE = 5 6 | 7 | /** 8 | * @param {number} pageNum The page to retrieve. 9 | * @return {Promise} A Promise that resolves to a Page object. 10 | */ 11 | function fetchPage (pageNum) { 12 | return store.getMapper('post').findAll({ 13 | limit: PAGE_SIZE, 14 | offset: (pageNum - 1) * PAGE_SIZE, 15 | orderBy: [['created_at', 'desc']] 16 | }).then(function (page) { 17 | page.data = store.add('post', page.data) 18 | return page 19 | }) 20 | } 21 | 22 | export default class Posts extends React.Component { 23 | constructor (props) { 24 | super(props) 25 | fetchPage(1).then((page) => { 26 | this.setState(this.getState(1, page)) 27 | }) 28 | this.state = { posts: [], currentPage: 1, total: 0 } 29 | } 30 | getState (currentPage, page) { 31 | return { 32 | currentPage: page.page, 33 | total: page.total, 34 | posts: store.filter('post', { 35 | limit: PAGE_SIZE, 36 | offset: (currentPage - 1) * PAGE_SIZE, 37 | orderBy: [['created_at', 'DESC']] 38 | }) 39 | } 40 | } 41 | fetchPage (pageNum) { 42 | fetchPage(pageNum).then((page) => { 43 | this.setState(this.getState(pageNum, page)) 44 | }) 45 | } 46 | prev (e, pageDecrease) { 47 | e.preventDefault() 48 | if (this.state.currentPage > 1) { 49 | this.fetchPage(this.state.currentPage - pageDecrease) 50 | } 51 | } 52 | next (e, pageIncrease) { 53 | e.preventDefault() 54 | if ((this.state.currentPage * PAGE_SIZE) < this.state.total) { 55 | this.fetchPage(this.state.currentPage + pageIncrease) 56 | } 57 | } 58 | render () { 59 | const currentPage = this.state.currentPage 60 | const total = this.state.total 61 | let posts = this.state.posts.map(function (post) { 62 | return ( 63 |
    64 |

    65 | {post.title} 66 | {post.created_at} 67 |

    68 |
    {post.content.substring(0, 200)}...
    69 |
    70 | ) 71 | }) 72 | const links = [ 73 |
  • 74 | this.prev(e, 1)}> 75 | 76 | 77 |
  • 78 | ] 79 | if (currentPage > 2) { 80 | links.push(
  • this.prev(e, 2)}> 81 | {currentPage - 2} 82 |
  • ) 83 | } 84 | if (currentPage > 1) { 85 | links.push(
  • this.prev(e, 1)}> 86 | {currentPage - 1} 87 |
  • ) 88 | } 89 | links.push(
  • {currentPage}
  • ) 90 | if ((currentPage * PAGE_SIZE) < total) { 91 | links.push(
  • this.next(e, 1)}> 92 | {currentPage + 1} 93 |
  • ) 94 | } 95 | if (((currentPage + 1) * PAGE_SIZE) < total) { 96 | links.push(
  • this.next(e, 2)}> 97 | {currentPage + 2} 98 |
  • ) 99 | } 100 | links.push(
  • 101 | this.next(e, 1)}> 102 | 103 | 104 |
  • ) 105 | let nav = 106 | if (total <= PAGE_SIZE) { 107 | nav = '' 108 | } 109 | if (!posts.length) { 110 | posts = 'No posts yet...' 111 | } 112 | return (
    113 |
    {posts}
    114 | {nav} 115 |
    ) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /client/react/src/store.js: -------------------------------------------------------------------------------- 1 | // DataStore is mostly recommended for use in the browser 2 | import { 3 | DataStore, 4 | Schema, 5 | utils 6 | } from 'js-data' 7 | import HttpAdapter from 'js-data-http' 8 | import * as schemas from '../../../_shared/schemas' 9 | import * as relations from '../../../_shared/relations' 10 | 11 | const convertToDate = function (record) { 12 | if (typeof record.created_at === 'string') { 13 | record.created_at = new Date(record.created_at) 14 | } 15 | if (typeof record.updated_at === 'string') { 16 | record.updated_at = new Date(record.updated_at) 17 | } 18 | } 19 | 20 | export const adapter = new HttpAdapter({ 21 | // Our API sits behind the /api path 22 | basePath: '/api' 23 | }) 24 | export const store = new DataStore({ 25 | mapperDefaults: { 26 | // Override the original to make sure the date properties are actually Date 27 | // objects 28 | createRecord (props, opts) { 29 | const result = this.constructor.prototype.createRecord.call(this, props, opts) 30 | if (Array.isArray(result)) { 31 | result.forEach(convertToDate) 32 | } else if (this.is(result)) { 33 | convertToDate(result) 34 | } 35 | return result 36 | } 37 | } 38 | }) 39 | 40 | store.registerAdapter('http', adapter, { default: true }) 41 | 42 | // The User Resource 43 | store.defineMapper('user', { 44 | // Our API endpoints use plural form in the path 45 | endpoint: 'users', 46 | schema: schemas.user, 47 | relations: relations.user, 48 | getLoggedInUser () { 49 | if (this.loggedInUser) { 50 | return utils.resolve(this.loggedInUser) 51 | } 52 | return store.getAdapter('http').GET('/api/users/loggedInUser') 53 | .then((response) => { 54 | const user = this.loggedInUser = response.data 55 | if (user) { 56 | this.loggedInUser = store.add('user', user) 57 | } 58 | return this.loggedInUser 59 | }) 60 | } 61 | }) 62 | 63 | // The Post Resource 64 | store.defineMapper('post', { 65 | // Our API endpoints use plural form in the path 66 | endpoint: 'posts', 67 | schema: schemas.post, 68 | relations: relations.post, 69 | // "GET /posts" doesn't return data as JSData expects, so we override the 70 | // default "wrap" method and add some extra logic to make sure that the 71 | // correct data gets turned into Record instances 72 | wrap (data, opts) { 73 | if (opts.op === 'afterFindAll') { 74 | data.data = this.createRecord(data.data) 75 | return data 76 | } else { 77 | return this.createRecord(data) 78 | } 79 | } 80 | }) 81 | 82 | // The Comment Resource 83 | store.defineMapper('comment', { 84 | // Our API endpoints use plural form in the path 85 | endpoint: 'comments', 86 | schema: schemas.comment, 87 | relations: relations.comment 88 | }) 89 | -------------------------------------------------------------------------------- /client/react/src/user.js: -------------------------------------------------------------------------------- 1 | var UserView = React.createClass({ 2 | contextTypes: { 3 | router: React.PropTypes.func 4 | }, 5 | /* 6 | * Lifecycle 7 | */ 8 | getInitialState() { 9 | var params = this.context.router.getCurrentParams(); 10 | User.find(params.id, {bypassCache: true}); 11 | return this.getState(params); 12 | }, 13 | componentDidMount: function () { 14 | User.on('DS.change', this.onChange); 15 | Post.on('DS.change', this.onChange); 16 | Comment.on('DS.change', this.onChange); 17 | }, 18 | componentWillUnmount: function () { 19 | User.off('DS.change', this.onChange); 20 | Post.off('DS.change', this.onChange); 21 | Comment.off('DS.change', this.onChange); 22 | }, 23 | /* 24 | * Event Handlers 25 | */ 26 | onChange: function () { 27 | this.setState(this.getState(this.context.router.getCurrentParams())); 28 | }, 29 | /* 30 | * Methods 31 | */ 32 | getState: function (params) { 33 | return { 34 | user: User.get(params.id) || {} 35 | }; 36 | }, 37 | render: function () { 38 | return ( 39 |
    40 |

    41 | {this.state.user.name } 42 |

    43 | 44 |
    45 |
    46 |

    Posts

    47 | 48 | {this.state.user.posts.map(function (post) { 49 | return ( 50 |
    51 | {post.title} 52 | 53 | {moment(post.created_at).fromNow()} 54 | 55 |
    56 | ); 57 | })} 58 |
    59 |
    60 |

    Comments

    61 | 62 | {this.state.user.comments.map(function (comment) { 63 | return ( 64 |
    65 | Go to post  66 | {comment.body} 67 | 68 | {moment(comment.created_at).fromNow()} 69 | 70 |
    71 | ); 72 | })} 73 |
    74 |
    75 |
    76 | ); 77 | } 78 | }); 79 | -------------------------------------------------------------------------------- /client/react/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | 3 | module.exports = { 4 | devtool: 'source-map', 5 | entry: './src/index.js', 6 | output: { 7 | path: '../public', 8 | publicPath: '/public/', 9 | filename: 'bundle.js' 10 | }, 11 | module: { 12 | loaders: [ 13 | { 14 | test: /\.js$/, 15 | exclude: /node_modules/, 16 | loader: 'babel-loader' 17 | } 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | rethinkdb_data 3 | *.log 4 | config.json -------------------------------------------------------------------------------- /server/config.default.json: -------------------------------------------------------------------------------- 1 | { 2 | "ADAPTER": "rethinkdb", 3 | "CLIENT": "react", 4 | "DB_HOST": "localhost", 5 | "DB_PORT": null, 6 | "DB": "blog", 7 | "GITHUB_CLIENT_ID": "", 8 | "GITHUB_CLIENT_SECRET": "", 9 | "GITHUB_CALLBACK_URL": "" 10 | } -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-data-examples-server", 3 | "description": "Backend for js-data-examples", 4 | "version": "3.0.0-alpha.1", 5 | "homepage": "https://github.com/js-data/js-data-examples", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/js-data/js-data-examples.git" 9 | }, 10 | "author": "js-data-examples project authors", 11 | "license": "MIT", 12 | "private": true, 13 | "scripts": { 14 | "start": "node src/index.js" 15 | }, 16 | "dependencies": { 17 | "bluebird": "^3.3.4", 18 | "body-parser": "^1.13.2", 19 | "cookie-parser": "^1.4.1", 20 | "express": "^4.13.4", 21 | "express-session": "^1.13.0", 22 | "js-data": "^3.0.0-beta.6", 23 | "js-data-mongodb": "^1.0.0-beta.1", 24 | "js-data-rethinkdb": "^3.0.0-beta.6", 25 | "js-data-sql": "^1.0.0-beta.3", 26 | "nconf": "^0.8.4", 27 | "passport": "^0.3.2", 28 | "passport-github": "^1.1.0", 29 | "source-map-support": "^0.4.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /server/rethinkdb/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | rethinkdb_data 3 | *.log 4 | config.js -------------------------------------------------------------------------------- /server/rethinkdb/README.md: -------------------------------------------------------------------------------- 1 | js-data logo 2 | 3 | ## server-side js-data w/RethinkDB 4 | 5 | #### Quick Start 6 | This will default to using the js-data + Angular frontend client 7 | 8 | 1. `cd js-data-examples/server/rethinkdb` 9 | 1. `rethinkdb` (Start rethinkdb, if you don't already have an instance running somewhere else 10 | 1. `node app/app.js` (In another terminal) 11 | 1. `open localhost:3000` (In another terminal) 12 | 13 | Modify `app/local.config.js` as necessary. 14 | -------------------------------------------------------------------------------- /server/rethinkdb/app/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var http = require('http'); 3 | var bodyParser = require('body-parser'); 4 | var cookieParser = require('cookie-parser'); 5 | var session = require('express-session'); 6 | var methodOverride = require('method-override'); 7 | var GitHubStrategy = require('passport-github').Strategy; 8 | var path = require('path'); 9 | var container = require('./container'); 10 | 11 | // This allows us to programmatically start the app in a test environment 12 | exports.createServer = function () { 13 | 14 | // Bootstrap the application, injecting a bunch of dependencies 15 | return container.resolve(function (User, safeCall, users, posts, comments, errorHandler, queryRewrite, rewriteRelations, passport, config) { 16 | var app = express(); 17 | 18 | // Simple route middleware to ensure user is authenticated. 19 | // Use this route middleware on any resource that needs to be protected. If 20 | // the request is authenticated (typically via a persistent login session), 21 | // the request will proceed. Otherwise, the user will be redirected to the 22 | // login page. 23 | function ensureAuthenticated(req, res, next) { 24 | if (req.isAuthenticated()) { 25 | return next(); 26 | } 27 | res.redirect('/login'); 28 | } 29 | 30 | function renderIndex(req, res, next) { 31 | res.render('index.html', {}, function (err, html) { 32 | if (err) { 33 | next(err); 34 | } else { 35 | res.send(html); 36 | } 37 | }); 38 | } 39 | 40 | // Passport session setup. 41 | // To support persistent login sessions, Passport needs to be able to 42 | // serialize users into and deserialize users out of the session. Typically, 43 | // this will be as simple as storing the user ID when serializing, and finding 44 | // the user by ID when deserializing. However, since this example does not 45 | // have a database of user records, the complete GitHub profile is serialized 46 | // and deserialized. 47 | passport.serializeUser(function (user, done) { 48 | done(null, user); 49 | }); 50 | 51 | passport.deserializeUser(function (obj, done) { 52 | done(null, obj); 53 | }); 54 | 55 | // Use the GitHubStrategy within Passport. 56 | // Strategies in Passport require a `verify` function, which accept 57 | // credentials (in this case, an accessToken, refreshToken, and GitHub 58 | // profile), and invoke a callback with a user object. 59 | passport.use(new GitHubStrategy({ 60 | clientID: config.GITHUB_CLIENT_ID, 61 | clientSecret: config.GITHUB_CLIENT_SECRET, 62 | callbackURL: config.GITHUB_CALLBACK_URL 63 | }, 64 | function (accessToken, refreshToken, profile, done) { 65 | User.findAll({ 66 | github_id: profile.id 67 | }).then(function (users) { 68 | if (users.length) { 69 | return users[0]; 70 | } else { 71 | return User.create({ 72 | username: profile.username, 73 | avatar_url: profile._json.avatar_url, 74 | github_id: profile.id, 75 | name: profile.displayName, 76 | created_at: new Date() 77 | }); 78 | } 79 | }).then(function (user) { 80 | return done(null, user); 81 | }).catch(function (err) { 82 | done(err); 83 | }); 84 | } 85 | )); 86 | 87 | // middleware 88 | app.use(queryRewrite); 89 | app.use(rewriteRelations); 90 | app.use(bodyParser.json()); 91 | app.use(cookieParser()); 92 | app.use(bodyParser.urlencoded({ extended: true })); 93 | app.use(methodOverride()); 94 | 95 | // I'm only using Express to render/serve the index.html file and other static assets for simplicity with the example apps 96 | app.set('views', path.resolve(config.PUBLIC_PATH)); 97 | app.set('view engine', 'ejs'); 98 | app.engine('html', require('ejs').renderFile); 99 | 100 | // Using Express/Passports for simplicity in the example. I would never use this in production. 101 | app.use(session({ secret: 'keyboard cat' })); 102 | app.use(passport.initialize()); 103 | app.use(passport.session()); 104 | // PUBLIC_PATH is used to choose with frontend client to use. Default is the js-data + Angular client. 105 | app.use(express.static(path.resolve(config.PUBLIC_PATH))); 106 | 107 | // app settings 108 | app.enable('trust proxy'); 109 | 110 | app.get('/', renderIndex); 111 | 112 | /******************************/ 113 | /********** comments **********/ 114 | /******************************/ 115 | app.route('/api/comments') 116 | // GET /comments 117 | .get(safeCall(comments.findAll)) 118 | // POST /comments 119 | .post(ensureAuthenticated, safeCall(comments.createOne)); 120 | 121 | app.route('/api/comments/:id') 122 | .put(ensureAuthenticated, safeCall(comments.updateOneById)) 123 | .delete(ensureAuthenticated, safeCall(comments.deleteOneById)); 124 | 125 | /*******************************/ 126 | /********** posts **********/ 127 | /*******************************/ 128 | app.route('/api/posts') 129 | .get(safeCall(posts.findAll)) 130 | .post(ensureAuthenticated, safeCall(posts.createOne)); 131 | 132 | app.route('/api/posts/:id') 133 | .get(safeCall(posts.findOneById)) 134 | .put(ensureAuthenticated, safeCall(posts.updateOneById)) 135 | .delete(ensureAuthenticated, safeCall(posts.deleteOneById)); 136 | 137 | /*******************************/ 138 | /********** users **************/ 139 | /*******************************/ 140 | app.get('/api/users/loggedInUser', function (req, res) { 141 | if (req.isAuthenticated()) { 142 | return res.json(req.user); 143 | } else { 144 | res.send(); 145 | } 146 | }); 147 | 148 | app.route('/api/users') 149 | .get(safeCall(users.findAll)); 150 | 151 | app.route('/api/users/:id') 152 | .get(safeCall(users.findOneById)); 153 | 154 | // Normally I would have a bunch of user-related routes, but I'm 155 | // just using Passport.js + Github for simplicity in the example 156 | 157 | /*******************************/ 158 | /*********** auth **************/ 159 | /*******************************/ 160 | app.get('/auth/github', passport.authenticate('github')); 161 | app.get('/auth/github/callback', passport.authenticate('github', { failureRedirect: '/login' }), function (req, res) { 162 | res.redirect('/'); 163 | }); 164 | app.post('/api/logout', function (req, res) { 165 | req.logout(); 166 | res.redirect('/'); 167 | }); 168 | app.post('/api/socket.io/', function (req, res, next) { 169 | next(); 170 | }); 171 | 172 | // Redirect all others to the index (HTML5 history) 173 | app.get('*', function (req, res, next) { 174 | if (req.originalUrl.indexOf('socket.io') === -1) { 175 | renderIndex(req, res, next); 176 | } 177 | }); 178 | 179 | // Catch-all error handler 180 | app.use(function (err, req, res, next) { 181 | errorHandler(err, req, res, next); 182 | }); 183 | 184 | return app; 185 | }); 186 | }; 187 | 188 | // This allows us to programmatically start the app in a test environment 189 | if (module === require.main || process.env.NODE_ENV === 'prod') { 190 | var app = exports.createServer(); 191 | var server = http.createServer(app); 192 | var config = container.get('config'); 193 | server.listen(config.PORT); 194 | } 195 | -------------------------------------------------------------------------------- /server/rethinkdb/app/container.js: -------------------------------------------------------------------------------- 1 | // dependable is and IOC container library 2 | // we register components with the container, and modules declare dependencies that they need injected 3 | var dependable = require('dependable'); 4 | var path = require('path'); 5 | 6 | var container = dependable.container(); 7 | 8 | // Load and register app config 9 | container.register('config', function () { 10 | return { 11 | PORT: process.env.PORT || 3000, 12 | 13 | // database configuration 14 | DB_HOST: process.env.DB_HOST || '127.0.0.1', 15 | DB_PORT: process.env.DB_PORT || 28015, 16 | DB_DATABASE: process.env.DB_DATABASE || 'jsdata', 17 | 18 | // Login won't work without these 19 | GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID || '', 20 | GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET || '', 21 | 22 | GITHUB_CALLBACK_URL: process.env.GITHUB_CALLBACK_URL || 'http://127.0.0.1:3000/auth/github/callback', 23 | 24 | // Default to using the js-data + angular example for the client. 25 | PUBLIC_PATH: process.env.PUBLIC_PATH || '../../../client/angular' 26 | }; 27 | }); 28 | 29 | // 3rd-party dependencies 30 | // Anything registered with the container becomes trivial to mock in tests 31 | container.register('mout', function () { 32 | return require('mout'); 33 | }); 34 | container.register('Promise', function () { 35 | return require('bluebird'); 36 | }); 37 | container.register('passport', function () { 38 | return require('passport'); 39 | }); 40 | 41 | // Create and register a js-data-rethinkdb adapter with the container 42 | container.register('rethinkdbAdapter', function (config) { 43 | var DSRethinkDbAdapter = require('js-data-rethinkdb'); 44 | return new DSRethinkDbAdapter({ 45 | host: config.DB_HOST, 46 | port: config.DB_PORT, 47 | db: config.DB_DATABASE, 48 | min: 10, 49 | max: 50 50 | }); 51 | }); 52 | 53 | // For custom RethinkDB queries, register the "r" object that comes from rethinkdbdash 54 | container.register('r', function (rethinkdbAdapter) { 55 | return rethinkdbAdapter.r; 56 | }); 57 | 58 | // Register the a new data store with the container 59 | container.register('DS', function (container, rethinkdbAdapter) { 60 | var JSData = require('js-data'); 61 | var store = new JSData.DS({ 62 | cacheResponse: false, 63 | bypassCache: true, 64 | 65 | // Important: If your relationships stop working, you might 66 | // have deleted this. 67 | linkRelations: false, 68 | 69 | // Because 70 | upsert: false, 71 | 72 | // Don't really need this stuff on the server, so let's improve 73 | // performance and disable these features 74 | notify: false, 75 | keepChangeHistory: false, 76 | resetHistoryOnInject: false, 77 | 78 | // Here you could set "log" to a function of your own, for 79 | // debugging or to hook it into your own logging code. 80 | // You would the same with the "error" option. 81 | log: false 82 | }); 83 | 84 | // Register the rethinkdb adapter as the default adapter 85 | store.registerAdapter('rethinkdb', rethinkdbAdapter, {'default': true}); 86 | 87 | return store; 88 | }); 89 | 90 | // Automatically load and register the modules in these folders 91 | container.load(path.join(__dirname, './controllers')); 92 | container.load(path.join(__dirname, './middleware')); 93 | container.load(path.join(__dirname, './models')); 94 | container.load(path.join(__dirname, './lib')); 95 | 96 | // Register the container with the container, useful for when you need dynamically resolve a dependency or avoid a circular dependency 97 | container.register('container', function () { 98 | return container; 99 | }); 100 | 101 | module.exports = container; 102 | -------------------------------------------------------------------------------- /server/rethinkdb/app/controllers/comments.js: -------------------------------------------------------------------------------- 1 | module.exports = function (Comment) { 2 | 3 | return { 4 | /** 5 | * GET /comments 6 | */ 7 | findAll: function (req, res) { 8 | return Comment.findAll(req.query, {'with': ['user']}).then(function (comments) { 9 | return res.status(200).send(comments).end(); 10 | }); 11 | }, 12 | 13 | /** 14 | * POST /comments 15 | */ 16 | createOne: function (req, res) { 17 | var comment = Comment.createInstance(req.body); 18 | comment.owner_id = req.user.id; 19 | return Comment.create(comment, {'with': ['user']}).then(function (comment) { 20 | return res.status(201).send(comment).end(); 21 | }); 22 | }, 23 | 24 | /** 25 | * PUT /comments/:id 26 | */ 27 | updateOneById: function (req, res, next) { 28 | return Comment.find(req.params.id).then(function (comment) { 29 | if (comment.owner_id !== req.user.id) { 30 | return next(404); 31 | } else { 32 | return Comment.update(comment.id, req.body).then(function (comment) { 33 | return res.status(200).send(comment).end(); 34 | }); 35 | } 36 | }); 37 | }, 38 | 39 | /** 40 | * DELETE /comments/:id 41 | */ 42 | deleteOneById: function (req, res, next) { 43 | return Comment.find(req.params.id).then(function (comment) { 44 | if (comment.owner_id !== req.user.id) { 45 | return next(404); 46 | } else { 47 | return Comment.destroy(comment.id).then(function () { 48 | return res.status(204).end(); 49 | }); 50 | } 51 | }); 52 | } 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /server/rethinkdb/app/controllers/posts.js: -------------------------------------------------------------------------------- 1 | module.exports = function (Post) { 2 | 3 | return { 4 | /** 5 | * GET /posts 6 | */ 7 | findAll: function (req, res) { 8 | return Post.findAll(req.query).then(function (posts) { 9 | return res.status(200).send(posts).end(); 10 | }); 11 | }, 12 | 13 | /** 14 | * GET /posts/:id 15 | */ 16 | findOneById: function (req, res) { 17 | return Post.find(req.params.id, {'with': ['user']}).then(function (post) { 18 | return res.status(200).send(post).end(); 19 | }); 20 | }, 21 | 22 | /** 23 | * POST /posts 24 | */ 25 | createOne: function (req, res) { 26 | var post = Post.createInstance(req.body); 27 | post.owner_id = req.user.id; 28 | return Post.create(post, {'with': ['user']}).then(function (post) { 29 | return res.status(201).send(post).end(); 30 | }); 31 | }, 32 | 33 | /** 34 | * PUT /posts/:id 35 | */ 36 | updateOneById: function (req, res, next) { 37 | return Post.find(req.params.id).then(function (post) { 38 | if (post.owner_id !== req.user.id) { 39 | return next(404); 40 | } else { 41 | return Post.update(post.id, req.body).then(function (post) { 42 | return res.status(200).send(post).end(); 43 | }); 44 | } 45 | }); 46 | }, 47 | 48 | /** 49 | * DELETE /posts/:id 50 | */ 51 | deleteOneById: function (req, res, next) { 52 | return Post.find(req.params.id).then(function (post) { 53 | if (post.owner_id !== req.user.id) { 54 | return next(404); 55 | } else { 56 | return Post.destroy(post.id).then(function () { 57 | return res.status(204).end(); 58 | }); 59 | } 60 | }); 61 | } 62 | }; 63 | }; 64 | -------------------------------------------------------------------------------- /server/rethinkdb/app/controllers/users.js: -------------------------------------------------------------------------------- 1 | module.exports = function (User) { 2 | 3 | return { 4 | /** 5 | * GET /users 6 | */ 7 | findAll: function (req, res) { 8 | return User.findAll(req.query).then(function (users) { 9 | return res.status(200).send(users).end(); 10 | }); 11 | }, 12 | 13 | /** 14 | * GET /users/:id 15 | */ 16 | findOneById: function (req, res) { 17 | return User.find(req.params.id, {'with': ['post', 'comment']}).then(function (user) { 18 | user.posts = user.posts || []; 19 | user.comments = user.comments || []; 20 | // only return up to 10 comments 21 | user.comments.slice(0, 10); 22 | return res.status(200).send(user).end(); 23 | }); 24 | } 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /server/rethinkdb/app/default.config.js: -------------------------------------------------------------------------------- 1 | // Change as necessary 2 | module.exports = { 3 | // Port Express will listen to 4 | PORT: 3000, 5 | 6 | // Rethinkdb configuration 7 | DB_HOST: '127.0.0.1', 8 | DB_PORT: 28015, 9 | DB_DATABASE: 'jsdata', 10 | 11 | // Login won't work without these 12 | GITHUB_CLIENT_ID: '', 13 | GITHUB_CLIENT_SECRET: '', 14 | 15 | GITHUB_CALLBACK_URL: '', 16 | }; 17 | -------------------------------------------------------------------------------- /server/sql/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bower_components/ 3 | 4 | .idea/ 5 | 6 | *.iml 7 | 8 | coverage/ 9 | 10 | rethinkdb_data/ 11 | 12 | local.config.js 13 | 14 | server/sql/.git/ 15 | 16 | node_modules 17 | 18 | npm-debug.log 19 | -------------------------------------------------------------------------------- /server/sql/Procfile: -------------------------------------------------------------------------------- 1 | web: node app/app.js 2 | -------------------------------------------------------------------------------- /server/sql/README.md: -------------------------------------------------------------------------------- 1 | js-data logo 2 | 3 | ## server-side js-data w/sql 4 | -------------------------------------------------------------------------------- /server/sql/app/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var http = require('http'); 3 | var bodyParser = require('body-parser'); 4 | var cookieParser = require('cookie-parser'); 5 | var session = require('express-session'); 6 | var methodOverride = require('method-override'); 7 | var GitHubStrategy = require('passport-github').Strategy; 8 | var path = require('path'); 9 | var container = require('./container'); 10 | 11 | // This allows us to programmatically start the app in a test environment 12 | exports.createServer = function () { 13 | 14 | // Bootstrap the application, injecting a bunch of dependencies 15 | return container.resolve(function (User, safeCall, users, posts, comments, errorHandler, queryRewrite, rewriteRelations, passport, config) { 16 | var app = express(); 17 | 18 | // Simple route middleware to ensure user is authenticated. 19 | // Use this route middleware on any resource that needs to be protected. If 20 | // the request is authenticated (typically via a persistent login session), 21 | // the request will proceed. Otherwise, the user will be redirected to the 22 | // login page. 23 | function ensureAuthenticated(req, res, next) { 24 | if (req.isAuthenticated()) { 25 | return next(); 26 | } 27 | res.redirect('/login'); 28 | } 29 | 30 | function renderIndex(req, res, next) { 31 | res.render('index.html', {}, function (err, html) { 32 | if (err) { 33 | next(err); 34 | } else { 35 | res.status(200).send(html).end(); 36 | } 37 | }); 38 | } 39 | 40 | passport.serializeUser(function (user, done) { 41 | done(null, user); 42 | }); 43 | 44 | passport.deserializeUser(function (user, done) { 45 | done(null, user); 46 | }); 47 | 48 | // Use the GitHubStrategy within Passport. 49 | // Strategies in Passport require a `verify` function, which accept 50 | // credentials (in this case, an accessToken, refreshToken, and GitHub 51 | // profile), and invoke a callback with a user object. 52 | passport.use(new GitHubStrategy({ 53 | clientID: config.GITHUB_CLIENT_ID, 54 | clientSecret: config.GITHUB_CLIENT_SECRET, 55 | callbackURL: config.GITHUB_CALLBACK_URL 56 | }, 57 | function (accessToken, refreshToken, profile, done) { 58 | User.findAll({ 59 | github_id: profile.id 60 | }).then(function (users) { 61 | if (users.length) { 62 | return users[0]; 63 | } else { 64 | return User.create({ 65 | username: profile.username, 66 | avatar_url: profile._json.avatar_url, 67 | github_id: profile.id, 68 | name: profile.displayName, 69 | created_at: new Date() 70 | }); 71 | } 72 | }).then(function (user) { 73 | return done(null, user); 74 | }).catch(function (err) { 75 | done(err); 76 | }); 77 | } 78 | )); 79 | 80 | // middleware 81 | app.use(queryRewrite); 82 | app.use(rewriteRelations); 83 | app.use(bodyParser.json()); 84 | app.use(cookieParser()); 85 | app.use(bodyParser.urlencoded({extended: true})); 86 | app.use(methodOverride()); 87 | 88 | // I'm only using Express to render/serve the index.html file and other static assets for simplicity with the example apps 89 | app.set('views', path.resolve(config.PUBLIC_PATH)); 90 | app.set('view engine', 'ejs'); 91 | app.engine('html', require('ejs').renderFile); 92 | 93 | // Using Express/Passports for simplicity in the example. I would never use this in production. 94 | app.use(session({secret: 'keyboard cat'})); 95 | app.use(passport.initialize()); 96 | app.use(passport.session()); 97 | // PUBLIC_PATH is used to choose with frontend client to use. Default is the js-data + Angular client. 98 | app.use(express.static(path.resolve(config.PUBLIC_PATH))); 99 | 100 | // app settings 101 | app.enable('trust proxy'); 102 | 103 | app.get('/', renderIndex); 104 | 105 | /******************************/ 106 | /********** comments **********/ 107 | /******************************/ 108 | app.route('/api/comments') 109 | // GET /comments 110 | .get(safeCall(comments.findAll)) 111 | // POST /comments 112 | .post(ensureAuthenticated, safeCall(comments.createOne)); 113 | 114 | app.route('/api/comments/:id') 115 | .put(ensureAuthenticated, safeCall(comments.updateOneById)) 116 | .delete(ensureAuthenticated, safeCall(comments.deleteOneById)); 117 | 118 | /*******************************/ 119 | /********** posts **************/ 120 | /*******************************/ 121 | app.route('/api/posts') 122 | .get(safeCall(posts.findAll)) 123 | .post(ensureAuthenticated, safeCall(posts.createOne)); 124 | 125 | app.route('/api/posts/:id') 126 | .get(safeCall(posts.findOneById)) 127 | .put(ensureAuthenticated, safeCall(posts.updateOneById)) 128 | .delete(ensureAuthenticated, safeCall(posts.deleteOneById)); 129 | 130 | /*******************************/ 131 | /********** users **************/ 132 | /*******************************/ 133 | app.get('/api/users/loggedInUser', function (req, res) { 134 | if (req.isAuthenticated()) { 135 | return res.json(req.user); 136 | } else { 137 | res.send(); 138 | } 139 | }); 140 | 141 | app.route('/api/users') 142 | .get(safeCall(users.findAll)); 143 | 144 | app.route('/api/users/:id') 145 | .get(safeCall(users.findOneById)); 146 | 147 | // Normally I would have a bunch of user-related routes, but I'm 148 | // just using Passport.js + Github for simplicity in the example 149 | 150 | /*******************************/ 151 | /*********** auth **************/ 152 | /*******************************/ 153 | app.get('/auth/github', passport.authenticate('github')); 154 | app.get('/auth/github/callback', passport.authenticate('github', {failureRedirect: '/login'}), function (req, res) { 155 | res.redirect('/'); 156 | }); 157 | app.post('/api/logout', function (req, res) { 158 | req.logout(); 159 | res.redirect('/'); 160 | }); 161 | 162 | // Redirect all others to the index (HTML5 history) 163 | app.get('*', function (req, res, next) { 164 | if (req.originalUrl.indexOf('socket.io') === -1) { 165 | renderIndex(req, res, next); 166 | } else { 167 | next(); 168 | } 169 | }); 170 | 171 | // Catch-all error handler 172 | app.use(function (err, req, res, next) { 173 | errorHandler(err, req, res, next); 174 | }); 175 | 176 | return app; 177 | }); 178 | }; 179 | 180 | // This allows us to programmatically start the app in a test environment 181 | if (module === require.main || process.env.NODE_ENV === 'prod') { 182 | var app = exports.createServer(); 183 | var server = http.createServer(app); 184 | var config = container.get('config'); 185 | 186 | // Add a socket server to be used as a message bus for the clients 187 | var io = require('socket.io').listen(server); 188 | 189 | server.listen(config.PORT); 190 | 191 | container.register('io', function () { 192 | return io; 193 | }); 194 | 195 | var query = container.get('query'); 196 | 197 | query.schema.hasTable('users').then(function (exists) { 198 | if (!exists) { 199 | return query.schema.createTable('users', function (t) { 200 | t.increments('id').primary(); 201 | t.string('username'); 202 | t.string('name'); 203 | t.integer('github_id').unsigned(); 204 | t.string('avatar_url'); 205 | t.timestamps(); 206 | }); 207 | } 208 | }).then(function () { 209 | return query.schema.hasTable('posts'); 210 | }).then(function (exists) { 211 | if (!exists) { 212 | return query.schema.createTable('posts', function (t) { 213 | t.increments('id').primary(); 214 | t.integer('owner_id').unsigned().references('id').inTable('users'); 215 | t.string('title'); 216 | t.text('body'); 217 | t.timestamps(); 218 | }); 219 | } 220 | }).then(function () { 221 | return query.schema.hasTable('comments'); 222 | }).then(function (exists) { 223 | if (!exists) { 224 | return query.schema.createTable('comments', function (t) { 225 | t.increments('id').primary(); 226 | t.integer('post_id').unsigned().references('id').inTable('posts'); 227 | t.integer('owner_id').unsigned().references('id').inTable('users'); 228 | t.text('body'); 229 | t.timestamps(); 230 | }); 231 | } 232 | }).catch(function (err) { 233 | console.log(err); 234 | }); 235 | } 236 | -------------------------------------------------------------------------------- /server/sql/app/container.js: -------------------------------------------------------------------------------- 1 | // dependable is and IOC container library 2 | // we register components with the container, and modules declare dependencies that they need injected 3 | var dependable = require('dependable'); 4 | var path = require('path'); 5 | 6 | var container = dependable.container(); 7 | 8 | // Load and register app config 9 | container.register('config', function () { 10 | return { 11 | PORT: process.env.PORT || 3000, 12 | 13 | // database configuration 14 | DB_CLIENT: process.env.DB_CLIENT || 'mysql', // or "pg" or "sqlite3" 15 | DB_HOST: process.env.DB_HOST || '127.0.0.1', 16 | DB_PORT: process.env.DB_PORT || 3306, 17 | DB_DATABASE: process.env.DB_DATABASE || 'jsdata', 18 | DB_USER: process.env.DB_USER || 'root', 19 | DB_PASSWORD: process.env.DB_PASSWORD || '', 20 | 21 | // Login won't work without these 22 | GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID || '', 23 | GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET || '', 24 | 25 | GITHUB_CALLBACK_URL: process.env.GITHUB_CALLBACK_URL || 'http://127.0.0.1:3000/auth/github/callback', 26 | 27 | // Default to using the js-data + angular example for the client. 28 | PUBLIC_PATH: process.env.PUBLIC_PATH || '../../../client/angular' 29 | }; 30 | }); 31 | 32 | // 3rd-party dependencies 33 | // Anything registered with the container becomes trivial to mock in tests 34 | container.register('mout', function () { 35 | return require('mout'); 36 | }); 37 | container.register('Promise', function () { 38 | return require('bluebird'); 39 | }); 40 | container.register('passport', function () { 41 | return require('passport'); 42 | }); 43 | 44 | // Create and register a js-data-sql adapter with the container 45 | container.register('sqlAdapter', function (config) { 46 | var DSSqlAdapter = require('js-data-sql'); 47 | return new DSSqlAdapter({ 48 | client: config.DB_CLIENT, 49 | connection: { 50 | host: config.DB_HOST, 51 | user: config.DB_USER, 52 | password: config.DB_PASSWORD, 53 | database: config.DB_DATABASE, 54 | port: config.DB_PORT 55 | } 56 | }); 57 | }); 58 | 59 | // For custom SQL queries, register the "query" object that comes from the adapter 60 | container.register('query', function (sqlAdapter) { 61 | return sqlAdapter.query; 62 | }); 63 | 64 | // Automatically load and register the modules in these folders 65 | container.load(path.join(__dirname, './controllers')); 66 | container.load(path.join(__dirname, './middleware')); 67 | container.load(path.join(__dirname, './models')); 68 | container.load(path.join(__dirname, './lib')); 69 | 70 | // Register the a new data store with the container 71 | container.register('DS', function (Promise, container, sqlAdapter, messageService) { 72 | var JSData = require('js-data'); 73 | JSData.DSUtils.Promise = Promise; 74 | var store = new JSData.DS({ 75 | cacheResponse: false, 76 | bypassCache: true, 77 | 78 | // Important: If your relationships stop working, you might 79 | // have deleted this. 80 | linkRelations: false, 81 | 82 | // Because 83 | upsert: false, 84 | 85 | // Don't really need this stuff on the server, so let's improve 86 | // performance and disable these features 87 | notify: false, 88 | keepChangeHistory: false, 89 | resetHistoryOnInject: false, 90 | 91 | // Here you could set "log" to a function of your own, for 92 | // debugging or to hook it into your own logging code. 93 | // You would the same with the "error" option. 94 | log: false, 95 | 96 | beforeCreate: function (Resource, instance, cb) { 97 | instance.created_at = new Date(); 98 | instance.updated_at = new Date(); 99 | return cb(null, instance); 100 | }, 101 | 102 | beforeUpdate: function (Resource, instance, cb) { 103 | instance.updated_at = new Date(); 104 | return cb(null, instance); 105 | }, 106 | 107 | afterCreate: function (Resource, instance, cb) { 108 | messageService.sendCreateMessage(Resource.name, instance); 109 | cb(null, instance); 110 | }, 111 | 112 | afterUpdate: function (Resource, instance, cb) { 113 | messageService.sendUpdateMessage(Resource.name, instance); 114 | cb(null, instance); 115 | }, 116 | 117 | afterDestroy: function (Resource, instance, cb) { 118 | messageService.sendDestroyMessage(Resource.name, instance); 119 | cb(null, instance); 120 | } 121 | }); 122 | 123 | // Register the sql adapter as the default adapter 124 | store.registerAdapter('sql', sqlAdapter, {'default': true}); 125 | 126 | return store; 127 | }); 128 | 129 | // Register the container with the container, useful for when you need dynamically resolve a dependency or avoid a circular dependency 130 | container.register('container', function () { 131 | return container; 132 | }); 133 | 134 | module.exports = container; 135 | -------------------------------------------------------------------------------- /server/sql/app/controllers/comments.js: -------------------------------------------------------------------------------- 1 | module.exports = function (Comment) { 2 | 3 | return { 4 | /** 5 | * GET /comments 6 | */ 7 | findAll: function (req, res) { 8 | return Comment.findAll(req.query, {'with': ['user']}).then(function (comments) { 9 | return res.status(200).send(comments).end(); 10 | }); 11 | }, 12 | 13 | /** 14 | * POST /comments 15 | */ 16 | createOne: function (req, res) { 17 | var comment = Comment.createInstance(req.body); 18 | comment.owner_id = req.user.id; 19 | return Comment.create(comment, {'with': ['user']}).then(function (comment) { 20 | return res.status(201).send(comment).end(); 21 | }); 22 | }, 23 | 24 | /** 25 | * PUT /comments/:id 26 | */ 27 | updateOneById: function (req, res, next) { 28 | return Comment.find(req.params.id).then(function (comment) { 29 | if (comment.owner_id !== req.user.id) { 30 | return next(404); 31 | } else { 32 | return Comment.update(comment.id, req.body).then(function (comment) { 33 | return res.status(200).send(comment).end(); 34 | }); 35 | } 36 | }); 37 | }, 38 | 39 | /** 40 | * DELETE /comments/:id 41 | */ 42 | deleteOneById: function (req, res, next) { 43 | return Comment.find(req.params.id).then(function (comment) { 44 | if (comment.owner_id !== req.user.id) { 45 | return next(404); 46 | } else { 47 | return Comment.destroy(comment.id).then(function () { 48 | return res.status(204).end(); 49 | }); 50 | } 51 | }); 52 | } 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /server/sql/app/controllers/posts.js: -------------------------------------------------------------------------------- 1 | module.exports = function (Post) { 2 | 3 | return { 4 | /** 5 | * GET /posts 6 | */ 7 | findAll: function (req, res) { 8 | return Post.findAll(req.query).then(function (posts) { 9 | return res.status(200).send(posts).end(); 10 | }); 11 | }, 12 | 13 | /** 14 | * GET /posts/:id 15 | */ 16 | findOneById: function (req, res) { 17 | return Post.find(req.params.id, {'with': ['user', 'comment']}).then(function (post) { 18 | return res.status(200).send(post).end(); 19 | }); 20 | }, 21 | 22 | /** 23 | * POST /posts 24 | */ 25 | createOne: function (req, res) { 26 | var post = Post.createInstance(req.body); 27 | post.owner_id = req.user.id; 28 | return Post.create(post, {'with': ['user']}).then(function (post) { 29 | return res.status(201).send(post).end(); 30 | }); 31 | }, 32 | 33 | /** 34 | * PUT /posts/:id 35 | */ 36 | updateOneById: function (req, res, next) { 37 | return Post.find(req.params.id).then(function (post) { 38 | if (post.owner_id !== req.user.id) { 39 | return next(404); 40 | } else { 41 | return Post.update(post.id, req.body).then(function (post) { 42 | return res.status(200).send(post).end(); 43 | }); 44 | } 45 | }); 46 | }, 47 | 48 | /** 49 | * DELETE /posts/:id 50 | */ 51 | deleteOneById: function (req, res, next) { 52 | return Post.find(req.params.id).then(function (post) { 53 | if (post.owner_id !== req.user.id) { 54 | return next(404); 55 | } else { 56 | return Post.destroy(post.id).then(function () { 57 | return res.status(204).end(); 58 | }); 59 | } 60 | }); 61 | } 62 | }; 63 | }; 64 | -------------------------------------------------------------------------------- /server/sql/app/controllers/users.js: -------------------------------------------------------------------------------- 1 | module.exports = function (User) { 2 | 3 | return { 4 | /** 5 | * GET /users 6 | */ 7 | findAll: function (req, res) { 8 | return User.findAll(req.query).then(function (users) { 9 | return res.status(200).send(users).end(); 10 | }); 11 | }, 12 | 13 | /** 14 | * GET /users/:id 15 | */ 16 | findOneById: function (req, res) { 17 | return User.find(req.params.id, {'with': ['post', 'comment']}).then(function (user) { 18 | user.posts = user.posts || []; 19 | user.comments = user.comments || []; 20 | // only return up to 10 comments 21 | user.comments.slice(0, 10); 22 | return res.status(200).send(user).end(); 23 | }); 24 | } 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /server/sql/app/lib/messageService.js: -------------------------------------------------------------------------------- 1 | // Our service for broadcasting messages to connected clients. 2 | // A way of implementing a sort of "3-way binding" 3 | module.exports = function (container) { 4 | 5 | function sendMessage(event, resource, instance) { 6 | var io = container.get('io'); 7 | var message = { 8 | id: instance.id, 9 | owner_id: instance.owner_id, 10 | resource: resource, 11 | event: event 12 | }; 13 | io.sockets.emit(event, message); 14 | } 15 | 16 | return { 17 | sendCreateMessage: function (resource, instance) { 18 | sendMessage('create', resource, instance); 19 | }, 20 | 21 | sendDestroyMessage: function (resource, instance) { 22 | sendMessage('destroy', resource, instance); 23 | }, 24 | 25 | sendUpdateMessage: function (resource, instance) { 26 | sendMessage('update', resource, instance); 27 | } 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /server/sql/app/lib/safeCall.js: -------------------------------------------------------------------------------- 1 | // This little generator simply saves us a lot of boilerplate code 2 | module.exports = function (Promise) { 3 | function safeCall(method) { 4 | return function (req, res, next) { 5 | return Promise.try(function () { 6 | return method(req, res, next); 7 | }).catch(next).error(next); 8 | }; 9 | } 10 | 11 | return safeCall; 12 | }; 13 | -------------------------------------------------------------------------------- /server/sql/app/middleware/errorHandler.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | var JSData = require('js-data'); 3 | var IllegalArgumentError = JSData.DSErrors.IllegalArgumentError; 4 | var RuntimeError = JSData.DSErrors.RuntimeError; 5 | return function (err, req, res) { 6 | var responder; 7 | if (err && req) { 8 | if (res) { 9 | // Normally you would be using some logging library 10 | console.log(err.message); 11 | console.log(err.stack); 12 | } 13 | } 14 | if (req && typeof req.send === 'function') { 15 | responder = req; 16 | } else if (res && typeof res.send === 'function') { 17 | responder = res; 18 | } 19 | if (err instanceof IllegalArgumentError) { 20 | responder.status(400).send(err.errors || err.message).end(); 21 | } else if (err instanceof RuntimeError) { 22 | responder.status(500).send(err.errors || err.message).end(); 23 | } else if (err === 'Not Found!' || (err.message && err.message === 'Not Found!')) { 24 | responder.status(404).end(); 25 | } else if (typeof err === 'number') { 26 | responder.status(err).end(); 27 | } else if (typeof err === 'object') { 28 | responder.status(400).send(err).end(); 29 | } else { 30 | responder.status(500).send({ error: '500 - Internal Server Error' }).end(); 31 | } 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /server/sql/app/middleware/queryRewrite.js: -------------------------------------------------------------------------------- 1 | // This is to fully parse the JSON in req.query 2 | module.exports = function () { 3 | return function (req, res, next) { 4 | if (req.query.where) { 5 | try { 6 | var where = JSON.parse(req.query.where); 7 | for (var key in where) { 8 | if (typeof where[key] === 'object' && '==' in where[key]) { 9 | where[key] = where[key]['==']; 10 | } 11 | } 12 | req.query.where = where; 13 | next(); 14 | } catch (err) { 15 | next(err); 16 | } 17 | } else { 18 | next(); 19 | } 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /server/sql/app/middleware/rewriteRelations.js: -------------------------------------------------------------------------------- 1 | // Removes 'with' from the query and saves it separately on the request. This 2 | // lets us use syntax like the following: 3 | // DS.findAll('user', req.query, { with: req.with }); 4 | // DS.find('user', 5, { with: req.with }) 5 | module.exports = function() { 6 | return function rewriteRelations(req, res, next) { 7 | if ('with' in req.query) { 8 | req.with = Array.isArray(req.query.with) ? req.query.with : [req.query.with]; 9 | delete req.query.with; 10 | } 11 | else { 12 | req.with = []; 13 | } 14 | 15 | next(); 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /server/sql/app/models/Comment.js: -------------------------------------------------------------------------------- 1 | module.exports = function (container, Promise, mout, messageService, config, DS) { 2 | if (DS.definitions.comment) { 3 | return DS.definitions.comment; 4 | } 5 | 6 | return DS.defineResource({ 7 | name: 'comment', 8 | table: 'comments', 9 | relations: { 10 | belongsTo: { 11 | user: { 12 | localField: 'user', 13 | localKey: 'owner_id' 14 | }, 15 | post: { 16 | localField: 'post', 17 | localKey: 'post_id' 18 | } 19 | } 20 | } 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /server/sql/app/models/Post.js: -------------------------------------------------------------------------------- 1 | module.exports = function (container, Promise, mout, messageService, config, DS) { 2 | if (DS.definitions.post) { 3 | return DS.definitions.post; 4 | } 5 | 6 | return DS.defineResource({ 7 | name: 'post', 8 | table: 'posts', 9 | relations: { 10 | belongsTo: { 11 | user: { 12 | localField: 'user', 13 | localKey: 'owner_id' 14 | } 15 | }, 16 | hasMany: { 17 | comment: { 18 | localField: 'comments', 19 | foreignKey: 'post_id' 20 | } 21 | } 22 | } 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /server/sql/app/models/User.js: -------------------------------------------------------------------------------- 1 | module.exports = function (container, Promise, mout, messageService, config, DS) { 2 | if (DS.definitions.user) { 3 | return DS.definitions.user; 4 | } 5 | 6 | return DS.defineResource({ 7 | name: 'user', 8 | table: 'users', 9 | relations: { 10 | hasMany: { 11 | post: { 12 | localField: 'posts', 13 | foreignKey: 'owner_id' 14 | }, 15 | comment: { 16 | localField: 'comments', 17 | foreignKey: 'owner_id' 18 | } 19 | } 20 | } 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /server/sql/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-data-sql-example", 3 | "description": "js-data + js-data-sql", 4 | "version": "1.1.0", 5 | "homepage": "http://www.js-data.io/docs/dssqladapter", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/js-data/js-data-examples.git" 9 | }, 10 | "author": { 11 | "name": "Jason Dobry", 12 | "email": "jason.dobry@gmail.com" 13 | }, 14 | "license": "MIT", 15 | "keywords": [ 16 | "data", 17 | "datastore", 18 | "store", 19 | "database", 20 | "adapter", 21 | "sql", 22 | "mysql", 23 | "postgresql", 24 | "mariadb", 25 | "sqlite" 26 | ], 27 | "scripts": { 28 | "start": "node app/app.js" 29 | }, 30 | "private": true, 31 | "dependencies": { 32 | "bluebird": "2.9.34", 33 | "body-parser": "1.13.2", 34 | "cookie-parser": "1.3.5", 35 | "dependable": "0.2.5", 36 | "ejs": "2.3.3", 37 | "express": "4.13.1", 38 | "express-session": "1.11.3", 39 | "js-data": "2.2.2", 40 | "js-data-sql": "0.9.2", 41 | "knex": "0.8.6", 42 | "mariasql": "0.1.23", 43 | "method-override": "2.3.4", 44 | "mout": "0.11.0", 45 | "mysql": "2.8.0", 46 | "passport": "0.2.2", 47 | "passport-github": "0.1.5", 48 | "pg": "4.4.0", 49 | "socket.io": "1.3.6", 50 | "sqlite3": "3.0.9" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /server/src/adapters/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function (adapter) { 4 | return require(`./${adapter}`) 5 | } 6 | -------------------------------------------------------------------------------- /server/src/adapters/mongodb.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const MongoDBAdapter = require('js-data-mongodb').MongoDBAdapter 4 | const nconf = require('nconf') 5 | 6 | const adapter = exports.adapter = new MongoDBAdapter() 7 | -------------------------------------------------------------------------------- /server/src/adapters/mysql.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const SqlAdapter = require('js-data-sql').SqlAdapter 4 | const nconf = require('nconf') 5 | 6 | const adapter = exports.adapter = new SqlAdapter({ 7 | knexOpts: { 8 | client: 'mysql', 9 | host: nconf.get('DB_HOST'), 10 | port: nconf.get('DB_PORT'), 11 | db: nconf.get('DB') 12 | } 13 | }) 14 | -------------------------------------------------------------------------------- /server/src/adapters/postgres.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const SqlAdapter = require('js-data-sql').SqlAdapter 4 | const nconf = require('nconf') 5 | 6 | const adapter = exports.adapter = new SqlAdapter({ 7 | knexOpts: { 8 | client: 'pg', 9 | host: nconf.get('DB_HOST'), 10 | port: nconf.get('DB_PORT'), 11 | db: nconf.get('DB') 12 | } 13 | }) 14 | -------------------------------------------------------------------------------- /server/src/adapters/rethinkdb.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const RethinkDBAdapter = require('js-data-rethinkdb').RethinkDBAdapter 4 | const nconf = require('nconf') 5 | 6 | const adapter = exports.adapter = new RethinkDBAdapter({ 7 | rOpts: { 8 | host: nconf.get('DB_HOST') || 'localhost', 9 | port: nconf.get('DB_PORT') || 28015, 10 | db: nconf.get('DB') || 'blog' 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /server/src/adapters/sqlite.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const SqlAdapter = require('js-data-sql').SqlAdapter 4 | const nconf = require('nconf') 5 | 6 | const adapter = exports.adapter = new SqlAdapter({ 7 | knexOpts: { 8 | client: 'sqlite3', 9 | host: nconf.get('DB_HOST'), 10 | port: nconf.get('DB_PORT'), 11 | db: nconf.get('DB') 12 | } 13 | }) 14 | -------------------------------------------------------------------------------- /server/src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | require('source-map-support').install() 4 | const Promise = require('bluebird') 5 | const JSData = require('js-data') 6 | const express = require('express') 7 | const nconf = require('nconf') 8 | 9 | // 1. Pull config from the commandline arguments 10 | // 2. Pull config from environment variables 11 | // 3. Pull config from a config file 12 | nconf.argv().env().file({ file: `${__dirname}/../config.json` }) 13 | 14 | // Make JSData use bluebird internally 15 | JSData.utils.Promise = Promise 16 | 17 | const utils = require('./utils') 18 | const middleware = require('./middleware') 19 | const store = require('./store').store 20 | const app = express() 21 | 22 | const PAGE_SIZE = 5 23 | 24 | // Application middleware 25 | app.use(require('body-parser').json()) 26 | app.use(require('express-session')({ secret: 'keyboard cat' })) 27 | app.use(middleware.passport.initialize()) 28 | app.use(middleware.passport.session()) 29 | app.use(express.static(`${__dirname}/../../client/public`)) 30 | app.use(middleware.queryRewrite) 31 | 32 | // Application routes 33 | app.get('/api/users/loggedInUser', function (req, res) { 34 | if (req.isAuthenticated()) { 35 | res.json(req.user) 36 | } 37 | res.end() 38 | }) 39 | 40 | app.get('/api/posts', utils.makeSafe(function (req, res, next) { 41 | req.query.limit = req.query.limit === undefined ? PAGE_SIZE : req.query.limit 42 | req.query.offset = req.query.offset === undefined ? 0 : req.query.offset 43 | const currentPage = (req.query.offset / PAGE_SIZE) + 1 44 | 45 | Promise.all([ 46 | store.count('post'), 47 | store.findAll('post', req.query) 48 | ]).spread(function (totalNumPosts, posts) { 49 | return { 50 | page: currentPage, 51 | data: posts, 52 | total: totalNumPosts 53 | } 54 | }).then(res.send.bind(res)) 55 | })) 56 | app.post('/api/posts', utils.makeSafe(function (req, res, next) { 57 | return store.create('post', { 58 | title: req.body.title, 59 | content: req.body.content 60 | }).then(res.send.bind(res)) 61 | })) 62 | app.get('/api/posts/:id', utils.makeSafe(function (req, res, next) { 63 | return store.find('post', req.params.id, { with: ['comment'] }).then(res.send.bind(res)) 64 | })) 65 | 66 | app.get('/auth/github', middleware.passport.authenticate('github')) 67 | app.get('/auth/github/callback', middleware.passport.authenticate('github', { failureRedirect: '/login' }), function (req, res) { 68 | res.redirect('/') 69 | }) 70 | app.post('/api/logout', function (req, res) { 71 | req.logout() 72 | res.redirect('/') 73 | }) 74 | 75 | // Serve index.html if no other route was matched 76 | app.get('*', middleware.indexRoute) 77 | 78 | // Generic catch-all error handler 79 | app.use(middleware.errorHandler) 80 | 81 | // Start the Express server 82 | const server = app.listen(process.env.PORT || 3000, '0.0.0.0', function () { 83 | const address = server.address().address 84 | const port = server.address().port 85 | console.log('App listening at http://%s:%s', address, port) 86 | console.log('Press Ctrl+C to quit.') 87 | }) 88 | -------------------------------------------------------------------------------- /server/src/middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const passport = require('passport') 4 | const GitHubStrategy = require('passport-github').Strategy 5 | const nconf = require('nconf') 6 | const store = require('./store').store 7 | 8 | passport.serializeUser(function (user, done) { done(null, user) }) 9 | passport.deserializeUser(function (obj, done) { done(null, obj) }) 10 | passport.use(new GitHubStrategy({ 11 | clientID: nconf.get('GITHUB_CLIENT_ID'), 12 | clientSecret: nconf.get('GITHUB_CLIENT_SECRET'), 13 | callbackURL: nconf.get('GITHUB_CALLBACK_URL') 14 | }, function (accessToken, refreshToken, profile, done) { 15 | store.findAll('user', { 16 | github_id: profile.id 17 | }).then(function (users) { 18 | if (users.length) { 19 | return users[0] 20 | } else { 21 | return store.create('user', { 22 | username: profile.username, 23 | avatar_url: profile._json.avatar_url, 24 | github_id: profile.id, 25 | name: profile.displayName 26 | }) 27 | } 28 | }).then(function (user) { 29 | return done(null, user) 30 | }).catch(done) 31 | } 32 | )) 33 | 34 | exports.passport = passport 35 | 36 | /** 37 | * TODO 38 | */ 39 | exports.indexRoute = function (req, res) { 40 | return res.sendFile('index.html', { 41 | root: `${__dirname}/../../client/public` 42 | }) 43 | } 44 | 45 | /** 46 | * TODO 47 | */ 48 | exports.queryRewrite = function (req, res, next) { 49 | try { 50 | req.query.jsdataOptions || (req.query.jsdataOptions = {}) 51 | if (req.query.with) { 52 | req.query.jsdataOptions.with = req.query.with 53 | req.query.with = undefined 54 | } 55 | if (req.query.where) { 56 | req.query.where = JSON.parse(req.query.where) 57 | } 58 | if (req.query.orderBy || req.query.sort) { 59 | const orderBy = req.query.orderBy || req.query.sort 60 | if (orderBy.length) { 61 | req.query.orderBy = orderBy.map(function (clause) { 62 | if (typeof clause === 'string') { 63 | return JSON.parse(clause) 64 | } 65 | return clause 66 | }) 67 | req.query.sort = undefined 68 | } 69 | } 70 | next() 71 | } catch (err) { 72 | next(err) 73 | } 74 | } 75 | 76 | /** 77 | * TODO 78 | */ 79 | exports.errorHandler = function (err, req, res, next) { 80 | let responder 81 | if (err && req) { 82 | if (res && next) { 83 | // Normally you would be using some logging library 84 | console.log(err) 85 | } 86 | } 87 | if (typeof req.send === 'function') { 88 | responder = req 89 | } else if (typeof res.send === 'function') { 90 | responder = res 91 | } 92 | if (err instanceof Error) { 93 | responder.status(500).send(err.message) 94 | } else if (typeof err === 'number') { 95 | responder.status(err) 96 | } else if (typeof err === 'object') { 97 | responder.status(400).send(err) 98 | } else { 99 | responder.status(500) 100 | } 101 | responder.end() 102 | } 103 | -------------------------------------------------------------------------------- /server/src/store.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Container is mostly recommended for use in Node.js 4 | const Container = require('js-data').Container 5 | const Schema = require('js-data').Schema 6 | const nconf = require('nconf') 7 | const schemas = require('../../_shared/schemas')(Schema) 8 | const relations = require('../../_shared/relations') 9 | const adapterName = nconf.get('ADAPTER') 10 | 11 | const adapter = exports.adapter = require('./adapters')(adapterName).adapter 12 | const store = exports.store = new Container({ 13 | mapperDefaults: { 14 | beforeCreate (props) { 15 | props.created_at = new Date() 16 | props.updated_at = new Date() 17 | }, 18 | beforeUpdate (props) { 19 | props.updated_at = new Date() 20 | } 21 | } 22 | }) 23 | 24 | store.registerAdapter(adapterName, adapter, { default: true }) 25 | 26 | // The Comment Resource 27 | store.defineMapper('user', { 28 | // Our table names use plural form 29 | table: 'users', 30 | schema: schemas.user, 31 | relations: relations.user 32 | }) 33 | 34 | // The Post Resource 35 | store.defineMapper('post', { 36 | // Our table names use plural form 37 | table: 'posts', 38 | schema: schemas.post, 39 | relations: relations.post 40 | }) 41 | 42 | // The Comment Resource 43 | store.defineMapper('comment', { 44 | // Our table names use plural form 45 | table: 'comments', 46 | schema: schemas.comment, 47 | relations: relations.comment 48 | }) 49 | -------------------------------------------------------------------------------- /server/src/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Promise = require('bluebird') 4 | 5 | /** 6 | * This little generator simply saves us a lot of boilerplate code. 7 | * 8 | * @param {Function} method Method to wrap and make safe. 9 | * @return {Function} Wrapped method. 10 | */ 11 | exports.makeSafe = function (method) { 12 | return function (req, res, next) { 13 | return Promise.try(function () { 14 | return method(req, res, next) 15 | }).catch(next) 16 | } 17 | } 18 | --------------------------------------------------------------------------------