From 259caae3d833f41807093bd01c21f73d77e90b78 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 22 Aug 2026 05:22:29 -0600 Subject: [PATCH 1/2] feat: create belongs-to-many related entities (#84) --- models/Relationships/BelongsToMany.cfc | 39 ++++++++++++++++--- ...8_11_102625_create_my_posts_tags_table.cfc | 9 +++-- .../Relationships/BelongsToManySpec.cfc | 21 ++++++++++ 3 files changed, 60 insertions(+), 9 deletions(-) diff --git a/models/Relationships/BelongsToMany.cfc b/models/Relationships/BelongsToMany.cfc index 6acb2d72..f129a59d 100644 --- a/models/Relationships/BelongsToMany.cfc +++ b/models/Relationships/BelongsToMany.cfc @@ -320,15 +320,41 @@ component } ); } + /** + * Creates a new related entity and attaches it to the parent through the pivot table. + * + * @attributes Attributes for the related entity. + * @pivotAttributes Additional attributes for the pivot row. + * @ignoreNonExistentAttributes Whether to ignore attributes not defined on the related entity. + * @options Options passed to the related entity save query. + * + * @return quick.models.BaseEntity + */ + public any function create( + struct attributes = {}, + struct pivotAttributes = {}, + boolean ignoreNonExistentAttributes = false, + struct options = {} + ) { + var entity = variables.related.create( + arguments.attributes, + arguments.ignoreNonExistentAttributes, + arguments.options + ); + attach( entity, arguments.pivotAttributes ); + return entity; + } + /** * Associates one or more ids of the related entity to the parent entity. * - * @id The id or array of ids of the related entity. + * @id The id or array of ids of the related entity. + * @pivotAttributes Additional attributes for each inserted pivot row. * * @return quick.models.BaseEntity */ - public any function attach( required any id ) { - variables.newPivotStatement().insert( parseIdsForInsert( arguments.id ) ); + public any function attach( required any id, struct pivotAttributes = {} ) { + variables.newPivotStatement().insert( parseIdsForInsert( arguments.id, arguments.pivotAttributes ) ); return variables.parent; } @@ -430,7 +456,8 @@ component * Normalizes a single id or entity or an array of ids and/or entities * in to an array of ids. * - * @value An id, entity, or combination of either in an array. + * @value An id, entity, or combination of either in an array. + * @pivotAttributes Additional attributes for each inserted pivot row. * * @doc_generic any * @return [any] @@ -455,10 +482,11 @@ component * @doc_generic any,any * @return [{any: any}] */ - public array function parseIdsForInsert( required any value ) { + public array function parseIdsForInsert( required any value, struct pivotAttributes = {} ) { var foreignPivotKeyValues = variables.parentKeys.map( function( parentKey ) { return variables.parent.retrieveAttribute( parentKey ); } ); + var additionalPivotAttributes = arguments.pivotAttributes; return arrayWrap( arguments.value ).map( function( values ) { // If the value is not a simple value, we will assume // it is an entity and return its key value. @@ -485,6 +513,7 @@ component insertRecord[ relatedPivotKey ] = val; } ); + insertRecord.append( additionalPivotAttributes, false ); return insertRecord; } ); } diff --git a/tests/resources/database/migrations/2020_08_11_102625_create_my_posts_tags_table.cfc b/tests/resources/database/migrations/2020_08_11_102625_create_my_posts_tags_table.cfc index ec8fca79..689b3778 100755 --- a/tests/resources/database/migrations/2020_08_11_102625_create_my_posts_tags_table.cfc +++ b/tests/resources/database/migrations/2020_08_11_102625_create_my_posts_tags_table.cfc @@ -1,10 +1,11 @@ component { function up( schema, qb ) { - schema.create( "my_posts_tags", function( t ) { - t.unsignedInteger( "custom_post_pk" ); - t.unsignedInteger( "tag_id" ); - t.primaryKey( [ "custom_post_pk", "tag_id" ] ); + schema.create( "my_posts_tags", function( t ) { + t.unsignedInteger( "custom_post_pk" ); + t.unsignedInteger( "tag_id" ); + t.string( "context" ).nullable(); + t.primaryKey( [ "custom_post_pk", "tag_id" ] ); } ); qb.table( "my_posts_tags" ) diff --git a/tests/specs/integration/BaseEntity/Relationships/BelongsToManySpec.cfc b/tests/specs/integration/BaseEntity/Relationships/BelongsToManySpec.cfc index e1801fde..25af60cb 100644 --- a/tests/specs/integration/BaseEntity/Relationships/BelongsToManySpec.cfc +++ b/tests/specs/integration/BaseEntity/Relationships/BelongsToManySpec.cfc @@ -19,6 +19,27 @@ component extends="tests.resources.ModuleIntegrationSpec" { expect( posts ).toBeArray(); expect( posts ).toHaveLength( 2 ); } ); + + it( "creates and attaches a related entity", function() { + var post = getInstance( "Post" ).findOrFail( 1245 ); + var tag = post.tags().create( { "name" : "testing" }, { "context" : "created through relationship" } ); + + expect( tag ).toBeInstanceOf( "Tag" ); + expect( tag.isLoaded() ).toBeTrue(); + expect( + post + .tags() + .where( "id", tag.getId() ) + .exists() + ).toBeTrue(); + expect( + post.tags() + .newPivotStatement() + .where( "custom_post_pk", post.getPost_Pk() ) + .where( "tag_id", tag.getId() ) + .value( "context" ) + ).toBe( "created through relationship" ); + } ); } ); } From c3c8ff42321a1f1e66854400b2a9ab28f583ffcc Mon Sep 17 00:00:00 2001 From: elpete <2583646+elpete@users.noreply.github.com> Date: Sat, 22 Aug 2026 11:24:43 +0000 Subject: [PATCH 2/2] Apply cfformat changes --- .../integration/BaseEntity/Relationships/BelongsToManySpec.cfc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/specs/integration/BaseEntity/Relationships/BelongsToManySpec.cfc b/tests/specs/integration/BaseEntity/Relationships/BelongsToManySpec.cfc index 25af60cb..de5ae5d4 100644 --- a/tests/specs/integration/BaseEntity/Relationships/BelongsToManySpec.cfc +++ b/tests/specs/integration/BaseEntity/Relationships/BelongsToManySpec.cfc @@ -27,8 +27,7 @@ component extends="tests.resources.ModuleIntegrationSpec" { expect( tag ).toBeInstanceOf( "Tag" ); expect( tag.isLoaded() ).toBeTrue(); expect( - post - .tags() + post.tags() .where( "id", tag.getId() ) .exists() ).toBeTrue();