@open-mercato/core 0.4.2-canary-5035717565 → 0.4.2-canary-802e036384

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -49,7 +49,7 @@ function resolveApiDocsBaseUrl() {
49
49
  if (apiOverride) {
50
50
  return apiOverride;
51
51
  }
52
- const appBase = process.env.NEXT_PUBLIC_APP_URL || process.env.APP_URL || "http://localhost:3000";
52
+ const appBase = process.env.NEXT_PUBLIC_APP_URL || process.env.APP_URL || "http://localhost:5050";
53
53
  return appendApiSegment(appBase);
54
54
  }
55
55
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/api_docs/lib/resources.ts"],
4
- "sourcesContent": ["export type ApiDocResource = {\n label: string\n description: string\n href: string\n external?: boolean\n actionLabel?: string\n}\n\nexport function getApiDocsResources(): ApiDocResource[] {\n return [\n {\n label: 'OpenAPI Explorer',\n description: 'Interactive HTML viewer with endpoint details, payload schemas, and sample requests.',\n href: '/docs/api',\n actionLabel: 'Open explorer',\n },\n {\n label: 'Official documentation',\n description: 'Guides and tutorials covering setup, modules, and customization.',\n href: 'https://docs.openmercato.com/',\n external: true,\n actionLabel: 'Open docs',\n },\n {\n label: 'OpenAPI JSON',\n description: 'Machine-readable OpenAPI 3.1 document for integrating with the platform.',\n href: '/api/docs/openapi',\n actionLabel: 'Download JSON',\n },\n {\n label: 'OpenAPI Markdown',\n description: 'Human-friendly Markdown rendering of the current OpenAPI document.',\n href: '/api/docs/markdown',\n actionLabel: 'Download Markdown',\n },\n ]\n}\n\nfunction appendApiSegment(baseUrl: string): string {\n try {\n const url = new URL(baseUrl)\n const normalizedPath = url.pathname.replace(/\\/+$/, '')\n if (!normalizedPath || normalizedPath === '/') {\n url.pathname = '/api'\n } else if (!normalizedPath.endsWith('/api')) {\n url.pathname = `${normalizedPath}/api`\n } else {\n url.pathname = normalizedPath\n }\n return url.toString()\n } catch {\n const normalized = baseUrl.replace(/\\/+$/, '')\n return normalized.endsWith('/api') ? normalized : `${normalized}/api`\n }\n}\n\nexport function resolveApiDocsBaseUrl(): string {\n const apiOverride = process.env.NEXT_PUBLIC_API_BASE_URL\n if (apiOverride) {\n return apiOverride\n }\n\n const appBase =\n process.env.NEXT_PUBLIC_APP_URL ||\n process.env.APP_URL ||\n 'http://localhost:3000'\n\n return appendApiSegment(appBase)\n}\n"],
4
+ "sourcesContent": ["export type ApiDocResource = {\n label: string\n description: string\n href: string\n external?: boolean\n actionLabel?: string\n}\n\nexport function getApiDocsResources(): ApiDocResource[] {\n return [\n {\n label: 'OpenAPI Explorer',\n description: 'Interactive HTML viewer with endpoint details, payload schemas, and sample requests.',\n href: '/docs/api',\n actionLabel: 'Open explorer',\n },\n {\n label: 'Official documentation',\n description: 'Guides and tutorials covering setup, modules, and customization.',\n href: 'https://docs.openmercato.com/',\n external: true,\n actionLabel: 'Open docs',\n },\n {\n label: 'OpenAPI JSON',\n description: 'Machine-readable OpenAPI 3.1 document for integrating with the platform.',\n href: '/api/docs/openapi',\n actionLabel: 'Download JSON',\n },\n {\n label: 'OpenAPI Markdown',\n description: 'Human-friendly Markdown rendering of the current OpenAPI document.',\n href: '/api/docs/markdown',\n actionLabel: 'Download Markdown',\n },\n ]\n}\n\nfunction appendApiSegment(baseUrl: string): string {\n try {\n const url = new URL(baseUrl)\n const normalizedPath = url.pathname.replace(/\\/+$/, '')\n if (!normalizedPath || normalizedPath === '/') {\n url.pathname = '/api'\n } else if (!normalizedPath.endsWith('/api')) {\n url.pathname = `${normalizedPath}/api`\n } else {\n url.pathname = normalizedPath\n }\n return url.toString()\n } catch {\n const normalized = baseUrl.replace(/\\/+$/, '')\n return normalized.endsWith('/api') ? normalized : `${normalized}/api`\n }\n}\n\nexport function resolveApiDocsBaseUrl(): string {\n const apiOverride = process.env.NEXT_PUBLIC_API_BASE_URL\n if (apiOverride) {\n return apiOverride\n }\n\n const appBase =\n process.env.NEXT_PUBLIC_APP_URL ||\n process.env.APP_URL ||\n 'http://localhost:5050'\n\n return appendApiSegment(appBase)\n}\n"],
5
5
  "mappings": "AAQO,SAAS,sBAAwC;AACtD,SAAO;AAAA,IACL;AAAA,MACE,OAAO;AAAA,MACP,aAAa;AAAA,MACb,MAAM;AAAA,MACN,aAAa;AAAA,IACf;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,aAAa;AAAA,MACb,MAAM;AAAA,MACN,UAAU;AAAA,MACV,aAAa;AAAA,IACf;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,aAAa;AAAA,MACb,MAAM;AAAA,MACN,aAAa;AAAA,IACf;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,aAAa;AAAA,MACb,MAAM;AAAA,MACN,aAAa;AAAA,IACf;AAAA,EACF;AACF;AAEA,SAAS,iBAAiB,SAAyB;AACjD,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,OAAO;AAC3B,UAAM,iBAAiB,IAAI,SAAS,QAAQ,QAAQ,EAAE;AACtD,QAAI,CAAC,kBAAkB,mBAAmB,KAAK;AAC7C,UAAI,WAAW;AAAA,IACjB,WAAW,CAAC,eAAe,SAAS,MAAM,GAAG;AAC3C,UAAI,WAAW,GAAG,cAAc;AAAA,IAClC,OAAO;AACL,UAAI,WAAW;AAAA,IACjB;AACA,WAAO,IAAI,SAAS;AAAA,EACtB,QAAQ;AACN,UAAM,aAAa,QAAQ,QAAQ,QAAQ,EAAE;AAC7C,WAAO,WAAW,SAAS,MAAM,IAAI,aAAa,GAAG,UAAU;AAAA,EACjE;AACF;AAEO,SAAS,wBAAgC;AAC9C,QAAM,cAAc,QAAQ,IAAI;AAChC,MAAI,aAAa;AACf,WAAO;AAAA,EACT;AAEA,QAAM,UACJ,QAAQ,IAAI,uBACZ,QAAQ,IAAI,WACZ;AAEF,SAAO,iBAAiB,OAAO;AACjC;",
6
6
  "names": []
7
7
  }
