We are so excited to bring you yet another minor release for our cborm project to version 2.5. This is a very exciting release as it brings about automatic RESTFul CRUD for ORM entities based on ColdBox 6 resources.

Install or Update

To get started with cborm, just install it via CommandBox: box install cborm. To update your current install just run a update cborm and off you go.

Fully Documented

 

 

What is cborm

The cborm module is a module that will enhance your experience when working with the ColdFusion ORM powered by Hibernate. It will not only enhance it with dynamic goodness but give you a fluent and human approach to working with Hibernate

  • Service Layers with all the methods you could probably think off to help you get started in any project
  • Virtual service layers so you can create virtual services for any entity in your application
  • Automatic RESTFul resources handler, focus on your domain objects and business logic, not the boilerplate of REST
  • ActiveEntity our implementation of Active Record for ORM
  • Fluent queries via Hibernate's criteria and detached criteria queries with some Dynamic CFML goodness
  • Automatic transaction demarcation for save and delete operations
  • Dynamic finders and counters for expressive and fluent shorthand SQL
  • Automatic Java casting
  • Entity population from json, structs, xml, and queryies including building up their relationships
  • Entity validation via cbValidation
  • Includes the Mementifier project to produce memento states from any entity, great for producing JSON
  • Ability for finders and queries to be returned as Java streams using our cbStreams project.

 

# A quick preview of some functionality

var book = new Book().findByTitle( "My Awesome Book" );
var book = new Book().getOrFail( 2 );
new Book().getOrFail( 4 ).delete();
new Book().deleteWhere( isActive:false, isPublished:false );

property name="userService" inject="entityService:User";

return userService.list();
return userService.list( asStream=true );

var count = userService.countWhere( age:20, isActive:true );
var users = userService.findAllByLastLoginBetween( "01/01/2019", "05/01/2019" );

userService
    .newCriteria()
    .eq( "name", "luis" )
    .isTrue( "isActive" )
    .getOrFail();

userService
    .newCriteria()
    .isTrue( "isActive" )
    .joinTo( "role" )
        .eq( "name", "admin" )
    .asStream()
    .list();

userService
    .newCriteria()
    .withProjections( property="id,fname:firstName,lname:lastName,age" )
    .isTrue( "isActive" )
    .joinTo( "role" )
        .eq( "name", "admin" )
    .asStruct()
    .list();

What's New With 2.5.0

This release not only has some bug fixes but several new features that pack a punch.

Release Notes

  • Features : Introduction of the automatic resource handler for ORM Entities based on ColdBox's 6 resources and RestHandler
  • Improvement : Natively allow for nested transactions and savepoints by not doing preemptive transaction commits when using transactions.
  • Bug : Fix on getOrFail() where if the id was 0, it would still return an empty object.
  • Task : Added formatting via cfformat

Automatic REST CRUD

In cborm 2.5 we introduced the Base Resource Handler for ORM entities. This base handler will create a nice framework for creating a RESTFul CRUD for your entities based on ColdBox Resources: https://coldbox.ortusbooks.com/the-basics/routing/routing-dsl/resourceful-routes

This means that we will create all the boilerplate code to list, show, create, update and delete your entities. Including relationships, validation, population, pagination and different ways to render (include/exclude) your data thanks to Mementifier. Get ready to start creating RESTFul services in no time!

You must be using ColdBox 6 for this feature to work

Settings

To start off with our resources support, you can start by adding the following settings to your config/Coldbox.cfc in the moduleSettings.cborm struct:

cborm = {
	 // Resource Settings
		resources : {
			// Enable the ORM Resource Event Loader
			eventLoader 	: false,
			// Pagination max rows
			maxRows 		: 25,
			// Pagination max row limit: 0 = no limit
			maxRowsLimit 	: 500
		}
}
  • eventLoader : If enabled, upon application startup it will register all the following events for EVERY entity managed by Hibernate
  • maxRows : By default the cborm resource handler will paginate results, you can choose your pagination window here.
  • maxRowsLimit : By default it will not allow more than 500 records to be returned from the listing method. However, you can make this 0 or anything you like.

Registered Events

