Development server

Local development on an editor instance is done using FontoXML development tools. This command can be executed from the command prompt/terminal and requires FontoXML development tools and it's requirements to be installed.

The development server is only intended for local development, not for demo, staging, acceptance or production hosting. Any features around authentication are deprecated and will be removed in a subsequent release. There will not be an alternative, the recommendation is to set up a production ready web-server.

The development server can be started by running the fdt editor run command. This command comes with the following options:

Option Shorthand Possible values Default value Description
--version
-V

-


Outputs the version number for the task.
--body-parser-limit
-
A string value.
5mb
Controls the maximum request body size.
--cache-control-disabled
-
-
Disable the Cache-Control header.
--cache-control-max-age
-
An integer value.
300
The value of max-age in the Cache-Control header, in seconds. Used for images and static files.
--delay
-D
An integer value.
0
Simulate a delay. Value supplied is delay in milliseconds.
--dist
-d
-
Serve the build (dist) directory. It sole purpose it to verify Fonto application builds.
--dist-auth
-
-

Enable authentication in dist mode.

Authentication is not supported and will be removed in a future release. This includes the --dist-auth flag.

--lock-not-acquired
-l
-
Simulates a state in which the lock(s) for the document(s) to be opened are not yet acquired but are available.
--lock-not-available
-L
-
Simulates a state in which the lock(s) for the document(s) to be opened are not yet acquired and are not available.
--memory-store-ttl
-
An integer value.
3600
Time in seconds to keep the documents in the (session/shared) memory store. Use -1 to save documents until server restart.
--port
-p
An integer value.
8080
Determine which port to listen to. Value supplied is port to listen to.
--release-locks-on-state-request
-
-

Document locks are released on every document state call.

--savemode
-s
  • disk
  • session-memory
  • shared-memory
  • off
disk
Sets the save mode for your local instance.
--verbose
-v
-
Sets the development server to verbose mode outputting more information.
--help
-h
-
Outputs a list of possible options.

Configuration with config.json or config.js

There are cases where you want to define your own default values for the fdt editor run command persistently. For example changing the initial document that opens once the editor has loaded or start the development server on a different port. To accomplish this there is a file in the root folder of your editor instance called either config.json or config.js. In this file you'll be able to change the default behavior of the development server.

Values passed by command line have priority over the values defined in config.json or config.js. For example fdt editor run --port 9090 will overwrite any default value you might have configured for your editor.

Changing anything in the aforementioned files requires you to restart the development server. This is done by stopping the server from the command prompt/terminal and starting it again using the fdt editor run command.

An example config.json

config.json
{
    "alwaysRegenerateSessionToken": false,
    "bodyParserLimit": "5mb",
    "cacheControlDisabled": false,
    "cacheControlMaxAge": 300,
    "delay": 0,
    "dist": false,
    "distAuth": false,
    "documentLoadLock": {
        "isLockAcquired": true,
        "isLockAvailable": true,
        "lockReason": "Unable to unlock the document, please try again later.",
        "releaseLocksOnStateRequest": false
    },
    "memoryStoreTtl": 3600,
    "port": 8080,
    "saveMode": "disk",
    "verbose": false
}

An example config.js

If you want a more flexible configuration, we recommend using config.js as it allows you to use JavaScript in the configuration object. The example below demonstrates a simple use case where the port number of the development server is passed through an environment variable called NODE_PORT.

config.js
module.exports = {
    "alwaysRegenerateSessionToken": false,
    "bodyParserLimit": "5mb",
    "cacheControlDisabled": false,
    "cacheControlMaxAge": 300,
    "delay": 0,
    "dist": false,
    "distAuth": false,
    "documentLoadLock": {
        "isLockAcquired": true,
        "isLockAvailable": true,
        "lockReason": "Unable to unlock the document, please try again later.",
        "releaseLocksOnStateRequest": false
    },
    "memoryStoreTtl": 3600,
    "port": process.env.NODE_PORT || 8080,
    "saveMode": "disk",
    "verbose": false
}


