feat: add column introspection api (#13183)
This is part of a set of stacked PRs to add support for view configurations, which will allow users to customize the columns shown in admin tables. The functionality in this PR is behind the view_configuration feature flag. **What** - Adds an API to introspect the remote query schema and extract columns that users can add to their table views. **Notes** - This is a brute forced approach to get the functionality in place and I expect it to evolve to something more elegant over time. Some ideas for things we might want to consider: - Compute the entity columns during build time and store as static data the API can serve quickly. - Offer developers more control over how their data can be exposed in this API with additional decorators in the DML.
This commit is contained in:
255
integration-tests/http/__tests__/views/admin/columns.spec.ts
Normal file
255
integration-tests/http/__tests__/views/admin/columns.spec.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
||||
import { ModuleRegistrationName } from "@medusajs/utils"
|
||||
import {
|
||||
adminHeaders,
|
||||
createAdminUser,
|
||||
} from "../../../../helpers/create-admin-user"
|
||||
import { setupTaxStructure } from "../../../../modules/__tests__/fixtures"
|
||||
import { createOrderSeeder } from "../../fixtures/order"
|
||||
|
||||
jest.setTimeout(300000)
|
||||
|
||||
const env = { MEDUSA_FF_VIEW_CONFIGURATIONS: true }
|
||||
|
||||
medusaIntegrationTestRunner({
|
||||
env,
|
||||
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||
beforeEach(async () => {
|
||||
const container = getContainer()
|
||||
await setupTaxStructure(container.resolve(ModuleRegistrationName.TAX))
|
||||
await createAdminUser(dbConnection, adminHeaders, container)
|
||||
})
|
||||
|
||||
describe("GET /admin/views/:entity/columns", () => {
|
||||
describe("orders entity", () => {
|
||||
let order, seeder
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create an order with all relationships
|
||||
seeder = await createOrderSeeder({
|
||||
api,
|
||||
container: getContainer(),
|
||||
})
|
||||
order = seeder.order
|
||||
})
|
||||
|
||||
it("should return all order columns including relationships", async () => {
|
||||
const response = await api.get(
|
||||
`/admin/views/orders/columns`,
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.columns).toBeDefined()
|
||||
expect(Array.isArray(response.data.columns)).toBe(true)
|
||||
|
||||
// Check for direct fields
|
||||
const displayIdColumn = response.data.columns.find(
|
||||
(c) => c.id === "display_id"
|
||||
)
|
||||
expect(displayIdColumn).toBeDefined()
|
||||
expect(displayIdColumn).toMatchObject({
|
||||
id: "display_id",
|
||||
name: "Display Id",
|
||||
field: "display_id",
|
||||
data_type: "string",
|
||||
semantic_type: "identifier",
|
||||
context: "order",
|
||||
})
|
||||
|
||||
// Check for missing relationships
|
||||
const salesChannelColumns = response.data.columns.filter((c) =>
|
||||
c.id.startsWith("sales_channel")
|
||||
)
|
||||
|
||||
const orderExtraColumns = response.data.columns.filter((c) =>
|
||||
c.id.startsWith("order_extra")
|
||||
)
|
||||
|
||||
// Check for shipping address columns
|
||||
const shippingAddressColumns = response.data.columns.filter((c) =>
|
||||
c.id.startsWith("shipping_address")
|
||||
)
|
||||
|
||||
// Check that we DON'T have the sales_channel relationship object
|
||||
const salesChannelField = response.data.columns.find(
|
||||
(c) => c.id === "sales_channel"
|
||||
)
|
||||
expect(salesChannelField).toBeUndefined()
|
||||
|
||||
// Check that sales_channel.name is marked as default visible
|
||||
const salesChannelNameField = response.data.columns.find(
|
||||
(c) => c.id === "sales_channel.name"
|
||||
)
|
||||
expect(salesChannelNameField).toBeDefined()
|
||||
expect(salesChannelNameField).toMatchObject({
|
||||
id: "sales_channel.name",
|
||||
field: "sales_channel.name",
|
||||
default_visible: true,
|
||||
})
|
||||
|
||||
// Check that other sales_channel fields are NOT default visible
|
||||
const salesChannelIdField = response.data.columns.find(
|
||||
(c) => c.id === "sales_channel.id"
|
||||
)
|
||||
expect(salesChannelIdField).toBeDefined()
|
||||
expect(salesChannelIdField.default_visible).toBe(false)
|
||||
|
||||
const salesChannelDescriptionField = response.data.columns.find(
|
||||
(c) => c.id === "sales_channel.description"
|
||||
)
|
||||
if (salesChannelDescriptionField) {
|
||||
expect(salesChannelDescriptionField.default_visible).toBe(false)
|
||||
}
|
||||
|
||||
// Check that we have the customer_display computed column
|
||||
const customerDisplayField = response.data.columns.find(
|
||||
(c) => c.id === "customer_display"
|
||||
)
|
||||
expect(customerDisplayField).toBeDefined()
|
||||
expect(customerDisplayField).toMatchObject({
|
||||
id: "customer_display",
|
||||
name: "Customer",
|
||||
field: "customer_display",
|
||||
data_type: "string",
|
||||
semantic_type: "computed",
|
||||
context: "display",
|
||||
default_visible: true,
|
||||
sortable: false,
|
||||
computed: {
|
||||
type: "customer_name",
|
||||
required_fields: [
|
||||
"customer.first_name",
|
||||
"customer.last_name",
|
||||
"customer.email",
|
||||
],
|
||||
optional_fields: ["customer.phone"],
|
||||
},
|
||||
default_order: 300,
|
||||
category: "relationship",
|
||||
})
|
||||
|
||||
// Check that we have the country computed column
|
||||
const countryField = response.data.columns.find(
|
||||
(c) => c.id === "country"
|
||||
)
|
||||
expect(countryField).toBeDefined()
|
||||
expect(countryField).toMatchObject({
|
||||
id: "country",
|
||||
name: "Country",
|
||||
field: "country",
|
||||
data_type: "string",
|
||||
semantic_type: "computed",
|
||||
context: "display",
|
||||
default_visible: true,
|
||||
sortable: false,
|
||||
computed: {
|
||||
type: "country_code",
|
||||
required_fields: ["shipping_address.country_code"],
|
||||
optional_fields: [],
|
||||
},
|
||||
default_order: 800,
|
||||
category: "metadata",
|
||||
})
|
||||
|
||||
// Check that we DON'T have customer or shipping_address objects
|
||||
const customerField = response.data.columns.find(
|
||||
(c) => c.id === "customer"
|
||||
)
|
||||
expect(customerField).toBeUndefined()
|
||||
|
||||
const shippingAddressField = response.data.columns.find(
|
||||
(c) => c.id === "shipping_address"
|
||||
)
|
||||
expect(shippingAddressField).toBeUndefined()
|
||||
|
||||
// Group columns by type
|
||||
const directFields = response.data.columns.filter(
|
||||
(c) => !c.id.includes(".")
|
||||
)
|
||||
const relationshipFields = response.data.columns.filter((c) =>
|
||||
c.id.includes(".")
|
||||
)
|
||||
|
||||
// Check that important fields have proper ordering
|
||||
const displayIdField = response.data.columns.find(
|
||||
(c) => c.id === "display_id"
|
||||
)
|
||||
expect(displayIdField?.default_order).toBe(100)
|
||||
expect(displayIdField?.category).toBe("identifier")
|
||||
|
||||
const totalField = response.data.columns.find((c) => c.id === "total")
|
||||
expect(totalField?.default_order).toBe(700)
|
||||
expect(totalField?.category).toBe("metric")
|
||||
|
||||
const createdAtField = response.data.columns.find(
|
||||
(c) => c.id === "created_at"
|
||||
)
|
||||
expect(createdAtField?.default_order).toBe(200)
|
||||
expect(createdAtField?.category).toBe("timestamp")
|
||||
})
|
||||
|
||||
it("should check filtering behavior", async () => {
|
||||
const response = await api.get(
|
||||
`/admin/views/orders/columns`,
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
// Check which fields might be getting filtered
|
||||
const allFieldIds = response.data.columns.map((c) => c.id)
|
||||
|
||||
// Check for fields that might be filtered by suffixes
|
||||
const linkFields = allFieldIds.filter((id) => id.includes("_link"))
|
||||
expect(linkFields).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("products entity", () => {
|
||||
it("should return product columns", async () => {
|
||||
const response = await api.get(
|
||||
`/admin/views/products/columns`,
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.columns).toBeDefined()
|
||||
expect(Array.isArray(response.data.columns)).toBe(true)
|
||||
|
||||
// Check for product-specific fields
|
||||
const titleColumn = response.data.columns.find(
|
||||
(c) => c.id === "title"
|
||||
)
|
||||
expect(titleColumn).toBeDefined()
|
||||
expect(titleColumn).toMatchObject({
|
||||
id: "title",
|
||||
name: "Title",
|
||||
field: "title",
|
||||
default_visible: true,
|
||||
})
|
||||
|
||||
const handleColumn = response.data.columns.find(
|
||||
(c) => c.id === "handle"
|
||||
)
|
||||
expect(handleColumn).toBeDefined()
|
||||
expect(handleColumn).toMatchObject({
|
||||
id: "handle",
|
||||
name: "Handle",
|
||||
field: "handle",
|
||||
default_visible: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("unsupported entity", () => {
|
||||
it("should return 400 for unsupported entity", async () => {
|
||||
const response = await api
|
||||
.get(`/admin/views/unsupported-entity/columns`, adminHeaders)
|
||||
.catch((e) => e.response)
|
||||
|
||||
expect(response.status).toEqual(400)
|
||||
expect(response.data.message).toContain("Unsupported entity")
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user