adminjs icon indicating copy to clipboard operation
adminjs copied to clipboard

[Bug]: Creating a new many-to-many relation does not create the junction record

Open chawes13 opened this issue 5 months ago • 1 comments

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


chawes13 avatar Aug 22 '25 13:08 chawes13

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',
            },
          }
}
]

AhmethanOzcan avatar Oct 09 '25 14:10 AhmethanOzcan