Once the resource event loader is activated, it will ask Hibernate for all the managed entities and register the following events in ColdBox so you can create interceptors that listen to them:

  • pre{entityName}List
  • post{entityName}List
  • pre{entityName}Save
  • post{entityName}Save
  • pre{entityName}Show
  • post{entityName}Show
  • pre{entityName}Update
  • post{entityName}Update
  • pre{entityName}Delete
  • post{entityName}Delete

Define Your Entities

It's time to focus on your entities now. Build out the properties, relationships, methods and make sure you add the mementifier and validation settings so our base handler can use it for validation and for data rendering:

/**
 * Copyright since 2020 by Ortus Solutions, Corp
 * www.ortussolutions.com
 * ---
* A setting object
*/
component
	persistent="true"
	table="setting"{

	/* *********************************************************************
	**							PROPERTIES
	********************************************************************* */

	property
		name="settingId"
		fieldtype="id"
		generator="uuid"
		ormtype="string"
		setter="false";

	property
		name="name"
		notnull="true"
		unique="true"
		length="255";

	property
		name="value"
		notnull="true"
		ormtype="text";

	/* *********************************************************************
	**							PK + CONSTRAINTS
	********************************************************************* */

	// Validation
	this.constraints ={
		"name" 		= { required=true, size="1..255", validator="UniqueValidator@cborm" },
		"value" 	= { required=true }
	};

	// Mementofication
	this.memento = {
		defaultIncludes = [ "name", "value" ]
	};

}

The mementifier is a critical piece as it will allow you to pass in to the rest service includes and excludes so you can decide what will be marshalled out of the service.

Register The Resource

Now that you have finished your entities, you can now register the resources you will be managing. Open your config/ColdBox.cfc or your Module's router:

resources( "settings" )
    .resources( "users" )
    .resources( "roles" );

This will generate all the resourceful routes as per the ColdBox Resources Routing:

Create the Handler

Now that your resources are created, create the handler that matches the resource. You can use CommandBox: coldbox create handler settings or just create the file manually. Make sure it extends our cborm resource handler: cborm.models.resources.BaseHandler

/**
 * Manage settings
 */
component extends="cborm.models.resources.BaseHandler"{

	// DI
	property name="ormService" inject="SettingService";

	// The default sorting order string: permission, name, data desc, etc.
	variables.sortOrder 	= "name";
	// The name of the entity this resource handler controls. Singular name please.
	variables.entity 		= "Setting";

}

Generated Actions

The base resource handler will generate the following methods for you that will match the routes that you registered via the resources() method.

ActionHTTP MethodPurposeRouteThrows
index()GETList resources/{resource}none
create()POSTCreate a resource/{resource}Validation
show()GETGet one resource/{resource}/:idEntityNotFound
update()PUT/PATCHUpdate a resource/{resource}/:idEntityNotFound, Validation
delete()DELETEDelete a resource/{resource}/:idEntityNotFound

Consistent Output

REST is all about uniformity and consistency. The output packet produced by any of the actions you create or use, will adhere to the following schema:

{
    "data": "", // The output of your actions: struct, string, array, etc
    "error": false, // If an error ocurred
    "pagination": { // Pagination information
        "totalPages": 1,
        "maxRows": 25,
        "offset": 0,
        "page": 1,
        "totalRecords": 5
    },
    "messages": [] // Info messages
}

Action Parameters

Each action can take in not only the incoming parameters that your entities require for population and saving, but also we have added a few to help you:

Index()

ParameterTypeDefaultDescription
includesstringemptyOne or a list of properties you want to include in the output packet apart from the default Includes defined in your entity. Adheres to the mementifier
excludesstringemptyOne or a list of properties you want to exclude in the output packet apart from the default excludes defined in your entity. Adheres to the mementifier
ignoreDefaultsbooleanfalseIf true, it will ignore all default includes and excludes and ONLY use the includes and excludes you pass. Adheres to the mementifier
sortOrderstringvariables.sortOrderThe sort ordering you want to apply to the result set. Adheres to the criteria query sort() method
pagenumeric1Pagination is included, so you can pass in the page of records you would like to visualize.

Create()

