cffractal 🚀

🚀

All Contributors

Master Branch Build Status

Transform business models to JSON data structures. Based on the Fractal PHP library.

Why CFFractal?

CFFractal in two sentences:

Views take models and return HTML. CFFractal takes models and returns JSON (or your favorite serialized format).

You would want to use CFFractal if:

  • You need to transform your business models to json in many different places.
  • You need to include and exclude relationships depending on the endpoint.
  • You don't want to repeat yourself all over the place.

Take a look at some example code:

// `books` can be anything from simple strings to structs to complex CFCs.

fractal
    .createData(
        resource = fractal.collection(
            data = books,
            transformer = getInstance( "BookTransformer" )
        ),
        includes = "author"
    )
    .convert();

// or

fractal
    .builder()
    .collection( books )
    .withTransformer( "BookTransformer" )
    .withIncludes( "author" )
    .convert();

Here's some more in-depth reasons:

  1. Conventions around nested resources

CFFractal is able to parse your includes and excludes for you. That means no more having to litter your transformers with if statements. No more huge parameter lists. Just one argument to pass: includes. We'll handle the rest.

Also, includes are in the hands of the caller. Does the caller not want the user? Great. Don't include it. Do they want 6 levels of nested relationships? Okay. We'll do it. Ultimate flexibility.

  1. Default includes

Some includes are opt-in. Others should always be included. CFFractal makes this easy while still delegating transformation to each resource.

  1. Nested includes

Do you want to get your post's author's comments? No worries! Your includes string can get that with includes=author.comments. Have more levels? Have at it!

  1. Sharing resource transformations

Adding a field to an entity? If you used CFFractal, you only need to add it to your transformer. Every included resource will get it automatically.

You have a user entity that you want to serialize, but not ever with the password? Just exclude it in your transform() function. All calls to CFFractal, including nested includes, will benefit from your attention to security for free.

  1. Consistency

It is frustrating as a consumer of an API to have to make multiple requests where one should have sufficed. One situation where this is sadly the case is inconsistent output between resources. Does the user struct include the last_logged_in key when requesting from one endpoint but not another? It is an easy mistake to make. CFFractal helps to reduce these mistakes by creating a single source of truth for resource transformations — a Transformer.

Another place that is easy to mess up consistency is the response output. Is your data nested in a data key? Are you separating resources into their primary keys and a map? It's too easy to have one endpoint behave differently than another. In CFFractal, Serializers define the structure of the response and can help ensure a consistent output across your API endpoints.

  1. Encapsulation

Playing with your model formats or your data layer code? It won't affect your API if you used CFFractal. Your Transformers are your single source of truth for model transformations. If changes need to be made, they are encapsulated in those files. Your API output will be unchanged from the consumer's point of view.

  1. Flexible

From Jon Clausen:

Having written API’s against MongoDB, ORM, and even a few that use legacy DAO/Gateway patterns, the benefit for me of the fractal transformation module is that I can use it for any of them, because of the transformer. If the models already know how to accomplish the transformations to a degree, like our mementos in ORM, then that’s great.

You don’t always have that, though, and you don’t always want a fully normalized expansion. Sometimes you need only a small subset when dealing with secondary one-to-one relationships that need some normalization of the top level element.

This eliminates three methods that I always found myself writing (or copy/pasting and adapting) for every API handler:

  1. The collection marshalling method
  2. The single entity response marshalling method
  3. The format entity method, which handles the expansion parameters in the collection

Examples

Simple Example

var fractal = new cffractal.models.Manager(
    itemSerializer = new cffractal.models.serializers.SimpleSerializer(),
    collectionSerializer = new cffractal.models.serializers.ResultsMapSerializer()
);

var book = {
    id = 1,
    title = "To Kill A Mockingbird",
    author = "Harper Lee",
    author_birthyear = "1926"
};

var resource = fractal.item( book, function( book ) {
    return {
        id = book.id,
        title = book.title,
        author = {
            name = book.author,
            year = book.author_birthyear
        },
        links = {
            uri = "/books/" & books.id
        }
    };
} );

var transformedData = fractal.createData( resource ).toJSON();

// {"data":{"id":1,"title":"To Kill A Mockingbird","author":{"name":"Harper Lee","year":"1926"},"links":{"uri":"/books/1"}}}

API

Manager

The manager class is responsible for kicking off the transformation process. This is generally the only class you need to inject in you handlers. For convenience, this class is usually aliased as fractal.

property name="fractal" inject="[email protected]";

There are three main functions to call off of fractal. The first two are factory functions: item and collection. These help you create fractal resources from your models, specify transformers, and set a serializer override for items and collections. (You can read all about those terms further down.)

item

Creates a new item resource.

