Skip to content

Commit 4038dc2

Browse files
committed
Add named return formatter registry
1 parent a814e40 commit 4038dc2

7 files changed

Lines changed: 570 additions & 59 deletions

File tree

ModuleConfig.cfc

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ component {
1010
settings = {
1111
"defaultGrammar": "AutoDiscover@qb",
1212
"defaultReturnFormat": "array",
13+
"defaultReturnFormatOptions": {},
1314
"preventDuplicateJoins": false,
1415
"validateOperatorsAndCombinators": true,
16+
"validateQueryExecuteReturnType": false,
1517
"collectQueryLog": true,
1618
"convertEmptyStringsToNull": true,
1719
"validateQueryParamStructKeys": true,
@@ -29,7 +31,8 @@ component {
2931
},
3032
"shouldMaxRowsOverrideToAll": function( maxRows ) {
3133
return maxRows <= 0;
32-
}
34+
},
35+
"returnFormatters": {}
3336
};
3437

3538
interceptorSettings = { "customInterceptionPoints": "preQBExecute,postQBExecute" };
@@ -55,15 +58,29 @@ component {
5558
.initArg( name = "integerSQLType", value = settings.integerSQLType )
5659
.initArg( name = "decimalSQLType", value = settings.decimalSQLType );
5760

61+
binder
62+
.map( alias = "StructFormatter@qb", force = true )
63+
.to( "qb.models.Query.Formatters.StructFormatter" )
64+
.initArg( name = "utils", ref = "QueryUtils@qb" );
65+
66+
binder
67+
.map( alias = "ReturnFormatterRegistry@qb", force = true )
68+
.to( "qb.models.Query.ReturnFormatterRegistry" )
69+
.initArg( name = "utils", ref = "QueryUtils@qb" )
70+
.initArg( name = "returnFormatters", value = settings.returnFormatters );
71+
5872
binder
5973
.map( alias = "QueryBuilder@qb", force = true )
6074
.to( "qb.models.Query.QueryBuilder" )
6175
.initArg( name = "grammar", ref = settings.defaultGrammar )
6276
.initArg( name = "utils", ref = "QueryUtils@qb" )
77+
.initArg( name = "returnFormatterRegistry", ref = "ReturnFormatterRegistry@qb" )
6378
.initArg( name = "preventDuplicateJoins", value = settings.preventDuplicateJoins )
6479
.initArg( name = "validateOperatorsAndCombinators", value = settings.validateOperatorsAndCombinators )
80+
.initArg( name = "validateQueryExecuteReturnType", value = settings.validateQueryExecuteReturnType )
6581
.initArg( name = "collectQueryLog", value = settings.collectQueryLog )
6682
.initArg( name = "returnFormat", value = settings.defaultReturnFormat )
83+
.initArg( name = "defaultReturnFormatOptions", value = settings.defaultReturnFormatOptions )
6784
.initArg( name = "defaultOptions", value = settings.defaultOptions )
6885
.initArg( name = "sqlCommenter", ref = "ColdBoxSQLCommenter@qb" )
6986
.initArg( name = "shouldMaxRowsOverrideToAll", value = settings.shouldMaxRowsOverrideToAll );
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
component accessors="true" {
2+
3+
property name="utils";
4+
5+
public StructFormatter function init( any utils = new qb.models.Query.QueryUtils() ) {
6+
variables.utils = arguments.utils;
7+
return this;
8+
}
9+
10+
public function toFormatter( struct options = {} ) {
11+
var formatterOptions = arguments.options;
12+
return function( q ) {
13+
if (
14+
!formatterOptions.keyExists( "columnKey" ) || isNull( formatterOptions.columnKey ) || !len(
15+
formatterOptions.columnKey
16+
)
17+
) {
18+
throw(
19+
type = "MissingColumnKey",
20+
message = "A columnKey option is required for the [struct] return formatter."
21+
);
22+
}
23+
24+
return variables.utils.queryToStructOfStructs( q, formatterOptions.columnKey );
25+
};
26+
}
27+
28+
}

models/Query/QueryBuilder.cfc

Lines changed: 57 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ component displayname="QueryBuilder" accessors="true" {
2323
*/
2424
property name="returnFormat";
2525

26+
/**
27+
* Registry used to resolve named return formats.
28+
*/
29+
property name="returnFormatterRegistry";
30+
2631
/**
2732
* preventDuplicateJoins
2833
* If true, QB will introspect all existing JoinClauses for a match before creating a new join clause.
@@ -38,6 +43,13 @@ component displayname="QueryBuilder" accessors="true" {
3843
*/
3944
property name="validateOperatorsAndCombinators";
4045

46+
/**
47+
* If true, QB throws when queryExecute returntype options are passed.
48+
* If false, QB strips those options so return formatters always receive a query.
49+
* @default false
50+
*/
51+
property name="validateQueryExecuteReturnType";
52+
4153
/**
4254
* paginationCollector
4355
* A component or struct with a `generateWithResults` method.
@@ -289,11 +301,17 @@ component displayname="QueryBuilder" accessors="true" {
289301
* Default: qb.models.Query.QueryUtils
290302
* @returnFormat The closure (or string format shortcut) that modifies the query
291303
* and is eventually returned to the caller. Default: 'array'
304+
* @defaultReturnFormatOptions Options passed to the default return formatter.
305+
* Default: {}
306+
* @returnFormatterRegistry Registry used to resolve named return formatters.
292307
* @preventDuplicateJoins Whether QB should ignore a .join() statement that matches an existing join
293308
* Default: false
294309
* @validateOperatorsAndCombinators
295310
* Whether QB validates operators/combinators before storing clauses.
296311
* Default: true
312+
* @validateQueryExecuteReturnType
313+
* Whether QB throws when queryExecute returntype options are passed.
314+
* Default: false
297315
* @paginationCollector The closure that processes the pagination result.
298316
* Default: cbpaginator.models.Pagination
299317
* @columnFormatter The closure that modifies each column before being
@@ -315,8 +333,11 @@ component displayname="QueryBuilder" accessors="true" {
315333
grammar = new qb.models.Grammars.BaseGrammar(),
316334
utils = new qb.models.Query.QueryUtils(),
317335
returnFormat = "array",
336+
struct defaultReturnFormatOptions = {},
337+
returnFormatterRegistry,
318338
preventDuplicateJoins = false,
319339
validateOperatorsAndCombinators = true,
340+
validateQueryExecuteReturnType = false,
320341
paginationCollector = new cbpaginator.models.Pagination(),
321342
columnFormatter,
322343
parentQuery,
@@ -330,6 +351,11 @@ component displayname="QueryBuilder" accessors="true" {
330351

331352
setPreventDuplicateJoins( arguments.preventDuplicateJoins );
332353
setValidateOperatorsAndCombinators( arguments.validateOperatorsAndCombinators );
354+
setValidateQueryExecuteReturnType( arguments.validateQueryExecuteReturnType );
355+
if ( isNull( arguments.returnFormatterRegistry ) ) {
356+
arguments.returnFormatterRegistry = new qb.models.Query.ReturnFormatterRegistry( arguments.utils );
357+
}
358+
setReturnFormatterRegistry( arguments.returnFormatterRegistry );
333359
if ( isNull( arguments.columnFormatter ) ) {
334360
arguments.columnFormatter = function( column ) {
335361
return column;
@@ -341,7 +367,7 @@ component displayname="QueryBuilder" accessors="true" {
341367
setParentQuery( arguments.parentQuery );
342368
}
343369
param variables.defaultOptions = {};
344-
setReturnFormat( arguments.returnFormat );
370+
setReturnFormat( arguments.returnFormat, arguments.defaultReturnFormatOptions );
345371
mergeDefaultOptions( arguments.defaultOptions );
346372
setSqlCommenter( arguments.sqlCommenter );
347373
setCollectQueryLog( arguments.collectQueryLog );
@@ -4310,10 +4336,6 @@ component displayname="QueryBuilder" accessors="true" {
43104336
return;
43114337
}
43124338

4313-
if ( hasExplicitReturnType( arguments.options ) ) {
4314-
return q;
4315-
}
4316-
43174339
if ( isQuery( q ) ) {
43184340
return returnFormat( q );
43194341
}
@@ -4329,10 +4351,6 @@ component displayname="QueryBuilder" accessors="true" {
43294351
return { result: q.result, query: returnFormat( q.query ) };
43304352
}
43314353

4332-
private boolean function hasExplicitReturnType( required struct options ) {
4333-
return arguments.options.keyExists( "returntype" ) && !isNull( arguments.options.returntype );
4334-
}
4335-
43364354
/**
43374355
* Run a query through the specified grammar then clear all bindings.
43384356
*
@@ -4345,6 +4363,7 @@ component displayname="QueryBuilder" accessors="true" {
43454363
*/
43464364
private any function runQuery( required string sql, struct options = {}, string returnObject = "query" ) {
43474365
structAppend( arguments.options, getDefaultOptions(), false );
4366+
normalizeQueryExecuteReturnTypeOptions( arguments.options );
43484367
var bindings = getBindings( except = getAggregate().isEmpty() ? [] : [ "select" ] );
43494368

43504369
var result = grammar.runQuery(
@@ -4397,10 +4416,12 @@ component displayname="QueryBuilder" accessors="true" {
43974416
grammar = getGrammar(),
43984417
utils = getUtils(),
43994418
returnFormat = getReturnFormat(),
4419+
returnFormatterRegistry = getReturnFormatterRegistry(),
44004420
paginationCollector = isNull( variables.paginationCollector ) ? javacast( "null", "" ) : variables.paginationCollector,
44014421
columnFormatter = isNull( getColumnFormatter() ) ? javacast( "null", "" ) : getColumnFormatter(),
44024422
parentQuery = isNull( getParentQuery() ) ? javacast( "null", "" ) : getParentQuery(),
4403-
defaultOptions = getDefaultOptions()
4423+
defaultOptions = getDefaultOptions(),
4424+
validateQueryExecuteReturnType = getValidateQueryExecuteReturnType()
44044425
);
44054426
}
44064427

@@ -4520,27 +4541,19 @@ component displayname="QueryBuilder" accessors="true" {
45204541
* Alternative, the return format can be a closure. The closure is passed the query as the only argument. The result of the closure is returned as the result of the query.
45214542
*
45224543
* @format "query", "array", or a closure.
4544+
* @options Options passed to named return formatter factories.
45234545
*
45244546
* @return qb.models.Query.QueryBuilder
45254547
*/
4526-
public QueryBuilder function setReturnFormat( required any format ) {
4548+
public QueryBuilder function setReturnFormat( required any format, struct options = {} ) {
45274549
structDelete( variables.defaultOptions, "returntype" );
45284550
if ( isClosure( arguments.format ) || isCustomFunction( arguments.format ) ) {
45294551
variables.returnFormat = format;
4530-
} else if ( arguments.format == "array" ) {
4531-
variables.returnFormat = function( q ) {
4532-
return getUtils().queryToArrayOfStructs( q );
4533-
};
4534-
} else if ( arguments.format == "query" ) {
4535-
variables.returnFormat = function( q ) {
4536-
return q;
4537-
};
4538-
} else if ( arguments.format == "none" ) {
4539-
variables.returnFormat = function( q ) {
4540-
return q;
4541-
};
45424552
} else {
4543-
throw( type = "InvalidFormat", message = "The format passed to Builder is invalid." );
4553+
variables.returnFormat = getReturnFormatterRegistry().getReturnFormatter(
4554+
arguments.format,
4555+
arguments.options
4556+
);
45444557
}
45454558

45464559
return this;
@@ -4566,17 +4579,35 @@ component displayname="QueryBuilder" accessors="true" {
45664579
*
45674580
* @returnFormat "query", "array", or a closure.
45684581
* @callback The code to execute with the given return format.
4582+
* @options Options passed to named return formatter factories.
45694583
*
45704584
* @return any
45714585
*/
4572-
public any function withReturnFormat( required any returnFormat, required any callback ) {
4586+
public any function withReturnFormat( required any returnFormat, required any callback, struct options = {} ) {
45734587
var originalReturnFormat = getReturnFormat();
4574-
setReturnFormat( arguments.returnFormat );
4588+
setReturnFormat( arguments.returnFormat, arguments.options );
45754589
var result = callback();
45764590
setReturnFormat( originalReturnFormat );
45774591
return result;
45784592
}
45794593

4594+
private void function normalizeQueryExecuteReturnTypeOptions( required struct options ) {
4595+
if ( !arguments.options.keyExists( "returntype" ) ) {
4596+
return;
4597+
}
4598+
4599+
if ( getValidateQueryExecuteReturnType() ) {
4600+
throw(
4601+
type = "InvalidQueryExecuteOption",
4602+
message = "The queryExecute returntype option cannot be used with qb return formatters."
4603+
);
4604+
}
4605+
4606+
structDelete( arguments.options, "returntype" );
4607+
structDelete( arguments.options, "columnkey" );
4608+
structDelete( arguments.options, "columnKey" );
4609+
}
4610+
45804611
/**
45814612
* Runs the code inside the callback with the given columns selected and then sets the columns back to its original value.
45824613
*

models/Query/QueryUtils.cfc

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,32 @@ component singleton displayname="QueryUtils" accessors="true" {
378378
return results;
379379
}
380380

381+
/**
382+
* Converts a query object to a struct of structs keyed by the provided column.
383+
*
384+
* @q The query to convert.
385+
* @columnKey The query column to use as the key for the returned struct.
386+
*
387+
* @return struct
388+
*/
389+
public struct function queryToStructOfStructs( required any q, required string columnKey ) {
390+
var rows = queryToArrayOfStructs( arguments.q );
391+
var results = {};
392+
393+
for ( var row in rows ) {
394+
if ( !row.keyExists( arguments.columnKey ) ) {
395+
throw(
396+
type = "MissingColumnKey",
397+
message = "The columnKey [#arguments.columnKey#] was not found in the query results."
398+
);
399+
}
400+
401+
results[ row[ arguments.columnKey ] ] = row;
402+
}
403+
404+
return results;
405+
}
406+
381407
/**
382408
* Remove a list of columns from a specified query.
383409
*

0 commit comments

Comments
 (0)