ParameterTypeDefaultDescription
includesstringemptyOne or a list of properties you want to include in the output packet apart from the default Includes defined in your entity. Adheres to the mementifier
excludesstringemptyOne or a list of properties you want to exclude in the output packet apart from the default excludes defined in your entity. Adheres to the mementifier
ignoreDefaultsbooleanfalseIf true, it will ignore all default includes and excludes and ONLY use the includes and excludes you pass. Adheres to the mementifier

Show()

ParameterTypeDefaultDescription
includesstringemptyOne or a list of properties you want to include in the output packet apart from the default Includes defined in your entity. Adheres to the mementifier
excludesstringemptyOne or a list of properties you want to exclude in the output packet apart from the default excludes defined in your entity. Adheres to the mementifier
ignoreDefaultsbooleanfalseIf true, it will ignore all default includes and excludes and ONLY use the includes and excludes you pass. Adheres to the mementifier
idnumeric0The incoming ID of the resource to show

Update()

ParameterTypeDefaultDescription
includesstringemptyOne or a list of properties you want to include in the output packet apart from the default Includes defined in your entity. Adheres to the mementifier
excludesstringemptyOne or a list of properties you want to exclude in the output packet apart from the default excludes defined in your entity. Adheres to the mementifier
ignoreDefaultsbooleanfalseIf true, it will ignore all default includes and excludes and ONLY use the includes and excludes you pass. Adheres to the mementifier
idnumeric0The incoming ID of the resource to show

Delete()

ParameterTypeDefaultDescription
idnumeric0The incoming ID of the resource to show

Overriding Actions

You can also override the actions we give you so you can spice them up as you see fit. A part from calling the super class to finalize your REST call, we have allso added some extra arguments to your base actions so you can have fine-grained control of populations, validations, querying and much more.

/**
 * Display all clientNotes by creating my own criteria object
 * GET /api/v1/clientNotes
 *
 * @override
 */
function index( event, rc, prc ){
	// Criterias and Filters
	param rc.sortOrder 			= "createdDate desc";
	param rc.page 				= 1;
	param rc.isActive 			= true;
	param rc.clientId 			= "";
	param rc.creatorId 			= "";

	// Build up a search criteria and let the base execute it
	arguments.criteria = newCriteria()
		.eq( "isActive", autoCast( "isActive", rc.isActive )  )
		.when( len( rc.clientId ), (c) => {
			c.eq( "client.clientId", rc.clientId );
		} )
		.when( len( rc.creatorId ), (c) => {
			c.eq( "creator.userId", rc.creatorId );
		} );

	super.index( argumentCollection=arguments );
}

/**
 * Display all employees using my own orm service search which
 * must return a struct of 
 * - count : The number of records
 * - records : The array of objects
 * GET /api/v1/employees
 *
 * @override
 */
function index( event, rc, prc ){
	// Criterias and Filters
	param rc.search 			= "";
	param rc.sortOrder 			= "lname";
	param rc.page 				= 1;
	param rc.isActive 			= true;
	param rc.hasPayroll 		= false;
	param rc.managerId 			= "";
	param rc.roleId				= "";

	// search
	arguments.results = variables.ormService.search(
		"searchTerm"	= rc.search,
		"sortOrder" 	= rc.sortOrder,
		"isActive"		= rc.isActive,
		"hasPayroll"	= rc.hasPayroll,
		"roleId"		= rc.roleId,
		"managerId"		= rc.managerId,
		"offset" 		= getPageOffset( rc.page ),
		"max" 			= getMaxRows()
	);

	// response
	super.index( argumentCollection=arguments );
}

index() Arguments

Parameter Type Default Description
criteria Criteria null You can pass your own criteria object that we will use to execute to retrieve the records
results struct { count:0, records:[] }

If you pass in a results struct, then you did the search and we will just marshall the results using pagination. The struct must contain the following:

  • count : The records found
  • records : The array of entities
### create\(\) Arguments
ParameterTypeDefaultDescription
populatestruct{}The arguments you want to send into the populateModel() method alongside the entity that's being created.
validatestruct{}The arguments you want to send into the validateOrFail() method alongside the entity that's being validated.

update() Arguments

ParameterTypeDefaultDescription
populatestruct{}The arguments you want to send into the populateModel() method alongside the entity that's being updated.
validatestruct{}The arguments you want to send into the validateOrFail() method alongside the entity that's being updated.

Happy Coding!