@ebay-dt/bonfire-cli 0.1.0-alpha.3 → 0.1.0
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.
- package/README.md +42 -0
- package/dist/command.d.ts +21 -2
- package/dist/command.d.ts.map +1 -1
- package/dist/command.js +235 -1
- package/dist/command.js.map +1 -1
- package/dist/convert.d.ts +47 -0
- package/dist/convert.d.ts.map +1 -0
- package/dist/convert.js +509 -0
- package/dist/convert.js.map +1 -0
- package/package.json +5 -3
- package/templates/instance/context/PROTOTYPE.md +108 -0
- package/templates/instance/context/ROOT.md +23 -0
- package/templates/instance/context/hooks/block-env-reads.mjs +144 -0
- package/templates/instance/context/mcp.json +8 -0
- package/templates/instance/context/settings.json +19 -0
- package/templates/instance/context/skills/color-scheme/SKILL.md +98 -0
- package/templates/instance/context/skills/item-data/SKILL.md +145 -0
- package/templates/instance/context/skills/setup-evomcp/SKILL.md +73 -0
- package/templates/instance/next.config.ts +10 -0
- package/templates/instance/src/actions/agents.ts +15 -0
- package/templates/instance/src/actions/prototypes.ts +37 -0
- package/templates/instance/src/app/(prototypes)/layout.tsx +40 -0
- package/templates/instance/src/app/(system)/api/prototype-dirty/route.ts +4 -0
- package/templates/instance/src/app/(system)/auth/login/route.ts +4 -0
- package/templates/instance/src/app/(system)/auth/logout/route.ts +4 -0
- package/templates/instance/src/app/(system)/auth/status/route.ts +4 -0
- package/templates/instance/src/app/(system)/layout.tsx +20 -0
- package/templates/instance/src/app/(system)/login/page.tsx +14 -0
- package/templates/instance/src/app/(system)/page.tsx +33 -0
- package/templates/instance/src/app/api/items/[itemId]/route.ts +21 -0
- package/templates/instance/src/app/api/items/route.ts +11 -0
- package/templates/instance/src/app/layout.tsx +56 -0
- package/templates/instance/src/bonfire.ts +25 -0
- package/templates/instance/src/lib/items/gateway-schemas.ts +100 -0
- package/templates/instance/src/lib/items/server.ts +220 -0
- package/templates/instance/src/proxy.ts +8 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use server'
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
CreatePrototypeInput,
|
|
5
|
+
DuplicatePrototypeInput,
|
|
6
|
+
EditPrototypeInput,
|
|
7
|
+
PrototypeCommandInput,
|
|
8
|
+
} from '@ebay-dt/bonfire-core'
|
|
9
|
+
import { bonfire } from '@/bonfire'
|
|
10
|
+
|
|
11
|
+
export async function createPrototype(input: CreatePrototypeInput) {
|
|
12
|
+
return bonfire.prototypes.actions.create(input)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function duplicatePrototype(input: DuplicatePrototypeInput) {
|
|
16
|
+
return bonfire.prototypes.actions.duplicate(input)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function editPrototype(input: EditPrototypeInput) {
|
|
20
|
+
return bonfire.prototypes.actions.edit(input)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function deletePrototype(input: PrototypeCommandInput) {
|
|
24
|
+
return bonfire.prototypes.actions.delete(input)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function savePrototype(input: PrototypeCommandInput) {
|
|
28
|
+
return bonfire.prototypes.actions.save(input)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function discardPrototype(input: PrototypeCommandInput) {
|
|
32
|
+
return bonfire.prototypes.actions.discard(input)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function deployPrototype(input: PrototypeCommandInput) {
|
|
36
|
+
return bonfire.prototypes.actions.deploy(input)
|
|
37
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { notFound } from 'next/navigation'
|
|
3
|
+
import { AppHeader, PrototypeEnvironment } from '@ebay-dt/bonfire-react'
|
|
4
|
+
import { bonfire } from '@/bonfire'
|
|
5
|
+
import { openInClaudeCode, openInCursor } from '@/actions/agents'
|
|
6
|
+
|
|
7
|
+
export default async function PrototypesLayout({
|
|
8
|
+
children,
|
|
9
|
+
}: Readonly<{ children: ReactNode }>) {
|
|
10
|
+
const [{ headerProps }, prototype] = await Promise.all([
|
|
11
|
+
bonfire.auth.getAppLayoutModel(),
|
|
12
|
+
bonfire.prototypes.getCurrentRouteModel(),
|
|
13
|
+
])
|
|
14
|
+
|
|
15
|
+
if (prototype === null) notFound()
|
|
16
|
+
|
|
17
|
+
const agentActions = { openInClaudeCode, openInCursor }
|
|
18
|
+
const header = <AppHeader {...headerProps} agentActions={agentActions} />
|
|
19
|
+
|
|
20
|
+
if (prototype.theme === undefined) {
|
|
21
|
+
return (
|
|
22
|
+
<>
|
|
23
|
+
{header}
|
|
24
|
+
{children}
|
|
25
|
+
</>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<PrototypeEnvironment
|
|
31
|
+
prototypeUrl={prototype.url}
|
|
32
|
+
theme={prototype.theme}
|
|
33
|
+
themeRegistry={prototype.themeRegistry}
|
|
34
|
+
agentActions={agentActions}
|
|
35
|
+
>
|
|
36
|
+
{header}
|
|
37
|
+
{children}
|
|
38
|
+
</PrototypeEnvironment>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { AppHeader } from '@ebay-dt/bonfire-react'
|
|
3
|
+
import { bonfire } from '@/bonfire'
|
|
4
|
+
import { openInClaudeCode, openInCursor } from '@/actions/agents'
|
|
5
|
+
|
|
6
|
+
export default async function SystemLayout({
|
|
7
|
+
children,
|
|
8
|
+
}: Readonly<{ children: ReactNode }>) {
|
|
9
|
+
const { headerProps } = await bonfire.auth.getAppLayoutModel()
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<>
|
|
13
|
+
<AppHeader
|
|
14
|
+
{...headerProps}
|
|
15
|
+
agentActions={{ openInClaudeCode, openInCursor }}
|
|
16
|
+
/>
|
|
17
|
+
{children}
|
|
18
|
+
</>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { GitHubCliLoginPage } from '@ebay-dt/bonfire-react'
|
|
2
|
+
import { bonfire } from '@/bonfire'
|
|
3
|
+
import type { Metadata } from 'next'
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = { title: 'Sign in — Design Sandbox' }
|
|
6
|
+
|
|
7
|
+
type LoginPageProps = {
|
|
8
|
+
searchParams?: Promise<Record<string, string | string[] | undefined>>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default async function LoginPage({ searchParams }: LoginPageProps) {
|
|
12
|
+
const model = await bonfire.auth.getLoginPageModel(searchParams)
|
|
13
|
+
return <GitHubCliLoginPage {...model} />
|
|
14
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { PrototypeDashboard } from '@ebay-dt/bonfire-react'
|
|
2
|
+
import { bonfire } from '@/bonfire'
|
|
3
|
+
import { openInClaudeCode, openInCursor } from '@/actions/agents'
|
|
4
|
+
import type { Metadata } from 'next'
|
|
5
|
+
|
|
6
|
+
interface PageProps {
|
|
7
|
+
searchParams: Promise<Record<string, string | string[] | undefined>>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const metadata: Metadata = { title: 'Dashboard — Design Sandbox' }
|
|
11
|
+
|
|
12
|
+
export default async function DashboardPage({ searchParams }: PageProps) {
|
|
13
|
+
const [dashboard, user] = await Promise.all([
|
|
14
|
+
bonfire.prototypes.getDashboardModel(searchParams),
|
|
15
|
+
bonfire.auth.getCurrentUser(),
|
|
16
|
+
])
|
|
17
|
+
const lifecycleEnabled = process.env.NODE_ENV !== 'production'
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<PrototypeDashboard
|
|
21
|
+
{...dashboard}
|
|
22
|
+
user={user}
|
|
23
|
+
{...(lifecycleEnabled
|
|
24
|
+
? {
|
|
25
|
+
agentActions: {
|
|
26
|
+
openInCursor,
|
|
27
|
+
openInClaudeCode,
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
: {})}
|
|
31
|
+
/>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createItemDetailRoute,
|
|
3
|
+
type ItemRouteDiagnostic,
|
|
4
|
+
} from '@ebay-dt/bonfire-preset-ebay/data/items/server'
|
|
5
|
+
import { getItemById } from '@/lib/items/server'
|
|
6
|
+
|
|
7
|
+
function onDiagnostic({ operation, code }: ItemRouteDiagnostic) {
|
|
8
|
+
console.error(`Item ${operation} failed (${code}).`)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const handleItemDetail = createItemDetailRoute(
|
|
12
|
+
{ getItemById },
|
|
13
|
+
{ onDiagnostic }
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
export async function GET(
|
|
17
|
+
request: Request,
|
|
18
|
+
context: RouteContext<'/api/items/[itemId]'>
|
|
19
|
+
) {
|
|
20
|
+
return handleItemDetail(request, context)
|
|
21
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createItemSearchRoute,
|
|
3
|
+
type ItemRouteDiagnostic,
|
|
4
|
+
} from '@ebay-dt/bonfire-preset-ebay/data/items/server'
|
|
5
|
+
import { searchItems } from '@/lib/items/server'
|
|
6
|
+
|
|
7
|
+
function onDiagnostic({ operation, code }: ItemRouteDiagnostic) {
|
|
8
|
+
console.error(`Item ${operation} failed (${code}).`)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const GET = createItemSearchRoute({ searchItems }, { onDiagnostic })
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Metadata } from 'next'
|
|
2
|
+
import { NextLifecycleProvider } from '@ebay-dt/bonfire-next/client'
|
|
3
|
+
import { EbayDesignSystemProvider } from '@ebay-dt/bonfire-preset-ebay'
|
|
4
|
+
import { bonfire } from '@/bonfire'
|
|
5
|
+
import {
|
|
6
|
+
createPrototype,
|
|
7
|
+
deletePrototype,
|
|
8
|
+
discardPrototype,
|
|
9
|
+
duplicatePrototype,
|
|
10
|
+
editPrototype,
|
|
11
|
+
deployPrototype,
|
|
12
|
+
savePrototype,
|
|
13
|
+
} from '@/actions/prototypes'
|
|
14
|
+
import '@ebay-dt/bonfire-react/styles/defaults.css'
|
|
15
|
+
import '@ebay-dt/bonfire-preset-ebay/styles.css'
|
|
16
|
+
|
|
17
|
+
export const metadata: Metadata = {
|
|
18
|
+
title: 'Design Sandbox',
|
|
19
|
+
description: 'eBay Design Technology prototype sandbox',
|
|
20
|
+
icons: { icon: 'https://fav.farm/🤙' },
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default async function RootLayout({
|
|
24
|
+
children,
|
|
25
|
+
}: Readonly<{
|
|
26
|
+
children: React.ReactNode
|
|
27
|
+
}>) {
|
|
28
|
+
const lifecycleEnabled = process.env.NODE_ENV !== 'production'
|
|
29
|
+
const { htmlProps, headerProps } = await bonfire.auth.getAppLayoutModel()
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<html lang="en" {...htmlProps}>
|
|
33
|
+
<body>
|
|
34
|
+
<EbayDesignSystemProvider>
|
|
35
|
+
<NextLifecycleProvider
|
|
36
|
+
enabled={lifecycleEnabled}
|
|
37
|
+
deploymentAvailable={bonfire.prototypes.capabilities.deployment}
|
|
38
|
+
user={headerProps.user}
|
|
39
|
+
dirtyEndpoint="/api/prototype-dirty"
|
|
40
|
+
actions={{
|
|
41
|
+
create: createPrototype,
|
|
42
|
+
duplicate: duplicatePrototype,
|
|
43
|
+
edit: editPrototype,
|
|
44
|
+
delete: deletePrototype,
|
|
45
|
+
save: savePrototype,
|
|
46
|
+
discard: discardPrototype,
|
|
47
|
+
...(lifecycleEnabled ? { deploy: deployPrototype } : {}),
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
{children}
|
|
51
|
+
</NextLifecycleProvider>
|
|
52
|
+
</EbayDesignSystemProvider>
|
|
53
|
+
</body>
|
|
54
|
+
</html>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import 'server-only'
|
|
2
|
+
import { themes } from '@ebay-dt/bonfire-preset-ebay'
|
|
3
|
+
import { projectPrototypeContext } from '@ebay-dt/bonfire-context'
|
|
4
|
+
import {
|
|
5
|
+
createBonfireNext,
|
|
6
|
+
createDeploymentProvider,
|
|
7
|
+
} from '@ebay-dt/bonfire-next/server'
|
|
8
|
+
const deploymentProvider = createDeploymentProvider({
|
|
9
|
+
endpoint: 'https://dtsvc.vip.qa.ebay.com/dt/v1/bonfire-deploy',
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
export const bonfire = createBonfireNext({
|
|
13
|
+
appId: 'design-sandbox-next',
|
|
14
|
+
name: 'Design Sandbox',
|
|
15
|
+
auth: { github: { hostname: 'github.corp.ebay.com' } },
|
|
16
|
+
themes,
|
|
17
|
+
repositoryRoot: process.cwd(),
|
|
18
|
+
primaryBranch: process.env.BF_PROTOTYPE_BASE_BRANCH,
|
|
19
|
+
sessionSecret: process.env.BF_SESSION_SECRET,
|
|
20
|
+
disableAutoBranchSwitch: process.env.BF_DISABLE_AUTO_BRANCH_SWITCH,
|
|
21
|
+
deploymentProvider,
|
|
22
|
+
preparePrototypeFiles: async ({ prototypePath, repositoryRoot }) => {
|
|
23
|
+
await projectPrototypeContext({ repositoryRoot, prototypePath })
|
|
24
|
+
},
|
|
25
|
+
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
const nullableString = z.string().nullable().optional()
|
|
4
|
+
const nullableNumber = z.number().nullable().optional()
|
|
5
|
+
const gatewayImageSchema = z.object({ imageUrl: nullableString }).nullable()
|
|
6
|
+
const gatewayPriceSchema = z
|
|
7
|
+
.object({
|
|
8
|
+
value: z.union([z.string(), z.number()]).nullable().optional(),
|
|
9
|
+
currency: nullableString,
|
|
10
|
+
convertedFromValue: z.union([z.string(), z.number()]).nullable().optional(),
|
|
11
|
+
})
|
|
12
|
+
.nullable()
|
|
13
|
+
|
|
14
|
+
export const gatewayErrorsSchema = z
|
|
15
|
+
.array(z.object({ message: nullableString }).passthrough())
|
|
16
|
+
.optional()
|
|
17
|
+
|
|
18
|
+
const gatewaySearchItemSchema = z
|
|
19
|
+
.object({
|
|
20
|
+
itemId: nullableString,
|
|
21
|
+
title: nullableString,
|
|
22
|
+
itemWebUrl: nullableString,
|
|
23
|
+
image: gatewayImageSchema.optional(),
|
|
24
|
+
condition: nullableString,
|
|
25
|
+
price: gatewayPriceSchema.optional(),
|
|
26
|
+
marketingPrice: z
|
|
27
|
+
.object({ originalPrice: gatewayPriceSchema.optional() })
|
|
28
|
+
.nullable()
|
|
29
|
+
.optional(),
|
|
30
|
+
})
|
|
31
|
+
.nullable()
|
|
32
|
+
|
|
33
|
+
export const gatewaySearchResponseSchema = z.object({
|
|
34
|
+
data: z
|
|
35
|
+
.object({
|
|
36
|
+
itemsBySearch: z
|
|
37
|
+
.object({
|
|
38
|
+
total: nullableNumber,
|
|
39
|
+
limit: nullableNumber,
|
|
40
|
+
offset: nullableNumber,
|
|
41
|
+
items: z.array(gatewaySearchItemSchema).nullable().optional(),
|
|
42
|
+
})
|
|
43
|
+
.nullable()
|
|
44
|
+
.optional(),
|
|
45
|
+
})
|
|
46
|
+
.optional(),
|
|
47
|
+
errors: gatewayErrorsSchema,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const gatewayItemSchema = z.object({
|
|
51
|
+
itemId: nullableString,
|
|
52
|
+
legacyItemId: nullableString,
|
|
53
|
+
title: nullableString,
|
|
54
|
+
bidCount: nullableNumber,
|
|
55
|
+
watchCount: nullableNumber,
|
|
56
|
+
brand: nullableString,
|
|
57
|
+
categories: z
|
|
58
|
+
.array(z.object({ categoryId: nullableString }).nullable())
|
|
59
|
+
.nullable()
|
|
60
|
+
.optional(),
|
|
61
|
+
categoryPath: nullableString,
|
|
62
|
+
color: nullableString,
|
|
63
|
+
conditionId: nullableString,
|
|
64
|
+
condition: nullableString,
|
|
65
|
+
conditionDescription: nullableString,
|
|
66
|
+
gender: nullableString,
|
|
67
|
+
image: gatewayImageSchema.optional(),
|
|
68
|
+
additionalImages: z.array(gatewayImageSchema).nullable().optional(),
|
|
69
|
+
localizedAspects: z
|
|
70
|
+
.array(
|
|
71
|
+
z
|
|
72
|
+
.object({
|
|
73
|
+
type: nullableString,
|
|
74
|
+
name: nullableString,
|
|
75
|
+
value: nullableString,
|
|
76
|
+
})
|
|
77
|
+
.nullable()
|
|
78
|
+
)
|
|
79
|
+
.nullable()
|
|
80
|
+
.optional(),
|
|
81
|
+
price: gatewayPriceSchema.optional(),
|
|
82
|
+
shippingOptions: z
|
|
83
|
+
.array(
|
|
84
|
+
z
|
|
85
|
+
.object({
|
|
86
|
+
shippingCarrierCode: nullableString,
|
|
87
|
+
quantityUsedForEstimate: nullableNumber,
|
|
88
|
+
trademarkSymbol: nullableString,
|
|
89
|
+
type: nullableString,
|
|
90
|
+
})
|
|
91
|
+
.nullable()
|
|
92
|
+
)
|
|
93
|
+
.nullable()
|
|
94
|
+
.optional(),
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
export const gatewayItemResponseSchema = z.object({
|
|
98
|
+
data: z.object({ item: gatewayItemSchema.nullable().optional() }).optional(),
|
|
99
|
+
errors: gatewayErrorsSchema,
|
|
100
|
+
})
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import 'server-only'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
itemDetailSchema,
|
|
5
|
+
itemSearchPageSchema,
|
|
6
|
+
type ItemDetail,
|
|
7
|
+
type ItemSearchPage,
|
|
8
|
+
} from '@ebay-dt/bonfire-preset-ebay/data/items'
|
|
9
|
+
import {
|
|
10
|
+
gatewayItemResponseSchema,
|
|
11
|
+
gatewaySearchResponseSchema,
|
|
12
|
+
} from './gateway-schemas'
|
|
13
|
+
|
|
14
|
+
const DEFAULT_GATEWAY_URL = 'https://dtsvc.vip.qa.ebay.com/dt/v1/gateway-gql'
|
|
15
|
+
|
|
16
|
+
const SEARCH_QUERY = `
|
|
17
|
+
query Search($q: String!, $limit: Int!, $offset: Int!) {
|
|
18
|
+
itemsBySearch(q: $q, limit: $limit, offset: $offset) {
|
|
19
|
+
items {
|
|
20
|
+
itemId title itemWebUrl image { imageUrl } condition
|
|
21
|
+
price { value currency }
|
|
22
|
+
marketingPrice { originalPrice { value currency } }
|
|
23
|
+
}
|
|
24
|
+
total limit offset
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
`
|
|
28
|
+
|
|
29
|
+
const ITEM_QUERY = `
|
|
30
|
+
query ItemById($id: String!) {
|
|
31
|
+
item(id: $id) {
|
|
32
|
+
itemId legacyItemId title bidCount watchCount brand
|
|
33
|
+
categories { categoryId }
|
|
34
|
+
categoryPath color conditionId condition conditionDescription gender
|
|
35
|
+
image { imageUrl }
|
|
36
|
+
additionalImages { imageUrl }
|
|
37
|
+
localizedAspects { type name value }
|
|
38
|
+
price { convertedFromValue value }
|
|
39
|
+
shippingOptions {
|
|
40
|
+
shippingCarrierCode quantityUsedForEstimate trademarkSymbol type
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
`
|
|
45
|
+
|
|
46
|
+
async function requestGateway(
|
|
47
|
+
operationName: string,
|
|
48
|
+
query: string,
|
|
49
|
+
variables: Record<string, unknown>,
|
|
50
|
+
signal: AbortSignal
|
|
51
|
+
): Promise<unknown> {
|
|
52
|
+
const endpoint = process.env.DT_GATEWAY_URL?.trim() || DEFAULT_GATEWAY_URL
|
|
53
|
+
const apiKey = process.env.DT_GATEWAY_API_KEY?.trim()
|
|
54
|
+
const response = await fetch(endpoint, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: {
|
|
57
|
+
'Content-Type': 'application/json',
|
|
58
|
+
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
|
59
|
+
},
|
|
60
|
+
body: JSON.stringify({ operationName, query, variables }),
|
|
61
|
+
cache: 'no-store',
|
|
62
|
+
signal,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
let result: unknown
|
|
66
|
+
try {
|
|
67
|
+
result = await response.json()
|
|
68
|
+
} catch {
|
|
69
|
+
throw new Error(`The item gateway returned HTTP ${response.status}.`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
throw new Error(`The item gateway returned HTTP ${response.status}.`)
|
|
74
|
+
}
|
|
75
|
+
return result
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function graphQLError(errors?: Array<{ message?: string | null }>) {
|
|
79
|
+
return errors
|
|
80
|
+
?.map(({ message }) => message)
|
|
81
|
+
.filter(Boolean)
|
|
82
|
+
.join('; ')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function optionalNumber(value: string | number | null | undefined) {
|
|
86
|
+
if (value == null || value === '') return undefined
|
|
87
|
+
const number = typeof value === 'number' ? value : Number(value)
|
|
88
|
+
return Number.isFinite(number) ? number : undefined
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function searchItems(
|
|
92
|
+
query: string,
|
|
93
|
+
limit: number,
|
|
94
|
+
offset: number,
|
|
95
|
+
signal: AbortSignal
|
|
96
|
+
): Promise<ItemSearchPage> {
|
|
97
|
+
const result = gatewaySearchResponseSchema.parse(
|
|
98
|
+
await requestGateway(
|
|
99
|
+
'Search',
|
|
100
|
+
SEARCH_QUERY,
|
|
101
|
+
{ q: query, limit, offset },
|
|
102
|
+
signal
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
const message = graphQLError(result.errors)
|
|
106
|
+
if (message) throw new Error(message)
|
|
107
|
+
|
|
108
|
+
const search = result.data?.itemsBySearch
|
|
109
|
+
return itemSearchPageSchema.parse({
|
|
110
|
+
total: search?.total ?? 0,
|
|
111
|
+
limit: search?.limit ?? limit,
|
|
112
|
+
offset: search?.offset ?? offset,
|
|
113
|
+
items:
|
|
114
|
+
search?.items?.flatMap((item) => {
|
|
115
|
+
if (!item?.itemId || !item.title || !item.itemWebUrl) return []
|
|
116
|
+
const priceValue = optionalNumber(item.price?.value)
|
|
117
|
+
const previousPriceValue = optionalNumber(
|
|
118
|
+
item.marketingPrice?.originalPrice?.value
|
|
119
|
+
)
|
|
120
|
+
return [
|
|
121
|
+
{
|
|
122
|
+
id: item.itemId,
|
|
123
|
+
title: item.title,
|
|
124
|
+
itemUrl: item.itemWebUrl,
|
|
125
|
+
imageUrl: item.image?.imageUrl ?? undefined,
|
|
126
|
+
condition: item.condition ?? undefined,
|
|
127
|
+
price:
|
|
128
|
+
priceValue != null && item.price?.currency
|
|
129
|
+
? { value: priceValue, currency: item.price.currency }
|
|
130
|
+
: undefined,
|
|
131
|
+
previousPrice:
|
|
132
|
+
previousPriceValue != null &&
|
|
133
|
+
item.marketingPrice?.originalPrice?.currency
|
|
134
|
+
? {
|
|
135
|
+
value: previousPriceValue,
|
|
136
|
+
currency: item.marketingPrice.originalPrice.currency,
|
|
137
|
+
}
|
|
138
|
+
: undefined,
|
|
139
|
+
},
|
|
140
|
+
]
|
|
141
|
+
}) ?? [],
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function getItemById(
|
|
146
|
+
itemId: string,
|
|
147
|
+
signal: AbortSignal
|
|
148
|
+
): Promise<ItemDetail | null> {
|
|
149
|
+
const result = gatewayItemResponseSchema.parse(
|
|
150
|
+
await requestGateway('ItemById', ITEM_QUERY, { id: itemId }, signal)
|
|
151
|
+
)
|
|
152
|
+
const message = graphQLError(result.errors)
|
|
153
|
+
if (message) throw new Error(message)
|
|
154
|
+
const item = result.data?.item
|
|
155
|
+
if (!item) return null
|
|
156
|
+
if (!item.itemId || !item.title) {
|
|
157
|
+
throw new Error('The item gateway returned an incomplete item.')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const imageUrls = [
|
|
161
|
+
item.image?.imageUrl,
|
|
162
|
+
...(item.additionalImages?.map((image) => image?.imageUrl) ?? []),
|
|
163
|
+
].filter((url): url is string => Boolean(url))
|
|
164
|
+
const priceValue = optionalNumber(item.price?.value)
|
|
165
|
+
const convertedFromValue = optionalNumber(item.price?.convertedFromValue)
|
|
166
|
+
|
|
167
|
+
return itemDetailSchema.parse({
|
|
168
|
+
id: item.itemId,
|
|
169
|
+
legacyId: item.legacyItemId ?? undefined,
|
|
170
|
+
title: item.title,
|
|
171
|
+
images: [...new Set(imageUrls)].map((url) => ({ url })),
|
|
172
|
+
price:
|
|
173
|
+
priceValue == null
|
|
174
|
+
? undefined
|
|
175
|
+
: { value: priceValue, convertedFromValue },
|
|
176
|
+
conditionId: item.conditionId ?? undefined,
|
|
177
|
+
condition: item.condition ?? undefined,
|
|
178
|
+
conditionDescription: item.conditionDescription ?? undefined,
|
|
179
|
+
brand: item.brand ?? undefined,
|
|
180
|
+
color: item.color ?? undefined,
|
|
181
|
+
gender: item.gender ?? undefined,
|
|
182
|
+
categoryPath:
|
|
183
|
+
item.categoryPath
|
|
184
|
+
?.split('|')
|
|
185
|
+
.map((part) => part.trim())
|
|
186
|
+
.filter(Boolean) ?? [],
|
|
187
|
+
categoryIds:
|
|
188
|
+
item.categories?.flatMap((category) =>
|
|
189
|
+
category?.categoryId ? [category.categoryId] : []
|
|
190
|
+
) ?? [],
|
|
191
|
+
aspects:
|
|
192
|
+
item.localizedAspects?.flatMap((aspect) =>
|
|
193
|
+
aspect?.name && aspect.value
|
|
194
|
+
? [
|
|
195
|
+
{
|
|
196
|
+
name: aspect.name,
|
|
197
|
+
value: aspect.value,
|
|
198
|
+
type: aspect.type ?? undefined,
|
|
199
|
+
},
|
|
200
|
+
]
|
|
201
|
+
: []
|
|
202
|
+
) ?? [],
|
|
203
|
+
shippingOptions:
|
|
204
|
+
item.shippingOptions?.flatMap((option) =>
|
|
205
|
+
option
|
|
206
|
+
? [
|
|
207
|
+
{
|
|
208
|
+
carrierCode: option.shippingCarrierCode ?? undefined,
|
|
209
|
+
quantityUsedForEstimate:
|
|
210
|
+
option.quantityUsedForEstimate ?? undefined,
|
|
211
|
+
trademarkSymbol: option.trademarkSymbol ?? undefined,
|
|
212
|
+
type: option.type ?? undefined,
|
|
213
|
+
},
|
|
214
|
+
]
|
|
215
|
+
: []
|
|
216
|
+
) ?? [],
|
|
217
|
+
bidCount: item.bidCount ?? undefined,
|
|
218
|
+
watchCount: item.watchCount ?? undefined,
|
|
219
|
+
})
|
|
220
|
+
}
|