NameTypeRequiredDefaultDescription
dataanytrueThe data or model to serialize with fractal.
transformerAbstractTransformer or closuretrueThe class or closure that will transform the given data
serializerSerializerfalseThe default item serializer for the ManagerThe serializer for the transformed data. If not provided, uses the default item serializer set for the Manager.
collection

Creates a new collection resource.

NameTypeRequiredDefaultDescription
dataanytrueThe data or model to serialize with fractal.
transformerAbstractTransformer or closuretrueThe class or closure that will transform the given data
serializerSerializerfalseThe default collection serializer for the ManagerThe serializer for the transformed data. If not provided, uses the default collection serializer set for the Manager.

Once you have a resource, you need to create the root scope. Scopes in cffractal represent the nesting level of the resource. Since resources can include sub-resources, we need a root scope to kick off the serializing process. You do this through the createData method.

createData

Creates a scope to serialize.

NameTypeRequiredDefaultDescription
resourceAbstractResourcetrueThe fractal resource to serailize with fractal.
includesstringfalse"" (empty string)A list of includes identifiers for the serialization.
identifierstringfalse"" (empty string)The identifier for the current scope. Defaults to "" (empty string), also know as the root scope.

The return value is a Scope object. To finish up the serialization process, we need to call convert or toJSON on this object. But before we get to that, let's review the options that go in to the serialization process.

Serializers

A Serializer is responsible for the shape of the response, both the data and the metadata, additional information about the item or collection (such as pagination or links).

Perhaps you always want your data nested under a data key for consistency. Maybe you want to separate the results as an array of ids from the resultsMap which is the data keyed by the id. You might want a metadata key always present for any additional information, like pagination, that doesn't fit inside the normal data keys. Whatever the shape, you can design a serializer that can produce it.

A serializer needs four methods:

data

Serializes the data portion of a resource.

NameTypeRequiredDefaultDescription
resourceAbstractResourcetrueThe fractal resource to process and serialize.
scopeScopetrueThe current scope instance. Included to pass along to the resource during processing.
meta

Serializes the metadata portion of a resource.

NameTypeRequiredDefaultDescription
resourceAbstractResourcetrueThe fractal resource to process and serialize.
scopeScopetrueThe current scope instance. Included to pass along to the resource during processing.
scopeData

Decides how to nest the data under the given identifier.

NameTypeRequiredDefaultDescription
dataanytrueThe serialized data.
identifierstringtrueThe current identifier for the serialization process.
scopeRootKey

Decides which key to use (if any) for the root of the serialized data.

NameTypeRequiredDefaultDescription
dataanytrueThe serialized data.
identifierstringtrueThe current identifier for the serialization process.

A default serializer is configured for the application when creating the Fractal manager for both items and collections. These can be configured separately Unless overridden, this is the serializer used for each scope in the serialization processes.

The current serializer for the Manager can be retrieved at any time by calling getItemSerializer and getCollectionSerializer. Additionally, a new default serializer can be set on the Manager by calling either setItemSerializer or setCollectionSerializer.

getItemSerializer

Retrieves the current default item serializer.

NameTypeRequiredDefaultDescription
No arguments
setItemSerializer

Sets a new default item serializer for the Manager.

NameTypeRequiredDefaultDescription
serializerSerializertrueThe new default item serializer for the Manager.
getCollectionSerializer

Retrieves the current default collection serializer.

NameTypeRequiredDefaultDescription
No arguments
setCollectionSerializer

Sets a new default collection serializer for the Manager.

NameTypeRequiredDefaultDescription
serializerSerializertrueThe new default collection serializer for the Manager.

Also, serializers can be overridden on individual resources. The API for getting and setting serializers on resources is the same as it is for the Manager, but is just getSerializer and setSerializer, respectively. A custom serializer can also be passed as the third argument to the item and collection factory functions.

function includeNotes( task ) {
    return collection(
        data = task.getNotes(),
        transformer = function( note ) {
            return note;
        },
        serializer = wirebox.getInstance( "[email protected]" )
    );
}

There are three serializers included out of the box with cffractal:

SimpleSerializer

The SimpleSerializer returns the processed resource data directly and nests the metadata under a meta key.

var model = {
    "foo" = "bar",
    "baz" = "qux"
};

// becomes

var transformed = {
    "foo" = "bar",
    "baz" = "qux",
    "meta" = {}
};

DataSerializer

The DataSerializer nests the processed resource data inside a data key and nests the metadata under a meta key. (These keys can be customized when initializing the object by passing in a dataKey and/or a metaKey value.)

var model = {
    "foo" = "bar",
    "baz" = "qux"
};

// becomes

var transformed = {
    "data" = {
        "foo" = "bar",
        "baz" = "qux"
    },
    "meta" = {}
};

ResultsMapSerializer

The ResultsMapSerializer nests the processed resource data inside a resultsMap struct keyed by an identifier column as well as an array of identifiers nested under a results key. The metadata is nested under a meta key.

If the processed resource is not an array, the data is returned unmodified.

The identifier column can be specified in the constructor.

var items = [
    { "id" = "F29958B1-5A2B-4785-BE0A11297D0B5373", "name" = "foo" },
    { "id" = "42A6EB0A-1196-4A76-8B9BE67422A54B26", "name" = "bar" }
];

// becomes

var transformed = {
    "results" = [
        "F29958B1-5A2B-4785-BE0A11297D0B5373",
        "42A6EB0A-1196-4A76-8B9BE67422A54B26"
    ],
    "resultsMap" = {
        "F29958B1-5A2B-4785-BE0A11297D0B5373" = {
            "id" = "F29958B1-5A2B-4785-BE0A11297D0B5373",
            "name" = "foo"
        },
        "42A6EB0A-1196-4A76-8B9BE67422A54B26" = {
            "id" = "42A6EB0A-1196-4A76-8B9BE67422A54B26",
            "name" = "bar"
        }
    },
    "meta" = {}
};

XMLSerializer

The XMLSerializer marshalls the data in to XML. It nests all items under a rootKey which can be specified in the constructor (default is root). Data is returned under either a data key or the root transformer's resourceKey, if defined. Meta is returned under a meta key. (This can be configured in the constructor with the metaKey argument.)

var model = {
    "foo" = "bar",
    "baz" = "qux"
};

// becomes

var transformed = "
    <root>
        <data>
            <foo>bar</foo>
            <baz>qux</baz>
        </data>
        <invalidTag></meta>
    </root>
";
Key Order in XML

By default, XMLSerializer explicitly sorts all keys alphabetically since the various CF engines behave differently with respect to struct key order. If you are using ordered structs or a LinkedHashMap and you need the output to reflect the incoming order of the keys, you can disable the alpha sort by passing sortKeys = false to the constructor of XMLSerializer and managing the order of the keys in your transformer.

API

init

Creates a new ResultsMapSerializer.

NameTypeRequiredDefaultDescription
identifierstringfalse"id"The name of the primary key for the transformed data. Used to key the results map and populate the results array.

Resources

Resources are a combination of your model, in whatever representation it may be in, and a transformer to take that data and normalize it for your API. Resources come in two flavors, items and collections. The API is identical for each. The difference comes down to how the data is processed and if pagination is considered.

You can create a resource either from the Manager or from inside a Transformer. The API is the same.

API

item

Creates a new Item resource.

NameTypeRequiredDefaultDescription
dataanytrueThe model to transform.
transformeranytrueThe transformer for the given model.
serializerSerializerfalseThe default item serializer on the Transformer or Manager.The serializer to use when serializing the data.
itemCallbackfunctionfalseAn optional callback to call after each item is serialized.
collection

Creates a new Collection resource.

NameTypeRequiredDefaultDescription
dataanytrueThe model to transform.
transformeranytrueThe transformer for the given model.
serializerSerializerfalseThe default collection serializer on the Transformer or Manager.The serializer to use when serializing the data.
itemCallbackfunctionfalseAn optional callback to call after each item is serialized.
Specifying Custom Serializers

As mentioned above, individual resources can have their serializer overridden. This is useful if you only want one scope level to be serialized in a certain fashion (say, with the DataSerializer), and the rest to be serialized differently (say, with the SimpleSerializer).

You can retrieve and set the custom serializers right from the resource.

getSerializer

Returns the current serializer for the resource.

NameTypeRequiredDefaultDescription
No arguments
setSerializer

Sets the serializer for the resource.

NameTypeRequiredDefaultDescription
serializerSerializertrueThe serializer to associate with this specific resource.
Transformer-level Serializers

Default serializers can also be set on the Transformer level. If one is set, it will be passed to the Manager if no resource-level serializer is passed. This allows you to specify a serializer for an entire transformer. The methods to set a serializer for a Transformer is the same as on the Manager. You can set the item and collection serializers individually (setItemSerializer and setCollectionSerializer) or you can set both at the same time (setSerializer).

Metadata

CFFractal has a convention for metadata that allows the resource to add metadata items individually that are later combined through a serializer. For instance, the SimpleSerializer adds all metadata fields directly on the transformed object. The DataSerializer instead nests all of the metadata under a meta key. (See the serializer section for more details.)

You can add metadata directly on a resource instance. The following metadata functions are available:

addMeta

Adds some data under a given identifier in the metadata.

NameTypeRequiredDefaultDescription
keystringtrueThe key to nest the data under in the metadata scope.
valueanytrueThe data to store under the given key.
hasMeta