@@ -198,7 +198,7 @@ function RecordsPage({ params }) {
198
198
  /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
199
199
  /* @__PURE__ */ jsxs("div", { children: [
200
200
  /* @__PURE__ */ jsx("div", { className: "font-medium mb-1", children: "1) Configure environment variables" }),
201
- /* @__PURE__ */ jsx("pre", { className: "bg-muted p-3 rounded text-xs overflow-auto", children: /* @__PURE__ */ jsx("code", { children: `export BASE_URL="http://localhost:3000/api"
201
+ /* @__PURE__ */ jsx("pre", { className: "bg-muted p-3 rounded text-xs overflow-auto", children: /* @__PURE__ */ jsx("code", { children: `export BASE_URL="http://localhost:5050/api"
202
202
  export API_KEY="<paste API key secret here>" # scoped with entities.features
203
203
  export ENTITY_ID="${entityId}"
204
204
  export RECORD_ID="<record uuid>"` }) }),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../../src/modules/entities/backend/entities/user/%5BentityId%5D/records/page.tsx"],
4
- "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useSearchParams } from 'next/navigation'\nimport type { ColumnDef, SortingState } from '@tanstack/react-table'\nimport { filterCustomFieldDefs, useCustomFieldDefs } from '@open-mercato/ui/backend/utils/customFieldDefs'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable, type DataTableExportFormat } from '@open-mercato/ui/backend/DataTable'\nimport type { PreparedExport } from '@open-mercato/shared/lib/crud/exporters'\nimport type { FilterDef } from '@open-mercato/ui/backend/FilterBar'\nimport { ContextHelp } from '@open-mercato/ui/backend/ContextHelp'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport Link from 'next/link'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\n\ntype RecordsResponse = {\n items: any[]\n total: number\n page: number\n pageSize: number\n totalPages: number\n}\n\nfunction toCsvUrl(base: string, params: URLSearchParams) {\n // Build a relative URL to avoid SSR/CSR origin mismatch hydration issues\n const p = new URLSearchParams(params)\n p.set('format', 'csv')\n const qs = p.toString()\n return qs ? `${base}?${qs}` : base\n}\n\nfunction normalizeCell(v: any): string {\n if (Array.isArray(v)) return v.filter((x) => x != null && x !== '').join(', ')\n if (v === true) return 'Yes'\n if (v === false) return 'No'\n if (v == null) return ''\n if (v instanceof Date) return v.toISOString()\n return String(v)\n}\n\nexport default function RecordsPage({ params }: { params: { entityId?: string } }) {\n const entityId = decodeURIComponent(params?.entityId || '')\n const [sorting, setSorting] = React.useState<SortingState>([{ id: 'id', desc: false }])\n const [page, setPage] = React.useState(1)\n const [pageSize, setPageSize] = React.useState(50)\n const [search, setSearch] = React.useState('')\n const [filterValues, setFilterValues] = React.useState<Record<string, any>>({})\n const [columns, setColumns] = React.useState<ColumnDef<any>[]>([])\n const [rawData, setRawData] = React.useState<any[]>([])\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(false)\n const scopeVersion = useOrganizationScopeVersion()\n const { data: cfDefs = [] } = useCustomFieldDefs(entityId, {\n enabled: Boolean(entityId),\n keyExtras: [scopeVersion],\n })\n\n // Fetch records whenever paging/sorting/filters change (do NOT refetch on cfDefs/search changes)\n React.useEffect(() => {\n let cancelled = false\n const run = async () => {\n setLoading(true)\n try {\n const params = new URLSearchParams()\n params.set('entityId', entityId)\n params.set('page', String(page))\n params.set('pageSize', String(pageSize))\n const s = sorting?.[0]\n if (s?.id) {\n params.set('sortField', String(s.id))\n params.set('sortDir', s.desc ? 'desc' : 'asc')\n }\n // Flatten filter values into query params\n for (const [k, v] of Object.entries(filterValues)) {\n if (v == null) continue\n if (Array.isArray(v)) {\n if (v.length) params.set(k, v.join(','))\n } else if (typeof v === 'object') {\n // dateRange-like shapes are not supported generically here; skip\n } else {\n params.set(k, String(v))\n }\n }\n const j = await readApiResultOrThrow<RecordsResponse>(\n `/api/entities/records?${params.toString()}`,\n undefined,\n {\n errorMessage: 'Failed to load records',\n fallback: {\n items: [],\n total: 0,\n page,\n pageSize,\n totalPages: 1,\n },\n },\n )\n if (!cancelled) {\n setRawData(j.items || [])\n setTotal(j.total)\n setTotalPages(j.totalPages)\n }\n } catch (e) {\n if (!cancelled) {\n setRawData([])\n setTotal(0)\n setTotalPages(1)\n }\n } finally {\n if (!cancelled) setLoading(false)\n }\n }\n if (entityId) run()\n return () => { cancelled = true }\n }, [entityId, page, pageSize, sorting, filterValues, scopeVersion])\n\n // Build columns from custom field definitions only (no data round-trip)\n React.useEffect(() => {\n const visibleDefs = filterCustomFieldDefs(cfDefs, 'list') as any\n const maxVisible = 10\n const cols: ColumnDef<any>[] = visibleDefs.map((d: any, idx: number) => ({\n accessorKey: d.key,\n header: d.label || d.key,\n meta: { priority: idx < 4 ? 1 : idx < 6 ? 2 : idx < 8 ? 3 : idx < maxVisible ? 4 : 5 },\n cell: ({ getValue }: { getValue: () => unknown }) => {\n const v = getValue() as any\n return <span className=\"truncate max-w-[24ch] inline-block align-top\" title={normalizeCell(v)}>{normalizeCell(v)}</span>\n },\n }))\n // Ensure hidden 'id' column exists for sorting/state\n const hasIdCol = cols.some((c) => (c as any).accessorKey === 'id' || (c as any).id === 'id')\n if (!hasIdCol) cols.unshift({ accessorKey: 'id', header: 'ID', meta: { hidden: true, priority: 6 } } as any)\n setColumns(cols)\n }, [cfDefs])\n\n // Client-side quick search filtering without triggering server refetch\n const data = React.useMemo(() => {\n if (!search.trim()) return rawData\n const q = search.trim().toLowerCase()\n return (rawData || []).filter((row: any) => {\n const values = Object.values(row || {})\n return values.some((v) => normalizeCell(v).toLowerCase().includes(q))\n })\n }, [rawData, search])\n\n const viewExportColumns = React.useMemo(() => {\n return (columns || [])\n .map((col) => {\n const accessorKey = (col as any).accessorKey\n if (!accessorKey || typeof accessorKey !== 'string') return null\n if ((col as any).meta?.hidden) return null\n const header = typeof col.header === 'string'\n ? col.header\n : accessorKey.startsWith('cf_')\n ? accessorKey.slice(3)\n : accessorKey\n return { field: accessorKey, header }\n })\n .filter((col): col is { field: string; header: string } => !!col)\n }, [columns])\n\n const buildFullExportUrl = React.useCallback((format: DataTableExportFormat) => {\n const qp = new URLSearchParams({\n entityId,\n format,\n exportScope: 'full',\n all: 'true',\n })\n const sort = sorting?.[0]\n if (sort?.id) {\n qp.set('sortField', String(sort.id))\n qp.set('sortDir', sort.desc ? 'desc' : 'asc')\n }\n return `/api/entities/records?${qp.toString()}`\n }, [entityId, sorting])\n\n const exportConfig = React.useMemo(() => {\n const safeEntityId = entityId.replace(/[^a-z0-9_-]/gi, '_') || 'records'\n return {\n view: {\n description: 'Exports the current list respecting filters and column visibility.',\n prepare: async (): Promise<{ prepared: PreparedExport; filename: string }> => {\n const rowsForExport = data.map((row) => {\n const out: Record<string, unknown> = {}\n for (const col of viewExportColumns) {\n out[col.field] = (row as Record<string, unknown>)[col.field]\n }\n return out\n })\n const prepared: PreparedExport = {\n columns: viewExportColumns.map((col) => ({ field: col.field, header: col.header })),\n rows: rowsForExport,\n }\n return { prepared, filename: `${safeEntityId}_view` }\n },\n },\n full: {\n description: 'Exports raw records with every field and custom field included.',\n getUrl: (format: DataTableExportFormat) => buildFullExportUrl(format),\n filename: () => `${safeEntityId}_full`,\n },\n }\n }, [buildFullExportUrl, data, entityId, viewExportColumns])\n\n const hasAnyFormFields = React.useMemo(() => filterCustomFieldDefs(cfDefs, 'form').length > 0, [cfDefs])\n const actions = (\n <>\n <Button asChild variant=\"outline\" size=\"sm\">\n <Link href={`/backend/entities/user/${encodeURIComponent(entityId)}`}>\n Edit Entity Definition\n </Link>\n </Button>\n {hasAnyFormFields && (\n <Button asChild>\n <Link href={`/backend/entities/user/${encodeURIComponent(entityId)}/records/create`}>\n Create\n </Link>\n </Button>\n )}\n </>\n )\n\n // Ensure filters are visible even if no custom fields are marked filterable\n const baseFilters: FilterDef[] = React.useMemo(() => ([\n { id: 'id', label: 'ID', type: 'text' },\n ]), [])\n\n return (\n <Page>\n <PageBody>\n <ContextHelp bulb title=\"API: Manage Records via cURL\" className=\"mb-4\">\n <p className=\"mb-2\">\n Interact with this custom entity via the backend API using cURL. Use API keys for machine-to-machine access\u2014mint one from the{' '}\n <a className=\"underline\" target=\"_blank\" rel=\"noreferrer\" href=\"https://docs.openmercato.com/user-guide/api-keys\">\n Managing API keys guide\n </a>{' '}\n or the{' '}\n <a className=\"underline\" target=\"_blank\" rel=\"noreferrer\" href=\"https://docs.openmercato.com/cli/api-keys\">\n API keys CLI documentation\n </a>{' '}\n before running these calls.\n </p>\n <div className=\"space-y-2\">\n <div>\n <div className=\"font-medium mb-1\">1) Configure environment variables</div>\n <pre className=\"bg-muted p-3 rounded text-xs overflow-auto\"><code>{`export BASE_URL=\"http://localhost:3000/api\"\nexport API_KEY=\"<paste API key secret here>\" # scoped with entities.features\nexport ENTITY_ID=\"${entityId}\"\nexport RECORD_ID=\"<record uuid>\"`}</code></pre>\n <p className=\"text-muted-foreground mt-1\">\n Need a new key? Follow the{' '}\n <a className=\"underline\" target=\"_blank\" rel=\"noreferrer\" href=\"https://docs.openmercato.com/user-guide/api-keys\">\n Managing API keys\n </a>{' '}\n walkthrough or mint one via{' '}\n <a className=\"underline\" target=\"_blank\" rel=\"noreferrer\" href=\"https://docs.openmercato.com/cli/api-keys\">\n mercato api_keys add\n </a>\n .\n </p>\n </div>\n\n <div>\n <div className=\"font-medium mb-1\">2) List records</div>\n <pre className=\"bg-muted p-3 rounded text-xs overflow-auto\"><code>{`curl -s -H \"X-Api-Key: $API_KEY\" \\\n \"$BASE_URL/entities/records?entityId=$ENTITY_ID\" | jq`}</code></pre>\n </div>\n\n <div>\n <div className=\"font-medium mb-1\">3) Read a single record (by id)</div>\n <pre className=\"bg-muted p-3 rounded text-xs overflow-auto\"><code>{`curl -s -H \"X-Api-Key: $API_KEY\" \\\n \"$BASE_URL/entities/records?entityId=$ENTITY_ID&id=$RECORD_ID\" | jq`}</code></pre>\n <p className=\"text-muted-foreground mt-1\">Note: Response is a list; filter by <code>id</code> to get a single item.</p>\n </div>\n\n <div>\n <div className=\"font-medium mb-1\">4) Create a record</div>\n <pre className=\"bg-muted p-3 rounded text-xs overflow-auto\"><code>{`curl -s -X POST \\\n -H \"X-Api-Key: $API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\n \\\\\"entityId\\\\\": \\\\\"$ENTITY_ID\\\\\",\n \\\\\"values\\\\\": {\n \\\\\"field_one\\\\\": \\\\\"Example\\\\\",\n \\\\\"field_two\\\\\": 123\n }\n }\" \\\n \"$BASE_URL/entities/records\" | jq`}</code></pre>\n <p className=\"text-muted-foreground mt-1\">For custom entities, send field keys without the <code>cf_</code> prefix. The API normalizes this server-side.</p>\n </div>\n\n <div>\n <div className=\"font-medium mb-1\">5) Update a record</div>\n <pre className=\"bg-muted p-3 rounded text-xs overflow-auto\"><code>{`curl -s -X PUT \\\n -H \"X-Api-Key: $API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\n \\\\\"entityId\\\\\": \\\\\"$ENTITY_ID\\\\\",\n \\\\\"recordId\\\\\": \\\\\"$RECORD_ID\\\\\",\n \\\\\"values\\\\\": {\n \\\\\"field_one\\\\\": \\\\\"Updated\\\\\"\n }\n }\" \\\n \"$BASE_URL/entities/records\" | jq`}</code></pre>\n </div>\n\n <div>\n <div className=\"font-medium mb-1\">6) Delete a record</div>\n <pre className=\"bg-muted p-3 rounded text-xs overflow-auto\"><code>{`curl -s -X DELETE \\\n -H \"X-Api-Key: $API_KEY\" \\\n \"$BASE_URL/entities/records?entityId=$ENTITY_ID&recordId=$RECORD_ID\" | jq`}</code></pre>\n </div>\n\n <div className=\"text-muted-foreground\">\n Security notes:\n <ul className=\"list-disc pl-5 mt-1 space-y-1\">\n <li>All endpoints require a valid API key. Keys inherit tenant, organization, and feature scope.</li>\n <li>Rotate keys regularly and delete unused ones in the admin UI.</li>\n <li>Store the secret in a secure vault; anyone with the header can act within the key&apos;s permissions.</li>\n </ul>\n </div>\n </div>\n </ContextHelp>\n <DataTable\n title={`Records: ${entityId}`}\n entityId={entityId}\n actions={actions}\n columns={columns}\n data={data}\n perspective={{ tableId: `entities.user.records.${entityId}` }}\n exporter={exportConfig}\n filters={baseFilters}\n filterValues={filterValues}\n rowActions={(row) => (\n <RowActions\n items={[\n { label: 'Edit', href: `/backend/entities/user/${encodeURIComponent(entityId)}/records/${encodeURIComponent(String((row as any).id))}` },\n { label: 'Delete', destructive: true, onSelect: async () => {\n try {\n if (typeof window !== 'undefined') {\n const ok = window.confirm('Delete this record?')\n if (!ok) return\n }\n const deleteCall = await apiCall(\n `/api/entities/records?entityId=${encodeURIComponent(entityId)}&recordId=${encodeURIComponent(String((row as any).id))}`,\n { method: 'DELETE' },\n )\n if (!deleteCall.ok) {\n await raiseCrudError(deleteCall.response, 'Failed to delete record')\n }\n const j = await readApiResultOrThrow<RecordsResponse>(\n `/api/entities/records?entityId=${encodeURIComponent(entityId)}&page=${page}&pageSize=${pageSize}`,\n undefined,\n {\n errorMessage: 'Failed to reload records',\n fallback: { items: [], total: 0, page, pageSize, totalPages: 1 },\n },\n )\n setRawData(j.items || [])\n setTotal(j.total || 0)\n setTotalPages(j.totalPages || 1)\n flash('Record has been removed', 'success')\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Failed to delete record'\n flash(message, 'error')\n }\n } },\n ]}\n />\n )}\n sortable\n sorting={sorting}\n onSortingChange={setSorting}\n searchValue={search}\n onSearchChange={(v) => { setSearch(v); setPage(1) }}\n onFiltersApply={(vals) => { setFilterValues(vals); setPage(1) }}\n onFiltersClear={() => { setFilterValues({}); setPage(1) }}\n pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}\n isLoading={loading}\n />\n </PageBody>\n </Page>\n )\n}\n"],
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useSearchParams } from 'next/navigation'\nimport type { ColumnDef, SortingState } from '@tanstack/react-table'\nimport { filterCustomFieldDefs, useCustomFieldDefs } from '@open-mercato/ui/backend/utils/customFieldDefs'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable, type DataTableExportFormat } from '@open-mercato/ui/backend/DataTable'\nimport type { PreparedExport } from '@open-mercato/shared/lib/crud/exporters'\nimport type { FilterDef } from '@open-mercato/ui/backend/FilterBar'\nimport { ContextHelp } from '@open-mercato/ui/backend/ContextHelp'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport Link from 'next/link'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\n\ntype RecordsResponse = {\n items: any[]\n total: number\n page: number\n pageSize: number\n totalPages: number\n}\n\nfunction toCsvUrl(base: string, params: URLSearchParams) {\n // Build a relative URL to avoid SSR/CSR origin mismatch hydration issues\n const p = new URLSearchParams(params)\n p.set('format', 'csv')\n const qs = p.toString()\n return qs ? `${base}?${qs}` : base\n}\n\nfunction normalizeCell(v: any): string {\n if (Array.isArray(v)) return v.filter((x) => x != null && x !== '').join(', ')\n if (v === true) return 'Yes'\n if (v === false) return 'No'\n if (v == null) return ''\n if (v instanceof Date) return v.toISOString()\n return String(v)\n}\n\nexport default function RecordsPage({ params }: { params: { entityId?: string } }) {\n const entityId = decodeURIComponent(params?.entityId || '')\n const [sorting, setSorting] = React.useState<SortingState>([{ id: 'id', desc: false }])\n const [page, setPage] = React.useState(1)\n const [pageSize, setPageSize] = React.useState(50)\n const [search, setSearch] = React.useState('')\n const [filterValues, setFilterValues] = React.useState<Record<string, any>>({})\n const [columns, setColumns] = React.useState<ColumnDef<any>[]>([])\n const [rawData, setRawData] = React.useState<any[]>([])\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(false)\n const scopeVersion = useOrganizationScopeVersion()\n const { data: cfDefs = [] } = useCustomFieldDefs(entityId, {\n enabled: Boolean(entityId),\n keyExtras: [scopeVersion],\n })\n\n // Fetch records whenever paging/sorting/filters change (do NOT refetch on cfDefs/search changes)\n React.useEffect(() => {\n let cancelled = false\n const run = async () => {\n setLoading(true)\n try {\n const params = new URLSearchParams()\n params.set('entityId', entityId)\n params.set('page', String(page))\n params.set('pageSize', String(pageSize))\n const s = sorting?.[0]\n if (s?.id) {\n params.set('sortField', String(s.id))\n params.set('sortDir', s.desc ? 'desc' : 'asc')\n }\n // Flatten filter values into query params\n for (const [k, v] of Object.entries(filterValues)) {\n if (v == null) continue\n if (Array.isArray(v)) {\n if (v.length) params.set(k, v.join(','))\n } else if (typeof v === 'object') {\n // dateRange-like shapes are not supported generically here; skip\n } else {\n params.set(k, String(v))\n }\n }\n const j = await readApiResultOrThrow<RecordsResponse>(\n `/api/entities/records?${params.toString()}`,\n undefined,\n {\n errorMessage: 'Failed to load records',\n fallback: {\n items: [],\n total: 0,\n page,\n pageSize,\n totalPages: 1,\n },\n },\n )\n if (!cancelled) {\n setRawData(j.items || [])\n setTotal(j.total)\n setTotalPages(j.totalPages)\n }\n } catch (e) {\n if (!cancelled) {\n setRawData([])\n setTotal(0)\n setTotalPages(1)\n }\n } finally {\n if (!cancelled) setLoading(false)\n }\n }\n if (entityId) run()\n return () => { cancelled = true }\n }, [entityId, page, pageSize, sorting, filterValues, scopeVersion])\n\n // Build columns from custom field definitions only (no data round-trip)\n React.useEffect(() => {\n const visibleDefs = filterCustomFieldDefs(cfDefs, 'list') as any\n const maxVisible = 10\n const cols: ColumnDef<any>[] = visibleDefs.map((d: any, idx: number) => ({\n accessorKey: d.key,\n header: d.label || d.key,\n meta: { priority: idx < 4 ? 1 : idx < 6 ? 2 : idx < 8 ? 3 : idx < maxVisible ? 4 : 5 },\n cell: ({ getValue }: { getValue: () => unknown }) => {\n const v = getValue() as any\n return <span className=\"truncate max-w-[24ch] inline-block align-top\" title={normalizeCell(v)}>{normalizeCell(v)}</span>\n },\n }))\n // Ensure hidden 'id' column exists for sorting/state\n const hasIdCol = cols.some((c) => (c as any).accessorKey === 'id' || (c as any).id === 'id')\n if (!hasIdCol) cols.unshift({ accessorKey: 'id', header: 'ID', meta: { hidden: true, priority: 6 } } as any)\n setColumns(cols)\n }, [cfDefs])\n\n // Client-side quick search filtering without triggering server refetch\n const data = React.useMemo(() => {\n if (!search.trim()) return rawData\n const q = search.trim().toLowerCase()\n return (rawData || []).filter((row: any) => {\n const values = Object.values(row || {})\n return values.some((v) => normalizeCell(v).toLowerCase().includes(q))\n })\n }, [rawData, search])\n\n const viewExportColumns = React.useMemo(() => {\n return (columns || [])\n .map((col) => {\n const accessorKey = (col as any).accessorKey\n if (!accessorKey || typeof accessorKey !== 'string') return null\n if ((col as any).meta?.hidden) return null\n const header = typeof col.header === 'string'\n ? col.header\n : accessorKey.startsWith('cf_')\n ? accessorKey.slice(3)\n : accessorKey\n return { field: accessorKey, header }\n })\n .filter((col): col is { field: string; header: string } => !!col)\n }, [columns])\n\n const buildFullExportUrl = React.useCallback((format: DataTableExportFormat) => {\n const qp = new URLSearchParams({\n entityId,\n format,\n exportScope: 'full',\n all: 'true',\n })\n const sort = sorting?.[0]\n if (sort?.id) {\n qp.set('sortField', String(sort.id))\n qp.set('sortDir', sort.desc ? 'desc' : 'asc')\n }\n return `/api/entities/records?${qp.toString()}`\n }, [entityId, sorting])\n\n const exportConfig = React.useMemo(() => {\n const safeEntityId = entityId.replace(/[^a-z0-9_-]/gi, '_') || 'records'\n return {\n view: {\n description: 'Exports the current list respecting filters and column visibility.',\n prepare: async (): Promise<{ prepared: PreparedExport; filename: string }> => {\n const rowsForExport = data.map((row) => {\n const out: Record<string, unknown> = {}\n for (const col of viewExportColumns) {\n out[col.field] = (row as Record<string, unknown>)[col.field]\n }\n return out\n })\n const prepared: PreparedExport = {\n columns: viewExportColumns.map((col) => ({ field: col.field, header: col.header })),\n rows: rowsForExport,\n }\n return { prepared, filename: `${safeEntityId}_view` }\n },\n },\n full: {\n description: 'Exports raw records with every field and custom field included.',\n getUrl: (format: DataTableExportFormat) => buildFullExportUrl(format),\n filename: () => `${safeEntityId}_full`,\n },\n }\n }, [buildFullExportUrl, data, entityId, viewExportColumns])\n\n const hasAnyFormFields = React.useMemo(() => filterCustomFieldDefs(cfDefs, 'form').length > 0, [cfDefs])\n const actions = (\n <>\n <Button asChild variant=\"outline\" size=\"sm\">\n <Link href={`/backend/entities/user/${encodeURIComponent(entityId)}`}>\n Edit Entity Definition\n </Link>\n </Button>\n {hasAnyFormFields && (\n <Button asChild>\n <Link href={`/backend/entities/user/${encodeURIComponent(entityId)}/records/create`}>\n Create\n </Link>\n </Button>\n )}\n </>\n )\n\n // Ensure filters are visible even if no custom fields are marked filterable\n const baseFilters: FilterDef[] = React.useMemo(() => ([\n { id: 'id', label: 'ID', type: 'text' },\n ]), [])\n\n return (\n <Page>\n <PageBody>\n <ContextHelp bulb title=\"API: Manage Records via cURL\" className=\"mb-4\">\n <p className=\"mb-2\">\n Interact with this custom entity via the backend API using cURL. Use API keys for machine-to-machine access\u2014mint one from the{' '}\n <a className=\"underline\" target=\"_blank\" rel=\"noreferrer\" href=\"https://docs.openmercato.com/user-guide/api-keys\">\n Managing API keys guide\n </a>{' '}\n or the{' '}\n <a className=\"underline\" target=\"_blank\" rel=\"noreferrer\" href=\"https://docs.openmercato.com/cli/api-keys\">\n API keys CLI documentation\n </a>{' '}\n before running these calls.\n </p>\n <div className=\"space-y-2\">\n <div>\n <div className=\"font-medium mb-1\">1) Configure environment variables</div>\n <pre className=\"bg-muted p-3 rounded text-xs overflow-auto\"><code>{`export BASE_URL=\"http://localhost:5050/api\"\nexport API_KEY=\"<paste API key secret here>\" # scoped with entities.features\nexport ENTITY_ID=\"${entityId}\"\nexport RECORD_ID=\"<record uuid>\"`}</code></pre>\n <p className=\"text-muted-foreground mt-1\">\n Need a new key? Follow the{' '}\n <a className=\"underline\" target=\"_blank\" rel=\"noreferrer\" href=\"https://docs.openmercato.com/user-guide/api-keys\">\n Managing API keys\n </a>{' '}\n walkthrough or mint one via{' '}\n <a className=\"underline\" target=\"_blank\" rel=\"noreferrer\" href=\"https://docs.openmercato.com/cli/api-keys\">\n mercato api_keys add\n </a>\n .\n </p>\n </div>\n\n <div>\n <div className=\"font-medium mb-1\">2) List records</div>\n <pre className=\"bg-muted p-3 rounded text-xs overflow-auto\"><code>{`curl -s -H \"X-Api-Key: $API_KEY\" \\\n \"$BASE_URL/entities/records?entityId=$ENTITY_ID\" | jq`}</code></pre>\n </div>\n\n <div>\n <div className=\"font-medium mb-1\">3) Read a single record (by id)</div>\n <pre className=\"bg-muted p-3 rounded text-xs overflow-auto\"><code>{`curl -s -H \"X-Api-Key: $API_KEY\" \\\n \"$BASE_URL/entities/records?entityId=$ENTITY_ID&id=$RECORD_ID\" | jq`}</code></pre>\n <p className=\"text-muted-foreground mt-1\">Note: Response is a list; filter by <code>id</code> to get a single item.</p>\n </div>\n\n <div>\n <div className=\"font-medium mb-1\">4) Create a record</div>\n <pre className=\"bg-muted p-3 rounded text-xs overflow-auto\"><code>{`curl -s -X POST \\\n -H \"X-Api-Key: $API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\n \\\\\"entityId\\\\\": \\\\\"$ENTITY_ID\\\\\",\n \\\\\"values\\\\\": {\n \\\\\"field_one\\\\\": \\\\\"Example\\\\\",\n \\\\\"field_two\\\\\": 123\n }\n }\" \\\n \"$BASE_URL/entities/records\" | jq`}</code></pre>\n <p className=\"text-muted-foreground mt-1\">For custom entities, send field keys without the <code>cf_</code> prefix. The API normalizes this server-side.</p>\n </div>\n\n <div>\n <div className=\"font-medium mb-1\">5) Update a record</div>\n <pre className=\"bg-muted p-3 rounded text-xs overflow-auto\"><code>{`curl -s -X PUT \\\n -H \"X-Api-Key: $API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\n \\\\\"entityId\\\\\": \\\\\"$ENTITY_ID\\\\\",\n \\\\\"recordId\\\\\": \\\\\"$RECORD_ID\\\\\",\n \\\\\"values\\\\\": {\n \\\\\"field_one\\\\\": \\\\\"Updated\\\\\"\n }\n }\" \\\n \"$BASE_URL/entities/records\" | jq`}</code></pre>\n </div>\n\n <div>\n <div className=\"font-medium mb-1\">6) Delete a record</div>\n <pre className=\"bg-muted p-3 rounded text-xs overflow-auto\"><code>{`curl -s -X DELETE \\\n -H \"X-Api-Key: $API_KEY\" \\\n \"$BASE_URL/entities/records?entityId=$ENTITY_ID&recordId=$RECORD_ID\" | jq`}</code></pre>\n </div>\n\n <div className=\"text-muted-foreground\">\n Security notes:\n <ul className=\"list-disc pl-5 mt-1 space-y-1\">\n <li>All endpoints require a valid API key. Keys inherit tenant, organization, and feature scope.</li>\n <li>Rotate keys regularly and delete unused ones in the admin UI.</li>\n <li>Store the secret in a secure vault; anyone with the header can act within the key&apos;s permissions.</li>\n </ul>\n </div>\n </div>\n </ContextHelp>\n <DataTable\n title={`Records: ${entityId}`}\n entityId={entityId}\n actions={actions}\n columns={columns}\n data={data}\n perspective={{ tableId: `entities.user.records.${entityId}` }}\n exporter={exportConfig}\n filters={baseFilters}\n filterValues={filterValues}\n rowActions={(row) => (\n <RowActions\n items={[\n { label: 'Edit', href: `/backend/entities/user/${encodeURIComponent(entityId)}/records/${encodeURIComponent(String((row as any).id))}` },\n { label: 'Delete', destructive: true, onSelect: async () => {\n try {\n if (typeof window !== 'undefined') {\n const ok = window.confirm('Delete this record?')\n if (!ok) return\n }\n const deleteCall = await apiCall(\n `/api/entities/records?entityId=${encodeURIComponent(entityId)}&recordId=${encodeURIComponent(String((row as any).id))}`,\n { method: 'DELETE' },\n )\n if (!deleteCall.ok) {\n await raiseCrudError(deleteCall.response, 'Failed to delete record')\n }\n const j = await readApiResultOrThrow<RecordsResponse>(\n `/api/entities/records?entityId=${encodeURIComponent(entityId)}&page=${page}&pageSize=${pageSize}`,\n undefined,\n {\n errorMessage: 'Failed to reload records',\n fallback: { items: [], total: 0, page, pageSize, totalPages: 1 },\n },\n )\n setRawData(j.items || [])\n setTotal(j.total || 0)\n setTotalPages(j.totalPages || 1)\n flash('Record has been removed', 'success')\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Failed to delete record'\n flash(message, 'error')\n }\n } },\n ]}\n />\n )}\n sortable\n sorting={sorting}\n onSortingChange={setSorting}\n searchValue={search}\n onSearchChange={(v) => { setSearch(v); setPage(1) }}\n onFiltersApply={(vals) => { setFilterValues(vals); setPage(1) }}\n onFiltersClear={() => { setFilterValues({}); setPage(1) }}\n pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}\n isLoading={loading}\n />\n </PageBody>\n </Page>\n )\n}\n"],
5
5
  "mappings": "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",
6
6
  "names": ["params"]
7
7
  }
@@ -382,7 +382,7 @@ async function executeCallApi(em, config, context, container) {
382
382
  );
383
383
  }
384
384
  function buildApiUrl(endpoint) {
385
- const appUrl = process.env.APP_URL || "http://localhost:3000";
385
+ const appUrl = process.env.APP_URL || "http://localhost:5050";
386
386
  if (endpoint.startsWith("/")) {
387
387
  if (!endpoint.startsWith("/api/")) {
388
388
  throw new Error(`CALL_API only supports /api/* paths, got: ${endpoint}`);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/workflows/lib/activity-executor.ts"],
4
- "sourcesContent": ["/**\n * Workflows Module - Activity Executor Service\n *\n * Executes workflow activities (send email, call API, emit events, etc.)\n * - Supports multiple activity types\n * - Implements retry logic with exponential backoff\n * - Handles timeouts\n * - Variable interpolation from workflow context\n *\n * Functional API (no classes) following Open Mercato conventions.\n */\n\nimport { EntityManager } from '@mikro-orm/core'\nimport type { AwilixContainer } from 'awilix'\nimport { WorkflowInstance } from '../data/entities'\nimport { createQueue, Queue } from '@open-mercato/queue'\nimport { WorkflowActivityJob, WORKFLOW_ACTIVITIES_QUEUE_NAME } from './activity-queue-types'\nimport { logWorkflowEvent } from './event-logger'\n\n// ============================================================================\n// Types and Interfaces\n// ============================================================================\n\nexport type ActivityType =\n | 'SEND_EMAIL'\n | 'CALL_API'\n | 'EMIT_EVENT'\n | 'UPDATE_ENTITY'\n | 'CALL_WEBHOOK'\n | 'EXECUTE_FUNCTION'\n\nexport interface ActivityDefinition {\n activityId: string // Unique identifier for activity\n activityName?: string // Optional, for debugging/logging\n activityType: ActivityType\n config: any\n async?: boolean // Flag to execute activity asynchronously via queue\n retryPolicy?: RetryPolicy\n timeoutMs?: number\n compensate?: boolean // Flag to execute compensation on failure\n}\n\nexport interface RetryPolicy {\n maxAttempts: number\n initialIntervalMs: number\n backoffCoefficient: number\n maxIntervalMs: number\n}\n\nexport interface ActivityContext {\n workflowInstance: WorkflowInstance\n workflowContext: Record<string, any>\n stepContext?: Record<string, any>\n stepInstanceId?: string\n transitionId?: string\n userId?: string\n}\n\nexport interface ActivityExecutionResult {\n activityId: string\n activityName?: string\n activityType: ActivityType\n success: boolean\n output?: any\n error?: string\n retryCount: number\n executionTimeMs: number\n async?: boolean // Marks activity as async (queued)\n jobId?: string // Queue job ID for async activities\n}\n\nexport class ActivityExecutionError extends Error {\n constructor(\n message: string,\n public activityType: ActivityType,\n public activityName?: string,\n public details?: any\n ) {\n super(message)\n this.name = 'ActivityExecutionError'\n }\n}\n\n// ============================================================================\n// Queue Integration for Async Activities\n// ============================================================================\n\nlet activityQueue: Queue<WorkflowActivityJob> | null = null\n\n/**\n * Get or create the activity queue (lazy initialization)\n */\nfunction getActivityQueue(): Queue<WorkflowActivityJob> {\n if (!activityQueue) {\n if (process.env.QUEUE_STRATEGY === 'async') {\n activityQueue = createQueue<WorkflowActivityJob>(\n WORKFLOW_ACTIVITIES_QUEUE_NAME,\n 'async',\n {\n connection: {\n url: process.env.REDIS_URL || process.env.QUEUE_REDIS_URL,\n },\n concurrency: parseInt(process.env.WORKFLOW_WORKER_CONCURRENCY || '5'),\n }\n )\n } else {\n activityQueue = createQueue<WorkflowActivityJob>(\n WORKFLOW_ACTIVITIES_QUEUE_NAME,\n 'local',\n {\n baseDir: process.env.QUEUE_BASE_DIR || '.queue',\n }\n )\n }\n }\n\n return activityQueue\n}\n\n/**\n * Enqueue an activity for background execution\n *\n * @param em - Entity manager\n * @param activity - Activity definition\n * @param context - Execution context\n * @returns Job ID\n */\nexport async function enqueueActivity(\n em: EntityManager,\n activity: ActivityDefinition,\n context: ActivityContext\n): Promise<string> {\n const { workflowInstance, workflowContext, stepContext, transitionId, stepInstanceId } =\n context\n\n // Interpolate config variables NOW (before queuing)\n const interpolatedConfig = interpolateVariables(activity.config, workflowContext, workflowInstance)\n\n // Create job payload\n const job: WorkflowActivityJob = {\n workflowInstanceId: workflowInstance.id,\n stepInstanceId,\n transitionId,\n activityId: activity.activityId,\n activityName: activity.activityName || activity.activityType,\n activityType: activity.activityType,\n activityConfig: interpolatedConfig,\n workflowContext,\n stepContext,\n retryPolicy: activity.retryPolicy,\n timeoutMs: activity.timeoutMs,\n tenantId: workflowInstance.tenantId,\n organizationId: workflowInstance.organizationId,\n userId: context.userId,\n }\n\n // Enqueue to queue\n const queue = getActivityQueue()\n const jobId = await queue.enqueue(job)\n\n // Log event\n await logWorkflowEvent(em, {\n workflowInstanceId: workflowInstance.id,\n stepInstanceId,\n eventType: 'ACTIVITY_QUEUED',\n eventData: {\n activityId: activity.activityId,\n activityName: activity.activityName,\n activityType: activity.activityType,\n async: true,\n jobId,\n },\n tenantId: workflowInstance.tenantId,\n organizationId: workflowInstance.organizationId,\n })\n\n return jobId\n}\n\n// ============================================================================\n// Main Activity Execution Functions\n// ============================================================================\n\n/**\n * Execute a single activity with retry logic and timeout\n *\n * @param em - Entity manager\n * @param container - DI container\n * @param activity - Activity definition\n * @param context - Execution context\n * @returns Execution result\n */\nexport async function executeActivity(\n em: EntityManager,\n container: AwilixContainer,\n activity: ActivityDefinition,\n context: ActivityContext\n): Promise<ActivityExecutionResult> {\n const retryPolicy = activity.retryPolicy || {\n maxAttempts: 1,\n initialIntervalMs: 0,\n backoffCoefficient: 1,\n maxIntervalMs: 0,\n }\n\n let lastError: any\n let retryCount = 0\n\n for (let attempt = 0; attempt < retryPolicy.maxAttempts; attempt++) {\n try {\n const startTime = Date.now()\n\n // Execute with timeout if specified\n const result = activity.timeoutMs\n ? await executeWithTimeout(\n () => executeActivityByType(em, container, activity, context),\n activity.timeoutMs\n )\n : await executeActivityByType(em, container, activity, context)\n\n const executionTimeMs = Date.now() - startTime\n\n return {\n activityId: activity.activityId,\n activityName: activity.activityName,\n activityType: activity.activityType,\n success: true,\n output: result,\n retryCount: attempt,\n executionTimeMs,\n async: activity.async || false,\n }\n } catch (error) {\n lastError = error\n retryCount = attempt + 1\n\n // If not the last attempt, apply backoff and retry\n if (attempt < retryPolicy.maxAttempts - 1) {\n const backoff = calculateBackoff(\n retryPolicy.initialIntervalMs,\n retryPolicy.backoffCoefficient,\n attempt,\n retryPolicy.maxIntervalMs\n )\n\n await sleep(backoff)\n }\n }\n }\n\n // All retries exhausted\n const errorMessage = lastError instanceof Error ? lastError.message : String(lastError)\n\n return {\n activityId: activity.activityId,\n activityName: activity.activityName,\n activityType: activity.activityType,\n success: false,\n error: `Activity failed after ${retryCount} attempts: ${errorMessage}`,\n retryCount,\n executionTimeMs: 0,\n async: activity.async || false,\n }\n}\n\n/**\n * Execute multiple activities in sequence\n * Supports both synchronous and asynchronous (queued) execution\n *\n * @param em - Entity manager\n * @param container - DI container\n * @param activities - Array of activity definitions\n * @param context - Execution context\n * @returns Array of execution results\n */\nexport async function executeActivities(\n em: EntityManager,\n container: AwilixContainer,\n activities: ActivityDefinition[],\n context: ActivityContext\n): Promise<ActivityExecutionResult[]> {\n const results: ActivityExecutionResult[] = []\n\n for (let i = 0; i < activities.length; i++) {\n const activity = activities[i]\n\n // Check if activity should run async\n if (activity.async) {\n // Enqueue for background execution\n const jobId = await enqueueActivity(em, activity, context)\n\n results.push({\n activityId: activity.activityId,\n activityName: activity.activityName,\n activityType: activity.activityType,\n success: true, // Queued successfully\n async: true,\n jobId,\n retryCount: 0,\n executionTimeMs: 0,\n })\n } else {\n // Execute synchronously (existing logic)\n const result = await executeActivity(em, container, activity, context)\n results.push(result)\n\n // Stop execution if activity fails (fail-fast)\n if (!result.success) {\n break\n }\n\n // Update workflow context with activity output\n if (result.output && typeof result.output === 'object') {\n const key = activity.activityName || activity.activityType\n context.workflowContext = {\n ...context.workflowContext,\n [key]: result.output,\n }\n }\n }\n }\n\n return results\n}\n\n// ============================================================================\n// Activity Type Handlers\n// ============================================================================\n\n/**\n * Execute activity based on its type\n */\nasync function executeActivityByType(\n em: EntityManager,\n container: AwilixContainer,\n activity: ActivityDefinition,\n context: ActivityContext\n): Promise<any> {\n // Interpolate config variables from context (including workflow metadata)\n const interpolatedConfig = interpolateVariables(activity.config, context.workflowContext, context.workflowInstance)\n\n switch (activity.activityType) {\n case 'SEND_EMAIL':\n return await executeSendEmail(interpolatedConfig, context, container)\n\n case 'CALL_API':\n return await executeCallApi(em, interpolatedConfig, context, container)\n\n case 'EMIT_EVENT':\n return await executeEmitEvent(interpolatedConfig, context, container)\n\n case 'UPDATE_ENTITY':\n return await executeUpdateEntity(em, interpolatedConfig, context, container)\n\n case 'CALL_WEBHOOK':\n return await executeCallWebhook(interpolatedConfig, context)\n\n case 'EXECUTE_FUNCTION':\n return await executeFunction(interpolatedConfig, context, container)\n\n default:\n throw new ActivityExecutionError(\n `Unknown activity type: ${activity.activityType}`,\n activity.activityType,\n activity.activityName\n )\n }\n}\n\n/**\n * SEND_EMAIL activity handler\n *\n * For MVP, this logs the email (actual email sending can be added later)\n */\nexport async function executeSendEmail(\n config: any,\n context: ActivityContext,\n container: AwilixContainer\n): Promise<any> {\n const { to, subject, template, templateData, body } = config\n\n if (!to || !subject) {\n throw new Error('SEND_EMAIL requires \"to\" and \"subject\" fields')\n }\n\n // For MVP: Log the email (actual email service integration can be added later)\n console.log(`[Workflow Activity] Send email to ${to}: ${subject}`)\n\n // Check if email service is available in container\n try {\n const emailService = container.resolve('emailService')\n if (emailService && typeof emailService.send === 'function') {\n await emailService.send({\n to,\n subject,\n template,\n templateData,\n body,\n })\n return { sent: true, to, subject, via: 'emailService' }\n }\n } catch (error) {\n // Email service not available, just log\n }\n\n return { sent: true, to, subject, via: 'console' }\n}\n\n/**\n * EMIT_EVENT activity handler\n *\n * Publishes a domain event to the event bus\n */\nexport async function executeEmitEvent(\n config: any,\n context: ActivityContext,\n container: AwilixContainer\n): Promise<any> {\n const { eventName, payload } = config\n\n if (!eventName) {\n throw new Error('EMIT_EVENT requires \"eventName\" field')\n }\n\n // Get event bus from container\n const eventBus = container.resolve('eventBus')\n\n if (!eventBus || typeof eventBus.emitEvent !== 'function') {\n throw new Error('Event bus not available in container')\n }\n\n // Publish event with workflow metadata\n const enrichedPayload = {\n ...payload,\n _workflow: {\n workflowInstanceId: context.workflowInstance.id,\n workflowId: context.workflowInstance.workflowId,\n tenantId: context.workflowInstance.tenantId,\n organizationId: context.workflowInstance.organizationId,\n },\n }\n\n await eventBus.emitEvent(eventName, enrichedPayload)\n\n return { emitted: true, eventName, payload: enrichedPayload }\n}\n\n/**\n * UPDATE_ENTITY activity handler\n *\n * Updates an entity via query engine\n */\nexport async function executeUpdateEntity(\n em: EntityManager,\n config: any,\n context: ActivityContext,\n container: AwilixContainer\n): Promise<any> {\n const { entityType, entityId, updates } = config\n\n if (!entityType || !entityId || !updates) {\n throw new Error('UPDATE_ENTITY requires \"entityType\", \"entityId\", and \"updates\" fields')\n }\n\n // Get query engine from container\n const queryEngine = container.resolve('queryEngine')\n\n if (!queryEngine || typeof queryEngine.update !== 'function') {\n throw new Error('Query engine not available in container')\n }\n\n // Execute update with tenant scoping\n await queryEngine.update({\n entity: entityType,\n where: { id: entityId },\n data: updates,\n tenantId: context.workflowInstance.tenantId,\n organizationId: context.workflowInstance.organizationId,\n })\n\n return { updated: true, entityType, entityId, updates }\n}\n\n/**\n * CALL_WEBHOOK activity handler\n *\n * Makes HTTP request to external URL\n */\nexport async function executeCallWebhook(\n config: any,\n context: ActivityContext\n): Promise<any> {\n const { url, method = 'POST', headers = {}, body } = config\n\n if (!url) {\n throw new Error('CALL_WEBHOOK requires \"url\" field')\n }\n\n // Make HTTP request\n const response = await fetch(url, {\n method,\n headers: {\n 'Content-Type': 'application/json',\n ...headers,\n },\n body: body ? JSON.stringify(body) : undefined,\n })\n\n // Parse response\n let result: any\n const contentType = response.headers.get('content-type')\n\n if (contentType && contentType.includes('application/json')) {\n result = await response.json()\n } else {\n result = await response.text()\n }\n\n // Check for HTTP errors\n if (!response.ok) {\n throw new Error(\n `Webhook request failed with status ${response.status}: ${JSON.stringify(result)}`\n )\n }\n\n return {\n status: response.status,\n statusText: response.statusText,\n result,\n }\n}\n\n/**\n * EXECUTE_FUNCTION activity handler\n *\n * Calls a registered function from DI container\n */\nexport async function executeFunction(\n config: any,\n context: ActivityContext,\n container: AwilixContainer\n): Promise<any> {\n const { functionName, args = {} } = config\n\n if (!functionName) {\n throw new Error('EXECUTE_FUNCTION requires \"functionName\" field')\n }\n\n // Look up function in container\n const fnKey = `workflowFunction:${functionName}`\n\n try {\n const fn = container.resolve(fnKey)\n\n if (typeof fn !== 'function') {\n throw new Error(`Registered workflow function \"${functionName}\" is not a function`)\n }\n\n // Call function with args and context\n const result = await fn(args, context)\n\n return { executed: true, functionName, result }\n } catch (error) {\n if (error instanceof Error && error.message.includes('not registered')) {\n throw new Error(\n `Workflow function \"${functionName}\" not registered in DI container (key: ${fnKey})`\n )\n }\n throw error\n }\n}\n\n/**\n * CALL_API activity handler\n *\n * Makes authenticated HTTP request to internal Open Mercato APIs\n * - Automatically creates one-time API key for authentication\n * - Injects tenant/organization context headers\n * - Validates URL security (SSRF prevention)\n * - Classifies errors (retriable vs non-retriable)\n * - Deletes API key after request (no stored credentials!)\n */\nexport async function executeCallApi(\n em: EntityManager,\n config: any,\n context: ActivityContext,\n container: AwilixContainer\n): Promise<any> {\n // 1. Interpolate variables in config (including {{workflow.*}}, {{context.*}}, {{env.*}}, {{now}})\n const interpolatedConfig = interpolateVariables(config, context.workflowContext, context.workflowInstance)\n\n const {\n endpoint,\n method = 'GET',\n headers = {},\n body,\n validateTenantMatch = true,\n } = interpolatedConfig\n\n\n if (!endpoint) {\n throw new Error('CALL_API requires \"endpoint\" field')\n }\n\n // 2. Build full URL (prepend APP_URL for relative paths)\n const fullUrl = buildApiUrl(endpoint)\n\n // 3. Import the one-time API key helper\n const { withOnetimeApiKey } = await import('../../api_keys/services/apiKeyService')\n\n // 4. Get EntityManager from container (for correct type)\n const apiKeyEm = container.resolve('em')\n\n // 5. Look up an admin role for the tenant to assign to the one-time key\n // CRITICAL: rolesJson must contain role IDs (UUIDs), not role names!\n const { Role } = await import('../../auth/data/entities')\n const adminRole = await apiKeyEm.findOne(Role, {\n tenantId: context.workflowInstance.tenantId,\n name: { $in: ['superadmin', 'admin', 'administrator'] } // Try common admin role names\n })\n\n if (!adminRole) {\n throw new Error(\n `[CALL_API] No admin role found for tenant ${context.workflowInstance.tenantId}. ` +\n `Cannot create one-time API key without role assignment. ` +\n `Ensure 'mercato init' has been run to create default roles.`\n )\n }\n\n // 6. Execute request with one-time API key (using role ID, not name)\n return await withOnetimeApiKey(\n apiKeyEm,\n {\n name: `__workflow_${context.workflowInstance.id}__`,\n description: `One-time key for workflow ${context.workflowInstance.workflowId} instance ${context.workflowInstance.id}`,\n tenantId: context.workflowInstance.tenantId,\n organizationId: context.workflowInstance.organizationId,\n roles: [adminRole.id], // \u2705 FIX: Use role ID (UUID), not role name\n expiresAt: null,\n },\n async (apiKeySecret) => {\n // Build request headers (auth + context + custom)\n const requestHeaders: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'Authorization': `apikey ${apiKeySecret}`,\n 'X-Tenant-Id': context.workflowInstance.tenantId,\n 'X-Organization-Id': context.workflowInstance.organizationId,\n 'X-Workflow-Instance-Id': context.workflowInstance.id,\n ...headers,\n }\n\n // Make HTTP request\n const response = await fetch(fullUrl, {\n method,\n headers: requestHeaders,\n body: body ? JSON.stringify(body) : undefined,\n })\n\n // Parse response body (JSON-safe)\n let responseBody: any\n const contentType = response.headers.get('content-type')\n\n try {\n if (contentType && contentType.includes('application/json')) {\n responseBody = await response.json()\n } else {\n responseBody = await response.text()\n }\n } catch (error) {\n responseBody = null\n }\n\n // Check for HTTP errors and classify\n if (!response.ok) {\n classifyAndThrowError(response.status, responseBody, fullUrl)\n }\n\n // Validate tenant match (security check)\n if (validateTenantMatch && responseBody && typeof responseBody === 'object') {\n if (responseBody.tenantId && responseBody.tenantId !== context.workflowInstance.tenantId) {\n throw new Error(\n `Tenant ID mismatch: workflow expects ${context.workflowInstance.tenantId} but API returned ${responseBody.tenantId}`\n )\n }\n }\n\n // Return structured result\n return {\n status: response.status,\n statusText: response.statusText,\n headers: Object.fromEntries(response.headers.entries()),\n body: responseBody,\n authenticated: true,\n tenantId: context.workflowInstance.tenantId,\n organizationId: context.workflowInstance.organizationId,\n }\n }\n )\n}\n\n// ============================================================================\n// CALL_API Helper Functions\n// ============================================================================\n\n/**\n * Build full API URL from endpoint\n * - Relative paths (/api/...) \u2192 prepend APP_URL\n * - Absolute URLs \u2192 validate domain matches APP_URL (SSRF prevention)\n */\nfunction buildApiUrl(endpoint: string): string {\n const appUrl = process.env.APP_URL || 'http://localhost:3000'\n\n // Relative path - prepend APP_URL\n if (endpoint.startsWith('/')) {\n // Security: Only allow /api/* paths\n if (!endpoint.startsWith('/api/')) {\n throw new Error(`CALL_API only supports /api/* paths, got: ${endpoint}`)\n }\n return `${appUrl}${endpoint}`\n }\n\n // Absolute URL - validate domain matches APP_URL (SSRF prevention)\n try {\n const endpointUrl = new URL(endpoint)\n const appUrlObj = new URL(appUrl)\n\n if (endpointUrl.host !== appUrlObj.host) {\n throw new Error(\n `SSRF Prevention: CALL_API endpoint domain (${endpointUrl.host}) does not match APP_URL (${appUrlObj.host})`\n )\n }\n\n return endpoint\n } catch (error) {\n if (error instanceof TypeError) {\n throw new Error(`Invalid endpoint URL: ${endpoint}`)\n }\n throw error\n }\n}\n\n/**\n * Classify HTTP error and throw appropriate error\n * - 400-499: Non-retriable (client error - validation/auth)\n * - 500-599: Retriable (server error)\n */\nfunction classifyAndThrowError(status: number, body: any, url: string): never {\n const bodyStr = typeof body === 'string' ? body : JSON.stringify(body)\n\n if (status >= 400 && status < 500) {\n // Client errors - non-retriable\n throw new Error(\n `CALL_API request failed with status ${status} (non-retriable): ${bodyStr}`\n )\n }\n\n if (status >= 500) {\n // Server errors - retriable\n const error: any = new Error(\n `CALL_API request failed with status ${status} (retriable): ${bodyStr}`\n )\n error.retriable = true\n throw error\n }\n\n // Other errors\n throw new Error(`CALL_API request failed with status ${status}: ${bodyStr}`)\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Interpolate variables in config from workflow context\n *\n * Supports syntax:\n * - {{context.field}} or {{context.nested.field}} - from workflow context\n * - {{workflow.instanceId}} - workflow instance ID\n * - {{workflow.tenantId}} - tenant ID\n * - {{workflow.organizationId}} - organization ID\n * - {{workflow.currentStepId}} - current step ID\n * - {{env.VAR_NAME}} - environment variables\n * - {{now}} - current ISO timestamp\n */\nfunction interpolateVariables(\n config: any,\n context: Record<string, any>,\n workflowInstance?: WorkflowInstance\n): any {\n if (typeof config === 'string') {\n // Check if this is a single variable reference (e.g., \"{{context.cart.items}}\")\n // This preserves the original type (array, object, number, boolean)\n const singleVarMatch = config.match(/^\\{\\{([^}]+)\\}\\}$/)\n\n if (singleVarMatch) {\n const trimmedPath = singleVarMatch[1].trim()\n\n // Handle {{workflow.*}} variables\n if (trimmedPath.startsWith('workflow.') && workflowInstance) {\n const workflowKey = trimmedPath.substring('workflow.'.length)\n switch (workflowKey) {\n case 'instanceId':\n return workflowInstance.id\n case 'tenantId':\n return workflowInstance.tenantId\n case 'organizationId':\n return workflowInstance.organizationId\n case 'currentStepId':\n return workflowInstance.currentStepId\n case 'workflowId':\n return workflowInstance.workflowId\n case 'version':\n return workflowInstance.version // Return as number\n default:\n return config // Return original if unknown\n }\n }\n\n // Handle {{env.*}} variables\n if (trimmedPath.startsWith('env.')) {\n const envKey = trimmedPath.substring('env.'.length)\n return process.env[envKey] ?? config\n }\n\n // Handle {{now}} - current timestamp\n if (trimmedPath === 'now') {\n return new Date().toISOString()\n }\n\n // Handle {{context.*}} variables (default behavior)\n const contextPath = trimmedPath.startsWith('context.')\n ? trimmedPath.substring('context.'.length)\n : trimmedPath\n\n const value = getNestedValue(context, contextPath)\n return value !== undefined ? value : config // Return raw value to preserve type\n }\n\n // Multiple interpolations or mixed text - return string\n return config.replace(/\\{\\{([^}]+)\\}\\}/g, (match, path) => {\n const trimmedPath = path.trim()\n\n // Handle {{workflow.*}} variables\n if (trimmedPath.startsWith('workflow.') && workflowInstance) {\n const workflowKey = trimmedPath.substring('workflow.'.length)\n switch (workflowKey) {\n case 'instanceId':\n return workflowInstance.id\n case 'tenantId':\n return workflowInstance.tenantId\n case 'organizationId':\n return workflowInstance.organizationId\n case 'currentStepId':\n return workflowInstance.currentStepId\n case 'workflowId':\n return workflowInstance.workflowId\n case 'version':\n return String(workflowInstance.version)\n default:\n return match // Unknown workflow key\n }\n }\n\n // Handle {{env.*}} variables\n if (trimmedPath.startsWith('env.')) {\n const envKey = trimmedPath.substring('env.'.length)\n const envValue = process.env[envKey]\n return envValue !== undefined ? envValue : match\n }\n\n // Handle {{now}} - current timestamp\n if (trimmedPath === 'now') {\n return new Date().toISOString()\n }\n\n // Handle {{context.*}} variables (default behavior)\n const contextPath = trimmedPath.startsWith('context.')\n ? trimmedPath.substring('context.'.length)\n : trimmedPath\n\n const value = getNestedValue(context, contextPath)\n return value !== undefined ? String(value) : match\n })\n }\n\n if (Array.isArray(config)) {\n return config.map((item) => interpolateVariables(item, context, workflowInstance))\n }\n\n if (config && typeof config === 'object') {\n const result: Record<string, any> = {}\n for (const [key, value] of Object.entries(config)) {\n result[key] = interpolateVariables(value, context, workflowInstance)\n }\n return result\n }\n\n return config\n}\n\n/**\n * Get nested value from object by path (e.g., \"user.email\")\n */\nfunction getNestedValue(obj: any, path: string): any {\n const parts = path.split('.')\n let value = obj\n\n for (const part of parts) {\n if (value && typeof value === 'object' && part in value) {\n value = value[part]\n } else {\n return undefined\n }\n }\n\n return value\n}\n\n/**\n * Calculate exponential backoff delay\n */\nfunction calculateBackoff(\n initialIntervalMs: number,\n backoffCoefficient: number,\n attempt: number,\n maxIntervalMs: number\n): number {\n const backoff = initialIntervalMs * Math.pow(backoffCoefficient, attempt)\n return Math.min(backoff, maxIntervalMs || Infinity)\n}\n\n/**\n * Sleep for specified milliseconds\n */\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/**\n * Execute a promise with timeout\n */\nasync function executeWithTimeout<T>(\n executor: () => Promise<T>,\n timeoutMs: number\n): Promise<T> {\n let timeoutId: NodeJS.Timeout\n\n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutId = setTimeout(() => {\n reject(new Error(`Activity execution timeout after ${timeoutMs}ms`))\n }, timeoutMs)\n })\n\n try {\n return await Promise.race([executor(), timeoutPromise])\n } finally {\n clearTimeout(timeoutId!)\n }\n}\n"],
4
+ "sourcesContent": ["/**\n * Workflows Module - Activity Executor Service\n *\n * Executes workflow activities (send email, call API, emit events, etc.)\n * - Supports multiple activity types\n * - Implements retry logic with exponential backoff\n * - Handles timeouts\n * - Variable interpolation from workflow context\n *\n * Functional API (no classes) following Open Mercato conventions.\n */\n\nimport { EntityManager } from '@mikro-orm/core'\nimport type { AwilixContainer } from 'awilix'\nimport { WorkflowInstance } from '../data/entities'\nimport { createQueue, Queue } from '@open-mercato/queue'\nimport { WorkflowActivityJob, WORKFLOW_ACTIVITIES_QUEUE_NAME } from './activity-queue-types'\nimport { logWorkflowEvent } from './event-logger'\n\n// ============================================================================\n// Types and Interfaces\n// ============================================================================\n\nexport type ActivityType =\n | 'SEND_EMAIL'\n | 'CALL_API'\n | 'EMIT_EVENT'\n | 'UPDATE_ENTITY'\n | 'CALL_WEBHOOK'\n | 'EXECUTE_FUNCTION'\n\nexport interface ActivityDefinition {\n activityId: string // Unique identifier for activity\n activityName?: string // Optional, for debugging/logging\n activityType: ActivityType\n config: any\n async?: boolean // Flag to execute activity asynchronously via queue\n retryPolicy?: RetryPolicy\n timeoutMs?: number\n compensate?: boolean // Flag to execute compensation on failure\n}\n\nexport interface RetryPolicy {\n maxAttempts: number\n initialIntervalMs: number\n backoffCoefficient: number\n maxIntervalMs: number\n}\n\nexport interface ActivityContext {\n workflowInstance: WorkflowInstance\n workflowContext: Record<string, any>\n stepContext?: Record<string, any>\n stepInstanceId?: string\n transitionId?: string\n userId?: string\n}\n\nexport interface ActivityExecutionResult {\n activityId: string\n activityName?: string\n activityType: ActivityType\n success: boolean\n output?: any\n error?: string\n retryCount: number\n executionTimeMs: number\n async?: boolean // Marks activity as async (queued)\n jobId?: string // Queue job ID for async activities\n}\n\nexport class ActivityExecutionError extends Error {\n constructor(\n message: string,\n public activityType: ActivityType,\n public activityName?: string,\n public details?: any\n ) {\n super(message)\n this.name = 'ActivityExecutionError'\n }\n}\n\n// ============================================================================\n// Queue Integration for Async Activities\n// ============================================================================\n\nlet activityQueue: Queue<WorkflowActivityJob> | null = null\n\n/**\n * Get or create the activity queue (lazy initialization)\n */\nfunction getActivityQueue(): Queue<WorkflowActivityJob> {\n if (!activityQueue) {\n if (process.env.QUEUE_STRATEGY === 'async') {\n activityQueue = createQueue<WorkflowActivityJob>(\n WORKFLOW_ACTIVITIES_QUEUE_NAME,\n 'async',\n {\n connection: {\n url: process.env.REDIS_URL || process.env.QUEUE_REDIS_URL,\n },\n concurrency: parseInt(process.env.WORKFLOW_WORKER_CONCURRENCY || '5'),\n }\n )\n } else {\n activityQueue = createQueue<WorkflowActivityJob>(\n WORKFLOW_ACTIVITIES_QUEUE_NAME,\n 'local',\n {\n baseDir: process.env.QUEUE_BASE_DIR || '.queue',\n }\n )\n }\n }\n\n return activityQueue\n}\n\n/**\n * Enqueue an activity for background execution\n *\n * @param em - Entity manager\n * @param activity - Activity definition\n * @param context - Execution context\n * @returns Job ID\n */\nexport async function enqueueActivity(\n em: EntityManager,\n activity: ActivityDefinition,\n context: ActivityContext\n): Promise<string> {\n const { workflowInstance, workflowContext, stepContext, transitionId, stepInstanceId } =\n context\n\n // Interpolate config variables NOW (before queuing)\n const interpolatedConfig = interpolateVariables(activity.config, workflowContext, workflowInstance)\n\n // Create job payload\n const job: WorkflowActivityJob = {\n workflowInstanceId: workflowInstance.id,\n stepInstanceId,\n transitionId,\n activityId: activity.activityId,\n activityName: activity.activityName || activity.activityType,\n activityType: activity.activityType,\n activityConfig: interpolatedConfig,\n workflowContext,\n stepContext,\n retryPolicy: activity.retryPolicy,\n timeoutMs: activity.timeoutMs,\n tenantId: workflowInstance.tenantId,\n organizationId: workflowInstance.organizationId,\n userId: context.userId,\n }\n\n // Enqueue to queue\n const queue = getActivityQueue()\n const jobId = await queue.enqueue(job)\n\n // Log event\n await logWorkflowEvent(em, {\n workflowInstanceId: workflowInstance.id,\n stepInstanceId,\n eventType: 'ACTIVITY_QUEUED',\n eventData: {\n activityId: activity.activityId,\n activityName: activity.activityName,\n activityType: activity.activityType,\n async: true,\n jobId,\n },\n tenantId: workflowInstance.tenantId,\n organizationId: workflowInstance.organizationId,\n })\n\n return jobId\n}\n\n// ============================================================================\n// Main Activity Execution Functions\n// ============================================================================\n\n/**\n * Execute a single activity with retry logic and timeout\n *\n * @param em - Entity manager\n * @param container - DI container\n * @param activity - Activity definition\n * @param context - Execution context\n * @returns Execution result\n */\nexport async function executeActivity(\n em: EntityManager,\n container: AwilixContainer,\n activity: ActivityDefinition,\n context: ActivityContext\n): Promise<ActivityExecutionResult> {\n const retryPolicy = activity.retryPolicy || {\n maxAttempts: 1,\n initialIntervalMs: 0,\n backoffCoefficient: 1,\n maxIntervalMs: 0,\n }\n\n let lastError: any\n let retryCount = 0\n\n for (let attempt = 0; attempt < retryPolicy.maxAttempts; attempt++) {\n try {\n const startTime = Date.now()\n\n // Execute with timeout if specified\n const result = activity.timeoutMs\n ? await executeWithTimeout(\n () => executeActivityByType(em, container, activity, context),\n activity.timeoutMs\n )\n : await executeActivityByType(em, container, activity, context)\n\n const executionTimeMs = Date.now() - startTime\n\n return {\n activityId: activity.activityId,\n activityName: activity.activityName,\n activityType: activity.activityType,\n success: true,\n output: result,\n retryCount: attempt,\n executionTimeMs,\n async: activity.async || false,\n }\n } catch (error) {\n lastError = error\n retryCount = attempt + 1\n\n // If not the last attempt, apply backoff and retry\n if (attempt < retryPolicy.maxAttempts - 1) {\n const backoff = calculateBackoff(\n retryPolicy.initialIntervalMs,\n retryPolicy.backoffCoefficient,\n attempt,\n retryPolicy.maxIntervalMs\n )\n\n await sleep(backoff)\n }\n }\n }\n\n // All retries exhausted\n const errorMessage = lastError instanceof Error ? lastError.message : String(lastError)\n\n return {\n activityId: activity.activityId,\n activityName: activity.activityName,\n activityType: activity.activityType,\n success: false,\n error: `Activity failed after ${retryCount} attempts: ${errorMessage}`,\n retryCount,\n executionTimeMs: 0,\n async: activity.async || false,\n }\n}\n\n/**\n * Execute multiple activities in sequence\n * Supports both synchronous and asynchronous (queued) execution\n *\n * @param em - Entity manager\n * @param container - DI container\n * @param activities - Array of activity definitions\n * @param context - Execution context\n * @returns Array of execution results\n */\nexport async function executeActivities(\n em: EntityManager,\n container: AwilixContainer,\n activities: ActivityDefinition[],\n context: ActivityContext\n): Promise<ActivityExecutionResult[]> {\n const results: ActivityExecutionResult[] = []\n\n for (let i = 0; i < activities.length; i++) {\n const activity = activities[i]\n\n // Check if activity should run async\n if (activity.async) {\n // Enqueue for background execution\n const jobId = await enqueueActivity(em, activity, context)\n\n results.push({\n activityId: activity.activityId,\n activityName: activity.activityName,\n activityType: activity.activityType,\n success: true, // Queued successfully\n async: true,\n jobId,\n retryCount: 0,\n executionTimeMs: 0,\n })\n } else {\n // Execute synchronously (existing logic)\n const result = await executeActivity(em, container, activity, context)\n results.push(result)\n\n // Stop execution if activity fails (fail-fast)\n if (!result.success) {\n break\n }\n\n // Update workflow context with activity output\n if (result.output && typeof result.output === 'object') {\n const key = activity.activityName || activity.activityType\n context.workflowContext = {\n ...context.workflowContext,\n [key]: result.output,\n }\n }\n }\n }\n\n return results\n}\n\n// ============================================================================\n// Activity Type Handlers\n// ============================================================================\n\n/**\n * Execute activity based on its type\n */\nasync function executeActivityByType(\n em: EntityManager,\n container: AwilixContainer,\n activity: ActivityDefinition,\n context: ActivityContext\n): Promise<any> {\n // Interpolate config variables from context (including workflow metadata)\n const interpolatedConfig = interpolateVariables(activity.config, context.workflowContext, context.workflowInstance)\n\n switch (activity.activityType) {\n case 'SEND_EMAIL':\n return await executeSendEmail(interpolatedConfig, context, container)\n\n case 'CALL_API':\n return await executeCallApi(em, interpolatedConfig, context, container)\n\n case 'EMIT_EVENT':\n return await executeEmitEvent(interpolatedConfig, context, container)\n\n case 'UPDATE_ENTITY':\n return await executeUpdateEntity(em, interpolatedConfig, context, container)\n\n case 'CALL_WEBHOOK':\n return await executeCallWebhook(interpolatedConfig, context)\n\n case 'EXECUTE_FUNCTION':\n return await executeFunction(interpolatedConfig, context, container)\n\n default:\n throw new ActivityExecutionError(\n `Unknown activity type: ${activity.activityType}`,\n activity.activityType,\n activity.activityName\n )\n }\n}\n\n/**\n * SEND_EMAIL activity handler\n *\n * For MVP, this logs the email (actual email sending can be added later)\n */\nexport async function executeSendEmail(\n config: any,\n context: ActivityContext,\n container: AwilixContainer\n): Promise<any> {\n const { to, subject, template, templateData, body } = config\n\n if (!to || !subject) {\n throw new Error('SEND_EMAIL requires \"to\" and \"subject\" fields')\n }\n\n // For MVP: Log the email (actual email service integration can be added later)\n console.log(`[Workflow Activity] Send email to ${to}: ${subject}`)\n\n // Check if email service is available in container\n try {\n const emailService = container.resolve('emailService')\n if (emailService && typeof emailService.send === 'function') {\n await emailService.send({\n to,\n subject,\n template,\n templateData,\n body,\n })\n return { sent: true, to, subject, via: 'emailService' }\n }\n } catch (error) {\n // Email service not available, just log\n }\n\n return { sent: true, to, subject, via: 'console' }\n}\n\n/**\n * EMIT_EVENT activity handler\n *\n * Publishes a domain event to the event bus\n */\nexport async function executeEmitEvent(\n config: any,\n context: ActivityContext,\n container: AwilixContainer\n): Promise<any> {\n const { eventName, payload } = config\n\n if (!eventName) {\n throw new Error('EMIT_EVENT requires \"eventName\" field')\n }\n\n // Get event bus from container\n const eventBus = container.resolve('eventBus')\n\n if (!eventBus || typeof eventBus.emitEvent !== 'function') {\n throw new Error('Event bus not available in container')\n }\n\n // Publish event with workflow metadata\n const enrichedPayload = {\n ...payload,\n _workflow: {\n workflowInstanceId: context.workflowInstance.id,\n workflowId: context.workflowInstance.workflowId,\n tenantId: context.workflowInstance.tenantId,\n organizationId: context.workflowInstance.organizationId,\n },\n }\n\n await eventBus.emitEvent(eventName, enrichedPayload)\n\n return { emitted: true, eventName, payload: enrichedPayload }\n}\n\n/**\n * UPDATE_ENTITY activity handler\n *\n * Updates an entity via query engine\n */\nexport async function executeUpdateEntity(\n em: EntityManager,\n config: any,\n context: ActivityContext,\n container: AwilixContainer\n): Promise<any> {\n const { entityType, entityId, updates } = config\n\n if (!entityType || !entityId || !updates) {\n throw new Error('UPDATE_ENTITY requires \"entityType\", \"entityId\", and \"updates\" fields')\n }\n\n // Get query engine from container\n const queryEngine = container.resolve('queryEngine')\n\n if (!queryEngine || typeof queryEngine.update !== 'function') {\n throw new Error('Query engine not available in container')\n }\n\n // Execute update with tenant scoping\n await queryEngine.update({\n entity: entityType,\n where: { id: entityId },\n data: updates,\n tenantId: context.workflowInstance.tenantId,\n organizationId: context.workflowInstance.organizationId,\n })\n\n return { updated: true, entityType, entityId, updates }\n}\n\n/**\n * CALL_WEBHOOK activity handler\n *\n * Makes HTTP request to external URL\n */\nexport async function executeCallWebhook(\n config: any,\n context: ActivityContext\n): Promise<any> {\n const { url, method = 'POST', headers = {}, body } = config\n\n if (!url) {\n throw new Error('CALL_WEBHOOK requires \"url\" field')\n }\n\n // Make HTTP request\n const response = await fetch(url, {\n method,\n headers: {\n 'Content-Type': 'application/json',\n ...headers,\n },\n body: body ? JSON.stringify(body) : undefined,\n })\n\n // Parse response\n let result: any\n const contentType = response.headers.get('content-type')\n\n if (contentType && contentType.includes('application/json')) {\n result = await response.json()\n } else {\n result = await response.text()\n }\n\n // Check for HTTP errors\n if (!response.ok) {\n throw new Error(\n `Webhook request failed with status ${response.status}: ${JSON.stringify(result)}`\n )\n }\n\n return {\n status: response.status,\n statusText: response.statusText,\n result,\n }\n}\n\n/**\n * EXECUTE_FUNCTION activity handler\n *\n * Calls a registered function from DI container\n */\nexport async function executeFunction(\n config: any,\n context: ActivityContext,\n container: AwilixContainer\n): Promise<any> {\n const { functionName, args = {} } = config\n\n if (!functionName) {\n throw new Error('EXECUTE_FUNCTION requires \"functionName\" field')\n }\n\n // Look up function in container\n const fnKey = `workflowFunction:${functionName}`\n\n try {\n const fn = container.resolve(fnKey)\n\n if (typeof fn !== 'function') {\n throw new Error(`Registered workflow function \"${functionName}\" is not a function`)\n }\n\n // Call function with args and context\n const result = await fn(args, context)\n\n return { executed: true, functionName, result }\n } catch (error) {\n if (error instanceof Error && error.message.includes('not registered')) {\n throw new Error(\n `Workflow function \"${functionName}\" not registered in DI container (key: ${fnKey})`\n )\n }\n throw error\n }\n}\n\n/**\n * CALL_API activity handler\n *\n * Makes authenticated HTTP request to internal Open Mercato APIs\n * - Automatically creates one-time API key for authentication\n * - Injects tenant/organization context headers\n * - Validates URL security (SSRF prevention)\n * - Classifies errors (retriable vs non-retriable)\n * - Deletes API key after request (no stored credentials!)\n */\nexport async function executeCallApi(\n em: EntityManager,\n config: any,\n context: ActivityContext,\n container: AwilixContainer\n): Promise<any> {\n // 1. Interpolate variables in config (including {{workflow.*}}, {{context.*}}, {{env.*}}, {{now}})\n const interpolatedConfig = interpolateVariables(config, context.workflowContext, context.workflowInstance)\n\n const {\n endpoint,\n method = 'GET',\n headers = {},\n body,\n validateTenantMatch = true,\n } = interpolatedConfig\n\n\n if (!endpoint) {\n throw new Error('CALL_API requires \"endpoint\" field')\n }\n\n // 2. Build full URL (prepend APP_URL for relative paths)\n const fullUrl = buildApiUrl(endpoint)\n\n // 3. Import the one-time API key helper\n const { withOnetimeApiKey } = await import('../../api_keys/services/apiKeyService')\n\n // 4. Get EntityManager from container (for correct type)\n const apiKeyEm = container.resolve('em')\n\n // 5. Look up an admin role for the tenant to assign to the one-time key\n // CRITICAL: rolesJson must contain role IDs (UUIDs), not role names!\n const { Role } = await import('../../auth/data/entities')\n const adminRole = await apiKeyEm.findOne(Role, {\n tenantId: context.workflowInstance.tenantId,\n name: { $in: ['superadmin', 'admin', 'administrator'] } // Try common admin role names\n })\n\n if (!adminRole) {\n throw new Error(\n `[CALL_API] No admin role found for tenant ${context.workflowInstance.tenantId}. ` +\n `Cannot create one-time API key without role assignment. ` +\n `Ensure 'mercato init' has been run to create default roles.`\n )\n }\n\n // 6. Execute request with one-time API key (using role ID, not name)\n return await withOnetimeApiKey(\n apiKeyEm,\n {\n name: `__workflow_${context.workflowInstance.id}__`,\n description: `One-time key for workflow ${context.workflowInstance.workflowId} instance ${context.workflowInstance.id}`,\n tenantId: context.workflowInstance.tenantId,\n organizationId: context.workflowInstance.organizationId,\n roles: [adminRole.id], // \u2705 FIX: Use role ID (UUID), not role name\n expiresAt: null,\n },\n async (apiKeySecret) => {\n // Build request headers (auth + context + custom)\n const requestHeaders: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'Authorization': `apikey ${apiKeySecret}`,\n 'X-Tenant-Id': context.workflowInstance.tenantId,\n 'X-Organization-Id': context.workflowInstance.organizationId,\n 'X-Workflow-Instance-Id': context.workflowInstance.id,\n ...headers,\n }\n\n // Make HTTP request\n const response = await fetch(fullUrl, {\n method,\n headers: requestHeaders,\n body: body ? JSON.stringify(body) : undefined,\n })\n\n // Parse response body (JSON-safe)\n let responseBody: any\n const contentType = response.headers.get('content-type')\n\n try {\n if (contentType && contentType.includes('application/json')) {\n responseBody = await response.json()\n } else {\n responseBody = await response.text()\n }\n } catch (error) {\n responseBody = null\n }\n\n // Check for HTTP errors and classify\n if (!response.ok) {\n classifyAndThrowError(response.status, responseBody, fullUrl)\n }\n\n // Validate tenant match (security check)\n if (validateTenantMatch && responseBody && typeof responseBody === 'object') {\n if (responseBody.tenantId && responseBody.tenantId !== context.workflowInstance.tenantId) {\n throw new Error(\n `Tenant ID mismatch: workflow expects ${context.workflowInstance.tenantId} but API returned ${responseBody.tenantId}`\n )\n }\n }\n\n // Return structured result\n return {\n status: response.status,\n statusText: response.statusText,\n headers: Object.fromEntries(response.headers.entries()),\n body: responseBody,\n authenticated: true,\n tenantId: context.workflowInstance.tenantId,\n organizationId: context.workflowInstance.organizationId,\n }\n }\n )\n}\n\n// ============================================================================\n// CALL_API Helper Functions\n// ============================================================================\n\n/**\n * Build full API URL from endpoint\n * - Relative paths (/api/...) \u2192 prepend APP_URL\n * - Absolute URLs \u2192 validate domain matches APP_URL (SSRF prevention)\n */\nfunction buildApiUrl(endpoint: string): string {\n const appUrl = process.env.APP_URL || 'http://localhost:5050'\n\n // Relative path - prepend APP_URL\n if (endpoint.startsWith('/')) {\n // Security: Only allow /api/* paths\n if (!endpoint.startsWith('/api/')) {\n throw new Error(`CALL_API only supports /api/* paths, got: ${endpoint}`)\n }\n return `${appUrl}${endpoint}`\n }\n\n // Absolute URL - validate domain matches APP_URL (SSRF prevention)\n try {\n const endpointUrl = new URL(endpoint)\n const appUrlObj = new URL(appUrl)\n\n if (endpointUrl.host !== appUrlObj.host) {\n throw new Error(\n `SSRF Prevention: CALL_API endpoint domain (${endpointUrl.host}) does not match APP_URL (${appUrlObj.host})`\n )\n }\n\n return endpoint\n } catch (error) {\n if (error instanceof TypeError) {\n throw new Error(`Invalid endpoint URL: ${endpoint}`)\n }\n throw error\n }\n}\n\n/**\n * Classify HTTP error and throw appropriate error\n * - 400-499: Non-retriable (client error - validation/auth)\n * - 500-599: Retriable (server error)\n */\nfunction classifyAndThrowError(status: number, body: any, url: string): never {\n const bodyStr = typeof body === 'string' ? body : JSON.stringify(body)\n\n if (status >= 400 && status < 500) {\n // Client errors - non-retriable\n throw new Error(\n `CALL_API request failed with status ${status} (non-retriable): ${bodyStr}`\n )\n }\n\n if (status >= 500) {\n // Server errors - retriable\n const error: any = new Error(\n `CALL_API request failed with status ${status} (retriable): ${bodyStr}`\n )\n error.retriable = true\n throw error\n }\n\n // Other errors\n throw new Error(`CALL_API request failed with status ${status}: ${bodyStr}`)\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Interpolate variables in config from workflow context\n *\n * Supports syntax:\n * - {{context.field}} or {{context.nested.field}} - from workflow context\n * - {{workflow.instanceId}} - workflow instance ID\n * - {{workflow.tenantId}} - tenant ID\n * - {{workflow.organizationId}} - organization ID\n * - {{workflow.currentStepId}} - current step ID\n * - {{env.VAR_NAME}} - environment variables\n * - {{now}} - current ISO timestamp\n */\nfunction interpolateVariables(\n config: any,\n context: Record<string, any>,\n workflowInstance?: WorkflowInstance\n): any {\n if (typeof config === 'string') {\n // Check if this is a single variable reference (e.g., \"{{context.cart.items}}\")\n // This preserves the original type (array, object, number, boolean)\n const singleVarMatch = config.match(/^\\{\\{([^}]+)\\}\\}$/)\n\n if (singleVarMatch) {\n const trimmedPath = singleVarMatch[1].trim()\n\n // Handle {{workflow.*}} variables\n if (trimmedPath.startsWith('workflow.') && workflowInstance) {\n const workflowKey = trimmedPath.substring('workflow.'.length)\n switch (workflowKey) {\n case 'instanceId':\n return workflowInstance.id\n case 'tenantId':\n return workflowInstance.tenantId\n case 'organizationId':\n return workflowInstance.organizationId\n case 'currentStepId':\n return workflowInstance.currentStepId\n case 'workflowId':\n return workflowInstance.workflowId\n case 'version':\n return workflowInstance.version // Return as number\n default:\n return config // Return original if unknown\n }\n }\n\n // Handle {{env.*}} variables\n if (trimmedPath.startsWith('env.')) {\n const envKey = trimmedPath.substring('env.'.length)\n return process.env[envKey] ?? config\n }\n\n // Handle {{now}} - current timestamp\n if (trimmedPath === 'now') {\n return new Date().toISOString()\n }\n\n // Handle {{context.*}} variables (default behavior)\n const contextPath = trimmedPath.startsWith('context.')\n ? trimmedPath.substring('context.'.length)\n : trimmedPath\n\n const value = getNestedValue(context, contextPath)\n return value !== undefined ? value : config // Return raw value to preserve type\n }\n\n // Multiple interpolations or mixed text - return string\n return config.replace(/\\{\\{([^}]+)\\}\\}/g, (match, path) => {\n const trimmedPath = path.trim()\n\n // Handle {{workflow.*}} variables\n if (trimmedPath.startsWith('workflow.') && workflowInstance) {\n const workflowKey = trimmedPath.substring('workflow.'.length)\n switch (workflowKey) {\n case 'instanceId':\n return workflowInstance.id\n case 'tenantId':\n return workflowInstance.tenantId\n case 'organizationId':\n return workflowInstance.organizationId\n case 'currentStepId':\n return workflowInstance.currentStepId\n case 'workflowId':\n return workflowInstance.workflowId\n case 'version':\n return String(workflowInstance.version)\n default:\n return match // Unknown workflow key\n }\n }\n\n // Handle {{env.*}} variables\n if (trimmedPath.startsWith('env.')) {\n const envKey = trimmedPath.substring('env.'.length)\n const envValue = process.env[envKey]\n return envValue !== undefined ? envValue : match\n }\n\n // Handle {{now}} - current timestamp\n if (trimmedPath === 'now') {\n return new Date().toISOString()\n }\n\n // Handle {{context.*}} variables (default behavior)\n const contextPath = trimmedPath.startsWith('context.')\n ? trimmedPath.substring('context.'.length)\n : trimmedPath\n\n const value = getNestedValue(context, contextPath)\n return value !== undefined ? String(value) : match\n })\n }\n\n if (Array.isArray(config)) {\n return config.map((item) => interpolateVariables(item, context, workflowInstance))\n }\n\n if (config && typeof config === 'object') {\n const result: Record<string, any> = {}\n for (const [key, value] of Object.entries(config)) {\n result[key] = interpolateVariables(value, context, workflowInstance)\n }\n return result\n }\n\n return config\n}\n\n/**\n * Get nested value from object by path (e.g., \"user.email\")\n */\nfunction getNestedValue(obj: any, path: string): any {\n const parts = path.split('.')\n let value = obj\n\n for (const part of parts) {\n if (value && typeof value === 'object' && part in value) {\n value = value[part]\n } else {\n return undefined\n }\n }\n\n return value\n}\n\n/**\n * Calculate exponential backoff delay\n */\nfunction calculateBackoff(\n initialIntervalMs: number,\n backoffCoefficient: number,\n attempt: number,\n maxIntervalMs: number\n): number {\n const backoff = initialIntervalMs * Math.pow(backoffCoefficient, attempt)\n return Math.min(backoff, maxIntervalMs || Infinity)\n}\n\n/**\n * Sleep for specified milliseconds\n */\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/**\n * Execute a promise with timeout\n */\nasync function executeWithTimeout<T>(\n executor: () => Promise<T>,\n timeoutMs: number\n): Promise<T> {\n let timeoutId: NodeJS.Timeout\n\n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutId = setTimeout(() => {\n reject(new Error(`Activity execution timeout after ${timeoutMs}ms`))\n }, timeoutMs)\n })\n\n try {\n return await Promise.race([executor(), timeoutPromise])\n } finally {\n clearTimeout(timeoutId!)\n }\n}\n"],
5
5
  "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.4.2-canary-5035717565",
3
+ "version": "0.4.2-canary-802e036384",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -207,7 +207,7 @@
207
207
  }
208
208
  },
209
209
  "dependencies": {
210
- "@open-mercato/shared": "0.4.2-canary-5035717565",
210
+ "@open-mercato/shared": "0.4.2-canary-802e036384",
211
211
  "@xyflow/react": "^12.6.0",
212
212
  "date-fns": "^4.1.0",
213
213
  "date-fns-tz": "^3.2.0"
@@ -63,7 +63,7 @@ export function resolveApiDocsBaseUrl(): string {
63
63
  const appBase =
64
64
  process.env.NEXT_PUBLIC_APP_URL ||
65
65
  process.env.APP_URL ||
66
- 'http://localhost:3000'
66
+ 'http://localhost:5050'
67
67
 
68
68
  return appendApiSegment(appBase)
69
69
  }
@@ -49,7 +49,7 @@ describe('Business Rules API - Execute Endpoint', () => {
49
49
  test('should return 401 when not authenticated', async () => {
50
50
  mockGetAuthFromRequest.mockResolvedValue(null)
51
51
 
52
- const request = new Request('http://localhost:3000/api/business_rules/execute', {
52
+ const request = new Request('http://localhost:5050/api/business_rules/execute', {
53
53
  method: 'POST',
54
54
  body: JSON.stringify({
55
55
  entityType: 'WorkOrder',
@@ -65,7 +65,7 @@ describe('Business Rules API - Execute Endpoint', () => {
65
65
  })
66
66
 
67
67
  test('should return 400 for invalid JSON', async () => {
68
- const request = new Request('http://localhost:3000/api/business_rules/execute', {
68
+ const request = new Request('http://localhost:5050/api/business_rules/execute', {
69
69
  method: 'POST',
70
70
  body: 'invalid-json',
71
71
  })
@@ -77,7 +77,7 @@ describe('Business Rules API - Execute Endpoint', () => {
77
77
  })
78
78
 
79
79
  test('should return 400 for missing entityType', async () => {
80
- const request = new Request('http://localhost:3000/api/business_rules/execute', {
80
+ const request = new Request('http://localhost:5050/api/business_rules/execute', {
81
81
  method: 'POST',
82
82
  body: JSON.stringify({
83
83
  data: { status: 'ACTIVE' },
@@ -114,7 +114,7 @@ describe('Business Rules API - Execute Endpoint', () => {
114
114
  mockEm.create.mockReturnValue({})
115
115
  mockEm.persistAndFlush.mockResolvedValue(undefined)
116
116
 
117
- const request = new Request('http://localhost:3000/api/business_rules/execute', {
117
+ const request = new Request('http://localhost:5050/api/business_rules/execute', {
118
118
  method: 'POST',
119
119
  body: JSON.stringify({
120
120
  entityType: 'WorkOrder',
@@ -154,7 +154,7 @@ describe('Business Rules API - Execute Endpoint', () => {
154
154
 
155
155
  mockEm.find.mockResolvedValue([mockRule])
156
156
 
157
- const request = new Request('http://localhost:3000/api/business_rules/execute', {
157
+ const request = new Request('http://localhost:5050/api/business_rules/execute', {
158
158
  method: 'POST',
159
159
  body: JSON.stringify({
160
160
  entityType: 'WorkOrder',
@@ -214,7 +214,7 @@ describe('Business Rules API - Execute Endpoint', () => {
214
214
  mockEm.create.mockReturnValue({})
215
215
  mockEm.persistAndFlush.mockResolvedValue(undefined)
216
216
 
217
- const request = new Request('http://localhost:3000/api/business_rules/execute', {
217
+ const request = new Request('http://localhost:5050/api/business_rules/execute', {
218
218
  method: 'POST',
219
219
  body: JSON.stringify({
220
220
  entityType: 'WorkOrder',
@@ -231,7 +231,7 @@ describe('Business Rules API - Execute Endpoint', () => {
231
231
  test('should include entityId when provided', async () => {
232
232
  mockEm.find.mockResolvedValue([])
233
233
 
234
- const request = new Request('http://localhost:3000/api/business_rules/execute', {
234
+ const request = new Request('http://localhost:5050/api/business_rules/execute', {
235
235
  method: 'POST',
236
236
  body: JSON.stringify({
237
237
  entityType: 'WorkOrder',
@@ -247,7 +247,7 @@ describe('Business Rules API - Execute Endpoint', () => {
247
247
  test('should handle execution errors gracefully', async () => {
248
248
  mockEm.find.mockRejectedValue(new Error('Database error'))
249
249
 
250
- const request = new Request('http://localhost:3000/api/business_rules/execute', {
250
+ const request = new Request('http://localhost:5050/api/business_rules/execute', {
251
251
  method: 'POST',
252
252
  body: JSON.stringify({
253
253
  entityType: 'WorkOrder',
@@ -288,7 +288,7 @@ describe('Business Rules API - Execute Endpoint', () => {
288
288
  mockEm.create.mockReturnValue({})
289
289
  mockEm.persistAndFlush.mockResolvedValue(undefined)
290
290
 
291
- const request = new Request('http://localhost:3000/api/business_rules/execute', {
291
+ const request = new Request('http://localhost:5050/api/business_rules/execute', {
292
292
  method: 'POST',
293
293
  body: JSON.stringify({
294
294
  entityType: 'WorkOrder',
@@ -45,7 +45,7 @@ describe('Business Rules API - Individual Log Operations', () => {
45
45
  test('should return 401 when not authenticated', async () => {
46
46
  mockGetAuthFromRequest.mockResolvedValue(null)
47
47
 
48
- const request = new Request(`http://localhost:3000/api/business_rules/logs/${validLogId}`)
48
+ const request = new Request(`http://localhost:5050/api/business_rules/logs/${validLogId}`)
49
49
  const response = await detailGET(request, { params: { id: validLogId } })
50
50
 
51
51
  expect(response.status).toBe(401)
@@ -78,7 +78,7 @@ describe('Business Rules API - Individual Log Operations', () => {
78
78
 
79
79
  mockEm.findOne.mockResolvedValue(mockLog)
80
80
 
81
- const request = new Request(`http://localhost:3000/api/business_rules/logs/${validLogId}`)
81
+ const request = new Request(`http://localhost:5050/api/business_rules/logs/${validLogId}`)
82
82
  const response = await detailGET(request, { params: { id: validLogId } })
83
83
 
84
84
  expect(response.status).toBe(200)
@@ -118,7 +118,7 @@ describe('Business Rules API - Individual Log Operations', () => {
118
118
 
119
119
  mockEm.findOne.mockResolvedValue(mockLog)
120
120
 
121
- const request = new Request(`http://localhost:3000/api/business_rules/logs/${validLogId}`)
121
+ const request = new Request(`http://localhost:5050/api/business_rules/logs/${validLogId}`)
122
122
  const response = await detailGET(request, { params: { id: validLogId } })
123
123
 
124
124
  expect(response.status).toBe(200)
@@ -131,7 +131,7 @@ describe('Business Rules API - Individual Log Operations', () => {
131
131
  test('should return 404 if log not found', async () => {
132
132
  mockEm.findOne.mockResolvedValue(null)
133
133
 
134
- const request = new Request('http://localhost:3000/api/business_rules/logs/99999')
134
+ const request = new Request('http://localhost:5050/api/business_rules/logs/99999')
135
135
  const response = await detailGET(request, { params: { id: '99999' } })
136
136
 
137
137
  expect(response.status).toBe(404)
@@ -140,7 +140,7 @@ describe('Business Rules API - Individual Log Operations', () => {
140
140
  })
141
141
 
142
142
  test('should return 400 for invalid log id', async () => {
143
- const request = new Request('http://localhost:3000/api/business_rules/logs/invalid-id')
143
+ const request = new Request('http://localhost:5050/api/business_rules/logs/invalid-id')
144
144
  const response = await detailGET(request, { params: { id: 'invalid-id' } })
145
145
 
146
146
  expect(response.status).toBe(400)
@@ -151,7 +151,7 @@ describe('Business Rules API - Individual Log Operations', () => {
151
151
  test('should populate rule relationship', async () => {
152
152
  mockEm.findOne.mockResolvedValue(null)
153
153
 
154
- const request = new Request(`http://localhost:3000/api/business_rules/logs/${validLogId}`)
154
+ const request = new Request(`http://localhost:5050/api/business_rules/logs/${validLogId}`)
155
155
  await detailGET(request, { params: { id: validLogId } })
156
156
 
157
157
  expect(mockEm.findOne).toHaveBeenCalledWith(
@@ -164,7 +164,7 @@ describe('Business Rules API - Individual Log Operations', () => {
164
164
  test('should filter by tenant and organization', async () => {
165
165
  mockEm.findOne.mockResolvedValue(null)
166
166
 
167
- const request = new Request(`http://localhost:3000/api/business_rules/logs/${validLogId}`)
167
+ const request = new Request(`http://localhost:5050/api/business_rules/logs/${validLogId}`)
168
168
  await detailGET(request, { params: { id: validLogId } })
169
169
 
170
170
  expect(mockEm.findOne).toHaveBeenCalledWith(