NICK
A NEARLY HEADLESS CMS
Plone Conference 2022, Namur
-
Rob Gietema @robgietema
ABOUT ME
What is Nick?
Why?
Architecture
Getting started
Documentation
Database
Contenttypes
Security
Catalog & Search
Vocabularies
Security
Utility calls
i18n
Logging
Database schema
Roadmap
WHAT WILL WE COVER?
WHAT IS NICK?
Headless CMS
Build with Node.js
RESTfull API compatible with plone.restapi (Volto)
WHY?
Fun to build!
Plone has a great architecture, great way to learn the
internals
Plone has a great Rest API
Started as a proof of concept on Ploneconf 2018 in
Tokyo
Frontend and backend using the same language
ISSUES WITH PLONE
Disclaimer: my opinion
Lots of legacy code
Lot of code to maintain ourself
Deployment
COMPLEX STACK
Python
Zope
Generic Setup (xml)
ZCML
Page templates
REST
Yaml
JSON
cfg
ini
Markdown
Javascript
Webpack
CSS / LESS / SASS
XSLT
Buildout
KSS
Portal Skins
Restricted Python
DTML
ARCHITECTURE
LANGUAGES
Javascript
JSON
Markdown
POSTGRES
Transactional
JSON integration
(text) indexing
I18N
gettext
GETTING STARTED
https://nickcms.org
GETTING STARTED
CREATE DATABASE nick;
CREATE USER nick WITH ENCRYPTED PASSWORD 'nick';
GRANT ALL PRIVILEGES ON DATABASE nick TO nick;
$ yarn bootstrap
$ yarn start
YEOMAN GENERATOR
$ npm install -g yo
$ npm install -g @robgietema/generator-nick
$ yo @robgietema/nick my-project
CREATE DATABASE my-project;
CREATE USER nick WITH ENCRYPTED PASSWORD 'my-project';
GRANT ALL PRIVILEGES ON DATABASE my-project TO my-project;
$ cd my-project
$ yarn bootstrap
$ yarn start
DEMO
http://localhost:3000
ONLINE DEMO
https://demo.nickcms.org
CONTRIBUTE
https://github.com/robgietema/nick
DOCUMENTATION
https://docs.nickcms.org
TESTS (GET.REQ)
GET /events/event-1/@breadcrumbs HTTP/1.1
Accept: application/json
TESTS (GET.RES)
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:8000/events/event-1/@breadcrumbs",
"items": [
{
"@id": "http://localhost:8000/events",
"title": "Events"
},
{
"@id": "http://localhost:8000/events/event-1",
"title": "Event 1"
}
]
TESTS (BREADCRUMBS.MD)
---
nav_order: 14
permalink: /breadcrumbs
---
# Breadcrumbs
Getting the breadcrumbs for the current page:
```
{% include_relative examples/breadcrumbs/get.req %}
```
Example response:
RUNNING TESTS
$ yarn test
RUNNING TESTS
DATABASE
Postgres
Knex.js ( )
Objection.js (
https://knexjs.org
https://vincit.github.io/objection.js/
KNEX.JS
knex.schema.createTable('group', (table) => {
table.string('id').primary();
table.string('title');
table.string('description');
table.string('email').unique();
}
MIGRATIONS
nick
└─ src
└─ migration
└─ 202203200901_permission.js
└─ 202203200902_role.js
└─ 202203200903_group.js
└─ 202203200904_user.js
└─ 202203200905_workflow.js
└─ 202203200906_type.js
└─ 202203200907_document.js
└─ ...
$ yarn migrate
202203200901_PERMISSION.JS
export const up = async (knex) => {
await knex.schema.createTable('permission', (table) => {
table.string('id').primary();
table.string('title');
});
};
export const down = async (knex) => {
await knex.schema.dropTable('permission');
};
OBJECTION.JS (MODELS)
/**
* Permission Model.
* @module models/permission/permission
*/
import { Model } from '../../models';
/**
* A model for Permission.
* @class Permission
* @extends Model
*/
export class Permission extends Model {
// Set relation mappings
static get relationMappings() {
OBJECTION.JS (BASE MODEL)
/**
* Objection Model.
* @module helpers/base-model/base-model
*/
import { mixin, Model as ObjectionModel } from 'objection';
import TableName from 'objection-table-name';
import _, {
difference,
isArray,
isEmpty,
isObject,
isString,
keys,
map,
OBJECTION.JS (COLLECTIONS)
/**
* ActionCollection.
* @module collection/action/action
*/
import { Collection } from '../../collections';
import _, { includes, map, omit } from 'lodash';
/**
* Action Collection
* @class ActionCollection
* @extends Collection
*/
export class ActionCollection extends Collection {
/**
OBJECTION.JS (BASE
COLLECTION)
/**
* Collection.
* @module collection/_collection/_collection
*/
import { map, omitBy } from 'lodash';
/**
* Base collection used to extend collections from.
* @class Collection
*/
export class Collection {
/**
* Construct a Collection.
* @constructs Collection
SEEDS / PROFILES
nick
└─ src
└─ profiles
└─ core
└─ types
└─ file.js
└─ image.js
└─ ...
└─ groups.js
└─ permissions.js
└─ default
└─ users.js
└─ ...
$ yarn seed
TRANSACTIONS
const trx = await Model.startTransaction();
...
// Get user
req.user = await User.fetchById(
getUserId(req),
{
related: '[_roles, _groups._roles]',
},
trx,
);
...
BLOBS
nick
└─ var
└─ blobstorage
└─ 1d2362de-8090-472b-a06a-0e4d23705f3c
└─ 2bd8d8f2-6d01-4f39-a799-a521acd17dbf
└─ 5e178390-2cf6-498b-9bb3-424c1aa4dea3
└─ ...
HELPERS
$ yarn bootstrap
$ yarn reset
CONFIG
nick/src/config.js
export const config = {
connection: {
port: 5432,
host: 'localhost',
database: 'nick',
user: 'nick',
password: 'nick',
},
blobsDir: `${__dirname}/var/blobstorage`,
port: 8000,
secret: 'secret',
clientMaxSize: '64mb',
systemUsers: ['admin', 'anonymous'],
$ yarn create-config
CONTENTTYPES
CONTENTTYPES (SCHEMA
BASED)
nick
└─ src
└─ profiles
└─ core
└─ types
└─ file.json
└─ folder.json
└─ image.json
└─ page.json
└─ site.json
IMAGE.JSON
{
"id": "Image",
"title:i18n": "Image",
"description:i18n": "Images can be referenced in pages or di
"global_allow": true,
"filter_content_types": true,
"allowed_content_types": [],
"schema": {
"fieldsets": [
{
"fields": ["image"],
"id": "default",
"title:i18n": "Default"
}
],
BEHAVIORS (SCHEMA BASED)
nick
└─ src
└─ profiles
└─ core
└─ behaviors
└─ basic.json
└─ blocks.json
└─ categorization.json
└─ dates.json
└─ dublin_core.json
└─ exclude_from_nav.json
└─ ownership.json
└─ short_name.json
└─ versions.json
BEHAVIORS (BASIC.JSON)
{
"id": "basic",
"title:i18n": "Basic metadata",
"description:i18n": "Adds title and description fields.",
"schema": {
"fieldsets": [
{
"fields": ["title", "description"],
"id": "default",
"title:i18n": "Default"
}
],
"properties": {
"description": {
"description:i18n": "A description of this item.",
NESTED BEHAVIORS
{
"id": "dublin_core",
"title:i18n": "Dublin Core metadata",
"description:i18n": "Adds standard metadatafields",
"schema": {
"behaviors": ["basic", "categorization", "ownership"]
}
}
BEHAVIORS (CLASS BASED)
nick
└─ src
└─ behaviors
└─ id_from_title
└─ id_from_title.js
ID_FROM_TITLE.JS
/**
* Id from title behavior.
* @module behaviors/id_from_title/id_from_title
*/
import slugify from 'slugify';
import { uniqueId } from '../../helpers';
/**
* Id from title behavior.
* @constant id_from_title
*/
export const id_from_title = {
/**
INITIAL CONTENT
nick/src/profiles/default/documents/events.event-1.json
{
"uuid": "405ca717-0c68-43a0-88ac-629a82658675",
"type": "Page",
"title": "Event 1",
"owner": "admin",
"workflow_state": "published",
"created": "2022-04-02T20:10:00.000Z",
"modified": "2022-04-02T20:10:00.000Z",
"effective": "2022-04-02T20:10:00.000Z",
"subjects": ["event"],
"blocks": {
"79ba8858-1dd3-4719-b731-5951e32fbf79": {
SHARING
{
"uuid": "80994493-74ca-4b94-9a7c-145a33a6dd80",
"type": "Folder",
"title": "Users",
"owner": "admin",
"workflow_state": "published",
"created": "2022-04-02T20:24:00.000Z",
"modified": "2022-04-02T20:24:00.000Z",
"effective": "2022-04-02T20:24:00.000Z",
"blocks": {
"79ba8858-1dd3-4719-b731-5951e32fbf79": {
"@type": "title"
}
},
"blocks layout": {
HISTORY
{
"uuid": "32215c67-86de-462a-8cc0-eabcd2b39c26",
"type": "Folder",
"title": "News",
"description": "News Items",
"owner": "admin",
"workflow_state": "published",
"created": "2022-04-02T20:22:00.000Z",
"modified": "2022-04-02T20:22:00.000Z",
"effective": "2022-04-02T20:22:00.000Z",
"blocks": {
"79ba8858-1dd3-4719-b731-5951e32fbf79": {
"@type": "title"
}
},
REDIRECTS
{
"purge": true,
"redirects": [{
"path": "/events/event-2",
"document": "79ba8858-1dd3-4719-b731-5951e32fbf79"
}]
}
REST API CALLS
GET
GET /news HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 200 OK
Content-Type: application/json
{
"title": "News",
"blocks": {
"79ba8858-1dd3-4719-b731-5951e32fbf79": {
"@type": "title"
}
},
"description": "News Items",
"blocks_layout": {
"items": [
"79ba8858-1dd3-4719-b731-5951e32fbf79"
]
POST
POST /news HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
Content-Type: application/json
{
"@type": "Page",
"title": "My News Item",
"description": "News Description"
}
HTTP/1.1 201 Created
Content-Type: application/json
{
"title": "My News Item",
"description": "News Description",
"@id": "http://localhost:8000/news/my-news-item",
"@type": "Page",
"id": "my-news-item",
"created": "2022-04-08T16:00:00.000Z",
"modified": "2022-04-08T16:00:00.000Z",
"UID": "a95388f2-e4b3-4292-98aa-62656cbd5b9c",
"is_folderish": true,
"review state": "private",
PATCH
PATCH /news/my-news-item HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
Content-Type: application/json
{
"title": "My New News Item"
}
HTTP/1.1 204 No Content
DELETE
DELETE /news/my-news-item HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 204 No Content
ORDERING
PATCH /news HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
Content-Type: application/json
{
"ordering": {"obj_id": "my-news-item", "delta": "top"}
}
HTTP/1.1 204 No Content
HISTORY
GET /news/@history HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"@id": "http://localhost:8000/news/@history/1",
"action": "Edited",
"actor": {
"@id": "http://localhost:8000/@users/admin",
"fullname": "Admin",
"id": "admin",
"username": "admin"
},
"comments": "Changed title",
GET VERSION
GET /news/@history/0 HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 200 OK
Content-Type: application/json
{
"title": "Old News",
"blocks": {
"79ba8858-1dd3-4719-b731-5951e32fbf79": {
"@type": "title"
}
},
"description": "News Items",
"blocks_layout": {
"items": [
"79ba8858-1dd3-4719-b731-5951e32fbf79"
]
REVERT TO VERSION
PATCH /news/my-news-item/@history HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
Content-Type: application/json
{
"version": 0
}
HTTP/1.1 200 OK
Content-Type: application/json
{
"message": "My News Item has been reverted to revision 0."
}
COPY
POST /news/@copy HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
Content-Type: application/json
{
"source": "/events/event-1"
}
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"source": "http://localhost:8000/events/event-1",
"target": "http://localhost:8000/news/event-1"
}
]
MOVE
POST /news/@move HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
Content-Type: application/json
{
"source": "/events/event-1"
}
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"source": "http://localhost:8000/events/event-1",
"target": "http://localhost:8000/news/event-1"
}
]
COPY MULTIPLE
POST /news/@copy HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
Content-Type: application/json
{
"source": [
"/events",
"/users"
]
}
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"source": "http://localhost:8000/events",
"target": "http://localhost:8000/news/events"
},
{
"source": "http://localhost:8000/users",
"target": "http://localhost:8000/news/users"
}
]
ACTIONS
GET /@actions HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 200 OK
Content-Type: application/json
{
"object": [
{
"id": "view",
"title": "View"
},
{
"id": "edit",
"title": "Edit"
},
{
"id": "folderContents",
CREATE LOCK
POST /news/my-news-item/@lock HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 200 OK
Content-Type: application/json
{
"created": "2022-04-08T16:00:00.000Z",
"creator": "admin",
"creator_name": "Admin",
"creator_url": "http://localhost:8000/@users/admin",
"locked": true,
"stealable": true,
"time": "2022-04-08T16:00:00.000Z",
"timeout": 600,
"token": "a95388f2-e4b3-4292-98aa-62656cbd5b9c"
}
GET LOCK
GET /news/my-news-item/@lock HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 200 OK
Content-Type: application/json
{
"created": "2022-04-08T16:00:00.000Z",
"creator": "admin",
"creator_name": "Admin",
"creator_url": "http://localhost:8000/@users/admin",
"locked": true,
"stealable": true,
"time": "2022-04-08T16:00:00.000Z",
"timeout": 600,
"token": "a95388f2-e4b3-4292-98aa-62656cbd5b9c"
}
UPDATE WITH LOCK
PATCH /news/my-news-item HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
Lock-Token: a95388f2-e4b3-4292-98aa-62656cbd5b9c
Content-Type: application/json
{
"title": "My New News Item"
}
HTTP/1.1 204 No Content
DELETE LOCK
DELETE /news/my-news-item/@lock HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 200 OK
Content-Type: application/json
{
"locked": false,
"stealable": true
}
SECURITY
AUTHENTICATION
POST /@login HTTP/1.1
Accept: application/json
{
"login": "admin",
"password": "admin"
}
HTTP/1.1 200 OK
Content-Type: application/json
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ
}
RENEW
POST /@login-renew HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 200 OK
Content-Type: application/json
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ
}
LOGOUT
POST /@logout HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 204 No Content
PERMISSION SYSTEM
Permissions
Roles (have permissions)
Groups (have roles)
Users (have roles, groups)
Local roles (user/group has a role on an object)
Local role permissions are inherited from the parent
Local role inheritence can be disabled per object
Workflows (have states and transitions)
States (have permissions per role)
Transitions (have permissions)
PERMISSIONS.JSON
{
"purge": true,
"permissions": [
{
"id": "View",
"title:i18n": "View"
},
{
"id": "Add",
"title:i18n": "Add"
},
{
"id": "Login",
"title:i18n": "Login"
},
ROLES.JSON
{
"purge": true,
"roles": [
{
"id": "Anonymous",
"title:i18n": "Anonymous",
"permissions": ["Login", "Register"]
},
{
"id": "Authenticated",
"title:i18n": "Authenticated",
"permissions": ["Logout", "Manage Preferences"]
},
{
"id": "Owner",
USERS.JSON
{
"purge": true,
"users": [
{
"id": "admin",
"password": "admin",
"fullname": "Admin",
"email": "admin@example.com",
"roles": ["Administrator"]
},
{
"id": "anonymous",
"password": "anonymous",
"fullname": "Anonymous",
"email": "anonymous@example.com",
GROUPS.JSON
{
"purge": true,
"groups": [
{
"id": "Administrators",
"title:i18n": "Administrators",
"description:i18n": "",
"email": "",
"roles": ["Administrator"]
}
]
}
WORKFLOWS.JSON
{
"purge": true,
"workflows": [
{
"id": "simple_publication_workflow",
"title:i18n": "Simple Publication Workflow",
"description:i18n": "Simple workflow that is useful for
"json": {
"initial_state": "private",
"states": {
"private": {
"title:i18n": "Private",
"description:i18n": "Can only be seen and edited b
"transitions": ["publish", "submit"],
"permissions": {
REST API CALLS
GET ROLES
GET /@roles HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"@id": "http://localhost:8000/@roles/Anonymous",
"@type": "role",
"id": "Anonymous",
"title": "Anonymous"
},
{
"@id": "http://localhost:8000/@roles/Authenticated",
"@type": "role",
"id": "Authenticated",
"title": "Authenticated"
LIST USERS
GET /@users HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"@id": "http://localhost:8000/@users/admin",
"id": "admin",
"fullname": "Admin",
"email": "admin@example.com",
"roles": [
"Administrator"
],
"groups": []
},
{
LIST WITH A QUERY
GET /@users?query=admin HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"@id": "http://localhost:8000/@users/admin",
"id": "admin",
"fullname": "Admin",
"email": "admin@example.com",
"roles": [
"Administrator"
],
"groups": []
}
]
GET USER
GET /@users/admin HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:8000/@users/admin",
"id": "admin",
"fullname": "Admin",
"email": "admin@example.com",
"roles": [
"Administrator"
],
"groups": []
}
UPDATE USER
PATCH /@users/headlessnick HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
Content-Type: application/json
{
"fullname": "Headless Nick"
}
HTTP/1.1 204 No Content
DELETE USER
DELETE /@users/headlessnick HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 204 No Content
REGISTRATION
POST /@users HTTP/1.1
Accept: application/json
Content-Type: application/json
{
"email": "nearly.headless.nick@example.com",
"fullname": "Nearly Headless Nick",
"username": "headlessnick",
"sendPasswordReset": true
}
HTTP/1.1 201 Created
Content-Type: application/json
{
"@id": "http://localhost:8000/@users/headlessnick",
"id": "headlessnick",
"fullname": "Nearly Headless Nick",
"email": "nearly.headless.nick@example.com",
"roles": [],
"groups": []
}
RESET PASSWORD
POST /@users/headlessnick/reset-password HTTP/1.1
Accept: application/json
HTTP/1.1 200 OK
SET RESET PASSWORD
POST /@users/headlessnick/reset-password HTTP/1.1
Accept: application/json
Content-Type: application/json
{
"reset_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWI
"new_password": "headless"
}
HTTP/1.1 200 OK
LIST GROUPS
GET /@groups HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"@id": "http://localhost:8000/@groups/Administrators",
"description": "",
"email": "",
"groupname": "Administrators",
"id": "Administrators",
"roles": [
"Administrator"
],
"title": "Administrators"
}
GET GROUP
GET /@groups/Administrators HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:8000/@groups/Administrators",
"id": "Administrators",
"title": "Administrators",
"description": "",
"groupname": "Administrators",
"email": "",
"roles": [
"Administrator"
]
}
ADD GROUP
POST /@groups HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
Content-Type: application/json
{
"groupname": "nicks",
"title": "Nicks",
"description": "Nearly Headless Nicks",
"email": "nearly.headless.nicks@example.com",
"roles": [
"Contributor"
]
}
HTTP/1.1 201 Created
Content-Type: application/json
{
"@id": "http://localhost:8000/@groups/nicks",
"id": "nicks",
"groupname": "nicks",
"description": "Nearly Headless Nicks",
"email": "nearly.headless.nicks@example.com",
"roles": [],
"title": "Nicks"
}
UPDATE GROUP
PATCH /@groups/nicks HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
Content-Type: application/json
{
"title": "Headless Nicks"
}
HTTP/1.1 204 No Content
DELETE GROUP
DELETE /@groups/nicks HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 204 No Content
GET WORKFLOW
GET /news/@workflow HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:8000/news/@workflow",
"history": [],
"state": {
"id": "published",
"title": "Published"
},
"transitions": [
{
"@id": "http://localhost:8000/news/@workflow/reject",
"title": "Send back"
},
WORKFLOW TRANSITION
POST /news/my-news-item/@workflow/publish HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 200 OK
Content-Type: application/json
{
"action": "publish",
"actor": "admin",
"comments": "",
"review_state": "published",
"time": "2022-04-08T16:00:00.000Z",
"title": "Published"
}
SHARING
GET /@sharing HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 200 OK
Content-Type: application/json
{
"available_roles": [
{
"id": "Anonymous",
"title": "Anonymous"
},
{
"id": "Authenticated",
"title": "Authenticated"
},
{
"id": "Owner",
ADD SHARING
POST /@sharing HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
Content-Type: application/json
{
"entries": [
{
"id": "Administrators",
"roles": {
"Contributor": true,
"Reader": true
},
"type": "user"
}
HTTP/1.1 204 No Content
CATALOG & SEARCH
CATALOG
Indexes
type (path, uuid, integer, date, text, string,
boolean, string[])
operators
Metadata
name
type
attribute
CATALOG.JSON
{
"indexes": [
{
"name": "path",
"type": "path",
"attr": "path",
"title:i18n": "Location",
"description:i18n": "The location of an item",
"group": "Metadata",
"enabled": true,
"sortable": false,
"operators": {
"string.absolutePath": {
"title:i18n": "Absolute path",
"description:i18n": "Location in the site structure"
QUERYSTRING
GET /@querystring HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:8000/@querystring",
"indexes": {
"created": {
"title": "Creation date",
"description": "The date an item was created",
"group": "Dates",
"enabled": true,
"sortable": true,
"values": {},
"vocabulary": null,
"operations": [
SEARCH
GET /@search?SearchableText=news* HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:8000/@search",
"items": [
{
"@id": "http://localhost:8000/news",
"@type": "Folder",
"title": "News",
"UID": "32215c67-86de-462a-8cc0-eabcd2b39c26",
"path": "/news",
"Description": "News Items",
"Title": "News",
"Subject": null,
BATCHING
GET /@search?b_size=2 HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:8000/@search",
"items": [
{
"@id": "http://localhost:8000/events",
"@type": "Folder",
"title": "Events",
"UID": "1a2123ba-14e8-4910-8e6b-c04a40d72a41",
"path": "/events",
"Description": null,
"Title": "Events",
"Subject": null,
DEPTH
GET /@search?path.depth=1 HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:8000/@search",
"items": [
{
"@id": "http://localhost:8000/events",
"@type": "Folder",
"title": "Events",
"UID": "1a2123ba-14e8-4910-8e6b-c04a40d72a41",
"path": "/events",
"Description": null,
"Title": "Events",
"Subject": null,
SORT
GET /@search?sort_on=effective HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:8000/@search",
"items": [
{
"@id": "http://localhost:8000/",
"@type": "Site",
"title": "Welcome to Nick!",
"UID": "92a80817-f5b7-400d-8f58-b08126f0f09b",
"path": "/",
"Description": "Congratulations! You have successfully i
"Title": "Welcome to Nick!",
"Subject": null,
VOCABULARIES
VOCABULARIES
nick
└─ src
└─ vocabularies
└─ actions
└─ actions.js
└─ behaviors
└─ behaviors.js
└─ groups
└─ groups.js
└─ image-scales
└─ image-scales.js
...
└─ index.js
CREATE VOCABULARY
/**
* Actions vocabulary.
* @module vocabularies/actions/actions
*/
import { Action } from '../../models';
/**
* Returns the acions vocabulary.
* @method actions
* @returns {Array} Array of terms.
*/
export async function actions(req, trx) {
const actions = await Action.fetchAll(
{},
PROFILE VOCABULARIES
nick
└─ src
└─ profiles
└─ core
└─ vocabularies
└─ boolean.json
PROFILE VOCABULARY
{
"id": "boolean",
"title:i18n": "boolean",
"items": [
{ "title:i18n": "Yes", "token": "Yes" },
{ "title:i18n": "No", "token": "No" }
]
}
LIST VOCABULARIES
GET /@vocabularies HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"@id": "http://localhost:8000/@vocabularies/actions",
"title": "actions"
},
{
"@id": "http://localhost:8000/@vocabularies/behaviors",
"title": "behaviors"
},
{
"@id": "http://localhost:8000/@vocabularies/boolean",
"title": "boolean"
GET VOCABULARY
GET /@vocabularies/actions HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:8000/@vocabularies/actions",
"items": [
{
"title": "View",
"token": "view"
},
{
"title": "Edit",
"token": "edit"
},
{
UTILITY CALLS
BREADCRUMBS
GET /events/event-1/@breadcrumbs HTTP/1.1
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:8000/events/event-1/@breadcrumbs",
"items": [
{
"@id": "http://localhost:8000/events",
"title": "Events"
},
{
"@id": "http://localhost:8000/events/event-1",
"title": "Event 1"
}
]
NAVIGATION
GET /news/@navigation HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:8000/news/@navigation",
"items": [
{
"@id": "http://localhost:8000/events",
"@type": "Folder",
"title": "Events",
"UID": "1a2123ba-14e8-4910-8e6b-c04a40d72a41",
"path": "/events",
"Description": null,
"Title": "Events",
"Subject": null,
CONTROLPANELS
nick
└─ src
└─ profiles
└─ core
└─ controlpanels
└─ mail.js
└─ language.js
{
"id": "mail",
"title:i18n": "Mail",
"group": "General",
"schema": {
"fieldsets": [
{
"behavior": "plone",
"fields": [
"host",
"port",
"secure",
"user",
"pass",
CONTROLPANELS
GET /@controlpanels HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"@id": "http://localhost:8000/@controlpanels/language",
"group": "General",
"title": "Language"
},
{
"@id": "http://localhost:8000/@controlpanels/mail",
"group": "General",
"title": "Mail"
}
]
GET CONTROLPANEL
GET /@controlpanels/mail HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:8000/@controlpanels/mail",
"group": "General",
"title": "Mail",
"data": {
"host": "localhost",
"pass": "",
"port": 25,
"user": "",
"debug": true,
"secure": true,
"email from name": "Webmaster",
UPDATE CONTROLPANEL
PATCH /@controlpanels/mail HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
Content-Type: application/json
{
"host": "mail.someserver.com",
"port": 25
}
HTTP/1.1 204 No Content
MAIL
POST /@email-send HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
Content-Type: application/json
{
"name": "John Doe",
"from": "john@doe.com",
"to": "jane@doe.com",
"subject": "Hello!",
"message": "Just want to say hi."
}
HTTP/1.1 204 OK
MAIL WEBMASTER
POST /@email-notification HTTP/1.1
Accept: application/json
Content-Type: application/json
{
"name": "John Doe",
"from": "john@doe.com",
"subject": "Hello!",
"message": "Just want to say hi."
}
HTTP/1.1 204 OK
SYSTEM
GET /@system HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:8000/@system",
"nick_version": "2.8.0",
"node_version": "v16.15.0",
"express_version": "4.18.2",
"objection_version": "3.0.1",
"knex_version": "2.3.0",
"postgres_version": "14.4"
}
DATABASE
GET /@database HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:8000/@database",
"db_name": "nick",
"db_size": "10 MB",
"pool": {
"min": 2,
"max": 10
},
"blob_size": "10 MB"
}
I18N
nick
└─ locales
└─ en
└─ LC_MESSAGES
└─ nick.po
└─ nl
└─ LC_MESSAGES
└─ nick.po
└─ nick.po
└─ en.js
└─ nl.js
I18N
$ yarn i18n
I18N IN JS
import { i18n } from './middleware';
...
app.use(i18n);
...
req.i18n('Transaction error.')
I18N IN JSON
{
"id": "basic",
"title:i18n": "Basic metadata",
"description:i18n": "Adds title and description fields.",
"schema": {
"fieldsets": [
{
"fields": ["title", "description"],
"id": "default",
"title:i18n": "Default"
}
],
"properties": {
"description": {
"description:i18n": "A description of this item.",
LOGGING (LOG4JS)
log.info(`Server listening on port ${config.port}`)
2022-05-30T22:21:06.317 INFO [server.js:12] Server listening o
DATABASE SCHEMA
DATABASE SCHEMA
ROADMAP
Querystring search
100% test coverage (currently 83%)
Multi-Lingual
Email login
Controlpanels
Developer docs
Websockets
QUESTIONS?
slideshare.net/robgietema/nick-ploneconf-2022

Nick: A Nearly Headless CMS

  • 1.
    NICK A NEARLY HEADLESSCMS Plone Conference 2022, Namur - Rob Gietema @robgietema
  • 2.
  • 3.
    What is Nick? Why? Architecture Gettingstarted Documentation Database Contenttypes Security Catalog & Search Vocabularies Security Utility calls i18n Logging Database schema Roadmap WHAT WILL WE COVER?
  • 4.
    WHAT IS NICK? HeadlessCMS Build with Node.js RESTfull API compatible with plone.restapi (Volto)
  • 5.
    WHY? Fun to build! Plonehas a great architecture, great way to learn the internals Plone has a great Rest API Started as a proof of concept on Ploneconf 2018 in Tokyo Frontend and backend using the same language
  • 6.
    ISSUES WITH PLONE Disclaimer:my opinion Lots of legacy code Lot of code to maintain ourself Deployment
  • 7.
    COMPLEX STACK Python Zope Generic Setup(xml) ZCML Page templates REST Yaml JSON cfg ini Markdown Javascript Webpack CSS / LESS / SASS XSLT Buildout KSS Portal Skins Restricted Python DTML
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
    GETTING STARTED CREATE DATABASEnick; CREATE USER nick WITH ENCRYPTED PASSWORD 'nick'; GRANT ALL PRIVILEGES ON DATABASE nick TO nick; $ yarn bootstrap $ yarn start
  • 14.
    YEOMAN GENERATOR $ npminstall -g yo $ npm install -g @robgietema/generator-nick $ yo @robgietema/nick my-project CREATE DATABASE my-project; CREATE USER nick WITH ENCRYPTED PASSWORD 'my-project'; GRANT ALL PRIVILEGES ON DATABASE my-project TO my-project; $ cd my-project $ yarn bootstrap $ yarn start
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
    TESTS (GET.REQ) GET /events/event-1/@breadcrumbsHTTP/1.1 Accept: application/json
  • 20.
    TESTS (GET.RES) HTTP/1.1 200OK Content-Type: application/json { "@id": "http://localhost:8000/events/event-1/@breadcrumbs", "items": [ { "@id": "http://localhost:8000/events", "title": "Events" }, { "@id": "http://localhost:8000/events/event-1", "title": "Event 1" } ]
  • 21.
    TESTS (BREADCRUMBS.MD) --- nav_order: 14 permalink:/breadcrumbs --- # Breadcrumbs Getting the breadcrumbs for the current page: ``` {% include_relative examples/breadcrumbs/get.req %} ``` Example response:
  • 22.
  • 23.
  • 24.
    DATABASE Postgres Knex.js ( ) Objection.js( https://knexjs.org https://vincit.github.io/objection.js/
  • 25.
    KNEX.JS knex.schema.createTable('group', (table) =>{ table.string('id').primary(); table.string('title'); table.string('description'); table.string('email').unique(); }
  • 26.
    MIGRATIONS nick └─ src └─ migration └─202203200901_permission.js └─ 202203200902_role.js └─ 202203200903_group.js └─ 202203200904_user.js └─ 202203200905_workflow.js └─ 202203200906_type.js └─ 202203200907_document.js └─ ... $ yarn migrate
  • 27.
    202203200901_PERMISSION.JS export const up= async (knex) => { await knex.schema.createTable('permission', (table) => { table.string('id').primary(); table.string('title'); }); }; export const down = async (knex) => { await knex.schema.dropTable('permission'); };
  • 28.
    OBJECTION.JS (MODELS) /** * PermissionModel. * @module models/permission/permission */ import { Model } from '../../models'; /** * A model for Permission. * @class Permission * @extends Model */ export class Permission extends Model { // Set relation mappings static get relationMappings() {
  • 29.
    OBJECTION.JS (BASE MODEL) /** *Objection Model. * @module helpers/base-model/base-model */ import { mixin, Model as ObjectionModel } from 'objection'; import TableName from 'objection-table-name'; import _, { difference, isArray, isEmpty, isObject, isString, keys, map,
  • 30.
    OBJECTION.JS (COLLECTIONS) /** * ActionCollection. *@module collection/action/action */ import { Collection } from '../../collections'; import _, { includes, map, omit } from 'lodash'; /** * Action Collection * @class ActionCollection * @extends Collection */ export class ActionCollection extends Collection { /**
  • 31.
    OBJECTION.JS (BASE COLLECTION) /** * Collection. *@module collection/_collection/_collection */ import { map, omitBy } from 'lodash'; /** * Base collection used to extend collections from. * @class Collection */ export class Collection { /** * Construct a Collection. * @constructs Collection
  • 32.
    SEEDS / PROFILES nick └─src └─ profiles └─ core └─ types └─ file.js └─ image.js └─ ... └─ groups.js └─ permissions.js └─ default └─ users.js └─ ... $ yarn seed
  • 33.
    TRANSACTIONS const trx =await Model.startTransaction(); ... // Get user req.user = await User.fetchById( getUserId(req), { related: '[_roles, _groups._roles]', }, trx, ); ...
  • 34.
    BLOBS nick └─ var └─ blobstorage └─1d2362de-8090-472b-a06a-0e4d23705f3c └─ 2bd8d8f2-6d01-4f39-a799-a521acd17dbf └─ 5e178390-2cf6-498b-9bb3-424c1aa4dea3 └─ ...
  • 35.
  • 36.
    CONFIG nick/src/config.js export const config= { connection: { port: 5432, host: 'localhost', database: 'nick', user: 'nick', password: 'nick', }, blobsDir: `${__dirname}/var/blobstorage`, port: 8000, secret: 'secret', clientMaxSize: '64mb', systemUsers: ['admin', 'anonymous'], $ yarn create-config
  • 37.
  • 38.
    CONTENTTYPES (SCHEMA BASED) nick └─ src └─profiles └─ core └─ types └─ file.json └─ folder.json └─ image.json └─ page.json └─ site.json
  • 39.
    IMAGE.JSON { "id": "Image", "title:i18n": "Image", "description:i18n":"Images can be referenced in pages or di "global_allow": true, "filter_content_types": true, "allowed_content_types": [], "schema": { "fieldsets": [ { "fields": ["image"], "id": "default", "title:i18n": "Default" } ],
  • 40.
    BEHAVIORS (SCHEMA BASED) nick └─src └─ profiles └─ core └─ behaviors └─ basic.json └─ blocks.json └─ categorization.json └─ dates.json └─ dublin_core.json └─ exclude_from_nav.json └─ ownership.json └─ short_name.json └─ versions.json
  • 41.
    BEHAVIORS (BASIC.JSON) { "id": "basic", "title:i18n":"Basic metadata", "description:i18n": "Adds title and description fields.", "schema": { "fieldsets": [ { "fields": ["title", "description"], "id": "default", "title:i18n": "Default" } ], "properties": { "description": { "description:i18n": "A description of this item.",
  • 42.
    NESTED BEHAVIORS { "id": "dublin_core", "title:i18n":"Dublin Core metadata", "description:i18n": "Adds standard metadatafields", "schema": { "behaviors": ["basic", "categorization", "ownership"] } }
  • 43.
    BEHAVIORS (CLASS BASED) nick └─src └─ behaviors └─ id_from_title └─ id_from_title.js
  • 44.
    ID_FROM_TITLE.JS /** * Id fromtitle behavior. * @module behaviors/id_from_title/id_from_title */ import slugify from 'slugify'; import { uniqueId } from '../../helpers'; /** * Id from title behavior. * @constant id_from_title */ export const id_from_title = { /**
  • 45.
    INITIAL CONTENT nick/src/profiles/default/documents/events.event-1.json { "uuid": "405ca717-0c68-43a0-88ac-629a82658675", "type":"Page", "title": "Event 1", "owner": "admin", "workflow_state": "published", "created": "2022-04-02T20:10:00.000Z", "modified": "2022-04-02T20:10:00.000Z", "effective": "2022-04-02T20:10:00.000Z", "subjects": ["event"], "blocks": { "79ba8858-1dd3-4719-b731-5951e32fbf79": {
  • 46.
    SHARING { "uuid": "80994493-74ca-4b94-9a7c-145a33a6dd80", "type": "Folder", "title":"Users", "owner": "admin", "workflow_state": "published", "created": "2022-04-02T20:24:00.000Z", "modified": "2022-04-02T20:24:00.000Z", "effective": "2022-04-02T20:24:00.000Z", "blocks": { "79ba8858-1dd3-4719-b731-5951e32fbf79": { "@type": "title" } }, "blocks layout": {
  • 47.
    HISTORY { "uuid": "32215c67-86de-462a-8cc0-eabcd2b39c26", "type": "Folder", "title":"News", "description": "News Items", "owner": "admin", "workflow_state": "published", "created": "2022-04-02T20:22:00.000Z", "modified": "2022-04-02T20:22:00.000Z", "effective": "2022-04-02T20:22:00.000Z", "blocks": { "79ba8858-1dd3-4719-b731-5951e32fbf79": { "@type": "title" } },
  • 48.
    REDIRECTS { "purge": true, "redirects": [{ "path":"/events/event-2", "document": "79ba8858-1dd3-4719-b731-5951e32fbf79" }] }
  • 49.
  • 50.
    GET GET /news HTTP/1.1 Accept:application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 200 OK Content-Type: application/json { "title": "News", "blocks": { "79ba8858-1dd3-4719-b731-5951e32fbf79": { "@type": "title" } }, "description": "News Items", "blocks_layout": { "items": [ "79ba8858-1dd3-4719-b731-5951e32fbf79" ]
  • 51.
    POST POST /news HTTP/1.1 Accept:application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ Content-Type: application/json { "@type": "Page", "title": "My News Item", "description": "News Description" } HTTP/1.1 201 Created Content-Type: application/json { "title": "My News Item", "description": "News Description", "@id": "http://localhost:8000/news/my-news-item", "@type": "Page", "id": "my-news-item", "created": "2022-04-08T16:00:00.000Z",
  • 52.
  • 53.
    PATCH PATCH /news/my-news-item HTTP/1.1 Accept:application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ Content-Type: application/json { "title": "My New News Item" } HTTP/1.1 204 No Content
  • 54.
    DELETE DELETE /news/my-news-item HTTP/1.1 Accept:application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 204 No Content
  • 55.
    ORDERING PATCH /news HTTP/1.1 Accept:application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ Content-Type: application/json { "ordering": {"obj_id": "my-news-item", "delta": "top"} } HTTP/1.1 204 No Content
  • 56.
    HISTORY GET /news/@history HTTP/1.1 Accept:application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 200 OK Content-Type: application/json [ { "@id": "http://localhost:8000/news/@history/1", "action": "Edited", "actor": { "@id": "http://localhost:8000/@users/admin", "fullname": "Admin", "id": "admin", "username": "admin" }, "comments": "Changed title",
  • 57.
    GET VERSION GET /news/@history/0HTTP/1.1 Accept: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 200 OK Content-Type: application/json { "title": "Old News", "blocks": { "79ba8858-1dd3-4719-b731-5951e32fbf79": { "@type": "title" } }, "description": "News Items", "blocks_layout": { "items": [ "79ba8858-1dd3-4719-b731-5951e32fbf79" ]
  • 58.
    REVERT TO VERSION PATCH/news/my-news-item/@history HTTP/1.1 Accept: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ Content-Type: application/json { "version": 0 } HTTP/1.1 200 OK Content-Type: application/json { "message": "My News Item has been reverted to revision 0." }
  • 59.
    COPY POST /news/@copy HTTP/1.1 Accept:application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ Content-Type: application/json { "source": "/events/event-1" } HTTP/1.1 200 OK Content-Type: application/json [ { "source": "http://localhost:8000/events/event-1", "target": "http://localhost:8000/news/event-1" } ]
  • 60.
    MOVE POST /news/@move HTTP/1.1 Accept:application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ Content-Type: application/json { "source": "/events/event-1" } HTTP/1.1 200 OK Content-Type: application/json [ { "source": "http://localhost:8000/events/event-1", "target": "http://localhost:8000/news/event-1" } ]
  • 61.
    COPY MULTIPLE POST /news/@copyHTTP/1.1 Accept: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ Content-Type: application/json { "source": [ "/events", "/users" ] } HTTP/1.1 200 OK Content-Type: application/json [ { "source": "http://localhost:8000/events", "target": "http://localhost:8000/news/events" }, {
  • 62.
  • 63.
    ACTIONS GET /@actions HTTP/1.1 Accept:application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 200 OK Content-Type: application/json { "object": [ { "id": "view", "title": "View" }, { "id": "edit", "title": "Edit" }, { "id": "folderContents",
  • 64.
    CREATE LOCK POST /news/my-news-item/@lockHTTP/1.1 Accept: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 200 OK Content-Type: application/json { "created": "2022-04-08T16:00:00.000Z", "creator": "admin", "creator_name": "Admin", "creator_url": "http://localhost:8000/@users/admin", "locked": true, "stealable": true, "time": "2022-04-08T16:00:00.000Z", "timeout": 600, "token": "a95388f2-e4b3-4292-98aa-62656cbd5b9c" }
  • 65.
    GET LOCK GET /news/my-news-item/@lockHTTP/1.1 Accept: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 200 OK Content-Type: application/json { "created": "2022-04-08T16:00:00.000Z", "creator": "admin", "creator_name": "Admin", "creator_url": "http://localhost:8000/@users/admin", "locked": true, "stealable": true, "time": "2022-04-08T16:00:00.000Z", "timeout": 600, "token": "a95388f2-e4b3-4292-98aa-62656cbd5b9c" }
  • 66.
    UPDATE WITH LOCK PATCH/news/my-news-item HTTP/1.1 Accept: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ Lock-Token: a95388f2-e4b3-4292-98aa-62656cbd5b9c Content-Type: application/json { "title": "My New News Item" } HTTP/1.1 204 No Content
  • 67.
    DELETE LOCK DELETE /news/my-news-item/@lockHTTP/1.1 Accept: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 200 OK Content-Type: application/json { "locked": false, "stealable": true }
  • 68.
  • 69.
    AUTHENTICATION POST /@login HTTP/1.1 Accept:application/json { "login": "admin", "password": "admin" } HTTP/1.1 200 OK Content-Type: application/json { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ }
  • 70.
    RENEW POST /@login-renew HTTP/1.1 Accept:application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 200 OK Content-Type: application/json { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ }
  • 71.
    LOGOUT POST /@logout HTTP/1.1 Accept:application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 204 No Content
  • 72.
    PERMISSION SYSTEM Permissions Roles (havepermissions) Groups (have roles) Users (have roles, groups) Local roles (user/group has a role on an object) Local role permissions are inherited from the parent Local role inheritence can be disabled per object Workflows (have states and transitions) States (have permissions per role) Transitions (have permissions)
  • 73.
    PERMISSIONS.JSON { "purge": true, "permissions": [ { "id":"View", "title:i18n": "View" }, { "id": "Add", "title:i18n": "Add" }, { "id": "Login", "title:i18n": "Login" },
  • 74.
    ROLES.JSON { "purge": true, "roles": [ { "id":"Anonymous", "title:i18n": "Anonymous", "permissions": ["Login", "Register"] }, { "id": "Authenticated", "title:i18n": "Authenticated", "permissions": ["Logout", "Manage Preferences"] }, { "id": "Owner",
  • 75.
    USERS.JSON { "purge": true, "users": [ { "id":"admin", "password": "admin", "fullname": "Admin", "email": "admin@example.com", "roles": ["Administrator"] }, { "id": "anonymous", "password": "anonymous", "fullname": "Anonymous", "email": "anonymous@example.com",
  • 76.
    GROUPS.JSON { "purge": true, "groups": [ { "id":"Administrators", "title:i18n": "Administrators", "description:i18n": "", "email": "", "roles": ["Administrator"] } ] }
  • 77.
    WORKFLOWS.JSON { "purge": true, "workflows": [ { "id":"simple_publication_workflow", "title:i18n": "Simple Publication Workflow", "description:i18n": "Simple workflow that is useful for "json": { "initial_state": "private", "states": { "private": { "title:i18n": "Private", "description:i18n": "Can only be seen and edited b "transitions": ["publish", "submit"], "permissions": {
  • 78.
  • 79.
    GET ROLES GET /@rolesHTTP/1.1 Accept: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 200 OK Content-Type: application/json [ { "@id": "http://localhost:8000/@roles/Anonymous", "@type": "role", "id": "Anonymous", "title": "Anonymous" }, { "@id": "http://localhost:8000/@roles/Authenticated", "@type": "role", "id": "Authenticated", "title": "Authenticated"
  • 80.
    LIST USERS GET /@usersHTTP/1.1 Accept: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 200 OK Content-Type: application/json [ { "@id": "http://localhost:8000/@users/admin", "id": "admin", "fullname": "Admin", "email": "admin@example.com", "roles": [ "Administrator" ], "groups": [] }, {
  • 81.
    LIST WITH AQUERY GET /@users?query=admin HTTP/1.1 Accept: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 200 OK Content-Type: application/json [ { "@id": "http://localhost:8000/@users/admin", "id": "admin", "fullname": "Admin", "email": "admin@example.com", "roles": [ "Administrator" ], "groups": [] } ]
  • 82.
    GET USER GET /@users/adminHTTP/1.1 Accept: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 200 OK Content-Type: application/json { "@id": "http://localhost:8000/@users/admin", "id": "admin", "fullname": "Admin", "email": "admin@example.com", "roles": [ "Administrator" ], "groups": [] }
  • 83.
    UPDATE USER PATCH /@users/headlessnickHTTP/1.1 Accept: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ Content-Type: application/json { "fullname": "Headless Nick" } HTTP/1.1 204 No Content
  • 84.
    DELETE USER DELETE /@users/headlessnickHTTP/1.1 Accept: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 204 No Content
  • 85.
    REGISTRATION POST /@users HTTP/1.1 Accept:application/json Content-Type: application/json { "email": "nearly.headless.nick@example.com", "fullname": "Nearly Headless Nick", "username": "headlessnick", "sendPasswordReset": true } HTTP/1.1 201 Created Content-Type: application/json { "@id": "http://localhost:8000/@users/headlessnick", "id": "headlessnick", "fullname": "Nearly Headless Nick", "email": "nearly.headless.nick@example.com", "roles": [],
  • 86.
  • 87.
    RESET PASSWORD POST /@users/headlessnick/reset-passwordHTTP/1.1 Accept: application/json HTTP/1.1 200 OK
  • 88.
    SET RESET PASSWORD POST/@users/headlessnick/reset-password HTTP/1.1 Accept: application/json Content-Type: application/json { "reset_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWI "new_password": "headless" } HTTP/1.1 200 OK
  • 89.
    LIST GROUPS GET /@groupsHTTP/1.1 Accept: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 200 OK Content-Type: application/json [ { "@id": "http://localhost:8000/@groups/Administrators", "description": "", "email": "", "groupname": "Administrators", "id": "Administrators", "roles": [ "Administrator" ], "title": "Administrators" }
  • 90.
    GET GROUP GET /@groups/AdministratorsHTTP/1.1 Accept: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 200 OK Content-Type: application/json { "@id": "http://localhost:8000/@groups/Administrators", "id": "Administrators", "title": "Administrators", "description": "", "groupname": "Administrators", "email": "", "roles": [ "Administrator" ] }
  • 91.
    ADD GROUP POST /@groupsHTTP/1.1 Accept: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ Content-Type: application/json { "groupname": "nicks", "title": "Nicks", "description": "Nearly Headless Nicks", "email": "nearly.headless.nicks@example.com", "roles": [ "Contributor" ] } HTTP/1.1 201 Created Content-Type: application/json { "@id": "http://localhost:8000/@groups/nicks", "id": "nicks",
  • 92.
    "groupname": "nicks", "description": "NearlyHeadless Nicks", "email": "nearly.headless.nicks@example.com", "roles": [], "title": "Nicks" }
  • 93.
    UPDATE GROUP PATCH /@groups/nicksHTTP/1.1 Accept: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ Content-Type: application/json { "title": "Headless Nicks" } HTTP/1.1 204 No Content
  • 94.
    DELETE GROUP DELETE /@groups/nicksHTTP/1.1 Accept: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 204 No Content
  • 95.
    GET WORKFLOW GET /news/@workflowHTTP/1.1 Accept: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 200 OK Content-Type: application/json { "@id": "http://localhost:8000/news/@workflow", "history": [], "state": { "id": "published", "title": "Published" }, "transitions": [ { "@id": "http://localhost:8000/news/@workflow/reject", "title": "Send back" },
  • 96.
    WORKFLOW TRANSITION POST /news/my-news-item/@workflow/publishHTTP/1.1 Accept: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 200 OK Content-Type: application/json { "action": "publish", "actor": "admin", "comments": "", "review_state": "published", "time": "2022-04-08T16:00:00.000Z", "title": "Published" }
  • 97.
    SHARING GET /@sharing HTTP/1.1 Accept:application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 200 OK Content-Type: application/json { "available_roles": [ { "id": "Anonymous", "title": "Anonymous" }, { "id": "Authenticated", "title": "Authenticated" }, { "id": "Owner",
  • 98.
    ADD SHARING POST /@sharingHTTP/1.1 Accept: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ Content-Type: application/json { "entries": [ { "id": "Administrators", "roles": { "Contributor": true, "Reader": true }, "type": "user" } HTTP/1.1 204 No Content
  • 99.
  • 100.
    CATALOG Indexes type (path, uuid,integer, date, text, string, boolean, string[]) operators Metadata name type attribute
  • 101.
    CATALOG.JSON { "indexes": [ { "name": "path", "type":"path", "attr": "path", "title:i18n": "Location", "description:i18n": "The location of an item", "group": "Metadata", "enabled": true, "sortable": false, "operators": { "string.absolutePath": { "title:i18n": "Absolute path", "description:i18n": "Location in the site structure"
  • 102.
    QUERYSTRING GET /@querystring HTTP/1.1 Accept:application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 200 OK Content-Type: application/json { "@id": "http://localhost:8000/@querystring", "indexes": { "created": { "title": "Creation date", "description": "The date an item was created", "group": "Dates", "enabled": true, "sortable": true, "values": {}, "vocabulary": null, "operations": [
  • 103.
    SEARCH GET /@search?SearchableText=news* HTTP/1.1 Accept:application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 200 OK Content-Type: application/json { "@id": "http://localhost:8000/@search", "items": [ { "@id": "http://localhost:8000/news", "@type": "Folder", "title": "News", "UID": "32215c67-86de-462a-8cc0-eabcd2b39c26", "path": "/news", "Description": "News Items", "Title": "News", "Subject": null,
  • 104.
    BATCHING GET /@search?b_size=2 HTTP/1.1 Accept:application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 200 OK Content-Type: application/json { "@id": "http://localhost:8000/@search", "items": [ { "@id": "http://localhost:8000/events", "@type": "Folder", "title": "Events", "UID": "1a2123ba-14e8-4910-8e6b-c04a40d72a41", "path": "/events", "Description": null, "Title": "Events", "Subject": null,
  • 105.
    DEPTH GET /@search?path.depth=1 HTTP/1.1 Accept:application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 200 OK Content-Type: application/json { "@id": "http://localhost:8000/@search", "items": [ { "@id": "http://localhost:8000/events", "@type": "Folder", "title": "Events", "UID": "1a2123ba-14e8-4910-8e6b-c04a40d72a41", "path": "/events", "Description": null, "Title": "Events", "Subject": null,
  • 106.
    SORT GET /@search?sort_on=effective HTTP/1.1 Accept:application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 200 OK Content-Type: application/json { "@id": "http://localhost:8000/@search", "items": [ { "@id": "http://localhost:8000/", "@type": "Site", "title": "Welcome to Nick!", "UID": "92a80817-f5b7-400d-8f58-b08126f0f09b", "path": "/", "Description": "Congratulations! You have successfully i "Title": "Welcome to Nick!", "Subject": null,
  • 107.
  • 108.
    VOCABULARIES nick └─ src └─ vocabularies └─actions └─ actions.js └─ behaviors └─ behaviors.js └─ groups └─ groups.js └─ image-scales └─ image-scales.js ... └─ index.js
  • 109.
    CREATE VOCABULARY /** * Actionsvocabulary. * @module vocabularies/actions/actions */ import { Action } from '../../models'; /** * Returns the acions vocabulary. * @method actions * @returns {Array} Array of terms. */ export async function actions(req, trx) { const actions = await Action.fetchAll( {},
  • 110.
    PROFILE VOCABULARIES nick └─ src └─profiles └─ core └─ vocabularies └─ boolean.json
  • 111.
    PROFILE VOCABULARY { "id": "boolean", "title:i18n":"boolean", "items": [ { "title:i18n": "Yes", "token": "Yes" }, { "title:i18n": "No", "token": "No" } ] }
  • 112.
    LIST VOCABULARIES GET /@vocabulariesHTTP/1.1 Accept: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 200 OK Content-Type: application/json [ { "@id": "http://localhost:8000/@vocabularies/actions", "title": "actions" }, { "@id": "http://localhost:8000/@vocabularies/behaviors", "title": "behaviors" }, { "@id": "http://localhost:8000/@vocabularies/boolean", "title": "boolean"
  • 113.
    GET VOCABULARY GET /@vocabularies/actionsHTTP/1.1 Accept: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 200 OK Content-Type: application/json { "@id": "http://localhost:8000/@vocabularies/actions", "items": [ { "title": "View", "token": "view" }, { "title": "Edit", "token": "edit" }, {
  • 114.
  • 115.
    BREADCRUMBS GET /events/event-1/@breadcrumbs HTTP/1.1 Accept:application/json HTTP/1.1 200 OK Content-Type: application/json { "@id": "http://localhost:8000/events/event-1/@breadcrumbs", "items": [ { "@id": "http://localhost:8000/events", "title": "Events" }, { "@id": "http://localhost:8000/events/event-1", "title": "Event 1" } ]
  • 116.
    NAVIGATION GET /news/@navigation HTTP/1.1 Accept:application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 200 OK Content-Type: application/json { "@id": "http://localhost:8000/news/@navigation", "items": [ { "@id": "http://localhost:8000/events", "@type": "Folder", "title": "Events", "UID": "1a2123ba-14e8-4910-8e6b-c04a40d72a41", "path": "/events", "Description": null, "Title": "Events", "Subject": null,
  • 117.
    CONTROLPANELS nick └─ src └─ profiles └─core └─ controlpanels └─ mail.js └─ language.js { "id": "mail", "title:i18n": "Mail", "group": "General", "schema": { "fieldsets": [ { "behavior": "plone", "fields": [ "host", "port", "secure", "user",
  • 118.
  • 119.
    CONTROLPANELS GET /@controlpanels HTTP/1.1 Accept:application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 200 OK Content-Type: application/json [ { "@id": "http://localhost:8000/@controlpanels/language", "group": "General", "title": "Language" }, { "@id": "http://localhost:8000/@controlpanels/mail", "group": "General", "title": "Mail" } ]
  • 120.
    GET CONTROLPANEL GET /@controlpanels/mailHTTP/1.1 Accept: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 200 OK Content-Type: application/json { "@id": "http://localhost:8000/@controlpanels/mail", "group": "General", "title": "Mail", "data": { "host": "localhost", "pass": "", "port": 25, "user": "", "debug": true, "secure": true, "email from name": "Webmaster",
  • 121.
    UPDATE CONTROLPANEL PATCH /@controlpanels/mailHTTP/1.1 Accept: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ Content-Type: application/json { "host": "mail.someserver.com", "port": 25 } HTTP/1.1 204 No Content
  • 122.
    MAIL POST /@email-send HTTP/1.1 Accept:application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ Content-Type: application/json { "name": "John Doe", "from": "john@doe.com", "to": "jane@doe.com", "subject": "Hello!", "message": "Just want to say hi." } HTTP/1.1 204 OK
  • 123.
    MAIL WEBMASTER POST /@email-notificationHTTP/1.1 Accept: application/json Content-Type: application/json { "name": "John Doe", "from": "john@doe.com", "subject": "Hello!", "message": "Just want to say hi." } HTTP/1.1 204 OK
  • 124.
    SYSTEM GET /@system HTTP/1.1 Accept:application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 200 OK Content-Type: application/json { "@id": "http://localhost:8000/@system", "nick_version": "2.8.0", "node_version": "v16.15.0", "express_version": "4.18.2", "objection_version": "3.0.1", "knex_version": "2.3.0", "postgres_version": "14.4" }
  • 125.
    DATABASE GET /@database HTTP/1.1 Accept:application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ HTTP/1.1 200 OK Content-Type: application/json { "@id": "http://localhost:8000/@database", "db_name": "nick", "db_size": "10 MB", "pool": { "min": 2, "max": 10 }, "blob_size": "10 MB" }
  • 126.
    I18N nick └─ locales └─ en └─LC_MESSAGES └─ nick.po └─ nl └─ LC_MESSAGES └─ nick.po └─ nick.po └─ en.js └─ nl.js
  • 127.
  • 128.
    I18N IN JS import{ i18n } from './middleware'; ... app.use(i18n); ... req.i18n('Transaction error.')
  • 129.
    I18N IN JSON { "id":"basic", "title:i18n": "Basic metadata", "description:i18n": "Adds title and description fields.", "schema": { "fieldsets": [ { "fields": ["title", "description"], "id": "default", "title:i18n": "Default" } ], "properties": { "description": { "description:i18n": "A description of this item.",
  • 130.
    LOGGING (LOG4JS) log.info(`Server listeningon port ${config.port}`) 2022-05-30T22:21:06.317 INFO [server.js:12] Server listening o
  • 131.
  • 132.
  • 133.
    ROADMAP Querystring search 100% testcoverage (currently 83%) Multi-Lingual Email login Controlpanels Developer docs Websockets
  • 134.