create-cartbase 0.1.15 → 0.1.17
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/LICENSE +21 -21
- package/README.md +25 -25
- package/dist/index.js +20 -20
- package/package.json +24 -24
- package/template/app/docs/auth.md +105 -105
- package/template/app/docs/carts.md +376 -376
- package/template/app/docs/categories.md +194 -194
- package/template/app/docs/checkout.md +714 -714
- package/template/app/docs/components.md +50 -13
- package/template/app/docs/consent.md +91 -91
- package/template/app/docs/deploy.md +197 -197
- package/template/app/docs/gift-cards.md +153 -153
- package/template/app/docs/metaobjects.md +126 -126
- package/template/app/docs/orders.md +221 -221
- package/template/app/docs/products.md +51 -2
- package/template/app/docs/regions.md +269 -269
- package/template/app/docs/reviews.md +223 -223
- package/template/app/docs/search.md +227 -227
- package/template/app/docs/store.md +47 -47
- package/template/app/docs/subscriptions.md +148 -148
- package/template/app/docs/variables.md +315 -315
- package/template/app/next.config.ts +6 -4
- package/template/app/package.json +1 -1
- package/template/app/postcss.config.cjs +11 -11
- package/template/app/src/app/checkout/checkout-page-client.tsx +73 -73
- package/template/app/src/app/checkout/page.tsx +48 -48
- package/template/app/src/app/globals.css +26 -26
- package/template/app/src/app/page.tsx +28 -28
- package/template/app/src/app/products/[handle]/page.tsx +87 -87
- package/template/app/src/app/providers.tsx +64 -64
- package/template/app/src/app/search/page.tsx +23 -23
- package/template/app/src/lib/browser-client.ts +35 -35
- package/template/app/src/lib/config.ts +41 -41
- package/template/app/src/lib/server-client.ts +25 -25
- package/template/app/src/lib/cart-actions.ts +0 -47
|
@@ -1,153 +1,153 @@
|
|
|
1
|
-
# Gift cards
|
|
2
|
-
|
|
3
|
-
Gift-card redemption is a **payment tender** (`pp_giftcard`), never a
|
|
4
|
-
discount: cart totals and VAT compute first and **never move**; applied
|
|
5
|
-
cards cover part (or all) of `cart.total` and the remainder provider
|
|
6
|
-
(Stripe/COD/manual) charges only what is left. Balances derive from an
|
|
7
|
-
append-only transactions ledger — resolved LIVE on every cart read, so a
|
|
8
|
-
balance spent elsewhere shrinks this cart's tender instead of
|
|
9
|
-
over-redeeming. Actual redemption happens ONLY at cart complete, atomically.
|
|
10
|
-
|
|
11
|
-
**Auth for every endpoint on this page:** anon `x-client-id`.
|
|
12
|
-
|
|
13
|
-
SDK module: `@cartbase/storefront/api/gift-cards`. Zero-remainder checkout
|
|
14
|
-
(cards cover everything) is part of the Buy-click sequence — see
|
|
15
|
-
[checkout.md](checkout.md).
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## Cart decoration (every cart read)
|
|
20
|
-
|
|
21
|
-
Every cart response carries three tender fields (declared on the `Cart` DTO
|
|
22
|
-
in `@cartbase/storefront/api/carts`; see [carts.md](carts.md) for the full
|
|
23
|
-
cart shape):
|
|
24
|
-
|
|
25
|
-
```jsonc
|
|
26
|
-
{
|
|
27
|
-
"cart": {
|
|
28
|
-
// …totals (NEVER moved by gift cards)…
|
|
29
|
-
"total": 54,
|
|
30
|
-
"gift_cards": [ // apply order; each covers min(live balance, remaining total)
|
|
31
|
-
{ "id": "gift_…", "last4": "PQRS", "amount": 25 }
|
|
32
|
-
// a disabled/expired/depleted card stays listed at amount: 0
|
|
33
|
-
],
|
|
34
|
-
"gift_card_total": 25, // Σ coverage = the pp_giftcard session amount
|
|
35
|
-
"gift_card_remainder": 29 // max(total − gift_card_total, 0) — what the remainder provider charges
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
# The decoration fields exist on every cart read (empty state shown here).
|
|
42
|
-
CART_JSON=$(curl -sf -X POST "$BASE/api/store/carts" \
|
|
43
|
-
-H "x-client-id: $CLIENT_ID" -H "content-type: application/json" \
|
|
44
|
-
-d '{"email":"gc-doc-'"$RUN"'@example.test",
|
|
45
|
-
"items":[{"variant_id":"variant_01tst000000000000000002","quantity":1}]}')
|
|
46
|
-
CART_ID=$(echo "$CART_JSON" | grep -o '"id":"cart_[^"]*"' | head -1 | cut -d'"' -f4)
|
|
47
|
-
echo "$CART_JSON" | grep -q '"gift_cards":\[\]'
|
|
48
|
-
echo "$CART_JSON" | grep -q '"gift_card_total":0'
|
|
49
|
-
echo "$CART_JSON" | grep -q '"gift_card_remainder"'
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
---
|
|
53
|
-
|
|
54
|
-
## POST /api/store/carts/:id/gift-cards — apply a code
|
|
55
|
-
|
|
56
|
-
- **Purpose** — apply a gift-card code to an open cart (idempotent
|
|
57
|
-
re-apply).
|
|
58
|
-
- **Auth** — anon `x-client-id`.
|
|
59
|
-
- **Request** — `{code}` (`.strict()`, 4–64 chars). The code travels ONCE,
|
|
60
|
-
here; it is hashed at rest and never echoed back.
|
|
61
|
-
- **Response** — `200 {cart, gift_card}` — a MASKED confirmation:
|
|
62
|
-
|
|
63
|
-
```jsonc
|
|
64
|
-
{
|
|
65
|
-
"cart": { "…": "decorated cart — gift_cards/gift_card_total/gift_card_remainder updated, total untouched" },
|
|
66
|
-
"gift_card": {
|
|
67
|
-
"id": "gift_…",
|
|
68
|
-
"last4": "PQRS", // last 4 chars of the code — never the code
|
|
69
|
-
"amount_applied": 25 // how much of THIS cart this card covers right now
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
- **Errors** — deliberately a single generic answer, **no code-existence
|
|
75
|
-
oracle**: unknown, disabled, expired, depleted and foreign-tenant codes
|
|
76
|
-
are all `400 {code: "invalid_gift_card"}`. Attempts are recorded BEFORE
|
|
77
|
-
lookup and rate-limited per cart (10 / 15 min) AND per IP (30 / 15 min) →
|
|
78
|
-
`429 {code: "rate_limited"}` — a valid code inside a burned window is
|
|
79
|
-
refused the same way. Also 404 `cart_not_found`, 409 `cart_completed`,
|
|
80
|
-
400 `validation_failed`.
|
|
81
|
-
- **SDK** — `giftCards.applyGiftCard(client, cartId, {code})`.
|
|
82
|
-
- **Components** — checkout gift-card field (show `last4` +
|
|
83
|
-
`amount_applied`; on 400 show one generic "code cannot be applied"
|
|
84
|
-
message — do NOT branch on failure reasons that the API deliberately
|
|
85
|
-
hides).
|
|
86
|
-
- **Settings** — cards are issued/disabled in the admin (gift-cards admin
|
|
87
|
-
surface); expiry + balance live on the card's ledger. Applying also syncs
|
|
88
|
-
the internal `pp_giftcard` payment session when a payment collection
|
|
89
|
-
already exists (creating one composes it — checkout.md).
|
|
90
|
-
|
|
91
|
-
```bash
|
|
92
|
-
# Executable error contract: the generic-oracle answer. (A happy-path apply
|
|
93
|
-
# needs an admin-issued card — shown in the jsonc above; proven end-to-end
|
|
94
|
-
# by tests/store/gift-cards.test.ts.)
|
|
95
|
-
STATUS=$(curl -s -o /dev/null -w '%{http_code}' -X POST \
|
|
96
|
-
"$BASE/api/store/carts/$CART_ID/gift-cards" \
|
|
97
|
-
-H "x-client-id: $CLIENT_ID" -H "content-type: application/json" \
|
|
98
|
-
-H "x-forwarded-for: 10.90.$((RANDOM % 250)).$((RANDOM % 250))" \
|
|
99
|
-
-d '{"code":"NOPE-NOPE-NOPE-NOPE"}')
|
|
100
|
-
test "$STATUS" = 400
|
|
101
|
-
RES=$(curl -s -X POST "$BASE/api/store/carts/$CART_ID/gift-cards" \
|
|
102
|
-
-H "x-client-id: $CLIENT_ID" -H "content-type: application/json" \
|
|
103
|
-
-H "x-forwarded-for: 10.90.$((RANDOM % 250)).$((RANDOM % 250))" \
|
|
104
|
-
-d '{"code":"ALSO-NOT-A-REAL-CODE"}')
|
|
105
|
-
echo "$RES" | grep -q '"code":"invalid_gift_card"'
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
---
|
|
109
|
-
|
|
110
|
-
## DELETE /api/store/carts/:id/gift-cards — remove an applied card
|
|
111
|
-
|
|
112
|
-
- **Purpose** — remove an applied card from an open cart (idempotent —
|
|
113
|
-
removing a never-applied id still returns the cart).
|
|
114
|
-
- **Auth** — anon `x-client-id`.
|
|
115
|
-
- **Request** — `{gift_card_id}` (`.strict()`; the id from
|
|
116
|
-
`cart.gift_cards[].id` or the apply confirmation).
|
|
117
|
-
- **Response** — `200 {cart}` (decoration recomputed; the `pp_giftcard`
|
|
118
|
-
session re-synced to the remaining tender).
|
|
119
|
-
- **Errors** — 404 `cart_not_found`, 409 `cart_completed`, 400
|
|
120
|
-
`validation_failed`.
|
|
121
|
-
- **SDK** — `giftCards.removeGiftCard(client, cartId, {gift_card_id})`.
|
|
122
|
-
- **Components** — checkout applied-cards list (remove chip).
|
|
123
|
-
|
|
124
|
-
```bash
|
|
125
|
-
# Idempotent remove is executable without an issued card.
|
|
126
|
-
curl -sf -X DELETE "$BASE/api/store/carts/$CART_ID/gift-cards" \
|
|
127
|
-
-H "x-client-id: $CLIENT_ID" -H "content-type: application/json" \
|
|
128
|
-
-d '{"gift_card_id":"gift_never_applied"}' | grep -q '"gift_card_total":0'
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
---
|
|
132
|
-
|
|
133
|
-
## What happens at complete (contract summary)
|
|
134
|
-
|
|
135
|
-
Redemption is atomic at `POST /carts/:id/complete` — idempotent per
|
|
136
|
-
(card, order); two carts racing one balance: exactly one completes, the
|
|
137
|
-
loser gets `402 gift_card_insufficient_balance` and its order is
|
|
138
|
-
compensated away. A remainder-payment failure reverses this order's
|
|
139
|
-
redemptions back to the cards before rethrowing. A gift session that no
|
|
140
|
-
longer covers its amount (card disabled/drained since apply) →
|
|
141
|
-
`402 payment_incomplete` / `402 gift_card_not_redeemable`, nothing burned.
|
|
142
|
-
Zero-remainder carts complete on the gift session alone — no provider
|
|
143
|
-
involved. Refunds/cancels always reverse tender **to the card**, never to
|
|
144
|
-
a bank. Buying gift-card products (digital issue-on-purchase, physical
|
|
145
|
-
activate-at-packing) is a catalog concern — see the product docs and the
|
|
146
|
-
gift-cards admin surface.
|
|
147
|
-
|
|
148
|
-
## Cleanup / accretion note
|
|
149
|
-
|
|
150
|
-
This page creates carts only (no store-facing delete endpoint exists —
|
|
151
|
-
same inert accretion as the suite's own cart tests) and applies no real
|
|
152
|
-
cards. Invalid-code attempts land in the rate-limit ledger under a unique
|
|
153
|
-
random IP per run, far below the per-IP window.
|
|
1
|
+
# Gift cards
|
|
2
|
+
|
|
3
|
+
Gift-card redemption is a **payment tender** (`pp_giftcard`), never a
|
|
4
|
+
discount: cart totals and VAT compute first and **never move**; applied
|
|
5
|
+
cards cover part (or all) of `cart.total` and the remainder provider
|
|
6
|
+
(Stripe/COD/manual) charges only what is left. Balances derive from an
|
|
7
|
+
append-only transactions ledger — resolved LIVE on every cart read, so a
|
|
8
|
+
balance spent elsewhere shrinks this cart's tender instead of
|
|
9
|
+
over-redeeming. Actual redemption happens ONLY at cart complete, atomically.
|
|
10
|
+
|
|
11
|
+
**Auth for every endpoint on this page:** anon `x-client-id`.
|
|
12
|
+
|
|
13
|
+
SDK module: `@cartbase/storefront/api/gift-cards`. Zero-remainder checkout
|
|
14
|
+
(cards cover everything) is part of the Buy-click sequence — see
|
|
15
|
+
[checkout.md](checkout.md).
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Cart decoration (every cart read)
|
|
20
|
+
|
|
21
|
+
Every cart response carries three tender fields (declared on the `Cart` DTO
|
|
22
|
+
in `@cartbase/storefront/api/carts`; see [carts.md](carts.md) for the full
|
|
23
|
+
cart shape):
|
|
24
|
+
|
|
25
|
+
```jsonc
|
|
26
|
+
{
|
|
27
|
+
"cart": {
|
|
28
|
+
// …totals (NEVER moved by gift cards)…
|
|
29
|
+
"total": 54,
|
|
30
|
+
"gift_cards": [ // apply order; each covers min(live balance, remaining total)
|
|
31
|
+
{ "id": "gift_…", "last4": "PQRS", "amount": 25 }
|
|
32
|
+
// a disabled/expired/depleted card stays listed at amount: 0
|
|
33
|
+
],
|
|
34
|
+
"gift_card_total": 25, // Σ coverage = the pp_giftcard session amount
|
|
35
|
+
"gift_card_remainder": 29 // max(total − gift_card_total, 0) — what the remainder provider charges
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# The decoration fields exist on every cart read (empty state shown here).
|
|
42
|
+
CART_JSON=$(curl -sf -X POST "$BASE/api/store/carts" \
|
|
43
|
+
-H "x-client-id: $CLIENT_ID" -H "content-type: application/json" \
|
|
44
|
+
-d '{"email":"gc-doc-'"$RUN"'@example.test",
|
|
45
|
+
"items":[{"variant_id":"variant_01tst000000000000000002","quantity":1}]}')
|
|
46
|
+
CART_ID=$(echo "$CART_JSON" | grep -o '"id":"cart_[^"]*"' | head -1 | cut -d'"' -f4)
|
|
47
|
+
echo "$CART_JSON" | grep -q '"gift_cards":\[\]'
|
|
48
|
+
echo "$CART_JSON" | grep -q '"gift_card_total":0'
|
|
49
|
+
echo "$CART_JSON" | grep -q '"gift_card_remainder"'
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## POST /api/store/carts/:id/gift-cards — apply a code
|
|
55
|
+
|
|
56
|
+
- **Purpose** — apply a gift-card code to an open cart (idempotent
|
|
57
|
+
re-apply).
|
|
58
|
+
- **Auth** — anon `x-client-id`.
|
|
59
|
+
- **Request** — `{code}` (`.strict()`, 4–64 chars). The code travels ONCE,
|
|
60
|
+
here; it is hashed at rest and never echoed back.
|
|
61
|
+
- **Response** — `200 {cart, gift_card}` — a MASKED confirmation:
|
|
62
|
+
|
|
63
|
+
```jsonc
|
|
64
|
+
{
|
|
65
|
+
"cart": { "…": "decorated cart — gift_cards/gift_card_total/gift_card_remainder updated, total untouched" },
|
|
66
|
+
"gift_card": {
|
|
67
|
+
"id": "gift_…",
|
|
68
|
+
"last4": "PQRS", // last 4 chars of the code — never the code
|
|
69
|
+
"amount_applied": 25 // how much of THIS cart this card covers right now
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- **Errors** — deliberately a single generic answer, **no code-existence
|
|
75
|
+
oracle**: unknown, disabled, expired, depleted and foreign-tenant codes
|
|
76
|
+
are all `400 {code: "invalid_gift_card"}`. Attempts are recorded BEFORE
|
|
77
|
+
lookup and rate-limited per cart (10 / 15 min) AND per IP (30 / 15 min) →
|
|
78
|
+
`429 {code: "rate_limited"}` — a valid code inside a burned window is
|
|
79
|
+
refused the same way. Also 404 `cart_not_found`, 409 `cart_completed`,
|
|
80
|
+
400 `validation_failed`.
|
|
81
|
+
- **SDK** — `giftCards.applyGiftCard(client, cartId, {code})`.
|
|
82
|
+
- **Components** — checkout gift-card field (show `last4` +
|
|
83
|
+
`amount_applied`; on 400 show one generic "code cannot be applied"
|
|
84
|
+
message — do NOT branch on failure reasons that the API deliberately
|
|
85
|
+
hides).
|
|
86
|
+
- **Settings** — cards are issued/disabled in the admin (gift-cards admin
|
|
87
|
+
surface); expiry + balance live on the card's ledger. Applying also syncs
|
|
88
|
+
the internal `pp_giftcard` payment session when a payment collection
|
|
89
|
+
already exists (creating one composes it — checkout.md).
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Executable error contract: the generic-oracle answer. (A happy-path apply
|
|
93
|
+
# needs an admin-issued card — shown in the jsonc above; proven end-to-end
|
|
94
|
+
# by tests/store/gift-cards.test.ts.)
|
|
95
|
+
STATUS=$(curl -s -o /dev/null -w '%{http_code}' -X POST \
|
|
96
|
+
"$BASE/api/store/carts/$CART_ID/gift-cards" \
|
|
97
|
+
-H "x-client-id: $CLIENT_ID" -H "content-type: application/json" \
|
|
98
|
+
-H "x-forwarded-for: 10.90.$((RANDOM % 250)).$((RANDOM % 250))" \
|
|
99
|
+
-d '{"code":"NOPE-NOPE-NOPE-NOPE"}')
|
|
100
|
+
test "$STATUS" = 400
|
|
101
|
+
RES=$(curl -s -X POST "$BASE/api/store/carts/$CART_ID/gift-cards" \
|
|
102
|
+
-H "x-client-id: $CLIENT_ID" -H "content-type: application/json" \
|
|
103
|
+
-H "x-forwarded-for: 10.90.$((RANDOM % 250)).$((RANDOM % 250))" \
|
|
104
|
+
-d '{"code":"ALSO-NOT-A-REAL-CODE"}')
|
|
105
|
+
echo "$RES" | grep -q '"code":"invalid_gift_card"'
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## DELETE /api/store/carts/:id/gift-cards — remove an applied card
|
|
111
|
+
|
|
112
|
+
- **Purpose** — remove an applied card from an open cart (idempotent —
|
|
113
|
+
removing a never-applied id still returns the cart).
|
|
114
|
+
- **Auth** — anon `x-client-id`.
|
|
115
|
+
- **Request** — `{gift_card_id}` (`.strict()`; the id from
|
|
116
|
+
`cart.gift_cards[].id` or the apply confirmation).
|
|
117
|
+
- **Response** — `200 {cart}` (decoration recomputed; the `pp_giftcard`
|
|
118
|
+
session re-synced to the remaining tender).
|
|
119
|
+
- **Errors** — 404 `cart_not_found`, 409 `cart_completed`, 400
|
|
120
|
+
`validation_failed`.
|
|
121
|
+
- **SDK** — `giftCards.removeGiftCard(client, cartId, {gift_card_id})`.
|
|
122
|
+
- **Components** — checkout applied-cards list (remove chip).
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# Idempotent remove is executable without an issued card.
|
|
126
|
+
curl -sf -X DELETE "$BASE/api/store/carts/$CART_ID/gift-cards" \
|
|
127
|
+
-H "x-client-id: $CLIENT_ID" -H "content-type: application/json" \
|
|
128
|
+
-d '{"gift_card_id":"gift_never_applied"}' | grep -q '"gift_card_total":0'
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## What happens at complete (contract summary)
|
|
134
|
+
|
|
135
|
+
Redemption is atomic at `POST /carts/:id/complete` — idempotent per
|
|
136
|
+
(card, order); two carts racing one balance: exactly one completes, the
|
|
137
|
+
loser gets `402 gift_card_insufficient_balance` and its order is
|
|
138
|
+
compensated away. A remainder-payment failure reverses this order's
|
|
139
|
+
redemptions back to the cards before rethrowing. A gift session that no
|
|
140
|
+
longer covers its amount (card disabled/drained since apply) →
|
|
141
|
+
`402 payment_incomplete` / `402 gift_card_not_redeemable`, nothing burned.
|
|
142
|
+
Zero-remainder carts complete on the gift session alone — no provider
|
|
143
|
+
involved. Refunds/cancels always reverse tender **to the card**, never to
|
|
144
|
+
a bank. Buying gift-card products (digital issue-on-purchase, physical
|
|
145
|
+
activate-at-packing) is a catalog concern — see the product docs and the
|
|
146
|
+
gift-cards admin surface.
|
|
147
|
+
|
|
148
|
+
## Cleanup / accretion note
|
|
149
|
+
|
|
150
|
+
This page creates carts only (no store-facing delete endpoint exists —
|
|
151
|
+
same inert accretion as the suite's own cart tests) and applies no real
|
|
152
|
+
cards. Invalid-code attempts land in the rate-limit ledger under a unique
|
|
153
|
+
random IP per run, far below the per-IP window.
|
|
@@ -1,126 +1,126 @@
|
|
|
1
|
-
# Metaobjects
|
|
2
|
-
|
|
3
|
-
Merchant-defined structured content (size charts, brand profiles, FAQ
|
|
4
|
-
blocks) served by TYPE + HANDLE (metaobjects card). **ACTIVE entries only —
|
|
5
|
-
drafts 404** (lib filter AND anon RLS, defense in depth).
|
|
6
|
-
|
|
7
|
-
Capabilities per definition:
|
|
8
|
-
- `renderable` → payload gains `seo {title, description}` (title falls back
|
|
9
|
-
to `displayName`, description to null);
|
|
10
|
-
- `online_store` → payload gains `url` (`/metaobjects/<type>/<handle>` —
|
|
11
|
-
the storefront prepends its origin).
|
|
12
|
-
|
|
13
|
-
References resolve at **read** time: a target the storefront cannot see
|
|
14
|
-
(deleted, or draft under anon RLS) yields `reference: null` — never
|
|
15
|
-
dropped, never a 500. `by-metafield` is a **reserved type slug** (the
|
|
16
|
-
static route shadows `/:type`; rejected at definition create).
|
|
17
|
-
|
|
18
|
-
Metaobject shape (all endpoints):
|
|
19
|
-
|
|
20
|
-
```jsonc
|
|
21
|
-
{
|
|
22
|
-
"id": "uuid",
|
|
23
|
-
"type": "size-chart",
|
|
24
|
-
"handle": "shirts-eu",
|
|
25
|
-
"displayName": "Shirts (EU)",
|
|
26
|
-
"fields": [
|
|
27
|
-
{ "key": "body", "kind": "rich_text", "value": "<table>…</table>" },
|
|
28
|
-
{ "key": "product", "kind": "product_reference", "value": "prod_…",
|
|
29
|
-
"reference": { "id": "prod_…", "title": "Linen Shirt", "handle": "linen-shirt" } }
|
|
30
|
-
],
|
|
31
|
-
"seo": { "title": "Shirts (EU)", "description": null }, // renderable only
|
|
32
|
-
"url": "/metaobjects/size-chart/shirts-eu", // online_store only
|
|
33
|
-
"updatedAt": "ISO-8601"
|
|
34
|
-
}
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
> Definitions + entries are admin-authored; the shared dev tenant seeds
|
|
38
|
-
> none, so the executable blocks prove the 404/400 contracts and the happy
|
|
39
|
-
> paths are pinned by `tests/store/metaobjects-store.test.ts` (admin-auth
|
|
40
|
-
> fixtures).
|
|
41
|
-
|
|
42
|
-
## GET /api/store/metaobjects/:type — list entries
|
|
43
|
-
|
|
44
|
-
- **Purpose**: list ACTIVE entries of one type (e.g. all FAQ blocks),
|
|
45
|
-
newest-updated first.
|
|
46
|
-
- **Auth**: anon (`x-client-id`).
|
|
47
|
-
- **Request**: query `{limit?, offset?}` — `limit` clamped to 1–100
|
|
48
|
-
(default 20; out-of-range values are clamped, not rejected).
|
|
49
|
-
- **Response**: `{metaobjects: [Metaobject…], count, offset, limit}` — list
|
|
50
|
-
payloads carry **raw** field values (`reference` NOT resolved; the
|
|
51
|
-
by-handle read is the resolve hot path). `seo`/`url` capabilities still
|
|
52
|
-
apply.
|
|
53
|
-
- **Errors**: `404 not_found` (unknown type).
|
|
54
|
-
- **SDK**: `metaobjects.listMetaobjects(client, type, query?)`
|
|
55
|
-
- **Components**: FAQ/content-block section renderers.
|
|
56
|
-
- **Settings**: definitions + entry status in admin → Content → Metaobjects.
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
|
|
60
|
-
"$BASE/api/store/metaobjects/doc-no-such-type-$RUN" -H "x-client-id: $CLIENT_ID")
|
|
61
|
-
test "$STATUS" = 404
|
|
62
|
-
curl -s "$BASE/api/store/metaobjects/doc-no-such-type-$RUN" -H "x-client-id: $CLIENT_ID" \
|
|
63
|
-
| grep -q '"code":"not_found"'
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
## GET /api/store/metaobjects/:type/:handle — one entry
|
|
67
|
-
|
|
68
|
-
- **Purpose**: render one entry — references RESOLVED (invisible targets →
|
|
69
|
-
`reference: null`).
|
|
70
|
-
- **Auth**: anon (`x-client-id`).
|
|
71
|
-
- **Response**: `{metaobject: Metaobject}` (shape above, references
|
|
72
|
-
resolved).
|
|
73
|
-
- **Errors**: `404 not_found` — unknown type, unknown handle, or a DRAFT
|
|
74
|
-
entry (drafts are indistinguishable from missing, by design).
|
|
75
|
-
- **SDK**: `metaobjects.getMetaobject(client, type, handle)`
|
|
76
|
-
- **Components**: size-chart modal, brand-profile block.
|
|
77
|
-
|
|
78
|
-
```bash
|
|
79
|
-
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
|
|
80
|
-
"$BASE/api/store/metaobjects/doc-no-such-type-$RUN/doc-h-$RUN" \
|
|
81
|
-
-H "x-client-id: $CLIENT_ID")
|
|
82
|
-
test "$STATUS" = 404
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
## GET /api/store/metaobjects/by-metafield — the product→chart chain
|
|
86
|
-
|
|
87
|
-
- **Purpose**: the canonical fetch chain — resolve an entity's
|
|
88
|
-
`metaobject_reference` METAFIELD into full metaobject payload(s) in ONE
|
|
89
|
-
call: storefront reads the product (id in hand) → this call with the
|
|
90
|
-
metafield key → the resolved chart.
|
|
91
|
-
- **Auth**: anon (`x-client-id`). Leak boundary (deliberate): only
|
|
92
|
-
`metaobject_reference` definitions resolve here — any other metafield key
|
|
93
|
-
404s, so no other metafield value can exit through this route; entries
|
|
94
|
-
return ACTIVE-only.
|
|
95
|
-
- **Request**: query `{entity_type, entity_id, key}` — all three required.
|
|
96
|
-
- **Response**: `metaobjects` always present, in stored order. `metaobject`
|
|
97
|
-
(first entry or null) is present **only for single-valued definitions** —
|
|
98
|
-
LIST definitions return `{metaobjects}` alone:
|
|
99
|
-
|
|
100
|
-
```jsonc
|
|
101
|
-
// single-valued definition
|
|
102
|
-
{ "metaobject": { /* Metaobject */ }, "metaobjects": [ /* [it] */ ] }
|
|
103
|
-
// list definition
|
|
104
|
-
{ "metaobjects": [ /* Metaobject[] in stored order */ ] }
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
- **Errors**: `400 invalid_data` (missing entity_type/entity_id/key) ·
|
|
108
|
-
`404 not_found` (no such definition, wrong field_type, or no stored
|
|
109
|
-
value for the entity).
|
|
110
|
-
- **SDK**: `metaobjects.getMetaobjectsByMetafield(client, query)`
|
|
111
|
-
- **Components**: PDP size-chart trigger; any metafield-driven block.
|
|
112
|
-
- **Settings**: the metafield definition (entity type + key +
|
|
113
|
-
`metaobject_reference` field type) in admin → Settings → Metafields.
|
|
114
|
-
|
|
115
|
-
```bash
|
|
116
|
-
# Missing params → 400 invalid_data.
|
|
117
|
-
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
|
|
118
|
-
"$BASE/api/store/metaobjects/by-metafield?entity_type=product" \
|
|
119
|
-
-H "x-client-id: $CLIENT_ID")
|
|
120
|
-
test "$STATUS" = 400
|
|
121
|
-
# Unknown key on a real seeded product → 404 (the leak-boundary contract).
|
|
122
|
-
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
|
|
123
|
-
"$BASE/api/store/metaobjects/by-metafield?entity_type=product&entity_id=prod_01tst00000000000000000001&key=doc-nokey-$RUN" \
|
|
124
|
-
-H "x-client-id: $CLIENT_ID")
|
|
125
|
-
test "$STATUS" = 404
|
|
126
|
-
```
|
|
1
|
+
# Metaobjects
|
|
2
|
+
|
|
3
|
+
Merchant-defined structured content (size charts, brand profiles, FAQ
|
|
4
|
+
blocks) served by TYPE + HANDLE (metaobjects card). **ACTIVE entries only —
|
|
5
|
+
drafts 404** (lib filter AND anon RLS, defense in depth).
|
|
6
|
+
|
|
7
|
+
Capabilities per definition:
|
|
8
|
+
- `renderable` → payload gains `seo {title, description}` (title falls back
|
|
9
|
+
to `displayName`, description to null);
|
|
10
|
+
- `online_store` → payload gains `url` (`/metaobjects/<type>/<handle>` —
|
|
11
|
+
the storefront prepends its origin).
|
|
12
|
+
|
|
13
|
+
References resolve at **read** time: a target the storefront cannot see
|
|
14
|
+
(deleted, or draft under anon RLS) yields `reference: null` — never
|
|
15
|
+
dropped, never a 500. `by-metafield` is a **reserved type slug** (the
|
|
16
|
+
static route shadows `/:type`; rejected at definition create).
|
|
17
|
+
|
|
18
|
+
Metaobject shape (all endpoints):
|
|
19
|
+
|
|
20
|
+
```jsonc
|
|
21
|
+
{
|
|
22
|
+
"id": "uuid",
|
|
23
|
+
"type": "size-chart",
|
|
24
|
+
"handle": "shirts-eu",
|
|
25
|
+
"displayName": "Shirts (EU)",
|
|
26
|
+
"fields": [
|
|
27
|
+
{ "key": "body", "kind": "rich_text", "value": "<table>…</table>" },
|
|
28
|
+
{ "key": "product", "kind": "product_reference", "value": "prod_…",
|
|
29
|
+
"reference": { "id": "prod_…", "title": "Linen Shirt", "handle": "linen-shirt" } }
|
|
30
|
+
],
|
|
31
|
+
"seo": { "title": "Shirts (EU)", "description": null }, // renderable only
|
|
32
|
+
"url": "/metaobjects/size-chart/shirts-eu", // online_store only
|
|
33
|
+
"updatedAt": "ISO-8601"
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
> Definitions + entries are admin-authored; the shared dev tenant seeds
|
|
38
|
+
> none, so the executable blocks prove the 404/400 contracts and the happy
|
|
39
|
+
> paths are pinned by `tests/store/metaobjects-store.test.ts` (admin-auth
|
|
40
|
+
> fixtures).
|
|
41
|
+
|
|
42
|
+
## GET /api/store/metaobjects/:type — list entries
|
|
43
|
+
|
|
44
|
+
- **Purpose**: list ACTIVE entries of one type (e.g. all FAQ blocks),
|
|
45
|
+
newest-updated first.
|
|
46
|
+
- **Auth**: anon (`x-client-id`).
|
|
47
|
+
- **Request**: query `{limit?, offset?}` — `limit` clamped to 1–100
|
|
48
|
+
(default 20; out-of-range values are clamped, not rejected).
|
|
49
|
+
- **Response**: `{metaobjects: [Metaobject…], count, offset, limit}` — list
|
|
50
|
+
payloads carry **raw** field values (`reference` NOT resolved; the
|
|
51
|
+
by-handle read is the resolve hot path). `seo`/`url` capabilities still
|
|
52
|
+
apply.
|
|
53
|
+
- **Errors**: `404 not_found` (unknown type).
|
|
54
|
+
- **SDK**: `metaobjects.listMetaobjects(client, type, query?)`
|
|
55
|
+
- **Components**: FAQ/content-block section renderers.
|
|
56
|
+
- **Settings**: definitions + entry status in admin → Content → Metaobjects.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
|
|
60
|
+
"$BASE/api/store/metaobjects/doc-no-such-type-$RUN" -H "x-client-id: $CLIENT_ID")
|
|
61
|
+
test "$STATUS" = 404
|
|
62
|
+
curl -s "$BASE/api/store/metaobjects/doc-no-such-type-$RUN" -H "x-client-id: $CLIENT_ID" \
|
|
63
|
+
| grep -q '"code":"not_found"'
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## GET /api/store/metaobjects/:type/:handle — one entry
|
|
67
|
+
|
|
68
|
+
- **Purpose**: render one entry — references RESOLVED (invisible targets →
|
|
69
|
+
`reference: null`).
|
|
70
|
+
- **Auth**: anon (`x-client-id`).
|
|
71
|
+
- **Response**: `{metaobject: Metaobject}` (shape above, references
|
|
72
|
+
resolved).
|
|
73
|
+
- **Errors**: `404 not_found` — unknown type, unknown handle, or a DRAFT
|
|
74
|
+
entry (drafts are indistinguishable from missing, by design).
|
|
75
|
+
- **SDK**: `metaobjects.getMetaobject(client, type, handle)`
|
|
76
|
+
- **Components**: size-chart modal, brand-profile block.
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
|
|
80
|
+
"$BASE/api/store/metaobjects/doc-no-such-type-$RUN/doc-h-$RUN" \
|
|
81
|
+
-H "x-client-id: $CLIENT_ID")
|
|
82
|
+
test "$STATUS" = 404
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## GET /api/store/metaobjects/by-metafield — the product→chart chain
|
|
86
|
+
|
|
87
|
+
- **Purpose**: the canonical fetch chain — resolve an entity's
|
|
88
|
+
`metaobject_reference` METAFIELD into full metaobject payload(s) in ONE
|
|
89
|
+
call: storefront reads the product (id in hand) → this call with the
|
|
90
|
+
metafield key → the resolved chart.
|
|
91
|
+
- **Auth**: anon (`x-client-id`). Leak boundary (deliberate): only
|
|
92
|
+
`metaobject_reference` definitions resolve here — any other metafield key
|
|
93
|
+
404s, so no other metafield value can exit through this route; entries
|
|
94
|
+
return ACTIVE-only.
|
|
95
|
+
- **Request**: query `{entity_type, entity_id, key}` — all three required.
|
|
96
|
+
- **Response**: `metaobjects` always present, in stored order. `metaobject`
|
|
97
|
+
(first entry or null) is present **only for single-valued definitions** —
|
|
98
|
+
LIST definitions return `{metaobjects}` alone:
|
|
99
|
+
|
|
100
|
+
```jsonc
|
|
101
|
+
// single-valued definition
|
|
102
|
+
{ "metaobject": { /* Metaobject */ }, "metaobjects": [ /* [it] */ ] }
|
|
103
|
+
// list definition
|
|
104
|
+
{ "metaobjects": [ /* Metaobject[] in stored order */ ] }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- **Errors**: `400 invalid_data` (missing entity_type/entity_id/key) ·
|
|
108
|
+
`404 not_found` (no such definition, wrong field_type, or no stored
|
|
109
|
+
value for the entity).
|
|
110
|
+
- **SDK**: `metaobjects.getMetaobjectsByMetafield(client, query)`
|
|
111
|
+
- **Components**: PDP size-chart trigger; any metafield-driven block.
|
|
112
|
+
- **Settings**: the metafield definition (entity type + key +
|
|
113
|
+
`metaobject_reference` field type) in admin → Settings → Metafields.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# Missing params → 400 invalid_data.
|
|
117
|
+
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
|
|
118
|
+
"$BASE/api/store/metaobjects/by-metafield?entity_type=product" \
|
|
119
|
+
-H "x-client-id: $CLIENT_ID")
|
|
120
|
+
test "$STATUS" = 400
|
|
121
|
+
# Unknown key on a real seeded product → 404 (the leak-boundary contract).
|
|
122
|
+
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
|
|
123
|
+
"$BASE/api/store/metaobjects/by-metafield?entity_type=product&entity_id=prod_01tst00000000000000000001&key=doc-nokey-$RUN" \
|
|
124
|
+
-H "x-client-id: $CLIENT_ID")
|
|
125
|
+
test "$STATUS" = 404
|
|
126
|
+
```
|