├── .gitignore
├── README.md
├── actions
└── todos.js
├── components
├── Footer.js
├── Header.js
├── MainSection.js
├── SyncStatus.js
├── TodoItem.js
└── TodoTextInput.js
├── constants
├── ActionTypes.js
└── TodoFilters.js
├── containers
└── App.js
├── index.html
├── index.js
├── package.json
├── reducers
├── index.js
├── syncState.js
└── todos.js
├── server.js
├── store
└── configureStore.js
├── web
└── .gitignore
├── webpack.config.js
└── websocket-server.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | coverage
4 | todos-server
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # pouch-websocket-sync-example
2 |
3 | Example "Todo-MVC" application of using [`pouch-websocket-sync`](https://github.com/pgte/pouch-websocket-sync#readme) together with React and Redux, keeping local database in sync with remote.
4 |
5 | [demo video](http://www.youtube.com/watch?v=8jOF23dfvl4)
6 |
7 | ## Pre-requisites
8 |
9 | You must have [Node.js](https://nodejs.org/en/) installed.
10 |
11 | ## Download
12 |
13 | Clone this repo:
14 |
15 | ```
16 | $ git clone git@github.com:pgte/pouch-websocket-sync-example.git
17 | $ cd pouch-websocket-sync-example
18 | ```
19 |
20 | ## Install dependencies:
21 |
22 | ```
23 | $ npm install
24 | ```
25 |
26 | ## Start
27 |
28 | Start web server:
29 |
30 | ```
31 | $ npm start
32 | ```
33 |
34 | Start websocket server:
35 |
36 | ```
37 | $ node websocket-server
38 | ```
39 |
40 | Open [http://localhost:3000](http://localhost:3000) in your browsers.
41 |
42 | ## License
43 |
44 | ISC
--------------------------------------------------------------------------------
/actions/todos.js:
--------------------------------------------------------------------------------
1 | import * as types from '../constants/ActionTypes'
2 |
3 | export function addTodo(text) {
4 | return { type: types.ADD_TODO, text }
5 | }
6 |
7 | export function deleteTodo(id) {
8 | return { type: types.DELETE_TODO, id }
9 | }
10 |
11 | export function editTodo(id, text) {
12 | return { type: types.EDIT_TODO, id, text }
13 | }
14 |
15 | export function completeTodo(id) {
16 | return { type: types.COMPLETE_TODO, id }
17 | }
18 |
19 | export function completeAll() {
20 | return { type: types.COMPLETE_ALL }
21 | }
22 |
23 | export function clearCompleted() {
24 | return { type: types.CLEAR_COMPLETED }
25 | }
26 |
--------------------------------------------------------------------------------
/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react'
2 | import classnames from 'classnames'
3 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters'
4 |
5 | const FILTER_TITLES = {
6 | [SHOW_ALL]: 'All',
7 | [SHOW_ACTIVE]: 'Active',
8 | [SHOW_COMPLETED]: 'Completed'
9 | }
10 |
11 | class Footer extends Component {
12 | renderTodoCount() {
13 | const { activeCount } = this.props
14 | const itemWord = activeCount === 1 ? 'item' : 'items'
15 |
16 | return (
17 |
18 | {activeCount || 'No'} {itemWord} left
19 |
20 | )
21 | }
22 |
23 | renderFilterLink(filter) {
24 | const title = FILTER_TITLES[filter]
25 | const { filter: selectedFilter, onShow } = this.props
26 |
27 | return (
28 | onShow(filter)}>
31 | {title}
32 |
33 | )
34 | }
35 |
36 | renderClearButton() {
37 | const { completedCount, onClearCompleted } = this.props
38 | if (completedCount > 0) {
39 | return (
40 |
44 | )
45 | }
46 | }
47 |
48 | render() {
49 | return (
50 |
61 | )
62 | }
63 | }
64 |
65 | Footer.propTypes = {
66 | completedCount: PropTypes.number.isRequired,
67 | activeCount: PropTypes.number.isRequired,
68 | filter: PropTypes.string.isRequired,
69 | onClearCompleted: PropTypes.func.isRequired,
70 | onShow: PropTypes.func.isRequired
71 | }
72 |
73 | export default Footer
74 |
--------------------------------------------------------------------------------
/components/Header.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react'
2 | import TodoTextInput from './TodoTextInput'
3 |
4 | class Header extends Component {
5 | handleSave(text) {
6 | if (text.length !== 0) {
7 | this.props.addTodo(text)
8 | }
9 | }
10 |
11 | render() {
12 | return (
13 |
19 | )
20 | }
21 | }
22 |
23 | Header.propTypes = {
24 | addTodo: PropTypes.func.isRequired
25 | }
26 |
27 | export default Header
28 |
--------------------------------------------------------------------------------
/components/MainSection.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import TodoItem from './TodoItem'
3 | import Footer from './Footer'
4 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters'
5 |
6 | const TODO_FILTERS = {
7 | [SHOW_ALL]: () => true,
8 | [SHOW_ACTIVE]: todo => !todo.completed,
9 | [SHOW_COMPLETED]: todo => todo.completed
10 | }
11 |
12 | class MainSection extends Component {
13 | constructor(props, context) {
14 | super(props, context)
15 | this.state = { filter: SHOW_ALL }
16 | }
17 |
18 | handleClearCompleted() {
19 | const atLeastOneCompleted = this.props.todos.some(todo => todo.completed)
20 | if (atLeastOneCompleted) {
21 | this.props.actions.clearCompleted()
22 | }
23 | }
24 |
25 | handleShow(filter) {
26 | this.setState({ filter })
27 | }
28 |
29 | renderToggleAll(completedCount) {
30 | const { todos, actions } = this.props
31 | if (todos.length > 0) {
32 | return (
33 |
37 | )
38 | }
39 | }
40 |
41 | renderFooter(completedCount) {
42 | const { todos } = this.props
43 | const { filter } = this.state
44 | const activeCount = todos.length - completedCount
45 |
46 | if (todos.length) {
47 | return (
48 |
53 | )
54 | }
55 | }
56 |
57 | render() {
58 | const { todos, actions } = this.props
59 | const { filter } = this.state
60 |
61 | const filteredTodos = todos.filter(TODO_FILTERS[filter])
62 | const completedCount = todos.reduce((count, todo) =>
63 | todo.completed ? count + 1 : count,
64 | 0
65 | )
66 |
67 | return (
68 |
69 | {this.renderToggleAll(completedCount)}
70 |
71 | {filteredTodos.map(todo =>
72 |
73 | )}
74 |
75 | {this.renderFooter(completedCount)}
76 |
77 | )
78 | }
79 | }
80 |
81 | MainSection.propTypes = {
82 | todos: PropTypes.array.isRequired,
83 | actions: PropTypes.object.isRequired
84 | }
85 |
86 | export default MainSection
87 |
--------------------------------------------------------------------------------
/components/SyncStatus.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react'
2 | import classnames from 'classnames'
3 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters'
4 |
5 | class SyncStatus extends Component {
6 |
7 | render() {
8 | const { text } = this.props.status
9 | return (
10 |
11 | syncStatus: {text}
12 |
13 | )
14 | }
15 | }
16 |
17 | export default SyncStatus
18 |
--------------------------------------------------------------------------------
/components/TodoItem.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import classnames from 'classnames'
3 | import TodoTextInput from './TodoTextInput'
4 |
5 | class TodoItem extends Component {
6 | constructor(props, context) {
7 | super(props, context)
8 | this.state = {
9 | editing: false
10 | }
11 | }
12 |
13 | handleDoubleClick() {
14 | this.setState({ editing: true })
15 | }
16 |
17 | handleSave(id, text) {
18 | if (text.length === 0) {
19 | this.props.deleteTodo(id)
20 | } else {
21 | this.props.editTodo(id, text)
22 | }
23 | this.setState({ editing: false })
24 | }
25 |
26 | render() {
27 | const { todo, completeTodo, deleteTodo } = this.props
28 |
29 | let element
30 | if (this.state.editing) {
31 | element = (
32 | this.handleSave(todo._id, text)} />
35 | )
36 | } else {
37 | element = (
38 |
39 | completeTodo(todo._id)} />
43 |
46 |
49 | )
50 | }
51 |
52 | return (
53 |
57 | {element}
58 |
59 | )
60 | }
61 | }
62 |
63 | TodoItem.propTypes = {
64 | todo: PropTypes.object.isRequired,
65 | editTodo: PropTypes.func.isRequired,
66 | deleteTodo: PropTypes.func.isRequired,
67 | completeTodo: PropTypes.func.isRequired
68 | }
69 |
70 | export default TodoItem
71 |
--------------------------------------------------------------------------------
/components/TodoTextInput.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import classnames from 'classnames'
3 |
4 | class TodoTextInput extends Component {
5 | constructor(props, context) {
6 | super(props, context)
7 | this.state = {
8 | text: this.props.text || ''
9 | }
10 | }
11 |
12 | handleSubmit(e) {
13 | const text = e.target.value.trim()
14 | if (e.which === 13) {
15 | this.props.onSave(text)
16 | if (this.props.newTodo) {
17 | this.setState({ text: '' })
18 | }
19 | }
20 | }
21 |
22 | handleChange(e) {
23 | this.setState({ text: e.target.value })
24 | }
25 |
26 | handleBlur(e) {
27 | if (!this.props.newTodo) {
28 | this.props.onSave(e.target.value)
29 | }
30 | }
31 |
32 | render() {
33 | return (
34 |
46 | )
47 | }
48 | }
49 |
50 | TodoTextInput.propTypes = {
51 | onSave: PropTypes.func.isRequired,
52 | text: PropTypes.string,
53 | placeholder: PropTypes.string,
54 | editing: PropTypes.bool,
55 | newTodo: PropTypes.bool
56 | }
57 |
58 | export default TodoTextInput
59 |
--------------------------------------------------------------------------------
/constants/ActionTypes.js:
--------------------------------------------------------------------------------
1 | export const ERROR = 'ERROR'
2 | export const ADD_TODO = 'ADD_TODO'
3 | export const INSERT_TODO = 'INSERT_TODO'
4 | export const DELETE_TODO = 'DELETE_TODO'
5 | export const EDIT_TODO = 'EDIT_TODO'
6 | export const UPDATE_TODO = 'UPDATE_TODO'
7 | export const COMPLETE_TODO = 'COMPLETE_TODO'
8 | export const COMPLETE_ALL = 'COMPLETE_ALL'
9 | export const CLEAR_COMPLETED = 'CLEAR_COMPLETED'
10 | export const SET_SYNC_STATE = 'SET_SYNC_STATE'
11 |
--------------------------------------------------------------------------------
/constants/TodoFilters.js:
--------------------------------------------------------------------------------
1 | export const SHOW_ALL = 'show_all'
2 | export const SHOW_COMPLETED = 'show_completed'
3 | export const SHOW_ACTIVE = 'show_active'
4 |
--------------------------------------------------------------------------------
/containers/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import { bindActionCreators } from 'redux'
3 | import { connect } from 'react-redux'
4 | import Header from '../components/Header'
5 | import MainSection from '../components/MainSection'
6 | import SyncStatus from '../components/SyncStatus'
7 | import * as TodoActions from '../actions/todos'
8 |
9 | class App extends Component {
10 | render() {
11 | const { todos, actions, syncState } = this.props
12 | return (
13 |
14 |
15 |
16 |
17 |
18 | )
19 | }
20 | }
21 |
22 | App.propTypes = {
23 | todos: PropTypes.array.isRequired,
24 | actions: PropTypes.object.isRequired
25 | }
26 |
27 | function mapStateToProps(state) {
28 | return {
29 | todos: state.todos,
30 | syncState: state.syncState,
31 | }
32 | }
33 |
34 | function mapDispatchToProps(dispatch) {
35 | return {
36 | actions: bindActionCreators(TodoActions, dispatch)
37 | }
38 | }
39 |
40 | export default connect(
41 | mapStateToProps,
42 | mapDispatchToProps
43 | )(App)
44 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Redux TodoMVC example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import 'babel-core/polyfill'
2 | import React from 'react'
3 | import { render } from 'react-dom'
4 | import { Provider } from 'react-redux'
5 | import App from './containers/App'
6 | import configureStore from './store/configureStore'
7 | import 'todomvc-app-css/index.css'
8 | import PouchDB from 'pouchdb'
9 |
10 | const store = configureStore()
11 |
12 | render(
13 |
14 |
15 | ,
16 | document.getElementById('root')
17 | )
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pouch-websocket-sync-example",
3 | "version": "0.1.0",
4 | "description": "Redux, PouchDB and Sync TodoMVC example",
5 | "scripts": {
6 | "start": "node server.js",
7 | "test": "NODE_ENV=test mocha --recursive --compilers js:babel-core/register --require ./test/setup.js",
8 | "test:watch": "npm test -- --watch"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "https://github.com/pgte/pouch-websocket-sync-example.git"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/pgte/pouch-websocket-sync-example/issues"
17 | },
18 | "homepage": "https://github.com/pgte/pouch-websocket-sync-example",
19 | "dependencies": {
20 | "classnames": "^2.1.2",
21 | "deep-equal": "^1.0.1",
22 | "json-path": "^0.1.3",
23 | "pipe-channels": "^0.1.1",
24 | "pouch-redux-middleware": "^0.1.0",
25 | "pouch-websocket-sync": "^0.1.5",
26 | "pouchdb": "^5.1.0",
27 | "react": "^0.14.0",
28 | "react-dom": "^0.14.0",
29 | "react-redux": "^4.0.0",
30 | "reconnect-core": "^1.1.0",
31 | "redux": "^3.0.0",
32 | "xtend": "^4.0.1"
33 | },
34 | "devDependencies": {
35 | "babel-core": "^5.6.18",
36 | "babel-loader": "^5.1.4",
37 | "babel-plugin-react-transform": "^1.1.0",
38 | "expect": "^1.8.0",
39 | "express": "^4.13.3",
40 | "jsdom": "^5.6.1",
41 | "mocha": "^2.2.5",
42 | "node-libs-browser": "^0.5.2",
43 | "raw-loader": "^0.5.1",
44 | "react-addons-test-utils": "^0.14.0",
45 | "react-transform-hmr": "^1.0.0",
46 | "style-loader": "^0.12.3",
47 | "todomvc-app-css": "^2.0.1",
48 | "webpack": "^1.9.11",
49 | "webpack-dev-middleware": "^1.2.0",
50 | "webpack-hot-middleware": "^2.2.0"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import todos from './todos'
3 | import syncState from './syncState'
4 |
5 | export default combineReducers({
6 | todos, syncState
7 | })
8 |
--------------------------------------------------------------------------------
/reducers/syncState.js:
--------------------------------------------------------------------------------
1 | import { SET_SYNC_STATE } from '../constants/ActionTypes'
2 |
3 | const initialState = {
4 | text: 'unknown'
5 | }
6 |
7 | export default function syncState(state = initialState, action) {
8 | switch (action.type) {
9 | case SET_SYNC_STATE:
10 | return { text: action.text }
11 |
12 | default:
13 | return state
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/reducers/todos.js:
--------------------------------------------------------------------------------
1 | import { ADD_TODO, INSERT_TODO, DELETE_TODO, EDIT_TODO, UPDATE_TODO, COMPLETE_TODO, COMPLETE_ALL, CLEAR_COMPLETED } from '../constants/ActionTypes'
2 |
3 | const initialState = []
4 |
5 | export default function todos(state = initialState, action) {
6 | switch (action.type) {
7 | case ADD_TODO:
8 | return [
9 | {
10 | _id: id(),
11 | completed: false,
12 | text: action.text
13 | },
14 | ...state
15 | ]
16 |
17 | case INSERT_TODO:
18 | return [
19 | action.todo,
20 | ...state
21 | ]
22 |
23 | case DELETE_TODO:
24 | return state.filter(todo =>
25 | todo._id !== action.id
26 | )
27 |
28 | case EDIT_TODO:
29 | return state.map(todo =>
30 | todo._id === action.id ?
31 | Object.assign({}, todo, { text: action.text }) :
32 | todo
33 | )
34 |
35 | case UPDATE_TODO:
36 | return state.map(todo =>
37 | todo._id === action.todo._id ?
38 | action.todo :
39 | todo
40 | )
41 |
42 | case COMPLETE_TODO:
43 | return state.map(todo =>
44 | todo._id === action.id ?
45 | Object.assign({}, todo, { completed: !todo.completed }) :
46 | todo
47 | )
48 |
49 | case COMPLETE_ALL:
50 | const areAllMarked = state.every(todo => todo.completed)
51 | return state.map(todo => Object.assign({}, todo, {
52 | completed: !areAllMarked
53 | }))
54 |
55 | case CLEAR_COMPLETED:
56 | return state.filter(todo => todo.completed === false)
57 |
58 | default:
59 | return state
60 | }
61 | }
62 |
63 | function id() {
64 | return Math.random().toString(36).substring(7);
65 | }
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack')
2 | var webpackDevMiddleware = require('webpack-dev-middleware')
3 | var webpackHotMiddleware = require('webpack-hot-middleware')
4 | var config = require('./webpack.config')
5 |
6 | var app = new (require('express'))()
7 | var port = 3000
8 |
9 | var compiler = webpack(config)
10 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }))
11 | app.use(webpackHotMiddleware(compiler))
12 |
13 | app.get("/", function(req, res) {
14 | res.sendFile(__dirname + '/index.html')
15 | })
16 |
17 | app.listen(port, function(error) {
18 | if (error) {
19 | console.error(error)
20 | } else {
21 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port)
22 | }
23 | })
24 |
--------------------------------------------------------------------------------
/store/configureStore.js:
--------------------------------------------------------------------------------
1 | import * as types from '../constants/ActionTypes'
2 | import PouchMiddleware from 'pouch-redux-middleware'
3 | import { createStore, applyMiddleware } from 'redux'
4 | import rootReducer from '../reducers'
5 | import PouchDB from 'pouchdb'
6 | import PouchSync from 'pouch-websocket-sync'
7 |
8 | const syncEvents = ['change', 'paused', 'active', 'denied', 'complete', 'error'];
9 | const clientEvents = ['connect', 'disconnect', 'reconnect'];
10 |
11 | const initialState = {
12 | todos: [],
13 | syncState: {
14 | text: 'unknown'
15 | }
16 | }
17 |
18 | export default function configureStore() {
19 | const db = new PouchDB('todos');
20 |
21 | const syncClient = PouchSync.createClient()
22 |
23 | const sync = syncClient.
24 | connect('ws://localhost:3001').
25 | on('error', function(err) {
26 | console.log(err);
27 | }).
28 | sync(db, {
29 | remoteName: 'todos-server',
30 | })
31 |
32 | syncEvents.forEach(function(event) {
33 | sync.on(event, function() {
34 | store.dispatch({type: types.SET_SYNC_STATE, text: event});
35 | })
36 | })
37 |
38 | clientEvents.forEach(function(event) {
39 | syncClient.on(event, function() {
40 | store.dispatch({type: types.SET_SYNC_STATE, text: event});
41 | })
42 | })
43 |
44 | const pouchMiddleware = PouchMiddleware({
45 | path: '/todos',
46 | db,
47 | actions: {
48 | remove: doc => store.dispatch({type: types.DELETE_TODO, id: doc._id}),
49 | insert: doc => store.dispatch({type: types.INSERT_TODO, todo: doc}),
50 | update: doc => store.dispatch({type: types.UPDATE_TODO, todo: doc}),
51 | }
52 | })
53 | const createStoreWithMiddleware = applyMiddleware(pouchMiddleware)(createStore)
54 | const store = createStoreWithMiddleware(rootReducer, initialState)
55 |
56 | if (module.hot) {
57 | // Enable Webpack hot module replacement for reducers
58 | module.hot.accept('../reducers', () => {
59 | const nextReducer = require('../reducers')
60 | store.replaceReducer(nextReducer)
61 | })
62 | }
63 |
64 | return store
65 | }
66 |
--------------------------------------------------------------------------------
/web/.gitignore:
--------------------------------------------------------------------------------
1 | todos-server
2 | node_modules
3 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var webpack = require('webpack')
3 |
4 | module.exports = {
5 | devtool: 'cheap-module-eval-source-map',
6 | entry: [
7 | 'webpack-hot-middleware/client',
8 | './index'
9 | ],
10 | output: {
11 | path: path.join(__dirname, 'dist'),
12 | filename: 'bundle.js',
13 | publicPath: '/static/'
14 | },
15 | plugins: [
16 | new webpack.optimize.OccurenceOrderPlugin(),
17 | new webpack.HotModuleReplacementPlugin(),
18 | new webpack.NoErrorsPlugin()
19 | ],
20 | module: {
21 | loaders: [{
22 | test: /\.js$/,
23 | loaders: [ 'babel' ],
24 | exclude: /node_modules/,
25 | include: __dirname
26 | }, {
27 | test: /\.css?$/,
28 | loaders: [ 'style', 'raw' ],
29 | include: __dirname
30 | }]
31 | }
32 | }
33 |
34 |
35 | // When inside Redux repo, prefer src to compiled version.
36 | // You can safely delete these lines in your project.
37 | var reduxSrc = path.join(__dirname, '..', '..', 'src')
38 | var reduxNodeModules = path.join(__dirname, '..', '..', 'node_modules')
39 | var fs = require('fs')
40 | if (fs.existsSync(reduxSrc) && fs.existsSync(reduxNodeModules)) {
41 | // Resolve Redux to source
42 | module.exports.resolve = { alias: { 'redux': reduxSrc } }
43 | // Compile Redux from source
44 | module.exports.module.loaders.push({
45 | test: /\.js$/,
46 | loaders: [ 'babel' ],
47 | include: reduxSrc
48 | })
49 | }
50 |
--------------------------------------------------------------------------------
/websocket-server.js:
--------------------------------------------------------------------------------
1 | const http = require('http');
2 | const PouchDB = require('pouchdb');
3 | const PouchSync = require('pouch-websocket-sync');
4 |
5 | const server = http.createServer();
6 | const wss = PouchSync.createServer(server, onRequest);
7 |
8 | wss.on('error', function(err) {
9 | console.error(err.stack);
10 | });
11 |
12 | const db = new PouchDB('todos-server');
13 |
14 | server.listen(3001, function() {
15 | console.log((new Date()) + ' Server is listening on', server.address());
16 | });
17 |
18 | function onRequest(credentials, dbName, callback) {
19 | if (dbName == 'todos-server') {
20 | callback(null, db);
21 | } else {
22 | callback(new Error('database not allowed'));
23 | }
24 | }
25 |
--------------------------------------------------------------------------------