Returns whether the resource has any metadata associated with it.

NameTypeRequiredDefaultDescription
No arguments.
getMeta

Returns the current metadata scope.

NameTypeRequiredDefaultDescription
No arguments.
Post-Transformation Callbacks

A powerful feature of CFFractal is the ability to add post-transformation callbacks that will fire after transforming each item. For an item resource, that means it will fire the callback once. For a collection resource, the callback will be fired for each item in the collection.

Here's an example of a post-transformation function:

// handlers/api/v1/books.cfc
component {

    this.API_BASE_URL = "/api/v1/books";

    function index( event, rc, prc ) {
        var books = fractal.collection(
            BookService.getAll(),
            function( book ) {
                return {
                    "id" = book.getId(),
                    "title" = book.getName(),
                    "yearPublished" = dateFormat( book.getPublishedDate(), "YYYY" )
                };
            }
        );

        books.addPostTransformationCallback( function( transformed, original, collection ) {
            transformed.href = this.API_BASE_URL & "/" & transformed.id;
            return transformed;
        } );

        var scope = fractal.createData( books );

        prc.response.setData(
            scope.convert();
        );
    }

}

Using a post-transformation callback, we are able to encapsulate data about the API version without coupling it to the transformer itself. Sweet!

There are countless more usages here. The key thing to note is that the value returned from the callback becomes the new transformed item. The function API is as follows:

addPostTransformationCallback

Add a post transformation callback to run after transforming each item. The value returned from the callback becomes the transformed item.

NameTypeRequiredDefaultDescription
callbackCallabletrueA callback to run after the resource has been transformed. The callback will be passed the transformed data, the original data, and the resource object as arguments.

You can also specify post-transformation callbacks during resource creation as either the fourth argument or using the itemCallback argument name.

// handlers/api/v1/books.cfc
component {

    this.API_BASE_URL = "/api/v1/books";

    function index( event, rc, prc ) {
        var books = fractal.collection(
            resource = BookService.getAll(),
            transformer = function( book ) {
                return {
                    "id" = book.getId(),
                    "title" = book.getName(),
                    "yearPublished" = dateFormat( book.getPublishedDate(), "YYYY" )
                };
            },
            itemCallback = function( transformed, original, collection ) {
                transformed.href = this.API_BASE_URL & "/" & transformed.id;
                return transformed;
            }
        );

        var scope = fractal.createData( books );

        prc.response.setData(
            scope.convert();
        );
    }

}
Null Default Values

If the data of a resource is null or any item or include in the resource is null, CFFractal returns the Manager's nullDefaultValue. This value can be set and retrieved from the Manager as follows:

getNullDefaultValue

Returns the current null default value.

NameTypeRequiredDefaultDescription
No arguments
setNullDefaultValue

Sets the null default value for the manager.

NameTypeRequiredDefaultDescription
nullDefaultValueanytrueThe null default value for all resources.

Additionally, this will be automatically configured for you in ColdBox to an empty string (""). This can be overridden using the nullDefaultValue setting in your config/ColdBox.cfc:

function configure() {
    moduleSettings = {
        cffractal = {
            nullDefaultValue = {}
        }
    };
}

Transformers

Transformers are like the view for your models. It defines how to transform your model in to a serializable representation.

Transformers come in two flavors, closures and components.

Closure Transformers

Closure transformers are useful for simple transformations. They are very convenient when you don't need any of the extra features of component transformers such as parsing includes and excludes because they are defined inline and very lightweight.

fractal.item( book, function( book ) {
    return {
        "id" = book.getId(),
        "title" = book.getName(),
        "yearPublished" = book.getPublishedDate()
    };
} );

If you use a resource in more than one place or would like access to includes and excludes, you are going to want to use a component transformer.

Component Transformers

Component transformers are where the power of transformers lie and will likely be the main transformer type for your API.

Component transformers should be singleton objects that extend cffractal.models.transformers.AbstractTransformer. Mark them as such in your DI container of choice. With WireBox, it is as simple as appending the singleton component metadata attribute.

component extends="cffractal.models.transformers.AbstractTransformer" singleton {
    function transform( book ) {
        return {
            "id" = book.getId(),
            "title" = book.getName(),
            "yearPublished" = book.getPublishedDate()
        };
    }
}

You might be thinking that this is no better than a closure transformer. The fact is, even in this state, we can now reuse this transformer without duplication! For this reason alone, you can see why most of your transformers will be components.

Component transformers get even better when we talk about includes and excludes!

Includes

Includes are nested resources that can or should be included when serializing a resource. Let's take our book example.

A book is written by an author and has a publisher. The author, in turn, has a country they live in.

In our API endpoint, retrieving a book should always return information about the author. It should optionally return information about the publisher as well as the author's country.

