Agrega el modulo @medusajs/medusa/auth con providers emailpass (explicito, para no perder el default al customizar el modulo) y google, leyendo GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET/GOOGLE_CALLBACK_URL desde commerce-secrets con required().
254 lines
8.3 KiB
TypeScript
254 lines
8.3 KiB
TypeScript
import { defineConfig, loadEnv } from "@medusajs/framework/utils"
|
|
|
|
loadEnv(process.env.NODE_ENV || "development", process.cwd())
|
|
|
|
const required = (name: string): string => {
|
|
const value = process.env[name]
|
|
if (!value) {
|
|
throw new Error(`La variable ${name} es obligatoria.`)
|
|
}
|
|
return value
|
|
}
|
|
|
|
// Mismos campos derivados de metadata/categories/collection/variants que
|
|
// mapProduct() en workloads/ecommerce/lib/headless/providers/medusa.ts --
|
|
// el documento indexado debe reflejar exactamente lo que el storefront
|
|
// ya muestra, no una vista distinta del catálogo.
|
|
function stringMetadata(
|
|
metadata: Record<string, unknown> | null | undefined,
|
|
key: string,
|
|
): string {
|
|
const value = metadata?.[key]
|
|
return typeof value === "string" ? value : ""
|
|
}
|
|
|
|
function booleanMetadata(
|
|
metadata: Record<string, unknown> | null | undefined,
|
|
key: string,
|
|
): boolean {
|
|
const value = metadata?.[key]
|
|
return value === true || value === "true"
|
|
}
|
|
|
|
async function meilisearchProductTransformer(
|
|
document: Record<string, unknown>,
|
|
) {
|
|
const metadata = (document.metadata ?? null) as Record<
|
|
string,
|
|
unknown
|
|
> | null
|
|
const categories = document.categories as
|
|
| Array<{ name?: string | null }>
|
|
| null
|
|
| undefined
|
|
const collection = document.collection as { title?: string | null } | null
|
|
const variants = document.variants as
|
|
| Array<{
|
|
inventory_quantity?: number | null
|
|
manage_inventory?: boolean | null
|
|
allow_backorder?: boolean | null
|
|
prices?: Array<{
|
|
amount?: number | null
|
|
currency_code?: string | null
|
|
}> | null
|
|
}>
|
|
| null
|
|
| undefined
|
|
const variant = variants?.[0]
|
|
const tags = document.tags as Array<{ value?: string | null }> | null
|
|
|
|
const category =
|
|
categories?.[0]?.name || stringMetadata(metadata, "category") || "Variedades"
|
|
|
|
const available =
|
|
!variant ||
|
|
variant.allow_backorder === true ||
|
|
variant.manage_inventory === false ||
|
|
(variant.inventory_quantity ?? 0) > 0
|
|
|
|
// variants.calculated_price requiere currency_code/region_id en el
|
|
// contexto de pricing, que el workflow de sync interno del plugin no
|
|
// provee -- hace fallar TODO el job de indexado (confirmado en vivo:
|
|
// "Method calculatePrices requires currency_code in the pricing
|
|
// context"). variants.prices es la lista cruda de precios (sin
|
|
// resolver promociones/reglas), no necesita contexto -- alcanza para
|
|
// el filtro/orden por precio en la busqueda, que no necesita el
|
|
// precio final promocional.
|
|
const copPrice = variant?.prices?.find(
|
|
(price) => price.currency_code?.toLowerCase() === "cop",
|
|
)
|
|
|
|
return {
|
|
id: document.id,
|
|
name: document.title || "Producto ARI Shopping",
|
|
shortDescription: document.subtitle || document.title || "",
|
|
description: document.description || "",
|
|
category,
|
|
collection: collection?.title || stringMetadata(metadata, "collection") || "",
|
|
brand: stringMetadata(metadata, "brand") || "ARI Shopping",
|
|
ageRange: stringMetadata(metadata, "ageRange") || "Todas",
|
|
tags: (tags || []).map((tag) => tag.value).filter(Boolean),
|
|
price: typeof copPrice?.amount === "number" ? copPrice.amount : null,
|
|
available,
|
|
isNew: booleanMetadata(metadata, "isNew"),
|
|
featured: booleanMetadata(metadata, "featured"),
|
|
}
|
|
}
|
|
|
|
module.exports = defineConfig({
|
|
projectConfig: {
|
|
databaseUrl: required("DATABASE_URL"),
|
|
redisUrl: required("REDIS_URL"),
|
|
workerMode: (process.env.MEDUSA_WORKER_MODE || "shared") as
|
|
| "shared"
|
|
| "worker"
|
|
| "server",
|
|
http: {
|
|
storeCors: required("STORE_CORS"),
|
|
adminCors: required("ADMIN_CORS"),
|
|
authCors: required("AUTH_CORS"),
|
|
jwtSecret: required("JWT_SECRET"),
|
|
cookieSecret: required("COOKIE_SECRET"),
|
|
},
|
|
// Todos los Ingress del lab terminan en Traefik por HTTP plano
|
|
// (entrypoint "web", sin TLS); Cloudflare es quien atiende HTTPS de
|
|
// cara al navegador. Con `secure: true` (el default en producción),
|
|
// express-session nunca emite Set-Cookie porque no detecta la
|
|
// conexión como HTTPS, y el dashboard admin queda con 401 en
|
|
// /admin/users/me pese a loguear bien. La API con Bearer token no se
|
|
// ve afectada por esto.
|
|
cookieOptions: {
|
|
secure: false,
|
|
},
|
|
},
|
|
|
|
admin: {
|
|
disable: process.env.DISABLE_MEDUSA_ADMIN === "true",
|
|
backendUrl:
|
|
process.env.MEDUSA_BACKEND_URL ||
|
|
"https://commerce.cruzcloud.net",
|
|
storefrontUrl:
|
|
process.env.STOREFRONT_URL ||
|
|
"https://shop.cruzcloud.net",
|
|
},
|
|
|
|
modules: [
|
|
{
|
|
resolve: "@medusajs/medusa/file",
|
|
options: {
|
|
providers: [
|
|
{
|
|
resolve: "@medusajs/medusa/file-s3",
|
|
id: "s3",
|
|
options: {
|
|
file_url: required("S3_FILE_URL"),
|
|
access_key_id: required("S3_ACCESS_KEY_ID"),
|
|
secret_access_key: required("S3_SECRET_ACCESS_KEY"),
|
|
region: process.env.S3_REGION || "us-east-1",
|
|
bucket: required("S3_BUCKET"),
|
|
endpoint: required("S3_ENDPOINT"),
|
|
cache_control:
|
|
"public, max-age=31536000, immutable",
|
|
additional_client_config: {
|
|
forcePathStyle: true,
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
resolve: "@medusajs/medusa/auth",
|
|
options: {
|
|
providers: [
|
|
// emailpass se declara explicito junto con google -- una vez
|
|
// que el modulo auth se customiza, la doc no deja claro si el
|
|
// provider default (emailpass) se sigue registrando solo.
|
|
{
|
|
resolve: "@medusajs/medusa/auth-emailpass",
|
|
id: "emailpass",
|
|
options: {},
|
|
},
|
|
{
|
|
resolve: "@medusajs/medusa/auth-google",
|
|
id: "google",
|
|
options: {
|
|
clientId: required("GOOGLE_CLIENT_ID"),
|
|
clientSecret: required("GOOGLE_CLIENT_SECRET"),
|
|
callbackUrl: required("GOOGLE_CALLBACK_URL"),
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
|
|
// ADR 0006 Etapa 2. Sin required(): a diferencia de S3/DB, Meilisearch
|
|
// es un servicio nuevo y todavia no probado en produccion en un host
|
|
// con historial de swap saturado -- si esta caido o mal configurado,
|
|
// el plugin se degrada solo (soporta enabled:false, "silently skipped"
|
|
// segun su propio README) en vez de tumbar el arranque de Medusa
|
|
// entero, que es lo que si haria required().
|
|
plugins: [
|
|
{
|
|
resolve: "@rokmohar/medusa-plugin-meilisearch",
|
|
options: {
|
|
config: {
|
|
host: process.env.MEILISEARCH_HOST ?? "",
|
|
apiKey: process.env.MEILISEARCH_API_KEY ?? "",
|
|
},
|
|
settings: {
|
|
products: {
|
|
type: "products",
|
|
enabled: Boolean(process.env.MEILISEARCH_HOST),
|
|
primaryKey: "id",
|
|
// Campos que el plugin pide via query.graph antes de pasarlos
|
|
// al transformer de arriba -- si falta alguno acá, llega
|
|
// undefined al transformer sin importar lo que este haga.
|
|
fields: [
|
|
"id",
|
|
"title",
|
|
"subtitle",
|
|
"description",
|
|
"handle",
|
|
"status",
|
|
"thumbnail",
|
|
"metadata",
|
|
"tags.value",
|
|
"categories.name",
|
|
"collection.title",
|
|
"variants.inventory_quantity",
|
|
"variants.manage_inventory",
|
|
"variants.allow_backorder",
|
|
"variants.prices.amount",
|
|
"variants.prices.currency_code",
|
|
],
|
|
indexSettings: {
|
|
// Tabla de ADR 0006 §3: Buscable / Filtrable-faceta / Ordenable.
|
|
searchableAttributes: [
|
|
"name",
|
|
"shortDescription",
|
|
"description",
|
|
"category",
|
|
"collection",
|
|
"brand",
|
|
"tags",
|
|
],
|
|
filterableAttributes: [
|
|
"category",
|
|
"collection",
|
|
"brand",
|
|
"ageRange",
|
|
"price",
|
|
"available",
|
|
],
|
|
sortableAttributes: ["price", "isNew", "featured"],
|
|
},
|
|
transformer: meilisearchProductTransformer,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
})
|