[Bug]: Creating a new many-to-many relation does not create the junction record
Contact Details
No response
What happened?
From an association's page, create a new record.
E.g., User, Group, and GroupUser. From user, create a new group and automatically have that user associated to it through GroupUser.
My junction table does have a primary key and it is not a composite key.
Expected Result The new group is created, as well as an entry for the user.
Actual Result The new group is created, but no users are associated to it.
Misc.
I see the following in my url /admin/resources/Group/actions/new?user=2db1e0c7-85f1-4726-b856-c035d2cd5096&junctionResourceId=GroupUser&joinKey=user&inverseJoinKey=group&redirectUrl=http%3A%2F%2Flocalhost%3A3000%2Fadmin%2Fresources%2FUser%2Frecords%2F2db1e0c7-85f1-4726-b856-c035d2cd5096%2Fshow%3Ftab%3Dgroups
Bug prevalence
Always
AdminJS dependencies version
adminjs 7.5.12 @adminjs/relations 1.1.2 and 1.1.0 (pinned the latter as a result of https://github.com/SoftwareBrothers/adminjs/issues/1743) @adminjs/prisma 5.0.4
What browsers do you see the problem on?
Microsoft Edge, Chrome, Safari, Firefox
Relevant log output
[prisma]: Query: INSERT INTO "public"."Group" ("id","name","description") VALUES ($1,$2,$3) RETURNING "public"."Group"."id", "public"."Group"."name", "public"."Group"."description"
Relevant code that's giving you issues
Error happens because somehow when adminjs relations create an entity it does not return its id value for later on adding into junction table(check original response in after to see it as default). So a custom before and after solves the issue
// If creating from a many-to-many junction, store the info and remove junction params
if (request.query?.junctionResourceId) {
// Store junction info for after hook (including keys before we delete them)
request.junctionInfo = {
junctionResourceId: request.query.junctionResourceId,
joinKey: request.query.joinKey,
inverseJoinKey: request.query.inverseJoinKey,
[request.query.joinKey]: request.query[request.query.joinKey],
redirectUrl: request.query.redirectUrl,
};
// Remove junction-related query params to prevent automatic junction creation
delete request.query.junctionResourceId;
delete request.query.joinKey;
delete request.query.inverseJoinKey;
}
return request;
};
const customJunctionAfter = async (originalResponse, request, context) => {
if (request.junctionInfo && originalResponse.record) {
const { resource } = context;
const knex = (resource as any).knex;
delete originalResponse.record.params.id;
try {
// Find the newly created record by matching the unique fields
const newRecord = await knex(resource.id())
.select('id')
.where(originalResponse.record.params)
.orderBy('id', 'desc')
.first();
if (newRecord?.id) {
// Upsert the junction table entry (insert or update if exists)
await knex(request.junctionInfo.junctionResourceId)
.insert({
[request.junctionInfo.joinKey]: request.junctionInfo[request.junctionInfo.joinKey],
[request.junctionInfo.inverseJoinKey]: newRecord.id,
})
.onConflict([request.junctionInfo.joinKey, request.junctionInfo.inverseJoinKey])
.merge();
}
} catch (error) {
console.error('Error creating junction entry:', error);
}
}
return originalResponse;
};
...
{
resource: productionDB.table('application_networks'),
options: {
...options,
actions: {
new: {
before: [customJunctionBefore],
after: [customJunctionAfter],
},
},
properties: {
network_name: {
availableValues: [
{ value: 'meta', label: 'Meta' },
{ value: 'google', label: 'Google' },
{ value: 'tiktok', label: 'TikTok' },
],
},
},
},
features: [
targetRelationSettingsFeature(),
owningRelationSettingsFeature({
componentLoader,
licenseKey,
relations: {
pages: {
type: RelationType.ManyToMany,
junction: {
joinKey: 'application_network_id',
inverseJoinKey: 'page_id',
throughResourceId: 'pages_application_networks',
},
target: {
resourceId: 'pages',
},
},
},
}),
],
}
// in page
features: [
targetRelationSettingsFeature(),
owningRelationSettingsFeature({
componentLoader,
licenseKey,
relations: {
application_networks: {
type: RelationType.ManyToMany,
junction: {
joinKey: 'page_id',
inverseJoinKey: 'application_network_id',
throughResourceId: 'pages_application_networks',
},
target: {
resourceId: 'application_networks',
},
}
}
]