Let's set up all the models we've talked about. (This will be our most comprehensive example yet.)

component name="Book" accessors="true" {

    property name="id";
    property name="name";
    property name="publishedDate";

    property name="authorId";
    property name="publisherId";

    // we'll assume these methods do the right thing...
    function getAuthor() { /* ... */ }
    function getPublisher() { /* ... */ }

}

component name="Author" accessors="true" {

    property name="id";
    property name="firstName";
    property name="lastName";

    property name="currentCountryId";

    // we'll assume these methods do the right thing...
    function getCurrentCountry() { /* ... */ }

}

component name="Publisher" accessors="true" {

    property name="id";
    property name="name";

}

component name="Country" accessors="true" {

    property name="id";
    property name="name";
    property name="latitude";
    property name="longitude";

}

It doesn't matter how these models are populated or how they find their relations. That's why the Transformer pattern is so powerful! Let's set up our transformers now so we can see how includes work.

component name="BookTransformer" extends="cffractal.models.transformers.AbstractTransformer" singleton {

    variables.defaultIncludes = [ "author" ];
    variables.availableIncludes = [ "publisher" ];

    function transform( book ) {
        return {
            "id" = book.getId(),
            "title" = book.getName(),
            "yearPublished" = dateFormat( book.getPublishedDate(), "YYYY" )
        };
    }

    function includeAuthor( book ) {
        return item(
            book.getAuthor(),
            wirebox.getInstance( "AuthorTransformer" )
        );
    }

    function includePublisher( book ) {
        return item(
            book.getPublisher(),
            wirebox.getInstance( "PublisherTransformer" )
        );
    }

}

component name="AuthorTransformer" extends="cffractal.models.transformers.AbstractTransformer" singleton {

    variables.availableIncludes = [ "country" ];

    function transform( author ) {
        return {
            "id" = author.getId(),
            "name" = author.getFirstName() & " " & author.getLastName();
        };
    }

    function includeCountry( author ) {
        return item(
            author.getCurrentCountry(),
            function( country ) {
                return {
                    "id" = country.getId(),
                    "name" = country.getName(),
                    "coordinate" = {
                        "latitude" = country.getLatitude(),
                        "longitude" = country.getLongitude()
                    }
                };
            }
        );
    }

}

component name="PublisherTransformer" extends="cffractal.models.transformers.AbstractTransformer" singleton {

    function transform( publisher ) {
        return {
            "id" = publisher.getId(),
            "name" = publisher.getName()
        };
    }

}

Whew. That may seem like a lot of transformers to write, but remember that this is both insulating us from changes to our model layer while at the same time reducing future duplication. We can now write our authors endpoint while reusing our existing AuthorsTransformer. Neat!

On to includes. There are two types of includes: defaultIncludes and availableIncludes. Both of these arrays contain resource names. During the transformation process, CFFractal will invoke a include[Resource Name Here] method on the transformer to retrieve the included data.

If you always want a related resource included, you want to specify it in your defaultIncludes array. Why would you go to the trouble of specifying a resource in the defaultIncludes array as opposed to doing it inline? Because defaultIncludes reuse existing transformers to do their transformation and serialization. We once again get to reuse our transformation layer with little additional effort.

If you want a resource to be available to include in your transformation if a caller desired, but not included by default, add it to your availableIncludes array. This grants you the flexibility to define all the relationships and nested resources in the transformer while only loading them as needed. How to include available includes will be seen in detail in the Scope section.

You might have noticed that there is no CountryTransformer component. Rather, we opted for a closure component for the Country resource. This probably isn't the right choice for our situation, but we opted for it here to show you that it is a possibility. Including it here as a closure component would mean any logic for transforming a Country outside of the AuthorTransformer component would have to be duplicated.

In the end, we get a flexible API call. If we set up our objects like this:

var book = new Book( {
    id = 1,
    name = "To Kill A Mockingbird",
    publishedDate = createDate( 1960, 07, 11 ),
    authorId = 54,
    publisherId = 41
} );

var author = new Author( {
    id = 54,
    firstName = "Harper",
    lastName = "Lee",
    countryId = 50
} );

var publisher = new Publisher( {
    id = 41,
    name = "J. B. Lippincott & Co."
} );

var country = new Country( {
    id = 50,
    name = "United States of America"
    latitude = "38.895N",
    longitude = "77.037W"
} );

var resource = fractal.item(
    book,
    wirebox.getInstance( "BookTransformer" )
);

With just a base call to the Manager's createData method:

var transformedData = fractal.createData( resource ).toJSON();

We get the following response:

{
    "data": {
        "id": 1,
        "title": "To Kill a Mockingbird",
        "yearPublished": "1960",
        "author": {
            "data": {
                "id": 54,
                "name": "Harper Lee"
            }
        }
    }
}

