fix: Several fixes to store product endpoints, moved several test suites to HTTP (#7601)
* chore: Move publishable api key tests to HTTP * chore: Move store tests to HTTP folder * fix: Add tests for store products, fix several bugs around publishable keys
This commit is contained in:
1 parent
d104d1a256
commit
e44fe78b96
24 files changed
+2301
-2763
No files matched your search
@@ -153,6 +153,7 @@ export function medusaIntegrationTestRunner({
|
||||
let isFirstTime = true
|
||||
|
||||
const beforeAll_ = async () => {
|
||||
console.log(`Creating database ${dbName}`)
|
||||
await dbUtils.create(dbName)
|
||||
|
||||
try {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
|
||||
import { refetchApiKey } from "../helpers"
|
||||
import { AdminUpdateApiKeyType } from "../validators"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
|
||||
export const GET = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
@@ -20,6 +21,13 @@ export const GET = async (
|
||||
req.remoteQueryConfig.fields
|
||||
)
|
||||
|
||||
if (!apiKey) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`API Key with id: ${req.params.id} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
res.status(200).json({ api_key: apiKey })
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ export const defaultAdminApiKeyFields = [
|
||||
"redacted",
|
||||
"type",
|
||||
"last_used_at",
|
||||
"updated_at",
|
||||
"created_at",
|
||||
"created_by",
|
||||
"revoked_at",
|
||||
|
||||
@@ -11,7 +11,7 @@ export const AdminGetApiKeyParams = createSelectParams()
|
||||
export type AdminGetApiKeysParamsType = z.infer<typeof AdminGetApiKeysParams>
|
||||
export const AdminGetApiKeysParams = createFindParams({
|
||||
offset: 0,
|
||||
limit: 50,
|
||||
limit: 20,
|
||||
}).merge(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
remapProductResponse,
|
||||
} from "../helpers"
|
||||
import { AdminUpdateProductType } from "../validators"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
|
||||
export const GET = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
@@ -35,6 +36,9 @@ export const GET = async (
|
||||
})
|
||||
|
||||
const [product] = await remoteQuery(queryObject)
|
||||
if (!product) {
|
||||
throw new MedusaError(MedusaError.Types.NOT_FOUND, "Product not found")
|
||||
}
|
||||
|
||||
res.status(200).json({ product: remapProductResponse(product) })
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { isPresent } from "@medusajs/utils"
|
||||
import { MedusaRequest, MedusaResponse } from "../../../../types/routing"
|
||||
import { refetchProduct, wrapVariantsWithInventoryQuantity } from "../helpers"
|
||||
import { StoreGetProductsParamsType } from "../validators"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest<StoreGetProductsParamsType>,
|
||||
@@ -34,6 +35,13 @@ export const GET = async (
|
||||
req.remoteQueryConfig.fields
|
||||
)
|
||||
|
||||
if (!product) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Product with id: ${req.params.id} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
if (withInventoryQuantity) {
|
||||
await wrapVariantsWithInventoryQuantity(req, product.variants || [])
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
StoreGetProductsParams,
|
||||
StoreGetProductsParamsType,
|
||||
} from "./validators"
|
||||
import { applyParamsAsFilters } from "../../utils/middlewares/common/apply-params-as-filters"
|
||||
|
||||
export const storeProductRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
@@ -57,6 +58,7 @@ export const storeProductRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
StoreGetProductsParams,
|
||||
QueryConfig.retrieveProductQueryConfig
|
||||
),
|
||||
applyParamsAsFilters({ id: "id" }),
|
||||
filterByValidSalesChannels(),
|
||||
setContext({
|
||||
stock_location_id: maybeApplyStockLocationId,
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { NextFunction } from "express"
|
||||
import { MedusaRequest } from "../../../../types/routing"
|
||||
|
||||
export function applyParamsAsFilters<TFilter extends object>(mappings: {
|
||||
[param: string]: string
|
||||
}) {
|
||||
return async (req: MedusaRequest, _, next: NextFunction) => {
|
||||
for (const [param, paramValue] of Object.entries(req.params)) {
|
||||
if (mappings[param]) {
|
||||
req.filterableFields[mappings[param]] = paramValue
|
||||
}
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
}
|
||||
+54
-56
@@ -1,8 +1,13 @@
|
||||
import { isPresent, MedusaError } from "@medusajs/utils"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
import { NextFunction } from "express"
|
||||
import { AuthenticatedMedusaRequest } from "../../../../types/routing"
|
||||
import { refetchEntity } from "../../refetch-entity"
|
||||
import { arrayDifference } from "@medusajs/utils"
|
||||
|
||||
// Selection of sales channels happens in the following priority:
|
||||
// - If a publishable API key is passed, we take the sales channels attached to it and filter them down based on the query params
|
||||
// - If a sales channel id is passed through query params, we use that
|
||||
// - If not, we use the default sales channel for the store
|
||||
export function filterByValidSalesChannels() {
|
||||
return async (req: AuthenticatedMedusaRequest, _, next: NextFunction) => {
|
||||
const publishableApiKey = req.get("x-publishable-api-key")
|
||||
@@ -10,68 +15,61 @@ export function filterByValidSalesChannels() {
|
||||
| string[]
|
||||
| undefined
|
||||
|
||||
if (publishableApiKey) {
|
||||
const apiKey = await refetchEntity(
|
||||
"api_key",
|
||||
{ token: publishableApiKey },
|
||||
req.scope,
|
||||
["id", "sales_channels_link.sales_channel_id"]
|
||||
)
|
||||
|
||||
if (!apiKey) {
|
||||
return next(
|
||||
new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Publishable API key not found`
|
||||
)
|
||||
)
|
||||
}
|
||||
let result = apiKey.sales_channels_link.map(
|
||||
(link) => link.sales_channel_id
|
||||
)
|
||||
|
||||
if (salesChannelIds?.length) {
|
||||
// If all sales channel ids are not in the publishable key, we throw an error
|
||||
const uniqueInParams = arrayDifference(salesChannelIds, result)
|
||||
if (uniqueInParams.length) {
|
||||
return next(
|
||||
new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Requested sales channel is not part of the publishable key mappings`
|
||||
)
|
||||
)
|
||||
}
|
||||
result = salesChannelIds
|
||||
}
|
||||
|
||||
req.filterableFields.sales_channel_id = result
|
||||
return next()
|
||||
}
|
||||
|
||||
if (salesChannelIds?.length) {
|
||||
req.filterableFields.sales_channel_id = salesChannelIds
|
||||
return next()
|
||||
}
|
||||
|
||||
const store = await refetchEntity("stores", {}, req.scope, [
|
||||
"default_sales_channel_id",
|
||||
])
|
||||
|
||||
if (!store) {
|
||||
try {
|
||||
throw new MedusaError(MedusaError.Types.INVALID_DATA, `Store not found`)
|
||||
} catch (e) {
|
||||
return next(e)
|
||||
}
|
||||
}
|
||||
// Always set sales channels in the following priority
|
||||
// - any existing sales chennel ids passed through query params
|
||||
// - if none, we set the default sales channel
|
||||
req.filterableFields.sales_channel_id = salesChannelIds ?? [
|
||||
store.default_sales_channel_id,
|
||||
]
|
||||
|
||||
// Return early if no publishable keys are found
|
||||
if (!isPresent(publishableApiKey)) {
|
||||
return next()
|
||||
return next(
|
||||
new MedusaError(MedusaError.Types.INVALID_DATA, `Store not found`)
|
||||
)
|
||||
}
|
||||
|
||||
// When publishable keys are present, we fetch for all sales chennels attached
|
||||
// to the publishable key and validate the sales channel filter against it
|
||||
const apiKey = await refetchEntity(
|
||||
"api_key",
|
||||
{ token: publishableApiKey },
|
||||
req.scope,
|
||||
["id", "sales_channels_link.sales_channel_id"]
|
||||
)
|
||||
|
||||
if (!apiKey) {
|
||||
try {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Publishable API key not found`
|
||||
)
|
||||
} catch (e) {
|
||||
return next(e)
|
||||
}
|
||||
}
|
||||
|
||||
const validSalesChannelIds = apiKey.sales_channels_link.map(
|
||||
(link) => link.sales_channel_id
|
||||
)
|
||||
|
||||
const invalidSalesChannelIds = (salesChannelIds || []).filter(
|
||||
(id) => !validSalesChannelIds.includes(id)
|
||||
)
|
||||
|
||||
if (invalidSalesChannelIds.length) {
|
||||
try {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Invalid sales channel filters provided - ${invalidSalesChannelIds.join(
|
||||
", "
|
||||
)}`
|
||||
)
|
||||
} catch (e) {
|
||||
return next(e)
|
||||
}
|
||||
if (store.default_sales_channel_id) {
|
||||
req.filterableFields.sales_channel_id = [store.default_sales_channel_id]
|
||||
}
|
||||
|
||||
return next()
|
||||
|
||||
@@ -90,6 +90,17 @@
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"revoked_by": {
|
||||
"name": "revoked_by",
|
||||
"type": "text",
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20240604080145 extends Migration {
|
||||
|
||||
async up(): Promise<void> {
|
||||
this.addSql('alter table if exists "api_key" add column if not exists "updated_at" timestamptz not null default now();');
|
||||
}
|
||||
|
||||
async down(): Promise<void> {
|
||||
this.addSql('alter table if exists "api_key" drop column if exists "updated_at";');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -65,6 +65,14 @@ export default class ApiKey {
|
||||
})
|
||||
created_at: Date
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
onUpdate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
updated_at?: Date
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
revoked_by: string | null = null
|
||||
|
||||
|
||||
Reference in new issue
Block a user