FORGEBOX Enterprise 🚀 - Take your ColdFusion (CFML) Development to Modern Times! Learn More...

PresideCMS Extension: Preside Data API

v3.0.7.00124 Public

Preside Data API Extension

The Data API extension provides developers with the power to rapidly develop APIs against their Preside data models. With a few simple object annotations and optional i18n resource keys, developers are able to provide full CRUD APIs to the data model with beautifully rendered API documentation.

Note: Version 3.0.0 requires Preside version 10.11.0 and above.

Accessing the API

The default root URI of the API is /api/data/v1/ (see Namespaces and multiple APIs, further below). OpenAPI v3 specification can be browsed at /api/data/v1/docs/spec/ and HTML documentation based on the spec can be found at /api/data/v1/docs/html/ (or /api/data/v1/docs/swagger/, if you prefer).

Configuring your entities

Object annotations

The bare minimum configuration to enable an object for the API is to set @dataApiEnabled true:

/**
 * @dataApiEnabled true
 *
 */
component {
	// ...
}

Additional optional annotation options at the object level are:

  • dataApiEntityName: Alternative entity name to use in API urls, e.g. instead of crm_contact, you may wish to use contact.
  • dataApiCategory: For the HTML documentation. Allows sub-groups of entities. Especially useful for large APIs.
  • dataApiQueueEnabled: Whether or not the change queue is enabled for this object
  • dataApiQueue: Specific queue name for this object
  • dataApiSortOrder: Sort order for paginated results. Default is date last modified ascending.
  • dataApiSavedFilters: Comma-separated list of saved filters to apply to all requests to this object (e.g. only return active records)
  • dataApiVerbs: Supported REST HTTP Verbs. If not supplied, all verbs and operations are supported (i.e. GET, POST, PUT and DELETE)
  • dataApiFields: Fields to return in GET requests (defaults to all non-excluded fields)
  • dataApiUpsertFields: Fields to accept in POST/PUT request (defaults to dataApiFields)
  • dataApiExcludeFields: Fields to exclude from GET API calls
  • dataApiUpsertExcludeFields: Fields not to accept in POST/PUT requests (defaults to dataApiExcludeFields)
  • dataApiFilterFields: Fields to allow as simple filters for paginated GET requests (defaults to foreign keys, boolean and enum fields)
  • dataApiAllowIdInsert: Whether or not to allow the ID field to be set during a POST operation to create a new record

Property annotations

Object properties support the following optional annotations:

  • dataApiAlias: An alternative name to use for the property in the API. i.e. instead of contact_status you may wish to just use status through the API.
  • dataApiRenderer: A non-default renderer to use when returning data through the API. See custom renderers, below.
  • dataApiDerivative: For image assets, specify the derivative to be used when rendering URLs.
  • dataApiType: For the documentation. The data type of the property, e.g. 'string'.
  • dataApiFormat: For the documentation. The format for the property, e.g. 'Email address'.
  • dataApiEnabled: Whether or not this property should be included in the API
  • dataApiUpsertEnabled: Whether or not this property should be included in the POST/PUT operations.

Custom renderers

If you specify a non-default renderer for an object property, it will be rendered using Preside's content rendering system. For example, the following property definition specifies a myCustomRenderer renderer:

property name="my_prop" dataApiAlias="myProp" dataApiRenderer="myCustomRenderer";

To implement this, you will need a corresponding viewlet at renderers.content.myCustomRenderer.dataapi or renderers.content.myCustomRenderer.default (a renderer context of dataapi will be used and the system will fallback to the default renderer if that context is not implemented).

For example:

// /handlers/renderers/content/MyCustomRenderer.cfc
component {

	private string function dataApi( event, rc, prc, args={} ){
		var value = args.data ?: "";

		return renderView( view="/renderers/content/myCustomRenderer/dataApi", args={ value=value } );
	}

}

Customizing documentation labels and descriptions

All of the labelling and text in the generated documentation can be found at /i18n/dataapi.properties and you should refer to that when customizing the default text. A bare minimum override might look like:

api.title=My Application's API
api.description=This is my awesome application's API and here is some general information about it.\n\
\n\
Team awesome xxx.
api.version=v2.0
api.favicon=data:image/x-icon;base64,iVBOR...

Categories

If you annotate your objects with a @dataApiCategory property, your categories can be documented with:

