Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/patrixr/pocket-cms

☁️ A pocket sized CMS written for nodejs
https://github.com/patrixr/pocket-cms

backend cms express hacktoberfest headless-cms node nosql schema

Last synced: about 2 months ago
JSON representation

☁️ A pocket sized CMS written for nodejs

Awesome Lists containing this project

README

        

# Pocket CMS

![](https://forthebadge.com/images/badges/uses-js.svg)
![](https://forthebadge.com/images/badges/made-with-vue.svg)
![](https://travis-ci.org/patrixr/pocket-cms.svg?branch=master)

### A pocket sized CMS written for Node and Express.

Designed as a non-intrusive middleware. Pocket offers :

* Automatic API generation
* Schema validation ([pocket schema](https://github.com/patrixr/pocket-schema))
* User and session management
* Admin panel
* Multi-db support (Currently Mongo and Nedb)
* File uploads
* Server stats

Features planned:

* Logs
* API Keys
* Plugin support

![](http://g.recordit.co/nCibS69Xzw.gif)

**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*

- [Getting started](#getting-started)
- [Configuration](#configuration)
- [Datastores](#datastores)
- [Mongo](#mongo)
- [Disk](#disk)
- [Filestores](#filestores)
- [Disk](#disk-1)
- [Using the CMS](#using-the-cms)
- [Resources](#resources)
- [Methods](#methods)
- [Creating a record](#creating-a-record)
- [Updating a record](#updating-a-record)
- [Updating multiple records](#updating-multiple-records)
- [Reading a single record](#reading-a-single-record)
- [Reading multiple records](#reading-multiple-records)
- [Removing a record](#removing-a-record)
- [Removing multiple records](#removing-multiple-records)
- [Attaching a file to a record](#attaching-a-file-to-a-record)
- [Deleting an attachment](#deleting-an-attachment)
- [Reading an attachment](#reading-an-attachment)
- [Schema](#schema)
- [CMS properties](#cms-properties)
- [Indexes](#indexes)
- [Hooks](#hooks)
- [Users](#users)
- [Groups](#groups)
- [REST API](#rest-api)
- [Authentication](#authentication)
- [Resource management](#resource-management)
- [ACL](#acl)
- [Admins](#admins)
- [Schema access configuration](#schema-access-configuration)
- [Group access configuration](#group-access-configuration)

## Getting started

Firstly, install `pocket-cms` as a dependency of your project.

```bash
npm install --save pocket-cms
```

In your Node project, hook pocket to your express server :

```javascript
var express = require("express");
var Pocket = require("pocket-cms");

var app = express();
var cms = new Pocket();

app.use(cms.middleware());

app.listen(3000, () => { console.log('Server running'); })
```
Done ! A pocket cms is now running in your server.

## Configuration

Pocket takes an optional configuration object during construction.

e.g

```javascript
new Pocket({
datastore: {
adapter: 'mongo',
options: {
dbName: 'pocket_dev',
url: 'localhost:27017'
}
}
})
```

The following options are available:

| Key | Description | Type | Default value |
| ------------- | ------------- | ------------- | ------------- |
| **session** | Authentication configuration |||
| session.secret | JWT secret | String | random |
| session.expiresIn | Session expiry time in seconds | Number | 60 days |
| **datastore** | Database configuration |||
| datastore.adapter | Available options : mongo, disk | String | disk |
| datastore.options | Datastore specific options | Object | |
| **filestore** | File upload configuration |||
| filestore.adapter | Available options: disk | String | disk |
| filestore.options | Filestore specific options | Object | |

### Datastores

Currently the following stores are available :

#### Mongo

The mongodb adapter **requires** the following options :

| Key | Description | Type |
| ------------- | ------------- | ------------- |
| **dbName** | Name of the mongo database | String |
| **url** | Database url (e.g `user:password@localhost:27017`) | String |

#### Disk

The disk adapter supports the following options :

| Key | Description | Type | Defaults |
| ------------- | ------------- | ------------- | ------------- |
| **dataFolder** | Folder in which the data will be stored | String | `${POCKET_HOME}/${ENV}_db` |

### Filestores

Currently Pocket only supports files saved on disk.
S3 support is on the roadmap

#### Disk

| Key | Description | Type | Defaults |
| ------------- | ------------- | ------------- | ------------- |
| **uploadFolder** | Folder in which the files will be stored | String | `${POCKET_HOME}/${ENV}_db/uploads` |

## Using the CMS

### Resources

Pocket uses the generic term `Resource` to define a data type within the CMS.
Each resource has :

* A schema
* Enables validation of posted data
* Allows the admin panel to generate a form to edit the records
* An automatically generated rest endpoint

Resources are created using the following CMS api :

```javascript
let cms = new Pocket();

cms.resource('cars', schema);
```

Resources are retrieved using the following CMS api :

```javascript
const cars = cms.resource('cars');

await cars.find({});
```

#### Methods

##### Creating a record

`await resource.create(payload, opts = {})` will create a record matching the payload parameter. Options :

* `skipValidation` allows the record to be created without being validated against its schema. *Use with care*
* `userId` will set the `_createdBy` property of a record

##### Updating a record

`await resource.mergeOne(id, payload, opts = {})` will update the record specified by `id` by overriding the properties set in `payload`. Options :

* `skipValidation` allows the record to be created without being validated against its schema. *Use with care*

##### Updating multiple records

`await resource.update(query, operations, options = {})` will update records specified by `query` with the `operations` formatted in a mongo like syntax. Options :

* `multi` will enable the update to run on multiple records. Defaults to true.

##### Reading a single record

`await resource.get(id)` will return the record specified by `id`

##### Reading multiple records

`await resource.find(query = {}, opts = {})` will return records that match the `query` argument. Options :

* `pageSize` and `page` will allow to retrieve paginated records

##### Removing a record

`await resource.removeOne(id)` will delete the record specified by `id`

##### Removing multiple records

`await resource.remove(query, opts = {})` will remove all elements that match the `query` argument. Options :

* `multi` will allow multiple records to be removed if set to true. Otherwise only one of them will be deleted

##### Attaching a file to a record

`await resource.attach(recordId, fileName, file)` will save the file specified by the `file` argument in the file store, add add it to the record's `_attachments` list. `file` can either be a **String** pointing to the file on disk or a **Stream**

##### Deleting an attachment

`await resource.deleteAttachment(recordId, attachmentId)` will delete the file from the file store and remove the attachment from its record

##### Reading an attachment

`await resource.readAttachment(attachmentId)` will return a node stream of the file

### Schema

Pocket exposes a `Schema` class which can be used to create a resource's schema.
More details can be found on the [Pocket Schema](https://github.com/patrixr/pocket-schema) repo.

e.g

```javascript
const Pocket = require('pocket-cms');
const { Schema } = Pocket;

const carSchema = new Schema({
additionalProperties: false,
fields: {
name: {
type: 'string',
index: {
unique: true
}
}
brand: {
type: 'text',
maxLength: 64
},
noOfWheels: {
type: 'number',
required: true,
},
color: {
type: 'select',
options: ['red', 'yellow', 'magenta']
},
tags: {
type: 'array',
items: { type: 'string' }
}
}
});

```

The following types are available :

* `any`

* `array|list` - options:
* `items?` A field definition of the expected array items

* `checkbox|boolean`

* `date` - options:
* `format?` The expected date format (defaults to YYYY-MM-DD)

* `datetime`

* `email` - options:
* `match?` A regular expression to match the email against

* `map` - options:
* `items?` A field definition of the expected map items

* `multiselect` - options:
* `options` List or options to select from. An async function can also be passed

* `number` - options:
* `min?` Minimum allowed value
* `max?` Maximum allowed value

* `object|json` - options:
* `schema?` Schema used to validate the object against

* `password` - options:
* `minLength?` The minimum length of the password

* `select|enum` - options:
* `options` List or options to select from. An async function can also be passed

* `text|string` - options:
* `minLength?` The minimum length of the string
* `maxLength?` The maximum length of the string
* `match?` A regular expression to match the string against

* `time`

* `timestamp`

#### CMS properties

When records are created/updated the CMS automatically adds and keeps track of a number or *private* properties which **cannot** be updated manually. All those private properties start by underscore `_`.

Currently those are:

* `_id`
* `_createdAt`
* `_updatedAt`
* `_createdBy`
* `_attachments` the list of attachments

#### Indexes

To mark a field as a database index, its schema field supports the `index` parameter. Which can either be :

* A `true|false` value
* An object with the following properties :
* **unique** `true|false`

Example :

```javascript
const Pocket = require('pocket-cms');
const { Schema } = Pocket;

const person = new Schema({
fields: {
name: {
type: 'string',
index: {
unique: true
}
}
});
```

#### Hooks

Pre and Post hooks can be added to a schema which allow adding extra functionality and ACL to the CMS.

Available api :

* `schema.before(action, method)`
* `schema.after(action, method)`

Available methods

* `create`
* `remove`
* `update`
* `save`
* `validate`
* `read`

Example usage

```javascript
const Pocket = require('pocket-cms');
const { Schema } = Pocket;

const postSchema = new Schema({
fields: {
message: {
type: 'string',
}
}
})
.before('save', async ({ record }, ctx) => {
const { user } = ctx;

if (await userCheck(user, record) === false) {
throw 'User should not save this record';
}
});

const cms = new Pocket();

pocket.resource('posts', postSchema);
```

### Users

The Pocket CMS class exposes a user manager that can be used to create/remove and authenticate users.

e.g

```javascript
const Pocket = require('pocket-cms');

const cms = new Pocket();

// Creating a user
await pocket.users.create('username', 'password', [ 'users' ]);

// Authenticating a user
const user = await pocket.users.auth('username', 'password');

// Extracting a user from the JWT auth token
const user = await pocket.users.fromJWT(token)
```

The underlying resource is named `_users`

#### Groups

By default the following groups are created :

* `admins`
* `users`

Groups can be added by using the underlying resource `_groups`

### REST API

For each resource created, a generic rest api is automatically created for it.

Here's a rundown of the different endpoints

#### Authentication

* `POST /users/signup` to create a user. The following JSON body is expected
* `username`
* `password`
* `groups` (defaults to `['users']`)

* `POST /users/login` to log in a user.
**Important**: This endpoint will return a Java Web Token, which should be included into following requests -> `Authorization: Bearer `. The following JSON body is expected
* `username`
* `password`

* `POST /users/logout` to logout out. **NOTE:** As authentication is done via JWT, this endpoint doesn't actually do anything. It exists as a placeholder for future additions (hooks/logs/etc)

* `GET /users/status` to retrieve the user and status of an existing JWT Token

#### Resource management

* `GET /rest/{resource}` lists records for the given resource. Available options :
* `pageSize` - The number of records to return per page
* `page` - The page to return

* `GET /rest/{resource}/{id}` returns a single record specified by `id`

* `POST /rest/{resource}` creates a record of the `resource` type

* `PUT /rest/{resource}/{id}` updates the record specified by `id` of the `resource` type

* `DELETE /rest/{resource}/{id} deletes the record specified by `id`

* `POST /rest/{resource}/{id}/attachments` uploads a file and a attach it to the record specified by `id`

* `GET /rest/{resource}/{id}/attachments/{attachmentId}` downloads the attachment of a record

* `DELETE /rest/{resource}/{id}/attachments/{attachmentId}` deletes the attachment of a record

The resource key of the endpoints listed above can all be prefixed with a user id to filter on records createdBy that user.

e.g

`GET /rest/users/:userId/{resource}/{id}` will return only records created by the user specified by `userId`

#### ACL

There are multiple rules and ways to control the access of resources by users.

We either `allow` or `deny` actions on certain resources.
The following actions exist:

* read
* create
* update
* remove

##### Admins

Users from the `admins` group are whitelisted and have permission to make any change to any resources.

Private CMS resources (prefixed with `_`) cannot be modified by any other group. Currently those are :

* `_users`
* `_groups`

##### Schema access configuration

A entire resource can be configured to only be accessible to a certain set of groups.

That is done on the schema level with the following 2 methods :
* `schema.allow(group, actions[])`
* `deny(group, actions[])`

**Note:** A wildcard `*` can be used as a group name to represent all of them

Example :

```javascript
const Pocket = require('pocket-cms');
const { Schema } = Pocket;

const postSchema = new Schema({
fields: {
message: {
type: 'string'
}
}
})
.allow('users', [ 'read' ])
.allow('moderators', [ 'read', 'create', 'update', 'delete' ])
```

##### Group access configuration

A group can be given access to a resource through its `permissions` field.

e.g

```javascript

pocket.resource('_groups').create({
name: 'moderators',
permissions: {
'*': ['read'],
'posts': ['read', 'create', 'update', 'delete' ]
}
});
```