Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 76 additions & 5 deletions models/BaseEntity.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -2960,11 +2960,13 @@ component accessors="true" {
message = 'This instance is missing `accessors="true"` in the component metadata. This is required for Quick to work properly. Please add it to your component metadata and reinit your application.'
);
}
meta[ "fullName" ] = meta.originalMetadata.fullname;
param meta.originalMetadata.mapping = listLast( meta.originalMetadata.fullname, "." );
meta[ "mapping" ] = meta.originalMetadata.mapping;
param meta.originalMetadata.entityName = listLast( meta.originalMetadata.name, "." );
meta[ "entityName" ] = meta.originalMetadata.entityName;
meta[ "fullName" ] = meta.originalMetadata.fullname;
param meta.originalMetadata.mapping = listLast( meta.originalMetadata.fullname, "." );
meta[ "mapping" ] = meta.originalMetadata.mapping;
param meta.originalMetadata.entityName = listLast( meta.originalMetadata.name, "." );
meta[ "entityName" ] = meta.originalMetadata.entityName;
param meta.localMetadata.properties = [];
guardDuplicatePropertyNames( meta.localMetadata, meta.mapping );
param meta.originalMetadata.table = variables._str.plural( variables._str.snake( meta.entityName ) );
meta[ "table" ] = meta.originalMetadata.table;
param meta.originalMetadata.readonly = false;
Expand Down Expand Up @@ -3212,6 +3214,75 @@ component accessors="true" {
return variables._meta.nonPersistentProperties.keyExists( arguments.name );
}

private void function guardDuplicatePropertyNames( required struct metadata, required string mapping ) {
var propertyNames = {};
var entityMapping = arguments.mapping;
arguments.metadata.properties.each( function( prop ) {
if ( propertyNames.keyExists( arguments.prop.name ) ) {
throwDuplicateProperty( entityMapping, arguments.prop.name );
}
propertyNames[ arguments.prop.name ] = true;
} );

// Some engines collapse duplicate declarations in component metadata. In
// that case, inspect the local component source when it is available.
if ( !arguments.metadata.keyExists( "path" ) || !fileExists( arguments.metadata.path ) ) {
return;
}

propertyNames = {};
var source = fileRead( arguments.metadata.path );
source = reReplace(
source,
"(?s)/[*].*?[*]/|<!---.*?--->",
" ",
"all"
);
source = reReplace( source, "(?m)//.*$", " ", "all" );
var propertyToken = chr( 60 ) & "cfproperty";
var declarations = reMatchNoCase( "(?is)(^|[^a-z0-9_])(property|#propertyToken#)\s[^;>]*", source );
declarations.each( function( declaration ) {
var nameAssignment = reFindNoCase(
"name\s*=\s*",
arguments.declaration,
1,
true
);
if ( nameAssignment.pos[ 1 ] == 0 ) {
return;
}
var valueStart = nameAssignment.pos[ 1 ] + nameAssignment.len[ 1 ];
var quote = mid( arguments.declaration, valueStart, 1 );
if ( quote != chr( 34 ) && quote != chr( 39 ) ) {
return;
}
var valueEnd = find(
quote,
arguments.declaration,
valueStart + 1
);
if ( valueEnd == 0 ) {
return;
}
var propertyName = mid(
arguments.declaration,
valueStart + 1,
valueEnd - valueStart - 1
);
if ( propertyNames.keyExists( propertyName ) ) {
throwDuplicateProperty( entityMapping, propertyName );
}
propertyNames[ propertyName ] = true;
} );
}

private void function throwDuplicateProperty( required string mapping, required string propertyName ) {
throw(
type = "QuickDuplicateProperty",
message = "[#arguments.mapping#] declares more than one property named [#arguments.propertyName#]. Property names must be unique."
);
}

private struct function generateCastsFromProperties( required array properties ) {
return arguments.properties.reduce( function( acc, prop ) {
if ( !arguments.prop.keyExists( "casts" ) || arguments.prop.casts == "" ) {
Expand Down
6 changes: 6 additions & 0 deletions tests/resources/app/models/AliasedUsernameUser.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
component extends="quick.models.BaseEntity" accessors="true" table="users" {

property name="id";
property name="username" column="first_name" sqltype="cf_sql_varchar";

}
14 changes: 14 additions & 0 deletions tests/resources/app/models/DuplicateUsernamePropertyUser.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
component
extends ="quick.models.BaseEntity"
accessors="true"
table ="users"
{

property name="id";
property
name ="username"
column ="first_name"
sqltype="cf_sql_varchar";
property name="username";

}
14 changes: 14 additions & 0 deletions tests/specs/integration/BaseEntity/AttributeSpec.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ component extends="tests.resources.ModuleIntegrationSpec" {
expect( entity.getActivoSN() ).toBeFalse();
} );

it( "rejects duplicate property names", function() {
expect( function() {
getInstance( "DuplicateUsernamePropertyUser" );
} ).toThrow();
} );

it( "can set a value to null using the `setColumnName` magic methods", function() {
var user = getInstance( "User" ).find( 1 );
expect( user.getUsername() ).toBe( "elpete" );
Expand Down Expand Up @@ -216,6 +222,14 @@ component extends="tests.resources.ModuleIntegrationSpec" {
} );
} );

it( "uses an explicit column when the property name is also a database column", function() {
var user = getInstance( "AliasedUsernameUser" ).findOrFail( 1 );

expect( user.getUsername() ).toBe( "Eric" );
expect( user.retrieveAttributesData() ).toHaveKey( "first_name" );
expect( user.retrieveAttributesData() ).notToHaveKey( "username" );
} );

// https://github.com/coldbox-modules/quick/issues/127
it( "can clear an attribute", () => {
var elpete = getInstance( "User" ).findOrFail( 1 );
Expand Down
Loading