category.my_category.name=My Category
category.my_category.description=Markdown enabled description of my category.
category.my_category.sort.order=20

Object and field level customizations

The following set of .properties file keys can be added per object in your own dataapi.properties files to customize the documentation per entity/field:

# OBJECT LEVEL:

dataapi:entity.my_entity.name=My entities
dataapi:entity.my_entity.name.singular=My entity
dataapi:entity.my_entity.description=Description of my entity (or API description for this section of the docs)
dataapi:entity.my_entity.sort.order=10

dataapi:operation.my_entity.get.description=Description for the paginated GET operation for your entity
dataapi:operation.my_entity.get.200.description=Description for the successful (200) response documentation for paginated GET requests for your entity

dataapi:operation.my_entity.get.by.id.description=Description for the GET /{recordId}/ operation for your entity
dataapi:operation.my_entity.get.by.id.200.description=Description for the successful (200) response documentation for GET /{recordId}/ requests for your entity
dataapi:operation.my_entity.get.by.id.404.description=Description for the not found (404) response documentation for GET /{recordId}/ requests for your entity

dataapi:operation.my_entity.put.description=Description for the PUT / (batch update) operation for your entity
dataapi:operation.my_entity.put.body.description=Description for the http json body for the PUT / (batch update) operation for your entity
dataapi:operation.my_entity.put.200.description=Description for the successful (200) response documentation for PUT / requests for your entity
dataapi:operation.my_entity.put.422.description=Description for the validation failed (422) response documentation for PUT / requests for your entity
dataapi:operation.my_entity.put.by.id.description=Description for the PUT /{recordid}/ (single record update) operation for your entity
dataapi:operation.my_entity.put.by.id.body.description=Description for the http json body for the PUT /{recordid}/ (single record update) operation for your entity
dataapi:operation.my_entity.put.by.id.200.description=Description for the successful (200) response documentation for PUT /{recordid}/ requests for your entity
dataapi:operation.my_entity.put.by.id.422.description=Description for the validation failed (422) response documentation for PUT /{recordid}/ requests for your entity
dataapi:operation.my_entity.put.by.id.404.description=Description for the record not found (404) response documentation for PUT /{recordid}/ requests for your entity

dataapi:operation.my_entity.post.description=Description of the POST / (batch insert) operation for your entity
dataapi:operation.my_entity.post.body.description=Description for the http json body for the POST / (batch insert) operation for your entity
dataapi:operation.my_entity.post.200.description=Description for the successful (200) response documentation for POST / requests for your entity
dataapi:operation.my_entity.post.422.description=Description for the validation failed (422) response documentation for POST / requests for your entity

dataapi:operation.my_entity.delete.description=Description of the DELETE /{recordid}/ operation for your entity
dataapi:operation.my_entity.delete.200.description=Description of the successful (200) response documentation for DELETE /{recordId}/ operations for your entity

# Field level
dataapi:operation.my_entity.get.params.fields.my_field.description=Description for *filter* field
dataapi:entity.my_entity.field.my_field.description=Description of field

Further customizations using interceptors

The following interception points are used to allow you to more deeply customize the integration.

onOpenApiSpecGeneration

Fired when generating OpenApi v3 specification for the API. A spec struct will be present in the interceptorArgs that you can use to augment the specification.

preDataApiSelectData

Fired before selecting data through the API. Receives the following keys in the interceptData:

  • selectDataArgs: Arguments that will be passed to the selectData() call
  • entity: Name of the entity being operated on

postDataApiSelectData

Fired after selecting data through the API. Receives the following keys in the interceptData:

  • selectDataArgs: Arguments that were passed to the selectData() call
  • entity: Name of the entity being operated on
  • data: Rendered and prepared data that will be returned to the API caller

preDataApiInsertData

Fires before inserting data through the API. Receives the following keys in the interceptData:

  • insertDataArgs: Arguments that will be passed to the insertData() call
  • entity: Name of the entity being operated on
  • record: The data that will be inserted (struct)

postDataApiInsertData

Fires after inserting data through the API. Receives the following keys in the interceptData:

  • insertDataArgs: Arguments that were passed to the insertData() call
  • entity: Name of the entity being operated on
  • record: The data that will be inserted (struct)
  • newId: Newly created record ID

preDataApiUpdateData

Fires before updating data through the API. Receives the following keys in the interceptData:

  • updateDataArgs: Arguments that will be passed to the updateData() call
  • entity: Name of the entity being operated on
  • recordId: ID of the record to be updated
  • data: The data that will be inserted (struct)

postDataApiUpdateData

Fires after updating data through the API. Receives the following keys in the interceptData:

  • updateDataArgs: Arguments that were passed to the updateData() call
  • entity: Name of the entity being operated on
  • recordId: ID of the record to be updated
  • data: The data that will be inserted (struct)

preDataApiDeleteData

Fires before deleting data through the API. Receives the following keys in the interceptData:

  • deleteDataArgs: Arguments that will be passed to the deleteData() call
  • entity: Name of the entity being operated on
  • recordId: ID of the record to be deleted

postDataApiDeleteData

Fires after deleting data through the API. Receives the following keys in the interceptData:

  • deleteDataArgs: Arguments that were passed to the deleteData() call
  • entity: Name of the entity being operated on
  • recordId: ID of the record to be deleted

Data Change Queue(s)

By default, they system enables a queue feature: settings.features.dataApiQueue. When enabled, API users can be subscribed, through the admin UI, to listen for data changes to all, or a number, of entities in the system.

Configuring the queue system

As of v3.0.0, you have the ability to configure multiple queue endpoints per API (see also, Namespaces and multiple APIs, below). This can be used to group entities into queues so that multiple different services can process the queues independently. The available settings per queue are:

  • pageSize: Number of records returned with each call to queue. Default is 1
  • atomicChanges: Whether or not the queue should return atomic changes. If true, each item in the queue will contain only the fields that have changed. If false, default, each item in the queue will contain the latest state of the record. Default is false.

Defining queues

Queues are defined in the Preside rest API configuration in Config.cfc. To add a queue definition to the default API:

settings.rest.apis[ "/data/v1" ].dataApiQueueEnabled = true;
settings.rest.apis[ "/data/v1" ].dataApiQueues = {
	  default    = { pageSize=100, atomicChanges=true } // the default queue
	, highvolume = { pageSize=1000, atomicChanges=false }
};

Per object queue settings:

Annotate your preside object CFC with @dataApiQueueEnabled (default is true) and @dataApiQueue (default is default) properties. For example:

/**
 * @dataApiQueueEnabled false
 */

or

/**
 * @dataApiQueue highvolume
 */

Namespaces and multiple APIs

Introduced in v2.0.0

By default, the API is exposed at /api/data/v1/. However, there will be occasions when you want to expose your data in different ways for different purposes. Or, if you are writing an extension, you may want to namespace your API so that it does not clash with any existing default API implementation. You can also use this feature to host multiple versions of your API concurrently.

With just a small amount of configuration, you can use all of the Data API's functionality in a separate, namespaced instance. First, configure the endpoints in your Config.cfc:

settings.rest.apis[ "/myGroovyApi/v1" ] = {
	  authProvider     = "dataApi"
	, description      = "REST API to expose data with an alternate structure"
	, dataApiNamespace = "myGroovyApi"
	, dataApiQueueEnabled = true
	, dataApiQueues       = {
		default = { pageSize=100, atomicChanges=true }
	  }
};
settings.rest.apis[ "/myGroovyApi/v1/docs" ] = {
	  description      = "Documentation for myGroovyApi REST API (no authentication required)"
	, dataApiNamespace = "myGroovyApi"
	, dataApiDocs      = true
};

A few things to note here:

  • The key within settings.rest.apis (e.g. /myGroovyApi/v1) is the base URI for the API. This will have /api prepended to it in the full URL.
  • dataApiNamespace is the namespace for the alternate API, and will be used when configuring objects. This will usually be the first part of the URI, but does not need to be.
  • dataApiDocs marks that this is the endpoint for the Swagger documentation. This whole endpoint could be omitted if you do not require the automatic document generation.
  • Use authProvider to mark an endpoint as needing authentication. If you omit this, the resource will not require authentication. In the API Manager in Preside, you can allow users to have access to individual APIs - so a user could have access to /api/myGroovyApi/v1 but not to the default /api/data/v1, if you wish.

Annotations

You can annotate your objects using exactly the same annotations as described above, but with :{namespace} appended. For example:

/**
 * @dataApiEnabled             true
 * @dataApiEnabled:myGroovyApi true
 * @dataApiVerbs:myGroovyApi   GET
 *
 */
component {
	property name="label" dataApiAlias:myGroovyApi="some_other_label";
}

This would enable this object both for the default /data/v1 API, and for your custom /myGroovyApi/v1 API. However, for myGroovyApi the object would only allow GET access, and the label field would be called some_other_label instead of label.

You could also use this to specify an alternate renderer for a property, e.g. dataApiRenderer:myGroovyApi="alternateRenderer".

Note that namespaces do not inherit any annotations from the default API. Any annotations must be made explicitly with the :{namespace} suffix.

Labelling and text

You can use all the same label and description customisations in your /i18n/dataapi.properties file as above; simply prefix the key name with {namespace}.. For example:

myGroovyApi.api.title=My Second API
myGroovyApi.api.description=This is an alternate API to my application

myGroovyApi.entity.my_entity.name=My alternate entity

Unlike annotations, i18n properties will cascade up to the defaults. So if you do not make any customisations, you will see all the default text.

Interceptors

All the same interceptors will run when actions are taken in a namespaced API. However, the interception point name will have _{namespace} as a suffix. Again, there is no fallback to the default interceptor, so any interceptors will need to be explicitly defined for your namespace. For example:

public void function preDataApiSelectData( event, interceptData ) {
	// action to take for preDataApiSelectData on the default API
}
public void function preDataApiSelectData_myGroovyApi( event, interceptData ) {
	// action to take for preDataApiSelectData on the myGroovyApi API
}

Configuring individual user access

Provided that your APIs use the 'dataApi' auth provider (default), you will be able to manage user access to the APIs through the admin interface.

  1. Navigate to: System > API Manager
  2. Find the API you want to manage & click 'Data API authentication'
  3. Click Add user
  4. Enter a user name (can lookup existing users also) and configure access to individual entities, their verbs and optionally queue access

Changelog

v3.0.7

  • #38 Specifying fields in request should respect property alias

v3.0.6

  • #37 Add dataApiDerivative argument to properties for rendering image asset URLs

v3.0.5

  • #36 Fix error where DELETE /queue fails for batch delete on default queue (array of IDs in JSON body)
  • #35 Ensure that 404 is shown when API resource URI not found (was throwing 500 error)

v3.0.4

  • #34 Ensure deleting entries from the queue works with all default queues in all namespaces

v3.0.3

  • #33 Ensure all queue items are picked up for default queues

v3.0.2

  • #29 Apply saved filters when queueing changes to data

v3.0.1

  • #27 Use field aliases when documenting and using filter fields in paginated GET requests
  • #26 Fix for errors raised when calling API for entity whose primary key is numeric

v3.0.0

Administration

  • Add admin UI for managing individual user access to entities, verbs and queues

Queue enhancements

  • Ability to turn off the queue feature entirely
  • Ability to turn off the queue feature per object
  • Allow queue to contain atomic data changes
  • Allow queue to return configurable number of records, rather than always 1
  • Allow multiple queues for different groups of objects and with individual queue settings

Documentation enhancements

  • Add new plain HTML documentation endpoint
  • Object properties should be able to specify type and format for their spec definitions
  • Ability to categorize entities (applies to HTML documentation only)
  • Ability to set sort order on entities
  • Ability to set favicon for an API
  • Use plural name of entity for default entity tag name

Miscellaneous

  • Automatically convert foreign keys to assets, links and pages to URLs.
  • Ability to exclude/include properties in the API through annotations on the properties

Bug fixes

  • Documentation of foreign keys: only use 'Foreign Key UUID' format when it really is a UUID (i.e., not for int)
  • Do not show authentication description when API does not use authentication

v2.0.2

  • Fix path to docs

v2.0.1

  • Better handling of namespaced routes and handler paths
  • Update queues to allow namespacing

v2.0.0

  • Add support for multiple configured APIs via namespacing
  • Add dataApiSavedFilters annotation

v1.0.19

  • Total record count should return filtered count if filter is applied

v1.0.18

  • Prevent stack overflow with recursive loading of services on application startup

v1.0.17

  • #2: Add preValidate interception point for upsert operations

v1.0.16

  • Fixes #1: ensure that booleans are rendered as 'true' or 'false', not '1' or '0'