However, with the same resource but also adding in our includes:

var transformedData = fractal
    .createData(
        resource = resource,
        includes = "author.country,publisher"
    )
    .toJSON();

We get a more in-depth response:

{
    "data": {
        "id": 1,
        "title": "To Kill a Mockingbird",
        "yearPublished": "1960",
        "author": {
            "data": {
                "id": 54,
                "name": "Harper Lee",
                "country": {
                    "data": {
                        "id": 50,
                        "name": "United States of America",
                        "coordinates": {
                            "latitude": "38.895N",
                            "longitude": "77.037W"
                        }
                    }
                }
            }
        },
        "publisher": {
            "data": {
                "id": 41,
                "name": "J. B. Lippincott & Co."
            }
        }
    }
}

Includes can also handle simple data columns that are optional in your default payload. Your include method can return the simple value directly.

Let's say in our example above we wanted pulbishedDate to be an available include, that is, not included by default in our payload.

component name="Book" accessors="true" {

    property name="id";
    property name="name";
    property name="publishedDate";

    property name="authorId";
    property name="publisherId";

    // we'll assume these methods do the right thing...
    function getAuthor() { /* ... */ }
    function getPublisher() { /* ... */ }

}

component name="BookTransformer" extends="cffractal.models.transformers.AbstractTransformer" singleton {

    variables.defaultIncludes = [ "author" ];
    variables.availableIncludes = [ "publisher", "yearPublished" ];

    function transform( book ) {
        return {
            "id" = book.getId(),
            "title" = book.getName()
        };
    }

    function includeAuthor( book ) {
        return item(
            book.getAuthor(),
            wirebox.getInstance( "AuthorTransformer" )
        );
    }

    function includePublisher( book ) {
        return item(
            book.getPublisher(),
            wirebox.getInstance( "PublisherTransformer" )
        );
    }

    function includeYearPublished( book ) {
        return dateFormat( book.getPublishedDate(), "dd mmm yyyy" );
    }

}

Now our default response looks like this:

{
    "data": {
        "id": 1,
        "title": "To Kill a Mockingbird",
        "author": {
            "data": {
                "id": 54,
                "name": "Harper Lee"
            }
        }
    }
}

And passing yearPublished as an include gives us this:

var transformedData = fractal
    .createData(
        resource = resource,
        includes = "yearPublished"
    )
    .toJSON();
{
    "data": {
        "id": 1,
        "title": "To Kill a Mockingbird",
        "yearPublished": "1960",
        "author": {
            "data": {
                "id": 54,
                "name": "Harper Lee"
            }
        }
    }
}

CFFractal will also strip out any unspecified availableIncludes from your serialized output. With regards to the example above, if you did not specify yearPublished in the includes, but your transform method returned it anyway, CFFractal would remove the column before returning it to you. That means that even with the following transformer:

component name="BookTransformer" extends="cffractal.models.transformers.AbstractTransformer" singleton {

    variables.defaultIncludes = [ "author" ];
    variables.availableIncludes = [ "publisher", "yearPublished" ];

    function transform( book ) {
        return {
            "id" = book.getId(),
            "title" = book.getName(),
            "yearPublished" = dateFormat( book.getPublishedDate(), "dd mmm yyyy" )
        };
    }

    function includeAuthor( book ) {
        return item(
            book.getAuthor(),
            wirebox.getInstance( "AuthorTransformer" )
        );
    }

    function includePublisher( book ) {
        return item(
            book.getPublisher(),
            wirebox.getInstance( "PublisherTransformer" )
        );
    }

    function includeYearPublished( book ) {
        return dateFormat( book.getPublishedDate(), "dd mmm yyyy" );
    }

}

And calling CFFractal with no includes:

var transformedData = fractal.createData(resource).toJSON();

You would not see the yearPublished key in the response:

{
    "data": {
        "id": 1,
        "title": "To Kill a Mockingbird",
        "author": {
            "data": {
                "id": 54,
                "name": "Harper Lee"
            }
        }
    }
}
Excludes

