Skip to content

Commit

Permalink
feat(QueryBuilder): Add findOrFail and existsOrFail methods
Browse files Browse the repository at this point in the history
  • Loading branch information
elpete committed Aug 23, 2023
1 parent 0c8408a commit 96b9047
Show file tree
Hide file tree
Showing 2 changed files with 262 additions and 1 deletion.
49 changes: 48 additions & 1 deletion models/Query/QueryBuilder.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -3243,6 +3243,27 @@ component displayname="QueryBuilder" accessors="true" {
return count( options = arguments.options ) > 0;
}

/**
* Returns true if any records exist with the configured query.
* If no records exist, it throws an RecordNotFound exception.
*
* @options Any options to pass to `queryExecute`. Default: {}.
* @errorMessage An optional string error message or callback to produce
* a string error message. If a callback is used, it is
* passed the unloaded entity as the only argument.
*
* @throws RecordNotFound
*
* @return Boolean
*/
public boolean function existsOrFail( struct options = {}, any errorMessage ) {
if ( !this.exists( arguments.options ) ) {
param arguments.errorMessage = "No rows found with constraints [#serializeJSON( this.getBindings() )#]";
throw( type = "RecordNotFound", message = arguments.errorMessage );
}
return true;
}

/*******************************************************************************\
| select functions |
\*******************************************************************************/
Expand Down Expand Up @@ -3300,7 +3321,7 @@ component displayname="QueryBuilder" accessors="true" {
*/
public any function firstOrFail( any errorMessage, struct options = {} ) {
var result = first( arguments.options );
if ( isEmpty( result ) ) {
if ( structIsEmpty( result ) ) {
param arguments.errorMessage = "No rows found with constraints [#serializeJSON( this.getBindings() )#]";
if ( isClosure( arguments.errorMessage ) || isCustomFunction( arguments.errorMessage ) ) {
arguments.errorMessage = arguments.errorMessage( this );
Expand Down Expand Up @@ -3341,6 +3362,32 @@ component displayname="QueryBuilder" accessors="true" {
return first( options = arguments.options );
}

/**
* Returns the first record with the id value as the primary key.
* If no record is found, it throws an `EntityNotFound` exception.
*
* @id The id value to find.
* @idColumn The name of the id column. Default: `id`.
* @errorMessage An optional string error message to be used in the exception.
*
* @throws RecordNotFound
*
* @return struct
*/
public any function findOrFail(
required any id,
string idColumn = "id",
any errorMessage,
struct options = {}
) {
var row = this.find( arguments.id, arguments.idColumn, arguments.options );
if ( structIsEmpty( row ) ) {
param arguments.errorMessage = "No record found with [#idColumn#] column equal to [#arguments.id#].";
throw( type = "RecordNotFound", message = arguments.errorMessage );
}
return row;
}

/**
* Returns the first value of a column in a query.
*
Expand Down
214 changes: 214 additions & 0 deletions tests/specs/Query/Abstract/QueryExecutionSpec.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,134 @@ component extends="testbox.system.BaseSpec" {
expect( runQueryLog ).toHaveLength( 2 );
} );
} );

describe( "firstOrFail", function() {
it( "retrieves the first record when calling `firstOrFail`", function() {
var builder = getBuilder();
var expectedQuery = queryNew( "id,name", "integer,varchar", [ { id: 1, name: "foo" } ] );
builder
.$( "runQuery" )
.$args( sql = "SELECT * FROM ""users"" WHERE ""name"" = ? LIMIT 1", options = {} )
.$results( expectedQuery );

var results = builder
.from( "users" )
.whereName( "foo" )
.firstOrFail();

expect( results ).toBeStruct();
expect( results ).toBe( { id: 1, name: "foo" } );
expect( getTestBindings( builder ) ).toBe( [ "foo" ] );

var runQueryLog = builder.$callLog().runQuery;
expect( runQueryLog ).toBeArray();
expect( runQueryLog ).toHaveLength( 1, "runQuery should have been called once" );
expect( runQueryLog[ 1 ] ).toBe( { sql: "SELECT * FROM ""users"" WHERE ""name"" = ? LIMIT 1", options: {} } );
} );

it( "throw a RecordNotFound exception if no rows are returned", function() {
var builder = getBuilder();
var expectedQuery = queryNew( "id,name", "integer,varchar", [] );
builder
.$( "runQuery" )
.$args( sql = "SELECT * FROM ""users"" WHERE ""name"" = ? LIMIT 1", options = {} )
.$results( expectedQuery );

expect( function() {
builder
.from( "users" )
.whereName( "foo" )
.firstOrFail();
} ).toThrow( type = "RecordNotFound" );

var runQueryLog = builder.$callLog().runQuery;
expect( runQueryLog ).toBeArray();
expect( runQueryLog ).toHaveLength( 1, "runQuery should have been called once" );
expect( runQueryLog[ 1 ] ).toBe( { sql: "SELECT * FROM ""users"" WHERE ""name"" = ? LIMIT 1", options: {} } );
} );

it( "can supply a custom errorMessage", function() {
var builder = getBuilder();
var expectedQuery = queryNew( "id,name", "integer,varchar", [] );
builder
.$( "runQuery" )
.$args( sql = "SELECT * FROM ""users"" WHERE ""name"" = ? LIMIT 1", options = {} )
.$results( expectedQuery );

expect( function() {
builder
.from( "users" )
.whereName( "foo" )
.firstOrFail( errorMessage = "Whoops" );
} ).toThrow( type = "RecordNotFound", regex = "Whoops" );

var runQueryLog = builder.$callLog().runQuery;
expect( runQueryLog ).toBeArray();
expect( runQueryLog ).toHaveLength( 1, "runQuery should have been called once" );
expect( runQueryLog[ 1 ] ).toBe( { sql: "SELECT * FROM ""users"" WHERE ""name"" = ? LIMIT 1", options: {} } );
} );
} );

describe( "findOrFail", function() {
it( "returns the first result by id when calling `find`", function() {
var builder = getBuilder();
builder.setReturnFormat( "query" );
var expectedQuery = queryNew( "id,name", "integer,varchar", [ { id: 1, name: "foo" } ] );
builder
.$( "runQuery" )
.$args( sql = "SELECT * FROM ""users"" WHERE ""id"" = ? LIMIT 1", options = {} )
.$results( expectedQuery );

var results = builder.from( "users" ).findOrFail( 1 );

expect( results ).toBeStruct();
expect( results ).toBe( { id: 1, name: "foo" } );
expect( getTestBindings( builder ) ).toBe( [ 1 ] );

var runQueryLog = builder.$callLog().runQuery;
expect( runQueryLog ).toBeArray();
expect( runQueryLog ).toHaveLength( 1, "runQuery should have been called once" );
expect( runQueryLog[ 1 ] ).toBe( { sql: "SELECT * FROM ""users"" WHERE ""id"" = ? LIMIT 1", options: {} } );
} );

it( "throw a RecordNotFound exception if no rows are returned", function() {
var builder = getBuilder();
builder.setReturnFormat( "query" );
var expectedQuery = queryNew( "id,name", "integer,varchar", [] );
builder
.$( "runQuery" )
.$args( sql = "SELECT * FROM ""users"" WHERE ""id"" = ? LIMIT 1", options = {} )
.$results( expectedQuery );

expect( function() {
builder.from( "users" ).findOrFail( 1 );
} ).toThrow( type = "RecordNotFound" );

var runQueryLog = builder.$callLog().runQuery;
expect( runQueryLog ).toBeArray();
expect( runQueryLog ).toHaveLength( 1, "runQuery should have been called once" );
expect( runQueryLog[ 1 ] ).toBe( { sql: "SELECT * FROM ""users"" WHERE ""id"" = ? LIMIT 1", options: {} } );
} );

it( "can supply a custom errorMessage", function() {
var builder = getBuilder();
builder.setReturnFormat( "query" );
var expectedQuery = queryNew( "id,name", "integer,varchar", [] );
builder
.$( "runQuery" )
.$args( sql = "SELECT * FROM ""users"" WHERE ""id"" = ? LIMIT 1", options = {} )
.$results( expectedQuery );

expect( function() {
builder.from( "users" ).findOrFail( id = 1, errorMessage = "Whoops" );
} ).toThrow( type = "RecordNotFound", regex = "Whoops" );

var runQueryLog = builder.$callLog().runQuery;
expect( runQueryLog ).toBeArray();
expect( runQueryLog ).toHaveLength( 1, "runQuery should have been called once" );
expect( runQueryLog[ 1 ] ).toBe( { sql: "SELECT * FROM ""users"" WHERE ""id"" = ? LIMIT 1", options: {} } );
} );
} );
} );

describe( "aggregate functions", function() {
Expand Down Expand Up @@ -890,6 +1018,92 @@ component extends="testbox.system.BaseSpec" {
).toBe( false );
} );
} );

describe( "existsOrFail", function() {
it( "returns true if any records are found for the query", function() {
var builder = getBuilder();
var expectedCount = 1;
var expectedQuery = queryNew( "aggregate", "integer", [ { aggregate: expectedCount } ] );
builder
.$( "runQuery" )
.$args(
sql = "SELECT COALESCE(COUNT(*), 0) AS ""aggregate"" FROM ""users"" WHERE ""id"" = ?",
options = {}
)
.$results( expectedQuery );

var results = builder
.from( "users" )
.where( "id", 1 )
.existsOrFail();

expect( results ).toBeTrue();

var runQueryLog = builder.$callLog().runQuery;
expect( runQueryLog ).toBeArray();
expect( runQueryLog ).toHaveLength( 1, "runQuery should have been called once" );
expect( runQueryLog[ 1 ] ).toBe( {
sql: "SELECT COALESCE(COUNT(*), 0) AS ""aggregate"" FROM ""users"" WHERE ""id"" = ?",
options: {}
} );
} );

it( "throws a RecordNotFound exception if no rows are found", function() {
var builder = getBuilder();
var expectedCount = 0;
var expectedQuery = queryNew( "aggregate", "integer", [ { aggregate: expectedCount } ] );
builder
.$( "runQuery" )
.$args(
sql = "SELECT COALESCE(COUNT(*), 0) AS ""aggregate"" FROM ""users"" WHERE ""id"" = ?",
options = {}
)
.$results( expectedQuery );

expect( function() {
builder
.from( "users" )
.where( "id", 1 )
.existsOrFail();
} ).toThrow( type = "RecordNotFound" );

var runQueryLog = builder.$callLog().runQuery;
expect( runQueryLog ).toBeArray();
expect( runQueryLog ).toHaveLength( 1, "runQuery should have been called once" );
expect( runQueryLog[ 1 ] ).toBe( {
sql: "SELECT COALESCE(COUNT(*), 0) AS ""aggregate"" FROM ""users"" WHERE ""id"" = ?",
options: {}
} );
} );

it( "can supply a custom errorMessage", function() {
var builder = getBuilder();
var expectedCount = 0;
var expectedQuery = queryNew( "aggregate", "integer", [ { aggregate: expectedCount } ] );
builder
.$( "runQuery" )
.$args(
sql = "SELECT COALESCE(COUNT(*), 0) AS ""aggregate"" FROM ""users"" WHERE ""id"" = ?",
options = {}
)
.$results( expectedQuery );

expect( function() {
builder
.from( "users" )
.where( "id", 1 )
.existsOrFail( errorMessage = "Whoops" );
} ).toThrow( type = "RecordNotFound", regex = "Whoops" );

var runQueryLog = builder.$callLog().runQuery;
expect( runQueryLog ).toBeArray();
expect( runQueryLog ).toHaveLength( 1, "runQuery should have been called once" );
expect( runQueryLog[ 1 ] ).toBe( {
sql: "SELECT COALESCE(COUNT(*), 0) AS ""aggregate"" FROM ""users"" WHERE ""id"" = ?",
options: {}
} );
} );
} );
} );

describe( "returnFormat", function() {
Expand Down

0 comments on commit 96b9047

Please sign in to comment.