v1.0.15

  • Add general error message documentation
  • When POSTing (creating records), do not ignore missing fields

v1.0.14

  • Add option for specifying whether or not ID field creation is supported

v1.0.5-1.0.13

  • Refactoring build system

v1.0.4

  • Change queue logic so that fetching next item from queue always returns the same item until the API user manually removes it from the queue

v1.0.3

  • Fix interceptor logic to not break deletes that use a filter other than record IDs

v1.0.2

  • Improve documentation of queue system API
  • Fix the possible field list for the 'fields' REST API param
  • Allow for default descriptions of commonly named fields

v1.0.1

  • Initial release

Here are all the versions for this package. Please note that you can leverage CommandBox package versioning to install any package you like. Please refer to our managing package version guide for more information.

Version Created Last Update Published By Stable Actions
Current
3.0.7.00124 Oct 14 2019 01:53 PM Oct 14 2019 01:53 PM
Version History
3.0.6.00121 Oct 13 2019 05:54 AM Oct 13 2019 05:54 AM
3.0.5.00119 Sep 25 2019 05:47 AM Sep 25 2019 05:47 AM
3.0.4.00116 Sep 23 2019 08:38 AM Sep 23 2019 08:38 AM
3.0.3.00112 Sep 16 2019 06:02 PM Sep 16 2019 06:02 PM
3.0.2.00109 Aug 02 2019 10:48 AM Aug 02 2019 10:48 AM
3.0.1.00106 Jul 30 2019 10:23 AM Jul 30 2019 10:23 AM
3.0.0.00104 Jul 29 2019 05:20 AM Jul 29 2019 05:20 AM
3.0.0-SNAPSHOT00102 Jul 29 2019 04:37 AM Jul 29 2019 04:37 AM
3.0.0-SNAPSHOT00101 Jul 29 2019 04:15 AM Jul 29 2019 04:15 AM
3.0.0-SNAPSHOT00100 Jul 27 2019 09:10 PM Jul 27 2019 09:10 PM
3.0.0-SNAPSHOT00099 Jul 27 2019 06:10 PM Jul 27 2019 06:10 PM
3.0.0-SNAPSHOT00098 Jul 26 2019 07:07 AM Jul 26 2019 07:07 AM
3.0.0-SNAPSHOT00097 Jul 26 2019 04:00 AM Jul 26 2019 04:00 AM
3.0.0-SNAPSHOT00096 Jul 25 2019 01:15 PM Jul 25 2019 01:15 PM
3.0.0-SNAPSHOT00095 Jul 25 2019 11:32 AM Jul 25 2019 11:32 AM
3.0.0-SNAPSHOT00094 Jul 25 2019 10:46 AM Jul 25 2019 10:46 AM
3.0.0-SNAPSHOT00091 Jul 22 2019 04:53 AM Jul 22 2019 04:53 AM
3.0.0-SNAPSHOT00088 Jul 18 2019 02:13 AM Jul 18 2019 02:13 AM
3.0.0-SNAPSHOT00084 Jul 17 2019 05:20 PM Jul 17 2019 05:20 PM
2.0.2.00064 Jun 17 2019 08:58 AM Jun 17 2019 08:58 AM
2.0.1.00061 Jun 17 2019 08:39 AM Jun 17 2019 08:39 AM
2.0.0.00058 Jun 13 2019 06:59 AM Jun 13 2019 06:59 AM
1.0.19.00053 Jun 06 2019 10:34 AM Jun 06 2019 10:34 AM
1.0.18.00045 May 24 2019 07:47 AM May 24 2019 07:47 AM
1.0.17.00043 Apr 30 2019 04:39 AM Apr 30 2019 04:39 AM
1.0.16.00038 Apr 24 2019 08:40 AM Apr 24 2019 08:40 AM
1.0.15.00036 Apr 13 2019 05:36 AM Apr 13 2019 05:36 AM
1.0.14.00032 Jan 18 2019 09:37 AM Jan 18 2019 09:37 AM
1.0.13.00029 Nov 08 2018 04:35 AM Nov 08 2018 04:35 AM

 

No collaborators yet.
  • Nov 08 2018 04:35 AM
  • Oct 14 2019 01:53 PM
  • 757
  • 1226
  • 7471