Excludes are the flip side of includes. They are used when you want to exclude something that is included by default. (It doesn't make too much sense to exclude an available include.) There are no methods to write for excludes. They operate on the transformers to both skip default includes and to remove keys from the transformed output.

Taking a look at our book example:

component name="BookTransformer" extends="cffractal.models.transformers.AbstractTransformer" singleton {

    variables.defaultIncludes = [ "author" ];
    variables.availableIncludes = [ "publisher", "yearPublished" ];

    function transform( book ) {
        return {
            "id" = book.getId(),
            "title" = book.getName(),
            "yearPublished" = dateFormat( book.getPublishedDate(), "dd mmm yyyy" )
        };
    }

    function includeAuthor( book ) {
        return item(
            book.getAuthor(),
            wirebox.getInstance( "AuthorTransformer" )
        );
    }

    function includePublisher( book ) {
        return item(
            book.getPublisher(),
            wirebox.getInstance( "PublisherTransformer" )
        );
    }

    function includeYearPublished( book ) {
        return dateFormat( book.getPublishedDate(), "dd mmm yyyy" );
    }

}

We can create our fractal object by passing in a list of excludes:

var transformedData = fractal.createData(
    resource = resource,
    excludes = "author"
).toJSON();

And the resulting response would not contain the author key in the response. In fact, the includeAuthor method was never even called.

{
    "data": {
        "id": 1,
        "title": "To Kill a Mockingbird"
    }
}

Nested excludes do not affect their parent scopes as includes do. This means that

var transformedData = fractal.createData(
    resource = resource,
    excludes = "author.id"
).toJSON();

Does not exclude the author entirely, but rather removes a key from the transformed author response.

{
    "data": {
        "id": 1,
        "title": "To Kill a Mockingbird",
        "author": {
            "data": {
                "name": "Harper Lee"
            }
        }
    }
}

As seen in the example above, excludes can also target normal properties of the transformed response to remove. They do not have to be includes.

Includes and Excludes in Transformers

Both types of transformers have access to the includes and excludes of the currently executing scope as well as the includes and excludes of the entire transformation. These are passed as arrays of scopes to the next four arguments to a transformer and each include method.

component name="BookTransformer" extends="cffractal.models.transformers.AbstractTransformer" singleton {

    variables.defaultIncludes = [ "author" ];

    function transform( book, scopedIncludes, scopedExcludes, allIncludes, allExcludes ) {
        return {
            "id" = book.getId(),
            "title" = book.getName(),
            "yearPublished" = dateFormat( book.getPublishedDate(), "dd mmm yyyy" )
        };
    }

    function includeAuthor( book, scopedIncludes, scopedExcludes, allIncludes, allExcludes ) {
        return item(
            book.getAuthor(),
            wirebox.getInstance( "AuthorTransformer" )
        );
    }

}

Scoped includes and excludes are ones that are pertinent to the current level of transformation. You won't find any nested scopes in those arguments. For instance, with a includes string of author.name, the AuthorTransformer would be passed [ "name" ] in its scoped includes.

Custom Managers

By default the "Manager" of your transformer defaults to [email protected]. A setManager() method is available in your transformers, which allows you to specific a custom manager. This manager, however, must implement the methods found in cffractal.models.Manager.

Scope

Scope is the last piece of the CFFractal puzzle. A Scope is a resource combined with any includes and a scope identifier. The scope identifier represents the current nesting level of the resource transformation. A scope identifier of "" (an empty string) represents the root level of the resource. As includes are processed, additional scopes are created with the correctly scoped includes to continue the transformation and serialization processes.

This concept is especially important for nested includes. In the example right before this section, there was an example of a nested include:

var transformedData = fractal
    .createData(
        resource = resource,
        includes = "author.country"
    )
    .toJSON();

This is asking CFFractal to include the resource's author and that author's country. In fact, author doesn't even need to be in the defaultIncludes array to be included in this case. Included a nested resource will include all of it's parent resources as well. (It does, however, at least need to be in the availableIncludes array to do anything.)

Let's step through this example to understand how it works.

  1. The resource tries to resolve the "author" include.
  2. It finds it on the BookTransformer and creates a new resource and embeds it in a new child scope where the scope identifier is "author", the current include name.
  3. The includes are then evaluated against the current scope identifier. While "country" is not a valid include from the root scope ("book"), in this child scope ("author") "country" is a valid include.
  4. "country" is processed under a further nested child scope with a scope identifier of "country".
  5. As this is the last step of the includes chain, each child scope is transformed, serialized, and then placed inside a key matching the scope identifier in its parent scope.

Scopes, while important, are mostly invisible in the CFFractal process. The root scope is created by the initial call to createData and child scopes are created inside the transformation process for you. Still, it is important to visualize the includes chain to help you when designing your transformers.

Builder

It is important to know the piece of CFFractal so you can use the full power of the library. But it can seem a bit verbose. To help alleviate this, CFFractal has a Builder class to help reduce boilerplate by leveraging conventions. (You also might find that you just like the syntax better.)

The Builder component turns code like this:

var fractal = new cffractal.models.Manager(
    itemSerializer = new cffractal.models.serializers.SimpleSerializer(),
    collectionSerializer = new cffractal.models.serializers.ResultsMapSerializer()
);

var book = {
    id = 1,
    title = "To Kill A Mockingbird",
    author = "Harper Lee",
    author_birthyear = "1926"
};

var resource = fractal.item( book, function( book ) {
    return {
        id = book.id,
        title = book.title,
        author = {
            name = book.author,
            year = book.author_birthyear
        },
        links = {
            uri = "/books/" & books.id
        }
    };
} );

var transformedData = fractal.createData( resource ).toJSON();

// {"data":{"id":1,"title":"To Kill A Mockingbird","author":{"name":"Harper Lee","year":"1926"},"links":{"uri":"/books/1"}}}

into code like this:

var fractal = new cffractal.models.Manager(
    itemSerializer = new cffractal.models.serializers.SimpleSerializer(),
    collectionSerializer = new cffractal.models.serializers.ResultsMapSerializer()
);

var book = {
    id = 1,
    title = "To Kill A Mockingbird",
    author = "Harper Lee",
    author_birthyear = "1926"
};

var result = fractal.builder()
    .item( book )
    .withTransformer( "BookTransformer" )
    .withSerializer( "SimpleSerializer" )
    .withIncludes( "author" )
    .convert();

// {"data":{"id":1,"title":"To Kill A Mockingbird","author":{"name":"Harper Lee","year":"1926"},"links":{"uri":"/books/1"}}}

The Builder component uses the same API under the hood, but you may find the flow more to your sensibilities. Additionally, the withTransformer and withSerializer methods will look up simple strings as WireBox mappings, streamlining your code even more.

The Builder has the following methods:

API

item

Sets the Item resource to be transformed.

NameTypeRequiredDefaultDescription
dataanytrueThe model to transform.
collection

Sets the Collection resource to be transformed.

NameTypeRequiredDefaultDescription
dataanytrueThe model to transform.
withTransformer

Sets the transformer to use. If the transformer is a simple value, the Builder will treat it as a WireBox binding.

NameTypeRequiredDefaultDescription
transformeranytrueThe transformer to use.
withSerializer

Sets the serializer to use. If the serializer is a simple value, the Builder will treat it as a WireBox binding.

NameTypeRequiredDefaultDescription
serializeranytrueThe serializer to use.
withIncludes

Sets the includes for the transformation.

NameTypeRequiredDefaultDescription
includesanytrueThe includes for the transformation.
withExcludes

Sets the excludes for the transformation.

NameTypeRequiredDefaultDescription
excludesanytrueThe excludes for the transformation.
withMeta

Adds a key / value pair to the metadata for the resource.

NameTypeRequiredDefaultDescription
keystringtrueThe metadata key.
valueanytrueThe metadata value.
withPagination

Sets the pagination metadata for the resource.

NameTypeRequiredDefaultDescription
paginationanytrueThe pagination metadata.
withItemCallback

Add a callback to be called after each item is transformed.

NameTypeRequiredDefaultDescription
callbackCallabletrueThe callback to run after each item has been transformed. The callback will be passed the transformed data, the original data, and the resource object as arguments.
convert

Transforms the data using the set properties through the fractal manager.

NameTypeRequiredDefaultDescription
No arguments
toJSON

Transform the data through cffractal and then serialize it to JSON.

NameTypeRequiredDefaultDescription
No arguments

Additional Resources

Have Questions?

Come find us on the CFML Slack (#box-products channel) and ask us there. We'd be happy to help!

v7.0.0

22 Aug 2018 — 15:04: 57 UTC

BREAKING

  • Excludes: Add ability to specify excludes (d89fa06)

chore

  • ci: Replace flaky gpg key download with solid Ortus one (754f451)

feat

  • Transformers: Pass includes and excludes inside a transformer (03ac095)
  • Transformers: Automatically remove unused available includes from the serialized output (89f4938)
  • Transformers: Allow for Transformer-level item and collection serializers (448a07d)
  • Transformers: Add item callbacks when creating resources (5e00c82)
  • Transfomers: Simple values can be returned from includes (67c1507)

fix

  • Includes: Fix processing double includes (ff33af5)

other

  • *: Temporarily remove emoji due to ForgeBox support (9cc3e37)

Changelog

6.0.0

Breaking Changes

  • Custom Serializers now need to implement two additional methods:
    • scopeData — Decides how to nest the data under the given identifier. Most implementations will return the current data under the last identifier: { "#listLast( arguments.identifier, "." )#" = data };
    • scopeRootKey — Decides which key to use (if any) for the root of the serialized data.
  • Transformers can set a resourceKey property to be used if the transformer is the root transformer in certain serializers. (variables.resourceKey = "book";) For instance, this property is used in the XMLSerializer to set the root node name. If no resourceKey is set, or a callback transfomer is used, a default resourceKey of data will be used.

 

 
$ box install cffractal
  • Jul 20 2017 07:54 PM
  • Aug 22 2018 10:04 AM
  • 966
  • 0
  • 1020