Skip to content

RFC: A tool that helps enforce consistency & rigor in responses #419

@atuttle

Description

@atuttle

A problem I've seen consistently throughout my time using Taffy and helping others is that we all —myself included— struggle to manually make our API responses consistent with themselves. E.g. /customers/1 and /customers both return customer data, but one might use firstName while the other uses first_name, because CFML's weak typing gives us plenty of opportunities to do the wrong thing.

I do not want to create something that feels like an ORM.

But it would be really nice to have something like this:

var customers = queryExecute("select firstname, lastname, email, id from customers where active = 1");

return rep(
	queryToArrayOf( 
		data: customers,
		type: types.customer,
		fields: [ "firstName", "lastName", "email", "id" ],
		callback: function(record){
			//you could manually massage records in here like you currently can with queryToArray
			return record;
		}
	)
);

👆🏻This assumes some sort of "Customer" type already exists, and understands how to pull the appropriate data from the query.

That's about as far as I've been able to develop this thought in my head so far. I will have the availability to spend time on this effort at work in the coming months, so now is a great time to get your ideas and feedback in so that they can be considered for the effort.

The proposal above should also work reasonably well for cases where you've got nested types.

var customers = queryExecute("select firstname, lastname, email, id from customers where active = 1");
var orders = queryExecute("
	select id, orderDateTime, itemCount, orderTotalCents 
	from orders 
	where customerId in (:idList)
"), {
	idList: { cfsqltype: "numeric", list: true, value: valueArray( customers.id ) }
});

return rep(
	queryToArrayOf( 
		data: customers,
		type: types.customer,
		fields: [ "firstName", "lastName", "email", "id" ],
		callback: function(record){
			record['orders'] = queryToArrayOf(
				data: 
					// use QoQ to pull each customer's orders from the shared order lookup
					queryExecute(
						"select * from orders where customerId = :id", 
						{ id: { cfsqltype: "numeric", value: record.id } },
						{ dbtype: "query" }
					),
				type: types.order,
				fields: [ "orderDateTime", "itemCount", "orderTotalCents" ]
			);
			return record;
		}
	)
);

We may not need the fields argument. Each type should be aware of all possible fields it would contain, and capable of loading them from queries by column name. If a column is missing from the query, it should be excluded from the result object.

The returned objects would be simple structs, with the properties defined by the types. The type class would enforce key name CaSe sEnSiTiViTy. So if you don't want a key in the result, don't select it in the query. I guess there might be a use case where you'll want to select more columns than you want in the result, so we'll make the fields argument optional.

You might have special handling you want to do for certain columns. E.g. if the column stores json data and you want it represented as real objects in the result, your implementation might look like this:

function parseRow( row ) {
	row['metadata'] = deserializeJson( row.metadata );
}

Given that you can specify custom functionality like this at the type-class level, do we need to leave leeway for you to add/override column type handlers at the resource-implementation level?

var customers = queryExecute("select firstname, lastname, email, id from customers where active = 1");

return rep(
	queryToArrayOf( 
		data: customers,
		type: types.customer,
		fields: [ "firstName", "lastName", "email", "id" ],
		customize: {
			email: function(e) { return lcase( arguments.e ); }
		}
	)
);

In addition to enforcing consistency in our outputs, other benefits of defining your types separately would include:

  • Ability to generate a data dictionary, so your consumers could know that a customer first name is a string with a max length of 100 characters
  • Maybe we could use this to enforce input formatting, if you're accepting complex inputs?
  • Can you think of more?

This is where you come in

  • What are your thoughts after reading the above?
  • What ideas do you have to make this even better?
  • What obvious flaws have I overlooked?
  • What use-cases won't this approach work for?

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions