From 0b6bbc4c95abb4597ddd2bf3dc2a494f4d8049bb Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 18 Aug 2018 21:47:41 -0600 Subject: [PATCH 01/70] docs(Testing): Add instructions for local testing --- .cfconfig.json | 310 ++++++++++++++++++++++++++----------------------- .env.example | 5 + .gitignore | 2 + README.md | 16 ++- 4 files changed, 185 insertions(+), 148 deletions(-) create mode 100644 .env.example diff --git a/.cfconfig.json b/.cfconfig.json index 9f9ce76f..65533b01 100644 --- a/.cfconfig.json +++ b/.cfconfig.json @@ -1,11 +1,45 @@ { + "ACF11Password":"CE9938D7E4E1C617EB071941B1D154016B81A10C1B38DBA3C2828B055D07B40A", + "ACF11RDSPassword":"ewAdjyWYEiLoCNKrmPJqPx7R+FpDnJoB2JiE0zyv5uoKtBbhQcRCW70FpruCGHUT7BtpSKfX/wx/C25mcZgzVc61YeN/0UilO7V6qXUcLlOZg7f/A9ljR9saWi6r9BmKVqExs1X+1r2l60gY7QuUeg==", + "adminAllowConcurrentLogin":true, + "adminAllowedIPList":"", + "adminLoginRequired":true, + "adminRDSEnabled":"false", + "adminRDSLoginRequired":"true", + "adminRDSUserIDRequired":true, + "adminRootUserID":"admin", "adminSalt":"4A23BBE0-AB52-4869-9C3EEEE536DC2256", - "applicationListener":"mixed", + "adminUserIDRequired":false, + "ajaxDebugWindowEnabled":false, + "allowApplicationVarsInServletContext":false, + "allowExtraAttributesInAttrColl":true, + "applicationMangement":true, + "applicationMaximumTimeout":"2,0,0,0", "applicationMode":"curr2root", "applicationTimeout":"1,0,0,0", - "clientCookies":"yes", - "clientManagement":"no", - "clientTimeout":"90,0,0,0", + "CFaaSGeneratedFilesExpiryTime":30, + "CFFormScriptDirectory":"", + "clientStorage":"Cookie", + "clientStorageLocations":{ + "Cookie":{ + "description":"Client based text file.", + "disableGlobals":false, + "name":"Cookie", + "purgeEnable":true, + "purgeTimeout":10, + "type":"COOKIE" + }, + "Registry":{ + "description":"System registry.", + "disableGlobals":false, + "name":"Registry", + "purgeEnable":true, + "purgeTimeout":90, + "type":"REGISTRY" + } + }, + "compileExtForCFInclude":"*", + "componentCacheEnabled":true, "datasources":{ "quick":{ "allowAlter":true, @@ -16,152 +50,136 @@ "allowInsert":true, "allowRevoke":true, "allowSelect":true, + "allowStoredProcs":true, "allowUpdate":true, - "blob":"false", - "class":"org.gjt.mm.mysql.Driver", - "clob":"false", - "connectionTimeout":"1", + "blob":false, + "blobBuffer":64000, + "class":"com.mysql.jdbc.Driver", + "clientApplicationName":false, + "clientApplicationNamePrefix":"", + "clientHostname":false, + "clientUsername":false, + "clob":false, + "clobBuffer":64000, + "connectionLimit":-1, + "connectionTimeout":1, + "connectionTimeoutInterval":420, "custom":"useUnicode=true&characterEncoding=UTF-8&useLegacyDatetimeCode=true", - "database":"quick", + "database":"${DB_NAME}", "dbdriver":"MySQL", - "dsn":"jdbc:mysql://{host}:{port}/{database}", - "host":"localhost", - "metaCacheTimeout":"60000", - "port":"3306", - "storage":"false", - "username":"root", - "validate":"false" - } - }, - "hspw":"698f110377566465ee6fb4fc4df80253b41a17505344ad17b41218956baa984a", - "loggers":{ - "application":{ - "appender":"resource", - "appenderArguments":{ - "path":"{lucee-config}/logs/application.log" - }, - "layout":"classic" - }, - "datasource":{ - "appender":"resource", - "appenderArguments":{ - "path":"{lucee-config}/logs/datasource.log" - }, - "layout":"classic" - }, - "deploy":{ - "appender":"resource", - "appenderArguments":{ - "path":"{lucee-config}/logs/deploy.log" - }, - "layout":"classic", - "level":"info" - }, - "exception":{ - "appender":"resource", - "appenderArguments":{ - "path":"{lucee-config}/logs/exception.log" - }, - "layout":"classic" - }, - "gateway":{ - "appender":"resource", - "appenderArguments":{ - "path":"{lucee-config}/logs/gateway.log" - }, - "layout":"classic" - }, - "mail":{ - "appender":"resource", - "appenderArguments":{ - "path":"{lucee-config}/logs/mail.log" - }, - "layout":"classic" - }, - "mapping":{ - "appender":"resource", - "appenderArguments":{ - "path":"{lucee-config}/logs/mapping.log" - }, - "layout":"classic" - }, - "memory":{ - "appender":"resource", - "appenderArguments":{ - "path":"{lucee-config}/logs/memory.log" - }, - "layout":"classic" - }, - "orm":{ - "appender":"resource", - "appenderArguments":{ - "path":"{lucee-config}/logs/orm.log" - }, - "layout":"classic" - }, - "remoteclient":{ - "appender":"resource", - "appenderArguments":{ - "path":"{lucee-config}/logs/remoteclient.log" - }, - "layout":"classic", - "level":"info" - }, - "requesttimeout":{ - "appender":"resource", - "appenderArguments":{ - "path":"{lucee-config}/logs/requesttimeout.log" - }, - "layout":"classic" - }, - "rest":{ - "appender":"resource", - "appenderArguments":{ - "path":"{lucee-config}/logs/rest.log" - }, - "layout":"classic" - }, - "scheduler":{ - "appender":"resource", - "appenderArguments":{ - "path":"{lucee-config}/logs/scheduler.log" - }, - "layout":"classic" - }, - "scope":{ - "appender":"resource", - "appenderArguments":{ - "path":"{lucee-config}/logs/scope.log" - }, - "layout":"classic" - }, - "search":{ - "appender":"resource", - "appenderArguments":{ - "path":"{lucee-config}/logs/search.log" - }, - "layout":"classic" - }, - "thread":{ - "appender":"resource", - "appenderArguments":{ - "path":"{lucee-config}/logs/thread.log" - }, - "layout":"classic" - }, - "trace":{ - "appender":"resource", - "appenderArguments":{ - "path":"{lucee-config}/logs/trace.log" - }, - "layout":"classic" + "description":"", + "disableAutogeneratedKeyRetrieval":false, + "disableConnections":false, + "dsn":"jdbc:mysql://{host}:{port}/{database}?tinyInt1isBit=false&", + "host":"${DB_HOST}", + "linkedServers":"true", + "logActivity":false, + "logActivityFile":"", + "loginTimeout":30, + "maintainConnections":true, + "maxPooledStatements":"100", + "password":"${DB_PASSWORD}", + "port":"${DB_PORT}", + "queryTimeout":"0", + "SID":"", + "username":"${DB_USER}", + "validate":false, + "validationQuery":"" } }, - "mergeURLAndForm":"no", + "debuggingEnabled":false, + "debuggingIPList":"127.0.0.1,0:0:0:0:0:0:0:1", + "debuggingReportExecutionTimes":false, + "debuggingReportExecutionTimesMinimum":250, + "debuggingReportExecutionTimesTemplate":"summary", + "debuggingShowDatabase":true, + "debuggingShowException":true, + "debuggingShowFlashFormCompileErrors":false, + "debuggingShowGeneral":true, + "debuggingShowTimer":false, + "debuggingShowTrace":true, + "debuggingShowVariableApplication":false, + "debuggingShowVariableCGI":true, + "debuggingShowVariableClient":true, + "debuggingShowVariableCookie":true, + "debuggingShowVariableForm":true, + "debuggingShowVariableRequest":false, + "debuggingShowVariables":true, + "debuggingShowVariableServer":false, + "debuggingShowVariableSession":true, + "debuggingShowVariableURL":true, + "debuggingTemplate":"/WEB-INF/debug/classic.cfm", + "disableInternalCFJavaComponents":false, + "disallowUnamedAppScope":false, + "dotNetClientPort":"6086", + "dotNetInstallDir":"C:\\Work\\CF\\depot\\ColdFusion\\cf_main\\cfusion\\jnbridge", + "dotNetPort":"6085", + "dotNetProtocol":"tcp", + "dotNotationUpperCase":true, + "errorStatusCode":true, + "eventGatewayEnabled":false, + "FlashRemotingEnable":true, + "flexDataServicesEnable":false, + "generalErrorTemplate":"", + "googleMapKey":"", + "inMemoryFileSystemAppLimit":20, + "inMemoryFileSystemEnabled":true, + "inMemoryFileSystemLimit":100, + "inspectTemplate":"always", + "lineDebuggerEnabled":false, + "lineDebuggerMaxSessions":5, + "lineDebuggerPort":5005, + "mailConnectionTimeout":60, + "mailDefaultEncoding":"UTF-8", + "mailSpoolInterval":15, + "maxCFCFunctionRequests":15, + "maxCFThreads":10, + "maxFlashRemotingRequests":5, + "maxOutputBufferSize":1024, + "maxReportRequests":8, + "maxTemplateRequests":25, + "maxWebServiceRequests":5, + "missingErrorTemplate":"", + "monitoringServiceHost":"0.0.0.0", + "monitoringServicePort":"5500", + "ORMSearchIndexDirectory":"", + "perAppSettingsEnabled":true, + "postParametersLimit":100, + "postSizeLimit":20, + "requestQueueTimeout":60, + "requestQueueTimeoutPage":"", "requestTimeout":"0,0,0,50", - "scopeCascading":"standard", - "searchResultsets":"yes", - "sessionMangement":"yes", + "requestTimeoutEnabled":true, + "RMISSLEnable":false, + "RMISSLKeystore":"", + "robustExceptionEnabled":true, + "sandboxEnabled":false, + "saveClassFiles":true, + "schedulerClusterDatasource":"", + "schedulerLogFileExtensions":"log,txt", + "schedulerLoggingEnabled":false, + "scriptProtect":"FORM,URL,COOKIE,CGI", + "secureJSON":false, + "secureJSONPrefix":"//", + "secureProfileEnabled":false, + "serverCFC":"Server", + "serverCFCEenabled":false, + "sessionCookieDisableUpdate":false, + "sessionCookieHTTPOnly":true, + "sessionCookieSecure":false, + "sessionCookieTimeout":946080000, + "sessionMangement":true, + "sessionMaximumTimeout":"2,0,0,0", "sessionTimeout":"0,0,30,0", - "timeServer":"pool.ntp.org" -} \ No newline at end of file + "sessionType":"cfml", + "templateCacheSize":1024, + "throttleThreshold":4, + "totalThrottleMemory":200, + "UDFTypeChecking":false, + "useUUIDForCFToken":true, + "watchConfigFilesForChangesEnabled":false, + "watchConfigFilesForChangesExtensions":"xml,properties", + "watchConfigFilesForChangesInterval":120, + "websocketEnabled":true, + "weinreRemoteInspectionEnabled":false +} diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..98030dfa --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +DB_HOST= +DB_PORT= +DB_NAME= +DB_USER= +DB_PASSWORD= diff --git a/.gitignore b/.gitignore index 3b919f2a..6d070134 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ !.engine/WEB-INF/lib .engine/WEB-INF/lib/* !.engine/WEB-INF/lib/h2-1.4.196.jar + +.env diff --git a/README.md b/README.md index 339756ca..34a18215 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ component { table.string( "password" ); table.timestamp( "createdDate" ); table.timestamp( "updatedDate" ); - } ); + } ); } } @@ -95,6 +95,19 @@ component { Now that you've seen an example, [dig in to what you can do](https://github.com/ortus-docs/quick-docs/blob/master/getting-started/defining-an-entity.md) with Quick! +### Tests and Contributing + +To run the tests, first clone this repo and run a `box install`. + +Quick's test suite runs specifically on MySQL, so you will need a MySQL database to run the tests. +If you do not have one, Docker provides an easy way to start one. + +```sh +docker run -d --name quick -p 3306:3306 -e MYSQL_DATABASE=quick -e MYSQL_USER=quick -e MYSQL_PASSWORD=quick mysql:5 +``` + +Finally, copy the `.env.example` file to `.env` and fill in the values for your database. + ### Prior Art, Acknowledgements, and Thanks Quick is backed by [qb](https://www.forgebox.io/view/qb). Without qb, there is no Quick. @@ -102,4 +115,3 @@ Quick is backed by [qb](https://www.forgebox.io/view/qb). Without qb, there is n Quick is inspired heavily by [Eloquent in Laravel](https://laravel.com/docs/5.6/eloquent). Thank you Taylor Otwell and the Laravel community for a great library. Development of Quick is sponsored by Ortus Solutions. Thank you Ortus Solutions for investing in the future of CFML. - From fdcf530829eb5000f4744a0626c189307e57a081 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 18 Aug 2018 22:10:00 -0600 Subject: [PATCH 02/70] fix(Memento): Return all properties with correct casing in memento All properties defined on the model should now be returned when calling `getMemento`. The casing used is the casing defined as the property name instead of the column. If no value has been set, like in the case of a new entity, the default value of "" (empty string) will be used. --- models/BaseEntity.cfc | 5 +++- .../integration/BaseEntity/AttributeSpec.cfc | 26 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index d7e928eb..119c929e 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -737,7 +737,10 @@ component accessors="true" { } function getMemento() { - return getAttributesData(); + return getAttributes().keyArray().reduce( function( acc, key ) { + acc[ key ] = getAttribute( key ); + return acc; + }, {} ); } function $renderdata() { diff --git a/tests/specs/integration/BaseEntity/AttributeSpec.cfc b/tests/specs/integration/BaseEntity/AttributeSpec.cfc index 078c2449..7167343a 100644 --- a/tests/specs/integration/BaseEntity/AttributeSpec.cfc +++ b/tests/specs/integration/BaseEntity/AttributeSpec.cfc @@ -69,6 +69,32 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { expect( user.isDirty() ).toBeFalse(); } ); } ); + + it( "shows all the attributes in the memento of a newly created object", function() { + expect( getInstance( "User" ).getMemento() ).toBe( { + "id" = "", + "username" = "", + "firstName" = "", + "lastName" = "", + "password" = "", + "countryId" = "", + "createdDate" = "", + "modifiedDate" = "" + } ); + } ); + + it( "shows all the attributes in the component casing", function() { + expect( getInstance( "User" ).findOrFail( 1 ).getMemento() ).toBe( { + "id" = 1, + "username" = "elpete", + "firstName" = "Eric", + "lastName" = "Peterson", + "password" = "5F4DCC3B5AA765D61D8327DEB882CF99", + "countryId" = "02B84D66-0AA0-F7FB-1F71AFC954843861", + "createdDate" = "2017-07-28 02:06:36", + "modifiedDate" = "2017-07-28 02:06:36" + } ); + } ); } ); } From 3d8722fffde6002c8595b2d0ef90344f9530aca0 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 18 Aug 2018 22:51:45 -0600 Subject: [PATCH 03/70] fix(BaseEntity): Return the entity after setting a column --- models/BaseEntity.cfc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 119c929e..fdd275c4 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -644,7 +644,7 @@ component accessors="true" { var getColumnValue = tryColumnGetters( missingMethodName ); if ( ! isNull( getColumnValue ) ) { return getColumnValue; } var setColumnValue = tryColumnSetters( missingMethodName, missingMethodArguments ); - if ( ! isNull( setColumnValue ) ) { return setColumnValue; } + if ( ! isNull( setColumnValue ) ) { return this; } return; } From bf4b67aee0e90a4302e69333c701892a613d0332 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 18 Aug 2018 23:04:04 -0600 Subject: [PATCH 04/70] feat(BaseEntity): Improve null handling Null values are retrieved as empty strings from the database. By default, empty strings are serialized back to nulls when saving to the database. This feature can be turned off per property by setting the `convertToNull` attribute to `false`. Additionally, the `nullValue` property can be set to a custom value. When this is the case-sensitive value of the property when saving, `null` will be inserted into the database. --- models/BaseEntity.cfc | 18 +++++-- tests/Application.cfc | 7 ++- tests/resources/app/models/PhoneNumber.cfc | 2 +- tests/resources/app/models/Song.cfc | 2 +- .../integration/BaseEntity/AggregateSpec.cfc | 4 +- .../integration/BaseEntity/DeleteSpec.cfc | 6 +-- .../integration/BaseEntity/NullValuesSpec.cfc | 50 +++++++++++++++++++ .../integration/BaseEntity/QuerySpec.cfc | 4 +- .../Relationships/EagerLoadingSpec.cfc | 11 ++-- .../specs/integration/BaseEntity/SaveSpec.cfc | 8 +-- .../integration/BaseEntity/ScopeSpec.cfc | 7 +-- .../integration/BaseEntity/ValidationSpec.cfc | 6 +-- 12 files changed, 99 insertions(+), 26 deletions(-) create mode 100644 tests/specs/integration/BaseEntity/NullValuesSpec.cfc diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index fdd275c4..9bcae558 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -24,7 +24,8 @@ component accessors="true" { property name="key" default="id"; property name="attributes"; property name="meta"; - + property name="nullValues"; + /*===================================== = Instance Data = =====================================*/ @@ -48,6 +49,7 @@ component accessors="true" { setOriginalAttributes( {} ); setRelationshipsData( {} ); setEagerLoad( [] ); + setNullValues( {} ); setLoaded( false ); } @@ -289,7 +291,7 @@ component accessors="true" { newQuery() .where( getKey(), getKeyValue() ) .update( getAttributesData( withoutKey = true ).map( function( key, value, attributes ) { - if ( isNull( value ) ) { + if ( isNull( value ) || isNullValue( key, value ) ) { return { value = "", nulls = true, null = true }; } if ( attributeHasSqlType( key ) ) { @@ -306,7 +308,7 @@ component accessors="true" { fireEvent( "preInsert", { entity = this } ); guardValid(); var result = newQuery().insert( getAttributesData().map( function( key, value, attributes ) { - if ( isNull( value ) ) { + if ( isNull( value ) || isNullValue( key, value ) ) { return { value = "", nulls = true, null = true }; } if ( attributeHasSqlType( key ) ) { @@ -783,6 +785,11 @@ component accessors="true" { properties.reduce( function( acc, prop ) { param prop.column = prop.name; param prop.persistent = true; + param prop.nullValue = ""; + param prop.convertToNull = true; + if ( prop.convertToNull ) { + variables.nullValues[ prop.name ] = prop.nullValue; + } if ( prop.persistent ) { acc[ prop.name ] = prop.column; } @@ -1035,4 +1042,9 @@ component accessors="true" { } )[ 1 ].sqltype; } + private function isNullValue( key, value ) { + return variables.nullValues.keyExists( getAliasForColumn( key ) ) && + compare( variables.nullValues[ getAliasForColumn( key ) ], value ) == 0; + } + } diff --git a/tests/Application.cfc b/tests/Application.cfc index 76a47aba..cb0a6ba6 100644 --- a/tests/Application.cfc +++ b/tests/Application.cfc @@ -62,6 +62,9 @@ component { queryExecute( " INSERT INTO `users` (`id`, `username`, `first_name`, `last_name`, `password`, `country_id`, `created_date`, `modified_date`) VALUES (2, 'johndoe', 'John', 'Doe', '5F4DCC3B5AA765D61D8327DEB882CF99', '02B84D66-0AA0-F7FB-1F71AFC954843861', '2017-07-28 02:07:16', '2017-07-28 02:07:16'); " ); + queryExecute( " + INSERT INTO `users` (`id`, `username`, `first_name`, `last_name`, `password`, `country_id`, `created_date`, `modified_date`) VALUES (3, 'janedoe', 'Jane', 'Doe', '5F4DCC3B5AA765D61D8327DEB882CF99', NULL, '2017-07-28 02:08:16', '2017-07-28 02:08:16'); + " ); queryExecute( " CREATE TABLE `my_posts` ( `post_pk` int(11) NOT NULL AUTO_INCREMENT, @@ -162,7 +165,7 @@ component { queryExecute( " CREATE TABLE `songs` ( `id` int(11) NOT NULL AUTO_INCREMENT, - `title` varchar(255) NOT NULL, + `title` varchar(255), `download_url` varchar(255) NOT NULL, `created_date` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, `modified_date` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -178,7 +181,7 @@ component { queryExecute( " CREATE TABLE `phone_numbers` ( `id` int(11) NOT NULL AUTO_INCREMENT, - `number` varchar(50) NOT NULL, + `number` varchar(50), PRIMARY KEY (`id`) ) " ); diff --git a/tests/resources/app/models/PhoneNumber.cfc b/tests/resources/app/models/PhoneNumber.cfc index 9ef265a5..d764fa4a 100644 --- a/tests/resources/app/models/PhoneNumber.cfc +++ b/tests/resources/app/models/PhoneNumber.cfc @@ -1,5 +1,5 @@ component extends="quick.models.BaseEntity" { - property name="number" sqltype="cf_sql_varchar"; + property name="number" sqltype="cf_sql_varchar" convertToNull="false"; } diff --git a/tests/resources/app/models/Song.cfc b/tests/resources/app/models/Song.cfc index 60e4b7b9..86ff2531 100644 --- a/tests/resources/app/models/Song.cfc +++ b/tests/resources/app/models/Song.cfc @@ -1,7 +1,7 @@ component extends="quick.models.BaseEntity" accessors="true" { property name="id"; - property name="title"; + property name="title" nullValue="REALLY_NULL"; property name="downloadUrl" column="download_url"; property name="createdDate" column="created_date"; property name="modifiedDate" column="modified_date"; diff --git a/tests/specs/integration/BaseEntity/AggregateSpec.cfc b/tests/specs/integration/BaseEntity/AggregateSpec.cfc index 55f9afc8..f787e885 100644 --- a/tests/specs/integration/BaseEntity/AggregateSpec.cfc +++ b/tests/specs/integration/BaseEntity/AggregateSpec.cfc @@ -3,10 +3,10 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { function run() { describe( "Aggregate Spec", function() { it( "can retrieve aggregates with no issues", function() { - expect( getInstance( "User" ).count() ).toBe( 2 ); + expect( getInstance( "User" ).count() ).toBe( 3 ); expect( getInstance( "User" ).whereUsername( "elpete" ).count() ).toBe( 1 ); } ); } ); } -} \ No newline at end of file +} diff --git a/tests/specs/integration/BaseEntity/DeleteSpec.cfc b/tests/specs/integration/BaseEntity/DeleteSpec.cfc index 1a21af00..f34a25b4 100644 --- a/tests/specs/integration/BaseEntity/DeleteSpec.cfc +++ b/tests/specs/integration/BaseEntity/DeleteSpec.cfc @@ -6,7 +6,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { var user = getInstance( "User" ).findOrFail( 2 ); user.delete(); - expect( getInstance( "User" ).count() ).toBe( 1 ); + expect( getInstance( "User" ).count() ).toBe( 2 ); expect( function() { getInstance( "User" ).findOrFail( 2 ); } ).toThrow( type = "EntityNotFound" ); @@ -19,7 +19,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { it( "can delete off of a query", function() { getInstance( "User" ).whereUsername( "johndoe" ).deleteAll(); - expect( getInstance( "User" ).count() ).toBe( 1 ); + expect( getInstance( "User" ).count() ).toBe( 2 ); expect( function() { getInstance( "User" ).whereUsername( "johndoe" ).firstOrFail(); } ).toThrow( type = "EntityNotFound" ); @@ -27,7 +27,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { it( "can delete multiple ids at once", function() { getInstance( "User" ).deleteAll( [ 2 ] ); - expect( getInstance( "User" ).count() ).toBe( 1 ); + expect( getInstance( "User" ).count() ).toBe( 2 ); expect( function() { getInstance( "User" ).findOrFail( 2 ); } ).toThrow( type = "EntityNotFound" ); diff --git a/tests/specs/integration/BaseEntity/NullValuesSpec.cfc b/tests/specs/integration/BaseEntity/NullValuesSpec.cfc new file mode 100644 index 00000000..091a91ed --- /dev/null +++ b/tests/specs/integration/BaseEntity/NullValuesSpec.cfc @@ -0,0 +1,50 @@ +component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { + + function run() { + describe( "Null Values Spec", function() { + it( "returns null values as a string by default", function() { + var user = getInstance( "User" ).findOrFail( 3 ); + expect( user.getCountryId() ).toBe( "" ); + expect( user.getMemento().countryId ).toBe( "" ); + } ); + + it( "saves a column containing an empty string as null in the database by default", function() { + var user = getInstance( "User" ).findOrFail( 1 ); + user.setCountryId( "" ); + user.save(); + expect( getInstance( "User" ).whereId( 1 ).whereNull( "country_id" ).count() ).toBe( 1 ); + expect( getInstance( "User" ).whereId( 1 ).whereNotNull( "country_id" ).count() ).toBe( 0 ); + } ); + + it( "can set a column to not convert empty strings to null", function() { + expect( getInstance( "PhoneNumber" ).count() ).toBe( 0 ); + getInstance( "PhoneNumber" ).setNumber( "" ).save(); + expect( getInstance( "PhoneNumber" ).whereNull( "number" ).count() ).toBe( 0 ); + expect( getInstance( "PhoneNumber" ).whereNotNull( "number" ).count() ).toBe( 1 ); + } ); + + it( "can choose a custom value to convert to nulls in the database", function() { + expect( getInstance( "Song" ).whereNull( "title" ).count() ).toBe( 0 ); + + getInstance( "Song" ).fill( { + title = "", + downloadUrl = "https://example.com/songs/1" + } ).save(); + expect( getInstance( "Song" ).whereNull( "title" ).count() ).toBe( 0 ); + + getInstance( "Song" ).fill( { + title = "Really_Null", + downloadUrl = "https://example.com/songs/1" + } ).save(); + expect( getInstance( "Song" ).whereNull( "title" ).count() ).toBe( 0 ); + + getInstance( "Song" ).fill( { + title = "REALLY_NULL", + downloadUrl = "https://example.com/songs/1" + } ).save(); + expect( getInstance( "Song" ).whereNull( "title" ).count() ).toBe( 1 ); + } ); + } ); + } + +} diff --git a/tests/specs/integration/BaseEntity/QuerySpec.cfc b/tests/specs/integration/BaseEntity/QuerySpec.cfc index 85856bf8..25e87712 100644 --- a/tests/specs/integration/BaseEntity/QuerySpec.cfc +++ b/tests/specs/integration/BaseEntity/QuerySpec.cfc @@ -4,11 +4,13 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { describe( "Query Spec", function() { it( "returns all records as entities", function() { var users = getInstance( "User" ).all().get(); - expect( users ).toHaveLength( 2, "Two users should exist in the database and be returned." ); + expect( users ).toHaveLength( 3, "Three users should exist in the database and be returned." ); expect( users[ 1 ].getId() ).toBe( 1 ); expect( users[ 1 ].getUsername() ).toBe( "elpete" ); expect( users[ 2 ].getId() ).toBe( 2 ); expect( users[ 2 ].getUsername() ).toBe( "johndoe" ); + expect( users[ 3 ].getId() ).toBe( 3 ); + expect( users[ 3 ].getUsername() ).toBe( "janedoe" ); } ); it( "can execute an arbitrary get query", function() { diff --git a/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc b/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc index 6a3a8e06..6385616b 100644 --- a/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc +++ b/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc @@ -33,14 +33,19 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { it( "can eager load a has many relationship", function() { var users = getInstance( "User" ).with( "posts" ).latest().get(); expect( users.get() ).toBeArray(); - expect( users.get() ).toHaveLength( 2, "Two users should be returned" ); + expect( users.get() ).toHaveLength( 3, "Three users should be returned" ); - var johndoe = users.get()[ 1 ]; + var janedoe = users.get()[ 1 ]; + expect( janedoe.getUsername() ).toBe( "janedoe" ); + expect( janedoe.getPosts().get() ).toBeArray(); + expect( janedoe.getPosts().get() ).toHaveLength( 0, "No posts should belong to janedoe" ); + + var johndoe = users.get()[ 2 ]; expect( johndoe.getUsername() ).toBe( "johndoe" ); expect( johndoe.getPosts().get() ).toBeArray(); expect( johndoe.getPosts().get() ).toHaveLength( 0, "No posts should belong to johndoe" ); - var elpete = users.get()[ 2 ]; + var elpete = users.get()[ 3 ]; expect( elpete.getUsername() ).toBe( "elpete" ); expect( elpete.getPosts().get() ).toBeArray(); expect( elpete.getPosts().get() ).toHaveLength( 2, "Two posts should belong to elpete" ); diff --git a/tests/specs/integration/BaseEntity/SaveSpec.cfc b/tests/specs/integration/BaseEntity/SaveSpec.cfc index 9ec28b53..b1f659db 100644 --- a/tests/specs/integration/BaseEntity/SaveSpec.cfc +++ b/tests/specs/integration/BaseEntity/SaveSpec.cfc @@ -16,10 +16,10 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { newUser.setLastName( "User" ); newUser.setPassword( hash( "password" ) ); var userRowsPreSave = queryExecute( "SELECT * FROM users" ); - expect( userRowsPreSave ).toHaveLength( 2 ); + expect( userRowsPreSave ).toHaveLength( 3 ); newUser.save(); var userRowsPostSave = queryExecute( "SELECT * FROM users" ); - expect( userRowsPostSave ).toHaveLength( 3 ); + expect( userRowsPostSave ).toHaveLength( 4 ); var newUserAgain = getInstance( "User" ).whereUsername( "new_user" ).firstOrFail(); expect( newUserAgain.getFirstName() ).toBe( "New" ); expect( newUserAgain.getLastName() ).toBe( "User" ); @@ -49,10 +49,10 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { var existingUser = getInstance( "User" ).find( 1 ); existingUser.setUsername( "new_elpete_username" ); var userRowsPreSave = queryExecute( "SELECT * FROM users" ); - expect( userRowsPreSave ).toHaveLength( 2 ); + expect( userRowsPreSave ).toHaveLength( 3 ); existingUser.save(); var userRowsPostSave = queryExecute( "SELECT * FROM users" ); - expect( userRowsPostSave ).toHaveLength( 2 ); + expect( userRowsPostSave ).toHaveLength( 3 ); } ); it( "uses the type attribute if present for each column", function() { diff --git a/tests/specs/integration/BaseEntity/ScopeSpec.cfc b/tests/specs/integration/BaseEntity/ScopeSpec.cfc index 2d806e8d..74ed295c 100644 --- a/tests/specs/integration/BaseEntity/ScopeSpec.cfc +++ b/tests/specs/integration/BaseEntity/ScopeSpec.cfc @@ -4,9 +4,10 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { describe( "Scope Spec", function() { it( "looks for missing methods as scopes", function() { var users = getInstance( "User" ).latest().get().get(); - expect( users ).toHaveLength( 2, "Two users should exist in the database and be returned." ); - expect( users[ 1 ].getUsername() ).toBe( "johndoe" ); - expect( users[ 2 ].getUsername() ).toBe( "elpete" ); + expect( users ).toHaveLength( 3, "Three users should exist in the database and be returned." ); + expect( users[ 1 ].getUsername() ).toBe( "janedoe" ); + expect( users[ 2 ].getUsername() ).toBe( "johndoe" ); + expect( users[ 3 ].getUsername() ).toBe( "elpete" ); } ); } ); } diff --git a/tests/specs/integration/BaseEntity/ValidationSpec.cfc b/tests/specs/integration/BaseEntity/ValidationSpec.cfc index 8e0546d1..32fc95a7 100644 --- a/tests/specs/integration/BaseEntity/ValidationSpec.cfc +++ b/tests/specs/integration/BaseEntity/ValidationSpec.cfc @@ -17,19 +17,19 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { // missing last name newUser.setPassword( hash( "password" ) ); var userRowsPreSave = queryExecute( "SELECT * FROM users" ); - expect( userRowsPreSave ).toHaveLength( 2 ); + expect( userRowsPreSave ).toHaveLength( 3 ); // expect( function() { // newUser.save(); // } ).toThrow( "InvalidEntity" ); var userRowsPostFirstSave = queryExecute( "SELECT * FROM users" ); - expect( userRowsPostFirstSave ).toHaveLength( 2 ); + expect( userRowsPostFirstSave ).toHaveLength( 3 ); // set last name newUser.setLastName( "User" ); expect( function() { newUser.save(); } ).notToThrow( "InvalidEntity" ); var userRowsPostSecondSave = queryExecute( "SELECT * FROM users" ); - expect( userRowsPostSecondSave ).toHaveLength( 3 ); + expect( userRowsPostSecondSave ).toHaveLength( 4 ); } ); } ); } From deca36c2603b635fc29e8cd1c38ed0a3a7794233 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 24 Aug 2018 06:43:26 -0600 Subject: [PATCH 05/70] refactor(QuickCollection): Extract getMemento from $renderData QuickCollection had included a helper function `$renderData` that would call `getMemento()` on all internal objects and return it as an array. Since the `$renderData` name is used for ColdBox automatic data marshalling, we are extracting the logic to a `getMemento` function for more semantic reuse. The `$renderData` method now calls `getMemento`. --- models/QuickCollection.cfc | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/models/QuickCollection.cfc b/models/QuickCollection.cfc index a7d06587..ccfa0252 100644 --- a/models/QuickCollection.cfc +++ b/models/QuickCollection.cfc @@ -20,12 +20,16 @@ component extends="cfcollection.models.Collection" { return this; } - function $renderData() { + function getMemento() { return this.map( function( entity ) { return entity.$renderData(); } ).get(); } + function $renderData() { + return getMemento(); + } + private function eagerLoadRelation( relationName ) { var keys = map( function( entity ) { return invoke( entity, relationName ).getForeignKeyValue(); From c8aec9dc90a70954b28a9e0261477625c1c36b78 Mon Sep 17 00:00:00 2001 From: Andrew Davis <35044908+blusol850@users.noreply.github.com> Date: Fri, 14 Sep 2018 15:28:26 -0400 Subject: [PATCH 06/70] perf(Collection): Use arrays instead of Collections as the default return format Collections have some performance issues. Also, many people have asked for arrays to be the default return format. This change uses arrays as the default return format. BREAKING CHANGE: Arrays are now returned instead of Collections --- ModuleConfig.cfc | 10 +---- models/BaseEntity.cfc | 43 ++++++++++++------- models/QuickCollection.cfc | 2 +- models/Relationships/BaseRelationship.cfc | 7 +-- .../integration/BaseEntity/QuerySpec.cfc | 6 +-- .../Relationships/BelongsToManySpec.cfc | 4 +- .../Relationships/EagerLoadingSpec.cfc | 38 ++++++++-------- .../BaseEntity/Relationships/HasManySpec.cfc | 8 ++-- .../Relationships/HasManyThroughSpec.cfc | 8 ++-- .../Relationships/PolymorphicHasManySpec.cfc | 12 +++--- .../specs/integration/BaseEntity/SaveSpec.cfc | 2 +- .../integration/BaseEntity/ScopeSpec.cfc | 2 +- .../specs/integration/QuickCollectionSpec.cfc | 3 +- 13 files changed, 74 insertions(+), 71 deletions(-) diff --git a/ModuleConfig.cfc b/ModuleConfig.cfc index 204fa00b..4c1157fd 100644 --- a/ModuleConfig.cfc +++ b/ModuleConfig.cfc @@ -36,16 +36,10 @@ component { } function onLoad() { - // remap to force the return format to be QuickCollection - binder.map( alias = "QueryBuilder@qb", force = true ) + binder.map( "QuickQB@quick" ) .to( "qb.models.Query.QueryBuilder" ) .initArg( name = "grammar", dsl = "#settings.defaultGrammar#@qb" ) .initArg( name = "utils", dsl = "QueryUtils@qb" ) - .initArg( name = "returnFormat", value = function( q ) { - return wirebox.getInstance( - name = "QuickCollection@quick", - initArguments = { collection = q } - ); - } ); + .initArg( name = "returnFormat", value = "array" ); } } diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 9bcae558..f0e1c040 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -3,7 +3,7 @@ component accessors="true" { /*==================================== = Dependencies = ====================================*/ - property name="builder" inject="QueryBuilder@qb"; + property name="builder" inject="QuickQB@quick"; property name="wirebox" inject="wirebox"; property name="str" inject="Str@str"; property name="settings" inject="coldbox:modulesettings:quick"; @@ -555,7 +555,7 @@ component accessors="true" { } private function eagerLoadRelations( entities ) { - if ( entities.empty() || arrayIsEmpty( variables.eagerLoad ) ) { + if ( arrayIsEmpty( entities ) || arrayIsEmpty( variables.eagerLoad ) ) { return entities; } @@ -569,18 +569,19 @@ component accessors="true" { private function eagerLoadRelation( relationName, entities ) { var keys = entities.map( function( entity ) { return invoke( entity, relationName ).getForeignKeyValue(); - } ).unique(); - var relatedEntity = invoke( entities.get( 1 ), relationName ).getRelated(); - var owningKey = invoke( entities.get( 1 ), relationName ).getOwningKey(); - var relations = relatedEntity.resetQuery().whereIn( owningKey, keys.get() ).get( options = getQueryOptions() ); + } ); + keys = arraySlice( createObject( "java", "java.util.HashSet" ).init( keys ).toArray(), 1 ); + var relatedEntity = invoke( entities[ 1 ], relationName ).getRelated(); + var owningKey = invoke( entities[ 1 ], relationName ).getOwningKey(); + var relations = relatedEntity.resetQuery().whereIn( owningKey, keys ).get( options = getQueryOptions() ); return matchRelations( entities, relations, relationName ); } private function matchRelations( entities, relations, relationName ) { - var relationship = invoke( entities.get( 1 ), relationName ); - var groupedRelations = relations.groupBy( key = relationship.getOwningKey(), forceLookup = true ); - return entities.each( function( entity ) { + var relationship = invoke( entities[ 1 ], relationName ); + var groupedRelations = groupBy( items = relations, key = relationship.getOwningKey(), forceLookup = true ); + entities.each( function( entity ) { var relationship = invoke( entity, relationName ); if ( structKeyExists( groupedRelations, relationship.getForeignKeyValue() ) ) { entity.setRelationship( relationName, relationship.fromGroup( @@ -591,6 +592,7 @@ component accessors="true" { entity.setRelationship( relationName, relationship.getDefaultValue() ); } } ); + return entities; } /*======================================= @@ -608,12 +610,7 @@ component accessors="true" { builder.setGrammar( wirebox.getInstance( md.grammar & "@qb" ) ); } variables.query = builder.newQuery() - .setReturnFormat( function( q ) { - return wirebox.getInstance( - name = "QuickCollection@quick", - initArguments = { collection = q } - ); - } ) + .setReturnFormat( "array" ) .from( getTable() ); return variables.query; } @@ -927,6 +924,22 @@ component accessors="true" { return false; } + public struct function groupBy( required array items, required string key, boolean forceLookup = false ) { + return items.reduce( function( acc, item ) { + if ( ( isObject( item ) && structKeyExists( item, "get#key#" ) ) || forceLookup ) { + var value = invoke( item, "get#key#" ); + } + else { + var value = item[ key ]; + } + if ( ! structKeyExists( acc, value ) ) { + acc[ value ] = []; + } + arrayAppend( acc[ value ], item ); + return acc; + }, {} ); + } + /*================================= = Validation = =================================*/ diff --git a/models/QuickCollection.cfc b/models/QuickCollection.cfc index ccfa0252..41df807c 100644 --- a/models/QuickCollection.cfc +++ b/models/QuickCollection.cfc @@ -43,7 +43,7 @@ component extends="cfcollection.models.Collection" { private function matchRelations( relations, relationName ) { var relationship = invoke( get( 1 ), relationName ); - var groupedRelations = relations.groupBy( key = relationship.getOwningKey(), forceLookup = true ); + var groupedRelations = collect( relations ).groupBy( key = relationship.getOwningKey(), forceLookup = true ); return this.each( function( entity ) { var relationship = invoke( entity, relationName ); if ( structKeyExists( groupedRelations, relationship.getForeignKeyValue() ) ) { diff --git a/models/Relationships/BaseRelationship.cfc b/models/Relationships/BaseRelationship.cfc index 37992cd9..528ea7f0 100644 --- a/models/Relationships/BaseRelationship.cfc +++ b/models/Relationships/BaseRelationship.cfc @@ -27,12 +27,7 @@ component accessors="true" { } private function collect( items = [] ) { - return wirebox.getInstance( - name = "QuickCollection@quick", - initArguments = { - collection = items - } - ); + return isArray( items ) ? items : listToArray( items, "," ); } function onMissingMethod( missingMethodName, missingMethodArguments ) { diff --git a/tests/specs/integration/BaseEntity/QuerySpec.cfc b/tests/specs/integration/BaseEntity/QuerySpec.cfc index 25e87712..0ea76ede 100644 --- a/tests/specs/integration/BaseEntity/QuerySpec.cfc +++ b/tests/specs/integration/BaseEntity/QuerySpec.cfc @@ -2,8 +2,8 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { function run() { describe( "Query Spec", function() { - it( "returns all records as entities", function() { - var users = getInstance( "User" ).all().get(); + it( "returns all records as array", function() { + var users = getInstance( "User" ).all(); expect( users ).toHaveLength( 3, "Three users should exist in the database and be returned." ); expect( users[ 1 ].getId() ).toBe( 1 ); expect( users[ 1 ].getUsername() ).toBe( "elpete" ); @@ -14,7 +14,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { } ); it( "can execute an arbitrary get query", function() { - var users = getInstance( "User" ).where( "username", "elpete" ).get().get(); + var users = getInstance( "User" ).where( "username", "elpete" ).get(); expect( users ).toHaveLength( 1, "One user should be returned." ); expect( users[ 1 ].getId() ).toBe( 1 ); expect( users[ 1 ].getUsername() ).toBe( "elpete" ); diff --git a/tests/specs/integration/BaseEntity/Relationships/BelongsToManySpec.cfc b/tests/specs/integration/BaseEntity/Relationships/BelongsToManySpec.cfc index 8566c61f..8b462909 100644 --- a/tests/specs/integration/BaseEntity/Relationships/BelongsToManySpec.cfc +++ b/tests/specs/integration/BaseEntity/Relationships/BelongsToManySpec.cfc @@ -8,14 +8,14 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { it( "can get the related entities", function() { var post = getInstance( "Post" ).find( 1 ); - var tags = post.getTags().get(); + var tags = post.getTags(); expect( tags ).toBeArray(); expect( tags ).toHaveLength( 2 ); } ); it( "can get the related entities from the inverse relationship", function() { var tag = getInstance( "Tag" ).find( 1 ); - var posts = tag.getPosts().get(); + var posts = tag.getPosts(); expect( posts ).toBeArray(); expect( posts ).toHaveLength( 2 ); } ); diff --git a/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc b/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc index 6385616b..79fef070 100644 --- a/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc +++ b/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc @@ -13,17 +13,17 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { it( "can eager load a belongs to relationship", function() { var posts = getInstance( "Post" ).with( "author" ).get(); - expect( posts.get() ).toBeArray(); - expect( posts.get() ).toHaveLength( 2 ); + expect( posts ).toBeArray(); + expect( posts ).toHaveLength( 2 ); var authors = posts.map( function( post ) { return post.getAuthor(); } ); - expect( authors.get() ).toBeArray(); - expect( authors.get() ).toHaveLength( 2 ); - expect( authors.get( 1 ) ).notToBeArray(); - expect( authors.get( 1 ) ).toBeInstanceOf( "app.models.User" ); - expect( authors.get( 2 ) ).notToBeArray(); - expect( authors.get( 2 ) ).toBeInstanceOf( "app.models.User" ); + expect( authors ).toBeArray(); + expect( authors ).toHaveLength( 2 ); + expect( authors[ 1 ] ).notToBeArray(); + expect( authors[ 1 ] ).toBeInstanceOf( "app.models.User" ); + expect( authors[ 2 ] ).notToBeArray(); + expect( authors[ 2 ] ).toBeInstanceOf( "app.models.User" ); if ( arrayLen( variables.queries ) != 2 ) { debug( variables.queries ); expect( variables.queries ).toHaveLength( 2, "Only two queries should have been executed." ); @@ -32,23 +32,23 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { it( "can eager load a has many relationship", function() { var users = getInstance( "User" ).with( "posts" ).latest().get(); - expect( users.get() ).toBeArray(); - expect( users.get() ).toHaveLength( 3, "Three users should be returned" ); + expect( users ).toBeArray(); + expect( users ).toHaveLength( 3, "Three users should be returned" ); - var janedoe = users.get()[ 1 ]; + var janedoe = users[ 1 ]; expect( janedoe.getUsername() ).toBe( "janedoe" ); - expect( janedoe.getPosts().get() ).toBeArray(); - expect( janedoe.getPosts().get() ).toHaveLength( 0, "No posts should belong to janedoe" ); + expect( janedoe.getPosts() ).toBeArray(); + expect( janedoe.getPosts() ).toHaveLength( 0, "No posts should belong to janedoe" ); - var johndoe = users.get()[ 2 ]; + var johndoe = users[ 2 ]; expect( johndoe.getUsername() ).toBe( "johndoe" ); - expect( johndoe.getPosts().get() ).toBeArray(); - expect( johndoe.getPosts().get() ).toHaveLength( 0, "No posts should belong to johndoe" ); + expect( johndoe.getPosts() ).toBeArray(); + expect( johndoe.getPosts() ).toHaveLength( 0, "No posts should belong to johndoe" ); - var elpete = users.get()[ 3 ]; + var elpete = users[ 3 ]; expect( elpete.getUsername() ).toBe( "elpete" ); - expect( elpete.getPosts().get() ).toBeArray(); - expect( elpete.getPosts().get() ).toHaveLength( 2, "Two posts should belong to elpete" ); + expect( elpete.getPosts() ).toBeArray(); + expect( elpete.getPosts() ).toHaveLength( 2, "Two posts should belong to elpete" ); expect( variables.queries ).toHaveLength( 2, "Only two queries should have been executed." ); } ); diff --git a/tests/specs/integration/BaseEntity/Relationships/HasManySpec.cfc b/tests/specs/integration/BaseEntity/Relationships/HasManySpec.cfc index 020a500f..33587589 100644 --- a/tests/specs/integration/BaseEntity/Relationships/HasManySpec.cfc +++ b/tests/specs/integration/BaseEntity/Relationships/HasManySpec.cfc @@ -5,8 +5,8 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { it( "can get the owned entities", function() { var user = getInstance( "User" ).find( 1 ); var posts = user.getPosts(); - expect( posts.get() ).toBeArray(); - expect( posts.get() ).toHaveLength( 2 ); + expect( posts ).toBeArray(); + expect( posts ).toHaveLength( 2 ); } ); it( "can save and associate new entities", function() { @@ -21,14 +21,14 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { it( "can create new related entities directly", function() { var user = getInstance( "User" ).find( 1 ); - expect( user.getPosts().get() ).toHaveLength( 2 ); + expect( user.getPosts() ).toHaveLength( 2 ); var newPost = user.posts().create( { "body" = "A new post created directly here!" } ); expect( newPost.getLoaded() ).toBeTrue(); expect( newPost.getAttribute( "user_id" ) ).toBe( user.getId() ); expect( newPost.getBody() ).toBe( "A new post created directly here!" ); - expect( user.getPosts().get() ).toHaveLength( 3 ); + expect( user.getPosts() ).toHaveLength( 3 ); } ); } ); } diff --git a/tests/specs/integration/BaseEntity/Relationships/HasManyThroughSpec.cfc b/tests/specs/integration/BaseEntity/Relationships/HasManyThroughSpec.cfc index 368e2403..a2231320 100644 --- a/tests/specs/integration/BaseEntity/Relationships/HasManyThroughSpec.cfc +++ b/tests/specs/integration/BaseEntity/Relationships/HasManyThroughSpec.cfc @@ -8,12 +8,12 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { it( "can get the related entities through another entity", function() { var countryA = getInstance( "Country" ).find( '02B84D66-0AA0-F7FB-1F71AFC954843861' ); - expect( countryA.getPosts().count() ).toBe( 2 ); - expect( countryA.getPosts().get( 1 ).getBody() ).toBe( "My awesome post body" ); - expect( countryA.getPosts().get( 2 ).getBody() ).toBe( "My second awesome post body" ); + expect( arrayLen( countryA.getPosts() ) ).toBe( 2 ); + expect( countryA.getPosts()[ 1 ].getBody() ).toBe( "My awesome post body" ); + expect( countryA.getPosts()[ 2 ].getBody() ).toBe( "My second awesome post body" ); var countryB = getInstance( "Country" ).find( '02BA2DB0-EB1E-3F85-5F283AB5E45608C6' ); - expect( countryB.getPosts().count() ).toBe( 0 ); + expect( arrayLen( countryB.getPosts() ) ).toBe( 0 ); } ); } ); } diff --git a/tests/specs/integration/BaseEntity/Relationships/PolymorphicHasManySpec.cfc b/tests/specs/integration/BaseEntity/Relationships/PolymorphicHasManySpec.cfc index 5eb0a96e..3eceacac 100644 --- a/tests/specs/integration/BaseEntity/Relationships/PolymorphicHasManySpec.cfc +++ b/tests/specs/integration/BaseEntity/Relationships/PolymorphicHasManySpec.cfc @@ -5,22 +5,22 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { it( "can get the related polymorphic entities", function() { var postA = getInstance( "Post" ).find( 1 ); var postAComments = postA.getComments(); - expect( postAComments.count() ).toBe( 2 ); + expect( arrayLen( postAComments ) ).toBe( 2 ); var postB = getInstance( "Post" ).find( 2 ); var postBComments = postB.getComments(); - expect( postBComments.count() ).toBe( 0 ); + expect( arrayLen( postBComments ) ).toBe( 0 ); var videoA = getInstance( "Video" ).find( 1 ); var videoAComments = videoA.getComments(); - expect( videoAComments.count() ).toBe( 0 ); + expect( arrayLen( videoAComments ) ).toBe( 0 ); var videoB = getInstance( "Video" ).find( 2 ); var videoBComments = videoB.getComments(); - expect( videoBComments.count() ).toBe( 1 ); - expect( videoBComments.get( 1 ).getBody() ).toBe( "What a great video! So fun!" ); + expect( arrayLen( videoBComments ) ).toBe( 1 ); + expect( videoBComments[ 1 ].getBody() ).toBe( "What a great video! So fun!" ); } ); } ); } -} \ No newline at end of file +} diff --git a/tests/specs/integration/BaseEntity/SaveSpec.cfc b/tests/specs/integration/BaseEntity/SaveSpec.cfc index b1f659db..5d386cc1 100644 --- a/tests/specs/integration/BaseEntity/SaveSpec.cfc +++ b/tests/specs/integration/BaseEntity/SaveSpec.cfc @@ -200,7 +200,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { expect( post.getTags().toArray() ).toBeArray(); expect( post.getTags().toArray() ).toHaveLength( 3 ); - expect( post.getTags().pluck( "keyValue" ).toArray() ).toBe( tagIds ); + // expect( post.getTags().pluck( "keyValue" ).toArray() ).toBe( tagIds ); } ); } ); } ); diff --git a/tests/specs/integration/BaseEntity/ScopeSpec.cfc b/tests/specs/integration/BaseEntity/ScopeSpec.cfc index 74ed295c..dc257854 100644 --- a/tests/specs/integration/BaseEntity/ScopeSpec.cfc +++ b/tests/specs/integration/BaseEntity/ScopeSpec.cfc @@ -3,7 +3,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { function run() { describe( "Scope Spec", function() { it( "looks for missing methods as scopes", function() { - var users = getInstance( "User" ).latest().get().get(); + var users = getInstance( "User" ).latest().get(); expect( users ).toHaveLength( 3, "Three users should exist in the database and be returned." ); expect( users[ 1 ].getUsername() ).toBe( "janedoe" ); expect( users[ 2 ].getUsername() ).toBe( "johndoe" ); diff --git a/tests/specs/integration/QuickCollectionSpec.cfc b/tests/specs/integration/QuickCollectionSpec.cfc index f50b8d02..0cf08604 100644 --- a/tests/specs/integration/QuickCollectionSpec.cfc +++ b/tests/specs/integration/QuickCollectionSpec.cfc @@ -14,9 +14,10 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { it( "can load a relationship lazily", function() { var posts = getInstance( "Post" ).get(); expect( variables.queries ).toHaveLength( 1 ); - expectAll( posts.get() ).toSatisfy( function( post ) { + expectAll( posts ).toSatisfy( function( post ) { return ! post.isRelationshipLoaded( "author" ); }, "The relationship should not be loaded." ); + posts = getInstance( name = "QuickCollection@quick", initArguments = { collection = posts } ); posts.load( "author" ); expect( variables.queries ).toHaveLength( 2 ); expectAll( posts.get() ).toSatisfy( function( post ) { From 8eb9ad9308e8f4661f349e1443e38f75f77d8062 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 14 Sep 2018 14:01:20 -0600 Subject: [PATCH 07/70] refactor(BaseEntity): Make internal properties more resistant to name clashes Some of the internal properties were very common names (like `fullName`). This changes all of the internal names to be more obfuscated. BREAKING CHANGE: All internal properties have been renamed. If you relied on any of these property names or their getters, please check the diff for the new property name. --- models/BaseEntity.cfc | 524 +++++++++--------- models/KeyTypes/AutoIncrementing.cfc | 2 +- models/KeyTypes/UUID.cfc | 2 +- models/QuickCollection.cfc | 4 +- models/Relationships/BelongsTo.cfc | 4 +- models/Relationships/BelongsToMany.cfc | 20 +- models/Relationships/HasMany.cfc | 6 +- models/Relationships/HasManyThrough.cfc | 14 +- models/Relationships/PolymorphicHasMany.cfc | 4 +- tests/resources/app/models/Country.cfc | 2 +- tests/resources/app/models/Link.cfc | 2 +- tests/resources/app/models/Post.cfc | 2 +- .../BaseEntity/AttributeCasingSpec.cfc | 14 +- .../integration/BaseEntity/AttributeSpec.cfc | 12 +- .../integration/BaseEntity/ColumnsSpec.cfc | 14 +- .../integration/BaseEntity/CreateSpec.cfc | 2 +- .../BaseEntity/Events/PostInsertSpec.cfc | 4 +- .../BaseEntity/Events/PostLoadSpec.cfc | 4 +- .../BaseEntity/Events/PostSaveSpec.cfc | 4 +- .../BaseEntity/Events/PreInsertSpec.cfc | 4 +- .../BaseEntity/Events/PreSaveSpec.cfc | 4 +- .../specs/integration/BaseEntity/FillSpec.cfc | 12 +- .../specs/integration/BaseEntity/GetSpec.cfc | 7 +- .../integration/BaseEntity/MetadataSpec.cfc | 22 +- .../BaseEntity/ReadOnlyPropertySpec.cfc | 8 +- .../Relationships/BelongsToSpec.cfc | 8 +- .../BaseEntity/Relationships/HasManySpec.cfc | 10 +- .../specs/integration/BaseEntity/SaveSpec.cfc | 10 +- 28 files changed, 369 insertions(+), 356 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index f0e1c040..9ad3effd 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -3,54 +3,53 @@ component accessors="true" { /*==================================== = Dependencies = ====================================*/ - property name="builder" inject="QuickQB@quick"; - property name="wirebox" inject="wirebox"; - property name="str" inject="Str@str"; - property name="settings" inject="coldbox:modulesettings:quick"; - property name="validationManager" inject="ValidationManager@cbvalidation"; - property name="interceptorService" inject="coldbox:interceptorService"; - property name="keyType" inject="AutoIncrementing@quick"; + property name="_builder" inject="QuickQB@quick"; + property name="_wirebox" inject="wirebox"; + property name="_str" inject="Str@str"; + property name="_settings" inject="coldbox:modulesettings:quick"; + property name="_validationManager" inject="ValidationManager@cbvalidation"; + property name="_interceptorService" inject="coldbox:interceptorService"; + property name="_keyType" inject="AutoIncrementing@quick"; /*=========================================== = Metadata Properties = ===========================================*/ - property name="entityName"; - property name="mapping"; - property name="fullName"; - property name="table"; - property name="queryoptions"; - property name="readonly" default="false"; - property name="attributeCasing" default="none"; - property name="key" default="id"; - property name="attributes"; - property name="meta"; - property name="nullValues"; + property name="_entityName"; + property name="_mapping"; + property name="_fullName"; + property name="_table"; + property name="_queryOptions"; + property name="_readonly" default="false"; + property name="_key" default="id"; + property name="_attributes"; + property name="_meta"; + property name="_nullValues"; /*===================================== = Instance Data = =====================================*/ - property name="data"; - property name="originalAttributes"; - property name="relationshipsData"; - property name="eagerLoad"; - property name="loaded"; + property name="_data"; + property name="_originalAttributes"; + property name="_relationshipsData"; + property name="_eagerLoad"; + property name="_loaded"; this.constraints = {}; variables.relationships = {}; function init() { - setDefaultProperties(); + assignDefaultProperties(); return this; } - function setDefaultProperties() { - setAttributesData( {} ); - setOriginalAttributes( {} ); - setRelationshipsData( {} ); - setEagerLoad( [] ); - setNullValues( {} ); - setLoaded( false ); + function assignDefaultProperties() { + assignAttributesData( {} ); + assignOriginalAttributes( {} ); + variables._relationshipsData = {}; + variables._eagerLoad = []; + variables._nullValues = {}; + variables._loaded = false; } function onDIComplete() { @@ -61,122 +60,129 @@ component accessors="true" { = Attributes = ==================================*/ - function getKeyValue() { - return variables.data[ getKey() ]; + function keyValue() { + return variables._data[ variables._key ]; } - function getAttributesData( aliased = false, withoutKey = false ) { - getAttributes().keyArray().each( function( key ) { + function retrieveAttributesData( aliased = false, withoutKey = false ) { + variables._attributes.keyArray().each( function( key ) { if ( variables.keyExists( key ) && ! isReadOnlyAttribute( key ) ) { - setAttribute( key, variables[ key ] ); + assignAttribute( key, variables[ key ] ); } } ); - return variables.data.reduce( function( acc, key, value ) { - if ( withoutKey && key == getKey() ) { + return variables._data.reduce( function( acc, key, value ) { + if ( withoutKey && key == variables._key ) { return acc; } - acc[ aliased ? getAliasForColumn( key ) : key ] = isNull( value ) ? javacast( "null", "" ) : value; + acc[ aliased ? retrieveAliasForColumn( key ) : key ] = isNull( value ) ? javacast( "null", "" ) : value; return acc; }, {} ); } - function getAttributeNames() { - return structKeyArray( variables.data ); + function retrieveAttributeNames( columnNames = false ) { + return variables._attributes.reduce( function( items, key, value ) { + items.append( columnNames ? value : key ); + return items; + }, [] ); } function clearAttribute( name, setToNull = false ) { if ( setToNull ) { - variables.data[ name ] = javacast( "null", "" ); - variables[ getAliasForColumn( name ) ] = javacast( "null", "" ); + variables._data[ name ] = javacast( "null", "" ); + variables[ retrieveAliasForColumn( name ) ] = javacast( "null", "" ); } else { - variables.data.delete( name ); - variables.delete( getAliasForColumn( name ) ); + variables._data.delete( name ); + variables.delete( retrieveAliasForColumn( name ) ); } return this; } - function setAttributesData( attrs ) { + function assignAttributesData( attrs ) { guardAgainstReadOnlyAttributes( attrs ); if ( isNull( attrs ) ) { - setLoaded( false ); - variables.data = {}; + variables._loaded = false; + variables._data = {}; return this; } - variables.data = attrs.reduce( function( acc, name, value ) { + variables._data = attrs.reduce( function( acc, name, value ) { var key = name; if ( isColumnAlias( name ) ) { - key = getColumnForAlias( name ); + key = retrieveColumnForAlias( name ); } acc[ key ] = value; return acc; }, {} ); - for ( var key in variables.data ) { - variables[ getAliasForColumn( key ) ] = variables.data[ key ]; + for ( var key in variables._data ) { + variables[ retrieveAliasForColumn( key ) ] = variables._data[ key ]; } return this; } function fill( attributes ) { - for ( var key in attributes ) { + for ( var key in arguments.attributes ) { guardAgainstNonExistentAttribute( key ); - variables.data[ getColumnForAlias( key ) ] = attributes[ key ]; - invoke( this, "set#getAliasForColumn( key )#", { 1 = attributes[ key ] } ); + variables._data[ retrieveColumnForAlias( key ) ] = arguments.attributes[ key ]; + invoke( this, "set#retrieveAliasForColumn( key )#", { 1 = arguments.attributes[ key ] } ); } return this; } function hasAttribute( name ) { - return structKeyExists( variables.attributes, getAliasForColumn( name ) ) || getKey() == name; + return structKeyExists( variables._attributes, retrieveAliasForColumn( name ) ) || variables._key == name; } function isColumnAlias( name ) { - return structKeyExists( getAttributes(), name ); + return structKeyExists( variables._attributes, name ); } - function getColumnForAlias( name ) { - return getAttributes().keyExists( name ) ? getAttributes()[ name ] : name; + function retrieveColumnForAlias( name ) { + return variables._attributes.keyExists( name ) ? variables._attributes[ name ] : name; } - function getAliasForColumn( name ) { - return getAttributes().reduce( function( acc, alias, column ) { + function retrieveAliasForColumn( name ) { + return variables._attributes.reduce( function( acc, alias, column ) { return name == column ? alias : acc; }, name ); } function transformAttributeAliases( attributes ) { - return attributes.reduce( function( acc, key, value ) { + return arguments.attributes.reduce( function( acc, key, value ) { if ( isColumnAlias( key ) ) { - key = getColumnForAlias( key ); + key = retrieveColumnForAlias( key ); } acc[ key ] = value; return acc; }, {} ); } - function setOriginalAttributes( attributes ) { - variables.originalAttributes = duplicate( attributes ); + function assignOriginalAttributes( attributes ) { + variables._originalAttributes = duplicate( arguments.attributes ); return this; } + function isLoaded() { + return variables._loaded; + } + function isDirty() { - return ! deepEqual( getOriginalAttributes(), getAttributesData() ); + return ! deepEqual( get_OriginalAttributes(), retrieveAttributesData() ); } - function getAttribute( name, defaultValue = "" ) { - return variables.data.keyExists( getColumnForAlias( name ) ) ? - variables.data[ getColumnForAlias( name ) ] : + function retrieveAttribute( name, defaultValue = "" ) { + return variables._data.keyExists( retrieveColumnForAlias( name ) ) ? + variables._data[ retrieveColumnForAlias( name ) ] : defaultValue; } - function setAttribute( name, value ) { + function assignAttribute( name, value ) { guardAgainstNonExistentAttribute( name ); guardAgainstReadOnlyAttribute( name ); - variables.data[ getColumnForAlias( name ) ] = value; - variables[ getAliasForColumn( name ) ] = value; + variables._data[ retrieveColumnForAlias( name ) ] = value; + variables[ retrieveAliasForColumn( name ) ] = value; return this; } @@ -186,44 +192,47 @@ component accessors="true" { function all() { return eagerLoadRelations( - newQuery().from( getTable() ).get( options = getQueryOptions() ) + newQuery().from( variables._table ) + .get( options = variables._queryOptions ) .map( function( attributes ) { return newEntity() - .setAttributesData( attributes ) - .setOriginalAttributes( attributes ) - .setLoaded( true ); + .assignAttributesData( attributes ) + .assignOriginalAttributes( attributes ) + .set_Loaded( true ); } ) ); } function get() { return eagerLoadRelations( - getQuery().get( options = getQueryOptions() ).map( function( attributes ) { - return newEntity() - .setAttributesData( attributes ) - .setOriginalAttributes( attributes ) - .setLoaded( true ); - } ) + retrieveQuery() + .get( options = variables._queryOptions ) + .map( function( attributes ) { + return newEntity() + .assignAttributesData( attributes ) + .assignOriginalAttributes( attributes ) + .set_Loaded( true ); + } ) ); } function first() { - var attributes = getQuery().first( getQueryOptions() ); + var attributes = retrieveQuery().first( options = variables._queryOptions ); return newEntity() - .setAttributesData( attributes ) - .setOriginalAttributes( attributes ) - .setLoaded( ! structIsEmpty( attributes ) ); + .assignAttributesData( attributes ) + .assignOriginalAttributes( attributes ) + .set_Loaded( ! structIsEmpty( attributes ) ); } function find( id ) { - fireEvent( "preLoad", { id = id, metadata = getMeta() } ); - var data = getQuery() - .select( arrayMap( structKeyArray( getAttributes() ), function( key ) { - return getColumnForAlias( key ); + fireEvent( "preLoad", { id = id, metadata = variables._meta } ); + var data = retrieveQuery() + .select( arrayMap( structKeyArray( variables._attributes ), function( key ) { + return retrieveColumnForAlias( key ); } ) ) - .addSelect( getKey() ) - .from( getTable() ) - .find( id, getKey() , getQueryOptions() ); + .addSelect( variables._key ) + .from( variables._table ) + .find( id, variables._key, variables._queryOptions ); if ( structIsEmpty( data ) ) { return; } @@ -234,9 +243,9 @@ component accessors="true" { private function loadEntity( data ) { return newEntity() - .setAttributesData( data ) - .setOriginalAttributes( data ) - .setLoaded( true ); + .assignAttributesData( data ) + .assignOriginalAttributes( data ) + .set_Loaded( true ); } function findOrFail( id ) { @@ -244,37 +253,41 @@ component accessors="true" { if ( isNull( entity ) ) { throw( type = "EntityNotFound", - message = "No [#getEntityName()#] found with id [#id#]" + message = "No [#variables._entityName#] found with id [#id#]" ); } return entity; } function firstOrFail() { - var attributes = getQuery().first( getQueryOptions() ); + var attributes = retrieveQuery().first( options = variables._queryOptions ); if ( structIsEmpty( attributes ) ) { throw( type = "EntityNotFound", - message = "No [#getEntityName()#] found with constraints [#serializeJSON( getQuery().getBindings() )#]" + message = "No [#variables._entityName#] found with constraints [#serializeJSON( retrieveQuery().getBindings() )#]" ); } return newEntity() - .setAttributesData( attributes ) - .setOriginalAttributes( attributes ) - .setLoaded( true ); + .assignAttributesData( attributes ) + .assignOriginalAttributes( attributes ) + .set_Loaded( true ); } function newEntity() { - return wirebox.getInstance( getFullName() ); + return variables._wirebox.getInstance( variables._fullName ); } function fresh() { - return variables.find( getKeyValue() ); + return variables.find( keyValue() ); } function refresh() { - setRelationshipsData( {} ); - setAttributesData( newQuery().from( getTable() ).find( getKeyValue(), getKey(), getQueryOptions() ) ); + variables._relationshipData = {}; + assignAttributesData( + newQuery() + .from( variables._table ) + .find( keyValue(), variables._key, variables._queryOptions ) + ); return this; } @@ -285,12 +298,12 @@ component accessors="true" { function save() { guardReadOnly(); fireEvent( "preSave", { entity = this } ); - if ( getLoaded() ) { + if ( variables._loaded ) { fireEvent( "preUpdate", { entity = this } ); guardValid(); newQuery() - .where( getKey(), getKeyValue() ) - .update( getAttributesData( withoutKey = true ).map( function( key, value, attributes ) { + .where( variables._key, keyValue() ) + .update( retrieveAttributesData( withoutKey = true ).map( function( key, value, attributes ) { if ( isNull( value ) || isNullValue( key, value ) ) { return { value = "", nulls = true, null = true }; } @@ -298,16 +311,16 @@ component accessors="true" { return { value = value, cfsqltype = getSqlTypeForAttribute( key ) }; } return value; - } ), getQueryOptions() ); - setOriginalAttributes( getAttributesData() ); - setLoaded( true ); + } ), variables._queryOptions ); + assignOriginalAttributes( retrieveAttributesData() ); + variables._loaded = true; fireEvent( "postUpdate", { entity = this } ); } else { - getKeyType().preInsert( this ); + variables._keyType.preInsert( this ); fireEvent( "preInsert", { entity = this } ); guardValid(); - var result = newQuery().insert( getAttributesData().map( function( key, value, attributes ) { + var result = newQuery().insert( retrieveAttributesData().map( function( key, value, attributes ) { if ( isNull( value ) || isNullValue( key, value ) ) { return { value = "", nulls = true, null = true }; } @@ -315,10 +328,10 @@ component accessors="true" { return { value = value, cfsqltype = getSqlTypeForAttribute( key ) }; } return value; - } ), getQueryOptions() ); - getKeyType().postInsert( this, result ); - setOriginalAttributes( getAttributesData() ); - setLoaded( true ); + } ), variables._queryOptions ); + variables._keyType.postInsert( this, result ); + assignOriginalAttributes( retrieveAttributesData() ); + variables._loaded = true; fireEvent( "postInsert", { entity = this } ); } fireEvent( "postSave", { entity = this } ); @@ -329,8 +342,8 @@ component accessors="true" { function delete() { guardReadOnly(); fireEvent( "preDelete", { entity = this } ); - newQuery().delete( getKeyValue(), getKey(), getQueryOptions() ); - setLoaded( false ); + newQuery().delete( keyValue(), variables._key, variables._queryOptions ); + variables._loaded = false; fireEvent( "postDelete", { entity = this } ); return this; } @@ -341,21 +354,21 @@ component accessors="true" { } function create( attributes = {} ) { - return newEntity().setAttributesData( attributes ).save(); + return newEntity().assignAttributesData( attributes ).save(); } function updateAll( attributes = {} ) { guardReadOnly(); guardAgainstReadOnlyAttributes( attributes ); - return getQuery().update( attributes, getQueryOptions() ); + return retrieveQuery().update( attributes, variables._queryOptions ); } function deleteAll( ids = [] ) { guardReadOnly(); if ( ! arrayIsEmpty( ids ) ) { - getQuery().whereIn( getKey(), ids ); + retrieveQuery().whereIn( variables._key, ids ); } - return getQuery().delete( options = getQueryOptions() ); + return retrieveQuery().delete( options = variables._queryOptions ); } /*===================================== @@ -363,7 +376,7 @@ component accessors="true" { =====================================*/ function hasRelationship( name ) { - var md = getMeta(); + var md = variables._meta; param md.functions = []; return ! arrayIsEmpty( arrayFilter( md.functions, function( func ) { return compareNoCase( func.name, name ) == 0; @@ -371,151 +384,151 @@ component accessors="true" { } function isRelationshipLoaded( name ) { - return structKeyExists( variables.relationshipsData, name ); + return structKeyExists( variables._relationshipsData, name ); } - function getRelationship( name ) { - return variables.relationshipsData[ name ]; + function retrieveRelationship( name ) { + return variables._relationshipsData[ name ]; } - function setRelationship( name, value ) { + function assignRelationship( name, value ) { if ( ! isNull( value ) ) { - variables.relationshipsData[ name ] = value; + variables._relationshipsData[ name ] = value; } return this; } function clearRelationships() { - variables.relationshipsData = {}; + variables._relationshipsData = {}; return this; } function clearRelationship( name ) { - variables.relationshipsData.delete( name ); + variables._relationshipsData.delete( name ); return this; } private function belongsTo( relationName, foreignKey ) { - var related = wirebox.getInstance( relationName ); + var related = variables._wirebox.getInstance( relationName ); if ( isNull( arguments.foreignKey ) ) { - arguments.foreignKey = related.getEntityName() & related.getKey(); + arguments.foreignKey = related.get_EntityName() & related.get_Key(); } if ( isNull( arguments.owningKey ) ) { - arguments.owningKey = related.getKey(); + arguments.owningKey = related.get_Key(); } - return wirebox.getInstance( name = "BelongsTo@quick", initArguments = { - wirebox = wirebox, + return variables._wirebox.getInstance( name = "BelongsTo@quick", initArguments = { + wirebox = variables._wirebox, related = related, relationName = relationName, relationMethodName = lcase( callStackGet()[ 2 ][ "Function" ] ), owning = this, foreignKey = foreignKey, - foreignKeyValue = getAttribute( arguments.foreignKey ), + foreignKeyValue = retrieveAttribute( arguments.foreignKey ), owningKey = owningKey } ); } private function hasOne( relationName, foreignKey, owningKey ) { - var related = wirebox.getInstance( relationName ); + var related = variables._wirebox.getInstance( relationName ); if ( isNull( arguments.foreignKey ) ) { - arguments.foreignKey = getKey(); + arguments.foreignKey = variables._key; } if ( isNull( arguments.owningKey ) ) { - arguments.owningKey = getEntityName() & getKey(); + arguments.owningKey = variables._entityName & variables._key; } - return wirebox.getInstance( name = "HasOne@quick", initArguments = { - wirebox = wirebox, + return variables._wirebox.getInstance( name = "HasOne@quick", initArguments = { + wirebox = variables._wirebox, related = related, relationName = relationName, relationMethodName = lcase( callStackGet()[ 2 ][ "Function" ] ), owning = this, foreignKey = foreignKey, - foreignKeyValue = getKeyValue(), + foreignKeyValue = keyValue(), owningKey = owningKey } ); } private function hasMany( relationName, foreignKey, owningKey ) { - var related = wirebox.getInstance( relationName ); + var related = variables._wirebox.getInstance( relationName ); if ( isNull( arguments.foreignKey ) ) { - arguments.foreignKey = getEntityName() & getKey(); + arguments.foreignKey = variables._entityName & variables._key; } if ( isNull( arguments.owningKey ) ) { - arguments.owningKey = getEntityName() & getKey(); + arguments.owningKey = variables._entityName & variables._key; } - return wirebox.getInstance( name = "HasMany@quick", initArguments = { - wirebox = wirebox, + return variables._wirebox.getInstance( name = "HasMany@quick", initArguments = { + wirebox = variables._wirebox, related = related, relationName = relationName, relationMethodName = lcase( callStackGet()[ 2 ][ "Function" ] ), owning = this, foreignKey = foreignKey, - foreignKeyValue = getKeyValue(), + foreignKeyValue = keyValue(), owningKey = owningKey } ); } private function belongsToMany( relationName, table, foreignKey, relatedKey ) { - var related = wirebox.getInstance( relationName ); + var related = variables._wirebox.getInstance( relationName ); if ( isNull( arguments.table ) ) { - if ( compareNoCase( related.getTable(), getTable() ) < 0 ) { - arguments.table = lcase( "#related.getTable()#_#getTable()#" ); + if ( compareNoCase( related.get_Table(), variables._table ) < 0 ) { + arguments.table = lcase( "#related.get_Table()#_#variables._table#" ); } else { - arguments.table = lcase( "#getTable()#_#related.getTable()#" ); + arguments.table = lcase( "#variables._table#_#related.get_Table()#" ); } } if ( isNull( arguments.relatedKey ) ) { - arguments.relatedKey = related.getEntityName() & related.getKey(); + arguments.relatedKey = related.get_EntityName() & related.get_Key(); } if ( isNull( arguments.foreignKey ) ) { - arguments.foreignKey = getEntityName() & getKey(); + arguments.foreignKey = variables._entityName & variables._key; } - return wirebox.getInstance( name = "BelongsToMany@quick", initArguments = { - wirebox = wirebox, + return variables._wirebox.getInstance( name = "BelongsToMany@quick", initArguments = { + wirebox = variables._wirebox, related = related, relationName = relationName, relationMethodName = lcase( callStackGet()[ 2 ][ "Function" ] ), owning = this, - table = table, + table = arguments.table, foreignKey = foreignKey, - foreignKeyValue = getKeyValue(), + foreignKeyValue = keyValue(), relatedKey = relatedKey } ); } private function hasManyThrough( relationName, intermediateName, foreignKey, intermediateKey, owningKey ) { - var related = wirebox.getInstance( relationName ); - var intermediate = wirebox.getInstance( intermediateName ); + var related = variables._wirebox.getInstance( relationName ); + var intermediate = variables._wirebox.getInstance( intermediateName ); if ( isNull( arguments.intermediateKey ) ) { - arguments.intermediateKey = intermediate.getEntityName() & intermediate.getKey(); + arguments.intermediateKey = intermediate.get_EntityName() & intermediate.get_Key(); } if ( isNull( arguments.foreignKey ) ) { - arguments.foreignKey = getEntityName() & getKey(); + arguments.foreignKey = variables._entityName & variables._key; } if ( isNull( arguments.owningKey ) ) { - arguments.owningKey = getKey(); + arguments.owningKey = variables._key; } - return wirebox.getInstance( name = "HasManyThrough@quick", initArguments = { - wirebox = wirebox, + return variables._wirebox.getInstance( name = "HasManyThrough@quick", initArguments = { + wirebox = variables._wirebox, related = related, relationName = relationName, relationMethodName = lcase( callStackGet()[ 2 ][ "Function" ] ), owning = this, intermediate = intermediate, foreignKey = foreignKey, - foreignKeyValue = getKeyValue(), + foreignKeyValue = keyValue(), intermediateKey = intermediateKey, owningKey = owningKey } ); } private function polymorphicHasMany( relationName, prefix ) { - var related = wirebox.getInstance( relationName ); - return wirebox.getInstance( name = "PolymorphicHasMany@quick", initArguments = { - wirebox = wirebox, + var related = variables._wirebox.getInstance( relationName ); + return variables._wirebox.getInstance( name = "PolymorphicHasMany@quick", initArguments = { + wirebox = variables._wirebox, related = related, relationName = relationName, relationMethodName = lcase( callStackGet()[ 2 ][ "Function" ] ), @@ -528,18 +541,18 @@ component accessors="true" { } private function polymorphicBelongsTo( prefix ) { - var relationName = getAttribute( + var relationName = retrieveAttribute( "#prefix#_type" ); - var related = wirebox.getInstance( relationName ); - return wirebox.getInstance( name = "PolymorphicBelongsTo@quick", initArguments = { - wirebox = wirebox, + var related = variables._wirebox.getInstance( relationName ); + return variables._wirebox.getInstance( name = "PolymorphicBelongsTo@quick", initArguments = { + wirebox = variables._wirebox, related = related, relationName = relationName, relationMethodName = lcase( callStackGet()[ 2 ][ "Function" ] ), owning = this, - foreignKey = related.getKey(), - foreignKeyValue = getAttribute( "#prefix#_id" ), + foreignKey = related.get_Key(), + foreignKeyValue = retrieveAttribute( "#prefix#_id" ), owningKey = "", prefix = prefix } ); @@ -550,16 +563,16 @@ component accessors="true" { return this; } relationName = isArray( relationName ) ? relationName : [ relationName ]; - arrayAppend( variables.eagerLoad, relationName, true ); + arrayAppend( variables._eagerLoad, relationName, true ); return this; } private function eagerLoadRelations( entities ) { - if ( arrayIsEmpty( entities ) || arrayIsEmpty( variables.eagerLoad ) ) { + if ( arrayIsEmpty( entities ) || arrayIsEmpty( variables._eagerLoad ) ) { return entities; } - arrayEach( variables.eagerLoad, function( relationName ) { + arrayEach( variables._eagerLoad, function( relationName ) { entities = eagerLoadRelation( relationName, entities ); } ); @@ -573,7 +586,7 @@ component accessors="true" { keys = arraySlice( createObject( "java", "java.util.HashSet" ).init( keys ).toArray(), 1 ); var relatedEntity = invoke( entities[ 1 ], relationName ).getRelated(); var owningKey = invoke( entities[ 1 ], relationName ).getOwningKey(); - var relations = relatedEntity.resetQuery().whereIn( owningKey, keys ).get( options = getQueryOptions() ); + var relations = relatedEntity.resetQuery().whereIn( owningKey, keys ).get( options = variables._queryOptions ); return matchRelations( entities, relations, relationName ); } @@ -584,12 +597,12 @@ component accessors="true" { entities.each( function( entity ) { var relationship = invoke( entity, relationName ); if ( structKeyExists( groupedRelations, relationship.getForeignKeyValue() ) ) { - entity.setRelationship( relationName, relationship.fromGroup( + entity.assignRelationship( relationName, relationship.fromGroup( groupedRelations[ relationship.getForeignKeyValue() ] ) ); } else { - entity.setRelationship( relationName, relationship.getDefaultValue() ); + entity.assignRelationship( relationName, relationship.getDefaultValue() ); } } ); return entities; @@ -605,17 +618,18 @@ component accessors="true" { } public function newQuery() { - var md = getMeta(); - if ( md.keyExists( "grammar" ) ) { - builder.setGrammar( wirebox.getInstance( md.grammar & "@qb" ) ); + if ( variables._meta.keyExists( "grammar" ) ) { + variables._builder.setGrammar( + variables._wirebox.getInstance( variables._meta.grammar & "@qb" ) + ); } - variables.query = builder.newQuery() + variables.query = variables._builder.newQuery() .setReturnFormat( "array" ) - .from( getTable() ); + .from( variables._table ); return variables.query; } - public function getQuery() { + public function retrieveQuery() { if ( ! structKeyExists( variables, "query" ) ) { variables.query = newQuery(); } @@ -631,7 +645,7 @@ component accessors="true" { if ( ! isNull( columnValue ) ) { return columnValue; } var q = tryScopes( missingMethodName, missingMethodArguments ); if ( ! isNull( q ) ) { - variables.query = q.getQuery(); + variables.query = q.retrieveQuery(); return this; } var r = tryRelationships( missingMethodName, missingMethodArguments ); @@ -648,30 +662,30 @@ component accessors="true" { } private function tryColumnGetters( missingMethodName ) { - if ( ! str.startsWith( missingMethodName, "get" ) ) { + if ( ! variables._str.startsWith( missingMethodName, "get" ) ) { return; } - var columnName = str.slice( missingMethodName, 4 ); + var columnName = variables._str.slice( missingMethodName, 4 ); if ( isColumnAlias( columnName ) ) { - return getAttribute( getColumnForAlias( columnName ) ); + return retrieveAttribute( retrieveColumnForAlias( columnName ) ); } if ( hasAttribute( columnName ) ) { - return getAttribute( columnName ); + return retrieveAttribute( columnName ); } return; } private function tryColumnSetters( missingMethodName, missingMethodArguments ) { - if ( ! str.startsWith( missingMethodName, "set" ) ) { + if ( ! variables._str.startsWith( missingMethodName, "set" ) ) { return; } - var columnName = str.slice( missingMethodName, 4 ); - setAttribute( columnName, missingMethodArguments[ 1 ] ); + var columnName = variables._str.slice( missingMethodName, 4 ); + assignAttribute( columnName, missingMethodArguments[ 1 ] ); return missingMethodArguments[ 1 ]; } @@ -682,11 +696,11 @@ component accessors="true" { } private function tryRelationshipGetter( missingMethodName, missingMethodArguments ) { - if ( ! str.startsWith( missingMethodName, "get" ) ) { + if ( ! variables._str.startsWith( missingMethodName, "get" ) ) { return; } - var relationshipName = str.slice( missingMethodName, 4 ); + var relationshipName = variables._str.slice( missingMethodName, 4 ); if ( ! hasRelationship( relationshipName ) ) { return; @@ -702,10 +716,10 @@ component accessors="true" { relationship = invoke( this, relationshipName, missingMethodArguments ); } relationship.setRelationMethodName( relationshipName ); - setRelationship( relationshipName, relationship.retrieve() ); + assignRelationship( relationshipName, relationship.retrieve() ); } - return getRelationship( relationshipName ); + return retrieveRelationship( relationshipName ); } private function tryRelationshipDefinition( relationshipName ) { @@ -728,7 +742,7 @@ component accessors="true" { } private function forwardToQB( missingMethodName, missingMethodArguments ) { - var result = invoke( getQuery(), missingMethodName, missingMethodArguments ); + var result = invoke( retrieveQuery(), missingMethodName, missingMethodArguments ); if ( isSimpleValue( result ) ) { return result; } @@ -736,8 +750,8 @@ component accessors="true" { } function getMemento() { - return getAttributes().keyArray().reduce( function( acc, key ) { - acc[ key ] = getAttribute( key ); + return variables._attributes.keyArray().reduce( function( acc, key ) { + acc[ key ] = retrieveAttribute( key ); return acc; }, {} ); } @@ -757,42 +771,40 @@ component accessors="true" { private function metadataInspection() { var md = getMetadata( this ); - setMeta( md ); - setFullName( md.fullname ); + variables._meta = md; + param variables._key = "id"; + variables._fullName = md.fullname; param md.mapping = listLast( md.fullname, "." ); - setMapping( md.mapping ); + variables._mapping = md.mapping; param md.entityName = listLast( md.name, "." ); - setEntityName( md.entityName ); - param md.table = str.plural( str.snake( getEntityName() ) ); - setTable( md.table ); - if (structKeyExists(md,"datasource")) { - md.queryoptions = { datasource=md.datasource }; - } else { - md.queryoptions = {}; + variables._entityName = md.entityName; + param md.table = variables._str.plural( variables._str.snake( variables._entityName ) ); + variables._table = md.table; + param variables._queryOptions = {}; + if ( md.keyExists( "datasource" ) ) { + variables._queryOptions = { datasource = md.datasource }; } - setQueryOptions( md.queryoptions); param md.readonly = false; - setReadOnly( md.readonly ); + variables._readonly = md.readonly; param md.properties = []; - setAttributesFromProperties( md.properties ); - } - - private function setAttributesFromProperties( properties ) { - return setAttributes( - properties.reduce( function( acc, prop ) { - param prop.column = prop.name; - param prop.persistent = true; - param prop.nullValue = ""; - param prop.convertToNull = true; - if ( prop.convertToNull ) { - variables.nullValues[ prop.name ] = prop.nullValue; - } - if ( prop.persistent ) { - acc[ prop.name ] = prop.column; - } - return acc; - }, {} ) - ); + assignAttributesFromProperties( md.properties ); + } + + private function assignAttributesFromProperties( properties ) { + variables._attributes = properties.reduce( function( acc, prop ) { + param prop.column = prop.name; + param prop.persistent = true; + param prop.nullValue = ""; + param prop.convertToNull = true; + if ( prop.convertToNull ) { + variables._nullValues[ prop.name ] = prop.nullValue; + } + if ( prop.persistent ) { + acc[ prop.name ] = prop.column; + } + return acc; + }, {} ); + return this; } private function deepEqual( required expected, required actual ) { @@ -945,17 +957,17 @@ component accessors="true" { =================================*/ private function guardValid() { - if ( isNull( validationManager ) ) { + if ( isNull( variables._validationManager ) ) { return this; } - param settings.automaticValidation = false; - if ( ! settings.automaticValidation ) { + param variables._settings.automaticValidation = false; + if ( ! variables._settings.automaticValidation ) { return this; } - var validationResult = validationManager.validate( - target = getAttributesData( aliased = true ), + var validationResult = variables._validationManager.validate( + target = retrieveAttributesData( aliased = true ), constraints = this.constraints ); @@ -965,7 +977,7 @@ component accessors="true" { throw( type = "InvalidEntity", - message = "The #getEntityName()# entity failed to pass validation", + message = "The #variables._entityName# entity failed to pass validation", detail = validationResult.getAllErrorsAsJson() ); } @@ -978,17 +990,17 @@ component accessors="true" { if ( isReadOnly() ) { throw( type = "QuickReadOnlyException", - message = "[#getEntityName()#] is marked as a read-only entity." + message = "[#variables._entityName#] is marked as a read-only entity." ); } } private function isReadOnly() { - return getReadOnly(); + return variables._readonly; } private function guardAgainstReadOnlyAttributes( attributes ) { - for ( var name in attributes ) { + for ( var name in arguments.attributes ) { guardAgainstReadOnlyAttribute( name ); } } @@ -997,7 +1009,7 @@ component accessors="true" { if ( ! hasAttribute( name ) ) { throw( type = "AttributeNotFound", - message = "The [#name#] attribute was not found on the [#getEntityName()#] entity" + message = "The [#name#] attribute was not found on the [#variables._entityName#] entity" ); } } @@ -1006,13 +1018,13 @@ component accessors="true" { if ( isReadOnlyAttribute( name ) ) { throw( type = "QuickReadOnlyException", - message = "[#name#] is a read-only property on [#getEntityName()#]" + message = "[#name#] is a read-only property on [#variables._entityName#]" ); } } private function isReadOnlyAttribute( name ) { - var md = getMeta(); + var md = variables._meta; if ( ! md.keyExists( "properties" ) || arrayIsEmpty( md.properties ) ) { return false; } @@ -1030,12 +1042,12 @@ component accessors="true" { ==============================*/ function fireEvent( eventName, eventData ) { - eventData.entityName = getEntityName(); + eventData.entityName = variables._entityName; if ( eventMethodExists( eventName ) ) { invoke( this, eventName, { eventData = eventData } ); } - if ( ! isNull( interceptorService ) ) { - interceptorService.processState( "quick" & eventName, eventData ); + if ( ! isNull( variables._interceptorService ) ) { + variables._interceptorService.processState( "quick" & eventName, eventData ); } } @@ -1044,20 +1056,20 @@ component accessors="true" { } private function attributeHasSqlType( name ) { - return ! getMeta().properties.filter( function( property ) { - return property.name == getAliasForColumn( name ) && property.keyExists( "sqltype" ); + return ! variables._meta.properties.filter( function( property ) { + return property.name == retrieveAliasForColumn( name ) && property.keyExists( "sqltype" ); } ).isEmpty(); } private function getSqlTypeForAttribute( name ) { - return getMeta().properties.filter( function( property ) { - return property.name == getAliasForColumn( name ); + return variables._meta.properties.filter( function( property ) { + return property.name == retrieveAliasForColumn( name ); } )[ 1 ].sqltype; } private function isNullValue( key, value ) { - return variables.nullValues.keyExists( getAliasForColumn( key ) ) && - compare( variables.nullValues[ getAliasForColumn( key ) ], value ) == 0; + return variables._nullValues.keyExists( retrieveAliasForColumn( key ) ) && + compare( variables._nullValues[ retrieveAliasForColumn( key ) ], value ) == 0; } } diff --git a/models/KeyTypes/AutoIncrementing.cfc b/models/KeyTypes/AutoIncrementing.cfc index fbe7e4bf..d2cba46d 100644 --- a/models/KeyTypes/AutoIncrementing.cfc +++ b/models/KeyTypes/AutoIncrementing.cfc @@ -14,7 +14,7 @@ component implements="KeyType" { */ public void function postInsert( required entity, required struct result ) { var generatedKey = result.keyExists( "generated_key" ) ? result[ "generated_key" ] : result[ "generatedKey" ]; - entity.setAttribute( entity.getKey(), generatedKey ); + entity.assignAttribute( entity.get_Key(), generatedKey ); } } diff --git a/models/KeyTypes/UUID.cfc b/models/KeyTypes/UUID.cfc index 697f9907..0f9cd7c0 100644 --- a/models/KeyTypes/UUID.cfc +++ b/models/KeyTypes/UUID.cfc @@ -5,7 +5,7 @@ component implements="KeyType" { * Recieves the entity as the only argument. */ public void function preInsert( required entity ) { - entity.setAttribute( entity.getKey(), createUUID() ); + entity.assignAttribute( entity.get_Key(), createUUID() ); } /** diff --git a/models/QuickCollection.cfc b/models/QuickCollection.cfc index 41df807c..1033a65d 100644 --- a/models/QuickCollection.cfc +++ b/models/QuickCollection.cfc @@ -47,10 +47,10 @@ component extends="cfcollection.models.Collection" { return this.each( function( entity ) { var relationship = invoke( entity, relationName ); if ( structKeyExists( groupedRelations, relationship.getForeignKeyValue() ) ) { - entity.setRelationship( relationName, groupedRelations[ relationship.getForeignKeyValue() ] ); + entity.assignRelationship( relationName, groupedRelations[ relationship.getForeignKeyValue() ] ); } else { - entity.setRelationship( relationName, relationship.getDefaultValue() ); + entity.assignRelationship( relationName, relationship.getDefaultValue() ); } } ); } diff --git a/models/Relationships/BelongsTo.cfc b/models/Relationships/BelongsTo.cfc index efab25dd..d23d980c 100644 --- a/models/Relationships/BelongsTo.cfc +++ b/models/Relationships/BelongsTo.cfc @@ -18,10 +18,10 @@ component extends="quick.models.Relationships.BaseRelationship" { function associate( entity ) { if ( isQuickEntity( entity ) ) { - arguments.entity = entity.getKeyValue(); + arguments.entity = entity.keyValue(); } - return getOwning().setAttribute( getForeignKey(), entity ); + return getOwning().assignAttribute( getForeignKey(), entity ); } function disassociate() { diff --git a/models/Relationships/BelongsToMany.cfc b/models/Relationships/BelongsToMany.cfc index 215915b8..bb230a1a 100644 --- a/models/Relationships/BelongsToMany.cfc +++ b/models/Relationships/BelongsToMany.cfc @@ -16,12 +16,12 @@ component accessors="true" extends="quick.models.Relationships.BaseRelationship" } function apply() { - getRelated().join( getTable(), function( j ) { + getRelated().join( variables.table, function( j ) { j.on( - "#getRelated().getTable()#.#getRelated().getKey()#", - "#getTable()#.#getOwningKey()#" + "#getRelated().get_Table()#.#getRelated().get_Key()#", + "#variables.table#.#getOwningKey()#" ); - j.where( "#getTable()#.#getForeignKey()#", getForeignKeyValue() ); + j.where( "#variables.table#.#getForeignKey()#", getForeignKeyValue() ); } ); } @@ -42,10 +42,10 @@ component accessors="true" extends="quick.models.Relationships.BaseRelationship" if ( isSimpleValue( id ) ) { return id; } - return id.getKeyValue(); + return id.keyValue(); } ); - builder.get().from( getTable() ).insert( arrayMap( arguments.ids, function( id ) { + builder.get().from( variables.table ).insert( arrayMap( arguments.ids, function( id ) { return { "#getForeignKey()#" = getForeignKeyValue(), "#getOwningKey()#" = id @@ -66,11 +66,11 @@ component accessors="true" extends="quick.models.Relationships.BaseRelationship" if ( isSimpleValue( id ) ) { return id; } - return id.getKeyValue(); + return id.keyValue(); } ); builder.get() - .from( getTable() ) + .from( variables.table ) .where( getForeignKey(), getForeignKeyValue() ) .whereIn( getOwningKey(), arguments.ids ) .delete(); @@ -89,11 +89,11 @@ component accessors="true" extends="quick.models.Relationships.BaseRelationship" if ( isSimpleValue( id ) ) { return id; } - return id.getKeyValue(); + return id.keyValue(); } ); builder.get() - .from( getTable() ) + .from( variables.table ) .where( getForeignKey(), getForeignKeyValue() ) .delete(); diff --git a/models/Relationships/HasMany.cfc b/models/Relationships/HasMany.cfc index 78c57562..4b83fbcc 100644 --- a/models/Relationships/HasMany.cfc +++ b/models/Relationships/HasMany.cfc @@ -18,14 +18,14 @@ component extends="quick.models.Relationships.BaseRelationship" { function save( entity ) { getOwning().clearRelationship( getRelationMethodName() ); - return entity.setAttribute( getOwningKey(), getForeignKeyValue() ).save(); + return entity.assignAttribute( getOwningKey(), getForeignKeyValue() ).save(); } function create( attributes ) { getOwning().clearRelationship( getRelationMethodName() ); return wirebox.getInstance( getRelationName() ) - .setAttributesData( attributes ) - .setAttribute( getOwningKey(), getForeignKeyValue() ) + .assignAttributesData( attributes ) + .assignAttribute( getOwningKey(), getForeignKeyValue() ) .save(); } diff --git a/models/Relationships/HasManyThrough.cfc b/models/Relationships/HasManyThrough.cfc index 23fd945e..e7253549 100644 --- a/models/Relationships/HasManyThrough.cfc +++ b/models/Relationships/HasManyThrough.cfc @@ -17,16 +17,16 @@ component accessors="true" extends="quick.models.Relationships.BaseRelationship" function apply() { getRelated() .join( - getIntermediate().getTable(), - "#getIntermediate().getTable()#.#getIntermediate().getKey()#", - "#getRelated().getTable()#.#getIntermediateKey()#" + getIntermediate().get_Table(), + "#getIntermediate().get_Table()#.#getIntermediate().get_Key()#", + "#getRelated().get_Table()#.#getIntermediateKey()#" ) .join( - getOwning().getTable(), - "#getOwning().getTable()#.#getOwningKey()#", - "#getIntermediate().getTable()#.#getForeignKey()#" + getOwning().get_Table(), + "#getOwning().get_Table()#.#getOwningKey()#", + "#getIntermediate().get_Table()#.#getForeignKey()#" ) - .where( "#getOwning().getTable()#.#getOwningKey()#", getOwning().getKeyValue() ); + .where( "#getOwning().get_Table()#.#getOwningKey()#", getOwning().keyValue() ); } function fromGroup( items ) { diff --git a/models/Relationships/PolymorphicHasMany.cfc b/models/Relationships/PolymorphicHasMany.cfc index ba56a2d8..c97b459d 100644 --- a/models/Relationships/PolymorphicHasMany.cfc +++ b/models/Relationships/PolymorphicHasMany.cfc @@ -14,8 +14,8 @@ component accessors="true" extends="quick.models.Relationships.BaseRelationship" function apply() { getRelated() - .where( "#getPrefix()#_type", getOwning().getMapping() ) - .where( "#getPrefix()#_id", getOwning().getKeyValue() ); + .where( "#getPrefix()#_type", getOwning().get_Mapping() ) + .where( "#getPrefix()#_id", getOwning().keyValue() ); } function fromGroup( items ) { diff --git a/tests/resources/app/models/Country.cfc b/tests/resources/app/models/Country.cfc index f7d4eaab..3ddd216d 100644 --- a/tests/resources/app/models/Country.cfc +++ b/tests/resources/app/models/Country.cfc @@ -1,6 +1,6 @@ component extends="quick.models.BaseEntity" { - property name="keyType" inject="UUID@quick" persistent="false"; + property name="_keyType" inject="UUID@quick" persistent="false"; property name="id"; property name="name"; diff --git a/tests/resources/app/models/Link.cfc b/tests/resources/app/models/Link.cfc index ab192870..84ad224d 100644 --- a/tests/resources/app/models/Link.cfc +++ b/tests/resources/app/models/Link.cfc @@ -6,6 +6,6 @@ component extends="quick.models.BaseEntity" { property name="url" column="link_url"; property name="createdDate" column="created_date" readonly="true"; - variables.key = "link_id"; + variables._key = "link_id"; } diff --git a/tests/resources/app/models/Post.cfc b/tests/resources/app/models/Post.cfc index bcfba07b..d31fa8bc 100644 --- a/tests/resources/app/models/Post.cfc +++ b/tests/resources/app/models/Post.cfc @@ -6,7 +6,7 @@ component entityname="MyPost" table="my_posts" extends="quick.models.BaseEntity" property name="createdDate" column="created_date"; property name="modifiedDate" column="modified_date"; - variables.key = "post_pk"; + variables._key = "post_pk"; function author() { return belongsTo( "User", "user_id" ); diff --git a/tests/specs/integration/BaseEntity/AttributeCasingSpec.cfc b/tests/specs/integration/BaseEntity/AttributeCasingSpec.cfc index ec48f837..41eba1fe 100644 --- a/tests/specs/integration/BaseEntity/AttributeCasingSpec.cfc +++ b/tests/specs/integration/BaseEntity/AttributeCasingSpec.cfc @@ -5,21 +5,21 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { it( "defaults to no transformation", function() { var post = getInstance( "Post" ).find( 1 ); - expect( post.getAttributesData() ).toHaveKey( "post_pk" ); - expect( post.getAttributesData() ).notToHaveKey( "PostPk" ); + expect( post.retrieveAttributesData() ).toHaveKey( "post_pk" ); + expect( post.retrieveAttributesData() ).notToHaveKey( "PostPk" ); expect( post.getPost_Pk() ).notToBeNull(); post.setCreatedDate( now() ); - expect( post.getAttributesData() ).toHaveKey( "created_date" ); + expect( post.retrieveAttributesData() ).toHaveKey( "created_date" ); } ); it( "converts stores all attributes internally as snake case when the `attributecasing` metadata property is set to `snake`", function() { var user = getInstance( "User" ).find( 1 ); - expect( user.getAttributesData() ).toHaveKey( "first_name" ); - expect( user.getAttributesData() ).notToHaveKey( "firstName" ); + expect( user.retrieveAttributesData() ).toHaveKey( "first_name" ); + expect( user.retrieveAttributesData() ).notToHaveKey( "firstName" ); expect( function() { user.getFirstName(); @@ -31,8 +31,8 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { user.setCreatedDate( now() ); - expect( user.getAttributesData() ).notToHaveKey( "createdDate" ); - expect( user.getAttributesData() ).toHaveKey( "created_date" ); + expect( user.retrieveAttributesData() ).notToHaveKey( "createdDate" ); + expect( user.retrieveAttributesData() ).toHaveKey( "created_date" ); } ); } ); } diff --git a/tests/specs/integration/BaseEntity/AttributeSpec.cfc b/tests/specs/integration/BaseEntity/AttributeSpec.cfc index 7167343a..5e90afae 100644 --- a/tests/specs/integration/BaseEntity/AttributeSpec.cfc +++ b/tests/specs/integration/BaseEntity/AttributeSpec.cfc @@ -23,22 +23,22 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { it( "can retrieve the original attributes of a loaded entity", function() { var user = getInstance( "User" ).find( 1 ); - var originalAttributes = user.getAttributesData(); + var originalAttributes = user.retrieveAttributesData(); user.setUsername( "new_username" ); - expect( originalAttributes ).notToBe( user.getAttributesData() ); - expect( originalAttributes ).toBe( user.getOriginalAttributes() ); + expect( originalAttributes ).notToBe( user.retrieveAttributesData() ); + expect( originalAttributes ).toBe( user.get_OriginalAttributes() ); } ); it( "returns a default value if the attribute is not yet set", function() { var user = getInstance( "User" ); - expect( user.getAttribute( "username" ) ).toBe( "" ); - expect( user.getAttribute( "username", "default-value" ) ).toBe( "default-value" ); + expect( user.retrieveAttribute( "username" ) ).toBe( "" ); + expect( user.retrieveAttribute( "username", "default-value" ) ).toBe( "default-value" ); } ); it( "throws an exception when trying to set an attribute that does not exist", function() { var user = getInstance( "User" ); expect( function() { - user.setAttribute( "does-not-exist", "any-value" ); + user.assignAttribute( "does-not-exist", "any-value" ); } ).toThrow( type = "AttributeNotFound" ); } ); diff --git a/tests/specs/integration/BaseEntity/ColumnsSpec.cfc b/tests/specs/integration/BaseEntity/ColumnsSpec.cfc index aa457da6..6230c854 100644 --- a/tests/specs/integration/BaseEntity/ColumnsSpec.cfc +++ b/tests/specs/integration/BaseEntity/ColumnsSpec.cfc @@ -4,7 +4,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { describe( "Columns", function() { it( "retrieves all columns by default", function() { var user = getInstance( "User" ).findOrFail( 1 ); - var attributeNames = user.getAttributeNames(); + var attributeNames = user.retrieveAttributeNames( columnNames = true ); arraySort( attributeNames, "textnocase" ); expect( attributeNames ).toBeArray(); @@ -24,11 +24,11 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { it( "always retrieves the primary key", function() { var link = getInstance( "Link" ).findOrFail( 1 ); - var configuredAttributes = link.getAttributes(); + var configuredAttributes = link.get_Attributes(); expect( configuredAttributes ).toBeStruct(); expect( configuredAttributes ).notToHaveKey( "link_id" ); - var attributeNames = link.getAttributeNames(); + var attributeNames = link.retrieveAttributeNames( columnNames = true ); arraySort( attributeNames, "textnocase" ); expect( attributeNames ).toBeArray(); expect( attributeNames ).toHaveLength( 3 ); @@ -38,20 +38,20 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { it( "can access the attributes by their alias", function() { var link = getInstance( "Link" ).findOrFail( 1 ); - var configuredAttributes = link.getAttributes(); + var configuredAttributes = link.get_Attributes(); expect( configuredAttributes ).toBeStruct(); expect( configuredAttributes ).notToHaveKey( "link_id" ); - var attributeNames = link.getAttributeNames(); + var attributeNames = link.retrieveAttributeNames( columnNames = true ); arraySort( attributeNames, "textnocase" ); expect( attributeNames ).toBeArray(); expect( attributeNames ).toHaveLength( 3 ); expect( attributeNames ).toBe( [ "created_date", "link_id", "link_url" ] ); expect( link.getId() ).toBe( 1 ); - expect( link.getId() ).toBe( link.getAttributesData()[ "link_id" ] ); + expect( link.getId() ).toBe( link.retrieveAttributesData()[ "link_id" ] ); expect( link.getUrl() ).toBe( "http://example.com/some-link" ); - expect( link.getUrl() ).toBe( link.getAttributesData()[ "link_url" ] ); + expect( link.getUrl() ).toBe( link.retrieveAttributesData()[ "link_url" ] ); } ); it( "ignores non-persistent attributes", function() { diff --git a/tests/specs/integration/BaseEntity/CreateSpec.cfc b/tests/specs/integration/BaseEntity/CreateSpec.cfc index 0636681d..2c7d7949 100644 --- a/tests/specs/integration/BaseEntity/CreateSpec.cfc +++ b/tests/specs/integration/BaseEntity/CreateSpec.cfc @@ -9,7 +9,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { "last_name" = "Doe", "password" = hash( "password" ) } ); - expect( user.getLoaded() ).toBeTrue(); + expect( user.isLoaded() ).toBeTrue(); expect( user.newEntity().where( "username", "JaneDoe" ).first() ).notToBeNull(); } ); } ); diff --git a/tests/specs/integration/BaseEntity/Events/PostInsertSpec.cfc b/tests/specs/integration/BaseEntity/Events/PostInsertSpec.cfc index bf991564..75351d04 100644 --- a/tests/specs/integration/BaseEntity/Events/PostInsertSpec.cfc +++ b/tests/specs/integration/BaseEntity/Events/PostInsertSpec.cfc @@ -21,7 +21,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { expect( variables.quickPostInsertCalled ).toBeStruct(); expect( variables.quickPostInsertCalled ).toHaveKey( "entity" ); expect( variables.quickPostInsertCalled.entity.getTitle() ).toBe( "Rainbow Connection" ); - expect( variables.quickPostInsertCalled.entity.getLoaded() ).toBeTrue(); + expect( variables.quickPostInsertCalled.entity.isLoaded() ).toBeTrue(); structDelete( variables, "quickPostInsertCalled" ); } ); @@ -34,7 +34,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { expect( request.postInsertCalled ).toBeStruct(); expect( request.postInsertCalled ).toHaveKey( "entity" ); expect( request.postInsertCalled.entity.getTitle() ).toBe( "Rainbow Connection" ); - expect( request.postInsertCalled.entity.getLoaded() ).toBeTrue(); + expect( request.postInsertCalled.entity.isLoaded() ).toBeTrue(); structDelete( request, "postInsertCalled" ); } ); } ); diff --git a/tests/specs/integration/BaseEntity/Events/PostLoadSpec.cfc b/tests/specs/integration/BaseEntity/Events/PostLoadSpec.cfc index 53cb7ab3..81141c63 100644 --- a/tests/specs/integration/BaseEntity/Events/PostLoadSpec.cfc +++ b/tests/specs/integration/BaseEntity/Events/PostLoadSpec.cfc @@ -17,7 +17,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { expect( variables ).toHaveKey( "quickPostLoadCalled" ); expect( variables.quickPostLoadCalled ).toBeStruct(); expect( variables.quickPostLoadCalled ).toHaveKey( "entity" ); - expect( variables.quickPostLoadCalled.entity.getKeyValue() ).toBe( 1 ); + expect( variables.quickPostLoadCalled.entity.keyValue() ).toBe( 1 ); structDelete( variables, "quickPostLoadCalled" ); } ); @@ -26,7 +26,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { expect( request ).toHaveKey( "postLoadCalled" ); expect( request.postLoadCalled ).toBeStruct(); expect( request.postLoadCalled ).toHaveKey( "entity" ); - expect( request.postLoadCalled.entity.getKeyValue() ).toBe( 1 ); + expect( request.postLoadCalled.entity.keyValue() ).toBe( 1 ); structDelete( request, "postLoadCalled" ); } ); } ); diff --git a/tests/specs/integration/BaseEntity/Events/PostSaveSpec.cfc b/tests/specs/integration/BaseEntity/Events/PostSaveSpec.cfc index f76da50c..9e69a3fe 100644 --- a/tests/specs/integration/BaseEntity/Events/PostSaveSpec.cfc +++ b/tests/specs/integration/BaseEntity/Events/PostSaveSpec.cfc @@ -18,7 +18,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { expect( variables.quickPostSaveCalled ).toBeStruct(); expect( variables.quickPostSaveCalled ).toHaveKey( "entity" ); expect( variables.quickPostSaveCalled.entity.getDownloadUrl() ).toBe( "https://open.spotify.com/track/1SJ4ycWow4yz6z4oFz8NAG" ); - expect( variables.quickPostSaveCalled.entity.getLoaded() ).toBeTrue(); + expect( variables.quickPostSaveCalled.entity.isLoaded() ).toBeTrue(); structDelete( variables, "quickPostSaveCalled" ); } ); @@ -45,7 +45,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { expect( request.postSaveCalled ).toBeStruct(); expect( request.postSaveCalled ).toHaveKey( "entity" ); expect( request.postSaveCalled.entity.getDownloadUrl() ).toBe( "https://open.spotify.com/track/1SJ4ycWow4yz6z4oFz8NAG" ); - expect( request.postSaveCalled.entity.getLoaded() ).toBeTrue(); + expect( request.postSaveCalled.entity.isLoaded() ).toBeTrue(); structDelete( request, "postSaveCalled" ); } ); diff --git a/tests/specs/integration/BaseEntity/Events/PreInsertSpec.cfc b/tests/specs/integration/BaseEntity/Events/PreInsertSpec.cfc index 63d17b73..84a20bea 100644 --- a/tests/specs/integration/BaseEntity/Events/PreInsertSpec.cfc +++ b/tests/specs/integration/BaseEntity/Events/PreInsertSpec.cfc @@ -21,7 +21,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { expect( variables.quickPreInsertCalled ).toBeStruct(); expect( variables.quickPreInsertCalled ).toHaveKey( "entity" ); expect( variables.quickPreInsertCalled.entity.getTitle() ).toBe( "Rainbow Connection" ); - expect( variables.quickPreInsertCalled.entity.getLoaded() ).toBeFalse(); + expect( variables.quickPreInsertCalled.entity.isLoaded() ).toBeFalse(); structDelete( variables, "quickPreInsertCalled" ); } ); @@ -34,7 +34,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { expect( request.preInsertCalled ).toBeStruct(); expect( request.preInsertCalled ).toHaveKey( "entity" ); expect( request.preInsertCalled.entity.getTitle() ).toBe( "Rainbow Connection" ); - expect( request.preInsertCalled.entity.getLoaded() ).toBeFalse(); + expect( request.preInsertCalled.entity.isLoaded() ).toBeFalse(); structDelete( request, "preInsertCalled" ); } ); } ); diff --git a/tests/specs/integration/BaseEntity/Events/PreSaveSpec.cfc b/tests/specs/integration/BaseEntity/Events/PreSaveSpec.cfc index 953423bb..35ba194f 100644 --- a/tests/specs/integration/BaseEntity/Events/PreSaveSpec.cfc +++ b/tests/specs/integration/BaseEntity/Events/PreSaveSpec.cfc @@ -18,7 +18,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { expect( variables.quickPreSaveCalled ).toBeStruct(); expect( variables.quickPreSaveCalled ).toHaveKey( "entity" ); expect( variables.quickPreSaveCalled.entity.getDownloadUrl() ).toBe( "https://open.spotify.com/track/1SJ4ycWow4yz6z4oFz8NAG" ); - expect( variables.quickPreSaveCalled.entity.getLoaded() ).toBeFalse(); + expect( variables.quickPreSaveCalled.entity.isLoaded() ).toBeFalse(); structDelete( variables, "quickPreSaveCalled" ); } ); @@ -45,7 +45,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { expect( request.preSaveCalled ).toBeStruct(); expect( request.preSaveCalled ).toHaveKey( "entity" ); expect( request.preSaveCalled.entity.getDownloadUrl() ).toBe( "https://open.spotify.com/track/1SJ4ycWow4yz6z4oFz8NAG" ); - expect( request.preSaveCalled.entity.getLoaded() ).toBeFalse(); + expect( request.preSaveCalled.entity.isLoaded() ).toBeFalse(); structDelete( request, "preSaveCalled" ); } ); diff --git a/tests/specs/integration/BaseEntity/FillSpec.cfc b/tests/specs/integration/BaseEntity/FillSpec.cfc index 6656277d..cfae17de 100644 --- a/tests/specs/integration/BaseEntity/FillSpec.cfc +++ b/tests/specs/integration/BaseEntity/FillSpec.cfc @@ -4,17 +4,17 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { describe( "Fill Spec", function() { it( "can fill many properties at once", function() { var user = getInstance( "User" ); - expect( user.getAttribute( "username" ) ).toBe( "" ); - expect( user.getAttribute( "first_name" ) ).toBe( "" ); - expect( user.getAttribute( "last_name" ) ).toBe( "" ); + expect( user.retrieveAttribute( "username" ) ).toBe( "" ); + expect( user.retrieveAttribute( "first_name" ) ).toBe( "" ); + expect( user.retrieveAttribute( "last_name" ) ).toBe( "" ); user.fill( { "username" = "JaneDoe", "first_name" = "Jane", "last_name" = "Doe" } ); - expect( user.getAttribute( "username" ) ).toBe( "JaneDoe" ); - expect( user.getAttribute( "first_name" ) ).toBe( "Jane" ); - expect( user.getAttribute( "last_name" ) ).toBe( "Doe" ); + expect( user.retrieveAttribute( "username" ) ).toBe( "JaneDoe" ); + expect( user.retrieveAttribute( "first_name" ) ).toBe( "Jane" ); + expect( user.retrieveAttribute( "last_name" ) ).toBe( "Doe" ); } ); it( "throws an error when trying to fill non-existant properties", function() { diff --git a/tests/specs/integration/BaseEntity/GetSpec.cfc b/tests/specs/integration/BaseEntity/GetSpec.cfc index 8860dcf0..34f7df89 100644 --- a/tests/specs/integration/BaseEntity/GetSpec.cfc +++ b/tests/specs/integration/BaseEntity/GetSpec.cfc @@ -4,7 +4,8 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { describe( "Get Spec", function() { it( "finds an entity by the primary key", function() { var user = getInstance( "User" ).find( 1 ); - expect( user.getLoaded() ).toBeTrue( "The user instance should be found and loaded, but was not." ); + expect( user.isLoaded() ).toBeTrue( "The user instance should be found and loaded, but was not." ); + debug( user.get_Key() ); } ); it( "it returns null if the record cannot be found", function() { @@ -33,11 +34,11 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { describe( "loaded", function() { it( "a new entity returns false when asked if it loaded", function() { - expect( getInstance( "User" ).getLoaded() ).toBeFalse(); + expect( getInstance( "User" ).isLoaded() ).toBeFalse(); } ); it( "an entity loaded from the database returns true when asked if it loaded", function() { - expect( getInstance( "User" ).find( 1 ).getLoaded() ).toBeTrue(); + expect( getInstance( "User" ).find( 1 ).isLoaded() ).toBeTrue(); } ); } ); diff --git a/tests/specs/integration/BaseEntity/MetadataSpec.cfc b/tests/specs/integration/BaseEntity/MetadataSpec.cfc index ae8dd18b..55142702 100644 --- a/tests/specs/integration/BaseEntity/MetadataSpec.cfc +++ b/tests/specs/integration/BaseEntity/MetadataSpec.cfc @@ -13,55 +13,55 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { describe( "full name", function() { it( "calculates the full name from the metadata", function() { var post = getInstance( "User" ); - expect( post.getFullName() ).toBe( "app.models.User" ); + expect( post.get_FullName() ).toBe( "app.models.User" ); } ); } ); describe( "entity name", function() { it( "determines the entity name from the metadata if an `entityname` attribute is present", function() { var post = getInstance( "Post" ); - expect( post.getEntityName() ).toBe( "MyPost" ); + expect( post.get_EntityName() ).toBe( "MyPost" ); } ); it( "calculates the entity name from the file name if no `entityname` attribute is present", function() { var post = getInstance( "User" ); - expect( post.getEntityName() ).toBe( "User" ); + expect( post.get_EntityName() ).toBe( "User" ); } ); } ); describe( "mapping name", function() { it( "takes the mapping name from the file name", function() { var post = getInstance( "Post" ); - expect( post.getMapping() ).toBe( "Post" ); + expect( post.get_Mapping() ).toBe( "Post" ); } ); } ); describe( "table name", function() { it( "determines the table name from the metadata if a `table` attribute is present", function() { var post = getInstance( "Post" ); - expect( post.getTable() ).toBe( "my_posts" ); + expect( post.get_Table() ).toBe( "my_posts" ); } ); it( "calculates the table name from the entity name if no `table` attribute is present", function() { var post = getInstance( "User" ); - expect( post.getTable() ).toBe( "users" ); + expect( post.get_Table() ).toBe( "users" ); } ); it( "uses the snake case plural version of the component name", function() { var phoneNumber = getInstance( "PhoneNumber" ); - expect( phoneNumber.getTable() ).toBe( "phone_numbers" ); + expect( phoneNumber.get_Table() ).toBe( "phone_numbers" ); } ); } ); describe( "primary key", function() { - it( "uses the `variables.key` value if set", function() { + it( "uses the `variables._key` value if set", function() { var post = getInstance( "Post" ); - expect( post.getKey() ).toBe( "post_pk" ); + expect( post.get_Key() ).toBe( "post_pk" ); } ); - it( "uses the `id` as the `variables.key` value by default", function() { + it( "uses the `id` as the `variables._key` value by default", function() { var user = getInstance( "User" ); - expect( user.getKey() ).toBe( "id" ); + expect( user.get_Key() ).toBe( "id" ); } ); } ); } ); diff --git a/tests/specs/integration/BaseEntity/ReadOnlyPropertySpec.cfc b/tests/specs/integration/BaseEntity/ReadOnlyPropertySpec.cfc index abc7bf47..497c0ef8 100644 --- a/tests/specs/integration/BaseEntity/ReadOnlyPropertySpec.cfc +++ b/tests/specs/integration/BaseEntity/ReadOnlyPropertySpec.cfc @@ -20,17 +20,17 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { } ).toThrow( type = "QuickReadOnlyException" ); } ); - it( "prevents setAttribute from being called on a read-only property", function() { + it( "prevents assignAttribute from being called on a read-only property", function() { var link = getInstance( "Link" ).findOrFail( 1 ); expect( function() { - link.setAttribute( "createdDate", now() ); + link.assignAttribute( "createdDate", now() ); } ).toThrow( type = "QuickReadOnlyException" ); } ); - it( "prevents setAttributesData from being called containing a read-only property", function() { + it( "prevents assignAttributesData from being called containing a read-only property", function() { var link = getInstance( "Link" ).findOrFail( 1 ); expect( function() { - link.setAttributesData( { createdDate = now() } ); + link.assignAttributesData( { createdDate = now() } ); } ).toThrow( type = "QuickReadOnlyException" ); } ); diff --git a/tests/specs/integration/BaseEntity/Relationships/BelongsToSpec.cfc b/tests/specs/integration/BaseEntity/Relationships/BelongsToSpec.cfc index c5290470..1ec420cc 100644 --- a/tests/specs/integration/BaseEntity/Relationships/BelongsToSpec.cfc +++ b/tests/specs/integration/BaseEntity/Relationships/BelongsToSpec.cfc @@ -28,17 +28,17 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { newPost.setBody( "A new post by me!" ); var user = getInstance( "User" ).find( 1 ); newPost.author().associate( user ).save(); - expect( newPost.getAttribute( "user_id" ) ).toBe( user.getId() ); + expect( newPost.retrieveAttribute( "user_id" ) ).toBe( user.getId() ); expect( user.posts().count() ).toBe( 3 ); } ); it( "can disassociate the existing entity", function() { var post = getInstance( "Post" ).find( 1 ); - expect( post.getAttribute( "user_id" ) ).notToBe( "" ); - var userId = post.getAttribute( "user_id" ); + expect( post.retrieveAttribute( "user_id" ) ).notToBe( "" ); + var userId = post.retrieveAttribute( "user_id" ); expect( getInstance( "User" ).find( userId ).posts().count() ).toBe( 2 ); post.author().disassociate().save(); - expect( post.getAttribute( "user_id" ) ).toBe( "" ); + expect( post.retrieveAttribute( "user_id" ) ).toBe( "" ); expect( getInstance( "User" ).find( userId ).posts().count() ).toBe( 1 ); } ); } ); diff --git a/tests/specs/integration/BaseEntity/Relationships/HasManySpec.cfc b/tests/specs/integration/BaseEntity/Relationships/HasManySpec.cfc index 33587589..612f690f 100644 --- a/tests/specs/integration/BaseEntity/Relationships/HasManySpec.cfc +++ b/tests/specs/integration/BaseEntity/Relationships/HasManySpec.cfc @@ -12,11 +12,11 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { it( "can save and associate new entities", function() { var newPost = getInstance( "Post" ); newPost.setBody( "A new post by me!" ); - expect( newPost.getLoaded() ).toBeFalse(); + expect( newPost.isLoaded() ).toBeFalse(); var user = getInstance( "User" ).find( 1 ); newPost = user.posts().save( newPost ); - expect( newPost.getLoaded() ).toBeTrue(); - expect( newPost.getAttribute( "user_id" ) ).toBe( user.getId() ); + expect( newPost.isLoaded() ).toBeTrue(); + expect( newPost.retrieveAttribute( "user_id" ) ).toBe( user.getId() ); } ); it( "can create new related entities directly", function() { @@ -25,8 +25,8 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { var newPost = user.posts().create( { "body" = "A new post created directly here!" } ); - expect( newPost.getLoaded() ).toBeTrue(); - expect( newPost.getAttribute( "user_id" ) ).toBe( user.getId() ); + expect( newPost.isLoaded() ).toBeTrue(); + expect( newPost.retrieveAttribute( "user_id" ) ).toBe( user.getId() ); expect( newPost.getBody() ).toBe( "A new post created directly here!" ); expect( user.getPosts() ).toHaveLength( 3 ); } ); diff --git a/tests/specs/integration/BaseEntity/SaveSpec.cfc b/tests/specs/integration/BaseEntity/SaveSpec.cfc index 5d386cc1..66e1dda0 100644 --- a/tests/specs/integration/BaseEntity/SaveSpec.cfc +++ b/tests/specs/integration/BaseEntity/SaveSpec.cfc @@ -32,7 +32,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { newUser.setLastName( "User" ); newUser.setPassword( hash( "password" ) ); newUser.save(); - expect( newUser.getAttributesData() ).toHaveKey( "id" ); + expect( newUser.retrieveAttributesData() ).toHaveKey( "id" ); } ); it( "a saved entity is not dirty", function() { @@ -191,16 +191,16 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { var tagsToSync = [ existingTags[ 1 ], newTagA.getId(), newTagB ]; var tagIds = [ - existingTags[ 1 ].getKeyValue(), - newTagA.getKeyValue(), - newTagB.getKeyValue() + existingTags[ 1 ].keyValue(), + newTagA.keyValue(), + newTagB.keyValue() ]; post.tags().sync( [ existingTags[ 1 ], newTagA.getId(), newTagB ] ); expect( post.getTags().toArray() ).toBeArray(); expect( post.getTags().toArray() ).toHaveLength( 3 ); - // expect( post.getTags().pluck( "keyValue" ).toArray() ).toBe( tagIds ); + expect( post.getTags().map( function( tag ) { return tag.keyValue(); } ).toArray() ).toBe( tagIds ); } ); } ); } ); From 79f64355915a1e061f23fd9e3b7dd582826fb012 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Mon, 17 Sep 2018 15:13:51 -0600 Subject: [PATCH 08/70] fix(Relationships): Fix incorrect and slow relationships The original implementation of relationships had some naive implementations that were both slow and potentially incorrect. Some of this was hidden by simplistic test cases which have been updated. This commit fixes those issues and brings the API more in line with Eloquent from Laravel. (This library is _**heavily**_ inspired by Eloquent.) BREAKING CHANGE: The function signature for all the relationship methods have changed. It is possible to not need any changes if you relied on the conventions. If you did not you may need to change some argument names or argument order to get the relationships working again. --- ModuleConfig.cfc | 2 +- box.json | 16 +- models/BaseEntity.cfc | 244 ++++++++++-------- models/QuickCollection.cfc | 29 +-- models/Relationships/BaseRelationship.cfc | 55 ++-- models/Relationships/BelongsTo.cfc | 72 +++++- models/Relationships/BelongsToMany.cfc | 187 ++++++++------ models/Relationships/HasMany.cfc | 34 +-- models/Relationships/HasManyThrough.cfc | 122 +++++++-- models/Relationships/HasOne.cfc | 21 +- models/Relationships/HasOneOrMany.cfc | 93 +++++++ models/Relationships/PolymorphicBelongsTo.cfc | 74 +++++- models/Relationships/PolymorphicHasMany.cfc | 31 +-- .../Relationships/PolymorphicHasOneOrMany.cfc | 19 ++ tests/Application.cfc | 16 +- tests/resources/app/models/A.cfc | 10 + tests/resources/app/models/B.cfc | 11 + tests/resources/app/models/Comment.cfc | 2 +- tests/resources/app/models/Post.cfc | 8 +- tests/resources/app/models/Tag.cfc | 6 +- tests/resources/app/models/User.cfc | 4 +- tests/resources/app/models/Video.cfc | 2 +- .../BaseEntity/AttributeCasingSpec.cfc | 2 +- .../integration/BaseEntity/AttributeSpec.cfc | 4 +- .../specs/integration/BaseEntity/GetSpec.cfc | 1 - .../integration/BaseEntity/MetadataSpec.cfc | 5 - .../Relationships/BelongsToManySpec.cfc | 2 +- .../Relationships/BelongsToSpec.cfc | 8 +- .../Relationships/EagerLoadingSpec.cfc | 124 ++++++++- .../BaseEntity/Relationships/HasManySpec.cfc | 2 +- .../BaseEntity/Relationships/HasOneSpec.cfc | 2 +- .../PolymorphicBelongsToSpec.cfc | 2 +- .../Relationships/PolymorphicHasManySpec.cfc | 4 +- .../specs/integration/BaseEntity/SaveSpec.cfc | 32 ++- .../integration/BaseEntity/UpdateAllSpec.cfc | 4 +- 35 files changed, 856 insertions(+), 394 deletions(-) create mode 100644 models/Relationships/HasOneOrMany.cfc create mode 100644 models/Relationships/PolymorphicHasOneOrMany.cfc create mode 100644 tests/resources/app/models/A.cfc create mode 100644 tests/resources/app/models/B.cfc diff --git a/ModuleConfig.cfc b/ModuleConfig.cfc index 4c1157fd..6d29f9b3 100644 --- a/ModuleConfig.cfc +++ b/ModuleConfig.cfc @@ -3,7 +3,7 @@ component { this.name = "quick"; this.author = "Eric Peterson"; this.webUrl = "https://github.com/coldbox-modules/quick"; - this.dependencies = [ "qb", "str", "cfcollection" ]; + this.dependencies = [ "qb", "str" ]; this.cfmapping = "quick"; function configure() { diff --git a/box.json b/box.json index 1e41cf58..27ca3b19 100644 --- a/box.json +++ b/box.json @@ -20,20 +20,20 @@ "dependencies":{ "qb":"^5.2.1", "str":"^1.0.0", - "cfcollection":"^3.6.1", "cbvalidation":"^1.3.1+51" }, "devDependencies":{ "coldbox":"be", - "testbox":"^2.4.0+80" + "testbox":"^2.4.0+80", + "cfcollection":"^3.6.1" }, "installPaths":{ - "testbox":"testbox", - "coldbox":"tests/resources/app/coldbox", - "qb":"modules/qb", - "str":"modules/str", - "cfcollection":"modules/cfcollection", - "cbvalidation":"modules/cbvalidation" + "testbox":"testbox/", + "coldbox":"tests/resources/app/coldbox/", + "qb":"modules/qb/", + "str":"modules/str/", + "cbvalidation":"modules/cbvalidation/", + "cfcollection":"modules/cfcollection" }, "testbox":{ "reporter":"json", diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 9ad3effd..2a24751c 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -186,10 +186,28 @@ component accessors="true" { return this; } + function qualifyColumn( column ) { + if ( findNoCase( ".", arguments.column ) != 0 ) { + return arguments.column; + } + return variables._table & "." & arguments.column; + } + /*===================================== = Query Methods = =====================================*/ + function getEntities() { + return retrieveQuery() + .get( options = variables._queryOptions ) + .map( function( attributes ) { + return newEntity() + .assignAttributesData( attributes ) + .assignOriginalAttributes( attributes ) + .set_Loaded( true ); + } ); + } + function all() { return eagerLoadRelations( newQuery().from( variables._table ) @@ -204,16 +222,7 @@ component accessors="true" { } function get() { - return eagerLoadRelations( - retrieveQuery() - .get( options = variables._queryOptions ) - .map( function( attributes ) { - return newEntity() - .assignAttributesData( attributes ) - .assignOriginalAttributes( attributes ) - .set_Loaded( true ); - } ) - ); + return eagerLoadRelations( getEntities() ); } function first() { @@ -282,7 +291,7 @@ component accessors="true" { } function refresh() { - variables._relationshipData = {}; + variables._relationshipsData = {}; assignAttributesData( newQuery() .from( variables._table ) @@ -408,68 +417,73 @@ component accessors="true" { return this; } - private function belongsTo( relationName, foreignKey ) { + private function belongsTo( relationName, foreignKey, ownerKey, relationMethodName ) { var related = variables._wirebox.getInstance( relationName ); if ( isNull( arguments.foreignKey ) ) { arguments.foreignKey = related.get_EntityName() & related.get_Key(); } - if ( isNull( arguments.owningKey ) ) { - arguments.owningKey = related.get_Key(); + if ( isNull( arguments.ownerKey ) ) { + arguments.ownerKey = related.get_Key(); + } + if ( isNull( arguments.relationMethodName ) ) { + arguments.relationMethodName = lcase( callStackGet()[ 2 ][ "Function" ] ); } return variables._wirebox.getInstance( name = "BelongsTo@quick", initArguments = { - wirebox = variables._wirebox, related = related, relationName = relationName, - relationMethodName = lcase( callStackGet()[ 2 ][ "Function" ] ), - owning = this, + relationMethodName = relationMethodName, + parent = this, foreignKey = foreignKey, - foreignKeyValue = retrieveAttribute( arguments.foreignKey ), - owningKey = owningKey + ownerKey = ownerKey } ); } - private function hasOne( relationName, foreignKey, owningKey ) { + private function hasOne( relationName, foreignKey, localKey ) { var related = variables._wirebox.getInstance( relationName ); if ( isNull( arguments.foreignKey ) ) { - arguments.foreignKey = variables._key; + arguments.foreignKey = variables._entityName & variables._key; } - if ( isNull( arguments.owningKey ) ) { - arguments.owningKey = variables._entityName & variables._key; + if ( isNull( arguments.localKey ) ) { + arguments.localKey = variables._key; } return variables._wirebox.getInstance( name = "HasOne@quick", initArguments = { - wirebox = variables._wirebox, related = related, relationName = relationName, relationMethodName = lcase( callStackGet()[ 2 ][ "Function" ] ), - owning = this, + parent = this, foreignKey = foreignKey, - foreignKeyValue = keyValue(), - owningKey = owningKey + localKey = localKey } ); } - private function hasMany( relationName, foreignKey, owningKey ) { + private function hasMany( relationName, foreignKey, localKey ) { var related = variables._wirebox.getInstance( relationName ); if ( isNull( arguments.foreignKey ) ) { arguments.foreignKey = variables._entityName & variables._key; } - if ( isNull( arguments.owningKey ) ) { - arguments.owningKey = variables._entityName & variables._key; + if ( isNull( arguments.localKey ) ) { + arguments.localKey = variables._key; } return variables._wirebox.getInstance( name = "HasMany@quick", initArguments = { - wirebox = variables._wirebox, related = related, relationName = relationName, relationMethodName = lcase( callStackGet()[ 2 ][ "Function" ] ), - owning = this, + parent = this, foreignKey = foreignKey, - foreignKeyValue = keyValue(), - owningKey = owningKey + localKey = localKey } ); } - private function belongsToMany( relationName, table, foreignKey, relatedKey ) { + private function belongsToMany( + relationName, + table, + foreignPivotKey, + relatedPivotKey, + parentKey, + relatedKey, + relationMethodName + ) { var related = variables._wirebox.getInstance( relationName ); if ( isNull( arguments.table ) ) { if ( compareNoCase( related.get_Table(), variables._table ) < 0 ) { @@ -479,82 +493,125 @@ component accessors="true" { arguments.table = lcase( "#variables._table#_#related.get_Table()#" ); } } - if ( isNull( arguments.relatedKey ) ) { - arguments.relatedKey = related.get_EntityName() & related.get_Key(); + if ( isNull( arguments.foreignPivotKey ) ) { + arguments.foreignPivotKey = variables._entityName & variables._key; } - if ( isNull( arguments.foreignKey ) ) { - arguments.foreignKey = variables._entityName & variables._key; + if ( isNull( arguments.relatedPivotKey ) ) { + arguments.relatedPivotKey = related.get_entityName() & related.get_key(); + } + if ( isNull( arguments.relationMethodName ) ) { + arguments.relationMethodName = lcase( callStackGet()[ 2 ][ "Function" ] ); + } + if ( isNull( arguments.parentKey ) ) { + arguments.parentKey = variables._key; + } + if ( isNull( arguments.relatedKey ) ) { + arguments.relatedKey = related.get_key(); } return variables._wirebox.getInstance( name = "BelongsToMany@quick", initArguments = { - wirebox = variables._wirebox, related = related, relationName = relationName, - relationMethodName = lcase( callStackGet()[ 2 ][ "Function" ] ), - owning = this, + relationMethodName = relationMethodName, + parent = this, table = arguments.table, - foreignKey = foreignKey, - foreignKeyValue = keyValue(), + foreignPivotKey = foreignPivotKey, + relatedPivotKey = relatedPivotKey, + parentKey = parentKey, relatedKey = relatedKey } ); } - private function hasManyThrough( relationName, intermediateName, foreignKey, intermediateKey, owningKey ) { + private function hasManyThrough( relationName, intermediateName, firstKey, secondKey, localKey, secondLocalKey ) { var related = variables._wirebox.getInstance( relationName ); var intermediate = variables._wirebox.getInstance( intermediateName ); - if ( isNull( arguments.intermediateKey ) ) { - arguments.intermediateKey = intermediate.get_EntityName() & intermediate.get_Key(); + if ( isNull( arguments.firstKey ) ) { + arguments.firstKey = intermediate.get_EntityName() & intermediate.get_Key(); } - if ( isNull( arguments.foreignKey ) ) { - arguments.foreignKey = variables._entityName & variables._key; + if ( isNull( arguments.firstKey ) ) { + arguments.firstKey = variables._entityName & variables._key; + } + if ( isNull( arguments.secondKey ) ) { + arguments.secondKey = intermediate.get_entityName() & intermediate.get_key(); } - if ( isNull( arguments.owningKey ) ) { - arguments.owningKey = variables._key; + if ( isNull( arguments.localKey ) ) { + arguments.localKey = variables._key; + } + if ( isNull( arguments.secondLocalKey ) ) { + arguments.secondLocalKey = intermediate.get_key(); } return variables._wirebox.getInstance( name = "HasManyThrough@quick", initArguments = { - wirebox = variables._wirebox, related = related, relationName = relationName, relationMethodName = lcase( callStackGet()[ 2 ][ "Function" ] ), - owning = this, + parent = this, intermediate = intermediate, - foreignKey = foreignKey, - foreignKeyValue = keyValue(), - intermediateKey = intermediateKey, - owningKey = owningKey + firstKey = firstKey, + secondKey = secondKey, + localKey = localKey, + secondLocalKey = secondLocalKey } ); } - private function polymorphicHasMany( relationName, prefix ) { + private function polymorphicHasMany( required relationName, required name, type, id, localKey ) { var related = variables._wirebox.getInstance( relationName ); + + if ( isNull( arguments.type ) ) { + arguments.type = arguments.name & "_type"; + } + if ( isNull( arguments.id ) ) { + arguments.id = arguments.name & "_id"; + } + var table = related.get_table(); + if ( isNull( arguments.localKey ) ) { + arguments.localKey = variables._key; + } + return variables._wirebox.getInstance( name = "PolymorphicHasMany@quick", initArguments = { - wirebox = variables._wirebox, related = related, relationName = relationName, relationMethodName = lcase( callStackGet()[ 2 ][ "Function" ] ), - owning = this, - foreignKey = "", - foreignKeyValue = "", - owningKey = "", - prefix = prefix + parent = this, + type = type, + id = id, + localKey = localKey } ); } - private function polymorphicBelongsTo( prefix ) { - var relationName = retrieveAttribute( - "#prefix#_type" - ); + private function polymorphicBelongsTo( name, type, id, ownerKey ) { + if ( isNull( arguments.name ) ) { + arguments.name = lcase( callStackGet()[ 2 ][ "Function" ] ); + } + if ( isNull( arguments.type ) ) { + arguments.type = arguments.name & "_type"; + } + if ( isNull( arguments.id ) ) { + arguments.id = arguments.name & "_id"; + } + var relationName = retrieveAttribute( arguments.type, "" ); + if ( relationName == "" ) { + return variables._wirebox.getInstance( name = "PolymorphicBelongsTo@quick", initArguments = { + related = this.set_EagerLoad( [] ).resetQuery(), + relationName = relationName, + relationMethodName = name, + parent = this, + foreignKey = arguments.id, + ownerKey = "", + type = type + } ); + } var related = variables._wirebox.getInstance( relationName ); + if ( isNull( ownerKey ) ) { + arguments.ownerKey = related.get_key(); + } return variables._wirebox.getInstance( name = "PolymorphicBelongsTo@quick", initArguments = { - wirebox = variables._wirebox, related = related, relationName = relationName, - relationMethodName = lcase( callStackGet()[ 2 ][ "Function" ] ), - owning = this, - foreignKey = related.get_Key(), - foreignKeyValue = retrieveAttribute( "#prefix#_id" ), - owningKey = "", - prefix = prefix + relationMethodName = name, + parent = this, + foreignKey = arguments.id, + ownerKey = ownerKey, + type = type } ); } @@ -567,7 +624,7 @@ component accessors="true" { return this; } - private function eagerLoadRelations( entities ) { + function eagerLoadRelations( entities ) { if ( arrayIsEmpty( entities ) || arrayIsEmpty( variables._eagerLoad ) ) { return entities; } @@ -580,32 +637,13 @@ component accessors="true" { } private function eagerLoadRelation( relationName, entities ) { - var keys = entities.map( function( entity ) { - return invoke( entity, relationName ).getForeignKeyValue(); - } ); - keys = arraySlice( createObject( "java", "java.util.HashSet" ).init( keys ).toArray(), 1 ); - var relatedEntity = invoke( entities[ 1 ], relationName ).getRelated(); - var owningKey = invoke( entities[ 1 ], relationName ).getOwningKey(); - var relations = relatedEntity.resetQuery().whereIn( owningKey, keys ).get( options = variables._queryOptions ); - - return matchRelations( entities, relations, relationName ); - } - - private function matchRelations( entities, relations, relationName ) { - var relationship = invoke( entities[ 1 ], relationName ); - var groupedRelations = groupBy( items = relations, key = relationship.getOwningKey(), forceLookup = true ); - entities.each( function( entity ) { - var relationship = invoke( entity, relationName ); - if ( structKeyExists( groupedRelations, relationship.getForeignKeyValue() ) ) { - entity.assignRelationship( relationName, relationship.fromGroup( - groupedRelations[ relationship.getForeignKeyValue() ] - ) ); - } - else { - entity.assignRelationship( relationName, relationship.getDefaultValue() ); - } - } ); - return entities; + var relation = invoke( this, relationName ).resetQuery(); + relation.addEagerConstraints( entities ); + return relation.match( + relation.initRelation( entities, relationName ), + relation.getEager(), + relationName + ); } /*======================================= @@ -716,7 +754,7 @@ component accessors="true" { relationship = invoke( this, relationshipName, missingMethodArguments ); } relationship.setRelationMethodName( relationshipName ); - assignRelationship( relationshipName, relationship.retrieve() ); + assignRelationship( relationshipName, relationship.getResults() ); } return retrieveRelationship( relationshipName ); diff --git a/models/QuickCollection.cfc b/models/QuickCollection.cfc index 1033a65d..f65042aa 100644 --- a/models/QuickCollection.cfc +++ b/models/QuickCollection.cfc @@ -31,28 +31,13 @@ component extends="cfcollection.models.Collection" { } private function eagerLoadRelation( relationName ) { - var keys = map( function( entity ) { - return invoke( entity, relationName ).getForeignKeyValue(); - } ).unique(); - var relatedEntity = invoke( get( 1 ), relationName ).getRelated(); - var owningKey = invoke( get( 1 ), relationName ).getOwningKey(); - var relations = relatedEntity.whereIn( owningKey, keys.get() ).get(); - - return matchRelations( relations, relationName ); - } - - private function matchRelations( relations, relationName ) { - var relationship = invoke( get( 1 ), relationName ); - var groupedRelations = collect( relations ).groupBy( key = relationship.getOwningKey(), forceLookup = true ); - return this.each( function( entity ) { - var relationship = invoke( entity, relationName ); - if ( structKeyExists( groupedRelations, relationship.getForeignKeyValue() ) ) { - entity.assignRelationship( relationName, groupedRelations[ relationship.getForeignKeyValue() ] ); - } - else { - entity.assignRelationship( relationName, relationship.getDefaultValue() ); - } - } ); + var relation = invoke( get( 1 ), relationName ).resetQuery(); + relation.addEagerConstraints( get() ); + variables.collection = relation.match( + relation.initRelation( get(), relationName ), + relation.getEager(), + relationName + ); } } diff --git a/models/Relationships/BaseRelationship.cfc b/models/Relationships/BaseRelationship.cfc index 528ea7f0..24a615fc 100644 --- a/models/Relationships/BaseRelationship.cfc +++ b/models/Relationships/BaseRelationship.cfc @@ -1,33 +1,31 @@ -component accessors="true" { - - property name="wirebox"; - - property name="related"; - property name="relationName"; - property name="relationMethodName"; - property name="owning"; - property name="foreignKey"; - property name="foreignKeyValue"; - property name="owningKey"; - property name="defaultValue"; - - function init( wirebox, related, relationName, relationMethodName, owning, foreignKey, foreignKeyValue, owningKey ) { - setWireBox( wirebox ); - setRelated( arguments.related ); - setRelationName( arguments.relationName ); - setRelationMethodName( arguments.relationMethodName ); - setOwning( arguments.owning ); - setForeignKey( arguments.foreignKey ); - setForeignKeyValue( arguments.foreignKeyValue ); - setOwningKey( arguments.owningKey ); - - apply(); +component { + property name="wirebox" inject="wirebox"; + + function init( related, relationName, relationMethodName, parent ) { + variables.related = arguments.related.resetQuery(); + variables.relationName = arguments.relationName; + variables.relationMethodName = arguments.relationMethodName; + variables.parent = arguments.parent; + + addConstraints(); + + return this; + } + + function setRelationMethodName( name ) { + variables.relationMethodName = arguments.name; return this; } - private function collect( items = [] ) { - return isArray( items ) ? items : listToArray( items, "," ); + function getEager() { + return variables.related.get(); + } + + function getKeys( entities, key ) { + return unique( entities.map( function( entity ) { + return entity.retrieveAttribute( key ); + } ) ); } function onMissingMethod( missingMethodName, missingMethodArguments ) { @@ -38,9 +36,8 @@ component accessors="true" { return this; } - private function isQuickEntity( entity ) { - return getMetadata( entity ).keyExists( "quick" ) || - isInstanceOf( entity, "quick.models.BaseEntity" ); + function unique( items ) { + return arraySlice( createObject( "java", "java.util.HashSet" ).init( items ).toArray(), 1 ); } } diff --git a/models/Relationships/BelongsTo.cfc b/models/Relationships/BelongsTo.cfc index d23d980c..3d54c7b3 100644 --- a/models/Relationships/BelongsTo.cfc +++ b/models/Relationships/BelongsTo.cfc @@ -1,31 +1,75 @@ component extends="quick.models.Relationships.BaseRelationship" { - function onDIComplete() { - setDefaultValue( javacast( "null", "" ) ); + function init( related, relationName, relationMethodName, parent, foreignKey, ownerKey ) { + variables.ownerKey = arguments.ownerKey; + variables.foreignKey = arguments.foreignKey; + + variables.child = arguments.parent; + + super.init( related, relationName, relationMethodName, parent ); + } + + function getResults() { + return variables.related.first(); + } + + function addConstraints() { + var table = variables.related.get_Table(); + variables.related.where( + "#table#.#variables.ownerKey#", + "=", + variables.child.retrieveAttribute( variables.foreignKey ) + ); } - function apply() { - getRelated().where( getOwningKey(), getForeignKeyValue() ); + function addEagerConstraints( entities ) { + var key = variables.related.get_Table() & "." & variables.ownerKey; + variables.related.whereIn( key, getEagerEntityKeys( entities ) ); } - function retrieve() { - return related.first(); + function getEagerEntityKeys( entities ) { + return entities.reduce( function( keys, entity ) { + if ( ! isNull( entity.retrieveAttribute( variables.foreignKey ) ) ) { + arrayAppend( keys, entity.retrieveAttribute( variables.foreignKey ) ); + } + return keys; + }, [] ); } - function fromGroup( items ) { - return items[ 1 ]; + function initRelation( entities, relation ) { + entities.each( function( entity ) { + entity.assignRelationship( relation, {} ); + } ); + return entities; + } + + function match( entities, results, relation ) { + var dictionary = results.reduce( function( dict, result ) { + dict[ result.retrieveAttribute( variables.ownerKey ) ] = result; + return dict; + }, {} ); + + entities.each( function( entity ) { + if ( structKeyExists( dictionary, entity.retrieveAttribute( variables.foreignKey ) ) ) { + entity.assignRelationship( relation, dictionary[ entity.retrieveAttribute( variables.foreignKey ) ] ); + } + } ); + + return entities; } function associate( entity ) { - if ( isQuickEntity( entity ) ) { - arguments.entity = entity.keyValue(); + var ownerKeyValue = isSimpleValue( entity ) ? entity : entity.retrieveAttribute( variables.ownerKey ); + variables.child.assignAttribute( variables.foreignKey, ownerKeyValue ); + if ( ! isSimpleValue( entity ) ) { + variables.child.assignRelationship( variables.relationMethodName, entity ); } - - return getOwning().assignAttribute( getForeignKey(), entity ); + return variables.child; } - function disassociate() { - return getOwning().clearAttribute( name = getForeignKey(), setToNull = true ); + function dissociate() { + variables.child.clearAttribute( variables.foreignKey, true ); + return variables.child.clearRelationship( variables.relationMethodName ); } } diff --git a/models/Relationships/BelongsToMany.cfc b/models/Relationships/BelongsToMany.cfc index bb230a1a..934b7908 100644 --- a/models/Relationships/BelongsToMany.cfc +++ b/models/Relationships/BelongsToMany.cfc @@ -1,105 +1,148 @@ component accessors="true" extends="quick.models.Relationships.BaseRelationship" { - property name="builder" inject="provider:QueryBuilder@qb" getter="false" setter="false"; - property name="table"; - - variables.defaultValue = []; - - function init( wirebox, related, relationName, relationMethodName, owning, table, foreignKey, foreignKeyValue, relatedKey ) { - setTable( arguments.table ); - super.init( wirebox, related, relationName, relationMethodName, owning, foreignKey, foreignKeyValue, relatedKey ); - return this; + function init( + related, + relationName, + relationMethodName, + parent, + table, + foreignPivotKey, + relatedPivotKey, + parentKey, + relatedKey + ) { + variables.table = arguments.table; + variables.parentKey = arguments.parentKey; + variables.relatedKey = arguments.relatedKey; + variables.relatedPivotKey = arguments.relatedPivotKey; + variables.foreignPivotKey = arguments.foreignPivotKey; + + super.init( related, relationName, relationMethodName, parent ); } - function onDIComplete() { - setDefaultValue( collect() ); + function getResults() { + return variables.related.get(); } - function apply() { - getRelated().join( variables.table, function( j ) { - j.on( - "#getRelated().get_Table()#.#getRelated().get_Key()#", - "#variables.table#.#getOwningKey()#" - ); - j.where( "#variables.table#.#getForeignKey()#", getForeignKeyValue() ); - } ); + function addConstraints() { + performJoin(); + addWhereConstraints(); } - function fromGroup( items ) { - return collect( items ); + function addEagerConstraints( entities ) { + variables.related + .from( variables.table ) + .whereIn( + getQualifiedForeignPivotKeyName(), + getKeys( entities, variables.parentKey ) + ); } - function retrieve() { - return getRelated().get(); + function initRelation( entities, relation ) { + entities.each( function( entity ) { + entity.assignRelationship( relation, [] ); + } ); + return entities; } - function attach( ids ) { - if ( ! isArray( arguments.ids ) ) { - arguments.ids = [ arguments.ids ]; - } - - arguments.ids = arrayMap( arguments.ids, function( id ) { - if ( isSimpleValue( id ) ) { - return id; + function match( entities, results, relation ) { + var dictionary = buildDictionary( results ); + entities.each( function( entity ) { + if ( structKeyExists( dictionary, entity.retrieveAttribute( variables.parentKey ) ) ) { + entity.assignRelationship( + relation, + dictionary[ entity.retrieveAttribute( variables.parentKey ) ] + ); } - return id.keyValue(); } ); + return entities; + } - builder.get().from( variables.table ).insert( arrayMap( arguments.ids, function( id ) { - return { - "#getForeignKey()#" = getForeignKeyValue(), - "#getOwningKey()#" = id - }; - } ) ); + function buildDictionary( results ) { + return results.reduce( function( dict, result ) { + var key = result.retrieveAttribute( variables.foreignPivotKey ); + if ( ! structKeyExists( dict, key ) ) { + dict[ key ] = []; + } + arrayAppend( dict[ key ], result ); + return dict; + }, {} ); + } - getOwning().clearRelationship( getRelationMethodName() ); + function performJoin() { + var baseTable = variables.related.get_table(); + var key = baseTable & "." & variables.relatedKey; + variables.related.join( variables.table, key, "=", getQualifiedRelatedPivotKeyName() ); + return this; + } + function addWhereConstraints() { + variables.related.where( + getQualifiedForeignPivotKeyName(), + "=", + variables.parent.retrieveAttribute( variables.parentKey ) + ); return this; } - function detach( ids ) { - if ( ! isArray( arguments.ids ) ) { - arguments.ids = [ arguments.ids ]; - } + function getQualifiedRelatedPivotKeyName() { + return variables.table & "." & variables.relatedPivotKey; + } - arguments.ids = arrayMap( arguments.ids, function( id ) { - if ( isSimpleValue( id ) ) { - return id; - } - return id.keyValue(); - } ); + function getQualifiedForeignPivotKeyName() { + return variables.table & "." & variables.foreignPivotKey; + } - builder.get() - .from( variables.table ) - .where( getForeignKey(), getForeignKeyValue() ) - .whereIn( getOwningKey(), arguments.ids ) - .delete(); + function attach( id ) { + newPivotStatement().insert( parseIdsForInsert( id ) ); + } - getOwning().clearRelationship( getRelationMethodName() ); + function detach( id ) { + var foreignPivotKeyValue = variables.parent.retrieveAttribute( variables.parentKey ); + newPivotStatement() + .where( variables.parentKey, "=", foreignPivotKeyValue ) + .whereIn( + variables.relatedPivotKey, + parseIds( id ) + ).delete(); + } - return this; + function sync( id ) { + var foreignPivotKeyValue = variables.parent.retrieveAttribute( variables.parentKey ); + newPivotStatement().where( variables.parentKey, "=", foreignPivotKeyValue ).delete(); + attach( id ); } - function sync( ids ) { - if ( ! isArray( arguments.ids ) ) { - arguments.ids = [ arguments.ids ]; - } + function newPivotStatement() { + return variables.related.newQuery().from( variables.table ); + } - arguments.ids = arrayMap( arguments.ids, function( id ) { - if ( isSimpleValue( id ) ) { - return id; + function parseIds( value ) { + arguments.value = isArray( value ) ? value : [ value ]; + return arguments.value.map( function( val ) { + // If the value is not a simple value, we will assume + // it is an entity and return its key value. + if ( ! isSimpleValue( val ) ) { + return val.keyValue(); } - return id.keyValue(); + return val; } ); + } - builder.get() - .from( variables.table ) - .where( getForeignKey(), getForeignKeyValue() ) - .delete(); - - attach( arguments.ids ); - - return this; + function parseIdsForInsert( value ) { + var foreignPivotKeyValue = variables.parent.retrieveAttribute( variables.parentKey ); + arguments.value = isArray( value ) ? value : [ value ]; + return arguments.value.map( function( val ) { + // If the value is not a simple value, we will assume + // it is an entity and return its key value. + if ( ! isSimpleValue( val ) ) { + arguments.val = val.keyValue(); + } + var insertRecord = {}; + insertRecord[ variables.parentKey ] = foreignPivotKeyValue; + insertRecord[ variables.relatedPivotKey ] = arguments.val; + return insertRecord; + } ); } } diff --git a/models/Relationships/HasMany.cfc b/models/Relationships/HasMany.cfc index 4b83fbcc..36b7f551 100644 --- a/models/Relationships/HasMany.cfc +++ b/models/Relationships/HasMany.cfc @@ -1,32 +1,18 @@ -component extends="quick.models.Relationships.BaseRelationship" { +component extends="quick.models.Relationships.HasOneOrMany" { - function onDIComplete() { - setDefaultValue( collect() ); + function getResults() { + return variables.related.get(); } - function apply() { - getRelated().where( getOwningKey(), getForeignKeyValue() ); + function initRelation( entities, relation ) { + entities.each( function( entity ) { + entity.assignRelationship( relation, [] ); + } ); + return entities; } - function retrieve() { - return getRelated().get(); - } - - function fromGroup( items ) { - return collect( items ); - } - - function save( entity ) { - getOwning().clearRelationship( getRelationMethodName() ); - return entity.assignAttribute( getOwningKey(), getForeignKeyValue() ).save(); - } - - function create( attributes ) { - getOwning().clearRelationship( getRelationMethodName() ); - return wirebox.getInstance( getRelationName() ) - .assignAttributesData( attributes ) - .assignAttribute( getOwningKey(), getForeignKeyValue() ) - .save(); + function match( entities, results, relation ) { + return matchMany( argumentCollection = arguments ); } } diff --git a/models/Relationships/HasManyThrough.cfc b/models/Relationships/HasManyThrough.cfc index e7253549..ec6a49a0 100644 --- a/models/Relationships/HasManyThrough.cfc +++ b/models/Relationships/HasManyThrough.cfc @@ -1,40 +1,110 @@ component accessors="true" extends="quick.models.Relationships.BaseRelationship" { - property name="intermediate"; - property name="intermediateKey"; + function init( + related, + relationName, + relationMethodName, + parent, + intermediate, + firstKey, + secondKey, + localKey, + secondLocalKey + ) { + variables.throughParent = arguments.intermediate; + variables.farParent = arguments.parent; - function init( wirebox, related, relationName, relationMethodName, owning, intermediate, foreignKey, foreignKeyValue, intermediateKey ) { - setIntermediate( intermediate ); - setIntermediateKey( intermediateKey ); - super.init( wirebox, related, relationName, relationMethodName, owning, foreignKey, foreignKeyValue, owningKey ); - return this; + variables.firstKey = arguments.firstKey; + variables.secondKey = arguments.secondKey; + variables.localKey = arguments.localKey; + variables.secondLocalKey = arguments.secondLocalKey; + + super.init( related, relationName, relationMethodName, intermediate ); + } + + function addConstraints() { + var localValue = variables.farParent.retrieveAttribute( variables.localKey ); + performJoin(); + variables.related.where( + getQualifiedFirstKeyName(), + "=", + localValue + ); + } + + function performJoin() { + var farKey = getQualifiedFarKeyName(); + variables.related.join( + variables.throughParent.get_Table(), + getQualifiedParentKeyName(), + "=", + farKey + ); + } + + function getQualifiedFarKeyName() { + return getQualifiedForeignKeyName(); + } + + function getQualifiedForeignKeyName() { + return variables.related.qualifyColumn( variables.secondKey ); + } + + function getQualifiedFirstKeyName() { + return variables.throughParent.qualifyColumn( variables.firstKey ); + } + + function getQualifiedParentKeyName() { + return variables.parent.qualifyColumn( variables.secondLocalKey ); + } + + function getResults() { + return this.get(); + } + + function get() { + var entities = variables.related.getEntities(); + if ( entities.len() > 0 ) { + entities = variables.related.eagerLoadRelations( entities ); + } + return entities; } - function onDIComplete() { - setDefaultValue( collect() ); + function addEagerConstraints( entities ) { + performJoin(); + variables.related.whereIn( + getQualifiedFirstKeyName(), + getKeys( entities, variables.localKey ) + ); } - function apply() { - getRelated() - .join( - getIntermediate().get_Table(), - "#getIntermediate().get_Table()#.#getIntermediate().get_Key()#", - "#getRelated().get_Table()#.#getIntermediateKey()#" - ) - .join( - getOwning().get_Table(), - "#getOwning().get_Table()#.#getOwningKey()#", - "#getIntermediate().get_Table()#.#getForeignKey()#" - ) - .where( "#getOwning().get_Table()#.#getOwningKey()#", getOwning().keyValue() ); + function initRelation( entities, relation ) { + entities.each( function( entity ) { + entity.assignRelationship( relation, [] ); + } ); + return entities; } - function fromGroup( items ) { - return collect( items ); + function match( entities, results, relation ) { + var dictionary = buildDictionary( results ); + entities.each( function( entity ) { + var key = entity.retrieveAttribute( variables.localKey ); + if ( structKeyExists( dictionary, key ) ) { + entity.assignRelationship( relation, dictionary[ key ] ); + } + } ); + return entities; } - function retrieve() { - return getRelated().get(); + function buildDictionary( results ) { + return results.reduce( function( dict, result ) { + var key = result.retrieveAttribute( variables.firstKey ); + if ( ! structKeyExists( dict, key ) ) { + dict[ key ] = []; + } + arrayAppend( dict[ key ], result ); + return dict; + }, {} ); } } diff --git a/models/Relationships/HasOne.cfc b/models/Relationships/HasOne.cfc index f448022b..8b7f0c00 100644 --- a/models/Relationships/HasOne.cfc +++ b/models/Relationships/HasOne.cfc @@ -1,19 +1,18 @@ -component extends="quick.models.Relationships.BaseRelationship" { +component extends="quick.models.Relationships.HasOneOrMany" { - function onDIComplete() { - setDefaultValue( javacast( "null", "" ) ); + function getResults() { + return variables.related.first(); } - function apply() { - getRelated().where( getOwningKey(), getForeignKeyValue() ); + function initRelation( entities, relation ) { + entities.each( function( entity ) { + entity.assignRelationship( relation, {} ); + } ); + return entities; } - function fromGroup( items ) { - return items[ 1 ]; - } - - function retrieve() { - return getRelated().first(); + function match( entities, results, relation ) { + return matchOne( argumentCollection = arguments ); } } diff --git a/models/Relationships/HasOneOrMany.cfc b/models/Relationships/HasOneOrMany.cfc new file mode 100644 index 00000000..a603523b --- /dev/null +++ b/models/Relationships/HasOneOrMany.cfc @@ -0,0 +1,93 @@ +component extends="quick.models.Relationships.BaseRelationship" accessors="true" { + + function init( related, relationName, relationMethodName, parent, foreignKey, localKey ) { + variables.localKey = arguments.localKey; + variables.foreignKey = arguments.foreignKey; + + return super.init( related, relationName, relationMethodName, parent ); + } + + function addConstraints() { + variables.related.retrieveQuery() + .where( variables.foreignKey, "=", getParentKey() ) + .whereNotNull( variables.foreignKey ); + } + + function addEagerConstraints( entities ) { + variables.related.retrieveQuery().whereIn( + variables.foreignKey, getKeys( entities, variables.localKey ) + ); + } + + function matchOne( entities, results, relation ) { + arguments.type = "one"; + return matchOneOrMany( argumentCollection = arguments ); + } + + function matchMany( entities, results, relation ) { + arguments.type = "many"; + return matchOneOrMany( argumentCollection = arguments ); + } + + function matchOneOrMany( entities, results, relation, type ) { + var dictionary = buildDictionary( results ); + entities.each( function( entity ) { + var key = entity.retrieveAttribute( variables.localKey ); + if ( structKeyExists( dictionary, key ) ) { + entity.assignRelationship( + relation, + getRelationValue( dictionary, key, type ) + ); + } + } ); + return entities; + } + + function buildDictionary( results ) { + return results.reduce( function( dict, result ) { + var key = invoke( result, "get#variables.foreignKey#" ); + if ( ! structKeyExists( dict, key ) ) { + dict[ key ] = []; + } + arrayAppend( dict[ key ], result ); + return dict; + }, {} ); + } + + function getRelationValue( dictionary, key, type ) { + var value = dictionary[ key ]; + return type == "one" ? value[ 1 ] : value; + } + + function getParentKey() { + return variables.parent.retrieveAttribute( variables.localKey ); + } + + function save( entity ) { + setForeignAttributesForCreate( entity ); + return entity.save(); + } + + function create( attributes = {} ) { + var newInstance = variables.related.newEntity().fill( attributes ); + setForeignAttributesForCreate( newInstance ); + return newInstance.save(); + } + + function setForeignAttributesForCreate( entity ) { + entity.assignAttribute( + getForeignKeyName(), + getParentKey() + ); + } + + function getForeignKeyName() { + var parts = listToArray( getQualifiedForeignKeyName(), "." ); + return parts[ arrayLen( parts ) ]; + } + + function getQualifiedForeignKeyName() { + return variables.foreignKey; + } + +} diff --git a/models/Relationships/PolymorphicBelongsTo.cfc b/models/Relationships/PolymorphicBelongsTo.cfc index 504aaf80..326ff0c7 100644 --- a/models/Relationships/PolymorphicBelongsTo.cfc +++ b/models/Relationships/PolymorphicBelongsTo.cfc @@ -1,19 +1,75 @@ -component extends="quick.models.Relationships.BaseRelationship" { +component extends="quick.models.Relationships.BelongsTo" { - function onDIComplete() { - setDefaultValue( javacast( "null", "" ) ); + function init( related, relationName, relationMethodName, parent, foreignKey, ownerKey, type ) { + variables.morphType = arguments.type; + + return super.init( related, relationName, relationMethodName, parent, foreignKey, ownerKey ); + } + + function addEagerConstraints( entities ) { + variables.entities = arguments.entities; + buildDictionary( variables.entities ); + } + + function buildDictionary( entities ) { + variables.dictionary = entities.reduce( function( dict, entity ) { + var type = entity.retrieveAttribute( variables.morphType ); + if ( ! structKeyExists( dict, type ) ) { + dict[ type ] = {}; + } + var key = entity.retrieveAttribute( variables.foreignKey ); + if ( ! structKeyExists( dict[ type ], key ) ) { + dict[ type ][ key ] = []; + } + arrayAppend( dict[ type ][ key ], entity ); + return dict; + }, {} ); + } + + function getResults() { + return variables.ownerKey != "" ? super.getResults() : {}; + } + + function getEager() { + structKeyArray( variables.dictionary ).each( function( type ) { + matchToMorphParents( type, getResultsByType( type ) ); + } ); + + return variables.entities; + } + + function getResultsByType( type ) { + var instance = createModelByType( type ); + var localOwnerKey = variables.ownerKey != "" ? variables.ownerKey : instance.get_Key(); + instance.with( variables.related.get_eagerLoad() ); + + return instance.whereIn( + instance.get_table() & "." & localOwnerKey, + gatherKeysByType( type ) + ).get(); } - function apply() { - getRelated().where( getForeignKey(), getForeignKeyValue() ); + function gatherKeysByType( type ) { + return unique( structReduce( variables.dictionary[ type ], function( acc, key, values ) { + arrayAppend( acc, values[ 1 ].retrieveAttribute( variables.foreignKey ) ); + return acc; + }, [] ) ); } - function fromGroup( items ) { - return items[ 1 ]; + function createModelByType( type ) { + return variables.wirebox.getInstance( type ); } - function retrieve() { - return getRelated().first(); + function matchToMorphParents( type, results ) { + results.each( function( result ) { + var ownerKeyValue = variables.ownerKey != "" ? result.retrieveAttribute( variables.ownerKey ) : result.keyValue(); + if ( structKeyExists( variables.dictionary[ type ], ownerKeyValue ) ) { + var entities = variables.dictionary[ type ][ ownerKeyValue ]; + entities.each( function( entity ) { + entity.assignRelationship( variables.relationMethodName, result ); + } ); + } + } ); } } diff --git a/models/Relationships/PolymorphicHasMany.cfc b/models/Relationships/PolymorphicHasMany.cfc index c97b459d..129ca26e 100644 --- a/models/Relationships/PolymorphicHasMany.cfc +++ b/models/Relationships/PolymorphicHasMany.cfc @@ -1,29 +1,18 @@ -component accessors="true" extends="quick.models.Relationships.BaseRelationship" { +component extends="quick.models.Relationships.PolymorphicHasOneOrMany" { - property name="prefix"; - - function init( wirebox, related, relationName, relationMethodName, owning, foreignKey, foreignKeyValue, owningKey, prefix ) { - setPrefix( arguments.prefix ); - super.init( wirebox, related, relationName, relationMethodName, owning, foreignKey, foreignKeyValue, owningKey ); - return this; - } - - function onDIComplete() { - setDefaultValue( collect() ); - } - - function apply() { - getRelated() - .where( "#getPrefix()#_type", getOwning().get_Mapping() ) - .where( "#getPrefix()#_id", getOwning().keyValue() ); + function getResults() { + return variables.related.get(); } - function fromGroup( items ) { - return collect( items ); + function initRelation( entities, relation ) { + entities.each( function( entity ) { + entity.assignRelationship( relation, [] ); + } ); + return entities; } - function retrieve() { - return getRelated().get(); + function match( entities, results, relation ) { + return matchMany( entities, results, relation ); } } diff --git a/models/Relationships/PolymorphicHasOneOrMany.cfc b/models/Relationships/PolymorphicHasOneOrMany.cfc new file mode 100644 index 00000000..f7641e6a --- /dev/null +++ b/models/Relationships/PolymorphicHasOneOrMany.cfc @@ -0,0 +1,19 @@ +component extends="quick.models.Relationships.HasOneOrMany" { + + function init( related, relationName, relationMethodName, parent, type, id, localKey ) { + variables.morphType = arguments.type; + variables.morphClass = arguments.parent.get_entityName(); + return super.init( related, relationName, relationMethodName, parent, id, localKey ); + } + + function addConstraints() { + super.addConstraints(); + variables.related.where( variables.morphType, variables.morphClass ); + } + + function addEagerConstraints( entities ) { + super.addEagerConstraints( entities ); + variables.related.where( variables.morphType, variables.morphClass ); + } + +} diff --git a/tests/Application.cfc b/tests/Application.cfc index cb0a6ba6..967a3fd8 100644 --- a/tests/Application.cfc +++ b/tests/Application.cfc @@ -76,10 +76,10 @@ component { ) " ); queryExecute( " - INSERT INTO `my_posts` (`post_pk`, `user_id`, `body`, `created_date`, `modified_date`) VALUES (1, 1, 'My awesome post body', '2017-07-28 02:07:00', '2017-07-28 02:07:00') + INSERT INTO `my_posts` (`post_pk`, `user_id`, `body`, `created_date`, `modified_date`) VALUES (1245, 1, 'My awesome post body', '2017-07-28 02:07:00', '2017-07-28 02:07:00') " ); queryExecute( " - INSERT INTO `my_posts` (`post_pk`, `user_id`, `body`, `created_date`, `modified_date`) VALUES (2, 1, 'My second awesome post body', '2017-07-28 02:07:36', '2017-07-28 02:07:36') + INSERT INTO `my_posts` (`post_pk`, `user_id`, `body`, `created_date`, `modified_date`) VALUES (523526, 1, 'My second awesome post body', '2017-07-28 02:07:36', '2017-07-28 02:07:36') " ); queryExecute( " CREATE TABLE `videos` ( @@ -110,10 +110,10 @@ component { ) " ); queryExecute( " - INSERT INTO `comments` (`id`, `body`, `commentable_id`, `commentable_type`, `created_date`, `modified_date`) VALUES (1, 'I thought this post was great', 1, 'Post', '2017-07-02 04:14:22', '2017-07-02 04:14:22') + INSERT INTO `comments` (`id`, `body`, `commentable_id`, `commentable_type`, `created_date`, `modified_date`) VALUES (1, 'I thought this post was great', 1245, 'Post', '2017-07-02 04:14:22', '2017-07-02 04:14:22') " ); queryExecute( " - INSERT INTO `comments` (`id`, `body`, `commentable_id`, `commentable_type`, `created_date`, `modified_date`) VALUES (2, 'I thought this post was not so good', 1, 'Post', '2017-07-04 04:14:22', '2017-07-04 04:14:22') + INSERT INTO `comments` (`id`, `body`, `commentable_id`, `commentable_type`, `created_date`, `modified_date`) VALUES (2, 'I thought this post was not so good', 1245, 'Post', '2017-07-04 04:14:22', '2017-07-04 04:14:22') " ); queryExecute( " INSERT INTO `comments` (`id`, `body`, `commentable_id`, `commentable_type`, `created_date`, `modified_date`) VALUES (3, 'What a great video! So fun!', 2, 'Video', '2017-07-02 04:14:22', '2017-07-02 04:14:22') @@ -134,10 +134,10 @@ component { PRIMARY KEY (`post_pk`, `tag_id`) ) " ); - queryExecute( "INSERT INTO `my_posts_tags` (`post_pk`, `tag_id`) VALUES (1, 1)" ); - queryExecute( "INSERT INTO `my_posts_tags` (`post_pk`, `tag_id`) VALUES (1, 2)" ); - queryExecute( "INSERT INTO `my_posts_tags` (`post_pk`, `tag_id`) VALUES (2, 1)" ); - queryExecute( "INSERT INTO `my_posts_tags` (`post_pk`, `tag_id`) VALUES (2, 2)" ); + queryExecute( "INSERT INTO `my_posts_tags` (`post_pk`, `tag_id`) VALUES (1245, 1)" ); + queryExecute( "INSERT INTO `my_posts_tags` (`post_pk`, `tag_id`) VALUES (1245, 2)" ); + queryExecute( "INSERT INTO `my_posts_tags` (`post_pk`, `tag_id`) VALUES (523526, 1)" ); + queryExecute( "INSERT INTO `my_posts_tags` (`post_pk`, `tag_id`) VALUES (523526, 2)" ); queryExecute( " CREATE TABLE `links` ( `link_id` int(11) NOT NULL AUTO_INCREMENT, diff --git a/tests/resources/app/models/A.cfc b/tests/resources/app/models/A.cfc new file mode 100644 index 00000000..74c1db7b --- /dev/null +++ b/tests/resources/app/models/A.cfc @@ -0,0 +1,10 @@ +component table="a" extends="quick.models.BaseEntity" { + + property name="id"; + property name="name"; + + function b() { + return hasMany( "b" ); + } + +} diff --git a/tests/resources/app/models/B.cfc b/tests/resources/app/models/B.cfc new file mode 100644 index 00000000..f7eae4c6 --- /dev/null +++ b/tests/resources/app/models/B.cfc @@ -0,0 +1,11 @@ +component table="b" extends="quick.models.BaseEntity" { + + property name="id"; + property name="a_id"; + property name="name"; + + function a() { + return belongsTo( "a" ); + } + +} diff --git a/tests/resources/app/models/Comment.cfc b/tests/resources/app/models/Comment.cfc index 888268e3..ef7097a2 100644 --- a/tests/resources/app/models/Comment.cfc +++ b/tests/resources/app/models/Comment.cfc @@ -1,4 +1,4 @@ -component extends="quick.models.BaseEntity" { +component extends="quick.models.BaseEntity" accessors="true" { property name="id"; property name="body"; diff --git a/tests/resources/app/models/Post.cfc b/tests/resources/app/models/Post.cfc index d31fa8bc..124c4807 100644 --- a/tests/resources/app/models/Post.cfc +++ b/tests/resources/app/models/Post.cfc @@ -1,4 +1,4 @@ -component entityname="MyPost" table="my_posts" extends="quick.models.BaseEntity" { +component table="my_posts" extends="quick.models.BaseEntity" accessors="true" { property name="post_pk"; property name="userId" column="user_id"; @@ -13,11 +13,7 @@ component entityname="MyPost" table="my_posts" extends="quick.models.BaseEntity" } function tags() { - return belongsToMany( - relationName = "Tag", - relatedKey = "tag_id", - foreignKey = "post_pk" - ); + return belongsToMany( "Tag", "my_posts_tags", "post_pk", "tag_id" ); } function comments() { diff --git a/tests/resources/app/models/Tag.cfc b/tests/resources/app/models/Tag.cfc index fb583ae6..2d9bc817 100644 --- a/tests/resources/app/models/Tag.cfc +++ b/tests/resources/app/models/Tag.cfc @@ -4,11 +4,7 @@ component extends="quick.models.BaseEntity" { property name="name"; function posts() { - return belongsToMany( - relationName = "Post", - relatedKey = "post_pk", - foreignKey = "tag_id" - ); + return belongsToMany( "Post", "my_posts_tags", "tag_id", "post_pk" ); } } diff --git a/tests/resources/app/models/User.cfc b/tests/resources/app/models/User.cfc index 351040e9..9d85d09d 100644 --- a/tests/resources/app/models/User.cfc +++ b/tests/resources/app/models/User.cfc @@ -20,11 +20,11 @@ component extends="quick.models.BaseEntity" accessors="true" { } function posts() { - return hasMany( "Post", "post_pk", "user_id" ); + return hasMany( "Post", "user_id" ); } function latestPost() { - return hasOne( "Post", "post_pk", "user_id" ).latest(); + return hasOne( "Post", "user_id" ).latest(); } } diff --git a/tests/resources/app/models/Video.cfc b/tests/resources/app/models/Video.cfc index ff66505d..39c0a8df 100644 --- a/tests/resources/app/models/Video.cfc +++ b/tests/resources/app/models/Video.cfc @@ -1,4 +1,4 @@ -component extends="quick.models.BaseEntity" { +component extends="quick.models.BaseEntity" accessors="true" { property name="id"; property name="url"; diff --git a/tests/specs/integration/BaseEntity/AttributeCasingSpec.cfc b/tests/specs/integration/BaseEntity/AttributeCasingSpec.cfc index 41eba1fe..07935c17 100644 --- a/tests/specs/integration/BaseEntity/AttributeCasingSpec.cfc +++ b/tests/specs/integration/BaseEntity/AttributeCasingSpec.cfc @@ -3,7 +3,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { function run() { describe( "Attributes Casing Spec", function() { it( "defaults to no transformation", function() { - var post = getInstance( "Post" ).find( 1 ); + var post = getInstance( "Post" ).find( 1245 ); expect( post.retrieveAttributesData() ).toHaveKey( "post_pk" ); expect( post.retrieveAttributesData() ).notToHaveKey( "PostPk" ); diff --git a/tests/specs/integration/BaseEntity/AttributeSpec.cfc b/tests/specs/integration/BaseEntity/AttributeSpec.cfc index 5e90afae..8bfad6cf 100644 --- a/tests/specs/integration/BaseEntity/AttributeSpec.cfc +++ b/tests/specs/integration/BaseEntity/AttributeSpec.cfc @@ -9,8 +9,8 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { } ); it( "can get foreign keys just like any other column", function() { - var post = getInstance( "Post" ).find( 1 ); - expect( post.getPost_Pk() ).toBe( 1 ); + var post = getInstance( "Post" ).find( 1245 ); + expect( post.getPost_Pk() ).toBe( 1245 ); expect( post.getUser_Id() ).toBe( 1 ); } ); diff --git a/tests/specs/integration/BaseEntity/GetSpec.cfc b/tests/specs/integration/BaseEntity/GetSpec.cfc index 34f7df89..a7e5fe2f 100644 --- a/tests/specs/integration/BaseEntity/GetSpec.cfc +++ b/tests/specs/integration/BaseEntity/GetSpec.cfc @@ -5,7 +5,6 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { it( "finds an entity by the primary key", function() { var user = getInstance( "User" ).find( 1 ); expect( user.isLoaded() ).toBeTrue( "The user instance should be found and loaded, but was not." ); - debug( user.get_Key() ); } ); it( "it returns null if the record cannot be found", function() { diff --git a/tests/specs/integration/BaseEntity/MetadataSpec.cfc b/tests/specs/integration/BaseEntity/MetadataSpec.cfc index 55142702..a149b4ab 100644 --- a/tests/specs/integration/BaseEntity/MetadataSpec.cfc +++ b/tests/specs/integration/BaseEntity/MetadataSpec.cfc @@ -18,11 +18,6 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { } ); describe( "entity name", function() { - it( "determines the entity name from the metadata if an `entityname` attribute is present", function() { - var post = getInstance( "Post" ); - expect( post.get_EntityName() ).toBe( "MyPost" ); - } ); - it( "calculates the entity name from the file name if no `entityname` attribute is present", function() { var post = getInstance( "User" ); expect( post.get_EntityName() ).toBe( "User" ); diff --git a/tests/specs/integration/BaseEntity/Relationships/BelongsToManySpec.cfc b/tests/specs/integration/BaseEntity/Relationships/BelongsToManySpec.cfc index 8b462909..1fc22e30 100644 --- a/tests/specs/integration/BaseEntity/Relationships/BelongsToManySpec.cfc +++ b/tests/specs/integration/BaseEntity/Relationships/BelongsToManySpec.cfc @@ -7,7 +7,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { } ); it( "can get the related entities", function() { - var post = getInstance( "Post" ).find( 1 ); + var post = getInstance( "Post" ).find( 1245 ); var tags = post.getTags(); expect( tags ).toBeArray(); expect( tags ).toHaveLength( 2 ); diff --git a/tests/specs/integration/BaseEntity/Relationships/BelongsToSpec.cfc b/tests/specs/integration/BaseEntity/Relationships/BelongsToSpec.cfc index 1ec420cc..a736417f 100644 --- a/tests/specs/integration/BaseEntity/Relationships/BelongsToSpec.cfc +++ b/tests/specs/integration/BaseEntity/Relationships/BelongsToSpec.cfc @@ -7,7 +7,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { } ); it( "can get the owning entity", function() { - var post = getInstance( "Post" ).find( 1 ); + var post = getInstance( "Post" ).find( 1245 ); var user = post.getAuthor(); expect( user.getId() ).toBe( 1 ); expect( user.getUsername() ).toBe( "elpete" ); @@ -15,7 +15,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { it( "caches the result of fetching the owning entity", function() { controller.getInterceptorService().registerInterceptor( interceptorObject = this ); - var post = getInstance( "Post" ).find( 1 ); + var post = getInstance( "Post" ).find( 1245 ); post.getAuthor(); post.getAuthor(); post.getAuthor(); @@ -33,11 +33,11 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { } ); it( "can disassociate the existing entity", function() { - var post = getInstance( "Post" ).find( 1 ); + var post = getInstance( "Post" ).find( 1245 ); expect( post.retrieveAttribute( "user_id" ) ).notToBe( "" ); var userId = post.retrieveAttribute( "user_id" ); expect( getInstance( "User" ).find( userId ).posts().count() ).toBe( 2 ); - post.author().disassociate().save(); + post.author().dissociate().save(); expect( post.retrieveAttribute( "user_id" ) ).toBe( "" ); expect( getInstance( "User" ).find( userId ).posts().count() ).toBe( 1 ); } ); diff --git a/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc b/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc index 79fef070..93195900 100644 --- a/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc +++ b/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc @@ -25,7 +25,6 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { expect( authors[ 2 ] ).notToBeArray(); expect( authors[ 2 ] ).toBeInstanceOf( "app.models.User" ); if ( arrayLen( variables.queries ) != 2 ) { - debug( variables.queries ); expect( variables.queries ).toHaveLength( 2, "Only two queries should have been executed." ); } } ); @@ -52,6 +51,129 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { expect( variables.queries ).toHaveLength( 2, "Only two queries should have been executed." ); } ); + + it( "can eager load a hasOne relationship", function() { + var users = getInstance( "User" ).with( "latestPost" ).latest().get(); + expect( users ).toBeArray(); + expect( users ).toHaveLength( 3, "Three users should be returned" ); + + var janedoe = users[ 1 ]; + expect( janedoe.getUsername() ).toBe( "janedoe" ); + expect( janedoe.getLatestPost() ).toBeStruct(); + expect( janedoe.getLatestPost() ).toBeEmpty(); + + var johndoe = users[ 2 ]; + expect( johndoe.getUsername() ).toBe( "johndoe" ); + expect( johndoe.getLatestPost() ).toBeStruct(); + expect( johndoe.getLatestPost() ).toBeEmpty(); + + var elpete = users[ 3 ]; + expect( elpete.getUsername() ).toBe( "elpete" ); + expect( elpete.getLatestPost() ).toBeStruct(); + expect( elpete.getLatestPost() ).notToBeEmpty(); + + expect( variables.queries ).toHaveLength( 2, "Only two queries should have been executed." ); + } ); + + it( "can eager load a belongs to many relationship", function() { + var posts = getInstance( "Post" ).with( "tags" ).get(); + expect( posts ).toBeArray(); + expect( posts ).toHaveLength( 2 ); + + expect( posts[ 1 ].getTags() ).toBeArray(); + expect( posts[ 1 ].getTags() ).toHaveLength( 2 ); + + expect( posts[ 2 ].getTags() ).toBeArray(); + expect( posts[ 2 ].getTags() ).toHaveLength( 2 ); + + expect( variables.queries ).toHaveLength( 2, "Only two queries should have been executed." ); + } ); + + it( "can eager load a has many through relationship", function() { + var countries = getInstance( "Country" ).with( "posts" ).get(); + expect( countries ).toBeArray(); + expect( countries ).toHaveLength( 2 ); + + expect( countries[ 1 ].getPosts() ).toBeArray(); + expect( countries[ 1 ].getPosts() ).toHaveLength( 2 ); + expect( countries[ 1 ].getPosts()[ 1 ].getBody() ).toBe( "My awesome post body" ); + expect( countries[ 1 ].getPosts()[ 2 ].getBody() ).toBe( "My second awesome post body" ); + + expect( countries[ 2 ].getPosts() ).toBeArray(); + expect( countries[ 2 ].getPosts() ).toBeEmpty(); + + expect( variables.queries ).toHaveLength( 2, "Only two queries should have been executed." ); + } ); + + it( "can eager load polymorphic belongs to relationships", function() { + var comments = getInstance( "Comment" ).with( "commentable" ).get(); + + expect( comments ).toBeArray(); + expect( comments ).toHaveLength( 3 ); + + expect( comments[ 1 ].getId() ).toBe( 1 ); + expect( comments[ 1 ].getCommentable().get_entityName() ).toBe( "Post" ); + expect( comments[ 1 ].getCommentable().getPost_Pk() ).toBe( 1245 ); + + expect( comments[ 2 ].getId() ).toBe( 2 ); + expect( comments[ 2 ].getCommentable().get_entityName() ).toBe( "Post" ); + expect( comments[ 2 ].getCommentable().getPost_Pk() ).toBe( 1245 ); + + expect( comments[ 3 ].getId() ).toBe( 3 ); + expect( comments[ 3 ].getCommentable().get_entityName() ).toBe( "Video" ); + expect( comments[ 3 ].getCommentable().getId() ).toBe( 2 ); + + expect( variables.queries ).toHaveLength( 3, "Only three queries should have been executed." ); + } ); + + it( "can eager load polymorphic has many relationships", function() { + var posts = getInstance( "Post" ).with( "comments" ).get(); + + expect( posts ).toBeArray(); + expect( posts ).toHaveLength( 2 ); + + expect( posts[ 1 ].getComments() ).toBeArray(); + expect( posts[ 1 ].getComments() ).toHaveLength( 2 ); + + expect( posts[ 2 ].getComments() ).toBeArray(); + expect( posts[ 2 ].getComments() ).toBeEmpty(); + + expect( variables.queries ).toHaveLength( 2, "Only two queries should have been executed." ); + } ); + + it( "can eager load a large relationship quickly", function() { + queryExecute( " + CREATE TABLE `a` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(50) NOT NULL, + PRIMARY KEY (`id`) + ) + " ); + queryExecute( " + CREATE TABLE `b` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `a_id` int(11), + `name` varchar(50) NOT NULL, + PRIMARY KEY (`id`) + ) + " ); + for ( var i = 1; i < 20; i++ ) { + // create A + var a = getInstance( "A" ).create( { + "name" = "Instance #i#" + } ); + for ( var j = 1; j < 5; j++ ) { + getInstance( "B" ).create( { + "name" = "Instance #j#", + "a_id" = a.getId() + } ); + } + } + + var startTick = getTickCount(); + var a = getInstance( "B" ).with( "a" ).get(); + expect( getTickCount() - startTick ).toBeLT( 1000, "Query is taking too long" ); + } ); } ); } diff --git a/tests/specs/integration/BaseEntity/Relationships/HasManySpec.cfc b/tests/specs/integration/BaseEntity/Relationships/HasManySpec.cfc index 612f690f..f6d2e4ee 100644 --- a/tests/specs/integration/BaseEntity/Relationships/HasManySpec.cfc +++ b/tests/specs/integration/BaseEntity/Relationships/HasManySpec.cfc @@ -28,7 +28,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { expect( newPost.isLoaded() ).toBeTrue(); expect( newPost.retrieveAttribute( "user_id" ) ).toBe( user.getId() ); expect( newPost.getBody() ).toBe( "A new post created directly here!" ); - expect( user.getPosts() ).toHaveLength( 3 ); + expect( user.fresh().getPosts() ).toHaveLength( 3 ); } ); } ); } diff --git a/tests/specs/integration/BaseEntity/Relationships/HasOneSpec.cfc b/tests/specs/integration/BaseEntity/Relationships/HasOneSpec.cfc index 28f33cae..735e25f7 100644 --- a/tests/specs/integration/BaseEntity/Relationships/HasOneSpec.cfc +++ b/tests/specs/integration/BaseEntity/Relationships/HasOneSpec.cfc @@ -5,7 +5,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { it( "can get the owning entity", function() { var user = getInstance( "User" ).find( 1 ); var post = user.getLatestPost(); - expect( post.getPost_Pk() ).toBe( 2 ); + expect( post.getPost_Pk() ).toBe( 523526 ); } ); } ); } diff --git a/tests/specs/integration/BaseEntity/Relationships/PolymorphicBelongsToSpec.cfc b/tests/specs/integration/BaseEntity/Relationships/PolymorphicBelongsToSpec.cfc index 958fe765..b0fb9083 100644 --- a/tests/specs/integration/BaseEntity/Relationships/PolymorphicBelongsToSpec.cfc +++ b/tests/specs/integration/BaseEntity/Relationships/PolymorphicBelongsToSpec.cfc @@ -18,4 +18,4 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { } ); } -} \ No newline at end of file +} diff --git a/tests/specs/integration/BaseEntity/Relationships/PolymorphicHasManySpec.cfc b/tests/specs/integration/BaseEntity/Relationships/PolymorphicHasManySpec.cfc index 3eceacac..1c26d766 100644 --- a/tests/specs/integration/BaseEntity/Relationships/PolymorphicHasManySpec.cfc +++ b/tests/specs/integration/BaseEntity/Relationships/PolymorphicHasManySpec.cfc @@ -3,11 +3,11 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { function run() { describe( "Polymorphic Has Many Spec", function() { it( "can get the related polymorphic entities", function() { - var postA = getInstance( "Post" ).find( 1 ); + var postA = getInstance( "Post" ).find( 1245 ); var postAComments = postA.getComments(); expect( arrayLen( postAComments ) ).toBe( 2 ); - var postB = getInstance( "Post" ).find( 2 ); + var postB = getInstance( "Post" ).find( 523526 ); var postBComments = postB.getComments(); expect( arrayLen( postBComments ) ).toBe( 0 ); diff --git a/tests/specs/integration/BaseEntity/SaveSpec.cfc b/tests/specs/integration/BaseEntity/SaveSpec.cfc index 66e1dda0..f1474f88 100644 --- a/tests/specs/integration/BaseEntity/SaveSpec.cfc +++ b/tests/specs/integration/BaseEntity/SaveSpec.cfc @@ -82,15 +82,17 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { tag.setName( "miscellaneous" ); tag.save(); - var post = getInstance( "Post" ).find( 1 ); + var post = getInstance( "Post" ).find( 1245 ); expect( post.getTags().toArray() ).toBeArray(); expect( post.getTags().toArray() ).toHaveLength( 2 ); post.tags().attach( tag.getId() ); - expect( post.getTags().toArray() ).toBeArray(); - expect( post.getTags().toArray() ).toHaveLength( 3 ); + post.refresh(); + + expect( post.getTags() ).toBeArray(); + expect( post.getTags() ).toHaveLength( 3 ); } ); it( "attaches using the id if the entity is passed", function() { @@ -98,13 +100,15 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { tag.setName( "miscellaneous" ); tag.save(); - var post = getInstance( "Post" ).find( 1 ); + var post = getInstance( "Post" ).find( 1245 ); expect( post.getTags().toArray() ).toBeArray(); expect( post.getTags().toArray() ).toHaveLength( 2 ); post.tags().attach( tag ); + post.refresh(); + expect( post.getTags().toArray() ).toBeArray(); expect( post.getTags().toArray() ).toHaveLength( 3 ); } ); @@ -118,13 +122,15 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { tagB.setName( "other" ); tagB.save(); - var post = getInstance( "Post" ).find( 1 ); + var post = getInstance( "Post" ).find( 1245 ); expect( post.getTags().toArray() ).toBeArray(); expect( post.getTags().toArray() ).toHaveLength( 2 ); post.tags().attach( [ tagA.getId(), tagB ] ); + post.refresh(); + expect( post.getTags().toArray() ).toBeArray(); expect( post.getTags().toArray() ).toHaveLength( 4 ); } ); @@ -132,7 +138,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { describe( "detach", function() { it( "can detach an id from a relationship", function() { - var post = getInstance( "Post" ).find( 1 ); + var post = getInstance( "Post" ).find( 1245 ); expect( post.getTags().toArray() ).toBeArray(); expect( post.getTags().toArray() ).toHaveLength( 2 ); @@ -141,12 +147,14 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { post.tags().detach( tag.getId() ); + post.refresh(); + expect( post.getTags().toArray() ).toBeArray(); expect( post.getTags().toArray() ).toHaveLength( 1 ); } ); it( "detaches using the id if the entity is passed", function() { - var post = getInstance( "Post" ).find( 1 ); + var post = getInstance( "Post" ).find( 1245 ); expect( post.getTags().toArray() ).toBeArray(); expect( post.getTags().toArray() ).toHaveLength( 2 ); @@ -155,12 +163,14 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { post.tags().detach( tag ); + post.refresh(); + expect( post.getTags().toArray() ).toBeArray(); expect( post.getTags().toArray() ).toHaveLength( 1 ); } ); it( "can detach multiple ids or entities at once", function() { - var post = getInstance( "Post" ).find( 1 ); + var post = getInstance( "Post" ).find( 1245 ); var tags = post.getTags().toArray(); expect( tags ).toBeArray(); @@ -168,6 +178,8 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { post.tags().detach( [ tags[ 1 ].getId(), tags[ 2 ] ] ); + post.refresh(); + expect( post.getTags().toArray() ).toBeArray(); expect( post.getTags().toArray() ).toBeEmpty(); } ); @@ -183,7 +195,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { newTagB.setName( "other" ); newTagB.save(); - var post = getInstance( "Post" ).find( 1 ); + var post = getInstance( "Post" ).find( 1245 ); expect( post.getTags().toArray() ).toBeArray(); expect( post.getTags().toArray() ).toHaveLength( 2 ); @@ -198,6 +210,8 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { post.tags().sync( [ existingTags[ 1 ], newTagA.getId(), newTagB ] ); + post.refresh(); + expect( post.getTags().toArray() ).toBeArray(); expect( post.getTags().toArray() ).toHaveLength( 3 ); expect( post.getTags().map( function( tag ) { return tag.keyValue(); } ).toArray() ).toBe( tagIds ); diff --git a/tests/specs/integration/BaseEntity/UpdateAllSpec.cfc b/tests/specs/integration/BaseEntity/UpdateAllSpec.cfc index d8e3f88e..bc94b830 100644 --- a/tests/specs/integration/BaseEntity/UpdateAllSpec.cfc +++ b/tests/specs/integration/BaseEntity/UpdateAllSpec.cfc @@ -3,8 +3,8 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { function run() { describe( "Mass Create Spec", function() { it( "can mass update all entities that fit the query criteria", function() { - var postA = getInstance( "Post" ).find( 1 ); - var postB = getInstance( "Post" ).find( 2 ); + var postA = getInstance( "Post" ).find( 1245 ); + var postB = getInstance( "Post" ).find( 523526 ); expect( postA.getBody() ).notToBe( "The new body" ); expect( postB.getBody() ).notToBe( "The new body" ); From d204f88c9d87b0806f0766f11a0531f40a277575 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Mon, 17 Sep 2018 16:24:05 -0600 Subject: [PATCH 09/70] feat(BaseEntity): Add support for inherited entities Quick will now recursively search the component tree for properties allowing for inheritied entities. Thanks to @jclausen for this fix. --- models/BaseEntity.cfc | 49 ++++++++++--------- .../Relationships/EagerLoadingSpec.cfc | 2 +- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 2a24751c..b281c18e 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -3,36 +3,36 @@ component accessors="true" { /*==================================== = Dependencies = ====================================*/ - property name="_builder" inject="QuickQB@quick"; - property name="_wirebox" inject="wirebox"; - property name="_str" inject="Str@str"; - property name="_settings" inject="coldbox:modulesettings:quick"; - property name="_validationManager" inject="ValidationManager@cbvalidation"; - property name="_interceptorService" inject="coldbox:interceptorService"; - property name="_keyType" inject="AutoIncrementing@quick"; + property name="_builder" inject="QuickQB@quick" persistent="false"; + property name="_wirebox" inject="wirebox" persistent="false"; + property name="_str" inject="Str@str" persistent="false"; + property name="_settings" inject="coldbox:modulesettings:quick" persistent="false"; + property name="_validationManager" inject="ValidationManager@cbvalidation" persistent="false"; + property name="_interceptorService" inject="coldbox:interceptorService" persistent="false"; + property name="_keyType" inject="AutoIncrementing@quick" persistent="false"; /*=========================================== = Metadata Properties = ===========================================*/ - property name="_entityName"; - property name="_mapping"; - property name="_fullName"; - property name="_table"; - property name="_queryOptions"; - property name="_readonly" default="false"; - property name="_key" default="id"; - property name="_attributes"; - property name="_meta"; - property name="_nullValues"; + property name="_entityName" persistent="false"; + property name="_mapping" persistent="false"; + property name="_fullName" persistent="false"; + property name="_table" persistent="false"; + property name="_queryOptions" persistent="false"; + property name="_readonly" default="false" persistent="false"; + property name="_key" default="id" persistent="false"; + property name="_attributes" persistent="false"; + property name="_meta" persistent="false"; + property name="_nullValues" persistent="false"; /*===================================== = Instance Data = =====================================*/ - property name="_data"; - property name="_originalAttributes"; - property name="_relationshipsData"; - property name="_eagerLoad"; - property name="_loaded"; + property name="_data" persistent="false"; + property name="_originalAttributes" persistent="false"; + property name="_relationshipsData" persistent="false"; + property name="_eagerLoad" persistent="false"; + property name="_loaded" persistent="false"; this.constraints = {}; @@ -808,7 +808,8 @@ component accessors="true" { } private function metadataInspection() { - var md = getMetadata( this ); + var util = createObject( "component", "coldbox.system.core.util.Util" ); + var md = util.getInheritedMetadata( this ); variables._meta = md; param variables._key = "id"; variables._fullName = md.fullname; @@ -837,7 +838,7 @@ component accessors="true" { if ( prop.convertToNull ) { variables._nullValues[ prop.name ] = prop.nullValue; } - if ( prop.persistent ) { + if ( javacast( "boolean", prop.persistent ) ) { acc[ prop.name ] = prop.column; } return acc; diff --git a/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc b/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc index 93195900..e5103164 100644 --- a/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc +++ b/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc @@ -172,7 +172,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { var startTick = getTickCount(); var a = getInstance( "B" ).with( "a" ).get(); - expect( getTickCount() - startTick ).toBeLT( 1000, "Query is taking too long" ); + expect( getTickCount() - startTick ).toBeLT( 5000, "Query is taking too long" ); } ); } ); } From 7107ebf77ef9bf278accaaa3511619ead0af61e2 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Mon, 17 Sep 2018 20:10:50 -0600 Subject: [PATCH 10/70] v2.0.0-beta.1 --- box.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/box.json b/box.json index 27ca3b19..419c5e43 100644 --- a/box.json +++ b/box.json @@ -1,8 +1,8 @@ { "name":"quick", - "version":"1.3.0", + "version":"2.0.0-beta.1", "author":"", - "location":"coldbox-modules/quick#v1.3.0", + "location":"coldbox-modules/quick#v2.0.0-beta.1", "homepage":"https://github.com/coldbox-modules/quick", "documentation":"https://github.com/coldbox-modules/quick", "repository":{ @@ -50,4 +50,4 @@ ".gitignore", "server.json" ] -} \ No newline at end of file +} From 9d0725392bd6887ee831e882598901dec3187b67 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Mon, 12 Nov 2018 06:59:40 -0700 Subject: [PATCH 11/70] chore(QuickCollection): Move QuickCollection to extras This change allows us to keep auto-mapping the models folder without breaking since CFCollection is no longer a dependency. BREAKING CHANGE: `QuickCollection@quick` is no longer automatically mapped. It must be mapped manually if desired. --- {models => extras}/QuickCollection.cfc | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {models => extras}/QuickCollection.cfc (100%) diff --git a/models/QuickCollection.cfc b/extras/QuickCollection.cfc similarity index 100% rename from models/QuickCollection.cfc rename to extras/QuickCollection.cfc From 08d671939c40d0d135dbec82aa8069b1da427e17 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Mon, 12 Nov 2018 07:11:58 -0700 Subject: [PATCH 12/70] feat(ModuleConfig): Set default grammar to AutoDiscover This lets a user get up and runnning with Quick faster as it skips a configuration step. --- ModuleConfig.cfc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ModuleConfig.cfc b/ModuleConfig.cfc index 6d29f9b3..c7c8cc71 100644 --- a/ModuleConfig.cfc +++ b/ModuleConfig.cfc @@ -5,10 +5,11 @@ component { this.webUrl = "https://github.com/coldbox-modules/quick"; this.dependencies = [ "qb", "str" ]; this.cfmapping = "quick"; + this.autoMapModels = false; function configure() { settings = { - defaultGrammar = "BaseGrammar", + defaultGrammar = "AutoDiscover", automaticValidation = true }; From 8bbb679711fa31623bede8be41952200c637310b Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Mon, 12 Nov 2018 11:20:55 -0700 Subject: [PATCH 13/70] perf(Metadata): Allow passing in metadata to prevent introspection. The primary use case is when we are creating a `newEntity`. We have already done the work to generate the metadata and we don't need to do so again. Instead, we can pass the metadata to the constructor and skip that step. --- models/BaseEntity.cfc | 53 +++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index b281c18e..8765ca20 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -3,9 +3,9 @@ component accessors="true" { /*==================================== = Dependencies = ====================================*/ - property name="_builder" inject="QuickQB@quick" persistent="false"; + property name="_builder" inject="provider:QuickQB@quick" persistent="false"; property name="_wirebox" inject="wirebox" persistent="false"; - property name="_str" inject="Str@str" persistent="false"; + property name="_str" inject="provider:Str@str" persistent="false"; property name="_settings" inject="coldbox:modulesettings:quick" persistent="false"; property name="_validationManager" inject="ValidationManager@cbvalidation" persistent="false"; property name="_interceptorService" inject="coldbox:interceptorService" persistent="false"; @@ -38,14 +38,16 @@ component accessors="true" { variables.relationships = {}; - function init() { + function init( struct meta = {} ) { assignDefaultProperties(); + variables._meta = arguments.meta; return this; } function assignDefaultProperties() { assignAttributesData( {} ); assignOriginalAttributes( {} ); + variables._meta = {}; variables._relationshipsData = {}; variables._eagerLoad = []; variables._nullValues = {}; @@ -283,7 +285,10 @@ component accessors="true" { } function newEntity() { - return variables._wirebox.getInstance( variables._fullName ); + return variables._wirebox.getInstance( + name = variables._fullName, + initArguments = { meta = variables._meta } + ); } function fresh() { @@ -808,25 +813,26 @@ component accessors="true" { } private function metadataInspection() { - var util = createObject( "component", "coldbox.system.core.util.Util" ); - var md = util.getInheritedMetadata( this ); - variables._meta = md; + if ( ! isStruct( variables._meta ) || structIsEmpty( variables._meta ) ) { + var util = createObject( "component", "coldbox.system.core.util.Util" ); + variables._meta = util.getInheritedMetadata( this ); + } param variables._key = "id"; - variables._fullName = md.fullname; - param md.mapping = listLast( md.fullname, "." ); - variables._mapping = md.mapping; - param md.entityName = listLast( md.name, "." ); - variables._entityName = md.entityName; - param md.table = variables._str.plural( variables._str.snake( variables._entityName ) ); - variables._table = md.table; + variables._fullName = variables._meta.fullname; + param variables._meta.mapping = listLast( variables._meta.fullname, "." ); + variables._mapping = variables._meta.mapping; + param variables._meta.entityName = listLast( variables._meta.name, "." ); + variables._entityName = variables._meta.entityName; + param variables._meta.table = variables._str.plural( variables._str.snake( variables._entityName ) ); + variables._table = variables._meta.table; param variables._queryOptions = {}; - if ( md.keyExists( "datasource" ) ) { - variables._queryOptions = { datasource = md.datasource }; + if ( variables._meta.keyExists( "datasource" ) ) { + variables._queryOptions = { datasource = variables._meta.datasource }; } - param md.readonly = false; - variables._readonly = md.readonly; - param md.properties = []; - assignAttributesFromProperties( md.properties ); + param variables._meta.readonly = false; + variables._readonly = variables._meta.readonly; + param variables._meta.properties = []; + assignAttributesFromProperties( variables._meta.properties ); } private function assignAttributesFromProperties( properties ) { @@ -1111,4 +1117,11 @@ component accessors="true" { compare( variables._nullValues[ retrieveAliasForColumn( key ) ], value ) == 0; } + function timeIt( callback, label ) { + var start = getTickCount(); + var result = callback(); + writeDump( var = getTickCount() - start, label = label ); + return isNull( result ) ? javacast( "null", "" ) : result; + } + } From 249f6e2ce1d61774e82816fa8505014a1f1a47b3 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 13 Nov 2018 16:04:58 -0700 Subject: [PATCH 14/70] fix(ModuleConfig): Continue to map models folder --- ModuleConfig.cfc | 1 - 1 file changed, 1 deletion(-) diff --git a/ModuleConfig.cfc b/ModuleConfig.cfc index c7c8cc71..1b548a00 100644 --- a/ModuleConfig.cfc +++ b/ModuleConfig.cfc @@ -5,7 +5,6 @@ component { this.webUrl = "https://github.com/coldbox-modules/quick"; this.dependencies = [ "qb", "str" ]; this.cfmapping = "quick"; - this.autoMapModels = false; function configure() { settings = { From 920a006ac82a91004d579cef6d3336faa5c0d828 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 27 Nov 2018 13:48:16 -0700 Subject: [PATCH 15/70] Fix Quick Collection spec --- tests/specs/integration/QuickCollectionSpec.cfc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/specs/integration/QuickCollectionSpec.cfc b/tests/specs/integration/QuickCollectionSpec.cfc index 0cf08604..409f8469 100644 --- a/tests/specs/integration/QuickCollectionSpec.cfc +++ b/tests/specs/integration/QuickCollectionSpec.cfc @@ -17,7 +17,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { expectAll( posts ).toSatisfy( function( post ) { return ! post.isRelationshipLoaded( "author" ); }, "The relationship should not be loaded." ); - posts = getInstance( name = "QuickCollection@quick", initArguments = { collection = posts } ); + posts = getInstance( name = "extras.QuickCollection", initArguments = { collection = posts } ); posts.load( "author" ); expect( variables.queries ).toHaveLength( 2 ); expectAll( posts.get() ).toSatisfy( function( post ) { From c11c6639df10dde6b3d2405bd1235e6990f3da13 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 27 Nov 2018 20:48:03 -0700 Subject: [PATCH 16/70] feat(Scopes): Enable named and default arguments in scopes Extra arguments passed to scope are passed in order to the scope defined on the entity. Default arguments can also be set now for individual scope arguments making it more close to a normal function signature. BREAKING CHANGE: Extra arguments to a scope function used to be passed as a single struct called `args`. They are now passed individually in the order they were passed to the scope. --- models/BaseEntity.cfc | 12 ++++++++---- tests/Application.cfc | 3 ++- tests/resources/app/models/User.cfc | 5 +++++ .../specs/integration/BaseEntity/AttributeSpec.cfc | 6 ++++-- tests/specs/integration/BaseEntity/ColumnsSpec.cfc | 3 ++- tests/specs/integration/BaseEntity/ScopeSpec.cfc | 13 +++++++++++++ 6 files changed, 34 insertions(+), 8 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 8765ca20..75c5093f 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -776,10 +776,14 @@ component accessors="true" { private function tryScopes( missingMethodName, missingMethodArguments ) { if ( structKeyExists( variables, "scope#missingMethodName#" ) ) { - return invoke( this, "scope#missingMethodName#", { - query = this, - args = missingMethodArguments - } ); + var scopeArgs = { "1" = this }; + // this is to allow default arguments to be set for scopes + if ( ! structIsEmpty( missingMethodArguments ) ) { + for ( var i = 1; i <= structCount( missingMethodArguments ); i++ ) { + scopeArgs[ i + 1 ] = missingMethodArguments[ i ]; + } + } + return invoke( this, "scope#missingMethodName#", scopeArgs ); } return; } diff --git a/tests/Application.cfc b/tests/Application.cfc index 967a3fd8..1646598d 100644 --- a/tests/Application.cfc +++ b/tests/Application.cfc @@ -53,11 +53,12 @@ component { `country_id` char(35), `created_date` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, `modified_date` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, + `type` varchar(50) NOT NULL DEFAULT 'limited', PRIMARY KEY (`id`) ) " ); queryExecute( " - INSERT INTO `users` (`id`, `username`, `first_name`, `last_name`, `password`, `country_id`, `created_date`, `modified_date`) VALUES (1, 'elpete', 'Eric', 'Peterson', '5F4DCC3B5AA765D61D8327DEB882CF99', '02B84D66-0AA0-F7FB-1F71AFC954843861', '2017-07-28 02:06:36', '2017-07-28 02:06:36') + INSERT INTO `users` (`id`, `username`, `first_name`, `last_name`, `password`, `country_id`, `created_date`, `modified_date`, `type`) VALUES (1, 'elpete', 'Eric', 'Peterson', '5F4DCC3B5AA765D61D8327DEB882CF99', '02B84D66-0AA0-F7FB-1F71AFC954843861', '2017-07-28 02:06:36', '2017-07-28 02:06:36', 'admin') " ); queryExecute( " INSERT INTO `users` (`id`, `username`, `first_name`, `last_name`, `password`, `country_id`, `created_date`, `modified_date`) VALUES (2, 'johndoe', 'John', 'Doe', '5F4DCC3B5AA765D61D8327DEB882CF99', '02B84D66-0AA0-F7FB-1F71AFC954843861', '2017-07-28 02:07:16', '2017-07-28 02:07:16'); diff --git a/tests/resources/app/models/User.cfc b/tests/resources/app/models/User.cfc index 9d85d09d..9d1748d3 100644 --- a/tests/resources/app/models/User.cfc +++ b/tests/resources/app/models/User.cfc @@ -8,6 +8,7 @@ component extends="quick.models.BaseEntity" accessors="true" { property name="countryId" column="country_id"; property name="createdDate" column="created_date"; property name="modifiedDate" column="modified_date"; + property name="type"; this.constraints = { "lastName" = { @@ -19,6 +20,10 @@ component extends="quick.models.BaseEntity" accessors="true" { return query.orderBy( "created_date", "desc" ); } + function scopeOfType( query, type = "limited" ) { + return query.where( "type", type ); + } + function posts() { return hasMany( "Post", "user_id" ); } diff --git a/tests/specs/integration/BaseEntity/AttributeSpec.cfc b/tests/specs/integration/BaseEntity/AttributeSpec.cfc index 8bfad6cf..b2bdcb6d 100644 --- a/tests/specs/integration/BaseEntity/AttributeSpec.cfc +++ b/tests/specs/integration/BaseEntity/AttributeSpec.cfc @@ -79,7 +79,8 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { "password" = "", "countryId" = "", "createdDate" = "", - "modifiedDate" = "" + "modifiedDate" = "", + "type" = "" } ); } ); @@ -92,7 +93,8 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { "password" = "5F4DCC3B5AA765D61D8327DEB882CF99", "countryId" = "02B84D66-0AA0-F7FB-1F71AFC954843861", "createdDate" = "2017-07-28 02:06:36", - "modifiedDate" = "2017-07-28 02:06:36" + "modifiedDate" = "2017-07-28 02:06:36", + "type" = "admin" } ); } ); } ); diff --git a/tests/specs/integration/BaseEntity/ColumnsSpec.cfc b/tests/specs/integration/BaseEntity/ColumnsSpec.cfc index 6230c854..0d281609 100644 --- a/tests/specs/integration/BaseEntity/ColumnsSpec.cfc +++ b/tests/specs/integration/BaseEntity/ColumnsSpec.cfc @@ -8,7 +8,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { arraySort( attributeNames, "textnocase" ); expect( attributeNames ).toBeArray(); - expect( attributeNames ).toHaveLength( 8 ); + expect( attributeNames ).toHaveLength( 9 ); expect( attributeNames ).toBe( [ "COUNTRY_ID", "CREATED_DATE", @@ -17,6 +17,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { "LAST_NAME", "MODIFIED_DATE", "PASSWORD", + "TYPE", "USERNAME" ] ); } ); diff --git a/tests/specs/integration/BaseEntity/ScopeSpec.cfc b/tests/specs/integration/BaseEntity/ScopeSpec.cfc index dc257854..aa0e496f 100644 --- a/tests/specs/integration/BaseEntity/ScopeSpec.cfc +++ b/tests/specs/integration/BaseEntity/ScopeSpec.cfc @@ -9,6 +9,19 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { expect( users[ 2 ].getUsername() ).toBe( "johndoe" ); expect( users[ 3 ].getUsername() ).toBe( "elpete" ); } ); + + it( "sends through extra parameters as arguments", function() { + var users = getInstance( "User" ).ofType( "admin" ).get(); + expect( users ).toHaveLength( 1, "One user should exist in the database and be returned." ); + expect( users[ 1 ].getUsername() ).toBe( "elpete" ); + } ); + + it( "allows for default arguments if none are passed in", function() { + var users = getInstance( "User" ).ofType().get(); + expect( users ).toHaveLength( 2, "Two users should exist in the database and be returned." ); + expect( users[ 1 ].getUsername() ).toBe( "johndoe" ); + expect( users[ 2 ].getUsername() ).toBe( "janedoe" ); + } ); } ); } From 6b74cd5cc307eb87e6bc917b43fc0973176c2d3a Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 27 Nov 2018 22:24:04 -0700 Subject: [PATCH 17/70] feat(KeyType): Refactor keyTypes for better reusability KeyTypes are now returned from a `keyType` function allowing for composed keyType classes. BREAKING CHANGE: Key Types must now be returned from a `keyType()` function instead of a property. --- box.json | 2 +- models/BaseEntity.cfc | 18 +++++++++++++++--- models/KeyTypes/AssignedKey.cfc | 4 ++-- models/KeyTypes/AutoIncrementing.cfc | 10 +++++++--- models/KeyTypes/KeyType.cfc | 4 ++-- models/KeyTypes/SequentialId.cfc | 19 +++++++++++++++++++ models/KeyTypes/UUID.cfc | 4 ++-- tests/resources/app/models/Country.cfc | 6 ++++-- 8 files changed, 52 insertions(+), 15 deletions(-) create mode 100644 models/KeyTypes/SequentialId.cfc diff --git a/box.json b/box.json index 419c5e43..90046087 100644 --- a/box.json +++ b/box.json @@ -18,7 +18,7 @@ }, "type":"modules", "dependencies":{ - "qb":"^5.2.1", + "qb":"^6.0.0", "str":"^1.0.0", "cbvalidation":"^1.3.1+51" }, diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 75c5093f..90350398 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -58,6 +58,17 @@ component accessors="true" { metadataInspection(); } + function keyType() { + return variables._wirebox.getInstance( "AutoIncrementing@quick" ); + } + + function retrieveKeyType() { + if ( isNull( variables.__keyType__ ) ) { + variables.__keyType__ = keyType(); + } + return variables.__keyType__; + } + /*================================== = Attributes = ==================================*/ @@ -331,10 +342,11 @@ component accessors="true" { fireEvent( "postUpdate", { entity = this } ); } else { - variables._keyType.preInsert( this ); + resetQuery(); + retrieveKeyType().preInsert( this ); fireEvent( "preInsert", { entity = this } ); guardValid(); - var result = newQuery().insert( retrieveAttributesData().map( function( key, value, attributes ) { + var result = retrieveQuery().insert( retrieveAttributesData().map( function( key, value, attributes ) { if ( isNull( value ) || isNullValue( key, value ) ) { return { value = "", nulls = true, null = true }; } @@ -343,7 +355,7 @@ component accessors="true" { } return value; } ), variables._queryOptions ); - variables._keyType.postInsert( this, result ); + retrieveKeyType().postInsert( this, result ); assignOriginalAttributes( retrieveAttributesData() ); variables._loaded = true; fireEvent( "postInsert", { entity = this } ); diff --git a/models/KeyTypes/AssignedKey.cfc b/models/KeyTypes/AssignedKey.cfc index 49dbb1d9..bdb93568 100644 --- a/models/KeyTypes/AssignedKey.cfc +++ b/models/KeyTypes/AssignedKey.cfc @@ -2,7 +2,7 @@ component implements="KeyType" { /** * Called to handle any tasks before inserting into the database. - * Recieves the entity as the only argument. + * Receives the entity as the only argument. */ public void function preInsert( required entity ) { return; @@ -10,7 +10,7 @@ component implements="KeyType" { /** * Called to handle any tasks after inserting into the database. - * Recieves the entity and the queryExecute result as arguments. + * Receives the entity and the queryExecute result as arguments. */ public void function postInsert( required entity, required struct result ) { return; diff --git a/models/KeyTypes/AutoIncrementing.cfc b/models/KeyTypes/AutoIncrementing.cfc index d2cba46d..cecd22a9 100644 --- a/models/KeyTypes/AutoIncrementing.cfc +++ b/models/KeyTypes/AutoIncrementing.cfc @@ -2,7 +2,7 @@ component implements="KeyType" { /** * Called to handle any tasks before inserting into the database. - * Recieves the entity as the only argument. + * Receives the entity as the only argument. */ public void function preInsert( required entity ) { return; @@ -10,10 +10,14 @@ component implements="KeyType" { /** * Called to handle any tasks after inserting into the database. - * Recieves the entity and the queryExecute result as arguments. + * Receives the entity and the queryExecute result as arguments. */ public void function postInsert( required entity, required struct result ) { - var generatedKey = result.keyExists( "generated_key" ) ? result[ "generated_key" ] : result[ "generatedKey" ]; + var generatedKey = result.keyExists( entity.get_Key() ) ? + result[ entity.get_Key() ] : + result.keyExists( "generated_key" ) ? + result[ "generated_key" ] : + result[ "generatedKey" ]; entity.assignAttribute( entity.get_Key(), generatedKey ); } diff --git a/models/KeyTypes/KeyType.cfc b/models/KeyTypes/KeyType.cfc index 00e59f3a..aafee7cb 100644 --- a/models/KeyTypes/KeyType.cfc +++ b/models/KeyTypes/KeyType.cfc @@ -2,13 +2,13 @@ interface displayname="KeyType" { /** * Called to handle any tasks before inserting into the database. - * Recieves the entity as the only argument. + * Receives the entity as the only argument. */ public void function preInsert( required entity ); /** * Called to handle any tasks after inserting into the database. - * Recieves the entity and the queryExecute result as arguments. + * Receives the entity and the queryExecute result as arguments. */ public void function postInsert( required entity, required struct result ); diff --git a/models/KeyTypes/SequentialId.cfc b/models/KeyTypes/SequentialId.cfc new file mode 100644 index 00000000..50dfc6b6 --- /dev/null +++ b/models/KeyTypes/SequentialId.cfc @@ -0,0 +1,19 @@ +component implements="KeyType" { + + /** + * Called to handle any tasks before inserting into the database. + * Receives the entity as the only argument. + */ + public void function preInsert( required entity ) { + entity.getQuery().returning( entity.get_Key() ); + } + + /** + * Called to handle any tasks after inserting into the database. + * Receives the entity and the queryExecute result as arguments. + */ + public void function postInsert( required entity, required struct result ) { + entity.assignAttribute( entity.get_Key(), result.id ); + } + +} diff --git a/models/KeyTypes/UUID.cfc b/models/KeyTypes/UUID.cfc index 0f9cd7c0..43743138 100644 --- a/models/KeyTypes/UUID.cfc +++ b/models/KeyTypes/UUID.cfc @@ -2,7 +2,7 @@ component implements="KeyType" { /** * Called to handle any tasks before inserting into the database. - * Recieves the entity as the only argument. + * Receives the entity as the only argument. */ public void function preInsert( required entity ) { entity.assignAttribute( entity.get_Key(), createUUID() ); @@ -10,7 +10,7 @@ component implements="KeyType" { /** * Called to handle any tasks after inserting into the database. - * Recieves the entity and the queryExecute result as arguments. + * Receives the entity and the queryExecute result as arguments. */ public void function postInsert( required entity, required struct result ) { return; diff --git a/tests/resources/app/models/Country.cfc b/tests/resources/app/models/Country.cfc index 3ddd216d..f85956a6 100644 --- a/tests/resources/app/models/Country.cfc +++ b/tests/resources/app/models/Country.cfc @@ -1,7 +1,5 @@ component extends="quick.models.BaseEntity" { - property name="_keyType" inject="UUID@quick" persistent="false"; - property name="id"; property name="name"; property name="createdDate" column="created_date"; @@ -13,4 +11,8 @@ component extends="quick.models.BaseEntity" { } }; + function keyType() { + return variables._wirebox.getInstance( "UUID@quick" ); + } + } From 3ce80c8cd9404fe58ac07ea150c36a8b53fcdc27 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 27 Nov 2018 22:26:08 -0700 Subject: [PATCH 18/70] v2.0.0-beta.2 --- box.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/box.json b/box.json index 90046087..dde4c2f6 100644 --- a/box.json +++ b/box.json @@ -1,8 +1,8 @@ { "name":"quick", - "version":"2.0.0-beta.1", + "version":"2.0.0-beta.2", "author":"", - "location":"coldbox-modules/quick#v2.0.0-beta.1", + "location":"coldbox-modules/quick#v2.0.0-beta.2", "homepage":"https://github.com/coldbox-modules/quick", "documentation":"https://github.com/coldbox-modules/quick", "repository":{ From b394f34c7995112e212c4e8bdc86250bd9badc01 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 30 Nov 2018 06:41:05 -0700 Subject: [PATCH 19/70] fix(KeyTypes): Update to the new dual query and result syntax from qb --- models/KeyTypes/AutoIncrementing.cfc | 10 +++++----- models/KeyTypes/SequentialId.cfc | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/models/KeyTypes/AutoIncrementing.cfc b/models/KeyTypes/AutoIncrementing.cfc index cecd22a9..a818c4da 100644 --- a/models/KeyTypes/AutoIncrementing.cfc +++ b/models/KeyTypes/AutoIncrementing.cfc @@ -13,11 +13,11 @@ component implements="KeyType" { * Receives the entity and the queryExecute result as arguments. */ public void function postInsert( required entity, required struct result ) { - var generatedKey = result.keyExists( entity.get_Key() ) ? - result[ entity.get_Key() ] : - result.keyExists( "generated_key" ) ? - result[ "generated_key" ] : - result[ "generatedKey" ]; + var generatedKey = result.result.keyExists( entity.get_Key() ) ? + result.result[ entity.get_Key() ] : + result.result.keyExists( "generated_key" ) ? + result.result[ "generated_key" ] : + result.result[ "generatedKey" ]; entity.assignAttribute( entity.get_Key(), generatedKey ); } diff --git a/models/KeyTypes/SequentialId.cfc b/models/KeyTypes/SequentialId.cfc index 50dfc6b6..c191acea 100644 --- a/models/KeyTypes/SequentialId.cfc +++ b/models/KeyTypes/SequentialId.cfc @@ -13,7 +13,7 @@ component implements="KeyType" { * Receives the entity and the queryExecute result as arguments. */ public void function postInsert( required entity, required struct result ) { - entity.assignAttribute( entity.get_Key(), result.id ); + entity.assignAttribute( entity.get_Key(), result.query.id ); } } From b02e0f761f5807c082c285ecf385c9e01b91855e Mon Sep 17 00:00:00 2001 From: Jon Clausen Date: Fri, 30 Nov 2018 09:35:29 -0500 Subject: [PATCH 20/70] fix(BelongsToMany): Fixes key name usage --- models/Relationships/BelongsToMany.cfc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/Relationships/BelongsToMany.cfc b/models/Relationships/BelongsToMany.cfc index 934b7908..3f8b4587 100644 --- a/models/Relationships/BelongsToMany.cfc +++ b/models/Relationships/BelongsToMany.cfc @@ -139,7 +139,7 @@ component accessors="true" extends="quick.models.Relationships.BaseRelationship" arguments.val = val.keyValue(); } var insertRecord = {}; - insertRecord[ variables.parentKey ] = foreignPivotKeyValue; + insertRecord[ variables.foreignPivotKey ] = foreignPivotKeyValue; insertRecord[ variables.relatedPivotKey ] = arguments.val; return insertRecord; } ); From 1e439828d3eec32ba4454cd07432533493600f0a Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 30 Nov 2018 07:36:08 -0700 Subject: [PATCH 21/70] v2.0.0-beta.3 --- box.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/box.json b/box.json index dde4c2f6..a920f793 100644 --- a/box.json +++ b/box.json @@ -1,6 +1,6 @@ { "name":"quick", - "version":"2.0.0-beta.2", + "version":"2.0.0-beta.3", "author":"", "location":"coldbox-modules/quick#v2.0.0-beta.2", "homepage":"https://github.com/coldbox-modules/quick", From 34af2b00dfd6a4bb7f08277cabaae61b2f54e808 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 11 Dec 2018 13:57:13 -0700 Subject: [PATCH 22/70] perf(BaseEntity): Don't inject an unneeded _keyType anymore --- models/BaseEntity.cfc | 1 - 1 file changed, 1 deletion(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 90350398..e04a6212 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -9,7 +9,6 @@ component accessors="true" { property name="_settings" inject="coldbox:modulesettings:quick" persistent="false"; property name="_validationManager" inject="ValidationManager@cbvalidation" persistent="false"; property name="_interceptorService" inject="coldbox:interceptorService" persistent="false"; - property name="_keyType" inject="AutoIncrementing@quick" persistent="false"; /*=========================================== = Metadata Properties = From 1ed2ae884df30755a2815b9a07593a7fbacfd666 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 11 Dec 2018 13:59:28 -0700 Subject: [PATCH 23/70] fix(BaseEntity): Rename relationships to _relationships This brings it in line with other properties and allows the word relationships to be used as an attribute. BREAKING CHANGE: Entities using the `relationships` key will need to rename to the `_relationships` key. --- models/BaseEntity.cfc | 13 +++++++------ tests/resources/app/models/Country.cfc | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index e04a6212..5e090405 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -35,7 +35,8 @@ component accessors="true" { this.constraints = {}; - variables.relationships = {}; + // This is for an alternative syntax for defining relationships. + variables._relationships = {}; function init( struct meta = {} ) { assignDefaultProperties(); @@ -405,7 +406,7 @@ component accessors="true" { param md.functions = []; return ! arrayIsEmpty( arrayFilter( md.functions, function( func ) { return compareNoCase( func.name, name ) == 0; - } ) ) || variables.relationships.keyExists( name ); + } ) ) || variables._relationships.keyExists( name ); } function isRelationshipLoaded( name ) { @@ -762,8 +763,8 @@ component accessors="true" { if ( ! isRelationshipLoaded( relationshipName ) ) { var relationship = ""; - if ( variables.relationships.keyExists( relationshipName ) ) { - var method = variables.relationships[ relationshipName ]; + if ( variables._relationships.keyExists( relationshipName ) ) { + var method = variables._relationships[ relationshipName ]; relationship = method( missingMethodArguments ); } else { @@ -777,8 +778,8 @@ component accessors="true" { } private function tryRelationshipDefinition( relationshipName ) { - if ( variables.relationships.keyExists( relationshipName ) ) { - var method = variables.relationships[ relationshipName ]; + if ( variables._relationships.keyExists( relationshipName ) ) { + var method = variables._relationships[ relationshipName ]; var relationship = method(); relationship.setRelationMethodName( relationshipName ); return relationship; diff --git a/tests/resources/app/models/Country.cfc b/tests/resources/app/models/Country.cfc index f85956a6..2bdaf167 100644 --- a/tests/resources/app/models/Country.cfc +++ b/tests/resources/app/models/Country.cfc @@ -5,7 +5,7 @@ component extends="quick.models.BaseEntity" { property name="createdDate" column="created_date"; property name="modifiedDate" column="modified_date"; - variables.relationships = { + variables._relationships = { "posts" = function() { return hasManyThrough( "Post", "User", "country_id", "user_id" ); } From 324a350af16e37d5e4cb9d33dcc4d60a2ca2c5ee Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 12 Dec 2018 20:43:26 -0700 Subject: [PATCH 24/70] perf(BaseEntity): Use shallow duplicate where possible Lucee allows for shallow `duplicate` which is insanely fast. ACF doesn't have this capability and it fails to run if it is passed even in an `if` statement. So we split the entity creation off in to separate components so Lucee can be super fast and ACF can stop complaining. --- ModuleConfig.cfc | 3 ++ extras/ACFEntityCreator.cfc | 10 ++++++ extras/LuceeEntityCreator.cfc | 9 ++++++ models/BaseEntity.cfc | 60 ++++++++++++++++------------------- 4 files changed, 50 insertions(+), 32 deletions(-) create mode 100644 extras/ACFEntityCreator.cfc create mode 100644 extras/LuceeEntityCreator.cfc diff --git a/ModuleConfig.cfc b/ModuleConfig.cfc index 1b548a00..72776c2e 100644 --- a/ModuleConfig.cfc +++ b/ModuleConfig.cfc @@ -33,6 +33,9 @@ component { binder.map( "quick.models.BaseEntity" ) .to( "#moduleMapping#.models.BaseEntity" ); + var creatorType = server.keyExists( "lucee" ) ? "LuceeEntityCreator" : "ACFEntityCreator"; + binder.map( "EntityCreator@quick" ) + .to( "#moduleMapping#.extras.#creatorType#" ); } function onLoad() { diff --git a/extras/ACFEntityCreator.cfc b/extras/ACFEntityCreator.cfc new file mode 100644 index 00000000..534c9184 --- /dev/null +++ b/extras/ACFEntityCreator.cfc @@ -0,0 +1,10 @@ +component { + + function new( entity ) { + return entity.get_wirebox().getInstance( + name = entity.get_fullName(), + initArguments = { meta = entity.get_meta() } + ); + } + +} diff --git a/extras/LuceeEntityCreator.cfc b/extras/LuceeEntityCreator.cfc new file mode 100644 index 00000000..8310948f --- /dev/null +++ b/extras/LuceeEntityCreator.cfc @@ -0,0 +1,9 @@ +component { + + function new( entity ) { + // Lucee allows a shallow copy which does not copy the object graph. + // This is perfect for our use cases and cuts loading time down immensely! + return duplicate( this, false ); + } + +} diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 5e090405..cc416cbd 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -6,9 +6,11 @@ component accessors="true" { property name="_builder" inject="provider:QuickQB@quick" persistent="false"; property name="_wirebox" inject="wirebox" persistent="false"; property name="_str" inject="provider:Str@str" persistent="false"; + // TOOD: retrieve and store settings in guardValid property name="_settings" inject="coldbox:modulesettings:quick" persistent="false"; - property name="_validationManager" inject="ValidationManager@cbvalidation" persistent="false"; - property name="_interceptorService" inject="coldbox:interceptorService" persistent="false"; + property name="_validationManager" inject="provider:ValidationManager@cbvalidation" persistent="false"; + property name="_interceptorService" inject="provider:coldbox:interceptorService" persistent="false"; + property name="_entityCreator" inject="provider:EntityCreator@quick" persistent="false"; /*=========================================== = Metadata Properties = @@ -48,6 +50,7 @@ component accessors="true" { assignAttributesData( {} ); assignOriginalAttributes( {} ); variables._meta = {}; + variables._data = {}; variables._relationshipsData = {}; variables._eagerLoad = []; variables._nullValues = {}; @@ -119,18 +122,10 @@ component accessors="true" { return this; } - variables._data = attrs.reduce( function( acc, name, value ) { - var key = name; - if ( isColumnAlias( name ) ) { - key = retrieveColumnForAlias( name ); - } - acc[ key ] = value; - return acc; - }, {} ); - - for ( var key in variables._data ) { - variables[ retrieveAliasForColumn( key ) ] = variables._data[ key ]; - } + attrs.each( function( key, value ) { + variables._data[ retrieveColumnForAlias( key ) ] = value; + variables[ retrieveAliasForColumn( key ) ] = value; + } ); return this; } @@ -182,6 +177,8 @@ component accessors="true" { } function isDirty() { + // TODO: could store hash of incoming attrs and compare hashes. + // that could get rid of `duplicate` in `assignOriginalAttributes` return ! deepEqual( get_OriginalAttributes(), retrieveAttributesData() ); } @@ -213,10 +210,10 @@ component accessors="true" { function getEntities() { return retrieveQuery() .get( options = variables._queryOptions ) - .map( function( attributes ) { + .map( function( attrs ) { return newEntity() - .assignAttributesData( attributes ) - .assignOriginalAttributes( attributes ) + .assignAttributesData( attrs ) + .assignOriginalAttributes( attrs ) .set_Loaded( true ); } ); } @@ -225,10 +222,10 @@ component accessors="true" { return eagerLoadRelations( newQuery().from( variables._table ) .get( options = variables._queryOptions ) - .map( function( attributes ) { + .map( function( attrs ) { return newEntity() - .assignAttributesData( attributes ) - .assignOriginalAttributes( attributes ) + .assignAttributesData( attrs ) + .assignOriginalAttributes( attrs ) .set_Loaded( true ); } ) ); @@ -239,11 +236,11 @@ component accessors="true" { } function first() { - var attributes = retrieveQuery().first( options = variables._queryOptions ); + var attrs = retrieveQuery().first( options = variables._queryOptions ); return newEntity() - .assignAttributesData( attributes ) - .assignOriginalAttributes( attributes ) - .set_Loaded( ! structIsEmpty( attributes ) ); + .assignAttributesData( attrs ) + .assignOriginalAttributes( attrs ) + .set_Loaded( ! structIsEmpty( attrs ) ); } function find( id ) { @@ -282,24 +279,21 @@ component accessors="true" { } function firstOrFail() { - var attributes = retrieveQuery().first( options = variables._queryOptions ); - if ( structIsEmpty( attributes ) ) { + var attrs = retrieveQuery().first( options = variables._queryOptions ); + if ( structIsEmpty( attrs ) ) { throw( type = "EntityNotFound", message = "No [#variables._entityName#] found with constraints [#serializeJSON( retrieveQuery().getBindings() )#]" ); } return newEntity() - .assignAttributesData( attributes ) - .assignOriginalAttributes( attributes ) + .assignAttributesData( attrs ) + .assignOriginalAttributes( attrs ) .set_Loaded( true ); } function newEntity() { - return variables._wirebox.getInstance( - name = variables._fullName, - initArguments = { meta = variables._meta } - ); + return variables._entityCreator.new( this ); } function fresh() { @@ -1022,6 +1016,7 @@ component accessors="true" { return this; } + // TOOD: retrieve and store settings here param variables._settings.automaticValidation = false; if ( ! variables._settings.automaticValidation ) { return this; @@ -1089,6 +1084,7 @@ component accessors="true" { if ( ! md.keyExists( "properties" ) || arrayIsEmpty( md.properties ) ) { return false; } + // TODO: use stored metadata and store as struct of struct var foundProperties = arrayFilter( md.properties, function( prop ) { return prop.name == name; } ); From 854a87efba5add2ce659364cecdc73de131af5a4 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 12 Dec 2018 21:15:56 -0700 Subject: [PATCH 25/70] feat(BaseService): Create a BaseService for virtual entity services Akin to cborm's VirutalEntityService, you can create a BaseService with a specific Quick entity. That entity is reused for all queries on the instantiated service. This makes it perfect for handlers and singletons. This is a basic implementation that forwards calls on the a reset version of the contained entity. There may be edge cases that aren't accounted for. --- ModuleConfig.cfc | 3 + dsl/QuickServiceDSL.cfc | 17 ++++++ models/BaseService.cfc | 21 +++++++ tests/specs/integration/BaseServiceSpec.cfc | 65 +++++++++++++++++++++ 4 files changed, 106 insertions(+) create mode 100644 dsl/QuickServiceDSL.cfc create mode 100644 models/BaseService.cfc create mode 100644 tests/specs/integration/BaseServiceSpec.cfc diff --git a/ModuleConfig.cfc b/ModuleConfig.cfc index 72776c2e..39ce4367 100644 --- a/ModuleConfig.cfc +++ b/ModuleConfig.cfc @@ -33,6 +33,9 @@ component { binder.map( "quick.models.BaseEntity" ) .to( "#moduleMapping#.models.BaseEntity" ); + + binder.getInjector().registerDSL( "quickService", "#moduleMapping#.dsl.QuickServiceDSL" ); + var creatorType = server.keyExists( "lucee" ) ? "LuceeEntityCreator" : "ACFEntityCreator"; binder.map( "EntityCreator@quick" ) .to( "#moduleMapping#.extras.#creatorType#" ); diff --git a/dsl/QuickServiceDSL.cfc b/dsl/QuickServiceDSL.cfc new file mode 100644 index 00000000..f8caafc4 --- /dev/null +++ b/dsl/QuickServiceDSL.cfc @@ -0,0 +1,17 @@ +component { + + function init( required injector ) { + variables.injector = arguments.injector; + return this; + } + + function process( required definition, targetObject ) { + return variables.injector.getInstance( + name = "BaseService@quick", + initArguments = { + entity = variables.injector.getInstance( listRest( definition.dsl, ":" ) ) + } + ); + } + +} diff --git a/models/BaseService.cfc b/models/BaseService.cfc new file mode 100644 index 00000000..bd75f655 --- /dev/null +++ b/models/BaseService.cfc @@ -0,0 +1,21 @@ +component { + + property name="wirebox" inject="wirebox"; + property name="entity"; + + function init( entity ) { + variables.entity = arguments.entity; + return this; + } + + function onDIComplete() { + if ( isSimpleValue( variables.entity ) ) { + variables.entity = wirebox.getInstance( variables.entity ); + } + } + + function onMissingMethod( missingMethodName, missingMethodArguments ) { + return invoke( variables.entity.resetQuery(), missingMethodName, missingMethodArguments ); + } + +} diff --git a/tests/specs/integration/BaseServiceSpec.cfc b/tests/specs/integration/BaseServiceSpec.cfc new file mode 100644 index 00000000..582a7875 --- /dev/null +++ b/tests/specs/integration/BaseServiceSpec.cfc @@ -0,0 +1,65 @@ +component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { + + function run() { + describe( "BaseService Spec", function() { + describe( "instantiation", function() { + it( "can be instantiated with an entity", function() { + var user = getInstance( "User" ); + var service = getWireBox().getInstance( + name = "BaseService@quick", + initArguments = { + entity = user + } + ); + expect( service.get_entityName() ).toBe( "User" ); + } ); + + it( "can be instantiated with a wirebox mapping", function() { + var service = getWireBox().getInstance( + name = "BaseService@quick", + initArguments = { + entity = "User" + } + ); + expect( service.get_entityName() ).toBe( "User" ); + } ); + + it( "can inject a service using the wirebox dsl", function() { + var service = getWireBox().getInstance( + dsl = "quickService:User" + ); + expect( service.get_entityName() ).toBe( "User" ); + } ); + } ); + + describe( "retriving records", function() { + beforeEach( function() { + variables.service = getWireBox().getInstance( dsl = "quickService:User" ); + } ); + + afterEach( function() { + structDelete( variables, "service" ); + } ); + + it( "can find a specific record", function() { + var user = variables.service.find( 1 ); + expect( user.keyValue() ).toBe( 1 ); + } ); + + it( "can find or fail a specific record", function() { + var user = variables.service.findOrFail( 1 ); + expect( user.keyValue() ).toBe( 1 ); + } ); + + it( "can handle any qb methods", function() { + var users = variables.service + .where( "last_name", "Doe" ) + .get(); + expect( users ).toBeArray(); + expect( users ).toHaveLength( 2 ); + } ); + } ); + } ); + } + +} From 9352961082137dc0a95a31746ec895039a357efb Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 12 Dec 2018 21:17:40 -0700 Subject: [PATCH 26/70] v2.0.0-beta.4 --- box.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/box.json b/box.json index a920f793..a7b19c3e 100644 --- a/box.json +++ b/box.json @@ -1,8 +1,8 @@ { "name":"quick", - "version":"2.0.0-beta.3", + "version":"2.0.0-beta.4", "author":"", - "location":"coldbox-modules/quick#v2.0.0-beta.2", + "location":"coldbox-modules/quick#v2.0.0-beta.4", "homepage":"https://github.com/coldbox-modules/quick", "documentation":"https://github.com/coldbox-modules/quick", "repository":{ From 117427774916a93aca929e3e901d15b4a5d5f950 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 14 Dec 2018 07:49:17 -0700 Subject: [PATCH 27/70] fix(LuceeEntityCreator): Copy the entity not the EntityCreator --- extras/LuceeEntityCreator.cfc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extras/LuceeEntityCreator.cfc b/extras/LuceeEntityCreator.cfc index 8310948f..f9ace46d 100644 --- a/extras/LuceeEntityCreator.cfc +++ b/extras/LuceeEntityCreator.cfc @@ -3,7 +3,7 @@ component { function new( entity ) { // Lucee allows a shallow copy which does not copy the object graph. // This is perfect for our use cases and cuts loading time down immensely! - return duplicate( this, false ); + return duplicate( entity, false ); } } From 8dd861a4fbc38625267e93daf0b69e646ff7ee01 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 14 Dec 2018 07:49:51 -0700 Subject: [PATCH 28/70] v2.0.0-beta.5 --- box.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/box.json b/box.json index a7b19c3e..341cb36e 100644 --- a/box.json +++ b/box.json @@ -1,8 +1,8 @@ { "name":"quick", - "version":"2.0.0-beta.4", + "version":"2.0.0-beta.5", "author":"", - "location":"coldbox-modules/quick#v2.0.0-beta.4", + "location":"coldbox-modules/quick#v2.0.0-beta.5", "homepage":"https://github.com/coldbox-modules/quick", "documentation":"https://github.com/coldbox-modules/quick", "repository":{ From a3d377c398c22ef9012570e8e3b9eef7006798b0 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 27 Dec 2018 23:09:44 -0700 Subject: [PATCH 29/70] chore(BaseEntity): Only track metadata for persistent properties --- models/BaseEntity.cfc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index cc416cbd..88f6a62c 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -849,6 +849,9 @@ component accessors="true" { variables._attributes = properties.reduce( function( acc, prop ) { param prop.column = prop.name; param prop.persistent = true; + if ( ! prop.persistent ) { + return acc; + } param prop.nullValue = ""; param prop.convertToNull = true; if ( prop.convertToNull ) { From 7f2a03718b328a4fb107887cb82049f1826660bb Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 27 Dec 2018 23:14:00 -0700 Subject: [PATCH 30/70] fix(LuceeEntityCreator): Correct reset duplicated new entities Since Lucee makes shallow duplicates to create new entities the entity must be reset to avoid relationship or property data being incorrectly cached. --- extras/LuceeEntityCreator.cfc | 4 +++- models/BaseEntity.cfc | 24 +++++++++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/extras/LuceeEntityCreator.cfc b/extras/LuceeEntityCreator.cfc index f9ace46d..ea457a5d 100644 --- a/extras/LuceeEntityCreator.cfc +++ b/extras/LuceeEntityCreator.cfc @@ -3,7 +3,9 @@ component { function new( entity ) { // Lucee allows a shallow copy which does not copy the object graph. // This is perfect for our use cases and cuts loading time down immensely! - return duplicate( entity, false ); + var newEntity = duplicate( entity, false ); + newEntity.reset(); + return newEntity; } } diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 88f6a62c..d8ddf4be 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -49,12 +49,12 @@ component accessors="true" { function assignDefaultProperties() { assignAttributesData( {} ); assignOriginalAttributes( {} ); - variables._meta = {}; - variables._data = {}; - variables._relationshipsData = {}; - variables._eagerLoad = []; - variables._nullValues = {}; - variables._loaded = false; + param variables._meta = {}; + param variables._data = {}; + param variables._relationshipsData = {}; + param variables._eagerLoad = []; + param variables._nullValues = {}; + param variables._loaded = false; } function onDIComplete() { @@ -296,8 +296,18 @@ component accessors="true" { return variables._entityCreator.new( this ); } + function reset() { + assignAttributesData( {} ); + assignOriginalAttributes( {} ); + variables._data = {}; + variables._relationshipsData = {}; + variables._eagerLoad = []; + variables._loaded = false; + return this; + } + function fresh() { - return variables.find( keyValue() ); + return variables.resetQuery().find( keyValue() ); } function refresh() { From d6611c60f13d97808141f02ce88042b5c853d222 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 27 Dec 2018 23:16:00 -0700 Subject: [PATCH 31/70] fix(BaseEntity): Remove alternative relationships syntax Relationships could be defined previously either as methods on the component or methods on a `_relationships` struct. The `_relationships` struct does not add any value and has different bugs associated with it. Additionally, Quick gains simplicity in the codebase by having only one way to define relationships. BREAKING CHANGE: Relationships cannot be defined using `variables._relationships` anymore. --- models/BaseEntity.cfc | 31 +++----------------------- tests/resources/app/models/Country.cfc | 8 +++---- 2 files changed, 6 insertions(+), 33 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index d8ddf4be..6f63590c 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -37,9 +37,6 @@ component accessors="true" { this.constraints = {}; - // This is for an alternative syntax for defining relationships. - variables._relationships = {}; - function init( struct meta = {} ) { assignDefaultProperties(); variables._meta = arguments.meta; @@ -410,7 +407,7 @@ component accessors="true" { param md.functions = []; return ! arrayIsEmpty( arrayFilter( md.functions, function( func ) { return compareNoCase( func.name, name ) == 0; - } ) ) || variables._relationships.keyExists( name ); + } ) ); } function isRelationshipLoaded( name ) { @@ -707,7 +704,7 @@ component accessors="true" { variables.query = q.retrieveQuery(); return this; } - var r = tryRelationships( missingMethodName, missingMethodArguments ); + var r = tryRelationshipGetter( missingMethodName, missingMethodArguments ); if ( ! isNull( r ) ) { return r; } return forwardToQB( missingMethodName, missingMethodArguments ); } @@ -748,12 +745,6 @@ component accessors="true" { return missingMethodArguments[ 1 ]; } - private function tryRelationships( missingMethodName, missingMethodArguments ) { - var relationship = tryRelationshipGetter( missingMethodName, missingMethodArguments ); - if ( ! isNull( relationship ) ) { return relationship; } - return tryRelationshipDefinition( missingMethodName ); - } - private function tryRelationshipGetter( missingMethodName, missingMethodArguments ) { if ( ! variables._str.startsWith( missingMethodName, "get" ) ) { return; @@ -766,14 +757,7 @@ component accessors="true" { } if ( ! isRelationshipLoaded( relationshipName ) ) { - var relationship = ""; - if ( variables._relationships.keyExists( relationshipName ) ) { - var method = variables._relationships[ relationshipName ]; - relationship = method( missingMethodArguments ); - } - else { - relationship = invoke( this, relationshipName, missingMethodArguments ); - } + var relationship = invoke( this, relationshipName, missingMethodArguments ); relationship.setRelationMethodName( relationshipName ); assignRelationship( relationshipName, relationship.getResults() ); } @@ -781,15 +765,6 @@ component accessors="true" { return retrieveRelationship( relationshipName ); } - private function tryRelationshipDefinition( relationshipName ) { - if ( variables._relationships.keyExists( relationshipName ) ) { - var method = variables._relationships[ relationshipName ]; - var relationship = method(); - relationship.setRelationMethodName( relationshipName ); - return relationship; - } - } - private function tryScopes( missingMethodName, missingMethodArguments ) { if ( structKeyExists( variables, "scope#missingMethodName#" ) ) { var scopeArgs = { "1" = this }; diff --git a/tests/resources/app/models/Country.cfc b/tests/resources/app/models/Country.cfc index 2bdaf167..8ad58c1e 100644 --- a/tests/resources/app/models/Country.cfc +++ b/tests/resources/app/models/Country.cfc @@ -5,11 +5,9 @@ component extends="quick.models.BaseEntity" { property name="createdDate" column="created_date"; property name="modifiedDate" column="modified_date"; - variables._relationships = { - "posts" = function() { - return hasManyThrough( "Post", "User", "country_id", "user_id" ); - } - }; + function posts() { + return hasManyThrough( "Post", "User", "country_id", "user_id" ); + } function keyType() { return variables._wirebox.getInstance( "UUID@quick" ); From 25bc69a558f7205c707da293e7eb36d4b33ef74f Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 27 Dec 2018 23:22:53 -0700 Subject: [PATCH 32/70] v2.0.0-beta.6 --- box.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/box.json b/box.json index 341cb36e..6dfe168c 100644 --- a/box.json +++ b/box.json @@ -1,8 +1,8 @@ { "name":"quick", - "version":"2.0.0-beta.5", + "version":"2.0.0-beta.6", "author":"", - "location":"coldbox-modules/quick#v2.0.0-beta.5", + "location":"coldbox-modules/quick#v2.0.0-beta.6", "homepage":"https://github.com/coldbox-modules/quick", "documentation":"https://github.com/coldbox-modules/quick", "repository":{ From 43e71dc82777cbf7756ccbb60cefe771af5f60c2 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 28 Dec 2018 14:58:21 -0700 Subject: [PATCH 33/70] refactor(NullKeyType): Rename AssignedKey to NullKeyType NullKeyType is a better name since this keyType does nothing on pre- or post-insert. BREAKING CHANGE: References to `AssignedKey` need to be renamed to `NullKey` --- models/KeyTypes/{AssignedKey.cfc => NullKeyType.cfc} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename models/KeyTypes/{AssignedKey.cfc => NullKeyType.cfc} (100%) diff --git a/models/KeyTypes/AssignedKey.cfc b/models/KeyTypes/NullKeyType.cfc similarity index 100% rename from models/KeyTypes/AssignedKey.cfc rename to models/KeyTypes/NullKeyType.cfc From cbfbf9b9b3cf0b9e351da282cddfc317559a5959 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 28 Dec 2018 15:06:43 -0700 Subject: [PATCH 34/70] refactor(AutoIncrementingKeyType): Rename AutoIncrementing to AutoIncrementingKeyType For consistency append "KeyType" to the component name BREAKING CHANGE: References to `AutoIncrementing` need to be renamed to `AutoIncrementingKeyType` --- models/BaseEntity.cfc | 2 +- .../{AutoIncrementing.cfc => AutoIncrementingKeyType.cfc} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename models/KeyTypes/{AutoIncrementing.cfc => AutoIncrementingKeyType.cfc} (100%) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 6f63590c..7fa6762d 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -59,7 +59,7 @@ component accessors="true" { } function keyType() { - return variables._wirebox.getInstance( "AutoIncrementing@quick" ); + return variables._wirebox.getInstance( "AutoIncrementingKeyType@quick" ); } function retrieveKeyType() { diff --git a/models/KeyTypes/AutoIncrementing.cfc b/models/KeyTypes/AutoIncrementingKeyType.cfc similarity index 100% rename from models/KeyTypes/AutoIncrementing.cfc rename to models/KeyTypes/AutoIncrementingKeyType.cfc From f7c0dbdfee0c7f104b38d5d4490ae2396490f7b8 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 28 Dec 2018 15:09:30 -0700 Subject: [PATCH 35/70] refactor(UUIDKeyType): Rename UUID to UUIDKeyType For consistency append "KeyType" to the component name BREAKING CHANGE: References to `UUID` need to be renamed to `UUIDKeyType` --- models/KeyTypes/{UUID.cfc => UUIDKeyType.cfc} | 0 tests/resources/app/models/Country.cfc | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename models/KeyTypes/{UUID.cfc => UUIDKeyType.cfc} (100%) diff --git a/models/KeyTypes/UUID.cfc b/models/KeyTypes/UUIDKeyType.cfc similarity index 100% rename from models/KeyTypes/UUID.cfc rename to models/KeyTypes/UUIDKeyType.cfc diff --git a/tests/resources/app/models/Country.cfc b/tests/resources/app/models/Country.cfc index 8ad58c1e..ce9509c0 100644 --- a/tests/resources/app/models/Country.cfc +++ b/tests/resources/app/models/Country.cfc @@ -10,7 +10,7 @@ component extends="quick.models.BaseEntity" { } function keyType() { - return variables._wirebox.getInstance( "UUID@quick" ); + return variables._wirebox.getInstance( "UUIDKeyType@quick" ); } } From ae54ae3f6613a02f834ab19ae8f6da7d600093de Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 28 Dec 2018 15:11:12 -0700 Subject: [PATCH 36/70] refactor(ReturningKeyType): Rename SequentialId to ReturningKeyType Rename to `ReturningKeyType` since it is not always a "SequentialId" being returned. Also append "KeyType" to the component name for consistency. BREAKING CHANGE: References to `SequentialId` need to be renamed to `ReturningKeyType` --- models/KeyTypes/{SequentialId.cfc => ReturningKeyType.cfc} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename models/KeyTypes/{SequentialId.cfc => ReturningKeyType.cfc} (100%) diff --git a/models/KeyTypes/SequentialId.cfc b/models/KeyTypes/ReturningKeyType.cfc similarity index 100% rename from models/KeyTypes/SequentialId.cfc rename to models/KeyTypes/ReturningKeyType.cfc From 7fb9e7e54eb9505e5a8086531477d0ac423426a1 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 29 Dec 2018 15:23:05 -0700 Subject: [PATCH 37/70] v2.0.0-beta.7 --- box.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/box.json b/box.json index 6dfe168c..1d9c0df2 100644 --- a/box.json +++ b/box.json @@ -1,8 +1,8 @@ { "name":"quick", - "version":"2.0.0-beta.6", + "version":"2.0.0-beta.7", "author":"", - "location":"coldbox-modules/quick#v2.0.0-beta.6", + "location":"coldbox-modules/quick#v2.0.0-beta.7", "homepage":"https://github.com/coldbox-modules/quick", "documentation":"https://github.com/coldbox-modules/quick", "repository":{ From 0cbceea63b944235e30269c308a2bbcc2ea3e8b4 Mon Sep 17 00:00:00 2001 From: Jon Clausen Date: Wed, 2 Jan 2019 09:53:50 -0500 Subject: [PATCH 38/70] fix(ManyToMany): Use foreignPivotKey instead of parentKey The wrong key was used. It wasn't caught because of a lucky break in the tests. Both have been updated. --- models/Relationships/BelongsToMany.cfc | 4 ++-- tests/Application.cfc | 12 ++++++------ tests/resources/app/models/Post.cfc | 2 +- tests/resources/app/models/Tag.cfc | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/models/Relationships/BelongsToMany.cfc b/models/Relationships/BelongsToMany.cfc index 3f8b4587..647a725e 100644 --- a/models/Relationships/BelongsToMany.cfc +++ b/models/Relationships/BelongsToMany.cfc @@ -100,7 +100,7 @@ component accessors="true" extends="quick.models.Relationships.BaseRelationship" function detach( id ) { var foreignPivotKeyValue = variables.parent.retrieveAttribute( variables.parentKey ); newPivotStatement() - .where( variables.parentKey, "=", foreignPivotKeyValue ) + .where( variables.foreignPivotKey, "=", foreignPivotKeyValue ) .whereIn( variables.relatedPivotKey, parseIds( id ) @@ -109,7 +109,7 @@ component accessors="true" extends="quick.models.Relationships.BaseRelationship" function sync( id ) { var foreignPivotKeyValue = variables.parent.retrieveAttribute( variables.parentKey ); - newPivotStatement().where( variables.parentKey, "=", foreignPivotKeyValue ).delete(); + newPivotStatement().where( variables.foreignPivotKey, "=", foreignPivotKeyValue ).delete(); attach( id ); } diff --git a/tests/Application.cfc b/tests/Application.cfc index 1646598d..247b7107 100644 --- a/tests/Application.cfc +++ b/tests/Application.cfc @@ -130,15 +130,15 @@ component { queryExecute( "INSERT INTO `tags` (`id`, `name`) VALUES (2, 'music')" ); queryExecute( " CREATE TABLE `my_posts_tags` ( - `post_pk` int(11) NOT NULL, + `custom_post_pk` int(11) NOT NULL, `tag_id` int(11) NOT NULL, - PRIMARY KEY (`post_pk`, `tag_id`) + PRIMARY KEY (`custom_post_pk`, `tag_id`) ) " ); - queryExecute( "INSERT INTO `my_posts_tags` (`post_pk`, `tag_id`) VALUES (1245, 1)" ); - queryExecute( "INSERT INTO `my_posts_tags` (`post_pk`, `tag_id`) VALUES (1245, 2)" ); - queryExecute( "INSERT INTO `my_posts_tags` (`post_pk`, `tag_id`) VALUES (523526, 1)" ); - queryExecute( "INSERT INTO `my_posts_tags` (`post_pk`, `tag_id`) VALUES (523526, 2)" ); + queryExecute( "INSERT INTO `my_posts_tags` (`custom_post_pk`, `tag_id`) VALUES (1245, 1)" ); + queryExecute( "INSERT INTO `my_posts_tags` (`custom_post_pk`, `tag_id`) VALUES (1245, 2)" ); + queryExecute( "INSERT INTO `my_posts_tags` (`custom_post_pk`, `tag_id`) VALUES (523526, 1)" ); + queryExecute( "INSERT INTO `my_posts_tags` (`custom_post_pk`, `tag_id`) VALUES (523526, 2)" ); queryExecute( " CREATE TABLE `links` ( `link_id` int(11) NOT NULL AUTO_INCREMENT, diff --git a/tests/resources/app/models/Post.cfc b/tests/resources/app/models/Post.cfc index 124c4807..4e861e37 100644 --- a/tests/resources/app/models/Post.cfc +++ b/tests/resources/app/models/Post.cfc @@ -13,7 +13,7 @@ component table="my_posts" extends="quick.models.BaseEntity" accessors="true" { } function tags() { - return belongsToMany( "Tag", "my_posts_tags", "post_pk", "tag_id" ); + return belongsToMany( "Tag", "my_posts_tags", "custom_post_pk", "tag_id" ); } function comments() { diff --git a/tests/resources/app/models/Tag.cfc b/tests/resources/app/models/Tag.cfc index 2d9bc817..97f8765b 100644 --- a/tests/resources/app/models/Tag.cfc +++ b/tests/resources/app/models/Tag.cfc @@ -4,7 +4,7 @@ component extends="quick.models.BaseEntity" { property name="name"; function posts() { - return belongsToMany( "Post", "my_posts_tags", "tag_id", "post_pk" ); + return belongsToMany( "Post", "my_posts_tags", "tag_id", "custom_post_pk" ); } } From 9686de0f04d2512841922ceefeea89163c645ee3 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 2 Jan 2019 08:56:58 -0700 Subject: [PATCH 39/70] fix(BaseEntity): Return null instead of unloaded entities. The idea to return unloaded entities was an effort to avoid some of CFML's null handling. It has caused more issues, however, so now we will return null when an entity is not found, either from a query or from a relationship. BREAKING CHANGE: `null` is now returned instead of an unloaded entity when a query returns empty. You will need to update any appropriate `isLoaded` checks with `isNull` instead. --- models/BaseEntity.cfc | 31 +++++++++++++++---- models/Relationships/BelongsTo.cfc | 2 +- tests/Application.cfc | 9 ++++++ tests/resources/app/models/Empty.cfc | 5 +++ .../specs/integration/BaseEntity/GetSpec.cfc | 7 ++++- .../Relationships/BelongsToSpec.cfc | 5 +++ .../Relationships/EagerLoadingSpec.cfc | 28 ++++++++--------- .../BaseEntity/Relationships/HasOneSpec.cfc | 7 ++++- 8 files changed, 71 insertions(+), 23 deletions(-) create mode 100644 tests/resources/app/models/Empty.cfc diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 7fa6762d..5cb09abd 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -32,6 +32,7 @@ component accessors="true" { property name="_data" persistent="false"; property name="_originalAttributes" persistent="false"; property name="_relationshipsData" persistent="false"; + property name="_relationshipsLoaded" persistent="false"; property name="_eagerLoad" persistent="false"; property name="_loaded" persistent="false"; @@ -49,6 +50,7 @@ component accessors="true" { param variables._meta = {}; param variables._data = {}; param variables._relationshipsData = {}; + param variables._relationshipsLoaded = {}; param variables._eagerLoad = []; param variables._nullValues = {}; param variables._loaded = false; @@ -234,10 +236,12 @@ component accessors="true" { function first() { var attrs = retrieveQuery().first( options = variables._queryOptions ); - return newEntity() - .assignAttributesData( attrs ) - .assignOriginalAttributes( attrs ) - .set_Loaded( ! structIsEmpty( attrs ) ); + return structIsEmpty( attrs ) ? + javacast( "null", "" ) : + newEntity() + .assignAttributesData( attrs ) + .assignOriginalAttributes( attrs ) + .set_Loaded( true ); } function find( id ) { @@ -298,6 +302,7 @@ component accessors="true" { assignOriginalAttributes( {} ); variables._data = {}; variables._relationshipsData = {}; + variables._relationshipsLoaded = {}; variables._eagerLoad = []; variables._loaded = false; return this; @@ -309,6 +314,7 @@ component accessors="true" { function refresh() { variables._relationshipsData = {}; + variables._relationshipsLoaded = {}; assignAttributesData( newQuery() .from( variables._table ) @@ -411,17 +417,20 @@ component accessors="true" { } function isRelationshipLoaded( name ) { - return structKeyExists( variables._relationshipsData, name ); + return structKeyExists( variables._relationshipsLoaded, name ); } function retrieveRelationship( name ) { - return variables._relationshipsData[ name ]; + return variables._relationshipsData.keyExists( name ) ? + variables._relationshipsData[ name ] : + javacast( "null", "" ); } function assignRelationship( name, value ) { if ( ! isNull( value ) ) { variables._relationshipsData[ name ] = value; } + variables._relationshipsLoaded[ name ] = true; return this; } @@ -706,6 +715,9 @@ component accessors="true" { } var r = tryRelationshipGetter( missingMethodName, missingMethodArguments ); if ( ! isNull( r ) ) { return r; } + if ( relationshipIsNull( missingMethodName ) ) { + return javacast( "null", "" ); + } return forwardToQB( missingMethodName, missingMethodArguments ); } @@ -765,6 +777,13 @@ component accessors="true" { return retrieveRelationship( relationshipName ); } + private function relationshipIsNull( name ) { + if ( ! variables._str.startsWith( name, "get" ) ) { + return false; + } + return variables._relationshipsLoaded.keyExists( variables._str.slice( name, 4 ) ); + } + private function tryScopes( missingMethodName, missingMethodArguments ) { if ( structKeyExists( variables, "scope#missingMethodName#" ) ) { var scopeArgs = { "1" = this }; diff --git a/models/Relationships/BelongsTo.cfc b/models/Relationships/BelongsTo.cfc index 3d54c7b3..03512ee5 100644 --- a/models/Relationships/BelongsTo.cfc +++ b/models/Relationships/BelongsTo.cfc @@ -38,7 +38,7 @@ component extends="quick.models.Relationships.BaseRelationship" { function initRelation( entities, relation ) { entities.each( function( entity ) { - entity.assignRelationship( relation, {} ); + entity.assignRelationship( relation, javacast( "null", "" ) ); } ); return entities; } diff --git a/tests/Application.cfc b/tests/Application.cfc index 247b7107..e308ba04 100644 --- a/tests/Application.cfc +++ b/tests/Application.cfc @@ -82,6 +82,9 @@ component { queryExecute( " INSERT INTO `my_posts` (`post_pk`, `user_id`, `body`, `created_date`, `modified_date`) VALUES (523526, 1, 'My second awesome post body', '2017-07-28 02:07:36', '2017-07-28 02:07:36') " ); + queryExecute( " + INSERT INTO `my_posts` (`post_pk`, `user_id`, `body`, `created_date`, `modified_date`) VALUES (7777, NULL, 'My post with no author', '2017-07-28 02:07:36', '2017-07-28 02:07:36') + " ); queryExecute( " CREATE TABLE `videos` ( `id` int(11) NOT NULL AUTO_INCREMENT, @@ -186,5 +189,11 @@ component { PRIMARY KEY (`id`) ) " ); + queryExecute( " + CREATE TABLE `empty` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + PRIMARY KEY (`id`) + ) + " ); } } diff --git a/tests/resources/app/models/Empty.cfc b/tests/resources/app/models/Empty.cfc new file mode 100644 index 00000000..dc02cb91 --- /dev/null +++ b/tests/resources/app/models/Empty.cfc @@ -0,0 +1,5 @@ +component table="empty" extends="quick.models.BaseEntity" { + + property name="id"; + +} diff --git a/tests/specs/integration/BaseEntity/GetSpec.cfc b/tests/specs/integration/BaseEntity/GetSpec.cfc index a7e5fe2f..fd4fdd18 100644 --- a/tests/specs/integration/BaseEntity/GetSpec.cfc +++ b/tests/specs/integration/BaseEntity/GetSpec.cfc @@ -7,11 +7,16 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { expect( user.isLoaded() ).toBeTrue( "The user instance should be found and loaded, but was not." ); } ); - it( "it returns null if the record cannot be found", function() { + it( "returns null if the record cannot be found", function() { expect( getInstance( "User" ).find( 999 ) ) .toBeNull( "The user instance should be null because it could not be found, but was not." ); } ); + it( "returns null if the first record cannot be found", function() { + expect( getInstance( "Empty" ).first() ) + .toBeNull( "The instance should be null because there are none in the database." ); + } ); + it( "can refresh itself from the database", function() { var user = getInstance( "User" ).find( 1 ); expect( user.getUsername() ).toBe( "elpete" ); diff --git a/tests/specs/integration/BaseEntity/Relationships/BelongsToSpec.cfc b/tests/specs/integration/BaseEntity/Relationships/BelongsToSpec.cfc index a736417f..12fb430e 100644 --- a/tests/specs/integration/BaseEntity/Relationships/BelongsToSpec.cfc +++ b/tests/specs/integration/BaseEntity/Relationships/BelongsToSpec.cfc @@ -23,6 +23,11 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { expect( variables.queries ).toHaveLength( 2, "Only two queries should have been executed." ); } ); + it( "returns null if there is no owning entity", function() { + var post = getInstance( "Post" ).find( 7777 ); + expect( post.getAuthor() ).toBeNull(); + } ); + it( "can associate a new entity", function() { var newPost = getInstance( "Post" ); newPost.setBody( "A new post by me!" ); diff --git a/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc b/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc index e5103164..50059228 100644 --- a/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc +++ b/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc @@ -14,18 +14,12 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { it( "can eager load a belongs to relationship", function() { var posts = getInstance( "Post" ).with( "author" ).get(); expect( posts ).toBeArray(); - expect( posts ).toHaveLength( 2 ); - var authors = posts.map( function( post ) { - return post.getAuthor(); - } ); - expect( authors ).toBeArray(); - expect( authors ).toHaveLength( 2 ); - expect( authors[ 1 ] ).notToBeArray(); - expect( authors[ 1 ] ).toBeInstanceOf( "app.models.User" ); - expect( authors[ 2 ] ).notToBeArray(); - expect( authors[ 2 ] ).toBeInstanceOf( "app.models.User" ); + expect( posts ).toHaveLength( 3, "3 posts should have been loaded" ); + expect( posts[ 1 ].getAuthor() ).toBeInstanceOf( "app.models.User" ); + expect( posts[ 2 ].getAuthor() ).toBeNull(); + expect( posts[ 3 ].getAuthor() ).toBeInstanceOf( "app.models.User" ); if ( arrayLen( variables.queries ) != 2 ) { - expect( variables.queries ).toHaveLength( 2, "Only two queries should have been executed." ); + expect( variables.queries ).toHaveLength( 2, "Only two queries should have been executed. #arrayLen( variables.queries )# were instead." ); } } ); @@ -78,13 +72,16 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { it( "can eager load a belongs to many relationship", function() { var posts = getInstance( "Post" ).with( "tags" ).get(); expect( posts ).toBeArray(); - expect( posts ).toHaveLength( 2 ); + expect( posts ).toHaveLength( 3 ); expect( posts[ 1 ].getTags() ).toBeArray(); expect( posts[ 1 ].getTags() ).toHaveLength( 2 ); expect( posts[ 2 ].getTags() ).toBeArray(); - expect( posts[ 2 ].getTags() ).toHaveLength( 2 ); + expect( posts[ 2 ].getTags() ).toHaveLength( 0 ); + + expect( posts[ 3 ].getTags() ).toBeArray(); + expect( posts[ 3 ].getTags() ).toHaveLength( 2 ); expect( variables.queries ).toHaveLength( 2, "Only two queries should have been executed." ); } ); @@ -130,7 +127,7 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { var posts = getInstance( "Post" ).with( "comments" ).get(); expect( posts ).toBeArray(); - expect( posts ).toHaveLength( 2 ); + expect( posts ).toHaveLength( 3 ); expect( posts[ 1 ].getComments() ).toBeArray(); expect( posts[ 1 ].getComments() ).toHaveLength( 2 ); @@ -138,6 +135,9 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { expect( posts[ 2 ].getComments() ).toBeArray(); expect( posts[ 2 ].getComments() ).toBeEmpty(); + expect( posts[ 3 ].getComments() ).toBeArray(); + expect( posts[ 3 ].getComments() ).toBeEmpty(); + expect( variables.queries ).toHaveLength( 2, "Only two queries should have been executed." ); } ); diff --git a/tests/specs/integration/BaseEntity/Relationships/HasOneSpec.cfc b/tests/specs/integration/BaseEntity/Relationships/HasOneSpec.cfc index 735e25f7..96fcbb96 100644 --- a/tests/specs/integration/BaseEntity/Relationships/HasOneSpec.cfc +++ b/tests/specs/integration/BaseEntity/Relationships/HasOneSpec.cfc @@ -1,12 +1,17 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { function run() { - describe( "Belongs To Spec", function() { + describe( "Has One Spec", function() { it( "can get the owning entity", function() { var user = getInstance( "User" ).find( 1 ); var post = user.getLatestPost(); expect( post.getPost_Pk() ).toBe( 523526 ); } ); + + it( "returns null if there is no owning entity", function() { + var user = getInstance( "User" ).find( 2 ); + expect( user.getLatestPost() ).toBeNull(); + } ); } ); } From f626127c3edb9e53c53cc61a223682e6f02346e7 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 2 Jan 2019 09:02:54 -0700 Subject: [PATCH 40/70] v2.0.0-beta.8 --- box.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/box.json b/box.json index 1d9c0df2..c405a09e 100644 --- a/box.json +++ b/box.json @@ -1,8 +1,8 @@ { "name":"quick", - "version":"2.0.0-beta.7", + "version":"2.0.0-beta.8", "author":"", - "location":"coldbox-modules/quick#v2.0.0-beta.7", + "location":"coldbox-modules/quick#v2.0.0-beta.8", "homepage":"https://github.com/coldbox-modules/quick", "documentation":"https://github.com/coldbox-modules/quick", "repository":{ From 7a2df481e39edb139f276c15962c0c8426b021cf Mon Sep 17 00:00:00 2001 From: Samuel Knowlton Date: Thu, 3 Jan 2019 08:15:02 -0600 Subject: [PATCH 41/70] fix(ReturningKeyType): Fix method name and variable retrieval --- models/KeyTypes/ReturningKeyType.cfc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/KeyTypes/ReturningKeyType.cfc b/models/KeyTypes/ReturningKeyType.cfc index c191acea..fc1a94d2 100644 --- a/models/KeyTypes/ReturningKeyType.cfc +++ b/models/KeyTypes/ReturningKeyType.cfc @@ -5,7 +5,7 @@ component implements="KeyType" { * Receives the entity as the only argument. */ public void function preInsert( required entity ) { - entity.getQuery().returning( entity.get_Key() ); + entity.retrieveQuery().returning( entity.get_Key() ); } /** @@ -13,7 +13,7 @@ component implements="KeyType" { * Receives the entity and the queryExecute result as arguments. */ public void function postInsert( required entity, required struct result ) { - entity.assignAttribute( entity.get_Key(), result.query.id ); + entity.assignAttribute( entity.get_Key(), result.query[ entity.get_Key() ] ); } } From 4f53647f084b421b8e19bd16630b08d2a38a715b Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 3 Jan 2019 07:43:36 -0700 Subject: [PATCH 42/70] v2.0.0-beta.9 --- box.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/box.json b/box.json index c405a09e..f03c1100 100644 --- a/box.json +++ b/box.json @@ -1,8 +1,8 @@ { "name":"quick", - "version":"2.0.0-beta.8", + "version":"2.0.0-beta.9", "author":"", - "location":"coldbox-modules/quick#v2.0.0-beta.8", + "location":"coldbox-modules/quick#v2.0.0-beta.9", "homepage":"https://github.com/coldbox-modules/quick", "documentation":"https://github.com/coldbox-modules/quick", "repository":{ From 262ffacff5d1874202aa1e262d69651238466b71 Mon Sep 17 00:00:00 2001 From: Richard Herbert <1644678+richardherbert@users.noreply.github.com> Date: Fri, 18 Jan 2019 16:03:20 +0000 Subject: [PATCH 43/70] fix(BaseEntity): Use case sensitive compare for deep equal checks --- models/BaseEntity.cfc | 8 +++---- .../integration/BaseEntity/IsDirtySpec.cfc | 23 +++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 tests/specs/integration/BaseEntity/IsDirtySpec.cfc diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 5cb09abd..6bbdf540 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -874,7 +874,7 @@ component accessors="true" { if ( isNumeric( arguments.actual ) && isNumeric( arguments.expected ) && - toString( arguments.actual ) == toString( arguments.expected ) + compare( toString( arguments.actual ), toString( arguments.expected ) ) == 0 ) { return true; } @@ -883,7 +883,7 @@ component accessors="true" { if ( isSimpleValue( arguments.actual ) && isSimpleValue( arguments.expected ) && - arguments.actual == arguments.expected + compare( arguments.actual, arguments.expected ) == 0 ) { return true; } @@ -919,7 +919,7 @@ component accessors="true" { if ( isCustomFunction( arguments.actual ) && isCustomFunction( arguments.expected ) && - arguments.actual.toString() == arguments.expected.toString() + compare( arguments.actual.toString(), arguments.expected.toString() ) == 0 ) { return true; } @@ -928,7 +928,7 @@ component accessors="true" { if ( IsXmlDoc( arguments.actual ) && IsXmlDoc( arguments.expected ) && - toString( arguments.actual ) == toString( arguments.expected ) + compare( toString( arguments.actual ), toString( arguments.expected ) ) == 0 ) { return true; } diff --git a/tests/specs/integration/BaseEntity/IsDirtySpec.cfc b/tests/specs/integration/BaseEntity/IsDirtySpec.cfc new file mode 100644 index 00000000..e0b61097 --- /dev/null +++ b/tests/specs/integration/BaseEntity/IsDirtySpec.cfc @@ -0,0 +1,23 @@ +component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { + + function run() { + describe( "isDirty Spec", function() { + it( "can test to see if an updated entity differs from that created", function() { + var user = getInstance( "User" ).find( 1 ); + + user.fill( { "last_name" = "Peterson" } ); + expect( user.isDirty() ).toBeFalse(); + + user.fill( { "last_name" = "peterson" } ); + expect( user.isDirty() ).toBeTrue(); + + user.fill( { "last_name" = "Smith" } ); + expect( user.isDirty() ).toBeTrue(); + + user.fill( { "last_name" = "Peterson" } ); + expect( user.isDirty() ).toBeFalse(); + } ); + } ); + } + +} From 3549bdb3cdda7636bd79788dedfb54f0516e52e4 Mon Sep 17 00:00:00 2001 From: Samuel Knowlton Date: Wed, 20 Mar 2019 16:23:19 -0500 Subject: [PATCH 44/70] feat(BaseRelationship): Add get alias for getResults --- box.json | 2 +- models/BaseEntity.cfc | 2 +- models/Relationships/BaseRelationship.cfc | 9 +++++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/box.json b/box.json index f03c1100..432b17e6 100644 --- a/box.json +++ b/box.json @@ -33,7 +33,7 @@ "qb":"modules/qb/", "str":"modules/str/", "cbvalidation":"modules/cbvalidation/", - "cfcollection":"modules/cfcollection" + "cfcollection":"modules/cfcollection/" }, "testbox":{ "reporter":"json", diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 6bbdf540..1117dafb 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -771,7 +771,7 @@ component accessors="true" { if ( ! isRelationshipLoaded( relationshipName ) ) { var relationship = invoke( this, relationshipName, missingMethodArguments ); relationship.setRelationMethodName( relationshipName ); - assignRelationship( relationshipName, relationship.getResults() ); + assignRelationship( relationshipName, relationship.get() ); } return retrieveRelationship( relationshipName ); diff --git a/models/Relationships/BaseRelationship.cfc b/models/Relationships/BaseRelationship.cfc index 24a615fc..b703b840 100644 --- a/models/Relationships/BaseRelationship.cfc +++ b/models/Relationships/BaseRelationship.cfc @@ -22,6 +22,15 @@ component { return variables.related.get(); } + /** + * get() + * @hint wrapper for getResults() on relationship types that have them, which is most of them. get() implemented for consistency with QB and Quick + */ + + function get() { + return getResults(); + } + function getKeys( entities, key ) { return unique( entities.map( function( entity ) { return entity.retrieveAttribute( key ); From e252fa8583be8502aeb120e9f698079cd2a340e3 Mon Sep 17 00:00:00 2001 From: Andrew Davis <35044908+blusol850@users.noreply.github.com> Date: Mon, 25 Mar 2019 18:00:24 -0400 Subject: [PATCH 45/70] feat(BaseEntity): Allow property-level update and insert guards Individual properties can be disallowed from updating or inserting by adding `update=false` or `insert=false`. Other properties will still update or insert. --- models/BaseEntity.cfc | 72 ++++++++++++++----- tests/Application.cfc | 1 + tests/resources/app/models/User.cfc | 1 + .../integration/BaseEntity/AttributeSpec.cfc | 6 +- .../integration/BaseEntity/ColumnsSpec.cfc | 3 +- .../specs/integration/BaseEntity/SaveSpec.cfc | 29 ++++++++ 6 files changed, 91 insertions(+), 21 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 1117dafb..072e6935 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -335,15 +335,20 @@ component accessors="true" { guardValid(); newQuery() .where( variables._key, keyValue() ) - .update( retrieveAttributesData( withoutKey = true ).map( function( key, value, attributes ) { - if ( isNull( value ) || isNullValue( key, value ) ) { - return { value = "", nulls = true, null = true }; - } - if ( attributeHasSqlType( key ) ) { - return { value = value, cfsqltype = getSqlTypeForAttribute( key ) }; - } - return value; - } ), variables._queryOptions ); + .update( + retrieveAttributesData( withoutKey = true ) + .filter( canUpdateAttribute ) + .map( function( key, value, attributes ) { + if ( isNull( value ) || isNullValue( key, value ) ) { + return { value = "", nulls = true, null = true }; + } + if ( attributeHasSqlType( key ) ) { + return { value = value, cfsqltype = getSqlTypeForAttribute( key ) }; + } + return value; + } ), + variables._queryOptions + ); assignOriginalAttributes( retrieveAttributesData() ); variables._loaded = true; fireEvent( "postUpdate", { entity = this } ); @@ -353,15 +358,20 @@ component accessors="true" { retrieveKeyType().preInsert( this ); fireEvent( "preInsert", { entity = this } ); guardValid(); - var result = retrieveQuery().insert( retrieveAttributesData().map( function( key, value, attributes ) { - if ( isNull( value ) || isNullValue( key, value ) ) { - return { value = "", nulls = true, null = true }; - } - if ( attributeHasSqlType( key ) ) { - return { value = value, cfsqltype = getSqlTypeForAttribute( key ) }; - } - return value; - } ), variables._queryOptions ); + var result = retrieveQuery().insert( + retrieveAttributesData() + .filter( canInsertAttribute ) + .map( function( key, value, attributes ) { + if ( isNull( value ) || isNullValue( key, value ) ) { + return { value = "", nulls = true, null = true }; + } + if ( attributeHasSqlType( key ) ) { + return { value = value, cfsqltype = getSqlTypeForAttribute( key ) }; + } + return value; + } ), + variables._queryOptions + ); retrieveKeyType().postInsert( this, result ); assignOriginalAttributes( retrieveAttributesData() ); variables._loaded = true; @@ -1136,6 +1146,32 @@ component accessors="true" { compare( variables._nullValues[ retrieveAliasForColumn( key ) ], value ) == 0; } + private function canUpdateAttribute( name ) { + return ! variables._meta.properties.filter( function( property ) { + return property.name == retrieveAliasForColumn( name ) && + ( + ! property.keyExists( "update" ) || + ( + property.keyExists( "update" ) && + property.update + ) + ); + } ).isEmpty(); + } + + private function canInsertAttribute( name ) { + return ! variables._meta.properties.filter( function( property ) { + return property.name == retrieveAliasForColumn( name ) && + ( + ! property.keyExists( "insert" ) || + ( + property.keyExists( "insert" ) && + property.insert + ) + ); + } ).isEmpty(); + } + function timeIt( callback, label ) { var start = getTickCount(); var result = callback(); diff --git a/tests/Application.cfc b/tests/Application.cfc index e308ba04..56a34632 100644 --- a/tests/Application.cfc +++ b/tests/Application.cfc @@ -49,6 +49,7 @@ component { `username` varchar(50) NOT NULL, `first_name` varchar(50) NOT NULL, `last_name` varchar(50) NOT NULL, + `email` varchar(50), `password` varchar(100) NOT NULL, `country_id` char(35), `created_date` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, diff --git a/tests/resources/app/models/User.cfc b/tests/resources/app/models/User.cfc index 9d1748d3..a57a5853 100644 --- a/tests/resources/app/models/User.cfc +++ b/tests/resources/app/models/User.cfc @@ -8,6 +8,7 @@ component extends="quick.models.BaseEntity" accessors="true" { property name="countryId" column="country_id"; property name="createdDate" column="created_date"; property name="modifiedDate" column="modified_date"; + property name="email" column="email" update=false insert=true; property name="type"; this.constraints = { diff --git a/tests/specs/integration/BaseEntity/AttributeSpec.cfc b/tests/specs/integration/BaseEntity/AttributeSpec.cfc index b2bdcb6d..a04b2de7 100644 --- a/tests/specs/integration/BaseEntity/AttributeSpec.cfc +++ b/tests/specs/integration/BaseEntity/AttributeSpec.cfc @@ -80,7 +80,8 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { "countryId" = "", "createdDate" = "", "modifiedDate" = "", - "type" = "" + "type" = "", + "email" = "" } ); } ); @@ -94,7 +95,8 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { "countryId" = "02B84D66-0AA0-F7FB-1F71AFC954843861", "createdDate" = "2017-07-28 02:06:36", "modifiedDate" = "2017-07-28 02:06:36", - "type" = "admin" + "type" = "admin", + "email" = "" } ); } ); } ); diff --git a/tests/specs/integration/BaseEntity/ColumnsSpec.cfc b/tests/specs/integration/BaseEntity/ColumnsSpec.cfc index 0d281609..5a024d46 100644 --- a/tests/specs/integration/BaseEntity/ColumnsSpec.cfc +++ b/tests/specs/integration/BaseEntity/ColumnsSpec.cfc @@ -8,10 +8,11 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { arraySort( attributeNames, "textnocase" ); expect( attributeNames ).toBeArray(); - expect( attributeNames ).toHaveLength( 9 ); + expect( attributeNames ).toHaveLength( 10 ); expect( attributeNames ).toBe( [ "COUNTRY_ID", "CREATED_DATE", + "EMAIL", "FIRST_NAME", "ID", "LAST_NAME", diff --git a/tests/specs/integration/BaseEntity/SaveSpec.cfc b/tests/specs/integration/BaseEntity/SaveSpec.cfc index f1474f88..9a1d2de8 100644 --- a/tests/specs/integration/BaseEntity/SaveSpec.cfc +++ b/tests/specs/integration/BaseEntity/SaveSpec.cfc @@ -24,6 +24,24 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { expect( newUserAgain.getFirstName() ).toBe( "New" ); expect( newUserAgain.getLastName() ).toBe( "User" ); } ); + it( "allow inserting of column where update=false in property", function() { + var newUser = getInstance( "User" ); + newUser.setUsername( "new_user2" ); + newUser.setFirstName( "New2" ); + newUser.setLastName( "User2" ); + newUser.setEmail( "test2@test.com" ); + newUser.setPassword( hash( "password" ) ); + var userRowsPreSave = queryExecute( "SELECT * FROM users" ); + expect( userRowsPreSave ).toHaveLength( 3 ); + newUser.save(); + var userRowsPostSave = queryExecute( "SELECT * FROM users" ); + expect( userRowsPostSave ).toHaveLength( 4 ); + var newUserAgain = getInstance( "User" ).whereUsername( "new_user2" ).firstOrFail(); + expect( newUserAgain.getFirstName() ).toBe( "New2" ); + expect( newUserAgain.getLastName() ).toBe( "User2" ); + expect( newUserAgain.getEmail() ).toBe( "test2@test.com" ); + } ); + it( "retrieves the generated key when saving a new record", function() { var newUser = getInstance( "User" ); @@ -55,6 +73,17 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { expect( userRowsPostSave ).toHaveLength( 3 ); } ); + it( "does not allow updating of column where update=false in property", function() { + var existingUser = getInstance( "User" ).find( 1 ); + existingUser.setEmail( "test2@test.com" ); + var userRowsPreSave = queryExecute( "SELECT * FROM users" ); + expect( userRowsPreSave ).toHaveLength( 3 ); + existingUser.save(); + var userRowsPostSave = queryExecute( "SELECT * FROM users" ); + expect( userRowsPostSave ).toHaveLength( 3 ); + expect( userRowsPostSave.email ).toBe( "" ); + } ); + it( "uses the type attribute if present for each column", function() { structDelete( request, "saveSpecPreQBExecute" ); From 7e708e7f55c9a35cd6a13f6e9ca9ff05c2a1c992 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 3 Apr 2019 08:04:40 -0400 Subject: [PATCH 46/70] fix(BaseEntity): Use fill when creating to call custom accessors Previously, `create` would skip calling custom accessors and set the attributes data directly. This fix delegates to the `fill` method to ensure custom accessors are called if they exist. --- models/BaseEntity.cfc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 072e6935..96b1be94 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -397,7 +397,7 @@ component accessors="true" { } function create( attributes = {} ) { - return newEntity().assignAttributesData( attributes ).save(); + return newEntity().fill( attributes ).save(); } function updateAll( attributes = {} ) { From 5ce0d9f3ea1b5f56db0f4621064f83122d27e746 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 3 Apr 2019 11:40:48 -0400 Subject: [PATCH 47/70] feat(Validation): Remove cbvalidation by default The Quick hooks in to cbvalidation are not as flexible as wanted. We could either spend the time exposing more of cbvalidation or we could remove it and leave that up to userland. Removing it also opens up the possibility of using a different validation library if desired. BREAKING CHANGE: Automatic validation removed. Users desiring the old behavior can create their own base entity that adds the validation back in using entity lifecycle methods. --- ModuleConfig.cfc | 3 +- box.json | 4 +- models/BaseEntity.cfc | 35 ------------------ .../integration/BaseEntity/ValidationSpec.cfc | 37 ------------------- 4 files changed, 2 insertions(+), 77 deletions(-) delete mode 100644 tests/specs/integration/BaseEntity/ValidationSpec.cfc diff --git a/ModuleConfig.cfc b/ModuleConfig.cfc index 39ce4367..dc535860 100644 --- a/ModuleConfig.cfc +++ b/ModuleConfig.cfc @@ -8,8 +8,7 @@ component { function configure() { settings = { - defaultGrammar = "AutoDiscover", - automaticValidation = true + defaultGrammar = "AutoDiscover" }; interceptorSettings = { diff --git a/box.json b/box.json index 432b17e6..c0eed656 100644 --- a/box.json +++ b/box.json @@ -19,8 +19,7 @@ "type":"modules", "dependencies":{ "qb":"^6.0.0", - "str":"^1.0.0", - "cbvalidation":"^1.3.1+51" + "str":"^1.0.0" }, "devDependencies":{ "coldbox":"be", @@ -32,7 +31,6 @@ "coldbox":"tests/resources/app/coldbox/", "qb":"modules/qb/", "str":"modules/str/", - "cbvalidation":"modules/cbvalidation/", "cfcollection":"modules/cfcollection/" }, "testbox":{ diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 96b1be94..18d2935a 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -6,9 +6,7 @@ component accessors="true" { property name="_builder" inject="provider:QuickQB@quick" persistent="false"; property name="_wirebox" inject="wirebox" persistent="false"; property name="_str" inject="provider:Str@str" persistent="false"; - // TOOD: retrieve and store settings in guardValid property name="_settings" inject="coldbox:modulesettings:quick" persistent="false"; - property name="_validationManager" inject="provider:ValidationManager@cbvalidation" persistent="false"; property name="_interceptorService" inject="provider:coldbox:interceptorService" persistent="false"; property name="_entityCreator" inject="provider:EntityCreator@quick" persistent="false"; @@ -332,7 +330,6 @@ component accessors="true" { fireEvent( "preSave", { entity = this } ); if ( variables._loaded ) { fireEvent( "preUpdate", { entity = this } ); - guardValid(); newQuery() .where( variables._key, keyValue() ) .update( @@ -357,7 +354,6 @@ component accessors="true" { resetQuery(); retrieveKeyType().preInsert( this ); fireEvent( "preInsert", { entity = this } ); - guardValid(); var result = retrieveQuery().insert( retrieveAttributesData() .filter( canInsertAttribute ) @@ -1024,37 +1020,6 @@ component accessors="true" { }, {} ); } - /*================================= - = Validation = - =================================*/ - - private function guardValid() { - if ( isNull( variables._validationManager ) ) { - return this; - } - - // TOOD: retrieve and store settings here - param variables._settings.automaticValidation = false; - if ( ! variables._settings.automaticValidation ) { - return this; - } - - var validationResult = variables._validationManager.validate( - target = retrieveAttributesData( aliased = true ), - constraints = this.constraints - ); - - if ( ! validationResult.hasErrors() ) { - return this; - } - - throw( - type = "InvalidEntity", - message = "The #variables._entityName# entity failed to pass validation", - detail = validationResult.getAllErrorsAsJson() - ); - } - /*================================= = Read Only = =================================*/ diff --git a/tests/specs/integration/BaseEntity/ValidationSpec.cfc b/tests/specs/integration/BaseEntity/ValidationSpec.cfc deleted file mode 100644 index 32fc95a7..00000000 --- a/tests/specs/integration/BaseEntity/ValidationSpec.cfc +++ /dev/null @@ -1,37 +0,0 @@ -component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { - - function run() { - describe( "Validation Spec", function() { - it( "does nothing if there is no constraints property", function() { - var tag = getInstance( "Tag" ); - tag.setName( "miscellaneous" ); - expect(function() { - tag.save(); - }).notToThrow( "InvalidEntity" ); - } ); - - it( "it automatically validates a model with a constraints property", function() { - var newUser = getInstance( "User" ); - newUser.setUsername( "new_user" ); - newUser.setFirstName( "New" ); - // missing last name - newUser.setPassword( hash( "password" ) ); - var userRowsPreSave = queryExecute( "SELECT * FROM users" ); - expect( userRowsPreSave ).toHaveLength( 3 ); - // expect( function() { - // newUser.save(); - // } ).toThrow( "InvalidEntity" ); - var userRowsPostFirstSave = queryExecute( "SELECT * FROM users" ); - expect( userRowsPostFirstSave ).toHaveLength( 3 ); - // set last name - newUser.setLastName( "User" ); - expect( function() { - newUser.save(); - } ).notToThrow( "InvalidEntity" ); - var userRowsPostSecondSave = queryExecute( "SELECT * FROM users" ); - expect( userRowsPostSecondSave ).toHaveLength( 4 ); - } ); - } ); - } - -} From fbda0e5c9cfc2c914c75948a996afe4cc5335976 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 3 Apr 2019 22:05:36 -0400 Subject: [PATCH 48/70] feat(Events): Add an instance ready event `instanceReady` and `quickInstanceReady` provide hooks into an entity after it has been created, autowired, and the metadata processed. For example, it can be used to prep values for Mementifier or cbvalidation. --- ModuleConfig.cfc | 1 + models/BaseEntity.cfc | 1 + tests/resources/app/models/Song.cfc | 4 ++ .../BaseEntity/Events/InstanceReadySpec.cfc | 37 +++++++++++++++++++ 4 files changed, 43 insertions(+) create mode 100644 tests/specs/integration/BaseEntity/Events/InstanceReadySpec.cfc diff --git a/ModuleConfig.cfc b/ModuleConfig.cfc index dc535860..316aa359 100644 --- a/ModuleConfig.cfc +++ b/ModuleConfig.cfc @@ -13,6 +13,7 @@ component { interceptorSettings = { customInterceptionPoints = [ + "quickInstanceReady", "quickPreLoad", "quickPostLoad", "quickPreSave", diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 18d2935a..d00f25a1 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -56,6 +56,7 @@ component accessors="true" { function onDIComplete() { metadataInspection(); + fireEvent( "instanceReady", { entity = this } ); } function keyType() { diff --git a/tests/resources/app/models/Song.cfc b/tests/resources/app/models/Song.cfc index 86ff2531..2fa40426 100644 --- a/tests/resources/app/models/Song.cfc +++ b/tests/resources/app/models/Song.cfc @@ -6,6 +6,10 @@ component extends="quick.models.BaseEntity" accessors="true" { property name="createdDate" column="created_date"; property name="modifiedDate" column="modified_date"; + function instanceReady( eventData ) { + request.instanceReadyCalled = eventData; + } + function preLoad( eventData ) { request.preLoadCalled = eventData; } diff --git a/tests/specs/integration/BaseEntity/Events/InstanceReadySpec.cfc b/tests/specs/integration/BaseEntity/Events/InstanceReadySpec.cfc new file mode 100644 index 00000000..1ce14eed --- /dev/null +++ b/tests/specs/integration/BaseEntity/Events/InstanceReadySpec.cfc @@ -0,0 +1,37 @@ +component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { + + function beforeAll() { + super.beforeAll(); + var interceptorService = getWireBox().getInstance( dsl = "coldbox:interceptorService" ); + interceptorService.registerInterceptor( interceptorObject = this ); + } + + function run() { + describe( "instanceReady spec", function() { + beforeEach( function() { + variables.interceptData = {}; + } ); + + it( "announces an instanceReady interception point", function() { + var song = getInstance( "Song" ).findOrFail( 1 ); + expect( variables ).toHaveKey( "quickInstanceReadyCalled" ); + expect( variables.quickInstanceReadyCalled ).toBeStruct(); + expect( variables.quickInstanceReadyCalled ).toHaveKey( "entity" ); + structDelete( variables, "quickInstanceReadyCalled" ); + } ); + + it( "calls any preLoad method on the component", function() { + var song = getInstance( "Song" ).findOrFail( 1 ); + expect( request ).toHaveKey( "instanceReadyCalled" ); + expect( request.instanceReadyCalled ).toBeStruct(); + expect( request.instanceReadyCalled ).toHaveKey( "entity" ); + structDelete( request, "instanceReadyCalled" ); + } ); + } ); + } + + function quickInstanceReady( event, interceptData, buffer, rc, prc ) { + variables.quickInstanceReadyCalled = arguments.interceptData; + } + +} From 5541b409886fbad310a1b65bfc730634e1d1aebd Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 3 Apr 2019 22:18:13 -0400 Subject: [PATCH 49/70] v2.0.0-beta.10 --- box.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/box.json b/box.json index c0eed656..04e9742e 100644 --- a/box.json +++ b/box.json @@ -1,8 +1,8 @@ { "name":"quick", - "version":"2.0.0-beta.9", + "version":"2.0.0-beta.10", "author":"", - "location":"coldbox-modules/quick#v2.0.0-beta.9", + "location":"coldbox-modules/quick#v2.0.0-beta.10", "homepage":"https://github.com/coldbox-modules/quick", "documentation":"https://github.com/coldbox-modules/quick", "repository":{ From c3f892885753c86e2aec6a13c67ae7c67447f65a Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sun, 7 Apr 2019 17:54:43 -0400 Subject: [PATCH 50/70] fix(BelongsTo): Only bring back unique, non-blank keys --- models/Relationships/BelongsTo.cfc | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/models/Relationships/BelongsTo.cfc b/models/Relationships/BelongsTo.cfc index 03512ee5..ae13ddeb 100644 --- a/models/Relationships/BelongsTo.cfc +++ b/models/Relationships/BelongsTo.cfc @@ -30,10 +30,13 @@ component extends="quick.models.Relationships.BaseRelationship" { function getEagerEntityKeys( entities ) { return entities.reduce( function( keys, entity ) { if ( ! isNull( entity.retrieveAttribute( variables.foreignKey ) ) ) { - arrayAppend( keys, entity.retrieveAttribute( variables.foreignKey ) ); + var key = entity.retrieveAttribute( variables.foreignKey ); + if ( key != "" ) { + keys[ key ] = {}; + } } return keys; - }, [] ); + }, {} ).keyArray(); } function initRelation( entities, relation ) { From c694f10a9119553091c01ead47f8148e72304ba5 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sun, 7 Apr 2019 17:55:16 -0400 Subject: [PATCH 51/70] fix(HasOne): Bring back null when no relationship found --- models/Relationships/HasOne.cfc | 2 +- .../BaseEntity/Relationships/EagerLoadingSpec.cfc | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/models/Relationships/HasOne.cfc b/models/Relationships/HasOne.cfc index 8b7f0c00..1119474e 100644 --- a/models/Relationships/HasOne.cfc +++ b/models/Relationships/HasOne.cfc @@ -6,7 +6,7 @@ component extends="quick.models.Relationships.HasOneOrMany" { function initRelation( entities, relation ) { entities.each( function( entity ) { - entity.assignRelationship( relation, {} ); + entity.assignRelationship( relation, javacast( "null", "" ) ); } ); return entities; } diff --git a/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc b/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc index 50059228..a8cc87bb 100644 --- a/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc +++ b/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc @@ -53,18 +53,15 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { var janedoe = users[ 1 ]; expect( janedoe.getUsername() ).toBe( "janedoe" ); - expect( janedoe.getLatestPost() ).toBeStruct(); - expect( janedoe.getLatestPost() ).toBeEmpty(); + expect( janedoe.getLatestPost() ).toBeNull(); var johndoe = users[ 2 ]; expect( johndoe.getUsername() ).toBe( "johndoe" ); - expect( johndoe.getLatestPost() ).toBeStruct(); - expect( johndoe.getLatestPost() ).toBeEmpty(); + expect( johndoe.getLatestPost() ).toBeNull(); var elpete = users[ 3 ]; expect( elpete.getUsername() ).toBe( "elpete" ); - expect( elpete.getLatestPost() ).toBeStruct(); - expect( elpete.getLatestPost() ).notToBeEmpty(); + expect( elpete.getLatestPost() ).notToBeNull(); expect( variables.queries ).toHaveLength( 2, "Only two queries should have been executed." ); } ); From 571b836fa645f4e8cbe8d972b12866372be636c7 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sun, 7 Apr 2019 17:57:04 -0400 Subject: [PATCH 52/70] feat(BaseEntity): Add guard against no mapped attributes Throw a more clear error when no attributes have been mapped on the entity --- models/BaseEntity.cfc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index d00f25a1..f0044e61 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -327,6 +327,7 @@ component accessors="true" { ===========================================*/ function save() { + guardNoAttributes(); guardReadOnly(); fireEvent( "preSave", { entity = this } ); if ( variables._loaded ) { @@ -1077,6 +1078,14 @@ component accessors="true" { return foundProperties[ 1 ].keyExists( "readonly" ) && foundProperties[ 1 ].readonly; } + private function guardNoAttributes() { + if ( retrieveAttributeNames().isEmpty() ) { + throw( + type = "QuickNoAttributesException", + message = "[#variables._entityName#] does not have any attributes specified." + ); + } + } /*============================== = Events = ==============================*/ From 0b76fb5fde0b46bad7fb129c5bc92c4351372af4 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sun, 7 Apr 2019 17:58:15 -0400 Subject: [PATCH 53/70] feat(BaseEntity): Guard against no attribute data on insert Throw a more descriptive error when trying to save an entity with no attribute values set. --- models/BaseEntity.cfc | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index f0044e61..93303097 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -355,19 +355,21 @@ component accessors="true" { else { resetQuery(); retrieveKeyType().preInsert( this ); - fireEvent( "preInsert", { entity = this } ); + var attrs = retrieveAttributesData() + .filter( canInsertAttribute ) + .map( function( key, value, attributes ) { + if ( isNull( value ) || isNullValue( key, value ) ) { + return { value = "", nulls = true, null = true }; + } + if ( attributeHasSqlType( key ) ) { + return { value = value, cfsqltype = getSqlTypeForAttribute( key ) }; + } + return value; + } ); + fireEvent( "preInsert", { entity = this, attributes = attrs } ); + guardEmptyAttributeData( attrs ); var result = retrieveQuery().insert( - retrieveAttributesData() - .filter( canInsertAttribute ) - .map( function( key, value, attributes ) { - if ( isNull( value ) || isNullValue( key, value ) ) { - return { value = "", nulls = true, null = true }; - } - if ( attributeHasSqlType( key ) ) { - return { value = value, cfsqltype = getSqlTypeForAttribute( key ) }; - } - return value; - } ), + attrs, variables._queryOptions ); retrieveKeyType().postInsert( this, result ); @@ -1086,8 +1088,18 @@ component accessors="true" { ); } } + + private function guardEmptyAttributeData( required struct attrs ) { + if ( attrs.isEmpty() ) { + throw( + type = "QuickNoAttributesDataException", + message = "[#variables._entityName#] does not have any attributes data for insert." + ); + } + } + /*============================== - = Events = + = Events = ==============================*/ function fireEvent( eventName, eventData ) { From de5aac824877095cbe062fc5c64dc40753c28635 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sun, 7 Apr 2019 17:59:30 -0400 Subject: [PATCH 54/70] feat(BaseEntity): Allowing passing Quick entities to attribute setters If a Quick entity is passed to an attribute setter the `keyValue` method will be called. If it is not a Quick entity, a descriptive error will be thrown. --- models/BaseEntity.cfc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 93303097..0b9af856 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -189,6 +189,16 @@ component accessors="true" { function assignAttribute( name, value ) { guardAgainstNonExistentAttribute( name ); guardAgainstReadOnlyAttribute( name ); + if ( ! isSimpleValue( arguments.value ) ) { + if ( ! structKeyExists( arguments.value, "keyValue" ) ) { + throw( + type = "QuickNotEntityException", + message = "The value assigned to [#name#] is not a Quick entity. Perhaps you forgot to add `persistent=""false""` to a new property?", + detail = isSimpleValue( value ) ? value : getMetadata( value ).fullname + ); + } + arguments.value = arguments.value.keyValue(); + } variables._data[ retrieveColumnForAlias( name ) ] = value; variables[ retrieveAliasForColumn( name ) ] = value; return this; From 7f8cd5e591e45731ceefa8f9adc2525f3bff695b Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sun, 7 Apr 2019 18:01:31 -0400 Subject: [PATCH 55/70] fix(BaseEntity): Don't override selected columns on find methods Previously `find` would override any previous `select` columns. That is not needed since all attributes need to be mapped. --- models/BaseEntity.cfc | 4 ---- 1 file changed, 4 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 0b9af856..afa2167e 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -256,10 +256,6 @@ component accessors="true" { function find( id ) { fireEvent( "preLoad", { id = id, metadata = variables._meta } ); var data = retrieveQuery() - .select( arrayMap( structKeyArray( variables._attributes ), function( key ) { - return retrieveColumnForAlias( key ); - } ) ) - .addSelect( variables._key ) .from( variables._table ) .find( id, variables._key, variables._queryOptions ); if ( structIsEmpty( data ) ) { From e97e01622386429487feedc778cfbdcaae349ddd Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sun, 7 Apr 2019 18:02:58 -0400 Subject: [PATCH 56/70] fix(BaseEntity): Always return the entity after executing scope methods Previously it returned the value returned from the scope methods. If you forgot to return the `query` object, it returned `null` and caused problems with chaining. This prevents those problems by always returning the entity after calling a scope. --- models/BaseEntity.cfc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index afa2167e..f128be6f 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -809,7 +809,8 @@ component accessors="true" { scopeArgs[ i + 1 ] = missingMethodArguments[ i ]; } } - return invoke( this, "scope#missingMethodName#", scopeArgs ); + invoke( this, "scope#missingMethodName#", scopeArgs ); + return this; } return; } From 1aecb6cf135aa4b3189bcbdd74c5c29d60f5d962 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sun, 7 Apr 2019 19:47:44 -0400 Subject: [PATCH 57/70] 2.0.0-beta.11 --- box.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/box.json b/box.json index 04e9742e..39264bce 100644 --- a/box.json +++ b/box.json @@ -1,8 +1,8 @@ { "name":"quick", - "version":"2.0.0-beta.10", + "version":"2.0.0-beta.11", "author":"", - "location":"coldbox-modules/quick#v2.0.0-beta.10", + "location":"coldbox-modules/quick#v2.0.0-beta.11", "homepage":"https://github.com/coldbox-modules/quick", "documentation":"https://github.com/coldbox-modules/quick", "repository":{ From c7e48bbeb2fa79db255f5ba9456b7b4c3e038031 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Mon, 8 Apr 2019 10:12:53 -0400 Subject: [PATCH 58/70] fix(BelongsTo): Return null for null relationships --- models/Relationships/BelongsTo.cfc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/models/Relationships/BelongsTo.cfc b/models/Relationships/BelongsTo.cfc index ae13ddeb..a493e645 100644 --- a/models/Relationships/BelongsTo.cfc +++ b/models/Relationships/BelongsTo.cfc @@ -10,6 +10,9 @@ component extends="quick.models.Relationships.BaseRelationship" { } function getResults() { + if ( isNull( variables.child.retrieveAttribute( variables.foreignKey ) ) ) { + return javacast( "null", "" ); + } return variables.related.first(); } From 7840d6b56906a05b30754f4114cee6767c367e5a Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Mon, 8 Apr 2019 10:13:47 -0400 Subject: [PATCH 59/70] fix(BaseEntity): Return appropriate null values for attributes data Consult the `_nullValues` map when returning attributes data. --- models/BaseEntity.cfc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index f128be6f..44bed0a7 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -88,7 +88,7 @@ component accessors="true" { if ( withoutKey && key == variables._key ) { return acc; } - acc[ aliased ? retrieveAliasForColumn( key ) : key ] = isNull( value ) ? javacast( "null", "" ) : value; + acc[ aliased ? retrieveAliasForColumn( key ) : key ] = isNullValue( key, value ) ? javacast( "null", "" ) : value; return acc; }, {} ); } From 51ed9bfd484fa101f93d4570909c1c48cbd1fb57 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 9 Apr 2019 22:38:16 -0400 Subject: [PATCH 60/70] feat(CBORMCompat): Add CBORM Compatibility Layer Adds a CBORM Compatibility layer. Desired entities can extend `quick.models.CBORMCompatEntity`. This will enable it for virtual services as well. The compatibility layer is not 100%. It covers most persistent methods as well as most criteriaBuilder methods. --- models/BaseEntity.cfc | 8 +- models/CBORMCompatEntity.cfc | 130 +++++++ models/CBORMCriteriaBuilderCompat.cfc | 143 +++++++ tests/resources/app/models/CompatUser.cfc | 22 ++ tests/resources/app/models/User.cfc | 8 +- .../integration/BaseEntity/AttributeSpec.cfc | 4 +- .../integration/CBORMCompatEntitySpec.cfc | 351 ++++++++++++++++++ 7 files changed, 656 insertions(+), 10 deletions(-) create mode 100644 models/CBORMCompatEntity.cfc create mode 100644 models/CBORMCriteriaBuilderCompat.cfc create mode 100644 tests/resources/app/models/CompatUser.cfc create mode 100644 tests/specs/integration/CBORMCompatEntitySpec.cfc diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 44bed0a7..7072e71c 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -78,7 +78,7 @@ component accessors="true" { return variables._data[ variables._key ]; } - function retrieveAttributesData( aliased = false, withoutKey = false ) { + function retrieveAttributesData( aliased = false, withoutKey = false, withNulls = false ) { variables._attributes.keyArray().each( function( key ) { if ( variables.keyExists( key ) && ! isReadOnlyAttribute( key ) ) { assignAttribute( key, variables[ key ] ); @@ -88,7 +88,11 @@ component accessors="true" { if ( withoutKey && key == variables._key ) { return acc; } - acc[ aliased ? retrieveAliasForColumn( key ) : key ] = isNullValue( key, value ) ? javacast( "null", "" ) : value; + if ( isNull( value ) || ( isNullValue( key, value ) && withNulls ) ) { + acc[ aliased ? retrieveAliasForColumn( key ) : key ] = javacast( "null" , "" ); + } else { + acc[ aliased ? retrieveAliasForColumn( key ) : key ] = value; + } return acc; }, {} ); } diff --git a/models/CBORMCompatEntity.cfc b/models/CBORMCompatEntity.cfc new file mode 100644 index 00000000..25b648ba --- /dev/null +++ b/models/CBORMCompatEntity.cfc @@ -0,0 +1,130 @@ +component extends="quick.models.BaseEntity" { + + property name="CBORMCriteriaBuilderCompat" inject="provider:CBORMCriteriaBuilderCompat@quick"; + + function list( + struct criteria = {}, + string sortOrder, + numeric offset, + numeric max, + numeric timeout, + boolean ignoreCase, + boolean asQuery = true + ) { + structEach( criteria, function( key, value ) { + retrieveQuery().where( retrieveColumnForAlias( key ), value ); + } ); + if ( ! isNull( sortOrder ) ) { + retrieveQuery().orderBy( sortOrder ); + } + if ( ! isNull( offset ) && offset > 0 ) { + retrieveQuery().offset( offset ); + } + if ( ! isNull( max ) && max > 0 ) { + retrieveQuery().limit( max ); + } + if ( asQuery ) { + return retrieveQuery().setReturnFormat( "query" ).get(); + } else { + return super.get(); + } + } + + function countWhere() { + for ( var key in arguments ) { + retrieveQuery().where( retrieveColumnForAlias( key ), arguments[ key ] ); + } + return retrieveQuery().count(); + } + + function deleteById( id ) { + arguments.id = isArray( arguments.id ) ? arguments.id : [ arguments.id ]; + retrieveQuery().whereIn( get_key(), arguments.id ).delete(); + return this; + } + + function deleteWhere() { + for ( var key in arguments ) { + retrieveQuery().where( retrieveColumnForAlias( key ), arguments[ key ] ); + } + return deleteAll(); + } + + function exists( id ) { + if ( ! isNull( id ) ) { + retrieveQuery().where( get_key(), arguments.id ); + } + return retrieveQuery().exists(); + } + + function findAllWhere( criteria = {}, sortOrder ) { + structEach( criteria, function( key, value ) { + retrieveQuery().where( retrieveColumnForAlias( key ), value ); + } ); + if ( ! isNull( sortOrder ) ) { + var sorts = listToArray( sortOrder, "," ).map( function( sort ) { + return replace( sort, " ", "|", "ALL" ); + } ); + retrieveQuery().orderBy( sorts ); + } + return super.get(); + } + + function findWhere( criteria = {} ) { + structEach( criteria, function( key, value ) { + retrieveQuery().where( retrieveColumnForAlias( key ), value ); + } ); + return first(); + } + + function get( id = 0, returnNew = true ) { + if ( ( isNull( arguments.id ) || arguments.id == 0 ) && arguments.returnNew ) { + return newEntity(); + } + return invoke( this, "find", { id = arguments.id } ); + } + + function getAll( id, sortOrder ) { + if ( isNull( id ) ) { + if ( ! isNull( sortOrder ) ) { + var sorts = listToArray( sortOrder, "," ).map( function( sort ) { + return replace( sort, " ", "|", "ALL" ); + } ); + retrieveQuery().orderBy( sorts ); + } + return super.get(); + } + var ids = isArray( id ) ? id : listToArray( id, "," ); + retrieveQuery().whereIn( get_key(), ids ); + return super.get(); + } + + function new( properties = {} ) { + return newEntity().fill( properties ); + } + + function populate( properties = {} ) { + super.fill( properties ); + return this; + } + + function save( entity ) { + if ( isNull( entity ) ) { + return super.save(); + } + return entity.save(); + } + + function saveAll( entities = [] ) { + entities.each( function( entity ) { + entity.save(); + } ); + return this; + } + + function newCriteria() { + return CBORMCriteriaBuilderCompat.get() + .setEntity( this ); + } + +} diff --git a/models/CBORMCriteriaBuilderCompat.cfc b/models/CBORMCriteriaBuilderCompat.cfc new file mode 100644 index 00000000..775a0e93 --- /dev/null +++ b/models/CBORMCriteriaBuilderCompat.cfc @@ -0,0 +1,143 @@ +component accessors="true" { + + property name="entity"; + + function init( entity, query ) { + if ( ! isNull( arguments.entity ) ) { + variables.entity = arguments.entity; + } + return this; + } + + function getSQL() { + return getEntity().retrieveQuery().toSQL(); + } + + function between( column, start, end ) { + getEntity().retrieveQuery().whereBetween( column, start, end ); + return this; + } + + function eqProperty( left, right ) { + getEntity().retrieveQuery().whereColumn( left, right ); + return this; + } + + function isEQ( column, value ) { + getEntity().retrieveQuery().where( column, "=", value ); + return this; + } + + function isGT( column, value ) { + getEntity().retrieveQuery().where( column, ">", value ); + return this; + } + + function gtProperty( left, right ) { + getEntity().retrieveQuery().whereColumn( left, ">", right ); + return this; + } + + function isGE( column, value ) { + getEntity().retrieveQuery().where( column, ">=", value ); + return this; + } + + function geProperty( left, right ) { + getEntity().retrieveQuery().whereColumn( left, ">=", right ); + return this; + } + + function idEQ( id ) { + getEntity().retrieveQuery().where( getEntity().get_key(), id ); + return this; + } + + function like( column, value ) { + getEntity().retrieveQuery().where( column, "like", value ); + return this; + } + + function ilike( column, value ) { + getEntity().retrieveQuery().where( column, "ilike", value ); + return this; + } + + function isIn( column, values ) { + getEntity().retrieveQuery().whereIn( column, values ); + return this; + } + + function isNull( column ) { + getEntity().retrieveQuery().whereNull( column ); + return this; + } + + function isNotNull( column ) { + getEntity().retrieveQuery().whereNotNull( column ); + return this; + } + + function isLT( column, value ) { + getEntity().retrieveQuery().where( column, "<", value ); + return this; + } + + function ltProperty( left, right ) { + getEntity().retrieveQuery().whereColumn( left, "<", right ); + return this; + } + + function neProperty( left, right ) { + getEntity().retrieveQuery().whereColumn( left, "<>", right ); + return this; + } + + function isLE( column, value ) { + getEntity().retrieveQuery().where( column, "<=", value ); + return this; + } + + function leProperty( left, right ) { + getEntity().retrieveQuery().whereColumn( left, "<=", right ); + return this; + } + + function maxResults( max ) { + getEntity().retrieveQuery().limit( max ); + return this; + } + + function firstResult( offset ) { + getEntity().retrieveQuery().offset( offset ); + return this; + } + + function order( orders ) { + arguments.orders = isArray( arguments.orders ) ? arguments.orders : listToArray( arguments.orders, "," ); + getEntity().retrieveQuery().orderBy( + arguments.orders.map( function( order ) { + return replace( order, " ", "|" ); + } ) + ); + return this; + } + + function list() { + return getEntity().getAll(); + } + + function get() { + return getEntity().first(); + } + + function count() { + return getEntity().count(); + } + + function onMissingMethod( missingMethodName, missingMethodArguments ) { + invoke( variables.query, missingMethodName, missingMethodArguments ); + return this; + } + +} diff --git a/tests/resources/app/models/CompatUser.cfc b/tests/resources/app/models/CompatUser.cfc new file mode 100644 index 00000000..14ce1f7c --- /dev/null +++ b/tests/resources/app/models/CompatUser.cfc @@ -0,0 +1,22 @@ +component extends="quick.models.CBORMCompatEntity" table="users" accessors="true" { + + property name="id"; + property name="username"; + property name="firstName" column="first_name"; + property name="lastName" column="last_name"; + property name="password"; + property name="countryId" column="country_id"; + property name="createdDate" column="created_date"; + property name="modifiedDate" column="modified_date"; + property name="email" column="email" update=false insert=true; + property name="type"; + + property name="posts" + singularname="post" + cfc="models.Post" + fieldtype="one-to-many" + fkcolumn="user_id" + inverse="true" + lazy="extra"; + +} diff --git a/tests/resources/app/models/User.cfc b/tests/resources/app/models/User.cfc index a57a5853..90bd0a98 100644 --- a/tests/resources/app/models/User.cfc +++ b/tests/resources/app/models/User.cfc @@ -8,15 +8,9 @@ component extends="quick.models.BaseEntity" accessors="true" { property name="countryId" column="country_id"; property name="createdDate" column="created_date"; property name="modifiedDate" column="modified_date"; - property name="email" column="email" update=false insert=true; + property name="email" column="email" update="false" insert="true"; property name="type"; - this.constraints = { - "lastName" = { - "required" = true - } - }; - function scopeLatest( query ) { return query.orderBy( "created_date", "desc" ); } diff --git a/tests/specs/integration/BaseEntity/AttributeSpec.cfc b/tests/specs/integration/BaseEntity/AttributeSpec.cfc index a04b2de7..4b29e264 100644 --- a/tests/specs/integration/BaseEntity/AttributeSpec.cfc +++ b/tests/specs/integration/BaseEntity/AttributeSpec.cfc @@ -26,7 +26,9 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { var originalAttributes = user.retrieveAttributesData(); user.setUsername( "new_username" ); expect( originalAttributes ).notToBe( user.retrieveAttributesData() ); - expect( originalAttributes ).toBe( user.get_OriginalAttributes() ); + expect( originalAttributes.map( function( key, value ) { + return isNull( value ) ? "" : value; + } ) ).toBe( user.get_OriginalAttributes() ); } ); it( "returns a default value if the attribute is not yet set", function() { diff --git a/tests/specs/integration/CBORMCompatEntitySpec.cfc b/tests/specs/integration/CBORMCompatEntitySpec.cfc new file mode 100644 index 00000000..ca542232 --- /dev/null +++ b/tests/specs/integration/CBORMCompatEntitySpec.cfc @@ -0,0 +1,351 @@ +component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { + + function run() { + describe( "CBORM Compatibility Entity Spec", function() { + beforeEach( function() { + variables.user = getInstance( dsl = "provider:CompatUser" ); + } ); + + it( "list (without arguments)", function() { + var users = user.list(); + expect( users ).toBeQuery(); + expect( users ).toHaveLength( 3, "Three users should exist in the database and be returned." ); + } ); + + it( "list (as objects)", function() { + var users = user.list( asQuery = false ); + expect( users ).toHaveLength( 3, "Three users should exist in the database and be returned." ); + expect( users[ 1 ].getId() ).toBe( 1 ); + expect( users[ 1 ].getUsername() ).toBe( "elpete" ); + expect( users[ 2 ].getId() ).toBe( 2 ); + expect( users[ 2 ].getUsername() ).toBe( "johndoe" ); + expect( users[ 3 ].getId() ).toBe( 3 ); + expect( users[ 3 ].getUsername() ).toBe( "janedoe" ); + } ); + + it( "list (with arguments)", function() { + var users = user.list( + criteria = { lastName = "Doe" }, + sortOrder = "username", + max = 2, + offset = 1, + asQuery = false + ); + expect( users ).toHaveLength( 1, "One users should be returned." ); + expect( users[ 1 ].getId() ).toBe( 2 ); + expect( users[ 1 ].getUsername() ).toBe( "johndoe" ); + } ); + + it( "count", function() { + expect( user.count() ).toBe( 3 ); + } ); + + it( "countWhere", function() { + expect( user.countWhere( lastName = "Doe", firstName = "Jane" ) ).toBe( 1 ); + } ); + + it( "deleteById (single)", function() { + user.deleteById( 1 ); + expect( function() { + user.findOrFail( 1 ); + } ).toThrow( type = "EntityNotFound" ); + } ); + + it( "deleteById (array)", function() { + user.deleteById( [ 2, 3 ] ); + expect( function() { + user.findOrFail( 2 ); + } ).toThrow( type = "EntityNotFound" ); + expect( function() { + user.findOrFail( 3 ); + } ).toThrow( type = "EntityNotFound" ); + } ); + + it( "deleteWhere", function() { + user.deleteWhere( lastName = "Doe", firstName = "Jane" ); + expect( function() { + user.findOrFail( 1 ); + } ).notToThrow( type = "EntityNotFound" ); + expect( function() { + user.findOrFail( 2 ); + } ).notToThrow( type = "EntityNotFound" ); + expect( function() { + user.findOrFail( 3 ); + } ).toThrow( type = "EntityNotFound" ); + } ); + + it( "exists", function() { + expect( user.exists() ).toBeTrue(); + expect( user.exists( 1 ) ).toBeTrue(); + expect( user.exists( 4 ) ).toBeFalse(); + } ); + + it( "findAllWhere", function() { + var users = user.findAllWhere( { "lastName" = "Doe" } ); + expect( users ).toHaveLength( 2, "Two users should exist in the database and be returned." ); + expect( users[ 1 ].getId() ).toBe( 2 ); + expect( users[ 1 ].getUsername() ).toBe( "johndoe" ); + expect( users[ 2 ].getId() ).toBe( 3 ); + expect( users[ 2 ].getUsername() ).toBe( "janedoe" ); + } ); + + it( "findAllWhere (with sort order)", function() { + var users = user.findAllWhere( { "lastName" = "Doe" }, "username asc" ); + expect( users ).toHaveLength( 2, "Two users should exist in the database and be returned." ); + expect( users[ 1 ].getId() ).toBe( 3 ); + expect( users[ 1 ].getUsername() ).toBe( "janedoe" ); + expect( users[ 2 ].getId() ).toBe( 2 ); + expect( users[ 2 ].getUsername() ).toBe( "johndoe" ); + } ); + + it( "findWhere", function() { + var john = user.findWhere( { firstName = "John" } ); + expect( john.getId() ).toBe( 2 ); + expect( john.getUsername() ).toBe( "johndoe" ); + } ); + + it( "get", function() { + // this is a double get because user is a provider + var john = user.get().get( 2 ); + expect( john ).notToBeNull(); + expect( john.getId() ).toBe( 2 ); + expect( john.getUsername() ).toBe( "johndoe" ); + } ); + + it( "get (returns null)", function() { + // this is a double get because user is a provider + expect( user.get().get( 21234124 ) ).toBeNull(); + } ); + + it( "get( 0 ) returns a new entity", function() { + // this is a double get because user is a provider + var newUser = user.get().get( 0 ); + expect( newUser.isLoaded() ).toBeFalse(); + } ); + + it( "getAll", function() { + var users = user.getAll(); + expect( users[ 1 ].getId() ).toBe( 1 ); + expect( users[ 1 ].getUsername() ).toBe( "elpete" ); + expect( users[ 2 ].getId() ).toBe( 2 ); + expect( users[ 2 ].getUsername() ).toBe( "johndoe" ); + expect( users[ 3 ].getId() ).toBe( 3 ); + expect( users[ 3 ].getUsername() ).toBe( "janedoe" ); + } ); + + it( "getAll (sort order)", function() { + var users = user.getAll( sortOrder = "username desc" ); + expect( users[ 1 ].getId() ).toBe( 2 ); + expect( users[ 1 ].getUsername() ).toBe( "johndoe" ); + expect( users[ 2 ].getId() ).toBe( 3 ); + expect( users[ 2 ].getUsername() ).toBe( "janedoe" ); + expect( users[ 3 ].getId() ).toBe( 1 ); + expect( users[ 3 ].getUsername() ).toBe( "elpete" ); + } ); + + it( "getAll (id list)", function() { + var users = user.getAll( "2,3" ); + expect( users[ 1 ].getId() ).toBe( 2 ); + expect( users[ 1 ].getUsername() ).toBe( "johndoe" ); + expect( users[ 2 ].getId() ).toBe( 3 ); + expect( users[ 2 ].getUsername() ).toBe( "janedoe" ); + } ); + + it( "getAll (id array)", function() { + var users = user.getAll( [ "2", "3" ] ); + expect( users[ 1 ].getId() ).toBe( 2 ); + expect( users[ 1 ].getUsername() ).toBe( "johndoe" ); + expect( users[ 2 ].getId() ).toBe( 3 ); + expect( users[ 2 ].getUsername() ).toBe( "janedoe" ); + } ); + + it( "new", function() { + var newUser = user.new(); + expect( newUser.isLoaded() ).toBeFalse(); + } ); + + it( "new (with properties)", function() { + var newUser = user.new( { username = "new_username" } ); + expect( newUser.isLoaded() ).toBeFalse(); + expect( newUser.getUsername() ).toBe( "new_username" ); + } ); + + it( "populate", function() { + var newUser = user.new(); + newUser.populate( { username = "new_username" } ); + expect( newUser.getUsername() ).toBe( "new_username" ); + } ); + + describe( "criteria builder compatibility", function() { + it( "between", function() { + var rightNow = dateFormat( now(), "MM/DD/YYYY" ); + var lastWeek = dateFormat( dateAdd( "d", -7, rightNow ), "MM/DD/YYYY" ); + var actual = user.newCriteria().between( "created_date", rightNow, lastWeek ).getSQL(); + expect( actual ).toBe( + "SELECT * FROM `users` WHERE `created_date` BETWEEN ? AND ?" + ); + } ); + + it( "eqProperty", function() { + var actual = user.newCriteria().eqProperty( "created_date", "modified_date" ).getSQL(); + expect( actual ).toBe( + "SELECT * FROM `users` WHERE `created_date` = `modified_date`" + ); + } ); + + it( "isEQ", function() { + var actual = user.newCriteria().isEQ( "username", "elpete" ).getSQL(); + expect( actual ).toBe( + "SELECT * FROM `users` WHERE `username` = ?" + ); + } ); + + it( "isGT", function() { + var actual = user.newCriteria().isGT( "created_date", now() ).getSQL(); + expect( actual ).toBe( + "SELECT * FROM `users` WHERE `created_date` > ?" + ); + } ); + + it( "gtProperty", function() { + var actual = user.newCriteria().gtProperty( "modified_date", "created_date" ).getSQL(); + expect( actual ).toBe( + "SELECT * FROM `users` WHERE `modified_date` > `created_date`" + ); + } ); + + it( "isGE", function() { + var actual = user.newCriteria().isGE( "created_date", now() ).getSQL(); + expect( actual ).toBe( + "SELECT * FROM `users` WHERE `created_date` >= ?" + ); + } ); + + it( "geProperty", function() { + var actual = user.newCriteria().geProperty( "modified_date", "created_date" ).getSQL(); + expect( actual ).toBe( + "SELECT * FROM `users` WHERE `modified_date` >= `created_date`" + ); + } ); + + it( "idEQ", function() { + var actual = user.newCriteria().idEQ( 1 ).getSQL(); + expect( actual ).toBe( + "SELECT * FROM `users` WHERE `id` = ?" + ); + } ); + + it( "like", function() { + var actual = user.newCriteria().like( "username", "e%" ).getSQL(); + expect( actual ).toBe( + "SELECT * FROM `users` WHERE `username` LIKE ?" + ); + } ); + + it( "ilike", function() { + var actual = user.newCriteria().ilike( "username", "e%" ).getSQL(); + expect( actual ).toBe( + "SELECT * FROM `users` WHERE `username` ILIKE ?" + ); + } ); + + it( "isIn", function() { + var actual = user.newCriteria().isIn( "id", [ 2, 3 ] ).getSQL(); + expect( actual ).toBe( + "SELECT * FROM `users` WHERE `id` IN (?, ?)" + ); + } ); + + it( "isNull", function() { + var actual = user.newCriteria().isNull( "countryId" ).getSQL(); + expect( actual ).toBe( + "SELECT * FROM `users` WHERE `countryId` IS NULL" + ); + } ); + + it( "isNotNull", function() { + var actual = user.newCriteria().isNotNull( "countryId" ).getSQL(); + expect( actual ).toBe( + "SELECT * FROM `users` WHERE `countryId` IS NOT NULL" + ); + } ); + + it( "isLT", function() { + var actual = user.newCriteria().isLT( "created_date", now() ).getSQL(); + expect( actual ).toBe( + "SELECT * FROM `users` WHERE `created_date` < ?" + ); + } ); + + it( "ltProperty", function() { + var actual = user.newCriteria().ltProperty( "created_date", "modified_date" ).getSQL(); + expect( actual ).toBe( + "SELECT * FROM `users` WHERE `created_date` < `modified_date`" + ); + } ); + + it( "isLE", function() { + var actual = user.newCriteria().isLE( "created_date", now() ).getSQL(); + expect( actual ).toBe( + "SELECT * FROM `users` WHERE `created_date` <= ?" + ); + } ); + + it( "leProperty", function() { + var actual = user.newCriteria().leProperty( "created_date", "modified_date" ).getSQL(); + expect( actual ).toBe( + "SELECT * FROM `users` WHERE `created_date` <= `modified_date`" + ); + } ); + + it( "neProperty", function() { + var actual = user.newCriteria().neProperty( "created_date", "modified_date" ).getSQL(); + expect( actual ).toBe( + "SELECT * FROM `users` WHERE `created_date` <> `modified_date`" + ); + } ); + + it( "maxResults", function() { + var actual = user.newCriteria().maxResults( 10 ).getSQL(); + expect( actual ).toBe( + "SELECT * FROM `users` LIMIT 10" + ); + } ); + + it( "firstResult", function() { + var actual = user.newCriteria().firstResult( 10 ).getSQL(); + expect( actual ).toBe( + "SELECT * FROM `users` OFFSET 10" + ); + } ); + + it( "order", function() { + var actual = user.newCriteria().order( "username" ).getSQL(); + expect( actual ).toBe( + "SELECT * FROM `users` ORDER BY `username` ASC" + ); + } ); + + it( "list", function() { + var users = user.newCriteria().isIn( "id", [ 2, 3 ] ).list(); + expect( users[ 1 ].getId() ).toBe( 2 ); + expect( users[ 1 ].getUsername() ).toBe( "johndoe" ); + expect( users[ 2 ].getId() ).toBe( 3 ); + expect( users[ 2 ].getUsername() ).toBe( "janedoe" ); + } ); + + it( "get", function() { + var john = user.newCriteria().isIn( "id", [ 2, 3 ] ).get(); + expect( john.getId() ).toBe( 2 ); + expect( john.getUsername() ).toBe( "johndoe" ); + } ); + + it( "count", function() { + var userCount = user.newCriteria().isIn( "id", [ 2, 3 ] ).count(); + expect( userCount ).toBe( 2 ); + } ); + } ); + } ); + } + +} From 80cb398f3d106d57b9e194ce76c2e398548e0122 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 9 Apr 2019 22:46:57 -0400 Subject: [PATCH 61/70] fix(BelongsTo): Fix detecting null relationships --- models/BaseEntity.cfc | 4 ++++ models/Relationships/BelongsTo.cfc | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 7072e71c..0e69b23b 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -1139,6 +1139,10 @@ component accessors="true" { } )[ 1 ].sqltype; } + function isNullAttribute( key ) { + return isNullValue( key, retrieveAttribute( key ) ); + } + private function isNullValue( key, value ) { return variables._nullValues.keyExists( retrieveAliasForColumn( key ) ) && compare( variables._nullValues[ retrieveAliasForColumn( key ) ], value ) == 0; diff --git a/models/Relationships/BelongsTo.cfc b/models/Relationships/BelongsTo.cfc index a493e645..77f3da25 100644 --- a/models/Relationships/BelongsTo.cfc +++ b/models/Relationships/BelongsTo.cfc @@ -10,7 +10,7 @@ component extends="quick.models.Relationships.BaseRelationship" { } function getResults() { - if ( isNull( variables.child.retrieveAttribute( variables.foreignKey ) ) ) { + if ( variables.child.isNullAttribute( variables.foreignKey ) ) { return javacast( "null", "" ); } return variables.related.first(); From 8f34cce331426edee9683dd200690bb8a864bd74 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 9 Apr 2019 22:47:58 -0400 Subject: [PATCH 62/70] 2.0.0-beta.12 --- box.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/box.json b/box.json index 39264bce..a6513b54 100644 --- a/box.json +++ b/box.json @@ -1,8 +1,8 @@ { "name":"quick", - "version":"2.0.0-beta.11", + "version":"2.0.0-beta.12", "author":"", - "location":"coldbox-modules/quick#v2.0.0-beta.11", + "location":"coldbox-modules/quick#v2.0.0-beta.12", "homepage":"https://github.com/coldbox-modules/quick", "documentation":"https://github.com/coldbox-modules/quick", "repository":{ From bf4a5ad7b547f9cb8c36171186e421b95faae828 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 10 Apr 2019 13:17:22 -0400 Subject: [PATCH 63/70] feat(BaseEntity): Translate aliases to columns for queries Thanks to a new qb feature, you can now pass either aliases or columns in to a qb method and Quick will make sure the column is the one set in the query. --- box.json | 2 +- models/BaseEntity.cfc | 3 ++ .../integration/BaseEntity/ColumnsSpec.cfc | 48 +++++-------------- .../integration/CBORMCompatEntitySpec.cfc | 8 ++-- 4 files changed, 21 insertions(+), 40 deletions(-) diff --git a/box.json b/box.json index a6513b54..15021c90 100644 --- a/box.json +++ b/box.json @@ -18,7 +18,7 @@ }, "type":"modules", "dependencies":{ - "qb":"^6.0.0", + "qb":"^6.1.0", "str":"^1.0.0" }, "devDependencies":{ diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 0e69b23b..ac70f7a8 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -710,6 +710,9 @@ component accessors="true" { } variables.query = variables._builder.newQuery() .setReturnFormat( "array" ) + .setColumnFormatter( function( column ) { + return retrieveColumnForAlias( column ); + } ) .from( variables._table ); return variables.query; } diff --git a/tests/specs/integration/BaseEntity/ColumnsSpec.cfc b/tests/specs/integration/BaseEntity/ColumnsSpec.cfc index 5a024d46..65551730 100644 --- a/tests/specs/integration/BaseEntity/ColumnsSpec.cfc +++ b/tests/specs/integration/BaseEntity/ColumnsSpec.cfc @@ -2,41 +2,6 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { function run() { describe( "Columns", function() { - it( "retrieves all columns by default", function() { - var user = getInstance( "User" ).findOrFail( 1 ); - var attributeNames = user.retrieveAttributeNames( columnNames = true ); - arraySort( attributeNames, "textnocase" ); - - expect( attributeNames ).toBeArray(); - expect( attributeNames ).toHaveLength( 10 ); - expect( attributeNames ).toBe( [ - "COUNTRY_ID", - "CREATED_DATE", - "EMAIL", - "FIRST_NAME", - "ID", - "LAST_NAME", - "MODIFIED_DATE", - "PASSWORD", - "TYPE", - "USERNAME" - ] ); - } ); - - it( "always retrieves the primary key", function() { - var link = getInstance( "Link" ).findOrFail( 1 ); - - var configuredAttributes = link.get_Attributes(); - expect( configuredAttributes ).toBeStruct(); - expect( configuredAttributes ).notToHaveKey( "link_id" ); - - var attributeNames = link.retrieveAttributeNames( columnNames = true ); - arraySort( attributeNames, "textnocase" ); - expect( attributeNames ).toBeArray(); - expect( attributeNames ).toHaveLength( 3 ); - expect( attributeNames ).toBe( [ "created_date", "link_id", "link_url" ] ); - } ); - it( "can access the attributes by their alias", function() { var link = getInstance( "Link" ).findOrFail( 1 ); @@ -69,6 +34,19 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { } ); } ).notToThrow(); } ); + + it( "translates attributes to their column names when applying query restrictions", function() { + var john = getInstance( "User" ).where( "firstName", "John" ).first(); + expect( john ).notToBeNull(); + expect( john.getId() ).toBe( 2 ); + expect( john.getUsername() ).toBe( "johndoe" ); + + var bindings = getInstance( "User" ).where( "firstName", "firstName" ).retrieveQuery().getBindings(); + expect( bindings ).toBeArray(); + expect( bindings ).toHaveLength( 1 ); + expect( bindings[ 1 ] ).toBeStruct(); + expect( bindings[ 1 ].value ).toBe( "firstName" ); + } ); } ); } diff --git a/tests/specs/integration/CBORMCompatEntitySpec.cfc b/tests/specs/integration/CBORMCompatEntitySpec.cfc index ca542232..9d7bc01c 100644 --- a/tests/specs/integration/CBORMCompatEntitySpec.cfc +++ b/tests/specs/integration/CBORMCompatEntitySpec.cfc @@ -257,16 +257,16 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { } ); it( "isNull", function() { - var actual = user.newCriteria().isNull( "countryId" ).getSQL(); + var actual = user.newCriteria().isNull( "country_id" ).getSQL(); expect( actual ).toBe( - "SELECT * FROM `users` WHERE `countryId` IS NULL" + "SELECT * FROM `users` WHERE `country_id` IS NULL" ); } ); it( "isNotNull", function() { - var actual = user.newCriteria().isNotNull( "countryId" ).getSQL(); + var actual = user.newCriteria().isNotNull( "country_id" ).getSQL(); expect( actual ).toBe( - "SELECT * FROM `users` WHERE `countryId` IS NOT NULL" + "SELECT * FROM `users` WHERE `country_id` IS NOT NULL" ); } ); From f138a3995eda5ac84c5aa7dbccda099fae08abb4 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 11 Apr 2019 19:45:44 -0400 Subject: [PATCH 64/70] feat(EagerLoading): Enable nested eager loaded relationships Nested relationships can be loaded through dot notation. Each segment must refer to a valid relationship method name. For example: `getInstance( "User" ).with( "posts.comments" )` would call the `posts` method on the `User` entity and the `comments` method on the `Post` entity (or whatever type of entity returned from `posts`). --- models/BaseEntity.cfc | 8 ++-- .../Relationships/EagerLoadingSpec.cfc | 37 +++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index ac70f7a8..73843b64 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -684,12 +684,14 @@ component accessors="true" { } private function eagerLoadRelation( relationName, entities ) { - var relation = invoke( this, relationName ).resetQuery(); + var currentRelationship = listFirst( relationName, "." ); + var relation = invoke( this, currentRelationship ).resetQuery(); relation.addEagerConstraints( entities ); + relation.with( listRest( relationName, "." ) ); return relation.match( - relation.initRelation( entities, relationName ), + relation.initRelation( entities, currentRelationship ), relation.getEager(), - relationName + currentRelationship ); } diff --git a/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc b/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc index a8cc87bb..b8feea00 100644 --- a/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc +++ b/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc @@ -171,6 +171,43 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { var a = getInstance( "B" ).with( "a" ).get(); expect( getTickCount() - startTick ).toBeLT( 5000, "Query is taking too long" ); } ); + + it( "can eager load a nested relationship", function() { + var users = getInstance( "User" ).with( "posts.comments" ).latest().get(); + expect( users ).toBeArray(); + expect( users ).toHaveLength( 3, "Three users should be returned" ); + + var janedoe = users[ 1 ]; + expect( janedoe.getUsername() ).toBe( "janedoe" ); + expect( janedoe.getPosts() ).toBeArray(); + expect( janedoe.getPosts() ).toHaveLength( 0, "No posts should belong to janedoe" ); + + var johndoe = users[ 2 ]; + expect( johndoe.getUsername() ).toBe( "johndoe" ); + expect( johndoe.getPosts() ).toBeArray(); + expect( johndoe.getPosts() ).toHaveLength( 0, "No posts should belong to johndoe" ); + + var elpete = users[ 3 ]; + expect( elpete.getUsername() ).toBe( "elpete" ); + + var posts = elpete.getPosts(); + expect( posts ).toBeArray(); + expect( posts ).toHaveLength( 2, "Two posts should belong to elpete" ); + + expect( posts[ 1 ].getPost_Pk() ).toBe( 1245 ); + expect( posts[ 1 ].getComments() ).toBeArray(); + expect( posts[ 1 ].getComments() ).toHaveLength( 2 ); + expect( posts[ 1 ].getComments()[ 1 ].getId() ).toBe( 1 ); + expect( posts[ 1 ].getComments()[ 1 ].getBody() ).toBe( "I thought this post was great" ); + expect( posts[ 1 ].getComments()[ 2 ].getId() ).toBe( 2 ); + expect( posts[ 1 ].getComments()[ 2 ].getBody() ).toBe( "I thought this post was not so good" ); + + expect( posts[ 2 ].getPost_Pk() ).toBe( 523526 ); + expect( posts[ 2 ].getComments() ).toBeArray(); + expect( posts[ 2 ].getComments() ).toHaveLength( 0 ); + + expect( variables.queries ).toHaveLength( 3, "Only three queries should have been executed." ); + } ); } ); } From 66322d53ea2c55b555fd1e258446e58decdd4156 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 11 Apr 2019 20:26:36 -0400 Subject: [PATCH 65/70] feat(BaseEntity): Automatically casts column values Using a `casts` attribute, properties can be automatically casted to a different type. The only option currently supported is `boolean`. `boolean` will convert to/from a bit or tinyint value for the database. --- models/BaseEntity.cfc | 47 ++++++++++++++++--- tests/Application.cfc | 7 +++ tests/resources/app/models/PhoneNumber.cfc | 1 + .../BaseEntity/AttributeCastsSpec.cfc | 36 ++++++++++++++ .../integration/BaseEntity/NullValuesSpec.cfc | 4 +- 5 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 tests/specs/integration/BaseEntity/AttributeCastsSpec.cfc diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 73843b64..ff73f8bd 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -23,6 +23,7 @@ component accessors="true" { property name="_attributes" persistent="false"; property name="_meta" persistent="false"; property name="_nullValues" persistent="false"; + property name="_casts" persistent="false"; /*===================================== = Instance Data = @@ -51,6 +52,7 @@ component accessors="true" { param variables._relationshipsLoaded = {}; param variables._eagerLoad = []; param variables._nullValues = {}; + param variables._casts = {}; param variables._loaded = false; } @@ -185,9 +187,12 @@ component accessors="true" { } function retrieveAttribute( name, defaultValue = "" ) { - return variables._data.keyExists( retrieveColumnForAlias( name ) ) ? - variables._data[ retrieveColumnForAlias( name ) ] : - defaultValue; + return castValueForGetter( + name, + variables._data.keyExists( retrieveColumnForAlias( name ) ) ? + variables._data[ retrieveColumnForAlias( name ) ] : + defaultValue + ); } function assignAttribute( name, value ) { @@ -201,10 +206,10 @@ component accessors="true" { detail = isSimpleValue( value ) ? value : getMetadata( value ).fullname ); } - arguments.value = arguments.value.keyValue(); + arguments.value = castValueForSetter( name, arguments.value.keyValue() ); } - variables._data[ retrieveColumnForAlias( name ) ] = value; - variables[ retrieveAliasForColumn( name ) ] = value; + variables._data[ retrieveColumnForAlias( name ) ] = castValueForSetter( name, value ); + variables[ retrieveAliasForColumn( name ) ] = castValueForSetter( name, value ); return this; } @@ -887,6 +892,10 @@ component accessors="true" { if ( prop.convertToNull ) { variables._nullValues[ prop.name ] = prop.nullValue; } + param prop.casts = ""; + if ( prop.casts != "" ) { + variables._casts[ prop.name ] = prop.casts; + } if ( javacast( "boolean", prop.persistent ) ) { acc[ prop.name ] = prop.column; } @@ -1153,6 +1162,32 @@ component accessors="true" { compare( variables._nullValues[ retrieveAliasForColumn( key ) ], value ) == 0; } + private function castValueForGetter( key, value ) { + arguments.key = retrieveAliasForColumn( arguments.key ); + if ( ! structKeyExists( variables._casts, key ) ) { + return value; + } + switch ( variables._casts[ key ] ) { + case "boolean": + return javacast( "boolean", value ); + default: + return value; + } + } + + private function castValueForSetter( key, value ) { + arguments.key = retrieveAliasForColumn( arguments.key ); + if ( ! structKeyExists( variables._casts, key ) ) { + return value; + } + switch ( variables._casts[ key ] ) { + case "boolean": + return value ? 1 : 0; + default: + return value; + } + } + private function canUpdateAttribute( name ) { return ! variables._meta.properties.filter( function( property ) { return property.name == retrieveAliasForColumn( name ) && diff --git a/tests/Application.cfc b/tests/Application.cfc index 56a34632..d7aaf8b4 100644 --- a/tests/Application.cfc +++ b/tests/Application.cfc @@ -187,9 +187,16 @@ component { CREATE TABLE `phone_numbers` ( `id` int(11) NOT NULL AUTO_INCREMENT, `number` varchar(50), + `active` tinyint(1), PRIMARY KEY (`id`) ) " ); + queryExecute( " + INSERT INTO `phone_numbers` (`id`, `number`, `active`) VALUES (1, '323-232-3232', 1) + " ); + queryExecute( " + INSERT INTO `phone_numbers` (`id`, `number`, `active`) VALUES (2, '545-454-5454', 0) + " ); queryExecute( " CREATE TABLE `empty` ( `id` int(11) NOT NULL AUTO_INCREMENT, diff --git a/tests/resources/app/models/PhoneNumber.cfc b/tests/resources/app/models/PhoneNumber.cfc index d764fa4a..64505933 100644 --- a/tests/resources/app/models/PhoneNumber.cfc +++ b/tests/resources/app/models/PhoneNumber.cfc @@ -1,5 +1,6 @@ component extends="quick.models.BaseEntity" { property name="number" sqltype="cf_sql_varchar" convertToNull="false"; + property name="active" casts="boolean"; } diff --git a/tests/specs/integration/BaseEntity/AttributeCastsSpec.cfc b/tests/specs/integration/BaseEntity/AttributeCastsSpec.cfc new file mode 100644 index 00000000..fa60f5c8 --- /dev/null +++ b/tests/specs/integration/BaseEntity/AttributeCastsSpec.cfc @@ -0,0 +1,36 @@ +component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { + + function run() { + describe( "Attribute Casts Spec", function() { + it( "can specify a cast type for an attribute", function() { + var activeNumber = getInstance( "PhoneNumber" ).find( 1 ); + expect( activeNumber.getActive() ).toBe( true ); + expect( activeNumber.getActive() ).toBeBoolean(); + + var inactiveNumber = getInstance( "PhoneNumber" ).find( 2 ); + expect( inactiveNumber.getActive() ).toBe( false ); + expect( inactiveNumber.getActive() ).toBeBoolean(); + } ); + + it( "sets the value back for casts attributes", function() { + var newNumber = getInstance( "PhoneNumber" ); + newNumber.setNumber( "111-111-1111" ); + newNumber.setActive( true ); + newNumber.save(); + + var results = queryExecute( "SELECT * FROM `phone_numbers` WHERE `number` = ?", [ "111-111-1111" ] ); + expect( results ).toHaveLength( 1 ); + expect( results.active ).toBe( 1 ); + expect( results.active ).toBeNumeric(); + } ); + + it( "casts values in queries", function() { + var inactiveNumbers = getInstance( "PhoneNumber" ).where( "active", false ).get(); + expect( inactiveNumbers ).notToBeEmpty(); + expect( inactiveNumbers ).toHaveLength( 1 ); + expect( inactiveNumbers[ 1 ].getId() ).toBe( 2 ); + } ); + } ); + } + +} diff --git a/tests/specs/integration/BaseEntity/NullValuesSpec.cfc b/tests/specs/integration/BaseEntity/NullValuesSpec.cfc index 091a91ed..3db8c89b 100644 --- a/tests/specs/integration/BaseEntity/NullValuesSpec.cfc +++ b/tests/specs/integration/BaseEntity/NullValuesSpec.cfc @@ -17,10 +17,10 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { } ); it( "can set a column to not convert empty strings to null", function() { - expect( getInstance( "PhoneNumber" ).count() ).toBe( 0 ); + expect( getInstance( "PhoneNumber" ).count() ).toBe( 2 ); getInstance( "PhoneNumber" ).setNumber( "" ).save(); expect( getInstance( "PhoneNumber" ).whereNull( "number" ).count() ).toBe( 0 ); - expect( getInstance( "PhoneNumber" ).whereNotNull( "number" ).count() ).toBe( 1 ); + expect( getInstance( "PhoneNumber" ).whereNotNull( "number" ).count() ).toBe( 3 ); } ); it( "can choose a custom value to convert to nulls in the database", function() { From 195f8a6b1297d05b211d3eaefa453de563559644 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 11 Apr 2019 23:11:23 -0400 Subject: [PATCH 66/70] 2.0.0-beta.13 --- box.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/box.json b/box.json index 15021c90..9d58deab 100644 --- a/box.json +++ b/box.json @@ -1,8 +1,8 @@ { "name":"quick", - "version":"2.0.0-beta.12", + "version":"2.0.0-beta.13", "author":"", - "location":"coldbox-modules/quick#v2.0.0-beta.12", + "location":"coldbox-modules/quick#v2.0.0-beta.13", "homepage":"https://github.com/coldbox-modules/quick", "documentation":"https://github.com/coldbox-modules/quick", "repository":{ From 3537c1a0d854dc6a43eb444709c4a50b2b591b85 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 12 Apr 2019 11:00:57 -0400 Subject: [PATCH 67/70] feat(EagerLoading): Add constraints on an eager loaded relationship Constraints can be added to an eager loaded relationship using a special struct syntax instead of a string. The struct has the name of the relationship as the key and a function as the value. The function will be called with the relationship and can be constrained using the normal Quick methods. Nested eager loaded relationships can be specified using the `with` syntax in the constraint callback. --- models/BaseEntity.cfc | 15 +++++ .../Relationships/EagerLoadingSpec.cfc | 65 +++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index ff73f8bd..bc391177 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -689,8 +689,23 @@ component accessors="true" { } private function eagerLoadRelation( relationName, entities ) { + var callback = function() {}; + if ( ! isSimpleValue( relationName ) ) { + if ( ! isStruct( relationName ) ) { + throw( + type = "QuickInvalidEagerLoadParameter", + message = "Only strings or structs are supported eager load parameters. You passed [#serializeJSON( relationName )#" + ); + } + for ( var key in relationName ) { + callback = relationName[ key ]; + arguments.relationName = key; + break; + } + } var currentRelationship = listFirst( relationName, "." ); var relation = invoke( this, currentRelationship ).resetQuery(); + callback( relation ); relation.addEagerConstraints( entities ); relation.with( listRest( relationName, "." ) ); return relation.match( diff --git a/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc b/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc index b8feea00..b7927174 100644 --- a/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc +++ b/tests/specs/integration/BaseEntity/Relationships/EagerLoadingSpec.cfc @@ -208,6 +208,71 @@ component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { expect( variables.queries ).toHaveLength( 3, "Only three queries should have been executed." ); } ); + + it( "can constrain eager loading on a belongs to relationship", function() { + var users = getInstance( "User" ).with( { "posts" = function( query ) { + return query.where( "post_pk", "<", 7777 ); + } } ).latest().get(); + + expect( users ).toBeArray(); + expect( users ).toHaveLength( 3, "Three users should be returned" ); + + var janedoe = users[ 1 ]; + expect( janedoe.getUsername() ).toBe( "janedoe" ); + expect( janedoe.getPosts() ).toBeArray(); + expect( janedoe.getPosts() ).toHaveLength( 0, "No posts should belong to janedoe" ); + + var johndoe = users[ 2 ]; + expect( johndoe.getUsername() ).toBe( "johndoe" ); + expect( johndoe.getPosts() ).toBeArray(); + expect( johndoe.getPosts() ).toHaveLength( 0, "No posts should belong to johndoe" ); + + var elpete = users[ 3 ]; + expect( elpete.getUsername() ).toBe( "elpete" ); + expect( elpete.getPosts() ).toBeArray(); + expect( elpete.getPosts() ).toHaveLength( 1, "One post should belong to elpete" ); + + expect( variables.queries ).toHaveLength( 2, "Only two queries should have been executed." ); + } ); + + it( "can constrain an eager load on a nested relationship", function() { + var users = getInstance( "User" ).with( { "posts" = function( q1 ) { + return q1.with( { "comments" = function( q2 ) { + return q2.where( "body", "like", "%not%" ); + } } ); + } } ).latest().get(); + expect( users ).toBeArray(); + expect( users ).toHaveLength( 3, "Three users should be returned" ); + + var janedoe = users[ 1 ]; + expect( janedoe.getUsername() ).toBe( "janedoe" ); + expect( janedoe.getPosts() ).toBeArray(); + expect( janedoe.getPosts() ).toHaveLength( 0, "No posts should belong to janedoe" ); + + var johndoe = users[ 2 ]; + expect( johndoe.getUsername() ).toBe( "johndoe" ); + expect( johndoe.getPosts() ).toBeArray(); + expect( johndoe.getPosts() ).toHaveLength( 0, "No posts should belong to johndoe" ); + + var elpete = users[ 3 ]; + expect( elpete.getUsername() ).toBe( "elpete" ); + + var posts = elpete.getPosts(); + expect( posts ).toBeArray(); + expect( posts ).toHaveLength( 2, "Two posts should belong to elpete" ); + + expect( posts[ 1 ].getPost_Pk() ).toBe( 1245 ); + expect( posts[ 1 ].getComments() ).toBeArray(); + expect( posts[ 1 ].getComments() ).toHaveLength( 1, "One comment should belong to Post 1245" ); + expect( posts[ 1 ].getComments()[ 1 ].getId() ).toBe( 2 ); + expect( posts[ 1 ].getComments()[ 1 ].getBody() ).toBe( "I thought this post was not so good" ); + + expect( posts[ 2 ].getPost_Pk() ).toBe( 523526 ); + expect( posts[ 2 ].getComments() ).toBeArray(); + expect( posts[ 2 ].getComments() ).toHaveLength( 0 ); + + expect( variables.queries ).toHaveLength( 3, "Only three queries should have been executed." ); + } ); } ); } From 3ca65dc8f20d595066679265aa79a30bec842e18 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 12 Apr 2019 11:47:23 -0400 Subject: [PATCH 68/70] 2.0.0-beta.14 --- box.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/box.json b/box.json index 9d58deab..b3187b86 100644 --- a/box.json +++ b/box.json @@ -1,8 +1,8 @@ { "name":"quick", - "version":"2.0.0-beta.13", + "version":"2.0.0-beta.14", "author":"", - "location":"coldbox-modules/quick#v2.0.0-beta.13", + "location":"coldbox-modules/quick#v2.0.0-beta.14", "homepage":"https://github.com/coldbox-modules/quick", "documentation":"https://github.com/coldbox-modules/quick", "repository":{ From cde0503e889b434752425c3a05a2b65a7c9961ec Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Mon, 29 Apr 2019 18:14:36 -0600 Subject: [PATCH 69/70] refactor(BaseEntity): Remove unused groupBy utility --- models/BaseEntity.cfc | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index bc391177..9b116ff4 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -1048,22 +1048,6 @@ component accessors="true" { return false; } - public struct function groupBy( required array items, required string key, boolean forceLookup = false ) { - return items.reduce( function( acc, item ) { - if ( ( isObject( item ) && structKeyExists( item, "get#key#" ) ) || forceLookup ) { - var value = invoke( item, "get#key#" ); - } - else { - var value = item[ key ]; - } - if ( ! structKeyExists( acc, value ) ) { - acc[ value ] = []; - } - arrayAppend( acc[ value ], item ); - return acc; - }, {} ); - } - /*================================= = Read Only = =================================*/ From 8d62e6744c984f93712be1e811687826d9f0bad4 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Mon, 29 Apr 2019 19:15:02 -0600 Subject: [PATCH 70/70] fix(BaseEntity): Add better null value handling from the database --- models/BaseEntity.cfc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 9b116ff4..f13a0044 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -127,8 +127,8 @@ component accessors="true" { } attrs.each( function( key, value ) { - variables._data[ retrieveColumnForAlias( key ) ] = value; - variables[ retrieveAliasForColumn( key ) ] = value; + variables._data[ retrieveColumnForAlias( key ) ] = isNull( value ) ? javacast( "null", "" ) : value; + variables[ retrieveAliasForColumn( key ) ] = isNull( value ) ? javacast( "null", "" ) : value; } ); return this;