The development server only has to be restarted if something is changed which affects the server itself or if you want to apply different options. This means that any changes to editor configuration or the XML that is loaded do not require a restart.

Hosting your own XML files

The development server can serve XML documents from the dev-cms/files folder in your editor. It accepts XML documents in different encodings:

  • UTF-8 (with or without byte order mark)
  • Deprecated: UTF-16 (little endian, with or without byte order mark)
  • Deprecated: ISO-8859-x

If the savemode of the development server is set to disk, new documents and new versions of the documents present in dev-cms/files will be placed in dev-cms/uploads.

Custom assets

In addition to its standard functionality the development server also offers a way to customize the way it responds to API calls. This for example allows you to create expected behavior for custom asset or reference types.

Below is an example on how to do this when implementing a new custom reference type called 'bibliographic'.

Create a file called configureDevCms.js in the dev-cms/ directory and paste the following contents:

configureDevCms.js
'use strict';

module.exports = (router, config) => {
	return {
		routes: [
			require('./routes/configureConnectorsCmsStandardBrowse')(router, config),
			require('./routes/configureConnectorsCmsStandardReferenceGet')(router, config)
	};
};

Create a routes folder in your dev-cms folder, which can be found in the root of your editor instance.

In the routes folder create a file named configureConnectorsCmsStandardBrowse.js and fill it with the following:

configureConnectorsCmsStandardBrowse.js
'use strict';

var taxonomy = require('./taxonomy.json'),
	taxonomy_faa = require('./taxonomy_faa.json'),
	taxonomy_ta = require('./taxonomy_ta.json'),
	taxonomy_l = require('./taxonomy_l.json'),
	sources = require('./sources.json');

module.exports = (router, config) => {
	router
		.route('/connectors/cms/standard/browse')
		.post((req, res, next) => {
			if (!req.body) {
				return next();
			}

			var assetTypes = req.body.assetTypes || [],
				folderId = req.body.folderId;

			// If not requesting glossary listing, continue as normal.
			if (assetTypes.indexOf('glossary') === -1) {
				return next();
			}

			// Override assetTypes and folderId to serve glossary items as documents from the glossary folder
			req.body.assetTypes = ['document'];
			if (folderId) {
				req.body.folderId = path.join('glossary', folderId);
			}
			else {
				req.body.folderId = 'glossary';
			}

			next();
		})
		.post((req, res, next) => {
				var assetTypes = req.body.assetTypes || [],
					folderId = req.body.folderId,
					query = req.body.query,
					results = null;

				if (assetTypes.indexOf('taxonomy') !== -1) {
					var type = 'taxonomy',
						internalTaxonomy = 'topical-area';

					switch (req.body.query.taxonomy) {
						case 'topical-area':
							internalTaxonomy = taxonomy;
							break;
						case 'facilities-and-activities':
							internalTaxonomy = taxonomy_faa;
							break;
						case 'target-audience':
							internalTaxonomy = taxonomy_ta;
							break;
						case 'lifetime':
							internalTaxonomy = taxonomy_l;
							break;
					}

					if (folderId) {
						results = internalTaxonomy.filter(function (pointer) {
							return (pointer.type == type || pointer.type == 'folder') && pointer.parent == folderId;
						});
					}
					else {
						results = internalTaxonomy.filter(function (pointer) {
							return (pointer.type == type || pointer.type == 'folder') && !pointer.parent;
						});
					}

					// Returns only files (by only matching on type equals current type; 'taxonomy')
					// matching the given query
					if (query) {
						results = results.filter(function (item) {
							// If a query is given, only return documents
							if (item.type !== type) {
								return false;
							}
							// If fulltext is provided, filter by label
							if (query.fulltext) {
								return item.displayName.toLowerCase().indexOf(query.fulltext.toLowerCase()) !== -1;
							}
							return true;
						});
					}

					results = results.map(function (item) {
						var data = {
							id: item.id,
							type: item.type,
							label: item.displayName,
							metadata: item.metadata
						};

						return data;
					});

					res.status(200).json({
						totalItemCount: results.length,
						items: results
					});
				}
				else if (assetTypes.indexOf('warehouse-chunk') !== -1) {
					req.body.assetTypes = ['document'];
					if (!folderId || folderId.indexOf('reusable-') === -1) {
						req.body.folderId = 'reusable-chunks';
					}

					return next();
				}
				else if (assetTypes.indexOf('collection') !== -1) {
					req.body.assetTypes = ['document'];
					if (!folderId || folderId.indexOf('copyable-') === -1) {
						req.body.folderId = 'copyable-chunks';
					}

					return next();
				}
				else {
					return next();
				}

		})
		.post((req, res, next) => {
			var assetTypes = req.body.assetTypes || [],
				query = req.body.query,
				results = null;

			if (assetTypes.indexOf('source') !== -1) {
				if (query) {
					results = sources.filter(function (item) {
						// If fulltext is provided, filter by label
						if (query.fulltext) {
							return item.fullyFormatted.toLowerCase().indexOf(query.fulltext.toLowerCase()) !== -1;
						}
						return true;
					});
				}

				results = results.map(function (item) {
					var data = {
						id: item.id,
						label: item.fullyFormatted,
						type: "source",
						folderCount: 0,
						metadata: {
							inlineFormatted: item.inlineFormatted,
							fullyFormatted: item.fullyFormatted,
							library: item.library,
							attachments: item.attachments,
							isSuspicious: item.isSuspicious,
							suspicuousReason: item.suspicuousReason,
							newTarget: item.newTarget,
							newFullyFormatted: item.newFullyFormatted
						}
					};

					return data;
				});

				results = results.filter(function (result) {
					return !result.metadata.isSuspicious;
				});

				res.status(200).json({
					totalItemCount: results.length,
					items: results
				});
			}
			else {
				return next();
			}
		});

	return router;
};

In the routes folder create a file named configureConnectorsCmsStandardReferenceGet.js and fill it with the following:

configureConnectorsCmsStandardReferenceGet.js
'use strict';

var sources = require('./sources.json');

module.exports = (router, config) => {
	router
		.route('/connectors/cms/standard/reference/get')
		.post((req, res, next) => {
			var referenceRepository = require('../../tools/fontoxml-dev-server/src/connectors-cms-standard/referenceRepository'),
				results = [];

			req.body.permanentIds.forEach(function (permanentId) {
				var reference = referenceRepository.get(permanentId),
					id = permanentId.split(':::')[2];

				if (reference) {
					if (reference.type === 'citation-reference' || reference.type === 'bibliographic-reference') {
						var metadata = sources.find(function (source) {
							return source.id === id;
						});

						reference.metadata = metadata || {};

						results.push({
							status: 200,
							body: reference
						});
					} else {
						results.push({
							status: 200,
							body: reference
						});
					}
				} else {
					results.push({
						status: 404,
						body: null
					});
				}
			});

			return res.status(200).json({
				results: results
			});
		});

	return router;
};

Custom endpoints

Using the above example you should be able to also customize any other of the APIs available.

Another functionality of the development is the ability to add fully custom API endpoints. This is very similar to customizing an existing API endpoint as described above.

An example:

'use strict';

module.exports = (router, config) => {
	router
		.route('/connectors/cms/standard/relationship/create')
		.post((req, res, next) => {
			var relationship = {
				permanentId: 'GUID_' + (Math.floor( Math.random() * 9 ) + 1),
				target: req.body.target,
				relationshipType: req.body.relationshipType,
				direction: 'source'
			};

			// return res.status(403).json({});
			return res.status(201).json(relationship);
		});

	return router;
};