SoftwareBrothers / adminjs

AdminJS is an admin panel for apps written in node.js
https://adminjs.co
MIT License
8.07k stars 650 forks source link

[Bug]: @adminjs/relations adding existing item doesn't work with Fastify #1598

Closed Dauniusha closed 6 months ago

Dauniusha commented 7 months ago

What happened?

I've tried to link existing record in M:N relation, but I've got 500 error. In my case request was: query: http://localhost:4000/admin/api/resources/products/records/5618be40-68d8-4fa0-a1b6-454976baeb29/addManyToManyRelation?relation=occasions body: { "targetId": "79c8f408-6e46-4612-ab75-1fd1feacc41d" }

Bug prevalence

Permanently

AdminJS dependencies version

"dependencies": { "@adminjs/design-system": "^4.0.3", "@adminjs/fastify": "^4.0.1", "@adminjs/relations": "^1.0.0", "@adminjs/sql": "^2.2.1", "@aws-sdk/client-s3": "^3.474.0", "@aws-sdk/lib-storage": "^3.476.0", "@fastify/session": "^10.5.0", "adminjs": "^7.3.1", "connect-pg-simple": "^9.0.1", "env-cmd": "^10.1.0", "fastify": "^4.24.3", "tslib": "^2.6.2" }, "devDependencies": { "@types/connect-pg-simple": "^7.0.3" },

What browsers do you see the problem on?

Chrome

Relevant log output

{
    "statusCode": 500,
    "error": "Internal Server Error",
    "message": "Undefined binding(s) detected when compiling SELECT. Undefined column(s): [occasion_id] query: select * from \"public\".\"occasion_products\" where \"product_id\" = ? and \"occasion_id\" = ? order by \"product_id\" asc limit ?"
}

Relevant code that's giving you issues

import AdminJSFastify from '@adminjs/fastify';
import FastifySession from '@fastify/session';
import AdminJS from 'adminjs';
import Fastify from 'fastify';
import Connect from 'connect-pg-simple';
import { Adapter, Resource, Database } from '@adminjs/sql';
import { ProductOptions } from './products/product.admin.js';
import { OccasionOptions } from './occasions/occasion.admin.js';
import { componentLoader } from './component-loader.js';
import { ProductGalleryOptions } from './products-gallery/product-gallery.admin.js';
import {
  owningRelationSettingsFeature,
  targetRelationSettingsFeature,
  RelationType,
} from '@adminjs/relations';

AdminJS.registerAdapter({ Resource, Database });

// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
const ConnectSession = Connect(FastifySession as any);

const sessionStore = new ConnectSession({
  conObject: {
    connectionString: process.env.DATABASE_URL,
    ssl: process.env.NODE_ENV === 'production',
  },
  tableName: process.env.SESSIONS_TABLE,
  createTableIfMissing: true,
});

const DEFAULT_ADMIN = {
  email: 'admin@example.com',
  password: 'password',
};

const authenticate = async (
  email: string,
  password: string,
): Promise<{
  email: string;
  password: string;
}> => {
  if (email === DEFAULT_ADMIN.email && password === DEFAULT_ADMIN.password) {
    return Promise.resolve(DEFAULT_ADMIN);
  }
  return null;
};

const start = async (): Promise<void> => {
  const app = Fastify();

  const db = await new Adapter('postgresql', {
    connectionString: process.env.DATABASE_URL,
    database: process.env.DATABASE_NAME,
  }).init();

  const admin = new AdminJS({
    resources: [
      {
        resource: db.table('products'),
        options: ProductOptions,
        features: [
          owningRelationSettingsFeature({
            licenseKey: process.env.LICENSE_KEY,
            componentLoader,
            relations: {
              occasions: {
                type: RelationType.ManyToMany,
                junction: {
                  throughResourceId: 'OccasionProduct',
                  joinKey: 'product_id',
                  inverseJoinKey: 'occasion_id',
                },
                target: {
                  resourceId: 'Occasion',
                },
              },
              gallery: {
                type: RelationType.OneToMany,
                target: {
                  resourceId: 'ProductsGallery',
                  joinKey: 'product_id',
                },
              },
            },
          }),
        ],
      },
      {
        resource: db.table('occasions'),
        options: OccasionOptions,
        features: [targetRelationSettingsFeature()],
      },
      {
        resource: db.table('occasion_products'),
        options: { id: 'OccasionProduct' },
      },
      {
        resource: db.table('products_gallery'),
        options: ProductGalleryOptions,
        features: [targetRelationSettingsFeature()],
      },
    ],
    componentLoader,
    rootPath: '/admin',
    env: {
      IMAGES_PATH: `${process.env.IMAGES_PATH}`,
    },
  });

  const cookieSecret = process.env.COOKIE_SECRET;

  await AdminJSFastify.buildAuthenticatedRouter(
    admin,
    {
      authenticate,
      cookiePassword: cookieSecret,
      cookieName: 'adminjs',
    },
    app,
    {
      store: sessionStore,
      saveUninitialized: true,
      secret: cookieSecret,
      cookie: {
        httpOnly: process.env.NODE_ENV === 'production',
        secure: process.env.NODE_ENV === 'production',
      },
    },
  );

  const PORT = parseInt(process.env.ADMIN_PANEL_PORT);
  app.listen({ port: PORT }, (err) => {
    if (err) {
      console.error(err);
    } else {
      console.log(
        `AdminJS started on http://localhost:${PORT}${admin.options.rootPath}`,
      );
    }
  });
};

void start();
Dauniusha commented 7 months ago

As I understand, there is just a problem with parsing Fastify request by AdminJs router. As a result we've got payload: { targetId: undefined } object after Fastify route handler and broken request in the plugin hook as well.