Skip to content

[Bug]: .get() in /utils/flat/get.ts returns an object instead of id ([object Object]) #1766

@Overk1lls

Description

@Overk1lls

Contact Details

ov3rfordream@gmail.com

What happened?

I’m working with two entities - Product and ProductCategory - which have a standard many-to-many relationship.

Products:

@Entity({ name: 'products' })
export class ProductEntity extends BaseEntity {
  @PrimaryGeneratedColumn('uuid')
  id: UUID;

  // ...

  @ManyToMany(() => ProductCategory, { eager: true })
  @JoinTable({
    name: 'product_category_assignments',
    joinColumn: { name: 'product_id', referencedColumnName: 'id' },
    inverseJoinColumn: { name: 'category_id', referencedColumnName: 'id' },
  })
  categories: ProductCategory[];
}

Categories:

@Entity({ name: 'product_categories' })
export class ProductCategory extends BaseEntity {
  @PrimaryGeneratedColumn('uuid')
  id: UUID;

  // ...

  @ManyToMany(() => ProductEntity, (p) => p.categories)
  products: ProductEntity[];
}

This is a typical setup. I'm using AdminJS to expose this relationship in the UI.

On the list view, AdminJS correctly displays "Categories Length: 1" (or similar), which is great.
However, in the edit view for a product, the dropdown for categories fails to populate with the currently assigned categories. Inspecting the logs and network requests reveals that the frontend is sending [object Object] instead of category ID(s).

Looking into it, I found that the root cause is in the flat.get() function in /src/utils/flat/get.ts.

When AdminJS tries to extract the category reference using:
flat.get(record.params, 'categories.0')

It ends up returning the entire object:

{
  "id": "39dd081d-d609-400b-a6d2-49d6b13afb28",
  "categoryId": "9075acbf-9b55-11ee-bbe7-3cecef014aad",
  "parentId": "71bb1f59-3ff5-44c6-bcf8-fb335fcc16d7",
  "name": "name"
}

Instead of just returning the id as expected (string | undefined). The problematic line is:
return (flat.unflatten(nestedProperties) as Record<string, unknown>)[TEMP_HOLDING_KEY]

This leads to the dropdown breaking - the frontend serializes the object as [object Object], and the backend gets a malformed request that fails to resolve related data.

AdminJS resource options:

// product
{
  resource: ProductEntity,
  options: {
    titleProperty: 'id',
    properties: {
      id: {
        isId: true,
      },
      categories: {
        reference: ProductCategory.name,
        isArray: true,
        isVisible: {
          edit: true,
          filter: true,
          list: true,
          show: false,
        },
      },
    }
  }
}

// category
{
  resource: ProductCategory,
  options: {
    titleProperty: 'name',
    properties: {
      id: {
        isId: true,
      },
      // ...
    }
   // ...
  }
}

Sample of record.params (flattened):

// ...
  'categories.0.id': '39dd081d-d609-400b-a6d2-49d6b13afb28',
  'categories.0.categoryId': '9075acbf-9b55-11ee-bbe7-3cecef014aad',
  'categories.0.parentId': '71bb1f59-3ff5-44c6-bcf8-fb335fcc16d7',
  'categories.0.name': 'name',
  'categories.0.parent': undefined,
  'categories.0.children': undefined,
  'categories.0.products': undefined,
// ...

Expected:

The call to:
flat.get(record.params, 'categories.0')

Should return:
'39dd081d-d609-400b-a6d2-49d6b13afb28'

Instead of:

{
  id: '...',
  name: '...',
  ...
}

Bug prevalence

I encounter this bug consistently every time I try to use a many-to-many relationship (with isArray: true and reference) in the edit view. It's reproducible in a clean setup using AdminJS with TypeORM and a typical many-to-many relation.

AdminJS dependencies version

"@adminjs/design-system": "^4.1.1",
"@adminjs/express": "^6.1.1",
"@adminjs/nestjs": "^6.1.0",
"@adminjs/passwords": "^4.0.0",
"@adminjs/typeorm": "^5.0.1",
"adminjs": "^7.8.15",

What browsers do you see the problem on?

Chrome

Relevant log output

query: SELECT `ProductCategory`.`id` AS `ProductCategory_id`, `ProductCategory`.`category_id` AS `ProductCategory_category_id`, `ProductCategory`.`parent_id` AS `ProductCategory_parent_id`, `ProductCategory`.`name` AS `ProductCategory_name` FROM `product_categories` `ProductCategory` ORDER BY `ProductCategory`.`name` ASC LIMIT 50
query: SELECT `ProductCategory`.`id` AS `ProductCategory_id`, `ProductCategory`.`category_id` AS `ProductCategory_category_id`, `ProductCategory`.`parent_id` AS `ProductCategory_parent_id`, `ProductCategory`.`name` AS `ProductCategory_name` FROM `product_categories` `ProductCategory` WHERE ((`ProductCategory`.`id` = ?)) LIMIT 1 -- PARAMETERS: ["[object Object]"]

Relevant code that's giving you issues

// /utils/flat/get.ts
const get = (params: FlattenParams = {}, propertyPath?: string, options?: GetOptions): any => {
  if (!propertyPath) {
    return flat.unflatten(params)
  }

  // when object has this key - simply return it
  // we cannot rely on typeof params[propertyPath !== 'undefined' because params can actually be
  // undefined and in such case if would pass and function would return [undefined]
  if (Object.keys(params).find((key) => (key === propertyPath))) {
    return params[propertyPath]
  }

  const regex = propertyKeyRegex(propertyPath, options)
  const selectedParams = selectParams(params, propertyPath, options)

  const nestedProperties = Object.keys(selectedParams).reduce((memo, key, index) => {
    let newKey = key.replace(regex, `${TEMP_HOLDING_KEY}${DELIMITER}`)

    // when user wants to take allSiblings we have to fix the indexes so nested items from
    // different siblings don't overlap
    //
    // Example for key `nested.1.el`:
    //  'nested.0.el.0.value': 'val0.0',
    //  'nested.0.el.1.value': 'val0.1',
    //  'nested.1.el.0.value': 'val1',
    //  'nested.1.el.1.value': 'val2',
    //
    // has to be changed to:
    //  'TEMP_HOLDING_KEY.0.value': 'val0.0',
    //  'TEMP_HOLDING_KEY.1.value': 'val0.1',
    //  'TEMP_HOLDING_KEY.2.value': 'val1',
    //  'TEMP_HOLDING_KEY.3.value': 'val2',
    if (options?.includeAllSiblings) {
      newKey = newKey.replace(
        new RegExp(`${TEMP_HOLDING_KEY}\\${DELIMITER}(\\d*)`),
        `${TEMP_HOLDING_KEY}${DELIMITER}${index}`,
      )
    }

    memo[newKey] = selectedParams[key]

    return memo
  }, {} as FlattenParams)

  if (Object.keys(nestedProperties).length) {
    return (flat.unflatten(nestedProperties) as Record<string, unknown>)[TEMP_HOLDING_KEY]
  }
  return undefined
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions